opencode-anthropic-fix 0.0.35 → 0.0.37
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/index.mjs +446 -9
- package/lib/config.mjs +13 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -148,6 +148,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
148
148
|
const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1000;
|
|
149
149
|
const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1000;
|
|
150
150
|
|
|
151
|
+
// Willow Mode: detect inactivity and suggest context reset.
|
|
152
|
+
// Named after the willow tree — when idle, the session "droops" and a gentle
|
|
153
|
+
// nudge suggests starting fresh rather than accumulating stale context.
|
|
154
|
+
const WILLOW_ENABLED = config.willow_mode?.enabled ?? true;
|
|
155
|
+
const WILLOW_IDLE_THRESHOLD_MS = (config.willow_mode?.idle_threshold_minutes ?? 30) * 60 * 1000;
|
|
156
|
+
const WILLOW_COOLDOWN_MS = (config.willow_mode?.cooldown_minutes ?? 60) * 60 * 1000;
|
|
157
|
+
const WILLOW_MIN_TURNS = config.willow_mode?.min_turns_before_suggest ?? 3;
|
|
158
|
+
let willowLastRequestTime = Date.now();
|
|
159
|
+
let willowLastSuggestionTime = 0;
|
|
160
|
+
|
|
151
161
|
/**
|
|
152
162
|
* Whether OPENCODE_ANTHROPIC_INITIAL_ACCOUNT env var pinned this session to a
|
|
153
163
|
* specific account. When true, syncActiveIndexFromDisk is skipped and strategy
|
|
@@ -882,7 +892,7 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
882
892
|
"Toggleable presets:",
|
|
883
893
|
" /anthropic betas add structured-outputs-2025-12-15",
|
|
884
894
|
" /anthropic betas add context-management-2025-06-27",
|
|
885
|
-
" /anthropic betas add
|
|
895
|
+
" /anthropic betas add task-budgets-2026-03-13",
|
|
886
896
|
" /anthropic betas add web-search-2025-03-05",
|
|
887
897
|
" /anthropic betas add compact-2026-01-12",
|
|
888
898
|
" /anthropic betas add mcp-servers-2025-12-04",
|
|
@@ -1294,6 +1304,363 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1294
1304
|
}
|
|
1295
1305
|
}
|
|
1296
1306
|
|
|
1307
|
+
// /anthropic review [pr <number>|branch <name>|status] — Claude Code Review (Bughunter) results
|
|
1308
|
+
if (primary === "review") {
|
|
1309
|
+
const action = (args[1] || "").toLowerCase();
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Execute a shell command and return { stdout, stderr, code }.
|
|
1313
|
+
* @param {string} cmd
|
|
1314
|
+
* @param {string[]} cmdArgs
|
|
1315
|
+
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
1316
|
+
*/
|
|
1317
|
+
async function execShell(cmd, cmdArgs) {
|
|
1318
|
+
const { execFile } = await import("node:child_process");
|
|
1319
|
+
return new Promise((resolve) => {
|
|
1320
|
+
execFile(cmd, cmdArgs, { timeout: 30_000, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
1321
|
+
resolve({
|
|
1322
|
+
stdout: (stdout || "").trim(),
|
|
1323
|
+
stderr: (stderr || "").trim(),
|
|
1324
|
+
code: err ? err.code || 1 : 0,
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Parse bughunter severity from check run output text.
|
|
1332
|
+
* @param {string} text
|
|
1333
|
+
* @returns {{ normal: number, nit: number, pre_existing: number } | null}
|
|
1334
|
+
*/
|
|
1335
|
+
function parseBughunterSeverity(text) {
|
|
1336
|
+
const m = text.match(/bughunter-severity:\s*(\{[^}]+\})/);
|
|
1337
|
+
if (!m) return null;
|
|
1338
|
+
try {
|
|
1339
|
+
return JSON.parse(m[1]);
|
|
1340
|
+
} catch {
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Format a severity object into a human-readable string.
|
|
1347
|
+
* @param {{ normal: number, nit: number, pre_existing: number }} sev
|
|
1348
|
+
*/
|
|
1349
|
+
function formatSeverity(sev) {
|
|
1350
|
+
const parts = [];
|
|
1351
|
+
if (sev.normal > 0) parts.push(`🔴 Important: ${sev.normal}`);
|
|
1352
|
+
if (sev.nit > 0) parts.push(`🟡 Nit: ${sev.nit}`);
|
|
1353
|
+
if (sev.pre_existing > 0) parts.push(`🟣 Pre-existing: ${sev.pre_existing}`);
|
|
1354
|
+
if (parts.length === 0) parts.push("No issues found");
|
|
1355
|
+
return parts.join(" | ");
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Check gh CLI availability
|
|
1359
|
+
const ghCheck = await execShell("gh", ["--version"]);
|
|
1360
|
+
if (ghCheck.code !== 0) {
|
|
1361
|
+
await sendCommandMessage(
|
|
1362
|
+
input.sessionID,
|
|
1363
|
+
"▣ Anthropic Review (error)\n\nGitHub CLI (gh) not found. Install it from https://cli.github.com/",
|
|
1364
|
+
);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Detect current repo
|
|
1369
|
+
const repoResult = await execShell("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]);
|
|
1370
|
+
if (repoResult.code !== 0 || !repoResult.stdout) {
|
|
1371
|
+
await sendCommandMessage(
|
|
1372
|
+
input.sessionID,
|
|
1373
|
+
"▣ Anthropic Review (error)\n\nCould not detect GitHub repository. Ensure you are in a git repo with a GitHub remote.",
|
|
1374
|
+
);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const repo = repoResult.stdout.trim();
|
|
1378
|
+
|
|
1379
|
+
try {
|
|
1380
|
+
// /anthropic review status — check if code review is set up for this repo
|
|
1381
|
+
if (action === "status") {
|
|
1382
|
+
// Check for recent check runs named "Claude Code Review"
|
|
1383
|
+
const checkResult = await execShell("gh", [
|
|
1384
|
+
"api",
|
|
1385
|
+
`repos/${repo}/commits/HEAD/check-runs`,
|
|
1386
|
+
"--jq",
|
|
1387
|
+
'.check_runs[] | select(.name | test("claude|bughunter"; "i")) | .name + " — " + .status + " (" + .conclusion + ")"',
|
|
1388
|
+
]);
|
|
1389
|
+
const lines = ["▣ Anthropic Review — Status", "", `Repository: ${repo}`, ""];
|
|
1390
|
+
if (checkResult.stdout) {
|
|
1391
|
+
lines.push("Recent Claude check runs:", checkResult.stdout);
|
|
1392
|
+
} else {
|
|
1393
|
+
lines.push(
|
|
1394
|
+
"No Claude Code Review check runs found on HEAD.",
|
|
1395
|
+
"",
|
|
1396
|
+
"Code Review must be enabled by an admin at claude.ai/admin-settings.",
|
|
1397
|
+
"It requires a Teams or Enterprise subscription.",
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// /anthropic review pr [<number>] — get review results for a PR
|
|
1405
|
+
if (!action || action === "pr") {
|
|
1406
|
+
const prNumber = args[2] ? parseInt(args[2], 10) : null;
|
|
1407
|
+
|
|
1408
|
+
// If no PR number, find the current branch's PR
|
|
1409
|
+
let prRef;
|
|
1410
|
+
if (prNumber) {
|
|
1411
|
+
prRef = String(prNumber);
|
|
1412
|
+
} else {
|
|
1413
|
+
const branchResult = await execShell("git", ["branch", "--show-current"]);
|
|
1414
|
+
const currentBranch = branchResult.stdout.trim();
|
|
1415
|
+
if (!currentBranch) {
|
|
1416
|
+
await sendCommandMessage(
|
|
1417
|
+
input.sessionID,
|
|
1418
|
+
"▣ Anthropic Review (error)\n\nDetached HEAD — specify a PR number: /anthropic review pr <number>",
|
|
1419
|
+
);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
// Find PR for current branch
|
|
1423
|
+
const prLookup = await execShell("gh", [
|
|
1424
|
+
"pr",
|
|
1425
|
+
"list",
|
|
1426
|
+
"--head",
|
|
1427
|
+
currentBranch,
|
|
1428
|
+
"--json",
|
|
1429
|
+
"number,title,state",
|
|
1430
|
+
"--limit",
|
|
1431
|
+
"1",
|
|
1432
|
+
]);
|
|
1433
|
+
if (prLookup.code !== 0 || !prLookup.stdout || prLookup.stdout === "[]") {
|
|
1434
|
+
await sendCommandMessage(
|
|
1435
|
+
input.sessionID,
|
|
1436
|
+
`▣ Anthropic Review (error)\n\nNo PR found for branch "${currentBranch}".\nUse: /anthropic review pr <number>`,
|
|
1437
|
+
);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const prs = JSON.parse(prLookup.stdout);
|
|
1441
|
+
if (!prs.length) {
|
|
1442
|
+
await sendCommandMessage(
|
|
1443
|
+
input.sessionID,
|
|
1444
|
+
`▣ Anthropic Review (error)\n\nNo PR found for branch "${currentBranch}".`,
|
|
1445
|
+
);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
prRef = String(prs[0].number);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Get check runs for the PR's head SHA
|
|
1452
|
+
const prData = await execShell("gh", ["pr", "view", prRef, "--json", "number,title,headRefOid,state,url"]);
|
|
1453
|
+
if (prData.code !== 0) {
|
|
1454
|
+
await sendCommandMessage(
|
|
1455
|
+
input.sessionID,
|
|
1456
|
+
`▣ Anthropic Review (error)\n\nCould not fetch PR #${prRef}: ${prData.stderr}`,
|
|
1457
|
+
);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const pr = JSON.parse(prData.stdout);
|
|
1461
|
+
const sha = pr.headRefOid;
|
|
1462
|
+
|
|
1463
|
+
// Fetch check runs for this SHA
|
|
1464
|
+
const checksResult = await execShell("gh", [
|
|
1465
|
+
"api",
|
|
1466
|
+
`repos/${repo}/commits/${sha}/check-runs`,
|
|
1467
|
+
"--jq",
|
|
1468
|
+
'.check_runs[] | select(.name | test("claude|bughunter"; "i"))',
|
|
1469
|
+
]);
|
|
1470
|
+
|
|
1471
|
+
const lines = [
|
|
1472
|
+
"▣ Anthropic Review",
|
|
1473
|
+
"",
|
|
1474
|
+
`PR #${pr.number}: ${pr.title}`,
|
|
1475
|
+
`State: ${pr.state} | Commit: ${sha.slice(0, 8)}`,
|
|
1476
|
+
`URL: ${pr.url}`,
|
|
1477
|
+
"",
|
|
1478
|
+
];
|
|
1479
|
+
|
|
1480
|
+
if (!checksResult.stdout) {
|
|
1481
|
+
lines.push(
|
|
1482
|
+
"No Claude Code Review check runs found for this PR.",
|
|
1483
|
+
"",
|
|
1484
|
+
"Possible reasons:",
|
|
1485
|
+
" • Code Review not enabled for this repository",
|
|
1486
|
+
" • Review still in progress (avg ~20 min)",
|
|
1487
|
+
" • PR is a draft (drafts are not auto-reviewed)",
|
|
1488
|
+
);
|
|
1489
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Parse all check runs (could be multiple)
|
|
1494
|
+
const checkRunsRaw = `[${checksResult.stdout.split("\n}\n").join("},\n")}]`
|
|
1495
|
+
.replace(/,\s*]$/, "]")
|
|
1496
|
+
.replace(/}\s*{/g, "},{");
|
|
1497
|
+
let checkRuns;
|
|
1498
|
+
try {
|
|
1499
|
+
checkRuns = JSON.parse(checkRunsRaw);
|
|
1500
|
+
if (!Array.isArray(checkRuns)) checkRuns = [checkRuns];
|
|
1501
|
+
} catch {
|
|
1502
|
+
// Single object
|
|
1503
|
+
try {
|
|
1504
|
+
checkRuns = [JSON.parse(checksResult.stdout)];
|
|
1505
|
+
} catch {
|
|
1506
|
+
lines.push(
|
|
1507
|
+
"Found check run(s) but could not parse output.",
|
|
1508
|
+
"",
|
|
1509
|
+
"Raw:",
|
|
1510
|
+
checksResult.stdout.slice(0, 500),
|
|
1511
|
+
);
|
|
1512
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
for (const run of checkRuns) {
|
|
1518
|
+
lines.push(`Check: ${run.name}`);
|
|
1519
|
+
lines.push(` Status: ${run.status} | Conclusion: ${run.conclusion || "pending"}`);
|
|
1520
|
+
if (run.html_url) lines.push(` Details: ${run.html_url}`);
|
|
1521
|
+
|
|
1522
|
+
// Parse bughunter severity
|
|
1523
|
+
const outputText = run.output?.text || "";
|
|
1524
|
+
const severity = parseBughunterSeverity(outputText);
|
|
1525
|
+
if (severity) {
|
|
1526
|
+
lines.push(` Findings: ${formatSeverity(severity)}`);
|
|
1527
|
+
const total = severity.normal + severity.nit + severity.pre_existing;
|
|
1528
|
+
lines.push(` Total: ${total} issue${total !== 1 ? "s" : ""}`);
|
|
1529
|
+
} else if (run.status === "completed") {
|
|
1530
|
+
lines.push(" Findings: No bughunter-severity data in output");
|
|
1531
|
+
} else {
|
|
1532
|
+
lines.push(" Review is still in progress...");
|
|
1533
|
+
}
|
|
1534
|
+
lines.push("");
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// /anthropic review branch [<name>] — find PR for branch and show review
|
|
1542
|
+
if (action === "branch") {
|
|
1543
|
+
const branchName = args[2] || (await execShell("git", ["branch", "--show-current"])).stdout.trim();
|
|
1544
|
+
if (!branchName) {
|
|
1545
|
+
await sendCommandMessage(
|
|
1546
|
+
input.sessionID,
|
|
1547
|
+
"▣ Anthropic Review (error)\n\nNo branch specified and HEAD is detached.",
|
|
1548
|
+
);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const prLookup = await execShell("gh", [
|
|
1553
|
+
"pr",
|
|
1554
|
+
"list",
|
|
1555
|
+
"--head",
|
|
1556
|
+
branchName,
|
|
1557
|
+
"--json",
|
|
1558
|
+
"number,title,state,headRefOid,url",
|
|
1559
|
+
"--limit",
|
|
1560
|
+
"5",
|
|
1561
|
+
]);
|
|
1562
|
+
if (prLookup.code !== 0 || !prLookup.stdout || prLookup.stdout === "[]") {
|
|
1563
|
+
await sendCommandMessage(
|
|
1564
|
+
input.sessionID,
|
|
1565
|
+
`▣ Anthropic Review (error)\n\nNo PRs found for branch "${branchName}".`,
|
|
1566
|
+
);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const prs = JSON.parse(prLookup.stdout);
|
|
1570
|
+
if (!prs.length) {
|
|
1571
|
+
await sendCommandMessage(
|
|
1572
|
+
input.sessionID,
|
|
1573
|
+
`▣ Anthropic Review (error)\n\nNo PRs found for branch "${branchName}".`,
|
|
1574
|
+
);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const lines = ["▣ Anthropic Review — Branch", "", `Branch: ${branchName}`, ""];
|
|
1579
|
+
|
|
1580
|
+
for (const pr of prs) {
|
|
1581
|
+
lines.push(`PR #${pr.number}: ${pr.title} (${pr.state})`);
|
|
1582
|
+
|
|
1583
|
+
// Fetch check runs
|
|
1584
|
+
const checksResult = await execShell("gh", [
|
|
1585
|
+
"api",
|
|
1586
|
+
`repos/${repo}/commits/${pr.headRefOid}/check-runs`,
|
|
1587
|
+
"--jq",
|
|
1588
|
+
'.check_runs[] | select(.name | test("claude|bughunter"; "i"))',
|
|
1589
|
+
]);
|
|
1590
|
+
|
|
1591
|
+
if (!checksResult.stdout) {
|
|
1592
|
+
lines.push(" No Claude Code Review check runs found.", "");
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Try to parse individual run
|
|
1597
|
+
let checkRuns;
|
|
1598
|
+
try {
|
|
1599
|
+
const raw = `[${checksResult.stdout.split("\n}\n").join("},\n")}]`
|
|
1600
|
+
.replace(/,\s*]$/, "]")
|
|
1601
|
+
.replace(/}\s*{/g, "},{");
|
|
1602
|
+
checkRuns = JSON.parse(raw);
|
|
1603
|
+
if (!Array.isArray(checkRuns)) checkRuns = [checkRuns];
|
|
1604
|
+
} catch {
|
|
1605
|
+
try {
|
|
1606
|
+
checkRuns = [JSON.parse(checksResult.stdout)];
|
|
1607
|
+
} catch {
|
|
1608
|
+
lines.push(" Could not parse check run output.", "");
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
for (const run of checkRuns) {
|
|
1614
|
+
lines.push(` Check: ${run.name} — ${run.status} (${run.conclusion || "pending"})`);
|
|
1615
|
+
const outputText = run.output?.text || "";
|
|
1616
|
+
const severity = parseBughunterSeverity(outputText);
|
|
1617
|
+
if (severity) {
|
|
1618
|
+
lines.push(` ${formatSeverity(severity)}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
lines.push("");
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
await sendCommandMessage(input.sessionID, lines.join("\n"));
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// /anthropic review help
|
|
1629
|
+
const helpLines = [
|
|
1630
|
+
"▣ Anthropic Review (Claude Code Review / Bughunter)",
|
|
1631
|
+
"",
|
|
1632
|
+
"Fetch and display code review results from Claude's automated PR reviewer.",
|
|
1633
|
+
"",
|
|
1634
|
+
"Usage:",
|
|
1635
|
+
" /anthropic review Review for current branch's PR",
|
|
1636
|
+
" /anthropic review pr <number> Review for a specific PR",
|
|
1637
|
+
" /anthropic review branch [<name>] Review for PRs on a branch",
|
|
1638
|
+
" /anthropic review status Check if review is configured",
|
|
1639
|
+
"",
|
|
1640
|
+
"Severity levels:",
|
|
1641
|
+
" 🔴 Important — bugs that should be fixed before merge",
|
|
1642
|
+
" 🟡 Nit — minor issues, worth fixing but not blocking",
|
|
1643
|
+
" 🟣 Pre-existing — bugs in codebase not introduced by this PR",
|
|
1644
|
+
"",
|
|
1645
|
+
"Requirements:",
|
|
1646
|
+
" • GitHub CLI (gh) must be installed and authenticated",
|
|
1647
|
+
" • Code Review must be enabled at claude.ai/admin-settings",
|
|
1648
|
+
" • Requires Teams or Enterprise subscription",
|
|
1649
|
+
"",
|
|
1650
|
+
"Machine-readable severity from check runs:",
|
|
1651
|
+
' gh api repos/OWNER/REPO/check-runs/ID --jq \'.output.text | split("bughunter-severity: ")[1] | split(" -->")[0] | fromjson\'',
|
|
1652
|
+
];
|
|
1653
|
+
await sendCommandMessage(input.sessionID, helpLines.join("\n"));
|
|
1654
|
+
return;
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
await sendCommandMessage(
|
|
1657
|
+
input.sessionID,
|
|
1658
|
+
`▣ Anthropic Review (error)\n\n${err instanceof Error ? err.message : String(err)}`,
|
|
1659
|
+
);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1297
1664
|
// Interactive CLI command is not compatible with slash flow.
|
|
1298
1665
|
if (primary === "manage" || primary === "mg") {
|
|
1299
1666
|
await sendCommandMessage(
|
|
@@ -1653,12 +2020,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1653
2020
|
if (!input.command) input.command = {};
|
|
1654
2021
|
input.command["anthropic"] = {
|
|
1655
2022
|
template: "/anthropic",
|
|
1656
|
-
description: "Manage Anthropic auth, config,
|
|
2023
|
+
description: "Manage Anthropic auth, config, betas, review (usage, login, config, set, betas, review, switch)",
|
|
1657
2024
|
};
|
|
1658
2025
|
},
|
|
1659
2026
|
"command.execute.before": async (input, output) => {
|
|
1660
2027
|
if (input.command !== "anthropic") return;
|
|
1661
2028
|
|
|
2029
|
+
// Signal that this command is fully handled by the plugin —
|
|
2030
|
+
// do NOT forward it to the agent for further processing.
|
|
2031
|
+
output.noReply = true;
|
|
2032
|
+
|
|
1662
2033
|
try {
|
|
1663
2034
|
await handleAnthropicSlashCommand(input);
|
|
1664
2035
|
} catch (err) {
|
|
@@ -1788,6 +2159,26 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
1788
2159
|
await accountManager.syncActiveIndexFromDisk();
|
|
1789
2160
|
}
|
|
1790
2161
|
|
|
2162
|
+
// Willow Mode: if the session has been idle for longer than the
|
|
2163
|
+
// configured threshold and has enough turns, show a gentle toast
|
|
2164
|
+
// suggesting the user consider starting a fresh context.
|
|
2165
|
+
if (WILLOW_ENABLED && showUsageToast) {
|
|
2166
|
+
const now = Date.now();
|
|
2167
|
+
const idleMs = now - willowLastRequestTime;
|
|
2168
|
+
const cooldownOk = now - willowLastSuggestionTime >= WILLOW_COOLDOWN_MS;
|
|
2169
|
+
if (idleMs >= WILLOW_IDLE_THRESHOLD_MS && cooldownOk && sessionMetrics.turns >= WILLOW_MIN_TURNS) {
|
|
2170
|
+
const idleMin = Math.round(idleMs / 60_000);
|
|
2171
|
+
willowLastSuggestionTime = now;
|
|
2172
|
+
toast(
|
|
2173
|
+
`🌿 Idle for ${idleMin}m with ${sessionMetrics.turns} turns of context. Consider /clear for a fresh start.`,
|
|
2174
|
+
"info",
|
|
2175
|
+
{ debounceKey: "willow-idle" },
|
|
2176
|
+
).catch(() => {});
|
|
2177
|
+
debugLog("willow mode: idle return detected", { idleMin, turns: sessionMetrics.turns });
|
|
2178
|
+
}
|
|
2179
|
+
willowLastRequestTime = now;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
1791
2182
|
// Try each account at most once. If the error is account-specific,
|
|
1792
2183
|
// switch to the next account. If it's service-wide, return immediately.
|
|
1793
2184
|
// QA fix M9: use enabled account count, not total (disabled accounts can't serve requests)
|
|
@@ -2060,9 +2451,41 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
|
|
|
2060
2451
|
...requestInit,
|
|
2061
2452
|
body,
|
|
2062
2453
|
headers: requestHeaders,
|
|
2454
|
+
// Disable keepalive when a previous ECONNRESET/EPIPE was detected
|
|
2455
|
+
// to force a fresh TCP connection and avoid stale socket reuse.
|
|
2456
|
+
...(requestInit._disableKeepalive ? { keepalive: false, agent: false } : {}),
|
|
2063
2457
|
});
|
|
2064
2458
|
} catch (err) {
|
|
2065
2459
|
const fetchError = err instanceof Error ? err : new Error(String(err));
|
|
2460
|
+
const errMsg = fetchError.message || "";
|
|
2461
|
+
const errCode = /** @type {any} */ (fetchError).code || "";
|
|
2462
|
+
|
|
2463
|
+
// ECONNRESET/EPIPE recovery: these indicate a stale TCP connection
|
|
2464
|
+
// (server closed it while we were writing/reading). Disable keepalive
|
|
2465
|
+
// on the next attempt to force a fresh connection.
|
|
2466
|
+
const isConnectionReset =
|
|
2467
|
+
errCode === "ECONNRESET" ||
|
|
2468
|
+
errCode === "EPIPE" ||
|
|
2469
|
+
errCode === "ECONNABORTED" ||
|
|
2470
|
+
errMsg.includes("ECONNRESET") ||
|
|
2471
|
+
errMsg.includes("EPIPE") ||
|
|
2472
|
+
errMsg.includes("socket hang up") ||
|
|
2473
|
+
errMsg.includes("network socket disconnected");
|
|
2474
|
+
|
|
2475
|
+
if (isConnectionReset) {
|
|
2476
|
+
requestInit._disableKeepalive = true;
|
|
2477
|
+
debugLog("connection reset detected, disabling keepalive for retry", {
|
|
2478
|
+
code: errCode,
|
|
2479
|
+
message: errMsg,
|
|
2480
|
+
});
|
|
2481
|
+
// Don't mark the account as failed — this is a transport issue, not auth.
|
|
2482
|
+
// Retry the same account with keepalive disabled.
|
|
2483
|
+
if (accountManager && account) {
|
|
2484
|
+
lastError = fetchError;
|
|
2485
|
+
attempt--; // Don't consume an account slot
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2066
2489
|
|
|
2067
2490
|
if (accountManager && account) {
|
|
2068
2491
|
accountManager.markFailure(account);
|
|
@@ -3133,17 +3556,21 @@ process.once("beforeExit", () => {
|
|
|
3133
3556
|
// Request building helpers (extracted from original fetch interceptor)
|
|
3134
3557
|
// ---------------------------------------------------------------------------
|
|
3135
3558
|
|
|
3136
|
-
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.
|
|
3559
|
+
const FALLBACK_CLAUDE_CLI_VERSION = "2.1.88";
|
|
3137
3560
|
const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
3138
|
-
const CLAUDE_CODE_BUILD_TIME = "2026-03-
|
|
3561
|
+
const CLAUDE_CODE_BUILD_TIME = "2026-03-30T21:59:52Z";
|
|
3139
3562
|
|
|
3140
|
-
// The @anthropic-ai/sdk version bundled with Claude Code v2.1.
|
|
3563
|
+
// The @anthropic-ai/sdk version bundled with Claude Code v2.1.88.
|
|
3141
3564
|
// This is distinct from the CLI version and goes in X-Stainless-Package-Version.
|
|
3142
|
-
// Verified by extracting VERSION="0.208.0" from the bundled cli.js of
|
|
3565
|
+
// Verified by extracting VERSION="0.208.0" from the bundled cli.js of all versions .83-.88.
|
|
3143
3566
|
const ANTHROPIC_SDK_VERSION = "0.208.0";
|
|
3144
3567
|
|
|
3145
3568
|
// Map of CLI version → bundled SDK version (update when CLI version changes)
|
|
3146
3569
|
const CLI_TO_SDK_VERSION = new Map([
|
|
3570
|
+
["2.1.88", "0.208.0"],
|
|
3571
|
+
["2.1.87", "0.208.0"],
|
|
3572
|
+
["2.1.86", "0.208.0"],
|
|
3573
|
+
["2.1.85", "0.208.0"],
|
|
3147
3574
|
["2.1.84", "0.208.0"],
|
|
3148
3575
|
["2.1.83", "0.208.0"],
|
|
3149
3576
|
["2.1.81", "0.208.0"],
|
|
@@ -3240,6 +3667,8 @@ const BETA_SHORTCUTS = new Map([
|
|
|
3240
3667
|
["fast", "fast-mode-2026-02-01"],
|
|
3241
3668
|
["fast-mode", "fast-mode-2026-02-01"],
|
|
3242
3669
|
["opus-fast", "fast-mode-2026-02-01"],
|
|
3670
|
+
["task-budgets", "task-budgets-2026-03-13"],
|
|
3671
|
+
["budgets", "task-budgets-2026-03-13"],
|
|
3243
3672
|
]);
|
|
3244
3673
|
const STAINLESS_HELPER_KEYS = [
|
|
3245
3674
|
"x_stainless_helper",
|
|
@@ -3962,9 +4391,6 @@ function buildAnthropicBetaHeader(
|
|
|
3962
4391
|
betas.push("structured-outputs-2025-12-15");
|
|
3963
4392
|
}
|
|
3964
4393
|
|
|
3965
|
-
// Tool examples — always-on in Claude Code
|
|
3966
|
-
betas.push("tool-examples-2025-10-29");
|
|
3967
|
-
|
|
3968
4394
|
// Web search — for models that support it
|
|
3969
4395
|
if (supportsWebSearch(model)) {
|
|
3970
4396
|
betas.push("web-search-2025-03-05");
|
|
@@ -4367,6 +4793,17 @@ function transformRequestBody(body, signature, runtime, betaHeader) {
|
|
|
4367
4793
|
return msg;
|
|
4368
4794
|
});
|
|
4369
4795
|
}
|
|
4796
|
+
// Task budgets: when the task-budgets beta is active, preserve or inject output_config.
|
|
4797
|
+
// The beta unlocks output_config.max_output_tokens for per-task budget control.
|
|
4798
|
+
// Model-router compatibility: the beta header + output_config body are forwarded as-is.
|
|
4799
|
+
if (betaHeader && betaHeader.includes("task-budgets-2026-03-13")) {
|
|
4800
|
+
if (!parsed.output_config) {
|
|
4801
|
+
// Default: set a reasonable per-task output budget for long-running agentic tasks.
|
|
4802
|
+
// Claude Code tasks typically need generous output budgets.
|
|
4803
|
+
parsed.output_config = { max_output_tokens: 16384 };
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4370
4807
|
// Fast mode: inject speed parameter for supported models
|
|
4371
4808
|
const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
|
|
4372
4809
|
if (fastModeEnabled && parsed.model && (isOpus46Model(parsed.model) || isSonnet46Model(parsed.model))) {
|
package/lib/config.mjs
CHANGED
|
@@ -154,6 +154,18 @@ export const DEFAULT_CONFIG = {
|
|
|
154
154
|
escalation_threshold: 150_000,
|
|
155
155
|
deescalation_threshold: 100_000,
|
|
156
156
|
},
|
|
157
|
+
/** Willow Mode: detect inactivity and suggest context reset. */
|
|
158
|
+
willow_mode: {
|
|
159
|
+
/** Enable idle detection and context-reset suggestions. */
|
|
160
|
+
enabled: true,
|
|
161
|
+
/** Minutes of inactivity before suggesting a fresh context. */
|
|
162
|
+
idle_threshold_minutes: 30,
|
|
163
|
+
/** Minimum minutes between willow suggestions (to avoid nagging). */
|
|
164
|
+
cooldown_minutes: 60,
|
|
165
|
+
/** Minimum turns in the current session before suggesting reset
|
|
166
|
+
* (no point suggesting reset at the start of a session). */
|
|
167
|
+
min_turns_before_suggest: 3,
|
|
168
|
+
},
|
|
157
169
|
};
|
|
158
170
|
|
|
159
171
|
export const VALID_STRATEGIES = ["sticky", "round-robin", "hybrid"];
|
|
@@ -181,6 +193,7 @@ function createDefaultConfig() {
|
|
|
181
193
|
headers: { ...DEFAULT_CONFIG.headers, overrides: {}, disable: [] },
|
|
182
194
|
cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
|
|
183
195
|
adaptive_context: { ...DEFAULT_CONFIG.adaptive_context },
|
|
196
|
+
willow_mode: { ...DEFAULT_CONFIG.willow_mode },
|
|
184
197
|
};
|
|
185
198
|
}
|
|
186
199
|
|