@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/claude-code-language-model.ts
2
- import { join as join11 } from "path";
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 res = await status.call(opencodeClient.session);
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
- const state = await fetchSessionRunState(sessionID);
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 || !proc.stdout) {
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 { existsSync as existsSync4 } from "fs";
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 = 16 * 1024;
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
- function decodeUserEnvelope(chunk) {
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 text = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
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
- ${text}`
9606
+ ${text2}`
9383
9607
  );
9384
9608
  } else {
9385
9609
  dropped++;
@@ -9390,11 +9614,61 @@ ${text}`
9390
9614
  dropped
9391
9615
  });
9392
9616
  }
9393
- return parts.join("\n\n");
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 enqueueTurn = (userMsg) => {
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
- enqueueTurn(decodeUserEnvelope(raw));
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 writeFileSync5 } from "fs";
10094
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
9757
10095
  import { homedir as homedir8 } from "os";
9758
- import { randomUUID as randomUUID5 } from "crypto";
9759
- import { dirname as dirname6, join as join10 } from "path";
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(join10(dir, "AGENTS.md"));
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 ?? join10(homedir8(), ".config");
9881
- const globalAgents = readPromptFileIfPresent(join10(configRoot, "opencode", "AGENTS.md"));
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 = join10(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
10230
+ const path11 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
9893
10231
  try {
9894
- writeFileSync5(path11, content, { encoding: "utf8", mode: 384 });
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) => join11(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
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 resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
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 (!useInteractive && !compactionMode) {
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) deliverPendingCompletions(state);
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) noteTurnStarted(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 writeFileSync6
14424
+ writeFileSync as writeFileSync7
14017
14425
  } from "fs";
14018
14426
  import { homedir as homedir9 } from "os";
14019
- import { dirname as dirname7, join as join12, resolve as resolve5 } from "path";
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 ? join12(xdg, "opencode") : null,
14028
- join12(homedir9(), ".cache", "opencode"),
14029
- join12(homedir9(), "Library", "Caches", "opencode")
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 ?? join12(homedir9(), ".config");
14034
- return join12(xdgConfig, "opencode", "opencode.json");
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(join12(dir, "package.json"), "utf8"));
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 ?? join12(homedir9(), ".local", "state");
14069
- return join12(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
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
- writeFileSync6(
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 = join12(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
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 = join12(stalePath, "package.json");
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 = join12(cacheRoot, "package.json");
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
- writeFileSync6(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
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];