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.
- package/dist/core/embeddings/natural-language-to-cypher.service.js +2 -4
- package/dist/mcp/constants.js +56 -228
- package/dist/mcp/handlers/swarm/abandon.handler.js +61 -0
- package/dist/mcp/handlers/swarm/advance.handler.js +78 -0
- package/dist/mcp/handlers/swarm/claim.handler.js +61 -0
- package/dist/mcp/handlers/swarm/index.js +5 -0
- package/dist/mcp/handlers/swarm/queries.js +140 -0
- package/dist/mcp/handlers/swarm/release.handler.js +41 -0
- package/dist/mcp/handlers/swarm-worker.handler.js +2 -13
- package/dist/mcp/tools/detect-dead-code.tool.js +33 -65
- package/dist/mcp/tools/detect-duplicate-code.tool.js +44 -53
- package/dist/mcp/tools/impact-analysis.tool.js +1 -1
- package/dist/mcp/tools/index.js +9 -9
- package/dist/mcp/tools/list-projects.tool.js +2 -2
- package/dist/mcp/tools/list-watchers.tool.js +2 -5
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +2 -2
- package/dist/mcp/tools/parse-typescript-project.tool.js +7 -17
- package/dist/mcp/tools/search-codebase.tool.js +11 -26
- package/dist/mcp/tools/session-bookmark.tool.js +7 -11
- package/dist/mcp/tools/session-cleanup.tool.js +2 -6
- package/dist/mcp/tools/session-note.tool.js +6 -21
- package/dist/mcp/tools/session-recall.tool.js +293 -0
- package/dist/mcp/tools/session-save.tool.js +280 -0
- package/dist/mcp/tools/start-watch-project.tool.js +1 -1
- package/dist/mcp/tools/swarm-advance-task.tool.js +56 -0
- package/dist/mcp/tools/swarm-claim-task.tool.js +24 -388
- package/dist/mcp/tools/swarm-cleanup.tool.js +3 -7
- package/dist/mcp/tools/swarm-complete-task.tool.js +14 -17
- package/dist/mcp/tools/swarm-get-tasks.tool.js +8 -26
- package/dist/mcp/tools/swarm-message.tool.js +10 -25
- package/dist/mcp/tools/swarm-pheromone.tool.js +7 -25
- package/dist/mcp/tools/swarm-post-task.tool.js +7 -19
- package/dist/mcp/tools/swarm-release-task.tool.js +53 -0
- package/dist/mcp/tools/swarm-sense.tool.js +10 -30
- package/dist/mcp/tools/traverse-from-node.tool.js +19 -41
- package/dist/mcp/utils.js +41 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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(
|
|
22
|
+
.describe('Maximum relationship traversal depth')
|
|
23
23
|
.default(DEFAULTS.traversalDepth),
|
|
24
|
-
maxNodesPerChain: z
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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('
|
|
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
|
|
149
|
-
content: z.string().min(10).describe('Full observation text
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|