@khalilgharbaoui/opencode-claude-code-plugin 0.40.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.d.ts CHANGED
@@ -338,6 +338,7 @@ type OpenCodePlugin = (input: unknown, options?: Record<string, unknown>) => Pro
338
338
  type LogLevel = "debug" | "info" | "notice" | "warn" | "error";
339
339
  type LogMode = "silent" | "debug";
340
340
 
341
+ type ClaudeCodeTransport = "auto" | "headless" | "interactive";
341
342
  interface ClaudeCodeConfig {
342
343
  provider: string;
343
344
  cliPath: string;
@@ -349,6 +350,8 @@ interface ClaudeCodeConfig {
349
350
  * so nothing process-wide may decide it.
350
351
  */
351
352
  hostApi?: "v1" | "v2";
353
+ /** Explicit transport selection; auto switches only on proven missing headless flags. */
354
+ transport?: ClaudeCodeTransport;
352
355
  /** Drive interactive claude (subscription) instead of headless --print. */
353
356
  interactive?: boolean;
354
357
  /** Deprecated/no-op with interactive: Claude Code's TUI requires manual confirmation for bypassPermissions. */
@@ -466,6 +469,8 @@ interface ClaudeCodeProviderSettings {
466
469
  cliPath?: string;
467
470
  /** Internal: set by the opencode 2 entrypoint. See `ClaudeCodeConfig.hostApi`. */
468
471
  hostApi?: "v1" | "v2";
472
+ /** Wins over the legacy interactive option and its environment fallback. */
473
+ transport?: ClaudeCodeTransport;
469
474
  /** Drive interactive claude (subscription) instead of headless --print. */
470
475
  interactive?: boolean;
471
476
  /** Deprecated/no-op with interactive: Claude Code's TUI requires manual confirmation for bypassPermissions. */
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);
@@ -6336,6 +6373,10 @@ async function fetchPlanUsage(cliPath, options = {}) {
6336
6373
  const timeoutMs = options.timeoutMs ?? 2e4;
6337
6374
  const run = options.runImpl ?? runCli;
6338
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
+ }
6339
6380
  const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
6340
6381
  const stdout = await run(
6341
6382
  cliPath,
@@ -8055,6 +8096,9 @@ The complete prior conversation appears above. The synthesis instructions follow
8055
8096
  `
8056
8097
  });
8057
8098
  }
8099
+ if (opts.compactionInstructions?.trim()) {
8100
+ content.push({ type: "text", text: opts.compactionInstructions });
8101
+ }
8058
8102
  } else if (includeHistoryContext) {
8059
8103
  const historyContext = compactConversationHistory(prompt);
8060
8104
  if (historyContext) {
@@ -9350,6 +9394,7 @@ ${text}`
9350
9394
  }
9351
9395
  function interactiveExtraArgs(opts) {
9352
9396
  const extraArgs = [];
9397
+ if (opts.tools) extraArgs.push("--tools", opts.tools.join(","));
9353
9398
  if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
9354
9399
  extraArgs.push(
9355
9400
  "--mcp-config",
@@ -9681,6 +9726,32 @@ function spawnInteractiveProcess(opts) {
9681
9726
  return ap;
9682
9727
  }
9683
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";
9753
+ }
9754
+
9684
9755
  // src/prompts.ts
9685
9756
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9686
9757
  import { homedir as homedir8 } from "os";
@@ -12549,8 +12620,14 @@ var ClaudeCodeLanguageModel = class {
12549
12620
  const toFinishReason2 = this.toFinishReason.bind(this);
12550
12621
  const handleControlRequest2 = this.handleControlRequest.bind(this);
12551
12622
  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";
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
+ }
12554
12631
  const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
12555
12632
  const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
12556
12633
  const sourceAccount = normalizeAccountName(
@@ -12566,7 +12643,6 @@ var ClaudeCodeLanguageModel = class {
12566
12643
  let cliPath = failover.cliPath;
12567
12644
  if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
12568
12645
  const asideTransportRef = { cliPath, interactive: !!useInteractive };
12569
- const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
12570
12646
  if (doctor) {
12571
12647
  const doctorOptions = {
12572
12648
  cliPath,
@@ -12686,7 +12762,7 @@ var ClaudeCodeLanguageModel = class {
12686
12762
  request: { body: { text: "" } }
12687
12763
  };
12688
12764
  }
12689
- if (!hasNewUserContent(options.prompt)) {
12765
+ if (!compactionMode && !hasNewUserContent(options.prompt)) {
12690
12766
  log.info("doStream short-circuit: no new user content");
12691
12767
  const stream2 = new ReadableStream({
12692
12768
  start(controller) {
@@ -12859,6 +12935,7 @@ var ClaudeCodeLanguageModel = class {
12859
12935
  const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(options.prompt, failoverAnswer.target) : options.prompt;
12860
12936
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
12861
12937
  compactionMode,
12938
+ compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
12862
12939
  cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
12863
12940
  stripContextReminders: this.stripContextRemindersEnabled()
12864
12941
  });
@@ -13222,7 +13299,21 @@ var ClaudeCodeLanguageModel = class {
13222
13299
  allDisallowed
13223
13300
  };
13224
13301
  };
13225
- if (useInteractive && !compactionMode) {
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) {
13226
13317
  if (state.activeProcess) {
13227
13318
  state.proc = state.activeProcess.proc;
13228
13319
  state.lineEmitter = state.activeProcess.lineEmitter;
@@ -13706,6 +13797,7 @@ var ClaudeCodeLanguageModel = class {
13706
13797
  state.asideSinkUnregister?.();
13707
13798
  state.asideSinkUnregister = null;
13708
13799
  state.proc.off("error", procErrorHandler);
13800
+ if (compactionMode && useInteractive) state.proc.kill();
13709
13801
  };
13710
13802
  const procErrorHandler = (err) => {
13711
13803
  log.error("process error", { error: err.message });
@@ -14719,6 +14811,7 @@ function createClaudeCode(settings = {}) {
14719
14811
  turnStats: settings.turnStats === true,
14720
14812
  forkSessions: settings.forkSessions === true,
14721
14813
  resumeAfterRestart: settings.resumeAfterRestart !== false,
14814
+ transport: settings.transport,
14722
14815
  interactive: settings.interactive,
14723
14816
  interactiveBypass: settings.interactiveBypass,
14724
14817
  interactiveAllowTools: settings.interactiveAllowTools,