@khalilgharbaoui/opencode-claude-code-plugin 0.41.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 +4 -1
- package/dist/index.js +388 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +24 -13
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 });
|
|
@@ -4533,6 +4537,20 @@ function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAUL
|
|
|
4533
4537
|
overrides: resolved.overridden
|
|
4534
4538
|
};
|
|
4535
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
|
+
}
|
|
4536
4554
|
function isReadOnlyPermissionMode(mode) {
|
|
4537
4555
|
return mode === READ_ONLY_PERMISSION_MODE;
|
|
4538
4556
|
}
|
|
@@ -4612,7 +4630,25 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
4612
4630
|
if (!question) return { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
4613
4631
|
options.abortSignal?.throwIfAborted();
|
|
4614
4632
|
const { proc, lineEmitter } = activeProcess;
|
|
4615
|
-
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) {
|
|
4616
4652
|
throw new Error("/btw requires the headless Claude Code transport; interactive sessions are not supported.");
|
|
4617
4653
|
}
|
|
4618
4654
|
if (!cliSupportsSideQuestion(options.cliVersion)) {
|
|
@@ -5762,6 +5798,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5762
5798
|
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5763
5799
|
"interactive transport cancelled the usage-limit auto-continue",
|
|
5764
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",
|
|
5765
5803
|
"interactive transport denied a permission prompt",
|
|
5766
5804
|
"interactive transport dropped non-text content blocks",
|
|
5767
5805
|
"interactive turn failed",
|
|
@@ -8554,8 +8592,10 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
8554
8592
|
|
|
8555
8593
|
// src/claude-session-wrapper.ts
|
|
8556
8594
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
8557
|
-
import {
|
|
8595
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
8596
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
8558
8597
|
import { unlink as unlink2 } from "fs/promises";
|
|
8598
|
+
import { join as join10 } from "path";
|
|
8559
8599
|
|
|
8560
8600
|
// src/claude-session-bun.ts
|
|
8561
8601
|
import * as os4 from "os";
|
|
@@ -8635,6 +8675,17 @@ var SCREEN_PATTERNS = [
|
|
|
8635
8675
|
"i"
|
|
8636
8676
|
)
|
|
8637
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
|
+
},
|
|
8638
8689
|
{
|
|
8639
8690
|
// The question alone is not enough: Claude can write "Do you want to
|
|
8640
8691
|
// proceed?" in a reply. The dialog always numbers its options.
|
|
@@ -8665,6 +8716,13 @@ function highlightedChoice(text) {
|
|
|
8665
8716
|
}
|
|
8666
8717
|
return last === "yes" || last === "no" ? last : null;
|
|
8667
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
|
+
}
|
|
8668
8726
|
function classifyScreen(text) {
|
|
8669
8727
|
for (const { kind, pattern } of SCREEN_PATTERNS) {
|
|
8670
8728
|
const match = pattern.exec(text);
|
|
@@ -8756,7 +8814,7 @@ var TurnUsageAccumulator = class {
|
|
|
8756
8814
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
8757
8815
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8758
8816
|
var RAW_KEEP_CHARS = 64 * 1024;
|
|
8759
|
-
var SCREEN_WINDOW_CHARS =
|
|
8817
|
+
var SCREEN_WINDOW_CHARS = 64 * 1024;
|
|
8760
8818
|
var INTERRUPT_MARKER = "[Request interrupted by user";
|
|
8761
8819
|
function isInterruptRecord(rec) {
|
|
8762
8820
|
if (!rec || rec.type !== "user" || !rec.message) return false;
|
|
@@ -8770,6 +8828,11 @@ function isInterruptRecord(rec) {
|
|
|
8770
8828
|
function isTurnDurationRecord(rec) {
|
|
8771
8829
|
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
8772
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
|
+
}
|
|
8773
8836
|
function resolveConfigDir(configDir) {
|
|
8774
8837
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8775
8838
|
if (!value) return path9.join(os4.homedir(), ".claude");
|
|
@@ -8804,6 +8867,10 @@ var ClaudeSession = class {
|
|
|
8804
8867
|
aborted = false;
|
|
8805
8868
|
signal;
|
|
8806
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;
|
|
8807
8874
|
env;
|
|
8808
8875
|
onScreen;
|
|
8809
8876
|
onExit;
|
|
@@ -8822,6 +8889,15 @@ var ClaudeSession = class {
|
|
|
8822
8889
|
/** The running turn's read state, shared with `flushTranscript`. */
|
|
8823
8890
|
scan = null;
|
|
8824
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();
|
|
8825
8901
|
abandonTurn = false;
|
|
8826
8902
|
o;
|
|
8827
8903
|
constructor(opts = {}) {
|
|
@@ -8829,7 +8905,9 @@ var ClaudeSession = class {
|
|
|
8829
8905
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
8830
8906
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
8831
8907
|
this.signal = opts.signal;
|
|
8832
|
-
this.resumeSessionId = opts.resumeSessionId;
|
|
8908
|
+
this.resumeSessionId = opts.forkOf ? void 0 : opts.resumeSessionId;
|
|
8909
|
+
this.forkOf = opts.forkOf;
|
|
8910
|
+
this.forkPending = !!opts.forkOf;
|
|
8833
8911
|
this.env = opts.env;
|
|
8834
8912
|
this.onScreen = opts.onScreen;
|
|
8835
8913
|
this.onExit = opts.onExit;
|
|
@@ -8872,12 +8950,16 @@ var ClaudeSession = class {
|
|
|
8872
8950
|
// Shorter than `bootQuietMs`, so a trust dialog is answered before boot
|
|
8873
8951
|
// could declare the TUI ready and paste a prompt into it.
|
|
8874
8952
|
permissionQuietMs: opts.permissionQuietMs ?? 1e3,
|
|
8875
|
-
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
|
|
8876
8958
|
};
|
|
8877
8959
|
}
|
|
8878
8960
|
/** The CLI arguments this session spawns with. */
|
|
8879
8961
|
spawnArgs() {
|
|
8880
|
-
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];
|
|
8881
8963
|
if (this.o.model) args.push("--model", this.o.model);
|
|
8882
8964
|
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8883
8965
|
args.push("--setting-sources", this.o.settingSources);
|
|
@@ -8946,6 +9028,9 @@ var ClaudeSession = class {
|
|
|
8946
9028
|
this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
|
|
8947
9029
|
this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
|
|
8948
9030
|
if (this.o.debug) process.stdout.write(chunk);
|
|
9031
|
+
this.scheduleScreenCheck();
|
|
9032
|
+
}
|
|
9033
|
+
scheduleScreenCheck() {
|
|
8949
9034
|
this.clearScreenTimer();
|
|
8950
9035
|
this.screenTimer = setTimeout(() => {
|
|
8951
9036
|
this.screenTimer = null;
|
|
@@ -9001,6 +9086,13 @@ var ClaudeSession = class {
|
|
|
9001
9086
|
this.write("\x1B");
|
|
9002
9087
|
action = "cancelled";
|
|
9003
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
|
+
}
|
|
9004
9096
|
case "login":
|
|
9005
9097
|
case "onboarding":
|
|
9006
9098
|
action = "fatal";
|
|
@@ -9139,8 +9231,10 @@ var ClaudeSession = class {
|
|
|
9139
9231
|
stopSeenAt: 0,
|
|
9140
9232
|
end: null,
|
|
9141
9233
|
sawUserRecord: false,
|
|
9142
|
-
lastBeat: Date.now()
|
|
9234
|
+
lastBeat: Date.now(),
|
|
9235
|
+
awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
|
|
9143
9236
|
};
|
|
9237
|
+
this.forkPending = false;
|
|
9144
9238
|
this.scan = scan;
|
|
9145
9239
|
const deadline = Date.now() + timeout;
|
|
9146
9240
|
while (Date.now() < deadline) {
|
|
@@ -9174,6 +9268,9 @@ var ClaudeSession = class {
|
|
|
9174
9268
|
this.turn = null;
|
|
9175
9269
|
this.scan = null;
|
|
9176
9270
|
this.abandonTurn = false;
|
|
9271
|
+
this.planApprovalToolUseId = null;
|
|
9272
|
+
this.planDialog = null;
|
|
9273
|
+
this.answeredPlans.clear();
|
|
9177
9274
|
settle();
|
|
9178
9275
|
}
|
|
9179
9276
|
}
|
|
@@ -9199,6 +9296,14 @@ var ClaudeSession = class {
|
|
|
9199
9296
|
rec = JSON.parse(s);
|
|
9200
9297
|
} catch {
|
|
9201
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);
|
|
9202
9307
|
scan.onRecord(s, rec);
|
|
9203
9308
|
if (rec) {
|
|
9204
9309
|
if (rec.type === "assistant" && rec.message) {
|
|
@@ -9230,6 +9335,74 @@ var ClaudeSession = class {
|
|
|
9230
9335
|
flushTranscript() {
|
|
9231
9336
|
this.scanTranscript();
|
|
9232
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
|
+
}
|
|
9233
9406
|
/**
|
|
9234
9407
|
* Stop the running turn the way the TUI's own Esc does, and keep the
|
|
9235
9408
|
* session alive for the next one. Resolves true once the turn ended, false
|
|
@@ -9358,7 +9531,14 @@ var ClaudeSession = class {
|
|
|
9358
9531
|
};
|
|
9359
9532
|
|
|
9360
9533
|
// src/claude-session-wrapper.ts
|
|
9361
|
-
|
|
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) {
|
|
9362
9542
|
let parsed;
|
|
9363
9543
|
try {
|
|
9364
9544
|
parsed = JSON.parse(chunk);
|
|
@@ -9370,16 +9550,22 @@ function decodeUserEnvelope(chunk) {
|
|
|
9370
9550
|
if (typeof content === "string") return content;
|
|
9371
9551
|
if (!Array.isArray(content)) return chunk;
|
|
9372
9552
|
const parts = [];
|
|
9553
|
+
const images = [];
|
|
9373
9554
|
let dropped = 0;
|
|
9374
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;
|
|
9375
9557
|
if (block?.type === "text" && typeof block.text === "string") {
|
|
9376
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++;
|
|
9377
9563
|
} else if (block?.type === "tool_result") {
|
|
9378
9564
|
const v = block.content;
|
|
9379
|
-
const
|
|
9565
|
+
const text2 = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
|
|
9380
9566
|
parts.push(
|
|
9381
9567
|
`[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
|
|
9382
|
-
${
|
|
9568
|
+
${text2}`
|
|
9383
9569
|
);
|
|
9384
9570
|
} else {
|
|
9385
9571
|
dropped++;
|
|
@@ -9390,11 +9576,61 @@ ${text}`
|
|
|
9390
9576
|
dropped
|
|
9391
9577
|
});
|
|
9392
9578
|
}
|
|
9393
|
-
|
|
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");
|
|
9394
9629
|
}
|
|
9395
9630
|
function interactiveExtraArgs(opts) {
|
|
9396
9631
|
const extraArgs = [];
|
|
9397
9632
|
if (opts.tools) extraArgs.push("--tools", opts.tools.join(","));
|
|
9633
|
+
if (opts.restricted) extraArgs.push("--restricted");
|
|
9398
9634
|
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
9399
9635
|
extraArgs.push(
|
|
9400
9636
|
"--mcp-config",
|
|
@@ -9552,6 +9788,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9552
9788
|
if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
|
|
9553
9789
|
else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
|
|
9554
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);
|
|
9555
9792
|
else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
|
|
9556
9793
|
},
|
|
9557
9794
|
onExit: (code) => {
|
|
@@ -9582,7 +9819,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9582
9819
|
let cancelledThrough = 0;
|
|
9583
9820
|
let queued = 0;
|
|
9584
9821
|
let chain = Promise.resolve();
|
|
9585
|
-
const runTurn = async (userMsg, turnNumber) => {
|
|
9822
|
+
const runTurn = async (userMsg, turnNumber, images) => {
|
|
9586
9823
|
const current = () => turnNumber === writes;
|
|
9587
9824
|
const emit2 = (line) => {
|
|
9588
9825
|
if (!current()) return;
|
|
@@ -9655,12 +9892,62 @@ function spawnInteractiveProcess(opts) {
|
|
|
9655
9892
|
);
|
|
9656
9893
|
if (proc.listenerCount("error") > 0) proc.emit("error", e);
|
|
9657
9894
|
else lineEmitter.emit("close");
|
|
9895
|
+
} finally {
|
|
9896
|
+
for (const file of images) void unlink2(file).catch(() => {
|
|
9897
|
+
});
|
|
9898
|
+
}
|
|
9899
|
+
};
|
|
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
|
+
});
|
|
9658
9945
|
}
|
|
9659
9946
|
};
|
|
9660
|
-
const enqueueTurn = (userMsg) => {
|
|
9947
|
+
const enqueueTurn = (userMsg, images = []) => {
|
|
9661
9948
|
const turnNumber = ++writes;
|
|
9662
9949
|
queued++;
|
|
9663
|
-
chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
|
|
9950
|
+
chain = chain.then(() => runTurn(userMsg, turnNumber, images)).finally(() => {
|
|
9664
9951
|
queued--;
|
|
9665
9952
|
});
|
|
9666
9953
|
};
|
|
@@ -9669,7 +9956,18 @@ function spawnInteractiveProcess(opts) {
|
|
|
9669
9956
|
writable: true,
|
|
9670
9957
|
write(chunk) {
|
|
9671
9958
|
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
9672
|
-
|
|
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);
|
|
9673
9971
|
return true;
|
|
9674
9972
|
},
|
|
9675
9973
|
end() {
|
|
@@ -9720,7 +10018,9 @@ function spawnInteractiveProcess(opts) {
|
|
|
9720
10018
|
cancelledThrough = writes;
|
|
9721
10019
|
return session.interrupt(timeoutMs);
|
|
9722
10020
|
},
|
|
9723
|
-
flushTranscript: () => session.flushTranscript()
|
|
10021
|
+
flushTranscript: () => session.flushTranscript(),
|
|
10022
|
+
planApprovalPending: () => session.pendingPlanApproval !== null,
|
|
10023
|
+
askAside
|
|
9724
10024
|
}
|
|
9725
10025
|
};
|
|
9726
10026
|
return ap;
|
|
@@ -9753,10 +10053,10 @@ async function selectTransport(requested, cliPath, available = hasInteractiveTra
|
|
|
9753
10053
|
}
|
|
9754
10054
|
|
|
9755
10055
|
// src/prompts.ts
|
|
9756
|
-
import { readFileSync as readFileSync7, writeFileSync as
|
|
10056
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
9757
10057
|
import { homedir as homedir8 } from "os";
|
|
9758
|
-
import { randomUUID as
|
|
9759
|
-
import { dirname as dirname6, join as
|
|
10058
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
10059
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
9760
10060
|
function readPromptFileIfPresent(path11) {
|
|
9761
10061
|
try {
|
|
9762
10062
|
const content = readFileSync7(path11, "utf8").trim();
|
|
@@ -9768,7 +10068,7 @@ function readPromptFileIfPresent(path11) {
|
|
|
9768
10068
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
9769
10069
|
let dir = cwd;
|
|
9770
10070
|
while (true) {
|
|
9771
|
-
const content = readPromptFileIfPresent(
|
|
10071
|
+
const content = readPromptFileIfPresent(join11(dir, "AGENTS.md"));
|
|
9772
10072
|
if (content) return content;
|
|
9773
10073
|
const parent = dirname6(dir);
|
|
9774
10074
|
if (parent === dir) return void 0;
|
|
@@ -9877,8 +10177,8 @@ ${options.compressionSummary.trim()}`
|
|
|
9877
10177
|
for (const s of extraSystemContent) {
|
|
9878
10178
|
if (s.trim()) parts.push(s.trim());
|
|
9879
10179
|
}
|
|
9880
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
9881
|
-
const globalAgents = readPromptFileIfPresent(
|
|
10180
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join11(homedir8(), ".config");
|
|
10181
|
+
const globalAgents = readPromptFileIfPresent(join11(configRoot, "opencode", "AGENTS.md"));
|
|
9882
10182
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
9883
10183
|
const forwarded = extraSystemContent.join("\n\n");
|
|
9884
10184
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -9889,9 +10189,9 @@ ${options.compressionSummary.trim()}`
|
|
|
9889
10189
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
9890
10190
|
const content = parts.join("\n\n");
|
|
9891
10191
|
if (!content) return void 0;
|
|
9892
|
-
const path11 =
|
|
10192
|
+
const path11 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
|
|
9893
10193
|
try {
|
|
9894
|
-
|
|
10194
|
+
writeFileSync6(path11, content, { encoding: "utf8", mode: 384 });
|
|
9895
10195
|
return path11;
|
|
9896
10196
|
} catch (err) {
|
|
9897
10197
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -10506,7 +10806,6 @@ function decideMcpHotReload(input) {
|
|
|
10506
10806
|
if (!input.hasActiveProcess) return decide("no-process");
|
|
10507
10807
|
const previousHash = input.previousHash ?? null;
|
|
10508
10808
|
if (previousHash === input.currentHash) return decide("unchanged");
|
|
10509
|
-
if (input.interactive) return decide("skipped-interactive");
|
|
10510
10809
|
if (input.pendingProxyCalls > 0) return decide("deferred-proxy-calls");
|
|
10511
10810
|
if (input.turnInFlight) return decide("deferred-turn-in-flight");
|
|
10512
10811
|
if (input.planQuestionPending) return decide("deferred-plan-question");
|
|
@@ -11305,6 +11604,24 @@ function finishWithQuestionCall(state, call) {
|
|
|
11305
11604
|
} catch {
|
|
11306
11605
|
}
|
|
11307
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
|
+
}
|
|
11308
11625
|
function drainNow(state) {
|
|
11309
11626
|
if (state.drainTimer) {
|
|
11310
11627
|
clearTimeout(state.drainTimer);
|
|
@@ -11901,6 +12218,10 @@ ${plan}
|
|
|
11901
12218
|
`
|
|
11902
12219
|
});
|
|
11903
12220
|
state.endTextBlock();
|
|
12221
|
+
if (ctx.interactive) {
|
|
12222
|
+
finishAwaitingPlanApproval(state);
|
|
12223
|
+
return;
|
|
12224
|
+
}
|
|
11904
12225
|
} else if (isWebSearchTool(block2.name) && isWebSearchHandledByCli(ctx.config.webSearch)) {
|
|
11905
12226
|
state.toolCallsById.delete(block2.id);
|
|
11906
12227
|
const query = typeof parsedInput?.query === "string" ? parsedInput.query : JSON.stringify(parsedInput);
|
|
@@ -12625,9 +12946,6 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12625
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]);
|
|
12626
12947
|
if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
|
|
12627
12948
|
const useInteractive = selectedTransport === "interactive";
|
|
12628
|
-
if (useInteractive && !compactionMode && !doctor && (this.config.permissionPreset === "read-only" || this.config.permissionMode === "plan")) {
|
|
12629
|
-
throw new Error("Interactive Claude transport cannot enforce the read-only preset or plan mode. Use the headless transport for this permission posture.");
|
|
12630
|
-
}
|
|
12631
12949
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
12632
12950
|
const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
|
|
12633
12951
|
const sourceAccount = normalizeAccountName(
|
|
@@ -12912,7 +13230,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12912
13230
|
sessionKey: sk,
|
|
12913
13231
|
prompt: options.prompt,
|
|
12914
13232
|
cliPath,
|
|
12915
|
-
transcriptPath: (id) =>
|
|
13233
|
+
transcriptPath: (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
|
|
12916
13234
|
});
|
|
12917
13235
|
if (resumePoint) {
|
|
12918
13236
|
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
@@ -12939,7 +13257,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12939
13257
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
12940
13258
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
12941
13259
|
});
|
|
12942
|
-
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();
|
|
12943
13262
|
const loadLiveToolInfo = this.createLiveToolInfoLoader();
|
|
12944
13263
|
const planModeQuestionActive = await this.resolvePlanModeQuestion(
|
|
12945
13264
|
compactionMode,
|
|
@@ -12975,6 +13294,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12975
13294
|
return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
|
|
12976
13295
|
}
|
|
12977
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
|
+
}
|
|
12978
13304
|
const failoverAccounts = failoverCandidates(
|
|
12979
13305
|
this.config.failoverAccounts,
|
|
12980
13306
|
sourceAccount
|
|
@@ -13379,6 +13705,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13379
13705
|
});
|
|
13380
13706
|
if (stoppedBeforeWork()) return;
|
|
13381
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");
|
|
13382
13714
|
const ap = spawnInteractiveProcess({
|
|
13383
13715
|
cwd,
|
|
13384
13716
|
cliPath,
|
|
@@ -13387,7 +13719,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13387
13719
|
fastMode,
|
|
13388
13720
|
mcpConfigPaths: mcp.paths,
|
|
13389
13721
|
pluginDirs: skillPluginDirs,
|
|
13390
|
-
permissionsAllow: allow,
|
|
13722
|
+
permissionsAllow: posture.allow,
|
|
13723
|
+
permissionMode: posture.permissionMode,
|
|
13724
|
+
restricted: posture.restricted,
|
|
13391
13725
|
disallowedTools: wiring.allDisallowed,
|
|
13392
13726
|
proxyServer: state.proxyServer,
|
|
13393
13727
|
systemPromptFile,
|
|
@@ -13519,7 +13853,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13519
13853
|
state.activeProcess = ap;
|
|
13520
13854
|
}
|
|
13521
13855
|
}
|
|
13522
|
-
if (state.activeProcess && !hasMatchedPendingResults && isTurnInFlight(state.activeProcess)) {
|
|
13856
|
+
if (state.activeProcess && !hasMatchedPendingResults && !state.activeProcess.interactiveControl?.planApprovalPending?.() && isTurnInFlight(state.activeProcess)) {
|
|
13523
13857
|
log.warn("previous turn still in flight; interrupting it", { sk });
|
|
13524
13858
|
const idle = await interruptTurn(state.activeProcess);
|
|
13525
13859
|
if (!idle) {
|
|
@@ -13689,7 +14023,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13689
14023
|
});
|
|
13690
14024
|
state.controllerClosed = true;
|
|
13691
14025
|
cleanupTurn();
|
|
13692
|
-
if (!
|
|
14026
|
+
if (!compactionMode) {
|
|
13693
14027
|
scheduleIdleProcessEviction(
|
|
13694
14028
|
sk,
|
|
13695
14029
|
resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
|
|
@@ -13705,6 +14039,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13705
14039
|
compactionMode,
|
|
13706
14040
|
fastMode,
|
|
13707
14041
|
planModeQuestionActive,
|
|
14042
|
+
interactive: useInteractive,
|
|
13708
14043
|
sourceAccount,
|
|
13709
14044
|
failoverAskActive,
|
|
13710
14045
|
usageLimitNoteActive,
|
|
@@ -14013,10 +14348,10 @@ import {
|
|
|
14013
14348
|
readFileSync as readFileSync8,
|
|
14014
14349
|
realpathSync as realpathSync4,
|
|
14015
14350
|
rmSync as rmSync3,
|
|
14016
|
-
writeFileSync as
|
|
14351
|
+
writeFileSync as writeFileSync7
|
|
14017
14352
|
} from "fs";
|
|
14018
14353
|
import { homedir as homedir9 } from "os";
|
|
14019
|
-
import { dirname as dirname7, join as
|
|
14354
|
+
import { dirname as dirname7, join as join13, resolve as resolve5 } from "path";
|
|
14020
14355
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
14021
14356
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
14022
14357
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -14024,14 +14359,14 @@ var alreadyRan = false;
|
|
|
14024
14359
|
function candidateCacheRoots() {
|
|
14025
14360
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
14026
14361
|
return [
|
|
14027
|
-
xdg ?
|
|
14028
|
-
|
|
14029
|
-
|
|
14362
|
+
xdg ? join13(xdg, "opencode") : null,
|
|
14363
|
+
join13(homedir9(), ".cache", "opencode"),
|
|
14364
|
+
join13(homedir9(), "Library", "Caches", "opencode")
|
|
14030
14365
|
].filter((p) => Boolean(p));
|
|
14031
14366
|
}
|
|
14032
14367
|
function userOpencodeJsonPath() {
|
|
14033
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
14034
|
-
return
|
|
14368
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join13(homedir9(), ".config");
|
|
14369
|
+
return join13(xdgConfig, "opencode", "opencode.json");
|
|
14035
14370
|
}
|
|
14036
14371
|
function userIntendsToUseUnscoped() {
|
|
14037
14372
|
const cfg = userOpencodeJsonPath();
|
|
@@ -14058,15 +14393,15 @@ function ourLoadedDir() {
|
|
|
14058
14393
|
function ourVersion(dir) {
|
|
14059
14394
|
if (!dir) return null;
|
|
14060
14395
|
try {
|
|
14061
|
-
const pkg = JSON.parse(readFileSync8(
|
|
14396
|
+
const pkg = JSON.parse(readFileSync8(join13(dir, "package.json"), "utf8"));
|
|
14062
14397
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
14063
14398
|
} catch {
|
|
14064
14399
|
return null;
|
|
14065
14400
|
}
|
|
14066
14401
|
}
|
|
14067
14402
|
function markerPath() {
|
|
14068
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
14069
|
-
return
|
|
14403
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join13(homedir9(), ".local", "state");
|
|
14404
|
+
return join13(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
14070
14405
|
}
|
|
14071
14406
|
function markerVersion() {
|
|
14072
14407
|
try {
|
|
@@ -14080,7 +14415,7 @@ function writeMarker(version) {
|
|
|
14080
14415
|
const file = markerPath();
|
|
14081
14416
|
try {
|
|
14082
14417
|
mkdirSync5(dirname7(file), { recursive: true });
|
|
14083
|
-
|
|
14418
|
+
writeFileSync7(
|
|
14084
14419
|
file,
|
|
14085
14420
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
14086
14421
|
"utf8"
|
|
@@ -14115,7 +14450,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
14115
14450
|
}
|
|
14116
14451
|
function cleanupOne(cacheRoot, ourDir) {
|
|
14117
14452
|
if (!existsSync5(cacheRoot)) return;
|
|
14118
|
-
const stalePath =
|
|
14453
|
+
const stalePath = join13(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
14119
14454
|
if (!existsSync5(stalePath)) return;
|
|
14120
14455
|
let realStalePath = stalePath;
|
|
14121
14456
|
try {
|
|
@@ -14123,7 +14458,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14123
14458
|
} catch {
|
|
14124
14459
|
}
|
|
14125
14460
|
if (ourDir && realStalePath === ourDir) return;
|
|
14126
|
-
const pkgJsonPath =
|
|
14461
|
+
const pkgJsonPath = join13(stalePath, "package.json");
|
|
14127
14462
|
if (!existsSync5(pkgJsonPath)) return;
|
|
14128
14463
|
let pkg = {};
|
|
14129
14464
|
try {
|
|
@@ -14143,13 +14478,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14143
14478
|
});
|
|
14144
14479
|
return;
|
|
14145
14480
|
}
|
|
14146
|
-
const cachePkgJson =
|
|
14481
|
+
const cachePkgJson = join13(cacheRoot, "package.json");
|
|
14147
14482
|
if (!existsSync5(cachePkgJson)) return;
|
|
14148
14483
|
try {
|
|
14149
14484
|
const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
|
|
14150
14485
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
14151
14486
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
14152
|
-
|
|
14487
|
+
writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
14153
14488
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
14154
14489
|
}
|
|
14155
14490
|
} catch (err) {
|
|
@@ -14720,8 +15055,10 @@ function warnIfAnthropicApiKey(ignore) {
|
|
|
14720
15055
|
function _resetPlanModeWarningForTests() {
|
|
14721
15056
|
warnedPlanModeNoExit = false;
|
|
14722
15057
|
}
|
|
14723
|
-
function warnIfPlanModeCannotExit(permissionMode) {
|
|
15058
|
+
function warnIfPlanModeCannotExit(permissionMode, transport) {
|
|
14724
15059
|
if (permissionMode !== "plan") return;
|
|
15060
|
+
const interactive = transport?.transport === "interactive" || transport?.transport === void 0 && transport?.interactive === true;
|
|
15061
|
+
if (interactive) return;
|
|
14725
15062
|
if (warnedPlanModeNoExit) return;
|
|
14726
15063
|
warnedPlanModeNoExit = true;
|
|
14727
15064
|
log.warn(
|
|
@@ -14762,7 +15099,7 @@ function createClaudeCode(settings = {}) {
|
|
|
14762
15099
|
}
|
|
14763
15100
|
warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
|
|
14764
15101
|
const preset = applyPermissionPreset(settings, DEFAULT_PROXY_TOOL_NAMES);
|
|
14765
|
-
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode);
|
|
15102
|
+
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode, settings);
|
|
14766
15103
|
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
14767
15104
|
const providerName = settings.providerID ?? settings.name ?? "claude-code";
|
|
14768
15105
|
const proxyTools = preset?.proxyTools ?? settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|