@lucascouts/claude-agent-tui 0.5.2 → 0.6.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 (41) hide show
  1. package/dist/acp-agent.d.ts +187 -13
  2. package/dist/acp-agent.d.ts.map +1 -1
  3. package/dist/acp-agent.js +444 -59
  4. package/dist/agent-catalog.d.ts +96 -0
  5. package/dist/agent-catalog.d.ts.map +1 -0
  6. package/dist/agent-catalog.js +287 -0
  7. package/dist/claude-path.d.ts.map +1 -1
  8. package/dist/claude-path.js +6 -0
  9. package/dist/end-of-turn.d.ts +6 -0
  10. package/dist/end-of-turn.d.ts.map +1 -1
  11. package/dist/end-of-turn.js +8 -1
  12. package/dist/engine-lifecycle.d.ts +66 -1
  13. package/dist/engine-lifecycle.d.ts.map +1 -1
  14. package/dist/engine-lifecycle.js +43 -4
  15. package/dist/engine-pty.d.ts +70 -2
  16. package/dist/engine-pty.d.ts.map +1 -1
  17. package/dist/engine-pty.js +80 -6
  18. package/dist/gate/settings-writer.d.ts +14 -0
  19. package/dist/gate/settings-writer.d.ts.map +1 -1
  20. package/dist/gate/settings-writer.js +49 -0
  21. package/dist/image-input.d.ts +31 -0
  22. package/dist/image-input.d.ts.map +1 -0
  23. package/dist/image-input.js +79 -0
  24. package/dist/image-vision-smoke.d.ts +52 -0
  25. package/dist/image-vision-smoke.d.ts.map +1 -0
  26. package/dist/image-vision-smoke.js +111 -0
  27. package/dist/index.js +6 -0
  28. package/dist/mcp-config-writer.d.ts +61 -0
  29. package/dist/mcp-config-writer.d.ts.map +1 -0
  30. package/dist/mcp-config-writer.js +172 -0
  31. package/dist/model-catalog.d.ts +29 -2
  32. package/dist/model-catalog.d.ts.map +1 -1
  33. package/dist/model-catalog.js +50 -10
  34. package/dist/settings.d.ts.map +1 -1
  35. package/dist/settings.js +9 -0
  36. package/dist/tools.d.ts.map +1 -1
  37. package/dist/tools.js +5 -1
  38. package/dist/usage.d.ts +3 -0
  39. package/dist/usage.d.ts.map +1 -1
  40. package/dist/usage.js +3 -0
  41. package/package.json +8 -8
@@ -0,0 +1,96 @@
1
+ /** A single discoverable main-thread agent persona, as advertised to the Zed `agent` selector. */
2
+ export interface AgentCatalogEntry {
3
+ /**
4
+ * The persona reference passed to `claude --agent <value>`. GUARANTEED to match
5
+ * {@link SAFE_AGENT_REF} (a single safe segment, or a namespaced `plugin:name`) — entries whose
6
+ * resolved name fails this are dropped at discovery, so this string is always safe to double-quote
7
+ * into a `-lc` shell command.
8
+ */
9
+ value: string;
10
+ /**
11
+ * Human-facing label. From the probe path it is the raw reference (e.g. `epic:analyst`); from the
12
+ * glob path it is the frontmatter `name`, else a humanized form of the filename.
13
+ */
14
+ displayName: string;
15
+ /** The frontmatter `description`, when present (glob path only — the probe lists names, not docs). */
16
+ description?: string;
17
+ }
18
+ /**
19
+ * Injectable seams for {@link discoverAgents} — defaults wire the `node:` stdlib. Tests pass fakes so
20
+ * discovery is exercised against an in-memory fs / canned probe output, never the real disk, the real
21
+ * `~/.claude`, or the real `claude` binary.
22
+ */
23
+ export interface DiscoverAgentsDeps {
24
+ /**
25
+ * Probe `claude` for its canonical agent list (the PRIMARY source). Returns the raw combined
26
+ * stdout+stderr of `claude --agent <invalid>` (which contains the `Available agents: …` line), or
27
+ * `null` when the probe cannot run (missing binary / timeout) so discovery falls back to the glob.
28
+ * Default: {@link defaultProbeClaudeAgents}. Tests inject a canned string (or `null`) to stay
29
+ * hermetic — a glob-path test MUST pass `() => null` to force the fallback.
30
+ */
31
+ probeClaudeAgents?: () => string | null;
32
+ /** Resolve the user's home directory (default: `os.homedir`). Glob path only. */
33
+ homedir?: () => string;
34
+ /** List the `*.md` filenames in `dir`; MUST return `[]` when `dir` is missing/unreadable. */
35
+ readdirMd?: (dir: string) => string[];
36
+ /** Read a file's UTF-8 contents (default: `fs.readFileSync(p, "utf8")`). */
37
+ readFile?: (path: string) => string;
38
+ }
39
+ /**
40
+ * The persona-SEGMENT allowlist (R3.3). A single un-namespaced segment: letters/digits/underscore/
41
+ * hyphen only (no spaces, no shell metacharacters, no path separators, no `:`). Guards the glob path
42
+ * (on-disk `.md` names) and is the per-segment building block of {@link SAFE_AGENT_REF}.
43
+ */
44
+ export declare const SAFE_AGENT_NAME: RegExp;
45
+ /** True iff `name` is a non-empty string matching {@link SAFE_AGENT_NAME} (R3.3). */
46
+ export declare function isSafeAgentName(name: unknown): name is string;
47
+ /**
48
+ * The persona-REFERENCE allowlist (R3.6). A `--agent "<value>"` reference is either a single safe
49
+ * segment (a built-in like `Explore`, or a user/project `.md` persona) OR a namespaced plugin persona
50
+ * `plugin:name` — exactly ONE optional `:`-separated second segment, each segment a
51
+ * {@link SAFE_AGENT_NAME}. The `:` is inert inside the double-quoted shell argument.
52
+ */
53
+ export declare const SAFE_AGENT_REF: RegExp;
54
+ /** True iff `name` is a non-empty string matching {@link SAFE_AGENT_REF} (R3.6). */
55
+ export declare function isSafeAgentRef(name: unknown): name is string;
56
+ /**
57
+ * Extract the agent names from a `claude --agent <invalid>` probe output. Matches the
58
+ * `Available agents: a, b, c` line (tolerant of leading text and a trailing newline — `.` stops at the
59
+ * newline so only that line is captured), splits on commas, trims, and drops empties. Returns `[]` when
60
+ * the line is absent (format changed → the caller falls back to the glob).
61
+ */
62
+ export declare function parseAvailableAgents(probeOutput: string): string[];
63
+ /** Frontmatter fields we extract — only `name`/`description` matter for the catalog. */
64
+ interface Frontmatter {
65
+ name?: string;
66
+ description?: string;
67
+ }
68
+ /**
69
+ * Minimal `---`-fenced frontmatter line-parser (NOT a full YAML dep). Reads the leading
70
+ * `---\n … \n---` block and pulls `key: value` lines for the keys we care about; surrounding quotes on
71
+ * a value are stripped. A file without an opening `---` fence yields `{}` (filename-fallback applies).
72
+ * Tiny + pure on purpose — the persona frontmatter we consume is flat `key: value`.
73
+ */
74
+ export declare function parseFrontmatter(content: string): Frontmatter;
75
+ /**
76
+ * Discover the main-thread agent personas selectable via `claude --agent <name>`.
77
+ *
78
+ * PRIMARY (Task 7, R3.5): the {@link DiscoverAgentsDeps.probeClaudeAgents} probe asks `claude` for its
79
+ * canonical list (enabled plugin personas + built-ins, exactly as `claude --agent` resolves them) and
80
+ * we parse the `Available agents:` line; the bare `claude` default and any name failing the R3.6
81
+ * reference allowlist are dropped, and the rest are deduped + sorted by `value`. When the probe yields
82
+ * at least one persona, that is the result.
83
+ *
84
+ * FALLBACK (R3.1): when the probe returns `null` (binary missing / timeout) OR its output has no
85
+ * `Available agents:` line OR every listed name was dropped, discovery degrades to the on-disk glob
86
+ * ({@link globDiscoverAgents}).
87
+ *
88
+ * Never throws: a failed probe, a missing/unreadable dir or file, or an empty/all-unsafe result yields
89
+ * `[]` (the empty-state that hides the picker in a later sub-task).
90
+ *
91
+ * @param cwd the SESSION cwd (project dir) whose `.claude/agents` takes precedence in the fallback.
92
+ * @param deps injectable probe/fs/home seams (default: `node:` stdlib + the real `claude` probe).
93
+ */
94
+ export declare function discoverAgents(cwd: string, deps?: DiscoverAgentsDeps): AgentCatalogEntry[];
95
+ export {};
96
+ //# sourceMappingURL=agent-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-catalog.d.ts","sourceRoot":"","sources":["../src/agent-catalog.ts"],"names":[],"mappings":"AA4CA,kGAAkG;AAClG,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sGAAsG;IACtG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACxC,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAqB,CAAC;AAElD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAE7D;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAyC,CAAC;AAErE,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAE5D;AA0CD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAOlE;AAkDD,wFAAwF;AACxF,UAAU,WAAW;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CA0B7D;AAiED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,iBAAiB,EAAE,CAe9F"}
@@ -0,0 +1,287 @@
1
+ // Story 056 / Task 3.1 + Task 7 (R3.1, R3.3, R3.5, R3.6) — main-thread agent-persona discovery for
2
+ // the Zed Agent Panel's `agent` selector.
3
+ //
4
+ // HYBRID DISCOVERY (refined in Task 7 — was glob-only):
5
+ // PRIMARY (R3.5): ask `claude` itself. We spawn `claude --agent <invalid-sentinel>` through a SAFE
6
+ // args-array child process (NO shell, 8s timeout, EMPTY stdin ⇒ no prompt ⇒ NO tokens/billing).
7
+ // `claude` rejects the sentinel and prints `Available agents: a, b, c` — listing EXACTLY what
8
+ // `claude --agent` accepts: enabled plugin personas (namespaced `plugin:name`) AND the built-ins
9
+ // (Explore, Plan, general-purpose, …). This is the same source the upstream SDK engine uses, so
10
+ // the panel reaches real parity with the original. LIVE-VERIFIED against 2.1.195 (deterministic,
11
+ // cwd-independent). The bare `claude` built-in = the no-persona default and is dropped (it maps to
12
+ // the option's existing "default" sentinel). The spawn mirrors claude-path.ts (execFileSync,
13
+ // args-list, NEVER a shell — C5); the only argument is a CONSTANT sentinel, never user input.
14
+ // FALLBACK (R3.1): when the probe cannot run (binary missing / timeout) OR the error format changes
15
+ // (no `Available agents:` line on a future version), discovery degrades to a glob of the on-disk
16
+ // persona files: `<cwd>/.claude/agents/*.md` (project — PRECEDENCE) + `~/.claude/agents/*.md`
17
+ // (user). Both dirs empty (the common case) yields `[]` and a later sub-task's gate hides the
18
+ // picker entirely.
19
+ //
20
+ // NOTE: `claude agents` (the subcommand) is the *background-agents* manager — it lists dispatched
21
+ // cloud SESSIONS, NOT persona definitions — so we never call it; the persona list comes from the
22
+ // `--agent` rejection message above.
23
+ //
24
+ // SECURITY (R3.3 / R3.6 — the highest-severity item). The `value` we return is interpolated into a
25
+ // `-lc` shell string via `--agent "<name>"` (Task 3.3). A plugin persona is NAMESPACED (`plugin:name`),
26
+ // so the boundary allowlist is {@link SAFE_AGENT_REF} = `/^[A-Za-z0-9_-]+(?::[A-Za-z0-9_-]+)?$/`: one
27
+ // optional `:`-separated segment, each segment letters/digits/`_`/`-` only (the `:` is inert inside the
28
+ // double-quoted argument). Every resolved name is checked and DROPPED if it fails — a name with spaces,
29
+ // quotes, path separators, command substitution, chaining, … is NEVER returned. The second layer is
30
+ // double-quoting at spawn time. {@link SAFE_AGENT_NAME} (single segment) still guards the glob path,
31
+ // where on-disk `.md` persona names are always un-namespaced.
32
+ //
33
+ // PURE + dependency-injectable (cf. claude-path.ts / usage-env.ts): all fs/home/probe access goes
34
+ // through the {@link DiscoverAgentsDeps} seams so the unit tests inject fakes and never touch the real
35
+ // disk or spawn the real `claude` binary. The only `node:` builtins are os/fs/path/child_process; no
36
+ // shell is ever invoked. Dependency-free: a minimal inline frontmatter line-parser stands in for a YAML
37
+ // dependency (FORK.md pins node-pty + the SDK as the only runtime deps; this adds none).
38
+ //
39
+ // node:test runner: `node --experimental-strip-types --test test/agent-catalog.test.ts`
40
+ import { homedir as osHomedir } from "node:os";
41
+ import { readdirSync, readFileSync } from "node:fs";
42
+ import { execFileSync } from "node:child_process";
43
+ import { join } from "node:path";
44
+ /**
45
+ * The persona-SEGMENT allowlist (R3.3). A single un-namespaced segment: letters/digits/underscore/
46
+ * hyphen only (no spaces, no shell metacharacters, no path separators, no `:`). Guards the glob path
47
+ * (on-disk `.md` names) and is the per-segment building block of {@link SAFE_AGENT_REF}.
48
+ */
49
+ export const SAFE_AGENT_NAME = /^[A-Za-z0-9_-]+$/;
50
+ /** True iff `name` is a non-empty string matching {@link SAFE_AGENT_NAME} (R3.3). */
51
+ export function isSafeAgentName(name) {
52
+ return typeof name === "string" && SAFE_AGENT_NAME.test(name);
53
+ }
54
+ /**
55
+ * The persona-REFERENCE allowlist (R3.6). A `--agent "<value>"` reference is either a single safe
56
+ * segment (a built-in like `Explore`, or a user/project `.md` persona) OR a namespaced plugin persona
57
+ * `plugin:name` — exactly ONE optional `:`-separated second segment, each segment a
58
+ * {@link SAFE_AGENT_NAME}. The `:` is inert inside the double-quoted shell argument.
59
+ */
60
+ export const SAFE_AGENT_REF = /^[A-Za-z0-9_-]+(?::[A-Za-z0-9_-]+)?$/;
61
+ /** True iff `name` is a non-empty string matching {@link SAFE_AGENT_REF} (R3.6). */
62
+ export function isSafeAgentRef(name) {
63
+ return typeof name === "string" && SAFE_AGENT_REF.test(name);
64
+ }
65
+ /** A sentinel `--agent` value that can never be a real persona — forces the "not found" + list. */
66
+ const PROBE_SENTINEL = "__acp_agent_probe_sentinel__";
67
+ /** The bare built-in `claude` = the no-persona default (the option's existing "default" sentinel). */
68
+ const DEFAULT_PERSONA = "claude";
69
+ /**
70
+ * Default {@link DiscoverAgentsDeps.probeClaudeAgents}: spawn `claude --agent <sentinel>` and return
71
+ * its combined output. `execFileSync` (args-array — NO shell, mirroring claude-path.ts / C5) throws on
72
+ * the expected non-zero exit; the `Available agents: …` line is carried on the error's stdout/stderr.
73
+ * Empty stdin + an 8s timeout keep it cheap and tokenless. Any failure (missing binary, timeout,
74
+ * unexpected shape) → `null`, which routes the caller to the glob fallback. The sole argument is the
75
+ * CONSTANT {@link PROBE_SENTINEL} — never user input — so there is no injection surface.
76
+ *
77
+ * OPT-IN (`FORK_AGENT_PROBE=1`): the spawn runs ONLY when production enables it (the fork entrypoint
78
+ * `index.ts` sets the flag). Unit tests import this module directly WITHOUT the flag, so the default
79
+ * probe is a no-op (`null` → glob fallback) — they never spawn the real `claude` and stay hermetic and
80
+ * fast. Tests that DO exercise the probe path inject {@link DiscoverAgentsDeps.probeClaudeAgents}
81
+ * directly, bypassing this gate.
82
+ */
83
+ function defaultProbeClaudeAgents() {
84
+ if (process.env.FORK_AGENT_PROBE !== "1")
85
+ return null; // opt-in — production (index.ts) enables it
86
+ try {
87
+ // stdio: stdin IGNORED (claude reads EOF → no hang, no prompt → no tokens); stdout+stderr PIPED so
88
+ // the `Available agents:` line (claude writes it to stderr) is captured, NEVER leaked to the fork's
89
+ // own stdout (the ACP wire) or stderr (the log). The list lands on the throw's stderr below.
90
+ const out = execFileSync("claude", ["--agent", PROBE_SENTINEL], {
91
+ stdio: ["ignore", "pipe", "pipe"],
92
+ timeout: 8000,
93
+ encoding: "utf8",
94
+ maxBuffer: 1024 * 1024,
95
+ });
96
+ return out.length > 0 ? out : null;
97
+ }
98
+ catch (err) {
99
+ const e = err;
100
+ const out = `${e?.stdout ?? ""}${e?.stderr ?? ""}`;
101
+ return out.length > 0 ? out : null;
102
+ }
103
+ }
104
+ /**
105
+ * Extract the agent names from a `claude --agent <invalid>` probe output. Matches the
106
+ * `Available agents: a, b, c` line (tolerant of leading text and a trailing newline — `.` stops at the
107
+ * newline so only that line is captured), splits on commas, trims, and drops empties. Returns `[]` when
108
+ * the line is absent (format changed → the caller falls back to the glob).
109
+ */
110
+ export function parseAvailableAgents(probeOutput) {
111
+ const m = /Available agents:\s*(.+)/.exec(probeOutput);
112
+ if (!m)
113
+ return [];
114
+ return m[1]
115
+ .split(",")
116
+ .map((s) => s.trim())
117
+ .filter((s) => s.length > 0);
118
+ }
119
+ /**
120
+ * Resolve a probe-listed agent name into an entry, or `null` to drop it. The bare `claude` built-in is
121
+ * the no-persona default (mapped to the option's existing "default" sentinel) so it is dropped; any
122
+ * name failing the R3.6 reference allowlist is dropped. `displayName` is the raw reference (e.g.
123
+ * `epic:analyst`), which is what the panel shows.
124
+ */
125
+ function entryFromProbeName(name) {
126
+ if (name === DEFAULT_PERSONA)
127
+ return null;
128
+ if (!isSafeAgentRef(name))
129
+ return null;
130
+ return { value: name, displayName: name };
131
+ }
132
+ /**
133
+ * Default `readdirMd`: list `*.md` filenames in `dir`, returning `[]` when the directory is missing or
134
+ * unreadable (the empty-state — never throws). Sorted for a stable first-wins ordering within a dir.
135
+ */
136
+ function defaultReaddirMd(dir) {
137
+ try {
138
+ return readdirSync(dir)
139
+ .filter((f) => f.toLowerCase().endsWith(".md"))
140
+ .sort();
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ }
146
+ /** Default `readFile`: UTF-8 file read via the `node:fs` stdlib. */
147
+ function defaultReadFile(path) {
148
+ return readFileSync(path, "utf8");
149
+ }
150
+ /** The leading `name` (sans `.md`) of a persona file, used as the value/displayName fallback. */
151
+ function baseName(filename) {
152
+ return filename.replace(/\.md$/i, "");
153
+ }
154
+ /**
155
+ * Humanize a filename stem into a display label when the frontmatter has no `name`:
156
+ * `code-reviewer` → `Code Reviewer`, `db_admin` → `Db Admin`. Pure string cosmetics.
157
+ */
158
+ function humanize(stem) {
159
+ return stem
160
+ .split(/[-_]+/)
161
+ .filter((w) => w.length > 0)
162
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
163
+ .join(" ");
164
+ }
165
+ /**
166
+ * Minimal `---`-fenced frontmatter line-parser (NOT a full YAML dep). Reads the leading
167
+ * `---\n … \n---` block and pulls `key: value` lines for the keys we care about; surrounding quotes on
168
+ * a value are stripped. A file without an opening `---` fence yields `{}` (filename-fallback applies).
169
+ * Tiny + pure on purpose — the persona frontmatter we consume is flat `key: value`.
170
+ */
171
+ export function parseFrontmatter(content) {
172
+ // The opening fence must be the very first line (allowing a leading BOM / blank lines is overkill
173
+ // for this format). Bail to `{}` when there is no fenced block.
174
+ const lines = content.split(/\r?\n/);
175
+ if (lines[0]?.trim() !== "---")
176
+ return {};
177
+ const out = {};
178
+ for (let i = 1; i < lines.length; i++) {
179
+ const line = lines[i];
180
+ if (line.trim() === "---")
181
+ break; // closing fence — stop
182
+ const m = /^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(line);
183
+ if (!m)
184
+ continue; // not a `key: value` line — skip (lists/comments/blanks)
185
+ const key = m[1].toLowerCase();
186
+ if (key !== "name" && key !== "description")
187
+ continue;
188
+ let value = m[2].trim();
189
+ // Strip a single pair of matching surrounding quotes.
190
+ if (value.length >= 2 &&
191
+ ((value.startsWith('"') && value.endsWith('"')) ||
192
+ (value.startsWith("'") && value.endsWith("'")))) {
193
+ value = value.slice(1, -1);
194
+ }
195
+ if (value.length > 0)
196
+ out[key] = value;
197
+ }
198
+ return out;
199
+ }
200
+ /**
201
+ * Resolve a single persona `*.md` file into an {@link AgentCatalogEntry}, or `null` when it cannot
202
+ * contribute a SAFE entry. The resolved name is the frontmatter `name` if present, else the filename
203
+ * stem; it is then run through the R3.3 allowlist and the file is DROPPED (→ `null`) on any failure —
204
+ * including an unreadable file. `displayName` falls back to a humanized stem.
205
+ */
206
+ function resolveEntry(dir, filename, readFile) {
207
+ let fm;
208
+ try {
209
+ fm = parseFrontmatter(readFile(join(dir, filename)));
210
+ }
211
+ catch {
212
+ return null; // unreadable file — skip it, never throw (R3.1 graceful)
213
+ }
214
+ const stem = baseName(filename);
215
+ const resolvedName = fm.name ?? stem;
216
+ // SECURITY (R3.3): drop any entry whose name escapes the single-segment allowlist — never returned.
217
+ if (!isSafeAgentName(resolvedName))
218
+ return null;
219
+ return {
220
+ value: resolvedName,
221
+ displayName: fm.name ?? humanize(stem),
222
+ ...(fm.description ? { description: fm.description } : {}),
223
+ };
224
+ }
225
+ /** Sort entries by `value` for a stable, scope-independent ordering. */
226
+ function sortByValue(entries) {
227
+ return entries.sort((a, b) => (a.value < b.value ? -1 : a.value > b.value ? 1 : 0));
228
+ }
229
+ /**
230
+ * FALLBACK glob discovery (R3.1): scan `<cwd>/.claude/agents/*.md` (project — PRECEDENCE) and
231
+ * `~/.claude/agents/*.md` (user), parsing frontmatter `name`/`description` (filename stem as the
232
+ * fallback name), sanitizing against the R3.3 allowlist, deduping by `value` (project beats user;
233
+ * first-wins within a dir), and sorting by `value`. Never throws.
234
+ */
235
+ function globDiscoverAgents(cwd, deps) {
236
+ const homedir = deps.homedir ?? osHomedir;
237
+ const readdirMd = deps.readdirMd ?? defaultReaddirMd;
238
+ const readFile = deps.readFile ?? defaultReadFile;
239
+ const projectDir = join(cwd, ".claude", "agents");
240
+ const userDir = join(homedir(), ".claude", "agents");
241
+ const byValue = new Map();
242
+ for (const dir of [projectDir, userDir]) {
243
+ for (const filename of readdirMd(dir)) {
244
+ const entry = resolveEntry(dir, filename, readFile);
245
+ if (entry && !byValue.has(entry.value)) {
246
+ byValue.set(entry.value, entry);
247
+ }
248
+ }
249
+ }
250
+ return sortByValue([...byValue.values()]);
251
+ }
252
+ /**
253
+ * Discover the main-thread agent personas selectable via `claude --agent <name>`.
254
+ *
255
+ * PRIMARY (Task 7, R3.5): the {@link DiscoverAgentsDeps.probeClaudeAgents} probe asks `claude` for its
256
+ * canonical list (enabled plugin personas + built-ins, exactly as `claude --agent` resolves them) and
257
+ * we parse the `Available agents:` line; the bare `claude` default and any name failing the R3.6
258
+ * reference allowlist are dropped, and the rest are deduped + sorted by `value`. When the probe yields
259
+ * at least one persona, that is the result.
260
+ *
261
+ * FALLBACK (R3.1): when the probe returns `null` (binary missing / timeout) OR its output has no
262
+ * `Available agents:` line OR every listed name was dropped, discovery degrades to the on-disk glob
263
+ * ({@link globDiscoverAgents}).
264
+ *
265
+ * Never throws: a failed probe, a missing/unreadable dir or file, or an empty/all-unsafe result yields
266
+ * `[]` (the empty-state that hides the picker in a later sub-task).
267
+ *
268
+ * @param cwd the SESSION cwd (project dir) whose `.claude/agents` takes precedence in the fallback.
269
+ * @param deps injectable probe/fs/home seams (default: `node:` stdlib + the real `claude` probe).
270
+ */
271
+ export function discoverAgents(cwd, deps = {}) {
272
+ // PRIMARY: ask `claude` — the source of truth for what `--agent` accepts.
273
+ const probe = deps.probeClaudeAgents ?? defaultProbeClaudeAgents;
274
+ const probeOut = probe();
275
+ if (probeOut) {
276
+ const byValue = new Map();
277
+ for (const name of parseAvailableAgents(probeOut)) {
278
+ const entry = entryFromProbeName(name);
279
+ if (entry && !byValue.has(entry.value))
280
+ byValue.set(entry.value, entry);
281
+ }
282
+ if (byValue.size > 0)
283
+ return sortByValue([...byValue.values()]);
284
+ }
285
+ // FALLBACK: the probe is unavailable or its format changed — glob the on-disk personas.
286
+ return globDiscoverAgents(cwd, deps);
287
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"claude-path.d.ts","sourceRoot":"","sources":["../src/claude-path.ts"],"names":[],"mappings":"AA6BA;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,YAAY,CAAC;AAenD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAc/E;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,WAAgC,GACrC,MAAM,GAAG,IAAI,CAMf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAChC,IAAI,CAON;AAED,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnD,qEAAqE;IACrE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,CA6BnE"}
1
+ {"version":3,"file":"claude-path.d.ts","sourceRoot":"","sources":["../src/claude-path.ts"],"names":[],"mappings":"AA+BA;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,YAAY,CAAC;AAenD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAc/E;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,WAAgC,GACrC,MAAM,GAAG,IAAI,CAMf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAChC,IAAI,CAON;AAED,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnD,qEAAqE;IACrE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,CAiCnE"}
@@ -26,6 +26,8 @@
26
26
  import { execFileSync } from "node:child_process";
27
27
  import { accessSync, constants } from "node:fs";
28
28
  import { delimiter, join } from "node:path";
29
+ // Story 058 R3.2 — version-aware image-vision smoke (observability only; NEVER gates image:true, R3.1).
30
+ import { reportImageVisionSmoke } from "./image-vision-smoke.js";
29
31
  /**
30
32
  * Provenance pin the live `claude` version is measured against — kept in lockstep
31
33
  * with `fork/.fork-provenance.json`'s externalRuntimeDeps `claude` entry. A live
@@ -121,6 +123,10 @@ export function resolveClaudePath(opts = {}) {
121
123
  log(`[claude-path] resolved claude ${candidate} (version ${detected})`);
122
124
  }
123
125
  reportVersionDrift(detected, PROVENANCE_CLAUDE_VERSION, log);
126
+ // Story 058 R3.2 — version-aware @image vision smoke: warn ONCE (one-liner) when this claude
127
+ // is NOT a version confirmed to vision-encode @image via Read. Observability only — it never
128
+ // blocks and never touches promptCapabilities.image (stays `image: true`, R3.1).
129
+ reportImageVisionSmoke(detected, log);
124
130
  }
125
131
  catch {
126
132
  // Observability is best-effort; never let it break resolution.
@@ -144,6 +144,12 @@ export interface TurnResolverOptions {
144
144
  logger?: StopReasonLogger;
145
145
  deltaTMs?: number;
146
146
  watchdogMs?: number;
147
+ /**
148
+ * Story 056 (#812) — fired EXACTLY ONCE, ONLY on a real end-of-turn boundary (NOT on cancel, NOT
149
+ * on watchdog timeout), AFTER the prompt resolves. Fire-and-forget; the callback MUST NOT
150
+ * throw/block.
151
+ */
152
+ onTurnResolved?: () => void;
147
153
  }
148
154
  /** A detector paired with the awaitable {@link PromptResponse} the prompt() loop returns. */
149
155
  export interface TurnResolver {
@@ -1 +1 @@
1
- {"version":3,"file":"end-of-turn.d.ts","sourceRoot":"","sources":["../src/end-of-turn.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"end-of-turn.d.ts","sourceRoot":"","sources":["../src/end-of-turn.ts"],"names":[],"mappings":"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"}
@@ -394,7 +394,14 @@ export function createTurnResolver(opts = {}) {
394
394
  action();
395
395
  }
396
396
  const detector = createEndOfTurnDetector({
397
- onEndOfTurn: (boundary) => settle(() => resolveFn({ stopReason: mapStopReason(readStopReason(boundary), opts.logger) })),
397
+ onEndOfTurn: (boundary) =>
398
+ // Story 056 (#812): fire onTurnResolved inside the SAME settle, AFTER the prompt resolves, so it
399
+ // runs EXACTLY ONCE per real end-of-turn. cancel() and onTurnTimeout intentionally do NOT call
400
+ // it — the settle latch + the detector's firedUuids guarantee a single call per turn.
401
+ settle(() => {
402
+ resolveFn({ stopReason: mapStopReason(readStopReason(boundary), opts.logger) });
403
+ opts.onTurnResolved?.();
404
+ }),
398
405
  onTurnTimeout: (error) => settle(() => rejectFn(error)),
399
406
  schedule: opts.schedule,
400
407
  sessionId: opts.sessionId,
@@ -147,8 +147,27 @@ export declare class SessionEngine {
147
147
  * launch AND the `|| claude` fresh fallback as `--permission-mode <mode>`, so an in-place re-spawn
148
148
  * (a dontAsk/bypass switch) reattaches the SAME sessionId/transcript under the new mode. Still no
149
149
  * `-p`/`--print`/`stream-json` — billing stays subscription `cli`.
150
+ *
151
+ * Story 056 (R3.3): an optional `agent` persona name is likewise carried via `flags` as the
152
+ * DOUBLE-QUOTED `--agent "<name>"`. Because `flags` is interpolated into BOTH the `--resume "<id>"`
153
+ * launch AND the `|| claude` fresh fallback, this single addition reaches both branches (R3.3). It is
154
+ * the SECOND layer of the command-injection defense — re-asserted via {@link isSafeAgentRef} (which
155
+ * accepts a namespaced `plugin:name`) so an unsafe name is DROPPED (no flag), never interpolated.
156
+ * Still no `-p`/`--print`/`stream-json`.
157
+ *
158
+ * Story 057 (R1.1/R1.2): an optional `additionalDirectories` list is folded into the SAME `flags`
159
+ * string as ONE double-quoted `--add-dir "<dir>"` per dir — so, like the agent flag, a single addition
160
+ * reaches BOTH the `--resume "<id>"` half AND the `|| claude` fallback half. Each dir is re-asserted via
161
+ * {@link isSafeDir} (absolute + existing + no shell metacharacter); an unsafe dir is DROPPED (logged),
162
+ * never interpolated. Still no `-p`/`--print`/`stream-json`.
163
+ *
164
+ * Story 057 (R2.2): an optional `mcpConfigFile` PATH is likewise folded into the SAME `flags` string as
165
+ * the DOUBLE-QUOTED `--mcp-config "<file>"` (a file path — the secret-bearing JSON stays off the command
166
+ * line), so this single addition reaches BOTH the `--resume "<id>"` launch AND the `|| claude` fresh
167
+ * fallback. R2.2 HARD rule: `--strict-mcp-config` is NEVER emitted, so a resumed turn keeps MERGING the
168
+ * scratch with the user's own `.mcp.json`/settings MCP servers. Still no `-p`/`--print`/`stream-json`.
150
169
  */
151
- export declare function buildResumeArgv(sessionId: string, permissionMode?: string, effortLevel?: string): [string, string];
170
+ export declare function buildResumeArgv(sessionId: string, permissionMode?: string, effortLevel?: string, agent?: string, additionalDirectories?: string[], mcpConfigFile?: string): [string, string];
152
171
  /** Options for {@link spawnResumePty}. */
153
172
  export interface SpawnResumeOptions {
154
173
  /** The prior session id to reattach to (== the JSONL transcript basename). */
@@ -166,6 +185,27 @@ export interface SpawnResumeOptions {
166
185
  permissionMode?: string;
167
186
  /** Story 046 (R2.2): carry `--effort <level>` into the resume argv for an effort re-spawn. */
168
187
  effortLevel?: string;
188
+ /**
189
+ * Story 056 (R3.3): carry the DOUBLE-QUOTED `--agent "<name>"` into the resume argv (both the
190
+ * `--resume` and the `|| claude` fallback branches). Injection-safe — re-asserted against the R3.3
191
+ * allowlist so an unsafe name is dropped. Absent → no flag.
192
+ */
193
+ agent?: string;
194
+ /**
195
+ * Story 057 (R1.1/R1.2): carry ONE double-quoted `--add-dir "<dir>"` per dir into the resume argv —
196
+ * folded into the shared `flags`, so the dirs reach BOTH the `--resume` and the `|| claude` fallback
197
+ * branches. Injection-safe — each dir re-asserted via {@link isSafeDir} so an unsafe dir is dropped.
198
+ * INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent/empty → no flag.
199
+ */
200
+ additionalDirectories?: string[];
201
+ /**
202
+ * Story 057 (R2.2): carry the DOUBLE-QUOTED `--mcp-config "<file>"` into the resume argv — folded into
203
+ * the shared `flags`, so the scratch-config path reaches BOTH the `--resume` and the `|| claude`
204
+ * fallback branches. NEVER paired with `--strict-mcp-config`, so the resumed turn keeps MERGING the
205
+ * scratch with the user's own `.mcp.json`/settings MCP servers (R2.2). A file path keeps the JSON off
206
+ * the command line. INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent → no flag.
207
+ */
208
+ mcpConfigFile?: string;
169
209
  }
170
210
  /**
171
211
  * Spawn the resume PTY, reusing story 013's sanitized env (R4.2) so the resumed turn keeps the
@@ -180,6 +220,12 @@ export declare function spawnResumePty(opts: SpawnResumeOptions): PtyEngineHandl
180
220
  export interface CreateSessionEngineOptions {
181
221
  /** Host working directory the spawned TUI runs in (story 013 spawn). */
182
222
  cwd: string;
223
+ /**
224
+ * Story 056 v4 — OPTIONAL pre-chosen session id for an in-place FRESH re-spawn (a pre-interaction
225
+ * selector change reuses the session's id). Forwarded to {@link spawnClaudePty}; absent → a fresh
226
+ * `randomUUID()` (the normal createSession path).
227
+ */
228
+ sessionId?: string;
183
229
  /** Base environment to sanitize; defaults to the parent process env. */
184
230
  baseEnv?: Record<string, string | undefined>;
185
231
  /** Injectable spawn function (defaults to node-pty's `spawn`); tests pass a fake. */
@@ -191,6 +237,25 @@ export interface CreateSessionEngineOptions {
191
237
  permissionMode?: string;
192
238
  /** Story 046 (R2.2): forwarded to {@link spawnClaudePty} as `--effort <level>` for a non-"default" seed. */
193
239
  effortLevel?: string;
240
+ /**
241
+ * Story 056 (R3.3): forwarded to {@link spawnClaudePty} as the DOUBLE-QUOTED `--agent "<name>"` for a
242
+ * fresh spawn seeded to a main-thread agent persona. Injection-safe (R3.3 allowlist re-assert).
243
+ * Absent → no flag.
244
+ */
245
+ agent?: string;
246
+ /**
247
+ * Story 057 (R1.1/R1.2): forwarded to {@link spawnClaudePty} as ONE double-quoted `--add-dir "<dir>"`
248
+ * per safe dir for a fresh spawn. Injection-safe (each dir re-asserted via {@link isSafeDir}).
249
+ * INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent/empty → no flag.
250
+ */
251
+ additionalDirectories?: string[];
252
+ /**
253
+ * Story 057 (R2.2): forwarded to {@link spawnClaudePty} as the DOUBLE-QUOTED `--mcp-config "<file>"`
254
+ * for a fresh spawn (the fork-controlled scratch path from `mcp-config-writer.ts`). NEVER paired with
255
+ * `--strict-mcp-config`, so claude MERGES the scratch with the user's own `.mcp.json`/settings MCP
256
+ * servers (R2.2). INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent → no flag.
257
+ */
258
+ mcpConfigFile?: string;
194
259
  /**
195
260
  * Story 034 (§9 hybrid gate): per-session SCRATCH settings file carrying the fork's
196
261
  * `PreToolUse` hook, forwarded to {@link spawnClaudePty} as `--settings "<file>"`. The
@@ -1 +1 @@
1
- {"version":3,"file":"engine-lifecycle.d.ts","sourceRoot":"","sources":["../src/engine-lifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine-lifecycle.d.ts","sourceRoot":"","sources":["../src/engine-lifecycle.ts"],"names":[],"mappings":"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"}