@soleri/cli 9.0.2 → 9.3.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 (49) hide show
  1. package/dist/commands/agent.js +116 -3
  2. package/dist/commands/agent.js.map +1 -1
  3. package/dist/commands/create.js +6 -2
  4. package/dist/commands/create.js.map +1 -1
  5. package/dist/commands/hooks.js +36 -13
  6. package/dist/commands/hooks.js.map +1 -1
  7. package/dist/commands/install.d.ts +1 -0
  8. package/dist/commands/install.js +61 -12
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/pack.js +0 -1
  11. package/dist/commands/pack.js.map +1 -1
  12. package/dist/commands/staging.d.ts +2 -0
  13. package/dist/commands/staging.js +175 -0
  14. package/dist/commands/staging.js.map +1 -0
  15. package/dist/hook-packs/full/manifest.json +2 -2
  16. package/dist/hook-packs/installer.d.ts +4 -11
  17. package/dist/hook-packs/installer.js +197 -23
  18. package/dist/hook-packs/installer.js.map +1 -1
  19. package/dist/hook-packs/installer.ts +223 -38
  20. package/dist/hook-packs/registry.d.ts +16 -13
  21. package/dist/hook-packs/registry.js +11 -18
  22. package/dist/hook-packs/registry.js.map +1 -1
  23. package/dist/hook-packs/registry.ts +31 -30
  24. package/dist/hook-packs/yolo-safety/manifest.json +23 -0
  25. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
  26. package/dist/hooks/templates.js +1 -1
  27. package/dist/hooks/templates.js.map +1 -1
  28. package/dist/main.js +2 -0
  29. package/dist/main.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/__tests__/create.test.ts +6 -2
  32. package/src/__tests__/hook-packs.test.ts +67 -25
  33. package/src/__tests__/wizard-e2e.mjs +153 -58
  34. package/src/commands/agent.ts +146 -3
  35. package/src/commands/create.ts +8 -2
  36. package/src/commands/hooks.ts +36 -31
  37. package/src/commands/install.ts +65 -22
  38. package/src/commands/pack.ts +0 -1
  39. package/src/commands/staging.ts +208 -0
  40. package/src/hook-packs/full/manifest.json +2 -2
  41. package/src/hook-packs/installer.ts +223 -38
  42. package/src/hook-packs/registry.ts +31 -30
  43. package/src/hook-packs/yolo-safety/manifest.json +23 -0
  44. package/src/hook-packs/yolo-safety/scripts/anti-deletion.sh +214 -0
  45. package/src/hooks/templates.ts +1 -1
  46. package/src/main.ts +2 -0
  47. package/dist/commands/cognee.d.ts +0 -10
  48. package/dist/commands/cognee.js +0 -364
  49. package/dist/commands/cognee.js.map +0 -1
@@ -35,11 +35,13 @@ function assert(cond, msg, ctx = '') {
35
35
  }
36
36
 
37
37
  function stripAnsi(s) {
38
+ /* oxlint-disable eslint(no-control-regex) -- intentional ANSI control char stripping */
38
39
  // eslint-disable-next-line no-control-regex
39
40
  return s
40
41
  .replace(new RegExp('\x1B\\[[0-9;]*[A-Za-z]', 'g'), '')
41
42
  .replace(new RegExp('\x1B\\].*?\x07', 'g'), '')
42
43
  .replace(new RegExp('\r', 'g'), '');
44
+ /* oxlint-enable eslint(no-control-regex) */
43
45
  }
44
46
 
45
47
  function sleep(ms) {
@@ -68,8 +70,12 @@ function runWizard(name, actions, opts = {}) {
68
70
  stdio: ['pipe', 'pipe', 'pipe'],
69
71
  });
70
72
 
71
- proc.stdout.on('data', (d) => { buffer += d.toString(); });
72
- proc.stderr.on('data', (d) => { buffer += d.toString(); });
73
+ proc.stdout.on('data', (d) => {
74
+ buffer += d.toString();
75
+ });
76
+ proc.stderr.on('data', (d) => {
77
+ buffer += d.toString();
78
+ });
73
79
 
74
80
  async function drive() {
75
81
  while (actionIndex < actions.length && !state.completed) {
@@ -82,6 +88,7 @@ function runWizard(name, actions, opts = {}) {
82
88
 
83
89
  if (matched) {
84
90
  actionIndex++;
91
+ // oxlint-disable-next-line eslint(no-await-in-loop)
85
92
  await sleep(a.delay || 150);
86
93
  if (!state.completed) {
87
94
  try {
@@ -89,6 +96,7 @@ function runWizard(name, actions, opts = {}) {
89
96
  } catch {}
90
97
  }
91
98
  } else {
99
+ // oxlint-disable-next-line eslint(no-await-in-loop)
92
100
  await sleep(100);
93
101
  }
94
102
  }
@@ -116,7 +124,9 @@ function runWizard(name, actions, opts = {}) {
116
124
  clearInterval(poller);
117
125
  proc.kill('SIGTERM');
118
126
  setTimeout(() => {
119
- try { proc.kill('SIGKILL'); } catch {}
127
+ try {
128
+ proc.kill('SIGKILL');
129
+ } catch {}
120
130
  resolve({
121
131
  exitCode: -1,
122
132
  output: stripAnsi(buffer) + '\n[TIMEOUT]',
@@ -153,13 +163,62 @@ function archetypeActions(outDir, { downCount = 0 } = {}) {
153
163
  // Note: agentId is slugify(label), not the archetype value.
154
164
  // e.g., "Full-Stack Assistant" → "full-stack-assistant"
155
165
  const ARCHETYPES = [
156
- { value: 'code-reviewer', agentId: 'code-reviewer', label: 'Code Reviewer', tone: 'mentor', totalSkills: 10, downCount: 0 },
157
- { value: 'security-auditor', agentId: 'security-auditor', label: 'Security Auditor', tone: 'precise', totalSkills: 10, downCount: 1 },
158
- { value: 'api-architect', agentId: 'api-architect', label: 'API Architect', tone: 'pragmatic', totalSkills: 10, downCount: 2 },
159
- { value: 'test-engineer', agentId: 'test-engineer', label: 'Test Engineer', tone: 'mentor', totalSkills: 10, downCount: 3 },
160
- { value: 'devops-pilot', agentId: 'devops-pilot', label: 'DevOps Pilot', tone: 'pragmatic', totalSkills: 10, downCount: 4 },
161
- { value: 'database-architect', agentId: 'database-architect', label: 'Database Architect', tone: 'precise', totalSkills: 10, downCount: 5 },
162
- { value: 'full-stack', agentId: 'full-stack-assistant', label: 'Full-Stack Assistant', tone: 'mentor', totalSkills: 11, downCount: 6 },
166
+ {
167
+ value: 'code-reviewer',
168
+ agentId: 'code-reviewer',
169
+ label: 'Code Reviewer',
170
+ tone: 'mentor',
171
+ totalSkills: 10,
172
+ downCount: 0,
173
+ },
174
+ {
175
+ value: 'security-auditor',
176
+ agentId: 'security-auditor',
177
+ label: 'Security Auditor',
178
+ tone: 'precise',
179
+ totalSkills: 10,
180
+ downCount: 1,
181
+ },
182
+ {
183
+ value: 'api-architect',
184
+ agentId: 'api-architect',
185
+ label: 'API Architect',
186
+ tone: 'pragmatic',
187
+ totalSkills: 10,
188
+ downCount: 2,
189
+ },
190
+ {
191
+ value: 'test-engineer',
192
+ agentId: 'test-engineer',
193
+ label: 'Test Engineer',
194
+ tone: 'mentor',
195
+ totalSkills: 10,
196
+ downCount: 3,
197
+ },
198
+ {
199
+ value: 'devops-pilot',
200
+ agentId: 'devops-pilot',
201
+ label: 'DevOps Pilot',
202
+ tone: 'pragmatic',
203
+ totalSkills: 10,
204
+ downCount: 4,
205
+ },
206
+ {
207
+ value: 'database-architect',
208
+ agentId: 'database-architect',
209
+ label: 'Database Architect',
210
+ tone: 'precise',
211
+ totalSkills: 10,
212
+ downCount: 5,
213
+ },
214
+ {
215
+ value: 'full-stack',
216
+ agentId: 'full-stack-assistant',
217
+ label: 'Full-Stack Assistant',
218
+ tone: 'mentor',
219
+ totalSkills: 11,
220
+ downCount: 6,
221
+ },
163
222
  ];
164
223
 
165
224
  // ══════════════════════════════════════════════════════════
@@ -168,43 +227,55 @@ const ARCHETYPES = [
168
227
 
169
228
  async function testCancelArchetype() {
170
229
  console.log('\n [1/14] Cancel at archetype (Ctrl+C)');
171
- const r = await runWizard('cancel-arch', [
172
- { waitFor: 'kind of agent', send: CTRL_C },
173
- ], { timeout: 15000 });
230
+ const r = await runWizard('cancel-arch', [{ waitFor: 'kind of agent', send: CTRL_C }], {
231
+ timeout: 15000,
232
+ });
174
233
  assert(r.actionsCompleted >= 1, 'prompt reached', 'cancel-archetype');
175
234
  }
176
235
 
177
236
  async function testCancelName() {
178
237
  console.log('\n [2/14] Cancel at display name');
179
- const r = await runWizard('cancel-name', [
180
- { waitFor: 'kind of agent', send: SPACE + ENTER },
181
- { waitFor: 'Display name', send: CTRL_C },
182
- ], { timeout: 15000 });
238
+ const r = await runWizard(
239
+ 'cancel-name',
240
+ [
241
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
242
+ { waitFor: 'Display name', send: CTRL_C },
243
+ ],
244
+ { timeout: 15000 },
245
+ );
183
246
  assert(r.actionsCompleted >= 2, 'reached name prompt', 'cancel-name');
184
247
  }
185
248
 
186
249
  async function testCancelRole() {
187
250
  console.log('\n [3/14] Cancel at role');
188
- const r = await runWizard('cancel-role', [
189
- { waitFor: 'kind of agent', send: SPACE + ENTER },
190
- { waitFor: 'Display name', send: ENTER },
191
- { waitFor: 'Role', send: CTRL_C },
192
- ], { timeout: 15000 });
251
+ const r = await runWizard(
252
+ 'cancel-role',
253
+ [
254
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
255
+ { waitFor: 'Display name', send: ENTER },
256
+ { waitFor: 'Role', send: CTRL_C },
257
+ ],
258
+ { timeout: 15000 },
259
+ );
193
260
  assert(r.actionsCompleted >= 3, 'reached role prompt', 'cancel-role');
194
261
  }
195
262
 
196
263
  async function testCancelSkills() {
197
264
  console.log('\n [4/14] Cancel at skills');
198
- const r = await runWizard('cancel-skills', [
199
- { waitFor: 'kind of agent', send: SPACE + ENTER },
200
- { waitFor: 'Display name', send: ENTER },
201
- { waitFor: 'Role', send: ENTER },
202
- { waitFor: 'Description', send: ENTER },
203
- { waitFor: /domain|expertise/i, send: ENTER },
204
- { waitFor: /principle|guiding/i, send: ENTER },
205
- { waitFor: /tone/i, send: ENTER },
206
- { waitFor: /skill/i, send: CTRL_C },
207
- ], { timeout: 15000 });
265
+ const r = await runWizard(
266
+ 'cancel-skills',
267
+ [
268
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
269
+ { waitFor: 'Display name', send: ENTER },
270
+ { waitFor: 'Role', send: ENTER },
271
+ { waitFor: 'Description', send: ENTER },
272
+ { waitFor: /domain|expertise/i, send: ENTER },
273
+ { waitFor: /principle|guiding/i, send: ENTER },
274
+ { waitFor: /tone/i, send: ENTER },
275
+ { waitFor: /skill/i, send: CTRL_C },
276
+ ],
277
+ { timeout: 15000 },
278
+ );
208
279
  assert(r.actionsCompleted >= 8, 'reached skills prompt', 'cancel-skills');
209
280
  }
210
281
 
@@ -217,20 +288,24 @@ async function testDeclineConfirm() {
217
288
  const outDir = join(TEST_ROOT, 'decline');
218
289
  mkdirSync(outDir, { recursive: true });
219
290
 
220
- const r = await runWizard('decline', [
221
- { waitFor: 'kind of agent', send: SPACE + ENTER },
222
- { waitFor: 'Display name', send: ENTER },
223
- { waitFor: 'Role', send: ENTER },
224
- { waitFor: 'Description', send: ENTER },
225
- { waitFor: /domain|expertise/i, send: ENTER },
226
- { waitFor: /principle|guiding/i, send: ENTER },
227
- { waitFor: /tone/i, send: ENTER },
228
- { waitFor: /skill/i, send: ENTER },
229
- { waitFor: /greeting/i, send: ENTER },
230
- { waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
231
- { waitFor: /hook|pack/i, send: ENTER },
232
- { waitFor: /create agent/i, send: LEFT + ENTER },
233
- ], { timeout: 15000 });
291
+ const r = await runWizard(
292
+ 'decline',
293
+ [
294
+ { waitFor: 'kind of agent', send: SPACE + ENTER },
295
+ { waitFor: 'Display name', send: ENTER },
296
+ { waitFor: 'Role', send: ENTER },
297
+ { waitFor: 'Description', send: ENTER },
298
+ { waitFor: /domain|expertise/i, send: ENTER },
299
+ { waitFor: /principle|guiding/i, send: ENTER },
300
+ { waitFor: /tone/i, send: ENTER },
301
+ { waitFor: /skill/i, send: ENTER },
302
+ { waitFor: /greeting/i, send: ENTER },
303
+ { waitFor: /output|directory/i, send: CTRL_U + outDir + ENTER, delay: 300 },
304
+ { waitFor: /hook|pack/i, send: ENTER },
305
+ { waitFor: /create agent/i, send: LEFT + ENTER },
306
+ ],
307
+ { timeout: 15000 },
308
+ );
234
309
 
235
310
  assert(r.actionsCompleted >= 12, `all prompts reached (${r.actionsCompleted}/12)`, 'decline');
236
311
  assert(!existsSync(join(outDir, 'code-reviewer', 'package.json')), 'no agent created', 'decline');
@@ -260,8 +335,11 @@ async function testArchetype(arch, idx) {
260
335
  const personaPath = join(ad, 'src', 'identity', 'persona.ts');
261
336
  if (existsSync(personaPath)) {
262
337
  const persona = readFileSync(personaPath, 'utf-8');
263
- assert(persona.includes(`'${arch.label}'`) || persona.includes(`"${arch.label}"`),
264
- `name = ${arch.label}`, ctx);
338
+ assert(
339
+ persona.includes(`'${arch.label}'`) || persona.includes(`"${arch.label}"`),
340
+ `name = ${arch.label}`,
341
+ ctx,
342
+ );
265
343
  assert(persona.includes(`tone: '${arch.tone}'`), `tone = ${arch.tone}`, ctx);
266
344
  } else {
267
345
  assert(false, 'persona.ts exists', ctx);
@@ -271,9 +349,21 @@ async function testArchetype(arch, idx) {
271
349
  const skillsDir = join(ad, 'skills');
272
350
  if (existsSync(skillsDir)) {
273
351
  const skills = readdirSync(skillsDir);
274
- assert(skills.length === arch.totalSkills, `${arch.totalSkills} skills (got ${skills.length})`, ctx);
352
+ assert(
353
+ skills.length === arch.totalSkills,
354
+ `${arch.totalSkills} skills (got ${skills.length})`,
355
+ ctx,
356
+ );
275
357
  // Core skills always present
276
- for (const core of ['brainstorming', 'systematic-debugging', 'verification-before-completion', 'health-check', 'context-resume', 'writing-plans', 'executing-plans']) {
358
+ for (const core of [
359
+ 'brainstorming',
360
+ 'systematic-debugging',
361
+ 'verification-before-completion',
362
+ 'health-check',
363
+ 'context-resume',
364
+ 'writing-plans',
365
+ 'executing-plans',
366
+ ]) {
277
367
  assert(skills.includes(core), `core skill: ${core}`, ctx);
278
368
  }
279
369
  } else {
@@ -310,8 +400,9 @@ async function testCustomArchetype() {
310
400
  const customName = 'GraphQL Guardian';
311
401
  const customId = 'graphql-guardian';
312
402
  const customRole = 'Validates GraphQL schemas against federation rules';
313
- const customDesc = 'This agent checks GraphQL schemas for breaking changes, naming conventions, and federation compatibility across subgraphs.';
314
- const customGreeting = "Hey! Drop your GraphQL schema and I will check it for issues.";
403
+ const customDesc =
404
+ 'This agent checks GraphQL schemas for breaking changes, naming conventions, and federation compatibility across subgraphs.';
405
+ const customGreeting = 'Hey! Drop your GraphQL schema and I will check it for issues.';
315
406
 
316
407
  const r = await runWizard('custom', [
317
408
  // Step 1: Select "✦ Create Custom" (9 downs — 9 archetypes before _custom)
@@ -422,7 +513,11 @@ async function testHookPacks() {
422
513
  // Validate hooks were installed
423
514
  const output = r.output;
424
515
  assert(output.includes('a11y') && output.includes('installed'), 'a11y pack installed', ctx);
425
- assert(output.includes('typescript-safety') && output.includes('installed'), 'typescript-safety pack installed', ctx);
516
+ assert(
517
+ output.includes('typescript-safety') && output.includes('installed'),
518
+ 'typescript-safety pack installed',
519
+ ctx,
520
+ );
426
521
 
427
522
  // Check .claude directory has hooks
428
523
  const claudeDir = join(ad, '.claude');
@@ -431,7 +526,9 @@ async function testHookPacks() {
431
526
  assert(files.length > 0, `.claude/ has hook files (${files.length})`, ctx);
432
527
  }
433
528
 
434
- console.log(` exit=${r.exitCode}, agent=${existsSync(ad)}, hooks=${r.output.includes('installed')}`);
529
+ console.log(
530
+ ` exit=${r.exitCode}, agent=${existsSync(ad)}, hooks=${r.output.includes('installed')}`,
531
+ );
435
532
  }
436
533
 
437
534
  // ══════════════════════════════════════════════════════════
@@ -455,6 +552,7 @@ await testDeclineConfirm();
455
552
 
456
553
  // All 7 archetypes (each scaffolds + builds, slower)
457
554
  for (let i = 0; i < ARCHETYPES.length; i++) {
555
+ // oxlint-disable-next-line eslint(no-await-in-loop)
458
556
  await testArchetype(ARCHETYPES[i], i);
459
557
  }
460
558
 
@@ -469,10 +567,7 @@ rmSync(TEST_ROOT, { recursive: true, force: true });
469
567
 
470
568
  // Clean up any MCP registrations
471
569
  try {
472
- const claudeJson = join(
473
- process.env.HOME || process.env.USERPROFILE || '',
474
- '.claude.json',
475
- );
570
+ const claudeJson = join(process.env.HOME || process.env.USERPROFILE || '', '.claude.json');
476
571
  if (existsSync(claudeJson)) {
477
572
  const c = JSON.parse(readFileSync(claudeJson, 'utf-8'));
478
573
  let changed = false;
@@ -5,12 +5,22 @@
5
5
  * `soleri agent update` — OTA engine upgrade with migration support.
6
6
  */
7
7
 
8
- import { join } from 'node:path';
9
- import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import {
10
+ existsSync,
11
+ readFileSync,
12
+ readdirSync,
13
+ writeFileSync,
14
+ mkdirSync,
15
+ renameSync,
16
+ cpSync,
17
+ rmSync,
18
+ } from 'node:fs';
19
+ import { homedir } from 'node:os';
10
20
  import { execFileSync } from 'node:child_process';
11
21
  import type { Command } from 'commander';
12
22
  import * as p from '@clack/prompts';
13
- import { PackLockfile, checkNpmVersion, checkVersionCompat } from '@soleri/core';
23
+ import { PackLockfile, checkNpmVersion, checkVersionCompat, SOLERI_HOME } from '@soleri/core';
14
24
  import {
15
25
  generateClaudeMdTemplate,
16
26
  generateInjectClaudeMd,
@@ -18,6 +28,7 @@ import {
18
28
  } from '@soleri/forge/lib';
19
29
  import type { AgentConfig } from '@soleri/forge/lib';
20
30
  import { detectAgent } from '../utils/agent-context.js';
31
+ import { installClaude } from './install.js';
21
32
 
22
33
  export function registerAgent(program: Command): void {
23
34
  const agent = program.command('agent').description('Agent lifecycle management');
@@ -330,6 +341,138 @@ export function registerAgent(program: Command): void {
330
341
  }
331
342
  });
332
343
 
344
+ // ─── migrate ──────────────────────────────────────────────
345
+ // Temporary command — moves agent data from ~/.{agentId}/ to ~/.soleri/{agentId}/.
346
+ // Will be removed in the next major version after all users migrate.
347
+ agent
348
+ .command('migrate')
349
+ .argument('<agentId>', 'Agent ID to migrate (e.g. ernesto, salvador)')
350
+ .option('--dry-run', 'Preview what would be moved without executing')
351
+ .description('Move agent data from ~/.{agentId}/ to ~/.soleri/{agentId}/ (one-time migration)')
352
+ .action((agentId: string, opts: { dryRun?: boolean }) => {
353
+ const legacyHome = join(homedir(), `.${agentId}`);
354
+ const newHome = join(SOLERI_HOME, agentId);
355
+
356
+ // Data files to migrate (relative to agent home)
357
+ const dataFiles = [
358
+ 'vault.db',
359
+ 'vault.db-shm',
360
+ 'vault.db-wal',
361
+ 'plans.json',
362
+ 'keys.json',
363
+ 'flags.json',
364
+ ];
365
+ const dataDirs = ['templates'];
366
+
367
+ // Check if legacy data exists
368
+ if (!existsSync(legacyHome)) {
369
+ p.log.info(`No legacy data found at ${legacyHome} — nothing to migrate.`);
370
+ return;
371
+ }
372
+
373
+ // Check if already migrated
374
+ if (existsSync(join(newHome, 'vault.db'))) {
375
+ p.log.warn(`Data already exists at ${newHome}/vault.db — migration may have already run.`);
376
+ p.log.info('If you want to force re-migration, remove the new directory first.');
377
+ return;
378
+ }
379
+
380
+ // Discover what to move
381
+ const toMove: Array<{ src: string; dst: string; type: 'file' | 'dir' }> = [];
382
+
383
+ for (const file of dataFiles) {
384
+ const src = join(legacyHome, file);
385
+ if (existsSync(src)) {
386
+ toMove.push({ src, dst: join(newHome, file), type: 'file' });
387
+ }
388
+ }
389
+
390
+ for (const dir of dataDirs) {
391
+ const src = join(legacyHome, dir);
392
+ if (existsSync(src)) {
393
+ toMove.push({ src, dst: join(newHome, dir), type: 'dir' });
394
+ }
395
+ }
396
+
397
+ if (toMove.length === 0) {
398
+ p.log.info(`No data files found in ${legacyHome} — nothing to migrate.`);
399
+ return;
400
+ }
401
+
402
+ // Preview
403
+ console.log(`\n Migration: ${legacyHome} → ${newHome}\n`);
404
+ for (const item of toMove) {
405
+ const label = item.type === 'dir' ? '(dir) ' : '';
406
+ console.log(` ${label}${item.src} → ${item.dst}`);
407
+ }
408
+ console.log('');
409
+
410
+ if (opts.dryRun) {
411
+ p.log.info(
412
+ `Dry run — ${toMove.length} items would be moved. Run without --dry-run to execute.`,
413
+ );
414
+ return;
415
+ }
416
+
417
+ // Execute migration
418
+ const s = p.spinner();
419
+ s.start('Migrating agent data...');
420
+
421
+ try {
422
+ // Create new home directory
423
+ mkdirSync(newHome, { recursive: true });
424
+
425
+ let moved = 0;
426
+ for (const item of toMove) {
427
+ mkdirSync(dirname(item.dst), { recursive: true });
428
+ try {
429
+ // Try atomic rename first (same filesystem)
430
+ renameSync(item.src, item.dst);
431
+ } catch {
432
+ // Cross-filesystem: copy then remove
433
+ if (item.type === 'dir') {
434
+ cpSync(item.src, item.dst, { recursive: true });
435
+ rmSync(item.src, { recursive: true });
436
+ } else {
437
+ cpSync(item.src, item.dst);
438
+ rmSync(item.src);
439
+ }
440
+ }
441
+ moved++;
442
+ }
443
+
444
+ s.stop(`Migrated ${moved} items to ${newHome}`);
445
+
446
+ // Detect agent definition (agent.yaml) to re-register MCP
447
+ const agentYaml = join(newHome, 'agent.yaml');
448
+ const legacyAgentYaml = join(legacyHome, 'agent.yaml');
449
+
450
+ if (existsSync(agentYaml) || existsSync(legacyAgentYaml)) {
451
+ // If agent.yaml is still in legacy dir, move it too
452
+ if (!existsSync(agentYaml) && existsSync(legacyAgentYaml)) {
453
+ p.log.info(
454
+ 'Note: agent.yaml is still at the old location. Move the entire agent folder if needed.',
455
+ );
456
+ }
457
+ }
458
+
459
+ // Re-register MCP pointing to new location
460
+ const agentDir = existsSync(agentYaml) ? newHome : legacyHome;
461
+ if (existsSync(join(agentDir, 'agent.yaml'))) {
462
+ installClaude(agentId, agentDir, true);
463
+ p.log.success('MCP registration updated to new path.');
464
+ }
465
+
466
+ p.log.info(
467
+ `Legacy directory preserved at ${legacyHome} (safe to remove manually after verifying).`,
468
+ );
469
+ } catch (err) {
470
+ s.stop('Migration failed');
471
+ p.log.error(err instanceof Error ? err.message : String(err));
472
+ process.exit(1);
473
+ }
474
+ });
475
+
333
476
  // ─── validate ──────────────────────────────────────────────
334
477
  agent
335
478
  .command('validate')
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
- import { resolve } from 'node:path';
2
+ import { resolve, join } from 'node:path';
3
+ import { homedir } from 'node:os';
3
4
  import type { Command } from 'commander';
4
5
  import * as p from '@clack/prompts';
5
6
  import {
@@ -14,6 +15,9 @@ import { runCreateWizard } from '../prompts/create-wizard.js';
14
15
  import { listPacks } from '../hook-packs/registry.js';
15
16
  import { installPack } from '../hook-packs/installer.js';
16
17
 
18
+ /** Default parent directory for new agents: ~/.soleri/ */
19
+ const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
20
+
17
21
  function parseSetupTarget(value?: string): SetupTarget | undefined {
18
22
  if (!value) return undefined;
19
23
  if ((SETUP_TARGETS as readonly string[]).includes(value)) {
@@ -37,6 +41,7 @@ export function registerCreate(program: Command): void {
37
41
  `Setup target: ${SETUP_TARGETS.join(', ')} (default: claude)`,
38
42
  )
39
43
  .option('-y, --yes', 'Skip confirmation prompts (use with --config for fully non-interactive)')
44
+ .option('--dir <path>', `Parent directory for the agent (default: ~/.soleri/)`)
40
45
  .option('--filetree', 'Create a file-tree agent (v7 — no TypeScript, no build step)')
41
46
  .option('--legacy', 'Create a legacy TypeScript agent (v6 — requires npm install + build)')
42
47
  .description('Create a new Soleri agent')
@@ -46,6 +51,7 @@ export function registerCreate(program: Command): void {
46
51
  opts?: {
47
52
  config?: string;
48
53
  yes?: boolean;
54
+ dir?: string;
49
55
  setupTarget?: string;
50
56
  filetree?: boolean;
51
57
  legacy?: boolean;
@@ -148,7 +154,7 @@ export function registerCreate(program: Command): void {
148
154
  })),
149
155
  };
150
156
 
151
- const outputDir = config.outputDir ?? process.cwd();
157
+ const outputDir = opts?.dir ? resolve(opts.dir) : (config.outputDir ?? SOLERI_HOME);
152
158
  const nonInteractive = !!(opts?.yes || opts?.config);
153
159
 
154
160
  if (!nonInteractive) {