@khalilgharbaoui/opencode-claude-code-plugin 0.38.0 → 0.40.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
@@ -1,3 +1,6 @@
1
+ // src/claude-code-language-model.ts
2
+ import { join as join11 } from "path";
3
+
1
4
  // src/ids.ts
2
5
  import { randomInt } from "crypto";
3
6
  var ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -72,6 +75,14 @@ function parseInput(raw) {
72
75
  return {};
73
76
  }
74
77
  }
78
+ function hostedToolResult(part) {
79
+ const result = part.result;
80
+ if (part.providerExecuted !== true) return part.result;
81
+ if (!result || typeof result !== "object" || typeof result.output !== "string") {
82
+ return part.result;
83
+ }
84
+ return result.output;
85
+ }
75
86
  function createHostToolPartTranslator(dialect = "v1") {
76
87
  if (dialect !== "v2") return (part) => part;
77
88
  const dropped = /* @__PURE__ */ new Set();
@@ -118,7 +129,8 @@ function createHostToolPartTranslator(dialect = "v1") {
118
129
  case "tool-result": {
119
130
  const tool = classify2(String(part.toolCallId), part.toolName);
120
131
  if (tool === null) return null;
121
- return tool ? { ...part, toolName: tool.name } : part;
132
+ const result = hostedToolResult(part);
133
+ return tool ? { ...part, toolName: tool.name, result } : { ...part, result };
122
134
  }
123
135
  default:
124
136
  return part;
@@ -4259,6 +4271,11 @@ function findForkParent(opts) {
4259
4271
  }
4260
4272
  return best;
4261
4273
  }
4274
+ function continuesRecordedConversation(recorded, prompt) {
4275
+ const { history, forkable } = splitForkHistory(prompt);
4276
+ if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
4277
+ return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
4278
+ }
4262
4279
  function isPrefix(recorded, history) {
4263
4280
  for (let i = 0; i < recorded.length; i++) {
4264
4281
  if (recorded[i].role !== history[i].role) return false;
@@ -4273,6 +4290,109 @@ function tailIsReplyOnly(history, from) {
4273
4290
  return true;
4274
4291
  }
4275
4292
 
4293
+ // src/session-resume-store.ts
4294
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
4295
+ import { homedir as homedir4 } from "os";
4296
+ import { join as join5 } from "path";
4297
+ var MAX_RESUME_RECORDS = 256;
4298
+ var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
4299
+ var records = null;
4300
+ var pathOverride = null;
4301
+ function resumeStorePath() {
4302
+ if (pathOverride) return pathOverride;
4303
+ const stateRoot = process.env.XDG_STATE_HOME || join5(homedir4(), ".local", "state");
4304
+ return join5(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
4305
+ }
4306
+ function isDigest(value) {
4307
+ const entry = value;
4308
+ return !!entry && (entry.role === "user" || entry.role === "assistant" || entry.role === "tool") && typeof entry.digest === "string";
4309
+ }
4310
+ function isRecord2(value) {
4311
+ const record = value;
4312
+ return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest);
4313
+ }
4314
+ function load() {
4315
+ if (records) return records;
4316
+ records = /* @__PURE__ */ new Map();
4317
+ try {
4318
+ const parsed = JSON.parse(readFileSync2(resumeStorePath(), "utf8"));
4319
+ const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
4320
+ if (entries && typeof entries === "object") {
4321
+ const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
4322
+ const valid = Object.entries(entries).filter((entry) => isRecord2(entry[1])).filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
4323
+ for (const [key, record] of valid) records.set(key, record);
4324
+ }
4325
+ } catch {
4326
+ }
4327
+ return records;
4328
+ }
4329
+ function save(update) {
4330
+ const file = resumeStorePath();
4331
+ const merged = /* @__PURE__ */ new Map();
4332
+ try {
4333
+ const onDisk = JSON.parse(readFileSync2(file, "utf8"))?.sessions;
4334
+ if (onDisk && typeof onDisk === "object") {
4335
+ for (const [key, record] of Object.entries(onDisk)) {
4336
+ if (isRecord2(record)) merged.set(key, record);
4337
+ }
4338
+ }
4339
+ } catch {
4340
+ }
4341
+ update(merged);
4342
+ const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
4343
+ const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
4344
+ records = new Map(kept);
4345
+ try {
4346
+ mkdirSync3(join5(file, ".."), { recursive: true, mode: 448 });
4347
+ const tmp = `${file}.${process.pid}.tmp`;
4348
+ writeFileSync3(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
4349
+ mode: 384
4350
+ });
4351
+ renameSync2(tmp, file);
4352
+ } catch (err) {
4353
+ log.debug("could not write the claude session resume store", {
4354
+ error: err instanceof Error ? err.message : String(err)
4355
+ });
4356
+ }
4357
+ }
4358
+ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
4359
+ const chain = conversationDigests(prompt);
4360
+ if (chain.length === 0) return;
4361
+ const current = load().get(sessionKey2);
4362
+ if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
4363
+ return;
4364
+ }
4365
+ save((merged) => {
4366
+ merged.delete(sessionKey2);
4367
+ merged.set(sessionKey2, { claudeSessionId, cliPath, chain, updatedAt: Date.now() });
4368
+ });
4369
+ }
4370
+ function forgetResumePoint(sessionKey2) {
4371
+ if (!load().has(sessionKey2)) return;
4372
+ save((merged) => {
4373
+ merged.delete(sessionKey2);
4374
+ });
4375
+ }
4376
+ function findResumePoint(opts) {
4377
+ const record = load().get(opts.sessionKey);
4378
+ if (!record) return void 0;
4379
+ const refuse = (reason) => {
4380
+ log.info("not resuming the claude session from before the restart", {
4381
+ sessionKey: opts.sessionKey,
4382
+ reason
4383
+ });
4384
+ return void 0;
4385
+ };
4386
+ if (record.cliPath !== opts.cliPath) return refuse("another claude binary");
4387
+ if (!continuesRecordedConversation(record.chain, opts.prompt)) {
4388
+ return refuse("the conversation changed");
4389
+ }
4390
+ if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
4391
+ return refuse("the transcript is gone");
4392
+ }
4393
+ return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
4394
+ }
4395
+
4276
4396
  // src/permission-presets.ts
4277
4397
  var READ_ONLY_DISALLOWED_CLI_TOOLS = [
4278
4398
  "Bash",
@@ -4385,7 +4505,7 @@ import { randomUUID as randomUUID2 } from "crypto";
4385
4505
  var MAX_HISTORY_EXCHANGES = 20;
4386
4506
  var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
4387
4507
  var pendingProcesses = /* @__PURE__ */ new WeakSet();
4388
- function isRecord2(value) {
4508
+ function isRecord3(value) {
4389
4509
  return value !== null && typeof value === "object" && !Array.isArray(value);
4390
4510
  }
4391
4511
  var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
@@ -4396,7 +4516,7 @@ function parseSideQuestionContent(content) {
4396
4516
  } else if (Array.isArray(content)) {
4397
4517
  const parts = [];
4398
4518
  for (const part of content) {
4399
- if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
4519
+ if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
4400
4520
  parts.push(part.text);
4401
4521
  }
4402
4522
  text = parts.join("\n");
@@ -4415,7 +4535,7 @@ function assistantText(content) {
4415
4535
  if (!Array.isArray(content)) return "";
4416
4536
  const parts = [];
4417
4537
  for (const part of content) {
4418
- if (isRecord2(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
4538
+ if (isRecord3(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
4419
4539
  }
4420
4540
  return parts.join("\n").trim();
4421
4541
  }
@@ -4445,9 +4565,9 @@ function dispatchSideQuestionResponse(activeProcess, line) {
4445
4565
  } catch {
4446
4566
  return false;
4447
4567
  }
4448
- if (!isRecord2(message) || message.type !== "control_response") return false;
4568
+ if (!isRecord3(message) || message.type !== "control_response") return false;
4449
4569
  const response = message.response;
4450
- if (!isRecord2(response) || typeof response.request_id !== "string") return false;
4570
+ if (!isRecord3(response) || typeof response.request_id !== "string") return false;
4451
4571
  return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
4452
4572
  }
4453
4573
  async function requestSideQuestion(activeProcess, question, options) {
@@ -4532,7 +4652,7 @@ async function requestSideQuestion(activeProcess, question, options) {
4532
4652
  return;
4533
4653
  }
4534
4654
  const result = response.response;
4535
- if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
4655
+ if (response.subtype !== "success" || !isRecord3(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
4536
4656
  fail(new Error("Claude Code returned an invalid /btw response."));
4537
4657
  return;
4538
4658
  }
@@ -4702,6 +4822,7 @@ function noteTurnLine(ap, line) {
4702
4822
  if (isTerminalResultLine(line)) settleTurn(ap);
4703
4823
  }
4704
4824
  function isTurnInFlight(ap) {
4825
+ if (ap.interactiveControl) return ap.interactiveControl.turnRunning();
4705
4826
  return ap.turnInFlight === true;
4706
4827
  }
4707
4828
  function awaitTurnIdle(ap, timeoutMs) {
@@ -4721,6 +4842,7 @@ function awaitTurnIdle(ap, timeoutMs) {
4721
4842
  });
4722
4843
  }
4723
4844
  function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
4845
+ if (ap.interactiveControl) return ap.interactiveControl.interrupt(timeoutMs);
4724
4846
  if (!ap.turnInFlight) return Promise.resolve(true);
4725
4847
  const stdin = ap.proc.stdin;
4726
4848
  if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
@@ -4761,6 +4883,18 @@ function setActiveProcess(key, ap) {
4761
4883
  cancelIdleProcessEviction(key);
4762
4884
  activeProcesses.set(key, ap);
4763
4885
  }
4886
+ function noteInteractiveProcessExit(sessionKey2, ap, code) {
4887
+ if (activeProcesses.get(sessionKey2) !== ap) return;
4888
+ cancelIdleProcessEviction(sessionKey2);
4889
+ activeProcesses.delete(sessionKey2);
4890
+ if (getPendingProxyCalls(sessionKey2).length > 0) {
4891
+ rejectAllPendingProxyCallsForSession(sessionKey2, new Error(CHILD_EXITED_MESSAGE));
4892
+ }
4893
+ if (code !== 0 && code !== null) {
4894
+ claudeSessions.delete(sessionKey2);
4895
+ forgetResumePoint(sessionKey2);
4896
+ }
4897
+ }
4764
4898
  function scheduleIdleProcessEviction(key, timeoutMs) {
4765
4899
  cancelIdleProcessEviction(key);
4766
4900
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
@@ -4906,6 +5040,7 @@ function deleteClaudeSessionId(key) {
4906
5040
  if (claudeSessionId) clearLedger(claudeSessionId);
4907
5041
  claudeSessions.delete(key);
4908
5042
  forgetForkFingerprint(key);
5043
+ forgetResumePoint(key);
4909
5044
  }
4910
5045
  function effortSessionKey(baseKey, effort) {
4911
5046
  return effort ? `${baseKey}::effort=${effort}` : baseKey;
@@ -5018,6 +5153,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5018
5153
  sessionKey: sessionKey2
5019
5154
  });
5020
5155
  claudeSessions.delete(sessionKey2);
5156
+ forgetResumePoint(sessionKey2);
5021
5157
  }
5022
5158
  });
5023
5159
  proc.stderr?.on("data", (data) => {
@@ -5032,6 +5168,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5032
5168
  error: stderr.slice(0, 200)
5033
5169
  });
5034
5170
  claudeSessions.delete(sessionKey2);
5171
+ forgetResumePoint(sessionKey2);
5035
5172
  } else {
5036
5173
  log.debug("ignoring session ID error from stale claude process", {
5037
5174
  sessionKey: sessionKey2
@@ -5510,7 +5647,7 @@ async function handleBtwCommand(client, input, options = {}) {
5510
5647
  // src/diagnostic-bundle.ts
5511
5648
  import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
5512
5649
  import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
5513
- import { homedir as homedir4 } from "os";
5650
+ import { homedir as homedir5 } from "os";
5514
5651
 
5515
5652
  // src/log-messages.ts
5516
5653
  var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
@@ -5586,6 +5723,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5586
5723
  "ignoring unknown proxyTools entries",
5587
5724
  "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
5588
5725
  "interactive system prompt disabled; opencode agent prompts will not be appended",
5726
+ "interactive transport cancelled the usage-limit auto-continue",
5727
+ "interactive transport cannot continue",
5728
+ "interactive transport denied a permission prompt",
5589
5729
  "interactive transport dropped non-text content blocks",
5590
5730
  "interactive turn failed",
5591
5731
  "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
@@ -5617,6 +5757,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5617
5757
  "remote URL images are not supported by Claude CLI, skipping",
5618
5758
  "replaying stdout the child emitted between turns",
5619
5759
  "result fallback timer fired \u2014 closing stream without result event",
5760
+ "resuming the claude session from before the restart instead of replaying it",
5620
5761
  "shared Claude capability already exists; leaving untouched",
5621
5762
  "skipping MCP server with unknown type",
5622
5763
  "skipping local MCP server with no command",
@@ -5840,7 +5981,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
5840
5981
  return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
5841
5982
  }
5842
5983
  function createRedactionContext(options = {}) {
5843
- const home = options.home ?? homedir4();
5984
+ const home = options.home ?? homedir5();
5844
5985
  const homes = /* @__PURE__ */ new Set();
5845
5986
  if (home) homes.add(home);
5846
5987
  if (home) {
@@ -6514,7 +6655,7 @@ function staleBuildWatch() {
6514
6655
  var DOCTOR_COMMAND = "claude-code-doctor";
6515
6656
  var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows, or `bundle` for a redacted log you can paste into an issue";
6516
6657
  var DOCTOR_MARKER = "\u258C **claude-code doctor**";
6517
- function isRecord3(value) {
6658
+ function isRecord4(value) {
6518
6659
  return value !== null && typeof value === "object" && !Array.isArray(value);
6519
6660
  }
6520
6661
  var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
@@ -6525,7 +6666,7 @@ function parseDoctorCommandContent(content) {
6525
6666
  } else if (Array.isArray(content)) {
6526
6667
  const parts = [];
6527
6668
  for (const part of content) {
6528
- if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
6669
+ if (!isRecord4(part) || part.type !== "text" || typeof part.text !== "string") return null;
6529
6670
  parts.push(part.text);
6530
6671
  }
6531
6672
  text = parts.join("\n");
@@ -7266,8 +7407,8 @@ var registry = {};
7266
7407
  var defaultSubagentModel;
7267
7408
  var defaultSubagentCacheTtl;
7268
7409
  var providerFallbackModels = [];
7269
- function setAgentRegistry(records) {
7270
- registry = records;
7410
+ function setAgentRegistry(records2) {
7411
+ registry = records2;
7271
7412
  }
7272
7413
  function getAgentRegistry() {
7273
7414
  return registry;
@@ -7407,7 +7548,7 @@ function parseFallbackModelList(value) {
7407
7548
  return value.trim().replace(/^\[|\]$/g, "").split(/[,\s]+/).map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
7408
7549
  }
7409
7550
  async function readAgentMarkdownRecords(directories) {
7410
- const records = {};
7551
+ const records2 = {};
7411
7552
  for (const directory of directories) {
7412
7553
  let entries;
7413
7554
  try {
@@ -7418,10 +7559,10 @@ async function readAgentMarkdownRecords(directories) {
7418
7559
  for (const entry of entries) {
7419
7560
  if (!entry.endsWith(".md")) continue;
7420
7561
  const name = entry.slice(0, -3);
7421
- if (records[name]) continue;
7562
+ if (records2[name]) continue;
7422
7563
  try {
7423
7564
  const text = await readFile(path7.join(directory, entry), "utf8");
7424
- records[name] = parseAgentFrontmatter(text);
7565
+ records2[name] = parseAgentFrontmatter(text);
7425
7566
  } catch (err) {
7426
7567
  log.debug("failed to read agent markdown", {
7427
7568
  file: path7.join(directory, entry),
@@ -7430,7 +7571,7 @@ async function readAgentMarkdownRecords(directories) {
7430
7571
  }
7431
7572
  }
7432
7573
  }
7433
- return records;
7574
+ return records2;
7434
7575
  }
7435
7576
  function agentDirectories(home, projectDirectory) {
7436
7577
  const directories = [];
@@ -8369,6 +8510,7 @@ async function resolveSkillPluginDirs(opts) {
8369
8510
 
8370
8511
  // src/claude-session-wrapper.ts
8371
8512
  import { EventEmitter as EventEmitter4 } from "events";
8513
+ import { existsSync as existsSync4 } from "fs";
8372
8514
  import { unlink as unlink2 } from "fs/promises";
8373
8515
 
8374
8516
  // src/claude-session-bun.ts
@@ -8402,10 +8544,99 @@ function encodeCwd(cwd) {
8402
8544
  }
8403
8545
  return real.replace(/[^a-zA-Z0-9]/g, "-");
8404
8546
  }
8547
+ var bunPtySpawner = (argv, opts) => {
8548
+ const [command, ...args] = argv;
8549
+ return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
8550
+ cwd: opts.cwd,
8551
+ env: opts.env,
8552
+ terminal: {
8553
+ cols: opts.cols,
8554
+ rows: opts.rows,
8555
+ data: (_term, data) => opts.onData(Buffer.from(data).toString("utf8"))
8556
+ }
8557
+ });
8558
+ };
8559
+ function words(text) {
8560
+ return text.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
8561
+ }
8562
+ var SCREEN_PATTERNS = [
8563
+ {
8564
+ kind: "login",
8565
+ pattern: new RegExp(
8566
+ [
8567
+ words("Select login method"),
8568
+ words("Please run /login"),
8569
+ words("Invalid API key"),
8570
+ words("OAuth token has expired"),
8571
+ words("Not logged in")
8572
+ ].join("|"),
8573
+ "i"
8574
+ )
8575
+ },
8576
+ {
8577
+ kind: "onboarding",
8578
+ pattern: new RegExp(
8579
+ [words("Choose the text style that looks best"), words("Let's get started.")].join("|"),
8580
+ "i"
8581
+ )
8582
+ },
8583
+ {
8584
+ kind: "trust",
8585
+ pattern: new RegExp(
8586
+ [
8587
+ words("Do you trust the files in this folder?"),
8588
+ words("Is this a project you created or one you trust?"),
8589
+ words("Yes, I trust this folder")
8590
+ ].join("|"),
8591
+ "i"
8592
+ )
8593
+ },
8594
+ {
8595
+ // The question alone is not enough: Claude can write "Do you want to
8596
+ // proceed?" in a reply. The dialog always numbers its options.
8597
+ kind: "permission",
8598
+ pattern: new RegExp(
8599
+ `${words("Do you want to")}\\s*(?:proceed|make\\s*this\\s*edit|create|overwrite|allow|run)[^?]{0,160}\\?[\\s\\S]{0,600}?1\\.\\s*Yes`,
8600
+ "i"
8601
+ )
8602
+ },
8603
+ {
8604
+ kind: "auto-continue",
8605
+ pattern: new RegExp(
8606
+ `${words("continuing automatically at")}[^\xB7\\n]{1,40}\xB7\\s*${words("esc to cancel")}`,
8607
+ "i"
8608
+ )
8609
+ }
8610
+ ];
8611
+ function stripTerminal(raw) {
8612
+ return raw.replace(
8613
+ /\x1B\[(\d*)C/g,
8614
+ (_match, count) => " ".repeat(Math.min(Number(count) || 1, 200))
8615
+ ).replace(/\x1B\[[0-9;?]*[ABDEFGHJKfd]/g, "\n").replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "").replace(/[ \t]+/g, " ");
8616
+ }
8617
+ function highlightedChoice(text) {
8618
+ let last = null;
8619
+ for (const match of text.matchAll(/❯\s*(?:\d+\s*\.\s*)?(Yes|No)\b/gi)) {
8620
+ last = match[1].toLowerCase();
8621
+ }
8622
+ return last === "yes" || last === "no" ? last : null;
8623
+ }
8624
+ function classifyScreen(text) {
8625
+ for (const { kind, pattern } of SCREEN_PATTERNS) {
8626
+ const match = pattern.exec(text);
8627
+ if (match) {
8628
+ return { kind, detail: match[0].replace(/\s+/g, " ").trim().slice(0, 200) };
8629
+ }
8630
+ }
8631
+ return null;
8632
+ }
8633
+ function configDirEnv(configDir) {
8634
+ return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
8635
+ }
8405
8636
  function interactiveSpawnEnv(opts) {
8406
8637
  return {
8407
8638
  ...process.env,
8408
- CLAUDE_CONFIG_DIR: opts.configDir,
8639
+ ...configDirEnv(opts.configDir),
8409
8640
  TERM: "xterm-256color",
8410
8641
  // Pin the binary so a mid-session autoupdate cannot invalidate the
8411
8642
  // detected version the flag gates read, and skip non-essential traffic.
@@ -8480,6 +8711,21 @@ var TurnUsageAccumulator = class {
8480
8711
  };
8481
8712
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
8482
8713
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
8714
+ var RAW_KEEP_CHARS = 64 * 1024;
8715
+ var SCREEN_WINDOW_CHARS = 16 * 1024;
8716
+ var INTERRUPT_MARKER = "[Request interrupted by user";
8717
+ function isInterruptRecord(rec) {
8718
+ if (!rec || rec.type !== "user" || !rec.message) return false;
8719
+ const content = rec.message.content;
8720
+ if (typeof content === "string") return content.startsWith(INTERRUPT_MARKER);
8721
+ if (!Array.isArray(content)) return false;
8722
+ return content.some(
8723
+ (block) => block?.type === "text" && typeof block.text === "string" && block.text.startsWith(INTERRUPT_MARKER)
8724
+ );
8725
+ }
8726
+ function isTurnDurationRecord(rec) {
8727
+ return rec?.type === "system" && rec.subtype === "turn_duration";
8728
+ }
8483
8729
  function resolveConfigDir(configDir) {
8484
8730
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
8485
8731
  if (!value) return path9.join(os4.homedir(), ".claude");
@@ -8489,11 +8735,21 @@ function resolveConfigDir(configDir) {
8489
8735
  }
8490
8736
  return path9.resolve(value);
8491
8737
  }
8738
+ function fatalScreenMessage(kind, configDir, cwd) {
8739
+ if (kind === "trust") {
8740
+ return `Claude Code's folder trust dialog for ${cwd} could not be answered. Run \`claude\` in that folder once by hand and trust it, then retry.`;
8741
+ }
8742
+ if (kind === "login") {
8743
+ return `Claude Code is not logged in for ${configDir}. Run \`claude\` there once by hand and log in (or \`claude auth login\`), then retry.`;
8744
+ }
8745
+ return `Claude Code has not finished its first-run setup for ${configDir}. Run \`claude\` there once by hand, then retry.`;
8746
+ }
8492
8747
  var ClaudeSession = class {
8493
8748
  sessionId;
8494
8749
  cwd;
8495
8750
  configDir;
8496
8751
  jsonlPath;
8752
+ /** The newest terminal output, ANSI included, for diagnostics. */
8497
8753
  raw = "";
8498
8754
  proc = null;
8499
8755
  cursor = 0;
@@ -8503,12 +8759,38 @@ var ClaudeSession = class {
8503
8759
  exitCode = null;
8504
8760
  aborted = false;
8505
8761
  signal;
8762
+ resumeSessionId;
8763
+ env;
8764
+ onScreen;
8765
+ onExit;
8766
+ spawnPty;
8767
+ /** The configured config dir, which alone is exported to the child. */
8768
+ explicitConfigDir;
8769
+ /** Output since the screen was last acted on, read by `checkScreen`. */
8770
+ screen = "";
8771
+ screenTimer = null;
8772
+ fatal = null;
8773
+ /** The folder trust dialog is up and not answered yet. */
8774
+ trustPending = false;
8775
+ trustMoves = 0;
8776
+ trustMovedAt = 0;
8777
+ turn = null;
8778
+ /** The running turn's read state, shared with `flushTranscript`. */
8779
+ scan = null;
8780
+ turnDenied = [];
8781
+ abandonTurn = false;
8506
8782
  o;
8507
8783
  constructor(opts = {}) {
8508
8784
  this.cwd = path9.resolve(opts.cwd ?? process.cwd());
8509
8785
  this.configDir = resolveConfigDir(opts.configDir);
8786
+ this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
8510
8787
  this.signal = opts.signal;
8511
- this.sessionId = randomUUID4();
8788
+ this.resumeSessionId = opts.resumeSessionId;
8789
+ this.env = opts.env;
8790
+ this.onScreen = opts.onScreen;
8791
+ this.onExit = opts.onExit;
8792
+ this.spawnPty = opts.spawnPty ?? bunPtySpawner;
8793
+ this.sessionId = opts.resumeSessionId ?? randomUUID4();
8512
8794
  this.jsonlPath = path9.join(
8513
8795
  this.configDir,
8514
8796
  "projects",
@@ -8538,9 +8820,47 @@ var ClaudeSession = class {
8538
8820
  submitMinMs: opts.submitMinMs ?? 200,
8539
8821
  submitConfirmMs: opts.submitConfirmMs ?? 1500,
8540
8822
  submitMaxRetries: opts.submitMaxRetries ?? 8,
8541
- debug: opts.debug ?? false
8823
+ debug: opts.debug ?? false,
8824
+ // The headless CLI's `tool_progress` heartbeat runs every 30 s, and the
8825
+ // wire-inactivity watchdog it keeps quiet fires at 60 s.
8826
+ heartbeatMs: opts.heartbeatMs ?? 3e4,
8827
+ interruptGraceMs: opts.interruptGraceMs ?? 5e3,
8828
+ // Shorter than `bootQuietMs`, so a trust dialog is answered before boot
8829
+ // could declare the TUI ready and paste a prompt into it.
8830
+ permissionQuietMs: opts.permissionQuietMs ?? 1e3,
8831
+ stopSettleMs: opts.stopSettleMs ?? 750
8542
8832
  };
8543
8833
  }
8834
+ /** The CLI arguments this session spawns with. */
8835
+ spawnArgs() {
8836
+ const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
8837
+ if (this.o.model) args.push("--model", this.o.model);
8838
+ if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
8839
+ args.push("--setting-sources", this.o.settingSources);
8840
+ }
8841
+ if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
8842
+ return args;
8843
+ }
8844
+ /** The child environment: the caller's (or the default one) plus `TERM`,
8845
+ * and `CLAUDE_CONFIG_DIR` only for a configured one (`configDirEnv`). */
8846
+ spawnEnv() {
8847
+ if (!this.env) {
8848
+ return interactiveSpawnEnv({
8849
+ configDir: this.explicitConfigDir,
8850
+ ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
8851
+ effort: this.o.effort
8852
+ });
8853
+ }
8854
+ return { ...this.env, ...configDirEnv(this.explicitConfigDir), TERM: "xterm-256color" };
8855
+ }
8856
+ /** True while a turn is between its paste and its end. */
8857
+ get turnRunning() {
8858
+ return this.turn !== null;
8859
+ }
8860
+ /** True once the child is gone (exited, killed or never started). */
8861
+ get hasExited() {
8862
+ return this.exited;
8863
+ }
8544
8864
  async start() {
8545
8865
  if (this.signal?.aborted) throw new Error("aborted before start");
8546
8866
  this.signal?.addEventListener(
@@ -8551,61 +8871,147 @@ var ClaudeSession = class {
8551
8871
  },
8552
8872
  { once: true }
8553
8873
  );
8554
- const claude = resolveClaude(this.o.cliPath ?? "claude");
8555
- const args = ["--session-id", this.sessionId];
8556
- if (this.o.model) args.push("--model", this.o.model);
8557
- if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
8558
- args.push("--setting-sources", this.o.settingSources);
8559
- }
8560
- if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
8874
+ const claude = this.o.cliPath ?? "claude";
8875
+ const args = this.spawnArgs();
8561
8876
  if (this.o.debug)
8562
8877
  process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
8563
8878
  `);
8564
8879
  this.lastDataAt = Date.now();
8565
- this.proc = Bun.spawn([claude, ...args], {
8880
+ const proc = this.spawnPty([claude, ...args], {
8566
8881
  cwd: this.cwd,
8567
- env: interactiveSpawnEnv({
8568
- // The resolved field, not `this.o.configDir`: same value (the
8569
- // constructor copies it in) but typed as always present.
8570
- configDir: this.configDir,
8571
- ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
8572
- effort: this.o.effort
8573
- }),
8574
- terminal: {
8575
- cols: this.o.cols,
8576
- rows: this.o.rows,
8577
- data: (_term, d) => {
8578
- this.lastDataAt = Date.now();
8579
- const chunk = Buffer.from(d).toString("utf8");
8580
- this.raw += chunk;
8581
- if (this.o.debug) process.stdout.write(chunk);
8582
- }
8583
- }
8882
+ env: this.spawnEnv(),
8883
+ cols: this.o.cols,
8884
+ rows: this.o.rows,
8885
+ onData: (chunk) => this.onData(chunk)
8584
8886
  });
8585
- this.proc.exited.then((code) => {
8586
- this.exitCode = typeof code === "number" ? code : null;
8587
- this.exited = true;
8588
- this.proc = null;
8589
- }).catch(() => {
8887
+ this.proc = proc;
8888
+ const markExited = (code) => {
8889
+ if (this.exited) return;
8890
+ this.exitCode = code;
8590
8891
  this.exited = true;
8591
8892
  this.proc = null;
8592
- });
8893
+ this.clearScreenTimer();
8894
+ this.onExit?.(code);
8895
+ };
8896
+ proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
8593
8897
  await this.waitForBoot();
8594
8898
  this.cursor = this.lineCount();
8595
8899
  }
8900
+ onData(chunk) {
8901
+ this.lastDataAt = Date.now();
8902
+ this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
8903
+ this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
8904
+ if (this.o.debug) process.stdout.write(chunk);
8905
+ this.clearScreenTimer();
8906
+ this.screenTimer = setTimeout(() => {
8907
+ this.screenTimer = null;
8908
+ this.checkScreen();
8909
+ }, this.o.permissionQuietMs);
8910
+ }
8911
+ clearScreenTimer() {
8912
+ if (this.screenTimer) {
8913
+ clearTimeout(this.screenTimer);
8914
+ this.screenTimer = null;
8915
+ }
8916
+ }
8917
+ /**
8918
+ * Answer the screen the TUI is blocked on, if it is one of the few this
8919
+ * session recognises. Returns what it did, or null. Each screen is acted on
8920
+ * once: the window is cleared afterwards, so the next check only reads what
8921
+ * the TUI drew since.
8922
+ */
8923
+ checkScreen() {
8924
+ if (!this.proc || this.exited) return null;
8925
+ const text = stripTerminal(this.screen);
8926
+ const state = this.trustPending ? { kind: "trust", detail: "folder trust dialog" } : classifyScreen(text);
8927
+ if (!state) return null;
8928
+ let action;
8929
+ switch (state.kind) {
8930
+ case "trust": {
8931
+ this.trustPending = true;
8932
+ const choice = highlightedChoice(text);
8933
+ if (choice === "yes") {
8934
+ this.write("\r");
8935
+ this.trustPending = false;
8936
+ action = "accepted";
8937
+ break;
8938
+ }
8939
+ if (choice === null) return null;
8940
+ if (this.trustMovedAt && this.lastDataAt <= this.trustMovedAt) return null;
8941
+ if (this.trustMoves >= 4) {
8942
+ action = "fatal";
8943
+ break;
8944
+ }
8945
+ this.trustMoves++;
8946
+ this.trustMovedAt = Date.now();
8947
+ this.write("\x1B[B");
8948
+ return null;
8949
+ }
8950
+ case "permission":
8951
+ if (!this.turn) return null;
8952
+ this.write("\x1B");
8953
+ this.turnDenied.push(state);
8954
+ action = "denied";
8955
+ break;
8956
+ case "auto-continue":
8957
+ this.write("\x1B");
8958
+ action = "cancelled";
8959
+ break;
8960
+ case "login":
8961
+ case "onboarding":
8962
+ action = "fatal";
8963
+ break;
8964
+ }
8965
+ this.screen = "";
8966
+ const event = { ...state, action };
8967
+ if (action === "fatal") {
8968
+ this.fatal = event;
8969
+ this.killProcess();
8970
+ }
8971
+ try {
8972
+ this.onScreen?.(event);
8973
+ } catch {
8974
+ }
8975
+ return event;
8976
+ }
8977
+ write(data) {
8978
+ try {
8979
+ this.proc?.terminal.write(data);
8980
+ } catch {
8981
+ }
8982
+ }
8983
+ fatalError() {
8984
+ if (!this.fatal) return null;
8985
+ return new Error(
8986
+ this.failureMessage(fatalScreenMessage(this.fatal.kind, this.configDir, this.cwd), true)
8987
+ );
8988
+ }
8596
8989
  /** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
8597
- * bootMinMs..bootMaxMs. */
8990
+ * bootMinMs..bootMaxMs. A screen still waiting to be answered when the TUI
8991
+ * goes quiet is answered first, and the wait goes on. */
8598
8992
  async waitForBoot() {
8599
8993
  const start = Date.now();
8600
8994
  while (Date.now() - start < this.o.bootMaxMs) {
8601
8995
  await delay(150);
8602
8996
  if (this.aborted) throw new Error("aborted during boot");
8997
+ const fatal = this.fatalError();
8998
+ if (fatal) throw fatal;
8603
8999
  if (this.exited) {
8604
9000
  throw new Error(this.failureMessage("claude exited during boot", true));
8605
9001
  }
8606
9002
  const elapsed = Date.now() - start;
8607
9003
  const sinceData = Date.now() - this.lastDataAt;
8608
- if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) return;
9004
+ if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) {
9005
+ const acted = this.checkScreen();
9006
+ if (!acted && !this.trustPending) return;
9007
+ const fatalNow = this.fatalError();
9008
+ if (fatalNow) throw fatalNow;
9009
+ }
9010
+ }
9011
+ if (this.trustPending) {
9012
+ this.fatal = { kind: "trust", detail: "folder trust dialog", action: "fatal" };
9013
+ this.killProcess();
9014
+ throw this.fatalError();
8609
9015
  }
8610
9016
  }
8611
9017
  /** Submit the freshly-injected prompt and confirm the turn was actually
@@ -8643,7 +9049,7 @@ var ClaudeSession = class {
8643
9049
  return lines.length > 0 ? lines.length - 1 : 0;
8644
9050
  }
8645
9051
  rawTail(max = 600) {
8646
- const clean = this.raw.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\s+/g, " ").trim();
9052
+ const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
8647
9053
  return clean.length > max ? clean.slice(-max) : clean;
8648
9054
  }
8649
9055
  failureMessage(reason, includeRaw = false) {
@@ -8657,61 +9063,169 @@ var ClaudeSession = class {
8657
9063
  return parts.join("; ");
8658
9064
  }
8659
9065
  /**
8660
- * Inject a turn into the live session and return the assistant reply once a
8661
- * terminal stop_reason is observed in the transcript.
9066
+ * Paste a turn into the live session and follow the transcript until the
9067
+ * turn ends, handing every new record to `onRecord` (raw line and parsed
9068
+ * record, the latter null when the line is not JSON). One turn at a time:
9069
+ * a second call while one runs is refused, because both would read the
9070
+ * same cursor.
8662
9071
  */
8663
- async ask(prompt, perTurnTimeoutMs) {
9072
+ async runTurnLoop(prompt, onRecord, timeout, onHeartbeat) {
8664
9073
  if (this.aborted) throw new Error("aborted");
9074
+ const fatal = this.fatalError();
9075
+ if (fatal) throw fatal;
8665
9076
  if (!this.proc || this.exited)
8666
9077
  throw new Error("session not started or already exited");
8667
- const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
8668
- const t0 = Date.now();
8669
- if (this.o.bracketedPaste) {
8670
- this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
8671
- } else {
8672
- this.proc.terminal.write(prompt);
8673
- }
8674
- await this.submitTurn();
8675
- const collected2 = [];
8676
- const usage = new TurnUsageAccumulator();
8677
- let stopReason = null;
8678
- const deadline = Date.now() + timeout;
8679
- while (Date.now() < deadline) {
8680
- await delay(this.o.pollMs);
8681
- if (this.aborted) throw new Error("aborted mid-turn");
8682
- const lines = this.readRawLines();
8683
- const lastComplete = lines.length - 1;
8684
- if (lastComplete <= this.cursor) {
8685
- if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
8686
- continue;
9078
+ if (this.turn) throw new Error("a turn is already running in this session");
9079
+ let settle;
9080
+ this.turn = new Promise((resolve6) => settle = resolve6);
9081
+ this.turnDenied = [];
9082
+ this.abandonTurn = false;
9083
+ this.cursor = Math.max(this.cursor, this.lineCount());
9084
+ this.screen = "";
9085
+ try {
9086
+ if (this.o.bracketedPaste) {
9087
+ this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
9088
+ } else {
9089
+ this.proc.terminal.write(prompt);
8687
9090
  }
8688
- for (let i = this.cursor; i < lastComplete; i++) {
8689
- const s = lines[i];
8690
- if (!s || !s.trim()) continue;
8691
- let rec;
8692
- try {
8693
- rec = JSON.parse(s);
8694
- } catch {
9091
+ await this.submitTurn();
9092
+ const scan = {
9093
+ onRecord,
9094
+ stopReason: null,
9095
+ stopSeenAt: 0,
9096
+ end: null,
9097
+ sawUserRecord: false,
9098
+ lastBeat: Date.now()
9099
+ };
9100
+ this.scan = scan;
9101
+ const deadline = Date.now() + timeout;
9102
+ while (Date.now() < deadline) {
9103
+ await delay(this.o.pollMs);
9104
+ if (this.aborted) throw new Error("aborted mid-turn");
9105
+ const fatalNow = this.fatalError();
9106
+ if (fatalNow) throw fatalNow;
9107
+ if (this.scanTranscript() || scan.end) {
9108
+ if (scan.end) {
9109
+ return {
9110
+ stopReason: scan.end === "stop" ? scan.stopReason : null,
9111
+ end: scan.end,
9112
+ denied: this.turnDenied
9113
+ };
9114
+ }
8695
9115
  continue;
8696
9116
  }
9117
+ if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
9118
+ return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
9119
+ }
9120
+ if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9121
+ if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9122
+ const now = Date.now();
9123
+ if (onHeartbeat && now - scan.lastBeat >= this.o.heartbeatMs) {
9124
+ scan.lastBeat = now;
9125
+ if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
9126
+ }
9127
+ }
9128
+ return { stopReason: null, end: null, denied: this.turnDenied };
9129
+ } finally {
9130
+ this.turn = null;
9131
+ this.scan = null;
9132
+ this.abandonTurn = false;
9133
+ settle();
9134
+ }
9135
+ }
9136
+ /**
9137
+ * Read every complete transcript record past the cursor into the running
9138
+ * turn. Returns whether anything new was read. The one place a record is
9139
+ * interpreted, shared by the poll loop and `flushTranscript`.
9140
+ */
9141
+ scanTranscript() {
9142
+ const scan = this.scan;
9143
+ if (!scan || scan.end) return false;
9144
+ const lines = this.readRawLines();
9145
+ const lastComplete = lines.length - 1;
9146
+ if (lastComplete <= this.cursor) return false;
9147
+ for (let i = this.cursor; i < lastComplete && !scan.end; i++) {
9148
+ const s = lines[i];
9149
+ if (!s || !s.trim()) {
9150
+ this.cursor = i + 1;
9151
+ continue;
9152
+ }
9153
+ let rec = null;
9154
+ try {
9155
+ rec = JSON.parse(s);
9156
+ } catch {
9157
+ }
9158
+ scan.onRecord(s, rec);
9159
+ if (rec) {
8697
9160
  if (rec.type === "assistant" && rec.message) {
8698
- for (const b of rec.message.content ?? []) {
8699
- if (b?.type === "text" && typeof b.text === "string")
8700
- collected2.push(b.text);
8701
- }
8702
- usage.add(rec);
8703
- if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
8704
- stopReason = rec.message.stop_reason;
9161
+ const reason = rec.message.stop_reason;
9162
+ if (typeof reason === "string") {
9163
+ scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
8705
9164
  }
9165
+ } else if (rec.type === "user") {
9166
+ if (scan.sawUserRecord && isInterruptRecord(rec)) scan.end = "interrupted";
9167
+ scan.sawUserRecord = true;
9168
+ } else if ((scan.sawUserRecord || scan.stopReason) && isTurnDurationRecord(rec)) {
9169
+ scan.end = scan.stopReason ? "stop" : "ended";
8706
9170
  }
8707
9171
  }
8708
- this.cursor = lastComplete;
8709
- if (stopReason) break;
9172
+ this.cursor = i + 1;
8710
9173
  }
9174
+ if (!scan.end) this.cursor = lastComplete;
9175
+ scan.lastBeat = Date.now();
9176
+ scan.stopSeenAt = Date.now();
9177
+ return true;
9178
+ }
9179
+ /**
9180
+ * Hand the running turn every record the TUI has already written, now
9181
+ * rather than at the next poll. The TUI writes a reply's records before it
9182
+ * runs the reply's tools, but a proxied tool's MCP call can reach the
9183
+ * plugin inside one poll interval, and the step it ends must not close
9184
+ * before the text that preceded the call was read. No-op between turns.
9185
+ */
9186
+ flushTranscript() {
9187
+ this.scanTranscript();
9188
+ }
9189
+ /**
9190
+ * Stop the running turn the way the TUI's own Esc does, and keep the
9191
+ * session alive for the next one. Resolves true once the turn ended, false
9192
+ * when it was abandoned after `interruptGraceMs` without the TUI saying so.
9193
+ */
9194
+ async interrupt(graceMs = this.o.interruptGraceMs) {
9195
+ const turn = this.turn;
9196
+ if (!turn) return true;
9197
+ this.write("\x1B");
9198
+ const ended = await Promise.race([turn.then(() => true), delay(graceMs).then(() => false)]);
9199
+ if (ended) return true;
9200
+ this.write("\x1B");
9201
+ this.abandonTurn = true;
9202
+ await Promise.race([turn, delay(this.o.pollMs * 4)]);
9203
+ return false;
9204
+ }
9205
+ /**
9206
+ * Inject a turn into the live session and return the assistant reply once a
9207
+ * terminal stop_reason is observed in the transcript.
9208
+ */
9209
+ async ask(prompt, perTurnTimeoutMs) {
9210
+ const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
9211
+ const t0 = Date.now();
9212
+ const collected2 = [];
9213
+ const usage = new TurnUsageAccumulator();
9214
+ const { stopReason } = await this.runTurnLoop(
9215
+ prompt,
9216
+ (_raw, rec) => {
9217
+ if (rec?.type !== "assistant" || !rec.message) return;
9218
+ for (const b of rec.message.content ?? []) {
9219
+ if (b?.type === "text" && typeof b.text === "string") collected2.push(b.text);
9220
+ }
9221
+ usage.add(rec);
9222
+ },
9223
+ timeout
9224
+ );
8711
9225
  if (!stopReason) {
8712
9226
  throw new Error(
8713
9227
  this.failureMessage(
8714
- `turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected2.length} text block(s))`
9228
+ `turn ended without a terminal assistant record after ${Date.now() - t0}ms (collected ${collected2.length} text block(s))`
8715
9229
  )
8716
9230
  );
8717
9231
  }
@@ -8733,9 +9247,9 @@ var ClaudeSession = class {
8733
9247
  }
8734
9248
  /**
8735
9249
  * Like ask(), but instead of collecting the reply text it re-emits each NEW
8736
- * raw JSONL transcript line via onLine (verbatim) until a terminal
8737
- * stop_reason. Used by the opencode plugin transport shim, which feeds
8738
- * these raw lines into the existing stream-json line handler unchanged.
9250
+ * raw JSONL transcript line via onLine (verbatim) until the turn ends. Used
9251
+ * by the opencode plugin transport shim, which feeds these raw lines into
9252
+ * the existing stream-json line handler unchanged.
8739
9253
  *
8740
9254
  * `usage` is the turn summed over DISTINCT API calls, which is exactly what
8741
9255
  * a headless `result` frame reports, so the shim can synthesize one and
@@ -8743,53 +9257,24 @@ var ClaudeSession = class {
8743
9257
  * `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
8744
9258
  * the newest real call, returned because a caller with no stream parser
8745
9259
  * (`askOnce`) has no other way to get the context side.
9260
+ *
9261
+ * A turn that ends any way other than a terminal stop (an interrupt, a
9262
+ * denied permission dialog, `turn_duration`) resolves with that `end` and a
9263
+ * null `stopReason`; only a timeout throws.
8746
9264
  */
8747
- async tailTurn(prompt, onLine, perTurnTimeoutMs) {
8748
- if (this.aborted) throw new Error("aborted");
8749
- if (!this.proc || this.exited)
8750
- throw new Error("session not started or already exited");
9265
+ async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
8751
9266
  const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
8752
- if (this.o.bracketedPaste) {
8753
- this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
8754
- } else {
8755
- this.proc.terminal.write(prompt);
8756
- }
8757
- await this.submitTurn();
8758
9267
  const usage = new TurnUsageAccumulator();
8759
- let stopReason = null;
8760
- const deadline = Date.now() + timeout;
8761
- while (Date.now() < deadline) {
8762
- await delay(this.o.pollMs);
8763
- if (this.aborted) throw new Error("aborted mid-turn");
8764
- const lines = this.readRawLines();
8765
- const lastComplete = lines.length - 1;
8766
- if (lastComplete <= this.cursor) {
8767
- if (this.exited) {
8768
- throw new Error(this.failureMessage("claude exited mid-turn", true));
8769
- }
8770
- continue;
8771
- }
8772
- for (let i = this.cursor; i < lastComplete; i++) {
8773
- const s = lines[i];
8774
- if (!s || !s.trim()) continue;
8775
- onLine(s);
8776
- let rec;
8777
- try {
8778
- rec = JSON.parse(s);
8779
- } catch {
8780
- continue;
8781
- }
8782
- usage.add(rec);
8783
- if (rec.type === "assistant" && rec.message) {
8784
- if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
8785
- stopReason = rec.message.stop_reason;
8786
- }
8787
- }
8788
- }
8789
- this.cursor = lastComplete;
8790
- if (stopReason) break;
8791
- }
8792
- if (!stopReason) {
9268
+ const { stopReason, end, denied } = await this.runTurnLoop(
9269
+ prompt,
9270
+ (raw, rec) => {
9271
+ onLine(raw);
9272
+ if (rec) usage.add(rec);
9273
+ },
9274
+ timeout,
9275
+ onHeartbeat
9276
+ );
9277
+ if (!end) {
8793
9278
  throw new Error(
8794
9279
  this.failureMessage(
8795
9280
  `turn timed out after ${timeout}ms (no terminal assistant record)`
@@ -8798,26 +9283,32 @@ var ClaudeSession = class {
8798
9283
  }
8799
9284
  return {
8800
9285
  stopReason,
9286
+ end,
8801
9287
  usage: usage.turnTotal,
8802
9288
  lastCallUsage: usage.lastCall,
8803
- callCount: usage.callCount
9289
+ callCount: usage.callCount,
9290
+ denied
8804
9291
  };
8805
9292
  }
8806
- dispose() {
8807
- if (this.proc) {
8808
- try {
8809
- this.proc.terminal.write("");
8810
- } catch {
8811
- }
8812
- try {
8813
- this.proc.kill();
8814
- } catch {
8815
- }
8816
- try {
8817
- this.proc.terminal.close();
8818
- } catch {
8819
- }
9293
+ killProcess() {
9294
+ const proc = this.proc;
9295
+ if (!proc) return;
9296
+ try {
9297
+ proc.terminal.write("");
9298
+ } catch {
8820
9299
  }
9300
+ try {
9301
+ proc.kill();
9302
+ } catch {
9303
+ }
9304
+ try {
9305
+ proc.terminal.close();
9306
+ } catch {
9307
+ }
9308
+ }
9309
+ dispose() {
9310
+ this.clearScreenTimer();
9311
+ this.killProcess();
8821
9312
  this.proc = null;
8822
9313
  }
8823
9314
  };
@@ -8866,6 +9357,9 @@ function interactiveExtraArgs(opts) {
8866
9357
  "--strict-mcp-config"
8867
9358
  );
8868
9359
  }
9360
+ if (opts.disallowedTools && opts.disallowedTools.length > 0) {
9361
+ extraArgs.push("--disallowedTools", ...opts.disallowedTools);
9362
+ }
8869
9363
  for (const dir of opts.pluginDirs ?? []) {
8870
9364
  extraArgs.push("--plugin-dir", dir);
8871
9365
  }
@@ -8891,9 +9385,106 @@ function interactiveExtraArgs(opts) {
8891
9385
  }
8892
9386
  return extraArgs;
8893
9387
  }
9388
+ var OpenToolCalls = class {
9389
+ open = /* @__PURE__ */ new Map();
9390
+ add(rec, now = Date.now()) {
9391
+ const content = rec?.message?.content;
9392
+ if (!Array.isArray(content)) return;
9393
+ for (const block of content) {
9394
+ if (rec.type === "assistant" && block?.type === "tool_use" && typeof block.id === "string") {
9395
+ this.open.set(block.id, { name: String(block.name ?? "tool"), startedAt: now });
9396
+ } else if (rec.type === "user" && block?.type === "tool_result") {
9397
+ this.open.delete(block.tool_use_id);
9398
+ }
9399
+ }
9400
+ }
9401
+ newest() {
9402
+ let newest = null;
9403
+ for (const [id, call] of this.open) newest = { id, ...call };
9404
+ return newest;
9405
+ }
9406
+ clear() {
9407
+ this.open.clear();
9408
+ }
9409
+ };
9410
+ function heartbeatFrame(sessionId, openCall, now = Date.now()) {
9411
+ if (openCall) {
9412
+ return JSON.stringify({
9413
+ type: "tool_progress",
9414
+ tool_use_id: `${openCall.id}-heartbeat`,
9415
+ tool_name: openCall.name,
9416
+ parent_tool_use_id: openCall.id,
9417
+ heartbeat: true,
9418
+ elapsed_time_seconds: Math.round((now - openCall.startedAt) / 1e3),
9419
+ session_id: sessionId
9420
+ });
9421
+ }
9422
+ return JSON.stringify({
9423
+ type: "system",
9424
+ subtype: "status",
9425
+ status: "requesting",
9426
+ session_id: sessionId
9427
+ });
9428
+ }
9429
+ function interactiveResultFrame(opts) {
9430
+ const completed = opts.end === "stop" && !!opts.stopReason;
9431
+ let result;
9432
+ let terminalReason = "completed";
9433
+ if (!completed) {
9434
+ if (opts.end === "failed") {
9435
+ terminalReason = "error_during_execution";
9436
+ result = `Interactive transport failed: ${opts.error ?? "unknown error"}`;
9437
+ } else if (opts.end === "interrupted") {
9438
+ terminalReason = "aborted";
9439
+ result = "Interactive transport: the turn was interrupted.";
9440
+ } else {
9441
+ terminalReason = "error_during_execution";
9442
+ result = "Interactive transport: the turn ended without a terminal stop_reason. Output above may be incomplete.";
9443
+ }
9444
+ }
9445
+ return JSON.stringify({
9446
+ type: "result",
9447
+ subtype: completed ? "success" : "error_during_execution",
9448
+ is_error: !completed,
9449
+ stop_reason: completed ? opts.stopReason : null,
9450
+ terminal_reason: terminalReason,
9451
+ result,
9452
+ session_id: opts.sessionId,
9453
+ usage: opts.usage ?? {},
9454
+ // The TUI has no `can_use_tool` channel, so a permission dialog is the
9455
+ // only way it asks; each one the session denied is reported in the
9456
+ // headless field's place, names and ids only (h #g109).
9457
+ permission_denials: opts.denials ?? [],
9458
+ total_cost_usd: null,
9459
+ duration_ms: 0
9460
+ });
9461
+ }
9462
+ function carriesReplyText(line) {
9463
+ try {
9464
+ const rec = JSON.parse(line);
9465
+ if (rec?.type !== "assistant" || !Array.isArray(rec.message?.content)) return false;
9466
+ return rec.message.content.some(
9467
+ (block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()
9468
+ );
9469
+ } catch {
9470
+ return false;
9471
+ }
9472
+ }
8894
9473
  function spawnInteractiveProcess(opts) {
8895
9474
  const extraArgs = interactiveExtraArgs(opts);
9475
+ const openCalls = new OpenToolCalls();
9476
+ let denials = [];
9477
+ const lineEmitter = new EventEmitter4();
9478
+ const proc = new EventEmitter4();
9479
+ let ap;
9480
+ let proxyClosed = false;
9481
+ const closeProxyServer = () => {
9482
+ if (proxyClosed || !opts.proxyServer) return;
9483
+ proxyClosed = true;
9484
+ void opts.proxyServer.close();
9485
+ };
8896
9486
  const session = new ClaudeSession({
9487
+ ...opts.tuning,
8897
9488
  cwd: opts.cwd,
8898
9489
  cliPath: opts.cliPath,
8899
9490
  configDir: opts.configDir,
@@ -8903,7 +9494,29 @@ function spawnInteractiveProcess(opts) {
8903
9494
  settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
8904
9495
  extraArgs,
8905
9496
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
8906
- effort: opts.effort ? cliEffortLevel(opts.effort) : void 0
9497
+ effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
9498
+ resumeSessionId: opts.resumeSessionId,
9499
+ env: opts.env,
9500
+ spawnPty: opts.spawnPty,
9501
+ onScreen: (event) => {
9502
+ if (event.action === "denied") {
9503
+ const call = openCalls.newest();
9504
+ denials.push(call ? { tool_name: call.name, tool_use_id: call.id } : { tool_name: "unknown" });
9505
+ }
9506
+ const data = { sessionId: session.sessionId, screen: event.kind, detail: event.detail };
9507
+ if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
9508
+ else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
9509
+ else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
9510
+ else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
9511
+ },
9512
+ onExit: (code) => {
9513
+ proc.exitCode = code;
9514
+ proc.signalCode = code === null ? "SIGTERM" : null;
9515
+ closeProxyServer();
9516
+ log.info("interactive claude exited", { sessionId: session.sessionId, code });
9517
+ proc.emit("exit", proc.exitCode, proc.signalCode);
9518
+ proc.emit("close", proc.exitCode, proc.signalCode);
9519
+ }
8907
9520
  });
8908
9521
  log.info("prepared interactive claude session", {
8909
9522
  cwd: opts.cwd,
@@ -8912,80 +9525,106 @@ function spawnInteractiveProcess(opts) {
8912
9525
  model: opts.model,
8913
9526
  effort: opts.effort,
8914
9527
  sessionId: session.sessionId,
9528
+ resumed: !!opts.resumeSessionId,
8915
9529
  jsonlPath: session.jsonlPath
8916
9530
  });
8917
- const lineEmitter = new EventEmitter4();
8918
- const errorHandlers = /* @__PURE__ */ new Set();
8919
9531
  let startPromise = null;
8920
9532
  const ensureStarted = () => {
8921
9533
  if (!startPromise) startPromise = session.start();
8922
9534
  return startPromise;
8923
9535
  };
8924
- const emitResult = (opts2) => {
8925
- lineEmitter.emit(
8926
- "line",
8927
- JSON.stringify({
8928
- type: "result",
8929
- subtype: opts2.subtype,
8930
- is_error: opts2.isError,
8931
- stop_reason: opts2.stopReason,
8932
- terminal_reason: opts2.terminalReason,
8933
- result: opts2.result,
8934
- session_id: session.sessionId,
8935
- usage: opts2.usage ?? {},
8936
- total_cost_usd: null,
8937
- duration_ms: 0
8938
- })
8939
- );
8940
- };
8941
- const runTurn = (userMsg) => {
8942
- void (async () => {
8943
- try {
8944
- await ensureStarted();
8945
- const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
8946
- lineEmitter.emit("line", raw);
8947
- });
8948
- log.info("interactive turn usage", {
8949
- apiCalls: callCount,
8950
- turnOutputTokens: usage?.output_tokens,
8951
- turnInputTokens: usage?.input_tokens,
8952
- turnCacheReadTokens: usage?.cache_read_input_tokens,
8953
- lastCallInputTokens: lastCallUsage?.input_tokens,
8954
- lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
8955
- lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
8956
- });
8957
- const timedOut = !stopReason;
8958
- emitResult({
8959
- subtype: timedOut ? "error_during_execution" : "success",
8960
- isError: timedOut,
8961
- stopReason: timedOut ? null : stopReason,
8962
- terminalReason: timedOut ? "error_during_execution" : "completed",
8963
- result: timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
8964
- usage
8965
- });
8966
- } catch (err) {
8967
- const e = err instanceof Error ? err : new Error(String(err));
8968
- log.error("interactive turn failed", { error: e.message });
8969
- emitResult({
8970
- subtype: "error_during_execution",
8971
- isError: true,
8972
- stopReason: null,
8973
- terminalReason: "error_during_execution",
8974
- result: `Interactive transport failed: ${e.message}`
8975
- });
8976
- if (errorHandlers.size > 0) {
8977
- for (const h of errorHandlers) h(e);
8978
- } else {
8979
- lineEmitter.emit("close");
8980
- }
9536
+ let writes = 0;
9537
+ let cancelledThrough = 0;
9538
+ let queued = 0;
9539
+ let chain = Promise.resolve();
9540
+ const runTurn = async (userMsg, turnNumber) => {
9541
+ const current = () => turnNumber === writes;
9542
+ const emit2 = (line) => {
9543
+ if (!current()) return;
9544
+ if (lineEmitter.listenerCount("line") === 0) {
9545
+ if (carriesReplyText(line)) bufferUnattendedLine(ap, line);
9546
+ return;
8981
9547
  }
8982
- })();
9548
+ lineEmitter.emit("line", line);
9549
+ };
9550
+ try {
9551
+ await ensureStarted();
9552
+ if (turnNumber <= cancelledThrough) {
9553
+ log.info("interactive turn cancelled before it started", { sessionId: session.sessionId });
9554
+ return;
9555
+ }
9556
+ openCalls.clear();
9557
+ denials = [];
9558
+ const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
9559
+ userMsg,
9560
+ (raw) => {
9561
+ try {
9562
+ openCalls.add(JSON.parse(raw));
9563
+ } catch {
9564
+ }
9565
+ emit2(raw);
9566
+ },
9567
+ void 0,
9568
+ // Only to a listening turn: it feeds that turn's watchdog, and the
9569
+ // TUI redraws its spinner the whole time it waits on a proxied call.
9570
+ () => {
9571
+ if (lineEmitter.listenerCount("line") > 0) {
9572
+ emit2(heartbeatFrame(session.sessionId, openCalls.newest()));
9573
+ }
9574
+ }
9575
+ );
9576
+ log.info("interactive turn ended", {
9577
+ end,
9578
+ stopReason,
9579
+ superseded: !current(),
9580
+ apiCalls: callCount,
9581
+ turnOutputTokens: usage?.output_tokens,
9582
+ turnInputTokens: usage?.input_tokens,
9583
+ turnCacheReadTokens: usage?.cache_read_input_tokens,
9584
+ lastCallInputTokens: lastCallUsage?.input_tokens,
9585
+ lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
9586
+ lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens,
9587
+ deniedPrompts: denied.length,
9588
+ cancelled: turnNumber <= cancelledThrough
9589
+ });
9590
+ emit2(
9591
+ interactiveResultFrame({
9592
+ sessionId: session.sessionId,
9593
+ end,
9594
+ stopReason,
9595
+ usage,
9596
+ denials
9597
+ })
9598
+ );
9599
+ } catch (err) {
9600
+ const e = err instanceof Error ? err : new Error(String(err));
9601
+ log.error("interactive turn failed", { error: e.message, superseded: !current() });
9602
+ if (!current()) return;
9603
+ emit2(
9604
+ interactiveResultFrame({
9605
+ sessionId: existsSync4(session.jsonlPath) ? session.sessionId : void 0,
9606
+ end: "failed",
9607
+ stopReason: null,
9608
+ error: e.message
9609
+ })
9610
+ );
9611
+ if (proc.listenerCount("error") > 0) proc.emit("error", e);
9612
+ else lineEmitter.emit("close");
9613
+ }
8983
9614
  };
8984
- const proc = {
9615
+ const enqueueTurn = (userMsg) => {
9616
+ const turnNumber = ++writes;
9617
+ queued++;
9618
+ chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
9619
+ queued--;
9620
+ });
9621
+ };
9622
+ Object.assign(proc, {
8985
9623
  stdin: {
9624
+ writable: true,
8986
9625
  write(chunk) {
8987
9626
  const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
8988
- runTurn(decodeUserEnvelope(raw));
9627
+ enqueueTurn(decodeUserEnvelope(raw));
8989
9628
  return true;
8990
9629
  },
8991
9630
  end() {
@@ -8995,47 +9634,61 @@ function spawnInteractiveProcess(opts) {
8995
9634
  stderr: null,
8996
9635
  pid: -1,
8997
9636
  killed: false,
8998
- on(event, fn) {
8999
- if (event === "error") errorHandlers.add(fn);
9000
- return proc;
9001
- },
9002
- once() {
9003
- return proc;
9004
- },
9005
- off(event, fn) {
9006
- if (event === "error") errorHandlers.delete(fn);
9007
- return proc;
9008
- },
9637
+ exitCode: null,
9638
+ signalCode: null,
9009
9639
  kill() {
9640
+ if (!proc.killed) {
9641
+ proc.killed = true;
9642
+ if (opts.systemPromptFile) {
9643
+ void unlink2(opts.systemPromptFile).catch(() => {
9644
+ });
9645
+ }
9646
+ }
9647
+ closeProxyServer();
9648
+ const started = startPromise !== null;
9010
9649
  try {
9011
9650
  session.dispose();
9012
9651
  } catch {
9013
9652
  }
9014
- if (opts.systemPromptFile) {
9015
- void unlink2(opts.systemPromptFile).catch(() => {
9016
- });
9653
+ if (!started && proc.exitCode === null && proc.signalCode === null) {
9654
+ proc.signalCode = "SIGTERM";
9655
+ proc.emit("exit", null, "SIGTERM");
9656
+ proc.emit("close", null, "SIGTERM");
9017
9657
  }
9018
- proc.killed = true;
9019
9658
  return true;
9020
9659
  }
9021
- };
9022
- return {
9660
+ });
9661
+ ap = {
9023
9662
  proc,
9024
9663
  lineEmitter,
9025
- proxyServer: null,
9664
+ proxyServer: opts.proxyServer ?? null,
9026
9665
  mcpHash: void 0,
9027
- systemPromptFile: opts.systemPromptFile
9666
+ unattendedLines: [],
9667
+ unattendedDropped: 0,
9668
+ systemPromptFile: opts.systemPromptFile,
9669
+ cliPath: opts.cliPath,
9670
+ interactiveControl: {
9671
+ turnRunning: () => queued > 0,
9672
+ // Stops the running turn and every queued one: they were all written
9673
+ // by the turn being aborted, or by one before it.
9674
+ interrupt: (timeoutMs) => {
9675
+ cancelledThrough = writes;
9676
+ return session.interrupt(timeoutMs);
9677
+ },
9678
+ flushTranscript: () => session.flushTranscript()
9679
+ }
9028
9680
  };
9681
+ return ap;
9029
9682
  }
9030
9683
 
9031
9684
  // src/prompts.ts
9032
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
9033
- import { homedir as homedir7 } from "os";
9685
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9686
+ import { homedir as homedir8 } from "os";
9034
9687
  import { randomUUID as randomUUID5 } from "crypto";
9035
- import { dirname as dirname6, join as join9 } from "path";
9688
+ import { dirname as dirname6, join as join10 } from "path";
9036
9689
  function readPromptFileIfPresent(path11) {
9037
9690
  try {
9038
- const content = readFileSync6(path11, "utf8").trim();
9691
+ const content = readFileSync7(path11, "utf8").trim();
9039
9692
  return content || void 0;
9040
9693
  } catch {
9041
9694
  return void 0;
@@ -9044,7 +9697,7 @@ function readPromptFileIfPresent(path11) {
9044
9697
  function nearestWorkspaceAgentsPrompt(cwd) {
9045
9698
  let dir = cwd;
9046
9699
  while (true) {
9047
- const content = readPromptFileIfPresent(join9(dir, "AGENTS.md"));
9700
+ const content = readPromptFileIfPresent(join10(dir, "AGENTS.md"));
9048
9701
  if (content) return content;
9049
9702
  const parent = dirname6(dir);
9050
9703
  if (parent === dir) return void 0;
@@ -9153,8 +9806,8 @@ ${options.compressionSummary.trim()}`
9153
9806
  for (const s of extraSystemContent) {
9154
9807
  if (s.trim()) parts.push(s.trim());
9155
9808
  }
9156
- const configRoot = process.env.XDG_CONFIG_HOME ?? join9(homedir7(), ".config");
9157
- const globalAgents = readPromptFileIfPresent(join9(configRoot, "opencode", "AGENTS.md"));
9809
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
9810
+ const globalAgents = readPromptFileIfPresent(join10(configRoot, "opencode", "AGENTS.md"));
9158
9811
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
9159
9812
  const forwarded = extraSystemContent.join("\n\n");
9160
9813
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -9165,9 +9818,9 @@ ${options.compressionSummary.trim()}`
9165
9818
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
9166
9819
  const content = parts.join("\n\n");
9167
9820
  if (!content) return void 0;
9168
- const path11 = join9(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
9821
+ const path11 = join10(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
9169
9822
  try {
9170
- writeFileSync4(path11, content, { encoding: "utf8", mode: 384 });
9823
+ writeFileSync5(path11, content, { encoding: "utf8", mode: 384 });
9171
9824
  return path11;
9172
9825
  } catch (err) {
9173
9826
  log.warn("failed to write system prompt file", { error: String(err) });
@@ -9885,8 +10538,8 @@ function synthesizeTitle(prompt) {
9885
10538
  "summary",
9886
10539
  "project"
9887
10540
  ]);
9888
- const words = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
9889
- const picked = (words.length > 0 ? words : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
10541
+ const words2 = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
10542
+ const picked = (words2.length > 0 ? words2 : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
9890
10543
  return picked || "New Session";
9891
10544
  }
9892
10545
 
@@ -10350,6 +11003,11 @@ function clearStartWatchdog(state) {
10350
11003
  function onStartWatchdogFire(state) {
10351
11004
  state.startWatchdog = null;
10352
11005
  if (state.controllerClosed || state.hasReceivedContent || state.hasReceivedProgress) return;
11006
+ const interactive = state.activeProcess?.interactiveControl;
11007
+ if (interactive) {
11008
+ if (interactive.turnRunning()) armStartWatchdog(state);
11009
+ return;
11010
+ }
10353
11011
  if (state.respawnAttempted) {
10354
11012
  log.error(
10355
11013
  "claude process still silent after respawn; ending turn",
@@ -10583,6 +11241,8 @@ function drainNow(state) {
10583
11241
  }
10584
11242
  if (state.drainBuffer.length === 0) return;
10585
11243
  if (state.controllerClosed) return;
11244
+ state.activeProcess?.interactiveControl?.flushTranscript();
11245
+ if (state.controllerClosed) return;
10586
11246
  const batch = state.drainBuffer.splice(0, state.drainBuffer.length);
10587
11247
  log.info("draining pending proxy calls into stream finish", {
10588
11248
  sessionKey: state.sessionKey,
@@ -12170,6 +12830,23 @@ var ClaudeCodeLanguageModel = class {
12170
12830
  }
12171
12831
  }
12172
12832
  }
12833
+ if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
12834
+ const configDir = resolveConfigDir(this.config.configDir);
12835
+ const resumePoint = findResumePoint({
12836
+ sessionKey: sk,
12837
+ prompt: options.prompt,
12838
+ cliPath,
12839
+ transcriptPath: (id) => join11(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
12840
+ });
12841
+ if (resumePoint) {
12842
+ setClaudeSessionId(sk, resumePoint.claudeSessionId);
12843
+ includeHistoryContext = false;
12844
+ log.notice("resuming the claude session from before the restart instead of replaying it", {
12845
+ sessionKey: sk,
12846
+ matchedMessages: resumePoint.matched
12847
+ });
12848
+ }
12849
+ }
12173
12850
  if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
12174
12851
  recordForkFingerprint(sk, options.prompt, cliPath);
12175
12852
  }
@@ -12419,13 +13096,145 @@ var ClaudeCodeLanguageModel = class {
12419
13096
  state.proxyServer = null;
12420
13097
  }
12421
13098
  }
13099
+ const resolveProxyWiring = async () => {
13100
+ const discovery = self.effectiveMcpConfig(
13101
+ cwd,
13102
+ void 0,
13103
+ runtimeStatus
13104
+ );
13105
+ const mcpResolution = self.resolvedProxyMcpTools(
13106
+ discovery.allEnabledServerNames,
13107
+ options.tools,
13108
+ new Set((resolvedProxy ?? []).map((def) => def.name))
13109
+ );
13110
+ const proxyMcpTools = mcpResolution?.defs ?? null;
13111
+ const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
13112
+ const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
13113
+ const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
13114
+ const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
13115
+ log.debug("opencode tool forwarding gate", {
13116
+ requested: self.config.proxyOpencodeTools ?? null,
13117
+ opencodeToolsRequested
13118
+ });
13119
+ const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
13120
+ resolved: false,
13121
+ taskDescription: void 0,
13122
+ taskParameters: void 0,
13123
+ questionDescription: void 0,
13124
+ hasQuestion: false
13125
+ };
13126
+ let enrichedProxy = resolvedProxy;
13127
+ let backgroundSubagentsSupported = false;
13128
+ if (enrichedProxy && taskProxyEnabled) {
13129
+ enrichedProxy = overlayTaskProxyDescription(
13130
+ enrichedProxy,
13131
+ liveToolInfo.taskDescription
13132
+ );
13133
+ log.info("task proxy description overlay", {
13134
+ applied: Boolean(liveToolInfo.taskDescription),
13135
+ liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
13136
+ listsAgentTypes: Boolean(
13137
+ liveToolInfo.taskDescription?.includes(
13138
+ "Available agent types"
13139
+ )
13140
+ )
13141
+ });
13142
+ const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
13143
+ backgroundSubagentsSupported = liveTaskSupportsBackground(
13144
+ liveToolInfo.taskParameters,
13145
+ hostDialect
13146
+ );
13147
+ enrichedProxy = applyBackgroundSubagentSupport(
13148
+ enrichedProxy,
13149
+ backgroundSubagentsSupported,
13150
+ hostDialect
13151
+ );
13152
+ recordBackgroundSubagentGate({
13153
+ supported: backgroundSubagentsSupported,
13154
+ hostApi: hostDialect,
13155
+ registryResolved: liveToolInfo.resolved
13156
+ });
13157
+ log.info("background subagent gate", {
13158
+ supported: backgroundSubagentsSupported,
13159
+ registryResolved: liveToolInfo.resolved,
13160
+ hostApi: hostDialect,
13161
+ 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"
13162
+ });
13163
+ }
13164
+ if (enrichedProxy && questionProxyEnabled) {
13165
+ enrichedProxy = overlayQuestionProxyDescription(
13166
+ enrichedProxy,
13167
+ liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
13168
+ );
13169
+ enrichedProxy = filterQuestionProxyByOpencodeSupport(
13170
+ enrichedProxy,
13171
+ liveToolInfo.hasQuestion
13172
+ );
13173
+ log.info("question proxy version gate", {
13174
+ opencodeHasQuestion: liveToolInfo.hasQuestion,
13175
+ kept: liveToolInfo.hasQuestion
13176
+ });
13177
+ }
13178
+ const opencodeToolDefs = resolveProxyOpencodeToolDefs({
13179
+ requested: self.config.proxyOpencodeTools,
13180
+ items: liveToolInfo.items,
13181
+ modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
13182
+ allowCodeExecution: self.config.permissionPreset !== "read-only",
13183
+ taken: new Set(
13184
+ [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
13185
+ (t) => t.name
13186
+ )
13187
+ )
13188
+ });
13189
+ if (opencodeToolDefs.length > 0) {
13190
+ log.info("forwarding opencode tools through the proxy", {
13191
+ tools: opencodeToolDefs.map((t) => t.name)
13192
+ });
13193
+ }
13194
+ const combinedList = [
13195
+ ...enrichedProxy ?? [],
13196
+ ...proxyMcpTools ?? [],
13197
+ ...opencodeToolDefs
13198
+ ];
13199
+ const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
13200
+ const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
13201
+ if (!state.proxyServer && combinedProxyTools) {
13202
+ state.proxyServer = await self.ensureProxyServer(
13203
+ combinedProxyTools,
13204
+ sk,
13205
+ pluginCompressEnabled,
13206
+ affinity
13207
+ );
13208
+ }
13209
+ const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
13210
+ const allDisallowed = resolveDisallowedTools({
13211
+ proxyTools: enrichedProxy,
13212
+ extraDisallowedTools: self.config.extraDisallowedTools,
13213
+ disableWebSearch: self.config.webSearch === "disabled"
13214
+ });
13215
+ return {
13216
+ excludeServers,
13217
+ taskProxyEnabled,
13218
+ backgroundSubagentsSupported,
13219
+ questionProxyActive,
13220
+ opencodeToolDefs,
13221
+ pluginCompressEnabled,
13222
+ allDisallowed
13223
+ };
13224
+ };
12422
13225
  if (useInteractive && !compactionMode) {
12423
- const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
12424
13226
  if (state.activeProcess) {
12425
13227
  state.proc = state.activeProcess.proc;
12426
13228
  state.lineEmitter = state.activeProcess.lineEmitter;
12427
13229
  log.debug("reusing active interactive session", { sk });
12428
13230
  } else {
13231
+ const wiring = await resolveProxyWiring();
13232
+ const mcp = self.effectiveMcpConfig(
13233
+ cwd,
13234
+ state.proxyServer?.configPath(),
13235
+ runtimeStatus,
13236
+ wiring.excludeServers
13237
+ );
12429
13238
  const allow = [
12430
13239
  ...mcp.allEnabledServerNames.map((n) => `mcp__${n}__*`),
12431
13240
  "mcp__opencode_proxy__*",
@@ -12439,12 +13248,27 @@ var ClaudeCodeLanguageModel = class {
12439
13248
  ];
12440
13249
  const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
12441
13250
  cwd,
12442
- self.config.multiStepContinuation !== false
13251
+ self.config.multiStepContinuation !== false,
12443
13252
  // Do not forward opencode's own system prompt into the
12444
13253
  // interactive TUI. Live subscription-account testing
12445
13254
  // showed that large forwarded payload can trigger Claude
12446
13255
  // Code's third-party-app usage gate, while our static
12447
- // CLI/AGENTS/continuation prompt remains safe.
13256
+ // CLI/AGENTS/continuation prompt remains safe. The
13257
+ // plugin's own short proxy hints are part of that static
13258
+ // prompt: without them the model narrates a subagent
13259
+ // dispatch instead of calling the proxy (h #g74).
13260
+ [
13261
+ ...wiring.taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
13262
+ ...wiring.backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
13263
+ ...wiring.questionProxyActive ? [QUESTION_PROXY_HINT] : []
13264
+ ],
13265
+ {
13266
+ compressEnabled: wiring.pluginCompressEnabled,
13267
+ opencodeCompressEnabled: wiring.opencodeToolDefs.some(
13268
+ (t) => t.name === "compress"
13269
+ ),
13270
+ compressionSummary: getCompressionSummary(sk)
13271
+ }
12448
13272
  );
12449
13273
  if (self.config.interactiveSystemPrompt === false) {
12450
13274
  log.warn(
@@ -12463,6 +13287,7 @@ var ClaudeCodeLanguageModel = class {
12463
13287
  ...self.skillBridgeSpawn(failover)
12464
13288
  });
12465
13289
  if (stoppedBeforeWork()) return;
13290
+ const resumeSessionId = getClaudeSessionId(sk);
12466
13291
  const ap = spawnInteractiveProcess({
12467
13292
  cwd,
12468
13293
  cliPath,
@@ -12472,10 +13297,24 @@ var ClaudeCodeLanguageModel = class {
12472
13297
  mcpConfigPaths: mcp.paths,
12473
13298
  pluginDirs: skillPluginDirs,
12474
13299
  permissionsAllow: allow,
13300
+ disallowedTools: wiring.allDisallowed,
13301
+ proxyServer: state.proxyServer,
12475
13302
  systemPromptFile,
12476
13303
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
12477
- effort: reasoningEffort
13304
+ effort: reasoningEffort,
13305
+ resumeSessionId,
13306
+ // The headless spawn's env, so hygiene, effort and the
13307
+ // agent's prompt cache TTL reach both transports alike.
13308
+ env: claudeSpawnEnv({
13309
+ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13310
+ effort: reasoningEffort,
13311
+ promptCacheTtl
13312
+ })
12478
13313
  });
13314
+ ap.proc.once(
13315
+ "exit",
13316
+ (code) => noteInteractiveProcessExit(sk, ap, code)
13317
+ );
12479
13318
  ap.mcpHash = mcp.bridgedHash;
12480
13319
  ap.mcpServers = mcp.allEnabledServerNames;
12481
13320
  setActiveProcess(sk, ap);
@@ -12484,6 +13323,7 @@ var ClaudeCodeLanguageModel = class {
12484
13323
  state.activeProcess = ap;
12485
13324
  log.info("spawned interactive claude session", {
12486
13325
  sk,
13326
+ resumed: !!resumeSessionId,
12487
13327
  cliPath,
12488
13328
  configDir: self.config.configDir,
12489
13329
  model: effectiveModelId
@@ -12505,121 +13345,15 @@ var ClaudeCodeLanguageModel = class {
12505
13345
  cliVersion
12506
13346
  });
12507
13347
  } else {
12508
- const discovery = self.effectiveMcpConfig(
12509
- cwd,
12510
- void 0,
12511
- runtimeStatus
12512
- );
12513
- const mcpResolution = self.resolvedProxyMcpTools(
12514
- discovery.allEnabledServerNames,
12515
- options.tools,
12516
- new Set((resolvedProxy ?? []).map((def) => def.name))
12517
- );
12518
- const proxyMcpTools = mcpResolution?.defs ?? null;
12519
- const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
12520
- const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
12521
- const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
12522
- const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
12523
- log.debug("opencode tool forwarding gate", {
12524
- requested: self.config.proxyOpencodeTools ?? null,
12525
- opencodeToolsRequested
12526
- });
12527
- const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
12528
- resolved: false,
12529
- taskDescription: void 0,
12530
- taskParameters: void 0,
12531
- questionDescription: void 0,
12532
- hasQuestion: false
12533
- };
12534
- let enrichedProxy = resolvedProxy;
12535
- let backgroundSubagentsSupported = false;
12536
- if (enrichedProxy && taskProxyEnabled) {
12537
- enrichedProxy = overlayTaskProxyDescription(
12538
- enrichedProxy,
12539
- liveToolInfo.taskDescription
12540
- );
12541
- log.info("task proxy description overlay", {
12542
- applied: Boolean(liveToolInfo.taskDescription),
12543
- liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
12544
- listsAgentTypes: Boolean(
12545
- liveToolInfo.taskDescription?.includes(
12546
- "Available agent types"
12547
- )
12548
- )
12549
- });
12550
- const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
12551
- backgroundSubagentsSupported = liveTaskSupportsBackground(
12552
- liveToolInfo.taskParameters,
12553
- hostDialect
12554
- );
12555
- enrichedProxy = applyBackgroundSubagentSupport(
12556
- enrichedProxy,
12557
- backgroundSubagentsSupported,
12558
- hostDialect
12559
- );
12560
- recordBackgroundSubagentGate({
12561
- supported: backgroundSubagentsSupported,
12562
- hostApi: hostDialect,
12563
- registryResolved: liveToolInfo.resolved
12564
- });
12565
- log.info("background subagent gate", {
12566
- supported: backgroundSubagentsSupported,
12567
- registryResolved: liveToolInfo.resolved,
12568
- hostApi: hostDialect,
12569
- 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"
12570
- });
12571
- }
12572
- if (enrichedProxy && questionProxyEnabled) {
12573
- enrichedProxy = overlayQuestionProxyDescription(
12574
- enrichedProxy,
12575
- liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
12576
- );
12577
- enrichedProxy = filterQuestionProxyByOpencodeSupport(
12578
- enrichedProxy,
12579
- liveToolInfo.hasQuestion
12580
- );
12581
- log.info("question proxy version gate", {
12582
- opencodeHasQuestion: liveToolInfo.hasQuestion,
12583
- kept: liveToolInfo.hasQuestion
12584
- });
12585
- }
12586
- const opencodeToolDefs = resolveProxyOpencodeToolDefs({
12587
- requested: self.config.proxyOpencodeTools,
12588
- items: liveToolInfo.items,
12589
- modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
12590
- allowCodeExecution: self.config.permissionPreset !== "read-only",
12591
- taken: new Set(
12592
- [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
12593
- (t) => t.name
12594
- )
12595
- )
12596
- });
12597
- if (opencodeToolDefs.length > 0) {
12598
- log.info("forwarding opencode tools through the proxy", {
12599
- tools: opencodeToolDefs.map((t) => t.name)
12600
- });
12601
- }
12602
- const combinedList = [
12603
- ...enrichedProxy ?? [],
12604
- ...proxyMcpTools ?? [],
12605
- ...opencodeToolDefs
12606
- ];
12607
- const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
12608
- const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
12609
- if (!state.proxyServer && combinedProxyTools) {
12610
- state.proxyServer = await self.ensureProxyServer(
12611
- combinedProxyTools,
12612
- sk,
12613
- pluginCompressEnabled,
12614
- affinity
12615
- );
12616
- }
12617
- const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
12618
- const allDisallowed = resolveDisallowedTools({
12619
- proxyTools: enrichedProxy,
12620
- extraDisallowedTools: self.config.extraDisallowedTools,
12621
- disableWebSearch: self.config.webSearch === "disabled"
12622
- });
13348
+ const {
13349
+ excludeServers,
13350
+ taskProxyEnabled,
13351
+ backgroundSubagentsSupported,
13352
+ questionProxyActive,
13353
+ opencodeToolDefs,
13354
+ pluginCompressEnabled,
13355
+ allDisallowed
13356
+ } = await resolveProxyWiring();
12623
13357
  const mcp = self.effectiveMcpConfig(
12624
13358
  cwd,
12625
13359
  state.proxyServer?.configPath(),
@@ -12723,6 +13457,9 @@ var ClaudeCodeLanguageModel = class {
12723
13457
  state.autoContinueState.startedAt = Date.now();
12724
13458
  const completeResult = (msg) => {
12725
13459
  if (state.controllerClosed) return;
13460
+ if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
13461
+ recordResumePoint(sk, msg.session_id, options.prompt, cliPath);
13462
+ }
12726
13463
  if (deliverPendingCompletions(state)) {
12727
13464
  if (state.drainBuffer.length > 0) drainNow(state);
12728
13465
  return;
@@ -13179,15 +13916,15 @@ var ClaudeCodeLanguageModel = class {
13179
13916
 
13180
13917
  // src/cleanup-stale.ts
13181
13918
  import {
13182
- existsSync as existsSync3,
13183
- mkdirSync as mkdirSync4,
13184
- readFileSync as readFileSync7,
13919
+ existsSync as existsSync5,
13920
+ mkdirSync as mkdirSync5,
13921
+ readFileSync as readFileSync8,
13185
13922
  realpathSync as realpathSync4,
13186
13923
  rmSync as rmSync3,
13187
- writeFileSync as writeFileSync5
13924
+ writeFileSync as writeFileSync6
13188
13925
  } from "fs";
13189
- import { homedir as homedir8 } from "os";
13190
- import { dirname as dirname7, join as join10, resolve as resolve5 } from "path";
13926
+ import { homedir as homedir9 } from "os";
13927
+ import { dirname as dirname7, join as join12, resolve as resolve5 } from "path";
13191
13928
  import { fileURLToPath as fileURLToPath4 } from "url";
13192
13929
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
13193
13930
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -13195,20 +13932,20 @@ var alreadyRan = false;
13195
13932
  function candidateCacheRoots() {
13196
13933
  const xdg = process.env.XDG_CACHE_HOME;
13197
13934
  return [
13198
- xdg ? join10(xdg, "opencode") : null,
13199
- join10(homedir8(), ".cache", "opencode"),
13200
- join10(homedir8(), "Library", "Caches", "opencode")
13935
+ xdg ? join12(xdg, "opencode") : null,
13936
+ join12(homedir9(), ".cache", "opencode"),
13937
+ join12(homedir9(), "Library", "Caches", "opencode")
13201
13938
  ].filter((p) => Boolean(p));
13202
13939
  }
13203
13940
  function userOpencodeJsonPath() {
13204
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
13205
- return join10(xdgConfig, "opencode", "opencode.json");
13941
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
13942
+ return join12(xdgConfig, "opencode", "opencode.json");
13206
13943
  }
13207
13944
  function userIntendsToUseUnscoped() {
13208
13945
  const cfg = userOpencodeJsonPath();
13209
- if (!existsSync3(cfg)) return false;
13946
+ if (!existsSync5(cfg)) return false;
13210
13947
  try {
13211
- const json = JSON.parse(readFileSync7(cfg, "utf8"));
13948
+ const json = JSON.parse(readFileSync8(cfg, "utf8"));
13212
13949
  const plugins = json.plugin;
13213
13950
  if (!Array.isArray(plugins)) return false;
13214
13951
  return plugins.some(
@@ -13229,19 +13966,19 @@ function ourLoadedDir() {
13229
13966
  function ourVersion(dir) {
13230
13967
  if (!dir) return null;
13231
13968
  try {
13232
- const pkg = JSON.parse(readFileSync7(join10(dir, "package.json"), "utf8"));
13969
+ const pkg = JSON.parse(readFileSync8(join12(dir, "package.json"), "utf8"));
13233
13970
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
13234
13971
  } catch {
13235
13972
  return null;
13236
13973
  }
13237
13974
  }
13238
13975
  function markerPath() {
13239
- const stateRoot = process.env.XDG_STATE_HOME ?? join10(homedir8(), ".local", "state");
13240
- return join10(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
13976
+ const stateRoot = process.env.XDG_STATE_HOME ?? join12(homedir9(), ".local", "state");
13977
+ return join12(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
13241
13978
  }
13242
13979
  function markerVersion() {
13243
13980
  try {
13244
- const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
13981
+ const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
13245
13982
  return typeof marker.version === "string" ? marker.version : null;
13246
13983
  } catch {
13247
13984
  return null;
@@ -13250,8 +13987,8 @@ function markerVersion() {
13250
13987
  function writeMarker(version) {
13251
13988
  const file = markerPath();
13252
13989
  try {
13253
- mkdirSync4(dirname7(file), { recursive: true });
13254
- writeFileSync5(
13990
+ mkdirSync5(dirname7(file), { recursive: true });
13991
+ writeFileSync6(
13255
13992
  file,
13256
13993
  JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
13257
13994
  "utf8"
@@ -13285,20 +14022,20 @@ function cleanupStaleUnscopedInstall() {
13285
14022
  if (version) writeMarker(version);
13286
14023
  }
13287
14024
  function cleanupOne(cacheRoot, ourDir) {
13288
- if (!existsSync3(cacheRoot)) return;
13289
- const stalePath = join10(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
13290
- if (!existsSync3(stalePath)) return;
14025
+ if (!existsSync5(cacheRoot)) return;
14026
+ const stalePath = join12(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
14027
+ if (!existsSync5(stalePath)) return;
13291
14028
  let realStalePath = stalePath;
13292
14029
  try {
13293
14030
  realStalePath = realpathSync4(stalePath);
13294
14031
  } catch {
13295
14032
  }
13296
14033
  if (ourDir && realStalePath === ourDir) return;
13297
- const pkgJsonPath = join10(stalePath, "package.json");
13298
- if (!existsSync3(pkgJsonPath)) return;
14034
+ const pkgJsonPath = join12(stalePath, "package.json");
14035
+ if (!existsSync5(pkgJsonPath)) return;
13299
14036
  let pkg = {};
13300
14037
  try {
13301
- pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
14038
+ pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
13302
14039
  } catch {
13303
14040
  return;
13304
14041
  }
@@ -13314,13 +14051,13 @@ function cleanupOne(cacheRoot, ourDir) {
13314
14051
  });
13315
14052
  return;
13316
14053
  }
13317
- const cachePkgJson = join10(cacheRoot, "package.json");
13318
- if (!existsSync3(cachePkgJson)) return;
14054
+ const cachePkgJson = join12(cacheRoot, "package.json");
14055
+ if (!existsSync5(cachePkgJson)) return;
13319
14056
  try {
13320
- const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
14057
+ const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
13321
14058
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
13322
14059
  delete cfg.dependencies[STALE_PACKAGE_NAME];
13323
- writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
14060
+ writeFileSync6(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
13324
14061
  log.info("cleanup-stale: pruned dep from cache package.json");
13325
14062
  }
13326
14063
  } catch (err) {
@@ -13981,6 +14718,7 @@ function createClaudeCode(settings = {}) {
13981
14718
  bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
13982
14719
  turnStats: settings.turnStats === true,
13983
14720
  forkSessions: settings.forkSessions === true,
14721
+ resumeAfterRestart: settings.resumeAfterRestart !== false,
13984
14722
  interactive: settings.interactive,
13985
14723
  interactiveBypass: settings.interactiveBypass,
13986
14724
  interactiveAllowTools: settings.interactiveAllowTools,
@@ -14066,12 +14804,12 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
14066
14804
  }
14067
14805
  return models;
14068
14806
  }
14069
- function isRecord4(value) {
14807
+ function isRecord5(value) {
14070
14808
  return typeof value === "object" && value !== null && !Array.isArray(value);
14071
14809
  }
14072
14810
  function completeUserModel(id, entry, providerID) {
14073
- const record = isRecord4(entry) ? entry : {};
14074
- const runtimeShaped = isRecord4(record.capabilities) && isRecord4(record.capabilities.input) && isRecord4(record.capabilities.output) && isRecord4(record.cost) && isRecord4(record.cost.cache) && isRecord4(record.api);
14811
+ const record = isRecord5(entry) ? entry : {};
14812
+ const runtimeShaped = isRecord5(record.capabilities) && isRecord5(record.capabilities.input) && isRecord5(record.capabilities.output) && isRecord5(record.cost) && isRecord5(record.cost.cache) && isRecord5(record.api);
14075
14813
  if (runtimeShaped) {
14076
14814
  return toConfigModel({ ...record, providerID });
14077
14815
  }
@@ -14084,8 +14822,8 @@ function completeUserModel(id, entry, providerID) {
14084
14822
  const merged = { ...base, ...record, id };
14085
14823
  for (const key of ["cost", "limit", "variants"]) {
14086
14824
  merged[key] = {
14087
- ...isRecord4(base[key]) ? base[key] : {},
14088
- ...isRecord4(record[key]) ? record[key] : {}
14825
+ ...isRecord5(base[key]) ? base[key] : {},
14826
+ ...isRecord5(record[key]) ? record[key] : {}
14089
14827
  };
14090
14828
  }
14091
14829
  return merged;
@@ -14182,7 +14920,7 @@ async function buildAgentRegistry(config) {
14182
14920
  typeof configuredTtl === "string" ? configuredTtl : void 0
14183
14921
  );
14184
14922
  setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
14185
- const records = await readAgentMarkdownRecords(
14923
+ const records2 = await readAgentMarkdownRecords(
14186
14924
  agentDirectories(
14187
14925
  process.env.HOME ?? process.env.USERPROFILE,
14188
14926
  getOpencodeProjectDirectory()
@@ -14197,18 +14935,18 @@ async function buildAgentRegistry(config) {
14197
14935
  const declaredChain = parseFallbackModelList(
14198
14936
  agent.fallbackModels ?? bag.fallbackModels
14199
14937
  );
14200
- records[name] = {
14201
- mode: pick("mode") ?? records[name]?.mode,
14202
- model: pick("model") ?? records[name]?.model,
14203
- forceModel: pick("forceModel") ?? records[name]?.forceModel,
14204
- reasoningEffort: pick("reasoningEffort") ?? records[name]?.reasoningEffort,
14205
- cacheTtl: pick("cacheTtl") ?? records[name]?.cacheTtl,
14206
- fallbackModels: declaredChain.length ? declaredChain : records[name]?.fallbackModels
14938
+ records2[name] = {
14939
+ mode: pick("mode") ?? records2[name]?.mode,
14940
+ model: pick("model") ?? records2[name]?.model,
14941
+ forceModel: pick("forceModel") ?? records2[name]?.forceModel,
14942
+ reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
14943
+ cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
14944
+ fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels
14207
14945
  };
14208
14946
  }
14209
- setAgentRegistry(records);
14947
+ setAgentRegistry(records2);
14210
14948
  log.debug("agent registry built", {
14211
- agents: Object.keys(records).length,
14949
+ agents: Object.keys(records2).length,
14212
14950
  defaultSubagentModel: getDefaultSubagentModel()
14213
14951
  });
14214
14952
  }