@agentproto/adapter-opencode 1.6.7 → 1.7.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.
package/OPENCODE.md CHANGED
@@ -82,6 +82,51 @@ curl -fsSL https://opencode.ai/install | bash
82
82
  The npx form `npx -y opencode-ai acp` works without a global install
83
83
  — the adapter prefers this for ephemeral / sandboxed spawns.
84
84
 
85
+ ## First-request size
86
+
87
+ A bare `opencode` start is not small. Measured on opencode 1.18.32 with a
88
+ capture endpoint that records the first `chat/completions` body (about 4
89
+ characters per token), prompt "Reply with exactly: OK. Do not use any tool.":
90
+
91
+ | Variant (cwd, mount) | Request body | ~Tokens |
92
+ |--------------------------------------------------------|-------------:|--------:|
93
+ | Before: `model-bench` (under `agentik-studio`), global opencode config | 548,771 chars | ~137k |
94
+ | Before: empty temp dir, global opencode config | 455,232 | ~114k |
95
+ | Clean `HOME`, `model-bench` (AGENTS.md + skills) | 129,642 | ~32k |
96
+ | Clean `HOME`, empty temp dir (opencode's own floor) | 33,379 | ~8.3k |
97
+ | Clean `HOME`, empty dir, `daemonMount` eager (280 tools) | 428,530 | ~107k |
98
+ | Clean `HOME`, empty dir, `daemonMount` + `deferredTools` | 96,608 | ~24k |
99
+ | After: executor default (`lean`), `model-bench` | 33,751 | ~8.4k |
100
+ | After: executor default (`lean`) + `daemonMount` (deferred) | 67,385 | ~17k |
101
+
102
+ Real counts, same probe on `opencode-go/longcat-2.5-preview-free` in
103
+ `model-bench` (the provider's reported `tokensIn`): 140,909 with
104
+ `contextProfile: "full"` (reproduces the original 140,903), 7,672 with the
105
+ `lean` default.
106
+
107
+ Where the 548k went: tool schemas 447k (the *global* `mcp.agentproto`
108
+ bridge in `~/.config/opencode/opencode.jsonc` injects ~280 tool schemas into
109
+ every opencode start), skills list 85k (`.claude/skills`, `.agents/skills`,
110
+ home skill dirs), the repo's `AGENTS.md` ~23k, opencode's base prompt ~9.5k.
111
+ opencode's floor — 9.7k of system prompt plus its 11 built-in tools — is
112
+ about 33k chars, roughly 8k tokens.
113
+
114
+ The `lean` context mode (`contextProfile: "lean"`) turns off exactly the
115
+ agentproto-side parts: `OPENCODE_DISABLE_EXTERNAL_SKILLS`,
116
+ `OPENCODE_DISABLE_PROJECT_CONFIG` (no `AGENTS.md` autoload, no project
117
+ `opencode.json`) and an inline config disabling the global `agentproto` MCP
118
+ server. It is the default for **executor** sessions (a depth-0 spawn with no
119
+ delegation reach). Opt out per spawn with `contextProfile: "full"`, or
120
+ globally with `defaults.adapters.opencode.contextProfile: "full"`; opt a
121
+ supervisor in with `contextProfile: "lean"`.
122
+
123
+ Caveats: `lean` drops the repo's `AGENTS.md` and project opencode config, so
124
+ the agent no longer sees repo conventions unless the task prompt carries them.
125
+ Only the global `agentproto` MCP server is disabled — other MCP servers a user
126
+ declared globally stay on (opencode has no flag to drop them). A supervisor
127
+ with a `daemonMount` still gets the eager daemon tool list unless
128
+ `deferredTools: true`.
129
+
85
130
  ## Auth
86
131
 
87
132
  OpenCode reads provider keys from the environment. Set whichever
@@ -127,3 +172,40 @@ Neither endpoint is a login: **Go and Zen are API keys, not OAuth**. The
127
172
  adapter's two `authSubscription` surfaces remain opencode's own
128
173
  `opencode auth login` flows for Claude Pro/Max and ChatGPT, untouched by these
129
174
  routes.
175
+
176
+ ### Console workspaces (orgs) as separate wallets
177
+
178
+ opencode keeps one console login (an `account` row in
179
+ `~/.local/share/opencode/opencode.db`). At every start it calls
180
+ `<account.url>/api/config` for the **active org** and merges the returned
181
+ `provider` block over all other config — env keys and inline config included.
182
+ With that login present, every spawn bills the active org no matter which
183
+ agentproto profile it names.
184
+
185
+ The adapter therefore declares `credentialDataHome: { env: "XDG_DATA_HOME" }`:
186
+ whenever a spawn engages a profile credential, opencode runs with a login-less
187
+ data dir (`<configDir>/auth-data`, or a throwaway temp dir), so a plain
188
+ api-key profile really bills its own key. Session usage and transcripts are read
189
+ from that isolated `opencode.db` when it exists.
190
+
191
+ A console org is a **sub-account** of the console login (see the auth package's
192
+ sub-account providers): a source-backed profile that pins `org:<orgId>` — no
193
+ token is stored. The provider lives in this adapter (`src/subaccounts.ts`) and is
194
+ registered by the CLI at start-up.
195
+
196
+ ```bash
197
+ agentproto auth subaccounts list opencode-console # list orgs of the logged-in console account
198
+ agentproto auth subaccounts list opencode-console --create --prefix opencode # -> opencode-<org-name> profiles
199
+ agentproto auth profile create opencode-ws01 --endpoint opencode-go \
200
+ --method api-key --source opencode-console --subaccount org:<orgId>
201
+ ```
202
+
203
+ `auth profile opencode-orgs` still works as a deprecated alias, and profiles
204
+ stored as `source: "opencode-console:<orgId>"` are migrated on read.
205
+
206
+ At spawn the provider reads the console session read-only from `opencode.db`,
207
+ fetches that org's provider block (`/api/config` with `x-org-id`), and injects
208
+ the bearer (`OPENCODE_CONSOLE_TOKEN`) plus the block (`OPENCODE_CONFIG_CONTENT`).
209
+ The session is never refreshed here (the refresh token rotates); an expired
210
+ session fails loud with a re-login hint. A `Go usage limit exceeded` turn
211
+ failure is tagged with the wallet profile that hit it.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,83 @@
1
1
  import { AgentCliHandle, AgentCliRuntime } from '@agentproto/driver-agent-cli';
2
2
  export { AgentCliHandle, AgentCliRuntime } from '@agentproto/driver-agent-cli';
3
+ import { SubaccountProvider } from '@agentproto/auth';
4
+
5
+ /**
6
+ * Sub-account provider for opencode console orgs ("workspaces").
7
+ *
8
+ * opencode keeps ONE console login (`account` row + `account_state.active_org_id`
9
+ * in its sqlite db) and, on every start, fetches `<account.url>/api/config`
10
+ * for the active org and merges the returned `provider` block over all other
11
+ * config — env keys and inline config included. So with that login present,
12
+ * every opencode spawn bills the active org, whatever agentproto profile it
13
+ * names. A profile on this source pins ONE org instead: the console session is
14
+ * read (never written) from opencode.db, the pinned org's provider block is
15
+ * fetched with it, and the spawn gets the token + that block through env while
16
+ * the driver runs opencode in a login-less data dir
17
+ * (`AgentCliDefinition.credentialDataHome`) so the stored login can't re-point
18
+ * the providers back.
19
+ *
20
+ * The profile stores only `source: "opencode-console"` + `subaccount: {kind:
21
+ * "org", id}` — no token. The token is read fresh from opencode.db at every
22
+ * spawn. Legacy profiles (`source: "opencode-console:<orgId>"`) are migrated on
23
+ * read. It is NOT refreshed
24
+ * here: the refresh token rotates, and rotating it behind opencode's back would
25
+ * log the operator out of opencode itself. An expired session fails loud.
26
+ */
27
+
28
+ declare const OPENCODE_CONSOLE_SOURCE = "opencode-console";
29
+ declare const OPENCODE_CONSOLE_ORG_KIND = "org";
30
+ /** Env var opencode's console provider block reads its bearer from. */
31
+ declare const OPENCODE_CONSOLE_TOKEN_ENV = "OPENCODE_CONSOLE_TOKEN";
32
+ declare function opencodeDbPath(env?: Record<string, string | undefined>): string;
33
+ interface OpencodeConsoleAccount {
34
+ id: string;
35
+ email?: string;
36
+ /** Console base URL, e.g. `https://opencode.ai/console`. */
37
+ url: string;
38
+ accessToken: string;
39
+ /** Epoch ms, when opencode recorded one. */
40
+ expiresAt?: number;
41
+ }
42
+ interface OpencodeConsoleDeps {
43
+ dbPath?: string;
44
+ fetch?: typeof fetch;
45
+ now?: () => number;
46
+ }
47
+ /**
48
+ * Read the logged-in console account from opencode.db, read-only. The active
49
+ * account wins when several are stored. Throws {@link SubaccountError}
50
+ * when there is no login, or when the session is expired.
51
+ */
52
+ declare function readOpencodeConsoleAccount(deps?: OpencodeConsoleDeps): Promise<OpencodeConsoleAccount>;
53
+ interface OpencodeConsoleOrg {
54
+ id: string;
55
+ name: string;
56
+ }
57
+ /** Orgs the logged-in console account can use (`GET /api/orgs`). */
58
+ declare function listOpencodeConsoleOrgs(deps?: OpencodeConsoleDeps): Promise<{
59
+ account: OpencodeConsoleAccount;
60
+ orgs: OpencodeConsoleOrg[];
61
+ }>;
62
+ interface ResolvedOpencodeConsoleOrg {
63
+ orgId: string;
64
+ /** The org-scoped console bearer — goes into {@link OPENCODE_CONSOLE_TOKEN_ENV}. */
65
+ credential: string;
66
+ /** Env beside the bearer: the org's `provider` block as inline config. */
67
+ extraEnv: Record<string, string>;
68
+ }
69
+ /**
70
+ * Resolve a pinned org to what the spawn needs: the bearer and the org's
71
+ * provider block (which names the org id in its `x-opencode-org-id` header).
72
+ * Fails loud — never falls back to another org or an ambient login.
73
+ */
74
+ declare function resolveOpencodeConsoleOrg(orgId: string, profileId: string, deps?: OpencodeConsoleDeps): Promise<ResolvedOpencodeConsoleOrg>;
75
+ /**
76
+ * The registry entry. `deps` is injectable for tests (db path, fetch, clock).
77
+ */
78
+ declare function createOpencodeSubaccountProvider(deps?: OpencodeConsoleDeps): SubaccountProvider;
79
+ /** The default instance, reading the operator's own opencode.db. */
80
+ declare const opencodeSubaccounts: SubaccountProvider;
3
81
 
4
82
  /**
5
83
  * @agentproto/adapter-opencode — AIP-45 adapter for sst/opencode.
@@ -19,6 +97,14 @@ export { AgentCliHandle, AgentCliRuntime } from '@agentproto/driver-agent-cli';
19
97
  * await session.close()
20
98
  */
21
99
 
100
+ /** Inline config the `lean` mode layers over the user's global opencode config. */
101
+ declare const LEAN_INLINE_CONFIG: {
102
+ readonly mcp: {
103
+ readonly agentproto: {
104
+ readonly enabled: false;
105
+ };
106
+ };
107
+ };
22
108
  declare const opencode: AgentCliHandle;
23
109
  declare function opencodeRuntime(): AgentCliRuntime;
24
110
  /**
@@ -32,7 +118,10 @@ declare function opencodeRuntime(): AgentCliRuntime;
32
118
  * `exportOpenCodeSession` in `@agentproto/runtime`'s transcript-export.ts
33
119
  * reads for `sessions export --json`), so this hook re-reads it directly.
34
120
  */
35
- declare function readOpenCodeUsage(sessionId: string): Promise<{
121
+ declare function readOpenCodeUsage(sessionId: string, ctx?: {
122
+ cwd?: string;
123
+ configDir?: string;
124
+ }): Promise<{
36
125
  costUsd?: number;
37
126
  tokensIn?: number;
38
127
  tokensOut?: number;
@@ -41,4 +130,4 @@ declare function readOpenCodeUsage(sessionId: string): Promise<{
41
130
  reasoningTokens?: number;
42
131
  } | null>;
43
132
 
44
- export { opencode, opencodeRuntime, readOpenCodeUsage };
133
+ export { LEAN_INLINE_CONFIG, OPENCODE_CONSOLE_ORG_KIND, OPENCODE_CONSOLE_SOURCE, OPENCODE_CONSOLE_TOKEN_ENV, createOpencodeSubaccountProvider, listOpencodeConsoleOrgs, opencode, opencodeDbPath, opencodeRuntime, opencodeSubaccounts, readOpenCodeUsage, readOpencodeConsoleAccount, resolveOpencodeConsoleOrg };
package/dist/index.mjs CHANGED
@@ -1,13 +1,154 @@
1
+ import { existsSync } from 'fs';
1
2
  import { homedir } from 'os';
2
3
  import { join } from 'path';
3
- import { defineAgentCli, createAgentCliRuntime } from '@agentproto/driver-agent-cli';
4
+ import { defineAgentCli, createAgentCliRuntime, CREDENTIAL_DATA_SUBDIR } from '@agentproto/driver-agent-cli';
4
5
  import { getModelsByProvider, listModels } from '@agentproto/model-catalog';
6
+ import { SubaccountError } from '@agentproto/auth';
5
7
 
6
8
  /**
7
9
  * @agentproto/adapter-opencode v0.1.0-alpha
8
10
  * AIP-45 adapter for sst/opencode via its built-in `opencode acp` mode.
9
11
  */
10
12
 
13
+ var OPENCODE_CONSOLE_SOURCE = "opencode-console";
14
+ var OPENCODE_CONSOLE_ORG_KIND = "org";
15
+ var OPENCODE_CONSOLE_TOKEN_ENV = "OPENCODE_CONSOLE_TOKEN";
16
+ var FETCH_TIMEOUT_MS = 1e4;
17
+ var EXPIRY_SKEW_MS = 6e4;
18
+ function parseLegacyOpencodeConsoleSource(source) {
19
+ if (!source?.startsWith(`${OPENCODE_CONSOLE_SOURCE}:`)) return void 0;
20
+ const orgId = source.slice(OPENCODE_CONSOLE_SOURCE.length + 1);
21
+ return orgId.length > 0 ? { kind: OPENCODE_CONSOLE_ORG_KIND, id: orgId } : void 0;
22
+ }
23
+ function opencodeDbPath(env = process.env) {
24
+ const dataHome = env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
25
+ return join(dataHome, "opencode", "opencode.db");
26
+ }
27
+ async function openReadonly(dbPath) {
28
+ const specifier = ["node", "sqlite"].join(":");
29
+ const { DatabaseSync } = await import(
30
+ /* @vite-ignore */
31
+ specifier
32
+ );
33
+ return new DatabaseSync(dbPath, { readOnly: true });
34
+ }
35
+ function loginHint() {
36
+ return "Log in once with `opencode auth login` (opencode.ai/console) so its console session is fresh, then retry.";
37
+ }
38
+ async function readOpencodeConsoleAccount(deps = {}) {
39
+ const dbPath = deps.dbPath ?? opencodeDbPath();
40
+ const now = deps.now ?? Date.now;
41
+ let row;
42
+ let db;
43
+ try {
44
+ db = await openReadonly(dbPath);
45
+ const active = db.prepare("SELECT active_account_id FROM account_state LIMIT 1").get();
46
+ row = active?.active_account_id ? db.prepare("SELECT * FROM account WHERE id = ?").get(active.active_account_id) : db.prepare("SELECT * FROM account LIMIT 1").get();
47
+ } catch (err) {
48
+ throw new SubaccountError(
49
+ `opencode console login unreadable at ${dbPath} (${err instanceof Error ? err.message : String(err)}). ${loginHint()}`
50
+ );
51
+ } finally {
52
+ try {
53
+ db?.close();
54
+ } catch {
55
+ }
56
+ }
57
+ if (!row || !row.access_token) {
58
+ throw new SubaccountError(
59
+ `no opencode console account in ${dbPath}. ${loginHint()}`
60
+ );
61
+ }
62
+ const expiresAt = typeof row.token_expiry === "number" ? row.token_expiry : void 0;
63
+ if (expiresAt !== void 0 && expiresAt - now() < EXPIRY_SKEW_MS) {
64
+ throw new SubaccountError(
65
+ `the opencode console session for ${row.email ?? row.id} expired at ${new Date(expiresAt).toISOString()}. ${loginHint()}`
66
+ );
67
+ }
68
+ return {
69
+ id: row.id,
70
+ ...row.email ? { email: row.email } : {},
71
+ url: row.url.replace(/\/+$/, ""),
72
+ accessToken: row.access_token,
73
+ ...expiresAt !== void 0 ? { expiresAt } : {}
74
+ };
75
+ }
76
+ async function consoleGet(account, path, deps, orgId) {
77
+ const doFetch = deps.fetch ?? fetch;
78
+ const res = await doFetch(`${account.url}${path}`, {
79
+ headers: {
80
+ accept: "application/json",
81
+ authorization: `Bearer ${account.accessToken}`,
82
+ ...orgId ? { "x-org-id": orgId } : {}
83
+ },
84
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
85
+ });
86
+ if (!res.ok) throw new Error(`${path} \u2192 HTTP ${res.status}`);
87
+ return res.json();
88
+ }
89
+ async function listOpencodeConsoleOrgs(deps = {}) {
90
+ const account = await readOpencodeConsoleAccount(deps);
91
+ let body;
92
+ try {
93
+ body = await consoleGet(account, "/api/orgs", deps);
94
+ } catch (err) {
95
+ throw new SubaccountError(
96
+ `opencode console org listing failed (${err instanceof Error ? err.message : String(err)}). ${loginHint()}`
97
+ );
98
+ }
99
+ const orgs = (Array.isArray(body) ? body : []).filter((o) => typeof o?.id === "string").map((o) => ({ id: o.id, name: typeof o.name === "string" ? o.name : o.id }));
100
+ return { account, orgs };
101
+ }
102
+ async function resolveOpencodeConsoleOrg(orgId, profileId, deps = {}) {
103
+ const account = await readOpencodeConsoleAccount(deps);
104
+ let provider;
105
+ try {
106
+ const body = await consoleGet(account, "/api/config", deps, orgId);
107
+ provider = body?.config?.provider;
108
+ } catch (err) {
109
+ throw new SubaccountError(
110
+ `profile "${profileId}": opencode org ${orgId} config fetch failed (${err instanceof Error ? err.message : String(err)}) \u2014 the console account ${account.email ?? account.id} may not belong to that org. ${loginHint()}`
111
+ );
112
+ }
113
+ if (!provider || typeof provider !== "object" || Object.keys(provider).length === 0) {
114
+ throw new SubaccountError(
115
+ `profile "${profileId}": opencode org ${orgId} returned no provider config.`
116
+ );
117
+ }
118
+ return {
119
+ orgId,
120
+ credential: account.accessToken,
121
+ extraEnv: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ provider }) }
122
+ };
123
+ }
124
+ function createOpencodeSubaccountProvider(deps = {}) {
125
+ return {
126
+ id: "opencode-console",
127
+ source: OPENCODE_CONSOLE_SOURCE,
128
+ kinds: [OPENCODE_CONSOLE_ORG_KIND],
129
+ defaultEndpoint: "opencode-go",
130
+ profilePrefix: "opencode",
131
+ migrateLegacySource: parseLegacyOpencodeConsoleSource,
132
+ async list() {
133
+ const { account, orgs } = await listOpencodeConsoleOrgs(deps);
134
+ return {
135
+ account: { id: account.id, ...account.email ? { label: account.email } : {} },
136
+ subaccounts: orgs.map((o) => ({ kind: OPENCODE_CONSOLE_ORG_KIND, id: o.id, name: o.name }))
137
+ };
138
+ },
139
+ async resolve(profile) {
140
+ const resolved = await resolveOpencodeConsoleOrg(profile.subaccount.id, profile.id, deps);
141
+ return {
142
+ credential: resolved.credential,
143
+ credentialEnvOverride: OPENCODE_CONSOLE_TOKEN_ENV,
144
+ env: resolved.extraEnv
145
+ };
146
+ }
147
+ };
148
+ }
149
+ var opencodeSubaccounts = createOpencodeSubaccountProvider();
150
+
151
+ // src/index.ts
11
152
  function buildOpencodeModelMenu() {
12
153
  const supported = [
13
154
  { provider: "anthropic", prefix: "anthropic", router: false },
@@ -34,6 +175,7 @@ function buildOpencodeModelMenu() {
34
175
  return a.id.localeCompare(b.id);
35
176
  });
36
177
  }
178
+ var LEAN_INLINE_CONFIG = { mcp: { agentproto: { enabled: false } } };
37
179
  var opencode = defineAgentCli({
38
180
  name: "opencode",
39
181
  id: "opencode",
@@ -71,6 +213,12 @@ var opencode = defineAgentCli({
71
213
  ]
72
214
  }
73
215
  },
216
+ // opencode keeps a console login (`account` row) in <XDG_DATA_HOME>/opencode/
217
+ // opencode.db and, at every start, merges that login's active-org provider
218
+ // block over env keys and inline config — so an engaged profile would bill
219
+ // the ACTIVE ORG, not its own credential. An engaged spawn therefore runs in
220
+ // a login-less data dir (`<configDir>/auth-data`).
221
+ credentialDataHome: { env: "XDG_DATA_HOME" },
74
222
  sandbox: "./SANDBOX.md",
75
223
  protocol: "acp",
76
224
  acp: "./opencode-acp.ACP.md",
@@ -158,15 +306,32 @@ var opencode = defineAgentCli({
158
306
  resumable: true,
159
307
  bidirectional: true
160
308
  },
161
- // No manifest `modes[]`: opencode's operation profiles (default / plan /
162
- // build) are POSTURE, which no longer lives in the manifest (SPEC §3.4a).
163
- // opencode's own ACP server already advertises these as native session modes
164
- // and switches them on the wire via `session/set_config_option`
165
- // (configId:"mode") / `session/set_mode` — precisely the harness ACP mode
166
- // registry (`SessionModeState.availableModes`) posture is now sourced from,
167
- // so declaring them here would only duplicate the harness's own truth. route
168
- // comes from the model catalog and opencode has no context mode, so the array
169
- // would be empty — omit it.
309
+ // `modes[]` carries ONE entry, the `context` axis (`lean`). opencode's
310
+ // operation profiles (default / plan / build) are POSTURE, which no longer
311
+ // lives in the manifest (SPEC §3.4a): opencode's own ACP server advertises
312
+ // them as native session modes and switches them on the wire via
313
+ // `session/set_config_option` (configId:"mode") / `session/set_mode` —
314
+ // precisely the harness ACP mode registry (`SessionModeState.availableModes`)
315
+ // posture is sourced from. route comes from the model catalog. What enters
316
+ // the model's context has no ACP home, so it is the one thing declared here.
317
+ modes: [
318
+ {
319
+ id: "lean",
320
+ kind: "context",
321
+ description: "Keep opencode's first request near its own floor (~8k tokens): no auto-discovered skills (`.claude/skills`, `.agents/skills`, `~/.claude/skills` \u2014 a repo with 100+ skills lists them all, ~20k tokens), no AGENTS.md / CLAUDE.md / project opencode.json pulled in from the cwd and its parents (agentproto's own AGENTS.md pointer in the first prompt already names the contract), and the user's global `agentproto` MCP bridge (`mcp.agentproto` in ~/.config/opencode/opencode.jsonc \u2014 280+ tool schemas, ~100k tokens on every start) switched off. A daemon mount (`daemonMount`) is unaffected and stays the way to reach daemon tools (deferred by default for executors).",
322
+ // OPENCODE_DISABLE_EXTERNAL_SKILLS and OPENCODE_DISABLE_PROJECT_CONFIG are
323
+ // read by the pinned opencode (1.18.x) at startup; measured in
324
+ // `OPENCODE.md` ("First-request size"). OPENCODE_CONFIG_CONTENT is the
325
+ // highest-precedence config layer and is deep-merged over the user's
326
+ // global config, so `enabled:false` turns the global bridge off without
327
+ // touching the file. Other user-declared global MCP servers are left alone.
328
+ env: {
329
+ OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
330
+ OPENCODE_DISABLE_PROJECT_CONFIG: "1",
331
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(LEAN_INLINE_CONFIG)
332
+ }
333
+ }
334
+ ],
170
335
  options: [
171
336
  {
172
337
  id: "model",
@@ -217,12 +382,13 @@ var opencode = defineAgentCli({
217
382
  function opencodeRuntime() {
218
383
  return createAgentCliRuntime(opencode);
219
384
  }
220
- async function readOpenCodeUsage(sessionId) {
385
+ async function readOpenCodeUsage(sessionId, ctx) {
221
386
  try {
222
387
  const sqliteSpecifier = ["node", "sqlite"].join(":");
223
388
  const { DatabaseSync } = await import(sqliteSpecifier);
389
+ const isolatedDb = ctx?.configDir ? join(ctx.configDir, CREDENTIAL_DATA_SUBDIR, "opencode", "opencode.db") : void 0;
224
390
  const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
225
- const dbPath = join(dataHome, "opencode", "opencode.db");
391
+ const dbPath = isolatedDb && existsSync(isolatedDb) ? isolatedDb : join(dataHome, "opencode", "opencode.db");
226
392
  const db = new DatabaseSync(dbPath, { readOnly: true });
227
393
  try {
228
394
  const row = db.prepare("SELECT * FROM session WHERE id = ?").get(sessionId);
@@ -243,6 +409,6 @@ async function readOpenCodeUsage(sessionId) {
243
409
  }
244
410
  }
245
411
 
246
- export { opencode, opencodeRuntime, readOpenCodeUsage };
412
+ export { LEAN_INLINE_CONFIG, OPENCODE_CONSOLE_ORG_KIND, OPENCODE_CONSOLE_SOURCE, OPENCODE_CONSOLE_TOKEN_ENV, createOpencodeSubaccountProvider, listOpencodeConsoleOrgs, opencode, opencodeDbPath, opencodeRuntime, opencodeSubaccounts, readOpenCodeUsage, readOpencodeConsoleAccount, resolveOpencodeConsoleOrg };
247
413
  //# sourceMappingURL=index.mjs.map
248
414
  //# sourceMappingURL=index.mjs.map
@@ -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 { getModelsByProvider, 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 * - OpenCode's own two hosted endpoints — `opencode-go/<id>` (the flat Go\n * subscription) and `opencode/<id>` (Zen, pay-as-you-go), both matching\n * opencode's own config spelling\n *\n * Groq is still omitted: it is not a billing/auth `CatalogProvider` (no\n * pricing generator), so there is nothing in the shared catalog to enumerate.\n * The `models.env` map carries its key env var, so a free-form `model`\n * override and a manually-curated profile continue to work. (The two OpenCode\n * endpoints USED to be omitted for the same reason — they are now first-class\n * catalog providers with their own generated route tables, which is what makes\n * them enumerable here.)\n *\n * `router: true` marks a provider whose surface lives ONLY in its generated\n * route table and is deliberately not spread into `LLM_PRICING_CATALOG` (a\n * bare `claude-sonnet-5` must keep meaning direct Anthropic, not a Zen-priced\n * route). `listModels` returns nothing for those, so they are read through\n * `getModelsByProvider`, which folds the route table in.\n */\nfunction buildOpencodeModelMenu(): Array<{ id: string; provider: string }> {\n const supported = [\n { provider: \"anthropic\", prefix: \"anthropic\", router: false },\n { provider: \"openai\", prefix: \"openai\", router: false },\n { provider: \"openrouter\", prefix: \"openrouter\", router: false },\n { provider: \"opencode-go\", prefix: \"opencode-go\", router: true },\n { provider: \"opencode\", prefix: \"opencode\", router: true },\n ] as const\n\n const seen = new Set<string>()\n const out: Array<{ id: string; provider: string }> = []\n\n for (const { provider, prefix, router } of supported) {\n const models = router\n ? getModelsByProvider(provider).filter(model => model.kind === \"llm\")\n : listModels({ kind: \"llm\", provider })\n for (const model of models) {\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 // `--print-logs --log-level ERROR` makes the ACP server echo its structured\n // logs to stderr. Without it a provider 429/usage-cap is swallowed into an\n // internal retry loop and `session/prompt` never resolves, leaving the\n // daemon with a silent 0-token busy session. The driver's stderr hook\n // (`parseStderrStreamError`) reads those lines back into a turn error.\n bin_args: [\"-y\", \"opencode-ai\", \"acp\", \"--print-logs\", \"--log-level\", \"ERROR\"],\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 // / OpenCode Go / OpenCode Zen models instead of a hardcoded 3-item list.\n // Groq stays omitted — it is not a billing/auth CatalogProvider, so there\n // is nothing to enumerate; the free-form `model` option and `models.env`\n // still support it.\n allowed: buildOpencodeModelMenu(),\n env: {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n // Both OpenCode endpoints read the SAME var — opencode's own convention\n // (models.dev records `env: [\"OPENCODE_API_KEY\"]` for each), mirrored in\n // the catalog's `PROVIDER_KEY_ENV`. Two DIFFERENT secrets share the name,\n // so which balance a spawn bills is decided by the resolved auth profile's\n // endpoint (`opencode` vs `opencode-go`), not by this map.\n opencode: \"OPENCODE_API_KEY\",\n \"opencode-go\": \"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 id: \"effort\",\n // string, NOT enum: opencode advertises an `effort` config option\n // (category `thought_level`) ONLY for models that have a reasoning\n // axis, and the accepted vocabulary is MODEL-DEPENDENT — probed live\n // against `opencode acp` 1.18.x, one reasoning model offered\n // `high | max | default` while opencode's own `run --variant` help\n // names `high, max, minimal`. A static enum would reject labels that\n // are valid for models this adapter also routes to, so the value is\n // passed through and the server decides. Applied via ACP\n // `session/set_config_option(configId:\"effort\")` after the session is\n // created — `define-agent-cli.ts` reads `config.options.effort` and\n // forwards it to the ACP arm's `connect({effort})`; there is no\n // `opencode acp` CLI flag for it (`--variant` is a `run`-subcommand\n // flag, and the adapter spawns `acp`). The apply is best-effort and\n // non-fatal (`packages/acp`): a label the resolved model doesn't offer,\n // or a model with no effort axis at all, is warned about and ignored\n // rather than killing the spawn. Omit to keep the model's own default.\n type: \"string\" as const,\n description:\n \"Reasoning effort (opencode's per-model thought level / `variant`). \" +\n \"Applied via ACP session/set_config_option(configId:\\\"effort\\\") after \" +\n \"the session is created; no `opencode acp` CLI flag exists for it. \" +\n \"The accepted vocabulary is MODEL-DEPENDENT — opencode advertises an \" +\n \"`effort` config option only for models with a reasoning axis, with \" +\n \"values like `high`/`max`/`default` (its `--variant` help also names \" +\n \"`minimal`). A label the resolved model doesn't offer is ignored \" +\n \"best-effort (never fails the spawn), and a model with no effort axis \" +\n \"ignores it entirely. Omit to keep the model's own default.\",\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 metadata: {\n // Opts opencode into `agentproto install skill/<slug>` fan-out (no\n // --target given). Confirmed against the installed opencode CLI +\n // published docs (opencode.ai/docs/skills/): it auto-discovers\n // `~/.config/opencode/skills/<name>/SKILL.md` (global) in addition to\n // project-local `.opencode/skills/`, `.claude/skills/`, `.agents/skills/`\n // — same flat-dir, one-subdir-per-skill shape as hermes.\n skills: { format: \"flat-dir\", dir: \"~/.config/opencode/skills\" },\n },\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<{\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n reasoningTokens?: number\n} | 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 // `SELECT *` so an older opencode.db without the cache/reasoning\n // columns still reads (the missing ones just stay absent).\n const row = db.prepare(\"SELECT * FROM session WHERE id = ?\").get(sessionId) as\n | {\n cost?: number | null\n tokens_input?: number | null\n tokens_output?: number | null\n tokens_cache_read?: number | null\n tokens_cache_write?: number | null\n tokens_reasoning?: number | null\n }\n | undefined\n if (!row) return null\n return {\n ...(row.cost != null ? { costUsd: Number(row.cost) } : {}),\n ...(row.tokens_input != null ? { tokensIn: row.tokens_input } : {}),\n ...(row.tokens_output != null ? { tokensOut: row.tokens_output } : {}),\n ...(row.tokens_cache_read != null ? { cacheReadTokens: row.tokens_cache_read } : {}),\n ...(row.tokens_cache_write != null ? { cacheWriteTokens: row.tokens_cache_write } : {}),\n ...(row.tokens_reasoning != null ? { reasoningTokens: row.tokens_reasoning } : {}),\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/subaccounts.ts","../src/index.ts"],"names":["join","homedir"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Sub-account provider for opencode console orgs (\"workspaces\").\n *\n * opencode keeps ONE console login (`account` row + `account_state.active_org_id`\n * in its sqlite db) and, on every start, fetches `<account.url>/api/config`\n * for the active org and merges the returned `provider` block over all other\n * config — env keys and inline config included. So with that login present,\n * every opencode spawn bills the active org, whatever agentproto profile it\n * names. A profile on this source pins ONE org instead: the console session is\n * read (never written) from opencode.db, the pinned org's provider block is\n * fetched with it, and the spawn gets the token + that block through env while\n * the driver runs opencode in a login-less data dir\n * (`AgentCliDefinition.credentialDataHome`) so the stored login can't re-point\n * the providers back.\n *\n * The profile stores only `source: \"opencode-console\"` + `subaccount: {kind:\n * \"org\", id}` — no token. The token is read fresh from opencode.db at every\n * spawn. Legacy profiles (`source: \"opencode-console:<orgId>\"`) are migrated on\n * read. It is NOT refreshed\n * here: the refresh token rotates, and rotating it behind opencode's back would\n * log the operator out of opencode itself. An expired session fails loud.\n */\n\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport {\n SubaccountError,\n type SubaccountProvider,\n type SubaccountPin,\n} from \"@agentproto/auth\"\n\nexport const OPENCODE_CONSOLE_SOURCE = \"opencode-console\"\nexport const OPENCODE_CONSOLE_ORG_KIND = \"org\"\n\n/** Env var opencode's console provider block reads its bearer from. */\nexport const OPENCODE_CONSOLE_TOKEN_ENV = \"OPENCODE_CONSOLE_TOKEN\"\n\nconst FETCH_TIMEOUT_MS = 10_000\n/** A session expiring inside this window is treated as expired. */\nconst EXPIRY_SKEW_MS = 60_000\n\n/** Legacy `\"opencode-console:<orgId>\"` → the org pin it encoded; undefined for any other source. */\nexport function parseLegacyOpencodeConsoleSource(source: string | undefined): SubaccountPin | undefined {\n if (!source?.startsWith(`${OPENCODE_CONSOLE_SOURCE}:`)) return undefined\n const orgId = source.slice(OPENCODE_CONSOLE_SOURCE.length + 1)\n return orgId.length > 0 ? { kind: OPENCODE_CONSOLE_ORG_KIND, id: orgId } : undefined\n}\n\nexport function opencodeDbPath(env: Record<string, string | undefined> = process.env): string {\n const dataHome = env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\")\n return join(dataHome, \"opencode\", \"opencode.db\")\n}\n\nexport interface OpencodeConsoleAccount {\n id: string\n email?: string\n /** Console base URL, e.g. `https://opencode.ai/console`. */\n url: string\n accessToken: string\n /** Epoch ms, when opencode recorded one. */\n expiresAt?: number\n}\n\nexport interface OpencodeConsoleDeps {\n dbPath?: string\n fetch?: typeof fetch\n now?: () => number\n}\n\ninterface SqliteHandle {\n prepare(sql: string): { get(...a: unknown[]): unknown }\n close(): void\n}\n\nasync function openReadonly(dbPath: string): Promise<SqliteHandle> {\n const specifier = [\"node\", \"sqlite\"].join(\":\")\n const { DatabaseSync } = (await import(/* @vite-ignore */ specifier)) as unknown as {\n DatabaseSync: new (p: string, o?: { readOnly?: boolean }) => SqliteHandle\n }\n return new DatabaseSync(dbPath, { readOnly: true })\n}\n\nfunction loginHint(): string {\n return \"Log in once with `opencode auth login` (opencode.ai/console) so its console session is fresh, then retry.\"\n}\n\n/**\n * Read the logged-in console account from opencode.db, read-only. The active\n * account wins when several are stored. Throws {@link SubaccountError}\n * when there is no login, or when the session is expired.\n */\nexport async function readOpencodeConsoleAccount(\n deps: OpencodeConsoleDeps = {},\n): Promise<OpencodeConsoleAccount> {\n const dbPath = deps.dbPath ?? opencodeDbPath()\n const now = deps.now ?? Date.now\n let row:\n | { id: string; email?: string; url: string; access_token: string; token_expiry?: number | null }\n | undefined\n let db: SqliteHandle | undefined\n try {\n db = await openReadonly(dbPath)\n const active = db.prepare(\"SELECT active_account_id FROM account_state LIMIT 1\").get() as\n | { active_account_id?: string | null }\n | undefined\n row = (active?.active_account_id\n ? db.prepare(\"SELECT * FROM account WHERE id = ?\").get(active.active_account_id)\n : db.prepare(\"SELECT * FROM account LIMIT 1\").get()) as typeof row\n } catch (err) {\n throw new SubaccountError(`opencode console login unreadable at ${dbPath} (${err instanceof Error ? err.message : String(err)}). ${loginHint()}`,\n )\n } finally {\n try {\n db?.close()\n } catch {\n // already closed\n }\n }\n if (!row || !row.access_token) {\n throw new SubaccountError(`no opencode console account in ${dbPath}. ${loginHint()}`,\n )\n }\n const expiresAt = typeof row.token_expiry === \"number\" ? row.token_expiry : undefined\n if (expiresAt !== undefined && expiresAt - now() < EXPIRY_SKEW_MS) {\n throw new SubaccountError(`the opencode console session for ${row.email ?? row.id} expired at ${new Date(expiresAt).toISOString()}. ${loginHint()}`,\n )\n }\n return {\n id: row.id,\n ...(row.email ? { email: row.email } : {}),\n url: row.url.replace(/\\/+$/, \"\"),\n accessToken: row.access_token,\n ...(expiresAt !== undefined ? { expiresAt } : {}),\n }\n}\n\nasync function consoleGet(\n account: OpencodeConsoleAccount,\n path: string,\n deps: OpencodeConsoleDeps,\n orgId?: string,\n): Promise<unknown> {\n const doFetch = deps.fetch ?? fetch\n const res = await doFetch(`${account.url}${path}`, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${account.accessToken}`,\n ...(orgId ? { \"x-org-id\": orgId } : {}),\n },\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n })\n if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`)\n return res.json()\n}\n\nexport interface OpencodeConsoleOrg {\n id: string\n name: string\n}\n\n/** Orgs the logged-in console account can use (`GET /api/orgs`). */\nexport async function listOpencodeConsoleOrgs(\n deps: OpencodeConsoleDeps = {},\n): Promise<{ account: OpencodeConsoleAccount; orgs: OpencodeConsoleOrg[] }> {\n const account = await readOpencodeConsoleAccount(deps)\n let body: unknown\n try {\n body = await consoleGet(account, \"/api/orgs\", deps)\n } catch (err) {\n throw new SubaccountError(`opencode console org listing failed (${err instanceof Error ? err.message : String(err)}). ${loginHint()}`,\n )\n }\n const orgs = (Array.isArray(body) ? body : [])\n .filter((o): o is { id: string; name?: string } => typeof (o as { id?: unknown })?.id === \"string\")\n .map(o => ({ id: o.id, name: typeof o.name === \"string\" ? o.name : o.id }))\n return { account, orgs }\n}\n\nexport interface ResolvedOpencodeConsoleOrg {\n orgId: string\n /** The org-scoped console bearer — goes into {@link OPENCODE_CONSOLE_TOKEN_ENV}. */\n credential: string\n /** Env beside the bearer: the org's `provider` block as inline config. */\n extraEnv: Record<string, string>\n}\n\n/**\n * Resolve a pinned org to what the spawn needs: the bearer and the org's\n * provider block (which names the org id in its `x-opencode-org-id` header).\n * Fails loud — never falls back to another org or an ambient login.\n */\nexport async function resolveOpencodeConsoleOrg(\n orgId: string,\n profileId: string,\n deps: OpencodeConsoleDeps = {},\n): Promise<ResolvedOpencodeConsoleOrg> {\n const account = await readOpencodeConsoleAccount(deps)\n let provider: unknown\n try {\n const body = (await consoleGet(account, \"/api/config\", deps, orgId)) as {\n config?: { provider?: unknown }\n }\n provider = body?.config?.provider\n } catch (err) {\n throw new SubaccountError(`profile \"${profileId}\": opencode org ${orgId} config fetch failed (${err instanceof Error ? err.message : String(err)}) ` +\n `— the console account ${account.email ?? account.id} may not belong to that org. ${loginHint()}`,\n )\n }\n if (!provider || typeof provider !== \"object\" || Object.keys(provider).length === 0) {\n throw new SubaccountError(`profile \"${profileId}\": opencode org ${orgId} returned no provider config.`,\n )\n }\n return {\n orgId,\n credential: account.accessToken,\n extraEnv: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ provider }) },\n }\n}\n\n/**\n * The registry entry. `deps` is injectable for tests (db path, fetch, clock).\n */\nexport function createOpencodeSubaccountProvider(deps: OpencodeConsoleDeps = {}): SubaccountProvider {\n return {\n id: \"opencode-console\",\n source: OPENCODE_CONSOLE_SOURCE,\n kinds: [OPENCODE_CONSOLE_ORG_KIND],\n defaultEndpoint: \"opencode-go\",\n profilePrefix: \"opencode\",\n migrateLegacySource: parseLegacyOpencodeConsoleSource,\n async list() {\n const { account, orgs } = await listOpencodeConsoleOrgs(deps)\n return {\n account: { id: account.id, ...(account.email ? { label: account.email } : {}) },\n subaccounts: orgs.map(o => ({ kind: OPENCODE_CONSOLE_ORG_KIND, id: o.id, name: o.name })),\n }\n },\n async resolve(profile) {\n const resolved = await resolveOpencodeConsoleOrg(profile.subaccount.id, profile.id, deps)\n return {\n credential: resolved.credential,\n credentialEnvOverride: OPENCODE_CONSOLE_TOKEN_ENV,\n env: resolved.extraEnv,\n }\n },\n }\n}\n\n/** The default instance, reading the operator's own opencode.db. */\nexport const opencodeSubaccounts: SubaccountProvider = createOpencodeSubaccountProvider()\n","/**\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 { existsSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport {\n CREDENTIAL_DATA_SUBDIR,\n createAgentCliRuntime,\n defineAgentCli,\n type AgentCliHandle,\n type AgentCliRuntime,\n} from \"@agentproto/driver-agent-cli\"\nimport { getModelsByProvider, 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 * - OpenCode's own two hosted endpoints — `opencode-go/<id>` (the flat Go\n * subscription) and `opencode/<id>` (Zen, pay-as-you-go), both matching\n * opencode's own config spelling\n *\n * Groq is still omitted: it is not a billing/auth `CatalogProvider` (no\n * pricing generator), so there is nothing in the shared catalog to enumerate.\n * The `models.env` map carries its key env var, so a free-form `model`\n * override and a manually-curated profile continue to work. (The two OpenCode\n * endpoints USED to be omitted for the same reason — they are now first-class\n * catalog providers with their own generated route tables, which is what makes\n * them enumerable here.)\n *\n * `router: true` marks a provider whose surface lives ONLY in its generated\n * route table and is deliberately not spread into `LLM_PRICING_CATALOG` (a\n * bare `claude-sonnet-5` must keep meaning direct Anthropic, not a Zen-priced\n * route). `listModels` returns nothing for those, so they are read through\n * `getModelsByProvider`, which folds the route table in.\n */\nfunction buildOpencodeModelMenu(): Array<{ id: string; provider: string }> {\n const supported = [\n { provider: \"anthropic\", prefix: \"anthropic\", router: false },\n { provider: \"openai\", prefix: \"openai\", router: false },\n { provider: \"openrouter\", prefix: \"openrouter\", router: false },\n { provider: \"opencode-go\", prefix: \"opencode-go\", router: true },\n { provider: \"opencode\", prefix: \"opencode\", router: true },\n ] as const\n\n const seen = new Set<string>()\n const out: Array<{ id: string; provider: string }> = []\n\n for (const { provider, prefix, router } of supported) {\n const models = router\n ? getModelsByProvider(provider).filter(model => model.kind === \"llm\")\n : listModels({ kind: \"llm\", provider })\n for (const model of models) {\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\n/** Inline config the `lean` mode layers over the user's global opencode config. */\nexport const LEAN_INLINE_CONFIG = { mcp: { agentproto: { enabled: false } } } as const\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 // `--print-logs --log-level ERROR` makes the ACP server echo its structured\n // logs to stderr. Without it a provider 429/usage-cap is swallowed into an\n // internal retry loop and `session/prompt` never resolves, leaving the\n // daemon with a silent 0-token busy session. The driver's stderr hook\n // (`parseStderrStreamError`) reads those lines back into a turn error.\n bin_args: [\"-y\", \"opencode-ai\", \"acp\", \"--print-logs\", \"--log-level\", \"ERROR\"],\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 // opencode keeps a console login (`account` row) in <XDG_DATA_HOME>/opencode/\n // opencode.db and, at every start, merges that login's active-org provider\n // block over env keys and inline config — so an engaged profile would bill\n // the ACTIVE ORG, not its own credential. An engaged spawn therefore runs in\n // a login-less data dir (`<configDir>/auth-data`).\n credentialDataHome: { env: \"XDG_DATA_HOME\" },\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 // / OpenCode Go / OpenCode Zen models instead of a hardcoded 3-item list.\n // Groq stays omitted — it is not a billing/auth CatalogProvider, so there\n // is nothing to enumerate; the free-form `model` option and `models.env`\n // still support it.\n allowed: buildOpencodeModelMenu(),\n env: {\n anthropic: \"ANTHROPIC_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n // Both OpenCode endpoints read the SAME var — opencode's own convention\n // (models.dev records `env: [\"OPENCODE_API_KEY\"]` for each), mirrored in\n // the catalog's `PROVIDER_KEY_ENV`. Two DIFFERENT secrets share the name,\n // so which balance a spawn bills is decided by the resolved auth profile's\n // endpoint (`opencode` vs `opencode-go`), not by this map.\n opencode: \"OPENCODE_API_KEY\",\n \"opencode-go\": \"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 // `modes[]` carries ONE entry, the `context` axis (`lean`). opencode's\n // operation profiles (default / plan / build) are POSTURE, which no longer\n // lives in the manifest (SPEC §3.4a): opencode's own ACP server advertises\n // them as native session modes and switches them on the wire via\n // `session/set_config_option` (configId:\"mode\") / `session/set_mode` —\n // precisely the harness ACP mode registry (`SessionModeState.availableModes`)\n // posture is sourced from. route comes from the model catalog. What enters\n // the model's context has no ACP home, so it is the one thing declared here.\n modes: [\n {\n id: \"lean\",\n kind: \"context\",\n description:\n \"Keep opencode's first request near its own floor (~8k tokens): no auto-discovered \" +\n \"skills (`.claude/skills`, `.agents/skills`, `~/.claude/skills` — a repo with 100+ skills \" +\n \"lists them all, ~20k tokens), no AGENTS.md / CLAUDE.md / project opencode.json pulled in \" +\n \"from the cwd and its parents (agentproto's own AGENTS.md pointer in the first prompt \" +\n \"already names the contract), and the user's global `agentproto` MCP bridge \" +\n \"(`mcp.agentproto` in ~/.config/opencode/opencode.jsonc — 280+ tool schemas, ~100k tokens \" +\n \"on every start) switched off. A daemon mount (`daemonMount`) is unaffected and stays \" +\n \"the way to reach daemon tools (deferred by default for executors).\",\n // OPENCODE_DISABLE_EXTERNAL_SKILLS and OPENCODE_DISABLE_PROJECT_CONFIG are\n // read by the pinned opencode (1.18.x) at startup; measured in\n // `OPENCODE.md` (\"First-request size\"). OPENCODE_CONFIG_CONTENT is the\n // highest-precedence config layer and is deep-merged over the user's\n // global config, so `enabled:false` turns the global bridge off without\n // touching the file. Other user-declared global MCP servers are left alone.\n env: {\n OPENCODE_DISABLE_EXTERNAL_SKILLS: \"1\",\n OPENCODE_DISABLE_PROJECT_CONFIG: \"1\",\n OPENCODE_CONFIG_CONTENT: JSON.stringify(LEAN_INLINE_CONFIG),\n },\n },\n ],\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 id: \"effort\",\n // string, NOT enum: opencode advertises an `effort` config option\n // (category `thought_level`) ONLY for models that have a reasoning\n // axis, and the accepted vocabulary is MODEL-DEPENDENT — probed live\n // against `opencode acp` 1.18.x, one reasoning model offered\n // `high | max | default` while opencode's own `run --variant` help\n // names `high, max, minimal`. A static enum would reject labels that\n // are valid for models this adapter also routes to, so the value is\n // passed through and the server decides. Applied via ACP\n // `session/set_config_option(configId:\"effort\")` after the session is\n // created — `define-agent-cli.ts` reads `config.options.effort` and\n // forwards it to the ACP arm's `connect({effort})`; there is no\n // `opencode acp` CLI flag for it (`--variant` is a `run`-subcommand\n // flag, and the adapter spawns `acp`). The apply is best-effort and\n // non-fatal (`packages/acp`): a label the resolved model doesn't offer,\n // or a model with no effort axis at all, is warned about and ignored\n // rather than killing the spawn. Omit to keep the model's own default.\n type: \"string\" as const,\n description:\n \"Reasoning effort (opencode's per-model thought level / `variant`). \" +\n \"Applied via ACP session/set_config_option(configId:\\\"effort\\\") after \" +\n \"the session is created; no `opencode acp` CLI flag exists for it. \" +\n \"The accepted vocabulary is MODEL-DEPENDENT — opencode advertises an \" +\n \"`effort` config option only for models with a reasoning axis, with \" +\n \"values like `high`/`max`/`default` (its `--variant` help also names \" +\n \"`minimal`). A label the resolved model doesn't offer is ignored \" +\n \"best-effort (never fails the spawn), and a model with no effort axis \" +\n \"ignores it entirely. Omit to keep the model's own default.\",\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 metadata: {\n // Opts opencode into `agentproto install skill/<slug>` fan-out (no\n // --target given). Confirmed against the installed opencode CLI +\n // published docs (opencode.ai/docs/skills/): it auto-discovers\n // `~/.config/opencode/skills/<name>/SKILL.md` (global) in addition to\n // project-local `.opencode/skills/`, `.claude/skills/`, `.agents/skills/`\n // — same flat-dir, one-subdir-per-skill shape as hermes.\n skills: { format: \"flat-dir\", dir: \"~/.config/opencode/skills\" },\n },\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 ctx?: { cwd?: string; configDir?: string },\n): Promise<{\n costUsd?: number\n tokensIn?: number\n tokensOut?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n reasoningTokens?: number\n} | 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 // An engaged-credential spawn ran with an isolated data home (see\n // `credentialDataHome`); its sessions live there, not in the global db.\n const isolatedDb = ctx?.configDir\n ? join(ctx.configDir, CREDENTIAL_DATA_SUBDIR, \"opencode\", \"opencode.db\")\n : undefined\n const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), \".local\", \"share\")\n const dbPath =\n isolatedDb && existsSync(isolatedDb) ? isolatedDb : join(dataHome, \"opencode\", \"opencode.db\")\n const db = new DatabaseSync(dbPath, { readOnly: true })\n try {\n // `SELECT *` so an older opencode.db without the cache/reasoning\n // columns still reads (the missing ones just stay absent).\n const row = db.prepare(\"SELECT * FROM session WHERE id = ?\").get(sessionId) as\n | {\n cost?: number | null\n tokens_input?: number | null\n tokens_output?: number | null\n tokens_cache_read?: number | null\n tokens_cache_write?: number | null\n tokens_reasoning?: number | null\n }\n | undefined\n if (!row) return null\n return {\n ...(row.cost != null ? { costUsd: Number(row.cost) } : {}),\n ...(row.tokens_input != null ? { tokensIn: row.tokens_input } : {}),\n ...(row.tokens_output != null ? { tokensOut: row.tokens_output } : {}),\n ...(row.tokens_cache_read != null ? { cacheReadTokens: row.tokens_cache_read } : {}),\n ...(row.tokens_cache_write != null ? { cacheWriteTokens: row.tokens_cache_write } : {}),\n ...(row.tokens_reasoning != null ? { reasoningTokens: row.tokens_reasoning } : {}),\n }\n } finally {\n db.close()\n }\n } catch {\n return null\n }\n}\n\nexport {\n OPENCODE_CONSOLE_SOURCE,\n OPENCODE_CONSOLE_ORG_KIND,\n OPENCODE_CONSOLE_TOKEN_ENV,\n createOpencodeSubaccountProvider,\n opencodeSubaccounts,\n opencodeDbPath,\n readOpencodeConsoleAccount,\n listOpencodeConsoleOrgs,\n resolveOpencodeConsoleOrg,\n} from \"./subaccounts.js\"\nexport type { AgentCliHandle, AgentCliRuntime }\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/adapter-opencode",
3
- "version": "1.6.7",
3
+ "version": "1.7.0",
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,8 +41,9 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@agentproto/model-catalog": "0.12.1",
45
- "@agentproto/driver-agent-cli": "2.9.0"
44
+ "@agentproto/auth": "1.2.0",
45
+ "@agentproto/driver-agent-cli": "2.10.0",
46
+ "@agentproto/model-catalog": "0.12.3"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/node": "^25.9.5",