@tanstack/ai-opencode 0.1.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.
Files changed (46) hide show
  1. package/README.md +18 -0
  2. package/dist/esm/adapters/projection.d.ts +14 -0
  3. package/dist/esm/adapters/projection.js +83 -0
  4. package/dist/esm/adapters/projection.js.map +1 -0
  5. package/dist/esm/adapters/text.d.ts +47 -0
  6. package/dist/esm/adapters/text.js +231 -0
  7. package/dist/esm/adapters/text.js.map +1 -0
  8. package/dist/esm/index.d.ts +16 -0
  9. package/dist/esm/index.js +23 -0
  10. package/dist/esm/index.js.map +1 -0
  11. package/dist/esm/messages/prompt.d.ts +16 -0
  12. package/dist/esm/messages/prompt.js +37 -0
  13. package/dist/esm/messages/prompt.js.map +1 -0
  14. package/dist/esm/model-meta.d.ts +14 -0
  15. package/dist/esm/model-meta.js +13 -0
  16. package/dist/esm/model-meta.js.map +1 -0
  17. package/dist/esm/process/permissions.d.ts +50 -0
  18. package/dist/esm/process/permissions.js +51 -0
  19. package/dist/esm/process/permissions.js.map +1 -0
  20. package/dist/esm/process/sandbox-server.d.ts +23 -0
  21. package/dist/esm/process/sandbox-server.js +72 -0
  22. package/dist/esm/process/sandbox-server.js.map +1 -0
  23. package/dist/esm/process/server.d.ts +77 -0
  24. package/dist/esm/process/server.js +160 -0
  25. package/dist/esm/process/server.js.map +1 -0
  26. package/dist/esm/provider-options.d.ts +18 -0
  27. package/dist/esm/stream/queue.d.ts +18 -0
  28. package/dist/esm/stream/queue.js +56 -0
  29. package/dist/esm/stream/queue.js.map +1 -0
  30. package/dist/esm/stream/sdk-types.d.ts +134 -0
  31. package/dist/esm/stream/translate.d.ts +51 -0
  32. package/dist/esm/stream/translate.js +305 -0
  33. package/dist/esm/stream/translate.js.map +1 -0
  34. package/package.json +62 -0
  35. package/src/adapters/projection.ts +220 -0
  36. package/src/adapters/text.ts +368 -0
  37. package/src/index.ts +40 -0
  38. package/src/messages/prompt.ts +67 -0
  39. package/src/model-meta.ts +24 -0
  40. package/src/process/permissions.ts +119 -0
  41. package/src/process/sandbox-server.ts +130 -0
  42. package/src/process/server.ts +270 -0
  43. package/src/provider-options.ts +19 -0
  44. package/src/stream/queue.ts +64 -0
  45. package/src/stream/sdk-types.ts +104 -0
  46. package/src/stream/translate.ts +419 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Permission modes for the OpenCode adapter, mirroring the Claude Code and
3
+ * Gemini CLI adapters' semantics:
4
+ *
5
+ * - `'default'`: bridged TanStack tools run; anything else that asks for
6
+ * permission is rejected with no prompt (a headless server must never hang
7
+ * on an interactive question).
8
+ * - `'acceptEdits'`: additionally auto-approves file-mutation requests
9
+ * (edit / write / patch).
10
+ * - `'bypassPermissions'`: approves everything.
11
+ */
12
+ export type OpencodePermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
13
+ /** Structural subset of an OpenCode `permission.updated` payload. */
14
+ export interface OpencodePermissionRequest {
15
+ id: string;
16
+ sessionID: string;
17
+ /** Permission category, e.g. `'edit'`, `'bash'`, `'webfetch'`, a tool id. */
18
+ type: string;
19
+ title: string;
20
+ /** Tool call id this permission gates, when it gates a tool. */
21
+ callID?: string;
22
+ }
23
+ /** OpenCode permission reply: allow once, allow always, or reject. */
24
+ export type OpencodePermissionResponse = 'once' | 'always' | 'reject';
25
+ /** Custom permission handler; replaces the adapter's default policy. */
26
+ export type PermissionHandler = (request: OpencodePermissionRequest) => Promise<OpencodePermissionResponse> | OpencodePermissionResponse;
27
+ /**
28
+ * Decide whether an OpenCode permission request targets one of the bridged
29
+ * TanStack tools. OpenCode names MCP tools `<server>_<tool>` (e.g.
30
+ * `tanstack_lookup_user`), so a request is bridged when its type or title is
31
+ * a registered tool name, or carries the `tanstack` server prefix.
32
+ */
33
+ export declare function matchBridgedToolName(request: OpencodePermissionRequest, bridgedToolNames: ReadonlySet<string> | undefined): boolean;
34
+ /**
35
+ * The adapter's default permission policy. Always answers immediately — never
36
+ * hangs a headless server on a question only an interactive user could
37
+ * answer.
38
+ */
39
+ export declare function resolvePermission(request: OpencodePermissionRequest, mode: OpencodePermissionMode, bridgedToolNames: ReadonlySet<string> | undefined): OpencodePermissionResponse;
40
+ /**
41
+ * Interactive variant: when the mode/bridge policy would reject, consult the
42
+ * client's approval decisions. Returns the OpenCode response plus, when the
43
+ * action still needs a client decision, the `approvalId`/`title` the adapter
44
+ * should surface via an `approval-requested` event.
45
+ */
46
+ export declare function resolveInteractivePermission(request: OpencodePermissionRequest, mode: OpencodePermissionMode, bridgedToolNames: ReadonlySet<string> | undefined, approvals: ReadonlyMap<string, boolean> | undefined): {
47
+ response: OpencodePermissionResponse;
48
+ approvalId?: string;
49
+ title?: string;
50
+ };
@@ -0,0 +1,51 @@
1
+ import { approvalId } from "@tanstack/ai-sandbox";
2
+ const EDIT_TYPES = /* @__PURE__ */ new Set(["edit", "write", "patch"]);
3
+ function matchBridgedToolName(request, bridgedToolNames) {
4
+ if (!bridgedToolNames || bridgedToolNames.size === 0) return false;
5
+ for (const field of [request.type, request.title]) {
6
+ if (typeof field !== "string" || field === "") continue;
7
+ if (bridgedToolNames.has(field)) return true;
8
+ if (field.startsWith("tanstack_") && bridgedToolNames.has(field.slice(9))) {
9
+ return true;
10
+ }
11
+ if (field.startsWith("tanstack.") && bridgedToolNames.has(field.slice(9))) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+ function resolvePermission(request, mode, bridgedToolNames) {
18
+ if (matchBridgedToolName(request, bridgedToolNames)) {
19
+ return "once";
20
+ }
21
+ if (mode === "bypassPermissions") {
22
+ return "once";
23
+ }
24
+ if (mode === "acceptEdits" && EDIT_TYPES.has(request.type)) {
25
+ return "once";
26
+ }
27
+ return "reject";
28
+ }
29
+ function resolveInteractivePermission(request, mode, bridgedToolNames, approvals) {
30
+ if (matchBridgedToolName(request, bridgedToolNames))
31
+ return { response: "once" };
32
+ if (mode === "bypassPermissions") return { response: "once" };
33
+ if (mode === "acceptEdits" && EDIT_TYPES.has(request.type)) {
34
+ return { response: "once" };
35
+ }
36
+ const id = approvalId({
37
+ provider: "opencode",
38
+ kind: "tool",
39
+ target: request.type || request.title
40
+ });
41
+ const granted = approvals?.get(id);
42
+ if (granted === true) return { response: "once" };
43
+ if (granted === false) return { response: "reject" };
44
+ return { response: "reject", approvalId: id, title: request.title };
45
+ }
46
+ export {
47
+ matchBridgedToolName,
48
+ resolveInteractivePermission,
49
+ resolvePermission
50
+ };
51
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sources":["../../../src/process/permissions.ts"],"sourcesContent":["import { approvalId } from '@tanstack/ai-sandbox'\n\n/**\n * Permission modes for the OpenCode adapter, mirroring the Claude Code and\n * Gemini CLI adapters' semantics:\n *\n * - `'default'`: bridged TanStack tools run; anything else that asks for\n * permission is rejected with no prompt (a headless server must never hang\n * on an interactive question).\n * - `'acceptEdits'`: additionally auto-approves file-mutation requests\n * (edit / write / patch).\n * - `'bypassPermissions'`: approves everything.\n */\nexport type OpencodePermissionMode =\n | 'default'\n | 'acceptEdits'\n | 'bypassPermissions'\n\n/** Structural subset of an OpenCode `permission.updated` payload. */\nexport interface OpencodePermissionRequest {\n id: string\n sessionID: string\n /** Permission category, e.g. `'edit'`, `'bash'`, `'webfetch'`, a tool id. */\n type: string\n title: string\n /** Tool call id this permission gates, when it gates a tool. */\n callID?: string\n}\n\n/** OpenCode permission reply: allow once, allow always, or reject. */\nexport type OpencodePermissionResponse = 'once' | 'always' | 'reject'\n\n/** Custom permission handler; replaces the adapter's default policy. */\nexport type PermissionHandler = (\n request: OpencodePermissionRequest,\n) => Promise<OpencodePermissionResponse> | OpencodePermissionResponse\n\n/** Permission categories treated as file mutations for `'acceptEdits'`. */\nconst EDIT_TYPES = new Set(['edit', 'write', 'patch'])\n\n/**\n * Decide whether an OpenCode permission request targets one of the bridged\n * TanStack tools. OpenCode names MCP tools `<server>_<tool>` (e.g.\n * `tanstack_lookup_user`), so a request is bridged when its type or title is\n * a registered tool name, or carries the `tanstack` server prefix.\n */\nexport function matchBridgedToolName(\n request: OpencodePermissionRequest,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): boolean {\n if (!bridgedToolNames || bridgedToolNames.size === 0) return false\n for (const field of [request.type, request.title]) {\n if (typeof field !== 'string' || field === '') continue\n if (bridgedToolNames.has(field)) return true\n if (field.startsWith('tanstack_') && bridgedToolNames.has(field.slice(9))) {\n return true\n }\n if (field.startsWith('tanstack.') && bridgedToolNames.has(field.slice(9))) {\n return true\n }\n }\n return false\n}\n\n/**\n * The adapter's default permission policy. Always answers immediately — never\n * hangs a headless server on a question only an interactive user could\n * answer.\n */\nexport function resolvePermission(\n request: OpencodePermissionRequest,\n mode: OpencodePermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): OpencodePermissionResponse {\n if (matchBridgedToolName(request, bridgedToolNames)) {\n return 'once'\n }\n if (mode === 'bypassPermissions') {\n return 'once'\n }\n if (mode === 'acceptEdits' && EDIT_TYPES.has(request.type)) {\n return 'once'\n }\n return 'reject'\n}\n\n/**\n * Interactive variant: when the mode/bridge policy would reject, consult the\n * client's approval decisions. Returns the OpenCode response plus, when the\n * action still needs a client decision, the `approvalId`/`title` the adapter\n * should surface via an `approval-requested` event.\n */\nexport function resolveInteractivePermission(\n request: OpencodePermissionRequest,\n mode: OpencodePermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n approvals: ReadonlyMap<string, boolean> | undefined,\n): {\n response: OpencodePermissionResponse\n approvalId?: string\n title?: string\n} {\n if (matchBridgedToolName(request, bridgedToolNames))\n return { response: 'once' }\n if (mode === 'bypassPermissions') return { response: 'once' }\n if (mode === 'acceptEdits' && EDIT_TYPES.has(request.type)) {\n return { response: 'once' }\n }\n\n const id = approvalId({\n provider: 'opencode',\n kind: 'tool',\n target: request.type || request.title,\n })\n const granted = approvals?.get(id)\n if (granted === true) return { response: 'once' }\n if (granted === false) return { response: 'reject' }\n return { response: 'reject', approvalId: id, title: request.title }\n}\n"],"names":[],"mappings":";AAsCA,MAAM,aAAa,oBAAI,IAAI,CAAC,QAAQ,SAAS,OAAO,CAAC;AAQ9C,SAAS,qBACd,SACA,kBACS;AACT,MAAI,CAAC,oBAAoB,iBAAiB,SAAS,EAAG,QAAO;AAC7D,aAAW,SAAS,CAAC,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACjD,QAAI,OAAO,UAAU,YAAY,UAAU,GAAI;AAC/C,QAAI,iBAAiB,IAAI,KAAK,EAAG,QAAO;AACxC,QAAI,MAAM,WAAW,WAAW,KAAK,iBAAiB,IAAI,MAAM,MAAM,CAAC,CAAC,GAAG;AACzE,aAAO;AAAA,IACT;AACA,QAAI,MAAM,WAAW,WAAW,KAAK,iBAAiB,IAAI,MAAM,MAAM,CAAC,CAAC,GAAG;AACzE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,kBACd,SACA,MACA,kBAC4B;AAC5B,MAAI,qBAAqB,SAAS,gBAAgB,GAAG;AACnD,WAAO;AAAA,EACT;AACA,MAAI,SAAS,qBAAqB;AAChC,WAAO;AAAA,EACT;AACA,MAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,IAAI,GAAG;AAC1D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAQO,SAAS,6BACd,SACA,MACA,kBACA,WAKA;AACA,MAAI,qBAAqB,SAAS,gBAAgB;AAChD,WAAO,EAAE,UAAU,OAAA;AACrB,MAAI,SAAS,oBAAqB,QAAO,EAAE,UAAU,OAAA;AACrD,MAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,IAAI,GAAG;AAC1D,WAAO,EAAE,UAAU,OAAA;AAAA,EACrB;AAEA,QAAM,KAAK,WAAW;AAAA,IACpB,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ,QAAQ,QAAQ,QAAQ;AAAA,EAAA,CACjC;AACD,QAAM,UAAU,WAAW,IAAI,EAAE;AACjC,MAAI,YAAY,KAAM,QAAO,EAAE,UAAU,OAAA;AACzC,MAAI,YAAY,MAAO,QAAO,EAAE,UAAU,SAAA;AAC1C,SAAO,EAAE,UAAU,UAAU,YAAY,IAAI,OAAO,QAAQ,MAAA;AAC9D;"}
@@ -0,0 +1,23 @@
1
+ import { SandboxHandle } from '@tanstack/ai-sandbox';
2
+ export interface SandboxOpencodeServer {
3
+ /** URL the host uses to reach the in-sandbox server. */
4
+ baseUrl: string;
5
+ /**
6
+ * Headers that authenticate requests to {@link baseUrl}, when the provider's
7
+ * channel is token-gated (e.g. Daytona's `x-daytona-preview-token`). The host
8
+ * opencode client must send these on every request or the preview proxy 404s.
9
+ */
10
+ headers?: Record<string, string>;
11
+ /** Stop the server process. */
12
+ dispose: () => Promise<void>;
13
+ }
14
+ export interface StartServerOptions {
15
+ port: number;
16
+ hostname?: string;
17
+ cwd: string;
18
+ /** Extra env for the server process (e.g. `OPENCODE_CONFIG_CONTENT`). */
19
+ env?: Record<string, string>;
20
+ timeoutMs?: number;
21
+ signal?: AbortSignal;
22
+ }
23
+ export declare function startOpencodeServerInSandbox(sandbox: SandboxHandle, options: StartServerOptions): Promise<SandboxOpencodeServer>;
@@ -0,0 +1,72 @@
1
+ const READY_MARKER = "opencode server listening";
2
+ async function startOpencodeServerInSandbox(sandbox, options) {
3
+ const hostname = options.hostname ?? "0.0.0.0";
4
+ const command = `opencode serve --hostname=${hostname} --port=${options.port}`;
5
+ const proc = await sandbox.process.spawn(command, {
6
+ cwd: options.cwd,
7
+ ...options.env ? { env: options.env } : {},
8
+ ...options.signal ? { signal: options.signal } : {}
9
+ });
10
+ await waitForReady(proc, options.timeoutMs ?? 3e4);
11
+ const channel = await sandbox.ports.connect(options.port);
12
+ const headers = channel.headers ?? (channel.token ? { Authorization: `Bearer ${channel.token}` } : void 0);
13
+ return {
14
+ baseUrl: channel.url,
15
+ ...headers ? { headers } : {},
16
+ dispose: () => proc.kill()
17
+ };
18
+ }
19
+ function waitForReady(proc, timeoutMs) {
20
+ return new Promise((resolve, reject) => {
21
+ let stdout = "";
22
+ let stderr = "";
23
+ const state = { settled: false };
24
+ const settle = (fn) => {
25
+ if (state.settled) return;
26
+ state.settled = true;
27
+ clearTimeout(timer);
28
+ fn();
29
+ };
30
+ const diagnostics = () => [stdout, stderr].map((s) => s.trim()).filter(Boolean).join("\n").slice(-500);
31
+ const timer = setTimeout(
32
+ () => settle(
33
+ () => reject(
34
+ new Error(
35
+ `opencode serve did not become ready within ${timeoutMs}ms${diagnostics() ? `: ${diagnostics()}` : ""}`
36
+ )
37
+ )
38
+ ),
39
+ timeoutMs
40
+ );
41
+ void (async () => {
42
+ try {
43
+ for await (const chunk of proc.stderr) stderr += chunk;
44
+ } catch {
45
+ }
46
+ })();
47
+ void (async () => {
48
+ try {
49
+ for await (const chunk of proc.stdout) {
50
+ stdout += chunk;
51
+ if (stdout.includes(READY_MARKER)) {
52
+ settle(resolve);
53
+ return;
54
+ }
55
+ }
56
+ settle(
57
+ () => reject(
58
+ new Error(
59
+ `opencode serve exited before becoming ready${diagnostics() ? `: ${diagnostics()}` : " (no output)"}`
60
+ )
61
+ )
62
+ );
63
+ } catch (error) {
64
+ settle(() => reject(error));
65
+ }
66
+ })();
67
+ });
68
+ }
69
+ export {
70
+ startOpencodeServerInSandbox
71
+ };
72
+ //# sourceMappingURL=sandbox-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-server.js","sources":["../../../src/process/sandbox-server.ts"],"sourcesContent":["/**\n * Boot an `opencode serve` HTTP server INSIDE a sandbox and expose its port so\n * the host `@opencode-ai/sdk` client can connect over `baseUrl`. Mirrors the\n * SDK's own server launch (`opencode serve --hostname=H --port=P`, ready when\n * stdout logs `opencode server listening`).\n */\nimport type { SandboxHandle, SpawnHandle } from '@tanstack/ai-sandbox'\n\nconst READY_MARKER = 'opencode server listening'\n\nexport interface SandboxOpencodeServer {\n /** URL the host uses to reach the in-sandbox server. */\n baseUrl: string\n /**\n * Headers that authenticate requests to {@link baseUrl}, when the provider's\n * channel is token-gated (e.g. Daytona's `x-daytona-preview-token`). The host\n * opencode client must send these on every request or the preview proxy 404s.\n */\n headers?: Record<string, string>\n /** Stop the server process. */\n dispose: () => Promise<void>\n}\n\nexport interface StartServerOptions {\n port: number\n hostname?: string\n cwd: string\n /** Extra env for the server process (e.g. `OPENCODE_CONFIG_CONTENT`). */\n env?: Record<string, string>\n timeoutMs?: number\n signal?: AbortSignal\n}\n\nexport async function startOpencodeServerInSandbox(\n sandbox: SandboxHandle,\n options: StartServerOptions,\n): Promise<SandboxOpencodeServer> {\n const hostname = options.hostname ?? '0.0.0.0'\n const command = `opencode serve --hostname=${hostname} --port=${options.port}`\n const proc: SpawnHandle = await sandbox.process.spawn(command, {\n cwd: options.cwd,\n ...(options.env ? { env: options.env } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n })\n\n await waitForReady(proc, options.timeoutMs ?? 30_000)\n\n const channel = await sandbox.ports.connect(options.port)\n // Carry the channel's auth so the host client can reach a token-gated preview.\n // Prefer ready-made `headers`; fall back to a bearer token if that's all the\n // provider issued.\n const headers =\n channel.headers ??\n (channel.token ? { Authorization: `Bearer ${channel.token}` } : undefined)\n return {\n baseUrl: channel.url,\n ...(headers ? { headers } : {}),\n dispose: () => proc.kill(),\n }\n}\n\nfunction waitForReady(proc: SpawnHandle, timeoutMs: number): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let stdout = ''\n // Capture stderr too: opencode logs startup failures (e.g. \"address already\n // in use\" when a previous run's server still holds the port) to stderr, not\n // stdout. Without this the error message is empty and the real cause is lost.\n let stderr = ''\n // Holder object so reads stay typed as `boolean` across async closures\n // (a plain `let` gets flow-narrowed to a literal and trips lint).\n const state = { settled: false }\n const settle = (fn: () => void): void => {\n if (state.settled) return\n state.settled = true\n clearTimeout(timer)\n fn()\n }\n const diagnostics = (): string =>\n [stdout, stderr]\n .map((s) => s.trim())\n .filter(Boolean)\n .join('\\n')\n .slice(-500)\n const timer = setTimeout(\n () =>\n settle(() =>\n reject(\n new Error(\n `opencode serve did not become ready within ${timeoutMs}ms${\n diagnostics() ? `: ${diagnostics()}` : ''\n }`,\n ),\n ),\n ),\n timeoutMs,\n )\n\n // Drain stderr in the background so its text is available for diagnostics.\n void (async () => {\n try {\n for await (const chunk of proc.stderr) stderr += chunk\n } catch {\n // Ignore: stderr drain errors are non-fatal; stdout drives readiness.\n }\n })()\n\n void (async () => {\n try {\n for await (const chunk of proc.stdout) {\n stdout += chunk\n if (stdout.includes(READY_MARKER)) {\n settle(resolve)\n return\n }\n }\n settle(() =>\n reject(\n new Error(\n `opencode serve exited before becoming ready${\n diagnostics() ? `: ${diagnostics()}` : ' (no output)'\n }`,\n ),\n ),\n )\n } catch (error) {\n settle(() => reject(error))\n }\n })()\n })\n}\n"],"names":[],"mappings":"AAQA,MAAM,eAAe;AAyBrB,eAAsB,6BACpB,SACA,SACgC;AAChC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,UAAU,6BAA6B,QAAQ,WAAW,QAAQ,IAAI;AAC5E,QAAM,OAAoB,MAAM,QAAQ,QAAQ,MAAM,SAAS;AAAA,IAC7D,KAAK,QAAQ;AAAA,IACb,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAA,IAAQ,CAAA;AAAA,IACzC,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAA,IAAW,CAAA;AAAA,EAAC,CACpD;AAED,QAAM,aAAa,MAAM,QAAQ,aAAa,GAAM;AAEpD,QAAM,UAAU,MAAM,QAAQ,MAAM,QAAQ,QAAQ,IAAI;AAIxD,QAAM,UACJ,QAAQ,YACP,QAAQ,QAAQ,EAAE,eAAe,UAAU,QAAQ,KAAK,GAAA,IAAO;AAClE,SAAO;AAAA,IACL,SAAS,QAAQ;AAAA,IACjB,GAAI,UAAU,EAAE,QAAA,IAAY,CAAA;AAAA,IAC5B,SAAS,MAAM,KAAK,KAAA;AAAA,EAAK;AAE7B;AAEA,SAAS,aAAa,MAAmB,WAAkC;AACzE,SAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,QAAI,SAAS;AAIb,QAAI,SAAS;AAGb,UAAM,QAAQ,EAAE,SAAS,MAAA;AACzB,UAAM,SAAS,CAAC,OAAyB;AACvC,UAAI,MAAM,QAAS;AACnB,YAAM,UAAU;AAChB,mBAAa,KAAK;AAClB,SAAA;AAAA,IACF;AACA,UAAM,cAAc,MAClB,CAAC,QAAQ,MAAM,EACZ,IAAI,CAAC,MAAM,EAAE,MAAM,EACnB,OAAO,OAAO,EACd,KAAK,IAAI,EACT,MAAM,IAAI;AACf,UAAM,QAAQ;AAAA,MACZ,MACE;AAAA,QAAO,MACL;AAAA,UACE,IAAI;AAAA,YACF,8CAA8C,SAAS,KACrD,YAAA,IAAgB,KAAK,YAAA,CAAa,KAAK,EACzC;AAAA,UAAA;AAAA,QACF;AAAA,MACF;AAAA,MAEJ;AAAA,IAAA;AAIF,UAAM,YAAY;AAChB,UAAI;AACF,yBAAiB,SAAS,KAAK,OAAQ,WAAU;AAAA,MACnD,QAAQ;AAAA,MAER;AAAA,IACF,GAAA;AAEA,UAAM,YAAY;AAChB,UAAI;AACF,yBAAiB,SAAS,KAAK,QAAQ;AACrC,oBAAU;AACV,cAAI,OAAO,SAAS,YAAY,GAAG;AACjC,mBAAO,OAAO;AACd;AAAA,UACF;AAAA,QACF;AACA;AAAA,UAAO,MACL;AAAA,YACE,IAAI;AAAA,cACF,8CACE,gBAAgB,KAAK,YAAA,CAAa,KAAK,cACzC;AAAA,YAAA;AAAA,UACF;AAAA,QACF;AAAA,MAEJ,SAAS,OAAO;AACd,eAAO,MAAM,OAAO,KAAK,CAAC;AAAA,MAC5B;AAAA,IACF,GAAA;AAAA,EACF,CAAC;AACH;"}
@@ -0,0 +1,77 @@
1
+ import { Config } from '@opencode-ai/sdk';
2
+ import { OpencodeAssistantMessage, OpencodeEvent } from '../stream/sdk-types.js';
3
+ import { OpencodePermissionRequest, OpencodePermissionResponse } from './permissions.js';
4
+ /** A live OpenCode session backed by an `opencode serve` HTTP server. */
5
+ export interface OpencodeSessionHandle {
6
+ sessionId: string;
7
+ /** Whether an existing session was actually resumed. */
8
+ resumed: boolean;
9
+ /**
10
+ * Run one prompt turn. Resolves with the final assistant message (finish
11
+ * reason, token usage, error) and its concatenated text once the harness
12
+ * goes idle. Streaming deltas arrive via `onEvent` while this is pending.
13
+ */
14
+ prompt: (text: string) => Promise<{
15
+ message: OpencodeAssistantMessage;
16
+ text: string;
17
+ }>;
18
+ /** Ask the harness to abort the in-flight prompt turn. */
19
+ abort: () => Promise<void>;
20
+ /** Tear down the event subscription and (if owned) the server. */
21
+ dispose: () => Promise<void>;
22
+ }
23
+ export interface StartOpencodeSessionOptions {
24
+ /** Connect to an already-running server instead of spawning one. */
25
+ baseUrl?: string;
26
+ /**
27
+ * Headers attached to every request to the opencode server — used to
28
+ * authenticate a token-gated preview channel (e.g. Daytona). Without them a
29
+ * gated preview proxy rejects the requests (404 "Not found.").
30
+ */
31
+ headers?: Record<string, string>;
32
+ /** Hostname for the spawned server. Defaults to the SDK default. */
33
+ hostname?: string;
34
+ /** Port for the spawned server. Defaults to the SDK default. */
35
+ port?: number;
36
+ /**
37
+ * Directory the opencode HTTP API scopes the session to. Omit to use the
38
+ * server's own launch cwd (the common case): the server is spawned with the
39
+ * correct working dir per-provider, so passing a directory here is only needed
40
+ * to override it. Passing a VIRTUAL sandbox path (e.g. `/workspace`) is wrong
41
+ * for host-running providers (local-process), where that path doesn't exist —
42
+ * the API then stalls on it. Leave undefined and rely on the server cwd.
43
+ */
44
+ directory?: string;
45
+ /** Provider id (the part before `/` in the model id). */
46
+ providerID: string;
47
+ /** Model id (the part after `/` in the model id). */
48
+ modelID: string;
49
+ /** Extra OpenCode config merged with the adapter's mcp/permission config. */
50
+ config?: Config;
51
+ /** Baseline permission policy applied to the spawned server. */
52
+ permission?: Config['permission'];
53
+ /** MCP servers (e.g. the TanStack tool bridge) for the session. */
54
+ mcpServers?: Array<{
55
+ name: string;
56
+ url: string;
57
+ }>;
58
+ /** Session id to resume; falls back to a fresh session when not found. */
59
+ resumeSessionId?: string;
60
+ onEvent: (event: OpencodeEvent) => void;
61
+ onPermissionRequest: (request: OpencodePermissionRequest) => Promise<OpencodePermissionResponse> | OpencodePermissionResponse;
62
+ /** Called when the event subscription fails mid-turn. */
63
+ onError?: (error: unknown) => void;
64
+ }
65
+ /**
66
+ * Boot (or attach to) an OpenCode HTTP server, resolve a session, and wire its
67
+ * event subscription + permission replies.
68
+ *
69
+ * This module is the only place that touches `@opencode-ai/sdk`; the rest of
70
+ * the package works with the structural types in `sdk-types.ts`.
71
+ *
72
+ * Resume semantics: when `resumeSessionId` is set and the server still knows
73
+ * the session (same machine, same data dir), it is reused. Otherwise a fresh
74
+ * session is created and `resumed: false` tells the adapter to send the
75
+ * flattened transcript.
76
+ */
77
+ export declare function startOpencodeSession(options: StartOpencodeSessionOptions): Promise<OpencodeSessionHandle>;
@@ -0,0 +1,160 @@
1
+ import { createOpencodeClient, createOpencode } from "@opencode-ai/sdk";
2
+ function sessionIdOf(event) {
3
+ const props = event.properties;
4
+ if (props?.sessionID !== void 0) return props.sessionID;
5
+ if (event.type === "message.part.updated") {
6
+ return event.properties.part.sessionID;
7
+ }
8
+ if (event.type === "message.updated") {
9
+ return event.properties.info.sessionID;
10
+ }
11
+ if (event.type === "permission.updated") {
12
+ return event.properties.sessionID;
13
+ }
14
+ return void 0;
15
+ }
16
+ function buildConfig(options) {
17
+ const mcp = { ...options.config?.mcp };
18
+ for (const server of options.mcpServers ?? []) {
19
+ mcp[server.name] = { type: "remote", url: server.url, enabled: true };
20
+ }
21
+ return {
22
+ ...options.config,
23
+ ...Object.keys(mcp).length > 0 && { mcp },
24
+ ...options.permission !== void 0 && { permission: options.permission }
25
+ };
26
+ }
27
+ async function startOpencodeSession(options) {
28
+ const { directory } = options;
29
+ const dirQuery = directory !== void 0 ? { directory } : {};
30
+ let client;
31
+ let ownedServer;
32
+ if (options.baseUrl !== void 0) {
33
+ client = createOpencodeClient({
34
+ baseUrl: options.baseUrl,
35
+ ...options.headers !== void 0 && { headers: options.headers },
36
+ ...directory !== void 0 && { directory }
37
+ });
38
+ } else {
39
+ const config = buildConfig(options);
40
+ const result = await createOpencode({
41
+ ...options.hostname !== void 0 && { hostname: options.hostname },
42
+ ...options.port !== void 0 && { port: options.port },
43
+ ...Object.keys(config).length > 0 && { config }
44
+ });
45
+ client = result.client;
46
+ ownedServer = result.server;
47
+ }
48
+ const lifecycle = { disposed: false };
49
+ const teardown = async () => {
50
+ if (lifecycle.disposed) return;
51
+ lifecycle.disposed = true;
52
+ ownedServer?.close();
53
+ await Promise.resolve();
54
+ };
55
+ try {
56
+ let sessionId;
57
+ let resumed = false;
58
+ if (options.resumeSessionId !== void 0) {
59
+ const existing = await client.session.get({
60
+ path: { id: options.resumeSessionId },
61
+ query: dirQuery
62
+ });
63
+ if (existing.data) {
64
+ sessionId = options.resumeSessionId;
65
+ resumed = true;
66
+ }
67
+ }
68
+ if (sessionId === void 0) {
69
+ const created = await client.session.create({
70
+ query: dirQuery,
71
+ body: {},
72
+ throwOnError: true
73
+ });
74
+ sessionId = created.data.id;
75
+ }
76
+ const resolvedSessionId = sessionId;
77
+ const handlePermission = async (permission) => {
78
+ try {
79
+ const response = await options.onPermissionRequest({
80
+ id: permission.id,
81
+ sessionID: permission.sessionID,
82
+ type: permission.type,
83
+ title: permission.title,
84
+ ...permission.callID !== void 0 && { callID: permission.callID }
85
+ });
86
+ await client.postSessionIdPermissionsPermissionId({
87
+ path: { id: permission.sessionID, permissionID: permission.id },
88
+ query: dirQuery,
89
+ body: { response },
90
+ throwOnError: true
91
+ });
92
+ } catch (error) {
93
+ if (!lifecycle.disposed) options.onError?.(error);
94
+ }
95
+ };
96
+ const subscription = await client.event.subscribe();
97
+ const stream = subscription.stream;
98
+ void (async () => {
99
+ try {
100
+ for await (const event of stream) {
101
+ if (lifecycle.disposed) break;
102
+ const sid = sessionIdOf(event);
103
+ if (sid !== void 0 && sid !== resolvedSessionId) continue;
104
+ if (event.type === "permission.updated") {
105
+ void handlePermission(event.properties);
106
+ continue;
107
+ }
108
+ options.onEvent(event);
109
+ }
110
+ } catch (error) {
111
+ if (!lifecycle.disposed) options.onError?.(error);
112
+ }
113
+ })();
114
+ return {
115
+ sessionId: resolvedSessionId,
116
+ resumed,
117
+ prompt: async (text) => {
118
+ const result = await client.session.prompt({
119
+ path: { id: resolvedSessionId },
120
+ query: dirQuery,
121
+ body: {
122
+ model: { providerID: options.providerID, modelID: options.modelID },
123
+ parts: [{ type: "text", text }]
124
+ },
125
+ throwOnError: true
126
+ });
127
+ const data = result.data;
128
+ const message = data.info;
129
+ const responseText = data.parts.filter(
130
+ (part) => part.type === "text"
131
+ ).map((part) => part.text).join("");
132
+ return { message, text: responseText };
133
+ },
134
+ abort: async () => {
135
+ try {
136
+ await client.session.abort({
137
+ path: { id: resolvedSessionId },
138
+ query: dirQuery
139
+ });
140
+ } catch {
141
+ }
142
+ },
143
+ dispose: async () => {
144
+ lifecycle.disposed = true;
145
+ try {
146
+ await stream.return(void 0);
147
+ } catch {
148
+ }
149
+ ownedServer?.close();
150
+ }
151
+ };
152
+ } catch (error) {
153
+ await teardown();
154
+ throw error;
155
+ }
156
+ }
157
+ export {
158
+ startOpencodeSession
159
+ };
160
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sources":["../../../src/process/server.ts"],"sourcesContent":["import { createOpencode, createOpencodeClient } from '@opencode-ai/sdk'\nimport type { Config, Event, OpencodeClient, Part } from '@opencode-ai/sdk'\nimport type {\n OpencodeAssistantMessage,\n OpencodeEvent,\n} from '../stream/sdk-types'\nimport type {\n OpencodePermissionRequest,\n OpencodePermissionResponse,\n} from './permissions'\n\n/** A live OpenCode session backed by an `opencode serve` HTTP server. */\nexport interface OpencodeSessionHandle {\n sessionId: string\n /** Whether an existing session was actually resumed. */\n resumed: boolean\n /**\n * Run one prompt turn. Resolves with the final assistant message (finish\n * reason, token usage, error) and its concatenated text once the harness\n * goes idle. Streaming deltas arrive via `onEvent` while this is pending.\n */\n prompt: (\n text: string,\n ) => Promise<{ message: OpencodeAssistantMessage; text: string }>\n /** Ask the harness to abort the in-flight prompt turn. */\n abort: () => Promise<void>\n /** Tear down the event subscription and (if owned) the server. */\n dispose: () => Promise<void>\n}\n\nexport interface StartOpencodeSessionOptions {\n /** Connect to an already-running server instead of spawning one. */\n baseUrl?: string\n /**\n * Headers attached to every request to the opencode server — used to\n * authenticate a token-gated preview channel (e.g. Daytona). Without them a\n * gated preview proxy rejects the requests (404 \"Not found.\").\n */\n headers?: Record<string, string>\n /** Hostname for the spawned server. Defaults to the SDK default. */\n hostname?: string\n /** Port for the spawned server. Defaults to the SDK default. */\n port?: number\n /**\n * Directory the opencode HTTP API scopes the session to. Omit to use the\n * server's own launch cwd (the common case): the server is spawned with the\n * correct working dir per-provider, so passing a directory here is only needed\n * to override it. Passing a VIRTUAL sandbox path (e.g. `/workspace`) is wrong\n * for host-running providers (local-process), where that path doesn't exist —\n * the API then stalls on it. Leave undefined and rely on the server cwd.\n */\n directory?: string\n /** Provider id (the part before `/` in the model id). */\n providerID: string\n /** Model id (the part after `/` in the model id). */\n modelID: string\n /** Extra OpenCode config merged with the adapter's mcp/permission config. */\n config?: Config\n /** Baseline permission policy applied to the spawned server. */\n permission?: Config['permission']\n /** MCP servers (e.g. the TanStack tool bridge) for the session. */\n mcpServers?: Array<{ name: string; url: string }>\n /** Session id to resume; falls back to a fresh session when not found. */\n resumeSessionId?: string\n onEvent: (event: OpencodeEvent) => void\n onPermissionRequest: (\n request: OpencodePermissionRequest,\n ) => Promise<OpencodePermissionResponse> | OpencodePermissionResponse\n /** Called when the event subscription fails mid-turn. */\n onError?: (error: unknown) => void\n}\n\n/** Locate the session id an OpenCode event belongs to, when it carries one. */\nfunction sessionIdOf(event: Event): string | undefined {\n const props = event.properties as { sessionID?: string } | undefined\n if (props?.sessionID !== undefined) return props.sessionID\n if (event.type === 'message.part.updated') {\n return event.properties.part.sessionID\n }\n if (event.type === 'message.updated') {\n return event.properties.info.sessionID\n }\n if (event.type === 'permission.updated') {\n return event.properties.sessionID\n }\n return undefined\n}\n\nfunction buildConfig(options: StartOpencodeSessionOptions): Config {\n const mcp: NonNullable<Config['mcp']> = { ...options.config?.mcp }\n for (const server of options.mcpServers ?? []) {\n mcp[server.name] = { type: 'remote', url: server.url, enabled: true }\n }\n return {\n ...options.config,\n ...(Object.keys(mcp).length > 0 && { mcp }),\n ...(options.permission !== undefined && { permission: options.permission }),\n }\n}\n\n/**\n * Boot (or attach to) an OpenCode HTTP server, resolve a session, and wire its\n * event subscription + permission replies.\n *\n * This module is the only place that touches `@opencode-ai/sdk`; the rest of\n * the package works with the structural types in `sdk-types.ts`.\n *\n * Resume semantics: when `resumeSessionId` is set and the server still knows\n * the session (same machine, same data dir), it is reused. Otherwise a fresh\n * session is created and `resumed: false` tells the adapter to send the\n * flattened transcript.\n */\nexport async function startOpencodeSession(\n options: StartOpencodeSessionOptions,\n): Promise<OpencodeSessionHandle> {\n const { directory } = options\n // Spread into a `query` object only when set; omitting lets opencode use the\n // server's launch cwd (correct for every provider — see `directory` docs).\n const dirQuery = directory !== undefined ? { directory } : {}\n\n let client: OpencodeClient\n let ownedServer: { close: () => void } | undefined\n\n if (options.baseUrl !== undefined) {\n client = createOpencodeClient({\n baseUrl: options.baseUrl,\n ...(options.headers !== undefined && { headers: options.headers }),\n ...(directory !== undefined && { directory }),\n })\n } else {\n const config = buildConfig(options)\n const result = await createOpencode({\n ...(options.hostname !== undefined && { hostname: options.hostname }),\n ...(options.port !== undefined && { port: options.port }),\n ...(Object.keys(config).length > 0 && { config }),\n })\n client = result.client\n ownedServer = result.server\n }\n\n // Mutated from several closures (the subscription loop, dispose, teardown);\n // a holder object keeps reads typed as `boolean` rather than being\n // flow-narrowed to a literal across those boundaries.\n const lifecycle = { disposed: false }\n\n const teardown = async (): Promise<void> => {\n if (lifecycle.disposed) return\n lifecycle.disposed = true\n ownedServer?.close()\n await Promise.resolve()\n }\n\n try {\n // Resolve the session before subscribing so the event filter has an id.\n let sessionId: string | undefined\n let resumed = false\n if (options.resumeSessionId !== undefined) {\n const existing = await client.session.get({\n path: { id: options.resumeSessionId },\n query: dirQuery,\n })\n if (existing.data) {\n sessionId = options.resumeSessionId\n resumed = true\n }\n }\n if (sessionId === undefined) {\n const created = await client.session.create({\n query: dirQuery,\n body: {},\n throwOnError: true,\n })\n sessionId = created.data.id\n }\n const resolvedSessionId = sessionId\n\n const handlePermission = async (\n permission: Extract<Event, { type: 'permission.updated' }>['properties'],\n ): Promise<void> => {\n try {\n const response = await options.onPermissionRequest({\n id: permission.id,\n sessionID: permission.sessionID,\n type: permission.type,\n title: permission.title,\n ...(permission.callID !== undefined && { callID: permission.callID }),\n })\n await client.postSessionIdPermissionsPermissionId({\n path: { id: permission.sessionID, permissionID: permission.id },\n query: dirQuery,\n body: { response },\n throwOnError: true,\n })\n } catch (error) {\n if (!lifecycle.disposed) options.onError?.(error)\n }\n }\n\n const subscription = await client.event.subscribe()\n const stream = subscription.stream\n\n void (async () => {\n try {\n for await (const event of stream) {\n if (lifecycle.disposed) break\n const sid = sessionIdOf(event)\n if (sid !== undefined && sid !== resolvedSessionId) continue\n if (event.type === 'permission.updated') {\n void handlePermission(event.properties)\n continue\n }\n // The SDK event union is a structural superset of the subset the\n // translator consumes; unknown event types match no translator\n // branch and are ignored.\n options.onEvent(event as OpencodeEvent)\n }\n } catch (error) {\n if (!lifecycle.disposed) options.onError?.(error)\n }\n })()\n\n return {\n sessionId: resolvedSessionId,\n resumed,\n prompt: async (text: string) => {\n const result = await client.session.prompt({\n path: { id: resolvedSessionId },\n query: dirQuery,\n body: {\n model: { providerID: options.providerID, modelID: options.modelID },\n parts: [{ type: 'text', text }],\n },\n throwOnError: true,\n })\n const data = result.data\n const message = data.info as OpencodeAssistantMessage\n const responseText = data.parts\n .filter(\n (part): part is Extract<Part, { type: 'text' }> =>\n part.type === 'text',\n )\n .map((part) => part.text)\n .join('')\n return { message, text: responseText }\n },\n abort: async () => {\n try {\n await client.session.abort({\n path: { id: resolvedSessionId },\n query: dirQuery,\n })\n } catch {\n // Best-effort: the turn may already be finishing.\n }\n },\n dispose: async () => {\n lifecycle.disposed = true\n try {\n await stream.return(undefined)\n } catch {\n // Ignore: stream may already be closed.\n }\n ownedServer?.close()\n },\n }\n } catch (error) {\n await teardown()\n throw error\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { OpencodePermissionMode } from './process/permissions.js';
2
+ /**
3
+ * Per-call provider options for the OpenCode adapter, passed via
4
+ * `modelOptions` on `chat()`.
5
+ */
6
+ export interface OpencodeTextProviderOptions {
7
+ /**
8
+ * Resume an existing OpenCode session. The adapter emits the session id of
9
+ * every fresh run via a CUSTOM `opencode.session-id` stream event; thread
10
+ * it back here to continue that session (only the latest user message is
11
+ * sent — the harness already holds the prior context).
12
+ */
13
+ sessionId?: string;
14
+ /** Per-call override of the configured permission mode. */
15
+ permissionMode?: OpencodePermissionMode;
16
+ /** Per-call override of the harness working directory. */
17
+ directory?: string;
18
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Minimal promise-based async queue bridging the OpenCode event
3
+ * subscription's callback-style notifications into the async-iterable world
4
+ * the stream translator consumes.
5
+ */
6
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
7
+ private readonly values;
8
+ private readonly waiters;
9
+ private ended;
10
+ private error;
11
+ private failed;
12
+ push(value: T): void;
13
+ /** Signal normal completion; pending and future reads resolve as done. */
14
+ end(): void;
15
+ /** Signal failure; pending and future reads reject (after buffered values drain). */
16
+ fail(error: unknown): void;
17
+ [Symbol.asyncIterator](): AsyncIterator<T>;
18
+ }
@@ -0,0 +1,56 @@
1
+ class AsyncQueue {
2
+ values = [];
3
+ waiters = [];
4
+ ended = false;
5
+ error = void 0;
6
+ failed = false;
7
+ push(value) {
8
+ if (this.ended || this.failed) return;
9
+ const waiter = this.waiters.shift();
10
+ if (waiter) {
11
+ waiter.resolve({ value, done: false });
12
+ } else {
13
+ this.values.push(value);
14
+ }
15
+ }
16
+ /** Signal normal completion; pending and future reads resolve as done. */
17
+ end() {
18
+ if (this.ended || this.failed) return;
19
+ this.ended = true;
20
+ for (const waiter of this.waiters.splice(0)) {
21
+ waiter.resolve({ value: void 0, done: true });
22
+ }
23
+ }
24
+ /** Signal failure; pending and future reads reject (after buffered values drain). */
25
+ fail(error) {
26
+ if (this.ended || this.failed) return;
27
+ this.failed = true;
28
+ this.error = error;
29
+ for (const waiter of this.waiters.splice(0)) {
30
+ waiter.reject(error);
31
+ }
32
+ }
33
+ [Symbol.asyncIterator]() {
34
+ return {
35
+ next: () => {
36
+ if (this.values.length > 0) {
37
+ return Promise.resolve({
38
+ value: this.values.shift(),
39
+ done: false
40
+ });
41
+ }
42
+ if (this.failed) return Promise.reject(this.error);
43
+ if (this.ended) {
44
+ return Promise.resolve({ value: void 0, done: true });
45
+ }
46
+ return new Promise((resolve, reject) => {
47
+ this.waiters.push({ resolve, reject });
48
+ });
49
+ }
50
+ };
51
+ }
52
+ }
53
+ export {
54
+ AsyncQueue
55
+ };
56
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sources":["../../../src/stream/queue.ts"],"sourcesContent":["/**\n * Minimal promise-based async queue bridging the OpenCode event\n * subscription's callback-style notifications into the async-iterable world\n * the stream translator consumes.\n */\nexport class AsyncQueue<T> implements AsyncIterable<T> {\n private readonly values: Array<T> = []\n private readonly waiters: Array<{\n resolve: (result: IteratorResult<T>) => void\n reject: (error: unknown) => void\n }> = []\n private ended = false\n private error: unknown = undefined\n private failed = false\n\n push(value: T): void {\n if (this.ended || this.failed) return\n const waiter = this.waiters.shift()\n if (waiter) {\n waiter.resolve({ value, done: false })\n } else {\n this.values.push(value)\n }\n }\n\n /** Signal normal completion; pending and future reads resolve as done. */\n end(): void {\n if (this.ended || this.failed) return\n this.ended = true\n for (const waiter of this.waiters.splice(0)) {\n waiter.resolve({ value: undefined, done: true })\n }\n }\n\n /** Signal failure; pending and future reads reject (after buffered values drain). */\n fail(error: unknown): void {\n if (this.ended || this.failed) return\n this.failed = true\n this.error = error\n for (const waiter of this.waiters.splice(0)) {\n waiter.reject(error)\n }\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T> {\n return {\n next: (): Promise<IteratorResult<T>> => {\n if (this.values.length > 0) {\n return Promise.resolve({\n value: this.values.shift() as T,\n done: false,\n })\n }\n if (this.failed) return Promise.reject(this.error)\n if (this.ended) {\n return Promise.resolve({ value: undefined, done: true })\n }\n return new Promise((resolve, reject) => {\n this.waiters.push({ resolve, reject })\n })\n },\n }\n }\n}\n"],"names":[],"mappings":"AAKO,MAAM,WAA0C;AAAA,EACpC,SAAmB,CAAA;AAAA,EACnB,UAGZ,CAAA;AAAA,EACG,QAAQ;AAAA,EACR,QAAiB;AAAA,EACjB,SAAS;AAAA,EAEjB,KAAK,OAAgB;AACnB,QAAI,KAAK,SAAS,KAAK,OAAQ;AAC/B,UAAM,SAAS,KAAK,QAAQ,MAAA;AAC5B,QAAI,QAAQ;AACV,aAAO,QAAQ,EAAE,OAAO,MAAM,OAAO;AAAA,IACvC,OAAO;AACL,WAAK,OAAO,KAAK,KAAK;AAAA,IACxB;AAAA,EACF;AAAA;AAAA,EAGA,MAAY;AACV,QAAI,KAAK,SAAS,KAAK,OAAQ;AAC/B,SAAK,QAAQ;AACb,eAAW,UAAU,KAAK,QAAQ,OAAO,CAAC,GAAG;AAC3C,aAAO,QAAQ,EAAE,OAAO,QAAW,MAAM,MAAM;AAAA,IACjD;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,OAAsB;AACzB,QAAI,KAAK,SAAS,KAAK,OAAQ;AAC/B,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,eAAW,UAAU,KAAK,QAAQ,OAAO,CAAC,GAAG;AAC3C,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,CAAC,OAAO,aAAa,IAAsB;AACzC,WAAO;AAAA,MACL,MAAM,MAAkC;AACtC,YAAI,KAAK,OAAO,SAAS,GAAG;AAC1B,iBAAO,QAAQ,QAAQ;AAAA,YACrB,OAAO,KAAK,OAAO,MAAA;AAAA,YACnB,MAAM;AAAA,UAAA,CACP;AAAA,QACH;AACA,YAAI,KAAK,OAAQ,QAAO,QAAQ,OAAO,KAAK,KAAK;AACjD,YAAI,KAAK,OAAO;AACd,iBAAO,QAAQ,QAAQ,EAAE,OAAO,QAAW,MAAM,MAAM;AAAA,QACzD;AACA,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,eAAK,QAAQ,KAAK,EAAE,SAAS,QAAQ;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IAAA;AAAA,EAEJ;AACF;"}