code-graph-context 2.14.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/core/embeddings/natural-language-to-cypher.service.js +2 -4
  2. package/dist/mcp/constants.js +56 -228
  3. package/dist/mcp/handlers/swarm/abandon.handler.js +61 -0
  4. package/dist/mcp/handlers/swarm/advance.handler.js +78 -0
  5. package/dist/mcp/handlers/swarm/claim.handler.js +61 -0
  6. package/dist/mcp/handlers/swarm/index.js +5 -0
  7. package/dist/mcp/handlers/swarm/queries.js +140 -0
  8. package/dist/mcp/handlers/swarm/release.handler.js +41 -0
  9. package/dist/mcp/handlers/swarm-worker.handler.js +2 -13
  10. package/dist/mcp/tools/detect-dead-code.tool.js +33 -65
  11. package/dist/mcp/tools/detect-duplicate-code.tool.js +44 -53
  12. package/dist/mcp/tools/impact-analysis.tool.js +1 -1
  13. package/dist/mcp/tools/index.js +9 -9
  14. package/dist/mcp/tools/list-projects.tool.js +2 -2
  15. package/dist/mcp/tools/list-watchers.tool.js +2 -5
  16. package/dist/mcp/tools/natural-language-to-cypher.tool.js +2 -2
  17. package/dist/mcp/tools/parse-typescript-project.tool.js +7 -17
  18. package/dist/mcp/tools/search-codebase.tool.js +11 -26
  19. package/dist/mcp/tools/session-bookmark.tool.js +7 -11
  20. package/dist/mcp/tools/session-cleanup.tool.js +2 -6
  21. package/dist/mcp/tools/session-note.tool.js +6 -21
  22. package/dist/mcp/tools/session-recall.tool.js +293 -0
  23. package/dist/mcp/tools/session-save.tool.js +280 -0
  24. package/dist/mcp/tools/start-watch-project.tool.js +1 -1
  25. package/dist/mcp/tools/swarm-advance-task.tool.js +56 -0
  26. package/dist/mcp/tools/swarm-claim-task.tool.js +24 -388
  27. package/dist/mcp/tools/swarm-cleanup.tool.js +3 -7
  28. package/dist/mcp/tools/swarm-complete-task.tool.js +14 -17
  29. package/dist/mcp/tools/swarm-get-tasks.tool.js +8 -26
  30. package/dist/mcp/tools/swarm-message.tool.js +10 -25
  31. package/dist/mcp/tools/swarm-pheromone.tool.js +7 -25
  32. package/dist/mcp/tools/swarm-post-task.tool.js +7 -19
  33. package/dist/mcp/tools/swarm-release-task.tool.js +53 -0
  34. package/dist/mcp/tools/swarm-sense.tool.js +10 -30
  35. package/dist/mcp/tools/traverse-from-node.tool.js +19 -41
  36. package/dist/mcp/utils.js +41 -1
  37. package/package.json +1 -1
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Session Save Tool
3
+ * Unified tool that merges save_session_bookmark and save_session_note into one call.
4
+ * Auto-detects bookmark vs note based on input fields provided.
5
+ */
6
+ import { z } from 'zod';
7
+ import { EmbeddingsService, getEmbeddingDimensions } from '../../core/embeddings/embeddings.service.js';
8
+ import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
9
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
10
+ import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
11
+ // ---------------------------------------------------------------------------
12
+ // Cypher queries (copied from their respective source tools)
13
+ // ---------------------------------------------------------------------------
14
+ const CREATE_BOOKMARK_QUERY = `
15
+ CREATE (b:SessionBookmark {
16
+ id: $bookmarkId,
17
+ projectId: $projectId,
18
+ sessionId: $sessionId,
19
+ agentId: $agentId,
20
+ summary: $summary,
21
+ workingSetNodeIds: $workingSetNodeIds,
22
+ taskContext: $taskContext,
23
+ findings: $findings,
24
+ nextSteps: $nextSteps,
25
+ metadata: $metadata,
26
+ createdAt: timestamp(),
27
+ updatedAt: timestamp()
28
+ })
29
+
30
+ // Link to referenced code nodes (exclude coordination nodes)
31
+ WITH b
32
+ OPTIONAL MATCH (target)
33
+ WHERE target.id IN $workingSetNodeIds
34
+ AND target.projectId = $projectId
35
+ AND NOT target:Pheromone
36
+ AND NOT target:SwarmTask
37
+ AND NOT target:SessionBookmark
38
+ AND NOT target:SessionNote
39
+ WITH b, collect(DISTINCT target) AS targets
40
+ FOREACH (t IN targets | MERGE (b)-[:REFERENCES]->(t))
41
+
42
+ RETURN b.id AS id,
43
+ b.sessionId AS sessionId,
44
+ b.agentId AS agentId,
45
+ b.summary AS summary,
46
+ b.taskContext AS taskContext,
47
+ b.createdAt AS createdAt,
48
+ size(targets) AS linkedNodes
49
+ `;
50
+ const CREATE_SESSION_NOTE_QUERY = `
51
+ // Create the SessionNote node
52
+ CREATE (n:SessionNote {
53
+ id: $noteId,
54
+ projectId: $projectId,
55
+ sessionId: $sessionId,
56
+ agentId: $agentId,
57
+ topic: $topic,
58
+ content: $content,
59
+ category: $category,
60
+ severity: $severity,
61
+ createdAt: timestamp(),
62
+ expiresAt: $expiresAt
63
+ })
64
+
65
+ // Link to referenced code nodes (filter out internal coordination nodes)
66
+ WITH n
67
+ UNWIND CASE WHEN size($aboutNodeIds) = 0 THEN [null] ELSE $aboutNodeIds END AS aboutNodeId
68
+ OPTIONAL MATCH (target)
69
+ WHERE aboutNodeId IS NOT NULL
70
+ AND target.id = aboutNodeId
71
+ AND target.projectId = $projectId
72
+ AND NOT target:SessionNote
73
+ AND NOT target:SessionBookmark
74
+ AND NOT target:Pheromone
75
+ AND NOT target:SwarmTask
76
+ WITH n, collect(target) AS targets
77
+ FOREACH (t IN [x IN targets WHERE x IS NOT NULL] | MERGE (n)-[:ABOUT]->(t))
78
+
79
+ // Link to the latest SessionBookmark for this session (if one exists)
80
+ WITH n
81
+ OPTIONAL MATCH (bm:SessionBookmark {projectId: $projectId, sessionId: $sessionId})
82
+ WITH n, bm ORDER BY bm.createdAt DESC
83
+ LIMIT 1
84
+ FOREACH (_ IN CASE WHEN bm IS NOT NULL THEN [1] ELSE [] END |
85
+ MERGE (bm)-[:HAS_NOTE]->(n)
86
+ )
87
+
88
+ RETURN n.id AS noteId
89
+ `;
90
+ const SET_NOTE_EMBEDDING_QUERY = `
91
+ MATCH (n:SessionNote {id: $noteId, projectId: $projectId})
92
+ SET n.embedding = $embedding
93
+ RETURN n.id AS noteId
94
+ `;
95
+ // ---------------------------------------------------------------------------
96
+ // Helpers
97
+ // ---------------------------------------------------------------------------
98
+ const generateBookmarkId = () => `bookmark_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
99
+ const generateNoteId = () => `note_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
100
+ // ---------------------------------------------------------------------------
101
+ // Tool registration
102
+ // ---------------------------------------------------------------------------
103
+ export const createSessionSaveTool = (server) => {
104
+ server.registerTool(TOOL_NAMES.sessionSave, {
105
+ title: TOOL_METADATA[TOOL_NAMES.sessionSave].title,
106
+ description: TOOL_METADATA[TOOL_NAMES.sessionSave].description,
107
+ inputSchema: {
108
+ projectId: z.string().describe('Project ID, name, or path'),
109
+ sessionId: z.string().describe('Session/conversation identifier'),
110
+ agentId: z.string().describe('Your agent identifier'),
111
+ type: z
112
+ .enum(['bookmark', 'note', 'auto'])
113
+ .optional()
114
+ .default('auto')
115
+ .describe('Force bookmark or note, or auto-detect from input'),
116
+ // Bookmark fields
117
+ summary: z.string().min(10).optional().describe('Current work state summary'),
118
+ workingSetNodeIds: z.array(z.string()).optional().describe('Code node IDs you are focused on'),
119
+ taskContext: z.string().optional().describe('High-level task being worked on'),
120
+ findings: z.string().optional().describe('Key discoveries or decisions'),
121
+ nextSteps: z.string().optional().describe('What to do next when resuming'),
122
+ // Note fields
123
+ topic: z.string().min(3).max(100).optional().describe('Short topic label'),
124
+ content: z.string().min(10).optional().describe('Full observation text'),
125
+ category: z
126
+ .enum(['architectural', 'bug', 'insight', 'decision', 'risk', 'todo'])
127
+ .optional()
128
+ .describe('Note category'),
129
+ severity: z.enum(['info', 'warning', 'critical']).optional().default('info').describe('Note severity'),
130
+ aboutNodeIds: z.array(z.string()).optional().describe('Code node IDs this note is about'),
131
+ expiresInHours: z.number().optional().describe('Auto-expire note after N hours'),
132
+ metadata: z.string().optional().describe('Additional structured data as JSON string'),
133
+ },
134
+ }, async ({ projectId, sessionId, agentId, type = 'auto', summary, workingSetNodeIds, taskContext, findings = '', nextSteps = '', topic, content, category, severity = 'info', aboutNodeIds = [], expiresInHours, metadata, }) => {
135
+ const neo4jService = new Neo4jService();
136
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
137
+ if (!projectResult.success) {
138
+ await neo4jService.close();
139
+ return projectResult.error;
140
+ }
141
+ const resolvedProjectId = projectResult.projectId;
142
+ // Determine effective operation mode
143
+ const hasBookmarkFields = workingSetNodeIds != null && workingSetNodeIds.length > 0;
144
+ const hasNoteFields = topic != null && content != null;
145
+ let effectiveType;
146
+ if (type === 'bookmark') {
147
+ effectiveType = 'bookmark';
148
+ }
149
+ else if (type === 'note') {
150
+ effectiveType = 'note';
151
+ }
152
+ else {
153
+ // auto-detect
154
+ if (hasBookmarkFields && hasNoteFields) {
155
+ effectiveType = 'both';
156
+ }
157
+ else if (hasBookmarkFields) {
158
+ effectiveType = 'bookmark';
159
+ }
160
+ else if (hasNoteFields) {
161
+ effectiveType = 'note';
162
+ }
163
+ else {
164
+ await neo4jService.close();
165
+ return createErrorResponse('Cannot auto-detect type: provide workingSetNodeIds for a bookmark, topic+content for a note, or both.');
166
+ }
167
+ }
168
+ try {
169
+ // Validate required fields per operation
170
+ if ((effectiveType === 'bookmark' || effectiveType === 'both') && !summary) {
171
+ return createErrorResponse('summary is required when saving a bookmark.');
172
+ }
173
+ if ((effectiveType === 'bookmark' || effectiveType === 'both') && !taskContext) {
174
+ return createErrorResponse('taskContext is required when saving a bookmark.');
175
+ }
176
+ if ((effectiveType === 'bookmark' || effectiveType === 'both') &&
177
+ (!workingSetNodeIds || workingSetNodeIds.length === 0)) {
178
+ return createErrorResponse('workingSetNodeIds is required when saving a bookmark.');
179
+ }
180
+ if ((effectiveType === 'note' || effectiveType === 'both') && !topic) {
181
+ return createErrorResponse('topic is required when saving a note.');
182
+ }
183
+ if ((effectiveType === 'note' || effectiveType === 'both') && !content) {
184
+ return createErrorResponse('content is required when saving a note.');
185
+ }
186
+ if ((effectiveType === 'note' || effectiveType === 'both') && !category) {
187
+ return createErrorResponse('category is required when saving a note.');
188
+ }
189
+ const result = { success: true, projectId: resolvedProjectId, sessionId, agentId };
190
+ // ── Create bookmark ──────────────────────────────────────────────────
191
+ if (effectiveType === 'bookmark' || effectiveType === 'both') {
192
+ const bookmarkId = generateBookmarkId();
193
+ const metadataJson = metadata ?? null;
194
+ const bookmarkRows = await neo4jService.run(CREATE_BOOKMARK_QUERY, {
195
+ bookmarkId,
196
+ projectId: resolvedProjectId,
197
+ sessionId,
198
+ agentId,
199
+ summary: summary,
200
+ workingSetNodeIds: workingSetNodeIds,
201
+ taskContext: taskContext,
202
+ findings,
203
+ nextSteps,
204
+ metadata: metadataJson,
205
+ });
206
+ if (bookmarkRows.length === 0) {
207
+ return createErrorResponse(effectiveType === 'both'
208
+ ? 'Failed to create session bookmark; note was not saved.'
209
+ : 'Failed to create session bookmark.');
210
+ }
211
+ const bm = bookmarkRows[0];
212
+ const linkedNodes = typeof bm.linkedNodes === 'object' && bm.linkedNodes?.toNumber
213
+ ? bm.linkedNodes.toNumber()
214
+ : (bm.linkedNodes ?? 0);
215
+ result.bookmark = {
216
+ bookmarkId: bm.id,
217
+ summary: bm.summary,
218
+ taskContext: bm.taskContext,
219
+ workingSetSize: workingSetNodeIds.length,
220
+ linkedNodes,
221
+ createdAt: typeof bm.createdAt === 'object' && bm.createdAt?.toNumber ? bm.createdAt.toNumber() : bm.createdAt,
222
+ message: `Session bookmark saved. ${linkedNodes} of ${workingSetNodeIds.length} working set nodes linked in graph.`,
223
+ };
224
+ }
225
+ // ── Create note ──────────────────────────────────────────────────────
226
+ if (effectiveType === 'note' || effectiveType === 'both') {
227
+ const noteId = generateNoteId();
228
+ const expiresAt = expiresInHours != null ? Date.now() + expiresInHours * 3600 * 1000 : null;
229
+ const noteRows = await neo4jService.run(CREATE_SESSION_NOTE_QUERY, {
230
+ noteId,
231
+ projectId: resolvedProjectId,
232
+ sessionId,
233
+ agentId,
234
+ topic: topic,
235
+ content: content,
236
+ category: category,
237
+ severity,
238
+ aboutNodeIds,
239
+ expiresAt,
240
+ });
241
+ if (noteRows.length === 0) {
242
+ return createErrorResponse('Failed to create session note.');
243
+ }
244
+ let hasEmbedding = false;
245
+ try {
246
+ await neo4jService.run(QUERIES.CREATE_SESSION_NOTES_VECTOR_INDEX(getEmbeddingDimensions()));
247
+ const embeddingsService = new EmbeddingsService();
248
+ const embeddingText = `${topic}\n\n${content}`;
249
+ const embedding = await embeddingsService.embedText(embeddingText);
250
+ await neo4jService.run(SET_NOTE_EMBEDDING_QUERY, {
251
+ noteId,
252
+ projectId: resolvedProjectId,
253
+ embedding,
254
+ });
255
+ hasEmbedding = true;
256
+ }
257
+ catch (embErr) {
258
+ await debugLog('Session save note embedding failed (non-fatal)', { error: String(embErr), noteId });
259
+ }
260
+ result.note = {
261
+ noteId,
262
+ topic: topic,
263
+ category: category,
264
+ severity,
265
+ hasEmbedding,
266
+ expiresAt: expiresAt != null ? new Date(expiresAt).toISOString() : null,
267
+ };
268
+ }
269
+ result.type = effectiveType;
270
+ return createSuccessResponse(JSON.stringify(result, null, 2));
271
+ }
272
+ catch (error) {
273
+ await debugLog('Session save error', { error: String(error) });
274
+ return createErrorResponse(error instanceof Error ? error : String(error));
275
+ }
276
+ finally {
277
+ await neo4jService.close();
278
+ }
279
+ });
280
+ };
@@ -15,7 +15,7 @@ const inputSchema = z.object({
15
15
  projectPath: z.string().describe('Path to the TypeScript project root directory'),
16
16
  tsconfigPath: z.string().describe('Path to TypeScript project tsconfig.json file'),
17
17
  projectId: z.string().optional().describe('Optional project ID override (auto-generated from path if omitted)'),
18
- debounceMs: z.number().optional().default(1000).describe('Debounce delay in milliseconds (default: 1000)'),
18
+ debounceMs: z.number().optional().default(1000).describe('Debounce delay in milliseconds'),
19
19
  });
20
20
  export const createStartWatchProjectTool = (server) => {
21
21
  server.registerTool(TOOL_NAMES.startWatchProject, {
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
3
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
4
+ import { SwarmAdvanceHandler } from '../handlers/swarm/index.js';
5
+ import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
6
+ export const createSwarmAdvanceTaskTool = (server) => {
7
+ server.registerTool(TOOL_NAMES.swarmAdvanceTask, {
8
+ title: TOOL_METADATA[TOOL_NAMES.swarmAdvanceTask].title,
9
+ description: TOOL_METADATA[TOOL_NAMES.swarmAdvanceTask].description,
10
+ inputSchema: {
11
+ projectId: z.string().describe('Project ID, name, or path'),
12
+ taskId: z.string().describe('Task ID to advance'),
13
+ agentId: z.string().describe('Your agent identifier'),
14
+ force: z.boolean().optional().default(false).describe('Force start from stuck claimed or available state'),
15
+ reason: z.string().optional().describe('Reason for force starting'),
16
+ },
17
+ }, async ({ projectId, taskId, agentId, force = false, reason }) => {
18
+ const neo4jService = new Neo4jService();
19
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
20
+ if (!projectResult.success) {
21
+ await neo4jService.close();
22
+ return projectResult.error;
23
+ }
24
+ const resolvedProjectId = projectResult.projectId;
25
+ try {
26
+ if (force) {
27
+ const { error, data } = await new SwarmAdvanceHandler(neo4jService).forceStart(resolvedProjectId, taskId, agentId, reason);
28
+ if (error) {
29
+ return createErrorResponse(`Cannot force_start task ${taskId}. ` +
30
+ (data
31
+ ? `Current state: ${data.status}, claimedBy: ${data.claimedBy || 'none'}. ` +
32
+ `force_start requires status=claimed|available and you must be the claimant.`
33
+ : 'Task not found.'));
34
+ }
35
+ return createSuccessResponse(JSON.stringify({ action: 'force_started', taskId: data.id, status: 'in_progress' }));
36
+ }
37
+ const { error, data } = await new SwarmAdvanceHandler(neo4jService).start(resolvedProjectId, taskId, agentId);
38
+ if (error) {
39
+ return createErrorResponse(`Cannot start task ${taskId}. ` +
40
+ (data
41
+ ? `Current state: ${data.status}, claimedBy: ${data.claimedBy || 'none'}. ` +
42
+ `Tip: Use force=true to recover from stuck claimed state, ` +
43
+ `or use swarm_release_task to give up the task.`
44
+ : 'Task not found.'));
45
+ }
46
+ return createSuccessResponse(JSON.stringify({ action: 'started', taskId: data.id, status: 'in_progress' }));
47
+ }
48
+ catch (error) {
49
+ await debugLog('Swarm advance task error', { error: String(error) });
50
+ return createErrorResponse(error instanceof Error ? error : String(error));
51
+ }
52
+ finally {
53
+ await neo4jService.close();
54
+ }
55
+ });
56
+ };