code-graph-context 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +71 -44
  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 +618 -50
  10. package/dist/core/parsers/workspace-parser.js +554 -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
@@ -84,6 +84,38 @@ function extractInjectionToken(node) {
84
84
  }
85
85
  return null;
86
86
  }
87
+ /**
88
+ * Extract constructor parameter types and @Inject tokens for edge detection.
89
+ * This allows INJECTS detection to work without AST access (for cross-chunk detection).
90
+ */
91
+ function extractConstructorParamTypes(node) {
92
+ const types = [];
93
+ const injectTokens = new Map();
94
+ const constructors = node.getConstructors();
95
+ if (constructors.length === 0)
96
+ return { types, injectTokens };
97
+ const constructor = constructors[0];
98
+ const parameters = constructor.getParameters();
99
+ for (const param of parameters) {
100
+ const typeNode = param.getTypeNode();
101
+ if (typeNode) {
102
+ const typeName = typeNode.getText();
103
+ types.push(typeName);
104
+ // Check for @Inject decorator
105
+ const decorators = param.getDecorators();
106
+ for (const decorator of decorators) {
107
+ if (decorator.getName() === 'Inject') {
108
+ const args = decorator.getArguments();
109
+ if (args.length > 0) {
110
+ const token = args[0].getText().replace(/['"]/g, '');
111
+ injectTokens.set(typeName, token);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return { types, injectTokens };
118
+ }
87
119
  function hasDynamicMethods(node) {
88
120
  const methods = node.getMethods();
89
121
  const dynamicMethods = ['forRoot', 'forFeature', 'forRootAsync', 'forFeatureAsync'];
@@ -103,6 +135,14 @@ function extractModuleControllers(node) {
103
135
  function extractModuleExports(node) {
104
136
  return extractModuleArrayProperty(node, 'exports');
105
137
  }
138
+ /**
139
+ * Escapes special regex characters in a string to prevent ReDoS attacks.
140
+ * @param str The string to escape
141
+ * @returns The escaped string safe for use in a regex
142
+ */
143
+ function escapeRegexChars(str) {
144
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
145
+ }
106
146
  function extractModuleArrayProperty(node, propertyName) {
107
147
  const decorator = node.getDecorator('Module');
108
148
  if (!decorator)
@@ -111,7 +151,9 @@ function extractModuleArrayProperty(node, propertyName) {
111
151
  if (args.length === 0)
112
152
  return [];
113
153
  const configText = args[0].getText();
114
- const regex = new RegExp(`${propertyName}\\s*:\\s*\\[([^\\]]+)\\]`);
154
+ // SECURITY: Escape propertyName to prevent ReDoS attacks
155
+ const escapedPropertyName = escapeRegexChars(propertyName);
156
+ const regex = new RegExp(`${escapedPropertyName}\\s*:\\s*\\[([^\\]]+)\\]`);
115
157
  const match = configText.match(regex);
116
158
  if (!match)
117
159
  return [];
@@ -252,59 +294,39 @@ function detectDependencyInjection(sourceNode, targetNode) {
252
294
  return false;
253
295
  if (targetNode.properties?.coreType !== CoreNodeType.CLASS_DECLARATION)
254
296
  return false;
255
- const constructors = sourceNode.sourceNode?.getConstructors();
256
- if (!constructors || constructors.length === 0)
257
- return false;
258
- const constructor = constructors[0];
259
- const parameters = constructor.getParameters();
260
297
  const targetName = targetNode.properties?.name;
261
- return parameters.some((param) => {
262
- const paramType = param.getTypeNode()?.getText();
263
- if (paramType === targetName)
264
- return true;
265
- const decorators = param.getDecorators();
266
- return decorators.some((decorator) => {
267
- if (decorator.getName() === 'Inject') {
268
- const args = decorator.getArguments();
269
- if (args.length > 0) {
270
- const token = args[0].getText().replace(/['"]/g, '');
271
- return token === targetName;
272
- }
273
- }
274
- return false;
275
- });
276
- });
298
+ if (!targetName)
299
+ return false;
300
+ // Use pre-extracted constructor params from context (works after AST cleanup)
301
+ const constructorParamTypes = sourceNode.properties?.context?.constructorParamTypes ?? [];
302
+ const injectTokens = sourceNode.properties?.context?.injectTokens ?? {};
303
+ // Check if target is in constructor params by type
304
+ if (constructorParamTypes.includes(targetName))
305
+ return true;
306
+ // Check if target is referenced via @Inject token
307
+ return Object.values(injectTokens).includes(targetName);
277
308
  }
278
309
  function extractInjectionTokenFromRelation(sourceNode, targetNode) {
279
- const constructors = sourceNode.sourceNode?.getConstructors();
280
- if (!constructors || constructors.length === 0)
310
+ const targetName = targetNode.properties?.name;
311
+ if (!targetName)
281
312
  return null;
282
- const constructor = constructors[0];
283
- const parameters = constructor.getParameters();
284
- for (const param of parameters) {
285
- const decorators = param.getDecorators();
286
- for (const decorator of decorators) {
287
- if (decorator.getName() === 'Inject') {
288
- const args = decorator.getArguments();
289
- if (args.length > 0) {
290
- return args[0].getText().replace(/['"]/g, '');
291
- }
292
- }
313
+ // Use pre-extracted inject tokens from context (works after AST cleanup)
314
+ const injectTokens = sourceNode.properties?.context?.injectTokens ?? {};
315
+ // Find the token that maps to the target
316
+ for (const [typeName, token] of Object.entries(injectTokens)) {
317
+ if (token === targetName || typeName === targetName) {
318
+ return token;
293
319
  }
294
320
  }
295
321
  return null;
296
322
  }
297
323
  function findParameterIndex(sourceNode, targetNode) {
298
- const constructors = sourceNode.sourceNode?.getConstructors();
299
- if (!constructors || constructors.length === 0)
300
- return 0;
301
- const constructor = constructors[0];
302
- const parameters = constructor.getParameters();
303
324
  const targetName = targetNode.properties?.name;
304
- return parameters.findIndex((param) => {
305
- const paramType = param.getTypeNode()?.getText();
306
- return paramType === targetName;
307
- });
325
+ if (!targetName)
326
+ return -1;
327
+ // Use pre-extracted constructor params from context (works after AST cleanup)
328
+ const constructorParamTypes = sourceNode.properties?.context?.constructorParamTypes ?? [];
329
+ return constructorParamTypes.indexOf(targetName);
308
330
  }
309
331
  function computeFullPathFromNodes(sourceNode, targetNode) {
310
332
  const basePath = sourceNode.properties?.context?.basePath ?? '';
@@ -781,6 +803,8 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
781
803
  const node = parsedNode.sourceNode;
782
804
  if (!node)
783
805
  return {};
806
+ // Extract constructor param types for INJECTS edge detection
807
+ const { types, injectTokens } = extractConstructorParamTypes(node);
784
808
  return {
785
809
  isAbstract: node.getAbstractKeyword() != null,
786
810
  isDefaultExport: node.isDefaultExport(),
@@ -790,6 +814,9 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
790
814
  methodCount: node.getMethods().length,
791
815
  propertyCount: node.getProperties().length,
792
816
  constructorParameterCount: countConstructorParameters(node),
817
+ // Pre-extracted for cross-chunk edge detection
818
+ constructorParamTypes: types,
819
+ injectTokens: Object.fromEntries(injectTokens),
793
820
  };
794
821
  },
795
822
  priority: 1,
@@ -193,7 +193,7 @@ export const CORE_TYPESCRIPT_SCHEMA = {
193
193
  relationships: [
194
194
  {
195
195
  edgeType: CoreEdgeType.EXTENDS,
196
- method: 'getBaseClass',
196
+ method: 'getExtends', // Use getExtends() instead of getBaseClass() - works in lazy mode
197
197
  cardinality: 'single',
198
198
  targetNodeType: CoreNodeType.CLASS_DECLARATION,
199
199
  },
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Timeout Configuration
3
+ * Centralized timeout settings for external services (Neo4j, OpenAI)
4
+ */
5
+ export const TIMEOUT_DEFAULTS = {
6
+ neo4j: {
7
+ queryTimeoutMs: 30_000, // 30 seconds
8
+ connectionTimeoutMs: 10_000, // 10 seconds
9
+ },
10
+ openai: {
11
+ embeddingTimeoutMs: 60_000, // 60 seconds
12
+ assistantTimeoutMs: 120_000, // 2 minutes (assistant/threads can take longer)
13
+ },
14
+ };
15
+ /**
16
+ * Get timeout configuration with environment variable overrides
17
+ */
18
+ export const getTimeoutConfig = () => ({
19
+ neo4j: {
20
+ queryTimeoutMs: parseInt(process.env.NEO4J_QUERY_TIMEOUT_MS ?? '', 10) || TIMEOUT_DEFAULTS.neo4j.queryTimeoutMs,
21
+ connectionTimeoutMs: parseInt(process.env.NEO4J_CONNECTION_TIMEOUT_MS ?? '', 10) || TIMEOUT_DEFAULTS.neo4j.connectionTimeoutMs,
22
+ },
23
+ openai: {
24
+ embeddingTimeoutMs: parseInt(process.env.OPENAI_EMBEDDING_TIMEOUT_MS ?? '', 10) || TIMEOUT_DEFAULTS.openai.embeddingTimeoutMs,
25
+ assistantTimeoutMs: parseInt(process.env.OPENAI_ASSISTANT_TIMEOUT_MS ?? '', 10) || TIMEOUT_DEFAULTS.openai.assistantTimeoutMs,
26
+ },
27
+ });
@@ -1,15 +1,57 @@
1
1
  import OpenAI from 'openai';
2
+ import { debugLog } from '../../mcp/utils.js';
3
+ import { getTimeoutConfig } from '../config/timeouts.js';
4
+ /**
5
+ * Custom error class for OpenAI configuration issues
6
+ * Provides helpful guidance on how to resolve the issue
7
+ */
8
+ export class OpenAIConfigError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'OpenAIConfigError';
12
+ }
13
+ }
14
+ /**
15
+ * Custom error class for OpenAI API issues (rate limits, quota, etc.)
16
+ */
17
+ export class OpenAIAPIError extends Error {
18
+ statusCode;
19
+ constructor(message, statusCode) {
20
+ super(message);
21
+ this.statusCode = statusCode;
22
+ this.name = 'OpenAIAPIError';
23
+ }
24
+ }
25
+ export const EMBEDDING_BATCH_CONFIG = {
26
+ maxBatchSize: 100, // OpenAI supports up to 2048, but 100 is efficient
27
+ delayBetweenBatchesMs: 500, // Rate limit protection (500ms = ~2 batches/sec)
28
+ };
2
29
  export class EmbeddingsService {
3
30
  openai;
4
31
  model;
5
32
  constructor(model = 'text-embedding-3-large') {
6
33
  const apiKey = process.env.OPENAI_API_KEY;
7
34
  if (!apiKey) {
8
- throw new Error('OPENAI_API_KEY environment variable is required');
35
+ throw new OpenAIConfigError('OPENAI_API_KEY environment variable is required.\n\n' +
36
+ 'To use semantic search features (search_codebase, natural_language_to_cypher), ' +
37
+ 'you need an OpenAI API key.\n\n' +
38
+ 'Set it in your environment:\n' +
39
+ ' export OPENAI_API_KEY=sk-...\n\n' +
40
+ 'Or in .env file:\n' +
41
+ ' OPENAI_API_KEY=sk-...\n\n' +
42
+ 'Alternative: Use impact_analysis or traverse_from_node which do not require OpenAI.');
9
43
  }
10
- this.openai = new OpenAI({ apiKey });
44
+ const timeoutConfig = getTimeoutConfig();
45
+ this.openai = new OpenAI({
46
+ apiKey,
47
+ timeout: timeoutConfig.openai.embeddingTimeoutMs,
48
+ maxRetries: 2, // Built-in retry for transient errors
49
+ });
11
50
  this.model = model;
12
51
  }
52
+ /**
53
+ * Embed a single text string
54
+ */
13
55
  async embedText(text) {
14
56
  try {
15
57
  const response = await this.openai.embeddings.create({
@@ -19,8 +61,86 @@ export class EmbeddingsService {
19
61
  return response.data[0].embedding;
20
62
  }
21
63
  catch (error) {
64
+ // Handle specific error types with helpful messages
65
+ if (error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) {
66
+ throw new OpenAIAPIError('OpenAI embedding request timed out. Consider increasing OPENAI_EMBEDDING_TIMEOUT_MS.');
67
+ }
68
+ if (error.status === 429) {
69
+ throw new OpenAIAPIError('OpenAI rate limit exceeded.\n\n' +
70
+ 'This usually means:\n' +
71
+ '- You have hit your API rate limit\n' +
72
+ '- You have exceeded your quota\n\n' +
73
+ 'Solutions:\n' +
74
+ '- Wait a few minutes and try again\n' +
75
+ '- Check your OpenAI usage at https://platform.openai.com/usage\n' +
76
+ '- Use impact_analysis or traverse_from_node which do not require OpenAI', 429);
77
+ }
78
+ if (error.status === 401) {
79
+ throw new OpenAIAPIError('OpenAI API key is invalid or expired.\n\n' + 'Please check your OPENAI_API_KEY environment variable.', 401);
80
+ }
81
+ if (error.status === 402 || error.message?.includes('quota') || error.message?.includes('billing')) {
82
+ throw new OpenAIAPIError('OpenAI quota exceeded or billing issue.\n\n' +
83
+ 'Solutions:\n' +
84
+ '- Check your OpenAI billing at https://platform.openai.com/settings/organization/billing\n' +
85
+ '- Add credits to your account\n' +
86
+ '- Use impact_analysis or traverse_from_node which do not require OpenAI', 402);
87
+ }
22
88
  console.error('Error creating embedding:', error);
23
89
  throw error;
24
90
  }
25
91
  }
92
+ /**
93
+ * Embed multiple texts in a single API call.
94
+ * OpenAI's embedding API supports batching natively.
95
+ */
96
+ async embedTexts(texts) {
97
+ if (texts.length === 0)
98
+ return [];
99
+ try {
100
+ const response = await this.openai.embeddings.create({
101
+ model: this.model,
102
+ input: texts,
103
+ });
104
+ // Map results back to original order (OpenAI returns with index)
105
+ return response.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
106
+ }
107
+ catch (error) {
108
+ if (error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) {
109
+ throw new OpenAIAPIError('OpenAI batch embedding request timed out. Consider reducing batch size or increasing timeout.');
110
+ }
111
+ // Rate limited - SDK already has maxRetries:2, don't add recursive retry
112
+ if (error.status === 429) {
113
+ throw new OpenAIAPIError('OpenAI rate limit exceeded. Wait a few minutes and try again.\n' +
114
+ 'Check your usage at https://platform.openai.com/usage', 429);
115
+ }
116
+ // Re-throw with context
117
+ throw new OpenAIAPIError(`OpenAI embedding failed: ${error.message}`, error.status);
118
+ }
119
+ }
120
+ /**
121
+ * Embed texts in batches with rate limiting.
122
+ * Returns array of embeddings in same order as input.
123
+ * @param texts Array of texts to embed
124
+ * @param batchSize Number of texts per API call (default: 100)
125
+ */
126
+ async embedTextsInBatches(texts, batchSize = EMBEDDING_BATCH_CONFIG.maxBatchSize) {
127
+ await debugLog('Batch embedding started', { textCount: texts.length });
128
+ const results = [];
129
+ const totalBatches = Math.ceil(texts.length / batchSize);
130
+ for (let i = 0; i < texts.length; i += batchSize) {
131
+ const batch = texts.slice(i, i + batchSize);
132
+ const batchIndex = Math.floor(i / batchSize) + 1;
133
+ await debugLog('Embedding batch progress', { batchIndex, totalBatches, batchSize: batch.length });
134
+ const batchResults = await this.embedTexts(batch);
135
+ results.push(...batchResults);
136
+ // Rate limit protection between batches
137
+ if (i + batchSize < texts.length) {
138
+ await this.delay(EMBEDDING_BATCH_CONFIG.delayBetweenBatchesMs);
139
+ }
140
+ }
141
+ return results;
142
+ }
143
+ delay(ms) {
144
+ return new Promise((resolve) => setTimeout(resolve, ms));
145
+ }
26
146
  }