@soleri/cli 9.14.3 → 9.16.7

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 (87) hide show
  1. package/dist/commands/add-domain.js +65 -0
  2. package/dist/commands/add-domain.js.map +1 -1
  3. package/dist/commands/agent.js +51 -20
  4. package/dist/commands/agent.js.map +1 -1
  5. package/dist/commands/brain.d.ts +8 -0
  6. package/dist/commands/brain.js +83 -0
  7. package/dist/commands/brain.js.map +1 -0
  8. package/dist/commands/chat.d.ts +11 -0
  9. package/dist/commands/chat.js +295 -0
  10. package/dist/commands/chat.js.map +1 -0
  11. package/dist/commands/create.js +11 -9
  12. package/dist/commands/create.js.map +1 -1
  13. package/dist/commands/dev.js +19 -0
  14. package/dist/commands/dev.js.map +1 -1
  15. package/dist/commands/dream.js +1 -12
  16. package/dist/commands/dream.js.map +1 -1
  17. package/dist/commands/hooks.js +29 -0
  18. package/dist/commands/hooks.js.map +1 -1
  19. package/dist/commands/install.js +3 -9
  20. package/dist/commands/install.js.map +1 -1
  21. package/dist/commands/knowledge.d.ts +9 -0
  22. package/dist/commands/knowledge.js +99 -0
  23. package/dist/commands/knowledge.js.map +1 -0
  24. package/dist/commands/pack.js +164 -3
  25. package/dist/commands/pack.js.map +1 -1
  26. package/dist/commands/schedule.d.ts +11 -0
  27. package/dist/commands/schedule.js +130 -0
  28. package/dist/commands/schedule.js.map +1 -0
  29. package/dist/commands/staging.d.ts +2 -17
  30. package/dist/commands/staging.js +4 -4
  31. package/dist/commands/staging.js.map +1 -1
  32. package/dist/commands/validate-skills.d.ts +10 -0
  33. package/dist/commands/validate-skills.js +47 -0
  34. package/dist/commands/validate-skills.js.map +1 -0
  35. package/dist/commands/vault.js +2 -11
  36. package/dist/commands/vault.js.map +1 -1
  37. package/dist/main.js +10 -0
  38. package/dist/main.js.map +1 -1
  39. package/dist/utils/checks.js +17 -32
  40. package/dist/utils/checks.js.map +1 -1
  41. package/dist/utils/core-resolver.d.ts +3 -0
  42. package/dist/utils/core-resolver.js +38 -0
  43. package/dist/utils/core-resolver.js.map +1 -0
  44. package/dist/utils/vault-db.d.ts +5 -0
  45. package/dist/utils/vault-db.js +17 -0
  46. package/dist/utils/vault-db.js.map +1 -0
  47. package/package.json +1 -1
  48. package/src/__tests__/doctor.test.ts +46 -1
  49. package/src/__tests__/hook-packs.test.ts +0 -18
  50. package/src/__tests__/hooks-convert.test.ts +0 -28
  51. package/src/__tests__/hooks-sync.test.ts +109 -0
  52. package/src/__tests__/hooks.test.ts +0 -20
  53. package/src/__tests__/install-verify.test.ts +1 -1
  54. package/src/__tests__/install.test.ts +7 -10
  55. package/src/__tests__/update.test.ts +0 -19
  56. package/src/__tests__/validator.test.ts +0 -16
  57. package/src/commands/add-domain.ts +89 -1
  58. package/src/commands/agent.ts +53 -17
  59. package/src/commands/brain.ts +93 -0
  60. package/src/commands/chat.ts +373 -0
  61. package/src/commands/create.ts +11 -8
  62. package/src/commands/dev.ts +21 -0
  63. package/src/commands/dream.ts +1 -11
  64. package/src/commands/hooks.ts +32 -0
  65. package/src/commands/install.ts +3 -8
  66. package/src/commands/knowledge.ts +124 -0
  67. package/src/commands/pack.ts +219 -1
  68. package/src/commands/schedule.ts +150 -0
  69. package/src/commands/staging.ts +5 -5
  70. package/src/commands/validate-skills.ts +58 -0
  71. package/src/commands/vault.ts +2 -11
  72. package/src/main.ts +10 -0
  73. package/src/utils/checks.ts +18 -30
  74. package/src/utils/core-resolver.ts +39 -0
  75. package/src/utils/vault-db.ts +15 -0
  76. package/dist/hook-packs/converter/template.test.ts +0 -133
  77. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -274
  78. package/dist/prompts/archetypes.d.ts +0 -22
  79. package/dist/prompts/archetypes.js +0 -298
  80. package/dist/prompts/archetypes.js.map +0 -1
  81. package/dist/prompts/playbook.d.ts +0 -64
  82. package/dist/prompts/playbook.js +0 -436
  83. package/dist/prompts/playbook.js.map +0 -1
  84. package/dist/utils/format-paths.d.ts +0 -14
  85. package/dist/utils/format-paths.js +0 -27
  86. package/dist/utils/format-paths.js.map +0 -1
  87. package/src/__tests__/dream.test.ts +0 -119
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Brain CLI — brain session management.
3
+ *
4
+ * `soleri brain close-orphans` — close orphaned sessions (default: --max-age 1h)
5
+ * `soleri brain close-orphans --max-age 2h` — close sessions older than 2h
6
+ */
7
+
8
+ import type { Command } from 'commander';
9
+ import { detectAgent } from '../utils/agent-context.js';
10
+ import { pass, fail, info, heading } from '../utils/logger.js';
11
+ import { resolveVaultDbPath } from '../utils/vault-db.js';
12
+
13
+ function parseMaxAge(value: string): number {
14
+ const match = value.match(/^(\d+)(h|m|s)$/);
15
+ if (!match) throw new Error(`Invalid --max-age format "${value}". Use e.g. 1h, 30m, 90s`);
16
+ const n = parseInt(match[1], 10);
17
+ const unit = match[2];
18
+ if (unit === 'h') return n * 60 * 60 * 1000;
19
+ if (unit === 'm') return n * 60 * 1000;
20
+ return n * 1000;
21
+ }
22
+
23
+ export function registerBrain(program: Command): void {
24
+ const brain = program.command('brain').description('Brain session management');
25
+
26
+ brain
27
+ .command('close-orphans')
28
+ .description('Close orphaned brain sessions that were never completed')
29
+ .option('--max-age <duration>', 'Close sessions older than this age (e.g. 1h, 30m)', '1h')
30
+ .action(async (opts: { maxAge: string }) => {
31
+ const agent = detectAgent();
32
+ if (!agent) {
33
+ fail('Not in a Soleri agent project', 'Run from an agent directory');
34
+ process.exit(1);
35
+ }
36
+
37
+ const dbPath = resolveVaultDbPath(agent.agentId);
38
+ if (!dbPath) {
39
+ info('Vault DB not found — no sessions to close.');
40
+ process.exit(0);
41
+ }
42
+
43
+ let maxAgeMs: number;
44
+ try {
45
+ maxAgeMs = parseMaxAge(opts.maxAge);
46
+ } catch (e: unknown) {
47
+ fail(
48
+ e instanceof Error ? e.message : String(e),
49
+ 'Example: soleri brain close-orphans --max-age 1h',
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const { Vault, Brain, BrainIntelligence } = await import('@soleri/core');
55
+ const vault = new Vault(dbPath);
56
+
57
+ try {
58
+ const brainInstance = new Brain(vault);
59
+ const intelligence = new BrainIntelligence(vault, brainInstance);
60
+
61
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString().replace('T', ' ').slice(0, 19);
62
+ const activeSessions = intelligence.listSessions({ active: true, limit: 1000 });
63
+ const orphans = activeSessions.filter((s) => s.startedAt < cutoff);
64
+
65
+ if (orphans.length === 0) {
66
+ info(`No orphaned sessions older than ${opts.maxAge}.`);
67
+ process.exit(0);
68
+ }
69
+
70
+ heading('Brain — Close Orphans');
71
+
72
+ let closed = 0;
73
+ for (const s of orphans) {
74
+ try {
75
+ intelligence.lifecycle({
76
+ action: 'end',
77
+ sessionId: s.id,
78
+ planOutcome: 'abandoned',
79
+ context: `auto-closed via CLI: no completion after ${opts.maxAge}`,
80
+ });
81
+ closed++;
82
+ } catch {
83
+ // best-effort — never block on failures
84
+ }
85
+ }
86
+
87
+ pass(`Closed ${closed} orphaned session${closed === 1 ? '' : 's'}`);
88
+ process.exit(0);
89
+ } finally {
90
+ vault.close();
91
+ }
92
+ });
93
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * `soleri chat` — interactive terminal chat with your agent.
3
+ *
4
+ * Spawns the agent's MCP server, connects via stdio JSON-RPC,
5
+ * and runs an interactive REPL using the agent loop.
6
+ *
7
+ * No external dependencies — MCP client is a minimal JSON-RPC/stdio
8
+ * implementation, Anthropic API is called via the core agent loop.
9
+ */
10
+
11
+ import { spawn } from 'node:child_process';
12
+ import type { ChildProcess } from 'node:child_process';
13
+ import { createInterface } from 'node:readline';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { homedir } from 'node:os';
17
+ import type { Command } from 'commander';
18
+ import * as p from '@clack/prompts';
19
+ import { detectAgent } from '../utils/agent-context.js';
20
+ import { runAgentLoop, SOLERI_HOME } from '@soleri/core';
21
+ import type { AgentTool, ChatMessage } from '@soleri/core';
22
+
23
+ // ─── MCP Types ──────────────────────────────────────────────────────────
24
+
25
+ interface JsonRpcRequest {
26
+ jsonrpc: '2.0';
27
+ id: number;
28
+ method: string;
29
+ params?: Record<string, unknown>;
30
+ }
31
+
32
+ interface JsonRpcNotification {
33
+ jsonrpc: '2.0';
34
+ method: string;
35
+ params?: Record<string, unknown>;
36
+ }
37
+
38
+ interface JsonRpcResponse {
39
+ jsonrpc: '2.0';
40
+ id: number;
41
+ result?: unknown;
42
+ error?: { code: number; message: string };
43
+ }
44
+
45
+ interface McpToolSchema {
46
+ name: string;
47
+ description?: string;
48
+ inputSchema?: { type?: string; properties?: Record<string, unknown>; required?: string[] };
49
+ }
50
+
51
+ // ─── Minimal MCP Stdio Client ───────────────────────────────────────────
52
+
53
+ class StdioMcpClient {
54
+ private proc: ChildProcess;
55
+ private pending = new Map<
56
+ number,
57
+ { resolve: (v: unknown) => void; reject: (e: Error) => void }
58
+ >();
59
+ private nextId = 1;
60
+ private buffer = '';
61
+
62
+ constructor(command: string, args: string[], env?: Record<string, string>) {
63
+ this.proc = spawn(command, args, {
64
+ stdio: ['pipe', 'pipe', 'inherit'],
65
+ env: { ...process.env, ...env },
66
+ });
67
+
68
+ this.proc.stdout!.on('data', (chunk: Buffer) => {
69
+ this.buffer += chunk.toString();
70
+ const lines = this.buffer.split('\n');
71
+ this.buffer = lines.pop() ?? '';
72
+ for (const line of lines) {
73
+ const trimmed = line.trim();
74
+ if (!trimmed) continue;
75
+ try {
76
+ const msg = JSON.parse(trimmed) as JsonRpcResponse;
77
+ if ('id' in msg && msg.id !== null && msg.id !== undefined) {
78
+ const handler = this.pending.get(msg.id);
79
+ if (handler) {
80
+ this.pending.delete(msg.id);
81
+ if (msg.error) {
82
+ handler.reject(new Error(msg.error.message));
83
+ } else {
84
+ handler.resolve(msg.result);
85
+ }
86
+ }
87
+ }
88
+ } catch {
89
+ // ignore non-JSON lines
90
+ }
91
+ }
92
+ });
93
+
94
+ this.proc.on('exit', () => {
95
+ for (const handler of this.pending.values()) {
96
+ handler.reject(new Error('MCP server exited'));
97
+ }
98
+ this.pending.clear();
99
+ });
100
+ }
101
+
102
+ private send(msg: JsonRpcRequest | JsonRpcNotification): void {
103
+ this.proc.stdin!.write(JSON.stringify(msg) + '\n');
104
+ }
105
+
106
+ private request(method: string, params?: Record<string, unknown>): Promise<unknown> {
107
+ const id = this.nextId++;
108
+ return new Promise((resolve, reject) => {
109
+ this.pending.set(id, { resolve, reject });
110
+ this.send({ jsonrpc: '2.0', id, method, params: params ?? {} });
111
+ setTimeout(() => {
112
+ if (this.pending.has(id)) {
113
+ this.pending.delete(id);
114
+ reject(new Error(`MCP request timeout: ${method}`));
115
+ }
116
+ }, 30_000);
117
+ });
118
+ }
119
+
120
+ async initialize(): Promise<void> {
121
+ await this.request('initialize', {
122
+ protocolVersion: '2024-11-05',
123
+ capabilities: {},
124
+ clientInfo: { name: 'soleri-chat', version: '0.0.1' },
125
+ });
126
+ this.send({ jsonrpc: '2.0', method: 'notifications/initialized' });
127
+ }
128
+
129
+ async listTools(): Promise<McpToolSchema[]> {
130
+ const result = (await this.request('tools/list', {})) as { tools?: McpToolSchema[] };
131
+ return result.tools ?? [];
132
+ }
133
+
134
+ async callTool(name: string, args: Record<string, unknown>): Promise<string> {
135
+ const result = (await this.request('tools/call', {
136
+ name,
137
+ arguments: args,
138
+ })) as { content?: Array<{ type: string; text?: string }>; isError?: boolean };
139
+
140
+ if (!result?.content) return '';
141
+ return result.content
142
+ .filter((c) => c.type === 'text')
143
+ .map((c) => c.text ?? '')
144
+ .join('\n');
145
+ }
146
+
147
+ kill(): void {
148
+ try {
149
+ this.proc.kill('SIGTERM');
150
+ } catch {
151
+ /* ignore */
152
+ }
153
+ }
154
+ }
155
+
156
+ // ─── Helper: Find Agent MCP Command ─────────────────────────────────────
157
+
158
+ interface McpCommand {
159
+ command: string;
160
+ args: string[];
161
+ env?: Record<string, string>;
162
+ }
163
+
164
+ function findMcpCommand(agentId: string): McpCommand | null {
165
+ const claudeConfigPath = join(homedir(), '.claude.json');
166
+ if (!existsSync(claudeConfigPath)) return null;
167
+
168
+ try {
169
+ const config = JSON.parse(readFileSync(claudeConfigPath, 'utf-8')) as {
170
+ mcpServers?: Record<
171
+ string,
172
+ { command?: string; args?: string[]; env?: Record<string, string> }
173
+ >;
174
+ };
175
+ const servers = config.mcpServers ?? {};
176
+ for (const [key, srv] of Object.entries(servers)) {
177
+ if (key === agentId || key.includes(agentId)) {
178
+ if (srv.command) {
179
+ return { command: srv.command, args: srv.args ?? [], env: srv.env };
180
+ }
181
+ }
182
+ }
183
+ } catch {
184
+ /* ignore */
185
+ }
186
+
187
+ // Fallback: try local dist/agent.js
188
+ return null;
189
+ }
190
+
191
+ // ─── Main Command ────────────────────────────────────────────────────────
192
+
193
+ export function registerChat(program: Command): void {
194
+ program
195
+ .command('chat')
196
+ .option('--model <model>', 'Claude model to use', 'claude-sonnet-4-20250514')
197
+ .option('--no-tools', 'Disable MCP tools (plain conversation)')
198
+ .description('Start an interactive chat session with your agent')
199
+ .action(async (opts: { model: string; tools: boolean }) => {
200
+ const ctx = detectAgent();
201
+ if (!ctx) {
202
+ p.log.error('No agent project detected in current directory.');
203
+ process.exit(1);
204
+ }
205
+
206
+ // ─── API Key ─────────────────────────────────────────────────
207
+ const apiKey =
208
+ process.env.ANTHROPIC_API_KEY ??
209
+ (() => {
210
+ try {
211
+ const keysPath = join(SOLERI_HOME, ctx.agentId, 'keys.json');
212
+ if (existsSync(keysPath)) {
213
+ const data = JSON.parse(readFileSync(keysPath, 'utf-8')) as {
214
+ anthropic?: string[];
215
+ };
216
+ return data.anthropic?.[0] ?? null;
217
+ }
218
+ } catch {
219
+ /* ignore */
220
+ }
221
+ return null;
222
+ })();
223
+
224
+ if (!apiKey) {
225
+ p.log.error(
226
+ 'ANTHROPIC_API_KEY is not set. Export it or add it to ' +
227
+ join(SOLERI_HOME, ctx.agentId, 'keys.json'),
228
+ );
229
+ process.exit(1);
230
+ }
231
+
232
+ // ─── System Prompt ────────────────────────────────────────────
233
+ const claudeMdPath = join(ctx.agentPath, 'CLAUDE.md');
234
+ const systemPrompt = existsSync(claudeMdPath)
235
+ ? readFileSync(claudeMdPath, 'utf-8')
236
+ : `You are ${ctx.agentId}, a helpful AI assistant powered by the Soleri engine.`;
237
+
238
+ // ─── MCP Tools ────────────────────────────────────────────────
239
+ let mcpClient: StdioMcpClient | null = null;
240
+ let tools: AgentTool[] = [];
241
+
242
+ if (opts.tools) {
243
+ const mcpCmd = findMcpCommand(ctx.agentId);
244
+
245
+ if (!mcpCmd) {
246
+ // Try local dist/agent.js
247
+ const localAgent = join(ctx.agentPath, 'dist', 'agent.js');
248
+ if (existsSync(localAgent)) {
249
+ mcpClient = new StdioMcpClient('node', [localAgent]);
250
+ }
251
+ } else {
252
+ mcpClient = new StdioMcpClient(mcpCmd.command, mcpCmd.args, mcpCmd.env);
253
+ }
254
+
255
+ if (mcpClient) {
256
+ const s = p.spinner();
257
+ s.start('Connecting to agent MCP server...');
258
+ try {
259
+ await mcpClient.initialize();
260
+ const mcpTools = await mcpClient.listTools();
261
+ tools = mcpTools.map((t) => ({
262
+ name: t.name,
263
+ description: t.description ?? '',
264
+ inputSchema: t.inputSchema ?? { type: 'object', properties: {} },
265
+ }));
266
+ s.stop(`Connected — ${tools.length} tools available`);
267
+ } catch (_err) {
268
+ s.stop('Could not connect to MCP server — running without tools');
269
+ mcpClient.kill();
270
+ mcpClient = null;
271
+ }
272
+ } else {
273
+ p.log.warn(
274
+ `No MCP server found for "${ctx.agentId}". ` +
275
+ 'Run `soleri dev` first, then re-run `soleri chat`. Running without tools.',
276
+ );
277
+ }
278
+ }
279
+
280
+ // ─── REPL ─────────────────────────────────────────────────────
281
+ console.log('');
282
+ p.intro(`Chat with ${ctx.agentId} (${opts.model})`);
283
+ console.log(' Type your message and press Enter. Ctrl+C or "exit" to quit.');
284
+ if (tools.length > 0) {
285
+ console.log(` Tools: ${tools.map((t) => t.name).join(', ')}`);
286
+ }
287
+ console.log('');
288
+
289
+ const history: ChatMessage[] = [];
290
+
291
+ const rl = createInterface({
292
+ input: process.stdin,
293
+ output: process.stdout,
294
+ terminal: true,
295
+ });
296
+
297
+ const prompt = (): Promise<string> =>
298
+ new Promise((resolve) => {
299
+ rl.question('\x1b[36mYou:\x1b[0m ', resolve);
300
+ });
301
+
302
+ const cleanup = () => {
303
+ rl.close();
304
+ if (mcpClient) mcpClient.kill();
305
+ console.log('\n Goodbye!');
306
+ process.exit(0);
307
+ };
308
+
309
+ rl.on('close', () => cleanup());
310
+ process.on('SIGINT', () => cleanup());
311
+
312
+ // eslint-disable-next-line no-constant-condition
313
+ while (true) {
314
+ // Sequential await is intentional — this is a REPL loop
315
+ // eslint-disable-next-line no-await-in-loop
316
+ const input = await prompt().catch(() => 'exit');
317
+ const trimmed = input.trim();
318
+
319
+ if (!trimmed || trimmed === 'exit' || trimmed === 'quit') {
320
+ cleanup();
321
+ return;
322
+ }
323
+
324
+ history.push({ role: 'user', content: trimmed, timestamp: Date.now() });
325
+
326
+ const thinking = p.spinner();
327
+ thinking.start('Thinking...');
328
+
329
+ try {
330
+ // eslint-disable-next-line no-await-in-loop
331
+ const result = await runAgentLoop(history, {
332
+ apiKey,
333
+ model: opts.model,
334
+ systemPrompt,
335
+ tools,
336
+ executor: async (toolName, toolInput) => {
337
+ if (!mcpClient) return { output: 'No MCP server connected', isError: true };
338
+ try {
339
+ const output = await mcpClient.callTool(toolName, toolInput);
340
+ return { output, isError: false };
341
+ } catch (err) {
342
+ return {
343
+ output: err instanceof Error ? err.message : String(err),
344
+ isError: true,
345
+ };
346
+ }
347
+ },
348
+ maxIterations: 20,
349
+ });
350
+
351
+ thinking.stop('');
352
+
353
+ const response = result.text;
354
+ if (response) {
355
+ console.log(`\n\x1b[32m${ctx.agentId}:\x1b[0m ${response}\n`);
356
+ history.push({
357
+ role: 'assistant',
358
+ content: response,
359
+ timestamp: Date.now(),
360
+ });
361
+ }
362
+
363
+ if (result.newMessages.length > 0) {
364
+ history.push(...result.newMessages);
365
+ }
366
+ } catch (err) {
367
+ thinking.stop('Error');
368
+ p.log.error(err instanceof Error ? err.message : String(err));
369
+ history.pop(); // remove the failed user message
370
+ }
371
+ }
372
+ });
373
+ }
@@ -187,15 +187,8 @@ export function registerCreate(program: Command): void {
187
187
  const outputDir = opts?.dir ? resolve(opts.dir) : (config.outputDir ?? process.cwd());
188
188
 
189
189
  // Preflight: ensure output directory exists and is writable
190
- if (!existsSync(outputDir)) {
191
- try {
192
- mkdirSync(outputDir, { recursive: true });
193
- } catch {
194
- p.log.error(`Cannot create ${outputDir} — check permissions`);
195
- process.exit(1);
196
- }
197
- }
198
190
  try {
191
+ mkdirSync(outputDir, { recursive: true });
199
192
  accessSync(outputDir, fsConstants.W_OK);
200
193
  } catch {
201
194
  p.log.error(`Cannot write to ${outputDir} — check permissions`);
@@ -398,6 +391,16 @@ export function registerCreate(program: Command): void {
398
391
  }
399
392
  }
400
393
 
394
+ // Preflight: ensure output directory exists and is writable
395
+ const legacyOutputDir = resolve(config.outputDir ?? process.cwd());
396
+ try {
397
+ mkdirSync(legacyOutputDir, { recursive: true });
398
+ accessSync(legacyOutputDir, fsConstants.W_OK);
399
+ } catch {
400
+ p.log.error(`Cannot write to ${legacyOutputDir} — check permissions`);
401
+ process.exit(1);
402
+ }
403
+
401
404
  // Scaffold + auto-build
402
405
  const s = p.spinner();
403
406
  s.start('Scaffolding and building agent...');
@@ -33,6 +33,7 @@ async function runFileTreeDev(agentPath: string, agentId: string): Promise<void>
33
33
  p.log.info('Press Ctrl+C to stop.\n');
34
34
 
35
35
  await regenerateClaudeMd(agentPath);
36
+ await syncSkills(agentPath, agentId);
36
37
 
37
38
  // Start the engine server
38
39
  let engineBin: string;
@@ -88,6 +89,7 @@ async function runFileTreeDev(agentPath: string, agentId: string): Promise<void>
88
89
  const changedFile = filename ? ` (${filename})` : '';
89
90
  p.log.info(`Change detected${changedFile} — regenerating CLAUDE.md`);
90
91
  await regenerateClaudeMd(agentPath);
92
+ await syncSkills(agentPath, agentId);
91
93
  }, 200);
92
94
  });
93
95
  } catch (err: unknown) {
@@ -130,6 +132,25 @@ async function regenerateClaudeMd(agentPath: string): Promise<void> {
130
132
  }
131
133
  }
132
134
 
135
+ async function syncSkills(agentPath: string, agentId: string): Promise<void> {
136
+ try {
137
+ const { syncSkillsToClaudeCode } = await import('@soleri/core');
138
+ const skillsDir = join(agentPath, 'skills');
139
+ const result = syncSkillsToClaudeCode([skillsDir], agentId, { projectRoot: agentPath });
140
+ const total = result.installed.length + result.updated.length;
141
+ if (total > 0) {
142
+ p.log.success(
143
+ `Skills synced: ${result.installed.length} installed, ${result.updated.length} updated`,
144
+ );
145
+ }
146
+ if (result.removed.length > 0) {
147
+ p.log.info(`Skills removed: ${result.removed.join(', ')}`);
148
+ }
149
+ } catch (err) {
150
+ p.log.warn(`Skill sync failed: ${err instanceof Error ? err.message : String(err)}`);
151
+ }
152
+ }
153
+
133
154
  function runLegacyDev(agentPath: string, agentId: string): void {
134
155
  p.log.info(`Starting ${agentId} in dev mode...`);
135
156
 
@@ -7,20 +7,10 @@
7
7
  * `soleri dream status` — show dream status + cron info
8
8
  */
9
9
 
10
- import { existsSync } from 'node:fs';
11
- import { join } from 'node:path';
12
10
  import type { Command } from 'commander';
13
11
  import { detectAgent } from '../utils/agent-context.js';
14
12
  import { pass, fail, info, heading, dim } from '../utils/logger.js';
15
- import { SOLERI_HOME } from '@soleri/core';
16
-
17
- function resolveVaultDbPath(agentId: string): string | null {
18
- const newDbPath = join(SOLERI_HOME, agentId, 'vault.db');
19
- const legacyDbPath = join(SOLERI_HOME, '..', `.${agentId}`, 'vault.db');
20
- if (existsSync(newDbPath)) return newDbPath;
21
- if (existsSync(legacyDbPath)) return legacyDbPath;
22
- return null;
23
- }
13
+ import { resolveVaultDbPath } from '../utils/vault-db.js';
24
14
 
25
15
  export function registerDream(program: Command): void {
26
16
  const dream = program.command('dream').description('Vault memory consolidation');
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { SUPPORTED_EDITORS, type EditorId } from '../hooks/templates.js';
5
5
  import { installHooks, removeHooks, detectInstalledHooks } from '../hooks/generator.js';
6
6
  import { detectAgent } from '../utils/agent-context.js';
7
+ import { syncHooksToClaudeSettings } from '@soleri/core';
7
8
  import { listPacks, getPack } from '../hook-packs/registry.js';
8
9
  import { installPack, removePack, isPackInstalled } from '../hook-packs/installer.js';
9
10
  import { promotePack, demotePack } from '../hook-packs/graduation.js';
@@ -24,6 +25,37 @@ import * as log from '../utils/logger.js';
24
25
  export function registerHooks(program: Command): void {
25
26
  const hooks = program.command('hooks').description('Manage editor hooks and hook packs');
26
27
 
28
+ hooks
29
+ .command('sync')
30
+ .description(
31
+ 'Sync lifecycle hooks into ~/.claude/settings.json (runs automatically on postinstall)',
32
+ )
33
+ .action(() => {
34
+ const ctx = detectAgent();
35
+ if (!ctx) {
36
+ log.fail('No agent project detected. Run from an agent directory.');
37
+ process.exit(1);
38
+ }
39
+ const result = syncHooksToClaudeSettings(ctx.agentId);
40
+ if (result.error) {
41
+ log.fail(`Failed to sync hooks for ${ctx.agentId}: ${result.error}`);
42
+ process.exit(1);
43
+ }
44
+ const events = [...result.installed, ...result.updated, ...result.skipped];
45
+ if (result.installed.length > 0) {
46
+ log.pass(`Installed hooks for ${ctx.agentId}: ${result.installed.join(', ')}`);
47
+ }
48
+ if (result.updated.length > 0) {
49
+ log.pass(`Updated hooks for ${ctx.agentId}: ${result.updated.join(', ')}`);
50
+ }
51
+ if (result.skipped.length > 0) {
52
+ log.info(`Already up to date: ${result.skipped.join(', ')}`);
53
+ }
54
+ if (events.length === 0) {
55
+ log.info(`No hooks found for ${ctx.agentId}`);
56
+ }
57
+ });
58
+
27
59
  hooks
28
60
  .command('add')
29
61
  .argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`)
@@ -1,5 +1,4 @@
1
1
  import type { Command } from 'commander';
2
- import { createRequire } from 'node:module';
3
2
  import {
4
3
  accessSync,
5
4
  constants as fsConstants,
@@ -13,6 +12,7 @@ import { homedir } from 'node:os';
13
12
  import * as p from '@clack/prompts';
14
13
  import { detectAgent } from '../utils/agent-context.js';
15
14
  import { detectArtifacts } from '../utils/agent-artifacts.js';
15
+ import { resolveInstalledEngineBin } from '../utils/core-resolver.js';
16
16
 
17
17
  /** Default parent directory for agents: ~/.soleri/ */
18
18
  const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
@@ -27,13 +27,8 @@ export const toPosix = (p: string): string => p.replace(/\\/g, '/');
27
27
  * Falls back to `npx @soleri/engine` if resolution fails (e.g. not installed globally).
28
28
  */
29
29
  export function resolveEngineBin(): { command: string; bin: string } {
30
- try {
31
- const require = createRequire(import.meta.url);
32
- const bin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
33
- return { command: 'node', bin };
34
- } catch {
35
- return { command: 'npx', bin: '@soleri/engine' };
36
- }
30
+ const bin = resolveInstalledEngineBin();
31
+ return bin ? { command: 'node', bin } : { command: 'npx', bin: '@soleri/engine' };
37
32
  }
38
33
 
39
34
  /** MCP server entry for file-tree agents (resolved engine path, no npx) */