@lucascouts/claude-agent-tui 0.5.2 → 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 +249 -21
- package/dist/acp-agent.js +573 -73
- package/dist/agent-catalog.d.ts +95 -0
- package/dist/agent-catalog.js +287 -0
- 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/claude-path.js +6 -0
- 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 +6 -1
- package/dist/end-of-turn.js +8 -1
- package/dist/engine-lifecycle.d.ts +66 -2
- package/dist/engine-lifecycle.js +43 -4
- package/dist/engine-pty.d.ts +70 -3
- package/dist/engine-pty.js +80 -6
- 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 +14 -1
- package/dist/gate/settings-writer.js +49 -0
- package/dist/image-input.d.ts +30 -0
- package/dist/image-input.js +79 -0
- package/dist/image-vision-smoke.d.ts +51 -0
- package/dist/image-vision-smoke.js +111 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +6 -0
- 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 +60 -0
- package/dist/mcp-config-writer.js +172 -0
- package/dist/model-catalog.d.ts +68 -3
- package/dist/model-catalog.js +123 -13
- 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/settings.js +9 -0
- 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/tools.js +5 -1
- package/dist/usage-env.d.ts +0 -1
- package/dist/usage.d.ts +3 -1
- package/dist/usage.js +3 -0
- package/dist/utils.d.ts +0 -1
- package/dist/zed-register.d.ts +0 -1
- package/package.json +12 -9
- package/dist/acp-agent.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/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/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
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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { McpServer } from "@agentclientprotocol/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* A single entry under claude's `--mcp-config` `mcpServers` map. A discriminated union over the
|
|
4
|
+
* transport claude understands:
|
|
5
|
+
* - http/sse → `{ type, url, headers? }` (headers as a plain `{ [name]: value }` object)
|
|
6
|
+
* - stdio → `{ command, args?, env? }` (NO `type` — claude infers stdio from `command`)
|
|
7
|
+
* ACP-transport servers have no `--mcp-config` equivalent and are dropped before this shape.
|
|
8
|
+
*/
|
|
9
|
+
export type ClaudeMcpEntry = {
|
|
10
|
+
type: "http" | "sse";
|
|
11
|
+
url: string;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
} | {
|
|
14
|
+
command: string;
|
|
15
|
+
args?: string[];
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
/** The full claude `--mcp-config` document: a name-keyed map of {@link ClaudeMcpEntry}. */
|
|
19
|
+
export interface ClaudeMcpConfig {
|
|
20
|
+
mcpServers: Record<string, ClaudeMcpEntry>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Translate the ACP `McpServer[]` union into claude's `--mcp-config` `{ mcpServers: { … } }` shape
|
|
24
|
+
* (R2.1). PURE and total — never throws; empty/undefined input yields `{ mcpServers: {} }`.
|
|
25
|
+
*
|
|
26
|
+
* Discriminator (per the ACP schema): `type === "http"|"sse"|"acp"` selects that transport; a server
|
|
27
|
+
* with NO `type` field is the bare stdio variant. Per-transport mapping:
|
|
28
|
+
* - http / sse → `{ type, url, headers? }`; `headers` is OMITTED when the ACP array is empty.
|
|
29
|
+
* - stdio → `{ command, args?, env? }`; `args` and `env` are each OMITTED when empty; NO `type`.
|
|
30
|
+
* - acp → DROPPED (ACP-channel transport has no `--mcp-config` equivalent); a one-line note
|
|
31
|
+
* is emitted to stderr (no url/headers/env — R2.3).
|
|
32
|
+
*
|
|
33
|
+
* Duplicate names collapse onto the same key (last wins) — claude's map is name-keyed, so this matches
|
|
34
|
+
* how claude itself would resolve a repeated server name.
|
|
35
|
+
*
|
|
36
|
+
* @param servers the ACP-declared MCP servers, or `undefined`/`[]` when the client declared none.
|
|
37
|
+
* @returns the claude `--mcp-config` document (possibly with an empty `mcpServers`).
|
|
38
|
+
*/
|
|
39
|
+
export declare function translateMcpServers(servers: readonly McpServer[] | undefined): ClaudeMcpConfig;
|
|
40
|
+
/**
|
|
41
|
+
* DURABLY write `config` to a fresh uuid-namespaced scratch file under the OS temp dir with mode
|
|
42
|
+
* `0600`, and RETURN its absolute path (R2.1, R2.3). The file is `JSON.stringify(config, null, 2)`
|
|
43
|
+
* plus a trailing newline. The caller passes the returned path to `claude --mcp-config <path>` and
|
|
44
|
+
* is responsible for removing it on teardown via {@link removeMcpScratch}.
|
|
45
|
+
*
|
|
46
|
+
* The file may carry MCP auth headers/env (secrets), so it is created owner-only (0600) and is NEVER
|
|
47
|
+
* logged here (R2.3).
|
|
48
|
+
*
|
|
49
|
+
* @param config the document from {@link translateMcpServers}.
|
|
50
|
+
* @returns the absolute path of the written scratch file.
|
|
51
|
+
* @throws {Error} if the durable write fails — the partial temp is cleaned up first (no residue).
|
|
52
|
+
*/
|
|
53
|
+
export declare function writeMcpScratch(config: ClaudeMcpConfig): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Idempotently remove a scratch file written by {@link writeMcpScratch} (R2.3 teardown). Uses
|
|
56
|
+
* `fs.rm(filePath, { force: true })`, so a missing/already-removed path is a no-op and NEVER throws.
|
|
57
|
+
*
|
|
58
|
+
* @param filePath the path returned by {@link writeMcpScratch}.
|
|
59
|
+
*/
|
|
60
|
+
export declare function removeMcpScratch(filePath: string): Promise<void>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Story 057 / Task 2.1 — translate ACP MCP servers → claude `--mcp-config` JSON + scratch-file seam.
|
|
2
|
+
//
|
|
3
|
+
// The fork spawns the real `claude` TUI; claude consumes externally-declared MCP servers via a
|
|
4
|
+
// `--mcp-config <file.json>` flag (a `{ mcpServers: { <name>: { … } } }` document). The Zed client,
|
|
5
|
+
// however, declares its MCP servers over ACP as a `McpServer[]` array (a discriminated union). This
|
|
6
|
+
// module is the standalone, OFFLINE-testable translation + scratch-file seam between the two:
|
|
7
|
+
//
|
|
8
|
+
// translateMcpServers(servers) → ClaudeMcpConfig (PURE — array → claude `{mcpServers:{…}}` shape)
|
|
9
|
+
// writeMcpScratch(config) → Promise<string> (DURABLE 0600 write → returns the scratch path)
|
|
10
|
+
// removeMcpScratch(path) → Promise<void> (idempotent unlink on teardown)
|
|
11
|
+
//
|
|
12
|
+
// It owns NO engine wiring — the before-spawn `--mcp-config` plumbing into acp-agent.ts/the engine is
|
|
13
|
+
// a separate Task (2.2/2.3). The scratch-JSON discipline (temp-file → fsync → atomic rename, with a
|
|
14
|
+
// guaranteed 0600 mode) mirrors `gate/settings-writer.ts`'s private `durableWrite`; that pattern is
|
|
15
|
+
// copied here rather than imported (it is not exported there).
|
|
16
|
+
//
|
|
17
|
+
// SECRETS (R2.3): the scratch file may carry MCP auth headers / env / URLs. Its mode is forced to
|
|
18
|
+
// `0600` (owner-only) and it is removable on teardown. The config object, headers, env, and url are
|
|
19
|
+
// NEVER logged — the ONLY log this module emits is a one-line note when an `acp`-transport server is
|
|
20
|
+
// dropped, and that note carries no secret-bearing field.
|
|
21
|
+
import * as fs from "node:fs/promises";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import * as os from "node:os";
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
/** Convert an ACP `{name,value}[]` list into a plain `{ [name]: value }` object. */
|
|
26
|
+
function pairsToObject(pairs) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const { name, value } of pairs) {
|
|
29
|
+
out[name] = value;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Translate the ACP `McpServer[]` union into claude's `--mcp-config` `{ mcpServers: { … } }` shape
|
|
35
|
+
* (R2.1). PURE and total — never throws; empty/undefined input yields `{ mcpServers: {} }`.
|
|
36
|
+
*
|
|
37
|
+
* Discriminator (per the ACP schema): `type === "http"|"sse"|"acp"` selects that transport; a server
|
|
38
|
+
* with NO `type` field is the bare stdio variant. Per-transport mapping:
|
|
39
|
+
* - http / sse → `{ type, url, headers? }`; `headers` is OMITTED when the ACP array is empty.
|
|
40
|
+
* - stdio → `{ command, args?, env? }`; `args` and `env` are each OMITTED when empty; NO `type`.
|
|
41
|
+
* - acp → DROPPED (ACP-channel transport has no `--mcp-config` equivalent); a one-line note
|
|
42
|
+
* is emitted to stderr (no url/headers/env — R2.3).
|
|
43
|
+
*
|
|
44
|
+
* Duplicate names collapse onto the same key (last wins) — claude's map is name-keyed, so this matches
|
|
45
|
+
* how claude itself would resolve a repeated server name.
|
|
46
|
+
*
|
|
47
|
+
* @param servers the ACP-declared MCP servers, or `undefined`/`[]` when the client declared none.
|
|
48
|
+
* @returns the claude `--mcp-config` document (possibly with an empty `mcpServers`).
|
|
49
|
+
*/
|
|
50
|
+
export function translateMcpServers(servers) {
|
|
51
|
+
const mcpServers = {};
|
|
52
|
+
if (!servers) {
|
|
53
|
+
return { mcpServers };
|
|
54
|
+
}
|
|
55
|
+
for (const server of servers) {
|
|
56
|
+
// The ACP union is NOT a uniform discriminated union: http/sse/acp carry a `type` tag, but the
|
|
57
|
+
// stdio variant has NO `type` field at all. So a `"type" in server` guard (not `server.type`)
|
|
58
|
+
// first splits the tagged members from the bare stdio variant; within the tagged set, `type`
|
|
59
|
+
// then selects http/sse vs acp.
|
|
60
|
+
if ("type" in server) {
|
|
61
|
+
if (server.type === "http" || server.type === "sse") {
|
|
62
|
+
const entry = { type: server.type, url: server.url };
|
|
63
|
+
if (server.headers.length > 0) {
|
|
64
|
+
entry.headers = pairsToObject(server.headers);
|
|
65
|
+
}
|
|
66
|
+
mcpServers[server.name] = entry;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// type === "acp": ACP-transport servers ride the ACP channel (mcp/connect…); claude's
|
|
70
|
+
// --mcp-config cannot express them, so drop. Log ONLY the (non-secret) name — never
|
|
71
|
+
// id/url/headers/env (R2.3).
|
|
72
|
+
console.error(`[mcp-config-writer] dropping ACP-transport MCP server ${JSON.stringify(server.name)}: ` +
|
|
73
|
+
`no --mcp-config equivalent (ACP-channel transport is not supported by the claude CLI).`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// No `type` field → the bare stdio variant: `{ command, args, env }`. NO `type` is emitted
|
|
78
|
+
// (claude infers stdio from `command`); empty `args`/`env` are omitted.
|
|
79
|
+
const entry = { command: server.command };
|
|
80
|
+
if (server.args.length > 0) {
|
|
81
|
+
entry.args = [...server.args];
|
|
82
|
+
}
|
|
83
|
+
if (server.env.length > 0) {
|
|
84
|
+
entry.env = pairsToObject(server.env);
|
|
85
|
+
}
|
|
86
|
+
mcpServers[server.name] = entry;
|
|
87
|
+
}
|
|
88
|
+
return { mcpServers };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Durably write `text` to `filePath` with mode `0600` via a temp-file → fsync → atomic `rename` so a
|
|
92
|
+
* reader (claude on startup) never observes a partial file. The 0600 mode is GUARANTEED twice: the
|
|
93
|
+
* temp is created with `mode: 0o600`, and `chmod(0o600)` is re-applied after the rename so a permissive
|
|
94
|
+
* umask cannot loosen it. On any failure the partial temp is removed before the error propagates.
|
|
95
|
+
*
|
|
96
|
+
* Mirrors `gate/settings-writer.ts`'s private `durableWrite` (copied — that helper is not exported),
|
|
97
|
+
* with the added 0600 guarantee this scratch file requires (R2.3).
|
|
98
|
+
*/
|
|
99
|
+
async function durableWrite0600(filePath, text) {
|
|
100
|
+
const dir = path.dirname(filePath);
|
|
101
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.fork-tmp-${process.pid}-${Date.now()}`);
|
|
102
|
+
let handle = null;
|
|
103
|
+
try {
|
|
104
|
+
// Create the temp owner-only from the outset so its bytes are never world-readable, even briefly.
|
|
105
|
+
handle = await fs.open(tmp, "w", 0o600);
|
|
106
|
+
await handle.writeFile(text, "utf8");
|
|
107
|
+
await handle.sync(); // flush the file's own bytes to disk before the rename
|
|
108
|
+
await handle.close();
|
|
109
|
+
handle = null;
|
|
110
|
+
await fs.rename(tmp, filePath); // atomic swap into place
|
|
111
|
+
// Re-assert 0600 AFTER the rename so the active umask cannot have loosened the open() mode.
|
|
112
|
+
await fs.chmod(filePath, 0o600);
|
|
113
|
+
// Best-effort fsync of the directory entry so the rename itself is durable.
|
|
114
|
+
try {
|
|
115
|
+
const dirHandle = await fs.open(dir, "r");
|
|
116
|
+
try {
|
|
117
|
+
await dirHandle.sync();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await dirHandle.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Some platforms reject O_RDONLY dir fsync; the file fsync above is the load-bearing one.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (handle !== null) {
|
|
129
|
+
try {
|
|
130
|
+
await handle.close();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await fs.rm(tmp, { force: true });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// ignore — nothing durable to clean
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* DURABLY write `config` to a fresh uuid-namespaced scratch file under the OS temp dir with mode
|
|
147
|
+
* `0600`, and RETURN its absolute path (R2.1, R2.3). The file is `JSON.stringify(config, null, 2)`
|
|
148
|
+
* plus a trailing newline. The caller passes the returned path to `claude --mcp-config <path>` and
|
|
149
|
+
* is responsible for removing it on teardown via {@link removeMcpScratch}.
|
|
150
|
+
*
|
|
151
|
+
* The file may carry MCP auth headers/env (secrets), so it is created owner-only (0600) and is NEVER
|
|
152
|
+
* logged here (R2.3).
|
|
153
|
+
*
|
|
154
|
+
* @param config the document from {@link translateMcpServers}.
|
|
155
|
+
* @returns the absolute path of the written scratch file.
|
|
156
|
+
* @throws {Error} if the durable write fails — the partial temp is cleaned up first (no residue).
|
|
157
|
+
*/
|
|
158
|
+
export async function writeMcpScratch(config) {
|
|
159
|
+
const filePath = path.join(os.tmpdir(), `fork-acp-mcp-${randomUUID()}.json`);
|
|
160
|
+
const text = `${JSON.stringify(config, null, 2)}\n`;
|
|
161
|
+
await durableWrite0600(filePath, text);
|
|
162
|
+
return filePath;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Idempotently remove a scratch file written by {@link writeMcpScratch} (R2.3 teardown). Uses
|
|
166
|
+
* `fs.rm(filePath, { force: true })`, so a missing/already-removed path is a no-op and NEVER throws.
|
|
167
|
+
*
|
|
168
|
+
* @param filePath the path returned by {@link writeMcpScratch}.
|
|
169
|
+
*/
|
|
170
|
+
export async function removeMcpScratch(filePath) {
|
|
171
|
+
await fs.rm(filePath, { force: true });
|
|
172
|
+
}
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -1,11 +1,76 @@
|
|
|
1
1
|
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Effort levels offered for reasoning-capable models — the SDK's `supportedEffortLevels` vocabulary.
|
|
4
|
+
* Story 060: exported so the ultracode guard test can assert membership stays the five real levels
|
|
5
|
+
* (it must NEVER contain {@link ULTRACODE_EFFORT}; the real `--effort` enum is exactly this set).
|
|
6
|
+
*/
|
|
7
|
+
export declare const REASONING_EFFORT_LEVELS: NonNullable<ModelInfo["supportedEffortLevels"]>;
|
|
8
|
+
/**
|
|
9
|
+
* Story 060 — the "ultracode" pseudo-level sentinel for the effort selector. NOT a real `--effort`
|
|
10
|
+
* value (claude 2.1.195 rejects `--effort ultracode`; the enum stays {@link REASONING_EFFORT_LEVELS}).
|
|
11
|
+
* Selecting it maps to `--effort xhigh` PLUS activating the ultracode keyword-trigger for the session
|
|
12
|
+
* (story 060 Task 3). Kept OUT of REASONING_EFFORT_LEVELS so no real-effort code path ever emits it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ULTRACODE_EFFORT = "ultracode";
|
|
15
|
+
/** The real effort level the ultracode pseudo-level maps to (its documented effort component). */
|
|
16
|
+
export declare const ULTRACODE_EFFORT_LEVEL = "xhigh";
|
|
17
|
+
/** Selector label — makes clear it is xhigh + orchestration, NOT a duplicate of the plain `xhigh` entry. */
|
|
18
|
+
export declare const ULTRACODE_EFFORT_LABEL = "ultracode (xhigh + orchestration)";
|
|
2
19
|
/**
|
|
3
20
|
* The static curated catalog advertised to the Zed Agent Panel's `model` selector. Each `value` is a
|
|
4
21
|
* `claude` TUI alias accepted by `/model <alias>` (live) and `--model <alias>` (spawn). `default` is
|
|
5
|
-
* first and is the safe fallback.
|
|
6
|
-
*
|
|
22
|
+
* first and is the safe fallback.
|
|
23
|
+
*
|
|
24
|
+
* ORDER + membership mirror the ORIGINAL's `/model` picker, LIVE-VERIFIED against 2.1.195 (PTY probe):
|
|
25
|
+
* `Default (recommended)`, `Opus`, `Sonnet`, `Sonnet (1M context)`, `Haiku`. The original gets this
|
|
26
|
+
* from the SDK `supportedModels()` the fork cut, so we curate it statically. The 1M alias is literally
|
|
27
|
+
* `sonnet[1m]` (`/model sonnet[1m]` accepted; `sonnet-1m` → "not found"). `opusplan` is a fork-only
|
|
28
|
+
* EXTRA (a real `claude` plan-mode preset the original does not list) kept as the trailing entry.
|
|
29
|
+
*
|
|
30
|
+
* Effort-capable models (`default`/`opus`/`sonnet`/`sonnet[1m]`) carry `supportsEffort` +
|
|
31
|
+
* `supportedEffortLevels`; `haiku`/`opusplan` advertise none. `supportsAutoMode: true` on the same four
|
|
32
|
+
* surfaces the `auto` permission mode (a model classifier) — the original drops `auto` on `haiku`
|
|
33
|
+
* (the SDK signal `reconcileModeFromTranscript` clamps), so haiku/opusplan omit it.
|
|
7
34
|
*/
|
|
8
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;
|
|
9
75
|
/** The safe fallback entry, kept as a named export so callers can seed/anchor on it without a lookup. */
|
|
10
76
|
export declare const DEFAULT_MODEL_INFO: ModelInfo;
|
|
11
|
-
//# sourceMappingURL=model-catalog.d.ts.map
|
package/dist/model-catalog.js
CHANGED
|
@@ -1,45 +1,155 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Effort levels offered for reasoning-capable models — the SDK's `supportedEffortLevels` vocabulary.
|
|
3
|
+
* Story 060: exported so the ultracode guard test can assert membership stays the five real levels
|
|
4
|
+
* (it must NEVER contain {@link ULTRACODE_EFFORT}; the real `--effort` enum is exactly this set).
|
|
5
|
+
*/
|
|
6
|
+
export const REASONING_EFFORT_LEVELS = [
|
|
3
7
|
"low",
|
|
4
8
|
"medium",
|
|
5
9
|
"high",
|
|
10
|
+
"xhigh",
|
|
11
|
+
"max",
|
|
6
12
|
];
|
|
13
|
+
/**
|
|
14
|
+
* Story 060 — the "ultracode" pseudo-level sentinel for the effort selector. NOT a real `--effort`
|
|
15
|
+
* value (claude 2.1.195 rejects `--effort ultracode`; the enum stays {@link REASONING_EFFORT_LEVELS}).
|
|
16
|
+
* Selecting it maps to `--effort xhigh` PLUS activating the ultracode keyword-trigger for the session
|
|
17
|
+
* (story 060 Task 3). Kept OUT of REASONING_EFFORT_LEVELS so no real-effort code path ever emits it.
|
|
18
|
+
*/
|
|
19
|
+
export const ULTRACODE_EFFORT = "ultracode";
|
|
20
|
+
/** The real effort level the ultracode pseudo-level maps to (its documented effort component). */
|
|
21
|
+
export const ULTRACODE_EFFORT_LEVEL = "xhigh";
|
|
22
|
+
/** Selector label — makes clear it is xhigh + orchestration, NOT a duplicate of the plain `xhigh` entry. */
|
|
23
|
+
export const ULTRACODE_EFFORT_LABEL = "ultracode (xhigh + orchestration)";
|
|
7
24
|
/**
|
|
8
25
|
* The static curated catalog advertised to the Zed Agent Panel's `model` selector. Each `value` is a
|
|
9
26
|
* `claude` TUI alias accepted by `/model <alias>` (live) and `--model <alias>` (spawn). `default` is
|
|
10
|
-
* first and is the safe fallback.
|
|
11
|
-
*
|
|
27
|
+
* first and is the safe fallback.
|
|
28
|
+
*
|
|
29
|
+
* ORDER + membership mirror the ORIGINAL's `/model` picker, LIVE-VERIFIED against 2.1.195 (PTY probe):
|
|
30
|
+
* `Default (recommended)`, `Opus`, `Sonnet`, `Sonnet (1M context)`, `Haiku`. The original gets this
|
|
31
|
+
* from the SDK `supportedModels()` the fork cut, so we curate it statically. The 1M alias is literally
|
|
32
|
+
* `sonnet[1m]` (`/model sonnet[1m]` accepted; `sonnet-1m` → "not found"). `opusplan` is a fork-only
|
|
33
|
+
* EXTRA (a real `claude` plan-mode preset the original does not list) kept as the trailing entry.
|
|
34
|
+
*
|
|
35
|
+
* Effort-capable models (`default`/`opus`/`sonnet`/`sonnet[1m]`) carry `supportsEffort` +
|
|
36
|
+
* `supportedEffortLevels`; `haiku`/`opusplan` advertise none. `supportsAutoMode: true` on the same four
|
|
37
|
+
* surfaces the `auto` permission mode (a model classifier) — the original drops `auto` on `haiku`
|
|
38
|
+
* (the SDK signal `reconcileModeFromTranscript` clamps), so haiku/opusplan omit it.
|
|
12
39
|
*/
|
|
13
40
|
export const MODEL_CATALOG = [
|
|
14
41
|
{
|
|
15
42
|
value: "default",
|
|
16
|
-
displayName: "Default",
|
|
17
|
-
|
|
43
|
+
displayName: "Default (recommended)",
|
|
44
|
+
// Story 069 (R3): `default` resolves to the recommended Opus, so it carries the Opus description.
|
|
45
|
+
description: "Best for everyday, complex tasks",
|
|
46
|
+
supportsEffort: true,
|
|
47
|
+
supportedEffortLevels: REASONING_EFFORT_LEVELS,
|
|
48
|
+
supportsAutoMode: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "opus",
|
|
52
|
+
displayName: "Opus",
|
|
53
|
+
description: "Best for everyday, complex tasks",
|
|
54
|
+
supportsEffort: true,
|
|
55
|
+
supportedEffortLevels: REASONING_EFFORT_LEVELS,
|
|
56
|
+
supportsAutoMode: true,
|
|
18
57
|
},
|
|
19
58
|
{
|
|
20
59
|
value: "sonnet",
|
|
21
60
|
displayName: "Sonnet",
|
|
22
|
-
description: "
|
|
61
|
+
description: "Efficient for routine tasks",
|
|
23
62
|
supportsEffort: true,
|
|
24
63
|
supportedEffortLevels: REASONING_EFFORT_LEVELS,
|
|
64
|
+
supportsAutoMode: true,
|
|
25
65
|
},
|
|
26
66
|
{
|
|
27
|
-
value: "
|
|
28
|
-
displayName: "
|
|
29
|
-
|
|
67
|
+
value: "sonnet[1m]",
|
|
68
|
+
displayName: "Sonnet (1M context)",
|
|
69
|
+
// Story 069 (R3): fork-only 1M variant — the Sonnet description plus its 1M-window note.
|
|
70
|
+
description: "Efficient for routine tasks, with a 1M-token context window",
|
|
30
71
|
supportsEffort: true,
|
|
31
72
|
supportedEffortLevels: REASONING_EFFORT_LEVELS,
|
|
73
|
+
supportsAutoMode: true,
|
|
32
74
|
},
|
|
33
75
|
{
|
|
34
76
|
value: "haiku",
|
|
35
77
|
displayName: "Haiku",
|
|
36
|
-
description: "
|
|
78
|
+
description: "Fastest for quick answers",
|
|
37
79
|
},
|
|
38
80
|
{
|
|
39
81
|
value: "opusplan",
|
|
40
|
-
displayName: "Opus Plan",
|
|
41
|
-
description: "Opus
|
|
82
|
+
displayName: "Opus Plan Mode",
|
|
83
|
+
description: "Use Opus in plan mode, Sonnet otherwise",
|
|
42
84
|
},
|
|
43
85
|
];
|
|
86
|
+
/**
|
|
87
|
+
* Story 068 (R1, R1.1, R2) — the REAL per-alias context window, keyed by the EXACT {@link MODEL_CATALOG}
|
|
88
|
+
* `value`. These windows are NOT uniform: `opus` is natively 1M, `sonnet`/`haiku` are 200K, and
|
|
89
|
+
* `sonnet[1m]` is the explicit 1M variant. This map is the single source of truth that
|
|
90
|
+
* `inferContextWindowFromModel` (acp-agent.ts) consults BEFORE the `\b1m\b` regex fallback — the bug it
|
|
91
|
+
* fixes is `opus` having wrongly reported 200K (the regex only ever matched the literal `1m` token).
|
|
92
|
+
*
|
|
93
|
+
* Story 069 (R2): `default` and `opusplan` seed to 1M — `default` is the recommended Opus (the claude TUI's
|
|
94
|
+
* `/model default` resolves to `claude-opus-4-8[1m]`, a 1M model) and `opusplan` plans with Opus. This is
|
|
95
|
+
* only the PRE-FIRST-TURN seed: once a turn arrives, `inferContextWindowFromModelId` (story 069)
|
|
96
|
+
* AUTHORITATIVELY refines the window from the transcript's real `model`. Keys MIRROR `MODEL_CATALOG`
|
|
97
|
+
* `value`s; the drift guard lives in the test (068 anti-drift: every catalog value has an explicit entry).
|
|
98
|
+
*/
|
|
99
|
+
export const MODEL_CONTEXT_WINDOWS = {
|
|
100
|
+
default: 1_000_000,
|
|
101
|
+
opus: 1_000_000,
|
|
102
|
+
sonnet: 200_000,
|
|
103
|
+
"sonnet[1m]": 1_000_000,
|
|
104
|
+
haiku: 200_000,
|
|
105
|
+
opusplan: 1_000_000,
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Story 069 (R1.1) — the REAL context window per concrete model ID, mirroring the claude CLI's
|
|
109
|
+
* `context:{window}` table. Used to AUTHORITATIVELY refine the window from a turn's actual `model`
|
|
110
|
+
* (the JSONL `model` field), correcting the alias seed. Opus is NOT uniform: 4.6 = 200K, 4.7+ = 1M.
|
|
111
|
+
* Dated snapshots / future versions are covered by the family+version heuristic in
|
|
112
|
+
* `inferContextWindowFromModelId`; this table is the exact-ID source of truth for today's gateway IDs.
|
|
113
|
+
*/
|
|
114
|
+
export const MODEL_ID_CONTEXT_WINDOWS = {
|
|
115
|
+
"claude-opus-4-8": 1_000_000,
|
|
116
|
+
"claude-opus-4-7": 1_000_000,
|
|
117
|
+
"claude-opus-4-6": 200_000,
|
|
118
|
+
"claude-fable-5": 1_000_000,
|
|
119
|
+
"claude-sonnet-5": 1_000_000,
|
|
120
|
+
"claude-sonnet-4-6": 200_000,
|
|
121
|
+
"claude-sonnet-4-5-20250929": 200_000,
|
|
122
|
+
"claude-sonnet-4-20250514": 200_000,
|
|
123
|
+
"claude-haiku-4-5": 200_000,
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Story 072 — the version/context PREFIX the claude `/model` picker now shows before the static tagline
|
|
127
|
+
* (e.g. "Opus 4.8 with 1M context · Best for everyday, complex tasks"). Keyed by catalog `value`.
|
|
128
|
+
*
|
|
129
|
+
* CURATED + DRIFT-PRONE, exactly like MODEL_CATALOG membership: the fork holds only aliases pre-turn and
|
|
130
|
+
* cannot derive the concrete version (the SDK `supportedModels()` was cut), so these MIRROR the LIVE
|
|
131
|
+
* picker and MUST be re-verified on each model launch (source: the user's live `/model` output). The
|
|
132
|
+
* static tagline stays on `ModelInfo.description` (069 R3 untouched); this only prepends "<version> · ".
|
|
133
|
+
* `opusplan` is intentionally absent — its tagline ("Use Opus in plan mode, Sonnet otherwise") already
|
|
134
|
+
* names the models, so it renders bare.
|
|
135
|
+
*/
|
|
136
|
+
export const MODEL_VERSION_LABELS = {
|
|
137
|
+
default: "Opus 4.8 with 1M context",
|
|
138
|
+
opus: "Opus 4.8 with 1M context",
|
|
139
|
+
sonnet: "Sonnet 5",
|
|
140
|
+
"sonnet[1m]": "Sonnet 5 with 1M context",
|
|
141
|
+
haiku: "Haiku 4.5",
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Story 072 — compose the Zed selector description: "<version label> · <tagline>", or the bare tagline
|
|
145
|
+
* when no label exists (e.g. `opusplan`). PURE + TOTAL: never throws on a missing label or tagline.
|
|
146
|
+
*/
|
|
147
|
+
export function modelSelectorDescription(info) {
|
|
148
|
+
const label = MODEL_VERSION_LABELS[info.value];
|
|
149
|
+
const tagline = info.description ?? "";
|
|
150
|
+
if (!label)
|
|
151
|
+
return tagline;
|
|
152
|
+
return tagline ? `${label} · ${tagline}` : label;
|
|
153
|
+
}
|
|
44
154
|
/** The safe fallback entry, kept as a named export so callers can seed/anchor on it without a lookup. */
|
|
45
155
|
export const DEFAULT_MODEL_INFO = MODEL_CATALOG[0];
|
|
@@ -64,4 +64,3 @@ export interface ClearNativePromptOptions {
|
|
|
64
64
|
* @returns an {@link InjectResult}: `'suppressed'` | `'cleared'` | `'stuck'`.
|
|
65
65
|
*/
|
|
66
66
|
export declare function clearNativePrompt(opts: ClearNativePromptOptions): Promise<InjectResult>;
|
|
67
|
-
//# sourceMappingURL=allow-inject.d.ts.map
|
|
@@ -44,6 +44,18 @@ export declare function denyDecision(toolCall: DenyToolCall, reason?: string): H
|
|
|
44
44
|
* @returns `{ hookSpecificOutput: { hookEventName:'PreToolUse', permissionDecision:'allow', … } }`.
|
|
45
45
|
*/
|
|
46
46
|
export declare function allowDecision(toolCall: DenyToolCall, reason?: string): HookResponse;
|
|
47
|
+
/** Story 064 — the interactive tool the gate denies fail-closed over the Zed bridge (R1). */
|
|
48
|
+
export declare const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
|
|
49
|
+
/** True iff `toolName` is the AskUserQuestion interactive picker tool (exact, case-sensitive). */
|
|
50
|
+
export declare function isAskUserQuestionTool(toolName: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Story 064 (R1.1) — the deny reason for AskUserQuestion over the Zed bridge. AskUserQuestion renders
|
|
53
|
+
* an interactive multiple-choice picker bound to the hidden PTY's stdin; the Zed user can neither see
|
|
54
|
+
* nor answer it, so allowing it would stall the turn until the watchdog fires. Deny it fail-closed with
|
|
55
|
+
* this reason so the model proceeds without it (interim until story 065 relays it via ACP elicitation).
|
|
56
|
+
* MUST name the tool and cite the bridge limitation (the body matches `/AskUserQuestion/` and `/bridge/i`).
|
|
57
|
+
*/
|
|
58
|
+
export declare function askUserQuestionDenyReason(): string;
|
|
47
59
|
/**
|
|
48
60
|
* Matcher-scoped deny predicate (R2.3 NUANCE): does this deny matcher target the given tool?
|
|
49
61
|
*
|
|
@@ -57,4 +69,3 @@ export declare function allowDecision(toolCall: DenyToolCall, reason?: string):
|
|
|
57
69
|
* @returns true iff the matcher scopes the deny to this tool.
|
|
58
70
|
*/
|
|
59
71
|
export declare function denyMatchesTool(matcher: string, toolName: string): boolean;
|
|
60
|
-
//# sourceMappingURL=deny.d.ts.map
|
package/dist/permissions/deny.js
CHANGED
|
@@ -64,6 +64,24 @@ export function allowDecision(toolCall, reason) {
|
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
/** Story 064 — the interactive tool the gate denies fail-closed over the Zed bridge (R1). */
|
|
68
|
+
export const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
|
|
69
|
+
/** True iff `toolName` is the AskUserQuestion interactive picker tool (exact, case-sensitive). */
|
|
70
|
+
export function isAskUserQuestionTool(toolName) {
|
|
71
|
+
return toolName === ASK_USER_QUESTION_TOOL;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Story 064 (R1.1) — the deny reason for AskUserQuestion over the Zed bridge. AskUserQuestion renders
|
|
75
|
+
* an interactive multiple-choice picker bound to the hidden PTY's stdin; the Zed user can neither see
|
|
76
|
+
* nor answer it, so allowing it would stall the turn until the watchdog fires. Deny it fail-closed with
|
|
77
|
+
* this reason so the model proceeds without it (interim until story 065 relays it via ACP elicitation).
|
|
78
|
+
* MUST name the tool and cite the bridge limitation (the body matches `/AskUserQuestion/` and `/bridge/i`).
|
|
79
|
+
*/
|
|
80
|
+
export function askUserQuestionDenyReason() {
|
|
81
|
+
return (`${ASK_USER_QUESTION_TOOL} is not supported over the Zed bridge: its interactive multiple-choice ` +
|
|
82
|
+
`picker renders in a hidden PTY the user cannot see or answer, so the turn would stall. Denying it ` +
|
|
83
|
+
`fail-closed — continue without asking an interactive question.`);
|
|
84
|
+
}
|
|
67
85
|
/**
|
|
68
86
|
* Matcher-scoped deny predicate (R2.3 NUANCE): does this deny matcher target the given tool?
|
|
69
87
|
*
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CreateElicitationRequest, CreateElicitationResponse } from "@agentclientprotocol/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* The gate decision this bridge yields. Mirrors hook-server's `DenyWithReason`: AskUserQuestion is
|
|
4
|
+
* ALWAYS denied at the wire, and the user's selection (or the dismissal note) is carried in `reason`.
|
|
5
|
+
*/
|
|
6
|
+
export interface ElicitationDecision {
|
|
7
|
+
decision: "deny";
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
/** The `AskUserQuestion` tool_input subset the bridge projects into a form elicitation. */
|
|
11
|
+
export interface AskUserQuestionInput {
|
|
12
|
+
questions: Array<{
|
|
13
|
+
/** The human-readable question text — becomes the property `title`. */
|
|
14
|
+
question: string;
|
|
15
|
+
/** The short key — becomes the property name AND a `required` entry. */
|
|
16
|
+
header: string;
|
|
17
|
+
/** Whether multiple options may be selected (currently projected as a single string-enum). */
|
|
18
|
+
multiSelect?: boolean;
|
|
19
|
+
options: Array<{
|
|
20
|
+
label: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The minimal ACP client surface the bridge needs — structurally satisfied by the kept
|
|
27
|
+
* `AgentSideConnection` (`client.unstable_createElicitation(params)`). Injected so a unit test drives
|
|
28
|
+
* the bridge with a fake client OFFLINE (no AgentSideConnection, no Zed).
|
|
29
|
+
*/
|
|
30
|
+
export interface ElicitationClient {
|
|
31
|
+
unstable_createElicitation(params: CreateElicitationRequest): Promise<CreateElicitationResponse>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Project an `AskUserQuestion` tool_input into an ACP form `CreateElicitationRequest`, scoped to the
|
|
35
|
+
* session and tied to the originating tool call by `tool_use.id` (R1.1, R1.2).
|
|
36
|
+
*
|
|
37
|
+
* Each question becomes one string-enum property keyed by its `header`: `{ type: "string", title:
|
|
38
|
+
* <question>, enum: <option labels, in order> }`, and every header is marked `required` (one entry per
|
|
39
|
+
* question, in order). The returned literal is shaped to satisfy the pinned SDK `zCreateElicitationRequest`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildElicitationRequest(toolUseId: string, sessionId: string, toolInput: AskUserQuestionInput): CreateElicitationRequest;
|
|
42
|
+
/**
|
|
43
|
+
* Map an elicitation outcome back to a gate decision (R2). EVERY branch returns a `deny`: the gate
|
|
44
|
+
* always denies AskUserQuestion at the wire and surfaces the result through `reason` (the R2.1 seam — a
|
|
45
|
+
* PreToolUse hook cannot synthesize a native tool_result).
|
|
46
|
+
*
|
|
47
|
+
* - `accept`: the reason embeds EVERY selected answer (e.g. `Env=prod, Cache=yes`) so the model can
|
|
48
|
+
* read the choice from the deny reason. `content` null/undefined is handled defensively (still deny).
|
|
49
|
+
* - `decline`/`cancel`/any unrecognized-or-missing action: the reason reads as a user DISMISSAL, not
|
|
50
|
+
* an answer (default-deny defensively for unknown actions).
|
|
51
|
+
*/
|
|
52
|
+
export declare function mapOutcomeToDecision(resp: CreateElicitationResponse): ElicitationDecision;
|
|
53
|
+
/** Options for {@link requestElicitation}. */
|
|
54
|
+
export interface RequestElicitationOptions {
|
|
55
|
+
/** The hard upper bound (ms) on the round-trip; a slower client fails closed to a cancel. */
|
|
56
|
+
timeoutMs: number;
|
|
57
|
+
/** Optional sink for the fail-closed diagnostics (defaults to no-op; production wires the logger). */
|
|
58
|
+
onWarn?: (message: string) => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Perform the bounded, fail-closed elicitation round-trip (R4). NEVER throws and NEVER hangs past
|
|
62
|
+
* ~`timeoutMs`.
|
|
63
|
+
*
|
|
64
|
+
* - Happy path (a well-formed response, action ∈ {accept,decline,cancel}) → returned UNCHANGED.
|
|
65
|
+
* - Timeout → `onWarn` (naming the timeout) and resolve `{ action: "cancel" }`.
|
|
66
|
+
* - Client throws → `onWarn` (including the underlying error text) and resolve `{ action: "cancel" }`.
|
|
67
|
+
* - Malformed response (action not accept|decline|cancel) → normalized to `{ action: "cancel" }`.
|
|
68
|
+
*
|
|
69
|
+
* The timer is always cleared on settle so no live handle keeps the event loop open.
|
|
70
|
+
*/
|
|
71
|
+
export declare function requestElicitation(client: ElicitationClient, req: CreateElicitationRequest, opts: RequestElicitationOptions): Promise<CreateElicitationResponse>;
|