very-happy-cli 0.2.150 → 0.2.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/{AcpBackend-C-dhGJgd.mjs → AcpBackend-Bs3wwYbt.mjs} +2 -2
  2. package/dist/{AcpBackend-CXawaWWx.cjs → AcpBackend-CogiKCng.cjs} +2 -2
  3. package/dist/{AcpSessionManager-DKOtSqJc.mjs → AcpSessionManager-BT8PJMEm.mjs} +1 -1
  4. package/dist/{AcpSessionManager-TrwEZR7y.cjs → AcpSessionManager-BazkBuFo.cjs} +9 -9
  5. package/dist/agentGuidance-CqBn77AI.cjs +553 -0
  6. package/dist/agentGuidance-Daxn5Nvj.mjs +529 -0
  7. package/dist/{agentHome-Dm-Wjs38.mjs → agentHome-CCYfs8Ec.mjs} +3 -3
  8. package/dist/{agentHome-rylNIkix.cjs → agentHome-CpJjrQZc.cjs} +3 -3
  9. package/dist/auto-DV5emt1-.mjs +466 -0
  10. package/dist/auto-DncmTDvw.cjs +472 -0
  11. package/dist/codex/happyMcpStdioBridge.cjs +4 -2
  12. package/dist/codex/happyMcpStdioBridge.mjs +4 -2
  13. package/dist/{config-Dhu6LU0h.mjs → config-DbKpQpdL.mjs} +5 -5
  14. package/dist/{config-CgPu2wtS.cjs → config-DeJgSMh-.cjs} +5 -5
  15. package/dist/{index-CpNYvMTJ.mjs → index-BauTv2jB.mjs} +900 -72
  16. package/dist/{index-BsYH1sjV.mjs → index-CN7OF2cU.mjs} +5 -5
  17. package/dist/{index-DvfCUuDK.cjs → index-CRaJ9k2p.cjs} +5 -5
  18. package/dist/{index-CzpooENj.mjs → index-DahaLbu4.mjs} +19 -19
  19. package/dist/{index-K6mUS0jF.cjs → index-ZIW1jrYm.cjs} +941 -104
  20. package/dist/{index-DEVkDRas.cjs → index-imt4mC41.cjs} +19 -19
  21. package/dist/index.cjs +4 -4
  22. package/dist/index.mjs +4 -4
  23. package/dist/install-C1K3nrxF.mjs +114 -0
  24. package/dist/install-CDR3qBhs.cjs +118 -0
  25. package/dist/{installTerminalHooks-NIbsQ1wi.cjs → installTerminalHooks-BoDxpaBO.cjs} +5 -5
  26. package/dist/{installTerminalHooks-BF_tmw9V.mjs → installTerminalHooks-VvmlO6Ue.mjs} +5 -5
  27. package/dist/lib.cjs +3 -3
  28. package/dist/lib.mjs +3 -3
  29. package/dist/limits-BNTWyUaZ.cjs +2175 -0
  30. package/dist/limits-CKR_HJnO.mjs +2080 -0
  31. package/dist/{mcp-_TVUSg8W.cjs → mcp-CCKCKL6l.cjs} +5 -5
  32. package/dist/{mcp-DzydgXpf.mjs → mcp-D4dAo5BQ.mjs} +5 -5
  33. package/dist/{piRuntime-CSAiJk0X.cjs → piRuntime-Bn5ypbjH.cjs} +2 -2
  34. package/dist/{piRuntime-ACOhil6t.mjs → piRuntime-CW-XA4rz.mjs} +2 -2
  35. package/dist/{piTerminal-DHxGB3Ic.cjs → piTerminal-B7K75aXB.cjs} +6 -6
  36. package/dist/{piTerminal-BlE4vbvZ.mjs → piTerminal-BLSXK1u2.mjs} +6 -6
  37. package/dist/{runGemini-BMYCU16c.cjs → runGemini-Bgg1BzDK.cjs} +7 -7
  38. package/dist/{runGemini-D5IV1MoF.mjs → runGemini-D5N11LxO.mjs} +7 -7
  39. package/dist/{runOpenClaw-Qs4eFtcS.cjs → runOpenClaw-B9qWHybc.cjs} +6 -6
  40. package/dist/{runOpenClaw-CWPXrKMJ.mjs → runOpenClaw-CCpyKbrn.mjs} +6 -6
  41. package/dist/{send-Dv0km8UI.mjs → send-C44kcXz5.mjs} +21 -7
  42. package/dist/{send-qXWGilm8.cjs → send-wl9ei8yG.cjs} +21 -7
  43. package/dist/{sessions-DoULfKui.mjs → sessions-DXJEcUg6.mjs} +57 -11
  44. package/dist/{sessions-CpIdDhH7.cjs → sessions-Tad8elmy.cjs} +57 -11
  45. package/dist/{spawn-DQuOmUP7.mjs → spawn-BOlb5y1_.mjs} +142 -28
  46. package/dist/{spawn-DqzG1bTH.cjs → spawn-DA8WAFmm.cjs} +142 -28
  47. package/dist/{teams-4mhNh5cj.mjs → teams-Bs8lI887.mjs} +15 -72
  48. package/dist/{teams-ByEgUzsH.cjs → teams-qVLrFcdd.cjs} +13 -70
  49. package/dist/{todo-D7SDXL0c.cjs → todo-mCMoGjJk.cjs} +4 -23
  50. package/dist/{todo-C9IAUf0K.mjs → todo-nzsqGZqp.mjs} +2 -21
  51. package/dist/{types-D2Gkpe0N.cjs → types-CN28qJl7.cjs} +38 -813
  52. package/dist/{types-BD542hb1.mjs → types-xKrxbCRv.mjs} +208 -978
  53. package/package.json +7 -7
  54. package/dist/agentGuidance-Dla-BXO8.cjs +0 -267
  55. package/dist/agentGuidance-DrfhtVP1.mjs +0 -251
  56. package/dist/limits-Dyma1REp.cjs +0 -1236
  57. package/dist/limits-FThmm7WR.mjs +0 -1165
@@ -1,23 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var index = require('./index-K6mUS0jF.cjs');
5
- var index$1 = require('./types-D2Gkpe0N.cjs');
4
+ var index = require('./index-ZIW1jrYm.cjs');
5
+ var index$1 = require('./types-CN28qJl7.cjs');
6
6
  require('node:os');
7
7
  require('node:crypto');
8
- require('./limits-Dyma1REp.cjs');
8
+ require('./limits-BNTWyUaZ.cjs');
9
9
  require('node:fs/promises');
10
10
  require('node:fs');
11
11
  require('node:path');
12
12
  require('zod');
13
13
  require('tweetnacl');
14
14
  require('node:util');
15
+ require('@paralleldrive/cuid2');
15
16
  require('cross-spawn');
16
17
  require('node:readline');
17
18
  require('./projectPath-ChLhdOyt.cjs');
18
19
  require('path');
19
20
  require('url');
20
- require('./agentGuidance-Dla-BXO8.cjs');
21
+ require('./agentGuidance-CqBn77AI.cjs');
21
22
  require('fs/promises');
22
23
  require('ink');
23
24
  require('react');
@@ -27,7 +28,6 @@ require('node:events');
27
28
  require('socket.io-client');
28
29
  require('os');
29
30
  require('child_process');
30
- require('@paralleldrive/cuid2');
31
31
  require('node:child_process');
32
32
  require('fs');
33
33
  require('@anthropic-ai/sandbox-runtime');
@@ -54,7 +54,7 @@ const ACTIONS_NEEDING_ID = /* @__PURE__ */ new Set(["read", "stop", "archive", "
54
54
  const ACTIONS_NEEDING_REQUEST_ID = /* @__PURE__ */ new Set(["approve", "deny"]);
55
55
  const REQUEST_ID_RE = /^[a-zA-Z0-9_.:-]{1,128}$/;
56
56
  function defaultOptions() {
57
- return { action: "help", all: false, includeArchived: false, forSession: false, json: false };
57
+ return { action: "help", all: false, includeArchived: false, forSession: false, full: false, answer: false, wait: false, json: false };
58
58
  }
59
59
  function parseSessionsArgs(args) {
60
60
  const options = defaultOptions();
@@ -72,6 +72,18 @@ function parseSessionsArgs(args) {
72
72
  options.includeArchived = true;
73
73
  } else if (arg === "--for-session") {
74
74
  options.forSession = true;
75
+ } else if (arg === "--full") {
76
+ options.full = true;
77
+ } else if (arg === "--answer") {
78
+ options.answer = true;
79
+ } else if (arg === "--wait") {
80
+ options.wait = true;
81
+ } else if (arg === "--timeout") {
82
+ const value = args[++i];
83
+ if (value === void 0) throw new Error("--timeout requires a value");
84
+ const parsed = Number(value);
85
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error("--timeout must be a positive number of seconds");
86
+ options.timeoutSec = parsed;
75
87
  } else if (arg === "--reason") {
76
88
  const value = args[++i];
77
89
  if (value === void 0) throw new Error("--reason requires a value");
@@ -124,6 +136,12 @@ function parseSessionsArgs(args) {
124
136
  }
125
137
  if (options.forSession && options.action !== "approve") throw new Error("--for-session only applies to `sessions approve`");
126
138
  if (options.reason !== void 0 && options.action !== "deny") throw new Error("--reason only applies to `sessions deny`");
139
+ if (options.action !== "read") {
140
+ if (options.full) throw new Error("--full only applies to `sessions read`");
141
+ if (options.answer) throw new Error("--answer only applies to `sessions read`");
142
+ if (options.wait) throw new Error("--wait only applies to `sessions read`");
143
+ }
144
+ if (options.timeoutSec !== void 0 && !options.wait) throw new Error("--timeout only applies to `sessions read --wait`");
127
145
  return options;
128
146
  }
129
147
  const SESSIONS_HELP = `
@@ -131,7 +149,8 @@ ${chalk.bold("very-happy sessions")} - Inspect and control sessions on this mach
131
149
 
132
150
  ${chalk.bold("Usage:")}
133
151
  very-happy sessions list [--all [--include-archived]] [--tag <name>] [--limit <n>] [--json]
134
- very-happy sessions read <id> [--limit <n>] [--json]
152
+ very-happy sessions read <id> [--limit <n>] [--full] [--answer]
153
+ [--wait [--timeout <s>]] [--json]
135
154
  very-happy sessions stop <id> [--json]
136
155
  very-happy sessions archive <id> [--json]
137
156
  very-happy sessions approve <id> <requestId> [--for-session] [--json]
@@ -143,7 +162,9 @@ ${chalk.bold("Actions:")}
143
162
  REST), attention first. Each row says whether this machine could
144
163
  decrypt it (\`decryptable\`); rows from other machines show only
145
164
  id / active / archived / timestamps / url.
146
- read The tail of a session as a role-tagged transcript.
165
+ read The tail of a session as a role-tagged transcript, plus where
166
+ the latest turn stands (\`turn\` in --json: ended, status and
167
+ \`answer\` = the agent's reply to the latest prompt).
147
168
  stop SIGTERM the session's process via the local daemon.
148
169
  archive Mark the session inactive server-side (it stays resumable).
149
170
  approve Answer a pending permission request \`requestId\` with approve \u2014
@@ -161,6 +182,14 @@ ${chalk.bold("Options:")}
161
182
  --limit <n> list: how many NOT-running / not-attention sessions to
162
183
  include (default ${index.DEFAULT_RECENT_LIMIT}; running and attention rows are
163
184
  never cut). read: how many messages (default 20, max ${index.MAX_READ_LIMIT}).
185
+ --full read only: do not truncate entries (default caps each
186
+ line at 500 chars).
187
+ --answer read only: print just the agent's reply to the latest
188
+ prompt (the text after its last tool call), untruncated.
189
+ --wait read only: block until the latest turn has ended
190
+ (claude / codex / pi), then read. Pairs with
191
+ \`spawn --prompt\` / \`send\` for "ask and collect".
192
+ --timeout <s> read --wait only: give up after <s> seconds (default 600).
164
193
  --for-session approve only: approved_for_session instead of approved.
165
194
  --reason <text> deny only: reason forwarded to the wrapper.
166
195
  --json Machine-readable output.
@@ -176,6 +205,8 @@ ${chalk.bold("Scope:")}
176
205
  the account content key \u2014 a credentials change, not a flag.
177
206
 
178
207
  ${chalk.bold("Exit codes:")}
208
+ 2 read --wait: the turn had not ended when --timeout ran out (the
209
+ partial state is still printed)
179
210
  0 success (approve/deny: the wrapper acknowledged the verdict; check
180
211
  \`settled\` in --json to see whether the request has left the pending set)
181
212
  1 bad arguments, unknown session, no local key, request not pending,
@@ -263,15 +294,30 @@ async function handleSessionsCommand(args) {
263
294
  }
264
295
  const sessionId = options.sessionId;
265
296
  if (options.action === "read") {
266
- const result = await index.readSessionTranscript(sessionId, options.limit ?? 20);
297
+ let timedOut = false;
298
+ if (options.wait) {
299
+ try {
300
+ await index.waitForTurnEnd(sessionId, { timeoutMs: (options.timeoutSec ?? 600) * 1e3 });
301
+ } catch (error) {
302
+ if (!(error instanceof index.TurnWaitTimeoutError)) throw error;
303
+ timedOut = true;
304
+ console.error(chalk.yellow("Timeout:"), error.message);
305
+ }
306
+ }
307
+ const result = await index.readSessionTranscript(sessionId, options.limit ?? 20, { full: options.full });
267
308
  if (options.json) {
268
- console.log(JSON.stringify(result));
309
+ console.log(JSON.stringify(options.answer ? { sessionId, turn: result.turn } : result));
310
+ } else if (options.answer) {
311
+ if (result.turn.answer.length > 0) console.log(result.turn.answer);
312
+ else console.error(chalk.dim("(the agent has not replied to the latest prompt yet)"));
269
313
  } else {
270
314
  console.log(formatSummaryLine(result.summary));
271
315
  console.log(`--- last ${result.messageCount} message(s) ---`);
272
316
  console.log(result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)");
317
+ const turn = result.turn;
318
+ console.log(chalk.dim(`--- latest turn: ${turn.ended ? `ended (${turn.status ?? "unknown"})` : "running"}${turn.error ? ` \u2014 ${turn.error}` : ""} ---`));
273
319
  }
274
- process.exit(0);
320
+ process.exit(timedOut ? 2 : 0);
275
321
  }
276
322
  if (options.action === "stop") {
277
323
  const stopped = await index.stopSession(sessionId);
@@ -1,14 +1,13 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync, statSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession } from './types-BD542hb1.mjs';
5
- import { s as sessionWebUrl, i as isValidSpawnOrigin, a as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS, w as waitForSessionKey, b as sendUserMessage } from './index-CpNYvMTJ.mjs';
6
- import { l as logger } from './limits-FThmm7WR.mjs';
4
+ import { f as forkSession, g as getProjectPath, F as ForkSourceMissingError, C as CodexAppServerClient, h as forkCodexThread, j as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, k as stopDaemonSession } from './types-xKrxbCRv.mjs';
5
+ import { d as readPersistedSessions, l as logger } from './limits-CKR_HJnO.mjs';
6
+ import { k as readSessionMetadata, m as resolveSpawnPermissionMode, e as sessionWebUrl, n as resolveFirstMessageMeta, o as isValidSpawnOrigin, i as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS, u as waitForSessionKey, d as sendUserMessage } from './index-BauTv2jB.mjs';
7
7
  import 'axios';
8
8
  import 'node:events';
9
9
  import 'socket.io-client';
10
10
  import 'zod';
11
- import '@paralleldrive/cuid2';
12
11
  import 'node:crypto';
13
12
  import 'child_process';
14
13
  import 'util';
@@ -19,6 +18,7 @@ import 'cross-spawn';
19
18
  import './projectPath-CSpqWdxp.mjs';
20
19
  import 'url';
21
20
  import 'os';
21
+ import '@paralleldrive/cuid2';
22
22
  import 'node:fs/promises';
23
23
  import 'node:os';
24
24
  import 'node:child_process';
@@ -28,7 +28,8 @@ import 'tweetnacl';
28
28
  import 'node:readline';
29
29
  import '@anthropic-ai/sandbox-runtime';
30
30
  import 'expo-server-sdk';
31
- import './agentGuidance-DrfhtVP1.mjs';
31
+ import 'node:util';
32
+ import './agentGuidance-Daxn5Nvj.mjs';
32
33
  import 'ink';
33
34
  import 'react';
34
35
  import '@anthropic-ai/claude-agent-sdk';
@@ -38,7 +39,6 @@ import 'open';
38
39
  import 'fastify';
39
40
  import 'fastify-type-provider-zod';
40
41
  import 'node:module';
41
- import 'node:util';
42
42
  import '@modelcontextprotocol/sdk/server/mcp.js';
43
43
  import 'node:http';
44
44
  import '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -47,6 +47,46 @@ import 'inquirer';
47
47
  import 'node:url';
48
48
  import 'node:readline/promises';
49
49
 
50
+ function nonEmpty(value) {
51
+ return typeof value === "string" && value.trim().length > 0;
52
+ }
53
+ function resolveForkSource(sessionId, persisted) {
54
+ if (!persisted) {
55
+ throw new Error(`Session ${sessionId} has no local record \u2014 only sessions spawned by this machine's daemon (in the last 14 days) can be forked from here.`);
56
+ }
57
+ const metadata = persisted.metadata;
58
+ const directory = metadata?.path;
59
+ if (!nonEmpty(directory)) throw new Error(`Session ${sessionId} has no working directory in its metadata.`);
60
+ const permissionMode = nonEmpty(metadata.permissionMode) ? metadata.permissionMode : void 0;
61
+ if (metadata.flavor === "codex") {
62
+ if (!nonEmpty(metadata.codexThreadId)) throw new Error(`Codex session ${sessionId} has no thread id yet (it never ran a turn) \u2014 nothing to fork.`);
63
+ return { agent: "codex", directory, codexThreadId: metadata.codexThreadId, permissionMode, parentSessionId: sessionId };
64
+ }
65
+ if (metadata.flavor && metadata.flavor !== "claude") {
66
+ throw new Error(`Session ${sessionId} runs ${metadata.flavor}; only claude and codex sessions can be forked.`);
67
+ }
68
+ if (!nonEmpty(metadata.claudeSessionId)) throw new Error(`Session ${sessionId} has no Claude conversation id yet (it never ran a turn) \u2014 nothing to fork.`);
69
+ return { agent: "claude", directory, claudeSessionId: metadata.claudeSessionId, permissionMode, parentSessionId: sessionId };
70
+ }
71
+ async function forkProviderConversation(source) {
72
+ if (source.agent === "claude") {
73
+ try {
74
+ return { resumeClaudeSessionId: await forkSession(getProjectPath(source.directory), source.claudeSessionId) };
75
+ } catch (error) {
76
+ if (error instanceof ForkSourceMissingError) throw new Error("Claude session file not found on this machine");
77
+ throw error;
78
+ }
79
+ }
80
+ const client = new CodexAppServerClient();
81
+ await client.connect();
82
+ try {
83
+ const result = await forkCodexThread(client, { threadId: source.codexThreadId, cwd: source.directory });
84
+ return { resumeCodexThreadId: result.newCodexThreadId };
85
+ } finally {
86
+ await client.disconnect();
87
+ }
88
+ }
89
+
50
90
  function parseEnvAssignment(raw) {
51
91
  const eq = raw.indexOf("=");
52
92
  if (eq <= 0) throw new Error(`--env expects KEY=VALUE, got: ${raw}`);
@@ -96,6 +136,15 @@ function parseSpawnArgs(args) {
96
136
  if (value === void 0) throw new Error("--env requires a value");
97
137
  const [key, val] = parseEnvAssignment(value);
98
138
  options.env = { ...options.env ?? {}, [key]: val };
139
+ } else if (arg === "--model" || arg === "-m") {
140
+ const value = args[++i];
141
+ if (value === void 0 || value.trim().length === 0) throw new Error("--model requires a value");
142
+ options.model = value;
143
+ } else if (arg === "--fork") {
144
+ const value = args[++i];
145
+ if (value === void 0) throw new Error("--fork requires a session id");
146
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(value)) throw new Error(`Invalid session id for --fork: ${value}`);
147
+ options.fork = value;
99
148
  } else if (arg === "--json") {
100
149
  options.json = true;
101
150
  } else if (arg === "--help" || arg === "-h") {
@@ -116,7 +165,9 @@ ${chalk.bold("very-happy spawn")} - Spawn a remote session via the local daemon
116
165
  ${chalk.bold("Usage:")}
117
166
  very-happy spawn --dir <path> [--prompt <text> | --prompt-file <file>]
118
167
  [--spawned-by <name>] [--permission-mode <mode>]
119
- [--agent <name>] [--env KEY=VALUE]... [--json]
168
+ [--agent <name>] [--model <id>] [--env KEY=VALUE]... [--json]
169
+ very-happy spawn --fork <sessionId> [--prompt \u2026] [--permission-mode <mode>]
170
+ [--model <id>] [--spawned-by <name>] [--json]
120
171
 
121
172
  ${chalk.bold("Options:")}
122
173
  --dir, -d <path> Working directory for the new session (required)
@@ -127,22 +178,34 @@ ${chalk.bold("Options:")}
127
178
  the web list and is searchable as #<name>.
128
179
  --permission-mode <m> Permission mode for the new session: default,
129
180
  acceptEdits, plan, yolo, bypassPermissions.
130
- WITHOUT this the session runs in 'default' and stops
131
- at the first un-allowlisted tool waiting for someone
132
- to approve \u2014 if nothing is watching, it just hangs.
133
- Unattended dispatchers want bypassPermissions.
181
+ Default = the web launcher's: yolo for claude /
182
+ codex / pi, 'default' for gemini / openclaw. A fork
183
+ keeps its source's mode. 'default' stops at the first
184
+ un-allowlisted tool until someone approves it (see
185
+ \`sessions approve\`).
134
186
  --agent <name> Backend to run: claude (default), codex, gemini,
135
187
  openclaw, pi (needs pi-acp on the daemon's PATH).
188
+ --model, -m <id> Model for the session, sent with the first message
189
+ (needs --prompt). Default = the web launcher's:
190
+ claude-opus-5-5 for claude (the machine default when
191
+ its Claude Code is too old for it); other agents use
192
+ their own default. 'default' = the machine default.
193
+ --fork <sessionId> Fork an existing session of THIS machine (same as the
194
+ web's fork): copies its conversation and continues
195
+ it in a new session, in the same directory and with
196
+ the same agent (so --dir / --agent are not needed).
136
197
  --env KEY=VALUE Extra environment for the session process. Repeatable.
137
198
  \${VAR} is expanded against the daemon's environment;
138
199
  an unresolved reference fails the spawn.
139
- --json Machine-readable output: {"sessionId", "url"}
200
+ --json Machine-readable output: {"sessionId", "url",
201
+ "permissionMode", \u2026}
140
202
  -h, --help Show this help
141
203
 
142
204
  ${chalk.bold("Behavior:")}
143
205
  Requires the Very Happy daemon to be running on this machine (same semantics
144
206
  as spawning from the web: an offline machine cannot spawn). Without
145
- --prompt / --prompt-file the session is spawned idle.
207
+ --prompt / --prompt-file the session is spawned idle. To collect the reply:
208
+ \`very-happy sessions read <id> --wait --answer\`.
146
209
 
147
210
  ${chalk.bold("Exit codes:")}
148
211
  0 success
@@ -150,9 +213,11 @@ ${chalk.bold("Exit codes:")}
150
213
  2 session spawned but first message failed (session URL still printed)
151
214
  `);
152
215
  }
153
- async function sendFirstMessage(sessionId, text) {
216
+ async function sendFirstMessage(sessionId, text, metaFor) {
154
217
  const persisted = await waitForSessionKey(sessionId, 15e3);
155
- await sendUserMessage(sessionId, persisted, text, "cli-spawn");
218
+ const meta = metaFor(persisted.metadata?.capabilities ?? null);
219
+ await sendUserMessage(sessionId, persisted, text, "cli-spawn", meta);
220
+ return meta;
156
221
  }
157
222
  async function handleSpawnCommand(args) {
158
223
  let options;
@@ -167,12 +232,32 @@ async function handleSpawnCommand(args) {
167
232
  printHelp();
168
233
  process.exit(0);
169
234
  }
170
- if (!options.dir) {
171
- console.error(chalk.red("Error:"), "--dir is required");
235
+ let forkSource = null;
236
+ if (options.fork !== void 0) {
237
+ try {
238
+ const persisted = readPersistedSessions()[options.fork];
239
+ const current = persisted ? await readSessionMetadata(options.fork, persisted) : null;
240
+ forkSource = resolveForkSource(options.fork, persisted && current ? { ...persisted, metadata: { ...persisted.metadata, ...current } } : persisted);
241
+ } catch (error) {
242
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
243
+ process.exit(1);
244
+ }
245
+ if (options.dir !== void 0 && resolve(options.dir) !== forkSource.directory) {
246
+ console.error(chalk.red("Error:"), `--dir ${resolve(options.dir)} differs from the forked session's directory ${forkSource.directory}; a fork always continues in its source directory.`);
247
+ process.exit(1);
248
+ }
249
+ if (options.agent !== void 0 && options.agent !== forkSource.agent) {
250
+ console.error(chalk.red("Error:"), `--agent ${options.agent} differs from the forked session's agent ${forkSource.agent}.`);
251
+ process.exit(1);
252
+ }
253
+ } else if (!options.dir) {
254
+ console.error(chalk.red("Error:"), "--dir is required (or --fork <sessionId>)");
172
255
  console.error(`Run ${chalk.cyan("very-happy spawn --help")} for usage.`);
173
256
  process.exit(1);
174
257
  }
175
- const directory = resolve(options.dir);
258
+ const directory = forkSource ? forkSource.directory : resolve(options.dir);
259
+ const agent = forkSource ? forkSource.agent : options.agent;
260
+ const permissionMode = options.permissionMode ?? forkSource?.permissionMode ?? resolveSpawnPermissionMode(agent, void 0);
176
261
  let prompt = options.prompt;
177
262
  if (options.promptFile !== void 0) {
178
263
  try {
@@ -186,6 +271,10 @@ async function handleSpawnCommand(args) {
186
271
  console.error(chalk.red("Error:"), "Prompt is empty");
187
272
  process.exit(1);
188
273
  }
274
+ if (options.model !== void 0 && prompt === void 0) {
275
+ console.error(chalk.red("Error:"), "--model needs --prompt / --prompt-file (the model is sent with the first message); for an idle session pass it later with `very-happy send --model`.");
276
+ process.exit(1);
277
+ }
189
278
  try {
190
279
  if (!statSync(directory).isDirectory()) {
191
280
  console.error(chalk.red("Error:"), `Not a directory: ${directory}`);
@@ -200,12 +289,23 @@ async function handleSpawnCommand(args) {
200
289
  console.error(`Start it with ${chalk.cyan("very-happy daemon start")} and retry.`);
201
290
  process.exit(1);
202
291
  }
203
- logger.debug(`[SPAWN CMD] Spawning session in ${directory}`);
292
+ let resumeIds = {};
293
+ if (forkSource) {
294
+ try {
295
+ resumeIds = await forkProviderConversation(forkSource);
296
+ } catch (error) {
297
+ console.error(chalk.red("Error:"), `Failed to fork session ${forkSource.parentSessionId}: ${error instanceof Error ? error.message : String(error)}`);
298
+ process.exit(1);
299
+ }
300
+ }
301
+ logger.debug(`[SPAWN CMD] Spawning session in ${directory}${forkSource ? ` (fork of ${forkSource.parentSessionId})` : ""}`);
204
302
  const result = await spawnDaemonSession(directory, void 0, {
205
303
  spawnedBy: options.spawnedBy,
206
- permissionMode: options.permissionMode,
207
- agent: options.agent,
208
- environmentVariables: options.env
304
+ permissionMode,
305
+ agent,
306
+ environmentVariables: options.env,
307
+ ...resumeIds,
308
+ ...forkSource ? { parentSessionId: forkSource.parentSessionId } : {}
209
309
  });
210
310
  if (result?.error || !result?.success || !result?.sessionId) {
211
311
  const message = result?.error || "Daemon returned no session ID";
@@ -214,24 +314,38 @@ async function handleSpawnCommand(args) {
214
314
  }
215
315
  const sessionId = result.sessionId;
216
316
  const url = sessionWebUrl(sessionId);
317
+ if (forkSource && result.resumed !== true) {
318
+ await stopDaemonSession(sessionId).catch(() => false);
319
+ console.error(chalk.red("Error:"), "The running daemon is too old to fork from the CLI (it spawned a fresh session, now stopped). Restart the daemon on this CLI version and retry.");
320
+ process.exit(1);
321
+ }
217
322
  let promptError = null;
323
+ let sentMeta = null;
218
324
  if (prompt !== void 0) {
219
325
  try {
220
- await sendFirstMessage(sessionId, prompt);
326
+ sentMeta = await sendFirstMessage(sessionId, prompt, (capabilities) => resolveFirstMessageMeta({
327
+ agent,
328
+ permissionMode,
329
+ explicitModel: options.model,
330
+ capabilities
331
+ }));
221
332
  } catch (error) {
222
333
  promptError = error instanceof Error ? error.message : String(error);
223
334
  }
224
335
  }
225
336
  if (options.json) {
226
- const payload = { sessionId, url };
337
+ const payload = { sessionId, url, permissionMode };
227
338
  if (options.spawnedBy !== void 0) {
228
339
  payload.spawnedBy = options.spawnedBy;
229
340
  }
230
- if (options.permissionMode !== void 0) {
231
- payload.permissionMode = options.permissionMode;
341
+ if (agent !== void 0) {
342
+ payload.agent = agent;
343
+ }
344
+ if (forkSource) {
345
+ payload.forkedFrom = forkSource.parentSessionId;
232
346
  }
233
- if (options.agent !== void 0) {
234
- payload.agent = options.agent;
347
+ if (sentMeta && sentMeta.model !== void 0) {
348
+ payload.model = sentMeta.model;
235
349
  }
236
350
  if (prompt !== void 0) {
237
351
  payload.promptDelivered = promptError === null;