code-graph-context 2.6.2 → 2.8.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/README.md +12 -22
- package/dist/core/utils/graph-factory.js +1 -1
- package/dist/mcp/constants.js +89 -7
- package/dist/mcp/handlers/graph-generator.handler.js +4 -0
- package/dist/mcp/mcp.server.js +2 -2
- package/dist/mcp/service-init.js +1 -1
- package/dist/mcp/services/watch-manager.js +24 -10
- package/dist/mcp/tools/index.js +11 -3
- package/dist/mcp/tools/parse-typescript-project.tool.js +4 -3
- package/dist/mcp/tools/session-bookmark.tool.js +335 -0
- package/dist/mcp/tools/session-cleanup.tool.js +139 -0
- package/dist/mcp/tools/session-note.tool.js +343 -0
- package/dist/mcp/tools/swarm-claim-task.tool.js +4 -11
- package/dist/mcp/tools/swarm-cleanup.tool.js +25 -13
- package/dist/mcp/tools/swarm-complete-task.tool.js +8 -33
- package/dist/mcp/tools/swarm-constants.js +2 -1
- package/dist/mcp/tools/swarm-get-tasks.tool.js +5 -12
- package/dist/mcp/tools/swarm-pheromone.tool.js +11 -4
- package/dist/mcp/tools/swarm-post-task.tool.js +5 -15
- package/dist/mcp/tools/swarm-sense.tool.js +9 -1
- package/dist/storage/neo4j/neo4j.service.js +13 -1
- package/package.json +1 -1
- package/dist/mcp/tools/swarm-orchestrate.tool.js +0 -469
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Bookmark Tools
|
|
3
|
+
* Save and restore session context for cross-session continuity
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Neo4j query to create a SessionBookmark node and link to code nodes
|
|
11
|
+
*/
|
|
12
|
+
const CREATE_BOOKMARK_QUERY = `
|
|
13
|
+
CREATE (b:SessionBookmark {
|
|
14
|
+
id: $bookmarkId,
|
|
15
|
+
projectId: $projectId,
|
|
16
|
+
sessionId: $sessionId,
|
|
17
|
+
agentId: $agentId,
|
|
18
|
+
summary: $summary,
|
|
19
|
+
workingSetNodeIds: $workingSetNodeIds,
|
|
20
|
+
taskContext: $taskContext,
|
|
21
|
+
findings: $findings,
|
|
22
|
+
nextSteps: $nextSteps,
|
|
23
|
+
metadata: $metadata,
|
|
24
|
+
createdAt: timestamp(),
|
|
25
|
+
updatedAt: timestamp()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Link to referenced code nodes (exclude coordination nodes)
|
|
29
|
+
WITH b
|
|
30
|
+
OPTIONAL MATCH (target)
|
|
31
|
+
WHERE target.id IN $workingSetNodeIds
|
|
32
|
+
AND target.projectId = $projectId
|
|
33
|
+
AND NOT target:Pheromone
|
|
34
|
+
AND NOT target:SwarmTask
|
|
35
|
+
AND NOT target:SessionBookmark
|
|
36
|
+
AND NOT target:SessionNote
|
|
37
|
+
WITH b, collect(DISTINCT target) AS targets
|
|
38
|
+
FOREACH (t IN targets | MERGE (b)-[:REFERENCES]->(t))
|
|
39
|
+
|
|
40
|
+
RETURN b.id AS id,
|
|
41
|
+
b.sessionId AS sessionId,
|
|
42
|
+
b.agentId AS agentId,
|
|
43
|
+
b.summary AS summary,
|
|
44
|
+
b.taskContext AS taskContext,
|
|
45
|
+
b.createdAt AS createdAt,
|
|
46
|
+
size(targets) AS linkedNodes
|
|
47
|
+
`;
|
|
48
|
+
/**
|
|
49
|
+
* Neo4j query to find the most recent SessionBookmark matching filters
|
|
50
|
+
*/
|
|
51
|
+
const FIND_BOOKMARK_QUERY = `
|
|
52
|
+
MATCH (b:SessionBookmark)
|
|
53
|
+
WHERE b.projectId = $projectId
|
|
54
|
+
AND ($sessionId IS NULL OR b.sessionId = $sessionId)
|
|
55
|
+
AND ($agentId IS NULL OR b.agentId = $agentId)
|
|
56
|
+
RETURN b.id AS id,
|
|
57
|
+
b.projectId AS projectId,
|
|
58
|
+
b.sessionId AS sessionId,
|
|
59
|
+
b.agentId AS agentId,
|
|
60
|
+
b.summary AS summary,
|
|
61
|
+
b.workingSetNodeIds AS workingSetNodeIds,
|
|
62
|
+
b.taskContext AS taskContext,
|
|
63
|
+
b.findings AS findings,
|
|
64
|
+
b.nextSteps AS nextSteps,
|
|
65
|
+
b.metadata AS metadata,
|
|
66
|
+
b.createdAt AS createdAt,
|
|
67
|
+
b.updatedAt AS updatedAt
|
|
68
|
+
ORDER BY b.createdAt DESC
|
|
69
|
+
LIMIT 1
|
|
70
|
+
`;
|
|
71
|
+
/**
|
|
72
|
+
* Neo4j query to get code nodes referenced by a bookmark
|
|
73
|
+
*/
|
|
74
|
+
const GET_BOOKMARK_WORKING_SET_QUERY = `
|
|
75
|
+
MATCH (b:SessionBookmark {id: $bookmarkId, projectId: $projectId})-[:REFERENCES]->(target)
|
|
76
|
+
WHERE NOT target:Pheromone
|
|
77
|
+
AND NOT target:SwarmTask
|
|
78
|
+
AND NOT target:SessionBookmark
|
|
79
|
+
AND NOT target:SessionNote
|
|
80
|
+
RETURN target.id AS id,
|
|
81
|
+
target.projectId AS projectId,
|
|
82
|
+
labels(target)[0] AS type,
|
|
83
|
+
target.name AS name,
|
|
84
|
+
target.filePath AS filePath,
|
|
85
|
+
CASE WHEN $includeCode THEN target.sourceCode ELSE null END AS sourceCode,
|
|
86
|
+
target.coreType AS coreType,
|
|
87
|
+
target.semanticType AS semanticType,
|
|
88
|
+
target.startLine AS startLine,
|
|
89
|
+
target.endLine AS endLine
|
|
90
|
+
ORDER BY target.filePath, target.startLine
|
|
91
|
+
`;
|
|
92
|
+
/**
|
|
93
|
+
* Neo4j query to get SessionNote nodes linked to a bookmark's session
|
|
94
|
+
*/
|
|
95
|
+
const GET_SESSION_NOTES_QUERY = `
|
|
96
|
+
MATCH (n:SessionNote)
|
|
97
|
+
WHERE n.projectId = $projectId
|
|
98
|
+
AND n.sessionId = $sessionId
|
|
99
|
+
AND (n.expiresAt IS NULL OR n.expiresAt > timestamp())
|
|
100
|
+
|
|
101
|
+
OPTIONAL MATCH (n)-[:ABOUT]->(codeNode)
|
|
102
|
+
WHERE NOT codeNode:SessionNote
|
|
103
|
+
AND NOT codeNode:SessionBookmark
|
|
104
|
+
AND NOT codeNode:Pheromone
|
|
105
|
+
AND NOT codeNode:SwarmTask
|
|
106
|
+
|
|
107
|
+
RETURN n.id AS id,
|
|
108
|
+
n.topic AS topic,
|
|
109
|
+
n.content AS content,
|
|
110
|
+
n.category AS category,
|
|
111
|
+
n.severity AS severity,
|
|
112
|
+
n.agentId AS agentId,
|
|
113
|
+
n.sessionId AS sessionId,
|
|
114
|
+
n.createdAt AS createdAt,
|
|
115
|
+
n.expiresAt AS expiresAt,
|
|
116
|
+
collect(DISTINCT {id: codeNode.id, name: codeNode.name, filePath: codeNode.filePath}) AS aboutNodes
|
|
117
|
+
ORDER BY n.createdAt ASC
|
|
118
|
+
LIMIT 50
|
|
119
|
+
`;
|
|
120
|
+
export const createSaveSessionBookmarkTool = (server) => {
|
|
121
|
+
server.registerTool(TOOL_NAMES.saveSessionBookmark, {
|
|
122
|
+
title: TOOL_METADATA[TOOL_NAMES.saveSessionBookmark].title,
|
|
123
|
+
description: TOOL_METADATA[TOOL_NAMES.saveSessionBookmark].description,
|
|
124
|
+
inputSchema: {
|
|
125
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
126
|
+
sessionId: z.string().describe('Unique session identifier (e.g., conversation ID) for cross-session recovery'),
|
|
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)'),
|
|
129
|
+
workingSetNodeIds: z
|
|
130
|
+
.array(z.string())
|
|
131
|
+
.describe('Code node IDs currently being focused on (from search_codebase or traverse_from_node)'),
|
|
132
|
+
taskContext: z.string().describe('High-level task currently being worked on'),
|
|
133
|
+
findings: z.string().optional().default('').describe('Key discoveries or decisions made so far'),
|
|
134
|
+
nextSteps: z.string().optional().default('').describe('What to do next when resuming this session'),
|
|
135
|
+
metadata: z.record(z.unknown()).optional().describe('Additional structured data to store with the bookmark'),
|
|
136
|
+
},
|
|
137
|
+
}, async ({ projectId, sessionId, agentId, summary, workingSetNodeIds, taskContext, findings = '', nextSteps = '', metadata, }) => {
|
|
138
|
+
const neo4jService = new Neo4jService();
|
|
139
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
140
|
+
if (!projectResult.success) {
|
|
141
|
+
await neo4jService.close();
|
|
142
|
+
return projectResult.error;
|
|
143
|
+
}
|
|
144
|
+
const resolvedProjectId = projectResult.projectId;
|
|
145
|
+
try {
|
|
146
|
+
const bookmarkId = `bookmark_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
147
|
+
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
148
|
+
const result = await neo4jService.run(CREATE_BOOKMARK_QUERY, {
|
|
149
|
+
bookmarkId,
|
|
150
|
+
projectId: resolvedProjectId,
|
|
151
|
+
sessionId,
|
|
152
|
+
agentId,
|
|
153
|
+
summary,
|
|
154
|
+
workingSetNodeIds,
|
|
155
|
+
taskContext,
|
|
156
|
+
findings,
|
|
157
|
+
nextSteps,
|
|
158
|
+
metadata: metadataJson,
|
|
159
|
+
});
|
|
160
|
+
if (result.length === 0) {
|
|
161
|
+
return createErrorResponse('Failed to create session bookmark');
|
|
162
|
+
}
|
|
163
|
+
const bookmark = result[0];
|
|
164
|
+
const linkedNodes = typeof bookmark.linkedNodes === 'object' && bookmark.linkedNodes?.toNumber
|
|
165
|
+
? bookmark.linkedNodes.toNumber()
|
|
166
|
+
: (bookmark.linkedNodes ?? 0);
|
|
167
|
+
return createSuccessResponse(JSON.stringify({
|
|
168
|
+
success: true,
|
|
169
|
+
bookmarkId: bookmark.id,
|
|
170
|
+
sessionId: bookmark.sessionId,
|
|
171
|
+
agentId: bookmark.agentId,
|
|
172
|
+
projectId: resolvedProjectId,
|
|
173
|
+
summary: bookmark.summary,
|
|
174
|
+
taskContext: bookmark.taskContext,
|
|
175
|
+
workingSetSize: workingSetNodeIds.length,
|
|
176
|
+
linkedNodes,
|
|
177
|
+
createdAt: typeof bookmark.createdAt === 'object' && bookmark.createdAt?.toNumber
|
|
178
|
+
? bookmark.createdAt.toNumber()
|
|
179
|
+
: bookmark.createdAt,
|
|
180
|
+
message: `Session bookmark saved. ${linkedNodes} of ${workingSetNodeIds.length} working set nodes linked in graph.`,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
await debugLog('Save session bookmark error', { error: String(error) });
|
|
185
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await neo4jService.close();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
export const createRestoreSessionBookmarkTool = (server) => {
|
|
193
|
+
server.registerTool(TOOL_NAMES.restoreSessionBookmark, {
|
|
194
|
+
title: TOOL_METADATA[TOOL_NAMES.restoreSessionBookmark].title,
|
|
195
|
+
description: TOOL_METADATA[TOOL_NAMES.restoreSessionBookmark].description,
|
|
196
|
+
inputSchema: {
|
|
197
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
198
|
+
sessionId: z
|
|
199
|
+
.string()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe('Specific session ID to restore. If omitted, restores the most recent bookmark.'),
|
|
202
|
+
agentId: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe('Filter bookmarks by agent ID. If omitted, returns bookmarks from any agent.'),
|
|
206
|
+
includeCode: z
|
|
207
|
+
.boolean()
|
|
208
|
+
.optional()
|
|
209
|
+
.default(true)
|
|
210
|
+
.describe('Include source code snippets for working set nodes (default: true)'),
|
|
211
|
+
snippetLength: z
|
|
212
|
+
.number()
|
|
213
|
+
.int()
|
|
214
|
+
.min(50)
|
|
215
|
+
.max(5000)
|
|
216
|
+
.optional()
|
|
217
|
+
.default(500)
|
|
218
|
+
.describe('Maximum characters per code snippet (default: 500)'),
|
|
219
|
+
},
|
|
220
|
+
}, async ({ projectId, sessionId, agentId, includeCode = true, snippetLength = 500 }) => {
|
|
221
|
+
const neo4jService = new Neo4jService();
|
|
222
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
223
|
+
if (!projectResult.success) {
|
|
224
|
+
await neo4jService.close();
|
|
225
|
+
return projectResult.error;
|
|
226
|
+
}
|
|
227
|
+
const resolvedProjectId = projectResult.projectId;
|
|
228
|
+
try {
|
|
229
|
+
// Find the most recent matching bookmark
|
|
230
|
+
const bookmarkRows = await neo4jService.run(FIND_BOOKMARK_QUERY, {
|
|
231
|
+
projectId: resolvedProjectId,
|
|
232
|
+
sessionId: sessionId ?? null,
|
|
233
|
+
agentId: agentId ?? null,
|
|
234
|
+
});
|
|
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
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
const bm = bookmarkRows[0];
|
|
245
|
+
// Fetch working set nodes linked in the graph
|
|
246
|
+
const workingSetRows = await neo4jService.run(GET_BOOKMARK_WORKING_SET_QUERY, {
|
|
247
|
+
bookmarkId: bm.id,
|
|
248
|
+
projectId: resolvedProjectId,
|
|
249
|
+
includeCode,
|
|
250
|
+
});
|
|
251
|
+
// Fetch any session notes for this session
|
|
252
|
+
const noteRows = await neo4jService.run(GET_SESSION_NOTES_QUERY, {
|
|
253
|
+
projectId: resolvedProjectId,
|
|
254
|
+
sessionId: bm.sessionId,
|
|
255
|
+
});
|
|
256
|
+
// Build working set with optional code truncation
|
|
257
|
+
const workingSet = workingSetRows.map((row) => {
|
|
258
|
+
const node = {
|
|
259
|
+
id: row.id,
|
|
260
|
+
type: row.type,
|
|
261
|
+
name: row.name,
|
|
262
|
+
filePath: row.filePath,
|
|
263
|
+
coreType: row.coreType,
|
|
264
|
+
semanticType: row.semanticType,
|
|
265
|
+
startLine: typeof row.startLine === 'object' && row.startLine?.toNumber ? row.startLine.toNumber() : row.startLine,
|
|
266
|
+
endLine: typeof row.endLine === 'object' && row.endLine?.toNumber ? row.endLine.toNumber() : row.endLine,
|
|
267
|
+
};
|
|
268
|
+
if (includeCode && row.sourceCode) {
|
|
269
|
+
const code = row.sourceCode;
|
|
270
|
+
if (code.length <= snippetLength) {
|
|
271
|
+
node.sourceCode = code;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const half = Math.floor(snippetLength / 2);
|
|
275
|
+
node.sourceCode =
|
|
276
|
+
code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
|
|
277
|
+
node.truncated = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return node;
|
|
281
|
+
});
|
|
282
|
+
const notes = noteRows.map((n) => {
|
|
283
|
+
const aboutNodes = (n.aboutNodes ?? []).filter((node) => node?.id != null);
|
|
284
|
+
return {
|
|
285
|
+
id: n.id,
|
|
286
|
+
topic: n.topic,
|
|
287
|
+
content: n.content,
|
|
288
|
+
category: n.category,
|
|
289
|
+
severity: n.severity,
|
|
290
|
+
agentId: n.agentId,
|
|
291
|
+
sessionId: n.sessionId,
|
|
292
|
+
createdAt: typeof n.createdAt === 'object' && n.createdAt?.toNumber ? n.createdAt.toNumber() : n.createdAt,
|
|
293
|
+
expiresAt: typeof n.expiresAt === 'object' && n.expiresAt?.toNumber ? n.expiresAt.toNumber() : n.expiresAt,
|
|
294
|
+
aboutNodes,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
// Identify working set nodes not found in the graph (stale IDs after re-parse)
|
|
298
|
+
const foundIds = new Set(workingSetRows.map((r) => r.id));
|
|
299
|
+
const storedIds = Array.isArray(bm.workingSetNodeIds) ? bm.workingSetNodeIds : [];
|
|
300
|
+
const staleNodeIds = storedIds.filter((id) => !foundIds.has(id));
|
|
301
|
+
return createSuccessResponse(JSON.stringify({
|
|
302
|
+
success: true,
|
|
303
|
+
bookmark: {
|
|
304
|
+
id: bm.id,
|
|
305
|
+
projectId: resolvedProjectId,
|
|
306
|
+
sessionId: bm.sessionId,
|
|
307
|
+
agentId: bm.agentId,
|
|
308
|
+
summary: bm.summary,
|
|
309
|
+
taskContext: bm.taskContext,
|
|
310
|
+
findings: bm.findings,
|
|
311
|
+
nextSteps: bm.nextSteps,
|
|
312
|
+
metadata: bm.metadata ? JSON.parse(bm.metadata) : null,
|
|
313
|
+
createdAt: typeof bm.createdAt === 'object' && bm.createdAt?.toNumber ? bm.createdAt.toNumber() : bm.createdAt,
|
|
314
|
+
updatedAt: typeof bm.updatedAt === 'object' && bm.updatedAt?.toNumber ? bm.updatedAt.toNumber() : bm.updatedAt,
|
|
315
|
+
},
|
|
316
|
+
workingSet,
|
|
317
|
+
notes,
|
|
318
|
+
staleNodeIds,
|
|
319
|
+
stats: {
|
|
320
|
+
workingSetTotal: storedIds.length,
|
|
321
|
+
workingSetFound: workingSet.length,
|
|
322
|
+
workingSetStale: staleNodeIds.length,
|
|
323
|
+
notesCount: notes.length,
|
|
324
|
+
},
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
await debugLog('Restore session bookmark error', { error: String(error) });
|
|
329
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
await neo4jService.close();
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Cleanup Tool
|
|
3
|
+
* Remove expired notes and prune old bookmarks
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Count expired notes (for dry run)
|
|
11
|
+
*/
|
|
12
|
+
const COUNT_EXPIRED_NOTES_QUERY = `
|
|
13
|
+
MATCH (n:SessionNote)
|
|
14
|
+
WHERE n.projectId = $projectId
|
|
15
|
+
AND n.expiresAt IS NOT NULL
|
|
16
|
+
AND n.expiresAt <= timestamp()
|
|
17
|
+
RETURN count(n) AS count
|
|
18
|
+
`;
|
|
19
|
+
/**
|
|
20
|
+
* Delete expired notes and their edges
|
|
21
|
+
*/
|
|
22
|
+
const DELETE_EXPIRED_NOTES_QUERY = `
|
|
23
|
+
MATCH (n:SessionNote)
|
|
24
|
+
WHERE n.projectId = $projectId
|
|
25
|
+
AND n.expiresAt IS NOT NULL
|
|
26
|
+
AND n.expiresAt <= timestamp()
|
|
27
|
+
WITH collect(n) AS toDelete
|
|
28
|
+
WITH size(toDelete) AS cnt, toDelete
|
|
29
|
+
FOREACH (n IN toDelete | DETACH DELETE n)
|
|
30
|
+
RETURN cnt AS deleted
|
|
31
|
+
`;
|
|
32
|
+
/**
|
|
33
|
+
* Find old bookmarks to prune (keeping N most recent per session)
|
|
34
|
+
*/
|
|
35
|
+
const COUNT_OLD_BOOKMARKS_QUERY = `
|
|
36
|
+
MATCH (b:SessionBookmark)
|
|
37
|
+
WHERE b.projectId = $projectId
|
|
38
|
+
WITH b.sessionId AS sessionId, b
|
|
39
|
+
ORDER BY b.createdAt DESC
|
|
40
|
+
WITH sessionId, collect(b) AS bookmarks
|
|
41
|
+
WHERE size(bookmarks) > $keepBookmarks
|
|
42
|
+
UNWIND bookmarks[$keepBookmarks..] AS old
|
|
43
|
+
RETURN count(old) AS count
|
|
44
|
+
`;
|
|
45
|
+
/**
|
|
46
|
+
* Delete old bookmarks (keeping N most recent per session)
|
|
47
|
+
*/
|
|
48
|
+
const DELETE_OLD_BOOKMARKS_QUERY = `
|
|
49
|
+
MATCH (b:SessionBookmark)
|
|
50
|
+
WHERE b.projectId = $projectId
|
|
51
|
+
WITH b.sessionId AS sessionId, b
|
|
52
|
+
ORDER BY b.createdAt DESC
|
|
53
|
+
WITH sessionId, collect(b) AS bookmarks
|
|
54
|
+
WHERE size(bookmarks) > $keepBookmarks
|
|
55
|
+
WITH reduce(all = [], bs IN collect(bookmarks[$keepBookmarks..]) | all + bs) AS toDelete
|
|
56
|
+
WITH size(toDelete) AS cnt, toDelete
|
|
57
|
+
FOREACH (b IN toDelete | DETACH DELETE b)
|
|
58
|
+
RETURN cnt AS deleted
|
|
59
|
+
`;
|
|
60
|
+
export const createCleanupSessionTool = (server) => {
|
|
61
|
+
server.registerTool(TOOL_NAMES.cleanupSession, {
|
|
62
|
+
title: TOOL_METADATA[TOOL_NAMES.cleanupSession].title,
|
|
63
|
+
description: TOOL_METADATA[TOOL_NAMES.cleanupSession].description,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
66
|
+
keepBookmarks: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(50)
|
|
71
|
+
.optional()
|
|
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)'),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ projectId, keepBookmarks = 3, dryRun = false }) => {
|
|
81
|
+
const neo4jService = new Neo4jService();
|
|
82
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
83
|
+
if (!projectResult.success) {
|
|
84
|
+
await neo4jService.close();
|
|
85
|
+
return projectResult.error;
|
|
86
|
+
}
|
|
87
|
+
const resolvedProjectId = projectResult.projectId;
|
|
88
|
+
try {
|
|
89
|
+
const params = { projectId: resolvedProjectId, keepBookmarks };
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
const [noteCount, bookmarkCount] = await Promise.all([
|
|
92
|
+
neo4jService.run(COUNT_EXPIRED_NOTES_QUERY, params),
|
|
93
|
+
neo4jService.run(COUNT_OLD_BOOKMARKS_QUERY, params),
|
|
94
|
+
]);
|
|
95
|
+
const expiredNotes = noteCount[0]?.count ?? 0;
|
|
96
|
+
const oldBookmarks = bookmarkCount[0]?.count ?? 0;
|
|
97
|
+
const toNumber = (v) => (typeof v === 'object' && v?.toNumber ? v.toNumber() : v);
|
|
98
|
+
return createSuccessResponse(JSON.stringify({
|
|
99
|
+
dryRun: true,
|
|
100
|
+
projectId: resolvedProjectId,
|
|
101
|
+
wouldDelete: {
|
|
102
|
+
expiredNotes: toNumber(expiredNotes),
|
|
103
|
+
oldBookmarks: toNumber(oldBookmarks),
|
|
104
|
+
},
|
|
105
|
+
keepBookmarks,
|
|
106
|
+
message: toNumber(expiredNotes) === 0 && toNumber(oldBookmarks) === 0
|
|
107
|
+
? 'Nothing to clean up.'
|
|
108
|
+
: `Would delete ${toNumber(expiredNotes)} expired notes and ${toNumber(oldBookmarks)} old bookmarks.`,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
const [noteResult, bookmarkResult] = await Promise.all([
|
|
112
|
+
neo4jService.run(DELETE_EXPIRED_NOTES_QUERY, params),
|
|
113
|
+
neo4jService.run(DELETE_OLD_BOOKMARKS_QUERY, params),
|
|
114
|
+
]);
|
|
115
|
+
const toNumber = (v) => (typeof v === 'object' && v?.toNumber ? v.toNumber() : v);
|
|
116
|
+
const deletedNotes = toNumber(noteResult[0]?.deleted ?? 0);
|
|
117
|
+
const deletedBookmarks = toNumber(bookmarkResult[0]?.deleted ?? 0);
|
|
118
|
+
return createSuccessResponse(JSON.stringify({
|
|
119
|
+
success: true,
|
|
120
|
+
projectId: resolvedProjectId,
|
|
121
|
+
deleted: {
|
|
122
|
+
expiredNotes: deletedNotes,
|
|
123
|
+
oldBookmarks: deletedBookmarks,
|
|
124
|
+
},
|
|
125
|
+
keepBookmarks,
|
|
126
|
+
message: deletedNotes === 0 && deletedBookmarks === 0
|
|
127
|
+
? 'Nothing to clean up.'
|
|
128
|
+
: `Deleted ${deletedNotes} expired notes and ${deletedBookmarks} old bookmarks.`,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await debugLog('Cleanup session error', { error: String(error) });
|
|
133
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await neo4jService.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
};
|