gitnexus 1.4.10 → 1.5.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 (211) hide show
  1. package/README.md +6 -5
  2. package/dist/_shared/graph/types.d.ts +65 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -0
  4. package/dist/_shared/graph/types.js +8 -0
  5. package/dist/_shared/graph/types.js.map +1 -0
  6. package/dist/_shared/index.d.ts +7 -0
  7. package/dist/_shared/index.d.ts.map +1 -0
  8. package/dist/_shared/index.js +6 -0
  9. package/dist/_shared/index.js.map +1 -0
  10. package/dist/_shared/language-detection.d.ts +23 -0
  11. package/dist/_shared/language-detection.d.ts.map +1 -0
  12. package/dist/_shared/language-detection.js +137 -0
  13. package/dist/_shared/language-detection.js.map +1 -0
  14. package/dist/_shared/languages.d.ts +25 -0
  15. package/dist/_shared/languages.d.ts.map +1 -0
  16. package/dist/_shared/languages.js +26 -0
  17. package/dist/_shared/languages.js.map +1 -0
  18. package/dist/_shared/lbug/schema-constants.d.ts +16 -0
  19. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -0
  20. package/dist/_shared/lbug/schema-constants.js +64 -0
  21. package/dist/_shared/lbug/schema-constants.js.map +1 -0
  22. package/dist/_shared/pipeline.d.ts +16 -0
  23. package/dist/_shared/pipeline.d.ts.map +1 -0
  24. package/dist/_shared/pipeline.js +5 -0
  25. package/dist/_shared/pipeline.js.map +1 -0
  26. package/dist/cli/ai-context.d.ts +4 -1
  27. package/dist/cli/ai-context.js +19 -11
  28. package/dist/cli/analyze.d.ts +6 -0
  29. package/dist/cli/analyze.js +105 -251
  30. package/dist/cli/eval-server.js +20 -11
  31. package/dist/cli/index-repo.js +20 -22
  32. package/dist/cli/index.js +8 -7
  33. package/dist/cli/mcp.js +1 -1
  34. package/dist/cli/serve.js +29 -1
  35. package/dist/cli/setup.js +9 -9
  36. package/dist/cli/skill-gen.js +15 -9
  37. package/dist/cli/wiki.d.ts +2 -0
  38. package/dist/cli/wiki.js +141 -26
  39. package/dist/config/ignore-service.js +102 -22
  40. package/dist/config/supported-languages.d.ts +8 -42
  41. package/dist/config/supported-languages.js +8 -43
  42. package/dist/core/augmentation/engine.js +19 -7
  43. package/dist/core/embeddings/embedder.js +19 -15
  44. package/dist/core/embeddings/embedding-pipeline.js +6 -6
  45. package/dist/core/embeddings/http-client.js +3 -3
  46. package/dist/core/embeddings/text-generator.js +9 -24
  47. package/dist/core/embeddings/types.d.ts +1 -1
  48. package/dist/core/embeddings/types.js +1 -7
  49. package/dist/core/graph/graph.js +6 -2
  50. package/dist/core/graph/types.d.ts +9 -59
  51. package/dist/core/ingestion/ast-cache.js +3 -3
  52. package/dist/core/ingestion/call-processor.d.ts +20 -2
  53. package/dist/core/ingestion/call-processor.js +347 -144
  54. package/dist/core/ingestion/call-routing.js +10 -4
  55. package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
  56. package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
  57. package/dist/core/ingestion/call-sites/java.d.ts +9 -0
  58. package/dist/core/ingestion/call-sites/java.js +30 -0
  59. package/dist/core/ingestion/cluster-enricher.js +6 -8
  60. package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
  61. package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
  62. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  63. package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
  64. package/dist/core/ingestion/cobol-processor.js +102 -56
  65. package/dist/core/ingestion/community-processor.js +21 -15
  66. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
  67. package/dist/core/ingestion/entry-point-scoring.js +5 -6
  68. package/dist/core/ingestion/export-detection.js +32 -9
  69. package/dist/core/ingestion/field-extractor.d.ts +1 -1
  70. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
  71. package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
  72. package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
  73. package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
  74. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
  75. package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
  76. package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
  77. package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
  78. package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
  79. package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
  80. package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
  81. package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
  82. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
  83. package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
  84. package/dist/core/ingestion/field-extractors/generic.js +6 -0
  85. package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
  86. package/dist/core/ingestion/field-extractors/typescript.js +1 -1
  87. package/dist/core/ingestion/field-types.d.ts +4 -2
  88. package/dist/core/ingestion/filesystem-walker.js +3 -3
  89. package/dist/core/ingestion/framework-detection.d.ts +1 -1
  90. package/dist/core/ingestion/framework-detection.js +355 -85
  91. package/dist/core/ingestion/heritage-processor.d.ts +24 -0
  92. package/dist/core/ingestion/heritage-processor.js +99 -8
  93. package/dist/core/ingestion/import-processor.js +44 -15
  94. package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
  95. package/dist/core/ingestion/import-resolvers/dart.js +1 -1
  96. package/dist/core/ingestion/import-resolvers/go.js +4 -2
  97. package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
  98. package/dist/core/ingestion/import-resolvers/php.js +4 -4
  99. package/dist/core/ingestion/import-resolvers/python.js +1 -1
  100. package/dist/core/ingestion/import-resolvers/rust.js +9 -3
  101. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  102. package/dist/core/ingestion/import-resolvers/standard.js +6 -5
  103. package/dist/core/ingestion/import-resolvers/swift.js +2 -1
  104. package/dist/core/ingestion/import-resolvers/utils.js +26 -7
  105. package/dist/core/ingestion/language-config.js +5 -4
  106. package/dist/core/ingestion/language-provider.d.ts +7 -2
  107. package/dist/core/ingestion/languages/c-cpp.js +106 -21
  108. package/dist/core/ingestion/languages/cobol.js +1 -1
  109. package/dist/core/ingestion/languages/csharp.js +96 -19
  110. package/dist/core/ingestion/languages/dart.js +23 -7
  111. package/dist/core/ingestion/languages/go.js +1 -1
  112. package/dist/core/ingestion/languages/index.d.ts +1 -1
  113. package/dist/core/ingestion/languages/index.js +2 -3
  114. package/dist/core/ingestion/languages/java.js +4 -1
  115. package/dist/core/ingestion/languages/kotlin.js +60 -13
  116. package/dist/core/ingestion/languages/php.js +102 -25
  117. package/dist/core/ingestion/languages/python.js +28 -5
  118. package/dist/core/ingestion/languages/ruby.js +56 -14
  119. package/dist/core/ingestion/languages/rust.js +55 -11
  120. package/dist/core/ingestion/languages/swift.js +112 -27
  121. package/dist/core/ingestion/languages/typescript.js +95 -19
  122. package/dist/core/ingestion/markdown-processor.js +5 -5
  123. package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
  124. package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
  125. package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
  126. package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
  127. package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
  128. package/dist/core/ingestion/method-extractors/generic.js +137 -0
  129. package/dist/core/ingestion/method-types.d.ts +61 -0
  130. package/dist/core/ingestion/method-types.js +2 -0
  131. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  132. package/dist/core/ingestion/mro-processor.js +12 -8
  133. package/dist/core/ingestion/named-binding-processor.js +2 -2
  134. package/dist/core/ingestion/named-bindings/rust.js +3 -1
  135. package/dist/core/ingestion/parsing-processor.js +74 -24
  136. package/dist/core/ingestion/pipeline.d.ts +2 -1
  137. package/dist/core/ingestion/pipeline.js +208 -102
  138. package/dist/core/ingestion/process-processor.js +12 -10
  139. package/dist/core/ingestion/resolution-context.js +3 -3
  140. package/dist/core/ingestion/route-extractors/middleware.js +31 -7
  141. package/dist/core/ingestion/route-extractors/php.js +2 -1
  142. package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
  143. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  144. package/dist/core/ingestion/structure-processor.js +4 -4
  145. package/dist/core/ingestion/symbol-table.d.ts +1 -1
  146. package/dist/core/ingestion/symbol-table.js +22 -6
  147. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  148. package/dist/core/ingestion/tree-sitter-queries.js +1 -1
  149. package/dist/core/ingestion/type-env.d.ts +2 -2
  150. package/dist/core/ingestion/type-env.js +75 -50
  151. package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
  152. package/dist/core/ingestion/type-extractors/csharp.js +24 -14
  153. package/dist/core/ingestion/type-extractors/dart.js +6 -8
  154. package/dist/core/ingestion/type-extractors/go.js +7 -6
  155. package/dist/core/ingestion/type-extractors/jvm.js +10 -21
  156. package/dist/core/ingestion/type-extractors/php.js +26 -13
  157. package/dist/core/ingestion/type-extractors/python.js +11 -15
  158. package/dist/core/ingestion/type-extractors/ruby.js +8 -3
  159. package/dist/core/ingestion/type-extractors/rust.js +6 -8
  160. package/dist/core/ingestion/type-extractors/shared.js +134 -50
  161. package/dist/core/ingestion/type-extractors/swift.js +16 -13
  162. package/dist/core/ingestion/type-extractors/typescript.js +23 -15
  163. package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
  164. package/dist/core/ingestion/utils/ast-helpers.js +72 -35
  165. package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
  166. package/dist/core/ingestion/utils/call-analysis.js +96 -49
  167. package/dist/core/ingestion/utils/event-loop.js +1 -1
  168. package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
  169. package/dist/core/ingestion/workers/parse-worker.js +364 -84
  170. package/dist/core/ingestion/workers/worker-pool.js +5 -10
  171. package/dist/core/lbug/csv-generator.js +54 -15
  172. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  173. package/dist/core/lbug/lbug-adapter.js +86 -23
  174. package/dist/core/lbug/schema.d.ts +3 -6
  175. package/dist/core/lbug/schema.js +6 -30
  176. package/dist/core/run-analyze.d.ts +49 -0
  177. package/dist/core/run-analyze.js +257 -0
  178. package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
  179. package/dist/core/tree-sitter/parser-loader.js +1 -1
  180. package/dist/core/wiki/cursor-client.js +2 -7
  181. package/dist/core/wiki/generator.js +38 -23
  182. package/dist/core/wiki/graph-queries.js +10 -10
  183. package/dist/core/wiki/html-viewer.js +7 -3
  184. package/dist/core/wiki/llm-client.d.ts +23 -2
  185. package/dist/core/wiki/llm-client.js +96 -26
  186. package/dist/core/wiki/prompts.js +7 -6
  187. package/dist/mcp/core/embedder.js +1 -1
  188. package/dist/mcp/core/lbug-adapter.d.ts +4 -1
  189. package/dist/mcp/core/lbug-adapter.js +17 -7
  190. package/dist/mcp/local/local-backend.js +247 -95
  191. package/dist/mcp/resources.js +14 -6
  192. package/dist/mcp/server.js +13 -5
  193. package/dist/mcp/staleness.js +5 -1
  194. package/dist/mcp/tools.js +100 -23
  195. package/dist/server/analyze-job.d.ts +53 -0
  196. package/dist/server/analyze-job.js +146 -0
  197. package/dist/server/analyze-worker.d.ts +13 -0
  198. package/dist/server/analyze-worker.js +59 -0
  199. package/dist/server/api.js +795 -44
  200. package/dist/server/git-clone.d.ts +25 -0
  201. package/dist/server/git-clone.js +91 -0
  202. package/dist/storage/git.js +1 -3
  203. package/dist/storage/repo-manager.d.ts +5 -2
  204. package/dist/storage/repo-manager.js +4 -4
  205. package/dist/types/pipeline.d.ts +1 -21
  206. package/dist/types/pipeline.js +1 -18
  207. package/hooks/claude/gitnexus-hook.cjs +52 -22
  208. package/package.json +5 -4
  209. package/scripts/build.js +69 -0
  210. package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
  211. package/dist/core/ingestion/utils/language-detection.js +0 -70
@@ -2,21 +2,19 @@
2
2
  * Analyze Command
3
3
  *
4
4
  * Indexes a repository and stores the knowledge graph in .gitnexus/
5
+ *
6
+ * Delegates core analysis to the shared runFullAnalysis orchestrator.
7
+ * This CLI wrapper handles: heap management, progress bar, SIGINT,
8
+ * skill generation (--skills), summary output, and process.exit().
5
9
  */
6
10
  import path from 'path';
7
11
  import { execFileSync } from 'child_process';
8
12
  import v8 from 'v8';
9
13
  import cliProgress from 'cli-progress';
10
- import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
11
- import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings } from '../core/lbug/lbug-adapter.js';
12
- // Embedding imports are lazy (dynamic import) so onnxruntime-node is never
13
- // loaded when embeddings are not requested. This avoids crashes on Node
14
- // versions whose ABI is not yet supported by the native binary (#89).
15
- // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
16
- import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, cleanupOldKuzuFiles } from '../storage/repo-manager.js';
17
- import { getCurrentCommit, getGitRoot, hasGitDir } from '../storage/git.js';
18
- import { generateAIContextFiles } from './ai-context.js';
19
- import { generateSkillFiles } from './skill-gen.js';
14
+ import { closeLbug } from '../core/lbug/lbug-adapter.js';
15
+ import { getStoragePaths, getGlobalRegistryPath } from '../storage/repo-manager.js';
16
+ import { getGitRoot, hasGitDir } from '../storage/git.js';
17
+ import { runFullAnalysis } from '../core/run-analyze.js';
20
18
  import fs from 'fs/promises';
21
19
  const HEAP_MB = 8192;
22
20
  const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
@@ -39,23 +37,6 @@ function ensureHeap() {
39
37
  }
40
38
  return true;
41
39
  }
42
- /** Threshold: auto-skip embeddings for repos with more nodes than this */
43
- const EMBEDDING_NODE_LIMIT = 50_000;
44
- const PHASE_LABELS = {
45
- extracting: 'Scanning files',
46
- structure: 'Building structure',
47
- parsing: 'Parsing code',
48
- imports: 'Resolving imports',
49
- calls: 'Tracing calls',
50
- heritage: 'Extracting inheritance',
51
- communities: 'Detecting communities',
52
- processes: 'Detecting processes',
53
- complete: 'Pipeline complete',
54
- lbug: 'Loading into LadybugDB',
55
- fts: 'Creating search indexes',
56
- embeddings: 'Generating embeddings',
57
- done: 'Done',
58
- };
59
40
  export const analyzeCommand = async (inputPath, options) => {
60
41
  if (ensureHeap())
61
42
  return;
@@ -91,26 +72,12 @@ export const analyzeCommand = async (inputPath, options) => {
91
72
  if (!repoHasGit) {
92
73
  console.log(' Warning: no .git directory found \u2014 commit-tracking and incremental updates disabled.\n');
93
74
  }
94
- const { storagePath, lbugPath } = getStoragePaths(repoPath);
95
- // Clean up stale KuzuDB files from before the LadybugDB migration.
96
- // If kuzu existed but lbug doesn't, we're doing a migration re-index — say so.
97
- const kuzuResult = await cleanupOldKuzuFiles(storagePath);
98
- if (kuzuResult.found && kuzuResult.needsReindex) {
99
- console.log(' Migrating from KuzuDB to LadybugDB — rebuilding index...\n');
100
- }
101
- const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
102
- const existingMeta = await loadMeta(storagePath);
103
- if (existingMeta && !options?.force && !options?.skills && existingMeta.lastCommit === currentCommit) {
104
- // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
105
- if (currentCommit !== '') {
106
- console.log(' Already up to date\n');
107
- return;
108
- }
109
- }
75
+ // KuzuDB migration cleanup is handled by runFullAnalysis internally.
76
+ // Note: --skills is handled after runFullAnalysis using the returned pipelineResult.
110
77
  if (process.env.GITNEXUS_NO_GITIGNORE) {
111
78
  console.log(' GITNEXUS_NO_GITIGNORE is set — skipping .gitignore (still reading .gitnexusignore)\n');
112
79
  }
113
- // Single progress bar for entire pipeline
80
+ // ── CLI progress bar setup ─────────────────────────────────────────
114
81
  const bar = new cliProgress.SingleBar({
115
82
  format: ' {bar} {percentage}% | {phase}',
116
83
  barCompleteChar: '\u2588',
@@ -122,35 +89,33 @@ export const analyzeCommand = async (inputPath, options) => {
122
89
  stopOnComplete: false,
123
90
  }, cliProgress.Presets.shades_grey);
124
91
  bar.start(100, 0, { phase: 'Initializing...' });
125
- // Graceful SIGINT handling — clean up resources and exit
92
+ // Graceful SIGINT handling
126
93
  let aborted = false;
127
94
  const sigintHandler = () => {
128
95
  if (aborted)
129
- process.exit(1); // Second Ctrl-C: force exit
96
+ process.exit(1);
130
97
  aborted = true;
131
98
  bar.stop();
132
99
  console.log('\n Interrupted — cleaning up...');
133
- closeLbug().catch(() => { }).finally(() => process.exit(130));
100
+ closeLbug()
101
+ .catch(() => { })
102
+ .finally(() => process.exit(130));
134
103
  };
135
104
  process.on('SIGINT', sigintHandler);
136
- // Route all console output through bar.log() so the bar doesn't stamp itself
137
- // multiple times when other code writes to stdout/stderr mid-render.
105
+ // Route console output through bar.log() to prevent progress bar corruption
138
106
  const origLog = console.log.bind(console);
139
107
  const origWarn = console.warn.bind(console);
140
108
  const origError = console.error.bind(console);
141
109
  const barLog = (...args) => {
142
- // Clear the bar line, print the message, then let the next bar.update redraw
143
110
  process.stdout.write('\x1b[2K\r');
144
- origLog(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
111
+ origLog(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' '));
145
112
  };
146
113
  console.log = barLog;
147
114
  console.warn = barLog;
148
115
  console.error = barLog;
149
- // Track elapsed time per phase — both updateBar and the interval use the
150
- // same format so they don't flicker against each other.
116
+ // Track elapsed time per phase
151
117
  let lastPhaseLabel = 'Initializing...';
152
118
  let phaseStart = Date.now();
153
- /** Update bar with phase label + elapsed seconds (shown after 3s). */
154
119
  const updateBar = (value, phaseLabel) => {
155
120
  if (phaseLabel !== lastPhaseLabel) {
156
121
  lastPhaseLabel = phaseLabel;
@@ -160,217 +125,106 @@ export const analyzeCommand = async (inputPath, options) => {
160
125
  const display = elapsed >= 3 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
161
126
  bar.update(value, { phase: display });
162
127
  };
163
- // Tick elapsed seconds for phases with infrequent progress callbacks
164
- // (e.g. CSV streaming, FTS indexing). Uses the same display format as
165
- // updateBar so there's no flickering.
166
128
  const elapsedTimer = setInterval(() => {
167
129
  const elapsed = Math.round((Date.now() - phaseStart) / 1000);
168
130
  if (elapsed >= 3) {
169
131
  bar.update({ phase: `${lastPhaseLabel} (${elapsed}s)` });
170
132
  }
171
133
  }, 1000);
172
- const t0Global = Date.now();
173
- // ── Cache embeddings from existing index before rebuild ────────────
174
- let cachedEmbeddingNodeIds = new Set();
175
- let cachedEmbeddings = [];
176
- if (options?.embeddings && existingMeta && !options?.force) {
177
- try {
178
- updateBar(0, 'Caching embeddings...');
179
- await initLbug(lbugPath);
180
- const cached = await loadCachedEmbeddings();
181
- cachedEmbeddingNodeIds = cached.embeddingNodeIds;
182
- cachedEmbeddings = cached.embeddings;
183
- await closeLbug();
184
- }
185
- catch {
186
- try {
187
- await closeLbug();
188
- }
189
- catch { }
190
- }
191
- }
192
- // ── Phase 1: Full Pipeline (0–60%) ─────────────────────────────────
193
- const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
194
- const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
195
- const scaled = Math.round(progress.percent * 0.6);
196
- updateBar(scaled, phaseLabel);
197
- });
198
- // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────────
199
- updateBar(60, 'Loading into LadybugDB...');
200
- await closeLbug();
201
- const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
202
- for (const f of lbugFiles) {
203
- try {
204
- await fs.rm(f, { recursive: true, force: true });
205
- }
206
- catch { }
207
- }
208
- const t0Lbug = Date.now();
209
- await initLbug(lbugPath);
210
- let lbugMsgCount = 0;
211
- const lbugResult = await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
212
- lbugMsgCount++;
213
- const progress = Math.min(84, 60 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 24));
214
- updateBar(progress, msg);
215
- });
216
- const lbugTime = ((Date.now() - t0Lbug) / 1000).toFixed(1);
217
- const lbugWarnings = lbugResult.warnings;
218
- // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
219
- updateBar(85, 'Creating search indexes...');
220
- const t0Fts = Date.now();
134
+ const t0 = Date.now();
135
+ // ── Run shared analysis orchestrator ───────────────────────────────
221
136
  try {
222
- await createFTSIndex('File', 'file_fts', ['name', 'content']);
223
- await createFTSIndex('Function', 'function_fts', ['name', 'content']);
224
- await createFTSIndex('Class', 'class_fts', ['name', 'content']);
225
- await createFTSIndex('Method', 'method_fts', ['name', 'content']);
226
- await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
227
- }
228
- catch (e) {
229
- // Non-fatal — FTS is best-effort
230
- }
231
- const ftsTime = ((Date.now() - t0Fts) / 1000).toFixed(1);
232
- // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
233
- if (cachedEmbeddings.length > 0) {
234
- // Check if cached embedding dimensions match current schema
235
- const cachedDims = cachedEmbeddings[0].embedding.length;
236
- const { EMBEDDING_DIMS } = await import('../core/lbug/schema.js');
237
- if (cachedDims !== EMBEDDING_DIMS) {
238
- // Dimensions changed (e.g. switched embedding model) — discard cache and re-embed all
239
- console.error(`⚠️ Embedding dimensions changed (${cachedDims}d → ${EMBEDDING_DIMS}d), discarding cache`);
240
- cachedEmbeddings = [];
241
- cachedEmbeddingNodeIds = new Set();
137
+ const result = await runFullAnalysis(repoPath, {
138
+ force: options?.force || options?.skills,
139
+ embeddings: options?.embeddings,
140
+ skipGit: options?.skipGit,
141
+ skipAgentsMd: options?.skipAgentsMd,
142
+ }, {
143
+ onProgress: (_phase, percent, message) => {
144
+ updateBar(percent, message);
145
+ },
146
+ onLog: barLog,
147
+ });
148
+ if (result.alreadyUpToDate) {
149
+ clearInterval(elapsedTimer);
150
+ process.removeListener('SIGINT', sigintHandler);
151
+ console.log = origLog;
152
+ console.warn = origWarn;
153
+ console.error = origError;
154
+ bar.stop();
155
+ console.log(' Already up to date\n');
156
+ // Safe to return without process.exit(0) — the early-return path in
157
+ // runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
158
+ return;
242
159
  }
243
- else {
244
- updateBar(88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
245
- const EMBED_BATCH = 200;
246
- for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
247
- const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
248
- const paramsList = batch.map(e => ({ nodeId: e.nodeId, embedding: e.embedding }));
249
- try {
250
- await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
160
+ // Skill generation (CLI-only, uses pipeline result from analysis)
161
+ if (options?.skills && result.pipelineResult) {
162
+ updateBar(99, 'Generating skill files...');
163
+ try {
164
+ const { generateSkillFiles } = await import('./skill-gen.js');
165
+ const { generateAIContextFiles } = await import('./ai-context.js');
166
+ const skillResult = await generateSkillFiles(repoPath, result.repoName, result.pipelineResult);
167
+ if (skillResult.skills.length > 0) {
168
+ barLog(` Generated ${skillResult.skills.length} skill files`);
169
+ // Re-generate AI context files now that we have skill info
170
+ const s = result.stats;
171
+ const communityResult = result.pipelineResult?.communityResult;
172
+ let aggregatedClusterCount = 0;
173
+ if (communityResult?.communities) {
174
+ const groups = new Map();
175
+ for (const c of communityResult.communities) {
176
+ const label = c.heuristicLabel || c.label || 'Unknown';
177
+ groups.set(label, (groups.get(label) || 0) + c.symbolCount);
178
+ }
179
+ aggregatedClusterCount = Array.from(groups.values()).filter((count) => count >= 5).length;
180
+ }
181
+ const { storagePath: sp } = getStoragePaths(repoPath);
182
+ await generateAIContextFiles(repoPath, sp, result.repoName, {
183
+ files: s.files ?? 0,
184
+ nodes: s.nodes ?? 0,
185
+ edges: s.edges ?? 0,
186
+ communities: s.communities,
187
+ clusters: aggregatedClusterCount,
188
+ processes: s.processes,
189
+ }, skillResult.skills, { skipAgentsMd: options?.skipAgentsMd });
251
190
  }
252
- catch { /* some may fail if node was removed, that's fine */ }
191
+ }
192
+ catch {
193
+ /* best-effort */
253
194
  }
254
195
  }
255
- }
256
- // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
257
- const stats = await getLbugStats();
258
- let embeddingTime = '0.0';
259
- let embeddingSkipped = true;
260
- let embeddingSkipReason = 'off (use --embeddings to enable)';
261
- if (options?.embeddings) {
262
- if (stats.nodes > EMBEDDING_NODE_LIMIT) {
263
- embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
264
- }
265
- else {
266
- embeddingSkipped = false;
196
+ const totalTime = ((Date.now() - t0) / 1000).toFixed(1);
197
+ clearInterval(elapsedTimer);
198
+ process.removeListener('SIGINT', sigintHandler);
199
+ console.log = origLog;
200
+ console.warn = origWarn;
201
+ console.error = origError;
202
+ bar.update(100, { phase: 'Done' });
203
+ bar.stop();
204
+ // ── Summary ────────────────────────────────────────────────────
205
+ const s = result.stats;
206
+ console.log(`\n Repository indexed successfully (${totalTime}s)\n`);
207
+ console.log(` ${(s.nodes ?? 0).toLocaleString()} nodes | ${(s.edges ?? 0).toLocaleString()} edges | ${s.communities ?? 0} clusters | ${s.processes ?? 0} flows`);
208
+ console.log(` ${repoPath}`);
209
+ try {
210
+ await fs.access(getGlobalRegistryPath());
267
211
  }
268
- }
269
- if (!embeddingSkipped) {
270
- const { isHttpMode } = await import('../core/embeddings/http-client.js');
271
- const httpMode = isHttpMode();
272
- updateBar(90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
273
- const t0Emb = Date.now();
274
- const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js');
275
- await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
276
- const scaled = 90 + Math.round((progress.percent / 100) * 8);
277
- const label = progress.phase === 'loading-model'
278
- ? (httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...')
279
- : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
280
- updateBar(scaled, label);
281
- }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
282
- embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
283
- }
284
- // ── Phase 5: Finalize (98–100%) ───────────────────────────────────
285
- updateBar(98, 'Saving metadata...');
286
- // Count embeddings in the index (cached + newly generated)
287
- let embeddingCount = 0;
288
- try {
289
- const embResult = await executeQuery(`MATCH (e:CodeEmbedding) RETURN count(e) AS cnt`);
290
- embeddingCount = embResult?.[0]?.cnt ?? 0;
291
- }
292
- catch { /* table may not exist if embeddings never ran */ }
293
- const meta = {
294
- repoPath,
295
- lastCommit: currentCommit,
296
- indexedAt: new Date().toISOString(),
297
- stats: {
298
- files: pipelineResult.totalFileCount,
299
- nodes: stats.nodes,
300
- edges: stats.edges,
301
- communities: pipelineResult.communityResult?.stats.totalCommunities,
302
- processes: pipelineResult.processResult?.stats.totalProcesses,
303
- embeddings: embeddingCount,
304
- },
305
- };
306
- await saveMeta(storagePath, meta);
307
- await registerRepo(repoPath, meta);
308
- // Only attempt to update .gitignore when a .git directory is present.
309
- // Use hasGitDir (filesystem check) rather than git CLI subprocess
310
- // so we skip correctly for --skip-git folders even if git CLI is available.
311
- if (hasGitDir(repoPath)) {
312
- await addToGitignore(repoPath);
313
- }
314
- const projectName = path.basename(repoPath);
315
- let aggregatedClusterCount = 0;
316
- if (pipelineResult.communityResult?.communities) {
317
- const groups = new Map();
318
- for (const c of pipelineResult.communityResult.communities) {
319
- const label = c.heuristicLabel || c.label || 'Unknown';
320
- groups.set(label, (groups.get(label) || 0) + c.symbolCount);
212
+ catch {
213
+ console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
321
214
  }
322
- aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
323
- }
324
- let generatedSkills = [];
325
- if (options?.skills && pipelineResult.communityResult) {
326
- updateBar(99, 'Generating skill files...');
327
- const skillResult = await generateSkillFiles(repoPath, projectName, pipelineResult);
328
- generatedSkills = skillResult.skills;
329
- }
330
- const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
331
- files: pipelineResult.totalFileCount,
332
- nodes: stats.nodes,
333
- edges: stats.edges,
334
- communities: pipelineResult.communityResult?.stats.totalCommunities,
335
- clusters: aggregatedClusterCount,
336
- processes: pipelineResult.processResult?.stats.totalProcesses,
337
- }, generatedSkills);
338
- await closeLbug();
339
- // Note: we intentionally do NOT call disposeEmbedder() here.
340
- // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
341
- // Since the process exits immediately after, Node.js reclaims everything.
342
- const totalTime = ((Date.now() - t0Global) / 1000).toFixed(1);
343
- clearInterval(elapsedTimer);
344
- process.removeListener('SIGINT', sigintHandler);
345
- console.log = origLog;
346
- console.warn = origWarn;
347
- console.error = origError;
348
- bar.update(100, { phase: 'Done' });
349
- bar.stop();
350
- // ── Summary ───────────────────────────────────────────────────────
351
- const embeddingsCached = cachedEmbeddings.length > 0;
352
- console.log(`\n Repository indexed successfully (${totalTime}s)${embeddingsCached ? ` [${cachedEmbeddings.length} embeddings cached]` : ''}\n`);
353
- console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineResult.processResult?.stats.totalProcesses || 0} flows`);
354
- console.log(` LadybugDB ${lbugTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
355
- console.log(` ${repoPath}`);
356
- if (aiContext.files.length > 0) {
357
- console.log(` Context: ${aiContext.files.join(', ')}`);
358
- }
359
- // Show a quiet summary if some edge types needed fallback insertion
360
- if (lbugWarnings.length > 0) {
361
- const totalFallback = lbugWarnings.reduce((sum, w) => {
362
- const m = w.match(/\((\d+) edges\)/);
363
- return sum + (m ? parseInt(m[1]) : 0);
364
- }, 0);
365
- console.log(` Note: ${totalFallback} edges across ${lbugWarnings.length} types inserted via fallback (schema will be updated in next release)`);
366
- }
367
- try {
368
- await fs.access(getGlobalRegistryPath());
369
- }
370
- catch {
371
- console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
215
+ console.log('');
216
+ }
217
+ catch (err) {
218
+ clearInterval(elapsedTimer);
219
+ process.removeListener('SIGINT', sigintHandler);
220
+ console.log = origLog;
221
+ console.warn = origWarn;
222
+ console.error = origError;
223
+ bar.stop();
224
+ console.error(`\n Analysis failed: ${err.message}\n`);
225
+ process.exitCode = 1;
226
+ return;
372
227
  }
373
- console.log('');
374
228
  // LadybugDB's native module holds open handles that prevent Node from exiting.
375
229
  // ONNX Runtime also registers native atexit hooks that segfault on some
376
230
  // platforms (#38, #40). Force-exit to ensure clean termination.
@@ -68,7 +68,9 @@ export function formatContextResult(result) {
68
68
  if (result.error)
69
69
  return `Error: ${result.error}`;
70
70
  if (result.status === 'ambiguous') {
71
- const lines = [`Multiple symbols named '${result.candidates?.[0]?.name || '?'}'. Disambiguate with file path:\n`];
71
+ const lines = [
72
+ `Multiple symbols named '${result.candidates?.[0]?.name || '?'}'. Disambiguate with file path:\n`,
73
+ ];
72
74
  for (const c of result.candidates || []) {
73
75
  lines.push(` ${c.kind} ${c.name} → ${c.filePath}:${c.line || '?'} (uid: ${c.uid})`);
74
76
  }
@@ -171,7 +173,7 @@ export function formatCypherResult(result) {
171
173
  const keys = Object.keys(result[0]);
172
174
  const lines = [`${result.length} row(s):\n`];
173
175
  for (const row of result.slice(0, 30)) {
174
- const parts = keys.map(k => `${k}: ${row[k]}`);
176
+ const parts = keys.map((k) => `${k}: ${row[k]}`);
175
177
  lines.push(` ${parts.join(' | ')}`);
176
178
  }
177
179
  if (result.length > 30) {
@@ -230,13 +232,20 @@ export function formatListReposResult(result) {
230
232
  */
231
233
  function formatToolResult(toolName, result) {
232
234
  switch (toolName) {
233
- case 'query': return formatQueryResult(result);
234
- case 'context': return formatContextResult(result);
235
- case 'impact': return formatImpactResult(result);
236
- case 'cypher': return formatCypherResult(result);
237
- case 'detect_changes': return formatDetectChangesResult(result);
238
- case 'list_repos': return formatListReposResult(result);
239
- default: return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
235
+ case 'query':
236
+ return formatQueryResult(result);
237
+ case 'context':
238
+ return formatContextResult(result);
239
+ case 'impact':
240
+ return formatImpactResult(result);
241
+ case 'cypher':
242
+ return formatCypherResult(result);
243
+ case 'detect_changes':
244
+ return formatDetectChangesResult(result);
245
+ case 'list_repos':
246
+ return formatListReposResult(result);
247
+ default:
248
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
240
249
  }
241
250
  }
242
251
  // ─── Next-Step Hints ──────────────────────────────────────────────────
@@ -269,7 +278,7 @@ export async function evalServerCommand(options) {
269
278
  process.exit(1);
270
279
  }
271
280
  const repos = await backend.listRepos();
272
- console.error(`GitNexus eval-server: ${repos.length} repo(s) loaded: ${repos.map(r => r.name).join(', ')}`);
281
+ console.error(`GitNexus eval-server: ${repos.length} repo(s) loaded: ${repos.map((r) => r.name).join(', ')}`);
273
282
  let idleTimer = null;
274
283
  function resetIdleTimer() {
275
284
  if (idleTimeoutSec <= 0)
@@ -289,7 +298,7 @@ export async function evalServerCommand(options) {
289
298
  if (req.method === 'GET' && req.url === '/health') {
290
299
  res.setHeader('Content-Type', 'application/json');
291
300
  res.writeHead(200);
292
- res.end(JSON.stringify({ status: 'ok', repos: repos.map(r => r.name) }));
301
+ res.end(JSON.stringify({ status: 'ok', repos: repos.map((r) => r.name) }));
293
302
  return;
294
303
  }
295
304
  // Shutdown
@@ -8,25 +8,23 @@
8
8
  * cloning a repo that ships its index, restoring from backup, or using a
9
9
  * shared team index).
10
10
  */
11
- import path from "path";
12
- import fs from "fs/promises";
13
- import { getStoragePaths, loadMeta, addToGitignore, registerRepo, } from "../storage/repo-manager.js";
14
- import { getGitRoot, isGitRepo } from "../storage/git.js";
11
+ import path from 'path';
12
+ import fs from 'fs/promises';
13
+ import { getStoragePaths, loadMeta, addToGitignore, registerRepo, } from '../storage/repo-manager.js';
14
+ import { getGitRoot, isGitRepo } from '../storage/git.js';
15
15
  export const indexCommand = async (inputPathParts, options) => {
16
- console.log("\n GitNexus Index\n");
17
- const inputPath = inputPathParts?.length
18
- ? inputPathParts.join(" ")
19
- : undefined;
16
+ console.log('\n GitNexus Index\n');
17
+ const inputPath = inputPathParts?.length ? inputPathParts.join(' ') : undefined;
20
18
  if (inputPathParts && inputPathParts.length > 1) {
21
19
  const resolvedCombinedPath = path.resolve(inputPath);
22
20
  try {
23
21
  await fs.access(resolvedCombinedPath);
24
22
  }
25
23
  catch {
26
- console.log(" The `index` command accepts a single path only.");
27
- console.log(" If your path contains spaces, wrap it in quotes.");
28
- console.log(` Received multiple path parts: ${inputPathParts.join(", ")}`);
29
- console.log("");
24
+ console.log(' The `index` command accepts a single path only.');
25
+ console.log(' If your path contains spaces, wrap it in quotes.');
26
+ console.log(` Received multiple path parts: ${inputPathParts.join(', ')}`);
27
+ console.log('');
30
28
  process.exitCode = 1;
31
29
  return;
32
30
  }
@@ -38,7 +36,7 @@ export const indexCommand = async (inputPathParts, options) => {
38
36
  else {
39
37
  const gitRoot = getGitRoot(process.cwd());
40
38
  if (!gitRoot) {
41
- console.log(" Not inside a git repository, try to run git init\n");
39
+ console.log(' Not inside a git repository, try to run git init\n');
42
40
  process.exitCode = 1;
43
41
  return;
44
42
  }
@@ -46,8 +44,8 @@ export const indexCommand = async (inputPathParts, options) => {
46
44
  }
47
45
  if (!options?.allowNonGit && !isGitRepo(repoPath)) {
48
46
  console.log(` Not a git repository: ${repoPath}`);
49
- console.log(" Initialize one with `git init` or choose a valid repo path.\n");
50
- console.log(" Or use --allow-non-git to register an existing .gitnexus index anyway.\n");
47
+ console.log(' Initialize one with `git init` or choose a valid repo path.\n');
48
+ console.log(' Or use --allow-non-git to register an existing .gitnexus index anyway.\n');
51
49
  process.exitCode = 1;
52
50
  return;
53
51
  }
@@ -58,7 +56,7 @@ export const indexCommand = async (inputPathParts, options) => {
58
56
  }
59
57
  catch {
60
58
  console.log(` No .gitnexus/ folder found at: ${storagePath}`);
61
- console.log(" Run `gitnexus analyze` to build the index first.\n");
59
+ console.log(' Run `gitnexus analyze` to build the index first.\n');
62
60
  process.exitCode = 1;
63
61
  return;
64
62
  }
@@ -68,7 +66,7 @@ export const indexCommand = async (inputPathParts, options) => {
68
66
  }
69
67
  catch {
70
68
  console.log(` .gitnexus/ folder exists but contains no LadybugDB index.`);
71
- console.log(" Run `gitnexus analyze` to build the index.\n");
69
+ console.log(' Run `gitnexus analyze` to build the index.\n');
72
70
  process.exitCode = 1;
73
71
  return;
74
72
  }
@@ -77,15 +75,15 @@ export const indexCommand = async (inputPathParts, options) => {
77
75
  if (!meta) {
78
76
  if (!options?.force) {
79
77
  console.log(` .gitnexus/ exists but meta.json is missing.`);
80
- console.log(" Use --force to register anyway (stats will be empty),");
81
- console.log(" or run `gitnexus analyze` to rebuild properly.\n");
78
+ console.log(' Use --force to register anyway (stats will be empty),');
79
+ console.log(' or run `gitnexus analyze` to rebuild properly.\n');
82
80
  process.exitCode = 1;
83
81
  return;
84
82
  }
85
83
  // --force: build a minimal meta so the repo can be registered
86
84
  meta = {
87
85
  repoPath,
88
- lastCommit: "",
86
+ lastCommit: '',
89
87
  indexedAt: new Date().toISOString(),
90
88
  };
91
89
  }
@@ -108,8 +106,8 @@ export const indexCommand = async (inputPathParts, options) => {
108
106
  if (stats.processes != null)
109
107
  parts.push(`${stats.processes} flows`);
110
108
  if (parts.length)
111
- console.log(` ${parts.join(" | ")}`);
109
+ console.log(` ${parts.join(' | ')}`);
112
110
  }
113
111
  console.log(` ${repoPath}`);
114
- console.log("");
112
+ console.log('');
115
113
  };
package/dist/cli/index.js CHANGED
@@ -7,10 +7,7 @@ import { createLazyAction } from './lazy-action.js';
7
7
  const _require = createRequire(import.meta.url);
8
8
  const pkg = _require('../../package.json');
9
9
  const program = new Command();
10
- program
11
- .name('gitnexus')
12
- .description('GitNexus local CLI and MCP server')
13
- .version(pkg.version);
10
+ program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version);
14
11
  program
15
12
  .command('setup')
16
13
  .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex')
@@ -21,6 +18,7 @@ program
21
18
  .option('-f, --force', 'Force full re-index even if up to date')
22
19
  .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
23
20
  .option('--skills', 'Generate repo-specific skill files from detected communities')
21
+ .option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
24
22
  .option('--skip-git', 'Index a folder without requiring a .git directory')
25
23
  .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
26
24
  .addHelpText('after', '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)')
@@ -60,9 +58,12 @@ program
60
58
  .description('Generate repository wiki from knowledge graph')
61
59
  .option('-f, --force', 'Force full regeneration even if up to date')
62
60
  .option('--provider <provider>', 'LLM provider: openai or cursor (default: openai)')
63
- .option('--model <model>', 'LLM model name (default depends on provider)')
64
- .option('--base-url <url>', 'LLM API base URL (for openai provider)')
65
- .option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
61
+ .option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
62
+ .option('--base-url <url>', 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1')
63
+ .option('--api-key <key>', 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)')
64
+ .option('--api-version <version>', 'Azure api-version query param, e.g. 2024-10-21 (legacy Azure API only)')
65
+ .option('--reasoning-model', 'Mark deployment as reasoning model (o1/o3/o4-mini) — strips temperature, uses max_completion_tokens')
66
+ .option('--no-reasoning-model', 'Disable reasoning model mode (overrides saved config)')
66
67
  .option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
67
68
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
68
69
  .option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)')
package/dist/cli/mcp.js CHANGED
@@ -29,7 +29,7 @@ export const mcpCommand = async () => {
29
29
  console.error('GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.');
30
30
  }
31
31
  else {
32
- console.error(`GitNexus: MCP server starting with ${repos.length} repo(s): ${repos.map(r => r.name).join(', ')}`);
32
+ console.error(`GitNexus: MCP server starting with ${repos.length} repo(s): ${repos.map((r) => r.name).join(', ')}`);
33
33
  }
34
34
  // Start MCP server (serves all repos, discovers new ones lazily)
35
35
  await startMCPServer(backend);