From e0258e8c1b71833d48804d0d7e2a3295e459f44c Mon Sep 17 00:00:00 2001 From: gavrielc Date: Fri, 17 Apr 2026 14:10:56 +0300 Subject: [PATCH] refactor(v2): move opencode provider off v2 trunk v2 ships with only claude baked in. opencode now lives on the `providers` branch and gets copied in via the /add-opencode skill. Removed: - src/providers/opencode.ts - container/agent-runner/src/providers/{opencode,mcp-to-opencode}.ts + test - @opencode-ai/sdk from agent-runner package.json + bun.lock - opencode-ai global install + OPENCODE_VERSION ARG from Dockerfile - opencode self-registration imports from both provider barrels - opencode test case from factory.test.ts Co-Authored-By: Claude Opus 4.7 (1M context) --- container/Dockerfile | 4 +- container/agent-runner/bun.lock | 3 - container/agent-runner/package.json | 1 - .../src/providers/factory.test.ts | 5 - container/agent-runner/src/providers/index.ts | 1 - .../src/providers/mcp-to-opencode.test.ts | 59 --- .../src/providers/mcp-to-opencode.ts | 39 -- .../agent-runner/src/providers/opencode.ts | 422 ------------------ src/providers/index.ts | 1 - src/providers/opencode.ts | 49 -- 10 files changed, 1 insertion(+), 583 deletions(-) delete mode 100644 container/agent-runner/src/providers/mcp-to-opencode.test.ts delete mode 100644 container/agent-runner/src/providers/mcp-to-opencode.ts delete mode 100644 container/agent-runner/src/providers/opencode.ts delete mode 100644 src/providers/opencode.ts diff --git a/container/Dockerfile b/container/Dockerfile index ac5fd4bc6..12d2bf6f4 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -18,7 +18,6 @@ ARG INSTALL_CJK_FONTS=false ARG CLAUDE_CODE_VERSION=2.1.112 ARG AGENT_BROWSER_VERSION=latest ARG VERCEL_VERSION=latest -ARG OPENCODE_VERSION=latest ARG BUN_VERSION=1.3.12 # ---- System dependencies ----------------------------------------------------- @@ -80,8 +79,7 @@ RUN --mount=type=cache,target=/root/.cache/pnpm \ pnpm install -g \ "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ "agent-browser@${AGENT_BROWSER_VERSION}" \ - "vercel@${VERCEL_VERSION}" \ - "opencode-ai@${OPENCODE_VERSION}" + "vercel@${VERCEL_VERSION}" # ---- agent-runner ------------------------------------------------------------ WORKDIR /app diff --git a/container/agent-runner/bun.lock b/container/agent-runner/bun.lock index 461d56c0b..99fe8406e 100644 --- a/container/agent-runner/bun.lock +++ b/container/agent-runner/bun.lock @@ -7,7 +7,6 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.92", "@modelcontextprotocol/sdk": "^1.12.1", - "@opencode-ai/sdk": "^1.4.3", "cron-parser": "^5.0.0", "zod": "^4.0.0", }, @@ -61,8 +60,6 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.7", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-onEtaooQyoDP5gTShQeQSf0Sd8V7949G9pPNyIyRXnVtFqyDIhUDLGtL/a/+EIW9x5s+Y6lDy/3oVoGMvQ0rQQ=="], - "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], diff --git a/container/agent-runner/package.json b/container/agent-runner/package.json index 042f30bad..06eb39403 100644 --- a/container/agent-runner/package.json +++ b/container/agent-runner/package.json @@ -11,7 +11,6 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.92", "@modelcontextprotocol/sdk": "^1.12.1", - "@opencode-ai/sdk": "^1.4.3", "cron-parser": "^5.0.0", "zod": "^4.0.0" }, diff --git a/container/agent-runner/src/providers/factory.test.ts b/container/agent-runner/src/providers/factory.test.ts index 15c7e6312..61fa7a8fa 100644 --- a/container/agent-runner/src/providers/factory.test.ts +++ b/container/agent-runner/src/providers/factory.test.ts @@ -3,17 +3,12 @@ import { describe, it, expect } from 'bun:test'; import { createProvider, type ProviderName } from './factory.js'; import { ClaudeProvider } from './claude.js'; import { MockProvider } from './mock.js'; -import { OpenCodeProvider } from './opencode.js'; describe('createProvider', () => { it('returns ClaudeProvider for claude', () => { expect(createProvider('claude')).toBeInstanceOf(ClaudeProvider); }); - it('returns OpenCodeProvider for opencode', () => { - expect(createProvider('opencode')).toBeInstanceOf(OpenCodeProvider); - }); - it('returns MockProvider for mock', () => { expect(createProvider('mock')).toBeInstanceOf(MockProvider); }); diff --git a/container/agent-runner/src/providers/index.ts b/container/agent-runner/src/providers/index.ts index 9bdf7f25b..70497cf32 100644 --- a/container/agent-runner/src/providers/index.ts +++ b/container/agent-runner/src/providers/index.ts @@ -4,4 +4,3 @@ import './claude.js'; import './mock.js'; -import './opencode.js'; diff --git a/container/agent-runner/src/providers/mcp-to-opencode.test.ts b/container/agent-runner/src/providers/mcp-to-opencode.test.ts deleted file mode 100644 index f41101a9f..000000000 --- a/container/agent-runner/src/providers/mcp-to-opencode.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, it, expect } from 'bun:test'; - -import { mcpServersToOpenCodeConfig } from './mcp-to-opencode.js'; - -describe('mcpServersToOpenCodeConfig', () => { - it('maps nanoclaw + extra server like v2 index.ts merge', () => { - const servers = { - nanoclaw: { - command: 'node', - args: ['/app/src/mcp-tools/index.js'], - env: { - SESSION_INBOUND_DB_PATH: '/workspace/inbound.db', - SESSION_OUTBOUND_DB_PATH: '/workspace/outbound.db', - SESSION_HEARTBEAT_PATH: '/workspace/.heartbeat', - }, - }, - extra: { - command: 'npx', - args: ['-y', 'some-mcp'], - env: { FOO: 'bar' }, - }, - }; - - const mcp = mcpServersToOpenCodeConfig(servers); - - expect(mcp.nanoclaw).toEqual({ - type: 'local', - command: ['node', '/app/src/mcp-tools/index.js'], - environment: { - SESSION_INBOUND_DB_PATH: '/workspace/inbound.db', - SESSION_OUTBOUND_DB_PATH: '/workspace/outbound.db', - SESSION_HEARTBEAT_PATH: '/workspace/.heartbeat', - }, - enabled: true, - }); - - expect(mcp.extra).toEqual({ - type: 'local', - command: ['npx', '-y', 'some-mcp'], - environment: { FOO: 'bar' }, - enabled: true, - }); - }); - - it('omits environment when env is empty', () => { - const mcp = mcpServersToOpenCodeConfig({ - x: { command: 'true', args: [], env: {} }, - }); - expect(mcp.x).toEqual({ - type: 'local', - command: ['true'], - enabled: true, - }); - }); - - it('returns empty record for undefined', () => { - expect(mcpServersToOpenCodeConfig(undefined)).toEqual({}); - }); -}); diff --git a/container/agent-runner/src/providers/mcp-to-opencode.ts b/container/agent-runner/src/providers/mcp-to-opencode.ts deleted file mode 100644 index 7e90e0ef5..000000000 --- a/container/agent-runner/src/providers/mcp-to-opencode.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { McpServerConfig } from './types.js'; - -/** OpenCode `mcp` entry shape (local stdio server). */ -export type OpenCodeMcpLocal = { - type: 'local'; - command: string[]; - environment?: Record; - enabled: true; -}; - -/** OpenCode `mcp` entry shape (remote HTTP server). */ -export type OpenCodeMcpRemote = { - type: 'remote'; - url: string; - headers?: Record; - enabled: true; -}; - -export type OpenCodeMcpEntry = OpenCodeMcpLocal | OpenCodeMcpRemote; - -/** - * Map NanoClaw v2 MCP definitions (same shape as Claude Agent SDK) into - * OpenCode config `mcp` field. Stdio-only until `McpServerConfig` gains remote. - */ -export function mcpServersToOpenCodeConfig( - servers: Record | undefined, -): Record { - const out: Record = {}; - if (!servers) return out; - for (const [name, cfg] of Object.entries(servers)) { - out[name] = { - type: 'local', - command: [cfg.command, ...cfg.args], - ...(Object.keys(cfg.env).length > 0 ? { environment: cfg.env } : {}), - enabled: true, - }; - } - return out; -} diff --git a/container/agent-runner/src/providers/opencode.ts b/container/agent-runner/src/providers/opencode.ts deleted file mode 100644 index 657c20984..000000000 --- a/container/agent-runner/src/providers/opencode.ts +++ /dev/null @@ -1,422 +0,0 @@ -import * as fs from 'fs'; -import { spawn, type ChildProcess } from 'child_process'; - -import { createOpencodeClient, type OpencodeClient } from '@opencode-ai/sdk'; - -import { registerProvider } from './provider-registry.js'; -import type { AgentProvider, AgentQuery, ProviderEvent, ProviderOptions, QueryInput } from './types.js'; -import { mcpServersToOpenCodeConfig } from './mcp-to-opencode.js'; - -function log(msg: string): void { - console.error(`[opencode-provider] ${msg}`); -} - -const SESSION_STATUS_RETRY_ERROR_AFTER = 3; - -/** Stale / dead OpenCode session heuristics (complement Claude-centric host patterns). */ -const STALE_SESSION_RE = - /no conversation found|ENOENT.*\.jsonl|session.*not found|NotFoundError|connection reset|ECONNRESET|404|event timeout/i; - -function spawnOpencodeServer(config: Record, timeoutMs = 10_000): Promise<{ url: string; proc: ChildProcess }> { - return new Promise((resolve, reject) => { - const hostname = '127.0.0.1'; - const port = 4096; - const proc = spawn('opencode', ['serve', `--hostname=${hostname}`, `--port=${port}`], { - env: { - ...process.env, - OPENCODE_CONFIG_CONTENT: JSON.stringify(config), - }, - }); - - const id = setTimeout(() => { - proc.kill('SIGKILL'); - reject(new Error(`Timeout waiting for OpenCode server to start after ${timeoutMs}ms`)); - }, timeoutMs); - - let output = ''; - proc.stdout?.on('data', (chunk: Buffer) => { - output += chunk.toString(); - for (const line of output.split('\n')) { - if (line.startsWith('opencode server listening')) { - const match = line.match(/on\s+(https?:\/\/[^\s]+)/); - if (match) { - clearTimeout(id); - resolve({ url: match[1], proc }); - } - } - } - }); - proc.stderr?.on('data', (chunk: Buffer) => { - output += chunk.toString(); - }); - proc.on('exit', (code) => { - clearTimeout(id); - let msg = `OpenCode server exited with code ${code}`; - if (output.trim()) msg += `\nServer output: ${output}`; - reject(new Error(msg)); - }); - proc.on('error', (err) => { - clearTimeout(id); - reject(err); - }); - }); -} - -function readClaudeMdForPrompt(): string | undefined { - const groupPath = '/workspace/agent/CLAUDE.md'; - const globalPath = '/workspace/global/CLAUDE.md'; - let content = ''; - if (fs.existsSync(groupPath)) { - content += fs.readFileSync(groupPath, 'utf-8'); - } - const isMain = process.env.NANOCLAW_IS_MAIN === '1'; - if (!isMain && fs.existsSync(globalPath)) { - if (content) content += '\n\n---\n\n'; - content += fs.readFileSync(globalPath, 'utf-8'); - } - return content || undefined; -} - -function wrapPromptWithContext(text: string, systemInstructions?: string): string { - let out = text; - if (systemInstructions) { - out = `\n${systemInstructions}\n\n\n${out}`; - } - const claudeMd = readClaudeMdForPrompt(); - if (claudeMd) { - out = `\n${claudeMd}\n\n\n${out}`; - } - return out; -} - -function buildOpenCodeConfig(options: ProviderOptions): Record { - const provider = process.env.OPENCODE_PROVIDER || 'anthropic'; - const model = process.env.OPENCODE_MODEL; - const smallModel = process.env.OPENCODE_SMALL_MODEL; - const proxyUrl = process.env.ANTHROPIC_BASE_URL; - - const providerModelId = model ? model.replace(new RegExp(`^${provider}/`), '') : undefined; - const providerSmallModelId = smallModel ? smallModel.replace(new RegExp(`^${provider}/`), '') : undefined; - const modelsToRegister = [providerModelId, providerSmallModelId] - .filter(Boolean) - .filter((mid, i, a) => a.indexOf(mid as string) === i); - - const providerOptions: Record = - provider === 'anthropic' - ? {} - : { - [provider]: { - options: { apiKey: 'placeholder', baseURL: proxyUrl }, - ...(modelsToRegister.length > 0 - ? { - models: Object.fromEntries( - modelsToRegister.map((mid) => [mid, { id: mid, name: mid, tool_call: true }]), - ), - } - : {}), - }, - }; - - const mcp = mcpServersToOpenCodeConfig(options.mcpServers); - - return { - ...(model ? { model } : {}), - ...(smallModel ? { small_model: smallModel } : {}), - enabled_providers: [provider], - permission: 'allow', - autoupdate: false, - snapshot: false, - provider: providerOptions, - mcp, - }; -} - -type SharedRuntime = { - proc: ChildProcess; - client: OpencodeClient; - stream: AsyncGenerator<{ type: string; properties: Record }, void, void>; - streamRelease: () => void; -}; - -let sharedRuntime: SharedRuntime | null = null; -let sharedConfigKey: string | null = null; -let sharedInit: Promise | null = null; - -function runtimeConfigKey(options: ProviderOptions): string { - return JSON.stringify({ - mcp: mcpServersToOpenCodeConfig(options.mcpServers), - model: process.env.OPENCODE_MODEL, - small: process.env.OPENCODE_SMALL_MODEL, - op: process.env.OPENCODE_PROVIDER, - }); -} - -async function ensureSharedRuntime(options: ProviderOptions): Promise { - const key = runtimeConfigKey(options); - if (sharedRuntime && sharedConfigKey === key) return sharedRuntime; - - if (sharedInit) return sharedInit; - - sharedInit = (async () => { - if (sharedRuntime) { - destroySharedRuntime(); - } - const config = buildOpenCodeConfig(options); - const { url, proc } = await spawnOpencodeServer(config); - const client = createOpencodeClient({ baseUrl: url }); - const sub = await client.event.subscribe(); - const stream = sub.stream as AsyncGenerator<{ type: string; properties: Record }, void, void>; - sharedRuntime = { - proc, - client, - stream, - streamRelease: () => { - void stream.return?.(undefined); - }, - }; - sharedConfigKey = key; - sharedInit = null; - return sharedRuntime; - })(); - - return sharedInit; -} - -export function destroySharedRuntime(): void { - if (sharedRuntime) { - try { - sharedRuntime.streamRelease(); - } catch { - /* ignore */ - } - try { - sharedRuntime.proc.kill('SIGKILL'); - } catch { - /* ignore */ - } - sharedRuntime = null; - sharedConfigKey = null; - } - sharedInit = null; -} - -function sessionErrorMessage(props: { error?: unknown }): string { - const err = props.error as { data?: { message?: string } } | undefined; - if (err && typeof err === 'object' && err.data && typeof err.data.message === 'string') { - return err.data.message; - } - return JSON.stringify(props.error) || 'OpenCode session error'; -} - -export class OpenCodeProvider implements AgentProvider { - readonly supportsNativeSlashCommands = false; - - private readonly options: ProviderOptions; - private activeSessionId: string | undefined; - - constructor(options: ProviderOptions = {}) { - this.options = options; - } - - isSessionInvalid(err: unknown): boolean { - const msg = err instanceof Error ? err.message : String(err); - return STALE_SESSION_RE.test(msg); - } - - query(input: QueryInput): AgentQuery { - if (input.continuation) { - this.activeSessionId = input.continuation; - } else { - this.activeSessionId = undefined; - } - - const pending: string[] = []; - let waiting: (() => void) | null = null; - let ended = false; - let aborted = false; - - const systemInstructions = input.systemContext?.instructions; - pending.push(wrapPromptWithContext(input.prompt, systemInstructions)); - - const kick = (): void => { - waiting?.(); - }; - - const self = this; - const IDLE_TIMEOUT_MS = 90_000; - - async function* gen(): AsyncGenerator { - let initYielded = false; - const rt = await ensureSharedRuntime(self.options); - const { client, stream } = rt; - - while (!aborted) { - while (pending.length === 0 && !ended && !aborted) { - await new Promise((resolve) => { - waiting = resolve; - }); - waiting = null; - } - - if (aborted) return; - if (pending.length === 0 && ended) return; - - const text = pending.shift()!; - let sessionId = self.activeSessionId; - - if (!sessionId) { - const created = await client.session.create(); - if (created.error) { - throw new Error(`OpenCode: failed to create session: ${JSON.stringify(created.error)}`); - } - sessionId = created.data?.id; - if (!sessionId) throw new Error('OpenCode: failed to create session (no id)'); - self.activeSessionId = sessionId; - } - - if (!initYielded) { - yield { type: 'init', continuation: sessionId }; - initYielded = true; - } - - const promptRes = await client.session.promptAsync({ - path: { id: sessionId }, - body: { parts: [{ type: 'text', text }] }, - }); - if (promptRes.error) { - self.activeSessionId = undefined; - throw new Error(`OpenCode promptAsync: ${JSON.stringify(promptRes.error)}`); - } - - const partTextByMessageId = new Map(); - const roleByMessageId = new Map(); - let lastEventAt = Date.now(); - let eventTimedOut = false; - const timeoutCheck = setInterval(() => { - if (Date.now() - lastEventAt > IDLE_TIMEOUT_MS) { - log(`OpenCode event timeout (${IDLE_TIMEOUT_MS}ms) — clearing session ${sessionId}`); - eventTimedOut = true; - self.activeSessionId = undefined; - destroySharedRuntime(); - kick(); - } - }, 5000); - - try { - turn: while (true) { - if (aborted) return; - if (eventTimedOut) { - throw new Error(`OpenCode event timeout (${IDLE_TIMEOUT_MS}ms)`); - } - - const { value: ev, done } = await stream.next(); - if (done) { - throw new Error('OpenCode SSE stream ended unexpectedly'); - } - - if (!ev?.type || ev.type === 'server.connected' || ev.type === 'server.heartbeat') continue; - - lastEventAt = Date.now(); - yield { type: 'activity' }; - - switch (ev.type) { - case 'message.updated': { - const info = ev.properties.info as { id?: string; role?: string } | undefined; - if (info?.id && info?.role) { - roleByMessageId.set(info.id, info.role); - } - break; - } - case 'message.part.updated': { - const part = ev.properties.part as { type?: string; messageID?: string; text?: string } | undefined; - if (part?.type === 'text' && part.messageID && part.text) { - partTextByMessageId.set(part.messageID, part.text); - } - break; - } - case 'permission.updated': { - const perm = ev.properties as { id?: string; sessionID?: string }; - if (perm.sessionID === sessionId && perm.id) { - try { - await client.postSessionIdPermissionsPermissionId({ - path: { id: sessionId, permissionID: perm.id }, - body: { response: 'always' }, - }); - } catch (err) { - log(`Failed to auto-reply permission: ${err instanceof Error ? err.message : String(err)}`); - } - } - break; - } - case 'session.status': { - const props = ev.properties as { - sessionID?: string; - status?: { type?: string; attempt?: number; message?: string }; - }; - if (props.sessionID !== sessionId) break; - const st = props.status; - if ( - st?.type === 'retry' && - typeof st.attempt === 'number' && - st.attempt >= SESSION_STATUS_RETRY_ERROR_AFTER && - st.message - ) { - self.activeSessionId = undefined; - throw new Error(`OpenCode retry limit (${st.attempt}): ${st.message}`); - } - break; - } - case 'session.error': { - const props = ev.properties as { sessionID?: string; error?: unknown }; - if (props.sessionID === sessionId || props.sessionID === undefined) { - self.activeSessionId = undefined; - throw new Error(sessionErrorMessage(props)); - } - break; - } - case 'session.idle': { - const sid = (ev.properties as { sessionID?: string }).sessionID; - if (sid === sessionId) { - break turn; - } - break; - } - default: - break; - } - } - } finally { - clearInterval(timeoutCheck); - } - - let resultText = ''; - for (const [msgId, role] of roleByMessageId) { - if (role === 'assistant') { - resultText = partTextByMessageId.get(msgId) ?? resultText; - } - } - yield { type: 'result', text: resultText || null }; - } - } - - return { - push: (message: string) => { - pending.push(wrapPromptWithContext(message, systemInstructions)); - kick(); - }, - end: () => { - ended = true; - kick(); - }, - events: gen(), - abort: () => { - aborted = true; - this.activeSessionId = undefined; - kick(); - destroySharedRuntime(); - }, - }; - } -} - -registerProvider('opencode', (opts) => new OpenCodeProvider(opts)); diff --git a/src/providers/index.ts b/src/providers/index.ts index f5c22c4ea..48edd5186 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -5,4 +5,3 @@ // // Skills add a new provider by appending one import line below. -import './opencode.js'; diff --git a/src/providers/opencode.ts b/src/providers/opencode.ts deleted file mode 100644 index 3e283e62d..000000000 --- a/src/providers/opencode.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Host-side container config for the `opencode` provider. - * - * OpenCode's `opencode serve` process stores state under XDG_DATA_HOME, which - * we pin to a per-session host directory mounted at /opencode-xdg. The - * OPENCODE_* env vars tell the CLI which provider/model to use at runtime - * (read on the host, injected into the container). NO_PROXY / no_proxy are - * merged with host values so the in-container OpenCode client can talk to - * 127.0.0.1 even when HTTPS_PROXY is set by OneCLI. - */ -import fs from 'fs'; -import path from 'path'; - -import { registerProviderContainerConfig } from './provider-container-registry.js'; - -function mergeNoProxy(current: string | undefined, additions: string): string { - if (!current?.trim()) return additions; - const parts = new Set( - current - .split(/[\s,]+/) - .map((s) => s.trim()) - .filter(Boolean), - ); - for (const addition of additions.split(',')) { - const trimmed = addition.trim(); - if (trimmed) parts.add(trimmed); - } - return [...parts].join(','); -} - -registerProviderContainerConfig('opencode', (ctx) => { - const opencodeDir = path.join(ctx.sessionDir, 'opencode-xdg'); - fs.mkdirSync(opencodeDir, { recursive: true }); - - const env: Record = { - XDG_DATA_HOME: '/opencode-xdg', - NO_PROXY: mergeNoProxy(ctx.hostEnv.NO_PROXY, '127.0.0.1,localhost'), - no_proxy: mergeNoProxy(ctx.hostEnv.no_proxy, '127.0.0.1,localhost'), - }; - for (const key of ['OPENCODE_PROVIDER', 'OPENCODE_MODEL', 'OPENCODE_SMALL_MODEL'] as const) { - const value = ctx.hostEnv[key]; - if (value) env[key] = value; - } - - return { - mounts: [{ hostPath: opencodeDir, containerPath: '/opencode-xdg', readonly: false }], - env, - }; -});