@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
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @tanstack/ai-opencode
2
+
3
+ OpenCode harness adapter for [TanStack AI](https://tanstack.com/ai) — run [OpenCode](https://opencode.ai) (via `@opencode-ai/sdk`) as a chat backend with local tool execution, token-level streaming, stateful sessions, and TanStack tool bridging.
4
+
5
+ ```typescript
6
+ import { chat } from '@tanstack/ai'
7
+ import { opencodeText } from '@tanstack/ai-opencode'
8
+
9
+ const stream = chat({
10
+ adapter: opencodeText('anthropic/claude-sonnet-4-5', {
11
+ directory: '/path/to/project',
12
+ permissionMode: 'acceptEdits',
13
+ }),
14
+ messages: [{ role: 'user', content: 'Fix the failing test.' }],
15
+ })
16
+ ```
17
+
18
+ Server-only (Node); requires the `opencode` CLI installed and authenticated. See the [OpenCode adapter docs](https://tanstack.com/ai/latest/docs/adapters/opencode) for sessions, tool bridging, permissions, and limitations.
@@ -0,0 +1,14 @@
1
+ import { SandboxHandle, WorkspaceProjection } from '@tanstack/ai-sandbox';
2
+ /**
3
+ * Project a `WorkspaceProjection` into the OpenCode sandbox. Safe to call on
4
+ * every `chatStream`. The secret-bearing `opencode.json` mcp section is
5
+ * (re)written on every call, re-resolving secrets, so OpenCode always reads
6
+ * current values and a snapshot can never serve a stale or rotated secret.
7
+ * The safe, idempotent, non-secret operations (gitSkill, agentSkill, plugins
8
+ * — all warn-and-skip for OpenCode) are guarded by a one-time marker so they
9
+ * produce at most one warning per sandbox lifetime.
10
+ *
11
+ * @param handle - The sandbox handle (`fs` + `process`).
12
+ * @param projection - The portable workspace inputs from `withSandbox`.
13
+ */
14
+ export declare function projectOpencodeWorkspace(handle: SandboxHandle, projection: WorkspaceProjection): Promise<void>;
@@ -0,0 +1,83 @@
1
+ import { resolveGitSkillDir, isSecretRef } from "@tanstack/ai-sandbox";
2
+ function isBearerMarker(value) {
3
+ return typeof value === "object" && value !== null && isSecretRef(value.__bearerRef);
4
+ }
5
+ function resolveHeaderValue(value, resolveSecret) {
6
+ if (isSecretRef(value)) return resolveSecret(value);
7
+ if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`;
8
+ return value;
9
+ }
10
+ function buildMcpSection(skills, resolveSecret) {
11
+ const mcp = {};
12
+ let count = 0;
13
+ for (const skill of skills) {
14
+ if (skill.kind !== "mcp") continue;
15
+ count += 1;
16
+ const headers = {};
17
+ const rawHeaders = skill.config.headers ?? {};
18
+ for (const [name, value] of Object.entries(rawHeaders)) {
19
+ headers[name] = resolveHeaderValue(value, resolveSecret);
20
+ }
21
+ const rawUrl = skill.config["url"];
22
+ const url = typeof rawUrl === "string" ? rawUrl : "";
23
+ mcp[skill.name] = { type: "remote", url, enabled: true, headers };
24
+ }
25
+ return count > 0 ? mcp : void 0;
26
+ }
27
+ async function projectMcpServers(handle, projection) {
28
+ const mcpSection = buildMcpSection(
29
+ projection.skills,
30
+ projection.resolveSecret
31
+ );
32
+ if (mcpSection === void 0) return;
33
+ const target = `${projection.root}/opencode.json`;
34
+ let existing = {};
35
+ if (await handle.fs.exists(target)) {
36
+ try {
37
+ const raw = await handle.fs.read(target);
38
+ const parsed = JSON.parse(raw);
39
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
40
+ existing = parsed;
41
+ }
42
+ } catch {
43
+ }
44
+ }
45
+ const merged = { ...existing, mcp: mcpSection };
46
+ await handle.fs.write(target, JSON.stringify(merged, null, 2));
47
+ }
48
+ function projectGitSkills(projection) {
49
+ for (const skill of projection.skills) {
50
+ if (skill.kind !== "git") continue;
51
+ const dir = skill.into ?? resolveGitSkillDir(projection.root, skill);
52
+ console.warn(
53
+ `[opencode] gitSkill "${skill.repo}" cloned to ${dir} but OpenCode has no recognised skills directory to link it into. The skill is available at that path — add an AGENTS.md reference to it manually if needed. Skipping.`
54
+ );
55
+ }
56
+ }
57
+ function projectAgentSkills(projection) {
58
+ for (const skill of projection.skills) {
59
+ if (skill.kind !== "agent-skill") continue;
60
+ console.warn(
61
+ `[opencode] agentSkill "${skill.name}" cannot be projected: OpenCode has no command to install a public skill by bare name. Provide it as a gitSkill instead. Skipping.`
62
+ );
63
+ }
64
+ }
65
+ function projectPlugins(projection) {
66
+ for (const name of projection.plugins) {
67
+ console.warn(
68
+ `[opencode] plugin "${name}" cannot be installed: OpenCode has no plugin install command. Skipping.`
69
+ );
70
+ }
71
+ }
72
+ async function projectOpencodeWorkspace(handle, projection) {
73
+ await projectMcpServers(handle, projection);
74
+ if (await handle.fs.exists(projection.markerPath)) return;
75
+ projectGitSkills(projection);
76
+ projectAgentSkills(projection);
77
+ projectPlugins(projection);
78
+ await handle.fs.write(projection.markerPath, "");
79
+ }
80
+ export {
81
+ projectOpencodeWorkspace
82
+ };
83
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.js","sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * OpenCode workspace projector — mirrors the Claude Code reference\n * implementation at `packages/ai-claude-code/src/adapters/projection.ts`.\n *\n * `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a\n * secret resolver, and a one-time marker path) via a capability. This adapter\n * reads it in `chatStream` setup and projects workspace inputs into OpenCode's\n * native format:\n *\n * - MCP servers → `opencode.json` at the workspace root (mcp section).\n * Written on EVERY call (never marker-gated) so rotated\n * secrets always re-apply.\n * - gitSkill repos → OpenCode has no recognised skills directory; we\n * warn-and-skip rather than invent a path.\n * - agentSkill → no bare-name primitive in opencode; warn-and-skip.\n * - plugins → opencode has no `opencode plugin install` command;\n * warn-and-skip.\n *\n * The `opencode.json` mcp-section shape mirrors the `OPENCODE_CONFIG_CONTENT`\n * shape already used by the adapter's host-tool-bridge, ensuring both\n * mechanisms use the same server-entry schema:\n *\n * ```json\n * { \"mcp\": { \"<name>\": { \"type\": \"remote\", \"url\": \"...\", \"enabled\": true,\n * \"headers\": { ... } } } }\n * ```\n *\n * OpenCode merges project-scoped `opencode.json` with its environment config,\n * so the workspace file coexists safely with the runtime `OPENCODE_CONFIG_CONTENT`\n * env written for the host-tool-bridge.\n *\n * External-convention caveat: the `opencode.json` location and the `remote`\n * MCP-entry shape are derived from the adapter's existing `OPENCODE_CONFIG_CONTENT`\n * usage. Where OpenCode has no clean primitive (gitSkill dir, agentSkill,\n * plugins) we no-op with a warning instead of fabricating a command.\n */\nimport { isSecretRef, resolveGitSkillDir } from '@tanstack/ai-sandbox'\nimport type {\n BearerRef,\n SandboxHandle,\n SecretRef,\n WorkspaceProjection,\n WorkspaceSkill,\n} from '@tanstack/ai-sandbox'\n\n/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */\nfunction isBearerMarker(value: unknown): value is BearerRef {\n return (\n typeof value === 'object' &&\n value !== null &&\n isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)\n )\n}\n\n/**\n * Resolve a single MCP header value: a `SecretRef` resolves to its plaintext,\n * a `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string\n * is passed through unchanged.\n */\nfunction resolveHeaderValue(\n value: string | SecretRef | BearerRef,\n resolveSecret: (ref: SecretRef) => string,\n): string {\n if (isSecretRef(value)) return resolveSecret(value)\n if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`\n return value\n}\n\n/** An OpenCode `remote`-type MCP server entry (mirrors OPENCODE_CONFIG_CONTENT shape). */\ninterface OpencodeMcpServer {\n type: 'remote'\n url: string\n enabled: boolean\n headers: Record<string, string>\n}\n\n/**\n * Build the `mcp` section of OpenCode's `opencode.json` from the\n * `{ kind: 'mcp' }` skills, resolving every header value (SecretRef / bearer\n * / string). Returns `undefined` when there are no MCP skills so the caller\n * can skip the write.\n */\nfunction buildMcpSection(\n skills: Array<WorkspaceSkill>,\n resolveSecret: (ref: SecretRef) => string,\n): Record<string, OpencodeMcpServer> | undefined {\n const mcp: Record<string, OpencodeMcpServer> = {}\n let count = 0\n for (const skill of skills) {\n if (skill.kind !== 'mcp') continue\n count += 1\n const headers: Record<string, string> = {}\n const rawHeaders = skill.config.headers ?? {}\n for (const [name, value] of Object.entries(rawHeaders)) {\n headers[name] = resolveHeaderValue(value, resolveSecret)\n }\n const rawUrl = skill.config['url']\n const url = typeof rawUrl === 'string' ? rawUrl : ''\n mcp[skill.name] = { type: 'remote', url, enabled: true, headers }\n }\n return count > 0 ? mcp : undefined\n}\n\n/**\n * Write (or merge-update) `opencode.json` at the workspace root with the\n * project-scoped MCP servers, re-resolving every secret. Runs on EVERY\n * projection call (never marker-gated) so rotated secrets always re-apply.\n *\n * If `opencode.json` already exists (e.g. committed to the repo) we read it,\n * merge only the `mcp` key, and rewrite, preserving other settings. When there\n * are no MCP skills the write is skipped entirely.\n */\nasync function projectMcpServers(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const mcpSection = buildMcpSection(\n projection.skills,\n projection.resolveSecret,\n )\n if (mcpSection === undefined) return\n\n const target = `${projection.root}/opencode.json`\n\n // Read the existing file if present so we can preserve non-mcp settings.\n let existing: Record<string, unknown> = {}\n if (await handle.fs.exists(target)) {\n try {\n const raw = await handle.fs.read(target)\n const parsed: unknown = JSON.parse(raw)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n !Array.isArray(parsed)\n ) {\n existing = parsed as Record<string, unknown>\n }\n } catch {\n // Unreadable or invalid JSON — start fresh so the MCP config lands cleanly.\n }\n }\n\n const merged = { ...existing, mcp: mcpSection }\n await handle.fs.write(target, JSON.stringify(merged, null, 2))\n}\n\n/**\n * OpenCode has no recognised skills directory where we can drop gitSkill repos.\n * AGENTS.md is already handled by the bootstrap layer (OpenCode reads it\n * natively). We warn-and-skip instead of inventing a path.\n */\nfunction projectGitSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'git') continue\n const dir = skill.into ?? resolveGitSkillDir(projection.root, skill)\n console.warn(\n `[opencode] gitSkill \"${skill.repo}\" cloned to ${dir} but OpenCode has no ` +\n 'recognised skills directory to link it into. The skill is available at that ' +\n 'path — add an AGENTS.md reference to it manually if needed. Skipping.',\n )\n }\n}\n\n/**\n * `agentSkill` references a public skill by bare name. OpenCode has no command\n * to fetch a skill from a bare name, so we warn and skip.\n */\nfunction projectAgentSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'agent-skill') continue\n console.warn(\n `[opencode] agentSkill \"${skill.name}\" cannot be projected: OpenCode has no ` +\n 'command to install a public skill by bare name. Provide it as a gitSkill ' +\n 'instead. Skipping.',\n )\n }\n}\n\n/**\n * OpenCode has no plugin install command. We warn-and-skip each declared plugin\n * rather than fabricating a command.\n */\nfunction projectPlugins(projection: WorkspaceProjection): void {\n for (const name of projection.plugins) {\n console.warn(\n `[opencode] plugin \"${name}\" cannot be installed: OpenCode has no plugin ` +\n 'install command. Skipping.',\n )\n }\n}\n\n/**\n * Project a `WorkspaceProjection` into the OpenCode sandbox. Safe to call on\n * every `chatStream`. The secret-bearing `opencode.json` mcp section is\n * (re)written on every call, re-resolving secrets, so OpenCode always reads\n * current values and a snapshot can never serve a stale or rotated secret.\n * The safe, idempotent, non-secret operations (gitSkill, agentSkill, plugins\n * — all warn-and-skip for OpenCode) are guarded by a one-time marker so they\n * produce at most one warning per sandbox lifetime.\n *\n * @param handle - The sandbox handle (`fs` + `process`).\n * @param projection - The portable workspace inputs from `withSandbox`.\n */\nexport async function projectOpencodeWorkspace(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n // Always re-resolve and rewrite the secret-bearing MCP config so rotated\n // secrets re-apply and snapshots can't serve stale values.\n await projectMcpServers(handle, projection)\n\n // Gate only the safe, idempotent, non-secret operations on the marker.\n if (await handle.fs.exists(projection.markerPath)) return\n\n projectGitSkills(projection)\n projectAgentSkills(projection)\n projectPlugins(projection)\n\n await handle.fs.write(projection.markerPath, '')\n}\n"],"names":[],"mappings":";AA8CA,SAAS,eAAe,OAAoC;AAC1D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAa,MAAoC,WAAW;AAEhE;AAOA,SAAS,mBACP,OACA,eACQ;AACR,MAAI,YAAY,KAAK,EAAG,QAAO,cAAc,KAAK;AAClD,MAAI,eAAe,KAAK,EAAG,QAAO,UAAU,cAAc,MAAM,WAAW,CAAC;AAC5E,SAAO;AACT;AAgBA,SAAS,gBACP,QACA,eAC+C;AAC/C,QAAM,MAAyC,CAAA;AAC/C,MAAI,QAAQ;AACZ,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,SAAS,MAAO;AAC1B,aAAS;AACT,UAAM,UAAkC,CAAA;AACxC,UAAM,aAAa,MAAM,OAAO,WAAW,CAAA;AAC3C,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACtD,cAAQ,IAAI,IAAI,mBAAmB,OAAO,aAAa;AAAA,IACzD;AACA,UAAM,SAAS,MAAM,OAAO,KAAK;AACjC,UAAM,MAAM,OAAO,WAAW,WAAW,SAAS;AAClD,QAAI,MAAM,IAAI,IAAI,EAAE,MAAM,UAAU,KAAK,SAAS,MAAM,QAAA;AAAA,EAC1D;AACA,SAAO,QAAQ,IAAI,MAAM;AAC3B;AAWA,eAAe,kBACb,QACA,YACe;AACf,QAAM,aAAa;AAAA,IACjB,WAAW;AAAA,IACX,WAAW;AAAA,EAAA;AAEb,MAAI,eAAe,OAAW;AAE9B,QAAM,SAAS,GAAG,WAAW,IAAI;AAGjC,MAAI,WAAoC,CAAA;AACxC,MAAI,MAAM,OAAO,GAAG,OAAO,MAAM,GAAG;AAClC,QAAI;AACF,YAAM,MAAM,MAAM,OAAO,GAAG,KAAK,MAAM;AACvC,YAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,UACE,OAAO,WAAW,YAClB,WAAW,QACX,CAAC,MAAM,QAAQ,MAAM,GACrB;AACA,mBAAW;AAAA,MACb;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,EAAE,GAAG,UAAU,KAAK,WAAA;AACnC,QAAM,OAAO,GAAG,MAAM,QAAQ,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC/D;AAOA,SAAS,iBAAiB,YAAuC;AAC/D,aAAW,SAAS,WAAW,QAAQ;AACrC,QAAI,MAAM,SAAS,MAAO;AAC1B,UAAM,MAAM,MAAM,QAAQ,mBAAmB,WAAW,MAAM,KAAK;AACnE,YAAQ;AAAA,MACN,wBAAwB,MAAM,IAAI,eAAe,GAAG;AAAA,IAAA;AAAA,EAIxD;AACF;AAMA,SAAS,mBAAmB,YAAuC;AACjE,aAAW,SAAS,WAAW,QAAQ;AACrC,QAAI,MAAM,SAAS,cAAe;AAClC,YAAQ;AAAA,MACN,0BAA0B,MAAM,IAAI;AAAA,IAAA;AAAA,EAIxC;AACF;AAMA,SAAS,eAAe,YAAuC;AAC7D,aAAW,QAAQ,WAAW,SAAS;AACrC,YAAQ;AAAA,MACN,sBAAsB,IAAI;AAAA,IAAA;AAAA,EAG9B;AACF;AAcA,eAAsB,yBACpB,QACA,YACe;AAGf,QAAM,kBAAkB,QAAQ,UAAU;AAG1C,MAAI,MAAM,OAAO,GAAG,OAAO,WAAW,UAAU,EAAG;AAEnD,mBAAiB,UAAU;AAC3B,qBAAmB,UAAU;AAC7B,iBAAe,UAAU;AAEzB,QAAM,OAAO,GAAG,MAAM,WAAW,YAAY,EAAE;AACjD;"}
@@ -0,0 +1,47 @@
1
+ import { BaseTextAdapter, StructuredOutputOptions, StructuredOutputResult } from '@tanstack/ai/adapters';
2
+ import { SandboxHandle } from '@tanstack/ai-sandbox';
3
+ import { DefaultMessageMetadataByModality, Modality, StreamChunk, TextOptions } from '@tanstack/ai';
4
+ import { OpencodePermissionMode, PermissionHandler } from '../process/permissions.js';
5
+ import { OpencodeModel } from '../model-meta.js';
6
+ import { OpencodeTextProviderOptions } from '../provider-options.js';
7
+ export interface OpencodeTextConfig {
8
+ /** Working directory inside the sandbox. Defaults to `/workspace`. */
9
+ directory?: string;
10
+ /**
11
+ * Port the in-sandbox `opencode serve` listens on. Defaults to 4096. For the
12
+ * Docker provider this port must also be published (`publishPorts: [4096]`)
13
+ * so the host can reach it.
14
+ */
15
+ port?: number;
16
+ /** Hostname the in-sandbox server binds. Defaults to `0.0.0.0`. */
17
+ hostname?: string;
18
+ /**
19
+ * OpenCode permission mode driving the dynamic permission handler. Defaults
20
+ * to `'default'`; set `'acceptEdits'` / `'bypassPermissions'` to let the
21
+ * harness edit files and run commands autonomously inside the sandbox.
22
+ */
23
+ permissionMode?: OpencodePermissionMode;
24
+ /** Custom permission handler; replaces the adapter's default policy. */
25
+ onPermissionRequest?: PermissionHandler;
26
+ }
27
+ export declare class OpencodeTextAdapter<TModel extends OpencodeModel> extends BaseTextAdapter<TModel, OpencodeTextProviderOptions, ReadonlyArray<Modality> & readonly ['text'], DefaultMessageMetadataByModality, ReadonlyArray<string>, unknown, never> {
28
+ readonly name: "opencode";
29
+ readonly requires: readonly [import('@tanstack/ai').Capability<SandboxHandle, "sandbox">];
30
+ private readonly adapterConfig;
31
+ constructor(config: OpencodeTextConfig, model: TModel);
32
+ private sandboxFrom;
33
+ private applySystemPrompts;
34
+ chatStream(options: TextOptions<OpencodeTextProviderOptions>): AsyncIterable<StreamChunk>;
35
+ structuredOutput(_options: StructuredOutputOptions<OpencodeTextProviderOptions>): Promise<StructuredOutputResult<unknown>>;
36
+ }
37
+ /**
38
+ * Creates an OpenCode harness adapter that runs **inside a sandbox**.
39
+ *
40
+ * It declares `requires: [SandboxCapability]`, spawns `opencode serve` inside
41
+ * the sandbox provided by `withSandbox(...)`, exposes its port, and connects
42
+ * the `@opencode-ai/sdk` HTTP client to it. OpenCode owns the agent loop and
43
+ * executes its native tools against the sandbox workspace. The sandbox image
44
+ * must provide the `opencode` executable (Docker: also publish the server port
45
+ * via `publishPorts`). chat()-provided tools aren't bridged yet.
46
+ */
47
+ export declare function opencodeText<TModel extends OpencodeModel>(model: TModel, config?: OpencodeTextConfig): OpencodeTextAdapter<TModel>;
@@ -0,0 +1,231 @@
1
+ import { normalizeSystemPrompts, EventType } from "@tanstack/ai";
2
+ import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
3
+ import { BaseTextAdapter } from "@tanstack/ai/adapters";
4
+ import { SandboxCapability, getSandbox, createBridgeEventChannel, getWorkspaceProjection, getToolBridgeProvisioner, nodeHttpBridgeProvisioner, buildApprovalRequestedEvent, mergeChunkStreams } from "@tanstack/ai-sandbox";
5
+ import { buildPrompt } from "../messages/prompt.js";
6
+ import { startOpencodeSession } from "../process/server.js";
7
+ import { startOpencodeServerInSandbox } from "../process/sandbox-server.js";
8
+ import { resolveInteractivePermission } from "../process/permissions.js";
9
+ import { AsyncQueue } from "../stream/queue.js";
10
+ import { translateOpencodeStream } from "../stream/translate.js";
11
+ import { projectOpencodeWorkspace } from "./projection.js";
12
+ const DEFAULT_WORKDIR = "/workspace";
13
+ const DEFAULT_PORT = 4096;
14
+ function splitModel(model) {
15
+ const slash = model.indexOf("/");
16
+ if (slash <= 0 || slash === model.length - 1) {
17
+ throw new Error(
18
+ `OpenCode models must be addressed as "provider/model" (e.g. "anthropic/claude-sonnet-4-5"); received "${model}".`
19
+ );
20
+ }
21
+ return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
22
+ }
23
+ class OpencodeTextAdapter extends BaseTextAdapter {
24
+ name = "opencode";
25
+ requires = [SandboxCapability];
26
+ adapterConfig;
27
+ constructor(config, model) {
28
+ super({}, model);
29
+ this.adapterConfig = config;
30
+ }
31
+ sandboxFrom(options) {
32
+ const ctx = options.capabilities;
33
+ if (!ctx) {
34
+ throw new Error(
35
+ 'Adapter "opencode" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.'
36
+ );
37
+ }
38
+ return getSandbox(ctx);
39
+ }
40
+ applySystemPrompts(options, prompt) {
41
+ const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((systemPrompt) => systemPrompt.content).filter((content) => content.trim() !== "");
42
+ if (systemPrompts.length === 0) return prompt;
43
+ return `${systemPrompts.join("\n\n")}
44
+
45
+ ${prompt}`;
46
+ }
47
+ async *chatStream(options) {
48
+ const { logger } = options;
49
+ let server;
50
+ let handle;
51
+ let bridge;
52
+ const externalSignal = options.abortController?.signal ?? options.request?.signal ?? void 0;
53
+ let onAbort;
54
+ const runId = options.runId ?? this.generateId();
55
+ const threadId = options.threadId ?? this.generateId();
56
+ const channel = createBridgeEventChannel({
57
+ model: this.model,
58
+ threadId,
59
+ runId
60
+ });
61
+ try {
62
+ const sandbox = this.sandboxFrom(options);
63
+ const directory = options.modelOptions?.directory ?? this.adapterConfig.directory ?? DEFAULT_WORKDIR;
64
+ if (options.capabilities !== void 0) {
65
+ const projection = getWorkspaceProjection(options.capabilities, {
66
+ optional: true
67
+ });
68
+ if (projection !== void 0) {
69
+ await projectOpencodeWorkspace(sandbox, projection);
70
+ }
71
+ }
72
+ const modelOptions = options.modelOptions;
73
+ const sessionId = modelOptions?.sessionId;
74
+ const { prompt: resumePrompt } = buildPrompt(options.messages, sessionId);
75
+ const { providerID, modelID } = splitModel(this.model);
76
+ const bridgedToolNames = new Set(
77
+ (options.tools ?? []).map((tool) => tool.name)
78
+ );
79
+ if (options.tools && options.tools.length > 0) {
80
+ const provisioner = (options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner;
81
+ bridge = await provisioner.provision(options.tools, {
82
+ provider: sandbox.provider,
83
+ context: options.context,
84
+ emitCustomEvent: channel.emitCustomEvent,
85
+ ...externalSignal ? { signal: externalSignal } : {}
86
+ });
87
+ }
88
+ const approvalRequests = [];
89
+ const queue = new AsyncQueue();
90
+ const mode = modelOptions?.permissionMode ?? this.adapterConfig.permissionMode ?? "default";
91
+ const permissionHandler = this.adapterConfig.onPermissionRequest ?? ((request) => {
92
+ const result = resolveInteractivePermission(
93
+ request,
94
+ mode,
95
+ bridgedToolNames,
96
+ options.approvals
97
+ );
98
+ if (result.approvalId !== void 0) {
99
+ approvalRequests.push(
100
+ buildApprovalRequestedEvent({
101
+ approvalId: result.approvalId,
102
+ title: result.title ?? request.title,
103
+ threadId,
104
+ runId,
105
+ detail: { provider: "opencode" }
106
+ })
107
+ );
108
+ }
109
+ return result.response;
110
+ });
111
+ logger.request(
112
+ `activity=chat provider=opencode model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? "none"}`,
113
+ { provider: "opencode", model: this.model }
114
+ );
115
+ const serverEnv = bridge ? {
116
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
117
+ mcp: {
118
+ [bridge.name]: {
119
+ type: "remote",
120
+ url: bridge.url,
121
+ enabled: true,
122
+ headers: { Authorization: `Bearer ${bridge.token}` }
123
+ }
124
+ }
125
+ })
126
+ } : void 0;
127
+ server = await startOpencodeServerInSandbox(sandbox, {
128
+ port: this.adapterConfig.port ?? DEFAULT_PORT,
129
+ ...this.adapterConfig.hostname !== void 0 && {
130
+ hostname: this.adapterConfig.hostname
131
+ },
132
+ cwd: directory,
133
+ ...serverEnv ? { env: serverEnv } : {},
134
+ ...externalSignal ? { signal: externalSignal } : {}
135
+ });
136
+ handle = await startOpencodeSession({
137
+ baseUrl: server.baseUrl,
138
+ // Forward the channel's auth headers (e.g. Daytona's preview token) so
139
+ // the host client can reach a token-gated preview proxy.
140
+ ...server.headers !== void 0 && { headers: server.headers },
141
+ // NOTE: do NOT pass `directory` here. `directory` is the VIRTUAL sandbox
142
+ // path (e.g. `/workspace`); the server is already spawned with that as its
143
+ // cwd (the provider handle maps it to the real workdir — `/workspace`
144
+ // inside Docker, a host temp dir for local-process). Forwarding the
145
+ // virtual path to the host-side opencode HTTP API breaks local-process,
146
+ // where `/workspace` doesn't exist → the API stalls until the request
147
+ // times out. Omitting it makes opencode use the server's (correct) cwd.
148
+ providerID,
149
+ modelID,
150
+ ...sessionId !== void 0 && { resumeSessionId: sessionId },
151
+ onEvent: (event) => queue.push({ kind: "event", event }),
152
+ onPermissionRequest: permissionHandler,
153
+ onError: (error) => queue.fail(error)
154
+ });
155
+ const session = handle;
156
+ if (externalSignal !== void 0) {
157
+ onAbort = () => void session.abort().catch(() => void 0);
158
+ if (externalSignal.aborted) onAbort();
159
+ else externalSignal.addEventListener("abort", onAbort, { once: true });
160
+ }
161
+ queue.push({ kind: "session", sessionId: session.sessionId });
162
+ const promptText = this.applySystemPrompts(
163
+ options,
164
+ session.resumed || sessionId === void 0 ? resumePrompt : buildPrompt(options.messages, void 0).prompt
165
+ );
166
+ session.prompt(promptText).then(({ message }) => {
167
+ queue.push({ kind: "done", message });
168
+ queue.end();
169
+ }).catch((error) => queue.fail(error));
170
+ yield* mergeChunkStreams(
171
+ translateOpencodeStream(queue, {
172
+ model: this.model,
173
+ runId,
174
+ threadId,
175
+ ...options.parentRunId !== void 0 && {
176
+ parentRunId: options.parentRunId
177
+ },
178
+ genId: () => this.generateId(),
179
+ bridgedToolNames,
180
+ onStreamEvent: (event) => logger.provider(`provider=opencode kind=${event.kind}`, {
181
+ chunk: event
182
+ })
183
+ }),
184
+ channel.stream
185
+ );
186
+ for (const event of approvalRequests) yield event;
187
+ } catch (error) {
188
+ const err = error;
189
+ const rawEvent = toRunErrorRawEvent(error);
190
+ logger.errors("opencode.chatStream fatal", {
191
+ error,
192
+ source: "opencode.chatStream"
193
+ });
194
+ yield {
195
+ type: EventType.RUN_ERROR,
196
+ model: options.model,
197
+ timestamp: Date.now(),
198
+ message: err.message || "Unknown error occurred",
199
+ ...err.code !== void 0 && { code: err.code },
200
+ ...rawEvent !== void 0 && { rawEvent },
201
+ error: {
202
+ message: err.message || "Unknown error occurred",
203
+ ...err.code !== void 0 && { code: err.code }
204
+ }
205
+ };
206
+ } finally {
207
+ if (externalSignal !== void 0 && onAbort !== void 0) {
208
+ externalSignal.removeEventListener("abort", onAbort);
209
+ }
210
+ channel.close();
211
+ await handle?.dispose();
212
+ await server?.dispose();
213
+ await bridge?.close();
214
+ }
215
+ }
216
+ structuredOutput(_options) {
217
+ return Promise.reject(
218
+ new Error(
219
+ "Structured output is not yet supported by the in-sandbox OpenCode adapter. Use a model adapter for structured output, or omit outputSchema."
220
+ )
221
+ );
222
+ }
223
+ }
224
+ function opencodeText(model, config = {}) {
225
+ return new OpencodeTextAdapter(config, model);
226
+ }
227
+ export {
228
+ OpencodeTextAdapter,
229
+ opencodeText
230
+ };
231
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport {\n SandboxCapability,\n buildApprovalRequestedEvent,\n createBridgeEventChannel,\n getSandbox,\n getToolBridgeProvisioner,\n getWorkspaceProjection,\n mergeChunkStreams,\n nodeHttpBridgeProvisioner,\n} from '@tanstack/ai-sandbox'\nimport { buildPrompt } from '../messages/prompt'\nimport { startOpencodeSession } from '../process/server'\nimport { startOpencodeServerInSandbox } from '../process/sandbox-server'\nimport { resolveInteractivePermission } from '../process/permissions'\nimport { AsyncQueue } from '../stream/queue'\nimport { translateOpencodeStream } from '../stream/translate'\nimport { projectOpencodeWorkspace } from './projection'\nimport type { HostToolBridge, SandboxHandle } from '@tanstack/ai-sandbox'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n DefaultMessageMetadataByModality,\n Modality,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { OpencodeSessionHandle } from '../process/server'\nimport type {\n OpencodePermissionMode,\n PermissionHandler,\n} from '../process/permissions'\nimport type { OpencodeStreamEvent } from '../stream/sdk-types'\nimport type { OpencodeModel } from '../model-meta'\nimport type { OpencodeTextProviderOptions } from '../provider-options'\n\nconst DEFAULT_WORKDIR = '/workspace'\nconst DEFAULT_PORT = 4096\n\nexport interface OpencodeTextConfig {\n /** Working directory inside the sandbox. Defaults to `/workspace`. */\n directory?: string\n /**\n * Port the in-sandbox `opencode serve` listens on. Defaults to 4096. For the\n * Docker provider this port must also be published (`publishPorts: [4096]`)\n * so the host can reach it.\n */\n port?: number\n /** Hostname the in-sandbox server binds. Defaults to `0.0.0.0`. */\n hostname?: string\n /**\n * OpenCode permission mode driving the dynamic permission handler. Defaults\n * to `'default'`; set `'acceptEdits'` / `'bypassPermissions'` to let the\n * harness edit files and run commands autonomously inside the sandbox.\n */\n permissionMode?: OpencodePermissionMode\n /** Custom permission handler; replaces the adapter's default policy. */\n onPermissionRequest?: PermissionHandler\n}\n\n/** Split a `provider/model` id into its provider and model halves. */\nfunction splitModel(model: string): { providerID: string; modelID: string } {\n const slash = model.indexOf('/')\n if (slash <= 0 || slash === model.length - 1) {\n throw new Error(\n `OpenCode models must be addressed as \"provider/model\" (e.g. \"anthropic/claude-sonnet-4-5\"); received \"${model}\".`,\n )\n }\n return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) }\n}\n\nexport class OpencodeTextAdapter<\n TModel extends OpencodeModel,\n> extends BaseTextAdapter<\n TModel,\n OpencodeTextProviderOptions,\n ReadonlyArray<Modality> & readonly ['text'],\n DefaultMessageMetadataByModality,\n ReadonlyArray<string>,\n unknown,\n never\n> {\n readonly name = 'opencode' as const\n\n override readonly requires = [SandboxCapability] as const\n\n private readonly adapterConfig: OpencodeTextConfig\n\n constructor(config: OpencodeTextConfig, model: TModel) {\n super({}, model)\n this.adapterConfig = config\n }\n\n private sandboxFrom(\n options: TextOptions<OpencodeTextProviderOptions>,\n ): SandboxHandle {\n const ctx = options.capabilities\n if (!ctx) {\n throw new Error(\n 'Adapter \"opencode\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware.',\n )\n }\n return getSandbox(ctx)\n }\n\n private applySystemPrompts(\n options: TextOptions<OpencodeTextProviderOptions>,\n prompt: string,\n ): string {\n const systemPrompts = normalizeSystemPrompts(options.systemPrompts)\n .map((systemPrompt) => systemPrompt.content)\n .filter((content) => content.trim() !== '')\n if (systemPrompts.length === 0) return prompt\n return `${systemPrompts.join('\\n\\n')}\\n\\n${prompt}`\n }\n\n async *chatStream(\n options: TextOptions<OpencodeTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const { logger } = options\n let server:\n | Awaited<ReturnType<typeof startOpencodeServerInSandbox>>\n | undefined\n let handle: OpencodeSessionHandle | undefined\n let bridge: HostToolBridge | undefined\n const externalSignal =\n options.abortController?.signal ?? options.request?.signal ?? undefined\n let onAbort: (() => void) | undefined\n const runId = options.runId ?? this.generateId()\n const threadId = options.threadId ?? this.generateId()\n // Surfaces custom events from bridged tools (e.g. code mode console logs)\n // on this run's live output stream.\n const channel = createBridgeEventChannel({\n model: this.model,\n threadId,\n runId,\n })\n\n try {\n const sandbox = this.sandboxFrom(options)\n const directory =\n options.modelOptions?.directory ??\n this.adapterConfig.directory ??\n DEFAULT_WORKDIR\n\n // Project workspace skills / MCP servers into the sandbox before starting\n // the opencode server so the workspace config is in place for the session.\n if (options.capabilities !== undefined) {\n const projection = getWorkspaceProjection(options.capabilities, {\n optional: true,\n })\n if (projection !== undefined) {\n await projectOpencodeWorkspace(sandbox, projection)\n }\n }\n\n const modelOptions = options.modelOptions\n const sessionId = modelOptions?.sessionId\n const { prompt: resumePrompt } = buildPrompt(options.messages, sessionId)\n const { providerID, modelID } = splitModel(this.model)\n\n // Bridge chat()-provided tools into the in-sandbox server over MCP\n // (configured via OPENCODE_CONFIG_CONTENT at server spawn).\n const bridgedToolNames = new Set(\n (options.tools ?? []).map((tool) => tool.name),\n )\n if (options.tools && options.tools.length > 0) {\n const provisioner =\n (options.capabilities\n ? getToolBridgeProvisioner(options.capabilities, { optional: true })\n : undefined) ?? nodeHttpBridgeProvisioner\n bridge = await provisioner.provision(options.tools, {\n provider: sandbox.provider,\n context: options.context,\n emitCustomEvent: channel.emitCustomEvent,\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n }\n\n // Approval-requested events for `ask`-policy actions with no client\n // decision yet, emitted after the stream so the client can approve + re-run.\n const approvalRequests: Array<StreamChunk> = []\n\n const queue = new AsyncQueue<OpencodeStreamEvent>()\n const mode =\n modelOptions?.permissionMode ??\n this.adapterConfig.permissionMode ??\n 'default'\n const permissionHandler: PermissionHandler =\n this.adapterConfig.onPermissionRequest ??\n ((request) => {\n const result = resolveInteractivePermission(\n request,\n mode,\n bridgedToolNames,\n options.approvals,\n )\n if (result.approvalId !== undefined) {\n approvalRequests.push(\n buildApprovalRequestedEvent({\n approvalId: result.approvalId,\n title: result.title ?? request.title,\n threadId,\n runId,\n detail: { provider: 'opencode' },\n }),\n )\n }\n return result.response\n })\n\n logger.request(\n `activity=chat provider=opencode model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${sessionId ?? 'none'}`,\n { provider: 'opencode', model: this.model },\n )\n\n const serverEnv = bridge\n ? {\n OPENCODE_CONFIG_CONTENT: JSON.stringify({\n mcp: {\n [bridge.name]: {\n type: 'remote',\n url: bridge.url,\n enabled: true,\n headers: { Authorization: `Bearer ${bridge.token}` },\n },\n },\n }),\n }\n : undefined\n\n server = await startOpencodeServerInSandbox(sandbox, {\n port: this.adapterConfig.port ?? DEFAULT_PORT,\n ...(this.adapterConfig.hostname !== undefined && {\n hostname: this.adapterConfig.hostname,\n }),\n cwd: directory,\n ...(serverEnv ? { env: serverEnv } : {}),\n ...(externalSignal ? { signal: externalSignal } : {}),\n })\n\n handle = await startOpencodeSession({\n baseUrl: server.baseUrl,\n // Forward the channel's auth headers (e.g. Daytona's preview token) so\n // the host client can reach a token-gated preview proxy.\n ...(server.headers !== undefined && { headers: server.headers }),\n // NOTE: do NOT pass `directory` here. `directory` is the VIRTUAL sandbox\n // path (e.g. `/workspace`); the server is already spawned with that as its\n // cwd (the provider handle maps it to the real workdir — `/workspace`\n // inside Docker, a host temp dir for local-process). Forwarding the\n // virtual path to the host-side opencode HTTP API breaks local-process,\n // where `/workspace` doesn't exist → the API stalls until the request\n // times out. Omitting it makes opencode use the server's (correct) cwd.\n providerID,\n modelID,\n ...(sessionId !== undefined && { resumeSessionId: sessionId }),\n onEvent: (event) => queue.push({ kind: 'event', event }),\n onPermissionRequest: permissionHandler,\n onError: (error) => queue.fail(error),\n })\n const session = handle\n\n if (externalSignal !== undefined) {\n onAbort = () => void session.abort().catch(() => undefined)\n if (externalSignal.aborted) onAbort()\n else externalSignal.addEventListener('abort', onAbort, { once: true })\n }\n\n queue.push({ kind: 'session', sessionId: session.sessionId })\n\n const promptText = this.applySystemPrompts(\n options,\n session.resumed || sessionId === undefined\n ? resumePrompt\n : buildPrompt(options.messages, undefined).prompt,\n )\n\n session\n .prompt(promptText)\n .then(({ message }) => {\n queue.push({ kind: 'done', message })\n queue.end()\n })\n .catch((error: unknown) => queue.fail(error))\n\n yield* mergeChunkStreams(\n translateOpencodeStream(queue, {\n model: this.model,\n runId,\n threadId,\n ...(options.parentRunId !== undefined && {\n parentRunId: options.parentRunId,\n }),\n genId: () => this.generateId(),\n bridgedToolNames,\n onStreamEvent: (event) =>\n logger.provider(`provider=opencode kind=${event.kind}`, {\n chunk: event,\n }),\n }),\n channel.stream,\n )\n\n // Surface pending approval requests (ask-policy actions awaiting a client\n // decision); the client approves and re-runs to continue.\n for (const event of approvalRequests) yield event\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('opencode.chatStream fatal', {\n error,\n source: 'opencode.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n ...(rawEvent !== undefined && { rawEvent }),\n error: {\n message: err.message || 'Unknown error occurred',\n ...(err.code !== undefined && { code: err.code }),\n },\n }\n } finally {\n if (externalSignal !== undefined && onAbort !== undefined) {\n externalSignal.removeEventListener('abort', onAbort)\n }\n channel.close()\n await handle?.dispose()\n await server?.dispose()\n await bridge?.close()\n }\n }\n\n structuredOutput(\n _options: StructuredOutputOptions<OpencodeTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n return Promise.reject(\n new Error(\n 'Structured output is not yet supported by the in-sandbox OpenCode adapter. ' +\n 'Use a model adapter for structured output, or omit outputSchema.',\n ),\n )\n }\n}\n\n/**\n * Creates an OpenCode harness adapter that runs **inside a sandbox**.\n *\n * It declares `requires: [SandboxCapability]`, spawns `opencode serve` inside\n * the sandbox provided by `withSandbox(...)`, exposes its port, and connects\n * the `@opencode-ai/sdk` HTTP client to it. OpenCode owns the agent loop and\n * executes its native tools against the sandbox workspace. The sandbox image\n * must provide the `opencode` executable (Docker: also publish the server port\n * via `publishPorts`). chat()-provided tools aren't bridged yet.\n */\nexport function opencodeText<TModel extends OpencodeModel>(\n model: TModel,\n config: OpencodeTextConfig = {},\n): OpencodeTextAdapter<TModel> {\n return new OpencodeTextAdapter(config, model)\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export { OpencodeTextAdapter, opencodeText } from './adapters/text.js';
2
+ export type { OpencodeTextConfig } from './adapters/text.js';
3
+ export type { OpencodeTextProviderOptions } from './provider-options.js';
4
+ export { OPENCODE_MODELS } from './model-meta.js';
5
+ export type { OpencodeModel, KnownOpencodeModel } from './model-meta.js';
6
+ export { SESSION_ID_EVENT, TODO_EVENT, BRIDGED_MCP_SERVER_NAME, translateOpencodeStream, resolveToolName, } from './stream/translate.js';
7
+ export type { TranslateContext } from './stream/translate.js';
8
+ export type { OpencodeAssistantMessage, OpencodeEvent, OpencodePart, OpencodeStreamEvent, OpencodeTokens, OpencodeToolState, } from './stream/sdk-types.js';
9
+ export { resolvePermission, matchBridgedToolName } from './process/permissions.js';
10
+ export type { OpencodePermissionMode, OpencodePermissionRequest, OpencodePermissionResponse, PermissionHandler, } from './process/permissions.js';
11
+ export { startOpencodeSession } from './process/server.js';
12
+ export type { OpencodeSessionHandle, StartOpencodeSessionOptions, } from './process/server.js';
13
+ export { buildPrompt } from './messages/prompt.js';
14
+ export type { BuiltPrompt } from './messages/prompt.js';
15
+ export { startOpencodeServerInSandbox } from './process/sandbox-server.js';
16
+ export type { SandboxOpencodeServer, StartServerOptions, } from './process/sandbox-server.js';
@@ -0,0 +1,23 @@
1
+ import { OpencodeTextAdapter, opencodeText } from "./adapters/text.js";
2
+ import { OPENCODE_MODELS } from "./model-meta.js";
3
+ import { BRIDGED_MCP_SERVER_NAME, SESSION_ID_EVENT, TODO_EVENT, resolveToolName, translateOpencodeStream } from "./stream/translate.js";
4
+ import { matchBridgedToolName, resolvePermission } from "./process/permissions.js";
5
+ import { startOpencodeSession } from "./process/server.js";
6
+ import { buildPrompt } from "./messages/prompt.js";
7
+ import { startOpencodeServerInSandbox } from "./process/sandbox-server.js";
8
+ export {
9
+ BRIDGED_MCP_SERVER_NAME,
10
+ OPENCODE_MODELS,
11
+ OpencodeTextAdapter,
12
+ SESSION_ID_EVENT,
13
+ TODO_EVENT,
14
+ buildPrompt,
15
+ matchBridgedToolName,
16
+ opencodeText,
17
+ resolvePermission,
18
+ resolveToolName,
19
+ startOpencodeServerInSandbox,
20
+ startOpencodeSession,
21
+ translateOpencodeStream
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"}
@@ -0,0 +1,16 @@
1
+ import { ModelMessage } from '@tanstack/ai';
2
+ export interface BuiltPrompt {
3
+ prompt: string;
4
+ /** OpenCode session id to resume, when the caller threaded one through. */
5
+ resume?: string;
6
+ }
7
+ /**
8
+ * Convert TanStack chat history into the OpenCode prompt + resume inputs.
9
+ *
10
+ * With a `sessionId`, the harness already holds the conversation context, so
11
+ * only the trailing user message is sent and the session is resumed. Without
12
+ * one, prior turns are flattened into a plain-text transcript preamble (tool
13
+ * messages and tool-call-only assistant turns are harness-internal noise and
14
+ * are skipped; prompts are text-only in v1).
15
+ */
16
+ export declare function buildPrompt(messages: Array<ModelMessage>, sessionId: string | undefined): BuiltPrompt;
@@ -0,0 +1,37 @@
1
+ function extractText(content) {
2
+ if (content === null) return "";
3
+ if (typeof content === "string") return content;
4
+ return content.map(
5
+ (part) => part.type === "text" && typeof part.content === "string" ? part.content : ""
6
+ ).join("");
7
+ }
8
+ function buildPrompt(messages, sessionId) {
9
+ const lastMessage = messages.at(-1);
10
+ const lastUserText = lastMessage?.role === "user" ? extractText(lastMessage.content).trim() : "";
11
+ if (!lastUserText) {
12
+ throw new Error(
13
+ "OpenCode adapter requires a trailing user message with text content."
14
+ );
15
+ }
16
+ if (sessionId !== void 0) {
17
+ return { prompt: lastUserText, resume: sessionId };
18
+ }
19
+ const priorTurns = messages.slice(0, -1).filter(
20
+ (message) => (message.role === "user" || message.role === "assistant") && extractText(message.content).trim() !== ""
21
+ ).map(
22
+ (message) => `${message.role === "user" ? "User" : "Assistant"}: ${extractText(message.content).trim()}`
23
+ );
24
+ if (priorTurns.length === 0) {
25
+ return { prompt: lastUserText };
26
+ }
27
+ return {
28
+ prompt: `Previous conversation:
29
+ ${priorTurns.join("\n")}
30
+
31
+ ${lastUserText}`
32
+ };
33
+ }
34
+ export {
35
+ buildPrompt
36
+ };
37
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sources":["../../../src/messages/prompt.ts"],"sourcesContent":["import type { ModelMessage } from '@tanstack/ai'\n\nexport interface BuiltPrompt {\n prompt: string\n /** OpenCode session id to resume, when the caller threaded one through. */\n resume?: string\n}\n\nfunction extractText(content: ModelMessage['content']): string {\n if (content === null) return ''\n if (typeof content === 'string') return content\n return content\n .map((part) =>\n part.type === 'text' && typeof part.content === 'string'\n ? part.content\n : '',\n )\n .join('')\n}\n\n/**\n * Convert TanStack chat history into the OpenCode prompt + resume inputs.\n *\n * With a `sessionId`, the harness already holds the conversation context, so\n * only the trailing user message is sent and the session is resumed. Without\n * one, prior turns are flattened into a plain-text transcript preamble (tool\n * messages and tool-call-only assistant turns are harness-internal noise and\n * are skipped; prompts are text-only in v1).\n */\nexport function buildPrompt(\n messages: Array<ModelMessage>,\n sessionId: string | undefined,\n): BuiltPrompt {\n const lastMessage = messages.at(-1)\n const lastUserText =\n lastMessage?.role === 'user' ? extractText(lastMessage.content).trim() : ''\n\n if (!lastUserText) {\n throw new Error(\n 'OpenCode adapter requires a trailing user message with text content.',\n )\n }\n\n if (sessionId !== undefined) {\n return { prompt: lastUserText, resume: sessionId }\n }\n\n const priorTurns = messages\n .slice(0, -1)\n .filter(\n (message) =>\n (message.role === 'user' || message.role === 'assistant') &&\n extractText(message.content).trim() !== '',\n )\n .map(\n (message) =>\n `${message.role === 'user' ? 'User' : 'Assistant'}: ${extractText(message.content).trim()}`,\n )\n\n if (priorTurns.length === 0) {\n return { prompt: lastUserText }\n }\n\n return {\n prompt: `Previous conversation:\\n${priorTurns.join('\\n')}\\n\\n${lastUserText}`,\n }\n}\n"],"names":[],"mappings":"AAQA,SAAS,YAAY,SAA0C;AAC7D,MAAI,YAAY,KAAM,QAAO;AAC7B,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QACJ;AAAA,IAAI,CAAC,SACJ,KAAK,SAAS,UAAU,OAAO,KAAK,YAAY,WAC5C,KAAK,UACL;AAAA,EAAA,EAEL,KAAK,EAAE;AACZ;AAWO,SAAS,YACd,UACA,WACa;AACb,QAAM,cAAc,SAAS,GAAG,EAAE;AAClC,QAAM,eACJ,aAAa,SAAS,SAAS,YAAY,YAAY,OAAO,EAAE,KAAA,IAAS;AAE3E,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,cAAc,QAAW;AAC3B,WAAO,EAAE,QAAQ,cAAc,QAAQ,UAAA;AAAA,EACzC;AAEA,QAAM,aAAa,SAChB,MAAM,GAAG,EAAE,EACX;AAAA,IACC,CAAC,aACE,QAAQ,SAAS,UAAU,QAAQ,SAAS,gBAC7C,YAAY,QAAQ,OAAO,EAAE,WAAW;AAAA,EAAA,EAE3C;AAAA,IACC,CAAC,YACC,GAAG,QAAQ,SAAS,SAAS,SAAS,WAAW,KAAK,YAAY,QAAQ,OAAO,EAAE,MAAM;AAAA,EAAA;AAG/F,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO,EAAE,QAAQ,aAAA;AAAA,EACnB;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,EAA2B,WAAW,KAAK,IAAI,CAAC;AAAA;AAAA,EAAO,YAAY;AAAA,EAAA;AAE/E;"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Models known to work with OpenCode. OpenCode is provider-agnostic — it
3
+ * resolves any `provider/model` id its configured providers support (via the
4
+ * Vercel AI SDK + Models.dev), so this list exists for autocomplete. Any
5
+ * string is accepted via the `(string & {})` escape hatch in
6
+ * {@link OpencodeModel}.
7
+ *
8
+ * Models are addressed as `provider_id/model_id` (e.g.
9
+ * `anthropic/claude-sonnet-4-5`); the adapter splits on the first `/`.
10
+ */
11
+ export declare const OPENCODE_MODELS: readonly ["anthropic/claude-opus-4-5", "anthropic/claude-sonnet-4-5", "openai/gpt-5.2", "openai/gpt-5.1-codex", "google/gemini-3-pro-preview", "opencode/claude-sonnet-4-5", "opencode/gpt-5.1-codex"];
12
+ export type KnownOpencodeModel = (typeof OPENCODE_MODELS)[number];
13
+ /** Any `provider/model` id accepted by OpenCode; known ids get autocomplete. */
14
+ export type OpencodeModel = KnownOpencodeModel | (string & {});
@@ -0,0 +1,13 @@
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"
9
+ ];
10
+ export {
11
+ OPENCODE_MODELS
12
+ };
13
+ //# sourceMappingURL=model-meta.js.map
@@ -0,0 +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;"}