gitnexus 1.4.10 → 1.5.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 (186) hide show
  1. package/README.md +6 -5
  2. package/dist/cli/ai-context.d.ts +4 -1
  3. package/dist/cli/ai-context.js +19 -11
  4. package/dist/cli/analyze.d.ts +6 -0
  5. package/dist/cli/analyze.js +105 -251
  6. package/dist/cli/eval-server.js +20 -11
  7. package/dist/cli/index-repo.js +20 -22
  8. package/dist/cli/index.js +8 -7
  9. package/dist/cli/mcp.js +1 -1
  10. package/dist/cli/serve.js +29 -1
  11. package/dist/cli/setup.js +9 -9
  12. package/dist/cli/skill-gen.js +15 -9
  13. package/dist/cli/wiki.d.ts +2 -0
  14. package/dist/cli/wiki.js +141 -26
  15. package/dist/config/ignore-service.js +102 -22
  16. package/dist/config/supported-languages.d.ts +8 -42
  17. package/dist/config/supported-languages.js +8 -43
  18. package/dist/core/augmentation/engine.js +19 -7
  19. package/dist/core/embeddings/embedder.js +19 -15
  20. package/dist/core/embeddings/embedding-pipeline.js +6 -6
  21. package/dist/core/embeddings/http-client.js +3 -3
  22. package/dist/core/embeddings/text-generator.js +9 -24
  23. package/dist/core/embeddings/types.d.ts +1 -1
  24. package/dist/core/embeddings/types.js +1 -7
  25. package/dist/core/graph/graph.js +6 -2
  26. package/dist/core/graph/types.d.ts +9 -59
  27. package/dist/core/ingestion/ast-cache.js +3 -3
  28. package/dist/core/ingestion/call-processor.d.ts +20 -2
  29. package/dist/core/ingestion/call-processor.js +347 -144
  30. package/dist/core/ingestion/call-routing.js +10 -4
  31. package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
  32. package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
  33. package/dist/core/ingestion/call-sites/java.d.ts +9 -0
  34. package/dist/core/ingestion/call-sites/java.js +30 -0
  35. package/dist/core/ingestion/cluster-enricher.js +6 -8
  36. package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
  37. package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
  38. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  39. package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
  40. package/dist/core/ingestion/cobol-processor.js +102 -56
  41. package/dist/core/ingestion/community-processor.js +21 -15
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +5 -6
  44. package/dist/core/ingestion/export-detection.js +32 -9
  45. package/dist/core/ingestion/field-extractor.d.ts +1 -1
  46. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
  48. package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
  49. package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
  50. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
  51. package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
  52. package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
  53. package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
  54. package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
  55. package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
  56. package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
  57. package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
  58. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
  59. package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
  60. package/dist/core/ingestion/field-extractors/generic.js +6 -0
  61. package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
  62. package/dist/core/ingestion/field-extractors/typescript.js +1 -1
  63. package/dist/core/ingestion/field-types.d.ts +4 -2
  64. package/dist/core/ingestion/filesystem-walker.js +3 -3
  65. package/dist/core/ingestion/framework-detection.d.ts +1 -1
  66. package/dist/core/ingestion/framework-detection.js +355 -85
  67. package/dist/core/ingestion/heritage-processor.d.ts +24 -0
  68. package/dist/core/ingestion/heritage-processor.js +99 -8
  69. package/dist/core/ingestion/import-processor.js +44 -15
  70. package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
  71. package/dist/core/ingestion/import-resolvers/dart.js +1 -1
  72. package/dist/core/ingestion/import-resolvers/go.js +4 -2
  73. package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
  74. package/dist/core/ingestion/import-resolvers/php.js +4 -4
  75. package/dist/core/ingestion/import-resolvers/python.js +1 -1
  76. package/dist/core/ingestion/import-resolvers/rust.js +9 -3
  77. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  78. package/dist/core/ingestion/import-resolvers/standard.js +6 -5
  79. package/dist/core/ingestion/import-resolvers/swift.js +2 -1
  80. package/dist/core/ingestion/import-resolvers/utils.js +26 -7
  81. package/dist/core/ingestion/language-config.js +5 -4
  82. package/dist/core/ingestion/language-provider.d.ts +7 -2
  83. package/dist/core/ingestion/languages/c-cpp.js +106 -21
  84. package/dist/core/ingestion/languages/cobol.js +1 -1
  85. package/dist/core/ingestion/languages/csharp.js +96 -19
  86. package/dist/core/ingestion/languages/dart.js +23 -7
  87. package/dist/core/ingestion/languages/go.js +1 -1
  88. package/dist/core/ingestion/languages/index.d.ts +1 -1
  89. package/dist/core/ingestion/languages/index.js +2 -3
  90. package/dist/core/ingestion/languages/java.js +4 -1
  91. package/dist/core/ingestion/languages/kotlin.js +60 -13
  92. package/dist/core/ingestion/languages/php.js +102 -25
  93. package/dist/core/ingestion/languages/python.js +28 -5
  94. package/dist/core/ingestion/languages/ruby.js +56 -14
  95. package/dist/core/ingestion/languages/rust.js +55 -11
  96. package/dist/core/ingestion/languages/swift.js +112 -27
  97. package/dist/core/ingestion/languages/typescript.js +95 -19
  98. package/dist/core/ingestion/markdown-processor.js +5 -5
  99. package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
  100. package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
  101. package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
  102. package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
  103. package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
  104. package/dist/core/ingestion/method-extractors/generic.js +137 -0
  105. package/dist/core/ingestion/method-types.d.ts +61 -0
  106. package/dist/core/ingestion/method-types.js +2 -0
  107. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  108. package/dist/core/ingestion/mro-processor.js +12 -8
  109. package/dist/core/ingestion/named-binding-processor.js +2 -2
  110. package/dist/core/ingestion/named-bindings/rust.js +3 -1
  111. package/dist/core/ingestion/parsing-processor.js +74 -24
  112. package/dist/core/ingestion/pipeline.d.ts +2 -1
  113. package/dist/core/ingestion/pipeline.js +208 -102
  114. package/dist/core/ingestion/process-processor.js +12 -10
  115. package/dist/core/ingestion/resolution-context.js +3 -3
  116. package/dist/core/ingestion/route-extractors/middleware.js +31 -7
  117. package/dist/core/ingestion/route-extractors/php.js +2 -1
  118. package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
  119. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  120. package/dist/core/ingestion/structure-processor.js +4 -4
  121. package/dist/core/ingestion/symbol-table.d.ts +1 -1
  122. package/dist/core/ingestion/symbol-table.js +22 -6
  123. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  124. package/dist/core/ingestion/tree-sitter-queries.js +1 -1
  125. package/dist/core/ingestion/type-env.d.ts +2 -2
  126. package/dist/core/ingestion/type-env.js +75 -50
  127. package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
  128. package/dist/core/ingestion/type-extractors/csharp.js +24 -14
  129. package/dist/core/ingestion/type-extractors/dart.js +6 -8
  130. package/dist/core/ingestion/type-extractors/go.js +7 -6
  131. package/dist/core/ingestion/type-extractors/jvm.js +10 -21
  132. package/dist/core/ingestion/type-extractors/php.js +26 -13
  133. package/dist/core/ingestion/type-extractors/python.js +11 -15
  134. package/dist/core/ingestion/type-extractors/ruby.js +8 -3
  135. package/dist/core/ingestion/type-extractors/rust.js +6 -8
  136. package/dist/core/ingestion/type-extractors/shared.js +134 -50
  137. package/dist/core/ingestion/type-extractors/swift.js +16 -13
  138. package/dist/core/ingestion/type-extractors/typescript.js +23 -15
  139. package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
  140. package/dist/core/ingestion/utils/ast-helpers.js +72 -35
  141. package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
  142. package/dist/core/ingestion/utils/call-analysis.js +96 -49
  143. package/dist/core/ingestion/utils/event-loop.js +1 -1
  144. package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
  145. package/dist/core/ingestion/workers/parse-worker.js +364 -84
  146. package/dist/core/ingestion/workers/worker-pool.js +5 -10
  147. package/dist/core/lbug/csv-generator.js +54 -15
  148. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  149. package/dist/core/lbug/lbug-adapter.js +86 -23
  150. package/dist/core/lbug/schema.d.ts +3 -6
  151. package/dist/core/lbug/schema.js +6 -30
  152. package/dist/core/run-analyze.d.ts +49 -0
  153. package/dist/core/run-analyze.js +257 -0
  154. package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
  155. package/dist/core/tree-sitter/parser-loader.js +1 -1
  156. package/dist/core/wiki/cursor-client.js +2 -7
  157. package/dist/core/wiki/generator.js +38 -23
  158. package/dist/core/wiki/graph-queries.js +10 -10
  159. package/dist/core/wiki/html-viewer.js +7 -3
  160. package/dist/core/wiki/llm-client.d.ts +23 -2
  161. package/dist/core/wiki/llm-client.js +96 -26
  162. package/dist/core/wiki/prompts.js +7 -6
  163. package/dist/mcp/core/embedder.js +1 -1
  164. package/dist/mcp/core/lbug-adapter.d.ts +4 -1
  165. package/dist/mcp/core/lbug-adapter.js +17 -7
  166. package/dist/mcp/local/local-backend.js +247 -95
  167. package/dist/mcp/resources.js +14 -6
  168. package/dist/mcp/server.js +13 -5
  169. package/dist/mcp/staleness.js +5 -1
  170. package/dist/mcp/tools.js +100 -23
  171. package/dist/server/analyze-job.d.ts +53 -0
  172. package/dist/server/analyze-job.js +146 -0
  173. package/dist/server/analyze-worker.d.ts +13 -0
  174. package/dist/server/analyze-worker.js +59 -0
  175. package/dist/server/api.js +795 -44
  176. package/dist/server/git-clone.d.ts +25 -0
  177. package/dist/server/git-clone.js +91 -0
  178. package/dist/storage/git.js +1 -3
  179. package/dist/storage/repo-manager.d.ts +5 -2
  180. package/dist/storage/repo-manager.js +4 -4
  181. package/dist/types/pipeline.d.ts +1 -21
  182. package/dist/types/pipeline.js +1 -18
  183. package/hooks/claude/gitnexus-hook.cjs +52 -22
  184. package/package.json +3 -2
  185. package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
  186. package/dist/core/ingestion/utils/language-detection.js +0 -70
@@ -81,10 +81,18 @@ function parseUri(uri) {
81
81
  const repoName = decodeURIComponent(repoMatch[1]);
82
82
  const rest = repoMatch[2];
83
83
  if (rest.startsWith('cluster/')) {
84
- return { repoName, resourceType: 'cluster', param: decodeURIComponent(rest.replace('cluster/', '')) };
84
+ return {
85
+ repoName,
86
+ resourceType: 'cluster',
87
+ param: decodeURIComponent(rest.replace('cluster/', '')),
88
+ };
85
89
  }
86
90
  if (rest.startsWith('process/')) {
87
- return { repoName, resourceType: 'process', param: decodeURIComponent(rest.replace('process/', '')) };
91
+ return {
92
+ repoName,
93
+ resourceType: 'process',
94
+ param: decodeURIComponent(rest.replace('process/', '')),
95
+ };
88
96
  }
89
97
  return { repoName, resourceType: rest };
90
98
  }
@@ -163,10 +171,10 @@ async function getContextResource(backend, repoName) {
163
171
  // Check staleness
164
172
  const repoPath = repo.repoPath;
165
173
  const lastCommit = repo.lastCommit || 'HEAD';
166
- const staleness = repoPath ? checkStaleness(repoPath, lastCommit) : { isStale: false, commitsBehind: 0 };
167
- const lines = [
168
- `project: ${context.projectName}`,
169
- ];
174
+ const staleness = repoPath
175
+ ? checkStaleness(repoPath, lastCommit)
176
+ : { isStale: false, commitsBehind: 0 };
177
+ const lines = [`project: ${context.projectName}`];
170
178
  if (staleness.isStale && staleness.hint) {
171
179
  lines.push('');
172
180
  lines.push(`staleness: "${staleness.hint}"`);
@@ -77,7 +77,7 @@ export function createMCPServer(backend) {
77
77
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
78
78
  const resources = getResourceDefinitions();
79
79
  return {
80
- resources: resources.map(r => ({
80
+ resources: resources.map((r) => ({
81
81
  uri: r.uri,
82
82
  name: r.name,
83
83
  description: r.description,
@@ -89,7 +89,7 @@ export function createMCPServer(backend) {
89
89
  server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
90
90
  const templates = getResourceTemplates();
91
91
  return {
92
- resourceTemplates: templates.map(t => ({
92
+ resourceTemplates: templates.map((t) => ({
93
93
  uriTemplate: t.uriTemplate,
94
94
  name: t.name,
95
95
  description: t.description,
@@ -168,7 +168,11 @@ export function createMCPServer(backend) {
168
168
  name: 'detect_impact',
169
169
  description: 'Analyze the impact of your current changes before committing. Guides through scope selection, change detection, process analysis, and risk assessment.',
170
170
  arguments: [
171
- { name: 'scope', description: 'What to analyze: unstaged, staged, all, or compare', required: false },
171
+ {
172
+ name: 'scope',
173
+ description: 'What to analyze: unstaged, staged, all, or compare',
174
+ required: false,
175
+ },
172
176
  { name: 'base_ref', description: 'Branch/commit for compare scope', required: false },
173
177
  ],
174
178
  },
@@ -176,7 +180,11 @@ export function createMCPServer(backend) {
176
180
  name: 'generate_map',
177
181
  description: 'Generate architecture documentation from the knowledge graph. Creates a codebase overview with execution flows and mermaid diagrams.',
178
182
  arguments: [
179
- { name: 'repo', description: 'Repository name (omit if only one indexed)', required: false },
183
+ {
184
+ name: 'repo',
185
+ description: 'Repository name (omit if only one indexed)',
186
+ required: false,
187
+ },
180
188
  ],
181
189
  },
182
190
  ],
@@ -247,7 +255,7 @@ export async function startMCPServer(backend) {
247
255
  return realStdoutWrite;
248
256
  const val = Reflect.get(target, prop, receiver);
249
257
  return typeof val === 'function' ? val.bind(target) : val;
250
- }
258
+ },
251
259
  });
252
260
  const transport = new CompatibleStdioServerTransport(process.stdin, _safeStdout);
253
261
  await server.connect(transport);
@@ -11,7 +11,11 @@ import { execFileSync } from 'child_process';
11
11
  export function checkStaleness(repoPath, lastCommit) {
12
12
  try {
13
13
  // Get count of commits between lastCommit and HEAD
14
- const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
14
+ const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], {
15
+ cwd: repoPath,
16
+ encoding: 'utf-8',
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ }).trim();
15
19
  const commitsBehind = parseInt(result, 10) || 0;
16
20
  if (commitsBehind > 0) {
17
21
  return {
package/dist/mcp/tools.js CHANGED
@@ -40,12 +40,29 @@ Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank
40
40
  type: 'object',
41
41
  properties: {
42
42
  query: { type: 'string', description: 'Natural language or keyword search query' },
43
- task_context: { type: 'string', description: 'What you are working on (e.g., "adding OAuth support"). Helps ranking.' },
44
- goal: { type: 'string', description: 'What you want to find (e.g., "existing auth validation logic"). Helps ranking.' },
43
+ task_context: {
44
+ type: 'string',
45
+ description: 'What you are working on (e.g., "adding OAuth support"). Helps ranking.',
46
+ },
47
+ goal: {
48
+ type: 'string',
49
+ description: 'What you want to find (e.g., "existing auth validation logic"). Helps ranking.',
50
+ },
45
51
  limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 },
46
- max_symbols: { type: 'number', description: 'Max symbols per process (default: 10)', default: 10 },
47
- include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
48
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
52
+ max_symbols: {
53
+ type: 'number',
54
+ description: 'Max symbols per process (default: 10)',
55
+ default: 10,
56
+ },
57
+ include_content: {
58
+ type: 'boolean',
59
+ description: 'Include full symbol source code (default: false)',
60
+ default: false,
61
+ },
62
+ repo: {
63
+ type: 'string',
64
+ description: 'Repository name or path. Omit if only one repo is indexed.',
65
+ },
49
66
  },
50
67
  required: ['query'],
51
68
  },
@@ -100,7 +117,10 @@ TIPS:
100
117
  type: 'object',
101
118
  properties: {
102
119
  query: { type: 'string', description: 'Cypher query to execute' },
103
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
120
+ repo: {
121
+ type: 'string',
122
+ description: 'Repository name or path. Omit if only one repo is indexed.',
123
+ },
104
124
  },
105
125
  required: ['query'],
106
126
  },
@@ -120,10 +140,20 @@ NOTE: ACCESSES edges (field read/write tracking) are included in context results
120
140
  type: 'object',
121
141
  properties: {
122
142
  name: { type: 'string', description: 'Symbol name (e.g., "validateUser", "AuthService")' },
123
- uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)' },
143
+ uid: {
144
+ type: 'string',
145
+ description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)',
146
+ },
124
147
  file_path: { type: 'string', description: 'File path to disambiguate common names' },
125
- include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
126
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
148
+ include_content: {
149
+ type: 'boolean',
150
+ description: 'Include full symbol source code (default: false)',
151
+ default: false,
152
+ },
153
+ repo: {
154
+ type: 'string',
155
+ description: 'Repository name or path. Omit if only one repo is indexed.',
156
+ },
127
157
  },
128
158
  required: [],
129
159
  },
@@ -140,9 +170,20 @@ Returns: changed symbols, affected processes, and a risk summary.`,
140
170
  inputSchema: {
141
171
  type: 'object',
142
172
  properties: {
143
- scope: { type: 'string', description: 'What to analyze: "unstaged" (default), "staged", "all", or "compare"', enum: ['unstaged', 'staged', 'all', 'compare'], default: 'unstaged' },
144
- base_ref: { type: 'string', description: 'Branch/commit for "compare" scope (e.g., "main")' },
145
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
173
+ scope: {
174
+ type: 'string',
175
+ description: 'What to analyze: "unstaged" (default), "staged", "all", or "compare"',
176
+ enum: ['unstaged', 'staged', 'all', 'compare'],
177
+ default: 'unstaged',
178
+ },
179
+ base_ref: {
180
+ type: 'string',
181
+ description: 'Branch/commit for "compare" scope (e.g., "main")',
182
+ },
183
+ repo: {
184
+ type: 'string',
185
+ description: 'Repository name or path. Omit if only one repo is indexed.',
186
+ },
146
187
  },
147
188
  required: [],
148
189
  },
@@ -162,11 +203,21 @@ Each edit is tagged with confidence:
162
203
  type: 'object',
163
204
  properties: {
164
205
  symbol_name: { type: 'string', description: 'Current symbol name to rename' },
165
- symbol_uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity)' },
206
+ symbol_uid: {
207
+ type: 'string',
208
+ description: 'Direct symbol UID from prior tool results (zero-ambiguity)',
209
+ },
166
210
  new_name: { type: 'string', description: 'The new name for the symbol' },
167
211
  file_path: { type: 'string', description: 'File path to disambiguate common names' },
168
- dry_run: { type: 'boolean', description: 'Preview edits without modifying files (default: true)', default: true },
169
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
212
+ dry_run: {
213
+ type: 'boolean',
214
+ description: 'Preview edits without modifying files (default: true)',
215
+ default: true,
216
+ },
217
+ repo: {
218
+ type: 'string',
219
+ description: 'Repository name or path. Omit if only one repo is indexed.',
220
+ },
170
221
  },
171
222
  required: ['new_name'],
172
223
  },
@@ -199,12 +250,26 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
199
250
  type: 'object',
200
251
  properties: {
201
252
  target: { type: 'string', description: 'Name of function, class, or file to analyze' },
202
- direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
203
- maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
204
- relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)' },
253
+ direction: {
254
+ type: 'string',
255
+ description: 'upstream (what depends on this) or downstream (what this depends on)',
256
+ },
257
+ maxDepth: {
258
+ type: 'number',
259
+ description: 'Max relationship depth (default: 3)',
260
+ default: 3,
261
+ },
262
+ relationTypes: {
263
+ type: 'array',
264
+ items: { type: 'string' },
265
+ description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)',
266
+ },
205
267
  includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
206
268
  minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
207
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
269
+ repo: {
270
+ type: 'string',
271
+ description: 'Repository name or path. Omit if only one repo is indexed.',
272
+ },
208
273
  },
209
274
  required: ['target', 'direction'],
210
275
  },
@@ -220,8 +285,14 @@ Returns: route nodes with their handlers, middleware wrapper chains (e.g., withA
220
285
  inputSchema: {
221
286
  type: 'object',
222
287
  properties: {
223
- route: { type: 'string', description: 'Filter by route path (e.g., "/api/grants"). Omit for all routes.' },
224
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
288
+ route: {
289
+ type: 'string',
290
+ description: 'Filter by route path (e.g., "/api/grants"). Omit for all routes.',
291
+ },
292
+ repo: {
293
+ type: 'string',
294
+ description: 'Repository name or path. Omit if only one repo is indexed.',
295
+ },
225
296
  },
226
297
  required: [],
227
298
  },
@@ -253,8 +324,14 @@ Returns routes that have both detected response keys AND consumers. Shows top-le
253
324
  inputSchema: {
254
325
  type: 'object',
255
326
  properties: {
256
- route: { type: 'string', description: 'Check a specific route (e.g., "/api/grants"). Omit to check all routes.' },
257
- repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
327
+ route: {
328
+ type: 'string',
329
+ description: 'Check a specific route (e.g., "/api/grants"). Omit to check all routes.',
330
+ },
331
+ repo: {
332
+ type: 'string',
333
+ description: 'Repository name or path. Omit if only one repo is indexed.',
334
+ },
258
335
  },
259
336
  required: [],
260
337
  },
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Analyze Job Manager
3
+ *
4
+ * Tracks server-side analysis jobs with:
5
+ * - In-memory Map storage
6
+ * - Single-slot concurrency (one active job at a time)
7
+ * - Same-repo deduplication (returns existing job)
8
+ * - Progress event emission for SSE relay
9
+ * - 1-hour TTL cleanup for completed/failed jobs
10
+ */
11
+ import type { ChildProcess } from 'child_process';
12
+ export interface AnalyzeJobProgress {
13
+ phase: string;
14
+ percent: number;
15
+ message: string;
16
+ }
17
+ export interface AnalyzeJob {
18
+ id: string;
19
+ status: 'queued' | 'cloning' | 'analyzing' | 'loading' | 'complete' | 'failed';
20
+ repoUrl?: string;
21
+ repoPath?: string;
22
+ repoName?: string;
23
+ progress: AnalyzeJobProgress;
24
+ error?: string;
25
+ startedAt: number;
26
+ completedAt?: number;
27
+ /** Number of times the worker has been retried after a crash. */
28
+ retryCount: number;
29
+ }
30
+ export declare class JobManager {
31
+ private jobs;
32
+ private children;
33
+ private timeouts;
34
+ private emitter;
35
+ private cleanupTimer;
36
+ constructor();
37
+ /** Create a new job, or return existing active job for the same repo. */
38
+ createJob(params: {
39
+ repoUrl?: string;
40
+ repoPath?: string;
41
+ }): AnalyzeJob;
42
+ getJob(id: string): AnalyzeJob | undefined;
43
+ updateJob(id: string, update: Partial<Pick<AnalyzeJob, 'status' | 'progress' | 'error' | 'repoPath' | 'repoName' | 'completedAt'>>): void;
44
+ /** Register a child process for a job — enables cancellation and timeout. */
45
+ registerChild(jobId: string, child: ChildProcess): void;
46
+ /** Cancel a running job — sends SIGTERM to child process. */
47
+ cancelJob(jobId: string, reason?: string): boolean;
48
+ /** Subscribe to progress events for a job. Returns unsubscribe function. */
49
+ onProgress(jobId: string, listener: (progress: AnalyzeJobProgress) => void): () => void;
50
+ dispose(): void;
51
+ private isTerminal;
52
+ private cleanup;
53
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Analyze Job Manager
3
+ *
4
+ * Tracks server-side analysis jobs with:
5
+ * - In-memory Map storage
6
+ * - Single-slot concurrency (one active job at a time)
7
+ * - Same-repo deduplication (returns existing job)
8
+ * - Progress event emission for SSE relay
9
+ * - 1-hour TTL cleanup for completed/failed jobs
10
+ */
11
+ import { randomUUID } from 'crypto';
12
+ import { EventEmitter } from 'events';
13
+ const JOB_TTL_MS = 60 * 60 * 1000; // 1 hour
14
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
15
+ const JOB_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
16
+ export class JobManager {
17
+ jobs = new Map();
18
+ children = new Map();
19
+ timeouts = new Map();
20
+ emitter = new EventEmitter();
21
+ cleanupTimer;
22
+ constructor() {
23
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
24
+ }
25
+ /** Create a new job, or return existing active job for the same repo. */
26
+ createJob(params) {
27
+ // Dedup: return existing active job for the same repo (by URL or path)
28
+ for (const job of this.jobs.values()) {
29
+ if (!this.isTerminal(job.status)) {
30
+ const isSameRepo = (params.repoUrl && job.repoUrl === params.repoUrl) ||
31
+ (params.repoPath && job.repoPath === params.repoPath);
32
+ if (isSameRepo) {
33
+ return job;
34
+ }
35
+ }
36
+ }
37
+ // Single-slot: reject if another job is active (different repo)
38
+ for (const job of this.jobs.values()) {
39
+ if (!this.isTerminal(job.status)) {
40
+ throw new Error(`Analysis already in progress (job ${job.id})`);
41
+ }
42
+ }
43
+ const job = {
44
+ id: randomUUID(),
45
+ status: 'queued',
46
+ repoUrl: params.repoUrl,
47
+ repoPath: params.repoPath,
48
+ progress: { phase: 'queued', percent: 0, message: 'Waiting to start...' },
49
+ startedAt: Date.now(),
50
+ retryCount: 0,
51
+ };
52
+ this.jobs.set(job.id, job);
53
+ return job;
54
+ }
55
+ getJob(id) {
56
+ return this.jobs.get(id);
57
+ }
58
+ updateJob(id, update) {
59
+ const job = this.jobs.get(id);
60
+ if (!job)
61
+ return;
62
+ Object.assign(job, update);
63
+ if (this.isTerminal(job.status)) {
64
+ job.completedAt = job.completedAt ?? Date.now();
65
+ }
66
+ // Emit exactly one event per updateJob call to prevent SSE double-write
67
+ if (update.status === 'complete' || update.status === 'failed') {
68
+ // Terminal event takes precedence — don't also emit the progress event
69
+ this.emitter.emit(`progress:${id}`, {
70
+ phase: update.status,
71
+ percent: update.status === 'complete' ? 100 : job.progress.percent,
72
+ message: update.status === 'complete' ? 'Complete' : update.error || 'Failed',
73
+ });
74
+ }
75
+ else if (update.progress) {
76
+ this.emitter.emit(`progress:${id}`, update.progress);
77
+ }
78
+ }
79
+ /** Register a child process for a job — enables cancellation and timeout. */
80
+ registerChild(jobId, child) {
81
+ this.children.set(jobId, child);
82
+ // 30-minute timeout
83
+ const timer = setTimeout(() => {
84
+ const job = this.jobs.get(jobId);
85
+ if (job && !this.isTerminal(job.status)) {
86
+ this.cancelJob(jobId, 'Analysis timed out (30 minute limit)');
87
+ }
88
+ }, JOB_TIMEOUT_MS);
89
+ this.timeouts.set(jobId, timer);
90
+ // Clean up tracking when child exits
91
+ child.on('exit', () => {
92
+ this.children.delete(jobId);
93
+ const t = this.timeouts.get(jobId);
94
+ if (t) {
95
+ clearTimeout(t);
96
+ this.timeouts.delete(jobId);
97
+ }
98
+ });
99
+ }
100
+ /** Cancel a running job — sends SIGTERM to child process. */
101
+ cancelJob(jobId, reason) {
102
+ const job = this.jobs.get(jobId);
103
+ if (!job || this.isTerminal(job.status))
104
+ return false;
105
+ const child = this.children.get(jobId);
106
+ if (child) {
107
+ child.kill('SIGTERM');
108
+ }
109
+ this.updateJob(jobId, {
110
+ status: 'failed',
111
+ error: reason || 'Analysis cancelled',
112
+ });
113
+ return true;
114
+ }
115
+ /** Subscribe to progress events for a job. Returns unsubscribe function. */
116
+ onProgress(jobId, listener) {
117
+ const event = `progress:${jobId}`;
118
+ this.emitter.on(event, listener);
119
+ return () => this.emitter.off(event, listener);
120
+ }
121
+ dispose() {
122
+ // Kill all active child processes
123
+ for (const child of this.children.values()) {
124
+ child.kill('SIGTERM');
125
+ }
126
+ this.children.clear();
127
+ // Clear all timeouts
128
+ for (const timer of this.timeouts.values()) {
129
+ clearTimeout(timer);
130
+ }
131
+ this.timeouts.clear();
132
+ clearInterval(this.cleanupTimer);
133
+ this.emitter.removeAllListeners();
134
+ }
135
+ isTerminal(status) {
136
+ return status === 'complete' || status === 'failed';
137
+ }
138
+ cleanup() {
139
+ const now = Date.now();
140
+ for (const [id, job] of this.jobs) {
141
+ if (this.isTerminal(job.status) && job.completedAt && now - job.completedAt > JOB_TTL_MS) {
142
+ this.jobs.delete(id);
143
+ }
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Analyze Worker — Forked Child Process
3
+ *
4
+ * This file is the entry point for `child_process.fork()`.
5
+ * It runs runFullAnalysis in an isolated process with 8GB heap.
6
+ *
7
+ * IPC Protocol:
8
+ * Parent -> Child: { type: 'start', repoPath: string, options: AnalyzeOptions }
9
+ * Child -> Parent: { type: 'progress', phase: string, percent: number, message: string }
10
+ * Child -> Parent: { type: 'complete', result: AnalyzeResult }
11
+ * Child -> Parent: { type: 'error', message: string }
12
+ */
13
+ export {};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Analyze Worker — Forked Child Process
3
+ *
4
+ * This file is the entry point for `child_process.fork()`.
5
+ * It runs runFullAnalysis in an isolated process with 8GB heap.
6
+ *
7
+ * IPC Protocol:
8
+ * Parent -> Child: { type: 'start', repoPath: string, options: AnalyzeOptions }
9
+ * Child -> Parent: { type: 'progress', phase: string, percent: number, message: string }
10
+ * Child -> Parent: { type: 'complete', result: AnalyzeResult }
11
+ * Child -> Parent: { type: 'error', message: string }
12
+ */
13
+ import { runFullAnalysis } from '../core/run-analyze.js';
14
+ import { closeLbug } from '../core/lbug/lbug-adapter.js';
15
+ function send(msg) {
16
+ process.send?.(msg);
17
+ }
18
+ // Catch uncaught exceptions and unhandled rejections — report to parent
19
+ process.on('uncaughtException', (err) => {
20
+ send({ type: 'error', message: err?.message || 'Uncaught exception in worker' });
21
+ setTimeout(() => process.exit(1), 500);
22
+ });
23
+ process.on('unhandledRejection', (reason) => {
24
+ send({ type: 'error', message: reason?.message || 'Unhandled rejection in worker' });
25
+ setTimeout(() => process.exit(1), 500);
26
+ });
27
+ // Handle graceful shutdown — notify parent before exit
28
+ process.on('SIGTERM', async () => {
29
+ send({ type: 'error', message: 'Analysis cancelled (worker received SIGTERM)' });
30
+ try {
31
+ await closeLbug();
32
+ }
33
+ catch { }
34
+ process.exit(0);
35
+ });
36
+ // Listen for start command from parent — guarded against re-entry
37
+ let started = false;
38
+ process.on('message', async (msg) => {
39
+ if (msg.type !== 'start' || started)
40
+ return;
41
+ started = true;
42
+ try {
43
+ const result = await runFullAnalysis(msg.repoPath, msg.options, {
44
+ onProgress: (phase, percent, message) => {
45
+ send({ type: 'progress', phase, percent, message });
46
+ },
47
+ onLog: (message) => {
48
+ send({ type: 'progress', phase: 'log', percent: -1, message });
49
+ },
50
+ });
51
+ send({ type: 'complete', result });
52
+ }
53
+ catch (err) {
54
+ send({ type: 'error', message: err?.message || 'Analysis failed' });
55
+ }
56
+ // LadybugDB's native module prevents clean exit — force it
57
+ // (same reason the CLI uses process.exit(0))
58
+ setTimeout(() => process.exit(0), 500);
59
+ });