@khalilgharbaoui/opencode-claude-code-plugin 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +26 -0
- package/dist/index.js +537 -198
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +11 -6
package/dist/index.js
CHANGED
|
@@ -1490,7 +1490,7 @@ function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${A
|
|
|
1490
1490
|
options: [
|
|
1491
1491
|
...candidates.map((candidate) => ({
|
|
1492
1492
|
label: candidate,
|
|
1493
|
-
description: `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session
|
|
1493
|
+
description: input.carryTranscript === false ? `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session, and any MCP server configured only in "${source}"'s Claude profile will be missing.` : `Run on "${candidate}" until ${until}. The conversation continues where it stopped if its Claude transcript can be carried across, and is replayed as a fresh Claude session if it cannot; either way, any MCP server configured only in "${source}"'s Claude profile will be missing.`
|
|
1494
1494
|
})),
|
|
1495
1495
|
{
|
|
1496
1496
|
label: STOP_ANSWER,
|
|
@@ -1601,20 +1601,20 @@ function stripAccountFailoverParts(prompt) {
|
|
|
1601
1601
|
}
|
|
1602
1602
|
return changed ? out : prompt;
|
|
1603
1603
|
}
|
|
1604
|
-
function failoverContinuationText(target) {
|
|
1604
|
+
function failoverContinuationText(target, resumed = false) {
|
|
1605
1605
|
return [
|
|
1606
|
-
`The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account and you are now in a fresh Claude session.`,
|
|
1607
|
-
"
|
|
1606
|
+
resumed ? `The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account. This is the same conversation, continued: everything you already know still stands.` : `The Claude account this conversation was running on hit its usage limit, so it has been moved to the "${target}" account and you are now in a fresh Claude session. The conversation so far is above.`,
|
|
1607
|
+
"Continue the task from where it stopped: do not start over, do not re-plan, and do not repeat work that is already done.",
|
|
1608
1608
|
"Do not mention the account switch unless you are asked about it."
|
|
1609
1609
|
].join(" ");
|
|
1610
1610
|
}
|
|
1611
|
-
function buildFailoverContinuationPrompt(prompt, target) {
|
|
1611
|
+
function buildFailoverContinuationPrompt(prompt, target, resumed = false) {
|
|
1612
1612
|
const stripped = stripAccountFailoverParts(prompt);
|
|
1613
1613
|
return [
|
|
1614
1614
|
...stripped,
|
|
1615
1615
|
{
|
|
1616
1616
|
role: "user",
|
|
1617
|
-
content: [{ type: "text", text: failoverContinuationText(target) }]
|
|
1617
|
+
content: [{ type: "text", text: failoverContinuationText(target, resumed) }]
|
|
1618
1618
|
}
|
|
1619
1619
|
];
|
|
1620
1620
|
}
|
|
@@ -1625,7 +1625,108 @@ function failoverUntil(resetsAt) {
|
|
|
1625
1625
|
// src/cli-version.ts
|
|
1626
1626
|
import { execFile } from "child_process";
|
|
1627
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
|
|
1628
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
|
+
}
|
|
1629
1730
|
var cache = /* @__PURE__ */ new Map();
|
|
1630
1731
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
1631
1732
|
var probeTimeoutMs = PROBE_TIMEOUT_MS;
|
|
@@ -1672,7 +1773,7 @@ function detectCliVersion(cliPath) {
|
|
|
1672
1773
|
let deadlineKill = false;
|
|
1673
1774
|
const promise = (async () => {
|
|
1674
1775
|
try {
|
|
1675
|
-
const { stdout } = await
|
|
1776
|
+
const { stdout } = await probeExec(cliPath, ["--version"], {
|
|
1676
1777
|
timeout: probeTimeoutMs
|
|
1677
1778
|
});
|
|
1678
1779
|
const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
|
|
@@ -1769,7 +1870,7 @@ function detectHeadlessSupport(cliPath) {
|
|
|
1769
1870
|
let deadlineKill = false;
|
|
1770
1871
|
const promise = (async () => {
|
|
1771
1872
|
try {
|
|
1772
|
-
const execution =
|
|
1873
|
+
const execution = probeExec(cliPath, [
|
|
1773
1874
|
"--print",
|
|
1774
1875
|
"--input-format",
|
|
1775
1876
|
"stream-json",
|
|
@@ -1805,7 +1906,7 @@ function detectCliSupportsFlag(cliPath, flag) {
|
|
|
1805
1906
|
let deadlineKill = false;
|
|
1806
1907
|
const promise = (async () => {
|
|
1807
1908
|
try {
|
|
1808
|
-
const execution =
|
|
1909
|
+
const execution = probeExec(cliPath, ["--help"], {
|
|
1809
1910
|
timeout: probeTimeoutMs,
|
|
1810
1911
|
killSignal: "SIGKILL",
|
|
1811
1912
|
maxBuffer: 4 * 1024 * 1024
|
|
@@ -1838,17 +1939,17 @@ import { unlink } from "fs/promises";
|
|
|
1838
1939
|
|
|
1839
1940
|
// src/proxy-mcp.ts
|
|
1840
1941
|
import { createServer } from "http";
|
|
1841
|
-
import * as
|
|
1842
|
-
import * as
|
|
1942
|
+
import * as fs3 from "fs";
|
|
1943
|
+
import * as path4 from "path";
|
|
1843
1944
|
import * as crypto from "crypto";
|
|
1844
1945
|
import { EventEmitter } from "events";
|
|
1845
1946
|
|
|
1846
1947
|
// src/tmp.ts
|
|
1847
|
-
import * as
|
|
1948
|
+
import * as fs2 from "fs";
|
|
1848
1949
|
import * as os from "os";
|
|
1849
|
-
import * as
|
|
1950
|
+
import * as path3 from "path";
|
|
1850
1951
|
var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
|
|
1851
|
-
var PID_TMP_DIR =
|
|
1952
|
+
var PID_TMP_DIR = path3.join(
|
|
1852
1953
|
os.tmpdir(),
|
|
1853
1954
|
`${PLUGIN_TMP_PREFIX}${process.pid}`
|
|
1854
1955
|
);
|
|
@@ -1861,7 +1962,7 @@ function currentUid() {
|
|
|
1861
1962
|
function untrustworthyReason(dir) {
|
|
1862
1963
|
let stat;
|
|
1863
1964
|
try {
|
|
1864
|
-
stat =
|
|
1965
|
+
stat = fs2.lstatSync(dir);
|
|
1865
1966
|
} catch {
|
|
1866
1967
|
return null;
|
|
1867
1968
|
}
|
|
@@ -1874,16 +1975,16 @@ function untrustworthyReason(dir) {
|
|
|
1874
1975
|
return null;
|
|
1875
1976
|
}
|
|
1876
1977
|
function ensureDir(dir) {
|
|
1877
|
-
|
|
1978
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1878
1979
|
try {
|
|
1879
|
-
|
|
1980
|
+
fs2.chmodSync(dir, 448);
|
|
1880
1981
|
} catch {
|
|
1881
1982
|
}
|
|
1882
1983
|
}
|
|
1883
1984
|
function chooseDir() {
|
|
1884
1985
|
const reason = untrustworthyReason(PID_TMP_DIR);
|
|
1885
1986
|
if (!reason) return PID_TMP_DIR;
|
|
1886
|
-
const fallback =
|
|
1987
|
+
const fallback = fs2.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
|
|
1887
1988
|
log.warn("refusing the pid-named scratch directory", {
|
|
1888
1989
|
path: PID_TMP_DIR,
|
|
1889
1990
|
reason,
|
|
@@ -1897,7 +1998,7 @@ function registerExitCleanup() {
|
|
|
1897
1998
|
process.on("exit", () => {
|
|
1898
1999
|
for (const dir of createdDirs) {
|
|
1899
2000
|
try {
|
|
1900
|
-
|
|
2001
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
1901
2002
|
} catch {
|
|
1902
2003
|
}
|
|
1903
2004
|
}
|
|
@@ -1918,23 +2019,23 @@ function sweepStalePluginTmpDirs(keep) {
|
|
|
1918
2019
|
const root = os.tmpdir();
|
|
1919
2020
|
let entries;
|
|
1920
2021
|
try {
|
|
1921
|
-
entries =
|
|
2022
|
+
entries = fs2.readdirSync(root);
|
|
1922
2023
|
} catch {
|
|
1923
2024
|
return removed;
|
|
1924
2025
|
}
|
|
1925
2026
|
const uid = currentUid();
|
|
1926
|
-
const keepPath = keep ?
|
|
2027
|
+
const keepPath = keep ? path3.resolve(keep) : null;
|
|
1927
2028
|
for (const entry of entries) {
|
|
1928
2029
|
const match = PID_DIR_NAME.exec(entry);
|
|
1929
2030
|
if (!match) continue;
|
|
1930
2031
|
const pid = Number(match[1]);
|
|
1931
2032
|
if (!Number.isSafeInteger(pid) || pid <= 0) continue;
|
|
1932
2033
|
if (pid === process.pid) continue;
|
|
1933
|
-
const dir =
|
|
1934
|
-
if (keepPath &&
|
|
2034
|
+
const dir = path3.join(root, entry);
|
|
2035
|
+
if (keepPath && path3.resolve(dir) === keepPath) continue;
|
|
1935
2036
|
let stat;
|
|
1936
2037
|
try {
|
|
1937
|
-
stat =
|
|
2038
|
+
stat = fs2.lstatSync(dir);
|
|
1938
2039
|
} catch {
|
|
1939
2040
|
continue;
|
|
1940
2041
|
}
|
|
@@ -1942,7 +2043,7 @@ function sweepStalePluginTmpDirs(keep) {
|
|
|
1942
2043
|
if (uid !== void 0 && stat.uid !== uid) continue;
|
|
1943
2044
|
if (isProcessAlive(pid)) continue;
|
|
1944
2045
|
try {
|
|
1945
|
-
|
|
2046
|
+
fs2.rmSync(dir, { recursive: true, force: true });
|
|
1946
2047
|
removed.push(dir);
|
|
1947
2048
|
} catch (err) {
|
|
1948
2049
|
log.debug("could not remove a stale scratch directory", {
|
|
@@ -2773,11 +2874,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2773
2874
|
2
|
|
2774
2875
|
);
|
|
2775
2876
|
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
2776
|
-
const outPath =
|
|
2877
|
+
const outPath = path4.join(
|
|
2777
2878
|
pluginTmpDir(),
|
|
2778
2879
|
`proxy-${hash}.json`
|
|
2779
2880
|
);
|
|
2780
|
-
|
|
2881
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
2781
2882
|
configFilePath = outPath;
|
|
2782
2883
|
return outPath;
|
|
2783
2884
|
},
|
|
@@ -2794,7 +2895,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2794
2895
|
});
|
|
2795
2896
|
if (configFilePath) {
|
|
2796
2897
|
try {
|
|
2797
|
-
|
|
2898
|
+
fs3.unlinkSync(configFilePath);
|
|
2798
2899
|
} catch {
|
|
2799
2900
|
}
|
|
2800
2901
|
configFilePath = null;
|
|
@@ -3363,8 +3464,8 @@ function clearCompression(sessionKey2) {
|
|
|
3363
3464
|
}
|
|
3364
3465
|
|
|
3365
3466
|
// src/mcp-bridge.ts
|
|
3366
|
-
import * as
|
|
3367
|
-
import * as
|
|
3467
|
+
import * as fs4 from "fs";
|
|
3468
|
+
import * as path5 from "path";
|
|
3368
3469
|
import * as os2 from "os";
|
|
3369
3470
|
import * as crypto2 from "crypto";
|
|
3370
3471
|
import {
|
|
@@ -3375,21 +3476,21 @@ var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
|
3375
3476
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3376
3477
|
function fileExists(p) {
|
|
3377
3478
|
try {
|
|
3378
|
-
return
|
|
3479
|
+
return fs4.statSync(p).isFile();
|
|
3379
3480
|
} catch {
|
|
3380
3481
|
return false;
|
|
3381
3482
|
}
|
|
3382
3483
|
}
|
|
3383
3484
|
function dirExists(p) {
|
|
3384
3485
|
try {
|
|
3385
|
-
return
|
|
3486
|
+
return fs4.statSync(p).isDirectory();
|
|
3386
3487
|
} catch {
|
|
3387
3488
|
return false;
|
|
3388
3489
|
}
|
|
3389
3490
|
}
|
|
3390
3491
|
function readAndParse(file) {
|
|
3391
3492
|
try {
|
|
3392
|
-
const raw =
|
|
3493
|
+
const raw = fs4.readFileSync(file, "utf8");
|
|
3393
3494
|
const errors = [];
|
|
3394
3495
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3395
3496
|
if (errors.length > 0) {
|
|
@@ -3425,14 +3526,14 @@ function deepMerge(target, source) {
|
|
|
3425
3526
|
}
|
|
3426
3527
|
function walkUp(opts) {
|
|
3427
3528
|
const out = [];
|
|
3428
|
-
let current =
|
|
3529
|
+
let current = path5.resolve(opts.start);
|
|
3429
3530
|
while (true) {
|
|
3430
3531
|
for (const target of opts.targets) {
|
|
3431
|
-
const candidate =
|
|
3532
|
+
const candidate = path5.join(current, target);
|
|
3432
3533
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3433
3534
|
}
|
|
3434
|
-
if (opts.stop && current ===
|
|
3435
|
-
const parent =
|
|
3535
|
+
if (opts.stop && current === path5.resolve(opts.stop)) break;
|
|
3536
|
+
const parent = path5.dirname(current);
|
|
3436
3537
|
if (parent === current) break;
|
|
3437
3538
|
current = parent;
|
|
3438
3539
|
}
|
|
@@ -3440,28 +3541,28 @@ function walkUp(opts) {
|
|
|
3440
3541
|
}
|
|
3441
3542
|
function detectWorktree(cwd) {
|
|
3442
3543
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3443
|
-
if (override) return
|
|
3444
|
-
let current =
|
|
3544
|
+
if (override) return path5.resolve(override);
|
|
3545
|
+
let current = path5.resolve(cwd);
|
|
3445
3546
|
while (true) {
|
|
3446
|
-
const gitPath =
|
|
3547
|
+
const gitPath = path5.join(current, ".git");
|
|
3447
3548
|
try {
|
|
3448
|
-
if (
|
|
3549
|
+
if (fs4.existsSync(gitPath)) return current;
|
|
3449
3550
|
} catch {
|
|
3450
3551
|
}
|
|
3451
|
-
const parent =
|
|
3552
|
+
const parent = path5.dirname(current);
|
|
3452
3553
|
if (parent === current) return void 0;
|
|
3453
3554
|
current = parent;
|
|
3454
3555
|
}
|
|
3455
3556
|
}
|
|
3456
3557
|
function globalConfigDir() {
|
|
3457
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3458
|
-
return
|
|
3558
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os2.homedir(), ".config");
|
|
3559
|
+
return path5.join(xdg, "opencode");
|
|
3459
3560
|
}
|
|
3460
3561
|
function loadGlobalConfig(hostApi = "v1") {
|
|
3461
3562
|
const dir = globalConfigDir();
|
|
3462
3563
|
let merged = {};
|
|
3463
3564
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3464
|
-
const file =
|
|
3565
|
+
const file = path5.join(dir, name);
|
|
3465
3566
|
if (!fileExists(file)) continue;
|
|
3466
3567
|
const parsed = readAndParse(file);
|
|
3467
3568
|
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
@@ -3471,7 +3572,7 @@ function loadGlobalConfig(hostApi = "v1") {
|
|
|
3471
3572
|
function loadProjectFilesInDir(dir, hostApi = "v1") {
|
|
3472
3573
|
let merged = {};
|
|
3473
3574
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3474
|
-
const file =
|
|
3575
|
+
const file = path5.join(dir, name);
|
|
3475
3576
|
if (!fileExists(file)) continue;
|
|
3476
3577
|
const parsed = readAndParse(file);
|
|
3477
3578
|
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
@@ -3482,7 +3583,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3482
3583
|
const dirs = [];
|
|
3483
3584
|
const seen = /* @__PURE__ */ new Set();
|
|
3484
3585
|
const push = (p) => {
|
|
3485
|
-
const abs =
|
|
3586
|
+
const abs = path5.resolve(p);
|
|
3486
3587
|
if (!seen.has(abs) && dirExists(abs)) {
|
|
3487
3588
|
seen.add(abs);
|
|
3488
3589
|
dirs.push(abs);
|
|
@@ -3498,7 +3599,7 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3498
3599
|
}
|
|
3499
3600
|
const home = os2.homedir();
|
|
3500
3601
|
if (home) {
|
|
3501
|
-
const homeDot =
|
|
3602
|
+
const homeDot = path5.join(home, ".opencode");
|
|
3502
3603
|
if (dirExists(homeDot)) push(homeDot);
|
|
3503
3604
|
}
|
|
3504
3605
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
@@ -3645,7 +3746,7 @@ function opencodeConfigLayers(cwd, hostApi = "v1") {
|
|
|
3645
3746
|
const projectDirs = [];
|
|
3646
3747
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3647
3748
|
for (const f of projectFiles) {
|
|
3648
|
-
const d =
|
|
3749
|
+
const d = path5.dirname(f);
|
|
3649
3750
|
if (!seenProjectDirs.has(d)) {
|
|
3650
3751
|
seenProjectDirs.add(d);
|
|
3651
3752
|
projectDirs.push(d);
|
|
@@ -3712,13 +3813,13 @@ function finishBridge(input) {
|
|
|
3712
3813
|
}
|
|
3713
3814
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3714
3815
|
const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3715
|
-
const outPath =
|
|
3816
|
+
const outPath = path5.join(
|
|
3716
3817
|
pluginTmpDir(),
|
|
3717
3818
|
`mcp-${bodyDigest}.json`
|
|
3718
3819
|
);
|
|
3719
3820
|
try {
|
|
3720
3821
|
if (!fileExists(outPath)) {
|
|
3721
|
-
|
|
3822
|
+
fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3722
3823
|
}
|
|
3723
3824
|
} catch (e) {
|
|
3724
3825
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -5308,11 +5409,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5308
5409
|
effort,
|
|
5309
5410
|
promptCacheTtl
|
|
5310
5411
|
});
|
|
5311
|
-
const
|
|
5412
|
+
const plan = planClaudeSpawn(cliPath, cliArgs);
|
|
5413
|
+
const proc = spawn(plan.file, plan.args, {
|
|
5312
5414
|
cwd,
|
|
5313
5415
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5314
5416
|
env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
|
|
5315
|
-
|
|
5417
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
5316
5418
|
});
|
|
5317
5419
|
const lineEmitter = new EventEmitter3();
|
|
5318
5420
|
const ap = {
|
|
@@ -5892,6 +5994,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5892
5994
|
"btw: a turn is running but no claude process appeared for it",
|
|
5893
5995
|
"btw: early aside failed; the message will ask again",
|
|
5894
5996
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
5997
|
+
"carried this conversation's claude transcript to the other account",
|
|
5895
5998
|
"carrying the claude conversation over to the new session key",
|
|
5896
5999
|
"claude --version output unparseable",
|
|
5897
6000
|
"claude account is out of usage; the turn ends with a note",
|
|
@@ -5989,6 +6092,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5989
6092
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
5990
6093
|
"replaying stdout the child emitted between turns",
|
|
5991
6094
|
"replaying the conversation as text: no claude session was available to continue",
|
|
6095
|
+
"replaying this conversation as text on the other account",
|
|
5992
6096
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
5993
6097
|
"resuming the claude session from before the restart instead of replaying it",
|
|
5994
6098
|
"shared Claude capability already exists; leaving untouched",
|
|
@@ -6033,6 +6137,8 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
6033
6137
|
emitted: "flag",
|
|
6034
6138
|
enabled: "flag",
|
|
6035
6139
|
executed: "flag",
|
|
6140
|
+
failedOver: "flag",
|
|
6141
|
+
renamed: "flag",
|
|
6036
6142
|
isUsingOverage: "flag",
|
|
6037
6143
|
hadProxyActivity: "flag",
|
|
6038
6144
|
hadReasoning: "flag",
|
|
@@ -6411,8 +6517,8 @@ function redactLogLine(raw, context) {
|
|
|
6411
6517
|
if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
|
|
6412
6518
|
return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
|
|
6413
6519
|
}
|
|
6414
|
-
function readLogTail(
|
|
6415
|
-
const fd = openSync(
|
|
6520
|
+
function readLogTail(path13, maxBytes = BUNDLE_TAIL_BYTES) {
|
|
6521
|
+
const fd = openSync(path13, "r");
|
|
6416
6522
|
try {
|
|
6417
6523
|
const size = fstatSync(fd).size;
|
|
6418
6524
|
const length = Math.min(size, maxBytes);
|
|
@@ -6562,13 +6668,20 @@ function parsePlanUsage(stdout) {
|
|
|
6562
6668
|
return { status: "failed", error: "no result object in the CLI's reply" };
|
|
6563
6669
|
}
|
|
6564
6670
|
function runCli(cliPath, args, timeoutMs, env) {
|
|
6671
|
+
const plan = planClaudeSpawn(cliPath, args);
|
|
6565
6672
|
return new Promise((resolve6, reject) => {
|
|
6566
6673
|
execFile2(
|
|
6567
|
-
|
|
6568
|
-
args,
|
|
6674
|
+
plan.file,
|
|
6675
|
+
plan.args,
|
|
6569
6676
|
// `killSignal` so a CLI wedged on a hook is actually gone, and a generous
|
|
6570
6677
|
// buffer because the reply is prose of unbounded length.
|
|
6571
|
-
{
|
|
6678
|
+
{
|
|
6679
|
+
timeout: timeoutMs,
|
|
6680
|
+
killSignal: "SIGKILL",
|
|
6681
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
6682
|
+
env,
|
|
6683
|
+
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
6684
|
+
},
|
|
6572
6685
|
(error, stdout) => {
|
|
6573
6686
|
if (stdout && stdout.trim()) return resolve6(stdout);
|
|
6574
6687
|
if (error) return reject(error);
|
|
@@ -6607,14 +6720,14 @@ async function fetchPlanUsage(cliPath, options = {}) {
|
|
|
6607
6720
|
}
|
|
6608
6721
|
|
|
6609
6722
|
// src/stale-build.ts
|
|
6610
|
-
import * as
|
|
6611
|
-
import * as
|
|
6723
|
+
import * as fs6 from "fs";
|
|
6724
|
+
import * as path7 from "path";
|
|
6612
6725
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6613
6726
|
|
|
6614
6727
|
// src/startup-diagnostics.ts
|
|
6615
6728
|
import { execFile as execFile3 } from "child_process";
|
|
6616
|
-
import * as
|
|
6617
|
-
import * as
|
|
6729
|
+
import * as fs5 from "fs";
|
|
6730
|
+
import * as path6 from "path";
|
|
6618
6731
|
import { promisify as promisify2 } from "util";
|
|
6619
6732
|
import { fileURLToPath } from "url";
|
|
6620
6733
|
|
|
@@ -6649,8 +6762,8 @@ var cachedPluginVersion;
|
|
|
6649
6762
|
function pluginVersion() {
|
|
6650
6763
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
6651
6764
|
try {
|
|
6652
|
-
const here =
|
|
6653
|
-
const raw =
|
|
6765
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
6766
|
+
const raw = fs5.readFileSync(path6.join(here, "..", "package.json"), "utf8");
|
|
6654
6767
|
const version = JSON.parse(raw).version;
|
|
6655
6768
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
6656
6769
|
} catch {
|
|
@@ -6674,7 +6787,7 @@ var opencodeVersionProbe;
|
|
|
6674
6787
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
6675
6788
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
6676
6789
|
opencodeVersionProbe = (async () => {
|
|
6677
|
-
if (!
|
|
6790
|
+
if (!path6.basename(execPath).toLowerCase().includes("opencode")) {
|
|
6678
6791
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
6679
6792
|
return void 0;
|
|
6680
6793
|
}
|
|
@@ -6903,10 +7016,10 @@ function defaultEntryPath() {
|
|
|
6903
7016
|
}
|
|
6904
7017
|
function readOnDiskBuild(entryPath) {
|
|
6905
7018
|
try {
|
|
6906
|
-
const raw =
|
|
7019
|
+
const raw = fs6.readFileSync(path7.join(path7.dirname(entryPath), "..", "package.json"), "utf8");
|
|
6907
7020
|
const version = JSON.parse(raw).version;
|
|
6908
7021
|
if (typeof version !== "string" || version.length === 0) return void 0;
|
|
6909
|
-
const stat =
|
|
7022
|
+
const stat = fs6.statSync(entryPath);
|
|
6910
7023
|
return { version, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
6911
7024
|
} catch {
|
|
6912
7025
|
return void 0;
|
|
@@ -6917,7 +7030,7 @@ function recordLoadedBuild() {
|
|
|
6917
7030
|
let mtimeMs;
|
|
6918
7031
|
let size;
|
|
6919
7032
|
try {
|
|
6920
|
-
const stat =
|
|
7033
|
+
const stat = fs6.statSync(entryPath);
|
|
6921
7034
|
mtimeMs = stat.mtimeMs;
|
|
6922
7035
|
size = stat.size;
|
|
6923
7036
|
} catch {
|
|
@@ -7354,7 +7467,7 @@ Could not build the report: ${message}`;
|
|
|
7354
7467
|
|
|
7355
7468
|
// src/agent-models.ts
|
|
7356
7469
|
import { readFile, readdir } from "fs/promises";
|
|
7357
|
-
import
|
|
7470
|
+
import path8 from "path";
|
|
7358
7471
|
|
|
7359
7472
|
// src/models.ts
|
|
7360
7473
|
var PROVIDER_ID = "claude-code";
|
|
@@ -7878,13 +7991,13 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7878
7991
|
const name = entry.slice(0, -3);
|
|
7879
7992
|
if (records2[name]) continue;
|
|
7880
7993
|
try {
|
|
7881
|
-
const text = await readFile(
|
|
7994
|
+
const text = await readFile(path8.join(directory, entry), "utf8");
|
|
7882
7995
|
records2[name] = parseAgentFrontmatter(text);
|
|
7883
7996
|
const prompt = agentMarkdownBody(text);
|
|
7884
7997
|
if (prompt) records2[name].prompt = prompt;
|
|
7885
7998
|
} catch (err) {
|
|
7886
7999
|
log.debug("failed to read agent markdown", {
|
|
7887
|
-
file:
|
|
8000
|
+
file: path8.join(directory, entry),
|
|
7888
8001
|
error: String(err)
|
|
7889
8002
|
});
|
|
7890
8003
|
}
|
|
@@ -7896,12 +8009,12 @@ function agentDirectories(home, projectDirectory) {
|
|
|
7896
8009
|
const directories = [];
|
|
7897
8010
|
if (projectDirectory) {
|
|
7898
8011
|
for (const name of AGENT_DIR_NAMES) {
|
|
7899
|
-
directories.push(
|
|
8012
|
+
directories.push(path8.join(projectDirectory, ".opencode", name));
|
|
7900
8013
|
}
|
|
7901
8014
|
}
|
|
7902
8015
|
if (home) {
|
|
7903
8016
|
for (const name of AGENT_DIR_NAMES) {
|
|
7904
|
-
directories.push(
|
|
8017
|
+
directories.push(path8.join(home, ".config", "opencode", name));
|
|
7905
8018
|
}
|
|
7906
8019
|
}
|
|
7907
8020
|
return directories;
|
|
@@ -8501,14 +8614,14 @@ function getTrailingUserMessages(prompt, opts = {}) {
|
|
|
8501
8614
|
|
|
8502
8615
|
// src/skill-bridge.ts
|
|
8503
8616
|
import * as crypto3 from "crypto";
|
|
8504
|
-
import * as
|
|
8617
|
+
import * as fs7 from "fs";
|
|
8505
8618
|
import * as os3 from "os";
|
|
8506
|
-
import * as
|
|
8619
|
+
import * as path9 from "path";
|
|
8507
8620
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8508
8621
|
function bundledSkillsDir() {
|
|
8509
8622
|
try {
|
|
8510
8623
|
const here = fileURLToPath3(import.meta.url);
|
|
8511
|
-
const dir =
|
|
8624
|
+
const dir = path9.resolve(path9.dirname(here), "..", "skills");
|
|
8512
8625
|
return dirExists2(dir) ? dir : null;
|
|
8513
8626
|
} catch {
|
|
8514
8627
|
return null;
|
|
@@ -8516,16 +8629,16 @@ function bundledSkillsDir() {
|
|
|
8516
8629
|
}
|
|
8517
8630
|
function dirExists2(p) {
|
|
8518
8631
|
try {
|
|
8519
|
-
return
|
|
8632
|
+
return fs7.statSync(p).isDirectory();
|
|
8520
8633
|
} catch {
|
|
8521
8634
|
return false;
|
|
8522
8635
|
}
|
|
8523
8636
|
}
|
|
8524
8637
|
function realPath(p) {
|
|
8525
8638
|
try {
|
|
8526
|
-
return
|
|
8639
|
+
return fs7.realpathSync(p);
|
|
8527
8640
|
} catch {
|
|
8528
|
-
return
|
|
8641
|
+
return path9.resolve(p);
|
|
8529
8642
|
}
|
|
8530
8643
|
}
|
|
8531
8644
|
function envEnabled(name) {
|
|
@@ -8546,10 +8659,10 @@ function declaredSkillName(source) {
|
|
|
8546
8659
|
return SAFE_SKILL_NAME.test(name) ? name : null;
|
|
8547
8660
|
}
|
|
8548
8661
|
function readSkill(root, entry) {
|
|
8549
|
-
const dir =
|
|
8662
|
+
const dir = path9.join(root, entry);
|
|
8550
8663
|
let source;
|
|
8551
8664
|
try {
|
|
8552
|
-
source =
|
|
8665
|
+
source = fs7.readFileSync(path9.join(dir, "SKILL.md"), "utf8");
|
|
8553
8666
|
} catch {
|
|
8554
8667
|
return null;
|
|
8555
8668
|
}
|
|
@@ -8567,47 +8680,47 @@ function skillRoots(cwd) {
|
|
|
8567
8680
|
const roots = [];
|
|
8568
8681
|
const seen = /* @__PURE__ */ new Set();
|
|
8569
8682
|
const push = (p) => {
|
|
8570
|
-
const abs =
|
|
8683
|
+
const abs = path9.resolve(p);
|
|
8571
8684
|
if (seen.has(abs)) return;
|
|
8572
8685
|
seen.add(abs);
|
|
8573
8686
|
if (dirExists2(abs)) roots.push(abs);
|
|
8574
8687
|
};
|
|
8575
8688
|
const home = os3.homedir();
|
|
8576
|
-
const homeDir = home ?
|
|
8577
|
-
let current =
|
|
8689
|
+
const homeDir = home ? path9.resolve(home) : null;
|
|
8690
|
+
let current = path9.resolve(cwd);
|
|
8578
8691
|
while (true) {
|
|
8579
|
-
push(
|
|
8692
|
+
push(path9.join(current, ".opencode", "skills"));
|
|
8580
8693
|
if (current !== homeDir) {
|
|
8581
|
-
push(
|
|
8582
|
-
push(
|
|
8694
|
+
push(path9.join(current, ".claude", "skills"));
|
|
8695
|
+
push(path9.join(current, ".agents", "skills"));
|
|
8583
8696
|
}
|
|
8584
|
-
const parent =
|
|
8697
|
+
const parent = path9.dirname(current);
|
|
8585
8698
|
if (parent === current) break;
|
|
8586
8699
|
current = parent;
|
|
8587
8700
|
}
|
|
8588
|
-
if (homeDir) push(
|
|
8701
|
+
if (homeDir) push(path9.join(homeDir, ".opencode", "skills"));
|
|
8589
8702
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
8590
8703
|
if (envDir) {
|
|
8591
|
-
push(
|
|
8592
|
-
push(
|
|
8704
|
+
push(path9.join(envDir, "skills"));
|
|
8705
|
+
push(path9.join(envDir, "skill"));
|
|
8593
8706
|
}
|
|
8594
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ?
|
|
8707
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path9.join(homeDir, ".config") : null);
|
|
8595
8708
|
if (xdg) {
|
|
8596
|
-
push(
|
|
8597
|
-
push(
|
|
8709
|
+
push(path9.join(xdg, "opencode", "skills"));
|
|
8710
|
+
push(path9.join(xdg, "opencode", "skill"));
|
|
8598
8711
|
}
|
|
8599
8712
|
if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
|
|
8600
8713
|
if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
|
|
8601
|
-
push(
|
|
8714
|
+
push(path9.join(homeDir, ".claude", "skills"));
|
|
8602
8715
|
}
|
|
8603
|
-
push(
|
|
8716
|
+
push(path9.join(homeDir, ".agents", "skills"));
|
|
8604
8717
|
}
|
|
8605
8718
|
return roots;
|
|
8606
8719
|
}
|
|
8607
8720
|
function collectSkills(root, claimed, found) {
|
|
8608
8721
|
let entries;
|
|
8609
8722
|
try {
|
|
8610
|
-
entries =
|
|
8723
|
+
entries = fs7.readdirSync(root, { withFileTypes: true });
|
|
8611
8724
|
} catch {
|
|
8612
8725
|
return;
|
|
8613
8726
|
}
|
|
@@ -8647,23 +8760,23 @@ function discoverBundledSkills() {
|
|
|
8647
8760
|
}
|
|
8648
8761
|
function claudeConfigDir(configDir) {
|
|
8649
8762
|
const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8650
|
-
if (explicit) return
|
|
8763
|
+
if (explicit) return path9.resolve(expandHome(explicit));
|
|
8651
8764
|
const home = os3.homedir();
|
|
8652
|
-
return home ?
|
|
8765
|
+
return home ? path9.resolve(home, ".claude") : null;
|
|
8653
8766
|
}
|
|
8654
8767
|
function installedPluginSkillRoots(configDir, cwd) {
|
|
8655
8768
|
if (!configDir) return [];
|
|
8656
8769
|
let parsed;
|
|
8657
8770
|
try {
|
|
8658
8771
|
parsed = JSON.parse(
|
|
8659
|
-
|
|
8772
|
+
fs7.readFileSync(path9.join(configDir, "plugins", "installed_plugins.json"), "utf8")
|
|
8660
8773
|
);
|
|
8661
8774
|
} catch {
|
|
8662
8775
|
return [];
|
|
8663
8776
|
}
|
|
8664
8777
|
const plugins = parsed?.plugins;
|
|
8665
8778
|
if (!plugins || typeof plugins !== "object") return [];
|
|
8666
|
-
const workspace =
|
|
8779
|
+
const workspace = path9.resolve(cwd);
|
|
8667
8780
|
const roots = [];
|
|
8668
8781
|
for (const installs of Object.values(plugins)) {
|
|
8669
8782
|
if (!Array.isArray(installs)) continue;
|
|
@@ -8671,28 +8784,28 @@ function installedPluginSkillRoots(configDir, cwd) {
|
|
|
8671
8784
|
const entry = install;
|
|
8672
8785
|
if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
|
|
8673
8786
|
if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
|
|
8674
|
-
roots.push(
|
|
8787
|
+
roots.push(path9.join(entry.installPath, "skills"));
|
|
8675
8788
|
}
|
|
8676
8789
|
}
|
|
8677
8790
|
return roots;
|
|
8678
8791
|
}
|
|
8679
8792
|
function coversWorkspace(projectPath, workspace) {
|
|
8680
8793
|
if (typeof projectPath !== "string" || !projectPath) return false;
|
|
8681
|
-
const root =
|
|
8794
|
+
const root = path9.resolve(projectPath);
|
|
8682
8795
|
if (root === workspace) return true;
|
|
8683
|
-
const rel =
|
|
8684
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
8796
|
+
const rel = path9.relative(root, workspace);
|
|
8797
|
+
return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
8685
8798
|
}
|
|
8686
8799
|
function discoverNativeClaudeSkills(opts) {
|
|
8687
8800
|
const found = [];
|
|
8688
8801
|
const scanned = /* @__PURE__ */ new Set();
|
|
8689
8802
|
const scan = (root, scope) => {
|
|
8690
|
-
const abs =
|
|
8803
|
+
const abs = path9.resolve(root);
|
|
8691
8804
|
if (scanned.has(abs)) return;
|
|
8692
8805
|
scanned.add(abs);
|
|
8693
8806
|
let entries;
|
|
8694
8807
|
try {
|
|
8695
|
-
entries =
|
|
8808
|
+
entries = fs7.readdirSync(abs, { withFileTypes: true });
|
|
8696
8809
|
} catch {
|
|
8697
8810
|
return;
|
|
8698
8811
|
}
|
|
@@ -8704,13 +8817,13 @@ function discoverNativeClaudeSkills(opts) {
|
|
|
8704
8817
|
}
|
|
8705
8818
|
};
|
|
8706
8819
|
const configDir = claudeConfigDir(opts.configDir);
|
|
8707
|
-
if (configDir) scan(
|
|
8820
|
+
if (configDir) scan(path9.join(configDir, "skills"), "user");
|
|
8708
8821
|
const home = os3.homedir();
|
|
8709
|
-
const homeDir = home ?
|
|
8710
|
-
let current =
|
|
8822
|
+
const homeDir = home ? path9.resolve(home) : null;
|
|
8823
|
+
let current = path9.resolve(opts.cwd);
|
|
8711
8824
|
while (true) {
|
|
8712
|
-
if (current !== homeDir) scan(
|
|
8713
|
-
const parent =
|
|
8825
|
+
if (current !== homeDir) scan(path9.join(current, ".claude", "skills"), "project");
|
|
8826
|
+
const parent = path9.dirname(current);
|
|
8714
8827
|
if (parent === current) break;
|
|
8715
8828
|
current = parent;
|
|
8716
8829
|
}
|
|
@@ -8744,10 +8857,10 @@ function dropNativelyLoadedSkills(skills, native) {
|
|
|
8744
8857
|
}
|
|
8745
8858
|
function readBundledSkillInfos() {
|
|
8746
8859
|
return discoverBundledSkills().flatMap((skill) => {
|
|
8747
|
-
const file =
|
|
8860
|
+
const file = path9.join(skill.dir, "SKILL.md");
|
|
8748
8861
|
let text;
|
|
8749
8862
|
try {
|
|
8750
|
-
text =
|
|
8863
|
+
text = fs7.readFileSync(file, "utf8");
|
|
8751
8864
|
} catch {
|
|
8752
8865
|
return [];
|
|
8753
8866
|
}
|
|
@@ -8771,15 +8884,15 @@ function readBundledSkillInfos() {
|
|
|
8771
8884
|
}
|
|
8772
8885
|
function linkSkill(source, target) {
|
|
8773
8886
|
try {
|
|
8774
|
-
|
|
8887
|
+
fs7.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
8775
8888
|
return;
|
|
8776
8889
|
} catch {
|
|
8777
|
-
|
|
8890
|
+
fs7.cpSync(source, target, { recursive: true, dereference: true });
|
|
8778
8891
|
}
|
|
8779
8892
|
}
|
|
8780
8893
|
function fileExists2(p) {
|
|
8781
8894
|
try {
|
|
8782
|
-
return
|
|
8895
|
+
return fs7.statSync(p).isFile();
|
|
8783
8896
|
} catch {
|
|
8784
8897
|
return false;
|
|
8785
8898
|
}
|
|
@@ -8788,14 +8901,14 @@ function buildSkillPluginDir(skills) {
|
|
|
8788
8901
|
if (skills.length === 0) return null;
|
|
8789
8902
|
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
8790
8903
|
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
8791
|
-
const root =
|
|
8792
|
-
const manifest =
|
|
8904
|
+
const root = path9.join(pluginTmpDir(), `skills-${hash}`);
|
|
8905
|
+
const manifest = path9.join(root, ".claude-plugin", "plugin.json");
|
|
8793
8906
|
if (fileExists2(manifest)) return root;
|
|
8794
8907
|
try {
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8908
|
+
fs7.rmSync(root, { recursive: true, force: true });
|
|
8909
|
+
fs7.mkdirSync(path9.join(root, ".claude-plugin"), { recursive: true });
|
|
8910
|
+
fs7.mkdirSync(path9.join(root, "skills"), { recursive: true });
|
|
8911
|
+
fs7.writeFileSync(
|
|
8799
8912
|
manifest,
|
|
8800
8913
|
JSON.stringify(
|
|
8801
8914
|
{
|
|
@@ -8808,7 +8921,7 @@ function buildSkillPluginDir(skills) {
|
|
|
8808
8921
|
{ encoding: "utf8", mode: 384 }
|
|
8809
8922
|
);
|
|
8810
8923
|
for (const skill of skills) {
|
|
8811
|
-
linkSkill(skill.dir,
|
|
8924
|
+
linkSkill(skill.dir, path9.join(root, "skills", skill.name));
|
|
8812
8925
|
}
|
|
8813
8926
|
} catch (err) {
|
|
8814
8927
|
log.warn("failed to stage opencode skill plugin dir", {
|
|
@@ -8824,7 +8937,7 @@ function registerBundledSkillPath(config) {
|
|
|
8824
8937
|
if (!dir) return false;
|
|
8825
8938
|
config.skills ??= {};
|
|
8826
8939
|
const paths = config.skills.paths ??= [];
|
|
8827
|
-
if (paths.some((p) =>
|
|
8940
|
+
if (paths.some((p) => path9.resolve(p) === dir)) return false;
|
|
8828
8941
|
paths.push(dir);
|
|
8829
8942
|
return true;
|
|
8830
8943
|
}
|
|
@@ -8878,12 +8991,13 @@ import { join as join10 } from "path";
|
|
|
8878
8991
|
|
|
8879
8992
|
// src/claude-session-bun.ts
|
|
8880
8993
|
import * as os4 from "os";
|
|
8881
|
-
import * as
|
|
8882
|
-
import * as
|
|
8994
|
+
import * as fs8 from "fs";
|
|
8995
|
+
import * as path10 from "path";
|
|
8883
8996
|
import { execFileSync } from "child_process";
|
|
8884
8997
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
8998
|
+
import { StringDecoder } from "string_decoder";
|
|
8885
8999
|
function resolveClaude(cmd = "claude") {
|
|
8886
|
-
if (
|
|
9000
|
+
if (path10.isAbsolute(cmd) && fs8.existsSync(cmd)) return cmd;
|
|
8887
9001
|
const viaBun = Bun.which(cmd);
|
|
8888
9002
|
if (viaBun) return viaBun;
|
|
8889
9003
|
const isWin = os4.platform() === "win32";
|
|
@@ -8892,24 +9006,30 @@ function resolveClaude(cmd = "claude") {
|
|
|
8892
9006
|
encoding: "utf8",
|
|
8893
9007
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8894
9008
|
});
|
|
8895
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
9009
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs8.existsSync(p));
|
|
8896
9010
|
if (first) return first;
|
|
8897
9011
|
} catch {
|
|
8898
9012
|
}
|
|
8899
9013
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
8900
9014
|
}
|
|
8901
9015
|
function encodeCwd(cwd) {
|
|
8902
|
-
const resolved =
|
|
9016
|
+
const resolved = path10.resolve(cwd);
|
|
8903
9017
|
let real = resolved;
|
|
8904
9018
|
try {
|
|
8905
|
-
real =
|
|
9019
|
+
real = fs8.realpathSync.native(resolved);
|
|
8906
9020
|
} catch {
|
|
8907
9021
|
}
|
|
8908
9022
|
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
8909
9023
|
}
|
|
8910
9024
|
var bunPtySpawner = (argv, opts) => {
|
|
8911
9025
|
const [command, ...args] = argv;
|
|
8912
|
-
|
|
9026
|
+
const resolved = resolveClaude(command ?? "claude");
|
|
9027
|
+
if (os4.platform() === "win32" && isBatchFile(resolved)) {
|
|
9028
|
+
throw new Error(
|
|
9029
|
+
`The interactive transport cannot run a Windows batch shim (${resolved}). Point \`cliPath\` at claude.exe, or use the default headless transport.`
|
|
9030
|
+
);
|
|
9031
|
+
}
|
|
9032
|
+
return Bun.spawn([resolved, ...args], {
|
|
8913
9033
|
cwd: opts.cwd,
|
|
8914
9034
|
env: opts.env,
|
|
8915
9035
|
terminal: {
|
|
@@ -9011,6 +9131,14 @@ function classifyScreen(text) {
|
|
|
9011
9131
|
}
|
|
9012
9132
|
return null;
|
|
9013
9133
|
}
|
|
9134
|
+
function interactiveTranscriptPath(opts) {
|
|
9135
|
+
return path10.join(
|
|
9136
|
+
resolveConfigDir(opts.configDir),
|
|
9137
|
+
"projects",
|
|
9138
|
+
encodeCwd(path10.resolve(opts.cwd)),
|
|
9139
|
+
`${opts.sessionId}.jsonl`
|
|
9140
|
+
);
|
|
9141
|
+
}
|
|
9014
9142
|
function configDirEnv(configDir) {
|
|
9015
9143
|
return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
|
|
9016
9144
|
}
|
|
@@ -9124,6 +9252,123 @@ function isInterruptRecord(rec) {
|
|
|
9124
9252
|
function isTurnDurationRecord(rec) {
|
|
9125
9253
|
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
9126
9254
|
}
|
|
9255
|
+
var TRANSCRIPT_CHUNK_BYTES = 1 << 20;
|
|
9256
|
+
var TranscriptCursor = class {
|
|
9257
|
+
/** The path is asked for on every poll rather than kept, because it is a
|
|
9258
|
+
* field of the session and a test may repoint it; a path that changes is
|
|
9259
|
+
* a different file, which the `dev:ino` check already starts over on. */
|
|
9260
|
+
constructor(resolvePath) {
|
|
9261
|
+
this.resolvePath = resolvePath;
|
|
9262
|
+
}
|
|
9263
|
+
resolvePath;
|
|
9264
|
+
/** Bytes of the current file already read. */
|
|
9265
|
+
offset = 0;
|
|
9266
|
+
/** `dev:ino` of the file those bytes came from, null before the first
|
|
9267
|
+
* successful stat. A different one is a new file, not growth. */
|
|
9268
|
+
fileId = null;
|
|
9269
|
+
/** Holds back an incomplete trailing UTF-8 sequence between two reads. */
|
|
9270
|
+
decoder = new StringDecoder("utf8");
|
|
9271
|
+
/** What was read after the last newline: a record the CLI is part way
|
|
9272
|
+
* through writing, completed by a later read. */
|
|
9273
|
+
partial = "";
|
|
9274
|
+
/** Complete lines seen since byte 0 of the current file. */
|
|
9275
|
+
seen = 0;
|
|
9276
|
+
/** Complete lines handed on: the line-index cursor. */
|
|
9277
|
+
taken = 0;
|
|
9278
|
+
/** Complete lines read and not handed on yet, oldest first. */
|
|
9279
|
+
queue = [];
|
|
9280
|
+
stats = { polls: 0, reads: 0, bytes: 0 };
|
|
9281
|
+
/** Complete lines in the file now. */
|
|
9282
|
+
lineCount() {
|
|
9283
|
+
this.poll(true);
|
|
9284
|
+
return this.seen;
|
|
9285
|
+
}
|
|
9286
|
+
/** The complete lines not handed on yet, oldest first. They stay queued
|
|
9287
|
+
* until `take` says how many of them were used. */
|
|
9288
|
+
pending() {
|
|
9289
|
+
this.poll(true);
|
|
9290
|
+
return this.queue;
|
|
9291
|
+
}
|
|
9292
|
+
/** Whether the file holds a complete line this cursor has not handed on. */
|
|
9293
|
+
hasUnread() {
|
|
9294
|
+
return this.pending().length > 0;
|
|
9295
|
+
}
|
|
9296
|
+
/** Mark the first `count` pending lines as handed on. */
|
|
9297
|
+
take(count) {
|
|
9298
|
+
const used = Math.min(count, this.queue.length);
|
|
9299
|
+
if (used <= 0) return;
|
|
9300
|
+
this.queue.splice(0, used);
|
|
9301
|
+
this.taken += used;
|
|
9302
|
+
}
|
|
9303
|
+
/** Hand on nothing of what is in the file now: the records written before a
|
|
9304
|
+
* turn started belong to the turn before it. */
|
|
9305
|
+
skipToEnd() {
|
|
9306
|
+
this.poll(false);
|
|
9307
|
+
this.taken = Math.max(this.taken, this.seen);
|
|
9308
|
+
this.queue.length = 0;
|
|
9309
|
+
}
|
|
9310
|
+
/** Read whatever was appended since the last look. */
|
|
9311
|
+
poll(collect) {
|
|
9312
|
+
this.stats.polls++;
|
|
9313
|
+
const filePath = this.resolvePath();
|
|
9314
|
+
let stat;
|
|
9315
|
+
try {
|
|
9316
|
+
stat = fs8.statSync(filePath);
|
|
9317
|
+
} catch {
|
|
9318
|
+
return;
|
|
9319
|
+
}
|
|
9320
|
+
const fileId = `${stat.dev}:${stat.ino}`;
|
|
9321
|
+
if (this.fileId !== null && (fileId !== this.fileId || stat.size < this.offset)) {
|
|
9322
|
+
this.offset = 0;
|
|
9323
|
+
this.seen = 0;
|
|
9324
|
+
this.partial = "";
|
|
9325
|
+
this.decoder = new StringDecoder("utf8");
|
|
9326
|
+
this.queue.length = 0;
|
|
9327
|
+
}
|
|
9328
|
+
this.fileId = fileId;
|
|
9329
|
+
if (stat.size <= this.offset) return;
|
|
9330
|
+
let fd = null;
|
|
9331
|
+
try {
|
|
9332
|
+
fd = fs8.openSync(filePath, "r");
|
|
9333
|
+
const buffer = Buffer.allocUnsafe(
|
|
9334
|
+
Math.min(TRANSCRIPT_CHUNK_BYTES, stat.size - this.offset)
|
|
9335
|
+
);
|
|
9336
|
+
while (this.offset < stat.size) {
|
|
9337
|
+
const want = Math.min(buffer.length, stat.size - this.offset);
|
|
9338
|
+
const read = fs8.readSync(fd, buffer, 0, want, this.offset);
|
|
9339
|
+
this.stats.reads++;
|
|
9340
|
+
if (read <= 0) break;
|
|
9341
|
+
this.offset += read;
|
|
9342
|
+
this.stats.bytes += read;
|
|
9343
|
+
this.ingest(this.decoder.write(buffer.subarray(0, read)), collect);
|
|
9344
|
+
}
|
|
9345
|
+
} catch {
|
|
9346
|
+
} finally {
|
|
9347
|
+
if (fd !== null) {
|
|
9348
|
+
try {
|
|
9349
|
+
fs8.closeSync(fd);
|
|
9350
|
+
} catch {
|
|
9351
|
+
}
|
|
9352
|
+
}
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
9355
|
+
/** Split decoded text into complete lines, keeping the unterminated tail. */
|
|
9356
|
+
ingest(text, collect) {
|
|
9357
|
+
if (!text) return;
|
|
9358
|
+
this.partial += text;
|
|
9359
|
+
let start = 0;
|
|
9360
|
+
for (; ; ) {
|
|
9361
|
+
const newline = this.partial.indexOf("\n", start);
|
|
9362
|
+
if (newline === -1) break;
|
|
9363
|
+
if (collect && this.seen >= this.taken) {
|
|
9364
|
+
this.queue.push(this.partial.slice(start, newline));
|
|
9365
|
+
}
|
|
9366
|
+
this.seen++;
|
|
9367
|
+
start = newline + 1;
|
|
9368
|
+
}
|
|
9369
|
+
if (start > 0) this.partial = this.partial.slice(start);
|
|
9370
|
+
}
|
|
9371
|
+
};
|
|
9127
9372
|
var normalizedPrompt = (text) => text.replace(/\s+/g, " ").trim();
|
|
9128
9373
|
function isOwnPromptRecord(rec, normalized) {
|
|
9129
9374
|
const content = rec?.type === "user" ? rec.message?.content : void 0;
|
|
@@ -9131,12 +9376,12 @@ function isOwnPromptRecord(rec, normalized) {
|
|
|
9131
9376
|
}
|
|
9132
9377
|
function resolveConfigDir(configDir) {
|
|
9133
9378
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
9134
|
-
if (!value) return
|
|
9379
|
+
if (!value) return path10.join(os4.homedir(), ".claude");
|
|
9135
9380
|
if (value === "~") return os4.homedir();
|
|
9136
9381
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
9137
|
-
return
|
|
9382
|
+
return path10.join(os4.homedir(), value.slice(2));
|
|
9138
9383
|
}
|
|
9139
|
-
return
|
|
9384
|
+
return path10.resolve(value);
|
|
9140
9385
|
}
|
|
9141
9386
|
function fatalScreenMessage(kind, configDir, cwd) {
|
|
9142
9387
|
if (kind === "trust") {
|
|
@@ -9155,8 +9400,8 @@ var ClaudeSession = class {
|
|
|
9155
9400
|
/** The newest terminal output, ANSI included, for diagnostics. */
|
|
9156
9401
|
raw = "";
|
|
9157
9402
|
proc = null;
|
|
9158
|
-
|
|
9159
|
-
|
|
9403
|
+
/** The transcript, read forwards only; its `taken` is the old line cursor. */
|
|
9404
|
+
transcript;
|
|
9160
9405
|
lastDataAt = 0;
|
|
9161
9406
|
exited = false;
|
|
9162
9407
|
exitCode = null;
|
|
@@ -9200,7 +9445,7 @@ var ClaudeSession = class {
|
|
|
9200
9445
|
abandonTurn = false;
|
|
9201
9446
|
o;
|
|
9202
9447
|
constructor(opts = {}) {
|
|
9203
|
-
this.cwd =
|
|
9448
|
+
this.cwd = path10.resolve(opts.cwd ?? process.cwd());
|
|
9204
9449
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
9205
9450
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
9206
9451
|
this.signal = opts.signal;
|
|
@@ -9212,12 +9457,13 @@ var ClaudeSession = class {
|
|
|
9212
9457
|
this.onExit = opts.onExit;
|
|
9213
9458
|
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
9214
9459
|
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
9215
|
-
this.jsonlPath =
|
|
9460
|
+
this.jsonlPath = path10.join(
|
|
9216
9461
|
this.configDir,
|
|
9217
9462
|
"projects",
|
|
9218
9463
|
encodeCwd(this.cwd),
|
|
9219
9464
|
`${this.sessionId}.jsonl`
|
|
9220
9465
|
);
|
|
9466
|
+
this.transcript = new TranscriptCursor(() => this.jsonlPath);
|
|
9221
9467
|
this.o = {
|
|
9222
9468
|
cwd: this.cwd,
|
|
9223
9469
|
cliPath: opts.cliPath,
|
|
@@ -9322,7 +9568,12 @@ var ClaudeSession = class {
|
|
|
9322
9568
|
};
|
|
9323
9569
|
proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
|
|
9324
9570
|
await this.waitForBoot();
|
|
9325
|
-
this.
|
|
9571
|
+
this.transcript.skipToEnd();
|
|
9572
|
+
}
|
|
9573
|
+
/** How much this session has read off the transcript. Read-only, and the
|
|
9574
|
+
* one thing `scripts/bench-transcript-poll.ts` measures. */
|
|
9575
|
+
get transcriptStats() {
|
|
9576
|
+
return this.transcript.stats;
|
|
9326
9577
|
}
|
|
9327
9578
|
onData(chunk) {
|
|
9328
9579
|
this.lastDataAt = Date.now();
|
|
@@ -9469,22 +9720,10 @@ var ClaudeSession = class {
|
|
|
9469
9720
|
while (Date.now() < until) {
|
|
9470
9721
|
await delay(80);
|
|
9471
9722
|
if (this.aborted || this.exited) return;
|
|
9472
|
-
if (this.
|
|
9723
|
+
if (this.transcript.hasUnread()) return;
|
|
9473
9724
|
}
|
|
9474
9725
|
}
|
|
9475
9726
|
}
|
|
9476
|
-
readRawLines() {
|
|
9477
|
-
try {
|
|
9478
|
-
return fs7.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
9479
|
-
} catch {
|
|
9480
|
-
return [];
|
|
9481
|
-
}
|
|
9482
|
-
}
|
|
9483
|
-
/** Count of complete lines (split('\n') minus the trailing/partial element). */
|
|
9484
|
-
lineCount() {
|
|
9485
|
-
const lines = this.readRawLines();
|
|
9486
|
-
return lines.length > 0 ? lines.length - 1 : 0;
|
|
9487
|
-
}
|
|
9488
9727
|
rawTail(max = 600) {
|
|
9489
9728
|
const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
|
|
9490
9729
|
return clean.length > max ? clean.slice(-max) : clean;
|
|
@@ -9517,7 +9756,7 @@ var ClaudeSession = class {
|
|
|
9517
9756
|
this.turn = new Promise((resolve6) => settle = resolve6);
|
|
9518
9757
|
this.turnDenied = [];
|
|
9519
9758
|
this.abandonTurn = false;
|
|
9520
|
-
this.
|
|
9759
|
+
this.transcript.skipToEnd();
|
|
9521
9760
|
this.screen = "";
|
|
9522
9761
|
try {
|
|
9523
9762
|
if (this.o.bracketedPaste) {
|
|
@@ -9582,30 +9821,26 @@ var ClaudeSession = class {
|
|
|
9582
9821
|
/**
|
|
9583
9822
|
* Read every complete transcript record past the cursor into the running
|
|
9584
9823
|
* turn. Returns whether anything new was read. The one place a record is
|
|
9585
|
-
* interpreted, shared by the poll loop and `flushTranscript
|
|
9824
|
+
* interpreted, shared by the poll loop and `flushTranscript`, and the only
|
|
9825
|
+
* one that consumes what `TranscriptCursor` has queued.
|
|
9586
9826
|
*/
|
|
9587
9827
|
scanTranscript() {
|
|
9588
9828
|
const scan = this.scan;
|
|
9589
9829
|
if (!scan || scan.end) return false;
|
|
9590
|
-
const lines = this.
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
for (let i =
|
|
9830
|
+
const lines = this.transcript.pending().slice();
|
|
9831
|
+
if (lines.length === 0) return false;
|
|
9832
|
+
let used = 0;
|
|
9833
|
+
for (let i = 0; i < lines.length && !scan.end; i++) {
|
|
9594
9834
|
const s = lines[i];
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
continue;
|
|
9598
|
-
}
|
|
9835
|
+
used = i + 1;
|
|
9836
|
+
if (!s || !s.trim()) continue;
|
|
9599
9837
|
let rec = null;
|
|
9600
9838
|
try {
|
|
9601
9839
|
rec = JSON.parse(s);
|
|
9602
9840
|
} catch {
|
|
9603
9841
|
}
|
|
9604
9842
|
if (scan.awaitingOwnPrompt !== null) {
|
|
9605
|
-
if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt))
|
|
9606
|
-
this.cursor = i + 1;
|
|
9607
|
-
continue;
|
|
9608
|
-
}
|
|
9843
|
+
if (!isOwnPromptRecord(rec, scan.awaitingOwnPrompt)) continue;
|
|
9609
9844
|
scan.awaitingOwnPrompt = null;
|
|
9610
9845
|
}
|
|
9611
9846
|
if (rec) this.notePlanApproval(rec);
|
|
@@ -9625,9 +9860,8 @@ var ClaudeSession = class {
|
|
|
9625
9860
|
scan.end = scan.stopReason ? "stop" : "ended";
|
|
9626
9861
|
}
|
|
9627
9862
|
}
|
|
9628
|
-
this.cursor = i + 1;
|
|
9629
9863
|
}
|
|
9630
|
-
|
|
9864
|
+
this.transcript.take(used);
|
|
9631
9865
|
scan.lastBeat = Date.now();
|
|
9632
9866
|
scan.stopSeenAt = Date.now();
|
|
9633
9867
|
return true;
|
|
@@ -10415,14 +10649,74 @@ function spawnInteractiveProcess(opts) {
|
|
|
10415
10649
|
return ap;
|
|
10416
10650
|
}
|
|
10417
10651
|
|
|
10652
|
+
// src/account-transcript.ts
|
|
10653
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
10654
|
+
import { constants as fsConstants } from "fs";
|
|
10655
|
+
import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir2 } from "fs/promises";
|
|
10656
|
+
import path11 from "path";
|
|
10657
|
+
var transcriptPath = interactiveTranscriptPath;
|
|
10658
|
+
function configDirForAccount(account) {
|
|
10659
|
+
const normalized = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
10660
|
+
return resolveConfigDir(
|
|
10661
|
+
normalized === DEFAULT_ACCOUNT ? void 0 : accountConfigDirPath(normalized)
|
|
10662
|
+
);
|
|
10663
|
+
}
|
|
10664
|
+
async function carryTranscriptToAccount(input) {
|
|
10665
|
+
const { sessionId, cwd, targetConfigDir } = input;
|
|
10666
|
+
const targetOwn = transcriptPath({ configDir: targetConfigDir, cwd, sessionId });
|
|
10667
|
+
const targetOwnStat = await statTranscript(targetOwn);
|
|
10668
|
+
if (targetOwnStat.kind === "other") {
|
|
10669
|
+
return { kind: "refused", reason: "target-not-a-file", detail: targetOwn };
|
|
10670
|
+
}
|
|
10671
|
+
let live;
|
|
10672
|
+
for (const dir of dedupe2([targetConfigDir, ...input.accountConfigDirs])) {
|
|
10673
|
+
const candidate = transcriptPath({ configDir: dir, cwd, sessionId });
|
|
10674
|
+
const stat = candidate === targetOwn ? targetOwnStat : await statTranscript(candidate);
|
|
10675
|
+
if (stat.kind !== "file") continue;
|
|
10676
|
+
if (!live || stat.size > live.size) live = { file: candidate, size: stat.size };
|
|
10677
|
+
}
|
|
10678
|
+
if (!live) return { kind: "refused", reason: "no-transcript-anywhere" };
|
|
10679
|
+
if (live.file === targetOwn) return { kind: "already-there", sessionId };
|
|
10680
|
+
const renamed = targetOwnStat.kind === "file";
|
|
10681
|
+
const carriedId = renamed ? (input.newSessionId ?? randomUUID6)() : sessionId;
|
|
10682
|
+
const target = renamed ? transcriptPath({ configDir: targetConfigDir, cwd, sessionId: carriedId }) : targetOwn;
|
|
10683
|
+
try {
|
|
10684
|
+
await mkdir2(path11.dirname(target), { recursive: true, mode: 448 });
|
|
10685
|
+
await copyFile(live.file, target, fsConstants.COPYFILE_EXCL);
|
|
10686
|
+
await chmod2(target, 384);
|
|
10687
|
+
} catch (err) {
|
|
10688
|
+
return { kind: "refused", reason: "copy-failed", detail: String(err) };
|
|
10689
|
+
}
|
|
10690
|
+
return { kind: "carried", sessionId: carriedId, from: live.file, to: target, renamed };
|
|
10691
|
+
}
|
|
10692
|
+
async function statTranscript(file) {
|
|
10693
|
+
try {
|
|
10694
|
+
const stat = await lstat2(file);
|
|
10695
|
+
return stat.isFile() ? { kind: "file", size: stat.size } : { kind: "other", size: 0 };
|
|
10696
|
+
} catch {
|
|
10697
|
+
return { kind: "none", size: 0 };
|
|
10698
|
+
}
|
|
10699
|
+
}
|
|
10700
|
+
function dedupe2(dirs) {
|
|
10701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10702
|
+
const out = [];
|
|
10703
|
+
for (const dir of dirs) {
|
|
10704
|
+
const resolved = path11.resolve(dir);
|
|
10705
|
+
if (seen.has(resolved)) continue;
|
|
10706
|
+
seen.add(resolved);
|
|
10707
|
+
out.push(resolved);
|
|
10708
|
+
}
|
|
10709
|
+
return out;
|
|
10710
|
+
}
|
|
10711
|
+
|
|
10418
10712
|
// src/prompts.ts
|
|
10419
|
-
import { readFileSync as
|
|
10713
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
10420
10714
|
import { homedir as homedir8 } from "os";
|
|
10421
|
-
import { randomUUID as
|
|
10715
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
10422
10716
|
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
|
|
10423
|
-
function readPromptFileIfPresent(
|
|
10717
|
+
function readPromptFileIfPresent(path13) {
|
|
10424
10718
|
try {
|
|
10425
|
-
const content =
|
|
10719
|
+
const content = readFileSync6(path13, "utf8").trim();
|
|
10426
10720
|
return content || void 0;
|
|
10427
10721
|
} catch {
|
|
10428
10722
|
return void 0;
|
|
@@ -10527,9 +10821,9 @@ function extractSystemMessages(prompt) {
|
|
|
10527
10821
|
}
|
|
10528
10822
|
var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
|
|
10529
10823
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
10530
|
-
const readFile2 = opts.readFile ?? ((
|
|
10824
|
+
const readFile2 = opts.readFile ?? ((path13) => {
|
|
10531
10825
|
try {
|
|
10532
|
-
return
|
|
10826
|
+
return readFileSync6(path13, "utf8");
|
|
10533
10827
|
} catch {
|
|
10534
10828
|
return null;
|
|
10535
10829
|
}
|
|
@@ -10541,16 +10835,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
|
|
|
10541
10835
|
}
|
|
10542
10836
|
const seen = /* @__PURE__ */ new Set();
|
|
10543
10837
|
for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
|
|
10544
|
-
const
|
|
10545
|
-
if (seen.has(
|
|
10546
|
-
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(
|
|
10547
|
-
seen.add(
|
|
10548
|
-
const file = readFile2(
|
|
10838
|
+
const path13 = match[1].trim();
|
|
10839
|
+
if (seen.has(path13) || !isAbsolute3(path13)) continue;
|
|
10840
|
+
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path13))) continue;
|
|
10841
|
+
seen.add(path13);
|
|
10842
|
+
const file = readFile2(path13);
|
|
10549
10843
|
if (!file?.trim()) continue;
|
|
10550
10844
|
const start = match.index + match[0].length + 1;
|
|
10551
10845
|
const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
|
|
10552
10846
|
if (content === null) continue;
|
|
10553
|
-
out.push({ text: `Instructions from: ${
|
|
10847
|
+
out.push({ text: `Instructions from: ${path13}
|
|
10554
10848
|
${content}`, content });
|
|
10555
10849
|
}
|
|
10556
10850
|
return out;
|
|
@@ -10587,10 +10881,10 @@ ${options.compressionSummary.trim()}`
|
|
|
10587
10881
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
10588
10882
|
const content = parts.join("\n\n");
|
|
10589
10883
|
if (!content) return void 0;
|
|
10590
|
-
const
|
|
10884
|
+
const path13 = join11(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
|
|
10591
10885
|
try {
|
|
10592
|
-
writeFileSync6(
|
|
10593
|
-
return
|
|
10886
|
+
writeFileSync6(path13, content, { encoding: "utf8", mode: 384 });
|
|
10887
|
+
return path13;
|
|
10594
10888
|
} catch (err) {
|
|
10595
10889
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
10596
10890
|
return void 0;
|
|
@@ -13606,12 +13900,51 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13606
13900
|
failedOver: failover.failedOver
|
|
13607
13901
|
});
|
|
13608
13902
|
deleteActiveProcess(sk);
|
|
13903
|
+
}
|
|
13904
|
+
let conversationOnTarget = false;
|
|
13905
|
+
const multiAccount = (this.config.failoverAccounts?.length ?? 0) > 1;
|
|
13906
|
+
const sessionBeforeAccountCheck = getClaudeSessionId(sk);
|
|
13907
|
+
if (!compactionMode && multiAccount && this.config.crossAccountResume !== false && sessionBeforeAccountCheck) {
|
|
13908
|
+
const carry = await carryTranscriptToAccount({
|
|
13909
|
+
sessionId: sessionBeforeAccountCheck,
|
|
13910
|
+
cwd,
|
|
13911
|
+
targetConfigDir: resolveConfigDir(this.skillBridgeSpawn(failover).configDir),
|
|
13912
|
+
// Every account's own directory, plus this model's configured one:
|
|
13913
|
+
// `configDir` is an option, so an account's directory is not always
|
|
13914
|
+
// the `~/.claude-<name>` the name alone would build.
|
|
13915
|
+
accountConfigDirs: [
|
|
13916
|
+
...(this.config.failoverAccounts ?? []).map(configDirForAccount),
|
|
13917
|
+
resolveConfigDir(this.config.configDir)
|
|
13918
|
+
]
|
|
13919
|
+
});
|
|
13920
|
+
if (carry.kind === "carried" || carry.kind === "already-there") {
|
|
13921
|
+
if (carry.sessionId !== sessionBeforeAccountCheck) {
|
|
13922
|
+
setClaudeSessionId(sk, carry.sessionId);
|
|
13923
|
+
}
|
|
13924
|
+
conversationOnTarget = true;
|
|
13925
|
+
if (carry.kind === "carried") {
|
|
13926
|
+
log.notice("carried this conversation's claude transcript to the other account", {
|
|
13927
|
+
sessionKey: sk,
|
|
13928
|
+
renamed: carry.renamed,
|
|
13929
|
+
sessionId: carry.sessionId,
|
|
13930
|
+
failedOver: failover.failedOver
|
|
13931
|
+
});
|
|
13932
|
+
}
|
|
13933
|
+
} else if (!getActiveProcess(sk)) {
|
|
13934
|
+
log.notice("replaying this conversation as text on the other account", {
|
|
13935
|
+
sessionKey: sk,
|
|
13936
|
+
reason: carry.reason,
|
|
13937
|
+
failedOver: failover.failedOver
|
|
13938
|
+
});
|
|
13939
|
+
deleteClaudeSessionId(sk);
|
|
13940
|
+
}
|
|
13941
|
+
} else if (!compactionMode && sessionBeforeAccountCheck && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
|
|
13609
13942
|
deleteClaudeSessionId(sk);
|
|
13610
13943
|
}
|
|
13611
13944
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
13612
13945
|
const hasActiveProcess = !!getActiveProcess(sk);
|
|
13613
13946
|
let includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
|
|
13614
|
-
if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
|
|
13947
|
+
if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
|
|
13615
13948
|
includeHistoryContext = true;
|
|
13616
13949
|
}
|
|
13617
13950
|
let forkFromClaudeSessionId;
|
|
@@ -13657,12 +13990,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13657
13990
|
let replayReason = "no-session-for-key";
|
|
13658
13991
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
13659
13992
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
13660
|
-
const
|
|
13993
|
+
const transcriptPath2 = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
13661
13994
|
const resumePoint = findResumePoint({
|
|
13662
13995
|
sessionKey: sk,
|
|
13663
13996
|
prompt: options.prompt,
|
|
13664
13997
|
cliPath,
|
|
13665
|
-
transcriptPath,
|
|
13998
|
+
transcriptPath: transcriptPath2,
|
|
13666
13999
|
onRefused: (reason) => {
|
|
13667
14000
|
replayReason = reason;
|
|
13668
14001
|
}
|
|
@@ -13679,7 +14012,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13679
14012
|
sessionKey: sk,
|
|
13680
14013
|
prompt: options.prompt,
|
|
13681
14014
|
cliPath,
|
|
13682
|
-
transcriptPath,
|
|
14015
|
+
transcriptPath: transcriptPath2,
|
|
13683
14016
|
// `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
|
|
13684
14017
|
// sibling key, and that one refreshes LRU order and cancels idle
|
|
13685
14018
|
// timers on everything it touches (the same reason the fork lookup
|
|
@@ -13724,7 +14057,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13724
14057
|
log.info("sending plan approval decision to claude", { sk });
|
|
13725
14058
|
}
|
|
13726
14059
|
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
13727
|
-
const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(
|
|
14060
|
+
const effectivePrompt = failoverAnswer?.kind === "switch" ? buildFailoverContinuationPrompt(
|
|
14061
|
+
options.prompt,
|
|
14062
|
+
failoverAnswer.target,
|
|
14063
|
+
conversationOnTarget
|
|
14064
|
+
) : options.prompt;
|
|
13728
14065
|
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(effectivePrompt, includeHistoryContext, {
|
|
13729
14066
|
compactionMode,
|
|
13730
14067
|
compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
|
|
@@ -14406,7 +14743,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14406
14743
|
candidates: failoverAccounts,
|
|
14407
14744
|
resetsAt: state.accountLimitHit?.resetsAt,
|
|
14408
14745
|
window: state.accountLimitHit?.window,
|
|
14409
|
-
reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock)
|
|
14746
|
+
reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock),
|
|
14747
|
+
carryTranscript: self.config.crossAccountResume !== false
|
|
14410
14748
|
});
|
|
14411
14749
|
log.warn(
|
|
14412
14750
|
`Claude account "${sourceAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
|
|
@@ -14908,7 +15246,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14908
15246
|
import {
|
|
14909
15247
|
existsSync as existsSync5,
|
|
14910
15248
|
mkdirSync as mkdirSync5,
|
|
14911
|
-
readFileSync as
|
|
15249
|
+
readFileSync as readFileSync7,
|
|
14912
15250
|
realpathSync as realpathSync4,
|
|
14913
15251
|
rmSync as rmSync3,
|
|
14914
15252
|
writeFileSync as writeFileSync7
|
|
@@ -14935,7 +15273,7 @@ function userIntendsToUseUnscoped() {
|
|
|
14935
15273
|
const cfg = userOpencodeJsonPath();
|
|
14936
15274
|
if (!existsSync5(cfg)) return false;
|
|
14937
15275
|
try {
|
|
14938
|
-
const json = JSON.parse(
|
|
15276
|
+
const json = JSON.parse(readFileSync7(cfg, "utf8"));
|
|
14939
15277
|
const plugins = json.plugin;
|
|
14940
15278
|
if (!Array.isArray(plugins)) return false;
|
|
14941
15279
|
return plugins.some(
|
|
@@ -14956,7 +15294,7 @@ function ourLoadedDir() {
|
|
|
14956
15294
|
function ourVersion(dir) {
|
|
14957
15295
|
if (!dir) return null;
|
|
14958
15296
|
try {
|
|
14959
|
-
const pkg = JSON.parse(
|
|
15297
|
+
const pkg = JSON.parse(readFileSync7(join13(dir, "package.json"), "utf8"));
|
|
14960
15298
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
14961
15299
|
} catch {
|
|
14962
15300
|
return null;
|
|
@@ -14968,7 +15306,7 @@ function markerPath() {
|
|
|
14968
15306
|
}
|
|
14969
15307
|
function markerVersion() {
|
|
14970
15308
|
try {
|
|
14971
|
-
const marker = JSON.parse(
|
|
15309
|
+
const marker = JSON.parse(readFileSync7(markerPath(), "utf8"));
|
|
14972
15310
|
return typeof marker.version === "string" ? marker.version : null;
|
|
14973
15311
|
} catch {
|
|
14974
15312
|
return null;
|
|
@@ -15025,7 +15363,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
15025
15363
|
if (!existsSync5(pkgJsonPath)) return;
|
|
15026
15364
|
let pkg = {};
|
|
15027
15365
|
try {
|
|
15028
|
-
pkg = JSON.parse(
|
|
15366
|
+
pkg = JSON.parse(readFileSync7(pkgJsonPath, "utf8"));
|
|
15029
15367
|
} catch {
|
|
15030
15368
|
return;
|
|
15031
15369
|
}
|
|
@@ -15044,7 +15382,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
15044
15382
|
const cachePkgJson = join13(cacheRoot, "package.json");
|
|
15045
15383
|
if (!existsSync5(cachePkgJson)) return;
|
|
15046
15384
|
try {
|
|
15047
|
-
const cfg = JSON.parse(
|
|
15385
|
+
const cfg = JSON.parse(readFileSync7(cachePkgJson, "utf8"));
|
|
15048
15386
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
15049
15387
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
15050
15388
|
writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -15124,7 +15462,7 @@ function registerOpencodeLogSink(client) {
|
|
|
15124
15462
|
}
|
|
15125
15463
|
|
|
15126
15464
|
// src/v2-client.ts
|
|
15127
|
-
import
|
|
15465
|
+
import path12 from "path";
|
|
15128
15466
|
function formatAgentTypeList(agents) {
|
|
15129
15467
|
const lines = agents.flatMap((agent) => {
|
|
15130
15468
|
if (agent.hidden === true || agent.mode === "primary") return [];
|
|
@@ -15153,7 +15491,7 @@ function toV1Session(info) {
|
|
|
15153
15491
|
if (!info || typeof info !== "object") return void 0;
|
|
15154
15492
|
const session = info;
|
|
15155
15493
|
const root = session.location?.directory;
|
|
15156
|
-
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ?
|
|
15494
|
+
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path12.resolve(root, session.subpath) : root : void 0;
|
|
15157
15495
|
return { ...info, directory, parentID: session.parentID };
|
|
15158
15496
|
}
|
|
15159
15497
|
function toV1Messages(messages) {
|
|
@@ -15712,6 +16050,7 @@ function createClaudeCode(settings = {}) {
|
|
|
15712
16050
|
forkSessions: settings.forkSessions === true,
|
|
15713
16051
|
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
15714
16052
|
resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
|
|
16053
|
+
crossAccountResume: settings.crossAccountResume !== false,
|
|
15715
16054
|
transport: settings.transport,
|
|
15716
16055
|
interactive: settings.interactive,
|
|
15717
16056
|
interactiveBypass: settings.interactiveBypass,
|