@lucascouts/claude-agent-tui 0.6.0 → 0.7.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 (99) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +1 -1
  3. package/dist/acp-agent.d.ts +62 -8
  4. package/dist/acp-agent.js +130 -15
  5. package/dist/agent-catalog.d.ts +0 -1
  6. package/dist/ansi-mirror.d.ts +0 -1
  7. package/dist/besteffort.d.ts +0 -1
  8. package/dist/billing/entrypoint-guard.d.ts +0 -1
  9. package/dist/claude-path.d.ts +0 -1
  10. package/dist/command-catalog.d.ts +84 -0
  11. package/dist/command-catalog.js +339 -0
  12. package/dist/diff-enriched-reader.d.ts +0 -1
  13. package/dist/diff-source.d.ts +0 -1
  14. package/dist/drift-checks.d.ts +0 -1
  15. package/dist/end-of-turn.d.ts +0 -1
  16. package/dist/engine-lifecycle.d.ts +0 -1
  17. package/dist/engine-pty.d.ts +0 -1
  18. package/dist/engine-watcher.d.ts +0 -1
  19. package/dist/engine.d.ts +0 -1
  20. package/dist/event-switch.d.ts +0 -1
  21. package/dist/gate/port.d.ts +0 -1
  22. package/dist/gate/settings-writer.d.ts +0 -1
  23. package/dist/image-input.d.ts +0 -1
  24. package/dist/image-vision-smoke.d.ts +0 -1
  25. package/dist/index.d.ts +0 -1
  26. package/dist/jsonl.d.ts +0 -1
  27. package/dist/lib.d.ts +0 -1
  28. package/dist/linearize.d.ts +1 -2
  29. package/dist/linearize.js +1 -1
  30. package/dist/live-diff-env.d.ts +0 -1
  31. package/dist/live-subagent-env.d.ts +0 -1
  32. package/dist/mcp-config-writer.d.ts +0 -1
  33. package/dist/model-catalog.d.ts +39 -1
  34. package/dist/model-catalog.js +77 -7
  35. package/dist/permissions/allow-inject.d.ts +0 -1
  36. package/dist/permissions/deny.d.ts +12 -1
  37. package/dist/permissions/deny.js +18 -0
  38. package/dist/permissions/elicitation-bridge.d.ts +71 -0
  39. package/dist/permissions/elicitation-bridge.js +146 -0
  40. package/dist/permissions/gate-wiring.d.ts +23 -3
  41. package/dist/permissions/gate-wiring.js +123 -1
  42. package/dist/permissions/hook-server.d.ts +11 -3
  43. package/dist/permissions/hook-server.js +10 -1
  44. package/dist/permissions/permission-mode.d.ts +0 -1
  45. package/dist/permissions/request-permission.d.ts +0 -1
  46. package/dist/settings.d.ts +0 -1
  47. package/dist/stop-reason-map.d.ts +0 -1
  48. package/dist/subagent-gate.d.ts +0 -1
  49. package/dist/subagent-source.d.ts +0 -1
  50. package/dist/subagent-watcher.d.ts +0 -1
  51. package/dist/tools.d.ts +0 -1
  52. package/dist/usage-env.d.ts +0 -1
  53. package/dist/usage.d.ts +0 -1
  54. package/dist/utils.d.ts +0 -1
  55. package/dist/zed-register.d.ts +0 -1
  56. package/package.json +6 -3
  57. package/dist/acp-agent.d.ts.map +0 -1
  58. package/dist/agent-catalog.d.ts.map +0 -1
  59. package/dist/ansi-mirror.d.ts.map +0 -1
  60. package/dist/besteffort.d.ts.map +0 -1
  61. package/dist/billing/entrypoint-guard.d.ts.map +0 -1
  62. package/dist/claude-path.d.ts.map +0 -1
  63. package/dist/diff-enriched-reader.d.ts.map +0 -1
  64. package/dist/diff-source.d.ts.map +0 -1
  65. package/dist/drift-checks.d.ts.map +0 -1
  66. package/dist/end-of-turn.d.ts.map +0 -1
  67. package/dist/engine-lifecycle.d.ts.map +0 -1
  68. package/dist/engine-pty.d.ts.map +0 -1
  69. package/dist/engine-watcher.d.ts.map +0 -1
  70. package/dist/engine.d.ts.map +0 -1
  71. package/dist/event-switch.d.ts.map +0 -1
  72. package/dist/gate/port.d.ts.map +0 -1
  73. package/dist/gate/settings-writer.d.ts.map +0 -1
  74. package/dist/image-input.d.ts.map +0 -1
  75. package/dist/image-vision-smoke.d.ts.map +0 -1
  76. package/dist/index.d.ts.map +0 -1
  77. package/dist/jsonl.d.ts.map +0 -1
  78. package/dist/lib.d.ts.map +0 -1
  79. package/dist/linearize.d.ts.map +0 -1
  80. package/dist/live-diff-env.d.ts.map +0 -1
  81. package/dist/live-subagent-env.d.ts.map +0 -1
  82. package/dist/mcp-config-writer.d.ts.map +0 -1
  83. package/dist/model-catalog.d.ts.map +0 -1
  84. package/dist/permissions/allow-inject.d.ts.map +0 -1
  85. package/dist/permissions/deny.d.ts.map +0 -1
  86. package/dist/permissions/gate-wiring.d.ts.map +0 -1
  87. package/dist/permissions/hook-server.d.ts.map +0 -1
  88. package/dist/permissions/permission-mode.d.ts.map +0 -1
  89. package/dist/permissions/request-permission.d.ts.map +0 -1
  90. package/dist/settings.d.ts.map +0 -1
  91. package/dist/stop-reason-map.d.ts.map +0 -1
  92. package/dist/subagent-gate.d.ts.map +0 -1
  93. package/dist/subagent-source.d.ts.map +0 -1
  94. package/dist/subagent-watcher.d.ts.map +0 -1
  95. package/dist/tools.d.ts.map +0 -1
  96. package/dist/usage-env.d.ts.map +0 -1
  97. package/dist/usage.d.ts.map +0 -1
  98. package/dist/utils.d.ts.map +0 -1
  99. package/dist/zed-register.d.ts.map +0 -1
@@ -0,0 +1,339 @@
1
+ // Story 063 / Task 1.2 (R1.1, R2, R3, R5) — OFFLINE discovery of custom slash-commands for the ACP
2
+ // fork's `available_commands` advertisement.
3
+ //
4
+ // The command analog of {@link file://./agent-catalog.ts}: PURE + dependency-injectable, sharing the
5
+ // exact style — a minimal inline frontmatter line-parser (NO YAML dep), graceful `readdirMd`/`readFile`
6
+ // seams (try/catch → the empty-state, NEVER a throw), an ordered `Map<name, entry>` first-wins dedup,
7
+ // and an alphabetical sort. Unlike `agent-catalog.ts` this surface is FULLY OFFLINE — no subprocess,
8
+ // no probe, no network: slash-commands live on disk (`<cwd>/.claude/commands/*.md`, `~/.claude/commands/
9
+ // *.md`) plus a curated built-in tier. The only `node:` builtins are os/fs/path; the single non-`node:`
10
+ // import is TYPE-ONLY (`AvailableCommand`), so it is erased at runtime and adds no runtime dependency
11
+ // (FORK.md pins node-pty + the SDK as the only runtime deps; this file adds none).
12
+ //
13
+ // PRECEDENCE (R1.1 / R7 / R8.1). Four disk surfaces are consulted, highest precedence first — commands
14
+ // out-scope skills at the SAME scope, and any cwd surface out-scopes any user surface:
15
+ // 1. `<cwd>/.claude/commands/*.md` (cwd commands)
16
+ // 2. `<cwd>/.claude/skills/*/SKILL.md` (cwd skills — R7)
17
+ // 3. `~/.claude/commands/*.md` (user commands)
18
+ // 4. `~/.claude/skills/*/SKILL.md` (user skills — R7)
19
+ // then the built-in tier (R9, populated in Task 1.5) is the LOWEST precedence: a built-in whose name
20
+ // collides with a disk entry loses. One entry per name (first-wins across the ordered surfaces). The
21
+ // visible result is FLAT ALPHABETICAL by `name` — tiers govern collision dedup ONLY, not grouping
22
+ // (determinism + parity with `agent-catalog.ts`'s `sortByValue`).
23
+ //
24
+ // SKILLS (R7 / R7.1): a skill lives at `<scope>/.claude/skills/<name>/SKILL.md`; the command `name` is
25
+ // the DIRECTORY name (same R3 allowlist as a command) and `description` comes from the `SKILL.md`
26
+ // frontmatter. A skills subdir WITHOUT a `SKILL.md` is NOT a command (the `readFile` throw skips it).
27
+ // Skills carry no `argument-hint`, so a skill entry is `{ name, description }` with no `input`.
28
+ //
29
+ // SECURITY (R3): a command `name` is the file's basename sans `.md`; it is checked against
30
+ // {@link SAFE_COMMAND_NAME} = `/^[a-z0-9_-]+$/` and the entry is DROPPED on any failure (a name with
31
+ // spaces, quotes, path separators, uppercase, … never renders). A missing/unreadable dir or file yields
32
+ // `[]` for that surface — discovery NEVER throws or crashes the session (R3 / R5).
33
+ //
34
+ // PURE + dependency-injectable (R5): all fs/home access goes through the {@link DiscoverCommandsDeps}
35
+ // seams so the unit tests inject an in-memory fs and never touch the real `~/.claude`; the built-in tier
36
+ // is likewise an injectable seam (`builtins`) defaulting to {@link BUILTIN_COMMANDS}.
37
+ //
38
+ // node:test runner: `node --experimental-strip-types --test test/command-catalog.test.ts`
39
+ import { homedir as osHomedir } from "node:os";
40
+ import { readdirSync, readFileSync } from "node:fs";
41
+ import { join } from "node:path";
42
+ /**
43
+ * The curated built-in slash-command tier (R9) — the interactive `claude` TUI's own built-ins, which are
44
+ * baked into the binary and therefore NOT disk-discoverable. This is a CURATED APPROXIMATION (C1), NOT a
45
+ * live probe: it stands beside the same static-curation idea as `MODEL_CATALOG`. It is the LOWEST-
46
+ * precedence tier, so any disk command/skill of the same name shadows a built-in (R1/R9). Names are
47
+ * lowercase (R3) and every entry carries a non-empty `description` (the SDK requires it). The list is
48
+ * intentionally small and conservative — a documented approximation, not an exhaustive enumeration.
49
+ */
50
+ export const BUILTIN_COMMANDS = [
51
+ { name: "clear", description: "Clear the conversation history and free up context" },
52
+ { name: "compact", description: "Summarize and compact the current conversation" },
53
+ { name: "config", description: "Open the settings / configuration panel" },
54
+ { name: "cost", description: "Show token usage and cost for the current session" },
55
+ { name: "help", description: "List available slash commands and usage help" },
56
+ { name: "init", description: "Initialize a CLAUDE.md with codebase documentation" },
57
+ { name: "memory", description: "Edit the project or user memory (CLAUDE.md) files" },
58
+ { name: "model", description: "Switch the active model for the session" },
59
+ { name: "resume", description: "Resume a previous conversation" },
60
+ { name: "review", description: "Review a pull request or the pending changes" },
61
+ ];
62
+ /**
63
+ * The command-name allowlist (R3). A single un-namespaced segment: LOWERCASE letters/digits/underscore/
64
+ * hyphen only (no spaces, no uppercase, no shell metacharacters, no path separators). Slash-command
65
+ * names are conventionally lowercase, so — unlike `agent-catalog.ts`'s {@link SAFE_AGENT_NAME} — this
66
+ * allowlist excludes uppercase.
67
+ */
68
+ export const SAFE_COMMAND_NAME = /^[a-z0-9_-]+$/;
69
+ /** True iff `name` is a non-empty string matching {@link SAFE_COMMAND_NAME} (R3). */
70
+ export function isSafeCommandName(name) {
71
+ return typeof name === "string" && SAFE_COMMAND_NAME.test(name);
72
+ }
73
+ /**
74
+ * Default `readdirMd`: list `*.md` filenames in `dir`, returning `[]` when the directory is missing or
75
+ * unreadable (the empty-state — never throws). Sorted for a stable first-wins ordering within a dir.
76
+ * Mirrors `agent-catalog.ts`'s `defaultReaddirMd`.
77
+ */
78
+ function defaultReaddirMd(dir) {
79
+ try {
80
+ return readdirSync(dir)
81
+ .filter((f) => f.toLowerCase().endsWith(".md"))
82
+ .sort();
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ }
88
+ /**
89
+ * Default `readdirDirs`: list the IMMEDIATE sub-directory names of `dir`, returning `[]` when the
90
+ * directory is missing or unreadable (the empty-state — never throws). Sorted for a stable first-wins
91
+ * ordering within the skills tier. Mirrors the graceful try/catch style of {@link defaultReaddirMd}.
92
+ */
93
+ function defaultReaddirDirs(dir) {
94
+ try {
95
+ return readdirSync(dir, { withFileTypes: true })
96
+ .filter((d) => d.isDirectory())
97
+ .map((d) => d.name)
98
+ .sort();
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ }
104
+ /** Default `readFile`: UTF-8 file read via the `node:fs` stdlib. */
105
+ function defaultReadFile(path) {
106
+ return readFileSync(path, "utf8");
107
+ }
108
+ /** The leading `name` (sans `.md`) of a command file — the command's name. */
109
+ function baseName(filename) {
110
+ return filename.replace(/\.md$/i, "");
111
+ }
112
+ /**
113
+ * Minimal `---`-fenced frontmatter line-parser (NOT a full YAML dep). Reads the leading `---\n … \n---`
114
+ * block and pulls the `description` and `argument-hint` `key: value` lines; a single pair of surrounding
115
+ * quotes on a value is stripped. The frontmatter key `argument-hint` (a HYPHEN) is returned as the
116
+ * camelCase {@link CommandFrontmatter.argumentHint}. A file whose first line is not `---` yields `{}`.
117
+ * Tiny + pure on purpose, mirroring `agent-catalog.ts`'s `parseFrontmatter` — the command frontmatter we
118
+ * consume is flat `key: value`.
119
+ */
120
+ export function parseCommandFrontmatter(content) {
121
+ // The opening fence must be the very first line. Bail to `{}` when there is no fenced block.
122
+ const lines = content.split(/\r?\n/);
123
+ if (lines[0]?.trim() !== "---")
124
+ return {};
125
+ const out = {};
126
+ for (let i = 1; i < lines.length; i++) {
127
+ const line = lines[i];
128
+ if (line.trim() === "---")
129
+ break; // closing fence — stop
130
+ const m = /^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(line);
131
+ if (!m)
132
+ continue; // not a `key: value` line — skip (lists/comments/blanks)
133
+ const key = m[1].toLowerCase();
134
+ if (key !== "description" && key !== "argument-hint")
135
+ continue;
136
+ let value = m[2].trim();
137
+ // Strip a single pair of matching surrounding quotes.
138
+ if (value.length >= 2 &&
139
+ ((value.startsWith('"') && value.endsWith('"')) ||
140
+ (value.startsWith("'") && value.endsWith("'")))) {
141
+ value = value.slice(1, -1);
142
+ }
143
+ if (value.length === 0)
144
+ continue;
145
+ if (key === "description")
146
+ out.description = value;
147
+ else
148
+ out.argumentHint = value; // `argument-hint` → camelCase `argumentHint`
149
+ }
150
+ return out;
151
+ }
152
+ /**
153
+ * Resolve a single command `*.md` file into an {@link AvailableCommand}, or `null` when it cannot
154
+ * contribute a SAFE entry. The name is the filename stem, run through the R3 allowlist — the file is
155
+ * DROPPED (→ `null`) on any failure, including an unreadable file (NEVER throws). `description` defaults
156
+ * to `""` (the SDK REQUIRES it) when absent; an `argument-hint` maps to `input: { hint }`, which is
157
+ * OMITTED entirely when there is none.
158
+ */
159
+ function resolveEntry(dir, filename, readFile) {
160
+ const name = baseName(filename);
161
+ // SECURITY (R3): drop any entry whose name escapes the allowlist — optional debug log, never a throw.
162
+ if (!isSafeCommandName(name)) {
163
+ if (process.env.FORK_COMMAND_DEBUG === "1") {
164
+ process.stderr.write(`[command-catalog] dropped unsafe command name: ${JSON.stringify(name)}\n`);
165
+ }
166
+ return null;
167
+ }
168
+ let fm;
169
+ try {
170
+ fm = parseCommandFrontmatter(readFile(join(dir, filename)));
171
+ }
172
+ catch {
173
+ return null; // unreadable file — skip it, never throw (R3 / R5 graceful)
174
+ }
175
+ const description = fm.description ?? ""; // AvailableCommand.description is REQUIRED
176
+ return {
177
+ name,
178
+ description,
179
+ ...(fm.argumentHint ? { input: { hint: fm.argumentHint } } : {}), // OMIT `input` when no hint
180
+ };
181
+ }
182
+ /**
183
+ * Resolve a single skill sub-directory into an {@link AvailableCommand}, or `null` when it cannot
184
+ * contribute a SAFE entry (R7 / R7.1). The name is the DIRECTORY name `subdirName`, run through the R3
185
+ * allowlist — DROPPED (→ `null`, same optional `FORK_COMMAND_DEBUG` log as {@link resolveEntry}) on any
186
+ * failure. The skill's `SKILL.md` is REQUIRED: `join(skillsDir, subdirName, "SKILL.md")` is read inside
187
+ * a try/catch and a subdir WITHOUT one (`readFile` throws) is NOT a skill (→ `null`, never throws).
188
+ * `description` is the `SKILL.md` frontmatter `description` (default `""` when absent). Skills carry NO
189
+ * `argument-hint`, so the entry is `{ name, description }` — no `input`.
190
+ */
191
+ function resolveSkillEntry(skillsDir, subdirName, readFile) {
192
+ const name = subdirName;
193
+ // SECURITY (R3): drop any skill whose dir name escapes the allowlist — optional debug log, never a throw.
194
+ if (!isSafeCommandName(name)) {
195
+ if (process.env.FORK_COMMAND_DEBUG === "1") {
196
+ process.stderr.write(`[command-catalog] dropped unsafe skill name: ${JSON.stringify(name)}\n`);
197
+ }
198
+ return null;
199
+ }
200
+ let fm;
201
+ try {
202
+ fm = parseCommandFrontmatter(readFile(join(skillsDir, subdirName, "SKILL.md")));
203
+ }
204
+ catch {
205
+ return null; // no readable SKILL.md — NOT a skill, skip it, never throw (R7 guard / R5 graceful)
206
+ }
207
+ const description = fm.description ?? ""; // AvailableCommand.description is REQUIRED
208
+ return { name, description }; // skills have no argument-hint → no `input`
209
+ }
210
+ /** Sort entries by `name` for a stable, scope-independent ordering (parity with `sortByValue`). */
211
+ function sortByName(entries) {
212
+ return entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
213
+ }
214
+ /**
215
+ * Read the set of ENABLED plugin MARKETPLACES from `~/.claude/plugins/installed_plugins.json` (R8). The
216
+ * gate is intentionally PRESENCE-in-the-manifest: per R8's explicitly-named source, an INSTALLED plugin
217
+ * == an ENABLED plugin for this story (a finer `settings.json` → `enabledPlugins` gate is out of scope).
218
+ *
219
+ * The real manifest shape is `{ version, plugins: { "<name>@<marketplace>": [ { installPath, … }, … ] } }`
220
+ * — a marketplace is enabled iff at least one `<name>@<marketplace>` key maps to a NON-EMPTY install
221
+ * array. Everything here is DEFENSIVE against manifest-shape drift (R8 risk): a MISSING or MALFORMED
222
+ * manifest, a non-object `plugins`, or any key/value that doesn't fit is treated as "disabled" and simply
223
+ * contributes nothing — this NEVER throws (an unreadable/parse failure degrades to the empty set → all
224
+ * plugins skipped).
225
+ *
226
+ * @param home the resolved home dir (the manifest lives under `home/.claude/plugins`).
227
+ * @param readFile the injectable UTF-8 file reader (throws on a missing manifest — caught here).
228
+ * @returns the set of enabled marketplace names (EMPTY when the manifest is absent/malformed).
229
+ */
230
+ function readEnabledMarketplaces(home, readFile) {
231
+ const path = join(home, ".claude", "plugins", "installed_plugins.json");
232
+ let parsed;
233
+ try {
234
+ parsed = JSON.parse(readFile(path));
235
+ }
236
+ catch {
237
+ return new Set(); // missing or malformed manifest → no enabled marketplaces (plugins skipped)
238
+ }
239
+ const enabled = new Set();
240
+ // `plugins` must be a plain, non-null object; anything else (array, primitive, null, absent) → empty.
241
+ const plugins = parsed?.plugins;
242
+ if (typeof plugins !== "object" || plugins === null || Array.isArray(plugins))
243
+ return enabled;
244
+ for (const [key, value] of Object.entries(plugins)) {
245
+ // An installed plugin: a `<name>@<marketplace>` key mapping to a NON-EMPTY install array.
246
+ if (!Array.isArray(value) || value.length === 0)
247
+ continue; // treat "not installed" as disabled
248
+ const at = key.lastIndexOf("@"); // marketplace = substring AFTER the last `@`
249
+ if (at < 0)
250
+ continue;
251
+ const marketplace = key.slice(at + 1);
252
+ if (marketplace.length > 0)
253
+ enabled.add(marketplace);
254
+ }
255
+ return enabled;
256
+ }
257
+ /**
258
+ * Discover the custom slash-commands to advertise via ACP `available_commands` — FULLY OFFLINE (no
259
+ * subprocess, no network).
260
+ *
261
+ * TIERS, highest precedence first:
262
+ * 1. `<cwd>/.claude/commands/*.md` (cwd commands — R1.1 precedence)
263
+ * 2. `<cwd>/.claude/skills/<name>/SKILL.md` (cwd skills — R7)
264
+ * 3. `~/.claude/commands/*.md` (user commands)
265
+ * 4. `~/.claude/skills/<name>/SKILL.md` (user skills — R7)
266
+ * 5. `~/.claude/plugins/marketplaces/<m>/{commands,skills}` (ENABLED-plugin tier — R8; a plugin
267
+ * surface loses a name collision to any higher tier — R8.1)
268
+ * 6. {@link DiscoverCommandsDeps.builtins} (the R9 built-in tier — LOWEST; a built-in loses a name
269
+ * collision to any disk entry)
270
+ *
271
+ * Each command `*.md` file yields `{ name, description, input? }` (R2): `name` = basename sans `.md`,
272
+ * `description` = frontmatter `description` (else `""`), `input: { hint }` from `argument-hint` (omitted
273
+ * when absent). Each skill `<name>/SKILL.md` yields `{ name, description }` (no `input`): `name` = the
274
+ * DIRECTORY name, `description` = the `SKILL.md` frontmatter (R7 / R7.1). Every name is dropped if it
275
+ * fails the R3 {@link SAFE_COMMAND_NAME} allowlist. Names are deduped FIRST-WINS across the ordered
276
+ * surfaces — so a cwd command out-ranks a cwd skill (cmd > skill, same scope), any cwd surface
277
+ * out-ranks any user surface, an enabled-plugin surface out-ranks the built-ins but LOSES to any
278
+ * cwd/user surface (R8.1) — then the merged set is returned FLAT ALPHABETICAL by `name`.
279
+ *
280
+ * Never throws (R3 / R5): a missing/unreadable dir or file yields `[]` for that surface; a skills subdir
281
+ * without a readable `SKILL.md` is skipped.
282
+ *
283
+ * @param cwd the SESSION cwd (project dir) whose `.claude/{commands,skills}` take precedence.
284
+ * @param deps injectable fs/home/builtins seams (default: `node:` stdlib + {@link BUILTIN_COMMANDS}).
285
+ */
286
+ export function discoverCommands(cwd, deps = {}) {
287
+ const homedir = deps.homedir ?? osHomedir;
288
+ const readdirMd = deps.readdirMd ?? defaultReaddirMd;
289
+ const readdirDirs = deps.readdirDirs ?? defaultReaddirDirs;
290
+ const readFile = deps.readFile ?? defaultReadFile;
291
+ const builtins = deps.builtins ?? BUILTIN_COMMANDS;
292
+ const home = homedir();
293
+ // Ordered disk surfaces (highest precedence first) — commands scanned via `readdirMd`+`resolveEntry`,
294
+ // skills via `readdirDirs`+`resolveSkillEntry`. A cwd command outranks a cwd skill; any cwd surface
295
+ // outranks any user surface (R1.1 / R7). The plugin tier (R8) slots in AFTER these, before builtins.
296
+ const userSources = [
297
+ { kind: "commands", dir: join(cwd, ".claude", "commands") }, // cwd commands — highest
298
+ { kind: "skills", dir: join(cwd, ".claude", "skills") }, // cwd skills
299
+ { kind: "commands", dir: join(home, ".claude", "commands") }, // user commands
300
+ { kind: "skills", dir: join(home, ".claude", "skills") }, // user skills
301
+ ];
302
+ const byName = new Map();
303
+ const add = (entry) => {
304
+ if (entry && !byName.has(entry.name))
305
+ byName.set(entry.name, entry);
306
+ };
307
+ // Fold one ordered disk surface into `byName` (first-wins): a commands dir via `readdirMd`, a skills
308
+ // dir via `readdirDirs`. Shared by the user tier and the per-marketplace plugin tier.
309
+ const foldSource = (source) => {
310
+ if (source.kind === "commands") {
311
+ for (const filename of readdirMd(source.dir))
312
+ add(resolveEntry(source.dir, filename, readFile));
313
+ }
314
+ else {
315
+ for (const subdir of readdirDirs(source.dir))
316
+ add(resolveSkillEntry(source.dir, subdir, readFile));
317
+ }
318
+ };
319
+ for (const source of userSources)
320
+ foldSource(source);
321
+ // Plugin tier (R8), AFTER the user surfaces and BEFORE builtins — gated to ENABLED marketplaces per
322
+ // `installed_plugins.json`. For each enabled marketplace (dirs already sorted), fold its `commands`
323
+ // then its `skills`; first-wins means any plugin name already claimed by a cwd/user surface is DROPPED
324
+ // (R8.1). A missing marketplaces dir / manifest degrades to skipping plugins (never throws).
325
+ const marketplacesDir = join(home, ".claude", "plugins", "marketplaces");
326
+ const enabled = readEnabledMarketplaces(home, readFile);
327
+ for (const m of readdirDirs(marketplacesDir)) {
328
+ if (!enabled.has(m))
329
+ continue; // treat a not-installed marketplace as disabled
330
+ foldSource({ kind: "commands", dir: join(marketplacesDir, m, "commands") });
331
+ foldSource({ kind: "skills", dir: join(marketplacesDir, m, "skills") });
332
+ }
333
+ // Built-in tier LAST (lowest precedence): a built-in whose name already appeared on disk is dropped.
334
+ for (const cmd of builtins) {
335
+ if (isSafeCommandName(cmd.name) && !byName.has(cmd.name))
336
+ byName.set(cmd.name, cmd);
337
+ }
338
+ return sortByName([...byName.values()]);
339
+ }
@@ -38,4 +38,3 @@ export interface DiffEnrichedReaderOptions {
38
38
  * @returns a {@link GetMessages} that yields the base messages with `toolUseResult` hydrated by uuid.
39
39
  */
40
40
  export declare function createDiffEnrichedReader(base: GetMessages, opts?: DiffEnrichedReaderOptions): GetMessages;
41
- //# sourceMappingURL=diff-enriched-reader.d.ts.map
@@ -101,4 +101,3 @@ export interface DiffToolCallUpdate {
101
101
  * diff content (an unsupported/not-a-diff-source source, or a translation with no `content`).
102
102
  */
103
103
  export declare function diffToolCallUpdate(source: DiffSource, toolCallId: string): DiffToolCallUpdate | null;
104
- //# sourceMappingURL=diff-source.d.ts.map
@@ -54,4 +54,3 @@ export declare function checkJsonlShape(lines: ReadonlyArray<Record<string, unkn
54
54
  * allow-inject.ts:29 `KEYSTROKE_YES`. Pure: depends only on `keystroke`.
55
55
  */
56
56
  export declare function checkAllowKeystroke(keystroke: string): CheckResult;
57
- //# sourceMappingURL=drift-checks.d.ts.map
@@ -175,4 +175,3 @@ export interface TurnResolver {
175
175
  * late boundary cannot produce a second settlement).
176
176
  */
177
177
  export declare function createTurnResolver(opts?: TurnResolverOptions): TurnResolver;
178
- //# sourceMappingURL=end-of-turn.d.ts.map
@@ -303,4 +303,3 @@ export interface CreateSessionEngineOptions {
303
303
  * ──────────────────────────────────────────────────────────────────────────────────────────────
304
304
  */
305
305
  export declare function createSessionEngine(opts: CreateSessionEngineOptions): SessionEngine;
306
- //# sourceMappingURL=engine-lifecycle.d.ts.map
@@ -218,4 +218,3 @@ export declare const CLEAR_INPUT_DELAY_MS = 60;
218
218
  * try/catch); the payload and `\r` writes are scheduled and post-exit-safe.
219
219
  */
220
220
  export declare function sendPrompt(p: IPty, text: string, schedule?: (fn: () => void, ms: number) => void): void;
221
- //# sourceMappingURL=engine-pty.d.ts.map
@@ -80,4 +80,3 @@ export interface JsonlWatcher extends SessionWatcher {
80
80
  * @returns a {@link JsonlWatcher} (a {@link SessionWatcher} plus `notifyEndOfTurn`).
81
81
  */
82
82
  export declare function createJsonlWatcher(opts: JsonlWatcherOptions): JsonlWatcher;
83
- //# sourceMappingURL=engine-watcher.d.ts.map
package/dist/engine.d.ts CHANGED
@@ -27,4 +27,3 @@ export declare class StubEngine implements Engine {
27
27
  }
28
28
  /** Factory for the default story-011 engine (the no-op stub). */
29
29
  export declare function createStubEngine(): Engine;
30
- //# sourceMappingURL=engine.d.ts.map
@@ -161,4 +161,3 @@ export type EventClassification = ContentClassification | LifecycleClassificatio
161
161
  */
162
162
  export declare function classifyEvent(event: JsonlEvent, opts?: ClassifyOptions): EventClassification;
163
163
  export {};
164
- //# sourceMappingURL=event-switch.d.ts.map
@@ -35,4 +35,3 @@ export type ServerFactory = () => PortServer;
35
35
  * the last bind error.
36
36
  */
37
37
  export declare function findFreePort(attempts?: number, makeServer?: ServerFactory): Promise<number>;
38
- //# sourceMappingURL=port.d.ts.map
@@ -158,4 +158,3 @@ export declare function restore(backup: Backup): Promise<RestoreResult>;
158
158
  * (ENOENT → treated as `{}`), mirroring the rest of the module's discipline.
159
159
  */
160
160
  export declare function applyUltracodeSettings(settingsPath: string, active: boolean): Promise<void>;
161
- //# sourceMappingURL=settings-writer.d.ts.map
@@ -28,4 +28,3 @@ export declare function materializeImage(data: string, mimeType: string | undefi
28
28
  * (no image materialized this turn) returns immediately.
29
29
  */
30
30
  export declare function cleanupMaterializedImages(paths: readonly string[] | undefined): void;
31
- //# sourceMappingURL=image-input.d.ts.map
@@ -49,4 +49,3 @@ export declare function imageVisionSmoke(version: string | null | undefined): Vi
49
49
  * of the caller's intent — it is pure aside from the single conditional `log` call (R3.1, R3.2).
50
50
  */
51
51
  export declare function reportImageVisionSmoke(version: string | null | undefined, log?: (...args: unknown[]) => void): VisionSmokeResult;
52
- //# sourceMappingURL=image-vision-smoke.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map
package/dist/jsonl.d.ts CHANGED
@@ -264,4 +264,3 @@ export declare function stripHeavyImages(message: unknown, imageSkipBytes?: numb
264
264
  * @returns the typed {@link JsonlEvent}.
265
265
  */
266
266
  export declare function projectEvent(message: unknown, opts?: ProjectOptions): JsonlEvent;
267
- //# sourceMappingURL=jsonl.d.ts.map
package/dist/lib.d.ts CHANGED
@@ -3,4 +3,3 @@ export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./u
3
3
  export { toolInfoFromToolUse, toDisplayPath, planEntries, toolUpdateFromToolResult, } from "./tools.js";
4
4
  export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
5
5
  export type { ClaudePlanEntry } from "./tools.js";
6
- //# sourceMappingURL=lib.d.ts.map
@@ -137,7 +137,7 @@ export declare function readOrderedMessages(sessionId: string, dir: string | und
137
137
  * Exported (story 043 R2.1) so acp-agent.ts can reuse it as the PRODUCTION reduced base it wraps in
138
138
  * the diff-enriched reader when `liveDiff` is ON (it is the same reduced reader `readOrderedMessages`
139
139
  * already falls back to). linearize.ts is a fork module imported directly from ../dist/ — NOT via
140
- * lib.ts, whose public surface is frozen to upstream v0.39.0 — so this export is safe.
140
+ * lib.ts, whose public surface is frozen to upstream v0.53.0 — so this export is safe.
141
141
  */
142
142
  export declare function defaultGetMessages(sessionId: string, opts?: {
143
143
  dir?: string;
@@ -216,4 +216,3 @@ export interface EndOfTurnBinding {
216
216
  * @returns an {@link EndOfTurnBinding} (`notifyEndOfTurn` + `stop`).
217
217
  */
218
218
  export declare function bindEndOfTurnReparse(opts: BindEndOfTurnOptions): EndOfTurnBinding;
219
- //# sourceMappingURL=linearize.d.ts.map
package/dist/linearize.js CHANGED
@@ -277,7 +277,7 @@ export async function readOrderedMessages(sessionId, dir, opts = {}) {
277
277
  * Exported (story 043 R2.1) so acp-agent.ts can reuse it as the PRODUCTION reduced base it wraps in
278
278
  * the diff-enriched reader when `liveDiff` is ON (it is the same reduced reader `readOrderedMessages`
279
279
  * already falls back to). linearize.ts is a fork module imported directly from ../dist/ — NOT via
280
- * lib.ts, whose public surface is frozen to upstream v0.39.0 — so this export is safe.
280
+ * lib.ts, whose public surface is frozen to upstream v0.53.0 — so this export is safe.
281
281
  */
282
282
  export async function defaultGetMessages(sessionId, opts) {
283
283
  const sdk = await import("@anthropic-ai/claude-agent-sdk");
@@ -4,4 +4,3 @@
4
4
  * index.ts calls this so the truth table is unit-checkable without running the entrypoint.
5
5
  */
6
6
  export declare function liveDiffEnabled(env?: Record<string, string | undefined>): boolean;
7
- //# sourceMappingURL=live-diff-env.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * index.ts calls this so the truth table is unit-checkable without running the entrypoint.
5
5
  */
6
6
  export declare function liveSubagentWatchEnabled(env?: Record<string, string | undefined>): boolean;
7
- //# sourceMappingURL=live-subagent-env.d.ts.map
@@ -58,4 +58,3 @@ export declare function writeMcpScratch(config: ClaudeMcpConfig): Promise<string
58
58
  * @param filePath the path returned by {@link writeMcpScratch}.
59
59
  */
60
60
  export declare function removeMcpScratch(filePath: string): Promise<void>;
61
- //# sourceMappingURL=mcp-config-writer.d.ts.map
@@ -33,6 +33,44 @@ export declare const ULTRACODE_EFFORT_LABEL = "ultracode (xhigh + orchestration)
33
33
  * (the SDK signal `reconcileModeFromTranscript` clamps), so haiku/opusplan omit it.
34
34
  */
35
35
  export declare const MODEL_CATALOG: ModelInfo[];
36
+ /**
37
+ * Story 068 (R1, R1.1, R2) — the REAL per-alias context window, keyed by the EXACT {@link MODEL_CATALOG}
38
+ * `value`. These windows are NOT uniform: `opus` is natively 1M, `sonnet`/`haiku` are 200K, and
39
+ * `sonnet[1m]` is the explicit 1M variant. This map is the single source of truth that
40
+ * `inferContextWindowFromModel` (acp-agent.ts) consults BEFORE the `\b1m\b` regex fallback — the bug it
41
+ * fixes is `opus` having wrongly reported 200K (the regex only ever matched the literal `1m` token).
42
+ *
43
+ * Story 069 (R2): `default` and `opusplan` seed to 1M — `default` is the recommended Opus (the claude TUI's
44
+ * `/model default` resolves to `claude-opus-4-8[1m]`, a 1M model) and `opusplan` plans with Opus. This is
45
+ * only the PRE-FIRST-TURN seed: once a turn arrives, `inferContextWindowFromModelId` (story 069)
46
+ * AUTHORITATIVELY refines the window from the transcript's real `model`. Keys MIRROR `MODEL_CATALOG`
47
+ * `value`s; the drift guard lives in the test (068 anti-drift: every catalog value has an explicit entry).
48
+ */
49
+ export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
50
+ /**
51
+ * Story 069 (R1.1) — the REAL context window per concrete model ID, mirroring the claude CLI's
52
+ * `context:{window}` table. Used to AUTHORITATIVELY refine the window from a turn's actual `model`
53
+ * (the JSONL `model` field), correcting the alias seed. Opus is NOT uniform: 4.6 = 200K, 4.7+ = 1M.
54
+ * Dated snapshots / future versions are covered by the family+version heuristic in
55
+ * `inferContextWindowFromModelId`; this table is the exact-ID source of truth for today's gateway IDs.
56
+ */
57
+ export declare const MODEL_ID_CONTEXT_WINDOWS: Record<string, number>;
58
+ /**
59
+ * Story 072 — the version/context PREFIX the claude `/model` picker now shows before the static tagline
60
+ * (e.g. "Opus 4.8 with 1M context · Best for everyday, complex tasks"). Keyed by catalog `value`.
61
+ *
62
+ * CURATED + DRIFT-PRONE, exactly like MODEL_CATALOG membership: the fork holds only aliases pre-turn and
63
+ * cannot derive the concrete version (the SDK `supportedModels()` was cut), so these MIRROR the LIVE
64
+ * picker and MUST be re-verified on each model launch (source: the user's live `/model` output). The
65
+ * static tagline stays on `ModelInfo.description` (069 R3 untouched); this only prepends "<version> · ".
66
+ * `opusplan` is intentionally absent — its tagline ("Use Opus in plan mode, Sonnet otherwise") already
67
+ * names the models, so it renders bare.
68
+ */
69
+ export declare const MODEL_VERSION_LABELS: Record<string, string>;
70
+ /**
71
+ * Story 072 — compose the Zed selector description: "<version label> · <tagline>", or the bare tagline
72
+ * when no label exists (e.g. `opusplan`). PURE + TOTAL: never throws on a missing label or tagline.
73
+ */
74
+ export declare function modelSelectorDescription(info: ModelInfo): string;
36
75
  /** The safe fallback entry, kept as a named export so callers can seed/anchor on it without a lookup. */
37
76
  export declare const DEFAULT_MODEL_INFO: ModelInfo;
38
- //# sourceMappingURL=model-catalog.d.ts.map