@khalilgharbaoui/opencode-claude-code-plugin 0.40.0 → 0.42.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.d.ts +9 -1
- package/dist/index.js +483 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +68 -14
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/claude-code-language-model.ts
|
|
2
|
-
import { join as
|
|
2
|
+
import { join as join12 } from "path";
|
|
3
3
|
|
|
4
4
|
// src/ids.ts
|
|
5
5
|
import { randomInt } from "crypto";
|
|
@@ -1719,6 +1719,10 @@ function cliSupportsPermissionPrompts(v) {
|
|
|
1719
1719
|
if (!v) return false;
|
|
1720
1720
|
return gte(v, { major: 2, minor: 1, patch: 263 });
|
|
1721
1721
|
}
|
|
1722
|
+
function cliSupportsDontAsk(v) {
|
|
1723
|
+
if (!v) return false;
|
|
1724
|
+
return gte(v, { major: 2, minor: 1, patch: 263 });
|
|
1725
|
+
}
|
|
1722
1726
|
function cliSupportsSideQuestion(v) {
|
|
1723
1727
|
if (!v) return false;
|
|
1724
1728
|
return gte(v, { major: 2, minor: 1, patch: 258 });
|
|
@@ -1728,6 +1732,43 @@ function cliSupportsThinking(v) {
|
|
|
1728
1732
|
return gte(v, { major: 2, minor: 0, patch: 0 });
|
|
1729
1733
|
}
|
|
1730
1734
|
var flagSupport = /* @__PURE__ */ new Map();
|
|
1735
|
+
var headlessSupport = /* @__PURE__ */ new Map();
|
|
1736
|
+
function detectHeadlessSupport(cliPath) {
|
|
1737
|
+
const key = `${cliPath}\0headless`;
|
|
1738
|
+
const cached = headlessSupport.get(key);
|
|
1739
|
+
if (cached) return cached;
|
|
1740
|
+
let deadlineKill = false;
|
|
1741
|
+
const promise = (async () => {
|
|
1742
|
+
try {
|
|
1743
|
+
const execution = execFileAsync(cliPath, [
|
|
1744
|
+
"--print",
|
|
1745
|
+
"--input-format",
|
|
1746
|
+
"stream-json",
|
|
1747
|
+
"--output-format",
|
|
1748
|
+
"stream-json",
|
|
1749
|
+
"--help"
|
|
1750
|
+
], { timeout: probeTimeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 });
|
|
1751
|
+
execution.child.stdin?.end();
|
|
1752
|
+
const { stdout } = await execution;
|
|
1753
|
+
if (!/^\s*Usage:/im.test(stdout) || !/^\s*Options:/im.test(stdout)) return "unknown";
|
|
1754
|
+
const flags = ["--print", "--input-format", "--output-format"];
|
|
1755
|
+
if (!flags.every((flag) => new RegExp(`^\\s+(?:-[A-Za-z],\\s+)?${flag}(?:\\s|,|$)`, "m").test(stdout))) {
|
|
1756
|
+
return "unsupported";
|
|
1757
|
+
}
|
|
1758
|
+
return stdout.includes("stream-json") ? "supported" : "unknown";
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
deadlineKill = killedByDeadline(error);
|
|
1761
|
+
const stderr = error?.stderr;
|
|
1762
|
+
if (typeof stderr === "string" && /^(?:error: )?(?:unknown|unrecognized|unsupported) option ['"]?--(?:print|input-format|output-format)(?:['"\s]|$)/im.test(stderr)) {
|
|
1763
|
+
return "unsupported";
|
|
1764
|
+
}
|
|
1765
|
+
return "unknown";
|
|
1766
|
+
}
|
|
1767
|
+
})();
|
|
1768
|
+
headlessSupport.set(key, promise);
|
|
1769
|
+
forgetDeadlineKill(headlessSupport, key, promise, () => deadlineKill);
|
|
1770
|
+
return promise;
|
|
1771
|
+
}
|
|
1731
1772
|
function detectCliSupportsFlag(cliPath, flag) {
|
|
1732
1773
|
const key = `${cliPath}\0${flag}`;
|
|
1733
1774
|
const cached = flagSupport.get(key);
|
|
@@ -4496,6 +4537,20 @@ function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAUL
|
|
|
4496
4537
|
overrides: resolved.overridden
|
|
4497
4538
|
};
|
|
4498
4539
|
}
|
|
4540
|
+
function interactivePermissionPosture(input) {
|
|
4541
|
+
if (!isReadOnlyPermissionMode(input.permissionMode)) {
|
|
4542
|
+
return { permissionMode: input.permissionMode, restricted: false, allow: input.allow };
|
|
4543
|
+
}
|
|
4544
|
+
if (!input.supportsReadOnly) return null;
|
|
4545
|
+
const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
|
|
4546
|
+
return {
|
|
4547
|
+
permissionMode: "dontAsk",
|
|
4548
|
+
restricted: true,
|
|
4549
|
+
allow: input.allow.filter(
|
|
4550
|
+
(rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
|
|
4551
|
+
)
|
|
4552
|
+
};
|
|
4553
|
+
}
|
|
4499
4554
|
function isReadOnlyPermissionMode(mode) {
|
|
4500
4555
|
return mode === READ_ONLY_PERMISSION_MODE;
|
|
4501
4556
|
}
|
|
@@ -4575,7 +4630,25 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
4575
4630
|
if (!question) return { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
4576
4631
|
options.abortSignal?.throwIfAborted();
|
|
4577
4632
|
const { proc, lineEmitter } = activeProcess;
|
|
4578
|
-
if (options.interactive
|
|
4633
|
+
if (options.interactive) {
|
|
4634
|
+
const askAside = activeProcess.interactiveControl?.askAside;
|
|
4635
|
+
if (!askAside) throw new Error("/btw needs a live interactive Claude Code session.");
|
|
4636
|
+
if (pendingProcesses.has(proc)) {
|
|
4637
|
+
throw new Error("Wait for the current /btw to finish before asking another.");
|
|
4638
|
+
}
|
|
4639
|
+
pendingProcesses.add(proc);
|
|
4640
|
+
try {
|
|
4641
|
+
const response = await askAside(question, {
|
|
4642
|
+
...options.history === void 0 ? {} : { history: options.history },
|
|
4643
|
+
timeoutMs: options.timeoutMs ?? 12e4,
|
|
4644
|
+
...options.abortSignal ? { abortSignal: options.abortSignal } : {}
|
|
4645
|
+
});
|
|
4646
|
+
return { response, synthetic: false };
|
|
4647
|
+
} finally {
|
|
4648
|
+
pendingProcesses.delete(proc);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
if (!proc.stdout) {
|
|
4579
4652
|
throw new Error("/btw requires the headless Claude Code transport; interactive sessions are not supported.");
|
|
4580
4653
|
}
|
|
4581
4654
|
if (!cliSupportsSideQuestion(options.cliVersion)) {
|
|
@@ -5725,6 +5798,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5725
5798
|
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5726
5799
|
"interactive transport cancelled the usage-limit auto-continue",
|
|
5727
5800
|
"interactive transport cannot continue",
|
|
5801
|
+
"interactive transport could not answer the plan approval; stopping the turn",
|
|
5802
|
+
"interactive transport could not stage an image",
|
|
5728
5803
|
"interactive transport denied a permission prompt",
|
|
5729
5804
|
"interactive transport dropped non-text content blocks",
|
|
5730
5805
|
"interactive turn failed",
|
|
@@ -6336,6 +6411,10 @@ async function fetchPlanUsage(cliPath, options = {}) {
|
|
|
6336
6411
|
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
6337
6412
|
const run = options.runImpl ?? runCli;
|
|
6338
6413
|
try {
|
|
6414
|
+
const support = options.headlessSupportImpl ?? (options.runImpl ? void 0 : detectHeadlessSupport);
|
|
6415
|
+
if (support && await support(cliPath) === "unsupported") {
|
|
6416
|
+
return { status: "failed", error: "Plan usage is unavailable: this Claude CLI does not support headless output. Check usage in the Claude CLI directly." };
|
|
6417
|
+
}
|
|
6339
6418
|
const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
|
|
6340
6419
|
const stdout = await run(
|
|
6341
6420
|
cliPath,
|
|
@@ -8055,6 +8134,9 @@ The complete prior conversation appears above. The synthesis instructions follow
|
|
|
8055
8134
|
`
|
|
8056
8135
|
});
|
|
8057
8136
|
}
|
|
8137
|
+
if (opts.compactionInstructions?.trim()) {
|
|
8138
|
+
content.push({ type: "text", text: opts.compactionInstructions });
|
|
8139
|
+
}
|
|
8058
8140
|
} else if (includeHistoryContext) {
|
|
8059
8141
|
const historyContext = compactConversationHistory(prompt);
|
|
8060
8142
|
if (historyContext) {
|
|
@@ -8510,8 +8592,10 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
8510
8592
|
|
|
8511
8593
|
// src/claude-session-wrapper.ts
|
|
8512
8594
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
8513
|
-
import {
|
|
8595
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
8596
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
8514
8597
|
import { unlink as unlink2 } from "fs/promises";
|
|
8598
|
+
import { join as join10 } from "path";
|
|
8515
8599
|
|
|
8516
8600
|
// src/claude-session-bun.ts
|
|
8517
8601
|
import * as os4 from "os";
|
|
@@ -8591,6 +8675,17 @@ var SCREEN_PATTERNS = [
|
|
|
8591
8675
|
"i"
|
|
8592
8676
|
)
|
|
8593
8677
|
},
|
|
8678
|
+
{
|
|
8679
|
+
// Measured verbatim on 2.1.288: "Claude has written up a plan and is ready
|
|
8680
|
+
// to execute. Would you like to proceed? ❯ 1. Yes, auto-accept edits
|
|
8681
|
+
// 2. Yes, manually approve edits 3. Tell Claude what to change". The
|
|
8682
|
+
// numbered last choice is what a reply quoting the question never has.
|
|
8683
|
+
kind: "plan-approval",
|
|
8684
|
+
pattern: new RegExp(
|
|
8685
|
+
`${words("Would you like to proceed?")}[\\s\\S]{0,400}?\\d\\s*\\.\\s*(?:${words("Tell Claude what to change")}|${words("No, keep planning")})`,
|
|
8686
|
+
"i"
|
|
8687
|
+
)
|
|
8688
|
+
},
|
|
8594
8689
|
{
|
|
8595
8690
|
// The question alone is not enough: Claude can write "Do you want to
|
|
8596
8691
|
// proceed?" in a reply. The dialog always numbers its options.
|
|
@@ -8621,6 +8716,13 @@ function highlightedChoice(text) {
|
|
|
8621
8716
|
}
|
|
8622
8717
|
return last === "yes" || last === "no" ? last : null;
|
|
8623
8718
|
}
|
|
8719
|
+
function planApprovalKeys(text) {
|
|
8720
|
+
const approve = /(\d)\s*\.\s*Yes,?\s*(?:and\s*)?manually\s*approve\s*edits/i.exec(text)?.[1] ?? /(\d)\s*\.\s*Yes,?\s*(?:and\s*)?auto-?\s*accept\s*edits/i.exec(text)?.[1] ?? null;
|
|
8721
|
+
const tell = /(\d)\s*\.\s*Tell\s*Claude\s*what\s*to\s*change/i.exec(text)?.[1];
|
|
8722
|
+
if (tell) return { approve, reject: tell, rejectTakesText: true };
|
|
8723
|
+
const keep = /(\d)\s*\.\s*No,?\s*keep\s*planning/i.exec(text)?.[1] ?? null;
|
|
8724
|
+
return { approve, reject: keep, rejectTakesText: false };
|
|
8725
|
+
}
|
|
8624
8726
|
function classifyScreen(text) {
|
|
8625
8727
|
for (const { kind, pattern } of SCREEN_PATTERNS) {
|
|
8626
8728
|
const match = pattern.exec(text);
|
|
@@ -8712,7 +8814,7 @@ var TurnUsageAccumulator = class {
|
|
|
8712
8814
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
8713
8815
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8714
8816
|
var RAW_KEEP_CHARS = 64 * 1024;
|
|
8715
|
-
var SCREEN_WINDOW_CHARS =
|
|
8817
|
+
var SCREEN_WINDOW_CHARS = 64 * 1024;
|
|
8716
8818
|
var INTERRUPT_MARKER = "[Request interrupted by user";
|
|
8717
8819
|
function isInterruptRecord(rec) {
|
|
8718
8820
|
if (!rec || rec.type !== "user" || !rec.message) return false;
|
|
@@ -8726,6 +8828,11 @@ function isInterruptRecord(rec) {
|
|
|
8726
8828
|
function isTurnDurationRecord(rec) {
|
|
8727
8829
|
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
8728
8830
|
}
|
|
8831
|
+
var normalizedPrompt = (text) => text.replace(/\s+/g, " ").trim();
|
|
8832
|
+
function isOwnPromptRecord(rec, normalized) {
|
|
8833
|
+
const content = rec?.type === "user" ? rec.message?.content : void 0;
|
|
8834
|
+
return typeof content === "string" && normalizedPrompt(content) === normalized;
|
|
8835
|
+
}
|
|
8729
8836
|
function resolveConfigDir(configDir) {
|
|
8730
8837
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8731
8838
|
if (!value) return path9.join(os4.homedir(), ".claude");
|
|
@@ -8760,6 +8867,10 @@ var ClaudeSession = class {
|
|
|
8760
8867
|
aborted = false;
|
|
8761
8868
|
signal;
|
|
8762
8869
|
resumeSessionId;
|
|
8870
|
+
forkOf;
|
|
8871
|
+
/** A fork whose first turn has not started: its transcript opens with the
|
|
8872
|
+
* parent's copied conversation, which belongs to no turn of this session. */
|
|
8873
|
+
forkPending = false;
|
|
8763
8874
|
env;
|
|
8764
8875
|
onScreen;
|
|
8765
8876
|
onExit;
|
|
@@ -8778,6 +8889,15 @@ var ClaudeSession = class {
|
|
|
8778
8889
|
/** The running turn's read state, shared with `flushTranscript`. */
|
|
8779
8890
|
scan = null;
|
|
8780
8891
|
turnDenied = [];
|
|
8892
|
+
/** The running turn's `ExitPlanMode` call that has no result yet. */
|
|
8893
|
+
planApprovalToolUseId = null;
|
|
8894
|
+
/** The approval dialog as drawn for one `ExitPlanMode` call, until it is
|
|
8895
|
+
* answered. Keyed by the call, so a redraw of a dialog already answered
|
|
8896
|
+
* (the TUI repaints it while it takes the key, measured on 2.1.288) can
|
|
8897
|
+
* never stand in for the next call's dialog. */
|
|
8898
|
+
planDialog = null;
|
|
8899
|
+
/** This turn's `ExitPlanMode` calls already answered. */
|
|
8900
|
+
answeredPlans = /* @__PURE__ */ new Set();
|
|
8781
8901
|
abandonTurn = false;
|
|
8782
8902
|
o;
|
|
8783
8903
|
constructor(opts = {}) {
|
|
@@ -8785,7 +8905,9 @@ var ClaudeSession = class {
|
|
|
8785
8905
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
8786
8906
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
8787
8907
|
this.signal = opts.signal;
|
|
8788
|
-
this.resumeSessionId = opts.resumeSessionId;
|
|
8908
|
+
this.resumeSessionId = opts.forkOf ? void 0 : opts.resumeSessionId;
|
|
8909
|
+
this.forkOf = opts.forkOf;
|
|
8910
|
+
this.forkPending = !!opts.forkOf;
|
|
8789
8911
|
this.env = opts.env;
|
|
8790
8912
|
this.onScreen = opts.onScreen;
|
|
8791
8913
|
this.onExit = opts.onExit;
|
|
@@ -8828,12 +8950,16 @@ var ClaudeSession = class {
|
|
|
8828
8950
|
// Shorter than `bootQuietMs`, so a trust dialog is answered before boot
|
|
8829
8951
|
// could declare the TUI ready and paste a prompt into it.
|
|
8830
8952
|
permissionQuietMs: opts.permissionQuietMs ?? 1e3,
|
|
8831
|
-
stopSettleMs: opts.stopSettleMs ?? 750
|
|
8953
|
+
stopSettleMs: opts.stopSettleMs ?? 750,
|
|
8954
|
+
// The decision arrives after opencode's own question round trip, by
|
|
8955
|
+
// which time the dialog has long been drawn; this only bounds a TUI
|
|
8956
|
+
// that never draws it.
|
|
8957
|
+
planDialogWaitMs: opts.planDialogWaitMs ?? 3e4
|
|
8832
8958
|
};
|
|
8833
8959
|
}
|
|
8834
8960
|
/** The CLI arguments this session spawns with. */
|
|
8835
8961
|
spawnArgs() {
|
|
8836
|
-
const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
8962
|
+
const args = this.forkOf ? ["--session-id", this.sessionId, "--resume", this.forkOf, "--fork-session"] : this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
8837
8963
|
if (this.o.model) args.push("--model", this.o.model);
|
|
8838
8964
|
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8839
8965
|
args.push("--setting-sources", this.o.settingSources);
|
|
@@ -8902,6 +9028,9 @@ var ClaudeSession = class {
|
|
|
8902
9028
|
this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
|
|
8903
9029
|
this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
|
|
8904
9030
|
if (this.o.debug) process.stdout.write(chunk);
|
|
9031
|
+
this.scheduleScreenCheck();
|
|
9032
|
+
}
|
|
9033
|
+
scheduleScreenCheck() {
|
|
8905
9034
|
this.clearScreenTimer();
|
|
8906
9035
|
this.screenTimer = setTimeout(() => {
|
|
8907
9036
|
this.screenTimer = null;
|
|
@@ -8957,6 +9086,13 @@ var ClaudeSession = class {
|
|
|
8957
9086
|
this.write("\x1B");
|
|
8958
9087
|
action = "cancelled";
|
|
8959
9088
|
break;
|
|
9089
|
+
case "plan-approval": {
|
|
9090
|
+
const id = this.planApprovalToolUseId;
|
|
9091
|
+
if (!this.turn || !id || this.answeredPlans.has(id) || this.planDialog?.id === id) return null;
|
|
9092
|
+
this.planDialog = { id, text };
|
|
9093
|
+
action = "parked";
|
|
9094
|
+
break;
|
|
9095
|
+
}
|
|
8960
9096
|
case "login":
|
|
8961
9097
|
case "onboarding":
|
|
8962
9098
|
action = "fatal";
|
|
@@ -9095,8 +9231,10 @@ var ClaudeSession = class {
|
|
|
9095
9231
|
stopSeenAt: 0,
|
|
9096
9232
|
end: null,
|
|
9097
9233
|
sawUserRecord: false,
|
|
9098
|
-
lastBeat: Date.now()
|
|
9234
|
+
lastBeat: Date.now(),
|
|
9235
|
+
awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
|
|
9099
9236
|
};
|
|
9237
|
+
this.forkPending = false;
|
|
9100
9238
|
this.scan = scan;
|
|
9101
9239
|
const deadline = Date.now() + timeout;
|
|
9102
9240
|
while (Date.now() < deadline) {
|
|
@@ -9130,6 +9268,9 @@ var ClaudeSession = class {
|
|
|
9130
9268
|
this.turn = null;
|
|
9131
9269
|
this.scan = null;
|
|
9132
9270
|
this.abandonTurn = false;
|
|
9271
|
+
this.planApprovalToolUseId = null;
|
|
9272
|
+
this.planDialog = null;
|
|
9273
|
+
this.answeredPlans.clear();
|
|
9133
9274
|
settle();
|
|
9134
9275
|
}
|
|
9135
9276
|
}
|
|
@@ -9155,6 +9296,14 @@ var ClaudeSession = class {
|
|
|
9155
9296
|
rec = JSON.parse(s);
|
|
9156
9297
|
} catch {
|
|
9157
9298
|
}
|
|
9299
|
+
if (scan.awaitingOwnPrompt !== null) {
|
|
9300
|
+
if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) {
|
|
9301
|
+
this.cursor = i + 1;
|
|
9302
|
+
continue;
|
|
9303
|
+
}
|
|
9304
|
+
scan.awaitingOwnPrompt = null;
|
|
9305
|
+
}
|
|
9306
|
+
if (rec) this.notePlanApproval(rec);
|
|
9158
9307
|
scan.onRecord(s, rec);
|
|
9159
9308
|
if (rec) {
|
|
9160
9309
|
if (rec.type === "assistant" && rec.message) {
|
|
@@ -9186,6 +9335,74 @@ var ClaudeSession = class {
|
|
|
9186
9335
|
flushTranscript() {
|
|
9187
9336
|
this.scanTranscript();
|
|
9188
9337
|
}
|
|
9338
|
+
/** Track the turn's `ExitPlanMode` call from the transcript: opened by the
|
|
9339
|
+
* assistant record that makes it, closed by the result that answers it. */
|
|
9340
|
+
notePlanApproval(rec) {
|
|
9341
|
+
const content = rec?.message?.content;
|
|
9342
|
+
if (!Array.isArray(content)) return;
|
|
9343
|
+
if (rec.type === "assistant") {
|
|
9344
|
+
for (const block of content) {
|
|
9345
|
+
if (block?.type === "tool_use" && block.name === "ExitPlanMode" && typeof block.id === "string") {
|
|
9346
|
+
this.planApprovalToolUseId = block.id;
|
|
9347
|
+
this.scheduleScreenCheck();
|
|
9348
|
+
}
|
|
9349
|
+
}
|
|
9350
|
+
} else if (rec.type === "user" && this.planApprovalToolUseId) {
|
|
9351
|
+
const answered = content.some(
|
|
9352
|
+
(block) => block?.type === "tool_result" && block.tool_use_id === this.planApprovalToolUseId
|
|
9353
|
+
);
|
|
9354
|
+
if (answered) {
|
|
9355
|
+
if (this.planDialog?.id === this.planApprovalToolUseId) this.planDialog = null;
|
|
9356
|
+
this.planApprovalToolUseId = null;
|
|
9357
|
+
}
|
|
9358
|
+
}
|
|
9359
|
+
}
|
|
9360
|
+
/**
|
|
9361
|
+
* The running turn's unanswered `ExitPlanMode` call id, or null. While it is
|
|
9362
|
+
* set the TUI is parked on (or about to draw) the approval dialog, and the
|
|
9363
|
+
* next message is the operator's decision, not a new turn.
|
|
9364
|
+
*/
|
|
9365
|
+
get pendingPlanApproval() {
|
|
9366
|
+
return this.turn ? this.planApprovalToolUseId : null;
|
|
9367
|
+
}
|
|
9368
|
+
/**
|
|
9369
|
+
* Answer the plan approval dialog the running turn is parked on. Waits up to
|
|
9370
|
+
* `planDialogWaitMs` for it to be drawn, because the transcript records the
|
|
9371
|
+
* call before the TUI draws the dialog, and resolves false when there is
|
|
9372
|
+
* none to answer or the drawn dialog has no matching choice.
|
|
9373
|
+
*/
|
|
9374
|
+
async answerPlanApproval(answer, timeoutMs = this.o.planDialogWaitMs) {
|
|
9375
|
+
const deadline = Date.now() + timeoutMs;
|
|
9376
|
+
let dialog = this.planDialog;
|
|
9377
|
+
while (!dialog || dialog.id !== this.planApprovalToolUseId) {
|
|
9378
|
+
if (!this.turn || this.exited || Date.now() >= deadline) return false;
|
|
9379
|
+
if (Date.now() - this.lastDataAt >= this.o.permissionQuietMs) this.checkScreen();
|
|
9380
|
+
await delay(this.o.pollMs);
|
|
9381
|
+
dialog = this.planDialog;
|
|
9382
|
+
}
|
|
9383
|
+
const keys = planApprovalKeys(dialog.text);
|
|
9384
|
+
const key = answer.approved ? keys.approve : keys.reject;
|
|
9385
|
+
if (!key) return false;
|
|
9386
|
+
this.answeredPlans.add(dialog.id);
|
|
9387
|
+
this.planDialog = null;
|
|
9388
|
+
this.screen = "";
|
|
9389
|
+
this.write(key);
|
|
9390
|
+
if (answer.approved || !keys.rejectTakesText) return true;
|
|
9391
|
+
await this.waitForQuiet();
|
|
9392
|
+
const text = (answer.feedback ?? "").replace(/\s+/g, " ").trim() || "no";
|
|
9393
|
+
this.write(text);
|
|
9394
|
+
await delay(this.o.submitMinMs);
|
|
9395
|
+
this.write("\r");
|
|
9396
|
+
return true;
|
|
9397
|
+
}
|
|
9398
|
+
/** Wait until the PTY has been quiet for `permissionQuietMs`, at most 3 s. */
|
|
9399
|
+
async waitForQuiet() {
|
|
9400
|
+
const until = Date.now() + 3e3;
|
|
9401
|
+
await delay(Math.min(this.o.permissionQuietMs, 3e3));
|
|
9402
|
+
while (Date.now() < until && Date.now() - this.lastDataAt < this.o.permissionQuietMs) {
|
|
9403
|
+
await delay(25);
|
|
9404
|
+
}
|
|
9405
|
+
}
|
|
9189
9406
|
/**
|
|
9190
9407
|
* Stop the running turn the way the TUI's own Esc does, and keep the
|
|
9191
9408
|
* session alive for the next one. Resolves true once the turn ended, false
|
|
@@ -9314,7 +9531,14 @@ var ClaudeSession = class {
|
|
|
9314
9531
|
};
|
|
9315
9532
|
|
|
9316
9533
|
// src/claude-session-wrapper.ts
|
|
9317
|
-
|
|
9534
|
+
var IMAGE_EXTENSIONS = {
|
|
9535
|
+
"image/png": "png",
|
|
9536
|
+
"image/jpeg": "jpg",
|
|
9537
|
+
"image/jpg": "jpg",
|
|
9538
|
+
"image/gif": "gif",
|
|
9539
|
+
"image/webp": "webp"
|
|
9540
|
+
};
|
|
9541
|
+
function decodeUserEnvelope(chunk, saveImage) {
|
|
9318
9542
|
let parsed;
|
|
9319
9543
|
try {
|
|
9320
9544
|
parsed = JSON.parse(chunk);
|
|
@@ -9326,16 +9550,22 @@ function decodeUserEnvelope(chunk) {
|
|
|
9326
9550
|
if (typeof content === "string") return content;
|
|
9327
9551
|
if (!Array.isArray(content)) return chunk;
|
|
9328
9552
|
const parts = [];
|
|
9553
|
+
const images = [];
|
|
9329
9554
|
let dropped = 0;
|
|
9330
9555
|
for (const block of content) {
|
|
9556
|
+
const extension = block?.type === "image" && block.source?.type === "base64" && typeof block.source.data === "string" ? IMAGE_EXTENSIONS[String(block.source.media_type).toLowerCase()] : void 0;
|
|
9331
9557
|
if (block?.type === "text" && typeof block.text === "string") {
|
|
9332
9558
|
parts.push(block.text);
|
|
9559
|
+
} else if (extension && saveImage) {
|
|
9560
|
+
const file = saveImage(Buffer.from(block.source.data, "base64"), extension);
|
|
9561
|
+
if (file) images.push(file);
|
|
9562
|
+
else dropped++;
|
|
9333
9563
|
} else if (block?.type === "tool_result") {
|
|
9334
9564
|
const v = block.content;
|
|
9335
|
-
const
|
|
9565
|
+
const text2 = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
|
|
9336
9566
|
parts.push(
|
|
9337
9567
|
`[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
|
|
9338
|
-
${
|
|
9568
|
+
${text2}`
|
|
9339
9569
|
);
|
|
9340
9570
|
} else {
|
|
9341
9571
|
dropped++;
|
|
@@ -9346,10 +9576,61 @@ ${text}`
|
|
|
9346
9576
|
dropped
|
|
9347
9577
|
});
|
|
9348
9578
|
}
|
|
9349
|
-
|
|
9579
|
+
const text = parts.join("\n\n");
|
|
9580
|
+
return images.length > 0 ? [...images, text].filter(Boolean).join("\n") : text;
|
|
9581
|
+
}
|
|
9582
|
+
function stageImage(data, extension) {
|
|
9583
|
+
try {
|
|
9584
|
+
const file = join10(pluginTmpDir(), `image-${randomUUID5()}.${extension}`);
|
|
9585
|
+
writeFileSync5(file, data, { mode: 384, flag: "wx" });
|
|
9586
|
+
return file;
|
|
9587
|
+
} catch (err) {
|
|
9588
|
+
log.warn("interactive transport could not stage an image", {
|
|
9589
|
+
error: err instanceof Error ? err.message : String(err)
|
|
9590
|
+
});
|
|
9591
|
+
return null;
|
|
9592
|
+
}
|
|
9593
|
+
}
|
|
9594
|
+
var APPROVAL_WORDS = /^(?:y|yes|yep|yeah|ok|okay|sure|approve|approved|proceed|go|go ahead|do it|lgtm)[.!\s]*$/i;
|
|
9595
|
+
var HOST_ANNOTATIONS = /<(?:dcp-)?system-reminder>[\s\S]*?<\/(?:dcp-)?system-reminder>|<dcp-message-id>[^<]*<\/dcp-message-id>/g;
|
|
9596
|
+
function toolResultText(content) {
|
|
9597
|
+
if (typeof content === "string") return content;
|
|
9598
|
+
if (!Array.isArray(content)) return "";
|
|
9599
|
+
return content.map((item) => item?.type === "text" && typeof item.text === "string" ? item.text : "").join("\n");
|
|
9600
|
+
}
|
|
9601
|
+
function planApprovalAnswer(raw, toolUseId) {
|
|
9602
|
+
let parsed;
|
|
9603
|
+
try {
|
|
9604
|
+
parsed = JSON.parse(raw);
|
|
9605
|
+
} catch {
|
|
9606
|
+
}
|
|
9607
|
+
const content = parsed?.type === "user" ? parsed.message?.content : void 0;
|
|
9608
|
+
if (Array.isArray(content)) {
|
|
9609
|
+
const result = content.find(
|
|
9610
|
+
(block) => block?.type === "tool_result" && block.tool_use_id === toolUseId
|
|
9611
|
+
);
|
|
9612
|
+
if (result) {
|
|
9613
|
+
if (result.is_error !== true) return { approved: true };
|
|
9614
|
+
const text = toolResultText(result.content);
|
|
9615
|
+
const feedback = text.startsWith(REJECTED_EXIT_PLAN_MODE_PREFIX) ? text.slice(REJECTED_EXIT_PLAN_MODE_PREFIX.length) : text;
|
|
9616
|
+
return { approved: false, feedback: feedback.trim() || "no" };
|
|
9617
|
+
}
|
|
9618
|
+
}
|
|
9619
|
+
const typed = decodeUserEnvelope(raw).replace(HOST_ANNOTATIONS, "").trim();
|
|
9620
|
+
return APPROVAL_WORDS.test(typed) ? { approved: true } : { approved: false, feedback: typed || "no" };
|
|
9621
|
+
}
|
|
9622
|
+
function asidePrompt(question, history = []) {
|
|
9623
|
+
return [
|
|
9624
|
+
"(Side question, asked while the main task continues elsewhere. Answer it from this conversation alone, briefly and without using any tools. Do not continue or change the main task.)",
|
|
9625
|
+
...history.map((exchange) => `Earlier side question: ${exchange.question}
|
|
9626
|
+
Your answer: ${exchange.response}`),
|
|
9627
|
+
question.trim()
|
|
9628
|
+
].join("\n\n");
|
|
9350
9629
|
}
|
|
9351
9630
|
function interactiveExtraArgs(opts) {
|
|
9352
9631
|
const extraArgs = [];
|
|
9632
|
+
if (opts.tools) extraArgs.push("--tools", opts.tools.join(","));
|
|
9633
|
+
if (opts.restricted) extraArgs.push("--restricted");
|
|
9353
9634
|
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
9354
9635
|
extraArgs.push(
|
|
9355
9636
|
"--mcp-config",
|
|
@@ -9507,6 +9788,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9507
9788
|
if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
|
|
9508
9789
|
else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
|
|
9509
9790
|
else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
|
|
9791
|
+
else if (event.action === "parked") log.info("interactive transport is waiting on a plan approval", data);
|
|
9510
9792
|
else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
|
|
9511
9793
|
},
|
|
9512
9794
|
onExit: (code) => {
|
|
@@ -9537,7 +9819,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9537
9819
|
let cancelledThrough = 0;
|
|
9538
9820
|
let queued = 0;
|
|
9539
9821
|
let chain = Promise.resolve();
|
|
9540
|
-
const runTurn = async (userMsg, turnNumber) => {
|
|
9822
|
+
const runTurn = async (userMsg, turnNumber, images) => {
|
|
9541
9823
|
const current = () => turnNumber === writes;
|
|
9542
9824
|
const emit2 = (line) => {
|
|
9543
9825
|
if (!current()) return;
|
|
@@ -9610,12 +9892,62 @@ function spawnInteractiveProcess(opts) {
|
|
|
9610
9892
|
);
|
|
9611
9893
|
if (proc.listenerCount("error") > 0) proc.emit("error", e);
|
|
9612
9894
|
else lineEmitter.emit("close");
|
|
9895
|
+
} finally {
|
|
9896
|
+
for (const file of images) void unlink2(file).catch(() => {
|
|
9897
|
+
});
|
|
9613
9898
|
}
|
|
9614
9899
|
};
|
|
9615
|
-
const
|
|
9900
|
+
const answerPlan = async (answer) => {
|
|
9901
|
+
log.info("interactive transport answering a plan approval", {
|
|
9902
|
+
sessionId: session.sessionId,
|
|
9903
|
+
approved: answer.approved
|
|
9904
|
+
});
|
|
9905
|
+
const answered = await session.answerPlanApproval(answer).catch(() => false);
|
|
9906
|
+
if (answered) return;
|
|
9907
|
+
log.warn("interactive transport could not answer the plan approval; stopping the turn", {
|
|
9908
|
+
sessionId: session.sessionId
|
|
9909
|
+
});
|
|
9910
|
+
cancelledThrough = writes;
|
|
9911
|
+
await session.interrupt().catch(() => false);
|
|
9912
|
+
};
|
|
9913
|
+
const askAside = async (question, options) => {
|
|
9914
|
+
const forked = existsSync4(session.jsonlPath);
|
|
9915
|
+
const aside = new ClaudeSession({
|
|
9916
|
+
...opts.tuning,
|
|
9917
|
+
cwd: opts.cwd,
|
|
9918
|
+
cliPath: opts.cliPath,
|
|
9919
|
+
configDir: opts.configDir,
|
|
9920
|
+
model: opts.model,
|
|
9921
|
+
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
9922
|
+
extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk" }),
|
|
9923
|
+
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
9924
|
+
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
9925
|
+
env: opts.env,
|
|
9926
|
+
spawnPty: opts.spawnPty,
|
|
9927
|
+
signal: options.abortSignal,
|
|
9928
|
+
...forked ? { forkOf: session.sessionId } : {}
|
|
9929
|
+
});
|
|
9930
|
+
log.info("btw: asking a fork of the interactive session", {
|
|
9931
|
+
sessionId: session.sessionId,
|
|
9932
|
+
asideSessionId: aside.sessionId,
|
|
9933
|
+
forked
|
|
9934
|
+
});
|
|
9935
|
+
try {
|
|
9936
|
+
await aside.start();
|
|
9937
|
+
const result = await aside.ask(asidePrompt(question, options.history), options.timeoutMs);
|
|
9938
|
+
const text = result.text.trim();
|
|
9939
|
+
if (!text) throw new Error("the side question got no answer");
|
|
9940
|
+
return text;
|
|
9941
|
+
} finally {
|
|
9942
|
+
aside.dispose();
|
|
9943
|
+
void unlink2(aside.jsonlPath).catch(() => {
|
|
9944
|
+
});
|
|
9945
|
+
}
|
|
9946
|
+
};
|
|
9947
|
+
const enqueueTurn = (userMsg, images = []) => {
|
|
9616
9948
|
const turnNumber = ++writes;
|
|
9617
9949
|
queued++;
|
|
9618
|
-
chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
|
|
9950
|
+
chain = chain.then(() => runTurn(userMsg, turnNumber, images)).finally(() => {
|
|
9619
9951
|
queued--;
|
|
9620
9952
|
});
|
|
9621
9953
|
};
|
|
@@ -9624,7 +9956,18 @@ function spawnInteractiveProcess(opts) {
|
|
|
9624
9956
|
writable: true,
|
|
9625
9957
|
write(chunk) {
|
|
9626
9958
|
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
9627
|
-
|
|
9959
|
+
const planToolUseId = session.pendingPlanApproval;
|
|
9960
|
+
if (planToolUseId) {
|
|
9961
|
+
void answerPlan(planApprovalAnswer(raw, planToolUseId));
|
|
9962
|
+
return true;
|
|
9963
|
+
}
|
|
9964
|
+
const images = [];
|
|
9965
|
+
const text = decodeUserEnvelope(raw, (data, extension) => {
|
|
9966
|
+
const file = stageImage(data, extension);
|
|
9967
|
+
if (file) images.push(file);
|
|
9968
|
+
return file;
|
|
9969
|
+
});
|
|
9970
|
+
enqueueTurn(text, images);
|
|
9628
9971
|
return true;
|
|
9629
9972
|
},
|
|
9630
9973
|
end() {
|
|
@@ -9675,17 +10018,45 @@ function spawnInteractiveProcess(opts) {
|
|
|
9675
10018
|
cancelledThrough = writes;
|
|
9676
10019
|
return session.interrupt(timeoutMs);
|
|
9677
10020
|
},
|
|
9678
|
-
flushTranscript: () => session.flushTranscript()
|
|
10021
|
+
flushTranscript: () => session.flushTranscript(),
|
|
10022
|
+
planApprovalPending: () => session.pendingPlanApproval !== null,
|
|
10023
|
+
askAside
|
|
9679
10024
|
}
|
|
9680
10025
|
};
|
|
9681
10026
|
return ap;
|
|
9682
10027
|
}
|
|
9683
10028
|
|
|
10029
|
+
// src/transport.ts
|
|
10030
|
+
function requestedTransport(config, env = process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT) {
|
|
10031
|
+
if (config.transport !== void 0) {
|
|
10032
|
+
if (!["auto", "headless", "interactive"].includes(config.transport)) {
|
|
10033
|
+
throw new Error(`Unknown Claude transport: ${config.transport}`);
|
|
10034
|
+
}
|
|
10035
|
+
return config.transport;
|
|
10036
|
+
}
|
|
10037
|
+
const enabled = config.interactive ?? (env !== void 0 && !["", "0", "false", "no", "off"].includes(env.trim().toLowerCase()));
|
|
10038
|
+
return enabled ? "interactive" : "headless";
|
|
10039
|
+
}
|
|
10040
|
+
function hasInteractiveTransport() {
|
|
10041
|
+
const runtime = globalThis;
|
|
10042
|
+
return typeof runtime.Bun?.Terminal === "function";
|
|
10043
|
+
}
|
|
10044
|
+
async function selectTransport(requested, cliPath, available = hasInteractiveTransport()) {
|
|
10045
|
+
if (requested === "headless") return "headless";
|
|
10046
|
+
if (requested === "interactive") {
|
|
10047
|
+
if (!available) throw new Error("Interactive Claude transport requires Bun.Terminal. Run opencode under a Bun runtime with PTY support.");
|
|
10048
|
+
return "interactive";
|
|
10049
|
+
}
|
|
10050
|
+
if (await detectHeadlessSupport(cliPath) !== "unsupported") return "headless";
|
|
10051
|
+
if (!available) throw new Error("Claude no longer supports the headless flags, and Bun.Terminal is unavailable. Run opencode under a Bun runtime with PTY support.");
|
|
10052
|
+
return "interactive";
|
|
10053
|
+
}
|
|
10054
|
+
|
|
9684
10055
|
// src/prompts.ts
|
|
9685
|
-
import { readFileSync as readFileSync7, writeFileSync as
|
|
10056
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
9686
10057
|
import { homedir as homedir8 } from "os";
|
|
9687
|
-
import { randomUUID as
|
|
9688
|
-
import { dirname as dirname6, join as
|
|
10058
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
10059
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
9689
10060
|
function readPromptFileIfPresent(path11) {
|
|
9690
10061
|
try {
|
|
9691
10062
|
const content = readFileSync7(path11, "utf8").trim();
|
|
@@ -9697,7 +10068,7 @@ function readPromptFileIfPresent(path11) {
|
|
|
9697
10068
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
9698
10069
|
let dir = cwd;
|
|
9699
10070
|
while (true) {
|
|
9700
|
-
const content = readPromptFileIfPresent(
|
|
10071
|
+
const content = readPromptFileIfPresent(join11(dir, "AGENTS.md"));
|
|
9701
10072
|
if (content) return content;
|
|
9702
10073
|
const parent = dirname6(dir);
|
|
9703
10074
|
if (parent === dir) return void 0;
|
|
@@ -9806,8 +10177,8 @@ ${options.compressionSummary.trim()}`
|
|
|
9806
10177
|
for (const s of extraSystemContent) {
|
|
9807
10178
|
if (s.trim()) parts.push(s.trim());
|
|
9808
10179
|
}
|
|
9809
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
9810
|
-
const globalAgents = readPromptFileIfPresent(
|
|
10180
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join11(homedir8(), ".config");
|
|
10181
|
+
const globalAgents = readPromptFileIfPresent(join11(configRoot, "opencode", "AGENTS.md"));
|
|
9811
10182
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
9812
10183
|
const forwarded = extraSystemContent.join("\n\n");
|
|
9813
10184
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -9818,9 +10189,9 @@ ${options.compressionSummary.trim()}`
|
|
|
9818
10189
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
9819
10190
|
const content = parts.join("\n\n");
|
|
9820
10191
|
if (!content) return void 0;
|
|
9821
|
-
const path11 =
|
|
10192
|
+
const path11 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
|
|
9822
10193
|
try {
|
|
9823
|
-
|
|
10194
|
+
writeFileSync6(path11, content, { encoding: "utf8", mode: 384 });
|
|
9824
10195
|
return path11;
|
|
9825
10196
|
} catch (err) {
|
|
9826
10197
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -10435,7 +10806,6 @@ function decideMcpHotReload(input) {
|
|
|
10435
10806
|
if (!input.hasActiveProcess) return decide("no-process");
|
|
10436
10807
|
const previousHash = input.previousHash ?? null;
|
|
10437
10808
|
if (previousHash === input.currentHash) return decide("unchanged");
|
|
10438
|
-
if (input.interactive) return decide("skipped-interactive");
|
|
10439
10809
|
if (input.pendingProxyCalls > 0) return decide("deferred-proxy-calls");
|
|
10440
10810
|
if (input.turnInFlight) return decide("deferred-turn-in-flight");
|
|
10441
10811
|
if (input.planQuestionPending) return decide("deferred-plan-question");
|
|
@@ -11234,6 +11604,24 @@ function finishWithQuestionCall(state, call) {
|
|
|
11234
11604
|
} catch {
|
|
11235
11605
|
}
|
|
11236
11606
|
}
|
|
11607
|
+
function finishAwaitingPlanApproval(state) {
|
|
11608
|
+
if (state.controllerClosed) return;
|
|
11609
|
+
state.endTextBlock();
|
|
11610
|
+
state.controller.enqueue({
|
|
11611
|
+
type: "finish",
|
|
11612
|
+
finishReason: state.toFinishReason("stop"),
|
|
11613
|
+
usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
|
|
11614
|
+
providerMetadata: {
|
|
11615
|
+
"claude-code": state.resultMeta
|
|
11616
|
+
}
|
|
11617
|
+
});
|
|
11618
|
+
state.controllerClosed = true;
|
|
11619
|
+
state.cleanupTurn();
|
|
11620
|
+
try {
|
|
11621
|
+
state.controller.close();
|
|
11622
|
+
} catch {
|
|
11623
|
+
}
|
|
11624
|
+
}
|
|
11237
11625
|
function drainNow(state) {
|
|
11238
11626
|
if (state.drainTimer) {
|
|
11239
11627
|
clearTimeout(state.drainTimer);
|
|
@@ -11830,6 +12218,10 @@ ${plan}
|
|
|
11830
12218
|
`
|
|
11831
12219
|
});
|
|
11832
12220
|
state.endTextBlock();
|
|
12221
|
+
if (ctx.interactive) {
|
|
12222
|
+
finishAwaitingPlanApproval(state);
|
|
12223
|
+
return;
|
|
12224
|
+
}
|
|
11833
12225
|
} else if (isWebSearchTool(block2.name) && isWebSearchHandledByCli(ctx.config.webSearch)) {
|
|
11834
12226
|
state.toolCallsById.delete(block2.id);
|
|
11835
12227
|
const query = typeof parsedInput?.query === "string" ? parsedInput.query : JSON.stringify(parsedInput);
|
|
@@ -12549,8 +12941,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12549
12941
|
const toFinishReason2 = this.toFinishReason.bind(this);
|
|
12550
12942
|
const handleControlRequest2 = this.handleControlRequest.bind(this);
|
|
12551
12943
|
const flagOn = (v) => v !== void 0 && !["", "0", "false", "no", "off"].includes(v.trim().toLowerCase());
|
|
12552
|
-
const
|
|
12553
|
-
const
|
|
12944
|
+
const transport = requestedTransport(this.config);
|
|
12945
|
+
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
12946
|
+
const selectedTransport = doctor ? transport === "interactive" ? "interactive" : "headless" : this.config.transport === void 0 && !hasInteractiveTransport() ? "headless" : this.isTitleRequest(scope, options) && !compactionMode ? "headless" : await Promise.race([selectTransport(transport, this.config.cliPath), turnAbort.whenAborted]);
|
|
12947
|
+
if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
|
|
12948
|
+
const useInteractive = selectedTransport === "interactive";
|
|
12554
12949
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
12555
12950
|
const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
|
|
12556
12951
|
const sourceAccount = normalizeAccountName(
|
|
@@ -12566,7 +12961,6 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12566
12961
|
let cliPath = failover.cliPath;
|
|
12567
12962
|
if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
|
|
12568
12963
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
12569
|
-
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
12570
12964
|
if (doctor) {
|
|
12571
12965
|
const doctorOptions = {
|
|
12572
12966
|
cliPath,
|
|
@@ -12686,7 +13080,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12686
13080
|
request: { body: { text: "" } }
|
|
12687
13081
|
};
|
|
12688
13082
|
}
|
|
12689
|
-
if (!hasNewUserContent(options.prompt)) {
|
|
13083
|
+
if (!compactionMode && !hasNewUserContent(options.prompt)) {
|
|
12690
13084
|
log.info("doStream short-circuit: no new user content");
|
|
12691
13085
|
const stream2 = new ReadableStream({
|
|
12692
13086
|
start(controller) {
|
|
@@ -12836,7 +13230,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12836
13230
|
sessionKey: sk,
|
|
12837
13231
|
prompt: options.prompt,
|
|
12838
13232
|
cliPath,
|
|
12839
|
-
transcriptPath: (id) =>
|
|
13233
|
+
transcriptPath: (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
|
|
12840
13234
|
});
|
|
12841
13235
|
if (resumePoint) {
|
|
12842
13236
|
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
@@ -12859,10 +13253,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12859
13253
|
const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
|
|
12860
13254
|
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
|
|
12861
13255
|
compactionMode,
|
|
13256
|
+
compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
|
|
12862
13257
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
12863
13258
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
12864
13259
|
});
|
|
12865
|
-
const
|
|
13260
|
+
const ptyPlanMode = useInteractive && !compactionMode && this.config.permissionMode === "plan";
|
|
13261
|
+
const resolvedProxy = compactionMode ? null : ptyPlanMode ? this.resolvedProxyTools()?.filter((def) => def.name !== "write" && def.name !== "edit") ?? null : this.resolvedProxyTools();
|
|
12866
13262
|
const loadLiveToolInfo = this.createLiveToolInfoLoader();
|
|
12867
13263
|
const planModeQuestionActive = await this.resolvePlanModeQuestion(
|
|
12868
13264
|
compactionMode,
|
|
@@ -12898,6 +13294,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12898
13294
|
return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
|
|
12899
13295
|
}
|
|
12900
13296
|
const [runtimeStatus, cliVersion] = prologue.value;
|
|
13297
|
+
const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
|
|
13298
|
+
const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
|
|
13299
|
+
if (ptyReadOnly && !ptyReadOnlySupported) {
|
|
13300
|
+
throw new Error(
|
|
13301
|
+
"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."
|
|
13302
|
+
);
|
|
13303
|
+
}
|
|
12901
13304
|
const failoverAccounts = failoverCandidates(
|
|
12902
13305
|
this.config.failoverAccounts,
|
|
12903
13306
|
sourceAccount
|
|
@@ -13222,7 +13625,21 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13222
13625
|
allDisallowed
|
|
13223
13626
|
};
|
|
13224
13627
|
};
|
|
13225
|
-
if (useInteractive &&
|
|
13628
|
+
if (useInteractive && compactionMode) {
|
|
13629
|
+
const ap = spawnInteractiveProcess({
|
|
13630
|
+
cwd,
|
|
13631
|
+
cliPath,
|
|
13632
|
+
configDir: self.config.configDir,
|
|
13633
|
+
model: spawnModelId,
|
|
13634
|
+
fastMode,
|
|
13635
|
+
tools: [],
|
|
13636
|
+
mcpConfigPaths: ['{"mcpServers":{}}'],
|
|
13637
|
+
env: claudeSpawnEnv({ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey })
|
|
13638
|
+
});
|
|
13639
|
+
state.proc = ap.proc;
|
|
13640
|
+
state.lineEmitter = ap.lineEmitter;
|
|
13641
|
+
state.activeProcess = ap;
|
|
13642
|
+
} else if (useInteractive) {
|
|
13226
13643
|
if (state.activeProcess) {
|
|
13227
13644
|
state.proc = state.activeProcess.proc;
|
|
13228
13645
|
state.lineEmitter = state.activeProcess.lineEmitter;
|
|
@@ -13288,6 +13705,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13288
13705
|
});
|
|
13289
13706
|
if (stoppedBeforeWork()) return;
|
|
13290
13707
|
const resumeSessionId = getClaudeSessionId(sk);
|
|
13708
|
+
const posture = interactivePermissionPosture({
|
|
13709
|
+
permissionMode: self.config.permissionMode,
|
|
13710
|
+
allow,
|
|
13711
|
+
supportsReadOnly: ptyReadOnlySupported
|
|
13712
|
+
});
|
|
13713
|
+
if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
|
|
13291
13714
|
const ap = spawnInteractiveProcess({
|
|
13292
13715
|
cwd,
|
|
13293
13716
|
cliPath,
|
|
@@ -13296,7 +13719,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13296
13719
|
fastMode,
|
|
13297
13720
|
mcpConfigPaths: mcp.paths,
|
|
13298
13721
|
pluginDirs: skillPluginDirs,
|
|
13299
|
-
permissionsAllow: allow,
|
|
13722
|
+
permissionsAllow: posture.allow,
|
|
13723
|
+
permissionMode: posture.permissionMode,
|
|
13724
|
+
restricted: posture.restricted,
|
|
13300
13725
|
disallowedTools: wiring.allDisallowed,
|
|
13301
13726
|
proxyServer: state.proxyServer,
|
|
13302
13727
|
systemPromptFile,
|
|
@@ -13428,7 +13853,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13428
13853
|
state.activeProcess = ap;
|
|
13429
13854
|
}
|
|
13430
13855
|
}
|
|
13431
|
-
if (state.activeProcess && !hasMatchedPendingResults && isTurnInFlight(state.activeProcess)) {
|
|
13856
|
+
if (state.activeProcess && !hasMatchedPendingResults && !state.activeProcess.interactiveControl?.planApprovalPending?.() && isTurnInFlight(state.activeProcess)) {
|
|
13432
13857
|
log.warn("previous turn still in flight; interrupting it", { sk });
|
|
13433
13858
|
const idle = await interruptTurn(state.activeProcess);
|
|
13434
13859
|
if (!idle) {
|
|
@@ -13598,7 +14023,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13598
14023
|
});
|
|
13599
14024
|
state.controllerClosed = true;
|
|
13600
14025
|
cleanupTurn();
|
|
13601
|
-
if (!
|
|
14026
|
+
if (!compactionMode) {
|
|
13602
14027
|
scheduleIdleProcessEviction(
|
|
13603
14028
|
sk,
|
|
13604
14029
|
resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
|
|
@@ -13614,6 +14039,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13614
14039
|
compactionMode,
|
|
13615
14040
|
fastMode,
|
|
13616
14041
|
planModeQuestionActive,
|
|
14042
|
+
interactive: useInteractive,
|
|
13617
14043
|
sourceAccount,
|
|
13618
14044
|
failoverAskActive,
|
|
13619
14045
|
usageLimitNoteActive,
|
|
@@ -13706,6 +14132,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13706
14132
|
state.asideSinkUnregister?.();
|
|
13707
14133
|
state.asideSinkUnregister = null;
|
|
13708
14134
|
state.proc.off("error", procErrorHandler);
|
|
14135
|
+
if (compactionMode && useInteractive) state.proc.kill();
|
|
13709
14136
|
};
|
|
13710
14137
|
const procErrorHandler = (err) => {
|
|
13711
14138
|
log.error("process error", { error: err.message });
|
|
@@ -13921,10 +14348,10 @@ import {
|
|
|
13921
14348
|
readFileSync as readFileSync8,
|
|
13922
14349
|
realpathSync as realpathSync4,
|
|
13923
14350
|
rmSync as rmSync3,
|
|
13924
|
-
writeFileSync as
|
|
14351
|
+
writeFileSync as writeFileSync7
|
|
13925
14352
|
} from "fs";
|
|
13926
14353
|
import { homedir as homedir9 } from "os";
|
|
13927
|
-
import { dirname as dirname7, join as
|
|
14354
|
+
import { dirname as dirname7, join as join13, resolve as resolve5 } from "path";
|
|
13928
14355
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
13929
14356
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
13930
14357
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -13932,14 +14359,14 @@ var alreadyRan = false;
|
|
|
13932
14359
|
function candidateCacheRoots() {
|
|
13933
14360
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
13934
14361
|
return [
|
|
13935
|
-
xdg ?
|
|
13936
|
-
|
|
13937
|
-
|
|
14362
|
+
xdg ? join13(xdg, "opencode") : null,
|
|
14363
|
+
join13(homedir9(), ".cache", "opencode"),
|
|
14364
|
+
join13(homedir9(), "Library", "Caches", "opencode")
|
|
13938
14365
|
].filter((p) => Boolean(p));
|
|
13939
14366
|
}
|
|
13940
14367
|
function userOpencodeJsonPath() {
|
|
13941
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
13942
|
-
return
|
|
14368
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join13(homedir9(), ".config");
|
|
14369
|
+
return join13(xdgConfig, "opencode", "opencode.json");
|
|
13943
14370
|
}
|
|
13944
14371
|
function userIntendsToUseUnscoped() {
|
|
13945
14372
|
const cfg = userOpencodeJsonPath();
|
|
@@ -13966,15 +14393,15 @@ function ourLoadedDir() {
|
|
|
13966
14393
|
function ourVersion(dir) {
|
|
13967
14394
|
if (!dir) return null;
|
|
13968
14395
|
try {
|
|
13969
|
-
const pkg = JSON.parse(readFileSync8(
|
|
14396
|
+
const pkg = JSON.parse(readFileSync8(join13(dir, "package.json"), "utf8"));
|
|
13970
14397
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
13971
14398
|
} catch {
|
|
13972
14399
|
return null;
|
|
13973
14400
|
}
|
|
13974
14401
|
}
|
|
13975
14402
|
function markerPath() {
|
|
13976
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
13977
|
-
return
|
|
14403
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join13(homedir9(), ".local", "state");
|
|
14404
|
+
return join13(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
13978
14405
|
}
|
|
13979
14406
|
function markerVersion() {
|
|
13980
14407
|
try {
|
|
@@ -13988,7 +14415,7 @@ function writeMarker(version) {
|
|
|
13988
14415
|
const file = markerPath();
|
|
13989
14416
|
try {
|
|
13990
14417
|
mkdirSync5(dirname7(file), { recursive: true });
|
|
13991
|
-
|
|
14418
|
+
writeFileSync7(
|
|
13992
14419
|
file,
|
|
13993
14420
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
13994
14421
|
"utf8"
|
|
@@ -14023,7 +14450,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
14023
14450
|
}
|
|
14024
14451
|
function cleanupOne(cacheRoot, ourDir) {
|
|
14025
14452
|
if (!existsSync5(cacheRoot)) return;
|
|
14026
|
-
const stalePath =
|
|
14453
|
+
const stalePath = join13(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
14027
14454
|
if (!existsSync5(stalePath)) return;
|
|
14028
14455
|
let realStalePath = stalePath;
|
|
14029
14456
|
try {
|
|
@@ -14031,7 +14458,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14031
14458
|
} catch {
|
|
14032
14459
|
}
|
|
14033
14460
|
if (ourDir && realStalePath === ourDir) return;
|
|
14034
|
-
const pkgJsonPath =
|
|
14461
|
+
const pkgJsonPath = join13(stalePath, "package.json");
|
|
14035
14462
|
if (!existsSync5(pkgJsonPath)) return;
|
|
14036
14463
|
let pkg = {};
|
|
14037
14464
|
try {
|
|
@@ -14051,13 +14478,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14051
14478
|
});
|
|
14052
14479
|
return;
|
|
14053
14480
|
}
|
|
14054
|
-
const cachePkgJson =
|
|
14481
|
+
const cachePkgJson = join13(cacheRoot, "package.json");
|
|
14055
14482
|
if (!existsSync5(cachePkgJson)) return;
|
|
14056
14483
|
try {
|
|
14057
14484
|
const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
|
|
14058
14485
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
14059
14486
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
14060
|
-
|
|
14487
|
+
writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
14061
14488
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
14062
14489
|
}
|
|
14063
14490
|
} catch (err) {
|
|
@@ -14628,8 +15055,10 @@ function warnIfAnthropicApiKey(ignore) {
|
|
|
14628
15055
|
function _resetPlanModeWarningForTests() {
|
|
14629
15056
|
warnedPlanModeNoExit = false;
|
|
14630
15057
|
}
|
|
14631
|
-
function warnIfPlanModeCannotExit(permissionMode) {
|
|
15058
|
+
function warnIfPlanModeCannotExit(permissionMode, transport) {
|
|
14632
15059
|
if (permissionMode !== "plan") return;
|
|
15060
|
+
const interactive = transport?.transport === "interactive" || transport?.transport === void 0 && transport?.interactive === true;
|
|
15061
|
+
if (interactive) return;
|
|
14633
15062
|
if (warnedPlanModeNoExit) return;
|
|
14634
15063
|
warnedPlanModeNoExit = true;
|
|
14635
15064
|
log.warn(
|
|
@@ -14670,7 +15099,7 @@ function createClaudeCode(settings = {}) {
|
|
|
14670
15099
|
}
|
|
14671
15100
|
warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
|
|
14672
15101
|
const preset = applyPermissionPreset(settings, DEFAULT_PROXY_TOOL_NAMES);
|
|
14673
|
-
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode);
|
|
15102
|
+
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode, settings);
|
|
14674
15103
|
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
14675
15104
|
const providerName = settings.providerID ?? settings.name ?? "claude-code";
|
|
14676
15105
|
const proxyTools = preset?.proxyTools ?? settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|
|
@@ -14719,6 +15148,7 @@ function createClaudeCode(settings = {}) {
|
|
|
14719
15148
|
turnStats: settings.turnStats === true,
|
|
14720
15149
|
forkSessions: settings.forkSessions === true,
|
|
14721
15150
|
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
15151
|
+
transport: settings.transport,
|
|
14722
15152
|
interactive: settings.interactive,
|
|
14723
15153
|
interactiveBypass: settings.interactiveBypass,
|
|
14724
15154
|
interactiveAllowTools: settings.interactiveAllowTools,
|