@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.
Files changed (100) hide show
  1. package/bin/fnc.js +34 -79
  2. package/package.json +6 -9
  3. package/share/fnclaude/templates/handoff.template.md +11 -0
  4. package/src/argv/classify.ts +48 -0
  5. package/src/argv/expand.ts +51 -0
  6. package/src/argv/intake.ts +52 -0
  7. package/src/argv/magic.ts +103 -0
  8. package/src/argv/parse.ts +213 -0
  9. package/src/argv/preserve-args.ts +333 -0
  10. package/src/argv/sentinel.ts +41 -0
  11. package/src/argv/short-flags.ts +152 -0
  12. package/src/config/load.ts +116 -0
  13. package/src/handoff/awaiter.ts +140 -0
  14. package/src/handoff/clean-env.ts +45 -0
  15. package/src/handoff/kill-and-exec.ts +110 -0
  16. package/src/handoff/spawn-launcher.ts +185 -0
  17. package/src/handoff/summary-file.ts +86 -0
  18. package/src/handoff/trigger.ts +90 -0
  19. package/src/help-version.ts +151 -0
  20. package/src/launch/compose-env.ts +34 -0
  21. package/src/launch/cross-cwd-parse.ts +69 -0
  22. package/src/launch/cross-cwd-relaunch.ts +95 -0
  23. package/src/launch/find-claude.ts +52 -0
  24. package/src/launch/live-permission-reader.ts +133 -0
  25. package/src/launch/ring-buffer.ts +92 -0
  26. package/src/main.ts +580 -437
  27. package/src/mcp/dispatch.ts +240 -0
  28. package/src/mcp/handlers/clipboard-backends.ts +176 -0
  29. package/src/mcp/handlers/clipboard.ts +62 -0
  30. package/src/mcp/handlers/restart.ts +156 -0
  31. package/src/mcp/handlers/spawn.ts +219 -0
  32. package/src/mcp/handlers/switch.ts +272 -0
  33. package/src/mcp/inject-config.ts +59 -0
  34. package/src/mcp/jsonrpc-server.ts +154 -0
  35. package/src/mcp/listener.ts +141 -0
  36. package/src/mcp/parent-dispatch.ts +154 -0
  37. package/src/mcp/socket-path.ts +48 -0
  38. package/src/mcp/wire.ts +181 -0
  39. package/src/name/auto-name.ts +162 -0
  40. package/src/name/llm-prompt.ts +14 -0
  41. package/src/name/sanitize.ts +57 -0
  42. package/src/name/sdk-llm.ts +42 -0
  43. package/src/noop/seed.ts +63 -0
  44. package/src/noop/template-source.ts +62 -0
  45. package/src/path/ensure-cwd.ts +95 -0
  46. package/src/path/resolve.ts +58 -0
  47. package/src/prompts/dir.ts +61 -0
  48. package/src/prompts/load.ts +100 -0
  49. package/src/prompts/select.ts +43 -0
  50. package/src/repo/clone-exec.ts +37 -0
  51. package/src/repo/clone.ts +45 -0
  52. package/src/repo/gh-runner.ts +68 -0
  53. package/src/repo/host-aliases.ts +58 -0
  54. package/src/repo/owner-lookup.ts +71 -0
  55. package/src/repo/ref.ts +146 -0
  56. package/src/repo/repo-settings.ts +99 -0
  57. package/src/repo/resolve-input.ts +179 -0
  58. package/src/repo/template.ts +92 -0
  59. package/src/warnings/buffer.ts +39 -0
  60. package/src/worktree/auto-tmux.ts +45 -0
  61. package/src/worktree/git-list.ts +73 -0
  62. package/src/worktree/intercept.ts +150 -0
  63. package/bin/preflight.js +0 -66
  64. package/prompts/agent-pitfall.md +0 -1
  65. package/prompts/noop-router.md +0 -186
  66. package/prompts/project-switch.md +0 -64
  67. package/prompts/restart.md +0 -50
  68. package/prompts/spawn.md +0 -62
  69. package/src/argParser.ts +0 -367
  70. package/src/args/preserve.ts +0 -338
  71. package/src/args.ts +0 -239
  72. package/src/argv.ts +0 -203
  73. package/src/autoname.ts +0 -273
  74. package/src/clipboard.ts +0 -149
  75. package/src/config.ts +0 -369
  76. package/src/errors.ts +0 -13
  77. package/src/handoff.ts +0 -108
  78. package/src/help.ts +0 -139
  79. package/src/hostAliases.ts +0 -139
  80. package/src/index.ts +0 -120
  81. package/src/mcp/client.ts +0 -645
  82. package/src/mcp/protocol.ts +0 -445
  83. package/src/mcp/socketListener.ts +0 -540
  84. package/src/noop.ts +0 -106
  85. package/src/passthrough.ts +0 -36
  86. package/src/paths.ts +0 -55
  87. package/src/prompts.ts +0 -279
  88. package/src/pty/unix.ts +0 -429
  89. package/src/pty/windows.ts +0 -125
  90. package/src/pty.ts +0 -380
  91. package/src/repoRef.ts +0 -158
  92. package/src/repoSettings.ts +0 -144
  93. package/src/resolver.ts +0 -519
  94. package/src/sanitize.ts +0 -120
  95. package/src/sessionState.ts +0 -220
  96. package/src/silentRelaunch.ts +0 -178
  97. package/src/spawn.ts +0 -163
  98. package/src/template.ts +0 -44
  99. package/src/warnings.ts +0 -34
  100. 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
- `;