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.
Files changed (47) hide show
  1. package/README.md +221 -101
  2. package/dist/core/config/fairsquare-framework-schema.js +47 -60
  3. package/dist/core/config/nestjs-framework-schema.js +11 -1
  4. package/dist/core/config/schema.js +1 -1
  5. package/dist/core/config/timeouts.js +27 -0
  6. package/dist/core/embeddings/embeddings.service.js +122 -2
  7. package/dist/core/embeddings/natural-language-to-cypher.service.js +428 -30
  8. package/dist/core/parsers/parser-factory.js +6 -6
  9. package/dist/core/parsers/typescript-parser.js +639 -44
  10. package/dist/core/parsers/workspace-parser.js +553 -0
  11. package/dist/core/utils/edge-factory.js +37 -0
  12. package/dist/core/utils/file-change-detection.js +105 -0
  13. package/dist/core/utils/file-utils.js +20 -0
  14. package/dist/core/utils/index.js +3 -0
  15. package/dist/core/utils/path-utils.js +75 -0
  16. package/dist/core/utils/progress-reporter.js +112 -0
  17. package/dist/core/utils/project-id.js +176 -0
  18. package/dist/core/utils/retry.js +41 -0
  19. package/dist/core/workspace/index.js +4 -0
  20. package/dist/core/workspace/workspace-detector.js +221 -0
  21. package/dist/mcp/constants.js +172 -7
  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 +46 -7
  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 +48 -127
  33. package/dist/mcp/tools/check-parse-status.tool.js +64 -0
  34. package/dist/mcp/tools/impact-analysis.tool.js +319 -0
  35. package/dist/mcp/tools/index.js +15 -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 +325 -60
  40. package/dist/mcp/tools/search-codebase.tool.js +57 -23
  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 -12
  45. package/dist/mcp/workers/parse-worker.js +198 -0
  46. package/dist/storage/neo4j/neo4j.service.js +273 -34
  47. 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
- const regex = new RegExp(`${propertyName}\\s*:\\s*\\[([^\\]]+)\\]`);
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: '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
  }