@khalilgharbaoui/opencode-claude-code-plugin 0.22.1 → 0.24.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 +33 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.js +1186 -676
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +35 -0
package/dist/index.js
CHANGED
|
@@ -127,6 +127,7 @@ var log = {
|
|
|
127
127
|
// src/todo-ledger.ts
|
|
128
128
|
var ledgers = /* @__PURE__ */ new Map();
|
|
129
129
|
var PENDING_CREATE_TTL_MS = 6e4;
|
|
130
|
+
var MAX_LEDGER_SESSIONS = 64;
|
|
130
131
|
var TASK_CREATED_PATTERN = /Task\s*#?\s*(\d+)\s+created/i;
|
|
131
132
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
132
133
|
function getOrCreate(sessionId) {
|
|
@@ -134,9 +135,18 @@ function getOrCreate(sessionId) {
|
|
|
134
135
|
if (!ledger) {
|
|
135
136
|
ledger = { todos: /* @__PURE__ */ new Map(), pendingCreates: /* @__PURE__ */ new Map() };
|
|
136
137
|
ledgers.set(sessionId, ledger);
|
|
138
|
+
capLedgers();
|
|
137
139
|
}
|
|
138
140
|
return ledger;
|
|
139
141
|
}
|
|
142
|
+
function capLedgers() {
|
|
143
|
+
while (ledgers.size > MAX_LEDGER_SESSIONS) {
|
|
144
|
+
const oldest = ledgers.keys().next();
|
|
145
|
+
if (oldest.done) break;
|
|
146
|
+
ledgers.delete(oldest.value);
|
|
147
|
+
log.info("todo ledger evicted oldest session", { sessionId: oldest.value });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
140
150
|
function prunePending(ledger) {
|
|
141
151
|
const cutoff = Date.now() - PENDING_CREATE_TTL_MS;
|
|
142
152
|
for (const [id, pending] of ledger.pendingCreates) {
|
|
@@ -349,31 +359,825 @@ function mapTool(name, input, opts) {
|
|
|
349
359
|
if (!input) return { name: "bash", executed: false };
|
|
350
360
|
const output = input?.content || input?.output || JSON.stringify(input);
|
|
351
361
|
return {
|
|
352
|
-
name: "bash",
|
|
353
|
-
input: {
|
|
354
|
-
command: `printf '%s\\n' ${singleQuoteForShell(`TASK OUTPUT: ${String(output)}`)}`,
|
|
355
|
-
description: "Displaying task output"
|
|
356
|
-
},
|
|
357
|
-
executed: false
|
|
362
|
+
name: "bash",
|
|
363
|
+
input: {
|
|
364
|
+
command: `printf '%s\\n' ${singleQuoteForShell(`TASK OUTPUT: ${String(output)}`)}`,
|
|
365
|
+
description: "Displaying task output"
|
|
366
|
+
},
|
|
367
|
+
executed: false
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (name.startsWith("mcp__")) {
|
|
371
|
+
const parts = name.slice(5).split("__");
|
|
372
|
+
if (parts.length >= 2) {
|
|
373
|
+
const serverName = parts[0];
|
|
374
|
+
const toolName = parts.slice(1).join("_");
|
|
375
|
+
const openCodeName = `${serverName}_${toolName}`;
|
|
376
|
+
log.debug("mapping MCP tool", { original: name, mapped: openCodeName });
|
|
377
|
+
return { name: openCodeName, input, executed: true };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (OPENCODE_HANDLED_TOOLS.has(name)) {
|
|
381
|
+
const mappedInput = mapToolInput(name, input);
|
|
382
|
+
const openCodeName = name.toLowerCase();
|
|
383
|
+
log.debug("mapping CLI-executed tool", { name, openCodeName });
|
|
384
|
+
return { name: openCodeName, input: mappedInput, executed: true };
|
|
385
|
+
}
|
|
386
|
+
return { name, input, executed: true };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/accounts.ts
|
|
390
|
+
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
391
|
+
import path from "path";
|
|
392
|
+
var BASE_PROVIDER_ID = "claude-code";
|
|
393
|
+
var DEFAULT_ACCOUNT = "default";
|
|
394
|
+
var SHARED_CAPABILITY_ITEMS = [
|
|
395
|
+
"CLAUDE.md",
|
|
396
|
+
"settings.json",
|
|
397
|
+
"skills",
|
|
398
|
+
"agents",
|
|
399
|
+
"commands",
|
|
400
|
+
"plugins"
|
|
401
|
+
];
|
|
402
|
+
function normalizeAccountName(account) {
|
|
403
|
+
return account.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
404
|
+
}
|
|
405
|
+
function resolveAccounts(value) {
|
|
406
|
+
if (!Array.isArray(value)) return null;
|
|
407
|
+
const accounts = value.map((account) => normalizeAccountName(String(account))).filter(Boolean);
|
|
408
|
+
return Array.from(/* @__PURE__ */ new Set([DEFAULT_ACCOUNT, ...accounts]));
|
|
409
|
+
}
|
|
410
|
+
function accountProviderId(account) {
|
|
411
|
+
return `${BASE_PROVIDER_ID}-${normalizeAccountName(account)}`;
|
|
412
|
+
}
|
|
413
|
+
function accountDisplayName(account) {
|
|
414
|
+
return `Claude Code (${titleizeAccount(account)})`;
|
|
415
|
+
}
|
|
416
|
+
function accountModelSuffix(account) {
|
|
417
|
+
const normalized = normalizeAccountName(account);
|
|
418
|
+
return normalized === DEFAULT_ACCOUNT ? void 0 : normalized;
|
|
419
|
+
}
|
|
420
|
+
function accountConfigDir(account) {
|
|
421
|
+
const normalized = normalizeAccountName(account);
|
|
422
|
+
if (!normalized || normalized === DEFAULT_ACCOUNT) return void 0;
|
|
423
|
+
return `~/.claude-${normalized}`;
|
|
424
|
+
}
|
|
425
|
+
function expandHome(value) {
|
|
426
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
427
|
+
if (value === "~") return home ?? value;
|
|
428
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
429
|
+
return home ? path.join(home, value.slice(2)) : value;
|
|
430
|
+
}
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
async function ensureAccountRuntime(account, baseCliPath) {
|
|
434
|
+
const configDir = accountConfigDir(account);
|
|
435
|
+
if (!configDir) return { cliPath: baseCliPath };
|
|
436
|
+
const expandedConfigDir = expandHome(configDir);
|
|
437
|
+
await mkdir(expandedConfigDir, { recursive: true });
|
|
438
|
+
try {
|
|
439
|
+
await ensureSharedCapabilities(expandedConfigDir);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
log.warn("failed to symlink shared capabilities; continuing anyway", {
|
|
442
|
+
account,
|
|
443
|
+
configDir: expandedConfigDir,
|
|
444
|
+
error: String(err)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
const cliPath = await writeAccountWrapper(
|
|
448
|
+
normalizeAccountName(account),
|
|
449
|
+
baseCliPath,
|
|
450
|
+
expandedConfigDir
|
|
451
|
+
);
|
|
452
|
+
return { cliPath, configDir: expandedConfigDir };
|
|
453
|
+
}
|
|
454
|
+
async function ensureSharedCapabilities(targetRoot) {
|
|
455
|
+
const sourceRoot = expandHome("~/.claude");
|
|
456
|
+
for (const item of SHARED_CAPABILITY_ITEMS) {
|
|
457
|
+
await ensureSharedCapabilityItem(sourceRoot, targetRoot, item);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
461
|
+
const source = path.join(sourceRoot, item);
|
|
462
|
+
const target = path.join(targetRoot, item);
|
|
463
|
+
let sourceStat;
|
|
464
|
+
try {
|
|
465
|
+
sourceStat = await lstat(source);
|
|
466
|
+
} catch {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const targetStat = await lstat(target);
|
|
471
|
+
if (targetStat.isSymbolicLink()) {
|
|
472
|
+
const current = await readlink(target);
|
|
473
|
+
const resolvedCurrent = path.resolve(path.dirname(target), current);
|
|
474
|
+
const resolvedSource = path.resolve(source);
|
|
475
|
+
if (resolvedCurrent === resolvedSource) return;
|
|
476
|
+
}
|
|
477
|
+
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
478
|
+
item,
|
|
479
|
+
target,
|
|
480
|
+
source
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
const type = sourceStat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
|
|
486
|
+
await symlink(source, target, type);
|
|
487
|
+
}
|
|
488
|
+
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
489
|
+
const cacheRoot = path.join(
|
|
490
|
+
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
491
|
+
"opencode-claude-code-plugin"
|
|
492
|
+
);
|
|
493
|
+
const wrapperPath = path.join(cacheRoot, `claude-${account}`);
|
|
494
|
+
const suffix = `@${account}`;
|
|
495
|
+
await mkdir(cacheRoot, { recursive: true });
|
|
496
|
+
const script = `#!/usr/bin/env bash
|
|
497
|
+
set -euo pipefail
|
|
498
|
+
|
|
499
|
+
args=()
|
|
500
|
+
while [[ $# -gt 0 ]]; do
|
|
501
|
+
if [[ "$1" == "--model" && $# -ge 2 ]]; then
|
|
502
|
+
model="$2"
|
|
503
|
+
if [[ "$model" == *${shellDoubleQuote(suffix)} ]]; then
|
|
504
|
+
model="\${model%${shellDoubleQuote(suffix)}}"
|
|
505
|
+
fi
|
|
506
|
+
args+=("$1" "$model")
|
|
507
|
+
shift 2
|
|
508
|
+
else
|
|
509
|
+
args+=("$1")
|
|
510
|
+
shift
|
|
511
|
+
fi
|
|
512
|
+
done
|
|
513
|
+
|
|
514
|
+
export CLAUDE_CONFIG_DIR=${shellSingleQuote(configDir)}
|
|
515
|
+
exec ${shellSingleQuote(baseCliPath)} "\${args[@]}"
|
|
516
|
+
`;
|
|
517
|
+
await writeFile(wrapperPath, script, "utf8");
|
|
518
|
+
await chmod(wrapperPath, 493);
|
|
519
|
+
return wrapperPath;
|
|
520
|
+
}
|
|
521
|
+
function shellSingleQuote(value) {
|
|
522
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
523
|
+
}
|
|
524
|
+
function shellDoubleQuote(value) {
|
|
525
|
+
return value.replace(/[$`"\\]/g, "\\$&");
|
|
526
|
+
}
|
|
527
|
+
function titleizeAccount(account) {
|
|
528
|
+
return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/cli-events.ts
|
|
532
|
+
function isRecord(value) {
|
|
533
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
534
|
+
}
|
|
535
|
+
function str(value) {
|
|
536
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
537
|
+
}
|
|
538
|
+
function num(value) {
|
|
539
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
540
|
+
}
|
|
541
|
+
var RATE_LIMIT_MARKER = "\u258C **rate limit:**";
|
|
542
|
+
var RATE_LIMIT_ACTION = "Enable extra usage or add credits on the account, wait for the window to reset, switch account, org or plan, or authenticate with an API key.";
|
|
543
|
+
var OVERAGE_DISABLED_REASONS = {
|
|
544
|
+
overage_not_provisioned: "extra usage is not set up on this account",
|
|
545
|
+
org_level_disabled: "extra usage is disabled for your organization",
|
|
546
|
+
org_level_disabled_until: "extra usage is disabled for your organization for now",
|
|
547
|
+
out_of_credits: "the account's usage credits are spent",
|
|
548
|
+
seat_tier_level_disabled: "your seat tier does not allow extra usage",
|
|
549
|
+
member_level_disabled: "extra usage is disabled for your member account",
|
|
550
|
+
seat_tier_zero_credit_limit: "your seat tier has a zero credit limit",
|
|
551
|
+
group_zero_credit_limit: "your group has a zero credit limit",
|
|
552
|
+
member_zero_credit_limit: "your member account has a zero credit limit",
|
|
553
|
+
org_service_level_disabled: "your organization's service level does not include extra usage",
|
|
554
|
+
no_limits_configured: "no usage limits are configured for this account",
|
|
555
|
+
fetch_error: "the CLI could not read the account's usage limits"
|
|
556
|
+
};
|
|
557
|
+
var RATE_LIMIT_WINDOWS = {
|
|
558
|
+
five_hour: "the 5-hour window",
|
|
559
|
+
seven_day: "the 7-day window",
|
|
560
|
+
seven_day_opus: "the 7-day Opus window",
|
|
561
|
+
seven_day_sonnet: "the 7-day Sonnet window",
|
|
562
|
+
seven_day_overage_included: "the 7-day extra-usage window",
|
|
563
|
+
overage: "extra usage"
|
|
564
|
+
};
|
|
565
|
+
function parseRateLimitEvent(msg) {
|
|
566
|
+
if (msg.type !== "rate_limit_event") return null;
|
|
567
|
+
const info = msg.rate_limit_info;
|
|
568
|
+
if (!isRecord(info)) return null;
|
|
569
|
+
return {
|
|
570
|
+
status: str(info.status),
|
|
571
|
+
rateLimitType: str(info.rateLimitType),
|
|
572
|
+
resetsAt: num(info.resetsAt),
|
|
573
|
+
utilization: num(info.utilization),
|
|
574
|
+
isUsingOverage: info.isUsingOverage === true,
|
|
575
|
+
overageStatus: str(info.overageStatus),
|
|
576
|
+
overageResetsAt: num(info.overageResetsAt),
|
|
577
|
+
overageDisabledReason: str(info.overageDisabledReason)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function resetsAtToMs(resetsAt) {
|
|
581
|
+
if (resetsAt === void 0 || !Number.isFinite(resetsAt)) return void 0;
|
|
582
|
+
return resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
583
|
+
}
|
|
584
|
+
function formatResetsAt(resetsAt) {
|
|
585
|
+
const ms = resetsAtToMs(resetsAt);
|
|
586
|
+
if (ms === void 0) return void 0;
|
|
587
|
+
const date = new Date(ms);
|
|
588
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
589
|
+
}
|
|
590
|
+
function rateLimitKey(info) {
|
|
591
|
+
return [
|
|
592
|
+
info.status ?? "?",
|
|
593
|
+
info.rateLimitType ?? "?",
|
|
594
|
+
info.overageStatus ?? "?",
|
|
595
|
+
info.overageDisabledReason ?? "?"
|
|
596
|
+
].join("|");
|
|
597
|
+
}
|
|
598
|
+
function isRateLimitRejected(info) {
|
|
599
|
+
return info.status === "rejected" || info.overageStatus === "rejected";
|
|
600
|
+
}
|
|
601
|
+
function describeRateLimit(info) {
|
|
602
|
+
if (!info.status && !info.overageStatus) return null;
|
|
603
|
+
const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
|
|
604
|
+
const resets = formatResetsAt(info.resetsAt);
|
|
605
|
+
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
606
|
+
const key = rateLimitKey(info);
|
|
607
|
+
if (isRateLimitRejected(info)) {
|
|
608
|
+
const parts = [];
|
|
609
|
+
parts.push(
|
|
610
|
+
info.status === "rejected" ? `Claude Code rejected this request: you are out of usage${window ? ` in ${window}` : ""}.` : "Claude Code rejected this request: paid extra usage is not available on this account."
|
|
611
|
+
);
|
|
612
|
+
const reason = info.overageDisabledReason ? OVERAGE_DISABLED_REASONS[info.overageDisabledReason] ?? info.overageDisabledReason : void 0;
|
|
613
|
+
if (reason) parts.push(`Extra usage is unavailable because ${reason}.`);
|
|
614
|
+
const resetAt = resets ?? overageResets;
|
|
615
|
+
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
616
|
+
parts.push(RATE_LIMIT_ACTION);
|
|
617
|
+
const message = parts.join(" ");
|
|
618
|
+
return { key, level: "warn", message, transcript: `
|
|
619
|
+
${RATE_LIMIT_MARKER} ${message}
|
|
620
|
+
` };
|
|
621
|
+
}
|
|
622
|
+
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
623
|
+
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
624
|
+
return {
|
|
625
|
+
key,
|
|
626
|
+
level: "notice",
|
|
627
|
+
message: `Approaching the usage limit${window ? ` for ${window}` : ""}${used}${resets ? `, resets at ${resets}` : ""}.`,
|
|
628
|
+
transcript: null
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
key,
|
|
633
|
+
level: "info",
|
|
634
|
+
message: `usage limits updated${window ? ` for ${window}` : ""}`,
|
|
635
|
+
transcript: null
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
var reportedRateLimits = /* @__PURE__ */ new Set();
|
|
639
|
+
function reportRateLimitEvent(msg) {
|
|
640
|
+
const info = parseRateLimitEvent(msg);
|
|
641
|
+
if (!info) return null;
|
|
642
|
+
const report = describeRateLimit(info);
|
|
643
|
+
if (!report) return null;
|
|
644
|
+
const data = { ...info };
|
|
645
|
+
if (reportedRateLimits.has(report.key)) {
|
|
646
|
+
log.debug(report.message, data);
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
reportedRateLimits.add(report.key);
|
|
650
|
+
log[report.level](report.message, data);
|
|
651
|
+
return report.transcript;
|
|
652
|
+
}
|
|
653
|
+
var API_KEY_SOURCES = /* @__PURE__ */ new Set([
|
|
654
|
+
"ANTHROPIC_API_KEY",
|
|
655
|
+
"apiKeyHelper",
|
|
656
|
+
"/login managed key",
|
|
657
|
+
"user",
|
|
658
|
+
"project",
|
|
659
|
+
"org",
|
|
660
|
+
"temporary"
|
|
661
|
+
]);
|
|
662
|
+
function parseSystemInit(msg) {
|
|
663
|
+
if (msg.type !== "system" || msg.subtype !== "init") return null;
|
|
664
|
+
const raw = msg;
|
|
665
|
+
const servers = [];
|
|
666
|
+
if (Array.isArray(raw.mcp_servers)) {
|
|
667
|
+
for (const entry of raw.mcp_servers) {
|
|
668
|
+
if (!isRecord(entry)) continue;
|
|
669
|
+
servers.push({ name: str(entry.name) ?? "unknown", status: str(entry.status) ?? "unknown" });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
apiKeySource: str(raw.apiKeySource),
|
|
674
|
+
permissionMode: str(raw.permissionMode),
|
|
675
|
+
model: str(raw.model),
|
|
676
|
+
cliVersion: str(raw.claude_code_version),
|
|
677
|
+
toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
|
|
678
|
+
mcpServers: servers
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
682
|
+
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
683
|
+
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription's Agent SDK credit.`;
|
|
684
|
+
return ignoreAnthropicApiKey ? `${base} \`ignoreAnthropicApiKey\` is already on, so the key is not coming from the environment: check the CLI's own settings (\`claude config\`) or an \`apiKeyHelper\`.` : `${base} Set the provider option \`ignoreAnthropicApiKey: true\` to strip the key from spawns and fall back to the stored subscription auth.`;
|
|
685
|
+
}
|
|
686
|
+
var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
687
|
+
var warnedMcpFailures = /* @__PURE__ */ new Set();
|
|
688
|
+
function reportSystemInit(msg, options = {}) {
|
|
689
|
+
const info = parseSystemInit(msg);
|
|
690
|
+
if (!info) return;
|
|
691
|
+
log.info("claude session init", {
|
|
692
|
+
apiKeySource: info.apiKeySource ?? null,
|
|
693
|
+
permissionMode: info.permissionMode ?? null,
|
|
694
|
+
model: info.model ?? null,
|
|
695
|
+
cliVersion: info.cliVersion ?? null,
|
|
696
|
+
tools: info.toolCount,
|
|
697
|
+
mcpServers: info.mcpServers
|
|
698
|
+
});
|
|
699
|
+
for (const server2 of info.mcpServers) {
|
|
700
|
+
if (server2.status === "connected") continue;
|
|
701
|
+
const key2 = `${server2.name}:${server2.status}`;
|
|
702
|
+
const message = `MCP server "${server2.name}" is ${server2.status} in Claude Code; its tools are not available to the model this session.`;
|
|
703
|
+
if (warnedMcpFailures.has(key2)) {
|
|
704
|
+
log.debug(message, { server: server2.name, status: server2.status });
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
warnedMcpFailures.add(key2);
|
|
708
|
+
log.warn(message, { server: server2.name, status: server2.status });
|
|
709
|
+
}
|
|
710
|
+
const apiKeyMessage = apiKeySourceWarning(info.apiKeySource, options.ignoreAnthropicApiKey);
|
|
711
|
+
if (!apiKeyMessage) return;
|
|
712
|
+
const key = `${info.apiKeySource}:${options.ignoreAnthropicApiKey ? "ignored" : "passed"}`;
|
|
713
|
+
if (warnedApiKeySources.has(key)) {
|
|
714
|
+
log.debug(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
warnedApiKeySources.add(key);
|
|
718
|
+
log.warn(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
719
|
+
}
|
|
720
|
+
var COMPACT_BOUNDARY_MARKER = "\u258C **context compacted:**";
|
|
721
|
+
function parseCompactBoundary(msg) {
|
|
722
|
+
if (msg.type !== "system" || msg.subtype !== "compact_boundary") return null;
|
|
723
|
+
const raw = msg;
|
|
724
|
+
const meta = isRecord(raw.compact_metadata) ? raw.compact_metadata : isRecord(raw.compactMetadata) ? raw.compactMetadata : void 0;
|
|
725
|
+
return {
|
|
726
|
+
trigger: str(meta?.trigger) ?? "unknown",
|
|
727
|
+
preTokens: num(meta?.pre_tokens) ?? num(meta?.preTokens),
|
|
728
|
+
postTokens: num(meta?.post_tokens) ?? num(meta?.postTokens)
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function formatCompactBoundaryNote(boundary) {
|
|
732
|
+
const how = boundary.trigger === "auto" ? "on its own" : `on a ${boundary.trigger} request`;
|
|
733
|
+
const sizes = boundary.preTokens !== void 0 && boundary.postTokens !== void 0 ? ` (${boundary.preTokens.toLocaleString("en-US")} tokens to ${boundary.postTokens.toLocaleString("en-US")})` : "";
|
|
734
|
+
return `
|
|
735
|
+
${COMPACT_BOUNDARY_MARKER} Claude Code compacted its own context ${how}${sizes}. Earlier detail in this conversation is now a summary.
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
function reportCompactBoundary(msg) {
|
|
739
|
+
const boundary = parseCompactBoundary(msg);
|
|
740
|
+
if (!boundary) return null;
|
|
741
|
+
log.notice("claude code compacted its own context", {
|
|
742
|
+
trigger: boundary.trigger,
|
|
743
|
+
preTokens: boundary.preTokens ?? null,
|
|
744
|
+
postTokens: boundary.postTokens ?? null
|
|
745
|
+
});
|
|
746
|
+
return formatCompactBoundaryNote(boundary);
|
|
747
|
+
}
|
|
748
|
+
var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
|
|
749
|
+
var RESULT_SUBTYPES = {
|
|
750
|
+
error_max_turns: "it hit its internal turn limit before finishing",
|
|
751
|
+
error_during_execution: "it failed while running the turn",
|
|
752
|
+
error_max_budget_usd: "it hit the configured spend limit for the turn",
|
|
753
|
+
error_max_structured_output_retries: "it could not produce valid structured output"
|
|
754
|
+
};
|
|
755
|
+
function describeResultFailure(msg) {
|
|
756
|
+
if (msg.type !== "result") return null;
|
|
757
|
+
const subtype = msg.subtype;
|
|
758
|
+
if (!subtype || subtype === "success") return null;
|
|
759
|
+
const explanation = RESULT_SUBTYPES[subtype];
|
|
760
|
+
return explanation ? `Claude Code ended the turn with \`${subtype}\`: ${explanation}.` : `Claude Code ended the turn with \`${subtype}\`.`;
|
|
761
|
+
}
|
|
762
|
+
function formatResultFailureNote(message) {
|
|
763
|
+
return `
|
|
764
|
+
${RESULT_ERROR_MARKER} ${message}
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
var STREAM_TIMEOUT_MARKER = "\u258C **stream timeout:**";
|
|
768
|
+
function formatStreamTimeoutNote(silenceMs) {
|
|
769
|
+
const seconds = Math.max(1, Math.round(silenceMs / 1e3));
|
|
770
|
+
return `
|
|
771
|
+
${STREAM_TIMEOUT_MARKER} The Claude Code CLI produced output and then went silent for ${seconds}s without finishing the turn, so it was closed without a result. The answer above may be incomplete.
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/plan-mode-question.ts
|
|
776
|
+
var QUESTION_TOOL_NAME = "question";
|
|
777
|
+
var APPROVED_EXIT_PLAN_MODE_MESSAGE = "User has approved your plan. You can now start coding. Start with updating your todo list if applicable.";
|
|
778
|
+
var REJECTED_EXIT_PLAN_MODE_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected. To tell you how to proceed, the user said:";
|
|
779
|
+
var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
780
|
+
var OPENCODE_QUESTION_RESULT_PREFIX = `User has answered your questions: "${PLAN_MODE_APPROVAL_QUESTION}"="`;
|
|
781
|
+
var OPENCODE_QUESTION_RESULT_SUFFIX = `". You can now continue with the user's answers in mind.`;
|
|
782
|
+
var KEY_SEPARATOR = "\0";
|
|
783
|
+
function isPlanModeQuestionActive(input) {
|
|
784
|
+
if (input.compactionMode) return false;
|
|
785
|
+
if (input.configured !== true) return false;
|
|
786
|
+
return input.opencodeHasQuestion;
|
|
787
|
+
}
|
|
788
|
+
var pendingQuestions = /* @__PURE__ */ new Map();
|
|
789
|
+
function pendingKey(sessionKey2, questionToolCallId) {
|
|
790
|
+
return `${sessionKey2}${KEY_SEPARATOR}${questionToolCallId}`;
|
|
791
|
+
}
|
|
792
|
+
function clearExitPlanModeQuestions(sessionKey2) {
|
|
793
|
+
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
794
|
+
for (const key of pendingQuestions.keys()) {
|
|
795
|
+
if (key.startsWith(prefix)) pendingQuestions.delete(key);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function hasExitPlanModeQuestions(sessionKey2) {
|
|
799
|
+
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
800
|
+
return [...pendingQuestions.keys()].some((key) => key.startsWith(prefix));
|
|
801
|
+
}
|
|
802
|
+
function createExitPlanModeQuestionCall(sessionKey2, exitPlanModeToolUseId, plan, questionToolCallId = `exit_plan_question_${exitPlanModeToolUseId}`) {
|
|
803
|
+
pendingQuestions.set(pendingKey(sessionKey2, questionToolCallId), exitPlanModeToolUseId);
|
|
804
|
+
return {
|
|
805
|
+
toolCallId: questionToolCallId,
|
|
806
|
+
toolName: QUESTION_TOOL_NAME,
|
|
807
|
+
input: {
|
|
808
|
+
questions: [
|
|
809
|
+
{
|
|
810
|
+
header: "Plan approval",
|
|
811
|
+
question: PLAN_MODE_APPROVAL_QUESTION,
|
|
812
|
+
options: [
|
|
813
|
+
{ label: "yes", description: "" },
|
|
814
|
+
{ label: "no", description: "" }
|
|
815
|
+
],
|
|
816
|
+
multiple: false,
|
|
817
|
+
custom: true
|
|
818
|
+
}
|
|
819
|
+
]
|
|
820
|
+
},
|
|
821
|
+
text: plan ? `
|
|
822
|
+
|
|
823
|
+
${plan}
|
|
824
|
+
` : "\n\n"
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function buildToolResultMessage(input) {
|
|
828
|
+
return JSON.stringify({
|
|
829
|
+
type: "user",
|
|
830
|
+
message: {
|
|
831
|
+
role: "user",
|
|
832
|
+
content: [
|
|
833
|
+
input.approved ? {
|
|
834
|
+
type: "tool_result",
|
|
835
|
+
tool_use_id: input.toolUseId,
|
|
836
|
+
content: APPROVED_EXIT_PLAN_MODE_MESSAGE
|
|
837
|
+
} : {
|
|
838
|
+
type: "tool_result",
|
|
839
|
+
tool_use_id: input.toolUseId,
|
|
840
|
+
content: `${REJECTED_EXIT_PLAN_MODE_PREFIX}
|
|
841
|
+
${input.feedback || "no"}`,
|
|
842
|
+
is_error: true
|
|
843
|
+
}
|
|
844
|
+
]
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
function tryParseJson(text) {
|
|
849
|
+
try {
|
|
850
|
+
return JSON.parse(text);
|
|
851
|
+
} catch {
|
|
852
|
+
return text;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function unwrapToolOutput(part) {
|
|
856
|
+
const output = part?.output ?? part?.result;
|
|
857
|
+
if (typeof output === "string") return tryParseJson(output);
|
|
858
|
+
if (!output || typeof output !== "object") return output;
|
|
859
|
+
switch (output.type) {
|
|
860
|
+
case "json":
|
|
861
|
+
case "error-json":
|
|
862
|
+
return output.value;
|
|
863
|
+
case "text":
|
|
864
|
+
case "error-text":
|
|
865
|
+
return tryParseJson(String(output.value ?? ""));
|
|
866
|
+
case "execution-denied":
|
|
867
|
+
return {
|
|
868
|
+
denied: true,
|
|
869
|
+
reason: String(output.reason ?? "question rejected")
|
|
870
|
+
};
|
|
871
|
+
case "content":
|
|
872
|
+
return Array.isArray(output.value) ? output.value.map((item) => {
|
|
873
|
+
if (item?.type === "text") return item.text;
|
|
874
|
+
return JSON.stringify(item);
|
|
875
|
+
}).join("\n") : output.value;
|
|
876
|
+
default:
|
|
877
|
+
return output;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function unwrapOpencodeQuestionResult(value) {
|
|
881
|
+
if (value.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) && value.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
882
|
+
return value.slice(
|
|
883
|
+
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
884
|
+
-OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
function collectAnswerStrings(value) {
|
|
890
|
+
if (typeof value === "string") return [unwrapOpencodeQuestionResult(value)];
|
|
891
|
+
if (Array.isArray(value)) return value.flatMap(collectAnswerStrings);
|
|
892
|
+
if (!value || typeof value !== "object") return [];
|
|
893
|
+
const obj = value;
|
|
894
|
+
if (obj.denied === true) return [String(obj.reason ?? "question rejected")];
|
|
895
|
+
for (const key of ["answers", "answer", "selected", "selection", "value"]) {
|
|
896
|
+
if (key in obj) return collectAnswerStrings(obj[key]);
|
|
897
|
+
}
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
function classifyQuestionResult(part) {
|
|
901
|
+
const output = unwrapToolOutput(part);
|
|
902
|
+
const answers = collectAnswerStrings(output).map((answer) => answer.trim()).filter(Boolean);
|
|
903
|
+
if (answers.length === 1 && answers[0].toLowerCase() === "yes") {
|
|
904
|
+
return { approved: true, feedback: "" };
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
approved: false,
|
|
908
|
+
feedback: answers.length > 0 ? answers.join("\n") : "no"
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function consumeExitPlanModeQuestionResult(sessionKey2, prompt) {
|
|
912
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
913
|
+
const msg = prompt[i];
|
|
914
|
+
if (!Array.isArray(msg.content)) continue;
|
|
915
|
+
for (const part of msg.content) {
|
|
916
|
+
if (part?.type !== "tool-result" || typeof part.toolCallId !== "string") {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
const key = pendingKey(sessionKey2, part.toolCallId);
|
|
920
|
+
const exitPlanModeToolUseId = pendingQuestions.get(key);
|
|
921
|
+
if (!exitPlanModeToolUseId) continue;
|
|
922
|
+
pendingQuestions.delete(key);
|
|
923
|
+
const result = classifyQuestionResult(part);
|
|
924
|
+
return buildToolResultMessage({
|
|
925
|
+
toolUseId: exitPlanModeToolUseId,
|
|
926
|
+
approved: result.approved,
|
|
927
|
+
feedback: result.feedback
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/account-failover.ts
|
|
935
|
+
var FAILOVER_MARKER = "\u258C **account failover:**";
|
|
936
|
+
var ACCOUNT_FAILOVER_TOOL_CALL_PREFIX = "account_failover_";
|
|
937
|
+
var KEY_SEPARATOR2 = "\0";
|
|
938
|
+
var STOP_ANSWER = "stop";
|
|
939
|
+
var ACCOUNT_LIMIT_PATTERNS = [
|
|
940
|
+
/third-party apps now draw from your extra usage/i,
|
|
941
|
+
/you[’'`]?ve hit your individual spend limit/i
|
|
942
|
+
];
|
|
943
|
+
function isAccountLimitError(input) {
|
|
944
|
+
if (input.rateLimit && isRateLimitRejected(input.rateLimit)) return true;
|
|
945
|
+
const text = input.resultText;
|
|
946
|
+
if (typeof text !== "string" || text.length === 0) return false;
|
|
947
|
+
return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
|
|
948
|
+
}
|
|
949
|
+
var accountOverrides = /* @__PURE__ */ new Map();
|
|
950
|
+
function setAccountOverride(source, target, until, now = Date.now()) {
|
|
951
|
+
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
952
|
+
const to = normalizeAccountName(target);
|
|
953
|
+
if (!to || to === from) return;
|
|
954
|
+
if (until !== void 0 && until <= now) {
|
|
955
|
+
log.notice("ignoring a failover reset time that is not in the future", {
|
|
956
|
+
source: from,
|
|
957
|
+
target: to,
|
|
958
|
+
until
|
|
959
|
+
});
|
|
960
|
+
until = void 0;
|
|
961
|
+
}
|
|
962
|
+
accountOverrides.set(from, { target: to, until });
|
|
963
|
+
log.warn(
|
|
964
|
+
`Claude account "${from}" is out of usage; this and every other session on it now runs on "${to}"${until ? ` until ${new Date(until).toISOString()}` : " until opencode restarts"}.`,
|
|
965
|
+
{ source: from, target: to, until: until ?? null }
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
function resolveAccountOverride(source, now = Date.now()) {
|
|
969
|
+
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
970
|
+
const entry = accountOverrides.get(from);
|
|
971
|
+
if (!entry) return void 0;
|
|
972
|
+
if (entry.until !== void 0 && entry.until <= now) {
|
|
973
|
+
accountOverrides.delete(from);
|
|
974
|
+
log.notice(
|
|
975
|
+
`Claude account "${from}" should have usage again; switching back from "${entry.target}".`,
|
|
976
|
+
{ source: from, target: entry.target, until: entry.until }
|
|
977
|
+
);
|
|
978
|
+
return void 0;
|
|
979
|
+
}
|
|
980
|
+
return entry.target;
|
|
981
|
+
}
|
|
982
|
+
function stripAccountSuffix(modelId) {
|
|
983
|
+
const at = modelId.indexOf("@");
|
|
984
|
+
return at === -1 ? modelId : modelId.slice(0, at);
|
|
985
|
+
}
|
|
986
|
+
async function resolveFailoverSpawn(input) {
|
|
987
|
+
const unchanged = {
|
|
988
|
+
cliPath: input.cliPath,
|
|
989
|
+
modelId: input.modelId,
|
|
990
|
+
failedOver: false
|
|
991
|
+
};
|
|
992
|
+
const source = normalizeAccountName(input.account || DEFAULT_ACCOUNT);
|
|
993
|
+
const target = resolveAccountOverride(source, input.now);
|
|
994
|
+
if (!target) return unchanged;
|
|
995
|
+
try {
|
|
996
|
+
const cliPath = target === DEFAULT_ACCOUNT ? input.baseCliPath : (await ensureAccountRuntime(target, input.baseCliPath)).cliPath;
|
|
997
|
+
return {
|
|
998
|
+
cliPath,
|
|
999
|
+
modelId: stripAccountSuffix(input.modelId),
|
|
1000
|
+
target,
|
|
1001
|
+
failedOver: true
|
|
1002
|
+
};
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
log.error("failed to prepare the failover account runtime; staying put", {
|
|
1005
|
+
source,
|
|
1006
|
+
target,
|
|
1007
|
+
error: String(err)
|
|
1008
|
+
});
|
|
1009
|
+
return unchanged;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function isAccountFailoverQuestionActive(input) {
|
|
1013
|
+
if (input.compactionMode) return false;
|
|
1014
|
+
if (input.interactive) return false;
|
|
1015
|
+
if (input.childSession) return false;
|
|
1016
|
+
if (input.configured === "off") return false;
|
|
1017
|
+
if (input.candidates.length === 0) return false;
|
|
1018
|
+
return input.opencodeHasQuestion;
|
|
1019
|
+
}
|
|
1020
|
+
function failoverCandidates(accounts, source) {
|
|
1021
|
+
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
1022
|
+
const out = [];
|
|
1023
|
+
for (const raw of accounts ?? []) {
|
|
1024
|
+
const name = normalizeAccountName(String(raw));
|
|
1025
|
+
if (!name || name === from || out.includes(name)) continue;
|
|
1026
|
+
out.push(name);
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
var pendingQuestions2 = /* @__PURE__ */ new Map();
|
|
1031
|
+
function pendingKey2(sessionKey2, toolCallId) {
|
|
1032
|
+
return `${sessionKey2}${KEY_SEPARATOR2}${toolCallId}`;
|
|
1033
|
+
}
|
|
1034
|
+
function clearAccountFailoverQuestions(sessionKey2) {
|
|
1035
|
+
const prefix = `${sessionKey2}${KEY_SEPARATOR2}`;
|
|
1036
|
+
for (const key of pendingQuestions2.keys()) {
|
|
1037
|
+
if (key.startsWith(prefix)) pendingQuestions2.delete(key);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function describeReset(resetsAt) {
|
|
1041
|
+
return formatResetsAt(resetsAt);
|
|
1042
|
+
}
|
|
1043
|
+
function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${ACCOUNT_FAILOVER_TOOL_CALL_PREFIX}${Math.random().toString(36).slice(2, 10)}`) {
|
|
1044
|
+
const source = normalizeAccountName(input.sourceAccount || DEFAULT_ACCOUNT);
|
|
1045
|
+
const candidates = input.candidates.map((c) => normalizeAccountName(c));
|
|
1046
|
+
const resets = describeReset(input.resetsAt);
|
|
1047
|
+
const until = resets ?? "opencode restarts";
|
|
1048
|
+
pendingQuestions2.set(pendingKey2(sessionKey2, toolCallId), {
|
|
1049
|
+
sourceAccount: source,
|
|
1050
|
+
candidates: [...candidates],
|
|
1051
|
+
resetsAt: input.resetsAt
|
|
1052
|
+
});
|
|
1053
|
+
const question = [
|
|
1054
|
+
`The Claude account "${source}" is out of usage`,
|
|
1055
|
+
input.window ? ` in ${input.window}` : "",
|
|
1056
|
+
resets ? `, which resets at ${resets}` : "",
|
|
1057
|
+
". Continue this task on another configured account? Leaving this unanswered waits, at no cost."
|
|
1058
|
+
].join("");
|
|
1059
|
+
return {
|
|
1060
|
+
toolCallId,
|
|
1061
|
+
toolName: QUESTION_TOOL_NAME,
|
|
1062
|
+
input: {
|
|
1063
|
+
questions: [
|
|
1064
|
+
{
|
|
1065
|
+
header: "Account limit",
|
|
1066
|
+
question,
|
|
1067
|
+
options: [
|
|
1068
|
+
...candidates.map((candidate) => ({
|
|
1069
|
+
label: candidate,
|
|
1070
|
+
description: `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session (a session cannot resume across accounts), and any MCP server configured only in "${source}"'s Claude profile will be missing.`
|
|
1071
|
+
})),
|
|
1072
|
+
{
|
|
1073
|
+
label: STOP_ANSWER,
|
|
1074
|
+
description: "End this turn now and leave the account as it is."
|
|
1075
|
+
}
|
|
1076
|
+
],
|
|
1077
|
+
multiple: false,
|
|
1078
|
+
custom: true
|
|
1079
|
+
}
|
|
1080
|
+
]
|
|
1081
|
+
},
|
|
1082
|
+
text: ""
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function isDenied(output) {
|
|
1086
|
+
return !!output && typeof output === "object" && output.denied === true;
|
|
1087
|
+
}
|
|
1088
|
+
function classify(pending, part) {
|
|
1089
|
+
const output = unwrapToolOutput(part);
|
|
1090
|
+
if (isDenied(output)) {
|
|
1091
|
+
return {
|
|
1092
|
+
kind: "stop",
|
|
1093
|
+
reason: String(output.reason ?? "question rejected")
|
|
358
1094
|
};
|
|
359
1095
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
1096
|
+
const answers = collectAnswerStrings(output).map((answer) => answer.trim()).filter(Boolean);
|
|
1097
|
+
if (answers.length === 0) return { kind: "stop", reason: "no answer" };
|
|
1098
|
+
const picked = normalizeAccountName(answers[0]);
|
|
1099
|
+
if (picked === STOP_ANSWER) {
|
|
1100
|
+
return { kind: "stop", reason: "the operator chose to stop" };
|
|
1101
|
+
}
|
|
1102
|
+
const target = pending.candidates.find((candidate) => candidate === picked);
|
|
1103
|
+
if (!target) {
|
|
1104
|
+
return { kind: "stop", reason: `unrecognised answer "${answers[0]}"` };
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
kind: "switch",
|
|
1108
|
+
target,
|
|
1109
|
+
sourceAccount: pending.sourceAccount,
|
|
1110
|
+
resetsAt: pending.resetsAt
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function consumeAccountFailoverAnswer(sessionKey2, prompt) {
|
|
1114
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1115
|
+
const msg = prompt[i];
|
|
1116
|
+
if (!Array.isArray(msg.content)) continue;
|
|
1117
|
+
for (const part of msg.content) {
|
|
1118
|
+
if (part?.type !== "tool-result" || typeof part.toolCallId !== "string") {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
const key = pendingKey2(sessionKey2, part.toolCallId);
|
|
1122
|
+
const pending = pendingQuestions2.get(key);
|
|
1123
|
+
if (!pending) continue;
|
|
1124
|
+
pendingQuestions2.delete(key);
|
|
1125
|
+
return classify(pending, part);
|
|
368
1126
|
}
|
|
369
1127
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
function formatFailoverNote(input) {
|
|
1131
|
+
const resets = describeReset(input.resetsAt);
|
|
1132
|
+
return `
|
|
1133
|
+
${FAILOVER_MARKER} "${input.sourceAccount}" is out of usage, so this conversation continues on "${input.target}" ${resets ? `until ${resets}` : "until opencode restarts"}. Claude cannot resume a session across accounts, so the thread is being replayed into a fresh one.
|
|
1134
|
+
`;
|
|
1135
|
+
}
|
|
1136
|
+
function formatFailoverStopNote(reason) {
|
|
1137
|
+
return `
|
|
1138
|
+
${FAILOVER_MARKER} Staying on this account (${reason}). The turn ends here; the usage limit is unchanged.
|
|
1139
|
+
`;
|
|
1140
|
+
}
|
|
1141
|
+
function isFailoverPart(part) {
|
|
1142
|
+
if (!part || typeof part.toolCallId !== "string") return false;
|
|
1143
|
+
if (part.type !== "tool-call" && part.type !== "tool-result") return false;
|
|
1144
|
+
return part.toolCallId.startsWith(ACCOUNT_FAILOVER_TOOL_CALL_PREFIX);
|
|
1145
|
+
}
|
|
1146
|
+
function stripAccountFailoverParts(prompt) {
|
|
1147
|
+
let changed = false;
|
|
1148
|
+
const out = [];
|
|
1149
|
+
for (const message of prompt) {
|
|
1150
|
+
const content = message.content;
|
|
1151
|
+
if (!Array.isArray(content) || !content.some(isFailoverPart)) {
|
|
1152
|
+
out.push(message);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
changed = true;
|
|
1156
|
+
const kept = content.filter((part) => !isFailoverPart(part));
|
|
1157
|
+
if (kept.length === 0) continue;
|
|
1158
|
+
out.push({ ...message, content: kept });
|
|
375
1159
|
}
|
|
376
|
-
return
|
|
1160
|
+
return changed ? out : prompt;
|
|
1161
|
+
}
|
|
1162
|
+
function failoverContinuationText(target) {
|
|
1163
|
+
return [
|
|
1164
|
+
`The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account and you are now in a fresh Claude session.`,
|
|
1165
|
+
"The conversation so far is above. Continue the task from where it stopped: do not start over, do not re-plan, and do not repeat work that is already done.",
|
|
1166
|
+
"Do not mention the account switch unless you are asked about it."
|
|
1167
|
+
].join(" ");
|
|
1168
|
+
}
|
|
1169
|
+
function buildFailoverContinuationPrompt(prompt, target) {
|
|
1170
|
+
const stripped = stripAccountFailoverParts(prompt);
|
|
1171
|
+
return [
|
|
1172
|
+
...stripped,
|
|
1173
|
+
{
|
|
1174
|
+
role: "user",
|
|
1175
|
+
content: [{ type: "text", text: failoverContinuationText(target) }]
|
|
1176
|
+
}
|
|
1177
|
+
];
|
|
1178
|
+
}
|
|
1179
|
+
function failoverUntil(resetsAt) {
|
|
1180
|
+
return resetsAtToMs(resetsAt);
|
|
377
1181
|
}
|
|
378
1182
|
|
|
379
1183
|
// src/cli-version.ts
|
|
@@ -381,6 +1185,17 @@ import { execFile } from "child_process";
|
|
|
381
1185
|
import { promisify } from "util";
|
|
382
1186
|
var execFileAsync = promisify(execFile);
|
|
383
1187
|
var cache = /* @__PURE__ */ new Map();
|
|
1188
|
+
var CLI_HYGIENE_ENV_VARS = [
|
|
1189
|
+
"DISABLE_AUTOUPDATER",
|
|
1190
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
|
|
1191
|
+
];
|
|
1192
|
+
function cliHygieneEnv(inherited = process.env) {
|
|
1193
|
+
const filled = {};
|
|
1194
|
+
for (const name of CLI_HYGIENE_ENV_VARS) {
|
|
1195
|
+
if (inherited[name] === void 0) filled[name] = "1";
|
|
1196
|
+
}
|
|
1197
|
+
return filled;
|
|
1198
|
+
}
|
|
384
1199
|
function detectCliVersion(cliPath) {
|
|
385
1200
|
const cached = cache.get(cliPath);
|
|
386
1201
|
if (cached) return cached;
|
|
@@ -478,15 +1293,15 @@ import { unlink } from "fs/promises";
|
|
|
478
1293
|
// src/proxy-mcp.ts
|
|
479
1294
|
import { createServer } from "http";
|
|
480
1295
|
import * as fs2 from "fs";
|
|
481
|
-
import * as
|
|
1296
|
+
import * as path3 from "path";
|
|
482
1297
|
import * as crypto from "crypto";
|
|
483
1298
|
import { EventEmitter } from "events";
|
|
484
1299
|
|
|
485
1300
|
// src/tmp.ts
|
|
486
1301
|
import * as fs from "fs";
|
|
487
1302
|
import * as os from "os";
|
|
488
|
-
import * as
|
|
489
|
-
var PLUGIN_TMP_DIR =
|
|
1303
|
+
import * as path2 from "path";
|
|
1304
|
+
var PLUGIN_TMP_DIR = path2.join(
|
|
490
1305
|
os.tmpdir(),
|
|
491
1306
|
`opencode-claude-code-${process.pid}`
|
|
492
1307
|
);
|
|
@@ -1173,7 +1988,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1173
1988
|
2
|
|
1174
1989
|
);
|
|
1175
1990
|
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
1176
|
-
const outPath =
|
|
1991
|
+
const outPath = path3.join(
|
|
1177
1992
|
pluginTmpDir(),
|
|
1178
1993
|
`proxy-${hash}.json`
|
|
1179
1994
|
);
|
|
@@ -1598,187 +2413,28 @@ function resolvePendingProxyCallById(toolCallId, result) {
|
|
|
1598
2413
|
}
|
|
1599
2414
|
function rejectPendingProxyCallById(toolCallId, error) {
|
|
1600
2415
|
const pending = pendingByCallId.get(toolCallId);
|
|
1601
|
-
if (!pending) return false;
|
|
1602
|
-
pendingByCallId.delete(toolCallId);
|
|
1603
|
-
indexRemove(pending.sessionKey, toolCallId);
|
|
1604
|
-
clearPendingTimers(pending);
|
|
1605
|
-
pending.reject(error);
|
|
1606
|
-
log.notice("rejected pending proxy call", {
|
|
1607
|
-
sessionKey: pending.sessionKey,
|
|
1608
|
-
toolCallId: pending.toolCallId,
|
|
1609
|
-
toolName: pending.toolName,
|
|
1610
|
-
error: error.message
|
|
1611
|
-
});
|
|
1612
|
-
return true;
|
|
1613
|
-
}
|
|
1614
|
-
function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
1615
|
-
const s = callIdsBySession.get(sessionKey2);
|
|
1616
|
-
if (!s) return 0;
|
|
1617
|
-
const ids = [...s];
|
|
1618
|
-
let count = 0;
|
|
1619
|
-
for (const id of ids) {
|
|
1620
|
-
if (rejectPendingProxyCallById(id, error)) count++;
|
|
1621
|
-
}
|
|
1622
|
-
return count;
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
// src/plan-mode-question.ts
|
|
1626
|
-
var QUESTION_TOOL_NAME = "question";
|
|
1627
|
-
var APPROVED_EXIT_PLAN_MODE_MESSAGE = "User has approved your plan. You can now start coding. Start with updating your todo list if applicable.";
|
|
1628
|
-
var REJECTED_EXIT_PLAN_MODE_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected. To tell you how to proceed, the user said:";
|
|
1629
|
-
var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
1630
|
-
var OPENCODE_QUESTION_RESULT_PREFIX = `User has answered your questions: "${PLAN_MODE_APPROVAL_QUESTION}"="`;
|
|
1631
|
-
var OPENCODE_QUESTION_RESULT_SUFFIX = `". You can now continue with the user's answers in mind.`;
|
|
1632
|
-
var KEY_SEPARATOR = "\0";
|
|
1633
|
-
function isPlanModeQuestionActive(input) {
|
|
1634
|
-
if (input.compactionMode) return false;
|
|
1635
|
-
if (input.configured !== true) return false;
|
|
1636
|
-
return input.opencodeHasQuestion;
|
|
1637
|
-
}
|
|
1638
|
-
var pendingQuestions = /* @__PURE__ */ new Map();
|
|
1639
|
-
function pendingKey(sessionKey2, questionToolCallId) {
|
|
1640
|
-
return `${sessionKey2}${KEY_SEPARATOR}${questionToolCallId}`;
|
|
1641
|
-
}
|
|
1642
|
-
function clearExitPlanModeQuestions(sessionKey2) {
|
|
1643
|
-
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
1644
|
-
for (const key of pendingQuestions.keys()) {
|
|
1645
|
-
if (key.startsWith(prefix)) pendingQuestions.delete(key);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
function hasExitPlanModeQuestions(sessionKey2) {
|
|
1649
|
-
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
1650
|
-
return [...pendingQuestions.keys()].some((key) => key.startsWith(prefix));
|
|
1651
|
-
}
|
|
1652
|
-
function createExitPlanModeQuestionCall(sessionKey2, exitPlanModeToolUseId, plan, questionToolCallId = `exit_plan_question_${exitPlanModeToolUseId}`) {
|
|
1653
|
-
pendingQuestions.set(pendingKey(sessionKey2, questionToolCallId), exitPlanModeToolUseId);
|
|
1654
|
-
return {
|
|
1655
|
-
toolCallId: questionToolCallId,
|
|
1656
|
-
toolName: QUESTION_TOOL_NAME,
|
|
1657
|
-
input: {
|
|
1658
|
-
questions: [
|
|
1659
|
-
{
|
|
1660
|
-
header: "Plan approval",
|
|
1661
|
-
question: PLAN_MODE_APPROVAL_QUESTION,
|
|
1662
|
-
options: [
|
|
1663
|
-
{ label: "yes", description: "" },
|
|
1664
|
-
{ label: "no", description: "" }
|
|
1665
|
-
],
|
|
1666
|
-
multiple: false,
|
|
1667
|
-
custom: true
|
|
1668
|
-
}
|
|
1669
|
-
]
|
|
1670
|
-
},
|
|
1671
|
-
text: plan ? `
|
|
1672
|
-
|
|
1673
|
-
${plan}
|
|
1674
|
-
` : "\n\n"
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1677
|
-
function buildToolResultMessage(input) {
|
|
1678
|
-
return JSON.stringify({
|
|
1679
|
-
type: "user",
|
|
1680
|
-
message: {
|
|
1681
|
-
role: "user",
|
|
1682
|
-
content: [
|
|
1683
|
-
input.approved ? {
|
|
1684
|
-
type: "tool_result",
|
|
1685
|
-
tool_use_id: input.toolUseId,
|
|
1686
|
-
content: APPROVED_EXIT_PLAN_MODE_MESSAGE
|
|
1687
|
-
} : {
|
|
1688
|
-
type: "tool_result",
|
|
1689
|
-
tool_use_id: input.toolUseId,
|
|
1690
|
-
content: `${REJECTED_EXIT_PLAN_MODE_PREFIX}
|
|
1691
|
-
${input.feedback || "no"}`,
|
|
1692
|
-
is_error: true
|
|
1693
|
-
}
|
|
1694
|
-
]
|
|
1695
|
-
}
|
|
1696
|
-
});
|
|
1697
|
-
}
|
|
1698
|
-
function tryParseJson(text) {
|
|
1699
|
-
try {
|
|
1700
|
-
return JSON.parse(text);
|
|
1701
|
-
} catch {
|
|
1702
|
-
return text;
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
function unwrapToolOutput(part) {
|
|
1706
|
-
const output = part?.output ?? part?.result;
|
|
1707
|
-
if (typeof output === "string") return tryParseJson(output);
|
|
1708
|
-
if (!output || typeof output !== "object") return output;
|
|
1709
|
-
switch (output.type) {
|
|
1710
|
-
case "json":
|
|
1711
|
-
case "error-json":
|
|
1712
|
-
return output.value;
|
|
1713
|
-
case "text":
|
|
1714
|
-
case "error-text":
|
|
1715
|
-
return tryParseJson(String(output.value ?? ""));
|
|
1716
|
-
case "execution-denied":
|
|
1717
|
-
return {
|
|
1718
|
-
denied: true,
|
|
1719
|
-
reason: String(output.reason ?? "question rejected")
|
|
1720
|
-
};
|
|
1721
|
-
case "content":
|
|
1722
|
-
return Array.isArray(output.value) ? output.value.map((item) => {
|
|
1723
|
-
if (item?.type === "text") return item.text;
|
|
1724
|
-
return JSON.stringify(item);
|
|
1725
|
-
}).join("\n") : output.value;
|
|
1726
|
-
default:
|
|
1727
|
-
return output;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
function unwrapOpencodeQuestionResult(value) {
|
|
1731
|
-
if (value.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) && value.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
1732
|
-
return value.slice(
|
|
1733
|
-
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
1734
|
-
-OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
return value;
|
|
1738
|
-
}
|
|
1739
|
-
function collectAnswerStrings(value) {
|
|
1740
|
-
if (typeof value === "string") return [unwrapOpencodeQuestionResult(value)];
|
|
1741
|
-
if (Array.isArray(value)) return value.flatMap(collectAnswerStrings);
|
|
1742
|
-
if (!value || typeof value !== "object") return [];
|
|
1743
|
-
const obj = value;
|
|
1744
|
-
if (obj.denied === true) return [String(obj.reason ?? "question rejected")];
|
|
1745
|
-
for (const key of ["answers", "answer", "selected", "selection", "value"]) {
|
|
1746
|
-
if (key in obj) return collectAnswerStrings(obj[key]);
|
|
1747
|
-
}
|
|
1748
|
-
return [];
|
|
1749
|
-
}
|
|
1750
|
-
function classifyQuestionResult(part) {
|
|
1751
|
-
const output = unwrapToolOutput(part);
|
|
1752
|
-
const answers = collectAnswerStrings(output).map((answer) => answer.trim()).filter(Boolean);
|
|
1753
|
-
if (answers.length === 1 && answers[0].toLowerCase() === "yes") {
|
|
1754
|
-
return { approved: true, feedback: "" };
|
|
1755
|
-
}
|
|
1756
|
-
return {
|
|
1757
|
-
approved: false,
|
|
1758
|
-
feedback: answers.length > 0 ? answers.join("\n") : "no"
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
function consumeExitPlanModeQuestionResult(sessionKey2, prompt) {
|
|
1762
|
-
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1763
|
-
const msg = prompt[i];
|
|
1764
|
-
if (!Array.isArray(msg.content)) continue;
|
|
1765
|
-
for (const part of msg.content) {
|
|
1766
|
-
if (part?.type !== "tool-result" || typeof part.toolCallId !== "string") {
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
const key = pendingKey(sessionKey2, part.toolCallId);
|
|
1770
|
-
const exitPlanModeToolUseId = pendingQuestions.get(key);
|
|
1771
|
-
if (!exitPlanModeToolUseId) continue;
|
|
1772
|
-
pendingQuestions.delete(key);
|
|
1773
|
-
const result = classifyQuestionResult(part);
|
|
1774
|
-
return buildToolResultMessage({
|
|
1775
|
-
toolUseId: exitPlanModeToolUseId,
|
|
1776
|
-
approved: result.approved,
|
|
1777
|
-
feedback: result.feedback
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
2416
|
+
if (!pending) return false;
|
|
2417
|
+
pendingByCallId.delete(toolCallId);
|
|
2418
|
+
indexRemove(pending.sessionKey, toolCallId);
|
|
2419
|
+
clearPendingTimers(pending);
|
|
2420
|
+
pending.reject(error);
|
|
2421
|
+
log.notice("rejected pending proxy call", {
|
|
2422
|
+
sessionKey: pending.sessionKey,
|
|
2423
|
+
toolCallId: pending.toolCallId,
|
|
2424
|
+
toolName: pending.toolName,
|
|
2425
|
+
error: error.message
|
|
2426
|
+
});
|
|
2427
|
+
return true;
|
|
2428
|
+
}
|
|
2429
|
+
function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
2430
|
+
const s = callIdsBySession.get(sessionKey2);
|
|
2431
|
+
if (!s) return 0;
|
|
2432
|
+
const ids = [...s];
|
|
2433
|
+
let count = 0;
|
|
2434
|
+
for (const id of ids) {
|
|
2435
|
+
if (rejectPendingProxyCallById(id, error)) count++;
|
|
1780
2436
|
}
|
|
1781
|
-
return
|
|
2437
|
+
return count;
|
|
1782
2438
|
}
|
|
1783
2439
|
|
|
1784
2440
|
// src/compression-store.ts
|
|
@@ -1811,7 +2467,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
1811
2467
|
var MAX_HISTORY_EXCHANGES = 20;
|
|
1812
2468
|
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
1813
2469
|
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
1814
|
-
function
|
|
2470
|
+
function isRecord2(value) {
|
|
1815
2471
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1816
2472
|
}
|
|
1817
2473
|
var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -1822,7 +2478,7 @@ function parseSideQuestionContent(content) {
|
|
|
1822
2478
|
} else if (Array.isArray(content)) {
|
|
1823
2479
|
const parts = [];
|
|
1824
2480
|
for (const part of content) {
|
|
1825
|
-
if (!
|
|
2481
|
+
if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
1826
2482
|
parts.push(part.text);
|
|
1827
2483
|
}
|
|
1828
2484
|
text = parts.join("\n");
|
|
@@ -1841,7 +2497,7 @@ function assistantText(content) {
|
|
|
1841
2497
|
if (!Array.isArray(content)) return "";
|
|
1842
2498
|
const parts = [];
|
|
1843
2499
|
for (const part of content) {
|
|
1844
|
-
if (
|
|
2500
|
+
if (isRecord2(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
1845
2501
|
}
|
|
1846
2502
|
return parts.join("\n").trim();
|
|
1847
2503
|
}
|
|
@@ -1871,9 +2527,9 @@ function dispatchSideQuestionResponse(activeProcess, line) {
|
|
|
1871
2527
|
} catch {
|
|
1872
2528
|
return false;
|
|
1873
2529
|
}
|
|
1874
|
-
if (!
|
|
2530
|
+
if (!isRecord2(message) || message.type !== "control_response") return false;
|
|
1875
2531
|
const response = message.response;
|
|
1876
|
-
if (!
|
|
2532
|
+
if (!isRecord2(response) || typeof response.request_id !== "string") return false;
|
|
1877
2533
|
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
1878
2534
|
}
|
|
1879
2535
|
async function requestSideQuestion(activeProcess, question, options) {
|
|
@@ -1958,7 +2614,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
1958
2614
|
return;
|
|
1959
2615
|
}
|
|
1960
2616
|
const result = response.response;
|
|
1961
|
-
if (response.subtype !== "success" || !
|
|
2617
|
+
if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
1962
2618
|
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
1963
2619
|
return;
|
|
1964
2620
|
}
|
|
@@ -2056,7 +2712,11 @@ function cliEffortLevel(effort) {
|
|
|
2056
2712
|
function claudeSpawnEnv(opts) {
|
|
2057
2713
|
const env = {
|
|
2058
2714
|
...process.env,
|
|
2059
|
-
TERM: "xterm-256color"
|
|
2715
|
+
TERM: "xterm-256color",
|
|
2716
|
+
// Pin the child to the binary whose version we detected, and keep it off
|
|
2717
|
+
// non-essential network calls. Fills gaps only, so an explicit shell value
|
|
2718
|
+
// survives: see `cliHygieneEnv` for why the version has to hold still.
|
|
2719
|
+
...cliHygieneEnv()
|
|
2060
2720
|
};
|
|
2061
2721
|
if (opts?.effort) {
|
|
2062
2722
|
env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
|
|
@@ -2289,11 +2949,31 @@ async function deleteActiveProcessAndWait(key, options = {}) {
|
|
|
2289
2949
|
function getClaudeSessionId(key) {
|
|
2290
2950
|
return claudeSessions.get(key);
|
|
2291
2951
|
}
|
|
2952
|
+
var MAX_CLAUDE_SESSION_ENTRIES = 64;
|
|
2953
|
+
function claudeSessionIsBusy(key) {
|
|
2954
|
+
if (activeProcesses.has(key)) return true;
|
|
2955
|
+
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
2956
|
+
}
|
|
2957
|
+
function capClaudeSessions() {
|
|
2958
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
2959
|
+
if (claudeSessions.size <= MAX_CLAUDE_SESSION_ENTRIES) return;
|
|
2960
|
+
if (claudeSessionIsBusy(key)) continue;
|
|
2961
|
+
log.info("claude session cap reached; releasing an idle session", {
|
|
2962
|
+
sessionKey: key,
|
|
2963
|
+
size: claudeSessions.size,
|
|
2964
|
+
cap: MAX_CLAUDE_SESSION_ENTRIES
|
|
2965
|
+
});
|
|
2966
|
+
deleteClaudeSessionId(key);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2292
2969
|
function setClaudeSessionId(key, sessionId) {
|
|
2970
|
+
claudeSessions.delete(key);
|
|
2293
2971
|
claudeSessions.set(key, sessionId);
|
|
2972
|
+
capClaudeSessions();
|
|
2294
2973
|
}
|
|
2295
2974
|
function deleteClaudeSessionId(key) {
|
|
2296
2975
|
clearExitPlanModeQuestions(key);
|
|
2976
|
+
clearAccountFailoverQuestions(key);
|
|
2297
2977
|
const claudeSessionId = claudeSessions.get(key);
|
|
2298
2978
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
2299
2979
|
claudeSessions.delete(key);
|
|
@@ -2350,6 +3030,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2350
3030
|
systemPromptFile,
|
|
2351
3031
|
effort,
|
|
2352
3032
|
startedAt: Date.now(),
|
|
3033
|
+
cliPath,
|
|
2353
3034
|
cliArgs: [...cliArgs],
|
|
2354
3035
|
unattendedLines: [],
|
|
2355
3036
|
unattendedDropped: 0
|
|
@@ -2832,267 +3513,34 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
2832
3513
|
(idle) => idle ? "idle" : "timeout"
|
|
2833
3514
|
)
|
|
2834
3515
|
]);
|
|
2835
|
-
if (outcome === "inline") {
|
|
2836
|
-
log.info("btw: answered inside the running turn, dropping the /btw message", {
|
|
2837
|
-
sessionID: input.sessionID,
|
|
2838
|
-
waitedMs: Date.now() - started
|
|
2839
|
-
});
|
|
2840
|
-
throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
2841
|
-
}
|
|
2842
|
-
log.info("btw: turn over, releasing the /btw message", {
|
|
2843
|
-
sessionID: input.sessionID,
|
|
2844
|
-
idle: outcome === "idle",
|
|
2845
|
-
waitedMs: Date.now() - started
|
|
2846
|
-
});
|
|
2847
|
-
if (outcome === "timeout") {
|
|
2848
|
-
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
2849
|
-
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
// src/cli-events.ts
|
|
2854
|
-
function isRecord2(value) {
|
|
2855
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2856
|
-
}
|
|
2857
|
-
function str(value) {
|
|
2858
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2859
|
-
}
|
|
2860
|
-
function num(value) {
|
|
2861
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2862
|
-
}
|
|
2863
|
-
var RATE_LIMIT_MARKER = "\u258C **rate limit:**";
|
|
2864
|
-
var RATE_LIMIT_ACTION = "Enable extra usage or add credits on the account, wait for the window to reset, switch account, org or plan, or authenticate with an API key.";
|
|
2865
|
-
var OVERAGE_DISABLED_REASONS = {
|
|
2866
|
-
overage_not_provisioned: "extra usage is not set up on this account",
|
|
2867
|
-
org_level_disabled: "extra usage is disabled for your organization",
|
|
2868
|
-
org_level_disabled_until: "extra usage is disabled for your organization for now",
|
|
2869
|
-
out_of_credits: "the account's usage credits are spent",
|
|
2870
|
-
seat_tier_level_disabled: "your seat tier does not allow extra usage",
|
|
2871
|
-
member_level_disabled: "extra usage is disabled for your member account",
|
|
2872
|
-
seat_tier_zero_credit_limit: "your seat tier has a zero credit limit",
|
|
2873
|
-
group_zero_credit_limit: "your group has a zero credit limit",
|
|
2874
|
-
member_zero_credit_limit: "your member account has a zero credit limit",
|
|
2875
|
-
org_service_level_disabled: "your organization's service level does not include extra usage",
|
|
2876
|
-
no_limits_configured: "no usage limits are configured for this account",
|
|
2877
|
-
fetch_error: "the CLI could not read the account's usage limits"
|
|
2878
|
-
};
|
|
2879
|
-
var RATE_LIMIT_WINDOWS = {
|
|
2880
|
-
five_hour: "the 5-hour window",
|
|
2881
|
-
seven_day: "the 7-day window",
|
|
2882
|
-
seven_day_opus: "the 7-day Opus window",
|
|
2883
|
-
seven_day_sonnet: "the 7-day Sonnet window",
|
|
2884
|
-
seven_day_overage_included: "the 7-day extra-usage window",
|
|
2885
|
-
overage: "extra usage"
|
|
2886
|
-
};
|
|
2887
|
-
function parseRateLimitEvent(msg) {
|
|
2888
|
-
if (msg.type !== "rate_limit_event") return null;
|
|
2889
|
-
const info = msg.rate_limit_info;
|
|
2890
|
-
if (!isRecord2(info)) return null;
|
|
2891
|
-
return {
|
|
2892
|
-
status: str(info.status),
|
|
2893
|
-
rateLimitType: str(info.rateLimitType),
|
|
2894
|
-
resetsAt: num(info.resetsAt),
|
|
2895
|
-
utilization: num(info.utilization),
|
|
2896
|
-
isUsingOverage: info.isUsingOverage === true,
|
|
2897
|
-
overageStatus: str(info.overageStatus),
|
|
2898
|
-
overageResetsAt: num(info.overageResetsAt),
|
|
2899
|
-
overageDisabledReason: str(info.overageDisabledReason)
|
|
2900
|
-
};
|
|
2901
|
-
}
|
|
2902
|
-
function formatResetsAt(resetsAt) {
|
|
2903
|
-
if (resetsAt === void 0) return void 0;
|
|
2904
|
-
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
2905
|
-
const date = new Date(ms);
|
|
2906
|
-
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
2907
|
-
}
|
|
2908
|
-
function rateLimitKey(info) {
|
|
2909
|
-
return [
|
|
2910
|
-
info.status ?? "?",
|
|
2911
|
-
info.rateLimitType ?? "?",
|
|
2912
|
-
info.overageStatus ?? "?",
|
|
2913
|
-
info.overageDisabledReason ?? "?"
|
|
2914
|
-
].join("|");
|
|
2915
|
-
}
|
|
2916
|
-
function isRateLimitRejected(info) {
|
|
2917
|
-
return info.status === "rejected" || info.overageStatus === "rejected";
|
|
2918
|
-
}
|
|
2919
|
-
function describeRateLimit(info) {
|
|
2920
|
-
if (!info.status && !info.overageStatus) return null;
|
|
2921
|
-
const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
|
|
2922
|
-
const resets = formatResetsAt(info.resetsAt);
|
|
2923
|
-
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
2924
|
-
const key = rateLimitKey(info);
|
|
2925
|
-
if (isRateLimitRejected(info)) {
|
|
2926
|
-
const parts = [];
|
|
2927
|
-
parts.push(
|
|
2928
|
-
info.status === "rejected" ? `Claude Code rejected this request: you are out of usage${window ? ` in ${window}` : ""}.` : "Claude Code rejected this request: paid extra usage is not available on this account."
|
|
2929
|
-
);
|
|
2930
|
-
const reason = info.overageDisabledReason ? OVERAGE_DISABLED_REASONS[info.overageDisabledReason] ?? info.overageDisabledReason : void 0;
|
|
2931
|
-
if (reason) parts.push(`Extra usage is unavailable because ${reason}.`);
|
|
2932
|
-
const resetAt = resets ?? overageResets;
|
|
2933
|
-
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
2934
|
-
parts.push(RATE_LIMIT_ACTION);
|
|
2935
|
-
const message = parts.join(" ");
|
|
2936
|
-
return { key, level: "warn", message, transcript: `
|
|
2937
|
-
${RATE_LIMIT_MARKER} ${message}
|
|
2938
|
-
` };
|
|
2939
|
-
}
|
|
2940
|
-
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
2941
|
-
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
2942
|
-
return {
|
|
2943
|
-
key,
|
|
2944
|
-
level: "notice",
|
|
2945
|
-
message: `Approaching the usage limit${window ? ` for ${window}` : ""}${used}${resets ? `, resets at ${resets}` : ""}.`,
|
|
2946
|
-
transcript: null
|
|
2947
|
-
};
|
|
2948
|
-
}
|
|
2949
|
-
return {
|
|
2950
|
-
key,
|
|
2951
|
-
level: "info",
|
|
2952
|
-
message: `usage limits updated${window ? ` for ${window}` : ""}`,
|
|
2953
|
-
transcript: null
|
|
2954
|
-
};
|
|
2955
|
-
}
|
|
2956
|
-
var reportedRateLimits = /* @__PURE__ */ new Set();
|
|
2957
|
-
function reportRateLimitEvent(msg) {
|
|
2958
|
-
const info = parseRateLimitEvent(msg);
|
|
2959
|
-
if (!info) return null;
|
|
2960
|
-
const report = describeRateLimit(info);
|
|
2961
|
-
if (!report) return null;
|
|
2962
|
-
const data = { ...info };
|
|
2963
|
-
if (reportedRateLimits.has(report.key)) {
|
|
2964
|
-
log.debug(report.message, data);
|
|
2965
|
-
return null;
|
|
2966
|
-
}
|
|
2967
|
-
reportedRateLimits.add(report.key);
|
|
2968
|
-
log[report.level](report.message, data);
|
|
2969
|
-
return report.transcript;
|
|
2970
|
-
}
|
|
2971
|
-
var API_KEY_SOURCES = /* @__PURE__ */ new Set([
|
|
2972
|
-
"ANTHROPIC_API_KEY",
|
|
2973
|
-
"apiKeyHelper",
|
|
2974
|
-
"/login managed key",
|
|
2975
|
-
"user",
|
|
2976
|
-
"project",
|
|
2977
|
-
"org",
|
|
2978
|
-
"temporary"
|
|
2979
|
-
]);
|
|
2980
|
-
function parseSystemInit(msg) {
|
|
2981
|
-
if (msg.type !== "system" || msg.subtype !== "init") return null;
|
|
2982
|
-
const raw = msg;
|
|
2983
|
-
const servers = [];
|
|
2984
|
-
if (Array.isArray(raw.mcp_servers)) {
|
|
2985
|
-
for (const entry of raw.mcp_servers) {
|
|
2986
|
-
if (!isRecord2(entry)) continue;
|
|
2987
|
-
servers.push({ name: str(entry.name) ?? "unknown", status: str(entry.status) ?? "unknown" });
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
return {
|
|
2991
|
-
apiKeySource: str(raw.apiKeySource),
|
|
2992
|
-
permissionMode: str(raw.permissionMode),
|
|
2993
|
-
model: str(raw.model),
|
|
2994
|
-
cliVersion: str(raw.claude_code_version),
|
|
2995
|
-
toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
|
|
2996
|
-
mcpServers: servers
|
|
2997
|
-
};
|
|
2998
|
-
}
|
|
2999
|
-
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
3000
|
-
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
3001
|
-
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription's Agent SDK credit.`;
|
|
3002
|
-
return ignoreAnthropicApiKey ? `${base} \`ignoreAnthropicApiKey\` is already on, so the key is not coming from the environment: check the CLI's own settings (\`claude config\`) or an \`apiKeyHelper\`.` : `${base} Set the provider option \`ignoreAnthropicApiKey: true\` to strip the key from spawns and fall back to the stored subscription auth.`;
|
|
3003
|
-
}
|
|
3004
|
-
var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
3005
|
-
var warnedMcpFailures = /* @__PURE__ */ new Set();
|
|
3006
|
-
function reportSystemInit(msg, options = {}) {
|
|
3007
|
-
const info = parseSystemInit(msg);
|
|
3008
|
-
if (!info) return;
|
|
3009
|
-
log.info("claude session init", {
|
|
3010
|
-
apiKeySource: info.apiKeySource ?? null,
|
|
3011
|
-
permissionMode: info.permissionMode ?? null,
|
|
3012
|
-
model: info.model ?? null,
|
|
3013
|
-
cliVersion: info.cliVersion ?? null,
|
|
3014
|
-
tools: info.toolCount,
|
|
3015
|
-
mcpServers: info.mcpServers
|
|
3016
|
-
});
|
|
3017
|
-
for (const server2 of info.mcpServers) {
|
|
3018
|
-
if (server2.status === "connected") continue;
|
|
3019
|
-
const key2 = `${server2.name}:${server2.status}`;
|
|
3020
|
-
const message = `MCP server "${server2.name}" is ${server2.status} in Claude Code; its tools are not available to the model this session.`;
|
|
3021
|
-
if (warnedMcpFailures.has(key2)) {
|
|
3022
|
-
log.debug(message, { server: server2.name, status: server2.status });
|
|
3023
|
-
continue;
|
|
3024
|
-
}
|
|
3025
|
-
warnedMcpFailures.add(key2);
|
|
3026
|
-
log.warn(message, { server: server2.name, status: server2.status });
|
|
3027
|
-
}
|
|
3028
|
-
const apiKeyMessage = apiKeySourceWarning(info.apiKeySource, options.ignoreAnthropicApiKey);
|
|
3029
|
-
if (!apiKeyMessage) return;
|
|
3030
|
-
const key = `${info.apiKeySource}:${options.ignoreAnthropicApiKey ? "ignored" : "passed"}`;
|
|
3031
|
-
if (warnedApiKeySources.has(key)) {
|
|
3032
|
-
log.debug(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
3033
|
-
return;
|
|
3034
|
-
}
|
|
3035
|
-
warnedApiKeySources.add(key);
|
|
3036
|
-
log.warn(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
3037
|
-
}
|
|
3038
|
-
var COMPACT_BOUNDARY_MARKER = "\u258C **context compacted:**";
|
|
3039
|
-
function parseCompactBoundary(msg) {
|
|
3040
|
-
if (msg.type !== "system" || msg.subtype !== "compact_boundary") return null;
|
|
3041
|
-
const raw = msg;
|
|
3042
|
-
const meta = isRecord2(raw.compact_metadata) ? raw.compact_metadata : isRecord2(raw.compactMetadata) ? raw.compactMetadata : void 0;
|
|
3043
|
-
return {
|
|
3044
|
-
trigger: str(meta?.trigger) ?? "unknown",
|
|
3045
|
-
preTokens: num(meta?.pre_tokens) ?? num(meta?.preTokens),
|
|
3046
|
-
postTokens: num(meta?.post_tokens) ?? num(meta?.postTokens)
|
|
3047
|
-
};
|
|
3048
|
-
}
|
|
3049
|
-
function formatCompactBoundaryNote(boundary) {
|
|
3050
|
-
const how = boundary.trigger === "auto" ? "on its own" : `on a ${boundary.trigger} request`;
|
|
3051
|
-
const sizes = boundary.preTokens !== void 0 && boundary.postTokens !== void 0 ? ` (${boundary.preTokens.toLocaleString("en-US")} tokens to ${boundary.postTokens.toLocaleString("en-US")})` : "";
|
|
3052
|
-
return `
|
|
3053
|
-
${COMPACT_BOUNDARY_MARKER} Claude Code compacted its own context ${how}${sizes}. Earlier detail in this conversation is now a summary.
|
|
3054
|
-
`;
|
|
3055
|
-
}
|
|
3056
|
-
function reportCompactBoundary(msg) {
|
|
3057
|
-
const boundary = parseCompactBoundary(msg);
|
|
3058
|
-
if (!boundary) return null;
|
|
3059
|
-
log.notice("claude code compacted its own context", {
|
|
3060
|
-
trigger: boundary.trigger,
|
|
3061
|
-
preTokens: boundary.preTokens ?? null,
|
|
3062
|
-
postTokens: boundary.postTokens ?? null
|
|
3516
|
+
if (outcome === "inline") {
|
|
3517
|
+
log.info("btw: answered inside the running turn, dropping the /btw message", {
|
|
3518
|
+
sessionID: input.sessionID,
|
|
3519
|
+
waitedMs: Date.now() - started
|
|
3520
|
+
});
|
|
3521
|
+
throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
3522
|
+
}
|
|
3523
|
+
log.info("btw: turn over, releasing the /btw message", {
|
|
3524
|
+
sessionID: input.sessionID,
|
|
3525
|
+
idle: outcome === "idle",
|
|
3526
|
+
waitedMs: Date.now() - started
|
|
3063
3527
|
});
|
|
3064
|
-
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
error_max_turns: "it hit its internal turn limit before finishing",
|
|
3069
|
-
error_during_execution: "it failed while running the turn",
|
|
3070
|
-
error_max_budget_usd: "it hit the configured spend limit for the turn",
|
|
3071
|
-
error_max_structured_output_retries: "it could not produce valid structured output"
|
|
3072
|
-
};
|
|
3073
|
-
function describeResultFailure(msg) {
|
|
3074
|
-
if (msg.type !== "result") return null;
|
|
3075
|
-
const subtype = msg.subtype;
|
|
3076
|
-
if (!subtype || subtype === "success") return null;
|
|
3077
|
-
const explanation = RESULT_SUBTYPES[subtype];
|
|
3078
|
-
return explanation ? `Claude Code ended the turn with \`${subtype}\`: ${explanation}.` : `Claude Code ended the turn with \`${subtype}\`.`;
|
|
3079
|
-
}
|
|
3080
|
-
function formatResultFailureNote(message) {
|
|
3081
|
-
return `
|
|
3082
|
-
${RESULT_ERROR_MARKER} ${message}
|
|
3083
|
-
`;
|
|
3528
|
+
if (outcome === "timeout") {
|
|
3529
|
+
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
3530
|
+
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
3531
|
+
}
|
|
3084
3532
|
}
|
|
3085
3533
|
|
|
3086
3534
|
// src/startup-diagnostics.ts
|
|
3087
3535
|
import { execFile as execFile2 } from "child_process";
|
|
3088
3536
|
import * as fs4 from "fs";
|
|
3089
|
-
import * as
|
|
3537
|
+
import * as path5 from "path";
|
|
3090
3538
|
import { promisify as promisify2 } from "util";
|
|
3091
3539
|
import { fileURLToPath } from "url";
|
|
3092
3540
|
|
|
3093
3541
|
// src/mcp-bridge.ts
|
|
3094
3542
|
import * as fs3 from "fs";
|
|
3095
|
-
import * as
|
|
3543
|
+
import * as path4 from "path";
|
|
3096
3544
|
import * as os2 from "os";
|
|
3097
3545
|
import * as crypto2 from "crypto";
|
|
3098
3546
|
import {
|
|
@@ -3153,14 +3601,14 @@ function deepMerge(target, source) {
|
|
|
3153
3601
|
}
|
|
3154
3602
|
function walkUp(opts) {
|
|
3155
3603
|
const out = [];
|
|
3156
|
-
let current =
|
|
3604
|
+
let current = path4.resolve(opts.start);
|
|
3157
3605
|
while (true) {
|
|
3158
3606
|
for (const target of opts.targets) {
|
|
3159
|
-
const candidate =
|
|
3607
|
+
const candidate = path4.join(current, target);
|
|
3160
3608
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3161
3609
|
}
|
|
3162
|
-
if (opts.stop && current ===
|
|
3163
|
-
const parent =
|
|
3610
|
+
if (opts.stop && current === path4.resolve(opts.stop)) break;
|
|
3611
|
+
const parent = path4.dirname(current);
|
|
3164
3612
|
if (parent === current) break;
|
|
3165
3613
|
current = parent;
|
|
3166
3614
|
}
|
|
@@ -3168,28 +3616,28 @@ function walkUp(opts) {
|
|
|
3168
3616
|
}
|
|
3169
3617
|
function detectWorktree(cwd) {
|
|
3170
3618
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3171
|
-
if (override) return
|
|
3172
|
-
let current =
|
|
3619
|
+
if (override) return path4.resolve(override);
|
|
3620
|
+
let current = path4.resolve(cwd);
|
|
3173
3621
|
while (true) {
|
|
3174
|
-
const gitPath =
|
|
3622
|
+
const gitPath = path4.join(current, ".git");
|
|
3175
3623
|
try {
|
|
3176
3624
|
if (fs3.existsSync(gitPath)) return current;
|
|
3177
3625
|
} catch {
|
|
3178
3626
|
}
|
|
3179
|
-
const parent =
|
|
3627
|
+
const parent = path4.dirname(current);
|
|
3180
3628
|
if (parent === current) return void 0;
|
|
3181
3629
|
current = parent;
|
|
3182
3630
|
}
|
|
3183
3631
|
}
|
|
3184
3632
|
function globalConfigDir() {
|
|
3185
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3186
|
-
return
|
|
3633
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
|
|
3634
|
+
return path4.join(xdg, "opencode");
|
|
3187
3635
|
}
|
|
3188
3636
|
function loadGlobalConfig() {
|
|
3189
3637
|
const dir = globalConfigDir();
|
|
3190
3638
|
let merged = {};
|
|
3191
3639
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3192
|
-
const file =
|
|
3640
|
+
const file = path4.join(dir, name);
|
|
3193
3641
|
if (!fileExists(file)) continue;
|
|
3194
3642
|
const parsed = readAndParse(file);
|
|
3195
3643
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -3199,7 +3647,7 @@ function loadGlobalConfig() {
|
|
|
3199
3647
|
function loadProjectFilesInDir(dir) {
|
|
3200
3648
|
let merged = {};
|
|
3201
3649
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3202
|
-
const file =
|
|
3650
|
+
const file = path4.join(dir, name);
|
|
3203
3651
|
if (!fileExists(file)) continue;
|
|
3204
3652
|
const parsed = readAndParse(file);
|
|
3205
3653
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -3210,7 +3658,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3210
3658
|
const dirs = [];
|
|
3211
3659
|
const seen = /* @__PURE__ */ new Set();
|
|
3212
3660
|
const push = (p) => {
|
|
3213
|
-
const abs =
|
|
3661
|
+
const abs = path4.resolve(p);
|
|
3214
3662
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
3215
3663
|
seen.add(abs);
|
|
3216
3664
|
dirs.push(abs);
|
|
@@ -3226,7 +3674,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3226
3674
|
}
|
|
3227
3675
|
const home = os2.homedir();
|
|
3228
3676
|
if (home) {
|
|
3229
|
-
const homeDot =
|
|
3677
|
+
const homeDot = path4.join(home, ".opencode");
|
|
3230
3678
|
if (dirExists(homeDot)) push(homeDot);
|
|
3231
3679
|
}
|
|
3232
3680
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -3351,7 +3799,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3351
3799
|
const projectDirs = [];
|
|
3352
3800
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3353
3801
|
for (const f of projectFiles) {
|
|
3354
|
-
const d =
|
|
3802
|
+
const d = path4.dirname(f);
|
|
3355
3803
|
if (!seenProjectDirs.has(d)) {
|
|
3356
3804
|
seenProjectDirs.add(d);
|
|
3357
3805
|
projectDirs.push(d);
|
|
@@ -3397,7 +3845,7 @@ function finishBridge(input) {
|
|
|
3397
3845
|
}
|
|
3398
3846
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3399
3847
|
const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3400
|
-
const outPath =
|
|
3848
|
+
const outPath = path4.join(
|
|
3401
3849
|
pluginTmpDir(),
|
|
3402
3850
|
`mcp-${bodyDigest}.json`
|
|
3403
3851
|
);
|
|
@@ -3479,6 +3927,24 @@ async function fetchSessionDirectory(sessionID) {
|
|
|
3479
3927
|
return void 0;
|
|
3480
3928
|
}
|
|
3481
3929
|
}
|
|
3930
|
+
async function fetchSessionParentId(sessionID) {
|
|
3931
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3932
|
+
const client = opencodeClient;
|
|
3933
|
+
if (!client?.session?.get) return void 0;
|
|
3934
|
+
try {
|
|
3935
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3936
|
+
const data = res.data;
|
|
3937
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3938
|
+
const parentID = data.parentID;
|
|
3939
|
+
return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
|
|
3940
|
+
} catch (err) {
|
|
3941
|
+
log.warn("failed to fetch opencode session parent", {
|
|
3942
|
+
sessionID,
|
|
3943
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3944
|
+
});
|
|
3945
|
+
return void 0;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3482
3948
|
async function getRuntimeMcpStatus() {
|
|
3483
3949
|
const client = opencodeClient;
|
|
3484
3950
|
if (!client?.mcp?.status) return void 0;
|
|
@@ -3536,8 +4002,8 @@ var cachedPluginVersion;
|
|
|
3536
4002
|
function pluginVersion() {
|
|
3537
4003
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
3538
4004
|
try {
|
|
3539
|
-
const here =
|
|
3540
|
-
const raw = fs4.readFileSync(
|
|
4005
|
+
const here = path5.dirname(fileURLToPath(import.meta.url));
|
|
4006
|
+
const raw = fs4.readFileSync(path5.join(here, "..", "package.json"), "utf8");
|
|
3541
4007
|
const version = JSON.parse(raw).version;
|
|
3542
4008
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
3543
4009
|
} catch {
|
|
@@ -3561,7 +4027,7 @@ var opencodeVersionProbe;
|
|
|
3561
4027
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
3562
4028
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
3563
4029
|
opencodeVersionProbe = (async () => {
|
|
3564
|
-
if (!
|
|
4030
|
+
if (!path5.basename(execPath).toLowerCase().includes("opencode")) {
|
|
3565
4031
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
3566
4032
|
return void 0;
|
|
3567
4033
|
}
|
|
@@ -3955,7 +4421,9 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
3955
4421
|
COMPACT_BOUNDARY_MARKER,
|
|
3956
4422
|
RATE_LIMIT_MARKER,
|
|
3957
4423
|
RESULT_ERROR_MARKER,
|
|
3958
|
-
DOCTOR_MARKER
|
|
4424
|
+
DOCTOR_MARKER,
|
|
4425
|
+
STREAM_TIMEOUT_MARKER,
|
|
4426
|
+
FAILOVER_MARKER
|
|
3959
4427
|
];
|
|
3960
4428
|
function isPluginNote(part) {
|
|
3961
4429
|
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
@@ -3968,6 +4436,7 @@ function stripPluginNotes(content) {
|
|
|
3968
4436
|
return kept.length === content.length ? content : kept;
|
|
3969
4437
|
}
|
|
3970
4438
|
function filterSideQuestionHistory(prompt) {
|
|
4439
|
+
prompt = stripAccountFailoverParts(prompt);
|
|
3971
4440
|
let pluginCommand = false;
|
|
3972
4441
|
const kept = prompt.filter((message) => {
|
|
3973
4442
|
if (message.role === "user") {
|
|
@@ -4340,7 +4809,7 @@ Now continuing with the current message:
|
|
|
4340
4809
|
|
|
4341
4810
|
// src/agent-models.ts
|
|
4342
4811
|
import { readFile, readdir } from "fs/promises";
|
|
4343
|
-
import
|
|
4812
|
+
import path6 from "path";
|
|
4344
4813
|
|
|
4345
4814
|
// src/models.ts
|
|
4346
4815
|
var PROVIDER_ID = "claude-code";
|
|
@@ -4718,11 +5187,11 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
4718
5187
|
const name = entry.slice(0, -3);
|
|
4719
5188
|
if (records[name]) continue;
|
|
4720
5189
|
try {
|
|
4721
|
-
const text = await readFile(
|
|
5190
|
+
const text = await readFile(path6.join(directory, entry), "utf8");
|
|
4722
5191
|
records[name] = parseAgentFrontmatter(text);
|
|
4723
5192
|
} catch (err) {
|
|
4724
5193
|
log.debug("failed to read agent markdown", {
|
|
4725
|
-
file:
|
|
5194
|
+
file: path6.join(directory, entry),
|
|
4726
5195
|
error: String(err)
|
|
4727
5196
|
});
|
|
4728
5197
|
}
|
|
@@ -4734,12 +5203,12 @@ function agentDirectories(home, projectDirectory) {
|
|
|
4734
5203
|
const directories = [];
|
|
4735
5204
|
if (projectDirectory) {
|
|
4736
5205
|
for (const name of AGENT_DIR_NAMES) {
|
|
4737
|
-
directories.push(
|
|
5206
|
+
directories.push(path6.join(projectDirectory, ".opencode", name));
|
|
4738
5207
|
}
|
|
4739
5208
|
}
|
|
4740
5209
|
if (home) {
|
|
4741
5210
|
for (const name of AGENT_DIR_NAMES) {
|
|
4742
|
-
directories.push(
|
|
5211
|
+
directories.push(path6.join(home, ".config", "opencode", name));
|
|
4743
5212
|
}
|
|
4744
5213
|
}
|
|
4745
5214
|
return directories;
|
|
@@ -4749,13 +5218,13 @@ function agentDirectories(home, projectDirectory) {
|
|
|
4749
5218
|
import * as crypto3 from "crypto";
|
|
4750
5219
|
import * as fs5 from "fs";
|
|
4751
5220
|
import * as os3 from "os";
|
|
4752
|
-
import * as
|
|
5221
|
+
import * as path7 from "path";
|
|
4753
5222
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4754
5223
|
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
4755
5224
|
function bundledSkillsDir() {
|
|
4756
5225
|
try {
|
|
4757
5226
|
const here = fileURLToPath2(import.meta.url);
|
|
4758
|
-
const dir =
|
|
5227
|
+
const dir = path7.resolve(path7.dirname(here), "..", "skills");
|
|
4759
5228
|
return dirExists2(dir) ? dir : null;
|
|
4760
5229
|
} catch {
|
|
4761
5230
|
return null;
|
|
@@ -4779,24 +5248,24 @@ function skillRoots(cwd) {
|
|
|
4779
5248
|
const roots = [];
|
|
4780
5249
|
const seen = /* @__PURE__ */ new Set();
|
|
4781
5250
|
const push = (p) => {
|
|
4782
|
-
const abs =
|
|
5251
|
+
const abs = path7.resolve(p);
|
|
4783
5252
|
if (seen.has(abs)) return;
|
|
4784
5253
|
seen.add(abs);
|
|
4785
5254
|
if (dirExists2(abs)) roots.push(abs);
|
|
4786
5255
|
};
|
|
4787
|
-
let current =
|
|
5256
|
+
let current = path7.resolve(cwd);
|
|
4788
5257
|
while (true) {
|
|
4789
|
-
push(
|
|
4790
|
-
const parent =
|
|
5258
|
+
push(path7.join(current, ".opencode", "skills"));
|
|
5259
|
+
const parent = path7.dirname(current);
|
|
4791
5260
|
if (parent === current) break;
|
|
4792
5261
|
current = parent;
|
|
4793
5262
|
}
|
|
4794
5263
|
const home = os3.homedir();
|
|
4795
|
-
if (home) push(
|
|
5264
|
+
if (home) push(path7.join(home, ".opencode", "skills"));
|
|
4796
5265
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
4797
|
-
if (envDir) push(
|
|
4798
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (home ?
|
|
4799
|
-
if (xdg) push(
|
|
5266
|
+
if (envDir) push(path7.join(envDir, "skills"));
|
|
5267
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path7.join(home, ".config") : null);
|
|
5268
|
+
if (xdg) push(path7.join(xdg, "opencode", "skills"));
|
|
4800
5269
|
return roots;
|
|
4801
5270
|
}
|
|
4802
5271
|
function collectSkills(root, claimed, found) {
|
|
@@ -4811,8 +5280,8 @@ function collectSkills(root, claimed, found) {
|
|
|
4811
5280
|
const name = entry.name;
|
|
4812
5281
|
if (name.startsWith(".")) continue;
|
|
4813
5282
|
if (claimed.has(name)) continue;
|
|
4814
|
-
const dir =
|
|
4815
|
-
if (!fileExists2(
|
|
5283
|
+
const dir = path7.join(root, name);
|
|
5284
|
+
if (!fileExists2(path7.join(dir, "SKILL.md"))) continue;
|
|
4816
5285
|
claimed.add(name);
|
|
4817
5286
|
found.push({ name, dir });
|
|
4818
5287
|
}
|
|
@@ -4836,7 +5305,7 @@ function registerBundledSkillPath(config) {
|
|
|
4836
5305
|
if (!dir) return false;
|
|
4837
5306
|
config.skills ??= {};
|
|
4838
5307
|
const paths = config.skills.paths ??= [];
|
|
4839
|
-
if (paths.some((p) =>
|
|
5308
|
+
if (paths.some((p) => path7.resolve(p) === dir)) return false;
|
|
4840
5309
|
paths.push(dir);
|
|
4841
5310
|
return true;
|
|
4842
5311
|
}
|
|
@@ -4852,13 +5321,13 @@ function buildSkillPluginDir(skills) {
|
|
|
4852
5321
|
if (skills.length === 0) return null;
|
|
4853
5322
|
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
4854
5323
|
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
4855
|
-
const root =
|
|
4856
|
-
const manifest =
|
|
5324
|
+
const root = path7.join(pluginTmpDir(), `skills-${hash}`);
|
|
5325
|
+
const manifest = path7.join(root, ".claude-plugin", "plugin.json");
|
|
4857
5326
|
if (fileExists2(manifest)) return root;
|
|
4858
5327
|
try {
|
|
4859
5328
|
fs5.rmSync(root, { recursive: true, force: true });
|
|
4860
|
-
fs5.mkdirSync(
|
|
4861
|
-
fs5.mkdirSync(
|
|
5329
|
+
fs5.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
|
|
5330
|
+
fs5.mkdirSync(path7.join(root, "skills"), { recursive: true });
|
|
4862
5331
|
fs5.writeFileSync(
|
|
4863
5332
|
manifest,
|
|
4864
5333
|
JSON.stringify(
|
|
@@ -4872,7 +5341,7 @@ function buildSkillPluginDir(skills) {
|
|
|
4872
5341
|
{ encoding: "utf8", mode: 384 }
|
|
4873
5342
|
);
|
|
4874
5343
|
for (const skill of skills) {
|
|
4875
|
-
linkSkill(skill.dir,
|
|
5344
|
+
linkSkill(skill.dir, path7.join(root, "skills", skill.name));
|
|
4876
5345
|
}
|
|
4877
5346
|
} catch (err) {
|
|
4878
5347
|
log.warn("failed to stage opencode skill plugin dir", {
|
|
@@ -4915,11 +5384,11 @@ import { unlink as unlink2 } from "fs/promises";
|
|
|
4915
5384
|
// src/claude-session-bun.ts
|
|
4916
5385
|
import * as os4 from "os";
|
|
4917
5386
|
import * as fs6 from "fs";
|
|
4918
|
-
import * as
|
|
5387
|
+
import * as path8 from "path";
|
|
4919
5388
|
import { execFileSync } from "child_process";
|
|
4920
5389
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
4921
5390
|
function resolveClaude(cmd = "claude") {
|
|
4922
|
-
if (
|
|
5391
|
+
if (path8.isAbsolute(cmd) && fs6.existsSync(cmd)) return cmd;
|
|
4923
5392
|
const viaBun = Bun.which(cmd);
|
|
4924
5393
|
if (viaBun) return viaBun;
|
|
4925
5394
|
const isWin = os4.platform() === "win32";
|
|
@@ -4935,18 +5404,31 @@ function resolveClaude(cmd = "claude") {
|
|
|
4935
5404
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
4936
5405
|
}
|
|
4937
5406
|
function encodeCwd(cwd) {
|
|
4938
|
-
return
|
|
5407
|
+
return path8.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
5408
|
+
}
|
|
5409
|
+
function interactiveSpawnEnv(opts) {
|
|
5410
|
+
return {
|
|
5411
|
+
...process.env,
|
|
5412
|
+
CLAUDE_CONFIG_DIR: opts.configDir,
|
|
5413
|
+
TERM: "xterm-256color",
|
|
5414
|
+
// Pin the binary so a mid-session autoupdate cannot invalidate the
|
|
5415
|
+
// detected version the flag gates read, and skip non-essential traffic.
|
|
5416
|
+
// Fills gaps only, so a var the user exported survives untouched.
|
|
5417
|
+
...cliHygieneEnv(),
|
|
5418
|
+
...opts.ignoreAnthropicApiKey ? { ANTHROPIC_API_KEY: void 0, ANTHROPIC_AUTH_TOKEN: void 0 } : {},
|
|
5419
|
+
...opts.effort ? { CLAUDE_CODE_EFFORT_LEVEL: opts.effort } : {}
|
|
5420
|
+
};
|
|
4939
5421
|
}
|
|
4940
5422
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
4941
5423
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4942
5424
|
function resolveConfigDir(configDir) {
|
|
4943
5425
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
4944
|
-
if (!value) return
|
|
5426
|
+
if (!value) return path8.join(os4.homedir(), ".claude");
|
|
4945
5427
|
if (value === "~") return os4.homedir();
|
|
4946
5428
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
4947
|
-
return
|
|
5429
|
+
return path8.join(os4.homedir(), value.slice(2));
|
|
4948
5430
|
}
|
|
4949
|
-
return
|
|
5431
|
+
return path8.resolve(value);
|
|
4950
5432
|
}
|
|
4951
5433
|
var ClaudeSession = class {
|
|
4952
5434
|
sessionId;
|
|
@@ -4964,11 +5446,11 @@ var ClaudeSession = class {
|
|
|
4964
5446
|
signal;
|
|
4965
5447
|
o;
|
|
4966
5448
|
constructor(opts = {}) {
|
|
4967
|
-
this.cwd =
|
|
5449
|
+
this.cwd = path8.resolve(opts.cwd ?? process.cwd());
|
|
4968
5450
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
4969
5451
|
this.signal = opts.signal;
|
|
4970
5452
|
this.sessionId = randomUUID4();
|
|
4971
|
-
this.jsonlPath =
|
|
5453
|
+
this.jsonlPath = path8.join(
|
|
4972
5454
|
this.configDir,
|
|
4973
5455
|
"projects",
|
|
4974
5456
|
encodeCwd(this.cwd),
|
|
@@ -5023,13 +5505,13 @@ var ClaudeSession = class {
|
|
|
5023
5505
|
this.lastDataAt = Date.now();
|
|
5024
5506
|
this.proc = Bun.spawn([claude, ...args], {
|
|
5025
5507
|
cwd: this.cwd,
|
|
5026
|
-
env: {
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
},
|
|
5508
|
+
env: interactiveSpawnEnv({
|
|
5509
|
+
// The resolved field, not `this.o.configDir`: same value (the
|
|
5510
|
+
// constructor copies it in) but typed as always present.
|
|
5511
|
+
configDir: this.configDir,
|
|
5512
|
+
ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
|
|
5513
|
+
effort: this.o.effort
|
|
5514
|
+
}),
|
|
5033
5515
|
terminal: {
|
|
5034
5516
|
cols: this.o.cols,
|
|
5035
5517
|
rows: this.o.rows,
|
|
@@ -6552,9 +7034,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6552
7034
|
cliToolCallIds: /* @__PURE__ */ new Set(),
|
|
6553
7035
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
6554
7036
|
});
|
|
7037
|
+
const failover = await resolveFailoverSpawn({
|
|
7038
|
+
account: this.config.account ?? DEFAULT_ACCOUNT,
|
|
7039
|
+
baseCliPath: this.config.baseCliPath ?? this.config.cliPath,
|
|
7040
|
+
cliPath: this.config.cliPath,
|
|
7041
|
+
modelId: effectiveModelId
|
|
7042
|
+
});
|
|
7043
|
+
const cliPath = failover.cliPath;
|
|
6555
7044
|
const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
|
|
6556
7045
|
getRuntimeMcpStatus(),
|
|
6557
|
-
detectCliVersion(
|
|
7046
|
+
detectCliVersion(cliPath),
|
|
6558
7047
|
this.resolvePlanModeQuestion(compactionMode)
|
|
6559
7048
|
]);
|
|
6560
7049
|
const systemPromptFile = buildAppendedSystemPrompt(
|
|
@@ -6565,10 +7054,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6565
7054
|
// An existing summary still carries: it is this key's prior context.
|
|
6566
7055
|
{ compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
|
|
6567
7056
|
);
|
|
6568
|
-
const { model: spawnModelId, fast: fastMode } = parseModelId(
|
|
7057
|
+
const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
|
|
6569
7058
|
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6570
7059
|
cwd,
|
|
6571
|
-
cliPath
|
|
7060
|
+
cliPath,
|
|
6572
7061
|
enabled: this.config.bridgeOpencodeSkills === true
|
|
6573
7062
|
});
|
|
6574
7063
|
const cliArgs = buildCliArgs({
|
|
@@ -6595,7 +7084,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6595
7084
|
});
|
|
6596
7085
|
const { spawn: spawn2 } = await import("child_process");
|
|
6597
7086
|
const { createInterface: createInterface2 } = await import("readline");
|
|
6598
|
-
const proc = spawn2(
|
|
7087
|
+
const proc = spawn2(cliPath, cliArgs, {
|
|
6599
7088
|
cwd,
|
|
6600
7089
|
stdio: ["pipe", "pipe", "pipe"],
|
|
6601
7090
|
env: claudeSpawnEnv({
|
|
@@ -6890,7 +7379,6 @@ ${plan}
|
|
|
6890
7379
|
}
|
|
6891
7380
|
async doStream(options) {
|
|
6892
7381
|
const warnings = [];
|
|
6893
|
-
const cliPath = this.config.cliPath;
|
|
6894
7382
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
6895
7383
|
const scope = this.requestScope(options);
|
|
6896
7384
|
const affinity = this.sessionAffinity(options);
|
|
@@ -6900,7 +7388,6 @@ ${plan}
|
|
|
6900
7388
|
this.getOpencodeAgent(options.providerOptions),
|
|
6901
7389
|
this.modelId
|
|
6902
7390
|
);
|
|
6903
|
-
const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
|
|
6904
7391
|
const reasoningEffort = compactionMode ? void 0 : resolveAgentEffort(
|
|
6905
7392
|
this.getOpencodeAgent(options.providerOptions),
|
|
6906
7393
|
this.getReasoningEffort(options.providerOptions)
|
|
@@ -6917,6 +7404,17 @@ ${plan}
|
|
|
6917
7404
|
const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
|
|
6918
7405
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
6919
7406
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
7407
|
+
const sourceAccount = normalizeAccountName(
|
|
7408
|
+
this.config.account ?? DEFAULT_ACCOUNT
|
|
7409
|
+
);
|
|
7410
|
+
const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
|
|
7411
|
+
let failover = useInteractive || compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
|
|
7412
|
+
account: sourceAccount,
|
|
7413
|
+
baseCliPath,
|
|
7414
|
+
cliPath: this.config.cliPath,
|
|
7415
|
+
modelId: effectiveModelId
|
|
7416
|
+
});
|
|
7417
|
+
let cliPath = failover.cliPath;
|
|
6920
7418
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
6921
7419
|
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
6922
7420
|
if (doctor) {
|
|
@@ -7059,15 +7557,90 @@ ${plan}
|
|
|
7059
7557
|
deleteActiveProcess(sk);
|
|
7060
7558
|
clearCompression(sk);
|
|
7061
7559
|
}
|
|
7560
|
+
const failoverAnswer = compactionMode || useInteractive ? null : consumeAccountFailoverAnswer(sk, options.prompt);
|
|
7561
|
+
if (failoverAnswer?.kind === "stop") {
|
|
7562
|
+
log.warn("account failover declined; ending the turn", {
|
|
7563
|
+
sessionKey: sk,
|
|
7564
|
+
account: sourceAccount,
|
|
7565
|
+
reason: failoverAnswer.reason
|
|
7566
|
+
});
|
|
7567
|
+
const note = formatFailoverStopNote(failoverAnswer.reason);
|
|
7568
|
+
const stream2 = new ReadableStream({
|
|
7569
|
+
start(controller) {
|
|
7570
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
7571
|
+
const id = generateId();
|
|
7572
|
+
controller.enqueue({ type: "text-start", id });
|
|
7573
|
+
controller.enqueue({ type: "text-delta", id, delta: note });
|
|
7574
|
+
controller.enqueue({ type: "text-end", id });
|
|
7575
|
+
controller.enqueue({
|
|
7576
|
+
type: "error",
|
|
7577
|
+
error: new Error(
|
|
7578
|
+
`Claude account "${sourceAccount}" is out of usage and no other account was picked.`
|
|
7579
|
+
)
|
|
7580
|
+
});
|
|
7581
|
+
controller.enqueue({
|
|
7582
|
+
type: "finish",
|
|
7583
|
+
finishReason: { unified: "error", raw: "account_limit" },
|
|
7584
|
+
usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
7585
|
+
providerMetadata: {
|
|
7586
|
+
"claude-code": {
|
|
7587
|
+
path: "account-failover-stop",
|
|
7588
|
+
synthetic: true,
|
|
7589
|
+
usageUnavailable: true
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7592
|
+
});
|
|
7593
|
+
controller.close();
|
|
7594
|
+
}
|
|
7595
|
+
});
|
|
7596
|
+
return { stream: stream2, request: { body: { text: "" } } };
|
|
7597
|
+
}
|
|
7598
|
+
let failoverNote = null;
|
|
7599
|
+
if (failoverAnswer?.kind === "switch") {
|
|
7600
|
+
setAccountOverride(
|
|
7601
|
+
failoverAnswer.sourceAccount,
|
|
7602
|
+
failoverAnswer.target,
|
|
7603
|
+
failoverUntil(failoverAnswer.resetsAt)
|
|
7604
|
+
);
|
|
7605
|
+
failover = await resolveFailoverSpawn({
|
|
7606
|
+
account: sourceAccount,
|
|
7607
|
+
baseCliPath,
|
|
7608
|
+
cliPath: this.config.cliPath,
|
|
7609
|
+
modelId: effectiveModelId
|
|
7610
|
+
});
|
|
7611
|
+
cliPath = failover.cliPath;
|
|
7612
|
+
asideTransportRef.cliPath = cliPath;
|
|
7613
|
+
failoverNote = formatFailoverNote({
|
|
7614
|
+
sourceAccount: failoverAnswer.sourceAccount,
|
|
7615
|
+
target: failoverAnswer.target,
|
|
7616
|
+
resetsAt: failoverAnswer.resetsAt
|
|
7617
|
+
});
|
|
7618
|
+
}
|
|
7619
|
+
const processForAccount = getActiveProcess(sk);
|
|
7620
|
+
if (!compactionMode && !useInteractive && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
|
|
7621
|
+
log.notice("claude process belongs to another account; starting fresh", {
|
|
7622
|
+
sessionKey: sk,
|
|
7623
|
+
was: processForAccount.cliPath,
|
|
7624
|
+
now: cliPath,
|
|
7625
|
+
failedOver: failover.failedOver
|
|
7626
|
+
});
|
|
7627
|
+
deleteActiveProcess(sk);
|
|
7628
|
+
deleteClaudeSessionId(sk);
|
|
7629
|
+
}
|
|
7062
7630
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
7063
7631
|
const hasActiveProcess = !!getActiveProcess(sk);
|
|
7064
|
-
|
|
7632
|
+
let includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
|
|
7633
|
+
if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
|
|
7634
|
+
includeHistoryContext = true;
|
|
7635
|
+
}
|
|
7636
|
+
const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
|
|
7065
7637
|
const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
|
|
7066
7638
|
if (exitPlanModeQuestionResult) {
|
|
7067
7639
|
log.info("sending plan approval decision to claude", { sk });
|
|
7068
7640
|
}
|
|
7069
7641
|
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
7070
|
-
const
|
|
7642
|
+
const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
|
|
7643
|
+
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
|
|
7071
7644
|
compactionMode,
|
|
7072
7645
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
7073
7646
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
@@ -7088,8 +7661,23 @@ ${plan}
|
|
|
7088
7661
|
);
|
|
7089
7662
|
const [runtimeStatus, cliVersion] = await Promise.all([
|
|
7090
7663
|
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus(),
|
|
7091
|
-
detectCliVersion(
|
|
7664
|
+
detectCliVersion(cliPath)
|
|
7092
7665
|
]);
|
|
7666
|
+
const failoverAccounts = failoverCandidates(
|
|
7667
|
+
this.config.failoverAccounts,
|
|
7668
|
+
sourceAccount
|
|
7669
|
+
);
|
|
7670
|
+
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover !== "off" && !compactionMode && !useInteractive && isAccountFailoverQuestionActive({
|
|
7671
|
+
configured: this.config.accountFailover,
|
|
7672
|
+
candidates: failoverAccounts,
|
|
7673
|
+
opencodeHasQuestion: (await loadLiveToolInfo()).hasQuestion,
|
|
7674
|
+
compactionMode,
|
|
7675
|
+
interactive: !!useInteractive,
|
|
7676
|
+
// A subagent follows its parent's account for free, because the
|
|
7677
|
+
// override is account-scoped. Asking it would put a form in a session
|
|
7678
|
+
// the operator is usually not even looking at.
|
|
7679
|
+
childSession: !!await fetchSessionParentId(affinity)
|
|
7680
|
+
});
|
|
7093
7681
|
log.info("doStream starting", {
|
|
7094
7682
|
cwd,
|
|
7095
7683
|
model: effectiveModelId,
|
|
@@ -7407,6 +7995,14 @@ ${plan}
|
|
|
7407
7995
|
currentTextId = null;
|
|
7408
7996
|
}
|
|
7409
7997
|
};
|
|
7998
|
+
if (failoverNote) {
|
|
7999
|
+
controller.enqueue({
|
|
8000
|
+
type: "text-delta",
|
|
8001
|
+
id: startTextBlock(),
|
|
8002
|
+
delta: failoverNote
|
|
8003
|
+
});
|
|
8004
|
+
endTextBlock();
|
|
8005
|
+
}
|
|
7410
8006
|
const reasoningIds = /* @__PURE__ */ new Map();
|
|
7411
8007
|
const reasoningStarted = /* @__PURE__ */ new Map();
|
|
7412
8008
|
let hadThinkingTextFromStream = false;
|
|
@@ -7441,7 +8037,12 @@ ${plan}
|
|
|
7441
8037
|
resultFallbackTimer = null;
|
|
7442
8038
|
}
|
|
7443
8039
|
};
|
|
7444
|
-
const
|
|
8040
|
+
const RESULT_FALLBACK_MS = (() => {
|
|
8041
|
+
const env = process.env.CLAUDE_CODE_RESULT_FALLBACK_MS;
|
|
8042
|
+
const parsed = env ? Number.parseInt(env, 10) : NaN;
|
|
8043
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
|
|
8044
|
+
})();
|
|
8045
|
+
const startResultFallback = (delayMs = RESULT_FALLBACK_MS) => {
|
|
7445
8046
|
clearFallbackTimer();
|
|
7446
8047
|
if (!hasReceivedContent && !hasReceivedProgress || controllerClosed) return;
|
|
7447
8048
|
resultFallbackTimer = setTimeout(() => {
|
|
@@ -7449,6 +8050,14 @@ ${plan}
|
|
|
7449
8050
|
log.warn("result fallback timer fired \u2014 closing stream without result event", {
|
|
7450
8051
|
delayMs
|
|
7451
8052
|
});
|
|
8053
|
+
if (!autoContinueState.aborted) {
|
|
8054
|
+
controller.enqueue({
|
|
8055
|
+
type: "text-delta",
|
|
8056
|
+
id: startTextBlock(),
|
|
8057
|
+
delta: formatStreamTimeoutNote(delayMs)
|
|
8058
|
+
});
|
|
8059
|
+
endTextBlock();
|
|
8060
|
+
}
|
|
7452
8061
|
closeHandler();
|
|
7453
8062
|
}, delayMs);
|
|
7454
8063
|
};
|
|
@@ -7579,6 +8188,7 @@ ${plan}
|
|
|
7579
8188
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
7580
8189
|
let resultMeta = {};
|
|
7581
8190
|
let resultFailure;
|
|
8191
|
+
let accountLimitHit = null;
|
|
7582
8192
|
const drainBuffer = [];
|
|
7583
8193
|
let drainTimer = null;
|
|
7584
8194
|
const DRAIN_QUIET_MS = 100;
|
|
@@ -7630,7 +8240,7 @@ ${plan}
|
|
|
7630
8240
|
} catch {
|
|
7631
8241
|
}
|
|
7632
8242
|
};
|
|
7633
|
-
const
|
|
8243
|
+
const finishWithQuestionCall = (call) => {
|
|
7634
8244
|
if (controllerClosed) return;
|
|
7635
8245
|
endTextBlock();
|
|
7636
8246
|
controller.enqueue({
|
|
@@ -7740,6 +8350,25 @@ ${plan}
|
|
|
7740
8350
|
});
|
|
7741
8351
|
}
|
|
7742
8352
|
activeProcess?.pendingProxyCompletions?.clear();
|
|
8353
|
+
if (accountLimitHit && failoverAskActive) {
|
|
8354
|
+
const call = createAccountFailoverQuestionCall(sk, {
|
|
8355
|
+
sourceAccount,
|
|
8356
|
+
candidates: failoverAccounts,
|
|
8357
|
+
resetsAt: accountLimitHit.resetsAt,
|
|
8358
|
+
window: accountLimitHit.window
|
|
8359
|
+
});
|
|
8360
|
+
log.warn(
|
|
8361
|
+
`Claude account "${sourceAccount}" is out of usage; asking which account to continue on.`,
|
|
8362
|
+
{
|
|
8363
|
+
sessionKey: sk,
|
|
8364
|
+
candidates: failoverAccounts,
|
|
8365
|
+
toolCallId: call.toolCallId,
|
|
8366
|
+
resetsAt: accountLimitHit.resetsAt ?? null
|
|
8367
|
+
}
|
|
8368
|
+
);
|
|
8369
|
+
finishWithQuestionCall(call);
|
|
8370
|
+
return;
|
|
8371
|
+
}
|
|
7743
8372
|
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
7744
8373
|
autoContinueState,
|
|
7745
8374
|
{
|
|
@@ -7873,6 +8502,13 @@ ${plan}
|
|
|
7873
8502
|
}
|
|
7874
8503
|
}
|
|
7875
8504
|
if (msg.type === "rate_limit_event") {
|
|
8505
|
+
const info = parseRateLimitEvent(msg);
|
|
8506
|
+
if (info && isRateLimitRejected(info)) {
|
|
8507
|
+
accountLimitHit = {
|
|
8508
|
+
resetsAt: info.resetsAt ?? info.overageResetsAt,
|
|
8509
|
+
window: info.rateLimitType
|
|
8510
|
+
};
|
|
8511
|
+
}
|
|
7876
8512
|
const note = reportRateLimitEvent(msg);
|
|
7877
8513
|
if (note) {
|
|
7878
8514
|
controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
|
|
@@ -8036,7 +8672,7 @@ ${plan}
|
|
|
8036
8672
|
id: planId2,
|
|
8037
8673
|
delta: questionCall.text
|
|
8038
8674
|
});
|
|
8039
|
-
|
|
8675
|
+
finishWithQuestionCall(questionCall);
|
|
8040
8676
|
return;
|
|
8041
8677
|
}
|
|
8042
8678
|
const planId = startTextBlock();
|
|
@@ -8217,7 +8853,7 @@ ${plan}
|
|
|
8217
8853
|
id: planId2,
|
|
8218
8854
|
delta: questionCall.text
|
|
8219
8855
|
});
|
|
8220
|
-
|
|
8856
|
+
finishWithQuestionCall(questionCall);
|
|
8221
8857
|
return;
|
|
8222
8858
|
}
|
|
8223
8859
|
const planId = startTextBlock();
|
|
@@ -8389,6 +9025,11 @@ ${plan}
|
|
|
8389
9025
|
delta: msg.result
|
|
8390
9026
|
});
|
|
8391
9027
|
}
|
|
9028
|
+
if (!accountLimitHit && msg.is_error && isAccountLimitError({
|
|
9029
|
+
resultText: typeof msg.result === "string" ? msg.result : null
|
|
9030
|
+
})) {
|
|
9031
|
+
accountLimitHit = {};
|
|
9032
|
+
}
|
|
8392
9033
|
const failure = describeResultFailure(msg);
|
|
8393
9034
|
if (failure) {
|
|
8394
9035
|
resultFailure = msg.subtype;
|
|
@@ -8812,148 +9453,6 @@ ${plan}
|
|
|
8812
9453
|
}
|
|
8813
9454
|
};
|
|
8814
9455
|
|
|
8815
|
-
// src/accounts.ts
|
|
8816
|
-
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
8817
|
-
import path8 from "path";
|
|
8818
|
-
var BASE_PROVIDER_ID = "claude-code";
|
|
8819
|
-
var DEFAULT_ACCOUNT = "default";
|
|
8820
|
-
var SHARED_CAPABILITY_ITEMS = [
|
|
8821
|
-
"CLAUDE.md",
|
|
8822
|
-
"settings.json",
|
|
8823
|
-
"skills",
|
|
8824
|
-
"agents",
|
|
8825
|
-
"commands",
|
|
8826
|
-
"plugins"
|
|
8827
|
-
];
|
|
8828
|
-
function normalizeAccountName(account) {
|
|
8829
|
-
return account.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8830
|
-
}
|
|
8831
|
-
function resolveAccounts(value) {
|
|
8832
|
-
if (!Array.isArray(value)) return null;
|
|
8833
|
-
const accounts = value.map((account) => normalizeAccountName(String(account))).filter(Boolean);
|
|
8834
|
-
return Array.from(/* @__PURE__ */ new Set([DEFAULT_ACCOUNT, ...accounts]));
|
|
8835
|
-
}
|
|
8836
|
-
function accountProviderId(account) {
|
|
8837
|
-
return `${BASE_PROVIDER_ID}-${normalizeAccountName(account)}`;
|
|
8838
|
-
}
|
|
8839
|
-
function accountDisplayName(account) {
|
|
8840
|
-
return `Claude Code (${titleizeAccount(account)})`;
|
|
8841
|
-
}
|
|
8842
|
-
function accountModelSuffix(account) {
|
|
8843
|
-
const normalized = normalizeAccountName(account);
|
|
8844
|
-
return normalized === DEFAULT_ACCOUNT ? void 0 : normalized;
|
|
8845
|
-
}
|
|
8846
|
-
function accountConfigDir(account) {
|
|
8847
|
-
const normalized = normalizeAccountName(account);
|
|
8848
|
-
if (!normalized || normalized === DEFAULT_ACCOUNT) return void 0;
|
|
8849
|
-
return `~/.claude-${normalized}`;
|
|
8850
|
-
}
|
|
8851
|
-
function expandHome(value) {
|
|
8852
|
-
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
8853
|
-
if (value === "~") return home ?? value;
|
|
8854
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
8855
|
-
return home ? path8.join(home, value.slice(2)) : value;
|
|
8856
|
-
}
|
|
8857
|
-
return value;
|
|
8858
|
-
}
|
|
8859
|
-
async function ensureAccountRuntime(account, baseCliPath) {
|
|
8860
|
-
const configDir = accountConfigDir(account);
|
|
8861
|
-
if (!configDir) return { cliPath: baseCliPath };
|
|
8862
|
-
const expandedConfigDir = expandHome(configDir);
|
|
8863
|
-
await mkdir(expandedConfigDir, { recursive: true });
|
|
8864
|
-
try {
|
|
8865
|
-
await ensureSharedCapabilities(expandedConfigDir);
|
|
8866
|
-
} catch (err) {
|
|
8867
|
-
log.warn("failed to symlink shared capabilities; continuing anyway", {
|
|
8868
|
-
account,
|
|
8869
|
-
configDir: expandedConfigDir,
|
|
8870
|
-
error: String(err)
|
|
8871
|
-
});
|
|
8872
|
-
}
|
|
8873
|
-
const cliPath = await writeAccountWrapper(
|
|
8874
|
-
normalizeAccountName(account),
|
|
8875
|
-
baseCliPath,
|
|
8876
|
-
expandedConfigDir
|
|
8877
|
-
);
|
|
8878
|
-
return { cliPath, configDir: expandedConfigDir };
|
|
8879
|
-
}
|
|
8880
|
-
async function ensureSharedCapabilities(targetRoot) {
|
|
8881
|
-
const sourceRoot = expandHome("~/.claude");
|
|
8882
|
-
for (const item of SHARED_CAPABILITY_ITEMS) {
|
|
8883
|
-
await ensureSharedCapabilityItem(sourceRoot, targetRoot, item);
|
|
8884
|
-
}
|
|
8885
|
-
}
|
|
8886
|
-
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
8887
|
-
const source = path8.join(sourceRoot, item);
|
|
8888
|
-
const target = path8.join(targetRoot, item);
|
|
8889
|
-
let sourceStat;
|
|
8890
|
-
try {
|
|
8891
|
-
sourceStat = await lstat(source);
|
|
8892
|
-
} catch {
|
|
8893
|
-
return;
|
|
8894
|
-
}
|
|
8895
|
-
try {
|
|
8896
|
-
const targetStat = await lstat(target);
|
|
8897
|
-
if (targetStat.isSymbolicLink()) {
|
|
8898
|
-
const current = await readlink(target);
|
|
8899
|
-
const resolvedCurrent = path8.resolve(path8.dirname(target), current);
|
|
8900
|
-
const resolvedSource = path8.resolve(source);
|
|
8901
|
-
if (resolvedCurrent === resolvedSource) return;
|
|
8902
|
-
}
|
|
8903
|
-
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
8904
|
-
item,
|
|
8905
|
-
target,
|
|
8906
|
-
source
|
|
8907
|
-
});
|
|
8908
|
-
return;
|
|
8909
|
-
} catch {
|
|
8910
|
-
}
|
|
8911
|
-
const type = sourceStat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
|
|
8912
|
-
await symlink(source, target, type);
|
|
8913
|
-
}
|
|
8914
|
-
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
8915
|
-
const cacheRoot = path8.join(
|
|
8916
|
-
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
8917
|
-
"opencode-claude-code-plugin"
|
|
8918
|
-
);
|
|
8919
|
-
const wrapperPath = path8.join(cacheRoot, `claude-${account}`);
|
|
8920
|
-
const suffix = `@${account}`;
|
|
8921
|
-
await mkdir(cacheRoot, { recursive: true });
|
|
8922
|
-
const script = `#!/usr/bin/env bash
|
|
8923
|
-
set -euo pipefail
|
|
8924
|
-
|
|
8925
|
-
args=()
|
|
8926
|
-
while [[ $# -gt 0 ]]; do
|
|
8927
|
-
if [[ "$1" == "--model" && $# -ge 2 ]]; then
|
|
8928
|
-
model="$2"
|
|
8929
|
-
if [[ "$model" == *${shellDoubleQuote(suffix)} ]]; then
|
|
8930
|
-
model="\${model%${shellDoubleQuote(suffix)}}"
|
|
8931
|
-
fi
|
|
8932
|
-
args+=("$1" "$model")
|
|
8933
|
-
shift 2
|
|
8934
|
-
else
|
|
8935
|
-
args+=("$1")
|
|
8936
|
-
shift
|
|
8937
|
-
fi
|
|
8938
|
-
done
|
|
8939
|
-
|
|
8940
|
-
export CLAUDE_CONFIG_DIR=${shellSingleQuote(configDir)}
|
|
8941
|
-
exec ${shellSingleQuote(baseCliPath)} "\${args[@]}"
|
|
8942
|
-
`;
|
|
8943
|
-
await writeFile(wrapperPath, script, "utf8");
|
|
8944
|
-
await chmod(wrapperPath, 493);
|
|
8945
|
-
return wrapperPath;
|
|
8946
|
-
}
|
|
8947
|
-
function shellSingleQuote(value) {
|
|
8948
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
8949
|
-
}
|
|
8950
|
-
function shellDoubleQuote(value) {
|
|
8951
|
-
return value.replace(/[$`"\\]/g, "\\$&");
|
|
8952
|
-
}
|
|
8953
|
-
function titleizeAccount(account) {
|
|
8954
|
-
return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
8955
|
-
}
|
|
8956
|
-
|
|
8957
9456
|
// src/cleanup-stale.ts
|
|
8958
9457
|
import {
|
|
8959
9458
|
existsSync as existsSync4,
|
|
@@ -9148,6 +9647,9 @@ function createClaudeCode(settings = {}) {
|
|
|
9148
9647
|
cwd: settings.cwd,
|
|
9149
9648
|
account: settings.account,
|
|
9150
9649
|
configDir: settings.configDir,
|
|
9650
|
+
failoverAccounts: settings.failoverAccounts,
|
|
9651
|
+
baseCliPath: settings.baseCliPath ?? cliPath,
|
|
9652
|
+
accountFailover: settings.accountFailover ?? "ask",
|
|
9151
9653
|
providerID: settings.providerID,
|
|
9152
9654
|
skipPermissions: settings.skipPermissions ?? true,
|
|
9153
9655
|
permissionMode: settings.permissionMode,
|
|
@@ -9274,7 +9776,11 @@ async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefault
|
|
|
9274
9776
|
npm: existing?.npm ?? pluginEntrypoint(),
|
|
9275
9777
|
options: {
|
|
9276
9778
|
...mergedOptions,
|
|
9277
|
-
...runtime
|
|
9779
|
+
...runtime,
|
|
9780
|
+
// The pre-wrapper binary, kept because `runtime` replaces `cliPath`
|
|
9781
|
+
// with the account's wrapper and a failover has to build ANOTHER
|
|
9782
|
+
// account's wrapper on top of the same base (src/account-failover.ts).
|
|
9783
|
+
baseCliPath: cliPath
|
|
9278
9784
|
}
|
|
9279
9785
|
// models is intentionally omitted: both callers overwrite it with
|
|
9280
9786
|
// configModelsForProvider(), which emits the flat config schema
|
|
@@ -9307,7 +9813,11 @@ async function expandAccountProviders(config) {
|
|
|
9307
9813
|
providerID,
|
|
9308
9814
|
{
|
|
9309
9815
|
...seedOptions,
|
|
9310
|
-
account
|
|
9816
|
+
account,
|
|
9817
|
+
// The resolved list, so this account's language model can offer
|
|
9818
|
+
// the others when it runs out of usage. `accounts` itself stays
|
|
9819
|
+
// stripped by cleanProviderOptions.
|
|
9820
|
+
failoverAccounts: accounts
|
|
9311
9821
|
},
|
|
9312
9822
|
accountDisplayName(account)
|
|
9313
9823
|
),
|