@soleri/cli 1.9.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.
- package/README.md +4 -0
- package/dist/commands/agent.d.ts +8 -0
- package/dist/commands/agent.js +150 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/create.js +30 -4
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/install-knowledge.js +65 -3
- package/dist/commands/install-knowledge.js.map +1 -1
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +80 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/pack.d.ts +10 -0
- package/dist/commands/pack.js +512 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +167 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +74 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/hook-packs/installer.d.ts +0 -7
- package/dist/hook-packs/installer.js +1 -14
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +1 -18
- package/dist/hook-packs/registry.d.ts +2 -1
- package/dist/hook-packs/registry.ts +1 -1
- package/dist/main.js +40 -1
- package/dist/main.js.map +1 -1
- package/dist/prompts/archetypes.d.ts +1 -0
- package/dist/prompts/archetypes.js +177 -62
- package/dist/prompts/archetypes.js.map +1 -1
- package/dist/prompts/create-wizard.js +98 -49
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/prompts/playbook.d.ts +8 -7
- package/dist/prompts/playbook.js +201 -15
- package/dist/prompts/playbook.js.map +1 -1
- package/dist/utils/checks.d.ts +0 -1
- package/dist/utils/checks.js +1 -1
- package/dist/utils/checks.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/archetypes.test.ts +84 -0
- package/src/__tests__/doctor.test.ts +2 -2
- package/src/__tests__/wizard-e2e.mjs +508 -0
- package/src/commands/agent.ts +181 -0
- package/src/commands/create.ts +146 -104
- package/src/commands/install-knowledge.ts +75 -4
- package/src/commands/install.ts +101 -0
- package/src/commands/pack.ts +585 -0
- package/src/commands/skills.ts +191 -0
- package/src/commands/uninstall.ts +93 -0
- package/src/hook-packs/installer.ts +1 -18
- package/src/hook-packs/registry.ts +1 -1
- package/src/main.ts +42 -1
- package/src/prompts/archetypes.ts +193 -62
- package/src/prompts/create-wizard.ts +114 -58
- package/src/prompts/playbook.ts +207 -21
- package/src/utils/checks.ts +1 -1
- package/code-reviewer/.claude/hookify.focus-ring-required.local.md +0 -21
- package/code-reviewer/.claude/hookify.no-ai-attribution.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-any-types.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-console-log.local.md +0 -21
- package/code-reviewer/.claude/hookify.no-important.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-inline-styles.local.md +0 -21
- package/code-reviewer/.claude/hookify.semantic-html.local.md +0 -18
- package/code-reviewer/.claude/hookify.ux-touch-targets.local.md +0 -18
- package/code-reviewer/.mcp.json +0 -11
- package/code-reviewer/README.md +0 -346
- package/code-reviewer/package-lock.json +0 -4484
- package/code-reviewer/package.json +0 -45
- package/code-reviewer/scripts/copy-assets.js +0 -15
- package/code-reviewer/scripts/setup.sh +0 -130
- package/code-reviewer/skills/brainstorming/SKILL.md +0 -170
- package/code-reviewer/skills/code-patrol/SKILL.md +0 -176
- package/code-reviewer/skills/context-resume/SKILL.md +0 -143
- package/code-reviewer/skills/executing-plans/SKILL.md +0 -201
- package/code-reviewer/skills/fix-and-learn/SKILL.md +0 -164
- package/code-reviewer/skills/health-check/SKILL.md +0 -225
- package/code-reviewer/skills/second-opinion/SKILL.md +0 -142
- package/code-reviewer/skills/systematic-debugging/SKILL.md +0 -230
- package/code-reviewer/skills/verification-before-completion/SKILL.md +0 -170
- package/code-reviewer/skills/writing-plans/SKILL.md +0 -207
- package/code-reviewer/src/__tests__/facades.test.ts +0 -598
- package/code-reviewer/src/activation/activate.ts +0 -125
- package/code-reviewer/src/activation/claude-md-content.ts +0 -217
- package/code-reviewer/src/activation/inject-claude-md.ts +0 -113
- package/code-reviewer/src/extensions/index.ts +0 -47
- package/code-reviewer/src/extensions/ops/example.ts +0 -28
- package/code-reviewer/src/identity/persona.ts +0 -62
- package/code-reviewer/src/index.ts +0 -278
- package/code-reviewer/src/intelligence/data/architecture.json +0 -5
- package/code-reviewer/src/intelligence/data/code-review.json +0 -5
- package/code-reviewer/tsconfig.json +0 -30
- package/code-reviewer/vitest.config.ts +0 -23
package/src/commands/create.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
31
|
-
p.log.error(
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
82
|
-
|
|
121
|
+
// Preview
|
|
122
|
+
const preview = previewScaffold(config);
|
|
83
123
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
if (!result.success) {
|
|
153
|
+
p.log.error(result.summary);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
115
156
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
if (result.success) {
|
|
170
|
+
p.note(result.summary, 'Next steps');
|
|
171
|
+
}
|
|
131
172
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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(`
|
|
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
|
+
}
|