@khalilgharbaoui/opencode-claude-code-plugin 0.23.0 → 0.25.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 +40 -6
- package/dist/index.d.ts +34 -0
- package/dist/index.js +1187 -674
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +40 -3
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
|
|
@@ -489,15 +1293,15 @@ import { unlink } from "fs/promises";
|
|
|
489
1293
|
// src/proxy-mcp.ts
|
|
490
1294
|
import { createServer } from "http";
|
|
491
1295
|
import * as fs2 from "fs";
|
|
492
|
-
import * as
|
|
1296
|
+
import * as path3 from "path";
|
|
493
1297
|
import * as crypto from "crypto";
|
|
494
1298
|
import { EventEmitter } from "events";
|
|
495
1299
|
|
|
496
1300
|
// src/tmp.ts
|
|
497
1301
|
import * as fs from "fs";
|
|
498
1302
|
import * as os from "os";
|
|
499
|
-
import * as
|
|
500
|
-
var PLUGIN_TMP_DIR =
|
|
1303
|
+
import * as path2 from "path";
|
|
1304
|
+
var PLUGIN_TMP_DIR = path2.join(
|
|
501
1305
|
os.tmpdir(),
|
|
502
1306
|
`opencode-claude-code-${process.pid}`
|
|
503
1307
|
);
|
|
@@ -1184,7 +1988,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1184
1988
|
2
|
|
1185
1989
|
);
|
|
1186
1990
|
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
1187
|
-
const outPath =
|
|
1991
|
+
const outPath = path3.join(
|
|
1188
1992
|
pluginTmpDir(),
|
|
1189
1993
|
`proxy-${hash}.json`
|
|
1190
1994
|
);
|
|
@@ -1605,191 +2409,32 @@ function resolvePendingProxyCallById(toolCallId, result) {
|
|
|
1605
2409
|
toolCallId: pending.toolCallId,
|
|
1606
2410
|
toolName: pending.toolName
|
|
1607
2411
|
});
|
|
1608
|
-
return true;
|
|
1609
|
-
}
|
|
1610
|
-
function rejectPendingProxyCallById(toolCallId, error) {
|
|
1611
|
-
const pending = pendingByCallId.get(toolCallId);
|
|
1612
|
-
if (!pending) return false;
|
|
1613
|
-
pendingByCallId.delete(toolCallId);
|
|
1614
|
-
indexRemove(pending.sessionKey, toolCallId);
|
|
1615
|
-
clearPendingTimers(pending);
|
|
1616
|
-
pending.reject(error);
|
|
1617
|
-
log.notice("rejected pending proxy call", {
|
|
1618
|
-
sessionKey: pending.sessionKey,
|
|
1619
|
-
toolCallId: pending.toolCallId,
|
|
1620
|
-
toolName: pending.toolName,
|
|
1621
|
-
error: error.message
|
|
1622
|
-
});
|
|
1623
|
-
return true;
|
|
1624
|
-
}
|
|
1625
|
-
function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
1626
|
-
const s = callIdsBySession.get(sessionKey2);
|
|
1627
|
-
if (!s) return 0;
|
|
1628
|
-
const ids = [...s];
|
|
1629
|
-
let count = 0;
|
|
1630
|
-
for (const id of ids) {
|
|
1631
|
-
if (rejectPendingProxyCallById(id, error)) count++;
|
|
1632
|
-
}
|
|
1633
|
-
return count;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// src/plan-mode-question.ts
|
|
1637
|
-
var QUESTION_TOOL_NAME = "question";
|
|
1638
|
-
var APPROVED_EXIT_PLAN_MODE_MESSAGE = "User has approved your plan. You can now start coding. Start with updating your todo list if applicable.";
|
|
1639
|
-
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:";
|
|
1640
|
-
var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
1641
|
-
var OPENCODE_QUESTION_RESULT_PREFIX = `User has answered your questions: "${PLAN_MODE_APPROVAL_QUESTION}"="`;
|
|
1642
|
-
var OPENCODE_QUESTION_RESULT_SUFFIX = `". You can now continue with the user's answers in mind.`;
|
|
1643
|
-
var KEY_SEPARATOR = "\0";
|
|
1644
|
-
function isPlanModeQuestionActive(input) {
|
|
1645
|
-
if (input.compactionMode) return false;
|
|
1646
|
-
if (input.configured !== true) return false;
|
|
1647
|
-
return input.opencodeHasQuestion;
|
|
1648
|
-
}
|
|
1649
|
-
var pendingQuestions = /* @__PURE__ */ new Map();
|
|
1650
|
-
function pendingKey(sessionKey2, questionToolCallId) {
|
|
1651
|
-
return `${sessionKey2}${KEY_SEPARATOR}${questionToolCallId}`;
|
|
1652
|
-
}
|
|
1653
|
-
function clearExitPlanModeQuestions(sessionKey2) {
|
|
1654
|
-
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
1655
|
-
for (const key of pendingQuestions.keys()) {
|
|
1656
|
-
if (key.startsWith(prefix)) pendingQuestions.delete(key);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
function hasExitPlanModeQuestions(sessionKey2) {
|
|
1660
|
-
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
1661
|
-
return [...pendingQuestions.keys()].some((key) => key.startsWith(prefix));
|
|
1662
|
-
}
|
|
1663
|
-
function createExitPlanModeQuestionCall(sessionKey2, exitPlanModeToolUseId, plan, questionToolCallId = `exit_plan_question_${exitPlanModeToolUseId}`) {
|
|
1664
|
-
pendingQuestions.set(pendingKey(sessionKey2, questionToolCallId), exitPlanModeToolUseId);
|
|
1665
|
-
return {
|
|
1666
|
-
toolCallId: questionToolCallId,
|
|
1667
|
-
toolName: QUESTION_TOOL_NAME,
|
|
1668
|
-
input: {
|
|
1669
|
-
questions: [
|
|
1670
|
-
{
|
|
1671
|
-
header: "Plan approval",
|
|
1672
|
-
question: PLAN_MODE_APPROVAL_QUESTION,
|
|
1673
|
-
options: [
|
|
1674
|
-
{ label: "yes", description: "" },
|
|
1675
|
-
{ label: "no", description: "" }
|
|
1676
|
-
],
|
|
1677
|
-
multiple: false,
|
|
1678
|
-
custom: true
|
|
1679
|
-
}
|
|
1680
|
-
]
|
|
1681
|
-
},
|
|
1682
|
-
text: plan ? `
|
|
1683
|
-
|
|
1684
|
-
${plan}
|
|
1685
|
-
` : "\n\n"
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
function buildToolResultMessage(input) {
|
|
1689
|
-
return JSON.stringify({
|
|
1690
|
-
type: "user",
|
|
1691
|
-
message: {
|
|
1692
|
-
role: "user",
|
|
1693
|
-
content: [
|
|
1694
|
-
input.approved ? {
|
|
1695
|
-
type: "tool_result",
|
|
1696
|
-
tool_use_id: input.toolUseId,
|
|
1697
|
-
content: APPROVED_EXIT_PLAN_MODE_MESSAGE
|
|
1698
|
-
} : {
|
|
1699
|
-
type: "tool_result",
|
|
1700
|
-
tool_use_id: input.toolUseId,
|
|
1701
|
-
content: `${REJECTED_EXIT_PLAN_MODE_PREFIX}
|
|
1702
|
-
${input.feedback || "no"}`,
|
|
1703
|
-
is_error: true
|
|
1704
|
-
}
|
|
1705
|
-
]
|
|
1706
|
-
}
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
function tryParseJson(text) {
|
|
1710
|
-
try {
|
|
1711
|
-
return JSON.parse(text);
|
|
1712
|
-
} catch {
|
|
1713
|
-
return text;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
function unwrapToolOutput(part) {
|
|
1717
|
-
const output = part?.output ?? part?.result;
|
|
1718
|
-
if (typeof output === "string") return tryParseJson(output);
|
|
1719
|
-
if (!output || typeof output !== "object") return output;
|
|
1720
|
-
switch (output.type) {
|
|
1721
|
-
case "json":
|
|
1722
|
-
case "error-json":
|
|
1723
|
-
return output.value;
|
|
1724
|
-
case "text":
|
|
1725
|
-
case "error-text":
|
|
1726
|
-
return tryParseJson(String(output.value ?? ""));
|
|
1727
|
-
case "execution-denied":
|
|
1728
|
-
return {
|
|
1729
|
-
denied: true,
|
|
1730
|
-
reason: String(output.reason ?? "question rejected")
|
|
1731
|
-
};
|
|
1732
|
-
case "content":
|
|
1733
|
-
return Array.isArray(output.value) ? output.value.map((item) => {
|
|
1734
|
-
if (item?.type === "text") return item.text;
|
|
1735
|
-
return JSON.stringify(item);
|
|
1736
|
-
}).join("\n") : output.value;
|
|
1737
|
-
default:
|
|
1738
|
-
return output;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
function unwrapOpencodeQuestionResult(value) {
|
|
1742
|
-
if (value.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) && value.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
1743
|
-
return value.slice(
|
|
1744
|
-
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
1745
|
-
-OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
1746
|
-
);
|
|
1747
|
-
}
|
|
1748
|
-
return value;
|
|
1749
|
-
}
|
|
1750
|
-
function collectAnswerStrings(value) {
|
|
1751
|
-
if (typeof value === "string") return [unwrapOpencodeQuestionResult(value)];
|
|
1752
|
-
if (Array.isArray(value)) return value.flatMap(collectAnswerStrings);
|
|
1753
|
-
if (!value || typeof value !== "object") return [];
|
|
1754
|
-
const obj = value;
|
|
1755
|
-
if (obj.denied === true) return [String(obj.reason ?? "question rejected")];
|
|
1756
|
-
for (const key of ["answers", "answer", "selected", "selection", "value"]) {
|
|
1757
|
-
if (key in obj) return collectAnswerStrings(obj[key]);
|
|
1758
|
-
}
|
|
1759
|
-
return [];
|
|
1760
|
-
}
|
|
1761
|
-
function classifyQuestionResult(part) {
|
|
1762
|
-
const output = unwrapToolOutput(part);
|
|
1763
|
-
const answers = collectAnswerStrings(output).map((answer) => answer.trim()).filter(Boolean);
|
|
1764
|
-
if (answers.length === 1 && answers[0].toLowerCase() === "yes") {
|
|
1765
|
-
return { approved: true, feedback: "" };
|
|
1766
|
-
}
|
|
1767
|
-
return {
|
|
1768
|
-
approved: false,
|
|
1769
|
-
feedback: answers.length > 0 ? answers.join("\n") : "no"
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
function consumeExitPlanModeQuestionResult(sessionKey2, prompt) {
|
|
1773
|
-
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1774
|
-
const msg = prompt[i];
|
|
1775
|
-
if (!Array.isArray(msg.content)) continue;
|
|
1776
|
-
for (const part of msg.content) {
|
|
1777
|
-
if (part?.type !== "tool-result" || typeof part.toolCallId !== "string") {
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
const key = pendingKey(sessionKey2, part.toolCallId);
|
|
1781
|
-
const exitPlanModeToolUseId = pendingQuestions.get(key);
|
|
1782
|
-
if (!exitPlanModeToolUseId) continue;
|
|
1783
|
-
pendingQuestions.delete(key);
|
|
1784
|
-
const result = classifyQuestionResult(part);
|
|
1785
|
-
return buildToolResultMessage({
|
|
1786
|
-
toolUseId: exitPlanModeToolUseId,
|
|
1787
|
-
approved: result.approved,
|
|
1788
|
-
feedback: result.feedback
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
2412
|
+
return true;
|
|
2413
|
+
}
|
|
2414
|
+
function rejectPendingProxyCallById(toolCallId, error) {
|
|
2415
|
+
const pending = pendingByCallId.get(toolCallId);
|
|
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++;
|
|
1791
2436
|
}
|
|
1792
|
-
return
|
|
2437
|
+
return count;
|
|
1793
2438
|
}
|
|
1794
2439
|
|
|
1795
2440
|
// src/compression-store.ts
|
|
@@ -1822,7 +2467,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
1822
2467
|
var MAX_HISTORY_EXCHANGES = 20;
|
|
1823
2468
|
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
1824
2469
|
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
1825
|
-
function
|
|
2470
|
+
function isRecord2(value) {
|
|
1826
2471
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1827
2472
|
}
|
|
1828
2473
|
var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -1833,7 +2478,7 @@ function parseSideQuestionContent(content) {
|
|
|
1833
2478
|
} else if (Array.isArray(content)) {
|
|
1834
2479
|
const parts = [];
|
|
1835
2480
|
for (const part of content) {
|
|
1836
|
-
if (!
|
|
2481
|
+
if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
1837
2482
|
parts.push(part.text);
|
|
1838
2483
|
}
|
|
1839
2484
|
text = parts.join("\n");
|
|
@@ -1852,7 +2497,7 @@ function assistantText(content) {
|
|
|
1852
2497
|
if (!Array.isArray(content)) return "";
|
|
1853
2498
|
const parts = [];
|
|
1854
2499
|
for (const part of content) {
|
|
1855
|
-
if (
|
|
2500
|
+
if (isRecord2(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
1856
2501
|
}
|
|
1857
2502
|
return parts.join("\n").trim();
|
|
1858
2503
|
}
|
|
@@ -1882,9 +2527,9 @@ function dispatchSideQuestionResponse(activeProcess, line) {
|
|
|
1882
2527
|
} catch {
|
|
1883
2528
|
return false;
|
|
1884
2529
|
}
|
|
1885
|
-
if (!
|
|
2530
|
+
if (!isRecord2(message) || message.type !== "control_response") return false;
|
|
1886
2531
|
const response = message.response;
|
|
1887
|
-
if (!
|
|
2532
|
+
if (!isRecord2(response) || typeof response.request_id !== "string") return false;
|
|
1888
2533
|
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
1889
2534
|
}
|
|
1890
2535
|
async function requestSideQuestion(activeProcess, question, options) {
|
|
@@ -1969,7 +2614,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
1969
2614
|
return;
|
|
1970
2615
|
}
|
|
1971
2616
|
const result = response.response;
|
|
1972
|
-
if (response.subtype !== "success" || !
|
|
2617
|
+
if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
1973
2618
|
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
1974
2619
|
return;
|
|
1975
2620
|
}
|
|
@@ -2304,11 +2949,31 @@ async function deleteActiveProcessAndWait(key, options = {}) {
|
|
|
2304
2949
|
function getClaudeSessionId(key) {
|
|
2305
2950
|
return claudeSessions.get(key);
|
|
2306
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
|
+
}
|
|
2307
2969
|
function setClaudeSessionId(key, sessionId) {
|
|
2970
|
+
claudeSessions.delete(key);
|
|
2308
2971
|
claudeSessions.set(key, sessionId);
|
|
2972
|
+
capClaudeSessions();
|
|
2309
2973
|
}
|
|
2310
2974
|
function deleteClaudeSessionId(key) {
|
|
2311
2975
|
clearExitPlanModeQuestions(key);
|
|
2976
|
+
clearAccountFailoverQuestions(key);
|
|
2312
2977
|
const claudeSessionId = claudeSessions.get(key);
|
|
2313
2978
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
2314
2979
|
claudeSessions.delete(key);
|
|
@@ -2365,6 +3030,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
2365
3030
|
systemPromptFile,
|
|
2366
3031
|
effort,
|
|
2367
3032
|
startedAt: Date.now(),
|
|
3033
|
+
cliPath,
|
|
2368
3034
|
cliArgs: [...cliArgs],
|
|
2369
3035
|
unattendedLines: [],
|
|
2370
3036
|
unattendedDropped: 0
|
|
@@ -2851,263 +3517,30 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
2851
3517
|
log.info("btw: answered inside the running turn, dropping the /btw message", {
|
|
2852
3518
|
sessionID: input.sessionID,
|
|
2853
3519
|
waitedMs: Date.now() - started
|
|
2854
|
-
});
|
|
2855
|
-
throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
2856
|
-
}
|
|
2857
|
-
log.info("btw: turn over, releasing the /btw message", {
|
|
2858
|
-
sessionID: input.sessionID,
|
|
2859
|
-
idle: outcome === "idle",
|
|
2860
|
-
waitedMs: Date.now() - started
|
|
2861
|
-
});
|
|
2862
|
-
if (outcome === "timeout") {
|
|
2863
|
-
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
2864
|
-
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
// src/cli-events.ts
|
|
2869
|
-
function isRecord2(value) {
|
|
2870
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2871
|
-
}
|
|
2872
|
-
function str(value) {
|
|
2873
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2874
|
-
}
|
|
2875
|
-
function num(value) {
|
|
2876
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2877
|
-
}
|
|
2878
|
-
var RATE_LIMIT_MARKER = "\u258C **rate limit:**";
|
|
2879
|
-
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.";
|
|
2880
|
-
var OVERAGE_DISABLED_REASONS = {
|
|
2881
|
-
overage_not_provisioned: "extra usage is not set up on this account",
|
|
2882
|
-
org_level_disabled: "extra usage is disabled for your organization",
|
|
2883
|
-
org_level_disabled_until: "extra usage is disabled for your organization for now",
|
|
2884
|
-
out_of_credits: "the account's usage credits are spent",
|
|
2885
|
-
seat_tier_level_disabled: "your seat tier does not allow extra usage",
|
|
2886
|
-
member_level_disabled: "extra usage is disabled for your member account",
|
|
2887
|
-
seat_tier_zero_credit_limit: "your seat tier has a zero credit limit",
|
|
2888
|
-
group_zero_credit_limit: "your group has a zero credit limit",
|
|
2889
|
-
member_zero_credit_limit: "your member account has a zero credit limit",
|
|
2890
|
-
org_service_level_disabled: "your organization's service level does not include extra usage",
|
|
2891
|
-
no_limits_configured: "no usage limits are configured for this account",
|
|
2892
|
-
fetch_error: "the CLI could not read the account's usage limits"
|
|
2893
|
-
};
|
|
2894
|
-
var RATE_LIMIT_WINDOWS = {
|
|
2895
|
-
five_hour: "the 5-hour window",
|
|
2896
|
-
seven_day: "the 7-day window",
|
|
2897
|
-
seven_day_opus: "the 7-day Opus window",
|
|
2898
|
-
seven_day_sonnet: "the 7-day Sonnet window",
|
|
2899
|
-
seven_day_overage_included: "the 7-day extra-usage window",
|
|
2900
|
-
overage: "extra usage"
|
|
2901
|
-
};
|
|
2902
|
-
function parseRateLimitEvent(msg) {
|
|
2903
|
-
if (msg.type !== "rate_limit_event") return null;
|
|
2904
|
-
const info = msg.rate_limit_info;
|
|
2905
|
-
if (!isRecord2(info)) return null;
|
|
2906
|
-
return {
|
|
2907
|
-
status: str(info.status),
|
|
2908
|
-
rateLimitType: str(info.rateLimitType),
|
|
2909
|
-
resetsAt: num(info.resetsAt),
|
|
2910
|
-
utilization: num(info.utilization),
|
|
2911
|
-
isUsingOverage: info.isUsingOverage === true,
|
|
2912
|
-
overageStatus: str(info.overageStatus),
|
|
2913
|
-
overageResetsAt: num(info.overageResetsAt),
|
|
2914
|
-
overageDisabledReason: str(info.overageDisabledReason)
|
|
2915
|
-
};
|
|
2916
|
-
}
|
|
2917
|
-
function formatResetsAt(resetsAt) {
|
|
2918
|
-
if (resetsAt === void 0) return void 0;
|
|
2919
|
-
const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
|
|
2920
|
-
const date = new Date(ms);
|
|
2921
|
-
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
2922
|
-
}
|
|
2923
|
-
function rateLimitKey(info) {
|
|
2924
|
-
return [
|
|
2925
|
-
info.status ?? "?",
|
|
2926
|
-
info.rateLimitType ?? "?",
|
|
2927
|
-
info.overageStatus ?? "?",
|
|
2928
|
-
info.overageDisabledReason ?? "?"
|
|
2929
|
-
].join("|");
|
|
2930
|
-
}
|
|
2931
|
-
function isRateLimitRejected(info) {
|
|
2932
|
-
return info.status === "rejected" || info.overageStatus === "rejected";
|
|
2933
|
-
}
|
|
2934
|
-
function describeRateLimit(info) {
|
|
2935
|
-
if (!info.status && !info.overageStatus) return null;
|
|
2936
|
-
const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
|
|
2937
|
-
const resets = formatResetsAt(info.resetsAt);
|
|
2938
|
-
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
2939
|
-
const key = rateLimitKey(info);
|
|
2940
|
-
if (isRateLimitRejected(info)) {
|
|
2941
|
-
const parts = [];
|
|
2942
|
-
parts.push(
|
|
2943
|
-
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."
|
|
2944
|
-
);
|
|
2945
|
-
const reason = info.overageDisabledReason ? OVERAGE_DISABLED_REASONS[info.overageDisabledReason] ?? info.overageDisabledReason : void 0;
|
|
2946
|
-
if (reason) parts.push(`Extra usage is unavailable because ${reason}.`);
|
|
2947
|
-
const resetAt = resets ?? overageResets;
|
|
2948
|
-
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
2949
|
-
parts.push(RATE_LIMIT_ACTION);
|
|
2950
|
-
const message = parts.join(" ");
|
|
2951
|
-
return { key, level: "warn", message, transcript: `
|
|
2952
|
-
${RATE_LIMIT_MARKER} ${message}
|
|
2953
|
-
` };
|
|
2954
|
-
}
|
|
2955
|
-
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
2956
|
-
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
2957
|
-
return {
|
|
2958
|
-
key,
|
|
2959
|
-
level: "notice",
|
|
2960
|
-
message: `Approaching the usage limit${window ? ` for ${window}` : ""}${used}${resets ? `, resets at ${resets}` : ""}.`,
|
|
2961
|
-
transcript: null
|
|
2962
|
-
};
|
|
2963
|
-
}
|
|
2964
|
-
return {
|
|
2965
|
-
key,
|
|
2966
|
-
level: "info",
|
|
2967
|
-
message: `usage limits updated${window ? ` for ${window}` : ""}`,
|
|
2968
|
-
transcript: null
|
|
2969
|
-
};
|
|
2970
|
-
}
|
|
2971
|
-
var reportedRateLimits = /* @__PURE__ */ new Set();
|
|
2972
|
-
function reportRateLimitEvent(msg) {
|
|
2973
|
-
const info = parseRateLimitEvent(msg);
|
|
2974
|
-
if (!info) return null;
|
|
2975
|
-
const report = describeRateLimit(info);
|
|
2976
|
-
if (!report) return null;
|
|
2977
|
-
const data = { ...info };
|
|
2978
|
-
if (reportedRateLimits.has(report.key)) {
|
|
2979
|
-
log.debug(report.message, data);
|
|
2980
|
-
return null;
|
|
2981
|
-
}
|
|
2982
|
-
reportedRateLimits.add(report.key);
|
|
2983
|
-
log[report.level](report.message, data);
|
|
2984
|
-
return report.transcript;
|
|
2985
|
-
}
|
|
2986
|
-
var API_KEY_SOURCES = /* @__PURE__ */ new Set([
|
|
2987
|
-
"ANTHROPIC_API_KEY",
|
|
2988
|
-
"apiKeyHelper",
|
|
2989
|
-
"/login managed key",
|
|
2990
|
-
"user",
|
|
2991
|
-
"project",
|
|
2992
|
-
"org",
|
|
2993
|
-
"temporary"
|
|
2994
|
-
]);
|
|
2995
|
-
function parseSystemInit(msg) {
|
|
2996
|
-
if (msg.type !== "system" || msg.subtype !== "init") return null;
|
|
2997
|
-
const raw = msg;
|
|
2998
|
-
const servers = [];
|
|
2999
|
-
if (Array.isArray(raw.mcp_servers)) {
|
|
3000
|
-
for (const entry of raw.mcp_servers) {
|
|
3001
|
-
if (!isRecord2(entry)) continue;
|
|
3002
|
-
servers.push({ name: str(entry.name) ?? "unknown", status: str(entry.status) ?? "unknown" });
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
return {
|
|
3006
|
-
apiKeySource: str(raw.apiKeySource),
|
|
3007
|
-
permissionMode: str(raw.permissionMode),
|
|
3008
|
-
model: str(raw.model),
|
|
3009
|
-
cliVersion: str(raw.claude_code_version),
|
|
3010
|
-
toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
|
|
3011
|
-
mcpServers: servers
|
|
3012
|
-
};
|
|
3013
|
-
}
|
|
3014
|
-
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
3015
|
-
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
3016
|
-
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.`;
|
|
3017
|
-
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.`;
|
|
3018
|
-
}
|
|
3019
|
-
var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
3020
|
-
var warnedMcpFailures = /* @__PURE__ */ new Set();
|
|
3021
|
-
function reportSystemInit(msg, options = {}) {
|
|
3022
|
-
const info = parseSystemInit(msg);
|
|
3023
|
-
if (!info) return;
|
|
3024
|
-
log.info("claude session init", {
|
|
3025
|
-
apiKeySource: info.apiKeySource ?? null,
|
|
3026
|
-
permissionMode: info.permissionMode ?? null,
|
|
3027
|
-
model: info.model ?? null,
|
|
3028
|
-
cliVersion: info.cliVersion ?? null,
|
|
3029
|
-
tools: info.toolCount,
|
|
3030
|
-
mcpServers: info.mcpServers
|
|
3031
|
-
});
|
|
3032
|
-
for (const server2 of info.mcpServers) {
|
|
3033
|
-
if (server2.status === "connected") continue;
|
|
3034
|
-
const key2 = `${server2.name}:${server2.status}`;
|
|
3035
|
-
const message = `MCP server "${server2.name}" is ${server2.status} in Claude Code; its tools are not available to the model this session.`;
|
|
3036
|
-
if (warnedMcpFailures.has(key2)) {
|
|
3037
|
-
log.debug(message, { server: server2.name, status: server2.status });
|
|
3038
|
-
continue;
|
|
3039
|
-
}
|
|
3040
|
-
warnedMcpFailures.add(key2);
|
|
3041
|
-
log.warn(message, { server: server2.name, status: server2.status });
|
|
3042
|
-
}
|
|
3043
|
-
const apiKeyMessage = apiKeySourceWarning(info.apiKeySource, options.ignoreAnthropicApiKey);
|
|
3044
|
-
if (!apiKeyMessage) return;
|
|
3045
|
-
const key = `${info.apiKeySource}:${options.ignoreAnthropicApiKey ? "ignored" : "passed"}`;
|
|
3046
|
-
if (warnedApiKeySources.has(key)) {
|
|
3047
|
-
log.debug(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
3048
|
-
return;
|
|
3049
|
-
}
|
|
3050
|
-
warnedApiKeySources.add(key);
|
|
3051
|
-
log.warn(apiKeyMessage, { apiKeySource: info.apiKeySource });
|
|
3052
|
-
}
|
|
3053
|
-
var COMPACT_BOUNDARY_MARKER = "\u258C **context compacted:**";
|
|
3054
|
-
function parseCompactBoundary(msg) {
|
|
3055
|
-
if (msg.type !== "system" || msg.subtype !== "compact_boundary") return null;
|
|
3056
|
-
const raw = msg;
|
|
3057
|
-
const meta = isRecord2(raw.compact_metadata) ? raw.compact_metadata : isRecord2(raw.compactMetadata) ? raw.compactMetadata : void 0;
|
|
3058
|
-
return {
|
|
3059
|
-
trigger: str(meta?.trigger) ?? "unknown",
|
|
3060
|
-
preTokens: num(meta?.pre_tokens) ?? num(meta?.preTokens),
|
|
3061
|
-
postTokens: num(meta?.post_tokens) ?? num(meta?.postTokens)
|
|
3062
|
-
};
|
|
3063
|
-
}
|
|
3064
|
-
function formatCompactBoundaryNote(boundary) {
|
|
3065
|
-
const how = boundary.trigger === "auto" ? "on its own" : `on a ${boundary.trigger} request`;
|
|
3066
|
-
const sizes = boundary.preTokens !== void 0 && boundary.postTokens !== void 0 ? ` (${boundary.preTokens.toLocaleString("en-US")} tokens to ${boundary.postTokens.toLocaleString("en-US")})` : "";
|
|
3067
|
-
return `
|
|
3068
|
-
${COMPACT_BOUNDARY_MARKER} Claude Code compacted its own context ${how}${sizes}. Earlier detail in this conversation is now a summary.
|
|
3069
|
-
`;
|
|
3070
|
-
}
|
|
3071
|
-
function reportCompactBoundary(msg) {
|
|
3072
|
-
const boundary = parseCompactBoundary(msg);
|
|
3073
|
-
if (!boundary) return null;
|
|
3074
|
-
log.notice("claude code compacted its own context", {
|
|
3075
|
-
trigger: boundary.trigger,
|
|
3076
|
-
preTokens: boundary.preTokens ?? null,
|
|
3077
|
-
postTokens: boundary.postTokens ?? null
|
|
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
|
|
3078
3527
|
});
|
|
3079
|
-
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
error_max_turns: "it hit its internal turn limit before finishing",
|
|
3084
|
-
error_during_execution: "it failed while running the turn",
|
|
3085
|
-
error_max_budget_usd: "it hit the configured spend limit for the turn",
|
|
3086
|
-
error_max_structured_output_retries: "it could not produce valid structured output"
|
|
3087
|
-
};
|
|
3088
|
-
function describeResultFailure(msg) {
|
|
3089
|
-
if (msg.type !== "result") return null;
|
|
3090
|
-
const subtype = msg.subtype;
|
|
3091
|
-
if (!subtype || subtype === "success") return null;
|
|
3092
|
-
const explanation = RESULT_SUBTYPES[subtype];
|
|
3093
|
-
return explanation ? `Claude Code ended the turn with \`${subtype}\`: ${explanation}.` : `Claude Code ended the turn with \`${subtype}\`.`;
|
|
3094
|
-
}
|
|
3095
|
-
function formatResultFailureNote(message) {
|
|
3096
|
-
return `
|
|
3097
|
-
${RESULT_ERROR_MARKER} ${message}
|
|
3098
|
-
`;
|
|
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
|
+
}
|
|
3099
3532
|
}
|
|
3100
3533
|
|
|
3101
3534
|
// src/startup-diagnostics.ts
|
|
3102
3535
|
import { execFile as execFile2 } from "child_process";
|
|
3103
3536
|
import * as fs4 from "fs";
|
|
3104
|
-
import * as
|
|
3537
|
+
import * as path5 from "path";
|
|
3105
3538
|
import { promisify as promisify2 } from "util";
|
|
3106
3539
|
import { fileURLToPath } from "url";
|
|
3107
3540
|
|
|
3108
3541
|
// src/mcp-bridge.ts
|
|
3109
3542
|
import * as fs3 from "fs";
|
|
3110
|
-
import * as
|
|
3543
|
+
import * as path4 from "path";
|
|
3111
3544
|
import * as os2 from "os";
|
|
3112
3545
|
import * as crypto2 from "crypto";
|
|
3113
3546
|
import {
|
|
@@ -3168,14 +3601,14 @@ function deepMerge(target, source) {
|
|
|
3168
3601
|
}
|
|
3169
3602
|
function walkUp(opts) {
|
|
3170
3603
|
const out = [];
|
|
3171
|
-
let current =
|
|
3604
|
+
let current = path4.resolve(opts.start);
|
|
3172
3605
|
while (true) {
|
|
3173
3606
|
for (const target of opts.targets) {
|
|
3174
|
-
const candidate =
|
|
3607
|
+
const candidate = path4.join(current, target);
|
|
3175
3608
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3176
3609
|
}
|
|
3177
|
-
if (opts.stop && current ===
|
|
3178
|
-
const parent =
|
|
3610
|
+
if (opts.stop && current === path4.resolve(opts.stop)) break;
|
|
3611
|
+
const parent = path4.dirname(current);
|
|
3179
3612
|
if (parent === current) break;
|
|
3180
3613
|
current = parent;
|
|
3181
3614
|
}
|
|
@@ -3183,28 +3616,28 @@ function walkUp(opts) {
|
|
|
3183
3616
|
}
|
|
3184
3617
|
function detectWorktree(cwd) {
|
|
3185
3618
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3186
|
-
if (override) return
|
|
3187
|
-
let current =
|
|
3619
|
+
if (override) return path4.resolve(override);
|
|
3620
|
+
let current = path4.resolve(cwd);
|
|
3188
3621
|
while (true) {
|
|
3189
|
-
const gitPath =
|
|
3622
|
+
const gitPath = path4.join(current, ".git");
|
|
3190
3623
|
try {
|
|
3191
3624
|
if (fs3.existsSync(gitPath)) return current;
|
|
3192
3625
|
} catch {
|
|
3193
3626
|
}
|
|
3194
|
-
const parent =
|
|
3627
|
+
const parent = path4.dirname(current);
|
|
3195
3628
|
if (parent === current) return void 0;
|
|
3196
3629
|
current = parent;
|
|
3197
3630
|
}
|
|
3198
3631
|
}
|
|
3199
3632
|
function globalConfigDir() {
|
|
3200
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3201
|
-
return
|
|
3633
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
|
|
3634
|
+
return path4.join(xdg, "opencode");
|
|
3202
3635
|
}
|
|
3203
3636
|
function loadGlobalConfig() {
|
|
3204
3637
|
const dir = globalConfigDir();
|
|
3205
3638
|
let merged = {};
|
|
3206
3639
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3207
|
-
const file =
|
|
3640
|
+
const file = path4.join(dir, name);
|
|
3208
3641
|
if (!fileExists(file)) continue;
|
|
3209
3642
|
const parsed = readAndParse(file);
|
|
3210
3643
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -3214,7 +3647,7 @@ function loadGlobalConfig() {
|
|
|
3214
3647
|
function loadProjectFilesInDir(dir) {
|
|
3215
3648
|
let merged = {};
|
|
3216
3649
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3217
|
-
const file =
|
|
3650
|
+
const file = path4.join(dir, name);
|
|
3218
3651
|
if (!fileExists(file)) continue;
|
|
3219
3652
|
const parsed = readAndParse(file);
|
|
3220
3653
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
@@ -3225,7 +3658,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3225
3658
|
const dirs = [];
|
|
3226
3659
|
const seen = /* @__PURE__ */ new Set();
|
|
3227
3660
|
const push = (p) => {
|
|
3228
|
-
const abs =
|
|
3661
|
+
const abs = path4.resolve(p);
|
|
3229
3662
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
3230
3663
|
seen.add(abs);
|
|
3231
3664
|
dirs.push(abs);
|
|
@@ -3241,7 +3674,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3241
3674
|
}
|
|
3242
3675
|
const home = os2.homedir();
|
|
3243
3676
|
if (home) {
|
|
3244
|
-
const homeDot =
|
|
3677
|
+
const homeDot = path4.join(home, ".opencode");
|
|
3245
3678
|
if (dirExists(homeDot)) push(homeDot);
|
|
3246
3679
|
}
|
|
3247
3680
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -3366,7 +3799,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3366
3799
|
const projectDirs = [];
|
|
3367
3800
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3368
3801
|
for (const f of projectFiles) {
|
|
3369
|
-
const d =
|
|
3802
|
+
const d = path4.dirname(f);
|
|
3370
3803
|
if (!seenProjectDirs.has(d)) {
|
|
3371
3804
|
seenProjectDirs.add(d);
|
|
3372
3805
|
projectDirs.push(d);
|
|
@@ -3412,7 +3845,7 @@ function finishBridge(input) {
|
|
|
3412
3845
|
}
|
|
3413
3846
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3414
3847
|
const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3415
|
-
const outPath =
|
|
3848
|
+
const outPath = path4.join(
|
|
3416
3849
|
pluginTmpDir(),
|
|
3417
3850
|
`mcp-${bodyDigest}.json`
|
|
3418
3851
|
);
|
|
@@ -3494,6 +3927,24 @@ async function fetchSessionDirectory(sessionID) {
|
|
|
3494
3927
|
return void 0;
|
|
3495
3928
|
}
|
|
3496
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
|
+
}
|
|
3497
3948
|
async function getRuntimeMcpStatus() {
|
|
3498
3949
|
const client = opencodeClient;
|
|
3499
3950
|
if (!client?.mcp?.status) return void 0;
|
|
@@ -3551,8 +4002,8 @@ var cachedPluginVersion;
|
|
|
3551
4002
|
function pluginVersion() {
|
|
3552
4003
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
3553
4004
|
try {
|
|
3554
|
-
const here =
|
|
3555
|
-
const raw = fs4.readFileSync(
|
|
4005
|
+
const here = path5.dirname(fileURLToPath(import.meta.url));
|
|
4006
|
+
const raw = fs4.readFileSync(path5.join(here, "..", "package.json"), "utf8");
|
|
3556
4007
|
const version = JSON.parse(raw).version;
|
|
3557
4008
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
3558
4009
|
} catch {
|
|
@@ -3576,7 +4027,7 @@ var opencodeVersionProbe;
|
|
|
3576
4027
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
3577
4028
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
3578
4029
|
opencodeVersionProbe = (async () => {
|
|
3579
|
-
if (!
|
|
4030
|
+
if (!path5.basename(execPath).toLowerCase().includes("opencode")) {
|
|
3580
4031
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
3581
4032
|
return void 0;
|
|
3582
4033
|
}
|
|
@@ -3970,7 +4421,9 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
3970
4421
|
COMPACT_BOUNDARY_MARKER,
|
|
3971
4422
|
RATE_LIMIT_MARKER,
|
|
3972
4423
|
RESULT_ERROR_MARKER,
|
|
3973
|
-
DOCTOR_MARKER
|
|
4424
|
+
DOCTOR_MARKER,
|
|
4425
|
+
STREAM_TIMEOUT_MARKER,
|
|
4426
|
+
FAILOVER_MARKER
|
|
3974
4427
|
];
|
|
3975
4428
|
function isPluginNote(part) {
|
|
3976
4429
|
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
@@ -3983,6 +4436,7 @@ function stripPluginNotes(content) {
|
|
|
3983
4436
|
return kept.length === content.length ? content : kept;
|
|
3984
4437
|
}
|
|
3985
4438
|
function filterSideQuestionHistory(prompt) {
|
|
4439
|
+
prompt = stripAccountFailoverParts(prompt);
|
|
3986
4440
|
let pluginCommand = false;
|
|
3987
4441
|
const kept = prompt.filter((message) => {
|
|
3988
4442
|
if (message.role === "user") {
|
|
@@ -4355,7 +4809,7 @@ Now continuing with the current message:
|
|
|
4355
4809
|
|
|
4356
4810
|
// src/agent-models.ts
|
|
4357
4811
|
import { readFile, readdir } from "fs/promises";
|
|
4358
|
-
import
|
|
4812
|
+
import path6 from "path";
|
|
4359
4813
|
|
|
4360
4814
|
// src/models.ts
|
|
4361
4815
|
var PROVIDER_ID = "claude-code";
|
|
@@ -4402,6 +4856,8 @@ var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
|
|
|
4402
4856
|
var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
4403
4857
|
var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
|
|
4404
4858
|
var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
|
|
4859
|
+
var opus55Cost = { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 };
|
|
4860
|
+
var opus55FastCost = { input: 8, output: 40, cacheRead: 0.4, cacheWrite: 10 };
|
|
4405
4861
|
function toConfigModel(model) {
|
|
4406
4862
|
const inputMods = [];
|
|
4407
4863
|
const outputMods = [];
|
|
@@ -4528,10 +4984,12 @@ var defaultModels = {
|
|
|
4528
4984
|
// `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
|
|
4529
4985
|
// (`claude-opus-4-6-fast`) are a different thing and are not registered here.
|
|
4530
4986
|
//
|
|
4531
|
-
// Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on
|
|
4532
|
-
// model name containing `opus-4-8` or `opus-5
|
|
4533
|
-
//
|
|
4534
|
-
//
|
|
4987
|
+
// Only Opus 4.8, Opus 5 and Opus 5.5 qualify: the CLI gates fast mode on
|
|
4988
|
+
// the resolved model name containing `opus-4-8` or `opus-5` (which
|
|
4989
|
+
// `claude-opus-5-5` satisfies too, and 2.1.280's model catalog lists
|
|
4990
|
+
// `fast_mode` for it outright), so registering a fast entry for any other
|
|
4991
|
+
// model would produce a picker option that silently runs at standard speed
|
|
4992
|
+
// while displaying the fast price.
|
|
4535
4993
|
"claude-opus-4-8-fast": defineModel({
|
|
4536
4994
|
id: "claude-opus-4-8-fast",
|
|
4537
4995
|
name: "Claude Opus 4.8 Fast",
|
|
@@ -4565,6 +5023,33 @@ var defaultModels = {
|
|
|
4565
5023
|
multiplier: 10,
|
|
4566
5024
|
releaseDate: "2026-07-24"
|
|
4567
5025
|
}),
|
|
5026
|
+
// Opus 5.5 needs Claude Code 2.1.280+: the API rejects it from an older CLI
|
|
5027
|
+
// with a 400 naming that floor, which the plugin surfaces as a turn error.
|
|
5028
|
+
// Thinking cannot be switched off for it (the CLI's catalog marks it
|
|
5029
|
+
// `rejects_disabled_thinking`); the plugin never asks for that, so nothing
|
|
5030
|
+
// here has to special-case it.
|
|
5031
|
+
"claude-opus-5-5": defineModel({
|
|
5032
|
+
id: "claude-opus-5-5",
|
|
5033
|
+
name: "Claude Opus 5.5",
|
|
5034
|
+
family: "opus",
|
|
5035
|
+
reasoning: true,
|
|
5036
|
+
context: 1e6,
|
|
5037
|
+
output: 128e3,
|
|
5038
|
+
cost: opus55Cost,
|
|
5039
|
+
multiplier: 4,
|
|
5040
|
+
releaseDate: "2026-09-22"
|
|
5041
|
+
}),
|
|
5042
|
+
"claude-opus-5-5-fast": defineModel({
|
|
5043
|
+
id: "claude-opus-5-5-fast",
|
|
5044
|
+
name: "Claude Opus 5.5 Fast",
|
|
5045
|
+
family: "opus",
|
|
5046
|
+
reasoning: true,
|
|
5047
|
+
context: 1e6,
|
|
5048
|
+
output: 128e3,
|
|
5049
|
+
cost: opus55FastCost,
|
|
5050
|
+
multiplier: 8,
|
|
5051
|
+
releaseDate: "2026-09-22"
|
|
5052
|
+
}),
|
|
4568
5053
|
"claude-fable-5": defineModel({
|
|
4569
5054
|
id: "claude-fable-5",
|
|
4570
5055
|
name: "Claude Fable 5",
|
|
@@ -4733,11 +5218,11 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
4733
5218
|
const name = entry.slice(0, -3);
|
|
4734
5219
|
if (records[name]) continue;
|
|
4735
5220
|
try {
|
|
4736
|
-
const text = await readFile(
|
|
5221
|
+
const text = await readFile(path6.join(directory, entry), "utf8");
|
|
4737
5222
|
records[name] = parseAgentFrontmatter(text);
|
|
4738
5223
|
} catch (err) {
|
|
4739
5224
|
log.debug("failed to read agent markdown", {
|
|
4740
|
-
file:
|
|
5225
|
+
file: path6.join(directory, entry),
|
|
4741
5226
|
error: String(err)
|
|
4742
5227
|
});
|
|
4743
5228
|
}
|
|
@@ -4749,12 +5234,12 @@ function agentDirectories(home, projectDirectory) {
|
|
|
4749
5234
|
const directories = [];
|
|
4750
5235
|
if (projectDirectory) {
|
|
4751
5236
|
for (const name of AGENT_DIR_NAMES) {
|
|
4752
|
-
directories.push(
|
|
5237
|
+
directories.push(path6.join(projectDirectory, ".opencode", name));
|
|
4753
5238
|
}
|
|
4754
5239
|
}
|
|
4755
5240
|
if (home) {
|
|
4756
5241
|
for (const name of AGENT_DIR_NAMES) {
|
|
4757
|
-
directories.push(
|
|
5242
|
+
directories.push(path6.join(home, ".config", "opencode", name));
|
|
4758
5243
|
}
|
|
4759
5244
|
}
|
|
4760
5245
|
return directories;
|
|
@@ -4764,13 +5249,13 @@ function agentDirectories(home, projectDirectory) {
|
|
|
4764
5249
|
import * as crypto3 from "crypto";
|
|
4765
5250
|
import * as fs5 from "fs";
|
|
4766
5251
|
import * as os3 from "os";
|
|
4767
|
-
import * as
|
|
5252
|
+
import * as path7 from "path";
|
|
4768
5253
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4769
5254
|
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
4770
5255
|
function bundledSkillsDir() {
|
|
4771
5256
|
try {
|
|
4772
5257
|
const here = fileURLToPath2(import.meta.url);
|
|
4773
|
-
const dir =
|
|
5258
|
+
const dir = path7.resolve(path7.dirname(here), "..", "skills");
|
|
4774
5259
|
return dirExists2(dir) ? dir : null;
|
|
4775
5260
|
} catch {
|
|
4776
5261
|
return null;
|
|
@@ -4794,24 +5279,24 @@ function skillRoots(cwd) {
|
|
|
4794
5279
|
const roots = [];
|
|
4795
5280
|
const seen = /* @__PURE__ */ new Set();
|
|
4796
5281
|
const push = (p) => {
|
|
4797
|
-
const abs =
|
|
5282
|
+
const abs = path7.resolve(p);
|
|
4798
5283
|
if (seen.has(abs)) return;
|
|
4799
5284
|
seen.add(abs);
|
|
4800
5285
|
if (dirExists2(abs)) roots.push(abs);
|
|
4801
5286
|
};
|
|
4802
|
-
let current =
|
|
5287
|
+
let current = path7.resolve(cwd);
|
|
4803
5288
|
while (true) {
|
|
4804
|
-
push(
|
|
4805
|
-
const parent =
|
|
5289
|
+
push(path7.join(current, ".opencode", "skills"));
|
|
5290
|
+
const parent = path7.dirname(current);
|
|
4806
5291
|
if (parent === current) break;
|
|
4807
5292
|
current = parent;
|
|
4808
5293
|
}
|
|
4809
5294
|
const home = os3.homedir();
|
|
4810
|
-
if (home) push(
|
|
5295
|
+
if (home) push(path7.join(home, ".opencode", "skills"));
|
|
4811
5296
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
4812
|
-
if (envDir) push(
|
|
4813
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (home ?
|
|
4814
|
-
if (xdg) push(
|
|
5297
|
+
if (envDir) push(path7.join(envDir, "skills"));
|
|
5298
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path7.join(home, ".config") : null);
|
|
5299
|
+
if (xdg) push(path7.join(xdg, "opencode", "skills"));
|
|
4815
5300
|
return roots;
|
|
4816
5301
|
}
|
|
4817
5302
|
function collectSkills(root, claimed, found) {
|
|
@@ -4826,8 +5311,8 @@ function collectSkills(root, claimed, found) {
|
|
|
4826
5311
|
const name = entry.name;
|
|
4827
5312
|
if (name.startsWith(".")) continue;
|
|
4828
5313
|
if (claimed.has(name)) continue;
|
|
4829
|
-
const dir =
|
|
4830
|
-
if (!fileExists2(
|
|
5314
|
+
const dir = path7.join(root, name);
|
|
5315
|
+
if (!fileExists2(path7.join(dir, "SKILL.md"))) continue;
|
|
4831
5316
|
claimed.add(name);
|
|
4832
5317
|
found.push({ name, dir });
|
|
4833
5318
|
}
|
|
@@ -4851,7 +5336,7 @@ function registerBundledSkillPath(config) {
|
|
|
4851
5336
|
if (!dir) return false;
|
|
4852
5337
|
config.skills ??= {};
|
|
4853
5338
|
const paths = config.skills.paths ??= [];
|
|
4854
|
-
if (paths.some((p) =>
|
|
5339
|
+
if (paths.some((p) => path7.resolve(p) === dir)) return false;
|
|
4855
5340
|
paths.push(dir);
|
|
4856
5341
|
return true;
|
|
4857
5342
|
}
|
|
@@ -4867,13 +5352,13 @@ function buildSkillPluginDir(skills) {
|
|
|
4867
5352
|
if (skills.length === 0) return null;
|
|
4868
5353
|
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
4869
5354
|
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
4870
|
-
const root =
|
|
4871
|
-
const manifest =
|
|
5355
|
+
const root = path7.join(pluginTmpDir(), `skills-${hash}`);
|
|
5356
|
+
const manifest = path7.join(root, ".claude-plugin", "plugin.json");
|
|
4872
5357
|
if (fileExists2(manifest)) return root;
|
|
4873
5358
|
try {
|
|
4874
5359
|
fs5.rmSync(root, { recursive: true, force: true });
|
|
4875
|
-
fs5.mkdirSync(
|
|
4876
|
-
fs5.mkdirSync(
|
|
5360
|
+
fs5.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
|
|
5361
|
+
fs5.mkdirSync(path7.join(root, "skills"), { recursive: true });
|
|
4877
5362
|
fs5.writeFileSync(
|
|
4878
5363
|
manifest,
|
|
4879
5364
|
JSON.stringify(
|
|
@@ -4887,7 +5372,7 @@ function buildSkillPluginDir(skills) {
|
|
|
4887
5372
|
{ encoding: "utf8", mode: 384 }
|
|
4888
5373
|
);
|
|
4889
5374
|
for (const skill of skills) {
|
|
4890
|
-
linkSkill(skill.dir,
|
|
5375
|
+
linkSkill(skill.dir, path7.join(root, "skills", skill.name));
|
|
4891
5376
|
}
|
|
4892
5377
|
} catch (err) {
|
|
4893
5378
|
log.warn("failed to stage opencode skill plugin dir", {
|
|
@@ -4930,11 +5415,11 @@ import { unlink as unlink2 } from "fs/promises";
|
|
|
4930
5415
|
// src/claude-session-bun.ts
|
|
4931
5416
|
import * as os4 from "os";
|
|
4932
5417
|
import * as fs6 from "fs";
|
|
4933
|
-
import * as
|
|
5418
|
+
import * as path8 from "path";
|
|
4934
5419
|
import { execFileSync } from "child_process";
|
|
4935
5420
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
4936
5421
|
function resolveClaude(cmd = "claude") {
|
|
4937
|
-
if (
|
|
5422
|
+
if (path8.isAbsolute(cmd) && fs6.existsSync(cmd)) return cmd;
|
|
4938
5423
|
const viaBun = Bun.which(cmd);
|
|
4939
5424
|
if (viaBun) return viaBun;
|
|
4940
5425
|
const isWin = os4.platform() === "win32";
|
|
@@ -4950,7 +5435,7 @@ function resolveClaude(cmd = "claude") {
|
|
|
4950
5435
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
4951
5436
|
}
|
|
4952
5437
|
function encodeCwd(cwd) {
|
|
4953
|
-
return
|
|
5438
|
+
return path8.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
4954
5439
|
}
|
|
4955
5440
|
function interactiveSpawnEnv(opts) {
|
|
4956
5441
|
return {
|
|
@@ -4969,12 +5454,12 @@ var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_t
|
|
|
4969
5454
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4970
5455
|
function resolveConfigDir(configDir) {
|
|
4971
5456
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
4972
|
-
if (!value) return
|
|
5457
|
+
if (!value) return path8.join(os4.homedir(), ".claude");
|
|
4973
5458
|
if (value === "~") return os4.homedir();
|
|
4974
5459
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
4975
|
-
return
|
|
5460
|
+
return path8.join(os4.homedir(), value.slice(2));
|
|
4976
5461
|
}
|
|
4977
|
-
return
|
|
5462
|
+
return path8.resolve(value);
|
|
4978
5463
|
}
|
|
4979
5464
|
var ClaudeSession = class {
|
|
4980
5465
|
sessionId;
|
|
@@ -4992,11 +5477,11 @@ var ClaudeSession = class {
|
|
|
4992
5477
|
signal;
|
|
4993
5478
|
o;
|
|
4994
5479
|
constructor(opts = {}) {
|
|
4995
|
-
this.cwd =
|
|
5480
|
+
this.cwd = path8.resolve(opts.cwd ?? process.cwd());
|
|
4996
5481
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
4997
5482
|
this.signal = opts.signal;
|
|
4998
5483
|
this.sessionId = randomUUID4();
|
|
4999
|
-
this.jsonlPath =
|
|
5484
|
+
this.jsonlPath = path8.join(
|
|
5000
5485
|
this.configDir,
|
|
5001
5486
|
"projects",
|
|
5002
5487
|
encodeCwd(this.cwd),
|
|
@@ -5884,14 +6369,14 @@ function reportFastModeState(msg, requested) {
|
|
|
5884
6369
|
const reason = msg.fast_mode_disabled_reason;
|
|
5885
6370
|
if (state === "cooldown") {
|
|
5886
6371
|
log.notice(
|
|
5887
|
-
"fast mode is in cooldown after a rate limit; this turn runs at standard speed and is billed at standard Opus rates, not the
|
|
6372
|
+
"fast mode is in cooldown after a rate limit; this turn runs at standard speed and is billed at standard Opus rates, not the fast price shown in the model picker.",
|
|
5888
6373
|
{ state, reason: reason ?? null }
|
|
5889
6374
|
);
|
|
5890
6375
|
return;
|
|
5891
6376
|
}
|
|
5892
6377
|
const key = reason ?? "unknown";
|
|
5893
6378
|
const explanation = reason ? FAST_MODE_REASONS[reason] : void 0;
|
|
5894
|
-
const message = `fast mode was requested but is off${explanation ? `: ${explanation}` : reason ? ` (${reason})` : ""}. Turns run at standard speed and are billed at standard Opus rates, not the
|
|
6379
|
+
const message = `fast mode was requested but is off${explanation ? `: ${explanation}` : reason ? ` (${reason})` : ""}. Turns run at standard speed and are billed at standard Opus rates, not the fast price shown in the model picker. Switch to the non-fast model id to make the picker's price accurate.`;
|
|
5895
6380
|
if (warnedFastModeReasons.has(key)) {
|
|
5896
6381
|
log.debug(message, { state, reason: reason ?? null });
|
|
5897
6382
|
return;
|
|
@@ -6580,9 +7065,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6580
7065
|
cliToolCallIds: /* @__PURE__ */ new Set(),
|
|
6581
7066
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
6582
7067
|
});
|
|
7068
|
+
const failover = await resolveFailoverSpawn({
|
|
7069
|
+
account: this.config.account ?? DEFAULT_ACCOUNT,
|
|
7070
|
+
baseCliPath: this.config.baseCliPath ?? this.config.cliPath,
|
|
7071
|
+
cliPath: this.config.cliPath,
|
|
7072
|
+
modelId: effectiveModelId
|
|
7073
|
+
});
|
|
7074
|
+
const cliPath = failover.cliPath;
|
|
6583
7075
|
const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
|
|
6584
7076
|
getRuntimeMcpStatus(),
|
|
6585
|
-
detectCliVersion(
|
|
7077
|
+
detectCliVersion(cliPath),
|
|
6586
7078
|
this.resolvePlanModeQuestion(compactionMode)
|
|
6587
7079
|
]);
|
|
6588
7080
|
const systemPromptFile = buildAppendedSystemPrompt(
|
|
@@ -6593,10 +7085,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6593
7085
|
// An existing summary still carries: it is this key's prior context.
|
|
6594
7086
|
{ compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
|
|
6595
7087
|
);
|
|
6596
|
-
const { model: spawnModelId, fast: fastMode } = parseModelId(
|
|
7088
|
+
const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
|
|
6597
7089
|
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6598
7090
|
cwd,
|
|
6599
|
-
cliPath
|
|
7091
|
+
cliPath,
|
|
6600
7092
|
enabled: this.config.bridgeOpencodeSkills === true
|
|
6601
7093
|
});
|
|
6602
7094
|
const cliArgs = buildCliArgs({
|
|
@@ -6623,7 +7115,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6623
7115
|
});
|
|
6624
7116
|
const { spawn: spawn2 } = await import("child_process");
|
|
6625
7117
|
const { createInterface: createInterface2 } = await import("readline");
|
|
6626
|
-
const proc = spawn2(
|
|
7118
|
+
const proc = spawn2(cliPath, cliArgs, {
|
|
6627
7119
|
cwd,
|
|
6628
7120
|
stdio: ["pipe", "pipe", "pipe"],
|
|
6629
7121
|
env: claudeSpawnEnv({
|
|
@@ -6918,7 +7410,6 @@ ${plan}
|
|
|
6918
7410
|
}
|
|
6919
7411
|
async doStream(options) {
|
|
6920
7412
|
const warnings = [];
|
|
6921
|
-
const cliPath = this.config.cliPath;
|
|
6922
7413
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
6923
7414
|
const scope = this.requestScope(options);
|
|
6924
7415
|
const affinity = this.sessionAffinity(options);
|
|
@@ -6928,7 +7419,6 @@ ${plan}
|
|
|
6928
7419
|
this.getOpencodeAgent(options.providerOptions),
|
|
6929
7420
|
this.modelId
|
|
6930
7421
|
);
|
|
6931
|
-
const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
|
|
6932
7422
|
const reasoningEffort = compactionMode ? void 0 : resolveAgentEffort(
|
|
6933
7423
|
this.getOpencodeAgent(options.providerOptions),
|
|
6934
7424
|
this.getReasoningEffort(options.providerOptions)
|
|
@@ -6945,6 +7435,17 @@ ${plan}
|
|
|
6945
7435
|
const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
|
|
6946
7436
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
6947
7437
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
7438
|
+
const sourceAccount = normalizeAccountName(
|
|
7439
|
+
this.config.account ?? DEFAULT_ACCOUNT
|
|
7440
|
+
);
|
|
7441
|
+
const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
|
|
7442
|
+
let failover = useInteractive || compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
|
|
7443
|
+
account: sourceAccount,
|
|
7444
|
+
baseCliPath,
|
|
7445
|
+
cliPath: this.config.cliPath,
|
|
7446
|
+
modelId: effectiveModelId
|
|
7447
|
+
});
|
|
7448
|
+
let cliPath = failover.cliPath;
|
|
6948
7449
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
6949
7450
|
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
6950
7451
|
if (doctor) {
|
|
@@ -7087,15 +7588,90 @@ ${plan}
|
|
|
7087
7588
|
deleteActiveProcess(sk);
|
|
7088
7589
|
clearCompression(sk);
|
|
7089
7590
|
}
|
|
7591
|
+
const failoverAnswer = compactionMode || useInteractive ? null : consumeAccountFailoverAnswer(sk, options.prompt);
|
|
7592
|
+
if (failoverAnswer?.kind === "stop") {
|
|
7593
|
+
log.warn("account failover declined; ending the turn", {
|
|
7594
|
+
sessionKey: sk,
|
|
7595
|
+
account: sourceAccount,
|
|
7596
|
+
reason: failoverAnswer.reason
|
|
7597
|
+
});
|
|
7598
|
+
const note = formatFailoverStopNote(failoverAnswer.reason);
|
|
7599
|
+
const stream2 = new ReadableStream({
|
|
7600
|
+
start(controller) {
|
|
7601
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
7602
|
+
const id = generateId();
|
|
7603
|
+
controller.enqueue({ type: "text-start", id });
|
|
7604
|
+
controller.enqueue({ type: "text-delta", id, delta: note });
|
|
7605
|
+
controller.enqueue({ type: "text-end", id });
|
|
7606
|
+
controller.enqueue({
|
|
7607
|
+
type: "error",
|
|
7608
|
+
error: new Error(
|
|
7609
|
+
`Claude account "${sourceAccount}" is out of usage and no other account was picked.`
|
|
7610
|
+
)
|
|
7611
|
+
});
|
|
7612
|
+
controller.enqueue({
|
|
7613
|
+
type: "finish",
|
|
7614
|
+
finishReason: { unified: "error", raw: "account_limit" },
|
|
7615
|
+
usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
7616
|
+
providerMetadata: {
|
|
7617
|
+
"claude-code": {
|
|
7618
|
+
path: "account-failover-stop",
|
|
7619
|
+
synthetic: true,
|
|
7620
|
+
usageUnavailable: true
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
});
|
|
7624
|
+
controller.close();
|
|
7625
|
+
}
|
|
7626
|
+
});
|
|
7627
|
+
return { stream: stream2, request: { body: { text: "" } } };
|
|
7628
|
+
}
|
|
7629
|
+
let failoverNote = null;
|
|
7630
|
+
if (failoverAnswer?.kind === "switch") {
|
|
7631
|
+
setAccountOverride(
|
|
7632
|
+
failoverAnswer.sourceAccount,
|
|
7633
|
+
failoverAnswer.target,
|
|
7634
|
+
failoverUntil(failoverAnswer.resetsAt)
|
|
7635
|
+
);
|
|
7636
|
+
failover = await resolveFailoverSpawn({
|
|
7637
|
+
account: sourceAccount,
|
|
7638
|
+
baseCliPath,
|
|
7639
|
+
cliPath: this.config.cliPath,
|
|
7640
|
+
modelId: effectiveModelId
|
|
7641
|
+
});
|
|
7642
|
+
cliPath = failover.cliPath;
|
|
7643
|
+
asideTransportRef.cliPath = cliPath;
|
|
7644
|
+
failoverNote = formatFailoverNote({
|
|
7645
|
+
sourceAccount: failoverAnswer.sourceAccount,
|
|
7646
|
+
target: failoverAnswer.target,
|
|
7647
|
+
resetsAt: failoverAnswer.resetsAt
|
|
7648
|
+
});
|
|
7649
|
+
}
|
|
7650
|
+
const processForAccount = getActiveProcess(sk);
|
|
7651
|
+
if (!compactionMode && !useInteractive && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
|
|
7652
|
+
log.notice("claude process belongs to another account; starting fresh", {
|
|
7653
|
+
sessionKey: sk,
|
|
7654
|
+
was: processForAccount.cliPath,
|
|
7655
|
+
now: cliPath,
|
|
7656
|
+
failedOver: failover.failedOver
|
|
7657
|
+
});
|
|
7658
|
+
deleteActiveProcess(sk);
|
|
7659
|
+
deleteClaudeSessionId(sk);
|
|
7660
|
+
}
|
|
7090
7661
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
7091
7662
|
const hasActiveProcess = !!getActiveProcess(sk);
|
|
7092
|
-
|
|
7663
|
+
let includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
|
|
7664
|
+
if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
|
|
7665
|
+
includeHistoryContext = true;
|
|
7666
|
+
}
|
|
7667
|
+
const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
|
|
7093
7668
|
const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
|
|
7094
7669
|
if (exitPlanModeQuestionResult) {
|
|
7095
7670
|
log.info("sending plan approval decision to claude", { sk });
|
|
7096
7671
|
}
|
|
7097
7672
|
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
7098
|
-
const
|
|
7673
|
+
const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
|
|
7674
|
+
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
|
|
7099
7675
|
compactionMode,
|
|
7100
7676
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
7101
7677
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
@@ -7116,8 +7692,23 @@ ${plan}
|
|
|
7116
7692
|
);
|
|
7117
7693
|
const [runtimeStatus, cliVersion] = await Promise.all([
|
|
7118
7694
|
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus(),
|
|
7119
|
-
detectCliVersion(
|
|
7695
|
+
detectCliVersion(cliPath)
|
|
7120
7696
|
]);
|
|
7697
|
+
const failoverAccounts = failoverCandidates(
|
|
7698
|
+
this.config.failoverAccounts,
|
|
7699
|
+
sourceAccount
|
|
7700
|
+
);
|
|
7701
|
+
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover !== "off" && !compactionMode && !useInteractive && isAccountFailoverQuestionActive({
|
|
7702
|
+
configured: this.config.accountFailover,
|
|
7703
|
+
candidates: failoverAccounts,
|
|
7704
|
+
opencodeHasQuestion: (await loadLiveToolInfo()).hasQuestion,
|
|
7705
|
+
compactionMode,
|
|
7706
|
+
interactive: !!useInteractive,
|
|
7707
|
+
// A subagent follows its parent's account for free, because the
|
|
7708
|
+
// override is account-scoped. Asking it would put a form in a session
|
|
7709
|
+
// the operator is usually not even looking at.
|
|
7710
|
+
childSession: !!await fetchSessionParentId(affinity)
|
|
7711
|
+
});
|
|
7121
7712
|
log.info("doStream starting", {
|
|
7122
7713
|
cwd,
|
|
7123
7714
|
model: effectiveModelId,
|
|
@@ -7435,6 +8026,14 @@ ${plan}
|
|
|
7435
8026
|
currentTextId = null;
|
|
7436
8027
|
}
|
|
7437
8028
|
};
|
|
8029
|
+
if (failoverNote) {
|
|
8030
|
+
controller.enqueue({
|
|
8031
|
+
type: "text-delta",
|
|
8032
|
+
id: startTextBlock(),
|
|
8033
|
+
delta: failoverNote
|
|
8034
|
+
});
|
|
8035
|
+
endTextBlock();
|
|
8036
|
+
}
|
|
7438
8037
|
const reasoningIds = /* @__PURE__ */ new Map();
|
|
7439
8038
|
const reasoningStarted = /* @__PURE__ */ new Map();
|
|
7440
8039
|
let hadThinkingTextFromStream = false;
|
|
@@ -7469,7 +8068,12 @@ ${plan}
|
|
|
7469
8068
|
resultFallbackTimer = null;
|
|
7470
8069
|
}
|
|
7471
8070
|
};
|
|
7472
|
-
const
|
|
8071
|
+
const RESULT_FALLBACK_MS = (() => {
|
|
8072
|
+
const env = process.env.CLAUDE_CODE_RESULT_FALLBACK_MS;
|
|
8073
|
+
const parsed = env ? Number.parseInt(env, 10) : NaN;
|
|
8074
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
|
|
8075
|
+
})();
|
|
8076
|
+
const startResultFallback = (delayMs = RESULT_FALLBACK_MS) => {
|
|
7473
8077
|
clearFallbackTimer();
|
|
7474
8078
|
if (!hasReceivedContent && !hasReceivedProgress || controllerClosed) return;
|
|
7475
8079
|
resultFallbackTimer = setTimeout(() => {
|
|
@@ -7477,6 +8081,14 @@ ${plan}
|
|
|
7477
8081
|
log.warn("result fallback timer fired \u2014 closing stream without result event", {
|
|
7478
8082
|
delayMs
|
|
7479
8083
|
});
|
|
8084
|
+
if (!autoContinueState.aborted) {
|
|
8085
|
+
controller.enqueue({
|
|
8086
|
+
type: "text-delta",
|
|
8087
|
+
id: startTextBlock(),
|
|
8088
|
+
delta: formatStreamTimeoutNote(delayMs)
|
|
8089
|
+
});
|
|
8090
|
+
endTextBlock();
|
|
8091
|
+
}
|
|
7480
8092
|
closeHandler();
|
|
7481
8093
|
}, delayMs);
|
|
7482
8094
|
};
|
|
@@ -7607,6 +8219,7 @@ ${plan}
|
|
|
7607
8219
|
const toolCallsById = /* @__PURE__ */ new Map();
|
|
7608
8220
|
let resultMeta = {};
|
|
7609
8221
|
let resultFailure;
|
|
8222
|
+
let accountLimitHit = null;
|
|
7610
8223
|
const drainBuffer = [];
|
|
7611
8224
|
let drainTimer = null;
|
|
7612
8225
|
const DRAIN_QUIET_MS = 100;
|
|
@@ -7658,7 +8271,7 @@ ${plan}
|
|
|
7658
8271
|
} catch {
|
|
7659
8272
|
}
|
|
7660
8273
|
};
|
|
7661
|
-
const
|
|
8274
|
+
const finishWithQuestionCall = (call) => {
|
|
7662
8275
|
if (controllerClosed) return;
|
|
7663
8276
|
endTextBlock();
|
|
7664
8277
|
controller.enqueue({
|
|
@@ -7768,6 +8381,25 @@ ${plan}
|
|
|
7768
8381
|
});
|
|
7769
8382
|
}
|
|
7770
8383
|
activeProcess?.pendingProxyCompletions?.clear();
|
|
8384
|
+
if (accountLimitHit && failoverAskActive) {
|
|
8385
|
+
const call = createAccountFailoverQuestionCall(sk, {
|
|
8386
|
+
sourceAccount,
|
|
8387
|
+
candidates: failoverAccounts,
|
|
8388
|
+
resetsAt: accountLimitHit.resetsAt,
|
|
8389
|
+
window: accountLimitHit.window
|
|
8390
|
+
});
|
|
8391
|
+
log.warn(
|
|
8392
|
+
`Claude account "${sourceAccount}" is out of usage; asking which account to continue on.`,
|
|
8393
|
+
{
|
|
8394
|
+
sessionKey: sk,
|
|
8395
|
+
candidates: failoverAccounts,
|
|
8396
|
+
toolCallId: call.toolCallId,
|
|
8397
|
+
resetsAt: accountLimitHit.resetsAt ?? null
|
|
8398
|
+
}
|
|
8399
|
+
);
|
|
8400
|
+
finishWithQuestionCall(call);
|
|
8401
|
+
return;
|
|
8402
|
+
}
|
|
7771
8403
|
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
7772
8404
|
autoContinueState,
|
|
7773
8405
|
{
|
|
@@ -7901,6 +8533,13 @@ ${plan}
|
|
|
7901
8533
|
}
|
|
7902
8534
|
}
|
|
7903
8535
|
if (msg.type === "rate_limit_event") {
|
|
8536
|
+
const info = parseRateLimitEvent(msg);
|
|
8537
|
+
if (info && isRateLimitRejected(info)) {
|
|
8538
|
+
accountLimitHit = {
|
|
8539
|
+
resetsAt: info.resetsAt ?? info.overageResetsAt,
|
|
8540
|
+
window: info.rateLimitType
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
7904
8543
|
const note = reportRateLimitEvent(msg);
|
|
7905
8544
|
if (note) {
|
|
7906
8545
|
controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
|
|
@@ -8064,7 +8703,7 @@ ${plan}
|
|
|
8064
8703
|
id: planId2,
|
|
8065
8704
|
delta: questionCall.text
|
|
8066
8705
|
});
|
|
8067
|
-
|
|
8706
|
+
finishWithQuestionCall(questionCall);
|
|
8068
8707
|
return;
|
|
8069
8708
|
}
|
|
8070
8709
|
const planId = startTextBlock();
|
|
@@ -8245,7 +8884,7 @@ ${plan}
|
|
|
8245
8884
|
id: planId2,
|
|
8246
8885
|
delta: questionCall.text
|
|
8247
8886
|
});
|
|
8248
|
-
|
|
8887
|
+
finishWithQuestionCall(questionCall);
|
|
8249
8888
|
return;
|
|
8250
8889
|
}
|
|
8251
8890
|
const planId = startTextBlock();
|
|
@@ -8417,6 +9056,11 @@ ${plan}
|
|
|
8417
9056
|
delta: msg.result
|
|
8418
9057
|
});
|
|
8419
9058
|
}
|
|
9059
|
+
if (!accountLimitHit && msg.is_error && isAccountLimitError({
|
|
9060
|
+
resultText: typeof msg.result === "string" ? msg.result : null
|
|
9061
|
+
})) {
|
|
9062
|
+
accountLimitHit = {};
|
|
9063
|
+
}
|
|
8420
9064
|
const failure = describeResultFailure(msg);
|
|
8421
9065
|
if (failure) {
|
|
8422
9066
|
resultFailure = msg.subtype;
|
|
@@ -8840,148 +9484,6 @@ ${plan}
|
|
|
8840
9484
|
}
|
|
8841
9485
|
};
|
|
8842
9486
|
|
|
8843
|
-
// src/accounts.ts
|
|
8844
|
-
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
8845
|
-
import path8 from "path";
|
|
8846
|
-
var BASE_PROVIDER_ID = "claude-code";
|
|
8847
|
-
var DEFAULT_ACCOUNT = "default";
|
|
8848
|
-
var SHARED_CAPABILITY_ITEMS = [
|
|
8849
|
-
"CLAUDE.md",
|
|
8850
|
-
"settings.json",
|
|
8851
|
-
"skills",
|
|
8852
|
-
"agents",
|
|
8853
|
-
"commands",
|
|
8854
|
-
"plugins"
|
|
8855
|
-
];
|
|
8856
|
-
function normalizeAccountName(account) {
|
|
8857
|
-
return account.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8858
|
-
}
|
|
8859
|
-
function resolveAccounts(value) {
|
|
8860
|
-
if (!Array.isArray(value)) return null;
|
|
8861
|
-
const accounts = value.map((account) => normalizeAccountName(String(account))).filter(Boolean);
|
|
8862
|
-
return Array.from(/* @__PURE__ */ new Set([DEFAULT_ACCOUNT, ...accounts]));
|
|
8863
|
-
}
|
|
8864
|
-
function accountProviderId(account) {
|
|
8865
|
-
return `${BASE_PROVIDER_ID}-${normalizeAccountName(account)}`;
|
|
8866
|
-
}
|
|
8867
|
-
function accountDisplayName(account) {
|
|
8868
|
-
return `Claude Code (${titleizeAccount(account)})`;
|
|
8869
|
-
}
|
|
8870
|
-
function accountModelSuffix(account) {
|
|
8871
|
-
const normalized = normalizeAccountName(account);
|
|
8872
|
-
return normalized === DEFAULT_ACCOUNT ? void 0 : normalized;
|
|
8873
|
-
}
|
|
8874
|
-
function accountConfigDir(account) {
|
|
8875
|
-
const normalized = normalizeAccountName(account);
|
|
8876
|
-
if (!normalized || normalized === DEFAULT_ACCOUNT) return void 0;
|
|
8877
|
-
return `~/.claude-${normalized}`;
|
|
8878
|
-
}
|
|
8879
|
-
function expandHome(value) {
|
|
8880
|
-
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
8881
|
-
if (value === "~") return home ?? value;
|
|
8882
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
8883
|
-
return home ? path8.join(home, value.slice(2)) : value;
|
|
8884
|
-
}
|
|
8885
|
-
return value;
|
|
8886
|
-
}
|
|
8887
|
-
async function ensureAccountRuntime(account, baseCliPath) {
|
|
8888
|
-
const configDir = accountConfigDir(account);
|
|
8889
|
-
if (!configDir) return { cliPath: baseCliPath };
|
|
8890
|
-
const expandedConfigDir = expandHome(configDir);
|
|
8891
|
-
await mkdir(expandedConfigDir, { recursive: true });
|
|
8892
|
-
try {
|
|
8893
|
-
await ensureSharedCapabilities(expandedConfigDir);
|
|
8894
|
-
} catch (err) {
|
|
8895
|
-
log.warn("failed to symlink shared capabilities; continuing anyway", {
|
|
8896
|
-
account,
|
|
8897
|
-
configDir: expandedConfigDir,
|
|
8898
|
-
error: String(err)
|
|
8899
|
-
});
|
|
8900
|
-
}
|
|
8901
|
-
const cliPath = await writeAccountWrapper(
|
|
8902
|
-
normalizeAccountName(account),
|
|
8903
|
-
baseCliPath,
|
|
8904
|
-
expandedConfigDir
|
|
8905
|
-
);
|
|
8906
|
-
return { cliPath, configDir: expandedConfigDir };
|
|
8907
|
-
}
|
|
8908
|
-
async function ensureSharedCapabilities(targetRoot) {
|
|
8909
|
-
const sourceRoot = expandHome("~/.claude");
|
|
8910
|
-
for (const item of SHARED_CAPABILITY_ITEMS) {
|
|
8911
|
-
await ensureSharedCapabilityItem(sourceRoot, targetRoot, item);
|
|
8912
|
-
}
|
|
8913
|
-
}
|
|
8914
|
-
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
8915
|
-
const source = path8.join(sourceRoot, item);
|
|
8916
|
-
const target = path8.join(targetRoot, item);
|
|
8917
|
-
let sourceStat;
|
|
8918
|
-
try {
|
|
8919
|
-
sourceStat = await lstat(source);
|
|
8920
|
-
} catch {
|
|
8921
|
-
return;
|
|
8922
|
-
}
|
|
8923
|
-
try {
|
|
8924
|
-
const targetStat = await lstat(target);
|
|
8925
|
-
if (targetStat.isSymbolicLink()) {
|
|
8926
|
-
const current = await readlink(target);
|
|
8927
|
-
const resolvedCurrent = path8.resolve(path8.dirname(target), current);
|
|
8928
|
-
const resolvedSource = path8.resolve(source);
|
|
8929
|
-
if (resolvedCurrent === resolvedSource) return;
|
|
8930
|
-
}
|
|
8931
|
-
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
8932
|
-
item,
|
|
8933
|
-
target,
|
|
8934
|
-
source
|
|
8935
|
-
});
|
|
8936
|
-
return;
|
|
8937
|
-
} catch {
|
|
8938
|
-
}
|
|
8939
|
-
const type = sourceStat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
|
|
8940
|
-
await symlink(source, target, type);
|
|
8941
|
-
}
|
|
8942
|
-
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
8943
|
-
const cacheRoot = path8.join(
|
|
8944
|
-
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
8945
|
-
"opencode-claude-code-plugin"
|
|
8946
|
-
);
|
|
8947
|
-
const wrapperPath = path8.join(cacheRoot, `claude-${account}`);
|
|
8948
|
-
const suffix = `@${account}`;
|
|
8949
|
-
await mkdir(cacheRoot, { recursive: true });
|
|
8950
|
-
const script = `#!/usr/bin/env bash
|
|
8951
|
-
set -euo pipefail
|
|
8952
|
-
|
|
8953
|
-
args=()
|
|
8954
|
-
while [[ $# -gt 0 ]]; do
|
|
8955
|
-
if [[ "$1" == "--model" && $# -ge 2 ]]; then
|
|
8956
|
-
model="$2"
|
|
8957
|
-
if [[ "$model" == *${shellDoubleQuote(suffix)} ]]; then
|
|
8958
|
-
model="\${model%${shellDoubleQuote(suffix)}}"
|
|
8959
|
-
fi
|
|
8960
|
-
args+=("$1" "$model")
|
|
8961
|
-
shift 2
|
|
8962
|
-
else
|
|
8963
|
-
args+=("$1")
|
|
8964
|
-
shift
|
|
8965
|
-
fi
|
|
8966
|
-
done
|
|
8967
|
-
|
|
8968
|
-
export CLAUDE_CONFIG_DIR=${shellSingleQuote(configDir)}
|
|
8969
|
-
exec ${shellSingleQuote(baseCliPath)} "\${args[@]}"
|
|
8970
|
-
`;
|
|
8971
|
-
await writeFile(wrapperPath, script, "utf8");
|
|
8972
|
-
await chmod(wrapperPath, 493);
|
|
8973
|
-
return wrapperPath;
|
|
8974
|
-
}
|
|
8975
|
-
function shellSingleQuote(value) {
|
|
8976
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
8977
|
-
}
|
|
8978
|
-
function shellDoubleQuote(value) {
|
|
8979
|
-
return value.replace(/[$`"\\]/g, "\\$&");
|
|
8980
|
-
}
|
|
8981
|
-
function titleizeAccount(account) {
|
|
8982
|
-
return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
8983
|
-
}
|
|
8984
|
-
|
|
8985
9487
|
// src/cleanup-stale.ts
|
|
8986
9488
|
import {
|
|
8987
9489
|
existsSync as existsSync4,
|
|
@@ -9176,6 +9678,9 @@ function createClaudeCode(settings = {}) {
|
|
|
9176
9678
|
cwd: settings.cwd,
|
|
9177
9679
|
account: settings.account,
|
|
9178
9680
|
configDir: settings.configDir,
|
|
9681
|
+
failoverAccounts: settings.failoverAccounts,
|
|
9682
|
+
baseCliPath: settings.baseCliPath ?? cliPath,
|
|
9683
|
+
accountFailover: settings.accountFailover ?? "ask",
|
|
9179
9684
|
providerID: settings.providerID,
|
|
9180
9685
|
skipPermissions: settings.skipPermissions ?? true,
|
|
9181
9686
|
permissionMode: settings.permissionMode,
|
|
@@ -9302,7 +9807,11 @@ async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefault
|
|
|
9302
9807
|
npm: existing?.npm ?? pluginEntrypoint(),
|
|
9303
9808
|
options: {
|
|
9304
9809
|
...mergedOptions,
|
|
9305
|
-
...runtime
|
|
9810
|
+
...runtime,
|
|
9811
|
+
// The pre-wrapper binary, kept because `runtime` replaces `cliPath`
|
|
9812
|
+
// with the account's wrapper and a failover has to build ANOTHER
|
|
9813
|
+
// account's wrapper on top of the same base (src/account-failover.ts).
|
|
9814
|
+
baseCliPath: cliPath
|
|
9306
9815
|
}
|
|
9307
9816
|
// models is intentionally omitted: both callers overwrite it with
|
|
9308
9817
|
// configModelsForProvider(), which emits the flat config schema
|
|
@@ -9335,7 +9844,11 @@ async function expandAccountProviders(config) {
|
|
|
9335
9844
|
providerID,
|
|
9336
9845
|
{
|
|
9337
9846
|
...seedOptions,
|
|
9338
|
-
account
|
|
9847
|
+
account,
|
|
9848
|
+
// The resolved list, so this account's language model can offer
|
|
9849
|
+
// the others when it runs out of usage. `accounts` itself stays
|
|
9850
|
+
// stripped by cleanProviderOptions.
|
|
9851
|
+
failoverAccounts: accounts
|
|
9339
9852
|
},
|
|
9340
9853
|
accountDisplayName(account)
|
|
9341
9854
|
),
|