@workerdeck/client 0.16.0 → 0.18.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/README.md CHANGED
@@ -86,6 +86,23 @@ The queue stream has no replay: on (re)connect, re-list jobs and treat the strea
86
86
 
87
87
  ## Rules you cannot infer from the types
88
88
 
89
+ - **`truncateResults` is for renderers only.** It asks the gateway to replay an oversized
90
+ `tool_result` as its *head*, with `truncated`/`total_chars` set and the rest available from
91
+ `client.toolResult(...)`. Ask for it only if you also fetch it back — otherwise you will show a
92
+ head as though it were the whole result, which is the one failure this option is designed to
93
+ avoid. `@workerdeck/react`'s `useClaudeSession` sets it; nothing else in this package does, and
94
+ the default must stay off.
95
+
96
+ - **`imageRefs` is the same bargain, for pictures.** It asks the gateway to replay a
97
+ `tool_result`'s base64 image parts as `image_ref` addresses — media type, decoded size, and the
98
+ part's index in the stored block — with the bytes available from `client.toolResultImage(...)`.
99
+ Measured across 214 local sessions this is **91% of all tool-result payload and none of what any
100
+ client drew**: one session's attach falls from 4,548 KB to 1,275 KB, and a session with no
101
+ pictures in it is byte-identical. Renderers only, same reason, same default. It is deliberately a
102
+ **separate flag** from `truncateResults` rather than a widening of it, and it is the one option
103
+ here that also applies to **live** events — the render path is ref-then-fetch, so bytes arriving
104
+ live would only be discarded or pinned in client state.
105
+
89
106
  - **One client per gateway.** Session ids are unique within a gateway, not across them; two
90
107
  clients for one gateway means two of everything that is meant to be shared.
91
108
  - **A refused call throws `WorkerDeckError`, and its `status` is the useful part.** 404 means this
package/build/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AttachedFrame, CreateJobRequest, CreateProfileRequest, CreateSessionRequest, FindHostFilesResponse, GetProfileResponse, JobEvent, JobInfo, ListHostDirResponse, ListHostRootsResponse, ListProfilesResponse, McpServerActionRequest, McpServerStatusInfo, MessageAttachment, PermissionMode, ProfileInfo, QueueStats, ReadHostFileResponse, ResolvePermissionRequest, SdkSessionSummary, SessionEvent, SessionFileInfo, SessionInfo, SubmitExecutionResultRequest, SubmitExecutionResultResponse, ToolCallRequestFrame, ToolExecutionOutput, UpdateProfileRequest, UpdateSessionRequest, WriteHostFileRequest, WriteHostFileResponse } from "@workerdeck/protocol";
1
+ import { AttachedFrame, CreateJobRequest, CreateProfileRequest, CreateSessionRequest, FindHostFilesResponse, GetProfileResponse, JobEvent, JobInfo, ListHostDirResponse, ListHostRootsResponse, ListProfilesResponse, McpServerActionRequest, McpServerStatusInfo, MessageAttachment, PermissionMode, ProfileInfo, QueueStats, ReadHostFileResponse, ResolvePermissionRequest, SdkSessionSummary, SessionEvent, SessionFileInfo, SessionInfo, SubmitExecutionResultRequest, SubmitExecutionResultResponse, ToolCallRequestFrame, ToolExecutionOutput, ToolResultBlock, UpdateProfileRequest, UpdateSessionRequest, WriteHostFileRequest, WriteHostFileResponse } from "@workerdeck/protocol";
2
2
 
3
3
  //#region src/host-url.d.ts
4
4
  /**
@@ -62,7 +62,7 @@ type ClientOptions = {
62
62
  /** Extra headers for REST calls (auth). Browsers can't set WS headers — use
63
63
  * `buildWsUrl` (ticket query param) or cookies for WS auth. */
64
64
  headers?: Record<string, string>; /** Override WS URL construction (auth tickets, proxies). */
65
- buildWsUrl?: (sessionId: string, afterSeq: number) => string; /** Override the queue WS URL (`{baseUrl}/queue/ws` by default). */
65
+ buildWsUrl?: (sessionId: string, afterSeq: number, truncateResults?: boolean, imageRefs?: boolean) => string; /** Override the queue WS URL (`{baseUrl}/queue/ws` by default). */
66
66
  buildQueueWsUrl?: () => string; /** Injectable for non-browser environments/tests. Defaults to globalThis.WebSocket. */
67
67
  WebSocketImpl?: typeof WebSocket;
68
68
  fetchImpl?: typeof fetch;
@@ -82,6 +82,43 @@ declare class WorkerDeckError extends Error {
82
82
  type AttachOptions = {
83
83
  /** Replay events with seq greater than this. Default 0 (full replay). */afterSeq?: number; /** Auto-reconnect with backoff on unexpected disconnects. Default true. */
84
84
  reconnect?: boolean;
85
+ /**
86
+ * Ask the gateway to replay an oversized tool result as its **head**, with
87
+ * `truncated`/`total_chars` on the block and the whole thing one
88
+ * {@link WorkerDeckClient.toolResult} call away. Measured after shipping, that
89
+ * is a **0.3%** cut on a real session, not the 68% it was designed against —
90
+ * the projection had counted base64 as text. The mechanism is right and the
91
+ * bytes were elsewhere; see {@link AttachOptions.imageRefs}.
92
+ *
93
+ * Default off, and the default must stay off: **only the unit that renders may
94
+ * ask for it**. `client` and `react` are separate packages an embedder can
95
+ * skew, and a caller that asked for heads without knowing how to fetch the
96
+ * rest would show one as though it were the whole result — the silent lie this
97
+ * rule family exists to prevent. `useClaudeSession` sets it; nothing else here
98
+ * does. Live events are never affected.
99
+ */
100
+ truncateResults?: boolean;
101
+ /**
102
+ * Ask the gateway to deliver a tool result's base64 image parts as
103
+ * `image_ref` addresses, their bytes one {@link
104
+ * WorkerDeckClient.toolResultImage} call away.
105
+ *
106
+ * This is where the bytes actually were: measured across 214 local sessions,
107
+ * **91% of all tool-result payload is base64 no client renders** — 489 MB
108
+ * against 44 MB of text, two thirds of it from `Read` looking at a PNG. One
109
+ * session's attach fell from 4,550 KB to 771 KB with no image in it.
110
+ *
111
+ * Default off under the same rule as {@link AttachOptions.truncateResults} —
112
+ * only the unit that renders may ask — and its **own** flag rather than a
113
+ * widening of that one, because this family's "additive at protocol 7"
114
+ * argument rests on a client that never asked being unable to receive one, by
115
+ * construction rather than by release archaeology.
116
+ *
117
+ * Unlike truncation this **also applies to live events**. The render path is
118
+ * ref-then-fetch, so bytes arriving live would only be discarded or pinned in
119
+ * client state.
120
+ */
121
+ imageRefs?: boolean;
85
122
  };
86
123
  type SessionHandleEvents = {
87
124
  /** Fired on every (re)attach with the server's session snapshot. */attached: AttachedFrame; /** Every session event, replayed and live, in seq order. */
@@ -218,6 +255,32 @@ declare class WorkerDeckClient {
218
255
  * an `<img src>`. Throws {@link WorkerDeckError} with the response status —
219
256
  * a 404 means the file is gone from disk, not that the route is missing. */
220
257
  readProducedFile(sessionId: string, fileId: string): Promise<Blob>;
258
+ /**
259
+ * The URL behind a `ProjectIcon.image`. Session-scoped, like
260
+ * {@link producedFileUrl}: the fetch rides the same `canSee` gate as every
261
+ * other `/sessions/:id/*` route, and it takes **no path** — the gateway
262
+ * serves whatever its own discovery resolved for this session's cwd.
263
+ */
264
+ projectIconUrl(sessionId: string): string;
265
+ /**
266
+ * Fetch a project icon's bytes.
267
+ *
268
+ * Here rather than left to each client for the reason `readProducedFile`
269
+ * exists, plus one this route makes sharper: a VS Code webview has **no
270
+ * external `connect-src` at all**, so it cannot point an `<img src>` at a
271
+ * gateway even in principle — the bytes have to come back through a bridged
272
+ * fetch, which is exactly what this wraps. Three clients building the same
273
+ * URL from `baseUrl` was the other half of the argument.
274
+ *
275
+ * Cache the result by `ProjectIcon.image.hash`, never by session: two
276
+ * sessions in one project serve identical bytes, and the hash is on the wire
277
+ * precisely so a client fetches once per project.
278
+ *
279
+ * A 404 is the uniform "no icon" — no project, a glyph-only project, or an
280
+ * icon the gateway refused. It is deliberately not distinguishable, so treat
281
+ * it as "draw no image", never as an error worth reporting.
282
+ */
283
+ projectIcon(sessionId: string): Promise<Blob>;
221
284
  /** The session's MCP servers and their tools, live from the engine. 501 when the
222
285
  * session's engine has no MCP surface; 409 while the session is parked. */
223
286
  listMcpServers(sessionId: string): Promise<McpServerStatusInfo[]>;
@@ -319,7 +382,43 @@ declare class WorkerDeckClient {
319
382
  reconnect?: boolean;
320
383
  }): QueueHandle;
321
384
  /** @internal used by SessionHandle */
322
- openSocket(sessionId: string, afterSeq: number): WebSocket;
385
+ openSocket(sessionId: string, afterSeq: number, truncateResults?: boolean, imageRefs?: boolean): WebSocket;
386
+ /**
387
+ * The whole of a tool result whose replay delivered only its head.
388
+ *
389
+ * `toolUseId` is required and the gateway verifies it against the block: a
390
+ * woken dormant session has a fresh log with fresh seqs, so a `sourceSeq`
391
+ * cached across a gateway restart can name a different event, and being handed
392
+ * another tool's output under the row you pressed is the exact failure this
393
+ * feature exists to remove. A 404 here means "ask again with a fresh attach",
394
+ * not "empty".
395
+ */
396
+ toolResult(sessionId: string, seq: number, toolUseId: string, options?: {
397
+ imageRefs?: boolean;
398
+ }): Promise<{
399
+ seq: number;
400
+ toolUseId: string;
401
+ content: ToolResultBlock['content'];
402
+ isError: boolean;
403
+ }>;
404
+ /**
405
+ * One image part's bytes, addressed by the `image_ref` a replay delivered in
406
+ * its place.
407
+ *
408
+ * A `Blob` and not a URL, and that is the whole reason this method exists: an
409
+ * `<img src>` pointing at the gateway carries a credential in exactly one of
410
+ * this project's four clients (the dashboard's same-origin implicit host,
411
+ * where the cookie rides along). Everywhere else — an added cross-origin
412
+ * gateway on a Bearer header, the VS Code webview whose every byte crosses a
413
+ * postMessage bridge, iOS — the URL is unauthenticated and the picture is a
414
+ * broken icon. Fetched rather than pointed at, then handed to
415
+ * `URL.createObjectURL`; `readProducedFile` is the shipped precedent.
416
+ *
417
+ * A 404 means "ask again with a fresh attach": a woken dormant session has a
418
+ * fresh log with fresh seqs, and the gateway refuses a stale address rather
419
+ * than serving another call's pixels under the row you are looking at.
420
+ */
421
+ toolResultImage(sessionId: string, seq: number, toolUseId: string, partIndex: number): Promise<Blob>;
323
422
  /** @internal used by QueueHandle */
324
423
  openQueueSocket(): WebSocket;
325
424
  }
package/build/index.mjs CHANGED
@@ -215,7 +215,7 @@ var SessionHandle = class {
215
215
  }
216
216
  #connect() {
217
217
  if (this.#closed) return;
218
- const ws = this.#client.openSocket(this.sessionId, this.#lastSeq);
218
+ const ws = this.#client.openSocket(this.sessionId, this.#lastSeq, this.#options.truncateResults, this.#options.imageRefs);
219
219
  this.#ws = ws;
220
220
  ws.onopen = () => {
221
221
  this.#retries = 0;
@@ -412,6 +412,38 @@ var WorkerDeckClient = class {
412
412
  if (!res.ok) throw new WorkerDeckError((await res.json().catch(() => ({}))).error ?? `produced file request failed with ${res.status}`, res.status);
413
413
  return await res.blob();
414
414
  }
415
+ /**
416
+ * The URL behind a `ProjectIcon.image`. Session-scoped, like
417
+ * {@link producedFileUrl}: the fetch rides the same `canSee` gate as every
418
+ * other `/sessions/:id/*` route, and it takes **no path** — the gateway
419
+ * serves whatever its own discovery resolved for this session's cwd.
420
+ */
421
+ projectIconUrl(sessionId) {
422
+ return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/project/icon`;
423
+ }
424
+ /**
425
+ * Fetch a project icon's bytes.
426
+ *
427
+ * Here rather than left to each client for the reason `readProducedFile`
428
+ * exists, plus one this route makes sharper: a VS Code webview has **no
429
+ * external `connect-src` at all**, so it cannot point an `<img src>` at a
430
+ * gateway even in principle — the bytes have to come back through a bridged
431
+ * fetch, which is exactly what this wraps. Three clients building the same
432
+ * URL from `baseUrl` was the other half of the argument.
433
+ *
434
+ * Cache the result by `ProjectIcon.image.hash`, never by session: two
435
+ * sessions in one project serve identical bytes, and the hash is on the wire
436
+ * precisely so a client fetches once per project.
437
+ *
438
+ * A 404 is the uniform "no icon" — no project, a glyph-only project, or an
439
+ * icon the gateway refused. It is deliberately not distinguishable, so treat
440
+ * it as "draw no image", never as an error worth reporting.
441
+ */
442
+ async projectIcon(sessionId) {
443
+ const res = await this.#fetch(this.projectIconUrl(sessionId), { headers: { ...this.#options.headers } });
444
+ if (!res.ok) throw new WorkerDeckError((await res.json().catch(() => ({}))).error ?? `project icon request failed with ${res.status}`, res.status);
445
+ return await res.blob();
446
+ }
415
447
  /** The session's MCP servers and their tools, live from the engine. 501 when the
416
448
  * session's engine has no MCP surface; 409 while the session is parked. */
417
449
  async listMcpServers(sessionId) {
@@ -566,10 +598,47 @@ var WorkerDeckClient = class {
566
598
  return new QueueHandle(this, options);
567
599
  }
568
600
  /** @internal used by SessionHandle */
569
- openSocket(sessionId, afterSeq) {
570
- const url = this.#options.buildWsUrl?.(sessionId, afterSeq) ?? `${this.#options.baseUrl.replace(/^http/, "ws")}/sessions/${encodeURIComponent(sessionId)}/ws?afterSeq=${afterSeq}`;
601
+ openSocket(sessionId, afterSeq, truncateResults = false, imageRefs = false) {
602
+ const query = `afterSeq=${afterSeq}` + (truncateResults ? "&truncateResults=1" : "") + (imageRefs ? "&imageRefs=1" : "");
603
+ const url = this.#options.buildWsUrl?.(sessionId, afterSeq, truncateResults, imageRefs) ?? `${this.#options.baseUrl.replace(/^http/, "ws")}/sessions/${encodeURIComponent(sessionId)}/ws?${query}`;
571
604
  return new this.#WebSocketImpl(url);
572
605
  }
606
+ /**
607
+ * The whole of a tool result whose replay delivered only its head.
608
+ *
609
+ * `toolUseId` is required and the gateway verifies it against the block: a
610
+ * woken dormant session has a fresh log with fresh seqs, so a `sourceSeq`
611
+ * cached across a gateway restart can name a different event, and being handed
612
+ * another tool's output under the row you pressed is the exact failure this
613
+ * feature exists to remove. A 404 here means "ask again with a fresh attach",
614
+ * not "empty".
615
+ */
616
+ async toolResult(sessionId, seq, toolUseId, options) {
617
+ return await this.#call("GET", `/sessions/${encodeURIComponent(sessionId)}/events/${seq}/result?toolUseId=${encodeURIComponent(toolUseId)}` + (options?.imageRefs ? "&imageRefs=1" : ""));
618
+ }
619
+ /**
620
+ * One image part's bytes, addressed by the `image_ref` a replay delivered in
621
+ * its place.
622
+ *
623
+ * A `Blob` and not a URL, and that is the whole reason this method exists: an
624
+ * `<img src>` pointing at the gateway carries a credential in exactly one of
625
+ * this project's four clients (the dashboard's same-origin implicit host,
626
+ * where the cookie rides along). Everywhere else — an added cross-origin
627
+ * gateway on a Bearer header, the VS Code webview whose every byte crosses a
628
+ * postMessage bridge, iOS — the URL is unauthenticated and the picture is a
629
+ * broken icon. Fetched rather than pointed at, then handed to
630
+ * `URL.createObjectURL`; `readProducedFile` is the shipped precedent.
631
+ *
632
+ * A 404 means "ask again with a fresh attach": a woken dormant session has a
633
+ * fresh log with fresh seqs, and the gateway refuses a stale address rather
634
+ * than serving another call's pixels under the row you are looking at.
635
+ */
636
+ async toolResultImage(sessionId, seq, toolUseId, partIndex) {
637
+ const path = `/sessions/${encodeURIComponent(sessionId)}/events/${seq}/result?toolUseId=${encodeURIComponent(toolUseId)}&part=${partIndex}`;
638
+ const res = await this.#fetch(`${this.#options.baseUrl}${path}`, { headers: { ...this.#options.headers } });
639
+ if (!res.ok) throw new WorkerDeckError((await res.json().catch(() => ({}))).error ?? `image part request failed with ${res.status}`, res.status);
640
+ return await res.blob();
641
+ }
573
642
  /** @internal used by QueueHandle */
574
643
  openQueueSocket() {
575
644
  const url = this.#options.buildQueueWsUrl?.() ?? `${this.#options.baseUrl.replace(/^http/, "ws")}/queue/ws`;
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["#client","#options","#lastSeq","#connectTimer","#connect","#listeners","#sendFrame","#closed","#ws","#retries","#outbox","#emit","#reconnect","#fetch","#WebSocketImpl","#call"],"sources":["../src/host-url.ts","../src/host-auth.ts","../src/index.ts"],"sourcesContent":["/**\n * Pure URL logic for gateway hosts — what an operator types, turned into the\n * `baseUrl` a `WorkerDeckClient` takes.\n *\n * Here rather than in each client because there were already two copies (the iOS\n * `Host.apiURL` and the VS Code extension's port) and a third was coming. Every\n * host that lets someone type a gateway address has to normalize it the same\n * way, or the same gateway saved on two devices is two gateways.\n */\nexport type HostUrl = { baseUrl: string }\n\n/** Normalized REST base for `WorkerDeckClient`, or undefined if unparseable. */\nexport function apiUrl(host: HostUrl): string | undefined {\n let text = host.baseUrl.trim()\n while (text.endsWith('/')) text = text.slice(0, -1)\n if (text === '') return undefined\n // A bare `mac.tailnet.ts.net:8787` is a host:port, not a scheme — tailnet\n // gateways are plain http, so that is the sane default to assume.\n if (!text.includes('://')) text = 'http://' + text\n if (!text.endsWith('/v1')) text += '/v1'\n try {\n // Validation only — the string, not the URL object, is what we keep.\n new URL(text)\n } catch {\n return undefined\n }\n return text\n}\n\n/**\n * Whether this gateway is the machine the caller runs on. Decided from the URL,\n * never by probing paths for existence — two checkouts of the same repo would\n * lie. In a remote development window the caller runs on the remote box, so\n * \"loopback\" correctly means *that* machine and its paths are real files there.\n */\nexport function isLoopbackHost(host: HostUrl): boolean {\n const api = apiUrl(host)\n if (!api) return false\n try {\n const { hostname } = new URL(api)\n return (\n hostname === '127.0.0.1' ||\n hostname === 'localhost' ||\n hostname === '::1' ||\n hostname === '[::1]'\n )\n } catch {\n return false\n }\n}\n","import type { ClientOptions } from './index.ts'\n\n/**\n * The `ClientOptions` a **browser** needs to reach a gateway that is not its own\n * origin.\n *\n * Here, beside `apiUrl`, for the same reason that is here: every host that lets\n * someone type a gateway address and a key has to present them identically, or\n * the same gateway works in one client and not another. It is browser-shaped on\n * purpose — a Node host (the VS Code extension) sends the key as a header on\n * both transports and needs none of this.\n *\n * Two transports, because a browser has no choice:\n *\n * - **REST** takes `Authorization: Bearer <key>`, like any service client.\n * - **WebSocket** takes `?key=<key>`, because a tab cannot put a header on an\n * upgrade handshake and the gateway's cookie belongs to another origin. The\n * CLI's auth accepts the key this way on upgrades *only*.\n *\n * The query-string transport is the weaker one and is worth naming: unlike a\n * header it is a permanent credential that lands in reverse-proxy access logs.\n * It is confined to the upgrade so what a leaked URL buys is one attach. If a\n * gateway later mints short-lived tickets, only the body of `buildWsUrl`\n * changes — callers of this function do not.\n */\nexport function hostAuth(options: {\n /** The gateway's API root, as `apiUrl()` returns it (ends in `/v1`). */\n baseUrl: string\n /** The operator's gateway key. Empty means an unauthenticated gateway. */\n key: string\n}): Pick<ClientOptions, 'headers' | 'buildWsUrl' | 'buildQueueWsUrl'> {\n const { baseUrl, key } = options\n if (key === '') return {}\n\n const wsRoot = baseUrl.replace(/^http/, 'ws')\n // Appended with the same `?`/`&` care the default URLs need: the session\n // socket already carries `afterSeq`, the queue socket carries nothing.\n const withKey = (url: string): string =>\n `${url}${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(key)}`\n\n return {\n headers: { authorization: `Bearer ${key}` },\n buildWsUrl: (sessionId, afterSeq) =>\n withKey(`${wsRoot}/sessions/${encodeURIComponent(sessionId)}/ws?afterSeq=${afterSeq}`),\n buildQueueWsUrl: () => withKey(`${wsRoot}/queue/ws`),\n }\n}\n","import type {\n AttachedFrame,\n ClientFrame,\n CreateJobRequest,\n CreateProfileRequest,\n CreateSessionRequest,\n JobEvent,\n JobInfo,\n FindHostFilesResponse,\n GetProfileResponse,\n ListHostDirResponse,\n ListHostRootsResponse,\n ListProfilesResponse,\n ListSessionFilesResponse,\n McpServerActionRequest,\n McpServersResponse,\n McpServerStatusInfo,\n MessageAttachment,\n ReadHostFileResponse,\n UploadAttachmentResponse,\n WriteHostFileRequest,\n WriteHostFileResponse,\n PermissionMode,\n ProfileInfo,\n QueueServerFrame,\n QueueStats,\n ResolvePermissionRequest,\n UpdateSessionRequest,\n SubmitExecutionResultRequest,\n SubmitExecutionResultResponse,\n SaveProfileResponse,\n SdkSessionSummary,\n ServerFrame,\n SessionEvent,\n SessionFileInfo,\n SessionInfo,\n ToolCallRequestFrame,\n ToolExecutionOutput,\n UpdateProfileRequest,\n} from '@workerdeck/protocol'\n\n/** Whatever the ambient `fetch` accepts as a body — `Blob`/`File` in a browser,\n * `Uint8Array` or a string in Node. Derived rather than named (`BodyInit` is a\n * DOM-lib type, and this package compiles against both). */\nexport type FetchBody = NonNullable<NonNullable<Parameters<typeof fetch>[1]>['body']>\n\nexport type ClientOptions = {\n /** REST base, e.g. \"http://127.0.0.1:8787/v1\". The ws:// URL is derived from it. */\n baseUrl: string\n /** Extra headers for REST calls (auth). Browsers can't set WS headers — use\n * `buildWsUrl` (ticket query param) or cookies for WS auth. */\n headers?: Record<string, string>\n /** Override WS URL construction (auth tickets, proxies). */\n buildWsUrl?: (sessionId: string, afterSeq: number) => string\n /** Override the queue WS URL (`{baseUrl}/queue/ws` by default). */\n buildQueueWsUrl?: () => string\n /** Injectable for non-browser environments/tests. Defaults to globalThis.WebSocket. */\n WebSocketImpl?: typeof WebSocket\n fetchImpl?: typeof fetch\n}\n\n/**\n * A REST call the gateway refused, carrying the status alongside the message.\n *\n * An `Error` subclass on purpose: every existing `e instanceof Error` check and\n * every `e.message` read keeps working unchanged. The status is what lets a\n * caller tell \"this server doesn't have that route\" (404 — stop asking) from\n * \"that file was too big\" (413 — tell the user), which a message string can't.\n */\nexport class WorkerDeckError extends Error {\n readonly status: number\n constructor(message: string, status: number) {\n super(message)\n this.name = 'WorkerDeckError'\n this.status = status\n }\n}\n\nexport type AttachOptions = {\n /** Replay events with seq greater than this. Default 0 (full replay). */\n afterSeq?: number\n /** Auto-reconnect with backoff on unexpected disconnects. Default true. */\n reconnect?: boolean\n}\n\nexport type SessionHandleEvents = {\n /** Fired on every (re)attach with the server's session snapshot. */\n attached: AttachedFrame\n /** Every session event, replayed and live, in seq order. */\n event: SessionEvent\n protocolError: string\n /** WS connectivity: true on open, false on close. */\n connectionChange: boolean\n /**\n * A reconnect has been scheduled, carrying how many have failed in a row (1 on\n * the first). The handle retries forever, so \"offline\" is a judgement a UI makes\n * about how long it has been failing rather than a state reported here.\n */\n reconnectAttempt: number\n /**\n * The server is asking this client to execute a tool call in its own sandbox.\n * Answer with {@link SessionHandle.sendToolCallResult} or\n * {@link SessionHandle.sendToolCallError}, echoing the same `executionId`.\n * Ignoring it is safe: the server fails the execution at `expiresAt`.\n */\n toolCallRequest: ToolCallRequestFrame\n /** A bridged call no longer needs an answer (turn interrupted, timed out, or\n * the session closed) — abandon any work in progress for this executionId. */\n toolCallCanceled: { executionId: string; reason: string }\n}\n\ntype Listener<T> = (payload: T) => void\n\nexport class SessionHandle {\n readonly sessionId: string\n #client: WorkerDeckClient\n #options: Required<Pick<AttachOptions, 'reconnect'>> & AttachOptions\n #ws: WebSocket | undefined\n #listeners = new Map<keyof SessionHandleEvents, Set<Listener<never>>>()\n #lastSeq: number\n #closed = false\n #retries = 0\n #outbox: string[] = []\n #connectTimer: ReturnType<typeof setTimeout> | undefined\n\n constructor(client: WorkerDeckClient, sessionId: string, options: AttachOptions = {}) {\n this.#client = client\n this.sessionId = sessionId\n this.#options = { reconnect: true, ...options }\n this.#lastSeq = options.afterSeq ?? 0\n // Deferred a tick so an attach that is detached in the same tick (React\n // StrictMode's throwaway dev mount) never opens a socket — closing a\n // WebSocket mid-upgrade breaks proxies (vite logs EPIPE) for nothing.\n this.#connectTimer = setTimeout(() => this.#connect(), 0)\n }\n\n get lastSeq(): number {\n return this.#lastSeq\n }\n\n on<K extends keyof SessionHandleEvents>(\n kind: K,\n listener: Listener<SessionHandleEvents[K]>,\n ): () => void {\n let set = this.#listeners.get(kind)\n if (!set) {\n set = new Set()\n this.#listeners.set(kind, set)\n }\n set.add(listener as Listener<never>)\n return () => set.delete(listener as Listener<never>)\n }\n\n /** Send a message, optionally naming attachments uploaded ahead of it with\n * {@link WorkerDeckClient.uploadAttachment} (ids in the order they should reach\n * the model). An unknown id fails the whole command — the server will not send a\n * message that quietly lost its picture. */\n send(text: string, attachmentIds?: string[]): void {\n this.#sendFrame({\n type: 'user_message',\n text,\n attachmentIds: attachmentIds?.length ? attachmentIds : undefined,\n })\n }\n\n approve(requestId: string, updatedInput?: Record<string, unknown>): void {\n this.#sendFrame({ type: 'permission_decision', requestId, behavior: 'allow', updatedInput })\n }\n\n deny(requestId: string, message?: string, interrupt?: boolean): void {\n this.#sendFrame({ type: 'permission_decision', requestId, behavior: 'deny', message, interrupt })\n }\n\n interrupt(): void {\n this.#sendFrame({ type: 'interrupt' })\n }\n\n setPermissionMode(mode: PermissionMode): void {\n this.#sendFrame({ type: 'set_permission_mode', mode })\n }\n\n /** Switch the model for subsequent responses; omit `model` for the default. */\n setModel(model?: string): void {\n this.#sendFrame({ type: 'set_model', model })\n }\n\n /** Answer a bridged tool call (see the `toolCallRequest` event). */\n sendToolCallResult(executionId: string, output: ToolExecutionOutput, logs?: string[]): void {\n this.#sendFrame({ type: 'tool_call_result', executionId, output, logs })\n }\n\n /** Report that a bridged tool call could not be executed. The failure is fed\n * to the model as tool output, so the agent can adapt rather than stall. */\n sendToolCallError(executionId: string, reason: string, error: string, logs?: string[]): void {\n this.#sendFrame({ type: 'tool_call_error', executionId, reason, error, logs })\n }\n\n /** Ask the server to terminate the session (the handle disconnects too). */\n closeSession(): void {\n this.#sendFrame({ type: 'close' })\n this.detach()\n }\n\n /** Skip the reconnect backoff and try again now — what a tab returning to the\n * foreground should do, rather than sitting out the remaining delay. No-op\n * while connected or after {@link SessionHandle.detach}. */\n reconnectNow(): void {\n if (this.#closed || (this.#ws && this.#ws.readyState === 1)) return\n clearTimeout(this.#connectTimer)\n this.#retries = 0\n this.#connect()\n }\n\n /** Disconnect this handle without touching the session. */\n detach(): void {\n this.#closed = true\n clearTimeout(this.#connectTimer)\n this.#ws?.close()\n this.#ws = undefined\n }\n\n #emit<K extends keyof SessionHandleEvents>(kind: K, payload: SessionHandleEvents[K]): void {\n const set = this.#listeners.get(kind)\n if (!set) return\n for (const listener of set) {\n try {\n ;(listener as Listener<SessionHandleEvents[K]>)(payload)\n } catch {\n // listener errors must not break the stream\n }\n }\n }\n\n #sendFrame(frame: ClientFrame): void {\n const payload = JSON.stringify(frame)\n // readyState 1 === OPEN (avoid touching the WebSocket global; impl may be injected)\n if (this.#ws && this.#ws.readyState === 1) this.#ws.send(payload)\n else this.#outbox.push(payload)\n }\n\n #connect(): void {\n if (this.#closed) return\n const ws = this.#client.openSocket(this.sessionId, this.#lastSeq)\n this.#ws = ws\n ws.onopen = () => {\n this.#retries = 0\n this.#emit('connectionChange', true)\n for (const payload of this.#outbox.splice(0)) ws.send(payload)\n }\n ws.onmessage = (msg: MessageEvent) => {\n const frame = JSON.parse(String(msg.data)) as ServerFrame\n if (frame.type === 'attached') {\n this.#emit('attached', frame)\n } else if (frame.type === 'event') {\n if (frame.event.seq <= this.#lastSeq) return\n this.#lastSeq = frame.event.seq\n this.#emit('event', frame.event)\n } else if (frame.type === 'tool_call_request') {\n this.#emit('toolCallRequest', frame)\n } else if (frame.type === 'tool_call_canceled') {\n this.#emit('toolCallCanceled', { executionId: frame.executionId, reason: frame.reason })\n } else if (frame.type === 'protocol_error') {\n this.#emit('protocolError', frame.message)\n }\n }\n ws.onclose = () => {\n this.#emit('connectionChange', false)\n if (this.#closed || !this.#options.reconnect) return\n const delay = Math.min(500 * 2 ** this.#retries++, 10_000)\n this.#emit('reconnectAttempt', this.#retries)\n this.#connectTimer = setTimeout(() => this.#connect(), delay)\n }\n ws.onerror = () => {\n // onclose follows; reconnect handled there\n }\n }\n}\n\nexport type QueueHandleEvents = {\n /** Fired on every (re)attach with the server's current stats. */\n attached: QueueStats\n /** Every job lifecycle/progress event, live. */\n event: JobEvent\n /** Refreshed stats pushed after job lifecycle changes. */\n stats: QueueStats\n /** WS connectivity: true on open, false on close. */\n connectionChange: boolean\n}\n\n/**\n * Live view of the server's job queue over `{basePath}/queue/ws`. The stream is\n * read-only — submit/cancel stay on the REST methods. There is no replay: on\n * (re)connect, re-list jobs and treat the stream as updates from there.\n */\nexport class QueueHandle {\n #client: WorkerDeckClient\n #reconnect: boolean\n #ws: WebSocket | undefined\n #listeners = new Map<keyof QueueHandleEvents, Set<Listener<never>>>()\n #closed = false\n #retries = 0\n #connectTimer: ReturnType<typeof setTimeout> | undefined\n\n constructor(client: WorkerDeckClient, options: { reconnect?: boolean } = {}) {\n this.#client = client\n this.#reconnect = options.reconnect ?? true\n // Deferred a tick for the same StrictMode reason as SessionHandle.\n this.#connectTimer = setTimeout(() => this.#connect(), 0)\n }\n\n on<K extends keyof QueueHandleEvents>(\n kind: K,\n listener: Listener<QueueHandleEvents[K]>,\n ): () => void {\n let set = this.#listeners.get(kind)\n if (!set) {\n set = new Set()\n this.#listeners.set(kind, set)\n }\n set.add(listener as Listener<never>)\n return () => set.delete(listener as Listener<never>)\n }\n\n detach(): void {\n this.#closed = true\n clearTimeout(this.#connectTimer)\n this.#ws?.close()\n this.#ws = undefined\n }\n\n #emit<K extends keyof QueueHandleEvents>(kind: K, payload: QueueHandleEvents[K]): void {\n const set = this.#listeners.get(kind)\n if (!set) return\n for (const listener of set) {\n try {\n ;(listener as Listener<QueueHandleEvents[K]>)(payload)\n } catch {\n // listener errors must not break the stream\n }\n }\n }\n\n #connect(): void {\n if (this.#closed) return\n const ws = this.#client.openQueueSocket()\n this.#ws = ws\n ws.onopen = () => {\n this.#retries = 0\n this.#emit('connectionChange', true)\n }\n ws.onmessage = (msg: MessageEvent) => {\n const frame = JSON.parse(String(msg.data)) as QueueServerFrame\n if (frame.type === 'queue_attached') {\n this.#emit('attached', frame.stats)\n this.#emit('stats', frame.stats)\n } else if (frame.type === 'job_event') {\n this.#emit('event', frame.event)\n } else if (frame.type === 'queue_stats') {\n this.#emit('stats', frame.stats)\n }\n }\n ws.onclose = () => {\n this.#emit('connectionChange', false)\n if (this.#closed || !this.#reconnect) return\n const delay = Math.min(500 * 2 ** this.#retries++, 10_000)\n this.#connectTimer = setTimeout(() => this.#connect(), delay)\n }\n ws.onerror = () => {\n // onclose follows; reconnect handled there\n }\n }\n}\n\nexport class WorkerDeckClient {\n #options: ClientOptions\n #fetch: typeof fetch\n #WebSocketImpl: typeof WebSocket\n\n constructor(options: ClientOptions) {\n this.#options = options\n this.#fetch = options.fetchImpl ?? fetch.bind(globalThis)\n this.#WebSocketImpl = options.WebSocketImpl ?? WebSocket\n }\n\n /**\n * Stable identity of the (gateway, principal) pair this client speaks as:\n * the base URL plus the auth headers it sends, order-insensitively.\n *\n * Exists for client-side caches that must survive the client *instance*\n * being rebuilt (a `useMemo` recreating it when a view switches gateways)\n * without ever sharing an entry across gateways — a session id is unique\n * only within one — or across credentials. Auth that rides outside\n * `headers` (a same-origin cookie, a fetch shim adding the key host-side)\n * is chosen per origin in every such host, so the base URL still separates\n * principals there; an embedder whose principal varies some other way on\n * one base URL should not key anything on this.\n */\n get identityKey(): string {\n const headers = Object.entries(this.#options.headers ?? {}).map(\n ([name, value]) => [name.toLowerCase(), value] as const,\n )\n headers.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n return JSON.stringify([this.#options.baseUrl, headers])\n }\n\n async createSession(request: CreateSessionRequest): Promise<SessionInfo> {\n const body = await this.#call('POST', '/sessions', request)\n return (body as { session: SessionInfo }).session\n }\n\n async listSessions(): Promise<SessionInfo[]> {\n const body = await this.#call('GET', '/sessions')\n return (body as { sessions: SessionInfo[] }).sessions\n }\n\n async getSession(id: string): Promise<SessionInfo> {\n const body = await this.#call('GET', `/sessions/${encodeURIComponent(id)}`)\n return (body as { session: SessionInfo }).session\n }\n\n /** Rename a session (or clear the name with `null`, restoring the derived\n * title). 409 when the session is parked. */\n async updateSession(id: string, patch: UpdateSessionRequest): Promise<SessionInfo> {\n const body = await this.#call('PATCH', `/sessions/${encodeURIComponent(id)}`, patch)\n return (body as { session: SessionInfo }).session\n }\n\n async deleteSession(id: string): Promise<SessionInfo> {\n const body = await this.#call('DELETE', `/sessions/${encodeURIComponent(id)}`)\n return (body as { session: SessionInfo }).session\n }\n\n /** List the files currently in a session's scratch filesystem (deliverables the\n * agent wrote; see the `file_delivered` event). 404s when the session's engine\n * has no file store (Claude-engine sessions). */\n async listSessionFiles(sessionId: string): Promise<SessionFileInfo[]> {\n const body = await this.#call('GET', `/sessions/${encodeURIComponent(sessionId)}/files`)\n return (body as ListSessionFilesResponse).files\n }\n\n /** Download one session file as text. */\n async fetchSessionFile(sessionId: string, path: string): Promise<string> {\n const res = await this.#fetch(this.sessionFileUrl(sessionId, path), {\n headers: this.#options.headers,\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(payload.error ?? `GET file failed with ${res.status}`, res.status)\n }\n return await res.text()\n }\n\n /**\n * Upload one file for the session, ahead of the message that will carry it.\n * The returned `id` goes to {@link SessionHandle.send}.\n *\n * The body is the raw bytes — no multipart — so anything `fetch` accepts as a\n * body works: a `File`/`Blob` from a picker, a `Uint8Array`, a string.\n */\n async uploadAttachment(\n sessionId: string,\n file: { name: string; mediaType: string; data: FetchBody },\n ): Promise<MessageAttachment> {\n const url = `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/attachments?name=${encodeURIComponent(file.name)}`\n const res = await this.#fetch(url, {\n method: 'POST',\n headers: { ...this.#options.headers, 'content-type': file.mediaType },\n body: file.data,\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(payload.error ?? `upload failed with ${res.status}`, res.status)\n }\n return ((await res.json()) as UploadAttachmentResponse).attachment\n }\n\n /** Direct URL for an uploaded attachment — an `<img src>` on a cookie-authenticated\n * same-origin server. Header-authenticated clients must fetch it themselves. */\n attachmentUrl(sessionId: string, attachmentId: string): string {\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(attachmentId)}`\n }\n\n /**\n * Direct URL for a file the session's ENGINE produced on the host — the\n * `fileId` of a `file_produced` event. Same caveat as `attachmentUrl`: usable\n * as an `<img src>` only where the credential is a same-origin cookie; a\n * header-authenticated client (the phone) fetches it and makes its own blob.\n *\n * Unlike `/fs/read`, this needs no host-file roots and no raised byte cap —\n * see the `file_produced` note in the protocol for why that is sound.\n */\n producedFileUrl(sessionId: string, fileId: string): string {\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/produced/${encodeURIComponent(fileId)}`\n }\n\n /** Fetch a produced file's bytes. For clients that cannot put a credential on\n * an `<img src>`. Throws {@link WorkerDeckError} with the response status —\n * a 404 means the file is gone from disk, not that the route is missing. */\n async readProducedFile(sessionId: string, fileId: string): Promise<Blob> {\n const res = await this.#fetch(this.producedFileUrl(sessionId, fileId), {\n headers: { ...this.#options.headers },\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(\n payload.error ?? `produced file request failed with ${res.status}`,\n res.status,\n )\n }\n return await res.blob()\n }\n\n /** The session's MCP servers and their tools, live from the engine. 501 when the\n * session's engine has no MCP surface; 409 while the session is parked. */\n async listMcpServers(sessionId: string): Promise<McpServerStatusInfo[]> {\n const body = await this.#call('GET', `/sessions/${encodeURIComponent(sessionId)}/mcp`)\n return (body as McpServersResponse).servers\n }\n\n /** Reconnect, enable or disable one MCP server; answers with the refreshed list. */\n async mcpServerAction(\n sessionId: string,\n serverName: string,\n action: McpServerActionRequest['action'],\n ): Promise<McpServerStatusInfo[]> {\n const body = await this.#call(\n 'POST',\n `/sessions/${encodeURIComponent(sessionId)}/mcp/${encodeURIComponent(serverName)}`,\n { action },\n )\n return (body as McpServersResponse).servers\n }\n\n /** Direct download URL for a session file (e.g. an <a download> href). Carries\n * no headers — on authenticated servers, use fetchSessionFile instead. */\n sessionFileUrl(sessionId: string, path: string): string {\n const encoded = path\n .split('/')\n .filter(Boolean)\n .map(encodeURIComponent)\n .join('/')\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/files/${encoded}`\n }\n\n /** Resolve a pending permission over REST — the remote-controller counterpart of the\n * WS `permission_decision` command (e.g. answering a job's AskUserQuestion from a\n * webhook consumer; the request rides on job_progress deliveries). Throws if the\n * request is unknown, already resolved, or expired. */\n async resolvePermission(\n sessionId: string,\n requestId: string,\n decision: ResolvePermissionRequest,\n ): Promise<void> {\n await this.#call(\n 'POST',\n `/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(requestId)}`,\n decision,\n )\n }\n\n /**\n * Deliver the result of a deferred tool execution — the callback a remote\n * worker (or a human) makes when the work a session parked on is done. The\n * session is rehydrated if its runner was torn down, and the agent loop\n * continues with this as the tool's output.\n *\n * Applied idempotently by `executionId`: a duplicate, or one racing the\n * execution watchdog, resolves with `applied: false` instead of applying twice.\n * Throws (404) when no session is waiting on that id.\n */\n async submitExecutionResult(\n executionId: string,\n result: SubmitExecutionResultRequest,\n ): Promise<SubmitExecutionResultResponse> {\n return (await this.#call(\n 'POST',\n `/executions/${encodeURIComponent(executionId)}/result`,\n result,\n )) as SubmitExecutionResultResponse\n }\n\n /** List the profiles (named Claude Code config dirs) this server declares, filtered\n * to what the caller may use. Feed a result's `name` to createSession({ profile }).\n * Servers predating profiles 404 here — catch and treat as none declared. */\n /** The profiles this caller may use, plus whether it may create new ones.\n * Each profile carries `managed: true` when it is store-backed and therefore\n * editable; profiles declared in server options are not. */\n async listProfiles(): Promise<ListProfilesResponse> {\n return (await this.#call('GET', '/profiles')) as ListProfilesResponse\n }\n\n /** One profile plus a fresh, view-only snapshot of its config directory (settings,\n * skills, agents, commands — env var names only, never values). */\n async getProfile(name: string): Promise<GetProfileResponse> {\n return (await this.#call('GET', `/profiles/${encodeURIComponent(name)}`)) as GetProfileResponse\n }\n\n /**\n * Create a managed profile. Requires a server with a profile store and a\n * principal allowed to manage profiles; 409 if the name is already taken by a\n * managed or a startup-declared profile.\n */\n async createProfile(profile: CreateProfileRequest): Promise<ProfileInfo> {\n const body = await this.#call('POST', '/profiles', profile)\n return (body as SaveProfileResponse).profile\n }\n\n /** Merge into a managed profile. The name is the route: profiles cannot be\n * renamed, since sessions and jobs are already pinned to the old one. */\n async updateProfile(name: string, patch: UpdateProfileRequest): Promise<ProfileInfo> {\n const body = await this.#call('PATCH', `/profiles/${encodeURIComponent(name)}`, patch)\n return (body as SaveProfileResponse).profile\n }\n\n /** Delete a managed profile. Startup-declared profiles are refused (403) —\n * they live in the server's options. */\n async deleteProfile(name: string): Promise<void> {\n await this.#call('DELETE', `/profiles/${encodeURIComponent(name)}`)\n }\n\n /** List an engine's on-disk sessions (for resume across server restarts).\n * Feed a result's `sessionId` to createSession({ resume }) — under a profile\n * of the same engine. `profile` names whose store to list (claude profiles →\n * the Agent SDK store, codex profiles → CODEX_HOME threads); absent, the\n * server resolves it implicitly when it declares exactly one profile, else\n * lists the Claude engine's store. */\n async listSdkSessions(params?: {\n dir?: string\n limit?: number\n offset?: number\n profile?: string\n }): Promise<SdkSessionSummary[]> {\n const search = new URLSearchParams()\n if (params?.dir) search.set('dir', params.dir)\n if (params?.limit !== undefined) search.set('limit', String(params.limit))\n if (params?.offset !== undefined) search.set('offset', String(params.offset))\n if (params?.profile) search.set('profile', params.profile)\n const qs = search.size > 0 ? `?${search.toString()}` : ''\n const body = await this.#call('GET', `/sdk-sessions${qs}`)\n return (body as { sdkSessions: SdkSessionSummary[] }).sdkSessions\n }\n\n // -- Host filesystem (requires the server to be configured with `hostFiles`) -\n\n /**\n * The host directories this server will let a client browse, and whether it\n * accepts writes. Servers without host-file access configured 404 here — catch\n * and treat as \"no file browser\", the same way `listProfiles` handles an older\n * server.\n *\n * These are operator-privileged routes: the auth key is the whole authorization\n * story, and they bypass the agent permission flow entirely. See the protocol\n * package's `HostFileRoot` for why that framing is deliberate.\n */\n async listHostRoots(): Promise<ListHostRootsResponse> {\n return (await this.#call('GET', '/fs/roots')) as ListHostRootsResponse\n }\n\n /** One host directory, not recursive. Symlinks are reported as symlinks, never\n * followed here — read one to find out whether it resolves somewhere allowed. */\n async listHostDir(path: string): Promise<ListHostDirResponse> {\n const qs = `?path=${encodeURIComponent(path)}`\n return (await this.#call('GET', `/fs/list${qs}`)) as ListHostDirResponse\n }\n\n /** Recursive fuzzy file search under one host directory — the `@file` picker's\n * query. Cheap enough to call per keystroke: build directories are skipped and\n * the walk is bounded, truncating rather than erroring. */\n async findHostFiles(path: string, query = '', limit?: number): Promise<FindHostFilesResponse> {\n const search = new URLSearchParams({ path, q: query })\n if (limit !== undefined) search.set('limit', String(limit))\n return (await this.#call('GET', `/fs/find?${search.toString()}`)) as FindHostFilesResponse\n }\n\n /** Read one host file. Binary content comes back base64-encoded; the returned\n * `hash` is what a later `writeHostFile` needs as its `expectedHash`. */\n async readHostFile(path: string): Promise<ReadHostFileResponse> {\n const qs = `?path=${encodeURIComponent(path)}`\n return (await this.#call('GET', `/fs/read${qs}`)) as ReadHostFileResponse\n }\n\n /**\n * Write one host file, conditionally — always. Pass the `hash` from the read this\n * edit is based on; a 409 means the agent (or anything else) changed the file\n * underneath you, and the edit must be rebased rather than forced. Omit\n * `expectedHash` only to create a file that does not exist yet.\n */\n async writeHostFile(request: WriteHostFileRequest): Promise<WriteHostFileResponse> {\n return (await this.#call('PUT', '/fs/write', request)) as WriteHostFileResponse\n }\n\n // -- Job queue (requires the server to be configured with `queue`) ----------\n\n /** Schedule a one-shot run. The returned job's `sessionId` (once running) can be\n * fed to `attach()` to watch the run live. */\n async createJob(request: CreateJobRequest): Promise<JobInfo> {\n const body = await this.#call('POST', '/jobs', request)\n return (body as { job: JobInfo }).job\n }\n\n async listJobs(): Promise<JobInfo[]> {\n const body = await this.#call('GET', '/jobs')\n return (body as { jobs: JobInfo[] }).jobs\n }\n\n async getJob(id: string): Promise<JobInfo> {\n const body = await this.#call('GET', `/jobs/${encodeURIComponent(id)}`)\n return (body as { job: JobInfo }).job\n }\n\n /** Cancel a queued or running job. */\n async cancelJob(id: string): Promise<JobInfo> {\n const body = await this.#call('DELETE', `/jobs/${encodeURIComponent(id)}`)\n return (body as { job: JobInfo }).job\n }\n\n async queueStats(): Promise<QueueStats> {\n const body = await this.#call('GET', '/queue')\n return (body as { stats: QueueStats }).stats\n }\n\n attach(sessionId: string, options?: AttachOptions): SessionHandle {\n return new SessionHandle(this, sessionId, options)\n }\n\n /** Stream the job queue live (requires the server to be configured with `queue`).\n * Servers without a queue refuse the socket — check REST first or expect retries. */\n attachQueue(options?: { reconnect?: boolean }): QueueHandle {\n return new QueueHandle(this, options)\n }\n\n /** @internal used by SessionHandle */\n openSocket(sessionId: string, afterSeq: number): WebSocket {\n const url =\n this.#options.buildWsUrl?.(sessionId, afterSeq) ??\n `${this.#options.baseUrl.replace(/^http/, 'ws')}/sessions/${encodeURIComponent(sessionId)}/ws?afterSeq=${afterSeq}`\n return new this.#WebSocketImpl(url)\n }\n\n /** @internal used by QueueHandle */\n openQueueSocket(): WebSocket {\n const url =\n this.#options.buildQueueWsUrl?.() ??\n `${this.#options.baseUrl.replace(/^http/, 'ws')}/queue/ws`\n return new this.#WebSocketImpl(url)\n }\n\n async #call(method: string, path: string, body?: unknown): Promise<unknown> {\n const res = await this.#fetch(`${this.#options.baseUrl}${path}`, {\n method,\n headers: {\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...this.#options.headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n if (!res.ok) {\n throw new WorkerDeckError(payload.error ?? `${method} ${path} failed with ${res.status}`, res.status)\n }\n return payload\n }\n}\n\nexport { apiUrl, isLoopbackHost } from './host-url.ts'\nexport type { HostUrl } from './host-url.ts'\nexport { hostAuth } from './host-auth.ts'\n"],"mappings":";;AAYA,SAAgB,OAAO,MAAmC;CACxD,IAAI,OAAO,KAAK,QAAQ,MAAM;AAC9B,QAAO,KAAK,SAAS,IAAI,CAAE,QAAO,KAAK,MAAM,GAAG,GAAG;AACnD,KAAI,SAAS,GAAI,QAAO,KAAA;AAGxB,KAAI,CAAC,KAAK,SAAS,MAAM,CAAE,QAAO,YAAY;AAC9C,KAAI,CAAC,KAAK,SAAS,MAAM,CAAE,SAAQ;AACnC,KAAI;AAEF,MAAI,IAAI,KAAK;SACP;AACN;;AAEF,QAAO;;;;;;;;AAST,SAAgB,eAAe,MAAwB;CACrD,MAAM,MAAM,OAAO,KAAK;AACxB,KAAI,CAAC,IAAK,QAAO;AACjB,KAAI;EACF,MAAM,EAAE,aAAa,IAAI,IAAI,IAAI;AACjC,SACE,aAAa,eACb,aAAa,eACb,aAAa,SACb,aAAa;SAET;AACN,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtBX,SAAgB,SAAS,SAK6C;CACpE,MAAM,EAAE,SAAS,QAAQ;AACzB,KAAI,QAAQ,GAAI,QAAO,EAAE;CAEzB,MAAM,SAAS,QAAQ,QAAQ,SAAS,KAAK;CAG7C,MAAM,WAAW,QACf,GAAG,MAAM,IAAI,SAAS,IAAI,GAAG,MAAM,IAAI,MAAM,mBAAmB,IAAI;AAEtE,QAAO;EACL,SAAS,EAAE,eAAe,UAAU,OAAO;EAC3C,aAAa,WAAW,aACtB,QAAQ,GAAG,OAAO,YAAY,mBAAmB,UAAU,CAAC,eAAe,WAAW;EACxF,uBAAuB,QAAQ,GAAG,OAAO,WAAW;EACrD;;;;;;;;;;;;ACwBH,IAAa,kBAAb,cAAqC,MAAM;CACzC;CACA,YAAY,SAAiB,QAAgB;AAC3C,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;;;AAuClB,IAAa,gBAAb,MAA2B;CACzB;CACA;CACA;CACA;CACA,6BAAa,IAAI,KAAsD;CACvE;CACA,UAAU;CACV,WAAW;CACX,UAAoB,EAAE;CACtB;CAEA,YAAY,QAA0B,WAAmB,UAAyB,EAAE,EAAE;AACpF,QAAA,SAAe;AACf,OAAK,YAAY;AACjB,QAAA,UAAgB;GAAE,WAAW;GAAM,GAAG;GAAS;AAC/C,QAAA,UAAgB,QAAQ,YAAY;AAIpC,QAAA,eAAqB,iBAAiB,MAAA,SAAe,EAAE,EAAE;;CAG3D,IAAI,UAAkB;AACpB,SAAO,MAAA;;CAGT,GACE,MACA,UACY;EACZ,IAAI,MAAM,MAAA,UAAgB,IAAI,KAAK;AACnC,MAAI,CAAC,KAAK;AACR,yBAAM,IAAI,KAAK;AACf,SAAA,UAAgB,IAAI,MAAM,IAAI;;AAEhC,MAAI,IAAI,SAA4B;AACpC,eAAa,IAAI,OAAO,SAA4B;;;;;;CAOtD,KAAK,MAAc,eAAgC;AACjD,QAAA,UAAgB;GACd,MAAM;GACN;GACA,eAAe,eAAe,SAAS,gBAAgB,KAAA;GACxD,CAAC;;CAGJ,QAAQ,WAAmB,cAA8C;AACvE,QAAA,UAAgB;GAAE,MAAM;GAAuB;GAAW,UAAU;GAAS;GAAc,CAAC;;CAG9F,KAAK,WAAmB,SAAkB,WAA2B;AACnE,QAAA,UAAgB;GAAE,MAAM;GAAuB;GAAW,UAAU;GAAQ;GAAS;GAAW,CAAC;;CAGnG,YAAkB;AAChB,QAAA,UAAgB,EAAE,MAAM,aAAa,CAAC;;CAGxC,kBAAkB,MAA4B;AAC5C,QAAA,UAAgB;GAAE,MAAM;GAAuB;GAAM,CAAC;;;CAIxD,SAAS,OAAsB;AAC7B,QAAA,UAAgB;GAAE,MAAM;GAAa;GAAO,CAAC;;;CAI/C,mBAAmB,aAAqB,QAA6B,MAAuB;AAC1F,QAAA,UAAgB;GAAE,MAAM;GAAoB;GAAa;GAAQ;GAAM,CAAC;;;;CAK1E,kBAAkB,aAAqB,QAAgB,OAAe,MAAuB;AAC3F,QAAA,UAAgB;GAAE,MAAM;GAAmB;GAAa;GAAQ;GAAO;GAAM,CAAC;;;CAIhF,eAAqB;AACnB,QAAA,UAAgB,EAAE,MAAM,SAAS,CAAC;AAClC,OAAK,QAAQ;;;;;CAMf,eAAqB;AACnB,MAAI,MAAA,UAAiB,MAAA,MAAY,MAAA,GAAS,eAAe,EAAI;AAC7D,eAAa,MAAA,aAAmB;AAChC,QAAA,UAAgB;AAChB,QAAA,SAAe;;;CAIjB,SAAe;AACb,QAAA,SAAe;AACf,eAAa,MAAA,aAAmB;AAChC,QAAA,IAAU,OAAO;AACjB,QAAA,KAAW,KAAA;;CAGb,MAA2C,MAAS,SAAuC;EACzF,MAAM,MAAM,MAAA,UAAgB,IAAI,KAAK;AACrC,MAAI,CAAC,IAAK;AACV,OAAK,MAAM,YAAY,IACrB,KAAI;AACA,YAA8C,QAAQ;UAClD;;CAMZ,WAAW,OAA0B;EACnC,MAAM,UAAU,KAAK,UAAU,MAAM;AAErC,MAAI,MAAA,MAAY,MAAA,GAAS,eAAe,EAAG,OAAA,GAAS,KAAK,QAAQ;MAC5D,OAAA,OAAa,KAAK,QAAQ;;CAGjC,WAAiB;AACf,MAAI,MAAA,OAAc;EAClB,MAAM,KAAK,MAAA,OAAa,WAAW,KAAK,WAAW,MAAA,QAAc;AACjE,QAAA,KAAW;AACX,KAAG,eAAe;AAChB,SAAA,UAAgB;AAChB,SAAA,KAAW,oBAAoB,KAAK;AACpC,QAAK,MAAM,WAAW,MAAA,OAAa,OAAO,EAAE,CAAE,IAAG,KAAK,QAAQ;;AAEhE,KAAG,aAAa,QAAsB;GACpC,MAAM,QAAQ,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC;AAC1C,OAAI,MAAM,SAAS,WACjB,OAAA,KAAW,YAAY,MAAM;YACpB,MAAM,SAAS,SAAS;AACjC,QAAI,MAAM,MAAM,OAAO,MAAA,QAAe;AACtC,UAAA,UAAgB,MAAM,MAAM;AAC5B,UAAA,KAAW,SAAS,MAAM,MAAM;cACvB,MAAM,SAAS,oBACxB,OAAA,KAAW,mBAAmB,MAAM;YAC3B,MAAM,SAAS,qBACxB,OAAA,KAAW,oBAAoB;IAAE,aAAa,MAAM;IAAa,QAAQ,MAAM;IAAQ,CAAC;YAC/E,MAAM,SAAS,iBACxB,OAAA,KAAW,iBAAiB,MAAM,QAAQ;;AAG9C,KAAG,gBAAgB;AACjB,SAAA,KAAW,oBAAoB,MAAM;AACrC,OAAI,MAAA,UAAgB,CAAC,MAAA,QAAc,UAAW;GAC9C,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,MAAA,WAAiB,IAAO;AAC1D,SAAA,KAAW,oBAAoB,MAAA,QAAc;AAC7C,SAAA,eAAqB,iBAAiB,MAAA,SAAe,EAAE,MAAM;;AAE/D,KAAG,gBAAgB;;;;;;;;AAsBvB,IAAa,cAAb,MAAyB;CACvB;CACA;CACA;CACA,6BAAa,IAAI,KAAoD;CACrE,UAAU;CACV,WAAW;CACX;CAEA,YAAY,QAA0B,UAAmC,EAAE,EAAE;AAC3E,QAAA,SAAe;AACf,QAAA,YAAkB,QAAQ,aAAa;AAEvC,QAAA,eAAqB,iBAAiB,MAAA,SAAe,EAAE,EAAE;;CAG3D,GACE,MACA,UACY;EACZ,IAAI,MAAM,MAAA,UAAgB,IAAI,KAAK;AACnC,MAAI,CAAC,KAAK;AACR,yBAAM,IAAI,KAAK;AACf,SAAA,UAAgB,IAAI,MAAM,IAAI;;AAEhC,MAAI,IAAI,SAA4B;AACpC,eAAa,IAAI,OAAO,SAA4B;;CAGtD,SAAe;AACb,QAAA,SAAe;AACf,eAAa,MAAA,aAAmB;AAChC,QAAA,IAAU,OAAO;AACjB,QAAA,KAAW,KAAA;;CAGb,MAAyC,MAAS,SAAqC;EACrF,MAAM,MAAM,MAAA,UAAgB,IAAI,KAAK;AACrC,MAAI,CAAC,IAAK;AACV,OAAK,MAAM,YAAY,IACrB,KAAI;AACA,YAA4C,QAAQ;UAChD;;CAMZ,WAAiB;AACf,MAAI,MAAA,OAAc;EAClB,MAAM,KAAK,MAAA,OAAa,iBAAiB;AACzC,QAAA,KAAW;AACX,KAAG,eAAe;AAChB,SAAA,UAAgB;AAChB,SAAA,KAAW,oBAAoB,KAAK;;AAEtC,KAAG,aAAa,QAAsB;GACpC,MAAM,QAAQ,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC;AAC1C,OAAI,MAAM,SAAS,kBAAkB;AACnC,UAAA,KAAW,YAAY,MAAM,MAAM;AACnC,UAAA,KAAW,SAAS,MAAM,MAAM;cACvB,MAAM,SAAS,YACxB,OAAA,KAAW,SAAS,MAAM,MAAM;YACvB,MAAM,SAAS,cACxB,OAAA,KAAW,SAAS,MAAM,MAAM;;AAGpC,KAAG,gBAAgB;AACjB,SAAA,KAAW,oBAAoB,MAAM;AACrC,OAAI,MAAA,UAAgB,CAAC,MAAA,UAAiB;GACtC,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,MAAA,WAAiB,IAAO;AAC1D,SAAA,eAAqB,iBAAiB,MAAA,SAAe,EAAE,MAAM;;AAE/D,KAAG,gBAAgB;;;AAMvB,IAAa,mBAAb,MAA8B;CAC5B;CACA;CACA;CAEA,YAAY,SAAwB;AAClC,QAAA,UAAgB;AAChB,QAAA,QAAc,QAAQ,aAAa,MAAM,KAAK,WAAW;AACzD,QAAA,gBAAsB,QAAQ,iBAAiB;;;;;;;;;;;;;;;CAgBjD,IAAI,cAAsB;EACxB,MAAM,UAAU,OAAO,QAAQ,MAAA,QAAc,WAAW,EAAE,CAAC,CAAC,KACzD,CAAC,MAAM,WAAW,CAAC,KAAK,aAAa,EAAE,MAAM,CAC/C;AACD,UAAQ,MAAM,CAAC,IAAI,CAAC,OAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,EAAG;AACxD,SAAO,KAAK,UAAU,CAAC,MAAA,QAAc,SAAS,QAAQ,CAAC;;CAGzD,MAAM,cAAc,SAAqD;AAEvE,UAAQ,MADW,MAAA,KAAW,QAAQ,aAAa,QAAQ,EACjB;;CAG5C,MAAM,eAAuC;AAE3C,UAAQ,MADW,MAAA,KAAW,OAAO,YAAY,EACJ;;CAG/C,MAAM,WAAW,IAAkC;AAEjD,UAAQ,MADW,MAAA,KAAW,OAAO,aAAa,mBAAmB,GAAG,GAAG,EACjC;;;;CAK5C,MAAM,cAAc,IAAY,OAAmD;AAEjF,UAAQ,MADW,MAAA,KAAW,SAAS,aAAa,mBAAmB,GAAG,IAAI,MAAM,EAC1C;;CAG5C,MAAM,cAAc,IAAkC;AAEpD,UAAQ,MADW,MAAA,KAAW,UAAU,aAAa,mBAAmB,GAAG,GAAG,EACpC;;;;;CAM5C,MAAM,iBAAiB,WAA+C;AAEpE,UAAQ,MADW,MAAA,KAAW,OAAO,aAAa,mBAAmB,UAAU,CAAC,QAAQ,EAC9C;;;CAI5C,MAAM,iBAAiB,WAAmB,MAA+B;EACvE,MAAM,MAAM,MAAM,MAAA,MAAY,KAAK,eAAe,WAAW,KAAK,EAAE,EAClE,SAAS,MAAA,QAAc,SACxB,CAAC;AACF,MAAI,CAAC,IAAI,GAEP,OAAM,IAAI,iBAAgB,MADH,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE,EACjB,SAAS,wBAAwB,IAAI,UAAU,IAAI,OAAO;AAE9F,SAAO,MAAM,IAAI,MAAM;;;;;;;;;CAUzB,MAAM,iBACJ,WACA,MAC4B;EAC5B,MAAM,MAAM,GAAG,MAAA,QAAc,QAAQ,YAAY,mBAAmB,UAAU,CAAC,oBAAoB,mBAAmB,KAAK,KAAK;EAChI,MAAM,MAAM,MAAM,MAAA,MAAY,KAAK;GACjC,QAAQ;GACR,SAAS;IAAE,GAAG,MAAA,QAAc;IAAS,gBAAgB,KAAK;IAAW;GACrE,MAAM,KAAK;GACZ,CAAC;AACF,MAAI,CAAC,IAAI,GAEP,OAAM,IAAI,iBAAgB,MADH,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE,EACjB,SAAS,sBAAsB,IAAI,UAAU,IAAI,OAAO;AAE5F,UAAS,MAAM,IAAI,MAAM,EAA+B;;;;CAK1D,cAAc,WAAmB,cAA8B;AAC7D,SAAO,GAAG,MAAA,QAAc,QAAQ,YAAY,mBAAmB,UAAU,CAAC,eAAe,mBAAmB,aAAa;;;;;;;;;;;CAY3H,gBAAgB,WAAmB,QAAwB;AACzD,SAAO,GAAG,MAAA,QAAc,QAAQ,YAAY,mBAAmB,UAAU,CAAC,YAAY,mBAAmB,OAAO;;;;;CAMlH,MAAM,iBAAiB,WAAmB,QAA+B;EACvE,MAAM,MAAM,MAAM,MAAA,MAAY,KAAK,gBAAgB,WAAW,OAAO,EAAE,EACrE,SAAS,EAAE,GAAG,MAAA,QAAc,SAAS,EACtC,CAAC;AACF,MAAI,CAAC,IAAI,GAEP,OAAM,IAAI,iBACR,MAFqB,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE,EAEzC,SAAS,qCAAqC,IAAI,UAC1D,IAAI,OACL;AAEH,SAAO,MAAM,IAAI,MAAM;;;;CAKzB,MAAM,eAAe,WAAmD;AAEtE,UAAQ,MADW,MAAA,KAAW,OAAO,aAAa,mBAAmB,UAAU,CAAC,MAAM,EAClD;;;CAItC,MAAM,gBACJ,WACA,YACA,QACgC;AAMhC,UAAQ,MALW,MAAA,KACjB,QACA,aAAa,mBAAmB,UAAU,CAAC,OAAO,mBAAmB,WAAW,IAChF,EAAE,QAAQ,CACX,EACmC;;;;CAKtC,eAAe,WAAmB,MAAsB;EACtD,MAAM,UAAU,KACb,MAAM,IAAI,CACV,OAAO,QAAQ,CACf,IAAI,mBAAmB,CACvB,KAAK,IAAI;AACZ,SAAO,GAAG,MAAA,QAAc,QAAQ,YAAY,mBAAmB,UAAU,CAAC,SAAS;;;;;;CAOrF,MAAM,kBACJ,WACA,WACA,UACe;AACf,QAAM,MAAA,KACJ,QACA,aAAa,mBAAmB,UAAU,CAAC,eAAe,mBAAmB,UAAU,IACvF,SACD;;;;;;;;;;;;CAaH,MAAM,sBACJ,aACA,QACwC;AACxC,SAAQ,MAAM,MAAA,KACZ,QACA,eAAe,mBAAmB,YAAY,CAAC,UAC/C,OACD;;;;;;;;CASH,MAAM,eAA8C;AAClD,SAAQ,MAAM,MAAA,KAAW,OAAO,YAAY;;;;CAK9C,MAAM,WAAW,MAA2C;AAC1D,SAAQ,MAAM,MAAA,KAAW,OAAO,aAAa,mBAAmB,KAAK,GAAG;;;;;;;CAQ1E,MAAM,cAAc,SAAqD;AAEvE,UAAQ,MADW,MAAA,KAAW,QAAQ,aAAa,QAAQ,EACtB;;;;CAKvC,MAAM,cAAc,MAAc,OAAmD;AAEnF,UAAQ,MADW,MAAA,KAAW,SAAS,aAAa,mBAAmB,KAAK,IAAI,MAAM,EACjD;;;;CAKvC,MAAM,cAAc,MAA6B;AAC/C,QAAM,MAAA,KAAW,UAAU,aAAa,mBAAmB,KAAK,GAAG;;;;;;;;CASrE,MAAM,gBAAgB,QAKW;EAC/B,MAAM,SAAS,IAAI,iBAAiB;AACpC,MAAI,QAAQ,IAAK,QAAO,IAAI,OAAO,OAAO,IAAI;AAC9C,MAAI,QAAQ,UAAU,KAAA,EAAW,QAAO,IAAI,SAAS,OAAO,OAAO,MAAM,CAAC;AAC1E,MAAI,QAAQ,WAAW,KAAA,EAAW,QAAO,IAAI,UAAU,OAAO,OAAO,OAAO,CAAC;AAC7E,MAAI,QAAQ,QAAS,QAAO,IAAI,WAAW,OAAO,QAAQ;EAC1D,MAAM,KAAK,OAAO,OAAO,IAAI,IAAI,OAAO,UAAU,KAAK;AAEvD,UAAQ,MADW,MAAA,KAAW,OAAO,gBAAgB,KAAK,EACJ;;;;;;;;;;;;CAexD,MAAM,gBAAgD;AACpD,SAAQ,MAAM,MAAA,KAAW,OAAO,YAAY;;;;CAK9C,MAAM,YAAY,MAA4C;EAC5D,MAAM,KAAK,SAAS,mBAAmB,KAAK;AAC5C,SAAQ,MAAM,MAAA,KAAW,OAAO,WAAW,KAAK;;;;;CAMlD,MAAM,cAAc,MAAc,QAAQ,IAAI,OAAgD;EAC5F,MAAM,SAAS,IAAI,gBAAgB;GAAE;GAAM,GAAG;GAAO,CAAC;AACtD,MAAI,UAAU,KAAA,EAAW,QAAO,IAAI,SAAS,OAAO,MAAM,CAAC;AAC3D,SAAQ,MAAM,MAAA,KAAW,OAAO,YAAY,OAAO,UAAU,GAAG;;;;CAKlE,MAAM,aAAa,MAA6C;EAC9D,MAAM,KAAK,SAAS,mBAAmB,KAAK;AAC5C,SAAQ,MAAM,MAAA,KAAW,OAAO,WAAW,KAAK;;;;;;;;CASlD,MAAM,cAAc,SAA+D;AACjF,SAAQ,MAAM,MAAA,KAAW,OAAO,aAAa,QAAQ;;;;CAOvD,MAAM,UAAU,SAA6C;AAE3D,UAAQ,MADW,MAAA,KAAW,QAAQ,SAAS,QAAQ,EACrB;;CAGpC,MAAM,WAA+B;AAEnC,UAAQ,MADW,MAAA,KAAW,OAAO,QAAQ,EACR;;CAGvC,MAAM,OAAO,IAA8B;AAEzC,UAAQ,MADW,MAAA,KAAW,OAAO,SAAS,mBAAmB,GAAG,GAAG,EACrC;;;CAIpC,MAAM,UAAU,IAA8B;AAE5C,UAAQ,MADW,MAAA,KAAW,UAAU,SAAS,mBAAmB,GAAG,GAAG,EACxC;;CAGpC,MAAM,aAAkC;AAEtC,UAAQ,MADW,MAAA,KAAW,OAAO,SAAS,EACP;;CAGzC,OAAO,WAAmB,SAAwC;AAChE,SAAO,IAAI,cAAc,MAAM,WAAW,QAAQ;;;;CAKpD,YAAY,SAAgD;AAC1D,SAAO,IAAI,YAAY,MAAM,QAAQ;;;CAIvC,WAAW,WAAmB,UAA6B;EACzD,MAAM,MACJ,MAAA,QAAc,aAAa,WAAW,SAAS,IAC/C,GAAG,MAAA,QAAc,QAAQ,QAAQ,SAAS,KAAK,CAAC,YAAY,mBAAmB,UAAU,CAAC,eAAe;AAC3G,SAAO,IAAI,MAAA,cAAoB,IAAI;;;CAIrC,kBAA6B;EAC3B,MAAM,MACJ,MAAA,QAAc,mBAAmB,IACjC,GAAG,MAAA,QAAc,QAAQ,QAAQ,SAAS,KAAK,CAAC;AAClD,SAAO,IAAI,MAAA,cAAoB,IAAI;;CAGrC,OAAA,KAAY,QAAgB,MAAc,MAAkC;EAC1E,MAAM,MAAM,MAAM,MAAA,MAAY,GAAG,MAAA,QAAc,UAAU,QAAQ;GAC/D;GACA,SAAS;IACP,GAAI,SAAS,KAAA,IAAY,EAAE,gBAAgB,oBAAoB,GAAG,EAAE;IACpE,GAAG,MAAA,QAAc;IAClB;GACD,MAAM,SAAS,KAAA,IAAY,KAAK,UAAU,KAAK,GAAG,KAAA;GACnD,CAAC;EACF,MAAM,UAAW,MAAM,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE;AACnD,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,gBAAgB,QAAQ,SAAS,GAAG,OAAO,GAAG,KAAK,eAAe,IAAI,UAAU,IAAI,OAAO;AAEvG,SAAO"}
1
+ {"version":3,"file":"index.mjs","names":["#client","#options","#lastSeq","#connectTimer","#connect","#listeners","#sendFrame","#closed","#ws","#retries","#outbox","#emit","#reconnect","#fetch","#WebSocketImpl","#call"],"sources":["../src/host-url.ts","../src/host-auth.ts","../src/index.ts"],"sourcesContent":["/**\n * Pure URL logic for gateway hosts — what an operator types, turned into the\n * `baseUrl` a `WorkerDeckClient` takes.\n *\n * Here rather than in each client because there were already two copies (the iOS\n * `Host.apiURL` and the VS Code extension's port) and a third was coming. Every\n * host that lets someone type a gateway address has to normalize it the same\n * way, or the same gateway saved on two devices is two gateways.\n */\nexport type HostUrl = { baseUrl: string }\n\n/** Normalized REST base for `WorkerDeckClient`, or undefined if unparseable. */\nexport function apiUrl(host: HostUrl): string | undefined {\n let text = host.baseUrl.trim()\n while (text.endsWith('/')) text = text.slice(0, -1)\n if (text === '') return undefined\n // A bare `mac.tailnet.ts.net:8787` is a host:port, not a scheme — tailnet\n // gateways are plain http, so that is the sane default to assume.\n if (!text.includes('://')) text = 'http://' + text\n if (!text.endsWith('/v1')) text += '/v1'\n try {\n // Validation only — the string, not the URL object, is what we keep.\n new URL(text)\n } catch {\n return undefined\n }\n return text\n}\n\n/**\n * Whether this gateway is the machine the caller runs on. Decided from the URL,\n * never by probing paths for existence — two checkouts of the same repo would\n * lie. In a remote development window the caller runs on the remote box, so\n * \"loopback\" correctly means *that* machine and its paths are real files there.\n */\nexport function isLoopbackHost(host: HostUrl): boolean {\n const api = apiUrl(host)\n if (!api) return false\n try {\n const { hostname } = new URL(api)\n return (\n hostname === '127.0.0.1' ||\n hostname === 'localhost' ||\n hostname === '::1' ||\n hostname === '[::1]'\n )\n } catch {\n return false\n }\n}\n","import type { ClientOptions } from './index.ts'\n\n/**\n * The `ClientOptions` a **browser** needs to reach a gateway that is not its own\n * origin.\n *\n * Here, beside `apiUrl`, for the same reason that is here: every host that lets\n * someone type a gateway address and a key has to present them identically, or\n * the same gateway works in one client and not another. It is browser-shaped on\n * purpose — a Node host (the VS Code extension) sends the key as a header on\n * both transports and needs none of this.\n *\n * Two transports, because a browser has no choice:\n *\n * - **REST** takes `Authorization: Bearer <key>`, like any service client.\n * - **WebSocket** takes `?key=<key>`, because a tab cannot put a header on an\n * upgrade handshake and the gateway's cookie belongs to another origin. The\n * CLI's auth accepts the key this way on upgrades *only*.\n *\n * The query-string transport is the weaker one and is worth naming: unlike a\n * header it is a permanent credential that lands in reverse-proxy access logs.\n * It is confined to the upgrade so what a leaked URL buys is one attach. If a\n * gateway later mints short-lived tickets, only the body of `buildWsUrl`\n * changes — callers of this function do not.\n */\nexport function hostAuth(options: {\n /** The gateway's API root, as `apiUrl()` returns it (ends in `/v1`). */\n baseUrl: string\n /** The operator's gateway key. Empty means an unauthenticated gateway. */\n key: string\n}): Pick<ClientOptions, 'headers' | 'buildWsUrl' | 'buildQueueWsUrl'> {\n const { baseUrl, key } = options\n if (key === '') return {}\n\n const wsRoot = baseUrl.replace(/^http/, 'ws')\n // Appended with the same `?`/`&` care the default URLs need: the session\n // socket already carries `afterSeq`, the queue socket carries nothing.\n const withKey = (url: string): string =>\n `${url}${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(key)}`\n\n return {\n headers: { authorization: `Bearer ${key}` },\n buildWsUrl: (sessionId, afterSeq) =>\n withKey(`${wsRoot}/sessions/${encodeURIComponent(sessionId)}/ws?afterSeq=${afterSeq}`),\n buildQueueWsUrl: () => withKey(`${wsRoot}/queue/ws`),\n }\n}\n","import type {\n AttachedFrame,\n ClientFrame,\n CreateJobRequest,\n CreateProfileRequest,\n CreateSessionRequest,\n JobEvent,\n JobInfo,\n FindHostFilesResponse,\n GetProfileResponse,\n ListHostDirResponse,\n ListHostRootsResponse,\n ListProfilesResponse,\n ListSessionFilesResponse,\n McpServerActionRequest,\n McpServersResponse,\n McpServerStatusInfo,\n MessageAttachment,\n ReadHostFileResponse,\n UploadAttachmentResponse,\n WriteHostFileRequest,\n WriteHostFileResponse,\n PermissionMode,\n ProfileInfo,\n QueueServerFrame,\n QueueStats,\n ResolvePermissionRequest,\n UpdateSessionRequest,\n SubmitExecutionResultRequest,\n SubmitExecutionResultResponse,\n SaveProfileResponse,\n SdkSessionSummary,\n ServerFrame,\n SessionEvent,\n SessionFileInfo,\n SessionInfo,\n ToolCallRequestFrame,\n ToolExecutionOutput,\n UpdateProfileRequest,\n ToolResultBlock,\n} from '@workerdeck/protocol'\n\n/** Whatever the ambient `fetch` accepts as a body — `Blob`/`File` in a browser,\n * `Uint8Array` or a string in Node. Derived rather than named (`BodyInit` is a\n * DOM-lib type, and this package compiles against both). */\nexport type FetchBody = NonNullable<NonNullable<Parameters<typeof fetch>[1]>['body']>\n\nexport type ClientOptions = {\n /** REST base, e.g. \"http://127.0.0.1:8787/v1\". The ws:// URL is derived from it. */\n baseUrl: string\n /** Extra headers for REST calls (auth). Browsers can't set WS headers — use\n * `buildWsUrl` (ticket query param) or cookies for WS auth. */\n headers?: Record<string, string>\n /** Override WS URL construction (auth tickets, proxies). */\n buildWsUrl?: (\n sessionId: string,\n afterSeq: number,\n truncateResults?: boolean,\n imageRefs?: boolean,\n ) => string\n /** Override the queue WS URL (`{baseUrl}/queue/ws` by default). */\n buildQueueWsUrl?: () => string\n /** Injectable for non-browser environments/tests. Defaults to globalThis.WebSocket. */\n WebSocketImpl?: typeof WebSocket\n fetchImpl?: typeof fetch\n}\n\n/**\n * A REST call the gateway refused, carrying the status alongside the message.\n *\n * An `Error` subclass on purpose: every existing `e instanceof Error` check and\n * every `e.message` read keeps working unchanged. The status is what lets a\n * caller tell \"this server doesn't have that route\" (404 — stop asking) from\n * \"that file was too big\" (413 — tell the user), which a message string can't.\n */\nexport class WorkerDeckError extends Error {\n readonly status: number\n constructor(message: string, status: number) {\n super(message)\n this.name = 'WorkerDeckError'\n this.status = status\n }\n}\n\nexport type AttachOptions = {\n /** Replay events with seq greater than this. Default 0 (full replay). */\n afterSeq?: number\n /** Auto-reconnect with backoff on unexpected disconnects. Default true. */\n reconnect?: boolean\n /**\n * Ask the gateway to replay an oversized tool result as its **head**, with\n * `truncated`/`total_chars` on the block and the whole thing one\n * {@link WorkerDeckClient.toolResult} call away. Measured after shipping, that\n * is a **0.3%** cut on a real session, not the 68% it was designed against —\n * the projection had counted base64 as text. The mechanism is right and the\n * bytes were elsewhere; see {@link AttachOptions.imageRefs}.\n *\n * Default off, and the default must stay off: **only the unit that renders may\n * ask for it**. `client` and `react` are separate packages an embedder can\n * skew, and a caller that asked for heads without knowing how to fetch the\n * rest would show one as though it were the whole result — the silent lie this\n * rule family exists to prevent. `useClaudeSession` sets it; nothing else here\n * does. Live events are never affected.\n */\n truncateResults?: boolean\n /**\n * Ask the gateway to deliver a tool result's base64 image parts as\n * `image_ref` addresses, their bytes one {@link\n * WorkerDeckClient.toolResultImage} call away.\n *\n * This is where the bytes actually were: measured across 214 local sessions,\n * **91% of all tool-result payload is base64 no client renders** — 489 MB\n * against 44 MB of text, two thirds of it from `Read` looking at a PNG. One\n * session's attach fell from 4,550 KB to 771 KB with no image in it.\n *\n * Default off under the same rule as {@link AttachOptions.truncateResults} —\n * only the unit that renders may ask — and its **own** flag rather than a\n * widening of that one, because this family's \"additive at protocol 7\"\n * argument rests on a client that never asked being unable to receive one, by\n * construction rather than by release archaeology.\n *\n * Unlike truncation this **also applies to live events**. The render path is\n * ref-then-fetch, so bytes arriving live would only be discarded or pinned in\n * client state.\n */\n imageRefs?: boolean\n}\n\nexport type SessionHandleEvents = {\n /** Fired on every (re)attach with the server's session snapshot. */\n attached: AttachedFrame\n /** Every session event, replayed and live, in seq order. */\n event: SessionEvent\n protocolError: string\n /** WS connectivity: true on open, false on close. */\n connectionChange: boolean\n /**\n * A reconnect has been scheduled, carrying how many have failed in a row (1 on\n * the first). The handle retries forever, so \"offline\" is a judgement a UI makes\n * about how long it has been failing rather than a state reported here.\n */\n reconnectAttempt: number\n /**\n * The server is asking this client to execute a tool call in its own sandbox.\n * Answer with {@link SessionHandle.sendToolCallResult} or\n * {@link SessionHandle.sendToolCallError}, echoing the same `executionId`.\n * Ignoring it is safe: the server fails the execution at `expiresAt`.\n */\n toolCallRequest: ToolCallRequestFrame\n /** A bridged call no longer needs an answer (turn interrupted, timed out, or\n * the session closed) — abandon any work in progress for this executionId. */\n toolCallCanceled: { executionId: string; reason: string }\n}\n\ntype Listener<T> = (payload: T) => void\n\nexport class SessionHandle {\n readonly sessionId: string\n #client: WorkerDeckClient\n #options: Required<Pick<AttachOptions, 'reconnect'>> & AttachOptions\n #ws: WebSocket | undefined\n #listeners = new Map<keyof SessionHandleEvents, Set<Listener<never>>>()\n #lastSeq: number\n #closed = false\n #retries = 0\n #outbox: string[] = []\n #connectTimer: ReturnType<typeof setTimeout> | undefined\n\n constructor(client: WorkerDeckClient, sessionId: string, options: AttachOptions = {}) {\n this.#client = client\n this.sessionId = sessionId\n this.#options = { reconnect: true, ...options }\n this.#lastSeq = options.afterSeq ?? 0\n // Deferred a tick so an attach that is detached in the same tick (React\n // StrictMode's throwaway dev mount) never opens a socket — closing a\n // WebSocket mid-upgrade breaks proxies (vite logs EPIPE) for nothing.\n this.#connectTimer = setTimeout(() => this.#connect(), 0)\n }\n\n get lastSeq(): number {\n return this.#lastSeq\n }\n\n on<K extends keyof SessionHandleEvents>(\n kind: K,\n listener: Listener<SessionHandleEvents[K]>,\n ): () => void {\n let set = this.#listeners.get(kind)\n if (!set) {\n set = new Set()\n this.#listeners.set(kind, set)\n }\n set.add(listener as Listener<never>)\n return () => set.delete(listener as Listener<never>)\n }\n\n /** Send a message, optionally naming attachments uploaded ahead of it with\n * {@link WorkerDeckClient.uploadAttachment} (ids in the order they should reach\n * the model). An unknown id fails the whole command — the server will not send a\n * message that quietly lost its picture. */\n send(text: string, attachmentIds?: string[]): void {\n this.#sendFrame({\n type: 'user_message',\n text,\n attachmentIds: attachmentIds?.length ? attachmentIds : undefined,\n })\n }\n\n approve(requestId: string, updatedInput?: Record<string, unknown>): void {\n this.#sendFrame({ type: 'permission_decision', requestId, behavior: 'allow', updatedInput })\n }\n\n deny(requestId: string, message?: string, interrupt?: boolean): void {\n this.#sendFrame({ type: 'permission_decision', requestId, behavior: 'deny', message, interrupt })\n }\n\n interrupt(): void {\n this.#sendFrame({ type: 'interrupt' })\n }\n\n setPermissionMode(mode: PermissionMode): void {\n this.#sendFrame({ type: 'set_permission_mode', mode })\n }\n\n /** Switch the model for subsequent responses; omit `model` for the default. */\n setModel(model?: string): void {\n this.#sendFrame({ type: 'set_model', model })\n }\n\n /** Answer a bridged tool call (see the `toolCallRequest` event). */\n sendToolCallResult(executionId: string, output: ToolExecutionOutput, logs?: string[]): void {\n this.#sendFrame({ type: 'tool_call_result', executionId, output, logs })\n }\n\n /** Report that a bridged tool call could not be executed. The failure is fed\n * to the model as tool output, so the agent can adapt rather than stall. */\n sendToolCallError(executionId: string, reason: string, error: string, logs?: string[]): void {\n this.#sendFrame({ type: 'tool_call_error', executionId, reason, error, logs })\n }\n\n /** Ask the server to terminate the session (the handle disconnects too). */\n closeSession(): void {\n this.#sendFrame({ type: 'close' })\n this.detach()\n }\n\n /** Skip the reconnect backoff and try again now — what a tab returning to the\n * foreground should do, rather than sitting out the remaining delay. No-op\n * while connected or after {@link SessionHandle.detach}. */\n reconnectNow(): void {\n if (this.#closed || (this.#ws && this.#ws.readyState === 1)) return\n clearTimeout(this.#connectTimer)\n this.#retries = 0\n this.#connect()\n }\n\n /** Disconnect this handle without touching the session. */\n detach(): void {\n this.#closed = true\n clearTimeout(this.#connectTimer)\n this.#ws?.close()\n this.#ws = undefined\n }\n\n #emit<K extends keyof SessionHandleEvents>(kind: K, payload: SessionHandleEvents[K]): void {\n const set = this.#listeners.get(kind)\n if (!set) return\n for (const listener of set) {\n try {\n ;(listener as Listener<SessionHandleEvents[K]>)(payload)\n } catch {\n // listener errors must not break the stream\n }\n }\n }\n\n #sendFrame(frame: ClientFrame): void {\n const payload = JSON.stringify(frame)\n // readyState 1 === OPEN (avoid touching the WebSocket global; impl may be injected)\n if (this.#ws && this.#ws.readyState === 1) this.#ws.send(payload)\n else this.#outbox.push(payload)\n }\n\n #connect(): void {\n if (this.#closed) return\n const ws = this.#client.openSocket(\n this.sessionId,\n this.#lastSeq,\n this.#options.truncateResults,\n this.#options.imageRefs,\n )\n this.#ws = ws\n ws.onopen = () => {\n this.#retries = 0\n this.#emit('connectionChange', true)\n for (const payload of this.#outbox.splice(0)) ws.send(payload)\n }\n ws.onmessage = (msg: MessageEvent) => {\n const frame = JSON.parse(String(msg.data)) as ServerFrame\n if (frame.type === 'attached') {\n this.#emit('attached', frame)\n } else if (frame.type === 'event') {\n if (frame.event.seq <= this.#lastSeq) return\n this.#lastSeq = frame.event.seq\n this.#emit('event', frame.event)\n } else if (frame.type === 'tool_call_request') {\n this.#emit('toolCallRequest', frame)\n } else if (frame.type === 'tool_call_canceled') {\n this.#emit('toolCallCanceled', { executionId: frame.executionId, reason: frame.reason })\n } else if (frame.type === 'protocol_error') {\n this.#emit('protocolError', frame.message)\n }\n }\n ws.onclose = () => {\n this.#emit('connectionChange', false)\n if (this.#closed || !this.#options.reconnect) return\n const delay = Math.min(500 * 2 ** this.#retries++, 10_000)\n this.#emit('reconnectAttempt', this.#retries)\n this.#connectTimer = setTimeout(() => this.#connect(), delay)\n }\n ws.onerror = () => {\n // onclose follows; reconnect handled there\n }\n }\n}\n\nexport type QueueHandleEvents = {\n /** Fired on every (re)attach with the server's current stats. */\n attached: QueueStats\n /** Every job lifecycle/progress event, live. */\n event: JobEvent\n /** Refreshed stats pushed after job lifecycle changes. */\n stats: QueueStats\n /** WS connectivity: true on open, false on close. */\n connectionChange: boolean\n}\n\n/**\n * Live view of the server's job queue over `{basePath}/queue/ws`. The stream is\n * read-only — submit/cancel stay on the REST methods. There is no replay: on\n * (re)connect, re-list jobs and treat the stream as updates from there.\n */\nexport class QueueHandle {\n #client: WorkerDeckClient\n #reconnect: boolean\n #ws: WebSocket | undefined\n #listeners = new Map<keyof QueueHandleEvents, Set<Listener<never>>>()\n #closed = false\n #retries = 0\n #connectTimer: ReturnType<typeof setTimeout> | undefined\n\n constructor(client: WorkerDeckClient, options: { reconnect?: boolean } = {}) {\n this.#client = client\n this.#reconnect = options.reconnect ?? true\n // Deferred a tick for the same StrictMode reason as SessionHandle.\n this.#connectTimer = setTimeout(() => this.#connect(), 0)\n }\n\n on<K extends keyof QueueHandleEvents>(\n kind: K,\n listener: Listener<QueueHandleEvents[K]>,\n ): () => void {\n let set = this.#listeners.get(kind)\n if (!set) {\n set = new Set()\n this.#listeners.set(kind, set)\n }\n set.add(listener as Listener<never>)\n return () => set.delete(listener as Listener<never>)\n }\n\n detach(): void {\n this.#closed = true\n clearTimeout(this.#connectTimer)\n this.#ws?.close()\n this.#ws = undefined\n }\n\n #emit<K extends keyof QueueHandleEvents>(kind: K, payload: QueueHandleEvents[K]): void {\n const set = this.#listeners.get(kind)\n if (!set) return\n for (const listener of set) {\n try {\n ;(listener as Listener<QueueHandleEvents[K]>)(payload)\n } catch {\n // listener errors must not break the stream\n }\n }\n }\n\n #connect(): void {\n if (this.#closed) return\n const ws = this.#client.openQueueSocket()\n this.#ws = ws\n ws.onopen = () => {\n this.#retries = 0\n this.#emit('connectionChange', true)\n }\n ws.onmessage = (msg: MessageEvent) => {\n const frame = JSON.parse(String(msg.data)) as QueueServerFrame\n if (frame.type === 'queue_attached') {\n this.#emit('attached', frame.stats)\n this.#emit('stats', frame.stats)\n } else if (frame.type === 'job_event') {\n this.#emit('event', frame.event)\n } else if (frame.type === 'queue_stats') {\n this.#emit('stats', frame.stats)\n }\n }\n ws.onclose = () => {\n this.#emit('connectionChange', false)\n if (this.#closed || !this.#reconnect) return\n const delay = Math.min(500 * 2 ** this.#retries++, 10_000)\n this.#connectTimer = setTimeout(() => this.#connect(), delay)\n }\n ws.onerror = () => {\n // onclose follows; reconnect handled there\n }\n }\n}\n\nexport class WorkerDeckClient {\n #options: ClientOptions\n #fetch: typeof fetch\n #WebSocketImpl: typeof WebSocket\n\n constructor(options: ClientOptions) {\n this.#options = options\n this.#fetch = options.fetchImpl ?? fetch.bind(globalThis)\n this.#WebSocketImpl = options.WebSocketImpl ?? WebSocket\n }\n\n /**\n * Stable identity of the (gateway, principal) pair this client speaks as:\n * the base URL plus the auth headers it sends, order-insensitively.\n *\n * Exists for client-side caches that must survive the client *instance*\n * being rebuilt (a `useMemo` recreating it when a view switches gateways)\n * without ever sharing an entry across gateways — a session id is unique\n * only within one — or across credentials. Auth that rides outside\n * `headers` (a same-origin cookie, a fetch shim adding the key host-side)\n * is chosen per origin in every such host, so the base URL still separates\n * principals there; an embedder whose principal varies some other way on\n * one base URL should not key anything on this.\n */\n get identityKey(): string {\n const headers = Object.entries(this.#options.headers ?? {}).map(\n ([name, value]) => [name.toLowerCase(), value] as const,\n )\n headers.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n return JSON.stringify([this.#options.baseUrl, headers])\n }\n\n async createSession(request: CreateSessionRequest): Promise<SessionInfo> {\n const body = await this.#call('POST', '/sessions', request)\n return (body as { session: SessionInfo }).session\n }\n\n async listSessions(): Promise<SessionInfo[]> {\n const body = await this.#call('GET', '/sessions')\n return (body as { sessions: SessionInfo[] }).sessions\n }\n\n async getSession(id: string): Promise<SessionInfo> {\n const body = await this.#call('GET', `/sessions/${encodeURIComponent(id)}`)\n return (body as { session: SessionInfo }).session\n }\n\n /** Rename a session (or clear the name with `null`, restoring the derived\n * title). 409 when the session is parked. */\n async updateSession(id: string, patch: UpdateSessionRequest): Promise<SessionInfo> {\n const body = await this.#call('PATCH', `/sessions/${encodeURIComponent(id)}`, patch)\n return (body as { session: SessionInfo }).session\n }\n\n async deleteSession(id: string): Promise<SessionInfo> {\n const body = await this.#call('DELETE', `/sessions/${encodeURIComponent(id)}`)\n return (body as { session: SessionInfo }).session\n }\n\n /** List the files currently in a session's scratch filesystem (deliverables the\n * agent wrote; see the `file_delivered` event). 404s when the session's engine\n * has no file store (Claude-engine sessions). */\n async listSessionFiles(sessionId: string): Promise<SessionFileInfo[]> {\n const body = await this.#call('GET', `/sessions/${encodeURIComponent(sessionId)}/files`)\n return (body as ListSessionFilesResponse).files\n }\n\n /** Download one session file as text. */\n async fetchSessionFile(sessionId: string, path: string): Promise<string> {\n const res = await this.#fetch(this.sessionFileUrl(sessionId, path), {\n headers: this.#options.headers,\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(payload.error ?? `GET file failed with ${res.status}`, res.status)\n }\n return await res.text()\n }\n\n /**\n * Upload one file for the session, ahead of the message that will carry it.\n * The returned `id` goes to {@link SessionHandle.send}.\n *\n * The body is the raw bytes — no multipart — so anything `fetch` accepts as a\n * body works: a `File`/`Blob` from a picker, a `Uint8Array`, a string.\n */\n async uploadAttachment(\n sessionId: string,\n file: { name: string; mediaType: string; data: FetchBody },\n ): Promise<MessageAttachment> {\n const url = `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/attachments?name=${encodeURIComponent(file.name)}`\n const res = await this.#fetch(url, {\n method: 'POST',\n headers: { ...this.#options.headers, 'content-type': file.mediaType },\n body: file.data,\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(payload.error ?? `upload failed with ${res.status}`, res.status)\n }\n return ((await res.json()) as UploadAttachmentResponse).attachment\n }\n\n /** Direct URL for an uploaded attachment — an `<img src>` on a cookie-authenticated\n * same-origin server. Header-authenticated clients must fetch it themselves. */\n attachmentUrl(sessionId: string, attachmentId: string): string {\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(attachmentId)}`\n }\n\n /**\n * Direct URL for a file the session's ENGINE produced on the host — the\n * `fileId` of a `file_produced` event. Same caveat as `attachmentUrl`: usable\n * as an `<img src>` only where the credential is a same-origin cookie; a\n * header-authenticated client (the phone) fetches it and makes its own blob.\n *\n * Unlike `/fs/read`, this needs no host-file roots and no raised byte cap —\n * see the `file_produced` note in the protocol for why that is sound.\n */\n producedFileUrl(sessionId: string, fileId: string): string {\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/produced/${encodeURIComponent(fileId)}`\n }\n\n /** Fetch a produced file's bytes. For clients that cannot put a credential on\n * an `<img src>`. Throws {@link WorkerDeckError} with the response status —\n * a 404 means the file is gone from disk, not that the route is missing. */\n async readProducedFile(sessionId: string, fileId: string): Promise<Blob> {\n const res = await this.#fetch(this.producedFileUrl(sessionId, fileId), {\n headers: { ...this.#options.headers },\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(\n payload.error ?? `produced file request failed with ${res.status}`,\n res.status,\n )\n }\n return await res.blob()\n }\n\n /**\n * The URL behind a `ProjectIcon.image`. Session-scoped, like\n * {@link producedFileUrl}: the fetch rides the same `canSee` gate as every\n * other `/sessions/:id/*` route, and it takes **no path** — the gateway\n * serves whatever its own discovery resolved for this session's cwd.\n */\n projectIconUrl(sessionId: string): string {\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/project/icon`\n }\n\n /**\n * Fetch a project icon's bytes.\n *\n * Here rather than left to each client for the reason `readProducedFile`\n * exists, plus one this route makes sharper: a VS Code webview has **no\n * external `connect-src` at all**, so it cannot point an `<img src>` at a\n * gateway even in principle — the bytes have to come back through a bridged\n * fetch, which is exactly what this wraps. Three clients building the same\n * URL from `baseUrl` was the other half of the argument.\n *\n * Cache the result by `ProjectIcon.image.hash`, never by session: two\n * sessions in one project serve identical bytes, and the hash is on the wire\n * precisely so a client fetches once per project.\n *\n * A 404 is the uniform \"no icon\" — no project, a glyph-only project, or an\n * icon the gateway refused. It is deliberately not distinguishable, so treat\n * it as \"draw no image\", never as an error worth reporting.\n */\n async projectIcon(sessionId: string): Promise<Blob> {\n const res = await this.#fetch(this.projectIconUrl(sessionId), {\n headers: { ...this.#options.headers },\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(\n payload.error ?? `project icon request failed with ${res.status}`,\n res.status,\n )\n }\n return await res.blob()\n }\n\n /** The session's MCP servers and their tools, live from the engine. 501 when the\n * session's engine has no MCP surface; 409 while the session is parked. */\n async listMcpServers(sessionId: string): Promise<McpServerStatusInfo[]> {\n const body = await this.#call('GET', `/sessions/${encodeURIComponent(sessionId)}/mcp`)\n return (body as McpServersResponse).servers\n }\n\n /** Reconnect, enable or disable one MCP server; answers with the refreshed list. */\n async mcpServerAction(\n sessionId: string,\n serverName: string,\n action: McpServerActionRequest['action'],\n ): Promise<McpServerStatusInfo[]> {\n const body = await this.#call(\n 'POST',\n `/sessions/${encodeURIComponent(sessionId)}/mcp/${encodeURIComponent(serverName)}`,\n { action },\n )\n return (body as McpServersResponse).servers\n }\n\n /** Direct download URL for a session file (e.g. an <a download> href). Carries\n * no headers — on authenticated servers, use fetchSessionFile instead. */\n sessionFileUrl(sessionId: string, path: string): string {\n const encoded = path\n .split('/')\n .filter(Boolean)\n .map(encodeURIComponent)\n .join('/')\n return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/files/${encoded}`\n }\n\n /** Resolve a pending permission over REST — the remote-controller counterpart of the\n * WS `permission_decision` command (e.g. answering a job's AskUserQuestion from a\n * webhook consumer; the request rides on job_progress deliveries). Throws if the\n * request is unknown, already resolved, or expired. */\n async resolvePermission(\n sessionId: string,\n requestId: string,\n decision: ResolvePermissionRequest,\n ): Promise<void> {\n await this.#call(\n 'POST',\n `/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(requestId)}`,\n decision,\n )\n }\n\n /**\n * Deliver the result of a deferred tool execution — the callback a remote\n * worker (or a human) makes when the work a session parked on is done. The\n * session is rehydrated if its runner was torn down, and the agent loop\n * continues with this as the tool's output.\n *\n * Applied idempotently by `executionId`: a duplicate, or one racing the\n * execution watchdog, resolves with `applied: false` instead of applying twice.\n * Throws (404) when no session is waiting on that id.\n */\n async submitExecutionResult(\n executionId: string,\n result: SubmitExecutionResultRequest,\n ): Promise<SubmitExecutionResultResponse> {\n return (await this.#call(\n 'POST',\n `/executions/${encodeURIComponent(executionId)}/result`,\n result,\n )) as SubmitExecutionResultResponse\n }\n\n /** List the profiles (named Claude Code config dirs) this server declares, filtered\n * to what the caller may use. Feed a result's `name` to createSession({ profile }).\n * Servers predating profiles 404 here — catch and treat as none declared. */\n /** The profiles this caller may use, plus whether it may create new ones.\n * Each profile carries `managed: true` when it is store-backed and therefore\n * editable; profiles declared in server options are not. */\n async listProfiles(): Promise<ListProfilesResponse> {\n return (await this.#call('GET', '/profiles')) as ListProfilesResponse\n }\n\n /** One profile plus a fresh, view-only snapshot of its config directory (settings,\n * skills, agents, commands — env var names only, never values). */\n async getProfile(name: string): Promise<GetProfileResponse> {\n return (await this.#call('GET', `/profiles/${encodeURIComponent(name)}`)) as GetProfileResponse\n }\n\n /**\n * Create a managed profile. Requires a server with a profile store and a\n * principal allowed to manage profiles; 409 if the name is already taken by a\n * managed or a startup-declared profile.\n */\n async createProfile(profile: CreateProfileRequest): Promise<ProfileInfo> {\n const body = await this.#call('POST', '/profiles', profile)\n return (body as SaveProfileResponse).profile\n }\n\n /** Merge into a managed profile. The name is the route: profiles cannot be\n * renamed, since sessions and jobs are already pinned to the old one. */\n async updateProfile(name: string, patch: UpdateProfileRequest): Promise<ProfileInfo> {\n const body = await this.#call('PATCH', `/profiles/${encodeURIComponent(name)}`, patch)\n return (body as SaveProfileResponse).profile\n }\n\n /** Delete a managed profile. Startup-declared profiles are refused (403) —\n * they live in the server's options. */\n async deleteProfile(name: string): Promise<void> {\n await this.#call('DELETE', `/profiles/${encodeURIComponent(name)}`)\n }\n\n /** List an engine's on-disk sessions (for resume across server restarts).\n * Feed a result's `sessionId` to createSession({ resume }) — under a profile\n * of the same engine. `profile` names whose store to list (claude profiles →\n * the Agent SDK store, codex profiles → CODEX_HOME threads); absent, the\n * server resolves it implicitly when it declares exactly one profile, else\n * lists the Claude engine's store. */\n async listSdkSessions(params?: {\n dir?: string\n limit?: number\n offset?: number\n profile?: string\n }): Promise<SdkSessionSummary[]> {\n const search = new URLSearchParams()\n if (params?.dir) search.set('dir', params.dir)\n if (params?.limit !== undefined) search.set('limit', String(params.limit))\n if (params?.offset !== undefined) search.set('offset', String(params.offset))\n if (params?.profile) search.set('profile', params.profile)\n const qs = search.size > 0 ? `?${search.toString()}` : ''\n const body = await this.#call('GET', `/sdk-sessions${qs}`)\n return (body as { sdkSessions: SdkSessionSummary[] }).sdkSessions\n }\n\n // -- Host filesystem (requires the server to be configured with `hostFiles`) -\n\n /**\n * The host directories this server will let a client browse, and whether it\n * accepts writes. Servers without host-file access configured 404 here — catch\n * and treat as \"no file browser\", the same way `listProfiles` handles an older\n * server.\n *\n * These are operator-privileged routes: the auth key is the whole authorization\n * story, and they bypass the agent permission flow entirely. See the protocol\n * package's `HostFileRoot` for why that framing is deliberate.\n */\n async listHostRoots(): Promise<ListHostRootsResponse> {\n return (await this.#call('GET', '/fs/roots')) as ListHostRootsResponse\n }\n\n /** One host directory, not recursive. Symlinks are reported as symlinks, never\n * followed here — read one to find out whether it resolves somewhere allowed. */\n async listHostDir(path: string): Promise<ListHostDirResponse> {\n const qs = `?path=${encodeURIComponent(path)}`\n return (await this.#call('GET', `/fs/list${qs}`)) as ListHostDirResponse\n }\n\n /** Recursive fuzzy file search under one host directory — the `@file` picker's\n * query. Cheap enough to call per keystroke: build directories are skipped and\n * the walk is bounded, truncating rather than erroring. */\n async findHostFiles(path: string, query = '', limit?: number): Promise<FindHostFilesResponse> {\n const search = new URLSearchParams({ path, q: query })\n if (limit !== undefined) search.set('limit', String(limit))\n return (await this.#call('GET', `/fs/find?${search.toString()}`)) as FindHostFilesResponse\n }\n\n /** Read one host file. Binary content comes back base64-encoded; the returned\n * `hash` is what a later `writeHostFile` needs as its `expectedHash`. */\n async readHostFile(path: string): Promise<ReadHostFileResponse> {\n const qs = `?path=${encodeURIComponent(path)}`\n return (await this.#call('GET', `/fs/read${qs}`)) as ReadHostFileResponse\n }\n\n /**\n * Write one host file, conditionally — always. Pass the `hash` from the read this\n * edit is based on; a 409 means the agent (or anything else) changed the file\n * underneath you, and the edit must be rebased rather than forced. Omit\n * `expectedHash` only to create a file that does not exist yet.\n */\n async writeHostFile(request: WriteHostFileRequest): Promise<WriteHostFileResponse> {\n return (await this.#call('PUT', '/fs/write', request)) as WriteHostFileResponse\n }\n\n // -- Job queue (requires the server to be configured with `queue`) ----------\n\n /** Schedule a one-shot run. The returned job's `sessionId` (once running) can be\n * fed to `attach()` to watch the run live. */\n async createJob(request: CreateJobRequest): Promise<JobInfo> {\n const body = await this.#call('POST', '/jobs', request)\n return (body as { job: JobInfo }).job\n }\n\n async listJobs(): Promise<JobInfo[]> {\n const body = await this.#call('GET', '/jobs')\n return (body as { jobs: JobInfo[] }).jobs\n }\n\n async getJob(id: string): Promise<JobInfo> {\n const body = await this.#call('GET', `/jobs/${encodeURIComponent(id)}`)\n return (body as { job: JobInfo }).job\n }\n\n /** Cancel a queued or running job. */\n async cancelJob(id: string): Promise<JobInfo> {\n const body = await this.#call('DELETE', `/jobs/${encodeURIComponent(id)}`)\n return (body as { job: JobInfo }).job\n }\n\n async queueStats(): Promise<QueueStats> {\n const body = await this.#call('GET', '/queue')\n return (body as { stats: QueueStats }).stats\n }\n\n attach(sessionId: string, options?: AttachOptions): SessionHandle {\n return new SessionHandle(this, sessionId, options)\n }\n\n /** Stream the job queue live (requires the server to be configured with `queue`).\n * Servers without a queue refuse the socket — check REST first or expect retries. */\n attachQueue(options?: { reconnect?: boolean }): QueueHandle {\n return new QueueHandle(this, options)\n }\n\n /** @internal used by SessionHandle */\n openSocket(\n sessionId: string,\n afterSeq: number,\n truncateResults = false,\n imageRefs = false,\n ): WebSocket {\n // A third *optional* parameter rather than an options object, so every\n // existing `buildWsUrl` implementation still typechecks. The hazard worth\n // naming: a custom one that ignores it yields a full replay — safe only\n // because every client keys its rendering off the server's own `truncated`\n // marker and never off what it asked for.\n const query =\n `afterSeq=${afterSeq}` +\n (truncateResults ? '&truncateResults=1' : '') +\n (imageRefs ? '&imageRefs=1' : '')\n const url =\n this.#options.buildWsUrl?.(sessionId, afterSeq, truncateResults, imageRefs) ??\n `${this.#options.baseUrl.replace(/^http/, 'ws')}/sessions/${encodeURIComponent(sessionId)}/ws?${query}`\n return new this.#WebSocketImpl(url)\n }\n\n /**\n * The whole of a tool result whose replay delivered only its head.\n *\n * `toolUseId` is required and the gateway verifies it against the block: a\n * woken dormant session has a fresh log with fresh seqs, so a `sourceSeq`\n * cached across a gateway restart can name a different event, and being handed\n * another tool's output under the row you pressed is the exact failure this\n * feature exists to remove. A 404 here means \"ask again with a fresh attach\",\n * not \"empty\".\n */\n async toolResult(\n sessionId: string,\n seq: number,\n toolUseId: string,\n options?: { imageRefs?: boolean },\n ): Promise<{ seq: number; toolUseId: string; content: ToolResultBlock['content']; isError: boolean }> {\n // `imageRefs` matters here for the same reason it does on the socket: without\n // it, pressing \"show everything\" on an image-bearing result ships every\n // screenshot's base64 in the JSON, and the reducer keeps only the text.\n return (await this.#call(\n 'GET',\n `/sessions/${encodeURIComponent(sessionId)}/events/${seq}/result?toolUseId=${encodeURIComponent(toolUseId)}` +\n (options?.imageRefs ? '&imageRefs=1' : ''),\n )) as { seq: number; toolUseId: string; content: ToolResultBlock['content']; isError: boolean }\n }\n\n /**\n * One image part's bytes, addressed by the `image_ref` a replay delivered in\n * its place.\n *\n * A `Blob` and not a URL, and that is the whole reason this method exists: an\n * `<img src>` pointing at the gateway carries a credential in exactly one of\n * this project's four clients (the dashboard's same-origin implicit host,\n * where the cookie rides along). Everywhere else — an added cross-origin\n * gateway on a Bearer header, the VS Code webview whose every byte crosses a\n * postMessage bridge, iOS — the URL is unauthenticated and the picture is a\n * broken icon. Fetched rather than pointed at, then handed to\n * `URL.createObjectURL`; `readProducedFile` is the shipped precedent.\n *\n * A 404 means \"ask again with a fresh attach\": a woken dormant session has a\n * fresh log with fresh seqs, and the gateway refuses a stale address rather\n * than serving another call's pixels under the row you are looking at.\n */\n async toolResultImage(\n sessionId: string,\n seq: number,\n toolUseId: string,\n partIndex: number,\n ): Promise<Blob> {\n const path =\n `/sessions/${encodeURIComponent(sessionId)}/events/${seq}/result` +\n `?toolUseId=${encodeURIComponent(toolUseId)}&part=${partIndex}`\n const res = await this.#fetch(`${this.#options.baseUrl}${path}`, {\n headers: { ...this.#options.headers },\n })\n if (!res.ok) {\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n throw new WorkerDeckError(\n payload.error ?? `image part request failed with ${res.status}`,\n res.status,\n )\n }\n return await res.blob()\n }\n\n /** @internal used by QueueHandle */\n openQueueSocket(): WebSocket {\n const url =\n this.#options.buildQueueWsUrl?.() ??\n `${this.#options.baseUrl.replace(/^http/, 'ws')}/queue/ws`\n return new this.#WebSocketImpl(url)\n }\n\n async #call(method: string, path: string, body?: unknown): Promise<unknown> {\n const res = await this.#fetch(`${this.#options.baseUrl}${path}`, {\n method,\n headers: {\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...this.#options.headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n const payload = (await res.json().catch(() => ({}))) as { error?: string }\n if (!res.ok) {\n throw new WorkerDeckError(payload.error ?? `${method} ${path} failed with ${res.status}`, res.status)\n }\n return payload\n }\n}\n\nexport { apiUrl, isLoopbackHost } from './host-url.ts'\nexport type { HostUrl } from './host-url.ts'\nexport { hostAuth } from './host-auth.ts'\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerdeck/client",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "description": "Typed WorkerDeck protocol client for browsers and Node: REST session management plus a WebSocket attach with auto-reconnect and replay-from-last-seq. Uses the platform's fetch and WebSocket; zero runtime deps.",
6
6
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  }
18
18
  },
19
19
  "dependencies": {
20
- "@workerdeck/protocol": "0.16.0"
20
+ "@workerdeck/protocol": "0.18.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.10.0",
@@ -26,8 +26,8 @@
26
26
  "tsdown": "^0.21.10",
27
27
  "vitest": "^3.2.7",
28
28
  "ws": "^8.21.1",
29
- "@workerdeck/core": "0.16.0",
30
- "@workerdeck/server": "0.16.0"
29
+ "@workerdeck/core": "0.18.0",
30
+ "@workerdeck/server": "0.18.0"
31
31
  },
32
32
  "author": "Tobias Strebitzer",
33
33
  "repository": {