@khalilgharbaoui/opencode-claude-code-plugin 0.42.2 → 0.43.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/dist/index.js +248 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +18 -9
package/dist/index.js
CHANGED
|
@@ -1327,7 +1327,7 @@ function formatUsageLimitNote(input) {
|
|
|
1327
1327
|
const what = [
|
|
1328
1328
|
`the Claude account "${account}" is out of usage`,
|
|
1329
1329
|
window ? ` in ${window}` : "",
|
|
1330
|
-
resets ? `, which resets at ${resets}` : "",
|
|
1330
|
+
resets ? `, which resets at ${resets}` : input.resetsText ? `, which resets ${input.resetsText}` : "",
|
|
1331
1331
|
"."
|
|
1332
1332
|
].join("");
|
|
1333
1333
|
const advice = others ? `Pick a model from ${others} and resend your message, or wait for the window to reset.` : "Wait for the window to reset, or enable extra usage on the account.";
|
|
@@ -1341,6 +1341,13 @@ function isAccountLimitError(input) {
|
|
|
1341
1341
|
if (typeof text !== "string" || text.length === 0) return false;
|
|
1342
1342
|
return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
|
|
1343
1343
|
}
|
|
1344
|
+
function limitFactsFromText(text) {
|
|
1345
|
+
const facts = {};
|
|
1346
|
+
if (/\bsession limit\b/i.test(text)) facts.window = "five_hour";
|
|
1347
|
+
const reset = /\bresets\s+([^·\n]{1,60}?)\s*$/im.exec(text);
|
|
1348
|
+
if (reset) facts.resetsText = reset[1].trim();
|
|
1349
|
+
return facts;
|
|
1350
|
+
}
|
|
1344
1351
|
var lastAccountLimits = /* @__PURE__ */ new Map();
|
|
1345
1352
|
function rememberAccountLimit(account, facts) {
|
|
1346
1353
|
const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
@@ -1423,7 +1430,6 @@ async function resolveFailoverSpawn(input) {
|
|
|
1423
1430
|
}
|
|
1424
1431
|
function isAccountFailoverQuestionActive(input) {
|
|
1425
1432
|
if (input.compactionMode) return false;
|
|
1426
|
-
if (input.interactive) return false;
|
|
1427
1433
|
if (input.childSession) return false;
|
|
1428
1434
|
if (input.configured !== "ask") return false;
|
|
1429
1435
|
if (input.candidates.length === 0) return false;
|
|
@@ -1723,6 +1729,10 @@ function cliSupportsDontAsk(v) {
|
|
|
1723
1729
|
if (!v) return false;
|
|
1724
1730
|
return gte(v, { major: 2, minor: 1, patch: 263 });
|
|
1725
1731
|
}
|
|
1732
|
+
function cliSupportsInteractiveBypass(v) {
|
|
1733
|
+
if (!v) return false;
|
|
1734
|
+
return gte(v, { major: 2, minor: 1, patch: 263 });
|
|
1735
|
+
}
|
|
1726
1736
|
var INTERACTIVE_MEASURED_CLI = "2.1.288";
|
|
1727
1737
|
var INTERACTIVE_MEASURED = { major: 2, minor: 1, patch: 288 };
|
|
1728
1738
|
function isUnmeasuredInteractiveCli(v) {
|
|
@@ -4559,18 +4569,36 @@ function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAUL
|
|
|
4559
4569
|
};
|
|
4560
4570
|
}
|
|
4561
4571
|
function interactivePermissionPosture(input) {
|
|
4562
|
-
if (
|
|
4563
|
-
|
|
4572
|
+
if (isReadOnlyPermissionMode(input.permissionMode)) {
|
|
4573
|
+
if (!input.supportsReadOnly) return null;
|
|
4574
|
+
const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
|
|
4575
|
+
return {
|
|
4576
|
+
permissionMode: "dontAsk",
|
|
4577
|
+
restricted: true,
|
|
4578
|
+
allow: input.allow.filter(
|
|
4579
|
+
(rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
|
|
4580
|
+
),
|
|
4581
|
+
bypass: false,
|
|
4582
|
+
disallowed: []
|
|
4583
|
+
};
|
|
4564
4584
|
}
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
(rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
|
|
4572
|
-
)
|
|
4585
|
+
const unchanged = {
|
|
4586
|
+
permissionMode: input.permissionMode === "bypassPermissions" ? void 0 : input.permissionMode,
|
|
4587
|
+
restricted: false,
|
|
4588
|
+
allow: input.allow,
|
|
4589
|
+
bypass: false,
|
|
4590
|
+
disallowed: []
|
|
4573
4591
|
};
|
|
4592
|
+
if (input.permissionMode === "plan" || !input.supportsBypass) return unchanged;
|
|
4593
|
+
const perTool = Object.entries(input.controlRequestToolBehaviors ?? {});
|
|
4594
|
+
const named = (behavior) => perTool.filter(([, value]) => value === behavior).map(([tool]) => tool);
|
|
4595
|
+
if (input.skipPermissions !== false || input.permissionMode === "bypassPermissions") {
|
|
4596
|
+
return { ...unchanged, bypass: true };
|
|
4597
|
+
}
|
|
4598
|
+
if ((input.controlRequestBehavior ?? "allow") === "allow") {
|
|
4599
|
+
return { ...unchanged, bypass: true, disallowed: named("deny") };
|
|
4600
|
+
}
|
|
4601
|
+
return { ...unchanged, permissionMode: "dontAsk", allow: named("allow") };
|
|
4574
4602
|
}
|
|
4575
4603
|
function isReadOnlyPermissionMode(mode) {
|
|
4576
4604
|
return mode === READ_ONLY_PERMISSION_MODE;
|
|
@@ -5825,7 +5853,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5825
5853
|
"interactive transport denied a permission prompt",
|
|
5826
5854
|
"interactive transport dropped non-text content blocks",
|
|
5827
5855
|
"interactive turn failed",
|
|
5828
|
-
"interactiveBypass ignored:
|
|
5856
|
+
"interactiveBypass is ignored: skipPermissions governs the interactive transport too",
|
|
5829
5857
|
"malformed data URI, skipping file part",
|
|
5830
5858
|
"no active process to respawn (start watchdog); ending turn",
|
|
5831
5859
|
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
@@ -5862,6 +5890,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5862
5890
|
"stale scratch directory sweep failed",
|
|
5863
5891
|
"task_batch resolving with child results missing",
|
|
5864
5892
|
"the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
|
|
5893
|
+
"the interactive turn did not take a forwarded user message",
|
|
5865
5894
|
"this opencode process is running an older plugin build than the one on disk",
|
|
5866
5895
|
"timed out pending proxy call",
|
|
5867
5896
|
"turn abort listener threw",
|
|
@@ -7541,6 +7570,20 @@ function parseModelId(modelId) {
|
|
|
7541
7570
|
if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
|
|
7542
7571
|
return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
|
|
7543
7572
|
}
|
|
7573
|
+
var WEB_SEARCH_USD = 0.01;
|
|
7574
|
+
function apiCallCostUsd(model, usage) {
|
|
7575
|
+
if (typeof model !== "string" || !usage || typeof usage !== "object") return null;
|
|
7576
|
+
const base = model.replace(/@.*$/, "").replace(/\[1m\]$/i, "").replace(/-\d{8}$/, "");
|
|
7577
|
+
const entry = (usage.speed === "fast" ? defaultModels[`${base}${FAST_SUFFIX}`] : void 0) ?? defaultModels[base];
|
|
7578
|
+
if (!entry) return null;
|
|
7579
|
+
const cost = entry.cost;
|
|
7580
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
7581
|
+
const split = usage.cache_creation;
|
|
7582
|
+
const write1h = split ? count(split.ephemeral_1h_input_tokens) : 0;
|
|
7583
|
+
const write5m = split ? count(split.ephemeral_5m_input_tokens) : count(usage.cache_creation_input_tokens);
|
|
7584
|
+
const perMillion = count(usage.input_tokens) * cost.input + count(usage.output_tokens) * cost.output + count(usage.cache_read_input_tokens) * cost.cache.read + write5m * cost.cache.write + write1h * cost.input * 2;
|
|
7585
|
+
return perMillion / 1e6 + count(usage.server_tool_use?.web_search_requests) * WEB_SEARCH_USD;
|
|
7586
|
+
}
|
|
7544
7587
|
|
|
7545
7588
|
// src/agent-models.ts
|
|
7546
7589
|
var AGENT_DIR_NAMES = ["agents", "agent"];
|
|
@@ -8860,6 +8903,7 @@ function isApiCallRecord(rec) {
|
|
|
8860
8903
|
}
|
|
8861
8904
|
var TurnUsageAccumulator = class {
|
|
8862
8905
|
calls = /* @__PURE__ */ new Map();
|
|
8906
|
+
models = /* @__PURE__ */ new Map();
|
|
8863
8907
|
lastKey = null;
|
|
8864
8908
|
/** Feed every transcript record of the turn, in file order. */
|
|
8865
8909
|
add(rec) {
|
|
@@ -8867,8 +8911,24 @@ var TurnUsageAccumulator = class {
|
|
|
8867
8911
|
const id = rec.message.id;
|
|
8868
8912
|
const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
|
|
8869
8913
|
this.calls.set(key, rec.message.usage);
|
|
8914
|
+
this.models.set(key, rec.message.model);
|
|
8870
8915
|
this.lastKey = key;
|
|
8871
8916
|
}
|
|
8917
|
+
/**
|
|
8918
|
+
* The turn's list-price cost over its distinct calls, the headless
|
|
8919
|
+
* `result`'s `total_cost_usd` (h #g208). Null when no call was made or any
|
|
8920
|
+
* call's model is one the catalog cannot price, never a partial sum.
|
|
8921
|
+
*/
|
|
8922
|
+
get costUsd() {
|
|
8923
|
+
if (this.calls.size === 0) return null;
|
|
8924
|
+
let total = 0;
|
|
8925
|
+
for (const [key, usage] of this.calls) {
|
|
8926
|
+
const cost = apiCallCostUsd(this.models.get(key), usage);
|
|
8927
|
+
if (cost === null) return null;
|
|
8928
|
+
total += cost;
|
|
8929
|
+
}
|
|
8930
|
+
return total;
|
|
8931
|
+
}
|
|
8872
8932
|
/** Distinct API calls seen, for diagnostics. */
|
|
8873
8933
|
get callCount() {
|
|
8874
8934
|
return this.calls.size;
|
|
@@ -8999,6 +9059,9 @@ var ClaudeSession = class {
|
|
|
8999
9059
|
* (the TUI repaints it while it takes the key, measured on 2.1.288) can
|
|
9000
9060
|
* never stand in for the next call's dialog. */
|
|
9001
9061
|
planDialog = null;
|
|
9062
|
+
/** `queue-operation` enqueue records read, which is how `queueInput` knows
|
|
9063
|
+
* the TUI took a message into the running turn. */
|
|
9064
|
+
enqueued = 0;
|
|
9002
9065
|
/** This turn's `ExitPlanMode` calls already answered. */
|
|
9003
9066
|
answeredPlans = /* @__PURE__ */ new Set();
|
|
9004
9067
|
abandonTurn = false;
|
|
@@ -9057,7 +9120,9 @@ var ClaudeSession = class {
|
|
|
9057
9120
|
// The decision arrives after opencode's own question round trip, by
|
|
9058
9121
|
// which time the dialog has long been drawn; this only bounds a TUI
|
|
9059
9122
|
// that never draws it.
|
|
9060
|
-
planDialogWaitMs: opts.planDialogWaitMs ?? 3e4
|
|
9123
|
+
planDialogWaitMs: opts.planDialogWaitMs ?? 3e4,
|
|
9124
|
+
// 100 times the longest silence measured while Claude works (h #g207).
|
|
9125
|
+
idleEndMs: opts.idleEndMs ?? 2e4
|
|
9061
9126
|
};
|
|
9062
9127
|
}
|
|
9063
9128
|
/** The CLI arguments this session spawns with. */
|
|
@@ -9335,7 +9400,8 @@ var ClaudeSession = class {
|
|
|
9335
9400
|
end: null,
|
|
9336
9401
|
sawUserRecord: false,
|
|
9337
9402
|
lastBeat: Date.now(),
|
|
9338
|
-
awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
|
|
9403
|
+
awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null,
|
|
9404
|
+
sawReply: false
|
|
9339
9405
|
};
|
|
9340
9406
|
this.forkPending = false;
|
|
9341
9407
|
this.scan = scan;
|
|
@@ -9358,6 +9424,9 @@ var ClaudeSession = class {
|
|
|
9358
9424
|
if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
|
|
9359
9425
|
return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
|
|
9360
9426
|
}
|
|
9427
|
+
if (scan.sawReply && !this.planApprovalToolUseId && Date.now() - this.lastDataAt >= this.o.idleEndMs && Date.now() - scan.stopSeenAt >= this.o.idleEndMs) {
|
|
9428
|
+
return { stopReason: null, end: "ended", denied: this.turnDenied };
|
|
9429
|
+
}
|
|
9361
9430
|
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
9362
9431
|
if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
|
|
9363
9432
|
const now = Date.now();
|
|
@@ -9407,9 +9476,11 @@ var ClaudeSession = class {
|
|
|
9407
9476
|
scan.awaitingOwnPrompt = null;
|
|
9408
9477
|
}
|
|
9409
9478
|
if (rec) this.notePlanApproval(rec);
|
|
9479
|
+
if (rec?.type === "queue-operation" && rec.operation === "enqueue") this.enqueued++;
|
|
9410
9480
|
scan.onRecord(s, rec);
|
|
9411
9481
|
if (rec) {
|
|
9412
9482
|
if (rec.type === "assistant" && rec.message) {
|
|
9483
|
+
scan.sawReply = true;
|
|
9413
9484
|
const reason = rec.message.stop_reason;
|
|
9414
9485
|
if (typeof reason === "string") {
|
|
9415
9486
|
scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
|
|
@@ -9498,6 +9569,29 @@ var ClaudeSession = class {
|
|
|
9498
9569
|
this.write("\r");
|
|
9499
9570
|
return true;
|
|
9500
9571
|
}
|
|
9572
|
+
/**
|
|
9573
|
+
* Type a message into the running turn's own input queue, not as a turn of
|
|
9574
|
+
* its own (h #g206). Measured on 2.1.288: text pasted and submitted while a
|
|
9575
|
+
* tool call runs is recorded as `queue-operation` enqueue and remove plus a
|
|
9576
|
+
* `queued_command` attachment, answered in the same turn, which still ends
|
|
9577
|
+
* once. Resolves true once the enqueue is in the transcript; false with no
|
|
9578
|
+
* turn running, or with a plan approval dialog up, which would take the
|
|
9579
|
+
* keys as its answer.
|
|
9580
|
+
*/
|
|
9581
|
+
async queueInput(text, timeoutMs = 3e3) {
|
|
9582
|
+
if (!this.turn || !this.proc || this.exited || this.pendingPlanApproval) return false;
|
|
9583
|
+
const before = this.enqueued;
|
|
9584
|
+
this.write(this.o.bracketedPaste ? "\x1B[200~" + text + "\x1B[201~" : text);
|
|
9585
|
+
await delay(this.o.submitMinMs);
|
|
9586
|
+
this.write("\r");
|
|
9587
|
+
const deadline = Date.now() + timeoutMs;
|
|
9588
|
+
while (this.enqueued === before) {
|
|
9589
|
+
if (!this.turn || this.exited || Date.now() >= deadline) return false;
|
|
9590
|
+
await delay(this.o.pollMs);
|
|
9591
|
+
this.scanTranscript();
|
|
9592
|
+
}
|
|
9593
|
+
return true;
|
|
9594
|
+
}
|
|
9501
9595
|
/** Wait until the PTY has been quiet for `permissionQuietMs`, at most 3 s. */
|
|
9502
9596
|
async waitForQuiet() {
|
|
9503
9597
|
const until = Date.now() + 3e3;
|
|
@@ -9585,11 +9679,29 @@ var ClaudeSession = class {
|
|
|
9585
9679
|
async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
|
|
9586
9680
|
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
9587
9681
|
const usage = new TurnUsageAccumulator();
|
|
9682
|
+
const startedAt = Date.now();
|
|
9683
|
+
let reportedDurationMs = null;
|
|
9684
|
+
let apiError = null;
|
|
9588
9685
|
const { stopReason, end, denied } = await this.runTurnLoop(
|
|
9589
9686
|
prompt,
|
|
9590
9687
|
(raw, rec) => {
|
|
9591
9688
|
onLine(raw);
|
|
9592
9689
|
if (rec) usage.add(rec);
|
|
9690
|
+
if (rec?.type === "assistant") {
|
|
9691
|
+
if (rec.isApiErrorMessage === true) {
|
|
9692
|
+
const text = (Array.isArray(rec.message?.content) ? rec.message.content : []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
9693
|
+
apiError = {
|
|
9694
|
+
text,
|
|
9695
|
+
...typeof rec.apiErrorStatus === "number" ? { status: rec.apiErrorStatus } : {},
|
|
9696
|
+
...typeof rec.error === "string" ? { kind: rec.error } : {}
|
|
9697
|
+
};
|
|
9698
|
+
} else if (rec.message?.model !== "<synthetic>") {
|
|
9699
|
+
apiError = null;
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
9702
|
+
if (isTurnDurationRecord(rec) && typeof rec.durationMs === "number") {
|
|
9703
|
+
reportedDurationMs = rec.durationMs;
|
|
9704
|
+
}
|
|
9593
9705
|
},
|
|
9594
9706
|
timeout,
|
|
9595
9707
|
onHeartbeat
|
|
@@ -9607,6 +9719,9 @@ var ClaudeSession = class {
|
|
|
9607
9719
|
usage: usage.turnTotal,
|
|
9608
9720
|
lastCallUsage: usage.lastCall,
|
|
9609
9721
|
callCount: usage.callCount,
|
|
9722
|
+
costUsd: usage.costUsd,
|
|
9723
|
+
durationMs: reportedDurationMs ?? Date.now() - startedAt,
|
|
9724
|
+
apiError,
|
|
9610
9725
|
denied
|
|
9611
9726
|
};
|
|
9612
9727
|
}
|
|
@@ -9754,13 +9869,19 @@ function interactiveExtraArgs(opts) {
|
|
|
9754
9869
|
if (opts.fastMode) {
|
|
9755
9870
|
flagSettings.fastMode = true;
|
|
9756
9871
|
}
|
|
9872
|
+
if (opts.bypass) {
|
|
9873
|
+
flagSettings.skipDangerousModePermissionPrompt = true;
|
|
9874
|
+
extraArgs.push("--dangerously-skip-permissions");
|
|
9875
|
+
}
|
|
9757
9876
|
if (Object.keys(flagSettings).length > 0) {
|
|
9758
9877
|
extraArgs.push("--settings", JSON.stringify(flagSettings));
|
|
9759
9878
|
}
|
|
9760
9879
|
if (opts.permissionMode === "bypassPermissions") {
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9880
|
+
if (!opts.bypass) {
|
|
9881
|
+
log.warn(
|
|
9882
|
+
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
|
|
9883
|
+
);
|
|
9884
|
+
}
|
|
9764
9885
|
} else if (opts.permissionMode) {
|
|
9765
9886
|
extraArgs.push("--permission-mode", opts.permissionMode);
|
|
9766
9887
|
}
|
|
@@ -9812,6 +9933,7 @@ function heartbeatFrame(sessionId, openCall, now = Date.now()) {
|
|
|
9812
9933
|
}
|
|
9813
9934
|
function interactiveResultFrame(opts) {
|
|
9814
9935
|
const completed = opts.end === "stop" && !!opts.stopReason;
|
|
9936
|
+
const apiError = completed && opts.apiError ? opts.apiError : null;
|
|
9815
9937
|
let result;
|
|
9816
9938
|
let terminalReason = "completed";
|
|
9817
9939
|
if (!completed) {
|
|
@@ -9829,20 +9951,35 @@ function interactiveResultFrame(opts) {
|
|
|
9829
9951
|
return JSON.stringify({
|
|
9830
9952
|
type: "result",
|
|
9831
9953
|
subtype: completed ? "success" : "error_during_execution",
|
|
9832
|
-
is_error: !completed,
|
|
9954
|
+
is_error: !completed || apiError !== null,
|
|
9833
9955
|
stop_reason: completed ? opts.stopReason : null,
|
|
9834
9956
|
terminal_reason: terminalReason,
|
|
9835
|
-
result,
|
|
9957
|
+
result: apiError ? apiError.text : result,
|
|
9958
|
+
...apiError?.status !== void 0 ? { api_error_status: apiError.status } : {},
|
|
9836
9959
|
session_id: opts.sessionId,
|
|
9837
9960
|
usage: opts.usage ?? {},
|
|
9838
9961
|
// The TUI has no `can_use_tool` channel, so a permission dialog is the
|
|
9839
9962
|
// only way it asks; each one the session denied is reported in the
|
|
9840
9963
|
// headless field's place, names and ids only (h #g109).
|
|
9841
9964
|
permission_denials: opts.denials ?? [],
|
|
9842
|
-
total_cost_usd: null,
|
|
9843
|
-
duration_ms: 0
|
|
9965
|
+
total_cost_usd: opts.costUsd ?? null,
|
|
9966
|
+
duration_ms: opts.durationMs ?? 0
|
|
9844
9967
|
});
|
|
9845
9968
|
}
|
|
9969
|
+
function interactiveSpawnEnv2(env) {
|
|
9970
|
+
if (env.CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS !== void 0) return env;
|
|
9971
|
+
return { ...env, CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS: "0" };
|
|
9972
|
+
}
|
|
9973
|
+
function streamFrameFromRecord(raw) {
|
|
9974
|
+
if (!raw.includes('"isApiErrorMessage":true')) return raw;
|
|
9975
|
+
try {
|
|
9976
|
+
const rec = JSON.parse(raw);
|
|
9977
|
+
if (rec?.isApiErrorMessage !== true || rec.is_api_error_message !== void 0) return raw;
|
|
9978
|
+
return JSON.stringify({ ...rec, is_api_error_message: true });
|
|
9979
|
+
} catch {
|
|
9980
|
+
return raw;
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9846
9983
|
function carriesReplyText(line) {
|
|
9847
9984
|
try {
|
|
9848
9985
|
const rec = JSON.parse(line);
|
|
@@ -9880,6 +10017,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9880
10017
|
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
9881
10018
|
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
9882
10019
|
resumeSessionId: opts.resumeSessionId,
|
|
10020
|
+
...opts.forkOf ? { forkOf: opts.forkOf } : {},
|
|
9883
10021
|
env: opts.env,
|
|
9884
10022
|
spawnPty: opts.spawnPty,
|
|
9885
10023
|
onScreen: (event) => {
|
|
@@ -9911,6 +10049,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9911
10049
|
effort: opts.effort,
|
|
9912
10050
|
sessionId: session.sessionId,
|
|
9913
10051
|
resumed: !!opts.resumeSessionId,
|
|
10052
|
+
forkedFrom: opts.forkOf ?? null,
|
|
9914
10053
|
jsonlPath: session.jsonlPath
|
|
9915
10054
|
});
|
|
9916
10055
|
let startPromise = null;
|
|
@@ -9940,14 +10079,14 @@ function spawnInteractiveProcess(opts) {
|
|
|
9940
10079
|
}
|
|
9941
10080
|
openCalls.clear();
|
|
9942
10081
|
denials = [];
|
|
9943
|
-
const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
|
|
10082
|
+
const { stopReason, end, usage, lastCallUsage, callCount, costUsd, durationMs, apiError, denied } = await session.tailTurn(
|
|
9944
10083
|
userMsg,
|
|
9945
10084
|
(raw) => {
|
|
9946
10085
|
try {
|
|
9947
10086
|
openCalls.add(JSON.parse(raw));
|
|
9948
10087
|
} catch {
|
|
9949
10088
|
}
|
|
9950
|
-
emit2(raw);
|
|
10089
|
+
emit2(streamFrameFromRecord(raw));
|
|
9951
10090
|
},
|
|
9952
10091
|
void 0,
|
|
9953
10092
|
// Only to a listening turn: it feeds that turn's watchdog, and the
|
|
@@ -9978,7 +10117,10 @@ function spawnInteractiveProcess(opts) {
|
|
|
9978
10117
|
end,
|
|
9979
10118
|
stopReason,
|
|
9980
10119
|
usage,
|
|
9981
|
-
denials
|
|
10120
|
+
denials,
|
|
10121
|
+
costUsd,
|
|
10122
|
+
durationMs,
|
|
10123
|
+
apiError
|
|
9982
10124
|
})
|
|
9983
10125
|
);
|
|
9984
10126
|
} catch (err) {
|
|
@@ -10022,7 +10164,9 @@ function spawnInteractiveProcess(opts) {
|
|
|
10022
10164
|
configDir: opts.configDir,
|
|
10023
10165
|
model: opts.model,
|
|
10024
10166
|
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
10025
|
-
|
|
10167
|
+
// Tool-free whatever the main spawn's posture: `dontAsk` with nothing
|
|
10168
|
+
// allowed, and never the skip flag, which would win over it (h #g210).
|
|
10169
|
+
extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk", bypass: false }),
|
|
10026
10170
|
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
10027
10171
|
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
10028
10172
|
env: opts.env,
|
|
@@ -10123,6 +10267,13 @@ function spawnInteractiveProcess(opts) {
|
|
|
10123
10267
|
},
|
|
10124
10268
|
flushTranscript: () => session.flushTranscript(),
|
|
10125
10269
|
planApprovalPending: () => session.pendingPlanApproval !== null,
|
|
10270
|
+
// Text only: an image in a mid-call message is dropped with the
|
|
10271
|
+
// decoder's own warning rather than staged for a turn that is not ours.
|
|
10272
|
+
queueInput: async (content) => {
|
|
10273
|
+
const text = decodeUserEnvelope(JSON.stringify({ type: "user", message: { role: "user", content } })).trim();
|
|
10274
|
+
if (!text) return false;
|
|
10275
|
+
return session.queueInput(text);
|
|
10276
|
+
},
|
|
10126
10277
|
askAside
|
|
10127
10278
|
}
|
|
10128
10279
|
};
|
|
@@ -11120,6 +11271,7 @@ function createTurnState(init) {
|
|
|
11120
11271
|
lastCallUsage: void 0,
|
|
11121
11272
|
resultFailure: void 0,
|
|
11122
11273
|
accountLimitHit: null,
|
|
11274
|
+
apiErrorTextShown: null,
|
|
11123
11275
|
usageLimitNote: false,
|
|
11124
11276
|
accountBlock: null,
|
|
11125
11277
|
modelRefusal: null,
|
|
@@ -12211,11 +12363,21 @@ ${plan}
|
|
|
12211
12363
|
state.hasReceivedContent = true;
|
|
12212
12364
|
}
|
|
12213
12365
|
const apiErrorReply = msg.is_api_error_message === true;
|
|
12214
|
-
const
|
|
12366
|
+
const limitFromReply = apiErrorReply && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
|
|
12215
12367
|
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
12216
12368
|
}));
|
|
12217
|
-
|
|
12369
|
+
const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
|
|
12370
|
+
if (limitFromReply) {
|
|
12218
12371
|
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
12372
|
+
const said = limitFactsFromText(
|
|
12373
|
+
msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
12374
|
+
);
|
|
12375
|
+
const known = state.accountLimitHit;
|
|
12376
|
+
state.accountLimitHit = {
|
|
12377
|
+
...known,
|
|
12378
|
+
window: known.window ?? said.window,
|
|
12379
|
+
...known.resetsAt === void 0 && said.resetsText ? { resetsText: said.resetsText } : {}
|
|
12380
|
+
};
|
|
12219
12381
|
}
|
|
12220
12382
|
if (hasText && !hasToolUse) {
|
|
12221
12383
|
startResultFallback(state);
|
|
@@ -12226,6 +12388,7 @@ ${plan}
|
|
|
12226
12388
|
for (const block2 of msg.message.content) {
|
|
12227
12389
|
if (block2.type === "text" && block2.text) {
|
|
12228
12390
|
if (limitErrorReply) continue;
|
|
12391
|
+
if (apiErrorReply) state.apiErrorTextShown = block2.text;
|
|
12229
12392
|
resetLastVisibleTextBlock(state);
|
|
12230
12393
|
const blockId = state.startTextBlock();
|
|
12231
12394
|
state.controller.enqueue({
|
|
@@ -12450,7 +12613,8 @@ ${plan}
|
|
|
12450
12613
|
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
12451
12614
|
// and says what to run; two notes about one failure is one too many.
|
|
12452
12615
|
!state.accountBlock && msg.is_error === true;
|
|
12453
|
-
if (!state.currentTextId && msg.is_error &&
|
|
12616
|
+
if (!state.currentTextId && msg.is_error && // Already on screen from the reply frame, word for word.
|
|
12617
|
+
!(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
12454
12618
|
const errId = state.startTextBlock();
|
|
12455
12619
|
state.controller.enqueue({
|
|
12456
12620
|
type: "text-delta",
|
|
@@ -13025,12 +13189,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13025
13189
|
if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
|
|
13026
13190
|
const useInteractive = selectedTransport === "interactive";
|
|
13027
13191
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
13028
|
-
const modelFallbackArmed = attempt?.armed === true && !compactionMode
|
|
13192
|
+
const modelFallbackArmed = attempt?.armed === true && !compactionMode;
|
|
13029
13193
|
const sourceAccount = normalizeAccountName(
|
|
13030
13194
|
this.config.account ?? DEFAULT_ACCOUNT
|
|
13031
13195
|
);
|
|
13032
13196
|
const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
|
|
13033
|
-
let failover =
|
|
13197
|
+
let failover = compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
|
|
13034
13198
|
account: sourceAccount,
|
|
13035
13199
|
baseCliPath,
|
|
13036
13200
|
cliPath: this.config.cliPath,
|
|
@@ -13183,7 +13347,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13183
13347
|
deleteActiveProcess(sk);
|
|
13184
13348
|
clearCompression(sk);
|
|
13185
13349
|
}
|
|
13186
|
-
const failoverAnswer = compactionMode ||
|
|
13350
|
+
const failoverAnswer = compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
|
|
13187
13351
|
sourceAccount,
|
|
13188
13352
|
candidates: failoverCandidates(this.config.failoverAccounts, sourceAccount)
|
|
13189
13353
|
});
|
|
@@ -13246,7 +13410,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13246
13410
|
});
|
|
13247
13411
|
}
|
|
13248
13412
|
const processForAccount = getActiveProcess(sk);
|
|
13249
|
-
if (!compactionMode &&
|
|
13413
|
+
if (!compactionMode && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
|
|
13250
13414
|
log.notice("claude process belongs to another account; starting fresh", {
|
|
13251
13415
|
sessionKey: sk,
|
|
13252
13416
|
was: processForAccount.cliPath,
|
|
@@ -13263,7 +13427,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13263
13427
|
includeHistoryContext = true;
|
|
13264
13428
|
}
|
|
13265
13429
|
let forkFromClaudeSessionId;
|
|
13266
|
-
if (this.config.forkSessions === true && includeHistoryContext && !compactionMode &&
|
|
13430
|
+
if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && failoverAnswer?.kind !== "switch") {
|
|
13267
13431
|
const parent = findForkParent({
|
|
13268
13432
|
sessionKey: sk,
|
|
13269
13433
|
prompt: options.prompt,
|
|
@@ -13319,7 +13483,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13319
13483
|
});
|
|
13320
13484
|
}
|
|
13321
13485
|
}
|
|
13322
|
-
if (this.config.forkSessions === true && !compactionMode
|
|
13486
|
+
if (this.config.forkSessions === true && !compactionMode) {
|
|
13323
13487
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
13324
13488
|
}
|
|
13325
13489
|
const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
|
|
@@ -13375,6 +13539,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13375
13539
|
if (useInteractive) reportUnmeasuredInteractiveCli(cliVersion);
|
|
13376
13540
|
const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
|
|
13377
13541
|
const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
|
|
13542
|
+
const ptyBypassSupported = cliSupportsInteractiveBypass(cliVersion);
|
|
13378
13543
|
if (ptyReadOnly && !ptyReadOnlySupported) {
|
|
13379
13544
|
throw new Error(
|
|
13380
13545
|
"The read-only preset on the interactive transport needs Claude Code 2.1.263 or newer (--restricted and --permission-mode dontAsk). Update claude, or use the headless transport."
|
|
@@ -13384,14 +13549,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13384
13549
|
this.config.failoverAccounts,
|
|
13385
13550
|
sourceAccount
|
|
13386
13551
|
);
|
|
13387
|
-
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode &&
|
|
13552
|
+
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
|
|
13388
13553
|
// rate-limit error, never the form. See `TurnMode`.
|
|
13389
13554
|
mode !== "generate" && isAccountFailoverQuestionActive({
|
|
13390
13555
|
configured: this.config.accountFailover,
|
|
13391
13556
|
candidates: failoverAccounts,
|
|
13392
13557
|
opencodeHasQuestion: (await loadLiveToolInfo()).hasQuestion,
|
|
13393
13558
|
compactionMode,
|
|
13394
|
-
interactive: !!useInteractive,
|
|
13395
13559
|
// A subagent follows its parent's account for free, because the
|
|
13396
13560
|
// override is account-scoped. Asking it would put a form in a session
|
|
13397
13561
|
// the operator is usually not even looking at.
|
|
@@ -13773,7 +13937,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13773
13937
|
}
|
|
13774
13938
|
if (interactiveBypassRequested) {
|
|
13775
13939
|
log.warn(
|
|
13776
|
-
"interactiveBypass ignored:
|
|
13940
|
+
"interactiveBypass is ignored: skipPermissions governs the interactive transport too"
|
|
13777
13941
|
);
|
|
13778
13942
|
}
|
|
13779
13943
|
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
@@ -13787,13 +13951,19 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13787
13951
|
const posture = interactivePermissionPosture({
|
|
13788
13952
|
permissionMode: self.config.permissionMode,
|
|
13789
13953
|
allow,
|
|
13790
|
-
supportsReadOnly: ptyReadOnlySupported
|
|
13954
|
+
supportsReadOnly: ptyReadOnlySupported,
|
|
13955
|
+
skipPermissions: self.config.skipPermissions,
|
|
13956
|
+
controlRequestBehavior: self.config.controlRequestBehavior,
|
|
13957
|
+
controlRequestToolBehaviors: self.config.controlRequestToolBehaviors,
|
|
13958
|
+
supportsBypass: ptyBypassSupported
|
|
13791
13959
|
});
|
|
13792
13960
|
if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
|
|
13793
13961
|
const ap = spawnInteractiveProcess({
|
|
13794
13962
|
cwd,
|
|
13795
13963
|
cliPath,
|
|
13796
|
-
|
|
13964
|
+
// On an account switch, the other account's directory: the
|
|
13965
|
+
// TUI writes its transcript there (h #g209).
|
|
13966
|
+
configDir: self.skillBridgeSpawn(failover).configDir,
|
|
13797
13967
|
model: spawnModelId,
|
|
13798
13968
|
fastMode,
|
|
13799
13969
|
mcpConfigPaths: mcp.paths,
|
|
@@ -13801,19 +13971,26 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13801
13971
|
permissionsAllow: posture.allow,
|
|
13802
13972
|
permissionMode: posture.permissionMode,
|
|
13803
13973
|
restricted: posture.restricted,
|
|
13804
|
-
|
|
13974
|
+
bypass: posture.bypass,
|
|
13975
|
+
disallowedTools: [...wiring.allDisallowed, ...posture.disallowed],
|
|
13805
13976
|
proxyServer: state.proxyServer,
|
|
13806
13977
|
systemPromptFile,
|
|
13807
13978
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
13808
13979
|
effort: reasoningEffort,
|
|
13809
13980
|
resumeSessionId,
|
|
13981
|
+
// A fork of a conversation already served (`forkSessions`):
|
|
13982
|
+
// the TUI branches it instead of the history being replayed.
|
|
13983
|
+
...forkFromClaudeSessionId && !resumeSessionId ? { forkOf: forkFromClaudeSessionId } : {},
|
|
13810
13984
|
// The headless spawn's env, so hygiene, effort and the
|
|
13811
|
-
// agent's prompt cache TTL reach both transports alike
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13985
|
+
// agent's prompt cache TTL reach both transports alike, plus
|
|
13986
|
+
// what keeps a proxied call in the foreground (h #g210).
|
|
13987
|
+
env: interactiveSpawnEnv2(
|
|
13988
|
+
claudeSpawnEnv({
|
|
13989
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
13990
|
+
effort: reasoningEffort,
|
|
13991
|
+
promptCacheTtl
|
|
13992
|
+
})
|
|
13993
|
+
)
|
|
13817
13994
|
});
|
|
13818
13995
|
ap.proc.once(
|
|
13819
13996
|
"exit",
|
|
@@ -13821,6 +13998,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13821
13998
|
);
|
|
13822
13999
|
ap.mcpHash = mcp.bridgedHash;
|
|
13823
14000
|
ap.mcpServers = mcp.allEnabledServerNames;
|
|
14001
|
+
ap.cliPath = cliPath;
|
|
13824
14002
|
setActiveProcess(sk, ap);
|
|
13825
14003
|
state.proc = ap.proc;
|
|
13826
14004
|
state.lineEmitter = ap.lineEmitter;
|
|
@@ -13828,6 +14006,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13828
14006
|
log.info("spawned interactive claude session", {
|
|
13829
14007
|
sk,
|
|
13830
14008
|
resumed: !!resumeSessionId,
|
|
14009
|
+
forked: !!forkFromClaudeSessionId && !resumeSessionId,
|
|
13831
14010
|
cliPath,
|
|
13832
14011
|
configDir: self.config.configDir,
|
|
13833
14012
|
model: effectiveModelId
|
|
@@ -14042,7 +14221,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14042
14221
|
sourceAccount,
|
|
14043
14222
|
candidates: failoverAccounts,
|
|
14044
14223
|
resetsAt: state.accountLimitHit?.resetsAt,
|
|
14045
|
-
window: state.accountLimitHit?.window
|
|
14224
|
+
window: state.accountLimitHit?.window,
|
|
14225
|
+
resetsText: state.accountLimitHit?.resetsText
|
|
14046
14226
|
})
|
|
14047
14227
|
});
|
|
14048
14228
|
state.endTextBlock();
|
|
@@ -14332,9 +14512,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14332
14512
|
state.proc.on("error", procErrorHandler);
|
|
14333
14513
|
if (hasMatchedPendingResults) {
|
|
14334
14514
|
state.cliAskedForWork = true;
|
|
14335
|
-
const forwardTrailingUserMessages = () => {
|
|
14515
|
+
const forwardTrailingUserMessages = async () => {
|
|
14336
14516
|
if (compactionMode || !state.activeProcess) return 0;
|
|
14337
|
-
|
|
14517
|
+
const control = state.activeProcess.interactiveControl;
|
|
14518
|
+
if (control && (!control.queueInput || !control.turnRunning())) return 0;
|
|
14338
14519
|
const trailing = getTrailingUserMessages(effectivePrompt, {
|
|
14339
14520
|
stripContextReminders: self.stripContextRemindersEnabled()
|
|
14340
14521
|
});
|
|
@@ -14345,6 +14526,21 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14345
14526
|
assistantIndex: trailing.assistantIndex,
|
|
14346
14527
|
count: trailing.messages.length
|
|
14347
14528
|
};
|
|
14529
|
+
if (control) {
|
|
14530
|
+
let queued = 0;
|
|
14531
|
+
for (const content of fresh) if (await control.queueInput(content)) queued++;
|
|
14532
|
+
if (queued < fresh.length) {
|
|
14533
|
+
log.warn("the interactive turn did not take a forwarded user message", {
|
|
14534
|
+
sessionKey: sk,
|
|
14535
|
+
messages: fresh.length - queued
|
|
14536
|
+
});
|
|
14537
|
+
}
|
|
14538
|
+
log.info("forwarded user messages into the interactive turn's input queue", {
|
|
14539
|
+
sessionKey: sk,
|
|
14540
|
+
messages: queued
|
|
14541
|
+
});
|
|
14542
|
+
return queued;
|
|
14543
|
+
}
|
|
14348
14544
|
for (const content of fresh) {
|
|
14349
14545
|
state.proc.stdin?.write(
|
|
14350
14546
|
JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n"
|
|
@@ -14356,7 +14552,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14356
14552
|
});
|
|
14357
14553
|
return fresh.length;
|
|
14358
14554
|
};
|
|
14359
|
-
if (!state.unattendedTurnEnded && forwardTrailingUserMessages() > 0) {
|
|
14555
|
+
if (!state.unattendedTurnEnded && await forwardTrailingUserMessages() > 0) {
|
|
14360
14556
|
await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
|
|
14361
14557
|
if (state.controllerClosed) return;
|
|
14362
14558
|
}
|
|
@@ -14391,7 +14587,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14391
14587
|
}
|
|
14392
14588
|
if (state.unattendedTurnEnded) {
|
|
14393
14589
|
deliverPendingCompletions(state);
|
|
14394
|
-
forwardTrailingUserMessages();
|
|
14590
|
+
await forwardTrailingUserMessages();
|
|
14395
14591
|
}
|
|
14396
14592
|
const unemitted = getPendingProxyCalls(sk).filter(
|
|
14397
14593
|
(call) => !call.emitted
|