zidane 2.0.1 → 2.1.1

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.
@@ -165,10 +165,11 @@ function createProcessContext(config) {
165
165
  });
166
166
  return { stdout, stderr, exitCode: 0 };
167
167
  } catch (err) {
168
+ const e = err;
168
169
  return {
169
- stdout: err.stdout ?? "",
170
- stderr: err.stderr ?? err.message,
171
- exitCode: err.code ?? 1
170
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
171
+ stderr: typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : String(err),
172
+ exitCode: typeof e.code === "number" ? e.code : 1
172
173
  };
173
174
  }
174
175
  },
@@ -6,18 +6,18 @@ import {
6
6
  mergeSkillsConfig,
7
7
  resolveSkills,
8
8
  validateResourcePath
9
- } from "./chunk-BCXXXJ3G.js";
9
+ } from "./chunk-DCYJYM3E.js";
10
10
  import {
11
11
  createProcessContext
12
- } from "./chunk-SZA4FKW5.js";
12
+ } from "./chunk-2EQT4EHD.js";
13
13
  import {
14
14
  connectMcpServers
15
- } from "./chunk-PJUUYBKF.js";
15
+ } from "./chunk-IJORSHFI.js";
16
16
  import {
17
17
  AgentAbortedError,
18
18
  AgentProviderError,
19
19
  toTypedError
20
- } from "./chunk-7JTBBZ2U.js";
20
+ } from "./chunk-LNN5UTS2.js";
21
21
 
22
22
  // src/tools/glob.ts
23
23
  var DEFAULT_LIMIT = 1e3;
@@ -69,7 +69,8 @@ var glob = {
69
69
  const entries = ctx.execution.type === "process" ? await globInProcess(pat, ctx.handle.cwd, max) : await globViaShell(pat, ctx, max);
70
70
  return entries.length > 0 ? entries.join("\n") : "(no matches)";
71
71
  } catch (err) {
72
- return `Glob error: ${err.message}`;
72
+ const message = err instanceof Error ? err.message : String(err);
73
+ return `Glob error: ${message}`;
73
74
  }
74
75
  }
75
76
  };
@@ -211,7 +212,8 @@ function createSkillsReadTool(options) {
211
212
  try {
212
213
  content = await ctx.execution.readFile(ctx.handle, validated.absolutePath);
213
214
  } catch (err) {
214
- return `Error reading "${relPath}" in skill "${skillName}": ${err.message}`;
215
+ const message = err instanceof Error ? err.message : String(err);
216
+ return `Error reading "${relPath}" in skill "${skillName}": ${message}`;
215
217
  }
216
218
  if (looksBinary(content)) {
217
219
  return JSON.stringify({
@@ -289,7 +291,8 @@ function createSkillsRunScriptTool(options) {
289
291
  stderr: result.stderr
290
292
  });
291
293
  } catch (err) {
292
- return `Error running script "${scriptRel}" for skill "${skillName}": ${err.message}`;
294
+ const message = err instanceof Error ? err.message : String(err);
295
+ return `Error running script "${scriptRel}" for skill "${skillName}": ${message}`;
293
296
  }
294
297
  }
295
298
  };
@@ -774,19 +777,23 @@ async function executeSingleTool(ctx, call, turnId) {
774
777
  hooks: ctx.hooks,
775
778
  harness: ctx.harness,
776
779
  turnId,
777
- callId
780
+ callId,
781
+ runId: ctx.runId,
782
+ ...ctx.session ? { session: ctx.session } : {},
783
+ ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
778
784
  };
779
785
  output = await toolDef.execute(effectiveInput, toolCtx);
780
786
  } catch (err) {
787
+ const error = err instanceof Error ? err : new Error(String(err));
781
788
  await ctx.hooks.callHook("tool:error", {
782
789
  turnId,
783
790
  callId,
784
791
  name: call.name,
785
792
  displayName,
786
793
  input: effectiveInput,
787
- error: err
794
+ error
788
795
  });
789
- output = `Tool error: ${err.message}`;
796
+ output = `Tool error: ${error.message}`;
790
797
  isError = true;
791
798
  }
792
799
  const transformCtx = {
@@ -921,6 +928,57 @@ function buildPromptMessage(provider, parts) {
921
928
 
922
929
  // src/agent.ts
923
930
  var noTools = { name: "none", system: "You are a helpful assistant.", tools: {} };
931
+ var HOOK_EVENT_NAMES = [
932
+ "system:before",
933
+ "turn:before",
934
+ "turn:after",
935
+ "stream:text",
936
+ "stream:end",
937
+ "stream:thinking",
938
+ "oauth:refresh",
939
+ "tool:gate",
940
+ "tool:before",
941
+ "tool:after",
942
+ "tool:error",
943
+ "tool:transform",
944
+ "context:transform",
945
+ "steer:inject",
946
+ "spawn:before",
947
+ "spawn:complete",
948
+ "spawn:error",
949
+ "child:stream:text",
950
+ "child:stream:thinking",
951
+ "child:stream:end",
952
+ "child:tool:before",
953
+ "child:tool:after",
954
+ "child:tool:error",
955
+ "child:turn:after",
956
+ "mcp:connect",
957
+ "mcp:error",
958
+ "mcp:close",
959
+ "mcp:tool:gate",
960
+ "mcp:tool:before",
961
+ "mcp:tool:after",
962
+ "mcp:tool:transform",
963
+ "mcp:tool:error",
964
+ "skills:resolve",
965
+ "skills:catalog",
966
+ "skills:activate",
967
+ "skills:deactivate",
968
+ "usage",
969
+ "output",
970
+ "agent:abort",
971
+ "agent:done",
972
+ "session:start",
973
+ "session:end",
974
+ "session:turns",
975
+ "session:meta",
976
+ "session:save"
977
+ ];
978
+ var HOOK_EVENT_SET = new Set(HOOK_EVENT_NAMES);
979
+ function isKnownHookEvent(event) {
980
+ return HOOK_EVENT_SET.has(event);
981
+ }
924
982
  function resolveBehavior(harnessBehavior, agentBehavior, runBehavior) {
925
983
  return {
926
984
  toolExecution: runBehavior?.toolExecution ?? agentBehavior?.toolExecution ?? harnessBehavior?.toolExecution ?? "parallel",
@@ -971,7 +1029,10 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
971
1029
  abortController = new AbortController();
972
1030
  const runId = `run_${++runCounter}`;
973
1031
  const promptLabel = typeof options.prompt === "string" ? options.prompt : Array.isArray(options.prompt) ? options.prompt.filter((p) => p.type === "text").map((p) => p.text).join("\n") : "";
974
- session?.startRun(runId, promptLabel);
1032
+ session?.startRun(runId, promptLabel, {
1033
+ ...options.parentRunId ? { parentRunId: options.parentRunId } : {},
1034
+ depth: typeof options.depth === "number" ? options.depth : 0
1035
+ });
975
1036
  if (session) {
976
1037
  await session.updateStatus("running");
977
1038
  await hooks.callHook("session:start", { sessionId: session.id, runId, prompt: promptLabel });
@@ -994,6 +1055,11 @@ function createAgent({ harness: harnessOption, provider, behavior: agentBehavior
994
1055
  const perRunUnregisters = [];
995
1056
  if (options.hooks) {
996
1057
  for (const [event, handler] of Object.entries(options.hooks)) {
1058
+ if (!isKnownHookEvent(event)) {
1059
+ throw new Error(
1060
+ `Unknown hook event "${event}" passed to run(). See AgentHooks for valid events.`
1061
+ );
1062
+ }
997
1063
  const handlerList = Array.isArray(handler) ? handler : [handler];
998
1064
  for (const fn of handlerList) {
999
1065
  if (typeof fn !== "function")
@@ -1140,6 +1206,7 @@ ${skillsCatalog}`;
1140
1206
  }
1141
1207
  const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
1142
1208
  const runStartMs = Date.now();
1209
+ const runDepth = typeof options.depth === "number" ? options.depth : 0;
1143
1210
  try {
1144
1211
  const stats = await runLoop({
1145
1212
  provider,
@@ -1162,6 +1229,8 @@ ${skillsCatalog}`;
1162
1229
  generateTurnId: () => session?.generateTurnId() ?? crypto.randomUUID(),
1163
1230
  maxTurns,
1164
1231
  maxTokens,
1232
+ ...session ? { session } : {},
1233
+ depth: runDepth,
1165
1234
  thinkingBudget,
1166
1235
  schema,
1167
1236
  runStartMs
@@ -1199,7 +1268,8 @@ ${skillsCatalog}`;
1199
1268
  await hooks.callHook("agent:done", stats);
1200
1269
  return stats;
1201
1270
  }
1202
- session?.errorRun(runId, err.message);
1271
+ const message = err instanceof Error ? err.message : String(err);
1272
+ session?.errorRun(runId, message);
1203
1273
  await finalizeSession("error");
1204
1274
  throw err;
1205
1275
  } finally {
@@ -1230,12 +1300,13 @@ ${skillsCatalog}`;
1230
1300
  function waitForIdle() {
1231
1301
  return idlePromise ?? Promise.resolve();
1232
1302
  }
1233
- function reset() {
1303
+ async function reset() {
1234
1304
  conversationTurns = [];
1235
1305
  steeringQueue.length = 0;
1236
1306
  followUpQueue.length = 0;
1237
- for (const record of skillActivationState.clear())
1238
- void hooks.callHook("skills:deactivate", { skill: record.skill, reason: "reset" });
1307
+ const cleared = skillActivationState.clear();
1308
+ for (const record of cleared)
1309
+ await hooks.callHook("skills:deactivate", { skill: record.skill, reason: "reset" });
1239
1310
  }
1240
1311
  async function activateSkill(name) {
1241
1312
  if (!resolvedSkills) {
@@ -1271,10 +1342,16 @@ ${skillsCatalog}`;
1271
1342
  };
1272
1343
  session.setMeta = (key, value) => {
1273
1344
  originalSetMeta(key, value);
1274
- hooks.callHook("session:meta", { sessionId: session.id, key, value });
1345
+ void Promise.resolve(hooks.callHook("session:meta", { sessionId: session.id, key, value })).catch((err) => {
1346
+ console.error("[zidane] session:meta listener rejected:", err);
1347
+ });
1275
1348
  };
1276
1349
  }
1350
+ let destroyed = false;
1277
1351
  async function destroy() {
1352
+ if (destroyed)
1353
+ return;
1354
+ destroyed = true;
1278
1355
  if (mcpConnection) {
1279
1356
  await mcpConnection.close();
1280
1357
  mcpConnection = null;
@@ -1313,11 +1390,34 @@ ${skillsCatalog}`;
1313
1390
  get activeSkills() {
1314
1391
  return skillActivationState.active();
1315
1392
  },
1316
- meta: provider.meta
1393
+ // Expose a frozen view of provider.meta. Hosts previously could mutate
1394
+ // the underlying provider meta (e.g. via `agent.meta.defaultModel = …`),
1395
+ // which quietly affected every other agent sharing the same provider
1396
+ // instance. Freezing forces callers to construct a new provider when
1397
+ // they want to override model/capabilities.
1398
+ meta: Object.freeze({ ...provider.meta })
1317
1399
  };
1318
1400
  }
1319
1401
 
1320
1402
  // src/tools/spawn.ts
1403
+ var BUBBLED_EVENTS = [
1404
+ "stream:text",
1405
+ "stream:thinking",
1406
+ "stream:end",
1407
+ "tool:before",
1408
+ "tool:after",
1409
+ "tool:error",
1410
+ "turn:after"
1411
+ ];
1412
+ var CHILD_EVENT_NAME = {
1413
+ "stream:text": "child:stream:text",
1414
+ "stream:thinking": "child:stream:thinking",
1415
+ "stream:end": "child:stream:end",
1416
+ "tool:before": "child:tool:before",
1417
+ "tool:after": "child:tool:after",
1418
+ "tool:error": "child:tool:error",
1419
+ "turn:after": "child:turn:after"
1420
+ };
1321
1421
  function extractText(message) {
1322
1422
  if (!message || typeof message !== "object")
1323
1423
  return "";
@@ -1325,15 +1425,60 @@ function extractText(message) {
1325
1425
  if (typeof msg.content === "string")
1326
1426
  return msg.content;
1327
1427
  if (Array.isArray(msg.content)) {
1328
- return msg.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
1428
+ return msg.content.filter((block) => !!block && typeof block === "object" && block.type === "text").map((block) => block.text).join("\n");
1329
1429
  }
1330
1430
  return "";
1331
1431
  }
1432
+ async function raceWithTimeout(task, timeoutMs) {
1433
+ if (!timeoutMs || timeoutMs <= 0)
1434
+ return task;
1435
+ let timer;
1436
+ try {
1437
+ return await new Promise((resolve, reject) => {
1438
+ timer = setTimeout(() => reject(new SpawnTimeoutError(timeoutMs)), timeoutMs);
1439
+ task.then(resolve, reject);
1440
+ });
1441
+ } finally {
1442
+ if (timer)
1443
+ clearTimeout(timer);
1444
+ }
1445
+ }
1446
+ var SpawnTimeoutError = class extends Error {
1447
+ timeoutMs;
1448
+ constructor(timeoutMs) {
1449
+ super(`Child agent timed out after ${timeoutMs}ms`);
1450
+ this.name = "SpawnTimeoutError";
1451
+ this.timeoutMs = timeoutMs;
1452
+ }
1453
+ };
1454
+ function bubbleHooks(childHooks, parentHooks, childId, depth) {
1455
+ const unregisters = [];
1456
+ const fire = parentHooks.callHook;
1457
+ for (const evt of BUBBLED_EVENTS) {
1458
+ const parentEvt = CHILD_EVENT_NAME[evt];
1459
+ const unregister = childHooks.hook(evt, (ctx) => {
1460
+ void fire(parentEvt, { ...ctx, childId, depth });
1461
+ });
1462
+ unregisters.push(unregister);
1463
+ }
1464
+ for (const evt of BUBBLED_EVENTS) {
1465
+ const parentEvt = CHILD_EVENT_NAME[evt];
1466
+ const unregister = childHooks.hook(parentEvt, (ctx) => {
1467
+ void fire(parentEvt, ctx);
1468
+ });
1469
+ unregisters.push(unregister);
1470
+ }
1471
+ return () => {
1472
+ for (const u of unregisters) u();
1473
+ };
1474
+ }
1332
1475
  function createSpawnTool(options = {}) {
1333
1476
  const localChildren = /* @__PURE__ */ new Map();
1334
1477
  let localCounter = 0;
1335
1478
  let localActiveCount = 0;
1336
1479
  const maxConcurrent = options.maxConcurrent ?? 3;
1480
+ const maxDepth = options.maxDepth ?? 3;
1481
+ const forwardHooks = options.forwardHooks ?? true;
1337
1482
  const localStats = {
1338
1483
  totalIn: 0,
1339
1484
  totalOut: 0,
@@ -1368,51 +1513,118 @@ function createSpawnTool(options = {}) {
1368
1513
  async execute(input, ctx) {
1369
1514
  const task = input.task;
1370
1515
  const systemOverride = input.system;
1516
+ const parentDepth = ctx.depth ?? 0;
1517
+ const childDepth = parentDepth + 1;
1518
+ if (childDepth > maxDepth) {
1519
+ return `Cannot spawn: maxDepth=${maxDepth} reached (parent depth=${parentDepth}). Deepen the cap with createSpawnTool({ maxDepth }).`;
1520
+ }
1371
1521
  if (localActiveCount >= maxConcurrent) {
1372
1522
  return `Cannot spawn: ${localActiveCount}/${maxConcurrent} sub-agents already running. Wait for one to complete.`;
1373
1523
  }
1524
+ if (ctx.signal.aborted) {
1525
+ return `[sub-agent pre-aborted] Parent signal was already aborted \u2014 skipped "${task.slice(0, 80)}"`;
1526
+ }
1374
1527
  const id = `child-${++localCounter}`;
1375
- const child = { id, task, startedAt: Date.now() };
1376
- const agent = createAgent({
1377
- harness: options.harness ?? ctx.harness,
1378
- provider: ctx.provider,
1379
- execution: ctx.execution
1380
- });
1381
- localChildren.set(id, child);
1382
1528
  localActiveCount++;
1383
- options.onSpawn?.(child);
1384
- await ctx.hooks.callHook("spawn:before", { id, task });
1529
+ const child = { id, task, startedAt: Date.now(), depth: childDepth };
1530
+ localChildren.set(id, child);
1531
+ let destroyError;
1532
+ let childRunStatus = "completed";
1533
+ let finalStats;
1534
+ let result = "";
1535
+ let unbubble;
1385
1536
  try {
1386
- const stats = await agent.run({
1537
+ const agent = createAgent({
1538
+ harness: options.harness ?? ctx.harness,
1539
+ provider: ctx.provider,
1540
+ execution: ctx.execution,
1541
+ // Share the parent's session on opt-in. Child turns get appended to
1542
+ // the same session.turns stream with the child's runId; the child
1543
+ // run itself is tagged with parentRunId below, via AgentRunOptions.
1544
+ ...options.persist && ctx.session ? { session: ctx.session } : {}
1545
+ });
1546
+ if (forwardHooks)
1547
+ unbubble = bubbleHooks(agent.hooks, ctx.hooks, id, childDepth);
1548
+ options.onSpawn?.(child);
1549
+ await ctx.hooks.callHook("spawn:before", { id, task, depth: childDepth });
1550
+ const runPromise = agent.run({
1387
1551
  prompt: task,
1388
1552
  model: options.model,
1389
1553
  system: systemOverride ?? options.system,
1390
1554
  thinking: options.thinking,
1391
- signal: ctx.signal
1555
+ signal: ctx.signal,
1556
+ depth: childDepth,
1557
+ ...options.persist && ctx.runId ? { parentRunId: ctx.runId } : {}
1392
1558
  });
1393
- localStats.totalIn += stats.totalIn;
1394
- localStats.totalOut += stats.totalOut;
1395
- localStats.turns += stats.turns;
1396
- localStats.elapsed += stats.elapsed;
1397
- options.onComplete?.(child, stats);
1398
- await ctx.hooks.callHook("spawn:complete", {
1559
+ try {
1560
+ finalStats = await raceWithTimeout(runPromise, options.timeoutMs);
1561
+ if (ctx.signal.aborted) {
1562
+ childRunStatus = "aborted";
1563
+ result = [
1564
+ `[sub-agent ${id}] Aborted after ${finalStats.turns} turns (${finalStats.elapsed}ms)`,
1565
+ `Tokens: ${finalStats.totalIn} in / ${finalStats.totalOut} out`
1566
+ ].join("\n");
1567
+ } else {
1568
+ const response = extractText(agent.turns.at(-1));
1569
+ result = [
1570
+ `[sub-agent ${id}] Completed in ${finalStats.turns} turns (${finalStats.elapsed}ms)`,
1571
+ `Tokens: ${finalStats.totalIn} in / ${finalStats.totalOut} out`,
1572
+ "",
1573
+ response || "(no text response)"
1574
+ ].join("\n");
1575
+ }
1576
+ } catch (err) {
1577
+ if (err instanceof SpawnTimeoutError) {
1578
+ childRunStatus = "timeout";
1579
+ agent.abort();
1580
+ try {
1581
+ finalStats = await runPromise;
1582
+ } catch {
1583
+ finalStats = { totalIn: 0, totalOut: 0, turns: 0, elapsed: err.timeoutMs };
1584
+ }
1585
+ result = `[sub-agent ${id}] Timed out after ${err.timeoutMs}ms`;
1586
+ } else {
1587
+ const error = err instanceof Error ? err : new Error(String(err));
1588
+ childRunStatus = "error";
1589
+ finalStats = { totalIn: 0, totalOut: 0, turns: 0, elapsed: 0 };
1590
+ result = `[sub-agent ${id}] Error: ${error.message}`;
1591
+ await ctx.hooks.callHook("spawn:error", { id, task, depth: childDepth, error });
1592
+ }
1593
+ } finally {
1594
+ try {
1595
+ await agent.destroy();
1596
+ } catch (err) {
1597
+ destroyError = err instanceof Error ? err : new Error(String(err));
1598
+ }
1599
+ }
1600
+ if (finalStats) {
1601
+ localStats.totalIn += finalStats.totalIn;
1602
+ localStats.totalOut += finalStats.totalOut;
1603
+ localStats.turns += finalStats.turns;
1604
+ localStats.elapsed += finalStats.elapsed;
1605
+ }
1606
+ const childRunStats = {
1399
1607
  id,
1400
1608
  task,
1401
- stats
1402
- });
1403
- const response = extractText(agent.turns.at(-1));
1404
- return [
1405
- `[sub-agent ${id}] Completed in ${stats.turns} turns (${stats.elapsed}ms)`,
1406
- `Tokens: ${stats.totalIn} in / ${stats.totalOut} out`,
1407
- "",
1408
- response || "(no text response)"
1409
- ].join("\n");
1410
- } catch (err) {
1411
- await ctx.hooks.callHook("spawn:error", { id, task, error: err });
1412
- return `[sub-agent ${id}] Error: ${err.message}`;
1609
+ stats: finalStats,
1610
+ depth: childDepth,
1611
+ status: childRunStatus,
1612
+ ...finalStats.output ? { output: finalStats.output } : {}
1613
+ };
1614
+ options.onComplete?.(child, finalStats, childRunStatus);
1615
+ await ctx.hooks.callHook("spawn:complete", childRunStats);
1616
+ if (destroyError) {
1617
+ await ctx.hooks.callHook("spawn:error", {
1618
+ id,
1619
+ task,
1620
+ depth: childDepth,
1621
+ error: destroyError
1622
+ });
1623
+ }
1624
+ return result;
1413
1625
  } finally {
1626
+ unbubble?.();
1414
1627
  localActiveCount--;
1415
- await agent.destroy();
1416
1628
  localChildren.delete(id);
1417
1629
  }
1418
1630
  }
@@ -5,27 +5,39 @@ import {
5
5
  toAnthropic,
6
6
  toolResultsMessage,
7
7
  userMessage
8
- } from "./chunk-LN4LLLHA.js";
8
+ } from "./chunk-S3FCOMRI.js";
9
9
  import {
10
10
  matchesContextExceeded
11
- } from "./chunk-7JTBBZ2U.js";
12
-
13
- // src/providers/anthropic.ts
14
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
15
- import { resolve as resolve2 } from "path";
11
+ } from "./chunk-LNN5UTS2.js";
16
12
 
17
13
  // src/providers/oauth.ts
18
- import { existsSync, readFileSync, writeFileSync } from "fs";
14
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
19
15
  import { resolve } from "path";
20
16
  import { getOAuthApiKey } from "@yaelg/pi-ai/oauth";
21
- var CREDENTIALS_FILE = resolve(process.cwd(), ".credentials.json");
17
+ function credentialsFilePath() {
18
+ return resolve(process.cwd(), ".credentials.json");
19
+ }
20
+ var CREDENTIALS_FILE_MODE = 384;
21
+ var refreshLocks = /* @__PURE__ */ new Map();
22
22
  function readOAuthCredentials() {
23
- if (!existsSync(CREDENTIALS_FILE))
23
+ const path = credentialsFilePath();
24
+ if (!existsSync(path))
24
25
  return {};
25
- return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
26
+ try {
27
+ const raw = readFileSync(path, "utf-8");
28
+ const parsed = JSON.parse(raw);
29
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
30
+ return {};
31
+ return parsed;
32
+ } catch {
33
+ return {};
34
+ }
26
35
  }
27
36
  function writeOAuthCredentials(credentials) {
28
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
37
+ const path = credentialsFilePath();
38
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
39
+ writeFileSync(tmp, JSON.stringify(credentials, null, 2), { mode: CREDENTIALS_FILE_MODE });
40
+ renameSync(tmp, path);
29
41
  }
30
42
  function credentialsFromParams(params, extraKeys = []) {
31
43
  if (typeof params?.access !== "string" || typeof params.refresh !== "string" || typeof params.expires !== "number")
@@ -45,7 +57,10 @@ async function resolveOAuthApiKey(options, callbacks) {
45
57
  return options.params.apiKey;
46
58
  const paramsCredentials = credentialsFromParams(options.params, options.extraCredentialKeys);
47
59
  if (paramsCredentials) {
48
- return await resolveCredentialSource("params", paramsCredentials);
60
+ return await withRefreshLock(
61
+ `params:${options.providerId}`,
62
+ () => resolveCredentialSource("params", paramsCredentials)
63
+ );
49
64
  }
50
65
  if (typeof options.params?.access === "string")
51
66
  return options.params.access;
@@ -53,21 +68,23 @@ async function resolveOAuthApiKey(options, callbacks) {
53
68
  return process.env[options.envKey];
54
69
  const readCredentials = options.readCredentials ?? readOAuthCredentials;
55
70
  const writeCredentials = options.writeCredentials ?? writeOAuthCredentials;
56
- const allCredentials = readCredentials();
57
- const storedCredentials = allCredentials[options.providerId];
58
- if (!storedCredentials)
59
- throw new Error(options.missingError);
60
- return await resolveCredentialSource("file", storedCredentials, allCredentials, writeCredentials);
61
- async function resolveCredentialSource(source, current, allCredentials2, persistCredentials) {
71
+ return await withRefreshLock(`file:${options.providerId}`, async () => {
72
+ const allCredentials = readCredentials();
73
+ const storedCredentials = allCredentials[options.providerId];
74
+ if (!storedCredentials)
75
+ throw new Error(options.missingError);
76
+ return await resolveCredentialSource("file", storedCredentials, allCredentials, writeCredentials);
77
+ });
78
+ async function resolveCredentialSource(source, current, allCredentials, persistCredentials) {
62
79
  try {
63
80
  const refreshOAuthApiKey = options.getOAuthApiKey ?? getOAuthApiKey;
64
81
  const result = await refreshOAuthApiKey(options.providerId, { [options.providerId]: current });
65
82
  if (!result)
66
83
  throw new Error(options.missingError);
67
84
  if (result.newCredentials !== current) {
68
- if (source === "file" && allCredentials2 && persistCredentials) {
69
- allCredentials2[options.providerId] = result.newCredentials;
70
- persistCredentials(allCredentials2);
85
+ if (source === "file" && allCredentials && persistCredentials) {
86
+ allCredentials[options.providerId] = result.newCredentials;
87
+ persistCredentials(allCredentials);
71
88
  }
72
89
  await callbacks?.onOAuthRefresh?.({
73
90
  provider: options.provider,
@@ -84,9 +101,22 @@ async function resolveOAuthApiKey(options, callbacks) {
84
101
  }
85
102
  }
86
103
  }
104
+ async function withRefreshLock(key, fn) {
105
+ const existing = refreshLocks.get(key);
106
+ if (existing)
107
+ return existing;
108
+ const task = (async () => {
109
+ try {
110
+ return await fn();
111
+ } finally {
112
+ refreshLocks.delete(key);
113
+ }
114
+ })();
115
+ refreshLocks.set(key, task);
116
+ return task;
117
+ }
87
118
 
88
119
  // src/providers/anthropic.ts
89
- var CREDENTIALS_FILE2 = resolve2(process.cwd(), ".credentials.json");
90
120
  var _sdkCtor = null;
91
121
  async function loadAnthropicSdk() {
92
122
  if (_sdkCtor)
@@ -109,11 +139,9 @@ function getConfiguredApiKey(anthropicParams) {
109
139
  return anthropicParams.access;
110
140
  if (process.env.ANTHROPIC_API_KEY)
111
141
  return process.env.ANTHROPIC_API_KEY;
112
- if (existsSync2(CREDENTIALS_FILE2)) {
113
- const creds = JSON.parse(readFileSync2(CREDENTIALS_FILE2, "utf-8"));
114
- if (creds.anthropic?.access)
115
- return creds.anthropic.access;
116
- }
142
+ const access = readOAuthCredentials().anthropic?.access;
143
+ if (typeof access === "string" && access.length > 0)
144
+ return access;
117
145
  throw new Error("No API key found. Run `bun run auth` first.");
118
146
  }
119
147
  function createClient(SDK, apiKey, isOAuth, baseURL) {
@@ -183,10 +211,13 @@ function classifyAnthropicError(err) {
183
211
  message
184
212
  };
185
213
  }
214
+ const status = anyErr.status;
215
+ const retryable = typeof status === "number" ? status === 429 || status >= 500 && status !== 501 : void 0;
186
216
  return {
187
217
  kind: "provider_error",
188
- providerCode: nativeType ?? (anyErr.status ? String(anyErr.status) : void 0),
189
- message
218
+ providerCode: nativeType ?? (status ? String(status) : void 0),
219
+ message,
220
+ ...retryable !== void 0 ? { retryable } : {}
190
221
  };
191
222
  }
192
223
  function anthropicPromptMessage(parts) {
@@ -368,11 +399,11 @@ function anthropic(anthropicParams) {
368
399
  // src/providers/cerebras.ts
369
400
  var BASE_URL = "https://api.cerebras.ai/v1";
370
401
  function getApiKey(params) {
371
- if (params?.apiKey)
402
+ if (typeof params?.apiKey === "string" && params.apiKey.length > 0)
372
403
  return params.apiKey;
373
404
  if (process.env.CEREBRAS_API_KEY)
374
405
  return process.env.CEREBRAS_API_KEY;
375
- throw new Error("No Cerebras API key found. Set CEREBRAS_API_KEY in your environment.");
406
+ throw new Error("No Cerebras API key found. Pass `apiKey` or set CEREBRAS_API_KEY in your environment.");
376
407
  }
377
408
  function cerebras(params) {
378
409
  const apiKey = getApiKey(params);
@@ -650,11 +681,11 @@ function openai(params) {
650
681
  // src/providers/openrouter.ts
651
682
  var BASE_URL2 = "https://openrouter.ai/api/v1";
652
683
  function getApiKey2(params) {
653
- if (params?.apiKey)
684
+ if (typeof params?.apiKey === "string" && params.apiKey.length > 0)
654
685
  return params.apiKey;
655
686
  if (process.env.OPENROUTER_API_KEY)
656
687
  return process.env.OPENROUTER_API_KEY;
657
- throw new Error("No OpenRouter API key found. Set OPENROUTER_API_KEY in your environment.");
688
+ throw new Error("No OpenRouter API key found. Pass `apiKey` or set OPENROUTER_API_KEY in your environment.");
658
689
  }
659
690
  function openrouter(params) {
660
691
  const apiKey = getApiKey2(params);