@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/README.md +42 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.js +765 -143
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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/
|
|
1429
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
(
|
|
2192
|
+
(resolve4, reject) => {
|
|
1641
2193
|
const entry = {
|
|
1642
2194
|
id: callId,
|
|
1643
2195
|
toolName,
|
|
1644
2196
|
input,
|
|
1645
|
-
resolve:
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
2310
|
+
const outPath = path4.join(
|
|
1759
2311
|
pluginTmpDir(),
|
|
1760
2312
|
`proxy-${hash}.json`
|
|
1761
2313
|
);
|
|
1762
|
-
|
|
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((
|
|
1772
|
-
server2.close(() =>
|
|
2323
|
+
await new Promise((resolve4) => {
|
|
2324
|
+
server2.close(() => resolve4());
|
|
1773
2325
|
});
|
|
1774
2326
|
if (configFilePath) {
|
|
1775
2327
|
try {
|
|
1776
|
-
|
|
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((
|
|
2362
|
+
return new Promise((resolve4, reject) => {
|
|
1811
2363
|
const chunks = [];
|
|
1812
2364
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
1813
|
-
req.on("end", () =>
|
|
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
|
|
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
|
|
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
|
|
1952
|
-
import { unlink as
|
|
1953
|
-
import { homedir as
|
|
1954
|
-
import { randomUUID as
|
|
1955
|
-
import { dirname as dirname3, join as
|
|
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(
|
|
2696
|
+
function readPromptFileIfPresent(path6) {
|
|
2145
2697
|
try {
|
|
2146
|
-
const content =
|
|
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(
|
|
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 ??
|
|
2207
|
-
const globalAgents = readPromptFileIfPresent(
|
|
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
|
|
2767
|
+
const path6 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
|
|
2216
2768
|
try {
|
|
2217
|
-
writeFileSync3(
|
|
2218
|
-
return
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
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
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
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
|
|
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 ?
|
|
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 =
|
|
4396
|
-
const target =
|
|
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 =
|
|
4408
|
-
const resolvedSource =
|
|
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 =
|
|
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 =
|
|
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
|
|
4468
|
-
readFileSync as
|
|
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
|
|
4474
|
-
import { join as
|
|
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 ?
|
|
4483
|
-
|
|
4484
|
-
|
|
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 ??
|
|
4489
|
-
return
|
|
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 (!
|
|
5111
|
+
if (!existsSync4(cfg)) return false;
|
|
4494
5112
|
try {
|
|
4495
|
-
const json = JSON.parse(
|
|
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(
|
|
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 (!
|
|
4532
|
-
const stalePath =
|
|
4533
|
-
if (!
|
|
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 =
|
|
4541
|
-
if (!
|
|
5158
|
+
const pkgJsonPath = join7(stalePath, "package.json");
|
|
5159
|
+
if (!existsSync4(pkgJsonPath)) return;
|
|
4542
5160
|
let pkg = {};
|
|
4543
5161
|
try {
|
|
4544
|
-
pkg = JSON.parse(
|
|
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 =
|
|
4561
|
-
if (!
|
|
5178
|
+
const cachePkgJson = join7(cacheRoot, "package.json");
|
|
5179
|
+
if (!existsSync4(cachePkgJson)) return;
|
|
4562
5180
|
try {
|
|
4563
|
-
const cfg = JSON.parse(
|
|
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) {
|