@workerdeck/client 0.6.0 → 0.9.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/build/index.d.mts CHANGED
@@ -1,6 +1,10 @@
1
- import { AttachedFrame, CreateJobRequest, CreateProfileRequest, CreateSessionRequest, GetProfileResponse, JobEvent, JobInfo, ListProfilesResponse, PermissionMode, ProfileInfo, QueueStats, ResolvePermissionRequest, SdkSessionSummary, SessionEvent, SessionFileInfo, SessionInfo, SubmitExecutionResultRequest, SubmitExecutionResultResponse, ToolCallRequestFrame, ToolExecutionOutput, UpdateProfileRequest } 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, UpdateProfileRequest, WriteHostFileRequest, WriteHostFileResponse } from "@workerdeck/protocol";
2
2
 
3
3
  //#region src/index.d.ts
4
+ /** Whatever the ambient `fetch` accepts as a body — `Blob`/`File` in a browser,
5
+ * `Uint8Array` or a string in Node. Derived rather than named (`BodyInit` is a
6
+ * DOM-lib type, and this package compiles against both). */
7
+ type FetchBody = NonNullable<NonNullable<Parameters<typeof fetch>[1]>['body']>;
4
8
  type ClientOptions = {
5
9
  /** REST base, e.g. "http://127.0.0.1:8787/v1". The ws:// URL is derived from it. */baseUrl: string;
6
10
  /** Extra headers for REST calls (auth). Browsers can't set WS headers — use
@@ -41,7 +45,11 @@ declare class SessionHandle {
41
45
  constructor(client: WorkerDeckClient, sessionId: string, options?: AttachOptions);
42
46
  get lastSeq(): number;
43
47
  on<K extends keyof SessionHandleEvents>(kind: K, listener: Listener<SessionHandleEvents[K]>): () => void;
44
- send(text: string): void;
48
+ /** Send a message, optionally naming attachments uploaded ahead of it with
49
+ * {@link WorkerDeckClient.uploadAttachment} (ids in the order they should reach
50
+ * the model). An unknown id fails the whole command — the server will not send a
51
+ * message that quietly lost its picture. */
52
+ send(text: string, attachmentIds?: string[]): void;
45
53
  approve(requestId: string, updatedInput?: Record<string, unknown>): void;
46
54
  deny(requestId: string, message?: string, interrupt?: boolean): void;
47
55
  interrupt(): void;
@@ -90,6 +98,26 @@ declare class WorkerDeckClient {
90
98
  listSessionFiles(sessionId: string): Promise<SessionFileInfo[]>;
91
99
  /** Download one session file as text. */
92
100
  fetchSessionFile(sessionId: string, path: string): Promise<string>;
101
+ /**
102
+ * Upload one file for the session, ahead of the message that will carry it.
103
+ * The returned `id` goes to {@link SessionHandle.send}.
104
+ *
105
+ * The body is the raw bytes — no multipart — so anything `fetch` accepts as a
106
+ * body works: a `File`/`Blob` from a picker, a `Uint8Array`, a string.
107
+ */
108
+ uploadAttachment(sessionId: string, file: {
109
+ name: string;
110
+ mediaType: string;
111
+ data: FetchBody;
112
+ }): Promise<MessageAttachment>;
113
+ /** Direct URL for an uploaded attachment — an `<img src>` on a cookie-authenticated
114
+ * same-origin server. Header-authenticated clients must fetch it themselves. */
115
+ attachmentUrl(sessionId: string, attachmentId: string): string;
116
+ /** The session's MCP servers and their tools, live from the engine. 501 when the
117
+ * session's engine has no MCP surface; 409 while the session is parked. */
118
+ listMcpServers(sessionId: string): Promise<McpServerStatusInfo[]>;
119
+ /** Reconnect, enable or disable one MCP server; answers with the refreshed list. */
120
+ mcpServerAction(sessionId: string, serverName: string, action: McpServerActionRequest['action']): Promise<McpServerStatusInfo[]>;
93
121
  /** Direct download URL for a session file (e.g. an <a download> href). Carries
94
122
  * no headers — on authenticated servers, use fetchSessionFile instead. */
95
123
  sessionFileUrl(sessionId: string, path: string): string;
@@ -131,13 +159,46 @@ declare class WorkerDeckClient {
131
159
  /** Delete a managed profile. Startup-declared profiles are refused (403) —
132
160
  * they live in the server's options. */
133
161
  deleteProfile(name: string): Promise<void>;
134
- /** List the Agent SDK's on-disk sessions (for resume across server restarts).
135
- * Feed a result's `sessionId` to createSession({ resume }). */
162
+ /** List an engine's on-disk sessions (for resume across server restarts).
163
+ * Feed a result's `sessionId` to createSession({ resume }) — under a profile
164
+ * of the same engine. `profile` names whose store to list (claude profiles →
165
+ * the Agent SDK store, codex profiles → CODEX_HOME threads); absent, the
166
+ * server resolves it implicitly when it declares exactly one profile, else
167
+ * lists the Claude engine's store. */
136
168
  listSdkSessions(params?: {
137
169
  dir?: string;
138
170
  limit?: number;
139
171
  offset?: number;
172
+ profile?: string;
140
173
  }): Promise<SdkSessionSummary[]>;
174
+ /**
175
+ * The host directories this server will let a client browse, and whether it
176
+ * accepts writes. Servers without host-file access configured 404 here — catch
177
+ * and treat as "no file browser", the same way `listProfiles` handles an older
178
+ * server.
179
+ *
180
+ * These are operator-privileged routes: the auth key is the whole authorization
181
+ * story, and they bypass the agent permission flow entirely. See the protocol
182
+ * package's `HostFileRoot` for why that framing is deliberate.
183
+ */
184
+ listHostRoots(): Promise<ListHostRootsResponse>;
185
+ /** One host directory, not recursive. Symlinks are reported as symlinks, never
186
+ * followed here — read one to find out whether it resolves somewhere allowed. */
187
+ listHostDir(path: string): Promise<ListHostDirResponse>;
188
+ /** Recursive fuzzy file search under one host directory — the `@file` picker's
189
+ * query. Cheap enough to call per keystroke: build directories are skipped and
190
+ * the walk is bounded, truncating rather than erroring. */
191
+ findHostFiles(path: string, query?: string, limit?: number): Promise<FindHostFilesResponse>;
192
+ /** Read one host file. Binary content comes back base64-encoded; the returned
193
+ * `hash` is what a later `writeHostFile` needs as its `expectedHash`. */
194
+ readHostFile(path: string): Promise<ReadHostFileResponse>;
195
+ /**
196
+ * Write one host file, conditionally — always. Pass the `hash` from the read this
197
+ * edit is based on; a 409 means the agent (or anything else) changed the file
198
+ * underneath you, and the edit must be rebased rather than forced. Omit
199
+ * `expectedHash` only to create a file that does not exist yet.
200
+ */
201
+ writeHostFile(request: WriteHostFileRequest): Promise<WriteHostFileResponse>;
141
202
  /** Schedule a one-shot run. The returned job's `sessionId` (once running) can be
142
203
  * fed to `attach()` to watch the run live. */
143
204
  createJob(request: CreateJobRequest): Promise<JobInfo>;
@@ -158,5 +219,5 @@ declare class WorkerDeckClient {
158
219
  openQueueSocket(): WebSocket;
159
220
  }
160
221
  //#endregion
161
- export { AttachOptions, ClientOptions, QueueHandle, QueueHandleEvents, SessionHandle, SessionHandleEvents, WorkerDeckClient };
222
+ export { AttachOptions, ClientOptions, FetchBody, QueueHandle, QueueHandleEvents, SessionHandle, SessionHandleEvents, WorkerDeckClient };
162
223
  //# sourceMappingURL=index.d.mts.map
package/build/index.mjs CHANGED
@@ -32,10 +32,15 @@ var SessionHandle = class {
32
32
  set.add(listener);
33
33
  return () => set.delete(listener);
34
34
  }
35
- send(text) {
35
+ /** Send a message, optionally naming attachments uploaded ahead of it with
36
+ * {@link WorkerDeckClient.uploadAttachment} (ids in the order they should reach
37
+ * the model). An unknown id fails the whole command — the server will not send a
38
+ * message that quietly lost its picture. */
39
+ send(text, attachmentIds) {
36
40
  this.#sendFrame({
37
41
  type: "user_message",
38
- text
42
+ text,
43
+ attachmentIds: attachmentIds?.length ? attachmentIds : void 0
39
44
  });
40
45
  }
41
46
  approve(requestId, updatedInput) {
@@ -248,6 +253,43 @@ var WorkerDeckClient = class {
248
253
  }
249
254
  return await res.text();
250
255
  }
256
+ /**
257
+ * Upload one file for the session, ahead of the message that will carry it.
258
+ * The returned `id` goes to {@link SessionHandle.send}.
259
+ *
260
+ * The body is the raw bytes — no multipart — so anything `fetch` accepts as a
261
+ * body works: a `File`/`Blob` from a picker, a `Uint8Array`, a string.
262
+ */
263
+ async uploadAttachment(sessionId, file) {
264
+ const url = `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/attachments?name=${encodeURIComponent(file.name)}`;
265
+ const res = await this.#fetch(url, {
266
+ method: "POST",
267
+ headers: {
268
+ ...this.#options.headers,
269
+ "content-type": file.mediaType
270
+ },
271
+ body: file.data
272
+ });
273
+ if (!res.ok) {
274
+ const payload = await res.json().catch(() => ({}));
275
+ throw new Error(payload.error ?? `upload failed with ${res.status}`);
276
+ }
277
+ return (await res.json()).attachment;
278
+ }
279
+ /** Direct URL for an uploaded attachment — an `<img src>` on a cookie-authenticated
280
+ * same-origin server. Header-authenticated clients must fetch it themselves. */
281
+ attachmentUrl(sessionId, attachmentId) {
282
+ return `${this.#options.baseUrl}/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(attachmentId)}`;
283
+ }
284
+ /** The session's MCP servers and their tools, live from the engine. 501 when the
285
+ * session's engine has no MCP surface; 409 while the session is parked. */
286
+ async listMcpServers(sessionId) {
287
+ return (await this.#call("GET", `/sessions/${encodeURIComponent(sessionId)}/mcp`)).servers;
288
+ }
289
+ /** Reconnect, enable or disable one MCP server; answers with the refreshed list. */
290
+ async mcpServerAction(sessionId, serverName, action) {
291
+ return (await this.#call("POST", `/sessions/${encodeURIComponent(sessionId)}/mcp/${encodeURIComponent(serverName)}`, { action })).servers;
292
+ }
251
293
  /** Direct download URL for a session file (e.g. an <a download> href). Carries
252
294
  * no headers — on authenticated servers, use fetchSessionFile instead. */
253
295
  sessionFileUrl(sessionId, path) {
@@ -306,16 +348,66 @@ var WorkerDeckClient = class {
306
348
  async deleteProfile(name) {
307
349
  await this.#call("DELETE", `/profiles/${encodeURIComponent(name)}`);
308
350
  }
309
- /** List the Agent SDK's on-disk sessions (for resume across server restarts).
310
- * Feed a result's `sessionId` to createSession({ resume }). */
351
+ /** List an engine's on-disk sessions (for resume across server restarts).
352
+ * Feed a result's `sessionId` to createSession({ resume }) — under a profile
353
+ * of the same engine. `profile` names whose store to list (claude profiles →
354
+ * the Agent SDK store, codex profiles → CODEX_HOME threads); absent, the
355
+ * server resolves it implicitly when it declares exactly one profile, else
356
+ * lists the Claude engine's store. */
311
357
  async listSdkSessions(params) {
312
358
  const search = new URLSearchParams();
313
359
  if (params?.dir) search.set("dir", params.dir);
314
360
  if (params?.limit !== void 0) search.set("limit", String(params.limit));
315
361
  if (params?.offset !== void 0) search.set("offset", String(params.offset));
362
+ if (params?.profile) search.set("profile", params.profile);
316
363
  const qs = search.size > 0 ? `?${search.toString()}` : "";
317
364
  return (await this.#call("GET", `/sdk-sessions${qs}`)).sdkSessions;
318
365
  }
366
+ /**
367
+ * The host directories this server will let a client browse, and whether it
368
+ * accepts writes. Servers without host-file access configured 404 here — catch
369
+ * and treat as "no file browser", the same way `listProfiles` handles an older
370
+ * server.
371
+ *
372
+ * These are operator-privileged routes: the auth key is the whole authorization
373
+ * story, and they bypass the agent permission flow entirely. See the protocol
374
+ * package's `HostFileRoot` for why that framing is deliberate.
375
+ */
376
+ async listHostRoots() {
377
+ return await this.#call("GET", "/fs/roots");
378
+ }
379
+ /** One host directory, not recursive. Symlinks are reported as symlinks, never
380
+ * followed here — read one to find out whether it resolves somewhere allowed. */
381
+ async listHostDir(path) {
382
+ const qs = `?path=${encodeURIComponent(path)}`;
383
+ return await this.#call("GET", `/fs/list${qs}`);
384
+ }
385
+ /** Recursive fuzzy file search under one host directory — the `@file` picker's
386
+ * query. Cheap enough to call per keystroke: build directories are skipped and
387
+ * the walk is bounded, truncating rather than erroring. */
388
+ async findHostFiles(path, query = "", limit) {
389
+ const search = new URLSearchParams({
390
+ path,
391
+ q: query
392
+ });
393
+ if (limit !== void 0) search.set("limit", String(limit));
394
+ return await this.#call("GET", `/fs/find?${search.toString()}`);
395
+ }
396
+ /** Read one host file. Binary content comes back base64-encoded; the returned
397
+ * `hash` is what a later `writeHostFile` needs as its `expectedHash`. */
398
+ async readHostFile(path) {
399
+ const qs = `?path=${encodeURIComponent(path)}`;
400
+ return await this.#call("GET", `/fs/read${qs}`);
401
+ }
402
+ /**
403
+ * Write one host file, conditionally — always. Pass the `hash` from the read this
404
+ * edit is based on; a 409 means the agent (or anything else) changed the file
405
+ * underneath you, and the edit must be rebased rather than forced. Omit
406
+ * `expectedHash` only to create a file that does not exist yet.
407
+ */
408
+ async writeHostFile(request) {
409
+ return await this.#call("PUT", "/fs/write", request);
410
+ }
319
411
  /** Schedule a one-shot run. The returned job's `sessionId` (once running) can be
320
412
  * fed to `attach()` to watch the run live. */
321
413
  async createJob(request) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["#client","#options","#lastSeq","#connectTimer","#connect","#listeners","#sendFrame","#closed","#ws","#outbox","#retries","#emit","#reconnect","#fetch","#WebSocketImpl","#call"],"sources":["../src/index.ts"],"sourcesContent":["import type {\n AttachedFrame,\n ClientFrame,\n CreateJobRequest,\n CreateProfileRequest,\n CreateSessionRequest,\n JobEvent,\n JobInfo,\n GetProfileResponse,\n ListProfilesResponse,\n ListSessionFilesResponse,\n PermissionMode,\n ProfileInfo,\n QueueServerFrame,\n QueueStats,\n ResolvePermissionRequest,\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\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\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 * 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(text: string): void {\n this.#sendFrame({ type: 'user_message', text })\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 /** 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.#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 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 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 Error(payload.error ?? `GET file failed with ${res.status}`)\n }\n return await res.text()\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 the Agent SDK's on-disk sessions (for resume across server restarts).\n * Feed a result's `sessionId` to createSession({ resume }). */\n async listSdkSessions(params?: {\n dir?: string\n limit?: number\n offset?: number\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 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 // -- 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 Error(payload.error ?? `${method} ${path} failed with ${res.status}`)\n }\n return payload\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["#client","#options","#lastSeq","#connectTimer","#connect","#listeners","#sendFrame","#closed","#ws","#outbox","#retries","#emit","#reconnect","#fetch","#WebSocketImpl","#call"],"sources":["../src/index.ts"],"sourcesContent":["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 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\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 * 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 /** 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.#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 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 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 Error(payload.error ?? `GET file failed with ${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 Error(payload.error ?? `upload failed with ${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 /** 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 Error(payload.error ?? `${method} ${path} failed with ${res.status}`)\n }\n return payload\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workerdeck/client",
3
- "version": "0.6.0",
3
+ "version": "0.9.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.6.0"
20
+ "@workerdeck/protocol": "0.9.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/server": "0.6.0",
30
- "@workerdeck/core": "0.6.0"
29
+ "@workerdeck/server": "0.9.0",
30
+ "@workerdeck/core": "0.9.0"
31
31
  },
32
32
  "author": "Tobias Strebitzer",
33
33
  "repository": {