@khalilgharbaoui/opencode-claude-code-plugin 0.45.0 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +27 -0
- package/dist/index.js +640 -225
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +28 -5
package/dist/index.js
CHANGED
|
@@ -916,6 +916,10 @@ function formatResultFailureNote(message) {
|
|
|
916
916
|
${RESULT_ERROR_MARKER} ${message}
|
|
917
917
|
`;
|
|
918
918
|
}
|
|
919
|
+
function formatCompactionFailure(cause, detail) {
|
|
920
|
+
const said = detail?.trim();
|
|
921
|
+
return `Claude Code could not compact this conversation (${cause}), so no summary was produced.` + (said ? ` The CLI said: ${said}` : "");
|
|
922
|
+
}
|
|
919
923
|
var STREAM_TIMEOUT_MARKER = "\u258C **stream timeout:**";
|
|
920
924
|
function formatStreamTimeoutNote(silenceMs) {
|
|
921
925
|
const seconds = Math.max(1, Math.round(silenceMs / 1e3));
|
|
@@ -1621,7 +1625,108 @@ function failoverUntil(resetsAt) {
|
|
|
1621
1625
|
// src/cli-version.ts
|
|
1622
1626
|
import { execFile } from "child_process";
|
|
1623
1627
|
import { promisify } from "util";
|
|
1628
|
+
|
|
1629
|
+
// src/windows-spawn.ts
|
|
1630
|
+
import * as fs from "fs";
|
|
1631
|
+
import * as path2 from "path";
|
|
1632
|
+
var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
1633
|
+
function envValue(env, name) {
|
|
1634
|
+
const direct = env[name];
|
|
1635
|
+
if (direct !== void 0) return direct;
|
|
1636
|
+
const match = Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase());
|
|
1637
|
+
return match === void 0 ? void 0 : env[match];
|
|
1638
|
+
}
|
|
1639
|
+
function defaultIsFile(candidate) {
|
|
1640
|
+
try {
|
|
1641
|
+
return fs.statSync(candidate).isFile();
|
|
1642
|
+
} catch {
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function executableExtensions(env = process.env) {
|
|
1647
|
+
const raw = envValue(env, "PATHEXT") ?? DEFAULT_PATHEXT;
|
|
1648
|
+
return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
|
|
1649
|
+
}
|
|
1650
|
+
function isBatchFile(file) {
|
|
1651
|
+
const ext = path2.win32.extname(file).toLowerCase();
|
|
1652
|
+
return ext === ".cmd" || ext === ".bat";
|
|
1653
|
+
}
|
|
1654
|
+
function resolveWindowsCommand(command, deps = {}) {
|
|
1655
|
+
const env = deps.env ?? process.env;
|
|
1656
|
+
const isFile = deps.isFile ?? defaultIsFile;
|
|
1657
|
+
const extensions = executableExtensions(env);
|
|
1658
|
+
const candidatesFor = (base) => {
|
|
1659
|
+
const ext = path2.win32.extname(base).toLowerCase();
|
|
1660
|
+
if (ext && extensions.includes(ext)) return [base];
|
|
1661
|
+
return [...extensions.map((candidate) => base + candidate), base];
|
|
1662
|
+
};
|
|
1663
|
+
if (command.includes("\\") || command.includes("/")) {
|
|
1664
|
+
for (const candidate of candidatesFor(command)) {
|
|
1665
|
+
if (isFile(candidate)) return candidate;
|
|
1666
|
+
}
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
const pathValue = envValue(env, "PATH") ?? "";
|
|
1670
|
+
for (const entry of pathValue.split(path2.win32.delimiter)) {
|
|
1671
|
+
const directory = entry.trim().replace(/^"(.*)"$/, "$1");
|
|
1672
|
+
if (!directory) continue;
|
|
1673
|
+
for (const candidate of candidatesFor(path2.win32.join(directory, command))) {
|
|
1674
|
+
if (isFile(candidate)) return candidate;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
function quoteBatchArgument(arg) {
|
|
1680
|
+
if (/[\r\n]/.test(arg)) {
|
|
1681
|
+
throw new Error("cmd.exe cannot carry a newline in an argument");
|
|
1682
|
+
}
|
|
1683
|
+
let out = '"';
|
|
1684
|
+
let backslashes = 0;
|
|
1685
|
+
for (const char of arg) {
|
|
1686
|
+
if (char === "\\") {
|
|
1687
|
+
backslashes += 1;
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
if (char === '"') {
|
|
1691
|
+
out += "\\".repeat(backslashes * 2) + '""';
|
|
1692
|
+
backslashes = 0;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
out += "\\".repeat(backslashes) + char;
|
|
1696
|
+
backslashes = 0;
|
|
1697
|
+
}
|
|
1698
|
+
return `${out}${"\\".repeat(backslashes * 2)}"`;
|
|
1699
|
+
}
|
|
1700
|
+
function planCmdInvocation(file, args, env = process.env) {
|
|
1701
|
+
const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
|
|
1702
|
+
const line = [`"${path2.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
|
|
1703
|
+
return {
|
|
1704
|
+
file: comspec,
|
|
1705
|
+
args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
|
|
1706
|
+
windowsVerbatimArguments: true
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function planClaudeSpawn(command, args, deps = {}) {
|
|
1710
|
+
const platform2 = deps.platform ?? process.platform;
|
|
1711
|
+
if (platform2 !== "win32") return { file: command, args: [...args] };
|
|
1712
|
+
const env = deps.env ?? process.env;
|
|
1713
|
+
const resolved = resolveWindowsCommand(command, deps) ?? command;
|
|
1714
|
+
if (isBatchFile(resolved)) return planCmdInvocation(resolved, args, env);
|
|
1715
|
+
return { file: resolved, args: [...args] };
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/cli-version.ts
|
|
1624
1719
|
var execFileAsync = promisify(execFile);
|
|
1720
|
+
function probeExec(cliPath, args, options) {
|
|
1721
|
+
const plan = planClaudeSpawn(cliPath, args);
|
|
1722
|
+
return execFileAsync(plan.file, plan.args, {
|
|
1723
|
+
...options,
|
|
1724
|
+
// utf8 is already `execFile`'s default; naming it is what keeps the
|
|
1725
|
+
// promisified overload returning strings rather than Buffers.
|
|
1726
|
+
encoding: "utf8",
|
|
1727
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1625
1730
|
var cache = /* @__PURE__ */ new Map();
|
|
1626
1731
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
1627
1732
|
var probeTimeoutMs = PROBE_TIMEOUT_MS;
|
|
@@ -1668,7 +1773,7 @@ function detectCliVersion(cliPath) {
|
|
|
1668
1773
|
let deadlineKill = false;
|
|
1669
1774
|
const promise = (async () => {
|
|
1670
1775
|
try {
|
|
1671
|
-
const { stdout } = await
|
|
1776
|
+
const { stdout } = await probeExec(cliPath, ["--version"], {
|
|
1672
1777
|
timeout: probeTimeoutMs
|
|
1673
1778
|
});
|
|
1674
1779
|
const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
|
|
@@ -1765,7 +1870,7 @@ function detectHeadlessSupport(cliPath) {
|
|
|
1765
1870
|
let deadlineKill = false;
|
|
1766
1871
|
const promise = (async () => {
|
|
1767
1872
|
try {
|
|
1768
|
-
const execution =
|
|
1873
|
+
const execution = probeExec(cliPath, [
|
|
1769
1874
|
"--print",
|
|
1770
1875
|
"--input-format",
|
|
1771
1876
|
"stream-json",
|
|
@@ -1801,7 +1906,7 @@ function detectCliSupportsFlag(cliPath, flag) {
|
|
|
1801
1906
|
let deadlineKill = false;
|
|
1802
1907
|
const promise = (async () => {
|
|
1803
1908
|
try {
|
|
1804
|
-
const execution =
|
|
1909
|
+
const execution = probeExec(cliPath, ["--help"], {
|
|
1805
1910
|
timeout: probeTimeoutMs,
|
|
1806
1911
|
killSignal: "SIGKILL",
|
|
1807
1912
|
maxBuffer: 4 * 1024 * 1024
|
|
@@ -1834,17 +1939,17 @@ import { unlink } from "fs/promises";
|
|
|
1834
1939
|
|
|
1835
1940
|
// src/proxy-mcp.ts
|
|
1836
1941
|
import { createServer } from "http";
|
|
1837
|
-
import * as
|
|
1838
|
-
import * as
|
|
1942
|
+
import * as fs3 from "fs";
|
|
1943
|
+
import * as path4 from "path";
|
|
1839
1944
|
import * as crypto from "crypto";
|
|
1840
1945
|
import { EventEmitter } from "events";
|
|
1841
1946
|
|
|
1842
1947
|
// src/tmp.ts
|
|
1843
|
-
import * as
|
|
1948
|
+
import * as fs2 from "fs";
|
|
1844
1949
|
import * as os from "os";
|
|
1845
|
-
import * as
|
|
1950
|
+
import * as path3 from "path";
|
|
1846
1951
|
var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
|
|
1847
|
-
var PID_TMP_DIR =
|
|
1952
|
+
var PID_TMP_DIR = path3.join(
|
|
1848
1953
|
os.tmpdir(),
|
|
1849
1954
|
`${PLUGIN_TMP_PREFIX}${process.pid}`
|
|
1850
1955
|
);
|
|
@@ -1857,7 +1962,7 @@ function currentUid() {
|
|
|
1857
1962
|
function untrustworthyReason(dir) {
|
|
1858
1963
|
let stat;
|
|
1859
1964
|
try {
|
|
1860
|
-
stat =
|
|
1965
|
+
stat = fs2.lstatSync(dir);
|
|
1861
1966
|
} catch {
|
|
1862
1967
|
return null;
|
|
1863
1968
|
}
|
|
@@ -1870,16 +1975,16 @@ function untrustworthyReason(dir) {
|
|
|
1870
1975
|
return null;
|
|
1871
1976
|
}
|
|
1872
1977
|
function ensureDir(dir) {
|
|
1873
|
-
|
|
1978
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1874
1979
|
try {
|
|
1875
|
-
|
|
1980
|
+
fs2.chmodSync(dir, 448);
|
|
1876
1981
|
} catch {
|
|
1877
1982
|
}
|
|
1878
1983
|
}
|
|
1879
1984
|
function chooseDir() {
|
|
1880
1985
|
const reason = untrustworthyReason(PID_TMP_DIR);
|
|
1881
1986
|
if (!reason) return PID_TMP_DIR;
|
|
1882
|
-
const fallback =
|
|
1987
|
+
const fallback = fs2.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
|
|
1883
1988
|
log.warn("refusing the pid-named scratch directory", {
|
|
1884
1989
|
path: PID_TMP_DIR,
|
|
1885
1990
|
reason,
|
|
@@ -1893,7 +1998,7 @@ function registerExitCleanup() {
|
|
|
1893
1998
|
process.on("exit", () => {
|
|
1894
1999
|
for (const dir of createdDirs) {
|
|
1895
2000
|
try {
|
|
1896
|
-
|
|
2001
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
1897
2002
|
} catch {
|
|
1898
2003
|
}
|
|
1899
2004
|
}
|
|
@@ -1914,23 +2019,23 @@ function sweepStalePluginTmpDirs(keep) {
|
|
|
1914
2019
|
const root = os.tmpdir();
|
|
1915
2020
|
let entries;
|
|
1916
2021
|
try {
|
|
1917
|
-
entries =
|
|
2022
|
+
entries = fs2.readdirSync(root);
|
|
1918
2023
|
} catch {
|
|
1919
2024
|
return removed;
|
|
1920
2025
|
}
|
|
1921
2026
|
const uid = currentUid();
|
|
1922
|
-
const keepPath = keep ?
|
|
2027
|
+
const keepPath = keep ? path3.resolve(keep) : null;
|
|
1923
2028
|
for (const entry of entries) {
|
|
1924
2029
|
const match = PID_DIR_NAME.exec(entry);
|
|
1925
2030
|
if (!match) continue;
|
|
1926
2031
|
const pid = Number(match[1]);
|
|
1927
2032
|
if (!Number.isSafeInteger(pid) || pid <= 0) continue;
|
|
1928
2033
|
if (pid === process.pid) continue;
|
|
1929
|
-
const dir =
|
|
1930
|
-
if (keepPath &&
|
|
2034
|
+
const dir = path3.join(root, entry);
|
|
2035
|
+
if (keepPath && path3.resolve(dir) === keepPath) continue;
|
|
1931
2036
|
let stat;
|
|
1932
2037
|
try {
|
|
1933
|
-
stat =
|
|
2038
|
+
stat = fs2.lstatSync(dir);
|
|
1934
2039
|
} catch {
|
|
1935
2040
|
continue;
|
|
1936
2041
|
}
|
|
@@ -1938,7 +2043,7 @@ function sweepStalePluginTmpDirs(keep) {
|
|
|
1938
2043
|
if (uid !== void 0 && stat.uid !== uid) continue;
|
|
1939
2044
|
if (isProcessAlive(pid)) continue;
|
|
1940
2045
|
try {
|
|
1941
|
-
|
|
2046
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
1942
2047
|
removed.push(dir);
|
|
1943
2048
|
} catch (err) {
|
|
1944
2049
|
log.debug("could not remove a stale scratch directory", {
|
|
@@ -2769,11 +2874,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2769
2874
|
2
|
|
2770
2875
|
);
|
|
2771
2876
|
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
2772
|
-
const outPath =
|
|
2877
|
+
const outPath = path4.join(
|
|
2773
2878
|
pluginTmpDir(),
|
|
2774
2879
|
`proxy-${hash}.json`
|
|
2775
2880
|
);
|
|
2776
|
-
|
|
2881
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
2777
2882
|
configFilePath = outPath;
|
|
2778
2883
|
return outPath;
|
|
2779
2884
|
},
|
|
@@ -2790,7 +2895,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2790
2895
|
});
|
|
2791
2896
|
if (configFilePath) {
|
|
2792
2897
|
try {
|
|
2793
|
-
|
|
2898
|
+
fs3.unlinkSync(configFilePath);
|
|
2794
2899
|
} catch {
|
|
2795
2900
|
}
|
|
2796
2901
|
configFilePath = null;
|
|
@@ -3359,8 +3464,8 @@ function clearCompression(sessionKey2) {
|
|
|
3359
3464
|
}
|
|
3360
3465
|
|
|
3361
3466
|
// src/mcp-bridge.ts
|
|
3362
|
-
import * as
|
|
3363
|
-
import * as
|
|
3467
|
+
import * as fs4 from "fs";
|
|
3468
|
+
import * as path5 from "path";
|
|
3364
3469
|
import * as os2 from "os";
|
|
3365
3470
|
import * as crypto2 from "crypto";
|
|
3366
3471
|
import {
|
|
@@ -3371,21 +3476,21 @@ var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
|
3371
3476
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3372
3477
|
function fileExists(p) {
|
|
3373
3478
|
try {
|
|
3374
|
-
return
|
|
3479
|
+
return fs4.statSync(p).isFile();
|
|
3375
3480
|
} catch {
|
|
3376
3481
|
return false;
|
|
3377
3482
|
}
|
|
3378
3483
|
}
|
|
3379
3484
|
function dirExists(p) {
|
|
3380
3485
|
try {
|
|
3381
|
-
return
|
|
3486
|
+
return fs4.statSync(p).isDirectory();
|
|
3382
3487
|
} catch {
|
|
3383
3488
|
return false;
|
|
3384
3489
|
}
|
|
3385
3490
|
}
|
|
3386
3491
|
function readAndParse(file) {
|
|
3387
3492
|
try {
|
|
3388
|
-
const raw =
|
|
3493
|
+
const raw = fs4.readFileSync(file, "utf8");
|
|
3389
3494
|
const errors = [];
|
|
3390
3495
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3391
3496
|
if (errors.length > 0) {
|
|
@@ -3421,14 +3526,14 @@ function deepMerge(target, source) {
|
|
|
3421
3526
|
}
|
|
3422
3527
|
function walkUp(opts) {
|
|
3423
3528
|
const out = [];
|
|
3424
|
-
let current =
|
|
3529
|
+
let current = path5.resolve(opts.start);
|
|
3425
3530
|
while (true) {
|
|
3426
3531
|
for (const target of opts.targets) {
|
|
3427
|
-
const candidate =
|
|
3532
|
+
const candidate = path5.join(current, target);
|
|
3428
3533
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3429
3534
|
}
|
|
3430
|
-
if (opts.stop && current ===
|
|
3431
|
-
const parent =
|
|
3535
|
+
if (opts.stop && current === path5.resolve(opts.stop)) break;
|
|
3536
|
+
const parent = path5.dirname(current);
|
|
3432
3537
|
if (parent === current) break;
|
|
3433
3538
|
current = parent;
|
|
3434
3539
|
}
|
|
@@ -3436,28 +3541,28 @@ function walkUp(opts) {
|
|
|
3436
3541
|
}
|
|
3437
3542
|
function detectWorktree(cwd) {
|
|
3438
3543
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3439
|
-
if (override) return
|
|
3440
|
-
let current =
|
|
3544
|
+
if (override) return path5.resolve(override);
|
|
3545
|
+
let current = path5.resolve(cwd);
|
|
3441
3546
|
while (true) {
|
|
3442
|
-
const gitPath =
|
|
3547
|
+
const gitPath = path5.join(current, ".git");
|
|
3443
3548
|
try {
|
|
3444
|
-
if (
|
|
3549
|
+
if (fs4.existsSync(gitPath)) return current;
|
|
3445
3550
|
} catch {
|
|
3446
3551
|
}
|
|
3447
|
-
const parent =
|
|
3552
|
+
const parent = path5.dirname(current);
|
|
3448
3553
|
if (parent === current) return void 0;
|
|
3449
3554
|
current = parent;
|
|
3450
3555
|
}
|
|
3451
3556
|
}
|
|
3452
3557
|
function globalConfigDir() {
|
|
3453
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3454
|
-
return
|
|
3558
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os2.homedir(), ".config");
|
|
3559
|
+
return path5.join(xdg, "opencode");
|
|
3455
3560
|
}
|
|
3456
3561
|
function loadGlobalConfig(hostApi = "v1") {
|
|
3457
3562
|
const dir = globalConfigDir();
|
|
3458
3563
|
let merged = {};
|
|
3459
3564
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3460
|
-
const file =
|
|
3565
|
+
const file = path5.join(dir, name);
|
|
3461
3566
|
if (!fileExists(file)) continue;
|
|
3462
3567
|
const parsed = readAndParse(file);
|
|
3463
3568
|
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
@@ -3467,7 +3572,7 @@ function loadGlobalConfig(hostApi = "v1") {
|
|
|
3467
3572
|
function loadProjectFilesInDir(dir, hostApi = "v1") {
|
|
3468
3573
|
let merged = {};
|
|
3469
3574
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3470
|
-
const file =
|
|
3575
|
+
const file = path5.join(dir, name);
|
|
3471
3576
|
if (!fileExists(file)) continue;
|
|
3472
3577
|
const parsed = readAndParse(file);
|
|
3473
3578
|
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
@@ -3478,7 +3583,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3478
3583
|
const dirs = [];
|
|
3479
3584
|
const seen = /* @__PURE__ */ new Set();
|
|
3480
3585
|
const push = (p) => {
|
|
3481
|
-
const abs =
|
|
3586
|
+
const abs = path5.resolve(p);
|
|
3482
3587
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
3483
3588
|
seen.add(abs);
|
|
3484
3589
|
dirs.push(abs);
|
|
@@ -3494,7 +3599,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3494
3599
|
}
|
|
3495
3600
|
const home = os2.homedir();
|
|
3496
3601
|
if (home) {
|
|
3497
|
-
const homeDot =
|
|
3602
|
+
const homeDot = path5.join(home, ".opencode");
|
|
3498
3603
|
if (dirExists(homeDot)) push(homeDot);
|
|
3499
3604
|
}
|
|
3500
3605
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -3641,7 +3746,7 @@ function opencodeConfigLayers(cwd, hostApi = "v1") {
|
|
|
3641
3746
|
const projectDirs = [];
|
|
3642
3747
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3643
3748
|
for (const f of projectFiles) {
|
|
3644
|
-
const d =
|
|
3749
|
+
const d = path5.dirname(f);
|
|
3645
3750
|
if (!seenProjectDirs.has(d)) {
|
|
3646
3751
|
seenProjectDirs.add(d);
|
|
3647
3752
|
projectDirs.push(d);
|
|
@@ -3708,13 +3813,13 @@ function finishBridge(input) {
|
|
|
3708
3813
|
}
|
|
3709
3814
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3710
3815
|
const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3711
|
-
const outPath =
|
|
3816
|
+
const outPath = path5.join(
|
|
3712
3817
|
pluginTmpDir(),
|
|
3713
3818
|
`mcp-${bodyDigest}.json`
|
|
3714
3819
|
);
|
|
3715
3820
|
try {
|
|
3716
3821
|
if (!fileExists(outPath)) {
|
|
3717
|
-
|
|
3822
|
+
fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3718
3823
|
}
|
|
3719
3824
|
} catch (e) {
|
|
3720
3825
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -4250,7 +4355,8 @@ function messageDigest(msg) {
|
|
|
4250
4355
|
}
|
|
4251
4356
|
return hash.digest("hex").slice(0, 32);
|
|
4252
4357
|
}
|
|
4253
|
-
|
|
4358
|
+
var ASSISTANT_TURN_DIGEST = "assistant-turn";
|
|
4359
|
+
function conversationDigests(prompt, opts) {
|
|
4254
4360
|
const out = [];
|
|
4255
4361
|
for (const msg of prompt) {
|
|
4256
4362
|
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
|
|
@@ -4258,7 +4364,7 @@ function conversationDigests(prompt) {
|
|
|
4258
4364
|
}
|
|
4259
4365
|
out.push({
|
|
4260
4366
|
role: msg.role,
|
|
4261
|
-
digest: messageDigest(msg)
|
|
4367
|
+
digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg)
|
|
4262
4368
|
});
|
|
4263
4369
|
}
|
|
4264
4370
|
return out;
|
|
@@ -4285,7 +4391,24 @@ function forkSiblingSignature(sessionKey2) {
|
|
|
4285
4391
|
blanked[3] = "*";
|
|
4286
4392
|
return blanked.join("::");
|
|
4287
4393
|
}
|
|
4288
|
-
function
|
|
4394
|
+
function modelSiblingSignature(sessionKey2) {
|
|
4395
|
+
const parts = sessionKey2.split("::");
|
|
4396
|
+
if (parts.length < 5) return null;
|
|
4397
|
+
if (parts[2] === "compaction") return null;
|
|
4398
|
+
const withoutEffort = parts[parts.length - 1].startsWith("effort=") ? parts.slice(0, -1) : parts;
|
|
4399
|
+
if (withoutEffort.length < 5) return null;
|
|
4400
|
+
const blanked = [...withoutEffort];
|
|
4401
|
+
blanked[1] = "*";
|
|
4402
|
+
return blanked.join("::");
|
|
4403
|
+
}
|
|
4404
|
+
function moveForkFingerprint(fromKey, toKey) {
|
|
4405
|
+
const record = fingerprints.get(fromKey);
|
|
4406
|
+
if (!record) return;
|
|
4407
|
+
fingerprints.delete(fromKey);
|
|
4408
|
+
fingerprints.delete(toKey);
|
|
4409
|
+
fingerprints.set(toKey, { ...record, seq: ++fingerprintSeq });
|
|
4410
|
+
}
|
|
4411
|
+
function splitForkHistory(prompt, opts) {
|
|
4289
4412
|
const messages = prompt;
|
|
4290
4413
|
let lastAssistant = -1;
|
|
4291
4414
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -4304,7 +4427,7 @@ function splitForkHistory(prompt) {
|
|
|
4304
4427
|
}
|
|
4305
4428
|
}
|
|
4306
4429
|
return {
|
|
4307
|
-
history: conversationDigests(messages.slice(0, lastAssistant + 1)),
|
|
4430
|
+
history: conversationDigests(messages.slice(0, lastAssistant + 1), opts),
|
|
4308
4431
|
forkable: true
|
|
4309
4432
|
};
|
|
4310
4433
|
}
|
|
@@ -4343,8 +4466,8 @@ function findForkParent(opts) {
|
|
|
4343
4466
|
}
|
|
4344
4467
|
return best;
|
|
4345
4468
|
}
|
|
4346
|
-
function continuesRecordedConversation(recorded, prompt) {
|
|
4347
|
-
const { history, forkable } = splitForkHistory(prompt);
|
|
4469
|
+
function continuesRecordedConversation(recorded, prompt, opts) {
|
|
4470
|
+
const { history, forkable } = splitForkHistory(prompt, opts);
|
|
4348
4471
|
if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
|
|
4349
4472
|
return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
|
|
4350
4473
|
}
|
|
@@ -4381,7 +4504,7 @@ function isDigest(value) {
|
|
|
4381
4504
|
}
|
|
4382
4505
|
function isRecord2(value) {
|
|
4383
4506
|
const record = value;
|
|
4384
|
-
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest);
|
|
4507
|
+
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest));
|
|
4385
4508
|
}
|
|
4386
4509
|
function load() {
|
|
4387
4510
|
if (records) return records;
|
|
@@ -4430,13 +4553,14 @@ function save(update) {
|
|
|
4430
4553
|
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
|
|
4431
4554
|
const chain = conversationDigests(prompt);
|
|
4432
4555
|
if (chain.length === 0) return;
|
|
4556
|
+
const shape2 = conversationDigests(prompt, { assistantContent: false });
|
|
4433
4557
|
const current = load().get(sessionKey2);
|
|
4434
|
-
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4558
|
+
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4435
4559
|
return;
|
|
4436
4560
|
}
|
|
4437
4561
|
save((merged) => {
|
|
4438
4562
|
merged.delete(sessionKey2);
|
|
4439
|
-
merged.set(sessionKey2, { claudeSessionId, cliPath, chain, updatedAt: Date.now() });
|
|
4563
|
+
merged.set(sessionKey2, { claudeSessionId, cliPath, chain, shape: shape2, updatedAt: Date.now() });
|
|
4440
4564
|
});
|
|
4441
4565
|
}
|
|
4442
4566
|
function forgetResumePoint(sessionKey2) {
|
|
@@ -4445,25 +4569,84 @@ function forgetResumePoint(sessionKey2) {
|
|
|
4445
4569
|
merged.delete(sessionKey2);
|
|
4446
4570
|
});
|
|
4447
4571
|
}
|
|
4572
|
+
function moveResumePoint(fromKey, toKey) {
|
|
4573
|
+
const record = load().get(fromKey);
|
|
4574
|
+
if (!record) return;
|
|
4575
|
+
save((merged) => {
|
|
4576
|
+
merged.delete(fromKey);
|
|
4577
|
+
if (merged.has(toKey)) return;
|
|
4578
|
+
merged.set(toKey, { ...record, updatedAt: Date.now() });
|
|
4579
|
+
});
|
|
4580
|
+
}
|
|
4448
4581
|
function findResumePoint(opts) {
|
|
4449
4582
|
const record = load().get(opts.sessionKey);
|
|
4450
|
-
if (!record)
|
|
4583
|
+
if (!record) {
|
|
4584
|
+
opts.onRefused?.("no-session-recorded");
|
|
4585
|
+
return void 0;
|
|
4586
|
+
}
|
|
4451
4587
|
const refuse = (reason) => {
|
|
4452
4588
|
log.info("not resuming the claude session from before the restart", {
|
|
4453
4589
|
sessionKey: opts.sessionKey,
|
|
4454
4590
|
reason
|
|
4455
4591
|
});
|
|
4592
|
+
opts.onRefused?.(reason);
|
|
4456
4593
|
return void 0;
|
|
4457
4594
|
};
|
|
4458
|
-
if (record.cliPath !== opts.cliPath) return refuse("another
|
|
4595
|
+
if (record.cliPath !== opts.cliPath) return refuse("another-claude-binary");
|
|
4459
4596
|
if (!continuesRecordedConversation(record.chain, opts.prompt)) {
|
|
4460
|
-
return refuse("
|
|
4597
|
+
return refuse("conversation-changed");
|
|
4461
4598
|
}
|
|
4462
4599
|
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4463
|
-
return refuse("
|
|
4600
|
+
return refuse("transcript-gone");
|
|
4464
4601
|
}
|
|
4465
4602
|
return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
|
|
4466
4603
|
}
|
|
4604
|
+
function findSiblingResumePoint(opts) {
|
|
4605
|
+
const signature = modelSiblingSignature(opts.sessionKey);
|
|
4606
|
+
if (!signature) {
|
|
4607
|
+
opts.onRefused?.("no-sibling-possible");
|
|
4608
|
+
return void 0;
|
|
4609
|
+
}
|
|
4610
|
+
let best;
|
|
4611
|
+
let bestUpdatedAt = -1;
|
|
4612
|
+
let refusal;
|
|
4613
|
+
for (const [key, record] of load()) {
|
|
4614
|
+
if (key === opts.sessionKey) continue;
|
|
4615
|
+
if (modelSiblingSignature(key) !== signature) continue;
|
|
4616
|
+
if (record.cliPath !== opts.cliPath) {
|
|
4617
|
+
refusal = "sibling-another-claude-binary";
|
|
4618
|
+
continue;
|
|
4619
|
+
}
|
|
4620
|
+
if (!record.shape || !continuesRecordedConversation(record.shape, opts.prompt, {
|
|
4621
|
+
assistantContent: false
|
|
4622
|
+
})) {
|
|
4623
|
+
refusal = "sibling-conversation-changed";
|
|
4624
|
+
continue;
|
|
4625
|
+
}
|
|
4626
|
+
if (opts.isBusy(key)) {
|
|
4627
|
+
refusal = "sibling-still-writing";
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4631
|
+
refusal = "sibling-transcript-gone";
|
|
4632
|
+
continue;
|
|
4633
|
+
}
|
|
4634
|
+
if (record.updatedAt >= bestUpdatedAt) {
|
|
4635
|
+
bestUpdatedAt = record.updatedAt;
|
|
4636
|
+
best = {
|
|
4637
|
+
claudeSessionId: record.claudeSessionId,
|
|
4638
|
+
siblingKey: key,
|
|
4639
|
+
matched: record.chain.length
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
if (!best) {
|
|
4644
|
+
opts.onRefused?.(
|
|
4645
|
+
refusal ?? "no-sibling-recorded"
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
return best;
|
|
4649
|
+
}
|
|
4467
4650
|
|
|
4468
4651
|
// src/permission-presets.ts
|
|
4469
4652
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
@@ -5164,6 +5347,23 @@ function deleteClaudeSessionId(key) {
|
|
|
5164
5347
|
forgetForkFingerprint(key);
|
|
5165
5348
|
forgetResumePoint(key);
|
|
5166
5349
|
}
|
|
5350
|
+
function transferClaudeSession(fromKey, toKey) {
|
|
5351
|
+
if (fromKey === toKey) return void 0;
|
|
5352
|
+
const claudeSessionId = claudeSessions.get(fromKey);
|
|
5353
|
+
if (!claudeSessionId) return void 0;
|
|
5354
|
+
if (claudeSessions.has(toKey)) return void 0;
|
|
5355
|
+
claudeSessions.delete(fromKey);
|
|
5356
|
+
setClaudeSessionId(toKey, claudeSessionId);
|
|
5357
|
+
moveResumePoint(fromKey, toKey);
|
|
5358
|
+
moveForkFingerprint(fromKey, toKey);
|
|
5359
|
+
clearExitPlanModeQuestions(fromKey);
|
|
5360
|
+
clearAccountFailoverQuestions(fromKey);
|
|
5361
|
+
log.notice("carrying the claude conversation over to the new session key", {
|
|
5362
|
+
fromKey,
|
|
5363
|
+
toKey
|
|
5364
|
+
});
|
|
5365
|
+
return claudeSessionId;
|
|
5366
|
+
}
|
|
5167
5367
|
function effortSessionKey(baseKey, effort) {
|
|
5168
5368
|
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
5169
5369
|
}
|
|
@@ -5186,8 +5386,14 @@ function invalidateOtherEffortSessions(baseKey, effort) {
|
|
|
5186
5386
|
);
|
|
5187
5387
|
}
|
|
5188
5388
|
}
|
|
5389
|
+
const currentKey = effortSessionKey(baseKey, effort);
|
|
5189
5390
|
for (const key of staleKeys) {
|
|
5190
5391
|
deleteActiveProcess(key);
|
|
5392
|
+
if (transferClaudeSession(key, currentKey)) {
|
|
5393
|
+
clearCompression(key);
|
|
5394
|
+
clearBackgroundTasks(key);
|
|
5395
|
+
continue;
|
|
5396
|
+
}
|
|
5191
5397
|
deleteClaudeSessionId(key);
|
|
5192
5398
|
clearCompression(key);
|
|
5193
5399
|
clearBackgroundTasks(key);
|
|
@@ -5203,11 +5409,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5203
5409
|
effort,
|
|
5204
5410
|
promptCacheTtl
|
|
5205
5411
|
});
|
|
5206
|
-
const
|
|
5412
|
+
const plan = planClaudeSpawn(cliPath, cliArgs);
|
|
5413
|
+
const proc = spawn(plan.file, plan.args, {
|
|
5207
5414
|
cwd,
|
|
5208
5415
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5209
5416
|
env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
|
|
5210
|
-
|
|
5417
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
5211
5418
|
});
|
|
5212
5419
|
const lineEmitter = new EventEmitter3();
|
|
5213
5420
|
const ap = {
|
|
@@ -5270,12 +5477,11 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5270
5477
|
}
|
|
5271
5478
|
}
|
|
5272
5479
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
5273
|
-
log.info("process exited with error
|
|
5480
|
+
log.info("process exited with error; keeping the claude session to resume", {
|
|
5274
5481
|
code,
|
|
5275
|
-
sessionKey: sessionKey2
|
|
5482
|
+
sessionKey: sessionKey2,
|
|
5483
|
+
resumable: claudeSessions.has(sessionKey2)
|
|
5276
5484
|
});
|
|
5277
|
-
claudeSessions.delete(sessionKey2);
|
|
5278
|
-
forgetResumePoint(sessionKey2);
|
|
5279
5485
|
}
|
|
5280
5486
|
});
|
|
5281
5487
|
proc.stderr?.on("data", (data) => {
|
|
@@ -5788,6 +5994,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5788
5994
|
"btw: a turn is running but no claude process appeared for it",
|
|
5789
5995
|
"btw: early aside failed; the message will ask again",
|
|
5790
5996
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
5997
|
+
"carrying the claude conversation over to the new session key",
|
|
5791
5998
|
"claude --version output unparseable",
|
|
5792
5999
|
"claude account is out of usage; the turn ends with a note",
|
|
5793
6000
|
"claude already registers a different skill under this name; not bridging ours",
|
|
@@ -5795,6 +6002,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5795
6002
|
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
5796
6003
|
"claude code compacted its own context",
|
|
5797
6004
|
"claude code reset its conversation",
|
|
6005
|
+
"claude could not compact the conversation; no summary was stored",
|
|
5798
6006
|
"claude finished the turn without a reply",
|
|
5799
6007
|
"claude kept missing the probe deadline; keeping the conservative answer for this process",
|
|
5800
6008
|
"claude process belongs to another account; starting fresh",
|
|
@@ -5810,6 +6018,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5810
6018
|
"cleanup-stale: could not record the sweep marker",
|
|
5811
6019
|
"cleanup-stale: error processing cache root",
|
|
5812
6020
|
"cleanup-stale: rmSync failed",
|
|
6021
|
+
"continuing this conversation's claude session under the new model or effort instead of replaying it",
|
|
5813
6022
|
"delivering proxy results after interrupted continuation",
|
|
5814
6023
|
"draining proxy calls queued between turns",
|
|
5815
6024
|
"empty user content; sending sentinel to satisfy CLI",
|
|
@@ -5881,6 +6090,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5881
6090
|
"rejected pending proxy call",
|
|
5882
6091
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
5883
6092
|
"replaying stdout the child emitted between turns",
|
|
6093
|
+
"replaying the conversation as text: no claude session was available to continue",
|
|
5884
6094
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
5885
6095
|
"resuming the claude session from before the restart instead of replaying it",
|
|
5886
6096
|
"shared Claude capability already exists; leaving untouched",
|
|
@@ -5936,6 +6146,7 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
5936
6146
|
inFlight: "flag",
|
|
5937
6147
|
anthropicApiKeyInEnv: "flag",
|
|
5938
6148
|
includeHistoryContext: "flag",
|
|
6149
|
+
resumable: "flag",
|
|
5939
6150
|
interactive: "flag",
|
|
5940
6151
|
interactiveTransport: "flag",
|
|
5941
6152
|
isError: "flag",
|
|
@@ -5957,6 +6168,8 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
5957
6168
|
dropped: "count",
|
|
5958
6169
|
exitCode: "count",
|
|
5959
6170
|
historyLength: "count",
|
|
6171
|
+
matchedMessages: "count",
|
|
6172
|
+
messages: "count",
|
|
5960
6173
|
inputTokens: "count",
|
|
5961
6174
|
lastTextLength: "count",
|
|
5962
6175
|
lines: "count",
|
|
@@ -6058,6 +6271,12 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
6058
6271
|
sessionId: "id",
|
|
6059
6272
|
sessionID: "id",
|
|
6060
6273
|
sessionKey: "id",
|
|
6274
|
+
// The two ends of a conversation carried from one session key to another
|
|
6275
|
+
// (h #g215), and the fork lookup's parent. All three are session keys.
|
|
6276
|
+
fromKey: "id",
|
|
6277
|
+
toKey: "id",
|
|
6278
|
+
siblingKey: "id",
|
|
6279
|
+
parentKey: "id",
|
|
6061
6280
|
toolCallId: "id",
|
|
6062
6281
|
toolCallIds: "id",
|
|
6063
6282
|
toolUseId: "id",
|
|
@@ -6294,8 +6513,8 @@ function redactLogLine(raw, context) {
|
|
|
6294
6513
|
if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
|
|
6295
6514
|
return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
|
|
6296
6515
|
}
|
|
6297
|
-
function readLogTail(
|
|
6298
|
-
const fd = openSync(
|
|
6516
|
+
function readLogTail(path12, maxBytes = BUNDLE_TAIL_BYTES) {
|
|
6517
|
+
const fd = openSync(path12, "r");
|
|
6299
6518
|
try {
|
|
6300
6519
|
const size = fstatSync(fd).size;
|
|
6301
6520
|
const length = Math.min(size, maxBytes);
|
|
@@ -6445,13 +6664,20 @@ function parsePlanUsage(stdout) {
|
|
|
6445
6664
|
return { status: "failed", error: "no result object in the CLI's reply" };
|
|
6446
6665
|
}
|
|
6447
6666
|
function runCli(cliPath, args, timeoutMs, env) {
|
|
6667
|
+
const plan = planClaudeSpawn(cliPath, args);
|
|
6448
6668
|
return new Promise((resolve6, reject) => {
|
|
6449
6669
|
execFile2(
|
|
6450
|
-
|
|
6451
|
-
args,
|
|
6670
|
+
plan.file,
|
|
6671
|
+
plan.args,
|
|
6452
6672
|
// `killSignal` so a CLI wedged on a hook is actually gone, and a generous
|
|
6453
6673
|
// buffer because the reply is prose of unbounded length.
|
|
6454
|
-
{
|
|
6674
|
+
{
|
|
6675
|
+
timeout: timeoutMs,
|
|
6676
|
+
killSignal: "SIGKILL",
|
|
6677
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
6678
|
+
env,
|
|
6679
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
6680
|
+
},
|
|
6455
6681
|
(error, stdout) => {
|
|
6456
6682
|
if (stdout && stdout.trim()) return resolve6(stdout);
|
|
6457
6683
|
if (error) return reject(error);
|
|
@@ -6490,14 +6716,14 @@ async function fetchPlanUsage(cliPath, options = {}) {
|
|
|
6490
6716
|
}
|
|
6491
6717
|
|
|
6492
6718
|
// src/stale-build.ts
|
|
6493
|
-
import * as
|
|
6494
|
-
import * as
|
|
6719
|
+
import * as fs6 from "fs";
|
|
6720
|
+
import * as path7 from "path";
|
|
6495
6721
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6496
6722
|
|
|
6497
6723
|
// src/startup-diagnostics.ts
|
|
6498
6724
|
import { execFile as execFile3 } from "child_process";
|
|
6499
|
-
import * as
|
|
6500
|
-
import * as
|
|
6725
|
+
import * as fs5 from "fs";
|
|
6726
|
+
import * as path6 from "path";
|
|
6501
6727
|
import { promisify as promisify2 } from "util";
|
|
6502
6728
|
import { fileURLToPath } from "url";
|
|
6503
6729
|
|
|
@@ -6532,8 +6758,8 @@ var cachedPluginVersion;
|
|
|
6532
6758
|
function pluginVersion() {
|
|
6533
6759
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
6534
6760
|
try {
|
|
6535
|
-
const here =
|
|
6536
|
-
const raw =
|
|
6761
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
6762
|
+
const raw = fs5.readFileSync(path6.join(here, "..", "package.json"), "utf8");
|
|
6537
6763
|
const version = JSON.parse(raw).version;
|
|
6538
6764
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
6539
6765
|
} catch {
|
|
@@ -6557,7 +6783,7 @@ var opencodeVersionProbe;
|
|
|
6557
6783
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
6558
6784
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
6559
6785
|
opencodeVersionProbe = (async () => {
|
|
6560
|
-
if (!
|
|
6786
|
+
if (!path6.basename(execPath).toLowerCase().includes("opencode")) {
|
|
6561
6787
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
6562
6788
|
return void 0;
|
|
6563
6789
|
}
|
|
@@ -6786,10 +7012,10 @@ function defaultEntryPath() {
|
|
|
6786
7012
|
}
|
|
6787
7013
|
function readOnDiskBuild(entryPath) {
|
|
6788
7014
|
try {
|
|
6789
|
-
const raw =
|
|
7015
|
+
const raw = fs6.readFileSync(path7.join(path7.dirname(entryPath), "..", "package.json"), "utf8");
|
|
6790
7016
|
const version = JSON.parse(raw).version;
|
|
6791
7017
|
if (typeof version !== "string" || version.length === 0) return void 0;
|
|
6792
|
-
const stat =
|
|
7018
|
+
const stat = fs6.statSync(entryPath);
|
|
6793
7019
|
return { version, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
6794
7020
|
} catch {
|
|
6795
7021
|
return void 0;
|
|
@@ -6800,7 +7026,7 @@ function recordLoadedBuild() {
|
|
|
6800
7026
|
let mtimeMs;
|
|
6801
7027
|
let size;
|
|
6802
7028
|
try {
|
|
6803
|
-
const stat =
|
|
7029
|
+
const stat = fs6.statSync(entryPath);
|
|
6804
7030
|
mtimeMs = stat.mtimeMs;
|
|
6805
7031
|
size = stat.size;
|
|
6806
7032
|
} catch {
|
|
@@ -7237,7 +7463,7 @@ Could not build the report: ${message}`;
|
|
|
7237
7463
|
|
|
7238
7464
|
// src/agent-models.ts
|
|
7239
7465
|
import { readFile, readdir } from "fs/promises";
|
|
7240
|
-
import
|
|
7466
|
+
import path8 from "path";
|
|
7241
7467
|
|
|
7242
7468
|
// src/models.ts
|
|
7243
7469
|
var PROVIDER_ID = "claude-code";
|
|
@@ -7761,13 +7987,13 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7761
7987
|
const name = entry.slice(0, -3);
|
|
7762
7988
|
if (records2[name]) continue;
|
|
7763
7989
|
try {
|
|
7764
|
-
const text = await readFile(
|
|
7990
|
+
const text = await readFile(path8.join(directory, entry), "utf8");
|
|
7765
7991
|
records2[name] = parseAgentFrontmatter(text);
|
|
7766
7992
|
const prompt = agentMarkdownBody(text);
|
|
7767
7993
|
if (prompt) records2[name].prompt = prompt;
|
|
7768
7994
|
} catch (err) {
|
|
7769
7995
|
log.debug("failed to read agent markdown", {
|
|
7770
|
-
file:
|
|
7996
|
+
file: path8.join(directory, entry),
|
|
7771
7997
|
error: String(err)
|
|
7772
7998
|
});
|
|
7773
7999
|
}
|
|
@@ -7779,12 +8005,12 @@ function agentDirectories(home, projectDirectory) {
|
|
|
7779
8005
|
const directories = [];
|
|
7780
8006
|
if (projectDirectory) {
|
|
7781
8007
|
for (const name of AGENT_DIR_NAMES) {
|
|
7782
|
-
directories.push(
|
|
8008
|
+
directories.push(path8.join(projectDirectory, ".opencode", name));
|
|
7783
8009
|
}
|
|
7784
8010
|
}
|
|
7785
8011
|
if (home) {
|
|
7786
8012
|
for (const name of AGENT_DIR_NAMES) {
|
|
7787
|
-
directories.push(
|
|
8013
|
+
directories.push(path8.join(home, ".config", "opencode", name));
|
|
7788
8014
|
}
|
|
7789
8015
|
}
|
|
7790
8016
|
return directories;
|
|
@@ -8276,7 +8502,7 @@ The complete prior conversation appears above. The synthesis instructions follow
|
|
|
8276
8502
|
content.push({
|
|
8277
8503
|
type: "text",
|
|
8278
8504
|
text: `<conversation_history>
|
|
8279
|
-
The following is a summary of our conversation so far
|
|
8505
|
+
The following is a summary of our conversation so far, replayed as text because no Claude session was available to continue it:
|
|
8280
8506
|
|
|
8281
8507
|
${historyContext}
|
|
8282
8508
|
|
|
@@ -8384,14 +8610,14 @@ function getTrailingUserMessages(prompt, opts = {}) {
|
|
|
8384
8610
|
|
|
8385
8611
|
// src/skill-bridge.ts
|
|
8386
8612
|
import * as crypto3 from "crypto";
|
|
8387
|
-
import * as
|
|
8613
|
+
import * as fs7 from "fs";
|
|
8388
8614
|
import * as os3 from "os";
|
|
8389
|
-
import * as
|
|
8615
|
+
import * as path9 from "path";
|
|
8390
8616
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8391
8617
|
function bundledSkillsDir() {
|
|
8392
8618
|
try {
|
|
8393
8619
|
const here = fileURLToPath3(import.meta.url);
|
|
8394
|
-
const dir =
|
|
8620
|
+
const dir = path9.resolve(path9.dirname(here), "..", "skills");
|
|
8395
8621
|
return dirExists2(dir) ? dir : null;
|
|
8396
8622
|
} catch {
|
|
8397
8623
|
return null;
|
|
@@ -8399,16 +8625,16 @@ function bundledSkillsDir() {
|
|
|
8399
8625
|
}
|
|
8400
8626
|
function dirExists2(p) {
|
|
8401
8627
|
try {
|
|
8402
|
-
return
|
|
8628
|
+
return fs7.statSync(p).isDirectory();
|
|
8403
8629
|
} catch {
|
|
8404
8630
|
return false;
|
|
8405
8631
|
}
|
|
8406
8632
|
}
|
|
8407
8633
|
function realPath(p) {
|
|
8408
8634
|
try {
|
|
8409
|
-
return
|
|
8635
|
+
return fs7.realpathSync(p);
|
|
8410
8636
|
} catch {
|
|
8411
|
-
return
|
|
8637
|
+
return path9.resolve(p);
|
|
8412
8638
|
}
|
|
8413
8639
|
}
|
|
8414
8640
|
function envEnabled(name) {
|
|
@@ -8429,10 +8655,10 @@ function declaredSkillName(source) {
|
|
|
8429
8655
|
return SAFE_SKILL_NAME.test(name) ? name : null;
|
|
8430
8656
|
}
|
|
8431
8657
|
function readSkill(root, entry) {
|
|
8432
|
-
const dir =
|
|
8658
|
+
const dir = path9.join(root, entry);
|
|
8433
8659
|
let source;
|
|
8434
8660
|
try {
|
|
8435
|
-
source =
|
|
8661
|
+
source = fs7.readFileSync(path9.join(dir, "SKILL.md"), "utf8");
|
|
8436
8662
|
} catch {
|
|
8437
8663
|
return null;
|
|
8438
8664
|
}
|
|
@@ -8450,47 +8676,47 @@ function skillRoots(cwd) {
|
|
|
8450
8676
|
const roots = [];
|
|
8451
8677
|
const seen = /* @__PURE__ */ new Set();
|
|
8452
8678
|
const push = (p) => {
|
|
8453
|
-
const abs =
|
|
8679
|
+
const abs = path9.resolve(p);
|
|
8454
8680
|
if (seen.has(abs)) return;
|
|
8455
8681
|
seen.add(abs);
|
|
8456
8682
|
if (dirExists2(abs)) roots.push(abs);
|
|
8457
8683
|
};
|
|
8458
8684
|
const home = os3.homedir();
|
|
8459
|
-
const homeDir = home ?
|
|
8460
|
-
let current =
|
|
8685
|
+
const homeDir = home ? path9.resolve(home) : null;
|
|
8686
|
+
let current = path9.resolve(cwd);
|
|
8461
8687
|
while (true) {
|
|
8462
|
-
push(
|
|
8688
|
+
push(path9.join(current, ".opencode", "skills"));
|
|
8463
8689
|
if (current !== homeDir) {
|
|
8464
|
-
push(
|
|
8465
|
-
push(
|
|
8690
|
+
push(path9.join(current, ".claude", "skills"));
|
|
8691
|
+
push(path9.join(current, ".agents", "skills"));
|
|
8466
8692
|
}
|
|
8467
|
-
const parent =
|
|
8693
|
+
const parent = path9.dirname(current);
|
|
8468
8694
|
if (parent === current) break;
|
|
8469
8695
|
current = parent;
|
|
8470
8696
|
}
|
|
8471
|
-
if (homeDir) push(
|
|
8697
|
+
if (homeDir) push(path9.join(homeDir, ".opencode", "skills"));
|
|
8472
8698
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
8473
8699
|
if (envDir) {
|
|
8474
|
-
push(
|
|
8475
|
-
push(
|
|
8700
|
+
push(path9.join(envDir, "skills"));
|
|
8701
|
+
push(path9.join(envDir, "skill"));
|
|
8476
8702
|
}
|
|
8477
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ?
|
|
8703
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path9.join(homeDir, ".config") : null);
|
|
8478
8704
|
if (xdg) {
|
|
8479
|
-
push(
|
|
8480
|
-
push(
|
|
8705
|
+
push(path9.join(xdg, "opencode", "skills"));
|
|
8706
|
+
push(path9.join(xdg, "opencode", "skill"));
|
|
8481
8707
|
}
|
|
8482
8708
|
if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
|
|
8483
8709
|
if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
|
|
8484
|
-
push(
|
|
8710
|
+
push(path9.join(homeDir, ".claude", "skills"));
|
|
8485
8711
|
}
|
|
8486
|
-
push(
|
|
8712
|
+
push(path9.join(homeDir, ".agents", "skills"));
|
|
8487
8713
|
}
|
|
8488
8714
|
return roots;
|
|
8489
8715
|
}
|
|
8490
8716
|
function collectSkills(root, claimed, found) {
|
|
8491
8717
|
let entries;
|
|
8492
8718
|
try {
|
|
8493
|
-
entries =
|
|
8719
|
+
entries = fs7.readdirSync(root, { withFileTypes: true });
|
|
8494
8720
|
} catch {
|
|
8495
8721
|
return;
|
|
8496
8722
|
}
|
|
@@ -8530,23 +8756,23 @@ function discoverBundledSkills() {
|
|
|
8530
8756
|
}
|
|
8531
8757
|
function claudeConfigDir(configDir) {
|
|
8532
8758
|
const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8533
|
-
if (explicit) return
|
|
8759
|
+
if (explicit) return path9.resolve(expandHome(explicit));
|
|
8534
8760
|
const home = os3.homedir();
|
|
8535
|
-
return home ?
|
|
8761
|
+
return home ? path9.resolve(home, ".claude") : null;
|
|
8536
8762
|
}
|
|
8537
8763
|
function installedPluginSkillRoots(configDir, cwd) {
|
|
8538
8764
|
if (!configDir) return [];
|
|
8539
8765
|
let parsed;
|
|
8540
8766
|
try {
|
|
8541
8767
|
parsed = JSON.parse(
|
|
8542
|
-
|
|
8768
|
+
fs7.readFileSync(path9.join(configDir, "plugins", "installed_plugins.json"), "utf8")
|
|
8543
8769
|
);
|
|
8544
8770
|
} catch {
|
|
8545
8771
|
return [];
|
|
8546
8772
|
}
|
|
8547
8773
|
const plugins = parsed?.plugins;
|
|
8548
8774
|
if (!plugins || typeof plugins !== "object") return [];
|
|
8549
|
-
const workspace =
|
|
8775
|
+
const workspace = path9.resolve(cwd);
|
|
8550
8776
|
const roots = [];
|
|
8551
8777
|
for (const installs of Object.values(plugins)) {
|
|
8552
8778
|
if (!Array.isArray(installs)) continue;
|
|
@@ -8554,28 +8780,28 @@ function installedPluginSkillRoots(configDir, cwd) {
|
|
|
8554
8780
|
const entry = install;
|
|
8555
8781
|
if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
|
|
8556
8782
|
if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
|
|
8557
|
-
roots.push(
|
|
8783
|
+
roots.push(path9.join(entry.installPath, "skills"));
|
|
8558
8784
|
}
|
|
8559
8785
|
}
|
|
8560
8786
|
return roots;
|
|
8561
8787
|
}
|
|
8562
8788
|
function coversWorkspace(projectPath, workspace) {
|
|
8563
8789
|
if (typeof projectPath !== "string" || !projectPath) return false;
|
|
8564
|
-
const root =
|
|
8790
|
+
const root = path9.resolve(projectPath);
|
|
8565
8791
|
if (root === workspace) return true;
|
|
8566
|
-
const rel =
|
|
8567
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
8792
|
+
const rel = path9.relative(root, workspace);
|
|
8793
|
+
return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
8568
8794
|
}
|
|
8569
8795
|
function discoverNativeClaudeSkills(opts) {
|
|
8570
8796
|
const found = [];
|
|
8571
8797
|
const scanned = /* @__PURE__ */ new Set();
|
|
8572
8798
|
const scan = (root, scope) => {
|
|
8573
|
-
const abs =
|
|
8799
|
+
const abs = path9.resolve(root);
|
|
8574
8800
|
if (scanned.has(abs)) return;
|
|
8575
8801
|
scanned.add(abs);
|
|
8576
8802
|
let entries;
|
|
8577
8803
|
try {
|
|
8578
|
-
entries =
|
|
8804
|
+
entries = fs7.readdirSync(abs, { withFileTypes: true });
|
|
8579
8805
|
} catch {
|
|
8580
8806
|
return;
|
|
8581
8807
|
}
|
|
@@ -8587,13 +8813,13 @@ function discoverNativeClaudeSkills(opts) {
|
|
|
8587
8813
|
}
|
|
8588
8814
|
};
|
|
8589
8815
|
const configDir = claudeConfigDir(opts.configDir);
|
|
8590
|
-
if (configDir) scan(
|
|
8816
|
+
if (configDir) scan(path9.join(configDir, "skills"), "user");
|
|
8591
8817
|
const home = os3.homedir();
|
|
8592
|
-
const homeDir = home ?
|
|
8593
|
-
let current =
|
|
8818
|
+
const homeDir = home ? path9.resolve(home) : null;
|
|
8819
|
+
let current = path9.resolve(opts.cwd);
|
|
8594
8820
|
while (true) {
|
|
8595
|
-
if (current !== homeDir) scan(
|
|
8596
|
-
const parent =
|
|
8821
|
+
if (current !== homeDir) scan(path9.join(current, ".claude", "skills"), "project");
|
|
8822
|
+
const parent = path9.dirname(current);
|
|
8597
8823
|
if (parent === current) break;
|
|
8598
8824
|
current = parent;
|
|
8599
8825
|
}
|
|
@@ -8627,10 +8853,10 @@ function dropNativelyLoadedSkills(skills, native) {
|
|
|
8627
8853
|
}
|
|
8628
8854
|
function readBundledSkillInfos() {
|
|
8629
8855
|
return discoverBundledSkills().flatMap((skill) => {
|
|
8630
|
-
const file =
|
|
8856
|
+
const file = path9.join(skill.dir, "SKILL.md");
|
|
8631
8857
|
let text;
|
|
8632
8858
|
try {
|
|
8633
|
-
text =
|
|
8859
|
+
text = fs7.readFileSync(file, "utf8");
|
|
8634
8860
|
} catch {
|
|
8635
8861
|
return [];
|
|
8636
8862
|
}
|
|
@@ -8654,15 +8880,15 @@ function readBundledSkillInfos() {
|
|
|
8654
8880
|
}
|
|
8655
8881
|
function linkSkill(source, target) {
|
|
8656
8882
|
try {
|
|
8657
|
-
|
|
8883
|
+
fs7.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
8658
8884
|
return;
|
|
8659
8885
|
} catch {
|
|
8660
|
-
|
|
8886
|
+
fs7.cpSync(source, target, { recursive: true, dereference: true });
|
|
8661
8887
|
}
|
|
8662
8888
|
}
|
|
8663
8889
|
function fileExists2(p) {
|
|
8664
8890
|
try {
|
|
8665
|
-
return
|
|
8891
|
+
return fs7.statSync(p).isFile();
|
|
8666
8892
|
} catch {
|
|
8667
8893
|
return false;
|
|
8668
8894
|
}
|
|
@@ -8671,14 +8897,14 @@ function buildSkillPluginDir(skills) {
|
|
|
8671
8897
|
if (skills.length === 0) return null;
|
|
8672
8898
|
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
8673
8899
|
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
8674
|
-
const root =
|
|
8675
|
-
const manifest =
|
|
8900
|
+
const root = path9.join(pluginTmpDir(), `skills-${hash}`);
|
|
8901
|
+
const manifest = path9.join(root, ".claude-plugin", "plugin.json");
|
|
8676
8902
|
if (fileExists2(manifest)) return root;
|
|
8677
8903
|
try {
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8904
|
+
fs7.rmSync(root, { recursive: true, force: true });
|
|
8905
|
+
fs7.mkdirSync(path9.join(root, ".claude-plugin"), { recursive: true });
|
|
8906
|
+
fs7.mkdirSync(path9.join(root, "skills"), { recursive: true });
|
|
8907
|
+
fs7.writeFileSync(
|
|
8682
8908
|
manifest,
|
|
8683
8909
|
JSON.stringify(
|
|
8684
8910
|
{
|
|
@@ -8691,7 +8917,7 @@ function buildSkillPluginDir(skills) {
|
|
|
8691
8917
|
{ encoding: "utf8", mode: 384 }
|
|
8692
8918
|
);
|
|
8693
8919
|
for (const skill of skills) {
|
|
8694
|
-
linkSkill(skill.dir,
|
|
8920
|
+
linkSkill(skill.dir, path9.join(root, "skills", skill.name));
|
|
8695
8921
|
}
|
|
8696
8922
|
} catch (err) {
|
|
8697
8923
|
log.warn("failed to stage opencode skill plugin dir", {
|
|
@@ -8707,7 +8933,7 @@ function registerBundledSkillPath(config) {
|
|
|
8707
8933
|
if (!dir) return false;
|
|
8708
8934
|
config.skills ??= {};
|
|
8709
8935
|
const paths = config.skills.paths ??= [];
|
|
8710
|
-
if (paths.some((p) =>
|
|
8936
|
+
if (paths.some((p) => path9.resolve(p) === dir)) return false;
|
|
8711
8937
|
paths.push(dir);
|
|
8712
8938
|
return true;
|
|
8713
8939
|
}
|
|
@@ -8761,12 +8987,13 @@ import { join as join10 } from "path";
|
|
|
8761
8987
|
|
|
8762
8988
|
// src/claude-session-bun.ts
|
|
8763
8989
|
import * as os4 from "os";
|
|
8764
|
-
import * as
|
|
8765
|
-
import * as
|
|
8990
|
+
import * as fs8 from "fs";
|
|
8991
|
+
import * as path10 from "path";
|
|
8766
8992
|
import { execFileSync } from "child_process";
|
|
8767
8993
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
8994
|
+
import { StringDecoder } from "string_decoder";
|
|
8768
8995
|
function resolveClaude(cmd = "claude") {
|
|
8769
|
-
if (
|
|
8996
|
+
if (path10.isAbsolute(cmd) && fs8.existsSync(cmd)) return cmd;
|
|
8770
8997
|
const viaBun = Bun.which(cmd);
|
|
8771
8998
|
if (viaBun) return viaBun;
|
|
8772
8999
|
const isWin = os4.platform() === "win32";
|
|
@@ -8775,24 +9002,30 @@ function resolveClaude(cmd = "claude") {
|
|
|
8775
9002
|
encoding: "utf8",
|
|
8776
9003
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8777
9004
|
});
|
|
8778
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
9005
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs8.existsSync(p));
|
|
8779
9006
|
if (first) return first;
|
|
8780
9007
|
} catch {
|
|
8781
9008
|
}
|
|
8782
9009
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
8783
9010
|
}
|
|
8784
9011
|
function encodeCwd(cwd) {
|
|
8785
|
-
const resolved =
|
|
9012
|
+
const resolved = path10.resolve(cwd);
|
|
8786
9013
|
let real = resolved;
|
|
8787
9014
|
try {
|
|
8788
|
-
real =
|
|
9015
|
+
real = fs8.realpathSync.native(resolved);
|
|
8789
9016
|
} catch {
|
|
8790
9017
|
}
|
|
8791
9018
|
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
8792
9019
|
}
|
|
8793
9020
|
var bunPtySpawner = (argv, opts) => {
|
|
8794
9021
|
const [command, ...args] = argv;
|
|
8795
|
-
|
|
9022
|
+
const resolved = resolveClaude(command ?? "claude");
|
|
9023
|
+
if (os4.platform() === "win32" && isBatchFile(resolved)) {
|
|
9024
|
+
throw new Error(
|
|
9025
|
+
`The interactive transport cannot run a Windows batch shim (${resolved}). Point \`cliPath\` at claude.exe, or use the default headless transport.`
|
|
9026
|
+
);
|
|
9027
|
+
}
|
|
9028
|
+
return Bun.spawn([resolved, ...args], {
|
|
8796
9029
|
cwd: opts.cwd,
|
|
8797
9030
|
env: opts.env,
|
|
8798
9031
|
terminal: {
|
|
@@ -9007,6 +9240,123 @@ function isInterruptRecord(rec) {
|
|
|
9007
9240
|
function isTurnDurationRecord(rec) {
|
|
9008
9241
|
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
9009
9242
|
}
|
|
9243
|
+
var TRANSCRIPT_CHUNK_BYTES = 1 << 20;
|
|
9244
|
+
var TranscriptCursor = class {
|
|
9245
|
+
/** The path is asked for on every poll rather than kept, because it is a
|
|
9246
|
+
* field of the session and a test may repoint it; a path that changes is
|
|
9247
|
+
* a different file, which the `dev:ino` check already starts over on. */
|
|
9248
|
+
constructor(resolvePath) {
|
|
9249
|
+
this.resolvePath = resolvePath;
|
|
9250
|
+
}
|
|
9251
|
+
resolvePath;
|
|
9252
|
+
/** Bytes of the current file already read. */
|
|
9253
|
+
offset = 0;
|
|
9254
|
+
/** `dev:ino` of the file those bytes came from, null before the first
|
|
9255
|
+
* successful stat. A different one is a new file, not growth. */
|
|
9256
|
+
fileId = null;
|
|
9257
|
+
/** Holds back an incomplete trailing UTF-8 sequence between two reads. */
|
|
9258
|
+
decoder = new StringDecoder("utf8");
|
|
9259
|
+
/** What was read after the last newline: a record the CLI is part way
|
|
9260
|
+
* through writing, completed by a later read. */
|
|
9261
|
+
partial = "";
|
|
9262
|
+
/** Complete lines seen since byte 0 of the current file. */
|
|
9263
|
+
seen = 0;
|
|
9264
|
+
/** Complete lines handed on: the line-index cursor. */
|
|
9265
|
+
taken = 0;
|
|
9266
|
+
/** Complete lines read and not handed on yet, oldest first. */
|
|
9267
|
+
queue = [];
|
|
9268
|
+
stats = { polls: 0, reads: 0, bytes: 0 };
|
|
9269
|
+
/** Complete lines in the file now. */
|
|
9270
|
+
lineCount() {
|
|
9271
|
+
this.poll(true);
|
|
9272
|
+
return this.seen;
|
|
9273
|
+
}
|
|
9274
|
+
/** The complete lines not handed on yet, oldest first. They stay queued
|
|
9275
|
+
* until `take` says how many of them were used. */
|
|
9276
|
+
pending() {
|
|
9277
|
+
this.poll(true);
|
|
9278
|
+
return this.queue;
|
|
9279
|
+
}
|
|
9280
|
+
/** Whether the file holds a complete line this cursor has not handed on. */
|
|
9281
|
+
hasUnread() {
|
|
9282
|
+
return this.pending().length > 0;
|
|
9283
|
+
}
|
|
9284
|
+
/** Mark the first `count` pending lines as handed on. */
|
|
9285
|
+
take(count) {
|
|
9286
|
+
const used = Math.min(count, this.queue.length);
|
|
9287
|
+
if (used <= 0) return;
|
|
9288
|
+
this.queue.splice(0, used);
|
|
9289
|
+
this.taken += used;
|
|
9290
|
+
}
|
|
9291
|
+
/** Hand on nothing of what is in the file now: the records written before a
|
|
9292
|
+
* turn started belong to the turn before it. */
|
|
9293
|
+
skipToEnd() {
|
|
9294
|
+
this.poll(false);
|
|
9295
|
+
this.taken = Math.max(this.taken, this.seen);
|
|
9296
|
+
this.queue.length = 0;
|
|
9297
|
+
}
|
|
9298
|
+
/** Read whatever was appended since the last look. */
|
|
9299
|
+
poll(collect) {
|
|
9300
|
+
this.stats.polls++;
|
|
9301
|
+
const filePath = this.resolvePath();
|
|
9302
|
+
let stat;
|
|
9303
|
+
try {
|
|
9304
|
+
stat = fs8.statSync(filePath);
|
|
9305
|
+
} catch {
|
|
9306
|
+
return;
|
|
9307
|
+
}
|
|
9308
|
+
const fileId = `${stat.dev}:${stat.ino}`;
|
|
9309
|
+
if (this.fileId !== null && (fileId !== this.fileId || stat.size < this.offset)) {
|
|
9310
|
+
this.offset = 0;
|
|
9311
|
+
this.seen = 0;
|
|
9312
|
+
this.partial = "";
|
|
9313
|
+
this.decoder = new StringDecoder("utf8");
|
|
9314
|
+
this.queue.length = 0;
|
|
9315
|
+
}
|
|
9316
|
+
this.fileId = fileId;
|
|
9317
|
+
if (stat.size <= this.offset) return;
|
|
9318
|
+
let fd = null;
|
|
9319
|
+
try {
|
|
9320
|
+
fd = fs8.openSync(filePath, "r");
|
|
9321
|
+
const buffer = Buffer.allocUnsafe(
|
|
9322
|
+
Math.min(TRANSCRIPT_CHUNK_BYTES, stat.size - this.offset)
|
|
9323
|
+
);
|
|
9324
|
+
while (this.offset < stat.size) {
|
|
9325
|
+
const want = Math.min(buffer.length, stat.size - this.offset);
|
|
9326
|
+
const read = fs8.readSync(fd, buffer, 0, want, this.offset);
|
|
9327
|
+
this.stats.reads++;
|
|
9328
|
+
if (read <= 0) break;
|
|
9329
|
+
this.offset += read;
|
|
9330
|
+
this.stats.bytes += read;
|
|
9331
|
+
this.ingest(this.decoder.write(buffer.subarray(0, read)), collect);
|
|
9332
|
+
}
|
|
9333
|
+
} catch {
|
|
9334
|
+
} finally {
|
|
9335
|
+
if (fd !== null) {
|
|
9336
|
+
try {
|
|
9337
|
+
fs8.closeSync(fd);
|
|
9338
|
+
} catch {
|
|
9339
|
+
}
|
|
9340
|
+
}
|
|
9341
|
+
}
|
|
9342
|
+
}
|
|
9343
|
+
/** Split decoded text into complete lines, keeping the unterminated tail. */
|
|
9344
|
+
ingest(text, collect) {
|
|
9345
|
+
if (!text) return;
|
|
9346
|
+
this.partial += text;
|
|
9347
|
+
let start = 0;
|
|
9348
|
+
for (; ; ) {
|
|
9349
|
+
const newline = this.partial.indexOf("\n", start);
|
|
9350
|
+
if (newline === -1) break;
|
|
9351
|
+
if (collect && this.seen >= this.taken) {
|
|
9352
|
+
this.queue.push(this.partial.slice(start, newline));
|
|
9353
|
+
}
|
|
9354
|
+
this.seen++;
|
|
9355
|
+
start = newline + 1;
|
|
9356
|
+
}
|
|
9357
|
+
if (start > 0) this.partial = this.partial.slice(start);
|
|
9358
|
+
}
|
|
9359
|
+
};
|
|
9010
9360
|
var normalizedPrompt = (text) => text.replace(/\s+/g, " ").trim();
|
|
9011
9361
|
function isOwnPromptRecord(rec, normalized) {
|
|
9012
9362
|
const content = rec?.type === "user" ? rec.message?.content : void 0;
|
|
@@ -9014,12 +9364,12 @@ function isOwnPromptRecord(rec, normalized) {
|
|
|
9014
9364
|
}
|
|
9015
9365
|
function resolveConfigDir(configDir) {
|
|
9016
9366
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
9017
|
-
if (!value) return
|
|
9367
|
+
if (!value) return path10.join(os4.homedir(), ".claude");
|
|
9018
9368
|
if (value === "~") return os4.homedir();
|
|
9019
9369
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
9020
|
-
return
|
|
9370
|
+
return path10.join(os4.homedir(), value.slice(2));
|
|
9021
9371
|
}
|
|
9022
|
-
return
|
|
9372
|
+
return path10.resolve(value);
|
|
9023
9373
|
}
|
|
9024
9374
|
function fatalScreenMessage(kind, configDir, cwd) {
|
|
9025
9375
|
if (kind === "trust") {
|
|
@@ -9038,8 +9388,8 @@ var ClaudeSession = class {
|
|
|
9038
9388
|
/** The newest terminal output, ANSI included, for diagnostics. */
|
|
9039
9389
|
raw = "";
|
|
9040
9390
|
proc = null;
|
|
9041
|
-
|
|
9042
|
-
|
|
9391
|
+
/** The transcript, read forwards only; its `taken` is the old line cursor. */
|
|
9392
|
+
transcript;
|
|
9043
9393
|
lastDataAt = 0;
|
|
9044
9394
|
exited = false;
|
|
9045
9395
|
exitCode = null;
|
|
@@ -9083,7 +9433,7 @@ var ClaudeSession = class {
|
|
|
9083
9433
|
abandonTurn = false;
|
|
9084
9434
|
o;
|
|
9085
9435
|
constructor(opts = {}) {
|
|
9086
|
-
this.cwd =
|
|
9436
|
+
this.cwd = path10.resolve(opts.cwd ?? process.cwd());
|
|
9087
9437
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
9088
9438
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
9089
9439
|
this.signal = opts.signal;
|
|
@@ -9095,12 +9445,13 @@ var ClaudeSession = class {
|
|
|
9095
9445
|
this.onExit = opts.onExit;
|
|
9096
9446
|
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
9097
9447
|
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
9098
|
-
this.jsonlPath =
|
|
9448
|
+
this.jsonlPath = path10.join(
|
|
9099
9449
|
this.configDir,
|
|
9100
9450
|
"projects",
|
|
9101
9451
|
encodeCwd(this.cwd),
|
|
9102
9452
|
`${this.sessionId}.jsonl`
|
|
9103
9453
|
);
|
|
9454
|
+
this.transcript = new TranscriptCursor(() => this.jsonlPath);
|
|
9104
9455
|
this.o = {
|
|
9105
9456
|
cwd: this.cwd,
|
|
9106
9457
|
cliPath: opts.cliPath,
|
|
@@ -9205,7 +9556,12 @@ var ClaudeSession = class {
|
|
|
9205
9556
|
};
|
|
9206
9557
|
proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
|
|
9207
9558
|
await this.waitForBoot();
|
|
9208
|
-
this.
|
|
9559
|
+
this.transcript.skipToEnd();
|
|
9560
|
+
}
|
|
9561
|
+
/** How much this session has read off the transcript. Read-only, and the
|
|
9562
|
+
* one thing `scripts/bench-transcript-poll.ts` measures. */
|
|
9563
|
+
get transcriptStats() {
|
|
9564
|
+
return this.transcript.stats;
|
|
9209
9565
|
}
|
|
9210
9566
|
onData(chunk) {
|
|
9211
9567
|
this.lastDataAt = Date.now();
|
|
@@ -9352,22 +9708,10 @@ var ClaudeSession = class {
|
|
|
9352
9708
|
while (Date.now() < until) {
|
|
9353
9709
|
await delay(80);
|
|
9354
9710
|
if (this.aborted || this.exited) return;
|
|
9355
|
-
if (this.
|
|
9711
|
+
if (this.transcript.hasUnread()) return;
|
|
9356
9712
|
}
|
|
9357
9713
|
}
|
|
9358
9714
|
}
|
|
9359
|
-
readRawLines() {
|
|
9360
|
-
try {
|
|
9361
|
-
return fs7.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
9362
|
-
} catch {
|
|
9363
|
-
return [];
|
|
9364
|
-
}
|
|
9365
|
-
}
|
|
9366
|
-
/** Count of complete lines (split('\n') minus the trailing/partial element). */
|
|
9367
|
-
lineCount() {
|
|
9368
|
-
const lines = this.readRawLines();
|
|
9369
|
-
return lines.length > 0 ? lines.length - 1 : 0;
|
|
9370
|
-
}
|
|
9371
9715
|
rawTail(max = 600) {
|
|
9372
9716
|
const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
|
|
9373
9717
|
return clean.length > max ? clean.slice(-max) : clean;
|
|
@@ -9400,7 +9744,7 @@ var ClaudeSession = class {
|
|
|
9400
9744
|
this.turn = new Promise((resolve6) => settle = resolve6);
|
|
9401
9745
|
this.turnDenied = [];
|
|
9402
9746
|
this.abandonTurn = false;
|
|
9403
|
-
this.
|
|
9747
|
+
this.transcript.skipToEnd();
|
|
9404
9748
|
this.screen = "";
|
|
9405
9749
|
try {
|
|
9406
9750
|
if (this.o.bracketedPaste) {
|
|
@@ -9465,30 +9809,26 @@ var ClaudeSession = class {
|
|
|
9465
9809
|
/**
|
|
9466
9810
|
* Read every complete transcript record past the cursor into the running
|
|
9467
9811
|
* turn. Returns whether anything new was read. The one place a record is
|
|
9468
|
-
* interpreted, shared by the poll loop and `flushTranscript
|
|
9812
|
+
* interpreted, shared by the poll loop and `flushTranscript`, and the only
|
|
9813
|
+
* one that consumes what `TranscriptCursor` has queued.
|
|
9469
9814
|
*/
|
|
9470
9815
|
scanTranscript() {
|
|
9471
9816
|
const scan = this.scan;
|
|
9472
9817
|
if (!scan || scan.end) return false;
|
|
9473
|
-
const lines = this.
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
for (let i =
|
|
9818
|
+
const lines = this.transcript.pending().slice();
|
|
9819
|
+
if (lines.length === 0) return false;
|
|
9820
|
+
let used = 0;
|
|
9821
|
+
for (let i = 0; i < lines.length && !scan.end; i++) {
|
|
9477
9822
|
const s = lines[i];
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
continue;
|
|
9481
|
-
}
|
|
9823
|
+
used = i + 1;
|
|
9824
|
+
if (!s || !s.trim()) continue;
|
|
9482
9825
|
let rec = null;
|
|
9483
9826
|
try {
|
|
9484
9827
|
rec = JSON.parse(s);
|
|
9485
9828
|
} catch {
|
|
9486
9829
|
}
|
|
9487
9830
|
if (scan.awaitingOwnPrompt !== null) {
|
|
9488
|
-
if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt))
|
|
9489
|
-
this.cursor = i + 1;
|
|
9490
|
-
continue;
|
|
9491
|
-
}
|
|
9831
|
+
if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) continue;
|
|
9492
9832
|
scan.awaitingOwnPrompt = null;
|
|
9493
9833
|
}
|
|
9494
9834
|
if (rec) this.notePlanApproval(rec);
|
|
@@ -9508,9 +9848,8 @@ var ClaudeSession = class {
|
|
|
9508
9848
|
scan.end = scan.stopReason ? "stop" : "ended";
|
|
9509
9849
|
}
|
|
9510
9850
|
}
|
|
9511
|
-
this.cursor = i + 1;
|
|
9512
9851
|
}
|
|
9513
|
-
|
|
9852
|
+
this.transcript.take(used);
|
|
9514
9853
|
scan.lastBeat = Date.now();
|
|
9515
9854
|
scan.stopSeenAt = Date.now();
|
|
9516
9855
|
return true;
|
|
@@ -10299,13 +10638,13 @@ function spawnInteractiveProcess(opts) {
|
|
|
10299
10638
|
}
|
|
10300
10639
|
|
|
10301
10640
|
// src/prompts.ts
|
|
10302
|
-
import { readFileSync as
|
|
10641
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
10303
10642
|
import { homedir as homedir8 } from "os";
|
|
10304
10643
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
10305
10644
|
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
|
|
10306
|
-
function readPromptFileIfPresent(
|
|
10645
|
+
function readPromptFileIfPresent(path12) {
|
|
10307
10646
|
try {
|
|
10308
|
-
const content =
|
|
10647
|
+
const content = readFileSync6(path12, "utf8").trim();
|
|
10309
10648
|
return content || void 0;
|
|
10310
10649
|
} catch {
|
|
10311
10650
|
return void 0;
|
|
@@ -10410,9 +10749,9 @@ function extractSystemMessages(prompt) {
|
|
|
10410
10749
|
}
|
|
10411
10750
|
var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
|
|
10412
10751
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
10413
|
-
const readFile2 = opts.readFile ?? ((
|
|
10752
|
+
const readFile2 = opts.readFile ?? ((path12) => {
|
|
10414
10753
|
try {
|
|
10415
|
-
return
|
|
10754
|
+
return readFileSync6(path12, "utf8");
|
|
10416
10755
|
} catch {
|
|
10417
10756
|
return null;
|
|
10418
10757
|
}
|
|
@@ -10424,16 +10763,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
|
|
|
10424
10763
|
}
|
|
10425
10764
|
const seen = /* @__PURE__ */ new Set();
|
|
10426
10765
|
for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
|
|
10427
|
-
const
|
|
10428
|
-
if (seen.has(
|
|
10429
|
-
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(
|
|
10430
|
-
seen.add(
|
|
10431
|
-
const file = readFile2(
|
|
10766
|
+
const path12 = match[1].trim();
|
|
10767
|
+
if (seen.has(path12) || !isAbsolute3(path12)) continue;
|
|
10768
|
+
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path12))) continue;
|
|
10769
|
+
seen.add(path12);
|
|
10770
|
+
const file = readFile2(path12);
|
|
10432
10771
|
if (!file?.trim()) continue;
|
|
10433
10772
|
const start = match.index + match[0].length + 1;
|
|
10434
10773
|
const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
|
|
10435
10774
|
if (content === null) continue;
|
|
10436
|
-
out.push({ text: `Instructions from: ${
|
|
10775
|
+
out.push({ text: `Instructions from: ${path12}
|
|
10437
10776
|
${content}`, content });
|
|
10438
10777
|
}
|
|
10439
10778
|
return out;
|
|
@@ -10470,10 +10809,10 @@ ${options.compressionSummary.trim()}`
|
|
|
10470
10809
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
10471
10810
|
const content = parts.join("\n\n");
|
|
10472
10811
|
if (!content) return void 0;
|
|
10473
|
-
const
|
|
10812
|
+
const path12 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID6()}.md`);
|
|
10474
10813
|
try {
|
|
10475
|
-
writeFileSync6(
|
|
10476
|
-
return
|
|
10814
|
+
writeFileSync6(path12, content, { encoding: "utf8", mode: 384 });
|
|
10815
|
+
return path12;
|
|
10477
10816
|
} catch (err) {
|
|
10478
10817
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
10479
10818
|
return void 0;
|
|
@@ -11326,6 +11665,7 @@ function createTurnState(init) {
|
|
|
11326
11665
|
accountLimitHit: null,
|
|
11327
11666
|
apiErrorTextShown: null,
|
|
11328
11667
|
usageLimitNote: false,
|
|
11668
|
+
compactionFailureText: null,
|
|
11329
11669
|
accountBlock: null,
|
|
11330
11670
|
modelRefusal: null,
|
|
11331
11671
|
drainBuffer: [],
|
|
@@ -12420,6 +12760,7 @@ ${plan}
|
|
|
12420
12760
|
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
12421
12761
|
}));
|
|
12422
12762
|
const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
|
|
12763
|
+
const compactionApiError = ctx.compactionMode && (apiErrorReply || typeof msg.error === "string");
|
|
12423
12764
|
if (limitFromReply) {
|
|
12424
12765
|
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
12425
12766
|
const said = limitFactsFromText(
|
|
@@ -12441,6 +12782,10 @@ ${plan}
|
|
|
12441
12782
|
for (const block2 of msg.message.content) {
|
|
12442
12783
|
if (block2.type === "text" && block2.text) {
|
|
12443
12784
|
if (limitErrorReply) continue;
|
|
12785
|
+
if (compactionApiError) {
|
|
12786
|
+
state.compactionFailureText ??= block2.text;
|
|
12787
|
+
continue;
|
|
12788
|
+
}
|
|
12444
12789
|
if (apiErrorReply) state.apiErrorTextShown = block2.text;
|
|
12445
12790
|
resetLastVisibleTextBlock(state);
|
|
12446
12791
|
const blockId = state.startTextBlock();
|
|
@@ -12666,7 +13011,14 @@ ${plan}
|
|
|
12666
13011
|
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
12667
13012
|
// and says what to run; two notes about one failure is one too many.
|
|
12668
13013
|
!state.accountBlock && msg.is_error === true;
|
|
12669
|
-
|
|
13014
|
+
const compactionFailure = ctx.compactionMode && (msg.is_error === true || typeof msg.subtype === "string" && msg.subtype !== "success");
|
|
13015
|
+
if (compactionFailure) {
|
|
13016
|
+
if (typeof msg.result === "string" && msg.result.trim()) {
|
|
13017
|
+
state.compactionFailureText ??= msg.result.trim();
|
|
13018
|
+
}
|
|
13019
|
+
state.resultFailure ??= state.accountBlock ?? (state.accountLimitHit ? "usage_limit" : void 0) ?? (typeof msg.subtype === "string" && msg.subtype !== "success" ? msg.subtype : "error");
|
|
13020
|
+
}
|
|
13021
|
+
if (!state.currentTextId && msg.is_error && !compactionFailure && // Already on screen from the reply frame, word for word.
|
|
12670
13022
|
!(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
12671
13023
|
const errId = state.startTextBlock();
|
|
12672
13024
|
state.controller.enqueue({
|
|
@@ -12681,21 +13033,23 @@ ${plan}
|
|
|
12681
13033
|
if (state.accountBlock && msg.is_error) {
|
|
12682
13034
|
state.resultFailure ??= state.accountBlock;
|
|
12683
13035
|
const offeringSwitch = ctx.failoverAskActive;
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
13036
|
+
if (!compactionFailure) {
|
|
13037
|
+
state.controller.enqueue({
|
|
13038
|
+
type: "text-delta",
|
|
13039
|
+
id: state.startTextBlock(),
|
|
13040
|
+
delta: formatAccountBlockNote({
|
|
13041
|
+
kind: state.accountBlock,
|
|
13042
|
+
account: ctx.sourceAccount,
|
|
13043
|
+
configDir: ctx.config.configDir,
|
|
13044
|
+
offeringSwitch,
|
|
13045
|
+
// Read only when the form is not taking the turn, which is the
|
|
13046
|
+
// default: moving the work by hand is then the operator's only
|
|
13047
|
+
// route and nothing else on screen says it exists (h #g194).
|
|
13048
|
+
candidates: ctx.failoverAccounts
|
|
13049
|
+
})
|
|
13050
|
+
});
|
|
13051
|
+
state.endTextBlock();
|
|
13052
|
+
}
|
|
12699
13053
|
log.warn(`Claude account "${ctx.sourceAccount}" cannot serve requests`, {
|
|
12700
13054
|
sessionKey: state.sessionKey,
|
|
12701
13055
|
kind: state.accountBlock,
|
|
@@ -12705,11 +13059,14 @@ ${plan}
|
|
|
12705
13059
|
const failure = describeResultFailure(msg);
|
|
12706
13060
|
if (failure) {
|
|
12707
13061
|
state.resultFailure = msg.subtype;
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
13062
|
+
if (!compactionFailure) {
|
|
13063
|
+
state.controller.enqueue({
|
|
13064
|
+
type: "text-delta",
|
|
13065
|
+
id: state.startTextBlock(),
|
|
13066
|
+
delta: formatResultFailureNote(failure)
|
|
13067
|
+
});
|
|
13068
|
+
}
|
|
13069
|
+
if (compactionFailure) state.compactionFailureText ??= failure;
|
|
12713
13070
|
log.warn(failure, { sessionKey: state.sessionKey, subtype: msg.subtype });
|
|
12714
13071
|
}
|
|
12715
13072
|
const turnStats = extractTurnStats(msg);
|
|
@@ -13519,13 +13876,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13519
13876
|
}
|
|
13520
13877
|
}
|
|
13521
13878
|
}
|
|
13879
|
+
let replayReason = "no-session-for-key";
|
|
13522
13880
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
13523
13881
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
13882
|
+
const transcriptPath = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
13524
13883
|
const resumePoint = findResumePoint({
|
|
13525
13884
|
sessionKey: sk,
|
|
13526
13885
|
prompt: options.prompt,
|
|
13527
13886
|
cliPath,
|
|
13528
|
-
transcriptPath
|
|
13887
|
+
transcriptPath,
|
|
13888
|
+
onRefused: (reason) => {
|
|
13889
|
+
replayReason = reason;
|
|
13890
|
+
}
|
|
13529
13891
|
});
|
|
13530
13892
|
if (resumePoint) {
|
|
13531
13893
|
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
@@ -13534,8 +13896,47 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13534
13896
|
sessionKey: sk,
|
|
13535
13897
|
matchedMessages: resumePoint.matched
|
|
13536
13898
|
});
|
|
13899
|
+
} else if (this.config.resumeAcrossModelChanges !== false) {
|
|
13900
|
+
const sibling = findSiblingResumePoint({
|
|
13901
|
+
sessionKey: sk,
|
|
13902
|
+
prompt: options.prompt,
|
|
13903
|
+
cliPath,
|
|
13904
|
+
transcriptPath,
|
|
13905
|
+
// `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
|
|
13906
|
+
// sibling key, and that one refreshes LRU order and cancels idle
|
|
13907
|
+
// timers on everything it touches (the same reason the fork lookup
|
|
13908
|
+
// above uses it).
|
|
13909
|
+
isBusy: claudeSessionIsWriting,
|
|
13910
|
+
onRefused: (reason) => {
|
|
13911
|
+
replayReason = reason;
|
|
13912
|
+
}
|
|
13913
|
+
});
|
|
13914
|
+
if (sibling) {
|
|
13915
|
+
deleteActiveProcess(sibling.siblingKey);
|
|
13916
|
+
const carried = transferClaudeSession(sibling.siblingKey, sk);
|
|
13917
|
+
if (carried) {
|
|
13918
|
+
includeHistoryContext = false;
|
|
13919
|
+
log.notice(
|
|
13920
|
+
"continuing this conversation's claude session under the new model or effort instead of replaying it",
|
|
13921
|
+
{
|
|
13922
|
+
sessionKey: sk,
|
|
13923
|
+
siblingKey: sibling.siblingKey,
|
|
13924
|
+
matchedMessages: sibling.matched
|
|
13925
|
+
}
|
|
13926
|
+
);
|
|
13927
|
+
} else {
|
|
13928
|
+
replayReason = "sibling-carry-over-failed";
|
|
13929
|
+
}
|
|
13930
|
+
}
|
|
13537
13931
|
}
|
|
13538
13932
|
}
|
|
13933
|
+
if (includeHistoryContext && !compactionMode) {
|
|
13934
|
+
log.notice("replaying the conversation as text: no claude session was available to continue", {
|
|
13935
|
+
sessionKey: sk,
|
|
13936
|
+
reason: replayReason,
|
|
13937
|
+
messages: options.prompt.length
|
|
13938
|
+
});
|
|
13939
|
+
}
|
|
13539
13940
|
if (this.config.forkSessions === true && !compactionMode) {
|
|
13540
13941
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
13541
13942
|
}
|
|
@@ -14288,6 +14689,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14288
14689
|
})
|
|
14289
14690
|
});
|
|
14290
14691
|
state.endTextBlock();
|
|
14692
|
+
state.resultFailure ??= "usage_limit";
|
|
14291
14693
|
}
|
|
14292
14694
|
if (runAutoContinue(state, msg)) return;
|
|
14293
14695
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
@@ -14321,6 +14723,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14321
14723
|
});
|
|
14322
14724
|
state.endTextBlock();
|
|
14323
14725
|
}
|
|
14726
|
+
if (compactionMode && state.resultFailure) {
|
|
14727
|
+
log.warn("claude could not compact the conversation; no summary was stored", {
|
|
14728
|
+
sessionKey: sk,
|
|
14729
|
+
cause: state.resultFailure
|
|
14730
|
+
});
|
|
14731
|
+
controller.enqueue({
|
|
14732
|
+
type: "error",
|
|
14733
|
+
error: new Error(
|
|
14734
|
+
formatCompactionFailure(state.resultFailure, state.compactionFailureText)
|
|
14735
|
+
)
|
|
14736
|
+
});
|
|
14737
|
+
}
|
|
14324
14738
|
const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
|
|
14325
14739
|
controller.enqueue({
|
|
14326
14740
|
type: "finish",
|
|
@@ -14716,7 +15130,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14716
15130
|
import {
|
|
14717
15131
|
existsSync as existsSync5,
|
|
14718
15132
|
mkdirSync as mkdirSync5,
|
|
14719
|
-
readFileSync as
|
|
15133
|
+
readFileSync as readFileSync7,
|
|
14720
15134
|
realpathSync as realpathSync4,
|
|
14721
15135
|
rmSync as rmSync3,
|
|
14722
15136
|
writeFileSync as writeFileSync7
|
|
@@ -14743,7 +15157,7 @@ function userIntendsToUseUnscoped() {
|
|
|
14743
15157
|
const cfg = userOpencodeJsonPath();
|
|
14744
15158
|
if (!existsSync5(cfg)) return false;
|
|
14745
15159
|
try {
|
|
14746
|
-
const json = JSON.parse(
|
|
15160
|
+
const json = JSON.parse(readFileSync7(cfg, "utf8"));
|
|
14747
15161
|
const plugins = json.plugin;
|
|
14748
15162
|
if (!Array.isArray(plugins)) return false;
|
|
14749
15163
|
return plugins.some(
|
|
@@ -14764,7 +15178,7 @@ function ourLoadedDir() {
|
|
|
14764
15178
|
function ourVersion(dir) {
|
|
14765
15179
|
if (!dir) return null;
|
|
14766
15180
|
try {
|
|
14767
|
-
const pkg = JSON.parse(
|
|
15181
|
+
const pkg = JSON.parse(readFileSync7(join13(dir, "package.json"), "utf8"));
|
|
14768
15182
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
14769
15183
|
} catch {
|
|
14770
15184
|
return null;
|
|
@@ -14776,7 +15190,7 @@ function markerPath() {
|
|
|
14776
15190
|
}
|
|
14777
15191
|
function markerVersion() {
|
|
14778
15192
|
try {
|
|
14779
|
-
const marker = JSON.parse(
|
|
15193
|
+
const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
|
|
14780
15194
|
return typeof marker.version === "string" ? marker.version : null;
|
|
14781
15195
|
} catch {
|
|
14782
15196
|
return null;
|
|
@@ -14833,7 +15247,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14833
15247
|
if (!existsSync5(pkgJsonPath)) return;
|
|
14834
15248
|
let pkg = {};
|
|
14835
15249
|
try {
|
|
14836
|
-
pkg = JSON.parse(
|
|
15250
|
+
pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
|
|
14837
15251
|
} catch {
|
|
14838
15252
|
return;
|
|
14839
15253
|
}
|
|
@@ -14852,7 +15266,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
14852
15266
|
const cachePkgJson = join13(cacheRoot, "package.json");
|
|
14853
15267
|
if (!existsSync5(cachePkgJson)) return;
|
|
14854
15268
|
try {
|
|
14855
|
-
const cfg = JSON.parse(
|
|
15269
|
+
const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
|
|
14856
15270
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
14857
15271
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
14858
15272
|
writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -14932,7 +15346,7 @@ function registerOpencodeLogSink(client) {
|
|
|
14932
15346
|
}
|
|
14933
15347
|
|
|
14934
15348
|
// src/v2-client.ts
|
|
14935
|
-
import
|
|
15349
|
+
import path11 from "path";
|
|
14936
15350
|
function formatAgentTypeList(agents) {
|
|
14937
15351
|
const lines = agents.flatMap((agent) => {
|
|
14938
15352
|
if (agent.hidden === true || agent.mode === "primary") return [];
|
|
@@ -14961,7 +15375,7 @@ function toV1Session(info) {
|
|
|
14961
15375
|
if (!info || typeof info !== "object") return void 0;
|
|
14962
15376
|
const session = info;
|
|
14963
15377
|
const root = session.location?.directory;
|
|
14964
|
-
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ?
|
|
15378
|
+
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path11.resolve(root, session.subpath) : root : void 0;
|
|
14965
15379
|
return { ...info, directory, parentID: session.parentID };
|
|
14966
15380
|
}
|
|
14967
15381
|
function toV1Messages(messages) {
|
|
@@ -15519,6 +15933,7 @@ function createClaudeCode(settings = {}) {
|
|
|
15519
15933
|
turnStats: settings.turnStats === true,
|
|
15520
15934
|
forkSessions: settings.forkSessions === true,
|
|
15521
15935
|
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
15936
|
+
resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
|
|
15522
15937
|
transport: settings.transport,
|
|
15523
15938
|
interactive: settings.interactive,
|
|
15524
15939
|
interactiveBypass: settings.interactiveBypass,
|