@veewo/gitnexus 1.3.11 → 1.4.6-rc

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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Eval Server — Lightweight HTTP server for SWE-bench evaluation
3
3
  *
4
- * Keeps KuzuDB warm in memory so tool calls from the agent are near-instant.
4
+ * Keeps LadybugDB warm in memory so tool calls from the agent are near-instant.
5
5
  * Designed to run inside Docker containers during SWE-bench evaluation.
6
6
  *
7
7
  * KEY DESIGN: Returns LLM-friendly text, not raw JSON.
@@ -24,11 +24,12 @@
24
24
  * POST /shutdown — Graceful shutdown.
25
25
  */
26
26
  import http from 'http';
27
+ import { writeSync } from 'node:fs';
27
28
  import { LocalBackend } from '../mcp/local/local-backend.js';
28
29
  // ─── Text Formatters ──────────────────────────────────────────────────
29
30
  // Convert structured JSON results into compact, LLM-friendly text.
30
31
  // Design: minimize tokens, maximize actionability.
31
- function formatQueryResult(result) {
32
+ export function formatQueryResult(result) {
32
33
  if (result.error)
33
34
  return `Error: ${result.error}`;
34
35
  const lines = [];
@@ -63,7 +64,7 @@ function formatQueryResult(result) {
63
64
  }
64
65
  return lines.join('\n').trim();
65
66
  }
66
- function formatContextResult(result) {
67
+ export function formatContextResult(result) {
67
68
  if (result.error)
68
69
  return `Error: ${result.error}`;
69
70
  if (result.status === 'ambiguous') {
@@ -120,9 +121,11 @@ function formatContextResult(result) {
120
121
  }
121
122
  return lines.join('\n').trim();
122
123
  }
123
- function formatImpactResult(result) {
124
- if (result.error)
125
- return `Error: ${result.error}`;
124
+ export function formatImpactResult(result) {
125
+ if (result.error) {
126
+ const suggestion = result.suggestion ? `\nSuggestion: ${result.suggestion}` : '';
127
+ return `Error: ${result.error}${suggestion}`;
128
+ }
126
129
  const target = result.target;
127
130
  const direction = result.direction;
128
131
  const byDepth = result.byDepth || {};
@@ -132,7 +135,11 @@ function formatImpactResult(result) {
132
135
  }
133
136
  const lines = [];
134
137
  const dirLabel = direction === 'upstream' ? 'depends on this (will break if changed)' : 'this depends on';
135
- lines.push(`Blast radius for ${target?.kind || ''} ${target?.name} (${direction}): ${total} symbol(s) ${dirLabel}\n`);
138
+ lines.push(`Blast radius for ${target?.kind || ''} ${target?.name} (${direction}): ${total} symbol(s) ${dirLabel}`);
139
+ if (result.partial) {
140
+ lines.push('⚠️ Partial results — graph traversal was interrupted. Deeper impacts may exist.');
141
+ }
142
+ lines.push('');
136
143
  const depthLabels = {
137
144
  1: 'WILL BREAK (direct)',
138
145
  2: 'LIKELY AFFECTED (indirect)',
@@ -154,7 +161,7 @@ function formatImpactResult(result) {
154
161
  }
155
162
  return lines.join('\n').trim();
156
163
  }
157
- function formatCypherResult(result) {
164
+ export function formatCypherResult(result) {
158
165
  if (result.error)
159
166
  return `Error: ${result.error}`;
160
167
  if (Array.isArray(result)) {
@@ -174,7 +181,7 @@ function formatCypherResult(result) {
174
181
  }
175
182
  return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
176
183
  }
177
- function formatDetectChangesResult(result) {
184
+ export function formatDetectChangesResult(result) {
178
185
  if (result.error)
179
186
  return `Error: ${result.error}`;
180
187
  const summary = result.summary || {};
@@ -205,7 +212,7 @@ function formatDetectChangesResult(result) {
205
212
  }
206
213
  return lines.join('\n').trim();
207
214
  }
208
- function formatListReposResult(result) {
215
+ export function formatListReposResult(result) {
209
216
  if (!Array.isArray(result) || result.length === 0) {
210
217
  return 'No indexed repositories.';
211
218
  }
@@ -349,10 +356,11 @@ export async function evalServerCommand(options) {
349
356
  console.error(` Auto-shutdown after ${idleTimeoutSec}s idle`);
350
357
  }
351
358
  try {
352
- process.stdout.write(`GITNEXUS_EVAL_SERVER_READY:${port}\n`);
359
+ // Use fd 1 directly — LadybugDB captures process.stdout (#324)
360
+ writeSync(1, `GITNEXUS_EVAL_SERVER_READY:${port}\n`);
353
361
  }
354
362
  catch {
355
- // stdout may not be available
363
+ // stdout may not be available (e.g., broken pipe)
356
364
  }
357
365
  });
358
366
  resetIdleTimer();
@@ -365,10 +373,19 @@ export async function evalServerCommand(options) {
365
373
  process.on('SIGINT', shutdown);
366
374
  process.on('SIGTERM', shutdown);
367
375
  }
376
+ export const MAX_BODY_SIZE = 1024 * 1024; // 1MB
368
377
  function readBody(req) {
369
378
  return new Promise((resolve, reject) => {
370
379
  const chunks = [];
371
- req.on('data', (chunk) => chunks.push(chunk));
380
+ let totalSize = 0;
381
+ req.on('data', (chunk) => {
382
+ totalSize += chunk.length;
383
+ if (totalSize > MAX_BODY_SIZE) {
384
+ req.destroy(new Error('Request body too large (max 1MB)'));
385
+ return;
386
+ }
387
+ chunks.push(chunk);
388
+ });
372
389
  req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
373
390
  req.on('error', reject);
374
391
  });
package/dist/cli/index.js CHANGED
@@ -1,74 +1,23 @@
1
1
  #!/usr/bin/env node
2
- // Raise Node heap limit for large repos (e.g. Linux kernel).
3
- // Must run before any heavy allocation. If already set by the user, respect it.
4
- if (!process.env.NODE_OPTIONS?.includes('--max-old-space-size')) {
5
- const execArgv = process.execArgv.join(' ');
6
- if (!execArgv.includes('--max-old-space-size')) {
7
- // Re-spawn with a larger heap (8 GB)
8
- const { execFileSync } = await import('node:child_process');
9
- try {
10
- execFileSync(process.execPath, ['--max-old-space-size=8192', ...process.argv.slice(1)], {
11
- stdio: 'inherit',
12
- env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim() },
13
- });
14
- process.exit(0);
15
- }
16
- catch (e) {
17
- const resolved = resolveChildProcessExit(e, 1);
18
- if (resolved.bySignal && resolved.signal) {
19
- process.stderr.write(`gitnexus: child process terminated by signal ${resolved.signal}\n`);
20
- }
21
- process.exit(resolved.code);
22
- }
23
- }
24
- }
2
+ // Heap re-spawn removed only analyze.ts needs the 8GB heap (via its own ensureHeap()).
3
+ // Removing it from here improves MCP server startup time significantly.
25
4
  import { Command } from 'commander';
26
- import fs from 'fs';
27
- import path from 'path';
28
- import { fileURLToPath } from 'url';
29
- import { analyzeCommand } from './analyze.js';
30
- import { serveCommand } from './serve.js';
31
- import { listCommand } from './list.js';
32
- import { statusCommand } from './status.js';
33
- import { mcpCommand } from './mcp.js';
34
- import { cleanCommand } from './clean.js';
35
- import { setupCommand } from './setup.js';
36
- import { augmentCommand } from './augment.js';
37
- import { wikiCommand } from './wiki.js';
38
- import { queryCommand, contextCommand, impactCommand, cypherCommand } from './tool.js';
39
- import { evalServerCommand } from './eval-server.js';
40
- import { benchmarkUnityCommand } from './benchmark-unity.js';
41
- import { benchmarkAgentContextCommand } from './benchmark-agent-context.js';
42
- import { unityBindingsCommand } from './unity-bindings.js';
43
- import { benchmarkU2E2ECommand } from './benchmark-u2-e2e.js';
44
- import { resolveChildProcessExit } from './exit-code.js';
45
- function resolveCliVersion() {
46
- try {
47
- const currentFile = fileURLToPath(import.meta.url);
48
- const packageJsonPath = path.resolve(path.dirname(currentFile), '..', '..', 'package.json');
49
- const raw = fs.readFileSync(packageJsonPath, 'utf-8');
50
- const parsed = JSON.parse(raw);
51
- if (typeof parsed.version === 'string' && parsed.version.length > 0) {
52
- return parsed.version;
53
- }
54
- }
55
- catch {
56
- // fall through to default
57
- }
58
- return '0.0.0';
59
- }
5
+ import { createRequire } from 'node:module';
6
+ import { createLazyAction } from './lazy-action.js';
7
+ const _require = createRequire(import.meta.url);
8
+ const pkg = _require('../../package.json');
60
9
  const program = new Command();
61
10
  const collectValues = (value, previous) => [...previous, value];
62
11
  program
63
12
  .name('gitnexus')
64
13
  .description('GitNexus local CLI and MCP server')
65
- .version(resolveCliVersion());
14
+ .version(pkg.version);
66
15
  program
67
16
  .command('setup')
68
17
  .description('One-time setup: configure MCP for a selected coding agent (claude/opencode/codex)')
69
18
  .option('--scope <scope>', 'Install target: global (default) or project')
70
19
  .option('--agent <agent>', 'Target coding agent: claude, opencode, or codex')
71
- .action(setupCommand);
20
+ .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
72
21
  program
73
22
  .command('analyze [path]')
74
23
  .description('Index a repository (full analysis)')
@@ -77,33 +26,36 @@ program
77
26
  .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
78
27
  .option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
79
28
  .option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
29
+ .option('--skills', 'Generate repo-specific skill files from detected communities')
30
+ .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
80
31
  .option('--scope-manifest <path>', 'Manifest file with scope rules (supports comments and * wildcard; recommended: .gitnexus/sync-manifest.txt)')
81
32
  .option('--scope-prefix <pathPrefix>', 'Add a scope path prefix rule (repeatable)', collectValues, [])
82
- .action(analyzeCommand);
33
+ .addHelpText('after', '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)')
34
+ .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
83
35
  program
84
36
  .command('serve')
85
37
  .description('Start local HTTP server for web UI connection')
86
38
  .option('-p, --port <port>', 'Port number', '4747')
87
39
  .option('--host <host>', 'Bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)')
88
- .action(serveCommand);
40
+ .action(createLazyAction(() => import('./serve.js'), 'serveCommand'));
89
41
  program
90
42
  .command('mcp')
91
43
  .description('Start MCP server (stdio) — serves all indexed repos')
92
- .action(mcpCommand);
44
+ .action(createLazyAction(() => import('./mcp.js'), 'mcpCommand'));
93
45
  program
94
46
  .command('list')
95
47
  .description('List all indexed repositories')
96
- .action(listCommand);
48
+ .action(createLazyAction(() => import('./list.js'), 'listCommand'));
97
49
  program
98
50
  .command('status')
99
51
  .description('Show index status for current repo')
100
- .action(statusCommand);
52
+ .action(createLazyAction(() => import('./status.js'), 'statusCommand'));
101
53
  program
102
54
  .command('clean')
103
55
  .description('Delete GitNexus index for current repo')
104
56
  .option('-f, --force', 'Skip confirmation prompt')
105
57
  .option('--all', 'Clean all indexed repos')
106
- .action(cleanCommand);
58
+ .action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
107
59
  program
108
60
  .command('wiki [path]')
109
61
  .description('Generate repository wiki from knowledge graph')
@@ -113,11 +65,11 @@ program
113
65
  .option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
114
66
  .option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
115
67
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
116
- .action(wikiCommand);
68
+ .action(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
117
69
  program
118
70
  .command('augment <pattern>')
119
71
  .description('Augment a search pattern with knowledge graph context (used by hooks)')
120
- .action(augmentCommand);
72
+ .action(createLazyAction(() => import('./augment.js'), 'augmentCommand'));
121
73
  // ─── Direct Tool Commands (no MCP overhead) ────────────────────────
122
74
  // These invoke LocalBackend directly for use in eval, scripts, and CI.
123
75
  program
@@ -130,7 +82,7 @@ program
130
82
  .option('--content', 'Include full symbol source code')
131
83
  .option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
132
84
  .option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
133
- .action(queryCommand);
85
+ .action(createLazyAction(() => import('./tool.js'), 'queryCommand'));
134
86
  program
135
87
  .command('context [name]')
136
88
  .description('360-degree view of a code symbol: callers, callees, processes')
@@ -140,15 +92,13 @@ program
140
92
  .option('--content', 'Include full symbol source code')
141
93
  .option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
142
94
  .option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
143
- .action(contextCommand);
95
+ .action(createLazyAction(() => import('./tool.js'), 'contextCommand'));
144
96
  program
145
97
  .command('unity-bindings <symbol>')
146
98
  .description('Experimental: inspect Unity resource bindings for a C# symbol')
147
99
  .option('--target-path <path>', 'Unity project root (default: cwd)')
148
100
  .option('--json', 'Output JSON')
149
- .action(async (symbol, options) => {
150
- await unityBindingsCommand(symbol, options);
151
- });
101
+ .action(createLazyAction(() => import('./unity-bindings.js'), 'unityBindingsCommand'));
152
102
  program
153
103
  .command('impact <target>')
154
104
  .description('Blast radius analysis: what breaks if you change a symbol')
@@ -159,19 +109,19 @@ program
159
109
  .option('--depth <n>', 'Max relationship depth (default: 3)')
160
110
  .option('--min-confidence <n>', 'Minimum edge confidence 0-1 (default: 0.3)')
161
111
  .option('--include-tests', 'Include test files in results')
162
- .action(impactCommand);
112
+ .action(createLazyAction(() => import('./tool.js'), 'impactCommand'));
163
113
  program
164
114
  .command('cypher <query>')
165
115
  .description('Execute raw Cypher query against the knowledge graph')
166
116
  .option('-r, --repo <name>', 'Target repository')
167
- .action(cypherCommand);
117
+ .action(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
168
118
  // ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
169
119
  program
170
120
  .command('eval-server')
171
121
  .description('Start lightweight HTTP server for fast tool calls during evaluation')
172
122
  .option('-p, --port <port>', 'Port number', '4848')
173
123
  .option('--idle-timeout <seconds>', 'Auto-shutdown after N seconds idle (0 = disabled)', '0')
174
- .action(evalServerCommand);
124
+ .action(createLazyAction(() => import('./eval-server.js'), 'evalServerCommand'));
175
125
  program
176
126
  .command('benchmark-unity <dataset>')
177
127
  .description('Run Unity accuracy baseline and hard-gated regression checks')
@@ -184,7 +134,7 @@ program
184
134
  .option('--scope-manifest <path>', 'Analyze scope manifest file')
185
135
  .option('--scope-prefix <pathPrefix>', 'Analyze scope path prefix (repeatable)', collectValues, [])
186
136
  .option('--skip-analyze', 'Skip analyze stage and evaluate current index only')
187
- .action(benchmarkUnityCommand);
137
+ .action(createLazyAction(() => import('./benchmark-unity.js'), 'benchmarkUnityCommand'));
188
138
  program
189
139
  .command('benchmark-agent-context <dataset>')
190
140
  .description('Run scenario-based agent refactor context benchmark')
@@ -197,15 +147,11 @@ program
197
147
  .option('--scope-manifest <path>', 'Analyze scope manifest file')
198
148
  .option('--scope-prefix <pathPrefix>', 'Analyze scope path prefix (repeatable)', collectValues, [])
199
149
  .option('--skip-analyze', 'Skip analyze stage and evaluate current index only')
200
- .action(async (dataset, options) => {
201
- await benchmarkAgentContextCommand(dataset, options);
202
- });
150
+ .action(createLazyAction(() => import('./benchmark-agent-context.js'), 'benchmarkAgentContextCommand'));
203
151
  program
204
152
  .command('benchmark-u2-e2e')
205
153
  .description('Run fail-fast full neonspark U2 E2E benchmark and emit evidence reports')
206
154
  .option('--config <path>', 'Path to E2E config JSON')
207
155
  .option('--report-dir <path>', 'Output directory for reports')
208
- .action(async (options) => {
209
- await benchmarkU2E2ECommand(options);
210
- });
156
+ .action(createLazyAction(() => import('./benchmark-u2-e2e.js'), 'benchmarkU2E2ECommand'));
211
157
  program.parse(process.argv);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Creates a lazy-loaded CLI action that defers module import until invocation.
3
+ * The generic constraints ensure the export name is a valid key of the module
4
+ * at compile time — catching typos when used with concrete module imports.
5
+ */
6
+ export declare function createLazyAction<TModule extends Record<string, unknown>, TKey extends string & keyof TModule>(loader: () => Promise<TModule>, exportName: TKey): (...args: unknown[]) => Promise<void>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Creates a lazy-loaded CLI action that defers module import until invocation.
3
+ * The generic constraints ensure the export name is a valid key of the module
4
+ * at compile time — catching typos when used with concrete module imports.
5
+ */
6
+ function isCallable(value) {
7
+ return typeof value === 'function';
8
+ }
9
+ export function createLazyAction(loader, exportName) {
10
+ return async (...args) => {
11
+ const module = await loader();
12
+ const action = module[exportName];
13
+ if (!isCallable(action)) {
14
+ throw new Error(`Lazy action export not found: ${exportName}`);
15
+ }
16
+ await action(...args);
17
+ };
18
+ }
package/dist/cli/mcp.js CHANGED
@@ -12,9 +12,11 @@ export const mcpCommand = async () => {
12
12
  process.env.GITNEXUS_UNITY_PARITY_WARMUP = '1';
13
13
  }
14
14
  // Prevent unhandled errors from crashing the MCP server process.
15
- // KuzuDB lock conflicts and transient errors should degrade gracefully.
15
+ // LadybugDB lock conflicts and transient errors should degrade gracefully.
16
16
  process.on('uncaughtException', (err) => {
17
17
  console.error(`GitNexus MCP: uncaught exception — ${err.message}`);
18
+ // Process is in an undefined state after uncaughtException — exit after flushing
19
+ setTimeout(() => process.exit(1), 100);
18
20
  });
19
21
  process.on('unhandledRejection', (reason) => {
20
22
  const msg = reason instanceof Error ? reason.message : String(reason);
package/dist/cli/setup.js CHANGED
@@ -14,10 +14,12 @@ import { promisify } from 'node:util';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { getGlobalDir, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
16
16
  import { getGitRoot } from '../storage/git.js';
17
+ import { glob } from 'glob';
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
19
20
  const execFileAsync = promisify(execFile);
20
- const FALLBACK_MCP_PACKAGE = 'gitnexus@latest';
21
+ const FALLBACK_MCP_PACKAGE = '@veewo/gitnexus@latest';
22
+ const LEGACY_CURSOR_AGENT = 'cursor';
21
23
  function resolveSetupScope(rawScope) {
22
24
  if (!rawScope || rawScope.trim() === '')
23
25
  return 'global';
@@ -27,16 +29,28 @@ function resolveSetupScope(rawScope) {
27
29
  }
28
30
  function resolveSetupAgent(rawAgent) {
29
31
  if (!rawAgent || rawAgent.trim() === '') {
30
- throw new Error('Missing --agent. Use one of: claude, opencode, codex.');
32
+ return 'claude';
31
33
  }
32
34
  if (rawAgent === 'claude' || rawAgent === 'opencode' || rawAgent === 'codex') {
33
35
  return rawAgent;
34
36
  }
35
37
  throw new Error(`Invalid --agent value "${rawAgent}". Use "claude", "opencode", or "codex".`);
36
38
  }
39
+ async function installLegacyCursorSkills(result) {
40
+ const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
41
+ try {
42
+ const installed = await installSkillsTo(skillsDir);
43
+ if (installed.length > 0) {
44
+ result.configured.push(`Cursor skills (${installed.length} skills → ~/.cursor/skills/)`);
45
+ }
46
+ }
47
+ catch (err) {
48
+ result.errors.push(`Cursor skills: ${err.message}`);
49
+ }
50
+ }
37
51
  /**
38
52
  * Resolve the package spec used by MCP commands.
39
- * Defaults to gitnexus@latest when package metadata is unavailable.
53
+ * Defaults to @veewo/gitnexus@latest when package metadata is unavailable.
40
54
  */
41
55
  function resolveMcpPackageSpec() {
42
56
  try {
@@ -262,36 +276,39 @@ async function installClaudeCodeHooks(result) {
262
276
  const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
263
277
  const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
264
278
  try {
265
- const content = await fs.readFile(src, 'utf-8');
279
+ let content = await fs.readFile(src, 'utf-8');
280
+ // Inject resolved CLI path so the copied hook can find the CLI
281
+ // even when it's no longer inside the npm package tree
282
+ const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
283
+ const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
284
+ const jsonCli = JSON.stringify(normalizedCli);
285
+ content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
266
286
  await fs.writeFile(dest, content, 'utf-8');
267
287
  }
268
288
  catch {
269
289
  // Script not found in source — skip
270
290
  }
271
- const hookCmd = `node "${path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/')}"`;
291
+ const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
292
+ const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
272
293
  // Merge hook config into ~/.claude/settings.json
273
294
  const existing = await readJsonFile(settingsPath) || {};
274
295
  if (!existing.hooks)
275
296
  existing.hooks = {};
276
- // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
277
- // Session context is delivered via CLAUDE.md / skills instead.
278
- // Add PreToolUse hook if not already present
279
- if (!existing.hooks.PreToolUse)
280
- existing.hooks.PreToolUse = [];
281
- const hasPreToolHook = existing.hooks.PreToolUse.some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus')));
282
- if (!hasPreToolHook) {
283
- existing.hooks.PreToolUse.push({
284
- matcher: 'Grep|Glob|Bash',
285
- hooks: [{
286
- type: 'command',
287
- command: hookCmd,
288
- timeout: 8000,
289
- statusMessage: 'Enriching with GitNexus graph context...',
290
- }],
291
- });
297
+ function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
298
+ if (!existing.hooks[eventName])
299
+ existing.hooks[eventName] = [];
300
+ const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some(hh => hh.command?.includes('gitnexus-hook')));
301
+ if (!hasHook) {
302
+ existing.hooks[eventName].push({
303
+ matcher,
304
+ hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
305
+ });
306
+ }
292
307
  }
308
+ ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with GitNexus graph context...');
309
+ ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking GitNexus index freshness...');
293
310
  await writeJsonFile(settingsPath, existing);
294
- result.configured.push('Claude Code hooks (PreToolUse)');
311
+ result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
295
312
  }
296
313
  catch (err) {
297
314
  result.errors.push(`Claude Code hooks: ${err.message}`);
@@ -383,7 +400,6 @@ async function saveSetupScope(scope, result) {
383
400
  }
384
401
  }
385
402
  // ─── Skill Installation ───────────────────────────────────────────
386
- const SKILL_NAMES = ['gitnexus-exploring', 'gitnexus-debugging', 'gitnexus-impact-analysis', 'gitnexus-refactoring', 'gitnexus-guide', 'gitnexus-cli'];
387
403
  /**
388
404
  * Install GitNexus skills to a target directory.
389
405
  * Each skill is installed as {targetDir}/{skillName}/SKILL.md.
@@ -395,23 +411,36 @@ const SKILL_NAMES = ['gitnexus-exploring', 'gitnexus-debugging', 'gitnexus-impac
395
411
  async function installSkillsTo(targetDir) {
396
412
  const installed = [];
397
413
  const skillsRoot = path.join(__dirname, '..', '..', 'skills');
398
- for (const skillName of SKILL_NAMES) {
414
+ let flatFiles = [];
415
+ let dirSkillFiles = [];
416
+ try {
417
+ [flatFiles, dirSkillFiles] = await Promise.all([
418
+ glob('*.md', { cwd: skillsRoot }),
419
+ glob('*/SKILL.md', { cwd: skillsRoot }),
420
+ ]);
421
+ }
422
+ catch {
423
+ return [];
424
+ }
425
+ const skillSources = new Map();
426
+ for (const relPath of dirSkillFiles) {
427
+ skillSources.set(path.dirname(relPath), { isDirectory: true });
428
+ }
429
+ for (const relPath of flatFiles) {
430
+ const skillName = path.basename(relPath, '.md');
431
+ if (!skillSources.has(skillName)) {
432
+ skillSources.set(skillName, { isDirectory: false });
433
+ }
434
+ }
435
+ for (const [skillName, source] of skillSources) {
399
436
  const skillDir = path.join(targetDir, skillName);
400
437
  try {
401
- // Try directory-based skill first (skills/{name}/SKILL.md)
402
- const dirSource = path.join(skillsRoot, skillName);
403
- let isDirectory = false;
404
- try {
405
- const stat = await fs.stat(dirSource);
406
- isDirectory = stat.isDirectory();
407
- }
408
- catch { /* not a directory */ }
409
- if (isDirectory) {
438
+ if (source.isDirectory) {
439
+ const dirSource = path.join(skillsRoot, skillName);
410
440
  await copyDirRecursive(dirSource, skillDir);
411
441
  installed.push(skillName);
412
442
  }
413
443
  else {
414
- // Fall back to flat file (skills/{name}.md)
415
444
  const flatSource = path.join(skillsRoot, `${skillName}.md`);
416
445
  const content = await fs.readFile(flatSource, 'utf-8');
417
446
  await fs.mkdir(skillDir, { recursive: true });
@@ -450,6 +479,7 @@ export const setupCommand = async (options = {}) => {
450
479
  console.log('');
451
480
  let scope;
452
481
  let agent;
482
+ const legacyCursorMode = !options.agent || options.agent.trim() === '';
453
483
  try {
454
484
  scope = resolveSetupScope(options.scope);
455
485
  agent = resolveSetupAgent(options.agent);
@@ -468,20 +498,29 @@ export const setupCommand = async (options = {}) => {
468
498
  errors: [],
469
499
  };
470
500
  if (scope === 'global') {
471
- // Configure only the selected agent MCP
472
- if (agent === 'claude') {
473
- await setupClaudeCode(result);
474
- // Claude-only hooks should only be installed when Claude is selected.
475
- await installClaudeCodeHooks(result);
476
- }
477
- else if (agent === 'opencode') {
478
- await setupOpenCode(result);
501
+ if (legacyCursorMode) {
502
+ await setupCursor(result);
503
+ await installLegacyCursorSkills(result);
504
+ await saveSetupScope(scope, result);
505
+ agent = LEGACY_CURSOR_AGENT;
479
506
  }
480
- else if (agent === 'codex') {
481
- await setupCodex(result);
507
+ else {
508
+ // Configure only the selected agent MCP
509
+ if (agent === 'claude') {
510
+ await setupClaudeCode(result);
511
+ // Claude-only hooks should only be installed when Claude is selected.
512
+ await installClaudeCodeHooks(result);
513
+ }
514
+ else if (agent === 'opencode') {
515
+ await setupOpenCode(result);
516
+ }
517
+ else if (agent === 'codex') {
518
+ await setupCodex(result);
519
+ }
520
+ // Install shared global skills once
521
+ await installGlobalAgentSkills(result);
522
+ await saveSetupScope(scope, result);
482
523
  }
483
- // Install shared global skills once
484
- await installGlobalAgentSkills(result);
485
524
  }
486
525
  else {
487
526
  const repoRoot = getGitRoot(process.cwd());
@@ -500,8 +539,8 @@ export const setupCommand = async (options = {}) => {
500
539
  await setupProjectOpenCode(repoRoot, result);
501
540
  }
502
541
  await installProjectAgentSkills(repoRoot, result);
542
+ await saveSetupScope(scope, result);
503
543
  }
504
- await saveSetupScope(scope, result);
505
544
  // Print results
506
545
  if (result.configured.length > 0) {
507
546
  console.log(' Configured:');
@@ -526,7 +565,7 @@ export const setupCommand = async (options = {}) => {
526
565
  console.log('');
527
566
  console.log(' Summary:');
528
567
  console.log(` Scope: ${scope}`);
529
- console.log(` Agent: ${agent}`);
568
+ console.log(` Agent: ${legacyCursorMode ? LEGACY_CURSOR_AGENT : agent}`);
530
569
  console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
531
570
  console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
532
571
  console.log('');
@@ -16,21 +16,26 @@ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
16
  async function runSetup(args, env, cwd = packageRoot) {
17
17
  return execFileAsync(process.execPath, [cliPath, 'setup', ...args], { cwd, env });
18
18
  }
19
- test('setup requires --agent', async () => {
19
+ test('setup without --agent uses legacy Cursor install path', async () => {
20
20
  const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
21
21
  try {
22
- try {
23
- await runSetup([], {
24
- ...process.env,
25
- HOME: fakeHome,
26
- USERPROFILE: fakeHome,
27
- });
28
- assert.fail('expected setup without --agent to fail');
29
- }
30
- catch (err) {
31
- assert.equal(typeof err?.stdout, 'string');
32
- assert.match(err.stdout, /Missing --agent/);
33
- }
22
+ await fs.mkdir(path.join(fakeHome, '.cursor'), { recursive: true });
23
+ await runSetup([], {
24
+ ...process.env,
25
+ HOME: fakeHome,
26
+ USERPROFILE: fakeHome,
27
+ });
28
+ const cursorMcpPath = path.join(fakeHome, '.cursor', 'mcp.json');
29
+ const cursorSkillPath = path.join(fakeHome, '.cursor', 'skills', 'gitnexus-cli', 'SKILL.md');
30
+ const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
31
+ const cursorMcpRaw = await fs.readFile(cursorMcpPath, 'utf-8');
32
+ const cursorMcp = JSON.parse(cursorMcpRaw);
33
+ assert.equal(cursorMcp.mcpServers?.gitnexus?.command, 'npx');
34
+ assert.deepEqual(cursorMcp.mcpServers?.gitnexus?.args, ['-y', expectedMcpPackage, 'mcp']);
35
+ await fs.access(cursorSkillPath);
36
+ const configRaw = await fs.readFile(configPath, 'utf-8');
37
+ const config = JSON.parse(configRaw);
38
+ assert.equal(config.setupScope, 'global');
34
39
  }
35
40
  finally {
36
41
  await fs.rm(fakeHome, { recursive: true, force: true });