agent-worker 0.14.0 → 0.15.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.
@@ -1,8 +1,7 @@
1
1
  import { gateway, generateText } from "ai";
2
2
  import { execa } from "execa";
3
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { stringify } from "yaml";
6
5
 
7
6
  //#region src/agent/models.ts
8
7
  const providerCache = {};
@@ -119,12 +118,21 @@ async function createModelWithProvider(modelName, provider) {
119
118
  * Examples: anthropic:claude-sonnet-4-5, openai:gpt-5.2, deepseek:deepseek-chat
120
119
  */
121
120
  function createModel(modelId) {
122
- if (modelId.includes("/")) return gateway(modelId);
121
+ if (modelId.includes("/")) {
122
+ if (process.env.AI_GATEWAY_API_KEY) return gateway(modelId);
123
+ const slashIndex = modelId.indexOf("/");
124
+ const provider = modelId.slice(0, slashIndex);
125
+ const modelName = modelId.slice(slashIndex + 1);
126
+ if (provider in providerCache && providerCache[provider]) return providerCache[provider](modelName);
127
+ throw new Error(`Provider '${provider}' not loaded. Call createModelAsync() first or set AI_GATEWAY_API_KEY for gateway access.`);
128
+ }
123
129
  if (!modelId.includes(":")) {
124
130
  const provider = modelId;
125
131
  if (provider in FRONTIER_MODELS) {
126
132
  const defaultModel = FRONTIER_MODELS[provider][0];
127
- return gateway(`${provider}/${defaultModel}`);
133
+ if (process.env.AI_GATEWAY_API_KEY) return gateway(`${provider}/${defaultModel}`);
134
+ if (provider in providerCache && providerCache[provider]) return providerCache[provider](defaultModel);
135
+ throw new Error(`Provider '${provider}' not loaded. Call createModelAsync() first or set AI_GATEWAY_API_KEY for gateway access.`);
128
136
  }
129
137
  throw new Error(`Unknown provider: ${modelId}. Supported: ${Object.keys(FRONTIER_MODELS).join(", ")}`);
130
138
  }
@@ -133,19 +141,24 @@ function createModel(modelId) {
133
141
  const modelName = modelId.slice(colonIndex + 1);
134
142
  if (!modelName) throw new Error(`Invalid model identifier: ${modelId}. Model name is required.`);
135
143
  if (provider in providerCache && providerCache[provider]) return providerCache[provider](modelName);
136
- throw new Error(`Provider '${provider}' not loaded. Use gateway format (${provider}/${modelName}) or call createModelAsync() for direct provider access.`);
144
+ throw new Error(`Provider '${provider}' not loaded. Call createModelAsync() first or set AI_GATEWAY_API_KEY for gateway access.`);
137
145
  }
138
146
  /**
139
147
  * Async version of createModel - supports lazy loading of direct providers
140
148
  * Use this when you need direct provider access (provider:model format)
141
149
  */
142
150
  async function createModelAsync(modelId) {
143
- if (modelId.includes("/")) return gateway(modelId);
151
+ if (modelId.includes("/")) {
152
+ if (process.env.AI_GATEWAY_API_KEY) return gateway(modelId);
153
+ const slashIndex = modelId.indexOf("/");
154
+ return loadProviderModel(modelId.slice(0, slashIndex), modelId.slice(slashIndex + 1));
155
+ }
144
156
  if (!modelId.includes(":")) {
145
157
  const provider = modelId;
146
158
  if (provider in FRONTIER_MODELS) {
147
159
  const defaultModel = FRONTIER_MODELS[provider][0];
148
- return gateway(`${provider}/${defaultModel}`);
160
+ if (process.env.AI_GATEWAY_API_KEY) return gateway(`${provider}/${defaultModel}`);
161
+ return loadProviderModel(provider, defaultModel);
149
162
  }
150
163
  throw new Error(`Unknown provider: ${modelId}. Supported: ${Object.keys(FRONTIER_MODELS).join(", ")}`);
151
164
  }
@@ -153,6 +166,13 @@ async function createModelAsync(modelId) {
153
166
  const provider = modelId.slice(0, colonIndex);
154
167
  const modelName = modelId.slice(colonIndex + 1);
155
168
  if (!modelName) throw new Error(`Invalid model identifier: ${modelId}. Model name is required.`);
169
+ return loadProviderModel(provider, modelName);
170
+ }
171
+ /**
172
+ * Load a provider SDK and create a model instance.
173
+ * Used as fallback when AI Gateway is not available.
174
+ */
175
+ async function loadProviderModel(provider, modelName) {
156
176
  const config = PROVIDER_PACKAGES[provider];
157
177
  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
178
  const providerFn = await loadProvider(provider, config.package, config.export);
@@ -210,6 +230,165 @@ const FRONTIER_MODELS = {
210
230
  ],
211
231
  xai: ["grok-4", "grok-4-fast-reasoning"]
212
232
  };
233
+ /**
234
+ * Environment variable that each provider uses for authentication.
235
+ * Ordered by priority — first match wins during auto-discovery.
236
+ *
237
+ * Gateway is first because it supports all providers via a single key.
238
+ */
239
+ const PROVIDER_ENV_KEYS = {
240
+ gateway: "AI_GATEWAY_API_KEY",
241
+ anthropic: "ANTHROPIC_API_KEY",
242
+ openai: "OPENAI_API_KEY",
243
+ deepseek: "DEEPSEEK_API_KEY",
244
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
245
+ groq: "GROQ_API_KEY",
246
+ mistral: "MISTRAL_API_KEY",
247
+ xai: "XAI_API_KEY"
248
+ };
249
+ /** Provider discovery priority order */
250
+ const DISCOVERY_ORDER = [
251
+ "gateway",
252
+ "anthropic",
253
+ "openai",
254
+ "deepseek",
255
+ "google",
256
+ "groq",
257
+ "mistral",
258
+ "xai"
259
+ ];
260
+ /**
261
+ * Reverse map: model name → provider name.
262
+ * Built from FRONTIER_MODELS so "deepseek-chat" → "deepseek", etc.
263
+ */
264
+ const MODEL_TO_PROVIDER = {};
265
+ for (const [provider, models] of Object.entries(FRONTIER_MODELS)) for (const model of models) {
266
+ const shortName = model.includes("/") ? model.split("/").pop() : model;
267
+ MODEL_TO_PROVIDER[model] = provider;
268
+ if (shortName !== model) MODEL_TO_PROVIDER[shortName] = provider;
269
+ }
270
+ /**
271
+ * Discover the best available provider by scanning environment variables.
272
+ *
273
+ * Note: This function does NOT read AGENT_MODEL — that's handled by
274
+ * resolveModelFallback() which supports comma-separated fallback chains.
275
+ *
276
+ * @param options.preferredModel - If set, prefer the provider that owns this model.
277
+ * E.g. "deepseek-chat" → prefer "deepseek" if DEEPSEEK_API_KEY is set.
278
+ * @param options.env - Environment to scan (defaults to process.env).
279
+ * @returns The discovered provider and model, or null if none available.
280
+ */
281
+ function discoverProvider(options) {
282
+ const env = options?.env ?? process.env;
283
+ const preferredModel = options?.preferredModel;
284
+ if (preferredModel && preferredModel !== "auto") {
285
+ const ownerProvider = MODEL_TO_PROVIDER[preferredModel];
286
+ if (ownerProvider) {
287
+ const envKey = PROVIDER_ENV_KEYS[ownerProvider];
288
+ if (envKey && env[envKey]) return {
289
+ provider: ownerProvider,
290
+ model: preferredModel.includes("/") ? preferredModel : `${ownerProvider}/${preferredModel}`
291
+ };
292
+ }
293
+ }
294
+ for (const provider of DISCOVERY_ORDER) {
295
+ if (!env[PROVIDER_ENV_KEYS[provider]]) continue;
296
+ if (provider === "gateway") {
297
+ if (preferredModel && preferredModel !== "auto") return {
298
+ provider: "gateway",
299
+ model: `${MODEL_TO_PROVIDER[preferredModel] || "anthropic"}/${preferredModel}`
300
+ };
301
+ return {
302
+ provider: "gateway",
303
+ model: getDefaultModel()
304
+ };
305
+ }
306
+ const defaultModel = FRONTIER_MODELS[provider]?.[0];
307
+ return {
308
+ provider,
309
+ model: defaultModel ? defaultModel.includes("/") ? defaultModel : `${provider}/${defaultModel}` : provider
310
+ };
311
+ }
312
+ return null;
313
+ }
314
+ /**
315
+ * Check if a value is the "auto" sentinel.
316
+ */
317
+ function isAutoProvider(value) {
318
+ return value === "auto";
319
+ }
320
+ /**
321
+ * Check if a model's provider has a valid API key in the environment.
322
+ */
323
+ function isModelAvailable(model, env) {
324
+ if (model === "auto") return true;
325
+ let provider;
326
+ provider = MODEL_TO_PROVIDER[model];
327
+ if (!provider && model.includes("/")) provider = model.split("/")[0];
328
+ if (!provider && model.includes(":")) provider = model.split(":")[0];
329
+ if (!provider) return false;
330
+ if (env[PROVIDER_ENV_KEYS["gateway"]]) return true;
331
+ const envKey = PROVIDER_ENV_KEYS[provider];
332
+ return !!envKey && !!env[envKey];
333
+ }
334
+ /**
335
+ * Resolve a model to a single concrete value, supporting fallback chains.
336
+ *
337
+ * Resolution order:
338
+ * 1. AGENT_DEFAULT_MODELS env var — comma-separated preference list
339
+ * (e.g. "deepseek-chat, anthropic/claude-sonnet-4-5")
340
+ * 2. Workflow YAML model field (single string, or "auto")
341
+ * 3. Full auto-discovery — scan all provider API keys
342
+ *
343
+ * The preference list does NOT contain "auto" — the env var itself IS
344
+ * the auto configuration. After exhausting the explicit list, the system
345
+ * implicitly falls back to full provider discovery.
346
+ *
347
+ * Example:
348
+ * AGENT_DEFAULT_MODELS="deepseek-chat, anthropic/claude-sonnet-4-5"
349
+ * → try deepseek-chat (need DEEPSEEK_API_KEY)
350
+ * → try claude-sonnet-4-5 (need ANTHROPIC_API_KEY)
351
+ * → implicit fallback: discover any available provider
352
+ *
353
+ * @returns Resolved { model, provider } — never contains "auto".
354
+ * @throws if nothing is available (no explicit candidate and no provider key).
355
+ */
356
+ function resolveModelFallback(config) {
357
+ const env = config.env ?? process.env;
358
+ const isProviderAuto = config.provider === "auto";
359
+ const autoModel = env.AGENT_DEFAULT_MODELS;
360
+ if (!isProviderAuto && config.model && config.model !== "auto" && !autoModel) return {
361
+ model: config.model,
362
+ provider: config.provider
363
+ };
364
+ const preferences = autoModel ? autoModel.split(",").map((s) => s.trim()).filter(Boolean) : [];
365
+ for (const candidate of preferences) if (isModelAvailable(candidate, env)) return {
366
+ model: candidate,
367
+ provider: isProviderAuto ? void 0 : config.provider
368
+ };
369
+ if (isProviderAuto && config.model && config.model !== "auto") {
370
+ const model = config.model;
371
+ if (isModelAvailable(model, env)) {
372
+ const ownerProvider = MODEL_TO_PROVIDER[model];
373
+ if (ownerProvider && !model.includes("/") && !model.includes(":")) return {
374
+ model: `${ownerProvider}/${model}`,
375
+ provider: void 0
376
+ };
377
+ return {
378
+ model,
379
+ provider: void 0
380
+ };
381
+ }
382
+ }
383
+ const discovered = discoverProvider({ env });
384
+ if (discovered) return {
385
+ model: discovered.model,
386
+ provider: void 0
387
+ };
388
+ const envVars = Object.values(PROVIDER_ENV_KEYS).join(", ");
389
+ const hint = preferences.length > 0 ? `Tried: ${preferences.join(", ")}. ` : "";
390
+ throw new Error(`No provider available for auto model resolution. ${hint}Set one of: ${envVars}`);
391
+ }
213
392
 
214
393
  //#endregion
215
394
  //#region src/backends/model-maps.ts
@@ -359,77 +538,13 @@ const DEFAULT_IDLE_TIMEOUT = 6e5;
359
538
  * This catches unresponsive backends (e.g., nested `claude -p` inside Claude Code).
360
539
  */
361
540
  const DEFAULT_STARTUP_TIMEOUT = 3e4;
362
- /**
363
- * Execute a command with idle timeout.
364
- *
365
- * The timeout resets every time the process writes to stdout or stderr.
366
- * If the process goes silent for longer than `timeout` ms, it's killed.
367
- */
368
541
  /** Minimum idle timeout to prevent accidental instant kills */
369
542
  const MIN_TIMEOUT_MS = 1e3;
370
- async function execWithIdleTimeout(options) {
371
- const { command, args, cwd, onStdout } = options;
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;
375
- let idleTimedOut = false;
376
- let hasReceivedOutput = false;
377
- let timer;
378
- let stdout = "";
379
- let stderr = "";
380
- const subprocess = execa(command, args, {
381
- cwd,
382
- stdin: "ignore",
383
- buffer: false
384
- });
385
- const resetTimer = () => {
386
- clearTimeout(timer);
387
- timer = setTimeout(() => {
388
- idleTimedOut = true;
389
- subprocess.kill();
390
- }, timeout);
391
- };
392
- subprocess.stdout?.on("data", (chunk) => {
393
- const text = chunk.toString();
394
- stdout += text;
395
- hasReceivedOutput = true;
396
- resetTimer();
397
- if (onStdout) try {
398
- onStdout(text);
399
- } catch (err) {
400
- console.error("onStdout callback error:", err);
401
- }
402
- });
403
- subprocess.stderr?.on("data", (chunk) => {
404
- stderr += chunk.toString();
405
- hasReceivedOutput = true;
406
- resetTimer();
407
- });
408
- if (startupTimeout > 0) timer = setTimeout(() => {
409
- if (!hasReceivedOutput) {
410
- idleTimedOut = true;
411
- subprocess.kill();
412
- }
413
- }, startupTimeout);
414
- else resetTimer();
415
- try {
416
- await subprocess;
417
- clearTimeout(timer);
418
- return {
419
- stdout: stdout.trimEnd(),
420
- stderr: stderr.trimEnd()
421
- };
422
- } catch (error) {
423
- clearTimeout(timer);
424
- if (idleTimedOut) throw new IdleTimeoutError(hasReceivedOutput ? timeout : startupTimeout, stdout, stderr);
425
- throw error;
426
- }
427
- }
428
543
  /**
429
- * Execute a command with idle timeout and return abort controller
430
- * This version returns both the promise and an abort function for external control
544
+ * Core implementation shared by both sync and abortable variants.
545
+ * Returns the promise, an abort function, and the subprocess handle.
431
546
  */
432
- function execWithIdleTimeoutAbortable(options) {
547
+ function execWithIdleTimeoutInternal(options) {
433
548
  const { command, args, cwd, onStdout } = options;
434
549
  const timeout = Math.max(options.timeout, MIN_TIMEOUT_MS);
435
550
  const rawStartup = options.startupTimeout !== void 0 ? options.startupTimeout : DEFAULT_STARTUP_TIMEOUT;
@@ -505,6 +620,23 @@ function execWithIdleTimeoutAbortable(options) {
505
620
  };
506
621
  }
507
622
  /**
623
+ * Execute a command with idle timeout.
624
+ *
625
+ * The timeout resets every time the process writes to stdout or stderr.
626
+ * If the process goes silent for longer than `timeout` ms, it's killed.
627
+ */
628
+ async function execWithIdleTimeout(options) {
629
+ const { promise } = execWithIdleTimeoutInternal(options);
630
+ return promise;
631
+ }
632
+ /**
633
+ * Execute a command with idle timeout and return abort handle.
634
+ * This version returns both the promise and an abort function for external control.
635
+ */
636
+ function execWithIdleTimeoutAbortable(options) {
637
+ return execWithIdleTimeoutInternal(options);
638
+ }
639
+ /**
508
640
  * Error thrown when a process is killed due to idle timeout
509
641
  */
510
642
  var IdleTimeoutError = class extends Error {
@@ -520,6 +652,45 @@ var IdleTimeoutError = class extends Error {
520
652
  }
521
653
  };
522
654
 
655
+ //#endregion
656
+ //#region src/backends/cli-helpers.ts
657
+ /**
658
+ * Shared helpers for CLI backends
659
+ *
660
+ * Eliminates duplicated error handling and availability check patterns
661
+ * across claude-code, codex, cursor, and opencode backends.
662
+ */
663
+ /**
664
+ * Handle errors from CLI backend execution.
665
+ *
666
+ * Standardizes the error handling pattern shared by all CLI backends:
667
+ * 1. IdleTimeoutError → human-readable timeout message
668
+ * 2. Process exit error → include exit code and stderr
669
+ * 3. Everything else → re-throw
670
+ */
671
+ function handleCliBackendError(error, backendName, timeout) {
672
+ if (error instanceof IdleTimeoutError) throw new Error(`${backendName} timed out after ${timeout}ms of inactivity`);
673
+ if (error && typeof error === "object" && "exitCode" in error) {
674
+ const execError = error;
675
+ throw new Error(`${backendName} failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
676
+ }
677
+ throw error;
678
+ }
679
+ /**
680
+ * Check if a CLI command is available by running `command --version`.
681
+ */
682
+ async function checkCliAvailable(command, args = ["--version"], timeout = 5e3) {
683
+ try {
684
+ await execa(command, args, {
685
+ stdin: "ignore",
686
+ timeout
687
+ });
688
+ return true;
689
+ } catch {
690
+ return false;
691
+ }
692
+ }
693
+
523
694
  //#endregion
524
695
  //#region src/backends/stream-json.ts
525
696
  /**
@@ -786,7 +957,8 @@ function formatToolInput(input) {
786
957
  *
787
958
  * MCP Configuration:
788
959
  * Claude supports per-invocation MCP config via --mcp-config flag.
789
- * Use setWorkspace() for workspace isolation, or setMcpConfigPath() directly.
960
+ * The loop writes mcp-config.json to the workspace; this backend
961
+ * auto-discovers it when workspace is set.
790
962
  *
791
963
  * @see https://docs.anthropic.com/en/docs/claude-code
792
964
  */
@@ -800,17 +972,6 @@ var ClaudeCodeBackend = class {
800
972
  ...options
801
973
  };
802
974
  }
803
- /**
804
- * Set up workspace directory with MCP config
805
- * Claude uses --mcp-config flag, so we just write the config file
806
- */
807
- setWorkspace(workspaceDir, mcpConfig) {
808
- this.options.workspace = workspaceDir;
809
- if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
810
- const mcpConfigPath = join(workspaceDir, "mcp-config.json");
811
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
812
- this.options.mcpConfigPath = mcpConfigPath;
813
- }
814
975
  async send(message, options) {
815
976
  const args = this.buildArgs(message, options);
816
977
  const cwd = this.options.workspace || this.options.cwd;
@@ -853,15 +1014,7 @@ var ClaudeCodeBackend = class {
853
1014
  }
854
1015
  }
855
1016
  async isAvailable() {
856
- try {
857
- await execa("claude", ["--version"], {
858
- stdin: "ignore",
859
- timeout: 5e3
860
- });
861
- return true;
862
- } catch {
863
- return false;
864
- }
1017
+ return checkCliAvailable("claude");
865
1018
  }
866
1019
  getInfo() {
867
1020
  return {
@@ -886,16 +1039,14 @@ var ClaudeCodeBackend = class {
886
1039
  if (outputFormat === "stream-json") args.push("--verbose");
887
1040
  if (this.options.continue) args.push("--continue");
888
1041
  if (this.options.resume) args.push("--resume", this.options.resume);
889
- if (this.options.mcpConfigPath) args.push("--mcp-config", this.options.mcpConfigPath);
1042
+ const mcpConfigPath = this.options.mcpConfigPath ?? (this.options.workspace ? (() => {
1043
+ const p = join(this.options.workspace, "mcp-config.json");
1044
+ return existsSync(p) ? p : void 0;
1045
+ })() : void 0);
1046
+ if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath);
890
1047
  return args;
891
1048
  }
892
1049
  /**
893
- * Set MCP config path (for workflow integration)
894
- */
895
- setMcpConfigPath(path) {
896
- this.options.mcpConfigPath = path;
897
- }
898
- /**
899
1050
  * Abort any running claude process
900
1051
  */
901
1052
  abort() {
@@ -908,16 +1059,6 @@ var ClaudeCodeBackend = class {
908
1059
 
909
1060
  //#endregion
910
1061
  //#region src/backends/codex.ts
911
- /**
912
- * OpenAI Codex CLI backend
913
- * Uses `codex exec` for non-interactive mode with JSON event output
914
- *
915
- * MCP Configuration:
916
- * Codex uses project-level MCP config. Use setWorkspace() to set up
917
- * a dedicated workspace directory with .codex/config.yaml for MCP settings.
918
- *
919
- * @see https://github.com/openai/codex
920
- */
921
1062
  var CodexBackend = class {
922
1063
  type = "codex";
923
1064
  options;
@@ -927,17 +1068,6 @@ var CodexBackend = class {
927
1068
  ...options
928
1069
  };
929
1070
  }
930
- /**
931
- * Set up workspace directory with MCP config
932
- * Creates .codex/config.yaml in the workspace with MCP server config
933
- */
934
- setWorkspace(workspaceDir, mcpConfig) {
935
- this.options.workspace = workspaceDir;
936
- const codexDir = join(workspaceDir, ".codex");
937
- if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
938
- const codexConfig = { mcp_servers: mcpConfig.mcpServers };
939
- writeFileSync(join(codexDir, "config.yaml"), stringify(codexConfig));
940
- }
941
1071
  async send(message, _options) {
942
1072
  const args = this.buildArgs(message);
943
1073
  const cwd = this.options.workspace || this.options.cwd;
@@ -952,24 +1082,11 @@ var CodexBackend = class {
952
1082
  });
953
1083
  return extractCodexResult(stdout);
954
1084
  } catch (error) {
955
- if (error instanceof IdleTimeoutError) throw new Error(`codex timed out after ${timeout}ms of inactivity`);
956
- if (error && typeof error === "object" && "exitCode" in error) {
957
- const execError = error;
958
- throw new Error(`codex failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
959
- }
960
- throw error;
1085
+ handleCliBackendError(error, "codex", timeout);
961
1086
  }
962
1087
  }
963
1088
  async isAvailable() {
964
- try {
965
- await execa("codex", ["--version"], {
966
- stdin: "ignore",
967
- timeout: 5e3
968
- });
969
- return true;
970
- } catch {
971
- return false;
972
- }
1089
+ return checkCliAvailable("codex");
973
1090
  }
974
1091
  getInfo() {
975
1092
  return {
@@ -993,16 +1110,6 @@ var CodexBackend = class {
993
1110
 
994
1111
  //#endregion
995
1112
  //#region src/backends/cursor.ts
996
- /**
997
- * Cursor CLI backend
998
- * Uses `cursor agent -p` for non-interactive mode with stream-json output
999
- *
1000
- * MCP Configuration:
1001
- * Cursor uses project-level MCP config via .cursor/mcp.json in the workspace.
1002
- * Use setWorkspace() to set up a dedicated workspace with MCP config.
1003
- *
1004
- * @see https://docs.cursor.com/context/model-context-protocol
1005
- */
1006
1113
  var CursorBackend = class {
1007
1114
  type = "cursor";
1008
1115
  options;
@@ -1019,16 +1126,6 @@ var CursorBackend = class {
1019
1126
  ...options
1020
1127
  };
1021
1128
  }
1022
- /**
1023
- * Set up workspace directory with MCP config
1024
- * Creates .cursor/mcp.json in the workspace
1025
- */
1026
- setWorkspace(workspaceDir, mcpConfig) {
1027
- this.options.workspace = workspaceDir;
1028
- const cursorDir = join(workspaceDir, ".cursor");
1029
- if (!existsSync(cursorDir)) mkdirSync(cursorDir, { recursive: true });
1030
- writeFileSync(join(cursorDir, "mcp.json"), JSON.stringify(mcpConfig, null, 2));
1031
- }
1032
1129
  async send(message, _options) {
1033
1130
  const { command, args } = await this.buildCommand(message);
1034
1131
  const cwd = this.options.workspace || this.options.cwd;
@@ -1043,12 +1140,7 @@ var CursorBackend = class {
1043
1140
  });
1044
1141
  return extractClaudeResult(stdout);
1045
1142
  } catch (error) {
1046
- if (error instanceof IdleTimeoutError) throw new Error(`cursor agent timed out after ${timeout}ms of inactivity`);
1047
- if (error && typeof error === "object" && "exitCode" in error) {
1048
- const execError = error;
1049
- throw new Error(`cursor agent failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
1050
- }
1051
- throw error;
1143
+ handleCliBackendError(error, "cursor agent", timeout);
1052
1144
  }
1053
1145
  }
1054
1146
  async isAvailable() {
@@ -1069,22 +1161,14 @@ var CursorBackend = class {
1069
1161
  */
1070
1162
  async resolveStyle() {
1071
1163
  if (this.resolvedStyle !== null) return this.resolvedStyle;
1072
- try {
1073
- await execa("cursor", ["agent", "--version"], {
1074
- stdin: "ignore",
1075
- timeout: 2e3
1076
- });
1164
+ if (await checkCliAvailable("cursor", ["agent", "--version"], 2e3)) {
1077
1165
  this.resolvedStyle = "subcommand";
1078
1166
  return "subcommand";
1079
- } catch {}
1080
- try {
1081
- await execa("agent", ["--version"], {
1082
- stdin: "ignore",
1083
- timeout: 2e3
1084
- });
1167
+ }
1168
+ if (await checkCliAvailable("agent", ["--version"], 2e3)) {
1085
1169
  this.resolvedStyle = "direct";
1086
1170
  return "direct";
1087
- } catch {}
1171
+ }
1088
1172
  return null;
1089
1173
  }
1090
1174
  async buildCommand(message) {
@@ -1111,16 +1195,6 @@ var CursorBackend = class {
1111
1195
 
1112
1196
  //#endregion
1113
1197
  //#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
1198
  var OpenCodeBackend = class {
1125
1199
  type = "opencode";
1126
1200
  options;
@@ -1130,29 +1204,6 @@ var OpenCodeBackend = class {
1130
1204
  ...options
1131
1205
  };
1132
1206
  }
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
1207
  async send(message, _options) {
1157
1208
  const args = this.buildArgs(message);
1158
1209
  const cwd = this.options.workspace || this.options.cwd;
@@ -1167,24 +1218,11 @@ var OpenCodeBackend = class {
1167
1218
  });
1168
1219
  return extractOpenCodeResult(stdout);
1169
1220
  } 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;
1221
+ handleCliBackendError(error, "opencode", timeout);
1176
1222
  }
1177
1223
  }
1178
1224
  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
- }
1225
+ return checkCliAvailable("opencode");
1188
1226
  }
1189
1227
  getInfo() {
1190
1228
  return {
@@ -1316,8 +1354,8 @@ var SdkBackend = class {
1316
1354
  * Mock AI Backend for testing
1317
1355
  *
1318
1356
  * In single-agent mode, provides a simple echo send().
1319
- * In workflow mode, the controller handles MCP tool orchestration
1320
- * via the mock runner strategy (controller/mock-runner.ts).
1357
+ * In workflow mode, the loop handles MCP tool orchestration
1358
+ * via the mock runner strategy (loop/mock-runner.ts).
1321
1359
  */
1322
1360
  var MockAIBackend = class {
1323
1361
  type = "mock";
@@ -1443,4 +1481,4 @@ async function listBackends() {
1443
1481
  }
1444
1482
 
1445
1483
  //#endregion
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 };
1484
+ 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, isAutoProvider as L, SUPPORTED_PROVIDERS as M, createModel as N, getModelForBackend as O, createModelAsync as P, resolveModelFallback as R, 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 };