@khalilgharbaoui/opencode-claude-code-plugin 0.8.2 → 0.9.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
@@ -1252,11 +1252,15 @@ function envFlagEnabled(value) {
1252
1252
  function isClaudeThinkingDisabled() {
1253
1253
  return envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_THINKING) || envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING);
1254
1254
  }
1255
- function claudeSpawnEnv() {
1255
+ function claudeSpawnEnv(opts) {
1256
1256
  const env = {
1257
1257
  ...process.env,
1258
1258
  TERM: "xterm-256color"
1259
1259
  };
1260
+ if (opts?.ignoreAnthropicApiKey) {
1261
+ delete env.ANTHROPIC_API_KEY;
1262
+ delete env.ANTHROPIC_AUTH_TOKEN;
1263
+ }
1260
1264
  if (!isClaudeThinkingDisabled() && process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0) {
1261
1265
  env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES = "1";
1262
1266
  }
@@ -1282,6 +1286,9 @@ function getActiveProcess(key) {
1282
1286
  if (ap) touch(key);
1283
1287
  return ap;
1284
1288
  }
1289
+ function setActiveProcess(key, ap) {
1290
+ activeProcesses.set(key, ap);
1291
+ }
1285
1292
  function deleteActiveProcess(key) {
1286
1293
  const ap = activeProcesses.get(key);
1287
1294
  if (ap) {
@@ -1301,13 +1308,13 @@ function deleteClaudeSessionId(key) {
1301
1308
  if (claudeSessionId) clearLedger(claudeSessionId);
1302
1309
  claudeSessions.delete(key);
1303
1310
  }
1304
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile) {
1311
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey) {
1305
1312
  evictIfNeeded();
1306
1313
  log.info("spawning new claude process", { cliPath, cliArgs, cwd, sessionKey: sessionKey2 });
1307
1314
  const proc = spawn(cliPath, cliArgs, {
1308
1315
  cwd,
1309
1316
  stdio: ["pipe", "pipe", "pipe"],
1310
- env: claudeSpawnEnv(),
1317
+ env: claudeSpawnEnv({ ignoreAnthropicApiKey }),
1311
1318
  shell: process.platform === "win32"
1312
1319
  });
1313
1320
  const lineEmitter = new EventEmitter();
@@ -1425,12 +1432,564 @@ function sessionKey(cwd, modelId) {
1425
1432
  return `${cwd}::${modelId}`;
1426
1433
  }
1427
1434
 
1428
- // src/proxy-mcp.ts
1429
- import { createServer } from "http";
1435
+ // src/claude-session-wrapper.ts
1436
+ import { EventEmitter as EventEmitter2 } from "events";
1437
+ import { unlink as unlink2 } from "fs/promises";
1438
+
1439
+ // src/claude-session-bun.ts
1440
+ import * as os3 from "os";
1430
1441
  import * as fs3 from "fs";
1431
1442
  import * as path3 from "path";
1443
+ import { execFileSync } from "child_process";
1444
+ import { randomUUID } from "crypto";
1445
+ function resolveClaude(cmd = "claude") {
1446
+ if (path3.isAbsolute(cmd) && fs3.existsSync(cmd)) return cmd;
1447
+ const viaBun = Bun.which(cmd);
1448
+ if (viaBun) return viaBun;
1449
+ const isWin = os3.platform() === "win32";
1450
+ try {
1451
+ const out = execFileSync(isWin ? "where" : "which", [cmd], {
1452
+ encoding: "utf8",
1453
+ stdio: ["ignore", "pipe", "ignore"]
1454
+ });
1455
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs3.existsSync(p));
1456
+ if (first) return first;
1457
+ } catch {
1458
+ }
1459
+ throw new Error(`Could not resolve command on PATH: ${cmd}`);
1460
+ }
1461
+ function encodeCwd(cwd) {
1462
+ return path3.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
1463
+ }
1464
+ var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
1465
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
1466
+ function resolveConfigDir(configDir) {
1467
+ const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
1468
+ if (!value) return path3.join(os3.homedir(), ".claude");
1469
+ if (value === "~") return os3.homedir();
1470
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
1471
+ return path3.join(os3.homedir(), value.slice(2));
1472
+ }
1473
+ return path3.resolve(value);
1474
+ }
1475
+ var ClaudeSession = class {
1476
+ sessionId;
1477
+ cwd;
1478
+ configDir;
1479
+ jsonlPath;
1480
+ raw = "";
1481
+ proc = null;
1482
+ cursor = 0;
1483
+ // index into transcript split('\n')
1484
+ lastDataAt = 0;
1485
+ exited = false;
1486
+ exitCode = null;
1487
+ aborted = false;
1488
+ signal;
1489
+ o;
1490
+ constructor(opts = {}) {
1491
+ this.cwd = path3.resolve(opts.cwd ?? process.cwd());
1492
+ this.configDir = resolveConfigDir(opts.configDir);
1493
+ this.signal = opts.signal;
1494
+ this.sessionId = randomUUID();
1495
+ this.jsonlPath = path3.join(
1496
+ this.configDir,
1497
+ "projects",
1498
+ encodeCwd(this.cwd),
1499
+ `${this.sessionId}.jsonl`
1500
+ );
1501
+ this.o = {
1502
+ cwd: this.cwd,
1503
+ cliPath: opts.cliPath,
1504
+ configDir: this.configDir,
1505
+ model: opts.model,
1506
+ settingSources: opts.settingSources,
1507
+ extraArgs: opts.extraArgs ?? [],
1508
+ ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
1509
+ cols: opts.cols ?? 200,
1510
+ rows: opts.rows ?? 50,
1511
+ bootMinMs: opts.bootMinMs ?? 3e3,
1512
+ bootQuietMs: opts.bootQuietMs ?? 1500,
1513
+ bootMaxMs: opts.bootMaxMs ?? 25e3,
1514
+ pollMs: opts.pollMs ?? 250,
1515
+ // Agentic turns (tool loops) routinely run for many minutes; a short
1516
+ // cap would surface as a mid-task error result. 30 min mirrors the
1517
+ // proxy-tool ceiling rather than a chat-reply expectation.
1518
+ turnTimeoutMs: opts.turnTimeoutMs ?? 18e5,
1519
+ bracketedPaste: opts.bracketedPaste ?? true,
1520
+ submitMinMs: opts.submitMinMs ?? 200,
1521
+ submitConfirmMs: opts.submitConfirmMs ?? 1500,
1522
+ submitMaxRetries: opts.submitMaxRetries ?? 8,
1523
+ debug: opts.debug ?? false
1524
+ };
1525
+ }
1526
+ async start() {
1527
+ if (this.signal?.aborted) throw new Error("aborted before start");
1528
+ this.signal?.addEventListener(
1529
+ "abort",
1530
+ () => {
1531
+ this.aborted = true;
1532
+ this.dispose();
1533
+ },
1534
+ { once: true }
1535
+ );
1536
+ const claude = resolveClaude(this.o.cliPath ?? "claude");
1537
+ const args = ["--session-id", this.sessionId];
1538
+ if (this.o.model) args.push("--model", this.o.model);
1539
+ if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
1540
+ args.push("--setting-sources", this.o.settingSources);
1541
+ }
1542
+ if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
1543
+ if (this.o.debug)
1544
+ process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
1545
+ `);
1546
+ this.lastDataAt = Date.now();
1547
+ this.proc = Bun.spawn([claude, ...args], {
1548
+ cwd: this.cwd,
1549
+ env: {
1550
+ ...process.env,
1551
+ CLAUDE_CONFIG_DIR: this.o.configDir,
1552
+ TERM: "xterm-256color",
1553
+ ...this.o.ignoreAnthropicApiKey ? { ANTHROPIC_API_KEY: void 0, ANTHROPIC_AUTH_TOKEN: void 0 } : {}
1554
+ },
1555
+ terminal: {
1556
+ cols: this.o.cols,
1557
+ rows: this.o.rows,
1558
+ data: (_term, d) => {
1559
+ this.lastDataAt = Date.now();
1560
+ const chunk = Buffer.from(d).toString("utf8");
1561
+ this.raw += chunk;
1562
+ if (this.o.debug) process.stdout.write(chunk);
1563
+ }
1564
+ }
1565
+ });
1566
+ this.proc.exited.then((code) => {
1567
+ this.exitCode = typeof code === "number" ? code : null;
1568
+ this.exited = true;
1569
+ this.proc = null;
1570
+ }).catch(() => {
1571
+ this.exited = true;
1572
+ this.proc = null;
1573
+ });
1574
+ await this.waitForBoot();
1575
+ this.cursor = this.lineCount();
1576
+ }
1577
+ /** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
1578
+ * bootMinMs..bootMaxMs. */
1579
+ async waitForBoot() {
1580
+ const start = Date.now();
1581
+ while (Date.now() - start < this.o.bootMaxMs) {
1582
+ await delay(150);
1583
+ if (this.aborted) throw new Error("aborted during boot");
1584
+ if (this.exited) {
1585
+ throw new Error(this.failureMessage("claude exited during boot", true));
1586
+ }
1587
+ const elapsed = Date.now() - start;
1588
+ const sinceData = Date.now() - this.lastDataAt;
1589
+ if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) return;
1590
+ }
1591
+ }
1592
+ /** Submit the freshly-injected prompt and confirm the turn was actually
1593
+ * accepted. A large bracketed paste collapses into a "[Pasted text]"
1594
+ * placeholder; an Enter sent while claude is still ingesting the paste is
1595
+ * silently dropped, so a single fixed-delay Enter races the paste and can
1596
+ * leave the prompt sitting unsubmitted (→ hang until turnTimeoutMs). Send
1597
+ * Enter, then poll for transcript growth past the cursor (the turn's records
1598
+ * are written on acceptance); resend Enter until accepted or the retry
1599
+ * budget is spent. Polling growth (not a blind delay) also stops us from
1600
+ * sending a stray Enter once the turn is in flight. */
1601
+ async submitTurn() {
1602
+ await delay(this.o.submitMinMs);
1603
+ for (let attempt = 0; attempt < this.o.submitMaxRetries; attempt++) {
1604
+ if (this.aborted || this.exited || !this.proc) return;
1605
+ this.proc.terminal.write("\r");
1606
+ const until = Date.now() + this.o.submitConfirmMs;
1607
+ while (Date.now() < until) {
1608
+ await delay(80);
1609
+ if (this.aborted || this.exited) return;
1610
+ if (this.lineCount() > this.cursor) return;
1611
+ }
1612
+ }
1613
+ }
1614
+ readRawLines() {
1615
+ try {
1616
+ return fs3.readFileSync(this.jsonlPath, "utf8").split("\n");
1617
+ } catch {
1618
+ return [];
1619
+ }
1620
+ }
1621
+ /** Count of complete lines (split('\n') minus the trailing/partial element). */
1622
+ lineCount() {
1623
+ const lines = this.readRawLines();
1624
+ return lines.length > 0 ? lines.length - 1 : 0;
1625
+ }
1626
+ rawTail(max = 600) {
1627
+ const clean = this.raw.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\s+/g, " ").trim();
1628
+ return clean.length > max ? clean.slice(-max) : clean;
1629
+ }
1630
+ failureMessage(reason, includeRaw = false) {
1631
+ const parts = [
1632
+ `${reason} (sessionId=${this.sessionId}, jsonlPath=${this.jsonlPath}, exitCode=${this.exitCode ?? "unknown"})`
1633
+ ];
1634
+ if (includeRaw) {
1635
+ const tail = this.rawTail();
1636
+ if (tail) parts.push(`terminalTail=${JSON.stringify(tail)}`);
1637
+ }
1638
+ return parts.join("; ");
1639
+ }
1640
+ /**
1641
+ * Inject a turn into the live session and return the assistant reply once a
1642
+ * terminal stop_reason is observed in the transcript.
1643
+ */
1644
+ async ask(prompt, perTurnTimeoutMs) {
1645
+ if (this.aborted) throw new Error("aborted");
1646
+ if (!this.proc || this.exited)
1647
+ throw new Error("session not started or already exited");
1648
+ const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
1649
+ const t0 = Date.now();
1650
+ if (this.o.bracketedPaste) {
1651
+ this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
1652
+ } else {
1653
+ this.proc.terminal.write(prompt);
1654
+ }
1655
+ await this.submitTurn();
1656
+ const collected = [];
1657
+ let lastUsage = null;
1658
+ let stopReason = null;
1659
+ const deadline = Date.now() + timeout;
1660
+ while (Date.now() < deadline) {
1661
+ await delay(this.o.pollMs);
1662
+ if (this.aborted) throw new Error("aborted mid-turn");
1663
+ const lines = this.readRawLines();
1664
+ const lastComplete = lines.length - 1;
1665
+ if (lastComplete <= this.cursor) {
1666
+ if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
1667
+ continue;
1668
+ }
1669
+ for (let i = this.cursor; i < lastComplete; i++) {
1670
+ const s = lines[i];
1671
+ if (!s || !s.trim()) continue;
1672
+ let rec;
1673
+ try {
1674
+ rec = JSON.parse(s);
1675
+ } catch {
1676
+ continue;
1677
+ }
1678
+ if (rec.type === "assistant" && rec.message) {
1679
+ for (const b of rec.message.content ?? []) {
1680
+ if (b?.type === "text" && typeof b.text === "string")
1681
+ collected.push(b.text);
1682
+ }
1683
+ if (rec.message.usage) lastUsage = rec.message.usage;
1684
+ if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
1685
+ stopReason = rec.message.stop_reason;
1686
+ }
1687
+ }
1688
+ }
1689
+ this.cursor = lastComplete;
1690
+ if (stopReason) break;
1691
+ }
1692
+ if (!stopReason) {
1693
+ throw new Error(
1694
+ this.failureMessage(
1695
+ `turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected.length} text block(s))`
1696
+ )
1697
+ );
1698
+ }
1699
+ const u = lastUsage ?? {};
1700
+ return {
1701
+ text: collected.join("\n").trim(),
1702
+ stopReason,
1703
+ usage: lastUsage,
1704
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
1705
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
1706
+ ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
1707
+ ephemeral5mTokens: u.cache_creation?.ephemeral_5m_input_tokens ?? 0,
1708
+ inputTokens: u.input_tokens ?? 0,
1709
+ outputTokens: u.output_tokens ?? 0,
1710
+ elapsedMs: Date.now() - t0
1711
+ };
1712
+ }
1713
+ /**
1714
+ * Like ask(), but instead of collecting the reply text it re-emits each NEW
1715
+ * raw JSONL transcript line via onLine (verbatim) until a terminal
1716
+ * stop_reason. Returns the terminal stop_reason + the last assistant usage.
1717
+ * Used by the opencode plugin transport shim, which feeds these raw lines
1718
+ * into the existing stream-json line handler unchanged.
1719
+ */
1720
+ async tailTurn(prompt, onLine, perTurnTimeoutMs) {
1721
+ if (this.aborted) throw new Error("aborted");
1722
+ if (!this.proc || this.exited)
1723
+ throw new Error("session not started or already exited");
1724
+ const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
1725
+ if (this.o.bracketedPaste) {
1726
+ this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
1727
+ } else {
1728
+ this.proc.terminal.write(prompt);
1729
+ }
1730
+ await this.submitTurn();
1731
+ let lastUsage = null;
1732
+ let totalOutput = 0;
1733
+ let stopReason = null;
1734
+ const deadline = Date.now() + timeout;
1735
+ while (Date.now() < deadline) {
1736
+ await delay(this.o.pollMs);
1737
+ if (this.aborted) throw new Error("aborted mid-turn");
1738
+ const lines = this.readRawLines();
1739
+ const lastComplete = lines.length - 1;
1740
+ if (lastComplete <= this.cursor) {
1741
+ if (this.exited) {
1742
+ throw new Error(this.failureMessage("claude exited mid-turn", true));
1743
+ }
1744
+ continue;
1745
+ }
1746
+ for (let i = this.cursor; i < lastComplete; i++) {
1747
+ const s = lines[i];
1748
+ if (!s || !s.trim()) continue;
1749
+ onLine(s);
1750
+ let rec;
1751
+ try {
1752
+ rec = JSON.parse(s);
1753
+ } catch {
1754
+ continue;
1755
+ }
1756
+ if (rec.type === "assistant" && rec.message) {
1757
+ if (rec.message.usage) {
1758
+ lastUsage = rec.message.usage;
1759
+ totalOutput += rec.message.usage.output_tokens ?? 0;
1760
+ }
1761
+ if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
1762
+ stopReason = rec.message.stop_reason;
1763
+ }
1764
+ }
1765
+ }
1766
+ this.cursor = lastComplete;
1767
+ if (stopReason) break;
1768
+ }
1769
+ let usage = lastUsage;
1770
+ if (lastUsage) {
1771
+ usage = { ...lastUsage, output_tokens: totalOutput };
1772
+ if (Array.isArray(lastUsage.iterations) && lastUsage.iterations.length > 0) {
1773
+ const iters = lastUsage.iterations.map((it) => ({ ...it }));
1774
+ iters[iters.length - 1] = {
1775
+ ...iters[iters.length - 1],
1776
+ output_tokens: totalOutput
1777
+ };
1778
+ usage.iterations = iters;
1779
+ }
1780
+ }
1781
+ if (!stopReason) {
1782
+ throw new Error(
1783
+ this.failureMessage(
1784
+ `turn timed out after ${timeout}ms (no terminal assistant record)`
1785
+ )
1786
+ );
1787
+ }
1788
+ return { stopReason, usage };
1789
+ }
1790
+ dispose() {
1791
+ if (this.proc) {
1792
+ try {
1793
+ this.proc.terminal.write("");
1794
+ } catch {
1795
+ }
1796
+ try {
1797
+ this.proc.kill();
1798
+ } catch {
1799
+ }
1800
+ try {
1801
+ this.proc.terminal.close();
1802
+ } catch {
1803
+ }
1804
+ }
1805
+ this.proc = null;
1806
+ }
1807
+ };
1808
+
1809
+ // src/claude-session-wrapper.ts
1810
+ function decodeUserEnvelope(chunk) {
1811
+ let parsed;
1812
+ try {
1813
+ parsed = JSON.parse(chunk);
1814
+ } catch {
1815
+ return chunk;
1816
+ }
1817
+ if (!parsed || parsed.type !== "user" || !parsed.message) return chunk;
1818
+ const content = parsed.message.content;
1819
+ if (typeof content === "string") return content;
1820
+ if (!Array.isArray(content)) return chunk;
1821
+ const parts = [];
1822
+ let dropped = 0;
1823
+ for (const block of content) {
1824
+ if (block?.type === "text" && typeof block.text === "string") {
1825
+ parts.push(block.text);
1826
+ } else if (block?.type === "tool_result") {
1827
+ const v = block.content;
1828
+ const text = typeof v === "string" ? v : Array.isArray(v) ? v.map((i) => i?.type === "text" ? i.text : "").filter(Boolean).join("\n") : "";
1829
+ parts.push(
1830
+ `[Tool result${block.tool_use_id ? ` ${block.tool_use_id}` : ""}]
1831
+ ${text}`
1832
+ );
1833
+ } else {
1834
+ dropped++;
1835
+ }
1836
+ }
1837
+ if (dropped > 0) {
1838
+ log.warn("interactive transport dropped non-text content blocks", {
1839
+ dropped
1840
+ });
1841
+ }
1842
+ return parts.join("\n\n");
1843
+ }
1844
+ function spawnInteractiveProcess(opts) {
1845
+ const extraArgs = [];
1846
+ if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
1847
+ extraArgs.push(
1848
+ "--mcp-config",
1849
+ ...opts.mcpConfigPaths,
1850
+ "--strict-mcp-config"
1851
+ );
1852
+ }
1853
+ if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
1854
+ extraArgs.push(
1855
+ "--settings",
1856
+ JSON.stringify({ permissions: { allow: opts.permissionsAllow } })
1857
+ );
1858
+ }
1859
+ if (opts.permissionMode === "bypassPermissions") {
1860
+ log.warn(
1861
+ "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
1862
+ );
1863
+ } else if (opts.permissionMode) {
1864
+ extraArgs.push("--permission-mode", opts.permissionMode);
1865
+ }
1866
+ if (opts.systemPromptFile) {
1867
+ extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
1868
+ }
1869
+ const session = new ClaudeSession({
1870
+ cwd: opts.cwd,
1871
+ cliPath: opts.cliPath,
1872
+ configDir: opts.configDir,
1873
+ model: opts.model,
1874
+ // Default null = normal CLAUDE.md + settings load, matching what the
1875
+ // headless spawn does. "" (skip everything) is for fast e2e runs only.
1876
+ settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
1877
+ extraArgs,
1878
+ ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey
1879
+ });
1880
+ log.info("prepared interactive claude session", {
1881
+ cwd: opts.cwd,
1882
+ cliPath: opts.cliPath ?? "claude",
1883
+ configDir: session.configDir,
1884
+ model: opts.model,
1885
+ sessionId: session.sessionId,
1886
+ jsonlPath: session.jsonlPath
1887
+ });
1888
+ const lineEmitter = new EventEmitter2();
1889
+ const errorHandlers = /* @__PURE__ */ new Set();
1890
+ let startPromise = null;
1891
+ const ensureStarted = () => {
1892
+ if (!startPromise) startPromise = session.start();
1893
+ return startPromise;
1894
+ };
1895
+ const emitResult = (subtype, isError, result, usage) => {
1896
+ lineEmitter.emit(
1897
+ "line",
1898
+ JSON.stringify({
1899
+ type: "result",
1900
+ subtype,
1901
+ is_error: isError,
1902
+ result,
1903
+ session_id: session.sessionId,
1904
+ usage: usage ?? {},
1905
+ total_cost_usd: null,
1906
+ duration_ms: 0
1907
+ })
1908
+ );
1909
+ };
1910
+ const runTurn = (userMsg) => {
1911
+ void (async () => {
1912
+ try {
1913
+ await ensureStarted();
1914
+ const { stopReason, usage } = await session.tailTurn(userMsg, (raw) => {
1915
+ lineEmitter.emit("line", raw);
1916
+ });
1917
+ const timedOut = !stopReason;
1918
+ emitResult(
1919
+ timedOut ? "error_during_execution" : stopReason,
1920
+ timedOut,
1921
+ timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
1922
+ usage
1923
+ );
1924
+ } catch (err) {
1925
+ const e = err instanceof Error ? err : new Error(String(err));
1926
+ log.error("interactive turn failed", { error: e.message });
1927
+ emitResult(
1928
+ "error_during_execution",
1929
+ true,
1930
+ `Interactive transport failed: ${e.message}`
1931
+ );
1932
+ if (errorHandlers.size > 0) {
1933
+ for (const h of errorHandlers) h(e);
1934
+ } else {
1935
+ lineEmitter.emit("close");
1936
+ }
1937
+ }
1938
+ })();
1939
+ };
1940
+ const proc = {
1941
+ stdin: {
1942
+ write(chunk) {
1943
+ const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
1944
+ runTurn(decodeUserEnvelope(raw));
1945
+ return true;
1946
+ },
1947
+ end() {
1948
+ }
1949
+ },
1950
+ stdout: null,
1951
+ stderr: null,
1952
+ pid: -1,
1953
+ killed: false,
1954
+ on(event, fn) {
1955
+ if (event === "error") errorHandlers.add(fn);
1956
+ return proc;
1957
+ },
1958
+ once() {
1959
+ return proc;
1960
+ },
1961
+ off(event, fn) {
1962
+ if (event === "error") errorHandlers.delete(fn);
1963
+ return proc;
1964
+ },
1965
+ kill() {
1966
+ try {
1967
+ session.dispose();
1968
+ } catch {
1969
+ }
1970
+ if (opts.systemPromptFile) {
1971
+ void unlink2(opts.systemPromptFile).catch(() => {
1972
+ });
1973
+ }
1974
+ proc.killed = true;
1975
+ return true;
1976
+ }
1977
+ };
1978
+ return {
1979
+ proc,
1980
+ lineEmitter,
1981
+ proxyServer: null,
1982
+ mcpHash: void 0,
1983
+ systemPromptFile: opts.systemPromptFile
1984
+ };
1985
+ }
1986
+
1987
+ // src/proxy-mcp.ts
1988
+ import { createServer } from "http";
1989
+ import * as fs4 from "fs";
1990
+ import * as path4 from "path";
1432
1991
  import * as crypto2 from "crypto";
1433
- import { EventEmitter as EventEmitter2 } from "events";
1992
+ import { EventEmitter as EventEmitter3 } from "events";
1434
1993
  var PROTOCOL_VERSION = "2024-11-05";
1435
1994
  var SERVER_NAME = "opencode_proxy";
1436
1995
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
@@ -1557,7 +2116,7 @@ var DEFAULT_PROXY_TOOLS = [
1557
2116
  }
1558
2117
  ];
1559
2118
  async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1560
- const calls = new EventEmitter2();
2119
+ const calls = new EventEmitter3();
1561
2120
  const pending = /* @__PURE__ */ new Map();
1562
2121
  const server2 = createServer(async (req, res) => {
1563
2122
  if (req.method !== "POST" || !req.url?.startsWith("/mcp")) {
@@ -1637,12 +2196,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1637
2196
  });
1638
2197
  let timer = null;
1639
2198
  const result = await new Promise(
1640
- (resolve3, reject) => {
2199
+ (resolve4, reject) => {
1641
2200
  const entry = {
1642
2201
  id: callId,
1643
2202
  toolName,
1644
2203
  input,
1645
- resolve: resolve3,
2204
+ resolve: resolve4,
1646
2205
  reject
1647
2206
  };
1648
2207
  pending.set(callId, entry);
@@ -1717,11 +2276,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1717
2276
  }
1718
2277
  }
1719
2278
  });
1720
- await new Promise((resolve3, reject) => {
2279
+ await new Promise((resolve4, reject) => {
1721
2280
  server2.once("error", reject);
1722
2281
  server2.listen(0, "127.0.0.1", () => {
1723
2282
  server2.off("error", reject);
1724
- resolve3();
2283
+ resolve4();
1725
2284
  });
1726
2285
  });
1727
2286
  const addr = server2.address();
@@ -1755,11 +2314,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1755
2314
  2
1756
2315
  );
1757
2316
  const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
1758
- const outPath = path3.join(
2317
+ const outPath = path4.join(
1759
2318
  pluginTmpDir(),
1760
2319
  `proxy-${hash}.json`
1761
2320
  );
1762
- fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
2321
+ fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
1763
2322
  configFilePath = outPath;
1764
2323
  return outPath;
1765
2324
  },
@@ -1768,12 +2327,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1768
2327
  entry.reject(new Error("proxy MCP server closed"));
1769
2328
  }
1770
2329
  pending.clear();
1771
- await new Promise((resolve3) => {
1772
- server2.close(() => resolve3());
2330
+ await new Promise((resolve4) => {
2331
+ server2.close(() => resolve4());
1773
2332
  });
1774
2333
  if (configFilePath) {
1775
2334
  try {
1776
- fs3.unlinkSync(configFilePath);
2335
+ fs4.unlinkSync(configFilePath);
1777
2336
  } catch {
1778
2337
  }
1779
2338
  configFilePath = null;
@@ -1807,10 +2366,10 @@ function disallowedToolFlags(tools) {
1807
2366
  return out;
1808
2367
  }
1809
2368
  function readBody(req) {
1810
- return new Promise((resolve3, reject) => {
2369
+ return new Promise((resolve4, reject) => {
1811
2370
  const chunks = [];
1812
2371
  req.on("data", (chunk) => chunks.push(chunk));
1813
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
2372
+ req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
1814
2373
  req.on("error", reject);
1815
2374
  });
1816
2375
  }
@@ -1823,10 +2382,10 @@ function writeJson(res, body) {
1823
2382
  }
1824
2383
 
1825
2384
  // src/proxy-broker.ts
1826
- import { EventEmitter as EventEmitter3 } from "events";
2385
+ import { EventEmitter as EventEmitter4 } from "events";
1827
2386
  var pendingByCallId = /* @__PURE__ */ new Map();
1828
2387
  var callIdsBySession = /* @__PURE__ */ new Map();
1829
- var emitter = new EventEmitter3();
2388
+ var emitter = new EventEmitter4();
1830
2389
  var PENDING_PROXY_CALL_TIMEOUT_MS = 10 * 60 * 1e3;
1831
2390
  function eventName(sessionKey2) {
1832
2391
  return `pending:${sessionKey2}`;
@@ -1948,11 +2507,11 @@ function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
1948
2507
  }
1949
2508
 
1950
2509
  // 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";
2510
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2511
+ import { unlink as unlink3 } from "fs/promises";
2512
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
2513
+ import { randomUUID as randomUUID3 } from "crypto";
2514
+ import { dirname as dirname3, join as join6 } from "path";
1956
2515
  var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
1957
2516
  function resolveCompactionModel(configured) {
1958
2517
  const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
@@ -2141,9 +2700,9 @@ function makeAutoContinueMessage() {
2141
2700
  }
2142
2701
  });
2143
2702
  }
2144
- function readPromptFileIfPresent(path5) {
2703
+ function readPromptFileIfPresent(path6) {
2145
2704
  try {
2146
- const content = readFileSync2(path5, "utf8").trim();
2705
+ const content = readFileSync3(path6, "utf8").trim();
2147
2706
  return content || void 0;
2148
2707
  } catch {
2149
2708
  return void 0;
@@ -2152,7 +2711,7 @@ function readPromptFileIfPresent(path5) {
2152
2711
  function nearestWorkspaceAgentsPrompt(cwd) {
2153
2712
  let dir = cwd;
2154
2713
  while (true) {
2155
- const content = readPromptFileIfPresent(join5(dir, "AGENTS.md"));
2714
+ const content = readPromptFileIfPresent(join6(dir, "AGENTS.md"));
2156
2715
  if (content) return content;
2157
2716
  const parent = dirname3(dir);
2158
2717
  if (parent === dir) return void 0;
@@ -2203,8 +2762,8 @@ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystem
2203
2762
  for (const s of extraSystemContent) {
2204
2763
  if (s.trim()) parts.push(s.trim());
2205
2764
  }
2206
- const configRoot = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
2207
- const globalAgents = readPromptFileIfPresent(join5(configRoot, "opencode", "AGENTS.md"));
2765
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
2766
+ const globalAgents = readPromptFileIfPresent(join6(configRoot, "opencode", "AGENTS.md"));
2208
2767
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
2209
2768
  if (globalAgents) parts.push(globalAgents);
2210
2769
  if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
@@ -2212,10 +2771,10 @@ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystem
2212
2771
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
2213
2772
  const content = parts.join("\n\n");
2214
2773
  if (!content) return void 0;
2215
- const path5 = join5(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
2774
+ const path6 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
2216
2775
  try {
2217
- writeFileSync3(path5, content, "utf8");
2218
- return path5;
2776
+ writeFileSync3(path6, content, "utf8");
2777
+ return path6;
2219
2778
  } catch (err) {
2220
2779
  log.warn("failed to write system prompt file", { error: String(err) });
2221
2780
  return void 0;
@@ -2755,12 +3314,14 @@ var ClaudeCodeLanguageModel = class {
2755
3314
  const proc = spawn2(this.config.cliPath, cliArgs, {
2756
3315
  cwd,
2757
3316
  stdio: ["pipe", "pipe", "pipe"],
2758
- env: claudeSpawnEnv(),
3317
+ env: claudeSpawnEnv({
3318
+ ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey
3319
+ }),
2759
3320
  shell: process.platform === "win32"
2760
3321
  });
2761
3322
  if (systemPromptFile) {
2762
3323
  proc.on("exit", () => {
2763
- void unlink2(systemPromptFile).catch(() => {
3324
+ void unlink3(systemPromptFile).catch(() => {
2764
3325
  });
2765
3326
  });
2766
3327
  }
@@ -2771,7 +3332,7 @@ var ClaudeCodeLanguageModel = class {
2771
3332
  const toolCalls = [];
2772
3333
  const toolCallStreams = /* @__PURE__ */ new Map();
2773
3334
  let gotPartialEvents = false;
2774
- const result = await new Promise((resolve3, reject) => {
3335
+ const result = await new Promise((resolve4, reject) => {
2775
3336
  const cleanup = () => {
2776
3337
  try {
2777
3338
  if (!proc.killed && proc.exitCode === null) proc.kill();
@@ -2879,7 +3440,7 @@ ${plan}
2879
3440
  usage: msg.usage
2880
3441
  };
2881
3442
  cleanup();
2882
- resolve3({
3443
+ resolve4({
2883
3444
  ...resultMeta,
2884
3445
  text: responseText,
2885
3446
  thinking: thinkingText,
@@ -2891,7 +3452,7 @@ ${plan}
2891
3452
  });
2892
3453
  rl.on("close", () => {
2893
3454
  cleanup();
2894
- resolve3({
3455
+ resolve4({
2895
3456
  ...resultMeta,
2896
3457
  text: responseText,
2897
3458
  thinking: thinkingText,
@@ -2996,6 +3557,10 @@ ${plan}
2996
3557
  const toUsage = this.toUsage.bind(this);
2997
3558
  const toFinishReason = this.toFinishReason.bind(this);
2998
3559
  const handleControlRequest = this.handleControlRequest.bind(this);
3560
+ const flagOn = (v) => v !== void 0 && !["", "0", "false", "no", "off"].includes(v.trim().toLowerCase());
3561
+ const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
3562
+ const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
3563
+ const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
2999
3564
  if (scope === "no-tools" && !compactionMode) {
3000
3565
  log.info("doStream no-tools title stub", {
3001
3566
  compactionMode,
@@ -3118,81 +3683,143 @@ ${plan}
3118
3683
  }
3119
3684
  }
3120
3685
  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);
3686
+ if (useInteractive && !compactionMode) {
3687
+ const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
3688
+ if (activeProcess) {
3689
+ proc = activeProcess.proc;
3690
+ lineEmitter = activeProcess.lineEmitter;
3691
+ log.debug("reusing active interactive session", { sk });
3692
+ } else {
3693
+ const allow = [
3694
+ ...mcp.allEnabledServerNames.map((n) => `mcp__${n}__*`),
3695
+ "mcp__opencode_proxy__*",
3696
+ ...self.config.interactiveAllowTools ?? [
3697
+ "Bash",
3698
+ "Edit",
3699
+ "Write",
3700
+ "Read",
3701
+ "WebFetch"
3702
+ ]
3703
+ ];
3704
+ const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
3705
+ cwd,
3706
+ self.config.multiStepContinuation !== false
3707
+ // Do not forward opencode's own system prompt into the
3708
+ // interactive TUI. Live subscription-account testing
3709
+ // showed that large forwarded payload can trigger Claude
3710
+ // Code's third-party-app usage gate, while our static
3711
+ // CLI/AGENTS/continuation prompt remains safe.
3712
+ );
3713
+ if (self.config.interactiveSystemPrompt === false) {
3714
+ log.warn(
3715
+ "interactive system prompt disabled; opencode agent prompts will not be appended"
3716
+ );
3717
+ }
3718
+ if (interactiveBypassRequested) {
3719
+ log.warn(
3720
+ "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
3721
+ );
3722
+ }
3723
+ const ap = spawnInteractiveProcess({
3724
+ cwd,
3725
+ cliPath,
3726
+ configDir: self.config.configDir,
3727
+ model: effectiveModelId,
3728
+ mcpConfigPaths: mcp.paths,
3729
+ permissionsAllow: allow,
3730
+ systemPromptFile,
3731
+ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey
3732
+ });
3733
+ ap.mcpHash = mcp.bridgedHash;
3734
+ setActiveProcess(sk, ap);
3735
+ proc = ap.proc;
3736
+ lineEmitter = ap.lineEmitter;
3737
+ activeProcess = ap;
3738
+ log.info("spawned interactive claude session", {
3739
+ sk,
3740
+ cliPath,
3741
+ configDir: self.config.configDir,
3742
+ model: effectiveModelId
3743
+ });
3147
3744
  }
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
3745
  } 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;
3746
+ let cliArgs;
3747
+ let spawnSystemPromptFile;
3748
+ let spawnProxyServer = null;
3749
+ let spawnMcpHash = null;
3750
+ if (compactionMode) {
3751
+ cliArgs = buildCliArgs({
3752
+ sessionKey: sk,
3753
+ skipPermissions,
3754
+ includeSessionId: false,
3755
+ model: effectiveModelId,
3756
+ permissionMode: self.config.permissionMode,
3757
+ cliVersion
3758
+ });
3759
+ } else {
3760
+ const discovery = self.effectiveMcpConfig(
3761
+ cwd,
3762
+ void 0,
3763
+ runtimeStatus
3764
+ );
3765
+ const proxyMcpTools = await self.resolvedProxyMcpTools(
3766
+ discovery.allEnabledServerNames
3767
+ );
3768
+ const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
3769
+ const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
3770
+ if (!proxyServer && combinedProxyTools) {
3771
+ proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
3772
+ }
3773
+ const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
3774
+ const extraDisallowed = [];
3775
+ if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
3776
+ const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
3777
+ const mcp = self.effectiveMcpConfig(
3778
+ cwd,
3779
+ proxyServer?.configPath(),
3780
+ runtimeStatus,
3781
+ excludeServers
3782
+ );
3783
+ const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
3784
+ cwd,
3785
+ self.config.multiStepContinuation !== false,
3786
+ extractSystemMessages(options.prompt)
3787
+ );
3788
+ cliArgs = buildCliArgs({
3789
+ sessionKey: sk,
3790
+ skipPermissions,
3791
+ model: self.modelId,
3792
+ permissionMode: self.config.permissionMode,
3793
+ mcpConfig: mcp.paths,
3794
+ strictMcpConfig: self.config.strictMcpConfig,
3795
+ disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
3796
+ appendSystemPromptFile: systemPromptFile,
3797
+ ...self.thinkingCliOptions(),
3798
+ cliVersion
3799
+ });
3800
+ spawnSystemPromptFile = systemPromptFile;
3801
+ spawnProxyServer = proxyServer;
3802
+ spawnMcpHash = mcp.bridgedHash;
3803
+ }
3804
+ if (activeProcess && !compactionMode) {
3805
+ proc = activeProcess.proc;
3806
+ lineEmitter = activeProcess.lineEmitter;
3807
+ log.debug("reusing active process", { sk });
3808
+ } else {
3809
+ const ap = spawnClaudeProcess(
3810
+ cliPath,
3811
+ cliArgs,
3812
+ cwd,
3813
+ sk,
3814
+ spawnProxyServer,
3815
+ spawnMcpHash,
3816
+ spawnSystemPromptFile,
3817
+ self.config.ignoreAnthropicApiKey
3818
+ );
3819
+ proc = ap.proc;
3820
+ lineEmitter = ap.lineEmitter;
3821
+ activeProcess = ap;
3822
+ }
3196
3823
  }
3197
3824
  controller.enqueue({ type: "stream-start", warnings });
3198
3825
  let currentTextId = null;
@@ -3651,11 +4278,6 @@ ${plan}
3651
4278
  if (block.type === "tool_use" && block.id && block.name) {
3652
4279
  noteToolActivity();
3653
4280
  const parsedInput = block.input ?? {};
3654
- toolCallsById.set(block.id, {
3655
- id: block.id,
3656
- name: block.name,
3657
- input: parsedInput
3658
- });
3659
4281
  if (isAskUserQuestionTool(block.name)) {
3660
4282
  const askId = startTextBlock();
3661
4283
  controller.enqueue({
@@ -3708,6 +4330,11 @@ ${plan}
3708
4330
  toolUseId: block.id
3709
4331
  });
3710
4332
  if (!skip) {
4333
+ toolCallsById.set(block.id, {
4334
+ id: block.id,
4335
+ name: block.name,
4336
+ input: parsedInput
4337
+ });
3711
4338
  if (!executed) skipResultForIds.add(block.id);
3712
4339
  controller.enqueue({
3713
4340
  type: "tool-input-start",
@@ -3999,6 +4626,8 @@ ${plan}
3999
4626
  };
4000
4627
  const procErrorHandler = (err) => {
4001
4628
  log.error("process error", { error: err.message });
4629
+ deleteActiveProcess(sk);
4630
+ deleteClaudeSessionId(sk);
4002
4631
  if (controllerClosed) return;
4003
4632
  if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
4004
4633
  rejectAllPendingProxyCallsForSession(
@@ -4322,7 +4951,7 @@ var defaultModels = {
4322
4951
 
4323
4952
  // src/accounts.ts
4324
4953
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
4325
- import path4 from "path";
4954
+ import path5 from "path";
4326
4955
  var BASE_PROVIDER_ID = "claude-code";
4327
4956
  var DEFAULT_ACCOUNT = "default";
4328
4957
  var SHARED_CAPABILITY_ITEMS = [
@@ -4360,7 +4989,7 @@ function expandHome(value) {
4360
4989
  const home = process.env.HOME ?? process.env.USERPROFILE;
4361
4990
  if (value === "~") return home ?? value;
4362
4991
  if (value.startsWith("~/") || value.startsWith("~\\")) {
4363
- return home ? path4.join(home, value.slice(2)) : value;
4992
+ return home ? path5.join(home, value.slice(2)) : value;
4364
4993
  }
4365
4994
  return value;
4366
4995
  }
@@ -4383,7 +5012,7 @@ async function ensureAccountRuntime(account, baseCliPath) {
4383
5012
  baseCliPath,
4384
5013
  expandedConfigDir
4385
5014
  );
4386
- return { cliPath, configDir };
5015
+ return { cliPath, configDir: expandedConfigDir };
4387
5016
  }
4388
5017
  async function ensureSharedCapabilities(targetRoot) {
4389
5018
  const sourceRoot = expandHome("~/.claude");
@@ -4392,8 +5021,8 @@ async function ensureSharedCapabilities(targetRoot) {
4392
5021
  }
4393
5022
  }
4394
5023
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
4395
- const source = path4.join(sourceRoot, item);
4396
- const target = path4.join(targetRoot, item);
5024
+ const source = path5.join(sourceRoot, item);
5025
+ const target = path5.join(targetRoot, item);
4397
5026
  let sourceStat;
4398
5027
  try {
4399
5028
  sourceStat = await lstat(source);
@@ -4404,8 +5033,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
4404
5033
  const targetStat = await lstat(target);
4405
5034
  if (targetStat.isSymbolicLink()) {
4406
5035
  const current = await readlink(target);
4407
- const resolvedCurrent = path4.resolve(path4.dirname(target), current);
4408
- const resolvedSource = path4.resolve(source);
5036
+ const resolvedCurrent = path5.resolve(path5.dirname(target), current);
5037
+ const resolvedSource = path5.resolve(source);
4409
5038
  if (resolvedCurrent === resolvedSource) return;
4410
5039
  }
4411
5040
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -4420,11 +5049,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
4420
5049
  await symlink(source, target, type);
4421
5050
  }
4422
5051
  async function writeAccountWrapper(account, baseCliPath, configDir) {
4423
- const cacheRoot = path4.join(
5052
+ const cacheRoot = path5.join(
4424
5053
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
4425
5054
  "opencode-claude-code-plugin"
4426
5055
  );
4427
- const wrapperPath = path4.join(cacheRoot, `claude-${account}`);
5056
+ const wrapperPath = path5.join(cacheRoot, `claude-${account}`);
4428
5057
  const suffix = `@${account}`;
4429
5058
  await mkdir(cacheRoot, { recursive: true });
4430
5059
  const script = `#!/usr/bin/env bash
@@ -4464,14 +5093,14 @@ function titleizeAccount(account) {
4464
5093
 
4465
5094
  // src/cleanup-stale.ts
4466
5095
  import {
4467
- existsSync as existsSync3,
4468
- readFileSync as readFileSync3,
5096
+ existsSync as existsSync4,
5097
+ readFileSync as readFileSync4,
4469
5098
  realpathSync,
4470
5099
  rmSync as rmSync2,
4471
5100
  writeFileSync as writeFileSync4
4472
5101
  } from "fs";
4473
- import { homedir as homedir4 } from "os";
4474
- import { join as join6, resolve as resolve2 } from "path";
5102
+ import { homedir as homedir5 } from "os";
5103
+ import { join as join7, resolve as resolve3 } from "path";
4475
5104
  import { fileURLToPath } from "url";
4476
5105
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
4477
5106
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -4479,20 +5108,20 @@ var alreadyRan = false;
4479
5108
  function candidateCacheRoots() {
4480
5109
  const xdg = process.env.XDG_CACHE_HOME;
4481
5110
  return [
4482
- xdg ? join6(xdg, "opencode") : null,
4483
- join6(homedir4(), ".cache", "opencode"),
4484
- join6(homedir4(), "Library", "Caches", "opencode")
5111
+ xdg ? join7(xdg, "opencode") : null,
5112
+ join7(homedir5(), ".cache", "opencode"),
5113
+ join7(homedir5(), "Library", "Caches", "opencode")
4485
5114
  ].filter((p) => Boolean(p));
4486
5115
  }
4487
5116
  function userOpencodeJsonPath() {
4488
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
4489
- return join6(xdgConfig, "opencode", "opencode.json");
5117
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
5118
+ return join7(xdgConfig, "opencode", "opencode.json");
4490
5119
  }
4491
5120
  function userIntendsToUseUnscoped() {
4492
5121
  const cfg = userOpencodeJsonPath();
4493
- if (!existsSync3(cfg)) return false;
5122
+ if (!existsSync4(cfg)) return false;
4494
5123
  try {
4495
- const json = JSON.parse(readFileSync3(cfg, "utf8"));
5124
+ const json = JSON.parse(readFileSync4(cfg, "utf8"));
4496
5125
  const plugins = json.plugin;
4497
5126
  if (!Array.isArray(plugins)) return false;
4498
5127
  return plugins.some(
@@ -4505,7 +5134,7 @@ function userIntendsToUseUnscoped() {
4505
5134
  function ourLoadedDir() {
4506
5135
  try {
4507
5136
  const filePath = fileURLToPath(import.meta.url);
4508
- return realpathSync(resolve2(filePath, "..", ".."));
5137
+ return realpathSync(resolve3(filePath, "..", ".."));
4509
5138
  } catch {
4510
5139
  return null;
4511
5140
  }
@@ -4528,20 +5157,20 @@ function cleanupStaleUnscopedInstall() {
4528
5157
  }
4529
5158
  }
4530
5159
  function cleanupOne(cacheRoot, ourDir) {
4531
- if (!existsSync3(cacheRoot)) return;
4532
- const stalePath = join6(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
4533
- if (!existsSync3(stalePath)) return;
5160
+ if (!existsSync4(cacheRoot)) return;
5161
+ const stalePath = join7(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
5162
+ if (!existsSync4(stalePath)) return;
4534
5163
  let realStalePath = stalePath;
4535
5164
  try {
4536
5165
  realStalePath = realpathSync(stalePath);
4537
5166
  } catch {
4538
5167
  }
4539
5168
  if (ourDir && realStalePath === ourDir) return;
4540
- const pkgJsonPath = join6(stalePath, "package.json");
4541
- if (!existsSync3(pkgJsonPath)) return;
5169
+ const pkgJsonPath = join7(stalePath, "package.json");
5170
+ if (!existsSync4(pkgJsonPath)) return;
4542
5171
  let pkg = {};
4543
5172
  try {
4544
- pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
5173
+ pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
4545
5174
  } catch {
4546
5175
  return;
4547
5176
  }
@@ -4557,10 +5186,10 @@ function cleanupOne(cacheRoot, ourDir) {
4557
5186
  });
4558
5187
  return;
4559
5188
  }
4560
- const cachePkgJson = join6(cacheRoot, "package.json");
4561
- if (!existsSync3(cachePkgJson)) return;
5189
+ const cachePkgJson = join7(cacheRoot, "package.json");
5190
+ if (!existsSync4(cachePkgJson)) return;
4562
5191
  try {
4563
- const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
5192
+ const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
4564
5193
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
4565
5194
  delete cfg.dependencies[STALE_PACKAGE_NAME];
4566
5195
  writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -4581,6 +5210,21 @@ function pickOpencodeDirectory(input) {
4581
5210
  if (isUsableDirectory(ctx.worktree)) return ctx.worktree;
4582
5211
  return void 0;
4583
5212
  }
5213
+ var warnedAnthropicApiKey = false;
5214
+ function warnIfAnthropicApiKey(ignore) {
5215
+ if (warnedAnthropicApiKey) return;
5216
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_AUTH_TOKEN) return;
5217
+ warnedAnthropicApiKey = true;
5218
+ if (ignore) {
5219
+ log.warn(
5220
+ "ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; stripping it from claude spawns (ignoreAnthropicApiKey) so requests use your subscription auth, not pay-as-you-go API billing."
5221
+ );
5222
+ } else {
5223
+ log.warn(
5224
+ "ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; claude may bill as pay-as-you-go API usage instead of your subscription / Agent SDK credit. Set provider option `ignoreAnthropicApiKey: true` to force subscription auth."
5225
+ );
5226
+ }
5227
+ }
4584
5228
  function createClaudeCode(settings = {}) {
4585
5229
  if (settings.logging) {
4586
5230
  configureLogger({
@@ -4590,6 +5234,7 @@ function createClaudeCode(settings = {}) {
4590
5234
  level: settings.logging.level ?? "info"
4591
5235
  });
4592
5236
  }
5237
+ warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
4593
5238
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
4594
5239
  const providerName = settings.providerID ?? settings.name ?? "claude-code";
4595
5240
  const proxyTools = settings.proxyTools ?? ["Bash", "Edit", "Write", "WebFetch"];
@@ -4615,7 +5260,12 @@ function createClaudeCode(settings = {}) {
4615
5260
  proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
4616
5261
  multiStepContinuation: settings.multiStepContinuation ?? true,
4617
5262
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
4618
- compactionModel: settings.compactionModel
5263
+ compactionModel: settings.compactionModel,
5264
+ ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
5265
+ interactive: settings.interactive,
5266
+ interactiveBypass: settings.interactiveBypass,
5267
+ interactiveAllowTools: settings.interactiveAllowTools,
5268
+ interactiveSystemPrompt: settings.interactiveSystemPrompt
4619
5269
  });
4620
5270
  };
4621
5271
  const provider = function(modelId) {