@soleri/cli 1.12.5 → 7.0.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 (38) hide show
  1. package/dist/commands/add-pack.d.ts +2 -0
  2. package/dist/commands/add-pack.js +154 -0
  3. package/dist/commands/add-pack.js.map +1 -0
  4. package/dist/commands/agent.js +151 -2
  5. package/dist/commands/agent.js.map +1 -1
  6. package/dist/commands/cognee.d.ts +10 -0
  7. package/dist/commands/cognee.js +364 -0
  8. package/dist/commands/cognee.js.map +1 -0
  9. package/dist/commands/create.js +63 -5
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/dev.js +104 -17
  12. package/dist/commands/dev.js.map +1 -1
  13. package/dist/commands/install.js +70 -18
  14. package/dist/commands/install.js.map +1 -1
  15. package/dist/commands/telegram.d.ts +10 -0
  16. package/dist/commands/telegram.js +423 -0
  17. package/dist/commands/telegram.js.map +1 -0
  18. package/dist/commands/uninstall.js +35 -6
  19. package/dist/commands/uninstall.js.map +1 -1
  20. package/dist/main.js +6 -0
  21. package/dist/main.js.map +1 -1
  22. package/dist/prompts/create-wizard.js +87 -6
  23. package/dist/prompts/create-wizard.js.map +1 -1
  24. package/dist/utils/agent-context.d.ts +9 -2
  25. package/dist/utils/agent-context.js +32 -0
  26. package/dist/utils/agent-context.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/commands/add-pack.ts +170 -0
  29. package/src/commands/agent.ts +174 -3
  30. package/src/commands/cognee.ts +416 -0
  31. package/src/commands/create.ts +90 -6
  32. package/src/commands/dev.ts +114 -18
  33. package/src/commands/install.ts +78 -19
  34. package/src/commands/telegram.ts +488 -0
  35. package/src/commands/uninstall.ts +41 -7
  36. package/src/main.ts +6 -0
  37. package/src/prompts/create-wizard.ts +93 -7
  38. package/src/utils/agent-context.ts +39 -2
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Cognee vector search management — enable, disable, setup, status.
3
+ *
4
+ * `soleri cognee enable` — Wire Cognee into the agent runtime
5
+ * `soleri cognee disable` — Remove Cognee integration from the agent
6
+ * `soleri cognee setup` — Interactive config wizard (base URL, embedding, auth)
7
+ * `soleri cognee status` — Check Cognee configuration and sidecar health
8
+ */
9
+
10
+ import { join } from 'node:path';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import type { Command } from 'commander';
14
+ import * as p from '@clack/prompts';
15
+ import type { AgentConfig } from '@soleri/forge/lib';
16
+ import { generateEntryPoint } from '@soleri/forge/lib';
17
+ import { detectAgent } from '../utils/agent-context.js';
18
+
19
+ // Docker Compose file name (copied into agent project on enable)
20
+ const COGNEE_COMPOSE_FILE = 'docker-compose.cognee.yml';
21
+
22
+ // npm scripts added on enable
23
+ const COGNEE_SCRIPTS: Record<string, string> = {
24
+ 'cognee:up': `docker compose -f ${COGNEE_COMPOSE_FILE} up -d`,
25
+ 'cognee:down': `docker compose -f ${COGNEE_COMPOSE_FILE} down`,
26
+ 'cognee:logs': `docker compose -f ${COGNEE_COMPOSE_FILE} logs -f cognee`,
27
+ };
28
+
29
+ // ─── Registration ───────────────────────────────────────────────────
30
+
31
+ export function registerCognee(program: Command): void {
32
+ const cmd = program
33
+ .command('cognee')
34
+ .description('Manage Cognee vector search integration for the current agent');
35
+
36
+ // ─── enable ─────────────────────────────────────────────────────
37
+ cmd
38
+ .command('enable')
39
+ .description('Enable Cognee vector search for the current agent')
40
+ .action(() => {
41
+ const ctx = detectAgent();
42
+ if (!ctx) {
43
+ p.log.error('No agent project detected in current directory.');
44
+ process.exit(1);
45
+ return;
46
+ }
47
+
48
+ // Check if already enabled
49
+ const pkgPath = join(ctx.agentPath, 'package.json');
50
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
51
+ if (pkg.soleri?.cognee === true) {
52
+ p.log.warn('Cognee is already enabled for this agent.');
53
+ p.log.info('Run `soleri cognee setup` to configure it.');
54
+ return;
55
+ }
56
+
57
+ // Reconstruct AgentConfig and regenerate entry point with cognee: true
58
+ const config = readAgentConfig(ctx.agentPath, ctx.agentId);
59
+ if (!config) {
60
+ p.log.error('Could not read agent config from persona.ts and entry point.');
61
+ process.exit(1);
62
+ return;
63
+ }
64
+
65
+ const s = p.spinner();
66
+
67
+ // 1. Regenerate entry point with cognee: true
68
+ s.start('Regenerating entry point with Cognee integration...');
69
+ const entryPointCode = generateEntryPoint({ ...config, cognee: true });
70
+ writeFileSync(join(ctx.agentPath, 'src', 'index.ts'), entryPointCode, 'utf-8');
71
+ s.stop('Entry point regenerated with Cognee integration');
72
+
73
+ // 2. Copy docker-compose.cognee.yml if available
74
+ const sourceCompose = join(ctx.agentPath, '..', '..', 'docker', COGNEE_COMPOSE_FILE);
75
+ const targetCompose = join(ctx.agentPath, COGNEE_COMPOSE_FILE);
76
+ if (existsSync(sourceCompose) && !existsSync(targetCompose)) {
77
+ copyFileSync(sourceCompose, targetCompose);
78
+ p.log.info(`Copied ${COGNEE_COMPOSE_FILE} to agent project`);
79
+ } else if (!existsSync(targetCompose)) {
80
+ p.log.warn(`${COGNEE_COMPOSE_FILE} not found — create one manually or run Cognee externally`);
81
+ }
82
+
83
+ // 3. Update package.json
84
+ let pkgChanged = false;
85
+ if (!pkg.soleri) pkg.soleri = {};
86
+ pkg.soleri.cognee = true;
87
+ pkgChanged = true;
88
+
89
+ if (!pkg.scripts) pkg.scripts = {};
90
+ for (const [name, cmd] of Object.entries(COGNEE_SCRIPTS)) {
91
+ if (!pkg.scripts[name]) {
92
+ pkg.scripts[name] = cmd;
93
+ pkgChanged = true;
94
+ }
95
+ }
96
+
97
+ if (pkgChanged) {
98
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
99
+ p.log.info('Updated package.json with cognee flag and scripts');
100
+ }
101
+
102
+ p.log.success('Cognee enabled!');
103
+ p.log.info('Next steps:');
104
+ p.log.info(' 1. Run `soleri cognee setup` to configure base URL and auth');
105
+ p.log.info(' 2. Run `npm run cognee:up` to start the Cognee sidecar');
106
+ p.log.info(' 3. Rebuild: `npm run build`');
107
+ });
108
+
109
+ // ─── setup ──────────────────────────────────────────────────────
110
+ cmd
111
+ .command('setup')
112
+ .description('Interactive Cognee configuration wizard')
113
+ .action(async () => {
114
+ const ctx = detectAgent();
115
+ if (!ctx) {
116
+ p.log.error('No agent project detected in current directory.');
117
+ process.exit(1);
118
+ return;
119
+ }
120
+
121
+ // Check if enabled
122
+ const pkgPath = join(ctx.agentPath, 'package.json');
123
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
124
+ if (!pkg.soleri?.cognee) {
125
+ p.log.error('Cognee is not enabled. Run `soleri cognee enable` first.');
126
+ process.exit(1);
127
+ return;
128
+ }
129
+
130
+ p.intro(`Cognee Setup for ${ctx.agentId}`);
131
+
132
+ // Step 1: Base URL
133
+ p.log.step('Step 1: Cognee API endpoint');
134
+ const baseUrl = await p.text({
135
+ message: 'Cognee API base URL:',
136
+ placeholder: 'http://localhost:8000',
137
+ defaultValue: 'http://localhost:8000',
138
+ });
139
+ if (p.isCancel(baseUrl)) {
140
+ p.cancel('Setup cancelled.');
141
+ process.exit(0);
142
+ }
143
+
144
+ // Step 2: Embedding provider
145
+ p.log.step('Step 2: Embedding provider');
146
+ const embeddingProvider = await p.select({
147
+ message: 'Which embedding provider?',
148
+ options: [
149
+ { value: 'ollama', label: 'Ollama (local, free — nomic-embed-text)' },
150
+ { value: 'openai', label: 'OpenAI (text-embedding-3-small)' },
151
+ { value: 'env', label: 'Use environment variables (skip)' },
152
+ ],
153
+ });
154
+ if (p.isCancel(embeddingProvider)) {
155
+ p.cancel('Setup cancelled.');
156
+ process.exit(0);
157
+ }
158
+
159
+ // Step 3: API token (optional)
160
+ p.log.step('Step 3: Authentication (optional)');
161
+ p.log.message(' Leave empty for local Cognee (AUTH_REQUIRED=false)');
162
+ const apiToken = await p.text({
163
+ message: 'Cognee API token:',
164
+ placeholder: '(empty for local, no auth)',
165
+ defaultValue: '',
166
+ });
167
+ if (p.isCancel(apiToken)) {
168
+ p.cancel('Setup cancelled.');
169
+ process.exit(0);
170
+ }
171
+
172
+ // Step 4: Dataset name
173
+ p.log.step('Step 4: Dataset');
174
+ const dataset = await p.text({
175
+ message: 'Dataset name for this agent:',
176
+ placeholder: ctx.agentId,
177
+ defaultValue: ctx.agentId,
178
+ });
179
+ if (p.isCancel(dataset)) {
180
+ p.cancel('Setup cancelled.');
181
+ process.exit(0);
182
+ }
183
+
184
+ // Save config
185
+ const configDir = join(homedir(), `.${ctx.agentId}`);
186
+ mkdirSync(configDir, { recursive: true });
187
+ const configPath = join(configDir, 'cognee.json');
188
+
189
+ const cogneeConfig: Record<string, unknown> = {
190
+ baseUrl: (baseUrl as string).trim(),
191
+ embeddingProvider: embeddingProvider === 'env' ? 'ollama' : embeddingProvider,
192
+ ...(apiToken ? { apiToken } : {}),
193
+ dataset: (dataset as string).trim(),
194
+ };
195
+
196
+ writeFileSync(configPath, JSON.stringify(cogneeConfig, null, 2) + '\n', 'utf-8');
197
+
198
+ p.outro(`Configuration saved to ${configPath}`);
199
+
200
+ console.log('');
201
+ p.log.info(' Start Cognee: npm run cognee:up');
202
+ p.log.info(' Check status: soleri cognee status');
203
+ console.log('');
204
+ });
205
+
206
+ // ─── disable ────────────────────────────────────────────────────
207
+ cmd
208
+ .command('disable')
209
+ .description('Remove Cognee vector search from the current agent')
210
+ .action(async () => {
211
+ const ctx = detectAgent();
212
+ if (!ctx) {
213
+ p.log.error('No agent project detected in current directory.');
214
+ process.exit(1);
215
+ return;
216
+ }
217
+
218
+ const pkgPath = join(ctx.agentPath, 'package.json');
219
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
220
+ if (!pkg.soleri?.cognee) {
221
+ p.log.warn('Cognee is not enabled for this agent.');
222
+ return;
223
+ }
224
+
225
+ const confirmed = await p.confirm({
226
+ message: `Disable Cognee vector search for ${ctx.agentId}? (Vault FTS5 search continues to work)`,
227
+ });
228
+ if (p.isCancel(confirmed) || !confirmed) {
229
+ p.cancel('Cancelled.');
230
+ return;
231
+ }
232
+
233
+ // 1. Regenerate entry point without cognee
234
+ const config = readAgentConfig(ctx.agentPath, ctx.agentId);
235
+ if (config) {
236
+ const s = p.spinner();
237
+ s.start('Regenerating entry point without Cognee...');
238
+ const entryPointCode = generateEntryPoint({ ...config, cognee: false });
239
+ writeFileSync(join(ctx.agentPath, 'src', 'index.ts'), entryPointCode, 'utf-8');
240
+ s.stop('Entry point regenerated without Cognee');
241
+ }
242
+
243
+ // 2. Remove docker-compose
244
+ const composePath = join(ctx.agentPath, COGNEE_COMPOSE_FILE);
245
+ if (existsSync(composePath)) {
246
+ unlinkSync(composePath);
247
+ p.log.info('Removed docker-compose.cognee.yml');
248
+ }
249
+
250
+ // 3. Update package.json
251
+ let changed = false;
252
+ if (pkg.soleri?.cognee) {
253
+ pkg.soleri.cognee = false;
254
+ changed = true;
255
+ }
256
+ for (const name of Object.keys(COGNEE_SCRIPTS)) {
257
+ if (pkg.scripts?.[name]) {
258
+ delete pkg.scripts[name];
259
+ changed = true;
260
+ }
261
+ }
262
+ if (changed) {
263
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
264
+ p.log.info('Removed cognee scripts from package.json');
265
+ }
266
+
267
+ p.log.success('Cognee disabled. Vault FTS5 search still works.');
268
+ p.log.info('Run `npm run build` to rebuild.');
269
+ });
270
+
271
+ // ─── status ─────────────────────────────────────────────────────
272
+ cmd
273
+ .command('status')
274
+ .description('Check Cognee configuration and sidecar health')
275
+ .action(async () => {
276
+ const ctx = detectAgent();
277
+ if (!ctx) {
278
+ p.log.error('No agent project detected in current directory.');
279
+ process.exit(1);
280
+ return;
281
+ }
282
+
283
+ const pkgPath = join(ctx.agentPath, 'package.json');
284
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
285
+ const enabled = pkg.soleri?.cognee === true;
286
+
287
+ console.log(`\n Agent: ${ctx.agentId}`);
288
+ console.log(` Cognee: ${enabled ? 'enabled' : 'disabled'}`);
289
+
290
+ if (!enabled) {
291
+ console.log('\n Run `soleri cognee enable` to add vector search.');
292
+ console.log('');
293
+ return;
294
+ }
295
+
296
+ // Check Docker Compose file
297
+ const composePath = join(ctx.agentPath, COGNEE_COMPOSE_FILE);
298
+ console.log(
299
+ ` Docker Compose: ${existsSync(composePath) ? 'present' : 'missing'}`,
300
+ );
301
+
302
+ // Check npm scripts
303
+ const hasScripts = Object.keys(COGNEE_SCRIPTS).every((s) => !!pkg.scripts?.[s]);
304
+ console.log(` Scripts: ${hasScripts ? 'all present' : 'some missing'}`);
305
+
306
+ // Check config file
307
+ const configPath = join(homedir(), `.${ctx.agentId}`, 'cognee.json');
308
+ if (existsSync(configPath)) {
309
+ try {
310
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
311
+ console.log(` Config: ${configPath}`);
312
+ console.log(` Base URL: ${config.baseUrl ?? 'not set'}`);
313
+ console.log(` Embedding: ${config.embeddingProvider ?? 'not set'}`);
314
+ console.log(` Dataset: ${config.dataset ?? ctx.agentId}`);
315
+ console.log(` API token: ${config.apiToken ? 'set' : 'not set (local mode)'}`);
316
+ } catch {
317
+ console.log(` Config: ${configPath} (invalid JSON)`);
318
+ }
319
+ } else {
320
+ console.log(` Config: not found at ${configPath}`);
321
+ console.log(' Run `soleri cognee setup` to configure.');
322
+ }
323
+
324
+ // Try health check
325
+ const baseUrl =
326
+ process.env.COGNEE_BASE_URL ??
327
+ (existsSync(configPath)
328
+ ? JSON.parse(readFileSync(configPath, 'utf-8')).baseUrl
329
+ : 'http://localhost:8000');
330
+
331
+ try {
332
+ const controller = new AbortController();
333
+ const timeout = setTimeout(() => controller.abort(), 5000);
334
+ const res = await fetch(`${baseUrl}/`, { signal: controller.signal });
335
+ clearTimeout(timeout);
336
+ console.log(
337
+ ` Sidecar: ${res.ok ? 'running' : `HTTP ${res.status}`} at ${baseUrl}`,
338
+ );
339
+ } catch {
340
+ console.log(` Sidecar: not reachable at ${baseUrl}`);
341
+ }
342
+
343
+ // Overall status
344
+ const ready = enabled && existsSync(configPath);
345
+ console.log(
346
+ `\n Status: ${ready ? 'configured' : 'needs configuration'}`,
347
+ );
348
+ if (!existsSync(configPath)) {
349
+ console.log(' Next: Run `soleri cognee setup`');
350
+ } else {
351
+ console.log(' Next: Run `npm run cognee:up` to start the sidecar');
352
+ }
353
+ console.log('');
354
+ });
355
+ }
356
+
357
+ // ─── Helpers ──────────────────────────────────────────────────────────
358
+
359
+ function readAgentConfig(agentPath: string, agentId: string): AgentConfig | null {
360
+ const personaCandidates = [
361
+ join(agentPath, 'src', 'identity', 'persona.ts'),
362
+ join(agentPath, 'src', 'activation', 'persona.ts'),
363
+ ];
364
+ const personaPath = personaCandidates.find((candidate) => existsSync(candidate));
365
+ if (!personaPath) return null;
366
+ const personaSrc = readFileSync(personaPath, 'utf-8');
367
+
368
+ const name = extractStringField(personaSrc, 'name') ?? agentId;
369
+ const role = extractStringField(personaSrc, 'role') ?? '';
370
+ const description = extractStringField(personaSrc, 'description') ?? '';
371
+ const tone =
372
+ (extractStringField(personaSrc, 'tone') as 'precise' | 'mentor' | 'pragmatic') ?? 'pragmatic';
373
+ const greeting = extractStringField(personaSrc, 'greeting') ?? `Hello! I'm ${name}.`;
374
+ const principles = extractArrayField(personaSrc, 'principles');
375
+
376
+ const indexPath = join(agentPath, 'src', 'index.ts');
377
+ const domains = existsSync(indexPath) ? extractDomains(readFileSync(indexPath, 'utf-8')) : [];
378
+
379
+ const pkg = JSON.parse(readFileSync(join(agentPath, 'package.json'), 'utf-8'));
380
+
381
+ return {
382
+ id: agentId,
383
+ name,
384
+ role,
385
+ description,
386
+ domains,
387
+ principles,
388
+ tone,
389
+ greeting,
390
+ outputDir: agentPath,
391
+ hookPacks: [],
392
+ model: pkg.soleri?.model ?? 'claude-code-sonnet-4',
393
+ setupTarget: pkg.soleri?.setupTarget ?? 'claude',
394
+ telegram: pkg.soleri?.telegram ?? false,
395
+ cognee: pkg.soleri?.cognee ?? false,
396
+ };
397
+ }
398
+
399
+ function extractStringField(src: string, field: string): string | undefined {
400
+ const re = new RegExp(`${field}:\\s*'([^']*)'`);
401
+ const m = src.match(re);
402
+ return m ? m[1].replace(/\\'/g, "'") : undefined;
403
+ }
404
+
405
+ function extractArrayField(src: string, field: string): string[] {
406
+ const re = new RegExp(`${field}:\\s*\\[([\\s\\S]*?)\\]`);
407
+ const m = src.match(re);
408
+ if (!m) return [];
409
+ return [...m[1].matchAll(/'([^']*)'/g)].map((x) => x[1]);
410
+ }
411
+
412
+ function extractDomains(indexSrc: string): string[] {
413
+ const m = indexSrc.match(/createDomainFacades\(runtime,\s*['"][^'"]+['"]\s*,\s*\[([\s\S]*?)\]\)/);
414
+ if (!m) return [];
415
+ return [...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
416
+ }
@@ -8,6 +8,7 @@ import {
8
8
  AgentConfigSchema,
9
9
  SETUP_TARGETS,
10
10
  type SetupTarget,
11
+ scaffoldFileTree,
11
12
  } from '@soleri/forge/lib';
12
13
  import { runCreateWizard } from '../prompts/create-wizard.js';
13
14
  import { listPacks } from '../hook-packs/registry.js';
@@ -21,9 +22,9 @@ function parseSetupTarget(value?: string): SetupTarget | undefined {
21
22
  return undefined;
22
23
  }
23
24
 
24
- function includesClaudeSetup(target: SetupTarget | undefined): boolean {
25
- const resolved = target ?? 'claude';
26
- return resolved === 'claude' || resolved === 'both';
25
+ function includesClaudeSetup(target: SetupTarget | string | undefined): boolean {
26
+ const resolved = target ?? 'opencode';
27
+ return resolved === 'claude' || resolved === 'both' || resolved === 'all';
27
28
  }
28
29
 
29
30
  export function registerCreate(program: Command): void {
@@ -36,9 +37,20 @@ export function registerCreate(program: Command): void {
36
37
  `Setup target: ${SETUP_TARGETS.join(', ')} (default: claude)`,
37
38
  )
38
39
  .option('-y, --yes', 'Skip confirmation prompts (use with --config for fully non-interactive)')
40
+ .option('--filetree', 'Create a file-tree agent (v7 — no TypeScript, no build step)')
41
+ .option('--legacy', 'Create a legacy TypeScript agent (v6 — requires npm install + build)')
39
42
  .description('Create a new Soleri agent')
40
43
  .action(
41
- async (name?: string, opts?: { config?: string; yes?: boolean; setupTarget?: string }) => {
44
+ async (
45
+ name?: string,
46
+ opts?: {
47
+ config?: string;
48
+ yes?: boolean;
49
+ setupTarget?: string;
50
+ filetree?: boolean;
51
+ legacy?: boolean;
52
+ },
53
+ ) => {
42
54
  try {
43
55
  let config;
44
56
 
@@ -81,6 +93,78 @@ export function registerCreate(program: Command): void {
81
93
  if (setupTarget) {
82
94
  config = { ...config, setupTarget };
83
95
  }
96
+ // ─── File-tree agent (v7) ──────────────────────────────
97
+ // Default to filetree unless --legacy is explicitly passed
98
+ const useFileTree = opts?.filetree || !opts?.legacy;
99
+
100
+ if (useFileTree) {
101
+ // Convert to AgentYaml format
102
+ // Cast to Record to access fields that may exist on the parsed config
103
+ // but aren't in the strict AgentConfig type (model, cognee, vaults, domainPacks)
104
+ const raw = config as Record<string, unknown>;
105
+ const agentYamlInput = {
106
+ id: config.id,
107
+ name: config.name,
108
+ role: config.role,
109
+ description: config.description,
110
+ domains: config.domains,
111
+ principles: config.principles,
112
+ tone: config.tone,
113
+ greeting: config.greeting,
114
+ setup: {
115
+ target: config.setupTarget,
116
+ model: (raw.model as string) ?? 'claude-code-sonnet-4',
117
+ },
118
+ engine: {
119
+ cognee: (raw.cognee as boolean) ?? false,
120
+ },
121
+ vaults: raw.vaults as
122
+ | Array<{ name: string; path: string; priority?: number }>
123
+ | undefined,
124
+ packs: (
125
+ raw.domainPacks as
126
+ | Array<{ name: string; package: string; version?: string }>
127
+ | undefined
128
+ )?.map((dp) => ({
129
+ name: dp.name,
130
+ package: dp.package,
131
+ version: dp.version,
132
+ })),
133
+ };
134
+
135
+ const outputDir = config.outputDir ?? process.cwd();
136
+ const nonInteractive = !!(opts?.yes || opts?.config);
137
+
138
+ if (!nonInteractive) {
139
+ p.log.info(
140
+ `Will create file-tree agent "${config.name}" in ${outputDir}/${config.id}`,
141
+ );
142
+ p.log.info(`Domains: ${config.domains.join(', ')}`);
143
+ p.log.info('No build step — agent is ready to use immediately.');
144
+
145
+ const confirmed = await p.confirm({ message: 'Create agent?' });
146
+ if (p.isCancel(confirmed) || !confirmed) {
147
+ p.outro('Cancelled.');
148
+ return;
149
+ }
150
+ }
151
+
152
+ const s = p.spinner();
153
+ s.start('Creating file-tree agent...');
154
+ const result = scaffoldFileTree(agentYamlInput, outputDir);
155
+ s.stop(result.success ? 'Agent created!' : 'Creation failed');
156
+
157
+ if (!result.success) {
158
+ p.log.error(result.summary);
159
+ process.exit(1);
160
+ }
161
+
162
+ p.note(result.summary, 'Next steps');
163
+ p.outro('Done!');
164
+ return;
165
+ }
166
+
167
+ // ─── Legacy TypeScript agent (v6) ─────────────────────
84
168
  const claudeSetup = includesClaudeSetup(config.setupTarget);
85
169
 
86
170
  const nonInteractive = !!(opts?.yes || opts?.config);
@@ -98,9 +182,9 @@ export function registerCreate(program: Command): void {
98
182
  const available = listPacks().map((pk) => pk.name);
99
183
  const unknown = selectedPacks.filter((pk) => !available.includes(pk));
100
184
  if (unknown.length > 0) {
101
- for (const name of unknown) {
185
+ for (const packName of unknown) {
102
186
  p.log.warn(
103
- `Unknown hook pack "${name}" — skipping. Available: ${available.join(', ')}`,
187
+ `Unknown hook pack "${packName}" — skipping. Available: ${available.join(', ')}`,
104
188
  );
105
189
  }
106
190
  selectedPacks = selectedPacks.filter((pk) => available.includes(pk));
@@ -1,4 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { watch, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import type { Command } from 'commander';
3
5
  import * as p from '@clack/prompts';
4
6
  import { detectAgent } from '../utils/agent-context.js';
@@ -14,26 +16,120 @@ export function registerDev(program: Command): void {
14
16
  process.exit(1);
15
17
  }
16
18
 
17
- p.log.info(`Starting ${ctx.agentId} in dev mode...`);
19
+ if (ctx.format === 'filetree') {
20
+ // v7: File-tree agent — watch files and regenerate CLAUDE.md
21
+ runFileTreeDev(ctx.agentPath, ctx.agentId);
22
+ } else {
23
+ // Legacy: TypeScript agent — run via tsx
24
+ runLegacyDev(ctx.agentPath, ctx.agentId);
25
+ }
26
+ });
27
+ }
18
28
 
19
- const child = spawn('npx', ['tsx', 'src/index.ts'], {
20
- cwd: ctx.agentPath,
21
- stdio: 'inherit',
22
- env: { ...process.env },
23
- });
29
+ function runFileTreeDev(agentPath: string, agentId: string): void {
30
+ p.log.info(`Starting ${agentId} in file-tree dev mode...`);
31
+ p.log.info('Starting Knowledge Engine + watching for file changes.');
32
+ p.log.info('CLAUDE.md will be regenerated automatically on changes.');
33
+ p.log.info('Press Ctrl+C to stop.\n');
24
34
 
25
- child.on('error', (err) => {
26
- p.log.error(`Failed to start: ${err.message}`);
27
- p.log.info('Make sure tsx is available: npm install -g tsx');
28
- process.exit(1);
29
- });
35
+ regenerateClaudeMd(agentPath);
36
+
37
+ // Start the engine server
38
+ const engineBin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
39
+ const engine = spawn('node', [engineBin, '--agent', join(agentPath, 'agent.yaml')], {
40
+ stdio: ['pipe', 'inherit', 'inherit'],
41
+ env: { ...process.env },
42
+ });
43
+
44
+ engine.on('error', (err) => {
45
+ p.log.error(`Engine failed to start: ${err.message}`);
46
+ p.log.info('Make sure @soleri/core is built: cd packages/core && npm run build');
47
+ });
48
+
49
+ // Watch directories for changes
50
+ const watchPaths = [
51
+ join(agentPath, 'agent.yaml'),
52
+ join(agentPath, 'instructions'),
53
+ join(agentPath, 'workflows'),
54
+ join(agentPath, 'skills'),
55
+ ];
56
+
57
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
58
+
59
+ for (const watchPath of watchPaths) {
60
+ try {
61
+ watch(watchPath, { recursive: true }, (_event, filename) => {
62
+ // Ignore CLAUDE.md changes (we generate it)
63
+ if (filename === 'CLAUDE.md' || filename === 'AGENTS.md') return;
64
+ // Ignore _engine.md changes (we generate it)
65
+ if (filename === '_engine.md') return;
30
66
 
31
- child.on('exit', (code, signal) => {
32
- if (signal) {
33
- p.log.warn(`Process terminated by signal ${signal}`);
34
- process.exit(1);
35
- }
36
- process.exit(code ?? 0);
67
+ // Debounce regenerate at most once per 200ms
68
+ if (debounceTimer) clearTimeout(debounceTimer);
69
+ debounceTimer = setTimeout(() => {
70
+ const changedFile = filename ? ` (${filename})` : '';
71
+ p.log.info(`Change detected${changedFile} — regenerating CLAUDE.md`);
72
+ regenerateClaudeMd(agentPath);
73
+ }, 200);
37
74
  });
38
- });
75
+ } catch {
76
+ // Directory may not exist yet — that's OK
77
+ }
78
+ }
79
+
80
+ // Graceful shutdown — kill engine too
81
+ const shutdown = () => {
82
+ p.log.info('\nStopping dev mode...');
83
+ engine.kill();
84
+ process.exit(0);
85
+ };
86
+
87
+ process.on('SIGINT', shutdown);
88
+ process.on('SIGTERM', shutdown);
89
+
90
+ engine.on('exit', (code) => {
91
+ if (code !== 0 && code !== null) {
92
+ p.log.error(`Engine exited with code ${code}`);
93
+ process.exit(1);
94
+ }
95
+ });
96
+ }
97
+
98
+ function regenerateClaudeMd(agentPath: string): void {
99
+ try {
100
+ // Dynamic import to avoid loading forge at CLI startup
101
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
102
+ const { composeClaudeMd } = require('@soleri/forge/lib');
103
+ const { content } = composeClaudeMd(agentPath);
104
+ writeFileSync(join(agentPath, 'CLAUDE.md'), content, 'utf-8');
105
+ p.log.success('CLAUDE.md regenerated');
106
+ } catch (err) {
107
+ p.log.error(
108
+ `Failed to regenerate CLAUDE.md: ${err instanceof Error ? err.message : String(err)}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ function runLegacyDev(agentPath: string, agentId: string): void {
114
+ p.log.info(`Starting ${agentId} in dev mode...`);
115
+
116
+ const child = spawn('npx', ['tsx', 'src/index.ts'], {
117
+ cwd: agentPath,
118
+ stdio: 'inherit',
119
+ env: { ...process.env },
120
+ });
121
+
122
+ child.on('error', (err) => {
123
+ p.log.error(`Failed to start: ${err.message}`);
124
+ p.log.info('Make sure tsx is available: npm install -g tsx');
125
+ process.exit(1);
126
+ });
127
+
128
+ child.on('exit', (code, signal) => {
129
+ if (signal) {
130
+ p.log.warn(`Process terminated by signal ${signal}`);
131
+ process.exit(1);
132
+ }
133
+ process.exit(code ?? 0);
134
+ });
39
135
  }