@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/args.ts
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stage-typed argv pipeline.
|
|
3
|
-
*
|
|
4
|
-
* fnclaude's argv flows through five stages — parse, resolve, intercept,
|
|
5
|
-
* auto-name, sanitize — before being handed to buildArgv. Earlier the
|
|
6
|
-
* intermediate state was a single mutable `Args` bag passed through every
|
|
7
|
-
* step, each free to rewrite any field. The pipeline's *ordering* was a
|
|
8
|
-
* runtime invariant only: nothing stopped `buildArgv` from being called
|
|
9
|
-
* before `applyWorktreeIntercept`, and `worktreeMatched` started life as a
|
|
10
|
-
* placeholder boolean on the parsed shape, even though it has no meaningful
|
|
11
|
-
* value until intercept has run.
|
|
12
|
-
*
|
|
13
|
-
* The new shape uses distinct types per stage. Each function takes its
|
|
14
|
-
* predecessor's output type as input and returns the next stage's type.
|
|
15
|
-
* The shapes are structurally readonly and brand-discriminated, so:
|
|
16
|
-
*
|
|
17
|
-
* - `parseArgs` returns `ParsedArgs`, which has NO `worktreeMatched`
|
|
18
|
-
* field at all — the invariant "the intercept hasn't run yet" is
|
|
19
|
-
* encoded in the type, not a sentinel value.
|
|
20
|
-
* - `applyWorktreeIntercept` returns `InterceptedArgs`, which has
|
|
21
|
-
* `worktreeMatched` materialized. Passing a `ParsedArgs` to `buildArgv`
|
|
22
|
-
* is a compile error because `InterceptedArgs` is what `buildArgv`
|
|
23
|
-
* accepts.
|
|
24
|
-
* - Stages are immutable; each function returns a new value. The
|
|
25
|
-
* pipeline composes by value, not by aliased reference.
|
|
26
|
-
*
|
|
27
|
-
* The brand fields (`__stage`) only exist in the type system — they're
|
|
28
|
-
* never assigned at runtime. The brand functions (`brandParsed` etc.) are
|
|
29
|
-
* named casts that document the boundary at which the stage transition
|
|
30
|
-
* happens.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
// ── Base shape ─────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Fields every stage carries. All readonly: stage transitions produce new
|
|
37
|
-
* objects, never mutate.
|
|
38
|
-
*/
|
|
39
|
-
export interface BaseArgs {
|
|
40
|
-
/**
|
|
41
|
-
* CWD is the directory claude will be launched in (first positional, or
|
|
42
|
-
* the noop fallback when no positionals are given). The interpretation
|
|
43
|
-
* narrows as stages progress — `ParsedArgs.cwd` is whatever the user
|
|
44
|
-
* typed; later stages have it resolved to an absolute path and/or
|
|
45
|
-
* swapped to a matched-worktree path.
|
|
46
|
-
*/
|
|
47
|
-
readonly cwd: string;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* ExtraDirs collects all -A / --also values in order. Positional 2 is the
|
|
51
|
-
* worktree slot; -A is the only way to supply extra dirs.
|
|
52
|
-
*/
|
|
53
|
-
readonly extraDirs: readonly string[];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Passthrough is everything else, preserved in order, to be forwarded to
|
|
57
|
-
* claude verbatim. Short flags are already translated to their long
|
|
58
|
-
* forms. Later stages may *extend* this slice (e.g. the intercept pushes
|
|
59
|
-
* `--worktree <name>`, auto-name prepends `--name <name>`).
|
|
60
|
-
*/
|
|
61
|
-
readonly passthrough: readonly string[];
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* NoTmux is true when the user passed --no-tmux (eaten by fnclaude; not
|
|
65
|
-
* forwarded to claude).
|
|
66
|
-
*/
|
|
67
|
-
readonly noTmux: boolean;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* WorktreeSet is true when the user passed -w / --worktree, OR supplied
|
|
71
|
-
* a 2nd positional after magic + subcommand consumption, OR the Resolve
|
|
72
|
-
* step picked up a `+workspace` suffix from a repo reference.
|
|
73
|
-
*/
|
|
74
|
-
readonly worktreeSet: boolean;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* WorktreeArg is the name/value given with -w / --worktree (or the 2nd
|
|
78
|
-
* positional / +workspace suffix), or undefined if the flag was bare.
|
|
79
|
-
*/
|
|
80
|
-
readonly worktreeArg: string | undefined;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* UsedNoopFallback is true when CWD was filled by the noop fallback (no
|
|
84
|
-
* positional path given). Caller uses this to gate seed-noop behavior —
|
|
85
|
-
* explicit paths don't get auto-seeded.
|
|
86
|
-
*/
|
|
87
|
-
readonly usedNoopFallback: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── Brand machinery ────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* `Branded<T, K>` tags `T` with a phantom string literal `K` so two
|
|
94
|
-
* otherwise-structurally-identical types become assignment-incompatible.
|
|
95
|
-
* The `__stage` property exists only in the type — runtime values never
|
|
96
|
-
* carry it.
|
|
97
|
-
*/
|
|
98
|
-
type Branded<T, K extends string> = T & { readonly __stage: K };
|
|
99
|
-
|
|
100
|
-
// ── Stage 1: parsed ────────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Output of `parseArgs`. The argv has been split into structural fields,
|
|
104
|
-
* but no I/O has run yet — `cwd` is still the user-typed string, the
|
|
105
|
-
* worktree intercept hasn't queried git, and no autoname has been generated.
|
|
106
|
-
*
|
|
107
|
-
* Has NO `worktreeMatched` field. That value only becomes meaningful after
|
|
108
|
-
* the intercept stage, and encoding its absence in the type means a stale
|
|
109
|
-
* `worktreeMatched: false` can't accidentally be read by a downstream step.
|
|
110
|
-
*/
|
|
111
|
-
export type ParsedArgs = Branded<BaseArgs, 'parsed'>;
|
|
112
|
-
|
|
113
|
-
// ── Stage 2: resolved ──────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Output of the Resolve / tilde-expand step. `cwd` is an absolute path
|
|
117
|
-
* (when a path or repo ref was resolved) or the noop fallback. The Resolve
|
|
118
|
-
* step may also have promoted a `+workspace` suffix into `worktreeSet` +
|
|
119
|
-
* `worktreeArg`.
|
|
120
|
-
*
|
|
121
|
-
* Still no `worktreeMatched` — that's the next stage.
|
|
122
|
-
*/
|
|
123
|
-
export type ResolvedArgs = Branded<BaseArgs, 'resolved'>;
|
|
124
|
-
|
|
125
|
-
// ── Stage 3: intercepted ───────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Output of `applyWorktreeIntercept`. `worktreeMatched` is now meaningful:
|
|
129
|
-
* true iff an existing worktree of the project repo matched
|
|
130
|
-
* `worktreeArg` (and `cwd` was swapped to that worktree's path). Downstream
|
|
131
|
-
* consumers (`buildArgv`'s auto-tmux gate, primarily) treat matched=true
|
|
132
|
-
* as "no new worktree being created this run" and avoid injecting flags
|
|
133
|
-
* that only make sense when claude is about to spin up a fresh worktree.
|
|
134
|
-
*
|
|
135
|
-
* `passthrough` may have been extended with `--worktree`, `--worktree <name>`,
|
|
136
|
-
* or `--name <name>` depending on whether the intercept matched.
|
|
137
|
-
*/
|
|
138
|
-
export interface InterceptedFields {
|
|
139
|
-
/**
|
|
140
|
-
* True iff -w / --worktree was resolved against an existing worktree of
|
|
141
|
-
* the project repo (and cwd was swapped to that worktree).
|
|
142
|
-
*/
|
|
143
|
-
readonly worktreeMatched: boolean;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export type InterceptedArgs = Branded<BaseArgs & InterceptedFields, 'intercepted'>;
|
|
147
|
-
|
|
148
|
-
// ── Brand constructors ─────────────────────────────────────────────────────
|
|
149
|
-
//
|
|
150
|
-
// Each brand function is a named cast — it doesn't validate anything,
|
|
151
|
-
// it just documents *where* the stage transition happens in the pipeline.
|
|
152
|
-
// The asserted shape carries all the invariants the stage promises.
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Stamp a `BaseArgs`-shaped value as `ParsedArgs`. Only `parseArgs` should
|
|
156
|
-
* call this.
|
|
157
|
-
*/
|
|
158
|
-
export function brandParsed(a: BaseArgs): ParsedArgs {
|
|
159
|
-
return a as ParsedArgs;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Stamp a `BaseArgs`-shaped value as `ResolvedArgs`. Called after the
|
|
164
|
-
* Resolve / tilde-expand step (or to short-circuit when the input is
|
|
165
|
-
* already absolute and doesn't need resolution).
|
|
166
|
-
*/
|
|
167
|
-
export function brandResolved(a: BaseArgs): ResolvedArgs {
|
|
168
|
-
return a as ResolvedArgs;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Stamp a `BaseArgs & InterceptedFields`-shaped value as `InterceptedArgs`.
|
|
173
|
-
* Only `applyWorktreeIntercept` should call this.
|
|
174
|
-
*/
|
|
175
|
-
export function brandIntercepted(a: BaseArgs & InterceptedFields): InterceptedArgs {
|
|
176
|
-
return a as InterceptedArgs;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── Stage transitions (replace one or more fields, restamp the brand) ──────
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Return a new `ResolvedArgs` with the given field overrides. Used by the
|
|
183
|
-
* Resolve step to swap `cwd` (and possibly `worktreeSet` / `worktreeArg`)
|
|
184
|
-
* without mutating the parsed value.
|
|
185
|
-
*/
|
|
186
|
-
export function withResolved(
|
|
187
|
-
a: ParsedArgs | ResolvedArgs,
|
|
188
|
-
overrides: Partial<BaseArgs>,
|
|
189
|
-
): ResolvedArgs {
|
|
190
|
-
return brandResolved({ ...(a as BaseArgs), ...overrides });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Return a new `InterceptedArgs` from a `ResolvedArgs` plus the
|
|
195
|
-
* intercept's outputs. `InterceptedFields` (currently just `worktreeMatched`)
|
|
196
|
-
* is mandatory in the overrides so the brand can't be applied without it.
|
|
197
|
-
*/
|
|
198
|
-
export function withIntercepted(
|
|
199
|
-
a: ResolvedArgs,
|
|
200
|
-
overrides: Partial<BaseArgs> & InterceptedFields,
|
|
201
|
-
): InterceptedArgs {
|
|
202
|
-
return brandIntercepted({ ...(a as BaseArgs), ...overrides });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Return a new `InterceptedArgs` with `passthrough` (or other fields)
|
|
207
|
-
* replaced. Used by the auto-name and sanitize steps — both operate on the
|
|
208
|
-
* passthrough slice and produce a new slice; the rest of the args carries
|
|
209
|
-
* through unchanged.
|
|
210
|
-
*
|
|
211
|
-
* Returns `InterceptedArgs` (not a separate "named" / "sanitized" type)
|
|
212
|
-
* because no new invariants are established at those steps — only the
|
|
213
|
-
* passthrough slice changes shape, and that's already an in-stage edit
|
|
214
|
-
* the intercept itself does.
|
|
215
|
-
*/
|
|
216
|
-
export function withPassthroughUpdate(
|
|
217
|
-
a: InterceptedArgs,
|
|
218
|
-
overrides: Partial<BaseArgs>,
|
|
219
|
-
): InterceptedArgs {
|
|
220
|
-
// Carry worktreeMatched through; only overrides win.
|
|
221
|
-
return brandIntercepted({
|
|
222
|
-
...(a as BaseArgs & InterceptedFields),
|
|
223
|
-
...overrides,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Back-compat re-export ──────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* `Args` was the single mutable bag the pipeline threaded through before
|
|
231
|
-
* the stage-typed refactor. Retained as an alias for `InterceptedArgs` so
|
|
232
|
-
* external consumers (the published `index.ts` surface, test helpers in
|
|
233
|
-
* downstream tooling) keep working while the refactor lands. New code
|
|
234
|
-
* should use the stage-specific types directly.
|
|
235
|
-
*
|
|
236
|
-
* @deprecated Use `ParsedArgs` / `ResolvedArgs` / `InterceptedArgs` per
|
|
237
|
-
* the position in the pipeline.
|
|
238
|
-
*/
|
|
239
|
-
export type Args = InterceptedArgs;
|
package/src/argv.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
// Port of buildArgv + the helpers it composes (src/main.go, lines 745–858 in
|
|
2
|
-
// the Go reference). Constructs the final argv slice that fnclaude execs
|
|
3
|
-
// claude with.
|
|
4
|
-
//
|
|
5
|
-
// Layered behaviours, in order:
|
|
6
|
-
// 1. Per-extra-dir flag injection: --add-dir <dir>, --mcp-config <dir>/.mcp.json
|
|
7
|
-
// when present, --settings <dir>/.claude/settings.json when present and
|
|
8
|
-
// --setting-sources isn't already in passthrough.
|
|
9
|
-
// 2. Self-MCP injection: an inline --mcp-config <json> pointing at the
|
|
10
|
-
// current fnclaude binary so the spawned claude can call our
|
|
11
|
-
// fnc_restart / fnc_switch_project / fnc_copy_to_clipboard tools.
|
|
12
|
-
// Gated on interactive sessions only.
|
|
13
|
-
// 3. Auto-tmux injection: --tmux when auto.tmux="worktree", a new
|
|
14
|
-
// worktree is being created (worktreeSet && !worktreeMatched), and
|
|
15
|
-
// neither --tmux nor --no-tmux is already in play.
|
|
16
|
-
// 4. System-prompt fragment injection: --append-system-prompt <merged-text>
|
|
17
|
-
// composed by selectFragments + withAppendedSystemPrompts.
|
|
18
|
-
|
|
19
|
-
import { existsSync } from 'node:fs';
|
|
20
|
-
import { isAbsolute, join } from 'node:path';
|
|
21
|
-
import type { InterceptedArgs } from './args.js';
|
|
22
|
-
import {
|
|
23
|
-
nameInPassthrough as _nameInPassthrough,
|
|
24
|
-
settingSourcesInPassthrough,
|
|
25
|
-
tokenInPassthrough,
|
|
26
|
-
} from './passthrough.js';
|
|
27
|
-
import type { Config } from './config.js';
|
|
28
|
-
import { resolveSelfPath } from './paths.js';
|
|
29
|
-
import { isInteractiveSession, selectFragments, type PromptSet } from './prompts.js';
|
|
30
|
-
|
|
31
|
-
// Re-export the passthrough inspection helpers from their canonical home so
|
|
32
|
-
// callers can reach them via "./argv.js" — mirrors the Go reference where
|
|
33
|
-
// they sit next to buildArgv. The single-source-of-truth implementation
|
|
34
|
-
// stays in passthrough.ts.
|
|
35
|
-
export { settingSourcesInPassthrough, tokenInPassthrough };
|
|
36
|
-
|
|
37
|
-
// ── MCP self-injection ─────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* mcpConfigEntry mirrors the Go struct of the same name; one server entry
|
|
41
|
-
* inside the --mcp-config JSON object.
|
|
42
|
-
*/
|
|
43
|
-
interface McpConfigEntry {
|
|
44
|
-
command: string;
|
|
45
|
-
args: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* buildFnclaudeMCPConfigJSON returns the inline JSON string to pass as
|
|
50
|
-
* --mcp-config so claude launches `fnclaude mcp` (or `fnclaude mcp --noop`)
|
|
51
|
-
* as its MCP server subprocess. Returns null on any error — same defensive
|
|
52
|
-
* pattern as Go's findPromptsDir symlink-resolution fallback: log nothing,
|
|
53
|
-
* let the session launch without the MCP server rather than failing.
|
|
54
|
-
*
|
|
55
|
-
* Path resolution: prefer argv[1] (the CLI script) over execPath (the bun
|
|
56
|
-
* interpreter) so the spawned `fnclaude mcp` runs the same CLI logic as
|
|
57
|
-
* the launching process. Symlinks are followed (so a ~/.local/bin/fnc-dev
|
|
58
|
-
* → repo/bin/fnclaude symlink resolves to the real binary path).
|
|
59
|
-
*/
|
|
60
|
-
export function buildFnclaudeMCPConfigJSON(noop: boolean): string | null {
|
|
61
|
-
const exe = resolveSelfPath();
|
|
62
|
-
|
|
63
|
-
const args = ['mcp'];
|
|
64
|
-
if (noop) args.push('--noop');
|
|
65
|
-
|
|
66
|
-
const cfg: { mcpServers: Record<string, McpConfigEntry> } = {
|
|
67
|
-
mcpServers: {
|
|
68
|
-
fnclaude: { command: exe, args },
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
return JSON.stringify(cfg);
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── system-prompt fragment merge ───────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* withAppendedSystemPrompts returns a copy of `passthrough` with the given
|
|
83
|
-
* `fragments` merged into a single --append-system-prompt value. Fragments
|
|
84
|
-
* are joined with a blank-line separator. If `passthrough` already contains
|
|
85
|
-
* a --append-system-prompt (either space-form or =form), the fragments are
|
|
86
|
-
* appended to that existing value. Empty fragments are dropped. Returns
|
|
87
|
-
* `passthrough` unchanged when no non-empty fragments remain.
|
|
88
|
-
*
|
|
89
|
-
* Never mutates the input slice.
|
|
90
|
-
*/
|
|
91
|
-
export function withAppendedSystemPrompts(
|
|
92
|
-
passthrough: readonly string[],
|
|
93
|
-
fragments: readonly string[],
|
|
94
|
-
): string[] {
|
|
95
|
-
const clean = fragments.filter((f) => f !== '');
|
|
96
|
-
if (clean.length === 0) return [...passthrough];
|
|
97
|
-
|
|
98
|
-
const joined = clean.join('\n\n');
|
|
99
|
-
for (let i = 0; i < passthrough.length; i++) {
|
|
100
|
-
const t = passthrough[i] as string;
|
|
101
|
-
if (t === '--append-system-prompt' && i + 1 < passthrough.length) {
|
|
102
|
-
const out = [...passthrough];
|
|
103
|
-
out[i + 1] = `${passthrough[i + 1] as string}\n\n${joined}`;
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
if (t.startsWith('--append-system-prompt=')) {
|
|
107
|
-
const existing = t.slice('--append-system-prompt='.length);
|
|
108
|
-
const out = [...passthrough];
|
|
109
|
-
out[i] = `--append-system-prompt=${existing}\n\n${joined}`;
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return [...passthrough, '--append-system-prompt', joined];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── buildArgv ──────────────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* buildArgv constructs the argv slice to exec claude with, given the
|
|
120
|
-
* fnclaude args at their final pipeline stage (`InterceptedArgs` — the
|
|
121
|
-
* intercept must have run so `worktreeMatched` is meaningful), the user's
|
|
122
|
-
* shell cwd (used to resolve relative extra-dir paths), the loaded config,
|
|
123
|
-
* and the set of prompt fragments loaded from the install dir.
|
|
124
|
-
*
|
|
125
|
-
* Accepting `InterceptedArgs` makes the ordering invariant a compile-time
|
|
126
|
-
* check: passing a `ParsedArgs` or `ResolvedArgs` is a type error,
|
|
127
|
-
* preventing the auto-tmux gate from reading a stale `worktreeMatched`
|
|
128
|
-
* value the parse stage couldn't know.
|
|
129
|
-
*
|
|
130
|
-
* `shellCWD` is the process working directory at fnclaude startup —
|
|
131
|
-
* normally `process.cwd()`. It's threaded through (rather than reached for
|
|
132
|
-
* directly) so tests can pin it without `chdir`-ing.
|
|
133
|
-
*/
|
|
134
|
-
export function buildArgv(
|
|
135
|
-
a: InterceptedArgs,
|
|
136
|
-
shellCWD: string,
|
|
137
|
-
cfg: Config,
|
|
138
|
-
prompts: PromptSet,
|
|
139
|
-
): string[] {
|
|
140
|
-
const suppressSettings = settingSourcesInPassthrough(a.passthrough);
|
|
141
|
-
|
|
142
|
-
const argv: string[] = ['claude'];
|
|
143
|
-
|
|
144
|
-
// 1. Inject --add-dir (+ optional --mcp-config / --settings) per extra dir.
|
|
145
|
-
for (const raw of a.extraDirs) {
|
|
146
|
-
const d = isAbsolute(raw) ? raw : join(shellCWD, raw);
|
|
147
|
-
argv.push('--add-dir', d);
|
|
148
|
-
|
|
149
|
-
const mcpConfig = join(d, '.mcp.json');
|
|
150
|
-
if (existsSync(mcpConfig)) {
|
|
151
|
-
argv.push('--mcp-config', mcpConfig);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (!suppressSettings) {
|
|
155
|
-
const settings = join(d, '.claude', 'settings.json');
|
|
156
|
-
if (existsSync(settings)) {
|
|
157
|
-
argv.push('--settings', settings);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 2. Inject the fnclaude self-MCP config (interactive sessions only).
|
|
163
|
-
if (isInteractiveSession(a.passthrough)) {
|
|
164
|
-
const configJSON = buildFnclaudeMCPConfigJSON(a.usedNoopFallback);
|
|
165
|
-
if (configJSON !== null) {
|
|
166
|
-
argv.push('--mcp-config', configJSON);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 3. Auto-inject --tmux per auto.tmux config.
|
|
171
|
-
//
|
|
172
|
-
// claude requires --worktree to be present when --tmux is used. The only
|
|
173
|
-
// auto mode compatible with that constraint is "worktree", which fires
|
|
174
|
-
// when the user is already creating a new worktree themselves:
|
|
175
|
-
//
|
|
176
|
-
// "worktree" — inject --tmux when the user passed -w / --worktree for
|
|
177
|
-
// a NEW worktree (worktreeSet && !worktreeMatched).
|
|
178
|
-
// --worktree is already in passthrough; claude's
|
|
179
|
-
// constraint is satisfied without fnclaude having to
|
|
180
|
-
// generate worktrees itself.
|
|
181
|
-
// "never" — no-op.
|
|
182
|
-
//
|
|
183
|
-
// fnclaude never auto-creates worktrees — that's always user-initiated.
|
|
184
|
-
if (
|
|
185
|
-
cfg.auto.tmux === 'worktree' &&
|
|
186
|
-
!tokenInPassthrough(a.passthrough, '--tmux') &&
|
|
187
|
-
!a.noTmux &&
|
|
188
|
-
a.worktreeSet &&
|
|
189
|
-
!a.worktreeMatched
|
|
190
|
-
) {
|
|
191
|
-
argv.push('--tmux');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 4. System-prompt fragments.
|
|
195
|
-
const fragments = selectFragments(prompts, a.passthrough, a.usedNoopFallback);
|
|
196
|
-
argv.push(...withAppendedSystemPrompts(a.passthrough, fragments));
|
|
197
|
-
|
|
198
|
-
return argv;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Re-export so callers that already import nameInPassthrough from argv.ts
|
|
202
|
-
// (parity with the Go file layout) don't have to reach into passthrough.
|
|
203
|
-
export const nameInPassthrough = _nameInPassthrough;
|