@khalilgharbaoui/opencode-claude-code-plugin 0.15.4 → 0.17.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 +48 -4
- package/dist/index.d.ts +32 -1
- package/dist/index.js +621 -169
- 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
|
|
|
@@ -498,6 +523,8 @@ var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
|
498
523
|
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
499
524
|
task: 60 * 60 * 1e3,
|
|
500
525
|
// 60 min
|
|
526
|
+
task_batch: 60 * 60 * 1e3,
|
|
527
|
+
// 60 min, same reasoning: it IS task calls
|
|
501
528
|
question: 30 * 60 * 1e3
|
|
502
529
|
// 30 min
|
|
503
530
|
};
|
|
@@ -537,14 +564,58 @@ function resolveProxyClientCeilingMs(overrides) {
|
|
|
537
564
|
function buildProxyTimeoutError(toolName, ms) {
|
|
538
565
|
const key = toolName.toLowerCase();
|
|
539
566
|
const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
|
|
540
|
-
if (key === "task") {
|
|
567
|
+
if (key === "task" || key === TASK_BATCH_TOOL_NAME) {
|
|
541
568
|
return new Error(
|
|
542
|
-
base + " (the subagent). The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
|
|
569
|
+
base + (key === "task" ? " (the subagent)." : " (the subagents).") + " The subagent may still be running but its result is no longer reachable in this session. Do not declare the dispatch failed, and do not 'schedule a wake-up' or defer -- that mechanism does not apply here. If the result is required, re-dispatch or verify it directly now."
|
|
543
570
|
);
|
|
544
571
|
}
|
|
545
572
|
return new Error(base);
|
|
546
573
|
}
|
|
547
|
-
var TASK_PROXY_NOTE = "This
|
|
574
|
+
var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
|
|
575
|
+
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
576
|
+
var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Same 60-minute proxy deadline as task (configurable via proxyToolTimeoutMs).";
|
|
577
|
+
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
578
|
+
function taskBatchInputError(input) {
|
|
579
|
+
const tasks = input?.tasks;
|
|
580
|
+
if (!Array.isArray(tasks) || tasks.length < 2) {
|
|
581
|
+
return "task_batch requires a `tasks` array with at least two items; use `task` for one subagent";
|
|
582
|
+
}
|
|
583
|
+
for (const [index, task] of tasks.entries()) {
|
|
584
|
+
if (task === null || typeof task !== "object" || Array.isArray(task)) {
|
|
585
|
+
return `task_batch tasks[${index}] must be an object`;
|
|
586
|
+
}
|
|
587
|
+
const item = task;
|
|
588
|
+
for (const field of TASK_INPUT_REQUIRED) {
|
|
589
|
+
if (typeof item[field] !== "string") {
|
|
590
|
+
return `task_batch tasks[${index}].${field} must be a string`;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
function taskBatchTasks(input) {
|
|
597
|
+
if (taskBatchInputError(input)) return [];
|
|
598
|
+
return input.tasks;
|
|
599
|
+
}
|
|
600
|
+
function taskBatchChildToolCallId(parentToolCallId, index) {
|
|
601
|
+
return `${parentToolCallId}_task_${index}`;
|
|
602
|
+
}
|
|
603
|
+
function formatTaskBatchResults(children) {
|
|
604
|
+
const total = children.length;
|
|
605
|
+
const sections = children.map(({ task, result }, index) => {
|
|
606
|
+
const label = typeof task.description === "string" ? task.description : `task ${index + 1}`;
|
|
607
|
+
const agent = typeof task.subagent_type === "string" ? ` (${task.subagent_type})` : "";
|
|
608
|
+
const header = `## task ${index + 1} of ${total}: ${label}${agent}`;
|
|
609
|
+
if (!result) return `${header}
|
|
610
|
+
[missing] opencode returned no result for this task in the batch`;
|
|
611
|
+
if (result.kind === "error") return `${header}
|
|
612
|
+
[error] ${result.message}`;
|
|
613
|
+
return `${header}
|
|
614
|
+
${result.isError ? "[error] " : ""}${result.text}`;
|
|
615
|
+
});
|
|
616
|
+
const failed = children.some(({ result }) => !result || result.kind === "error" || result.isError);
|
|
617
|
+
return { kind: "text", text: sections.join("\n\n"), ...failed ? { isError: true } : {} };
|
|
618
|
+
}
|
|
548
619
|
var AGENT_TYPES_HEADING = "Available agent types";
|
|
549
620
|
var AGENT_BLURB_LIMIT = 140;
|
|
550
621
|
var QUESTION_PROXY_NOTE = "This routes structured questions through opencode's native `question` tool, which renders a TUI form with the options you provide and blocks until the operator answers. Claude Code's built-in AskUserQuestion is disabled in this environment; this proxy is the ONLY way to ask the operator for a decision or clarification. Answers come back as arrays of selected labels (set `multiple: true` to allow more than one). If the operator dismisses the form the call returns an error \u2014 treat that as 'no answer' and stop, do not guess. Question calls get a 30-minute proxy deadline by default (configurable via proxyToolTimeoutMs); for long-AFK scenarios prefer fewer, high-signal questions.";
|
|
@@ -572,7 +643,7 @@ function overlayTaskProxyDescription(tools, liveDescription) {
|
|
|
572
643
|
const agentTypes = extractAgentTypeList(liveDescription);
|
|
573
644
|
if (!agentTypes) return tools;
|
|
574
645
|
return tools.map(
|
|
575
|
-
(t) => t.name === "task" ? { ...t, description: `${agentTypes}
|
|
646
|
+
(t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME ? { ...t, description: `${agentTypes}
|
|
576
647
|
|
|
577
648
|
${t.description}` } : t
|
|
578
649
|
);
|
|
@@ -590,6 +661,32 @@ function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
|
|
|
590
661
|
if (opencodeHasQuestion) return tools;
|
|
591
662
|
return tools.filter((t) => t.name !== "question");
|
|
592
663
|
}
|
|
664
|
+
var TASK_INPUT_PROPERTIES = {
|
|
665
|
+
description: {
|
|
666
|
+
type: "string",
|
|
667
|
+
description: "A short (3-5 words) description of the task"
|
|
668
|
+
},
|
|
669
|
+
prompt: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "The task for the agent to perform"
|
|
672
|
+
},
|
|
673
|
+
subagent_type: {
|
|
674
|
+
type: "string",
|
|
675
|
+
description: "The type of specialized agent to use for this task"
|
|
676
|
+
},
|
|
677
|
+
task_id: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "Set this only if you mean to resume a previous task: pass the prior task_id to continue the same subagent session instead of creating a fresh one."
|
|
680
|
+
},
|
|
681
|
+
command: {
|
|
682
|
+
type: "string",
|
|
683
|
+
description: "The command that triggered this task"
|
|
684
|
+
},
|
|
685
|
+
background: {
|
|
686
|
+
type: "boolean",
|
|
687
|
+
description: "Run the task in the background when supported by opencode"
|
|
688
|
+
}
|
|
689
|
+
};
|
|
593
690
|
var DEFAULT_PROXY_TOOLS = [
|
|
594
691
|
{
|
|
595
692
|
name: "bash",
|
|
@@ -683,35 +780,30 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
683
780
|
{
|
|
684
781
|
name: "task",
|
|
685
782
|
description: "Launch an opencode subagent to handle a complex multi-step task autonomously. Routed through opencode's task tool so subagent orchestration, permission, and lifecycle are handled by opencode. Use `subagent_type` to pick which configured subagent runs (e.g. `build`, `general`, `explore`, or any custom subagent declared in opencode.json). " + TASK_PROXY_NOTE,
|
|
783
|
+
inputSchema: {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: TASK_INPUT_PROPERTIES,
|
|
786
|
+
required: TASK_INPUT_REQUIRED
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: TASK_BATCH_TOOL_NAME,
|
|
791
|
+
description: "Launch two or more independent opencode subagents at the same time and get all their results back together. Put one ordinary task input in `tasks` for each subagent. " + TASK_BATCH_PROXY_NOTE,
|
|
686
792
|
inputSchema: {
|
|
687
793
|
type: "object",
|
|
688
794
|
properties: {
|
|
689
|
-
|
|
690
|
-
type: "
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
type: "string",
|
|
699
|
-
description: "The type of specialized agent to use for this task"
|
|
700
|
-
},
|
|
701
|
-
task_id: {
|
|
702
|
-
type: "string",
|
|
703
|
-
description: "Set this only if you mean to resume a previous task \u2014 pass the prior task_id to continue the same subagent session instead of creating a fresh one."
|
|
704
|
-
},
|
|
705
|
-
command: {
|
|
706
|
-
type: "string",
|
|
707
|
-
description: "The command that triggered this task"
|
|
708
|
-
},
|
|
709
|
-
background: {
|
|
710
|
-
type: "boolean",
|
|
711
|
-
description: "Run the task in the background when supported by opencode"
|
|
795
|
+
tasks: {
|
|
796
|
+
type: "array",
|
|
797
|
+
minItems: 2,
|
|
798
|
+
description: "Independent subagent tasks to run concurrently",
|
|
799
|
+
items: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: TASK_INPUT_PROPERTIES,
|
|
802
|
+
required: TASK_INPUT_REQUIRED
|
|
803
|
+
}
|
|
712
804
|
}
|
|
713
805
|
},
|
|
714
|
-
required: ["
|
|
806
|
+
required: ["tasks"]
|
|
715
807
|
}
|
|
716
808
|
},
|
|
717
809
|
{
|
|
@@ -897,6 +989,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
897
989
|
});
|
|
898
990
|
return;
|
|
899
991
|
}
|
|
992
|
+
if (toolName === TASK_BATCH_TOOL_NAME) {
|
|
993
|
+
const problem = taskBatchInputError(input);
|
|
994
|
+
if (problem) {
|
|
995
|
+
writeToolCallResult(res, requestId, { kind: "error", message: problem });
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
900
999
|
const interceptor = interceptors?.get(toolName);
|
|
901
1000
|
if (interceptor) {
|
|
902
1001
|
let intercepted;
|
|
@@ -932,12 +1031,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
932
1031
|
});
|
|
933
1032
|
let timer = null;
|
|
934
1033
|
const result = await new Promise(
|
|
935
|
-
(
|
|
1034
|
+
(resolve5, reject2) => {
|
|
936
1035
|
const entry = {
|
|
937
1036
|
id: callId,
|
|
938
1037
|
toolName,
|
|
939
1038
|
input,
|
|
940
|
-
resolve:
|
|
1039
|
+
resolve: resolve5,
|
|
941
1040
|
reject: reject2,
|
|
942
1041
|
channel
|
|
943
1042
|
};
|
|
@@ -1019,11 +1118,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1019
1118
|
}
|
|
1020
1119
|
}
|
|
1021
1120
|
});
|
|
1022
|
-
await new Promise((
|
|
1121
|
+
await new Promise((resolve5, reject2) => {
|
|
1023
1122
|
server2.once("error", reject2);
|
|
1024
1123
|
server2.listen(0, "127.0.0.1", () => {
|
|
1025
1124
|
server2.off("error", reject2);
|
|
1026
|
-
|
|
1125
|
+
resolve5();
|
|
1027
1126
|
});
|
|
1028
1127
|
});
|
|
1029
1128
|
const addr = server2.address();
|
|
@@ -1077,8 +1176,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
1077
1176
|
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
1078
1177
|
}
|
|
1079
1178
|
pending.clear();
|
|
1080
|
-
await new Promise((
|
|
1081
|
-
server2.close(() =>
|
|
1179
|
+
await new Promise((resolve5) => {
|
|
1180
|
+
server2.close(() => resolve5());
|
|
1082
1181
|
});
|
|
1083
1182
|
if (configFilePath) {
|
|
1084
1183
|
try {
|
|
@@ -1101,6 +1200,7 @@ function disallowedToolFlags(tools) {
|
|
|
1101
1200
|
grep: ["Grep"],
|
|
1102
1201
|
webfetch: ["WebFetch"],
|
|
1103
1202
|
task: ["Agent"],
|
|
1203
|
+
task_batch: ["Agent"],
|
|
1104
1204
|
// `question` disables Claude Code's built-in `AskUserQuestion` so the
|
|
1105
1205
|
// structured-questions path flows through opencode's native `question`
|
|
1106
1206
|
// tool instead — same UI/permission/audit benefits as the other
|
|
@@ -1136,10 +1236,10 @@ function resolveDisallowedTools(options) {
|
|
|
1136
1236
|
return out;
|
|
1137
1237
|
}
|
|
1138
1238
|
function readBody(req) {
|
|
1139
|
-
return new Promise((
|
|
1239
|
+
return new Promise((resolve5, reject) => {
|
|
1140
1240
|
const chunks = [];
|
|
1141
1241
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
1142
|
-
req.on("end", () =>
|
|
1242
|
+
req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
|
|
1143
1243
|
req.on("error", reject);
|
|
1144
1244
|
});
|
|
1145
1245
|
}
|
|
@@ -1621,7 +1721,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
1621
1721
|
}
|
|
1622
1722
|
});
|
|
1623
1723
|
pendingProcesses.add(proc);
|
|
1624
|
-
return new Promise((
|
|
1724
|
+
return new Promise((resolve5, reject) => {
|
|
1625
1725
|
const event = `side-question:${requestId}`;
|
|
1626
1726
|
let settled = false;
|
|
1627
1727
|
let sent = false;
|
|
@@ -1676,7 +1776,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
1676
1776
|
}
|
|
1677
1777
|
settled = true;
|
|
1678
1778
|
cleanup();
|
|
1679
|
-
|
|
1779
|
+
resolve5({ response: result.response, synthetic: result.synthetic });
|
|
1680
1780
|
};
|
|
1681
1781
|
const timer = setTimeout(() => {
|
|
1682
1782
|
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
@@ -1731,6 +1831,8 @@ function takeUnattendedLines(ap) {
|
|
|
1731
1831
|
}
|
|
1732
1832
|
var activeProcesses = /* @__PURE__ */ new Map();
|
|
1733
1833
|
var claudeSessions = /* @__PURE__ */ new Map();
|
|
1834
|
+
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
1835
|
+
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
1734
1836
|
var MAX_ACTIVE_PROCESSES = 16;
|
|
1735
1837
|
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
1736
1838
|
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
@@ -1778,15 +1880,107 @@ function evictIfNeeded() {
|
|
|
1778
1880
|
deleteActiveProcess(oldestKey);
|
|
1779
1881
|
}
|
|
1780
1882
|
}
|
|
1883
|
+
var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
|
|
1884
|
+
function isTerminalResultLine(line) {
|
|
1885
|
+
if (!line.includes('"result"')) return false;
|
|
1886
|
+
try {
|
|
1887
|
+
return JSON.parse(line).type === "result";
|
|
1888
|
+
} catch {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
function settleTurn(ap) {
|
|
1893
|
+
ap.turnInFlight = false;
|
|
1894
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
1895
|
+
ap.turnIdleWaiters = [];
|
|
1896
|
+
for (const wake of waiters) wake();
|
|
1897
|
+
}
|
|
1898
|
+
function noteTurnStarted(ap) {
|
|
1899
|
+
if (ap.asideTransport?.interactive) return;
|
|
1900
|
+
ap.turnInFlight = true;
|
|
1901
|
+
}
|
|
1902
|
+
function noteTurnLine(ap, line) {
|
|
1903
|
+
if (!ap.turnInFlight) return;
|
|
1904
|
+
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
1905
|
+
}
|
|
1906
|
+
function isTurnInFlight(ap) {
|
|
1907
|
+
return ap.turnInFlight === true;
|
|
1908
|
+
}
|
|
1909
|
+
function awaitTurnIdle(ap, timeoutMs) {
|
|
1910
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
1911
|
+
return new Promise((resolve5) => {
|
|
1912
|
+
const wake = () => {
|
|
1913
|
+
clearTimeout(timer);
|
|
1914
|
+
resolve5(true);
|
|
1915
|
+
};
|
|
1916
|
+
const timer = setTimeout(() => {
|
|
1917
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
1918
|
+
const at = waiters.indexOf(wake);
|
|
1919
|
+
if (at >= 0) waiters.splice(at, 1);
|
|
1920
|
+
resolve5(false);
|
|
1921
|
+
}, timeoutMs);
|
|
1922
|
+
(ap.turnIdleWaiters ??= []).push(wake);
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
1926
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
1927
|
+
const stdin = ap.proc.stdin;
|
|
1928
|
+
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
1929
|
+
log.notice("cannot interrupt this transport; waiting for the turn to end");
|
|
1930
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
1931
|
+
}
|
|
1932
|
+
try {
|
|
1933
|
+
stdin.write(
|
|
1934
|
+
JSON.stringify({
|
|
1935
|
+
type: "control_request",
|
|
1936
|
+
request_id: randomUUID3(),
|
|
1937
|
+
request: { subtype: "interrupt" }
|
|
1938
|
+
}) + "\n"
|
|
1939
|
+
);
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
log.warn("failed to write interrupt control request", {
|
|
1942
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1943
|
+
});
|
|
1944
|
+
return Promise.resolve(false);
|
|
1945
|
+
}
|
|
1946
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
1947
|
+
}
|
|
1948
|
+
function cancelIdleProcessEviction(key) {
|
|
1949
|
+
const timer = idleEvictionTimers.get(key);
|
|
1950
|
+
if (!timer) return;
|
|
1951
|
+
clearTimeout(timer);
|
|
1952
|
+
idleEvictionTimers.delete(key);
|
|
1953
|
+
}
|
|
1781
1954
|
function getActiveProcess(key) {
|
|
1782
1955
|
const ap = activeProcesses.get(key);
|
|
1783
|
-
if (ap)
|
|
1956
|
+
if (ap) {
|
|
1957
|
+
cancelIdleProcessEviction(key);
|
|
1958
|
+
touch(key);
|
|
1959
|
+
}
|
|
1784
1960
|
return ap;
|
|
1785
1961
|
}
|
|
1786
1962
|
function setActiveProcess(key, ap) {
|
|
1963
|
+
cancelIdleProcessEviction(key);
|
|
1787
1964
|
activeProcesses.set(key, ap);
|
|
1788
1965
|
}
|
|
1966
|
+
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
1967
|
+
cancelIdleProcessEviction(key);
|
|
1968
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
const scheduledProcess = activeProcesses.get(key);
|
|
1972
|
+
if (!scheduledProcess) return;
|
|
1973
|
+
const timer = setTimeout(() => {
|
|
1974
|
+
idleEvictionTimers.delete(key);
|
|
1975
|
+
if (activeProcesses.get(key) !== scheduledProcess) return;
|
|
1976
|
+
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
1977
|
+
deleteActiveProcess(key);
|
|
1978
|
+
}, timeoutMs);
|
|
1979
|
+
timer.unref();
|
|
1980
|
+
idleEvictionTimers.set(key, timer);
|
|
1981
|
+
}
|
|
1789
1982
|
function detachActiveProcess(key) {
|
|
1983
|
+
cancelIdleProcessEviction(key);
|
|
1790
1984
|
const ap = activeProcesses.get(key);
|
|
1791
1985
|
if (!ap) return void 0;
|
|
1792
1986
|
activeProcesses.delete(key);
|
|
@@ -1802,14 +1996,14 @@ function hasProcessExited(proc) {
|
|
|
1802
1996
|
}
|
|
1803
1997
|
function waitForProcessExit(proc, timeoutMs) {
|
|
1804
1998
|
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
1805
|
-
return new Promise((
|
|
1999
|
+
return new Promise((resolve5) => {
|
|
1806
2000
|
const onExit = () => {
|
|
1807
2001
|
clearTimeout(timer);
|
|
1808
|
-
|
|
2002
|
+
resolve5(true);
|
|
1809
2003
|
};
|
|
1810
2004
|
const timer = setTimeout(() => {
|
|
1811
2005
|
proc.off("exit", onExit);
|
|
1812
|
-
|
|
2006
|
+
resolve5(hasProcessExited(proc));
|
|
1813
2007
|
}, timeoutMs);
|
|
1814
2008
|
proc.once("exit", onExit);
|
|
1815
2009
|
});
|
|
@@ -1905,6 +2099,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1905
2099
|
const rl = createInterface({ input: proc.stdout });
|
|
1906
2100
|
rl.on("line", (line) => {
|
|
1907
2101
|
if (dispatchSideQuestionResponse(ap, line)) return;
|
|
2102
|
+
noteTurnLine(ap, line);
|
|
1908
2103
|
if (lineEmitter.listenerCount("line") === 0) {
|
|
1909
2104
|
bufferUnattendedLine(ap, line);
|
|
1910
2105
|
return;
|
|
@@ -1912,8 +2107,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1912
2107
|
lineEmitter.emit("line", line);
|
|
1913
2108
|
});
|
|
1914
2109
|
rl.on("close", () => {
|
|
2110
|
+
settleTurn(ap);
|
|
1915
2111
|
lineEmitter.emit("close");
|
|
1916
2112
|
});
|
|
2113
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
1917
2114
|
activeProcesses.set(sessionKey2, ap);
|
|
1918
2115
|
proc.on("error", (err) => {
|
|
1919
2116
|
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
@@ -1926,7 +2123,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
1926
2123
|
});
|
|
1927
2124
|
}
|
|
1928
2125
|
const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
|
|
1929
|
-
if (ownsSessionKey)
|
|
2126
|
+
if (ownsSessionKey) {
|
|
2127
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
2128
|
+
activeProcesses.delete(sessionKey2);
|
|
2129
|
+
}
|
|
1930
2130
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
1931
2131
|
log.info("process exited with error, clearing session", {
|
|
1932
2132
|
code,
|
|
@@ -1997,6 +2197,7 @@ function buildCliArgs(opts) {
|
|
|
1997
2197
|
strictMcpConfig,
|
|
1998
2198
|
disallowedTools,
|
|
1999
2199
|
appendSystemPromptFile,
|
|
2200
|
+
pluginDirs,
|
|
2000
2201
|
thinking,
|
|
2001
2202
|
thinkingDisplay,
|
|
2002
2203
|
fastMode,
|
|
@@ -2045,6 +2246,9 @@ function buildCliArgs(opts) {
|
|
|
2045
2246
|
if (appendSystemPromptFile) {
|
|
2046
2247
|
args.push("--append-system-prompt-file", appendSystemPromptFile);
|
|
2047
2248
|
}
|
|
2249
|
+
for (const dir of pluginDirs ?? []) {
|
|
2250
|
+
args.push("--plugin-dir", dir);
|
|
2251
|
+
}
|
|
2048
2252
|
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
2049
2253
|
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
2050
2254
|
}
|
|
@@ -2169,7 +2373,7 @@ async function waitForSessionIdle(client, sessionID, options = {}) {
|
|
|
2169
2373
|
if (options.stop?.()) return true;
|
|
2170
2374
|
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
2171
2375
|
if (Date.now() - started >= timeoutMs) return false;
|
|
2172
|
-
await new Promise((
|
|
2376
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2173
2377
|
}
|
|
2174
2378
|
}
|
|
2175
2379
|
async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
@@ -2180,7 +2384,7 @@ async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
|
2180
2384
|
if (emitAsideInline(sessionID, text)) return true;
|
|
2181
2385
|
if (Date.now() - started >= timeoutMs) return false;
|
|
2182
2386
|
if (await sessionStatus(client, sessionID) !== "busy") return false;
|
|
2183
|
-
await new Promise((
|
|
2387
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2184
2388
|
}
|
|
2185
2389
|
}
|
|
2186
2390
|
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
@@ -2201,7 +2405,7 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
|
2201
2405
|
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
2202
2406
|
return void 0;
|
|
2203
2407
|
}
|
|
2204
|
-
await new Promise((
|
|
2408
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2205
2409
|
}
|
|
2206
2410
|
}
|
|
2207
2411
|
async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
@@ -2213,7 +2417,7 @@ async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
|
2213
2417
|
if (status === "busy") return true;
|
|
2214
2418
|
if (status === "unknown") return isProcessBusy(active);
|
|
2215
2419
|
if (Date.now() - started >= settleMs) return false;
|
|
2216
|
-
await new Promise((
|
|
2420
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
2217
2421
|
}
|
|
2218
2422
|
}
|
|
2219
2423
|
function errorText(error) {
|
|
@@ -2260,10 +2464,10 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
2260
2464
|
let inlineDone = false;
|
|
2261
2465
|
let markInlineDelivered = () => {
|
|
2262
2466
|
};
|
|
2263
|
-
const inlineDelivered = new Promise((
|
|
2467
|
+
const inlineDelivered = new Promise((resolve5) => {
|
|
2264
2468
|
markInlineDelivered = () => {
|
|
2265
2469
|
inlineDone = true;
|
|
2266
|
-
|
|
2470
|
+
resolve5("inline");
|
|
2267
2471
|
};
|
|
2268
2472
|
});
|
|
2269
2473
|
if (isSideQuestionPending(active)) {
|
|
@@ -3093,34 +3297,166 @@ function agentDirectories(home, projectDirectory) {
|
|
|
3093
3297
|
return directories;
|
|
3094
3298
|
}
|
|
3095
3299
|
|
|
3096
|
-
// src/
|
|
3300
|
+
// src/skill-bridge.ts
|
|
3301
|
+
import * as crypto2 from "crypto";
|
|
3097
3302
|
import * as fs3 from "fs";
|
|
3098
|
-
import * as path4 from "path";
|
|
3099
3303
|
import * as os2 from "os";
|
|
3100
|
-
import * as
|
|
3304
|
+
import * as path4 from "path";
|
|
3305
|
+
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
3306
|
+
function dirExists(p) {
|
|
3307
|
+
try {
|
|
3308
|
+
return fs3.statSync(p).isDirectory();
|
|
3309
|
+
} catch {
|
|
3310
|
+
return false;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
function fileExists(p) {
|
|
3314
|
+
try {
|
|
3315
|
+
return fs3.statSync(p).isFile();
|
|
3316
|
+
} catch {
|
|
3317
|
+
return false;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function skillRoots(cwd) {
|
|
3321
|
+
const roots = [];
|
|
3322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3323
|
+
const push = (p) => {
|
|
3324
|
+
const abs = path4.resolve(p);
|
|
3325
|
+
if (seen.has(abs)) return;
|
|
3326
|
+
seen.add(abs);
|
|
3327
|
+
if (dirExists(abs)) roots.push(abs);
|
|
3328
|
+
};
|
|
3329
|
+
let current = path4.resolve(cwd);
|
|
3330
|
+
while (true) {
|
|
3331
|
+
push(path4.join(current, ".opencode", "skills"));
|
|
3332
|
+
const parent = path4.dirname(current);
|
|
3333
|
+
if (parent === current) break;
|
|
3334
|
+
current = parent;
|
|
3335
|
+
}
|
|
3336
|
+
const home = os2.homedir();
|
|
3337
|
+
if (home) push(path4.join(home, ".opencode", "skills"));
|
|
3338
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3339
|
+
if (envDir) push(path4.join(envDir, "skills"));
|
|
3340
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
|
|
3341
|
+
if (xdg) push(path4.join(xdg, "opencode", "skills"));
|
|
3342
|
+
return roots;
|
|
3343
|
+
}
|
|
3344
|
+
function discoverOpencodeSkills(cwd) {
|
|
3345
|
+
const found = [];
|
|
3346
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3347
|
+
for (const root of skillRoots(cwd)) {
|
|
3348
|
+
let entries;
|
|
3349
|
+
try {
|
|
3350
|
+
entries = fs3.readdirSync(root, { withFileTypes: true });
|
|
3351
|
+
} catch {
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
for (const entry of entries) {
|
|
3355
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3356
|
+
const name = entry.name;
|
|
3357
|
+
if (name.startsWith(".")) continue;
|
|
3358
|
+
if (claimed.has(name)) continue;
|
|
3359
|
+
const dir = path4.join(root, name);
|
|
3360
|
+
if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
|
|
3361
|
+
claimed.add(name);
|
|
3362
|
+
found.push({ name, dir });
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
return found.sort((a, b) => a.name.localeCompare(b.name));
|
|
3366
|
+
}
|
|
3367
|
+
function linkSkill(source, target) {
|
|
3368
|
+
try {
|
|
3369
|
+
fs3.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
3370
|
+
return;
|
|
3371
|
+
} catch {
|
|
3372
|
+
fs3.cpSync(source, target, { recursive: true, dereference: true });
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
function buildSkillPluginDir(skills) {
|
|
3376
|
+
if (skills.length === 0) return null;
|
|
3377
|
+
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
3378
|
+
const hash = crypto2.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
3379
|
+
const root = path4.join(pluginTmpDir(), `skills-${hash}`);
|
|
3380
|
+
const manifest = path4.join(root, ".claude-plugin", "plugin.json");
|
|
3381
|
+
if (fileExists(manifest)) return root;
|
|
3382
|
+
try {
|
|
3383
|
+
fs3.rmSync(root, { recursive: true, force: true });
|
|
3384
|
+
fs3.mkdirSync(path4.join(root, ".claude-plugin"), { recursive: true });
|
|
3385
|
+
fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
|
|
3386
|
+
fs3.writeFileSync(
|
|
3387
|
+
manifest,
|
|
3388
|
+
JSON.stringify(
|
|
3389
|
+
{
|
|
3390
|
+
name: SKILL_PLUGIN_NAME,
|
|
3391
|
+
description: "Skills discovered from this opencode installation, bridged into Claude Code."
|
|
3392
|
+
},
|
|
3393
|
+
null,
|
|
3394
|
+
2
|
|
3395
|
+
),
|
|
3396
|
+
{ encoding: "utf8", mode: 384 }
|
|
3397
|
+
);
|
|
3398
|
+
for (const skill of skills) {
|
|
3399
|
+
linkSkill(skill.dir, path4.join(root, "skills", skill.name));
|
|
3400
|
+
}
|
|
3401
|
+
} catch (err) {
|
|
3402
|
+
log.warn("failed to stage opencode skill plugin dir", {
|
|
3403
|
+
root,
|
|
3404
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3405
|
+
});
|
|
3406
|
+
return null;
|
|
3407
|
+
}
|
|
3408
|
+
return root;
|
|
3409
|
+
}
|
|
3410
|
+
async function resolveSkillPluginDirs(opts) {
|
|
3411
|
+
if (!opts.enabled) return [];
|
|
3412
|
+
const skills = discoverOpencodeSkills(opts.cwd);
|
|
3413
|
+
if (skills.length === 0) return [];
|
|
3414
|
+
const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
|
|
3415
|
+
if (!supported) {
|
|
3416
|
+
log.notice(
|
|
3417
|
+
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
3418
|
+
{ skills: skills.length }
|
|
3419
|
+
);
|
|
3420
|
+
return [];
|
|
3421
|
+
}
|
|
3422
|
+
const dir = buildSkillPluginDir(skills);
|
|
3423
|
+
if (!dir) return [];
|
|
3424
|
+
log.info("bridged opencode skills into claude", {
|
|
3425
|
+
count: skills.length,
|
|
3426
|
+
names: skills.map((s) => s.name),
|
|
3427
|
+
pluginDir: dir
|
|
3428
|
+
});
|
|
3429
|
+
return [dir];
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
// src/mcp-bridge.ts
|
|
3433
|
+
import * as fs4 from "fs";
|
|
3434
|
+
import * as path5 from "path";
|
|
3435
|
+
import * as os3 from "os";
|
|
3436
|
+
import * as crypto3 from "crypto";
|
|
3101
3437
|
import {
|
|
3102
3438
|
parse as parseJsonc,
|
|
3103
3439
|
printParseErrorCode
|
|
3104
3440
|
} from "jsonc-parser";
|
|
3105
3441
|
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
3106
3442
|
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
3107
|
-
function
|
|
3443
|
+
function fileExists2(p) {
|
|
3108
3444
|
try {
|
|
3109
|
-
return
|
|
3445
|
+
return fs4.statSync(p).isFile();
|
|
3110
3446
|
} catch {
|
|
3111
3447
|
return false;
|
|
3112
3448
|
}
|
|
3113
3449
|
}
|
|
3114
|
-
function
|
|
3450
|
+
function dirExists2(p) {
|
|
3115
3451
|
try {
|
|
3116
|
-
return
|
|
3452
|
+
return fs4.statSync(p).isDirectory();
|
|
3117
3453
|
} catch {
|
|
3118
3454
|
return false;
|
|
3119
3455
|
}
|
|
3120
3456
|
}
|
|
3121
3457
|
function readAndParse(file) {
|
|
3122
3458
|
try {
|
|
3123
|
-
const raw =
|
|
3459
|
+
const raw = fs4.readFileSync(file, "utf8");
|
|
3124
3460
|
const errors = [];
|
|
3125
3461
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3126
3462
|
if (errors.length > 0) {
|
|
@@ -3156,14 +3492,14 @@ function deepMerge(target, source) {
|
|
|
3156
3492
|
}
|
|
3157
3493
|
function walkUp(opts) {
|
|
3158
3494
|
const out = [];
|
|
3159
|
-
let current =
|
|
3495
|
+
let current = path5.resolve(opts.start);
|
|
3160
3496
|
while (true) {
|
|
3161
3497
|
for (const target of opts.targets) {
|
|
3162
|
-
const candidate =
|
|
3498
|
+
const candidate = path5.join(current, target);
|
|
3163
3499
|
if (opts.predicate(candidate)) out.push(candidate);
|
|
3164
3500
|
}
|
|
3165
|
-
if (opts.stop && current ===
|
|
3166
|
-
const parent =
|
|
3501
|
+
if (opts.stop && current === path5.resolve(opts.stop)) break;
|
|
3502
|
+
const parent = path5.dirname(current);
|
|
3167
3503
|
if (parent === current) break;
|
|
3168
3504
|
current = parent;
|
|
3169
3505
|
}
|
|
@@ -3171,29 +3507,29 @@ function walkUp(opts) {
|
|
|
3171
3507
|
}
|
|
3172
3508
|
function detectWorktree(cwd) {
|
|
3173
3509
|
const override = process.env.OPENCODE_WORKTREE;
|
|
3174
|
-
if (override) return
|
|
3175
|
-
let current =
|
|
3510
|
+
if (override) return path5.resolve(override);
|
|
3511
|
+
let current = path5.resolve(cwd);
|
|
3176
3512
|
while (true) {
|
|
3177
|
-
const gitPath =
|
|
3513
|
+
const gitPath = path5.join(current, ".git");
|
|
3178
3514
|
try {
|
|
3179
|
-
if (
|
|
3515
|
+
if (fs4.existsSync(gitPath)) return current;
|
|
3180
3516
|
} catch {
|
|
3181
3517
|
}
|
|
3182
|
-
const parent =
|
|
3518
|
+
const parent = path5.dirname(current);
|
|
3183
3519
|
if (parent === current) return void 0;
|
|
3184
3520
|
current = parent;
|
|
3185
3521
|
}
|
|
3186
3522
|
}
|
|
3187
3523
|
function globalConfigDir() {
|
|
3188
|
-
const xdg = process.env.XDG_CONFIG_HOME ??
|
|
3189
|
-
return
|
|
3524
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
|
|
3525
|
+
return path5.join(xdg, "opencode");
|
|
3190
3526
|
}
|
|
3191
3527
|
function loadGlobalConfig() {
|
|
3192
3528
|
const dir = globalConfigDir();
|
|
3193
3529
|
let merged = {};
|
|
3194
3530
|
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3195
|
-
const file =
|
|
3196
|
-
if (!
|
|
3531
|
+
const file = path5.join(dir, name);
|
|
3532
|
+
if (!fileExists2(file)) continue;
|
|
3197
3533
|
const parsed = readAndParse(file);
|
|
3198
3534
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
3199
3535
|
}
|
|
@@ -3202,8 +3538,8 @@ function loadGlobalConfig() {
|
|
|
3202
3538
|
function loadProjectFilesInDir(dir) {
|
|
3203
3539
|
let merged = {};
|
|
3204
3540
|
for (const name of PROJECT_FILE_NAMES) {
|
|
3205
|
-
const file =
|
|
3206
|
-
if (!
|
|
3541
|
+
const file = path5.join(dir, name);
|
|
3542
|
+
if (!fileExists2(file)) continue;
|
|
3207
3543
|
const parsed = readAndParse(file);
|
|
3208
3544
|
if (parsed) merged = deepMerge(merged, parsed);
|
|
3209
3545
|
}
|
|
@@ -3213,8 +3549,8 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3213
3549
|
const dirs = [];
|
|
3214
3550
|
const seen = /* @__PURE__ */ new Set();
|
|
3215
3551
|
const push = (p) => {
|
|
3216
|
-
const abs =
|
|
3217
|
-
if (!seen.has(abs) &&
|
|
3552
|
+
const abs = path5.resolve(p);
|
|
3553
|
+
if (!seen.has(abs) && dirExists2(abs)) {
|
|
3218
3554
|
seen.add(abs);
|
|
3219
3555
|
dirs.push(abs);
|
|
3220
3556
|
}
|
|
@@ -3223,17 +3559,17 @@ function dotOpencodeDirs(cwd, worktree) {
|
|
|
3223
3559
|
start: cwd,
|
|
3224
3560
|
stop: worktree,
|
|
3225
3561
|
targets: [".opencode"],
|
|
3226
|
-
predicate:
|
|
3562
|
+
predicate: dirExists2
|
|
3227
3563
|
})) {
|
|
3228
3564
|
push(dir);
|
|
3229
3565
|
}
|
|
3230
|
-
const home =
|
|
3566
|
+
const home = os3.homedir();
|
|
3231
3567
|
if (home) {
|
|
3232
|
-
const homeDot =
|
|
3233
|
-
if (
|
|
3568
|
+
const homeDot = path5.join(home, ".opencode");
|
|
3569
|
+
if (dirExists2(homeDot)) push(homeDot);
|
|
3234
3570
|
}
|
|
3235
3571
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3236
|
-
if (envDir &&
|
|
3572
|
+
if (envDir && dirExists2(envDir)) push(envDir);
|
|
3237
3573
|
return dirs;
|
|
3238
3574
|
}
|
|
3239
3575
|
function substituteEnvPlaceholders(source) {
|
|
@@ -3341,7 +3677,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3341
3677
|
let merged = {};
|
|
3342
3678
|
merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
|
|
3343
3679
|
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3344
|
-
if (explicitConfig &&
|
|
3680
|
+
if (explicitConfig && fileExists2(explicitConfig)) {
|
|
3345
3681
|
const parsed = readAndParse(explicitConfig);
|
|
3346
3682
|
if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
|
|
3347
3683
|
}
|
|
@@ -3349,12 +3685,12 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3349
3685
|
start: cwd,
|
|
3350
3686
|
stop: worktree,
|
|
3351
3687
|
targets: PROJECT_FILE_NAMES,
|
|
3352
|
-
predicate:
|
|
3688
|
+
predicate: fileExists2
|
|
3353
3689
|
});
|
|
3354
3690
|
const projectDirs = [];
|
|
3355
3691
|
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3356
3692
|
for (const f of projectFiles) {
|
|
3357
|
-
const d =
|
|
3693
|
+
const d = path5.dirname(f);
|
|
3358
3694
|
if (!seenProjectDirs.has(d)) {
|
|
3359
3695
|
seenProjectDirs.add(d);
|
|
3360
3696
|
projectDirs.push(d);
|
|
@@ -3383,7 +3719,7 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3383
3719
|
enabledServerNames.push(name);
|
|
3384
3720
|
}
|
|
3385
3721
|
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3386
|
-
const hash =
|
|
3722
|
+
const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3387
3723
|
return { servers: merged, enabledServerNames, hash };
|
|
3388
3724
|
}
|
|
3389
3725
|
function finishBridge(input) {
|
|
@@ -3399,13 +3735,13 @@ function finishBridge(input) {
|
|
|
3399
3735
|
};
|
|
3400
3736
|
}
|
|
3401
3737
|
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3402
|
-
const outPath =
|
|
3738
|
+
const outPath = path5.join(
|
|
3403
3739
|
pluginTmpDir(),
|
|
3404
3740
|
`mcp-${hash}.json`
|
|
3405
3741
|
);
|
|
3406
3742
|
try {
|
|
3407
|
-
if (!
|
|
3408
|
-
|
|
3743
|
+
if (!fileExists2(outPath)) {
|
|
3744
|
+
fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3409
3745
|
}
|
|
3410
3746
|
} catch (e) {
|
|
3411
3747
|
log.warn("failed to write bridged MCP config", {
|
|
@@ -3447,17 +3783,39 @@ function getOpencodeProjectDirectory() {
|
|
|
3447
3783
|
function isUsableDirectory(d) {
|
|
3448
3784
|
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3449
3785
|
}
|
|
3450
|
-
function
|
|
3786
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
3787
|
+
if (configured) return configured;
|
|
3788
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
3789
|
+
if (isUsableDirectory(live)) return live;
|
|
3790
|
+
return captured ?? live;
|
|
3791
|
+
}
|
|
3792
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
3793
|
+
if (configured) return configured;
|
|
3794
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
3451
3795
|
return resolveSpawnCwdFrom(
|
|
3452
3796
|
configured,
|
|
3453
3797
|
process.cwd(),
|
|
3454
|
-
opencodeProjectDirectory
|
|
3798
|
+
opencodeProjectDirectory,
|
|
3799
|
+
sessionDir
|
|
3455
3800
|
);
|
|
3456
3801
|
}
|
|
3457
|
-
function
|
|
3458
|
-
if (
|
|
3459
|
-
|
|
3460
|
-
return
|
|
3802
|
+
async function fetchSessionDirectory(sessionID) {
|
|
3803
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3804
|
+
const client = opencodeClient;
|
|
3805
|
+
if (!client?.session?.get) return void 0;
|
|
3806
|
+
try {
|
|
3807
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3808
|
+
const data = res.data;
|
|
3809
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3810
|
+
const dir = data.directory;
|
|
3811
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
3812
|
+
} catch (err) {
|
|
3813
|
+
log.warn("failed to fetch opencode session directory", {
|
|
3814
|
+
sessionID,
|
|
3815
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3816
|
+
});
|
|
3817
|
+
return void 0;
|
|
3818
|
+
}
|
|
3461
3819
|
}
|
|
3462
3820
|
async function getRuntimeMcpStatus() {
|
|
3463
3821
|
const client = opencodeClient;
|
|
@@ -3516,40 +3874,40 @@ import { EventEmitter as EventEmitter4 } from "events";
|
|
|
3516
3874
|
import { unlink as unlink2 } from "fs/promises";
|
|
3517
3875
|
|
|
3518
3876
|
// src/claude-session-bun.ts
|
|
3519
|
-
import * as
|
|
3520
|
-
import * as
|
|
3521
|
-
import * as
|
|
3877
|
+
import * as os4 from "os";
|
|
3878
|
+
import * as fs5 from "fs";
|
|
3879
|
+
import * as path6 from "path";
|
|
3522
3880
|
import { execFileSync } from "child_process";
|
|
3523
|
-
import { randomUUID as
|
|
3881
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3524
3882
|
function resolveClaude(cmd = "claude") {
|
|
3525
|
-
if (
|
|
3883
|
+
if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
|
|
3526
3884
|
const viaBun = Bun.which(cmd);
|
|
3527
3885
|
if (viaBun) return viaBun;
|
|
3528
|
-
const isWin =
|
|
3886
|
+
const isWin = os4.platform() === "win32";
|
|
3529
3887
|
try {
|
|
3530
3888
|
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
3531
3889
|
encoding: "utf8",
|
|
3532
3890
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3533
3891
|
});
|
|
3534
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) =>
|
|
3892
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs5.existsSync(p));
|
|
3535
3893
|
if (first) return first;
|
|
3536
3894
|
} catch {
|
|
3537
3895
|
}
|
|
3538
3896
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
3539
3897
|
}
|
|
3540
3898
|
function encodeCwd(cwd) {
|
|
3541
|
-
return
|
|
3899
|
+
return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
|
|
3542
3900
|
}
|
|
3543
3901
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
3544
3902
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3545
3903
|
function resolveConfigDir(configDir) {
|
|
3546
3904
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
3547
|
-
if (!value) return
|
|
3548
|
-
if (value === "~") return
|
|
3905
|
+
if (!value) return path6.join(os4.homedir(), ".claude");
|
|
3906
|
+
if (value === "~") return os4.homedir();
|
|
3549
3907
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
3550
|
-
return
|
|
3908
|
+
return path6.join(os4.homedir(), value.slice(2));
|
|
3551
3909
|
}
|
|
3552
|
-
return
|
|
3910
|
+
return path6.resolve(value);
|
|
3553
3911
|
}
|
|
3554
3912
|
var ClaudeSession = class {
|
|
3555
3913
|
sessionId;
|
|
@@ -3567,11 +3925,11 @@ var ClaudeSession = class {
|
|
|
3567
3925
|
signal;
|
|
3568
3926
|
o;
|
|
3569
3927
|
constructor(opts = {}) {
|
|
3570
|
-
this.cwd =
|
|
3928
|
+
this.cwd = path6.resolve(opts.cwd ?? process.cwd());
|
|
3571
3929
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
3572
3930
|
this.signal = opts.signal;
|
|
3573
|
-
this.sessionId =
|
|
3574
|
-
this.jsonlPath =
|
|
3931
|
+
this.sessionId = randomUUID4();
|
|
3932
|
+
this.jsonlPath = path6.join(
|
|
3575
3933
|
this.configDir,
|
|
3576
3934
|
"projects",
|
|
3577
3935
|
encodeCwd(this.cwd),
|
|
@@ -3694,7 +4052,7 @@ var ClaudeSession = class {
|
|
|
3694
4052
|
}
|
|
3695
4053
|
readRawLines() {
|
|
3696
4054
|
try {
|
|
3697
|
-
return
|
|
4055
|
+
return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
|
|
3698
4056
|
} catch {
|
|
3699
4057
|
return [];
|
|
3700
4058
|
}
|
|
@@ -4072,11 +4430,11 @@ function spawnInteractiveProcess(opts) {
|
|
|
4072
4430
|
}
|
|
4073
4431
|
|
|
4074
4432
|
// src/claude-code-language-model.ts
|
|
4075
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
4433
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4076
4434
|
import { unlink as unlink3 } from "fs/promises";
|
|
4077
|
-
import { homedir as
|
|
4078
|
-
import { randomUUID as
|
|
4079
|
-
import { dirname as
|
|
4435
|
+
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
4436
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
4437
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
4080
4438
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
4081
4439
|
function resolveCompactionModel(configured) {
|
|
4082
4440
|
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
@@ -4283,9 +4641,9 @@ ${body}`;
|
|
|
4283
4641
|
}
|
|
4284
4642
|
});
|
|
4285
4643
|
}
|
|
4286
|
-
function readPromptFileIfPresent(
|
|
4644
|
+
function readPromptFileIfPresent(path9) {
|
|
4287
4645
|
try {
|
|
4288
|
-
const content = readFileSync3(
|
|
4646
|
+
const content = readFileSync3(path9, "utf8").trim();
|
|
4289
4647
|
return content || void 0;
|
|
4290
4648
|
} catch {
|
|
4291
4649
|
return void 0;
|
|
@@ -4294,9 +4652,9 @@ function readPromptFileIfPresent(path8) {
|
|
|
4294
4652
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
4295
4653
|
let dir = cwd;
|
|
4296
4654
|
while (true) {
|
|
4297
|
-
const content = readPromptFileIfPresent(
|
|
4655
|
+
const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
|
|
4298
4656
|
if (content) return content;
|
|
4299
|
-
const parent =
|
|
4657
|
+
const parent = dirname4(dir);
|
|
4300
4658
|
if (parent === dir) return void 0;
|
|
4301
4659
|
dir = parent;
|
|
4302
4660
|
}
|
|
@@ -4316,8 +4674,9 @@ blocker. The user can interrupt or abort at any time; turn endings should
|
|
|
4316
4674
|
mark meaningful checkpoints, not every completed substep.`;
|
|
4317
4675
|
var SUBAGENT_DISPATCH_HINT = `## opencode subagents
|
|
4318
4676
|
|
|
4319
|
-
Subagent dispatch in this environment goes through exactly
|
|
4677
|
+
Subagent dispatch in this environment goes through exactly two tools: \`mcp__opencode_proxy__task\` for one subagent and \`mcp__opencode_proxy__task_batch\` for two or more at once.
|
|
4320
4678
|
|
|
4679
|
+
- Two or more independent subagents in one response: make ONE \`mcp__opencode_proxy__task_batch\` call with a \`tasks\` array (each item is a normal task input). Claude Code runs MCP calls one at a time, so several \`mcp__opencode_proxy__task\` calls in the same response run serially; \`task_batch\` runs them concurrently in opencode and returns every result together, labelled in order.
|
|
4321
4680
|
- When the user mentions \`@<agent>\` or an instruction says "call the task tool with subagent: <name>", call \`mcp__opencode_proxy__task\` with \`subagent_type: "<name>"\`.
|
|
4322
4681
|
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__task\`), then call it.
|
|
4323
4682
|
- Claude Code's built-in TaskCreate/TaskUpdate/TaskList manage a local todo list. They cannot dispatch subagents; creating a task there runs nothing. Never report a subagent as dispatched unless \`mcp__opencode_proxy__task\` returned its result.
|
|
@@ -4379,19 +4738,22 @@ ${options.compressionSummary.trim()}`
|
|
|
4379
4738
|
for (const s of extraSystemContent) {
|
|
4380
4739
|
if (s.trim()) parts.push(s.trim());
|
|
4381
4740
|
}
|
|
4382
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
4383
|
-
const globalAgents = readPromptFileIfPresent(
|
|
4741
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
|
|
4742
|
+
const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
|
|
4384
4743
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4744
|
+
const forwarded = extraSystemContent.join("\n\n");
|
|
4745
|
+
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
4746
|
+
const pushWorkspace = !!workspaceAgents && workspaceAgents !== globalAgents && !forwarded.includes(workspaceAgents);
|
|
4747
|
+
if (pushGlobal) parts.push(globalAgents);
|
|
4748
|
+
if (pushWorkspace) parts.push(workspaceAgents);
|
|
4749
|
+
if (pushGlobal || pushWorkspace) parts.push(AGENTS_MAINTENANCE_HINT);
|
|
4388
4750
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
4389
4751
|
const content = parts.join("\n\n");
|
|
4390
4752
|
if (!content) return void 0;
|
|
4391
|
-
const
|
|
4753
|
+
const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
4392
4754
|
try {
|
|
4393
|
-
|
|
4394
|
-
return
|
|
4755
|
+
writeFileSync4(path9, content, "utf8");
|
|
4756
|
+
return path9;
|
|
4395
4757
|
} catch (err) {
|
|
4396
4758
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
4397
4759
|
return void 0;
|
|
@@ -4518,11 +4880,24 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4518
4880
|
DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
|
|
4519
4881
|
);
|
|
4520
4882
|
const picked = [];
|
|
4883
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4521
4884
|
const unknown = [];
|
|
4885
|
+
const pick = (def) => {
|
|
4886
|
+
if (seen.has(def.name)) return;
|
|
4887
|
+
seen.add(def.name);
|
|
4888
|
+
picked.push(def);
|
|
4889
|
+
};
|
|
4522
4890
|
for (const n of names) {
|
|
4523
4891
|
const def = defsByName.get(String(n).toLowerCase());
|
|
4524
|
-
if (def)
|
|
4525
|
-
|
|
4892
|
+
if (!def) {
|
|
4893
|
+
unknown.push(String(n));
|
|
4894
|
+
continue;
|
|
4895
|
+
}
|
|
4896
|
+
pick(def);
|
|
4897
|
+
if (def.name === "task") {
|
|
4898
|
+
const batch = defsByName.get(TASK_BATCH_TOOL_NAME);
|
|
4899
|
+
if (batch) pick(batch);
|
|
4900
|
+
}
|
|
4526
4901
|
}
|
|
4527
4902
|
if (unknown.length > 0) {
|
|
4528
4903
|
const known = [...defsByName.keys()].join(", ");
|
|
@@ -4712,6 +5087,41 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4712
5087
|
}
|
|
4713
5088
|
return null;
|
|
4714
5089
|
}
|
|
5090
|
+
/**
|
|
5091
|
+
* The result opencode produced for a pending proxy call, if the prompt
|
|
5092
|
+
* carries it. For `task_batch` that means every child's result gathered
|
|
5093
|
+
* back onto the parent: opencode runs the children in one step and hands
|
|
5094
|
+
* all their results to the next call together, so a partial set is not
|
|
5095
|
+
* expected. If it ever happens the batch still resolves, with the gap
|
|
5096
|
+
* named in the text, because leaving the parent pending would send this
|
|
5097
|
+
* turn down the fresh-envelope path and reject the call as orphaned.
|
|
5098
|
+
*/
|
|
5099
|
+
extractPendingProxyResultForCall(prompt, call) {
|
|
5100
|
+
if (call.toolName !== TASK_BATCH_TOOL_NAME) {
|
|
5101
|
+
return this.extractPendingProxyResult(prompt, call.toolCallId);
|
|
5102
|
+
}
|
|
5103
|
+
const tasks = taskBatchTasks(call.input);
|
|
5104
|
+
if (tasks.length === 0) {
|
|
5105
|
+
return { kind: "error", message: "task_batch input is not a list of task objects" };
|
|
5106
|
+
}
|
|
5107
|
+
const children = tasks.map((task, index) => ({
|
|
5108
|
+
task,
|
|
5109
|
+
result: this.extractPendingProxyResult(
|
|
5110
|
+
prompt,
|
|
5111
|
+
taskBatchChildToolCallId(call.toolCallId, index)
|
|
5112
|
+
)
|
|
5113
|
+
}));
|
|
5114
|
+
const answered = children.filter((child) => child.result !== null).length;
|
|
5115
|
+
if (answered === 0) return null;
|
|
5116
|
+
if (answered < children.length) {
|
|
5117
|
+
log.warn("task_batch resolving with child results missing", {
|
|
5118
|
+
toolCallId: call.toolCallId,
|
|
5119
|
+
answered,
|
|
5120
|
+
total: children.length
|
|
5121
|
+
});
|
|
5122
|
+
}
|
|
5123
|
+
return formatTaskBatchResults(children);
|
|
5124
|
+
}
|
|
4715
5125
|
/**
|
|
4716
5126
|
* Resolve the session affinity token for this LLM call. Delegates to the
|
|
4717
5127
|
* exported `resolveSessionAffinity` helper so the logic is unit-testable.
|
|
@@ -4972,9 +5382,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
4972
5382
|
return this.doGenerateViaStream(options);
|
|
4973
5383
|
}
|
|
4974
5384
|
const warnings = [];
|
|
4975
|
-
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
4976
5385
|
const scope = this.requestScope(options);
|
|
4977
5386
|
const affinity = this.sessionAffinity(options);
|
|
5387
|
+
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
4978
5388
|
const effectiveModelId = resolveAgentModel(
|
|
4979
5389
|
this.getOpencodeAgent(options.providerOptions),
|
|
4980
5390
|
this.modelId
|
|
@@ -5113,7 +5523,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
5113
5523
|
const toolCalls = [];
|
|
5114
5524
|
const toolCallStreams = /* @__PURE__ */ new Map();
|
|
5115
5525
|
let gotPartialEvents = false;
|
|
5116
|
-
const result = await new Promise((
|
|
5526
|
+
const result = await new Promise((resolve5, reject) => {
|
|
5117
5527
|
const cleanup = () => {
|
|
5118
5528
|
try {
|
|
5119
5529
|
if (!proc.killed && proc.exitCode === null) proc.kill();
|
|
@@ -5248,7 +5658,7 @@ ${plan}
|
|
|
5248
5658
|
usage: msg.usage
|
|
5249
5659
|
};
|
|
5250
5660
|
cleanup();
|
|
5251
|
-
|
|
5661
|
+
resolve5({
|
|
5252
5662
|
...resultMeta,
|
|
5253
5663
|
text: responseText,
|
|
5254
5664
|
thinking: thinkingText,
|
|
@@ -5260,7 +5670,7 @@ ${plan}
|
|
|
5260
5670
|
});
|
|
5261
5671
|
rl.on("close", () => {
|
|
5262
5672
|
cleanup();
|
|
5263
|
-
|
|
5673
|
+
resolve5({
|
|
5264
5674
|
...resultMeta,
|
|
5265
5675
|
text: responseText,
|
|
5266
5676
|
thinking: thinkingText,
|
|
@@ -5366,11 +5776,11 @@ ${plan}
|
|
|
5366
5776
|
}
|
|
5367
5777
|
async doStream(options) {
|
|
5368
5778
|
const warnings = [];
|
|
5369
|
-
const cwd = resolveSpawnCwd(this.config.cwd);
|
|
5370
5779
|
const cliPath = this.config.cliPath;
|
|
5371
5780
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
5372
5781
|
const scope = this.requestScope(options);
|
|
5373
5782
|
const affinity = this.sessionAffinity(options);
|
|
5783
|
+
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
5374
5784
|
const compactionMode = this.isCompactionCall(options);
|
|
5375
5785
|
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
|
|
5376
5786
|
this.getOpencodeAgent(options.providerOptions),
|
|
@@ -5523,7 +5933,7 @@ ${plan}
|
|
|
5523
5933
|
const self = this;
|
|
5524
5934
|
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
5525
5935
|
call,
|
|
5526
|
-
result: this.
|
|
5936
|
+
result: this.extractPendingProxyResultForCall(options.prompt, call)
|
|
5527
5937
|
}));
|
|
5528
5938
|
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
5529
5939
|
(m) => m.result !== null
|
|
@@ -5743,6 +6153,11 @@ ${plan}
|
|
|
5743
6153
|
compressionSummary: getCompressionSummary(sk)
|
|
5744
6154
|
}
|
|
5745
6155
|
);
|
|
6156
|
+
const skillPluginDirs = await resolveSkillPluginDirs({
|
|
6157
|
+
cwd,
|
|
6158
|
+
cliPath,
|
|
6159
|
+
enabled: self.config.bridgeOpencodeSkills === true
|
|
6160
|
+
});
|
|
5746
6161
|
cliArgs = buildCliArgs({
|
|
5747
6162
|
sessionKey: sk,
|
|
5748
6163
|
skipPermissions,
|
|
@@ -5752,6 +6167,7 @@ ${plan}
|
|
|
5752
6167
|
strictMcpConfig: self.config.strictMcpConfig,
|
|
5753
6168
|
disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
|
|
5754
6169
|
appendSystemPromptFile: systemPromptFile,
|
|
6170
|
+
pluginDirs: skillPluginDirs,
|
|
5755
6171
|
...self.thinkingCliOptions(),
|
|
5756
6172
|
fastMode,
|
|
5757
6173
|
cliVersion
|
|
@@ -5781,6 +6197,13 @@ ${plan}
|
|
|
5781
6197
|
activeProcess = ap;
|
|
5782
6198
|
}
|
|
5783
6199
|
}
|
|
6200
|
+
if (activeProcess && !hasMatchedPendingResults && isTurnInFlight(activeProcess)) {
|
|
6201
|
+
log.warn("previous turn still in flight; interrupting it", { sk });
|
|
6202
|
+
const idle = await interruptTurn(activeProcess);
|
|
6203
|
+
if (!idle) {
|
|
6204
|
+
log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
5784
6207
|
controller.enqueue({ type: "stream-start", warnings });
|
|
5785
6208
|
let currentTextId = null;
|
|
5786
6209
|
const textBlockIndices = /* @__PURE__ */ new Set();
|
|
@@ -5919,7 +6342,10 @@ ${plan}
|
|
|
5919
6342
|
lineEmitter.on("close", closeHandler);
|
|
5920
6343
|
proc.on("error", procErrorHandler);
|
|
5921
6344
|
try {
|
|
5922
|
-
if (!deliverPendingCompletions(true))
|
|
6345
|
+
if (!deliverPendingCompletions(true)) {
|
|
6346
|
+
noteTurnStarted(newAp);
|
|
6347
|
+
proc.stdin?.write(watchdogMessage + "\n");
|
|
6348
|
+
}
|
|
5923
6349
|
log.debug("re-sent user message after respawn", {
|
|
5924
6350
|
textLength: watchdogMessage.length
|
|
5925
6351
|
});
|
|
@@ -5969,20 +6395,34 @@ ${plan}
|
|
|
5969
6395
|
const finishWithToolCalls = (calls) => {
|
|
5970
6396
|
if (controllerClosed) return;
|
|
5971
6397
|
if (calls.length === 0) return;
|
|
5972
|
-
|
|
6398
|
+
const enqueueToolCall = (toolCallId, toolName, input) => {
|
|
5973
6399
|
controller.enqueue({
|
|
5974
6400
|
type: "tool-input-start",
|
|
5975
|
-
id:
|
|
5976
|
-
toolName
|
|
6401
|
+
id: toolCallId,
|
|
6402
|
+
toolName
|
|
5977
6403
|
});
|
|
5978
6404
|
controller.enqueue({
|
|
5979
6405
|
type: "tool-call",
|
|
5980
|
-
toolCallId
|
|
5981
|
-
toolName
|
|
5982
|
-
input: JSON.stringify(
|
|
6406
|
+
toolCallId,
|
|
6407
|
+
toolName,
|
|
6408
|
+
input: JSON.stringify(input),
|
|
5983
6409
|
providerExecuted: false
|
|
5984
6410
|
});
|
|
5985
|
-
skipResultForIds.add(
|
|
6411
|
+
skipResultForIds.add(toolCallId);
|
|
6412
|
+
};
|
|
6413
|
+
for (const call of calls) {
|
|
6414
|
+
if (call.toolName === TASK_BATCH_TOOL_NAME) {
|
|
6415
|
+
for (const [index, task] of taskBatchTasks(call.input).entries()) {
|
|
6416
|
+
enqueueToolCall(
|
|
6417
|
+
taskBatchChildToolCallId(call.toolCallId, index),
|
|
6418
|
+
"task",
|
|
6419
|
+
task
|
|
6420
|
+
);
|
|
6421
|
+
}
|
|
6422
|
+
skipResultForIds.add(call.toolCallId);
|
|
6423
|
+
} else {
|
|
6424
|
+
enqueueToolCall(call.toolCallId, call.toolName, call.input);
|
|
6425
|
+
}
|
|
5986
6426
|
markPendingProxyCallEmitted(call.toolCallId);
|
|
5987
6427
|
}
|
|
5988
6428
|
controller.enqueue({
|
|
@@ -6146,6 +6586,7 @@ ${plan}
|
|
|
6146
6586
|
});
|
|
6147
6587
|
turnCompleted = false;
|
|
6148
6588
|
resetAutoContinueWindow();
|
|
6589
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
6149
6590
|
proc.stdin?.write(makeAutoContinueMessage() + "\n");
|
|
6150
6591
|
return;
|
|
6151
6592
|
}
|
|
@@ -6186,6 +6627,9 @@ ${plan}
|
|
|
6186
6627
|
});
|
|
6187
6628
|
controllerClosed = true;
|
|
6188
6629
|
cleanupTurn();
|
|
6630
|
+
if (!useInteractive && !compactionMode) {
|
|
6631
|
+
scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
|
|
6632
|
+
}
|
|
6189
6633
|
try {
|
|
6190
6634
|
controller.close();
|
|
6191
6635
|
} catch {
|
|
@@ -6952,6 +7396,11 @@ ${plan}
|
|
|
6952
7396
|
options.abortSignal.addEventListener("abort", () => {
|
|
6953
7397
|
autoContinueState.aborted = true;
|
|
6954
7398
|
if (turnCompleted || controllerClosed) return;
|
|
7399
|
+
if (activeProcess) {
|
|
7400
|
+
void interruptTurn(activeProcess).then((idle) => {
|
|
7401
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
7402
|
+
});
|
|
7403
|
+
}
|
|
6955
7404
|
if (!hasReceivedContent) {
|
|
6956
7405
|
log.info(
|
|
6957
7406
|
"abort signal received before content, closing stream immediately",
|
|
@@ -7039,6 +7488,7 @@ ${plan}
|
|
|
7039
7488
|
);
|
|
7040
7489
|
}
|
|
7041
7490
|
}
|
|
7491
|
+
if (activeProcess) noteTurnStarted(activeProcess);
|
|
7042
7492
|
proc.stdin?.write(userMsg + "\n");
|
|
7043
7493
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
7044
7494
|
armStartWatchdog();
|
|
@@ -7070,7 +7520,7 @@ ${plan}
|
|
|
7070
7520
|
|
|
7071
7521
|
// src/accounts.ts
|
|
7072
7522
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
7073
|
-
import
|
|
7523
|
+
import path7 from "path";
|
|
7074
7524
|
var BASE_PROVIDER_ID = "claude-code";
|
|
7075
7525
|
var DEFAULT_ACCOUNT = "default";
|
|
7076
7526
|
var SHARED_CAPABILITY_ITEMS = [
|
|
@@ -7108,7 +7558,7 @@ function expandHome(value) {
|
|
|
7108
7558
|
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
7109
7559
|
if (value === "~") return home ?? value;
|
|
7110
7560
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
7111
|
-
return home ?
|
|
7561
|
+
return home ? path7.join(home, value.slice(2)) : value;
|
|
7112
7562
|
}
|
|
7113
7563
|
return value;
|
|
7114
7564
|
}
|
|
@@ -7140,8 +7590,8 @@ async function ensureSharedCapabilities(targetRoot) {
|
|
|
7140
7590
|
}
|
|
7141
7591
|
}
|
|
7142
7592
|
async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
7143
|
-
const source =
|
|
7144
|
-
const target =
|
|
7593
|
+
const source = path7.join(sourceRoot, item);
|
|
7594
|
+
const target = path7.join(targetRoot, item);
|
|
7145
7595
|
let sourceStat;
|
|
7146
7596
|
try {
|
|
7147
7597
|
sourceStat = await lstat(source);
|
|
@@ -7152,8 +7602,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7152
7602
|
const targetStat = await lstat(target);
|
|
7153
7603
|
if (targetStat.isSymbolicLink()) {
|
|
7154
7604
|
const current = await readlink(target);
|
|
7155
|
-
const resolvedCurrent =
|
|
7156
|
-
const resolvedSource =
|
|
7605
|
+
const resolvedCurrent = path7.resolve(path7.dirname(target), current);
|
|
7606
|
+
const resolvedSource = path7.resolve(source);
|
|
7157
7607
|
if (resolvedCurrent === resolvedSource) return;
|
|
7158
7608
|
}
|
|
7159
7609
|
log.warn("shared Claude capability already exists; leaving untouched", {
|
|
@@ -7168,11 +7618,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
|
|
|
7168
7618
|
await symlink(source, target, type);
|
|
7169
7619
|
}
|
|
7170
7620
|
async function writeAccountWrapper(account, baseCliPath, configDir) {
|
|
7171
|
-
const cacheRoot =
|
|
7621
|
+
const cacheRoot = path7.join(
|
|
7172
7622
|
process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
|
|
7173
7623
|
"opencode-claude-code-plugin"
|
|
7174
7624
|
);
|
|
7175
|
-
const wrapperPath =
|
|
7625
|
+
const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
|
|
7176
7626
|
const suffix = `@${account}`;
|
|
7177
7627
|
await mkdir(cacheRoot, { recursive: true });
|
|
7178
7628
|
const script = `#!/usr/bin/env bash
|
|
@@ -7215,11 +7665,11 @@ import {
|
|
|
7215
7665
|
existsSync as existsSync4,
|
|
7216
7666
|
readFileSync as readFileSync4,
|
|
7217
7667
|
realpathSync,
|
|
7218
|
-
rmSync as
|
|
7219
|
-
writeFileSync as
|
|
7668
|
+
rmSync as rmSync3,
|
|
7669
|
+
writeFileSync as writeFileSync5
|
|
7220
7670
|
} from "fs";
|
|
7221
|
-
import { homedir as
|
|
7222
|
-
import { join as
|
|
7671
|
+
import { homedir as homedir6 } from "os";
|
|
7672
|
+
import { join as join8, resolve as resolve4 } from "path";
|
|
7223
7673
|
import { fileURLToPath } from "url";
|
|
7224
7674
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
7225
7675
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -7227,14 +7677,14 @@ var alreadyRan = false;
|
|
|
7227
7677
|
function candidateCacheRoots() {
|
|
7228
7678
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
7229
7679
|
return [
|
|
7230
|
-
xdg ?
|
|
7231
|
-
|
|
7232
|
-
|
|
7680
|
+
xdg ? join8(xdg, "opencode") : null,
|
|
7681
|
+
join8(homedir6(), ".cache", "opencode"),
|
|
7682
|
+
join8(homedir6(), "Library", "Caches", "opencode")
|
|
7233
7683
|
].filter((p) => Boolean(p));
|
|
7234
7684
|
}
|
|
7235
7685
|
function userOpencodeJsonPath() {
|
|
7236
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
7237
|
-
return
|
|
7686
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
|
|
7687
|
+
return join8(xdgConfig, "opencode", "opencode.json");
|
|
7238
7688
|
}
|
|
7239
7689
|
function userIntendsToUseUnscoped() {
|
|
7240
7690
|
const cfg = userOpencodeJsonPath();
|
|
@@ -7253,7 +7703,7 @@ function userIntendsToUseUnscoped() {
|
|
|
7253
7703
|
function ourLoadedDir() {
|
|
7254
7704
|
try {
|
|
7255
7705
|
const filePath = fileURLToPath(import.meta.url);
|
|
7256
|
-
return realpathSync(
|
|
7706
|
+
return realpathSync(resolve4(filePath, "..", ".."));
|
|
7257
7707
|
} catch {
|
|
7258
7708
|
return null;
|
|
7259
7709
|
}
|
|
@@ -7277,7 +7727,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
7277
7727
|
}
|
|
7278
7728
|
function cleanupOne(cacheRoot, ourDir) {
|
|
7279
7729
|
if (!existsSync4(cacheRoot)) return;
|
|
7280
|
-
const stalePath =
|
|
7730
|
+
const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
7281
7731
|
if (!existsSync4(stalePath)) return;
|
|
7282
7732
|
let realStalePath = stalePath;
|
|
7283
7733
|
try {
|
|
@@ -7285,7 +7735,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7285
7735
|
} catch {
|
|
7286
7736
|
}
|
|
7287
7737
|
if (ourDir && realStalePath === ourDir) return;
|
|
7288
|
-
const pkgJsonPath =
|
|
7738
|
+
const pkgJsonPath = join8(stalePath, "package.json");
|
|
7289
7739
|
if (!existsSync4(pkgJsonPath)) return;
|
|
7290
7740
|
let pkg = {};
|
|
7291
7741
|
try {
|
|
@@ -7297,7 +7747,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7297
7747
|
if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
|
|
7298
7748
|
log.info("cleanup-stale: removing unscoped install", { stalePath });
|
|
7299
7749
|
try {
|
|
7300
|
-
|
|
7750
|
+
rmSync3(stalePath, { recursive: true, force: true });
|
|
7301
7751
|
} catch (err) {
|
|
7302
7752
|
log.warn("cleanup-stale: rmSync failed", {
|
|
7303
7753
|
stalePath,
|
|
@@ -7305,13 +7755,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7305
7755
|
});
|
|
7306
7756
|
return;
|
|
7307
7757
|
}
|
|
7308
|
-
const cachePkgJson =
|
|
7758
|
+
const cachePkgJson = join8(cacheRoot, "package.json");
|
|
7309
7759
|
if (!existsSync4(cachePkgJson)) return;
|
|
7310
7760
|
try {
|
|
7311
7761
|
const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
|
|
7312
7762
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
7313
7763
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
7314
|
-
|
|
7764
|
+
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
7315
7765
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
7316
7766
|
}
|
|
7317
7767
|
} catch (err) {
|
|
@@ -7323,16 +7773,16 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
7323
7773
|
|
|
7324
7774
|
// src/startup-diagnostics.ts
|
|
7325
7775
|
import { execFile as execFile2 } from "child_process";
|
|
7326
|
-
import * as
|
|
7327
|
-
import * as
|
|
7776
|
+
import * as fs6 from "fs";
|
|
7777
|
+
import * as path8 from "path";
|
|
7328
7778
|
import { promisify as promisify2 } from "util";
|
|
7329
7779
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7330
7780
|
var cachedPluginVersion;
|
|
7331
7781
|
function pluginVersion() {
|
|
7332
7782
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
7333
7783
|
try {
|
|
7334
|
-
const here =
|
|
7335
|
-
const raw =
|
|
7784
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
7785
|
+
const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
|
|
7336
7786
|
const version = JSON.parse(raw).version;
|
|
7337
7787
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
7338
7788
|
} catch {
|
|
@@ -7356,7 +7806,7 @@ var opencodeVersionProbe;
|
|
|
7356
7806
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
7357
7807
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
7358
7808
|
opencodeVersionProbe = (async () => {
|
|
7359
|
-
if (!
|
|
7809
|
+
if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
|
|
7360
7810
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
7361
7811
|
return void 0;
|
|
7362
7812
|
}
|
|
@@ -7526,6 +7976,8 @@ function createClaudeCode(settings = {}) {
|
|
|
7526
7976
|
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
|
|
7527
7977
|
compactionModel: settings.compactionModel,
|
|
7528
7978
|
ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
|
|
7979
|
+
idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
|
|
7980
|
+
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
7529
7981
|
interactive: settings.interactive,
|
|
7530
7982
|
interactiveBypass: settings.interactiveBypass,
|
|
7531
7983
|
interactiveAllowTools: settings.interactiveAllowTools,
|