@veewo/gitnexus 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +21 -13
  2. package/dist/cli/analyze-custom-modules-regression.test.d.ts +1 -0
  3. package/dist/cli/analyze-custom-modules-regression.test.js +75 -0
  4. package/dist/cli/analyze-modules-diagnostics.test.d.ts +1 -0
  5. package/dist/cli/analyze-modules-diagnostics.test.js +36 -0
  6. package/dist/cli/index.js +3 -2
  7. package/dist/cli/setup.d.ts +4 -3
  8. package/dist/cli/setup.js +163 -20
  9. package/dist/cli/setup.test.js +180 -34
  10. package/dist/core/ingestion/modules/assignment-engine.d.ts +33 -0
  11. package/dist/core/ingestion/modules/assignment-engine.js +179 -0
  12. package/dist/core/ingestion/modules/assignment-engine.test.d.ts +1 -0
  13. package/dist/core/ingestion/modules/assignment-engine.test.js +111 -0
  14. package/dist/core/ingestion/modules/config-loader.d.ts +2 -0
  15. package/dist/core/ingestion/modules/config-loader.js +186 -0
  16. package/dist/core/ingestion/modules/config-loader.test.d.ts +1 -0
  17. package/dist/core/ingestion/modules/config-loader.test.js +57 -0
  18. package/dist/core/ingestion/modules/rule-matcher.d.ts +12 -0
  19. package/dist/core/ingestion/modules/rule-matcher.js +63 -0
  20. package/dist/core/ingestion/modules/rule-matcher.test.d.ts +1 -0
  21. package/dist/core/ingestion/modules/rule-matcher.test.js +58 -0
  22. package/dist/core/ingestion/modules/types.d.ts +44 -0
  23. package/dist/core/ingestion/modules/types.js +2 -0
  24. package/dist/mcp/local/cluster-aggregation.d.ts +20 -0
  25. package/dist/mcp/local/cluster-aggregation.js +48 -0
  26. package/dist/mcp/local/cluster-aggregation.test.d.ts +1 -0
  27. package/dist/mcp/local/cluster-aggregation.test.js +22 -0
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -24,11 +24,16 @@ npx gitnexus analyze
24
24
 
25
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 + skills, run `npx gitnexus setup` once (default global mode), or use `npx gitnexus setup --scope project` for project-local mode.
27
+ To configure MCP + skills, run `npx gitnexus setup --agent <claude|opencode|codex>` once (default global mode), or add `--scope project` for project-local mode.
28
28
 
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
29
+ `gitnexus setup` requires an agent selection:
30
+ - `--agent claude`: configure Claude MCP only
31
+ - `--agent opencode`: configure OpenCode MCP only
32
+ - `--agent codex`: configure Codex MCP only
33
+
34
+ It also supports two scopes:
35
+ - `global` (default): writes MCP to the selected agent's global config + installs global skills
36
+ - `project`: writes MCP to the selected agent's project-local config + installs repo-local skills
32
37
 
33
38
  ## Team Deployment and Distribution
34
39
 
@@ -38,6 +43,7 @@ For small-team rollout (single stable channel only), follow:
38
43
  Key links:
39
44
  - [npm publish workflow](../.github/workflows/publish.yml)
40
45
  - [CLI package config](./package.json)
46
+ - [Agent install + acceptance runbook](../INSTALL-GUIDE.md)
41
47
 
42
48
  ### Editor Support
43
49
 
@@ -64,7 +70,7 @@ If you prefer to configure manually instead of using `gitnexus setup`:
64
70
  ### Claude Code (full support — MCP + skills + hooks)
65
71
 
66
72
  ```bash
67
- claude mcp add gitnexus -- npx -y gitnexus@latest mcp
73
+ claude mcp add gitnexus -- npx -y @veewo/gitnexus@latest mcp
68
74
  ```
69
75
 
70
76
  ### Cursor / Windsurf
@@ -76,7 +82,7 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
76
82
  "mcpServers": {
77
83
  "gitnexus": {
78
84
  "command": "npx",
79
- "args": ["-y", "gitnexus@latest", "mcp"]
85
+ "args": ["-y", "@veewo/gitnexus@latest", "mcp"]
80
86
  }
81
87
  }
82
88
  }
@@ -84,14 +90,14 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
84
90
 
85
91
  ### OpenCode
86
92
 
87
- Add to `~/.config/opencode/config.json`:
93
+ Add to `~/.config/opencode/opencode.json`:
88
94
 
89
95
  ```json
90
96
  {
91
97
  "mcp": {
92
98
  "gitnexus": {
93
- "command": "npx",
94
- "args": ["-y", "gitnexus@latest", "mcp"]
99
+ "type": "local",
100
+ "command": ["npx", "-y", "@veewo/gitnexus@latest", "mcp"]
95
101
  }
96
102
  }
97
103
  }
@@ -100,7 +106,7 @@ Add to `~/.config/opencode/config.json`:
100
106
  ### Codex
101
107
 
102
108
  ```bash
103
- codex mcp add gitnexus -- npx -y gitnexus@latest mcp
109
+ codex mcp add gitnexus -- npx -y @veewo/gitnexus@latest mcp
104
110
  ```
105
111
 
106
112
  ## How It Works
@@ -154,13 +160,14 @@ Your AI agent gets these tools automatically:
154
160
  ## CLI Commands
155
161
 
156
162
  ```bash
157
- gitnexus setup # Default: global MCP + global skills
158
- gitnexus setup --scope project # Project-local MCP + project-local skills
163
+ gitnexus setup --agent claude # Global setup for Claude
164
+ gitnexus setup --agent codex # Global setup for Codex
165
+ gitnexus setup --scope project --agent opencode # Project-local setup for OpenCode
159
166
  gitnexus analyze [path] # Index a repository (or update stale index)
160
167
  gitnexus analyze --force # Force full re-index
161
168
  gitnexus analyze --embeddings # Enable semantic embeddings (off by default)
162
169
  gitnexus analyze --scope-prefix Assets/NEON/Code --scope-prefix Packages/com.veewo.* # Scoped multi-directory indexing
163
- gitnexus analyze --scope-manifest ./scope.txt --repo-alias neonspark-v1-subset # Scoped indexing + stable repo alias
170
+ gitnexus analyze --scope-manifest .gitnexus/sync-manifest.txt --repo-alias neonspark-v1-subset # Scoped indexing + stable repo alias
164
171
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
165
172
  gitnexus serve # Start local HTTP server (multi-repo) for web UI
166
173
  gitnexus list # List all indexed repositories
@@ -216,6 +223,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
216
223
  Installation rules:
217
224
 
218
225
  - `gitnexus setup` controls skill scope:
226
+ - requires `--agent <claude|opencode|codex>`
219
227
  - default `global`: installs to `~/.agents/skills/gitnexus/`
220
228
  - `--scope project`: installs to `.agents/skills/gitnexus/` in current repo
221
229
  - `gitnexus analyze` always updates `AGENTS.md` / `CLAUDE.md`; skill install follows configured setup scope.
@@ -0,0 +1,75 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
7
+ const FIXTURE_REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../benchmarks/fixtures/unity-mini');
8
+ async function copyFixtureRepo() {
9
+ const tmpRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-custom-modules-'));
10
+ await fs.cp(FIXTURE_REPO, tmpRepo, { recursive: true });
11
+ await fs.mkdir(path.join(tmpRepo, '.gitnexus'), { recursive: true });
12
+ return tmpRepo;
13
+ }
14
+ test('pipeline mixed mode writes config module + auto fallback memberships', { timeout: 120_000 }, async () => {
15
+ const repoPath = await copyFixtureRepo();
16
+ const configPath = path.join(repoPath, '.gitnexus', 'modules.json');
17
+ await fs.writeFile(configPath, JSON.stringify({
18
+ version: 1,
19
+ mode: 'mixed',
20
+ modules: [
21
+ {
22
+ name: 'Factory',
23
+ defaultPriority: 100,
24
+ rules: [
25
+ {
26
+ id: 'factory-file-rule',
27
+ when: {
28
+ all: [{ field: 'file.path', op: 'contains', value: 'MinionFactory.cs' }],
29
+ },
30
+ },
31
+ ],
32
+ },
33
+ {
34
+ name: 'Battle',
35
+ defaultPriority: 100,
36
+ rules: [],
37
+ },
38
+ ],
39
+ }), 'utf-8');
40
+ const result = await runPipelineFromRepo(repoPath, () => { }, { includeExtensions: ['.cs'] });
41
+ const communities = [...result.graph.iterNodes()].filter((n) => n.label === 'Community');
42
+ const memberships = [...result.graph.iterRelationships()].filter((r) => r.type === 'MEMBER_OF');
43
+ const commById = new Map(communities.map((c) => [c.id, c]));
44
+ const labels = communities.map((c) => String(c.properties.heuristicLabel || c.properties.name || c.id));
45
+ assert.ok(labels.includes('Factory'));
46
+ assert.ok(labels.includes('Battle'));
47
+ const createMembership = memberships.find((m) => m.sourceId.includes('MinionFactory.cs:Create'));
48
+ assert.ok(createMembership);
49
+ const targetCommunity = commById.get(createMembership.targetId);
50
+ assert.ok(targetCommunity);
51
+ assert.equal(String(targetCommunity.properties.heuristicLabel), 'Factory');
52
+ const uniquePerSymbol = new Set(memberships.map((m) => m.sourceId));
53
+ assert.equal(uniquePerSymbol.size, memberships.length);
54
+ const battleCommunity = communities.find((c) => String(c.properties.heuristicLabel) === 'Battle');
55
+ assert.ok(battleCommunity);
56
+ assert.ok(!memberships.some((m) => m.targetId === battleCommunity.id));
57
+ const membershipCommunities = new Set(memberships.map((m) => m.targetId));
58
+ const processCommunities = new Set((result.processResult?.processes || []).flatMap((p) => p.communities || []));
59
+ for (const processCommunity of processCommunities) {
60
+ assert.ok(membershipCommunities.has(processCommunity));
61
+ }
62
+ });
63
+ test('pipeline mixed + invalid modules.json fails fast', { timeout: 120_000 }, async () => {
64
+ const repoPath = await copyFixtureRepo();
65
+ const configPath = path.join(repoPath, '.gitnexus', 'modules.json');
66
+ await fs.writeFile(configPath, JSON.stringify({
67
+ version: 1,
68
+ mode: 'mixed',
69
+ modules: [
70
+ { name: 'Dup', defaultPriority: 100, rules: [] },
71
+ { name: 'Dup', defaultPriority: 100, rules: [] },
72
+ ],
73
+ }), 'utf-8');
74
+ await assert.rejects(runPipelineFromRepo(repoPath, () => { }, { includeExtensions: ['.cs'] }), /duplicate module name/i);
75
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { formatModuleDiagnostics } from './analyze.js';
7
+ test('prints one-time fallback warning for mixed + missing modules.json', () => {
8
+ const lines = formatModuleDiagnostics({
9
+ mode: 'mixed',
10
+ usedFallbackAuto: true,
11
+ warnings: ['modules.json missing in mixed mode, fallback to auto'],
12
+ emptyModules: [],
13
+ configuredModuleCount: 0,
14
+ finalModuleCount: 3,
15
+ });
16
+ assert.ok(lines.some((line) => /fallback to auto/i.test(line)));
17
+ });
18
+ test('prints warning for empty modules', () => {
19
+ const lines = formatModuleDiagnostics({
20
+ mode: 'mixed',
21
+ usedFallbackAuto: false,
22
+ warnings: [],
23
+ emptyModules: ['Battle'],
24
+ configuredModuleCount: 1,
25
+ finalModuleCount: 2,
26
+ });
27
+ assert.ok(lines.some((line) => /empty module/i.test(line)));
28
+ assert.ok(lines.some((line) => /Battle/.test(line)));
29
+ });
30
+ test('README mentions modules.json mode semantics and fallback behavior', async () => {
31
+ const here = path.dirname(fileURLToPath(import.meta.url));
32
+ const readmePath = path.resolve(here, '../../README.md');
33
+ const readme = await fs.readFile(readmePath, 'utf-8');
34
+ assert.match(readme, /\.gitnexus\/modules\.json/);
35
+ assert.match(readme, /mixed.*fallback.*auto/i);
36
+ });
package/dist/cli/index.js CHANGED
@@ -59,8 +59,9 @@ program
59
59
  .version(resolveCliVersion());
60
60
  program
61
61
  .command('setup')
62
- .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex')
62
+ .description('One-time setup: configure MCP for a selected coding agent (claude/opencode/codex)')
63
63
  .option('--scope <scope>', 'Install target: global (default) or project')
64
+ .option('--agent <agent>', 'Target coding agent: claude, opencode, or codex')
64
65
  .action(setupCommand);
65
66
  program
66
67
  .command('analyze [path]')
@@ -69,7 +70,7 @@ program
69
70
  .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
70
71
  .option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
71
72
  .option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
72
- .option('--scope-manifest <path>', 'Manifest file with scope rules (supports comments and * wildcard)')
73
+ .option('--scope-manifest <path>', 'Manifest file with scope rules (supports comments and * wildcard; recommended: .gitnexus/sync-manifest.txt)')
73
74
  .option('--scope-prefix <pathPrefix>', 'Add a scope path prefix rule (repeatable)', collectValues, [])
74
75
  .action(analyzeCommand);
75
76
  program
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Setup Command
3
3
  *
4
- * One-time global MCP configuration writer.
5
- * Detects installed AI editors and writes the appropriate MCP config
6
- * so the GitNexus MCP server is available in all projects.
4
+ * One-time MCP configuration writer with explicit agent targeting.
5
+ * Configures only the selected coding agent's MCP entry
6
+ * in either global or project scope.
7
7
  */
8
8
  interface SetupOptions {
9
9
  scope?: string;
10
+ agent?: string;
10
11
  }
11
12
  export declare const setupCommand: (options?: SetupOptions) => Promise<void>;
12
13
  export {};
package/dist/cli/setup.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Setup Command
3
3
  *
4
- * One-time global MCP configuration writer.
5
- * Detects installed AI editors and writes the appropriate MCP config
6
- * so the GitNexus MCP server is available in all projects.
4
+ * One-time MCP configuration writer with explicit agent targeting.
5
+ * Configures only the selected coding agent's MCP entry
6
+ * in either global or project scope.
7
7
  */
8
8
  import fs from 'fs/promises';
9
+ import { readFileSync } from 'node:fs';
9
10
  import path from 'path';
10
11
  import os from 'os';
11
12
  import { execFile } from 'node:child_process';
@@ -16,6 +17,7 @@ import { getGitRoot } from '../storage/git.js';
16
17
  const __filename = fileURLToPath(import.meta.url);
17
18
  const __dirname = path.dirname(__filename);
18
19
  const execFileAsync = promisify(execFile);
20
+ const FALLBACK_MCP_PACKAGE = 'gitnexus@latest';
19
21
  function resolveSetupScope(rawScope) {
20
22
  if (!rawScope || rawScope.trim() === '')
21
23
  return 'global';
@@ -23,6 +25,34 @@ function resolveSetupScope(rawScope) {
23
25
  return rawScope;
24
26
  throw new Error(`Invalid --scope value "${rawScope}". Use "global" or "project".`);
25
27
  }
28
+ function resolveSetupAgent(rawAgent) {
29
+ if (!rawAgent || rawAgent.trim() === '') {
30
+ throw new Error('Missing --agent. Use one of: claude, opencode, codex.');
31
+ }
32
+ if (rawAgent === 'claude' || rawAgent === 'opencode' || rawAgent === 'codex') {
33
+ return rawAgent;
34
+ }
35
+ throw new Error(`Invalid --agent value "${rawAgent}". Use "claude", "opencode", or "codex".`);
36
+ }
37
+ /**
38
+ * Resolve the package spec used by MCP commands.
39
+ * Defaults to gitnexus@latest when package metadata is unavailable.
40
+ */
41
+ function resolveMcpPackageSpec() {
42
+ try {
43
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
44
+ const raw = readFileSync(packageJsonPath, 'utf-8');
45
+ const parsed = JSON.parse(raw);
46
+ if (typeof parsed.name === 'string' && parsed.name.trim().length > 0) {
47
+ return `${parsed.name}@latest`;
48
+ }
49
+ }
50
+ catch {
51
+ // Fallback keeps behavior for unusual runtimes.
52
+ }
53
+ return FALLBACK_MCP_PACKAGE;
54
+ }
55
+ const MCP_PACKAGE_SPEC = resolveMcpPackageSpec();
26
56
  /**
27
57
  * The MCP server entry for all editors.
28
58
  * On Windows, npx must be invoked via cmd /c since it's a .cmd script.
@@ -31,12 +61,19 @@ function getMcpEntry() {
31
61
  if (process.platform === 'win32') {
32
62
  return {
33
63
  command: 'cmd',
34
- args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'],
64
+ args: ['/c', 'npx', '-y', MCP_PACKAGE_SPEC, 'mcp'],
35
65
  };
36
66
  }
37
67
  return {
38
68
  command: 'npx',
39
- args: ['-y', 'gitnexus@latest', 'mcp'],
69
+ args: ['-y', MCP_PACKAGE_SPEC, 'mcp'],
70
+ };
71
+ }
72
+ function getOpenCodeMcpEntry() {
73
+ const entry = getMcpEntry();
74
+ return {
75
+ type: 'local',
76
+ command: [entry.command, ...entry.args],
40
77
  };
41
78
  }
42
79
  /**
@@ -53,6 +90,20 @@ function mergeMcpConfig(existing) {
53
90
  existing.mcpServers.gitnexus = getMcpEntry();
54
91
  return existing;
55
92
  }
93
+ /**
94
+ * Merge gitnexus entry into an OpenCode config JSON object.
95
+ * Returns the updated config.
96
+ */
97
+ function mergeOpenCodeConfig(existing) {
98
+ if (!existing || typeof existing !== 'object') {
99
+ existing = {};
100
+ }
101
+ if (!existing.mcp || typeof existing.mcp !== 'object') {
102
+ existing.mcp = {};
103
+ }
104
+ existing.mcp.gitnexus = getOpenCodeMcpEntry();
105
+ return existing;
106
+ }
56
107
  /**
57
108
  * Try to read a JSON file, returning null if it doesn't exist or is invalid.
58
109
  */
@@ -84,6 +135,53 @@ async function dirExists(dirPath) {
84
135
  return false;
85
136
  }
86
137
  }
138
+ /**
139
+ * Check if a regular file exists.
140
+ */
141
+ async function fileExists(filePath) {
142
+ try {
143
+ const stat = await fs.stat(filePath);
144
+ return stat.isFile();
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ }
150
+ /**
151
+ * Escape a value for TOML string literals.
152
+ */
153
+ function toTomlString(value) {
154
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
155
+ }
156
+ function buildCodexMcpTable() {
157
+ const entry = getMcpEntry();
158
+ return [
159
+ '[mcp_servers.gitnexus]',
160
+ `command = ${toTomlString(entry.command)}`,
161
+ `args = [${entry.args.map(toTomlString).join(', ')}]`,
162
+ ].join('\n');
163
+ }
164
+ function mergeCodexConfig(existingRaw) {
165
+ const table = buildCodexMcpTable();
166
+ const normalized = existingRaw.replace(/\r\n/g, '\n');
167
+ const tablePattern = /\[mcp_servers\.gitnexus\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
168
+ if (tablePattern.test(normalized)) {
169
+ return normalized.replace(tablePattern, table).trimEnd() + '\n';
170
+ }
171
+ const trimmed = normalized.trimEnd();
172
+ if (trimmed.length === 0)
173
+ return `${table}\n`;
174
+ return `${trimmed}\n\n${table}\n`;
175
+ }
176
+ async function resolveOpenCodeConfigPath(opencodeDir) {
177
+ const preferredPath = path.join(opencodeDir, 'opencode.json');
178
+ const legacyPath = path.join(opencodeDir, 'config.json');
179
+ if (await fileExists(preferredPath))
180
+ return preferredPath;
181
+ if (await fileExists(legacyPath))
182
+ return legacyPath;
183
+ return preferredPath;
184
+ }
87
185
  // ─── Editor-specific setup ─────────────────────────────────────────
88
186
  async function setupCursor(result) {
89
187
  const cursorDir = path.join(os.homedir(), '.cursor');
@@ -113,7 +211,7 @@ async function setupClaudeCode(result) {
113
211
  console.log('');
114
212
  console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
115
213
  console.log('');
116
- console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp');
214
+ console.log(` claude mcp add gitnexus -- npx -y ${MCP_PACKAGE_SPEC} mcp`);
117
215
  console.log('');
118
216
  result.configured.push('Claude Code (MCP manual step printed)');
119
217
  }
@@ -204,15 +302,12 @@ async function setupOpenCode(result) {
204
302
  result.skipped.push('OpenCode (not installed)');
205
303
  return;
206
304
  }
207
- const configPath = path.join(opencodeDir, 'config.json');
305
+ const configPath = await resolveOpenCodeConfigPath(opencodeDir);
208
306
  try {
209
307
  const existing = await readJsonFile(configPath);
210
- const config = existing || {};
211
- if (!config.mcp)
212
- config.mcp = {};
213
- config.mcp.gitnexus = getMcpEntry();
308
+ const config = mergeOpenCodeConfig(existing);
214
309
  await writeJsonFile(configPath, config);
215
- result.configured.push('OpenCode');
310
+ result.configured.push(`OpenCode (${path.basename(configPath)})`);
216
311
  }
217
312
  catch (err) {
218
313
  result.errors.push(`OpenCode: ${err.message}`);
@@ -244,6 +339,38 @@ async function setupProjectMcp(repoRoot, result) {
244
339
  result.errors.push(`Project MCP: ${err.message}`);
245
340
  }
246
341
  }
342
+ async function setupProjectCodex(repoRoot, result) {
343
+ const codexConfigPath = path.join(repoRoot, '.codex', 'config.toml');
344
+ try {
345
+ let existingRaw = '';
346
+ try {
347
+ existingRaw = await fs.readFile(codexConfigPath, 'utf-8');
348
+ }
349
+ catch (err) {
350
+ if (err?.code !== 'ENOENT')
351
+ throw err;
352
+ }
353
+ const merged = mergeCodexConfig(existingRaw);
354
+ await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
355
+ await fs.writeFile(codexConfigPath, merged, 'utf-8');
356
+ result.configured.push(`Project Codex MCP (${path.relative(repoRoot, codexConfigPath)})`);
357
+ }
358
+ catch (err) {
359
+ result.errors.push(`Project Codex MCP: ${err.message}`);
360
+ }
361
+ }
362
+ async function setupProjectOpenCode(repoRoot, result) {
363
+ const opencodePath = path.join(repoRoot, 'opencode.json');
364
+ try {
365
+ const existing = await readJsonFile(opencodePath);
366
+ const merged = mergeOpenCodeConfig(existing);
367
+ await writeJsonFile(opencodePath, merged);
368
+ result.configured.push(`Project OpenCode MCP (${path.relative(repoRoot, opencodePath)})`);
369
+ }
370
+ catch (err) {
371
+ result.errors.push(`Project OpenCode MCP: ${err.message}`);
372
+ }
373
+ }
247
374
  async function saveSetupScope(scope, result) {
248
375
  try {
249
376
  const existing = await loadCLIConfig();
@@ -321,8 +448,10 @@ export const setupCommand = async (options = {}) => {
321
448
  console.log(' ==============');
322
449
  console.log('');
323
450
  let scope;
451
+ let agent;
324
452
  try {
325
453
  scope = resolveSetupScope(options.scope);
454
+ agent = resolveSetupAgent(options.agent);
326
455
  }
327
456
  catch (err) {
328
457
  console.log(` ${err?.message || String(err)}\n`);
@@ -338,15 +467,20 @@ export const setupCommand = async (options = {}) => {
338
467
  errors: [],
339
468
  };
340
469
  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);
470
+ // Configure only the selected agent MCP
471
+ if (agent === 'claude') {
472
+ await setupClaudeCode(result);
473
+ // Claude-only hooks should only be installed when Claude is selected.
474
+ await installClaudeCodeHooks(result);
475
+ }
476
+ else if (agent === 'opencode') {
477
+ await setupOpenCode(result);
478
+ }
479
+ else if (agent === 'codex') {
480
+ await setupCodex(result);
481
+ }
346
482
  // Install shared global skills once
347
483
  await installGlobalAgentSkills(result);
348
- // Optional Claude-specific hooks
349
- await installClaudeCodeHooks(result);
350
484
  }
351
485
  else {
352
486
  const repoRoot = getGitRoot(process.cwd());
@@ -355,7 +489,15 @@ export const setupCommand = async (options = {}) => {
355
489
  process.exitCode = 1;
356
490
  return;
357
491
  }
358
- await setupProjectMcp(repoRoot, result);
492
+ if (agent === 'claude') {
493
+ await setupProjectMcp(repoRoot, result);
494
+ }
495
+ else if (agent === 'codex') {
496
+ await setupProjectCodex(repoRoot, result);
497
+ }
498
+ else if (agent === 'opencode') {
499
+ await setupProjectOpenCode(repoRoot, result);
500
+ }
359
501
  await installProjectAgentSkills(repoRoot, result);
360
502
  }
361
503
  await saveSetupScope(scope, result);
@@ -383,6 +525,7 @@ export const setupCommand = async (options = {}) => {
383
525
  console.log('');
384
526
  console.log(' Summary:');
385
527
  console.log(` Scope: ${scope}`);
528
+ console.log(` Agent: ${agent}`);
386
529
  console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
387
530
  console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
388
531
  console.log('');