@fnclaude/cli 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.ts CHANGED
@@ -27,34 +27,46 @@ import {
27
27
  type LlmClientFn,
28
28
  } from './autoname.js';
29
29
  import { parseArgs } from './argParser.js';
30
+ import {
31
+ brandResolved,
32
+ withPassthroughUpdate,
33
+ withResolved,
34
+ type InterceptedArgs,
35
+ type ResolvedArgs,
36
+ } from './args.js';
30
37
  import { buildArgv } from './argv.js';
31
- import { loadConfig } from './config.js';
38
+ import { loadConfig, type Config } from './config.js';
32
39
  import { handoffSocketPath, type HandoffSpec } from './handoff.js';
33
40
  import { helpText, version, wantsHelp, wantsVersion } from './help.js';
34
41
  import { loadHostAliases } from './hostAliases.js';
35
42
  import { expandTildePath } from './paths.js';
36
- import { loadPrompts } from './prompts.js';
43
+ import { loadPrompts, type PromptSet } from './prompts.js';
37
44
  import { detectCrossCwd, runWithPTY } from './pty.js';
38
45
  import { loadRepoSettings } from './repoSettings.js';
39
- import { Resolve } from './resolver.js';
46
+ import { Resolve, type RepoSettings, type ResolveDeps } from './resolver.js';
40
47
  import { sanitizeNamesInPassthrough } from './sanitize.js';
41
48
  import { runMCPServer } from './mcp/client.js';
42
49
  import { seedNoop } from './noop.js';
43
50
  import { silentRelaunch, silentRelaunchHandoff } from './silentRelaunch.js';
44
- import { applyWorktreeIntercept } from './worktree.js';
45
- import { flushWarnings, warn } from './warnings.js';
51
+ import { applyWorktreeIntercept, type GitRunner } from './worktree.js';
52
+ import { flushWarnings } from './warnings.js';
46
53
 
47
54
  /**
48
- * Pluggable seam set used by `run()`. Tests substitute in-memory implementations
49
- * to drive the orchestration without launching real subprocesses, dialing
50
- * real sockets, or touching the real filesystem.
55
+ * `RunIO` process-shaped seams. Streams, paths, the launch environment,
56
+ * and the external behaviour the pipeline depends on (claude binary
57
+ * lookup, PTY runner, relaunch, MCP dispatcher, noop seeder, autoname
58
+ * LLM call). Plus the *inner* dependency seams of the pipeline modules
59
+ * themselves — `gitRunner` (consumed by applyWorktreeIntercept) and
60
+ * `resolveDeps` (consumed by Resolve) — surfaced here so tests can swap
61
+ * the I/O each module does without a wrapper layer of outer functions.
51
62
  *
52
- * The default values (when the caller omits a field) resolve to the
53
- * production implementations imported above. Mirrors the Go `run()`'s
54
- * package-level `runMCPServerFn` / `gitRunner` indirections, expanded so the
55
- * full integration is testable without monkey-patching.
63
+ * Earlier shape had both outer (`RunDeps.applyWorktreeIntercept`) and
64
+ * inner (`applyWorktreeIntercept`'s `GitRunner` parameter) seams for the
65
+ * same boundary. Collapsed to the inner seam only — the outer one was
66
+ * dead weight in production (the function never varies) and in tests it
67
+ * just wrapped the inner seam with two extra lines of closure plumbing.
56
68
  */
57
- export interface RunDeps {
69
+ export interface RunIO {
58
70
  /** Source argv (typically `process.argv.slice(2)`). */
59
71
  argv?: readonly string[];
60
72
  /** Stream where the help/version/error text is written. */
@@ -64,6 +76,7 @@ export interface RunDeps {
64
76
  home?: string;
65
77
  /** Shell cwd at startup. */
66
78
  cwd?: string;
79
+
67
80
  /** PATH lookup for the claude binary; returns null when not found. */
68
81
  lookupClaude?: (name: string) => string | null;
69
82
  /** Override the run-with-pty step. */
@@ -72,24 +85,63 @@ export interface RunDeps {
72
85
  silentRelaunch?: typeof silentRelaunch;
73
86
  /** Override the silent-relaunch-handoff step. */
74
87
  silentRelaunchHandoff?: typeof silentRelaunchHandoff;
88
+ /** Override runMCPServer (the `mcp` subcommand dispatcher). */
89
+ runMCPServer?: typeof runMCPServer;
75
90
  /** Override seedNoop (best-effort dir seeder). */
76
91
  seedNoop?: typeof seedNoop;
77
92
  /** Override generateName for auto-name (skip the LLM call). */
78
93
  generateName?: typeof generateName;
79
- /** Override loadPrompts (skip disk lookup). */
80
- loadPrompts?: typeof loadPrompts;
81
- /** Override loadConfig (use a fixed config). */
82
- loadConfig?: typeof loadConfig;
83
- /** Override loadRepoSettings (skip the four-tier settings.json merge). */
84
- loadRepoSettings?: typeof loadRepoSettings;
85
- /** Override loadHostAliases. */
86
- loadHostAliases?: typeof loadHostAliases;
87
- /** Override Resolve. */
88
- resolve?: typeof Resolve;
89
- /** Override applyWorktreeIntercept. */
90
- applyWorktreeIntercept?: typeof applyWorktreeIntercept;
91
- /** Override runMCPServer (the `mcp` subcommand dispatcher). */
92
- runMCPServer?: typeof runMCPServer;
94
+
95
+ /**
96
+ * GitRunner for applyWorktreeIntercept. Tests that want to drive a
97
+ * fake `git worktree list` reply pass one here; production uses the
98
+ * module's `defaultGitRunner`.
99
+ */
100
+ gitRunner?: GitRunner;
101
+
102
+ /**
103
+ * Resolver I/O seams (path-exists check, gh CLI, clone). Tests pass a
104
+ * stub set so the resolver runs without touching the network or
105
+ * filesystem; production uses `productionDeps()` from resolver.ts.
106
+ */
107
+ resolveDeps?: ResolveDeps;
108
+ }
109
+
110
+ /**
111
+ * `RunConfig` — pre-loaded data the pipeline reads. When a field is
112
+ * supplied here, the corresponding loader (loadConfig / loadPrompts /
113
+ * loadRepoSettings / loadHostAliases) is skipped and the supplied value
114
+ * is used directly. Tests build a hermetic config payload up-front; in
115
+ * production every field is omitted and the loaders run for real.
116
+ *
117
+ * These were previously expressed as `loadConfig: typeof loadConfig`
118
+ * function seams in the unified `RunDeps`. The 1:1 thin-wrapper pattern
119
+ * was double-injection — in production they have zero variance, and in
120
+ * tests they were always loader stubs that returned a fixed payload.
121
+ * Storing the payload directly removes a layer of function plumbing.
122
+ */
123
+ export interface RunConfig {
124
+ /** Pre-loaded config. Omit to call `loadConfig()`. */
125
+ config?: Config;
126
+ /** Pre-loaded prompts. Omit to call `loadPrompts()`. */
127
+ prompts?: PromptSet;
128
+ /** Pre-loaded repo settings. Omit to call `loadRepoSettings(home, cwd)`. */
129
+ repoSettings?: RepoSettings;
130
+ /** Pre-loaded host aliases. Omit to call `loadHostAliases(home)`. */
131
+ hostAliases?: Record<string, string>;
132
+ }
133
+
134
+ /**
135
+ * Top-level deps for `run()` — two named groups (`io` and `data`),
136
+ * each optional, each with optional fields. Tests typically populate
137
+ * only the fields they care about. Production omits everything (passes
138
+ * `{}` or nothing) and lets every default kick in.
139
+ */
140
+ export interface RunDeps {
141
+ /** Process-shaped seams (streams, env, external behaviours). */
142
+ io?: RunIO;
143
+ /** Pre-loaded data payloads (skips the corresponding loaders). */
144
+ data?: RunConfig;
93
145
  }
94
146
 
95
147
  function lookupClaudeFromPath(name: string): string | null {
@@ -117,34 +169,37 @@ function lookupClaudeFromPath(name: string): string | null {
117
169
  * through to returning claude's exit code, mirroring Go's behavior.
118
170
  */
119
171
  export async function run(deps: RunDeps = {}): Promise<number> {
120
- const argv = deps.argv ?? process.argv.slice(2);
121
- const stdout = deps.stdout ?? process.stdout;
122
- const stderr = deps.stderr ?? process.stderr;
123
- const home = deps.home ?? process.env.HOME ?? homedir();
124
- const shellCWD = deps.cwd ?? process.cwd();
125
- const lookupClaude = deps.lookupClaude ?? lookupClaudeFromPath;
126
- const runPTY = deps.runWithPTY ?? runWithPTY;
127
- const relaunch = deps.silentRelaunch ?? silentRelaunch;
128
- const relaunchHandoff = deps.silentRelaunchHandoff ?? silentRelaunchHandoff;
129
- const seedNoopFn = deps.seedNoop ?? seedNoop;
130
- const generateNameFn = deps.generateName ?? generateName;
131
- const loadPromptsFn = deps.loadPrompts ?? loadPrompts;
132
- const loadConfigFn = deps.loadConfig ?? loadConfig;
133
- const loadRepoSettingsFn = deps.loadRepoSettings ?? loadRepoSettings;
134
- const loadHostAliasesFn = deps.loadHostAliases ?? loadHostAliases;
135
- const resolveFn = deps.resolve ?? Resolve;
136
- const applyWorktreeInterceptFn = deps.applyWorktreeIntercept ?? applyWorktreeIntercept;
137
- const runMCPServerFn = deps.runMCPServer ?? runMCPServer;
172
+ const io = deps.io ?? {};
173
+ const data = deps.data ?? {};
174
+
175
+ const argv = io.argv ?? process.argv.slice(2);
176
+ const stdout = io.stdout ?? process.stdout;
177
+ const stderr = io.stderr ?? process.stderr;
178
+ const home = io.home ?? process.env.HOME ?? homedir();
179
+ const shellCWD = io.cwd ?? process.cwd();
180
+ const lookupClaude = io.lookupClaude ?? lookupClaudeFromPath;
181
+ const runPTY = io.runWithPTY ?? runWithPTY;
182
+ const relaunch = io.silentRelaunch ?? silentRelaunch;
183
+ const relaunchHandoff = io.silentRelaunchHandoff ?? silentRelaunchHandoff;
184
+ const seedNoopFn = io.seedNoop ?? seedNoop;
185
+ const generateNameFn = io.generateName ?? generateName;
186
+ const runMCPServerFn = io.runMCPServer ?? runMCPServer;
138
187
 
139
188
  // Defer-flush warnings on exit, AFTER claude has finished and the user is
140
189
  // back at their shell. The silent-relaunch path uses execve which skips
141
190
  // this defer; that's intentional — the relaunched fnclaude will re-emit
142
191
  // any warnings that still apply.
192
+ //
193
+ // Loaders (loadConfig / loadRepoSettings / loadHostAliases / loadPrompts)
194
+ // and other setup steps return their warnings; we accumulate them in this
195
+ // local list and drain it via flushWarnings at the deferred-flush point.
196
+ // No module-global sink — keeps tests hermetic.
197
+ const warnings: string[] = [];
143
198
  let flushed = false;
144
199
  const flushOnce = (): void => {
145
200
  if (flushed) return;
146
201
  flushed = true;
147
- flushWarnings(stderr);
202
+ flushWarnings(warnings, stderr);
148
203
  };
149
204
 
150
205
  try {
@@ -173,90 +228,156 @@ export async function run(deps: RunDeps = {}): Promise<number> {
173
228
  }
174
229
 
175
230
  // ── Parse fnclaude's own argv. ────────────────────────────────────────
176
- let a;
231
+ let parsed;
177
232
  try {
178
- a = parseArgs(argv, home);
233
+ parsed = parseArgs(argv, home);
179
234
  } catch (err) {
180
235
  stderr.write(`${(err as Error).message}\n`);
181
236
  return 1;
182
237
  }
183
238
 
184
239
  // ── Seed the noop dir iff fallback was used. ─────────────────────────
185
- if (a.usedNoopFallback) {
240
+ if (parsed.usedNoopFallback) {
186
241
  try {
187
- await seedNoopFn(a.cwd);
242
+ await seedNoopFn(parsed.cwd);
188
243
  } catch (err) {
189
- warn(`fnclaude: noop seed failed: ${(err as Error).message}`);
244
+ warnings.push(`fnclaude: noop seed failed: ${(err as Error).message}`);
190
245
  }
191
246
  }
192
247
 
193
- const cfg = loadConfigFn();
248
+ // ── Config: pre-loaded or freshly loaded from disk. ──────────────────
249
+ let cfg: Config;
250
+ if (data.config !== undefined) {
251
+ cfg = data.config;
252
+ } else {
253
+ const loaded = loadConfig();
254
+ cfg = loaded.config;
255
+ warnings.push(...loaded.warnings);
256
+ }
194
257
 
195
258
  // ── Repo-reference resolver (path-or-repo two-lookup). ───────────────
259
+ //
260
+ // Produces a `ResolvedArgs` either way — the resolver path overwrites
261
+ // cwd (and possibly worktreeSet/worktreeArg), the tilde-only path
262
+ // expands cwd, and the absolute-path / noop-fallback path stamps the
263
+ // existing fields straight through.
264
+ let resolved: ResolvedArgs;
196
265
  if (
197
- !a.usedNoopFallback &&
198
- a.cwd !== '' &&
199
- !isAbsolute(a.cwd) &&
200
- !a.cwd.startsWith('~')
266
+ !parsed.usedNoopFallback &&
267
+ parsed.cwd !== '' &&
268
+ !isAbsolute(parsed.cwd) &&
269
+ !parsed.cwd.startsWith('~')
201
270
  ) {
202
- const rs = loadRepoSettingsFn(home, shellCWD);
203
- const aliases = loadHostAliasesFn(home);
271
+ let rs: RepoSettings;
272
+ if (data.repoSettings !== undefined) {
273
+ rs = data.repoSettings;
274
+ } else {
275
+ const loaded = loadRepoSettings(home, shellCWD);
276
+ rs = loaded.settings;
277
+ warnings.push(...loaded.warnings);
278
+ }
279
+ let aliases: Record<string, string>;
280
+ if (data.hostAliases !== undefined) {
281
+ aliases = data.hostAliases;
282
+ } else {
283
+ const loaded = loadHostAliases(home);
284
+ aliases = loaded.aliases;
285
+ warnings.push(...loaded.warnings);
286
+ }
287
+ let result;
204
288
  try {
205
- const result = await resolveFn({
206
- input: a.cwd,
207
- cwd: shellCWD,
208
- home,
209
- settings: rs,
210
- hostAliases: aliases,
211
- });
212
- a.cwd = result.path;
213
- // If the user's reference had a +workspace suffix AND they didn't
214
- // pass -w explicitly, propagate the workspace to the intercept
215
- // layer.
216
- if (result.workspace !== undefined && result.workspace !== '' && !a.worktreeSet) {
217
- a.worktreeSet = true;
218
- a.worktreeArg = result.workspace;
219
- }
289
+ // Resolver's inner deps (path-exists, gh CLI, clone): use the
290
+ // injected ones in tests, fall back to productionDeps in real
291
+ // runs. Passing `undefined` lets Resolve default-construct
292
+ // productionDeps() itself.
293
+ result = await (io.resolveDeps
294
+ ? Resolve(
295
+ {
296
+ input: parsed.cwd,
297
+ cwd: shellCWD,
298
+ home,
299
+ settings: rs,
300
+ hostAliases: aliases,
301
+ },
302
+ io.resolveDeps,
303
+ )
304
+ : Resolve({
305
+ input: parsed.cwd,
306
+ cwd: shellCWD,
307
+ home,
308
+ settings: rs,
309
+ hostAliases: aliases,
310
+ }));
220
311
  } catch (err) {
221
312
  stderr.write(`${(err as Error).message}\n`);
222
313
  return 1;
223
314
  }
224
- } else if (a.cwd.startsWith('~')) {
315
+ // If the user's reference had a +workspace suffix AND they didn't
316
+ // pass -w explicitly, propagate the workspace to the intercept
317
+ // layer.
318
+ const promoteWorkspace =
319
+ result.workspace !== undefined && result.workspace !== '' && !parsed.worktreeSet;
320
+ resolved = withResolved(parsed, {
321
+ cwd: result.path,
322
+ ...(promoteWorkspace
323
+ ? { worktreeSet: true, worktreeArg: result.workspace! }
324
+ : {}),
325
+ });
326
+ } else if (parsed.cwd.startsWith('~')) {
225
327
  // Tilde-expand absolute-shaped inputs that didn't go through the
226
328
  // resolver (resolver expands tildes for its short-circuit path, but
227
329
  // it isn't called for tilde-prefixed inputs here).
228
- a.cwd = expandTildePath(a.cwd);
330
+ resolved = withResolved(parsed, { cwd: expandTildePath(parsed.cwd) });
331
+ } else {
332
+ resolved = brandResolved(parsed);
229
333
  }
230
334
 
231
335
  // ── -w / --worktree intercept. ───────────────────────────────────────
232
- applyWorktreeInterceptFn(a, shellCWD);
336
+ //
337
+ // GitRunner is the inner seam — production uses the module's default
338
+ // (synchronous `git -C <dir> ...`); tests inject a stub that yields
339
+ // the fake `git worktree list --porcelain` shape they want.
340
+ const intercepted = io.gitRunner
341
+ ? applyWorktreeIntercept(resolved, shellCWD, io.gitRunner)
342
+ : applyWorktreeIntercept(resolved, shellCWD);
233
343
 
234
344
  // ── Resolve the launch cwd relative to shell cwd. ────────────────────
235
- const launchCWD = isAbsolute(a.cwd) ? a.cwd : join(shellCWD, a.cwd);
345
+ const launchCWD = isAbsolute(intercepted.cwd)
346
+ ? intercepted.cwd
347
+ : join(shellCWD, intercepted.cwd);
236
348
 
237
349
  // ── Auto-name if qualifying. ──────────────────────────────────────────
238
- if (shouldAutoName(a.passthrough)) {
239
- const prompt = extractPrompt(a.passthrough);
350
+ let named: InterceptedArgs = intercepted;
351
+ if (shouldAutoName(named.passthrough)) {
352
+ const prompt = extractPrompt(named.passthrough);
240
353
  const apiKey = process.env.ANTHROPIC_API_KEY ?? '';
241
354
  let llmFn: LlmClientFn | undefined;
242
355
  if (apiKey !== '') llmFn = defaultLlmClient(apiKey);
243
356
  else llmFn = claudeCliFn(cfg.name.model);
244
357
  const name = await generateNameFn(prompt, cfg.name, apiKey, llmFn);
245
- a.passthrough = ['--name', name, ...a.passthrough];
358
+ named = withPassthroughUpdate(named, {
359
+ passthrough: ['--name', name, ...named.passthrough],
360
+ });
246
361
  }
247
362
 
248
363
  // ── Sanitize any --name / -n value to a path-safe slug. ──────────────
249
- {
250
- const { args: sanitized, warnings } = sanitizeNamesInPassthrough(a.passthrough);
251
- a.passthrough = sanitized;
252
- for (const w of warnings) warn(w);
253
- }
364
+ const sanitizeResult = sanitizeNamesInPassthrough(named.passthrough);
365
+ warnings.push(...sanitizeResult.warnings);
366
+ const sanitized = withPassthroughUpdate(named, {
367
+ passthrough: sanitizeResult.args,
368
+ });
254
369
 
255
370
  // ── Build the claude argv. ───────────────────────────────────────────
256
- const promptsResult = loadPromptsFn();
257
- for (const w of promptsResult.warnings) warn(w);
371
+ let prompts: PromptSet;
372
+ if (data.prompts !== undefined) {
373
+ prompts = data.prompts;
374
+ } else {
375
+ const loaded = loadPrompts();
376
+ prompts = loaded.prompts;
377
+ warnings.push(...loaded.warnings);
378
+ }
258
379
 
259
- const claudeArgv = buildArgv(a, shellCWD, cfg, promptsResult.prompts);
380
+ const claudeArgv = buildArgv(sanitized, shellCWD, cfg, prompts);
260
381
 
261
382
  // ── Verify claude is on PATH before starting the PTY. ────────────────
262
383
  if (lookupClaude('claude') === null) {
package/src/mcp/client.ts CHANGED
@@ -14,10 +14,13 @@ import { connect, type Socket } from 'node:net';
14
14
  import type { Readable, Writable } from 'node:stream';
15
15
  import {
16
16
  encodeRequest,
17
- type Op,
18
17
  readResponse,
18
+ type CopyRequest,
19
19
  type Request,
20
20
  type Response,
21
+ type RestartRequest,
22
+ type SpawnRequest,
23
+ type SwitchRequest,
21
24
  } from './protocol.js';
22
25
  import pkg from '../../package.json' with { type: 'json' };
23
26
 
@@ -420,8 +423,8 @@ async function callRestart(
420
423
  );
421
424
  return;
422
425
  }
423
- const req: Request = {
424
- op: 'restart' satisfies Op,
426
+ const req: RestartRequest = {
427
+ op: 'restart',
425
428
  session_id: sid,
426
429
  model: readStringArg(args, 'model'),
427
430
  effort: readStringArg(args, 'effort'),
@@ -446,8 +449,8 @@ async function callSwitch(
446
449
  sendToolError(opts.stdout, id, SOCKET_UNAVAILABLE_MSG);
447
450
  return;
448
451
  }
449
- const req: Request = {
450
- op: 'switch' satisfies Op,
452
+ const req: SwitchRequest = {
453
+ op: 'switch',
451
454
  destination: readStringArg(args, 'destination'),
452
455
  name: readStringArg(args, 'name'),
453
456
  summary: readStringArg(args, 'summary'),
@@ -476,8 +479,8 @@ async function callSpawn(
476
479
  sendToolError(opts.stdout, id, SOCKET_UNAVAILABLE_MSG);
477
480
  return;
478
481
  }
479
- const req: Request = {
480
- op: 'spawn' satisfies Op,
482
+ const req: SpawnRequest = {
483
+ op: 'spawn',
481
484
  destination: readStringArg(args, 'destination'),
482
485
  name: readStringArg(args, 'name'),
483
486
  summary: readStringArg(args, 'summary'),
@@ -505,8 +508,8 @@ async function callCopy(
505
508
  sendToolError(opts.stdout, id, SOCKET_UNAVAILABLE_MSG);
506
509
  return;
507
510
  }
508
- const req: Request = {
509
- op: 'copy_to_clipboard' satisfies Op,
511
+ const req: CopyRequest = {
512
+ op: 'copy_to_clipboard',
510
513
  text: readStringArg(args, 'text'),
511
514
  };
512
515
  await dialAndRelay(opts, dial, id, req);
@@ -76,50 +76,90 @@ export const ActionError: Action = 'error';
76
76
  // ── Request ────────────────────────────────────────────────────────────────
77
77
 
78
78
  /**
79
- * Request sent from the MCP subprocess to the parent listener.
79
+ * Shared override fields applicable to OpRestart / OpSwitch / OpSpawn.
80
+ *
81
+ * Empty/undef string values mean "preserve what was on the original
82
+ * command line" (for restart/transfer) or "don't pass this flag" (for
83
+ * spawn). For boolean fields: undefined = preserve existing; true =
84
+ * ensure present; false = ensure absent.
85
+ */
86
+ export interface RequestOverrides {
87
+ model?: string;
88
+ effort?: string;
89
+ permission_mode?: string;
90
+ allowed_tools?: string;
91
+ agent?: string;
92
+ brief?: boolean | null;
93
+ chrome?: boolean | null;
94
+ ide?: boolean | null;
95
+ verbose?: boolean | null;
96
+ }
97
+
98
+ /**
99
+ * Discriminated union of Request variants — exactly one shape per Op.
100
+ * Adding a new Op requires extending this union *and* every `switch`
101
+ * over `req.op` (the dispatcher uses an exhaustive-never check so the
102
+ * compiler enforces this).
80
103
  *
81
104
  * The wire shape uses snake_case keys (matching Go's `json:` tags). All
82
105
  * non-required fields are optional on the wire; the listener tolerates
83
106
  * missing keys.
84
- *
85
- * Override fields (Model/Effort/PermissionMode/AllowedTools/Agent and the
86
- * four bools) are applicable to OpRestart/OpSwitch/OpSpawn. Empty/undef
87
- * string values mean "preserve what was on the original command line"
88
- * (for restart/transfer) or "don't pass this flag" (for spawn). For
89
- * boolean fields: undefined = preserve existing; true = ensure present;
90
- * false = ensure absent.
91
107
  */
92
- export interface Request {
93
- op: Op;
108
+ export type Request =
109
+ | RestartRequest
110
+ | SwitchRequest
111
+ | SpawnRequest
112
+ | CopyRequest;
94
113
 
95
- /** OpRestart: required UUID — the current Claude session. */
114
+ /** OpRestart: restart the current session in place. */
115
+ export interface RestartRequest extends RequestOverrides {
116
+ op: 'restart';
117
+ /** Required UUID — the current Claude session. */
118
+ session_id?: string;
119
+ }
120
+
121
+ /** OpSwitch: kill claude and relaunch at destination. */
122
+ export interface SwitchRequest extends RequestOverrides {
123
+ op: 'switch';
124
+ /** Destination project ref. */
125
+ destination?: string;
126
+ /** 3-6 word kebab-case session topic. */
127
+ name?: string;
128
+ /** Continuity summary content. */
129
+ summary?: string;
130
+ /**
131
+ * Optional UUID for live-permission-mode auto-capture. Used to read the
132
+ * session JSONL when no explicit permission_mode override was set.
133
+ */
96
134
  session_id?: string;
135
+ /**
136
+ * Deprecated; no longer read by the server. Left on the type so older
137
+ * clients that still serialize confirmed=true don't break.
138
+ */
139
+ confirmed?: boolean;
140
+ }
97
141
 
98
- /** OpSwitch / OpSpawn: destination project ref. */
142
+ /** OpSpawn: launch a sibling fnclaude in a new window. */
143
+ export interface SpawnRequest extends RequestOverrides {
144
+ op: 'spawn';
145
+ /** Destination project ref. */
99
146
  destination?: string;
100
- /** OpSwitch / OpSpawn: 3-6 word kebab-case session topic. */
147
+ /** 3-6 word kebab-case session topic. */
101
148
  name?: string;
102
- /** OpSwitch / OpSpawn: continuity summary content. */
149
+ /** Continuity summary content. */
103
150
  summary?: string;
104
151
  /**
105
152
  * Deprecated; no longer read by the server. Left on the type so older
106
153
  * clients that still serialize confirmed=true don't break.
107
154
  */
108
155
  confirmed?: boolean;
156
+ }
109
157
 
110
- /** OpCopy: text to write to the clipboard. */
158
+ /** OpCopy: write text to the clipboard. Carries no override fields. */
159
+ export interface CopyRequest {
160
+ op: 'copy_to_clipboard';
161
+ /** Text to write to the clipboard. */
111
162
  text?: string;
112
-
113
- // Overrides.
114
- model?: string;
115
- effort?: string;
116
- permission_mode?: string;
117
- allowed_tools?: string;
118
- agent?: string;
119
- brief?: boolean | null;
120
- chrome?: boolean | null;
121
- ide?: boolean | null;
122
- verbose?: boolean | null;
123
163
  }
124
164
 
125
165
  // ── Response ───────────────────────────────────────────────────────────────