code-graph-context 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -101
- package/dist/core/config/fairsquare-framework-schema.js +47 -60
- package/dist/core/config/nestjs-framework-schema.js +11 -1
- package/dist/core/config/schema.js +1 -1
- package/dist/core/config/timeouts.js +27 -0
- package/dist/core/embeddings/embeddings.service.js +122 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +428 -30
- package/dist/core/parsers/parser-factory.js +6 -6
- package/dist/core/parsers/typescript-parser.js +639 -44
- package/dist/core/parsers/workspace-parser.js +553 -0
- package/dist/core/utils/edge-factory.js +37 -0
- package/dist/core/utils/file-change-detection.js +105 -0
- package/dist/core/utils/file-utils.js +20 -0
- package/dist/core/utils/index.js +3 -0
- package/dist/core/utils/path-utils.js +75 -0
- package/dist/core/utils/progress-reporter.js +112 -0
- package/dist/core/utils/project-id.js +176 -0
- package/dist/core/utils/retry.js +41 -0
- package/dist/core/workspace/index.js +4 -0
- package/dist/core/workspace/workspace-detector.js +221 -0
- package/dist/mcp/constants.js +172 -7
- package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
- package/dist/mcp/handlers/file-change-detection.js +105 -0
- package/dist/mcp/handlers/graph-generator.handler.js +97 -32
- package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
- package/dist/mcp/handlers/streaming-import.handler.js +210 -0
- package/dist/mcp/handlers/traversal.handler.js +130 -71
- package/dist/mcp/mcp.server.js +46 -7
- package/dist/mcp/service-init.js +79 -0
- package/dist/mcp/services/job-manager.js +165 -0
- package/dist/mcp/services/watch-manager.js +376 -0
- package/dist/mcp/services.js +48 -127
- package/dist/mcp/tools/check-parse-status.tool.js +64 -0
- package/dist/mcp/tools/impact-analysis.tool.js +319 -0
- package/dist/mcp/tools/index.js +15 -1
- package/dist/mcp/tools/list-projects.tool.js +62 -0
- package/dist/mcp/tools/list-watchers.tool.js +51 -0
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
- package/dist/mcp/tools/parse-typescript-project.tool.js +325 -60
- package/dist/mcp/tools/search-codebase.tool.js +57 -23
- package/dist/mcp/tools/start-watch-project.tool.js +100 -0
- package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
- package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
- package/dist/mcp/utils.js +35 -12
- package/dist/mcp/workers/parse-worker.js +198 -0
- package/dist/storage/neo4j/neo4j.service.js +273 -34
- package/package.json +4 -2
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Analysis Tool
|
|
3
|
+
* Analyzes what would be affected if a node is modified
|
|
4
|
+
* Reuses cross-file edge pattern from incremental parsing
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
8
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
9
|
+
import { createErrorResponse, createSuccessResponse, debugLog, resolveProjectIdOrError } from '../utils.js';
|
|
10
|
+
// Default relationship weights for core AST relationships
|
|
11
|
+
const DEFAULT_RELATIONSHIP_WEIGHTS = {
|
|
12
|
+
// Critical - inheritance/interface contracts
|
|
13
|
+
EXTENDS: 0.95,
|
|
14
|
+
IMPLEMENTS: 0.95,
|
|
15
|
+
// High - direct code dependencies
|
|
16
|
+
CALLS: 0.75,
|
|
17
|
+
HAS_MEMBER: 0.65,
|
|
18
|
+
TYPED_AS: 0.6,
|
|
19
|
+
// Medium - module dependencies
|
|
20
|
+
IMPORTS: 0.5,
|
|
21
|
+
EXPORTS: 0.5,
|
|
22
|
+
// Lower - structural
|
|
23
|
+
CONTAINS: 0.3,
|
|
24
|
+
HAS_PARAMETER: 0.3,
|
|
25
|
+
DECORATED_WITH: 0.4,
|
|
26
|
+
};
|
|
27
|
+
// Schema for framework-specific configuration
|
|
28
|
+
const FrameworkConfigSchema = z.object({
|
|
29
|
+
relationshipWeights: z.record(z.string(), z.number().min(0).max(1)).optional(),
|
|
30
|
+
highRiskTypes: z.array(z.string()).optional(),
|
|
31
|
+
name: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
export const createImpactAnalysisTool = (server) => {
|
|
34
|
+
server.registerTool(TOOL_NAMES.impactAnalysis, {
|
|
35
|
+
title: TOOL_METADATA[TOOL_NAMES.impactAnalysis].title,
|
|
36
|
+
description: TOOL_METADATA[TOOL_NAMES.impactAnalysis].description,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
39
|
+
nodeId: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('The node ID to analyze impact for (from search_codebase or traverse_from_node results)'),
|
|
43
|
+
filePath: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Alternatively, provide a file path to analyze all exports from that file'),
|
|
47
|
+
maxDepth: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(6)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Maximum depth to traverse for transitive dependents (default: 4)')
|
|
54
|
+
.default(4),
|
|
55
|
+
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.'),
|
|
56
|
+
},
|
|
57
|
+
}, async ({ projectId, nodeId, filePath, maxDepth = 4, frameworkConfig }) => {
|
|
58
|
+
const neo4jService = new Neo4jService();
|
|
59
|
+
try {
|
|
60
|
+
// Resolve project ID from name, path, or ID
|
|
61
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
62
|
+
if (!projectResult.success)
|
|
63
|
+
return projectResult.error;
|
|
64
|
+
const resolvedProjectId = projectResult.projectId;
|
|
65
|
+
if (!nodeId && !filePath) {
|
|
66
|
+
return createErrorResponse('Either nodeId or filePath must be provided');
|
|
67
|
+
}
|
|
68
|
+
await debugLog('Impact analysis started', {
|
|
69
|
+
projectId: resolvedProjectId,
|
|
70
|
+
nodeId,
|
|
71
|
+
filePath,
|
|
72
|
+
maxDepth,
|
|
73
|
+
frameworkConfig,
|
|
74
|
+
});
|
|
75
|
+
// Merge default weights with framework-specific weights
|
|
76
|
+
const weights = { ...DEFAULT_RELATIONSHIP_WEIGHTS, ...frameworkConfig?.relationshipWeights };
|
|
77
|
+
const highRiskTypes = new Set(frameworkConfig?.highRiskTypes ?? []);
|
|
78
|
+
let targetInfo;
|
|
79
|
+
let directDependents;
|
|
80
|
+
if (nodeId) {
|
|
81
|
+
// Get target node info
|
|
82
|
+
const targetResult = await neo4jService.run(QUERIES.GET_NODE_BY_ID, { nodeId, projectId: resolvedProjectId });
|
|
83
|
+
if (targetResult.length === 0) {
|
|
84
|
+
return createErrorResponse(`Node with ID "${nodeId}" not found in project "${resolvedProjectId}"`);
|
|
85
|
+
}
|
|
86
|
+
const target = targetResult[0];
|
|
87
|
+
targetInfo = {
|
|
88
|
+
id: target.id,
|
|
89
|
+
name: target.name ?? 'Unknown',
|
|
90
|
+
type: target.semanticType ?? target.coreType ?? target.labels?.[0] ?? 'Unknown',
|
|
91
|
+
filePath: target.filePath ?? '',
|
|
92
|
+
};
|
|
93
|
+
// Get direct dependents using cross-file edge pattern
|
|
94
|
+
const directResult = await neo4jService.run(QUERIES.GET_NODE_IMPACT, {
|
|
95
|
+
nodeId,
|
|
96
|
+
projectId: resolvedProjectId,
|
|
97
|
+
});
|
|
98
|
+
directDependents = normalizeDependents(directResult);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// File-based analysis - find all Class/Function/Interface entities in the file
|
|
102
|
+
// and aggregate their impact analysis results
|
|
103
|
+
const entitiesQuery = `
|
|
104
|
+
MATCH (n)
|
|
105
|
+
WHERE n.projectId = $projectId
|
|
106
|
+
AND (n.filePath = $filePath OR n.filePath ENDS WITH '/' + $filePath)
|
|
107
|
+
AND (n:Class OR n:Function OR n:Interface)
|
|
108
|
+
RETURN n.id AS nodeId, n.name AS name, labels(n) AS labels,
|
|
109
|
+
n.semanticType AS semanticType, n.coreType AS coreType
|
|
110
|
+
`;
|
|
111
|
+
const entities = await neo4jService.run(entitiesQuery, {
|
|
112
|
+
filePath,
|
|
113
|
+
projectId: resolvedProjectId,
|
|
114
|
+
});
|
|
115
|
+
if (entities.length === 0) {
|
|
116
|
+
// No exportable entities found
|
|
117
|
+
targetInfo = {
|
|
118
|
+
id: filePath,
|
|
119
|
+
name: filePath.split('/').pop() ?? filePath,
|
|
120
|
+
type: 'SourceFile',
|
|
121
|
+
filePath: filePath,
|
|
122
|
+
};
|
|
123
|
+
directDependents = [];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Use first entity as the primary target for display
|
|
127
|
+
const primaryEntity = entities[0];
|
|
128
|
+
targetInfo = {
|
|
129
|
+
id: primaryEntity.nodeId,
|
|
130
|
+
name: primaryEntity.name ?? filePath.split('/').pop() ?? filePath,
|
|
131
|
+
type: primaryEntity.semanticType ?? primaryEntity.coreType ?? 'Class',
|
|
132
|
+
filePath: filePath,
|
|
133
|
+
};
|
|
134
|
+
// Aggregate impact from all entities in the file
|
|
135
|
+
const allDependentsMap = new Map();
|
|
136
|
+
for (const entity of entities) {
|
|
137
|
+
const entityResult = await neo4jService.run(QUERIES.GET_NODE_IMPACT, {
|
|
138
|
+
nodeId: entity.nodeId,
|
|
139
|
+
projectId: resolvedProjectId,
|
|
140
|
+
});
|
|
141
|
+
for (const dep of normalizeDependents(entityResult)) {
|
|
142
|
+
// Dedupe by nodeId, keeping highest weight
|
|
143
|
+
const existing = allDependentsMap.get(dep.nodeId);
|
|
144
|
+
if (!existing || dep.weight > existing.weight) {
|
|
145
|
+
allDependentsMap.set(dep.nodeId, dep);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
directDependents = Array.from(allDependentsMap.values());
|
|
150
|
+
// Update nodeId for transitive analysis if we have dependents
|
|
151
|
+
if (directDependents.length > 0 && entities.length > 0) {
|
|
152
|
+
// Use first entity's nodeId for transitive analysis
|
|
153
|
+
nodeId = primaryEntity.nodeId;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Get transitive dependents if nodeId provided
|
|
158
|
+
let transitiveDependents = [];
|
|
159
|
+
if (nodeId && maxDepth > 1) {
|
|
160
|
+
const transitiveResult = await neo4jService.run(QUERIES.GET_TRANSITIVE_DEPENDENTS(maxDepth), {
|
|
161
|
+
nodeId,
|
|
162
|
+
projectId: resolvedProjectId,
|
|
163
|
+
});
|
|
164
|
+
transitiveDependents = normalizeTransitiveDependents(transitiveResult);
|
|
165
|
+
// Filter out direct dependents from transitive
|
|
166
|
+
const directIds = new Set(directDependents.map((d) => d.nodeId));
|
|
167
|
+
transitiveDependents = transitiveDependents.filter((d) => !directIds.has(d.nodeId));
|
|
168
|
+
}
|
|
169
|
+
// Calculate risk score
|
|
170
|
+
const riskScore = calculateRiskScore(directDependents, transitiveDependents, weights, highRiskTypes);
|
|
171
|
+
const riskLevel = getRiskLevel(riskScore);
|
|
172
|
+
// Group dependents by type
|
|
173
|
+
const directByType = groupByType(directDependents);
|
|
174
|
+
const directByRelationship = groupByRelationship(directDependents);
|
|
175
|
+
const transitiveByType = groupByType(transitiveDependents);
|
|
176
|
+
// Get affected files
|
|
177
|
+
const affectedFiles = getAffectedFiles([...directDependents, ...transitiveDependents]);
|
|
178
|
+
// Find critical paths (high-weight relationships)
|
|
179
|
+
const criticalPaths = findCriticalPaths(directDependents, targetInfo, weights);
|
|
180
|
+
// Build summary
|
|
181
|
+
const summary = buildSummary(targetInfo, directDependents.length, transitiveDependents.length, affectedFiles.length, riskLevel);
|
|
182
|
+
const result = {
|
|
183
|
+
target: targetInfo,
|
|
184
|
+
riskLevel,
|
|
185
|
+
riskScore: Math.round(riskScore * 100) / 100,
|
|
186
|
+
summary,
|
|
187
|
+
directDependents: {
|
|
188
|
+
count: directDependents.length,
|
|
189
|
+
byType: directByType,
|
|
190
|
+
byRelationship: directByRelationship,
|
|
191
|
+
},
|
|
192
|
+
transitiveDependents: {
|
|
193
|
+
count: transitiveDependents.length,
|
|
194
|
+
maxDepth: getMaxDepth(transitiveDependents),
|
|
195
|
+
byType: transitiveByType,
|
|
196
|
+
},
|
|
197
|
+
affectedFiles,
|
|
198
|
+
criticalPaths,
|
|
199
|
+
};
|
|
200
|
+
await debugLog('Impact analysis complete', {
|
|
201
|
+
nodeId: nodeId ?? filePath,
|
|
202
|
+
riskLevel,
|
|
203
|
+
directCount: directDependents.length,
|
|
204
|
+
transitiveCount: transitiveDependents.length,
|
|
205
|
+
});
|
|
206
|
+
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error('Impact analysis error:', error);
|
|
210
|
+
await debugLog('Impact analysis error', { nodeId, filePath, error });
|
|
211
|
+
return createErrorResponse(error);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
await neo4jService.close();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
// Helper functions
|
|
219
|
+
const normalizeDependents = (results) => {
|
|
220
|
+
return results.map((r) => ({
|
|
221
|
+
nodeId: r.nodeId,
|
|
222
|
+
name: r.name ?? 'Unknown',
|
|
223
|
+
labels: r.labels ?? [],
|
|
224
|
+
semanticType: r.semanticType,
|
|
225
|
+
coreType: r.coreType,
|
|
226
|
+
filePath: r.filePath ?? '',
|
|
227
|
+
relationshipType: r.relationshipType ?? 'UNKNOWN',
|
|
228
|
+
weight: typeof r.weight === 'object'
|
|
229
|
+
? r.weight.toNumber()
|
|
230
|
+
: (r.weight ?? 0.5),
|
|
231
|
+
}));
|
|
232
|
+
};
|
|
233
|
+
const normalizeTransitiveDependents = (results) => {
|
|
234
|
+
return results.map((r) => ({
|
|
235
|
+
nodeId: r.nodeId,
|
|
236
|
+
name: r.name ?? 'Unknown',
|
|
237
|
+
labels: r.labels ?? [],
|
|
238
|
+
semanticType: r.semanticType,
|
|
239
|
+
coreType: r.coreType,
|
|
240
|
+
filePath: r.filePath ?? '',
|
|
241
|
+
relationshipType: r.relationshipPath?.[0] ?? 'UNKNOWN',
|
|
242
|
+
weight: 0.5,
|
|
243
|
+
depth: typeof r.depth === 'object' ? r.depth.toNumber() : r.depth,
|
|
244
|
+
relationshipPath: r.relationshipPath,
|
|
245
|
+
}));
|
|
246
|
+
};
|
|
247
|
+
const calculateRiskScore = (directDependents, transitiveDependents, weights, highRiskTypes) => {
|
|
248
|
+
if (directDependents.length === 0)
|
|
249
|
+
return 0;
|
|
250
|
+
let score = 0;
|
|
251
|
+
// Factor 1: Number of direct dependents (logarithmic, max 0.3)
|
|
252
|
+
score += Math.min(Math.log10(directDependents.length + 1) / 2, 0.3);
|
|
253
|
+
// Factor 2: Average relationship weight of direct deps (max 0.3)
|
|
254
|
+
const avgWeight = directDependents.reduce((sum, d) => sum + (weights[d.relationshipType] ?? d.weight), 0) / directDependents.length;
|
|
255
|
+
score += avgWeight * 0.3;
|
|
256
|
+
// Factor 3: High-risk types affected (max 0.2)
|
|
257
|
+
const highRiskCount = directDependents.filter((d) => highRiskTypes.has(d.semanticType ?? '') || highRiskTypes.has(d.coreType ?? '')).length;
|
|
258
|
+
if (highRiskTypes.size > 0) {
|
|
259
|
+
score += Math.min(highRiskCount / Math.max(highRiskTypes.size, 3), 1) * 0.2;
|
|
260
|
+
}
|
|
261
|
+
// Factor 4: Transitive impact (max 0.2)
|
|
262
|
+
score += Math.min(Math.log10(transitiveDependents.length + 1) / 3, 0.2);
|
|
263
|
+
return Math.min(score, 1);
|
|
264
|
+
};
|
|
265
|
+
const getRiskLevel = (score) => {
|
|
266
|
+
if (score >= 0.75)
|
|
267
|
+
return 'CRITICAL';
|
|
268
|
+
if (score >= 0.5)
|
|
269
|
+
return 'HIGH';
|
|
270
|
+
if (score >= 0.25)
|
|
271
|
+
return 'MEDIUM';
|
|
272
|
+
return 'LOW';
|
|
273
|
+
};
|
|
274
|
+
const groupByType = (dependents) => {
|
|
275
|
+
const groups = {};
|
|
276
|
+
for (const dep of dependents) {
|
|
277
|
+
const type = dep.semanticType ?? dep.coreType ?? dep.labels?.[0] ?? 'Unknown';
|
|
278
|
+
groups[type] = (groups[type] ?? 0) + 1;
|
|
279
|
+
}
|
|
280
|
+
return groups;
|
|
281
|
+
};
|
|
282
|
+
const groupByRelationship = (dependents) => {
|
|
283
|
+
const groups = {};
|
|
284
|
+
for (const dep of dependents) {
|
|
285
|
+
groups[dep.relationshipType] = (groups[dep.relationshipType] ?? 0) + 1;
|
|
286
|
+
}
|
|
287
|
+
return groups;
|
|
288
|
+
};
|
|
289
|
+
const getAffectedFiles = (dependents) => {
|
|
290
|
+
const files = new Set();
|
|
291
|
+
for (const dep of dependents) {
|
|
292
|
+
if (dep.filePath)
|
|
293
|
+
files.add(dep.filePath);
|
|
294
|
+
}
|
|
295
|
+
return Array.from(files).sort();
|
|
296
|
+
};
|
|
297
|
+
const getMaxDepth = (dependents) => {
|
|
298
|
+
if (dependents.length === 0)
|
|
299
|
+
return 0;
|
|
300
|
+
return Math.max(...dependents.map((d) => d.depth ?? 1));
|
|
301
|
+
};
|
|
302
|
+
const findCriticalPaths = (directDependents, target, weights) => {
|
|
303
|
+
const paths = [];
|
|
304
|
+
for (const dep of directDependents) {
|
|
305
|
+
const relWeight = weights[dep.relationshipType] ?? 0.5;
|
|
306
|
+
// Only include high-weight relationships
|
|
307
|
+
if (relWeight >= 0.6) {
|
|
308
|
+
const depType = dep.semanticType ?? dep.coreType ?? '';
|
|
309
|
+
paths.push(`${dep.name} (${depType}) -[${dep.relationshipType}]-> ${target.name} (${target.type})`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return paths.slice(0, 10);
|
|
313
|
+
};
|
|
314
|
+
const buildSummary = (target, directCount, transitiveCount, fileCount, riskLevel) => {
|
|
315
|
+
if (directCount === 0) {
|
|
316
|
+
return `${target.name} (${target.type}) has no external dependents - safe to modify`;
|
|
317
|
+
}
|
|
318
|
+
return `Modifying ${target.name} (${target.type}) affects ${directCount} direct and ${transitiveCount} transitive dependents across ${fileCount} files. Risk: ${riskLevel}`;
|
|
319
|
+
};
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
* MCP Tool Factory
|
|
3
3
|
* Centralized tool creation and registration
|
|
4
4
|
*/
|
|
5
|
+
import { createCheckParseStatusTool } from './check-parse-status.tool.js';
|
|
5
6
|
import { createHelloTool } from './hello.tool.js';
|
|
7
|
+
import { createImpactAnalysisTool } from './impact-analysis.tool.js';
|
|
8
|
+
import { createListProjectsTool } from './list-projects.tool.js';
|
|
9
|
+
import { createListWatchersTool } from './list-watchers.tool.js';
|
|
6
10
|
import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.tool.js';
|
|
7
11
|
import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
|
|
8
12
|
import { createSearchCodebaseTool } from './search-codebase.tool.js';
|
|
13
|
+
import { createStartWatchProjectTool } from './start-watch-project.tool.js';
|
|
14
|
+
import { createStopWatchProjectTool } from './stop-watch-project.tool.js';
|
|
9
15
|
import { createTestNeo4jConnectionTool } from './test-neo4j-connection.tool.js';
|
|
10
16
|
import { createTraverseFromNodeTool } from './traverse-from-node.tool.js';
|
|
11
17
|
/**
|
|
@@ -19,6 +25,14 @@ export const registerAllTools = (server) => {
|
|
|
19
25
|
createSearchCodebaseTool(server);
|
|
20
26
|
createTraverseFromNodeTool(server);
|
|
21
27
|
createNaturalLanguageToCypherTool(server);
|
|
22
|
-
|
|
28
|
+
createImpactAnalysisTool(server);
|
|
29
|
+
// Register project parsing tools
|
|
23
30
|
createParseTypescriptProjectTool(server);
|
|
31
|
+
createCheckParseStatusTool(server);
|
|
32
|
+
// Register project management tools
|
|
33
|
+
createListProjectsTool(server);
|
|
34
|
+
// Register file watch tools
|
|
35
|
+
createStartWatchProjectTool(server);
|
|
36
|
+
createStopWatchProjectTool(server);
|
|
37
|
+
createListWatchersTool(server);
|
|
24
38
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Projects Tool
|
|
3
|
+
* Lists all parsed projects in the database
|
|
4
|
+
*/
|
|
5
|
+
import { LIST_PROJECTS_QUERY } from '../../core/utils/project-id.js';
|
|
6
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
|
+
import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
|
|
9
|
+
export const createListProjectsTool = (server) => {
|
|
10
|
+
server.registerTool(TOOL_NAMES.listProjects, {
|
|
11
|
+
title: TOOL_METADATA[TOOL_NAMES.listProjects].title,
|
|
12
|
+
description: TOOL_METADATA[TOOL_NAMES.listProjects].description,
|
|
13
|
+
inputSchema: {},
|
|
14
|
+
}, async () => {
|
|
15
|
+
const neo4jService = new Neo4jService();
|
|
16
|
+
try {
|
|
17
|
+
await debugLog('Listing projects');
|
|
18
|
+
const results = await neo4jService.run(LIST_PROJECTS_QUERY, {});
|
|
19
|
+
if (results.length === 0) {
|
|
20
|
+
return createSuccessResponse('No projects found. Use parse_typescript_project to add a project first.');
|
|
21
|
+
}
|
|
22
|
+
const projects = results.map((r) => ({
|
|
23
|
+
projectId: r.projectId,
|
|
24
|
+
name: r.name,
|
|
25
|
+
path: r.path,
|
|
26
|
+
status: r.status ?? 'unknown',
|
|
27
|
+
nodeCount: r.nodeCount,
|
|
28
|
+
edgeCount: r.edgeCount,
|
|
29
|
+
updatedAt: r.updatedAt?.toString() ?? 'Unknown',
|
|
30
|
+
}));
|
|
31
|
+
await debugLog('Projects listed', { count: projects.length });
|
|
32
|
+
// Format output for readability
|
|
33
|
+
const header = `Found ${projects.length} project(s):\n\n`;
|
|
34
|
+
const formatStats = (p) => {
|
|
35
|
+
if (p.status === 'complete' && p.nodeCount !== null) {
|
|
36
|
+
return ` Stats: ${p.nodeCount} nodes, ${p.edgeCount ?? 0} edges`;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
};
|
|
40
|
+
const projectList = projects
|
|
41
|
+
.map((p) => `- ${p.name} [${p.status}]\n` +
|
|
42
|
+
` ID: ${p.projectId}\n` +
|
|
43
|
+
` Path: ${p.path}\n` +
|
|
44
|
+
formatStats(p) +
|
|
45
|
+
(formatStats(p) ? '\n' : '') +
|
|
46
|
+
` Updated: ${p.updatedAt}`)
|
|
47
|
+
.join('\n\n');
|
|
48
|
+
const tip = '\n\nTip: Use the project name (e.g., "' +
|
|
49
|
+
projects[0].name +
|
|
50
|
+
'") in other tools instead of the full projectId.';
|
|
51
|
+
return createSuccessResponse(header + projectList + tip);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error('List projects error:', error);
|
|
55
|
+
await debugLog('List projects error', { error });
|
|
56
|
+
return createErrorResponse(error);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await neo4jService.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Watchers Tool
|
|
3
|
+
* Lists all active file watchers
|
|
4
|
+
*/
|
|
5
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
6
|
+
import { watchManager } from '../services/watch-manager.js';
|
|
7
|
+
import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
|
|
8
|
+
export const createListWatchersTool = (server) => {
|
|
9
|
+
server.registerTool(TOOL_NAMES.listWatchers, {
|
|
10
|
+
title: TOOL_METADATA[TOOL_NAMES.listWatchers].title,
|
|
11
|
+
description: TOOL_METADATA[TOOL_NAMES.listWatchers].description,
|
|
12
|
+
inputSchema: {},
|
|
13
|
+
}, async () => {
|
|
14
|
+
try {
|
|
15
|
+
await debugLog('Listing watchers');
|
|
16
|
+
const watchers = watchManager.listWatchers();
|
|
17
|
+
if (watchers.length === 0) {
|
|
18
|
+
return createSuccessResponse('No active file watchers.\n\n' +
|
|
19
|
+
'To start watching a project:\n' +
|
|
20
|
+
'- Use start_watch_project with a projectId\n' +
|
|
21
|
+
'- Or use parse_typescript_project with watch: true (requires async: false)');
|
|
22
|
+
}
|
|
23
|
+
await debugLog('Watchers listed', { count: watchers.length });
|
|
24
|
+
const header = `Found ${watchers.length} active watcher(s):\n\n`;
|
|
25
|
+
const watcherList = watchers
|
|
26
|
+
.map((w) => {
|
|
27
|
+
const lines = [
|
|
28
|
+
`- ${w.projectId} [${w.status}]`,
|
|
29
|
+
` Path: ${w.projectPath}`,
|
|
30
|
+
` Debounce: ${w.debounceMs}ms`,
|
|
31
|
+
` Pending changes: ${w.pendingChanges}`,
|
|
32
|
+
];
|
|
33
|
+
if (w.lastUpdateTime) {
|
|
34
|
+
lines.push(` Last update: ${w.lastUpdateTime}`);
|
|
35
|
+
}
|
|
36
|
+
if (w.errorMessage) {
|
|
37
|
+
lines.push(` Error: ${w.errorMessage}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
})
|
|
41
|
+
.join('\n\n');
|
|
42
|
+
const tip = '\n\nUse stop_watch_project with a project ID to stop watching.';
|
|
43
|
+
return createSuccessResponse(header + watcherList + tip);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('List watchers error:', error);
|
|
47
|
+
await debugLog('List watchers error', { error });
|
|
48
|
+
return createErrorResponse(error instanceof Error ? error : new Error(String(error)));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -6,7 +6,7 @@ import { z } from 'zod';
|
|
|
6
6
|
import { NaturalLanguageToCypherService } from '../../core/embeddings/natural-language-to-cypher.service.js';
|
|
7
7
|
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
8
8
|
import { TOOL_NAMES, TOOL_METADATA, MESSAGES } from '../constants.js';
|
|
9
|
-
import { createErrorResponse, createSuccessResponse, formatQueryResults, debugLog } from '../utils.js';
|
|
9
|
+
import { createErrorResponse, createSuccessResponse, formatQueryResults, debugLog, resolveProjectIdOrError, } from '../utils.js';
|
|
10
10
|
// Service instance - initialized asynchronously
|
|
11
11
|
let naturalLanguageToCypherService = null;
|
|
12
12
|
/**
|
|
@@ -30,20 +30,43 @@ export const createNaturalLanguageToCypherTool = (server) => {
|
|
|
30
30
|
title: TOOL_METADATA[TOOL_NAMES.naturalLanguageToCypher].title,
|
|
31
31
|
description: TOOL_METADATA[TOOL_NAMES.naturalLanguageToCypher].description,
|
|
32
32
|
inputSchema: {
|
|
33
|
+
projectId: z.string().describe('Project ID, name, or path (e.g., "backend" or "proj_a1b2c3d4e5f6")'),
|
|
33
34
|
query: z.string().describe('Natural language query to convert to Cypher'),
|
|
34
35
|
},
|
|
35
|
-
}, async ({ query }) => {
|
|
36
|
+
}, async ({ projectId, query }) => {
|
|
37
|
+
const neo4jService = new Neo4jService();
|
|
36
38
|
try {
|
|
39
|
+
// Resolve project ID from name, path, or ID
|
|
40
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
41
|
+
if (!projectResult.success)
|
|
42
|
+
return projectResult.error;
|
|
43
|
+
const resolvedProjectId = projectResult.projectId;
|
|
37
44
|
if (!naturalLanguageToCypherService) {
|
|
38
|
-
await debugLog('Natural language service not available', { query });
|
|
45
|
+
await debugLog('Natural language service not available', { projectId: resolvedProjectId, query });
|
|
39
46
|
return createSuccessResponse(MESSAGES.errors.serviceNotInitialized);
|
|
40
47
|
}
|
|
41
|
-
await debugLog('Natural language to Cypher conversion started', { query });
|
|
42
|
-
const cypherResult = await naturalLanguageToCypherService.promptToQuery(query);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
await debugLog('Natural language to Cypher conversion started', { projectId: resolvedProjectId, query });
|
|
49
|
+
const cypherResult = await naturalLanguageToCypherService.promptToQuery(query, resolvedProjectId);
|
|
50
|
+
// Validate Cypher syntax using EXPLAIN (no execution, just parse)
|
|
51
|
+
const parameters = { ...cypherResult.parameters, projectId: resolvedProjectId };
|
|
52
|
+
try {
|
|
53
|
+
await neo4jService.run(`EXPLAIN ${cypherResult.cypher}`, parameters);
|
|
54
|
+
}
|
|
55
|
+
catch (validationError) {
|
|
56
|
+
const message = validationError instanceof Error ? validationError.message : String(validationError);
|
|
57
|
+
await debugLog('Generated Cypher validation failed', {
|
|
58
|
+
cypher: cypherResult.cypher,
|
|
59
|
+
error: message,
|
|
60
|
+
});
|
|
61
|
+
return createErrorResponse(`Generated Cypher query has syntax errors:\n\n` +
|
|
62
|
+
`Query: ${cypherResult.cypher}\n\n` +
|
|
63
|
+
`Error: ${message}\n\n` +
|
|
64
|
+
`Try rephrasing your request or use a simpler query.`);
|
|
65
|
+
}
|
|
66
|
+
// Execute the validated query
|
|
67
|
+
const results = await neo4jService.run(cypherResult.cypher, parameters);
|
|
46
68
|
await debugLog('Cypher query executed', {
|
|
69
|
+
projectId: resolvedProjectId,
|
|
47
70
|
cypher: cypherResult.cypher,
|
|
48
71
|
resultsCount: results.length,
|
|
49
72
|
});
|
|
@@ -55,5 +78,8 @@ export const createNaturalLanguageToCypherTool = (server) => {
|
|
|
55
78
|
await debugLog('Natural language to Cypher error', { query, error });
|
|
56
79
|
return createErrorResponse(error);
|
|
57
80
|
}
|
|
81
|
+
finally {
|
|
82
|
+
await neo4jService.close();
|
|
83
|
+
}
|
|
58
84
|
});
|
|
59
85
|
};
|