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
@@ -70,17 +70,11 @@ export const createParseTypescriptProjectTool = (server) => {
70
70
  configFileGlobs: z
71
71
  .array(z.string())
72
72
  .optional()
73
- .describe('Custom glob patterns for config files (default: all JSON, YAML, .env, Dockerfile, .sh, .py)')
73
+ .describe('Custom glob patterns for config files')
74
74
  .default(CONFIG_FILE_PATTERNS.defaultGlobs),
75
- projectId: z
76
- .string()
77
- .optional()
78
- .describe('Optional project ID override. If not provided, auto-generated from projectPath'),
75
+ projectId: z.string().optional().describe('Project ID override; auto-generated from projectPath if omitted'),
79
76
  clearExisting: z.boolean().optional().describe('Clear existing graph data for this project first'),
80
- excludeNodeTypes: z
81
- .array(z.string())
82
- .optional()
83
- .describe('Node types to skip during parsing, e.g. ["TestFile", "Parameter"]'),
77
+ excludeNodeTypes: z.array(z.string()).optional().describe('Node types to skip during parsing'),
84
78
  projectType: z
85
79
  .enum(['nestjs', 'fairsquare', 'both', 'vanilla', 'auto'])
86
80
  .optional()
@@ -90,7 +84,7 @@ export const createParseTypescriptProjectTool = (server) => {
90
84
  .number()
91
85
  .optional()
92
86
  .default(100)
93
- .describe('Files per chunk for streaming import (default: 50). Set to 0 to disable streaming.'),
87
+ .describe('Files per chunk for streaming import. Set to 0 to disable streaming.'),
94
88
  useStreaming: z
95
89
  .enum(['auto', 'always', 'never'])
96
90
  .optional()
@@ -100,17 +94,13 @@ export const createParseTypescriptProjectTool = (server) => {
100
94
  .boolean()
101
95
  .optional()
102
96
  .default(true)
103
- .describe('Run parsing in background and return job ID immediately. Use check_parse_status to monitor.'),
97
+ .describe('Run in background and return job ID immediately; poll with check_parse_status'),
104
98
  watch: z
105
99
  .boolean()
106
100
  .optional()
107
101
  .default(false)
108
- .describe('Start file watching after parse completes. Only works with async: false.'),
109
- watchDebounceMs: z
110
- .number()
111
- .optional()
112
- .default(1000)
113
- .describe('Debounce delay for watch mode in milliseconds (default: 1000)'),
102
+ .describe('Start file watching after parse completes; requires async: false'),
103
+ watchDebounceMs: z.number().optional().default(1000).describe('Debounce delay for watch mode in milliseconds'),
114
104
  },
115
105
  }, async ({ tsconfigPath, projectPath, projectId, configFileGlobs, clearExisting, projectType = 'auto', chunkSize = 100, useStreaming = 'auto', async: asyncMode = false, watch = false, watchDebounceMs = 1000, }) => {
116
106
  try {
@@ -7,7 +7,7 @@ import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
7
7
  import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
8
8
  import { TOOL_NAMES, TOOL_METADATA, DEFAULTS } from '../constants.js';
9
9
  import { TraversalHandler } from '../handlers/traversal.handler.js';
10
- import { createErrorResponse, createSuccessResponse, debugLog, sanitizeNumericInput, resolveProjectIdOrError, } from '../utils.js';
10
+ import { createEmptyResponse, createErrorResponse, debugLog, sanitizeNumericInput, resolveProjectIdOrError, } from '../utils.js';
11
11
  export const createSearchCodebaseTool = (server) => {
12
12
  server.registerTool(TOOL_NAMES.searchCodebase, {
13
13
  title: TOOL_METADATA[TOOL_NAMES.searchCodebase].title,
@@ -19,41 +19,28 @@ export const createSearchCodebaseTool = (server) => {
19
19
  .number()
20
20
  .int()
21
21
  .optional()
22
- .describe(`Maximum depth to traverse relationships (default: ${DEFAULTS.traversalDepth}, max: 10)`)
22
+ .describe('Maximum relationship traversal depth')
23
23
  .default(DEFAULTS.traversalDepth),
24
- maxNodesPerChain: z
25
- .number()
26
- .int()
27
- .optional()
28
- .describe('Maximum chains to show per depth level (default: 5, applied independently at each depth)')
29
- .default(5),
30
- skip: z.number().int().optional().describe('Number of results to skip for pagination (default: 0)').default(0),
31
- includeCode: z
32
- .boolean()
33
- .optional()
34
- .describe('Include source code snippets in results (default: true)')
35
- .default(true),
24
+ maxNodesPerChain: z.number().int().optional().describe('Maximum chains to show per depth level').default(5),
25
+ skip: z.number().int().optional().describe('Results to skip for pagination').default(0),
26
+ includeCode: z.boolean().optional().describe('Include source code snippets in results').default(true),
36
27
  snippetLength: z
37
28
  .number()
38
29
  .int()
39
30
  .optional()
40
- .describe(`Length of code snippets to include (default: ${DEFAULTS.codeSnippetLength})`)
31
+ .describe('Code snippet character limit')
41
32
  .default(DEFAULTS.codeSnippetLength),
42
33
  topK: z
43
34
  .number()
44
35
  .int()
45
36
  .optional()
46
- .describe('Number of top vector matches to return (default: 3, max: 10). The best match is traversed; others shown as alternatives.')
37
+ .describe('Number of top vector matches; best match is traversed, others shown as alternatives')
47
38
  .default(3),
48
- minSimilarity: z
49
- .number()
50
- .optional()
51
- .describe('Minimum similarity score threshold (0.0-1.0). Results below this are filtered out. Default: 0.65')
52
- .default(0.65),
39
+ minSimilarity: z.number().optional().describe('Minimum similarity score threshold').default(0.65),
53
40
  useWeightedTraversal: z
54
41
  .boolean()
55
42
  .optional()
56
- .describe('Use weighted traversal strategy that scores each node for relevance (default: false)')
43
+ .describe('Score each traversed node for relevance; higher quality but slower')
57
44
  .default(true),
58
45
  },
59
46
  }, async ({ projectId, query, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, topK = 3, minSimilarity = 0.65, useWeightedTraversal = true, }) => {
@@ -81,15 +68,13 @@ export const createSearchCodebaseTool = (server) => {
81
68
  minSimilarity,
82
69
  });
83
70
  if (vectorResults.length === 0) {
84
- return createSuccessResponse(`No code found with similarity >= ${minSimilarity}. ` +
85
- `Try rephrasing your query or lowering the minSimilarity threshold. Query: "${query}"`);
71
+ return createEmptyResponse(`No code found with similarity >= ${minSimilarity}`, 'Try rephrasing your query or lowering the minSimilarity threshold.');
86
72
  }
87
73
  // Filter results that meet the similarity threshold
88
74
  const qualifiedResults = vectorResults.filter((r) => r.score >= minSimilarity);
89
75
  if (qualifiedResults.length === 0) {
90
76
  const bestScore = vectorResults[0].score;
91
- return createSuccessResponse(`No sufficiently relevant code found. Best match score: ${bestScore.toFixed(3)} ` +
92
- `(threshold: ${minSimilarity}). Try rephrasing your query.`);
77
+ return createEmptyResponse(`No sufficiently relevant code found (best match: ${bestScore.toFixed(3)}, threshold: ${minSimilarity})`, 'Try rephrasing your query or lowering minSimilarity.');
93
78
  }
94
79
  // Best match — traverse from this node
95
80
  const bestMatch = qualifiedResults[0];
@@ -5,7 +5,7 @@
5
5
  import { z } from 'zod';
6
6
  import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
7
7
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
8
- import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
8
+ import { createEmptyResponse, createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog, } from '../utils.js';
9
9
  /**
10
10
  * Neo4j query to create a SessionBookmark node and link to code nodes
11
11
  */
@@ -125,7 +125,7 @@ export const createSaveSessionBookmarkTool = (server) => {
125
125
  projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
126
126
  sessionId: z.string().describe('Unique session identifier (e.g., conversation ID) for cross-session recovery'),
127
127
  agentId: z.string().describe('Agent identifier for this bookmark'),
128
- summary: z.string().min(10).describe('Brief summary of current work state (min 10 characters)'),
128
+ summary: z.string().min(10).describe('Brief summary of current work state'),
129
129
  workingSetNodeIds: z
130
130
  .array(z.string())
131
131
  .describe('Code node IDs currently being focused on (from search_codebase or traverse_from_node)'),
@@ -207,7 +207,7 @@ export const createRestoreSessionBookmarkTool = (server) => {
207
207
  .boolean()
208
208
  .optional()
209
209
  .default(true)
210
- .describe('Include source code snippets for working set nodes (default: true)'),
210
+ .describe('Include source code snippets for working set nodes'),
211
211
  snippetLength: z
212
212
  .number()
213
213
  .int()
@@ -215,7 +215,7 @@ export const createRestoreSessionBookmarkTool = (server) => {
215
215
  .max(5000)
216
216
  .optional()
217
217
  .default(500)
218
- .describe('Maximum characters per code snippet (default: 500)'),
218
+ .describe('Max characters per code snippet'),
219
219
  },
220
220
  }, async ({ projectId, sessionId, agentId, includeCode = true, snippetLength = 500 }) => {
221
221
  const neo4jService = new Neo4jService();
@@ -233,13 +233,9 @@ export const createRestoreSessionBookmarkTool = (server) => {
233
233
  agentId: agentId ?? null,
234
234
  });
235
235
  if (bookmarkRows.length === 0) {
236
- return createSuccessResponse(JSON.stringify({
237
- success: false,
238
- message: sessionId
239
- ? `No bookmark found for session "${sessionId}"${agentId ? ` and agent "${agentId}"` : ''}`
240
- : `No bookmarks found for this project${agentId ? ` and agent "${agentId}"` : ''}`,
241
- projectId: resolvedProjectId,
242
- }));
236
+ return createEmptyResponse(sessionId
237
+ ? `No bookmark found for session "${sessionId}" in project ${resolvedProjectId}`
238
+ : `No bookmarks found for project ${resolvedProjectId}`, 'Save a bookmark first with save_session_bookmark, or check the sessionId.');
243
239
  }
244
240
  const bm = bookmarkRows[0];
245
241
  // Fetch working set nodes linked in the graph
@@ -70,12 +70,8 @@ export const createCleanupSessionTool = (server) => {
70
70
  .max(50)
71
71
  .optional()
72
72
  .default(3)
73
- .describe('Number of most recent bookmarks to keep per session (default: 3)'),
74
- dryRun: z
75
- .boolean()
76
- .optional()
77
- .default(false)
78
- .describe('Preview what would be deleted without deleting (default: false)'),
73
+ .describe('Recent bookmarks to keep per session'),
74
+ dryRun: z.boolean().optional().default(false).describe('Preview what would be deleted without deleting'),
79
75
  },
80
76
  }, async ({ projectId, keepBookmarks = 3, dryRun = false }) => {
81
77
  const neo4jService = new Neo4jService();
@@ -145,19 +145,11 @@ export const createSaveSessionNoteTool = (server) => {
145
145
  projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
146
146
  sessionId: z.string().describe('Session identifier (e.g., conversation ID or session name)'),
147
147
  agentId: z.string().describe('Agent identifier that is saving the note'),
148
- topic: z.string().min(3).max(100).describe('Short topic label for the note (3-100 characters)'),
149
- content: z.string().min(10).describe('Full observation text (minimum 10 characters)'),
148
+ topic: z.string().min(3).max(100).describe('Short topic label for the note'),
149
+ content: z.string().min(10).describe('Full observation text'),
150
150
  category: z.enum(NOTE_CATEGORIES).describe('Category: architectural, bug, insight, decision, risk, or todo'),
151
- severity: z
152
- .enum(NOTE_SEVERITIES)
153
- .optional()
154
- .default('info')
155
- .describe('Severity level: info (default), warning, or critical'),
156
- aboutNodeIds: z
157
- .array(z.string())
158
- .optional()
159
- .default([])
160
- .describe('Code node IDs this note is about (links to graph nodes via [:ABOUT])'),
151
+ severity: z.enum(NOTE_SEVERITIES).optional().default('info').describe('Severity level'),
152
+ aboutNodeIds: z.array(z.string()).optional().default([]).describe('Code node IDs this note is about'),
161
153
  expiresInHours: z
162
154
  .number()
163
155
  .positive()
@@ -246,21 +238,14 @@ export const createRecallSessionNotesTool = (server) => {
246
238
  severity: z.enum(NOTE_SEVERITIES).optional().describe('Filter by severity: info, warning, critical'),
247
239
  sessionId: z.string().optional().describe('Filter by session ID'),
248
240
  agentId: z.string().optional().describe('Filter by agent ID'),
249
- limit: z
250
- .number()
251
- .int()
252
- .min(1)
253
- .max(50)
254
- .optional()
255
- .default(10)
256
- .describe('Maximum number of notes to return (default: 10, max: 50)'),
241
+ limit: z.number().int().min(1).max(50).optional().default(10).describe('Maximum number of notes to return'),
257
242
  minSimilarity: z
258
243
  .number()
259
244
  .min(0)
260
245
  .max(1)
261
246
  .optional()
262
247
  .default(0.3)
263
- .describe('Minimum similarity score for vector search (0.0-1.0, default: 0.3)'),
248
+ .describe('Minimum similarity score for vector search'),
264
249
  },
265
250
  }, async ({ projectId, query, category, severity, sessionId, agentId, limit = 10, minSimilarity = 0.3 }) => {
266
251
  const neo4jService = new Neo4jService();
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Session Recall Tool
3
+ * Unified tool merging restore_session_bookmark and recall_session_notes
4
+ */
5
+ import { z } from 'zod';
6
+ import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
7
+ import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
8
+ import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
9
+ import { createEmptyResponse, createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog, } from '../utils.js';
10
+ const NOTE_CATEGORIES = ['architectural', 'bug', 'insight', 'decision', 'risk', 'todo'];
11
+ const NOTE_SEVERITIES = ['info', 'warning', 'critical'];
12
+ /**
13
+ * Neo4j query to find the most recent SessionBookmark matching filters
14
+ */
15
+ const FIND_BOOKMARK_QUERY = `
16
+ MATCH (b:SessionBookmark)
17
+ WHERE b.projectId = $projectId
18
+ AND ($sessionId IS NULL OR b.sessionId = $sessionId)
19
+ AND ($agentId IS NULL OR b.agentId = $agentId)
20
+ RETURN b.id AS id,
21
+ b.projectId AS projectId,
22
+ b.sessionId AS sessionId,
23
+ b.agentId AS agentId,
24
+ b.summary AS summary,
25
+ b.workingSetNodeIds AS workingSetNodeIds,
26
+ b.taskContext AS taskContext,
27
+ b.findings AS findings,
28
+ b.nextSteps AS nextSteps,
29
+ b.metadata AS metadata,
30
+ b.createdAt AS createdAt,
31
+ b.updatedAt AS updatedAt
32
+ ORDER BY b.createdAt DESC
33
+ LIMIT 1
34
+ `;
35
+ /**
36
+ * Neo4j query to get code nodes referenced by a bookmark
37
+ */
38
+ const GET_BOOKMARK_WORKING_SET_QUERY = `
39
+ MATCH (b:SessionBookmark {id: $bookmarkId, projectId: $projectId})-[:REFERENCES]->(target)
40
+ WHERE NOT target:Pheromone
41
+ AND NOT target:SwarmTask
42
+ AND NOT target:SessionBookmark
43
+ AND NOT target:SessionNote
44
+ RETURN target.id AS id,
45
+ target.projectId AS projectId,
46
+ labels(target)[0] AS type,
47
+ target.name AS name,
48
+ target.filePath AS filePath,
49
+ CASE WHEN $includeCode THEN target.sourceCode ELSE null END AS sourceCode,
50
+ target.coreType AS coreType,
51
+ target.semanticType AS semanticType,
52
+ target.startLine AS startLine,
53
+ target.endLine AS endLine
54
+ ORDER BY target.filePath, target.startLine
55
+ `;
56
+ /**
57
+ * Semantic (vector) search for session notes
58
+ */
59
+ const VECTOR_SEARCH_NOTES_QUERY = `
60
+ CALL db.index.vector.queryNodes('session_notes_idx', toInteger($limit * 10), $queryEmbedding)
61
+ YIELD node AS n, score
62
+ WHERE n.projectId = $projectId
63
+ AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
64
+ AND ($category IS NULL OR n.category = $category)
65
+ AND ($severity IS NULL OR n.severity = $severity)
66
+ AND ($sessionId IS NULL OR n.sessionId = $sessionId)
67
+ AND ($agentId IS NULL OR n.agentId = $agentId)
68
+ AND score >= $minSimilarity
69
+
70
+ OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
71
+ WHERE NOT codeNode:SessionNote
72
+ AND NOT codeNode:SessionBookmark
73
+ AND NOT codeNode:Pheromone
74
+ AND NOT codeNode:SwarmTask
75
+
76
+ RETURN
77
+ n.id AS id,
78
+ n.topic AS topic,
79
+ n.content AS content,
80
+ n.category AS category,
81
+ n.severity AS severity,
82
+ n.agentId AS agentId,
83
+ n.sessionId AS sessionId,
84
+ n.createdAt AS createdAt,
85
+ n.expiresAt AS expiresAt,
86
+ score AS relevance,
87
+ collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
88
+
89
+ ORDER BY score DESC
90
+ LIMIT toInteger($limit)
91
+ `;
92
+ /**
93
+ * Filter-based (non-semantic) search for session notes
94
+ */
95
+ const FILTER_SEARCH_NOTES_QUERY = `
96
+ MATCH (n:SessionNote)
97
+ WHERE n.projectId = $projectId
98
+ AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
99
+ AND ($category IS NULL OR n.category = $category)
100
+ AND ($severity IS NULL OR n.severity = $severity)
101
+ AND ($sessionId IS NULL OR n.sessionId = $sessionId)
102
+ AND ($agentId IS NULL OR n.agentId = $agentId)
103
+
104
+ OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
105
+ WHERE NOT codeNode:SessionNote
106
+ AND NOT codeNode:SessionBookmark
107
+ AND NOT codeNode:Pheromone
108
+ AND NOT codeNode:SwarmTask
109
+
110
+ RETURN
111
+ n.id AS id,
112
+ n.topic AS topic,
113
+ n.content AS content,
114
+ n.category AS category,
115
+ n.severity AS severity,
116
+ n.agentId AS agentId,
117
+ n.sessionId AS sessionId,
118
+ n.createdAt AS createdAt,
119
+ n.expiresAt AS expiresAt,
120
+ null AS relevance,
121
+ collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
122
+
123
+ ORDER BY n.createdAt DESC
124
+ LIMIT toInteger($limit)
125
+ `;
126
+ export const createSessionRecallTool = (server) => {
127
+ server.registerTool(TOOL_NAMES.sessionRecall, {
128
+ title: TOOL_METADATA[TOOL_NAMES.sessionRecall].title,
129
+ description: TOOL_METADATA[TOOL_NAMES.sessionRecall].description,
130
+ inputSchema: {
131
+ projectId: z.string().describe('Project ID, name, or path'),
132
+ sessionId: z.string().optional().describe('Session ID to restore (latest bookmark + all notes)'),
133
+ agentId: z.string().optional().describe('Filter by agent ID'),
134
+ query: z.string().optional().describe('Semantic search query for notes'),
135
+ category: z.enum(NOTE_CATEGORIES).optional().describe('Filter notes by category'),
136
+ severity: z.enum(NOTE_SEVERITIES).optional().describe('Filter notes by severity'),
137
+ includeCode: z.boolean().optional().default(true).describe('Include source code for working set nodes'),
138
+ snippetLength: z.number().int().optional().default(500).describe('Code snippet character limit'),
139
+ limit: z.number().int().min(1).max(50).optional().default(10).describe('Maximum notes to return'),
140
+ minSimilarity: z
141
+ .number()
142
+ .min(0)
143
+ .max(1)
144
+ .optional()
145
+ .default(0.3)
146
+ .describe('Minimum similarity for semantic search'),
147
+ },
148
+ }, async ({ projectId, sessionId, agentId, query, category, severity, includeCode = true, snippetLength = 500, limit = 10, minSimilarity = 0.3, }) => {
149
+ const neo4jService = new Neo4jService();
150
+ const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
151
+ if (!projectResult.success) {
152
+ await neo4jService.close();
153
+ return projectResult.error;
154
+ }
155
+ const resolvedProjectId = projectResult.projectId;
156
+ try {
157
+ let bookmark = null;
158
+ let workingSet = [];
159
+ let staleNodeIds = [];
160
+ // If sessionId provided, fetch the latest bookmark and its working set
161
+ if (sessionId) {
162
+ const bookmarkRows = await neo4jService.run(FIND_BOOKMARK_QUERY, {
163
+ projectId: resolvedProjectId,
164
+ sessionId,
165
+ agentId: agentId ?? null,
166
+ });
167
+ if (bookmarkRows.length > 0) {
168
+ const bm = bookmarkRows[0];
169
+ const workingSetRows = await neo4jService.run(GET_BOOKMARK_WORKING_SET_QUERY, {
170
+ bookmarkId: bm.id,
171
+ projectId: resolvedProjectId,
172
+ includeCode,
173
+ });
174
+ workingSet = workingSetRows.map((row) => {
175
+ const node = {
176
+ id: row.id,
177
+ type: row.type,
178
+ name: row.name,
179
+ filePath: row.filePath,
180
+ coreType: row.coreType,
181
+ semanticType: row.semanticType,
182
+ startLine: typeof row.startLine === 'object' && row.startLine?.toNumber
183
+ ? row.startLine.toNumber()
184
+ : row.startLine,
185
+ endLine: typeof row.endLine === 'object' && row.endLine?.toNumber ? row.endLine.toNumber() : row.endLine,
186
+ };
187
+ if (includeCode && row.sourceCode) {
188
+ const code = row.sourceCode;
189
+ if (code.length <= snippetLength) {
190
+ node.sourceCode = code;
191
+ }
192
+ else {
193
+ const half = Math.floor(snippetLength / 2);
194
+ node.sourceCode =
195
+ code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
196
+ node.truncated = true;
197
+ }
198
+ }
199
+ return node;
200
+ });
201
+ const foundIds = new Set(workingSetRows.map((r) => r.id));
202
+ const storedIds = Array.isArray(bm.workingSetNodeIds) ? bm.workingSetNodeIds : [];
203
+ staleNodeIds = storedIds.filter((id) => !foundIds.has(id));
204
+ bookmark = {
205
+ id: bm.id,
206
+ projectId: resolvedProjectId,
207
+ sessionId: bm.sessionId,
208
+ agentId: bm.agentId,
209
+ summary: bm.summary,
210
+ taskContext: bm.taskContext,
211
+ findings: bm.findings,
212
+ nextSteps: bm.nextSteps,
213
+ metadata: bm.metadata ? JSON.parse(bm.metadata) : null,
214
+ createdAt: typeof bm.createdAt === 'object' && bm.createdAt?.toNumber ? bm.createdAt.toNumber() : bm.createdAt,
215
+ updatedAt: typeof bm.updatedAt === 'object' && bm.updatedAt?.toNumber ? bm.updatedAt.toNumber() : bm.updatedAt,
216
+ };
217
+ }
218
+ }
219
+ // Fetch notes — semantic if query provided, filter-based otherwise
220
+ let rawNotes;
221
+ if (query) {
222
+ const embeddingsService = new EmbeddingsService();
223
+ const queryEmbedding = await embeddingsService.embedText(query);
224
+ rawNotes = await neo4jService.run(VECTOR_SEARCH_NOTES_QUERY, {
225
+ projectId: resolvedProjectId,
226
+ queryEmbedding,
227
+ limit: Math.floor(limit),
228
+ minSimilarity,
229
+ category: category ?? null,
230
+ severity: severity ?? null,
231
+ sessionId: sessionId ?? null,
232
+ agentId: agentId ?? null,
233
+ });
234
+ }
235
+ else {
236
+ rawNotes = await neo4jService.run(FILTER_SEARCH_NOTES_QUERY, {
237
+ projectId: resolvedProjectId,
238
+ limit: Math.floor(limit),
239
+ category: category ?? null,
240
+ severity: severity ?? null,
241
+ sessionId: sessionId ?? null,
242
+ agentId: agentId ?? null,
243
+ });
244
+ }
245
+ const notes = rawNotes.map((row) => {
246
+ const createdAt = typeof row.createdAt === 'object' && row.createdAt?.toNumber ? row.createdAt.toNumber() : row.createdAt;
247
+ const expiresAt = typeof row.expiresAt === 'object' && row.expiresAt?.toNumber ? row.expiresAt.toNumber() : row.expiresAt;
248
+ const aboutNodes = (row.aboutNodes ?? []).filter((n) => n?.id != null);
249
+ return {
250
+ id: row.id,
251
+ topic: row.topic,
252
+ content: row.content,
253
+ category: row.category,
254
+ severity: row.severity,
255
+ relevance: row.relevance != null ? Math.round(row.relevance * 1000) / 1000 : null,
256
+ agentId: row.agentId,
257
+ sessionId: row.sessionId,
258
+ createdAt,
259
+ expiresAt,
260
+ aboutNodes,
261
+ };
262
+ });
263
+ if (!bookmark && notes.length === 0) {
264
+ return createEmptyResponse(sessionId
265
+ ? `No bookmark or notes found for session "${sessionId}" in project ${resolvedProjectId}`
266
+ : `No notes found for project ${resolvedProjectId}`, query
267
+ ? 'Try a different query, or lower minSimilarity.'
268
+ : 'Save notes or bookmarks with session_save.');
269
+ }
270
+ return createSuccessResponse(JSON.stringify({
271
+ success: true,
272
+ projectId: resolvedProjectId,
273
+ searchMode: query ? 'semantic' : 'filter',
274
+ bookmark,
275
+ workingSet,
276
+ staleNodeIds,
277
+ notes,
278
+ stats: {
279
+ notesCount: notes.length,
280
+ workingSetFound: workingSet.length,
281
+ workingSetStale: staleNodeIds.length,
282
+ },
283
+ }, null, 2));
284
+ }
285
+ catch (error) {
286
+ await debugLog('Session recall error', { error: String(error) });
287
+ return createErrorResponse(error instanceof Error ? error : String(error));
288
+ }
289
+ finally {
290
+ await neo4jService.close();
291
+ }
292
+ });
293
+ };