agent-worker 0.13.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
@@ -354,68 +533,24 @@ const DEFAULT_IDLE_TIMEOUT = 6e5;
354
533
  * producing output (tool calls, analysis, etc.).
355
534
  */
356
535
  /**
357
- * Execute a command with idle timeout.
358
- *
359
- * The timeout resets every time the process writes to stdout or stderr.
360
- * If the process goes silent for longer than `timeout` ms, it's killed.
536
+ * Default startup timeout (30 seconds).
537
+ * If the process produces zero output within this window, it's killed.
538
+ * This catches unresponsive backends (e.g., nested `claude -p` inside Claude Code).
361
539
  */
540
+ const DEFAULT_STARTUP_TIMEOUT = 3e4;
362
541
  /** Minimum idle timeout to prevent accidental instant kills */
363
542
  const MIN_TIMEOUT_MS = 1e3;
364
- async function execWithIdleTimeout(options) {
365
- const { command, args, cwd, onStdout } = options;
366
- const timeout = Math.max(options.timeout, MIN_TIMEOUT_MS);
367
- let idleTimedOut = false;
368
- let timer;
369
- let stdout = "";
370
- let stderr = "";
371
- const subprocess = execa(command, args, {
372
- cwd,
373
- stdin: "ignore",
374
- buffer: false
375
- });
376
- const resetTimer = () => {
377
- clearTimeout(timer);
378
- timer = setTimeout(() => {
379
- idleTimedOut = true;
380
- subprocess.kill();
381
- }, timeout);
382
- };
383
- subprocess.stdout?.on("data", (chunk) => {
384
- const text = chunk.toString();
385
- stdout += text;
386
- resetTimer();
387
- if (onStdout) try {
388
- onStdout(text);
389
- } catch (err) {
390
- console.error("onStdout callback error:", err);
391
- }
392
- });
393
- subprocess.stderr?.on("data", (chunk) => {
394
- stderr += chunk.toString();
395
- resetTimer();
396
- });
397
- resetTimer();
398
- try {
399
- await subprocess;
400
- clearTimeout(timer);
401
- return {
402
- stdout: stdout.trimEnd(),
403
- stderr: stderr.trimEnd()
404
- };
405
- } catch (error) {
406
- clearTimeout(timer);
407
- if (idleTimedOut) throw new IdleTimeoutError(timeout, stdout, stderr);
408
- throw error;
409
- }
410
- }
411
543
  /**
412
- * Execute a command with idle timeout and return abort controller
413
- * 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.
414
546
  */
415
- function execWithIdleTimeoutAbortable(options) {
547
+ function execWithIdleTimeoutInternal(options) {
416
548
  const { command, args, cwd, onStdout } = options;
417
549
  const timeout = Math.max(options.timeout, MIN_TIMEOUT_MS);
550
+ const rawStartup = options.startupTimeout !== void 0 ? options.startupTimeout : DEFAULT_STARTUP_TIMEOUT;
551
+ const startupTimeout = rawStartup > 0 ? Math.min(rawStartup, timeout) : 0;
418
552
  let idleTimedOut = false;
553
+ let hasReceivedOutput = false;
419
554
  let timer;
420
555
  let stdout = "";
421
556
  let stderr = "";
@@ -435,6 +570,7 @@ function execWithIdleTimeoutAbortable(options) {
435
570
  subprocess.stdout?.on("data", (chunk) => {
436
571
  const text = chunk.toString();
437
572
  stdout += text;
573
+ hasReceivedOutput = true;
438
574
  resetTimer();
439
575
  if (onStdout) try {
440
576
  onStdout(text);
@@ -444,9 +580,16 @@ function execWithIdleTimeoutAbortable(options) {
444
580
  });
445
581
  subprocess.stderr?.on("data", (chunk) => {
446
582
  stderr += chunk.toString();
583
+ hasReceivedOutput = true;
447
584
  resetTimer();
448
585
  });
449
- resetTimer();
586
+ if (startupTimeout > 0) timer = setTimeout(() => {
587
+ if (!hasReceivedOutput) {
588
+ idleTimedOut = true;
589
+ subprocess.kill();
590
+ }
591
+ }, startupTimeout);
592
+ else resetTimer();
450
593
  const abort = () => {
451
594
  if (!isAborted) {
452
595
  isAborted = true;
@@ -469,7 +612,7 @@ function execWithIdleTimeoutAbortable(options) {
469
612
  } catch (error) {
470
613
  clearTimeout(timer);
471
614
  if (isAborted) throw new Error("Process aborted by user");
472
- if (idleTimedOut) throw new IdleTimeoutError(timeout, stdout, stderr);
615
+ if (idleTimedOut) throw new IdleTimeoutError(hasReceivedOutput ? timeout : startupTimeout, stdout, stderr);
473
616
  throw error;
474
617
  }
475
618
  })(),
@@ -477,6 +620,23 @@ function execWithIdleTimeoutAbortable(options) {
477
620
  };
478
621
  }
479
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
+ /**
480
640
  * Error thrown when a process is killed due to idle timeout
481
641
  */
482
642
  var IdleTimeoutError = class extends Error {
@@ -492,6 +652,45 @@ var IdleTimeoutError = class extends Error {
492
652
  }
493
653
  };
494
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
+
495
694
  //#endregion
496
695
  //#region src/backends/stream-json.ts
497
696
  /**
@@ -758,7 +957,8 @@ function formatToolInput(input) {
758
957
  *
759
958
  * MCP Configuration:
760
959
  * Claude supports per-invocation MCP config via --mcp-config flag.
761
- * 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.
762
962
  *
763
963
  * @see https://docs.anthropic.com/en/docs/claude-code
764
964
  */
@@ -772,17 +972,6 @@ var ClaudeCodeBackend = class {
772
972
  ...options
773
973
  };
774
974
  }
775
- /**
776
- * Set up workspace directory with MCP config
777
- * Claude uses --mcp-config flag, so we just write the config file
778
- */
779
- setWorkspace(workspaceDir, mcpConfig) {
780
- this.options.workspace = workspaceDir;
781
- if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
782
- const mcpConfigPath = join(workspaceDir, "mcp-config.json");
783
- writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
784
- this.options.mcpConfigPath = mcpConfigPath;
785
- }
786
975
  async send(message, options) {
787
976
  const args = this.buildArgs(message, options);
788
977
  const cwd = this.options.workspace || this.options.cwd;
@@ -813,7 +1002,10 @@ var ClaudeCodeBackend = class {
813
1002
  return { content: stdout.trim() };
814
1003
  } catch (error) {
815
1004
  this.currentAbort = void 0;
816
- if (error instanceof IdleTimeoutError) throw new Error(`claude timed out after ${timeout}ms of inactivity`);
1005
+ if (error instanceof IdleTimeoutError) {
1006
+ 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.`);
1007
+ throw new Error(`claude timed out after ${timeout}ms of inactivity`);
1008
+ }
817
1009
  if (error && typeof error === "object" && "exitCode" in error) {
818
1010
  const execError = error;
819
1011
  throw new Error(`claude failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
@@ -822,15 +1014,7 @@ var ClaudeCodeBackend = class {
822
1014
  }
823
1015
  }
824
1016
  async isAvailable() {
825
- try {
826
- await execa("claude", ["--version"], {
827
- stdin: "ignore",
828
- timeout: 5e3
829
- });
830
- return true;
831
- } catch {
832
- return false;
833
- }
1017
+ return checkCliAvailable("claude");
834
1018
  }
835
1019
  getInfo() {
836
1020
  return {
@@ -855,16 +1039,14 @@ var ClaudeCodeBackend = class {
855
1039
  if (outputFormat === "stream-json") args.push("--verbose");
856
1040
  if (this.options.continue) args.push("--continue");
857
1041
  if (this.options.resume) args.push("--resume", this.options.resume);
858
- 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);
859
1047
  return args;
860
1048
  }
861
1049
  /**
862
- * Set MCP config path (for workflow integration)
863
- */
864
- setMcpConfigPath(path) {
865
- this.options.mcpConfigPath = path;
866
- }
867
- /**
868
1050
  * Abort any running claude process
869
1051
  */
870
1052
  abort() {
@@ -877,16 +1059,6 @@ var ClaudeCodeBackend = class {
877
1059
 
878
1060
  //#endregion
879
1061
  //#region src/backends/codex.ts
880
- /**
881
- * OpenAI Codex CLI backend
882
- * Uses `codex exec` for non-interactive mode with JSON event output
883
- *
884
- * MCP Configuration:
885
- * Codex uses project-level MCP config. Use setWorkspace() to set up
886
- * a dedicated workspace directory with .codex/config.yaml for MCP settings.
887
- *
888
- * @see https://github.com/openai/codex
889
- */
890
1062
  var CodexBackend = class {
891
1063
  type = "codex";
892
1064
  options;
@@ -896,17 +1068,6 @@ var CodexBackend = class {
896
1068
  ...options
897
1069
  };
898
1070
  }
899
- /**
900
- * Set up workspace directory with MCP config
901
- * Creates .codex/config.yaml in the workspace with MCP server config
902
- */
903
- setWorkspace(workspaceDir, mcpConfig) {
904
- this.options.workspace = workspaceDir;
905
- const codexDir = join(workspaceDir, ".codex");
906
- if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
907
- const codexConfig = { mcp_servers: mcpConfig.mcpServers };
908
- writeFileSync(join(codexDir, "config.yaml"), stringify(codexConfig));
909
- }
910
1071
  async send(message, _options) {
911
1072
  const args = this.buildArgs(message);
912
1073
  const cwd = this.options.workspace || this.options.cwd;
@@ -921,24 +1082,11 @@ var CodexBackend = class {
921
1082
  });
922
1083
  return extractCodexResult(stdout);
923
1084
  } catch (error) {
924
- if (error instanceof IdleTimeoutError) throw new Error(`codex timed out after ${timeout}ms of inactivity`);
925
- if (error && typeof error === "object" && "exitCode" in error) {
926
- const execError = error;
927
- throw new Error(`codex failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
928
- }
929
- throw error;
1085
+ handleCliBackendError(error, "codex", timeout);
930
1086
  }
931
1087
  }
932
1088
  async isAvailable() {
933
- try {
934
- await execa("codex", ["--version"], {
935
- stdin: "ignore",
936
- timeout: 5e3
937
- });
938
- return true;
939
- } catch {
940
- return false;
941
- }
1089
+ return checkCliAvailable("codex");
942
1090
  }
943
1091
  getInfo() {
944
1092
  return {
@@ -962,37 +1110,22 @@ var CodexBackend = class {
962
1110
 
963
1111
  //#endregion
964
1112
  //#region src/backends/cursor.ts
965
- /**
966
- * Cursor CLI backend
967
- * Uses `cursor agent -p` for non-interactive mode with stream-json output
968
- *
969
- * MCP Configuration:
970
- * Cursor uses project-level MCP config via .cursor/mcp.json in the workspace.
971
- * Use setWorkspace() to set up a dedicated workspace with MCP config.
972
- *
973
- * @see https://docs.cursor.com/context/model-context-protocol
974
- */
975
1113
  var CursorBackend = class {
976
1114
  type = "cursor";
977
1115
  options;
978
- /** Resolved command: "cursor" (subcommand style) */
979
- resolvedCommand = null;
1116
+ /**
1117
+ * Resolved command style:
1118
+ * - "subcommand": `cursor agent -p ...` (IDE-bundled CLI)
1119
+ * - "direct": `agent -p ...` (standalone install via cursor.com/install)
1120
+ * - null: not yet resolved
1121
+ */
1122
+ resolvedStyle = null;
980
1123
  constructor(options = {}) {
981
1124
  this.options = {
982
1125
  timeout: DEFAULT_IDLE_TIMEOUT,
983
1126
  ...options
984
1127
  };
985
1128
  }
986
- /**
987
- * Set up workspace directory with MCP config
988
- * Creates .cursor/mcp.json in the workspace
989
- */
990
- setWorkspace(workspaceDir, mcpConfig) {
991
- this.options.workspace = workspaceDir;
992
- const cursorDir = join(workspaceDir, ".cursor");
993
- if (!existsSync(cursorDir)) mkdirSync(cursorDir, { recursive: true });
994
- writeFileSync(join(cursorDir, "mcp.json"), JSON.stringify(mcpConfig, null, 2));
995
- }
996
1129
  async send(message, _options) {
997
1130
  const { command, args } = await this.buildCommand(message);
998
1131
  const cwd = this.options.workspace || this.options.cwd;
@@ -1007,16 +1140,11 @@ var CursorBackend = class {
1007
1140
  });
1008
1141
  return extractClaudeResult(stdout);
1009
1142
  } catch (error) {
1010
- if (error instanceof IdleTimeoutError) throw new Error(`cursor agent timed out after ${timeout}ms of inactivity`);
1011
- if (error && typeof error === "object" && "exitCode" in error) {
1012
- const execError = error;
1013
- throw new Error(`cursor agent failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
1014
- }
1015
- throw error;
1143
+ handleCliBackendError(error, "cursor agent", timeout);
1016
1144
  }
1017
1145
  }
1018
1146
  async isAvailable() {
1019
- return await this.resolveCommand() !== null;
1147
+ return await this.resolveStyle() !== null;
1020
1148
  }
1021
1149
  getInfo() {
1022
1150
  return {
@@ -1025,24 +1153,26 @@ var CursorBackend = class {
1025
1153
  };
1026
1154
  }
1027
1155
  /**
1028
- * Resolve which cursor command is available.
1029
- * Checks `cursor agent --version` (subcommand style).
1156
+ * Resolve which cursor command style is available.
1157
+ * Tries in order:
1158
+ * 1. `cursor agent --version` — IDE-bundled CLI (subcommand style)
1159
+ * 2. `agent --version` — standalone install via cursor.com/install (direct style)
1030
1160
  * Result is cached after first resolution.
1031
1161
  */
1032
- async resolveCommand() {
1033
- if (this.resolvedCommand !== null) return this.resolvedCommand;
1034
- try {
1035
- await execa("cursor", ["agent", "--version"], {
1036
- stdin: "ignore",
1037
- timeout: 2e3
1038
- });
1039
- this.resolvedCommand = "cursor";
1040
- return "cursor";
1041
- } catch {}
1162
+ async resolveStyle() {
1163
+ if (this.resolvedStyle !== null) return this.resolvedStyle;
1164
+ if (await checkCliAvailable("cursor", ["agent", "--version"], 2e3)) {
1165
+ this.resolvedStyle = "subcommand";
1166
+ return "subcommand";
1167
+ }
1168
+ if (await checkCliAvailable("agent", ["--version"], 2e3)) {
1169
+ this.resolvedStyle = "direct";
1170
+ return "direct";
1171
+ }
1042
1172
  return null;
1043
1173
  }
1044
1174
  async buildCommand(message) {
1045
- const cmd = await this.resolveCommand();
1175
+ const style = await this.resolveStyle();
1046
1176
  const agentArgs = [
1047
1177
  "-p",
1048
1178
  "--force",
@@ -1051,7 +1181,11 @@ var CursorBackend = class {
1051
1181
  message
1052
1182
  ];
1053
1183
  if (this.options.model) agentArgs.push("--model", this.options.model);
1054
- if (!cmd) throw new Error("cursor agent CLI not found");
1184
+ if (!style) throw new Error("cursor agent CLI not found. Install via: curl -fsS https://cursor.com/install | bash");
1185
+ if (style === "direct") return {
1186
+ command: "agent",
1187
+ args: agentArgs
1188
+ };
1055
1189
  return {
1056
1190
  command: "cursor",
1057
1191
  args: ["agent", ...agentArgs]
@@ -1061,16 +1195,6 @@ var CursorBackend = class {
1061
1195
 
1062
1196
  //#endregion
1063
1197
  //#region src/backends/opencode.ts
1064
- /**
1065
- * OpenCode CLI backend
1066
- * Uses `opencode run` for non-interactive mode with JSON event output
1067
- *
1068
- * MCP Configuration:
1069
- * OpenCode uses project-level MCP config via opencode.json in the workspace.
1070
- * Use setWorkspace() to set up a dedicated workspace with MCP config.
1071
- *
1072
- * @see https://opencode.ai/docs/
1073
- */
1074
1198
  var OpenCodeBackend = class {
1075
1199
  type = "opencode";
1076
1200
  options;
@@ -1080,29 +1204,6 @@ var OpenCodeBackend = class {
1080
1204
  ...options
1081
1205
  };
1082
1206
  }
1083
- /**
1084
- * Set up workspace directory with MCP config
1085
- * Creates opencode.json in the workspace with MCP server config
1086
- */
1087
- setWorkspace(workspaceDir, mcpConfig) {
1088
- this.options.workspace = workspaceDir;
1089
- if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
1090
- const opencodeMcp = {};
1091
- for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
1092
- const serverConfig = config;
1093
- opencodeMcp[name] = {
1094
- type: "local",
1095
- command: [serverConfig.command, ...serverConfig.args || []],
1096
- enabled: true,
1097
- ...serverConfig.env ? { environment: serverConfig.env } : {}
1098
- };
1099
- }
1100
- const opencodeConfig = {
1101
- $schema: "https://opencode.ai/config.json",
1102
- mcp: opencodeMcp
1103
- };
1104
- writeFileSync(join(workspaceDir, "opencode.json"), JSON.stringify(opencodeConfig, null, 2));
1105
- }
1106
1207
  async send(message, _options) {
1107
1208
  const args = this.buildArgs(message);
1108
1209
  const cwd = this.options.workspace || this.options.cwd;
@@ -1117,24 +1218,11 @@ var OpenCodeBackend = class {
1117
1218
  });
1118
1219
  return extractOpenCodeResult(stdout);
1119
1220
  } catch (error) {
1120
- if (error instanceof IdleTimeoutError) throw new Error(`opencode timed out after ${timeout}ms of inactivity`);
1121
- if (error && typeof error === "object" && "exitCode" in error) {
1122
- const execError = error;
1123
- throw new Error(`opencode failed (exit ${execError.exitCode}): ${execError.stderr || execError.shortMessage}`);
1124
- }
1125
- throw error;
1221
+ handleCliBackendError(error, "opencode", timeout);
1126
1222
  }
1127
1223
  }
1128
1224
  async isAvailable() {
1129
- try {
1130
- await execa("opencode", ["--version"], {
1131
- stdin: "ignore",
1132
- timeout: 5e3
1133
- });
1134
- return true;
1135
- } catch {
1136
- return false;
1137
- }
1225
+ return checkCliAvailable("opencode");
1138
1226
  }
1139
1227
  getInfo() {
1140
1228
  return {
@@ -1266,8 +1354,8 @@ var SdkBackend = class {
1266
1354
  * Mock AI Backend for testing
1267
1355
  *
1268
1356
  * In single-agent mode, provides a simple echo send().
1269
- * In workflow mode, the controller handles MCP tool orchestration
1270
- * 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).
1271
1359
  */
1272
1360
  var MockAIBackend = class {
1273
1361
  type = "mock";
@@ -1393,4 +1481,4 @@ async function listBackends() {
1393
1481
  }
1394
1482
 
1395
1483
  //#endregion
1396
- 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 };