@khalilgharbaoui/opencode-claude-code-plugin 0.42.1 → 0.42.3

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
@@ -1723,6 +1723,21 @@ function cliSupportsDontAsk(v) {
1723
1723
  if (!v) return false;
1724
1724
  return gte(v, { major: 2, minor: 1, patch: 263 });
1725
1725
  }
1726
+ var INTERACTIVE_MEASURED_CLI = "2.1.288";
1727
+ var INTERACTIVE_MEASURED = { major: 2, minor: 1, patch: 288 };
1728
+ function isUnmeasuredInteractiveCli(v) {
1729
+ if (!v) return false;
1730
+ return gte(v, INTERACTIVE_MEASURED) && !(v.major === INTERACTIVE_MEASURED.major && v.minor === INTERACTIVE_MEASURED.minor && v.patch === INTERACTIVE_MEASURED.patch);
1731
+ }
1732
+ var warnedUnmeasuredInteractive = /* @__PURE__ */ new Set();
1733
+ function reportUnmeasuredInteractiveCli(v) {
1734
+ if (!v || !isUnmeasuredInteractiveCli(v) || warnedUnmeasuredInteractive.has(v.raw)) return;
1735
+ warnedUnmeasuredInteractive.add(v.raw);
1736
+ log.warn(
1737
+ "the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
1738
+ { cliVersion: v.raw, measuredCli: INTERACTIVE_MEASURED_CLI }
1739
+ );
1740
+ }
1726
1741
  function cliSupportsSideQuestion(v) {
1727
1742
  if (!v) return false;
1728
1743
  return gte(v, { major: 2, minor: 1, patch: 258 });
@@ -5431,6 +5446,7 @@ function snapshotActiveProcesses(now = Date.now()) {
5431
5446
  ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
5432
5447
  effort: ap.effort,
5433
5448
  attached: ap.lineEmitter.listenerCount("line") > 0,
5449
+ transport: ap.interactiveControl ? "interactive" : "headless",
5434
5450
  proxyUrl: ap.proxyServer?.url,
5435
5451
  lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
5436
5452
  });
@@ -5845,6 +5861,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5845
5861
  "skipping remote MCP server with no url",
5846
5862
  "stale scratch directory sweep failed",
5847
5863
  "task_batch resolving with child results missing",
5864
+ "the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
5865
+ "the interactive turn did not take a forwarded user message",
5848
5866
  "this opencode process is running an older plugin build than the one on disk",
5849
5867
  "timed out pending proxy call",
5850
5868
  "turn abort listener threw",
@@ -5994,6 +6012,7 @@ var BUNDLE_DATA_ALLOWLIST = {
5994
6012
  toolNames: "name",
5995
6013
  cliFloor: "version",
5996
6014
  cliVersion: "version",
6015
+ measuredCli: "version",
5997
6016
  // The two halves of the stale-build WARN: what this process loaded and what
5998
6017
  // is on disk now. Both are this package's own version strings.
5999
6018
  loadedVersion: "version",
@@ -6453,6 +6472,34 @@ import * as fs4 from "fs";
6453
6472
  import * as path5 from "path";
6454
6473
  import { promisify as promisify2 } from "util";
6455
6474
  import { fileURLToPath } from "url";
6475
+
6476
+ // src/transport.ts
6477
+ function requestedTransport(config, env = process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT) {
6478
+ if (config.transport !== void 0) {
6479
+ if (!["auto", "headless", "interactive"].includes(config.transport)) {
6480
+ throw new Error(`Unknown Claude transport: ${config.transport}`);
6481
+ }
6482
+ return config.transport;
6483
+ }
6484
+ const enabled = config.interactive ?? (env !== void 0 && !["", "0", "false", "no", "off"].includes(env.trim().toLowerCase()));
6485
+ return enabled ? "interactive" : "headless";
6486
+ }
6487
+ function hasInteractiveTransport() {
6488
+ const runtime = globalThis;
6489
+ return typeof runtime.Bun?.Terminal === "function";
6490
+ }
6491
+ async function selectTransport(requested, cliPath, available = hasInteractiveTransport()) {
6492
+ if (requested === "headless") return "headless";
6493
+ if (requested === "interactive") {
6494
+ if (!available) throw new Error("Interactive Claude transport requires Bun.Terminal. Run opencode under a Bun runtime with PTY support.");
6495
+ return "interactive";
6496
+ }
6497
+ if (await detectHeadlessSupport(cliPath) !== "unsupported") return "headless";
6498
+ 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.");
6499
+ return "interactive";
6500
+ }
6501
+
6502
+ // src/startup-diagnostics.ts
6456
6503
  var cachedPluginVersion;
6457
6504
  function pluginVersion() {
6458
6505
  if (cachedPluginVersion) return cachedPluginVersion;
@@ -6535,6 +6582,15 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
6535
6582
  error: err instanceof Error ? err.message : String(err)
6536
6583
  });
6537
6584
  }
6585
+ let transport;
6586
+ try {
6587
+ transport = requestedTransport({
6588
+ transport: firstOption(providers, "transport"),
6589
+ interactive: firstOption(providers, "interactive")
6590
+ });
6591
+ } catch {
6592
+ transport = "invalid";
6593
+ }
6538
6594
  return {
6539
6595
  plugin: pluginVersion(),
6540
6596
  opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
@@ -6547,7 +6603,8 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
6547
6603
  permissionPresets: Object.entries(providers).map(
6548
6604
  ([name, entry]) => summarizePermissionPreset(name, entry?.options)
6549
6605
  ),
6550
- interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
6606
+ transport,
6607
+ interactiveTransport: transport === "interactive",
6551
6608
  planModeQuestion: firstOption(providers, "planModeQuestion") === true,
6552
6609
  anthropicApiKeyInEnv: Boolean(
6553
6610
  process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
@@ -6816,7 +6873,14 @@ function formatDoctorReport(report) {
6816
6873
  lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
6817
6874
  lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
6818
6875
  lines.push(`| permissionPreset | ${describePermissionPresets(report.permissionPresets)} |`);
6819
- lines.push(`| transport | ${report.transport} |`);
6876
+ lines.push(
6877
+ `| transport | ${report.transport}${report.transport === "auto" ? " (headless until claude refuses `--print`)" : ""}; this conversation: ${report.transportInUse} |`
6878
+ );
6879
+ if (report.interactiveMeasured) {
6880
+ lines.push(
6881
+ `| interactive transport measured on | Claude Code ${report.interactiveMeasured.measured}${report.interactiveMeasured.unmeasured ? ` (this CLI, ${report.claudeCli.version}, is newer and unmeasured: if a turn hangs, send \`/claude-code-doctor bundle\`)` : ""} |`
6882
+ );
6883
+ }
6820
6884
  lines.push(`| planModeQuestion | ${report.planModeQuestion} |`);
6821
6885
  lines.push(`| turnStats | ${report.turnStats} |`);
6822
6886
  lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
@@ -6837,11 +6901,11 @@ function formatDoctorReport(report) {
6837
6901
  if (report.processes.length === 0) {
6838
6902
  lines.push("None. The next message in a Claude Code session spawns one.");
6839
6903
  } else {
6840
- lines.push("| session | model | pid | in flight | age | effort |");
6841
- lines.push("|---|---|---|---|---|---|");
6904
+ lines.push("| session | model | transport | pid | in flight | age | effort |");
6905
+ lines.push("|---|---|---|---|---|---|---|");
6842
6906
  for (const proc of report.processes) {
6843
6907
  lines.push(
6844
- `| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
6908
+ `| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.transport} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
6845
6909
  );
6846
6910
  }
6847
6911
  }
@@ -7061,7 +7125,9 @@ async function gatherDoctorReport(options) {
7061
7125
  proxyTools: base.proxyTools,
7062
7126
  mcpServers: base.mcpServers,
7063
7127
  permissionPresets: base.permissionPresets,
7064
- transport: options.interactive || base.interactiveTransport ? "interactive" : "headless",
7128
+ transport: base.transport,
7129
+ transportInUse: options.interactive ? "interactive" : "headless",
7130
+ interactiveMeasured: base.transport !== "headless" || options.interactive || processes.some((proc) => proc.transport === "interactive") ? { measured: INTERACTIVE_MEASURED_CLI, unmeasured: isUnmeasuredInteractiveCli(cli) } : void 0,
7065
7131
  planModeQuestion: base.planModeQuestion,
7066
7132
  turnStats: options.turnStats,
7067
7133
  anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
@@ -8934,6 +9000,9 @@ var ClaudeSession = class {
8934
9000
  * (the TUI repaints it while it takes the key, measured on 2.1.288) can
8935
9001
  * never stand in for the next call's dialog. */
8936
9002
  planDialog = null;
9003
+ /** `queue-operation` enqueue records read, which is how `queueInput` knows
9004
+ * the TUI took a message into the running turn. */
9005
+ enqueued = 0;
8937
9006
  /** This turn's `ExitPlanMode` calls already answered. */
8938
9007
  answeredPlans = /* @__PURE__ */ new Set();
8939
9008
  abandonTurn = false;
@@ -8992,7 +9061,9 @@ var ClaudeSession = class {
8992
9061
  // The decision arrives after opencode's own question round trip, by
8993
9062
  // which time the dialog has long been drawn; this only bounds a TUI
8994
9063
  // that never draws it.
8995
- planDialogWaitMs: opts.planDialogWaitMs ?? 3e4
9064
+ planDialogWaitMs: opts.planDialogWaitMs ?? 3e4,
9065
+ // 100 times the longest silence measured while Claude works (h #g207).
9066
+ idleEndMs: opts.idleEndMs ?? 2e4
8996
9067
  };
8997
9068
  }
8998
9069
  /** The CLI arguments this session spawns with. */
@@ -9270,7 +9341,8 @@ var ClaudeSession = class {
9270
9341
  end: null,
9271
9342
  sawUserRecord: false,
9272
9343
  lastBeat: Date.now(),
9273
- awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
9344
+ awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null,
9345
+ sawReply: false
9274
9346
  };
9275
9347
  this.forkPending = false;
9276
9348
  this.scan = scan;
@@ -9293,6 +9365,9 @@ var ClaudeSession = class {
9293
9365
  if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
9294
9366
  return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
9295
9367
  }
9368
+ if (scan.sawReply && !this.planApprovalToolUseId && Date.now() - this.lastDataAt >= this.o.idleEndMs && Date.now() - scan.stopSeenAt >= this.o.idleEndMs) {
9369
+ return { stopReason: null, end: "ended", denied: this.turnDenied };
9370
+ }
9296
9371
  if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9297
9372
  if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9298
9373
  const now = Date.now();
@@ -9342,9 +9417,11 @@ var ClaudeSession = class {
9342
9417
  scan.awaitingOwnPrompt = null;
9343
9418
  }
9344
9419
  if (rec) this.notePlanApproval(rec);
9420
+ if (rec?.type === "queue-operation" && rec.operation === "enqueue") this.enqueued++;
9345
9421
  scan.onRecord(s, rec);
9346
9422
  if (rec) {
9347
9423
  if (rec.type === "assistant" && rec.message) {
9424
+ scan.sawReply = true;
9348
9425
  const reason = rec.message.stop_reason;
9349
9426
  if (typeof reason === "string") {
9350
9427
  scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
@@ -9433,6 +9510,29 @@ var ClaudeSession = class {
9433
9510
  this.write("\r");
9434
9511
  return true;
9435
9512
  }
9513
+ /**
9514
+ * Type a message into the running turn's own input queue, not as a turn of
9515
+ * its own (h #g206). Measured on 2.1.288: text pasted and submitted while a
9516
+ * tool call runs is recorded as `queue-operation` enqueue and remove plus a
9517
+ * `queued_command` attachment, answered in the same turn, which still ends
9518
+ * once. Resolves true once the enqueue is in the transcript; false with no
9519
+ * turn running, or with a plan approval dialog up, which would take the
9520
+ * keys as its answer.
9521
+ */
9522
+ async queueInput(text, timeoutMs = 3e3) {
9523
+ if (!this.turn || !this.proc || this.exited || this.pendingPlanApproval) return false;
9524
+ const before = this.enqueued;
9525
+ this.write(this.o.bracketedPaste ? "\x1B[200~" + text + "\x1B[201~" : text);
9526
+ await delay(this.o.submitMinMs);
9527
+ this.write("\r");
9528
+ const deadline = Date.now() + timeoutMs;
9529
+ while (this.enqueued === before) {
9530
+ if (!this.turn || this.exited || Date.now() >= deadline) return false;
9531
+ await delay(this.o.pollMs);
9532
+ this.scanTranscript();
9533
+ }
9534
+ return true;
9535
+ }
9436
9536
  /** Wait until the PTY has been quiet for `permissionQuietMs`, at most 3 s. */
9437
9537
  async waitForQuiet() {
9438
9538
  const until = Date.now() + 3e3;
@@ -10058,38 +10158,19 @@ function spawnInteractiveProcess(opts) {
10058
10158
  },
10059
10159
  flushTranscript: () => session.flushTranscript(),
10060
10160
  planApprovalPending: () => session.pendingPlanApproval !== null,
10161
+ // Text only: an image in a mid-call message is dropped with the
10162
+ // decoder's own warning rather than staged for a turn that is not ours.
10163
+ queueInput: async (content) => {
10164
+ const text = decodeUserEnvelope(JSON.stringify({ type: "user", message: { role: "user", content } })).trim();
10165
+ if (!text) return false;
10166
+ return session.queueInput(text);
10167
+ },
10061
10168
  askAside
10062
10169
  }
10063
10170
  };
10064
10171
  return ap;
10065
10172
  }
10066
10173
 
10067
- // src/transport.ts
10068
- function requestedTransport(config, env = process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT) {
10069
- if (config.transport !== void 0) {
10070
- if (!["auto", "headless", "interactive"].includes(config.transport)) {
10071
- throw new Error(`Unknown Claude transport: ${config.transport}`);
10072
- }
10073
- return config.transport;
10074
- }
10075
- const enabled = config.interactive ?? (env !== void 0 && !["", "0", "false", "no", "off"].includes(env.trim().toLowerCase()));
10076
- return enabled ? "interactive" : "headless";
10077
- }
10078
- function hasInteractiveTransport() {
10079
- const runtime = globalThis;
10080
- return typeof runtime.Bun?.Terminal === "function";
10081
- }
10082
- async function selectTransport(requested, cliPath, available = hasInteractiveTransport()) {
10083
- if (requested === "headless") return "headless";
10084
- if (requested === "interactive") {
10085
- if (!available) throw new Error("Interactive Claude transport requires Bun.Terminal. Run opencode under a Bun runtime with PTY support.");
10086
- return "interactive";
10087
- }
10088
- if (await detectHeadlessSupport(cliPath) !== "unsupported") return "headless";
10089
- 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.");
10090
- return "interactive";
10091
- }
10092
-
10093
10174
  // src/prompts.ts
10094
10175
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
10095
10176
  import { homedir as homedir8 } from "os";
@@ -13333,6 +13414,7 @@ var ClaudeCodeLanguageModel = class {
13333
13414
  return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
13334
13415
  }
13335
13416
  const [runtimeStatus, cliVersion] = prologue.value;
13417
+ if (useInteractive) reportUnmeasuredInteractiveCli(cliVersion);
13336
13418
  const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
13337
13419
  const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
13338
13420
  if (ptyReadOnly && !ptyReadOnlySupported) {
@@ -14292,9 +14374,10 @@ var ClaudeCodeLanguageModel = class {
14292
14374
  state.proc.on("error", procErrorHandler);
14293
14375
  if (hasMatchedPendingResults) {
14294
14376
  state.cliAskedForWork = true;
14295
- const forwardTrailingUserMessages = () => {
14377
+ const forwardTrailingUserMessages = async () => {
14296
14378
  if (compactionMode || !state.activeProcess) return 0;
14297
- if (state.activeProcess.interactiveControl) return 0;
14379
+ const control = state.activeProcess.interactiveControl;
14380
+ if (control && (!control.queueInput || !control.turnRunning())) return 0;
14298
14381
  const trailing = getTrailingUserMessages(effectivePrompt, {
14299
14382
  stripContextReminders: self.stripContextRemindersEnabled()
14300
14383
  });
@@ -14305,6 +14388,21 @@ var ClaudeCodeLanguageModel = class {
14305
14388
  assistantIndex: trailing.assistantIndex,
14306
14389
  count: trailing.messages.length
14307
14390
  };
14391
+ if (control) {
14392
+ let queued = 0;
14393
+ for (const content of fresh) if (await control.queueInput(content)) queued++;
14394
+ if (queued < fresh.length) {
14395
+ log.warn("the interactive turn did not take a forwarded user message", {
14396
+ sessionKey: sk,
14397
+ messages: fresh.length - queued
14398
+ });
14399
+ }
14400
+ log.info("forwarded user messages into the interactive turn's input queue", {
14401
+ sessionKey: sk,
14402
+ messages: queued
14403
+ });
14404
+ return queued;
14405
+ }
14308
14406
  for (const content of fresh) {
14309
14407
  state.proc.stdin?.write(
14310
14408
  JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n"
@@ -14316,7 +14414,7 @@ var ClaudeCodeLanguageModel = class {
14316
14414
  });
14317
14415
  return fresh.length;
14318
14416
  };
14319
- if (!state.unattendedTurnEnded && forwardTrailingUserMessages() > 0) {
14417
+ if (!state.unattendedTurnEnded && await forwardTrailingUserMessages() > 0) {
14320
14418
  await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
14321
14419
  if (state.controllerClosed) return;
14322
14420
  }
@@ -14351,7 +14449,7 @@ var ClaudeCodeLanguageModel = class {
14351
14449
  }
14352
14450
  if (state.unattendedTurnEnded) {
14353
14451
  deliverPendingCompletions(state);
14354
- forwardTrailingUserMessages();
14452
+ await forwardTrailingUserMessages();
14355
14453
  }
14356
14454
  const unemitted = getPendingProxyCalls(sk).filter(
14357
14455
  (call) => !call.emitted