@khalilgharbaoui/opencode-claude-code-plugin 0.41.0 → 0.42.1
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 +467 -57
- 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 });
|
|
@@ -3779,12 +3783,16 @@ async function fetchSessionParentId(sessionID) {
|
|
|
3779
3783
|
return void 0;
|
|
3780
3784
|
}
|
|
3781
3785
|
}
|
|
3782
|
-
async function fetchSessionRunState(sessionID) {
|
|
3786
|
+
async function fetchSessionRunState(sessionID, directory) {
|
|
3783
3787
|
if (!sessionID || sessionID === "default") return "unknown";
|
|
3784
3788
|
const status = opencodeClient?.session?.status;
|
|
3785
3789
|
if (!status) return "unknown";
|
|
3786
3790
|
try {
|
|
3787
|
-
const
|
|
3791
|
+
const dir = directory ?? await fetchSessionDirectory(sessionID);
|
|
3792
|
+
const res = await status.call(
|
|
3793
|
+
opencodeClient.session,
|
|
3794
|
+
dir ? { query: { directory: dir } } : void 0
|
|
3795
|
+
);
|
|
3788
3796
|
const data = res.data;
|
|
3789
3797
|
if (!data || typeof data !== "object") return "unknown";
|
|
3790
3798
|
const entry = data[sessionID];
|
|
@@ -3804,8 +3812,10 @@ async function settleSessionRunState(sessionID, options = {}) {
|
|
|
3804
3812
|
const windowMs = options.windowMs ?? 1500;
|
|
3805
3813
|
const started = Date.now();
|
|
3806
3814
|
let last = "unknown";
|
|
3815
|
+
let directory = options.directory;
|
|
3807
3816
|
for (; ; ) {
|
|
3808
|
-
|
|
3817
|
+
directory ??= await fetchSessionDirectory(sessionID);
|
|
3818
|
+
const state = await fetchSessionRunState(sessionID, directory ?? "");
|
|
3809
3819
|
if (state === "busy") return "busy";
|
|
3810
3820
|
if (state !== "unknown") last = state;
|
|
3811
3821
|
if (Date.now() - started >= windowMs) return last;
|
|
@@ -4533,6 +4543,20 @@ function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAUL
|
|
|
4533
4543
|
overrides: resolved.overridden
|
|
4534
4544
|
};
|
|
4535
4545
|
}
|
|
4546
|
+
function interactivePermissionPosture(input) {
|
|
4547
|
+
if (!isReadOnlyPermissionMode(input.permissionMode)) {
|
|
4548
|
+
return { permissionMode: input.permissionMode, restricted: false, allow: input.allow };
|
|
4549
|
+
}
|
|
4550
|
+
if (!input.supportsReadOnly) return null;
|
|
4551
|
+
const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
|
|
4552
|
+
return {
|
|
4553
|
+
permissionMode: "dontAsk",
|
|
4554
|
+
restricted: true,
|
|
4555
|
+
allow: input.allow.filter(
|
|
4556
|
+
(rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
|
|
4557
|
+
)
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
4536
4560
|
function isReadOnlyPermissionMode(mode) {
|
|
4537
4561
|
return mode === READ_ONLY_PERMISSION_MODE;
|
|
4538
4562
|
}
|
|
@@ -4612,7 +4636,25 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
4612
4636
|
if (!question) return { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
4613
4637
|
options.abortSignal?.throwIfAborted();
|
|
4614
4638
|
const { proc, lineEmitter } = activeProcess;
|
|
4615
|
-
if (options.interactive
|
|
4639
|
+
if (options.interactive) {
|
|
4640
|
+
const askAside = activeProcess.interactiveControl?.askAside;
|
|
4641
|
+
if (!askAside) throw new Error("/btw needs a live interactive Claude Code session.");
|
|
4642
|
+
if (pendingProcesses.has(proc)) {
|
|
4643
|
+
throw new Error("Wait for the current /btw to finish before asking another.");
|
|
4644
|
+
}
|
|
4645
|
+
pendingProcesses.add(proc);
|
|
4646
|
+
try {
|
|
4647
|
+
const response = await askAside(question, {
|
|
4648
|
+
...options.history === void 0 ? {} : { history: options.history },
|
|
4649
|
+
timeoutMs: options.timeoutMs ?? 12e4,
|
|
4650
|
+
...options.abortSignal ? { abortSignal: options.abortSignal } : {}
|
|
4651
|
+
});
|
|
4652
|
+
return { response, synthetic: false };
|
|
4653
|
+
} finally {
|
|
4654
|
+
pendingProcesses.delete(proc);
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
if (!proc.stdout) {
|
|
4616
4658
|
throw new Error("/btw requires the headless Claude Code transport; interactive sessions are not supported.");
|
|
4617
4659
|
}
|
|
4618
4660
|
if (!cliSupportsSideQuestion(options.cliVersion)) {
|
|
@@ -5762,6 +5804,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5762
5804
|
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5763
5805
|
"interactive transport cancelled the usage-limit auto-continue",
|
|
5764
5806
|
"interactive transport cannot continue",
|
|
5807
|
+
"interactive transport could not answer the plan approval; stopping the turn",
|
|
5808
|
+
"interactive transport could not stage an image",
|
|
5765
5809
|
"interactive transport denied a permission prompt",
|
|
5766
5810
|
"interactive transport dropped non-text content blocks",
|
|
5767
5811
|
"interactive turn failed",
|
|
@@ -8181,6 +8225,38 @@ Now continuing with the current message:
|
|
|
8181
8225
|
}
|
|
8182
8226
|
});
|
|
8183
8227
|
}
|
|
8228
|
+
function getTrailingUserMessages(prompt, opts = {}) {
|
|
8229
|
+
prompt = stripAccountFailoverParts(prompt);
|
|
8230
|
+
if (opts.stripContextReminders) prompt = stripContextReminders(prompt).prompt;
|
|
8231
|
+
let assistantIndex = -1;
|
|
8232
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
8233
|
+
if (prompt[i].role === "assistant") {
|
|
8234
|
+
assistantIndex = i;
|
|
8235
|
+
break;
|
|
8236
|
+
}
|
|
8237
|
+
}
|
|
8238
|
+
const messages = [];
|
|
8239
|
+
for (let i = assistantIndex + 1; i < prompt.length; i++) {
|
|
8240
|
+
const msg = prompt[i];
|
|
8241
|
+
if (msg.role !== "user") continue;
|
|
8242
|
+
if (parseSideQuestionContent(msg.content) !== null) continue;
|
|
8243
|
+
const blocks = [];
|
|
8244
|
+
if (typeof msg.content === "string") {
|
|
8245
|
+
if (msg.content.trim()) blocks.push({ type: "text", text: msg.content });
|
|
8246
|
+
} else if (Array.isArray(msg.content)) {
|
|
8247
|
+
for (const part of msg.content) {
|
|
8248
|
+
if (part.type === "text") {
|
|
8249
|
+
if (part.text && part.text.trim()) blocks.push({ type: "text", text: part.text });
|
|
8250
|
+
} else if (part.type === "file" || part.type === "image") {
|
|
8251
|
+
const block = toImageBlock(part);
|
|
8252
|
+
if (block) blocks.push(block);
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
}
|
|
8256
|
+
if (blocks.length > 0) messages.push(blocks);
|
|
8257
|
+
}
|
|
8258
|
+
return { assistantIndex, messages };
|
|
8259
|
+
}
|
|
8184
8260
|
|
|
8185
8261
|
// src/skill-bridge.ts
|
|
8186
8262
|
import * as crypto3 from "crypto";
|
|
@@ -8554,8 +8630,10 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
8554
8630
|
|
|
8555
8631
|
// src/claude-session-wrapper.ts
|
|
8556
8632
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
8557
|
-
import {
|
|
8633
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
8634
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
8558
8635
|
import { unlink as unlink2 } from "fs/promises";
|
|
8636
|
+
import { join as join10 } from "path";
|
|
8559
8637
|
|
|
8560
8638
|
// src/claude-session-bun.ts
|
|
8561
8639
|
import * as os4 from "os";
|
|
@@ -8635,6 +8713,17 @@ var SCREEN_PATTERNS = [
|
|
|
8635
8713
|
"i"
|
|
8636
8714
|
)
|
|
8637
8715
|
},
|
|
8716
|
+
{
|
|
8717
|
+
// Measured verbatim on 2.1.288: "Claude has written up a plan and is ready
|
|
8718
|
+
// to execute. Would you like to proceed? ❯ 1. Yes, auto-accept edits
|
|
8719
|
+
// 2. Yes, manually approve edits 3. Tell Claude what to change". The
|
|
8720
|
+
// numbered last choice is what a reply quoting the question never has.
|
|
8721
|
+
kind: "plan-approval",
|
|
8722
|
+
pattern: new RegExp(
|
|
8723
|
+
`${words("Would you like to proceed?")}[\\s\\S]{0,400}?\\d\\s*\\.\\s*(?:${words("Tell Claude what to change")}|${words("No, keep planning")})`,
|
|
8724
|
+
"i"
|
|
8725
|
+
)
|
|
8726
|
+
},
|
|
8638
8727
|
{
|
|
8639
8728
|
// The question alone is not enough: Claude can write "Do you want to
|
|
8640
8729
|
// proceed?" in a reply. The dialog always numbers its options.
|
|
@@ -8665,6 +8754,13 @@ function highlightedChoice(text) {
|
|
|
8665
8754
|
}
|
|
8666
8755
|
return last === "yes" || last === "no" ? last : null;
|
|
8667
8756
|
}
|
|
8757
|
+
function planApprovalKeys(text) {
|
|
8758
|
+
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;
|
|
8759
|
+
const tell = /(\d)\s*\.\s*Tell\s*Claude\s*what\s*to\s*change/i.exec(text)?.[1];
|
|
8760
|
+
if (tell) return { approve, reject: tell, rejectTakesText: true };
|
|
8761
|
+
const keep = /(\d)\s*\.\s*No,?\s*keep\s*planning/i.exec(text)?.[1] ?? null;
|
|
8762
|
+
return { approve, reject: keep, rejectTakesText: false };
|
|
8763
|
+
}
|
|
8668
8764
|
function classifyScreen(text) {
|
|
8669
8765
|
for (const { kind, pattern } of SCREEN_PATTERNS) {
|
|
8670
8766
|
const match = pattern.exec(text);
|
|
@@ -8756,7 +8852,7 @@ var TurnUsageAccumulator = class {
|
|
|
8756
8852
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
8757
8853
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8758
8854
|
var RAW_KEEP_CHARS = 64 * 1024;
|
|
8759
|
-
var SCREEN_WINDOW_CHARS =
|
|
8855
|
+
var SCREEN_WINDOW_CHARS = 64 * 1024;
|
|
8760
8856
|
var INTERRUPT_MARKER = "[Request interrupted by user";
|
|
8761
8857
|
function isInterruptRecord(rec) {
|
|
8762
8858
|
if (!rec || rec.type !== "user" || !rec.message) return false;
|
|
@@ -8770,6 +8866,11 @@ function isInterruptRecord(rec) {
|
|
|
8770
8866
|
function isTurnDurationRecord(rec) {
|
|
8771
8867
|
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
8772
8868
|
}
|
|
8869
|
+
var normalizedPrompt = (text) => text.replace(/\s+/g, " ").trim();
|
|
8870
|
+
function isOwnPromptRecord(rec, normalized) {
|
|
8871
|
+
const content = rec?.type === "user" ? rec.message?.content : void 0;
|
|
8872
|
+
return typeof content === "string" && normalizedPrompt(content) === normalized;
|
|
8873
|
+
}
|
|
8773
8874
|
function resolveConfigDir(configDir) {
|
|
8774
8875
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8775
8876
|
if (!value) return path9.join(os4.homedir(), ".claude");
|
|
@@ -8804,6 +8905,10 @@ var ClaudeSession = class {
|
|
|
8804
8905
|
aborted = false;
|
|
8805
8906
|
signal;
|
|
8806
8907
|
resumeSessionId;
|
|
8908
|
+
forkOf;
|
|
8909
|
+
/** A fork whose first turn has not started: its transcript opens with the
|
|
8910
|
+
* parent's copied conversation, which belongs to no turn of this session. */
|
|
8911
|
+
forkPending = false;
|
|
8807
8912
|
env;
|
|
8808
8913
|
onScreen;
|
|
8809
8914
|
onExit;
|
|
@@ -8822,6 +8927,15 @@ var ClaudeSession = class {
|
|
|
8822
8927
|
/** The running turn's read state, shared with `flushTranscript`. */
|
|
8823
8928
|
scan = null;
|
|
8824
8929
|
turnDenied = [];
|
|
8930
|
+
/** The running turn's `ExitPlanMode` call that has no result yet. */
|
|
8931
|
+
planApprovalToolUseId = null;
|
|
8932
|
+
/** The approval dialog as drawn for one `ExitPlanMode` call, until it is
|
|
8933
|
+
* answered. Keyed by the call, so a redraw of a dialog already answered
|
|
8934
|
+
* (the TUI repaints it while it takes the key, measured on 2.1.288) can
|
|
8935
|
+
* never stand in for the next call's dialog. */
|
|
8936
|
+
planDialog = null;
|
|
8937
|
+
/** This turn's `ExitPlanMode` calls already answered. */
|
|
8938
|
+
answeredPlans = /* @__PURE__ */ new Set();
|
|
8825
8939
|
abandonTurn = false;
|
|
8826
8940
|
o;
|
|
8827
8941
|
constructor(opts = {}) {
|
|
@@ -8829,7 +8943,9 @@ var ClaudeSession = class {
|
|
|
8829
8943
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
8830
8944
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
8831
8945
|
this.signal = opts.signal;
|
|
8832
|
-
this.resumeSessionId = opts.resumeSessionId;
|
|
8946
|
+
this.resumeSessionId = opts.forkOf ? void 0 : opts.resumeSessionId;
|
|
8947
|
+
this.forkOf = opts.forkOf;
|
|
8948
|
+
this.forkPending = !!opts.forkOf;
|
|
8833
8949
|
this.env = opts.env;
|
|
8834
8950
|
this.onScreen = opts.onScreen;
|
|
8835
8951
|
this.onExit = opts.onExit;
|
|
@@ -8872,12 +8988,16 @@ var ClaudeSession = class {
|
|
|
8872
8988
|
// Shorter than `bootQuietMs`, so a trust dialog is answered before boot
|
|
8873
8989
|
// could declare the TUI ready and paste a prompt into it.
|
|
8874
8990
|
permissionQuietMs: opts.permissionQuietMs ?? 1e3,
|
|
8875
|
-
stopSettleMs: opts.stopSettleMs ?? 750
|
|
8991
|
+
stopSettleMs: opts.stopSettleMs ?? 750,
|
|
8992
|
+
// The decision arrives after opencode's own question round trip, by
|
|
8993
|
+
// which time the dialog has long been drawn; this only bounds a TUI
|
|
8994
|
+
// that never draws it.
|
|
8995
|
+
planDialogWaitMs: opts.planDialogWaitMs ?? 3e4
|
|
8876
8996
|
};
|
|
8877
8997
|
}
|
|
8878
8998
|
/** The CLI arguments this session spawns with. */
|
|
8879
8999
|
spawnArgs() {
|
|
8880
|
-
const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
9000
|
+
const args = this.forkOf ? ["--session-id", this.sessionId, "--resume", this.forkOf, "--fork-session"] : this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
8881
9001
|
if (this.o.model) args.push("--model", this.o.model);
|
|
8882
9002
|
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8883
9003
|
args.push("--setting-sources", this.o.settingSources);
|
|
@@ -8946,6 +9066,9 @@ var ClaudeSession = class {
|
|
|
8946
9066
|
this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
|
|
8947
9067
|
this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
|
|
8948
9068
|
if (this.o.debug) process.stdout.write(chunk);
|
|
9069
|
+
this.scheduleScreenCheck();
|
|
9070
|
+
}
|
|
9071
|
+
scheduleScreenCheck() {
|
|
8949
9072
|
this.clearScreenTimer();
|
|
8950
9073
|
this.screenTimer = setTimeout(() => {
|
|
8951
9074
|
this.screenTimer = null;
|
|
@@ -9001,6 +9124,13 @@ var ClaudeSession = class {
|
|
|
9001
9124
|
this.write("\x1B");
|
|
9002
9125
|
action = "cancelled";
|
|
9003
9126
|
break;
|
|
9127
|
+
case "plan-approval": {
|
|
9128
|
+
const id = this.planApprovalToolUseId;
|
|
9129
|
+
if (!this.turn || !id || this.answeredPlans.has(id) || this.planDialog?.id === id) return null;
|
|
9130
|
+
this.planDialog = { id, text };
|
|
9131
|
+
action = "parked";
|
|
9132
|
+
break;
|
|
9133
|
+
}
|
|
9004
9134
|
case "login":
|
|
9005
9135
|
case "onboarding":
|
|
9006
9136
|
action = "fatal";
|
|
@@ -9139,8 +9269,10 @@ var ClaudeSession = class {
|
|
|
9139
9269
|
stopSeenAt: 0,
|
|
9140
9270
|
end: null,
|
|
9141
9271
|
sawUserRecord: false,
|
|
9142
|
-
lastBeat: Date.now()
|
|
9272
|
+
lastBeat: Date.now(),
|
|
9273
|
+
awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
|
|
9143
9274
|
};
|
|
9275
|
+
this.forkPending = false;
|
|
9144
9276
|
this.scan = scan;
|
|
9145
9277
|
const deadline = Date.now() + timeout;
|
|
9146
9278
|
while (Date.now() < deadline) {
|
|
@@ -9174,6 +9306,9 @@ var ClaudeSession = class {
|
|
|
9174
9306
|
this.turn = null;
|
|
9175
9307
|
this.scan = null;
|
|
9176
9308
|
this.abandonTurn = false;
|
|
9309
|
+
this.planApprovalToolUseId = null;
|
|
9310
|
+
this.planDialog = null;
|
|
9311
|
+
this.answeredPlans.clear();
|
|
9177
9312
|
settle();
|
|
9178
9313
|
}
|
|
9179
9314
|
}
|
|
@@ -9199,6 +9334,14 @@ var ClaudeSession = class {
|
|
|
9199
9334
|
rec = JSON.parse(s);
|
|
9200
9335
|
} catch {
|
|
9201
9336
|
}
|
|
9337
|
+
if (scan.awaitingOwnPrompt !== null) {
|
|
9338
|
+
if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) {
|
|
9339
|
+
this.cursor = i + 1;
|
|
9340
|
+
continue;
|
|
9341
|
+
}
|
|
9342
|
+
scan.awaitingOwnPrompt = null;
|
|
9343
|
+
}
|
|
9344
|
+
if (rec) this.notePlanApproval(rec);
|
|
9202
9345
|
scan.onRecord(s, rec);
|
|
9203
9346
|
if (rec) {
|
|
9204
9347
|
if (rec.type === "assistant" && rec.message) {
|
|
@@ -9230,6 +9373,74 @@ var ClaudeSession = class {
|
|
|
9230
9373
|
flushTranscript() {
|
|
9231
9374
|
this.scanTranscript();
|
|
9232
9375
|
}
|
|
9376
|
+
/** Track the turn's `ExitPlanMode` call from the transcript: opened by the
|
|
9377
|
+
* assistant record that makes it, closed by the result that answers it. */
|
|
9378
|
+
notePlanApproval(rec) {
|
|
9379
|
+
const content = rec?.message?.content;
|
|
9380
|
+
if (!Array.isArray(content)) return;
|
|
9381
|
+
if (rec.type === "assistant") {
|
|
9382
|
+
for (const block of content) {
|
|
9383
|
+
if (block?.type === "tool_use" && block.name === "ExitPlanMode" && typeof block.id === "string") {
|
|
9384
|
+
this.planApprovalToolUseId = block.id;
|
|
9385
|
+
this.scheduleScreenCheck();
|
|
9386
|
+
}
|
|
9387
|
+
}
|
|
9388
|
+
} else if (rec.type === "user" && this.planApprovalToolUseId) {
|
|
9389
|
+
const answered = content.some(
|
|
9390
|
+
(block) => block?.type === "tool_result" && block.tool_use_id === this.planApprovalToolUseId
|
|
9391
|
+
);
|
|
9392
|
+
if (answered) {
|
|
9393
|
+
if (this.planDialog?.id === this.planApprovalToolUseId) this.planDialog = null;
|
|
9394
|
+
this.planApprovalToolUseId = null;
|
|
9395
|
+
}
|
|
9396
|
+
}
|
|
9397
|
+
}
|
|
9398
|
+
/**
|
|
9399
|
+
* The running turn's unanswered `ExitPlanMode` call id, or null. While it is
|
|
9400
|
+
* set the TUI is parked on (or about to draw) the approval dialog, and the
|
|
9401
|
+
* next message is the operator's decision, not a new turn.
|
|
9402
|
+
*/
|
|
9403
|
+
get pendingPlanApproval() {
|
|
9404
|
+
return this.turn ? this.planApprovalToolUseId : null;
|
|
9405
|
+
}
|
|
9406
|
+
/**
|
|
9407
|
+
* Answer the plan approval dialog the running turn is parked on. Waits up to
|
|
9408
|
+
* `planDialogWaitMs` for it to be drawn, because the transcript records the
|
|
9409
|
+
* call before the TUI draws the dialog, and resolves false when there is
|
|
9410
|
+
* none to answer or the drawn dialog has no matching choice.
|
|
9411
|
+
*/
|
|
9412
|
+
async answerPlanApproval(answer, timeoutMs = this.o.planDialogWaitMs) {
|
|
9413
|
+
const deadline = Date.now() + timeoutMs;
|
|
9414
|
+
let dialog = this.planDialog;
|
|
9415
|
+
while (!dialog || dialog.id !== this.planApprovalToolUseId) {
|
|
9416
|
+
if (!this.turn || this.exited || Date.now() >= deadline) return false;
|
|
9417
|
+
if (Date.now() - this.lastDataAt >= this.o.permissionQuietMs) this.checkScreen();
|
|
9418
|
+
await delay(this.o.pollMs);
|
|
9419
|
+
dialog = this.planDialog;
|
|
9420
|
+
}
|
|
9421
|
+
const keys = planApprovalKeys(dialog.text);
|
|
9422
|
+
const key = answer.approved ? keys.approve : keys.reject;
|
|
9423
|
+
if (!key) return false;
|
|
9424
|
+
this.answeredPlans.add(dialog.id);
|
|
9425
|
+
this.planDialog = null;
|
|
9426
|
+
this.screen = "";
|
|
9427
|
+
this.write(key);
|
|
9428
|
+
if (answer.approved || !keys.rejectTakesText) return true;
|
|
9429
|
+
await this.waitForQuiet();
|
|
9430
|
+
const text = (answer.feedback ?? "").replace(/\s+/g, " ").trim() || "no";
|
|
9431
|
+
this.write(text);
|
|
9432
|
+
await delay(this.o.submitMinMs);
|
|
9433
|
+
this.write("\r");
|
|
9434
|
+
return true;
|
|
9435
|
+
}
|
|
9436
|
+
/** Wait until the PTY has been quiet for `permissionQuietMs`, at most 3 s. */
|
|
9437
|
+
async waitForQuiet() {
|
|
9438
|
+
const until = Date.now() + 3e3;
|
|
9439
|
+
await delay(Math.min(this.o.permissionQuietMs, 3e3));
|
|
9440
|
+
while (Date.now() < until && Date.now() - this.lastDataAt < this.o.permissionQuietMs) {
|
|
9441
|
+
await delay(25);
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9233
9444
|
/**
|
|
9234
9445
|
* Stop the running turn the way the TUI's own Esc does, and keep the
|
|
9235
9446
|
* session alive for the next one. Resolves true once the turn ended, false
|
|
@@ -9358,7 +9569,14 @@ var ClaudeSession = class {
|
|
|
9358
9569
|
};
|
|
9359
9570
|
|
|
9360
9571
|
// src/claude-session-wrapper.ts
|
|
9361
|
-
|
|
9572
|
+
var IMAGE_EXTENSIONS = {
|
|
9573
|
+
"image/png": "png",
|
|
9574
|
+
"image/jpeg": "jpg",
|
|
9575
|
+
"image/jpg": "jpg",
|
|
9576
|
+
"image/gif": "gif",
|
|
9577
|
+
"image/webp": "webp"
|
|
9578
|
+
};
|
|
9579
|
+
function decodeUserEnvelope(chunk, saveImage) {
|
|
9362
9580
|
let parsed;
|
|
9363
9581
|
try {
|
|
9364
9582
|
parsed = JSON.parse(chunk);
|
|
@@ -9370,16 +9588,22 @@ function decodeUserEnvelope(chunk) {
|
|
|
9370
9588
|
if (typeof content === "string") return content;
|
|
9371
9589
|
if (!Array.isArray(content)) return chunk;
|
|
9372
9590
|
const parts = [];
|
|
9591
|
+
const images = [];
|
|
9373
9592
|
let dropped = 0;
|
|
9374
9593
|
for (const block of content) {
|
|
9594
|
+
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
9595
|
if (block?.type === "text" && typeof block.text === "string") {
|
|
9376
9596
|
parts.push(block.text);
|
|
9597
|
+
} else if (extension && saveImage) {
|
|
9598
|
+
const file = saveImage(Buffer.from(block.source.data, "base64"), extension);
|
|
9599
|
+
if (file) images.push(file);
|
|
9600
|
+
else dropped++;
|
|
9377
9601
|
} else if (block?.type === "tool_result") {
|
|
9378
9602
|
const v = block.content;
|
|
9379
|
-
const
|
|
9603
|
+
const text2 = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
|
|
9380
9604
|
parts.push(
|
|
9381
9605
|
`[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
|
|
9382
|
-
${
|
|
9606
|
+
${text2}`
|
|
9383
9607
|
);
|
|
9384
9608
|
} else {
|
|
9385
9609
|
dropped++;
|
|
@@ -9390,11 +9614,61 @@ ${text}`
|
|
|
9390
9614
|
dropped
|
|
9391
9615
|
});
|
|
9392
9616
|
}
|
|
9393
|
-
|
|
9617
|
+
const text = parts.join("\n\n");
|
|
9618
|
+
return images.length > 0 ? [...images, text].filter(Boolean).join("\n") : text;
|
|
9619
|
+
}
|
|
9620
|
+
function stageImage(data, extension) {
|
|
9621
|
+
try {
|
|
9622
|
+
const file = join10(pluginTmpDir(), `image-${randomUUID5()}.${extension}`);
|
|
9623
|
+
writeFileSync5(file, data, { mode: 384, flag: "wx" });
|
|
9624
|
+
return file;
|
|
9625
|
+
} catch (err) {
|
|
9626
|
+
log.warn("interactive transport could not stage an image", {
|
|
9627
|
+
error: err instanceof Error ? err.message : String(err)
|
|
9628
|
+
});
|
|
9629
|
+
return null;
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9632
|
+
var APPROVAL_WORDS = /^(?:y|yes|yep|yeah|ok|okay|sure|approve|approved|proceed|go|go ahead|do it|lgtm)[.!\s]*$/i;
|
|
9633
|
+
var HOST_ANNOTATIONS = /<(?:dcp-)?system-reminder>[\s\S]*?<\/(?:dcp-)?system-reminder>|<dcp-message-id>[^<]*<\/dcp-message-id>/g;
|
|
9634
|
+
function toolResultText(content) {
|
|
9635
|
+
if (typeof content === "string") return content;
|
|
9636
|
+
if (!Array.isArray(content)) return "";
|
|
9637
|
+
return content.map((item) => item?.type === "text" && typeof item.text === "string" ? item.text : "").join("\n");
|
|
9638
|
+
}
|
|
9639
|
+
function planApprovalAnswer(raw, toolUseId) {
|
|
9640
|
+
let parsed;
|
|
9641
|
+
try {
|
|
9642
|
+
parsed = JSON.parse(raw);
|
|
9643
|
+
} catch {
|
|
9644
|
+
}
|
|
9645
|
+
const content = parsed?.type === "user" ? parsed.message?.content : void 0;
|
|
9646
|
+
if (Array.isArray(content)) {
|
|
9647
|
+
const result = content.find(
|
|
9648
|
+
(block) => block?.type === "tool_result" && block.tool_use_id === toolUseId
|
|
9649
|
+
);
|
|
9650
|
+
if (result) {
|
|
9651
|
+
if (result.is_error !== true) return { approved: true };
|
|
9652
|
+
const text = toolResultText(result.content);
|
|
9653
|
+
const feedback = text.startsWith(REJECTED_EXIT_PLAN_MODE_PREFIX) ? text.slice(REJECTED_EXIT_PLAN_MODE_PREFIX.length) : text;
|
|
9654
|
+
return { approved: false, feedback: feedback.trim() || "no" };
|
|
9655
|
+
}
|
|
9656
|
+
}
|
|
9657
|
+
const typed = decodeUserEnvelope(raw).replace(HOST_ANNOTATIONS, "").trim();
|
|
9658
|
+
return APPROVAL_WORDS.test(typed) ? { approved: true } : { approved: false, feedback: typed || "no" };
|
|
9659
|
+
}
|
|
9660
|
+
function asidePrompt(question, history = []) {
|
|
9661
|
+
return [
|
|
9662
|
+
"(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.)",
|
|
9663
|
+
...history.map((exchange) => `Earlier side question: ${exchange.question}
|
|
9664
|
+
Your answer: ${exchange.response}`),
|
|
9665
|
+
question.trim()
|
|
9666
|
+
].join("\n\n");
|
|
9394
9667
|
}
|
|
9395
9668
|
function interactiveExtraArgs(opts) {
|
|
9396
9669
|
const extraArgs = [];
|
|
9397
9670
|
if (opts.tools) extraArgs.push("--tools", opts.tools.join(","));
|
|
9671
|
+
if (opts.restricted) extraArgs.push("--restricted");
|
|
9398
9672
|
if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
|
|
9399
9673
|
extraArgs.push(
|
|
9400
9674
|
"--mcp-config",
|
|
@@ -9552,6 +9826,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9552
9826
|
if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
|
|
9553
9827
|
else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
|
|
9554
9828
|
else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
|
|
9829
|
+
else if (event.action === "parked") log.info("interactive transport is waiting on a plan approval", data);
|
|
9555
9830
|
else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
|
|
9556
9831
|
},
|
|
9557
9832
|
onExit: (code) => {
|
|
@@ -9582,7 +9857,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
9582
9857
|
let cancelledThrough = 0;
|
|
9583
9858
|
let queued = 0;
|
|
9584
9859
|
let chain = Promise.resolve();
|
|
9585
|
-
const runTurn = async (userMsg, turnNumber) => {
|
|
9860
|
+
const runTurn = async (userMsg, turnNumber, images) => {
|
|
9586
9861
|
const current = () => turnNumber === writes;
|
|
9587
9862
|
const emit2 = (line) => {
|
|
9588
9863
|
if (!current()) return;
|
|
@@ -9655,12 +9930,62 @@ function spawnInteractiveProcess(opts) {
|
|
|
9655
9930
|
);
|
|
9656
9931
|
if (proc.listenerCount("error") > 0) proc.emit("error", e);
|
|
9657
9932
|
else lineEmitter.emit("close");
|
|
9933
|
+
} finally {
|
|
9934
|
+
for (const file of images) void unlink2(file).catch(() => {
|
|
9935
|
+
});
|
|
9658
9936
|
}
|
|
9659
9937
|
};
|
|
9660
|
-
const
|
|
9938
|
+
const answerPlan = async (answer) => {
|
|
9939
|
+
log.info("interactive transport answering a plan approval", {
|
|
9940
|
+
sessionId: session.sessionId,
|
|
9941
|
+
approved: answer.approved
|
|
9942
|
+
});
|
|
9943
|
+
const answered = await session.answerPlanApproval(answer).catch(() => false);
|
|
9944
|
+
if (answered) return;
|
|
9945
|
+
log.warn("interactive transport could not answer the plan approval; stopping the turn", {
|
|
9946
|
+
sessionId: session.sessionId
|
|
9947
|
+
});
|
|
9948
|
+
cancelledThrough = writes;
|
|
9949
|
+
await session.interrupt().catch(() => false);
|
|
9950
|
+
};
|
|
9951
|
+
const askAside = async (question, options) => {
|
|
9952
|
+
const forked = existsSync4(session.jsonlPath);
|
|
9953
|
+
const aside = new ClaudeSession({
|
|
9954
|
+
...opts.tuning,
|
|
9955
|
+
cwd: opts.cwd,
|
|
9956
|
+
cliPath: opts.cliPath,
|
|
9957
|
+
configDir: opts.configDir,
|
|
9958
|
+
model: opts.model,
|
|
9959
|
+
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
9960
|
+
extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk" }),
|
|
9961
|
+
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
9962
|
+
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
9963
|
+
env: opts.env,
|
|
9964
|
+
spawnPty: opts.spawnPty,
|
|
9965
|
+
signal: options.abortSignal,
|
|
9966
|
+
...forked ? { forkOf: session.sessionId } : {}
|
|
9967
|
+
});
|
|
9968
|
+
log.info("btw: asking a fork of the interactive session", {
|
|
9969
|
+
sessionId: session.sessionId,
|
|
9970
|
+
asideSessionId: aside.sessionId,
|
|
9971
|
+
forked
|
|
9972
|
+
});
|
|
9973
|
+
try {
|
|
9974
|
+
await aside.start();
|
|
9975
|
+
const result = await aside.ask(asidePrompt(question, options.history), options.timeoutMs);
|
|
9976
|
+
const text = result.text.trim();
|
|
9977
|
+
if (!text) throw new Error("the side question got no answer");
|
|
9978
|
+
return text;
|
|
9979
|
+
} finally {
|
|
9980
|
+
aside.dispose();
|
|
9981
|
+
void unlink2(aside.jsonlPath).catch(() => {
|
|
9982
|
+
});
|
|
9983
|
+
}
|
|
9984
|
+
};
|
|
9985
|
+
const enqueueTurn = (userMsg, images = []) => {
|
|
9661
9986
|
const turnNumber = ++writes;
|
|
9662
9987
|
queued++;
|
|
9663
|
-
chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
|
|
9988
|
+
chain = chain.then(() => runTurn(userMsg, turnNumber, images)).finally(() => {
|
|
9664
9989
|
queued--;
|
|
9665
9990
|
});
|
|
9666
9991
|
};
|
|
@@ -9669,7 +9994,18 @@ function spawnInteractiveProcess(opts) {
|
|
|
9669
9994
|
writable: true,
|
|
9670
9995
|
write(chunk) {
|
|
9671
9996
|
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
9672
|
-
|
|
9997
|
+
const planToolUseId = session.pendingPlanApproval;
|
|
9998
|
+
if (planToolUseId) {
|
|
9999
|
+
void answerPlan(planApprovalAnswer(raw, planToolUseId));
|
|
10000
|
+
return true;
|
|
10001
|
+
}
|
|
10002
|
+
const images = [];
|
|
10003
|
+
const text = decodeUserEnvelope(raw, (data, extension) => {
|
|
10004
|
+
const file = stageImage(data, extension);
|
|
10005
|
+
if (file) images.push(file);
|
|
10006
|
+
return file;
|
|
10007
|
+
});
|
|
10008
|
+
enqueueTurn(text, images);
|
|
9673
10009
|
return true;
|
|
9674
10010
|
},
|
|
9675
10011
|
end() {
|
|
@@ -9720,7 +10056,9 @@ function spawnInteractiveProcess(opts) {
|
|
|
9720
10056
|
cancelledThrough = writes;
|
|
9721
10057
|
return session.interrupt(timeoutMs);
|
|
9722
10058
|
},
|
|
9723
|
-
flushTranscript: () => session.flushTranscript()
|
|
10059
|
+
flushTranscript: () => session.flushTranscript(),
|
|
10060
|
+
planApprovalPending: () => session.pendingPlanApproval !== null,
|
|
10061
|
+
askAside
|
|
9724
10062
|
}
|
|
9725
10063
|
};
|
|
9726
10064
|
return ap;
|
|
@@ -9753,10 +10091,10 @@ async function selectTransport(requested, cliPath, available = hasInteractiveTra
|
|
|
9753
10091
|
}
|
|
9754
10092
|
|
|
9755
10093
|
// src/prompts.ts
|
|
9756
|
-
import { readFileSync as readFileSync7, writeFileSync as
|
|
10094
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
9757
10095
|
import { homedir as homedir8 } from "os";
|
|
9758
|
-
import { randomUUID as
|
|
9759
|
-
import { dirname as dirname6, join as
|
|
10096
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
10097
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
9760
10098
|
function readPromptFileIfPresent(path11) {
|
|
9761
10099
|
try {
|
|
9762
10100
|
const content = readFileSync7(path11, "utf8").trim();
|
|
@@ -9768,7 +10106,7 @@ function readPromptFileIfPresent(path11) {
|
|
|
9768
10106
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
9769
10107
|
let dir = cwd;
|
|
9770
10108
|
while (true) {
|
|
9771
|
-
const content = readPromptFileIfPresent(
|
|
10109
|
+
const content = readPromptFileIfPresent(join11(dir, "AGENTS.md"));
|
|
9772
10110
|
if (content) return content;
|
|
9773
10111
|
const parent = dirname6(dir);
|
|
9774
10112
|
if (parent === dir) return void 0;
|
|
@@ -9877,8 +10215,8 @@ ${options.compressionSummary.trim()}`
|
|
|
9877
10215
|
for (const s of extraSystemContent) {
|
|
9878
10216
|
if (s.trim()) parts.push(s.trim());
|
|
9879
10217
|
}
|
|
9880
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
9881
|
-
const globalAgents = readPromptFileIfPresent(
|
|
10218
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join11(homedir8(), ".config");
|
|
10219
|
+
const globalAgents = readPromptFileIfPresent(join11(configRoot, "opencode", "AGENTS.md"));
|
|
9882
10220
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
9883
10221
|
const forwarded = extraSystemContent.join("\n\n");
|
|
9884
10222
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -9889,9 +10227,9 @@ ${options.compressionSummary.trim()}`
|
|
|
9889
10227
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
9890
10228
|
const content = parts.join("\n\n");
|
|
9891
10229
|
if (!content) return void 0;
|
|
9892
|
-
const path11 =
|
|
10230
|
+
const path11 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
|
|
9893
10231
|
try {
|
|
9894
|
-
|
|
10232
|
+
writeFileSync6(path11, content, { encoding: "utf8", mode: 384 });
|
|
9895
10233
|
return path11;
|
|
9896
10234
|
} catch (err) {
|
|
9897
10235
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -10506,7 +10844,6 @@ function decideMcpHotReload(input) {
|
|
|
10506
10844
|
if (!input.hasActiveProcess) return decide("no-process");
|
|
10507
10845
|
const previousHash = input.previousHash ?? null;
|
|
10508
10846
|
if (previousHash === input.currentHash) return decide("unchanged");
|
|
10509
|
-
if (input.interactive) return decide("skipped-interactive");
|
|
10510
10847
|
if (input.pendingProxyCalls > 0) return decide("deferred-proxy-calls");
|
|
10511
10848
|
if (input.turnInFlight) return decide("deferred-turn-in-flight");
|
|
10512
10849
|
if (input.planQuestionPending) return decide("deferred-plan-question");
|
|
@@ -11305,6 +11642,24 @@ function finishWithQuestionCall(state, call) {
|
|
|
11305
11642
|
} catch {
|
|
11306
11643
|
}
|
|
11307
11644
|
}
|
|
11645
|
+
function finishAwaitingPlanApproval(state) {
|
|
11646
|
+
if (state.controllerClosed) return;
|
|
11647
|
+
state.endTextBlock();
|
|
11648
|
+
state.controller.enqueue({
|
|
11649
|
+
type: "finish",
|
|
11650
|
+
finishReason: state.toFinishReason("stop"),
|
|
11651
|
+
usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
|
|
11652
|
+
providerMetadata: {
|
|
11653
|
+
"claude-code": state.resultMeta
|
|
11654
|
+
}
|
|
11655
|
+
});
|
|
11656
|
+
state.controllerClosed = true;
|
|
11657
|
+
state.cleanupTurn();
|
|
11658
|
+
try {
|
|
11659
|
+
state.controller.close();
|
|
11660
|
+
} catch {
|
|
11661
|
+
}
|
|
11662
|
+
}
|
|
11308
11663
|
function drainNow(state) {
|
|
11309
11664
|
if (state.drainTimer) {
|
|
11310
11665
|
clearTimeout(state.drainTimer);
|
|
@@ -11901,6 +12256,10 @@ ${plan}
|
|
|
11901
12256
|
`
|
|
11902
12257
|
});
|
|
11903
12258
|
state.endTextBlock();
|
|
12259
|
+
if (ctx.interactive) {
|
|
12260
|
+
finishAwaitingPlanApproval(state);
|
|
12261
|
+
return;
|
|
12262
|
+
}
|
|
11904
12263
|
} else if (isWebSearchTool(block2.name) && isWebSearchHandledByCli(ctx.config.webSearch)) {
|
|
11905
12264
|
state.toolCallsById.delete(block2.id);
|
|
11906
12265
|
const query = typeof parsedInput?.query === "string" ? parsedInput.query : JSON.stringify(parsedInput);
|
|
@@ -12181,6 +12540,7 @@ async function isProxyCallStillServed(callId) {
|
|
|
12181
12540
|
return await fetchSessionRunState(session) === "busy";
|
|
12182
12541
|
}
|
|
12183
12542
|
setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
|
|
12543
|
+
var FORWARD_SETTLE_MS = 250;
|
|
12184
12544
|
var CHILD_EXIT_STATUS_GRACE_MS = 250;
|
|
12185
12545
|
var ClaudeCodeLanguageModel = class {
|
|
12186
12546
|
specificationVersion = "v3";
|
|
@@ -12625,9 +12985,6 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12625
12985
|
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
12986
|
if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
|
|
12627
12987
|
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
12988
|
const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
|
|
12632
12989
|
const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
|
|
12633
12990
|
const sourceAccount = normalizeAccountName(
|
|
@@ -12912,7 +13269,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12912
13269
|
sessionKey: sk,
|
|
12913
13270
|
prompt: options.prompt,
|
|
12914
13271
|
cliPath,
|
|
12915
|
-
transcriptPath: (id) =>
|
|
13272
|
+
transcriptPath: (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
|
|
12916
13273
|
});
|
|
12917
13274
|
if (resumePoint) {
|
|
12918
13275
|
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
@@ -12939,7 +13296,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12939
13296
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
12940
13297
|
stripContextReminders: this.stripContextRemindersEnabled()
|
|
12941
13298
|
});
|
|
12942
|
-
const
|
|
13299
|
+
const ptyPlanMode = useInteractive && !compactionMode && this.config.permissionMode === "plan";
|
|
13300
|
+
const resolvedProxy = compactionMode ? null : ptyPlanMode ? this.resolvedProxyTools()?.filter((def) => def.name !== "write" && def.name !== "edit") ?? null : this.resolvedProxyTools();
|
|
12943
13301
|
const loadLiveToolInfo = this.createLiveToolInfoLoader();
|
|
12944
13302
|
const planModeQuestionActive = await this.resolvePlanModeQuestion(
|
|
12945
13303
|
compactionMode,
|
|
@@ -12975,6 +13333,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12975
13333
|
return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
|
|
12976
13334
|
}
|
|
12977
13335
|
const [runtimeStatus, cliVersion] = prologue.value;
|
|
13336
|
+
const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
|
|
13337
|
+
const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
|
|
13338
|
+
if (ptyReadOnly && !ptyReadOnlySupported) {
|
|
13339
|
+
throw new Error(
|
|
13340
|
+
"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."
|
|
13341
|
+
);
|
|
13342
|
+
}
|
|
12978
13343
|
const failoverAccounts = failoverCandidates(
|
|
12979
13344
|
this.config.failoverAccounts,
|
|
12980
13345
|
sourceAccount
|
|
@@ -13096,7 +13461,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13096
13461
|
});
|
|
13097
13462
|
return;
|
|
13098
13463
|
}
|
|
13099
|
-
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
13464
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason, session: stopped });
|
|
13100
13465
|
void interruptTurn(stoppedProcess).then((idle) => {
|
|
13101
13466
|
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
13102
13467
|
});
|
|
@@ -13379,6 +13744,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13379
13744
|
});
|
|
13380
13745
|
if (stoppedBeforeWork()) return;
|
|
13381
13746
|
const resumeSessionId = getClaudeSessionId(sk);
|
|
13747
|
+
const posture = interactivePermissionPosture({
|
|
13748
|
+
permissionMode: self.config.permissionMode,
|
|
13749
|
+
allow,
|
|
13750
|
+
supportsReadOnly: ptyReadOnlySupported
|
|
13751
|
+
});
|
|
13752
|
+
if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
|
|
13382
13753
|
const ap = spawnInteractiveProcess({
|
|
13383
13754
|
cwd,
|
|
13384
13755
|
cliPath,
|
|
@@ -13387,7 +13758,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13387
13758
|
fastMode,
|
|
13388
13759
|
mcpConfigPaths: mcp.paths,
|
|
13389
13760
|
pluginDirs: skillPluginDirs,
|
|
13390
|
-
permissionsAllow: allow,
|
|
13761
|
+
permissionsAllow: posture.allow,
|
|
13762
|
+
permissionMode: posture.permissionMode,
|
|
13763
|
+
restricted: posture.restricted,
|
|
13391
13764
|
disallowedTools: wiring.allDisallowed,
|
|
13392
13765
|
proxyServer: state.proxyServer,
|
|
13393
13766
|
systemPromptFile,
|
|
@@ -13519,7 +13892,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13519
13892
|
state.activeProcess = ap;
|
|
13520
13893
|
}
|
|
13521
13894
|
}
|
|
13522
|
-
if (state.activeProcess && !hasMatchedPendingResults && isTurnInFlight(state.activeProcess)) {
|
|
13895
|
+
if (state.activeProcess && !hasMatchedPendingResults && !state.activeProcess.interactiveControl?.planApprovalPending?.() && isTurnInFlight(state.activeProcess)) {
|
|
13523
13896
|
log.warn("previous turn still in flight; interrupting it", { sk });
|
|
13524
13897
|
const idle = await interruptTurn(state.activeProcess);
|
|
13525
13898
|
if (!idle) {
|
|
@@ -13689,7 +14062,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13689
14062
|
});
|
|
13690
14063
|
state.controllerClosed = true;
|
|
13691
14064
|
cleanupTurn();
|
|
13692
|
-
if (!
|
|
14065
|
+
if (!compactionMode) {
|
|
13693
14066
|
scheduleIdleProcessEviction(
|
|
13694
14067
|
sk,
|
|
13695
14068
|
resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
|
|
@@ -13705,6 +14078,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13705
14078
|
compactionMode,
|
|
13706
14079
|
fastMode,
|
|
13707
14080
|
planModeQuestionActive,
|
|
14081
|
+
interactive: useInteractive,
|
|
13708
14082
|
sourceAccount,
|
|
13709
14083
|
failoverAskActive,
|
|
13710
14084
|
usageLimitNoteActive,
|
|
@@ -13918,6 +14292,34 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13918
14292
|
state.proc.on("error", procErrorHandler);
|
|
13919
14293
|
if (hasMatchedPendingResults) {
|
|
13920
14294
|
state.cliAskedForWork = true;
|
|
14295
|
+
const forwardTrailingUserMessages = () => {
|
|
14296
|
+
if (compactionMode || !state.activeProcess) return 0;
|
|
14297
|
+
if (state.activeProcess.interactiveControl) return 0;
|
|
14298
|
+
const trailing = getTrailingUserMessages(effectivePrompt, {
|
|
14299
|
+
stripContextReminders: self.stripContextRemindersEnabled()
|
|
14300
|
+
});
|
|
14301
|
+
const sent = state.activeProcess.forwardedUserMessages?.assistantIndex === trailing.assistantIndex ? state.activeProcess.forwardedUserMessages.count : 0;
|
|
14302
|
+
const fresh = trailing.messages.slice(sent);
|
|
14303
|
+
if (fresh.length === 0) return 0;
|
|
14304
|
+
state.activeProcess.forwardedUserMessages = {
|
|
14305
|
+
assistantIndex: trailing.assistantIndex,
|
|
14306
|
+
count: trailing.messages.length
|
|
14307
|
+
};
|
|
14308
|
+
for (const content of fresh) {
|
|
14309
|
+
state.proc.stdin?.write(
|
|
14310
|
+
JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n"
|
|
14311
|
+
);
|
|
14312
|
+
}
|
|
14313
|
+
log.info("forwarded user messages that arrived beside tool results", {
|
|
14314
|
+
sessionKey: sk,
|
|
14315
|
+
messages: fresh.length
|
|
14316
|
+
});
|
|
14317
|
+
return fresh.length;
|
|
14318
|
+
};
|
|
14319
|
+
if (!state.unattendedTurnEnded && forwardTrailingUserMessages() > 0) {
|
|
14320
|
+
await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
|
|
14321
|
+
if (state.controllerClosed) return;
|
|
14322
|
+
}
|
|
13921
14323
|
for (const { call, result } of previousPendingProxyMatches) {
|
|
13922
14324
|
if (result) {
|
|
13923
14325
|
const channelClosed = isPendingProxyCallChannelClosed(call);
|
|
@@ -13947,7 +14349,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13947
14349
|
);
|
|
13948
14350
|
}
|
|
13949
14351
|
}
|
|
13950
|
-
if (state.unattendedTurnEnded)
|
|
14352
|
+
if (state.unattendedTurnEnded) {
|
|
14353
|
+
deliverPendingCompletions(state);
|
|
14354
|
+
forwardTrailingUserMessages();
|
|
14355
|
+
}
|
|
13951
14356
|
const unemitted = getPendingProxyCalls(sk).filter(
|
|
13952
14357
|
(call) => !call.emitted
|
|
13953
14358
|
);
|
|
@@ -13976,7 +14381,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13976
14381
|
}
|
|
13977
14382
|
}
|
|
13978
14383
|
state.cliAskedForWork = true;
|
|
13979
|
-
if (state.activeProcess)
|
|
14384
|
+
if (state.activeProcess) {
|
|
14385
|
+
noteTurnStarted(state.activeProcess);
|
|
14386
|
+
state.activeProcess.forwardedUserMessages = void 0;
|
|
14387
|
+
}
|
|
13980
14388
|
state.proc.stdin?.write(userMsg + "\n");
|
|
13981
14389
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
13982
14390
|
armStartWatchdog(state);
|
|
@@ -14013,10 +14421,10 @@ import {
|
|
|
14013
14421
|
readFileSync as readFileSync8,
|
|
14014
14422
|
realpathSync as realpathSync4,
|
|
14015
14423
|
rmSync as rmSync3,
|
|
14016
|
-
writeFileSync as
|
|
14424
|
+
writeFileSync as writeFileSync7
|
|
14017
14425
|
} from "fs";
|
|
14018
14426
|
import { homedir as homedir9 } from "os";
|
|
14019
|
-
import { dirname as dirname7, join as
|
|
14427
|
+
import { dirname as dirname7, join as join13, resolve as resolve5 } from "path";
|
|
14020
14428
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
14021
14429
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
14022
14430
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -14024,14 +14432,14 @@ var alreadyRan = false;
|
|
|
14024
14432
|
function candidateCacheRoots() {
|
|
14025
14433
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
14026
14434
|
return [
|
|
14027
|
-
xdg ?
|
|
14028
|
-
|
|
14029
|
-
|
|
14435
|
+
xdg ? join13(xdg, "opencode") : null,
|
|
14436
|
+
join13(homedir9(), ".cache", "opencode"),
|
|
14437
|
+
join13(homedir9(), "Library", "Caches", "opencode")
|
|
14030
14438
|
].filter((p) => Boolean(p));
|
|
14031
14439
|
}
|
|
14032
14440
|
function userOpencodeJsonPath() {
|
|
14033
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
14034
|
-
return
|
|
14441
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join13(homedir9(), ".config");
|
|
14442
|
+
return join13(xdgConfig, "opencode", "opencode.json");
|
|
14035
14443
|
}
|
|
14036
14444
|
function userIntendsToUseUnscoped() {
|
|
14037
14445
|
const cfg = userOpencodeJsonPath();
|
|
@@ -14058,15 +14466,15 @@ function ourLoadedDir() {
|
|
|
14058
14466
|
function ourVersion(dir) {
|
|
14059
14467
|
if (!dir) return null;
|
|
14060
14468
|
try {
|
|
14061
|
-
const pkg = JSON.parse(readFileSync8(
|
|
14469
|
+
const pkg = JSON.parse(readFileSync8(join13(dir, "package.json"), "utf8"));
|
|
14062
14470
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
14063
14471
|
} catch {
|
|
14064
14472
|
return null;
|
|
14065
14473
|
}
|
|
14066
14474
|
}
|
|
14067
14475
|
function markerPath() {
|
|
14068
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
14069
|
-
return
|
|
14476
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join13(homedir9(), ".local", "state");
|
|
14477
|
+
return join13(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
14070
14478
|
}
|
|
14071
14479
|
function markerVersion() {
|
|
14072
14480
|
try {
|
|
@@ -14080,7 +14488,7 @@ function writeMarker(version) {
|
|
|
14080
14488
|
const file = markerPath();
|
|
14081
14489
|
try {
|
|
14082
14490
|
mkdirSync5(dirname7(file), { recursive: true });
|
|
14083
|
-
|
|
14491
|
+
writeFileSync7(
|
|
14084
14492
|
file,
|
|
14085
14493
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
14086
14494
|
"utf8"
|
|
@@ -14115,7 +14523,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
14115
14523
|
}
|
|
14116
14524
|
function cleanupOne(cacheRoot, ourDir) {
|
|
14117
14525
|
if (!existsSync5(cacheRoot)) return;
|
|
14118
|
-
const stalePath =
|
|
14526
|
+
const stalePath = join13(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
14119
14527
|
if (!existsSync5(stalePath)) return;
|
|
14120
14528
|
let realStalePath = stalePath;
|
|
14121
14529
|
try {
|
|
@@ -14123,7 +14531,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14123
14531
|
} catch {
|
|
14124
14532
|
}
|
|
14125
14533
|
if (ourDir && realStalePath === ourDir) return;
|
|
14126
|
-
const pkgJsonPath =
|
|
14534
|
+
const pkgJsonPath = join13(stalePath, "package.json");
|
|
14127
14535
|
if (!existsSync5(pkgJsonPath)) return;
|
|
14128
14536
|
let pkg = {};
|
|
14129
14537
|
try {
|
|
@@ -14143,13 +14551,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14143
14551
|
});
|
|
14144
14552
|
return;
|
|
14145
14553
|
}
|
|
14146
|
-
const cachePkgJson =
|
|
14554
|
+
const cachePkgJson = join13(cacheRoot, "package.json");
|
|
14147
14555
|
if (!existsSync5(cachePkgJson)) return;
|
|
14148
14556
|
try {
|
|
14149
14557
|
const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
|
|
14150
14558
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
14151
14559
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
14152
|
-
|
|
14560
|
+
writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
14153
14561
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
14154
14562
|
}
|
|
14155
14563
|
} catch (err) {
|
|
@@ -14720,8 +15128,10 @@ function warnIfAnthropicApiKey(ignore) {
|
|
|
14720
15128
|
function _resetPlanModeWarningForTests() {
|
|
14721
15129
|
warnedPlanModeNoExit = false;
|
|
14722
15130
|
}
|
|
14723
|
-
function warnIfPlanModeCannotExit(permissionMode) {
|
|
15131
|
+
function warnIfPlanModeCannotExit(permissionMode, transport) {
|
|
14724
15132
|
if (permissionMode !== "plan") return;
|
|
15133
|
+
const interactive = transport?.transport === "interactive" || transport?.transport === void 0 && transport?.interactive === true;
|
|
15134
|
+
if (interactive) return;
|
|
14725
15135
|
if (warnedPlanModeNoExit) return;
|
|
14726
15136
|
warnedPlanModeNoExit = true;
|
|
14727
15137
|
log.warn(
|
|
@@ -14762,7 +15172,7 @@ function createClaudeCode(settings = {}) {
|
|
|
14762
15172
|
}
|
|
14763
15173
|
warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
|
|
14764
15174
|
const preset = applyPermissionPreset(settings, DEFAULT_PROXY_TOOL_NAMES);
|
|
14765
|
-
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode);
|
|
15175
|
+
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode, settings);
|
|
14766
15176
|
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
14767
15177
|
const providerName = settings.providerID ?? settings.name ?? "claude-code";
|
|
14768
15178
|
const proxyTools = preset?.proxyTools ?? settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|