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.
- package/README.md +6 -3
- package/dist/{backends-BWzhErjT.mjs → backends-BYWmuyF9.mjs} +1 -1
- package/dist/{backends-CziIqKRg.mjs → backends-C7pQwuAx.mjs} +310 -222
- package/dist/cli/index.mjs +2044 -478
- package/dist/context-CdcZpO-0.mjs +4 -0
- package/dist/create-tool-gcUuI1FD.mjs +32 -0
- package/dist/index.d.mts +65 -87
- package/dist/index.mjs +465 -21
- package/dist/{memory-provider-BtLYtdQH.mjs → memory-provider-ZLOKyCxA.mjs} +8 -3
- package/dist/runner-DB-b57iZ.mjs +670 -0
- package/dist/workflow-DQ6Eju4n.mjs +664 -0
- package/package.json +4 -3
- package/dist/context-BqEyt2SF.mjs +0 -4
- package/dist/logger-Bfdo83xL.mjs +0 -63
- package/dist/runner-CnxROIev.mjs +0 -1496
- package/dist/worker-DBJ8136Q.mjs +0 -448
- package/dist/workflow-CIE3WPNx.mjs +0 -272
- /package/dist/{display-pretty-BCJq5v9d.mjs → display-pretty-Kyd40DEF.mjs} +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { gateway, generateText } from "ai";
|
|
2
2
|
import { execa } from "execa";
|
|
3
|
-
import { existsSync
|
|
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("/"))
|
|
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.
|
|
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("/"))
|
|
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
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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
|
-
*
|
|
413
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
979
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
1033
|
-
if (this.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
this.
|
|
1040
|
-
return "
|
|
1041
|
-
}
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1270
|
-
* via the mock runner strategy (
|
|
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 };
|