code-graph-context 2.6.2 → 2.7.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/utils/graph-factory.js +1 -1
- package/dist/mcp/constants.js +75 -2
- package/dist/mcp/handlers/graph-generator.handler.js +1 -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 +8 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +4 -3
- package/dist/mcp/tools/session-bookmark.tool.js +314 -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-orchestrate.tool.js +5 -3
- 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 +9 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Shared utilities for creating/converting graph nodes and edges
|
|
4
4
|
*/
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
-
import { CoreEdgeType, CORE_TYPESCRIPT_SCHEMA } from '../config/schema.js';
|
|
6
|
+
import { CoreEdgeType, CORE_TYPESCRIPT_SCHEMA, } from '../config/schema.js';
|
|
7
7
|
// ============================================
|
|
8
8
|
// Node ID Generation
|
|
9
9
|
// ============================================
|
package/dist/mcp/constants.js
CHANGED
|
@@ -39,6 +39,10 @@ export const TOOL_NAMES = {
|
|
|
39
39
|
swarmCompleteTask: 'swarm_complete_task',
|
|
40
40
|
swarmGetTasks: 'swarm_get_tasks',
|
|
41
41
|
swarmOrchestrate: 'swarm_orchestrate',
|
|
42
|
+
saveSessionBookmark: 'save_session_bookmark',
|
|
43
|
+
restoreSessionBookmark: 'restore_session_bookmark',
|
|
44
|
+
saveSessionNote: 'save_session_note',
|
|
45
|
+
recallSessionNotes: 'recall_session_notes',
|
|
42
46
|
};
|
|
43
47
|
// Tool Metadata
|
|
44
48
|
export const TOOL_METADATA = {
|
|
@@ -205,9 +209,9 @@ Parameters:
|
|
|
205
209
|
},
|
|
206
210
|
[TOOL_NAMES.swarmPheromone]: {
|
|
207
211
|
title: 'Swarm Pheromone',
|
|
208
|
-
description: `Mark a code node with a pheromone for coordination. Types: exploring (2min), modifying (10min), claiming (1hr), completed (24hr), warning (permanent), blocked (5min), proposal (1hr), needs_review (30min).
|
|
212
|
+
description: `Mark a code node with a pheromone for coordination. Types: exploring (2min), modifying (10min), claiming (1hr), completed (24hr), warning (permanent), blocked (5min), proposal (1hr), needs_review (30min), session_context (8hr).
|
|
209
213
|
|
|
210
|
-
Workflow states (exploring/claiming/modifying/completed/blocked) are mutually exclusive per agent+node. Use remove:true to delete. Pheromones decay automatically.`,
|
|
214
|
+
Workflow states (exploring/claiming/modifying/completed/blocked) are mutually exclusive per agent+node. Flag types (warning/proposal/needs_review/session_context) can coexist. Use remove:true to delete. Pheromones decay automatically.`,
|
|
211
215
|
},
|
|
212
216
|
[TOOL_NAMES.swarmSense]: {
|
|
213
217
|
title: 'Swarm Sense',
|
|
@@ -261,6 +265,75 @@ Sort by: priority (default), created, updated. Add includeStats:true for aggrega
|
|
|
261
265
|
|
|
262
266
|
Use dryRun:true to preview plan. maxAgents controls parallelism (default: 3). Failed tasks auto-retry via pheromone decay.`,
|
|
263
267
|
},
|
|
268
|
+
[TOOL_NAMES.saveSessionBookmark]: {
|
|
269
|
+
title: 'Save Session Bookmark',
|
|
270
|
+
description: `Save current session context as a bookmark for cross-session continuity.
|
|
271
|
+
|
|
272
|
+
Records your working set (code node IDs), task context, findings, and next steps so a future session can resume exactly where you left off.
|
|
273
|
+
|
|
274
|
+
Parameters:
|
|
275
|
+
- projectId (required): Project ID, name, or path
|
|
276
|
+
- sessionId (required): Unique session/conversation ID for recovery
|
|
277
|
+
- agentId (required): Your agent identifier
|
|
278
|
+
- summary (required, min 10 chars): Brief description of current work state
|
|
279
|
+
- workingSetNodeIds (required): Code node IDs you are focused on
|
|
280
|
+
- taskContext (required): High-level task being worked on
|
|
281
|
+
- findings: Key discoveries or decisions made so far
|
|
282
|
+
- nextSteps: What to do next when resuming
|
|
283
|
+
- metadata: Additional structured data
|
|
284
|
+
|
|
285
|
+
Returns bookmarkId for use with restore_session_bookmark.`,
|
|
286
|
+
},
|
|
287
|
+
[TOOL_NAMES.restoreSessionBookmark]: {
|
|
288
|
+
title: 'Restore Session Bookmark',
|
|
289
|
+
description: `Restore a previously saved session bookmark to resume work.
|
|
290
|
+
|
|
291
|
+
Retrieves the bookmark, fetches working set code nodes (with source), and returns any session notes. Use after conversation compaction or when resuming a task in a new session.
|
|
292
|
+
|
|
293
|
+
Parameters:
|
|
294
|
+
- projectId (required): Project ID, name, or path
|
|
295
|
+
- sessionId: Specific session to restore (latest bookmark if omitted)
|
|
296
|
+
- agentId: Filter by agent ID (any agent if omitted)
|
|
297
|
+
- includeCode (default: true): Include source code for working set nodes
|
|
298
|
+
- snippetLength (default: 500): Max characters per code snippet
|
|
299
|
+
|
|
300
|
+
Returns: bookmark data, working set nodes, session notes, and staleNodeIds (nodes no longer in graph after re-parse).`,
|
|
301
|
+
},
|
|
302
|
+
[TOOL_NAMES.saveSessionNote]: {
|
|
303
|
+
title: 'Save Session Note',
|
|
304
|
+
description: `Save an observation, decision, insight, or risk as a durable session note linked to code nodes.
|
|
305
|
+
|
|
306
|
+
Notes survive session compaction and are recalled by restore_session_bookmark or recall_session_notes.
|
|
307
|
+
|
|
308
|
+
Parameters:
|
|
309
|
+
- projectId (required): Project ID, name, or path
|
|
310
|
+
- sessionId (required): Session/conversation identifier
|
|
311
|
+
- agentId (required): Your agent identifier
|
|
312
|
+
- topic (required, 3-100 chars): Short topic label
|
|
313
|
+
- content (required, min 10 chars): Full observation text
|
|
314
|
+
- category (required): architectural, bug, insight, decision, risk, or todo
|
|
315
|
+
- severity (default: info): info, warning, or critical
|
|
316
|
+
- aboutNodeIds: Code node IDs this note is about (creates [:ABOUT] links)
|
|
317
|
+
- expiresInHours: Auto-expire after N hours (omit for permanent)
|
|
318
|
+
|
|
319
|
+
Returns noteId, hasEmbedding (enables semantic recall), and expiresAt.`,
|
|
320
|
+
},
|
|
321
|
+
[TOOL_NAMES.recallSessionNotes]: {
|
|
322
|
+
title: 'Recall Session Notes',
|
|
323
|
+
description: `Search and retrieve saved session notes. Supports semantic vector search (when query provided) or filter-based search.
|
|
324
|
+
|
|
325
|
+
Parameters:
|
|
326
|
+
- projectId (required): Project ID, name, or path
|
|
327
|
+
- query: Natural language search — triggers semantic vector search when provided
|
|
328
|
+
- category: Filter by architectural, bug, insight, decision, risk, todo
|
|
329
|
+
- severity: Filter by info, warning, or critical
|
|
330
|
+
- sessionId: Filter by session ID
|
|
331
|
+
- agentId: Filter by agent ID
|
|
332
|
+
- limit (default: 10, max: 50): Maximum notes to return
|
|
333
|
+
- minSimilarity (default: 0.3): Minimum similarity for vector search
|
|
334
|
+
|
|
335
|
+
Returns notes with topic, content, category, severity, relevance score (vector mode), and linked aboutNodes.`,
|
|
336
|
+
},
|
|
264
337
|
};
|
|
265
338
|
// Default Values
|
|
266
339
|
export const DEFAULTS = {
|
|
@@ -180,6 +180,7 @@ export class GraphGeneratorHandler {
|
|
|
180
180
|
async createVectorIndexes() {
|
|
181
181
|
console.error('Creating vector indexes...');
|
|
182
182
|
await this.neo4jService.run(QUERIES.CREATE_EMBEDDED_VECTOR_INDEX);
|
|
183
|
+
await this.neo4jService.run(QUERIES.CREATE_SESSION_NOTES_VECTOR_INDEX);
|
|
183
184
|
await debugLog('Vector indexes created');
|
|
184
185
|
}
|
|
185
186
|
flattenProperties(properties) {
|
package/dist/mcp/mcp.server.js
CHANGED
|
@@ -24,8 +24,8 @@ import { registerAllTools } from './tools/index.js';
|
|
|
24
24
|
import { debugLog } from './utils.js';
|
|
25
25
|
// Track server state for debugging
|
|
26
26
|
let serverStartTime;
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const toolCallCount = 0;
|
|
28
|
+
const lastToolCall = null;
|
|
29
29
|
/**
|
|
30
30
|
* Log memory usage and server stats
|
|
31
31
|
*/
|
package/dist/mcp/service-init.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
-
import { ensureNeo4jRunning, isDockerInstalled, isDockerRunning
|
|
7
|
+
import { ensureNeo4jRunning, isDockerInstalled, isDockerRunning } from '../cli/neo4j-docker.js';
|
|
8
8
|
import { Neo4jService, QUERIES } from '../storage/neo4j/neo4j.service.js';
|
|
9
9
|
import { FILE_PATHS, LOG_CONFIG } from './constants.js';
|
|
10
10
|
import { initializeNaturalLanguageService } from './tools/natural-language-to-cypher.tool.js';
|
|
@@ -47,7 +47,7 @@ class WatchManager {
|
|
|
47
47
|
// This is expected if the client doesn't support logging capability
|
|
48
48
|
debugLog('sendNotification: MCP message failed (expected if client lacks logging)', {
|
|
49
49
|
type: notification.type,
|
|
50
|
-
error: String(error)
|
|
50
|
+
error: String(error),
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -62,7 +62,8 @@ class WatchManager {
|
|
|
62
62
|
}
|
|
63
63
|
// Enforce maximum watcher limit
|
|
64
64
|
if (this.watchers.size >= WATCH.maxWatchers) {
|
|
65
|
-
throw new Error(`Maximum watcher limit (${WATCH.maxWatchers}) reached. ` +
|
|
65
|
+
throw new Error(`Maximum watcher limit (${WATCH.maxWatchers}) reached. ` +
|
|
66
|
+
`Stop an existing watcher before starting a new one.`);
|
|
66
67
|
}
|
|
67
68
|
const fullConfig = {
|
|
68
69
|
projectPath: config.projectPath,
|
|
@@ -138,7 +139,13 @@ class WatchManager {
|
|
|
138
139
|
* Handle a file system event
|
|
139
140
|
*/
|
|
140
141
|
handleFileEvent(state, type, filePath) {
|
|
141
|
-
debugLog('handleFileEvent START', {
|
|
142
|
+
debugLog('handleFileEvent START', {
|
|
143
|
+
type,
|
|
144
|
+
filePath,
|
|
145
|
+
projectId: state.projectId,
|
|
146
|
+
status: state.status,
|
|
147
|
+
isStopping: state.isStopping,
|
|
148
|
+
});
|
|
142
149
|
// Ignore events if watcher is stopping or not active
|
|
143
150
|
if (state.isStopping || state.status !== 'active') {
|
|
144
151
|
debugLog('Ignoring event - watcher not active or stopping', {
|
|
@@ -194,12 +201,12 @@ class WatchManager {
|
|
|
194
201
|
projectId: state.projectId,
|
|
195
202
|
isProcessing: state.isProcessing,
|
|
196
203
|
pendingCount: state.pendingEvents.length,
|
|
197
|
-
isStopping: state.isStopping
|
|
204
|
+
isStopping: state.isStopping,
|
|
198
205
|
});
|
|
199
206
|
// Don't process if already processing, no events, or watcher is stopping
|
|
200
207
|
if (state.isProcessing || state.pendingEvents.length === 0 || state.isStopping) {
|
|
201
208
|
await debugLog('processEvents: early return', {
|
|
202
|
-
reason: state.isProcessing ? 'already processing' : state.pendingEvents.length === 0 ? 'no events' : 'stopping'
|
|
209
|
+
reason: state.isProcessing ? 'already processing' : state.pendingEvents.length === 0 ? 'no events' : 'stopping',
|
|
203
210
|
});
|
|
204
211
|
return;
|
|
205
212
|
}
|
|
@@ -226,12 +233,12 @@ class WatchManager {
|
|
|
226
233
|
}
|
|
227
234
|
await debugLog('processEvents: calling incrementalParseHandler', {
|
|
228
235
|
projectPath: state.projectPath,
|
|
229
|
-
projectId: state.projectId
|
|
236
|
+
projectId: state.projectId,
|
|
230
237
|
});
|
|
231
238
|
const result = await this.incrementalParseHandler(state.projectPath, state.projectId, state.tsconfigPath);
|
|
232
239
|
await debugLog('processEvents: incrementalParseHandler returned', {
|
|
233
240
|
nodesUpdated: result.nodesUpdated,
|
|
234
|
-
edgesUpdated: result.edgesUpdated
|
|
241
|
+
edgesUpdated: result.edgesUpdated,
|
|
235
242
|
});
|
|
236
243
|
state.lastUpdateTime = new Date();
|
|
237
244
|
const elapsedMs = Date.now() - startTime;
|
|
@@ -285,7 +292,10 @@ class WatchManager {
|
|
|
285
292
|
debugLog('handleWatcherError: cleanup succeeded', { projectId: state.projectId });
|
|
286
293
|
})
|
|
287
294
|
.catch((cleanupError) => {
|
|
288
|
-
debugLog('handleWatcherError: cleanup failed', {
|
|
295
|
+
debugLog('handleWatcherError: cleanup failed', {
|
|
296
|
+
projectId: state.projectId,
|
|
297
|
+
cleanupError: String(cleanupError),
|
|
298
|
+
});
|
|
289
299
|
console.error(`[WatchManager] Failed to cleanup errored watcher ${state.projectId}:`, cleanupError);
|
|
290
300
|
});
|
|
291
301
|
}
|
|
@@ -306,7 +316,7 @@ class WatchManager {
|
|
|
306
316
|
debugLog('syncMissedChanges: completed', {
|
|
307
317
|
projectId: state.projectId,
|
|
308
318
|
nodesUpdated: result.nodesUpdated,
|
|
309
|
-
edgesUpdated: result.edgesUpdated
|
|
319
|
+
edgesUpdated: result.edgesUpdated,
|
|
310
320
|
});
|
|
311
321
|
if (result.nodesUpdated > 0 || result.edgesUpdated > 0) {
|
|
312
322
|
console.error(`[WatchManager] Synced missed changes for ${state.projectId}: ` +
|
|
@@ -314,7 +324,11 @@ class WatchManager {
|
|
|
314
324
|
}
|
|
315
325
|
})
|
|
316
326
|
.catch((error) => {
|
|
317
|
-
debugLog('syncMissedChanges: error', {
|
|
327
|
+
debugLog('syncMissedChanges: error', {
|
|
328
|
+
projectId: state.projectId,
|
|
329
|
+
error: String(error),
|
|
330
|
+
isStopping: state.isStopping,
|
|
331
|
+
});
|
|
318
332
|
// Only log if watcher hasn't been stopped
|
|
319
333
|
if (!state.isStopping) {
|
|
320
334
|
console.error(`[WatchManager] Failed to sync missed changes for ${state.projectId}:`, error);
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import { createListWatchersTool } from './list-watchers.tool.js';
|
|
|
12
12
|
import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.tool.js';
|
|
13
13
|
import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
|
|
14
14
|
import { createSearchCodebaseTool } from './search-codebase.tool.js';
|
|
15
|
+
import { createRestoreSessionBookmarkTool, createSaveSessionBookmarkTool } from './session-bookmark.tool.js';
|
|
16
|
+
import { createRecallSessionNotesTool, createSaveSessionNoteTool } from './session-note.tool.js';
|
|
15
17
|
import { createStartWatchProjectTool } from './start-watch-project.tool.js';
|
|
16
18
|
import { createStopWatchProjectTool } from './stop-watch-project.tool.js';
|
|
17
19
|
import { createSwarmClaimTaskTool } from './swarm-claim-task.tool.js';
|
|
@@ -73,4 +75,10 @@ export const registerAllTools = (server) => {
|
|
|
73
75
|
createSwarmGetTasksTool(server);
|
|
74
76
|
// Register swarm orchestration tool (meta-tool for coordinating multi-agent work)
|
|
75
77
|
createSwarmOrchestrateTool(server);
|
|
78
|
+
// Register session bookmark tools (cross-session context continuity)
|
|
79
|
+
createSaveSessionBookmarkTool(server);
|
|
80
|
+
createRestoreSessionBookmarkTool(server);
|
|
81
|
+
// Register session note tools (durable observations and decisions)
|
|
82
|
+
createSaveSessionNoteTool(server);
|
|
83
|
+
createRecallSessionNotesTool(server);
|
|
76
84
|
};
|
|
@@ -90,8 +90,8 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
90
90
|
.optional()
|
|
91
91
|
.default('auto')
|
|
92
92
|
.describe('When to use streaming import: auto (>100 files), always, or never'),
|
|
93
|
-
async: z
|
|
94
|
-
.
|
|
93
|
+
async: z.coerce
|
|
94
|
+
.boolean()
|
|
95
95
|
.optional()
|
|
96
96
|
.default(true)
|
|
97
97
|
.describe('Run parsing in background and return job ID immediately. Use check_parse_status to monitor.'),
|
|
@@ -224,7 +224,8 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
224
224
|
});
|
|
225
225
|
const discoveredFiles = await parser.discoverSourceFiles();
|
|
226
226
|
const totalFiles = discoveredFiles.length;
|
|
227
|
-
const shouldUseStreaming = useStreaming === 'always' ||
|
|
227
|
+
const shouldUseStreaming = useStreaming === 'always' ||
|
|
228
|
+
(useStreaming === 'auto' && totalFiles > PARSING.streamingThreshold && chunkSize > 0);
|
|
228
229
|
console.error(`📊 Project has ${totalFiles} files. Streaming: ${shouldUseStreaming ? 'enabled' : 'disabled'}`);
|
|
229
230
|
if (shouldUseStreaming && clearExisting !== false) {
|
|
230
231
|
// Use streaming import for large projects
|
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
RETURN n.id AS id,
|
|
100
|
+
n.sessionId AS sessionId,
|
|
101
|
+
n.agentId AS agentId,
|
|
102
|
+
n.content AS content,
|
|
103
|
+
n.createdAt AS createdAt
|
|
104
|
+
ORDER BY n.createdAt ASC
|
|
105
|
+
LIMIT 50
|
|
106
|
+
`;
|
|
107
|
+
export const createSaveSessionBookmarkTool = (server) => {
|
|
108
|
+
server.registerTool(TOOL_NAMES.saveSessionBookmark, {
|
|
109
|
+
title: TOOL_METADATA[TOOL_NAMES.saveSessionBookmark].title,
|
|
110
|
+
description: TOOL_METADATA[TOOL_NAMES.saveSessionBookmark].description,
|
|
111
|
+
inputSchema: {
|
|
112
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
113
|
+
sessionId: z.string().describe('Unique session identifier (e.g., conversation ID) for cross-session recovery'),
|
|
114
|
+
agentId: z.string().describe('Agent identifier for this bookmark'),
|
|
115
|
+
summary: z.string().min(10).describe('Brief summary of current work state (min 10 characters)'),
|
|
116
|
+
workingSetNodeIds: z
|
|
117
|
+
.array(z.string())
|
|
118
|
+
.describe('Code node IDs currently being focused on (from search_codebase or traverse_from_node)'),
|
|
119
|
+
taskContext: z.string().describe('High-level task currently being worked on'),
|
|
120
|
+
findings: z.string().optional().default('').describe('Key discoveries or decisions made so far'),
|
|
121
|
+
nextSteps: z.string().optional().default('').describe('What to do next when resuming this session'),
|
|
122
|
+
metadata: z.record(z.unknown()).optional().describe('Additional structured data to store with the bookmark'),
|
|
123
|
+
},
|
|
124
|
+
}, async ({ projectId, sessionId, agentId, summary, workingSetNodeIds, taskContext, findings = '', nextSteps = '', metadata, }) => {
|
|
125
|
+
const neo4jService = new Neo4jService();
|
|
126
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
127
|
+
if (!projectResult.success) {
|
|
128
|
+
await neo4jService.close();
|
|
129
|
+
return projectResult.error;
|
|
130
|
+
}
|
|
131
|
+
const resolvedProjectId = projectResult.projectId;
|
|
132
|
+
try {
|
|
133
|
+
const bookmarkId = `bookmark_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
134
|
+
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
135
|
+
const result = await neo4jService.run(CREATE_BOOKMARK_QUERY, {
|
|
136
|
+
bookmarkId,
|
|
137
|
+
projectId: resolvedProjectId,
|
|
138
|
+
sessionId,
|
|
139
|
+
agentId,
|
|
140
|
+
summary,
|
|
141
|
+
workingSetNodeIds,
|
|
142
|
+
taskContext,
|
|
143
|
+
findings,
|
|
144
|
+
nextSteps,
|
|
145
|
+
metadata: metadataJson,
|
|
146
|
+
});
|
|
147
|
+
if (result.length === 0) {
|
|
148
|
+
return createErrorResponse('Failed to create session bookmark');
|
|
149
|
+
}
|
|
150
|
+
const bookmark = result[0];
|
|
151
|
+
const linkedNodes = typeof bookmark.linkedNodes === 'object' && bookmark.linkedNodes?.toNumber
|
|
152
|
+
? bookmark.linkedNodes.toNumber()
|
|
153
|
+
: (bookmark.linkedNodes ?? 0);
|
|
154
|
+
return createSuccessResponse(JSON.stringify({
|
|
155
|
+
success: true,
|
|
156
|
+
bookmarkId: bookmark.id,
|
|
157
|
+
sessionId: bookmark.sessionId,
|
|
158
|
+
agentId: bookmark.agentId,
|
|
159
|
+
projectId: resolvedProjectId,
|
|
160
|
+
summary: bookmark.summary,
|
|
161
|
+
taskContext: bookmark.taskContext,
|
|
162
|
+
workingSetSize: workingSetNodeIds.length,
|
|
163
|
+
linkedNodes,
|
|
164
|
+
createdAt: typeof bookmark.createdAt === 'object' && bookmark.createdAt?.toNumber
|
|
165
|
+
? bookmark.createdAt.toNumber()
|
|
166
|
+
: bookmark.createdAt,
|
|
167
|
+
message: `Session bookmark saved. ${linkedNodes} of ${workingSetNodeIds.length} working set nodes linked in graph.`,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
await debugLog('Save session bookmark error', { error: String(error) });
|
|
172
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
await neo4jService.close();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
export const createRestoreSessionBookmarkTool = (server) => {
|
|
180
|
+
server.registerTool(TOOL_NAMES.restoreSessionBookmark, {
|
|
181
|
+
title: TOOL_METADATA[TOOL_NAMES.restoreSessionBookmark].title,
|
|
182
|
+
description: TOOL_METADATA[TOOL_NAMES.restoreSessionBookmark].description,
|
|
183
|
+
inputSchema: {
|
|
184
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
185
|
+
sessionId: z
|
|
186
|
+
.string()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe('Specific session ID to restore. If omitted, restores the most recent bookmark.'),
|
|
189
|
+
agentId: z
|
|
190
|
+
.string()
|
|
191
|
+
.optional()
|
|
192
|
+
.describe('Filter bookmarks by agent ID. If omitted, returns bookmarks from any agent.'),
|
|
193
|
+
includeCode: z
|
|
194
|
+
.boolean()
|
|
195
|
+
.optional()
|
|
196
|
+
.default(true)
|
|
197
|
+
.describe('Include source code snippets for working set nodes (default: true)'),
|
|
198
|
+
snippetLength: z
|
|
199
|
+
.number()
|
|
200
|
+
.int()
|
|
201
|
+
.min(50)
|
|
202
|
+
.max(5000)
|
|
203
|
+
.optional()
|
|
204
|
+
.default(500)
|
|
205
|
+
.describe('Maximum characters per code snippet (default: 500)'),
|
|
206
|
+
},
|
|
207
|
+
}, async ({ projectId, sessionId, agentId, includeCode = true, snippetLength = 500 }) => {
|
|
208
|
+
const neo4jService = new Neo4jService();
|
|
209
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
210
|
+
if (!projectResult.success) {
|
|
211
|
+
await neo4jService.close();
|
|
212
|
+
return projectResult.error;
|
|
213
|
+
}
|
|
214
|
+
const resolvedProjectId = projectResult.projectId;
|
|
215
|
+
try {
|
|
216
|
+
// Find the most recent matching bookmark
|
|
217
|
+
const bookmarkRows = await neo4jService.run(FIND_BOOKMARK_QUERY, {
|
|
218
|
+
projectId: resolvedProjectId,
|
|
219
|
+
sessionId: sessionId ?? null,
|
|
220
|
+
agentId: agentId ?? null,
|
|
221
|
+
});
|
|
222
|
+
if (bookmarkRows.length === 0) {
|
|
223
|
+
return createSuccessResponse(JSON.stringify({
|
|
224
|
+
success: false,
|
|
225
|
+
message: sessionId
|
|
226
|
+
? `No bookmark found for session "${sessionId}"${agentId ? ` and agent "${agentId}"` : ''}`
|
|
227
|
+
: `No bookmarks found for this project${agentId ? ` and agent "${agentId}"` : ''}`,
|
|
228
|
+
projectId: resolvedProjectId,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
const bm = bookmarkRows[0];
|
|
232
|
+
// Fetch working set nodes linked in the graph
|
|
233
|
+
const workingSetRows = await neo4jService.run(GET_BOOKMARK_WORKING_SET_QUERY, {
|
|
234
|
+
bookmarkId: bm.id,
|
|
235
|
+
projectId: resolvedProjectId,
|
|
236
|
+
includeCode,
|
|
237
|
+
});
|
|
238
|
+
// Fetch any session notes for this session
|
|
239
|
+
const noteRows = await neo4jService.run(GET_SESSION_NOTES_QUERY, {
|
|
240
|
+
projectId: resolvedProjectId,
|
|
241
|
+
sessionId: bm.sessionId,
|
|
242
|
+
});
|
|
243
|
+
// Build working set with optional code truncation
|
|
244
|
+
const workingSet = workingSetRows.map((row) => {
|
|
245
|
+
const node = {
|
|
246
|
+
id: row.id,
|
|
247
|
+
type: row.type,
|
|
248
|
+
name: row.name,
|
|
249
|
+
filePath: row.filePath,
|
|
250
|
+
coreType: row.coreType,
|
|
251
|
+
semanticType: row.semanticType,
|
|
252
|
+
startLine: typeof row.startLine === 'object' && row.startLine?.toNumber ? row.startLine.toNumber() : row.startLine,
|
|
253
|
+
endLine: typeof row.endLine === 'object' && row.endLine?.toNumber ? row.endLine.toNumber() : row.endLine,
|
|
254
|
+
};
|
|
255
|
+
if (includeCode && row.sourceCode) {
|
|
256
|
+
const code = row.sourceCode;
|
|
257
|
+
if (code.length <= snippetLength) {
|
|
258
|
+
node.sourceCode = code;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const half = Math.floor(snippetLength / 2);
|
|
262
|
+
node.sourceCode =
|
|
263
|
+
code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
|
|
264
|
+
node.truncated = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return node;
|
|
268
|
+
});
|
|
269
|
+
const notes = noteRows.map((n) => ({
|
|
270
|
+
id: n.id,
|
|
271
|
+
sessionId: n.sessionId,
|
|
272
|
+
agentId: n.agentId,
|
|
273
|
+
content: n.content,
|
|
274
|
+
createdAt: typeof n.createdAt === 'object' && n.createdAt?.toNumber ? n.createdAt.toNumber() : n.createdAt,
|
|
275
|
+
}));
|
|
276
|
+
// Identify working set nodes not found in the graph (stale IDs after re-parse)
|
|
277
|
+
const foundIds = new Set(workingSetRows.map((r) => r.id));
|
|
278
|
+
const storedIds = Array.isArray(bm.workingSetNodeIds) ? bm.workingSetNodeIds : [];
|
|
279
|
+
const staleNodeIds = storedIds.filter((id) => !foundIds.has(id));
|
|
280
|
+
return createSuccessResponse(JSON.stringify({
|
|
281
|
+
success: true,
|
|
282
|
+
bookmark: {
|
|
283
|
+
id: bm.id,
|
|
284
|
+
projectId: resolvedProjectId,
|
|
285
|
+
sessionId: bm.sessionId,
|
|
286
|
+
agentId: bm.agentId,
|
|
287
|
+
summary: bm.summary,
|
|
288
|
+
taskContext: bm.taskContext,
|
|
289
|
+
findings: bm.findings,
|
|
290
|
+
nextSteps: bm.nextSteps,
|
|
291
|
+
metadata: bm.metadata ? JSON.parse(bm.metadata) : null,
|
|
292
|
+
createdAt: typeof bm.createdAt === 'object' && bm.createdAt?.toNumber ? bm.createdAt.toNumber() : bm.createdAt,
|
|
293
|
+
updatedAt: typeof bm.updatedAt === 'object' && bm.updatedAt?.toNumber ? bm.updatedAt.toNumber() : bm.updatedAt,
|
|
294
|
+
},
|
|
295
|
+
workingSet,
|
|
296
|
+
notes,
|
|
297
|
+
staleNodeIds,
|
|
298
|
+
stats: {
|
|
299
|
+
workingSetTotal: storedIds.length,
|
|
300
|
+
workingSetFound: workingSet.length,
|
|
301
|
+
workingSetStale: staleNodeIds.length,
|
|
302
|
+
notesCount: notes.length,
|
|
303
|
+
},
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
await debugLog('Restore session bookmark error', { error: String(error) });
|
|
308
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await neo4jService.close();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
};
|