@khalilgharbaoui/opencode-claude-code-plugin 0.15.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/dist/index.d.ts +22 -1
- package/dist/index.js +478 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -440,10 +440,35 @@ function cliSupportsThinking(v) {
|
|
|
440
440
|
if (!v) return false;
|
|
441
441
|
return gte(v, { major: 2, minor: 0, patch: 0 });
|
|
442
442
|
}
|
|
443
|
+
var flagSupport = /* @__PURE__ */ new Map();
|
|
444
|
+
function detectCliSupportsFlag(cliPath, flag) {
|
|
445
|
+
const key = `${cliPath}\0${flag}`;
|
|
446
|
+
const cached = flagSupport.get(key);
|
|
447
|
+
if (cached) return cached;
|
|
448
|
+
const promise = (async () => {
|
|
449
|
+
try {
|
|
450
|
+
const { stdout } = await execFileAsync(cliPath, ["--help"], {
|
|
451
|
+
timeout: 5e3,
|
|
452
|
+
maxBuffer: 4 * 1024 * 1024
|
|
453
|
+
});
|
|
454
|
+
return stdout.includes(flag);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
log.warn("failed to probe claude cli flag support", {
|
|
457
|
+
cliPath,
|
|
458
|
+
flag,
|
|
459
|
+
error: err instanceof Error ? err.message : String(err)
|
|
460
|
+
});
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
})();
|
|
464
|
+
flagSupport.set(key, promise);
|
|
465
|
+
return promise;
|
|
466
|
+
}
|
|
443
467
|
|
|
444
468
|
// src/session-manager.ts
|
|
445
469
|
import { spawn } from "child_process";
|
|
446
470
|
import { createInterface } from "readline";
|
|
471
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
447
472
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
448
473
|
import { unlink } from "fs/promises";
|
|
449
474
|
|
|
@@ -932,12 +957,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
932
957
|
});
|
|
933
958
|
let timer = null;
|
|
934
959
|
const result = await new Promise(
|
|
935
|
-
(
|
|
960
|
+
(resolve5, reject2) => {
|
|
936
961
|
const entry = {
|
|
937
962
|
id: callId,
|
|
938
963
|
toolName,
|
|
939
964
|
input,
|
|
940
|
-
resolve:
|
|
965
|
+
resolve: resolve5,
|
|
941
966
|
reject: reject2,
|
|
942
967
|
channel
|
|
943
968
|
};
|
|
@@ -1019,11 +1044,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1019
1044
|
}
|
|
1020
1045
|
}
|
|
1021
1046
|
});
|
|
1022
|
-
await new Promise((
|
|
1047
|
+
await new Promise((resolve5, reject2) => {
|
|
1023
1048
|
server2.once("error", reject2);
|
|
1024
1049
|
server2.listen(0, "127.0.0.1", () => {
|
|
1025
1050
|
server2.off("error", reject2);
|
|
1026
|
-
|
|
1051
|
+
resolve5();
|
|
1027
1052
|
});
|
|
1028
1053
|
});
|
|
1029
1054
|
const addr = server2.address();
|
|
@@ -1077,8 +1102,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1077
1102
|
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
1078
1103
|
}
|
|
1079
1104
|
pending.clear();
|
|
1080
|
-
await new Promise((
|
|
1081
|
-
server2.close(() =>
|
|
1105
|
+
await new Promise((resolve5) => {
|
|
1106
|
+
server2.close(() => resolve5());
|
|
1082
1107
|
});
|
|
1083
1108
|
if (configFilePath) {
|
|
1084
1109
|
try {
|
|
@@ -1136,10 +1161,10 @@ function resolveDisallowedTools(options) {
|
|
|
1136
1161
|
return out;
|
|
1137
1162
|
}
|
|
1138
1163
|
function readBody(req) {
|
|
1139
|
-
return new Promise((
|
|
1164
|
+
return new Promise((resolve5, reject) => {
|
|
1140
1165
|
const chunks = [];
|
|
1141
1166
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
1142
|
-
req.on("end", () =>
|
|
1167
|
+
req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
|
|
1143
1168
|
req.on("error", reject);
|
|
1144
1169
|
});
|
|
1145
1170
|
}
|
|
@@ -1621,7 +1646,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
1621
1646
|
}
|
|
1622
1647
|
});
|
|
1623
1648
|
pendingProcesses.add(proc);
|
|
1624
|
-
return new Promise((
|
|
1649
|
+
return new Promise((resolve5, reject) => {
|
|
1625
1650
|
const event = `side-question:${requestId}`;
|
|
1626
1651
|
let settled = false;
|
|
1627
1652
|
let sent = false;
|
|
@@ -1676,7 +1701,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
1676
1701
|
}
|
|
1677
1702
|
settled = true;
|
|
1678
1703
|
cleanup();
|
|
1679
|
-
|
|
1704
|
+
resolve5({ response: result.response, synthetic: result.synthetic });
|
|
1680
1705
|
};
|
|
1681
1706
|
const timer = setTimeout(() => {
|
|
1682
1707
|
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
@@ -1731,6 +1756,8 @@ function takeUnattendedLines(ap) {
|
|
|
1731
1756
|
}
|
|
1732
1757
|
var activeProcesses = /* @__PURE__ */ new Map();
|
|
1733
1758
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1759
|
+
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
1760
|
+
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
1734
1761
|
var MAX_ACTIVE_PROCESSES = 16;
|
|
1735
1762
|
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
1736
1763
|
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
@@ -1778,15 +1805,107 @@ function evictIfNeeded() {
|
|
|
1778
1805
|
deleteActiveProcess(oldestKey);
|
|
1779
1806
|
}
|
|
1780
1807
|
}
|
|
1808
|
+
var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
|
|
1809
|
+
function isTerminalResultLine(line) {
|
|
1810
|
+
if (!line.includes('"result"')) return false;
|
|
1811
|
+
try {
|
|
1812
|
+
return JSON.parse(line).type === "result";
|
|
1813
|
+
} catch {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function settleTurn(ap) {
|
|
1818
|
+
ap.turnInFlight = false;
|
|
1819
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
1820
|
+
ap.turnIdleWaiters = [];
|
|
1821
|
+
for (const wake of waiters) wake();
|
|
1822
|
+
}
|
|
1823
|
+
function noteTurnStarted(ap) {
|
|
1824
|
+
if (ap.asideTransport?.interactive) return;
|
|
1825
|
+
ap.turnInFlight = true;
|
|
1826
|
+
}
|
|
1827
|
+
function noteTurnLine(ap, line) {
|
|
1828
|
+
if (!ap.turnInFlight) return;
|
|
1829
|
+
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
1830
|
+
}
|
|
1831
|
+
function isTurnInFlight(ap) {
|
|
1832
|
+
return ap.turnInFlight === true;
|
|
1833
|
+
}
|
|
1834
|
+
function awaitTurnIdle(ap, timeoutMs) {
|
|
1835
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
1836
|
+
return new Promise((resolve5) => {
|
|
1837
|
+
const wake = () => {
|
|
1838
|
+
clearTimeout(timer);
|
|
1839
|
+
resolve5(true);
|
|
1840
|
+
};
|
|
1841
|
+
const timer = setTimeout(() => {
|
|
1842
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
1843
|
+
const at = waiters.indexOf(wake);
|
|
1844
|
+
if (at >= 0) waiters.splice(at, 1);
|
|
1845
|
+
resolve5(false);
|
|
1846
|
+
}, timeoutMs);
|
|
1847
|
+
(ap.turnIdleWaiters ??= []).push(wake);
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
1851
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
1852
|
+
const stdin = ap.proc.stdin;
|
|
1853
|
+
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
1854
|
+
log.notice("cannot interrupt this transport; waiting for the turn to end");
|
|
1855
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
1856
|
+
}
|
|
1857
|
+
try {
|
|
1858
|
+
stdin.write(
|
|
1859
|
+
JSON.stringify({
|
|
1860
|
+
type: "control_request",
|
|
1861
|
+
request_id: randomUUID3(),
|
|
1862
|
+
request: { subtype: "interrupt" }
|
|
1863
|
+
}) + "\n"
|
|
1864
|
+
);
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
log.warn("failed to write interrupt control request", {
|
|
1867
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1868
|
+
});
|
|
1869
|
+
return Promise.resolve(false);
|
|
1870
|
+
}
|
|
1871
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
1872
|
+
}
|
|
1873
|
+
function cancelIdleProcessEviction(key) {
|
|
1874
|
+
const timer = idleEvictionTimers.get(key);
|
|
1875
|
+
if (!timer) return;
|
|
1876
|
+
clearTimeout(timer);
|
|
1877
|
+
idleEvictionTimers.delete(key);
|
|
1878
|
+
}
|
|
1781
1879
|
function getActiveProcess(key) {
|
|
1782
1880
|
const ap = activeProcesses.get(key);
|
|
1783
|
-
if (ap)
|
|
1881
|
+
if (ap) {
|
|
1882
|
+
cancelIdleProcessEviction(key);
|
|
1883
|
+
touch(key);
|
|
1884
|
+
}
|
|
1784
1885
|
return ap;
|
|
1785
1886
|
}
|
|
1786
1887
|
function setActiveProcess(key, ap) {
|
|
1888
|
+
cancelIdleProcessEviction(key);
|
|
1787
1889
|
activeProcesses.set(key, ap);
|
|
1788
1890
|
}
|
|
1891
|
+
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
1892
|
+
cancelIdleProcessEviction(key);
|
|
1893
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const scheduledProcess = activeProcesses.get(key);
|
|
1897
|
+
if (!scheduledProcess) return;
|
|
1898
|
+
const timer = setTimeout(() => {
|
|
1899
|
+
idleEvictionTimers.delete(key);
|
|
1900
|
+
if (activeProcesses.get(key) !== scheduledProcess) return;
|
|
1901
|
+
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
1902
|
+
deleteActiveProcess(key);
|
|
1903
|
+
}, timeoutMs);
|
|
1904
|
+
timer.unref();
|
|
1905
|
+
idleEvictionTimers.set(key, timer);
|
|
1906
|
+
}
|
|
1789
1907
|
function detachActiveProcess(key) {
|
|
1908
|
+
cancelIdleProcessEviction(key);
|
|
1790
1909
|
const ap = activeProcesses.get(key);
|
|
1791
1910
|
if (!ap) return void 0;
|
|
1792
1911
|
activeProcesses.delete(key);
|
|
@@ -1802,14 +1921,14 @@ function hasProcessExited(proc) {
|
|
|
1802
1921
|
}
|
|
1803
1922
|
function waitForProcessExit(proc, timeoutMs) {
|
|
1804
1923
|
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
1805
|
-
return new Promise((
|
|
1924
|
+
return new Promise((resolve5) => {
|
|
1806
1925
|
const onExit = () => {
|
|
1807
1926
|
clearTimeout(timer);
|
|
1808
|
-
|
|
1927
|
+
resolve5(true);
|
|
1809
1928
|
};
|
|
1810
1929
|
const timer = setTimeout(() => {
|
|
1811
1930
|
proc.off("exit", onExit);
|
|
1812
|
-
|
|
1931
|
+
resolve5(hasProcessExited(proc));
|
|
1813
1932
|
}, timeoutMs);
|
|
1814
1933
|
proc.once("exit", onExit);
|
|
1815
1934
|
});
|
|
@@ -1905,6 +2024,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1905
2024
|
const rl = createInterface({ input: proc.stdout });
|
|
1906
2025
|
rl.on("line", (line) => {
|
|
1907
2026
|
if (dispatchSideQuestionResponse(ap, line)) return;
|
|
2027
|
+
noteTurnLine(ap, line);
|
|
1908
2028
|
if (lineEmitter.listenerCount("line") === 0) {
|
|
1909
2029
|
bufferUnattendedLine(ap, line);
|
|
1910
2030
|
return;
|
|
@@ -1912,8 +2032,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1912
2032
|
lineEmitter.emit("line", line);
|
|
1913
2033
|
});
|
|
1914
2034
|
rl.on("close", () => {
|
|
2035
|
+
settleTurn(ap);
|
|
1915
2036
|
lineEmitter.emit("close");
|
|
1916
2037
|
});
|
|
2038
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
1917
2039
|
activeProcesses.set(sessionKey2, ap);
|
|
1918
2040
|
proc.on("error", (err) => {
|
|
1919
2041
|
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
@@ -1926,7 +2048,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1926
2048
|
});
|
|
1927
2049
|
}
|
|
1928
2050
|
const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
|
|
1929
|
-
if (ownsSessionKey)
|
|
2051
|
+
if (ownsSessionKey) {
|
|
2052
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
2053
|
+
activeProcesses.delete(sessionKey2);
|
|
2054
|
+
}
|
|
1930
2055
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
1931
2056
|
log.info("process exited with error, clearing session", {
|
|
1932
2057
|
code,
|
|
@@ -1997,6 +2122,7 @@ function buildCliArgs(opts) {
|
|
|
1997
2122
|
strictMcpConfig,
|
|
1998
2123
|
disallowedTools,
|
|
1999
2124
|
appendSystemPromptFile,
|
|
2125
|
+
pluginDirs,
|
|
2000
2126
|
thinking,
|
|
2001
2127
|
thinkingDisplay,
|
|
2002
2128
|
fastMode,
|
|
@@ -2045,6 +2171,9 @@ function buildCliArgs(opts) {
|
|
|
2045
2171
|
if (appendSystemPromptFile) {
|
|
2046
2172
|
args.push("--append-system-prompt-file", appendSystemPromptFile);
|
|
2047
2173
|
}
|
|
2174
|
+
for (const dir of pluginDirs ?? []) {
|
|
2175
|
+
args.push("--plugin-dir", dir);
|
|
2176
|
+
}
|
|
2048
2177
|
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
2049
2178
|
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
2050
2179
|
}
|
|
@@ -2169,7 +2298,7 @@ async function waitForSessionIdle(client, sessionID, options = {}) {
|
|
|
2169
2298
|
if (options.stop?.()) return true;
|
|
2170
2299
|
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
2171
2300
|
if (Date.now() - started >= timeoutMs) return false;
|
|
2172
|
-
await new Promise((
|
|
2301
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2173
2302
|
}
|
|
2174
2303
|
}
|
|
2175
2304
|
async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
@@ -2180,7 +2309,7 @@ async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
|
2180
2309
|
if (emitAsideInline(sessionID, text)) return true;
|
|
2181
2310
|
if (Date.now() - started >= timeoutMs) return false;
|
|
2182
2311
|
if (await sessionStatus(client, sessionID) !== "busy") return false;
|
|
2183
|
-
await new Promise((
|
|
2312
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2184
2313
|
}
|
|
2185
2314
|
}
|
|
2186
2315
|
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
@@ -2201,7 +2330,7 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
|
2201
2330
|
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
2202
2331
|
return void 0;
|
|
2203
2332
|
}
|
|
2204
|
-
await new Promise((
|
|
2333
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2205
2334
|
}
|
|
2206
2335
|
}
|
|
2207
2336
|
async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
@@ -2213,7 +2342,7 @@ async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
|
2213
2342
|
if (status === "busy") return true;
|
|
2214
2343
|
if (status === "unknown") return isProcessBusy(active);
|
|
2215
2344
|
if (Date.now() - started >= settleMs) return false;
|
|
2216
|
-
await new Promise((
|
|
2345
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2217
2346
|
}
|
|
2218
2347
|
}
|
|
2219
2348
|
function errorText(error) {
|
|
@@ -2260,10 +2389,10 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
2260
2389
|
let inlineDone = false;
|
|
2261
2390
|
let markInlineDelivered = () => {
|
|
2262
2391
|
};
|
|
2263
|
-
const inlineDelivered = new Promise((
|
|
2392
|
+
const inlineDelivered = new Promise((resolve5) => {
|
|
2264
2393
|
markInlineDelivered = () => {
|
|
2265
2394
|
inlineDone = true;
|
|
2266
|
-
|
|
2395
|
+
resolve5("inline");
|
|
2267
2396
|
};
|
|
2268
2397
|
});
|
|
2269
2398
|
if (isSideQuestionPending(active)) {
|
|
@@ -2511,7 +2640,7 @@ function compactConversationHistory(prompt, opts = {}) {
|
|
|
2511
2640
|
return buildCompactionHistory(prompt);
|
|
2512
2641
|
}
|
|
2513
2642
|
const conversationMessages = prompt.filter(
|
|
2514
|
-
(m) => m.role === "user" || m.role === "assistant"
|
|
2643
|
+
(m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
|
|
2515
2644
|
);
|
|
2516
2645
|
if (conversationMessages.length <= 1) {
|
|
2517
2646
|
return null;
|
|
@@ -2519,28 +2648,8 @@ function compactConversationHistory(prompt, opts = {}) {
|
|
|
2519
2648
|
const historyParts = [];
|
|
2520
2649
|
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
2521
2650
|
const msg = conversationMessages[i];
|
|
2522
|
-
const role = msg.role === "user" ? "User" : "Assistant";
|
|
2523
|
-
|
|
2524
|
-
if (typeof msg.content === "string") {
|
|
2525
|
-
text = msg.content;
|
|
2526
|
-
} else if (Array.isArray(msg.content)) {
|
|
2527
|
-
const textParts = msg.content.filter((p) => p.type === "text" && p.text).map((p) => p.text);
|
|
2528
|
-
text = textParts.join("\n");
|
|
2529
|
-
const toolCalls = msg.content.filter(
|
|
2530
|
-
(p) => p.type === "tool-call"
|
|
2531
|
-
);
|
|
2532
|
-
const toolResults = msg.content.filter(
|
|
2533
|
-
(p) => p.type === "tool-result"
|
|
2534
|
-
);
|
|
2535
|
-
if (toolCalls.length > 0) {
|
|
2536
|
-
text += `
|
|
2537
|
-
[Called ${toolCalls.length} tool(s): ${toolCalls.map((t) => t.toolName).join(", ")}]`;
|
|
2538
|
-
}
|
|
2539
|
-
if (toolResults.length > 0) {
|
|
2540
|
-
text += `
|
|
2541
|
-
[Received ${toolResults.length} tool result(s)]`;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2651
|
+
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
2652
|
+
const { text } = renderMessageContentForCompaction(msg);
|
|
2544
2653
|
if (text.trim()) {
|
|
2545
2654
|
const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
|
|
2546
2655
|
historyParts.push(`${role}: ${truncated}`);
|
|
@@ -2583,7 +2692,27 @@ function buildCompactionHistory(prompt) {
|
|
|
2583
2692
|
}
|
|
2584
2693
|
function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
|
|
2585
2694
|
const compactionMode = opts.compactionMode === true;
|
|
2695
|
+
const cliToolCallIds = opts.cliToolCallIds;
|
|
2586
2696
|
const content = [];
|
|
2697
|
+
const pushToolResult = (part) => {
|
|
2698
|
+
const id = part.toolCallId;
|
|
2699
|
+
const text = getToolResultText(part);
|
|
2700
|
+
if (!cliToolCallIds || cliToolCallIds.has(id)) {
|
|
2701
|
+
content.push({ type: "tool_result", tool_use_id: id, content: text });
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
log.info("rendering opencode-side tool result as text", {
|
|
2705
|
+
toolCallId: id,
|
|
2706
|
+
toolName: part.toolName,
|
|
2707
|
+
chars: text.length
|
|
2708
|
+
});
|
|
2709
|
+
content.push({
|
|
2710
|
+
type: "text",
|
|
2711
|
+
text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
|
|
2712
|
+
${text}
|
|
2713
|
+
</opencode_tool_result>`
|
|
2714
|
+
});
|
|
2715
|
+
};
|
|
2587
2716
|
if (compactionMode) {
|
|
2588
2717
|
const transcript = compactConversationHistory(prompt, {
|
|
2589
2718
|
mode: "compaction"
|
|
@@ -2653,12 +2782,7 @@ Now continuing with the current message:
|
|
|
2653
2782
|
});
|
|
2654
2783
|
}
|
|
2655
2784
|
} else if (part.type === "tool-result") {
|
|
2656
|
-
|
|
2657
|
-
content.push({
|
|
2658
|
-
type: "tool_result",
|
|
2659
|
-
tool_use_id: p.toolCallId,
|
|
2660
|
-
content: getToolResultText(p)
|
|
2661
|
-
});
|
|
2785
|
+
pushToolResult(part);
|
|
2662
2786
|
}
|
|
2663
2787
|
}
|
|
2664
2788
|
}
|
|
@@ -2666,12 +2790,7 @@ Now continuing with the current message:
|
|
|
2666
2790
|
if (Array.isArray(msg.content)) {
|
|
2667
2791
|
for (const part of msg.content) {
|
|
2668
2792
|
if (part?.type === "tool-result") {
|
|
2669
|
-
|
|
2670
|
-
content.push({
|
|
2671
|
-
type: "tool_result",
|
|
2672
|
-
tool_use_id: p.toolCallId,
|
|
2673
|
-
content: getToolResultText(p)
|
|
2674
|
-
});
|
|
2793
|
+
pushToolResult(part);
|
|
2675
2794
|
}
|
|
2676
2795
|
}
|
|
2677
2796
|
}
|
|
@@ -3103,34 +3222,166 @@ function agentDirectories(home, projectDirectory) {
|
|
|
3103
3222
|
return directories;
|
|
3104
3223
|
}
|
|
3105
3224
|
|
|
3106
|
-
// src/
|
|
3225
|
+
// src/skill-bridge.ts
|
|
3226
|
+
import * as crypto2 from "crypto";
|
|
3107
3227
|
import * as fs3 from "fs";
|
|
3108
|
-
import * as path4 from "path";
|
|
3109
3228
|
import * as os2 from "os";
|
|
3110
|
-
import * as
|
|
3229
|
+
import * as path4 from "path";
|
|
3230
|
+
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
3231
|
+
function dirExists(p) {
|
|
3232
|
+
try {
|
|
3233
|
+
return fs3.statSync(p).isDirectory();
|
|
3234
|
+
} catch {
|
|
3235
|
+
return false;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
function fileExists(p) {
|
|
3239
|
+
try {
|
|
3240
|
+
return fs3.statSync(p).isFile();
|
|
3241
|
+
} catch {
|
|
3242
|
+
return false;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function skillRoots(cwd) {
|
|
3246
|
+
const roots = [];
|
|
3247
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3248
|
+
const push = (p) => {
|
|
3249
|
+
const abs = path4.resolve(p);
|
|
3250
|
+
if (seen.has(abs)) return;
|
|
3251
|
+
seen.add(abs);
|
|
3252
|
+
if (dirExists(abs)) roots.push(abs);
|
|
3253
|
+
};
|
|
3254
|
+
let current = path4.resolve(cwd);
|
|
3255
|
+
while (true) {
|
|
3256
|
+
push(path4.join(current, ".opencode", "skills"));
|
|
3257
|
+
const parent = path4.dirname(current);
|
|
3258
|
+
if (parent === current) break;
|
|
3259
|
+
current = parent;
|
|
3260
|
+
}
|
|
3261
|
+
const home = os2.homedir();
|
|
3262
|
+
if (home) push(path4.join(home, ".opencode", "skills"));
|
|
3263
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3264
|
+
if (envDir) push(path4.join(envDir, "skills"));
|
|
3265
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
|
|
3266
|
+
if (xdg) push(path4.join(xdg, "opencode", "skills"));
|
|
3267
|
+
return roots;
|
|
3268
|
+
}
|
|
3269
|
+
function discoverOpencodeSkills(cwd) {
|
|
3270
|
+
const found = [];
|
|
3271
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3272
|
+
for (const root of skillRoots(cwd)) {
|
|
3273
|
+
let entries;
|
|
3274
|
+
try {
|
|
3275
|
+
entries = fs3.readdirSync(root, { withFileTypes: true });
|
|
3276
|
+
} catch {
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3279
|
+
for (const entry of entries) {
|
|
3280
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3281
|
+
const name = entry.name;
|
|
3282
|
+
if (name.startsWith(".")) continue;
|
|
3283
|
+
if (claimed.has(name)) continue;
|
|
3284
|
+
const dir = path4.join(root, name);
|
|
3285
|
+
if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
|
|
3286
|
+
claimed.add(name);
|
|
3287
|
+
found.push({ name, dir });
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return found.sort((a, b) => a.name.localeCompare(b.name));
|
|
3291
|
+
}
|
|
3292
|
+
function linkSkill(source, target) {
|
|
3293
|
+
try {
|
|
3294
|
+
fs3.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
3295
|
+
return;
|
|
3296
|
+
} catch {
|
|
3297
|
+
fs3.cpSync(source, target, { recursive: true, dereference: true });
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
function buildSkillPluginDir(skills) {
|
|
3301
|
+
if (skills.length === 0) return null;
|
|
3302
|
+
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
3303
|
+
const hash = crypto2.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
3304
|
+
const root = path4.join(pluginTmpDir(), `skills-${hash}`);
|
|
3305
|
+
const manifest = path4.join(root, ".claude-plugin", "plugin.json");
|
|
3306
|
+
if (fileExists(manifest)) return root;
|
|
3307
|
+
try {
|
|
3308
|
+
fs3.rmSync(root, { recursive: true, force: true });
|
|
3309
|
+
fs3.mkdirSync(path4.join(root, ".claude-plugin"), { recursive: true });
|
|
3310
|
+
fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
|
|
3311
|
+
fs3.writeFileSync(
|
|
3312
|
+
manifest,
|
|
3313
|
+
JSON.stringify(
|
|
3314
|
+
{
|
|
3315
|
+
name: SKILL_PLUGIN_NAME,
|
|
3316
|
+
description: "Skills discovered from this opencode installation, bridged into Claude Code."
|
|
3317
|
+
},
|
|
3318
|
+
null,
|
|
3319
|
+
2
|
|
3320
|
+
),
|
|
3321
|
+
{ encoding: "utf8", mode: 384 }
|
|
3322
|
+
);
|
|
3323
|
+
for (const skill of skills) {
|
|
3324
|
+
linkSkill(skill.dir, path4.join(root, "skills", skill.name));
|
|
3325
|
+
}
|
|
3326
|
+
} catch (err) {
|
|
3327
|
+
log.warn("failed to stage opencode skill plugin dir", {
|
|
3328
|
+
root,
|
|
3329
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3330
|
+
});
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
return root;
|
|
3334
|
+
}
|
|
3335
|
+
async function resolveSkillPluginDirs(opts) {
|
|
3336
|
+
if (!opts.enabled) return [];
|
|
3337
|
+
const skills = discoverOpencodeSkills(opts.cwd);
|
|
3338
|
+
if (skills.length === 0) return [];
|
|
3339
|
+
const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
|
|
3340
|
+
if (!supported) {
|
|
3341
|
+
log.notice(
|
|
3342
|
+
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
3343
|
+
{ skills: skills.length }
|
|
3344
|
+
);
|
|
3345
|
+
return [];
|
|
3346
|
+
}
|
|
3347
|
+
const dir = buildSkillPluginDir(skills);
|
|
3348
|
+
if (!dir) return [];
|
|
3349
|
+
log.info("bridged opencode skills into claude", {
|
|
3350
|
+
count: skills.length,
|
|
3351
|
+
names: skills.map((s) => s.name),
|
|
3352
|
+
pluginDir: dir
|
|
3353
|
+
});
|
|
3354
|
+
return [dir];
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
// src/mcp-bridge.ts
|
|
3358
|
+
import * as fs4 from "fs";
|
|
3359
|
+
import * as path5 from "path";
|
|
3360
|
+
import * as os3 from "os";
|
|
3361
|
+
import * as crypto3 from "crypto";
|
|
3111
3362
|
import {
|
|
3112
3363
|
parse as parseJsonc,
|
|
3113
3364
|
printParseErrorCode
|
|
3114
3365
|
} from "jsonc-parser";
|
|
3115
3366
|
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
3116
3367
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3117
|
-
function
|
|
3368
|
+
function fileExists2(p) {
|
|
3118
3369
|
try {
|
|
3119
|
-
return
|
|
3370
|
+
return fs4.statSync(p).isFile();
|
|
3120
3371
|
} catch {
|
|
3121
3372
|
return false;
|
|
3122
3373
|
}
|
|
3123
3374
|
}
|
|
3124
|
-
function
|
|
3375
|
+
function dirExists2(p) {
|
|
3125
3376
|
try {
|
|
3126
|
-
return
|
|
3377
|
+
return fs4.statSync(p).isDirectory();
|
|
3127
3378
|
} catch {
|
|
3128
3379
|
return false;
|
|
3129
3380
|
}
|
|
3130
3381
|
}
|
|
3131
3382
|
function readAndParse(file) {
|
|
3132
3383
|
try {
|
|
3133
|
-
const raw =
|
|
3384
|
+
const raw = fs4.readFileSync(file, "utf8");
|
|
3134
3385
|
const errors = [];
|
|
3135
3386
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3136
3387
|
if (errors.length > 0) {
|
|
@@ -3166,14 +3417,14 @@ function deepMerge(target, source) {
|
|
|
3166
3417
|
}
|
|
3167
3418
|
function walkUp(opts) {
|
|
3168
3419
|
const out = [];
|
|
3169
|
-
let current =
|
|
3420
|
+
let current = path5.resolve(opts.start);
|
|
3170
3421
|
while (true) {
|
|
3171
3422
|
for (const target of opts.targets) {
|
|
3172
|
-
const candidate =
|
|
3423
|
+
const candidate = path5.join(current, target);
|
|
3173
3424
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3174
3425
|
}
|
|
3175
|
-
if (opts.stop && current ===
|
|
3176
|
-
const parent =
|
|
3426
|
+
if (opts.stop && current === path5.resolve(opts.stop)) break;
|
|
3427
|
+
const parent = path5.dirname(current);
|
|
3177
3428
|
if (parent === current) break;
|
|
3178
3429
|
current = parent;
|
|
3179
3430
|
}
|
|
@@ -3181,29 +3432,29 @@ function walkUp(opts) {
|
|
|
3181
3432
|
}
|
|
3182
3433
|
function detectWorktree(cwd) {
|
|
3183
3434
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3184
|
-
if (override) return
|
|
3185
|
-
let current =
|
|
3435
|
+
if (override) return path5.resolve(override);
|
|
3436
|
+
let current = path5.resolve(cwd);
|
|
3186
3437
|
while (true) {
|
|
3187
|
-
const gitPath =
|
|
3438
|
+
const gitPath = path5.join(current, ".git");
|
|
3188
3439
|
try {
|
|
3189
|
-
if (
|
|
3440
|
+
if (fs4.existsSync(gitPath)) return current;
|
|
3190
3441
|
} catch {
|
|
3191
3442
|
}
|
|
3192
|
-
const parent =
|
|
3443
|
+
const parent = path5.dirname(current);
|
|
3193
3444
|
if (parent === current) return void 0;
|
|
3194
3445
|
current = parent;
|
|
3195
3446
|
}
|
|
3196
3447
|
}
|
|
3197
3448
|
function globalConfigDir() {
|
|
3198
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3199
|
-
return
|
|
3449
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
|
|
3450
|
+
return path5.join(xdg, "opencode");
|
|
3200
3451
|
}
|
|
3201
3452
|
function loadGlobalConfig() {
|
|
3202
3453
|
const dir = globalConfigDir();
|
|
3203
3454
|
let merged = {};
|
|
3204
3455
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3205
|
-
const file =
|
|
3206
|
-
if (!
|
|
3456
|
+
const file = path5.join(dir, name);
|
|
3457
|
+
if (!fileExists2(file)) continue;
|
|
3207
3458
|
const parsed = readAndParse(file);
|
|
3208
3459
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
3209
3460
|
}
|
|
@@ -3212,8 +3463,8 @@ function loadGlobalConfig() {
|
|
|
3212
3463
|
function loadProjectFilesInDir(dir) {
|
|
3213
3464
|
let merged = {};
|
|
3214
3465
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3215
|
-
const file =
|
|
3216
|
-
if (!
|
|
3466
|
+
const file = path5.join(dir, name);
|
|
3467
|
+
if (!fileExists2(file)) continue;
|
|
3217
3468
|
const parsed = readAndParse(file);
|
|
3218
3469
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
3219
3470
|
}
|
|
@@ -3223,8 +3474,8 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3223
3474
|
const dirs = [];
|
|
3224
3475
|
const seen = /* @__PURE__ */ new Set();
|
|
3225
3476
|
const push = (p) => {
|
|
3226
|
-
const abs =
|
|
3227
|
-
if (!seen.has(abs) &&
|
|
3477
|
+
const abs = path5.resolve(p);
|
|
3478
|
+
if (!seen.has(abs) && dirExists2(abs)) {
|
|
3228
3479
|
seen.add(abs);
|
|
3229
3480
|
dirs.push(abs);
|
|
3230
3481
|
}
|
|
@@ -3233,17 +3484,17 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3233
3484
|
start: cwd,
|
|
3234
3485
|
stop: worktree,
|
|
3235
3486
|
targets: [".opencode"],
|
|
3236
|
-
predicate:
|
|
3487
|
+
predicate: dirExists2
|
|
3237
3488
|
})) {
|
|
3238
3489
|
push(dir);
|
|
3239
3490
|
}
|
|
3240
|
-
const home =
|
|
3491
|
+
const home = os3.homedir();
|
|
3241
3492
|
if (home) {
|
|
3242
|
-
const homeDot =
|
|
3243
|
-
if (
|
|
3493
|
+
const homeDot = path5.join(home, ".opencode");
|
|
3494
|
+
if (dirExists2(homeDot)) push(homeDot);
|
|
3244
3495
|
}
|
|
3245
3496
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3246
|
-
if (envDir &&
|
|
3497
|
+
if (envDir && dirExists2(envDir)) push(envDir);
|
|
3247
3498
|
return dirs;
|
|
3248
3499
|
}
|
|
3249
3500
|
function substituteEnvPlaceholders(source) {
|
|
@@ -3351,7 +3602,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3351
3602
|
let merged = {};
|
|
3352
3603
|
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
3353
3604
|
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3354
|
-
if (explicitConfig &&
|
|
3605
|
+
if (explicitConfig && fileExists2(explicitConfig)) {
|
|
3355
3606
|
const parsed = readAndParse(explicitConfig);
|
|
3356
3607
|
if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
|
|
3357
3608
|
}
|
|
@@ -3359,12 +3610,12 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3359
3610
|
start: cwd,
|
|
3360
3611
|
stop: worktree,
|
|
3361
3612
|
targets: PROJECT_FILE_NAMES,
|
|
3362
|
-
predicate:
|
|
3613
|
+
predicate: fileExists2
|
|
3363
3614
|
});
|
|
3364
3615
|
const projectDirs = [];
|
|
3365
3616
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3366
3617
|
for (const f of projectFiles) {
|
|
3367
|
-
const d =
|
|
3618
|
+
const d = path5.dirname(f);
|
|
3368
3619
|
if (!seenProjectDirs.has(d)) {
|
|
3369
3620
|
seenProjectDirs.add(d);
|
|
3370
3621
|
projectDirs.push(d);
|
|
@@ -3393,7 +3644,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3393
3644
|
enabledServerNames.push(name);
|
|
3394
3645
|
}
|
|
3395
3646
|
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3396
|
-
const hash =
|
|
3647
|
+
const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3397
3648
|
return { servers: merged, enabledServerNames, hash };
|
|
3398
3649
|
}
|
|
3399
3650
|
function finishBridge(input) {
|
|
@@ -3409,13 +3660,13 @@ function finishBridge(input) {
|
|
|
3409
3660
|
};
|
|
3410
3661
|
}
|
|
3411
3662
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3412
|
-
const outPath =
|
|
3663
|
+
const outPath = path5.join(
|
|
3413
3664
|
pluginTmpDir(),
|
|
3414
3665
|
`mcp-${hash}.json`
|
|
3415
3666
|
);
|
|
3416
3667
|
try {
|
|
3417
|
-
if (!
|
|
3418
|
-
|
|
3668
|
+
if (!fileExists2(outPath)) {
|
|
3669
|
+
fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3419
3670
|
}
|
|
3420
3671
|
} catch (e) {
|
|
3421
3672
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -3457,17 +3708,39 @@ function getOpencodeProjectDirectory() {
|
|
|
3457
3708
|
function isUsableDirectory(d) {
|
|
3458
3709
|
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3459
3710
|
}
|
|
3460
|
-
function
|
|
3711
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
3712
|
+
if (configured) return configured;
|
|
3713
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
3714
|
+
if (isUsableDirectory(live)) return live;
|
|
3715
|
+
return captured ?? live;
|
|
3716
|
+
}
|
|
3717
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
3718
|
+
if (configured) return configured;
|
|
3719
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
3461
3720
|
return resolveSpawnCwdFrom(
|
|
3462
3721
|
configured,
|
|
3463
3722
|
process.cwd(),
|
|
3464
|
-
opencodeProjectDirectory
|
|
3723
|
+
opencodeProjectDirectory,
|
|
3724
|
+
sessionDir
|
|
3465
3725
|
);
|
|
3466
3726
|
}
|
|
3467
|
-
function
|
|
3468
|
-
if (
|
|
3469
|
-
|
|
3470
|
-
return
|
|
3727
|
+
async function fetchSessionDirectory(sessionID) {
|
|
3728
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3729
|
+
const client = opencodeClient;
|
|
3730
|
+
if (!client?.session?.get) return void 0;
|
|
3731
|
+
try {
|
|
3732
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3733
|
+
const data = res.data;
|
|
3734
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3735
|
+
const dir = data.directory;
|
|
3736
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
3737
|
+
} catch (err) {
|
|
3738
|
+
log.warn("failed to fetch opencode session directory", {
|
|
3739
|
+
sessionID,
|
|
3740
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3741
|
+
});
|
|
3742
|
+
return void 0;
|
|
3743
|
+
}
|
|
3471
3744
|
}
|
|
3472
3745
|
async function getRuntimeMcpStatus() {
|
|
3473
3746
|
const client = opencodeClient;
|
|
@@ -3526,40 +3799,40 @@ import { EventEmitter as EventEmitter4 } from "events";
|
|
|
3526
3799
|
import { unlink as unlink2 } from "fs/promises";
|
|
3527
3800
|
|
|
3528
3801
|
// src/claude-session-bun.ts
|
|
3529
|
-
import * as
|
|
3530
|
-
import * as
|
|
3531
|
-
import * as
|
|
3802
|
+
import * as os4 from "os";
|
|
3803
|
+
import * as fs5 from "fs";
|
|
3804
|
+
import * as path6 from "path";
|
|
3532
3805
|
import { execFileSync } from "child_process";
|
|
3533
|
-
import { randomUUID as
|
|
3806
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3534
3807
|
function resolveClaude(cmd = "claude") {
|
|
3535
|
-
if (
|
|
3808
|
+
if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
|
|
3536
3809
|
const viaBun = Bun.which(cmd);
|
|
3537
3810
|
if (viaBun) return viaBun;
|
|
3538
|
-
const isWin =
|
|
3811
|
+
const isWin = os4.platform() === "win32";
|
|
3539
3812
|
try {
|
|
3540
3813
|
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
3541
3814
|
encoding: "utf8",
|
|
3542
3815
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3543
3816
|
});
|
|
3544
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
3817
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs5.existsSync(p));
|
|
3545
3818
|
if (first) return first;
|
|
3546
3819
|
} catch {
|
|
3547
3820
|
}
|
|
3548
3821
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
3549
3822
|
}
|
|
3550
3823
|
function encodeCwd(cwd) {
|
|
3551
|
-
return
|
|
3824
|
+
return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
3552
3825
|
}
|
|
3553
3826
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
3554
3827
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3555
3828
|
function resolveConfigDir(configDir) {
|
|
3556
3829
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
3557
|
-
if (!value) return
|
|
3558
|
-
if (value === "~") return
|
|
3830
|
+
if (!value) return path6.join(os4.homedir(), ".claude");
|
|
3831
|
+
if (value === "~") return os4.homedir();
|
|
3559
3832
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
3560
|
-
return
|
|
3833
|
+
return path6.join(os4.homedir(), value.slice(2));
|
|
3561
3834
|
}
|
|
3562
|
-
return
|
|
3835
|
+
return path6.resolve(value);
|
|
3563
3836
|
}
|
|
3564
3837
|
var ClaudeSession = class {
|
|
3565
3838
|
sessionId;
|
|
@@ -3577,11 +3850,11 @@ var ClaudeSession = class {
|
|
|
3577
3850
|
signal;
|
|
3578
3851
|
o;
|
|
3579
3852
|
constructor(opts = {}) {
|
|
3580
|
-
this.cwd =
|
|
3853
|
+
this.cwd = path6.resolve(opts.cwd ?? process.cwd());
|
|
3581
3854
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
3582
3855
|
this.signal = opts.signal;
|
|
3583
|
-
this.sessionId =
|
|
3584
|
-
this.jsonlPath =
|
|
3856
|
+
this.sessionId = randomUUID4();
|
|
3857
|
+
this.jsonlPath = path6.join(
|
|
3585
3858
|
this.configDir,
|
|
3586
3859
|
"projects",
|
|
3587
3860
|
encodeCwd(this.cwd),
|
|
@@ -3704,7 +3977,7 @@ var ClaudeSession = class {
|
|
|
3704
3977
|
}
|
|
3705
3978
|
readRawLines() {
|
|
3706
3979
|
try {
|
|
3707
|
-
return
|
|
3980
|
+
return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
3708
3981
|
} catch {
|
|
3709
3982
|
return [];
|
|
3710
3983
|
}
|
|
@@ -4082,11 +4355,11 @@ function spawnInteractiveProcess(opts) {
|
|
|
4082
4355
|
}
|
|
4083
4356
|
|
|
4084
4357
|
// src/claude-code-language-model.ts
|
|
4085
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
4358
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4086
4359
|
import { unlink as unlink3 } from "fs/promises";
|
|
4087
|
-
import { homedir as
|
|
4088
|
-
import { randomUUID as
|
|
4089
|
-
import { dirname as
|
|
4360
|
+
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
4361
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4362
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
4090
4363
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
4091
4364
|
function resolveCompactionModel(configured) {
|
|
4092
4365
|
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
@@ -4293,9 +4566,9 @@ ${body}`;
|
|
|
4293
4566
|
}
|
|
4294
4567
|
});
|
|
4295
4568
|
}
|
|
4296
|
-
function readPromptFileIfPresent(
|
|
4569
|
+
function readPromptFileIfPresent(path9) {
|
|
4297
4570
|
try {
|
|
4298
|
-
const content = readFileSync3(
|
|
4571
|
+
const content = readFileSync3(path9, "utf8").trim();
|
|
4299
4572
|
return content || void 0;
|
|
4300
4573
|
} catch {
|
|
4301
4574
|
return void 0;
|
|
@@ -4304,9 +4577,9 @@ function readPromptFileIfPresent(path8) {
|
|
|
4304
4577
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
4305
4578
|
let dir = cwd;
|
|
4306
4579
|
while (true) {
|
|
4307
|
-
const content = readPromptFileIfPresent(
|
|
4580
|
+
const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
|
|
4308
4581
|
if (content) return content;
|
|
4309
|
-
const parent =
|
|
4582
|
+
const parent = dirname4(dir);
|
|
4310
4583
|
if (parent === dir) return void 0;
|
|
4311
4584
|
dir = parent;
|
|
4312
4585
|
}
|
|
@@ -4389,19 +4662,22 @@ ${options.compressionSummary.trim()}`
|
|
|
4389
4662
|
for (const s of extraSystemContent) {
|
|
4390
4663
|
if (s.trim()) parts.push(s.trim());
|
|
4391
4664
|
}
|
|
4392
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
4393
|
-
const globalAgents = readPromptFileIfPresent(
|
|
4665
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
|
|
4666
|
+
const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
|
|
4394
4667
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4668
|
+
const forwarded = extraSystemContent.join("\n\n");
|
|
4669
|
+
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
4670
|
+
const pushWorkspace = !!workspaceAgents && workspaceAgents !== globalAgents && !forwarded.includes(workspaceAgents);
|
|
4671
|
+
if (pushGlobal) parts.push(globalAgents);
|
|
4672
|
+
if (pushWorkspace) parts.push(workspaceAgents);
|
|
4673
|
+
if (pushGlobal || pushWorkspace) parts.push(AGENTS_MAINTENANCE_HINT);
|
|
4398
4674
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
4399
4675
|
const content = parts.join("\n\n");
|
|
4400
4676
|
if (!content) return void 0;
|
|
4401
|
-
const
|
|
4677
|
+
const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
4402
4678
|
try {
|
|
4403
|
-
|
|
4404
|
-
return
|
|
4679
|
+
writeFileSync4(path9, content, "utf8");
|
|
4680
|
+
return path9;
|
|
4405
4681
|
} catch (err) {
|
|
4406
4682
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
4407
4683
|
return void 0;
|
|
@@ -4982,9 +5258,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4982
5258
|
return this.doGenerateViaStream(options);
|
|
4983
5259
|
}
|
|
4984
5260
|
const warnings = [];
|
|
4985
|
-
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
4986
5261
|
const scope = this.requestScope(options);
|
|
4987
5262
|
const affinity = this.sessionAffinity(options);
|
|
5263
|
+
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
4988
5264
|
const effectiveModelId = resolveAgentModel(
|
|
4989
5265
|
this.getOpencodeAgent(options.providerOptions),
|
|
4990
5266
|
this.modelId
|
|
@@ -5058,7 +5334,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5058
5334
|
}
|
|
5059
5335
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
5060
5336
|
const includeHistoryContext = !hasExistingSession && hasPriorConversation;
|
|
5061
|
-
const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ??
|
|
5337
|
+
const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? // doGenerate has no proxy wiring, so this process issued no tool calls
|
|
5338
|
+
// at all: every tool result reaching it belongs to opencode and must be
|
|
5339
|
+
// rendered as text rather than an orphaned `tool_result` (issue #29).
|
|
5340
|
+
getClaudeUserMessage(options.prompt, includeHistoryContext, {
|
|
5341
|
+
cliToolCallIds: /* @__PURE__ */ new Set()
|
|
5342
|
+
});
|
|
5062
5343
|
const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
|
|
5063
5344
|
getRuntimeMcpStatus(),
|
|
5064
5345
|
detectCliVersion(this.config.cliPath),
|
|
@@ -5118,7 +5399,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5118
5399
|
const toolCalls = [];
|
|
5119
5400
|
const toolCallStreams = /* @__PURE__ */ new Map();
|
|
5120
5401
|
let gotPartialEvents = false;
|
|
5121
|
-
const result = await new Promise((
|
|
5402
|
+
const result = await new Promise((resolve5, reject) => {
|
|
5122
5403
|
const cleanup = () => {
|
|
5123
5404
|
try {
|
|
5124
5405
|
if (!proc.killed && proc.exitCode === null) proc.kill();
|
|
@@ -5253,7 +5534,7 @@ ${plan}
|
|
|
5253
5534
|
usage: msg.usage
|
|
5254
5535
|
};
|
|
5255
5536
|
cleanup();
|
|
5256
|
-
|
|
5537
|
+
resolve5({
|
|
5257
5538
|
...resultMeta,
|
|
5258
5539
|
text: responseText,
|
|
5259
5540
|
thinking: thinkingText,
|
|
@@ -5265,7 +5546,7 @@ ${plan}
|
|
|
5265
5546
|
});
|
|
5266
5547
|
rl.on("close", () => {
|
|
5267
5548
|
cleanup();
|
|
5268
|
-
|
|
5549
|
+
resolve5({
|
|
5269
5550
|
...resultMeta,
|
|
5270
5551
|
text: responseText,
|
|
5271
5552
|
thinking: thinkingText,
|
|
@@ -5371,11 +5652,11 @@ ${plan}
|
|
|
5371
5652
|
}
|
|
5372
5653
|
async doStream(options) {
|
|
5373
5654
|
const warnings = [];
|
|
5374
|
-
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
5375
5655
|
const cliPath = this.config.cliPath;
|
|
5376
5656
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
5377
5657
|
const scope = this.requestScope(options);
|
|
5378
5658
|
const affinity = this.sessionAffinity(options);
|
|
5659
|
+
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
5379
5660
|
const compactionMode = this.isCompactionCall(options);
|
|
5380
5661
|
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
|
|
5381
5662
|
this.getOpencodeAgent(options.providerOptions),
|
|
@@ -5514,8 +5795,10 @@ ${plan}
|
|
|
5514
5795
|
if (exitPlanModeQuestionResult) {
|
|
5515
5796
|
log.info("sending plan approval decision to claude", { sk });
|
|
5516
5797
|
}
|
|
5798
|
+
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
5517
5799
|
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, {
|
|
5518
|
-
compactionMode
|
|
5800
|
+
compactionMode,
|
|
5801
|
+
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId))
|
|
5519
5802
|
});
|
|
5520
5803
|
const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
|
|
5521
5804
|
const loadLiveToolInfo = this.createLiveToolInfoLoader();
|
|
@@ -5524,7 +5807,6 @@ ${plan}
|
|
|
5524
5807
|
loadLiveToolInfo
|
|
5525
5808
|
);
|
|
5526
5809
|
const self = this;
|
|
5527
|
-
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
5528
5810
|
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
5529
5811
|
call,
|
|
5530
5812
|
result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
|
|
@@ -5747,6 +6029,11 @@ ${plan}
|
|
|
5747
6029
|
compressionSummary: getCompressionSummary(sk)
|
|
5748
6030
|
}
|
|
5749
6031
|
);
|
|
6032
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6033
|
+
cwd,
|
|
6034
|
+
cliPath,
|
|
6035
|
+
enabled: self.config.bridgeOpencodeSkills === true
|
|
6036
|
+
});
|
|
5750
6037
|
cliArgs = buildCliArgs({
|
|
5751
6038
|
sessionKey: sk,
|
|
5752
6039
|
skipPermissions,
|
|
@@ -5756,6 +6043,7 @@ ${plan}
|
|
|
5756
6043
|
strictMcpConfig: self.config.strictMcpConfig,
|
|
5757
6044
|
disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
|
|
5758
6045
|
appendSystemPromptFile: systemPromptFile,
|
|
6046
|
+
pluginDirs: skillPluginDirs,
|
|
5759
6047
|
...self.thinkingCliOptions(),
|
|
5760
6048
|
fastMode,
|
|
5761
6049
|
cliVersion
|
|
@@ -5785,6 +6073,13 @@ ${plan}
|
|
|
5785
6073
|
activeProcess = ap;
|
|
5786
6074
|
}
|
|
5787
6075
|
}
|
|
6076
|
+
if (activeProcess && !hasMatchedPendingResults && isTurnInFlight(activeProcess)) {
|
|
6077
|
+
log.warn("previous turn still in flight; interrupting it", { sk });
|
|
6078
|
+
const idle = await interruptTurn(activeProcess);
|
|
6079
|
+
if (!idle) {
|
|
6080
|
+
log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
5788
6083
|
controller.enqueue({ type: "stream-start", warnings });
|
|
5789
6084
|
let currentTextId = null;
|
|
5790
6085
|
const textBlockIndices = /* @__PURE__ */ new Set();
|
|
@@ -5923,7 +6218,10 @@ ${plan}
|
|
|
5923
6218
|
lineEmitter.on("close", closeHandler);
|
|
5924
6219
|
proc.on("error", procErrorHandler);
|
|
5925
6220
|
try {
|
|
5926
|
-
if (!deliverPendingCompletions(true))
|
|
6221
|
+
if (!deliverPendingCompletions(true)) {
|
|
6222
|
+
noteTurnStarted(newAp);
|
|
6223
|
+
proc.stdin?.write(watchdogMessage + "\n");
|
|
6224
|
+
}
|
|
5927
6225
|
log.debug("re-sent user message after respawn", {
|
|
5928
6226
|
textLength: watchdogMessage.length
|
|
5929
6227
|
});
|
|
@@ -6150,6 +6448,7 @@ ${plan}
|
|
|
6150
6448
|
});
|
|
6151
6449
|
turnCompleted = false;
|
|
6152
6450
|
resetAutoContinueWindow();
|
|
6451
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
6153
6452
|
proc.stdin?.write(makeAutoContinueMessage() + "\n");
|
|
6154
6453
|
return;
|
|
6155
6454
|
}
|
|
@@ -6190,6 +6489,9 @@ ${plan}
|
|
|
6190
6489
|
});
|
|
6191
6490
|
controllerClosed = true;
|
|
6192
6491
|
cleanupTurn();
|
|
6492
|
+
if (!useInteractive && !compactionMode) {
|
|
6493
|
+
scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
|
|
6494
|
+
}
|
|
6193
6495
|
try {
|
|
6194
6496
|
controller.close();
|
|
6195
6497
|
} catch {
|
|
@@ -6956,6 +7258,11 @@ ${plan}
|
|
|
6956
7258
|
options.abortSignal.addEventListener("abort", () => {
|
|
6957
7259
|
autoContinueState.aborted = true;
|
|
6958
7260
|
if (turnCompleted || controllerClosed) return;
|
|
7261
|
+
if (activeProcess) {
|
|
7262
|
+
void interruptTurn(activeProcess).then((idle) => {
|
|
7263
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
7264
|
+
});
|
|
7265
|
+
}
|
|
6959
7266
|
if (!hasReceivedContent) {
|
|
6960
7267
|
log.info(
|
|
6961
7268
|
"abort signal received before content, closing stream immediately",
|
|
@@ -7043,6 +7350,7 @@ ${plan}
|
|
|
7043
7350
|
);
|
|
7044
7351
|
}
|
|
7045
7352
|
}
|
|
7353
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
7046
7354
|
proc.stdin?.write(userMsg + "\n");
|
|
7047
7355
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
7048
7356
|
armStartWatchdog();
|
|
@@ -7074,7 +7382,7 @@ ${plan}
|
|
|
7074
7382
|
|
|
7075
7383
|
// src/accounts.ts
|
|
7076
7384
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
7077
|
-
import
|
|
7385
|
+
import path7 from "path";
|
|
7078
7386
|
var BASE_PROVIDER_ID = "claude-code";
|
|
7079
7387
|
var DEFAULT_ACCOUNT = "default";
|
|
7080
7388
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -7112,7 +7420,7 @@ function expandHome(value) {
|
|
|
7112
7420
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
7113
7421
|
if (value === "~") return home ?? value;
|
|
7114
7422
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
7115
|
-
return home ?
|
|
7423
|
+
return home ? path7.join(home, value.slice(2)) : value;
|
|
7116
7424
|
}
|
|
7117
7425
|
return value;
|
|
7118
7426
|
}
|
|
@@ -7144,8 +7452,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
7144
7452
|
}
|
|
7145
7453
|
}
|
|
7146
7454
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
7147
|
-
const source =
|
|
7148
|
-
const target =
|
|
7455
|
+
const source = path7.join(sourceRoot, item);
|
|
7456
|
+
const target = path7.join(targetRoot, item);
|
|
7149
7457
|
let sourceStat;
|
|
7150
7458
|
try {
|
|
7151
7459
|
sourceStat = await lstat(source);
|
|
@@ -7156,8 +7464,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7156
7464
|
const targetStat = await lstat(target);
|
|
7157
7465
|
if (targetStat.isSymbolicLink()) {
|
|
7158
7466
|
const current = await readlink(target);
|
|
7159
|
-
const resolvedCurrent =
|
|
7160
|
-
const resolvedSource =
|
|
7467
|
+
const resolvedCurrent = path7.resolve(path7.dirname(target), current);
|
|
7468
|
+
const resolvedSource = path7.resolve(source);
|
|
7161
7469
|
if (resolvedCurrent === resolvedSource) return;
|
|
7162
7470
|
}
|
|
7163
7471
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -7172,11 +7480,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7172
7480
|
await symlink(source, target, type);
|
|
7173
7481
|
}
|
|
7174
7482
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
7175
|
-
const cacheRoot =
|
|
7483
|
+
const cacheRoot = path7.join(
|
|
7176
7484
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
7177
7485
|
"opencode-claude-code-plugin"
|
|
7178
7486
|
);
|
|
7179
|
-
const wrapperPath =
|
|
7487
|
+
const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
|
|
7180
7488
|
const suffix = `@${account}`;
|
|
7181
7489
|
await mkdir(cacheRoot, { recursive: true });
|
|
7182
7490
|
const script = `#!/usr/bin/env bash
|
|
@@ -7219,11 +7527,11 @@ import {
|
|
|
7219
7527
|
existsSync as existsSync4,
|
|
7220
7528
|
readFileSync as readFileSync4,
|
|
7221
7529
|
realpathSync,
|
|
7222
|
-
rmSync as
|
|
7223
|
-
writeFileSync as
|
|
7530
|
+
rmSync as rmSync3,
|
|
7531
|
+
writeFileSync as writeFileSync5
|
|
7224
7532
|
} from "fs";
|
|
7225
|
-
import { homedir as
|
|
7226
|
-
import { join as
|
|
7533
|
+
import { homedir as homedir6 } from "os";
|
|
7534
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
7227
7535
|
import { fileURLToPath } from "url";
|
|
7228
7536
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7229
7537
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -7231,14 +7539,14 @@ var alreadyRan = false;
|
|
|
7231
7539
|
function candidateCacheRoots() {
|
|
7232
7540
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
7233
7541
|
return [
|
|
7234
|
-
xdg ?
|
|
7235
|
-
|
|
7236
|
-
|
|
7542
|
+
xdg ? join8(xdg, "opencode") : null,
|
|
7543
|
+
join8(homedir6(), ".cache", "opencode"),
|
|
7544
|
+
join8(homedir6(), "Library", "Caches", "opencode")
|
|
7237
7545
|
].filter((p) => Boolean(p));
|
|
7238
7546
|
}
|
|
7239
7547
|
function userOpencodeJsonPath() {
|
|
7240
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
7241
|
-
return
|
|
7548
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
|
|
7549
|
+
return join8(xdgConfig, "opencode", "opencode.json");
|
|
7242
7550
|
}
|
|
7243
7551
|
function userIntendsToUseUnscoped() {
|
|
7244
7552
|
const cfg = userOpencodeJsonPath();
|
|
@@ -7257,7 +7565,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7257
7565
|
function ourLoadedDir() {
|
|
7258
7566
|
try {
|
|
7259
7567
|
const filePath = fileURLToPath(import.meta.url);
|
|
7260
|
-
return realpathSync(
|
|
7568
|
+
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7261
7569
|
} catch {
|
|
7262
7570
|
return null;
|
|
7263
7571
|
}
|
|
@@ -7281,7 +7589,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
7281
7589
|
}
|
|
7282
7590
|
function cleanupOne(cacheRoot, ourDir) {
|
|
7283
7591
|
if (!existsSync4(cacheRoot)) return;
|
|
7284
|
-
const stalePath =
|
|
7592
|
+
const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
7285
7593
|
if (!existsSync4(stalePath)) return;
|
|
7286
7594
|
let realStalePath = stalePath;
|
|
7287
7595
|
try {
|
|
@@ -7289,7 +7597,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7289
7597
|
} catch {
|
|
7290
7598
|
}
|
|
7291
7599
|
if (ourDir && realStalePath === ourDir) return;
|
|
7292
|
-
const pkgJsonPath =
|
|
7600
|
+
const pkgJsonPath = join8(stalePath, "package.json");
|
|
7293
7601
|
if (!existsSync4(pkgJsonPath)) return;
|
|
7294
7602
|
let pkg = {};
|
|
7295
7603
|
try {
|
|
@@ -7301,7 +7609,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7301
7609
|
if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
|
|
7302
7610
|
log.info("cleanup-stale: removing unscoped install", { stalePath });
|
|
7303
7611
|
try {
|
|
7304
|
-
|
|
7612
|
+
rmSync3(stalePath, { recursive: true, force: true });
|
|
7305
7613
|
} catch (err) {
|
|
7306
7614
|
log.warn("cleanup-stale: rmSync failed", {
|
|
7307
7615
|
stalePath,
|
|
@@ -7309,13 +7617,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7309
7617
|
});
|
|
7310
7618
|
return;
|
|
7311
7619
|
}
|
|
7312
|
-
const cachePkgJson =
|
|
7620
|
+
const cachePkgJson = join8(cacheRoot, "package.json");
|
|
7313
7621
|
if (!existsSync4(cachePkgJson)) return;
|
|
7314
7622
|
try {
|
|
7315
7623
|
const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
|
|
7316
7624
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
7317
7625
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
7318
|
-
|
|
7626
|
+
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
7319
7627
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
7320
7628
|
}
|
|
7321
7629
|
} catch (err) {
|
|
@@ -7327,16 +7635,16 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7327
7635
|
|
|
7328
7636
|
// src/startup-diagnostics.ts
|
|
7329
7637
|
import { execFile as execFile2 } from "child_process";
|
|
7330
|
-
import * as
|
|
7331
|
-
import * as
|
|
7638
|
+
import * as fs6 from "fs";
|
|
7639
|
+
import * as path8 from "path";
|
|
7332
7640
|
import { promisify as promisify2 } from "util";
|
|
7333
7641
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7334
7642
|
var cachedPluginVersion;
|
|
7335
7643
|
function pluginVersion() {
|
|
7336
7644
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7337
7645
|
try {
|
|
7338
|
-
const here =
|
|
7339
|
-
const raw =
|
|
7646
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
7647
|
+
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7340
7648
|
const version = JSON.parse(raw).version;
|
|
7341
7649
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
7342
7650
|
} catch {
|
|
@@ -7360,7 +7668,7 @@ var opencodeVersionProbe;
|
|
|
7360
7668
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
7361
7669
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
7362
7670
|
opencodeVersionProbe = (async () => {
|
|
7363
|
-
if (!
|
|
7671
|
+
if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
|
|
7364
7672
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
7365
7673
|
return void 0;
|
|
7366
7674
|
}
|
|
@@ -7530,6 +7838,8 @@ function createClaudeCode(settings = {}) {
|
|
|
7530
7838
|
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
|
|
7531
7839
|
compactionModel: settings.compactionModel,
|
|
7532
7840
|
ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
|
|
7841
|
+
idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
|
|
7842
|
+
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
7533
7843
|
interactive: settings.interactive,
|
|
7534
7844
|
interactiveBypass: settings.interactiveBypass,
|
|
7535
7845
|
interactiveAllowTools: settings.interactiveAllowTools,
|