@veewo/gitnexus 1.4.7-rc → 1.4.8-rc.2

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,16 @@ AI coding tools don't understand your codebase structure. They edit a function w
18
18
  ## Quick Start
19
19
 
20
20
  ```bash
21
- # Index your repo (run from repo root)
22
- npx -y @veewo/gitnexus@latest analyze
21
+ npm install -g @veewo/gitnexus
22
+ gitnexus setup --cli-spec @veewo/gitnexus
23
+ gitnexus analyze
23
24
  ```
24
25
 
25
26
  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
27
 
27
- To configure MCP for your editor, run `npx -y @veewo/gitnexus@latest setup` once or set it up manually below.
28
+ After `setup`, repository workflows resolve any npx package fallback from `~/.gitnexus/config.json` instead of defaulting to `@latest`.
29
+
30
+ To configure MCP for your editor, run `gitnexus setup --cli-spec <packageSpec>` once — or set it up manually below.
28
31
 
29
32
  `gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
30
33
 
@@ -52,7 +55,7 @@ If you prefer to configure manually instead of using `gitnexus setup`:
52
55
  ### Claude Code (full support — MCP + skills + hooks)
53
56
 
54
57
  ```bash
55
- claude mcp add gitnexus -- npx -y @veewo/gitnexus@latest mcp
58
+ claude mcp add gitnexus -- gitnexus mcp
56
59
  ```
57
60
 
58
61
  ### Cursor / Windsurf
@@ -63,8 +66,8 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
63
66
  {
64
67
  "mcpServers": {
65
68
  "gitnexus": {
66
- "command": "npx",
67
- "args": ["-y", "@veewo/gitnexus@latest", "mcp"]
69
+ "command": "gitnexus",
70
+ "args": ["mcp"]
68
71
  }
69
72
  }
70
73
  }
@@ -78,8 +81,8 @@ Add to `~/.config/opencode/config.json`:
78
81
  {
79
82
  "mcp": {
80
83
  "gitnexus": {
81
- "command": "npx",
82
- "args": ["-y", "@veewo/gitnexus@latest", "mcp"]
84
+ "command": "gitnexus",
85
+ "args": ["mcp"]
83
86
  }
84
87
  }
85
88
  }
@@ -137,10 +140,10 @@ Your AI agent gets these tools automatically:
137
140
 
138
141
  ```bash
139
142
  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
143
+ gitnexus analyze [path] # Index a repository (or update stale index)
144
+ gitnexus analyze --force # Force full re-index
145
+ gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
146
+ gitnexus analyze --verbose # Log skipped files when parsers are unavailable
144
147
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
145
148
  gitnexus serve # Start local HTTP server (multi-repo) for web UI
146
149
  gitnexus list # List all indexed repositories
@@ -153,7 +156,7 @@ gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-m
153
156
 
154
157
  ## Multi-Repo Support
155
158
 
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.
159
+ GitNexus supports indexing multiple repositories. Each `gitnexus analyze` registers the repo in a global registry (`~/.gitnexus/registry.json`). The MCP server serves all indexed repos automatically.
157
160
 
158
161
  ## Supported Languages
159
162
 
@@ -188,7 +191,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
188
191
  - **Impact Analysis** — Analyze blast radius before changes
189
192
  - **Refactoring** — Plan safe refactors using dependency mapping
190
193
 
191
- Installed automatically by both `npx -y @veewo/gitnexus@latest analyze` (per-repo) and `gitnexus setup` (global).
194
+ Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setup` (global).
192
195
 
193
196
  ## Requirements
194
197
 
@@ -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 resolve the pinned npx package spec from \`~/.gitnexus/config.json\` (\`cliPackageSpec\` first, then \`cliVersion\`) and run \`${reindexCmd}\` with that exact package spec (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;
@@ -314,7 +295,7 @@ async function installClaudeCodeHooks(result) {
314
295
  result.errors.push(`Claude Code hooks: ${err.message}`);
315
296
  }
316
297
  }
317
- async function setupOpenCode(result) {
298
+ async function setupOpenCode(result, mcpPackageSpec) {
318
299
  const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
319
300
  if (!(await dirExists(opencodeDir))) {
320
301
  result.skipped.push('OpenCode (not installed)');
@@ -323,7 +304,7 @@ async function setupOpenCode(result) {
323
304
  const configPath = await resolveOpenCodeConfigPath(opencodeDir);
324
305
  try {
325
306
  const existing = await readJsonFile(configPath);
326
- const config = mergeOpenCodeConfig(existing);
307
+ const config = mergeOpenCodeConfig(existing, mcpPackageSpec);
327
308
  await writeJsonFile(configPath, config);
328
309
  result.configured.push(`OpenCode (${path.basename(configPath)})`);
329
310
  }
@@ -331,8 +312,8 @@ async function setupOpenCode(result) {
331
312
  result.errors.push(`OpenCode: ${err.message}`);
332
313
  }
333
314
  }
334
- async function setupCodex(result) {
335
- const entry = getMcpEntry();
315
+ async function setupCodex(result, mcpPackageSpec) {
316
+ const entry = getMcpEntry(mcpPackageSpec);
336
317
  try {
337
318
  await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], { timeout: 15000 });
338
319
  result.configured.push('Codex');
@@ -345,11 +326,11 @@ async function setupCodex(result) {
345
326
  result.errors.push(`Codex: ${err.message}`);
346
327
  }
347
328
  }
348
- async function setupProjectMcp(repoRoot, result) {
329
+ async function setupProjectMcp(repoRoot, result, mcpPackageSpec) {
349
330
  const mcpPath = path.join(repoRoot, '.mcp.json');
350
331
  try {
351
332
  const existing = await readJsonFile(mcpPath);
352
- const updated = mergeMcpConfig(existing);
333
+ const updated = mergeMcpConfig(existing, mcpPackageSpec);
353
334
  await writeJsonFile(mcpPath, updated);
354
335
  result.configured.push(`Project MCP (${path.relative(repoRoot, mcpPath)})`);
355
336
  }
@@ -357,7 +338,7 @@ async function setupProjectMcp(repoRoot, result) {
357
338
  result.errors.push(`Project MCP: ${err.message}`);
358
339
  }
359
340
  }
360
- async function setupProjectCodex(repoRoot, result) {
341
+ async function setupProjectCodex(repoRoot, result, mcpPackageSpec) {
361
342
  const codexConfigPath = path.join(repoRoot, '.codex', 'config.toml');
362
343
  try {
363
344
  let existingRaw = '';
@@ -368,7 +349,7 @@ async function setupProjectCodex(repoRoot, result) {
368
349
  if (err?.code !== 'ENOENT')
369
350
  throw err;
370
351
  }
371
- const merged = mergeCodexConfig(existingRaw);
352
+ const merged = mergeCodexConfig(existingRaw, mcpPackageSpec);
372
353
  await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
373
354
  await fs.writeFile(codexConfigPath, merged, 'utf-8');
374
355
  result.configured.push(`Project Codex MCP (${path.relative(repoRoot, codexConfigPath)})`);
@@ -377,11 +358,11 @@ async function setupProjectCodex(repoRoot, result) {
377
358
  result.errors.push(`Project Codex MCP: ${err.message}`);
378
359
  }
379
360
  }
380
- async function setupProjectOpenCode(repoRoot, result) {
361
+ async function setupProjectOpenCode(repoRoot, result, mcpPackageSpec) {
381
362
  const opencodePath = path.join(repoRoot, 'opencode.json');
382
363
  try {
383
364
  const existing = await readJsonFile(opencodePath);
384
- const merged = mergeOpenCodeConfig(existing);
365
+ const merged = mergeOpenCodeConfig(existing, mcpPackageSpec);
385
366
  await writeJsonFile(opencodePath, merged);
386
367
  result.configured.push(`Project OpenCode MCP (${path.relative(repoRoot, opencodePath)})`);
387
368
  }
@@ -389,11 +370,28 @@ async function setupProjectOpenCode(repoRoot, result) {
389
370
  result.errors.push(`Project OpenCode MCP: ${err.message}`);
390
371
  }
391
372
  }
392
- async function saveSetupScope(scope, result) {
373
+ function extractVersionFromPackageSpec(packageSpec) {
374
+ const trimmed = packageSpec.trim();
375
+ if (!trimmed)
376
+ return undefined;
377
+ if (trimmed.startsWith('@')) {
378
+ const at = trimmed.indexOf('@', 1);
379
+ return at > 0 ? trimmed.slice(at + 1) : undefined;
380
+ }
381
+ const at = trimmed.lastIndexOf('@');
382
+ return at > 0 ? trimmed.slice(at + 1) : undefined;
383
+ }
384
+ async function saveSetupConfig(scope, packageSpec, result) {
393
385
  try {
394
386
  const existing = await loadCLIConfig();
395
- await saveCLIConfig({ ...existing, setupScope: scope });
387
+ await saveCLIConfig({
388
+ ...existing,
389
+ setupScope: scope,
390
+ cliPackageSpec: packageSpec,
391
+ cliVersion: extractVersionFromPackageSpec(packageSpec),
392
+ });
396
393
  result.configured.push(`Default setup scope (${scope})`);
394
+ result.configured.push(`CLI package spec (${packageSpec})`);
397
395
  }
398
396
  catch (err) {
399
397
  result.errors.push(`Persist setup scope: ${err.message}`);
@@ -477,6 +475,11 @@ export const setupCommand = async (options = {}) => {
477
475
  console.log(' GitNexus Setup');
478
476
  console.log(' ==============');
479
477
  console.log('');
478
+ if (options.cliSpec && options.cliVersion) {
479
+ console.log(' Use either --cli-spec or --cli-version, not both.\n');
480
+ process.exitCode = 1;
481
+ return;
482
+ }
480
483
  let scope;
481
484
  let agent;
482
485
  const legacyCursorMode = !options.agent || options.agent.trim() === '';
@@ -492,6 +495,13 @@ export const setupCommand = async (options = {}) => {
492
495
  // Ensure global directory exists
493
496
  const globalDir = getGlobalDir();
494
497
  await fs.mkdir(globalDir, { recursive: true });
498
+ const existingConfig = await loadCLIConfig();
499
+ const resolvedCliSpec = resolveCliSpec({
500
+ explicitSpec: options.cliSpec,
501
+ explicitVersion: options.cliVersion,
502
+ config: existingConfig,
503
+ });
504
+ const mcpPackageSpec = resolvedCliSpec.packageSpec || DEFAULT_MCP_PACKAGE_SPEC;
495
505
  const result = {
496
506
  configured: [],
497
507
  skipped: [],
@@ -499,27 +509,27 @@ export const setupCommand = async (options = {}) => {
499
509
  };
500
510
  if (scope === 'global') {
501
511
  if (legacyCursorMode) {
502
- await setupCursor(result);
512
+ await setupCursor(result, mcpPackageSpec);
503
513
  await installLegacyCursorSkills(result);
504
- await saveSetupScope(scope, result);
514
+ await saveSetupConfig(scope, mcpPackageSpec, result);
505
515
  agent = LEGACY_CURSOR_AGENT;
506
516
  }
507
517
  else {
508
518
  // Configure only the selected agent MCP
509
519
  if (agent === 'claude') {
510
- await setupClaudeCode(result);
520
+ await setupClaudeCode(result, mcpPackageSpec);
511
521
  // Claude-only hooks should only be installed when Claude is selected.
512
- await installClaudeCodeHooks(result);
522
+ await installClaudeCodeHooks(result, mcpPackageSpec);
513
523
  }
514
524
  else if (agent === 'opencode') {
515
- await setupOpenCode(result);
525
+ await setupOpenCode(result, mcpPackageSpec);
516
526
  }
517
527
  else if (agent === 'codex') {
518
- await setupCodex(result);
528
+ await setupCodex(result, mcpPackageSpec);
519
529
  }
520
530
  // Install shared global skills once
521
531
  await installGlobalAgentSkills(result);
522
- await saveSetupScope(scope, result);
532
+ await saveSetupConfig(scope, mcpPackageSpec, result);
523
533
  }
524
534
  }
525
535
  else {
@@ -530,16 +540,16 @@ export const setupCommand = async (options = {}) => {
530
540
  return;
531
541
  }
532
542
  if (agent === 'claude') {
533
- await setupProjectMcp(repoRoot, result);
543
+ await setupProjectMcp(repoRoot, result, mcpPackageSpec);
534
544
  }
535
545
  else if (agent === 'codex') {
536
- await setupProjectCodex(repoRoot, result);
546
+ await setupProjectCodex(repoRoot, result, mcpPackageSpec);
537
547
  }
538
548
  else if (agent === 'opencode') {
539
- await setupProjectOpenCode(repoRoot, result);
549
+ await setupProjectOpenCode(repoRoot, result, mcpPackageSpec);
540
550
  }
541
551
  await installProjectAgentSkills(repoRoot, result);
542
- await saveSetupScope(scope, result);
552
+ await saveSetupConfig(scope, mcpPackageSpec, result);
543
553
  }
544
554
  // Print results
545
555
  if (result.configured.length > 0) {
@@ -566,6 +576,7 @@ export const setupCommand = async (options = {}) => {
566
576
  console.log(' Summary:');
567
577
  console.log(` Scope: ${scope}`);
568
578
  console.log(` Agent: ${legacyCursorMode ? LEGACY_CURSOR_AGENT : agent}`);
579
+ console.log(` CLI package spec: ${mcpPackageSpec}`);
569
580
  console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
570
581
  console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
571
582
  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 = '';
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(
@@ -116,10 +150,11 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
116
150
  { encoding: 'utf-8', timeout, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
117
151
  );
118
152
  }
153
+ if (!npxSpec) return null;
119
154
  // On Windows, invoke npx.cmd directly (no shell needed)
120
155
  return spawnSync(
121
156
  isWin ? 'npx.cmd' : 'npx',
122
- ['-y', 'gitnexus', ...args],
157
+ ['-y', npxSpec, ...args],
123
158
  { encoding: 'utf-8', timeout: timeout + 5000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
124
159
  );
125
160
  }
@@ -141,10 +176,11 @@ function handlePreToolUse(input) {
141
176
  if (!pattern || pattern.length < 3) return;
142
177
 
143
178
  const cliPath = resolveCliPath();
179
+ const npxSpec = resolveNpxSpec();
144
180
  let result = '';
145
181
  try {
146
- const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
147
- if (!child.error && child.status === 0) {
182
+ const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000, npxSpec);
183
+ if (child && !child.error && child.status === 0) {
148
184
  result = child.stderr || '';
149
185
  }
150
186
  } catch { /* graceful failure */ }
@@ -166,7 +202,7 @@ function sendHookResponse(hookEventName, message) {
166
202
  /**
167
203
  * PostToolUse handler — detect index staleness after git mutations.
168
204
  *
169
- * Instead of spawning a full `npx -y @veewo/gitnexus@latest analyze` synchronously (which blocks
205
+ * Instead of spawning a full analyze command synchronously (which blocks
170
206
  * the agent for up to 120s and risks KuzuDB corruption on timeout), we do a
171
207
  * lightweight staleness check: compare `git rev-parse HEAD` against the
172
208
  * lastCommit stored in `.gitnexus/meta.json`. If they differ, notify the
@@ -210,10 +246,17 @@ function handlePostToolUse(input) {
210
246
  // If HEAD matches last indexed commit, no reindex needed
211
247
  if (currentHead && currentHead === lastCommit) return;
212
248
 
213
- const analyzeCmd = `npx -y @veewo/gitnexus@latest analyze${hadEmbeddings ? ' --embeddings' : ''}`;
249
+ const npxSpec = resolveNpxSpec();
250
+ const analyzeArgs = `analyze${hadEmbeddings ? ' --embeddings' : ''}`;
251
+ const analyzeCmd = `gitnexus ${analyzeArgs}`;
252
+ const fallbackCmd = npxSpec
253
+ ? `npx -y ${npxSpec} ${analyzeArgs}`
254
+ : null;
214
255
  sendHookResponse('PostToolUse',
215
256
  `GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
216
- `Run \`${analyzeCmd}\` to update the knowledge graph.`
257
+ (fallbackCmd
258
+ ? `Run \`${analyzeCmd}\` (or \`${fallbackCmd}\`) to update the knowledge graph.`
259
+ : `Run \`${analyzeCmd}\` to update the knowledge graph. If local CLI is unavailable, populate ~/.gitnexus/config.json via \`gitnexus setup --cli-spec <packageSpec>\` first.`)
217
260
  );
218
261
  }
219
262
 
@@ -64,7 +64,32 @@ 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
+ if command -v gitnexus >/dev/null 2>&1; then
68
+ RESULT=$(cd "$CWD" && gitnexus augment "$PATTERN" 2>&1 1>/dev/null)
69
+ else
70
+ if [ -n "$GITNEXUS_CLI_SPEC" ]; then
71
+ :
72
+ elif [ -n "$GITNEXUS_CLI_VERSION" ]; then
73
+ GITNEXUS_CLI_SPEC="@veewo/gitnexus@$GITNEXUS_CLI_VERSION"
74
+ elif [ -f "${HOME}/.gitnexus/config.json" ]; then
75
+ GITNEXUS_CLI_SPEC="$(
76
+ node -e 'const fs=require("fs");const os=require("os");const path=require("path");
77
+ try {
78
+ const raw=fs.readFileSync(path.join(os.homedir(),".gitnexus","config.json"),"utf8");
79
+ const parsed=JSON.parse(raw);
80
+ const spec=typeof parsed.cliPackageSpec==="string" && parsed.cliPackageSpec.trim()
81
+ ? parsed.cliPackageSpec.trim()
82
+ : typeof parsed.cliVersion==="string" && parsed.cliVersion.trim()
83
+ ? `@veewo/gitnexus@${parsed.cliVersion.trim()}`
84
+ : "";
85
+ if (spec) process.stdout.write(spec);
86
+ } catch {}'
87
+ )"
88
+ fi
89
+
90
+ [ -z "$GITNEXUS_CLI_SPEC" ] && exit 0
91
+ RESULT=$(cd "$CWD" && npx -y "$GITNEXUS_CLI_SPEC" augment "$PATTERN" 2>&1 1>/dev/null)
92
+ fi
68
93
 
69
94
  if [ -n "$RESULT" ]; then
70
95
  ESCAPED=$(echo "$RESULT" | jq -Rs .)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veewo/gitnexus",
3
- "version": "1.4.7-rc",
3
+ "version": "1.4.8-rc.2",
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,39 @@ 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. After `setup`, treat `~/.gitnexus/config.json` as the only npx version source.
9
+
10
+ ```bash
11
+ if command -v gitnexus >/dev/null 2>&1; then
12
+ GN="gitnexus"
13
+ else
14
+ GITNEXUS_CLI_SPEC="$(
15
+ node -e 'const fs=require("fs");const os=require("os");const path=require("path");
16
+ try {
17
+ const raw=fs.readFileSync(path.join(os.homedir(),".gitnexus","config.json"),"utf8");
18
+ const parsed=JSON.parse(raw);
19
+ const spec=typeof parsed.cliPackageSpec==="string" && parsed.cliPackageSpec.trim()
20
+ ? parsed.cliPackageSpec.trim()
21
+ : typeof parsed.cliVersion==="string" && parsed.cliVersion.trim()
22
+ ? `@veewo/gitnexus@${parsed.cliVersion.trim()}`
23
+ : "";
24
+ if (spec) process.stdout.write(spec);
25
+ } catch {}'
26
+ )"
27
+ if [ -z "$GITNEXUS_CLI_SPEC" ]; then
28
+ echo "Missing GitNexus CLI package spec in ~/.gitnexus/config.json. Run gitnexus setup --cli-spec <packageSpec> first." >&2
29
+ exit 1
30
+ fi
31
+ GN="npx -y ${GITNEXUS_CLI_SPEC}"
32
+ fi
33
+ ```
9
34
 
10
35
  ## Commands
11
36
 
12
37
  ### analyze — Build or refresh the index
13
38
 
14
39
  ```bash
15
- npx -y @veewo/gitnexus@latest analyze
40
+ $GN analyze
16
41
  ```
17
42
 
18
43
  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 +52,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
27
52
  ### status — Check index freshness
28
53
 
29
54
  ```bash
30
- npx -y @veewo/gitnexus@latest status
55
+ $GN status
31
56
  ```
32
57
 
33
58
  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 +60,7 @@ Shows whether the current repo has a GitNexus index, when it was last updated, a
35
60
  ### clean — Delete the index
36
61
 
37
62
  ```bash
38
- npx -y @veewo/gitnexus@latest clean
63
+ $GN clean
39
64
  ```
40
65
 
41
66
  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 +73,7 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
48
73
  ### wiki — Generate documentation from the graph
49
74
 
50
75
  ```bash
51
- npx -y @veewo/gitnexus@latest wiki
76
+ $GN wiki
52
77
  ```
53
78
 
54
79
  Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
@@ -65,7 +90,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
65
90
  ### list — Show all indexed repos
66
91
 
67
92
  ```bash
68
- npx -y @veewo/gitnexus@latest list
93
+ $GN list
69
94
  ```
70
95
 
71
96
  Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
@@ -75,11 +100,11 @@ Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_
75
100
  For Unity resource retrieval:
76
101
 
77
102
  ```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
103
+ $GN context DoorObj --repo neonnew-core --file Assets/NEON/Code/Game/Doors/DoorObj.cs --unity-resources on --unity-hydration compact
79
104
  ```
80
105
 
81
106
  ```bash
82
- npx -y @veewo/gitnexus@latest query "DoorObj binding" --repo neonnew-core --unity-resources on --unity-hydration compact
107
+ $GN query "DoorObj binding" --repo neonnew-core --unity-resources on --unity-hydration compact
83
108
  ```
84
109
 
85
110
  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 resolve the pinned npx package spec from `~/.gitnexus/config.json` and run `npx -y <resolved-cli-spec> 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 resolve the pinned npx package spec from `~/.gitnexus/config.json` and run `npx -y <resolved-cli-spec> 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 resolve the pinned npx package spec from `~/.gitnexus/config.json` and run `npx -y <resolved-cli-spec> 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 resolve the pinned npx package spec from `~/.gitnexus/config.json` and run `npx -y <resolved-cli-spec> 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 resolve the pinned npx package spec from `~/.gitnexus/config.json` and run `npx -y <resolved-cli-spec> 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 resolve the pinned npx package spec from `~/.gitnexus/config.json` and run `npx -y <resolved-cli-spec> analyze`.
27
27
 
28
28
  ## Checklists
29
29