@agentos-software/opencode 0.0.0-nathan-binding-workspace.abc7ec9

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.
@@ -0,0 +1,3681 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import {
5
+ copyFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const SOURCE_REPOSITORY = "anomalyco/opencode";
19
+ const SOURCE_VERSION = "1.3.13";
20
+ const SOURCE_TARBALL_URL = `https://github.com/${SOURCE_REPOSITORY}/archive/refs/tags/v${SOURCE_VERSION}.tar.gz`;
21
+
22
+ // Upstream `packages/app/package.json` pins `ghostty-web: github:anomalyco/ghostty-web#main`,
23
+ // but the bundled bun.lock snapshots the SHA below. Because `main` is a moving ref, a fresh
24
+ // `bun install --frozen-lockfile` resolves to whatever `main` points at *now* and fails the
25
+ // lockfile check. Rewriting the manifest to the lockfile's SHA before install keeps frozen-
26
+ // lockfile guarantees intact (i.e. CI still breaks loudly if either side drifts) without
27
+ // trusting arbitrary new HEADs on the ghostty-web branch.
28
+ const GHOSTTY_WEB_PINNED_SHA = "4af877d";
29
+
30
+ const __dirname = dirname(fileURLToPath(import.meta.url));
31
+ const packageDir = resolve(__dirname, "..");
32
+ const distDir = resolve(packageDir, "dist");
33
+ const cacheDir = resolve(packageDir, "node_modules", ".cache", "opencode-build");
34
+ const patchPath = resolve(packageDir, "upstream", `opencode-v${SOURCE_VERSION}.patch`);
35
+ const bundleDir = resolve(distDir, "opencode-acp");
36
+ const manifestPath = resolve(distDir, "opencode-acp.manifest.json");
37
+ const SQL_JS_VERSION = "1.14.1";
38
+ const bunBin = resolve(
39
+ packageDir,
40
+ "node_modules",
41
+ ".bin",
42
+ process.platform === "win32" ? "bun.cmd" : "bun",
43
+ );
44
+
45
+ function run(command, args, options = {}) {
46
+ const result = spawnSync(command, args, {
47
+ stdio: "inherit",
48
+ ...options,
49
+ });
50
+ if (result.status !== 0) {
51
+ throw new Error(
52
+ `Command failed (${result.status ?? "unknown"}): ${command} ${args.join(" ")}`,
53
+ );
54
+ }
55
+ return result;
56
+ }
57
+
58
+ function pinGhosttyWebRef(sourceRoot) {
59
+ const manifestPath = resolve(sourceRoot, "packages", "app", "package.json");
60
+ const raw = readFileSync(manifestPath, "utf-8");
61
+ const movingRef = '"ghostty-web": "github:anomalyco/ghostty-web#main"';
62
+ const pinnedRef = `"ghostty-web": "github:anomalyco/ghostty-web#${GHOSTTY_WEB_PINNED_SHA}"`;
63
+ if (raw.includes(pinnedRef)) return;
64
+ if (!raw.includes(movingRef)) {
65
+ throw new Error(
66
+ `Expected ghostty-web ref ${movingRef} in ${manifestPath}; upstream layout changed — re-audit GHOSTTY_WEB_PINNED_SHA before updating.`,
67
+ );
68
+ }
69
+ writeFileSync(manifestPath, raw.replace(movingRef, pinnedRef));
70
+ }
71
+
72
+ const PATCHED_SOURCE_FILES = [
73
+ "packages/opencode/src/cli/cmd/acp.ts",
74
+ "packages/opencode/src/config/config.ts",
75
+ "packages/opencode/src/plugin/index.ts",
76
+ "packages/opencode/src/server/instance.ts",
77
+ "packages/opencode/src/server/server.ts",
78
+ ];
79
+
80
+ async function ensureNodeAcpPatch(sourceRoot, tarballPath) {
81
+ const serverFile = resolve(
82
+ sourceRoot,
83
+ "packages",
84
+ "opencode",
85
+ "src",
86
+ "server",
87
+ "server.ts",
88
+ );
89
+ const pluginFile = resolve(
90
+ sourceRoot,
91
+ "packages",
92
+ "opencode",
93
+ "src",
94
+ "plugin",
95
+ "index.ts",
96
+ );
97
+ const acpFile = resolve(
98
+ sourceRoot,
99
+ "packages",
100
+ "opencode",
101
+ "src",
102
+ "cli",
103
+ "cmd",
104
+ "acp.ts",
105
+ );
106
+ const serverSource = readFileSync(serverFile, "utf-8");
107
+ const pluginSource = readFileSync(pluginFile, "utf-8");
108
+ const acpSource = readFileSync(acpFile, "utf-8");
109
+ const alreadyPatched =
110
+ serverSource.includes('from "node:http"') &&
111
+ !serverSource.includes('from "hono/bun"') &&
112
+ pluginSource.includes("Bun shell is unavailable in the Node ACP build") &&
113
+ acpSource.includes("const server = await Server.listen(opts)");
114
+
115
+ if (alreadyPatched) {
116
+ return;
117
+ }
118
+
119
+ const scratchRoot = await mkdtemp(join(tmpdir(), "agentos-opencode-patch-"));
120
+ try {
121
+ run("tar", ["-xzf", tarballPath, "--strip-components=1", "-C", scratchRoot]);
122
+ run("git", ["apply", "--whitespace=nowarn", patchPath], { cwd: scratchRoot });
123
+ for (const relativePath of PATCHED_SOURCE_FILES) {
124
+ copyFileSync(
125
+ resolve(scratchRoot, relativePath),
126
+ resolve(sourceRoot, relativePath),
127
+ );
128
+ }
129
+ } finally {
130
+ rmSync(scratchRoot, { recursive: true, force: true });
131
+ }
132
+
133
+ const verifiedServerSource = readFileSync(serverFile, "utf-8");
134
+ const verifiedPluginSource = readFileSync(pluginFile, "utf-8");
135
+ const verifiedAcpSource = readFileSync(acpFile, "utf-8");
136
+ if (
137
+ !verifiedServerSource.includes('from "node:http"') ||
138
+ verifiedServerSource.includes('from "hono/bun"') ||
139
+ !verifiedPluginSource.includes("Bun shell is unavailable in the Node ACP build") ||
140
+ !verifiedAcpSource.includes("const server = await Server.listen(opts)")
141
+ ) {
142
+ throw new Error("Failed to stage the Node ACP patches into the prepared OpenCode source tree");
143
+ }
144
+ }
145
+
146
+ async function downloadFile(url, destination) {
147
+ const response = await fetch(url);
148
+ if (!response.ok) {
149
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
150
+ }
151
+
152
+ const buffer = Buffer.from(await response.arrayBuffer());
153
+ await writeFile(destination, buffer);
154
+ }
155
+
156
+ async function readMigrations(sourceRoot) {
157
+ const migrationRoot = join(sourceRoot, "packages", "opencode", "migration");
158
+ const entries = (await readdir(migrationRoot, { withFileTypes: true }))
159
+ .filter((entry) => entry.isDirectory() && /^\d{14}/.test(entry.name))
160
+ .map((entry) => entry.name)
161
+ .sort();
162
+
163
+ return Promise.all(
164
+ entries.map(async (name) => {
165
+ const sql = await readFile(
166
+ join(migrationRoot, name, "migration.sql"),
167
+ "utf8",
168
+ );
169
+ const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);
170
+ const timestamp = match
171
+ ? Date.UTC(
172
+ Number(match[1]),
173
+ Number(match[2]) - 1,
174
+ Number(match[3]),
175
+ Number(match[4]),
176
+ Number(match[5]),
177
+ Number(match[6]),
178
+ )
179
+ : 0;
180
+ return { name, sql, timestamp };
181
+ }),
182
+ );
183
+ }
184
+
185
+ async function rewriteSourceFile(sourceRoot, relativePath, transform) {
186
+ const filePath = resolve(sourceRoot, relativePath);
187
+ const original = await readFile(filePath, "utf8");
188
+ const next = transform(original);
189
+ if (next !== original) {
190
+ await writeFile(filePath, next);
191
+ }
192
+ }
193
+
194
+ async function ensureSqlJsDependency(sourceRoot) {
195
+ const packageJsonPath = resolve(
196
+ sourceRoot,
197
+ "packages",
198
+ "opencode",
199
+ "package.json",
200
+ );
201
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
202
+ const dependencies = packageJson.dependencies ?? {};
203
+ const bunStoreDir = resolve(sourceRoot, "node_modules", ".bun");
204
+ const hasInstalledSqlJs =
205
+ existsSync(bunStoreDir) &&
206
+ (await readdir(bunStoreDir)).some((entry) => entry.startsWith("sql.js@"));
207
+ if (dependencies["sql.js"] === SQL_JS_VERSION && hasInstalledSqlJs) {
208
+ return;
209
+ }
210
+
211
+ packageJson.dependencies = {
212
+ ...dependencies,
213
+ "sql.js": SQL_JS_VERSION,
214
+ };
215
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
216
+ run(bunBin, ["install"], { cwd: sourceRoot });
217
+ }
218
+
219
+ async function applyNodeAcpRuntimeTweaks(sourceRoot) {
220
+ await writeFile(
221
+ resolve(sourceRoot, "packages/opencode/src/cli/cmd/acp.ts"),
222
+ `import type { Argv, InferredOptionTypes } from "yargs"
223
+ import { cmd } from "./cmd"
224
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
225
+ import { Log } from "../../util/log"
226
+
227
+ const options = {
228
+ port: {
229
+ type: "number" as const,
230
+ describe: "port to listen on",
231
+ default: 0,
232
+ },
233
+ hostname: {
234
+ type: "string" as const,
235
+ describe: "hostname to listen on",
236
+ default: "127.0.0.1",
237
+ },
238
+ mdns: {
239
+ type: "boolean" as const,
240
+ describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
241
+ default: false,
242
+ },
243
+ "mdns-domain": {
244
+ type: "string" as const,
245
+ describe: "custom domain name for mDNS service (default: opencode.local)",
246
+ default: "opencode.local",
247
+ },
248
+ cors: {
249
+ type: "string" as const,
250
+ array: true,
251
+ describe: "additional domains to allow for CORS",
252
+ default: [] as string[],
253
+ },
254
+ }
255
+
256
+ type NetworkOptions = InferredOptionTypes<typeof options>
257
+
258
+ function withNetworkOptions<T>(yargs: Argv<T>) {
259
+ return yargs.options(options)
260
+ }
261
+
262
+ async function resolveNetworkOptions(args: NetworkOptions) {
263
+ const { Config } = await import("../../config/config")
264
+ const config = await Config.getGlobal()
265
+ const portExplicitlySet = process.argv.includes("--port")
266
+ const hostnameExplicitlySet = process.argv.includes("--hostname")
267
+ const mdnsExplicitlySet = process.argv.includes("--mdns")
268
+ const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
269
+ const corsExplicitlySet = process.argv.includes("--cors")
270
+
271
+ const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
272
+ const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"])
273
+ const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
274
+ const hostname = hostnameExplicitlySet
275
+ ? args.hostname
276
+ : mdns && !config?.server?.hostname
277
+ ? "0.0.0.0"
278
+ : (config?.server?.hostname ?? args.hostname)
279
+ const configCors = config?.server?.cors ?? []
280
+ const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
281
+ const cors = [...configCors, ...argsCors]
282
+
283
+ return { hostname, port, mdns, mdnsDomain, cors }
284
+ }
285
+
286
+ function wrapData<T>(data: T) {
287
+ return { data }
288
+ }
289
+
290
+ async function loadProviderCatalog(directory: string | undefined) {
291
+ const [{ Config }] = await Promise.all([import("../../config/config")])
292
+
293
+ return withDirectory(directory, async () => {
294
+ const config = await Config.get()
295
+ const providers: any[] = []
296
+
297
+ if (config?.provider?.anthropic || config?.model?.startsWith("anthropic/") || process.env.ANTHROPIC_API_KEY) {
298
+ providers.push({
299
+ id: "anthropic",
300
+ name: "Anthropic",
301
+ source: "config",
302
+ env: ["ANTHROPIC_API_KEY"],
303
+ options: {
304
+ ...(config?.provider?.anthropic?.options ?? {}),
305
+ },
306
+ models: {
307
+ "claude-sonnet-4-20250514": {
308
+ id: "claude-sonnet-4-20250514",
309
+ providerID: "anthropic",
310
+ api: { id: "claude-sonnet-4-20250514", url: "", npm: "@ai-sdk/anthropic" },
311
+ name: "Claude Sonnet 4",
312
+ family: "claude-sonnet-4",
313
+ capabilities: {
314
+ temperature: true,
315
+ reasoning: true,
316
+ attachment: true,
317
+ toolcall: true,
318
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
319
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
320
+ interleaved: false,
321
+ },
322
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
323
+ limit: { context: 200000, output: 32000 },
324
+ status: "active",
325
+ options: {},
326
+ headers: {},
327
+ release_date: "2025-05-14",
328
+ variants: {},
329
+ },
330
+ "claude-opus-4-1-20250805": {
331
+ id: "claude-opus-4-1-20250805",
332
+ providerID: "anthropic",
333
+ api: { id: "claude-opus-4-1-20250805", url: "", npm: "@ai-sdk/anthropic" },
334
+ name: "Claude Opus 4.1",
335
+ family: "claude-opus-4-1",
336
+ capabilities: {
337
+ temperature: true,
338
+ reasoning: true,
339
+ attachment: true,
340
+ toolcall: true,
341
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
342
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
343
+ interleaved: false,
344
+ },
345
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
346
+ limit: { context: 200000, output: 32000 },
347
+ status: "active",
348
+ options: {},
349
+ headers: {},
350
+ release_date: "2025-08-05",
351
+ variants: {},
352
+ },
353
+ "claude-haiku-4-5-20251001": {
354
+ id: "claude-haiku-4-5-20251001",
355
+ providerID: "anthropic",
356
+ api: { id: "claude-haiku-4-5-20251001", url: "", npm: "@ai-sdk/anthropic" },
357
+ name: "Claude Haiku 4.5",
358
+ family: "claude-haiku-4-5",
359
+ capabilities: {
360
+ temperature: true,
361
+ reasoning: false,
362
+ attachment: true,
363
+ toolcall: true,
364
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
365
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
366
+ interleaved: false,
367
+ },
368
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
369
+ limit: { context: 200000, output: 16000 },
370
+ status: "active",
371
+ options: {},
372
+ headers: {},
373
+ release_date: "2025-10-01",
374
+ variants: {},
375
+ },
376
+ },
377
+ })
378
+ }
379
+
380
+ if (config?.provider?.openai || config?.model?.startsWith("openai/") || process.env.OPENAI_API_KEY) {
381
+ providers.push({
382
+ id: "openai",
383
+ name: "OpenAI",
384
+ source: "config",
385
+ env: ["OPENAI_API_KEY"],
386
+ options: {
387
+ ...(config?.provider?.openai?.options ?? {}),
388
+ },
389
+ models: {
390
+ "gpt-5": {
391
+ id: "gpt-5",
392
+ providerID: "openai",
393
+ api: { id: "gpt-5", url: "", npm: "@ai-sdk/openai" },
394
+ name: "GPT-5",
395
+ family: "gpt-5",
396
+ capabilities: {
397
+ temperature: true,
398
+ reasoning: true,
399
+ attachment: true,
400
+ toolcall: true,
401
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
402
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
403
+ interleaved: false,
404
+ },
405
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
406
+ limit: { context: 200000, output: 32000 },
407
+ status: "active",
408
+ options: {},
409
+ headers: {},
410
+ release_date: "2025-01-01",
411
+ variants: {},
412
+ },
413
+ "gpt-5-mini": {
414
+ id: "gpt-5-mini",
415
+ providerID: "openai",
416
+ api: { id: "gpt-5-mini", url: "", npm: "@ai-sdk/openai" },
417
+ name: "GPT-5 Mini",
418
+ family: "gpt-5-mini",
419
+ capabilities: {
420
+ temperature: true,
421
+ reasoning: true,
422
+ attachment: true,
423
+ toolcall: true,
424
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
425
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
426
+ interleaved: false,
427
+ },
428
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
429
+ limit: { context: 200000, output: 16000 },
430
+ status: "active",
431
+ options: {},
432
+ headers: {},
433
+ release_date: "2025-01-01",
434
+ variants: {},
435
+ },
436
+ },
437
+ })
438
+ }
439
+
440
+ if (
441
+ config?.provider?.google ||
442
+ config?.model?.startsWith("google/") ||
443
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY
444
+ ) {
445
+ providers.push({
446
+ id: "google",
447
+ name: "Google",
448
+ source: "config",
449
+ env: ["GOOGLE_GENERATIVE_AI_API_KEY"],
450
+ options: {
451
+ ...(config?.provider?.google?.options ?? {}),
452
+ },
453
+ models: {
454
+ "gemini-2.5-pro": {
455
+ id: "gemini-2.5-pro",
456
+ providerID: "google",
457
+ api: { id: "gemini-2.5-pro", url: "", npm: "@ai-sdk/google" },
458
+ name: "Gemini 2.5 Pro",
459
+ family: "gemini-2.5-pro",
460
+ capabilities: {
461
+ temperature: true,
462
+ reasoning: true,
463
+ attachment: true,
464
+ toolcall: true,
465
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
466
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
467
+ interleaved: false,
468
+ },
469
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
470
+ limit: { context: 200000, output: 32000 },
471
+ status: "active",
472
+ options: {},
473
+ headers: {},
474
+ release_date: "2025-01-01",
475
+ variants: {},
476
+ },
477
+ "gemini-2.5-flash": {
478
+ id: "gemini-2.5-flash",
479
+ providerID: "google",
480
+ api: { id: "gemini-2.5-flash", url: "", npm: "@ai-sdk/google" },
481
+ name: "Gemini 2.5 Flash",
482
+ family: "gemini-2.5-flash",
483
+ capabilities: {
484
+ temperature: true,
485
+ reasoning: true,
486
+ attachment: true,
487
+ toolcall: true,
488
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
489
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
490
+ interleaved: false,
491
+ },
492
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
493
+ limit: { context: 200000, output: 16000 },
494
+ status: "active",
495
+ options: {},
496
+ headers: {},
497
+ release_date: "2025-01-01",
498
+ variants: {},
499
+ },
500
+ },
501
+ })
502
+ }
503
+
504
+ if (
505
+ config?.provider?.["google-vertex"] ||
506
+ config?.model?.startsWith("google-vertex/") ||
507
+ (process.env.GOOGLE_VERTEX_PROJECT && process.env.GOOGLE_VERTEX_LOCATION)
508
+ ) {
509
+ providers.push({
510
+ id: "google-vertex",
511
+ name: "Google Vertex",
512
+ source: "config",
513
+ env: [],
514
+ options: {
515
+ ...(config?.provider?.["google-vertex"]?.options ?? {}),
516
+ },
517
+ models: {
518
+ "gemini-2.5-pro": {
519
+ id: "gemini-2.5-pro",
520
+ providerID: "google-vertex",
521
+ api: { id: "gemini-2.5-pro", url: "", npm: "@ai-sdk/google-vertex" },
522
+ name: "Gemini 2.5 Pro",
523
+ family: "gemini-2.5-pro",
524
+ capabilities: {
525
+ temperature: true,
526
+ reasoning: true,
527
+ attachment: true,
528
+ toolcall: true,
529
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
530
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
531
+ interleaved: false,
532
+ },
533
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
534
+ limit: { context: 200000, output: 32000 },
535
+ status: "active",
536
+ options: {},
537
+ headers: {},
538
+ release_date: "2025-01-01",
539
+ variants: {},
540
+ },
541
+ },
542
+ })
543
+ }
544
+
545
+ if (config?.provider?.groq || config?.model?.startsWith("groq/") || process.env.GROQ_API_KEY) {
546
+ providers.push({
547
+ id: "groq",
548
+ name: "Groq",
549
+ source: "config",
550
+ env: ["GROQ_API_KEY"],
551
+ options: {
552
+ ...(config?.provider?.groq?.options ?? {}),
553
+ },
554
+ models: {
555
+ "llama-3.3-70b-versatile": {
556
+ id: "llama-3.3-70b-versatile",
557
+ providerID: "groq",
558
+ api: { id: "llama-3.3-70b-versatile", url: "", npm: "@ai-sdk/groq" },
559
+ name: "Llama 3.3 70B Versatile",
560
+ family: "llama-3.3-70b",
561
+ capabilities: {
562
+ temperature: true,
563
+ reasoning: true,
564
+ attachment: false,
565
+ toolcall: true,
566
+ input: { text: true, audio: false, image: false, video: false, pdf: false },
567
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
568
+ interleaved: false,
569
+ },
570
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
571
+ limit: { context: 200000, output: 32000 },
572
+ status: "active",
573
+ options: {},
574
+ headers: {},
575
+ release_date: "2025-01-01",
576
+ variants: {},
577
+ },
578
+ },
579
+ })
580
+ }
581
+
582
+ if (config?.provider?.mistral || config?.model?.startsWith("mistral/") || process.env.MISTRAL_API_KEY) {
583
+ providers.push({
584
+ id: "mistral",
585
+ name: "Mistral",
586
+ source: "config",
587
+ env: ["MISTRAL_API_KEY"],
588
+ options: {
589
+ ...(config?.provider?.mistral?.options ?? {}),
590
+ },
591
+ models: {
592
+ "mistral-small-latest": {
593
+ id: "mistral-small-latest",
594
+ providerID: "mistral",
595
+ api: { id: "mistral-small-latest", url: "", npm: "@ai-sdk/mistral" },
596
+ name: "Mistral Small Latest",
597
+ family: "mistral-small",
598
+ capabilities: {
599
+ temperature: true,
600
+ reasoning: true,
601
+ attachment: true,
602
+ toolcall: true,
603
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
604
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
605
+ interleaved: false,
606
+ },
607
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
608
+ limit: { context: 200000, output: 32000 },
609
+ status: "active",
610
+ options: {},
611
+ headers: {},
612
+ release_date: "2025-01-01",
613
+ variants: {},
614
+ },
615
+ },
616
+ })
617
+ }
618
+
619
+ if (providers.length === 0) {
620
+ providers.push({
621
+ id: "anthropic",
622
+ name: "Anthropic",
623
+ source: "custom",
624
+ env: ["ANTHROPIC_API_KEY"],
625
+ options: {},
626
+ models: {
627
+ "claude-sonnet-4-20250514": {
628
+ id: "claude-sonnet-4-20250514",
629
+ providerID: "anthropic",
630
+ api: { id: "claude-sonnet-4-20250514", url: "", npm: "@ai-sdk/anthropic" },
631
+ name: "Claude Sonnet 4",
632
+ family: "claude-sonnet-4",
633
+ capabilities: {
634
+ temperature: true,
635
+ reasoning: true,
636
+ attachment: true,
637
+ toolcall: true,
638
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
639
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
640
+ interleaved: false,
641
+ },
642
+ cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
643
+ limit: { context: 200000, output: 32000 },
644
+ status: "active",
645
+ options: {},
646
+ headers: {},
647
+ release_date: "2025-05-14",
648
+ variants: {},
649
+ },
650
+ },
651
+ })
652
+ }
653
+
654
+ const defaults = Object.fromEntries(
655
+ providers.map((provider) => {
656
+ const specifiedModel =
657
+ typeof config.model === "string" && config.model.startsWith(provider.id + "/")
658
+ ? config.model.slice(provider.id.length + 1)
659
+ : undefined
660
+ return [
661
+ provider.id,
662
+ specifiedModel ?? Object.keys(provider.models)[0] ?? "",
663
+ ]
664
+ }),
665
+ )
666
+
667
+ return { providers, default: defaults }
668
+ })
669
+ }
670
+
671
+ async function runServiceWithCurrentInstance<T>(
672
+ serviceModule: { Service: any; defaultLayer: any },
673
+ ctx: any,
674
+ fn: (service: any) => any,
675
+ ): Promise<T> {
676
+ const [{ Effect, ManagedRuntime }, { InstanceRef }, { memoMap }, { Instance }] = await Promise.all([
677
+ import("effect"),
678
+ import("../../effect/instance-ref"),
679
+ import("../../effect/run-service"),
680
+ import("../../project/instance"),
681
+ ])
682
+ console.error("[opencode-acp] serviceRuntime:ctx", ctx.directory)
683
+ ;(globalThis as typeof globalThis & { __agentosOpencodeInstanceFallback?: unknown }).__agentosOpencodeInstanceFallback =
684
+ ctx
685
+ const runtime = ManagedRuntime.make(serviceModule.defaultLayer, { memoMap })
686
+ try {
687
+ const result = await Instance.restore(
688
+ ctx,
689
+ () =>
690
+ runtime.runPromise(
691
+ Effect.provideService(serviceModule.Service.use(fn), InstanceRef, ctx),
692
+ ),
693
+ )
694
+ console.error("[opencode-acp] serviceRuntime:done", ctx.directory)
695
+ return result
696
+ } catch (error) {
697
+ console.error(
698
+ "[opencode-acp] serviceRuntime:error",
699
+ error instanceof Error ? error.stack ?? error.message : String(error),
700
+ )
701
+ throw error
702
+ }
703
+ }
704
+
705
+ async function loadAgentCatalog(directory: string | undefined) {
706
+ const { Config } = await import("../../config/config")
707
+
708
+ return withDirectory(directory, async () => {
709
+ const cfg = await Config.get()
710
+ const agents = new Map<string, any>([
711
+ [
712
+ "build",
713
+ {
714
+ name: "build",
715
+ description: "The default agent. Executes tools based on configured permissions.",
716
+ mode: "primary",
717
+ },
718
+ ],
719
+ [
720
+ "plan",
721
+ {
722
+ name: "plan",
723
+ description: "Plan mode. Disallows all edit tools.",
724
+ mode: "primary",
725
+ },
726
+ ],
727
+ [
728
+ "general",
729
+ {
730
+ name: "general",
731
+ description:
732
+ "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.",
733
+ mode: "subagent",
734
+ },
735
+ ],
736
+ [
737
+ "explore",
738
+ {
739
+ name: "explore",
740
+ description:
741
+ "Fast agent specialized for exploring codebases. Use this when you need to quickly find files, search code, or answer questions about the codebase.",
742
+ mode: "subagent",
743
+ },
744
+ ],
745
+ ["compaction", { name: "compaction", mode: "primary", hidden: true }],
746
+ ["title", { name: "title", mode: "primary", hidden: true }],
747
+ ["summary", { name: "summary", mode: "primary", hidden: true }],
748
+ ])
749
+
750
+ for (const [key, value] of Object.entries(cfg.agent ?? {})) {
751
+ if (value?.disable) {
752
+ agents.delete(key)
753
+ continue
754
+ }
755
+ const current = agents.get(key) ?? {
756
+ name: key,
757
+ mode: "all",
758
+ }
759
+ agents.set(key, {
760
+ ...current,
761
+ ...(value?.name ? { name: value.name } : {}),
762
+ ...(value?.description ? { description: value.description } : {}),
763
+ ...(value?.mode ? { mode: value.mode } : {}),
764
+ ...(value?.hidden !== undefined ? { hidden: value.hidden } : {}),
765
+ })
766
+ }
767
+
768
+ const defaultAgent = cfg.default_agent ?? "build"
769
+ return Array.from(agents.values()).sort((a, b) => {
770
+ const aRank = a.name === defaultAgent ? 0 : 1
771
+ const bRank = b.name === defaultAgent ? 0 : 1
772
+ if (aRank !== bRank) return aRank - bRank
773
+ return a.name.localeCompare(b.name)
774
+ })
775
+ })
776
+ }
777
+
778
+ async function loadCommandCatalog(directory: string | undefined) {
779
+ const { Config } = await import("../../config/config")
780
+
781
+ return withDirectory(directory, async () => {
782
+ const cfg = await Config.get()
783
+ const commands = new Map<string, any>([
784
+ ["init", { name: "init", description: "create/update AGENTS.md" }],
785
+ ["review", { name: "review", description: "review changes [commit|branch|pr], defaults to uncommitted" }],
786
+ ])
787
+
788
+ for (const [name, command] of Object.entries(cfg.command ?? {})) {
789
+ commands.set(name, {
790
+ name,
791
+ ...(command?.description ? { description: command.description } : {}),
792
+ })
793
+ }
794
+
795
+ return Array.from(commands.values()).sort((a, b) => a.name.localeCompare(b.name))
796
+ })
797
+ }
798
+
799
+ async function withDirectory<T>(
800
+ directory: string | undefined,
801
+ fn: (ctx: any) => Promise<T>,
802
+ ): Promise<T> {
803
+ console.error("[opencode-acp] withDirectory:start", directory ?? process.cwd())
804
+ const [{ Instance }, { InstanceBootstrap }] = await Promise.all([
805
+ import("../../project/instance"),
806
+ import("../../project/bootstrap"),
807
+ ])
808
+ console.error("[opencode-acp] withDirectory:modules:done", directory ?? process.cwd())
809
+ return Instance.provide({
810
+ directory: directory ?? process.cwd(),
811
+ init: InstanceBootstrap,
812
+ fn: () => {
813
+ const ctx = Instance.current
814
+ ;(globalThis as typeof globalThis & { __agentosOpencodeInstanceFallback?: unknown }).__agentosOpencodeInstanceFallback =
815
+ ctx
816
+ return fn(ctx)
817
+ },
818
+ })
819
+ }
820
+
821
+ async function ensureProjectors() {
822
+ if ((globalThis as any).__agentosOpencodeProjectorsReady) return
823
+ console.error("[opencode-acp] projectors:ensure:start")
824
+ const [{ SyncEvent }, { default: sessionProjectors }] = await Promise.all([
825
+ import("../../sync"),
826
+ import("../../session/projectors"),
827
+ ])
828
+ SyncEvent.init({
829
+ projectors: sessionProjectors,
830
+ })
831
+ ;(globalThis as any).__agentosOpencodeProjectorsReady = true
832
+ console.error("[opencode-acp] projectors:ensure:done")
833
+ }
834
+
835
+ async function createGlobalEventStream(signal?: AbortSignal) {
836
+ const { GlobalBus } = await import("../../bus/global")
837
+ let closed = false
838
+ const queue: any[] = []
839
+ let nextResolve: ((result: IteratorResult<any>) => void) | undefined
840
+
841
+ const close = () => {
842
+ if (closed) return
843
+ closed = true
844
+ GlobalBus.off("event", onEvent)
845
+ signal?.removeEventListener("abort", close)
846
+ nextResolve?.({ done: true, value: undefined })
847
+ nextResolve = undefined
848
+ }
849
+
850
+ const onEvent = (event: any) => {
851
+ if (closed) return
852
+ if (nextResolve) {
853
+ const resolve = nextResolve
854
+ nextResolve = undefined
855
+ resolve({ done: false, value: event })
856
+ return
857
+ }
858
+ queue.push(event)
859
+ }
860
+
861
+ GlobalBus.on("event", onEvent)
862
+ if (signal) {
863
+ if (signal.aborted) close()
864
+ else signal.addEventListener("abort", close, { once: true })
865
+ }
866
+
867
+ return {
868
+ stream: (async function* () {
869
+ try {
870
+ while (true) {
871
+ if (queue.length > 0) {
872
+ yield queue.shift()
873
+ continue
874
+ }
875
+
876
+ const next = await new Promise<IteratorResult<any>>((resolve) => {
877
+ nextResolve = resolve
878
+ })
879
+ if (next.done) return
880
+ yield next.value
881
+ }
882
+ } finally {
883
+ close()
884
+ }
885
+ })(),
886
+ }
887
+ }
888
+
889
+ function createLocalSdk() {
890
+ return {
891
+ global: {
892
+ event: async ({ signal }: { signal?: AbortSignal }) => createGlobalEventStream(signal),
893
+ },
894
+ permission: {
895
+ reply: async ({ requestID, reply, directory }: any) =>
896
+ wrapData(
897
+ await withDirectory(directory, async () => {
898
+ const { Permission } = await import("../../permission")
899
+ await Permission.reply({ requestID, reply })
900
+ return true
901
+ }),
902
+ ),
903
+ },
904
+ config: {
905
+ get: async ({ directory }: any) =>
906
+ wrapData(
907
+ await (async () => {
908
+ const { Config } = await import("../../config/config")
909
+ return withDirectory(directory, async () => {
910
+ console.error("[opencode-acp] sdk.config.get:start", directory ?? process.cwd())
911
+ const result = await Config.get()
912
+ console.error("[opencode-acp] sdk.config.get:done", directory ?? process.cwd())
913
+ return result
914
+ })
915
+ })(),
916
+ ),
917
+ providers: async ({ directory }: any) =>
918
+ wrapData(
919
+ await (async () => {
920
+ console.error("[opencode-acp] sdk.config.providers:start", directory ?? process.cwd())
921
+ return withDirectory(directory, async () => {
922
+ const result = await loadProviderCatalog(directory)
923
+ console.error(
924
+ "[opencode-acp] sdk.config.providers:done",
925
+ directory ?? process.cwd(),
926
+ result.providers.length,
927
+ )
928
+ return result
929
+ })
930
+ })(),
931
+ ),
932
+ },
933
+ app: {
934
+ agents: async ({ directory }: any) =>
935
+ wrapData(
936
+ await loadAgentCatalog(directory),
937
+ ),
938
+ },
939
+ command: {
940
+ list: async ({ directory }: any) =>
941
+ wrapData(
942
+ await loadCommandCatalog(directory),
943
+ ),
944
+ },
945
+ mcp: {
946
+ add: async ({ directory, name, config }: any) =>
947
+ wrapData(
948
+ await (async () => {
949
+ const { MCP } = await import("../../mcp")
950
+ return withDirectory(directory, async () => MCP.add(name, config))
951
+ })(),
952
+ ),
953
+ },
954
+ session: {
955
+ create: async ({ directory, title, permission, workspaceID, parentID }: any) =>
956
+ wrapData(
957
+ await (async () => {
958
+ await ensureProjectors()
959
+ return withDirectory(directory, async (ctx) => {
960
+ const { Session } = await import("../../session")
961
+ console.error("[opencode-acp] sdk.session.create:start", directory ?? process.cwd())
962
+ try {
963
+ const result = await runServiceWithCurrentInstance(Session, ctx, (service) =>
964
+ service.create({
965
+ ...(title ? { title } : {}),
966
+ ...(permission ? { permission } : {}),
967
+ ...(workspaceID ? { workspaceID } : {}),
968
+ ...(parentID ? { parentID } : {}),
969
+ }),
970
+ )
971
+ console.error("[opencode-acp] sdk.session.create:done", directory ?? process.cwd())
972
+ return result
973
+ } catch (error) {
974
+ console.error(
975
+ "[opencode-acp] sdk.session.create:error",
976
+ error instanceof Error ? error.stack ?? error.message : String(error),
977
+ )
978
+ throw error
979
+ }
980
+ })
981
+ })(),
982
+ ),
983
+ get: async ({ sessionID, directory }: any) =>
984
+ wrapData(
985
+ await (async () => {
986
+ const { Session } = await import("../../session")
987
+ return withDirectory(directory, async (ctx) =>
988
+ runServiceWithCurrentInstance(Session, ctx, (service) => service.get(sessionID)),
989
+ )
990
+ })(),
991
+ ),
992
+ list: async ({ directory, roots, start, search, limit }: any) =>
993
+ wrapData(
994
+ await (async () => {
995
+ const { Session } = await import("../../session")
996
+ return withDirectory(directory, async (ctx) => {
997
+ return runServiceWithCurrentInstance(Session, ctx, (service) =>
998
+ service.list({ directory, roots, start, search, limit }),
999
+ )
1000
+ })
1001
+ })(),
1002
+ ),
1003
+ fork: async ({ sessionID, messageID, directory }: any) =>
1004
+ wrapData(
1005
+ await (async () => {
1006
+ const { Session } = await import("../../session")
1007
+ await ensureProjectors()
1008
+ return withDirectory(directory, async (ctx) =>
1009
+ runServiceWithCurrentInstance(Session, ctx, (service) =>
1010
+ service.fork({
1011
+ sessionID,
1012
+ ...(messageID ? { messageID } : {}),
1013
+ }),
1014
+ ),
1015
+ )
1016
+ })(),
1017
+ ),
1018
+ messages: async ({ sessionID, limit, directory }: any) =>
1019
+ wrapData(
1020
+ await (async () => {
1021
+ const { Session } = await import("../../session")
1022
+ return withDirectory(directory, async (ctx) =>
1023
+ runServiceWithCurrentInstance(Session, ctx, (service) =>
1024
+ service.messages({ sessionID, ...(limit ? { limit } : {}) }),
1025
+ ),
1026
+ )
1027
+ })(),
1028
+ ),
1029
+ message: async ({ sessionID, messageID, directory }: any) =>
1030
+ wrapData(
1031
+ await (async () => {
1032
+ const { MessageV2 } = await import("../../session/message-v2")
1033
+ return withDirectory(directory, async () => MessageV2.get({ sessionID, messageID }))
1034
+ })(),
1035
+ ),
1036
+ prompt: async ({ sessionID, directory, ...input }: any) =>
1037
+ wrapData(
1038
+ await (async () => {
1039
+ const { SessionPrompt } = await import("../../session/prompt")
1040
+ await ensureProjectors()
1041
+ return withDirectory(directory, async (ctx) =>
1042
+ runServiceWithCurrentInstance(SessionPrompt, ctx, (service) =>
1043
+ service.prompt({ sessionID, ...input }),
1044
+ ),
1045
+ )
1046
+ })(),
1047
+ ),
1048
+ command: async ({ sessionID, directory, ...input }: any) =>
1049
+ wrapData(
1050
+ await (async () => {
1051
+ const { SessionPrompt } = await import("../../session/prompt")
1052
+ await ensureProjectors()
1053
+ return withDirectory(directory, async (ctx) =>
1054
+ runServiceWithCurrentInstance(SessionPrompt, ctx, (service) =>
1055
+ service.command({ sessionID, ...input }),
1056
+ ),
1057
+ )
1058
+ })(),
1059
+ ),
1060
+ summarize: async ({ sessionID, directory, providerID, modelID, auto = false }: any) =>
1061
+ wrapData(
1062
+ await (async () => {
1063
+ const [{ Session }, { SessionRevert }, { SessionCompaction }, { SessionPrompt }, { Agent }] =
1064
+ await Promise.all([
1065
+ import("../../session"),
1066
+ import("../../session/revert"),
1067
+ import("../../session/compaction"),
1068
+ import("../../session/prompt"),
1069
+ import("../../agent/agent"),
1070
+ ])
1071
+ await ensureProjectors()
1072
+ return withDirectory(directory, async (ctx) => {
1073
+ const session = await Session.get(sessionID)
1074
+ await SessionRevert.cleanup(session)
1075
+ const messages = await Session.messages({ sessionID })
1076
+ let currentAgent = await Agent.defaultAgent()
1077
+ for (let i = messages.length - 1; i >= 0; i--) {
1078
+ const info = messages[i].info
1079
+ if (info.role === "user") {
1080
+ currentAgent = info.agent || (await Agent.defaultAgent())
1081
+ break
1082
+ }
1083
+ }
1084
+ await SessionCompaction.create({
1085
+ sessionID,
1086
+ agent: currentAgent,
1087
+ model: { providerID, modelID },
1088
+ auto,
1089
+ })
1090
+ await runServiceWithCurrentInstance(SessionPrompt, ctx, (service) =>
1091
+ service.loop({ sessionID }),
1092
+ )
1093
+ return true
1094
+ })
1095
+ })(),
1096
+ ),
1097
+ abort: async ({ sessionID, directory }: any) =>
1098
+ wrapData(
1099
+ await (async () => {
1100
+ const { SessionPrompt } = await import("../../session/prompt")
1101
+ return withDirectory(directory, async (ctx) => {
1102
+ await runServiceWithCurrentInstance(SessionPrompt, ctx, (service) =>
1103
+ service.cancel(sessionID),
1104
+ )
1105
+ return true
1106
+ })
1107
+ })(),
1108
+ ),
1109
+ },
1110
+ } as any
1111
+ }
1112
+
1113
+ export const AcpCommand = cmd({
1114
+ command: "acp",
1115
+ describe: "start ACP (Agent Client Protocol) server",
1116
+ builder: (yargs) => {
1117
+ return withNetworkOptions(yargs).option("cwd", {
1118
+ describe: "working directory",
1119
+ type: "string",
1120
+ default: process.cwd(),
1121
+ })
1122
+ },
1123
+ handler: async (args: NetworkOptions) => {
1124
+ process.env.OPENCODE_CLIENT = "acp"
1125
+ process.env.OPENCODE_DISABLE_MODELS_FETCH = process.env.OPENCODE_DISABLE_MODELS_FETCH ?? "1"
1126
+ process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS ?? "1"
1127
+ const [{ ACP }] = await Promise.all([import("../../acp/agent")])
1128
+
1129
+ const log = Log.create({ service: "acp-command" })
1130
+ console.error("[opencode-acp] bootstrap:entered")
1131
+ console.error("[opencode-acp] network:resolve:start")
1132
+ const opts = await resolveNetworkOptions(args)
1133
+ console.error("[opencode-acp] network:resolve:done", JSON.stringify(opts))
1134
+
1135
+ console.error("[opencode-acp] sdk:create:start")
1136
+ const sdk = createLocalSdk()
1137
+ console.error("[opencode-acp] sdk:create:done")
1138
+
1139
+ console.error("[opencode-acp] streams:create:start")
1140
+ const input = new WritableStream<Uint8Array>({
1141
+ write(chunk) {
1142
+ return new Promise<void>((resolve, reject) => {
1143
+ process.stdout.write(chunk, (err) => {
1144
+ if (err) {
1145
+ reject(err)
1146
+ } else {
1147
+ resolve()
1148
+ }
1149
+ })
1150
+ })
1151
+ },
1152
+ })
1153
+ const output = new ReadableStream<Uint8Array>({
1154
+ start(controller) {
1155
+ process.stdin.on("data", (chunk: Buffer) => {
1156
+ controller.enqueue(new Uint8Array(chunk))
1157
+ })
1158
+ process.stdin.on("end", () => controller.close())
1159
+ process.stdin.on("error", (err) => controller.error(err))
1160
+ },
1161
+ })
1162
+ console.error("[opencode-acp] streams:create:done")
1163
+
1164
+ console.error("[opencode-acp] ndjson:start")
1165
+ const stream = ndJsonStream(input, output)
1166
+ console.error("[opencode-acp] ndjson:done")
1167
+ console.error("[opencode-acp] agent:init:start")
1168
+ const agent = await ACP.init({ sdk })
1169
+ console.error("[opencode-acp] agent:init:done")
1170
+
1171
+ console.error("[opencode-acp] connection:start")
1172
+ new AgentSideConnection((conn) => {
1173
+ return agent.create(conn, { sdk })
1174
+ }, stream)
1175
+ console.error("[opencode-acp] connection:done")
1176
+
1177
+ log.info("setup connection")
1178
+ process.stdin.resume()
1179
+ await new Promise((resolve, reject) => {
1180
+ process.stdin.on("end", resolve)
1181
+ process.stdin.on("error", reject)
1182
+ })
1183
+ },
1184
+ })
1185
+ `,
1186
+ );
1187
+
1188
+ await rewriteSourceFile(
1189
+ sourceRoot,
1190
+ "packages/opencode/src/server/instance.ts",
1191
+ (contents) =>
1192
+ contents
1193
+ .replace('import { TuiRoutes } from "./routes/tui"\n', "")
1194
+ .replace('import { PtyRoutes } from "./routes/pty"\n', "")
1195
+ .replace(' .route("/pty", PtyRoutes())\n', "")
1196
+ .replace(' .route("/tui", TuiRoutes())\n', ""),
1197
+ );
1198
+
1199
+ await rewriteSourceFile(
1200
+ sourceRoot,
1201
+ "packages/opencode/src/agent/agent.ts",
1202
+ (contents) =>
1203
+ contents.replace(
1204
+ ` [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
1205
+ "allow",`,
1206
+ ` [path.relative(ctx.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
1207
+ "allow",`,
1208
+ ),
1209
+ );
1210
+
1211
+ await rewriteSourceFile(
1212
+ sourceRoot,
1213
+ "packages/opencode/src/shell/shell.ts",
1214
+ (contents) => contents.replaceAll("Bun.which(", "which("),
1215
+ );
1216
+
1217
+ await rewriteSourceFile(
1218
+ sourceRoot,
1219
+ "packages/opencode/src/server/server.ts",
1220
+ (contents) =>
1221
+ contents
1222
+ .replace('import { MDNS } from "./mdns"\n', "")
1223
+ .replace(
1224
+ ` if (shouldPublishMDNS) {
1225
+ MDNS.publish(server.port, opts.mdnsDomain)
1226
+ } else if (opts.mdns) {`,
1227
+ ` let mdns:
1228
+ | {
1229
+ publish(port: number, domain?: string): void
1230
+ unpublish(): void
1231
+ }
1232
+ | undefined
1233
+ if (shouldPublishMDNS) {
1234
+ ;({ MDNS: mdns } = await import("./mdns"))
1235
+ mdns.publish(server.port, opts.mdnsDomain)
1236
+ } else if (opts.mdns) {`,
1237
+ )
1238
+ .replace(
1239
+ " if (shouldPublishMDNS) MDNS.unpublish()\n",
1240
+ " if (shouldPublishMDNS) mdns?.unpublish()\n",
1241
+ ),
1242
+ );
1243
+
1244
+ await rewriteSourceFile(
1245
+ sourceRoot,
1246
+ "packages/opencode/src/project/bootstrap.ts",
1247
+ (contents) =>
1248
+ contents.replace(
1249
+ ` Bus.subscribe(Command.Event.Executed, async (payload) => {
1250
+ if (payload.properties.name === Command.Default.INIT) {
1251
+ Project.setInitialized(Instance.project.id)
1252
+ }
1253
+ })
1254
+ `,
1255
+ ` Log.Default.info("bootstrap step", { step: "bus.subscribe:skipped" })
1256
+ `,
1257
+ ),
1258
+ );
1259
+
1260
+ await rewriteSourceFile(
1261
+ sourceRoot,
1262
+ "packages/opencode/src/acp/agent.ts",
1263
+ (contents) =>
1264
+ contents
1265
+ .replaceAll(
1266
+ ` console.error("[opencode-acp] agent.loadSessionMode:commands:done", directory, commands.length)
1267
+ `,
1268
+ "",
1269
+ )
1270
+ .replace(
1271
+ ` const defaultAgentName = await AgentModule.defaultAgent()
1272
+ const resolvedModeId =
1273
+ availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
1274
+ `,
1275
+ ` const resolvedModeId = availableModes[0].id
1276
+ `,
1277
+ )
1278
+ .replace(
1279
+ ` const model = await defaultModel(this.config, directory)
1280
+ `,
1281
+ ` console.error("[opencode-acp] agent.newSession:defaultModel:start", directory)
1282
+ const model = await defaultModel(this.config, directory)
1283
+ console.error("[opencode-acp] agent.newSession:defaultModel:done", directory, model.providerID, model.modelID)
1284
+ `,
1285
+ )
1286
+ .replace(
1287
+ ` const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
1288
+ `,
1289
+ ` console.error("[opencode-acp] agent.newSession:sessionManager.create:start", directory)
1290
+ const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
1291
+ console.error("[opencode-acp] agent.newSession:sessionManager.create:done", directory, state.id)
1292
+ `,
1293
+ )
1294
+ .replace(
1295
+ ` const load = await this.loadSessionMode({
1296
+ `,
1297
+ ` console.error("[opencode-acp] agent.newSession:loadSessionMode:start", directory)
1298
+ const load = await this.loadSessionMode({
1299
+ `,
1300
+ )
1301
+ .replace(
1302
+ ` const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
1303
+ `,
1304
+ ` console.error("[opencode-acp] agent.loadSessionMode:providers:start", directory)
1305
+ const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
1306
+ console.error("[opencode-acp] agent.loadSessionMode:providers:done", directory, providers.length)
1307
+ `,
1308
+ )
1309
+ .replace(
1310
+ ` const modeState = await this.resolveModeState(directory, sessionId)
1311
+ `,
1312
+ ` console.error("[opencode-acp] agent.loadSessionMode:resolveModeState:start", directory)
1313
+ const modeState = await this.resolveModeState(directory, sessionId)
1314
+ console.error("[opencode-acp] agent.loadSessionMode:resolveModeState:done", directory, modeState.availableModes.length)
1315
+ `,
1316
+ )
1317
+ .replace(
1318
+ ` const commands = await this.config.sdk.command
1319
+ `,
1320
+ ` console.error("[opencode-acp] agent.loadSessionMode:commands:start", directory)
1321
+ const commands = await this.config.sdk.command
1322
+ `,
1323
+ ),
1324
+ );
1325
+
1326
+ await rewriteSourceFile(
1327
+ sourceRoot,
1328
+ "packages/opencode/src/project/bootstrap.ts",
1329
+ (contents) =>
1330
+ contents.replace(
1331
+ ` Log.Default.info("bootstrap step", { step: "snapshot.init:start" })
1332
+ Snapshot.init()
1333
+ Log.Default.info("bootstrap step", { step: "snapshot.init:done" })
1334
+ `,
1335
+ ` Log.Default.info("bootstrap step", { step: "snapshot.init:skipped" })
1336
+ `,
1337
+ ),
1338
+ );
1339
+
1340
+ await rewriteSourceFile(
1341
+ sourceRoot,
1342
+ "packages/opencode/src/util/filesystem.ts",
1343
+ (contents) => {
1344
+ if (contents.includes("cachedAgentOsGuestPathMappings")) {
1345
+ return contents;
1346
+ }
1347
+
1348
+ return contents
1349
+ .replace(
1350
+ 'import { dirname, join, relative, resolve as pathResolve, win32 } from "path"\n',
1351
+ `import { dirname, join, relative, resolve as pathResolve, win32 } from "path"
1352
+
1353
+ type AgentOsGuestPathMapping = {
1354
+ guestPath?: string
1355
+ hostPath?: string
1356
+ }
1357
+
1358
+ let cachedAgentOsGuestPathMappings:
1359
+ | Array<{ guestPath: string; hostPath: string }>
1360
+ | undefined
1361
+
1362
+ function runtimeWindowsPath(p: string): string {
1363
+ if (process.platform !== "win32") return p
1364
+ return p
1365
+ .replace(/^\\/([a-zA-Z]):(?:[\\\\/]|$)/, (_, drive) => \`\${drive.toUpperCase()}:/\`)
1366
+ .replace(/^\\/([a-zA-Z])(?:\\/|$)/, (_, drive) => \`\${drive.toUpperCase()}:/\`)
1367
+ .replace(/^\\/cygdrive\\/([a-zA-Z])(?:\\/|$)/, (_, drive) => \`\${drive.toUpperCase()}:/\`)
1368
+ .replace(/^\\/mnt\\/([a-zA-Z])(?:\\/|$)/, (_, drive) => \`\${drive.toUpperCase()}:/\`)
1369
+ }
1370
+
1371
+ function agentOsGuestPathMappings() {
1372
+ if (cachedAgentOsGuestPathMappings) return cachedAgentOsGuestPathMappings
1373
+ const raw = process.env.AGENT_OS_GUEST_PATH_MAPPINGS
1374
+ if (!raw) {
1375
+ cachedAgentOsGuestPathMappings = []
1376
+ return cachedAgentOsGuestPathMappings
1377
+ }
1378
+
1379
+ try {
1380
+ const parsed = JSON.parse(raw)
1381
+ if (!Array.isArray(parsed)) {
1382
+ cachedAgentOsGuestPathMappings = []
1383
+ return cachedAgentOsGuestPathMappings
1384
+ }
1385
+
1386
+ cachedAgentOsGuestPathMappings = parsed
1387
+ .filter(
1388
+ (item): item is AgentOsGuestPathMapping =>
1389
+ typeof item === "object" &&
1390
+ item !== null &&
1391
+ typeof item.guestPath === "string" &&
1392
+ typeof item.hostPath === "string",
1393
+ )
1394
+ .map((item) => ({
1395
+ guestPath: item.guestPath === "/" ? "/" : pathResolve(runtimeWindowsPath(item.guestPath)),
1396
+ hostPath: pathResolve(runtimeWindowsPath(item.hostPath)),
1397
+ }))
1398
+ .sort((left, right) => right.guestPath.length - left.guestPath.length)
1399
+ return cachedAgentOsGuestPathMappings
1400
+ } catch {
1401
+ cachedAgentOsGuestPathMappings = []
1402
+ return cachedAgentOsGuestPathMappings
1403
+ }
1404
+ }
1405
+
1406
+ function runtimePath(p: string): string {
1407
+ if (!p.startsWith("/")) return p
1408
+
1409
+ const normalized = pathResolve(runtimeWindowsPath(p))
1410
+ for (const mapping of agentOsGuestPathMappings()) {
1411
+ if (
1412
+ mapping.guestPath !== "/" &&
1413
+ normalized !== mapping.guestPath &&
1414
+ !normalized.startsWith(\`\${mapping.guestPath}/\`)
1415
+ ) {
1416
+ continue
1417
+ }
1418
+
1419
+ const suffix =
1420
+ mapping.guestPath === "/"
1421
+ ? normalized.slice(1)
1422
+ : normalized.slice(mapping.guestPath.length).replace(/^[/\\\\]+/, "")
1423
+ return suffix ? join(mapping.hostPath, suffix) : mapping.hostPath
1424
+ }
1425
+
1426
+ return p
1427
+ }
1428
+ `,
1429
+ )
1430
+ .replace(
1431
+ ` return existsSync(p)
1432
+ `,
1433
+ ` return existsSync(runtimePath(p))
1434
+ `,
1435
+ )
1436
+ .replace(
1437
+ ` return statSync(p).isDirectory()
1438
+ `,
1439
+ ` return statSync(runtimePath(p)).isDirectory()
1440
+ `,
1441
+ )
1442
+ .replace(
1443
+ ` return statSync(p, { throwIfNoEntry: false }) ?? undefined
1444
+ `,
1445
+ ` return statSync(runtimePath(p), { throwIfNoEntry: false }) ?? undefined
1446
+ `,
1447
+ )
1448
+ .replace(
1449
+ ` return statFile(p).catch((e) => {
1450
+ `,
1451
+ ` return statFile(runtimePath(p)).catch((e) => {
1452
+ `,
1453
+ )
1454
+ .replace(
1455
+ ` return readFile(p, "utf-8")
1456
+ `,
1457
+ ` return readFile(runtimePath(p), "utf-8")
1458
+ `,
1459
+ )
1460
+ .replace(
1461
+ ` return JSON.parse(await readFile(p, "utf-8"))
1462
+ `,
1463
+ ` return JSON.parse(await readFile(runtimePath(p), "utf-8"))
1464
+ `,
1465
+ )
1466
+ .replace(
1467
+ ` return readFile(p)
1468
+ `,
1469
+ ` return readFile(runtimePath(p))
1470
+ `,
1471
+ )
1472
+ .replace(
1473
+ ` const buf = await readFile(p)
1474
+ `,
1475
+ ` const buf = await readFile(runtimePath(p))
1476
+ `,
1477
+ )
1478
+ .replace(
1479
+ ` try {
1480
+ if (mode) {
1481
+ await writeFile(p, content, { mode })
1482
+ } else {
1483
+ await writeFile(p, content)
1484
+ }
1485
+ } catch (e) {
1486
+ if (isEnoent(e)) {
1487
+ await mkdir(dirname(p), { recursive: true })
1488
+ if (mode) {
1489
+ await writeFile(p, content, { mode })
1490
+ } else {
1491
+ await writeFile(p, content)
1492
+ }
1493
+ return
1494
+ }
1495
+ throw e
1496
+ }
1497
+ `,
1498
+ ` const target = runtimePath(p)
1499
+ try {
1500
+ if (mode) {
1501
+ await writeFile(target, content, { mode })
1502
+ } else {
1503
+ await writeFile(target, content)
1504
+ }
1505
+ } catch (e) {
1506
+ if (isEnoent(e)) {
1507
+ await mkdir(dirname(target), { recursive: true })
1508
+ if (mode) {
1509
+ await writeFile(target, content, { mode })
1510
+ } else {
1511
+ await writeFile(target, content)
1512
+ }
1513
+ return
1514
+ }
1515
+ throw e
1516
+ }
1517
+ `,
1518
+ )
1519
+ .replace(
1520
+ ` const dir = dirname(p)
1521
+ `,
1522
+ ` const target = runtimePath(p)
1523
+ const dir = dirname(target)
1524
+ `,
1525
+ )
1526
+ .replace(
1527
+ ` const writeStream = createWriteStream(p)
1528
+ `,
1529
+ ` const writeStream = createWriteStream(target)
1530
+ `,
1531
+ )
1532
+ .replace(
1533
+ ` await chmod(p, mode)
1534
+ `,
1535
+ ` await chmod(target, mode)
1536
+ `,
1537
+ );
1538
+ },
1539
+ );
1540
+
1541
+ for (const relativePath of [
1542
+ "packages/opencode/src/cli/cmd/mcp.ts",
1543
+ "packages/opencode/src/config/config.ts",
1544
+ "packages/opencode/src/config/migrate-tui-config.ts",
1545
+ "packages/opencode/src/config/paths.ts",
1546
+ "packages/opencode/src/plugin/install.ts",
1547
+ ]) {
1548
+ await rewriteSourceFile(sourceRoot, relativePath, (contents) =>
1549
+ contents.replaceAll(
1550
+ '"jsonc-parser"',
1551
+ '"jsonc-parser/lib/esm/main.js"',
1552
+ ),
1553
+ );
1554
+
1555
+ await rewriteSourceFile(
1556
+ sourceRoot,
1557
+ "packages/opencode/src/plugin/index.ts",
1558
+ (contents) =>
1559
+ contents.replace(
1560
+ ` const state = Instance.state(async () => {
1561
+ const client = createOpencodeClient({
1562
+ `,
1563
+ ` const state = Instance.state(async () => {
1564
+ if (Flag.OPENCODE_CLIENT === "acp") {
1565
+ log.info("skipping plugin runtime in ACP mode")
1566
+ return {
1567
+ hooks: [],
1568
+ input: {
1569
+ client: undefined as any,
1570
+ project: Instance.project,
1571
+ worktree: Instance.worktree,
1572
+ directory: Instance.directory,
1573
+ serverUrl: "",
1574
+ $: Bun.$,
1575
+ } as PluginInput,
1576
+ }
1577
+ }
1578
+
1579
+ const client = createOpencodeClient({
1580
+ `,
1581
+ ),
1582
+ );
1583
+
1584
+ await rewriteSourceFile(
1585
+ sourceRoot,
1586
+ "packages/opencode/src/session/index.ts",
1587
+ (contents) =>
1588
+ contents
1589
+ .replace('import { SessionPrompt } from "./prompt"\n', "")
1590
+ .replace('import { Command } from "../command"\n', "")
1591
+ .replace(
1592
+ ` const initialize = Effect.fn("Session.initialize")(function* (input: {
1593
+ sessionID: SessionID
1594
+ modelID: ModelID
1595
+ providerID: ProviderID
1596
+ messageID: MessageID
1597
+ }) {
1598
+ yield* Effect.promise(() =>
1599
+ SessionPrompt.command({
1600
+ sessionID: input.sessionID,
1601
+ messageID: input.messageID,
1602
+ model: input.providerID + "/" + input.modelID,
1603
+ command: Command.Default.INIT,
1604
+ arguments: "",
1605
+ }),
1606
+ )
1607
+ })
1608
+ `,
1609
+ ` const initialize = Effect.fn("Session.initialize")(function* (input: {
1610
+ sessionID: SessionID
1611
+ modelID: ModelID
1612
+ providerID: ProviderID
1613
+ messageID: MessageID
1614
+ }) {
1615
+ const [{ SessionPrompt }, { Command }] = yield* Effect.promise(() =>
1616
+ Promise.all([import("./prompt"), import("../command")]),
1617
+ )
1618
+ yield* Effect.promise(() =>
1619
+ SessionPrompt.command({
1620
+ sessionID: input.sessionID,
1621
+ messageID: input.messageID,
1622
+ model: input.providerID + "/" + input.modelID,
1623
+ command: Command.Default.INIT,
1624
+ arguments: "",
1625
+ }),
1626
+ )
1627
+ })
1628
+ `,
1629
+ ),
1630
+ );
1631
+
1632
+ await rewriteSourceFile(
1633
+ sourceRoot,
1634
+ "packages/opencode/src/session/prompt.ts",
1635
+ (contents) =>
1636
+ contents
1637
+ .replace(
1638
+ / const text = yield\* Effect\.promise\(async \(signal\) => \{\n[\s\S]*? return result\.text\n \}\)\n/,
1639
+ ` const instanceCtx = yield* InstanceState.context
1640
+ const text = yield* Effect.promise((signal) =>
1641
+ Instance.restore(instanceCtx, async () => {
1642
+ const mdl = ag.model
1643
+ ? await Provider.getModel(ag.model.providerID, ag.model.modelID)
1644
+ : ((await Provider.getSmallModel(input.providerID)) ??
1645
+ (await Provider.getModel(input.providerID, input.modelID)))
1646
+ const msgs = onlySubtasks
1647
+ ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\\n") }]
1648
+ : await MessageV2.toModelMessages(context, mdl)
1649
+ const result = await LLM.stream({
1650
+ agent: ag,
1651
+ user: firstInfo,
1652
+ system: [],
1653
+ small: true,
1654
+ tools: {},
1655
+ model: mdl,
1656
+ abort: signal,
1657
+ sessionID: input.session.id,
1658
+ retries: 2,
1659
+ messages: [{ role: "user", content: "Generate a title for this conversation:\\n" }, ...msgs],
1660
+ })
1661
+ return result.text
1662
+ }),
1663
+ )
1664
+ `,
1665
+ )
1666
+ .replace(
1667
+ ` const getModel = (providerID: ProviderID, modelID: ModelID, sessionID: SessionID) =>
1668
+ Effect.promise(() =>
1669
+ Provider.getModel(providerID, modelID).catch((e) => {
1670
+ if (Provider.ModelNotFoundError.isInstance(e)) {
1671
+ const hint = e.data.suggestions?.length ? \` Did you mean: \${e.data.suggestions.join(", ")}?\` : ""
1672
+ Bus.publish(Session.Event.Error, {
1673
+ sessionID,
1674
+ error: new NamedError.Unknown({
1675
+ message: \`Model not found: \${e.data.providerID}/\${e.data.modelID}.\${hint}\`,
1676
+ }).toObject(),
1677
+ })
1678
+ }
1679
+ throw e
1680
+ }),
1681
+ )
1682
+ `,
1683
+ ` const getModel = (providerID: ProviderID, modelID: ModelID, sessionID: SessionID) =>
1684
+ Effect.gen(function* () {
1685
+ const instanceCtx = yield* InstanceState.context
1686
+ return yield* Effect.promise(() =>
1687
+ Instance.restore(instanceCtx, () =>
1688
+ Provider.getModel(providerID, modelID).catch((e) => {
1689
+ if (Provider.ModelNotFoundError.isInstance(e)) {
1690
+ const hint = e.data.suggestions?.length ? \` Did you mean: \${e.data.suggestions.join(", ")}?\` : ""
1691
+ Bus.publish(Session.Event.Error, {
1692
+ sessionID,
1693
+ error: new NamedError.Unknown({
1694
+ message: \`Model not found: \${e.data.providerID}/\${e.data.modelID}.\${hint}\`,
1695
+ }).toObject(),
1696
+ })
1697
+ }
1698
+ throw e
1699
+ }),
1700
+ ),
1701
+ )
1702
+ })
1703
+ `,
1704
+ )
1705
+ .replace(
1706
+ ` const model = input.model ?? ag.model ?? (yield* lastModel(input.sessionID))
1707
+ const full =
1708
+ !input.variant && ag.variant
1709
+ ? yield* Effect.promise(() => Provider.getModel(model.providerID, model.modelID).catch(() => undefined))
1710
+ : undefined
1711
+ `,
1712
+ ` const model = input.model ?? ag.model ?? (yield* lastModel(input.sessionID))
1713
+ const instanceCtx = yield* InstanceState.context
1714
+ const full =
1715
+ !input.variant && ag.variant
1716
+ ? yield* Effect.promise(() =>
1717
+ Instance.restore(instanceCtx, () =>
1718
+ Provider.getModel(model.providerID, model.modelID).catch(() => undefined),
1719
+ ),
1720
+ )
1721
+ : undefined
1722
+ `,
1723
+ )
1724
+ .replace(
1725
+ ` Effect.promise(() => Provider.getModel(info.model.providerID, info.model.modelID)).pipe(
1726
+ `,
1727
+ ` Effect.gen(function* () {
1728
+ const instanceCtx = yield* InstanceState.context
1729
+ return yield* Effect.promise(() =>
1730
+ Instance.restore(instanceCtx, () =>
1731
+ Provider.getModel(info.model.providerID, info.model.modelID),
1732
+ ),
1733
+ )
1734
+ }).pipe(
1735
+ `,
1736
+ )
1737
+ .replace(
1738
+ ` const tools = yield* resolveTools({
1739
+ agent,
1740
+ session,
1741
+ model,
1742
+ tools: lastUser.tools,
1743
+ processor: handle,
1744
+ bypassAgentCheck,
1745
+ messages: msgs,
1746
+ })
1747
+ `,
1748
+ ` const tools = yield* resolveTools({
1749
+ agent,
1750
+ session,
1751
+ model,
1752
+ tools: lastUser.tools,
1753
+ processor: handle,
1754
+ bypassAgentCheck,
1755
+ messages: msgs,
1756
+ })
1757
+ `,
1758
+ )
1759
+ .replace(
1760
+ ` const [skills, env, instructions, modelMsgs] = yield* Effect.promise(() =>
1761
+ Promise.all([
1762
+ SystemPrompt.skills(agent),
1763
+ SystemPrompt.environment(model),
1764
+ InstructionPrompt.system(),
1765
+ MessageV2.toModelMessages(msgs, model),
1766
+ ]),
1767
+ )
1768
+ `,
1769
+ ` const instanceCtx = yield* InstanceState.context
1770
+ const [skills, env, instructions, modelMsgs] = yield* Effect.promise(() =>
1771
+ Instance.restore(instanceCtx, () =>
1772
+ (async () => {
1773
+ console.error("[opencode-acp] prompt.system:skills:start", sessionID)
1774
+ const skills = await Instance.restore(instanceCtx, () => SystemPrompt.skills(agent))
1775
+ console.error("[opencode-acp] prompt.system:skills:done", sessionID)
1776
+ console.error("[opencode-acp] prompt.system:env:start", sessionID)
1777
+ const env = await Instance.restore(instanceCtx, () => SystemPrompt.environment(model))
1778
+ console.error("[opencode-acp] prompt.system:env:done", sessionID)
1779
+ console.error("[opencode-acp] prompt.system:instructions:start", sessionID)
1780
+ const instructions = await Instance.restore(instanceCtx, () => InstructionPrompt.system())
1781
+ console.error("[opencode-acp] prompt.system:instructions:done", sessionID)
1782
+ console.error("[opencode-acp] prompt.system:modelMessages:start", sessionID)
1783
+ const modelMsgs = await Instance.restore(instanceCtx, () =>
1784
+ MessageV2.toModelMessages(msgs, model),
1785
+ )
1786
+ console.error("[opencode-acp] prompt.system:modelMessages:done", sessionID)
1787
+ return [skills, env, instructions, modelMsgs] as const
1788
+ })(),
1789
+ ),
1790
+ )
1791
+ `,
1792
+ )
1793
+ .replace(
1794
+ ` const defaultLayer = Layer.unwrap(
1795
+ Effect.sync(() =>
1796
+ layer.pipe(
1797
+ Layer.provide(SessionStatus.layer),
1798
+ Layer.provide(SessionCompaction.defaultLayer),
1799
+ Layer.provide(SessionProcessor.defaultLayer),
1800
+ Layer.provide(Command.defaultLayer),
1801
+ Layer.provide(Permission.layer),
1802
+ Layer.provide(MCP.defaultLayer),
1803
+ Layer.provide(LSP.defaultLayer),
1804
+ Layer.provide(FileTime.defaultLayer),
1805
+ Layer.provide(ToolRegistry.defaultLayer),
1806
+ Layer.provide(Truncate.layer),
1807
+ Layer.provide(AppFileSystem.defaultLayer),
1808
+ Layer.provide(Plugin.defaultLayer),
1809
+ Layer.provide(Session.defaultLayer),
1810
+ Layer.provide(Agent.defaultLayer),
1811
+ Layer.provide(Bus.layer),
1812
+ ),
1813
+ ),
1814
+ )
1815
+ `,
1816
+ ` export const defaultLayer = Layer.unwrap(
1817
+ Effect.sync(() =>
1818
+ layer.pipe(
1819
+ Layer.provide(SessionStatus.layer),
1820
+ Layer.provide(SessionCompaction.defaultLayer),
1821
+ Layer.provide(SessionProcessor.defaultLayer),
1822
+ Layer.provide(Command.defaultLayer),
1823
+ Layer.provide(Permission.layer),
1824
+ Layer.provide(MCP.defaultLayer),
1825
+ Layer.provide(LSP.defaultLayer),
1826
+ Layer.provide(FileTime.defaultLayer),
1827
+ Layer.provide(ToolRegistry.defaultLayer),
1828
+ Layer.provide(Truncate.layer),
1829
+ Layer.provide(AppFileSystem.defaultLayer),
1830
+ Layer.provide(Plugin.defaultLayer),
1831
+ Layer.provide(Session.defaultLayer),
1832
+ Layer.provide(Agent.defaultLayer),
1833
+ Layer.provide(Bus.layer),
1834
+ ),
1835
+ ),
1836
+ )
1837
+ `,
1838
+ ),
1839
+ );
1840
+
1841
+ await rewriteSourceFile(
1842
+ sourceRoot,
1843
+ "packages/opencode/src/session/llm.ts",
1844
+ (contents) => {
1845
+ let updated = contents;
1846
+ if (
1847
+ !updated.includes(
1848
+ 'import { InstanceState } from "@/effect/instance-state"\n',
1849
+ )
1850
+ ) {
1851
+ updated = updated.replace(
1852
+ 'import { Installation } from "@/installation"\n',
1853
+ 'import { Installation } from "@/installation"\nimport { InstanceState } from "@/effect/instance-state"\n',
1854
+ );
1855
+ }
1856
+ updated = updated
1857
+ .replace(
1858
+ ` stream(input) {
1859
+ return Stream.scoped(
1860
+ Stream.unwrap(
1861
+ Effect.gen(function* () {
1862
+ const ctrl = yield* Effect.acquireRelease(
1863
+ Effect.sync(() => new AbortController()),
1864
+ (ctrl) => Effect.sync(() => ctrl.abort()),
1865
+ )
1866
+
1867
+ const result = yield* Effect.promise(() => LLM.stream({ ...input, abort: ctrl.signal }))
1868
+
1869
+ return Stream.fromAsyncIterable(result.fullStream, (e) =>
1870
+ e instanceof Error ? e : new Error(String(e)),
1871
+ )
1872
+ }),
1873
+ ),
1874
+ )
1875
+ },
1876
+ `,
1877
+ ` stream(input) {
1878
+ return Stream.scoped(
1879
+ Stream.unwrap(
1880
+ Effect.gen(function* () {
1881
+ const instanceCtx = yield* InstanceState.context
1882
+ const ctrl = yield* Effect.acquireRelease(
1883
+ Effect.sync(() => new AbortController()),
1884
+ (ctrl) => Effect.sync(() => ctrl.abort()),
1885
+ )
1886
+
1887
+ const result = yield* Effect.promise(() =>
1888
+ Instance.restore(instanceCtx, () => LLM.stream({ ...input, abort: ctrl.signal })),
1889
+ )
1890
+
1891
+ return Stream.fromAsyncIterable(result.fullStream, (e) =>
1892
+ e instanceof Error ? e : new Error(String(e)),
1893
+ )
1894
+ }),
1895
+ ),
1896
+ )
1897
+ },
1898
+ `,
1899
+ )
1900
+ .replace(
1901
+ ` const [language, cfg, provider, auth] = await Promise.all([
1902
+ Provider.getLanguage(input.model),
1903
+ Config.get(),
1904
+ Provider.getProvider(input.model.providerID),
1905
+ Auth.get(input.model.providerID),
1906
+ ])
1907
+ `,
1908
+ ` const instanceCtx = Instance.current
1909
+ const [language, cfg, provider, auth] = await Instance.restore(instanceCtx, () =>
1910
+ Promise.all([
1911
+ Provider.getLanguage(input.model),
1912
+ Config.get(),
1913
+ Provider.getProvider(input.model.providerID),
1914
+ Auth.get(input.model.providerID),
1915
+ ]),
1916
+ )
1917
+ `,
1918
+ )
1919
+ .replace(
1920
+ ` await Plugin.trigger(
1921
+ "experimental.chat.system.transform",
1922
+ { sessionID: input.sessionID, model: input.model },
1923
+ { system },
1924
+ )
1925
+ `,
1926
+ ` await Instance.restore(instanceCtx, () =>
1927
+ Plugin.trigger(
1928
+ "experimental.chat.system.transform",
1929
+ { sessionID: input.sessionID, model: input.model },
1930
+ { system },
1931
+ ),
1932
+ )
1933
+ `,
1934
+ )
1935
+ .replace(
1936
+ ` const params = await Plugin.trigger(
1937
+ "chat.params",
1938
+ {
1939
+ sessionID: input.sessionID,
1940
+ agent: input.agent.name,
1941
+ model: input.model,
1942
+ provider,
1943
+ message: input.user,
1944
+ },
1945
+ {
1946
+ temperature: input.model.capabilities.temperature
1947
+ ? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
1948
+ : undefined,
1949
+ topP: input.agent.topP ?? ProviderTransform.topP(input.model),
1950
+ topK: ProviderTransform.topK(input.model),
1951
+ options,
1952
+ },
1953
+ )
1954
+ `,
1955
+ ` const params = await Instance.restore(instanceCtx, () =>
1956
+ Plugin.trigger(
1957
+ "chat.params",
1958
+ {
1959
+ sessionID: input.sessionID,
1960
+ agent: input.agent.name,
1961
+ model: input.model,
1962
+ provider,
1963
+ message: input.user,
1964
+ },
1965
+ {
1966
+ temperature: input.model.capabilities.temperature
1967
+ ? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
1968
+ : undefined,
1969
+ topP: input.agent.topP ?? ProviderTransform.topP(input.model),
1970
+ topK: ProviderTransform.topK(input.model),
1971
+ options,
1972
+ },
1973
+ ),
1974
+ )
1975
+ `,
1976
+ )
1977
+ .replace(
1978
+ ` const { headers } = await Plugin.trigger(
1979
+ "chat.headers",
1980
+ {
1981
+ sessionID: input.sessionID,
1982
+ agent: input.agent.name,
1983
+ model: input.model,
1984
+ provider,
1985
+ message: input.user,
1986
+ },
1987
+ {
1988
+ headers: {},
1989
+ },
1990
+ )
1991
+ `,
1992
+ ` const { headers } = await Instance.restore(instanceCtx, () =>
1993
+ Plugin.trigger(
1994
+ "chat.headers",
1995
+ {
1996
+ sessionID: input.sessionID,
1997
+ agent: input.agent.name,
1998
+ model: input.model,
1999
+ provider,
2000
+ message: input.user,
2001
+ },
2002
+ {
2003
+ headers: {},
2004
+ },
2005
+ ),
2006
+ )
2007
+ `,
2008
+ )
2009
+ .replace(
2010
+ ` const tools = await resolveTools(input)
2011
+ `,
2012
+ ` const tools = await Instance.restore(instanceCtx, () => resolveTools(input))
2013
+ `,
2014
+ )
2015
+ .replace(
2016
+ ` headers: {
2017
+ ...(input.model.providerID.startsWith("opencode")
2018
+ ? {
2019
+ "x-opencode-project": Instance.project.id,
2020
+ "x-opencode-session": input.sessionID,
2021
+ "x-opencode-request": input.user.id,
2022
+ "x-opencode-client": Flag.OPENCODE_CLIENT,
2023
+ }
2024
+ : {
2025
+ "User-Agent": \`opencode/\${Installation.VERSION}\`,
2026
+ }),
2027
+ ...input.model.headers,
2028
+ ...headers,
2029
+ },
2030
+ `,
2031
+ ` headers: {
2032
+ ...(input.model.providerID.startsWith("opencode")
2033
+ ? Instance.restore(instanceCtx, () => ({
2034
+ "x-opencode-project": Instance.project.id,
2035
+ "x-opencode-session": input.sessionID,
2036
+ "x-opencode-request": input.user.id,
2037
+ "x-opencode-client": Flag.OPENCODE_CLIENT,
2038
+ }))
2039
+ : {
2040
+ "User-Agent": \`opencode/\${Installation.VERSION}\`,
2041
+ }),
2042
+ ...input.model.headers,
2043
+ ...headers,
2044
+ },
2045
+ `,
2046
+ )
2047
+ .replace(
2048
+ ` return streamText({
2049
+ `,
2050
+ ` return Instance.restore(instanceCtx, () => streamText({
2051
+ `,
2052
+ )
2053
+ .replace(
2054
+ ` })
2055
+ }
2056
+ `,
2057
+ ` }))
2058
+ }
2059
+ `,
2060
+ );
2061
+ return updated;
2062
+ },
2063
+ );
2064
+
2065
+ await rewriteSourceFile(
2066
+ sourceRoot,
2067
+ "packages/opencode/src/session/prompt.ts",
2068
+ (contents) =>
2069
+ contents.replace(
2070
+ ` execute(args, options) {
2071
+ return Effect.runPromise(
2072
+ Effect.gen(function* () {
2073
+ const ctx = context(args, options)
2074
+ yield* plugin.trigger(
2075
+ "tool.execute.before",
2076
+ { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
2077
+ { args },
2078
+ )
2079
+ const result = yield* Effect.promise(() => item.execute(args, ctx))
2080
+ const output = {
2081
+ ...result,
2082
+ attachments: result.attachments?.map((attachment) => ({
2083
+ ...attachment,
2084
+ id: PartID.ascending(),
2085
+ sessionID: ctx.sessionID,
2086
+ messageID: input.processor.message.id,
2087
+ })),
2088
+ }
2089
+ yield* plugin.trigger(
2090
+ "tool.execute.after",
2091
+ { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
2092
+ output,
2093
+ )
2094
+ return output
2095
+ }),
2096
+ )
2097
+ },
2098
+ `,
2099
+ ` execute(args, options) {
2100
+ const instanceCtx =
2101
+ ((globalThis as typeof globalThis & { __agentosOpencodeInstanceFallback?: unknown })
2102
+ .__agentosOpencodeInstanceFallback ??
2103
+ Instance.current) as any
2104
+ return Instance.restore(instanceCtx, () =>
2105
+ Effect.runPromise(
2106
+ Effect.gen(function* () {
2107
+ const ctx = context(args, options)
2108
+ yield* plugin.trigger(
2109
+ "tool.execute.before",
2110
+ { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
2111
+ { args },
2112
+ )
2113
+ const result = yield* Effect.promise(() => item.execute(args, ctx))
2114
+ const output = {
2115
+ ...result,
2116
+ attachments: result.attachments?.map((attachment) => ({
2117
+ ...attachment,
2118
+ id: PartID.ascending(),
2119
+ sessionID: ctx.sessionID,
2120
+ messageID: input.processor.message.id,
2121
+ })),
2122
+ }
2123
+ yield* plugin.trigger(
2124
+ "tool.execute.after",
2125
+ { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
2126
+ output,
2127
+ )
2128
+ return output
2129
+ }),
2130
+ ),
2131
+ )
2132
+ },
2133
+ `,
2134
+ ),
2135
+ );
2136
+
2137
+ await rewriteSourceFile(
2138
+ sourceRoot,
2139
+ "packages/opencode/src/session/instruction.ts",
2140
+ (contents) =>
2141
+ contents
2142
+ .replace(
2143
+ `async function resolveRelative(instruction: string): Promise<string[]> {
2144
+ if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
2145
+ return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
2146
+ }
2147
+ if (!Flag.OPENCODE_CONFIG_DIR) {
2148
+ log.warn(
2149
+ \`Skipping relative instruction "\${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled\`,
2150
+ )
2151
+ return []
2152
+ }
2153
+ return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
2154
+ }
2155
+ `,
2156
+ `async function resolveRelative(instruction: string): Promise<string[]> {
2157
+ const ctx = Instance.current
2158
+ if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
2159
+ return Instance.restore(ctx, () =>
2160
+ Filesystem.globUp(instruction, ctx.directory, ctx.worktree).catch(() => []),
2161
+ )
2162
+ }
2163
+ if (!Flag.OPENCODE_CONFIG_DIR) {
2164
+ log.warn(
2165
+ \`Skipping relative instruction "\${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled\`,
2166
+ )
2167
+ return []
2168
+ }
2169
+ return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
2170
+ }
2171
+ `,
2172
+ )
2173
+ .replace(
2174
+ ` export async function systemPaths() {
2175
+ const config = await Config.get()
2176
+ `,
2177
+ ` export async function systemPaths() {
2178
+ const ctx = Instance.current
2179
+ const config = await Instance.restore(ctx, () => Config.get())
2180
+ `,
2181
+ )
2182
+ .replace(
2183
+ ` const matches = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
2184
+ `,
2185
+ ` const matches = await Instance.restore(ctx, () =>
2186
+ Filesystem.findUp(file, ctx.directory, ctx.worktree),
2187
+ )
2188
+ `,
2189
+ )
2190
+ .replace(
2191
+ ` : await resolveRelative(instruction)
2192
+ `,
2193
+ ` : await Instance.restore(ctx, () => resolveRelative(instruction))
2194
+ `,
2195
+ )
2196
+ .replace(
2197
+ ` export async function system() {
2198
+ const config = await Config.get()
2199
+ const paths = await systemPaths()
2200
+ `,
2201
+ ` export async function system() {
2202
+ const ctx = Instance.current
2203
+ const config = await Instance.restore(ctx, () => Config.get())
2204
+ const paths = await Instance.restore(ctx, () => systemPaths())
2205
+ `,
2206
+ )
2207
+ .replace(
2208
+ ` export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: string) {
2209
+ const system = await systemPaths()
2210
+ const already = loaded(messages)
2211
+ const results: { filepath: string; content: string }[] = []
2212
+
2213
+ const target = path.resolve(filepath)
2214
+ let current = path.dirname(target)
2215
+ const root = path.resolve(Instance.directory)
2216
+ `,
2217
+ ` export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: string) {
2218
+ const ctx = Instance.current
2219
+ const system = await Instance.restore(ctx, () => systemPaths())
2220
+ const already = loaded(messages)
2221
+ const results: { filepath: string; content: string }[] = []
2222
+
2223
+ const target = path.resolve(filepath)
2224
+ let current = path.dirname(target)
2225
+ const root = path.resolve(ctx.directory)
2226
+ `,
2227
+ ),
2228
+ );
2229
+
2230
+ await rewriteSourceFile(
2231
+ sourceRoot,
2232
+ "packages/opencode/src/provider/provider.ts",
2233
+ (contents) =>
2234
+ contents
2235
+ .replace(
2236
+ 'import { Config } from "../config/config"\n',
2237
+ 'import { Config } from "../config/config"\nimport { Instance } from "../project/instance"\n',
2238
+ )
2239
+ .replace(
2240
+ ` async function loadProviders() {
2241
+ const cfg = await Config.get()
2242
+ `,
2243
+ ` async function loadProviders() {
2244
+ const ctx = Instance.current
2245
+ const cfg = await Instance.restore(ctx, () => Config.get())
2246
+ `,
2247
+ )
2248
+ .replace(
2249
+ ` export async function defaultModel() {
2250
+ const cfg = await Config.get()
2251
+ `,
2252
+ ` export async function defaultModel() {
2253
+ const ctx = Instance.current
2254
+ const cfg = await Instance.restore(ctx, () => Config.get())
2255
+ `,
2256
+ )
2257
+ .replace(
2258
+ ` const providers = await loadProviders()
2259
+ for (const provider of Object.values(providers)) {
2260
+ `,
2261
+ ` const providers = await Instance.restore(ctx, () => loadProviders())
2262
+ for (const provider of Object.values(providers)) {
2263
+ `,
2264
+ ),
2265
+ );
2266
+
2267
+ await rewriteSourceFile(
2268
+ sourceRoot,
2269
+ "packages/opencode/src/effect/instance-state.ts",
2270
+ (contents) =>
2271
+ contents.replace(
2272
+ ` const fiber = Fiber.getCurrent()
2273
+ const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined
2274
+ if (!ctx) return fn
2275
+ return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
2276
+ `,
2277
+ ` const fiber = Fiber.getCurrent()
2278
+ const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined
2279
+ const fallback = (globalThis as typeof globalThis & { __agentosOpencodeInstanceFallback?: unknown })
2280
+ .__agentosOpencodeInstanceFallback as typeof ctx
2281
+ const boundCtx = ctx ?? fallback
2282
+ if (!boundCtx) return fn
2283
+ return ((...args: any[]) => Instance.restore(boundCtx, () => fn(...args))) as F
2284
+ `,
2285
+ ).replace(
2286
+ ` export const context = Effect.fnUntraced(function* () {
2287
+ return (yield* InstanceRef) ?? Instance.current
2288
+ })()
2289
+ `,
2290
+ ` export const context = Effect.fnUntraced(function* () {
2291
+ const ref = yield* InstanceRef
2292
+ if (ref) return ref
2293
+ const fallback = (globalThis as typeof globalThis & { __agentosOpencodeInstanceFallback?: unknown })
2294
+ .__agentosOpencodeInstanceFallback
2295
+ if (fallback) return fallback as typeof ref
2296
+ try {
2297
+ return Instance.current
2298
+ } catch (error) {
2299
+ console.error("[opencode-acp] missing-instance-context", new Error().stack)
2300
+ throw error
2301
+ }
2302
+ })()
2303
+ `,
2304
+ ),
2305
+ );
2306
+
2307
+ await writeFile(
2308
+ resolve(sourceRoot, "packages/opencode/src/effect/run-service.ts"),
2309
+ `import { Effect, Layer, ManagedRuntime } from "effect"
2310
+ import * as ServiceMap from "effect/ServiceMap"
2311
+ import { Instance } from "@/project/instance"
2312
+ import { Context } from "@/util/context"
2313
+ import { InstanceRef } from "./instance-ref"
2314
+
2315
+ export const memoMap = Layer.makeMemoMapUnsafe()
2316
+
2317
+ function attach<A, E, R>(effect: Effect.Effect<A, E, R>) {
2318
+ try {
2319
+ const ctx = Instance.current
2320
+ return {
2321
+ ctx,
2322
+ effect: Effect.provideService(effect, InstanceRef, ctx),
2323
+ }
2324
+ } catch (err) {
2325
+ if (!(err instanceof Context.NotFound)) throw err
2326
+ }
2327
+ return { ctx: undefined, effect }
2328
+ }
2329
+
2330
+ export function makeRuntime<I, S, E>(service: ServiceMap.Service<I, S>, layer: Layer.Layer<I, E>) {
2331
+ let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
2332
+ const getRuntime = () => (rt ??= ManagedRuntime.make(layer, { memoMap }))
2333
+
2334
+ return {
2335
+ runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => {
2336
+ const attached = attach(service.use(fn))
2337
+ return attached.ctx
2338
+ ? Instance.restore(attached.ctx, () => getRuntime().runSync(attached.effect))
2339
+ : getRuntime().runSync(attached.effect)
2340
+ },
2341
+ runPromiseExit: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) => {
2342
+ const attached = attach(service.use(fn))
2343
+ return attached.ctx
2344
+ ? Instance.restore(attached.ctx, () => getRuntime().runPromiseExit(attached.effect, options))
2345
+ : getRuntime().runPromiseExit(attached.effect, options)
2346
+ },
2347
+ runPromise: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) => {
2348
+ const attached = attach(service.use(fn))
2349
+ return attached.ctx
2350
+ ? Instance.restore(attached.ctx, () => getRuntime().runPromise(attached.effect, options))
2351
+ : getRuntime().runPromise(attached.effect, options)
2352
+ },
2353
+ runFork: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => {
2354
+ const attached = attach(service.use(fn))
2355
+ return attached.ctx
2356
+ ? Instance.restore(attached.ctx, () => getRuntime().runFork(attached.effect))
2357
+ : getRuntime().runFork(attached.effect)
2358
+ },
2359
+ runCallback: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => {
2360
+ const attached = attach(service.use(fn))
2361
+ return attached.ctx
2362
+ ? Instance.restore(attached.ctx, () => getRuntime().runCallback(attached.effect))
2363
+ : getRuntime().runCallback(attached.effect)
2364
+ },
2365
+ }
2366
+ }
2367
+ `,
2368
+ );
2369
+
2370
+ await rewriteSourceFile(
2371
+ sourceRoot,
2372
+ "packages/opencode/src/tool/tool.ts",
2373
+ (contents) =>
2374
+ contents
2375
+ .replace(
2376
+ 'import { Truncate } from "./truncate"\n',
2377
+ 'import { Truncate } from "./truncate"\nimport { InstanceState } from "@/effect/instance-state"\n',
2378
+ )
2379
+ .replace(
2380
+ ` const toolInfo = init instanceof Function ? await init(initCtx) : init
2381
+ const execute = toolInfo.execute
2382
+ toolInfo.execute = async (args, ctx) => {
2383
+ `,
2384
+ ` const toolInfo =
2385
+ init instanceof Function ? await InstanceState.bind(() => init(initCtx))() : init
2386
+ const execute = toolInfo.execute
2387
+ toolInfo.execute = InstanceState.bind(async (args, ctx) => {
2388
+ `,
2389
+ )
2390
+ .replace(
2391
+ ` }
2392
+ return toolInfo
2393
+ `,
2394
+ ` })
2395
+ return toolInfo
2396
+ `,
2397
+ ),
2398
+ );
2399
+
2400
+ await rewriteSourceFile(
2401
+ sourceRoot,
2402
+ "packages/opencode/src/storage/db.ts",
2403
+ (contents) =>
2404
+ contents.replace(
2405
+ ` db.run("PRAGMA journal_mode = WAL")
2406
+ db.run("PRAGMA synchronous = NORMAL")
2407
+ db.run("PRAGMA busy_timeout = 5000")
2408
+ db.run("PRAGMA cache_size = -64000")
2409
+ db.run("PRAGMA foreign_keys = ON")
2410
+ db.run("PRAGMA wal_checkpoint(PASSIVE)")`,
2411
+ ` db.$client.exec("PRAGMA journal_mode = WAL")
2412
+ db.$client.exec("PRAGMA synchronous = NORMAL")
2413
+ db.$client.exec("PRAGMA busy_timeout = 5000")
2414
+ db.$client.exec("PRAGMA cache_size = -64000")
2415
+ db.$client.exec("PRAGMA foreign_keys = ON")
2416
+ db.$client.exec("PRAGMA wal_checkpoint(PASSIVE)")`,
2417
+ ),
2418
+ );
2419
+ }
2420
+
2421
+ await rewriteSourceFile(
2422
+ sourceRoot,
2423
+ "packages/opencode/src/provider/provider.ts",
2424
+ (contents) =>
2425
+ contents
2426
+ .replace(
2427
+ `// Direct imports for bundled providers
2428
+ import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
2429
+ import { createAnthropic } from "@ai-sdk/anthropic"
2430
+ import { createAzure } from "@ai-sdk/azure"
2431
+ import { createGoogleGenerativeAI } from "@ai-sdk/google"
2432
+ import { createVertex } from "@ai-sdk/google-vertex"
2433
+ import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
2434
+ import { createOpenAI } from "@ai-sdk/openai"
2435
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
2436
+ import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
2437
+ import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
2438
+ import { createXai } from "@ai-sdk/xai"
2439
+ import { createMistral } from "@ai-sdk/mistral"
2440
+ import { createGroq } from "@ai-sdk/groq"
2441
+ import { createDeepInfra } from "@ai-sdk/deepinfra"
2442
+ import { createCerebras } from "@ai-sdk/cerebras"
2443
+ import { createCohere } from "@ai-sdk/cohere"
2444
+ import { createGateway } from "@ai-sdk/gateway"
2445
+ import { createTogetherAI } from "@ai-sdk/togetherai"
2446
+ import { createPerplexity } from "@ai-sdk/perplexity"
2447
+ import { createVercel } from "@ai-sdk/vercel"
2448
+ import { createGitLab } from "@gitlab/gitlab-ai-provider"
2449
+ import { ProviderTransform } from "./transform"
2450
+ `,
2451
+ `import type { LanguageModelV2 } from "@openrouter/ai-sdk-provider"
2452
+ import { ProviderTransform } from "./transform"
2453
+ `,
2454
+ )
2455
+ .replace(
2456
+ ` const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
2457
+ "@ai-sdk/amazon-bedrock": createAmazonBedrock,
2458
+ "@ai-sdk/anthropic": createAnthropic,
2459
+ "@ai-sdk/azure": createAzure,
2460
+ "@ai-sdk/google": createGoogleGenerativeAI,
2461
+ "@ai-sdk/google-vertex": createVertex,
2462
+ "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
2463
+ "@ai-sdk/openai": createOpenAI,
2464
+ "@ai-sdk/openai-compatible": createOpenAICompatible,
2465
+ "@openrouter/ai-sdk-provider": createOpenRouter,
2466
+ "@ai-sdk/xai": createXai,
2467
+ "@ai-sdk/mistral": createMistral,
2468
+ "@ai-sdk/groq": createGroq,
2469
+ "@ai-sdk/deepinfra": createDeepInfra,
2470
+ "@ai-sdk/cerebras": createCerebras,
2471
+ "@ai-sdk/cohere": createCohere,
2472
+ "@ai-sdk/gateway": createGateway,
2473
+ "@ai-sdk/togetherai": createTogetherAI,
2474
+ "@ai-sdk/perplexity": createPerplexity,
2475
+ "@ai-sdk/vercel": createVercel,
2476
+ "@gitlab/gitlab-ai-provider": createGitLab,
2477
+ // @ts-ignore (TODO: kill this code so we dont have to maintain it)
2478
+ "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
2479
+ }
2480
+ `,
2481
+ ` const BUNDLED_PROVIDERS: Record<string, (options: any) => Promise<SDK>> = {
2482
+ "@ai-sdk/amazon-bedrock": async (options) =>
2483
+ (await import("@ai-sdk/amazon-bedrock")).createAmazonBedrock(options),
2484
+ "@ai-sdk/anthropic": async (options) => (await import("@ai-sdk/anthropic")).createAnthropic(options),
2485
+ "@ai-sdk/azure": async (options) => (await import("@ai-sdk/azure")).createAzure(options),
2486
+ "@ai-sdk/google": async (options) => (await import("@ai-sdk/google")).createGoogleGenerativeAI(options),
2487
+ "@ai-sdk/google-vertex": async (options) => (await import("@ai-sdk/google-vertex")).createVertex(options),
2488
+ "@ai-sdk/google-vertex/anthropic": async (options) =>
2489
+ (await import("@ai-sdk/google-vertex/anthropic")).createVertexAnthropic(options),
2490
+ "@ai-sdk/openai": async (options) => (await import("@ai-sdk/openai")).createOpenAI(options),
2491
+ "@ai-sdk/openai-compatible": async (options) =>
2492
+ (await import("@ai-sdk/openai-compatible")).createOpenAICompatible(options),
2493
+ "@openrouter/ai-sdk-provider": async (options) =>
2494
+ (await import("@openrouter/ai-sdk-provider")).createOpenRouter(options),
2495
+ "@ai-sdk/xai": async (options) => (await import("@ai-sdk/xai")).createXai(options),
2496
+ "@ai-sdk/mistral": async (options) => (await import("@ai-sdk/mistral")).createMistral(options),
2497
+ "@ai-sdk/groq": async (options) => (await import("@ai-sdk/groq")).createGroq(options),
2498
+ "@ai-sdk/deepinfra": async (options) => (await import("@ai-sdk/deepinfra")).createDeepInfra(options),
2499
+ "@ai-sdk/cerebras": async (options) => (await import("@ai-sdk/cerebras")).createCerebras(options),
2500
+ "@ai-sdk/cohere": async (options) => (await import("@ai-sdk/cohere")).createCohere(options),
2501
+ "@ai-sdk/gateway": async (options) => (await import("@ai-sdk/gateway")).createGateway(options),
2502
+ "@ai-sdk/togetherai": async (options) => (await import("@ai-sdk/togetherai")).createTogetherAI(options),
2503
+ "@ai-sdk/perplexity": async (options) => (await import("@ai-sdk/perplexity")).createPerplexity(options),
2504
+ "@ai-sdk/vercel": async (options) => (await import("@ai-sdk/vercel")).createVercel(options),
2505
+ "@gitlab/gitlab-ai-provider": async (options) =>
2506
+ (await import("@gitlab/gitlab-ai-provider")).createGitLab(options),
2507
+ "@ai-sdk/github-copilot": async (options) =>
2508
+ (await import("./sdk/openai-compatible/src")).createOpenaiCompatible(options),
2509
+ }
2510
+ `,
2511
+ )
2512
+ .replace(
2513
+ " async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string) {\n",
2514
+ " async getModel(sdk: any, modelID: string) {\n",
2515
+ )
2516
+ .replace(
2517
+ ` const loaded = bundledFn({
2518
+ name: model.providerID,
2519
+ ...options,
2520
+ })
2521
+ `,
2522
+ ` const loaded = await bundledFn({
2523
+ name: model.providerID,
2524
+ ...options,
2525
+ })
2526
+ `,
2527
+ ),
2528
+ );
2529
+
2530
+ await writeFile(
2531
+ resolve(sourceRoot, "packages/opencode/src/provider/provider.ts"),
2532
+ `import z from "zod"
2533
+ import { createAnthropic } from "@ai-sdk/anthropic"
2534
+ import { createGoogleGenerativeAI } from "@ai-sdk/google"
2535
+ import { createVertex } from "@ai-sdk/google-vertex"
2536
+ import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
2537
+ import { createGroq } from "@ai-sdk/groq"
2538
+ import { createMistral } from "@ai-sdk/mistral"
2539
+ import { createOpenAI } from "@ai-sdk/openai"
2540
+ import type { LanguageModelV3 } from "@ai-sdk/provider"
2541
+ import { NamedError } from "@opencode-ai/util/error"
2542
+ import { Config } from "../config/config"
2543
+ import { ModelID, ProviderID } from "./schema"
2544
+
2545
+ type ProviderConfig = {
2546
+ name?: string
2547
+ env?: string[]
2548
+ npm?: string
2549
+ api?: string
2550
+ options?: Record<string, any>
2551
+ models?: Record<string, any>
2552
+ }
2553
+
2554
+ type ProviderSeed = {
2555
+ name: string
2556
+ env: string[]
2557
+ npm: string
2558
+ models: Array<{
2559
+ id: string
2560
+ name: string
2561
+ family?: string
2562
+ reasoning?: boolean
2563
+ attachment?: boolean
2564
+ input?: Partial<{ text: boolean; audio: boolean; image: boolean; video: boolean; pdf: boolean }>
2565
+ output?: Partial<{ text: boolean; audio: boolean; image: boolean; video: boolean; pdf: boolean }>
2566
+ limit?: Partial<{ context: number; input: number; output: number }>
2567
+ cost?: Partial<{
2568
+ input: number
2569
+ output: number
2570
+ cache: Partial<{ read: number; write: number }>
2571
+ }>
2572
+ releaseDate?: string
2573
+ }>
2574
+ }
2575
+
2576
+ export namespace Provider {
2577
+ export const Model = z
2578
+ .object({
2579
+ id: ModelID.zod,
2580
+ providerID: ProviderID.zod,
2581
+ api: z.object({
2582
+ id: z.string(),
2583
+ url: z.string(),
2584
+ npm: z.string(),
2585
+ }),
2586
+ name: z.string(),
2587
+ family: z.string().optional(),
2588
+ capabilities: z.object({
2589
+ temperature: z.boolean(),
2590
+ reasoning: z.boolean(),
2591
+ attachment: z.boolean(),
2592
+ toolcall: z.boolean(),
2593
+ input: z.object({
2594
+ text: z.boolean(),
2595
+ audio: z.boolean(),
2596
+ image: z.boolean(),
2597
+ video: z.boolean(),
2598
+ pdf: z.boolean(),
2599
+ }),
2600
+ output: z.object({
2601
+ text: z.boolean(),
2602
+ audio: z.boolean(),
2603
+ image: z.boolean(),
2604
+ video: z.boolean(),
2605
+ pdf: z.boolean(),
2606
+ }),
2607
+ interleaved: z.union([
2608
+ z.boolean(),
2609
+ z.object({
2610
+ field: z.enum(["reasoning_content", "reasoning_details"]),
2611
+ }),
2612
+ ]),
2613
+ }),
2614
+ cost: z.object({
2615
+ input: z.number(),
2616
+ output: z.number(),
2617
+ cache: z.object({
2618
+ read: z.number(),
2619
+ write: z.number(),
2620
+ }),
2621
+ experimentalOver200K: z
2622
+ .object({
2623
+ input: z.number(),
2624
+ output: z.number(),
2625
+ cache: z.object({
2626
+ read: z.number(),
2627
+ write: z.number(),
2628
+ }),
2629
+ })
2630
+ .optional(),
2631
+ }),
2632
+ limit: z.object({
2633
+ context: z.number(),
2634
+ input: z.number().optional(),
2635
+ output: z.number(),
2636
+ }),
2637
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
2638
+ options: z.record(z.string(), z.any()),
2639
+ headers: z.record(z.string(), z.string()),
2640
+ release_date: z.string(),
2641
+ variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
2642
+ })
2643
+ .meta({
2644
+ ref: "Model",
2645
+ })
2646
+ export type Model = z.infer<typeof Model>
2647
+
2648
+ export const Info = z
2649
+ .object({
2650
+ id: ProviderID.zod,
2651
+ name: z.string(),
2652
+ source: z.enum(["env", "config", "custom", "api"]),
2653
+ env: z.string().array(),
2654
+ key: z.string().optional(),
2655
+ options: z.record(z.string(), z.any()),
2656
+ models: z.record(z.string(), Model),
2657
+ })
2658
+ .meta({
2659
+ ref: "Provider",
2660
+ })
2661
+ export type Info = z.infer<typeof Info>
2662
+
2663
+ const DEFAULT_CONTEXT_LIMIT = 200_000
2664
+ const DEFAULT_OUTPUT_LIMIT = 32_000
2665
+
2666
+ const PROVIDER_SEEDS: Record<string, ProviderSeed> = {
2667
+ anthropic: {
2668
+ name: "Anthropic",
2669
+ env: ["ANTHROPIC_API_KEY"],
2670
+ npm: "@ai-sdk/anthropic",
2671
+ models: [
2672
+ {
2673
+ id: "claude-sonnet-4-20250514",
2674
+ name: "Claude Sonnet 4",
2675
+ family: "claude-sonnet-4",
2676
+ reasoning: true,
2677
+ attachment: true,
2678
+ input: { image: true, pdf: true },
2679
+ releaseDate: "2025-05-14",
2680
+ },
2681
+ {
2682
+ id: "claude-opus-4-1-20250805",
2683
+ name: "Claude Opus 4.1",
2684
+ family: "claude-opus-4-1",
2685
+ reasoning: true,
2686
+ attachment: true,
2687
+ input: { image: true, pdf: true },
2688
+ releaseDate: "2025-08-05",
2689
+ },
2690
+ {
2691
+ id: "claude-haiku-4-5-20251001",
2692
+ name: "Claude Haiku 4.5",
2693
+ family: "claude-haiku-4-5",
2694
+ reasoning: false,
2695
+ attachment: true,
2696
+ input: { image: true, pdf: true },
2697
+ limit: { output: 16_000 },
2698
+ releaseDate: "2025-10-01",
2699
+ },
2700
+ ],
2701
+ },
2702
+ openai: {
2703
+ name: "OpenAI",
2704
+ env: ["OPENAI_API_KEY"],
2705
+ npm: "@ai-sdk/openai",
2706
+ models: [
2707
+ {
2708
+ id: "gpt-5",
2709
+ name: "GPT-5",
2710
+ family: "gpt-5",
2711
+ reasoning: true,
2712
+ attachment: true,
2713
+ input: { image: true, pdf: true },
2714
+ releaseDate: "2025-01-01",
2715
+ },
2716
+ {
2717
+ id: "gpt-5-mini",
2718
+ name: "GPT-5 Mini",
2719
+ family: "gpt-5-mini",
2720
+ reasoning: true,
2721
+ attachment: true,
2722
+ input: { image: true, pdf: true },
2723
+ limit: { output: 16_000 },
2724
+ releaseDate: "2025-01-01",
2725
+ },
2726
+ {
2727
+ id: "gpt-5-nano",
2728
+ name: "GPT-5 Nano",
2729
+ family: "gpt-5-nano",
2730
+ reasoning: false,
2731
+ attachment: true,
2732
+ input: { image: true, pdf: true },
2733
+ limit: { output: 8_000 },
2734
+ releaseDate: "2025-01-01",
2735
+ },
2736
+ ],
2737
+ },
2738
+ google: {
2739
+ name: "Google",
2740
+ env: ["GOOGLE_GENERATIVE_AI_API_KEY"],
2741
+ npm: "@ai-sdk/google",
2742
+ models: [
2743
+ {
2744
+ id: "gemini-2.5-pro",
2745
+ name: "Gemini 2.5 Pro",
2746
+ family: "gemini-2.5-pro",
2747
+ reasoning: true,
2748
+ attachment: true,
2749
+ input: { image: true, pdf: true },
2750
+ releaseDate: "2025-01-01",
2751
+ },
2752
+ {
2753
+ id: "gemini-2.5-flash",
2754
+ name: "Gemini 2.5 Flash",
2755
+ family: "gemini-2.5-flash",
2756
+ reasoning: true,
2757
+ attachment: true,
2758
+ input: { image: true, pdf: true },
2759
+ limit: { output: 16_000 },
2760
+ releaseDate: "2025-01-01",
2761
+ },
2762
+ ],
2763
+ },
2764
+ "google-vertex": {
2765
+ name: "Google Vertex",
2766
+ env: [],
2767
+ npm: "@ai-sdk/google-vertex",
2768
+ models: [
2769
+ {
2770
+ id: "gemini-2.5-pro",
2771
+ name: "Gemini 2.5 Pro",
2772
+ family: "gemini-2.5-pro",
2773
+ reasoning: true,
2774
+ attachment: true,
2775
+ input: { image: true, pdf: true },
2776
+ releaseDate: "2025-01-01",
2777
+ },
2778
+ ],
2779
+ },
2780
+ groq: {
2781
+ name: "Groq",
2782
+ env: ["GROQ_API_KEY"],
2783
+ npm: "@ai-sdk/groq",
2784
+ models: [
2785
+ {
2786
+ id: "llama-3.3-70b-versatile",
2787
+ name: "Llama 3.3 70B Versatile",
2788
+ family: "llama-3.3-70b",
2789
+ reasoning: true,
2790
+ releaseDate: "2025-01-01",
2791
+ },
2792
+ ],
2793
+ },
2794
+ mistral: {
2795
+ name: "Mistral",
2796
+ env: ["MISTRAL_API_KEY"],
2797
+ npm: "@ai-sdk/mistral",
2798
+ models: [
2799
+ {
2800
+ id: "mistral-small-latest",
2801
+ name: "Mistral Small Latest",
2802
+ family: "mistral-small",
2803
+ reasoning: true,
2804
+ attachment: true,
2805
+ input: { image: true, pdf: true },
2806
+ releaseDate: "2025-01-01",
2807
+ },
2808
+ ],
2809
+ },
2810
+ }
2811
+
2812
+ const providerCache = new Map<string, any>()
2813
+ const languageCache = new Map<string, LanguageModelV3>()
2814
+ const SDK_FACTORIES: Record<string, (options: Record<string, any>) => any> = {
2815
+ "@ai-sdk/anthropic": createAnthropic,
2816
+ "@ai-sdk/google": createGoogleGenerativeAI,
2817
+ "@ai-sdk/google-vertex": createVertex,
2818
+ "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
2819
+ "@ai-sdk/groq": createGroq,
2820
+ "@ai-sdk/mistral": createMistral,
2821
+ "@ai-sdk/openai": createOpenAI,
2822
+ }
2823
+ const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
2824
+
2825
+ function firstEnv(names: string[]) {
2826
+ for (const name of names) {
2827
+ const value = process.env[name]
2828
+ if (typeof value === "string" && value.length > 0) {
2829
+ return value
2830
+ }
2831
+ }
2832
+ return undefined
2833
+ }
2834
+
2835
+ function cloneRecord<T extends Record<string, any>>(value: T | undefined): T {
2836
+ return { ...(value ?? ({} as T)) }
2837
+ }
2838
+
2839
+ function buildModel(
2840
+ providerID: ProviderID,
2841
+ seed: ProviderSeed,
2842
+ input: {
2843
+ id: string
2844
+ name?: string
2845
+ family?: string
2846
+ reasoning?: boolean
2847
+ attachment?: boolean
2848
+ toolCall?: boolean
2849
+ status?: "alpha" | "beta" | "deprecated" | "active"
2850
+ input?: Partial<{ text: boolean; audio: boolean; image: boolean; video: boolean; pdf: boolean }>
2851
+ output?: Partial<{ text: boolean; audio: boolean; image: boolean; video: boolean; pdf: boolean }>
2852
+ limit?: Partial<{ context: number; input: number; output: number }>
2853
+ cost?: Partial<{
2854
+ input: number
2855
+ output: number
2856
+ cache: Partial<{ read: number; write: number }>
2857
+ }>
2858
+ headers?: Record<string, string>
2859
+ options?: Record<string, any>
2860
+ api?: {
2861
+ id?: string
2862
+ url?: string
2863
+ npm?: string
2864
+ }
2865
+ releaseDate?: string
2866
+ variants?: Record<string, Record<string, any>>
2867
+ },
2868
+ ): Model {
2869
+ return {
2870
+ id: ModelID.make(input.id),
2871
+ providerID,
2872
+ api: {
2873
+ id: input.api?.id ?? input.id,
2874
+ url: input.api?.url ?? "",
2875
+ npm: input.api?.npm ?? seed.npm,
2876
+ },
2877
+ name: input.name ?? input.id,
2878
+ family: input.family,
2879
+ capabilities: {
2880
+ temperature: true,
2881
+ reasoning: input.reasoning ?? false,
2882
+ attachment: input.attachment ?? false,
2883
+ toolcall: input.toolCall ?? true,
2884
+ input: {
2885
+ text: input.input?.text ?? true,
2886
+ audio: input.input?.audio ?? false,
2887
+ image: input.input?.image ?? false,
2888
+ video: input.input?.video ?? false,
2889
+ pdf: input.input?.pdf ?? false,
2890
+ },
2891
+ output: {
2892
+ text: input.output?.text ?? true,
2893
+ audio: input.output?.audio ?? false,
2894
+ image: input.output?.image ?? false,
2895
+ video: input.output?.video ?? false,
2896
+ pdf: input.output?.pdf ?? false,
2897
+ },
2898
+ interleaved: false,
2899
+ },
2900
+ cost: {
2901
+ input: input.cost?.input ?? 0,
2902
+ output: input.cost?.output ?? 0,
2903
+ cache: {
2904
+ read: input.cost?.cache?.read ?? 0,
2905
+ write: input.cost?.cache?.write ?? 0,
2906
+ },
2907
+ },
2908
+ limit: {
2909
+ context: input.limit?.context ?? DEFAULT_CONTEXT_LIMIT,
2910
+ ...(input.limit?.input !== undefined ? { input: input.limit.input } : {}),
2911
+ output: input.limit?.output ?? DEFAULT_OUTPUT_LIMIT,
2912
+ },
2913
+ status: input.status ?? "active",
2914
+ options: cloneRecord(input.options),
2915
+ headers: cloneRecord(input.headers),
2916
+ release_date: input.releaseDate ?? "2025-01-01",
2917
+ variants: input.variants ?? {},
2918
+ }
2919
+ }
2920
+
2921
+ function buildSeedModels(providerID: ProviderID, seed: ProviderSeed) {
2922
+ return Object.fromEntries(
2923
+ seed.models.map((model) => [model.id, buildModel(providerID, seed, model)]),
2924
+ ) as Record<string, Model>
2925
+ }
2926
+
2927
+ function applyConfiguredModels(
2928
+ providerID: ProviderID,
2929
+ seed: ProviderSeed,
2930
+ provider: Info,
2931
+ configuredProvider: ProviderConfig | undefined,
2932
+ ) {
2933
+ for (const [modelID, raw] of Object.entries(configuredProvider?.models ?? {})) {
2934
+ const existing = provider.models[modelID]
2935
+ provider.models[modelID] = buildModel(providerID, seed, {
2936
+ id: modelID,
2937
+ name: raw?.name ?? existing?.name ?? modelID,
2938
+ family: raw?.family ?? existing?.family,
2939
+ reasoning: raw?.reasoning ?? existing?.capabilities.reasoning ?? false,
2940
+ attachment: raw?.attachment ?? existing?.capabilities.attachment ?? false,
2941
+ toolCall: raw?.tool_call ?? existing?.capabilities.toolcall ?? true,
2942
+ status: raw?.status ?? existing?.status ?? "active",
2943
+ input: {
2944
+ text: raw?.modalities?.input?.includes("text") ?? existing?.capabilities.input.text ?? true,
2945
+ audio: raw?.modalities?.input?.includes("audio") ?? existing?.capabilities.input.audio ?? false,
2946
+ image: raw?.modalities?.input?.includes("image") ?? existing?.capabilities.input.image ?? false,
2947
+ video: raw?.modalities?.input?.includes("video") ?? existing?.capabilities.input.video ?? false,
2948
+ pdf: raw?.modalities?.input?.includes("pdf") ?? existing?.capabilities.input.pdf ?? false,
2949
+ },
2950
+ output: {
2951
+ text: raw?.modalities?.output?.includes("text") ?? existing?.capabilities.output.text ?? true,
2952
+ audio: raw?.modalities?.output?.includes("audio") ?? existing?.capabilities.output.audio ?? false,
2953
+ image: raw?.modalities?.output?.includes("image") ?? existing?.capabilities.output.image ?? false,
2954
+ video: raw?.modalities?.output?.includes("video") ?? existing?.capabilities.output.video ?? false,
2955
+ pdf: raw?.modalities?.output?.includes("pdf") ?? existing?.capabilities.output.pdf ?? false,
2956
+ },
2957
+ limit: {
2958
+ context: raw?.limit?.context ?? existing?.limit.context,
2959
+ input: raw?.limit?.input ?? existing?.limit.input,
2960
+ output: raw?.limit?.output ?? existing?.limit.output,
2961
+ },
2962
+ cost: {
2963
+ input: raw?.cost?.input ?? existing?.cost.input,
2964
+ output: raw?.cost?.output ?? existing?.cost.output,
2965
+ cache: {
2966
+ read: raw?.cost?.cache_read ?? existing?.cost.cache.read,
2967
+ write: raw?.cost?.cache_write ?? existing?.cost.cache.write,
2968
+ },
2969
+ },
2970
+ headers: {
2971
+ ...existing?.headers,
2972
+ ...cloneRecord(raw?.headers),
2973
+ },
2974
+ options: {
2975
+ ...existing?.options,
2976
+ ...cloneRecord(raw?.options),
2977
+ },
2978
+ api: {
2979
+ id: raw?.id ?? existing?.api.id ?? modelID,
2980
+ url: raw?.provider?.api ?? configuredProvider?.api ?? existing?.api.url ?? "",
2981
+ npm: raw?.provider?.npm ?? configuredProvider?.npm ?? existing?.api.npm ?? seed.npm,
2982
+ },
2983
+ releaseDate: raw?.release_date ?? existing?.release_date,
2984
+ variants: raw?.variants ?? existing?.variants,
2985
+ })
2986
+ }
2987
+ }
2988
+
2989
+ async function loadProviders() {
2990
+ const cfg = await Config.get()
2991
+ const configured = (cfg.provider ?? {}) as Record<string, ProviderConfig>
2992
+ const providers: Record<string, Info> = {}
2993
+
2994
+ for (const [providerName, seed] of Object.entries(PROVIDER_SEEDS)) {
2995
+ const providerID = ProviderID.make(providerName)
2996
+ const configuredProvider = configured[providerName]
2997
+ const configuredModel = typeof cfg.model === "string" && cfg.model.startsWith(providerName + "/")
2998
+ const key = firstEnv(configuredProvider?.env ?? seed.env) ?? configuredProvider?.options?.apiKey
2999
+
3000
+ if (!configuredProvider && !configuredModel && !key) {
3001
+ continue
3002
+ }
3003
+
3004
+ const info: Info = {
3005
+ id: providerID,
3006
+ name: configuredProvider?.name ?? seed.name,
3007
+ source: configuredProvider ? "config" : key ? "env" : "custom",
3008
+ env: configuredProvider?.env ?? seed.env,
3009
+ ...(typeof key === "string" && key.length > 0 ? { key } : {}),
3010
+ options: {
3011
+ ...cloneRecord(configuredProvider?.options),
3012
+ },
3013
+ models: buildSeedModels(providerID, seed),
3014
+ }
3015
+
3016
+ applyConfiguredModels(providerID, seed, info, configuredProvider)
3017
+ providers[providerID] = info
3018
+ }
3019
+
3020
+ if (Object.keys(providers).length === 0) {
3021
+ const fallback = PROVIDER_SEEDS.anthropic
3022
+ providers[ProviderID.anthropic] = {
3023
+ id: ProviderID.anthropic,
3024
+ name: fallback.name,
3025
+ source: "custom",
3026
+ env: fallback.env,
3027
+ options: {},
3028
+ models: buildSeedModels(ProviderID.anthropic, fallback),
3029
+ }
3030
+ }
3031
+
3032
+ return providers as Record<ProviderID, Info>
3033
+ }
3034
+
3035
+ function modelKey(providerID: ProviderID, modelID: ModelID) {
3036
+ return String(providerID) + "/" + String(modelID)
3037
+ }
3038
+
3039
+ export async function list() {
3040
+ return loadProviders()
3041
+ }
3042
+
3043
+ export async function getProvider(providerID: ProviderID) {
3044
+ const providers = await loadProviders()
3045
+ const provider = providers[providerID]
3046
+ if (!provider) {
3047
+ throw new InitError({ providerID })
3048
+ }
3049
+ return provider
3050
+ }
3051
+
3052
+ export async function getModel(providerID: ProviderID, modelID: ModelID) {
3053
+ const provider = await getProvider(providerID)
3054
+ const model = provider.models[modelID]
3055
+ if (model) return model
3056
+
3057
+ const suggestions = Object.keys(provider.models).filter(
3058
+ (candidate) => candidate.includes(String(modelID)) || String(modelID).includes(candidate),
3059
+ )
3060
+ throw new ModelNotFoundError({
3061
+ providerID,
3062
+ modelID,
3063
+ ...(suggestions.length ? { suggestions: suggestions.slice(0, 3) } : {}),
3064
+ })
3065
+ }
3066
+
3067
+ async function getSdk(provider: Info, model: Model) {
3068
+ const cacheKey = JSON.stringify({
3069
+ providerID: provider.id,
3070
+ apiId: model.api.id,
3071
+ baseURL: provider.options?.baseURL,
3072
+ headers: provider.options?.headers,
3073
+ key: provider.key,
3074
+ })
3075
+ if (providerCache.has(cacheKey)) {
3076
+ return providerCache.get(cacheKey)
3077
+ }
3078
+
3079
+ const options = {
3080
+ ...cloneRecord(provider.options),
3081
+ ...(provider.key ? { apiKey: provider.key } : {}),
3082
+ headers: {
3083
+ ...cloneRecord(provider.options?.headers),
3084
+ ...cloneRecord(model.headers),
3085
+ },
3086
+ }
3087
+
3088
+ const factory = SDK_FACTORIES[model.api.npm]
3089
+ if (!factory) {
3090
+ throw new InitError(
3091
+ { providerID: provider.id },
3092
+ {
3093
+ cause: new Error(
3094
+ "Unsupported provider in ACP VM build: " + provider.id + " (" + model.api.npm + ")",
3095
+ ),
3096
+ },
3097
+ )
3098
+ }
3099
+
3100
+ const sdk = factory(options)
3101
+
3102
+ providerCache.set(cacheKey, sdk)
3103
+ return sdk
3104
+ }
3105
+
3106
+ export async function getLanguage(model: Model) {
3107
+ const key = modelKey(model.providerID, model.id)
3108
+ const cached = languageCache.get(key)
3109
+ if (cached) return cached
3110
+
3111
+ try {
3112
+ const provider = await getProvider(model.providerID)
3113
+ const sdk = await getSdk(provider, model)
3114
+ const language =
3115
+ model.providerID === ProviderID.openai && typeof sdk.responses === "function"
3116
+ ? sdk.responses(model.api.id)
3117
+ : sdk.languageModel(model.api.id)
3118
+ languageCache.set(key, language)
3119
+ return language
3120
+ } catch (cause) {
3121
+ throw new InitError({ providerID: model.providerID }, { cause })
3122
+ }
3123
+ }
3124
+
3125
+ export async function closest(providerID: ProviderID, query: string[]) {
3126
+ const provider = await getProvider(providerID).catch(() => undefined)
3127
+ if (!provider) return undefined
3128
+ for (const item of query) {
3129
+ const match = Object.keys(provider.models).find((modelID) => modelID.includes(item))
3130
+ if (match) return { providerID, modelID: ModelID.make(match) }
3131
+ }
3132
+ return undefined
3133
+ }
3134
+
3135
+ export async function getSmallModel(providerID: ProviderID) {
3136
+ const provider = await getProvider(providerID).catch(() => undefined)
3137
+ if (!provider) return undefined
3138
+
3139
+ const preferred =
3140
+ providerID === ProviderID.anthropic
3141
+ ? ["haiku", "mini", "nano"]
3142
+ : ["mini", "nano", "haiku"]
3143
+
3144
+ for (const token of preferred) {
3145
+ const match = Object.values(provider.models).find((model) => model.id.includes(token))
3146
+ if (match) return match
3147
+ }
3148
+
3149
+ return undefined
3150
+ }
3151
+
3152
+ export async function defaultModel() {
3153
+ const cfg = await Config.get()
3154
+ if (cfg.model) {
3155
+ const parsed = parseModel(cfg.model)
3156
+ const model = await getModel(parsed.providerID, parsed.modelID).catch(() => undefined)
3157
+ if (model) {
3158
+ return {
3159
+ providerID: parsed.providerID,
3160
+ modelID: parsed.modelID,
3161
+ }
3162
+ }
3163
+ }
3164
+
3165
+ const providers = await loadProviders()
3166
+ for (const provider of Object.values(providers)) {
3167
+ const [model] = sort(Object.values(provider.models))
3168
+ if (model) {
3169
+ return {
3170
+ providerID: provider.id,
3171
+ modelID: model.id,
3172
+ }
3173
+ }
3174
+ }
3175
+
3176
+ return {
3177
+ providerID: ProviderID.anthropic,
3178
+ modelID: ModelID.make("claude-sonnet-4-20250514"),
3179
+ }
3180
+ }
3181
+
3182
+ export function sort<T extends { id: string }>(models: T[]) {
3183
+ return [...models].sort((a, b) => {
3184
+ const aPriority = priority.findIndex((item) => a.id.includes(item))
3185
+ const bPriority = priority.findIndex((item) => b.id.includes(item))
3186
+ const aRank = aPriority === -1 ? Number.MAX_SAFE_INTEGER : aPriority
3187
+ const bRank = bPriority === -1 ? Number.MAX_SAFE_INTEGER : bPriority
3188
+ if (aRank !== bRank) return aRank - bRank
3189
+
3190
+ const aLatest = a.id.includes("latest") ? 1 : 0
3191
+ const bLatest = b.id.includes("latest") ? 1 : 0
3192
+ if (aLatest !== bLatest) return aLatest - bLatest
3193
+
3194
+ return a.id.localeCompare(b.id)
3195
+ })
3196
+ }
3197
+
3198
+ export function parseModel(model: string) {
3199
+ const [providerID, ...rest] = model.split("/")
3200
+ return {
3201
+ providerID: ProviderID.make(providerID),
3202
+ modelID: ModelID.make(rest.join("/")),
3203
+ }
3204
+ }
3205
+
3206
+ export const ModelNotFoundError = NamedError.create(
3207
+ "ProviderModelNotFoundError",
3208
+ z.object({
3209
+ providerID: ProviderID.zod,
3210
+ modelID: ModelID.zod,
3211
+ suggestions: z.array(z.string()).optional(),
3212
+ }),
3213
+ )
3214
+
3215
+ export const InitError = NamedError.create(
3216
+ "ProviderInitError",
3217
+ z.object({
3218
+ providerID: ProviderID.zod,
3219
+ }),
3220
+ )
3221
+ }
3222
+ `,
3223
+ );
3224
+
3225
+ await writeFile(
3226
+ resolve(sourceRoot, "packages/opencode/src/plugin/index.ts"),
3227
+ `import { Effect, Layer, ServiceMap } from "effect"
3228
+
3229
+ export namespace Plugin {
3230
+ export interface Interface {
3231
+ readonly trigger: <Name extends string, Input, Output>(
3232
+ name: Name,
3233
+ input: Input,
3234
+ output: Output,
3235
+ ) => Effect.Effect<Output>
3236
+ readonly list: () => Effect.Effect<any[]>
3237
+ readonly init: () => Effect.Effect<void>
3238
+ }
3239
+
3240
+ export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {}
3241
+
3242
+ const noop = Service.of({
3243
+ trigger: <Name extends string, Input, Output>(_name: Name, _input: Input, output: Output) =>
3244
+ Effect.succeed(output),
3245
+ list: () => Effect.succeed([]),
3246
+ init: () => Effect.void,
3247
+ })
3248
+
3249
+ export const layer = Layer.succeed(Service, noop)
3250
+ export const defaultLayer = layer
3251
+
3252
+ export async function trigger<Name extends string, Input, Output>(
3253
+ _name: Name,
3254
+ _input: Input,
3255
+ output: Output,
3256
+ ): Promise<Output> {
3257
+ return output
3258
+ }
3259
+
3260
+ export async function list(): Promise<any[]> {
3261
+ return []
3262
+ }
3263
+
3264
+ export async function init(): Promise<void> {}
3265
+ }
3266
+ `,
3267
+ );
3268
+
3269
+ await writeFile(
3270
+ resolve(sourceRoot, "packages/opencode/src/project/bootstrap.ts"),
3271
+ `import { Instance } from "./instance"
3272
+ import { Log } from "@/util/log"
3273
+
3274
+ export async function InstanceBootstrap() {
3275
+ Log.Default.info("bootstrapping", { directory: Instance.directory })
3276
+ Log.Default.info("bootstrap step", { step: "minimal:init" })
3277
+ }
3278
+ `,
3279
+ );
3280
+
3281
+ await rewriteSourceFile(
3282
+ sourceRoot,
3283
+ "packages/opencode/src/project/instance.ts",
3284
+ (contents) =>
3285
+ contents
3286
+ .replace(
3287
+ `function boot(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
3288
+ return iife(async () => {
3289
+ `,
3290
+ `function boot(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
3291
+ return iife(async () => {
3292
+ Log.Default.info("instance boot:start", { directory: input.directory })
3293
+ `,
3294
+ )
3295
+ .replace(
3296
+ ` await context.provide(ctx, async () => {
3297
+ await input.init?.()
3298
+ })
3299
+ return ctx
3300
+ `,
3301
+ ` Log.Default.info("instance boot:ctx", {
3302
+ directory: ctx.directory,
3303
+ worktree: ctx.worktree,
3304
+ projectID: ctx.project.id,
3305
+ })
3306
+ await context.provide(ctx, async () => {
3307
+ Log.Default.info("instance boot:init:start", { directory: ctx.directory })
3308
+ await input.init?.()
3309
+ Log.Default.info("instance boot:init:done", { directory: ctx.directory })
3310
+ })
3311
+ Log.Default.info("instance boot:done", { directory: ctx.directory })
3312
+ return ctx
3313
+ `,
3314
+ )
3315
+ .replace(
3316
+ ` const ctx = await existing
3317
+ return context.provide(ctx, async () => {
3318
+ return input.fn()
3319
+ })
3320
+ `,
3321
+ ` Log.Default.info("instance provide:await:start", { directory })
3322
+ const ctx = await existing
3323
+ Log.Default.info("instance provide:await:done", { directory })
3324
+ return context.provide(ctx, async () => {
3325
+ Log.Default.info("instance provide:fn:start", { directory })
3326
+ const result = await input.fn()
3327
+ Log.Default.info("instance provide:fn:done", { directory })
3328
+ return result
3329
+ })
3330
+ `,
3331
+ ),
3332
+ );
3333
+
3334
+ await rewriteSourceFile(
3335
+ sourceRoot,
3336
+ "packages/opencode/src/project/project.ts",
3337
+ (contents) =>
3338
+ contents.includes('log.info("phase2 select project"')
3339
+ ? contents
3340
+ : contents
3341
+ .replace(
3342
+ ` // Phase 2: upsert
3343
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
3344
+ `,
3345
+ ` // Phase 2: upsert
3346
+ log.info("phase2 select project", { projectID: data.id })
3347
+ const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
3348
+ log.info("phase2 select project done", { projectID: data.id, found: !!row })
3349
+ `,
3350
+ )
3351
+ .replace(
3352
+ ` yield* db((d) =>
3353
+ d
3354
+ .insert(ProjectTable)
3355
+ .values({
3356
+ `,
3357
+ ` log.info("phase2 upsert project", {
3358
+ projectID: result.id,
3359
+ sandboxes: result.sandboxes.length,
3360
+ })
3361
+ yield* db((d) =>
3362
+ d
3363
+ .insert(ProjectTable)
3364
+ .values({
3365
+ `,
3366
+ )
3367
+ .replace(
3368
+ ` if (data.id !== ProjectID.global) {
3369
+ `,
3370
+ ` log.info("phase2 upsert project done", { projectID: result.id })
3371
+ if (data.id !== ProjectID.global) {
3372
+ `,
3373
+ ),
3374
+ );
3375
+
3376
+ await writeFile(
3377
+ resolve(sourceRoot, "packages/opencode/src/share/share-next.ts"),
3378
+ `export namespace ShareNext {
3379
+ const EMPTY_API = {
3380
+ create: "",
3381
+ sync: () => "",
3382
+ remove: () => "",
3383
+ data: () => "",
3384
+ }
3385
+
3386
+ export async function url() {
3387
+ return ""
3388
+ }
3389
+
3390
+ export async function request() {
3391
+ return {
3392
+ headers: {},
3393
+ api: EMPTY_API,
3394
+ baseUrl: "",
3395
+ }
3396
+ }
3397
+
3398
+ export async function init() {}
3399
+
3400
+ export async function create(_sessionID: string) {
3401
+ return { id: "", url: "", secret: "" }
3402
+ }
3403
+
3404
+ export async function remove(_sessionID: string) {}
3405
+ }
3406
+ `,
3407
+ );
3408
+
3409
+ await writeFile(
3410
+ resolve(sourceRoot, "packages/opencode/src/cli/cmd/tui/win32.ts"),
3411
+ `export function win32DisableProcessedInput() {}
3412
+ export function win32FlushInputBuffer() {}
3413
+ export function win32InstallCtrlCGuard() {
3414
+ return
3415
+ }
3416
+ `,
3417
+ );
3418
+ }
3419
+
3420
+ function patchBuiltBundle(bundlePath) {
3421
+ const original = readFileSync(bundlePath, "utf-8");
3422
+ if (
3423
+ original.includes(
3424
+ "bash tool command scan failed, falling back to raw permission request",
3425
+ )
3426
+ ) {
3427
+ return;
3428
+ }
3429
+
3430
+ const updated = original.replace(
3431
+ ` async execute(params, ctx) {
3432
+ const cwd = params.workdir ? await resolvePath(params.workdir, Instance.directory, shell2) : Instance.directory;
3433
+ if (params.timeout !== undefined && params.timeout < 0) {
3434
+ throw new Error(\`Invalid timeout value: \${params.timeout}. Timeout must be a positive number.\`);
3435
+ }
3436
+ const timeout4 = params.timeout ?? DEFAULT_TIMEOUT;
3437
+ const ps2 = PS.has(name21);
3438
+ const root = await parse10(params.command, ps2);
3439
+ const scan5 = await collect6(root, cwd, ps2, shell2);
3440
+ if (!Instance.containsPath(cwd))
3441
+ scan5.dirs.add(cwd);
3442
+ await ask(ctx, scan5);
3443
+ return run7({
3444
+ `,
3445
+ ` async execute(params, ctx) {
3446
+ const cwd = params.workdir ? await resolvePath(params.workdir, Instance.directory, shell2) : Instance.directory;
3447
+ if (params.timeout !== undefined && params.timeout < 0) {
3448
+ throw new Error(\`Invalid timeout value: \${params.timeout}. Timeout must be a positive number.\`);
3449
+ }
3450
+ const timeout4 = params.timeout ?? DEFAULT_TIMEOUT;
3451
+ const ps2 = PS.has(name21);
3452
+ let scan5;
3453
+ try {
3454
+ const root = await parse10(params.command, ps2);
3455
+ scan5 = await collect6(root, cwd, ps2, shell2);
3456
+ } catch (error48) {
3457
+ log7.warn("bash tool command scan failed, falling back to raw permission request", {
3458
+ command: params.command,
3459
+ error: error48 instanceof Error ? error48.message : String(error48)
3460
+ });
3461
+ scan5 = {
3462
+ dirs: new Set,
3463
+ patterns: new Set([params.command]),
3464
+ always: new Set([params.command])
3465
+ };
3466
+ }
3467
+ if (!Instance.containsPath(cwd))
3468
+ scan5.dirs.add(cwd);
3469
+ await ask(ctx, scan5);
3470
+ return run7({
3471
+ `,
3472
+ );
3473
+
3474
+ if (updated === original) {
3475
+ throw new Error(
3476
+ "Failed to patch built OpenCode ACP bundle for bash scan fallback",
3477
+ );
3478
+ }
3479
+
3480
+ writeFileSync(bundlePath, updated);
3481
+ }
3482
+
3483
+ async function assertPreparedSource(sourceRoot) {
3484
+ const instanceSource = await readFile(
3485
+ resolve(sourceRoot, "packages/opencode/src/server/instance.ts"),
3486
+ "utf8",
3487
+ );
3488
+ if (
3489
+ instanceSource.includes('import { PtyRoutes } from "./routes/pty"') ||
3490
+ instanceSource.includes('import { TuiRoutes } from "./routes/tui"') ||
3491
+ instanceSource.includes('.route("/pty", PtyRoutes())') ||
3492
+ instanceSource.includes('.route("/tui", TuiRoutes())')
3493
+ ) {
3494
+ throw new Error("Prepared OpenCode source still exposes PTY/TUI routes in the ACP build");
3495
+ }
3496
+
3497
+ const shellSource = await readFile(
3498
+ resolve(sourceRoot, "packages/opencode/src/shell/shell.ts"),
3499
+ "utf8",
3500
+ );
3501
+ if (shellSource.includes("Bun.which(")) {
3502
+ throw new Error("Prepared OpenCode source still references Bun.which in shell.ts");
3503
+ }
3504
+
3505
+ const win32Source = await readFile(
3506
+ resolve(sourceRoot, "packages/opencode/src/cli/cmd/tui/win32.ts"),
3507
+ "utf8",
3508
+ );
3509
+ if (win32Source.includes("bun:ffi")) {
3510
+ throw new Error("Prepared OpenCode source still references bun:ffi in the Win32 TUI shim");
3511
+ }
3512
+
3513
+ const dbNodeSource = await readFile(
3514
+ resolve(sourceRoot, "packages/opencode/src/storage/db.node.ts"),
3515
+ "utf8",
3516
+ );
3517
+ if (
3518
+ !dbNodeSource.includes('from "drizzle-orm/node-sqlite"') ||
3519
+ !dbNodeSource.includes('from "node:sqlite"')
3520
+ ) {
3521
+ throw new Error("Prepared OpenCode source does not use the native node:sqlite database path");
3522
+ }
3523
+ }
3524
+
3525
+ async function assertBundleClean(bundlePath) {
3526
+ const bundle = await readFile(bundlePath, "utf8");
3527
+ for (const pattern of [
3528
+ "bun:ffi",
3529
+ "bun-pty",
3530
+ "hono/bun",
3531
+ '.route("/pty", PtyRoutes())',
3532
+ '.route("/tui", TuiRoutes())',
3533
+ "Bun.which(",
3534
+ "bun:sqlite",
3535
+ ]) {
3536
+ if (bundle.includes(pattern)) {
3537
+ throw new Error(
3538
+ `OpenCode ACP bundle still contains forbidden runtime dependency: ${pattern}`,
3539
+ );
3540
+ }
3541
+ }
3542
+ }
3543
+
3544
+ async function main() {
3545
+ if (!existsSync(bunBin)) {
3546
+ throw new Error(
3547
+ `bun is not installed for @agentos-software/opencode (expected ${bunBin}). Run pnpm install first.`,
3548
+ );
3549
+ }
3550
+
3551
+ mkdirSync(distDir, { recursive: true });
3552
+ mkdirSync(cacheDir, { recursive: true });
3553
+ rmSync(bundleDir, { recursive: true, force: true });
3554
+
3555
+ const patch = readFileSync(patchPath, "utf-8");
3556
+ const buildScript = readFileSync(fileURLToPath(import.meta.url), "utf-8");
3557
+ const patchHash = createHash("sha256")
3558
+ .update(`${SOURCE_VERSION}\n${patch}\n${buildScript}`)
3559
+ .digest("hex")
3560
+ .slice(0, 16);
3561
+ const sourceRoot = join(cacheDir, `source-v${SOURCE_VERSION}-${patchHash}`);
3562
+ const preparedMarker = join(sourceRoot, ".agentos-prepared.json");
3563
+ const tarballPath = join(cacheDir, `opencode-v${SOURCE_VERSION}.tar.gz`);
3564
+
3565
+ if (!existsSync(preparedMarker)) {
3566
+ rmSync(sourceRoot, { recursive: true, force: true });
3567
+ mkdirSync(sourceRoot, { recursive: true });
3568
+
3569
+ if (!existsSync(tarballPath)) {
3570
+ process.stdout.write(`Downloading OpenCode v${SOURCE_VERSION} source...\n`);
3571
+ await downloadFile(SOURCE_TARBALL_URL, tarballPath);
3572
+ }
3573
+
3574
+ run("tar", ["-xzf", tarballPath, "--strip-components=1", "-C", sourceRoot]);
3575
+ pinGhosttyWebRef(sourceRoot);
3576
+ run(bunBin, ["install", "--frozen-lockfile"], { cwd: sourceRoot });
3577
+ await ensureNodeAcpPatch(sourceRoot, tarballPath);
3578
+ await applyNodeAcpRuntimeTweaks(sourceRoot);
3579
+ await assertPreparedSource(sourceRoot);
3580
+
3581
+ writeFileSync(
3582
+ preparedMarker,
3583
+ JSON.stringify(
3584
+ {
3585
+ sourceVersion: SOURCE_VERSION,
3586
+ sourceRepository: SOURCE_REPOSITORY,
3587
+ patchHash,
3588
+ },
3589
+ null,
3590
+ 2,
3591
+ ) + "\n",
3592
+ );
3593
+ }
3594
+
3595
+ await ensureNodeAcpPatch(sourceRoot, tarballPath);
3596
+ await applyNodeAcpRuntimeTweaks(sourceRoot);
3597
+ await assertPreparedSource(sourceRoot);
3598
+
3599
+ const migrations = await readMigrations(sourceRoot);
3600
+ const buildHelperDir = await mkdtemp(join(tmpdir(), "agentos-opencode-build-"));
3601
+ const buildHelperPath = join(buildHelperDir, "build-opencode-acp.mjs");
3602
+ const bunVersion =
3603
+ spawnSync(bunBin, ["--version"], { encoding: "utf-8" }).stdout?.trim() ??
3604
+ "unknown";
3605
+
3606
+ try {
3607
+ await writeFile(
3608
+ buildHelperPath,
3609
+ `
3610
+ import { mkdir } from "node:fs/promises";
3611
+ import { dirname, join } from "node:path";
3612
+
3613
+ const outdir = process.env.OUTDIR;
3614
+ if (!outdir) {
3615
+ throw new Error("OUTDIR is required");
3616
+ }
3617
+
3618
+ const result = await Bun.build({
3619
+ target: "node",
3620
+ format: "esm",
3621
+ outdir,
3622
+ entrypoints: ["./packages/opencode/src/cli/cmd/acp.ts"],
3623
+ define: {
3624
+ OPENCODE_MIGRATIONS: ${JSON.stringify(JSON.stringify(migrations))},
3625
+ OPENCODE_LIBC: ${JSON.stringify(JSON.stringify("glibc"))},
3626
+ },
3627
+ });
3628
+ if (!result.success) {
3629
+ for (const log of result.logs) {
3630
+ console.error(log);
3631
+ }
3632
+ throw new Error("OpenCode ACP bundle build failed");
3633
+ }
3634
+ for (const output of result.outputs) {
3635
+ const filePath = join(outdir, output.path);
3636
+ await mkdir(dirname(filePath), { recursive: true });
3637
+ await Bun.write(filePath, output);
3638
+ }
3639
+ `,
3640
+ );
3641
+
3642
+ run(bunBin, [buildHelperPath], {
3643
+ cwd: sourceRoot,
3644
+ env: {
3645
+ ...process.env,
3646
+ OUTDIR: bundleDir,
3647
+ },
3648
+ });
3649
+ } finally {
3650
+ rmSync(buildHelperDir, { recursive: true, force: true });
3651
+ }
3652
+
3653
+ await assertBundleClean(join(bundleDir, "acp.js"));
3654
+ patchBuiltBundle(join(bundleDir, "acp.js"));
3655
+
3656
+ writeFileSync(
3657
+ manifestPath,
3658
+ JSON.stringify(
3659
+ {
3660
+ source: {
3661
+ repository: SOURCE_REPOSITORY,
3662
+ version: SOURCE_VERSION,
3663
+ tarballUrl: SOURCE_TARBALL_URL,
3664
+ },
3665
+ build: {
3666
+ bunVersion,
3667
+ patchHash,
3668
+ externalDependencies: [],
3669
+ entry: "./opencode-acp/acp.js",
3670
+ },
3671
+ },
3672
+ null,
3673
+ 2,
3674
+ ) + "\n",
3675
+ );
3676
+ }
3677
+
3678
+ void main().catch((error) => {
3679
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
3680
+ process.exitCode = 1;
3681
+ });