@khalilgharbaoui/opencode-claude-code-plugin 0.31.0 → 0.32.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 +41 -2
- package/dist/index.js +651 -198
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +45 -1
package/dist/index.js
CHANGED
|
@@ -415,6 +415,7 @@ var DEFAULT_PROXY_TOOL_NAMES = [
|
|
|
415
415
|
"Task"
|
|
416
416
|
];
|
|
417
417
|
var PROXY_MCP_SERVER_NAME = "opencode_proxy";
|
|
418
|
+
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
418
419
|
|
|
419
420
|
// src/cli-events.ts
|
|
420
421
|
function isRecord(value) {
|
|
@@ -595,9 +596,44 @@ function parseSystemInit(msg) {
|
|
|
595
596
|
cliVersion: str(raw.claude_code_version),
|
|
596
597
|
toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
|
|
597
598
|
mcpServers: servers,
|
|
598
|
-
mcpServerErrors: parseMcpServerErrors(msg)
|
|
599
|
+
mcpServerErrors: parseMcpServerErrors(msg),
|
|
600
|
+
pluginErrors: parsePluginDiagnostics(raw.plugin_errors),
|
|
601
|
+
pluginWarnings: parsePluginDiagnostics(raw.plugin_warnings),
|
|
602
|
+
loadedPlugins: parseLoadedPlugins(raw.plugins)
|
|
599
603
|
};
|
|
600
604
|
}
|
|
605
|
+
function parsePluginDiagnostics(raw) {
|
|
606
|
+
if (!Array.isArray(raw)) return [];
|
|
607
|
+
const diagnostics = [];
|
|
608
|
+
for (const entry of raw) {
|
|
609
|
+
if (!isRecord(entry)) continue;
|
|
610
|
+
diagnostics.push({
|
|
611
|
+
plugin: str(entry.plugin) ?? "unknown",
|
|
612
|
+
type: str(entry.type) ?? "unknown",
|
|
613
|
+
message: str(entry.message) ?? ""
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return diagnostics;
|
|
617
|
+
}
|
|
618
|
+
function parseLoadedPlugins(raw) {
|
|
619
|
+
if (!Array.isArray(raw)) return [];
|
|
620
|
+
const loaded = [];
|
|
621
|
+
for (const entry of raw) {
|
|
622
|
+
if (!isRecord(entry)) continue;
|
|
623
|
+
for (const id of [str(entry.source), str(entry.name)]) {
|
|
624
|
+
if (id && !loaded.includes(id)) loaded.push(id);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return loaded;
|
|
628
|
+
}
|
|
629
|
+
function describePluginLoadFailure(diagnostic) {
|
|
630
|
+
const detail = diagnostic.message ? ` Claude Code said: ${diagnostic.message}` : "";
|
|
631
|
+
const name = diagnostic.plugin.split("@", 1)[0];
|
|
632
|
+
if (name === SKILL_PLUGIN_NAME) {
|
|
633
|
+
return `Claude Code did not load the plugin's own skill bridge "${diagnostic.plugin}" (${diagnostic.type}), so the opencode skills it bridges are missing this session. This is a plugin bug or a corrupted scratch directory rather than your config: report it with this line.${detail}`;
|
|
634
|
+
}
|
|
635
|
+
return `Claude Code did not load plugin "${diagnostic.plugin}" (${diagnostic.type}), so its skills, commands and MCP servers are missing this session.${detail}`;
|
|
636
|
+
}
|
|
601
637
|
function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
|
|
602
638
|
if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
|
|
603
639
|
const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription.`;
|
|
@@ -607,9 +643,32 @@ var warnedApiKeySources = /* @__PURE__ */ new Set();
|
|
|
607
643
|
var warnedMcpFailures = /* @__PURE__ */ new Set();
|
|
608
644
|
var warnedMcpSkips = /* @__PURE__ */ new Set();
|
|
609
645
|
var lastMcpServerErrors = /* @__PURE__ */ new Map();
|
|
646
|
+
var warnedPluginDiagnostics = /* @__PURE__ */ new Set();
|
|
647
|
+
var lastPluginLoadFailures = /* @__PURE__ */ new Map();
|
|
610
648
|
function snapshotMcpServerErrors() {
|
|
611
649
|
return [...lastMcpServerErrors.values()];
|
|
612
650
|
}
|
|
651
|
+
function snapshotPluginLoadFailures() {
|
|
652
|
+
return [...lastPluginLoadFailures.values()];
|
|
653
|
+
}
|
|
654
|
+
function reportPluginDiagnostics(info) {
|
|
655
|
+
const report = (kind, diagnostic) => {
|
|
656
|
+
const loaded = kind === "warning" && info.loadedPlugins.includes(diagnostic.plugin);
|
|
657
|
+
const message = loaded ? `Claude Code has advisory feedback for plugin "${diagnostic.plugin}" (${diagnostic.type}): ${diagnostic.message}` : describePluginLoadFailure(diagnostic);
|
|
658
|
+
if (!loaded) lastPluginLoadFailures.set(diagnostic.plugin, { kind, ...diagnostic });
|
|
659
|
+
const key = `${kind}:${diagnostic.plugin}:${diagnostic.type}`;
|
|
660
|
+
const data = { plugin: diagnostic.plugin, type: diagnostic.type, kind };
|
|
661
|
+
if (warnedPluginDiagnostics.has(key)) {
|
|
662
|
+
log.debug(message, data);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
warnedPluginDiagnostics.add(key);
|
|
666
|
+
if (loaded) log.info(message, data);
|
|
667
|
+
else log.warn(message, data);
|
|
668
|
+
};
|
|
669
|
+
for (const diagnostic of info.pluginErrors) report("error", diagnostic);
|
|
670
|
+
for (const diagnostic of info.pluginWarnings) report("warning", diagnostic);
|
|
671
|
+
}
|
|
613
672
|
function reportSystemInit(msg, options = {}) {
|
|
614
673
|
const info = parseSystemInit(msg);
|
|
615
674
|
if (!info) return;
|
|
@@ -620,8 +679,11 @@ function reportSystemInit(msg, options = {}) {
|
|
|
620
679
|
cliVersion: info.cliVersion ?? null,
|
|
621
680
|
tools: info.toolCount,
|
|
622
681
|
mcpServers: info.mcpServers,
|
|
623
|
-
mcpServerErrors: info.mcpServerErrors
|
|
682
|
+
mcpServerErrors: info.mcpServerErrors,
|
|
683
|
+
pluginErrors: info.pluginErrors,
|
|
684
|
+
pluginWarnings: info.pluginWarnings
|
|
624
685
|
});
|
|
686
|
+
reportPluginDiagnostics(info);
|
|
625
687
|
for (const error of info.mcpServerErrors) {
|
|
626
688
|
lastMcpServerErrors.set(error.name, error);
|
|
627
689
|
const key2 = `${error.name}:${error.type}`;
|
|
@@ -1621,7 +1683,8 @@ function buildProxyTimeoutError(toolName, ms) {
|
|
|
1621
1683
|
}
|
|
1622
1684
|
return new Error(base);
|
|
1623
1685
|
}
|
|
1624
|
-
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.
|
|
1686
|
+
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. Calls block until the subagent finishes. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
|
|
1687
|
+
var TASK_BACKGROUND_NOTE = 'Fire-and-collect: set `background: true` and the call returns at once with `<task id="ses_..." state="running">` instead of the subagent\'s answer. Keep working on something that does not overlap it, then end your turn: when the subagent finishes, opencode delivers its result to this conversation on its own as a new message. Do NOT poll, sleep or loop waiting for it. The `id` in that envelope is the task_id: pass it to task_status to read a result the notification did not deliver, or to task_cancel to stop a background subagent you no longer want.';
|
|
1625
1688
|
var TASK_BATCH_TOOL_NAME = "task_batch";
|
|
1626
1689
|
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. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
|
|
1627
1690
|
var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
|
|
@@ -1707,6 +1770,75 @@ function overlayQuestionProxyDescription(tools, liveDescription) {
|
|
|
1707
1770
|
${QUESTION_PROXY_NOTE}` } : t
|
|
1708
1771
|
);
|
|
1709
1772
|
}
|
|
1773
|
+
function liveTaskSupportsBackground(taskParameters, dialect = "v1") {
|
|
1774
|
+
if (dialect === "v2") return true;
|
|
1775
|
+
const properties = taskParameters?.properties;
|
|
1776
|
+
if (!properties || typeof properties !== "object") return false;
|
|
1777
|
+
return "background" in properties;
|
|
1778
|
+
}
|
|
1779
|
+
var BACKGROUND_TASK_TOOL_DEFS = [
|
|
1780
|
+
{
|
|
1781
|
+
name: "task_status",
|
|
1782
|
+
description: "Read the current state of a background subagent started with `task` and `background: true`, and collect its result if it has finished. Use this only to recover a result opencode's automatic completion notification did not deliver (the turn was interrupted, errored, or was compacted across). It is NOT a progress poll: a healthy background task delivers its own result, so calling this in a loop spends turns and learns nothing. A result is handed over once; asking again reports the state without repeating the output.",
|
|
1783
|
+
inputSchema: {
|
|
1784
|
+
type: "object",
|
|
1785
|
+
properties: {
|
|
1786
|
+
task_id: {
|
|
1787
|
+
type: "string",
|
|
1788
|
+
description: 'The id from the `<task id="..." state="running">` envelope the background dispatch returned.'
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
required: ["task_id"]
|
|
1792
|
+
}
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
name: "task_cancel",
|
|
1796
|
+
description: "Stop a background subagent started with `task` and `background: true`. Use it when the work is no longer wanted, has been superseded, or is running away. A cancelled subagent sends no completion notification. This cannot stop a foreground task call: those block this conversation and end on their own.",
|
|
1797
|
+
inputSchema: {
|
|
1798
|
+
type: "object",
|
|
1799
|
+
properties: {
|
|
1800
|
+
task_id: {
|
|
1801
|
+
type: "string",
|
|
1802
|
+
description: 'The id from the `<task id="..." state="running">` envelope the background dispatch returned.'
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
required: ["task_id"]
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
];
|
|
1809
|
+
function applyBackgroundSubagentSupport(tools, supported) {
|
|
1810
|
+
if (!tools.some((t) => t.name === "task")) return tools;
|
|
1811
|
+
const adjusted = tools.map((t) => {
|
|
1812
|
+
if (t.name !== "task" && t.name !== TASK_BATCH_TOOL_NAME) return t;
|
|
1813
|
+
if (supported) {
|
|
1814
|
+
return { ...t, description: `${t.description}
|
|
1815
|
+
|
|
1816
|
+
${TASK_BACKGROUND_NOTE}` };
|
|
1817
|
+
}
|
|
1818
|
+
return { ...t, inputSchema: stripBackgroundFromSchema(t.name, t.inputSchema) };
|
|
1819
|
+
});
|
|
1820
|
+
return supported ? [...adjusted, ...BACKGROUND_TASK_TOOL_DEFS] : adjusted;
|
|
1821
|
+
}
|
|
1822
|
+
function stripBackgroundFromSchema(toolName, schema) {
|
|
1823
|
+
if (toolName === TASK_BATCH_TOOL_NAME) {
|
|
1824
|
+
return {
|
|
1825
|
+
...schema,
|
|
1826
|
+
properties: {
|
|
1827
|
+
tasks: {
|
|
1828
|
+
type: "array",
|
|
1829
|
+
minItems: 2,
|
|
1830
|
+
description: "Independent subagent tasks to run concurrently",
|
|
1831
|
+
items: {
|
|
1832
|
+
type: "object",
|
|
1833
|
+
properties: TASK_INPUT_PROPERTIES_NO_BACKGROUND,
|
|
1834
|
+
required: TASK_INPUT_REQUIRED
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
return { ...schema, properties: TASK_INPUT_PROPERTIES_NO_BACKGROUND };
|
|
1841
|
+
}
|
|
1710
1842
|
function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
|
|
1711
1843
|
if (opencodeHasQuestion) return tools;
|
|
1712
1844
|
return tools.filter((t) => t.name !== "question");
|
|
@@ -1734,9 +1866,12 @@ var TASK_INPUT_PROPERTIES = {
|
|
|
1734
1866
|
},
|
|
1735
1867
|
background: {
|
|
1736
1868
|
type: "boolean",
|
|
1737
|
-
description:
|
|
1869
|
+
description: 'Run the subagent in the background and return immediately with a `<task id="..." state="running">` envelope instead of its answer. You are notified automatically when it completes. Do NOT sleep, poll or proactively check on its progress.'
|
|
1738
1870
|
}
|
|
1739
1871
|
};
|
|
1872
|
+
var TASK_INPUT_PROPERTIES_NO_BACKGROUND = Object.fromEntries(
|
|
1873
|
+
Object.entries(TASK_INPUT_PROPERTIES).filter(([name]) => name !== "background")
|
|
1874
|
+
);
|
|
1740
1875
|
var DEFAULT_PROXY_TOOLS = [
|
|
1741
1876
|
{
|
|
1742
1877
|
name: "bash",
|
|
@@ -2797,6 +2932,362 @@ function clearCompression(sessionKey2) {
|
|
|
2797
2932
|
compressions.delete(sessionKey2);
|
|
2798
2933
|
}
|
|
2799
2934
|
|
|
2935
|
+
// src/runtime-status.ts
|
|
2936
|
+
var opencodeClient = null;
|
|
2937
|
+
function setOpencodeClient(client) {
|
|
2938
|
+
if (client && typeof client === "object") {
|
|
2939
|
+
opencodeClient = client;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function getOpencodeClient() {
|
|
2943
|
+
return opencodeClient;
|
|
2944
|
+
}
|
|
2945
|
+
var opencodeProjectDirectory;
|
|
2946
|
+
function setOpencodeProjectDirectory(dir) {
|
|
2947
|
+
opencodeProjectDirectory = dir;
|
|
2948
|
+
}
|
|
2949
|
+
function getOpencodeProjectDirectory() {
|
|
2950
|
+
return opencodeProjectDirectory;
|
|
2951
|
+
}
|
|
2952
|
+
function isUsableDirectory(d) {
|
|
2953
|
+
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
2954
|
+
}
|
|
2955
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
2956
|
+
if (configured) return configured;
|
|
2957
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
2958
|
+
if (isUsableDirectory(live)) return live;
|
|
2959
|
+
return captured ?? live;
|
|
2960
|
+
}
|
|
2961
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
2962
|
+
if (configured) return configured;
|
|
2963
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
2964
|
+
return resolveSpawnCwdFrom(
|
|
2965
|
+
configured,
|
|
2966
|
+
process.cwd(),
|
|
2967
|
+
opencodeProjectDirectory,
|
|
2968
|
+
sessionDir
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
async function fetchSessionDirectory(sessionID) {
|
|
2972
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
2973
|
+
const client = opencodeClient;
|
|
2974
|
+
if (!client?.session?.get) return void 0;
|
|
2975
|
+
try {
|
|
2976
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
2977
|
+
const data = res.data;
|
|
2978
|
+
if (!data || typeof data !== "object") return void 0;
|
|
2979
|
+
const dir = data.directory;
|
|
2980
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
log.warn("failed to fetch opencode session directory", {
|
|
2983
|
+
sessionID,
|
|
2984
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2985
|
+
});
|
|
2986
|
+
return void 0;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
async function fetchSessionParentId(sessionID) {
|
|
2990
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
2991
|
+
const client = opencodeClient;
|
|
2992
|
+
if (!client?.session?.get) return void 0;
|
|
2993
|
+
try {
|
|
2994
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
2995
|
+
const data = res.data;
|
|
2996
|
+
if (!data || typeof data !== "object") return void 0;
|
|
2997
|
+
const parentID = data.parentID;
|
|
2998
|
+
return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
log.warn("failed to fetch opencode session parent", {
|
|
3001
|
+
sessionID,
|
|
3002
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3003
|
+
});
|
|
3004
|
+
return void 0;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
async function fetchSessionRunState(sessionID) {
|
|
3008
|
+
if (!sessionID || sessionID === "default") return "unknown";
|
|
3009
|
+
const status = opencodeClient?.session?.status;
|
|
3010
|
+
if (!status) return "unknown";
|
|
3011
|
+
try {
|
|
3012
|
+
const res = await status.call(opencodeClient.session);
|
|
3013
|
+
const data = res.data;
|
|
3014
|
+
if (!data || typeof data !== "object") return "unknown";
|
|
3015
|
+
const entry = data[sessionID];
|
|
3016
|
+
if (!entry || typeof entry !== "object") return "idle";
|
|
3017
|
+
const type = entry.type;
|
|
3018
|
+
return type === "idle" ? "idle" : "busy";
|
|
3019
|
+
} catch (err) {
|
|
3020
|
+
log.debug("failed to read opencode session status", {
|
|
3021
|
+
sessionID,
|
|
3022
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3023
|
+
});
|
|
3024
|
+
return "unknown";
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
async function settleSessionRunState(sessionID, options = {}) {
|
|
3028
|
+
const pollMs = options.pollMs ?? 150;
|
|
3029
|
+
const windowMs = options.windowMs ?? 1500;
|
|
3030
|
+
const started = Date.now();
|
|
3031
|
+
let last = "unknown";
|
|
3032
|
+
for (; ; ) {
|
|
3033
|
+
const state = await fetchSessionRunState(sessionID);
|
|
3034
|
+
if (state === "busy") return "busy";
|
|
3035
|
+
if (state !== "unknown") last = state;
|
|
3036
|
+
if (Date.now() - started >= windowMs) return last;
|
|
3037
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
async function getRuntimeMcpStatus() {
|
|
3041
|
+
const client = opencodeClient;
|
|
3042
|
+
if (!client?.mcp?.status) return void 0;
|
|
3043
|
+
try {
|
|
3044
|
+
const res = await client.mcp.status();
|
|
3045
|
+
const data = res.data;
|
|
3046
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3047
|
+
const out = {};
|
|
3048
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
3049
|
+
if (entry && typeof entry === "object") {
|
|
3050
|
+
const status = entry.status;
|
|
3051
|
+
if (typeof status === "string") out[name] = status;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return out;
|
|
3055
|
+
} catch (err) {
|
|
3056
|
+
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3057
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3058
|
+
});
|
|
3059
|
+
return void 0;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
3063
|
+
const client = opencodeClient;
|
|
3064
|
+
if (!client?.tool?.list) return void 0;
|
|
3065
|
+
try {
|
|
3066
|
+
const res = await client.tool.list({
|
|
3067
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
3068
|
+
});
|
|
3069
|
+
const data = res.data;
|
|
3070
|
+
if (!Array.isArray(data)) return void 0;
|
|
3071
|
+
const out = [];
|
|
3072
|
+
for (const entry of data) {
|
|
3073
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3074
|
+
const e = entry;
|
|
3075
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
3076
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
3077
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3078
|
+
if (!id) continue;
|
|
3079
|
+
out.push({ id, description, parameters });
|
|
3080
|
+
}
|
|
3081
|
+
return out;
|
|
3082
|
+
} catch (err) {
|
|
3083
|
+
log.warn("failed to fetch opencode tool list", {
|
|
3084
|
+
provider,
|
|
3085
|
+
model,
|
|
3086
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3087
|
+
});
|
|
3088
|
+
return void 0;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
async function fetchSessionReplies(sessionID) {
|
|
3092
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3093
|
+
const client = opencodeClient;
|
|
3094
|
+
if (!client?.session?.messages) return void 0;
|
|
3095
|
+
try {
|
|
3096
|
+
const res = await client.session.messages({ path: { id: sessionID } });
|
|
3097
|
+
const data = res.data;
|
|
3098
|
+
if (!Array.isArray(data)) return void 0;
|
|
3099
|
+
const out = [];
|
|
3100
|
+
for (const entry of data) {
|
|
3101
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3102
|
+
const info = entry.info;
|
|
3103
|
+
if (!info || typeof info !== "object") continue;
|
|
3104
|
+
const record = info;
|
|
3105
|
+
const parts = entry.parts;
|
|
3106
|
+
const text = Array.isArray(parts) ? parts.flatMap((part) => {
|
|
3107
|
+
if (!part || typeof part !== "object") return [];
|
|
3108
|
+
const p = part;
|
|
3109
|
+
if (p.type !== "text" || typeof p.text !== "string") return [];
|
|
3110
|
+
return p.synthetic === true ? [] : [p.text];
|
|
3111
|
+
}).join("\n").trim() : "";
|
|
3112
|
+
const error = record.error;
|
|
3113
|
+
const message = error && typeof error === "object" ? typeof error.data?.message === "string" ? error.data.message : typeof error.name === "string" ? error.name : void 0 : void 0;
|
|
3114
|
+
out.push({
|
|
3115
|
+
role: typeof record.role === "string" ? record.role : "",
|
|
3116
|
+
completed: typeof record.time?.completed === "number",
|
|
3117
|
+
error: message,
|
|
3118
|
+
text
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
return out;
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
log.warn("failed to fetch opencode session messages", {
|
|
3124
|
+
sessionID,
|
|
3125
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3126
|
+
});
|
|
3127
|
+
return void 0;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
async function abortSession(sessionID) {
|
|
3131
|
+
if (!sessionID || sessionID === "default") return false;
|
|
3132
|
+
const client = opencodeClient;
|
|
3133
|
+
if (!client?.session?.abort) return false;
|
|
3134
|
+
try {
|
|
3135
|
+
await client.session.abort({ path: { id: sessionID } });
|
|
3136
|
+
return true;
|
|
3137
|
+
} catch (err) {
|
|
3138
|
+
log.warn("failed to abort opencode session", {
|
|
3139
|
+
sessionID,
|
|
3140
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3141
|
+
});
|
|
3142
|
+
return false;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// src/background-tasks.ts
|
|
3147
|
+
var TASK_STATUS_TOOL_NAME = "task_status";
|
|
3148
|
+
var TASK_CANCEL_TOOL_NAME = "task_cancel";
|
|
3149
|
+
var MAX_LEDGERS = 32;
|
|
3150
|
+
var MAX_COLLECTED_PER_SESSION = 64;
|
|
3151
|
+
var collected = /* @__PURE__ */ new Map();
|
|
3152
|
+
function ledgerFor(sessionKey2) {
|
|
3153
|
+
let ids = collected.get(sessionKey2);
|
|
3154
|
+
if (!ids) {
|
|
3155
|
+
ids = /* @__PURE__ */ new Set();
|
|
3156
|
+
collected.set(sessionKey2, ids);
|
|
3157
|
+
while (collected.size > MAX_LEDGERS) {
|
|
3158
|
+
const oldest = collected.keys().next();
|
|
3159
|
+
if (oldest.done) break;
|
|
3160
|
+
collected.delete(oldest.value);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
return ids;
|
|
3164
|
+
}
|
|
3165
|
+
function markCollected(sessionKey2, taskId) {
|
|
3166
|
+
const ids = ledgerFor(sessionKey2);
|
|
3167
|
+
ids.add(taskId);
|
|
3168
|
+
while (ids.size > MAX_COLLECTED_PER_SESSION) {
|
|
3169
|
+
const oldest = ids.values().next();
|
|
3170
|
+
if (oldest.done) break;
|
|
3171
|
+
ids.delete(oldest.value);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
function hasCollectedBackgroundTask(sessionKey2, taskId) {
|
|
3175
|
+
return collected.get(sessionKey2)?.has(taskId) === true;
|
|
3176
|
+
}
|
|
3177
|
+
function clearBackgroundTasks(sessionKey2) {
|
|
3178
|
+
collected.delete(sessionKey2);
|
|
3179
|
+
}
|
|
3180
|
+
function readTaskId(input) {
|
|
3181
|
+
const raw = input.task_id ?? input.taskId ?? input.sessionID;
|
|
3182
|
+
if (typeof raw !== "string") return null;
|
|
3183
|
+
const id = raw.trim();
|
|
3184
|
+
return id.length > 0 ? id : null;
|
|
3185
|
+
}
|
|
3186
|
+
var MISSING_TASK_ID = 'task_id is required: pass the id from the `<task id="...">` envelope the background dispatch returned.';
|
|
3187
|
+
async function guardParent(taskId, callerSessionId) {
|
|
3188
|
+
if (!callerSessionId || callerSessionId === "default") {
|
|
3189
|
+
return `Cannot check that task_id ${taskId} belongs to this conversation (this turn has no opencode session id), so it is not inspected or cancelled.`;
|
|
3190
|
+
}
|
|
3191
|
+
const parent = await fetchSessionParentId(taskId);
|
|
3192
|
+
if (parent === callerSessionId) return null;
|
|
3193
|
+
return `task_id ${taskId} is not a subagent of this conversation, so it cannot be inspected or cancelled from here. Use the id from a \`<task id="...">\` envelope this conversation received.`;
|
|
3194
|
+
}
|
|
3195
|
+
function lastAssistantReply(replies) {
|
|
3196
|
+
for (let index = replies.length - 1; index >= 0; index--) {
|
|
3197
|
+
const reply = replies[index];
|
|
3198
|
+
if (reply.role === "assistant") return reply;
|
|
3199
|
+
}
|
|
3200
|
+
return void 0;
|
|
3201
|
+
}
|
|
3202
|
+
async function collectBackgroundTask(input, options) {
|
|
3203
|
+
const taskId = readTaskId(input);
|
|
3204
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3205
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3206
|
+
if (refused) return { kind: "error", message: refused };
|
|
3207
|
+
const replies = await fetchSessionReplies(taskId);
|
|
3208
|
+
if (replies === void 0) {
|
|
3209
|
+
return {
|
|
3210
|
+
kind: "error",
|
|
3211
|
+
message: `Could not read background task ${taskId} from opencode. It may have been deleted, or this opencode build does not expose the session transcript. The completion notification is still the primary delivery path: end your turn and wait for it.`
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
const runState = await fetchSessionRunState(taskId);
|
|
3215
|
+
if (runState === "busy") {
|
|
3216
|
+
return {
|
|
3217
|
+
kind: "text",
|
|
3218
|
+
text: `<task id="${taskId}" state="running">
|
|
3219
|
+
Still working. Do not call this again in a loop: when it finishes opencode delivers the result to this conversation on its own. End your turn, or work on something that does not overlap it.
|
|
3220
|
+
</task>`
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
const reply = lastAssistantReply(replies);
|
|
3224
|
+
if (!reply) {
|
|
3225
|
+
return {
|
|
3226
|
+
kind: "text",
|
|
3227
|
+
text: `<task id="${taskId}" state="unknown">
|
|
3228
|
+
opencode has no assistant reply for this task yet and it is not running. It may still be starting up.
|
|
3229
|
+
</task>`
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
if (reply.error) {
|
|
3233
|
+
markCollected(options.sessionKey, taskId);
|
|
3234
|
+
return {
|
|
3235
|
+
kind: "text",
|
|
3236
|
+
isError: true,
|
|
3237
|
+
text: `<task id="${taskId}" state="error">
|
|
3238
|
+
${reply.error}
|
|
3239
|
+
</task>`
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
|
|
3243
|
+
return {
|
|
3244
|
+
kind: "text",
|
|
3245
|
+
text: `<task id="${taskId}" state="completed">
|
|
3246
|
+
Already delivered to this conversation earlier (by this tool or by opencode's completion notification). Scroll back for the result rather than collecting it again.
|
|
3247
|
+
</task>`
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
markCollected(options.sessionKey, taskId);
|
|
3251
|
+
log.info("collected background subagent result", {
|
|
3252
|
+
sessionKey: options.sessionKey,
|
|
3253
|
+
taskId,
|
|
3254
|
+
textLength: reply.text.length
|
|
3255
|
+
});
|
|
3256
|
+
return {
|
|
3257
|
+
kind: "text",
|
|
3258
|
+
text: `<task id="${taskId}" state="completed">
|
|
3259
|
+
<task_result>
|
|
3260
|
+
${reply.text || "(the subagent produced no text)"}
|
|
3261
|
+
</task_result>
|
|
3262
|
+
</task>`
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
async function cancelBackgroundTask(input, options) {
|
|
3266
|
+
const taskId = readTaskId(input);
|
|
3267
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3268
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3269
|
+
if (refused) return { kind: "error", message: refused };
|
|
3270
|
+
const before = await fetchSessionRunState(taskId);
|
|
3271
|
+
const aborted = await abortSession(taskId);
|
|
3272
|
+
if (!aborted) {
|
|
3273
|
+
return {
|
|
3274
|
+
kind: "error",
|
|
3275
|
+
message: `Could not cancel background task ${taskId}: opencode did not accept the abort. It may have already finished, or this opencode build does not expose session abort.`
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
collected.get(options.sessionKey)?.delete(taskId);
|
|
3279
|
+
log.info("cancelled background subagent", {
|
|
3280
|
+
sessionKey: options.sessionKey,
|
|
3281
|
+
taskId,
|
|
3282
|
+
wasRunning: before === "busy"
|
|
3283
|
+
});
|
|
3284
|
+
return {
|
|
3285
|
+
kind: "text",
|
|
3286
|
+
text: `<task id="${taskId}" state="cancelled">
|
|
3287
|
+
` + (before === "busy" ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
|
|
2800
3291
|
// src/permission-presets.ts
|
|
2801
3292
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
2802
3293
|
"Bash",
|
|
@@ -3333,11 +3824,13 @@ function deleteActiveProcessesForSession(sessionID) {
|
|
|
3333
3824
|
clearCompression(key);
|
|
3334
3825
|
if (!released.includes(key)) released.push(key);
|
|
3335
3826
|
}
|
|
3827
|
+
for (const key of released) clearBackgroundTasks(key);
|
|
3336
3828
|
return released;
|
|
3337
3829
|
}
|
|
3338
3830
|
function killAllActiveProcesses() {
|
|
3339
3831
|
const keys = [...activeProcesses.keys()];
|
|
3340
3832
|
for (const key of keys) deleteActiveProcess(key);
|
|
3833
|
+
for (const key of keys) clearBackgroundTasks(key);
|
|
3341
3834
|
return keys;
|
|
3342
3835
|
}
|
|
3343
3836
|
var processExitCleanupWired = false;
|
|
@@ -3449,6 +3942,7 @@ function invalidateOtherEffortSessions(baseKey, effort) {
|
|
|
3449
3942
|
deleteActiveProcess(key);
|
|
3450
3943
|
deleteClaudeSessionId(key);
|
|
3451
3944
|
clearCompression(key);
|
|
3945
|
+
clearBackgroundTasks(key);
|
|
3452
3946
|
}
|
|
3453
3947
|
}
|
|
3454
3948
|
function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
|
|
@@ -4435,163 +4929,6 @@ function finishBridge(input) {
|
|
|
4435
4929
|
};
|
|
4436
4930
|
}
|
|
4437
4931
|
|
|
4438
|
-
// src/runtime-status.ts
|
|
4439
|
-
var opencodeClient = null;
|
|
4440
|
-
function setOpencodeClient(client) {
|
|
4441
|
-
if (client && typeof client === "object") {
|
|
4442
|
-
opencodeClient = client;
|
|
4443
|
-
}
|
|
4444
|
-
}
|
|
4445
|
-
function getOpencodeClient() {
|
|
4446
|
-
return opencodeClient;
|
|
4447
|
-
}
|
|
4448
|
-
var opencodeProjectDirectory;
|
|
4449
|
-
function setOpencodeProjectDirectory(dir) {
|
|
4450
|
-
opencodeProjectDirectory = dir;
|
|
4451
|
-
}
|
|
4452
|
-
function getOpencodeProjectDirectory() {
|
|
4453
|
-
return opencodeProjectDirectory;
|
|
4454
|
-
}
|
|
4455
|
-
function isUsableDirectory(d) {
|
|
4456
|
-
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
4457
|
-
}
|
|
4458
|
-
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
4459
|
-
if (configured) return configured;
|
|
4460
|
-
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
4461
|
-
if (isUsableDirectory(live)) return live;
|
|
4462
|
-
return captured ?? live;
|
|
4463
|
-
}
|
|
4464
|
-
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
4465
|
-
if (configured) return configured;
|
|
4466
|
-
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
4467
|
-
return resolveSpawnCwdFrom(
|
|
4468
|
-
configured,
|
|
4469
|
-
process.cwd(),
|
|
4470
|
-
opencodeProjectDirectory,
|
|
4471
|
-
sessionDir
|
|
4472
|
-
);
|
|
4473
|
-
}
|
|
4474
|
-
async function fetchSessionDirectory(sessionID) {
|
|
4475
|
-
if (!sessionID || sessionID === "default") return void 0;
|
|
4476
|
-
const client = opencodeClient;
|
|
4477
|
-
if (!client?.session?.get) return void 0;
|
|
4478
|
-
try {
|
|
4479
|
-
const res = await client.session.get({ path: { id: sessionID } });
|
|
4480
|
-
const data = res.data;
|
|
4481
|
-
if (!data || typeof data !== "object") return void 0;
|
|
4482
|
-
const dir = data.directory;
|
|
4483
|
-
return isUsableDirectory(dir) ? dir : void 0;
|
|
4484
|
-
} catch (err) {
|
|
4485
|
-
log.warn("failed to fetch opencode session directory", {
|
|
4486
|
-
sessionID,
|
|
4487
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4488
|
-
});
|
|
4489
|
-
return void 0;
|
|
4490
|
-
}
|
|
4491
|
-
}
|
|
4492
|
-
async function fetchSessionParentId(sessionID) {
|
|
4493
|
-
if (!sessionID || sessionID === "default") return void 0;
|
|
4494
|
-
const client = opencodeClient;
|
|
4495
|
-
if (!client?.session?.get) return void 0;
|
|
4496
|
-
try {
|
|
4497
|
-
const res = await client.session.get({ path: { id: sessionID } });
|
|
4498
|
-
const data = res.data;
|
|
4499
|
-
if (!data || typeof data !== "object") return void 0;
|
|
4500
|
-
const parentID = data.parentID;
|
|
4501
|
-
return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
|
|
4502
|
-
} catch (err) {
|
|
4503
|
-
log.warn("failed to fetch opencode session parent", {
|
|
4504
|
-
sessionID,
|
|
4505
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4506
|
-
});
|
|
4507
|
-
return void 0;
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
async function fetchSessionRunState(sessionID) {
|
|
4511
|
-
if (!sessionID || sessionID === "default") return "unknown";
|
|
4512
|
-
const status = opencodeClient?.session?.status;
|
|
4513
|
-
if (!status) return "unknown";
|
|
4514
|
-
try {
|
|
4515
|
-
const res = await status.call(opencodeClient.session);
|
|
4516
|
-
const data = res.data;
|
|
4517
|
-
if (!data || typeof data !== "object") return "unknown";
|
|
4518
|
-
const entry = data[sessionID];
|
|
4519
|
-
if (!entry || typeof entry !== "object") return "idle";
|
|
4520
|
-
const type = entry.type;
|
|
4521
|
-
return type === "idle" ? "idle" : "busy";
|
|
4522
|
-
} catch (err) {
|
|
4523
|
-
log.debug("failed to read opencode session status", {
|
|
4524
|
-
sessionID,
|
|
4525
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4526
|
-
});
|
|
4527
|
-
return "unknown";
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
async function settleSessionRunState(sessionID, options = {}) {
|
|
4531
|
-
const pollMs = options.pollMs ?? 150;
|
|
4532
|
-
const windowMs = options.windowMs ?? 1500;
|
|
4533
|
-
const started = Date.now();
|
|
4534
|
-
let last = "unknown";
|
|
4535
|
-
for (; ; ) {
|
|
4536
|
-
const state = await fetchSessionRunState(sessionID);
|
|
4537
|
-
if (state === "busy") return "busy";
|
|
4538
|
-
if (state !== "unknown") last = state;
|
|
4539
|
-
if (Date.now() - started >= windowMs) return last;
|
|
4540
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4541
|
-
}
|
|
4542
|
-
}
|
|
4543
|
-
async function getRuntimeMcpStatus() {
|
|
4544
|
-
const client = opencodeClient;
|
|
4545
|
-
if (!client?.mcp?.status) return void 0;
|
|
4546
|
-
try {
|
|
4547
|
-
const res = await client.mcp.status();
|
|
4548
|
-
const data = res.data;
|
|
4549
|
-
if (!data || typeof data !== "object") return void 0;
|
|
4550
|
-
const out = {};
|
|
4551
|
-
for (const [name, entry] of Object.entries(data)) {
|
|
4552
|
-
if (entry && typeof entry === "object") {
|
|
4553
|
-
const status = entry.status;
|
|
4554
|
-
if (typeof status === "string") out[name] = status;
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
return out;
|
|
4558
|
-
} catch (err) {
|
|
4559
|
-
log.warn("failed to fetch opencode MCP runtime status", {
|
|
4560
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4561
|
-
});
|
|
4562
|
-
return void 0;
|
|
4563
|
-
}
|
|
4564
|
-
}
|
|
4565
|
-
async function fetchOpencodeToolList(provider, model, directory) {
|
|
4566
|
-
const client = opencodeClient;
|
|
4567
|
-
if (!client?.tool?.list) return void 0;
|
|
4568
|
-
try {
|
|
4569
|
-
const res = await client.tool.list({
|
|
4570
|
-
query: { provider, model, ...directory ? { directory } : {} }
|
|
4571
|
-
});
|
|
4572
|
-
const data = res.data;
|
|
4573
|
-
if (!Array.isArray(data)) return void 0;
|
|
4574
|
-
const out = [];
|
|
4575
|
-
for (const entry of data) {
|
|
4576
|
-
if (!entry || typeof entry !== "object") continue;
|
|
4577
|
-
const e = entry;
|
|
4578
|
-
const id = typeof e.id === "string" ? e.id : null;
|
|
4579
|
-
const description = typeof e.description === "string" ? e.description : "";
|
|
4580
|
-
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
4581
|
-
if (!id) continue;
|
|
4582
|
-
out.push({ id, description, parameters });
|
|
4583
|
-
}
|
|
4584
|
-
return out;
|
|
4585
|
-
} catch (err) {
|
|
4586
|
-
log.warn("failed to fetch opencode tool list", {
|
|
4587
|
-
provider,
|
|
4588
|
-
model,
|
|
4589
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4590
|
-
});
|
|
4591
|
-
return void 0;
|
|
4592
|
-
}
|
|
4593
|
-
}
|
|
4594
|
-
|
|
4595
4932
|
// src/startup-diagnostics.ts
|
|
4596
4933
|
var cachedPluginVersion;
|
|
4597
4934
|
function pluginVersion() {
|
|
@@ -4869,6 +5206,18 @@ function formatDoctorReport(report) {
|
|
|
4869
5206
|
lines.push("");
|
|
4870
5207
|
lines.push("A skipped server is missing from the model's tools with no other sign of it.");
|
|
4871
5208
|
}
|
|
5209
|
+
if (report.pluginLoadFailures.length > 0) {
|
|
5210
|
+
lines.push("");
|
|
5211
|
+
lines.push("**Plugins Claude Code did not load**");
|
|
5212
|
+
lines.push("");
|
|
5213
|
+
lines.push("| plugin | kind | category | Claude Code said |");
|
|
5214
|
+
lines.push("|---|---|---|---|");
|
|
5215
|
+
for (const failure of report.pluginLoadFailures) {
|
|
5216
|
+
lines.push(
|
|
5217
|
+
`| ${failure.plugin} | ${failure.kind} | \`${failure.type}\` | ${failure.message || "no detail"} |`
|
|
5218
|
+
);
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
4872
5221
|
lines.push("");
|
|
4873
5222
|
lines.push("**Plan usage**");
|
|
4874
5223
|
lines.push("");
|
|
@@ -4967,6 +5316,7 @@ async function gatherDoctorReport(options) {
|
|
|
4967
5316
|
pendingCalls: snapshotPendingProxyCalls(),
|
|
4968
5317
|
proxyServers,
|
|
4969
5318
|
mcpServerErrors: snapshotMcpServerErrors(),
|
|
5319
|
+
pluginLoadFailures: snapshotPluginLoadFailures(),
|
|
4970
5320
|
planUsage
|
|
4971
5321
|
};
|
|
4972
5322
|
}
|
|
@@ -4981,6 +5331,7 @@ async function buildDoctorReport(options) {
|
|
|
4981
5331
|
pendingCalls: report.pendingCalls.length,
|
|
4982
5332
|
proxyServers: report.proxyServers.map((server2) => server2.auth.status),
|
|
4983
5333
|
mcpServerErrors: report.mcpServerErrors.length,
|
|
5334
|
+
pluginLoadFailures: report.pluginLoadFailures.length,
|
|
4984
5335
|
planUsage: report.planUsage.status
|
|
4985
5336
|
});
|
|
4986
5337
|
return formatDoctorReport(report);
|
|
@@ -6078,7 +6429,6 @@ import * as fs5 from "fs";
|
|
|
6078
6429
|
import * as os3 from "os";
|
|
6079
6430
|
import * as path7 from "path";
|
|
6080
6431
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6081
|
-
var SKILL_PLUGIN_NAME = "opencode-skills";
|
|
6082
6432
|
function bundledSkillsDir() {
|
|
6083
6433
|
try {
|
|
6084
6434
|
const here = fileURLToPath2(import.meta.url);
|
|
@@ -6485,6 +6835,69 @@ function interactiveSpawnEnv(opts) {
|
|
|
6485
6835
|
...opts.effort ? { CLAUDE_CODE_EFFORT_LEVEL: opts.effort } : {}
|
|
6486
6836
|
};
|
|
6487
6837
|
}
|
|
6838
|
+
function usageInputSide(usage) {
|
|
6839
|
+
return (usage?.input_tokens ?? 0) + (usage?.cache_read_input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0);
|
|
6840
|
+
}
|
|
6841
|
+
function isApiCallRecord(rec) {
|
|
6842
|
+
if (!rec || rec.type !== "assistant" || !rec.message) return false;
|
|
6843
|
+
return usageInputSide(rec.message.usage) > 0;
|
|
6844
|
+
}
|
|
6845
|
+
var TurnUsageAccumulator = class {
|
|
6846
|
+
calls = /* @__PURE__ */ new Map();
|
|
6847
|
+
lastKey = null;
|
|
6848
|
+
/** Feed every transcript record of the turn, in file order. */
|
|
6849
|
+
add(rec) {
|
|
6850
|
+
if (!isApiCallRecord(rec)) return;
|
|
6851
|
+
const id = rec.message.id;
|
|
6852
|
+
const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
|
|
6853
|
+
this.calls.set(key, rec.message.usage);
|
|
6854
|
+
this.lastKey = key;
|
|
6855
|
+
}
|
|
6856
|
+
/** Distinct API calls seen, for diagnostics. */
|
|
6857
|
+
get callCount() {
|
|
6858
|
+
return this.calls.size;
|
|
6859
|
+
}
|
|
6860
|
+
/**
|
|
6861
|
+
* The newest real call's usage, verbatim (its own `iterations` included):
|
|
6862
|
+
* the conversation's context occupancy, which is what a finish must report.
|
|
6863
|
+
* See `lastCallContextUsage` in `usage.ts`, the one convention for this.
|
|
6864
|
+
*/
|
|
6865
|
+
get lastCall() {
|
|
6866
|
+
return this.lastKey === null ? null : this.calls.get(this.lastKey) ?? null;
|
|
6867
|
+
}
|
|
6868
|
+
/**
|
|
6869
|
+
* The turn summed over DISTINCT calls: the shape and the meaning of a
|
|
6870
|
+
* headless `result` frame's usage, which is what `total_cost_usd` and
|
|
6871
|
+
* `turnStats` are about. Deliberately carries no `iterations`: that is one
|
|
6872
|
+
* response's server-side field and a sum of calls has no such list, so
|
|
6873
|
+
* `toUsage` falls back to these flat counters, while `lastCall` keeps its
|
|
6874
|
+
* own for `lastCallContextUsage` to read.
|
|
6875
|
+
*/
|
|
6876
|
+
get turnTotal() {
|
|
6877
|
+
if (this.calls.size === 0) return null;
|
|
6878
|
+
const total = {
|
|
6879
|
+
input_tokens: 0,
|
|
6880
|
+
output_tokens: 0,
|
|
6881
|
+
cache_read_input_tokens: 0,
|
|
6882
|
+
cache_creation_input_tokens: 0,
|
|
6883
|
+
cache_creation: {
|
|
6884
|
+
ephemeral_1h_input_tokens: 0,
|
|
6885
|
+
ephemeral_5m_input_tokens: 0
|
|
6886
|
+
},
|
|
6887
|
+
output_tokens_details: { thinking_tokens: 0 }
|
|
6888
|
+
};
|
|
6889
|
+
for (const u of this.calls.values()) {
|
|
6890
|
+
total.input_tokens += u.input_tokens ?? 0;
|
|
6891
|
+
total.output_tokens += u.output_tokens ?? 0;
|
|
6892
|
+
total.cache_read_input_tokens += u.cache_read_input_tokens ?? 0;
|
|
6893
|
+
total.cache_creation_input_tokens += u.cache_creation_input_tokens ?? 0;
|
|
6894
|
+
total.cache_creation.ephemeral_1h_input_tokens += u.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
6895
|
+
total.cache_creation.ephemeral_5m_input_tokens += u.cache_creation?.ephemeral_5m_input_tokens ?? 0;
|
|
6896
|
+
total.output_tokens_details.thinking_tokens += u.output_tokens_details?.thinking_tokens ?? 0;
|
|
6897
|
+
}
|
|
6898
|
+
return total;
|
|
6899
|
+
}
|
|
6900
|
+
};
|
|
6488
6901
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
6489
6902
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6490
6903
|
function resolveConfigDir(configDir) {
|
|
@@ -6679,8 +7092,8 @@ var ClaudeSession = class {
|
|
|
6679
7092
|
this.proc.terminal.write(prompt);
|
|
6680
7093
|
}
|
|
6681
7094
|
await this.submitTurn();
|
|
6682
|
-
const
|
|
6683
|
-
|
|
7095
|
+
const collected2 = [];
|
|
7096
|
+
const usage = new TurnUsageAccumulator();
|
|
6684
7097
|
let stopReason = null;
|
|
6685
7098
|
const deadline = Date.now() + timeout;
|
|
6686
7099
|
while (Date.now() < deadline) {
|
|
@@ -6704,9 +7117,9 @@ var ClaudeSession = class {
|
|
|
6704
7117
|
if (rec.type === "assistant" && rec.message) {
|
|
6705
7118
|
for (const b of rec.message.content ?? []) {
|
|
6706
7119
|
if (b?.type === "text" && typeof b.text === "string")
|
|
6707
|
-
|
|
7120
|
+
collected2.push(b.text);
|
|
6708
7121
|
}
|
|
6709
|
-
|
|
7122
|
+
usage.add(rec);
|
|
6710
7123
|
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
6711
7124
|
stopReason = rec.message.stop_reason;
|
|
6712
7125
|
}
|
|
@@ -6718,15 +7131,17 @@ var ClaudeSession = class {
|
|
|
6718
7131
|
if (!stopReason) {
|
|
6719
7132
|
throw new Error(
|
|
6720
7133
|
this.failureMessage(
|
|
6721
|
-
`turn timed out after ${timeout}ms (no terminal assistant record; collected ${
|
|
7134
|
+
`turn timed out after ${timeout}ms (no terminal assistant record; collected ${collected2.length} text block(s))`
|
|
6722
7135
|
)
|
|
6723
7136
|
);
|
|
6724
7137
|
}
|
|
6725
|
-
const
|
|
7138
|
+
const turnTotal = usage.turnTotal;
|
|
7139
|
+
const u = turnTotal ?? {};
|
|
6726
7140
|
return {
|
|
6727
|
-
text:
|
|
7141
|
+
text: collected2.join("\n").trim(),
|
|
6728
7142
|
stopReason,
|
|
6729
|
-
usage:
|
|
7143
|
+
usage: turnTotal,
|
|
7144
|
+
lastCallUsage: usage.lastCall,
|
|
6730
7145
|
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
6731
7146
|
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
6732
7147
|
ephemeral1hTokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
|
|
@@ -6739,9 +7154,15 @@ var ClaudeSession = class {
|
|
|
6739
7154
|
/**
|
|
6740
7155
|
* Like ask(), but instead of collecting the reply text it re-emits each NEW
|
|
6741
7156
|
* raw JSONL transcript line via onLine (verbatim) until a terminal
|
|
6742
|
-
* stop_reason.
|
|
6743
|
-
*
|
|
6744
|
-
*
|
|
7157
|
+
* stop_reason. Used by the opencode plugin transport shim, which feeds
|
|
7158
|
+
* these raw lines into the existing stream-json line handler unchanged.
|
|
7159
|
+
*
|
|
7160
|
+
* `usage` is the turn summed over DISTINCT API calls, which is exactly what
|
|
7161
|
+
* a headless `result` frame reports, so the shim can synthesize one and
|
|
7162
|
+
* every downstream consumer (the finish's `lastCallContextUsage`,
|
|
7163
|
+
* `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
|
|
7164
|
+
* the newest real call, returned because a caller with no stream parser
|
|
7165
|
+
* (`askOnce`) has no other way to get the context side.
|
|
6745
7166
|
*/
|
|
6746
7167
|
async tailTurn(prompt, onLine, perTurnTimeoutMs) {
|
|
6747
7168
|
if (this.aborted) throw new Error("aborted");
|
|
@@ -6754,8 +7175,7 @@ var ClaudeSession = class {
|
|
|
6754
7175
|
this.proc.terminal.write(prompt);
|
|
6755
7176
|
}
|
|
6756
7177
|
await this.submitTurn();
|
|
6757
|
-
|
|
6758
|
-
let totalOutput = 0;
|
|
7178
|
+
const usage = new TurnUsageAccumulator();
|
|
6759
7179
|
let stopReason = null;
|
|
6760
7180
|
const deadline = Date.now() + timeout;
|
|
6761
7181
|
while (Date.now() < deadline) {
|
|
@@ -6779,11 +7199,8 @@ var ClaudeSession = class {
|
|
|
6779
7199
|
} catch {
|
|
6780
7200
|
continue;
|
|
6781
7201
|
}
|
|
7202
|
+
usage.add(rec);
|
|
6782
7203
|
if (rec.type === "assistant" && rec.message) {
|
|
6783
|
-
if (rec.message.usage) {
|
|
6784
|
-
lastUsage = rec.message.usage;
|
|
6785
|
-
totalOutput += rec.message.usage.output_tokens ?? 0;
|
|
6786
|
-
}
|
|
6787
7204
|
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
6788
7205
|
stopReason = rec.message.stop_reason;
|
|
6789
7206
|
}
|
|
@@ -6792,18 +7209,6 @@ var ClaudeSession = class {
|
|
|
6792
7209
|
this.cursor = lastComplete;
|
|
6793
7210
|
if (stopReason) break;
|
|
6794
7211
|
}
|
|
6795
|
-
let usage = lastUsage;
|
|
6796
|
-
if (lastUsage) {
|
|
6797
|
-
usage = { ...lastUsage, output_tokens: totalOutput };
|
|
6798
|
-
if (Array.isArray(lastUsage.iterations) && lastUsage.iterations.length > 0) {
|
|
6799
|
-
const iters = lastUsage.iterations.map((it) => ({ ...it }));
|
|
6800
|
-
iters[iters.length - 1] = {
|
|
6801
|
-
...iters[iters.length - 1],
|
|
6802
|
-
output_tokens: totalOutput
|
|
6803
|
-
};
|
|
6804
|
-
usage.iterations = iters;
|
|
6805
|
-
}
|
|
6806
|
-
}
|
|
6807
7212
|
if (!stopReason) {
|
|
6808
7213
|
throw new Error(
|
|
6809
7214
|
this.failureMessage(
|
|
@@ -6811,7 +7216,12 @@ var ClaudeSession = class {
|
|
|
6811
7216
|
)
|
|
6812
7217
|
);
|
|
6813
7218
|
}
|
|
6814
|
-
return {
|
|
7219
|
+
return {
|
|
7220
|
+
stopReason,
|
|
7221
|
+
usage: usage.turnTotal,
|
|
7222
|
+
lastCallUsage: usage.lastCall,
|
|
7223
|
+
callCount: usage.callCount
|
|
7224
|
+
};
|
|
6815
7225
|
}
|
|
6816
7226
|
dispose() {
|
|
6817
7227
|
if (this.proc) {
|
|
@@ -6950,9 +7360,18 @@ function spawnInteractiveProcess(opts) {
|
|
|
6950
7360
|
void (async () => {
|
|
6951
7361
|
try {
|
|
6952
7362
|
await ensureStarted();
|
|
6953
|
-
const { stopReason, usage } = await session.tailTurn(userMsg, (raw) => {
|
|
7363
|
+
const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
|
|
6954
7364
|
lineEmitter.emit("line", raw);
|
|
6955
7365
|
});
|
|
7366
|
+
log.info("interactive turn usage", {
|
|
7367
|
+
apiCalls: callCount,
|
|
7368
|
+
turnOutputTokens: usage?.output_tokens,
|
|
7369
|
+
turnInputTokens: usage?.input_tokens,
|
|
7370
|
+
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
7371
|
+
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
7372
|
+
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
7373
|
+
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
|
|
7374
|
+
});
|
|
6956
7375
|
const timedOut = !stopReason;
|
|
6957
7376
|
emitResult(
|
|
6958
7377
|
timedOut ? "error_during_execution" : stopReason,
|
|
@@ -7068,6 +7487,14 @@ Subagent dispatch in this environment goes through exactly two tools: \`mcp__ope
|
|
|
7068
7487
|
- 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.
|
|
7069
7488
|
- 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.
|
|
7070
7489
|
- Do not verify a subagent's existence by searching config files \u2014 the tool's description lists the available agent types, and invalid types fail fast with a clear error.`;
|
|
7490
|
+
var BACKGROUND_SUBAGENT_HINT = `## Background subagents
|
|
7491
|
+
|
|
7492
|
+
This opencode host runs background subagents, so \`mcp__opencode_proxy__task\` takes \`background: true\`.
|
|
7493
|
+
|
|
7494
|
+
- A background dispatch returns at once with \`<task id="ses_..." state="running">\` instead of the subagent's answer. That is success, not a truncated result.
|
|
7495
|
+
- After starting one, keep working on something that does not overlap it, then end your turn normally. When the subagent finishes, opencode delivers its result into this conversation as a new message on its own. Do not sleep, poll, or ask the operator to wait.
|
|
7496
|
+
- The \`id\` in that envelope is the task_id. \`mcp__opencode_proxy__task_status\` reads a result back if the automatic delivery did not arrive (an interrupted or errored turn); \`mcp__opencode_proxy__task_cancel\` stops a background subagent you no longer want. Neither is a progress poll.
|
|
7497
|
+
- Foreground is still the default and still the right choice when you need the answer before you can continue. Use background only for work that genuinely runs alongside yours.`;
|
|
7071
7498
|
var QUESTION_PROXY_HINT = `## Asking the operator questions
|
|
7072
7499
|
|
|
7073
7500
|
Structured questions in this environment go through exactly one tool: \`mcp__opencode_proxy__question\`.
|
|
@@ -7639,9 +8066,11 @@ async function fetchLiveToolInfo(config, modelId) {
|
|
|
7639
8066
|
config.cwd
|
|
7640
8067
|
);
|
|
7641
8068
|
const question = items?.find((item) => item.id === "question");
|
|
8069
|
+
const task = items?.find((item) => item.id === "task");
|
|
7642
8070
|
return {
|
|
7643
8071
|
resolved: items !== void 0,
|
|
7644
|
-
taskDescription:
|
|
8072
|
+
taskDescription: task?.description,
|
|
8073
|
+
taskParameters: task?.parameters,
|
|
7645
8074
|
questionDescription: question?.description,
|
|
7646
8075
|
hasQuestion: !!question,
|
|
7647
8076
|
items
|
|
@@ -7684,9 +8113,14 @@ async function resolvePlanModeQuestion(config, compactionMode, loadLiveToolInfo)
|
|
|
7684
8113
|
}
|
|
7685
8114
|
return active;
|
|
7686
8115
|
}
|
|
7687
|
-
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress) {
|
|
8116
|
+
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
|
|
7688
8117
|
const timeoutOverrides = config.proxyToolTimeoutMs;
|
|
7689
8118
|
const interceptors = /* @__PURE__ */ new Map();
|
|
8119
|
+
if (tools.some((t) => t.name === TASK_STATUS_TOOL_NAME)) {
|
|
8120
|
+
const opts = { sessionKey: sessionKeyForCalls, callerSessionId };
|
|
8121
|
+
interceptors.set(TASK_STATUS_TOOL_NAME, (input) => collectBackgroundTask(input, opts));
|
|
8122
|
+
interceptors.set(TASK_CANCEL_TOOL_NAME, (input) => cancelBackgroundTask(input, opts));
|
|
8123
|
+
}
|
|
7690
8124
|
if (interceptCompress && tools.some((t) => t.name === "compress")) {
|
|
7691
8125
|
interceptors.set("compress", (input) => {
|
|
7692
8126
|
const summary = typeof input.summary === "string" ? input.summary.trim() : "";
|
|
@@ -9304,12 +9738,13 @@ var ClaudeCodeLanguageModel = class {
|
|
|
9304
9738
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
9305
9739
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
9306
9740
|
*/
|
|
9307
|
-
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress) {
|
|
9741
|
+
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
|
|
9308
9742
|
return ensureProxyServer(
|
|
9309
9743
|
this.config,
|
|
9310
9744
|
tools,
|
|
9311
9745
|
sessionKeyForCalls,
|
|
9312
|
-
interceptCompress
|
|
9746
|
+
interceptCompress,
|
|
9747
|
+
callerSessionId
|
|
9313
9748
|
);
|
|
9314
9749
|
}
|
|
9315
9750
|
/**
|
|
@@ -10092,10 +10527,12 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10092
10527
|
const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
|
|
10093
10528
|
resolved: false,
|
|
10094
10529
|
taskDescription: void 0,
|
|
10530
|
+
taskParameters: void 0,
|
|
10095
10531
|
questionDescription: void 0,
|
|
10096
10532
|
hasQuestion: false
|
|
10097
10533
|
};
|
|
10098
10534
|
let enrichedProxy = resolvedProxy;
|
|
10535
|
+
let backgroundSubagentsSupported = false;
|
|
10099
10536
|
if (enrichedProxy && taskProxyEnabled) {
|
|
10100
10537
|
enrichedProxy = overlayTaskProxyDescription(
|
|
10101
10538
|
enrichedProxy,
|
|
@@ -10110,6 +10547,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10110
10547
|
)
|
|
10111
10548
|
)
|
|
10112
10549
|
});
|
|
10550
|
+
backgroundSubagentsSupported = liveTaskSupportsBackground(
|
|
10551
|
+
liveToolInfo.taskParameters,
|
|
10552
|
+
self.config.hostApi === "v2" ? "v2" : "v1"
|
|
10553
|
+
);
|
|
10554
|
+
enrichedProxy = applyBackgroundSubagentSupport(
|
|
10555
|
+
enrichedProxy,
|
|
10556
|
+
backgroundSubagentsSupported
|
|
10557
|
+
);
|
|
10558
|
+
log.info("background subagent gate", {
|
|
10559
|
+
supported: backgroundSubagentsSupported,
|
|
10560
|
+
registryResolved: liveToolInfo.resolved,
|
|
10561
|
+
hostApi: self.config.hostApi ?? "v1",
|
|
10562
|
+
note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
|
|
10563
|
+
});
|
|
10113
10564
|
}
|
|
10114
10565
|
if (enrichedProxy && questionProxyEnabled) {
|
|
10115
10566
|
enrichedProxy = overlayQuestionProxyDescription(
|
|
@@ -10150,7 +10601,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10150
10601
|
state.proxyServer = await self.ensureProxyServer(
|
|
10151
10602
|
combinedProxyTools,
|
|
10152
10603
|
sk,
|
|
10153
|
-
pluginCompressEnabled
|
|
10604
|
+
pluginCompressEnabled,
|
|
10605
|
+
affinity
|
|
10154
10606
|
);
|
|
10155
10607
|
}
|
|
10156
10608
|
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
@@ -10171,6 +10623,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10171
10623
|
[
|
|
10172
10624
|
...extractSystemMessages(options.prompt),
|
|
10173
10625
|
...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
|
|
10626
|
+
...backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
|
|
10174
10627
|
...questionProxyActive ? [QUESTION_PROXY_HINT] : []
|
|
10175
10628
|
],
|
|
10176
10629
|
{
|