@ai-sdk/harness-opencode 0.0.0-6b196531-20260710185421

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1002 @@
1
+ // src/opencode-harness.ts
2
+ import { randomBytes } from "crypto";
3
+ import { readFile } from "fs/promises";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import {
7
+ commonTool,
8
+ HarnessCapabilityUnsupportedError,
9
+ harnessV1DiagnosticFromBridgeFrame
10
+ } from "@ai-sdk/harness";
11
+ import {
12
+ classifyDiskLog,
13
+ createBridgeErrorHandler,
14
+ createBridgeStartupError,
15
+ drainBridgeProcessStream,
16
+ forwardBridgeProcessStream,
17
+ markBridgeStarting,
18
+ resolveSandboxHomeDir,
19
+ SandboxChannel,
20
+ shellQuote,
21
+ waitForBridgeReady,
22
+ writeSkills as writeHarnessSkills
23
+ } from "@ai-sdk/harness/utils";
24
+ import {
25
+ tool
26
+ } from "@ai-sdk/provider-utils";
27
+ import { WebSocket } from "ws";
28
+ import { z as z2 } from "zod/v4";
29
+
30
+ // src/opencode-auth.ts
31
+ import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
32
+ function resolveOpenCodeProvider({
33
+ model,
34
+ provider
35
+ }) {
36
+ if (provider === "anthropic" || provider === "openai") {
37
+ return provider;
38
+ }
39
+ if (model?.includes("/")) {
40
+ const [modelProvider] = model.split("/");
41
+ if (modelProvider === "anthropic" || modelProvider === "openai") {
42
+ return modelProvider;
43
+ }
44
+ }
45
+ return "anthropic";
46
+ }
47
+ function splitOpenCodeModel(model, provider) {
48
+ if (!model) return {};
49
+ if (model.includes("/")) {
50
+ const [providerID, ...rest] = model.split("/");
51
+ return {
52
+ providerID,
53
+ modelID: rest.join("/"),
54
+ model
55
+ };
56
+ }
57
+ return {
58
+ providerID: provider,
59
+ modelID: model,
60
+ model: provider ? `${provider}/${model}` : model
61
+ };
62
+ }
63
+ function resolveOpenCodeEnv({
64
+ auth,
65
+ model,
66
+ provider,
67
+ processEnv = process.env
68
+ }) {
69
+ const selectedProvider = resolveOpenCodeProvider({ model, provider });
70
+ if (auth?.openaiCompatible) {
71
+ return pickOpenAICompatible(auth.openaiCompatible, processEnv);
72
+ }
73
+ if (selectedProvider === "openai") {
74
+ if (auth?.openai) {
75
+ return pickOpenAI({ explicit: auth.openai, processEnv });
76
+ }
77
+ } else if (auth?.anthropic) {
78
+ return pickAnthropic({ explicit: auth.anthropic, processEnv });
79
+ }
80
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: processEnv });
81
+ if (auth?.gateway) {
82
+ return pickGateway({ explicit: auth.gateway, gatewayAuthFromEnv });
83
+ }
84
+ if (gatewayAuthFromEnv.apiKey) {
85
+ return pickGateway({ explicit: {}, gatewayAuthFromEnv });
86
+ }
87
+ return selectedProvider === "openai" ? pickOpenAI({ processEnv }) : pickAnthropic({ processEnv });
88
+ }
89
+ function pickOpenAICompatible(explicit, processEnv) {
90
+ const env = {};
91
+ const apiKey = explicit.apiKey ?? processEnv.OPENAI_API_KEY;
92
+ if (apiKey) env.OPENAI_API_KEY = apiKey;
93
+ const baseUrl = explicit.baseUrl ?? processEnv.OPENAI_BASE_URL;
94
+ if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
95
+ const name = explicit.name ?? processEnv.OPENAI_NAME;
96
+ if (name) env.OPENAI_NAME = name;
97
+ if (explicit.queryParams && Object.keys(explicit.queryParams).length > 0) {
98
+ env.OPENAI_QUERY_PARAMS_JSON = JSON.stringify(explicit.queryParams);
99
+ } else if (processEnv.OPENAI_QUERY_PARAMS_JSON) {
100
+ env.OPENAI_QUERY_PARAMS_JSON = processEnv.OPENAI_QUERY_PARAMS_JSON;
101
+ }
102
+ return env;
103
+ }
104
+ function pickOpenAI({
105
+ explicit,
106
+ processEnv
107
+ }) {
108
+ const env = {};
109
+ const apiKey = explicit?.apiKey ?? processEnv.OPENAI_API_KEY;
110
+ if (apiKey) env.OPENAI_API_KEY = apiKey;
111
+ const baseUrl = explicit?.baseUrl ?? processEnv.OPENAI_BASE_URL;
112
+ if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
113
+ const organization = explicit?.organization ?? processEnv.OPENAI_ORGANIZATION;
114
+ if (organization) env.OPENAI_ORGANIZATION = organization;
115
+ const project = explicit?.project ?? processEnv.OPENAI_PROJECT;
116
+ if (project) env.OPENAI_PROJECT = project;
117
+ return env;
118
+ }
119
+ function pickAnthropic({
120
+ explicit,
121
+ processEnv
122
+ }) {
123
+ const env = {};
124
+ const apiKey = explicit?.apiKey ?? processEnv.ANTHROPIC_API_KEY;
125
+ if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
126
+ const authToken = explicit?.authToken ?? processEnv.ANTHROPIC_AUTH_TOKEN;
127
+ if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
128
+ const baseUrl = explicit?.baseUrl ?? processEnv.ANTHROPIC_BASE_URL;
129
+ if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
130
+ return env;
131
+ }
132
+ function pickGateway({
133
+ explicit,
134
+ gatewayAuthFromEnv
135
+ }) {
136
+ const env = {};
137
+ const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey;
138
+ if (apiKey) env.AI_GATEWAY_API_KEY = apiKey;
139
+ env.AI_GATEWAY_BASE_URL = toOpenCodeGatewayBaseUrl(
140
+ explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl
141
+ );
142
+ return env;
143
+ }
144
+ function toOpenCodeGatewayBaseUrl(baseUrl) {
145
+ const trimmed = baseUrl.replace(/\/+$/, "");
146
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
147
+ }
148
+
149
+ // src/opencode-bridge-protocol.ts
150
+ import {
151
+ harnessV1BridgeInboundCommandSchemas,
152
+ harnessV1BridgeOutboundMessageSchema,
153
+ harnessV1BridgeReadySchema,
154
+ harnessV1BridgeStartBaseSchema
155
+ } from "@ai-sdk/harness";
156
+ import { z } from "zod/v4";
157
+ var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
158
+ var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
159
+ operation: z.enum(["prompt", "compact"]).optional(),
160
+ provider: z.string().optional(),
161
+ variant: z.string().optional(),
162
+ instructions: z.string().optional(),
163
+ resumeSessionId: z.string().optional()
164
+ });
165
+ var inboundMessageSchema = z.discriminatedUnion("type", [
166
+ startMessageSchema,
167
+ ...harnessV1BridgeInboundCommandSchemas
168
+ ]);
169
+
170
+ // src/version.ts
171
+ var VERSION = true ? "0.0.0-6b196531-20260710185421" : "0.0.0-test";
172
+
173
+ // src/opencode-harness.ts
174
+ var OPENCODE_CLIENT_APP = `ai-sdk/harness-opencode/${VERSION}`;
175
+ var optionalStringRecord = z2.record(z2.string(), z2.unknown()).optional();
176
+ var OPENCODE_BUILTIN_TOOLS = {
177
+ read: commonTool("read", {
178
+ nativeName: "view",
179
+ toolUseKind: "readonly",
180
+ description: "Read file contents",
181
+ inputSchema: z2.looseObject({
182
+ file_path: z2.string().optional(),
183
+ path: z2.string().optional()
184
+ })
185
+ }),
186
+ write: commonTool("write", {
187
+ nativeName: "write",
188
+ toolUseKind: "edit",
189
+ description: "Write content to a file",
190
+ inputSchema: z2.looseObject({
191
+ file_path: z2.string().optional(),
192
+ path: z2.string().optional(),
193
+ content: z2.string().optional()
194
+ })
195
+ }),
196
+ edit: commonTool("edit", {
197
+ nativeName: "edit",
198
+ toolUseKind: "edit",
199
+ description: "Edit a file by replacing text",
200
+ inputSchema: z2.looseObject({
201
+ file_path: z2.string().optional(),
202
+ path: z2.string().optional(),
203
+ old_string: z2.string().optional(),
204
+ new_string: z2.string().optional()
205
+ })
206
+ }),
207
+ bash: commonTool("bash", {
208
+ nativeName: "bash",
209
+ toolUseKind: "bash",
210
+ description: "Execute a shell command",
211
+ inputSchema: z2.looseObject({
212
+ command: z2.string().optional()
213
+ })
214
+ }),
215
+ glob: commonTool("glob", {
216
+ nativeName: "glob",
217
+ toolUseKind: "readonly",
218
+ description: "Find files matching a glob pattern",
219
+ inputSchema: z2.looseObject({
220
+ pattern: z2.string().optional(),
221
+ path: z2.string().optional()
222
+ })
223
+ }),
224
+ grep: commonTool("grep", {
225
+ nativeName: "grep",
226
+ toolUseKind: "readonly",
227
+ description: "Search file contents with regex",
228
+ inputSchema: z2.looseObject({
229
+ pattern: z2.string().optional(),
230
+ path: z2.string().optional()
231
+ })
232
+ }),
233
+ ls: tool({
234
+ description: "List directory contents",
235
+ inputSchema: z2.looseObject({
236
+ path: z2.string().optional()
237
+ })
238
+ }),
239
+ webfetch: tool({
240
+ description: "Fetch a URL",
241
+ inputSchema: z2.looseObject({
242
+ url: z2.string().optional(),
243
+ prompt: z2.string().optional()
244
+ })
245
+ }),
246
+ skill: tool({
247
+ description: "Load an OpenCode skill by name",
248
+ inputSchema: z2.looseObject({
249
+ name: z2.string().optional()
250
+ })
251
+ }),
252
+ todowrite: tool({
253
+ description: "Replace the OpenCode session todo list",
254
+ inputSchema: z2.looseObject({
255
+ todos: z2.array(
256
+ z2.looseObject({
257
+ content: z2.string().optional(),
258
+ status: z2.string().optional(),
259
+ priority: z2.string().optional()
260
+ })
261
+ ).optional()
262
+ })
263
+ }),
264
+ agent: tool({
265
+ description: "Run an OpenCode subagent",
266
+ inputSchema: z2.looseObject({
267
+ agent: z2.string().optional(),
268
+ prompt: z2.string().optional(),
269
+ description: z2.string().optional(),
270
+ metadata: optionalStringRecord
271
+ })
272
+ })
273
+ };
274
+ var BOOTSTRAP_DIR = "/tmp/harness/opencode";
275
+ var openCodeBridgeCoordsSchema = z2.object({
276
+ port: z2.number(),
277
+ token: z2.string(),
278
+ lastSeenEventId: z2.number(),
279
+ sandboxId: z2.string().optional()
280
+ });
281
+ var openCodeResumeStateSchema = z2.object({
282
+ openCodeSessionId: z2.string().optional(),
283
+ bridge: openCodeBridgeCoordsSchema.optional()
284
+ });
285
+ function createOpenCode(settings = {}) {
286
+ let cachedBootstrap;
287
+ return {
288
+ specificationVersion: "harness-v1",
289
+ harnessId: "opencode",
290
+ builtinTools: OPENCODE_BUILTIN_TOOLS,
291
+ supportsBuiltinToolApprovals: true,
292
+ lifecycleStateSchema: openCodeResumeStateSchema,
293
+ getBootstrap: async () => {
294
+ if (cachedBootstrap != null) return cachedBootstrap;
295
+ const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
296
+ readBridgeAsset("package.json"),
297
+ readBridgeAsset("pnpm-lock.yaml"),
298
+ readBridgeAsset("index.mjs"),
299
+ readBridgeAsset("host-tool-mcp.mjs")
300
+ ]);
301
+ cachedBootstrap = {
302
+ harnessId: "opencode",
303
+ bootstrapDir: BOOTSTRAP_DIR,
304
+ files: [
305
+ { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
306
+ { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
307
+ { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
308
+ {
309
+ path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
310
+ content: hostToolMcp
311
+ }
312
+ ],
313
+ commands: [
314
+ { command: `mkdir -p ${BOOTSTRAP_DIR}` },
315
+ {
316
+ command: `pnpm --dir ${BOOTSTRAP_DIR} install --frozen-lockfile --store-dir ${BOOTSTRAP_DIR}/.pnpm-store`
317
+ },
318
+ {
319
+ command: `cd ${BOOTSTRAP_DIR} && node node_modules/opencode-ai/postinstall.mjs && ./node_modules/.bin/opencode --version`
320
+ }
321
+ ]
322
+ };
323
+ return cachedBootstrap;
324
+ },
325
+ doStart: async (startOpts) => {
326
+ const sandboxSession = startOpts.sandboxSession;
327
+ const session = sandboxSession.restricted();
328
+ const sandboxId = sandboxSession.id;
329
+ const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
330
+ const isResume = lifecycleState != null;
331
+ const isContinue = startOpts.continueFrom != null;
332
+ const resumeData = isResume && typeof lifecycleState?.data === "object" ? lifecycleState.data : void 0;
333
+ const resumeSessionId = typeof resumeData?.openCodeSessionId === "string" && resumeData.openCodeSessionId.length > 0 ? resumeData.openCodeSessionId : void 0;
334
+ const coords = resumeData?.bridge;
335
+ const workDir = startOpts.sessionWorkDir;
336
+ const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
337
+ const bridgeStateDir = `${sessionDataDir}/bridge`;
338
+ const timeoutMs = settings.startupTimeoutMs ?? 12e4;
339
+ const model = splitOpenCodeModel(settings.model, settings.provider).model;
340
+ const report = startOpts.observability?.report;
341
+ const onDiagnostic = report ? (frame) => report(
342
+ harnessV1DiagnosticFromBridgeFrame(frame, {
343
+ sessionId: startOpts.sessionId,
344
+ timestamp: Date.now()
345
+ })
346
+ ) : void 0;
347
+ const onBridgeError = createBridgeErrorHandler({
348
+ harnessId: "opencode",
349
+ sessionId: startOpts.sessionId
350
+ });
351
+ if (coords) {
352
+ try {
353
+ const attachUrl = await sandboxSession.getPortUrl({
354
+ port: coords.port,
355
+ protocol: "ws"
356
+ }) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
357
+ const attachChannel = new SandboxChannel({
358
+ connect: () => openWebSocket(attachUrl),
359
+ outboundSchema: outboundMessageSchema,
360
+ initialLastSeenEventId: coords.lastSeenEventId,
361
+ onDiagnostic,
362
+ onBridgeError
363
+ });
364
+ await attachChannel.open(isContinue ? { resume: true } : void 0);
365
+ return createSession({
366
+ sessionId: startOpts.sessionId,
367
+ channel: attachChannel,
368
+ proc: void 0,
369
+ model,
370
+ provider: settings.provider,
371
+ reasoningVariant: settings.reasoningVariant,
372
+ openCodeSessionId: resumeSessionId,
373
+ isResume: true,
374
+ seedResumeSessionOnFirstPrompt: false,
375
+ rerunContinue: false,
376
+ bridgePort: coords.port,
377
+ bridgeToken: coords.token,
378
+ sandboxId,
379
+ debug: startOpts.observability?.debug,
380
+ permissionMode: startOpts.permissionMode,
381
+ builtinToolFiltering: startOpts.builtinToolFiltering
382
+ });
383
+ } catch {
384
+ }
385
+ }
386
+ let respawnStrategy = isResume ? "rerun" : void 0;
387
+ if (coords && isContinue) {
388
+ const logRaw = await Promise.resolve(
389
+ session.readTextFile({
390
+ path: `${bridgeStateDir}/event-log.ndjson`,
391
+ abortSignal: startOpts.abortSignal
392
+ })
393
+ ).catch(() => null);
394
+ if (await classifyDiskLog(logRaw) === "replay") {
395
+ respawnStrategy = "replay";
396
+ }
397
+ }
398
+ const port = resolveBridgePort(sandboxSession, settings.port);
399
+ const token = randomBytes(32).toString("hex");
400
+ const sandboxHomeDir = await resolveSandboxHomeDir({
401
+ sandbox: session,
402
+ abortSignal: startOpts.abortSignal
403
+ });
404
+ const xdgConfigHome = `${sandboxHomeDir}/.config`;
405
+ const xdgCacheHome = `${sandboxHomeDir}/.cache`;
406
+ const xdgDataHome = `${sandboxHomeDir}/.local/share`;
407
+ const xdgStateHome = `${sandboxHomeDir}/.local/state`;
408
+ const skillSetup = startOpts.skills && startOpts.skills.length > 0 ? await writeOpenCodeSkills({
409
+ sandbox: session,
410
+ skills: startOpts.skills,
411
+ homeDir: sandboxHomeDir,
412
+ abortSignal: startOpts.abortSignal
413
+ }) : void 0;
414
+ const env = {
415
+ ...resolveOpenCodeEnv({
416
+ auth: settings.auth,
417
+ model: settings.model,
418
+ provider: settings.provider
419
+ }),
420
+ AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
421
+ BRIDGE_CHANNEL_TOKEN: token,
422
+ BRIDGE_WS_PORT: String(port),
423
+ HOME: sandboxHomeDir,
424
+ USERPROFILE: sandboxHomeDir,
425
+ XDG_CONFIG_HOME: xdgConfigHome,
426
+ XDG_CACHE_HOME: xdgCacheHome,
427
+ XDG_DATA_HOME: xdgDataHome,
428
+ XDG_STATE_HOME: xdgStateHome,
429
+ ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
430
+ };
431
+ if (respawnStrategy === void 0) {
432
+ await session.run({
433
+ command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)} ${shellQuote(xdgConfigHome)} ${shellQuote(xdgCacheHome)} ${shellQuote(xdgDataHome)} ${shellQuote(xdgStateHome)}`,
434
+ abortSignal: startOpts.abortSignal
435
+ });
436
+ }
437
+ await markBridgeStarting({
438
+ sandbox: session,
439
+ bridgeStateDir,
440
+ bridgeType: "opencode",
441
+ abortSignal: startOpts.abortSignal
442
+ });
443
+ const proc = await session.spawn({
444
+ command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)}${skillSetup ? ` --skills-dir ${shellQuote(skillSetup.skillsDir)}` : ""}`,
445
+ env,
446
+ abortSignal: startOpts.abortSignal
447
+ });
448
+ const stderrTail = [];
449
+ const bridgeStderrDone = forwardBridgeProcessStream({
450
+ stream: proc.stderr,
451
+ streamName: "stderr",
452
+ source: "opencode",
453
+ collectTail: stderrTail
454
+ });
455
+ const { port: boundPort } = await waitForBridgeReady({
456
+ proc,
457
+ sandbox: session,
458
+ bridgeStateDir,
459
+ bridgeType: "opencode",
460
+ timeoutMs,
461
+ abortSignal: startOpts.abortSignal,
462
+ createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
463
+ message: "OpenCode bridge did not become ready in time.",
464
+ proc: proc2,
465
+ stdoutTail,
466
+ stderrTail,
467
+ stderrDone: bridgeStderrDone
468
+ }),
469
+ createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
470
+ message: "OpenCode bridge exited before becoming ready.",
471
+ proc: proc2,
472
+ stdoutTail,
473
+ stderrTail,
474
+ stderrDone: bridgeStderrDone
475
+ })
476
+ });
477
+ void drainBridgeProcessStream(proc.stdout);
478
+ const wsUrl = await sandboxSession.getPortUrl({
479
+ port: boundPort,
480
+ protocol: "ws"
481
+ }) + `?agent_bridge_token=${encodeURIComponent(token)}`;
482
+ const channel = new SandboxChannel({
483
+ connect: () => openWebSocket(wsUrl),
484
+ outboundSchema: outboundMessageSchema,
485
+ onDiagnostic,
486
+ onBridgeError,
487
+ ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
488
+ });
489
+ await channel.open(
490
+ respawnStrategy === "replay" ? { resume: true } : void 0
491
+ );
492
+ return createSession({
493
+ sessionId: startOpts.sessionId,
494
+ channel,
495
+ proc,
496
+ model,
497
+ provider: settings.provider,
498
+ reasoningVariant: settings.reasoningVariant,
499
+ openCodeSessionId: resumeSessionId,
500
+ isResume: respawnStrategy !== void 0,
501
+ seedResumeSessionOnFirstPrompt: respawnStrategy !== void 0,
502
+ rerunContinue: respawnStrategy === "rerun",
503
+ bridgePort: boundPort,
504
+ bridgeToken: token,
505
+ sandboxId,
506
+ debug: startOpts.observability?.debug,
507
+ permissionMode: startOpts.permissionMode,
508
+ builtinToolFiltering: startOpts.builtinToolFiltering
509
+ });
510
+ }
511
+ };
512
+ }
513
+ function resolveBridgePort(sandboxSession, override) {
514
+ if (override !== void 0) return override;
515
+ if (sandboxSession.ports.length > 0) return sandboxSession.ports[0];
516
+ throw new HarnessCapabilityUnsupportedError({
517
+ harnessId: "opencode",
518
+ message: "The OpenCode harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createOpenCode({ port })`."
519
+ });
520
+ }
521
+ async function readBridgeAsset(name) {
522
+ const candidates = [
523
+ new URL(`./bridge/${name}`, import.meta.url),
524
+ new URL(`../bridge/${name}`, import.meta.url)
525
+ ];
526
+ let lastErr;
527
+ for (const url of candidates) {
528
+ try {
529
+ return await readFile(fileURLToPath(url), "utf8");
530
+ } catch (err) {
531
+ const code = err.code;
532
+ if (code !== "ENOENT") throw err;
533
+ lastErr = err;
534
+ }
535
+ }
536
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
537
+ }
538
+ async function writeOpenCodeSkills({
539
+ sandbox,
540
+ skills,
541
+ homeDir,
542
+ abortSignal
543
+ }) {
544
+ const skillsDir = path.posix.join(homeDir, ".agents", "skills");
545
+ await writeHarnessSkills({
546
+ sandbox,
547
+ rootDir: skillsDir,
548
+ skills,
549
+ abortSignal,
550
+ invalidSkillNameMessage: ({ name }) => `Invalid OpenCode skill name: ${name}`,
551
+ invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid OpenCode skill file path for ${skillName}: ${filePath}`
552
+ });
553
+ return { skillsDir };
554
+ }
555
+ function openWebSocket(url) {
556
+ return new Promise((resolve, reject) => {
557
+ const ws = new WebSocket(url);
558
+ const onOpen = () => {
559
+ ws.off("error", onError);
560
+ resolve(ws);
561
+ };
562
+ const onError = (err) => {
563
+ ws.off("open", onOpen);
564
+ reject(err);
565
+ };
566
+ ws.once("open", onOpen);
567
+ ws.once("error", onError);
568
+ });
569
+ }
570
+ function createSession({
571
+ sessionId,
572
+ channel,
573
+ proc,
574
+ model,
575
+ provider,
576
+ reasoningVariant,
577
+ openCodeSessionId,
578
+ isResume,
579
+ seedResumeSessionOnFirstPrompt,
580
+ rerunContinue,
581
+ bridgePort,
582
+ bridgeToken,
583
+ sandboxId,
584
+ debug,
585
+ permissionMode,
586
+ builtinToolFiltering
587
+ }) {
588
+ let stopped = false;
589
+ let stopPromise;
590
+ let latestOpenCodeSessionId = openCodeSessionId;
591
+ let pendingResumeSessionId = seedResumeSessionOnFirstPrompt ? openCodeSessionId : void 0;
592
+ let instructionsApplied = isResume;
593
+ let activeTurn = false;
594
+ const pendingCompactionParts = [];
595
+ channel.on("bridge-thread", (msg) => {
596
+ latestOpenCodeSessionId = msg.threadId;
597
+ });
598
+ const wireTurn = (turnOpts) => {
599
+ activeTurn = true;
600
+ let pendingResolve;
601
+ let pendingReject;
602
+ const done = new Promise((resolve, reject) => {
603
+ pendingResolve = resolve;
604
+ pendingReject = reject;
605
+ });
606
+ const unsubs = [];
607
+ const forward = (event) => {
608
+ try {
609
+ turnOpts.emit(event);
610
+ } catch {
611
+ }
612
+ };
613
+ const eventTypes = [
614
+ "stream-start",
615
+ "text-start",
616
+ "text-delta",
617
+ "text-end",
618
+ "reasoning-start",
619
+ "reasoning-delta",
620
+ "reasoning-end",
621
+ "tool-call",
622
+ "tool-approval-request",
623
+ "tool-result",
624
+ "file-change",
625
+ "finish-step",
626
+ "compaction",
627
+ "raw"
628
+ ];
629
+ let isSettled = false;
630
+ const settleSuccess = () => {
631
+ if (isSettled) return;
632
+ isSettled = true;
633
+ activeTurn = false;
634
+ for (const u of unsubs) u();
635
+ pendingResolve();
636
+ };
637
+ const settleError = (err) => {
638
+ if (isSettled) return;
639
+ isSettled = true;
640
+ activeTurn = false;
641
+ for (const u of unsubs) u();
642
+ pendingReject(err);
643
+ };
644
+ for (const type of eventTypes) {
645
+ unsubs.push(
646
+ channel.on(type, (msg) => {
647
+ forward(msg);
648
+ })
649
+ );
650
+ }
651
+ unsubs.push(
652
+ channel.on("finish", (msg) => {
653
+ forward(msg);
654
+ settleSuccess();
655
+ })
656
+ );
657
+ unsubs.push(
658
+ channel.on("error", (msg) => {
659
+ forward(msg);
660
+ settleError(msg.error);
661
+ })
662
+ );
663
+ const onClose = (_code, reason) => {
664
+ if (isSettled) return;
665
+ if (reason === "suspended") {
666
+ settleSuccess();
667
+ return;
668
+ }
669
+ settleError(
670
+ new Error("OpenCode bridge closed before the turn finished.")
671
+ );
672
+ };
673
+ channel.onClose(onClose);
674
+ const onAbort = () => {
675
+ if (isSettled) return;
676
+ try {
677
+ channel.send({ type: "abort" });
678
+ } catch {
679
+ }
680
+ settleError(
681
+ turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError")
682
+ );
683
+ };
684
+ if (turnOpts.abortSignal) {
685
+ if (turnOpts.abortSignal.aborted) {
686
+ onAbort();
687
+ } else {
688
+ turnOpts.abortSignal.addEventListener("abort", onAbort, {
689
+ once: true
690
+ });
691
+ }
692
+ }
693
+ while (pendingCompactionParts.length > 0) {
694
+ forward(pendingCompactionParts.shift());
695
+ }
696
+ return {
697
+ submitToolResult: async (input) => {
698
+ channel.send({
699
+ type: "tool-result",
700
+ toolCallId: input.toolCallId,
701
+ output: input.output,
702
+ isError: input.isError
703
+ });
704
+ },
705
+ submitToolApproval: async (input) => {
706
+ channel.send({
707
+ type: "tool-approval-response",
708
+ approvalId: input.approvalId,
709
+ approved: input.approved,
710
+ reason: input.reason
711
+ });
712
+ },
713
+ submitUserMessage: async (text) => {
714
+ channel.send({ type: "user-message", text });
715
+ },
716
+ done
717
+ };
718
+ };
719
+ const startBase = () => ({
720
+ model,
721
+ provider,
722
+ ...reasoningVariant ? { variant: reasoningVariant } : {},
723
+ ...permissionMode ? { permissionMode } : {},
724
+ ...builtinToolFiltering ? { builtinToolFiltering } : {},
725
+ ...pendingResumeSessionId ? { resumeSessionId: pendingResumeSessionId } : latestOpenCodeSessionId ? { resumeSessionId: latestOpenCodeSessionId } : {},
726
+ ...debug ? { debug } : {}
727
+ });
728
+ return {
729
+ sessionId,
730
+ isResume,
731
+ modelId: model,
732
+ doPromptTurn: async (promptOpts) => {
733
+ const control = wireTurn({
734
+ emit: promptOpts.emit,
735
+ abortSignal: promptOpts.abortSignal
736
+ });
737
+ const applyInstructions = !instructionsApplied && !!promptOpts.instructions;
738
+ instructionsApplied = true;
739
+ channel.send({
740
+ type: "start",
741
+ operation: "prompt",
742
+ prompt: extractUserText(promptOpts.prompt),
743
+ tools: (promptOpts.tools ?? []).map((t) => ({
744
+ name: t.name,
745
+ description: t.description,
746
+ inputSchema: t.inputSchema
747
+ })),
748
+ ...applyInstructions ? { instructions: promptOpts.instructions } : {},
749
+ ...startBase()
750
+ });
751
+ pendingResumeSessionId = void 0;
752
+ return control;
753
+ },
754
+ doContinueTurn: async (continueOpts) => {
755
+ const control = wireTurn({
756
+ emit: continueOpts.emit,
757
+ abortSignal: continueOpts.abortSignal
758
+ });
759
+ if (rerunContinue) {
760
+ channel.send({
761
+ type: "start",
762
+ operation: "prompt",
763
+ prompt: "Continue.",
764
+ tools: (continueOpts.tools ?? []).map((t) => ({
765
+ name: t.name,
766
+ description: t.description,
767
+ inputSchema: t.inputSchema
768
+ })),
769
+ ...startBase()
770
+ });
771
+ pendingResumeSessionId = void 0;
772
+ }
773
+ return control;
774
+ },
775
+ doCompact: async (customInstructions) => {
776
+ if (customInstructions?.trim()) {
777
+ throw new HarnessCapabilityUnsupportedError({
778
+ harnessId: "opencode",
779
+ message: "Harness 'opencode' supports native manual compaction, but OpenCode does not expose custom compaction instructions through the supported API."
780
+ });
781
+ }
782
+ if (activeTurn) {
783
+ throw new HarnessCapabilityUnsupportedError({
784
+ harnessId: "opencode",
785
+ message: "Harness 'opencode' supports manual compaction between turns; compacting during an active turn is not supported by the bridge transport."
786
+ });
787
+ }
788
+ await runCompactOperation({
789
+ channel,
790
+ model,
791
+ provider,
792
+ permissionMode,
793
+ debug,
794
+ resumeSessionId: latestOpenCodeSessionId,
795
+ onCompaction: (part) => pendingCompactionParts.push(part)
796
+ });
797
+ },
798
+ doDetach: async () => {
799
+ if (stopped) {
800
+ throw new Error(
801
+ `OpenCode session ${sessionId} is already stopped; cannot detach.`
802
+ );
803
+ }
804
+ stopped = true;
805
+ const lastSeenEventId = await channel.suspend();
806
+ return {
807
+ type: "resume-session",
808
+ harnessId: "opencode",
809
+ specificationVersion: "harness-v1",
810
+ data: {
811
+ ...latestOpenCodeSessionId ? { openCodeSessionId: latestOpenCodeSessionId } : {},
812
+ bridge: {
813
+ port: bridgePort,
814
+ token: bridgeToken,
815
+ lastSeenEventId,
816
+ sandboxId
817
+ }
818
+ }
819
+ };
820
+ },
821
+ doDestroy: async () => {
822
+ if (stopped) return stopPromise;
823
+ stopped = true;
824
+ stopPromise = (async () => {
825
+ channel.beginClose();
826
+ try {
827
+ if (!channel.isClosed()) {
828
+ channel.send({ type: "shutdown" });
829
+ }
830
+ } catch {
831
+ }
832
+ let stopTimer;
833
+ try {
834
+ if (proc) {
835
+ await Promise.race([
836
+ proc.wait(),
837
+ new Promise((resolve) => {
838
+ stopTimer = setTimeout(resolve, 5e3);
839
+ stopTimer.unref?.();
840
+ })
841
+ ]);
842
+ }
843
+ } finally {
844
+ if (stopTimer) clearTimeout(stopTimer);
845
+ try {
846
+ await proc?.kill();
847
+ } catch {
848
+ }
849
+ channel.close();
850
+ }
851
+ })();
852
+ return stopPromise;
853
+ },
854
+ doStop: async () => {
855
+ if (stopped) {
856
+ throw new Error(
857
+ `OpenCode session ${sessionId} is already stopped; cannot stop.`
858
+ );
859
+ }
860
+ stopped = true;
861
+ channel.beginClose();
862
+ const data = channel.isClosed() ? {} : await new Promise((resolve, reject) => {
863
+ const timer = setTimeout(() => {
864
+ unsub();
865
+ reject(
866
+ new Error(
867
+ `OpenCode session ${sessionId} did not reply to detach within 5s.`
868
+ )
869
+ );
870
+ }, 5e3);
871
+ timer.unref?.();
872
+ const unsub = channel.on("bridge-detach", (msg) => {
873
+ clearTimeout(timer);
874
+ unsub();
875
+ resolve(msg.data);
876
+ });
877
+ try {
878
+ channel.send({ type: "detach" });
879
+ } catch (err) {
880
+ clearTimeout(timer);
881
+ unsub();
882
+ reject(err);
883
+ }
884
+ });
885
+ let stopTimer;
886
+ try {
887
+ if (proc) {
888
+ await Promise.race([
889
+ proc.wait(),
890
+ new Promise((resolve) => {
891
+ stopTimer = setTimeout(resolve, 5e3);
892
+ stopTimer.unref?.();
893
+ })
894
+ ]);
895
+ }
896
+ } finally {
897
+ if (stopTimer) clearTimeout(stopTimer);
898
+ try {
899
+ await proc?.kill();
900
+ } catch {
901
+ }
902
+ channel.close();
903
+ }
904
+ const payload = {
905
+ type: "resume-session",
906
+ harnessId: "opencode",
907
+ specificationVersion: "harness-v1",
908
+ data: data ?? {}
909
+ };
910
+ return payload;
911
+ },
912
+ doSuspendTurn: async () => {
913
+ if (stopped) {
914
+ throw new Error(
915
+ `OpenCode session ${sessionId} is stopped; cannot suspend.`
916
+ );
917
+ }
918
+ stopped = true;
919
+ await channel.interrupt();
920
+ const lastSeenEventId = await channel.suspend();
921
+ const payload = {
922
+ type: "continue-turn",
923
+ harnessId: "opencode",
924
+ specificationVersion: "harness-v1",
925
+ data: {
926
+ ...latestOpenCodeSessionId ? { openCodeSessionId: latestOpenCodeSessionId } : {},
927
+ bridge: {
928
+ port: bridgePort,
929
+ token: bridgeToken,
930
+ lastSeenEventId,
931
+ sandboxId
932
+ }
933
+ }
934
+ };
935
+ return payload;
936
+ }
937
+ };
938
+ }
939
+ async function runCompactOperation({
940
+ channel,
941
+ model,
942
+ provider,
943
+ permissionMode,
944
+ debug,
945
+ resumeSessionId,
946
+ onCompaction
947
+ }) {
948
+ let pendingResolve;
949
+ let pendingReject;
950
+ const done = new Promise((resolve, reject) => {
951
+ pendingResolve = resolve;
952
+ pendingReject = reject;
953
+ });
954
+ const unsubs = [
955
+ channel.on("compaction", (msg) => onCompaction(msg)),
956
+ channel.on("finish", () => {
957
+ for (const u of unsubs) u();
958
+ pendingResolve();
959
+ }),
960
+ channel.on("error", (msg) => {
961
+ for (const u of unsubs) u();
962
+ pendingReject(msg.error);
963
+ })
964
+ ];
965
+ channel.send({
966
+ type: "start",
967
+ operation: "compact",
968
+ prompt: "",
969
+ tools: [],
970
+ model,
971
+ provider,
972
+ ...permissionMode ? { permissionMode } : {},
973
+ ...resumeSessionId ? { resumeSessionId } : {},
974
+ ...debug ? { debug } : {}
975
+ });
976
+ await done;
977
+ }
978
+ function extractUserText(prompt) {
979
+ if (typeof prompt === "string") return prompt;
980
+ const { content } = prompt;
981
+ if (typeof content === "string") return content;
982
+ const parts = [];
983
+ for (const part of content) {
984
+ if (part.type !== "text") {
985
+ throw new HarnessCapabilityUnsupportedError({
986
+ harnessId: "opencode",
987
+ message: `The OpenCode harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
988
+ });
989
+ }
990
+ parts.push(part.text);
991
+ }
992
+ return parts.join("\n\n");
993
+ }
994
+
995
+ // src/index.ts
996
+ var openCode = createOpenCode();
997
+ export {
998
+ VERSION,
999
+ createOpenCode,
1000
+ openCode
1001
+ };
1002
+ //# sourceMappingURL=index.js.map