@soleri/cli 1.8.0 → 1.10.0

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 (57) 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 +30 -4
  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 -32
  31. package/dist/prompts/archetypes.js.map +1 -1
  32. package/dist/prompts/create-wizard.js +105 -60
  33. package/dist/prompts/create-wizard.js.map +1 -1
  34. package/dist/prompts/playbook.d.ts +8 -7
  35. package/dist/prompts/playbook.js +312 -30
  36. package/dist/prompts/playbook.js.map +1 -1
  37. package/dist/utils/checks.d.ts +0 -1
  38. package/dist/utils/checks.js +1 -1
  39. package/dist/utils/checks.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/archetypes.test.ts +84 -0
  42. package/src/__tests__/doctor.test.ts +2 -2
  43. package/src/__tests__/wizard-e2e.mjs +508 -0
  44. package/src/commands/agent.ts +181 -0
  45. package/src/commands/create.ts +146 -104
  46. package/src/commands/install-knowledge.ts +75 -4
  47. package/src/commands/install.ts +101 -0
  48. package/src/commands/pack.ts +585 -0
  49. package/src/commands/skills.ts +191 -0
  50. package/src/commands/uninstall.ts +93 -0
  51. package/src/hook-packs/installer.ts +1 -18
  52. package/src/hook-packs/registry.ts +1 -1
  53. package/src/main.ts +42 -1
  54. package/src/prompts/archetypes.ts +193 -62
  55. package/src/prompts/create-wizard.ts +114 -58
  56. package/src/prompts/playbook.ts +207 -21
  57. package/src/utils/checks.ts +1 -1
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Agent lifecycle CLI — status, update, diff.
3
+ *
4
+ * `soleri agent status` — health report with version, packs, vault stats.
5
+ * `soleri agent update` — OTA engine upgrade with migration support.
6
+ */
7
+
8
+ import { join } from 'node:path';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { execFileSync } from 'node:child_process';
11
+ import type { Command } from 'commander';
12
+ import * as p from '@clack/prompts';
13
+ import { PackLockfile, checkNpmVersion, checkVersionCompat } from '@soleri/core';
14
+ import { detectAgent } from '../utils/agent-context.js';
15
+
16
+ export function registerAgent(program: Command): void {
17
+ const agent = program.command('agent').description('Agent lifecycle management');
18
+
19
+ // ─── status ─────────────────────────────────────────────────
20
+ agent
21
+ .command('status')
22
+ .option('--json', 'Output as JSON')
23
+ .description('Show agent health: version, packs, vault, and update availability')
24
+ .action((opts: { json?: boolean }) => {
25
+ const ctx = detectAgent();
26
+ if (!ctx) {
27
+ p.log.error('No agent project detected in current directory.');
28
+ process.exit(1);
29
+ return;
30
+ }
31
+
32
+ // Read agent package.json
33
+ const pkgPath = join(ctx.agentPath, 'package.json');
34
+ const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf-8')) : {};
35
+ const agentName = pkg.name || 'unknown';
36
+ const agentVersion = pkg.version || '0.0.0';
37
+
38
+ // Read @soleri/core version
39
+ const corePkgPath = join(ctx.agentPath, 'node_modules', '@soleri', 'core', 'package.json');
40
+ const coreVersion = existsSync(corePkgPath)
41
+ ? JSON.parse(readFileSync(corePkgPath, 'utf-8')).version || 'unknown'
42
+ : pkg.dependencies?.['@soleri/core'] || 'not installed';
43
+
44
+ // Check for core update
45
+ const latestCore = checkNpmVersion('@soleri/core');
46
+
47
+ // Read lockfile
48
+ const lockfilePath = join(ctx.agentPath, 'soleri.lock');
49
+ const lockfile = new PackLockfile(lockfilePath);
50
+ const packs = lockfile.list();
51
+
52
+ // Count vault entries if db exists
53
+ const dbPath = join(ctx.agentPath, 'data', 'vault.db');
54
+ const hasVault = existsSync(dbPath);
55
+
56
+ if (opts.json) {
57
+ console.log(
58
+ JSON.stringify(
59
+ {
60
+ agent: agentName,
61
+ version: agentVersion,
62
+ engine: coreVersion,
63
+ engineLatest: latestCore,
64
+ packs: packs.map((p) => ({
65
+ id: p.id,
66
+ version: p.version,
67
+ type: p.type,
68
+ source: p.source,
69
+ })),
70
+ vault: { exists: hasVault },
71
+ },
72
+ null,
73
+ 2,
74
+ ),
75
+ );
76
+ return;
77
+ }
78
+
79
+ console.log(`\n Agent: ${agentName} v${agentVersion}`);
80
+ console.log(
81
+ ` Engine: @soleri/core ${coreVersion}${latestCore && latestCore !== coreVersion ? ` (update available: ${latestCore})` : ''}`,
82
+ );
83
+
84
+ if (packs.length > 0) {
85
+ console.log(`\n Packs (${packs.length} installed):`);
86
+ for (const pack of packs) {
87
+ const badge =
88
+ pack.source === 'npm' ? ' [npm]' : pack.source === 'built-in' ? ' [built-in]' : '';
89
+ console.log(` ${pack.id}@${pack.version} ${pack.type}${badge}`);
90
+ }
91
+ } else {
92
+ console.log('\n Packs: none installed');
93
+ }
94
+
95
+ console.log(`\n Vault: ${hasVault ? 'initialized' : 'not initialized'}`);
96
+ console.log('');
97
+ });
98
+
99
+ // ─── update ─────────────────────────────────────────────────
100
+ agent
101
+ .command('update')
102
+ .option('--check', 'Show what would change without updating')
103
+ .option('--dry-run', 'Preview migration steps')
104
+ .description('Update agent engine to latest compatible version')
105
+ .action((opts: { check?: boolean; dryRun?: boolean }) => {
106
+ const ctx = detectAgent();
107
+ if (!ctx) {
108
+ p.log.error('No agent project detected in current directory.');
109
+ process.exit(1);
110
+ return;
111
+ }
112
+
113
+ const pkgPath = join(ctx.agentPath, 'package.json');
114
+ if (!existsSync(pkgPath)) {
115
+ p.log.error('No package.json found in agent directory.');
116
+ process.exit(1);
117
+ return;
118
+ }
119
+
120
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
121
+ const currentRange = pkg.dependencies?.['@soleri/core'] || '';
122
+ const latestCore = checkNpmVersion('@soleri/core');
123
+
124
+ if (!latestCore) {
125
+ p.log.error('Could not check npm for latest @soleri/core version.');
126
+ process.exit(1);
127
+ return;
128
+ }
129
+
130
+ // Check compatibility
131
+ const compatible = checkVersionCompat(latestCore, currentRange);
132
+
133
+ if (opts.check) {
134
+ console.log(`\n Current: @soleri/core ${currentRange}`);
135
+ console.log(` Latest: @soleri/core ${latestCore}`);
136
+ console.log(` Compatible: ${compatible ? 'yes' : 'no (range: ' + currentRange + ')'}`);
137
+ console.log('');
138
+ return;
139
+ }
140
+
141
+ if (opts.dryRun) {
142
+ p.log.info(`Would update @soleri/core to ${latestCore}`);
143
+ p.log.info('Would run: npm install @soleri/core@' + latestCore);
144
+ return;
145
+ }
146
+
147
+ const s = p.spinner();
148
+ s.start(`Updating @soleri/core to ${latestCore}...`);
149
+
150
+ try {
151
+ execFileSync('npm', ['install', `@soleri/core@${latestCore}`], {
152
+ cwd: ctx.agentPath,
153
+ stdio: 'pipe',
154
+ timeout: 120_000,
155
+ });
156
+
157
+ s.stop(`Updated to @soleri/core@${latestCore}`);
158
+ p.log.info('Run `soleri test` to verify the update.');
159
+ } catch (err) {
160
+ s.stop('Update failed');
161
+ p.log.error(err instanceof Error ? err.message : String(err));
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ // ─── diff ───────────────────────────────────────────────────
167
+ agent
168
+ .command('diff')
169
+ .description('Show drift between agent templates and latest engine templates')
170
+ .action(() => {
171
+ const ctx = detectAgent();
172
+ if (!ctx) {
173
+ p.log.error('No agent project detected in current directory.');
174
+ process.exit(1);
175
+ return;
176
+ }
177
+
178
+ p.log.info('Template diff is available after `soleri agent update --check`.');
179
+ p.log.info('Full template comparison will be added in a future release.');
180
+ });
181
+ }
@@ -2,137 +2,179 @@ 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
+ config = await runCreateWizard(name);
62
+ if (!config) {
63
+ p.outro('Cancelled.');
64
+ return;
65
+ }
27
66
  }
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}`);
67
+
68
+ const setupTarget = parseSetupTarget(opts?.setupTarget);
69
+ if (opts?.setupTarget && !setupTarget) {
70
+ p.log.error(
71
+ `Invalid --setup-target "${opts.setupTarget}". Expected one of: ${SETUP_TARGETS.join(', ')}`,
72
+ );
32
73
  process.exit(1);
33
74
  }
34
- config = parsed.data;
35
- } else {
36
- // Interactive wizard
37
- config = await runCreateWizard(name);
38
- if (!config) {
39
- p.outro('Cancelled.');
40
- return;
75
+ if (setupTarget) {
76
+ config = { ...config, setupTarget };
41
77
  }
42
- }
78
+ const claudeSetup = includesClaudeSetup(config.setupTarget);
79
+
80
+ const nonInteractive = !!(opts?.yes || opts?.config);
43
81
 
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
- );
82
+ // Hook packs from config file or interactive prompt
83
+ let selectedPacks: string[] = [];
84
+ if (!claudeSetup && config.hookPacks && config.hookPacks.length > 0) {
85
+ p.log.warn(
86
+ 'Hook packs are Claude-only. Skipping because setup target excludes Claude.',
87
+ );
88
+ } else if (config.hookPacks && config.hookPacks.length > 0) {
89
+ selectedPacks = config.hookPacks;
90
+
91
+ // Validate pack names against registry — warn and skip unknown
92
+ const available = listPacks().map((pk) => pk.name);
93
+ const unknown = selectedPacks.filter((pk) => !available.includes(pk));
94
+ if (unknown.length > 0) {
95
+ for (const name of unknown) {
96
+ p.log.warn(
97
+ `Unknown hook pack "${name}" — skipping. Available: ${available.join(', ')}`,
98
+ );
99
+ }
100
+ selectedPacks = selectedPacks.filter((pk) => available.includes(pk));
101
+ }
102
+ } else if (!nonInteractive && claudeSetup) {
103
+ const packs = listPacks();
104
+ const packChoices = packs.map((pk) => ({
105
+ value: pk.name,
106
+ label: pk.name,
107
+ hint: `${pk.description} (${pk.hooks.length} hooks)`,
108
+ }));
109
+
110
+ const chosen = await p.multiselect({
111
+ message: 'Install hook packs? (Claude quality gates for ~/.claude/)',
112
+ options: packChoices,
113
+ required: false,
114
+ });
115
+
116
+ if (!p.isCancel(chosen)) {
117
+ selectedPacks = chosen as string[];
59
118
  }
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
119
  }
79
- }
80
120
 
81
- // Preview
82
- const preview = previewScaffold(config);
121
+ // Preview
122
+ const preview = previewScaffold(config);
83
123
 
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
- }
124
+ p.log.info(`Will create ${preview.files.length} files in ${preview.agentDir}`);
125
+ p.log.info(`Facades: ${preview.facades.map((f) => f.name).join(', ')}`);
126
+ p.log.info(`Domains: ${preview.domains.join(', ')}`);
127
+ p.log.info(`Setup target: ${config.setupTarget ?? 'claude'}`);
128
+ if (config.tone) {
129
+ p.log.info(`Tone: ${config.tone}`);
130
+ }
131
+ if (config.skills?.length) {
132
+ p.log.info(`Skills: ${config.skills.length} selected`);
133
+ }
134
+ if (selectedPacks.length > 0) {
135
+ p.log.info(`Hook packs: ${selectedPacks.join(', ')}`);
136
+ }
96
137
 
97
- if (!nonInteractive) {
98
- const confirmed = await p.confirm({ message: 'Create agent?' });
99
- if (p.isCancel(confirmed) || !confirmed) {
100
- p.outro('Cancelled.');
101
- return;
138
+ if (!nonInteractive) {
139
+ const confirmed = await p.confirm({ message: 'Create agent?' });
140
+ if (p.isCancel(confirmed) || !confirmed) {
141
+ p.outro('Cancelled.');
142
+ return;
143
+ }
102
144
  }
103
- }
104
145
 
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');
146
+ // Scaffold + auto-build
147
+ const s = p.spinner();
148
+ s.start('Scaffolding and building agent...');
149
+ const result = scaffold(config);
150
+ s.stop(result.success ? 'Agent created and built!' : 'Scaffolding failed');
110
151
 
111
- if (!result.success) {
112
- p.log.error(result.summary);
113
- process.exit(1);
114
- }
152
+ if (!result.success) {
153
+ p.log.error(result.summary);
154
+ process.exit(1);
155
+ }
115
156
 
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`);
157
+ // Install selected hook packs
158
+ if (selectedPacks.length > 0) {
159
+ for (const packName of selectedPacks) {
160
+ const { installed } = installPack(packName, { projectDir: result.agentDir });
161
+ if (installed.length > 0) {
162
+ p.log.success(`Hook pack "${packName}" installed (${installed.length} hooks)`);
163
+ } else {
164
+ p.log.info(`Hook pack "${packName}" — all hooks already present`);
165
+ }
124
166
  }
125
167
  }
126
- }
127
168
 
128
- if (result.success) {
129
- p.note(result.summary, 'Next steps');
130
- }
169
+ if (result.success) {
170
+ p.note(result.summary, 'Next steps');
171
+ }
131
172
 
132
- p.outro('Done!');
133
- } catch (err) {
134
- p.log.error(err instanceof Error ? err.message : String(err));
135
- process.exit(1);
136
- }
137
- });
173
+ p.outro('Done!');
174
+ } catch (err) {
175
+ p.log.error(err instanceof Error ? err.message : String(err));
176
+ process.exit(1);
177
+ }
178
+ },
179
+ );
138
180
  }
@@ -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
+ }