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.
- package/README.md +2 -2
- 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 +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
|
|
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
|
|
98
|
-
|
|
99
|
-
.array(z.string())
|
|
97
|
+
projectId: z
|
|
98
|
+
.string()
|
|
100
99
|
.optional()
|
|
101
|
-
.describe('
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
111
|
+
.describe('Minimum confidence threshold for results')
|
|
116
112
|
.default('LOW'),
|
|
117
|
-
summaryOnly: z
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
108
|
+
.describe('Minimum similarity threshold for semantic duplicates')
|
|
106
109
|
.default(0.8),
|
|
107
|
-
includeCode: z
|
|
108
|
-
|
|
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
|
|
115
|
+
.describe('Node types to analyze')
|
|
124
116
|
.default('all'),
|
|
125
117
|
summaryOnly: z
|
|
126
118
|
.boolean()
|
|
127
119
|
.optional()
|
|
128
|
-
.describe('Return
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
},
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|