code-graph-context 2.13.3 → 2.14.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 CHANGED
@@ -162,7 +162,7 @@ If you prefer to edit the config files directly:
162
162
  "code-graph-context": {
163
163
  "command": "code-graph-context",
164
164
  "env": {
165
- "OPENAI_ENABLED": "true",
165
+ "OPENAI_EMBEDDINGS_ENABLED": "true",
166
166
  "OPENAI_API_KEY": "sk-your-key-here"
167
167
  }
168
168
  }
@@ -194,8 +194,8 @@ If you prefer to edit the config files directly:
194
194
  | `EMBEDDING_SIDECAR_PORT` | No | `8787` | Port for local embedding server |
195
195
  | `EMBEDDING_DEVICE` | No | auto (`mps`/`cpu`) | Device for embeddings. Auto-detects MPS on Apple Silicon |
196
196
  | `EMBEDDING_HALF_PRECISION` | No | `false` | Set `true` for float16 (uses ~0.5x memory) |
197
- | `OPENAI_ENABLED` | No | `false` | Set `true` to use OpenAI instead of local |
198
- | `OPENAI_API_KEY` | No* | - | Required when `OPENAI_ENABLED=true` |
197
+ | `OPENAI_EMBEDDINGS_ENABLED` | No | `false` | Set `true` to use OpenAI instead of local embeddings |
198
+ | `OPENAI_API_KEY` | No* | - | Required when `OPENAI_EMBEDDINGS_ENABLED=true`; also enables `natural_language_to_cypher` |
199
199
 
200
200
  ---
201
201
 
@@ -582,7 +582,7 @@ If you prefer OpenAI embeddings (higher quality, requires API key):
582
582
 
583
583
  ```bash
584
584
  claude mcp add --scope user code-graph-context \
585
- -e OPENAI_ENABLED=true \
585
+ -e OPENAI_EMBEDDINGS_ENABLED=true \
586
586
  -e OPENAI_API_KEY=sk-your-key-here \
587
587
  -- code-graph-context
588
588
  ```
@@ -626,7 +626,7 @@ claude mcp add --scope user code-graph-context \
626
626
  ```bash
627
627
  claude mcp remove code-graph-context
628
628
  claude mcp add --scope user code-graph-context \
629
- -e OPENAI_ENABLED=true \
629
+ -e OPENAI_EMBEDDINGS_ENABLED=true \
630
630
  -e OPENAI_API_KEY=sk-your-key-here \
631
631
  -- code-graph-context
632
632
  ```
package/dist/cli/cli.js CHANGED
@@ -82,7 +82,7 @@ ${c.bold}Next steps:${c.reset}
82
82
 
83
83
  ${c.dim}Local embeddings are used by default (no API key needed).
84
84
  To use OpenAI instead, add:
85
- "OPENAI_ENABLED": "true",
85
+ "OPENAI_EMBEDDINGS_ENABLED": "true",
86
86
  "OPENAI_API_KEY": "sk-..."${c.reset}
87
87
 
88
88
  3. Restart Claude Code
@@ -199,7 +199,7 @@ const setupSidecar = async () => {
199
199
  if (!pythonVersion) {
200
200
  log(sym.err, 'Python 3 is not installed');
201
201
  console.log(`\n Install Python 3.10+: ${c.cyan}https://www.python.org/downloads/${c.reset}`);
202
- console.log(` ${c.dim}Or use OpenAI embeddings instead: set OPENAI_ENABLED=true${c.reset}\n`);
202
+ console.log(` ${c.dim}Or use OpenAI embeddings instead: set OPENAI_EMBEDDINGS_ENABLED=true${c.reset}\n`);
203
203
  return;
204
204
  }
205
205
  log(sym.ok, `${pythonVersion}`);
@@ -250,7 +250,7 @@ const setupSidecar = async () => {
250
250
  verifySpinner.stop(verified, verified ? 'sentence-transformers OK' : 'sentence-transformers import failed');
251
251
  if (!verified) {
252
252
  console.log(`\n ${c.dim}Try: ${python} -c "from sentence_transformers import SentenceTransformer"${c.reset}`);
253
- console.log(` ${c.dim}Or use OpenAI embeddings instead: set OPENAI_ENABLED=true${c.reset}\n`);
253
+ console.log(` ${c.dim}Or use OpenAI embeddings instead: set OPENAI_EMBEDDINGS_ENABLED=true${c.reset}\n`);
254
254
  return;
255
255
  }
256
256
  // Pre-download the embedding model so first real use is fast
@@ -160,7 +160,7 @@ export class EmbeddingSidecar {
160
160
  reject(new Error('python3 not found. Local embeddings require Python 3.10+.\n\n' +
161
161
  'Install Python and the sidecar dependencies:\n' +
162
162
  ' pip install -r sidecar/requirements.txt\n\n' +
163
- 'Or set OPENAI_ENABLED=true to use OpenAI instead.'));
163
+ 'Or set OPENAI_EMBEDDINGS_ENABLED=true to use OpenAI instead.'));
164
164
  });
165
165
  check.on('close', (code) => {
166
166
  if (code !== 0) {
@@ -2,10 +2,10 @@
2
2
  * Embeddings Service — barrel module
3
3
  *
4
4
  * Exports a common interface and a factory. Consumers do `new EmbeddingsService()`
5
- * and get the right implementation based on OPENAI_ENABLED.
5
+ * and get the right implementation based on OPENAI_EMBEDDINGS_ENABLED.
6
6
  *
7
- * OPENAI_ENABLED=true → OpenAI text-embedding-3-large (requires OPENAI_API_KEY)
8
- * default → Local Python sidecar with Qwen3-Embedding-0.6B
7
+ * OPENAI_EMBEDDINGS_ENABLED=true → OpenAI text-embedding-3-large (requires OPENAI_API_KEY)
8
+ * default → Local Python sidecar with Qwen3-Embedding-0.6B
9
9
  */
10
10
  import { LocalEmbeddingsService } from './local-embeddings.service.js';
11
11
  import { OpenAIEmbeddingsService } from './openai-embeddings.service.js';
@@ -33,8 +33,21 @@ export const EMBEDDING_DIMENSIONS = {
33
33
  'nomic-ai/nomic-embed-text-v1.5': 768,
34
34
  };
35
35
  export const isOpenAIEnabled = () => {
36
- return process.env.OPENAI_ENABLED?.toLowerCase() === 'true';
36
+ if (process.env.OPENAI_EMBEDDINGS_ENABLED?.toLowerCase() === 'true') {
37
+ return true;
38
+ }
39
+ // Backward-compat: OPENAI_ENABLED is deprecated in favour of OPENAI_EMBEDDINGS_ENABLED
40
+ if (process.env.OPENAI_ENABLED?.toLowerCase() === 'true') {
41
+ console.error(JSON.stringify({
42
+ level: 'warn',
43
+ message: '[code-graph-context] OPENAI_ENABLED is deprecated. Use OPENAI_EMBEDDINGS_ENABLED=true instead.',
44
+ }));
45
+ return true;
46
+ }
47
+ return false;
37
48
  };
49
+ /** Returns true when OPENAI_API_KEY is present, regardless of embedding provider. */
50
+ export const isOpenAIAvailable = () => !!process.env.OPENAI_API_KEY;
38
51
  /**
39
52
  * Get the vector dimensions for the active embedding provider.
40
53
  * For known models, returns a static value. For unknown local models,
@@ -50,7 +63,7 @@ export const getEmbeddingDimensions = () => {
50
63
  return EMBEDDING_DIMENSIONS[model] ?? 1536;
51
64
  };
52
65
  /**
53
- * Factory that returns the correct service based on OPENAI_ENABLED.
66
+ * Factory that returns the correct service based on OPENAI_EMBEDDINGS_ENABLED.
54
67
  * Drop-in replacement everywhere `new EmbeddingsService()` was used.
55
68
  */
56
69
  export class EmbeddingsService {
@@ -7,176 +7,31 @@ export class NaturalLanguageToCypherService {
7
7
  MODEL = 'gpt-4o'; // GPT-4o for better Cypher generation accuracy
8
8
  schemaPath = null;
9
9
  cachedSemanticTypes = null;
10
- messageInstructions = `
11
- === THE SCHEMA FILE IS THE SOURCE OF TRUTH ===
12
- ALWAYS read neo4j-apoc-schema.json FIRST before generating any query. It contains:
13
- 1. rawSchema: All node labels (keys), their properties, and relationships from Neo4j APOC
14
- 2. discoveredSchema (if available): Dynamically discovered nodeTypes, relationshipTypes, semanticTypes, commonPatterns
15
-
16
- === LABEL TYPES - TWO CATEGORIES ===
17
- Check rawSchema keys for ALL valid labels. Labels fall into two categories:
18
-
19
- 1. CORE LABELS (base TypeScript AST):
20
- SourceFile, Class, Function, Method, Interface, Property, Parameter, Constructor, Import, Export, Decorator, Enum, Variable, TypeAlias
21
-
22
- 2. FRAMEWORK LABELS (from framework enhancements - check rawSchema keys):
23
- These REPLACE the core label for enhanced nodes. Check rawSchema keys for available framework labels in this project.
24
- A node with a framework label was originally a Class but got enhanced - always use the actual label from rawSchema.
25
-
26
- === AST TYPE NAME MAPPING ===
27
- AST type names are NOT valid labels. Always map them:
28
- - ClassDeclaration Class (or a framework label from rawSchema if enhanced)
29
- - FunctionDeclarationFunction
30
- - MethodDeclaration Method
31
- - InterfaceDeclaration Interface
32
- - PropertyDeclaration → Property
33
- - ParameterDeclaration → Parameter
34
-
35
- === FINDING SPECIFIC NODES ===
36
- Class/entity names are property values, NOT labels:
37
- WRONG: (n:MyClassName) - using class names as labels
38
- CORRECT: (n:Class {name: 'MyClassName'}) - use label from rawSchema, name as property
39
- CORRECT: (n:LabelFromSchema {name: 'EntityName'}) - always check rawSchema for valid labels
40
-
41
- Examples:
42
- - "Count all classes" -> MATCH (n:Class) WHERE n.projectId = $projectId RETURN count(n)
43
- - "Find class by name" -> MATCH (n:Class {name: 'ClassName'}) WHERE n.projectId = $projectId RETURN n
44
- - "Methods in a class" -> MATCH (c:Class {name: 'ClassName'})-[:HAS_MEMBER]->(m:Method) WHERE c.projectId = $projectId RETURN m
45
-
46
- === PROJECT ISOLATION (REQUIRED) ===
47
- ALL queries MUST filter by projectId on every node pattern:
48
- WHERE n.projectId = $projectId
49
-
50
- === RESPONSE FORMAT ===
51
- Return ONLY valid JSON:
52
- {
53
- "cypher": "MATCH (n:Label) WHERE n.projectId = $projectId RETURN n",
54
- "parameters": { "param": "value" } | null,
55
- "explanation": "What this query does"
56
- }
57
- Do NOT include projectId in parameters - it's injected automatically.
58
-
59
- Query Generation Process - FOLLOW THIS EXACTLY:
60
- 1. SEARCH THE SCHEMA FILE FIRST: Use file_search to read neo4j-apoc-schema.json BEFORE generating any query
61
- 2. EXTRACT VALID LABELS: The keys in rawSchema ARE the valid labels (e.g., "Class", "Method", "Function", etc.)
62
- - rawSchema is ALWAYS available and contains all labels currently in the graph
63
- - discoveredSchema.nodeTypes (if available) provides counts and sample properties
64
- 3. CHECK RELATIONSHIPS: Look at rawSchema[label].relationships for each label to see available relationship types
65
- 4. CHECK SEMANTIC TYPES: Look at discoveredSchema.semanticTypes (if available) for framework-specific classifications
66
- - semanticTypes are PROPERTY values stored in n.semanticType, NOT labels - check discoveredSchema for valid values
67
- 5. REVIEW PATTERNS: Check discoveredSchema.commonPatterns (if available) for frequent relationship patterns
68
- 6. EXAMINE PROPERTIES: Use rawSchema[label].properties for exact property names and types
69
- 7. GENERATE QUERY: Write the Cypher query using ONLY labels, relationships, and properties from the schema
70
- 8. VALIDATE LABELS: Double-check that every label in your query exists as a key in rawSchema
71
- 9. ADD PROJECT FILTER: Always include WHERE n.projectId = $projectId for every node pattern in the query
72
-
73
- Critical Rules:
74
- - ALWAYS filter by projectId on every node in the query (e.g., WHERE n.projectId = $projectId)
75
- - Use the schema information from the file_search tool - do not guess node labels or relationships
76
- - Use ONLY node labels and properties found in the schema
77
- - For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName)
78
- - Use parameterized queries with $ syntax for any dynamic values
79
- - Return only the data relevant to the user's request
80
-
81
- === CORE RELATIONSHIPS ===
82
- - CONTAINS: SourceFile contains declarations (use for "in file", "declared in", "defined in")
83
- - HAS_MEMBER: Class/Interface has methods/properties (use for "has method", "contains property", "members")
84
- - HAS_PARAMETER: Method/Function has parameters (use for "takes parameter", "accepts")
85
- - EXTENDS: Class/Interface extends parent (use for "extends", "inherits from", "parent class", "subclass")
86
- - IMPLEMENTS: Class implements Interface (use for "implements", "conforms to")
87
- - IMPORTS: SourceFile imports another (use for "imports", "depends on", "requires")
88
- - TYPED_AS: Parameter/Property has type annotation (use for "typed as", "has type", "returns")
89
- - CALLS: Method/Function calls another (use for "calls", "invokes", "uses")
90
- - DECORATED_WITH: Node has a Decorator (use for "decorated with", "has decorator", "@SomeDecorator")
91
-
92
- === FRAMEWORK RELATIONSHIPS ===
93
- Framework-specific relationships are defined in rawSchema. Check rawSchema[label].relationships for each label to discover:
94
- - What relationship types exist (e.g., INJECTS, EXPOSES, MODULE_IMPORTS, INTERNAL_API_CALL, etc.)
95
- - Direction (in/out) and target labels for each relationship
96
- - These vary by project - ALWAYS check the schema file for available relationships
97
-
98
- CRITICAL: Do NOT confuse EXTENDS (inheritance) with HAS_MEMBER (composition). "extends" always means EXTENDS relationship.
99
-
100
- EXTENDS DIRECTION - CRITICAL:
101
- The arrow points FROM child TO parent. The child "extends" toward the parent.
102
- - CORRECT: (child:Class)-[:EXTENDS]->(parent:Class {name: 'ParentClassName'})
103
- - WRONG: (parent:Class {name: 'ParentClassName'})-[:EXTENDS]->(child:Class)
104
-
105
- Examples:
106
- - "Classes extending X" -> MATCH (c:Class)-[:EXTENDS]->(p:Class {name: 'X'}) WHERE c.projectId = $projectId RETURN c
107
- - "What extends Y" -> MATCH (c:Class)-[:EXTENDS]->(p:Class {name: 'Y'}) WHERE c.projectId = $projectId RETURN c
108
- - "Classes that extend X with >5 methods" ->
109
- MATCH (c:Class)-[:EXTENDS]->(p:Class {name: 'X'})
110
- WHERE c.projectId = $projectId
111
- WITH c
112
- MATCH (c)-[:HAS_MEMBER]->(m:Method)
113
- WITH c, count(m) AS methodCount
114
- WHERE methodCount > 5
115
- RETURN c, methodCount
116
-
117
- === SEMANTIC TYPES (Framework Classifications) - PRIMARY QUERY METHOD ===
118
- *** MOST QUERIES SHOULD USE SEMANTIC TYPES - CHECK discoveredSchema.semanticTypes FIRST ***
119
-
120
- Semantic types are the PRIMARY way to find framework-specific nodes. They are stored in:
121
- discoveredSchema.semanticTypes -> Array of all semantic type values in this project
122
-
123
- The semanticType is a PROPERTY on nodes, not a label. Query patterns:
124
- - EXACT MATCH: MATCH (c) WHERE c.projectId = $projectId AND c.semanticType = 'ExactTypeFromSchema' RETURN c
125
- - PARTIAL MATCH: MATCH (c) WHERE c.projectId = $projectId AND c.semanticType CONTAINS 'Pattern' RETURN c
126
-
127
- Common semantic type patterns (verify against discoveredSchema.semanticTypes):
128
- - Controllers: types containing 'Controller'
129
- - Services: types containing 'Service', 'Provider', or 'Injectable'
130
- - Repositories: types containing 'Repository', 'DAL', or 'DAO'
131
- - Modules: types containing 'Module'
132
-
133
- FALLBACK - If semantic type doesn't exist, use name patterns:
134
- - "Find all controllers" -> MATCH (c:Class) WHERE c.projectId = $projectId AND c.name CONTAINS 'Controller' RETURN c
135
- - "Find all services" -> MATCH (c:Class) WHERE c.projectId = $projectId AND c.name CONTAINS 'Service' RETURN c
136
-
137
- === DECORATOR QUERIES ===
138
- Use DECORATED_WITH relationship to find nodes with specific decorators:
139
- - "Classes with @X" -> MATCH (c:Class)-[:DECORATED_WITH]->(d:Decorator {name: 'X'}) WHERE c.projectId = $projectId RETURN c
140
- - "Methods with @Y" -> MATCH (m:Method)-[:DECORATED_WITH]->(d:Decorator {name: 'Y'}) WHERE m.projectId = $projectId RETURN m
141
-
142
- === MODULE/DIRECTORY QUERIES ===
143
- Use filePath property for location-based queries:
144
- - "in account module" -> WHERE n.filePath CONTAINS '/account/'
145
- - "in auth folder" -> WHERE n.filePath CONTAINS '/auth/'
146
-
147
- Examples:
148
- - "Items in account folder" ->
149
- MATCH (c:Class) WHERE c.projectId = $projectId AND c.filePath CONTAINS '/account/' RETURN c
150
- - FALLBACK (if no framework labels):
151
- MATCH (c:Class) WHERE c.projectId = $projectId AND c.name CONTAINS 'Service' AND c.filePath CONTAINS '/account/' RETURN c
152
-
153
- === FRAMEWORK-SPECIFIC PATTERNS ===
154
-
155
- Backend Projects (decorator-based frameworks):
156
- - Check rawSchema for framework labels that REPLACE the Class label
157
- - Use framework relationships (INJECTS, EXPOSES, etc.) from rawSchema[label].relationships
158
- - Check discoveredSchema.semanticTypes for framework classifications
159
-
160
- Frontend Projects (React, functional):
161
- - React components are typically Function nodes, NOT Class nodes
162
- - Hooks are Function nodes (useAuth, useState, etc.)
163
- - Example: "Find UserProfile component" -> MATCH (f:Function {name: 'UserProfile'}) WHERE f.projectId = $projectId RETURN f
164
-
165
- Tip: Check rawSchema keys to understand if project uses framework labels or just core TypeScript labels.
166
-
167
- IMPORTANT - Cypher Syntax (NOT SQL):
168
- - Cypher does NOT use GROUP BY. Aggregation happens automatically in RETURN.
169
- - WRONG (SQL): RETURN label, count(n) GROUP BY label
170
- - CORRECT (Cypher): RETURN labels(n) AS label, count(n) AS count
171
- - For grouping, non-aggregated values in RETURN automatically become grouping keys
172
- - Use labels(n) to get node labels as an array
173
- - Use collect() for aggregating into lists
174
- - Use count(), sum(), avg(), min(), max() for aggregations
175
- - Common patterns:
176
- - Count by type: MATCH (n) RETURN labels(n)[0] AS type, count(n) AS count
177
- - Group with collect: MATCH (n)-[:REL]->(m) RETURN n.name, collect(m.name) AS related
178
-
179
- Provide ONLY the JSON response with no additional text, markdown formatting, or explanations outside the JSON structure.
10
+ /**
11
+ * System instructions for the assistant (set once at creation time).
12
+ * Kept focused on Cypher rules and output format — schema data is injected per-query.
13
+ */
14
+ assistantInstructions = `You are a Neo4j Cypher query generator. You receive a schema and a natural language request, and you return a single JSON object. No prose, no markdown, no explanation outside the JSON.
15
+
16
+ OUTPUT FORMAT (strict):
17
+ {"cypher": "...", "parameters": null, "explanation": "..."}
18
+
19
+ - "cypher": valid Neo4j Cypher query
20
+ - "parameters": object of extra parameters or null (NEVER include projectId it is injected automatically)
21
+ - "explanation": one sentence describing what the query does
22
+
23
+ RULES:
24
+ 1. ALL node patterns MUST include: WHERE n.projectId = $projectId
25
+ 2. Use ONLY node labels listed in the schema's nodeTypes[].label
26
+ 3. Entity names are PROPERTY values, NOT labels: (n:Class {name: 'MyService'}) not (n:MyService)
27
+ 4. AST type names are NOT labels: ClassDeclaration Class, MethodDeclaration → Method, InterfaceDeclaration → Interface, FunctionDeclaration → Function, PropertyDeclaration → Property, ParameterDeclaration → Parameter
28
+ 5. semanticType is a PROPERTY, not a label: WHERE n.semanticType = 'NestController'
29
+ 6. EXTENDS direction: child parent. (child:Class)-[:EXTENDS]->(parent:Class)
30
+ 7. Cypher has no GROUP BY — aggregation is automatic in RETURN
31
+ 8. Use $-prefixed parameters for dynamic values
32
+
33
+ CORE RELATIONSHIPS:
34
+ CONTAINS (file→declaration), HAS_MEMBER (class→method/property), HAS_PARAMETER (method→param), EXTENDS (child→parent), IMPLEMENTS (class→interface), IMPORTS (file→file), TYPED_AS (node→type), CALLS (caller→callee), DECORATED_WITH (node→decorator)
180
35
  `;
181
36
  constructor() {
182
37
  const apiKey = process.env.OPENAI_API_KEY;
@@ -191,121 +46,79 @@ Provide ONLY the JSON response with no additional text, markdown formatting, or
191
46
  });
192
47
  }
193
48
  async getOrCreateAssistant(schemaPath) {
194
- // Store schema path for later use in prompt injection
49
+ // Store schema path for later use schema is injected directly into each prompt
195
50
  this.schemaPath = schemaPath;
196
51
  if (process.env.OPENAI_ASSISTANT_ID) {
197
52
  this.assistantId = process.env.OPENAI_ASSISTANT_ID;
198
53
  console.error(`Using existing assistant with ID: ${this.assistantId}`);
199
54
  return this.assistantId;
200
55
  }
201
- const schemaFile = await this.openai.files.create({
202
- file: fs.createReadStream(schemaPath),
203
- purpose: 'assistants',
204
- });
205
- // Create a vector store for the schema file
206
- const vectorStore = await this.openai.vectorStores.create({
207
- name: 'Neo4j APOC Schema Vector Store',
208
- file_ids: [schemaFile.id],
209
- metadata: { type: 'neo4j_apoc_schema' },
210
- });
211
- const vectorStoreId = vectorStore.id;
212
- // Create a new assistant
213
- const assistantConfig = {
214
- name: 'Neo4j Cypher Query Agent',
215
- description: 'An agent that helps convert natural language to Neo4j Cypher queries',
56
+ const assistant = await this.openai.beta.assistants.create({
57
+ name: 'Neo4j Cypher Query Generator',
58
+ description: 'Converts natural language to Neo4j Cypher queries. Returns JSON only.',
216
59
  model: this.MODEL,
217
- instructions: `
218
- You are a specialized assistant that helps convert natural language requests into Neo4j Cypher queries.
219
- When users ask questions about their codebase data, you'll analyze their intent and generate appropriate
220
- Cypher queries based on the Neo4j schema provided in files.
221
- ${this.messageInstructions}
222
- `,
223
- tools: [
224
- {
225
- type: 'code_interpreter',
226
- },
227
- {
228
- type: 'file_search',
229
- },
230
- ],
231
- tool_resources: {
232
- code_interpreter: {
233
- file_ids: [schemaFile.id],
234
- },
235
- file_search: {
236
- vector_store_ids: [vectorStoreId],
237
- },
238
- },
239
- };
240
- const assistant = await this.openai.beta.assistants.create(assistantConfig);
60
+ instructions: this.assistantInstructions,
61
+ response_format: { type: 'json_object' },
62
+ // No tools schema is injected directly into each message
63
+ tools: [],
64
+ });
241
65
  this.assistantId = assistant.id;
66
+ console.error(`Created assistant with ID: ${this.assistantId}`);
242
67
  return this.assistantId;
243
68
  }
244
69
  /**
245
- * Load and format the schema context for direct injection into prompts.
246
- * This supplements the file_search tool by providing explicit schema information.
70
+ * Load the schema and format it for direct injection into the user message.
71
+ * This is the ONLY way the LLM sees the schema — no file_search.
247
72
  */
248
73
  loadSchemaContext() {
249
74
  if (!this.schemaPath) {
250
- return 'No schema available. Use node types from file_search.';
75
+ return 'No schema available.';
251
76
  }
252
77
  try {
253
78
  const content = fs.readFileSync(this.schemaPath, 'utf-8');
254
79
  const schema = JSON.parse(content);
255
- if (!schema.discoveredSchema) {
256
- return 'No discovered schema available.';
80
+ if (!schema || !schema.nodeTypes) {
81
+ return 'No schema available.';
257
82
  }
258
- const ds = schema.discoveredSchema;
259
- // Format node types
260
- const nodeTypes = ds.nodeTypes?.map((n) => n.label).join(', ') ?? 'none';
261
- // Get function count vs class count to hint at framework
262
- const functionCount = ds.nodeTypes?.find((n) => n.label === 'Function')?.count ?? 0;
263
- const classCount = ds.nodeTypes?.find((n) => n.label === 'Class')?.count ?? 0;
264
- const decoratorCount = ds.nodeTypes?.find((n) => n.label === 'Decorator')?.count ?? 0;
265
- // Format relationship types
266
- const relTypes = ds.relationshipTypes?.map((r) => r.type).join(', ') ?? 'none';
267
- // Format semantic types and categorize them
268
- const semanticTypeList = ds.semanticTypes?.map((s) => s.type) ?? [];
269
- const semTypes = semanticTypeList.length > 0 ? semanticTypeList.join(', ') : 'none';
83
+ // Format node types with properties
84
+ const nodeTypeLines = schema.nodeTypes
85
+ ?.map((n) => ` ${n.label} (${n.count} nodes) — properties: ${(n.properties ?? []).join(', ')}`)
86
+ .join('\n') ?? 'none';
87
+ // Format relationship types with connection patterns
88
+ const relTypeLines = schema.relationshipTypes
89
+ ?.map((r) => {
90
+ const conns = (r.connections ?? []).map((c) => `${c.from}→${c.to}`).join(', ');
91
+ return ` ${r.type} (${r.count}) ${conns}`;
92
+ })
93
+ .join('\n') ?? 'none';
94
+ // Format semantic types
95
+ const semanticTypeList = schema.semanticTypes?.map((s) => s.type) ?? [];
96
+ const semTypeLines = schema.semanticTypes
97
+ ?.map((s) => ` ${s.type} (on ${s.label}, ${s.count} nodes)`)
98
+ .join('\n') ?? 'none';
99
+ // Format common patterns
100
+ const patternLines = schema.commonPatterns
101
+ ?.map((p) => ` (${p.from})-[:${p.relationship}]->(${p.to}) × ${p.count}`)
102
+ .join('\n') ?? 'none';
270
103
  // Cache categorized semantic types for dynamic example generation
271
104
  this.cachedSemanticTypes = this.categorizeSemanticTypes(semanticTypeList);
272
- // Framework hint based on graph composition
273
- let frameworkHint = '';
274
- if (decoratorCount > 10 && classCount > functionCount) {
275
- // Use discovered semantic types instead of assuming NestJS
276
- const sampleType = this.cachedSemanticTypes?.controller[0] ?? this.cachedSemanticTypes?.service[0] ?? 'YourSemanticType';
277
- frameworkHint = `\nFRAMEWORK DETECTED: Decorator-based codebase. Use Class nodes with semanticType property (e.g., semanticType = "${sampleType}").`;
278
- }
279
- else if (functionCount > classCount) {
280
- frameworkHint = '\nFRAMEWORK DETECTED: React/functional codebase. Use Function nodes for components.';
281
- }
282
- return `
283
- === VALID NODE LABELS (use ONLY these after the colon) ===
284
- ${nodeTypes}
105
+ return `SCHEMA:
285
106
 
286
- === VALID RELATIONSHIP TYPES ===
287
- ${relTypes}
107
+ NODE LABELS (use ONLY these):
108
+ ${nodeTypeLines}
288
109
 
289
- === SEMANTIC TYPES - USE THESE FOR FRAMEWORK QUERIES ===
290
- Available semantic types in this project: ${semTypes}
110
+ RELATIONSHIP TYPES:
111
+ ${relTypeLines}
291
112
 
292
- *** SEMANTIC TYPES ARE THE PRIMARY WAY TO QUERY FRAMEWORK-SPECIFIC NODES ***
293
- Query pattern: WHERE n.semanticType = 'TypeFromListAbove'
294
- Example: MATCH (n:Class) WHERE n.projectId = $projectId AND n.semanticType = '${semanticTypeList[0] ?? 'SemanticType'}' RETURN n
295
- ${frameworkHint}
113
+ SEMANTIC TYPES (query via WHERE n.semanticType = 'value'):
114
+ ${semTypeLines}
296
115
 
297
- === CRITICAL RULES ===
298
- 1. Use ONLY the labels listed above after the colon (:Label)
299
- 2. Semantic types are PROPERTY values, NOT labels - use WHERE n.semanticType = 'Type'
300
- 3. Class/entity names are PROPERTY values, NOT labels - use WHERE n.name = 'Name'
301
- 4. WRONG: (n:ClassName) - using names as labels
302
- 5. CORRECT: (n:Class {name: 'ClassName'}) or (n:LabelFromSchema {name: 'Name'})
303
- 6. CORRECT: (n:Class) WHERE n.semanticType = 'TypeFromSemanticTypesList'
304
- `.trim();
116
+ COMMON PATTERNS:
117
+ ${patternLines}`;
305
118
  }
306
119
  catch (error) {
307
120
  console.warn('Failed to load schema for prompt injection:', error);
308
- return 'Schema load failed. Use file_search for schema information.';
121
+ return 'Schema load failed.';
309
122
  }
310
123
  }
311
124
  /**
@@ -409,14 +222,14 @@ FALLBACK PATTERNS (use when semantic types don't exist):
409
222
  // Generate dynamic examples based on discovered semantic types
410
223
  const dynamicSemanticExamples = this.cachedSemanticTypes
411
224
  ? this.generateDynamicSemanticExamples(this.cachedSemanticTypes)
412
- : '\nNo semantic types discovered. Use name patterns for all queries (e.g., c.name CONTAINS "Controller").\n';
413
- const prompt = `Please convert this request to a valid Neo4j Cypher query: ${userPrompt}.
225
+ : '';
226
+ const prompt = `Convert to Cypher: ${userPrompt}
414
227
 
415
228
  ${schemaContext}
416
229
  ${dynamicSemanticExamples}
417
- The query will be scoped to project: ${projectId}
418
- Remember to include WHERE n.projectId = $projectId for all node patterns.
419
- `;
230
+ Project: ${projectId} add WHERE n.projectId = $projectId on every node pattern.
231
+
232
+ Respond with ONLY a JSON object: {"cypher": "...", "parameters": null, "explanation": "..."}`;
420
233
  // SECURITY: Only log prompt length, not full content which may contain sensitive data
421
234
  console.error(`NL-to-Cypher: Processing prompt (${prompt.length} chars) for project ${projectId}`);
422
235
  const run = await this.openai.beta.threads.createAndRunPoll({
@@ -465,10 +278,10 @@ Remember to include WHERE n.projectId = $projectId for all node patterns.
465
278
  }
466
279
  // SECURITY: Don't log the full text value which may contain sensitive queries
467
280
  console.error(`NL-to-Cypher: Parsing response (${textValue.length} chars)`);
468
- // Parse the response with proper error handling
281
+ // Extract JSON from the LLM response, handling markdown fences and prose preamble
469
282
  let result;
470
283
  try {
471
- result = JSON.parse(textValue);
284
+ result = JSON.parse(this.extractJson(textValue));
472
285
  }
473
286
  catch (parseError) {
474
287
  const message = parseError instanceof Error ? parseError.message : String(parseError);
@@ -481,6 +294,38 @@ Remember to include WHERE n.projectId = $projectId for all node patterns.
481
294
  this.validateLabelUsage(result.cypher);
482
295
  return result;
483
296
  }
297
+ /**
298
+ * Extracts JSON from an LLM response that may contain markdown fences or prose preamble.
299
+ * Tries in order: raw parse, markdown fence extraction, first `{...}` block extraction.
300
+ */
301
+ extractJson(text) {
302
+ const trimmed = text.trim();
303
+ // 1. Already valid JSON — return as-is
304
+ if (trimmed.startsWith('{')) {
305
+ return trimmed;
306
+ }
307
+ // 2. Wrapped in markdown code fences: ```json ... ``` or ``` ... ```
308
+ const fenceMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
309
+ if (fenceMatch) {
310
+ return fenceMatch[1].trim();
311
+ }
312
+ // 3. JSON object embedded in prose — find the first top-level { ... }
313
+ const startIdx = trimmed.indexOf('{');
314
+ if (startIdx !== -1) {
315
+ let depth = 0;
316
+ for (let i = startIdx; i < trimmed.length; i++) {
317
+ if (trimmed[i] === '{')
318
+ depth++;
319
+ else if (trimmed[i] === '}')
320
+ depth--;
321
+ if (depth === 0) {
322
+ return trimmed.substring(startIdx, i + 1);
323
+ }
324
+ }
325
+ }
326
+ // 4. Give up — return original text so JSON.parse produces a useful error
327
+ return trimmed;
328
+ }
484
329
  /**
485
330
  * Validates that the generated Cypher query contains projectId filters.
486
331
  * This is a security measure to ensure project isolation is maintained
@@ -521,7 +366,7 @@ Remember to include WHERE n.projectId = $projectId for all node patterns.
521
366
  }
522
367
  /**
523
368
  * Load valid labels dynamically from the schema file.
524
- * Returns all keys from rawSchema AND discoveredSchema.nodeTypes which represent actual Neo4j labels.
369
+ * Returns all labels from nodeTypes in the discovered schema.
525
370
  */
526
371
  loadValidLabelsFromSchema() {
527
372
  // Fallback to core TypeScript labels if schema not available
@@ -550,14 +395,8 @@ Remember to include WHERE n.projectId = $projectId for all node patterns.
550
395
  const content = fs.readFileSync(this.schemaPath, 'utf-8');
551
396
  const schema = JSON.parse(content);
552
397
  const allLabels = new Set(coreLabels);
553
- // Extract labels from rawSchema keys
554
- if (schema.rawSchema?.records?.[0]?._fields?.[0]) {
555
- const schemaLabels = Object.keys(schema.rawSchema.records[0]._fields[0]);
556
- schemaLabels.forEach((label) => allLabels.add(label));
557
- }
558
- // Also extract labels from discoveredSchema.nodeTypes (includes framework labels)
559
- if (schema.discoveredSchema?.nodeTypes) {
560
- for (const nodeType of schema.discoveredSchema.nodeTypes) {
398
+ if (schema?.nodeTypes) {
399
+ for (const nodeType of schema.nodeTypes) {
561
400
  if (nodeType.label) {
562
401
  allLabels.add(nodeType.label);
563
402
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OpenAI Embeddings Service
3
3
  * Uses OpenAI's text-embedding API. Requires OPENAI_API_KEY.
4
- * Opt-in via OPENAI_ENABLED=true.
4
+ * Opt-in via OPENAI_EMBEDDINGS_ENABLED=true.
5
5
  */
6
6
  import OpenAI from 'openai';
7
7
  import { debugLog } from '../../mcp/utils.js';
@@ -6,9 +6,23 @@ export const hashFile = async (filePath) => {
6
6
  const content = await fs.readFile(filePath);
7
7
  return crypto.createHash('sha256').update(content).digest('hex');
8
8
  };
9
+ const serializeForLog = (data) => {
10
+ if (data instanceof Error) {
11
+ return { name: data.name, message: data.message, stack: data.stack };
12
+ }
13
+ if (data !== null && typeof data === 'object') {
14
+ const result = {};
15
+ for (const key of Object.keys(data)) {
16
+ result[key] = serializeForLog(data[key]);
17
+ }
18
+ return result;
19
+ }
20
+ return data;
21
+ };
9
22
  export const debugLog = async (message, data) => {
10
23
  const timestamp = new Date().toISOString();
11
- const logEntry = `[${timestamp}] ${message}\n${data ? JSON.stringify(data, null, LOG_CONFIG.jsonIndent) : ''}\n${LOG_CONFIG.separator}\n`;
24
+ const serialized = data !== undefined ? serializeForLog(data) : undefined;
25
+ const logEntry = `[${timestamp}] ${message}\n${serialized !== undefined ? JSON.stringify(serialized, null, LOG_CONFIG.jsonIndent) : ''}\n${LOG_CONFIG.separator}\n`;
12
26
  try {
13
27
  await fs.appendFile(path.join(process.cwd(), LOG_CONFIG.debugLogFile), logEntry);
14
28
  }
@@ -418,7 +418,7 @@ export const WATCH = {
418
418
  export const MESSAGES = {
419
419
  errors: {
420
420
  noRelevantCode: 'No relevant code found.',
421
- serviceNotInitialized: 'ERROR: Natural Language to Cypher service is not initialized yet. Please try again in a few moments.',
421
+ serviceNotInitialized: 'natural_language_to_cypher requires OPENAI_API_KEY. Set it and restart the MCP server to enable this tool.',
422
422
  connectionTestFailed: 'Connection test failed',
423
423
  neo4jRequirement: 'Note: This server requires Neo4j with APOC plugin installed',
424
424
  genericError: 'ERROR:',
@@ -5,7 +5,8 @@
5
5
  import fs from 'fs/promises';
6
6
  import { join } from 'path';
7
7
  import { ensureNeo4jRunning, isDockerInstalled, isDockerRunning } from '../cli/neo4j-docker.js';
8
- import { isOpenAIEnabled, getEmbeddingDimensions } from '../core/embeddings/embeddings.service.js';
8
+ import { isOpenAIEnabled, isOpenAIAvailable, getEmbeddingDimensions } from '../core/embeddings/embeddings.service.js';
9
+ import { LIST_PROJECTS_QUERY } from '../core/utils/project-id.js';
9
10
  import { Neo4jService, QUERIES } from '../storage/neo4j/neo4j.service.js';
10
11
  import { FILE_PATHS, LOG_CONFIG } from './constants.js';
11
12
  import { initializeNaturalLanguageService } from './tools/natural-language-to-cypher.tool.js';
@@ -22,24 +23,24 @@ const checkConfiguration = async () => {
22
23
  message: `[code-graph-context] Embedding provider: ${provider} (${dims} dimensions)`,
23
24
  }));
24
25
  await debugLog('Embedding configuration', { provider, dimensions: dims });
25
- if (openai && !process.env.OPENAI_API_KEY) {
26
+ if (openai && !isOpenAIAvailable()) {
26
27
  console.error(JSON.stringify({
27
28
  level: 'warn',
28
- message: '[code-graph-context] OPENAI_ENABLED=true but OPENAI_API_KEY not set. Embedding calls will fail.',
29
+ message: '[code-graph-context] OPENAI_EMBEDDINGS_ENABLED=true but OPENAI_API_KEY not set. Embedding calls will fail.',
29
30
  }));
30
- await debugLog('Configuration warning', { warning: 'OPENAI_ENABLED=true but OPENAI_API_KEY not set' });
31
+ await debugLog('Configuration warning', { warning: 'OPENAI_EMBEDDINGS_ENABLED=true but OPENAI_API_KEY not set' });
31
32
  }
32
33
  if (!openai) {
33
34
  console.error(JSON.stringify({
34
35
  level: 'info',
35
36
  message: '[code-graph-context] Using local embeddings (Python sidecar). Starts on first embedding request.',
36
37
  }));
37
- if (!process.env.OPENAI_API_KEY) {
38
- console.error(JSON.stringify({
39
- level: 'info',
40
- message: '[code-graph-context] natural_language_to_cypher requires OPENAI_API_KEY and is unavailable.',
41
- }));
42
- }
38
+ }
39
+ if (!isOpenAIAvailable()) {
40
+ console.error(JSON.stringify({
41
+ level: 'info',
42
+ message: '[code-graph-context] natural_language_to_cypher unavailable: OPENAI_API_KEY not set.',
43
+ }));
43
44
  }
44
45
  };
45
46
  /**
@@ -87,26 +88,34 @@ export const initializeServices = async () => {
87
88
  await ensureNeo4j();
88
89
  // Initialize services sequentially - schema must be written before NL service reads it
89
90
  await initializeNeo4jSchema();
90
- await initializeNaturalLanguageService();
91
+ if (isOpenAIAvailable()) {
92
+ await initializeNaturalLanguageService();
93
+ }
94
+ else {
95
+ console.error(JSON.stringify({
96
+ level: 'info',
97
+ message: '[code-graph-context] natural_language_to_cypher unavailable: OPENAI_API_KEY not set',
98
+ }));
99
+ }
91
100
  };
92
101
  /**
93
102
  * Dynamically discover schema from the actual graph contents.
94
103
  * This is framework-agnostic - it discovers what's actually in the graph.
95
104
  */
96
- const discoverSchemaFromGraph = async (neo4jService) => {
105
+ const discoverSchemaFromGraph = async (neo4jService, projectId) => {
97
106
  try {
98
107
  // Discover actual node types, relationships, and patterns from the graph
99
108
  const [nodeTypes, relationshipTypes, semanticTypes, commonPatterns] = await Promise.all([
100
- neo4jService.run(QUERIES.DISCOVER_NODE_TYPES),
101
- neo4jService.run(QUERIES.DISCOVER_RELATIONSHIP_TYPES),
102
- neo4jService.run(QUERIES.DISCOVER_SEMANTIC_TYPES),
103
- neo4jService.run(QUERIES.DISCOVER_COMMON_PATTERNS),
109
+ neo4jService.run(QUERIES.DISCOVER_NODE_TYPES, { projectId }),
110
+ neo4jService.run(QUERIES.DISCOVER_RELATIONSHIP_TYPES, { projectId }),
111
+ neo4jService.run(QUERIES.DISCOVER_SEMANTIC_TYPES, { projectId }),
112
+ neo4jService.run(QUERIES.DISCOVER_COMMON_PATTERNS, { projectId }),
104
113
  ]);
105
114
  return {
106
115
  nodeTypes: nodeTypes.map((r) => ({
107
116
  label: r.label,
108
117
  count: typeof r.nodeCount === 'object' ? r.nodeCount.toNumber() : r.nodeCount,
109
- properties: r.sampleProperties ?? [],
118
+ properties: r.properties ?? [],
110
119
  })),
111
120
  relationshipTypes: relationshipTypes.map((r) => ({
112
121
  type: r.relationshipType,
@@ -115,6 +124,7 @@ const discoverSchemaFromGraph = async (neo4jService) => {
115
124
  })),
116
125
  semanticTypes: semanticTypes.map((r) => ({
117
126
  type: r.semanticType,
127
+ label: r.nodeLabel,
118
128
  count: typeof r.count === 'object' ? r.count.toNumber() : r.count,
119
129
  })),
120
130
  commonPatterns: commonPatterns.map((r) => ({
@@ -136,13 +146,11 @@ const discoverSchemaFromGraph = async (neo4jService) => {
136
146
  const initializeNeo4jSchema = async () => {
137
147
  try {
138
148
  const neo4jService = new Neo4jService();
139
- const rawSchema = await neo4jService.getSchema();
149
+ // Find the most recently updated project to scope discovery queries
150
+ const projects = await neo4jService.run(LIST_PROJECTS_QUERY, {});
151
+ const projectId = projects.length > 0 ? projects[0].projectId : null;
140
152
  // Dynamically discover what's actually in the graph
141
- const discoveredSchema = await discoverSchemaFromGraph(neo4jService);
142
- const schema = {
143
- rawSchema,
144
- discoveredSchema,
145
- };
153
+ const schema = projectId ? await discoverSchemaFromGraph(neo4jService, projectId) : null;
146
154
  const schemaPath = join(process.cwd(), FILE_PATHS.schemaOutput);
147
155
  await fs.writeFile(schemaPath, JSON.stringify(schema, null, LOG_CONFIG.jsonIndentation));
148
156
  await debugLog('Neo4j schema cached successfully', { schemaPath });
@@ -6,7 +6,7 @@ import { join } from 'path';
6
6
  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
- import { TOOL_NAMES, TOOL_METADATA, MESSAGES, FILE_PATHS } from '../constants.js';
9
+ import { TOOL_NAMES, TOOL_METADATA, FILE_PATHS } from '../constants.js';
10
10
  import { createErrorResponse, createSuccessResponse, formatQueryResults, debugLog, resolveProjectIdOrError, } from '../utils.js';
11
11
  // Service instance - initialized asynchronously
12
12
  let naturalLanguageToCypherService = null;
@@ -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(MESSAGES.errors.serviceNotInitialized);
48
+ return createSuccessResponse('natural_language_to_cypher requires OPENAI_API_KEY. Set it 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)
@@ -48,29 +48,6 @@ export class Neo4jService {
48
48
  getDriver() {
49
49
  return this.driver;
50
50
  }
51
- async getSchema() {
52
- const session = this.driver.session();
53
- const timeoutConfig = getTimeoutConfig();
54
- try {
55
- return await session.run(QUERIES.APOC_SCHEMA, {}, {
56
- timeout: timeoutConfig.neo4j.queryTimeoutMs,
57
- });
58
- }
59
- catch (error) {
60
- console.error('Error fetching schema:', error);
61
- throw error;
62
- }
63
- finally {
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
51
  /**
75
52
  * Close the Neo4j driver connection.
76
53
  * Should be called when the service is no longer needed to release resources.
@@ -82,10 +59,6 @@ export class Neo4jService {
82
59
  }
83
60
  }
84
61
  export const QUERIES = {
85
- APOC_SCHEMA: `
86
- CALL apoc.meta.schema() YIELD value
87
- RETURN value as schema
88
- `,
89
62
  // Project-scoped deletion - only deletes nodes for the specified project
90
63
  // Uses APOC batched deletion to avoid transaction memory limits on large projects
91
64
  CLEAR_PROJECT: `
@@ -444,65 +417,67 @@ export const QUERIES = {
444
417
  // DYNAMIC SCHEMA DISCOVERY QUERIES
445
418
  // ============================================
446
419
  /**
447
- * Get all distinct node labels with counts and sample properties
420
+ * Get all distinct node labels with counts, property keys, and property types.
421
+ * Samples up to 10 nodes per label to collect comprehensive property info.
448
422
  */
449
423
  DISCOVER_NODE_TYPES: `
450
424
  CALL db.labels() YIELD label
451
425
  CALL {
452
426
  WITH label
453
427
  MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
454
- WITH n LIMIT 1
455
- RETURN keys(n) AS sampleProperties
428
+ RETURN count(n) AS nodeCount
456
429
  }
457
430
  CALL {
458
431
  WITH label
459
432
  MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
460
- RETURN count(n) AS nodeCount
433
+ WITH n LIMIT 10
434
+ UNWIND keys(n) AS key
435
+ WITH DISTINCT key, n[key] AS val
436
+ RETURN collect(DISTINCT key) AS properties
461
437
  }
462
- RETURN label, nodeCount, sampleProperties
438
+ RETURN label, nodeCount, properties
463
439
  ORDER BY nodeCount DESC
464
440
  `,
465
441
  /**
466
- * Get all distinct relationship types with counts and which node types they connect
442
+ * Get all distinct relationship types with counts and all connection patterns
467
443
  */
468
444
  DISCOVER_RELATIONSHIP_TYPES: `
469
445
  CALL db.relationshipTypes() YIELD relationshipType
470
446
  CALL {
471
447
  WITH relationshipType
472
448
  MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId AND b.projectId = $projectId
473
- WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
474
- RETURN fromLabel, toLabel
475
- LIMIT 10
449
+ RETURN count(r) AS relCount
476
450
  }
477
451
  CALL {
478
452
  WITH relationshipType
479
- MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId
480
- RETURN count(r) AS relCount
453
+ MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId AND b.projectId = $projectId
454
+ WITH DISTINCT labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
455
+ RETURN collect({from: fromLabel, to: toLabel}) AS connections
481
456
  }
482
- RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
457
+ RETURN relationshipType, relCount, connections
483
458
  ORDER BY relCount DESC
484
459
  `,
485
460
  /**
486
- * Get sample nodes of each semantic type for context
461
+ * Get semantic types with counts and which label they appear on
487
462
  */
488
463
  DISCOVER_SEMANTIC_TYPES: `
489
464
  MATCH (n)
490
465
  WHERE n.semanticType IS NOT NULL AND n.projectId = $projectId
491
- WITH n.semanticType AS semanticType, count(*) AS count
466
+ WITH n.semanticType AS semanticType, labels(n)[0] AS nodeLabel, count(*) AS count
467
+ RETURN semanticType, nodeLabel, count
492
468
  ORDER BY count DESC
493
- RETURN semanticType, count
494
469
  `,
495
470
  /**
496
- * Get example query patterns based on actual graph structure
471
+ * Get all relationship patterns between node types
497
472
  */
498
473
  DISCOVER_COMMON_PATTERNS: `
499
474
  MATCH (a)-[r]->(b)
500
475
  WHERE a.projectId = $projectId AND b.projectId = $projectId
501
476
  WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
502
- WHERE count > 5
477
+ WHERE count > 2
503
478
  RETURN fromType, relType, toType, count
504
479
  ORDER BY count DESC
505
- LIMIT 20
480
+ LIMIT 50
506
481
  `,
507
482
  // ============================================
508
483
  // IMPACT ANALYSIS QUERIES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "2.13.3",
3
+ "version": "2.14.1",
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",