code-graph-context 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -101
- package/dist/core/config/fairsquare-framework-schema.js +47 -60
- package/dist/core/config/nestjs-framework-schema.js +11 -1
- package/dist/core/config/schema.js +1 -1
- package/dist/core/config/timeouts.js +27 -0
- package/dist/core/embeddings/embeddings.service.js +122 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +428 -30
- package/dist/core/parsers/parser-factory.js +6 -6
- package/dist/core/parsers/typescript-parser.js +639 -44
- package/dist/core/parsers/workspace-parser.js +553 -0
- package/dist/core/utils/edge-factory.js +37 -0
- package/dist/core/utils/file-change-detection.js +105 -0
- package/dist/core/utils/file-utils.js +20 -0
- package/dist/core/utils/index.js +3 -0
- package/dist/core/utils/path-utils.js +75 -0
- package/dist/core/utils/progress-reporter.js +112 -0
- package/dist/core/utils/project-id.js +176 -0
- package/dist/core/utils/retry.js +41 -0
- package/dist/core/workspace/index.js +4 -0
- package/dist/core/workspace/workspace-detector.js +221 -0
- package/dist/mcp/constants.js +172 -7
- package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
- package/dist/mcp/handlers/file-change-detection.js +105 -0
- package/dist/mcp/handlers/graph-generator.handler.js +97 -32
- package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
- package/dist/mcp/handlers/streaming-import.handler.js +210 -0
- package/dist/mcp/handlers/traversal.handler.js +130 -71
- package/dist/mcp/mcp.server.js +46 -7
- package/dist/mcp/service-init.js +79 -0
- package/dist/mcp/services/job-manager.js +165 -0
- package/dist/mcp/services/watch-manager.js +376 -0
- package/dist/mcp/services.js +48 -127
- package/dist/mcp/tools/check-parse-status.tool.js +64 -0
- package/dist/mcp/tools/impact-analysis.tool.js +319 -0
- package/dist/mcp/tools/index.js +15 -1
- package/dist/mcp/tools/list-projects.tool.js +62 -0
- package/dist/mcp/tools/list-watchers.tool.js +51 -0
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
- package/dist/mcp/tools/parse-typescript-project.tool.js +325 -60
- package/dist/mcp/tools/search-codebase.tool.js +57 -23
- package/dist/mcp/tools/start-watch-project.tool.js +100 -0
- package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
- package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
- package/dist/mcp/utils.js +35 -12
- package/dist/mcp/workers/parse-worker.js +198 -0
- package/dist/storage/neo4j/neo4j.service.js +273 -34
- package/package.json +4 -2
|
@@ -103,6 +103,14 @@ function extractModuleControllers(node) {
|
|
|
103
103
|
function extractModuleExports(node) {
|
|
104
104
|
return extractModuleArrayProperty(node, 'exports');
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Escapes special regex characters in a string to prevent ReDoS attacks.
|
|
108
|
+
* @param str The string to escape
|
|
109
|
+
* @returns The escaped string safe for use in a regex
|
|
110
|
+
*/
|
|
111
|
+
function escapeRegexChars(str) {
|
|
112
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
}
|
|
106
114
|
function extractModuleArrayProperty(node, propertyName) {
|
|
107
115
|
const decorator = node.getDecorator('Module');
|
|
108
116
|
if (!decorator)
|
|
@@ -111,7 +119,9 @@ function extractModuleArrayProperty(node, propertyName) {
|
|
|
111
119
|
if (args.length === 0)
|
|
112
120
|
return [];
|
|
113
121
|
const configText = args[0].getText();
|
|
114
|
-
|
|
122
|
+
// SECURITY: Escape propertyName to prevent ReDoS attacks
|
|
123
|
+
const escapedPropertyName = escapeRegexChars(propertyName);
|
|
124
|
+
const regex = new RegExp(`${escapedPropertyName}\\s*:\\s*\\[([^\\]]+)\\]`);
|
|
115
125
|
const match = configText.match(regex);
|
|
116
126
|
if (!match)
|
|
117
127
|
return [];
|
|
@@ -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
|
}
|