@khalilgharbaoui/opencode-claude-code-plugin 0.8.2 → 0.9.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
@@ -1282,6 +1282,9 @@ function getActiveProcess(key) {
1282
1282
  if (ap) touch(key);
1283
1283
  return ap;
1284
1284
  }
1285
+ function setActiveProcess(key, ap) {
1286
+ activeProcesses.set(key, ap);
1287
+ }
1285
1288
  function deleteActiveProcess(key) {
1286
1289
  const ap = activeProcesses.get(key);
1287
1290
  if (ap) {
@@ -1425,12 +1428,561 @@ function sessionKey(cwd, modelId) {
1425
1428
  return `${cwd}::${modelId}`;
1426
1429
  }
1427
1430
 
1428
- // src/proxy-mcp.ts
1429
- import { createServer } from "http";
1431
+ // src/claude-session-wrapper.ts
1432
+ import { EventEmitter as EventEmitter2 } from "events";
1433
+ import { unlink as unlink2 } from "fs/promises";
1434
+
1435
+ // src/claude-session-bun.ts
1436
+ import * as os3 from "os";
1430
1437
  import * as fs3 from "fs";
1431
1438
  import * as path3 from "path";
1439
+ import { execFileSync } from "child_process";
1440
+ import { randomUUID } from "crypto";
1441
+ function resolveClaude(cmd = "claude") {
1442
+ if (path3.isAbsolute(cmd) && fs3.existsSync(cmd)) return cmd;
1443
+ const viaBun = Bun.which(cmd);
1444
+ if (viaBun) return viaBun;
1445
+ const isWin = os3.platform() === "win32";
1446
+ try {
1447
+ const out = execFileSync(isWin ? "where" : "which", [cmd], {
1448
+ encoding: "utf8",
1449
+ stdio: ["ignore", "pipe", "ignore"]
1450
+ });
1451
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs3.existsSync(p));
1452
+ if (first) return first;
1453
+ } catch {
1454
+ }
1455
+ throw new Error(`Could not resolve command on PATH: ${cmd}`);
1456
+ }
1457
+ function encodeCwd(cwd) {
1458
+ return path3.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
1459
+ }
1460
+ var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
1461
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
1462
+ function resolveConfigDir(configDir) {
1463
+ const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
1464
+ if (!value) return path3.join(os3.homedir(), ".claude");
1465
+ if (value === "~") return os3.homedir();
1466
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
1467
+ return path3.join(os3.homedir(), value.slice(2));
1468
+ }
1469
+ return path3.resolve(value);
1470
+ }
1471
+ var ClaudeSession = class {
1472
+ sessionId;
1473
+ cwd;
1474
+ configDir;
1475
+ jsonlPath;
1476
+ raw = "";
1477
+ proc = null;
1478
+ cursor = 0;
1479
+ // index into transcript split('\n')
1480
+ lastDataAt = 0;
1481
+ exited = false;
1482
+ exitCode = null;
1483
+ aborted = false;
1484
+ signal;
1485
+ o;
1486
+ constructor(opts = {}) {
1487
+ this.cwd = path3.resolve(opts.cwd ?? process.cwd());
1488
+ this.configDir = resolveConfigDir(opts.configDir);
1489
+ this.signal = opts.signal;
1490
+ this.sessionId = randomUUID();
1491
+ this.jsonlPath = path3.join(
1492
+ this.configDir,
1493
+ "projects",
1494
+ encodeCwd(this.cwd),
1495
+ `${this.sessionId}.jsonl`
1496
+ );
1497
+ this.o = {
1498
+ cwd: this.cwd,
1499
+ cliPath: opts.cliPath,
1500
+ configDir: this.configDir,
1501
+ model: opts.model,
1502
+ settingSources: opts.settingSources,
1503
+ extraArgs: opts.extraArgs ?? [],
1504
+ cols: opts.cols ?? 200,
1505
+ rows: opts.rows ?? 50,
1506
+ bootMinMs: opts.bootMinMs ?? 3e3,
1507
+ bootQuietMs: opts.bootQuietMs ?? 1500,
1508
+ bootMaxMs: opts.bootMaxMs ?? 25e3,
1509
+ pollMs: opts.pollMs ?? 250,
1510
+ // Agentic turns (tool loops) routinely run for many minutes; a short
1511
+ // cap would surface as a mid-task error result. 30 min mirrors the
1512
+ // proxy-tool ceiling rather than a chat-reply expectation.
1513
+ turnTimeoutMs: opts.turnTimeoutMs ?? 18e5,
1514
+ bracketedPaste: opts.bracketedPaste ?? true,
1515
+ submitMinMs: opts.submitMinMs ?? 200,
1516
+ submitConfirmMs: opts.submitConfirmMs ?? 1500,
1517
+ submitMaxRetries: opts.submitMaxRetries ?? 8,
1518
+ debug: opts.debug ?? false
1519
+ };
1520
+ }
1521
+ async start() {
1522
+ if (this.signal?.aborted) throw new Error("aborted before start");
1523
+ this.signal?.addEventListener(
1524
+ "abort",
1525
+ () => {
1526
+ this.aborted = true;
1527
+ this.dispose();
1528
+ },
1529
+ { once: true }
1530
+ );
1531
+ const claude = resolveClaude(this.o.cliPath ?? "claude");
1532
+ const args = ["--session-id", this.sessionId];
1533
+ if (this.o.model) args.push("--model", this.o.model);
1534
+ if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
1535
+ args.push("--setting-sources", this.o.settingSources);
1536
+ }
1537
+ if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
1538
+ if (this.o.debug)
1539
+ process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
1540
+ `);
1541
+ this.lastDataAt = Date.now();
1542
+ this.proc = Bun.spawn([claude, ...args], {
1543
+ cwd: this.cwd,
1544
+ env: {
1545
+ ...process.env,
1546
+ CLAUDE_CONFIG_DIR: this.o.configDir,
1547
+ TERM: "xterm-256color"
1548
+ },
1549
+ terminal: {
1550
+ cols: this.o.cols,
1551
+ rows: this.o.rows,
1552
+ data: (_term, d) => {
1553
+ this.lastDataAt = Date.now();
1554
+ const chunk = Buffer.from(d).toString("utf8");
1555
+ this.raw += chunk;
1556
+ if (this.o.debug) process.stdout.write(chunk);
1557
+ }
1558
+ }
1559
+ });
1560
+ this.proc.exited.then((code) => {
1561
+ this.exitCode = typeof code === "number" ? code : null;
1562
+ this.exited = true;
1563
+ this.proc = null;
1564
+ }).catch(() => {
1565
+ this.exited = true;
1566
+ this.proc = null;
1567
+ });
1568
+ await this.waitForBoot();
1569
+ this.cursor = this.lineCount();
1570
+ }
1571
+ /** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
1572
+ * bootMinMs..bootMaxMs. */
1573
+ async waitForBoot() {
1574
+ const start = Date.now();
1575
+ while (Date.now() - start < this.o.bootMaxMs) {
1576
+ await delay(150);
1577
+ if (this.aborted) throw new Error("aborted during boot");
1578
+ if (this.exited) {
1579
+ throw new Error(this.failureMessage("claude exited during boot", true));
1580
+ }
1581
+ const elapsed = Date.now() - start;
1582
+ const sinceData = Date.now() - this.lastDataAt;
1583
+ if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) return;
1584
+ }
1585
+ }
1586
+ /** Submit the freshly-injected prompt and confirm the turn was actually
1587
+ * accepted. A large bracketed paste collapses into a "[Pasted text]"
1588
+ * placeholder; an Enter sent while claude is still ingesting the paste is
1589
+ * silently dropped, so a single fixed-delay Enter races the paste and can
1590
+ * leave the prompt sitting unsubmitted (→ hang until turnTimeoutMs). Send
1591
+ * Enter, then poll for transcript growth past the cursor (the turn's records
1592
+ * are written on acceptance); resend Enter until accepted or the retry
1593
+ * budget is spent. Polling growth (not a blind delay) also stops us from
1594
+ * sending a stray Enter once the turn is in flight. */
1595
+ async submitTurn() {
1596
+ await delay(this.o.submitMinMs);
1597
+ for (let attempt = 0; attempt < this.o.submitMaxRetries; attempt++) {
1598
+ if (this.aborted || this.exited || !this.proc) return;
1599
+ this.proc.terminal.write("\r");
1600
+ const until = Date.now() + this.o.submitConfirmMs;
1601
+ while (Date.now() < until) {
1602
+ await delay(80);
1603
+ if (this.aborted || this.exited) return;
1604
+ if (this.lineCount() > this.cursor) return;
1605
+ }
1606
+ }
1607
+ }
1608
+ readRawLines() {
1609
+ try {
1610
+ return fs3.readFileSync(this.jsonlPath, "utf8").split("\n");
1611
+ } catch {
1612
+ return [];
1613
+ }
1614
+ }
1615
+ /** Count of complete lines (split('\n') minus the trailing/partial element). */
1616
+ lineCount() {
1617
+ const lines = this.readRawLines();
1618
+ return lines.length > 0 ? lines.length - 1 : 0;
1619
+ }
1620
+ rawTail(max = 600) {
1621
+ const clean = this.raw.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\s+/g, " ").trim();
1622
+ return clean.length > max ? clean.slice(-max) : clean;
1623
+ }
1624
+ failureMessage(reason, includeRaw = false) {
1625
+ const parts = [
1626
+ `${reason} (sessionId=${this.sessionId}, jsonlPath=${this.jsonlPath}, exitCode=${this.exitCode ?? "unknown"})`
1627
+ ];
1628
+ if (includeRaw) {
1629
+ const tail = this.rawTail();
1630
+ if (tail) parts.push(`terminalTail=${JSON.stringify(tail)}`);
1631
+ }
1632
+ return parts.join("; ");
1633
+ }
1634
+ /**
1635
+ * Inject a turn into the live session and return the assistant reply once a
1636
+ * terminal stop_reason is observed in the transcript.
1637
+ */
1638
+ async ask(prompt, perTurnTimeoutMs) {
1639
+ if (this.aborted) throw new Error("aborted");
1640
+ if (!this.proc || this.exited)
1641
+ throw new Error("session not started or already exited");
1642
+ const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
1643
+ const t0 = Date.now();
1644
+ if (this.o.bracketedPaste) {
1645
+ this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
1646
+ } else {
1647
+ this.proc.terminal.write(prompt);
1648
+ }
1649
+ await this.submitTurn();
1650
+ const collected = [];
1651
+ let lastUsage = null;
1652
+ let stopReason = null;
1653
+ const deadline = Date.now() + timeout;
1654
+ while (Date.now() < deadline) {
1655
+ await delay(this.o.pollMs);
1656
+ if (this.aborted) throw new Error("aborted mid-turn");
1657
+ const lines = this.readRawLines();
1658
+ const lastComplete = lines.length - 1;
1659
+ if (lastComplete <= this.cursor) {
1660
+ if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
1661
+ continue;
1662
+ }
1663
+ for (let i = this.cursor; i < lastComplete; i++) {
1664
+ const s = lines[i];
1665
+ if (!s || !s.trim()) continue;
1666
+ let rec;
1667
+ try {
1668
+ rec = JSON.parse(s);
1669
+ } catch {
1670
+ continue;
1671
+ }
1672
+ if (rec.type === "assistant" && rec.message) {
1673
+ for (const b of rec.message.content ?? []) {
1674
+ if (b?.type === "text" && typeof b.text === "string")
1675
+ collected.push(b.text);
1676
+ }
1677
+ if (rec.message.usage) lastUsage = rec.message.usage;
1678
+ if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
1679
+ stopReason = rec.message.stop_reason;
1680
+ }
1681
+ }
1682
+ }
1683
+ this.cursor = lastComplete;
1684
+ if (stopReason) break;
1685
+ }
1686
+ if (!stopReason) {
1687
+ throw new Error(
1688
+ this.failureMessage(
1689
+ `turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected.length} text block(s))`
1690
+ )
1691
+ );
1692
+ }
1693
+ const u = lastUsage ?? {};
1694
+ return {
1695
+ text: collected.join("\n").trim(),
1696
+ stopReason,
1697
+ usage: lastUsage,
1698
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
1699
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
1700
+ ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
1701
+ ephemeral5mTokens: u.cache_creation?.ephemeral_5m_input_tokens ?? 0,
1702
+ inputTokens: u.input_tokens ?? 0,
1703
+ outputTokens: u.output_tokens ?? 0,
1704
+ elapsedMs: Date.now() - t0
1705
+ };
1706
+ }
1707
+ /**
1708
+ * Like ask(), but instead of collecting the reply text it re-emits each NEW
1709
+ * raw JSONL transcript line via onLine (verbatim) until a terminal
1710
+ * stop_reason. Returns the terminal stop_reason + the last assistant usage.
1711
+ * Used by the opencode plugin transport shim, which feeds these raw lines
1712
+ * into the existing stream-json line handler unchanged.
1713
+ */
1714
+ async tailTurn(prompt, onLine, perTurnTimeoutMs) {
1715
+ if (this.aborted) throw new Error("aborted");
1716
+ if (!this.proc || this.exited)
1717
+ throw new Error("session not started or already exited");
1718
+ const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
1719
+ if (this.o.bracketedPaste) {
1720
+ this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
1721
+ } else {
1722
+ this.proc.terminal.write(prompt);
1723
+ }
1724
+ await this.submitTurn();
1725
+ let lastUsage = null;
1726
+ let totalOutput = 0;
1727
+ let stopReason = null;
1728
+ const deadline = Date.now() + timeout;
1729
+ while (Date.now() < deadline) {
1730
+ await delay(this.o.pollMs);
1731
+ if (this.aborted) throw new Error("aborted mid-turn");
1732
+ const lines = this.readRawLines();
1733
+ const lastComplete = lines.length - 1;
1734
+ if (lastComplete <= this.cursor) {
1735
+ if (this.exited) {
1736
+ throw new Error(this.failureMessage("claude exited mid-turn", true));
1737
+ }
1738
+ continue;
1739
+ }
1740
+ for (let i = this.cursor; i < lastComplete; i++) {
1741
+ const s = lines[i];
1742
+ if (!s || !s.trim()) continue;
1743
+ onLine(s);
1744
+ let rec;
1745
+ try {
1746
+ rec = JSON.parse(s);
1747
+ } catch {
1748
+ continue;
1749
+ }
1750
+ if (rec.type === "assistant" && rec.message) {
1751
+ if (rec.message.usage) {
1752
+ lastUsage = rec.message.usage;
1753
+ totalOutput += rec.message.usage.output_tokens ?? 0;
1754
+ }
1755
+ if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
1756
+ stopReason = rec.message.stop_reason;
1757
+ }
1758
+ }
1759
+ }
1760
+ this.cursor = lastComplete;
1761
+ if (stopReason) break;
1762
+ }
1763
+ let usage = lastUsage;
1764
+ if (lastUsage) {
1765
+ usage = { ...lastUsage, output_tokens: totalOutput };
1766
+ if (Array.isArray(lastUsage.iterations) && lastUsage.iterations.length > 0) {
1767
+ const iters = lastUsage.iterations.map((it) => ({ ...it }));
1768
+ iters[iters.length - 1] = {
1769
+ ...iters[iters.length - 1],
1770
+ output_tokens: totalOutput
1771
+ };
1772
+ usage.iterations = iters;
1773
+ }
1774
+ }
1775
+ if (!stopReason) {
1776
+ throw new Error(
1777
+ this.failureMessage(
1778
+ `turn timed out after ${timeout}ms (no terminal assistant record)`
1779
+ )
1780
+ );
1781
+ }
1782
+ return { stopReason, usage };
1783
+ }
1784
+ dispose() {
1785
+ if (this.proc) {
1786
+ try {
1787
+ this.proc.terminal.write("");
1788
+ } catch {
1789
+ }
1790
+ try {
1791
+ this.proc.kill();
1792
+ } catch {
1793
+ }
1794
+ try {
1795
+ this.proc.terminal.close();
1796
+ } catch {
1797
+ }
1798
+ }
1799
+ this.proc = null;
1800
+ }
1801
+ };
1802
+
1803
+ // src/claude-session-wrapper.ts
1804
+ function decodeUserEnvelope(chunk) {
1805
+ let parsed;
1806
+ try {
1807
+ parsed = JSON.parse(chunk);
1808
+ } catch {
1809
+ return chunk;
1810
+ }
1811
+ if (!parsed || parsed.type !== "user" || !parsed.message) return chunk;
1812
+ const content = parsed.message.content;
1813
+ if (typeof content === "string") return content;
1814
+ if (!Array.isArray(content)) return chunk;
1815
+ const parts = [];
1816
+ let dropped = 0;
1817
+ for (const block of content) {
1818
+ if (block?.type === "text" && typeof block.text === "string") {
1819
+ parts.push(block.text);
1820
+ } else if (block?.type === "tool_result") {
1821
+ const v = block.content;
1822
+ const text = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
1823
+ parts.push(
1824
+ `[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
1825
+ ${text}`
1826
+ );
1827
+ } else {
1828
+ dropped++;
1829
+ }
1830
+ }
1831
+ if (dropped > 0) {
1832
+ log.warn("interactive transport dropped non-text content blocks", {
1833
+ dropped
1834
+ });
1835
+ }
1836
+ return parts.join("\n\n");
1837
+ }
1838
+ function spawnInteractiveProcess(opts) {
1839
+ const extraArgs = [];
1840
+ if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
1841
+ extraArgs.push(
1842
+ "--mcp-config",
1843
+ ...opts.mcpConfigPaths,
1844
+ "--strict-mcp-config"
1845
+ );
1846
+ }
1847
+ if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
1848
+ extraArgs.push(
1849
+ "--settings",
1850
+ JSON.stringify({ permissions: { allow: opts.permissionsAllow } })
1851
+ );
1852
+ }
1853
+ if (opts.permissionMode === "bypassPermissions") {
1854
+ log.warn(
1855
+ "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
1856
+ );
1857
+ } else if (opts.permissionMode) {
1858
+ extraArgs.push("--permission-mode", opts.permissionMode);
1859
+ }
1860
+ if (opts.systemPromptFile) {
1861
+ extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
1862
+ }
1863
+ const session = new ClaudeSession({
1864
+ cwd: opts.cwd,
1865
+ cliPath: opts.cliPath,
1866
+ configDir: opts.configDir,
1867
+ model: opts.model,
1868
+ // Default null = normal CLAUDE.md + settings load, matching what the
1869
+ // headless spawn does. "" (skip everything) is for fast e2e runs only.
1870
+ settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
1871
+ extraArgs
1872
+ });
1873
+ log.info("prepared interactive claude session", {
1874
+ cwd: opts.cwd,
1875
+ cliPath: opts.cliPath ?? "claude",
1876
+ configDir: session.configDir,
1877
+ model: opts.model,
1878
+ sessionId: session.sessionId,
1879
+ jsonlPath: session.jsonlPath
1880
+ });
1881
+ const lineEmitter = new EventEmitter2();
1882
+ const errorHandlers = /* @__PURE__ */ new Set();
1883
+ let startPromise = null;
1884
+ const ensureStarted = () => {
1885
+ if (!startPromise) startPromise = session.start();
1886
+ return startPromise;
1887
+ };
1888
+ const emitResult = (subtype, isError, result, usage) => {
1889
+ lineEmitter.emit(
1890
+ "line",
1891
+ JSON.stringify({
1892
+ type: "result",
1893
+ subtype,
1894
+ is_error: isError,
1895
+ result,
1896
+ session_id: session.sessionId,
1897
+ usage: usage ?? {},
1898
+ total_cost_usd: null,
1899
+ duration_ms: 0
1900
+ })
1901
+ );
1902
+ };
1903
+ const runTurn = (userMsg) => {
1904
+ void (async () => {
1905
+ try {
1906
+ await ensureStarted();
1907
+ const { stopReason, usage } = await session.tailTurn(userMsg, (raw) => {
1908
+ lineEmitter.emit("line", raw);
1909
+ });
1910
+ const timedOut = !stopReason;
1911
+ emitResult(
1912
+ timedOut ? "error_during_execution" : stopReason,
1913
+ timedOut,
1914
+ timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
1915
+ usage
1916
+ );
1917
+ } catch (err) {
1918
+ const e = err instanceof Error ? err : new Error(String(err));
1919
+ log.error("interactive turn failed", { error: e.message });
1920
+ emitResult(
1921
+ "error_during_execution",
1922
+ true,
1923
+ `Interactive transport failed: ${e.message}`
1924
+ );
1925
+ if (errorHandlers.size > 0) {
1926
+ for (const h of errorHandlers) h(e);
1927
+ } else {
1928
+ lineEmitter.emit("close");
1929
+ }
1930
+ }
1931
+ })();
1932
+ };
1933
+ const proc = {
1934
+ stdin: {
1935
+ write(chunk) {
1936
+ const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
1937
+ runTurn(decodeUserEnvelope(raw));
1938
+ return true;
1939
+ },
1940
+ end() {
1941
+ }
1942
+ },
1943
+ stdout: null,
1944
+ stderr: null,
1945
+ pid: -1,
1946
+ killed: false,
1947
+ on(event, fn) {
1948
+ if (event === "error") errorHandlers.add(fn);
1949
+ return proc;
1950
+ },
1951
+ once() {
1952
+ return proc;
1953
+ },
1954
+ off(event, fn) {
1955
+ if (event === "error") errorHandlers.delete(fn);
1956
+ return proc;
1957
+ },
1958
+ kill() {
1959
+ try {
1960
+ session.dispose();
1961
+ } catch {
1962
+ }
1963
+ if (opts.systemPromptFile) {
1964
+ void unlink2(opts.systemPromptFile).catch(() => {
1965
+ });
1966
+ }
1967
+ proc.killed = true;
1968
+ return true;
1969
+ }
1970
+ };
1971
+ return {
1972
+ proc,
1973
+ lineEmitter,
1974
+ proxyServer: null,
1975
+ mcpHash: void 0,
1976
+ systemPromptFile: opts.systemPromptFile
1977
+ };
1978
+ }
1979
+
1980
+ // src/proxy-mcp.ts
1981
+ import { createServer } from "http";
1982
+ import * as fs4 from "fs";
1983
+ import * as path4 from "path";
1432
1984
  import * as crypto2 from "crypto";
1433
- import { EventEmitter as EventEmitter2 } from "events";
1985
+ import { EventEmitter as EventEmitter3 } from "events";
1434
1986
  var PROTOCOL_VERSION = "2024-11-05";
1435
1987
  var SERVER_NAME = "opencode_proxy";
1436
1988
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
@@ -1557,7 +2109,7 @@ var DEFAULT_PROXY_TOOLS = [
1557
2109
  }
1558
2110
  ];
1559
2111
  async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1560
- const calls = new EventEmitter2();
2112
+ const calls = new EventEmitter3();
1561
2113
  const pending = /* @__PURE__ */ new Map();
1562
2114
  const server2 = createServer(async (req, res) => {
1563
2115
  if (req.method !== "POST" || !req.url?.startsWith("/mcp")) {
@@ -1637,12 +2189,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1637
2189
  });
1638
2190
  let timer = null;
1639
2191
  const result = await new Promise(
1640
- (resolve3, reject) => {
2192
+ (resolve4, reject) => {
1641
2193
  const entry = {
1642
2194
  id: callId,
1643
2195
  toolName,
1644
2196
  input,
1645
- resolve: resolve3,
2197
+ resolve: resolve4,
1646
2198
  reject
1647
2199
  };
1648
2200
  pending.set(callId, entry);
@@ -1717,11 +2269,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1717
2269
  }
1718
2270
  }
1719
2271
  });
1720
- await new Promise((resolve3, reject) => {
2272
+ await new Promise((resolve4, reject) => {
1721
2273
  server2.once("error", reject);
1722
2274
  server2.listen(0, "127.0.0.1", () => {
1723
2275
  server2.off("error", reject);
1724
- resolve3();
2276
+ resolve4();
1725
2277
  });
1726
2278
  });
1727
2279
  const addr = server2.address();
@@ -1755,11 +2307,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1755
2307
  2
1756
2308
  );
1757
2309
  const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
1758
- const outPath = path3.join(
2310
+ const outPath = path4.join(
1759
2311
  pluginTmpDir(),
1760
2312
  `proxy-${hash}.json`
1761
2313
  );
1762
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
2314
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
1763
2315
  configFilePath = outPath;
1764
2316
  return outPath;
1765
2317
  },
@@ -1768,12 +2320,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1768
2320
  entry.reject(new Error("proxy MCP server closed"));
1769
2321
  }
1770
2322
  pending.clear();
1771
- await new Promise((resolve3) => {
1772
- server2.close(() => resolve3());
2323
+ await new Promise((resolve4) => {
2324
+ server2.close(() => resolve4());
1773
2325
  });
1774
2326
  if (configFilePath) {
1775
2327
  try {
1776
- fs3.unlinkSync(configFilePath);
2328
+ fs4.unlinkSync(configFilePath);
1777
2329
  } catch {
1778
2330
  }
1779
2331
  configFilePath = null;
@@ -1807,10 +2359,10 @@ function disallowedToolFlags(tools) {
1807
2359
  return out;
1808
2360
  }
1809
2361
  function readBody(req) {
1810
- return new Promise((resolve3, reject) => {
2362
+ return new Promise((resolve4, reject) => {
1811
2363
  const chunks = [];
1812
2364
  req.on("data", (chunk) => chunks.push(chunk));
1813
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
2365
+ req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
1814
2366
  req.on("error", reject);
1815
2367
  });
1816
2368
  }
@@ -1823,10 +2375,10 @@ function writeJson(res, body) {
1823
2375
  }
1824
2376
 
1825
2377
  // src/proxy-broker.ts
1826
- import { EventEmitter as EventEmitter3 } from "events";
2378
+ import { EventEmitter as EventEmitter4 } from "events";
1827
2379
  var pendingByCallId = /* @__PURE__ */ new Map();
1828
2380
  var callIdsBySession = /* @__PURE__ */ new Map();
1829
- var emitter = new EventEmitter3();
2381
+ var emitter = new EventEmitter4();
1830
2382
  var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
1831
2383
  function eventName(sessionKey2) {
1832
2384
  return `pending:${sessionKey2}`;
@@ -1948,11 +2500,11 @@ function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
1948
2500
  }
1949
2501
 
1950
2502
  // src/claude-code-language-model.ts
1951
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1952
- import { unlink as unlink2 } from "fs/promises";
1953
- import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
1954
- import { randomUUID as randomUUID2 } from "crypto";
1955
- import { dirname as dirname3, join as join5 } from "path";
2503
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2504
+ import { unlink as unlink3 } from "fs/promises";
2505
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
2506
+ import { randomUUID as randomUUID3 } from "crypto";
2507
+ import { dirname as dirname3, join as join6 } from "path";
1956
2508
  var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
1957
2509
  function resolveCompactionModel(configured) {
1958
2510
  const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
@@ -2141,9 +2693,9 @@ function makeAutoContinueMessage() {
2141
2693
  }
2142
2694
  });
2143
2695
  }
2144
- function readPromptFileIfPresent(path5) {
2696
+ function readPromptFileIfPresent(path6) {
2145
2697
  try {
2146
- const content = readFileSync2(path5, "utf8").trim();
2698
+ const content = readFileSync3(path6, "utf8").trim();
2147
2699
  return content || void 0;
2148
2700
  } catch {
2149
2701
  return void 0;
@@ -2152,7 +2704,7 @@ function readPromptFileIfPresent(path5) {
2152
2704
  function nearestWorkspaceAgentsPrompt(cwd) {
2153
2705
  let dir = cwd;
2154
2706
  while (true) {
2155
- const content = readPromptFileIfPresent(join5(dir, "AGENTS.md"));
2707
+ const content = readPromptFileIfPresent(join6(dir, "AGENTS.md"));
2156
2708
  if (content) return content;
2157
2709
  const parent = dirname3(dir);
2158
2710
  if (parent === dir) return void 0;
@@ -2203,8 +2755,8 @@ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystem
2203
2755
  for (const s of extraSystemContent) {
2204
2756
  if (s.trim()) parts.push(s.trim());
2205
2757
  }
2206
- const configRoot = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
2207
- const globalAgents = readPromptFileIfPresent(join5(configRoot, "opencode", "AGENTS.md"));
2758
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
2759
+ const globalAgents = readPromptFileIfPresent(join6(configRoot, "opencode", "AGENTS.md"));
2208
2760
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
2209
2761
  if (globalAgents) parts.push(globalAgents);
2210
2762
  if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
@@ -2212,10 +2764,10 @@ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystem
2212
2764
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
2213
2765
  const content = parts.join("\n\n");
2214
2766
  if (!content) return void 0;
2215
- const path5 = join5(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
2767
+ const path6 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
2216
2768
  try {
2217
- writeFileSync3(path5, content, "utf8");
2218
- return path5;
2769
+ writeFileSync3(path6, content, "utf8");
2770
+ return path6;
2219
2771
  } catch (err) {
2220
2772
  log.warn("failed to write system prompt file", { error: String(err) });
2221
2773
  return void 0;
@@ -2760,7 +3312,7 @@ var ClaudeCodeLanguageModel = class {
2760
3312
  });
2761
3313
  if (systemPromptFile) {
2762
3314
  proc.on("exit", () => {
2763
- void unlink2(systemPromptFile).catch(() => {
3315
+ void unlink3(systemPromptFile).catch(() => {
2764
3316
  });
2765
3317
  });
2766
3318
  }
@@ -2771,7 +3323,7 @@ var ClaudeCodeLanguageModel = class {
2771
3323
  const toolCalls = [];
2772
3324
  const toolCallStreams = /* @__PURE__ */ new Map();
2773
3325
  let gotPartialEvents = false;
2774
- const result = await new Promise((resolve3, reject) => {
3326
+ const result = await new Promise((resolve4, reject) => {
2775
3327
  const cleanup = () => {
2776
3328
  try {
2777
3329
  if (!proc.killed && proc.exitCode === null) proc.kill();
@@ -2879,7 +3431,7 @@ ${plan}
2879
3431
  usage: msg.usage
2880
3432
  };
2881
3433
  cleanup();
2882
- resolve3({
3434
+ resolve4({
2883
3435
  ...resultMeta,
2884
3436
  text: responseText,
2885
3437
  thinking: thinkingText,
@@ -2891,7 +3443,7 @@ ${plan}
2891
3443
  });
2892
3444
  rl.on("close", () => {
2893
3445
  cleanup();
2894
- resolve3({
3446
+ resolve4({
2895
3447
  ...resultMeta,
2896
3448
  text: responseText,
2897
3449
  thinking: thinkingText,
@@ -2996,6 +3548,10 @@ ${plan}
2996
3548
  const toUsage = this.toUsage.bind(this);
2997
3549
  const toFinishReason = this.toFinishReason.bind(this);
2998
3550
  const handleControlRequest = this.handleControlRequest.bind(this);
3551
+ const flagOn = (v) => v !== void 0 && !["", "0", "false", "no", "off"].includes(v.trim().toLowerCase());
3552
+ const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
3553
+ const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
3554
+ const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
2999
3555
  if (scope === "no-tools" && !compactionMode) {
3000
3556
  log.info("doStream no-tools title stub", {
3001
3557
  compactionMode,
@@ -3118,81 +3674,141 @@ ${plan}
3118
3674
  }
3119
3675
  }
3120
3676
  const setup = async () => {
3121
- let cliArgs;
3122
- let spawnSystemPromptFile;
3123
- let spawnProxyServer = null;
3124
- let spawnMcpHash = null;
3125
- if (compactionMode) {
3126
- cliArgs = buildCliArgs({
3127
- sessionKey: sk,
3128
- skipPermissions,
3129
- includeSessionId: false,
3130
- model: effectiveModelId,
3131
- permissionMode: self.config.permissionMode,
3132
- cliVersion
3133
- });
3134
- } else {
3135
- const discovery = self.effectiveMcpConfig(
3136
- cwd,
3137
- void 0,
3138
- runtimeStatus
3139
- );
3140
- const proxyMcpTools = await self.resolvedProxyMcpTools(
3141
- discovery.allEnabledServerNames
3142
- );
3143
- const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
3144
- const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
3145
- if (!proxyServer && combinedProxyTools) {
3146
- proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
3677
+ if (useInteractive && !compactionMode) {
3678
+ const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
3679
+ if (activeProcess) {
3680
+ proc = activeProcess.proc;
3681
+ lineEmitter = activeProcess.lineEmitter;
3682
+ log.debug("reusing active interactive session", { sk });
3683
+ } else {
3684
+ const allow = [
3685
+ ...mcp.allEnabledServerNames.map((n) => `mcp__${n}__*`),
3686
+ "mcp__opencode_proxy__*",
3687
+ ...self.config.interactiveAllowTools ?? [
3688
+ "Bash",
3689
+ "Edit",
3690
+ "Write",
3691
+ "Read",
3692
+ "WebFetch"
3693
+ ]
3694
+ ];
3695
+ const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
3696
+ cwd,
3697
+ self.config.multiStepContinuation !== false
3698
+ // Do not forward opencode's own system prompt into the
3699
+ // interactive TUI. Live subscription-account testing
3700
+ // showed that large forwarded payload can trigger Claude
3701
+ // Code's third-party-app usage gate, while our static
3702
+ // CLI/AGENTS/continuation prompt remains safe.
3703
+ );
3704
+ if (self.config.interactiveSystemPrompt === false) {
3705
+ log.warn(
3706
+ "interactive system prompt disabled; opencode agent prompts will not be appended"
3707
+ );
3708
+ }
3709
+ if (interactiveBypassRequested) {
3710
+ log.warn(
3711
+ "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
3712
+ );
3713
+ }
3714
+ const ap = spawnInteractiveProcess({
3715
+ cwd,
3716
+ cliPath,
3717
+ configDir: self.config.configDir,
3718
+ model: effectiveModelId,
3719
+ mcpConfigPaths: mcp.paths,
3720
+ permissionsAllow: allow,
3721
+ systemPromptFile
3722
+ });
3723
+ ap.mcpHash = mcp.bridgedHash;
3724
+ setActiveProcess(sk, ap);
3725
+ proc = ap.proc;
3726
+ lineEmitter = ap.lineEmitter;
3727
+ activeProcess = ap;
3728
+ log.info("spawned interactive claude session", {
3729
+ sk,
3730
+ cliPath,
3731
+ configDir: self.config.configDir,
3732
+ model: effectiveModelId
3733
+ });
3147
3734
  }
3148
- const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
3149
- const extraDisallowed = [];
3150
- if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
3151
- const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
3152
- const mcp = self.effectiveMcpConfig(
3153
- cwd,
3154
- proxyServer?.configPath(),
3155
- runtimeStatus,
3156
- excludeServers
3157
- );
3158
- const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
3159
- cwd,
3160
- self.config.multiStepContinuation !== false,
3161
- extractSystemMessages(options.prompt)
3162
- );
3163
- cliArgs = buildCliArgs({
3164
- sessionKey: sk,
3165
- skipPermissions,
3166
- model: self.modelId,
3167
- permissionMode: self.config.permissionMode,
3168
- mcpConfig: mcp.paths,
3169
- strictMcpConfig: self.config.strictMcpConfig,
3170
- disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
3171
- appendSystemPromptFile: systemPromptFile,
3172
- ...self.thinkingCliOptions(),
3173
- cliVersion
3174
- });
3175
- spawnSystemPromptFile = systemPromptFile;
3176
- spawnProxyServer = proxyServer;
3177
- spawnMcpHash = mcp.bridgedHash;
3178
- }
3179
- if (activeProcess && !compactionMode) {
3180
- proc = activeProcess.proc;
3181
- lineEmitter = activeProcess.lineEmitter;
3182
- log.debug("reusing active process", { sk });
3183
3735
  } else {
3184
- const ap = spawnClaudeProcess(
3185
- cliPath,
3186
- cliArgs,
3187
- cwd,
3188
- sk,
3189
- spawnProxyServer,
3190
- spawnMcpHash,
3191
- spawnSystemPromptFile
3192
- );
3193
- proc = ap.proc;
3194
- lineEmitter = ap.lineEmitter;
3195
- activeProcess = ap;
3736
+ let cliArgs;
3737
+ let spawnSystemPromptFile;
3738
+ let spawnProxyServer = null;
3739
+ let spawnMcpHash = null;
3740
+ if (compactionMode) {
3741
+ cliArgs = buildCliArgs({
3742
+ sessionKey: sk,
3743
+ skipPermissions,
3744
+ includeSessionId: false,
3745
+ model: effectiveModelId,
3746
+ permissionMode: self.config.permissionMode,
3747
+ cliVersion
3748
+ });
3749
+ } else {
3750
+ const discovery = self.effectiveMcpConfig(
3751
+ cwd,
3752
+ void 0,
3753
+ runtimeStatus
3754
+ );
3755
+ const proxyMcpTools = await self.resolvedProxyMcpTools(
3756
+ discovery.allEnabledServerNames
3757
+ );
3758
+ const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
3759
+ const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
3760
+ if (!proxyServer && combinedProxyTools) {
3761
+ proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
3762
+ }
3763
+ const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
3764
+ const extraDisallowed = [];
3765
+ if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
3766
+ const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
3767
+ const mcp = self.effectiveMcpConfig(
3768
+ cwd,
3769
+ proxyServer?.configPath(),
3770
+ runtimeStatus,
3771
+ excludeServers
3772
+ );
3773
+ const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
3774
+ cwd,
3775
+ self.config.multiStepContinuation !== false,
3776
+ extractSystemMessages(options.prompt)
3777
+ );
3778
+ cliArgs = buildCliArgs({
3779
+ sessionKey: sk,
3780
+ skipPermissions,
3781
+ model: self.modelId,
3782
+ permissionMode: self.config.permissionMode,
3783
+ mcpConfig: mcp.paths,
3784
+ strictMcpConfig: self.config.strictMcpConfig,
3785
+ disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
3786
+ appendSystemPromptFile: systemPromptFile,
3787
+ ...self.thinkingCliOptions(),
3788
+ cliVersion
3789
+ });
3790
+ spawnSystemPromptFile = systemPromptFile;
3791
+ spawnProxyServer = proxyServer;
3792
+ spawnMcpHash = mcp.bridgedHash;
3793
+ }
3794
+ if (activeProcess && !compactionMode) {
3795
+ proc = activeProcess.proc;
3796
+ lineEmitter = activeProcess.lineEmitter;
3797
+ log.debug("reusing active process", { sk });
3798
+ } else {
3799
+ const ap = spawnClaudeProcess(
3800
+ cliPath,
3801
+ cliArgs,
3802
+ cwd,
3803
+ sk,
3804
+ spawnProxyServer,
3805
+ spawnMcpHash,
3806
+ spawnSystemPromptFile
3807
+ );
3808
+ proc = ap.proc;
3809
+ lineEmitter = ap.lineEmitter;
3810
+ activeProcess = ap;
3811
+ }
3196
3812
  }
3197
3813
  controller.enqueue({ type: "stream-start", warnings });
3198
3814
  let currentTextId = null;
@@ -3651,11 +4267,6 @@ ${plan}
3651
4267
  if (block.type === "tool_use" && block.id && block.name) {
3652
4268
  noteToolActivity();
3653
4269
  const parsedInput = block.input ?? {};
3654
- toolCallsById.set(block.id, {
3655
- id: block.id,
3656
- name: block.name,
3657
- input: parsedInput
3658
- });
3659
4270
  if (isAskUserQuestionTool(block.name)) {
3660
4271
  const askId = startTextBlock();
3661
4272
  controller.enqueue({
@@ -3708,6 +4319,11 @@ ${plan}
3708
4319
  toolUseId: block.id
3709
4320
  });
3710
4321
  if (!skip) {
4322
+ toolCallsById.set(block.id, {
4323
+ id: block.id,
4324
+ name: block.name,
4325
+ input: parsedInput
4326
+ });
3711
4327
  if (!executed) skipResultForIds.add(block.id);
3712
4328
  controller.enqueue({
3713
4329
  type: "tool-input-start",
@@ -3999,6 +4615,8 @@ ${plan}
3999
4615
  };
4000
4616
  const procErrorHandler = (err) => {
4001
4617
  log.error("process error", { error: err.message });
4618
+ deleteActiveProcess(sk);
4619
+ deleteClaudeSessionId(sk);
4002
4620
  if (controllerClosed) return;
4003
4621
  if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
4004
4622
  rejectAllPendingProxyCallsForSession(
@@ -4322,7 +4940,7 @@ var defaultModels = {
4322
4940
 
4323
4941
  // src/accounts.ts
4324
4942
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
4325
- import path4 from "path";
4943
+ import path5 from "path";
4326
4944
  var BASE_PROVIDER_ID = "claude-code";
4327
4945
  var DEFAULT_ACCOUNT = "default";
4328
4946
  var SHARED_CAPABILITY_ITEMS = [
@@ -4360,7 +4978,7 @@ function expandHome(value) {
4360
4978
  const home = process.env.HOME ?? process.env.USERPROFILE;
4361
4979
  if (value === "~") return home ?? value;
4362
4980
  if (value.startsWith("~/") || value.startsWith("~\\")) {
4363
- return home ? path4.join(home, value.slice(2)) : value;
4981
+ return home ? path5.join(home, value.slice(2)) : value;
4364
4982
  }
4365
4983
  return value;
4366
4984
  }
@@ -4383,7 +5001,7 @@ async function ensureAccountRuntime(account, baseCliPath) {
4383
5001
  baseCliPath,
4384
5002
  expandedConfigDir
4385
5003
  );
4386
- return { cliPath, configDir };
5004
+ return { cliPath, configDir: expandedConfigDir };
4387
5005
  }
4388
5006
  async function ensureSharedCapabilities(targetRoot) {
4389
5007
  const sourceRoot = expandHome("~/.claude");
@@ -4392,8 +5010,8 @@ async function ensureSharedCapabilities(targetRoot) {
4392
5010
  }
4393
5011
  }
4394
5012
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
4395
- const source = path4.join(sourceRoot, item);
4396
- const target = path4.join(targetRoot, item);
5013
+ const source = path5.join(sourceRoot, item);
5014
+ const target = path5.join(targetRoot, item);
4397
5015
  let sourceStat;
4398
5016
  try {
4399
5017
  sourceStat = await lstat(source);
@@ -4404,8 +5022,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
4404
5022
  const targetStat = await lstat(target);
4405
5023
  if (targetStat.isSymbolicLink()) {
4406
5024
  const current = await readlink(target);
4407
- const resolvedCurrent = path4.resolve(path4.dirname(target), current);
4408
- const resolvedSource = path4.resolve(source);
5025
+ const resolvedCurrent = path5.resolve(path5.dirname(target), current);
5026
+ const resolvedSource = path5.resolve(source);
4409
5027
  if (resolvedCurrent === resolvedSource) return;
4410
5028
  }
4411
5029
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -4420,11 +5038,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
4420
5038
  await symlink(source, target, type);
4421
5039
  }
4422
5040
  async function writeAccountWrapper(account, baseCliPath, configDir) {
4423
- const cacheRoot = path4.join(
5041
+ const cacheRoot = path5.join(
4424
5042
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
4425
5043
  "opencode-claude-code-plugin"
4426
5044
  );
4427
- const wrapperPath = path4.join(cacheRoot, `claude-${account}`);
5045
+ const wrapperPath = path5.join(cacheRoot, `claude-${account}`);
4428
5046
  const suffix = `@${account}`;
4429
5047
  await mkdir(cacheRoot, { recursive: true });
4430
5048
  const script = `#!/usr/bin/env bash
@@ -4464,14 +5082,14 @@ function titleizeAccount(account) {
4464
5082
 
4465
5083
  // src/cleanup-stale.ts
4466
5084
  import {
4467
- existsSync as existsSync3,
4468
- readFileSync as readFileSync3,
5085
+ existsSync as existsSync4,
5086
+ readFileSync as readFileSync4,
4469
5087
  realpathSync,
4470
5088
  rmSync as rmSync2,
4471
5089
  writeFileSync as writeFileSync4
4472
5090
  } from "fs";
4473
- import { homedir as homedir4 } from "os";
4474
- import { join as join6, resolve as resolve2 } from "path";
5091
+ import { homedir as homedir5 } from "os";
5092
+ import { join as join7, resolve as resolve3 } from "path";
4475
5093
  import { fileURLToPath } from "url";
4476
5094
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
4477
5095
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -4479,20 +5097,20 @@ var alreadyRan = false;
4479
5097
  function candidateCacheRoots() {
4480
5098
  const xdg = process.env.XDG_CACHE_HOME;
4481
5099
  return [
4482
- xdg ? join6(xdg, "opencode") : null,
4483
- join6(homedir4(), ".cache", "opencode"),
4484
- join6(homedir4(), "Library", "Caches", "opencode")
5100
+ xdg ? join7(xdg, "opencode") : null,
5101
+ join7(homedir5(), ".cache", "opencode"),
5102
+ join7(homedir5(), "Library", "Caches", "opencode")
4485
5103
  ].filter((p) => Boolean(p));
4486
5104
  }
4487
5105
  function userOpencodeJsonPath() {
4488
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
4489
- return join6(xdgConfig, "opencode", "opencode.json");
5106
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
5107
+ return join7(xdgConfig, "opencode", "opencode.json");
4490
5108
  }
4491
5109
  function userIntendsToUseUnscoped() {
4492
5110
  const cfg = userOpencodeJsonPath();
4493
- if (!existsSync3(cfg)) return false;
5111
+ if (!existsSync4(cfg)) return false;
4494
5112
  try {
4495
- const json = JSON.parse(readFileSync3(cfg, "utf8"));
5113
+ const json = JSON.parse(readFileSync4(cfg, "utf8"));
4496
5114
  const plugins = json.plugin;
4497
5115
  if (!Array.isArray(plugins)) return false;
4498
5116
  return plugins.some(
@@ -4505,7 +5123,7 @@ function userIntendsToUseUnscoped() {
4505
5123
  function ourLoadedDir() {
4506
5124
  try {
4507
5125
  const filePath = fileURLToPath(import.meta.url);
4508
- return realpathSync(resolve2(filePath, "..", ".."));
5126
+ return realpathSync(resolve3(filePath, "..", ".."));
4509
5127
  } catch {
4510
5128
  return null;
4511
5129
  }
@@ -4528,20 +5146,20 @@ function cleanupStaleUnscopedInstall() {
4528
5146
  }
4529
5147
  }
4530
5148
  function cleanupOne(cacheRoot, ourDir) {
4531
- if (!existsSync3(cacheRoot)) return;
4532
- const stalePath = join6(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
4533
- if (!existsSync3(stalePath)) return;
5149
+ if (!existsSync4(cacheRoot)) return;
5150
+ const stalePath = join7(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
5151
+ if (!existsSync4(stalePath)) return;
4534
5152
  let realStalePath = stalePath;
4535
5153
  try {
4536
5154
  realStalePath = realpathSync(stalePath);
4537
5155
  } catch {
4538
5156
  }
4539
5157
  if (ourDir && realStalePath === ourDir) return;
4540
- const pkgJsonPath = join6(stalePath, "package.json");
4541
- if (!existsSync3(pkgJsonPath)) return;
5158
+ const pkgJsonPath = join7(stalePath, "package.json");
5159
+ if (!existsSync4(pkgJsonPath)) return;
4542
5160
  let pkg = {};
4543
5161
  try {
4544
- pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
5162
+ pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
4545
5163
  } catch {
4546
5164
  return;
4547
5165
  }
@@ -4557,10 +5175,10 @@ function cleanupOne(cacheRoot, ourDir) {
4557
5175
  });
4558
5176
  return;
4559
5177
  }
4560
- const cachePkgJson = join6(cacheRoot, "package.json");
4561
- if (!existsSync3(cachePkgJson)) return;
5178
+ const cachePkgJson = join7(cacheRoot, "package.json");
5179
+ if (!existsSync4(cachePkgJson)) return;
4562
5180
  try {
4563
- const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
5181
+ const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
4564
5182
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
4565
5183
  delete cfg.dependencies[STALE_PACKAGE_NAME];
4566
5184
  writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -4615,7 +5233,11 @@ function createClaudeCode(settings = {}) {
4615
5233
  proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
4616
5234
  multiStepContinuation: settings.multiStepContinuation ?? true,
4617
5235
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
4618
- compactionModel: settings.compactionModel
5236
+ compactionModel: settings.compactionModel,
5237
+ interactive: settings.interactive,
5238
+ interactiveBypass: settings.interactiveBypass,
5239
+ interactiveAllowTools: settings.interactiveAllowTools,
5240
+ interactiveSystemPrompt: settings.interactiveSystemPrompt
4619
5241
  });
4620
5242
  };
4621
5243
  const provider = function(modelId) {