@veewo/gitnexus 1.3.5 → 1.3.6

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
@@ -22,11 +22,13 @@ AI coding tools don't understand your codebase structure. They edit a function w
22
22
  npx gitnexus analyze
23
23
  ```
24
24
 
25
- 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.
25
+ That's it. This indexes the codebase, updates `AGENTS.md` / `CLAUDE.md` context files, and (when using project scope) installs repo-local agent skills.
26
26
 
27
- To configure MCP for your editor, run `npx gitnexus setup` once or set it up manually below.
27
+ To configure MCP + skills, run `npx gitnexus setup` once (default global mode), or use `npx gitnexus setup --scope project` for project-local mode.
28
28
 
29
- `gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
29
+ `gitnexus setup` supports two scopes:
30
+ - `global` (default): configures global editor MCP + installs global skills
31
+ - `project`: writes repo-local `.mcp.json` + installs repo-local skills
30
32
 
31
33
  ## Team Deployment and Distribution
32
34
 
@@ -152,7 +154,8 @@ Your AI agent gets these tools automatically:
152
154
  ## CLI Commands
153
155
 
154
156
  ```bash
155
- gitnexus setup # Configure MCP for your editors (one-time)
157
+ gitnexus setup # Default: global MCP + global skills
158
+ gitnexus setup --scope project # Project-local MCP + project-local skills
156
159
  gitnexus analyze [path] # Index a repository (or update stale index)
157
160
  gitnexus analyze --force # Force full re-index
158
161
  gitnexus analyze --embeddings # Enable semantic embeddings (off by default)
@@ -212,9 +215,10 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
212
215
 
213
216
  Installation rules:
214
217
 
215
- - `gitnexus analyze` installs repo-local skills to `.agents/skills/gitnexus/` and updates `AGENTS.md` / `CLAUDE.md`.
216
- - `gitnexus setup` installs global skills to `~/.agents/skills/gitnexus/`.
217
- - If needed, create editor-specific symlinks yourself (for example map `.claude/skills/gitnexus` to `~/.agents/skills/gitnexus`).
218
+ - `gitnexus setup` controls skill scope:
219
+ - default `global`: installs to `~/.agents/skills/gitnexus/`
220
+ - `--scope project`: installs to `.agents/skills/gitnexus/` in current repo
221
+ - `gitnexus analyze` always updates `AGENTS.md` / `CLAUDE.md`; skill install follows configured setup scope.
218
222
 
219
223
  ## Requirements
220
224
 
@@ -5,6 +5,7 @@
5
5
  * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
6
6
  * CLAUDE.md is for Claude Code which only reads that file.
7
7
  */
8
+ type SkillScope = 'project' | 'global';
8
9
  interface RepoStats {
9
10
  files?: number;
10
11
  nodes?: number;
@@ -16,7 +17,9 @@ interface RepoStats {
16
17
  /**
17
18
  * Generate AI context files after indexing
18
19
  */
19
- export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats): Promise<{
20
+ export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, options?: {
21
+ skillScope?: SkillScope;
22
+ }): Promise<{
20
23
  files: string[];
21
24
  }>;
22
25
  export {};
@@ -23,7 +23,10 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
23
23
  * - One-line quick start (read context resource) gives agents an entry point
24
24
  * - Tools/Resources sections are labeled "Reference" — agents treat them as lookup, not workflow
25
25
  */
26
- function generateGitNexusContent(projectName, stats) {
26
+ function generateGitNexusContent(projectName, stats, skillScope) {
27
+ const skillRoot = skillScope === 'global'
28
+ ? '~/.agents/skills/gitnexus'
29
+ : '.agents/skills/gitnexus';
27
30
  return `${GITNEXUS_START_MARKER}
28
31
  # GitNexus MCP
29
32
 
@@ -41,12 +44,12 @@ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} s
41
44
 
42
45
  | Task | Read this skill file |
43
46
  |------|---------------------|
44
- | Understand architecture / "How does X work?" | \`.agents/skills/gitnexus/gitnexus-exploring/SKILL.md\` |
45
- | Blast radius / "What breaks if I change X?" | \`.agents/skills/gitnexus/gitnexus-impact-analysis/SKILL.md\` |
46
- | Trace bugs / "Why is X failing?" | \`.agents/skills/gitnexus/gitnexus-debugging/SKILL.md\` |
47
- | Rename / extract / split / refactor | \`.agents/skills/gitnexus/gitnexus-refactoring/SKILL.md\` |
48
- | Tools, resources, schema reference | \`.agents/skills/gitnexus/gitnexus-guide/SKILL.md\` |
49
- | Index, status, clean, wiki CLI commands | \`.agents/skills/gitnexus/gitnexus-cli/SKILL.md\` |
47
+ | Understand architecture / "How does X work?" | \`${skillRoot}/gitnexus-exploring/SKILL.md\` |
48
+ | Blast radius / "What breaks if I change X?" | \`${skillRoot}/gitnexus-impact-analysis/SKILL.md\` |
49
+ | Trace bugs / "Why is X failing?" | \`${skillRoot}/gitnexus-debugging/SKILL.md\` |
50
+ | Rename / extract / split / refactor | \`${skillRoot}/gitnexus-refactoring/SKILL.md\` |
51
+ | Tools, resources, schema reference | \`${skillRoot}/gitnexus-guide/SKILL.md\` |
52
+ | Index, status, clean, wiki CLI commands | \`${skillRoot}/gitnexus-cli/SKILL.md\` |
50
53
 
51
54
  ${GITNEXUS_END_MARKER}`;
52
55
  }
@@ -164,8 +167,9 @@ Use GitNexus tools to accomplish this task.
164
167
  /**
165
168
  * Generate AI context files after indexing
166
169
  */
167
- export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats) {
168
- const content = generateGitNexusContent(projectName, stats);
170
+ export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, options) {
171
+ const skillScope = options?.skillScope === 'global' ? 'global' : 'project';
172
+ const content = generateGitNexusContent(projectName, stats, skillScope);
169
173
  const createdFiles = [];
170
174
  // Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Codex, Cline, etc.)
171
175
  const agentsPath = path.join(repoPath, 'AGENTS.md');
@@ -175,10 +179,12 @@ export async function generateAIContextFiles(repoPath, _storagePath, projectName
175
179
  const claudePath = path.join(repoPath, 'CLAUDE.md');
176
180
  const claudeResult = await upsertGitNexusSection(claudePath, content);
177
181
  createdFiles.push(`CLAUDE.md (${claudeResult})`);
178
- // Install skills to .agents/skills/gitnexus/
179
- const installedSkills = await installSkills(repoPath);
180
- if (installedSkills.length > 0) {
181
- createdFiles.push(`.agents/skills/gitnexus/ (${installedSkills.length} skills)`);
182
+ // Install repo-local skills only when project scope is selected.
183
+ if (skillScope === 'project') {
184
+ const installedSkills = await installSkills(repoPath);
185
+ if (installedSkills.length > 0) {
186
+ createdFiles.push(`.agents/skills/gitnexus/ (${installedSkills.length} skills)`);
187
+ }
182
188
  }
183
189
  return { files: createdFiles };
184
190
  }
@@ -11,7 +11,7 @@ test('generateAIContextFiles installs repo skills under .agents/skills/gitnexus'
11
11
  nodes: 1,
12
12
  edges: 2,
13
13
  processes: 3,
14
- });
14
+ }, { skillScope: 'project' });
15
15
  const agentsPath = path.join(repoPath, 'AGENTS.md');
16
16
  const claudePath = path.join(repoPath, 'CLAUDE.md');
17
17
  const skillPath = path.join(repoPath, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
@@ -28,3 +28,25 @@ test('generateAIContextFiles installs repo skills under .agents/skills/gitnexus'
28
28
  await fs.rm(repoPath, { recursive: true, force: true });
29
29
  }
30
30
  });
31
+ test('generateAIContextFiles with global scope skips repo skill install', async () => {
32
+ const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-ai-context-global-'));
33
+ try {
34
+ const result = await generateAIContextFiles(repoPath, '', 'demo-repo', {
35
+ nodes: 1,
36
+ edges: 2,
37
+ processes: 3,
38
+ }, { skillScope: 'global' });
39
+ const agentsPath = path.join(repoPath, 'AGENTS.md');
40
+ const claudePath = path.join(repoPath, 'CLAUDE.md');
41
+ const localSkillsDir = path.join(repoPath, '.agents', 'skills', 'gitnexus');
42
+ const agentsContent = await fs.readFile(agentsPath, 'utf-8');
43
+ const claudeContent = await fs.readFile(claudePath, 'utf-8');
44
+ assert.match(agentsContent, /~\/\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
45
+ assert.match(claudeContent, /~\/\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
46
+ assert.ok(!result.files.some((entry) => entry.includes('.agents/skills/gitnexus/')), 'did not expect repo-local skills in generated file summary');
47
+ await assert.rejects(fs.access(localSkillsDir));
48
+ }
49
+ finally {
50
+ await fs.rm(repoPath, { recursive: true, force: true });
51
+ }
52
+ });
@@ -13,7 +13,7 @@ import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReuse
13
13
  // loaded when embeddings are not requested. This avoids crashes on Node
14
14
  // versions whose ABI is not yet supported by the native binary (#89).
15
15
  // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
16
- import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
16
+ import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, loadCLIConfig } from '../storage/repo-manager.js';
17
17
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
18
18
  import { generateAIContextFiles } from './ai-context.js';
19
19
  import fs from 'fs/promises';
@@ -312,6 +312,8 @@ export const analyzeCommand = async (inputPath, options) => {
312
312
  communities: pipelineResult.communityResult?.stats.totalCommunities,
313
313
  clusters: aggregatedClusterCount,
314
314
  processes: pipelineResult.processResult?.stats.totalProcesses,
315
+ }, {
316
+ skillScope: ((await loadCLIConfig()).setupScope === 'global') ? 'global' : 'project',
315
317
  });
316
318
  await closeKuzu();
317
319
  // Note: we intentionally do NOT call disposeEmbedder() here.
package/dist/cli/index.js CHANGED
@@ -60,6 +60,7 @@ program
60
60
  program
61
61
  .command('setup')
62
62
  .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex')
63
+ .option('--scope <scope>', 'Install target: global (default) or project')
63
64
  .action(setupCommand);
64
65
  program
65
66
  .command('analyze [path]')
@@ -5,4 +5,8 @@
5
5
  * Detects installed AI editors and writes the appropriate MCP config
6
6
  * so the GitNexus MCP server is available in all projects.
7
7
  */
8
- export declare const setupCommand: () => Promise<void>;
8
+ interface SetupOptions {
9
+ scope?: string;
10
+ }
11
+ export declare const setupCommand: (options?: SetupOptions) => Promise<void>;
12
+ export {};
package/dist/cli/setup.js CHANGED
@@ -11,10 +11,18 @@ import os from 'os';
11
11
  import { execFile } from 'node:child_process';
12
12
  import { promisify } from 'node:util';
13
13
  import { fileURLToPath } from 'url';
14
- import { getGlobalDir } from '../storage/repo-manager.js';
14
+ import { getGlobalDir, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
15
+ import { getGitRoot } from '../storage/git.js';
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
17
18
  const execFileAsync = promisify(execFile);
19
+ function resolveSetupScope(rawScope) {
20
+ if (!rawScope || rawScope.trim() === '')
21
+ return 'global';
22
+ if (rawScope === 'global' || rawScope === 'project')
23
+ return rawScope;
24
+ throw new Error(`Invalid --scope value "${rawScope}". Use "global" or "project".`);
25
+ }
18
26
  /**
19
27
  * The MCP server entry for all editors.
20
28
  * On Windows, npx must be invoked via cmd /c since it's a .cmd script.
@@ -125,6 +133,18 @@ async function installGlobalAgentSkills(result) {
125
133
  result.errors.push(`Global agent skills: ${err.message}`);
126
134
  }
127
135
  }
136
+ async function installProjectAgentSkills(repoRoot, result) {
137
+ const skillsDir = path.join(repoRoot, '.agents', 'skills', 'gitnexus');
138
+ try {
139
+ const installed = await installSkillsTo(skillsDir);
140
+ if (installed.length > 0) {
141
+ result.configured.push(`Project agent skills (${installed.length} skills → ${path.relative(repoRoot, skillsDir)}/)`);
142
+ }
143
+ }
144
+ catch (err) {
145
+ result.errors.push(`Project agent skills: ${err.message}`);
146
+ }
147
+ }
128
148
  /**
129
149
  * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
130
150
  * Merges hook config without overwriting existing hooks.
@@ -212,6 +232,28 @@ async function setupCodex(result) {
212
232
  result.errors.push(`Codex: ${err.message}`);
213
233
  }
214
234
  }
235
+ async function setupProjectMcp(repoRoot, result) {
236
+ const mcpPath = path.join(repoRoot, '.mcp.json');
237
+ try {
238
+ const existing = await readJsonFile(mcpPath);
239
+ const updated = mergeMcpConfig(existing);
240
+ await writeJsonFile(mcpPath, updated);
241
+ result.configured.push(`Project MCP (${path.relative(repoRoot, mcpPath)})`);
242
+ }
243
+ catch (err) {
244
+ result.errors.push(`Project MCP: ${err.message}`);
245
+ }
246
+ }
247
+ async function saveSetupScope(scope, result) {
248
+ try {
249
+ const existing = await loadCLIConfig();
250
+ await saveCLIConfig({ ...existing, setupScope: scope });
251
+ result.configured.push(`Default setup scope (${scope})`);
252
+ }
253
+ catch (err) {
254
+ result.errors.push(`Persist setup scope: ${err.message}`);
255
+ }
256
+ }
215
257
  // ─── Skill Installation ───────────────────────────────────────────
216
258
  const SKILL_NAMES = ['gitnexus-exploring', 'gitnexus-debugging', 'gitnexus-impact-analysis', 'gitnexus-refactoring', 'gitnexus-guide', 'gitnexus-cli'];
217
259
  /**
@@ -273,11 +315,20 @@ async function copyDirRecursive(src, dest) {
273
315
  }
274
316
  }
275
317
  // ─── Main command ──────────────────────────────────────────────────
276
- export const setupCommand = async () => {
318
+ export const setupCommand = async (options = {}) => {
277
319
  console.log('');
278
320
  console.log(' GitNexus Setup');
279
321
  console.log(' ==============');
280
322
  console.log('');
323
+ let scope;
324
+ try {
325
+ scope = resolveSetupScope(options.scope);
326
+ }
327
+ catch (err) {
328
+ console.log(` ${err?.message || String(err)}\n`);
329
+ process.exitCode = 1;
330
+ return;
331
+ }
281
332
  // Ensure global directory exists
282
333
  const globalDir = getGlobalDir();
283
334
  await fs.mkdir(globalDir, { recursive: true });
@@ -286,15 +337,28 @@ export const setupCommand = async () => {
286
337
  skipped: [],
287
338
  errors: [],
288
339
  };
289
- // Detect and configure each editor's MCP
290
- await setupCursor(result);
291
- await setupClaudeCode(result);
292
- await setupOpenCode(result);
293
- await setupCodex(result);
294
- // Install shared global skills once
295
- await installGlobalAgentSkills(result);
296
- // Optional Claude-specific hooks
297
- await installClaudeCodeHooks(result);
340
+ if (scope === 'global') {
341
+ // Detect and configure each editor's MCP
342
+ await setupCursor(result);
343
+ await setupClaudeCode(result);
344
+ await setupOpenCode(result);
345
+ await setupCodex(result);
346
+ // Install shared global skills once
347
+ await installGlobalAgentSkills(result);
348
+ // Optional Claude-specific hooks
349
+ await installClaudeCodeHooks(result);
350
+ }
351
+ else {
352
+ const repoRoot = getGitRoot(process.cwd());
353
+ if (!repoRoot) {
354
+ console.log(' --scope project requires running inside a git repository\n');
355
+ process.exitCode = 1;
356
+ return;
357
+ }
358
+ await setupProjectMcp(repoRoot, result);
359
+ await installProjectAgentSkills(repoRoot, result);
360
+ }
361
+ await saveSetupScope(scope, result);
298
362
  // Print results
299
363
  if (result.configured.length > 0) {
300
364
  console.log(' Configured:');
@@ -318,6 +382,7 @@ export const setupCommand = async () => {
318
382
  }
319
383
  console.log('');
320
384
  console.log(' Summary:');
385
+ console.log(` Scope: ${scope}`);
321
386
  console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
322
387
  console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
323
388
  console.log('');
@@ -22,7 +22,11 @@ test('setup installs global skills under ~/.agents/skills/gitnexus', async () =>
22
22
  },
23
23
  });
24
24
  const skillPath = path.join(fakeHome, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
25
+ const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
25
26
  await fs.access(skillPath);
27
+ const configRaw = await fs.readFile(configPath, 'utf-8');
28
+ const config = JSON.parse(configRaw);
29
+ assert.equal(config.setupScope, 'global');
26
30
  assert.ok(true);
27
31
  }
28
32
  finally {
@@ -80,3 +84,41 @@ process.exit(0);
80
84
  await fs.rm(fakeBin, { recursive: true, force: true });
81
85
  }
82
86
  });
87
+ test('setup with --scope project writes local MCP and repo-local skills only', async () => {
88
+ const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
89
+ const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
90
+ try {
91
+ await execFileAsync('git', ['init'], {
92
+ cwd: fakeRepo,
93
+ env: {
94
+ ...process.env,
95
+ HOME: fakeHome,
96
+ USERPROFILE: fakeHome,
97
+ },
98
+ });
99
+ await execFileAsync(process.execPath, [cliPath, 'setup', '--scope', 'project'], {
100
+ cwd: fakeRepo,
101
+ env: {
102
+ ...process.env,
103
+ HOME: fakeHome,
104
+ USERPROFILE: fakeHome,
105
+ },
106
+ });
107
+ const projectMcpPath = path.join(fakeRepo, '.mcp.json');
108
+ const localSkillPath = path.join(fakeRepo, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
109
+ const globalSkillPath = path.join(fakeHome, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
110
+ const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
111
+ const projectMcpRaw = await fs.readFile(projectMcpPath, 'utf-8');
112
+ const projectMcp = JSON.parse(projectMcpRaw);
113
+ const configRaw = await fs.readFile(configPath, 'utf-8');
114
+ const config = JSON.parse(configRaw);
115
+ assert.equal(projectMcp.mcpServers?.gitnexus?.command, 'npx');
116
+ await fs.access(localSkillPath);
117
+ await assert.rejects(fs.access(globalSkillPath));
118
+ assert.equal(config.setupScope, 'project');
119
+ }
120
+ finally {
121
+ await fs.rm(fakeHome, { recursive: true, force: true });
122
+ await fs.rm(fakeRepo, { recursive: true, force: true });
123
+ }
124
+ });
@@ -110,6 +110,7 @@ export interface CLIConfig {
110
110
  apiKey?: string;
111
111
  model?: string;
112
112
  baseUrl?: string;
113
+ setupScope?: 'global' | 'project';
113
114
  }
114
115
  /**
115
116
  * Get the path to the global CLI config file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veewo/gitnexus",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
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",