@fnclaude/cli 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fnc.js +34 -79
- package/package.json +6 -9
- package/share/fnclaude/templates/handoff.template.md +11 -0
- package/src/argv/classify.ts +48 -0
- package/src/argv/expand.ts +51 -0
- package/src/argv/intake.ts +52 -0
- package/src/argv/magic.ts +103 -0
- package/src/argv/parse.ts +213 -0
- package/src/argv/preserve-args.ts +333 -0
- package/src/argv/sentinel.ts +41 -0
- package/src/argv/short-flags.ts +152 -0
- package/src/config/load.ts +116 -0
- package/src/handoff/awaiter.ts +140 -0
- package/src/handoff/clean-env.ts +45 -0
- package/src/handoff/kill-and-exec.ts +110 -0
- package/src/handoff/spawn-launcher.ts +185 -0
- package/src/handoff/summary-file.ts +86 -0
- package/src/handoff/trigger.ts +90 -0
- package/src/help-version.ts +151 -0
- package/src/launch/compose-env.ts +34 -0
- package/src/launch/cross-cwd-parse.ts +69 -0
- package/src/launch/cross-cwd-relaunch.ts +95 -0
- package/src/launch/find-claude.ts +52 -0
- package/src/launch/live-permission-reader.ts +133 -0
- package/src/launch/ring-buffer.ts +92 -0
- package/src/main.ts +580 -437
- package/src/mcp/dispatch.ts +240 -0
- package/src/mcp/handlers/clipboard-backends.ts +176 -0
- package/src/mcp/handlers/clipboard.ts +62 -0
- package/src/mcp/handlers/restart.ts +156 -0
- package/src/mcp/handlers/spawn.ts +219 -0
- package/src/mcp/handlers/switch.ts +272 -0
- package/src/mcp/inject-config.ts +59 -0
- package/src/mcp/jsonrpc-server.ts +154 -0
- package/src/mcp/listener.ts +141 -0
- package/src/mcp/parent-dispatch.ts +154 -0
- package/src/mcp/socket-path.ts +48 -0
- package/src/mcp/wire.ts +181 -0
- package/src/name/auto-name.ts +162 -0
- package/src/name/llm-prompt.ts +14 -0
- package/src/name/sanitize.ts +57 -0
- package/src/name/sdk-llm.ts +42 -0
- package/src/noop/seed.ts +63 -0
- package/src/noop/template-source.ts +62 -0
- package/src/path/ensure-cwd.ts +95 -0
- package/src/path/resolve.ts +58 -0
- package/src/prompts/dir.ts +61 -0
- package/src/prompts/load.ts +100 -0
- package/src/prompts/select.ts +43 -0
- package/src/repo/clone-exec.ts +37 -0
- package/src/repo/clone.ts +45 -0
- package/src/repo/gh-runner.ts +68 -0
- package/src/repo/host-aliases.ts +58 -0
- package/src/repo/owner-lookup.ts +71 -0
- package/src/repo/ref.ts +146 -0
- package/src/repo/repo-settings.ts +99 -0
- package/src/repo/resolve-input.ts +179 -0
- package/src/repo/template.ts +92 -0
- package/src/warnings/buffer.ts +39 -0
- package/src/worktree/auto-tmux.ts +45 -0
- package/src/worktree/git-list.ts +73 -0
- package/src/worktree/intercept.ts +150 -0
- package/bin/preflight.js +0 -66
- package/prompts/agent-pitfall.md +0 -1
- package/prompts/noop-router.md +0 -186
- package/prompts/project-switch.md +0 -64
- package/prompts/restart.md +0 -50
- package/prompts/spawn.md +0 -62
- package/src/argParser.ts +0 -367
- package/src/args/preserve.ts +0 -338
- package/src/args.ts +0 -239
- package/src/argv.ts +0 -203
- package/src/autoname.ts +0 -273
- package/src/clipboard.ts +0 -149
- package/src/config.ts +0 -369
- package/src/errors.ts +0 -13
- package/src/handoff.ts +0 -108
- package/src/help.ts +0 -139
- package/src/hostAliases.ts +0 -139
- package/src/index.ts +0 -120
- package/src/mcp/client.ts +0 -645
- package/src/mcp/protocol.ts +0 -445
- package/src/mcp/socketListener.ts +0 -540
- package/src/noop.ts +0 -106
- package/src/passthrough.ts +0 -36
- package/src/paths.ts +0 -55
- package/src/prompts.ts +0 -279
- package/src/pty/unix.ts +0 -429
- package/src/pty/windows.ts +0 -125
- package/src/pty.ts +0 -380
- package/src/repoRef.ts +0 -158
- package/src/repoSettings.ts +0 -144
- package/src/resolver.ts +0 -519
- package/src/sanitize.ts +0 -120
- package/src/sessionState.ts +0 -220
- package/src/silentRelaunch.ts +0 -178
- package/src/spawn.ts +0 -163
- package/src/template.ts +0 -44
- package/src/warnings.ts +0 -34
- package/src/worktree.ts +0 -201
package/src/argParser.ts
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bespoke argv parser for fnclaude — ported from the Go implementation at
|
|
3
|
-
* src/main.go in the upstream Go repo. Hand-rolled (no commander/cac) because
|
|
4
|
-
* fnclaude's magic-positional rules don't map cleanly onto generic parsers.
|
|
5
|
-
*
|
|
6
|
-
* Phases, in order:
|
|
7
|
-
* 1. Subcommand expansion (resume/res/continue/con/fork/fk → long flags)
|
|
8
|
-
* 2. Short-flag translation (-B → --brief, -Gval → --agent val, etc.)
|
|
9
|
-
* 3. Magic-word positional (model + effort aliases consumed off the front)
|
|
10
|
-
* 4. Positional consumption (path, optional worktree name)
|
|
11
|
-
* 5. Pass-through (unrecognized flags forwarded to claude)
|
|
12
|
-
*
|
|
13
|
-
* See src/main.go in the Go repo for the canonical spec and src/main_test.go
|
|
14
|
-
* + src/subcommand_test.go + src/positional_worktree_test.go for behavior
|
|
15
|
-
* the TS port preserves.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { join as pathJoin } from 'node:path';
|
|
19
|
-
import { brandParsed, type ParsedArgs } from './args.js';
|
|
20
|
-
|
|
21
|
-
// ── Magic-word vocabularies ────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
const MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku']);
|
|
24
|
-
const EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
25
|
-
|
|
26
|
-
// Capital short flags with no value.
|
|
27
|
-
const SHORT_NO_VALUE: Record<string, string> = {
|
|
28
|
-
B: '--brief',
|
|
29
|
-
C: '--chrome',
|
|
30
|
-
D: '--dangerously-skip-permissions',
|
|
31
|
-
F: '--fork-session',
|
|
32
|
-
I: '--ide',
|
|
33
|
-
V: '--verbose',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Capital short flags that REQUIRE a value (next argv token or =val).
|
|
37
|
-
const SHORT_REQUIRED: Record<string, string> = {
|
|
38
|
-
G: '--agent',
|
|
39
|
-
M: '--permission-mode',
|
|
40
|
-
W: '--allowedTools',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Capital short flags that optionally take a value (greedy; only consumed
|
|
44
|
-
// when the next token is non-flag).
|
|
45
|
-
const SHORT_OPTIONAL: Record<string, string> = {
|
|
46
|
-
P: '--from-pr',
|
|
47
|
-
R: '--remote-control',
|
|
48
|
-
T: '--tmux',
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Subcommand-style positionals → long-flag expansion. `fork` includes
|
|
52
|
-
// --resume because --fork-session requires it on claude's side.
|
|
53
|
-
const SUBCOMMAND_FLAGS: Record<string, readonly string[]> = {
|
|
54
|
-
resume: ['--resume'],
|
|
55
|
-
res: ['--resume'],
|
|
56
|
-
continue: ['--continue'],
|
|
57
|
-
con: ['--continue'],
|
|
58
|
-
fork: ['--resume', '--fork-session'],
|
|
59
|
-
fk: ['--resume', '--fork-session'],
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Return the default cwd for invocations with no positional path —
|
|
66
|
-
* `$XDG_CONFIG_HOME/fnclaude/noop`, falling back to `$home/.config/fnclaude/noop`.
|
|
67
|
-
* Matches Go's `defaultNoopDir`.
|
|
68
|
-
*/
|
|
69
|
-
export function defaultNoopDir(home: string): string {
|
|
70
|
-
const base = process.env.XDG_CONFIG_HOME || pathJoin(home, '.config');
|
|
71
|
-
return pathJoin(base, 'fnclaude', 'noop');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Look up the long form of a value-taking short flag char.
|
|
76
|
-
*/
|
|
77
|
-
function valueShortLong(ch: string): string | undefined {
|
|
78
|
-
return SHORT_REQUIRED[ch] ?? SHORT_OPTIONAL[ch];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface ParseShortFlagResult {
|
|
82
|
-
tokens: string[];
|
|
83
|
-
consumed: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Parse a single short-flag token like `-B`, `-BVC`, `-G=val`, `-G`, `-Gval`.
|
|
88
|
-
*
|
|
89
|
-
* Returns the long-form tokens to emit and the number of extra argv elements
|
|
90
|
-
* consumed from `rest` (the slice starting AFTER the current token). The
|
|
91
|
-
* caller is responsible for advancing its own cursor by `1 + consumed`.
|
|
92
|
-
*
|
|
93
|
-
* Throws on:
|
|
94
|
-
* - A required-value short flag in the middle of a collapsed group
|
|
95
|
-
* - A required-value short flag at EOF with no value
|
|
96
|
-
* - A required-value short flag followed by another flag
|
|
97
|
-
*/
|
|
98
|
-
export function parseShortFlag(arg: string, rest: readonly string[]): ParseShortFlagResult {
|
|
99
|
-
// Strip the leading '-'.
|
|
100
|
-
const body = arg.slice(1);
|
|
101
|
-
|
|
102
|
-
// -X=val form: only valid for known value-taking single-char flags. Anything
|
|
103
|
-
// else falls through and we emit the token verbatim.
|
|
104
|
-
if (body.length >= 3 && body[1] === '=') {
|
|
105
|
-
const ch = body[0] as string;
|
|
106
|
-
const val = body.slice(2);
|
|
107
|
-
const long = valueShortLong(ch);
|
|
108
|
-
if (long === undefined) {
|
|
109
|
-
// Unknown -X=val — pass the original token verbatim. Same fallthrough
|
|
110
|
-
// as Go's parseShortFlag.
|
|
111
|
-
return { tokens: [arg], consumed: 0 };
|
|
112
|
-
}
|
|
113
|
-
return { tokens: [`${long}=${val}`], consumed: 0 };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const out: string[] = [];
|
|
117
|
-
for (let pos = 0; pos < body.length; pos++) {
|
|
118
|
-
const ch = body[pos] as string;
|
|
119
|
-
const isLast = pos === body.length - 1;
|
|
120
|
-
|
|
121
|
-
if (SHORT_NO_VALUE[ch]) {
|
|
122
|
-
out.push(SHORT_NO_VALUE[ch]!);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const required = SHORT_REQUIRED[ch];
|
|
127
|
-
if (required) {
|
|
128
|
-
if (!isLast) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`fnclaude: flag -${ch} cannot be in middle of collapsed group, requires a value`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
if (rest.length === 0 || rest[0]!.startsWith('-')) {
|
|
134
|
-
throw new Error(`fnclaude: -${ch} requires a value`);
|
|
135
|
-
}
|
|
136
|
-
return { tokens: [...out, required, rest[0]!], consumed: 1 };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const optional = SHORT_OPTIONAL[ch];
|
|
140
|
-
if (optional) {
|
|
141
|
-
if (!isLast) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`fnclaude: flag -${ch} cannot be in middle of collapsed group, requires a value`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
if (rest.length > 0 && !rest[0]!.startsWith('-')) {
|
|
147
|
-
return { tokens: [...out, optional, rest[0]!], consumed: 1 };
|
|
148
|
-
}
|
|
149
|
-
return { tokens: [...out, optional], consumed: 0 };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Unknown single-char short flag — pass through verbatim. Lets claude
|
|
153
|
-
// either accept it or surface its own error.
|
|
154
|
-
out.push(`-${ch}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return { tokens: out, consumed: 0 };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Main parser ────────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* parseArgs is the canonical argv parser. `home` is the user's home dir
|
|
164
|
-
* (typically `os.homedir()`); it's used to derive the noop fallback path.
|
|
165
|
-
*
|
|
166
|
-
* Returns a `ParsedArgs` — the first stage of the immutable argv pipeline.
|
|
167
|
-
* Notably absent: `worktreeMatched`. That value only becomes meaningful
|
|
168
|
-
* after the intercept stage and is part of `InterceptedArgs` instead.
|
|
169
|
-
*
|
|
170
|
-
* Throws Error on invalid input (too many positionals, missing values,
|
|
171
|
-
* collapsed-group misuse, two subcommands, etc.). The Go original returns
|
|
172
|
-
* `(Args, error)`; TS uses throw for the natural shape.
|
|
173
|
-
*/
|
|
174
|
-
export function parseArgs(argv: readonly string[], home: string): ParsedArgs {
|
|
175
|
-
let firstPath = '';
|
|
176
|
-
const extraDirs: string[] = [];
|
|
177
|
-
const passthrough: string[] = [];
|
|
178
|
-
let noTmux = false;
|
|
179
|
-
let worktreeSet = false;
|
|
180
|
-
let worktreeArg: string | undefined;
|
|
181
|
-
|
|
182
|
-
// Magic slots: filled at most once each, in strict order.
|
|
183
|
-
let magicModel = '';
|
|
184
|
-
let magicEffort = '';
|
|
185
|
-
|
|
186
|
-
// Subcommand expansion: long-flag tokens to prepend to passthrough.
|
|
187
|
-
let subcommandExpansion: readonly string[] | undefined;
|
|
188
|
-
let subcommandToken = '';
|
|
189
|
-
|
|
190
|
-
// 0 = position 1 (check model)
|
|
191
|
-
// 1 = position 2 (check effort, only if model matched)
|
|
192
|
-
// 2 = magic done
|
|
193
|
-
let magicState = 0;
|
|
194
|
-
|
|
195
|
-
let inFlags = false;
|
|
196
|
-
let firstPathSet = false;
|
|
197
|
-
|
|
198
|
-
let i = 0;
|
|
199
|
-
while (i < argv.length) {
|
|
200
|
-
const arg = argv[i] as string;
|
|
201
|
-
|
|
202
|
-
// ── Positional phase (before first flag-shaped token) ───────────────
|
|
203
|
-
if (!inFlags && !arg.startsWith('-')) {
|
|
204
|
-
// Subcommand check fires at every positional slot, independent of
|
|
205
|
-
// magic state — does NOT advance magicState, so `fnc resume opus xhigh`
|
|
206
|
-
// and `fnc opus xhigh resume` parse equivalently.
|
|
207
|
-
const subFlags = SUBCOMMAND_FLAGS[arg];
|
|
208
|
-
if (subFlags) {
|
|
209
|
-
if (subcommandExpansion !== undefined) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`fnclaude: only one subcommand allowed (got "${subcommandToken}" and "${arg}")`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
subcommandExpansion = subFlags;
|
|
215
|
-
subcommandToken = arg;
|
|
216
|
-
i++;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (magicState === 0) {
|
|
221
|
-
if (MODEL_ALIASES.has(arg)) {
|
|
222
|
-
magicModel = arg;
|
|
223
|
-
magicState = 1; // advance to effort check at position 2
|
|
224
|
-
i++;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
magicState = 2; // not a model alias → magic done; arg is cwd
|
|
228
|
-
} else if (magicState === 1) {
|
|
229
|
-
if (EFFORT_LEVELS.has(arg)) {
|
|
230
|
-
magicEffort = arg;
|
|
231
|
-
magicState = 2;
|
|
232
|
-
i++;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
magicState = 2; // not an effort level → magic done; arg is cwd
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (!firstPathSet) {
|
|
239
|
-
firstPath = arg;
|
|
240
|
-
firstPathSet = true;
|
|
241
|
-
} else if (!worktreeSet) {
|
|
242
|
-
worktreeSet = true;
|
|
243
|
-
worktreeArg = arg;
|
|
244
|
-
} else {
|
|
245
|
-
throw new Error(
|
|
246
|
-
`fnclaude: too many positional arguments (got "${arg}"; max is 2 — cwd and worktree-name)`,
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
i++;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ── Flag territory ──────────────────────────────────────────────────
|
|
254
|
-
inFlags = true;
|
|
255
|
-
|
|
256
|
-
if (arg === '--no-tmux') {
|
|
257
|
-
noTmux = true;
|
|
258
|
-
i++;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// -A / --also (space form).
|
|
263
|
-
if (arg === '-A' || arg === '--also') {
|
|
264
|
-
const next = argv[i + 1];
|
|
265
|
-
if (next === undefined || next.startsWith('-')) {
|
|
266
|
-
const which = next === undefined ? arg : `${arg} ${next}`;
|
|
267
|
-
throw new Error(`fnclaude: ${which} requires a directory argument`);
|
|
268
|
-
}
|
|
269
|
-
extraDirs.push(next);
|
|
270
|
-
i += 2;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
// -A=val / --also=val.
|
|
274
|
-
if (arg.startsWith('-A=')) {
|
|
275
|
-
const val = arg.slice(3);
|
|
276
|
-
if (val === '') throw new Error('fnclaude: -A= requires a directory argument');
|
|
277
|
-
extraDirs.push(val);
|
|
278
|
-
i++;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (arg.startsWith('--also=')) {
|
|
282
|
-
const val = arg.slice('--also='.length);
|
|
283
|
-
if (val === '') throw new Error('fnclaude: --also= requires a directory argument');
|
|
284
|
-
extraDirs.push(val);
|
|
285
|
-
i++;
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// -w / --worktree (intercepted; NOT pushed to passthrough here).
|
|
290
|
-
if (arg === '-w' || arg === '--worktree') {
|
|
291
|
-
worktreeSet = true;
|
|
292
|
-
const next = argv[i + 1];
|
|
293
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
294
|
-
worktreeArg = next;
|
|
295
|
-
i += 2;
|
|
296
|
-
} else {
|
|
297
|
-
i++;
|
|
298
|
-
}
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (arg.startsWith('-w=')) {
|
|
302
|
-
worktreeSet = true;
|
|
303
|
-
worktreeArg = arg.slice(3);
|
|
304
|
-
i++;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (arg.startsWith('--worktree=')) {
|
|
308
|
-
worktreeSet = true;
|
|
309
|
-
worktreeArg = arg.slice('--worktree='.length);
|
|
310
|
-
i++;
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Single-dash short flags (length >= 2, second char is not '-').
|
|
315
|
-
if (arg.length >= 2 && arg[0] === '-' && arg[1] !== '-') {
|
|
316
|
-
const { tokens, consumed } = parseShortFlag(arg, argv.slice(i + 1));
|
|
317
|
-
passthrough.push(...tokens);
|
|
318
|
-
i += 1 + consumed;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Everything else passes through.
|
|
323
|
-
passthrough.push(arg);
|
|
324
|
-
i++;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Build the magic prefix (--model + --effort) and the subcommand prefix.
|
|
328
|
-
const magicPrefix: string[] = [];
|
|
329
|
-
if (magicModel) magicPrefix.push('--model', magicModel);
|
|
330
|
-
if (magicEffort) magicPrefix.push('--effort', magicEffort);
|
|
331
|
-
|
|
332
|
-
let finalPassthrough: string[];
|
|
333
|
-
if (magicPrefix.length > 0) {
|
|
334
|
-
finalPassthrough = [...magicPrefix, ...passthrough];
|
|
335
|
-
} else {
|
|
336
|
-
finalPassthrough = passthrough;
|
|
337
|
-
}
|
|
338
|
-
if (subcommandExpansion && subcommandExpansion.length > 0) {
|
|
339
|
-
finalPassthrough = [...subcommandExpansion, ...finalPassthrough];
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// CWD fallback.
|
|
343
|
-
const cwd = firstPathSet ? firstPath : defaultNoopDir(home);
|
|
344
|
-
const usedNoopFallback = !firstPathSet;
|
|
345
|
-
|
|
346
|
-
return brandParsed({
|
|
347
|
-
cwd,
|
|
348
|
-
extraDirs,
|
|
349
|
-
passthrough: finalPassthrough,
|
|
350
|
-
noTmux,
|
|
351
|
-
worktreeSet,
|
|
352
|
-
worktreeArg,
|
|
353
|
-
usedNoopFallback,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// ── Passthrough inspection helpers ─────────────────────────────────────────
|
|
358
|
-
//
|
|
359
|
-
// The canonical implementations live in passthrough.ts. Re-exported here
|
|
360
|
-
// for back-compat with callers that grew up importing them from argParser.
|
|
361
|
-
// New code should import from passthrough.ts directly.
|
|
362
|
-
|
|
363
|
-
export {
|
|
364
|
-
nameInPassthrough,
|
|
365
|
-
settingSourcesInPassthrough,
|
|
366
|
-
tokenInPassthrough,
|
|
367
|
-
} from './passthrough.js';
|
package/src/args/preserve.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* preserveArgs + applyOverrides — flag-merging helpers for fnclaude
|
|
3
|
-
* relaunches (restart / transfer). Ported from src/preserve_args.go.
|
|
4
|
-
*
|
|
5
|
-
* preserveArgs picks the subset of os.Args[1:] to carry across a relaunch:
|
|
6
|
-
* - keeps leading magic words (model alias / effort level) at the front
|
|
7
|
-
* - strips contiguous non-flag positional tokens (cwd + worktree-name slots)
|
|
8
|
-
* - keeps everything from the first flag onward, minus any flag in deny
|
|
9
|
-
* (with its value token if the flag takes a value)
|
|
10
|
-
*
|
|
11
|
-
* applyOverrides folds MCP-supplied Request override fields into the
|
|
12
|
-
* preserved slice — strip-then-append for strings, three-state nil/true/
|
|
13
|
-
* false semantics for booleans.
|
|
14
|
-
*
|
|
15
|
-
* The Go test suite (src/preserve_args_test.go) is the contract; the TS
|
|
16
|
-
* port mirrors every case 1:1.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { RequestOverrides } from '../mcp/protocol.js';
|
|
20
|
-
|
|
21
|
-
// ── Magic-word vocabularies (must match argParser.ts) ──────────────────────
|
|
22
|
-
|
|
23
|
-
const MODEL_ALIASES: ReadonlySet<string> = new Set(['opus', 'sonnet', 'haiku']);
|
|
24
|
-
const EFFORT_LEVELS: ReadonlySet<string> = new Set([
|
|
25
|
-
'low',
|
|
26
|
-
'medium',
|
|
27
|
-
'high',
|
|
28
|
-
'xhigh',
|
|
29
|
-
'max',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
/** True when tok is a model alias or effort level. */
|
|
33
|
-
export function isMagicWord(tok: string): boolean {
|
|
34
|
-
return MODEL_ALIASES.has(tok) || EFFORT_LEVELS.has(tok);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** True when tok looks flag-shaped (starts with "-"). */
|
|
38
|
-
export function isFlag(tok: string): boolean {
|
|
39
|
-
return tok.length > 0 && tok.charCodeAt(0) === 0x2d /* '-' */;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Divide args into [leading-magic-words] and [the rest]. The split stops
|
|
44
|
-
* at the first non-magic token. Used by socket-listener handlers to
|
|
45
|
-
* reposition the magic prefix around the launch cwd / destination.
|
|
46
|
-
*/
|
|
47
|
-
export function splitLeadingMagic(args: readonly string[]): {
|
|
48
|
-
magic: string[];
|
|
49
|
-
rest: string[];
|
|
50
|
-
} {
|
|
51
|
-
let i = 0;
|
|
52
|
-
while (i < args.length && isMagicWord(args[i] as string)) i++;
|
|
53
|
-
return { magic: args.slice(0, i), rest: args.slice(i) };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** True when args contains flag as a standalone token or `flag=value`. */
|
|
57
|
-
export function flagPresent(args: readonly string[], flag: string): boolean {
|
|
58
|
-
const prefix = `${flag}=`;
|
|
59
|
-
for (const t of args) {
|
|
60
|
-
if (t === flag || t.startsWith(prefix)) return true;
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── preserveArgs ───────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Return the subset of origArgs to carry across an fnclaude relaunch.
|
|
69
|
-
*
|
|
70
|
-
* `deny` is a set of flag tokens (long or short form) to strip. Pass null
|
|
71
|
-
* to preserve all flags. For each denied flag, the flag token AND the
|
|
72
|
-
* immediately-following value token are stripped — UNLESS the flag is in
|
|
73
|
-
* `bareOK`, in which case the bare (no-value) form is allowed and only
|
|
74
|
-
* the flag token is consumed when the following token is itself a flag.
|
|
75
|
-
* The `--flag=value` form is always handled as a single token.
|
|
76
|
-
*
|
|
77
|
-
* Returns a fresh array. Mirrors Go's `preserveArgs(origArgs, deny, bareOK)`.
|
|
78
|
-
*/
|
|
79
|
-
export function preserveArgs(
|
|
80
|
-
origArgs: readonly string[],
|
|
81
|
-
deny: ReadonlySet<string> | null,
|
|
82
|
-
bareOK: ReadonlySet<string> | null,
|
|
83
|
-
): string[] {
|
|
84
|
-
const out: string[] = [];
|
|
85
|
-
let i = 0;
|
|
86
|
-
|
|
87
|
-
// Phase 1: collect leading magic words.
|
|
88
|
-
while (i < origArgs.length) {
|
|
89
|
-
const tok = origArgs[i] as string;
|
|
90
|
-
if (isMagicWord(tok)) {
|
|
91
|
-
out.push(tok);
|
|
92
|
-
i++;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Phase 2: skip contiguous positional path tokens (non-flag, non-magic).
|
|
99
|
-
while (i < origArgs.length) {
|
|
100
|
-
const tok = origArgs[i] as string;
|
|
101
|
-
if (isFlag(tok)) break; // reached flags — stop skipping paths
|
|
102
|
-
i++;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Phase 3: keep flag region, minus any denylisted flag (+ its value).
|
|
106
|
-
while (i < origArgs.length) {
|
|
107
|
-
const tok = origArgs[i] as string;
|
|
108
|
-
|
|
109
|
-
// `--` separates flags from the original session's initial prompt
|
|
110
|
-
// (everything after is the prompt body). Carrying it across a
|
|
111
|
-
// relaunch shadows the transfer's @summary file or re-prompts after
|
|
112
|
-
// --resume on restart — drop the separator and the entire tail.
|
|
113
|
-
if (tok === '--') break;
|
|
114
|
-
|
|
115
|
-
// Equals-form (--flag=value): match by the flag-prefix-before-= part.
|
|
116
|
-
if (deny !== null) {
|
|
117
|
-
const eq = tok.indexOf('=');
|
|
118
|
-
if (eq > 0) {
|
|
119
|
-
const flagPart = tok.slice(0, eq);
|
|
120
|
-
if (deny.has(flagPart)) {
|
|
121
|
-
i++;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Bare-token deny check.
|
|
128
|
-
if (deny !== null && deny.has(tok)) {
|
|
129
|
-
// Skip the flag token. Also try to consume the following value
|
|
130
|
-
// token unless bareOK says the bare form is acceptable AND the
|
|
131
|
-
// next token is itself a flag.
|
|
132
|
-
i++;
|
|
133
|
-
if (i < origArgs.length) {
|
|
134
|
-
const next = origArgs[i] as string;
|
|
135
|
-
if (bareOK !== null && bareOK.has(tok)) {
|
|
136
|
-
// bareOK: consume the next token only if it's a value
|
|
137
|
-
// (non-flag, doesn't start with '-'). If it's a flag, leave
|
|
138
|
-
// it alone — the bare form is allowed here.
|
|
139
|
-
if (!isFlag(next)) i++;
|
|
140
|
-
} else {
|
|
141
|
-
// Not bareOK: always consume the next token as the value.
|
|
142
|
-
i++;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
out.push(tok);
|
|
149
|
-
i++;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── Transfer denylist ──────────────────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Flag tokens that must be stripped when preserving args across a project
|
|
159
|
-
* transfer (fnc_switch_project). These are destination-bound or
|
|
160
|
-
* session-state-bound: carrying them into the new session would either be
|
|
161
|
-
* wrong (--add-dir is the OLD project's dir; -A the OLD extras;
|
|
162
|
-
* --mcp-config the OLD config; --settings the OLD settings) or actively
|
|
163
|
-
* bogus (--resume / --continue / --fork-session / --from-pr reference the
|
|
164
|
-
* OLD session id or PR; -w/--worktree is the OLD worktree name; --name is
|
|
165
|
-
* the OLD session name and the transfer supplies a new one).
|
|
166
|
-
*
|
|
167
|
-
* Source of truth: src/preserve_args.go's `transferDenyFlags`.
|
|
168
|
-
*/
|
|
169
|
-
export const transferDenyFlags: ReadonlySet<string> = new Set([
|
|
170
|
-
'-A',
|
|
171
|
-
'--also',
|
|
172
|
-
'--add-dir',
|
|
173
|
-
'--mcp-config',
|
|
174
|
-
'--settings',
|
|
175
|
-
'-w',
|
|
176
|
-
'--worktree',
|
|
177
|
-
'-P',
|
|
178
|
-
'--from-pr',
|
|
179
|
-
'-r',
|
|
180
|
-
'--resume',
|
|
181
|
-
'-c',
|
|
182
|
-
'--continue',
|
|
183
|
-
'-F',
|
|
184
|
-
'--fork-session',
|
|
185
|
-
'-n',
|
|
186
|
-
'--name',
|
|
187
|
-
]);
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Subset of transferDenyFlags that can appear in bare (no-value) form.
|
|
191
|
-
* For these, preserveArgs only consumes the following token when it
|
|
192
|
-
* doesn't look like another flag.
|
|
193
|
-
*
|
|
194
|
-
* Source of truth: src/preserve_args.go's `transferDenyBareOK`.
|
|
195
|
-
*/
|
|
196
|
-
export const transferDenyBareOK: ReadonlySet<string> = new Set([
|
|
197
|
-
'-w',
|
|
198
|
-
'--worktree',
|
|
199
|
-
'-r',
|
|
200
|
-
'--resume',
|
|
201
|
-
'-c',
|
|
202
|
-
'--continue',
|
|
203
|
-
'-F',
|
|
204
|
-
'--fork-session',
|
|
205
|
-
'-P',
|
|
206
|
-
'--from-pr',
|
|
207
|
-
]);
|
|
208
|
-
|
|
209
|
-
// ── applyOverrides ─────────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Fold req's override fields into the preserved arg slice.
|
|
213
|
-
*
|
|
214
|
-
* Three-state semantics:
|
|
215
|
-
* - string field set (non-empty): strip any existing occurrence of the
|
|
216
|
-
* corresponding flag (including bare-magic-word form for --model and
|
|
217
|
-
* --effort), then append "--flag <value>" at the end.
|
|
218
|
-
* - null/undefined boolean: preserve any existing occurrence.
|
|
219
|
-
* - true boolean: strip existing, append the bare "--flag".
|
|
220
|
-
* - false boolean: strip existing, do NOT append.
|
|
221
|
-
*
|
|
222
|
-
* Overrides always emit FLAG form (--model sonnet), never magic-positional
|
|
223
|
-
* form, to avoid the awkward case of mixing magic preservation with
|
|
224
|
-
* overrides.
|
|
225
|
-
*/
|
|
226
|
-
export function applyOverrides(
|
|
227
|
-
preserved: readonly string[],
|
|
228
|
-
req: RequestOverrides,
|
|
229
|
-
): string[] {
|
|
230
|
-
let out = preserved.slice();
|
|
231
|
-
|
|
232
|
-
// String overrides.
|
|
233
|
-
if (req.model !== undefined && req.model !== '') {
|
|
234
|
-
out = stripFlag(out, '--model');
|
|
235
|
-
out = stripBareMagic(out, MODEL_ALIASES);
|
|
236
|
-
out.push('--model', req.model);
|
|
237
|
-
}
|
|
238
|
-
if (req.effort !== undefined && req.effort !== '') {
|
|
239
|
-
out = stripFlag(out, '--effort');
|
|
240
|
-
out = stripBareMagic(out, EFFORT_LEVELS);
|
|
241
|
-
out.push('--effort', req.effort);
|
|
242
|
-
}
|
|
243
|
-
if (req.permission_mode !== undefined && req.permission_mode !== '') {
|
|
244
|
-
out = stripFlag(out, '--permission-mode');
|
|
245
|
-
out.push('--permission-mode', req.permission_mode);
|
|
246
|
-
}
|
|
247
|
-
if (req.allowed_tools !== undefined && req.allowed_tools !== '') {
|
|
248
|
-
out = stripFlag(out, '--allowedTools');
|
|
249
|
-
out.push('--allowedTools', req.allowed_tools);
|
|
250
|
-
}
|
|
251
|
-
if (req.agent !== undefined && req.agent !== '') {
|
|
252
|
-
out = stripFlag(out, '--agent');
|
|
253
|
-
out.push('--agent', req.agent);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Boolean overrides.
|
|
257
|
-
out = applyBoolOverride(out, '--brief', req.brief);
|
|
258
|
-
out = applyBoolOverride(out, '--chrome', req.chrome);
|
|
259
|
-
out = applyBoolOverride(out, '--ide', req.ide);
|
|
260
|
-
out = applyBoolOverride(out, '--verbose', req.verbose);
|
|
261
|
-
|
|
262
|
-
return out;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Remove every occurrence of flag (and its value if present in space-
|
|
267
|
-
* separated form) and every "flag=value" token in args.
|
|
268
|
-
*/
|
|
269
|
-
export function stripFlag(args: readonly string[], flag: string): string[] {
|
|
270
|
-
const result: string[] = [];
|
|
271
|
-
let i = 0;
|
|
272
|
-
while (i < args.length) {
|
|
273
|
-
const tok = args[i] as string;
|
|
274
|
-
if (tok === flag) {
|
|
275
|
-
// Space form: consume value if present and not another flag.
|
|
276
|
-
i++;
|
|
277
|
-
if (i < args.length && !isFlag(args[i] as string)) {
|
|
278
|
-
i++;
|
|
279
|
-
}
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (tok.startsWith(`${flag}=`)) {
|
|
283
|
-
i++;
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
result.push(tok);
|
|
287
|
-
i++;
|
|
288
|
-
}
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** Remove every occurrence of flag as a bare token. */
|
|
293
|
-
export function stripFlagBare(args: readonly string[], flag: string): string[] {
|
|
294
|
-
const result: string[] = [];
|
|
295
|
-
for (const tok of args) {
|
|
296
|
-
if (tok === flag) continue;
|
|
297
|
-
result.push(tok);
|
|
298
|
-
}
|
|
299
|
-
return result;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Remove any token that's a member of the given magic-word set. Used so a
|
|
304
|
-
* Model/Effort override strips the bare positional form (e.g. "opus" or
|
|
305
|
-
* "max") and the resulting argv carries only the explicit --model /
|
|
306
|
-
* --effort flag pair appended later.
|
|
307
|
-
*/
|
|
308
|
-
export function stripBareMagic(
|
|
309
|
-
args: readonly string[],
|
|
310
|
-
magic: ReadonlySet<string>,
|
|
311
|
-
): string[] {
|
|
312
|
-
const result: string[] = [];
|
|
313
|
-
for (const tok of args) {
|
|
314
|
-
if (magic.has(tok)) continue;
|
|
315
|
-
result.push(tok);
|
|
316
|
-
}
|
|
317
|
-
return result;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Apply a tri-state boolean override for a bare flag.
|
|
322
|
-
*
|
|
323
|
-
* - undefined/null → preserve existing.
|
|
324
|
-
* - true → strip existing dupes + append once.
|
|
325
|
-
* - false → strip existing.
|
|
326
|
-
*/
|
|
327
|
-
export function applyBoolOverride(
|
|
328
|
-
args: readonly string[],
|
|
329
|
-
flag: string,
|
|
330
|
-
b: boolean | null | undefined,
|
|
331
|
-
): string[] {
|
|
332
|
-
if (b === null || b === undefined) {
|
|
333
|
-
return args.slice();
|
|
334
|
-
}
|
|
335
|
-
const out = stripFlagBare(args, flag);
|
|
336
|
-
if (b) out.push(flag);
|
|
337
|
-
return out;
|
|
338
|
-
}
|