@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.
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 -219
  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/autoname.ts DELETED
@@ -1,273 +0,0 @@
1
- // Port of src/autoname.go from the Go reference implementation.
2
- //
3
- // shouldAutoName — predicate for automatic --name injection.
4
- // generateName — Anthropic API call (or claude CLI fallback) to produce a
5
- // short kebab-case session label from the initial prompt.
6
-
7
- import Anthropic from '@anthropic-ai/sdk';
8
- import type { NameConfig } from './config.js';
9
-
10
- // ── predicates ──────────────────────────────────────────────────────────────
11
-
12
- /**
13
- * shouldAutoName returns true when the passthrough slice meets all conditions
14
- * for automatic --name injection:
15
- *
16
- * - contains "--" followed by at least one non-empty token
17
- * - does NOT already contain --name / -n / --name=* / -n=*
18
- * - does NOT contain -p / --print
19
- * - does NOT contain -r / --resume / -r=* / --resume=*
20
- * - does NOT contain -c / --continue
21
- * - does NOT contain --from-pr / --from-pr=* / -P / -P=*
22
- */
23
- export function shouldAutoName(passthrough: readonly string[]): boolean {
24
- // Find "--" and verify at least one non-empty token follows.
25
- const sepIdx = passthrough.indexOf('--');
26
- if (sepIdx < 0) return false;
27
-
28
- const hasPrompt = passthrough.slice(sepIdx + 1).some((t) => t !== '');
29
- if (!hasPrompt) return false;
30
-
31
- // Check for disqualifying tokens.
32
- for (const t of passthrough) {
33
- if (
34
- t === '--name' ||
35
- t === '-n' ||
36
- t.startsWith('--name=') ||
37
- t.startsWith('-n=')
38
- )
39
- return false;
40
- if (t === '-p' || t === '--print') return false;
41
- if (
42
- t === '-r' ||
43
- t === '--resume' ||
44
- t.startsWith('-r=') ||
45
- t.startsWith('--resume=')
46
- )
47
- return false;
48
- if (t === '-c' || t === '--continue') return false;
49
- if (
50
- t === '--from-pr' ||
51
- t.startsWith('--from-pr=') ||
52
- t === '-P' ||
53
- t.startsWith('-P=')
54
- )
55
- return false;
56
- }
57
- return true;
58
- }
59
-
60
- /**
61
- * extractPrompt returns the first non-empty token after "--" in passthrough.
62
- * Returns undefined if not found.
63
- */
64
- export function extractPrompt(passthrough: readonly string[]): string | undefined {
65
- const sepIdx = passthrough.indexOf('--');
66
- if (sepIdx < 0) return undefined;
67
- for (const t of passthrough.slice(sepIdx + 1)) {
68
- if (t !== '') return t;
69
- }
70
- return undefined;
71
- }
72
-
73
- // ── stop-words + heuristic fallback ────────────────────────────────────────
74
-
75
- const STOP_WORDS = new Set([
76
- 'a',
77
- 'an',
78
- 'the',
79
- 'is',
80
- 'are',
81
- 'was',
82
- 'were',
83
- 'do',
84
- 'does',
85
- 'did',
86
- 'of',
87
- 'for',
88
- 'to',
89
- 'in',
90
- 'on',
91
- 'at',
92
- 'with',
93
- 'this',
94
- 'that',
95
- 'please',
96
- 'can',
97
- 'could',
98
- 'would',
99
- 'should',
100
- ]);
101
-
102
- /**
103
- * heuristicName derives a session name from a prompt without any LLM call.
104
- * Takes up to 3 non-stop-word, alphanumeric tokens joined with "-".
105
- */
106
- export function heuristicName(prompt: string): string {
107
- const words = prompt.toLowerCase().split(/\s+/);
108
- const kept: string[] = [];
109
- for (const w of words) {
110
- if (STOP_WORDS.has(w)) continue;
111
- const clean = w.replace(/[^a-z0-9]/g, '');
112
- if (clean !== '') kept.push(clean);
113
- if (kept.length === 3) break;
114
- }
115
- return kept.length === 0 ? 'session' : kept.join('-');
116
- }
117
-
118
- // ── slug sanitization ───────────────────────────────────────────────────────
119
-
120
- const RE_NON_SLUG = /[^a-z0-9-]+/g;
121
- const RE_MULTI_DASH = /-{2,}/g;
122
- const RE_WHITESPACE = /\s+/g;
123
-
124
- /**
125
- * sanitizeSlug cleans raw LLM output into a valid kebab slug (up to 3 dash-
126
- * separated segments). Returns undefined when nothing survives sanitization.
127
- */
128
- export function sanitizeSlug(raw: string): string | undefined {
129
- let s = raw.trim().toLowerCase();
130
- s = s.replace(RE_WHITESPACE, '-');
131
- s = s.replace(RE_NON_SLUG, '');
132
- s = s.replace(RE_MULTI_DASH, '-');
133
- s = s.replace(/^-+|-+$/g, '');
134
- // Take first 3 dash-segments.
135
- const parts = s.split('-');
136
- s = parts.slice(0, 3).join('-');
137
- // Trim again in case joining re-introduced edge dashes.
138
- s = s.replace(/^-+|-+$/g, '');
139
- return s !== '' ? s : undefined;
140
- }
141
-
142
- // ── LLM client abstraction ──────────────────────────────────────────────────
143
-
144
- export const NAME_SYSTEM_PROMPT =
145
- "Generate a 1-3 word lowercase hyphen-separated label for this user's request. " +
146
- 'Output ONLY the label — no punctuation, no quotes, no explanation, no leading ' +
147
- "'Label:'. Examples: 'fix-login-bug', 'add-dark-mode', 'refactor-auth'.";
148
-
149
- /**
150
- * LlmClientFn is the injectable seam for the LLM call. Tests can swap in a
151
- * fake without touching the Anthropic SDK.
152
- */
153
- export type LlmClientFn = (
154
- model: string,
155
- prompt: string,
156
- signal: AbortSignal,
157
- ) => Promise<string>;
158
-
159
- /**
160
- * defaultLlmClient returns an LlmClientFn backed by the real Anthropic API.
161
- */
162
- export function defaultLlmClient(apiKey: string): LlmClientFn {
163
- return async (model, prompt, signal) => {
164
- const client = new Anthropic({ apiKey });
165
- const msg = await client.messages.create(
166
- {
167
- model,
168
- max_tokens: 30,
169
- system: NAME_SYSTEM_PROMPT,
170
- messages: [{ role: 'user', content: prompt }],
171
- },
172
- { signal },
173
- );
174
- for (const blk of msg.content) {
175
- if (blk.type === 'text') return blk.text;
176
- }
177
- throw new Error('no text block in response');
178
- };
179
- }
180
-
181
- /**
182
- * claudeCliFn shells out to `claude -p --model <model> <combined-prompt>`.
183
- * Used as fallback when ANTHROPIC_API_KEY is absent.
184
- */
185
- export type SpawnFn = (
186
- cmd: string,
187
- args: string[],
188
- signal: AbortSignal,
189
- ) => Promise<string>;
190
-
191
- // Production spawn implementation using Bun.spawn.
192
- export const defaultSpawnFn: SpawnFn = async (cmd, args, signal) => {
193
- const proc = Bun.spawn([cmd, ...args], {
194
- stdout: 'pipe',
195
- stderr: 'pipe',
196
- });
197
-
198
- // Kill on abort.
199
- signal.addEventListener('abort', () => {
200
- try {
201
- proc.kill();
202
- } catch {
203
- // ignore
204
- }
205
- });
206
-
207
- const [stdout] = await Promise.all([
208
- new Response(proc.stdout).text(),
209
- proc.exited,
210
- ]);
211
-
212
- const exitCode = proc.exitCode;
213
- if (exitCode !== 0) {
214
- throw new Error(`claude exited with code ${exitCode}`);
215
- }
216
- return stdout;
217
- };
218
-
219
- export function claudeCliFn(model: string, spawnFn: SpawnFn = defaultSpawnFn): LlmClientFn {
220
- return async (_model, prompt, signal) => {
221
- const combined = `${NAME_SYSTEM_PROMPT}\n\nUser request: ${prompt}`;
222
- return spawnFn('claude', ['-p', '--model', model, combined], signal);
223
- };
224
- }
225
-
226
- // ── generateName ────────────────────────────────────────────────────────────
227
-
228
- /**
229
- * generateName produces a session name for the given prompt.
230
- *
231
- * llmFn may be omitted; it is selected automatically:
232
- * - defaultLlmClient when apiKey is non-empty
233
- * - claudeCliFn otherwise (falls back to the user's existing auth)
234
- *
235
- * On any error the function falls back to heuristicName silently.
236
- */
237
- export async function generateName(
238
- prompt: string | undefined,
239
- cfg: NameConfig,
240
- apiKey: string,
241
- llmFn?: LlmClientFn,
242
- ): Promise<string> {
243
- const resolved = prompt ?? '';
244
- let usingCLI = false;
245
- if (!llmFn) {
246
- if (apiKey) {
247
- llmFn = defaultLlmClient(apiKey);
248
- } else {
249
- llmFn = claudeCliFn(cfg.model);
250
- usingCLI = true;
251
- }
252
- }
253
-
254
- // cfg.timeout is in milliseconds (NameConfig stores ms).
255
- let timeoutMs = cfg.timeout;
256
- if (timeoutMs <= 0) {
257
- // claude -p cold-start is multi-second; give the CLI path more room.
258
- timeoutMs = usingCLI ? 15_000 : 3_000;
259
- }
260
-
261
- const controller = new AbortController();
262
- const timer = setTimeout(() => controller.abort(), timeoutMs);
263
-
264
- try {
265
- const raw = await llmFn(cfg.model, resolved, controller.signal);
266
- const name = sanitizeSlug(raw);
267
- return name !== undefined ? name : heuristicName(resolved);
268
- } catch {
269
- return heuristicName(resolved);
270
- } finally {
271
- clearTimeout(timer);
272
- }
273
- }
package/src/clipboard.ts DELETED
@@ -1,149 +0,0 @@
1
- // Port of src/clipboard.go from the Go reference implementation.
2
- //
3
- // pickClipboardTool — detects the platform-appropriate clipboard binary.
4
- // copyToClipboard — spawns that binary and pipes text into its stdin.
5
-
6
- // ── types ────────────────────────────────────────────────────────────────────
7
-
8
- /** Encodes the platform-detected clipboard tool choice. */
9
- export interface ClipboardTool {
10
- name: string;
11
- args: string[];
12
- }
13
-
14
- /**
15
- * SpawnResult is the injectable seam used in tests to avoid exec'ing real
16
- * clipboard binaries. Production code calls defaultSpawnClipboard.
17
- */
18
- export type ClipboardSpawnFn = (
19
- name: string,
20
- args: string[],
21
- text: string,
22
- ) => Promise<void>;
23
-
24
- // ── detection ─────────────────────────────────────────────────────────────────
25
-
26
- /**
27
- * pickClipboardTool returns the first tool that matches the current runtime
28
- * + environment, or null if no supported clipboard integration is available.
29
- *
30
- * Detection rules (matches the Go reference):
31
- *
32
- * Linux (Wayland — $WAYLAND_DISPLAY set): wl-copy
33
- * Linux (X11 — $DISPLAY set): xclip -selection clipboard
34
- * macOS (process.platform === 'darwin'): pbcopy
35
- * Windows (process.platform === 'win32'): clip
36
- *
37
- * Pure function of (platform, env-lookup) — trivially testable without
38
- * exec'ing anything.
39
- */
40
- export function pickClipboardTool(
41
- platform: NodeJS.Platform,
42
- env: (key: string) => string | undefined,
43
- ): ClipboardTool | null {
44
- switch (platform) {
45
- case 'linux': {
46
- if (env('WAYLAND_DISPLAY')) {
47
- return { name: 'wl-copy', args: [] };
48
- }
49
- if (env('DISPLAY')) {
50
- return { name: 'xclip', args: ['-selection', 'clipboard'] };
51
- }
52
- // Headless Linux: no supported tool.
53
- return null;
54
- }
55
- case 'darwin':
56
- return { name: 'pbcopy', args: [] };
57
- case 'win32':
58
- return { name: 'clip', args: [] };
59
- default:
60
- return null;
61
- }
62
- }
63
-
64
- // ── spawn implementation ──────────────────────────────────────────────────────
65
-
66
- /**
67
- * defaultClipboardSpawn spawns cmd with args and writes text to its stdin
68
- * using Bun.spawn. Throws on non-zero exit.
69
- */
70
- export const defaultClipboardSpawn: ClipboardSpawnFn = async (name, args, text) => {
71
- const proc = Bun.spawn([name, ...args], {
72
- stdin: 'pipe',
73
- stdout: 'ignore',
74
- stderr: 'ignore',
75
- });
76
-
77
- // Write text into stdin then close it.
78
- const writer = proc.stdin;
79
- writer.write(text);
80
- await writer.end();
81
-
82
- const exitCode = await proc.exited;
83
- if (exitCode !== 0) {
84
- throw new Error(`${name} exited with code ${exitCode}`);
85
- }
86
- };
87
-
88
- // ── xsel fallback ────────────────────────────────────────────────────────────
89
-
90
- /**
91
- * xselFallback tries xsel --clipboard --input when xclip is unavailable or
92
- * fails. Returns true on success, false + error on failure.
93
- */
94
- async function xselFallback(
95
- text: string,
96
- spawnFn: ClipboardSpawnFn,
97
- ): Promise<{ ok: boolean; err?: Error }> {
98
- try {
99
- await spawnFn('xsel', ['--clipboard', '--input'], text);
100
- return { ok: true };
101
- } catch (e) {
102
- return { ok: false, err: e as Error };
103
- }
104
- }
105
-
106
- // ── public API ────────────────────────────────────────────────────────────────
107
-
108
- /**
109
- * copyToClipboard writes text to the user's clipboard via the
110
- * platform-appropriate tool. Returns { ok: true } on success or
111
- * { ok: false, error } on failure (tool missing, exec error, no detector
112
- * matched, etc.).
113
- *
114
- * Detection precedence and tool choice are documented on pickClipboardTool.
115
- */
116
- export async function copyToClipboard(
117
- text: string,
118
- spawnFn: ClipboardSpawnFn = defaultClipboardSpawn,
119
- platform: NodeJS.Platform = process.platform,
120
- env: (key: string) => string | undefined = (k) => process.env[k],
121
- ): Promise<{ ok: boolean; error?: Error }> {
122
- const tool = pickClipboardTool(platform, env);
123
- if (!tool) {
124
- return {
125
- ok: false,
126
- error: new Error(
127
- `no clipboard integration available for platform=${platform}`,
128
- ),
129
- };
130
- }
131
-
132
- try {
133
- await spawnFn(tool.name, tool.args, text);
134
- return { ok: true };
135
- } catch (primaryErr) {
136
- // X11 fallback: try xsel before giving up.
137
- if (tool.name === 'xclip') {
138
- const fb = await xselFallback(text, spawnFn);
139
- if (fb.ok) return { ok: true };
140
- return {
141
- ok: false,
142
- error: new Error(
143
- `xclip failed (${(primaryErr as Error).message}); xsel fallback failed (${fb.err?.message})`,
144
- ),
145
- };
146
- }
147
- return { ok: false, error: primaryErr as Error };
148
- }
149
- }