@khalilgharbaoui/opencode-claude-code-plugin 0.42.3 → 0.44.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
@@ -1327,7 +1327,7 @@ function formatUsageLimitNote(input) {
1327
1327
  const what = [
1328
1328
  `the Claude account "${account}" is out of usage`,
1329
1329
  window ? ` in ${window}` : "",
1330
- resets ? `, which resets at ${resets}` : "",
1330
+ resets ? `, which resets at ${resets}` : input.resetsText ? `, which resets ${input.resetsText}` : "",
1331
1331
  "."
1332
1332
  ].join("");
1333
1333
  const advice = others ? `Pick a model from ${others} and resend your message, or wait for the window to reset.` : "Wait for the window to reset, or enable extra usage on the account.";
@@ -1341,6 +1341,13 @@ function isAccountLimitError(input) {
1341
1341
  if (typeof text !== "string" || text.length === 0) return false;
1342
1342
  return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
1343
1343
  }
1344
+ function limitFactsFromText(text) {
1345
+ const facts = {};
1346
+ if (/\bsession limit\b/i.test(text)) facts.window = "five_hour";
1347
+ const reset = /\bresets\s+([^·\n]{1,60}?)\s*$/im.exec(text);
1348
+ if (reset) facts.resetsText = reset[1].trim();
1349
+ return facts;
1350
+ }
1344
1351
  var lastAccountLimits = /* @__PURE__ */ new Map();
1345
1352
  function rememberAccountLimit(account, facts) {
1346
1353
  const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
@@ -1423,7 +1430,6 @@ async function resolveFailoverSpawn(input) {
1423
1430
  }
1424
1431
  function isAccountFailoverQuestionActive(input) {
1425
1432
  if (input.compactionMode) return false;
1426
- if (input.interactive) return false;
1427
1433
  if (input.childSession) return false;
1428
1434
  if (input.configured !== "ask") return false;
1429
1435
  if (input.candidates.length === 0) return false;
@@ -1723,6 +1729,10 @@ function cliSupportsDontAsk(v) {
1723
1729
  if (!v) return false;
1724
1730
  return gte(v, { major: 2, minor: 1, patch: 263 });
1725
1731
  }
1732
+ function cliSupportsInteractiveBypass(v) {
1733
+ if (!v) return false;
1734
+ return gte(v, { major: 2, minor: 1, patch: 263 });
1735
+ }
1726
1736
  var INTERACTIVE_MEASURED_CLI = "2.1.288";
1727
1737
  var INTERACTIVE_MEASURED = { major: 2, minor: 1, patch: 288 };
1728
1738
  function isUnmeasuredInteractiveCli(v) {
@@ -4559,18 +4569,36 @@ function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAUL
4559
4569
  };
4560
4570
  }
4561
4571
  function interactivePermissionPosture(input) {
4562
- if (!isReadOnlyPermissionMode(input.permissionMode)) {
4563
- return { permissionMode: input.permissionMode, restricted: false, allow: input.allow };
4572
+ if (isReadOnlyPermissionMode(input.permissionMode)) {
4573
+ if (!input.supportsReadOnly) return null;
4574
+ const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
4575
+ return {
4576
+ permissionMode: "dontAsk",
4577
+ restricted: true,
4578
+ allow: input.allow.filter(
4579
+ (rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
4580
+ ),
4581
+ bypass: false,
4582
+ disallowed: []
4583
+ };
4564
4584
  }
4565
- if (!input.supportsReadOnly) return null;
4566
- const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
4567
- return {
4568
- permissionMode: "dontAsk",
4569
- restricted: true,
4570
- allow: input.allow.filter(
4571
- (rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
4572
- )
4585
+ const unchanged = {
4586
+ permissionMode: input.permissionMode === "bypassPermissions" ? void 0 : input.permissionMode,
4587
+ restricted: false,
4588
+ allow: input.allow,
4589
+ bypass: false,
4590
+ disallowed: []
4573
4591
  };
4592
+ if (input.permissionMode === "plan" || !input.supportsBypass) return unchanged;
4593
+ const perTool = Object.entries(input.controlRequestToolBehaviors ?? {});
4594
+ const named = (behavior) => perTool.filter(([, value]) => value === behavior).map(([tool]) => tool);
4595
+ if (input.skipPermissions !== false || input.permissionMode === "bypassPermissions") {
4596
+ return { ...unchanged, bypass: true };
4597
+ }
4598
+ if ((input.controlRequestBehavior ?? "allow") === "allow") {
4599
+ return { ...unchanged, bypass: true, disallowed: named("deny") };
4600
+ }
4601
+ return { ...unchanged, permissionMode: "dontAsk", allow: named("allow") };
4574
4602
  }
4575
4603
  function isReadOnlyPermissionMode(mode) {
4576
4604
  return mode === READ_ONLY_PERMISSION_MODE;
@@ -5825,7 +5853,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5825
5853
  "interactive transport denied a permission prompt",
5826
5854
  "interactive transport dropped non-text content blocks",
5827
5855
  "interactive turn failed",
5828
- "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
5856
+ "interactiveBypass is ignored: skipPermissions governs the interactive transport too",
5829
5857
  "malformed data URI, skipping file part",
5830
5858
  "no active process to respawn (start watchdog); ending turn",
5831
5859
  "no proxyTools entry was recognised; nothing will be proxied this turn",
@@ -7542,6 +7570,20 @@ function parseModelId(modelId) {
7542
7570
  if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
7543
7571
  return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
7544
7572
  }
7573
+ var WEB_SEARCH_USD = 0.01;
7574
+ function apiCallCostUsd(model, usage) {
7575
+ if (typeof model !== "string" || !usage || typeof usage !== "object") return null;
7576
+ const base = model.replace(/@.*$/, "").replace(/\[1m\]$/i, "").replace(/-\d{8}$/, "");
7577
+ const entry = (usage.speed === "fast" ? defaultModels[`${base}${FAST_SUFFIX}`] : void 0) ?? defaultModels[base];
7578
+ if (!entry) return null;
7579
+ const cost = entry.cost;
7580
+ const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
7581
+ const split = usage.cache_creation;
7582
+ const write1h = split ? count(split.ephemeral_1h_input_tokens) : 0;
7583
+ const write5m = split ? count(split.ephemeral_5m_input_tokens) : count(usage.cache_creation_input_tokens);
7584
+ const perMillion = count(usage.input_tokens) * cost.input + count(usage.output_tokens) * cost.output + count(usage.cache_read_input_tokens) * cost.cache.read + write5m * cost.cache.write + write1h * cost.input * 2;
7585
+ return perMillion / 1e6 + count(usage.server_tool_use?.web_search_requests) * WEB_SEARCH_USD;
7586
+ }
7545
7587
 
7546
7588
  // src/agent-models.ts
7547
7589
  var AGENT_DIR_NAMES = ["agents", "agent"];
@@ -7691,6 +7733,13 @@ function parseAgentFrontmatter(text) {
7691
7733
  }
7692
7734
  return record;
7693
7735
  }
7736
+ function agentMarkdownBody(text) {
7737
+ if (!text.startsWith("---")) return text.trim() || void 0;
7738
+ const lines = text.split(/\r?\n/);
7739
+ const close = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
7740
+ if (close === -1) return void 0;
7741
+ return lines.slice(close + 1).join("\n").trim() || void 0;
7742
+ }
7694
7743
  function parseFallbackModelList(value) {
7695
7744
  if (Array.isArray(value)) {
7696
7745
  return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
@@ -7714,6 +7763,8 @@ async function readAgentMarkdownRecords(directories) {
7714
7763
  try {
7715
7764
  const text = await readFile(path7.join(directory, entry), "utf8");
7716
7765
  records2[name] = parseAgentFrontmatter(text);
7766
+ const prompt = agentMarkdownBody(text);
7767
+ if (prompt) records2[name].prompt = prompt;
7717
7768
  } catch (err) {
7718
7769
  log.debug("failed to read agent markdown", {
7719
7770
  file: path7.join(directory, entry),
@@ -8861,6 +8912,7 @@ function isApiCallRecord(rec) {
8861
8912
  }
8862
8913
  var TurnUsageAccumulator = class {
8863
8914
  calls = /* @__PURE__ */ new Map();
8915
+ models = /* @__PURE__ */ new Map();
8864
8916
  lastKey = null;
8865
8917
  /** Feed every transcript record of the turn, in file order. */
8866
8918
  add(rec) {
@@ -8868,8 +8920,24 @@ var TurnUsageAccumulator = class {
8868
8920
  const id = rec.message.id;
8869
8921
  const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
8870
8922
  this.calls.set(key, rec.message.usage);
8923
+ this.models.set(key, rec.message.model);
8871
8924
  this.lastKey = key;
8872
8925
  }
8926
+ /**
8927
+ * The turn's list-price cost over its distinct calls, the headless
8928
+ * `result`'s `total_cost_usd` (h #g208). Null when no call was made or any
8929
+ * call's model is one the catalog cannot price, never a partial sum.
8930
+ */
8931
+ get costUsd() {
8932
+ if (this.calls.size === 0) return null;
8933
+ let total = 0;
8934
+ for (const [key, usage] of this.calls) {
8935
+ const cost = apiCallCostUsd(this.models.get(key), usage);
8936
+ if (cost === null) return null;
8937
+ total += cost;
8938
+ }
8939
+ return total;
8940
+ }
8873
8941
  /** Distinct API calls seen, for diagnostics. */
8874
8942
  get callCount() {
8875
8943
  return this.calls.size;
@@ -9620,11 +9688,29 @@ var ClaudeSession = class {
9620
9688
  async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
9621
9689
  const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
9622
9690
  const usage = new TurnUsageAccumulator();
9691
+ const startedAt = Date.now();
9692
+ let reportedDurationMs = null;
9693
+ let apiError = null;
9623
9694
  const { stopReason, end, denied } = await this.runTurnLoop(
9624
9695
  prompt,
9625
9696
  (raw, rec) => {
9626
9697
  onLine(raw);
9627
9698
  if (rec) usage.add(rec);
9699
+ if (rec?.type === "assistant") {
9700
+ if (rec.isApiErrorMessage === true) {
9701
+ const text = (Array.isArray(rec.message?.content) ? rec.message.content : []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
9702
+ apiError = {
9703
+ text,
9704
+ ...typeof rec.apiErrorStatus === "number" ? { status: rec.apiErrorStatus } : {},
9705
+ ...typeof rec.error === "string" ? { kind: rec.error } : {}
9706
+ };
9707
+ } else if (rec.message?.model !== "<synthetic>") {
9708
+ apiError = null;
9709
+ }
9710
+ }
9711
+ if (isTurnDurationRecord(rec) && typeof rec.durationMs === "number") {
9712
+ reportedDurationMs = rec.durationMs;
9713
+ }
9628
9714
  },
9629
9715
  timeout,
9630
9716
  onHeartbeat
@@ -9642,6 +9728,9 @@ var ClaudeSession = class {
9642
9728
  usage: usage.turnTotal,
9643
9729
  lastCallUsage: usage.lastCall,
9644
9730
  callCount: usage.callCount,
9731
+ costUsd: usage.costUsd,
9732
+ durationMs: reportedDurationMs ?? Date.now() - startedAt,
9733
+ apiError,
9645
9734
  denied
9646
9735
  };
9647
9736
  }
@@ -9789,13 +9878,19 @@ function interactiveExtraArgs(opts) {
9789
9878
  if (opts.fastMode) {
9790
9879
  flagSettings.fastMode = true;
9791
9880
  }
9881
+ if (opts.bypass) {
9882
+ flagSettings.skipDangerousModePermissionPrompt = true;
9883
+ extraArgs.push("--dangerously-skip-permissions");
9884
+ }
9792
9885
  if (Object.keys(flagSettings).length > 0) {
9793
9886
  extraArgs.push("--settings", JSON.stringify(flagSettings));
9794
9887
  }
9795
9888
  if (opts.permissionMode === "bypassPermissions") {
9796
- log.warn(
9797
- "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
9798
- );
9889
+ if (!opts.bypass) {
9890
+ log.warn(
9891
+ "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
9892
+ );
9893
+ }
9799
9894
  } else if (opts.permissionMode) {
9800
9895
  extraArgs.push("--permission-mode", opts.permissionMode);
9801
9896
  }
@@ -9847,6 +9942,7 @@ function heartbeatFrame(sessionId, openCall, now = Date.now()) {
9847
9942
  }
9848
9943
  function interactiveResultFrame(opts) {
9849
9944
  const completed = opts.end === "stop" && !!opts.stopReason;
9945
+ const apiError = completed && opts.apiError ? opts.apiError : null;
9850
9946
  let result;
9851
9947
  let terminalReason = "completed";
9852
9948
  if (!completed) {
@@ -9864,20 +9960,35 @@ function interactiveResultFrame(opts) {
9864
9960
  return JSON.stringify({
9865
9961
  type: "result",
9866
9962
  subtype: completed ? "success" : "error_during_execution",
9867
- is_error: !completed,
9963
+ is_error: !completed || apiError !== null,
9868
9964
  stop_reason: completed ? opts.stopReason : null,
9869
9965
  terminal_reason: terminalReason,
9870
- result,
9966
+ result: apiError ? apiError.text : result,
9967
+ ...apiError?.status !== void 0 ? { api_error_status: apiError.status } : {},
9871
9968
  session_id: opts.sessionId,
9872
9969
  usage: opts.usage ?? {},
9873
9970
  // The TUI has no `can_use_tool` channel, so a permission dialog is the
9874
9971
  // only way it asks; each one the session denied is reported in the
9875
9972
  // headless field's place, names and ids only (h #g109).
9876
9973
  permission_denials: opts.denials ?? [],
9877
- total_cost_usd: null,
9878
- duration_ms: 0
9974
+ total_cost_usd: opts.costUsd ?? null,
9975
+ duration_ms: opts.durationMs ?? 0
9879
9976
  });
9880
9977
  }
9978
+ function interactiveSpawnEnv2(env) {
9979
+ if (env.CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS !== void 0) return env;
9980
+ return { ...env, CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS: "0" };
9981
+ }
9982
+ function streamFrameFromRecord(raw) {
9983
+ if (!raw.includes('"isApiErrorMessage":true')) return raw;
9984
+ try {
9985
+ const rec = JSON.parse(raw);
9986
+ if (rec?.isApiErrorMessage !== true || rec.is_api_error_message !== void 0) return raw;
9987
+ return JSON.stringify({ ...rec, is_api_error_message: true });
9988
+ } catch {
9989
+ return raw;
9990
+ }
9991
+ }
9881
9992
  function carriesReplyText(line) {
9882
9993
  try {
9883
9994
  const rec = JSON.parse(line);
@@ -9915,6 +10026,7 @@ function spawnInteractiveProcess(opts) {
9915
10026
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
9916
10027
  effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
9917
10028
  resumeSessionId: opts.resumeSessionId,
10029
+ ...opts.forkOf ? { forkOf: opts.forkOf } : {},
9918
10030
  env: opts.env,
9919
10031
  spawnPty: opts.spawnPty,
9920
10032
  onScreen: (event) => {
@@ -9946,6 +10058,7 @@ function spawnInteractiveProcess(opts) {
9946
10058
  effort: opts.effort,
9947
10059
  sessionId: session.sessionId,
9948
10060
  resumed: !!opts.resumeSessionId,
10061
+ forkedFrom: opts.forkOf ?? null,
9949
10062
  jsonlPath: session.jsonlPath
9950
10063
  });
9951
10064
  let startPromise = null;
@@ -9975,14 +10088,14 @@ function spawnInteractiveProcess(opts) {
9975
10088
  }
9976
10089
  openCalls.clear();
9977
10090
  denials = [];
9978
- const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
10091
+ const { stopReason, end, usage, lastCallUsage, callCount, costUsd, durationMs, apiError, denied } = await session.tailTurn(
9979
10092
  userMsg,
9980
10093
  (raw) => {
9981
10094
  try {
9982
10095
  openCalls.add(JSON.parse(raw));
9983
10096
  } catch {
9984
10097
  }
9985
- emit2(raw);
10098
+ emit2(streamFrameFromRecord(raw));
9986
10099
  },
9987
10100
  void 0,
9988
10101
  // Only to a listening turn: it feeds that turn's watchdog, and the
@@ -10013,7 +10126,10 @@ function spawnInteractiveProcess(opts) {
10013
10126
  end,
10014
10127
  stopReason,
10015
10128
  usage,
10016
- denials
10129
+ denials,
10130
+ costUsd,
10131
+ durationMs,
10132
+ apiError
10017
10133
  })
10018
10134
  );
10019
10135
  } catch (err) {
@@ -10057,7 +10173,9 @@ function spawnInteractiveProcess(opts) {
10057
10173
  configDir: opts.configDir,
10058
10174
  model: opts.model,
10059
10175
  settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
10060
- extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk" }),
10176
+ // Tool-free whatever the main spawn's posture: `dontAsk` with nothing
10177
+ // allowed, and never the skip flag, which would win over it (h #g210).
10178
+ extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk", bypass: false }),
10061
10179
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
10062
10180
  effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
10063
10181
  env: opts.env,
@@ -10175,7 +10293,7 @@ function spawnInteractiveProcess(opts) {
10175
10293
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
10176
10294
  import { homedir as homedir8 } from "os";
10177
10295
  import { randomUUID as randomUUID6 } from "crypto";
10178
- import { dirname as dirname6, join as join11 } from "path";
10296
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
10179
10297
  function readPromptFileIfPresent(path11) {
10180
10298
  try {
10181
10299
  const content = readFileSync7(path11, "utf8").trim();
@@ -10281,6 +10399,36 @@ function extractSystemMessages(prompt) {
10281
10399
  }
10282
10400
  return out;
10283
10401
  }
10402
+ var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
10403
+ function userAuthoredInstructions(forwarded, opts = {}) {
10404
+ const readFile2 = opts.readFile ?? ((path11) => {
10405
+ try {
10406
+ return readFileSync7(path11, "utf8");
10407
+ } catch {
10408
+ return null;
10409
+ }
10410
+ });
10411
+ const out = [];
10412
+ const agentPrompt = opts.agentPrompt?.trim();
10413
+ if (agentPrompt && forwarded.includes(agentPrompt)) {
10414
+ out.push({ text: agentPrompt, content: agentPrompt });
10415
+ }
10416
+ const seen = /* @__PURE__ */ new Set();
10417
+ for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
10418
+ const path11 = match[1].trim();
10419
+ if (seen.has(path11) || !isAbsolute3(path11)) continue;
10420
+ if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path11))) continue;
10421
+ seen.add(path11);
10422
+ const file = readFile2(path11);
10423
+ if (!file?.trim()) continue;
10424
+ const start = match.index + match[0].length + 1;
10425
+ const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
10426
+ if (content === null) continue;
10427
+ out.push({ text: `Instructions from: ${path11}
10428
+ ${content}`, content });
10429
+ }
10430
+ return out;
10431
+ }
10284
10432
  function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystemContent = [], options = {}) {
10285
10433
  const parts = [];
10286
10434
  if (options.compressionSummary?.trim()) {
@@ -10305,6 +10453,11 @@ ${options.compressionSummary.trim()}`
10305
10453
  if (pushGlobal) parts.push(globalAgents);
10306
10454
  if (pushWorkspace) parts.push(workspaceAgents);
10307
10455
  if (pushGlobal || pushWorkspace) parts.push(AGENTS_MAINTENANCE_HINT);
10456
+ for (const instruction of options.userInstructions ?? []) {
10457
+ const authored = instruction.content.trim();
10458
+ if (authored === globalAgents || authored === workspaceAgents) continue;
10459
+ parts.push(instruction.text);
10460
+ }
10308
10461
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
10309
10462
  const content = parts.join("\n\n");
10310
10463
  if (!content) return void 0;
@@ -11162,6 +11315,7 @@ function createTurnState(init) {
11162
11315
  lastCallUsage: void 0,
11163
11316
  resultFailure: void 0,
11164
11317
  accountLimitHit: null,
11318
+ apiErrorTextShown: null,
11165
11319
  usageLimitNote: false,
11166
11320
  accountBlock: null,
11167
11321
  modelRefusal: null,
@@ -12253,11 +12407,21 @@ ${plan}
12253
12407
  state.hasReceivedContent = true;
12254
12408
  }
12255
12409
  const apiErrorReply = msg.is_api_error_message === true;
12256
- const limitErrorReply = apiErrorReply && ctx.usageLimitNoteActive && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
12410
+ const limitFromReply = apiErrorReply && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
12257
12411
  resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
12258
12412
  }));
12259
- if (limitErrorReply) {
12413
+ const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
12414
+ if (limitFromReply) {
12260
12415
  state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
12416
+ const said = limitFactsFromText(
12417
+ msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
12418
+ );
12419
+ const known = state.accountLimitHit;
12420
+ state.accountLimitHit = {
12421
+ ...known,
12422
+ window: known.window ?? said.window,
12423
+ ...known.resetsAt === void 0 && said.resetsText ? { resetsText: said.resetsText } : {}
12424
+ };
12261
12425
  }
12262
12426
  if (hasText && !hasToolUse) {
12263
12427
  startResultFallback(state);
@@ -12268,6 +12432,7 @@ ${plan}
12268
12432
  for (const block2 of msg.message.content) {
12269
12433
  if (block2.type === "text" && block2.text) {
12270
12434
  if (limitErrorReply) continue;
12435
+ if (apiErrorReply) state.apiErrorTextShown = block2.text;
12271
12436
  resetLastVisibleTextBlock(state);
12272
12437
  const blockId = state.startTextBlock();
12273
12438
  state.controller.enqueue({
@@ -12492,7 +12657,8 @@ ${plan}
12492
12657
  state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
12493
12658
  // and says what to run; two notes about one failure is one too many.
12494
12659
  !state.accountBlock && msg.is_error === true;
12495
- if (!state.currentTextId && msg.is_error && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
12660
+ if (!state.currentTextId && msg.is_error && // Already on screen from the reply frame, word for word.
12661
+ !(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
12496
12662
  const errId = state.startTextBlock();
12497
12663
  state.controller.enqueue({
12498
12664
  type: "text-delta",
@@ -13067,12 +13233,12 @@ var ClaudeCodeLanguageModel = class {
13067
13233
  if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
13068
13234
  const useInteractive = selectedTransport === "interactive";
13069
13235
  const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
13070
- const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
13236
+ const modelFallbackArmed = attempt?.armed === true && !compactionMode;
13071
13237
  const sourceAccount = normalizeAccountName(
13072
13238
  this.config.account ?? DEFAULT_ACCOUNT
13073
13239
  );
13074
13240
  const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
13075
- let failover = useInteractive || compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
13241
+ let failover = compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
13076
13242
  account: sourceAccount,
13077
13243
  baseCliPath,
13078
13244
  cliPath: this.config.cliPath,
@@ -13225,7 +13391,7 @@ var ClaudeCodeLanguageModel = class {
13225
13391
  deleteActiveProcess(sk);
13226
13392
  clearCompression(sk);
13227
13393
  }
13228
- const failoverAnswer = compactionMode || useInteractive || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
13394
+ const failoverAnswer = compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
13229
13395
  sourceAccount,
13230
13396
  candidates: failoverCandidates(this.config.failoverAccounts, sourceAccount)
13231
13397
  });
@@ -13288,7 +13454,7 @@ var ClaudeCodeLanguageModel = class {
13288
13454
  });
13289
13455
  }
13290
13456
  const processForAccount = getActiveProcess(sk);
13291
- if (!compactionMode && !useInteractive && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13457
+ if (!compactionMode && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13292
13458
  log.notice("claude process belongs to another account; starting fresh", {
13293
13459
  sessionKey: sk,
13294
13460
  was: processForAccount.cliPath,
@@ -13305,7 +13471,7 @@ var ClaudeCodeLanguageModel = class {
13305
13471
  includeHistoryContext = true;
13306
13472
  }
13307
13473
  let forkFromClaudeSessionId;
13308
- if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && !useInteractive && failoverAnswer?.kind !== "switch") {
13474
+ if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && failoverAnswer?.kind !== "switch") {
13309
13475
  const parent = findForkParent({
13310
13476
  sessionKey: sk,
13311
13477
  prompt: options.prompt,
@@ -13361,7 +13527,7 @@ var ClaudeCodeLanguageModel = class {
13361
13527
  });
13362
13528
  }
13363
13529
  }
13364
- if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
13530
+ if (this.config.forkSessions === true && !compactionMode) {
13365
13531
  recordForkFingerprint(sk, options.prompt, cliPath);
13366
13532
  }
13367
13533
  const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
@@ -13417,6 +13583,7 @@ var ClaudeCodeLanguageModel = class {
13417
13583
  if (useInteractive) reportUnmeasuredInteractiveCli(cliVersion);
13418
13584
  const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
13419
13585
  const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
13586
+ const ptyBypassSupported = cliSupportsInteractiveBypass(cliVersion);
13420
13587
  if (ptyReadOnly && !ptyReadOnlySupported) {
13421
13588
  throw new Error(
13422
13589
  "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."
@@ -13426,14 +13593,13 @@ var ClaudeCodeLanguageModel = class {
13426
13593
  this.config.failoverAccounts,
13427
13594
  sourceAccount
13428
13595
  );
13429
- const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && !useInteractive && // A `doGenerate` caller takes the override and reports the plain
13596
+ const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
13430
13597
  // rate-limit error, never the form. See `TurnMode`.
13431
13598
  mode !== "generate" && isAccountFailoverQuestionActive({
13432
13599
  configured: this.config.accountFailover,
13433
13600
  candidates: failoverAccounts,
13434
13601
  opencodeHasQuestion: (await loadLiveToolInfo()).hasQuestion,
13435
13602
  compactionMode,
13436
- interactive: !!useInteractive,
13437
13603
  // A subagent follows its parent's account for free, because the
13438
13604
  // override is account-scoped. Asking it would put a form in a session
13439
13605
  // the operator is usually not even looking at.
@@ -13784,6 +13950,14 @@ var ClaudeCodeLanguageModel = class {
13784
13950
  "WebFetch"
13785
13951
  ]
13786
13952
  ];
13953
+ const userInstructions = self.config.interactiveUserInstructions === true ? userAuthoredInstructions(extractSystemMessages(options.prompt).join("\n\n"), {
13954
+ agentPrompt: getAgentRegistry()[self.getOpencodeAgent(options) ?? ""]?.prompt
13955
+ }) : [];
13956
+ if (userInstructions.length > 0) {
13957
+ log.info("forwarding user-authored instructions to the interactive TUI", {
13958
+ count: userInstructions.length
13959
+ });
13960
+ }
13787
13961
  const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
13788
13962
  cwd,
13789
13963
  self.config.multiStepContinuation !== false,
@@ -13805,7 +13979,8 @@ var ClaudeCodeLanguageModel = class {
13805
13979
  opencodeCompressEnabled: wiring.opencodeToolDefs.some(
13806
13980
  (t) => t.name === "compress"
13807
13981
  ),
13808
- compressionSummary: getCompressionSummary(sk)
13982
+ compressionSummary: getCompressionSummary(sk),
13983
+ userInstructions
13809
13984
  }
13810
13985
  );
13811
13986
  if (self.config.interactiveSystemPrompt === false) {
@@ -13815,7 +13990,7 @@ var ClaudeCodeLanguageModel = class {
13815
13990
  }
13816
13991
  if (interactiveBypassRequested) {
13817
13992
  log.warn(
13818
- "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
13993
+ "interactiveBypass is ignored: skipPermissions governs the interactive transport too"
13819
13994
  );
13820
13995
  }
13821
13996
  const skillPluginDirs = await resolveSkillPluginDirs({
@@ -13829,13 +14004,19 @@ var ClaudeCodeLanguageModel = class {
13829
14004
  const posture = interactivePermissionPosture({
13830
14005
  permissionMode: self.config.permissionMode,
13831
14006
  allow,
13832
- supportsReadOnly: ptyReadOnlySupported
14007
+ supportsReadOnly: ptyReadOnlySupported,
14008
+ skipPermissions: self.config.skipPermissions,
14009
+ controlRequestBehavior: self.config.controlRequestBehavior,
14010
+ controlRequestToolBehaviors: self.config.controlRequestToolBehaviors,
14011
+ supportsBypass: ptyBypassSupported
13833
14012
  });
13834
14013
  if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
13835
14014
  const ap = spawnInteractiveProcess({
13836
14015
  cwd,
13837
14016
  cliPath,
13838
- configDir: self.config.configDir,
14017
+ // On an account switch, the other account's directory: the
14018
+ // TUI writes its transcript there (h #g209).
14019
+ configDir: self.skillBridgeSpawn(failover).configDir,
13839
14020
  model: spawnModelId,
13840
14021
  fastMode,
13841
14022
  mcpConfigPaths: mcp.paths,
@@ -13843,19 +14024,26 @@ var ClaudeCodeLanguageModel = class {
13843
14024
  permissionsAllow: posture.allow,
13844
14025
  permissionMode: posture.permissionMode,
13845
14026
  restricted: posture.restricted,
13846
- disallowedTools: wiring.allDisallowed,
14027
+ bypass: posture.bypass,
14028
+ disallowedTools: [...wiring.allDisallowed, ...posture.disallowed],
13847
14029
  proxyServer: state.proxyServer,
13848
14030
  systemPromptFile,
13849
14031
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13850
14032
  effort: reasoningEffort,
13851
14033
  resumeSessionId,
14034
+ // A fork of a conversation already served (`forkSessions`):
14035
+ // the TUI branches it instead of the history being replayed.
14036
+ ...forkFromClaudeSessionId && !resumeSessionId ? { forkOf: forkFromClaudeSessionId } : {},
13852
14037
  // The headless spawn's env, so hygiene, effort and the
13853
- // agent's prompt cache TTL reach both transports alike.
13854
- env: claudeSpawnEnv({
13855
- ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13856
- effort: reasoningEffort,
13857
- promptCacheTtl
13858
- })
14038
+ // agent's prompt cache TTL reach both transports alike, plus
14039
+ // what keeps a proxied call in the foreground (h #g210).
14040
+ env: interactiveSpawnEnv2(
14041
+ claudeSpawnEnv({
14042
+ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
14043
+ effort: reasoningEffort,
14044
+ promptCacheTtl
14045
+ })
14046
+ )
13859
14047
  });
13860
14048
  ap.proc.once(
13861
14049
  "exit",
@@ -13863,6 +14051,7 @@ var ClaudeCodeLanguageModel = class {
13863
14051
  );
13864
14052
  ap.mcpHash = mcp.bridgedHash;
13865
14053
  ap.mcpServers = mcp.allEnabledServerNames;
14054
+ ap.cliPath = cliPath;
13866
14055
  setActiveProcess(sk, ap);
13867
14056
  state.proc = ap.proc;
13868
14057
  state.lineEmitter = ap.lineEmitter;
@@ -13870,6 +14059,7 @@ var ClaudeCodeLanguageModel = class {
13870
14059
  log.info("spawned interactive claude session", {
13871
14060
  sk,
13872
14061
  resumed: !!resumeSessionId,
14062
+ forked: !!forkFromClaudeSessionId && !resumeSessionId,
13873
14063
  cliPath,
13874
14064
  configDir: self.config.configDir,
13875
14065
  model: effectiveModelId
@@ -14084,7 +14274,8 @@ var ClaudeCodeLanguageModel = class {
14084
14274
  sourceAccount,
14085
14275
  candidates: failoverAccounts,
14086
14276
  resetsAt: state.accountLimitHit?.resetsAt,
14087
- window: state.accountLimitHit?.window
14277
+ window: state.accountLimitHit?.window,
14278
+ resetsText: state.accountLimitHit?.resetsText
14088
14279
  })
14089
14280
  });
14090
14281
  state.endTextBlock();
@@ -15323,7 +15514,8 @@ function createClaudeCode(settings = {}) {
15323
15514
  interactive: settings.interactive,
15324
15515
  interactiveBypass: settings.interactiveBypass,
15325
15516
  interactiveAllowTools: settings.interactiveAllowTools,
15326
- interactiveSystemPrompt: settings.interactiveSystemPrompt
15517
+ interactiveSystemPrompt: settings.interactiveSystemPrompt,
15518
+ interactiveUserInstructions: settings.interactiveUserInstructions
15327
15519
  });
15328
15520
  };
15329
15521
  const provider = function(modelId) {
@@ -15542,7 +15734,8 @@ async function buildAgentRegistry(config) {
15542
15734
  forceModel: pick("forceModel") ?? records2[name]?.forceModel,
15543
15735
  reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
15544
15736
  cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
15545
- fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels
15737
+ fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels,
15738
+ prompt: pick("prompt") ?? records2[name]?.prompt
15546
15739
  };
15547
15740
  }
15548
15741
  setAgentRegistry(records2);