@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/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
|
-
}
|