@khalilgharbaoui/opencode-claude-code-plugin 0.39.0 → 0.41.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
@@ -1728,6 +1728,43 @@ function cliSupportsThinking(v) {
1728
1728
  return gte(v, { major: 2, minor: 0, patch: 0 });
1729
1729
  }
1730
1730
  var flagSupport = /* @__PURE__ */ new Map();
1731
+ var headlessSupport = /* @__PURE__ */ new Map();
1732
+ function detectHeadlessSupport(cliPath) {
1733
+ const key = `${cliPath}\0headless`;
1734
+ const cached = headlessSupport.get(key);
1735
+ if (cached) return cached;
1736
+ let deadlineKill = false;
1737
+ const promise = (async () => {
1738
+ try {
1739
+ const execution = execFileAsync(cliPath, [
1740
+ "--print",
1741
+ "--input-format",
1742
+ "stream-json",
1743
+ "--output-format",
1744
+ "stream-json",
1745
+ "--help"
1746
+ ], { timeout: probeTimeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 });
1747
+ execution.child.stdin?.end();
1748
+ const { stdout } = await execution;
1749
+ if (!/^\s*Usage:/im.test(stdout) || !/^\s*Options:/im.test(stdout)) return "unknown";
1750
+ const flags = ["--print", "--input-format", "--output-format"];
1751
+ if (!flags.every((flag) => new RegExp(`^\\s+(?:-[A-Za-z],\\s+)?${flag}(?:\\s|,|$)`, "m").test(stdout))) {
1752
+ return "unsupported";
1753
+ }
1754
+ return stdout.includes("stream-json") ? "supported" : "unknown";
1755
+ } catch (error) {
1756
+ deadlineKill = killedByDeadline(error);
1757
+ const stderr = error?.stderr;
1758
+ if (typeof stderr === "string" && /^(?:error: )?(?:unknown|unrecognized|unsupported) option ['"]?--(?:print|input-format|output-format)(?:['"\s]|$)/im.test(stderr)) {
1759
+ return "unsupported";
1760
+ }
1761
+ return "unknown";
1762
+ }
1763
+ })();
1764
+ headlessSupport.set(key, promise);
1765
+ forgetDeadlineKill(headlessSupport, key, promise, () => deadlineKill);
1766
+ return promise;
1767
+ }
1731
1768
  function detectCliSupportsFlag(cliPath, flag) {
1732
1769
  const key = `${cliPath}\0${flag}`;
1733
1770
  const cached = flagSupport.get(key);
@@ -4887,6 +4924,9 @@ function noteInteractiveProcessExit(sessionKey2, ap, code) {
4887
4924
  if (activeProcesses.get(sessionKey2) !== ap) return;
4888
4925
  cancelIdleProcessEviction(sessionKey2);
4889
4926
  activeProcesses.delete(sessionKey2);
4927
+ if (getPendingProxyCalls(sessionKey2).length > 0) {
4928
+ rejectAllPendingProxyCallsForSession(sessionKey2, new Error(CHILD_EXITED_MESSAGE));
4929
+ }
4890
4930
  if (code !== 0 && code !== null) {
4891
4931
  claudeSessions.delete(sessionKey2);
4892
4932
  forgetResumePoint(sessionKey2);
@@ -6333,6 +6373,10 @@ async function fetchPlanUsage(cliPath, options = {}) {
6333
6373
  const timeoutMs = options.timeoutMs ?? 2e4;
6334
6374
  const run = options.runImpl ?? runCli;
6335
6375
  try {
6376
+ const support = options.headlessSupportImpl ?? (options.runImpl ? void 0 : detectHeadlessSupport);
6377
+ if (support && await support(cliPath) === "unsupported") {
6378
+ return { status: "failed", error: "Plan usage is unavailable: this Claude CLI does not support headless output. Check usage in the Claude CLI directly." };
6379
+ }
6336
6380
  const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
6337
6381
  const stdout = await run(
6338
6382
  cliPath,
@@ -8052,6 +8096,9 @@ The complete prior conversation appears above. The synthesis instructions follow
8052
8096
  `
8053
8097
  });
8054
8098
  }
8099
+ if (opts.compactionInstructions?.trim()) {
8100
+ content.push({ type: "text", text: opts.compactionInstructions });
8101
+ }
8055
8102
  } else if (includeHistoryContext) {
8056
8103
  const historyContext = compactConversationHistory(prompt);
8057
8104
  if (historyContext) {
@@ -8772,6 +8819,8 @@ var ClaudeSession = class {
8772
8819
  trustMoves = 0;
8773
8820
  trustMovedAt = 0;
8774
8821
  turn = null;
8822
+ /** The running turn's read state, shared with `flushTranscript`. */
8823
+ scan = null;
8775
8824
  turnDenied = [];
8776
8825
  abandonTurn = false;
8777
8826
  o;
@@ -9084,67 +9133,103 @@ var ClaudeSession = class {
9084
9133
  this.proc.terminal.write(prompt);
9085
9134
  }
9086
9135
  await this.submitTurn();
9087
- let stopReason = null;
9088
- let stopSeenAt = 0;
9089
- let end = null;
9090
- let sawUserRecord = false;
9091
- let lastBeat = Date.now();
9136
+ const scan = {
9137
+ onRecord,
9138
+ stopReason: null,
9139
+ stopSeenAt: 0,
9140
+ end: null,
9141
+ sawUserRecord: false,
9142
+ lastBeat: Date.now()
9143
+ };
9144
+ this.scan = scan;
9092
9145
  const deadline = Date.now() + timeout;
9093
9146
  while (Date.now() < deadline) {
9094
9147
  await delay(this.o.pollMs);
9095
9148
  if (this.aborted) throw new Error("aborted mid-turn");
9096
9149
  const fatalNow = this.fatalError();
9097
9150
  if (fatalNow) throw fatalNow;
9098
- const lines = this.readRawLines();
9099
- const lastComplete = lines.length - 1;
9100
- if (lastComplete > this.cursor) {
9101
- for (let i = this.cursor; i < lastComplete && !end; i++) {
9102
- const s = lines[i];
9103
- if (!s || !s.trim()) continue;
9104
- let rec = null;
9105
- try {
9106
- rec = JSON.parse(s);
9107
- } catch {
9108
- }
9109
- onRecord(s, rec);
9110
- if (!rec) continue;
9111
- if (rec.type === "assistant" && rec.message) {
9112
- const reason = rec.message.stop_reason;
9113
- if (typeof reason === "string") {
9114
- stopReason = TERMINAL_STOP.has(reason) ? reason : null;
9115
- }
9116
- } else if (rec.type === "user") {
9117
- if (sawUserRecord && isInterruptRecord(rec)) end = "interrupted";
9118
- sawUserRecord = true;
9119
- } else if ((sawUserRecord || stopReason) && isTurnDurationRecord(rec)) {
9120
- end = stopReason ? "stop" : "ended";
9121
- }
9122
- this.cursor = i + 1;
9151
+ if (this.scanTranscript() || scan.end) {
9152
+ if (scan.end) {
9153
+ return {
9154
+ stopReason: scan.end === "stop" ? scan.stopReason : null,
9155
+ end: scan.end,
9156
+ denied: this.turnDenied
9157
+ };
9123
9158
  }
9124
- if (!end) this.cursor = lastComplete;
9125
- lastBeat = Date.now();
9126
- stopSeenAt = Date.now();
9127
- if (end) return { stopReason: end === "stop" ? stopReason : null, end, denied: this.turnDenied };
9128
9159
  continue;
9129
9160
  }
9130
- if (stopReason && Date.now() - stopSeenAt >= this.o.stopSettleMs) {
9131
- return { stopReason, end: "stop", denied: this.turnDenied };
9161
+ if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
9162
+ return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
9132
9163
  }
9133
9164
  if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9134
9165
  if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9135
9166
  const now = Date.now();
9136
- if (onHeartbeat && now - lastBeat >= this.o.heartbeatMs) {
9137
- lastBeat = now;
9167
+ if (onHeartbeat && now - scan.lastBeat >= this.o.heartbeatMs) {
9168
+ scan.lastBeat = now;
9138
9169
  if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
9139
9170
  }
9140
9171
  }
9141
9172
  return { stopReason: null, end: null, denied: this.turnDenied };
9142
9173
  } finally {
9143
9174
  this.turn = null;
9175
+ this.scan = null;
9144
9176
  this.abandonTurn = false;
9145
9177
  settle();
9146
9178
  }
9147
9179
  }
9180
+ /**
9181
+ * Read every complete transcript record past the cursor into the running
9182
+ * turn. Returns whether anything new was read. The one place a record is
9183
+ * interpreted, shared by the poll loop and `flushTranscript`.
9184
+ */
9185
+ scanTranscript() {
9186
+ const scan = this.scan;
9187
+ if (!scan || scan.end) return false;
9188
+ const lines = this.readRawLines();
9189
+ const lastComplete = lines.length - 1;
9190
+ if (lastComplete <= this.cursor) return false;
9191
+ for (let i = this.cursor; i < lastComplete && !scan.end; i++) {
9192
+ const s = lines[i];
9193
+ if (!s || !s.trim()) {
9194
+ this.cursor = i + 1;
9195
+ continue;
9196
+ }
9197
+ let rec = null;
9198
+ try {
9199
+ rec = JSON.parse(s);
9200
+ } catch {
9201
+ }
9202
+ scan.onRecord(s, rec);
9203
+ if (rec) {
9204
+ if (rec.type === "assistant" && rec.message) {
9205
+ const reason = rec.message.stop_reason;
9206
+ if (typeof reason === "string") {
9207
+ scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
9208
+ }
9209
+ } else if (rec.type === "user") {
9210
+ if (scan.sawUserRecord && isInterruptRecord(rec)) scan.end = "interrupted";
9211
+ scan.sawUserRecord = true;
9212
+ } else if ((scan.sawUserRecord || scan.stopReason) && isTurnDurationRecord(rec)) {
9213
+ scan.end = scan.stopReason ? "stop" : "ended";
9214
+ }
9215
+ }
9216
+ this.cursor = i + 1;
9217
+ }
9218
+ if (!scan.end) this.cursor = lastComplete;
9219
+ scan.lastBeat = Date.now();
9220
+ scan.stopSeenAt = Date.now();
9221
+ return true;
9222
+ }
9223
+ /**
9224
+ * Hand the running turn every record the TUI has already written, now
9225
+ * rather than at the next poll. The TUI writes a reply's records before it
9226
+ * runs the reply's tools, but a proxied tool's MCP call can reach the
9227
+ * plugin inside one poll interval, and the step it ends must not close
9228
+ * before the text that preceded the call was read. No-op between turns.
9229
+ */
9230
+ flushTranscript() {
9231
+ this.scanTranscript();
9232
+ }
9148
9233
  /**
9149
9234
  * Stop the running turn the way the TUI's own Esc does, and keep the
9150
9235
  * session alive for the next one. Resolves true once the turn ended, false
@@ -9309,6 +9394,7 @@ ${text}`
9309
9394
  }
9310
9395
  function interactiveExtraArgs(opts) {
9311
9396
  const extraArgs = [];
9397
+ if (opts.tools) extraArgs.push("--tools", opts.tools.join(","));
9312
9398
  if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
9313
9399
  extraArgs.push(
9314
9400
  "--mcp-config",
@@ -9316,6 +9402,9 @@ function interactiveExtraArgs(opts) {
9316
9402
  "--strict-mcp-config"
9317
9403
  );
9318
9404
  }
9405
+ if (opts.disallowedTools && opts.disallowedTools.length > 0) {
9406
+ extraArgs.push("--disallowedTools", ...opts.disallowedTools);
9407
+ }
9319
9408
  for (const dir of opts.pluginDirs ?? []) {
9320
9409
  extraArgs.push("--plugin-dir", dir);
9321
9410
  }
@@ -9415,12 +9504,30 @@ function interactiveResultFrame(opts) {
9415
9504
  duration_ms: 0
9416
9505
  });
9417
9506
  }
9507
+ function carriesReplyText(line) {
9508
+ try {
9509
+ const rec = JSON.parse(line);
9510
+ if (rec?.type !== "assistant" || !Array.isArray(rec.message?.content)) return false;
9511
+ return rec.message.content.some(
9512
+ (block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()
9513
+ );
9514
+ } catch {
9515
+ return false;
9516
+ }
9517
+ }
9418
9518
  function spawnInteractiveProcess(opts) {
9419
9519
  const extraArgs = interactiveExtraArgs(opts);
9420
9520
  const openCalls = new OpenToolCalls();
9421
9521
  let denials = [];
9422
9522
  const lineEmitter = new EventEmitter4();
9423
9523
  const proc = new EventEmitter4();
9524
+ let ap;
9525
+ let proxyClosed = false;
9526
+ const closeProxyServer = () => {
9527
+ if (proxyClosed || !opts.proxyServer) return;
9528
+ proxyClosed = true;
9529
+ void opts.proxyServer.close();
9530
+ };
9424
9531
  const session = new ClaudeSession({
9425
9532
  ...opts.tuning,
9426
9533
  cwd: opts.cwd,
@@ -9450,6 +9557,7 @@ function spawnInteractiveProcess(opts) {
9450
9557
  onExit: (code) => {
9451
9558
  proc.exitCode = code;
9452
9559
  proc.signalCode = code === null ? "SIGTERM" : null;
9560
+ closeProxyServer();
9453
9561
  log.info("interactive claude exited", { sessionId: session.sessionId, code });
9454
9562
  proc.emit("exit", proc.exitCode, proc.signalCode);
9455
9563
  proc.emit("close", proc.exitCode, proc.signalCode);
@@ -9477,7 +9585,12 @@ function spawnInteractiveProcess(opts) {
9477
9585
  const runTurn = async (userMsg, turnNumber) => {
9478
9586
  const current = () => turnNumber === writes;
9479
9587
  const emit2 = (line) => {
9480
- if (current()) lineEmitter.emit("line", line);
9588
+ if (!current()) return;
9589
+ if (lineEmitter.listenerCount("line") === 0) {
9590
+ if (carriesReplyText(line)) bufferUnattendedLine(ap, line);
9591
+ return;
9592
+ }
9593
+ lineEmitter.emit("line", line);
9481
9594
  };
9482
9595
  try {
9483
9596
  await ensureStarted();
@@ -9497,7 +9610,13 @@ function spawnInteractiveProcess(opts) {
9497
9610
  emit2(raw);
9498
9611
  },
9499
9612
  void 0,
9500
- () => emit2(heartbeatFrame(session.sessionId, openCalls.newest()))
9613
+ // Only to a listening turn: it feeds that turn's watchdog, and the
9614
+ // TUI redraws its spinner the whole time it waits on a proxied call.
9615
+ () => {
9616
+ if (lineEmitter.listenerCount("line") > 0) {
9617
+ emit2(heartbeatFrame(session.sessionId, openCalls.newest()));
9618
+ }
9619
+ }
9501
9620
  );
9502
9621
  log.info("interactive turn ended", {
9503
9622
  end,
@@ -9570,6 +9689,7 @@ function spawnInteractiveProcess(opts) {
9570
9689
  });
9571
9690
  }
9572
9691
  }
9692
+ closeProxyServer();
9573
9693
  const started = startPromise !== null;
9574
9694
  try {
9575
9695
  session.dispose();
@@ -9583,11 +9703,13 @@ function spawnInteractiveProcess(opts) {
9583
9703
  return true;
9584
9704
  }
9585
9705
  });
9586
- return {
9706
+ ap = {
9587
9707
  proc,
9588
9708
  lineEmitter,
9589
- proxyServer: null,
9709
+ proxyServer: opts.proxyServer ?? null,
9590
9710
  mcpHash: void 0,
9711
+ unattendedLines: [],
9712
+ unattendedDropped: 0,
9591
9713
  systemPromptFile: opts.systemPromptFile,
9592
9714
  cliPath: opts.cliPath,
9593
9715
  interactiveControl: {
@@ -9597,9 +9719,37 @@ function spawnInteractiveProcess(opts) {
9597
9719
  interrupt: (timeoutMs) => {
9598
9720
  cancelledThrough = writes;
9599
9721
  return session.interrupt(timeoutMs);
9600
- }
9722
+ },
9723
+ flushTranscript: () => session.flushTranscript()
9601
9724
  }
9602
9725
  };
9726
+ return ap;
9727
+ }
9728
+
9729
+ // src/transport.ts
9730
+ function requestedTransport(config, env = process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT) {
9731
+ if (config.transport !== void 0) {
9732
+ if (!["auto", "headless", "interactive"].includes(config.transport)) {
9733
+ throw new Error(`Unknown Claude transport: ${config.transport}`);
9734
+ }
9735
+ return config.transport;
9736
+ }
9737
+ const enabled = config.interactive ?? (env !== void 0 && !["", "0", "false", "no", "off"].includes(env.trim().toLowerCase()));
9738
+ return enabled ? "interactive" : "headless";
9739
+ }
9740
+ function hasInteractiveTransport() {
9741
+ const runtime = globalThis;
9742
+ return typeof runtime.Bun?.Terminal === "function";
9743
+ }
9744
+ async function selectTransport(requested, cliPath, available = hasInteractiveTransport()) {
9745
+ if (requested === "headless") return "headless";
9746
+ if (requested === "interactive") {
9747
+ if (!available) throw new Error("Interactive Claude transport requires Bun.Terminal. Run opencode under a Bun runtime with PTY support.");
9748
+ return "interactive";
9749
+ }
9750
+ if (await detectHeadlessSupport(cliPath) !== "unsupported") return "headless";
9751
+ 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.");
9752
+ return "interactive";
9603
9753
  }
9604
9754
 
9605
9755
  // src/prompts.ts
@@ -11162,6 +11312,8 @@ function drainNow(state) {
11162
11312
  }
11163
11313
  if (state.drainBuffer.length === 0) return;
11164
11314
  if (state.controllerClosed) return;
11315
+ state.activeProcess?.interactiveControl?.flushTranscript();
11316
+ if (state.controllerClosed) return;
11165
11317
  const batch = state.drainBuffer.splice(0, state.drainBuffer.length);
11166
11318
  log.info("draining pending proxy calls into stream finish", {
11167
11319
  sessionKey: state.sessionKey,
@@ -12468,8 +12620,14 @@ var ClaudeCodeLanguageModel = class {
12468
12620
  const toFinishReason2 = this.toFinishReason.bind(this);
12469
12621
  const handleControlRequest2 = this.handleControlRequest.bind(this);
12470
12622
  const flagOn = (v) => v !== void 0 && !["", "0", "false", "no", "off"].includes(v.trim().toLowerCase());
12471
- const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
12472
- const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
12623
+ const transport = requestedTransport(this.config);
12624
+ const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
12625
+ 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
+ if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
12627
+ 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
+ }
12473
12631
  const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
12474
12632
  const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
12475
12633
  const sourceAccount = normalizeAccountName(
@@ -12485,7 +12643,6 @@ var ClaudeCodeLanguageModel = class {
12485
12643
  let cliPath = failover.cliPath;
12486
12644
  if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
12487
12645
  const asideTransportRef = { cliPath, interactive: !!useInteractive };
12488
- const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
12489
12646
  if (doctor) {
12490
12647
  const doctorOptions = {
12491
12648
  cliPath,
@@ -12605,7 +12762,7 @@ var ClaudeCodeLanguageModel = class {
12605
12762
  request: { body: { text: "" } }
12606
12763
  };
12607
12764
  }
12608
- if (!hasNewUserContent(options.prompt)) {
12765
+ if (!compactionMode && !hasNewUserContent(options.prompt)) {
12609
12766
  log.info("doStream short-circuit: no new user content");
12610
12767
  const stream2 = new ReadableStream({
12611
12768
  start(controller) {
@@ -12778,6 +12935,7 @@ var ClaudeCodeLanguageModel = class {
12778
12935
  const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
12779
12936
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
12780
12937
  compactionMode,
12938
+ compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
12781
12939
  cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
12782
12940
  stripContextReminders: this.stripContextRemindersEnabled()
12783
12941
  });
@@ -13015,13 +13173,159 @@ var ClaudeCodeLanguageModel = class {
13015
13173
  state.proxyServer = null;
13016
13174
  }
13017
13175
  }
13018
- if (useInteractive && !compactionMode) {
13019
- const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
13176
+ const resolveProxyWiring = async () => {
13177
+ const discovery = self.effectiveMcpConfig(
13178
+ cwd,
13179
+ void 0,
13180
+ runtimeStatus
13181
+ );
13182
+ const mcpResolution = self.resolvedProxyMcpTools(
13183
+ discovery.allEnabledServerNames,
13184
+ options.tools,
13185
+ new Set((resolvedProxy ?? []).map((def) => def.name))
13186
+ );
13187
+ const proxyMcpTools = mcpResolution?.defs ?? null;
13188
+ const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
13189
+ const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
13190
+ const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
13191
+ const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
13192
+ log.debug("opencode tool forwarding gate", {
13193
+ requested: self.config.proxyOpencodeTools ?? null,
13194
+ opencodeToolsRequested
13195
+ });
13196
+ const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
13197
+ resolved: false,
13198
+ taskDescription: void 0,
13199
+ taskParameters: void 0,
13200
+ questionDescription: void 0,
13201
+ hasQuestion: false
13202
+ };
13203
+ let enrichedProxy = resolvedProxy;
13204
+ let backgroundSubagentsSupported = false;
13205
+ if (enrichedProxy && taskProxyEnabled) {
13206
+ enrichedProxy = overlayTaskProxyDescription(
13207
+ enrichedProxy,
13208
+ liveToolInfo.taskDescription
13209
+ );
13210
+ log.info("task proxy description overlay", {
13211
+ applied: Boolean(liveToolInfo.taskDescription),
13212
+ liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
13213
+ listsAgentTypes: Boolean(
13214
+ liveToolInfo.taskDescription?.includes(
13215
+ "Available agent types"
13216
+ )
13217
+ )
13218
+ });
13219
+ const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
13220
+ backgroundSubagentsSupported = liveTaskSupportsBackground(
13221
+ liveToolInfo.taskParameters,
13222
+ hostDialect
13223
+ );
13224
+ enrichedProxy = applyBackgroundSubagentSupport(
13225
+ enrichedProxy,
13226
+ backgroundSubagentsSupported,
13227
+ hostDialect
13228
+ );
13229
+ recordBackgroundSubagentGate({
13230
+ supported: backgroundSubagentsSupported,
13231
+ hostApi: hostDialect,
13232
+ registryResolved: liveToolInfo.resolved
13233
+ });
13234
+ log.info("background subagent gate", {
13235
+ supported: backgroundSubagentsSupported,
13236
+ registryResolved: liveToolInfo.resolved,
13237
+ hostApi: hostDialect,
13238
+ note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
13239
+ });
13240
+ }
13241
+ if (enrichedProxy && questionProxyEnabled) {
13242
+ enrichedProxy = overlayQuestionProxyDescription(
13243
+ enrichedProxy,
13244
+ liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
13245
+ );
13246
+ enrichedProxy = filterQuestionProxyByOpencodeSupport(
13247
+ enrichedProxy,
13248
+ liveToolInfo.hasQuestion
13249
+ );
13250
+ log.info("question proxy version gate", {
13251
+ opencodeHasQuestion: liveToolInfo.hasQuestion,
13252
+ kept: liveToolInfo.hasQuestion
13253
+ });
13254
+ }
13255
+ const opencodeToolDefs = resolveProxyOpencodeToolDefs({
13256
+ requested: self.config.proxyOpencodeTools,
13257
+ items: liveToolInfo.items,
13258
+ modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
13259
+ allowCodeExecution: self.config.permissionPreset !== "read-only",
13260
+ taken: new Set(
13261
+ [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
13262
+ (t) => t.name
13263
+ )
13264
+ )
13265
+ });
13266
+ if (opencodeToolDefs.length > 0) {
13267
+ log.info("forwarding opencode tools through the proxy", {
13268
+ tools: opencodeToolDefs.map((t) => t.name)
13269
+ });
13270
+ }
13271
+ const combinedList = [
13272
+ ...enrichedProxy ?? [],
13273
+ ...proxyMcpTools ?? [],
13274
+ ...opencodeToolDefs
13275
+ ];
13276
+ const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
13277
+ const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
13278
+ if (!state.proxyServer && combinedProxyTools) {
13279
+ state.proxyServer = await self.ensureProxyServer(
13280
+ combinedProxyTools,
13281
+ sk,
13282
+ pluginCompressEnabled,
13283
+ affinity
13284
+ );
13285
+ }
13286
+ const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
13287
+ const allDisallowed = resolveDisallowedTools({
13288
+ proxyTools: enrichedProxy,
13289
+ extraDisallowedTools: self.config.extraDisallowedTools,
13290
+ disableWebSearch: self.config.webSearch === "disabled"
13291
+ });
13292
+ return {
13293
+ excludeServers,
13294
+ taskProxyEnabled,
13295
+ backgroundSubagentsSupported,
13296
+ questionProxyActive,
13297
+ opencodeToolDefs,
13298
+ pluginCompressEnabled,
13299
+ allDisallowed
13300
+ };
13301
+ };
13302
+ if (useInteractive && compactionMode) {
13303
+ const ap = spawnInteractiveProcess({
13304
+ cwd,
13305
+ cliPath,
13306
+ configDir: self.config.configDir,
13307
+ model: spawnModelId,
13308
+ fastMode,
13309
+ tools: [],
13310
+ mcpConfigPaths: ['{"mcpServers":{}}'],
13311
+ env: claudeSpawnEnv({ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey })
13312
+ });
13313
+ state.proc = ap.proc;
13314
+ state.lineEmitter = ap.lineEmitter;
13315
+ state.activeProcess = ap;
13316
+ } else if (useInteractive) {
13020
13317
  if (state.activeProcess) {
13021
13318
  state.proc = state.activeProcess.proc;
13022
13319
  state.lineEmitter = state.activeProcess.lineEmitter;
13023
13320
  log.debug("reusing active interactive session", { sk });
13024
13321
  } else {
13322
+ const wiring = await resolveProxyWiring();
13323
+ const mcp = self.effectiveMcpConfig(
13324
+ cwd,
13325
+ state.proxyServer?.configPath(),
13326
+ runtimeStatus,
13327
+ wiring.excludeServers
13328
+ );
13025
13329
  const allow = [
13026
13330
  ...mcp.allEnabledServerNames.map((n) => `mcp__${n}__*`),
13027
13331
  "mcp__opencode_proxy__*",
@@ -13035,12 +13339,27 @@ var ClaudeCodeLanguageModel = class {
13035
13339
  ];
13036
13340
  const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
13037
13341
  cwd,
13038
- self.config.multiStepContinuation !== false
13342
+ self.config.multiStepContinuation !== false,
13039
13343
  // Do not forward opencode's own system prompt into the
13040
13344
  // interactive TUI. Live subscription-account testing
13041
13345
  // showed that large forwarded payload can trigger Claude
13042
13346
  // Code's third-party-app usage gate, while our static
13043
- // CLI/AGENTS/continuation prompt remains safe.
13347
+ // CLI/AGENTS/continuation prompt remains safe. The
13348
+ // plugin's own short proxy hints are part of that static
13349
+ // prompt: without them the model narrates a subagent
13350
+ // dispatch instead of calling the proxy (h #g74).
13351
+ [
13352
+ ...wiring.taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
13353
+ ...wiring.backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
13354
+ ...wiring.questionProxyActive ? [QUESTION_PROXY_HINT] : []
13355
+ ],
13356
+ {
13357
+ compressEnabled: wiring.pluginCompressEnabled,
13358
+ opencodeCompressEnabled: wiring.opencodeToolDefs.some(
13359
+ (t) => t.name === "compress"
13360
+ ),
13361
+ compressionSummary: getCompressionSummary(sk)
13362
+ }
13044
13363
  );
13045
13364
  if (self.config.interactiveSystemPrompt === false) {
13046
13365
  log.warn(
@@ -13069,6 +13388,8 @@ var ClaudeCodeLanguageModel = class {
13069
13388
  mcpConfigPaths: mcp.paths,
13070
13389
  pluginDirs: skillPluginDirs,
13071
13390
  permissionsAllow: allow,
13391
+ disallowedTools: wiring.allDisallowed,
13392
+ proxyServer: state.proxyServer,
13072
13393
  systemPromptFile,
13073
13394
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13074
13395
  effort: reasoningEffort,
@@ -13115,121 +13436,15 @@ var ClaudeCodeLanguageModel = class {
13115
13436
  cliVersion
13116
13437
  });
13117
13438
  } else {
13118
- const discovery = self.effectiveMcpConfig(
13119
- cwd,
13120
- void 0,
13121
- runtimeStatus
13122
- );
13123
- const mcpResolution = self.resolvedProxyMcpTools(
13124
- discovery.allEnabledServerNames,
13125
- options.tools,
13126
- new Set((resolvedProxy ?? []).map((def) => def.name))
13127
- );
13128
- const proxyMcpTools = mcpResolution?.defs ?? null;
13129
- const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
13130
- const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
13131
- const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
13132
- const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
13133
- log.debug("opencode tool forwarding gate", {
13134
- requested: self.config.proxyOpencodeTools ?? null,
13135
- opencodeToolsRequested
13136
- });
13137
- const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
13138
- resolved: false,
13139
- taskDescription: void 0,
13140
- taskParameters: void 0,
13141
- questionDescription: void 0,
13142
- hasQuestion: false
13143
- };
13144
- let enrichedProxy = resolvedProxy;
13145
- let backgroundSubagentsSupported = false;
13146
- if (enrichedProxy && taskProxyEnabled) {
13147
- enrichedProxy = overlayTaskProxyDescription(
13148
- enrichedProxy,
13149
- liveToolInfo.taskDescription
13150
- );
13151
- log.info("task proxy description overlay", {
13152
- applied: Boolean(liveToolInfo.taskDescription),
13153
- liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
13154
- listsAgentTypes: Boolean(
13155
- liveToolInfo.taskDescription?.includes(
13156
- "Available agent types"
13157
- )
13158
- )
13159
- });
13160
- const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
13161
- backgroundSubagentsSupported = liveTaskSupportsBackground(
13162
- liveToolInfo.taskParameters,
13163
- hostDialect
13164
- );
13165
- enrichedProxy = applyBackgroundSubagentSupport(
13166
- enrichedProxy,
13167
- backgroundSubagentsSupported,
13168
- hostDialect
13169
- );
13170
- recordBackgroundSubagentGate({
13171
- supported: backgroundSubagentsSupported,
13172
- hostApi: hostDialect,
13173
- registryResolved: liveToolInfo.resolved
13174
- });
13175
- log.info("background subagent gate", {
13176
- supported: backgroundSubagentsSupported,
13177
- registryResolved: liveToolInfo.resolved,
13178
- hostApi: hostDialect,
13179
- note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
13180
- });
13181
- }
13182
- if (enrichedProxy && questionProxyEnabled) {
13183
- enrichedProxy = overlayQuestionProxyDescription(
13184
- enrichedProxy,
13185
- liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
13186
- );
13187
- enrichedProxy = filterQuestionProxyByOpencodeSupport(
13188
- enrichedProxy,
13189
- liveToolInfo.hasQuestion
13190
- );
13191
- log.info("question proxy version gate", {
13192
- opencodeHasQuestion: liveToolInfo.hasQuestion,
13193
- kept: liveToolInfo.hasQuestion
13194
- });
13195
- }
13196
- const opencodeToolDefs = resolveProxyOpencodeToolDefs({
13197
- requested: self.config.proxyOpencodeTools,
13198
- items: liveToolInfo.items,
13199
- modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
13200
- allowCodeExecution: self.config.permissionPreset !== "read-only",
13201
- taken: new Set(
13202
- [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
13203
- (t) => t.name
13204
- )
13205
- )
13206
- });
13207
- if (opencodeToolDefs.length > 0) {
13208
- log.info("forwarding opencode tools through the proxy", {
13209
- tools: opencodeToolDefs.map((t) => t.name)
13210
- });
13211
- }
13212
- const combinedList = [
13213
- ...enrichedProxy ?? [],
13214
- ...proxyMcpTools ?? [],
13215
- ...opencodeToolDefs
13216
- ];
13217
- const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
13218
- const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
13219
- if (!state.proxyServer && combinedProxyTools) {
13220
- state.proxyServer = await self.ensureProxyServer(
13221
- combinedProxyTools,
13222
- sk,
13223
- pluginCompressEnabled,
13224
- affinity
13225
- );
13226
- }
13227
- const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
13228
- const allDisallowed = resolveDisallowedTools({
13229
- proxyTools: enrichedProxy,
13230
- extraDisallowedTools: self.config.extraDisallowedTools,
13231
- disableWebSearch: self.config.webSearch === "disabled"
13232
- });
13439
+ const {
13440
+ excludeServers,
13441
+ taskProxyEnabled,
13442
+ backgroundSubagentsSupported,
13443
+ questionProxyActive,
13444
+ opencodeToolDefs,
13445
+ pluginCompressEnabled,
13446
+ allDisallowed
13447
+ } = await resolveProxyWiring();
13233
13448
  const mcp = self.effectiveMcpConfig(
13234
13449
  cwd,
13235
13450
  state.proxyServer?.configPath(),
@@ -13582,6 +13797,7 @@ var ClaudeCodeLanguageModel = class {
13582
13797
  state.asideSinkUnregister?.();
13583
13798
  state.asideSinkUnregister = null;
13584
13799
  state.proc.off("error", procErrorHandler);
13800
+ if (compactionMode && useInteractive) state.proc.kill();
13585
13801
  };
13586
13802
  const procErrorHandler = (err) => {
13587
13803
  log.error("process error", { error: err.message });
@@ -14595,6 +14811,7 @@ function createClaudeCode(settings = {}) {
14595
14811
  turnStats: settings.turnStats === true,
14596
14812
  forkSessions: settings.forkSessions === true,
14597
14813
  resumeAfterRestart: settings.resumeAfterRestart !== false,
14814
+ transport: settings.transport,
14598
14815
  interactive: settings.interactive,
14599
14816
  interactiveBypass: settings.interactiveBypass,
14600
14817
  interactiveAllowTools: settings.interactiveAllowTools,