@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/README.md +215 -27
- package/package.json +1 -1
- package/src/argParser.ts +19 -32
- package/src/args/preserve.ts +2 -2
- package/src/args.ts +200 -23
- package/src/argv.ts +18 -19
- package/src/config.ts +85 -52
- package/src/help.ts +1 -1
- package/src/hostAliases.ts +40 -15
- package/src/index.ts +11 -5
- package/src/main.ts +209 -88
- package/src/mcp/client.ts +12 -9
- package/src/mcp/protocol.ts +66 -26
- package/src/mcp/socketListener.ts +20 -10
- package/src/passthrough.ts +36 -0
- package/src/paths.ts +31 -0
- package/src/prompts.ts +5 -12
- package/src/pty/unix.ts +250 -107
- package/src/pty.ts +20 -13
- package/src/repoSettings.ts +35 -11
- package/src/silentRelaunch.ts +3 -3
- package/src/spawn.ts +2 -28
- package/src/warnings.ts +15 -31
- package/src/worktree.ts +57 -43
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
|
|
51
|
+
import { applyWorktreeIntercept, type GitRunner } from './worktree.js';
|
|
52
|
+
import { flushWarnings } from './warnings.js';
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
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
|
|
231
|
+
let parsed;
|
|
177
232
|
try {
|
|
178
|
-
|
|
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 (
|
|
240
|
+
if (parsed.usedNoopFallback) {
|
|
186
241
|
try {
|
|
187
|
-
await seedNoopFn(
|
|
242
|
+
await seedNoopFn(parsed.cwd);
|
|
188
243
|
} catch (err) {
|
|
189
|
-
|
|
244
|
+
warnings.push(`fnclaude: noop seed failed: ${(err as Error).message}`);
|
|
190
245
|
}
|
|
191
246
|
}
|
|
192
247
|
|
|
193
|
-
|
|
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
|
-
!
|
|
198
|
-
|
|
199
|
-
!isAbsolute(
|
|
200
|
-
!
|
|
266
|
+
!parsed.usedNoopFallback &&
|
|
267
|
+
parsed.cwd !== '' &&
|
|
268
|
+
!isAbsolute(parsed.cwd) &&
|
|
269
|
+
!parsed.cwd.startsWith('~')
|
|
201
270
|
) {
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
+
resolved = withResolved(parsed, { cwd: expandTildePath(parsed.cwd) });
|
|
331
|
+
} else {
|
|
332
|
+
resolved = brandResolved(parsed);
|
|
229
333
|
}
|
|
230
334
|
|
|
231
335
|
// ── -w / --worktree intercept. ───────────────────────────────────────
|
|
232
|
-
|
|
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(
|
|
345
|
+
const launchCWD = isAbsolute(intercepted.cwd)
|
|
346
|
+
? intercepted.cwd
|
|
347
|
+
: join(shellCWD, intercepted.cwd);
|
|
236
348
|
|
|
237
349
|
// ── Auto-name if qualifying. ──────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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(
|
|
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:
|
|
424
|
-
op: 'restart'
|
|
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:
|
|
450
|
-
op: 'switch'
|
|
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:
|
|
480
|
-
op: 'spawn'
|
|
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:
|
|
509
|
-
op: 'copy_to_clipboard'
|
|
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);
|
package/src/mcp/protocol.ts
CHANGED
|
@@ -76,50 +76,90 @@ export const ActionError: Action = 'error';
|
|
|
76
76
|
// ── Request ────────────────────────────────────────────────────────────────
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
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
|
|
93
|
-
|
|
108
|
+
export type Request =
|
|
109
|
+
| RestartRequest
|
|
110
|
+
| SwitchRequest
|
|
111
|
+
| SpawnRequest
|
|
112
|
+
| CopyRequest;
|
|
94
113
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
147
|
+
/** 3-6 word kebab-case session topic. */
|
|
101
148
|
name?: string;
|
|
102
|
-
/**
|
|
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
|
-
|
|
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 ───────────────────────────────────────────────────────────────
|