@agentproto/adapter-opencode 1.2.0 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -43,7 +43,10 @@ var opencode = defineAgentCli({
43
43
  { method: "curl", url: "https://opencode.ai/install" }
44
44
  ],
45
45
  version_check: {
46
- cmd: "npm view opencode-ai version",
46
+ // PRESENCE probe local binary check, not a registry query. Covers BOTH
47
+ // install methods: the npm -g package and the curl installer both put an
48
+ // `opencode` executable on PATH, and it runs `--version` fully offline.
49
+ cmd: "opencode --version",
47
50
  parse: "(\\d+\\.\\d+\\.\\d+)",
48
51
  range: ">=1.0.0",
49
52
  timeout_ms: 15e3
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/adapter-opencode — AIP-45 adapter for sst/opencode.\n *\n * OpenCode ships first-party ACP support (`opencode acp`) so no\n * third-party wrapper is needed. We spawn it via `npx -y opencode-ai\n * acp` and drive it over stdio JSON-RPC the same way the claude-code\n * adapter drives @agentclientprotocol/claude-agent-acp.\n *\n * import { opencode, opencodeRuntime } from \"@agentproto/adapter-opencode\"\n * const session = await opencodeRuntime().start({\n * env: { ANTHROPIC_API_KEY: \"sk-...\" },\n * })\n * for await (const evt of session.send({ role: \"user\", content: \"...\" })) {\n * console.log(evt)\n * }\n * await session.close()\n */\n\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport {\n createAgentCliRuntime,\n defineAgentCli,\n type AgentCliHandle,\n type AgentCliRuntime,\n} from \"@agentproto/driver-agent-cli\"\nimport { listModels } from \"@agentproto/model-catalog\"\n\n/**\n * Build OpenCode's model menu from the shared provider catalog instead of a\n * hand-maintained allowlist. OpenCode routes by the model id's own\n * `<provider>/<id>` prefix, so every entry carries the billing provider the\n * runtime's eligibility projection needs.\n *\n * Only providers OpenCode genuinely supports today are included:\n * - Anthropic and OpenAI (direct vendor prefixes)\n * - OpenRouter (`openrouter/<vendor>/<id>` router prefix)\n *\n * Groq and OpenCode-hosted are omitted from the generated menu because they\n * are not represented in the shared catalog / profile system today. The\n * `models.env` map still carries their key env vars, so free-form `model`\n * overrides and manually-curated profiles continue to work.\n */\nfunction buildOpencodeModelMenu(): Array<{ id: string; provider: string }> {\n const supported = [\n { provider: \"anthropic\", prefix: \"anthropic\" },\n { provider: \"openai\", prefix: \"openai\" },\n { provider: \"openrouter\", prefix: \"openrouter\" },\n ] as const\n\n const seen = new Set<string>()\n const out: Array<{ id: string; provider: string }> = []\n\n for (const { provider, prefix } of supported) {\n for (const model of listModels({ kind: \"llm\", provider })) {\n const bareId = model.id\n const canonicalId = bareId.includes(\"/\") ? bareId : `${prefix}/${bareId}`\n const id = provider === \"openrouter\" ? `openrouter/${bareId}` : canonicalId\n if (seen.has(id)) continue\n seen.add(id)\n out.push({ id, provider })\n }\n }\n\n return out.sort((a, b) => {\n if (a.provider !== b.provider) return a.provider.localeCompare(b.provider)\n return a.id.localeCompare(b.id)\n })\n}\n\nexport const opencode: AgentCliHandle = defineAgentCli({\n name: \"opencode\",\n id: \"opencode\",\n description:\n \"sst/opencode — open-source coding agent with first-party ACP mode. Spawned via `npx -y opencode-ai acp` and driven over stdio JSON-RPC. Multi-provider (Anthropic / OpenAI / OpenRouter / Groq / OpenCode hosted).\",\n version: \"0.1.0\",\n bin: \"npx\",\n bin_args: [\"-y\", \"opencode-ai\", \"acp\"],\n install: [\n { method: \"npm\", package: \"opencode-ai\", global: true },\n { method: \"curl\", url: \"https://opencode.ai/install\" },\n ],\n version_check: {\n cmd: \"npm view opencode-ai version\",\n parse: \"(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)\",\n range: \">=1.0.0\",\n timeout_ms: 15_000,\n },\n auth: {\n ref: \"./SECRETS.md\",\n state: {\n env: [\n \"OPENCODE_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"GROQ_API_KEY\",\n ],\n },\n },\n sandbox: \"./SANDBOX.md\",\n protocol: \"acp\",\n acp: \"./opencode-acp.ACP.md\",\n session: {\n mode: \"persistent\",\n idle_timeout_ms: 1_800_000,\n context_carryover: true,\n },\n // opencode routes by the model id's own `<provider>/<id>` prefix itself, no\n // adapter mode — unlike claude-sdk/claude-code, which need ANTHROPIC_BASE_URL\n // pre-wired by a mode. So the route falls out of the chosen model →\n // derived-from-model. The API key env var is also derived from the model's\n // provider prefix, so the runtime must know to present api-key auth on the\n // model-derived direct endpoint.\n routeSelection: \"derived-from-model\",\n modelDerivedApiKey: true,\n // opencode ships TWO native OAuth logins reached via `opencode auth login`\n // (provider selection), both stored in its own auth.json — the runtime\n // declares one provider-scoped `authSubscription` surface per login (see\n // `subscriptionSurfaceFor` in `@agentproto/runtime`'s spawn-defaults.ts,\n // which resolves the matching entry for a spawn's resolved provider).\n // Both are EXTERNAL: the runtime injects no bearer — an agentproto-held\n // `sk-ant-oat…`/ChatGPT access token presented on opencode's x-api-key\n // channel is rejected upstream as an invalid key, so the ONLY working\n // subscription path is the CLI's own login. Each entry verifies its own\n // login is present (fail-loud, via the `opencode` provision recipe's\n // `anthropic-oauth`/`openai-oauth` methods) and scrubs the matching\n // api-key var so a leftover ANTHROPIC_API_KEY/OPENAI_API_KEY can't\n // override it.\n // - anthropic: \"Claude Pro/Max\" OAuth login (browser/headless).\n // - openai: \"ChatGPT Pro/Plus\" OAuth login (browser/headless, against\n // auth.openai.com). Reverse-engineered from the shipped binary (no\n // OSS source available for this build): opencode keys BOTH the\n // ChatGPT OAuth login and the plain API-key credential for this\n // provider under the SAME auth.json key `openai` (there is no\n // separate \"chatgpt\" key) — the generic `Cli.providers.login` /\n // `Auth.set(provider.id, …)` write path is provider-id-keyed, not\n // method-keyed. See the `opencode` provision recipe's `openai-oauth`\n // method docblock for the full trace.\n // Each is scoped to its own `provider`, so neither ever lights up the\n // other's (or openrouter/groq's) models.\n authSubscription: [\n { external: true, provider: \"anthropic\" },\n { external: true, provider: \"openai\" },\n ],\n models: {\n // Default to a canonical catalog model (claude-sonnet-4-5). The legacy\n // alias `claude-sonnet-4-6` still resolves to the same model, but the\n // generated menu uses canonical ids from the shared catalog.\n default: \"anthropic/claude-sonnet-4-5\",\n // Generated from the shared provider catalog so the Configuration Lab /\n // harness picker shows genuinely runnable Anthropic / OpenAI / OpenRouter\n // models instead of a hardcoded 3-item list. Groq and OpenCode-hosted are\n // omitted because they are not represented in the catalog/profile system\n // today; the free-form `model` option and `models.env` still support them.\n allowed: buildOpencodeModelMenu(),\n env: {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n opencode: \"OPENCODE_API_KEY\",\n groq: \"GROQ_API_KEY\",\n },\n },\n capabilities: {\n streaming: true,\n tool_calls: true,\n sub_agents: false,\n file_io: true,\n // OpenCode forwards ACP image content blocks to the underlying\n // provider (Anthropic Messages API for Claude models, OpenAI vision\n // for GPT-5). Hosts SHOULD send `{type: \"image\", data, mimeType}`\n // blocks alongside text in `session.send`.\n multimodal: true,\n // OpenCode persists session state internally and the ACP server\n // implements newSession/loadSession/resumeSession. Pair with the\n // `native-resume` continuation strategy for cold-start reattach.\n resumable: true,\n bidirectional: true,\n },\n // No manifest `modes[]`: opencode's operation profiles (default / plan /\n // build) are POSTURE, which no longer lives in the manifest (SPEC §3.4a).\n // opencode's own ACP server already advertises these as native session modes\n // and switches them on the wire via `session/set_config_option`\n // (configId:\"mode\") / `session/set_mode` — precisely the harness ACP mode\n // registry (`SessionModeState.availableModes`) posture is now sourced from,\n // so declaring them here would only duplicate the harness's own truth. route\n // comes from the model catalog and opencode has no context mode, so the array\n // would be empty — omit it.\n options: [\n {\n id: \"model\",\n type: \"string\",\n description:\n \"Provider/model override for this operator binding (e.g. `openrouter/anthropic/claude-sonnet-4-6`). Applied via ACP \" +\n \"session/set_config_option after the session is created (see \" +\n \"`models.apply`, default \\\"config\\\"); no CLI flag for this exists on \" +\n \"`opencode acp`. An id the server can't resolve REFUSES the spawn \" +\n \"with the server's reason (derived-from-model guard in \" +\n \"`define-agent-cli.ts`): opencode routes AND bills by the id's own \" +\n \"provider prefix, so silently keeping the server's default model \" +\n \"would run a model (and a bill) the operator didn't ask for.\",\n },\n ],\n continuation: {\n default: \"native-resume\",\n supported: [\"native-resume\", \"pinned-session\", \"transcript\", \"none\"],\n pinned_session: {\n idle_timeout_ms: 1_800_000,\n key_scope: [\"conversation\", \"operator\"],\n },\n },\n tags: [\"opencode\", \"sst\", \"acp\", \"agent-runtime\", \"coding\"],\n})\n\nexport function opencodeRuntime(): AgentCliRuntime {\n return createAgentCliRuntime(opencode)\n}\n\n/**\n * Best-effort per-session usage reader, mirroring `readHermesUsage` in\n * `@agentproto/adapter-hermes`. OpenCode's live ACP `usage_update` event only\n * ever carries `{used, size, cost}` — no token fields exist on that wire\n * event at all (confirmed by disassembling the installed opencode acp\n * binary) — so `session_usage` can't get tokensIn/tokensOut from the live\n * stream the way it does for other adapters. OpenCode does persist full\n * token detail to its own sqlite store though (the same store\n * `exportOpenCodeSession` in `@agentproto/runtime`'s transcript-export.ts\n * reads for `sessions export --json`), so this hook re-reads it directly.\n */\nexport async function readOpenCodeUsage(\n sessionId: string,\n): Promise<{ costUsd?: number; tokensIn?: number; tokensOut?: number } | null> {\n try {\n // node:sqlite is a Node 22+ builtin. Build the specifier at runtime so the\n // bundler (esbuild/tsup) can't statically rewrite it — it strips the\n // `node:` prefix off this not-yet-recognised builtin, turning the import\n // into a missing `sqlite` package that throws and silently yields null.\n const sqliteSpecifier = [\"node\", \"sqlite\"].join(\":\")\n const { DatabaseSync } = (await import(sqliteSpecifier)) as unknown as {\n DatabaseSync: new (p: string, o?: { readOnly?: boolean }) => {\n prepare(sql: string): { get(...a: unknown[]): unknown }\n close(): void\n }\n }\n const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\")\n const dbPath = join(dataHome, \"opencode\", \"opencode.db\")\n const db = new DatabaseSync(dbPath, { readOnly: true })\n try {\n const row = db\n .prepare(\"SELECT cost, tokens_input AS ti, tokens_output AS to_ FROM session WHERE id = ?\")\n .get(sessionId) as { cost?: number; ti?: number; to_?: number } | undefined\n if (!row) return null\n return {\n ...(row.cost != null ? { costUsd: Number(row.cost) } : {}),\n ...(row.ti != null ? { tokensIn: row.ti } : {}),\n ...(row.to_ != null ? { tokensOut: row.to_ } : {}),\n }\n } finally {\n db.close()\n }\n } catch {\n return null\n }\n}\n\nexport type { AgentCliHandle, AgentCliRuntime }\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/adapter-opencode — AIP-45 adapter for sst/opencode.\n *\n * OpenCode ships first-party ACP support (`opencode acp`) so no\n * third-party wrapper is needed. We spawn it via `npx -y opencode-ai\n * acp` and drive it over stdio JSON-RPC the same way the claude-code\n * adapter drives @agentclientprotocol/claude-agent-acp.\n *\n * import { opencode, opencodeRuntime } from \"@agentproto/adapter-opencode\"\n * const session = await opencodeRuntime().start({\n * env: { ANTHROPIC_API_KEY: \"sk-...\" },\n * })\n * for await (const evt of session.send({ role: \"user\", content: \"...\" })) {\n * console.log(evt)\n * }\n * await session.close()\n */\n\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport {\n createAgentCliRuntime,\n defineAgentCli,\n type AgentCliHandle,\n type AgentCliRuntime,\n} from \"@agentproto/driver-agent-cli\"\nimport { listModels } from \"@agentproto/model-catalog\"\n\n/**\n * Build OpenCode's model menu from the shared provider catalog instead of a\n * hand-maintained allowlist. OpenCode routes by the model id's own\n * `<provider>/<id>` prefix, so every entry carries the billing provider the\n * runtime's eligibility projection needs.\n *\n * Only providers OpenCode genuinely supports today are included:\n * - Anthropic and OpenAI (direct vendor prefixes)\n * - OpenRouter (`openrouter/<vendor>/<id>` router prefix)\n *\n * Groq and OpenCode-hosted are omitted from the generated menu because they\n * are not represented in the shared catalog / profile system today. The\n * `models.env` map still carries their key env vars, so free-form `model`\n * overrides and manually-curated profiles continue to work.\n */\nfunction buildOpencodeModelMenu(): Array<{ id: string; provider: string }> {\n const supported = [\n { provider: \"anthropic\", prefix: \"anthropic\" },\n { provider: \"openai\", prefix: \"openai\" },\n { provider: \"openrouter\", prefix: \"openrouter\" },\n ] as const\n\n const seen = new Set<string>()\n const out: Array<{ id: string; provider: string }> = []\n\n for (const { provider, prefix } of supported) {\n for (const model of listModels({ kind: \"llm\", provider })) {\n const bareId = model.id\n const canonicalId = bareId.includes(\"/\") ? bareId : `${prefix}/${bareId}`\n const id = provider === \"openrouter\" ? `openrouter/${bareId}` : canonicalId\n if (seen.has(id)) continue\n seen.add(id)\n out.push({ id, provider })\n }\n }\n\n return out.sort((a, b) => {\n if (a.provider !== b.provider) return a.provider.localeCompare(b.provider)\n return a.id.localeCompare(b.id)\n })\n}\n\nexport const opencode: AgentCliHandle = defineAgentCli({\n name: \"opencode\",\n id: \"opencode\",\n description:\n \"sst/opencode — open-source coding agent with first-party ACP mode. Spawned via `npx -y opencode-ai acp` and driven over stdio JSON-RPC. Multi-provider (Anthropic / OpenAI / OpenRouter / Groq / OpenCode hosted).\",\n version: \"0.1.0\",\n bin: \"npx\",\n bin_args: [\"-y\", \"opencode-ai\", \"acp\"],\n install: [\n { method: \"npm\", package: \"opencode-ai\", global: true },\n { method: \"curl\", url: \"https://opencode.ai/install\" },\n ],\n version_check: {\n // PRESENCE probe — local binary check, not a registry query. Covers BOTH\n // install methods: the npm -g package and the curl installer both put an\n // `opencode` executable on PATH, and it runs `--version` fully offline.\n cmd: \"opencode --version\",\n parse: \"(\\\\d+\\\\.\\\\d+\\\\.\\\\d+)\",\n range: \">=1.0.0\",\n timeout_ms: 15_000,\n },\n auth: {\n ref: \"./SECRETS.md\",\n state: {\n env: [\n \"OPENCODE_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"GROQ_API_KEY\",\n ],\n },\n },\n sandbox: \"./SANDBOX.md\",\n protocol: \"acp\",\n acp: \"./opencode-acp.ACP.md\",\n session: {\n mode: \"persistent\",\n idle_timeout_ms: 1_800_000,\n context_carryover: true,\n },\n // opencode routes by the model id's own `<provider>/<id>` prefix itself, no\n // adapter mode — unlike claude-sdk/claude-code, which need ANTHROPIC_BASE_URL\n // pre-wired by a mode. So the route falls out of the chosen model →\n // derived-from-model. The API key env var is also derived from the model's\n // provider prefix, so the runtime must know to present api-key auth on the\n // model-derived direct endpoint.\n routeSelection: \"derived-from-model\",\n modelDerivedApiKey: true,\n // opencode ships TWO native OAuth logins reached via `opencode auth login`\n // (provider selection), both stored in its own auth.json — the runtime\n // declares one provider-scoped `authSubscription` surface per login (see\n // `subscriptionSurfaceFor` in `@agentproto/runtime`'s spawn-defaults.ts,\n // which resolves the matching entry for a spawn's resolved provider).\n // Both are EXTERNAL: the runtime injects no bearer — an agentproto-held\n // `sk-ant-oat…`/ChatGPT access token presented on opencode's x-api-key\n // channel is rejected upstream as an invalid key, so the ONLY working\n // subscription path is the CLI's own login. Each entry verifies its own\n // login is present (fail-loud, via the `opencode` provision recipe's\n // `anthropic-oauth`/`openai-oauth` methods) and scrubs the matching\n // api-key var so a leftover ANTHROPIC_API_KEY/OPENAI_API_KEY can't\n // override it.\n // - anthropic: \"Claude Pro/Max\" OAuth login (browser/headless).\n // - openai: \"ChatGPT Pro/Plus\" OAuth login (browser/headless, against\n // auth.openai.com). Reverse-engineered from the shipped binary (no\n // OSS source available for this build): opencode keys BOTH the\n // ChatGPT OAuth login and the plain API-key credential for this\n // provider under the SAME auth.json key `openai` (there is no\n // separate \"chatgpt\" key) — the generic `Cli.providers.login` /\n // `Auth.set(provider.id, …)` write path is provider-id-keyed, not\n // method-keyed. See the `opencode` provision recipe's `openai-oauth`\n // method docblock for the full trace.\n // Each is scoped to its own `provider`, so neither ever lights up the\n // other's (or openrouter/groq's) models.\n authSubscription: [\n { external: true, provider: \"anthropic\" },\n { external: true, provider: \"openai\" },\n ],\n models: {\n // Default to a canonical catalog model (claude-sonnet-4-5). The legacy\n // alias `claude-sonnet-4-6` still resolves to the same model, but the\n // generated menu uses canonical ids from the shared catalog.\n default: \"anthropic/claude-sonnet-4-5\",\n // Generated from the shared provider catalog so the Configuration Lab /\n // harness picker shows genuinely runnable Anthropic / OpenAI / OpenRouter\n // models instead of a hardcoded 3-item list. Groq and OpenCode-hosted are\n // omitted because they are not represented in the catalog/profile system\n // today; the free-form `model` option and `models.env` still support them.\n allowed: buildOpencodeModelMenu(),\n env: {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n opencode: \"OPENCODE_API_KEY\",\n groq: \"GROQ_API_KEY\",\n },\n },\n capabilities: {\n streaming: true,\n tool_calls: true,\n sub_agents: false,\n file_io: true,\n // OpenCode forwards ACP image content blocks to the underlying\n // provider (Anthropic Messages API for Claude models, OpenAI vision\n // for GPT-5). Hosts SHOULD send `{type: \"image\", data, mimeType}`\n // blocks alongside text in `session.send`.\n multimodal: true,\n // OpenCode persists session state internally and the ACP server\n // implements newSession/loadSession/resumeSession. Pair with the\n // `native-resume` continuation strategy for cold-start reattach.\n resumable: true,\n bidirectional: true,\n },\n // No manifest `modes[]`: opencode's operation profiles (default / plan /\n // build) are POSTURE, which no longer lives in the manifest (SPEC §3.4a).\n // opencode's own ACP server already advertises these as native session modes\n // and switches them on the wire via `session/set_config_option`\n // (configId:\"mode\") / `session/set_mode` — precisely the harness ACP mode\n // registry (`SessionModeState.availableModes`) posture is now sourced from,\n // so declaring them here would only duplicate the harness's own truth. route\n // comes from the model catalog and opencode has no context mode, so the array\n // would be empty — omit it.\n options: [\n {\n id: \"model\",\n type: \"string\",\n description:\n \"Provider/model override for this operator binding (e.g. `openrouter/anthropic/claude-sonnet-4-6`). Applied via ACP \" +\n \"session/set_config_option after the session is created (see \" +\n \"`models.apply`, default \\\"config\\\"); no CLI flag for this exists on \" +\n \"`opencode acp`. An id the server can't resolve REFUSES the spawn \" +\n \"with the server's reason (derived-from-model guard in \" +\n \"`define-agent-cli.ts`): opencode routes AND bills by the id's own \" +\n \"provider prefix, so silently keeping the server's default model \" +\n \"would run a model (and a bill) the operator didn't ask for.\",\n },\n ],\n continuation: {\n default: \"native-resume\",\n supported: [\"native-resume\", \"pinned-session\", \"transcript\", \"none\"],\n pinned_session: {\n idle_timeout_ms: 1_800_000,\n key_scope: [\"conversation\", \"operator\"],\n },\n },\n tags: [\"opencode\", \"sst\", \"acp\", \"agent-runtime\", \"coding\"],\n})\n\nexport function opencodeRuntime(): AgentCliRuntime {\n return createAgentCliRuntime(opencode)\n}\n\n/**\n * Best-effort per-session usage reader, mirroring `readHermesUsage` in\n * `@agentproto/adapter-hermes`. OpenCode's live ACP `usage_update` event only\n * ever carries `{used, size, cost}` — no token fields exist on that wire\n * event at all (confirmed by disassembling the installed opencode acp\n * binary) — so `session_usage` can't get tokensIn/tokensOut from the live\n * stream the way it does for other adapters. OpenCode does persist full\n * token detail to its own sqlite store though (the same store\n * `exportOpenCodeSession` in `@agentproto/runtime`'s transcript-export.ts\n * reads for `sessions export --json`), so this hook re-reads it directly.\n */\nexport async function readOpenCodeUsage(\n sessionId: string,\n): Promise<{ costUsd?: number; tokensIn?: number; tokensOut?: number } | null> {\n try {\n // node:sqlite is a Node 22+ builtin. Build the specifier at runtime so the\n // bundler (esbuild/tsup) can't statically rewrite it — it strips the\n // `node:` prefix off this not-yet-recognised builtin, turning the import\n // into a missing `sqlite` package that throws and silently yields null.\n const sqliteSpecifier = [\"node\", \"sqlite\"].join(\":\")\n const { DatabaseSync } = (await import(sqliteSpecifier)) as unknown as {\n DatabaseSync: new (p: string, o?: { readOnly?: boolean }) => {\n prepare(sql: string): { get(...a: unknown[]): unknown }\n close(): void\n }\n }\n const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\")\n const dbPath = join(dataHome, \"opencode\", \"opencode.db\")\n const db = new DatabaseSync(dbPath, { readOnly: true })\n try {\n const row = db\n .prepare(\"SELECT cost, tokens_input AS ti, tokens_output AS to_ FROM session WHERE id = ?\")\n .get(sessionId) as { cost?: number; ti?: number; to_?: number } | undefined\n if (!row) return null\n return {\n ...(row.cost != null ? { costUsd: Number(row.cost) } : {}),\n ...(row.ti != null ? { tokensIn: row.ti } : {}),\n ...(row.to_ != null ? { tokensOut: row.to_ } : {}),\n }\n } finally {\n db.close()\n }\n } catch {\n return null\n }\n}\n\nexport type { AgentCliHandle, AgentCliRuntime }\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/adapter-opencode",
3
- "version": "1.2.0",
3
+ "version": "1.2.3",
4
4
  "description": "@agentproto/adapter-opencode — AIP-45 AGENT-CLI adapter for sst/opencode. Spawns `opencode acp` (via npx -y opencode-ai) and drives OpenCode as an agentproto agent CLI over stdio JSON-RPC.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -41,14 +41,14 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@agentproto/driver-agent-cli": "2.4.1",
45
- "@agentproto/model-catalog": "0.9.2"
44
+ "@agentproto/driver-agent-cli": "2.4.2",
45
+ "@agentproto/model-catalog": "0.9.4"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "^25.6.2",
48
+ "@types/node": "^25.9.5",
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^5.9.3",
51
- "vitest": "^3.2.4",
51
+ "vitest": "^3.2.7",
52
52
  "@agentproto/tooling": "0.1.0"
53
53
  },
54
54
  "scripts": {