@khalilgharbaoui/opencode-claude-code-plugin 0.40.0 → 0.42.0

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