@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.
@@ -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 `![${pictureCaption(name)}](${address})`;
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(PICTURE_SYNTAX)].map((match) => match[1]);
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
- ...result
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
+ }
package/hooks/hooks.json CHANGED
@@ -56,5 +56,6 @@
56
56
  ]
57
57
  }
58
58
  ]
59
- }
59
+ },
60
+ "modules": ["./mod.ts"]
60
61
  }