@khalilgharbaoui/opencode-claude-code-plugin 0.11.2 → 0.12.1
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 +99 -14
- package/dist/index.d.ts +67 -19
- package/dist/index.js +473 -69
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -393,7 +393,7 @@ var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
393
393
|
"image/webp"
|
|
394
394
|
]);
|
|
395
395
|
function toImageBlock(part) {
|
|
396
|
-
const raw = part.data ?? part.url ?? part.source?.data;
|
|
396
|
+
const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
|
|
397
397
|
if (!raw) {
|
|
398
398
|
log.warn("file part without data, skipping");
|
|
399
399
|
return null;
|
|
@@ -728,6 +728,161 @@ Now continuing with the current message:
|
|
|
728
728
|
});
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
+
// src/plan-mode-question.ts
|
|
732
|
+
var QUESTION_TOOL_NAME = "question";
|
|
733
|
+
var APPROVED_EXIT_PLAN_MODE_MESSAGE = "User has approved your plan. You can now start coding. Start with updating your todo list if applicable.";
|
|
734
|
+
var REJECTED_EXIT_PLAN_MODE_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected. To tell you how to proceed, the user said:";
|
|
735
|
+
var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
736
|
+
var OPENCODE_QUESTION_RESULT_PREFIX = `User has answered your questions: "${PLAN_MODE_APPROVAL_QUESTION}"="`;
|
|
737
|
+
var OPENCODE_QUESTION_RESULT_SUFFIX = `". You can now continue with the user's answers in mind.`;
|
|
738
|
+
var KEY_SEPARATOR = "\0";
|
|
739
|
+
function isPlanModeQuestionActive(input) {
|
|
740
|
+
if (input.compactionMode) return false;
|
|
741
|
+
if (input.configured !== true) return false;
|
|
742
|
+
return input.opencodeHasQuestion;
|
|
743
|
+
}
|
|
744
|
+
var pendingQuestions = /* @__PURE__ */ new Map();
|
|
745
|
+
function pendingKey(sessionKey2, questionToolCallId) {
|
|
746
|
+
return `${sessionKey2}${KEY_SEPARATOR}${questionToolCallId}`;
|
|
747
|
+
}
|
|
748
|
+
function clearExitPlanModeQuestions(sessionKey2) {
|
|
749
|
+
const prefix = `${sessionKey2}${KEY_SEPARATOR}`;
|
|
750
|
+
for (const key of pendingQuestions.keys()) {
|
|
751
|
+
if (key.startsWith(prefix)) pendingQuestions.delete(key);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function createExitPlanModeQuestionCall(sessionKey2, exitPlanModeToolUseId, plan, questionToolCallId = `exit_plan_question_${exitPlanModeToolUseId}`) {
|
|
755
|
+
pendingQuestions.set(pendingKey(sessionKey2, questionToolCallId), exitPlanModeToolUseId);
|
|
756
|
+
return {
|
|
757
|
+
toolCallId: questionToolCallId,
|
|
758
|
+
toolName: QUESTION_TOOL_NAME,
|
|
759
|
+
input: {
|
|
760
|
+
questions: [
|
|
761
|
+
{
|
|
762
|
+
header: "Plan approval",
|
|
763
|
+
question: PLAN_MODE_APPROVAL_QUESTION,
|
|
764
|
+
options: [
|
|
765
|
+
{ label: "yes", description: "" },
|
|
766
|
+
{ label: "no", description: "" }
|
|
767
|
+
],
|
|
768
|
+
multiple: false,
|
|
769
|
+
custom: true
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
},
|
|
773
|
+
text: plan ? `
|
|
774
|
+
|
|
775
|
+
${plan}
|
|
776
|
+
` : "\n\n"
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function buildToolResultMessage(input) {
|
|
780
|
+
return JSON.stringify({
|
|
781
|
+
type: "user",
|
|
782
|
+
message: {
|
|
783
|
+
role: "user",
|
|
784
|
+
content: [
|
|
785
|
+
input.approved ? {
|
|
786
|
+
type: "tool_result",
|
|
787
|
+
tool_use_id: input.toolUseId,
|
|
788
|
+
content: APPROVED_EXIT_PLAN_MODE_MESSAGE
|
|
789
|
+
} : {
|
|
790
|
+
type: "tool_result",
|
|
791
|
+
tool_use_id: input.toolUseId,
|
|
792
|
+
content: `${REJECTED_EXIT_PLAN_MODE_PREFIX}
|
|
793
|
+
${input.feedback || "no"}`,
|
|
794
|
+
is_error: true
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function tryParseJson(text) {
|
|
801
|
+
try {
|
|
802
|
+
return JSON.parse(text);
|
|
803
|
+
} catch {
|
|
804
|
+
return text;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function unwrapToolOutput(part) {
|
|
808
|
+
const output = part?.output ?? part?.result;
|
|
809
|
+
if (typeof output === "string") return tryParseJson(output);
|
|
810
|
+
if (!output || typeof output !== "object") return output;
|
|
811
|
+
switch (output.type) {
|
|
812
|
+
case "json":
|
|
813
|
+
case "error-json":
|
|
814
|
+
return output.value;
|
|
815
|
+
case "text":
|
|
816
|
+
case "error-text":
|
|
817
|
+
return tryParseJson(String(output.value ?? ""));
|
|
818
|
+
case "execution-denied":
|
|
819
|
+
return {
|
|
820
|
+
denied: true,
|
|
821
|
+
reason: String(output.reason ?? "question rejected")
|
|
822
|
+
};
|
|
823
|
+
case "content":
|
|
824
|
+
return Array.isArray(output.value) ? output.value.map((item) => {
|
|
825
|
+
if (item?.type === "text") return item.text;
|
|
826
|
+
return JSON.stringify(item);
|
|
827
|
+
}).join("\n") : output.value;
|
|
828
|
+
default:
|
|
829
|
+
return output;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function unwrapOpencodeQuestionResult(value) {
|
|
833
|
+
if (value.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) && value.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
834
|
+
return value.slice(
|
|
835
|
+
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
836
|
+
-OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
function collectAnswerStrings(value) {
|
|
842
|
+
if (typeof value === "string") return [unwrapOpencodeQuestionResult(value)];
|
|
843
|
+
if (Array.isArray(value)) return value.flatMap(collectAnswerStrings);
|
|
844
|
+
if (!value || typeof value !== "object") return [];
|
|
845
|
+
const obj = value;
|
|
846
|
+
if (obj.denied === true) return [String(obj.reason ?? "question rejected")];
|
|
847
|
+
for (const key of ["answers", "answer", "selected", "selection", "value"]) {
|
|
848
|
+
if (key in obj) return collectAnswerStrings(obj[key]);
|
|
849
|
+
}
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
function classifyQuestionResult(part) {
|
|
853
|
+
const output = unwrapToolOutput(part);
|
|
854
|
+
const answers = collectAnswerStrings(output).map((answer) => answer.trim()).filter(Boolean);
|
|
855
|
+
if (answers.length === 1 && answers[0].toLowerCase() === "yes") {
|
|
856
|
+
return { approved: true, feedback: "" };
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
approved: false,
|
|
860
|
+
feedback: answers.length > 0 ? answers.join("\n") : "no"
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function consumeExitPlanModeQuestionResult(sessionKey2, prompt) {
|
|
864
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
865
|
+
const msg = prompt[i];
|
|
866
|
+
if (!Array.isArray(msg.content)) continue;
|
|
867
|
+
for (const part of msg.content) {
|
|
868
|
+
if (part?.type !== "tool-result" || typeof part.toolCallId !== "string") {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const key = pendingKey(sessionKey2, part.toolCallId);
|
|
872
|
+
const exitPlanModeToolUseId = pendingQuestions.get(key);
|
|
873
|
+
if (!exitPlanModeToolUseId) continue;
|
|
874
|
+
pendingQuestions.delete(key);
|
|
875
|
+
const result = classifyQuestionResult(part);
|
|
876
|
+
return buildToolResultMessage({
|
|
877
|
+
toolUseId: exitPlanModeToolUseId,
|
|
878
|
+
approved: result.approved,
|
|
879
|
+
feedback: result.feedback
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
|
|
731
886
|
// src/mcp-bridge.ts
|
|
732
887
|
import * as fs2 from "fs";
|
|
733
888
|
import * as path2 from "path";
|
|
@@ -1341,6 +1496,7 @@ function setClaudeSessionId(key, sessionId) {
|
|
|
1341
1496
|
claudeSessions.set(key, sessionId);
|
|
1342
1497
|
}
|
|
1343
1498
|
function deleteClaudeSessionId(key) {
|
|
1499
|
+
clearExitPlanModeQuestions(key);
|
|
1344
1500
|
const claudeSessionId = claudeSessions.get(key);
|
|
1345
1501
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
1346
1502
|
claudeSessions.delete(key);
|
|
@@ -2071,8 +2227,10 @@ var SERVER_NAME = "opencode_proxy";
|
|
|
2071
2227
|
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
2072
2228
|
var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2073
2229
|
var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
|
|
2074
|
-
task: 60 * 60 * 1e3
|
|
2230
|
+
task: 60 * 60 * 1e3,
|
|
2075
2231
|
// 60 min
|
|
2232
|
+
question: 30 * 60 * 1e3
|
|
2233
|
+
// 30 min
|
|
2076
2234
|
};
|
|
2077
2235
|
var MAX_PROXY_TIMEOUT_MS = 2 ** 31 - 1;
|
|
2078
2236
|
function resolveProxyCallTimeoutMs(toolName, input, overrides) {
|
|
@@ -2120,6 +2278,7 @@ function buildProxyTimeoutError(toolName, ms) {
|
|
|
2120
2278
|
var TASK_PROXY_NOTE = "This is the ONLY tool that dispatches 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 \u2014 invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
|
|
2121
2279
|
var AGENT_TYPES_HEADING = "Available agent types";
|
|
2122
2280
|
var AGENT_BLURB_LIMIT = 140;
|
|
2281
|
+
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.";
|
|
2123
2282
|
function extractAgentTypeList(liveDescription) {
|
|
2124
2283
|
const live = liveDescription?.trim();
|
|
2125
2284
|
if (!live) return void 0;
|
|
@@ -2148,6 +2307,19 @@ function overlayTaskProxyDescription(tools, liveDescription) {
|
|
|
2148
2307
|
${t.description}` } : t
|
|
2149
2308
|
);
|
|
2150
2309
|
}
|
|
2310
|
+
function overlayQuestionProxyDescription(tools, liveDescription) {
|
|
2311
|
+
const live = liveDescription?.trim();
|
|
2312
|
+
if (!live) return tools;
|
|
2313
|
+
return tools.map(
|
|
2314
|
+
(t) => t.name === "question" ? { ...t, description: `${live}
|
|
2315
|
+
|
|
2316
|
+
${QUESTION_PROXY_NOTE}` } : t
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
function filterQuestionProxyByOpencodeSupport(tools, opencodeHasQuestion) {
|
|
2320
|
+
if (opencodeHasQuestion) return tools;
|
|
2321
|
+
return tools.filter((t) => t.name !== "question");
|
|
2322
|
+
}
|
|
2151
2323
|
var DEFAULT_PROXY_TOOLS = [
|
|
2152
2324
|
{
|
|
2153
2325
|
name: "bash",
|
|
@@ -2271,6 +2443,56 @@ var DEFAULT_PROXY_TOOLS = [
|
|
|
2271
2443
|
},
|
|
2272
2444
|
required: ["description", "prompt", "subagent_type"]
|
|
2273
2445
|
}
|
|
2446
|
+
},
|
|
2447
|
+
{
|
|
2448
|
+
name: "question",
|
|
2449
|
+
description: "Ask the operator structured questions with options and receive their answers back. Routed through opencode's native `question` tool so the prompt renders as a real TUI form (with options and a custom-answer field) instead of a plain text turn. Use this when you need a decision, clarification, or preference from the operator mid-task. " + QUESTION_PROXY_NOTE,
|
|
2450
|
+
inputSchema: {
|
|
2451
|
+
type: "object",
|
|
2452
|
+
properties: {
|
|
2453
|
+
questions: {
|
|
2454
|
+
type: "array",
|
|
2455
|
+
description: "Questions to ask.",
|
|
2456
|
+
items: {
|
|
2457
|
+
type: "object",
|
|
2458
|
+
properties: {
|
|
2459
|
+
question: {
|
|
2460
|
+
type: "string",
|
|
2461
|
+
description: "Complete question."
|
|
2462
|
+
},
|
|
2463
|
+
header: {
|
|
2464
|
+
type: "string",
|
|
2465
|
+
description: "Very short label (max 30 chars)."
|
|
2466
|
+
},
|
|
2467
|
+
options: {
|
|
2468
|
+
type: "array",
|
|
2469
|
+
description: "Available choices.",
|
|
2470
|
+
items: {
|
|
2471
|
+
type: "object",
|
|
2472
|
+
properties: {
|
|
2473
|
+
label: {
|
|
2474
|
+
type: "string",
|
|
2475
|
+
description: "Display text (1-5 words, concise)."
|
|
2476
|
+
},
|
|
2477
|
+
description: {
|
|
2478
|
+
type: "string",
|
|
2479
|
+
description: "Explanation of choice."
|
|
2480
|
+
}
|
|
2481
|
+
},
|
|
2482
|
+
required: ["label", "description"]
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
multiple: {
|
|
2486
|
+
type: "boolean",
|
|
2487
|
+
description: "Allow selecting multiple choices. Defaults to false."
|
|
2488
|
+
}
|
|
2489
|
+
},
|
|
2490
|
+
required: ["question", "header", "options"]
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
},
|
|
2494
|
+
required: ["questions"]
|
|
2495
|
+
}
|
|
2274
2496
|
}
|
|
2275
2497
|
];
|
|
2276
2498
|
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides) {
|
|
@@ -2388,23 +2610,14 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2388
2610
|
if (timer) clearTimeout(timer);
|
|
2389
2611
|
pending.delete(callId);
|
|
2390
2612
|
});
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
jsonrpc: "2.0",
|
|
2394
|
-
id: requestId,
|
|
2395
|
-
result: {
|
|
2396
|
-
content: [{ type: "text", text: result.message }],
|
|
2397
|
-
isError: true
|
|
2398
|
-
}
|
|
2399
|
-
});
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2613
|
+
const text = result.kind === "error" ? result.message : result.text;
|
|
2614
|
+
const isError = result.kind === "error" || result.isError === true;
|
|
2402
2615
|
writeJson(res, {
|
|
2403
2616
|
jsonrpc: "2.0",
|
|
2404
2617
|
id: requestId,
|
|
2405
2618
|
result: {
|
|
2406
|
-
content: [{ type: "text", text
|
|
2407
|
-
isError
|
|
2619
|
+
content: [{ type: "text", text }],
|
|
2620
|
+
isError
|
|
2408
2621
|
}
|
|
2409
2622
|
});
|
|
2410
2623
|
return;
|
|
@@ -2532,7 +2745,13 @@ function disallowedToolFlags(tools) {
|
|
|
2532
2745
|
glob: ["Glob"],
|
|
2533
2746
|
grep: ["Grep"],
|
|
2534
2747
|
webfetch: ["WebFetch"],
|
|
2535
|
-
task: ["Agent"]
|
|
2748
|
+
task: ["Agent"],
|
|
2749
|
+
// `question` disables Claude Code's built-in `AskUserQuestion` so the
|
|
2750
|
+
// structured-questions path flows through opencode's native `question`
|
|
2751
|
+
// tool instead — same UI/permission/audit benefits as the other
|
|
2752
|
+
// proxies. Without this, the model can call both and the two paths
|
|
2753
|
+
// diverge (opencode's form vs the headless deny-and-render fallback).
|
|
2754
|
+
question: ["AskUserQuestion"]
|
|
2536
2755
|
};
|
|
2537
2756
|
const out = [];
|
|
2538
2757
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2923,6 +3142,14 @@ Subagent dispatch in this environment goes through exactly one tool: \`mcp__open
|
|
|
2923
3142
|
- 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.
|
|
2924
3143
|
- 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.
|
|
2925
3144
|
- 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.`;
|
|
3145
|
+
var QUESTION_PROXY_HINT = `## Asking the operator questions
|
|
3146
|
+
|
|
3147
|
+
Structured questions in this environment go through exactly one tool: \`mcp__opencode_proxy__question\`.
|
|
3148
|
+
|
|
3149
|
+
- When you need to ask the operator a question with options, call \`mcp__opencode_proxy__question\` with a \`questions\` array (each item has \`question\`, \`header\`, \`options\` of \`{label, description}\`, and optional \`multiple\`).
|
|
3150
|
+
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__question\`), then call it by its FULL name.
|
|
3151
|
+
- Do NOT call bare \`question\` \u2014 that is not a tool. Always use the full \`mcp__opencode_proxy__question\` name when invoking it.
|
|
3152
|
+
- Claude Code's built-in \`AskUserQuestion\` is disabled in this environment; the proxy is the only way to ask structured questions.`;
|
|
2926
3153
|
var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
|
|
2927
3154
|
|
|
2928
3155
|
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
@@ -3098,22 +3325,67 @@ var ClaudeCodeLanguageModel = class {
|
|
|
3098
3325
|
return out.length > 0 ? out : null;
|
|
3099
3326
|
}
|
|
3100
3327
|
/**
|
|
3101
|
-
* Live
|
|
3102
|
-
*
|
|
3103
|
-
*
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
3106
|
-
*
|
|
3107
|
-
*
|
|
3108
|
-
*
|
|
3328
|
+
* Live tool info derived from a single `client.tool.list()` fetch:
|
|
3329
|
+
*
|
|
3330
|
+
* - `taskDescription`: opencode's `task` tool description exactly as the
|
|
3331
|
+
* registry renders it for native models, including the "Available
|
|
3332
|
+
* agent types" list. Overlaid onto the static `task` proxy def so
|
|
3333
|
+
* Claude sees the same subagent catalog native models see, instead
|
|
3334
|
+
* of hunting through config files.
|
|
3335
|
+
* - `questionDescription` / `hasQuestion`: opencode's `question` tool
|
|
3336
|
+
* description and whether the registry has the entry at all. Older
|
|
3337
|
+
* builds lack it, in which case a `mcp__opencode_proxy__question`
|
|
3338
|
+
* call resolves to `⚙ invalid`; the version gate drops the def.
|
|
3339
|
+
*
|
|
3340
|
+
* Returns undefined/false when the SDK client is unavailable (direct
|
|
3341
|
+
* AI-SDK use, tests) so the static defs stand. `resolved` distinguishes
|
|
3342
|
+
* "the registry answered and has no `question` entry" from "nobody
|
|
3343
|
+
* answered": only the former is a real version-gate signal.
|
|
3109
3344
|
*/
|
|
3110
|
-
async
|
|
3345
|
+
async fetchLiveToolInfo() {
|
|
3111
3346
|
const items = await fetchOpencodeToolList(
|
|
3112
3347
|
this.config.provider,
|
|
3113
3348
|
this.modelId,
|
|
3114
3349
|
this.config.cwd
|
|
3115
3350
|
);
|
|
3116
|
-
|
|
3351
|
+
const question = items?.find((item) => item.id === "question");
|
|
3352
|
+
return {
|
|
3353
|
+
resolved: items !== void 0,
|
|
3354
|
+
taskDescription: items?.find((item) => item.id === "task")?.description,
|
|
3355
|
+
questionDescription: question?.description,
|
|
3356
|
+
hasQuestion: !!question
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
/** Share one lazy registry request within a turn without making it stale. */
|
|
3360
|
+
createLiveToolInfoLoader() {
|
|
3361
|
+
let pending;
|
|
3362
|
+
return () => {
|
|
3363
|
+
pending ??= this.fetchLiveToolInfo();
|
|
3364
|
+
return pending;
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Whether the ExitPlanMode approval bridge is live for this turn: the
|
|
3369
|
+
* operator opted in AND opencode's registry actually has the `question`
|
|
3370
|
+
* tool. Without the registry entry the emitted tool-call would render as
|
|
3371
|
+
* `⚙ invalid` and wedge the turn, so the plugin keeps the text path.
|
|
3372
|
+
*/
|
|
3373
|
+
async resolvePlanModeQuestion(compactionMode, loadLiveToolInfo = () => this.fetchLiveToolInfo()) {
|
|
3374
|
+
if (compactionMode || this.config.planModeQuestion !== true) return false;
|
|
3375
|
+
const info = await loadLiveToolInfo();
|
|
3376
|
+
const active = isPlanModeQuestionActive({
|
|
3377
|
+
configured: this.config.planModeQuestion,
|
|
3378
|
+
opencodeHasQuestion: info.hasQuestion,
|
|
3379
|
+
compactionMode
|
|
3380
|
+
});
|
|
3381
|
+
if (!active) {
|
|
3382
|
+
log.info("plan-mode question gate", {
|
|
3383
|
+
opencodeHasQuestion: info.hasQuestion,
|
|
3384
|
+
registryResolved: info.resolved,
|
|
3385
|
+
active
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
return active;
|
|
3117
3389
|
}
|
|
3118
3390
|
/**
|
|
3119
3391
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
@@ -3487,14 +3759,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
3487
3759
|
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
3488
3760
|
const includeHistoryContext = !hasExistingSession && hasPriorConversation;
|
|
3489
3761
|
const reasoningEffort = this.getReasoningEffort(options.providerOptions);
|
|
3490
|
-
const userMsg = getClaudeUserMessage(
|
|
3491
|
-
|
|
3492
|
-
includeHistoryContext,
|
|
3493
|
-
reasoningEffort
|
|
3494
|
-
);
|
|
3495
|
-
const [runtimeStatus, cliVersion] = await Promise.all([
|
|
3762
|
+
const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? getClaudeUserMessage(options.prompt, includeHistoryContext, reasoningEffort);
|
|
3763
|
+
const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
|
|
3496
3764
|
getRuntimeMcpStatus(),
|
|
3497
|
-
detectCliVersion(this.config.cliPath)
|
|
3765
|
+
detectCliVersion(this.config.cliPath),
|
|
3766
|
+
this.resolvePlanModeQuestion(compactionMode)
|
|
3498
3767
|
]);
|
|
3499
3768
|
const systemPromptFile = buildAppendedSystemPrompt(
|
|
3500
3769
|
cwd,
|
|
@@ -3583,6 +3852,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
3583
3852
|
if (block.name === "ExitPlanMode") {
|
|
3584
3853
|
const parsedInput = block.input ?? {};
|
|
3585
3854
|
const plan = parsedInput?.plan || "";
|
|
3855
|
+
if (planModeQuestionActive) {
|
|
3856
|
+
const questionCall = createExitPlanModeQuestionCall(
|
|
3857
|
+
sk,
|
|
3858
|
+
block.id,
|
|
3859
|
+
plan
|
|
3860
|
+
);
|
|
3861
|
+
responseText += questionCall.text;
|
|
3862
|
+
toolCalls.push({
|
|
3863
|
+
id: questionCall.toolCallId,
|
|
3864
|
+
name: questionCall.toolName,
|
|
3865
|
+
args: questionCall.input
|
|
3866
|
+
});
|
|
3867
|
+
continue;
|
|
3868
|
+
}
|
|
3586
3869
|
responseText += `
|
|
3587
3870
|
|
|
3588
3871
|
${plan}
|
|
@@ -3633,7 +3916,19 @@ ${plan}
|
|
|
3633
3916
|
error: String(err)
|
|
3634
3917
|
});
|
|
3635
3918
|
}
|
|
3636
|
-
|
|
3919
|
+
if (tc.name === "ExitPlanMode" && planModeQuestionActive) {
|
|
3920
|
+
const parsedInput = args;
|
|
3921
|
+
const plan = parsedInput?.plan || "";
|
|
3922
|
+
const questionCall = createExitPlanModeQuestionCall(sk, tc.id, plan);
|
|
3923
|
+
responseText += questionCall.text;
|
|
3924
|
+
toolCalls.push({
|
|
3925
|
+
id: questionCall.toolCallId,
|
|
3926
|
+
name: questionCall.toolName,
|
|
3927
|
+
args: questionCall.input
|
|
3928
|
+
});
|
|
3929
|
+
} else {
|
|
3930
|
+
toolCalls.push({ id: tc.id, name: tc.name, args });
|
|
3931
|
+
}
|
|
3637
3932
|
toolCallStreams.delete(msg.index);
|
|
3638
3933
|
}
|
|
3639
3934
|
}
|
|
@@ -3706,6 +4001,16 @@ ${plan}
|
|
|
3706
4001
|
});
|
|
3707
4002
|
}
|
|
3708
4003
|
for (const tc of result.toolCalls) {
|
|
4004
|
+
if (tc.name === QUESTION_TOOL_NAME) {
|
|
4005
|
+
content.push({
|
|
4006
|
+
type: "tool-call",
|
|
4007
|
+
toolCallId: tc.id,
|
|
4008
|
+
toolName: tc.name,
|
|
4009
|
+
input: JSON.stringify(tc.args),
|
|
4010
|
+
providerExecuted: false
|
|
4011
|
+
});
|
|
4012
|
+
continue;
|
|
4013
|
+
}
|
|
3709
4014
|
const {
|
|
3710
4015
|
name: mappedName,
|
|
3711
4016
|
input: mappedInput,
|
|
@@ -3728,11 +4033,13 @@ ${plan}
|
|
|
3728
4033
|
const usage = this.toUsage(result.usage);
|
|
3729
4034
|
return {
|
|
3730
4035
|
content,
|
|
3731
|
-
// Claude CLI's `result` message signals a fully-completed turn
|
|
3732
|
-
// tools have already been executed internally and final assistant
|
|
3733
|
-
//
|
|
3734
|
-
// loop
|
|
3735
|
-
finishReason: this.toFinishReason(
|
|
4036
|
+
// Claude CLI's `result` message normally signals a fully-completed turn:
|
|
4037
|
+
// tools have already been executed internally and final assistant text
|
|
4038
|
+
// has been produced. ExitPlanMode is the exception: we surface it as
|
|
4039
|
+
// opencode's native question tool so the outer loop must run that tool.
|
|
4040
|
+
finishReason: this.toFinishReason(
|
|
4041
|
+
result.toolCalls.some((tc) => tc.name === QUESTION_TOOL_NAME) ? "tool-calls" : "stop"
|
|
4042
|
+
),
|
|
3736
4043
|
usage,
|
|
3737
4044
|
request: { body: { text: userMsg } },
|
|
3738
4045
|
response: {
|
|
@@ -3836,13 +4143,19 @@ ${plan}
|
|
|
3836
4143
|
const hasActiveProcess = !!getActiveProcess(sk);
|
|
3837
4144
|
const includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
|
|
3838
4145
|
const reasoningEffort = this.getReasoningEffort(options.providerOptions);
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
4146
|
+
const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
|
|
4147
|
+
if (exitPlanModeQuestionResult) {
|
|
4148
|
+
log.info("sending plan approval decision to claude", { sk });
|
|
4149
|
+
}
|
|
4150
|
+
const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, reasoningEffort, {
|
|
4151
|
+
compactionMode
|
|
4152
|
+
});
|
|
3845
4153
|
const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
|
|
4154
|
+
const loadLiveToolInfo = this.createLiveToolInfoLoader();
|
|
4155
|
+
const planModeQuestionActive = await this.resolvePlanModeQuestion(
|
|
4156
|
+
compactionMode,
|
|
4157
|
+
loadLiveToolInfo
|
|
4158
|
+
);
|
|
3846
4159
|
const self = this;
|
|
3847
4160
|
const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
|
|
3848
4161
|
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
@@ -3987,26 +4300,53 @@ ${plan}
|
|
|
3987
4300
|
);
|
|
3988
4301
|
const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
|
|
3989
4302
|
const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
|
|
4303
|
+
const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
|
|
4304
|
+
const liveToolInfo = taskProxyEnabled || questionProxyEnabled ? await loadLiveToolInfo() : {
|
|
4305
|
+
resolved: false,
|
|
4306
|
+
taskDescription: void 0,
|
|
4307
|
+
questionDescription: void 0,
|
|
4308
|
+
hasQuestion: false
|
|
4309
|
+
};
|
|
3990
4310
|
let enrichedProxy = resolvedProxy;
|
|
3991
|
-
if (
|
|
3992
|
-
const liveTaskDescription = await self.fetchLiveTaskDescription();
|
|
4311
|
+
if (enrichedProxy && taskProxyEnabled) {
|
|
3993
4312
|
enrichedProxy = overlayTaskProxyDescription(
|
|
3994
|
-
|
|
3995
|
-
|
|
4313
|
+
enrichedProxy,
|
|
4314
|
+
liveToolInfo.taskDescription
|
|
3996
4315
|
);
|
|
3997
4316
|
log.info("task proxy description overlay", {
|
|
3998
|
-
applied: Boolean(
|
|
3999
|
-
liveDescriptionLength:
|
|
4317
|
+
applied: Boolean(liveToolInfo.taskDescription),
|
|
4318
|
+
liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
|
|
4000
4319
|
listsAgentTypes: Boolean(
|
|
4001
|
-
|
|
4320
|
+
liveToolInfo.taskDescription?.includes(
|
|
4321
|
+
"Available agent types"
|
|
4322
|
+
)
|
|
4002
4323
|
)
|
|
4003
4324
|
});
|
|
4004
4325
|
}
|
|
4005
|
-
|
|
4326
|
+
if (enrichedProxy && questionProxyEnabled) {
|
|
4327
|
+
enrichedProxy = overlayQuestionProxyDescription(
|
|
4328
|
+
enrichedProxy,
|
|
4329
|
+
liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
|
|
4330
|
+
);
|
|
4331
|
+
enrichedProxy = filterQuestionProxyByOpencodeSupport(
|
|
4332
|
+
enrichedProxy,
|
|
4333
|
+
liveToolInfo.hasQuestion
|
|
4334
|
+
);
|
|
4335
|
+
log.info("question proxy version gate", {
|
|
4336
|
+
opencodeHasQuestion: liveToolInfo.hasQuestion,
|
|
4337
|
+
kept: liveToolInfo.hasQuestion
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
const combinedList = [
|
|
4341
|
+
...enrichedProxy ?? [],
|
|
4342
|
+
...proxyMcpTools ?? []
|
|
4343
|
+
];
|
|
4344
|
+
const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
|
|
4006
4345
|
if (!proxyServer && combinedProxyTools) {
|
|
4007
4346
|
proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
|
|
4008
4347
|
}
|
|
4009
|
-
const
|
|
4348
|
+
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
4349
|
+
const proxyDisallowed = enrichedProxy ? disallowedToolFlags(enrichedProxy) : [];
|
|
4010
4350
|
const extraDisallowed = [];
|
|
4011
4351
|
if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
|
|
4012
4352
|
const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
|
|
@@ -4021,7 +4361,8 @@ ${plan}
|
|
|
4021
4361
|
self.config.multiStepContinuation !== false,
|
|
4022
4362
|
[
|
|
4023
4363
|
...extractSystemMessages(options.prompt),
|
|
4024
|
-
...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : []
|
|
4364
|
+
...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
|
|
4365
|
+
...questionProxyActive ? [QUESTION_PROXY_HINT] : []
|
|
4025
4366
|
]
|
|
4026
4367
|
);
|
|
4027
4368
|
cliArgs = buildCliArgs({
|
|
@@ -4253,6 +4594,37 @@ ${plan}
|
|
|
4253
4594
|
} catch {
|
|
4254
4595
|
}
|
|
4255
4596
|
};
|
|
4597
|
+
const finishWithExitPlanQuestion = (call) => {
|
|
4598
|
+
if (controllerClosed) return;
|
|
4599
|
+
endTextBlock();
|
|
4600
|
+
controller.enqueue({
|
|
4601
|
+
type: "tool-input-start",
|
|
4602
|
+
id: call.toolCallId,
|
|
4603
|
+
toolName: call.toolName,
|
|
4604
|
+
providerExecuted: false
|
|
4605
|
+
});
|
|
4606
|
+
controller.enqueue({
|
|
4607
|
+
type: "tool-call",
|
|
4608
|
+
toolCallId: call.toolCallId,
|
|
4609
|
+
toolName: call.toolName,
|
|
4610
|
+
input: JSON.stringify(call.input),
|
|
4611
|
+
providerExecuted: false
|
|
4612
|
+
});
|
|
4613
|
+
controller.enqueue({
|
|
4614
|
+
type: "finish",
|
|
4615
|
+
finishReason: toFinishReason("tool-calls"),
|
|
4616
|
+
usage: toUsage(resultMeta.usage),
|
|
4617
|
+
providerMetadata: {
|
|
4618
|
+
"claude-code": resultMeta
|
|
4619
|
+
}
|
|
4620
|
+
});
|
|
4621
|
+
controllerClosed = true;
|
|
4622
|
+
cleanupTurn();
|
|
4623
|
+
try {
|
|
4624
|
+
controller.close();
|
|
4625
|
+
} catch {
|
|
4626
|
+
}
|
|
4627
|
+
};
|
|
4256
4628
|
const drainNow = () => {
|
|
4257
4629
|
if (drainTimer) {
|
|
4258
4630
|
clearTimeout(drainTimer);
|
|
@@ -4578,6 +4950,21 @@ ${plan}
|
|
|
4578
4950
|
endTextBlock();
|
|
4579
4951
|
} else if (tc.name === "ExitPlanMode") {
|
|
4580
4952
|
const plan = parsedInput?.plan || "";
|
|
4953
|
+
if (planModeQuestionActive) {
|
|
4954
|
+
const questionCall = createExitPlanModeQuestionCall(
|
|
4955
|
+
sk,
|
|
4956
|
+
tc.id,
|
|
4957
|
+
plan
|
|
4958
|
+
);
|
|
4959
|
+
const planId2 = startTextBlock();
|
|
4960
|
+
controller.enqueue({
|
|
4961
|
+
type: "text-delta",
|
|
4962
|
+
id: planId2,
|
|
4963
|
+
delta: questionCall.text
|
|
4964
|
+
});
|
|
4965
|
+
finishWithExitPlanQuestion(questionCall);
|
|
4966
|
+
return;
|
|
4967
|
+
}
|
|
4581
4968
|
const planId = startTextBlock();
|
|
4582
4969
|
controller.enqueue({
|
|
4583
4970
|
type: "text-delta",
|
|
@@ -4744,6 +5131,21 @@ ${plan}
|
|
|
4744
5131
|
endTextBlock();
|
|
4745
5132
|
} else if (block.name === "ExitPlanMode") {
|
|
4746
5133
|
const plan = parsedInput?.plan || "";
|
|
5134
|
+
if (planModeQuestionActive) {
|
|
5135
|
+
const questionCall = createExitPlanModeQuestionCall(
|
|
5136
|
+
sk,
|
|
5137
|
+
block.id,
|
|
5138
|
+
plan
|
|
5139
|
+
);
|
|
5140
|
+
const planId2 = startTextBlock();
|
|
5141
|
+
controller.enqueue({
|
|
5142
|
+
type: "text-delta",
|
|
5143
|
+
id: planId2,
|
|
5144
|
+
delta: questionCall.text
|
|
5145
|
+
});
|
|
5146
|
+
finishWithExitPlanQuestion(questionCall);
|
|
5147
|
+
return;
|
|
5148
|
+
}
|
|
4747
5149
|
const planId = startTextBlock();
|
|
4748
5150
|
controller.enqueue({
|
|
4749
5151
|
type: "text-delta",
|
|
@@ -5238,21 +5640,21 @@ var defaultModels = {
|
|
|
5238
5640
|
family: "haiku",
|
|
5239
5641
|
reasoning: false,
|
|
5240
5642
|
context: 2e5,
|
|
5241
|
-
output:
|
|
5643
|
+
output: 64e3,
|
|
5242
5644
|
cost: haikuCost,
|
|
5243
5645
|
multiplier: 1,
|
|
5244
|
-
releaseDate: "
|
|
5646
|
+
releaseDate: "2025-10-01"
|
|
5245
5647
|
}),
|
|
5246
5648
|
"claude-sonnet-4-5": defineModel({
|
|
5247
5649
|
id: "claude-sonnet-4-5",
|
|
5248
5650
|
name: "Claude Sonnet 4.5",
|
|
5249
5651
|
family: "sonnet",
|
|
5250
5652
|
reasoning: true,
|
|
5251
|
-
context:
|
|
5252
|
-
output:
|
|
5653
|
+
context: 2e5,
|
|
5654
|
+
output: 64e3,
|
|
5253
5655
|
cost: sonnetCost,
|
|
5254
5656
|
multiplier: 3,
|
|
5255
|
-
releaseDate: "2025-
|
|
5657
|
+
releaseDate: "2025-09-29"
|
|
5256
5658
|
}),
|
|
5257
5659
|
"claude-sonnet-4-6": defineModel({
|
|
5258
5660
|
id: "claude-sonnet-4-6",
|
|
@@ -5260,7 +5662,7 @@ var defaultModels = {
|
|
|
5260
5662
|
family: "sonnet",
|
|
5261
5663
|
reasoning: true,
|
|
5262
5664
|
context: 1e6,
|
|
5263
|
-
output:
|
|
5665
|
+
output: 128e3,
|
|
5264
5666
|
cost: sonnetCost,
|
|
5265
5667
|
multiplier: 3,
|
|
5266
5668
|
releaseDate: "2025-06-19"
|
|
@@ -5281,11 +5683,11 @@ var defaultModels = {
|
|
|
5281
5683
|
name: "Claude Opus 4.5",
|
|
5282
5684
|
family: "opus",
|
|
5283
5685
|
reasoning: true,
|
|
5284
|
-
context:
|
|
5285
|
-
output:
|
|
5686
|
+
context: 2e5,
|
|
5687
|
+
output: 64e3,
|
|
5286
5688
|
cost: opusCost,
|
|
5287
5689
|
multiplier: 5,
|
|
5288
|
-
releaseDate: "2025-
|
|
5690
|
+
releaseDate: "2025-11-01"
|
|
5289
5691
|
}),
|
|
5290
5692
|
"claude-opus-4-6": defineModel({
|
|
5291
5693
|
id: "claude-opus-4-6",
|
|
@@ -5293,7 +5695,7 @@ var defaultModels = {
|
|
|
5293
5695
|
family: "opus",
|
|
5294
5696
|
reasoning: true,
|
|
5295
5697
|
context: 1e6,
|
|
5296
|
-
output:
|
|
5698
|
+
output: 128e3,
|
|
5297
5699
|
cost: opusCost,
|
|
5298
5700
|
multiplier: 5,
|
|
5299
5701
|
releaseDate: "2025-06-19"
|
|
@@ -5304,7 +5706,7 @@ var defaultModels = {
|
|
|
5304
5706
|
family: "opus",
|
|
5305
5707
|
reasoning: true,
|
|
5306
5708
|
context: 1e6,
|
|
5307
|
-
output:
|
|
5709
|
+
output: 128e3,
|
|
5308
5710
|
cost: opusCost,
|
|
5309
5711
|
multiplier: 5,
|
|
5310
5712
|
releaseDate: "2025-07-16"
|
|
@@ -5315,7 +5717,7 @@ var defaultModels = {
|
|
|
5315
5717
|
family: "opus",
|
|
5316
5718
|
reasoning: true,
|
|
5317
5719
|
context: 1e6,
|
|
5318
|
-
output:
|
|
5720
|
+
output: 128e3,
|
|
5319
5721
|
cost: opusCost,
|
|
5320
5722
|
multiplier: 5,
|
|
5321
5723
|
releaseDate: "2026-05-28"
|
|
@@ -5337,7 +5739,7 @@ var defaultModels = {
|
|
|
5337
5739
|
family: "fable",
|
|
5338
5740
|
reasoning: true,
|
|
5339
5741
|
context: 1e6,
|
|
5340
|
-
output:
|
|
5742
|
+
output: 128e3,
|
|
5341
5743
|
cost: fableCost,
|
|
5342
5744
|
multiplier: 10,
|
|
5343
5745
|
releaseDate: "2026-06-09"
|
|
@@ -5352,7 +5754,7 @@ var defaultModels = {
|
|
|
5352
5754
|
family: "mythos",
|
|
5353
5755
|
reasoning: true,
|
|
5354
5756
|
context: 1e6,
|
|
5355
|
-
output:
|
|
5757
|
+
output: 128e3,
|
|
5356
5758
|
cost: fableCost,
|
|
5357
5759
|
multiplier: 10,
|
|
5358
5760
|
releaseDate: "2026-06-09"
|
|
@@ -5709,6 +6111,7 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
5709
6111
|
proxyTools: stringList(firstOption(providers, "proxyTools")),
|
|
5710
6112
|
mcpServers,
|
|
5711
6113
|
interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
|
|
6114
|
+
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
5712
6115
|
anthropicApiKeyInEnv: Boolean(
|
|
5713
6116
|
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
5714
6117
|
)
|
|
@@ -5797,6 +6200,7 @@ function createClaudeCode(settings = {}) {
|
|
|
5797
6200
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
5798
6201
|
proxyTools,
|
|
5799
6202
|
proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
|
|
6203
|
+
planModeQuestion: settings.planModeQuestion ?? false,
|
|
5800
6204
|
webSearch: settings.webSearch,
|
|
5801
6205
|
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
5802
6206
|
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
|