@tanstack/ai-opencode 0.2.3 → 0.2.5

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.
@@ -1,13 +1,24 @@
1
- const OPENCODE_MODELS = [
2
- "anthropic/claude-opus-4-5",
3
- "anthropic/claude-sonnet-4-5",
4
- "openai/gpt-5.2",
5
- "openai/gpt-5.1-codex",
6
- "google/gemini-3-pro-preview",
7
- "opencode/claude-sonnet-4-5",
8
- "opencode/gpt-5.1-codex"
1
+ //#region src/model-meta.ts
2
+ /**
3
+ * Models known to work with OpenCode. OpenCode is provider-agnostic — it
4
+ * resolves any `provider/model` id its configured providers support (via the
5
+ * Vercel AI SDK + Models.dev), so this list exists for autocomplete. Any
6
+ * string is accepted via the `(string & {})` escape hatch in
7
+ * {@link OpencodeModel}.
8
+ *
9
+ * Models are addressed as `provider_id/model_id` (e.g.
10
+ * `anthropic/claude-sonnet-4-5`); the adapter splits on the first `/`.
11
+ */
12
+ var OPENCODE_MODELS = [
13
+ "anthropic/claude-opus-4-5",
14
+ "anthropic/claude-sonnet-4-5",
15
+ "openai/gpt-5.2",
16
+ "openai/gpt-5.1-codex",
17
+ "google/gemini-3-pro-preview",
18
+ "opencode/claude-sonnet-4-5",
19
+ "opencode/gpt-5.1-codex"
9
20
  ];
10
- export {
11
- OPENCODE_MODELS
12
- };
13
- //# sourceMappingURL=model-meta.js.map
21
+ //#endregion
22
+ export { OPENCODE_MODELS };
23
+
24
+ //# sourceMappingURL=model-meta.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-meta.js","sources":["../../src/model-meta.ts"],"sourcesContent":["/**\n * Models known to work with OpenCode. OpenCode is provider-agnostic — it\n * resolves any `provider/model` id its configured providers support (via the\n * Vercel AI SDK + Models.dev), so this list exists for autocomplete. Any\n * string is accepted via the `(string & {})` escape hatch in\n * {@link OpencodeModel}.\n *\n * Models are addressed as `provider_id/model_id` (e.g.\n * `anthropic/claude-sonnet-4-5`); the adapter splits on the first `/`.\n */\nexport const OPENCODE_MODELS = [\n 'anthropic/claude-opus-4-5',\n 'anthropic/claude-sonnet-4-5',\n 'openai/gpt-5.2',\n 'openai/gpt-5.1-codex',\n 'google/gemini-3-pro-preview',\n 'opencode/claude-sonnet-4-5',\n 'opencode/gpt-5.1-codex',\n] as const\n\nexport type KnownOpencodeModel = (typeof OPENCODE_MODELS)[number]\n\n/** Any `provider/model` id accepted by OpenCode; known ids get autocomplete. */\nexport type OpencodeModel = KnownOpencodeModel | (string & {})\n"],"names":[],"mappings":"AAUO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;"}
1
+ {"version":3,"file":"model-meta.js","names":[],"sources":["../../src/model-meta.ts"],"sourcesContent":["/**\n * Models known to work with OpenCode. OpenCode is provider-agnostic — it\n * resolves any `provider/model` id its configured providers support (via the\n * Vercel AI SDK + Models.dev), so this list exists for autocomplete. Any\n * string is accepted via the `(string & {})` escape hatch in\n * {@link OpencodeModel}.\n *\n * Models are addressed as `provider_id/model_id` (e.g.\n * `anthropic/claude-sonnet-4-5`); the adapter splits on the first `/`.\n */\nexport const OPENCODE_MODELS = [\n 'anthropic/claude-opus-4-5',\n 'anthropic/claude-sonnet-4-5',\n 'openai/gpt-5.2',\n 'openai/gpt-5.1-codex',\n 'google/gemini-3-pro-preview',\n 'opencode/claude-sonnet-4-5',\n 'opencode/gpt-5.1-codex',\n] as const\n\nexport type KnownOpencodeModel = (typeof OPENCODE_MODELS)[number]\n\n/** Any `provider/model` id accepted by OpenCode; known ids get autocomplete. */\nexport type OpencodeModel = KnownOpencodeModel | (string & {})\n"],"mappings":";;;;;;;;;;;AAUA,IAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
@@ -1,51 +1,63 @@
1
1
  import { approvalId } from "@tanstack/ai-sandbox";
2
- const EDIT_TYPES = /* @__PURE__ */ new Set(["edit", "write", "patch"]);
2
+ //#region src/process/permissions.ts
3
+ /** Permission categories treated as file mutations for `'acceptEdits'`. */
4
+ var EDIT_TYPES = /* @__PURE__ */ new Set([
5
+ "edit",
6
+ "write",
7
+ "patch"
8
+ ]);
9
+ /**
10
+ * Decide whether an OpenCode permission request targets one of the bridged
11
+ * TanStack tools. OpenCode names MCP tools `<server>_<tool>` (e.g.
12
+ * `tanstack_lookup_user`), so a request is bridged when its type or title is
13
+ * a registered tool name, or carries the `tanstack` server prefix.
14
+ */
3
15
  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
+ if (!bridgedToolNames || bridgedToolNames.size === 0) return false;
17
+ for (const field of [request.type, request.title]) {
18
+ if (typeof field !== "string" || field === "") continue;
19
+ if (bridgedToolNames.has(field)) return true;
20
+ if (field.startsWith("tanstack_") && bridgedToolNames.has(field.slice(9))) return true;
21
+ if (field.startsWith("tanstack.") && bridgedToolNames.has(field.slice(9))) return true;
22
+ }
23
+ return false;
16
24
  }
25
+ /**
26
+ * The adapter's default permission policy. Always answers immediately — never
27
+ * hangs a headless server on a question only an interactive user could
28
+ * answer.
29
+ */
17
30
  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";
31
+ if (matchBridgedToolName(request, bridgedToolNames)) return "once";
32
+ if (mode === "bypassPermissions") return "once";
33
+ if (mode === "acceptEdits" && EDIT_TYPES.has(request.type)) return "once";
34
+ return "reject";
28
35
  }
36
+ /**
37
+ * Interactive variant: when the mode/bridge policy would reject, consult the
38
+ * client's approval decisions. Returns the OpenCode response plus, when the
39
+ * action still needs a client decision, the `approvalId`/`title` the adapter
40
+ * should surface via an `approval-requested` event.
41
+ */
29
42
  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 };
43
+ if (matchBridgedToolName(request, bridgedToolNames)) return { response: "once" };
44
+ if (mode === "bypassPermissions") return { response: "once" };
45
+ if (mode === "acceptEdits" && EDIT_TYPES.has(request.type)) return { response: "once" };
46
+ const id = approvalId({
47
+ provider: "opencode",
48
+ kind: "tool",
49
+ target: request.type || request.title
50
+ });
51
+ const granted = approvals?.get(id);
52
+ if (granted === true) return { response: "once" };
53
+ if (granted === false) return { response: "reject" };
54
+ return {
55
+ response: "reject",
56
+ approvalId: id,
57
+ title: request.title
58
+ };
45
59
  }
46
- export {
47
- matchBridgedToolName,
48
- resolveInteractivePermission,
49
- resolvePermission
50
- };
51
- //# sourceMappingURL=permissions.js.map
60
+ //#endregion
61
+ export { matchBridgedToolName, resolveInteractivePermission, resolvePermission };
62
+
63
+ //# sourceMappingURL=permissions.js.map
@@ -1 +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;"}
1
+ {"version":3,"file":"permissions.js","names":[],"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"],"mappings":";;;AAsCA,IAAM,6BAAa,IAAI,IAAI;CAAC;CAAQ;CAAS;AAAO,CAAC;;;;;;;AAQrD,SAAgB,qBACd,SACA,kBACS;CACT,IAAI,CAAC,oBAAoB,iBAAiB,SAAS,GAAG,OAAO;CAC7D,KAAK,MAAM,SAAS,CAAC,QAAQ,MAAM,QAAQ,KAAK,GAAG;EACjD,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI;EAC/C,IAAI,iBAAiB,IAAI,KAAK,GAAG,OAAO;EACxC,IAAI,MAAM,WAAW,WAAW,KAAK,iBAAiB,IAAI,MAAM,MAAM,CAAC,CAAC,GACtE,OAAO;EAET,IAAI,MAAM,WAAW,WAAW,KAAK,iBAAiB,IAAI,MAAM,MAAM,CAAC,CAAC,GACtE,OAAO;CAEX;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,kBACd,SACA,MACA,kBAC4B;CAC5B,IAAI,qBAAqB,SAAS,gBAAgB,GAChD,OAAO;CAET,IAAI,SAAS,qBACX,OAAO;CAET,IAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,IAAI,GACvD,OAAO;CAET,OAAO;AACT;;;;;;;AAQA,SAAgB,6BACd,SACA,MACA,kBACA,WAKA;CACA,IAAI,qBAAqB,SAAS,gBAAgB,GAChD,OAAO,EAAE,UAAU,OAAO;CAC5B,IAAI,SAAS,qBAAqB,OAAO,EAAE,UAAU,OAAO;CAC5D,IAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,IAAI,GACvD,OAAO,EAAE,UAAU,OAAO;CAG5B,MAAM,KAAK,WAAW;EACpB,UAAU;EACV,MAAM;EACN,QAAQ,QAAQ,QAAQ,QAAQ;CAClC,CAAC;CACD,MAAM,UAAU,WAAW,IAAI,EAAE;CACjC,IAAI,YAAY,MAAM,OAAO,EAAE,UAAU,OAAO;CAChD,IAAI,YAAY,OAAO,OAAO,EAAE,UAAU,SAAS;CACnD,OAAO;EAAE,UAAU;EAAU,YAAY;EAAI,OAAO,QAAQ;CAAM;AACpE"}
@@ -1,72 +1,56 @@
1
- const READY_MARKER = "opencode server listening";
1
+ //#region src/process/sandbox-server.ts
2
+ var READY_MARKER = "opencode server listening";
2
3
  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
- };
4
+ const command = `opencode serve --hostname=${options.hostname ?? "0.0.0.0"} --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
18
  }
19
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
- });
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(() => settle(() => reject(/* @__PURE__ */ new Error(`opencode serve did not become ready within ${timeoutMs}ms${diagnostics() ? `: ${diagnostics()}` : ""}`))), timeoutMs);
32
+ (async () => {
33
+ try {
34
+ for await (const chunk of proc.stderr) stderr += chunk;
35
+ } catch {}
36
+ })();
37
+ (async () => {
38
+ try {
39
+ for await (const chunk of proc.stdout) {
40
+ stdout += chunk;
41
+ if (stdout.includes(READY_MARKER)) {
42
+ settle(resolve);
43
+ return;
44
+ }
45
+ }
46
+ settle(() => reject(/* @__PURE__ */ new Error(`opencode serve exited before becoming ready${diagnostics() ? `: ${diagnostics()}` : " (no output)"}`)));
47
+ } catch (error) {
48
+ settle(() => reject(error));
49
+ }
50
+ })();
51
+ });
68
52
  }
69
- export {
70
- startOpencodeServerInSandbox
71
- };
72
- //# sourceMappingURL=sandbox-server.js.map
53
+ //#endregion
54
+ export { startOpencodeServerInSandbox };
55
+
56
+ //# sourceMappingURL=sandbox-server.js.map
@@ -1 +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;"}
1
+ {"version":3,"file":"sandbox-server.js","names":[],"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"],"mappings":";AAQA,IAAM,eAAe;AAyBrB,eAAsB,6BACpB,SACA,SACgC;CAEhC,MAAM,UAAU,6BADC,QAAQ,YAAY,UACiB,UAAU,QAAQ;CACxE,MAAM,OAAoB,MAAM,QAAQ,QAAQ,MAAM,SAAS;EAC7D,KAAK,QAAQ;EACb,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;EAC1C,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;CACrD,CAAC;CAED,MAAM,aAAa,MAAM,QAAQ,aAAa,GAAM;CAEpD,MAAM,UAAU,MAAM,QAAQ,MAAM,QAAQ,QAAQ,IAAI;CAIxD,MAAM,UACJ,QAAQ,YACP,QAAQ,QAAQ,EAAE,eAAe,UAAU,QAAQ,QAAQ,IAAI,KAAA;CAClE,OAAO;EACL,SAAS,QAAQ;EACjB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC7B,eAAe,KAAK,KAAK;CAC3B;AACF;AAEA,SAAS,aAAa,MAAmB,WAAkC;CACzE,OAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,IAAI,SAAS;EAIb,IAAI,SAAS;EAGb,MAAM,QAAQ,EAAE,SAAS,MAAM;EAC/B,MAAM,UAAU,OAAyB;GACvC,IAAI,MAAM,SAAS;GACnB,MAAM,UAAU;GAChB,aAAa,KAAK;GAClB,GAAG;EACL;EACA,MAAM,oBACJ,CAAC,QAAQ,MAAM,CAAC,CACb,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,OAAO,OAAO,CAAC,CACf,KAAK,IAAI,CAAC,CACV,MAAM,IAAI;EACf,MAAM,QAAQ,iBAEV,aACE,uBACE,IAAI,MACF,8CAA8C,UAAU,IACtD,YAAY,IAAI,KAAK,YAAY,MAAM,IAE3C,CACF,CACF,GACF,SACF;EAGA,CAAM,YAAY;GAChB,IAAI;IACF,WAAW,MAAM,SAAS,KAAK,QAAQ,UAAU;GACnD,QAAQ,CAER;EACF,EAAA,CAAG;EAEH,CAAM,YAAY;GAChB,IAAI;IACF,WAAW,MAAM,SAAS,KAAK,QAAQ;KACrC,UAAU;KACV,IAAI,OAAO,SAAS,YAAY,GAAG;MACjC,OAAO,OAAO;MACd;KACF;IACF;IACA,aACE,uBACE,IAAI,MACF,8CACE,YAAY,IAAI,KAAK,YAAY,MAAM,gBAE3C,CACF,CACF;GACF,SAAS,OAAO;IACd,aAAa,OAAO,KAAK,CAAC;GAC5B;EACF,EAAA,CAAG;CACL,CAAC;AACH"}