@khalilgharbaoui/opencode-claude-code-plugin 0.15.4 → 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 +444 -130
- 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)) {
|
|
@@ -3093,34 +3222,166 @@ function agentDirectories(home, projectDirectory) {
|
|
|
3093
3222
|
return directories;
|
|
3094
3223
|
}
|
|
3095
3224
|
|
|
3096
|
-
// src/
|
|
3225
|
+
// src/skill-bridge.ts
|
|
3226
|
+
import * as crypto2 from "crypto";
|
|
3097
3227
|
import * as fs3 from "fs";
|
|
3098
|
-
import * as path4 from "path";
|
|
3099
3228
|
import * as os2 from "os";
|
|
3100
|
-
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";
|
|
3101
3362
|
import {
|
|
3102
3363
|
parse as parseJsonc,
|
|
3103
3364
|
printParseErrorCode
|
|
3104
3365
|
} from "jsonc-parser";
|
|
3105
3366
|
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
3106
3367
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3107
|
-
function
|
|
3368
|
+
function fileExists2(p) {
|
|
3108
3369
|
try {
|
|
3109
|
-
return
|
|
3370
|
+
return fs4.statSync(p).isFile();
|
|
3110
3371
|
} catch {
|
|
3111
3372
|
return false;
|
|
3112
3373
|
}
|
|
3113
3374
|
}
|
|
3114
|
-
function
|
|
3375
|
+
function dirExists2(p) {
|
|
3115
3376
|
try {
|
|
3116
|
-
return
|
|
3377
|
+
return fs4.statSync(p).isDirectory();
|
|
3117
3378
|
} catch {
|
|
3118
3379
|
return false;
|
|
3119
3380
|
}
|
|
3120
3381
|
}
|
|
3121
3382
|
function readAndParse(file) {
|
|
3122
3383
|
try {
|
|
3123
|
-
const raw =
|
|
3384
|
+
const raw = fs4.readFileSync(file, "utf8");
|
|
3124
3385
|
const errors = [];
|
|
3125
3386
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3126
3387
|
if (errors.length > 0) {
|
|
@@ -3156,14 +3417,14 @@ function deepMerge(target, source) {
|
|
|
3156
3417
|
}
|
|
3157
3418
|
function walkUp(opts) {
|
|
3158
3419
|
const out = [];
|
|
3159
|
-
let current =
|
|
3420
|
+
let current = path5.resolve(opts.start);
|
|
3160
3421
|
while (true) {
|
|
3161
3422
|
for (const target of opts.targets) {
|
|
3162
|
-
const candidate =
|
|
3423
|
+
const candidate = path5.join(current, target);
|
|
3163
3424
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3164
3425
|
}
|
|
3165
|
-
if (opts.stop && current ===
|
|
3166
|
-
const parent =
|
|
3426
|
+
if (opts.stop && current === path5.resolve(opts.stop)) break;
|
|
3427
|
+
const parent = path5.dirname(current);
|
|
3167
3428
|
if (parent === current) break;
|
|
3168
3429
|
current = parent;
|
|
3169
3430
|
}
|
|
@@ -3171,29 +3432,29 @@ function walkUp(opts) {
|
|
|
3171
3432
|
}
|
|
3172
3433
|
function detectWorktree(cwd) {
|
|
3173
3434
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3174
|
-
if (override) return
|
|
3175
|
-
let current =
|
|
3435
|
+
if (override) return path5.resolve(override);
|
|
3436
|
+
let current = path5.resolve(cwd);
|
|
3176
3437
|
while (true) {
|
|
3177
|
-
const gitPath =
|
|
3438
|
+
const gitPath = path5.join(current, ".git");
|
|
3178
3439
|
try {
|
|
3179
|
-
if (
|
|
3440
|
+
if (fs4.existsSync(gitPath)) return current;
|
|
3180
3441
|
} catch {
|
|
3181
3442
|
}
|
|
3182
|
-
const parent =
|
|
3443
|
+
const parent = path5.dirname(current);
|
|
3183
3444
|
if (parent === current) return void 0;
|
|
3184
3445
|
current = parent;
|
|
3185
3446
|
}
|
|
3186
3447
|
}
|
|
3187
3448
|
function globalConfigDir() {
|
|
3188
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3189
|
-
return
|
|
3449
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
|
|
3450
|
+
return path5.join(xdg, "opencode");
|
|
3190
3451
|
}
|
|
3191
3452
|
function loadGlobalConfig() {
|
|
3192
3453
|
const dir = globalConfigDir();
|
|
3193
3454
|
let merged = {};
|
|
3194
3455
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3195
|
-
const file =
|
|
3196
|
-
if (!
|
|
3456
|
+
const file = path5.join(dir, name);
|
|
3457
|
+
if (!fileExists2(file)) continue;
|
|
3197
3458
|
const parsed = readAndParse(file);
|
|
3198
3459
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
3199
3460
|
}
|
|
@@ -3202,8 +3463,8 @@ function loadGlobalConfig() {
|
|
|
3202
3463
|
function loadProjectFilesInDir(dir) {
|
|
3203
3464
|
let merged = {};
|
|
3204
3465
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3205
|
-
const file =
|
|
3206
|
-
if (!
|
|
3466
|
+
const file = path5.join(dir, name);
|
|
3467
|
+
if (!fileExists2(file)) continue;
|
|
3207
3468
|
const parsed = readAndParse(file);
|
|
3208
3469
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
3209
3470
|
}
|
|
@@ -3213,8 +3474,8 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3213
3474
|
const dirs = [];
|
|
3214
3475
|
const seen = /* @__PURE__ */ new Set();
|
|
3215
3476
|
const push = (p) => {
|
|
3216
|
-
const abs =
|
|
3217
|
-
if (!seen.has(abs) &&
|
|
3477
|
+
const abs = path5.resolve(p);
|
|
3478
|
+
if (!seen.has(abs) && dirExists2(abs)) {
|
|
3218
3479
|
seen.add(abs);
|
|
3219
3480
|
dirs.push(abs);
|
|
3220
3481
|
}
|
|
@@ -3223,17 +3484,17 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3223
3484
|
start: cwd,
|
|
3224
3485
|
stop: worktree,
|
|
3225
3486
|
targets: [".opencode"],
|
|
3226
|
-
predicate:
|
|
3487
|
+
predicate: dirExists2
|
|
3227
3488
|
})) {
|
|
3228
3489
|
push(dir);
|
|
3229
3490
|
}
|
|
3230
|
-
const home =
|
|
3491
|
+
const home = os3.homedir();
|
|
3231
3492
|
if (home) {
|
|
3232
|
-
const homeDot =
|
|
3233
|
-
if (
|
|
3493
|
+
const homeDot = path5.join(home, ".opencode");
|
|
3494
|
+
if (dirExists2(homeDot)) push(homeDot);
|
|
3234
3495
|
}
|
|
3235
3496
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3236
|
-
if (envDir &&
|
|
3497
|
+
if (envDir && dirExists2(envDir)) push(envDir);
|
|
3237
3498
|
return dirs;
|
|
3238
3499
|
}
|
|
3239
3500
|
function substituteEnvPlaceholders(source) {
|
|
@@ -3341,7 +3602,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3341
3602
|
let merged = {};
|
|
3342
3603
|
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
3343
3604
|
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3344
|
-
if (explicitConfig &&
|
|
3605
|
+
if (explicitConfig && fileExists2(explicitConfig)) {
|
|
3345
3606
|
const parsed = readAndParse(explicitConfig);
|
|
3346
3607
|
if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
|
|
3347
3608
|
}
|
|
@@ -3349,12 +3610,12 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3349
3610
|
start: cwd,
|
|
3350
3611
|
stop: worktree,
|
|
3351
3612
|
targets: PROJECT_FILE_NAMES,
|
|
3352
|
-
predicate:
|
|
3613
|
+
predicate: fileExists2
|
|
3353
3614
|
});
|
|
3354
3615
|
const projectDirs = [];
|
|
3355
3616
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3356
3617
|
for (const f of projectFiles) {
|
|
3357
|
-
const d =
|
|
3618
|
+
const d = path5.dirname(f);
|
|
3358
3619
|
if (!seenProjectDirs.has(d)) {
|
|
3359
3620
|
seenProjectDirs.add(d);
|
|
3360
3621
|
projectDirs.push(d);
|
|
@@ -3383,7 +3644,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3383
3644
|
enabledServerNames.push(name);
|
|
3384
3645
|
}
|
|
3385
3646
|
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3386
|
-
const hash =
|
|
3647
|
+
const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3387
3648
|
return { servers: merged, enabledServerNames, hash };
|
|
3388
3649
|
}
|
|
3389
3650
|
function finishBridge(input) {
|
|
@@ -3399,13 +3660,13 @@ function finishBridge(input) {
|
|
|
3399
3660
|
};
|
|
3400
3661
|
}
|
|
3401
3662
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3402
|
-
const outPath =
|
|
3663
|
+
const outPath = path5.join(
|
|
3403
3664
|
pluginTmpDir(),
|
|
3404
3665
|
`mcp-${hash}.json`
|
|
3405
3666
|
);
|
|
3406
3667
|
try {
|
|
3407
|
-
if (!
|
|
3408
|
-
|
|
3668
|
+
if (!fileExists2(outPath)) {
|
|
3669
|
+
fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3409
3670
|
}
|
|
3410
3671
|
} catch (e) {
|
|
3411
3672
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -3447,17 +3708,39 @@ function getOpencodeProjectDirectory() {
|
|
|
3447
3708
|
function isUsableDirectory(d) {
|
|
3448
3709
|
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3449
3710
|
}
|
|
3450
|
-
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;
|
|
3451
3720
|
return resolveSpawnCwdFrom(
|
|
3452
3721
|
configured,
|
|
3453
3722
|
process.cwd(),
|
|
3454
|
-
opencodeProjectDirectory
|
|
3723
|
+
opencodeProjectDirectory,
|
|
3724
|
+
sessionDir
|
|
3455
3725
|
);
|
|
3456
3726
|
}
|
|
3457
|
-
function
|
|
3458
|
-
if (
|
|
3459
|
-
|
|
3460
|
-
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
|
+
}
|
|
3461
3744
|
}
|
|
3462
3745
|
async function getRuntimeMcpStatus() {
|
|
3463
3746
|
const client = opencodeClient;
|
|
@@ -3516,40 +3799,40 @@ import { EventEmitter as EventEmitter4 } from "events";
|
|
|
3516
3799
|
import { unlink as unlink2 } from "fs/promises";
|
|
3517
3800
|
|
|
3518
3801
|
// src/claude-session-bun.ts
|
|
3519
|
-
import * as
|
|
3520
|
-
import * as
|
|
3521
|
-
import * as
|
|
3802
|
+
import * as os4 from "os";
|
|
3803
|
+
import * as fs5 from "fs";
|
|
3804
|
+
import * as path6 from "path";
|
|
3522
3805
|
import { execFileSync } from "child_process";
|
|
3523
|
-
import { randomUUID as
|
|
3806
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3524
3807
|
function resolveClaude(cmd = "claude") {
|
|
3525
|
-
if (
|
|
3808
|
+
if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
|
|
3526
3809
|
const viaBun = Bun.which(cmd);
|
|
3527
3810
|
if (viaBun) return viaBun;
|
|
3528
|
-
const isWin =
|
|
3811
|
+
const isWin = os4.platform() === "win32";
|
|
3529
3812
|
try {
|
|
3530
3813
|
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
3531
3814
|
encoding: "utf8",
|
|
3532
3815
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3533
3816
|
});
|
|
3534
|
-
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));
|
|
3535
3818
|
if (first) return first;
|
|
3536
3819
|
} catch {
|
|
3537
3820
|
}
|
|
3538
3821
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
3539
3822
|
}
|
|
3540
3823
|
function encodeCwd(cwd) {
|
|
3541
|
-
return
|
|
3824
|
+
return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
3542
3825
|
}
|
|
3543
3826
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
3544
3827
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3545
3828
|
function resolveConfigDir(configDir) {
|
|
3546
3829
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
3547
|
-
if (!value) return
|
|
3548
|
-
if (value === "~") return
|
|
3830
|
+
if (!value) return path6.join(os4.homedir(), ".claude");
|
|
3831
|
+
if (value === "~") return os4.homedir();
|
|
3549
3832
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
3550
|
-
return
|
|
3833
|
+
return path6.join(os4.homedir(), value.slice(2));
|
|
3551
3834
|
}
|
|
3552
|
-
return
|
|
3835
|
+
return path6.resolve(value);
|
|
3553
3836
|
}
|
|
3554
3837
|
var ClaudeSession = class {
|
|
3555
3838
|
sessionId;
|
|
@@ -3567,11 +3850,11 @@ var ClaudeSession = class {
|
|
|
3567
3850
|
signal;
|
|
3568
3851
|
o;
|
|
3569
3852
|
constructor(opts = {}) {
|
|
3570
|
-
this.cwd =
|
|
3853
|
+
this.cwd = path6.resolve(opts.cwd ?? process.cwd());
|
|
3571
3854
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
3572
3855
|
this.signal = opts.signal;
|
|
3573
|
-
this.sessionId =
|
|
3574
|
-
this.jsonlPath =
|
|
3856
|
+
this.sessionId = randomUUID4();
|
|
3857
|
+
this.jsonlPath = path6.join(
|
|
3575
3858
|
this.configDir,
|
|
3576
3859
|
"projects",
|
|
3577
3860
|
encodeCwd(this.cwd),
|
|
@@ -3694,7 +3977,7 @@ var ClaudeSession = class {
|
|
|
3694
3977
|
}
|
|
3695
3978
|
readRawLines() {
|
|
3696
3979
|
try {
|
|
3697
|
-
return
|
|
3980
|
+
return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
3698
3981
|
} catch {
|
|
3699
3982
|
return [];
|
|
3700
3983
|
}
|
|
@@ -4072,11 +4355,11 @@ function spawnInteractiveProcess(opts) {
|
|
|
4072
4355
|
}
|
|
4073
4356
|
|
|
4074
4357
|
// src/claude-code-language-model.ts
|
|
4075
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
4358
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4076
4359
|
import { unlink as unlink3 } from "fs/promises";
|
|
4077
|
-
import { homedir as
|
|
4078
|
-
import { randomUUID as
|
|
4079
|
-
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";
|
|
4080
4363
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
4081
4364
|
function resolveCompactionModel(configured) {
|
|
4082
4365
|
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
@@ -4283,9 +4566,9 @@ ${body}`;
|
|
|
4283
4566
|
}
|
|
4284
4567
|
});
|
|
4285
4568
|
}
|
|
4286
|
-
function readPromptFileIfPresent(
|
|
4569
|
+
function readPromptFileIfPresent(path9) {
|
|
4287
4570
|
try {
|
|
4288
|
-
const content = readFileSync3(
|
|
4571
|
+
const content = readFileSync3(path9, "utf8").trim();
|
|
4289
4572
|
return content || void 0;
|
|
4290
4573
|
} catch {
|
|
4291
4574
|
return void 0;
|
|
@@ -4294,9 +4577,9 @@ function readPromptFileIfPresent(path8) {
|
|
|
4294
4577
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
4295
4578
|
let dir = cwd;
|
|
4296
4579
|
while (true) {
|
|
4297
|
-
const content = readPromptFileIfPresent(
|
|
4580
|
+
const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
|
|
4298
4581
|
if (content) return content;
|
|
4299
|
-
const parent =
|
|
4582
|
+
const parent = dirname4(dir);
|
|
4300
4583
|
if (parent === dir) return void 0;
|
|
4301
4584
|
dir = parent;
|
|
4302
4585
|
}
|
|
@@ -4379,19 +4662,22 @@ ${options.compressionSummary.trim()}`
|
|
|
4379
4662
|
for (const s of extraSystemContent) {
|
|
4380
4663
|
if (s.trim()) parts.push(s.trim());
|
|
4381
4664
|
}
|
|
4382
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
4383
|
-
const globalAgents = readPromptFileIfPresent(
|
|
4665
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
|
|
4666
|
+
const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
|
|
4384
4667
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
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);
|
|
4388
4674
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
4389
4675
|
const content = parts.join("\n\n");
|
|
4390
4676
|
if (!content) return void 0;
|
|
4391
|
-
const
|
|
4677
|
+
const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
4392
4678
|
try {
|
|
4393
|
-
|
|
4394
|
-
return
|
|
4679
|
+
writeFileSync4(path9, content, "utf8");
|
|
4680
|
+
return path9;
|
|
4395
4681
|
} catch (err) {
|
|
4396
4682
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
4397
4683
|
return void 0;
|
|
@@ -4972,9 +5258,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4972
5258
|
return this.doGenerateViaStream(options);
|
|
4973
5259
|
}
|
|
4974
5260
|
const warnings = [];
|
|
4975
|
-
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
4976
5261
|
const scope = this.requestScope(options);
|
|
4977
5262
|
const affinity = this.sessionAffinity(options);
|
|
5263
|
+
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
4978
5264
|
const effectiveModelId = resolveAgentModel(
|
|
4979
5265
|
this.getOpencodeAgent(options.providerOptions),
|
|
4980
5266
|
this.modelId
|
|
@@ -5113,7 +5399,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5113
5399
|
const toolCalls = [];
|
|
5114
5400
|
const toolCallStreams = /* @__PURE__ */ new Map();
|
|
5115
5401
|
let gotPartialEvents = false;
|
|
5116
|
-
const result = await new Promise((
|
|
5402
|
+
const result = await new Promise((resolve5, reject) => {
|
|
5117
5403
|
const cleanup = () => {
|
|
5118
5404
|
try {
|
|
5119
5405
|
if (!proc.killed && proc.exitCode === null) proc.kill();
|
|
@@ -5248,7 +5534,7 @@ ${plan}
|
|
|
5248
5534
|
usage: msg.usage
|
|
5249
5535
|
};
|
|
5250
5536
|
cleanup();
|
|
5251
|
-
|
|
5537
|
+
resolve5({
|
|
5252
5538
|
...resultMeta,
|
|
5253
5539
|
text: responseText,
|
|
5254
5540
|
thinking: thinkingText,
|
|
@@ -5260,7 +5546,7 @@ ${plan}
|
|
|
5260
5546
|
});
|
|
5261
5547
|
rl.on("close", () => {
|
|
5262
5548
|
cleanup();
|
|
5263
|
-
|
|
5549
|
+
resolve5({
|
|
5264
5550
|
...resultMeta,
|
|
5265
5551
|
text: responseText,
|
|
5266
5552
|
thinking: thinkingText,
|
|
@@ -5366,11 +5652,11 @@ ${plan}
|
|
|
5366
5652
|
}
|
|
5367
5653
|
async doStream(options) {
|
|
5368
5654
|
const warnings = [];
|
|
5369
|
-
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
5370
5655
|
const cliPath = this.config.cliPath;
|
|
5371
5656
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
5372
5657
|
const scope = this.requestScope(options);
|
|
5373
5658
|
const affinity = this.sessionAffinity(options);
|
|
5659
|
+
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
5374
5660
|
const compactionMode = this.isCompactionCall(options);
|
|
5375
5661
|
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
|
|
5376
5662
|
this.getOpencodeAgent(options.providerOptions),
|
|
@@ -5743,6 +6029,11 @@ ${plan}
|
|
|
5743
6029
|
compressionSummary: getCompressionSummary(sk)
|
|
5744
6030
|
}
|
|
5745
6031
|
);
|
|
6032
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6033
|
+
cwd,
|
|
6034
|
+
cliPath,
|
|
6035
|
+
enabled: self.config.bridgeOpencodeSkills === true
|
|
6036
|
+
});
|
|
5746
6037
|
cliArgs = buildCliArgs({
|
|
5747
6038
|
sessionKey: sk,
|
|
5748
6039
|
skipPermissions,
|
|
@@ -5752,6 +6043,7 @@ ${plan}
|
|
|
5752
6043
|
strictMcpConfig: self.config.strictMcpConfig,
|
|
5753
6044
|
disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
|
|
5754
6045
|
appendSystemPromptFile: systemPromptFile,
|
|
6046
|
+
pluginDirs: skillPluginDirs,
|
|
5755
6047
|
...self.thinkingCliOptions(),
|
|
5756
6048
|
fastMode,
|
|
5757
6049
|
cliVersion
|
|
@@ -5781,6 +6073,13 @@ ${plan}
|
|
|
5781
6073
|
activeProcess = ap;
|
|
5782
6074
|
}
|
|
5783
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
|
+
}
|
|
5784
6083
|
controller.enqueue({ type: "stream-start", warnings });
|
|
5785
6084
|
let currentTextId = null;
|
|
5786
6085
|
const textBlockIndices = /* @__PURE__ */ new Set();
|
|
@@ -5919,7 +6218,10 @@ ${plan}
|
|
|
5919
6218
|
lineEmitter.on("close", closeHandler);
|
|
5920
6219
|
proc.on("error", procErrorHandler);
|
|
5921
6220
|
try {
|
|
5922
|
-
if (!deliverPendingCompletions(true))
|
|
6221
|
+
if (!deliverPendingCompletions(true)) {
|
|
6222
|
+
noteTurnStarted(newAp);
|
|
6223
|
+
proc.stdin?.write(watchdogMessage + "\n");
|
|
6224
|
+
}
|
|
5923
6225
|
log.debug("re-sent user message after respawn", {
|
|
5924
6226
|
textLength: watchdogMessage.length
|
|
5925
6227
|
});
|
|
@@ -6146,6 +6448,7 @@ ${plan}
|
|
|
6146
6448
|
});
|
|
6147
6449
|
turnCompleted = false;
|
|
6148
6450
|
resetAutoContinueWindow();
|
|
6451
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
6149
6452
|
proc.stdin?.write(makeAutoContinueMessage() + "\n");
|
|
6150
6453
|
return;
|
|
6151
6454
|
}
|
|
@@ -6186,6 +6489,9 @@ ${plan}
|
|
|
6186
6489
|
});
|
|
6187
6490
|
controllerClosed = true;
|
|
6188
6491
|
cleanupTurn();
|
|
6492
|
+
if (!useInteractive && !compactionMode) {
|
|
6493
|
+
scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
|
|
6494
|
+
}
|
|
6189
6495
|
try {
|
|
6190
6496
|
controller.close();
|
|
6191
6497
|
} catch {
|
|
@@ -6952,6 +7258,11 @@ ${plan}
|
|
|
6952
7258
|
options.abortSignal.addEventListener("abort", () => {
|
|
6953
7259
|
autoContinueState.aborted = true;
|
|
6954
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
|
+
}
|
|
6955
7266
|
if (!hasReceivedContent) {
|
|
6956
7267
|
log.info(
|
|
6957
7268
|
"abort signal received before content, closing stream immediately",
|
|
@@ -7039,6 +7350,7 @@ ${plan}
|
|
|
7039
7350
|
);
|
|
7040
7351
|
}
|
|
7041
7352
|
}
|
|
7353
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
7042
7354
|
proc.stdin?.write(userMsg + "\n");
|
|
7043
7355
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
7044
7356
|
armStartWatchdog();
|
|
@@ -7070,7 +7382,7 @@ ${plan}
|
|
|
7070
7382
|
|
|
7071
7383
|
// src/accounts.ts
|
|
7072
7384
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
7073
|
-
import
|
|
7385
|
+
import path7 from "path";
|
|
7074
7386
|
var BASE_PROVIDER_ID = "claude-code";
|
|
7075
7387
|
var DEFAULT_ACCOUNT = "default";
|
|
7076
7388
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -7108,7 +7420,7 @@ function expandHome(value) {
|
|
|
7108
7420
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
7109
7421
|
if (value === "~") return home ?? value;
|
|
7110
7422
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
7111
|
-
return home ?
|
|
7423
|
+
return home ? path7.join(home, value.slice(2)) : value;
|
|
7112
7424
|
}
|
|
7113
7425
|
return value;
|
|
7114
7426
|
}
|
|
@@ -7140,8 +7452,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
7140
7452
|
}
|
|
7141
7453
|
}
|
|
7142
7454
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
7143
|
-
const source =
|
|
7144
|
-
const target =
|
|
7455
|
+
const source = path7.join(sourceRoot, item);
|
|
7456
|
+
const target = path7.join(targetRoot, item);
|
|
7145
7457
|
let sourceStat;
|
|
7146
7458
|
try {
|
|
7147
7459
|
sourceStat = await lstat(source);
|
|
@@ -7152,8 +7464,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7152
7464
|
const targetStat = await lstat(target);
|
|
7153
7465
|
if (targetStat.isSymbolicLink()) {
|
|
7154
7466
|
const current = await readlink(target);
|
|
7155
|
-
const resolvedCurrent =
|
|
7156
|
-
const resolvedSource =
|
|
7467
|
+
const resolvedCurrent = path7.resolve(path7.dirname(target), current);
|
|
7468
|
+
const resolvedSource = path7.resolve(source);
|
|
7157
7469
|
if (resolvedCurrent === resolvedSource) return;
|
|
7158
7470
|
}
|
|
7159
7471
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -7168,11 +7480,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7168
7480
|
await symlink(source, target, type);
|
|
7169
7481
|
}
|
|
7170
7482
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
7171
|
-
const cacheRoot =
|
|
7483
|
+
const cacheRoot = path7.join(
|
|
7172
7484
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
7173
7485
|
"opencode-claude-code-plugin"
|
|
7174
7486
|
);
|
|
7175
|
-
const wrapperPath =
|
|
7487
|
+
const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
|
|
7176
7488
|
const suffix = `@${account}`;
|
|
7177
7489
|
await mkdir(cacheRoot, { recursive: true });
|
|
7178
7490
|
const script = `#!/usr/bin/env bash
|
|
@@ -7215,11 +7527,11 @@ import {
|
|
|
7215
7527
|
existsSync as existsSync4,
|
|
7216
7528
|
readFileSync as readFileSync4,
|
|
7217
7529
|
realpathSync,
|
|
7218
|
-
rmSync as
|
|
7219
|
-
writeFileSync as
|
|
7530
|
+
rmSync as rmSync3,
|
|
7531
|
+
writeFileSync as writeFileSync5
|
|
7220
7532
|
} from "fs";
|
|
7221
|
-
import { homedir as
|
|
7222
|
-
import { join as
|
|
7533
|
+
import { homedir as homedir6 } from "os";
|
|
7534
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
7223
7535
|
import { fileURLToPath } from "url";
|
|
7224
7536
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7225
7537
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -7227,14 +7539,14 @@ var alreadyRan = false;
|
|
|
7227
7539
|
function candidateCacheRoots() {
|
|
7228
7540
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
7229
7541
|
return [
|
|
7230
|
-
xdg ?
|
|
7231
|
-
|
|
7232
|
-
|
|
7542
|
+
xdg ? join8(xdg, "opencode") : null,
|
|
7543
|
+
join8(homedir6(), ".cache", "opencode"),
|
|
7544
|
+
join8(homedir6(), "Library", "Caches", "opencode")
|
|
7233
7545
|
].filter((p) => Boolean(p));
|
|
7234
7546
|
}
|
|
7235
7547
|
function userOpencodeJsonPath() {
|
|
7236
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
7237
|
-
return
|
|
7548
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
|
|
7549
|
+
return join8(xdgConfig, "opencode", "opencode.json");
|
|
7238
7550
|
}
|
|
7239
7551
|
function userIntendsToUseUnscoped() {
|
|
7240
7552
|
const cfg = userOpencodeJsonPath();
|
|
@@ -7253,7 +7565,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7253
7565
|
function ourLoadedDir() {
|
|
7254
7566
|
try {
|
|
7255
7567
|
const filePath = fileURLToPath(import.meta.url);
|
|
7256
|
-
return realpathSync(
|
|
7568
|
+
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7257
7569
|
} catch {
|
|
7258
7570
|
return null;
|
|
7259
7571
|
}
|
|
@@ -7277,7 +7589,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
7277
7589
|
}
|
|
7278
7590
|
function cleanupOne(cacheRoot, ourDir) {
|
|
7279
7591
|
if (!existsSync4(cacheRoot)) return;
|
|
7280
|
-
const stalePath =
|
|
7592
|
+
const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
7281
7593
|
if (!existsSync4(stalePath)) return;
|
|
7282
7594
|
let realStalePath = stalePath;
|
|
7283
7595
|
try {
|
|
@@ -7285,7 +7597,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7285
7597
|
} catch {
|
|
7286
7598
|
}
|
|
7287
7599
|
if (ourDir && realStalePath === ourDir) return;
|
|
7288
|
-
const pkgJsonPath =
|
|
7600
|
+
const pkgJsonPath = join8(stalePath, "package.json");
|
|
7289
7601
|
if (!existsSync4(pkgJsonPath)) return;
|
|
7290
7602
|
let pkg = {};
|
|
7291
7603
|
try {
|
|
@@ -7297,7 +7609,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7297
7609
|
if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
|
|
7298
7610
|
log.info("cleanup-stale: removing unscoped install", { stalePath });
|
|
7299
7611
|
try {
|
|
7300
|
-
|
|
7612
|
+
rmSync3(stalePath, { recursive: true, force: true });
|
|
7301
7613
|
} catch (err) {
|
|
7302
7614
|
log.warn("cleanup-stale: rmSync failed", {
|
|
7303
7615
|
stalePath,
|
|
@@ -7305,13 +7617,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7305
7617
|
});
|
|
7306
7618
|
return;
|
|
7307
7619
|
}
|
|
7308
|
-
const cachePkgJson =
|
|
7620
|
+
const cachePkgJson = join8(cacheRoot, "package.json");
|
|
7309
7621
|
if (!existsSync4(cachePkgJson)) return;
|
|
7310
7622
|
try {
|
|
7311
7623
|
const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
|
|
7312
7624
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
7313
7625
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
7314
|
-
|
|
7626
|
+
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
7315
7627
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
7316
7628
|
}
|
|
7317
7629
|
} catch (err) {
|
|
@@ -7323,16 +7635,16 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7323
7635
|
|
|
7324
7636
|
// src/startup-diagnostics.ts
|
|
7325
7637
|
import { execFile as execFile2 } from "child_process";
|
|
7326
|
-
import * as
|
|
7327
|
-
import * as
|
|
7638
|
+
import * as fs6 from "fs";
|
|
7639
|
+
import * as path8 from "path";
|
|
7328
7640
|
import { promisify as promisify2 } from "util";
|
|
7329
7641
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7330
7642
|
var cachedPluginVersion;
|
|
7331
7643
|
function pluginVersion() {
|
|
7332
7644
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7333
7645
|
try {
|
|
7334
|
-
const here =
|
|
7335
|
-
const raw =
|
|
7646
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
7647
|
+
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7336
7648
|
const version = JSON.parse(raw).version;
|
|
7337
7649
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
7338
7650
|
} catch {
|
|
@@ -7356,7 +7668,7 @@ var opencodeVersionProbe;
|
|
|
7356
7668
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
7357
7669
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
7358
7670
|
opencodeVersionProbe = (async () => {
|
|
7359
|
-
if (!
|
|
7671
|
+
if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
|
|
7360
7672
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
7361
7673
|
return void 0;
|
|
7362
7674
|
}
|
|
@@ -7526,6 +7838,8 @@ function createClaudeCode(settings = {}) {
|
|
|
7526
7838
|
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
|
|
7527
7839
|
compactionModel: settings.compactionModel,
|
|
7528
7840
|
ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
|
|
7841
|
+
idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
|
|
7842
|
+
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
7529
7843
|
interactive: settings.interactive,
|
|
7530
7844
|
interactiveBypass: settings.interactiveBypass,
|
|
7531
7845
|
interactiveAllowTools: settings.interactiveAllowTools,
|