@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/paths.ts DELETED
@@ -1,55 +0,0 @@
1
- // Path helpers. Ported from expandTildePath in src/resolver.go.
2
-
3
- import { realpathSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { join } from 'node:path';
6
- import process from 'node:process';
7
-
8
- /**
9
- * Expand a leading "~" or "~/" to the user's home directory.
10
- *
11
- * - "~" → homedir()
12
- * - "~/foo" → join(homedir(), "foo")
13
- * - Everything else (including mid-token "~", "~user/...", absolute paths,
14
- * relative paths, empty string) is returned unchanged. This matches the
15
- * Go reference and POSIX shell behaviour: only bare `~` and `~/` expand.
16
- *
17
- * The user's home dir comes from `os.homedir()` rather than being threaded
18
- * through every call site — the Go version passes `home` explicitly because
19
- * Go style discourages global env reads inside helpers; in TS the harness
20
- * is fine.
21
- */
22
- export function expandTildePath(p: string): string {
23
- if (p === '~') return homedir();
24
- if (p.startsWith('~/')) return join(homedir(), p.slice(2));
25
- return p;
26
- }
27
-
28
- /**
29
- * Return the absolute, symlink-resolved path to this fnclaude script.
30
- *
31
- * Preference order:
32
- * 1. `process.argv[1]` — the CLI script path. Anchors to the script's
33
- * neighbours (so `prompts/`, `mcp` subcommand spawn, spawn-launcher
34
- * `{bin}` substitution all resolve relative to the script), not the
35
- * Bun interpreter under `process.execPath`.
36
- * 2. `process.execPath` — fallback when `argv[1]` is empty.
37
- *
38
- * Symlinks are resolved when possible so callers receive the real
39
- * destination path; failure to resolve is non-fatal (returns the
40
- * unresolved path).
41
- *
42
- * This used to be duplicated in `spawn.ts:selfPath`, `argv.ts`'s
43
- * `buildFnclaudeMCPConfigJSON`, and `prompts.ts:findPromptsDir` — three
44
- * verbatim copies of the same six lines. Single source of truth here.
45
- */
46
- export function resolveSelfPath(): string {
47
- const argv1 = process.argv.length > 1 ? process.argv[1] : undefined;
48
- let exe = argv1 !== undefined && argv1 !== '' ? argv1 : process.execPath;
49
- try {
50
- exe = realpathSync(exe);
51
- } catch {
52
- // symlink resolution failure is non-fatal — use the unresolved path
53
- }
54
- return exe;
55
- }
package/src/prompts.ts DELETED
@@ -1,279 +0,0 @@
1
- // System-prompt fragment loading. Ported from src/prompts.go.
2
- //
3
- // PromptSet holds the five fragments fnclaude can inject into a claude
4
- // launch via --append-system-prompt. Each field is the literal text of one
5
- // .md file from the install dir, trimmed of trailing whitespace.
6
- //
7
- // An empty string means the file was missing — callers MUST skip injection
8
- // rather than appending an empty fragment. loadPrompts returns deferred
9
- // warnings the caller can surface to the user after session setup.
10
- //
11
- // The Go reference is sync (it uses `os.ReadFile` directly). The TS port is
12
- // async to fit Bun/Node idioms; CLI startup is already async-friendly so
13
- // awaiting `loadPrompts()` adds no observable delay.
14
-
15
- import { statSync } from 'node:fs';
16
- import { readFile, stat } from 'node:fs/promises';
17
- import { dirname, join } from 'node:path';
18
- import process from 'node:process';
19
- import { fileURLToPath } from 'node:url';
20
- import { resolveSelfPath } from './paths.js';
21
-
22
- export interface PromptSet {
23
- readonly agentPitfall: string;
24
- readonly projectSwitch: string;
25
- readonly spawn: string;
26
- readonly restart: string;
27
- readonly noopRouter: string;
28
- }
29
-
30
- const EMPTY_PROMPT_SET: PromptSet = {
31
- agentPitfall: '',
32
- projectSwitch: '',
33
- spawn: '',
34
- restart: '',
35
- noopRouter: '',
36
- };
37
-
38
- const PROMPT_FILE_NAMES: Record<keyof PromptSet, string> = {
39
- agentPitfall: 'agent-pitfall.md',
40
- projectSwitch: 'project-switch.md',
41
- spawn: 'spawn.md',
42
- restart: 'restart.md',
43
- noopRouter: 'noop-router.md',
44
- };
45
-
46
- export interface LoadPromptsResult {
47
- readonly prompts: PromptSet;
48
- readonly warnings: string[];
49
- }
50
-
51
- /**
52
- * Locate the prompts install dir and read each known fragment. Search order:
53
- * 1. `$FNC_PROMPTS_DIR` (test/override hook).
54
- * 2. `<exe-dir>/prompts/` — Go-style dev layout (exe + sibling prompts/).
55
- * 3. `<exe-dir>/../prompts/` — npm package layout: bin/fnc.js's parent
56
- * contains the shipped prompts/. This is the production path for any
57
- * `npm i -g @fnclaude/cli` install.
58
- * 4. `<exe-dir>/../share/fnclaude/prompts/` — FHS/AUR install layout.
59
- * 5. `<module-dir>/../prompts/` — umbrella-install layout: when invoked
60
- * via `npm i -g fnclaude` (the umbrella package), `process.argv[1]`
61
- * points at the umbrella's `bin/fnc.js`, which `await import`s into
62
- * `@fnclaude/cli/bin/fnc.js`. Candidates 2–4 all anchor at the
63
- * umbrella's exe-dir and miss the cli package entirely. Anchoring at
64
- * this module's own location reliably reaches the cli package root,
65
- * since `prompts.ts` always lives inside `@fnclaude/cli` regardless
66
- * of which bin invoked it. Works for both the `dist/prompts.js`
67
- * (installed) and `src/prompts.ts` (dev) layouts — both sit one
68
- * level under the package root where `prompts/` ships.
69
- *
70
- * Symlinks in the exe path are resolved before the search.
71
- *
72
- * When the dir is missing entirely (typical for a registry install without
73
- * the data files), a clear actionable warning is queued and the returned
74
- * PromptSet is empty — no fragments will be injected but the session still
75
- * launches.
76
- */
77
- export function loadPrompts(): LoadPromptsResult {
78
- const warnings: string[] = [];
79
- const { dir, error } = findPromptsDir();
80
- if (dir === null) {
81
- warnings.push(formatMissingDirWarning(error ?? 'unknown error'));
82
- return { prompts: EMPTY_PROMPT_SET, warnings };
83
- }
84
-
85
- const prompts: Record<keyof PromptSet, string> = {
86
- agentPitfall: '',
87
- projectSwitch: '',
88
- spawn: '',
89
- restart: '',
90
- noopRouter: '',
91
- };
92
- for (const key of Object.keys(PROMPT_FILE_NAMES) as (keyof PromptSet)[]) {
93
- const fileName = PROMPT_FILE_NAMES[key];
94
- const { content, warning } = readPromptFileSync(dir, fileName);
95
- prompts[key] = content;
96
- if (warning !== null) warnings.push(warning);
97
- }
98
- return { prompts, warnings };
99
- }
100
-
101
- export interface FindPromptsDirResult {
102
- readonly dir: string | null;
103
- readonly error: string | null;
104
- }
105
-
106
- export function findPromptsDir(): FindPromptsDirResult {
107
- const envDir = process.env.FNC_PROMPTS_DIR;
108
- if (envDir !== undefined && envDir !== '') {
109
- try {
110
- statSync(envDir);
111
- return { dir: envDir, error: null };
112
- } catch (err) {
113
- return {
114
- dir: null,
115
- error: `FNC_PROMPTS_DIR=${JSON.stringify(envDir)} does not exist: ${errorMessage(err)}`,
116
- };
117
- }
118
- }
119
-
120
- // Anchor the search at the script's neighbours, not bun's bin dir;
121
- // resolveSelfPath handles the argv[1] / execPath / realpathSync logic.
122
- const exeDir = dirname(resolveSelfPath());
123
-
124
- // Anchor at this module's own location to catch the umbrella-install
125
- // layout where argv[1] points at the fnclaude umbrella's bin/fnc.js
126
- // (which delegates into @fnclaude/cli via dynamic import). The exe-dir
127
- // candidates miss the cli package in that shape; this one doesn't.
128
- const moduleDir = dirname(fileURLToPath(import.meta.url));
129
-
130
- const candidates = [
131
- join(exeDir, 'prompts'),
132
- join(exeDir, '..', 'prompts'),
133
- join(exeDir, '..', 'share', 'fnclaude', 'prompts'),
134
- join(moduleDir, '..', 'prompts'),
135
- ];
136
- for (const c of candidates) {
137
- try {
138
- if (statSync(c).isDirectory()) {
139
- return { dir: c, error: null };
140
- }
141
- } catch {
142
- // continue to next candidate
143
- }
144
- }
145
- return {
146
- dir: null,
147
- error: `prompts directory not found alongside fnclaude binary (searched: ${candidates.join(', ')})`,
148
- };
149
- }
150
-
151
- export interface ReadPromptFileResult {
152
- readonly content: string;
153
- readonly warning: string | null;
154
- }
155
-
156
- /**
157
- * Async variant for callers that want non-blocking I/O. Returns the trimmed
158
- * file content, or empty string + warning on read failure.
159
- */
160
- export async function readPromptFile(
161
- dir: string,
162
- name: string,
163
- ): Promise<ReadPromptFileResult> {
164
- const path = join(dir, name);
165
- try {
166
- await stat(path); // surface ENOENT before reading
167
- const data = await readFile(path, 'utf8');
168
- return { content: trimTrailingWhitespace(data), warning: null };
169
- } catch (err) {
170
- return {
171
- content: '',
172
- warning: formatMissingFileWarning(path, name, errorMessage(err)),
173
- };
174
- }
175
- }
176
-
177
- /**
178
- * Sync variant used by `loadPrompts`. The Go reference is fully sync at
179
- * startup; we mirror that here so the function as a whole can stay sync and
180
- * the warnings are available immediately to the caller.
181
- */
182
- export function readPromptFileSync(dir: string, name: string): ReadPromptFileResult {
183
- const path = join(dir, name);
184
- try {
185
- // eslint-disable-next-line @typescript-eslint/no-require-imports
186
- const { readFileSync } = require('node:fs') as typeof import('node:fs');
187
- return {
188
- content: trimTrailingWhitespace(readFileSync(path, 'utf8')),
189
- warning: null,
190
- };
191
- } catch (err) {
192
- return {
193
- content: '',
194
- warning: formatMissingFileWarning(path, name, errorMessage(err)),
195
- };
196
- }
197
- }
198
-
199
- /**
200
- * isInteractiveSession reports whether the passthrough flags indicate an
201
- * interactive session (vs. a -p / --print one-shot run). Drives the
202
- * fragment-injection gate in selectFragments and the self-MCP injection
203
- * gate in buildArgv — neither is useful for non-interactive runs.
204
- */
205
- export function isInteractiveSession(passthrough: readonly string[]): boolean {
206
- return !passthrough.some((t) => t === '-p' || t === '--print');
207
- }
208
-
209
- /**
210
- * selectFragments returns the prompt fragments to inject for this session,
211
- * in stable order. Empty strings (missing files) are dropped.
212
- *
213
- * - All interactive sessions (non -p/--print) get agent-pitfall + spawn
214
- * (sibling-session capability applies whether the user is in noop
215
- * routing the conversation or in a project doing focused work).
216
- * - Noop fallback sessions also get noop-router (the router instructions
217
- * that replaced the embedded noop CLAUDE.md).
218
- * - Non-noop sessions also get project-switch + restart (capability hints
219
- * so the user can request a switch to another repo or restart the
220
- * current session at any time).
221
- *
222
- * -p/--print sessions get nothing — agent spawning, project-switching,
223
- * sibling spawning, and restart don't apply to one-shot non-interactive runs.
224
- */
225
- export function selectFragments(
226
- ps: PromptSet,
227
- passthrough: readonly string[],
228
- usedNoopFallback: boolean,
229
- ): string[] {
230
- if (!isInteractiveSession(passthrough)) return [];
231
- const frags: string[] = [];
232
- if (ps.agentPitfall !== '') frags.push(ps.agentPitfall);
233
- if (ps.spawn !== '') frags.push(ps.spawn);
234
- if (usedNoopFallback) {
235
- if (ps.noopRouter !== '') frags.push(ps.noopRouter);
236
- } else {
237
- if (ps.projectSwitch !== '') frags.push(ps.projectSwitch);
238
- if (ps.restart !== '') frags.push(ps.restart);
239
- }
240
- return frags;
241
- }
242
-
243
- function trimTrailingWhitespace(s: string): string {
244
- let i = s.length;
245
- while (i > 0) {
246
- const c = s.charCodeAt(i - 1);
247
- // \n, \r, space, tab
248
- if (c !== 0x0a && c !== 0x0d && c !== 0x20 && c !== 0x09) break;
249
- i--;
250
- }
251
- return s.slice(0, i);
252
- }
253
-
254
- function errorMessage(err: unknown): string {
255
- if (err instanceof Error) return err.message;
256
- return String(err);
257
- }
258
-
259
- function formatMissingFileWarning(path: string, name: string, err: string): string {
260
- return (
261
- `fnclaude: prompt fragment ${path} missing or unreadable: ${err} — ` +
262
- `the ${JSON.stringify(name)} system-prompt fragment will be skipped this session. ` +
263
- `If you're seeing this on a fresh install, your prompts/ directory ` +
264
- `may be incomplete; reinstall fnclaude or point FNC_PROMPTS_DIR at ` +
265
- `a complete prompts/ checkout.`
266
- );
267
- }
268
-
269
- function formatMissingDirWarning(err: string): string {
270
- return (
271
- `fnclaude: ${err} — no system-prompt fragments will be injected for this session.\n` +
272
- ` This usually means fnclaude was installed without its sibling prompts/\n` +
273
- ` directory (e.g. via \`go install\`, which doesn't ship data files). To fix:\n` +
274
- ` • Install via the AUR package, or download a release archive (which\n` +
275
- ` ships prompts/ alongside the binary).\n` +
276
- ` • Or set FNC_PROMPTS_DIR to a local prompts/ checkout, e.g. point it\n` +
277
- ` at the prompts/ dir in a clone of the fnclaude repo.`
278
- );
279
- }