agent-worker 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,30 +6,45 @@ import { stringify } from "yaml";
6
6
 
7
7
  //#region src/agent/models.ts
8
8
  const providerCache = {};
9
+ /** Provider SDK package mapping */
10
+ const PROVIDER_PACKAGES = {
11
+ anthropic: {
12
+ package: "@ai-sdk/anthropic",
13
+ export: "anthropic"
14
+ },
15
+ openai: {
16
+ package: "@ai-sdk/openai",
17
+ export: "openai"
18
+ },
19
+ deepseek: {
20
+ package: "@ai-sdk/deepseek",
21
+ export: "deepseek"
22
+ },
23
+ google: {
24
+ package: "@ai-sdk/google",
25
+ export: "google"
26
+ },
27
+ groq: {
28
+ package: "@ai-sdk/groq",
29
+ export: "groq"
30
+ },
31
+ mistral: {
32
+ package: "@ai-sdk/mistral",
33
+ export: "mistral"
34
+ },
35
+ xai: {
36
+ package: "@ai-sdk/xai",
37
+ export: "xai"
38
+ }
39
+ };
9
40
  /**
10
- * Lazy load a provider, caching the result
11
- * Supports custom baseURL and apiKey for providers using compatible APIs (e.g., MiniMax using Claude API)
41
+ * Lazy load a provider SDK, caching the result.
42
+ * Only caches standard providers (no custom baseURL/apiKey).
12
43
  */
13
- async function loadProvider(name, packageName, exportName, options) {
44
+ async function loadProvider(name, packageName, exportName) {
14
45
  if (name in providerCache) return providerCache[name] ?? null;
15
46
  try {
16
- const module = await import(packageName);
17
- if (options?.baseURL || options?.apiKeyEnvVar) {
18
- const createProvider = module[`create${exportName.charAt(0).toUpperCase() + exportName.slice(1)}`];
19
- if (createProvider) {
20
- const providerOptions = {};
21
- if (options.baseURL) providerOptions.baseURL = options.baseURL;
22
- if (options.apiKeyEnvVar) {
23
- const apiKey = process.env[options.apiKeyEnvVar];
24
- if (!apiKey) throw new Error(`Environment variable ${options.apiKeyEnvVar} is not set (required for ${name} provider)`);
25
- providerOptions.apiKey = apiKey;
26
- }
27
- const provider = createProvider(providerOptions);
28
- providerCache[name] = provider;
29
- return provider;
30
- }
31
- }
32
- const exportedProvider = module[exportName];
47
+ const exportedProvider = (await import(packageName))[exportName];
33
48
  providerCache[name] = exportedProvider;
34
49
  return exportedProvider;
35
50
  } catch {
@@ -38,6 +53,55 @@ async function loadProvider(name, packageName, exportName, options) {
38
53
  }
39
54
  }
40
55
  /**
56
+ * Create a provider instance with custom baseURL and/or apiKey.
57
+ * Not cached — each call creates a fresh instance.
58
+ */
59
+ async function createCustomProvider(packageName, exportName, options) {
60
+ const createFn = (await import(packageName))[`create${exportName.charAt(0).toUpperCase() + exportName.slice(1)}`];
61
+ if (!createFn) throw new Error(`Package ${packageName} does not export create${exportName.charAt(0).toUpperCase() + exportName.slice(1)}`);
62
+ return createFn(options);
63
+ }
64
+ /**
65
+ * Resolve api_key field: '$ENV_VAR' → process.env.ENV_VAR, literal → as-is
66
+ */
67
+ function resolveApiKey(apiKey) {
68
+ if (apiKey.startsWith("$")) {
69
+ const envVar = apiKey.slice(1);
70
+ const value = process.env[envVar];
71
+ if (!value) throw new Error(`Environment variable ${envVar} is not set`);
72
+ return value;
73
+ }
74
+ return apiKey;
75
+ }
76
+ /**
77
+ * Create a model using explicit provider configuration.
78
+ * Use this when provider details (base_url, api_key) are specified separately from the model name.
79
+ *
80
+ * Example:
81
+ * createModelWithProvider("MiniMax-M2.5", { name: "anthropic", base_url: "https://api.minimax.io/anthropic/v1", api_key: "$MINIMAX_API_KEY" })
82
+ */
83
+ async function createModelWithProvider(modelName, provider) {
84
+ if (typeof provider === "string") {
85
+ const pkg = PROVIDER_PACKAGES[provider];
86
+ if (!pkg) throw new Error(`Unknown provider: ${provider}. Supported: ${Object.keys(PROVIDER_PACKAGES).join(", ")}`);
87
+ const providerFn = await loadProvider(provider, pkg.package, pkg.export);
88
+ if (!providerFn) throw new Error(`Install ${pkg.package} to use ${provider} models directly`);
89
+ return providerFn(modelName);
90
+ }
91
+ const { name, base_url, api_key } = provider;
92
+ const pkg = PROVIDER_PACKAGES[name];
93
+ if (!pkg) throw new Error(`Unknown provider: ${name}. Supported: ${Object.keys(PROVIDER_PACKAGES).join(", ")}`);
94
+ if (!base_url && !api_key) {
95
+ const providerFn = await loadProvider(name, pkg.package, pkg.export);
96
+ if (!providerFn) throw new Error(`Install ${pkg.package} to use ${name} models directly`);
97
+ return providerFn(modelName);
98
+ }
99
+ const opts = {};
100
+ if (base_url) opts.baseURL = base_url;
101
+ if (api_key) opts.apiKey = resolveApiKey(api_key);
102
+ return (await createCustomProvider(pkg.package, pkg.export, opts))(modelName);
103
+ }
104
+ /**
41
105
  * Parse model identifier and return the appropriate provider model
42
106
  *
43
107
  * Supports three formats:
@@ -89,61 +153,14 @@ async function createModelAsync(modelId) {
89
153
  const provider = modelId.slice(0, colonIndex);
90
154
  const modelName = modelId.slice(colonIndex + 1);
91
155
  if (!modelName) throw new Error(`Invalid model identifier: ${modelId}. Model name is required.`);
92
- const providerConfigs = {
93
- anthropic: {
94
- package: "@ai-sdk/anthropic",
95
- export: "anthropic"
96
- },
97
- openai: {
98
- package: "@ai-sdk/openai",
99
- export: "openai"
100
- },
101
- deepseek: {
102
- package: "@ai-sdk/deepseek",
103
- export: "deepseek"
104
- },
105
- google: {
106
- package: "@ai-sdk/google",
107
- export: "google"
108
- },
109
- groq: {
110
- package: "@ai-sdk/groq",
111
- export: "groq"
112
- },
113
- mistral: {
114
- package: "@ai-sdk/mistral",
115
- export: "mistral"
116
- },
117
- xai: {
118
- package: "@ai-sdk/xai",
119
- export: "xai"
120
- },
121
- minimax: {
122
- package: "@ai-sdk/anthropic",
123
- export: "anthropic",
124
- options: {
125
- baseURL: "https://api.minimax.io/anthropic/v1",
126
- apiKeyEnvVar: "MINIMAX_API_KEY"
127
- }
128
- },
129
- minimax_cn: {
130
- package: "@ai-sdk/anthropic",
131
- export: "anthropic",
132
- options: {
133
- baseURL: "https://api.minimaxi.com/anthropic/v1",
134
- apiKeyEnvVar: "MINIMAX_CN_API_KEY"
135
- }
136
- }
137
- };
138
- const config = providerConfigs[provider];
139
- if (!config) throw new Error(`Unknown provider: ${provider}. Supported: ${Object.keys(providerConfigs).join(", ")}. Or use gateway format: provider/model (e.g., openai/gpt-5.2)`);
140
- const providerFn = await loadProvider(provider, config.package, config.export, config.options);
156
+ const config = PROVIDER_PACKAGES[provider];
157
+ if (!config) throw new Error(`Unknown provider: ${provider}. Supported: ${Object.keys(PROVIDER_PACKAGES).join(", ")}. Or use gateway format: provider/model (e.g., openai/gpt-5.2)`);
158
+ const providerFn = await loadProvider(provider, config.package, config.export);
141
159
  if (!providerFn) throw new Error(`Install ${config.package} to use ${provider} models directly`);
142
160
  return providerFn(modelName);
143
161
  }
144
162
  /**
145
163
  * List of supported providers for direct access
146
- * Note: minimax uses Claude-compatible API via @ai-sdk/anthropic with custom baseURL
147
164
  */
148
165
  const SUPPORTED_PROVIDERS = [
149
166
  "anthropic",
@@ -152,8 +169,7 @@ const SUPPORTED_PROVIDERS = [
152
169
  "google",
153
170
  "groq",
154
171
  "mistral",
155
- "xai",
156
- "minimax"
172
+ "xai"
157
173
  ];
158
174
  /**
159
175
  * Default provider when none specified
@@ -192,8 +208,7 @@ const FRONTIER_MODELS = {
192
208
  "pixtral-large-latest",
193
209
  "magistral-medium-2506"
194
210
  ],
195
- xai: ["grok-4", "grok-4-fast-reasoning"],
196
- minimax: ["MiniMax-M2"]
211
+ xai: ["grok-4", "grok-4-fast-reasoning"]
197
212
  };
198
213
 
199
214
  //#endregion
@@ -209,7 +224,8 @@ const BACKEND_DEFAULT_MODELS = {
209
224
  default: "claude-sonnet-4-5",
210
225
  claude: "sonnet",
211
226
  cursor: "sonnet-4.5",
212
- codex: "gpt-5.2-codex"
227
+ codex: "gpt-5.2-codex",
228
+ opencode: "deepseek/deepseek-chat"
213
229
  };
214
230
  /**
215
231
  * Model aliases for SDK (Anthropic format)
@@ -272,6 +288,22 @@ const CODEX_MODEL_MAP = {
272
288
  "o3-mini": "o3-mini"
273
289
  };
274
290
  /**
291
+ * Model translation for OpenCode CLI backend
292
+ * OpenCode uses provider/model format natively
293
+ */
294
+ const OPENCODE_MODEL_MAP = {
295
+ "deepseek-chat": "deepseek/deepseek-chat",
296
+ "deepseek-reasoner": "deepseek/deepseek-reasoner",
297
+ "deepseek/deepseek-chat": "deepseek/deepseek-chat",
298
+ "deepseek/deepseek-reasoner": "deepseek/deepseek-reasoner",
299
+ sonnet: "anthropic/claude-sonnet-4-5-20250514",
300
+ opus: "anthropic/claude-opus-4-20250514",
301
+ "claude-sonnet-4-5": "anthropic/claude-sonnet-4-5-20250514",
302
+ "claude-opus-4": "anthropic/claude-opus-4-20250514",
303
+ "gpt-5.2": "openai/gpt-5.2",
304
+ o3: "openai/o3"
305
+ };
306
+ /**
275
307
  * Get the model name for a specific backend
276
308
  * Translates generic model names to backend-specific format
277
309
  */
@@ -283,6 +315,7 @@ function getModelForBackend(model, backend) {
283
315
  case "cursor": return CURSOR_MODEL_MAP[model] || CURSOR_MODEL_MAP[normalizedModel] || normalizedModel;
284
316
  case "claude": return CLAUDE_MODEL_MAP[model] || CLAUDE_MODEL_MAP[normalizedModel] || normalizedModel;
285
317
  case "codex": return CODEX_MODEL_MAP[model] || CODEX_MODEL_MAP[normalizedModel] || normalizedModel;
318
+ case "opencode": return OPENCODE_MODEL_MAP[model] || OPENCODE_MODEL_MAP[normalizedModel] || model;
286
319
  default: return normalizedModel;
287
320
  }
288
321
  }
@@ -321,6 +354,12 @@ const DEFAULT_IDLE_TIMEOUT = 6e5;
321
354
  * producing output (tool calls, analysis, etc.).
322
355
  */
323
356
  /**
357
+ * Default startup timeout (30 seconds).
358
+ * If the process produces zero output within this window, it's killed.
359
+ * This catches unresponsive backends (e.g., nested `claude -p` inside Claude Code).
360
+ */
361
+ const DEFAULT_STARTUP_TIMEOUT = 3e4;
362
+ /**
324
363
  * Execute a command with idle timeout.
325
364
  *
326
365
  * The timeout resets every time the process writes to stdout or stderr.
@@ -331,7 +370,10 @@ const MIN_TIMEOUT_MS = 1e3;
331
370
  async function execWithIdleTimeout(options) {
332
371
  const { command, args, cwd, onStdout } = options;
333
372
  const timeout = Math.max(options.timeout, MIN_TIMEOUT_MS);
373
+ const rawStartup = options.startupTimeout !== void 0 ? options.startupTimeout : DEFAULT_STARTUP_TIMEOUT;
374
+ const startupTimeout = rawStartup > 0 ? Math.min(rawStartup, timeout) : 0;
334
375
  let idleTimedOut = false;
376
+ let hasReceivedOutput = false;
335
377
  let timer;
336
378
  let stdout = "";
337
379
  let stderr = "";
@@ -350,6 +392,7 @@ async function execWithIdleTimeout(options) {
350
392
  subprocess.stdout?.on("data", (chunk) => {
351
393
  const text = chunk.toString();
352
394
  stdout += text;
395
+ hasReceivedOutput = true;
353
396
  resetTimer();
354
397
  if (onStdout) try {
355
398
  onStdout(text);
@@ -359,9 +402,16 @@ async function execWithIdleTimeout(options) {
359
402
  });
360
403
  subprocess.stderr?.on("data", (chunk) => {
361
404
  stderr += chunk.toString();
405
+ hasReceivedOutput = true;
362
406
  resetTimer();
363
407
  });
364
- resetTimer();
408
+ if (startupTimeout > 0) timer = setTimeout(() => {
409
+ if (!hasReceivedOutput) {
410
+ idleTimedOut = true;
411
+ subprocess.kill();
412
+ }
413
+ }, startupTimeout);
414
+ else resetTimer();
365
415
  try {
366
416
  await subprocess;
367
417
  clearTimeout(timer);
@@ -371,7 +421,7 @@ async function execWithIdleTimeout(options) {
371
421
  };
372
422
  } catch (error) {
373
423
  clearTimeout(timer);
374
- if (idleTimedOut) throw new IdleTimeoutError(timeout, stdout, stderr);
424
+ if (idleTimedOut) throw new IdleTimeoutError(hasReceivedOutput ? timeout : startupTimeout, stdout, stderr);
375
425
  throw error;
376
426
  }
377
427
  }
@@ -382,7 +432,10 @@ async function execWithIdleTimeout(options) {
382
432
  function execWithIdleTimeoutAbortable(options) {
383
433
  const { command, args, cwd, onStdout } = options;
384
434
  const timeout = Math.max(options.timeout, MIN_TIMEOUT_MS);
435
+ const rawStartup = options.startupTimeout !== void 0 ? options.startupTimeout : DEFAULT_STARTUP_TIMEOUT;
436
+ const startupTimeout = rawStartup > 0 ? Math.min(rawStartup, timeout) : 0;
385
437
  let idleTimedOut = false;
438
+ let hasReceivedOutput = false;
386
439
  let timer;
387
440
  let stdout = "";
388
441
  let stderr = "";
@@ -402,6 +455,7 @@ function execWithIdleTimeoutAbortable(options) {
402
455
  subprocess.stdout?.on("data", (chunk) => {
403
456
  const text = chunk.toString();
404
457
  stdout += text;
458
+ hasReceivedOutput = true;
405
459
  resetTimer();
406
460
  if (onStdout) try {
407
461
  onStdout(text);
@@ -411,9 +465,16 @@ function execWithIdleTimeoutAbortable(options) {
411
465
  });
412
466
  subprocess.stderr?.on("data", (chunk) => {
413
467
  stderr += chunk.toString();
468
+ hasReceivedOutput = true;
414
469
  resetTimer();
415
470
  });
416
- resetTimer();
471
+ if (startupTimeout > 0) timer = setTimeout(() => {
472
+ if (!hasReceivedOutput) {
473
+ idleTimedOut = true;
474
+ subprocess.kill();
475
+ }
476
+ }, startupTimeout);
477
+ else resetTimer();
417
478
  const abort = () => {
418
479
  if (!isAborted) {
419
480
  isAborted = true;
@@ -436,7 +497,7 @@ function execWithIdleTimeoutAbortable(options) {
436
497
  } catch (error) {
437
498
  clearTimeout(timer);
438
499
  if (isAborted) throw new Error("Process aborted by user");
439
- if (idleTimedOut) throw new IdleTimeoutError(timeout, stdout, stderr);
500
+ if (idleTimedOut) throw new IdleTimeoutError(hasReceivedOutput ? timeout : startupTimeout, stdout, stderr);
440
501
  throw error;
441
502
  }
442
503
  })(),
@@ -780,7 +841,10 @@ var ClaudeCodeBackend = class {
780
841
  return { content: stdout.trim() };
781
842
  } catch (error) {
782
843
  this.currentAbort = void 0;
783
- if (error instanceof IdleTimeoutError) throw new Error(`claude timed out after ${timeout}ms of inactivity`);
844
+ if (error instanceof IdleTimeoutError) {
845
+ if (error.stdout === "" && error.stderr === "") throw new Error(`claude produced no output within ${error.timeout}ms. This often happens when running nested 'claude -p' inside an existing Claude Code session. Consider using the SDK backend (model: "anthropic/claude-sonnet-4-5") instead.`);
846
+ throw new Error(`claude timed out after ${timeout}ms of inactivity`);
847
+ }
784
848
  if (error && typeof error === "object" && "exitCode" in error) {
785
849
  const execError = error;
786
850
  throw new Error(`claude failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
@@ -931,8 +995,7 @@ var CodexBackend = class {
931
995
  //#region src/backends/cursor.ts
932
996
  /**
933
997
  * Cursor CLI backend
934
- * Uses `cursor agent -p` (preferred) or `cursor-agent -p` (fallback)
935
- * for non-interactive mode with stream-json output
998
+ * Uses `cursor agent -p` for non-interactive mode with stream-json output
936
999
  *
937
1000
  * MCP Configuration:
938
1001
  * Cursor uses project-level MCP config via .cursor/mcp.json in the workspace.
@@ -943,8 +1006,13 @@ var CodexBackend = class {
943
1006
  var CursorBackend = class {
944
1007
  type = "cursor";
945
1008
  options;
946
- /** Resolved command: "cursor" (subcommand style) or "cursor-agent" (standalone) */
947
- resolvedCommand = null;
1009
+ /**
1010
+ * Resolved command style:
1011
+ * - "subcommand": `cursor agent -p ...` (IDE-bundled CLI)
1012
+ * - "direct": `agent -p ...` (standalone install via cursor.com/install)
1013
+ * - null: not yet resolved
1014
+ */
1015
+ resolvedStyle = null;
948
1016
  constructor(options = {}) {
949
1017
  this.options = {
950
1018
  timeout: DEFAULT_IDLE_TIMEOUT,
@@ -984,7 +1052,7 @@ var CursorBackend = class {
984
1052
  }
985
1053
  }
986
1054
  async isAvailable() {
987
- return await this.resolveCommand() !== null;
1055
+ return await this.resolveStyle() !== null;
988
1056
  }
989
1057
  getInfo() {
990
1058
  return {
@@ -993,32 +1061,34 @@ var CursorBackend = class {
993
1061
  };
994
1062
  }
995
1063
  /**
996
- * Resolve which cursor command is available.
997
- * Prefers `cursor agent` (subcommand), falls back to `cursor-agent` (standalone).
1064
+ * Resolve which cursor command style is available.
1065
+ * Tries in order:
1066
+ * 1. `cursor agent --version` — IDE-bundled CLI (subcommand style)
1067
+ * 2. `agent --version` — standalone install via cursor.com/install (direct style)
998
1068
  * Result is cached after first resolution.
999
1069
  */
1000
- async resolveCommand() {
1001
- if (this.resolvedCommand !== null) return this.resolvedCommand;
1070
+ async resolveStyle() {
1071
+ if (this.resolvedStyle !== null) return this.resolvedStyle;
1002
1072
  try {
1003
1073
  await execa("cursor", ["agent", "--version"], {
1004
1074
  stdin: "ignore",
1005
1075
  timeout: 2e3
1006
1076
  });
1007
- this.resolvedCommand = "cursor";
1008
- return "cursor";
1077
+ this.resolvedStyle = "subcommand";
1078
+ return "subcommand";
1009
1079
  } catch {}
1010
1080
  try {
1011
- await execa("cursor-agent", ["--version"], {
1081
+ await execa("agent", ["--version"], {
1012
1082
  stdin: "ignore",
1013
1083
  timeout: 2e3
1014
1084
  });
1015
- this.resolvedCommand = "cursor-agent";
1016
- return "cursor-agent";
1085
+ this.resolvedStyle = "direct";
1086
+ return "direct";
1017
1087
  } catch {}
1018
1088
  return null;
1019
1089
  }
1020
1090
  async buildCommand(message) {
1021
- const cmd = await this.resolveCommand();
1091
+ const style = await this.resolveStyle();
1022
1092
  const agentArgs = [
1023
1093
  "-p",
1024
1094
  "--force",
@@ -1027,16 +1097,165 @@ var CursorBackend = class {
1027
1097
  message
1028
1098
  ];
1029
1099
  if (this.options.model) agentArgs.push("--model", this.options.model);
1030
- if (cmd === "cursor") return {
1100
+ if (!style) throw new Error("cursor agent CLI not found. Install via: curl -fsS https://cursor.com/install | bash");
1101
+ if (style === "direct") return {
1102
+ command: "agent",
1103
+ args: agentArgs
1104
+ };
1105
+ return {
1031
1106
  command: "cursor",
1032
1107
  args: ["agent", ...agentArgs]
1033
1108
  };
1109
+ }
1110
+ };
1111
+
1112
+ //#endregion
1113
+ //#region src/backends/opencode.ts
1114
+ /**
1115
+ * OpenCode CLI backend
1116
+ * Uses `opencode run` for non-interactive mode with JSON event output
1117
+ *
1118
+ * MCP Configuration:
1119
+ * OpenCode uses project-level MCP config via opencode.json in the workspace.
1120
+ * Use setWorkspace() to set up a dedicated workspace with MCP config.
1121
+ *
1122
+ * @see https://opencode.ai/docs/
1123
+ */
1124
+ var OpenCodeBackend = class {
1125
+ type = "opencode";
1126
+ options;
1127
+ constructor(options = {}) {
1128
+ this.options = {
1129
+ timeout: DEFAULT_IDLE_TIMEOUT,
1130
+ ...options
1131
+ };
1132
+ }
1133
+ /**
1134
+ * Set up workspace directory with MCP config
1135
+ * Creates opencode.json in the workspace with MCP server config
1136
+ */
1137
+ setWorkspace(workspaceDir, mcpConfig) {
1138
+ this.options.workspace = workspaceDir;
1139
+ if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
1140
+ const opencodeMcp = {};
1141
+ for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
1142
+ const serverConfig = config;
1143
+ opencodeMcp[name] = {
1144
+ type: "local",
1145
+ command: [serverConfig.command, ...serverConfig.args || []],
1146
+ enabled: true,
1147
+ ...serverConfig.env ? { environment: serverConfig.env } : {}
1148
+ };
1149
+ }
1150
+ const opencodeConfig = {
1151
+ $schema: "https://opencode.ai/config.json",
1152
+ mcp: opencodeMcp
1153
+ };
1154
+ writeFileSync(join(workspaceDir, "opencode.json"), JSON.stringify(opencodeConfig, null, 2));
1155
+ }
1156
+ async send(message, _options) {
1157
+ const args = this.buildArgs(message);
1158
+ const cwd = this.options.workspace || this.options.cwd;
1159
+ const timeout = this.options.timeout ?? DEFAULT_IDLE_TIMEOUT;
1160
+ try {
1161
+ const { stdout } = await execWithIdleTimeout({
1162
+ command: "opencode",
1163
+ args,
1164
+ cwd,
1165
+ timeout,
1166
+ onStdout: this.options.streamCallbacks ? createStreamParser(this.options.streamCallbacks, "OpenCode", opencodeAdapter) : void 0
1167
+ });
1168
+ return extractOpenCodeResult(stdout);
1169
+ } catch (error) {
1170
+ if (error instanceof IdleTimeoutError) throw new Error(`opencode timed out after ${timeout}ms of inactivity`);
1171
+ if (error && typeof error === "object" && "exitCode" in error) {
1172
+ const execError = error;
1173
+ throw new Error(`opencode failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
1174
+ }
1175
+ throw error;
1176
+ }
1177
+ }
1178
+ async isAvailable() {
1179
+ try {
1180
+ await execa("opencode", ["--version"], {
1181
+ stdin: "ignore",
1182
+ timeout: 5e3
1183
+ });
1184
+ return true;
1185
+ } catch {
1186
+ return false;
1187
+ }
1188
+ }
1189
+ getInfo() {
1034
1190
  return {
1035
- command: "cursor-agent",
1036
- args: agentArgs
1191
+ name: "OpenCode CLI",
1192
+ model: this.options.model
1037
1193
  };
1038
1194
  }
1195
+ buildArgs(message) {
1196
+ const args = [
1197
+ "run",
1198
+ "--format",
1199
+ "json",
1200
+ message
1201
+ ];
1202
+ if (this.options.model) args.push("--model", this.options.model);
1203
+ return args;
1204
+ }
1039
1205
  };
1206
+ /**
1207
+ * Adapter for OpenCode --format json output.
1208
+ *
1209
+ * Events:
1210
+ * { type: "step_start", sessionID: "..." }
1211
+ * { type: "tool_use", part: { tool: "bash", state: { input: {...} } } }
1212
+ * { type: "text", part: { text: "..." } } → skipped (result only)
1213
+ * { type: "step_finish", part: { cost, tokens } }
1214
+ */
1215
+ const opencodeAdapter = (raw) => {
1216
+ const event = raw;
1217
+ if (event.type === "step_start") return {
1218
+ kind: "init",
1219
+ sessionId: event.sessionID
1220
+ };
1221
+ if (event.type === "tool_use") {
1222
+ const { tool, state } = event.part;
1223
+ const args = state.input ? JSON.stringify(state.input) : "";
1224
+ return {
1225
+ kind: "tool_call",
1226
+ name: tool,
1227
+ args: args.length > 100 ? args.slice(0, 100) + "..." : args
1228
+ };
1229
+ }
1230
+ if (event.type === "text") return { kind: "skip" };
1231
+ if (event.type === "step_finish") {
1232
+ const { cost, tokens } = event.part;
1233
+ return {
1234
+ kind: "completed",
1235
+ costUsd: cost,
1236
+ usage: tokens ? {
1237
+ input: tokens.input,
1238
+ output: tokens.output
1239
+ } : void 0
1240
+ };
1241
+ }
1242
+ return null;
1243
+ };
1244
+ /**
1245
+ * Extract final result from OpenCode --format json output.
1246
+ *
1247
+ * Priority:
1248
+ * 1. Last text event
1249
+ * 2. Raw stdout fallback
1250
+ */
1251
+ function extractOpenCodeResult(stdout) {
1252
+ const lines = stdout.trim().split("\n");
1253
+ for (let i = lines.length - 1; i >= 0; i--) try {
1254
+ const event = JSON.parse(lines[i]);
1255
+ if (event.type === "text" && event.part?.text) return { content: event.part.text };
1256
+ } catch {}
1257
+ return { content: stdout.trim() };
1258
+ }
1040
1259
 
1041
1260
  //#endregion
1042
1261
  //#region src/backends/sdk.ts
@@ -1049,15 +1268,17 @@ var SdkBackend = class {
1049
1268
  modelId;
1050
1269
  model = null;
1051
1270
  maxTokens;
1271
+ provider;
1052
1272
  constructor(options) {
1053
1273
  this.modelId = options.model;
1054
1274
  this.maxTokens = options.maxTokens ?? 4096;
1055
- try {
1275
+ this.provider = options.provider;
1276
+ if (!this.provider) try {
1056
1277
  this.model = createModel(this.modelId);
1057
1278
  } catch {}
1058
1279
  }
1059
1280
  async send(message, options) {
1060
- if (!this.model) this.model = await createModelAsync(this.modelId);
1281
+ if (!this.model) this.model = this.provider ? await createModelWithProvider(this.modelId, this.provider) : await createModelAsync(this.modelId);
1061
1282
  const result = await generateText({
1062
1283
  model: this.model,
1063
1284
  system: options?.system,
@@ -1075,7 +1296,7 @@ var SdkBackend = class {
1075
1296
  }
1076
1297
  async isAvailable() {
1077
1298
  try {
1078
- if (!this.model) this.model = await createModelAsync(this.modelId);
1299
+ if (!this.model) this.model = this.provider ? await createModelWithProvider(this.modelId, this.provider) : await createModelAsync(this.modelId);
1079
1300
  return true;
1080
1301
  } catch {
1081
1302
  return false;
@@ -1133,10 +1354,14 @@ function createBackend(config) {
1133
1354
  };
1134
1355
  const model = getModelForBackend(normalized.model, normalized.type);
1135
1356
  switch (normalized.type) {
1136
- case "default": return new SdkBackend({
1137
- model,
1138
- maxTokens: normalized.maxTokens
1139
- });
1357
+ case "default": {
1358
+ const provider = normalized.provider;
1359
+ return new SdkBackend({
1360
+ model,
1361
+ maxTokens: normalized.maxTokens,
1362
+ provider
1363
+ });
1364
+ }
1140
1365
  case "claude": return new ClaudeCodeBackend({
1141
1366
  ...normalized.options,
1142
1367
  model
@@ -1149,6 +1374,10 @@ function createBackend(config) {
1149
1374
  ...normalized.options,
1150
1375
  model
1151
1376
  });
1377
+ case "opencode": return new OpenCodeBackend({
1378
+ ...normalized.options,
1379
+ model
1380
+ });
1152
1381
  default: throw new Error(`Unknown backend type: ${normalized.type}`);
1153
1382
  }
1154
1383
  }
@@ -1163,16 +1392,19 @@ async function checkBackends() {
1163
1392
  const claude = new ClaudeCodeBackend();
1164
1393
  const codex = new CodexBackend();
1165
1394
  const cursor = new CursorBackend();
1166
- const [claudeAvailable, codexAvailable, cursorAvailable] = await Promise.all([
1395
+ const opencode = new OpenCodeBackend();
1396
+ const [claudeAvailable, codexAvailable, cursorAvailable, opencodeAvailable] = await Promise.all([
1167
1397
  withTimeout(claude.isAvailable(), 3e3),
1168
1398
  withTimeout(codex.isAvailable(), 3e3),
1169
- withTimeout(cursor.isAvailable(), 3e3)
1399
+ withTimeout(cursor.isAvailable(), 3e3),
1400
+ withTimeout(opencode.isAvailable(), 3e3)
1170
1401
  ]);
1171
1402
  return {
1172
1403
  default: true,
1173
1404
  claude: claudeAvailable,
1174
1405
  codex: codexAvailable,
1175
1406
  cursor: cursorAvailable,
1407
+ opencode: opencodeAvailable,
1176
1408
  mock: true
1177
1409
  };
1178
1410
  }
@@ -1201,9 +1433,14 @@ async function listBackends() {
1201
1433
  type: "cursor",
1202
1434
  available: availability.cursor,
1203
1435
  name: "Cursor Agent CLI"
1436
+ },
1437
+ {
1438
+ type: "opencode",
1439
+ available: availability.opencode,
1440
+ name: "OpenCode CLI"
1204
1441
  }
1205
1442
  ];
1206
1443
  }
1207
1444
 
1208
1445
  //#endregion
1209
- export { createModelAsync as A, SDK_MODEL_ALIASES as C, FRONTIER_MODELS as D, parseModel as E, SUPPORTED_PROVIDERS as O, CURSOR_MODEL_MAP as S, normalizeBackendType as T, execWithIdleTimeout as _, createMockBackend as a, CLAUDE_MODEL_MAP as b, CodexBackend as c, codexAdapter as d, createStreamParser as f, IdleTimeoutError as g, formatEvent as h, MockAIBackend as i, getDefaultModel as j, createModel as k, ClaudeCodeBackend as l, extractCodexResult as m, createBackend as n, SdkBackend as o, extractClaudeResult as p, listBackends as r, CursorBackend as s, checkBackends as t, claudeAdapter as u, DEFAULT_IDLE_TIMEOUT as v, getModelForBackend as w, CODEX_MODEL_MAP as x, BACKEND_DEFAULT_MODELS as y };
1446
+ export { parseModel as A, CLAUDE_MODEL_MAP as C, SDK_MODEL_ALIASES as D, OPENCODE_MODEL_MAP as E, createModelWithProvider as F, getDefaultModel as I, SUPPORTED_PROVIDERS as M, createModel as N, getModelForBackend as O, createModelAsync as P, BACKEND_DEFAULT_MODELS as S, CURSOR_MODEL_MAP as T, extractCodexResult as _, createMockBackend as a, execWithIdleTimeout as b, extractOpenCodeResult as c, CodexBackend as d, ClaudeCodeBackend as f, extractClaudeResult as g, createStreamParser as h, MockAIBackend as i, FRONTIER_MODELS as j, normalizeBackendType as k, opencodeAdapter as l, codexAdapter as m, createBackend as n, SdkBackend as o, claudeAdapter as p, listBackends as r, OpenCodeBackend as s, checkBackends as t, CursorBackend as u, formatEvent as v, CODEX_MODEL_MAP as w, DEFAULT_IDLE_TIMEOUT as x, IdleTimeoutError as y };