@soleri/cli 1.9.0 → 1.10.1

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 (94) hide show
  1. package/README.md +4 -0
  2. package/dist/commands/agent.d.ts +8 -0
  3. package/dist/commands/agent.js +150 -0
  4. package/dist/commands/agent.js.map +1 -0
  5. package/dist/commands/create.js +38 -6
  6. package/dist/commands/create.js.map +1 -1
  7. package/dist/commands/install-knowledge.js +65 -3
  8. package/dist/commands/install-knowledge.js.map +1 -1
  9. package/dist/commands/install.d.ts +2 -0
  10. package/dist/commands/install.js +80 -0
  11. package/dist/commands/install.js.map +1 -0
  12. package/dist/commands/pack.d.ts +10 -0
  13. package/dist/commands/pack.js +512 -0
  14. package/dist/commands/pack.js.map +1 -0
  15. package/dist/commands/skills.d.ts +8 -0
  16. package/dist/commands/skills.js +167 -0
  17. package/dist/commands/skills.js.map +1 -0
  18. package/dist/commands/uninstall.d.ts +2 -0
  19. package/dist/commands/uninstall.js +74 -0
  20. package/dist/commands/uninstall.js.map +1 -0
  21. package/dist/hook-packs/installer.d.ts +0 -7
  22. package/dist/hook-packs/installer.js +1 -14
  23. package/dist/hook-packs/installer.js.map +1 -1
  24. package/dist/hook-packs/installer.ts +1 -18
  25. package/dist/hook-packs/registry.d.ts +2 -1
  26. package/dist/hook-packs/registry.ts +1 -1
  27. package/dist/main.js +40 -1
  28. package/dist/main.js.map +1 -1
  29. package/dist/prompts/archetypes.d.ts +1 -0
  30. package/dist/prompts/archetypes.js +177 -62
  31. package/dist/prompts/archetypes.js.map +1 -1
  32. package/dist/prompts/create-wizard.d.ts +3 -3
  33. package/dist/prompts/create-wizard.js +99 -50
  34. package/dist/prompts/create-wizard.js.map +1 -1
  35. package/dist/prompts/playbook.d.ts +8 -7
  36. package/dist/prompts/playbook.js +201 -15
  37. package/dist/prompts/playbook.js.map +1 -1
  38. package/dist/utils/checks.d.ts +0 -1
  39. package/dist/utils/checks.js +1 -1
  40. package/dist/utils/checks.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/__tests__/archetypes.test.ts +84 -0
  43. package/src/__tests__/doctor.test.ts +2 -2
  44. package/src/__tests__/wizard-e2e.mjs +508 -0
  45. package/src/commands/agent.ts +181 -0
  46. package/src/commands/create.ts +152 -104
  47. package/src/commands/install-knowledge.ts +75 -4
  48. package/src/commands/install.ts +101 -0
  49. package/src/commands/pack.ts +585 -0
  50. package/src/commands/skills.ts +191 -0
  51. package/src/commands/uninstall.ts +93 -0
  52. package/src/hook-packs/installer.ts +1 -18
  53. package/src/hook-packs/registry.ts +1 -1
  54. package/src/main.ts +42 -1
  55. package/src/prompts/archetypes.ts +193 -62
  56. package/src/prompts/create-wizard.ts +117 -61
  57. package/src/prompts/playbook.ts +207 -21
  58. package/src/utils/checks.ts +1 -1
  59. package/code-reviewer/.claude/hookify.focus-ring-required.local.md +0 -21
  60. package/code-reviewer/.claude/hookify.no-ai-attribution.local.md +0 -18
  61. package/code-reviewer/.claude/hookify.no-any-types.local.md +0 -18
  62. package/code-reviewer/.claude/hookify.no-console-log.local.md +0 -21
  63. package/code-reviewer/.claude/hookify.no-important.local.md +0 -18
  64. package/code-reviewer/.claude/hookify.no-inline-styles.local.md +0 -21
  65. package/code-reviewer/.claude/hookify.semantic-html.local.md +0 -18
  66. package/code-reviewer/.claude/hookify.ux-touch-targets.local.md +0 -18
  67. package/code-reviewer/.mcp.json +0 -11
  68. package/code-reviewer/README.md +0 -346
  69. package/code-reviewer/package-lock.json +0 -4484
  70. package/code-reviewer/package.json +0 -45
  71. package/code-reviewer/scripts/copy-assets.js +0 -15
  72. package/code-reviewer/scripts/setup.sh +0 -130
  73. package/code-reviewer/skills/brainstorming/SKILL.md +0 -170
  74. package/code-reviewer/skills/code-patrol/SKILL.md +0 -176
  75. package/code-reviewer/skills/context-resume/SKILL.md +0 -143
  76. package/code-reviewer/skills/executing-plans/SKILL.md +0 -201
  77. package/code-reviewer/skills/fix-and-learn/SKILL.md +0 -164
  78. package/code-reviewer/skills/health-check/SKILL.md +0 -225
  79. package/code-reviewer/skills/second-opinion/SKILL.md +0 -142
  80. package/code-reviewer/skills/systematic-debugging/SKILL.md +0 -230
  81. package/code-reviewer/skills/verification-before-completion/SKILL.md +0 -170
  82. package/code-reviewer/skills/writing-plans/SKILL.md +0 -207
  83. package/code-reviewer/src/__tests__/facades.test.ts +0 -598
  84. package/code-reviewer/src/activation/activate.ts +0 -125
  85. package/code-reviewer/src/activation/claude-md-content.ts +0 -217
  86. package/code-reviewer/src/activation/inject-claude-md.ts +0 -113
  87. package/code-reviewer/src/extensions/index.ts +0 -47
  88. package/code-reviewer/src/extensions/ops/example.ts +0 -28
  89. package/code-reviewer/src/identity/persona.ts +0 -62
  90. package/code-reviewer/src/index.ts +0 -278
  91. package/code-reviewer/src/intelligence/data/architecture.json +0 -5
  92. package/code-reviewer/src/intelligence/data/code-review.json +0 -5
  93. package/code-reviewer/tsconfig.json +0 -30
  94. package/code-reviewer/vitest.config.ts +0 -23
@@ -2,137 +2,185 @@ import { readFileSync, existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import type { Command } from 'commander';
4
4
  import * as p from '@clack/prompts';
5
- import { previewScaffold, scaffold, AgentConfigSchema } from '@soleri/forge/lib';
5
+ import {
6
+ previewScaffold,
7
+ scaffold,
8
+ AgentConfigSchema,
9
+ SETUP_TARGETS,
10
+ type SetupTarget,
11
+ } from '@soleri/forge/lib';
6
12
  import { runCreateWizard } from '../prompts/create-wizard.js';
7
13
  import { listPacks } from '../hook-packs/registry.js';
8
14
  import { installPack } from '../hook-packs/installer.js';
9
15
 
16
+ function parseSetupTarget(value?: string): SetupTarget | undefined {
17
+ if (!value) return undefined;
18
+ if ((SETUP_TARGETS as readonly string[]).includes(value)) {
19
+ return value as SetupTarget;
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ function includesClaudeSetup(target: SetupTarget | undefined): boolean {
25
+ const resolved = target ?? 'claude';
26
+ return resolved === 'claude' || resolved === 'both';
27
+ }
28
+
10
29
  export function registerCreate(program: Command): void {
11
30
  program
12
31
  .command('create')
13
32
  .argument('[name]', 'Agent ID (kebab-case)')
14
33
  .option('-c, --config <path>', 'Path to JSON config file (skip interactive prompts)')
34
+ .option(
35
+ '--setup-target <target>',
36
+ `Setup target: ${SETUP_TARGETS.join(', ')} (default: claude)`,
37
+ )
15
38
  .option('-y, --yes', 'Skip confirmation prompts (use with --config for fully non-interactive)')
16
39
  .description('Create a new Soleri agent')
17
- .action(async (name?: string, opts?: { config?: string; yes?: boolean }) => {
18
- try {
19
- let config;
20
-
21
- if (opts?.config) {
22
- // Non-interactive: read from config file
23
- const configPath = resolve(opts.config);
24
- if (!existsSync(configPath)) {
25
- p.log.error(`Config file not found: ${configPath}`);
26
- process.exit(1);
40
+ .action(
41
+ async (name?: string, opts?: { config?: string; yes?: boolean; setupTarget?: string }) => {
42
+ try {
43
+ let config;
44
+
45
+ if (opts?.config) {
46
+ // Non-interactive: read from config file
47
+ const configPath = resolve(opts.config);
48
+ if (!existsSync(configPath)) {
49
+ p.log.error(`Config file not found: ${configPath}`);
50
+ process.exit(1);
51
+ }
52
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
53
+ const parsed = AgentConfigSchema.safeParse(raw);
54
+ if (!parsed.success) {
55
+ p.log.error(`Invalid config: ${parsed.error.message}`);
56
+ process.exit(1);
57
+ }
58
+ config = parsed.data;
59
+ } else {
60
+ // Interactive wizard
61
+ const wizardResult = await runCreateWizard(name);
62
+ if (!wizardResult) {
63
+ p.outro('Cancelled.');
64
+ return;
65
+ }
66
+ const parsed = AgentConfigSchema.safeParse(wizardResult);
67
+ if (!parsed.success) {
68
+ p.log.error(`Invalid config: ${parsed.error.message}`);
69
+ process.exit(1);
70
+ }
71
+ config = parsed.data;
27
72
  }
28
- const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
29
- const parsed = AgentConfigSchema.safeParse(raw);
30
- if (!parsed.success) {
31
- p.log.error(`Invalid config: ${parsed.error.message}`);
73
+
74
+ const setupTarget = parseSetupTarget(opts?.setupTarget);
75
+ if (opts?.setupTarget && !setupTarget) {
76
+ p.log.error(
77
+ `Invalid --setup-target "${opts.setupTarget}". Expected one of: ${SETUP_TARGETS.join(', ')}`,
78
+ );
32
79
  process.exit(1);
33
80
  }
34
- config = parsed.data;
35
- } else {
36
- // Interactive wizard
37
- config = await runCreateWizard(name);
38
- if (!config) {
39
- p.outro('Cancelled.');
40
- return;
81
+ if (setupTarget) {
82
+ config = { ...config, setupTarget };
41
83
  }
42
- }
84
+ const claudeSetup = includesClaudeSetup(config.setupTarget);
85
+
86
+ const nonInteractive = !!(opts?.yes || opts?.config);
87
+
88
+ // Hook packs — from config file or interactive prompt
89
+ let selectedPacks: string[] = [];
90
+ if (!claudeSetup && config.hookPacks && config.hookPacks.length > 0) {
91
+ p.log.warn(
92
+ 'Hook packs are Claude-only. Skipping because setup target excludes Claude.',
93
+ );
94
+ } else if (config.hookPacks && config.hookPacks.length > 0) {
95
+ selectedPacks = config.hookPacks;
43
96
 
44
- const nonInteractive = !!(opts?.yes || opts?.config);
45
-
46
- // Hook packs from config file or interactive prompt
47
- let selectedPacks: string[] = [];
48
- if (config.hookPacks && config.hookPacks.length > 0) {
49
- selectedPacks = config.hookPacks;
50
-
51
- // Validate pack names against registry — warn and skip unknown
52
- const available = listPacks().map((pk) => pk.name);
53
- const unknown = selectedPacks.filter((pk) => !available.includes(pk));
54
- if (unknown.length > 0) {
55
- for (const name of unknown) {
56
- p.log.warn(
57
- `Unknown hook pack "${name}" — skipping. Available: ${available.join(', ')}`,
58
- );
97
+ // Validate pack names against registry — warn and skip unknown
98
+ const available = listPacks().map((pk) => pk.name);
99
+ const unknown = selectedPacks.filter((pk) => !available.includes(pk));
100
+ if (unknown.length > 0) {
101
+ for (const name of unknown) {
102
+ p.log.warn(
103
+ `Unknown hook pack "${name}" — skipping. Available: ${available.join(', ')}`,
104
+ );
105
+ }
106
+ selectedPacks = selectedPacks.filter((pk) => available.includes(pk));
107
+ }
108
+ } else if (!nonInteractive && claudeSetup) {
109
+ const packs = listPacks();
110
+ const packChoices = packs.map((pk) => ({
111
+ value: pk.name,
112
+ label: pk.name,
113
+ hint: `${pk.description} (${pk.hooks.length} hooks)`,
114
+ }));
115
+
116
+ const chosen = await p.multiselect({
117
+ message: 'Install hook packs? (Claude quality gates for ~/.claude/)',
118
+ options: packChoices,
119
+ required: false,
120
+ });
121
+
122
+ if (!p.isCancel(chosen)) {
123
+ selectedPacks = chosen as string[];
59
124
  }
60
- selectedPacks = selectedPacks.filter((pk) => available.includes(pk));
61
- }
62
- } else if (!nonInteractive) {
63
- const packs = listPacks();
64
- const packChoices = packs.map((pk) => ({
65
- value: pk.name,
66
- label: pk.name,
67
- hint: `${pk.description} (${pk.hooks.length} hooks)`,
68
- }));
69
-
70
- const chosen = await p.multiselect({
71
- message: 'Install hook packs? (quality gates for ~/.claude/)',
72
- options: packChoices,
73
- required: false,
74
- });
75
-
76
- if (!p.isCancel(chosen)) {
77
- selectedPacks = chosen as string[];
78
125
  }
79
- }
80
126
 
81
- // Preview
82
- const preview = previewScaffold(config);
127
+ // Preview
128
+ const preview = previewScaffold(config);
83
129
 
84
- p.log.info(`Will create ${preview.files.length} files in ${preview.agentDir}`);
85
- p.log.info(`Facades: ${preview.facades.map((f) => f.name).join(', ')}`);
86
- p.log.info(`Domains: ${preview.domains.join(', ')}`);
87
- if (config.tone) {
88
- p.log.info(`Tone: ${config.tone}`);
89
- }
90
- if (config.skills?.length) {
91
- p.log.info(`Skills: ${config.skills.length} selected`);
92
- }
93
- if (selectedPacks.length > 0) {
94
- p.log.info(`Hook packs: ${selectedPacks.join(', ')}`);
95
- }
130
+ p.log.info(`Will create ${preview.files.length} files in ${preview.agentDir}`);
131
+ p.log.info(`Facades: ${preview.facades.map((f) => f.name).join(', ')}`);
132
+ p.log.info(`Domains: ${preview.domains.join(', ')}`);
133
+ p.log.info(`Setup target: ${config.setupTarget ?? 'claude'}`);
134
+ if (config.tone) {
135
+ p.log.info(`Tone: ${config.tone}`);
136
+ }
137
+ if (config.skills?.length) {
138
+ p.log.info(`Skills: ${config.skills.length} selected`);
139
+ }
140
+ if (selectedPacks.length > 0) {
141
+ p.log.info(`Hook packs: ${selectedPacks.join(', ')}`);
142
+ }
96
143
 
97
- if (!nonInteractive) {
98
- const confirmed = await p.confirm({ message: 'Create agent?' });
99
- if (p.isCancel(confirmed) || !confirmed) {
100
- p.outro('Cancelled.');
101
- return;
144
+ if (!nonInteractive) {
145
+ const confirmed = await p.confirm({ message: 'Create agent?' });
146
+ if (p.isCancel(confirmed) || !confirmed) {
147
+ p.outro('Cancelled.');
148
+ return;
149
+ }
102
150
  }
103
- }
104
151
 
105
- // Scaffold + auto-build
106
- const s = p.spinner();
107
- s.start('Scaffolding and building agent...');
108
- const result = scaffold(config);
109
- s.stop(result.success ? 'Agent created and built!' : 'Scaffolding failed');
152
+ // Scaffold + auto-build
153
+ const s = p.spinner();
154
+ s.start('Scaffolding and building agent...');
155
+ const result = scaffold(config);
156
+ s.stop(result.success ? 'Agent created and built!' : 'Scaffolding failed');
110
157
 
111
- if (!result.success) {
112
- p.log.error(result.summary);
113
- process.exit(1);
114
- }
158
+ if (!result.success) {
159
+ p.log.error(result.summary);
160
+ process.exit(1);
161
+ }
115
162
 
116
- // Install selected hook packs
117
- if (selectedPacks.length > 0) {
118
- for (const packName of selectedPacks) {
119
- const { installed } = installPack(packName, { projectDir: result.agentDir });
120
- if (installed.length > 0) {
121
- p.log.success(`Hook pack "${packName}" installed (${installed.length} hooks)`);
122
- } else {
123
- p.log.info(`Hook pack "${packName}" — all hooks already present`);
163
+ // Install selected hook packs
164
+ if (selectedPacks.length > 0) {
165
+ for (const packName of selectedPacks) {
166
+ const { installed } = installPack(packName, { projectDir: result.agentDir });
167
+ if (installed.length > 0) {
168
+ p.log.success(`Hook pack "${packName}" installed (${installed.length} hooks)`);
169
+ } else {
170
+ p.log.info(`Hook pack "${packName}" — all hooks already present`);
171
+ }
124
172
  }
125
173
  }
126
- }
127
174
 
128
- if (result.success) {
129
- p.note(result.summary, 'Next steps');
130
- }
175
+ if (result.success) {
176
+ p.note(result.summary, 'Next steps');
177
+ }
131
178
 
132
- p.outro('Done!');
133
- } catch (err) {
134
- p.log.error(err instanceof Error ? err.message : String(err));
135
- process.exit(1);
136
- }
137
- });
179
+ p.outro('Done!');
180
+ } catch (err) {
181
+ p.log.error(err instanceof Error ? err.message : String(err));
182
+ process.exit(1);
183
+ }
184
+ },
185
+ );
138
186
  }
@@ -1,13 +1,74 @@
1
1
  import { resolve } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
2
6
  import type { Command } from 'commander';
3
7
  import * as p from '@clack/prompts';
4
8
  import { installKnowledge } from '@soleri/forge/lib';
5
9
  import { detectAgent } from '../utils/agent-context.js';
6
10
 
11
+ /**
12
+ * Resolve a pack identifier to a local path.
13
+ * If `pack` is a local path, return it directly.
14
+ * If it looks like a package name, download from npm.
15
+ */
16
+ async function resolvePackPath(pack: string): Promise<string> {
17
+ // Local path — use directly
18
+ if (pack.startsWith('.') || pack.startsWith('/') || existsSync(pack)) {
19
+ return resolve(pack);
20
+ }
21
+
22
+ // npm package name — resolve to @soleri/knowledge-{name} or use as-is if scoped
23
+ const npmName = pack.startsWith('@') ? pack : `@soleri/knowledge-${pack.replace(/@.*$/, '')}`;
24
+ const version = pack.includes('@') && !pack.startsWith('@') ? pack.split('@').pop() : undefined;
25
+ const spec = version ? `${npmName}@${version}` : npmName;
26
+
27
+ const tmpDir = join(tmpdir(), `soleri-pack-${Date.now()}`);
28
+
29
+ p.log.info(`Resolving ${spec} from npm...`);
30
+
31
+ try {
32
+ execFileSync('npm', ['pack', spec, '--pack-destination', tmpDir], {
33
+ stdio: 'pipe',
34
+ timeout: 30_000,
35
+ });
36
+
37
+ // Find the tarball
38
+ const { readdirSync } = await import('node:fs');
39
+ const { mkdirSync } = await import('node:fs');
40
+ mkdirSync(tmpDir, { recursive: true });
41
+
42
+ // npm pack creates a .tgz file — extract it
43
+ const files = readdirSync(tmpDir).filter((f: string) => f.endsWith('.tgz'));
44
+ if (files.length === 0) {
45
+ throw new Error(`No tarball found after npm pack ${spec}`);
46
+ }
47
+
48
+ const extractDir = join(tmpDir, 'extracted');
49
+ mkdirSync(extractDir, { recursive: true });
50
+ execFileSync('tar', ['xzf', join(tmpDir, files[0]), '-C', extractDir], {
51
+ stdio: 'pipe',
52
+ timeout: 15_000,
53
+ });
54
+
55
+ // npm pack extracts to a 'package/' subdirectory
56
+ const packageDir = join(extractDir, 'package');
57
+ if (!existsSync(packageDir)) {
58
+ throw new Error(`Extracted package directory not found at ${packageDir}`);
59
+ }
60
+
61
+ return packageDir;
62
+ } catch (e) {
63
+ const msg = e instanceof Error ? e.message : String(e);
64
+ throw new Error(`Failed to resolve ${spec} from npm: ${msg}`, { cause: e });
65
+ }
66
+ }
67
+
7
68
  export function registerInstallKnowledge(program: Command): void {
8
69
  program
9
70
  .command('install-knowledge')
10
- .argument('<pack>', 'Path to knowledge bundle file or directory')
71
+ .argument('<pack>', 'Path to knowledge bundle, directory, or npm package name')
11
72
  .option('--no-facades', 'Skip facade generation for new domains')
12
73
  .description('Install knowledge packs into the agent in the current directory')
13
74
  .action(async (pack: string, opts: { facades: boolean }) => {
@@ -17,10 +78,20 @@ export function registerInstallKnowledge(program: Command): void {
17
78
  process.exit(1);
18
79
  }
19
80
 
20
- const bundlePath = resolve(pack);
21
-
22
81
  const s = p.spinner();
23
- s.start(`Installing knowledge from ${bundlePath}...`);
82
+ s.start(`Resolving knowledge pack: ${pack}...`);
83
+
84
+ let bundlePath: string;
85
+ try {
86
+ bundlePath = await resolvePackPath(pack);
87
+ } catch (err) {
88
+ s.stop('Resolution failed');
89
+ p.log.error(err instanceof Error ? err.message : String(err));
90
+ process.exit(1);
91
+ return; // unreachable, for TS
92
+ }
93
+
94
+ s.message(`Installing knowledge from ${bundlePath}...`);
24
95
 
25
96
  try {
26
97
  const result = await installKnowledge({
@@ -0,0 +1,101 @@
1
+ import type { Command } from 'commander';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import * as p from '@clack/prompts';
6
+ import { detectAgent } from '../utils/agent-context.js';
7
+
8
+ type Target = 'claude' | 'codex' | 'both';
9
+
10
+ function installClaude(agentId: string, agentDir: string): void {
11
+ const configPath = join(homedir(), '.claude.json');
12
+ let config: Record<string, unknown> = {};
13
+
14
+ if (existsSync(configPath)) {
15
+ try {
16
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
17
+ } catch {
18
+ p.log.error(`Failed to parse ${configPath}. Fix it manually or delete it to start fresh.`);
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
24
+ config.mcpServers = {};
25
+ }
26
+
27
+ (config.mcpServers as Record<string, unknown>)[agentId] = {
28
+ type: 'stdio',
29
+ command: 'node',
30
+ args: [join(agentDir, 'dist', 'index.js')],
31
+ env: {},
32
+ };
33
+
34
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
35
+ p.log.success(`Registered ${agentId} in ~/.claude.json`);
36
+ }
37
+
38
+ function installCodex(agentId: string, agentDir: string): void {
39
+ const codexDir = join(homedir(), '.codex');
40
+ const configPath = join(codexDir, 'config.toml');
41
+
42
+ if (!existsSync(codexDir)) {
43
+ mkdirSync(codexDir, { recursive: true });
44
+ }
45
+
46
+ let content = '';
47
+ if (existsSync(configPath)) {
48
+ content = readFileSync(configPath, 'utf-8');
49
+ }
50
+
51
+ // Remove existing section for this agent if present
52
+ const sectionHeader = `[mcp_servers.${agentId}]`;
53
+ const sectionRegex = new RegExp(`\\[mcp_servers\\.${escapeRegExp(agentId)}\\][^\\[]*`, 's');
54
+ content = content.replace(sectionRegex, '').trim();
55
+
56
+ const entryPoint = join(agentDir, 'dist', 'index.js');
57
+ const section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${entryPoint}"]\n`;
58
+
59
+ content = content + section;
60
+
61
+ writeFileSync(configPath, content.trim() + '\n', 'utf-8');
62
+ p.log.success(`Registered ${agentId} in ~/.codex/config.toml`);
63
+ }
64
+
65
+ function escapeRegExp(s: string): string {
66
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
+ }
68
+
69
+ export function registerInstall(program: Command): void {
70
+ program
71
+ .command('install')
72
+ .argument('[dir]', 'Agent directory (defaults to cwd)')
73
+ .option('--target <target>', 'Registration target: claude, codex, or both', 'claude')
74
+ .description('Register agent as MCP server in editor config')
75
+ .action(async (dir?: string, opts?: { target?: string }) => {
76
+ const resolvedDir = dir ? resolve(dir) : undefined;
77
+ const ctx = detectAgent(resolvedDir);
78
+
79
+ if (!ctx) {
80
+ p.log.error('Not in an agent project. Run from an agent directory or pass its path.');
81
+ process.exit(1);
82
+ }
83
+
84
+ const target = (opts?.target ?? 'claude') as Target;
85
+
86
+ if (target !== 'claude' && target !== 'codex' && target !== 'both') {
87
+ p.log.error(`Invalid target "${target}". Use: claude, codex, or both`);
88
+ process.exit(1);
89
+ }
90
+
91
+ if (target === 'claude' || target === 'both') {
92
+ installClaude(ctx.agentId, ctx.agentPath);
93
+ }
94
+
95
+ if (target === 'codex' || target === 'both') {
96
+ installCodex(ctx.agentId, ctx.agentPath);
97
+ }
98
+
99
+ p.log.info(`Agent ${ctx.agentId} is now available as an MCP server.`);
100
+ });
101
+ }