code-graph-context 2.14.1 → 3.0.1

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 (38) hide show
  1. package/README.md +2 -2
  2. package/dist/core/embeddings/natural-language-to-cypher.service.js +2 -4
  3. package/dist/mcp/constants.js +56 -228
  4. package/dist/mcp/handlers/swarm/abandon.handler.js +61 -0
  5. package/dist/mcp/handlers/swarm/advance.handler.js +78 -0
  6. package/dist/mcp/handlers/swarm/claim.handler.js +61 -0
  7. package/dist/mcp/handlers/swarm/index.js +5 -0
  8. package/dist/mcp/handlers/swarm/queries.js +140 -0
  9. package/dist/mcp/handlers/swarm/release.handler.js +41 -0
  10. package/dist/mcp/handlers/swarm-worker.handler.js +2 -13
  11. package/dist/mcp/tools/detect-dead-code.tool.js +33 -65
  12. package/dist/mcp/tools/detect-duplicate-code.tool.js +44 -53
  13. package/dist/mcp/tools/impact-analysis.tool.js +1 -1
  14. package/dist/mcp/tools/index.js +9 -9
  15. package/dist/mcp/tools/list-projects.tool.js +2 -2
  16. package/dist/mcp/tools/list-watchers.tool.js +2 -5
  17. package/dist/mcp/tools/natural-language-to-cypher.tool.js +2 -2
  18. package/dist/mcp/tools/parse-typescript-project.tool.js +7 -17
  19. package/dist/mcp/tools/search-codebase.tool.js +11 -26
  20. package/dist/mcp/tools/session-bookmark.tool.js +7 -11
  21. package/dist/mcp/tools/session-cleanup.tool.js +2 -6
  22. package/dist/mcp/tools/session-note.tool.js +6 -21
  23. package/dist/mcp/tools/session-recall.tool.js +293 -0
  24. package/dist/mcp/tools/session-save.tool.js +280 -0
  25. package/dist/mcp/tools/start-watch-project.tool.js +1 -1
  26. package/dist/mcp/tools/swarm-advance-task.tool.js +56 -0
  27. package/dist/mcp/tools/swarm-claim-task.tool.js +24 -388
  28. package/dist/mcp/tools/swarm-cleanup.tool.js +3 -7
  29. package/dist/mcp/tools/swarm-complete-task.tool.js +14 -17
  30. package/dist/mcp/tools/swarm-get-tasks.tool.js +8 -26
  31. package/dist/mcp/tools/swarm-message.tool.js +10 -25
  32. package/dist/mcp/tools/swarm-pheromone.tool.js +7 -25
  33. package/dist/mcp/tools/swarm-post-task.tool.js +7 -19
  34. package/dist/mcp/tools/swarm-release-task.tool.js +53 -0
  35. package/dist/mcp/tools/swarm-sense.tool.js +10 -30
  36. package/dist/mcp/tools/traverse-from-node.tool.js +19 -41
  37. package/dist/mcp/utils.js +41 -1
  38. package/package.json +3 -3
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Shared Cypher queries for swarm task handlers.
3
+ *
4
+ * Queries that are used by multiple handlers live here.
5
+ * Handler-specific queries stay in their handler files.
6
+ */
7
+ /**
8
+ * Get current task state — used by all handlers for diagnostic error messages.
9
+ */
10
+ export const GET_TASK_STATE_QUERY = `
11
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
12
+ RETURN t.id as id,
13
+ t.title as title,
14
+ t.status as status,
15
+ t.claimedBy as claimedBy,
16
+ t.claimedAt as claimedAt,
17
+ t.startedAt as startedAt,
18
+ t.abandonCount as abandonCount,
19
+ t.previousClaimedBy as previousClaimedBy
20
+ `;
21
+ /**
22
+ * Claim a specific task by ID.
23
+ * Uses APOC locking for atomic claim under concurrency.
24
+ * Shared by claim-by-id and claim-and-start-by-id flows.
25
+ */
26
+ export const CLAIM_TASK_BY_ID_QUERY = `
27
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
28
+ WHERE t.status IN ['available', 'blocked']
29
+
30
+ // Check if dependencies are complete
31
+ OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
32
+ WHERE dep.status <> 'completed'
33
+ WITH t, count(dep) as incompleteDeps
34
+
35
+ // Only claim if all dependencies are complete
36
+ WHERE incompleteDeps = 0
37
+
38
+ // Acquire exclusive lock to prevent race conditions
39
+ CALL apoc.lock.nodes([t])
40
+
41
+ // Double-check status after acquiring lock
42
+ WITH t WHERE t.status IN ['available', 'blocked']
43
+
44
+ // Atomic claim
45
+ SET t.status = $targetStatus,
46
+ t.claimedBy = $agentId,
47
+ t.claimedAt = timestamp(),
48
+ t.startedAt = CASE WHEN $targetStatus = 'in_progress' THEN timestamp() ELSE null END,
49
+ t.updatedAt = timestamp()
50
+
51
+ // Return task details with target info
52
+ WITH t
53
+ OPTIONAL MATCH (t)-[:TARGETS]->(target)
54
+ RETURN t.id as id,
55
+ t.projectId as projectId,
56
+ t.swarmId as swarmId,
57
+ t.title as title,
58
+ t.description as description,
59
+ t.type as type,
60
+ t.priority as priority,
61
+ t.priorityScore as priorityScore,
62
+ t.status as status,
63
+ t.targetNodeIds as targetNodeIds,
64
+ t.targetFilePaths as targetFilePaths,
65
+ t.dependencies as dependencies,
66
+ t.claimedBy as claimedBy,
67
+ t.claimedAt as claimedAt,
68
+ t.startedAt as startedAt,
69
+ t.createdBy as createdBy,
70
+ t.metadata as metadata,
71
+ collect(DISTINCT {
72
+ id: target.id,
73
+ type: labels(target)[0],
74
+ name: target.name,
75
+ filePath: target.filePath
76
+ }) as targets
77
+ `;
78
+ /**
79
+ * Claim the highest priority available task matching criteria.
80
+ * Uses APOC locking for atomic claim under concurrency.
81
+ * Supports both 'claimed' and 'in_progress' target states.
82
+ */
83
+ export const CLAIM_NEXT_TASK_QUERY = `
84
+ // Find available or blocked tasks (blocked tasks may have deps completed now)
85
+ MATCH (t:SwarmTask {projectId: $projectId, swarmId: $swarmId})
86
+ WHERE t.status IN ['available', 'blocked']
87
+ AND ($types IS NULL OR size($types) = 0 OR t.type IN $types)
88
+ AND ($minPriority IS NULL OR t.priorityScore >= $minPriority)
89
+
90
+ // Exclude tasks with incomplete dependencies
91
+ OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
92
+ WHERE dep.status <> 'completed'
93
+ WITH t, count(dep) as incompleteDeps
94
+ WHERE incompleteDeps = 0
95
+
96
+ // Re-establish context for ordering (required by Cypher syntax)
97
+ WITH t
98
+ ORDER BY t.priorityScore DESC, t.createdAt ASC
99
+ LIMIT 1
100
+
101
+ // Acquire exclusive lock to prevent race conditions
102
+ CALL apoc.lock.nodes([t])
103
+
104
+ // Double-check status after acquiring lock (another worker may have claimed it)
105
+ WITH t WHERE t.status IN ['available', 'blocked']
106
+
107
+ // Atomic claim - supports both claim and claim_and_start via $targetStatus
108
+ SET t.status = $targetStatus,
109
+ t.claimedBy = $agentId,
110
+ t.claimedAt = timestamp(),
111
+ t.startedAt = CASE WHEN $targetStatus = 'in_progress' THEN timestamp() ELSE null END,
112
+ t.updatedAt = timestamp()
113
+
114
+ // Return task details with target info
115
+ WITH t
116
+ OPTIONAL MATCH (t)-[:TARGETS]->(target)
117
+ RETURN t.id as id,
118
+ t.projectId as projectId,
119
+ t.swarmId as swarmId,
120
+ t.title as title,
121
+ t.description as description,
122
+ t.type as type,
123
+ t.priority as priority,
124
+ t.priorityScore as priorityScore,
125
+ t.status as status,
126
+ t.targetNodeIds as targetNodeIds,
127
+ t.targetFilePaths as targetFilePaths,
128
+ t.dependencies as dependencies,
129
+ t.claimedBy as claimedBy,
130
+ t.claimedAt as claimedAt,
131
+ t.startedAt as startedAt,
132
+ t.createdBy as createdBy,
133
+ t.metadata as metadata,
134
+ collect(DISTINCT {
135
+ id: target.id,
136
+ type: labels(target)[0],
137
+ name: target.name,
138
+ filePath: target.filePath
139
+ }) as targets
140
+ `;
@@ -0,0 +1,41 @@
1
+ import { GET_TASK_STATE_QUERY } from './queries.js';
2
+ /**
3
+ * Query to release a claimed task (unclaim it)
4
+ */
5
+ const RELEASE_TASK_QUERY = `
6
+ MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
7
+ WHERE t.status IN ['claimed', 'in_progress'] AND t.claimedBy = $agentId
8
+
9
+ SET t.status = 'available',
10
+ t.claimedBy = null,
11
+ t.claimedAt = null,
12
+ t.startedAt = null,
13
+ t.updatedAt = timestamp(),
14
+ t.releaseReason = $reason
15
+
16
+ RETURN t.id as id,
17
+ t.title as title,
18
+ t.status as status
19
+ `;
20
+ export class SwarmReleaseHandler {
21
+ neo4jService;
22
+ constructor(neo4jService) {
23
+ this.neo4jService = neo4jService;
24
+ }
25
+ async release(projectId, taskId, agentId, reason) {
26
+ const result = await this.neo4jService.run(RELEASE_TASK_QUERY, {
27
+ taskId,
28
+ projectId,
29
+ agentId,
30
+ reason: reason || 'No reason provided',
31
+ });
32
+ if (result.length === 0) {
33
+ const stateResult = await this.neo4jService.run(GET_TASK_STATE_QUERY, {
34
+ taskId,
35
+ projectId,
36
+ });
37
+ return { error: true, data: stateResult[0] };
38
+ }
39
+ return { error: false, data: result[0] };
40
+ }
41
+ }
@@ -47,13 +47,12 @@ swarm_sense({
47
47
  \`\`\`
48
48
 
49
49
  ### 2. Claim a Task
50
- Claim the highest-priority available task:
50
+ Claim and start the highest-priority available task:
51
51
  \`\`\`
52
52
  swarm_claim_task({
53
53
  projectId: "${config.projectId}",
54
54
  swarmId: "${config.swarmId}",
55
- agentId: "${agentId}",
56
- action: "claim"
55
+ agentId: "${agentId}"
57
56
  })
58
57
  \`\`\`
59
58
 
@@ -80,16 +79,6 @@ swarm_pheromone({
80
79
  })
81
80
  \`\`\`
82
81
 
83
- ### 4. Start the Task
84
- \`\`\`
85
- swarm_claim_task({
86
- projectId: "${config.projectId}",
87
- taskId: "<claimed task id>",
88
- agentId: "${agentId}",
89
- action: "start"
90
- })
91
- \`\`\`
92
-
93
82
  ### 5. Execute the Task
94
83
  Read the task description carefully and execute it:
95
84
  - Use Read tool to understand the current code
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  import { toNumber, isUIComponent, isPackageExport, isExcludedByPattern, getShortPath, } from '../../core/utils/shared-utils.js';
7
7
  import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
8
8
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
9
- import { createErrorResponse, createSuccessResponse, debugLog, resolveProjectIdOrError } from '../utils.js';
9
+ import { autoResolveProjectId, createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
10
10
  // Default file patterns to exclude
11
11
  const DEFAULT_ENTRY_POINT_FILE_PATTERNS = [
12
12
  // Common entry points
@@ -94,67 +94,42 @@ export const createDetectDeadCodeTool = (server) => {
94
94
  title: TOOL_METADATA[TOOL_NAMES.detectDeadCode].title,
95
95
  description: TOOL_METADATA[TOOL_NAMES.detectDeadCode].description,
96
96
  inputSchema: {
97
- projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
98
- excludePatterns: z
99
- .array(z.string())
97
+ projectId: z
98
+ .string()
100
99
  .optional()
101
- .describe('Additional file patterns to exclude as entry points (e.g., ["*.config.ts", "*.seed.ts"])'),
102
- excludeSemanticTypes: z
103
- .array(z.string())
104
- .optional()
105
- .describe('Additional semantic types to exclude (e.g., ["EntityClass", "DTOClass"])'),
100
+ .describe('Project ID, name, or path (auto-resolves if only one project exists)'),
101
+ excludePatterns: z.array(z.string()).optional().describe('Additional file patterns to exclude as entry points'),
102
+ excludeSemanticTypes: z.array(z.string()).optional().describe('Additional semantic types to exclude'),
106
103
  includeEntryPoints: z
107
104
  .boolean()
108
105
  .optional()
109
- .describe('Include excluded entry points in a separate audit section for review (default: true). ' +
110
- 'Entry points are always excluded from main results.')
106
+ .describe('Include excluded entry points in audit section')
111
107
  .default(true),
112
108
  minConfidence: z
113
109
  .enum(['LOW', 'MEDIUM', 'HIGH'])
114
110
  .optional()
115
- .describe('Minimum confidence level to include in results (default: LOW)')
111
+ .describe('Minimum confidence threshold for results')
116
112
  .default('LOW'),
117
- summaryOnly: z
118
- .boolean()
119
- .optional()
120
- .describe('Return only summary statistics without full dead code list (default: false)')
121
- .default(false),
122
- limit: z
123
- .number()
124
- .int()
125
- .min(1)
126
- .max(500)
127
- .optional()
128
- .describe('Maximum number of dead code items to return per page (default: 100)')
129
- .default(100),
130
- offset: z
131
- .number()
132
- .int()
133
- .min(0)
134
- .optional()
135
- .describe('Number of items to skip for pagination (default: 0)')
136
- .default(0),
113
+ summaryOnly: z.boolean().optional().describe('Return summary statistics only, no item list').default(false),
114
+ limit: z.number().int().min(1).max(500).optional().describe('Max dead code items per page').default(100),
115
+ offset: z.number().int().min(0).optional().describe('Items to skip for pagination').default(0),
137
116
  filterCategory: z
138
117
  .enum(['library-export', 'ui-component', 'internal-unused', 'all'])
139
118
  .optional()
140
- .describe('Filter by category: library-export, ui-component, internal-unused, or all (default: all)')
119
+ .describe('Filter results to a specific category')
141
120
  .default('all'),
142
121
  excludeLibraryExports: z
143
122
  .boolean()
144
123
  .optional()
145
- .describe('Exclude all items from packages/* directories (default: false)')
124
+ .describe('Exclude items from packages/* directories')
146
125
  .default(false),
147
- excludeCoreTypes: z
148
- .array(z.string())
149
- .optional()
150
- .describe('Exclude specific core types from results (e.g., ["InterfaceDeclaration", "TypeAliasDeclaration"] to skip type definitions)')
151
- .default([]),
126
+ excludeCoreTypes: z.array(z.string()).optional().describe('Core types to exclude from results').default([]),
152
127
  },
153
- }, async ({ projectId, excludePatterns = [], excludeSemanticTypes = [], includeEntryPoints = true, minConfidence = 'LOW', summaryOnly = false, limit = 100, offset = 0, filterCategory = 'all', excludeLibraryExports = false, excludeCoreTypes = [], }) => {
128
+ }, async ({ projectId = undefined, excludePatterns = [], excludeSemanticTypes = [], includeEntryPoints = true, minConfidence = 'LOW', summaryOnly = false, limit = 100, offset = 0, filterCategory = 'all', excludeLibraryExports = false, excludeCoreTypes = [], }) => {
154
129
  const neo4jService = new Neo4jService();
155
130
  try {
156
131
  // Resolve project ID
157
- const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
132
+ const projectResult = await autoResolveProjectId(projectId, neo4jService);
158
133
  if (!projectResult.success)
159
134
  return projectResult.error;
160
135
  const resolvedProjectId = projectResult.projectId;
@@ -340,36 +315,31 @@ export const createDetectDeadCodeTool = (server) => {
340
315
  .sort((a, b) => b[1] - a[1])
341
316
  .slice(0, 20)
342
317
  .map(([file, count]) => ({ file, count }));
318
+ // Always-included summary stats
319
+ const summaryStats = {
320
+ summary,
321
+ riskLevel,
322
+ totalCount: filteredItems.length,
323
+ totalBeforeFilter: deadCodeItems.length,
324
+ byConfidence,
325
+ byCategory,
326
+ byType,
327
+ affectedFiles,
328
+ topFilesByDeadCode,
329
+ excludedEntryPointsCount,
330
+ };
343
331
  // Build result based on summaryOnly flag
344
332
  let result;
345
333
  if (summaryOnly) {
346
- // Summary mode: statistics only, no full arrays
347
- result = {
348
- summary,
349
- riskLevel,
350
- totalCount: filteredItems.length,
351
- totalBeforeFilter: deadCodeItems.length,
352
- byConfidence,
353
- byCategory,
354
- byType,
355
- affectedFiles,
356
- topFilesByDeadCode,
357
- excludedEntryPointsCount,
358
- };
334
+ // Summary mode: statistics only, no item list
335
+ result = summaryStats;
359
336
  }
360
337
  else {
361
- // Paginated mode: apply limit/offset
338
+ // Paginated mode: apply limit/offset, include both stats and items
362
339
  const paginatedItems = filteredItems.slice(offset, offset + limit);
363
340
  const hasMore = offset + limit < filteredItems.length;
364
341
  result = {
365
- summary,
366
- riskLevel,
367
- totalCount: filteredItems.length,
368
- totalBeforeFilter: deadCodeItems.length,
369
- byConfidence,
370
- byCategory,
371
- byType,
372
- topFilesByDeadCode,
342
+ ...summaryStats,
373
343
  deadCode: paginatedItems,
374
344
  pagination: {
375
345
  offset,
@@ -377,10 +347,8 @@ export const createDetectDeadCodeTool = (server) => {
377
347
  returned: paginatedItems.length,
378
348
  hasMore,
379
349
  },
380
- excludedEntryPointsCount,
381
350
  // Only include full entry points array on first page
382
351
  ...(offset === 0 && includeEntryPoints ? { excludedEntryPoints } : {}),
383
- affectedFiles,
384
352
  };
385
353
  }
386
354
  return createSuccessResponse(JSON.stringify(result, null, 2));
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  import { toNumber, isUIComponent, getMonorepoAppName, getShortPath, truncateSourceCode, } from '../../core/utils/shared-utils.js';
7
7
  import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
8
8
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
9
- import { createErrorResponse, createSuccessResponse, debugLog, resolveProjectIdOrError } from '../utils.js';
9
+ import { autoResolveProjectId, createEmptyResponse, createErrorResponse, createSuccessResponse, debugLog, } from '../utils.js';
10
10
  /**
11
11
  * Determine confidence based on duplicate characteristics.
12
12
  */
@@ -91,63 +91,49 @@ export const createDetectDuplicateCodeTool = (server) => {
91
91
  title: TOOL_METADATA[TOOL_NAMES.detectDuplicateCode].title,
92
92
  description: TOOL_METADATA[TOOL_NAMES.detectDuplicateCode].description,
93
93
  inputSchema: {
94
- projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
94
+ projectId: z
95
+ .string()
96
+ .optional()
97
+ .describe('Project ID, name, or path (auto-resolves if only one project exists)'),
95
98
  type: z
96
99
  .enum(['structural', 'semantic', 'all'])
97
100
  .optional()
98
- .describe('Detection approach: structural (AST hash), semantic (embeddings), or all (default: all)')
101
+ .describe('Detection approach: structural (AST hash), semantic (embeddings), or all')
99
102
  .default('all'),
100
103
  minSimilarity: z
101
104
  .number()
102
105
  .min(0.5)
103
106
  .max(1.0)
104
107
  .optional()
105
- .describe('Minimum similarity for semantic duplicates (0.5-1.0, default: 0.80)')
108
+ .describe('Minimum similarity threshold for semantic duplicates')
106
109
  .default(0.8),
107
- includeCode: z
108
- .boolean()
109
- .optional()
110
- .describe('Include source code snippets in results (default: false)')
111
- .default(false),
112
- maxResults: z
113
- .number()
114
- .int()
115
- .min(1)
116
- .max(100)
117
- .optional()
118
- .describe('Maximum number of duplicate groups to return (default: 20)')
119
- .default(20),
110
+ includeCode: z.boolean().optional().describe('Include source code snippets in results').default(false),
111
+ maxResults: z.number().int().min(1).max(100).optional().describe('Max duplicate groups to return').default(20),
120
112
  scope: z
121
113
  .enum(['methods', 'functions', 'classes', 'all'])
122
114
  .optional()
123
- .describe('Node types to analyze (default: all)')
115
+ .describe('Node types to analyze')
124
116
  .default('all'),
125
117
  summaryOnly: z
126
118
  .boolean()
127
119
  .optional()
128
- .describe('Return only summary statistics without full duplicates list (default: false)')
120
+ .describe('Return summary statistics only, no duplicates list')
129
121
  .default(false),
130
- offset: z
131
- .number()
132
- .int()
133
- .min(0)
134
- .optional()
135
- .describe('Number of groups to skip for pagination (default: 0)')
136
- .default(0),
122
+ offset: z.number().int().min(0).optional().describe('Groups to skip for pagination').default(0),
137
123
  vectorNeighbors: z
138
124
  .number()
139
125
  .int()
140
126
  .min(10)
141
127
  .max(200)
142
128
  .optional()
143
- .describe('Number of vector neighbors to search per node for semantic duplicates (default: 50, higher = more thorough)')
129
+ .describe('Vector neighbors per node (higher = more thorough)')
144
130
  .default(50),
145
131
  },
146
132
  }, async ({ projectId, type = 'all', minSimilarity = 0.8, includeCode = false, maxResults = 20, scope = 'all', summaryOnly = false, offset = 0, vectorNeighbors = 50, }) => {
147
133
  const neo4jService = new Neo4jService();
148
134
  try {
149
135
  // Resolve project ID
150
- const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
136
+ const projectResult = await autoResolveProjectId(projectId, neo4jService);
151
137
  if (!projectResult.success)
152
138
  return projectResult.error;
153
139
  const resolvedProjectId = projectResult.projectId;
@@ -352,49 +338,55 @@ export const createDetectDuplicateCodeTool = (server) => {
352
338
  };
353
339
  }
354
340
  }
341
+ // Early return for zero results
342
+ if (totalGroups === 0) {
343
+ return createEmptyResponse('No duplicate code found', semanticDiagnostic
344
+ ? `Semantic analysis note: ${semanticDiagnostic}. Try lowering minSimilarity or changing scope.`
345
+ : 'Try lowering minSimilarity or broadening scope.');
346
+ }
355
347
  // Build summary with warning if no embeddings
356
- let summary = totalGroups === 0
357
- ? 'No duplicate code found'
358
- : `Found ${totalGroups} duplicate code groups across ${affectedFiles.length} files`;
348
+ let summary = `Found ${totalGroups} duplicate code groups across ${affectedFiles.length} files`;
359
349
  if (semanticQueryError) {
360
350
  summary += ` (Warning: ${semanticQueryError})`;
361
351
  }
362
352
  else if ((type === 'semantic' || type === 'all') && embeddingCount === 0 && allSemanticGroups.length === 0) {
363
353
  summary += ' (Warning: No embeddings for semantic detection)';
364
354
  }
355
+ // Build summary stats (always included)
356
+ const fileDuplicateCounts = {};
357
+ for (const group of duplicateGroups) {
358
+ for (const item of group.items) {
359
+ const shortPath = getShortPath(item.filePath);
360
+ fileDuplicateCounts[shortPath] = (fileDuplicateCounts[shortPath] ?? 0) + 1;
361
+ }
362
+ }
363
+ const topFilesByDuplicates = Object.entries(fileDuplicateCounts)
364
+ .sort((a, b) => b[1] - a[1])
365
+ .slice(0, 20)
366
+ .map(([file, count]) => ({ file, count }));
367
+ const stats = {
368
+ totalGroups,
369
+ totalDuplicates,
370
+ byType,
371
+ affectedFiles,
372
+ topFilesByDuplicates,
373
+ };
365
374
  // Build result based on summaryOnly flag
366
375
  let result;
367
376
  if (summaryOnly) {
368
- // Summary mode: statistics only, no full arrays
369
- const fileDuplicateCounts = {};
370
- for (const group of duplicateGroups) {
371
- for (const item of group.items) {
372
- const shortPath = getShortPath(item.filePath);
373
- fileDuplicateCounts[shortPath] = (fileDuplicateCounts[shortPath] ?? 0) + 1;
374
- }
375
- }
376
- const topFilesByDuplicates = Object.entries(fileDuplicateCounts)
377
- .sort((a, b) => b[1] - a[1])
378
- .slice(0, 20)
379
- .map(([file, count]) => ({ file, count }));
377
+ // Summary mode: statistics only, no groups list
380
378
  result = {
381
379
  summary,
382
- totalGroups,
383
- totalDuplicates,
384
- byType,
385
- affectedFiles,
386
- topFilesByDuplicates,
380
+ ...stats,
387
381
  };
388
382
  }
389
383
  else {
390
- // Paginated mode: apply offset/maxResults
384
+ // Paginated mode: apply offset/maxResults, include stats AND groups
391
385
  const paginatedGroups = duplicateGroups.slice(offset, offset + maxResults);
392
386
  const hasMore = offset + maxResults < duplicateGroups.length;
393
387
  result = {
394
388
  summary,
395
- totalGroups,
396
- totalDuplicates,
397
- byType,
389
+ ...stats,
398
390
  duplicates: paginatedGroups,
399
391
  pagination: {
400
392
  offset,
@@ -402,7 +394,6 @@ export const createDetectDuplicateCodeTool = (server) => {
402
394
  returned: paginatedGroups.length,
403
395
  hasMore,
404
396
  },
405
- affectedFiles,
406
397
  };
407
398
  }
408
399
  // Add pre-computed diagnostic to result
@@ -66,7 +66,7 @@ export const createImpactAnalysisTool = (server) => {
66
66
  .min(1)
67
67
  .max(6)
68
68
  .optional()
69
- .describe('Maximum depth to traverse for transitive dependents (default: 4)')
69
+ .describe('Maximum depth for transitive dependent traversal')
70
70
  .default(4),
71
71
  frameworkConfig: FrameworkConfigSchema.optional().describe('Framework-specific configuration for risk scoring. Includes relationshipWeights (e.g., {"INJECTS": 0.9}), highRiskTypes (e.g., ["Controller", "Service"]), and optional name.'),
72
72
  },
@@ -12,11 +12,12 @@ 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
15
  import { createCleanupSessionTool } from './session-cleanup.tool.js';
17
- import { createRecallSessionNotesTool, createSaveSessionNoteTool } from './session-note.tool.js';
16
+ import { createSessionRecallTool } from './session-recall.tool.js';
17
+ import { createSessionSaveTool } from './session-save.tool.js';
18
18
  import { createStartWatchProjectTool } from './start-watch-project.tool.js';
19
19
  import { createStopWatchProjectTool } from './stop-watch-project.tool.js';
20
+ import { createSwarmAdvanceTaskTool } from './swarm-advance-task.tool.js';
20
21
  import { createSwarmClaimTaskTool } from './swarm-claim-task.tool.js';
21
22
  import { createSwarmCleanupTool } from './swarm-cleanup.tool.js';
22
23
  import { createSwarmCompleteTaskTool } from './swarm-complete-task.tool.js';
@@ -24,6 +25,7 @@ import { createSwarmGetTasksTool } from './swarm-get-tasks.tool.js';
24
25
  import { createSwarmMessageTool } from './swarm-message.tool.js';
25
26
  import { createSwarmPheromoneTool } from './swarm-pheromone.tool.js';
26
27
  import { createSwarmPostTaskTool } from './swarm-post-task.tool.js';
28
+ import { createSwarmReleaseTaskTool } from './swarm-release-task.tool.js';
27
29
  import { createSwarmSenseTool } from './swarm-sense.tool.js';
28
30
  import { createTestNeo4jConnectionTool } from './test-neo4j-connection.tool.js';
29
31
  import { createTraverseFromNodeTool } from './traverse-from-node.tool.js';
@@ -72,16 +74,14 @@ export const registerAllTools = (server) => {
72
74
  // Register swarm task tools (blackboard for explicit task management)
73
75
  createSwarmPostTaskTool(server);
74
76
  createSwarmClaimTaskTool(server);
77
+ createSwarmAdvanceTaskTool(server);
78
+ createSwarmReleaseTaskTool(server);
75
79
  createSwarmCompleteTaskTool(server);
76
80
  createSwarmGetTasksTool(server);
77
81
  // Register swarm messaging tools (direct agent-to-agent communication)
78
82
  createSwarmMessageTool(server);
79
- // Register session bookmark tools (cross-session context continuity)
80
- createSaveSessionBookmarkTool(server);
81
- createRestoreSessionBookmarkTool(server);
82
- // Register session note tools (durable observations and decisions)
83
- createSaveSessionNoteTool(server);
84
- createRecallSessionNotesTool(server);
85
- // Register session cleanup tool
83
+ // Register session tools (unified save/recall + cleanup)
84
+ createSessionSaveTool(server);
85
+ createSessionRecallTool(server);
86
86
  createCleanupSessionTool(server);
87
87
  };
@@ -5,7 +5,7 @@
5
5
  import { LIST_PROJECTS_QUERY } from '../../core/utils/project-id.js';
6
6
  import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
7
7
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
8
- import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
8
+ import { createEmptyResponse, createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
9
9
  export const createListProjectsTool = (server) => {
10
10
  server.registerTool(TOOL_NAMES.listProjects, {
11
11
  title: TOOL_METADATA[TOOL_NAMES.listProjects].title,
@@ -16,7 +16,7 @@ export const createListProjectsTool = (server) => {
16
16
  try {
17
17
  const results = await neo4jService.run(LIST_PROJECTS_QUERY, {});
18
18
  if (results.length === 0) {
19
- return createSuccessResponse('No projects found. Use parse_typescript_project to add a project first.');
19
+ return createEmptyResponse('No projects found in the database', 'Use parse_typescript_project to add a project first.');
20
20
  }
21
21
  const projects = results.map((r) => ({
22
22
  projectId: r.projectId,
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
6
6
  import { watchManager } from '../services/watch-manager.js';
7
- import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
7
+ import { createEmptyResponse, createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
8
8
  export const createListWatchersTool = (server) => {
9
9
  server.registerTool(TOOL_NAMES.listWatchers, {
10
10
  title: TOOL_METADATA[TOOL_NAMES.listWatchers].title,
@@ -14,10 +14,7 @@ export const createListWatchersTool = (server) => {
14
14
  try {
15
15
  const watchers = watchManager.listWatchers();
16
16
  if (watchers.length === 0) {
17
- return createSuccessResponse('No active file watchers.\n\n' +
18
- 'To start watching a project:\n' +
19
- '- Use start_watch_project with a projectId\n' +
20
- '- Or use parse_typescript_project with watch: true (requires async: false)');
17
+ return createEmptyResponse('No active file watchers', 'Use start_watch_project to begin watching, or parse_typescript_project with watch=true.');
21
18
  }
22
19
  const header = `Found ${watchers.length} active watcher(s):\n\n`;
23
20
  const watcherList = watchers
@@ -7,7 +7,7 @@ import { z } from 'zod';
7
7
  import { NaturalLanguageToCypherService } from '../../core/embeddings/natural-language-to-cypher.service.js';
8
8
  import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
9
9
  import { TOOL_NAMES, TOOL_METADATA, FILE_PATHS } from '../constants.js';
10
- import { createErrorResponse, createSuccessResponse, formatQueryResults, debugLog, resolveProjectIdOrError, } from '../utils.js';
10
+ import { createEmptyResponse, createErrorResponse, createSuccessResponse, formatQueryResults, debugLog, resolveProjectIdOrError, } from '../utils.js';
11
11
  // Service instance - initialized asynchronously
12
12
  let naturalLanguageToCypherService = null;
13
13
  /**
@@ -45,7 +45,7 @@ export const createNaturalLanguageToCypherTool = (server) => {
45
45
  const resolvedProjectId = projectResult.projectId;
46
46
  if (!naturalLanguageToCypherService) {
47
47
  await debugLog('Natural language service not available', { projectId: resolvedProjectId, query });
48
- return createSuccessResponse('natural_language_to_cypher requires OPENAI_API_KEY. Set it and restart the MCP server to enable this tool.');
48
+ return createEmptyResponse('natural_language_to_cypher requires OPENAI_API_KEY', 'Set OPENAI_API_KEY and restart the MCP server to enable this tool.');
49
49
  }
50
50
  const cypherResult = await naturalLanguageToCypherService.promptToQuery(query, resolvedProjectId);
51
51
  // Validate Cypher syntax using EXPLAIN (no execution, just parse)