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

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 });
@@ -3783,12 +3798,16 @@ async function fetchSessionParentId(sessionID) {
3783
3798
  return void 0;
3784
3799
  }
3785
3800
  }
3786
- async function fetchSessionRunState(sessionID) {
3801
+ async function fetchSessionRunState(sessionID, directory) {
3787
3802
  if (!sessionID || sessionID === "default") return "unknown";
3788
3803
  const status = opencodeClient?.session?.status;
3789
3804
  if (!status) return "unknown";
3790
3805
  try {
3791
- const res = await status.call(opencodeClient.session);
3806
+ const dir = directory ?? await fetchSessionDirectory(sessionID);
3807
+ const res = await status.call(
3808
+ opencodeClient.session,
3809
+ dir ? { query: { directory: dir } } : void 0
3810
+ );
3792
3811
  const data = res.data;
3793
3812
  if (!data || typeof data !== "object") return "unknown";
3794
3813
  const entry = data[sessionID];
@@ -3808,8 +3827,10 @@ async function settleSessionRunState(sessionID, options = {}) {
3808
3827
  const windowMs = options.windowMs ?? 1500;
3809
3828
  const started = Date.now();
3810
3829
  let last = "unknown";
3830
+ let directory = options.directory;
3811
3831
  for (; ; ) {
3812
- const state = await fetchSessionRunState(sessionID);
3832
+ directory ??= await fetchSessionDirectory(sessionID);
3833
+ const state = await fetchSessionRunState(sessionID, directory ?? "");
3813
3834
  if (state === "busy") return "busy";
3814
3835
  if (state !== "unknown") last = state;
3815
3836
  if (Date.now() - started >= windowMs) return last;
@@ -5425,6 +5446,7 @@ function snapshotActiveProcesses(now = Date.now()) {
5425
5446
  ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
5426
5447
  effort: ap.effort,
5427
5448
  attached: ap.lineEmitter.listenerCount("line") > 0,
5449
+ transport: ap.interactiveControl ? "interactive" : "headless",
5428
5450
  proxyUrl: ap.proxyServer?.url,
5429
5451
  lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
5430
5452
  });
@@ -5839,6 +5861,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5839
5861
  "skipping remote MCP server with no url",
5840
5862
  "stale scratch directory sweep failed",
5841
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",
5842
5865
  "this opencode process is running an older plugin build than the one on disk",
5843
5866
  "timed out pending proxy call",
5844
5867
  "turn abort listener threw",
@@ -5988,6 +6011,7 @@ var BUNDLE_DATA_ALLOWLIST = {
5988
6011
  toolNames: "name",
5989
6012
  cliFloor: "version",
5990
6013
  cliVersion: "version",
6014
+ measuredCli: "version",
5991
6015
  // The two halves of the stale-build WARN: what this process loaded and what
5992
6016
  // is on disk now. Both are this package's own version strings.
5993
6017
  loadedVersion: "version",
@@ -6447,6 +6471,34 @@ import * as fs4 from "fs";
6447
6471
  import * as path5 from "path";
6448
6472
  import { promisify as promisify2 } from "util";
6449
6473
  import { fileURLToPath } from "url";
6474
+
6475
+ // src/transport.ts
6476
+ function requestedTransport(config, env = process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT) {
6477
+ if (config.transport !== void 0) {
6478
+ if (!["auto", "headless", "interactive"].includes(config.transport)) {
6479
+ throw new Error(`Unknown Claude transport: ${config.transport}`);
6480
+ }
6481
+ return config.transport;
6482
+ }
6483
+ const enabled = config.interactive ?? (env !== void 0 && !["", "0", "false", "no", "off"].includes(env.trim().toLowerCase()));
6484
+ return enabled ? "interactive" : "headless";
6485
+ }
6486
+ function hasInteractiveTransport() {
6487
+ const runtime = globalThis;
6488
+ return typeof runtime.Bun?.Terminal === "function";
6489
+ }
6490
+ async function selectTransport(requested, cliPath, available = hasInteractiveTransport()) {
6491
+ if (requested === "headless") return "headless";
6492
+ if (requested === "interactive") {
6493
+ if (!available) throw new Error("Interactive Claude transport requires Bun.Terminal. Run opencode under a Bun runtime with PTY support.");
6494
+ return "interactive";
6495
+ }
6496
+ if (await detectHeadlessSupport(cliPath) !== "unsupported") return "headless";
6497
+ 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.");
6498
+ return "interactive";
6499
+ }
6500
+
6501
+ // src/startup-diagnostics.ts
6450
6502
  var cachedPluginVersion;
6451
6503
  function pluginVersion() {
6452
6504
  if (cachedPluginVersion) return cachedPluginVersion;
@@ -6529,6 +6581,15 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
6529
6581
  error: err instanceof Error ? err.message : String(err)
6530
6582
  });
6531
6583
  }
6584
+ let transport;
6585
+ try {
6586
+ transport = requestedTransport({
6587
+ transport: firstOption(providers, "transport"),
6588
+ interactive: firstOption(providers, "interactive")
6589
+ });
6590
+ } catch {
6591
+ transport = "invalid";
6592
+ }
6532
6593
  return {
6533
6594
  plugin: pluginVersion(),
6534
6595
  opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
@@ -6541,7 +6602,8 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
6541
6602
  permissionPresets: Object.entries(providers).map(
6542
6603
  ([name, entry]) => summarizePermissionPreset(name, entry?.options)
6543
6604
  ),
6544
- interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
6605
+ transport,
6606
+ interactiveTransport: transport === "interactive",
6545
6607
  planModeQuestion: firstOption(providers, "planModeQuestion") === true,
6546
6608
  anthropicApiKeyInEnv: Boolean(
6547
6609
  process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
@@ -6810,7 +6872,14 @@ function formatDoctorReport(report) {
6810
6872
  lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
6811
6873
  lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
6812
6874
  lines.push(`| permissionPreset | ${describePermissionPresets(report.permissionPresets)} |`);
6813
- lines.push(`| transport | ${report.transport} |`);
6875
+ lines.push(
6876
+ `| transport | ${report.transport}${report.transport === "auto" ? " (headless until claude refuses `--print`)" : ""}; this conversation: ${report.transportInUse} |`
6877
+ );
6878
+ if (report.interactiveMeasured) {
6879
+ lines.push(
6880
+ `| 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\`)` : ""} |`
6881
+ );
6882
+ }
6814
6883
  lines.push(`| planModeQuestion | ${report.planModeQuestion} |`);
6815
6884
  lines.push(`| turnStats | ${report.turnStats} |`);
6816
6885
  lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
@@ -6831,11 +6900,11 @@ function formatDoctorReport(report) {
6831
6900
  if (report.processes.length === 0) {
6832
6901
  lines.push("None. The next message in a Claude Code session spawns one.");
6833
6902
  } else {
6834
- lines.push("| session | model | pid | in flight | age | effort |");
6835
- lines.push("|---|---|---|---|---|---|");
6903
+ lines.push("| session | model | transport | pid | in flight | age | effort |");
6904
+ lines.push("|---|---|---|---|---|---|---|");
6836
6905
  for (const proc of report.processes) {
6837
6906
  lines.push(
6838
- `| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
6907
+ `| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.transport} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
6839
6908
  );
6840
6909
  }
6841
6910
  }
@@ -7055,7 +7124,9 @@ async function gatherDoctorReport(options) {
7055
7124
  proxyTools: base.proxyTools,
7056
7125
  mcpServers: base.mcpServers,
7057
7126
  permissionPresets: base.permissionPresets,
7058
- transport: options.interactive || base.interactiveTransport ? "interactive" : "headless",
7127
+ transport: base.transport,
7128
+ transportInUse: options.interactive ? "interactive" : "headless",
7129
+ interactiveMeasured: base.transport !== "headless" || options.interactive || processes.some((proc) => proc.transport === "interactive") ? { measured: INTERACTIVE_MEASURED_CLI, unmeasured: isUnmeasuredInteractiveCli(cli) } : void 0,
7059
7130
  planModeQuestion: base.planModeQuestion,
7060
7131
  turnStats: options.turnStats,
7061
7132
  anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
@@ -8219,6 +8290,38 @@ Now continuing with the current message:
8219
8290
  }
8220
8291
  });
8221
8292
  }
8293
+ function getTrailingUserMessages(prompt, opts = {}) {
8294
+ prompt = stripAccountFailoverParts(prompt);
8295
+ if (opts.stripContextReminders) prompt = stripContextReminders(prompt).prompt;
8296
+ let assistantIndex = -1;
8297
+ for (let i = prompt.length - 1; i >= 0; i--) {
8298
+ if (prompt[i].role === "assistant") {
8299
+ assistantIndex = i;
8300
+ break;
8301
+ }
8302
+ }
8303
+ const messages = [];
8304
+ for (let i = assistantIndex + 1; i < prompt.length; i++) {
8305
+ const msg = prompt[i];
8306
+ if (msg.role !== "user") continue;
8307
+ if (parseSideQuestionContent(msg.content) !== null) continue;
8308
+ const blocks = [];
8309
+ if (typeof msg.content === "string") {
8310
+ if (msg.content.trim()) blocks.push({ type: "text", text: msg.content });
8311
+ } else if (Array.isArray(msg.content)) {
8312
+ for (const part of msg.content) {
8313
+ if (part.type === "text") {
8314
+ if (part.text && part.text.trim()) blocks.push({ type: "text", text: part.text });
8315
+ } else if (part.type === "file" || part.type === "image") {
8316
+ const block = toImageBlock(part);
8317
+ if (block) blocks.push(block);
8318
+ }
8319
+ }
8320
+ }
8321
+ if (blocks.length > 0) messages.push(blocks);
8322
+ }
8323
+ return { assistantIndex, messages };
8324
+ }
8222
8325
 
8223
8326
  // src/skill-bridge.ts
8224
8327
  import * as crypto3 from "crypto";
@@ -10026,32 +10129,6 @@ function spawnInteractiveProcess(opts) {
10026
10129
  return ap;
10027
10130
  }
10028
10131
 
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
-
10055
10132
  // src/prompts.ts
10056
10133
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
10057
10134
  import { homedir as homedir8 } from "os";
@@ -12502,6 +12579,7 @@ async function isProxyCallStillServed(callId) {
12502
12579
  return await fetchSessionRunState(session) === "busy";
12503
12580
  }
12504
12581
  setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
12582
+ var FORWARD_SETTLE_MS = 250;
12505
12583
  var CHILD_EXIT_STATUS_GRACE_MS = 250;
12506
12584
  var ClaudeCodeLanguageModel = class {
12507
12585
  specificationVersion = "v3";
@@ -13294,6 +13372,7 @@ var ClaudeCodeLanguageModel = class {
13294
13372
  return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
13295
13373
  }
13296
13374
  const [runtimeStatus, cliVersion] = prologue.value;
13375
+ if (useInteractive) reportUnmeasuredInteractiveCli(cliVersion);
13297
13376
  const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
13298
13377
  const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
13299
13378
  if (ptyReadOnly && !ptyReadOnlySupported) {
@@ -13422,7 +13501,7 @@ var ClaudeCodeLanguageModel = class {
13422
13501
  });
13423
13502
  return;
13424
13503
  }
13425
- log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
13504
+ log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason, session: stopped });
13426
13505
  void interruptTurn(stoppedProcess).then((idle) => {
13427
13506
  log.info("interrupt sent for aborted turn", { sk, idle });
13428
13507
  });
@@ -14253,6 +14332,34 @@ var ClaudeCodeLanguageModel = class {
14253
14332
  state.proc.on("error", procErrorHandler);
14254
14333
  if (hasMatchedPendingResults) {
14255
14334
  state.cliAskedForWork = true;
14335
+ const forwardTrailingUserMessages = () => {
14336
+ if (compactionMode || !state.activeProcess) return 0;
14337
+ if (state.activeProcess.interactiveControl) return 0;
14338
+ const trailing = getTrailingUserMessages(effectivePrompt, {
14339
+ stripContextReminders: self.stripContextRemindersEnabled()
14340
+ });
14341
+ const sent = state.activeProcess.forwardedUserMessages?.assistantIndex === trailing.assistantIndex ? state.activeProcess.forwardedUserMessages.count : 0;
14342
+ const fresh = trailing.messages.slice(sent);
14343
+ if (fresh.length === 0) return 0;
14344
+ state.activeProcess.forwardedUserMessages = {
14345
+ assistantIndex: trailing.assistantIndex,
14346
+ count: trailing.messages.length
14347
+ };
14348
+ for (const content of fresh) {
14349
+ state.proc.stdin?.write(
14350
+ JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n"
14351
+ );
14352
+ }
14353
+ log.info("forwarded user messages that arrived beside tool results", {
14354
+ sessionKey: sk,
14355
+ messages: fresh.length
14356
+ });
14357
+ return fresh.length;
14358
+ };
14359
+ if (!state.unattendedTurnEnded && forwardTrailingUserMessages() > 0) {
14360
+ await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
14361
+ if (state.controllerClosed) return;
14362
+ }
14256
14363
  for (const { call, result } of previousPendingProxyMatches) {
14257
14364
  if (result) {
14258
14365
  const channelClosed = isPendingProxyCallChannelClosed(call);
@@ -14282,7 +14389,10 @@ var ClaudeCodeLanguageModel = class {
14282
14389
  );
14283
14390
  }
14284
14391
  }
14285
- if (state.unattendedTurnEnded) deliverPendingCompletions(state);
14392
+ if (state.unattendedTurnEnded) {
14393
+ deliverPendingCompletions(state);
14394
+ forwardTrailingUserMessages();
14395
+ }
14286
14396
  const unemitted = getPendingProxyCalls(sk).filter(
14287
14397
  (call) => !call.emitted
14288
14398
  );
@@ -14311,7 +14421,10 @@ var ClaudeCodeLanguageModel = class {
14311
14421
  }
14312
14422
  }
14313
14423
  state.cliAskedForWork = true;
14314
- if (state.activeProcess) noteTurnStarted(state.activeProcess);
14424
+ if (state.activeProcess) {
14425
+ noteTurnStarted(state.activeProcess);
14426
+ state.activeProcess.forwardedUserMessages = void 0;
14427
+ }
14315
14428
  state.proc.stdin?.write(userMsg + "\n");
14316
14429
  log.debug("sent user message", { textLength: userMsg.length });
14317
14430
  armStartWatchdog(state);