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.
- package/README.md +221 -101
- package/dist/core/config/fairsquare-framework-schema.js +47 -60
- package/dist/core/config/nestjs-framework-schema.js +71 -44
- 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 +416 -17
- package/dist/core/parsers/parser-factory.js +5 -3
- package/dist/core/parsers/typescript-parser.js +618 -50
- package/dist/core/parsers/workspace-parser.js +554 -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 +153 -5
- 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 +45 -6
- 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 +2 -2
- package/dist/mcp/tools/check-parse-status.tool.js +64 -0
- package/dist/mcp/tools/impact-analysis.tool.js +84 -18
- package/dist/mcp/tools/index.js +13 -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 +318 -58
- package/dist/mcp/tools/search-codebase.tool.js +56 -16
- 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 -13
- package/dist/mcp/workers/parse-worker.js +198 -0
- package/dist/storage/neo4j/neo4j.service.js +147 -48
- 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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
280
|
-
if (!
|
|
310
|
+
const targetName = targetNode.properties?.name;
|
|
311
|
+
if (!targetName)
|
|
281
312
|
return null;
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
}
|