@fnclaude/cli 1.1.1 → 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 -219
- 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/config.ts
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
// Port of src/config.go (fnclaude/fnclaude Go reference).
|
|
2
|
-
//
|
|
3
|
-
// Holds all fnclaude configuration, merged from defaults, the config file,
|
|
4
|
-
// and environment variables (env overrides config, config overrides built-in
|
|
5
|
-
// defaults).
|
|
6
|
-
//
|
|
7
|
-
// TOML parsing uses Bun's built-in `Bun.TOML.parse` — no external dependency.
|
|
8
|
-
|
|
9
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { errorMessage } from './errors.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Resolve the user's home directory. Honors `$HOME` first (matches Go's
|
|
16
|
-
* `os.UserHomeDir()` precedence on Unix), then falls back to `os.homedir()`.
|
|
17
|
-
* Tests rely on being able to override HOME at runtime.
|
|
18
|
-
*/
|
|
19
|
-
function home(): string {
|
|
20
|
-
return process.env.HOME ?? homedir();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ── public types ───────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export type TmuxMode = 'never' | 'worktree';
|
|
26
|
-
/**
|
|
27
|
-
* `'never'`, `'ask'`, or a non-negative integer-as-string (e.g. `'5'`).
|
|
28
|
-
*
|
|
29
|
-
* The template-literal `${number}` variant narrows correctly: a bare
|
|
30
|
-
* `string` would collapse the union, so runtime validation gates env-var
|
|
31
|
-
* and config-file inputs into this type via `normalizeHandoffMode`.
|
|
32
|
-
*/
|
|
33
|
-
export type HandoffMode = 'never' | 'ask' | `${number}`;
|
|
34
|
-
|
|
35
|
-
export interface NameConfig {
|
|
36
|
-
/** Model used for the noop name session. */
|
|
37
|
-
model: string;
|
|
38
|
-
/** Timeout for the noop name session, expressed in milliseconds. */
|
|
39
|
-
timeout: number;
|
|
40
|
-
/** When true, suppresses the missing-API-key startup warning. */
|
|
41
|
-
quietMissingAPIKey: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface AutoConfig {
|
|
45
|
-
/**
|
|
46
|
-
* Auto-injection of --tmux. "never" or "worktree".
|
|
47
|
-
* Anything else (including the deprecated "always") is normalized to
|
|
48
|
-
* "never" with a stderr warning during config load.
|
|
49
|
-
*/
|
|
50
|
-
tmux: TmuxMode;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Auto-handoff prompt mode. One of:
|
|
54
|
-
* "never" — never auto-switch; user pastes the rendered command.
|
|
55
|
-
* "ask" — noop session asks; on yes, fnclaude relaunches.
|
|
56
|
-
* "<N>" — non-negative integer; auto-switch after N seconds.
|
|
57
|
-
* Invalid values normalize to "ask" with a stderr warning during load.
|
|
58
|
-
*/
|
|
59
|
-
handoff: HandoffMode;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Launcher template used by fnc_spawn_session to open a sibling
|
|
63
|
-
* fnclaude in a new window. Whitespace-tokenized into argv; tokens
|
|
64
|
-
* are then placeholder-substituted before exec. Supported
|
|
65
|
-
* placeholders: {bin}, {dest}, {name}, {summary}. Empty means
|
|
66
|
-
* "auto-detect from environment, fall back to paste-flow".
|
|
67
|
-
*/
|
|
68
|
-
spawnCommand: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ExecConfig {
|
|
72
|
-
/**
|
|
73
|
-
* Additional environment variables to inject into the claude child's
|
|
74
|
-
* environment, sourced from [exec.env] in the config file. Appended
|
|
75
|
-
* AFTER os env when spawning claude — by exec last-wins semantics a
|
|
76
|
-
* configured key beats any inherited value with the same name.
|
|
77
|
-
*/
|
|
78
|
-
env: Record<string, string>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface Config {
|
|
82
|
-
name: NameConfig;
|
|
83
|
-
auto: AutoConfig;
|
|
84
|
-
exec: ExecConfig;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── defaults ───────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
export function defaultConfig(): Config {
|
|
90
|
-
return {
|
|
91
|
-
name: {
|
|
92
|
-
model: 'claude-haiku-4-5',
|
|
93
|
-
timeout: 3_000, // 3s
|
|
94
|
-
quietMissingAPIKey: false,
|
|
95
|
-
},
|
|
96
|
-
auto: {
|
|
97
|
-
tmux: 'never',
|
|
98
|
-
handoff: 'ask',
|
|
99
|
-
spawnCommand: '',
|
|
100
|
-
},
|
|
101
|
-
exec: {
|
|
102
|
-
env: {},
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── config file path ───────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
export function configFilePath(): string {
|
|
110
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
111
|
-
const base = xdg && xdg.length > 0 ? xdg : join(home(), '.config');
|
|
112
|
-
return join(base, 'fnclaude', 'config.toml');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── helpers ────────────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* parseBoolEnv returns true for "1", "true", "yes" (case-insensitive),
|
|
119
|
-
* false for anything else.
|
|
120
|
-
*/
|
|
121
|
-
export function parseBoolEnv(v: string): boolean {
|
|
122
|
-
switch (v.trim().toLowerCase()) {
|
|
123
|
-
case '1':
|
|
124
|
-
case 'true':
|
|
125
|
-
case 'yes':
|
|
126
|
-
return true;
|
|
127
|
-
default:
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Result of a normalize-mode call: the validated value plus an optional
|
|
134
|
-
* warning describing any fallback that was applied. Callers thread the
|
|
135
|
-
* warning into their own returned warnings list rather than mutating a
|
|
136
|
-
* module-global sink.
|
|
137
|
-
*/
|
|
138
|
-
export interface NormalizeResult<T> {
|
|
139
|
-
value: T;
|
|
140
|
-
warning: string | undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* normalizeTmuxMode validates against the supported set and falls back to
|
|
145
|
-
* "never" for anything else, returning the fallback value and an optional
|
|
146
|
-
* warning describing what was rejected (the empty-string case is the
|
|
147
|
-
* absent-value default path and produces no warning).
|
|
148
|
-
*/
|
|
149
|
-
export function normalizeTmuxMode(v: string): NormalizeResult<TmuxMode> {
|
|
150
|
-
if (v === 'never' || v === 'worktree') return { value: v, warning: undefined };
|
|
151
|
-
if (v === '') return { value: 'never', warning: undefined };
|
|
152
|
-
return {
|
|
153
|
-
value: 'never',
|
|
154
|
-
warning: `fnclaude: auto.tmux=${JSON.stringify(v)} is not a valid mode (use "never" or "worktree"), falling back to "never"`,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* normalizeHandoffMode validates against the supported set and falls back
|
|
160
|
-
* to "ask" for anything else (with an optional warning, except empty
|
|
161
|
-
* string). Valid: "never", "ask", or a non-negative integer (as a string).
|
|
162
|
-
*/
|
|
163
|
-
export function normalizeHandoffMode(v: string): NormalizeResult<HandoffMode> {
|
|
164
|
-
if (v === 'never' || v === 'ask') return { value: v, warning: undefined };
|
|
165
|
-
if (v === '') return { value: 'ask', warning: undefined };
|
|
166
|
-
// Non-negative integer (no decimal, no unit). The regex guarantees the
|
|
167
|
-
// template-literal shape, which TS's type narrowing can't infer from a
|
|
168
|
-
// .test() call alone — so assert it explicitly once.
|
|
169
|
-
if (/^\d+$/.test(v)) return { value: v as `${number}`, warning: undefined };
|
|
170
|
-
return {
|
|
171
|
-
value: 'ask',
|
|
172
|
-
warning: `fnclaude: auto.handoff=${JSON.stringify(v)} is not a valid mode (use "never", "ask", or a non-negative integer), falling back to "ask"`,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* parseDuration accepts a Go-style duration string (e.g., "3s", "150ms",
|
|
178
|
-
* "1m30s") and returns the equivalent in milliseconds. Returns undefined
|
|
179
|
-
* on parse failure. This is the same surface as Go's time.ParseDuration
|
|
180
|
-
* for the config use-case (we don't need ns/us precision).
|
|
181
|
-
*/
|
|
182
|
-
export function parseDuration(s: string): number | undefined {
|
|
183
|
-
if (!s) return undefined;
|
|
184
|
-
// Whole number with unit suffix(es).
|
|
185
|
-
// Units: ns, us, µs, ms, s, m, h. (We support all common units.)
|
|
186
|
-
const unitToMs: Record<string, number> = {
|
|
187
|
-
ns: 1e-6,
|
|
188
|
-
us: 1e-3,
|
|
189
|
-
'µs': 1e-3,
|
|
190
|
-
ms: 1,
|
|
191
|
-
s: 1_000,
|
|
192
|
-
m: 60_000,
|
|
193
|
-
h: 3_600_000,
|
|
194
|
-
};
|
|
195
|
-
const re = /([0-9]*\.?[0-9]+)(ns|us|µs|ms|s|m|h)/g;
|
|
196
|
-
let total = 0;
|
|
197
|
-
let matched = 0;
|
|
198
|
-
let consumed = 0;
|
|
199
|
-
// RegExp.exec returns null for "no match" — third-party API shape, kept
|
|
200
|
-
// as null rather than coerced.
|
|
201
|
-
let m: RegExpExecArray | null;
|
|
202
|
-
while ((m = re.exec(s)) !== null) {
|
|
203
|
-
if (m.index !== consumed) return undefined; // gap between matches
|
|
204
|
-
const num = parseFloat(m[1] as string);
|
|
205
|
-
const unit = m[2] as string;
|
|
206
|
-
if (!Number.isFinite(num) || num < 0) return undefined;
|
|
207
|
-
total += num * (unitToMs[unit] as number);
|
|
208
|
-
consumed = m.index + m[0].length;
|
|
209
|
-
matched++;
|
|
210
|
-
}
|
|
211
|
-
if (matched === 0 || consumed !== s.length) return undefined;
|
|
212
|
-
return total;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ── raw TOML shape (mirrors the Go rawConfig) ──────────────────────────────
|
|
216
|
-
|
|
217
|
-
interface RawConfig {
|
|
218
|
-
name?: {
|
|
219
|
-
model?: string;
|
|
220
|
-
timeout?: string;
|
|
221
|
-
quiet_missing_api_key?: boolean;
|
|
222
|
-
};
|
|
223
|
-
auto?: {
|
|
224
|
-
tmux?: string;
|
|
225
|
-
handoff?: string;
|
|
226
|
-
spawn_command?: string;
|
|
227
|
-
// legacy keys (silently ignored): dangerously_skip_permissions, ide
|
|
228
|
-
[k: string]: unknown;
|
|
229
|
-
};
|
|
230
|
-
exec?: {
|
|
231
|
-
env?: Record<string, string>;
|
|
232
|
-
};
|
|
233
|
-
[k: string]: unknown;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── loadConfig ─────────────────────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Result of `loadConfig` — the merged Config plus any non-fatal warnings
|
|
240
|
-
* raised during the load (malformed file, invalid mode value, bogus
|
|
241
|
-
* duration, etc.). The caller threads warnings into the deferred-flush
|
|
242
|
-
* mechanism in `main.ts`; this module owns no global mutable state.
|
|
243
|
-
*/
|
|
244
|
-
export interface LoadConfigResult {
|
|
245
|
-
config: Config;
|
|
246
|
-
warnings: readonly string[];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* loadConfig loads the configuration from the config file and environment
|
|
251
|
-
* variables, merging over built-in defaults. Order of precedence:
|
|
252
|
-
*
|
|
253
|
-
* env var > config file > built-in default
|
|
254
|
-
*
|
|
255
|
-
* A missing config file is not an error. A malformed config file produces
|
|
256
|
-
* a warning and falls back to defaults.
|
|
257
|
-
*/
|
|
258
|
-
export function loadConfig(): LoadConfigResult {
|
|
259
|
-
const cfg = defaultConfig();
|
|
260
|
-
const warnings: string[] = [];
|
|
261
|
-
const path = configFilePath();
|
|
262
|
-
|
|
263
|
-
const recordNormalize = <T>(
|
|
264
|
-
r: NormalizeResult<T>,
|
|
265
|
-
set: (v: T) => void,
|
|
266
|
-
): void => {
|
|
267
|
-
set(r.value);
|
|
268
|
-
if (r.warning !== undefined) warnings.push(r.warning);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
if (existsSync(path)) {
|
|
272
|
-
let raw: RawConfig | undefined;
|
|
273
|
-
try {
|
|
274
|
-
const body = readFileSync(path, 'utf8');
|
|
275
|
-
raw = Bun.TOML.parse(body) as RawConfig;
|
|
276
|
-
} catch (err) {
|
|
277
|
-
warnings.push(
|
|
278
|
-
`fnclaude: config file ${path} is malformed, using defaults: ${errorMessage(err)}`,
|
|
279
|
-
);
|
|
280
|
-
raw = undefined;
|
|
281
|
-
}
|
|
282
|
-
if (raw) {
|
|
283
|
-
if (raw.name?.model) cfg.name.model = raw.name.model;
|
|
284
|
-
if (raw.name?.timeout) {
|
|
285
|
-
const d = parseDuration(raw.name.timeout);
|
|
286
|
-
if (d !== undefined) {
|
|
287
|
-
cfg.name.timeout = d;
|
|
288
|
-
} else {
|
|
289
|
-
warnings.push(
|
|
290
|
-
`fnclaude: invalid timeout ${JSON.stringify(raw.name.timeout)} in config, using default`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (typeof raw.name?.quiet_missing_api_key === 'boolean') {
|
|
295
|
-
cfg.name.quietMissingAPIKey = raw.name.quiet_missing_api_key;
|
|
296
|
-
}
|
|
297
|
-
if (typeof raw.auto?.tmux === 'string' && raw.auto.tmux !== '') {
|
|
298
|
-
recordNormalize(normalizeTmuxMode(raw.auto.tmux), (v) => {
|
|
299
|
-
cfg.auto.tmux = v;
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
if (typeof raw.auto?.handoff === 'string' && raw.auto.handoff !== '') {
|
|
303
|
-
recordNormalize(normalizeHandoffMode(raw.auto.handoff), (v) => {
|
|
304
|
-
cfg.auto.handoff = v;
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
if (
|
|
308
|
-
typeof raw.auto?.spawn_command === 'string' &&
|
|
309
|
-
raw.auto.spawn_command !== ''
|
|
310
|
-
) {
|
|
311
|
-
cfg.auto.spawnCommand = raw.auto.spawn_command;
|
|
312
|
-
}
|
|
313
|
-
if (raw.exec?.env && Object.keys(raw.exec.env).length > 0) {
|
|
314
|
-
cfg.exec.env = { ...raw.exec.env };
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Env-var overrides.
|
|
320
|
-
const e = process.env;
|
|
321
|
-
if (e.FNCLAUDE_NAME_MODEL) cfg.name.model = e.FNCLAUDE_NAME_MODEL;
|
|
322
|
-
if (e.FNCLAUDE_NAME_TIMEOUT) {
|
|
323
|
-
const d = parseDuration(e.FNCLAUDE_NAME_TIMEOUT);
|
|
324
|
-
if (d !== undefined) {
|
|
325
|
-
cfg.name.timeout = d;
|
|
326
|
-
} else {
|
|
327
|
-
warnings.push(
|
|
328
|
-
`fnclaude: invalid FNCLAUDE_NAME_TIMEOUT ${JSON.stringify(e.FNCLAUDE_NAME_TIMEOUT)}, using current value`,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (e.FNCLAUDE_QUIET_MISSING_API_KEY) {
|
|
333
|
-
cfg.name.quietMissingAPIKey = parseBoolEnv(e.FNCLAUDE_QUIET_MISSING_API_KEY);
|
|
334
|
-
}
|
|
335
|
-
if (e.FNCLAUDE_TMUX) {
|
|
336
|
-
recordNormalize(normalizeTmuxMode(e.FNCLAUDE_TMUX), (v) => {
|
|
337
|
-
cfg.auto.tmux = v;
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
if (e.FNCLAUDE_HANDOFF) {
|
|
341
|
-
recordNormalize(normalizeHandoffMode(e.FNCLAUDE_HANDOFF), (v) => {
|
|
342
|
-
cfg.auto.handoff = v;
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
if (e.FNCLAUDE_SPAWN_COMMAND) cfg.auto.spawnCommand = e.FNCLAUDE_SPAWN_COMMAND;
|
|
346
|
-
|
|
347
|
-
return { config: cfg, warnings };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ── envFromConfig ─────────────────────────────────────────────────────────
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* envFromConfig returns cfg.exec.env rendered as a sorted array of
|
|
354
|
-
* "KEY=VALUE" strings, ready to append to the parent's env before
|
|
355
|
-
* spawning claude. Sort order is deterministic so debug output is stable.
|
|
356
|
-
*
|
|
357
|
-
* Precedence rule: callers append this AFTER the inherited env; if the
|
|
358
|
-
* spawning API resolves duplicate keys by last-wins (Node's
|
|
359
|
-
* child_process.spawn does, since it accepts an object), a configured key
|
|
360
|
-
* here overrides the inherited value of the same name when callers merge
|
|
361
|
-
* appropriately.
|
|
362
|
-
*/
|
|
363
|
-
export function envFromConfig(cfg: Config): string[] {
|
|
364
|
-
const env = cfg.exec?.env;
|
|
365
|
-
if (!env) return [];
|
|
366
|
-
const keys = Object.keys(env).sort();
|
|
367
|
-
if (keys.length === 0) return [];
|
|
368
|
-
return keys.map((k) => `${k}=${env[k]}`);
|
|
369
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Error-handling helpers shared across the CLI.
|
|
2
|
-
//
|
|
3
|
-
// `errorMessage` collapses the "throw value can be anything" branch into a
|
|
4
|
-
// single safe path: real Errors yield their `.message`, anything else gets
|
|
5
|
-
// stringified. Used at every catch-and-format site that previously did
|
|
6
|
-
// `(err as Error).message` — a cast that silently produces `undefined` (and
|
|
7
|
-
// then crashes the error-handling path itself) whenever the thrown value
|
|
8
|
-
// isn't actually an Error instance.
|
|
9
|
-
|
|
10
|
-
export function errorMessage(err: unknown): string {
|
|
11
|
-
if (err instanceof Error) return err.message;
|
|
12
|
-
return String(err);
|
|
13
|
-
}
|
package/src/handoff.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Port of src/handoff.go (fnclaude/fnclaude Go reference).
|
|
2
|
-
//
|
|
3
|
-
// Resolves the directory and per-session filenames that fnc uses for its
|
|
4
|
-
// AF_UNIX socket (parent listener) and handoff-summary scratch files, and
|
|
5
|
-
// renders the FNCLAUDE_HANDOFF + FNC_SOCKET pair to inject into the claude
|
|
6
|
-
// child's environment.
|
|
7
|
-
|
|
8
|
-
import { randomBytes } from 'node:crypto';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Resolve the directory where handoff files (socket and summary content)
|
|
14
|
-
* should live. Single point of truth, used by both handoffSocketPath and
|
|
15
|
-
* handoffContentPath.
|
|
16
|
-
*
|
|
17
|
-
* Preference order:
|
|
18
|
-
*
|
|
19
|
-
* 1. $XDG_RUNTIME_DIR — the Linux/systemd ideal: tmpfs, mode 700, auto-
|
|
20
|
-
* cleared on user logout. Permissions are restrictive by default so
|
|
21
|
-
* other users on the box can't read handoff content (often includes
|
|
22
|
-
* conversation context, tool-call results, or other session-private
|
|
23
|
-
* data).
|
|
24
|
-
* 2. os.tmpdir() — the OS-native fallback. On Unix this honors $TMPDIR
|
|
25
|
-
* then falls back to /tmp; on macOS launchd sets $TMPDIR to a per-user
|
|
26
|
-
* mode-700 dir under /var/folders/; on Windows it returns %TMP% /
|
|
27
|
-
* %TEMP% / %USERPROFILE%. Using tmpdir() (vs a hardcoded "/tmp"
|
|
28
|
-
* literal) is what makes this code portable.
|
|
29
|
-
*/
|
|
30
|
-
export function handoffBaseDir(): string {
|
|
31
|
-
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
32
|
-
if (xdg && xdg.length > 0) return xdg;
|
|
33
|
-
return tmpdir();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Return the AF_UNIX socket path the parent listens on for MCP-side
|
|
38
|
-
* Requests. PID is included so concurrent fnclaude sessions don't collide.
|
|
39
|
-
*
|
|
40
|
-
* AF_UNIX paths on Linux/Darwin are limited to ~108 bytes (sun_path
|
|
41
|
-
* length); handoffBaseDir + "fnclaude-mcp-<pid>.sock" stays well under
|
|
42
|
-
* that cap for every realistic PID.
|
|
43
|
-
*/
|
|
44
|
-
export function handoffSocketPath(pid: number): string {
|
|
45
|
-
return join(handoffBaseDir(), `fnclaude-mcp-${pid}.sock`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Return the env-var entries (KEY=VALUE strings) that fnc injects into
|
|
50
|
-
* the claude child's environment when auto-handoff is active.
|
|
51
|
-
*
|
|
52
|
-
* - FNCLAUDE_HANDOFF=<mode> tells the noop session which UX to use when
|
|
53
|
-
* proposing a project transfer.
|
|
54
|
-
* - FNC_SOCKET=<path> tells the `fnclaude mcp` subprocess (spawned by
|
|
55
|
-
* claude) where to dial the parent's AF_UNIX listener.
|
|
56
|
-
*
|
|
57
|
-
* `mode` is the resolved Auto.Handoff value ("never", "ask", or a
|
|
58
|
-
* non-negative integer); all three are valid here because the listener
|
|
59
|
-
* still needs to answer OpRestart and OpCopy regardless of the noop
|
|
60
|
-
* proposal mode.
|
|
61
|
-
*/
|
|
62
|
-
export function handoffEnv(mode: string, socketPath: string): string[] {
|
|
63
|
-
return [`FNCLAUDE_HANDOFF=${mode}`, `FNC_SOCKET=${socketPath}`];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Return a unique path where the listener can write the handoff summary
|
|
68
|
-
* content for an OpSwitch Request. Uses a random hex token to guarantee
|
|
69
|
-
* uniqueness — no risk of collision via PID recycling even if the user
|
|
70
|
-
* delays pasting a rendered relaunch command for hours.
|
|
71
|
-
*/
|
|
72
|
-
export function handoffContentPath(): string {
|
|
73
|
-
const base = handoffBaseDir();
|
|
74
|
-
// 8 bytes → 16 hex chars, 64 bits of entropy.
|
|
75
|
-
const token = randomBytes(8).toString('hex');
|
|
76
|
-
return join(base, `fnclaude-handoff-content-${token}.md`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Configures fnc's auto-handoff machinery for a single PTY run. Passed as
|
|
81
|
-
* a parameter to the spawn entry point; `null` means handoff disabled,
|
|
82
|
-
* no env injection, no socket listener (the legacy code path).
|
|
83
|
-
*/
|
|
84
|
-
export interface HandoffSpec {
|
|
85
|
-
/**
|
|
86
|
-
* Resolved Auto.Handoff value ("never", "ask", or a non-negative
|
|
87
|
-
* integer-as-string). The parent's socket-listener dispatcher consults
|
|
88
|
-
* Mode when answering OpSwitch (initial, non-Confirmed) requests.
|
|
89
|
-
*/
|
|
90
|
-
mode: string;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Filesystem path of the AF_UNIX socket the parent listens on for
|
|
94
|
-
* MCP-side Requests. fnc generates this per-session from its PID so
|
|
95
|
-
* concurrent sessions don't collide. The MCP subprocess receives it
|
|
96
|
-
* via $FNC_SOCKET and dials it for every tool invocation.
|
|
97
|
-
*/
|
|
98
|
-
socketPath: string;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Snapshot of process.argv from the fnclaude invocation (typically
|
|
102
|
-
* `process.argv.slice(2)`), threaded through to the socket listener so
|
|
103
|
-
* handleRestart and handleSwitch can preserve user-supplied flags across
|
|
104
|
-
* the relaunch. Empty array is allowed — handlers fall back to the
|
|
105
|
-
* flag-less relaunch shape.
|
|
106
|
-
*/
|
|
107
|
-
originalArgs: string[];
|
|
108
|
-
}
|
package/src/help.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// Help text + flag scanners for fnclaude's own --help / --version.
|
|
2
|
-
// Ported from src/main.go (helpText, wantsHelp, wantsVersion) in the Go
|
|
3
|
-
// reference.
|
|
4
|
-
|
|
5
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Binary version. Read from package.json at build time, inlined by the
|
|
9
|
-
* TypeScript compiler and bundler.
|
|
10
|
-
*/
|
|
11
|
-
export let version = pkg.version;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Test helper: override the reported version. Kept narrow on purpose —
|
|
15
|
-
* production callers should not mutate this at runtime.
|
|
16
|
-
*/
|
|
17
|
-
export function setVersion(v: string): void {
|
|
18
|
-
version = v;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* True when the user passed -v or --version anywhere in argv BEFORE a
|
|
23
|
-
* literal "--" terminator. fnclaude shadows claude's -v short flag (the
|
|
24
|
-
* only lowercase short fnclaude claims); to reach claude's own --version,
|
|
25
|
-
* the user runs `claude --version` directly.
|
|
26
|
-
*/
|
|
27
|
-
export function wantsVersion(argv: readonly string[]): boolean {
|
|
28
|
-
for (const t of argv) {
|
|
29
|
-
if (t === '--') return false;
|
|
30
|
-
if (t === '-v' || t === '--version') return true;
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* True when the user passed -h or --help anywhere in argv BEFORE a literal
|
|
37
|
-
* "--" terminator. Tokens after "--" are part of the prompt to claude and
|
|
38
|
-
* aren't fnclaude flags.
|
|
39
|
-
*/
|
|
40
|
-
export function wantsHelp(argv: readonly string[]): boolean {
|
|
41
|
-
for (const t of argv) {
|
|
42
|
-
if (t === '--') return false;
|
|
43
|
-
if (t === '-h' || t === '--help') return true;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Full --help text. Sourced verbatim from src/main.go's `helpText` constant
|
|
50
|
-
* in the Go reference; keep in sync when either side changes.
|
|
51
|
-
*/
|
|
52
|
-
export const helpText = `fnclaude — claude CLI launcher with quality-of-life features
|
|
53
|
-
|
|
54
|
-
Usage:
|
|
55
|
-
fnclaude [MODEL] [EFFORT] [CWD [WORKTREE]] [FLAGS...] [-- PROMPT]
|
|
56
|
-
|
|
57
|
-
Magic positional words (positions 1+2 only, before any path):
|
|
58
|
-
Position 1 — model alias: opus | sonnet | haiku → --model <alias>
|
|
59
|
-
Position 2 — effort level: low | medium | high | xhigh | max → --effort <level>
|
|
60
|
-
(only honored when position 1 was a model alias)
|
|
61
|
-
To use a directory literally named opus/max/etc., prefix with ./
|
|
62
|
-
|
|
63
|
-
Subcommand positionals (any positional slot, max one per invocation):
|
|
64
|
-
resume | res → --resume (session picker)
|
|
65
|
-
continue | con → --continue (resume most recent)
|
|
66
|
-
fork | fk → --resume --fork-session (picker; fork on select)
|
|
67
|
-
Order-independent: "fnc resume opus" and "fnc opus resume" parse equivalently.
|
|
68
|
-
To use a directory literally named one of these, prefix with ./
|
|
69
|
-
|
|
70
|
-
Positional paths (max 2 after magic/subcommand tokens):
|
|
71
|
-
1st remaining → cwd to launch claude in (fallback $XDG_CONFIG_HOME/fnclaude/noop)
|
|
72
|
-
2nd remaining → worktree name (same as -w <name>); see Worktree intercept below
|
|
73
|
-
3rd+ remaining → error. Use -A/--also for extra dirs.
|
|
74
|
-
|
|
75
|
-
Reserved subcommands:
|
|
76
|
-
mcp [--noop] — internal MCP server (invoked automatically by claude
|
|
77
|
-
via injected --mcp-config; not for direct use)
|
|
78
|
-
To use a directory literally named mcp, prefix with ./
|
|
79
|
-
|
|
80
|
-
fnclaude-owned flags:
|
|
81
|
-
-A, --also <dir> additional extra-dir (repeatable; the only way to add
|
|
82
|
-
extra dirs — positional extras no longer supported)
|
|
83
|
-
--no-tmux suppress auto-tmux injection for this invocation
|
|
84
|
-
-h, --help show this help
|
|
85
|
-
-v, --version print fnclaude's version and exit
|
|
86
|
-
(shadows claude's -v; use \`claude --version\` directly for that)
|
|
87
|
-
|
|
88
|
-
Capital-letter shortcuts (translate to claude long-form flags):
|
|
89
|
-
-B → --brief -M → --permission-mode <mode>
|
|
90
|
-
-C → --chrome -P → --from-pr [value]
|
|
91
|
-
-D → --dangerously-skip-permissions -R → --remote-control [name]
|
|
92
|
-
-F → --fork-session -T → --tmux [classic]
|
|
93
|
-
-G → --agent <agent> -V → --verbose
|
|
94
|
-
-I → --ide -W → --allowedTools <tools>
|
|
95
|
-
|
|
96
|
-
All other claude flags pass through verbatim — run \`claude --help\` for the full
|
|
97
|
-
reference. POSIX collapsing is supported (-BVC = -B -V -C); only the last flag in
|
|
98
|
-
a collapsed group may take a value.
|
|
99
|
-
|
|
100
|
-
Cross-cwd resume: when claude shows the resume picker and you select a session
|
|
101
|
-
from a different cwd, fnclaude transparently re-launches in that cwd.
|
|
102
|
-
|
|
103
|
-
Worktree intercept: -w <name> matching an existing worktree of the project repo
|
|
104
|
-
swaps fnclaude's cwd to that worktree. Non-matching names pass through and the
|
|
105
|
-
new worktree's name is also set as the session --name.
|
|
106
|
-
|
|
107
|
-
Auto-name: when --, a prompt, and no --name/-n flag are all present, fnclaude
|
|
108
|
-
generates a 1-3 word session label via Haiku. With ANTHROPIC_API_KEY set, the
|
|
109
|
-
SDK is called directly; without it, fnclaude shells out to \`claude -p\` (which
|
|
110
|
-
uses your subscription auth). Falls back silently to a heuristic if both fail.
|
|
111
|
-
|
|
112
|
-
Config file:
|
|
113
|
-
$XDG_CONFIG_HOME/fnclaude/config.toml (or ~/.config/fnclaude/config.toml)
|
|
114
|
-
[exec.env] NAME = "value" entries are injected into claude's environment.
|
|
115
|
-
|
|
116
|
-
Environment variables (override config; precedence: CLI > env > config > default):
|
|
117
|
-
ANTHROPIC_API_KEY direct-API auth for auto-name (else shells \`claude -p\`)
|
|
118
|
-
FNCLAUDE_NAME_MODEL model for auto-name (default: claude-haiku-4-5)
|
|
119
|
-
FNCLAUDE_NAME_TIMEOUT auto-name LLM timeout (default: 3s API / 15s CLI)
|
|
120
|
-
FNCLAUDE_QUIET_MISSING_API_KEY deprecated no-op (warning was removed)
|
|
121
|
-
FNCLAUDE_TMUX never | worktree | always (default: never)
|
|
122
|
-
FNCLAUDE_HANDOFF never | ask | <N> seconds (default: ask)
|
|
123
|
-
controls noop router's proposing UX
|
|
124
|
-
(user-initiated project switches always
|
|
125
|
-
available; see README)
|
|
126
|
-
FNC_PROMPTS_DIR override install-dir prompts location
|
|
127
|
-
(default: <exe-dir>/prompts or
|
|
128
|
-
<exe-dir>/../share/fnclaude/prompts)
|
|
129
|
-
|
|
130
|
-
Examples:
|
|
131
|
-
fnclaude # interactive in ~/.config/fnclaude/noop
|
|
132
|
-
fnclaude opus max ~/src/proj # opus + max effort, launch in ~/src/proj
|
|
133
|
-
fnclaude ~/src/proj my-wt # cwd + worktree (same as -w my-wt)
|
|
134
|
-
fnclaude ~/src/proj -A ~/src/extra # main + extra dir (mcp/settings injected)
|
|
135
|
-
fnclaude ~/src/proj -- "fix the bug" # auto-name from prompt
|
|
136
|
-
fnclaude -A docs/ ~/src/proj -V # ergonomic flag form
|
|
137
|
-
|
|
138
|
-
For more, see https://github.com/fnclaude/fnclaude
|
|
139
|
-
`;
|