@sjawhar/claude-envoy 1.18.0 → 1.19.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/.claude-plugin/plugin.json +2 -2
- package/.omp-plugin/plugin.json +2 -2
- package/AGENTS.md +167 -42
- package/README.md +95 -177
- package/dist/dispatch.js +54 -6
- package/dist/envoy-channel.js +2413 -875
- package/dist/session-hook.js +54 -6
- package/hooks/claude-mod.d.ts +500 -0
- package/hooks/hooks.json +2 -1
- package/hooks/mod-protocol.ts +728 -0
- package/hooks/mod-writer.ts +506 -0
- package/hooks/mod.ts +716 -0
- package/package.json +2 -2
- package/scripts/smoke-clear-rebind.sh +1 -1
- package/scripts/smoke-session-title.sh +1 -1
- package/src/agent-stream-claude.ts +558 -0
- package/src/agent-stream-runner.ts +340 -0
- package/src/bounded-set.ts +54 -0
- package/src/channel-forwarder.ts +1 -1
- package/src/claude-rows.ts +195 -0
- package/src/envoy-channel-server.ts +394 -136
- package/src/envoy-off.ts +6 -1
- package/src/installed-build.ts +26 -0
- package/src/legion-inbox.ts +22 -11
- package/src/mod-link.ts +562 -0
- package/src/mod-session.ts +499 -0
- package/src/replay-state.ts +2 -18
- package/src/state-file.ts +39 -0
- package/src/user-turn-router.ts +496 -0
- package/src/user-turn-state.ts +134 -0
- package/scripts/smoke-channel.sh +0 -178
- package/src/claude-channel.ts +0 -705
package/dist/session-hook.js
CHANGED
|
@@ -13922,6 +13922,16 @@ function actorLabel(actor, titles) {
|
|
|
13922
13922
|
return `${actorName(actor, titles)}${actorAttribution(actor)}`;
|
|
13923
13923
|
}
|
|
13924
13924
|
// ../contracts/src/agent-stream.ts
|
|
13925
|
+
var AGENT_STREAM_SUBJECT_PREFIX = "agentstream.";
|
|
13926
|
+
var AGENT_STREAM_PROTOCOL = 1;
|
|
13927
|
+
function agentStreamFramesSubject(sessionID) {
|
|
13928
|
+
return `${AGENT_STREAM_SUBJECT_PREFIX}${sessionID}.frames`;
|
|
13929
|
+
}
|
|
13930
|
+
function agentStreamControlSubject(sessionID) {
|
|
13931
|
+
return `${AGENT_STREAM_SUBJECT_PREFIX}${sessionID}.control`;
|
|
13932
|
+
}
|
|
13933
|
+
var AGENT_STREAM_WATCH_TTL_MS = 30000;
|
|
13934
|
+
var AGENT_STREAM_WATCH_INTERVAL_MS = 1e4;
|
|
13925
13935
|
var AGENT_STREAM_LIMITS = {
|
|
13926
13936
|
partChars: 16000,
|
|
13927
13937
|
toolChars: 8000,
|
|
@@ -13933,6 +13943,16 @@ var AGENT_STREAM_LIMITS = {
|
|
|
13933
13943
|
commandNameChars: 100,
|
|
13934
13944
|
commandDescriptionChars: 200
|
|
13935
13945
|
};
|
|
13946
|
+
var AGENT_STREAM_TRUNCATION_SUFFIX = `
|
|
13947
|
+
\u2026 truncated`;
|
|
13948
|
+
function isTypableCommandName(name) {
|
|
13949
|
+
return /^[^\s\p{Cc}]+$/u.test(name);
|
|
13950
|
+
}
|
|
13951
|
+
function capAgentStreamText(text, limit) {
|
|
13952
|
+
if (text.length <= limit)
|
|
13953
|
+
return text;
|
|
13954
|
+
return text.slice(0, limit) + AGENT_STREAM_TRUNCATION_SUFFIX;
|
|
13955
|
+
}
|
|
13936
13956
|
// ../contracts/src/claim-holds.ts
|
|
13937
13957
|
var HOLDING_OF = {
|
|
13938
13958
|
gone: "lapsed",
|
|
@@ -14007,6 +14027,24 @@ function isIdPrefix(value) {
|
|
|
14007
14027
|
function pictureCaption(name) {
|
|
14008
14028
|
return name.replace(/[\\[\]`]/g, "\\$&").replace(/\s+/g, " ");
|
|
14009
14029
|
}
|
|
14030
|
+
var PICTURE_CAPTION = String.raw`(?:\\.|[^\\\]\n]){0,512}`;
|
|
14031
|
+
var PICTURE_ADDRESS = String.raw`dispatch://[^\s()]{1,2048}@v[1-9][0-9]{0,18}`;
|
|
14032
|
+
var PICTURE_TITLE = String.raw`(?:\s{1,4}"(?:\\.|[^\\"]){0,256}")?`;
|
|
14033
|
+
var PICTURE_SYNTAX = String.raw`!\[${PICTURE_CAPTION}\]\((${PICTURE_ADDRESS})${PICTURE_TITLE}\)`;
|
|
14034
|
+
function pictureSyntax() {
|
|
14035
|
+
return new RegExp(PICTURE_SYNTAX, "g");
|
|
14036
|
+
}
|
|
14037
|
+
function blockPlaceholder(type) {
|
|
14038
|
+
return `[${type}]`;
|
|
14039
|
+
}
|
|
14040
|
+
var PICTURE_PLACEHOLDER = blockPlaceholder("image");
|
|
14041
|
+
function trimmedLines(text) {
|
|
14042
|
+
return text.split(`
|
|
14043
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
14044
|
+
}
|
|
14045
|
+
function comparableLines(text) {
|
|
14046
|
+
return trimmedLines(text.replace(pictureSyntax(), ""));
|
|
14047
|
+
}
|
|
14010
14048
|
var PICTURE_TYPES = {
|
|
14011
14049
|
"image/png": true,
|
|
14012
14050
|
"image/jpeg": true,
|
|
@@ -17218,9 +17256,9 @@ class DispatchClient {
|
|
|
17218
17256
|
async messageReply(id, input, options = {}) {
|
|
17219
17257
|
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
17220
17258
|
}
|
|
17221
|
-
async deliveryReply(resource, id, input) {
|
|
17259
|
+
async deliveryReply(resource, id, input, options = {}) {
|
|
17222
17260
|
const collection = resource === "message" ? "messages" : "comments";
|
|
17223
|
-
await this.#json("POST", ["api", "v1", collection, id, "reply"], input);
|
|
17261
|
+
await this.#json("POST", ["api", "v1", collection, id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
17224
17262
|
}
|
|
17225
17263
|
async getMessage(issue, id) {
|
|
17226
17264
|
return this.#json("GET", [
|
|
@@ -18266,6 +18304,9 @@ function toolImage(bytes) {
|
|
|
18266
18304
|
return;
|
|
18267
18305
|
return { data: Buffer.from(bytes).toString("base64"), mimeType };
|
|
18268
18306
|
}
|
|
18307
|
+
function imageBlocks(images) {
|
|
18308
|
+
return images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }));
|
|
18309
|
+
}
|
|
18269
18310
|
function pictureLine(name, address) {
|
|
18270
18311
|
return ``;
|
|
18271
18312
|
}
|
|
@@ -18279,9 +18320,8 @@ function withPictureLines(text, lines) {
|
|
|
18279
18320
|
${lines.join(`
|
|
18280
18321
|
`)}`;
|
|
18281
18322
|
}
|
|
18282
|
-
var PICTURE_SYNTAX = /!\[(?:\\.|[^\\\]])*\]\((dispatch:\/\/[^\s()]+@v[1-9][0-9]*)(?:\s+"(?:\\.|[^\\"])*")?\)/g;
|
|
18283
18323
|
function pictureAddresses(text) {
|
|
18284
|
-
return [...text.matchAll(
|
|
18324
|
+
return [...text.matchAll(pictureSyntax())].map((match) => match[1]);
|
|
18285
18325
|
}
|
|
18286
18326
|
function picturesNewestFirst(texts) {
|
|
18287
18327
|
const ordered = texts.map((item, index) => ({ item, index, at: Date.parse(item.at) })).sort((left, right) => (Number.isNaN(right.at) ? 0 : right.at) - (Number.isNaN(left.at) ? 0 : left.at) || left.index - right.index);
|
|
@@ -18374,13 +18414,14 @@ function dispatchReplyPath(resource, id) {
|
|
|
18374
18414
|
return `/api/v1/${collection}/${encodeURIComponent(id)}/reply`;
|
|
18375
18415
|
}
|
|
18376
18416
|
async function postDeliveryReply(config, sessionId, delivery, result) {
|
|
18417
|
+
const { followUp, ...reply } = result;
|
|
18377
18418
|
const client = new DispatchClient(config.url, config.credential);
|
|
18378
18419
|
await client.deliveryReply(delivery.resource, delivery.id, {
|
|
18379
18420
|
actor: { kind: "session", id: sessionId },
|
|
18380
18421
|
attempt: delivery.attempt,
|
|
18381
18422
|
...delivery.replyFields,
|
|
18382
|
-
...
|
|
18383
|
-
});
|
|
18423
|
+
...reply
|
|
18424
|
+
}, followUp === undefined ? {} : { followUp });
|
|
18384
18425
|
}
|
|
18385
18426
|
function expectsLaneReceipt(frame) {
|
|
18386
18427
|
return frame.reply !== undefined && frame.reply !== "" && frame.subject === frame.directSubject && frame.envelopeTopic !== undefined && frame.envelopeTopic !== frame.directSubject;
|
|
@@ -26444,6 +26485,13 @@ function dispatchTarget(env, config) {
|
|
|
26444
26485
|
const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
|
|
26445
26486
|
return { enabled: true, url, source };
|
|
26446
26487
|
}
|
|
26488
|
+
function activeDispatchSettings(resolution) {
|
|
26489
|
+
if (resolution.enabled)
|
|
26490
|
+
return resolution;
|
|
26491
|
+
if (resolution.error !== null)
|
|
26492
|
+
throw new Error(`dispatch config: ${resolution.error}`);
|
|
26493
|
+
return null;
|
|
26494
|
+
}
|
|
26447
26495
|
|
|
26448
26496
|
// ../envoy-client/src/envoy-settings.ts
|
|
26449
26497
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The part of Claude Code's mod API that `./mod.ts` uses, hand-written from the declarations
|
|
3
|
+
* Claude Code 2.1.310 ships (`@anthropic-ai/claude-code`'s `index.d.ts`, 17k lines), measured on
|
|
4
|
+
* that release.
|
|
5
|
+
*
|
|
6
|
+
* Hand-written on purpose: taking the package as a devDependency for those 17k lines would move
|
|
7
|
+
* `bun.lock`, and `bun.lock` is an input of claude-envoy's build (`scripts/build-inputs.txt`), so
|
|
8
|
+
* every change to it rebuilds and re-commits `dist/` and `legion/`. The mod ships from source and
|
|
9
|
+
* imports nothing of the package at run time, so nothing but type-checking needs it.
|
|
10
|
+
*
|
|
11
|
+
* What is here is what the mod uses, copied faithfully, with two reductions, both safe because
|
|
12
|
+
* the mod only reads or relays the values:
|
|
13
|
+
* - a type the mod receives and passes on whole (`TurnStepResult`) declares the fields the mod
|
|
14
|
+
* reads, not every field Claude Code puts on it;
|
|
15
|
+
* - a string union the mod never branches on (`ModelForkResult`'s `reason`) is declared `string`.
|
|
16
|
+
* `on(...)` really answers a `Registration` whose `catch` sets a handler; the mod sets none, so
|
|
17
|
+
* the overloads here answer `void`.
|
|
18
|
+
*
|
|
19
|
+
* When this plugin is built against a newer Claude Code, check this file against that release's
|
|
20
|
+
* declarations: nothing else does.
|
|
21
|
+
*/
|
|
22
|
+
declare module "claude-code" {
|
|
23
|
+
/** One block of a message in Messages API form (`ApiContentBlock`). */
|
|
24
|
+
export type ApiContentBlock = {
|
|
25
|
+
type: string
|
|
26
|
+
[field: string]: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The version of the engine the session runs on (`SessionVersion`). */
|
|
30
|
+
export type SessionVersion = {
|
|
31
|
+
/** The engine's full version, as `claude --version` prints it. */
|
|
32
|
+
version: string
|
|
33
|
+
/** The release the version is built from; absent when it is not spelled as one. */
|
|
34
|
+
base?: string
|
|
35
|
+
/** When the binary was built, ISO 8601; absent in a run from source that stamps none. */
|
|
36
|
+
builtAt?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The input of `session.start`: the session as `$.session` reads it at that moment. */
|
|
40
|
+
export type SessionStartInput = {
|
|
41
|
+
/** The directory the session runs in, absolute. */
|
|
42
|
+
readonly cwd: string
|
|
43
|
+
/** Whether a person is at the prompt: true under the REPL, false for `-p` or the SDK. */
|
|
44
|
+
readonly isInteractive: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What `session.start`'s `next(e)` resolves to; a hook's own value changes nothing. */
|
|
48
|
+
export type SessionStartResult = {
|
|
49
|
+
cwd: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Why the session ended, the classic SessionEnd hook's own word. */
|
|
53
|
+
export type SessionEndReason = "clear" | "resume" | "logout" | "prompt_input_exit" | "other"
|
|
54
|
+
|
|
55
|
+
/** The input of `session.end`. */
|
|
56
|
+
export type SessionEndInput = {
|
|
57
|
+
readonly reason: SessionEndReason
|
|
58
|
+
/** The ending session's id; after a `/clear` or a resume the process goes on under another. */
|
|
59
|
+
readonly sessionId: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** What `session.end`'s `next(e)` resolves to. */
|
|
63
|
+
export type SessionEndResult = {
|
|
64
|
+
sessionId: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The input of `turn.start`: the prompt a model turn begins with. */
|
|
68
|
+
export type TurnStartInput = {
|
|
69
|
+
readonly text: string
|
|
70
|
+
/** The turn's id, minted here; every `turn.step` and the `turn.complete` carry it. */
|
|
71
|
+
readonly turnId: string
|
|
72
|
+
/** The loop the turn runs in: a subagent's id; absent on the main loop. */
|
|
73
|
+
readonly agentId?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What `turn.start`'s `next(e)` resolves to. */
|
|
77
|
+
export type TurnStartResult = {
|
|
78
|
+
turnId: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Why a turn ended. */
|
|
82
|
+
export type TurnCompleteReason = "answer" | "aborted" | "refusal" | "error"
|
|
83
|
+
|
|
84
|
+
/** The input of `turn.complete`: the assistant's final message of a turn. */
|
|
85
|
+
export type TurnCompleteInput = {
|
|
86
|
+
readonly answer: string
|
|
87
|
+
readonly durationMs: number
|
|
88
|
+
readonly isAborted: boolean
|
|
89
|
+
readonly turnId: string
|
|
90
|
+
readonly reason: TurnCompleteReason
|
|
91
|
+
/** The loop the turn ran in: a subagent's id; absent on the main loop. */
|
|
92
|
+
readonly agentId?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** What `turn.complete`'s `next(e)` resolves to. */
|
|
96
|
+
export type TurnCompleteResult = {
|
|
97
|
+
text: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** How hard a request asks the model to think; absent for a model without effort. */
|
|
101
|
+
export type TurnEffort = "low" | "medium" | "high" | "xhigh" | "max" | number
|
|
102
|
+
|
|
103
|
+
/** The input of `turn.step`: one model request inside a turn. A hook rewrites `model` or
|
|
104
|
+
* `effort` going down; the rest is pinned. */
|
|
105
|
+
export type TurnStepInput = {
|
|
106
|
+
readonly turnId: string
|
|
107
|
+
/** The step's position in the turn, from 0. */
|
|
108
|
+
readonly index: number
|
|
109
|
+
readonly model: string
|
|
110
|
+
readonly effort?: TurnEffort
|
|
111
|
+
readonly messageCount: number
|
|
112
|
+
/** The loop the request is made in: a subagent's id; absent on main. */
|
|
113
|
+
readonly agentId?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The engine's handle on its own streamed item; opaque, one step's only. */
|
|
117
|
+
export type ChunkRef = {
|
|
118
|
+
readonly ref?: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A piece of the response's visible text, in block `index`. */
|
|
122
|
+
export type TurnStepTextChunk = ChunkRef & {
|
|
123
|
+
readonly kind: "text"
|
|
124
|
+
readonly index: number
|
|
125
|
+
readonly text: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A piece of the model's thinking, in block `index`. */
|
|
129
|
+
export type TurnStepThinkingChunk = ChunkRef & {
|
|
130
|
+
readonly kind: "thinking"
|
|
131
|
+
readonly index: number
|
|
132
|
+
readonly text: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The model begins a tool call in block `index`; its arguments follow as `input` chunks. */
|
|
136
|
+
export type TurnStepToolChunk = ChunkRef & {
|
|
137
|
+
readonly kind: "tool"
|
|
138
|
+
readonly index: number
|
|
139
|
+
/** The call's `tool_use_id`. */
|
|
140
|
+
readonly id: string
|
|
141
|
+
/** The tool's name (`Read`, `Bash`, `mcp__server__tool`). */
|
|
142
|
+
readonly name: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A piece of a tool call's arguments as they arrive: JSON text, partial. */
|
|
146
|
+
export type TurnStepInputChunk = ChunkRef & {
|
|
147
|
+
readonly kind: "input"
|
|
148
|
+
readonly index: number
|
|
149
|
+
readonly json: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Why the model stopped; null when no response arrived. */
|
|
153
|
+
export type TurnStopReason =
|
|
154
|
+
| "end_turn"
|
|
155
|
+
| "max_tokens"
|
|
156
|
+
| "stop_sequence"
|
|
157
|
+
| "tool_use"
|
|
158
|
+
| "pause_turn"
|
|
159
|
+
| "compaction"
|
|
160
|
+
| "refusal"
|
|
161
|
+
| "model_context_window_exceeded"
|
|
162
|
+
| null
|
|
163
|
+
|
|
164
|
+
/** The response is whole: the last chunk of one response. */
|
|
165
|
+
export type TurnStepStopChunk = ChunkRef & {
|
|
166
|
+
readonly kind: "stop"
|
|
167
|
+
readonly stopReason: TurnStopReason
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** An item of the engine's stream the other chunk kinds do not model; pass it on unread. */
|
|
171
|
+
export type TurnStepEngineChunk = {
|
|
172
|
+
readonly kind: "engine"
|
|
173
|
+
readonly ref: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One piece of a `turn.step` response as it streams through the chain. */
|
|
177
|
+
export type TurnStepChunk =
|
|
178
|
+
| TurnStepTextChunk
|
|
179
|
+
| TurnStepThinkingChunk
|
|
180
|
+
| TurnStepToolChunk
|
|
181
|
+
| TurnStepInputChunk
|
|
182
|
+
| TurnStepStopChunk
|
|
183
|
+
| TurnStepEngineChunk
|
|
184
|
+
|
|
185
|
+
/** What a `turn.step` resolves to, in the fields the mod reads: the rest of the object is
|
|
186
|
+
* relayed whole and never looked at. */
|
|
187
|
+
export type TurnStepResult = {
|
|
188
|
+
readonly turnId: string
|
|
189
|
+
readonly index: number
|
|
190
|
+
readonly stopReason: TurnStopReason
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A stream a hook's `next(e)` answers on a streaming event. */
|
|
194
|
+
export type HookStream<C, R> = AsyncGenerator<C, R> & {
|
|
195
|
+
/** Settles with what beneath returned once the stream has been read to its end. */
|
|
196
|
+
readonly result: Promise<R>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Which door a row came in by; a closed set, pinned on the event. */
|
|
200
|
+
export type SessionAppendDoor =
|
|
201
|
+
| "prompt"
|
|
202
|
+
| "command"
|
|
203
|
+
| "response"
|
|
204
|
+
| "tool-result"
|
|
205
|
+
| "tool-message"
|
|
206
|
+
| "delivery"
|
|
207
|
+
| "attachment"
|
|
208
|
+
| "hook-context"
|
|
209
|
+
| "note"
|
|
210
|
+
| "compaction"
|
|
211
|
+
| "notice"
|
|
212
|
+
| "queue"
|
|
213
|
+
|
|
214
|
+
/** One row of a conversation as `session.append` hands it. */
|
|
215
|
+
export type SessionAppendMessage = {
|
|
216
|
+
readonly type: "user" | "assistant" | "attachment" | "system"
|
|
217
|
+
/** An attachment's type or a notice's subtype; absent on user and assistant rows. */
|
|
218
|
+
readonly name?: string
|
|
219
|
+
/** Under which role a request carries the row; absent when none does. */
|
|
220
|
+
readonly role?: "user" | "assistant"
|
|
221
|
+
/** True on a user-side row the person does not see as typed. */
|
|
222
|
+
readonly isMeta?: true
|
|
223
|
+
readonly content: readonly ApiContentBlock[]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Who caused a row: the person, the model, a tool, the engine, a settings hook, a plugin.
|
|
227
|
+
* Claude Code spells it as a union of one arm per kind, each with the field that kind carries;
|
|
228
|
+
* the mod relays the kind and those fields, and branches on none. */
|
|
229
|
+
export type SessionAppendOrigin = {
|
|
230
|
+
readonly kind: string
|
|
231
|
+
readonly name?: string
|
|
232
|
+
readonly model?: string
|
|
233
|
+
readonly tool?: string
|
|
234
|
+
readonly event?: string
|
|
235
|
+
readonly server?: string
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The input of `session.append`: one row being appended to a conversation of this session. */
|
|
239
|
+
export type SessionAppendInput = {
|
|
240
|
+
readonly message: SessionAppendMessage
|
|
241
|
+
readonly door: SessionAppendDoor
|
|
242
|
+
readonly origin: SessionAppendOrigin
|
|
243
|
+
/** The row's id, the same in the transcript file and on every later read. */
|
|
244
|
+
readonly uuid: string
|
|
245
|
+
/** The loop whose conversation keeps the row: a subagent's id; absent on main. */
|
|
246
|
+
readonly agentId?: string
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The row as stored, or the refusal of a plugin's own append. */
|
|
250
|
+
export type SessionAppendResult =
|
|
251
|
+
| { readonly message: SessionAppendMessage; readonly uuid: string; readonly deny?: undefined }
|
|
252
|
+
| { readonly deny: string; readonly message?: undefined; readonly uuid?: undefined }
|
|
253
|
+
|
|
254
|
+
/** The argument of `$.session.append`: a user row the model reads, or a system notice. */
|
|
255
|
+
export type SessionAppendArgs = {
|
|
256
|
+
readonly message: {
|
|
257
|
+
readonly type: "user" | "system"
|
|
258
|
+
readonly content: readonly ApiContentBlock[]
|
|
259
|
+
}
|
|
260
|
+
/** The running subagent whose conversation the row joins; absent for main. */
|
|
261
|
+
readonly agentId?: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The argument of `$.prompt.submit`. */
|
|
265
|
+
export type PromptSubmitArgs = {
|
|
266
|
+
readonly text: string
|
|
267
|
+
/** Submit the text as the person's own words, read without the plugin's frame. */
|
|
268
|
+
readonly asUser?: true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** What `$.prompt.submit` resolves to: the prompt that entered, or why it did not. */
|
|
272
|
+
export type PromptSubmitResult =
|
|
273
|
+
| { readonly text: string; readonly drop?: undefined }
|
|
274
|
+
| { readonly drop: string; readonly text?: undefined }
|
|
275
|
+
|
|
276
|
+
/** The argument of `$.command.run`. */
|
|
277
|
+
export type CommandRunArgs = {
|
|
278
|
+
readonly command: string
|
|
279
|
+
/** Everything after the name, as the person would type it; left out, `""`. */
|
|
280
|
+
readonly args?: string
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** What `$.command.run` resolves to. */
|
|
284
|
+
export type CommandRunResult = {
|
|
285
|
+
/** The command's output as a transcript line; absent when it showed nothing as text. */
|
|
286
|
+
readonly text?: string
|
|
287
|
+
/** What the model reads after the output and the person never sees. */
|
|
288
|
+
readonly context?: readonly string[]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Where a command comes from. */
|
|
292
|
+
export type CommandSource = "builtin" | "plugin" | "user" | "mcp"
|
|
293
|
+
|
|
294
|
+
/** One slash command as `$.command.list()` answers it. */
|
|
295
|
+
export type CommandInfo = {
|
|
296
|
+
readonly name: string
|
|
297
|
+
readonly description: string
|
|
298
|
+
readonly source: CommandSource
|
|
299
|
+
/** Which plugin added it, when `source` is `plugin` and the engine knows. */
|
|
300
|
+
readonly plugin?: string
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** A `/config` row's value. */
|
|
304
|
+
export type ConfigValue = boolean | string | number | readonly string[]
|
|
305
|
+
|
|
306
|
+
/** How a `/config` row takes its value. */
|
|
307
|
+
export type ConfigKind = "boolean" | "choice" | "text" | "number"
|
|
308
|
+
|
|
309
|
+
/** Where a hook's plugin sits among the chain's tiers. */
|
|
310
|
+
export type Tier = "prepend" | "user" | "append" | "builtin" | "core"
|
|
311
|
+
|
|
312
|
+
/** Who owns a row or made a call. */
|
|
313
|
+
export type Origin = {
|
|
314
|
+
readonly plugin: string
|
|
315
|
+
readonly tier: Tier
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** One `/config` row as `$.config.list()` answers it. */
|
|
319
|
+
export type ConfigRow = {
|
|
320
|
+
readonly key: string
|
|
321
|
+
readonly label: string
|
|
322
|
+
readonly description?: string
|
|
323
|
+
readonly kind: ConfigKind
|
|
324
|
+
readonly value: ConfigValue
|
|
325
|
+
/** The values a `choice` row takes, in order; absent otherwise. */
|
|
326
|
+
readonly options?: readonly string[]
|
|
327
|
+
readonly provider: Origin
|
|
328
|
+
/** Whether a trusted source owns the value, so the menu refuses a change. */
|
|
329
|
+
readonly isLocked: boolean
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The argument of `$.config.set`. */
|
|
333
|
+
export type ConfigSetArgs = {
|
|
334
|
+
readonly key: string
|
|
335
|
+
readonly value: ConfigValue
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** What `$.config.set` resolves to: the value once written, or why not. */
|
|
339
|
+
export type ConfigSetResult =
|
|
340
|
+
| { readonly value: ConfigValue; readonly deny?: undefined }
|
|
341
|
+
| { readonly deny: string; readonly value?: undefined }
|
|
342
|
+
|
|
343
|
+
/** The argument of `$.model.fork`. */
|
|
344
|
+
export type ModelForkRequest = {
|
|
345
|
+
/** The one user message, appended to the session's own transcript as the main thread last
|
|
346
|
+
* sent it. */
|
|
347
|
+
readonly prompt: string
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** What `$.model.fork` resolves to. `reason` is the arm's own word (`api-error`,
|
|
351
|
+
* `empty-reply`, `aborted`, `nothing-to-fork`), which the mod relays and never branches on. */
|
|
352
|
+
export type ModelForkResult =
|
|
353
|
+
| { readonly isAnswered: true; readonly text: string }
|
|
354
|
+
| { readonly isAnswered: false; readonly reason: string }
|
|
355
|
+
|
|
356
|
+
/** Options of `$.http.fetch`. */
|
|
357
|
+
export type HttpInit = {
|
|
358
|
+
/** `GET` (default), `POST`, ... */
|
|
359
|
+
method?: string
|
|
360
|
+
/** Request headers. */
|
|
361
|
+
headers?: Record<string, string>
|
|
362
|
+
/** The request body, as text. */
|
|
363
|
+
body?: string
|
|
364
|
+
/** The absolute path of a Unix domain socket the request goes over instead of TCP (near
|
|
365
|
+
* 100 B at most): one per session, in a private directory. The URL still gives path, query
|
|
366
|
+
* and Host. No relative path, no NUL. */
|
|
367
|
+
socketPath?: string
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** What `$.http.fetch` resolves with. */
|
|
371
|
+
export type HttpResponse = {
|
|
372
|
+
readonly status: number
|
|
373
|
+
/** True for a 2xx status. */
|
|
374
|
+
readonly ok: boolean
|
|
375
|
+
/** Response headers, lower-cased names. */
|
|
376
|
+
readonly headers: Record<string, string>
|
|
377
|
+
/** The body, as text. */
|
|
378
|
+
readonly text: string
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Options of `$.clock.sleep`. */
|
|
382
|
+
export type SleepOptions = {
|
|
383
|
+
/** Ends the wait early with a rejection. */
|
|
384
|
+
signal?: AbortSignal
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The engine as a hook receives it, in the nouns the mod calls. */
|
|
388
|
+
export interface EngineInterface {
|
|
389
|
+
readonly session: {
|
|
390
|
+
/** The session's id, as it stands now: a `/clear` moves it. */
|
|
391
|
+
id: () => Promise<string>
|
|
392
|
+
/** The version of the engine the session runs on. */
|
|
393
|
+
version: () => Promise<SessionVersion>
|
|
394
|
+
/** The session's model, by the id the engine resolved. */
|
|
395
|
+
model: () => Promise<string>
|
|
396
|
+
/** Appends one row to a conversation of this session. */
|
|
397
|
+
append: (args: SessionAppendArgs) => Promise<SessionAppendResult>
|
|
398
|
+
}
|
|
399
|
+
readonly prompt: {
|
|
400
|
+
/** Submits a prompt; resolves once its turn ended. */
|
|
401
|
+
submit: (input: PromptSubmitArgs) => Promise<PromptSubmitResult>
|
|
402
|
+
}
|
|
403
|
+
readonly command: {
|
|
404
|
+
list: () => Promise<CommandInfo[]>
|
|
405
|
+
run: (input: CommandRunArgs) => Promise<CommandRunResult>
|
|
406
|
+
}
|
|
407
|
+
readonly config: {
|
|
408
|
+
list: () => Promise<ConfigRow[]>
|
|
409
|
+
set: (input: ConfigSetArgs) => Promise<ConfigSetResult>
|
|
410
|
+
}
|
|
411
|
+
readonly model: {
|
|
412
|
+
fork: (request: ModelForkRequest) => Promise<ModelForkResult>
|
|
413
|
+
}
|
|
414
|
+
readonly http: {
|
|
415
|
+
/** Fetches `url` through the host; `init.socketPath` sends it over a Unix socket. */
|
|
416
|
+
fetch: (url: string, init?: HttpInit) => Promise<HttpResponse>
|
|
417
|
+
}
|
|
418
|
+
readonly env: {
|
|
419
|
+
/** The variable's value, or undefined when it is unset. `name` must be a string
|
|
420
|
+
* literal: `claude plugin validate` lists the names a module reads. */
|
|
421
|
+
get: (name: string) => Promise<string | undefined>
|
|
422
|
+
}
|
|
423
|
+
readonly clock: {
|
|
424
|
+
/** Resolves after `ms` milliseconds; rejects at once when `signal` aborts. */
|
|
425
|
+
sleep: (ms: number, options?: SleepOptions) => Promise<void>
|
|
426
|
+
}
|
|
427
|
+
readonly store: {
|
|
428
|
+
/** The value under `key`, or undefined when it is unset. */
|
|
429
|
+
get: (key: string) => Promise<unknown>
|
|
430
|
+
/** Sets `key` to `value`, which must be JSON data: `get` reads back
|
|
431
|
+
* `JSON.parse(JSON.stringify(value))`. Rejects a function, a cycle, or a store past
|
|
432
|
+
* 4 MiB of JSON text in all. */
|
|
433
|
+
set: (key: string, value: unknown) => Promise<void>
|
|
434
|
+
delete: (key: string) => Promise<void>
|
|
435
|
+
/** Every key set, in insertion order. */
|
|
436
|
+
keys: () => Promise<string[]>
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Registers the hooks the mod uses. Claude Code's own `On` is one generic pair of overloads
|
|
441
|
+
* over every event; these are the events the mod registers, each spelled out. */
|
|
442
|
+
export type On = {
|
|
443
|
+
(
|
|
444
|
+
pattern: "session.start",
|
|
445
|
+
hook: (
|
|
446
|
+
$: EngineInterface,
|
|
447
|
+
e: SessionStartInput,
|
|
448
|
+
next: (e: SessionStartInput) => Promise<SessionStartResult>,
|
|
449
|
+
) => Promise<SessionStartResult>,
|
|
450
|
+
): void
|
|
451
|
+
(
|
|
452
|
+
pattern: "session.end",
|
|
453
|
+
hook: (
|
|
454
|
+
$: EngineInterface,
|
|
455
|
+
e: SessionEndInput,
|
|
456
|
+
next: (e: SessionEndInput) => Promise<SessionEndResult>,
|
|
457
|
+
) => Promise<SessionEndResult>,
|
|
458
|
+
): void
|
|
459
|
+
(
|
|
460
|
+
pattern: "turn.start",
|
|
461
|
+
hook: (
|
|
462
|
+
$: EngineInterface,
|
|
463
|
+
e: TurnStartInput,
|
|
464
|
+
next: (e: TurnStartInput) => Promise<TurnStartResult>,
|
|
465
|
+
) => Promise<TurnStartResult>,
|
|
466
|
+
): void
|
|
467
|
+
(
|
|
468
|
+
pattern: "turn.complete",
|
|
469
|
+
hook: (
|
|
470
|
+
$: EngineInterface,
|
|
471
|
+
e: TurnCompleteInput,
|
|
472
|
+
next: (e: TurnCompleteInput) => Promise<TurnCompleteResult>,
|
|
473
|
+
) => Promise<TurnCompleteResult>,
|
|
474
|
+
): void
|
|
475
|
+
/** Claude Code spells the generator's return `TurnStepResult | void`, which lets a hook
|
|
476
|
+
* return nothing and leave the result beneath standing; `undefined` says the same thing of a
|
|
477
|
+
* generator and is the spelling this package's lint allows. */
|
|
478
|
+
(
|
|
479
|
+
pattern: "turn.step",
|
|
480
|
+
hook: (
|
|
481
|
+
$: EngineInterface,
|
|
482
|
+
e: TurnStepInput,
|
|
483
|
+
next: (e: TurnStepInput) => HookStream<TurnStepChunk, TurnStepResult>,
|
|
484
|
+
) => AsyncGenerator<TurnStepChunk, TurnStepResult | undefined>,
|
|
485
|
+
): void
|
|
486
|
+
(
|
|
487
|
+
pattern: "session.append",
|
|
488
|
+
matcher: { readonly door: SessionAppendDoor },
|
|
489
|
+
hook: SessionAppendHook,
|
|
490
|
+
): void
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** The hook `on("session.append", { door }, hook)` takes, named so one hook can serve every
|
|
494
|
+
* door's registration. */
|
|
495
|
+
export type SessionAppendHook = (
|
|
496
|
+
$: EngineInterface,
|
|
497
|
+
e: SessionAppendInput,
|
|
498
|
+
next: (e: SessionAppendInput) => Promise<SessionAppendResult>,
|
|
499
|
+
) => Promise<SessionAppendResult>
|
|
500
|
+
}
|