code-graph-context 1.1.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.
Files changed (47) hide show
  1. package/README.md +221 -101
  2. package/dist/core/config/fairsquare-framework-schema.js +47 -60
  3. package/dist/core/config/nestjs-framework-schema.js +11 -1
  4. package/dist/core/config/schema.js +1 -1
  5. package/dist/core/config/timeouts.js +27 -0
  6. package/dist/core/embeddings/embeddings.service.js +122 -2
  7. package/dist/core/embeddings/natural-language-to-cypher.service.js +416 -17
  8. package/dist/core/parsers/parser-factory.js +5 -3
  9. package/dist/core/parsers/typescript-parser.js +614 -45
  10. package/dist/core/parsers/workspace-parser.js +553 -0
  11. package/dist/core/utils/edge-factory.js +37 -0
  12. package/dist/core/utils/file-change-detection.js +105 -0
  13. package/dist/core/utils/file-utils.js +20 -0
  14. package/dist/core/utils/index.js +3 -0
  15. package/dist/core/utils/path-utils.js +75 -0
  16. package/dist/core/utils/progress-reporter.js +112 -0
  17. package/dist/core/utils/project-id.js +176 -0
  18. package/dist/core/utils/retry.js +41 -0
  19. package/dist/core/workspace/index.js +4 -0
  20. package/dist/core/workspace/workspace-detector.js +221 -0
  21. package/dist/mcp/constants.js +153 -5
  22. package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
  23. package/dist/mcp/handlers/file-change-detection.js +105 -0
  24. package/dist/mcp/handlers/graph-generator.handler.js +97 -32
  25. package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
  26. package/dist/mcp/handlers/streaming-import.handler.js +210 -0
  27. package/dist/mcp/handlers/traversal.handler.js +130 -71
  28. package/dist/mcp/mcp.server.js +45 -6
  29. package/dist/mcp/service-init.js +79 -0
  30. package/dist/mcp/services/job-manager.js +165 -0
  31. package/dist/mcp/services/watch-manager.js +376 -0
  32. package/dist/mcp/services.js +2 -2
  33. package/dist/mcp/tools/check-parse-status.tool.js +64 -0
  34. package/dist/mcp/tools/impact-analysis.tool.js +84 -18
  35. package/dist/mcp/tools/index.js +13 -1
  36. package/dist/mcp/tools/list-projects.tool.js +62 -0
  37. package/dist/mcp/tools/list-watchers.tool.js +51 -0
  38. package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
  39. package/dist/mcp/tools/parse-typescript-project.tool.js +318 -58
  40. package/dist/mcp/tools/search-codebase.tool.js +56 -16
  41. package/dist/mcp/tools/start-watch-project.tool.js +100 -0
  42. package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
  43. package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
  44. package/dist/mcp/utils.js +35 -13
  45. package/dist/mcp/workers/parse-worker.js +198 -0
  46. package/dist/storage/neo4j/neo4j.service.js +147 -48
  47. package/package.json +4 -2
@@ -1,5 +1,6 @@
1
1
  import neo4j from 'neo4j-driver';
2
2
  import { MAX_TRAVERSAL_DEPTH } from '../../constants.js';
3
+ import { getTimeoutConfig } from '../../core/config/timeouts.js';
3
4
  export class Neo4jService {
4
5
  driver;
5
6
  constructor() {
@@ -9,20 +10,39 @@ export class Neo4jService {
9
10
  const uri = process.env.NEO4J_URI ?? 'bolt://localhost:7687';
10
11
  const user = process.env.NEO4J_USER ?? 'neo4j';
11
12
  const password = process.env.NEO4J_PASSWORD ?? 'PASSWORD';
12
- return neo4j.driver(uri, neo4j.auth.basic(user, password));
13
+ const timeoutConfig = getTimeoutConfig();
14
+ return neo4j.driver(uri, neo4j.auth.basic(user, password), {
15
+ connectionTimeout: timeoutConfig.neo4j.connectionTimeoutMs,
16
+ maxTransactionRetryTime: timeoutConfig.neo4j.queryTimeoutMs,
17
+ });
13
18
  }
14
19
  async run(query, params = {}) {
15
20
  const session = this.driver.session();
21
+ const timeoutConfig = getTimeoutConfig();
16
22
  try {
17
- const result = await session.run(query, params);
23
+ const result = await session.run(query, params, {
24
+ timeout: timeoutConfig.neo4j.queryTimeoutMs,
25
+ });
18
26
  return result.records.map((record) => record.toObject());
19
27
  }
20
28
  catch (error) {
29
+ // Provide helpful error message for timeout
30
+ if (error.code === 'Neo.TransientError.Transaction.Terminated') {
31
+ throw new Error(`Neo4j query timed out after ${timeoutConfig.neo4j.queryTimeoutMs}ms. ` +
32
+ 'Consider simplifying the query or increasing NEO4J_QUERY_TIMEOUT_MS.');
33
+ }
21
34
  console.error('Error running query:', error);
22
35
  throw error;
23
36
  }
24
37
  finally {
25
- await session.close();
38
+ // Wrap session close in try-catch to avoid masking the original error
39
+ try {
40
+ await session.close();
41
+ }
42
+ catch (closeError) {
43
+ // Log but don't re-throw to preserve original error
44
+ console.warn('Error closing Neo4j session:', closeError);
45
+ }
26
46
  }
27
47
  }
28
48
  getDriver() {
@@ -30,15 +50,34 @@ export class Neo4jService {
30
50
  }
31
51
  async getSchema() {
32
52
  const session = this.driver.session();
53
+ const timeoutConfig = getTimeoutConfig();
33
54
  try {
34
- return await session.run(QUERIES.APOC_SCHEMA);
55
+ return await session.run(QUERIES.APOC_SCHEMA, {}, {
56
+ timeout: timeoutConfig.neo4j.queryTimeoutMs,
57
+ });
35
58
  }
36
59
  catch (error) {
37
60
  console.error('Error fetching schema:', error);
38
61
  throw error;
39
62
  }
40
63
  finally {
41
- await session.close();
64
+ // Wrap session close in try-catch to avoid masking the original error
65
+ try {
66
+ await session.close();
67
+ }
68
+ catch (closeError) {
69
+ // Log but don't re-throw to preserve original error
70
+ console.warn('Error closing Neo4j session:', closeError);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Close the Neo4j driver connection.
76
+ * Should be called when the service is no longer needed to release resources.
77
+ */
78
+ async close() {
79
+ if (this.driver) {
80
+ await this.driver.close();
42
81
  }
43
82
  }
44
83
  }
@@ -47,7 +86,34 @@ export const QUERIES = {
47
86
  CALL apoc.meta.schema() YIELD value
48
87
  RETURN value as schema
49
88
  `,
50
- CLEAR_DATABASE: 'MATCH (n) DETACH DELETE n',
89
+ // Project-scoped deletion - only deletes nodes for the specified project
90
+ // Uses APOC batched deletion to avoid transaction memory limits on large projects
91
+ CLEAR_PROJECT: `
92
+ CALL apoc.periodic.iterate(
93
+ 'MATCH (n) WHERE n.projectId = $projectId RETURN n',
94
+ 'DETACH DELETE n',
95
+ {batchSize: 1000, params: {projectId: $projectId}}
96
+ )
97
+ YIELD batches, total
98
+ RETURN batches, total
99
+ `,
100
+ // Full database clear - use with caution, clears ALL projects
101
+ // Uses APOC batched deletion to avoid transaction memory limits
102
+ CLEAR_DATABASE: `
103
+ CALL apoc.periodic.iterate(
104
+ 'MATCH (n) RETURN n',
105
+ 'DETACH DELETE n',
106
+ {batchSize: 1000}
107
+ )
108
+ YIELD batches, total
109
+ RETURN batches, total
110
+ `,
111
+ // Create indexes on projectId for efficient filtering across key node types
112
+ CREATE_PROJECT_INDEX_EMBEDDED: 'CREATE INDEX project_embedded_idx IF NOT EXISTS FOR (n:Embedded) ON (n.projectId)',
113
+ CREATE_PROJECT_INDEX_SOURCEFILE: 'CREATE INDEX project_sourcefile_idx IF NOT EXISTS FOR (n:SourceFile) ON (n.projectId)',
114
+ // Create composite indexes on projectId + id for efficient lookups
115
+ CREATE_PROJECT_ID_INDEX_EMBEDDED: 'CREATE INDEX project_id_embedded_idx IF NOT EXISTS FOR (n:Embedded) ON (n.projectId, n.id)',
116
+ CREATE_PROJECT_ID_INDEX_SOURCEFILE: 'CREATE INDEX project_id_sourcefile_idx IF NOT EXISTS FOR (n:SourceFile) ON (n.projectId, n.id)',
51
117
  CREATE_NODE: `
52
118
  UNWIND $nodes AS nodeData
53
119
  CALL apoc.create.node(nodeData.labels, nodeData.properties) YIELD node
@@ -55,16 +121,17 @@ export const QUERIES = {
55
121
  `,
56
122
  CREATE_RELATIONSHIP: `
57
123
  UNWIND $edges AS edgeData
58
- MATCH (start) WHERE start.id = edgeData.startNodeId
59
- MATCH (end) WHERE end.id = edgeData.endNodeId
124
+ MATCH (start) WHERE start.id = edgeData.startNodeId AND start.projectId = $projectId
125
+ MATCH (end) WHERE end.id = edgeData.endNodeId AND end.projectId = $projectId
60
126
  WITH start, end, edgeData
61
127
  CALL apoc.create.relationship(start, edgeData.type, edgeData.properties, end) YIELD rel
62
128
  RETURN count(*) as created
63
129
  `,
64
130
  CREATE_INDEX: (label, property) => `CREATE INDEX IF NOT EXISTS FOR (n:${label}) ON (n.${property})`,
65
131
  GET_STATS: `
66
- MATCH (n)
67
- RETURN labels(n)[0] as nodeType, count(*) as count
132
+ MATCH (n)
133
+ WHERE n.projectId = $projectId
134
+ RETURN labels(n)[0] as nodeType, count(*) as count
68
135
  ORDER BY count DESC
69
136
  `,
70
137
  CREATE_EMBEDDED_VECTOR_INDEX: `
@@ -75,9 +142,16 @@ export const QUERIES = {
75
142
  \`vector.similarity_function\`: 'cosine'
76
143
  }}
77
144
  `,
145
+ // Vector search with configurable fetch multiplier for project filtering.
146
+ // fetchMultiplier (default: 10) controls how many extra results to fetch before filtering by projectId.
147
+ // minSimilarity (default: 0.3) filters out low-confidence matches for nonsense queries.
148
+ // Higher values = more accurate results but slower; lower values = faster but may miss results.
78
149
  VECTOR_SEARCH: `
79
- CALL db.index.vector.queryNodes('embedded_nodes_idx', $limit, $embedding)
150
+ CALL db.index.vector.queryNodes('embedded_nodes_idx', toInteger($limit * coalesce($fetchMultiplier, 10)), $embedding)
80
151
  YIELD node, score
152
+ WHERE node.projectId = $projectId AND score >= coalesce($minSimilarity, 0.3)
153
+ WITH node, score
154
+ LIMIT toInteger($limit)
81
155
  RETURN {
82
156
  id: node.id,
83
157
  labels: labels(node),
@@ -93,18 +167,22 @@ export const QUERIES = {
93
167
  `,
94
168
  GET_SOURCE_FILE_TRACKING_INFO: `
95
169
  MATCH (sf:SourceFile)
96
- RETURN sf.filePath AS filePath, sf.mtime AS mtime, sf.size AS size, sf.contentHash AS contentHash
170
+ WHERE sf.projectId = $projectId
171
+ RETURN sf.filePath AS filePath,
172
+ COALESCE(sf.mtime, 0) AS mtime,
173
+ COALESCE(sf.size, 0) AS size,
174
+ COALESCE(sf.contentHash, '') AS contentHash
97
175
  `,
98
176
  // Get cross-file edges before deletion (edges where one endpoint is outside the subgraph)
99
177
  // These will be recreated after import using deterministic IDs
100
178
  GET_CROSS_FILE_EDGES: `
101
179
  MATCH (sf:SourceFile)
102
- WHERE sf.filePath IN $filePaths
180
+ WHERE sf.filePath IN $filePaths AND sf.projectId = $projectId
103
181
  OPTIONAL MATCH (sf)-[*]->(child)
104
182
  WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesToDelete
105
183
  UNWIND nodesToDelete AS n
106
184
  MATCH (n)-[r]-(other)
107
- WHERE NOT other IN nodesToDelete
185
+ WHERE NOT other IN nodesToDelete AND other.projectId = $projectId
108
186
  RETURN DISTINCT
109
187
  startNode(r).id AS startNodeId,
110
188
  endNode(r).id AS endNodeId,
@@ -114,7 +192,7 @@ export const QUERIES = {
114
192
  // Delete source file subgraphs (nodes and all their edges)
115
193
  DELETE_SOURCE_FILE_SUBGRAPHS: `
116
194
  MATCH (sf:SourceFile)
117
- WHERE sf.filePath IN $filePaths
195
+ WHERE sf.filePath IN $filePaths AND sf.projectId = $projectId
118
196
  OPTIONAL MATCH (sf)-[*]->(child)
119
197
  DETACH DELETE sf, child
120
198
  `,
@@ -122,23 +200,21 @@ export const QUERIES = {
122
200
  RECREATE_CROSS_FILE_EDGES: `
123
201
  UNWIND $edges AS edge
124
202
  MATCH (startNode {id: edge.startNodeId})
203
+ WHERE startNode.projectId = $projectId
125
204
  MATCH (endNode {id: edge.endNodeId})
205
+ WHERE endNode.projectId = $projectId
126
206
  CALL apoc.create.relationship(startNode, edge.edgeType, edge.edgeProperties, endNode) YIELD rel
127
207
  RETURN count(rel) AS recreatedCount
128
208
  `,
129
- // Clean up dangling edges (edges pointing to non-existent nodes)
130
- // Run after incremental parse to remove edges to renamed/deleted nodes
131
- CLEANUP_DANGLING_EDGES: `
132
- MATCH ()-[r]->()
133
- WHERE startNode(r) IS NULL OR endNode(r) IS NULL
134
- DELETE r
135
- RETURN count(r) AS deletedCount
136
- `,
209
+ // Note: Dangling edge cleanup is not needed because:
210
+ // 1. DETACH DELETE removes all edges when deleting nodes
211
+ // 2. Edges cannot exist without both endpoints in Neo4j
212
+ // The previous query (WHERE startNode(r) IS NULL OR endNode(r) IS NULL) could never match anything
137
213
  // Get existing nodes (excluding files being reparsed) for edge target matching
138
214
  // Returns minimal info needed for edge detection: id, name, coreType, semanticType
139
215
  GET_EXISTING_NODES_FOR_EDGE_DETECTION: `
140
216
  MATCH (sf:SourceFile)-[*]->(n)
141
- WHERE NOT sf.filePath IN $excludeFilePaths
217
+ WHERE NOT sf.filePath IN $excludeFilePaths AND sf.projectId = $projectId
142
218
  RETURN n.id AS id,
143
219
  n.name AS name,
144
220
  n.coreType AS coreType,
@@ -149,29 +225,45 @@ export const QUERIES = {
149
225
  EXPLORE_ALL_CONNECTIONS: (maxDepth = MAX_TRAVERSAL_DEPTH, direction = 'BOTH', relationshipTypes) => {
150
226
  const safeMaxDepth = Math.min(Math.max(maxDepth, 1), MAX_TRAVERSAL_DEPTH);
151
227
  // Build relationship pattern based on direction
228
+ // For INCOMING, we reverse the match order: (connected)-[*]->(start) instead of (start)<-[*]-(connected)
229
+ // This is because Neo4j variable-length patterns like <-[*1..N]- require ALL edges to point toward start,
230
+ // but in multi-hop paths (A→B→C), intermediate edges (A→B) don't point toward C, causing 0 results.
152
231
  let relPattern = '';
232
+ let isReversed = false;
153
233
  if (direction === 'OUTGOING') {
154
234
  relPattern = `-[*1..${safeMaxDepth}]->`;
155
235
  }
156
236
  else if (direction === 'INCOMING') {
157
- relPattern = `<-[*1..${safeMaxDepth}]-`;
237
+ relPattern = `-[*1..${safeMaxDepth}]->`; // Same pattern as OUTGOING
238
+ isReversed = true; // But we'll reverse start/connected in MATCH
158
239
  }
159
240
  else {
160
241
  relPattern = `-[*1..${safeMaxDepth}]-`;
161
242
  }
162
243
  // Build relationship type filter if specified
244
+ // SECURITY: Validate relationship types to prevent Cypher injection
245
+ // Only allow uppercase letters and underscores (valid Neo4j relationship type format)
163
246
  let relTypeFilter = '';
164
247
  if (relationshipTypes && relationshipTypes.length > 0) {
165
- const types = relationshipTypes.map((t) => `'${t}'`).join(', ');
166
- relTypeFilter = `AND all(rel in relationships(path) WHERE type(rel) IN [${types}])`;
248
+ const validRelTypePattern = /^[A-Z_]+$/;
249
+ const validatedTypes = relationshipTypes.filter((t) => validRelTypePattern.test(t));
250
+ if (validatedTypes.length !== relationshipTypes.length) {
251
+ console.warn('Some relationship types were filtered out due to invalid format. Valid format: uppercase letters and underscores only.');
252
+ }
253
+ if (validatedTypes.length > 0) {
254
+ const types = validatedTypes.map((t) => `'${t}'`).join(', ');
255
+ relTypeFilter = `AND all(rel in relationships(path) WHERE type(rel) IN [${types}])`;
256
+ }
167
257
  }
258
+ // For INCOMING, reverse the match: (connected)-[*]->(start) finds nodes that can REACH start
259
+ const matchPattern = isReversed ? `(connected)${relPattern}(start)` : `(start)${relPattern}(connected)`;
168
260
  return `
169
- MATCH (start) WHERE start.id = $nodeId
261
+ MATCH (start) WHERE start.id = $nodeId AND start.projectId = $projectId
170
262
 
171
263
  CALL {
172
264
  WITH start
173
- MATCH path = (start)${relPattern}(connected)
174
- WHERE connected <> start
265
+ MATCH path = ${matchPattern}
266
+ WHERE connected <> start AND connected.projectId = $projectId
175
267
  ${relTypeFilter}
176
268
  WITH path, connected, length(path) as depth
177
269
 
@@ -267,13 +359,13 @@ export const QUERIES = {
267
359
  return `
268
360
  // Unwind the source nodes we're exploring from
269
361
  UNWIND $sourceNodeIds AS sourceId
270
- MATCH (source) WHERE source.id = sourceId
362
+ MATCH (source) WHERE source.id = sourceId AND source.projectId = $projectId
271
363
 
272
364
  // Find immediate neighbors (exactly 1 hop)
273
365
  MATCH (source)${relPattern}(neighbor)
274
366
 
275
- // Filter: skip already visited nodes to avoid cycles
276
- WHERE NOT neighbor.id IN $visitedNodeIds
367
+ // Filter: skip already visited nodes and ensure same project
368
+ WHERE NOT neighbor.id IN $visitedNodeIds AND neighbor.projectId = $projectId
277
369
 
278
370
  // Calculate the three scoring components
279
371
  WITH source, neighbor, rel,
@@ -326,7 +418,7 @@ export const QUERIES = {
326
418
 
327
419
  // Sort by score and limit to top N per depth
328
420
  ORDER BY combinedScore DESC
329
- LIMIT ${maxNodesPerDepth}
421
+ LIMIT toInteger(${maxNodesPerDepth})
330
422
  `;
331
423
  },
332
424
  // ============================================
@@ -339,13 +431,13 @@ export const QUERIES = {
339
431
  CALL db.labels() YIELD label
340
432
  CALL {
341
433
  WITH label
342
- MATCH (n) WHERE label IN labels(n)
434
+ MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
343
435
  WITH n LIMIT 1
344
436
  RETURN keys(n) AS sampleProperties
345
437
  }
346
438
  CALL {
347
439
  WITH label
348
- MATCH (n) WHERE label IN labels(n)
440
+ MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
349
441
  RETURN count(n) AS nodeCount
350
442
  }
351
443
  RETURN label, nodeCount, sampleProperties
@@ -358,14 +450,14 @@ export const QUERIES = {
358
450
  CALL db.relationshipTypes() YIELD relationshipType
359
451
  CALL {
360
452
  WITH relationshipType
361
- MATCH (a)-[r]->(b) WHERE type(r) = relationshipType
453
+ MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId AND b.projectId = $projectId
362
454
  WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
363
455
  RETURN fromLabel, toLabel
364
456
  LIMIT 10
365
457
  }
366
458
  CALL {
367
459
  WITH relationshipType
368
- MATCH ()-[r]->() WHERE type(r) = relationshipType
460
+ MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId
369
461
  RETURN count(r) AS relCount
370
462
  }
371
463
  RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
@@ -376,7 +468,7 @@ export const QUERIES = {
376
468
  */
377
469
  DISCOVER_SEMANTIC_TYPES: `
378
470
  MATCH (n)
379
- WHERE n.semanticType IS NOT NULL
471
+ WHERE n.semanticType IS NOT NULL AND n.projectId = $projectId
380
472
  WITH n.semanticType AS semanticType, count(*) AS count
381
473
  ORDER BY count DESC
382
474
  RETURN semanticType, count
@@ -386,6 +478,7 @@ export const QUERIES = {
386
478
  */
387
479
  DISCOVER_COMMON_PATTERNS: `
388
480
  MATCH (a)-[r]->(b)
481
+ WHERE a.projectId = $projectId AND b.projectId = $projectId
389
482
  WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
390
483
  WHERE count > 5
391
484
  RETURN fromType, relType, toType, count
@@ -400,7 +493,7 @@ export const QUERIES = {
400
493
  * Get node details by ID
401
494
  */
402
495
  GET_NODE_BY_ID: `
403
- MATCH (n) WHERE n.id = $nodeId
496
+ MATCH (n) WHERE n.id = $nodeId AND n.projectId = $projectId
404
497
  RETURN n.id AS id,
405
498
  n.name AS name,
406
499
  labels(n) AS labels,
@@ -413,9 +506,9 @@ export const QUERIES = {
413
506
  * Based on GET_CROSS_FILE_EDGES pattern but for a single node
414
507
  */
415
508
  GET_NODE_IMPACT: `
416
- MATCH (target) WHERE target.id = $nodeId
509
+ MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
417
510
  MATCH (dependent)-[r]->(target)
418
- WHERE dependent.id <> target.id
511
+ WHERE dependent.id <> target.id AND dependent.projectId = $projectId
419
512
  RETURN DISTINCT
420
513
  dependent.id AS nodeId,
421
514
  dependent.name AS name,
@@ -431,12 +524,17 @@ export const QUERIES = {
431
524
  * Directly reuses GET_CROSS_FILE_EDGES pattern
432
525
  */
433
526
  GET_FILE_IMPACT: `
434
- MATCH (sf:SourceFile {filePath: $filePath})
435
- OPTIONAL MATCH (sf)-[*]->(child)
436
- WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesInFile
437
- UNWIND nodesInFile AS n
527
+ MATCH (sf:SourceFile)
528
+ WHERE sf.projectId = $projectId
529
+ AND (sf.filePath = $filePath OR sf.filePath ENDS WITH '/' + $filePath)
530
+ MATCH (sf)-[:CONTAINS]->(entity)
531
+ WHERE entity:Class OR entity:Function OR entity:Interface
532
+ WITH collect(DISTINCT entity) AS entitiesInFile, sf.filePath AS sourceFilePath
533
+ UNWIND entitiesInFile AS n
438
534
  MATCH (dependent)-[r]->(n)
439
- WHERE NOT dependent IN nodesInFile
535
+ WHERE NOT dependent IN entitiesInFile
536
+ AND dependent.projectId = $projectId
537
+ AND dependent.filePath <> sourceFilePath
440
538
  RETURN DISTINCT
441
539
  dependent.id AS nodeId,
442
540
  dependent.name AS name,
@@ -453,8 +551,9 @@ export const QUERIES = {
453
551
  * Get transitive dependents - nodes that depend on dependents (for deeper impact)
454
552
  */
455
553
  GET_TRANSITIVE_DEPENDENTS: (maxDepth = 4) => `
456
- MATCH (target) WHERE target.id = $nodeId
554
+ MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
457
555
  MATCH path = (dependent)-[*2..${maxDepth}]->(target)
556
+ WHERE dependent.projectId = $projectId AND all(n IN nodes(path) WHERE n.projectId = $projectId)
458
557
  WITH dependent,
459
558
  length(path) AS depth,
460
559
  [r IN relationships(path) | type(r)] AS relationshipPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "MCP server that builds code graphs to provide rich context to LLMs",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/drewdrewH/code-graph-context#readme",
@@ -49,12 +49,15 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^1.15.1",
52
+ "@parcel/watcher": "^2.5.1",
52
53
  "commander": "^14.0.0",
53
54
  "dotenv": "^17.2.3",
54
55
  "glob": "^11.0.3",
55
56
  "neo4j": "^2.0.0-RC2",
56
57
  "neo4j-driver": "^5.28.1",
57
58
  "openai": "^5.10.1",
59
+ "ts-morph": "^26.0.0",
60
+ "yaml": "^2.8.2",
58
61
  "zod": "^3.25.76"
59
62
  },
60
63
  "devDependencies": {
@@ -73,7 +76,6 @@
73
76
  "globals": "^16.2.0",
74
77
  "prettier": "^3.5.3",
75
78
  "reflect-metadata": "^0.2.2",
76
- "ts-morph": "^26.0.0",
77
79
  "ts-node": "^10.9.2",
78
80
  "typescript": "^5.8.3",
79
81
  "typescript-eslint": "^8.34.1",