@veewo/gitnexus 1.4.6-rc → 1.4.8-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.
package/README.md CHANGED
@@ -18,13 +18,17 @@ AI coding tools don't understand your codebase structure. They edit a function w
18
18
  ## Quick Start
19
19
 
20
20
  ```bash
21
+ # Optional: pin one version for the whole session (example RC)
22
+ # export GITNEXUS_CLI_SPEC="@veewo/gitnexus@1.4.7-rc"
23
+ GITNEXUS_CLI_SPEC="${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}"
24
+
21
25
  # Index your repo (run from repo root)
22
- npx -y @veewo/gitnexus@latest analyze
26
+ npx -y "${GITNEXUS_CLI_SPEC}" analyze
23
27
  ```
24
28
 
25
29
  That's it. This indexes the codebase, installs agent skills, registers Claude Code hooks, and creates `AGENTS.md` / `CLAUDE.md` context files — all in one command.
26
30
 
27
- To configure MCP for your editor, run `npx -y @veewo/gitnexus@latest setup` once — or set it up manually below.
31
+ To configure MCP for your editor, run `npx -y "${GITNEXUS_CLI_SPEC}" setup --cli-spec "$GITNEXUS_CLI_SPEC"` once — or set it up manually below.
28
32
 
29
33
  `gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
30
34
 
@@ -52,7 +56,7 @@ If you prefer to configure manually instead of using `gitnexus setup`:
52
56
  ### Claude Code (full support — MCP + skills + hooks)
53
57
 
54
58
  ```bash
55
- claude mcp add gitnexus -- npx -y @veewo/gitnexus@latest mcp
59
+ claude mcp add gitnexus -- npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" mcp
56
60
  ```
57
61
 
58
62
  ### Cursor / Windsurf
@@ -137,10 +141,10 @@ Your AI agent gets these tools automatically:
137
141
 
138
142
  ```bash
139
143
  gitnexus setup # Configure MCP for your editors (one-time)
140
- npx -y @veewo/gitnexus@latest analyze [path] # Index a repository (or update stale index)
141
- npx -y @veewo/gitnexus@latest analyze --force # Force full re-index
142
- npx -y @veewo/gitnexus@latest analyze --embeddings # Enable embedding generation (slower, better search)
143
- npx -y @veewo/gitnexus@latest analyze --verbose # Log skipped files when parsers are unavailable
144
+ npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze [path] # Index a repository (or update stale index)
145
+ npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze --force # Force full re-index
146
+ npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze --embeddings # Enable embedding generation (slower, better search)
147
+ npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze --verbose # Log skipped files when parsers are unavailable
144
148
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
145
149
  gitnexus serve # Start local HTTP server (multi-repo) for web UI
146
150
  gitnexus list # List all indexed repositories
@@ -153,7 +157,7 @@ gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-m
153
157
 
154
158
  ## Multi-Repo Support
155
159
 
156
- GitNexus supports indexing multiple repositories. Each `npx -y @veewo/gitnexus@latest analyze` registers the repo in a global registry (`~/.gitnexus/registry.json`). The MCP server serves all indexed repos automatically.
160
+ GitNexus supports indexing multiple repositories. Each `npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze` registers the repo in a global registry (`~/.gitnexus/registry.json`). The MCP server serves all indexed repos automatically.
157
161
 
158
162
  ## Supported Languages
159
163
 
@@ -188,7 +192,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
188
192
  - **Impact Analysis** — Analyze blast radius before changes
189
193
  - **Refactoring** — Plan safe refactors using dependency mapping
190
194
 
191
- Installed automatically by both `npx -y @veewo/gitnexus@latest analyze` (per-repo) and `gitnexus setup` (global).
195
+ Installed automatically by both `npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze` (per-repo) and `gitnexus setup` (global).
192
196
 
193
197
  ## Requirements
194
198
 
@@ -20,6 +20,7 @@ interface RepoStats {
20
20
  */
21
21
  export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, options?: {
22
22
  skillScope?: SkillScope;
23
+ cliPackageSpec?: string;
23
24
  }, generatedSkills?: GeneratedSkillInfo[]): Promise<{
24
25
  files: string[];
25
26
  }>;
@@ -8,6 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
11
12
  // ESM equivalent of __dirname
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = path.dirname(__filename);
@@ -24,13 +25,14 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
24
25
  * - Exact tool commands with parameters — vague directives get ignored
25
26
  * - Self-review checklist — forces model to verify its own work
26
27
  */
27
- function generateGitNexusContent(projectName, stats, skillScope, generatedSkills) {
28
+ function generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec, generatedSkills) {
28
29
  const skillRoot = skillScope === 'global'
29
30
  ? '~/.agents/skills/gitnexus'
30
31
  : '.agents/skills/gitnexus';
31
32
  const generatedRows = (generatedSkills && generatedSkills.length > 0)
32
33
  ? `\n${generatedSkills.map((s) => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`).join('\n')}`
33
34
  : '';
35
+ const reindexCmd = buildNpxCommand(cliPackageSpec, 'analyze');
34
36
  return `${GITNEXUS_START_MARKER}
35
37
  # GitNexus MCP
36
38
 
@@ -42,7 +44,7 @@ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} s
42
44
  2. **Match your task to a skill below** and **read that skill file**
43
45
  3. **Follow the skill's workflow and checklist**
44
46
 
45
- > If step 1 warns the index is stale, ask user whether to rebuild index via \`npx -y @veewo/gitnexus@latest analyze\` first (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
47
+ > If step 1 warns the index is stale, ask user whether to rebuild index via \`gitnexus analyze\` when local CLI exists; otherwise run \`${reindexCmd}\` (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
46
48
 
47
49
  ## Skills
48
50
 
@@ -173,7 +175,8 @@ Use GitNexus tools to accomplish this task.
173
175
  */
174
176
  export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, options, generatedSkills) {
175
177
  const skillScope = options?.skillScope === 'global' ? 'global' : 'project';
176
- const content = generateGitNexusContent(projectName, stats, skillScope, generatedSkills);
178
+ const cliPackageSpec = options?.cliPackageSpec || resolveCliSpec().packageSpec;
179
+ const content = generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec, generatedSkills);
177
180
  const createdFiles = [];
178
181
  // Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Codex, Cline, etc.)
179
182
  const agentsPath = path.join(repoPath, 'AGENTS.md');
@@ -22,6 +22,7 @@ import { resolveEffectiveAnalyzeOptions } from './analyze-options.js';
22
22
  import { formatFallbackSummary, formatUnityDiagnosticsSummary } from './analyze-summary.js';
23
23
  import { resolveChildProcessExit } from './exit-code.js';
24
24
  import { toPipelineRuntimeSummary } from './analyze-runtime-summary.js';
25
+ import { resolveCliSpec } from '../config/cli-spec.js';
25
26
  const HEAP_MB = 8192;
26
27
  const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
27
28
  /** Re-exec the process with an 8GB heap if we're currently below that. */
@@ -98,6 +99,14 @@ export const analyzeCommand = async (inputPath, options) => {
98
99
  }
99
100
  const currentCommit = getCurrentCommit(repoPath);
100
101
  const existingMeta = await loadMeta(storagePath);
102
+ let hasLbugIndex = false;
103
+ try {
104
+ await fs.stat(lbugPath);
105
+ hasLbugIndex = true;
106
+ }
107
+ catch {
108
+ hasLbugIndex = false;
109
+ }
101
110
  let includeExtensions = [];
102
111
  let scopeRules = [];
103
112
  let repoAlias;
@@ -121,7 +130,10 @@ export const analyzeCommand = async (inputPath, options) => {
121
130
  process.exitCode = 1;
122
131
  return;
123
132
  }
124
- if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit && !options?.skills) {
133
+ if (existingMeta && !hasLbugIndex && !options?.force) {
134
+ console.log(' Existing metadata found, but LadybugDB index file is missing — rebuilding index...\n');
135
+ }
136
+ if (existingMeta && hasLbugIndex && !options?.force && existingMeta.lastCommit === currentCommit && !options?.skills) {
125
137
  const hasScopePrefixInput = Array.isArray(options?.scopePrefix)
126
138
  ? options.scopePrefix.length > 0
127
139
  : Boolean(options?.scopePrefix);
@@ -368,6 +380,7 @@ export const analyzeCommand = async (inputPath, options) => {
368
380
  const skillResult = await generateSkillFiles(repoPath, projectName, pipelineForSkills);
369
381
  generatedSkills = skillResult.skills;
370
382
  }
383
+ const cliConfig = await loadCLIConfig();
371
384
  const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
372
385
  files: pipelineRuntime.totalFileCount,
373
386
  nodes: stats.nodes,
@@ -376,7 +389,8 @@ export const analyzeCommand = async (inputPath, options) => {
376
389
  clusters: aggregatedClusterCount,
377
390
  processes: pipelineRuntime.processResult?.stats.totalProcesses,
378
391
  }, {
379
- skillScope: ((await loadCLIConfig()).setupScope === 'global') ? 'global' : 'project',
392
+ skillScope: (cliConfig.setupScope === 'global') ? 'global' : 'project',
393
+ cliPackageSpec: resolveCliSpec({ config: cliConfig }).packageSpec,
380
394
  }, generatedSkills);
381
395
  await closeLbug();
382
396
  // Note: we intentionally do NOT call disposeEmbedder() here.
package/dist/cli/index.js CHANGED
@@ -17,6 +17,8 @@ program
17
17
  .description('One-time setup: configure MCP for a selected coding agent (claude/opencode/codex)')
18
18
  .option('--scope <scope>', 'Install target: global (default) or project')
19
19
  .option('--agent <agent>', 'Target coding agent: claude, opencode, or codex')
20
+ .option('--cli-version <version>', 'Pin npx GitNexus version/tag for generated MCP commands (e.g. 1.4.7-rc)')
21
+ .option('--cli-spec <spec>', 'Pin full npx package spec for generated MCP commands (e.g. @veewo/gitnexus@1.4.7-rc)')
20
22
  .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
21
23
  program
22
24
  .command('analyze [path]')
@@ -8,6 +8,8 @@
8
8
  interface SetupOptions {
9
9
  scope?: string;
10
10
  agent?: string;
11
+ cliVersion?: string;
12
+ cliSpec?: string;
11
13
  }
12
14
  export declare const setupCommand: (options?: SetupOptions) => Promise<void>;
13
15
  export {};
package/dist/cli/setup.js CHANGED
@@ -6,7 +6,6 @@
6
6
  * in either global or project scope.
7
7
  */
8
8
  import fs from 'fs/promises';
9
- import { readFileSync } from 'node:fs';
10
9
  import path from 'path';
11
10
  import os from 'os';
12
11
  import { execFile } from 'node:child_process';
@@ -15,10 +14,10 @@ import { fileURLToPath } from 'url';
15
14
  import { getGlobalDir, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
16
15
  import { getGitRoot } from '../storage/git.js';
17
16
  import { glob } from 'glob';
17
+ import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = path.dirname(__filename);
20
20
  const execFileAsync = promisify(execFile);
21
- const FALLBACK_MCP_PACKAGE = '@veewo/gitnexus@latest';
22
21
  const LEGACY_CURSOR_AGENT = 'cursor';
23
22
  function resolveSetupScope(rawScope) {
24
23
  if (!rawScope || rawScope.trim() === '')
@@ -48,43 +47,25 @@ async function installLegacyCursorSkills(result) {
48
47
  result.errors.push(`Cursor skills: ${err.message}`);
49
48
  }
50
49
  }
51
- /**
52
- * Resolve the package spec used by MCP commands.
53
- * Defaults to @veewo/gitnexus@latest when package metadata is unavailable.
54
- */
55
- function resolveMcpPackageSpec() {
56
- try {
57
- const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
58
- const raw = readFileSync(packageJsonPath, 'utf-8');
59
- const parsed = JSON.parse(raw);
60
- if (typeof parsed.name === 'string' && parsed.name.trim().length > 0) {
61
- return `${parsed.name}@latest`;
62
- }
63
- }
64
- catch {
65
- // Fallback keeps behavior for unusual runtimes.
66
- }
67
- return FALLBACK_MCP_PACKAGE;
68
- }
69
- const MCP_PACKAGE_SPEC = resolveMcpPackageSpec();
50
+ const DEFAULT_MCP_PACKAGE_SPEC = resolveCliSpec().packageSpec;
70
51
  /**
71
52
  * The MCP server entry for all editors.
72
53
  * On Windows, npx must be invoked via cmd /c since it's a .cmd script.
73
54
  */
74
- function getMcpEntry() {
55
+ function getMcpEntry(mcpPackageSpec) {
75
56
  if (process.platform === 'win32') {
76
57
  return {
77
58
  command: 'cmd',
78
- args: ['/c', 'npx', '-y', MCP_PACKAGE_SPEC, 'mcp'],
59
+ args: ['/c', 'npx', '-y', mcpPackageSpec, 'mcp'],
79
60
  };
80
61
  }
81
62
  return {
82
63
  command: 'npx',
83
- args: ['-y', MCP_PACKAGE_SPEC, 'mcp'],
64
+ args: ['-y', mcpPackageSpec, 'mcp'],
84
65
  };
85
66
  }
86
- function getOpenCodeMcpEntry() {
87
- const entry = getMcpEntry();
67
+ function getOpenCodeMcpEntry(mcpPackageSpec) {
68
+ const entry = getMcpEntry(mcpPackageSpec);
88
69
  return {
89
70
  type: 'local',
90
71
  command: [entry.command, ...entry.args],
@@ -94,28 +75,28 @@ function getOpenCodeMcpEntry() {
94
75
  * Merge gitnexus entry into an existing MCP config JSON object.
95
76
  * Returns the updated config.
96
77
  */
97
- function mergeMcpConfig(existing) {
78
+ function mergeMcpConfig(existing, mcpPackageSpec) {
98
79
  if (!existing || typeof existing !== 'object') {
99
80
  existing = {};
100
81
  }
101
82
  if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
102
83
  existing.mcpServers = {};
103
84
  }
104
- existing.mcpServers.gitnexus = getMcpEntry();
85
+ existing.mcpServers.gitnexus = getMcpEntry(mcpPackageSpec);
105
86
  return existing;
106
87
  }
107
88
  /**
108
89
  * Merge gitnexus entry into an OpenCode config JSON object.
109
90
  * Returns the updated config.
110
91
  */
111
- function mergeOpenCodeConfig(existing) {
92
+ function mergeOpenCodeConfig(existing, mcpPackageSpec) {
112
93
  if (!existing || typeof existing !== 'object') {
113
94
  existing = {};
114
95
  }
115
96
  if (!existing.mcp || typeof existing.mcp !== 'object') {
116
97
  existing.mcp = {};
117
98
  }
118
- existing.mcp.gitnexus = getOpenCodeMcpEntry();
99
+ existing.mcp.gitnexus = getOpenCodeMcpEntry(mcpPackageSpec);
119
100
  return existing;
120
101
  }
121
102
  /**
@@ -167,16 +148,16 @@ async function fileExists(filePath) {
167
148
  function toTomlString(value) {
168
149
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
169
150
  }
170
- function buildCodexMcpTable() {
171
- const entry = getMcpEntry();
151
+ function buildCodexMcpTable(mcpPackageSpec) {
152
+ const entry = getMcpEntry(mcpPackageSpec);
172
153
  return [
173
154
  '[mcp_servers.gitnexus]',
174
155
  `command = ${toTomlString(entry.command)}`,
175
156
  `args = [${entry.args.map(toTomlString).join(', ')}]`,
176
157
  ].join('\n');
177
158
  }
178
- function mergeCodexConfig(existingRaw) {
179
- const table = buildCodexMcpTable();
159
+ function mergeCodexConfig(existingRaw, mcpPackageSpec) {
160
+ const table = buildCodexMcpTable(mcpPackageSpec);
180
161
  const normalized = existingRaw.replace(/\r\n/g, '\n');
181
162
  const tablePattern = /^\[mcp_servers\.gitnexus\][\s\S]*?(?=^\[[^\]]+\]|(?![\s\S]))/m;
182
163
  if (tablePattern.test(normalized)) {
@@ -198,7 +179,7 @@ async function resolveOpenCodeConfigPath(opencodeDir) {
198
179
  return preferredPath;
199
180
  }
200
181
  // ─── Editor-specific setup ─────────────────────────────────────────
201
- async function setupCursor(result) {
182
+ async function setupCursor(result, mcpPackageSpec) {
202
183
  const cursorDir = path.join(os.homedir(), '.cursor');
203
184
  if (!(await dirExists(cursorDir))) {
204
185
  result.skipped.push('Cursor (not installed)');
@@ -207,7 +188,7 @@ async function setupCursor(result) {
207
188
  const mcpPath = path.join(cursorDir, 'mcp.json');
208
189
  try {
209
190
  const existing = await readJsonFile(mcpPath);
210
- const updated = mergeMcpConfig(existing);
191
+ const updated = mergeMcpConfig(existing, mcpPackageSpec);
211
192
  await writeJsonFile(mcpPath, updated);
212
193
  result.configured.push('Cursor');
213
194
  }
@@ -215,7 +196,7 @@ async function setupCursor(result) {
215
196
  result.errors.push(`Cursor: ${err.message}`);
216
197
  }
217
198
  }
218
- async function setupClaudeCode(result) {
199
+ async function setupClaudeCode(result, mcpPackageSpec) {
219
200
  const claudeDir = path.join(os.homedir(), '.claude');
220
201
  const hasClaude = await dirExists(claudeDir);
221
202
  if (!hasClaude) {
@@ -226,7 +207,7 @@ async function setupClaudeCode(result) {
226
207
  console.log('');
227
208
  console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
228
209
  console.log('');
229
- console.log(` claude mcp add gitnexus -- npx -y ${MCP_PACKAGE_SPEC} mcp`);
210
+ console.log(` claude mcp add gitnexus -- ${buildNpxCommand(mcpPackageSpec, 'mcp')}`);
230
211
  console.log('');
231
212
  result.configured.push('Claude Code (MCP manual step printed)');
232
213
  }
@@ -262,7 +243,7 @@ async function installProjectAgentSkills(repoRoot, result) {
262
243
  * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
263
244
  * Merges hook config without overwriting existing hooks.
264
245
  */
265
- async function installClaudeCodeHooks(result) {
246
+ async function installClaudeCodeHooks(result, mcpPackageSpec) {
266
247
  const claudeDir = path.join(os.homedir(), '.claude');
267
248
  if (!(await dirExists(claudeDir)))
268
249
  return;
@@ -283,6 +264,7 @@ async function installClaudeCodeHooks(result) {
283
264
  const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
284
265
  const jsonCli = JSON.stringify(normalizedCli);
285
266
  content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
267
+ content = content.replace("const DEFAULT_NPX_SPEC = '@veewo/gitnexus@latest';", `const DEFAULT_NPX_SPEC = ${JSON.stringify(mcpPackageSpec)};`);
286
268
  await fs.writeFile(dest, content, 'utf-8');
287
269
  }
288
270
  catch {
@@ -314,7 +296,7 @@ async function installClaudeCodeHooks(result) {
314
296
  result.errors.push(`Claude Code hooks: ${err.message}`);
315
297
  }
316
298
  }
317
- async function setupOpenCode(result) {
299
+ async function setupOpenCode(result, mcpPackageSpec) {
318
300
  const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
319
301
  if (!(await dirExists(opencodeDir))) {
320
302
  result.skipped.push('OpenCode (not installed)');
@@ -323,7 +305,7 @@ async function setupOpenCode(result) {
323
305
  const configPath = await resolveOpenCodeConfigPath(opencodeDir);
324
306
  try {
325
307
  const existing = await readJsonFile(configPath);
326
- const config = mergeOpenCodeConfig(existing);
308
+ const config = mergeOpenCodeConfig(existing, mcpPackageSpec);
327
309
  await writeJsonFile(configPath, config);
328
310
  result.configured.push(`OpenCode (${path.basename(configPath)})`);
329
311
  }
@@ -331,8 +313,8 @@ async function setupOpenCode(result) {
331
313
  result.errors.push(`OpenCode: ${err.message}`);
332
314
  }
333
315
  }
334
- async function setupCodex(result) {
335
- const entry = getMcpEntry();
316
+ async function setupCodex(result, mcpPackageSpec) {
317
+ const entry = getMcpEntry(mcpPackageSpec);
336
318
  try {
337
319
  await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], { timeout: 15000 });
338
320
  result.configured.push('Codex');
@@ -345,11 +327,11 @@ async function setupCodex(result) {
345
327
  result.errors.push(`Codex: ${err.message}`);
346
328
  }
347
329
  }
348
- async function setupProjectMcp(repoRoot, result) {
330
+ async function setupProjectMcp(repoRoot, result, mcpPackageSpec) {
349
331
  const mcpPath = path.join(repoRoot, '.mcp.json');
350
332
  try {
351
333
  const existing = await readJsonFile(mcpPath);
352
- const updated = mergeMcpConfig(existing);
334
+ const updated = mergeMcpConfig(existing, mcpPackageSpec);
353
335
  await writeJsonFile(mcpPath, updated);
354
336
  result.configured.push(`Project MCP (${path.relative(repoRoot, mcpPath)})`);
355
337
  }
@@ -357,7 +339,7 @@ async function setupProjectMcp(repoRoot, result) {
357
339
  result.errors.push(`Project MCP: ${err.message}`);
358
340
  }
359
341
  }
360
- async function setupProjectCodex(repoRoot, result) {
342
+ async function setupProjectCodex(repoRoot, result, mcpPackageSpec) {
361
343
  const codexConfigPath = path.join(repoRoot, '.codex', 'config.toml');
362
344
  try {
363
345
  let existingRaw = '';
@@ -368,7 +350,7 @@ async function setupProjectCodex(repoRoot, result) {
368
350
  if (err?.code !== 'ENOENT')
369
351
  throw err;
370
352
  }
371
- const merged = mergeCodexConfig(existingRaw);
353
+ const merged = mergeCodexConfig(existingRaw, mcpPackageSpec);
372
354
  await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
373
355
  await fs.writeFile(codexConfigPath, merged, 'utf-8');
374
356
  result.configured.push(`Project Codex MCP (${path.relative(repoRoot, codexConfigPath)})`);
@@ -377,11 +359,11 @@ async function setupProjectCodex(repoRoot, result) {
377
359
  result.errors.push(`Project Codex MCP: ${err.message}`);
378
360
  }
379
361
  }
380
- async function setupProjectOpenCode(repoRoot, result) {
362
+ async function setupProjectOpenCode(repoRoot, result, mcpPackageSpec) {
381
363
  const opencodePath = path.join(repoRoot, 'opencode.json');
382
364
  try {
383
365
  const existing = await readJsonFile(opencodePath);
384
- const merged = mergeOpenCodeConfig(existing);
366
+ const merged = mergeOpenCodeConfig(existing, mcpPackageSpec);
385
367
  await writeJsonFile(opencodePath, merged);
386
368
  result.configured.push(`Project OpenCode MCP (${path.relative(repoRoot, opencodePath)})`);
387
369
  }
@@ -389,11 +371,28 @@ async function setupProjectOpenCode(repoRoot, result) {
389
371
  result.errors.push(`Project OpenCode MCP: ${err.message}`);
390
372
  }
391
373
  }
392
- async function saveSetupScope(scope, result) {
374
+ function extractVersionFromPackageSpec(packageSpec) {
375
+ const trimmed = packageSpec.trim();
376
+ if (!trimmed)
377
+ return undefined;
378
+ if (trimmed.startsWith('@')) {
379
+ const at = trimmed.indexOf('@', 1);
380
+ return at > 0 ? trimmed.slice(at + 1) : undefined;
381
+ }
382
+ const at = trimmed.lastIndexOf('@');
383
+ return at > 0 ? trimmed.slice(at + 1) : undefined;
384
+ }
385
+ async function saveSetupConfig(scope, packageSpec, result) {
393
386
  try {
394
387
  const existing = await loadCLIConfig();
395
- await saveCLIConfig({ ...existing, setupScope: scope });
388
+ await saveCLIConfig({
389
+ ...existing,
390
+ setupScope: scope,
391
+ cliPackageSpec: packageSpec,
392
+ cliVersion: extractVersionFromPackageSpec(packageSpec),
393
+ });
396
394
  result.configured.push(`Default setup scope (${scope})`);
395
+ result.configured.push(`CLI package spec (${packageSpec})`);
397
396
  }
398
397
  catch (err) {
399
398
  result.errors.push(`Persist setup scope: ${err.message}`);
@@ -477,6 +476,11 @@ export const setupCommand = async (options = {}) => {
477
476
  console.log(' GitNexus Setup');
478
477
  console.log(' ==============');
479
478
  console.log('');
479
+ if (options.cliSpec && options.cliVersion) {
480
+ console.log(' Use either --cli-spec or --cli-version, not both.\n');
481
+ process.exitCode = 1;
482
+ return;
483
+ }
480
484
  let scope;
481
485
  let agent;
482
486
  const legacyCursorMode = !options.agent || options.agent.trim() === '';
@@ -492,6 +496,13 @@ export const setupCommand = async (options = {}) => {
492
496
  // Ensure global directory exists
493
497
  const globalDir = getGlobalDir();
494
498
  await fs.mkdir(globalDir, { recursive: true });
499
+ const existingConfig = await loadCLIConfig();
500
+ const resolvedCliSpec = resolveCliSpec({
501
+ explicitSpec: options.cliSpec,
502
+ explicitVersion: options.cliVersion,
503
+ config: existingConfig,
504
+ });
505
+ const mcpPackageSpec = resolvedCliSpec.packageSpec || DEFAULT_MCP_PACKAGE_SPEC;
495
506
  const result = {
496
507
  configured: [],
497
508
  skipped: [],
@@ -499,27 +510,27 @@ export const setupCommand = async (options = {}) => {
499
510
  };
500
511
  if (scope === 'global') {
501
512
  if (legacyCursorMode) {
502
- await setupCursor(result);
513
+ await setupCursor(result, mcpPackageSpec);
503
514
  await installLegacyCursorSkills(result);
504
- await saveSetupScope(scope, result);
515
+ await saveSetupConfig(scope, mcpPackageSpec, result);
505
516
  agent = LEGACY_CURSOR_AGENT;
506
517
  }
507
518
  else {
508
519
  // Configure only the selected agent MCP
509
520
  if (agent === 'claude') {
510
- await setupClaudeCode(result);
521
+ await setupClaudeCode(result, mcpPackageSpec);
511
522
  // Claude-only hooks should only be installed when Claude is selected.
512
- await installClaudeCodeHooks(result);
523
+ await installClaudeCodeHooks(result, mcpPackageSpec);
513
524
  }
514
525
  else if (agent === 'opencode') {
515
- await setupOpenCode(result);
526
+ await setupOpenCode(result, mcpPackageSpec);
516
527
  }
517
528
  else if (agent === 'codex') {
518
- await setupCodex(result);
529
+ await setupCodex(result, mcpPackageSpec);
519
530
  }
520
531
  // Install shared global skills once
521
532
  await installGlobalAgentSkills(result);
522
- await saveSetupScope(scope, result);
533
+ await saveSetupConfig(scope, mcpPackageSpec, result);
523
534
  }
524
535
  }
525
536
  else {
@@ -530,16 +541,16 @@ export const setupCommand = async (options = {}) => {
530
541
  return;
531
542
  }
532
543
  if (agent === 'claude') {
533
- await setupProjectMcp(repoRoot, result);
544
+ await setupProjectMcp(repoRoot, result, mcpPackageSpec);
534
545
  }
535
546
  else if (agent === 'codex') {
536
- await setupProjectCodex(repoRoot, result);
547
+ await setupProjectCodex(repoRoot, result, mcpPackageSpec);
537
548
  }
538
549
  else if (agent === 'opencode') {
539
- await setupProjectOpenCode(repoRoot, result);
550
+ await setupProjectOpenCode(repoRoot, result, mcpPackageSpec);
540
551
  }
541
552
  await installProjectAgentSkills(repoRoot, result);
542
- await saveSetupScope(scope, result);
553
+ await saveSetupConfig(scope, mcpPackageSpec, result);
543
554
  }
544
555
  // Print results
545
556
  if (result.configured.length > 0) {
@@ -566,6 +577,7 @@ export const setupCommand = async (options = {}) => {
566
577
  console.log(' Summary:');
567
578
  console.log(` Scope: ${scope}`);
568
579
  console.log(` Agent: ${legacyCursorMode ? LEGACY_CURSOR_AGENT : agent}`);
580
+ console.log(` CLI package spec: ${mcpPackageSpec}`);
569
581
  console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
570
582
  console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
571
583
  console.log('');
@@ -61,6 +61,26 @@ test('setup rejects invalid --agent', async () => {
61
61
  await fs.rm(fakeHome, { recursive: true, force: true });
62
62
  }
63
63
  });
64
+ test('setup rejects using --cli-spec and --cli-version together', async () => {
65
+ const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
66
+ try {
67
+ try {
68
+ await runSetup(['--agent', 'opencode', '--cli-spec', '@veewo/gitnexus@1.4.7-rc', '--cli-version', '1.4.7-rc'], {
69
+ ...process.env,
70
+ HOME: fakeHome,
71
+ USERPROFILE: fakeHome,
72
+ });
73
+ assert.fail('expected setup with conflicting CLI options to fail');
74
+ }
75
+ catch (err) {
76
+ assert.equal(typeof err?.stdout, 'string');
77
+ assert.match(err.stdout, /Use either --cli-spec or --cli-version/);
78
+ }
79
+ }
80
+ finally {
81
+ await fs.rm(fakeHome, { recursive: true, force: true });
82
+ }
83
+ });
64
84
  test('setup installs global skills under ~/.agents/skills/gitnexus', async () => {
65
85
  const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
66
86
  try {
@@ -148,6 +168,30 @@ test('setup configures OpenCode MCP in ~/.config/opencode/opencode.json', async
148
168
  await fs.rm(fakeHome, { recursive: true, force: true });
149
169
  }
150
170
  });
171
+ test('setup --cli-version pins MCP package spec and persists it in config', async () => {
172
+ const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
173
+ try {
174
+ const opencodeDir = path.join(fakeHome, '.config', 'opencode');
175
+ await fs.mkdir(opencodeDir, { recursive: true });
176
+ await runSetup(['--agent', 'opencode', '--cli-version', '1.4.7-rc'], {
177
+ ...process.env,
178
+ HOME: fakeHome,
179
+ USERPROFILE: fakeHome,
180
+ });
181
+ const opencodeConfigPath = path.join(opencodeDir, 'opencode.json');
182
+ const opencodeRaw = await fs.readFile(opencodeConfigPath, 'utf-8');
183
+ const opencodeConfig = JSON.parse(opencodeRaw);
184
+ const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
185
+ const savedConfigRaw = await fs.readFile(configPath, 'utf-8');
186
+ const savedConfig = JSON.parse(savedConfigRaw);
187
+ assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['npx', '-y', '@veewo/gitnexus@1.4.7-rc', 'mcp']);
188
+ assert.equal(savedConfig.cliPackageSpec, '@veewo/gitnexus@1.4.7-rc');
189
+ assert.equal(savedConfig.cliVersion, '1.4.7-rc');
190
+ }
191
+ finally {
192
+ await fs.rm(fakeHome, { recursive: true, force: true });
193
+ }
194
+ });
151
195
  test('setup keeps using legacy ~/.config/opencode/config.json when it already exists', async () => {
152
196
  const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
153
197
  try {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Shows the indexing status of the current repository.
5
5
  */
6
- import { findRepo, getStoragePaths, hasKuzuIndex } from '../storage/repo-manager.js';
6
+ import { findRepo, getStoragePaths, hasKuzuIndex, hasLbugIndex } from '../storage/repo-manager.js';
7
7
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
8
8
  export const statusCommand = async () => {
9
9
  const cwd = process.cwd();
@@ -28,6 +28,13 @@ export const statusCommand = async () => {
28
28
  }
29
29
  const currentCommit = getCurrentCommit(repo.repoPath);
30
30
  const isUpToDate = currentCommit === repo.meta.lastCommit;
31
+ const lbugReady = await hasLbugIndex(repo.storagePath);
32
+ if (!lbugReady) {
33
+ console.log(`Repository: ${repo.repoPath}`);
34
+ console.log('Status: ⚠️ index metadata exists but LadybugDB is missing');
35
+ console.log('Run: gitnexus analyze --force');
36
+ return;
37
+ }
31
38
  console.log(`Repository: ${repo.repoPath}`);
32
39
  console.log(`Indexed: ${new Date(repo.meta.indexedAt).toLocaleString()}`);
33
40
  console.log(`Indexed commit: ${repo.meta.lastCommit?.slice(0, 7)}`);
@@ -0,0 +1,29 @@
1
+ export declare const DEFAULT_GITNEXUS_PACKAGE_NAME = "@veewo/gitnexus";
2
+ export declare const DEFAULT_GITNEXUS_DIST_TAG = "latest";
3
+ export declare const CLI_SPEC_ENV_KEY = "GITNEXUS_CLI_SPEC";
4
+ export declare const CLI_VERSION_ENV_KEY = "GITNEXUS_CLI_VERSION";
5
+ type CliSpecSource = 'explicit-spec' | 'explicit-version' | 'env-spec' | 'env-version' | 'config-spec' | 'config-version' | 'default';
6
+ export interface CliSpecConfigLike {
7
+ cliPackageSpec?: string;
8
+ cliVersion?: string;
9
+ }
10
+ export interface ResolveCliSpecInput {
11
+ packageName?: string;
12
+ explicitSpec?: string;
13
+ explicitVersion?: string;
14
+ config?: CliSpecConfigLike;
15
+ env?: NodeJS.ProcessEnv;
16
+ defaultDistTag?: string;
17
+ }
18
+ export interface ResolvedCliSpec {
19
+ packageName: string;
20
+ packageSpec: string;
21
+ source: CliSpecSource;
22
+ }
23
+ /**
24
+ * Resolve package name from package.json with fallback for unusual runtimes.
25
+ */
26
+ export declare function resolveGitNexusPackageName(): string;
27
+ export declare function resolveCliSpec(input?: ResolveCliSpecInput): ResolvedCliSpec;
28
+ export declare function buildNpxCommand(packageSpec: string, subcommand: string): string;
29
+ export {};
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export const DEFAULT_GITNEXUS_PACKAGE_NAME = '@veewo/gitnexus';
5
+ export const DEFAULT_GITNEXUS_DIST_TAG = 'latest';
6
+ export const CLI_SPEC_ENV_KEY = 'GITNEXUS_CLI_SPEC';
7
+ export const CLI_VERSION_ENV_KEY = 'GITNEXUS_CLI_VERSION';
8
+ let cachedPackageName = null;
9
+ /**
10
+ * Resolve package name from package.json with fallback for unusual runtimes.
11
+ */
12
+ export function resolveGitNexusPackageName() {
13
+ if (cachedPackageName)
14
+ return cachedPackageName;
15
+ try {
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const packageJsonPath = path.join(__dirname, '..', '..', '..', 'package.json');
19
+ const raw = readFileSync(packageJsonPath, 'utf-8');
20
+ const parsed = JSON.parse(raw);
21
+ const trimmed = typeof parsed.name === 'string' ? parsed.name.trim() : '';
22
+ cachedPackageName = trimmed || DEFAULT_GITNEXUS_PACKAGE_NAME;
23
+ }
24
+ catch {
25
+ cachedPackageName = DEFAULT_GITNEXUS_PACKAGE_NAME;
26
+ }
27
+ return cachedPackageName;
28
+ }
29
+ function hasPinnedVersion(packageSpec) {
30
+ const trimmed = packageSpec.trim();
31
+ if (!trimmed)
32
+ return false;
33
+ if (trimmed.startsWith('@')) {
34
+ return trimmed.indexOf('@', 1) !== -1;
35
+ }
36
+ return trimmed.includes('@');
37
+ }
38
+ function looksLikePackageSpec(token) {
39
+ if (!token)
40
+ return false;
41
+ if (token.startsWith('@'))
42
+ return true;
43
+ if (token.includes('/'))
44
+ return true;
45
+ return token.includes('@');
46
+ }
47
+ function normalizePackageSpec(packageName, raw, defaultDistTag) {
48
+ const token = raw.trim();
49
+ if (!token)
50
+ return `${packageName}@${defaultDistTag}`;
51
+ if (looksLikePackageSpec(token)) {
52
+ return hasPinnedVersion(token) ? token : `${token}@${defaultDistTag}`;
53
+ }
54
+ return `${packageName}@${token}`;
55
+ }
56
+ export function resolveCliSpec(input = {}) {
57
+ const packageName = (input.packageName || resolveGitNexusPackageName()).trim() || DEFAULT_GITNEXUS_PACKAGE_NAME;
58
+ const env = input.env || process.env;
59
+ const config = input.config || {};
60
+ const defaultDistTag = (input.defaultDistTag || DEFAULT_GITNEXUS_DIST_TAG).trim() || DEFAULT_GITNEXUS_DIST_TAG;
61
+ const candidates = [
62
+ { value: input.explicitSpec, source: 'explicit-spec' },
63
+ { value: input.explicitVersion, source: 'explicit-version' },
64
+ { value: env[CLI_SPEC_ENV_KEY], source: 'env-spec' },
65
+ { value: env[CLI_VERSION_ENV_KEY], source: 'env-version' },
66
+ { value: config.cliPackageSpec, source: 'config-spec' },
67
+ { value: config.cliVersion, source: 'config-version' },
68
+ ];
69
+ for (const candidate of candidates) {
70
+ const trimmed = (candidate.value || '').trim();
71
+ if (!trimmed)
72
+ continue;
73
+ return {
74
+ packageName,
75
+ packageSpec: normalizePackageSpec(packageName, trimmed, defaultDistTag),
76
+ source: candidate.source,
77
+ };
78
+ }
79
+ return {
80
+ packageName,
81
+ packageSpec: `${packageName}@${defaultDistTag}`,
82
+ source: 'default',
83
+ };
84
+ }
85
+ export function buildNpxCommand(packageSpec, subcommand) {
86
+ return `npx -y ${packageSpec} ${subcommand}`.trim();
87
+ }
@@ -5,6 +5,8 @@
5
5
  * All resources use repo-scoped URIs: gitnexus://repo/{name}/context
6
6
  */
7
7
  import { checkStaleness } from './staleness.js';
8
+ import { loadCLIConfig } from '../storage/repo-manager.js';
9
+ import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
8
10
  /**
9
11
  * Static resources — includes per-repo resources and the global repos list
10
12
  */
@@ -121,6 +123,11 @@ export async function readResource(uri, backend) {
121
123
  throw new Error(`Unknown resource: ${uri}`);
122
124
  }
123
125
  }
126
+ async function resolveAnalyzeNpxCommand() {
127
+ const config = await loadCLIConfig();
128
+ const packageSpec = resolveCliSpec({ config }).packageSpec;
129
+ return buildNpxCommand(packageSpec, 'analyze');
130
+ }
124
131
  // ─── Resource Implementations ─────────────────────────────────────────
125
132
  /**
126
133
  * Repos resource — list all indexed repositories
@@ -186,7 +193,8 @@ async function getContextResource(backend, repoName) {
186
193
  lines.push(' - cypher: Raw graph queries');
187
194
  lines.push(' - list_repos: Discover all indexed repositories');
188
195
  lines.push('');
189
- lines.push('re_index: If data is stale, ask user whether to run `npx -y @veewo/gitnexus@latest analyze` (reuses previous analyze scope/options unless `--no-reuse-options` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.');
196
+ const analyzeCmd = await resolveAnalyzeNpxCommand();
197
+ lines.push(`re_index: If data is stale, ask user whether to run \`gitnexus analyze\` when local CLI exists; otherwise run \`${analyzeCmd}\` (reuses previous analyze scope/options unless \`--no-reuse-options\` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.`);
190
198
  lines.push('');
191
199
  lines.push('resources_available:');
192
200
  lines.push(' - gitnexus://repos: All indexed repositories');
@@ -371,8 +379,9 @@ async function getProcessDetailResource(name, backend, repoName) {
371
379
  */
372
380
  async function getSetupResource(backend) {
373
381
  const repos = await backend.listRepos();
382
+ const analyzeCmd = await resolveAnalyzeNpxCommand();
374
383
  if (repos.length === 0) {
375
- return '# GitNexus\n\nNo repositories indexed. Run: `npx -y @veewo/gitnexus@latest analyze` in a repository.';
384
+ return `# GitNexus\n\nNo repositories indexed. Run: \`gitnexus analyze\` when local CLI exists; otherwise \`${analyzeCmd}\` in a repository.`;
376
385
  }
377
386
  const sections = [];
378
387
  for (const repo of repos) {
@@ -64,6 +64,10 @@ export declare const getStoragePaths: (repoPath: string) => {
64
64
  * Non-destructive — safe to call from status commands.
65
65
  */
66
66
  export declare const hasKuzuIndex: (storagePath: string) => Promise<boolean>;
67
+ /**
68
+ * Check whether a LadybugDB index exists in the given storage path.
69
+ */
70
+ export declare const hasLbugIndex: (storagePath: string) => Promise<boolean>;
67
71
  /**
68
72
  * Clean up stale KuzuDB files after migration to LadybugDB.
69
73
  *
@@ -135,6 +139,8 @@ export interface CLIConfig {
135
139
  model?: string;
136
140
  baseUrl?: string;
137
141
  setupScope?: 'global' | 'project';
142
+ cliPackageSpec?: string;
143
+ cliVersion?: string;
138
144
  }
139
145
  /**
140
146
  * Get the path to the global CLI config file
@@ -52,6 +52,18 @@ export const hasKuzuIndex = async (storagePath) => {
52
52
  return false;
53
53
  }
54
54
  };
55
+ /**
56
+ * Check whether a LadybugDB index exists in the given storage path.
57
+ */
58
+ export const hasLbugIndex = async (storagePath) => {
59
+ try {
60
+ await fs.stat(path.join(storagePath, 'lbug'));
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ };
55
67
  /**
56
68
  * Clean up stale KuzuDB files after migration to LadybugDB.
57
69
  *
@@ -279,6 +291,7 @@ export const listRegisteredRepos = async (opts) => {
279
291
  for (const entry of entries) {
280
292
  try {
281
293
  await fs.access(path.join(entry.storagePath, 'meta.json'));
294
+ await fs.access(path.join(entry.storagePath, 'lbug'));
282
295
  valid.push(entry);
283
296
  }
284
297
  catch {
@@ -13,7 +13,41 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const os = require('os');
16
17
  const { spawnSync } = require('child_process');
18
+ const DEFAULT_NPX_SPEC = '@veewo/gitnexus@latest';
19
+ const DEFAULT_PACKAGE_NAME = '@veewo/gitnexus';
20
+
21
+ function normalizeNpxSpec(raw) {
22
+ const value = (raw || '').trim();
23
+ if (!value) return DEFAULT_NPX_SPEC;
24
+ if (value.startsWith('@') || value.includes('/') || value.includes('@')) {
25
+ if (value.startsWith('@') && value.indexOf('@', 1) === -1) return `${value}@latest`;
26
+ if (!value.startsWith('@') && !value.includes('/') && !value.includes('@')) return `${DEFAULT_PACKAGE_NAME}@${value}`;
27
+ if (!value.startsWith('@') && value.includes('@')) return value;
28
+ if (!value.includes('@')) return `${value}@latest`;
29
+ return value;
30
+ }
31
+ return `${DEFAULT_PACKAGE_NAME}@${value}`;
32
+ }
33
+
34
+ function resolveNpxSpec() {
35
+ if (process.env.GITNEXUS_CLI_SPEC) return normalizeNpxSpec(process.env.GITNEXUS_CLI_SPEC);
36
+ if (process.env.GITNEXUS_CLI_VERSION) return normalizeNpxSpec(process.env.GITNEXUS_CLI_VERSION);
37
+
38
+ try {
39
+ const raw = fs.readFileSync(path.join(os.homedir(), '.gitnexus', 'config.json'), 'utf-8');
40
+ const parsed = JSON.parse(raw);
41
+ if (typeof parsed.cliPackageSpec === 'string' && parsed.cliPackageSpec.trim()) {
42
+ return normalizeNpxSpec(parsed.cliPackageSpec);
43
+ }
44
+ if (typeof parsed.cliVersion === 'string' && parsed.cliVersion.trim()) {
45
+ return normalizeNpxSpec(parsed.cliVersion);
46
+ }
47
+ } catch {}
48
+
49
+ return DEFAULT_NPX_SPEC;
50
+ }
17
51
 
18
52
  /**
19
53
  * Read JSON input from stdin synchronously.
@@ -107,7 +141,7 @@ function resolveCliPath() {
107
141
  * Spawn a gitnexus CLI command synchronously.
108
142
  * Returns the stderr output (KuzuDB captures stdout at OS level).
109
143
  */
110
- function runGitNexusCli(cliPath, args, cwd, timeout) {
144
+ function runGitNexusCli(cliPath, args, cwd, timeout, npxSpec) {
111
145
  const isWin = process.platform === 'win32';
112
146
  if (cliPath) {
113
147
  return spawnSync(
@@ -119,7 +153,7 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
119
153
  // On Windows, invoke npx.cmd directly (no shell needed)
120
154
  return spawnSync(
121
155
  isWin ? 'npx.cmd' : 'npx',
122
- ['-y', 'gitnexus', ...args],
156
+ ['-y', npxSpec, ...args],
123
157
  { encoding: 'utf-8', timeout: timeout + 5000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
124
158
  );
125
159
  }
@@ -141,9 +175,10 @@ function handlePreToolUse(input) {
141
175
  if (!pattern || pattern.length < 3) return;
142
176
 
143
177
  const cliPath = resolveCliPath();
178
+ const npxSpec = resolveNpxSpec();
144
179
  let result = '';
145
180
  try {
146
- const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
181
+ const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000, npxSpec);
147
182
  if (!child.error && child.status === 0) {
148
183
  result = child.stderr || '';
149
184
  }
@@ -166,7 +201,7 @@ function sendHookResponse(hookEventName, message) {
166
201
  /**
167
202
  * PostToolUse handler — detect index staleness after git mutations.
168
203
  *
169
- * Instead of spawning a full `npx -y @veewo/gitnexus@latest analyze` synchronously (which blocks
204
+ * Instead of spawning a full analyze command synchronously (which blocks
170
205
  * the agent for up to 120s and risks KuzuDB corruption on timeout), we do a
171
206
  * lightweight staleness check: compare `git rev-parse HEAD` against the
172
207
  * lastCommit stored in `.gitnexus/meta.json`. If they differ, notify the
@@ -210,10 +245,13 @@ function handlePostToolUse(input) {
210
245
  // If HEAD matches last indexed commit, no reindex needed
211
246
  if (currentHead && currentHead === lastCommit) return;
212
247
 
213
- const analyzeCmd = `npx -y @veewo/gitnexus@latest analyze${hadEmbeddings ? ' --embeddings' : ''}`;
248
+ const npxSpec = resolveNpxSpec();
249
+ const analyzeArgs = `analyze${hadEmbeddings ? ' --embeddings' : ''}`;
250
+ const analyzeCmd = `gitnexus ${analyzeArgs}`;
251
+ const fallbackCmd = `npx -y ${npxSpec} ${analyzeArgs}`;
214
252
  sendHookResponse('PostToolUse',
215
253
  `GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
216
- `Run \`${analyzeCmd}\` to update the knowledge graph.`
254
+ `Run \`${analyzeCmd}\` (or \`${fallbackCmd}\`) to update the knowledge graph.`
217
255
  );
218
256
  }
219
257
 
@@ -64,7 +64,11 @@ fi
64
64
 
65
65
  # Run gitnexus augment — must be fast (<500ms target)
66
66
  # augment writes to stderr (KuzuDB captures stdout at OS level), so capture stderr and discard stdout
67
- RESULT=$(cd "$CWD" && npx -y @veewo/gitnexus@latest augment "$PATTERN" 2>&1 1>/dev/null)
67
+ GITNEXUS_CLI_SPEC="${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}"
68
+ if [ -n "$GITNEXUS_CLI_VERSION" ]; then
69
+ GITNEXUS_CLI_SPEC="@veewo/gitnexus@$GITNEXUS_CLI_VERSION"
70
+ fi
71
+ RESULT=$(cd "$CWD" && npx -y "$GITNEXUS_CLI_SPEC" augment "$PATTERN" 2>&1 1>/dev/null)
68
72
 
69
73
  if [ -n "$RESULT" ]; then
70
74
  ESCAPED=$(echo "$RESULT" | jq -Rs .)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veewo/gitnexus",
3
- "version": "1.4.6-rc",
3
+ "version": "1.4.8-rc",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -5,14 +5,25 @@ description: "Use when the user needs to run GitNexus CLI commands like analyze/
5
5
 
6
6
  # GitNexus CLI Commands
7
7
 
8
- All commands work via `npx` no global install required.
8
+ Use one command alias in the session so every CLI/MCP call stays on one version line.
9
+
10
+ ```bash
11
+ # If user prompt specifies a version (example: 1.4.7-rc), set it once:
12
+ # export GITNEXUS_CLI_SPEC="@veewo/gitnexus@1.4.7-rc"
13
+ GITNEXUS_CLI_SPEC="${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}"
14
+ if command -v gitnexus >/dev/null 2>&1; then
15
+ GN="gitnexus"
16
+ else
17
+ GN="npx -y ${GITNEXUS_CLI_SPEC}"
18
+ fi
19
+ ```
9
20
 
10
21
  ## Commands
11
22
 
12
23
  ### analyze — Build or refresh the index
13
24
 
14
25
  ```bash
15
- npx -y @veewo/gitnexus@latest analyze
26
+ $GN analyze
16
27
  ```
17
28
 
18
29
  Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
@@ -27,7 +38,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
27
38
  ### status — Check index freshness
28
39
 
29
40
  ```bash
30
- npx -y @veewo/gitnexus@latest status
41
+ $GN status
31
42
  ```
32
43
 
33
44
  Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.
@@ -35,7 +46,7 @@ Shows whether the current repo has a GitNexus index, when it was last updated, a
35
46
  ### clean — Delete the index
36
47
 
37
48
  ```bash
38
- npx -y @veewo/gitnexus@latest clean
49
+ $GN clean
39
50
  ```
40
51
 
41
52
  Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.
@@ -48,7 +59,7 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
48
59
  ### wiki — Generate documentation from the graph
49
60
 
50
61
  ```bash
51
- npx -y @veewo/gitnexus@latest wiki
62
+ $GN wiki
52
63
  ```
53
64
 
54
65
  Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
@@ -65,7 +76,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
65
76
  ### list — Show all indexed repos
66
77
 
67
78
  ```bash
68
- npx -y @veewo/gitnexus@latest list
79
+ $GN list
69
80
  ```
70
81
 
71
82
  Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
@@ -75,11 +86,11 @@ Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_
75
86
  For Unity resource retrieval:
76
87
 
77
88
  ```bash
78
- npx -y @veewo/gitnexus@latest context DoorObj --repo neonnew-core --file Assets/NEON/Code/Game/Doors/DoorObj.cs --unity-resources on --unity-hydration compact
89
+ $GN context DoorObj --repo neonnew-core --file Assets/NEON/Code/Game/Doors/DoorObj.cs --unity-resources on --unity-hydration compact
79
90
  ```
80
91
 
81
92
  ```bash
82
- npx -y @veewo/gitnexus@latest query "DoorObj binding" --repo neonnew-core --unity-resources on --unity-hydration compact
93
+ $GN query "DoorObj binding" --repo neonnew-core --unity-resources on --unity-hydration compact
83
94
  ```
84
95
 
85
96
  Rules:
@@ -23,7 +23,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
23
23
  5. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
24
24
  ```
25
25
 
26
- > If "Index is stale" → run `npx -y @veewo/gitnexus@latest analyze` in terminal.
26
+ > If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
27
27
 
28
28
  ## Checklist
29
29
 
@@ -24,7 +24,7 @@ description: "Use when the user asks how code works, wants to understand archite
24
24
  6. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
25
25
  ```
26
26
 
27
- > If step 2 says "Index is stale" → run `npx -y @veewo/gitnexus@latest analyze` in terminal.
27
+ > If step 2 says "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
28
28
 
29
29
  ## Checklist
30
30
 
@@ -15,7 +15,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
15
15
  2. **Match your task to a skill below** and **read that skill file**
16
16
  3. **Follow the skill's workflow and checklist**
17
17
 
18
- > If step 1 warns the index is stale, run `npx -y @veewo/gitnexus@latest analyze` in the terminal first.
18
+ > If step 1 warns the index is stale, run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
19
19
 
20
20
  ## Skills
21
21
 
@@ -23,7 +23,7 @@ description: "Use when the user wants to know what will break if they change som
23
23
  4. Assess risk and report to user
24
24
  ```
25
25
 
26
- > If "Index is stale" → run `npx -y @veewo/gitnexus@latest analyze` in terminal.
26
+ > If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
27
27
 
28
28
  ## Checklist
29
29
 
@@ -26,7 +26,7 @@ description: "Use when the user wants to review a pull request, understand what
26
26
  6. Summarize findings with risk assessment
27
27
  ```
28
28
 
29
- > If "Index is stale" → run `npx -y @veewo/gitnexus@latest analyze` in terminal before reviewing.
29
+ > If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze` before reviewing.
30
30
 
31
31
  ## Checklist
32
32
 
@@ -23,7 +23,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru
23
23
  5. Plan update order: interfaces → implementations → callers → tests
24
24
  ```
25
25
 
26
- > If "Index is stale" → run `npx -y @veewo/gitnexus@latest analyze` in terminal.
26
+ > If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
27
27
 
28
28
  ## Checklists
29
29