@khalilgharbaoui/opencode-claude-code-plugin 0.8.1 → 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 +778 -151
- 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;
|
|
@@ -3378,11 +3994,13 @@ ${plan}
|
|
|
3378
3994
|
}
|
|
3379
3995
|
if (block.type === "tool_use" && block.id && block.name) {
|
|
3380
3996
|
noteToolActivity();
|
|
3381
|
-
|
|
3997
|
+
const entry = {
|
|
3382
3998
|
id: block.id,
|
|
3383
3999
|
name: block.name,
|
|
3384
|
-
inputJson: ""
|
|
3385
|
-
|
|
4000
|
+
inputJson: "",
|
|
4001
|
+
started: false
|
|
4002
|
+
};
|
|
4003
|
+
toolCallMap.set(idx, entry);
|
|
3386
4004
|
if (block.name !== "AskUserQuestion" && block.name !== "ask_user_question" && block.name !== "ExitPlanMode" && !block.name.startsWith(PROXY_TOOL_PREFIX)) {
|
|
3387
4005
|
const { name: mappedName, skip, executed } = mapTool(
|
|
3388
4006
|
block.name,
|
|
@@ -3394,6 +4012,7 @@ ${plan}
|
|
|
3394
4012
|
}
|
|
3395
4013
|
);
|
|
3396
4014
|
if (!skip) {
|
|
4015
|
+
entry.started = true;
|
|
3397
4016
|
controller.enqueue({
|
|
3398
4017
|
type: "tool-input-start",
|
|
3399
4018
|
id: block.id,
|
|
@@ -3445,11 +4064,13 @@ ${plan}
|
|
|
3445
4064
|
const tc = toolCallMap.get(idx);
|
|
3446
4065
|
if (tc) {
|
|
3447
4066
|
tc.inputJson += delta.partial_json;
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
4067
|
+
if (tc.started) {
|
|
4068
|
+
controller.enqueue({
|
|
4069
|
+
type: "tool-input-delta",
|
|
4070
|
+
id: tc.id,
|
|
4071
|
+
delta: delta.partial_json
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
3453
4074
|
}
|
|
3454
4075
|
}
|
|
3455
4076
|
if (!KNOWN_DELTA_TYPES.has(delta.type)) {
|
|
@@ -3646,11 +4267,6 @@ ${plan}
|
|
|
3646
4267
|
if (block.type === "tool_use" && block.id && block.name) {
|
|
3647
4268
|
noteToolActivity();
|
|
3648
4269
|
const parsedInput = block.input ?? {};
|
|
3649
|
-
toolCallsById.set(block.id, {
|
|
3650
|
-
id: block.id,
|
|
3651
|
-
name: block.name,
|
|
3652
|
-
input: parsedInput
|
|
3653
|
-
});
|
|
3654
4270
|
if (isAskUserQuestionTool(block.name)) {
|
|
3655
4271
|
const askId = startTextBlock();
|
|
3656
4272
|
controller.enqueue({
|
|
@@ -3703,6 +4319,11 @@ ${plan}
|
|
|
3703
4319
|
toolUseId: block.id
|
|
3704
4320
|
});
|
|
3705
4321
|
if (!skip) {
|
|
4322
|
+
toolCallsById.set(block.id, {
|
|
4323
|
+
id: block.id,
|
|
4324
|
+
name: block.name,
|
|
4325
|
+
input: parsedInput
|
|
4326
|
+
});
|
|
3706
4327
|
if (!executed) skipResultForIds.add(block.id);
|
|
3707
4328
|
controller.enqueue({
|
|
3708
4329
|
type: "tool-input-start",
|
|
@@ -3994,6 +4615,8 @@ ${plan}
|
|
|
3994
4615
|
};
|
|
3995
4616
|
const procErrorHandler = (err) => {
|
|
3996
4617
|
log.error("process error", { error: err.message });
|
|
4618
|
+
deleteActiveProcess(sk);
|
|
4619
|
+
deleteClaudeSessionId(sk);
|
|
3997
4620
|
if (controllerClosed) return;
|
|
3998
4621
|
if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
|
|
3999
4622
|
rejectAllPendingProxyCallsForSession(
|
|
@@ -4317,7 +4940,7 @@ var defaultModels = {
|
|
|
4317
4940
|
|
|
4318
4941
|
// src/accounts.ts
|
|
4319
4942
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
4320
|
-
import
|
|
4943
|
+
import path5 from "path";
|
|
4321
4944
|
var BASE_PROVIDER_ID = "claude-code";
|
|
4322
4945
|
var DEFAULT_ACCOUNT = "default";
|
|
4323
4946
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -4355,7 +4978,7 @@ function expandHome(value) {
|
|
|
4355
4978
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
4356
4979
|
if (value === "~") return home ?? value;
|
|
4357
4980
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
4358
|
-
return home ?
|
|
4981
|
+
return home ? path5.join(home, value.slice(2)) : value;
|
|
4359
4982
|
}
|
|
4360
4983
|
return value;
|
|
4361
4984
|
}
|
|
@@ -4378,7 +5001,7 @@ async function ensureAccountRuntime(account, baseCliPath) {
|
|
|
4378
5001
|
baseCliPath,
|
|
4379
5002
|
expandedConfigDir
|
|
4380
5003
|
);
|
|
4381
|
-
return { cliPath, configDir };
|
|
5004
|
+
return { cliPath, configDir: expandedConfigDir };
|
|
4382
5005
|
}
|
|
4383
5006
|
async function ensureSharedCapabilities(targetRoot) {
|
|
4384
5007
|
const sourceRoot = expandHome("~/.claude");
|
|
@@ -4387,8 +5010,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
4387
5010
|
}
|
|
4388
5011
|
}
|
|
4389
5012
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
4390
|
-
const source =
|
|
4391
|
-
const target =
|
|
5013
|
+
const source = path5.join(sourceRoot, item);
|
|
5014
|
+
const target = path5.join(targetRoot, item);
|
|
4392
5015
|
let sourceStat;
|
|
4393
5016
|
try {
|
|
4394
5017
|
sourceStat = await lstat(source);
|
|
@@ -4399,8 +5022,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
4399
5022
|
const targetStat = await lstat(target);
|
|
4400
5023
|
if (targetStat.isSymbolicLink()) {
|
|
4401
5024
|
const current = await readlink(target);
|
|
4402
|
-
const resolvedCurrent =
|
|
4403
|
-
const resolvedSource =
|
|
5025
|
+
const resolvedCurrent = path5.resolve(path5.dirname(target), current);
|
|
5026
|
+
const resolvedSource = path5.resolve(source);
|
|
4404
5027
|
if (resolvedCurrent === resolvedSource) return;
|
|
4405
5028
|
}
|
|
4406
5029
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -4415,11 +5038,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
4415
5038
|
await symlink(source, target, type);
|
|
4416
5039
|
}
|
|
4417
5040
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
4418
|
-
const cacheRoot =
|
|
5041
|
+
const cacheRoot = path5.join(
|
|
4419
5042
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
4420
5043
|
"opencode-claude-code-plugin"
|
|
4421
5044
|
);
|
|
4422
|
-
const wrapperPath =
|
|
5045
|
+
const wrapperPath = path5.join(cacheRoot, `claude-${account}`);
|
|
4423
5046
|
const suffix = `@${account}`;
|
|
4424
5047
|
await mkdir(cacheRoot, { recursive: true });
|
|
4425
5048
|
const script = `#!/usr/bin/env bash
|
|
@@ -4459,14 +5082,14 @@ function titleizeAccount(account) {
|
|
|
4459
5082
|
|
|
4460
5083
|
// src/cleanup-stale.ts
|
|
4461
5084
|
import {
|
|
4462
|
-
existsSync as
|
|
4463
|
-
readFileSync as
|
|
5085
|
+
existsSync as existsSync4,
|
|
5086
|
+
readFileSync as readFileSync4,
|
|
4464
5087
|
realpathSync,
|
|
4465
5088
|
rmSync as rmSync2,
|
|
4466
5089
|
writeFileSync as writeFileSync4
|
|
4467
5090
|
} from "fs";
|
|
4468
|
-
import { homedir as
|
|
4469
|
-
import { join as
|
|
5091
|
+
import { homedir as homedir5 } from "os";
|
|
5092
|
+
import { join as join7, resolve as resolve3 } from "path";
|
|
4470
5093
|
import { fileURLToPath } from "url";
|
|
4471
5094
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
4472
5095
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -4474,20 +5097,20 @@ var alreadyRan = false;
|
|
|
4474
5097
|
function candidateCacheRoots() {
|
|
4475
5098
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
4476
5099
|
return [
|
|
4477
|
-
xdg ?
|
|
4478
|
-
|
|
4479
|
-
|
|
5100
|
+
xdg ? join7(xdg, "opencode") : null,
|
|
5101
|
+
join7(homedir5(), ".cache", "opencode"),
|
|
5102
|
+
join7(homedir5(), "Library", "Caches", "opencode")
|
|
4480
5103
|
].filter((p) => Boolean(p));
|
|
4481
5104
|
}
|
|
4482
5105
|
function userOpencodeJsonPath() {
|
|
4483
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
4484
|
-
return
|
|
5106
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
|
|
5107
|
+
return join7(xdgConfig, "opencode", "opencode.json");
|
|
4485
5108
|
}
|
|
4486
5109
|
function userIntendsToUseUnscoped() {
|
|
4487
5110
|
const cfg = userOpencodeJsonPath();
|
|
4488
|
-
if (!
|
|
5111
|
+
if (!existsSync4(cfg)) return false;
|
|
4489
5112
|
try {
|
|
4490
|
-
const json = JSON.parse(
|
|
5113
|
+
const json = JSON.parse(readFileSync4(cfg, "utf8"));
|
|
4491
5114
|
const plugins = json.plugin;
|
|
4492
5115
|
if (!Array.isArray(plugins)) return false;
|
|
4493
5116
|
return plugins.some(
|
|
@@ -4500,7 +5123,7 @@ function userIntendsToUseUnscoped() {
|
|
|
4500
5123
|
function ourLoadedDir() {
|
|
4501
5124
|
try {
|
|
4502
5125
|
const filePath = fileURLToPath(import.meta.url);
|
|
4503
|
-
return realpathSync(
|
|
5126
|
+
return realpathSync(resolve3(filePath, "..", ".."));
|
|
4504
5127
|
} catch {
|
|
4505
5128
|
return null;
|
|
4506
5129
|
}
|
|
@@ -4523,20 +5146,20 @@ function cleanupStaleUnscopedInstall() {
|
|
|
4523
5146
|
}
|
|
4524
5147
|
}
|
|
4525
5148
|
function cleanupOne(cacheRoot, ourDir) {
|
|
4526
|
-
if (!
|
|
4527
|
-
const stalePath =
|
|
4528
|
-
if (!
|
|
5149
|
+
if (!existsSync4(cacheRoot)) return;
|
|
5150
|
+
const stalePath = join7(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
5151
|
+
if (!existsSync4(stalePath)) return;
|
|
4529
5152
|
let realStalePath = stalePath;
|
|
4530
5153
|
try {
|
|
4531
5154
|
realStalePath = realpathSync(stalePath);
|
|
4532
5155
|
} catch {
|
|
4533
5156
|
}
|
|
4534
5157
|
if (ourDir && realStalePath === ourDir) return;
|
|
4535
|
-
const pkgJsonPath =
|
|
4536
|
-
if (!
|
|
5158
|
+
const pkgJsonPath = join7(stalePath, "package.json");
|
|
5159
|
+
if (!existsSync4(pkgJsonPath)) return;
|
|
4537
5160
|
let pkg = {};
|
|
4538
5161
|
try {
|
|
4539
|
-
pkg = JSON.parse(
|
|
5162
|
+
pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
|
|
4540
5163
|
} catch {
|
|
4541
5164
|
return;
|
|
4542
5165
|
}
|
|
@@ -4552,10 +5175,10 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
4552
5175
|
});
|
|
4553
5176
|
return;
|
|
4554
5177
|
}
|
|
4555
|
-
const cachePkgJson =
|
|
4556
|
-
if (!
|
|
5178
|
+
const cachePkgJson = join7(cacheRoot, "package.json");
|
|
5179
|
+
if (!existsSync4(cachePkgJson)) return;
|
|
4557
5180
|
try {
|
|
4558
|
-
const cfg = JSON.parse(
|
|
5181
|
+
const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
|
|
4559
5182
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
4560
5183
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
4561
5184
|
writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -4610,7 +5233,11 @@ function createClaudeCode(settings = {}) {
|
|
|
4610
5233
|
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
|
|
4611
5234
|
multiStepContinuation: settings.multiStepContinuation ?? true,
|
|
4612
5235
|
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
|
|
4613
|
-
compactionModel: settings.compactionModel
|
|
5236
|
+
compactionModel: settings.compactionModel,
|
|
5237
|
+
interactive: settings.interactive,
|
|
5238
|
+
interactiveBypass: settings.interactiveBypass,
|
|
5239
|
+
interactiveAllowTools: settings.interactiveAllowTools,
|
|
5240
|
+
interactiveSystemPrompt: settings.interactiveSystemPrompt
|
|
4614
5241
|
});
|
|
4615
5242
|
};
|
|
4616
5243
|
const provider = function(modelId) {
|