@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.
- package/NOTICE +1 -1
- package/README.md +1 -1
- package/dist/acp-agent.d.ts +62 -8
- package/dist/acp-agent.js +130 -15
- package/dist/agent-catalog.d.ts +0 -1
- package/dist/ansi-mirror.d.ts +0 -1
- package/dist/besteffort.d.ts +0 -1
- package/dist/billing/entrypoint-guard.d.ts +0 -1
- package/dist/claude-path.d.ts +0 -1
- package/dist/command-catalog.d.ts +84 -0
- package/dist/command-catalog.js +339 -0
- package/dist/diff-enriched-reader.d.ts +0 -1
- package/dist/diff-source.d.ts +0 -1
- package/dist/drift-checks.d.ts +0 -1
- package/dist/end-of-turn.d.ts +0 -1
- package/dist/engine-lifecycle.d.ts +0 -1
- package/dist/engine-pty.d.ts +0 -1
- package/dist/engine-watcher.d.ts +0 -1
- package/dist/engine.d.ts +0 -1
- package/dist/event-switch.d.ts +0 -1
- package/dist/gate/port.d.ts +0 -1
- package/dist/gate/settings-writer.d.ts +0 -1
- package/dist/image-input.d.ts +0 -1
- package/dist/image-vision-smoke.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/jsonl.d.ts +0 -1
- package/dist/lib.d.ts +0 -1
- package/dist/linearize.d.ts +1 -2
- package/dist/linearize.js +1 -1
- package/dist/live-diff-env.d.ts +0 -1
- package/dist/live-subagent-env.d.ts +0 -1
- package/dist/mcp-config-writer.d.ts +0 -1
- package/dist/model-catalog.d.ts +39 -1
- package/dist/model-catalog.js +77 -7
- package/dist/permissions/allow-inject.d.ts +0 -1
- package/dist/permissions/deny.d.ts +12 -1
- package/dist/permissions/deny.js +18 -0
- package/dist/permissions/elicitation-bridge.d.ts +71 -0
- package/dist/permissions/elicitation-bridge.js +146 -0
- package/dist/permissions/gate-wiring.d.ts +23 -3
- package/dist/permissions/gate-wiring.js +123 -1
- package/dist/permissions/hook-server.d.ts +11 -3
- package/dist/permissions/hook-server.js +10 -1
- package/dist/permissions/permission-mode.d.ts +0 -1
- package/dist/permissions/request-permission.d.ts +0 -1
- package/dist/settings.d.ts +0 -1
- package/dist/stop-reason-map.d.ts +0 -1
- package/dist/subagent-gate.d.ts +0 -1
- package/dist/subagent-source.d.ts +0 -1
- package/dist/subagent-watcher.d.ts +0 -1
- package/dist/tools.d.ts +0 -1
- package/dist/usage-env.d.ts +0 -1
- package/dist/usage.d.ts +0 -1
- package/dist/utils.d.ts +0 -1
- package/dist/zed-register.d.ts +0 -1
- package/package.json +6 -3
- package/dist/acp-agent.d.ts.map +0 -1
- package/dist/agent-catalog.d.ts.map +0 -1
- package/dist/ansi-mirror.d.ts.map +0 -1
- package/dist/besteffort.d.ts.map +0 -1
- package/dist/billing/entrypoint-guard.d.ts.map +0 -1
- package/dist/claude-path.d.ts.map +0 -1
- package/dist/diff-enriched-reader.d.ts.map +0 -1
- package/dist/diff-source.d.ts.map +0 -1
- package/dist/drift-checks.d.ts.map +0 -1
- package/dist/end-of-turn.d.ts.map +0 -1
- package/dist/engine-lifecycle.d.ts.map +0 -1
- package/dist/engine-pty.d.ts.map +0 -1
- package/dist/engine-watcher.d.ts.map +0 -1
- package/dist/engine.d.ts.map +0 -1
- package/dist/event-switch.d.ts.map +0 -1
- package/dist/gate/port.d.ts.map +0 -1
- package/dist/gate/settings-writer.d.ts.map +0 -1
- package/dist/image-input.d.ts.map +0 -1
- package/dist/image-vision-smoke.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jsonl.d.ts.map +0 -1
- package/dist/lib.d.ts.map +0 -1
- package/dist/linearize.d.ts.map +0 -1
- package/dist/live-diff-env.d.ts.map +0 -1
- package/dist/live-subagent-env.d.ts.map +0 -1
- package/dist/mcp-config-writer.d.ts.map +0 -1
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/permissions/allow-inject.d.ts.map +0 -1
- package/dist/permissions/deny.d.ts.map +0 -1
- package/dist/permissions/gate-wiring.d.ts.map +0 -1
- package/dist/permissions/hook-server.d.ts.map +0 -1
- package/dist/permissions/permission-mode.d.ts.map +0 -1
- package/dist/permissions/request-permission.d.ts.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/stop-reason-map.d.ts.map +0 -1
- package/dist/subagent-gate.d.ts.map +0 -1
- package/dist/subagent-source.d.ts.map +0 -1
- package/dist/subagent-watcher.d.ts.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/usage-env.d.ts.map +0 -1
- package/dist/usage.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- 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
|
package/dist/diff-source.d.ts
CHANGED
|
@@ -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
|
package/dist/drift-checks.d.ts
CHANGED
|
@@ -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
|
package/dist/end-of-turn.d.ts
CHANGED
|
@@ -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
|
package/dist/engine-pty.d.ts
CHANGED
|
@@ -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
|
package/dist/engine-watcher.d.ts
CHANGED
|
@@ -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
package/dist/event-switch.d.ts
CHANGED
package/dist/gate/port.d.ts
CHANGED
|
@@ -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
|
package/dist/image-input.d.ts
CHANGED
|
@@ -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
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
|
package/dist/linearize.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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");
|
package/dist/live-diff-env.d.ts
CHANGED
|
@@ -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
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -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
|