@khalilgharbaoui/opencode-claude-code-plugin 0.46.0 → 0.46.1

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
@@ -1625,7 +1625,108 @@ function failoverUntil(resetsAt) {
1625
1625
  // src/cli-version.ts
1626
1626
  import { execFile } from "child_process";
1627
1627
  import { promisify } from "util";
1628
+
1629
+ // src/windows-spawn.ts
1630
+ import * as fs from "fs";
1631
+ import * as path2 from "path";
1632
+ var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
1633
+ function envValue(env, name) {
1634
+ const direct = env[name];
1635
+ if (direct !== void 0) return direct;
1636
+ const match = Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase());
1637
+ return match === void 0 ? void 0 : env[match];
1638
+ }
1639
+ function defaultIsFile(candidate) {
1640
+ try {
1641
+ return fs.statSync(candidate).isFile();
1642
+ } catch {
1643
+ return false;
1644
+ }
1645
+ }
1646
+ function executableExtensions(env = process.env) {
1647
+ const raw = envValue(env, "PATHEXT") ?? DEFAULT_PATHEXT;
1648
+ return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
1649
+ }
1650
+ function isBatchFile(file) {
1651
+ const ext = path2.win32.extname(file).toLowerCase();
1652
+ return ext === ".cmd" || ext === ".bat";
1653
+ }
1654
+ function resolveWindowsCommand(command, deps = {}) {
1655
+ const env = deps.env ?? process.env;
1656
+ const isFile = deps.isFile ?? defaultIsFile;
1657
+ const extensions = executableExtensions(env);
1658
+ const candidatesFor = (base) => {
1659
+ const ext = path2.win32.extname(base).toLowerCase();
1660
+ if (ext && extensions.includes(ext)) return [base];
1661
+ return [...extensions.map((candidate) => base + candidate), base];
1662
+ };
1663
+ if (command.includes("\\") || command.includes("/")) {
1664
+ for (const candidate of candidatesFor(command)) {
1665
+ if (isFile(candidate)) return candidate;
1666
+ }
1667
+ return null;
1668
+ }
1669
+ const pathValue = envValue(env, "PATH") ?? "";
1670
+ for (const entry of pathValue.split(path2.win32.delimiter)) {
1671
+ const directory = entry.trim().replace(/^"(.*)"$/, "$1");
1672
+ if (!directory) continue;
1673
+ for (const candidate of candidatesFor(path2.win32.join(directory, command))) {
1674
+ if (isFile(candidate)) return candidate;
1675
+ }
1676
+ }
1677
+ return null;
1678
+ }
1679
+ function quoteBatchArgument(arg) {
1680
+ if (/[\r\n]/.test(arg)) {
1681
+ throw new Error("cmd.exe cannot carry a newline in an argument");
1682
+ }
1683
+ let out = '"';
1684
+ let backslashes = 0;
1685
+ for (const char of arg) {
1686
+ if (char === "\\") {
1687
+ backslashes += 1;
1688
+ continue;
1689
+ }
1690
+ if (char === '"') {
1691
+ out += "\\".repeat(backslashes * 2) + '""';
1692
+ backslashes = 0;
1693
+ continue;
1694
+ }
1695
+ out += "\\".repeat(backslashes) + char;
1696
+ backslashes = 0;
1697
+ }
1698
+ return `${out}${"\\".repeat(backslashes * 2)}"`;
1699
+ }
1700
+ function planCmdInvocation(file, args, env = process.env) {
1701
+ const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
1702
+ const line = [`"${path2.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
1703
+ return {
1704
+ file: comspec,
1705
+ args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
1706
+ windowsVerbatimArguments: true
1707
+ };
1708
+ }
1709
+ function planClaudeSpawn(command, args, deps = {}) {
1710
+ const platform2 = deps.platform ?? process.platform;
1711
+ if (platform2 !== "win32") return { file: command, args: [...args] };
1712
+ const env = deps.env ?? process.env;
1713
+ const resolved = resolveWindowsCommand(command, deps) ?? command;
1714
+ if (isBatchFile(resolved)) return planCmdInvocation(resolved, args, env);
1715
+ return { file: resolved, args: [...args] };
1716
+ }
1717
+
1718
+ // src/cli-version.ts
1628
1719
  var execFileAsync = promisify(execFile);
1720
+ function probeExec(cliPath, args, options) {
1721
+ const plan = planClaudeSpawn(cliPath, args);
1722
+ return execFileAsync(plan.file, plan.args, {
1723
+ ...options,
1724
+ // utf8 is already `execFile`'s default; naming it is what keeps the
1725
+ // promisified overload returning strings rather than Buffers.
1726
+ encoding: "utf8",
1727
+ windowsVerbatimArguments: plan.windowsVerbatimArguments
1728
+ });
1729
+ }
1629
1730
  var cache = /* @__PURE__ */ new Map();
1630
1731
  var PROBE_TIMEOUT_MS = 5e3;
1631
1732
  var probeTimeoutMs = PROBE_TIMEOUT_MS;
@@ -1672,7 +1773,7 @@ function detectCliVersion(cliPath) {
1672
1773
  let deadlineKill = false;
1673
1774
  const promise = (async () => {
1674
1775
  try {
1675
- const { stdout } = await execFileAsync(cliPath, ["--version"], {
1776
+ const { stdout } = await probeExec(cliPath, ["--version"], {
1676
1777
  timeout: probeTimeoutMs
1677
1778
  });
1678
1779
  const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
@@ -1769,7 +1870,7 @@ function detectHeadlessSupport(cliPath) {
1769
1870
  let deadlineKill = false;
1770
1871
  const promise = (async () => {
1771
1872
  try {
1772
- const execution = execFileAsync(cliPath, [
1873
+ const execution = probeExec(cliPath, [
1773
1874
  "--print",
1774
1875
  "--input-format",
1775
1876
  "stream-json",
@@ -1805,7 +1906,7 @@ function detectCliSupportsFlag(cliPath, flag) {
1805
1906
  let deadlineKill = false;
1806
1907
  const promise = (async () => {
1807
1908
  try {
1808
- const execution = execFileAsync(cliPath, ["--help"], {
1909
+ const execution = probeExec(cliPath, ["--help"], {
1809
1910
  timeout: probeTimeoutMs,
1810
1911
  killSignal: "SIGKILL",
1811
1912
  maxBuffer: 4 * 1024 * 1024
@@ -1838,17 +1939,17 @@ import { unlink } from "fs/promises";
1838
1939
 
1839
1940
  // src/proxy-mcp.ts
1840
1941
  import { createServer } from "http";
1841
- import * as fs2 from "fs";
1842
- import * as path3 from "path";
1942
+ import * as fs3 from "fs";
1943
+ import * as path4 from "path";
1843
1944
  import * as crypto from "crypto";
1844
1945
  import { EventEmitter } from "events";
1845
1946
 
1846
1947
  // src/tmp.ts
1847
- import * as fs from "fs";
1948
+ import * as fs2 from "fs";
1848
1949
  import * as os from "os";
1849
- import * as path2 from "path";
1950
+ import * as path3 from "path";
1850
1951
  var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
1851
- var PID_TMP_DIR = path2.join(
1952
+ var PID_TMP_DIR = path3.join(
1852
1953
  os.tmpdir(),
1853
1954
  `${PLUGIN_TMP_PREFIX}${process.pid}`
1854
1955
  );
@@ -1861,7 +1962,7 @@ function currentUid() {
1861
1962
  function untrustworthyReason(dir) {
1862
1963
  let stat;
1863
1964
  try {
1864
- stat = fs.lstatSync(dir);
1965
+ stat = fs2.lstatSync(dir);
1865
1966
  } catch {
1866
1967
  return null;
1867
1968
  }
@@ -1874,16 +1975,16 @@ function untrustworthyReason(dir) {
1874
1975
  return null;
1875
1976
  }
1876
1977
  function ensureDir(dir) {
1877
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
1978
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
1878
1979
  try {
1879
- fs.chmodSync(dir, 448);
1980
+ fs2.chmodSync(dir, 448);
1880
1981
  } catch {
1881
1982
  }
1882
1983
  }
1883
1984
  function chooseDir() {
1884
1985
  const reason = untrustworthyReason(PID_TMP_DIR);
1885
1986
  if (!reason) return PID_TMP_DIR;
1886
- const fallback = fs.mkdtempSync(path2.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
1987
+ const fallback = fs2.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
1887
1988
  log.warn("refusing the pid-named scratch directory", {
1888
1989
  path: PID_TMP_DIR,
1889
1990
  reason,
@@ -1897,7 +1998,7 @@ function registerExitCleanup() {
1897
1998
  process.on("exit", () => {
1898
1999
  for (const dir of createdDirs) {
1899
2000
  try {
1900
- fs.rmSync(dir, { recursive: true, force: true });
2001
+ fs2.rmSync(dir, { recursive: true, force: true });
1901
2002
  } catch {
1902
2003
  }
1903
2004
  }
@@ -1918,23 +2019,23 @@ function sweepStalePluginTmpDirs(keep) {
1918
2019
  const root = os.tmpdir();
1919
2020
  let entries;
1920
2021
  try {
1921
- entries = fs.readdirSync(root);
2022
+ entries = fs2.readdirSync(root);
1922
2023
  } catch {
1923
2024
  return removed;
1924
2025
  }
1925
2026
  const uid = currentUid();
1926
- const keepPath = keep ? path2.resolve(keep) : null;
2027
+ const keepPath = keep ? path3.resolve(keep) : null;
1927
2028
  for (const entry of entries) {
1928
2029
  const match = PID_DIR_NAME.exec(entry);
1929
2030
  if (!match) continue;
1930
2031
  const pid = Number(match[1]);
1931
2032
  if (!Number.isSafeInteger(pid) || pid <= 0) continue;
1932
2033
  if (pid === process.pid) continue;
1933
- const dir = path2.join(root, entry);
1934
- if (keepPath && path2.resolve(dir) === keepPath) continue;
2034
+ const dir = path3.join(root, entry);
2035
+ if (keepPath && path3.resolve(dir) === keepPath) continue;
1935
2036
  let stat;
1936
2037
  try {
1937
- stat = fs.lstatSync(dir);
2038
+ stat = fs2.lstatSync(dir);
1938
2039
  } catch {
1939
2040
  continue;
1940
2041
  }
@@ -1942,7 +2043,7 @@ function sweepStalePluginTmpDirs(keep) {
1942
2043
  if (uid !== void 0 && stat.uid !== uid) continue;
1943
2044
  if (isProcessAlive(pid)) continue;
1944
2045
  try {
1945
- fs.rmSync(dir, { recursive: true, force: true });
2046
+ fs2.rmSync(dir, { recursive: true, force: true });
1946
2047
  removed.push(dir);
1947
2048
  } catch (err) {
1948
2049
  log.debug("could not remove a stale scratch directory", {
@@ -2773,11 +2874,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2773
2874
  2
2774
2875
  );
2775
2876
  const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
2776
- const outPath = path3.join(
2877
+ const outPath = path4.join(
2777
2878
  pluginTmpDir(),
2778
2879
  `proxy-${hash}.json`
2779
2880
  );
2780
- fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
2881
+ fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
2781
2882
  configFilePath = outPath;
2782
2883
  return outPath;
2783
2884
  },
@@ -2794,7 +2895,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2794
2895
  });
2795
2896
  if (configFilePath) {
2796
2897
  try {
2797
- fs2.unlinkSync(configFilePath);
2898
+ fs3.unlinkSync(configFilePath);
2798
2899
  } catch {
2799
2900
  }
2800
2901
  configFilePath = null;
@@ -3363,8 +3464,8 @@ function clearCompression(sessionKey2) {
3363
3464
  }
3364
3465
 
3365
3466
  // src/mcp-bridge.ts
3366
- import * as fs3 from "fs";
3367
- import * as path4 from "path";
3467
+ import * as fs4 from "fs";
3468
+ import * as path5 from "path";
3368
3469
  import * as os2 from "os";
3369
3470
  import * as crypto2 from "crypto";
3370
3471
  import {
@@ -3375,21 +3476,21 @@ var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
3375
3476
  var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
3376
3477
  function fileExists(p) {
3377
3478
  try {
3378
- return fs3.statSync(p).isFile();
3479
+ return fs4.statSync(p).isFile();
3379
3480
  } catch {
3380
3481
  return false;
3381
3482
  }
3382
3483
  }
3383
3484
  function dirExists(p) {
3384
3485
  try {
3385
- return fs3.statSync(p).isDirectory();
3486
+ return fs4.statSync(p).isDirectory();
3386
3487
  } catch {
3387
3488
  return false;
3388
3489
  }
3389
3490
  }
3390
3491
  function readAndParse(file) {
3391
3492
  try {
3392
- const raw = fs3.readFileSync(file, "utf8");
3493
+ const raw = fs4.readFileSync(file, "utf8");
3393
3494
  const errors = [];
3394
3495
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3395
3496
  if (errors.length > 0) {
@@ -3425,14 +3526,14 @@ function deepMerge(target, source) {
3425
3526
  }
3426
3527
  function walkUp(opts) {
3427
3528
  const out = [];
3428
- let current = path4.resolve(opts.start);
3529
+ let current = path5.resolve(opts.start);
3429
3530
  while (true) {
3430
3531
  for (const target of opts.targets) {
3431
- const candidate = path4.join(current, target);
3532
+ const candidate = path5.join(current, target);
3432
3533
  if (opts.predicate(candidate)) out.push(candidate);
3433
3534
  }
3434
- if (opts.stop && current === path4.resolve(opts.stop)) break;
3435
- const parent = path4.dirname(current);
3535
+ if (opts.stop && current === path5.resolve(opts.stop)) break;
3536
+ const parent = path5.dirname(current);
3436
3537
  if (parent === current) break;
3437
3538
  current = parent;
3438
3539
  }
@@ -3440,28 +3541,28 @@ function walkUp(opts) {
3440
3541
  }
3441
3542
  function detectWorktree(cwd) {
3442
3543
  const override = process.env.OPENCODE_WORKTREE;
3443
- if (override) return path4.resolve(override);
3444
- let current = path4.resolve(cwd);
3544
+ if (override) return path5.resolve(override);
3545
+ let current = path5.resolve(cwd);
3445
3546
  while (true) {
3446
- const gitPath = path4.join(current, ".git");
3547
+ const gitPath = path5.join(current, ".git");
3447
3548
  try {
3448
- if (fs3.existsSync(gitPath)) return current;
3549
+ if (fs4.existsSync(gitPath)) return current;
3449
3550
  } catch {
3450
3551
  }
3451
- const parent = path4.dirname(current);
3552
+ const parent = path5.dirname(current);
3452
3553
  if (parent === current) return void 0;
3453
3554
  current = parent;
3454
3555
  }
3455
3556
  }
3456
3557
  function globalConfigDir() {
3457
- const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
3458
- return path4.join(xdg, "opencode");
3558
+ const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os2.homedir(), ".config");
3559
+ return path5.join(xdg, "opencode");
3459
3560
  }
3460
3561
  function loadGlobalConfig(hostApi = "v1") {
3461
3562
  const dir = globalConfigDir();
3462
3563
  let merged = {};
3463
3564
  for (const name of FILE_NAMES.slice().reverse()) {
3464
- const file = path4.join(dir, name);
3565
+ const file = path5.join(dir, name);
3465
3566
  if (!fileExists(file)) continue;
3466
3567
  const parsed = readAndParse(file);
3467
3568
  if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
@@ -3471,7 +3572,7 @@ function loadGlobalConfig(hostApi = "v1") {
3471
3572
  function loadProjectFilesInDir(dir, hostApi = "v1") {
3472
3573
  let merged = {};
3473
3574
  for (const name of PROJECT_FILE_NAMES) {
3474
- const file = path4.join(dir, name);
3575
+ const file = path5.join(dir, name);
3475
3576
  if (!fileExists(file)) continue;
3476
3577
  const parsed = readAndParse(file);
3477
3578
  if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
@@ -3482,7 +3583,7 @@ function dotOpencodeDirs(cwd, worktree) {
3482
3583
  const dirs = [];
3483
3584
  const seen = /* @__PURE__ */ new Set();
3484
3585
  const push = (p) => {
3485
- const abs = path4.resolve(p);
3586
+ const abs = path5.resolve(p);
3486
3587
  if (!seen.has(abs) && dirExists(abs)) {
3487
3588
  seen.add(abs);
3488
3589
  dirs.push(abs);
@@ -3498,7 +3599,7 @@ function dotOpencodeDirs(cwd, worktree) {
3498
3599
  }
3499
3600
  const home = os2.homedir();
3500
3601
  if (home) {
3501
- const homeDot = path4.join(home, ".opencode");
3602
+ const homeDot = path5.join(home, ".opencode");
3502
3603
  if (dirExists(homeDot)) push(homeDot);
3503
3604
  }
3504
3605
  const envDir = process.env.OPENCODE_CONFIG_DIR;
@@ -3645,7 +3746,7 @@ function opencodeConfigLayers(cwd, hostApi = "v1") {
3645
3746
  const projectDirs = [];
3646
3747
  const seenProjectDirs = /* @__PURE__ */ new Set();
3647
3748
  for (const f of projectFiles) {
3648
- const d = path4.dirname(f);
3749
+ const d = path5.dirname(f);
3649
3750
  if (!seenProjectDirs.has(d)) {
3650
3751
  seenProjectDirs.add(d);
3651
3752
  projectDirs.push(d);
@@ -3712,13 +3813,13 @@ function finishBridge(input) {
3712
3813
  }
3713
3814
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
3714
3815
  const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
3715
- const outPath = path4.join(
3816
+ const outPath = path5.join(
3716
3817
  pluginTmpDir(),
3717
3818
  `mcp-${bodyDigest}.json`
3718
3819
  );
3719
3820
  try {
3720
3821
  if (!fileExists(outPath)) {
3721
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3822
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3722
3823
  }
3723
3824
  } catch (e) {
3724
3825
  log.warn("failed to write bridged MCP config", {
@@ -5308,11 +5409,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
5308
5409
  effort,
5309
5410
  promptCacheTtl
5310
5411
  });
5311
- const proc = spawn(cliPath, cliArgs, {
5412
+ const plan = planClaudeSpawn(cliPath, cliArgs);
5413
+ const proc = spawn(plan.file, plan.args, {
5312
5414
  cwd,
5313
5415
  stdio: ["pipe", "pipe", "pipe"],
5314
5416
  env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
5315
- shell: process.platform === "win32"
5417
+ windowsVerbatimArguments: plan.windowsVerbatimArguments
5316
5418
  });
5317
5419
  const lineEmitter = new EventEmitter3();
5318
5420
  const ap = {
@@ -6411,8 +6513,8 @@ function redactLogLine(raw, context) {
6411
6513
  if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
6412
6514
  return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
6413
6515
  }
6414
- function readLogTail(path11, maxBytes = BUNDLE_TAIL_BYTES) {
6415
- const fd = openSync(path11, "r");
6516
+ function readLogTail(path12, maxBytes = BUNDLE_TAIL_BYTES) {
6517
+ const fd = openSync(path12, "r");
6416
6518
  try {
6417
6519
  const size = fstatSync(fd).size;
6418
6520
  const length = Math.min(size, maxBytes);
@@ -6562,13 +6664,20 @@ function parsePlanUsage(stdout) {
6562
6664
  return { status: "failed", error: "no result object in the CLI's reply" };
6563
6665
  }
6564
6666
  function runCli(cliPath, args, timeoutMs, env) {
6667
+ const plan = planClaudeSpawn(cliPath, args);
6565
6668
  return new Promise((resolve6, reject) => {
6566
6669
  execFile2(
6567
- cliPath,
6568
- args,
6670
+ plan.file,
6671
+ plan.args,
6569
6672
  // `killSignal` so a CLI wedged on a hook is actually gone, and a generous
6570
6673
  // buffer because the reply is prose of unbounded length.
6571
- { timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024, env },
6674
+ {
6675
+ timeout: timeoutMs,
6676
+ killSignal: "SIGKILL",
6677
+ maxBuffer: 4 * 1024 * 1024,
6678
+ env,
6679
+ windowsVerbatimArguments: plan.windowsVerbatimArguments
6680
+ },
6572
6681
  (error, stdout) => {
6573
6682
  if (stdout && stdout.trim()) return resolve6(stdout);
6574
6683
  if (error) return reject(error);
@@ -6607,14 +6716,14 @@ async function fetchPlanUsage(cliPath, options = {}) {
6607
6716
  }
6608
6717
 
6609
6718
  // src/stale-build.ts
6610
- import * as fs5 from "fs";
6611
- import * as path6 from "path";
6719
+ import * as fs6 from "fs";
6720
+ import * as path7 from "path";
6612
6721
  import { fileURLToPath as fileURLToPath2 } from "url";
6613
6722
 
6614
6723
  // src/startup-diagnostics.ts
6615
6724
  import { execFile as execFile3 } from "child_process";
6616
- import * as fs4 from "fs";
6617
- import * as path5 from "path";
6725
+ import * as fs5 from "fs";
6726
+ import * as path6 from "path";
6618
6727
  import { promisify as promisify2 } from "util";
6619
6728
  import { fileURLToPath } from "url";
6620
6729
 
@@ -6649,8 +6758,8 @@ var cachedPluginVersion;
6649
6758
  function pluginVersion() {
6650
6759
  if (cachedPluginVersion) return cachedPluginVersion;
6651
6760
  try {
6652
- const here = path5.dirname(fileURLToPath(import.meta.url));
6653
- const raw = fs4.readFileSync(path5.join(here, "..", "package.json"), "utf8");
6761
+ const here = path6.dirname(fileURLToPath(import.meta.url));
6762
+ const raw = fs5.readFileSync(path6.join(here, "..", "package.json"), "utf8");
6654
6763
  const version = JSON.parse(raw).version;
6655
6764
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
6656
6765
  } catch {
@@ -6674,7 +6783,7 @@ var opencodeVersionProbe;
6674
6783
  function detectOpencodeVersion(execPath = process.execPath) {
6675
6784
  if (opencodeVersionProbe) return opencodeVersionProbe;
6676
6785
  opencodeVersionProbe = (async () => {
6677
- if (!path5.basename(execPath).toLowerCase().includes("opencode")) {
6786
+ if (!path6.basename(execPath).toLowerCase().includes("opencode")) {
6678
6787
  log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
6679
6788
  return void 0;
6680
6789
  }
@@ -6903,10 +7012,10 @@ function defaultEntryPath() {
6903
7012
  }
6904
7013
  function readOnDiskBuild(entryPath) {
6905
7014
  try {
6906
- const raw = fs5.readFileSync(path6.join(path6.dirname(entryPath), "..", "package.json"), "utf8");
7015
+ const raw = fs6.readFileSync(path7.join(path7.dirname(entryPath), "..", "package.json"), "utf8");
6907
7016
  const version = JSON.parse(raw).version;
6908
7017
  if (typeof version !== "string" || version.length === 0) return void 0;
6909
- const stat = fs5.statSync(entryPath);
7018
+ const stat = fs6.statSync(entryPath);
6910
7019
  return { version, mtimeMs: stat.mtimeMs, size: stat.size };
6911
7020
  } catch {
6912
7021
  return void 0;
@@ -6917,7 +7026,7 @@ function recordLoadedBuild() {
6917
7026
  let mtimeMs;
6918
7027
  let size;
6919
7028
  try {
6920
- const stat = fs5.statSync(entryPath);
7029
+ const stat = fs6.statSync(entryPath);
6921
7030
  mtimeMs = stat.mtimeMs;
6922
7031
  size = stat.size;
6923
7032
  } catch {
@@ -7354,7 +7463,7 @@ Could not build the report: ${message}`;
7354
7463
 
7355
7464
  // src/agent-models.ts
7356
7465
  import { readFile, readdir } from "fs/promises";
7357
- import path7 from "path";
7466
+ import path8 from "path";
7358
7467
 
7359
7468
  // src/models.ts
7360
7469
  var PROVIDER_ID = "claude-code";
@@ -7878,13 +7987,13 @@ async function readAgentMarkdownRecords(directories) {
7878
7987
  const name = entry.slice(0, -3);
7879
7988
  if (records2[name]) continue;
7880
7989
  try {
7881
- const text = await readFile(path7.join(directory, entry), "utf8");
7990
+ const text = await readFile(path8.join(directory, entry), "utf8");
7882
7991
  records2[name] = parseAgentFrontmatter(text);
7883
7992
  const prompt = agentMarkdownBody(text);
7884
7993
  if (prompt) records2[name].prompt = prompt;
7885
7994
  } catch (err) {
7886
7995
  log.debug("failed to read agent markdown", {
7887
- file: path7.join(directory, entry),
7996
+ file: path8.join(directory, entry),
7888
7997
  error: String(err)
7889
7998
  });
7890
7999
  }
@@ -7896,12 +8005,12 @@ function agentDirectories(home, projectDirectory) {
7896
8005
  const directories = [];
7897
8006
  if (projectDirectory) {
7898
8007
  for (const name of AGENT_DIR_NAMES) {
7899
- directories.push(path7.join(projectDirectory, ".opencode", name));
8008
+ directories.push(path8.join(projectDirectory, ".opencode", name));
7900
8009
  }
7901
8010
  }
7902
8011
  if (home) {
7903
8012
  for (const name of AGENT_DIR_NAMES) {
7904
- directories.push(path7.join(home, ".config", "opencode", name));
8013
+ directories.push(path8.join(home, ".config", "opencode", name));
7905
8014
  }
7906
8015
  }
7907
8016
  return directories;
@@ -8501,14 +8610,14 @@ function getTrailingUserMessages(prompt, opts = {}) {
8501
8610
 
8502
8611
  // src/skill-bridge.ts
8503
8612
  import * as crypto3 from "crypto";
8504
- import * as fs6 from "fs";
8613
+ import * as fs7 from "fs";
8505
8614
  import * as os3 from "os";
8506
- import * as path8 from "path";
8615
+ import * as path9 from "path";
8507
8616
  import { fileURLToPath as fileURLToPath3 } from "url";
8508
8617
  function bundledSkillsDir() {
8509
8618
  try {
8510
8619
  const here = fileURLToPath3(import.meta.url);
8511
- const dir = path8.resolve(path8.dirname(here), "..", "skills");
8620
+ const dir = path9.resolve(path9.dirname(here), "..", "skills");
8512
8621
  return dirExists2(dir) ? dir : null;
8513
8622
  } catch {
8514
8623
  return null;
@@ -8516,16 +8625,16 @@ function bundledSkillsDir() {
8516
8625
  }
8517
8626
  function dirExists2(p) {
8518
8627
  try {
8519
- return fs6.statSync(p).isDirectory();
8628
+ return fs7.statSync(p).isDirectory();
8520
8629
  } catch {
8521
8630
  return false;
8522
8631
  }
8523
8632
  }
8524
8633
  function realPath(p) {
8525
8634
  try {
8526
- return fs6.realpathSync(p);
8635
+ return fs7.realpathSync(p);
8527
8636
  } catch {
8528
- return path8.resolve(p);
8637
+ return path9.resolve(p);
8529
8638
  }
8530
8639
  }
8531
8640
  function envEnabled(name) {
@@ -8546,10 +8655,10 @@ function declaredSkillName(source) {
8546
8655
  return SAFE_SKILL_NAME.test(name) ? name : null;
8547
8656
  }
8548
8657
  function readSkill(root, entry) {
8549
- const dir = path8.join(root, entry);
8658
+ const dir = path9.join(root, entry);
8550
8659
  let source;
8551
8660
  try {
8552
- source = fs6.readFileSync(path8.join(dir, "SKILL.md"), "utf8");
8661
+ source = fs7.readFileSync(path9.join(dir, "SKILL.md"), "utf8");
8553
8662
  } catch {
8554
8663
  return null;
8555
8664
  }
@@ -8567,47 +8676,47 @@ function skillRoots(cwd) {
8567
8676
  const roots = [];
8568
8677
  const seen = /* @__PURE__ */ new Set();
8569
8678
  const push = (p) => {
8570
- const abs = path8.resolve(p);
8679
+ const abs = path9.resolve(p);
8571
8680
  if (seen.has(abs)) return;
8572
8681
  seen.add(abs);
8573
8682
  if (dirExists2(abs)) roots.push(abs);
8574
8683
  };
8575
8684
  const home = os3.homedir();
8576
- const homeDir = home ? path8.resolve(home) : null;
8577
- let current = path8.resolve(cwd);
8685
+ const homeDir = home ? path9.resolve(home) : null;
8686
+ let current = path9.resolve(cwd);
8578
8687
  while (true) {
8579
- push(path8.join(current, ".opencode", "skills"));
8688
+ push(path9.join(current, ".opencode", "skills"));
8580
8689
  if (current !== homeDir) {
8581
- push(path8.join(current, ".claude", "skills"));
8582
- push(path8.join(current, ".agents", "skills"));
8690
+ push(path9.join(current, ".claude", "skills"));
8691
+ push(path9.join(current, ".agents", "skills"));
8583
8692
  }
8584
- const parent = path8.dirname(current);
8693
+ const parent = path9.dirname(current);
8585
8694
  if (parent === current) break;
8586
8695
  current = parent;
8587
8696
  }
8588
- if (homeDir) push(path8.join(homeDir, ".opencode", "skills"));
8697
+ if (homeDir) push(path9.join(homeDir, ".opencode", "skills"));
8589
8698
  const envDir = process.env.OPENCODE_CONFIG_DIR;
8590
8699
  if (envDir) {
8591
- push(path8.join(envDir, "skills"));
8592
- push(path8.join(envDir, "skill"));
8700
+ push(path9.join(envDir, "skills"));
8701
+ push(path9.join(envDir, "skill"));
8593
8702
  }
8594
- const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path8.join(homeDir, ".config") : null);
8703
+ const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path9.join(homeDir, ".config") : null);
8595
8704
  if (xdg) {
8596
- push(path8.join(xdg, "opencode", "skills"));
8597
- push(path8.join(xdg, "opencode", "skill"));
8705
+ push(path9.join(xdg, "opencode", "skills"));
8706
+ push(path9.join(xdg, "opencode", "skill"));
8598
8707
  }
8599
8708
  if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
8600
8709
  if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
8601
- push(path8.join(homeDir, ".claude", "skills"));
8710
+ push(path9.join(homeDir, ".claude", "skills"));
8602
8711
  }
8603
- push(path8.join(homeDir, ".agents", "skills"));
8712
+ push(path9.join(homeDir, ".agents", "skills"));
8604
8713
  }
8605
8714
  return roots;
8606
8715
  }
8607
8716
  function collectSkills(root, claimed, found) {
8608
8717
  let entries;
8609
8718
  try {
8610
- entries = fs6.readdirSync(root, { withFileTypes: true });
8719
+ entries = fs7.readdirSync(root, { withFileTypes: true });
8611
8720
  } catch {
8612
8721
  return;
8613
8722
  }
@@ -8647,23 +8756,23 @@ function discoverBundledSkills() {
8647
8756
  }
8648
8757
  function claudeConfigDir(configDir) {
8649
8758
  const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
8650
- if (explicit) return path8.resolve(expandHome(explicit));
8759
+ if (explicit) return path9.resolve(expandHome(explicit));
8651
8760
  const home = os3.homedir();
8652
- return home ? path8.resolve(home, ".claude") : null;
8761
+ return home ? path9.resolve(home, ".claude") : null;
8653
8762
  }
8654
8763
  function installedPluginSkillRoots(configDir, cwd) {
8655
8764
  if (!configDir) return [];
8656
8765
  let parsed;
8657
8766
  try {
8658
8767
  parsed = JSON.parse(
8659
- fs6.readFileSync(path8.join(configDir, "plugins", "installed_plugins.json"), "utf8")
8768
+ fs7.readFileSync(path9.join(configDir, "plugins", "installed_plugins.json"), "utf8")
8660
8769
  );
8661
8770
  } catch {
8662
8771
  return [];
8663
8772
  }
8664
8773
  const plugins = parsed?.plugins;
8665
8774
  if (!plugins || typeof plugins !== "object") return [];
8666
- const workspace = path8.resolve(cwd);
8775
+ const workspace = path9.resolve(cwd);
8667
8776
  const roots = [];
8668
8777
  for (const installs of Object.values(plugins)) {
8669
8778
  if (!Array.isArray(installs)) continue;
@@ -8671,28 +8780,28 @@ function installedPluginSkillRoots(configDir, cwd) {
8671
8780
  const entry = install;
8672
8781
  if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
8673
8782
  if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
8674
- roots.push(path8.join(entry.installPath, "skills"));
8783
+ roots.push(path9.join(entry.installPath, "skills"));
8675
8784
  }
8676
8785
  }
8677
8786
  return roots;
8678
8787
  }
8679
8788
  function coversWorkspace(projectPath, workspace) {
8680
8789
  if (typeof projectPath !== "string" || !projectPath) return false;
8681
- const root = path8.resolve(projectPath);
8790
+ const root = path9.resolve(projectPath);
8682
8791
  if (root === workspace) return true;
8683
- const rel = path8.relative(root, workspace);
8684
- return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
8792
+ const rel = path9.relative(root, workspace);
8793
+ return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
8685
8794
  }
8686
8795
  function discoverNativeClaudeSkills(opts) {
8687
8796
  const found = [];
8688
8797
  const scanned = /* @__PURE__ */ new Set();
8689
8798
  const scan = (root, scope) => {
8690
- const abs = path8.resolve(root);
8799
+ const abs = path9.resolve(root);
8691
8800
  if (scanned.has(abs)) return;
8692
8801
  scanned.add(abs);
8693
8802
  let entries;
8694
8803
  try {
8695
- entries = fs6.readdirSync(abs, { withFileTypes: true });
8804
+ entries = fs7.readdirSync(abs, { withFileTypes: true });
8696
8805
  } catch {
8697
8806
  return;
8698
8807
  }
@@ -8704,13 +8813,13 @@ function discoverNativeClaudeSkills(opts) {
8704
8813
  }
8705
8814
  };
8706
8815
  const configDir = claudeConfigDir(opts.configDir);
8707
- if (configDir) scan(path8.join(configDir, "skills"), "user");
8816
+ if (configDir) scan(path9.join(configDir, "skills"), "user");
8708
8817
  const home = os3.homedir();
8709
- const homeDir = home ? path8.resolve(home) : null;
8710
- let current = path8.resolve(opts.cwd);
8818
+ const homeDir = home ? path9.resolve(home) : null;
8819
+ let current = path9.resolve(opts.cwd);
8711
8820
  while (true) {
8712
- if (current !== homeDir) scan(path8.join(current, ".claude", "skills"), "project");
8713
- const parent = path8.dirname(current);
8821
+ if (current !== homeDir) scan(path9.join(current, ".claude", "skills"), "project");
8822
+ const parent = path9.dirname(current);
8714
8823
  if (parent === current) break;
8715
8824
  current = parent;
8716
8825
  }
@@ -8744,10 +8853,10 @@ function dropNativelyLoadedSkills(skills, native) {
8744
8853
  }
8745
8854
  function readBundledSkillInfos() {
8746
8855
  return discoverBundledSkills().flatMap((skill) => {
8747
- const file = path8.join(skill.dir, "SKILL.md");
8856
+ const file = path9.join(skill.dir, "SKILL.md");
8748
8857
  let text;
8749
8858
  try {
8750
- text = fs6.readFileSync(file, "utf8");
8859
+ text = fs7.readFileSync(file, "utf8");
8751
8860
  } catch {
8752
8861
  return [];
8753
8862
  }
@@ -8771,15 +8880,15 @@ function readBundledSkillInfos() {
8771
8880
  }
8772
8881
  function linkSkill(source, target) {
8773
8882
  try {
8774
- fs6.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
8883
+ fs7.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
8775
8884
  return;
8776
8885
  } catch {
8777
- fs6.cpSync(source, target, { recursive: true, dereference: true });
8886
+ fs7.cpSync(source, target, { recursive: true, dereference: true });
8778
8887
  }
8779
8888
  }
8780
8889
  function fileExists2(p) {
8781
8890
  try {
8782
- return fs6.statSync(p).isFile();
8891
+ return fs7.statSync(p).isFile();
8783
8892
  } catch {
8784
8893
  return false;
8785
8894
  }
@@ -8788,14 +8897,14 @@ function buildSkillPluginDir(skills) {
8788
8897
  if (skills.length === 0) return null;
8789
8898
  const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
8790
8899
  const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
8791
- const root = path8.join(pluginTmpDir(), `skills-${hash}`);
8792
- const manifest = path8.join(root, ".claude-plugin", "plugin.json");
8900
+ const root = path9.join(pluginTmpDir(), `skills-${hash}`);
8901
+ const manifest = path9.join(root, ".claude-plugin", "plugin.json");
8793
8902
  if (fileExists2(manifest)) return root;
8794
8903
  try {
8795
- fs6.rmSync(root, { recursive: true, force: true });
8796
- fs6.mkdirSync(path8.join(root, ".claude-plugin"), { recursive: true });
8797
- fs6.mkdirSync(path8.join(root, "skills"), { recursive: true });
8798
- fs6.writeFileSync(
8904
+ fs7.rmSync(root, { recursive: true, force: true });
8905
+ fs7.mkdirSync(path9.join(root, ".claude-plugin"), { recursive: true });
8906
+ fs7.mkdirSync(path9.join(root, "skills"), { recursive: true });
8907
+ fs7.writeFileSync(
8799
8908
  manifest,
8800
8909
  JSON.stringify(
8801
8910
  {
@@ -8808,7 +8917,7 @@ function buildSkillPluginDir(skills) {
8808
8917
  { encoding: "utf8", mode: 384 }
8809
8918
  );
8810
8919
  for (const skill of skills) {
8811
- linkSkill(skill.dir, path8.join(root, "skills", skill.name));
8920
+ linkSkill(skill.dir, path9.join(root, "skills", skill.name));
8812
8921
  }
8813
8922
  } catch (err) {
8814
8923
  log.warn("failed to stage opencode skill plugin dir", {
@@ -8824,7 +8933,7 @@ function registerBundledSkillPath(config) {
8824
8933
  if (!dir) return false;
8825
8934
  config.skills ??= {};
8826
8935
  const paths = config.skills.paths ??= [];
8827
- if (paths.some((p) => path8.resolve(p) === dir)) return false;
8936
+ if (paths.some((p) => path9.resolve(p) === dir)) return false;
8828
8937
  paths.push(dir);
8829
8938
  return true;
8830
8939
  }
@@ -8878,12 +8987,13 @@ import { join as join10 } from "path";
8878
8987
 
8879
8988
  // src/claude-session-bun.ts
8880
8989
  import * as os4 from "os";
8881
- import * as fs7 from "fs";
8882
- import * as path9 from "path";
8990
+ import * as fs8 from "fs";
8991
+ import * as path10 from "path";
8883
8992
  import { execFileSync } from "child_process";
8884
8993
  import { randomUUID as randomUUID4 } from "crypto";
8994
+ import { StringDecoder } from "string_decoder";
8885
8995
  function resolveClaude(cmd = "claude") {
8886
- if (path9.isAbsolute(cmd) && fs7.existsSync(cmd)) return cmd;
8996
+ if (path10.isAbsolute(cmd) && fs8.existsSync(cmd)) return cmd;
8887
8997
  const viaBun = Bun.which(cmd);
8888
8998
  if (viaBun) return viaBun;
8889
8999
  const isWin = os4.platform() === "win32";
@@ -8892,24 +9002,30 @@ function resolveClaude(cmd = "claude") {
8892
9002
  encoding: "utf8",
8893
9003
  stdio: ["ignore", "pipe", "ignore"]
8894
9004
  });
8895
- const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs7.existsSync(p));
9005
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs8.existsSync(p));
8896
9006
  if (first) return first;
8897
9007
  } catch {
8898
9008
  }
8899
9009
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
8900
9010
  }
8901
9011
  function encodeCwd(cwd) {
8902
- const resolved = path9.resolve(cwd);
9012
+ const resolved = path10.resolve(cwd);
8903
9013
  let real = resolved;
8904
9014
  try {
8905
- real = fs7.realpathSync.native(resolved);
9015
+ real = fs8.realpathSync.native(resolved);
8906
9016
  } catch {
8907
9017
  }
8908
9018
  return real.replace(/[^a-zA-Z0-9]/g, "-");
8909
9019
  }
8910
9020
  var bunPtySpawner = (argv, opts) => {
8911
9021
  const [command, ...args] = argv;
8912
- return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
9022
+ const resolved = resolveClaude(command ?? "claude");
9023
+ if (os4.platform() === "win32" && isBatchFile(resolved)) {
9024
+ throw new Error(
9025
+ `The interactive transport cannot run a Windows batch shim (${resolved}). Point \`cliPath\` at claude.exe, or use the default headless transport.`
9026
+ );
9027
+ }
9028
+ return Bun.spawn([resolved, ...args], {
8913
9029
  cwd: opts.cwd,
8914
9030
  env: opts.env,
8915
9031
  terminal: {
@@ -9124,6 +9240,123 @@ function isInterruptRecord(rec) {
9124
9240
  function isTurnDurationRecord(rec) {
9125
9241
  return rec?.type === "system" && rec.subtype === "turn_duration";
9126
9242
  }
9243
+ var TRANSCRIPT_CHUNK_BYTES = 1 << 20;
9244
+ var TranscriptCursor = class {
9245
+ /** The path is asked for on every poll rather than kept, because it is a
9246
+ * field of the session and a test may repoint it; a path that changes is
9247
+ * a different file, which the `dev:ino` check already starts over on. */
9248
+ constructor(resolvePath) {
9249
+ this.resolvePath = resolvePath;
9250
+ }
9251
+ resolvePath;
9252
+ /** Bytes of the current file already read. */
9253
+ offset = 0;
9254
+ /** `dev:ino` of the file those bytes came from, null before the first
9255
+ * successful stat. A different one is a new file, not growth. */
9256
+ fileId = null;
9257
+ /** Holds back an incomplete trailing UTF-8 sequence between two reads. */
9258
+ decoder = new StringDecoder("utf8");
9259
+ /** What was read after the last newline: a record the CLI is part way
9260
+ * through writing, completed by a later read. */
9261
+ partial = "";
9262
+ /** Complete lines seen since byte 0 of the current file. */
9263
+ seen = 0;
9264
+ /** Complete lines handed on: the line-index cursor. */
9265
+ taken = 0;
9266
+ /** Complete lines read and not handed on yet, oldest first. */
9267
+ queue = [];
9268
+ stats = { polls: 0, reads: 0, bytes: 0 };
9269
+ /** Complete lines in the file now. */
9270
+ lineCount() {
9271
+ this.poll(true);
9272
+ return this.seen;
9273
+ }
9274
+ /** The complete lines not handed on yet, oldest first. They stay queued
9275
+ * until `take` says how many of them were used. */
9276
+ pending() {
9277
+ this.poll(true);
9278
+ return this.queue;
9279
+ }
9280
+ /** Whether the file holds a complete line this cursor has not handed on. */
9281
+ hasUnread() {
9282
+ return this.pending().length > 0;
9283
+ }
9284
+ /** Mark the first `count` pending lines as handed on. */
9285
+ take(count) {
9286
+ const used = Math.min(count, this.queue.length);
9287
+ if (used <= 0) return;
9288
+ this.queue.splice(0, used);
9289
+ this.taken += used;
9290
+ }
9291
+ /** Hand on nothing of what is in the file now: the records written before a
9292
+ * turn started belong to the turn before it. */
9293
+ skipToEnd() {
9294
+ this.poll(false);
9295
+ this.taken = Math.max(this.taken, this.seen);
9296
+ this.queue.length = 0;
9297
+ }
9298
+ /** Read whatever was appended since the last look. */
9299
+ poll(collect) {
9300
+ this.stats.polls++;
9301
+ const filePath = this.resolvePath();
9302
+ let stat;
9303
+ try {
9304
+ stat = fs8.statSync(filePath);
9305
+ } catch {
9306
+ return;
9307
+ }
9308
+ const fileId = `${stat.dev}:${stat.ino}`;
9309
+ if (this.fileId !== null && (fileId !== this.fileId || stat.size < this.offset)) {
9310
+ this.offset = 0;
9311
+ this.seen = 0;
9312
+ this.partial = "";
9313
+ this.decoder = new StringDecoder("utf8");
9314
+ this.queue.length = 0;
9315
+ }
9316
+ this.fileId = fileId;
9317
+ if (stat.size <= this.offset) return;
9318
+ let fd = null;
9319
+ try {
9320
+ fd = fs8.openSync(filePath, "r");
9321
+ const buffer = Buffer.allocUnsafe(
9322
+ Math.min(TRANSCRIPT_CHUNK_BYTES, stat.size - this.offset)
9323
+ );
9324
+ while (this.offset < stat.size) {
9325
+ const want = Math.min(buffer.length, stat.size - this.offset);
9326
+ const read = fs8.readSync(fd, buffer, 0, want, this.offset);
9327
+ this.stats.reads++;
9328
+ if (read <= 0) break;
9329
+ this.offset += read;
9330
+ this.stats.bytes += read;
9331
+ this.ingest(this.decoder.write(buffer.subarray(0, read)), collect);
9332
+ }
9333
+ } catch {
9334
+ } finally {
9335
+ if (fd !== null) {
9336
+ try {
9337
+ fs8.closeSync(fd);
9338
+ } catch {
9339
+ }
9340
+ }
9341
+ }
9342
+ }
9343
+ /** Split decoded text into complete lines, keeping the unterminated tail. */
9344
+ ingest(text, collect) {
9345
+ if (!text) return;
9346
+ this.partial += text;
9347
+ let start = 0;
9348
+ for (; ; ) {
9349
+ const newline = this.partial.indexOf("\n", start);
9350
+ if (newline === -1) break;
9351
+ if (collect && this.seen >= this.taken) {
9352
+ this.queue.push(this.partial.slice(start, newline));
9353
+ }
9354
+ this.seen++;
9355
+ start = newline + 1;
9356
+ }
9357
+ if (start > 0) this.partial = this.partial.slice(start);
9358
+ }
9359
+ };
9127
9360
  var normalizedPrompt = (text) => text.replace(/\s+/g, " ").trim();
9128
9361
  function isOwnPromptRecord(rec, normalized) {
9129
9362
  const content = rec?.type === "user" ? rec.message?.content : void 0;
@@ -9131,12 +9364,12 @@ function isOwnPromptRecord(rec, normalized) {
9131
9364
  }
9132
9365
  function resolveConfigDir(configDir) {
9133
9366
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
9134
- if (!value) return path9.join(os4.homedir(), ".claude");
9367
+ if (!value) return path10.join(os4.homedir(), ".claude");
9135
9368
  if (value === "~") return os4.homedir();
9136
9369
  if (value.startsWith("~/") || value.startsWith("~\\")) {
9137
- return path9.join(os4.homedir(), value.slice(2));
9370
+ return path10.join(os4.homedir(), value.slice(2));
9138
9371
  }
9139
- return path9.resolve(value);
9372
+ return path10.resolve(value);
9140
9373
  }
9141
9374
  function fatalScreenMessage(kind, configDir, cwd) {
9142
9375
  if (kind === "trust") {
@@ -9155,8 +9388,8 @@ var ClaudeSession = class {
9155
9388
  /** The newest terminal output, ANSI included, for diagnostics. */
9156
9389
  raw = "";
9157
9390
  proc = null;
9158
- cursor = 0;
9159
- // index into transcript split('\n')
9391
+ /** The transcript, read forwards only; its `taken` is the old line cursor. */
9392
+ transcript;
9160
9393
  lastDataAt = 0;
9161
9394
  exited = false;
9162
9395
  exitCode = null;
@@ -9200,7 +9433,7 @@ var ClaudeSession = class {
9200
9433
  abandonTurn = false;
9201
9434
  o;
9202
9435
  constructor(opts = {}) {
9203
- this.cwd = path9.resolve(opts.cwd ?? process.cwd());
9436
+ this.cwd = path10.resolve(opts.cwd ?? process.cwd());
9204
9437
  this.configDir = resolveConfigDir(opts.configDir);
9205
9438
  this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
9206
9439
  this.signal = opts.signal;
@@ -9212,12 +9445,13 @@ var ClaudeSession = class {
9212
9445
  this.onExit = opts.onExit;
9213
9446
  this.spawnPty = opts.spawnPty ?? bunPtySpawner;
9214
9447
  this.sessionId = opts.resumeSessionId ?? randomUUID4();
9215
- this.jsonlPath = path9.join(
9448
+ this.jsonlPath = path10.join(
9216
9449
  this.configDir,
9217
9450
  "projects",
9218
9451
  encodeCwd(this.cwd),
9219
9452
  `${this.sessionId}.jsonl`
9220
9453
  );
9454
+ this.transcript = new TranscriptCursor(() => this.jsonlPath);
9221
9455
  this.o = {
9222
9456
  cwd: this.cwd,
9223
9457
  cliPath: opts.cliPath,
@@ -9322,7 +9556,12 @@ var ClaudeSession = class {
9322
9556
  };
9323
9557
  proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
9324
9558
  await this.waitForBoot();
9325
- this.cursor = this.lineCount();
9559
+ this.transcript.skipToEnd();
9560
+ }
9561
+ /** How much this session has read off the transcript. Read-only, and the
9562
+ * one thing `scripts/bench-transcript-poll.ts` measures. */
9563
+ get transcriptStats() {
9564
+ return this.transcript.stats;
9326
9565
  }
9327
9566
  onData(chunk) {
9328
9567
  this.lastDataAt = Date.now();
@@ -9469,22 +9708,10 @@ var ClaudeSession = class {
9469
9708
  while (Date.now() < until) {
9470
9709
  await delay(80);
9471
9710
  if (this.aborted || this.exited) return;
9472
- if (this.lineCount() > this.cursor) return;
9711
+ if (this.transcript.hasUnread()) return;
9473
9712
  }
9474
9713
  }
9475
9714
  }
9476
- readRawLines() {
9477
- try {
9478
- return fs7.readFileSync(this.jsonlPath, "utf8").split("\n");
9479
- } catch {
9480
- return [];
9481
- }
9482
- }
9483
- /** Count of complete lines (split('\n') minus the trailing/partial element). */
9484
- lineCount() {
9485
- const lines = this.readRawLines();
9486
- return lines.length > 0 ? lines.length - 1 : 0;
9487
- }
9488
9715
  rawTail(max = 600) {
9489
9716
  const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
9490
9717
  return clean.length > max ? clean.slice(-max) : clean;
@@ -9517,7 +9744,7 @@ var ClaudeSession = class {
9517
9744
  this.turn = new Promise((resolve6) => settle = resolve6);
9518
9745
  this.turnDenied = [];
9519
9746
  this.abandonTurn = false;
9520
- this.cursor = Math.max(this.cursor, this.lineCount());
9747
+ this.transcript.skipToEnd();
9521
9748
  this.screen = "";
9522
9749
  try {
9523
9750
  if (this.o.bracketedPaste) {
@@ -9582,30 +9809,26 @@ var ClaudeSession = class {
9582
9809
  /**
9583
9810
  * Read every complete transcript record past the cursor into the running
9584
9811
  * turn. Returns whether anything new was read. The one place a record is
9585
- * interpreted, shared by the poll loop and `flushTranscript`.
9812
+ * interpreted, shared by the poll loop and `flushTranscript`, and the only
9813
+ * one that consumes what `TranscriptCursor` has queued.
9586
9814
  */
9587
9815
  scanTranscript() {
9588
9816
  const scan = this.scan;
9589
9817
  if (!scan || scan.end) return false;
9590
- const lines = this.readRawLines();
9591
- const lastComplete = lines.length - 1;
9592
- if (lastComplete <= this.cursor) return false;
9593
- for (let i = this.cursor; i < lastComplete && !scan.end; i++) {
9818
+ const lines = this.transcript.pending().slice();
9819
+ if (lines.length === 0) return false;
9820
+ let used = 0;
9821
+ for (let i = 0; i < lines.length && !scan.end; i++) {
9594
9822
  const s = lines[i];
9595
- if (!s || !s.trim()) {
9596
- this.cursor = i + 1;
9597
- continue;
9598
- }
9823
+ used = i + 1;
9824
+ if (!s || !s.trim()) continue;
9599
9825
  let rec = null;
9600
9826
  try {
9601
9827
  rec = JSON.parse(s);
9602
9828
  } catch {
9603
9829
  }
9604
9830
  if (scan.awaitingOwnPrompt !== null) {
9605
- if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) {
9606
- this.cursor = i + 1;
9607
- continue;
9608
- }
9831
+ if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) continue;
9609
9832
  scan.awaitingOwnPrompt = null;
9610
9833
  }
9611
9834
  if (rec) this.notePlanApproval(rec);
@@ -9625,9 +9848,8 @@ var ClaudeSession = class {
9625
9848
  scan.end = scan.stopReason ? "stop" : "ended";
9626
9849
  }
9627
9850
  }
9628
- this.cursor = i + 1;
9629
9851
  }
9630
- if (!scan.end) this.cursor = lastComplete;
9852
+ this.transcript.take(used);
9631
9853
  scan.lastBeat = Date.now();
9632
9854
  scan.stopSeenAt = Date.now();
9633
9855
  return true;
@@ -10416,13 +10638,13 @@ function spawnInteractiveProcess(opts) {
10416
10638
  }
10417
10639
 
10418
10640
  // src/prompts.ts
10419
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
10641
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
10420
10642
  import { homedir as homedir8 } from "os";
10421
10643
  import { randomUUID as randomUUID6 } from "crypto";
10422
10644
  import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
10423
- function readPromptFileIfPresent(path11) {
10645
+ function readPromptFileIfPresent(path12) {
10424
10646
  try {
10425
- const content = readFileSync7(path11, "utf8").trim();
10647
+ const content = readFileSync6(path12, "utf8").trim();
10426
10648
  return content || void 0;
10427
10649
  } catch {
10428
10650
  return void 0;
@@ -10527,9 +10749,9 @@ function extractSystemMessages(prompt) {
10527
10749
  }
10528
10750
  var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
10529
10751
  function userAuthoredInstructions(forwarded, opts = {}) {
10530
- const readFile2 = opts.readFile ?? ((path11) => {
10752
+ const readFile2 = opts.readFile ?? ((path12) => {
10531
10753
  try {
10532
- return readFileSync7(path11, "utf8");
10754
+ return readFileSync6(path12, "utf8");
10533
10755
  } catch {
10534
10756
  return null;
10535
10757
  }
@@ -10541,16 +10763,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
10541
10763
  }
10542
10764
  const seen = /* @__PURE__ */ new Set();
10543
10765
  for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
10544
- const path11 = match[1].trim();
10545
- if (seen.has(path11) || !isAbsolute3(path11)) continue;
10546
- if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path11))) continue;
10547
- seen.add(path11);
10548
- const file = readFile2(path11);
10766
+ const path12 = match[1].trim();
10767
+ if (seen.has(path12) || !isAbsolute3(path12)) continue;
10768
+ if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path12))) continue;
10769
+ seen.add(path12);
10770
+ const file = readFile2(path12);
10549
10771
  if (!file?.trim()) continue;
10550
10772
  const start = match.index + match[0].length + 1;
10551
10773
  const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
10552
10774
  if (content === null) continue;
10553
- out.push({ text: `Instructions from: ${path11}
10775
+ out.push({ text: `Instructions from: ${path12}
10554
10776
  ${content}`, content });
10555
10777
  }
10556
10778
  return out;
@@ -10587,10 +10809,10 @@ ${options.compressionSummary.trim()}`
10587
10809
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
10588
10810
  const content = parts.join("\n\n");
10589
10811
  if (!content) return void 0;
10590
- const path11 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
10812
+ const path12 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
10591
10813
  try {
10592
- writeFileSync6(path11, content, { encoding: "utf8", mode: 384 });
10593
- return path11;
10814
+ writeFileSync6(path12, content, { encoding: "utf8", mode: 384 });
10815
+ return path12;
10594
10816
  } catch (err) {
10595
10817
  log.warn("failed to write system prompt file", { error: String(err) });
10596
10818
  return void 0;
@@ -14908,7 +15130,7 @@ var ClaudeCodeLanguageModel = class {
14908
15130
  import {
14909
15131
  existsSync as existsSync5,
14910
15132
  mkdirSync as mkdirSync5,
14911
- readFileSync as readFileSync8,
15133
+ readFileSync as readFileSync7,
14912
15134
  realpathSync as realpathSync4,
14913
15135
  rmSync as rmSync3,
14914
15136
  writeFileSync as writeFileSync7
@@ -14935,7 +15157,7 @@ function userIntendsToUseUnscoped() {
14935
15157
  const cfg = userOpencodeJsonPath();
14936
15158
  if (!existsSync5(cfg)) return false;
14937
15159
  try {
14938
- const json = JSON.parse(readFileSync8(cfg, "utf8"));
15160
+ const json = JSON.parse(readFileSync7(cfg, "utf8"));
14939
15161
  const plugins = json.plugin;
14940
15162
  if (!Array.isArray(plugins)) return false;
14941
15163
  return plugins.some(
@@ -14956,7 +15178,7 @@ function ourLoadedDir() {
14956
15178
  function ourVersion(dir) {
14957
15179
  if (!dir) return null;
14958
15180
  try {
14959
- const pkg = JSON.parse(readFileSync8(join13(dir, "package.json"), "utf8"));
15181
+ const pkg = JSON.parse(readFileSync7(join13(dir, "package.json"), "utf8"));
14960
15182
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
14961
15183
  } catch {
14962
15184
  return null;
@@ -14968,7 +15190,7 @@ function markerPath() {
14968
15190
  }
14969
15191
  function markerVersion() {
14970
15192
  try {
14971
- const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
15193
+ const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
14972
15194
  return typeof marker.version === "string" ? marker.version : null;
14973
15195
  } catch {
14974
15196
  return null;
@@ -15025,7 +15247,7 @@ function cleanupOne(cacheRoot, ourDir) {
15025
15247
  if (!existsSync5(pkgJsonPath)) return;
15026
15248
  let pkg = {};
15027
15249
  try {
15028
- pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
15250
+ pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
15029
15251
  } catch {
15030
15252
  return;
15031
15253
  }
@@ -15044,7 +15266,7 @@ function cleanupOne(cacheRoot, ourDir) {
15044
15266
  const cachePkgJson = join13(cacheRoot, "package.json");
15045
15267
  if (!existsSync5(cachePkgJson)) return;
15046
15268
  try {
15047
- const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
15269
+ const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
15048
15270
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
15049
15271
  delete cfg.dependencies[STALE_PACKAGE_NAME];
15050
15272
  writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -15124,7 +15346,7 @@ function registerOpencodeLogSink(client) {
15124
15346
  }
15125
15347
 
15126
15348
  // src/v2-client.ts
15127
- import path10 from "path";
15349
+ import path11 from "path";
15128
15350
  function formatAgentTypeList(agents) {
15129
15351
  const lines = agents.flatMap((agent) => {
15130
15352
  if (agent.hidden === true || agent.mode === "primary") return [];
@@ -15153,7 +15375,7 @@ function toV1Session(info) {
15153
15375
  if (!info || typeof info !== "object") return void 0;
15154
15376
  const session = info;
15155
15377
  const root = session.location?.directory;
15156
- const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path10.resolve(root, session.subpath) : root : void 0;
15378
+ const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path11.resolve(root, session.subpath) : root : void 0;
15157
15379
  return { ...info, directory, parentID: session.parentID };
15158
15380
  }
15159
15381
  function toV1Messages(messages) {