Skip to content

Commit 8fffc2d

Browse files
committed
feat: Implement Ideation Sandbox offline sync (#9695)
- Ignore resources/content inside .npmignore to prevent package bloat - Create DiscussionSyncer.mjs to generate local Markdown files from Discussions - Add recursive FETCH_DISCUSSIONS_FOR_SYNC GraphQL queries - Wire discussion synchronization loop into SyncService
1 parent e7e1649 commit 8fffc2d

9 files changed

Lines changed: 746 additions & 16 deletions

File tree

.npmignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# Additional rules, which are solely npm related, to not bloat the package size.
22
apps/devindex/resources/*.json
3-
resources/content/issues
4-
resources/content/issue-archive
5-
resources/content/release-notes
6-
resources/content/.sync-metadata.json
3+
resources/content/
74

85
# Original content of the .gitignore file
96
# See http://help.github.com/ignore-files/ for more about ignoring files.

ai/mcp/server/github-workflow/config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ const defaultConfig = {
7070
* @type {string}
7171
*/
7272
archiveDir: path.resolve(projectRoot, 'resources/content/issue-archive'),
73+
/**
74+
* The path to the directory for discussions.
75+
* @type {string}
76+
*/
77+
discussionsDir: path.resolve(projectRoot, 'resources/content/discussions'),
7378
/**
7479
* The path to the synchronization metadata file.
7580
* @type {string}

ai/mcp/server/github-workflow/openapi.yaml

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,122 @@ paths:
676676
schema:
677677
$ref: '#/components/schemas/ErrorResponse'
678678

679+
/discussions:
680+
post:
681+
summary: Create a new GitHub Discussion
682+
operationId: create_discussion
683+
x-pass-as-object: true
684+
x-annotations:
685+
readOnlyHint: false
686+
description: |
687+
Creates a new discussion on GitHub. This is used for the Ideation Sandbox, keeping brainstorming separate from issues.
688+
689+
**MANDATORY WORKFLOW:**
690+
- Always provide a clear title and body.
691+
- Categories are mapped internally; default is 'Ideas'. If another categorical structure is needed, specify it.
692+
tags: [Issues]
693+
requestBody:
694+
required: true
695+
content:
696+
application/json:
697+
schema:
698+
type: object
699+
required:
700+
- title
701+
- body
702+
properties:
703+
title:
704+
type: string
705+
description: The title of the discussion.
706+
body:
707+
type: string
708+
description: The Markdown body of the discussion.
709+
category:
710+
type: string
711+
description: The name of the category (e.g., 'Ideas', 'Q&A'). Defaults to 'Ideas'.
712+
default: "Ideas"
713+
responses:
714+
'201':
715+
description: Discussion created successfully.
716+
content:
717+
application/json:
718+
schema:
719+
$ref: '#/components/schemas/CreateIssueResponse'
720+
'500':
721+
description: Internal server error.
722+
content:
723+
application/json:
724+
schema:
725+
$ref: '#/components/schemas/ErrorResponse'
726+
727+
/discussions/comments/manage:
728+
post:
729+
summary: Manage Discussion Comments (Create or Update)
730+
operationId: manage_discussion_comment
731+
x-pass-as-object: true
732+
x-annotations:
733+
readOnlyHint: false
734+
description: |
735+
Unified tool for creating and updating comments on GitHub discussions.
736+
737+
**Action: 'create'**
738+
- Creates a new comment on a discussion.
739+
- Requires `agent`, `body`, and `discussion_number`.
740+
741+
**Action: 'update'**
742+
- Updates an existing discussion comment.
743+
- Requires `comment_id` and `body`.
744+
tags: [Issues]
745+
requestBody:
746+
required: true
747+
content:
748+
application/json:
749+
schema:
750+
type: object
751+
required:
752+
- action
753+
- body
754+
properties:
755+
action:
756+
type: string
757+
enum: [create, update]
758+
description: The action to perform.
759+
example: create
760+
discussion_number:
761+
type: integer
762+
description: The number of the discussion (required for 'create').
763+
example: 456
764+
comment_id:
765+
type: string
766+
description: The global node ID of the comment (required for 'update').
767+
example: "DC_kwDOABcD1234567890"
768+
body:
769+
type: string
770+
description: The content of the comment.
771+
agent:
772+
type: string
773+
description: The agent identity in the format '[Model Name] ([Agent Wrapper])' (required for 'create').
774+
example: "Gemini 3.1 Pro (Antigravity)"
775+
responses:
776+
'200':
777+
description: Comment created or updated successfully.
778+
content:
779+
application/json:
780+
schema:
781+
$ref: '#/components/schemas/SuccessResponse'
782+
'400':
783+
description: Bad Request.
784+
content:
785+
application/json:
786+
schema:
787+
$ref: '#/components/schemas/ErrorResponse'
788+
'500':
789+
description: Internal server error.
790+
content:
791+
application/json:
792+
schema:
793+
$ref: '#/components/schemas/ErrorResponse'
794+
679795
/issues/relationship:
680796
post:
681797
summary: Manage Issue Relationships (Parent-Child and Blocked-By)
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import aiConfig from '../config.mjs';
2+
import Base from '../../../../../src/core/Base.mjs';
3+
import GraphqlService from './GraphqlService.mjs';
4+
import RepositoryService from './RepositoryService.mjs';
5+
import logger from '../logger.mjs';
6+
import {GET_REPO_AND_DISCUSSION_CATEGORIES, GET_DISCUSSION_ID} from './queries/discussionQueries.mjs';
7+
import {CREATE_DISCUSSION, ADD_DISCUSSION_COMMENT, UPDATE_DISCUSSION_COMMENT} from './queries/mutations.mjs';
8+
9+
const AGENT_ICONS = {
10+
gemini : '✦',
11+
claude : '❋',
12+
gpt : '●',
13+
default: '◆'
14+
};
15+
16+
/**
17+
* @summary Service for interacting with GitHub Discussions via the GraphQL API.
18+
*
19+
* This service provides a high-level abstraction for managing GitHub discussions.
20+
* Capabilities include:
21+
* - Creating discussions inside specific categories (default 'Ideas')
22+
* - Managing discussion comments
23+
*
24+
* @class Neo.ai.mcp.server.github-workflow.services.DiscussionService
25+
* @extends Neo.core.Base
26+
* @singleton
27+
*/
28+
class DiscussionService extends Base {
29+
static config = {
30+
/**
31+
* @member {String} className='Neo.ai.mcp.server.github-workflow.services.DiscussionService'
32+
* @protected
33+
*/
34+
className: 'Neo.ai.mcp.server.github-workflow.services.DiscussionService',
35+
/**
36+
* @member {Boolean} singleton=true
37+
* @protected
38+
*/
39+
singleton: true,
40+
/**
41+
* @member {String[]} writePermissions=['ADMIN', 'MAINTAIN', 'WRITE', 'READ']
42+
* @protected
43+
*/
44+
writePermissions: ['ADMIN', 'MAINTAIN', 'WRITE', 'READ'] // Discussions are typically accessible across more roles, but keeping standard
45+
}
46+
47+
/**
48+
* Creates a new GitHub Discussion.
49+
* @param {object} options The options for creating the discussion.
50+
* @param {string} options.title The title of the discussion.
51+
* @param {string} options.body The Markdown body of the discussion.
52+
* @param {string} options.category The name of the category (e.g., 'Ideas', 'Q&A'). Defaults to 'Ideas'.
53+
* @returns {Promise<object>} A promise that resolves to the new discussion data.
54+
*/
55+
async createDiscussion({title, body, category = 'Ideas'}) {
56+
logger.info(`Attempting to create GitHub Discussion: "${title}" in category "${category}"`);
57+
58+
try {
59+
// First, get the repository ID and discussion categories
60+
const repoData = await GraphqlService.query(GET_REPO_AND_DISCUSSION_CATEGORIES, {
61+
owner: aiConfig.owner,
62+
repo: aiConfig.repo
63+
});
64+
65+
const repositoryId = repoData.repository.id;
66+
const categories = repoData.repository.discussionCategories.nodes;
67+
68+
// Find the ID for the requested category name
69+
const categoryNode = categories.find(cat => cat.name.toLowerCase() === category.toLowerCase());
70+
71+
if (!categoryNode) {
72+
const available = categories.map(cat => cat.name).join(', ');
73+
return {
74+
error : 'Category Not Found',
75+
message: `Discussion category '${category}' does not exist. Available categories: ${available}`,
76+
code : 'INVALID_CATEGORY'
77+
};
78+
}
79+
80+
const categoryId = categoryNode.id;
81+
82+
// Create the discussion
83+
const result = await GraphqlService.query(CREATE_DISCUSSION, {
84+
repositoryId,
85+
categoryId,
86+
title,
87+
body
88+
});
89+
90+
const discussion = result.createDiscussion.discussion;
91+
92+
logger.info(`Successfully created GitHub Discussion #${discussion.number}: ${discussion.url}`);
93+
94+
return {
95+
discussionNumber: discussion.number,
96+
url: discussion.url,
97+
id: discussion.id
98+
};
99+
100+
} catch (error) {
101+
logger.error('Error creating GitHub Discussion:', error);
102+
return {
103+
error : 'GraphQL API request failed',
104+
message: error.message,
105+
code : 'GRAPHQL_API_ERROR'
106+
};
107+
}
108+
}
109+
110+
/**
111+
* Extracts the agent type from the agent string for icon selection.
112+
* @param {string} agent The full agent identifier
113+
* @returns {string} The agent type key for AGENT_ICONS lookup
114+
*/
115+
getAgentType(agent) {
116+
const agentLower = agent.toLowerCase();
117+
118+
if (agentLower.includes('gemini')) return 'gemini';
119+
if (agentLower.includes('claude')) return 'claude';
120+
if (agentLower.includes('gpt')) return 'gpt';
121+
122+
return 'default';
123+
}
124+
125+
/**
126+
* Creates a comment on a specific discussion.
127+
* @param {object} options The options object
128+
* @param {number} options.discussion_number The number of the discussion.
129+
* @param {string} options.body The raw content of the comment.
130+
* @param {string} options.agent The identity of the calling agent.
131+
* @returns {Promise<object>} A promise that resolves to a success message.
132+
*/
133+
async createComment({discussion_number, body, agent}) {
134+
// Agent Header Formatting
135+
const header = `**Input from ${agent}:**\n\n`;
136+
const agentIcon = AGENT_ICONS[this.getAgentType(agent)];
137+
const headingMatch = body.match(/^(#+\s*)(.*)$/);
138+
let processedBody;
139+
140+
if (headingMatch) {
141+
const headingMarkers = headingMatch[1];
142+
const headingContent = headingMatch[2];
143+
processedBody = `${headingMarkers}${agentIcon} ${headingContent}\n${body.substring(headingMatch[0].length)}`;
144+
} else {
145+
processedBody = `${agentIcon} ${body}`;
146+
}
147+
148+
const finalBody = `${header}${processedBody.split('\n').map(line => `> ${line}`).join('\n')}`;
149+
150+
try {
151+
// Get Discussion subjectId
152+
const idData = await GraphqlService.query(GET_DISCUSSION_ID, {
153+
owner : aiConfig.owner,
154+
repo : aiConfig.repo,
155+
number: discussion_number
156+
});
157+
158+
if (!idData.repository.discussion) {
159+
return {
160+
error : 'Not Found',
161+
message: `Could not find discussion #${discussion_number}.`,
162+
code : 'NOT_FOUND'
163+
};
164+
}
165+
166+
const discussionId = idData.repository.discussion.id;
167+
168+
// Use ADD_DISCUSSION_COMMENT mutation
169+
await GraphqlService.query(ADD_DISCUSSION_COMMENT, { discussionId, body: finalBody });
170+
return { message: `Successfully created comment on discussion #${discussion_number}` };
171+
172+
} catch (error) {
173+
logger.error(`Error creating comment on discussion #${discussion_number} via GraphQL:`, error);
174+
return {
175+
error : 'GraphQL API request failed',
176+
message: error.message,
177+
code : 'GRAPHQL_API_ERROR'
178+
};
179+
}
180+
}
181+
182+
/**
183+
* Updates an existing comment on a discussion.
184+
* @param {string} comment_id The global node ID of the comment to update
185+
* @param {string} body The new body content for the comment
186+
* @returns {Promise<object>} A promise that resolves to a success message or a structured error.
187+
*/
188+
async updateComment(comment_id, body) {
189+
try {
190+
const result = await GraphqlService.query(UPDATE_DISCUSSION_COMMENT, {
191+
commentId: comment_id,
192+
body
193+
});
194+
195+
return {
196+
message : `Successfully updated discussion comment ${comment_id}`,
197+
commentId: result.updateDiscussionComment.comment.id,
198+
url : result.updateDiscussionComment.comment.url,
199+
updatedAt: result.updateDiscussionComment.comment.updatedAt
200+
};
201+
} catch (error) {
202+
logger.error(`Error updating discussion comment ${comment_id} via GraphQL:`, error);
203+
return {
204+
error : 'GraphQL API request failed',
205+
message: error.message,
206+
code : 'GRAPHQL_API_ERROR'
207+
};
208+
}
209+
}
210+
211+
/**
212+
* Consolidates comment management into a single method.
213+
* @param {object} options The options object
214+
* @param {number} [options.discussion_number] The number of the discussion (required for create).
215+
* @param {string} [options.comment_id] The global node ID of the comment (required for update).
216+
* @param {string} options.body The content of the comment.
217+
* @param {string} [options.agent] The identity of the calling agent (required for create).
218+
* @param {string} options.action The action to perform: 'create' or 'update'.
219+
* @returns {Promise<object>}
220+
*/
221+
async manageDiscussionComment({discussion_number, comment_id, body, agent, action}) {
222+
if (!['create', 'update'].includes(action)) {
223+
return {
224+
error: 'Bad Request',
225+
message: "Invalid action. Must be 'create' or 'update'.",
226+
code: 'INVALID_ARGUMENTS'
227+
};
228+
}
229+
230+
if (action === 'create') {
231+
if (!agent || !discussion_number) {
232+
return {
233+
error: 'Bad Request',
234+
message: "Missing required argument: 'agent' and 'discussion_number' are required for creating comments.",
235+
code: 'MISSING_ARGUMENTS'
236+
};
237+
}
238+
return this.createComment({discussion_number, body, agent});
239+
} else {
240+
if (!comment_id) {
241+
return {
242+
error: 'Bad Request',
243+
message: "Missing required argument: 'comment_id' is required for updating comments.",
244+
code: 'MISSING_ARGUMENTS'
245+
};
246+
}
247+
return this.updateComment(comment_id, body);
248+
}
249+
}
250+
}
251+
252+
export default Neo.setupClass(DiscussionService);

0 commit comments

Comments
 (0)