cursor-opencode-provider 0.5.0 → 0.6.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 +22 -13
- package/dist/context/agents.d.ts +1 -1
- package/dist/context/agents.js +1 -1
- package/dist/context/auth-store.js +2 -2
- package/dist/context/build.d.ts +3 -1
- package/dist/context/build.js +4 -2
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/paths.d.ts +34 -53
- package/dist/context/paths.js +54 -118
- package/dist/context/plugins.js +1 -0
- package/dist/context/rules.js +2 -1
- package/dist/context/skills.js +1 -0
- package/dist/image-save-tool.d.ts +19 -0
- package/dist/image-save-tool.js +25 -0
- package/dist/image-save.d.ts +58 -0
- package/dist/image-save.js +189 -0
- package/dist/image-staging.d.ts +59 -0
- package/dist/image-staging.js +91 -0
- package/dist/index.d.ts +1 -1
- package/dist/language-model.d.ts +3 -2
- package/dist/language-model.js +589 -41
- package/dist/plan-execution-kickoff.d.ts +55 -0
- package/dist/plan-execution-kickoff.js +162 -0
- package/dist/plugin-opencode2.js +52 -3
- package/dist/plugin-v2.d.ts +11 -9
- package/dist/plugin-v2.js +6 -13
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +61 -5
- package/dist/pricing-data.d.ts +16 -4
- package/dist/pricing-data.js +16 -4
- package/dist/protocol/ask-question.d.ts +133 -0
- package/dist/protocol/ask-question.js +320 -0
- package/dist/protocol/conversation-bind.d.ts +1 -0
- package/dist/protocol/conversation-bind.js +3 -0
- package/dist/protocol/create-plan.d.ts +141 -0
- package/dist/protocol/create-plan.js +373 -0
- package/dist/protocol/generate-image.d.ts +54 -0
- package/dist/protocol/generate-image.js +101 -0
- package/dist/protocol/interactions.d.ts +110 -8
- package/dist/protocol/interactions.js +295 -35
- package/dist/protocol/messages.js +118 -6
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/switch-mode.d.ts +178 -0
- package/dist/protocol/switch-mode.js +417 -0
- package/dist/protocol/tool-call-bridge.js +10 -5
- package/dist/protocol/tools.d.ts +27 -11
- package/dist/protocol/tools.js +98 -42
- package/dist/usage.d.ts +2 -2
- package/dist/usage.js +2 -2
- package/dist/web-search-tool.d.ts +22 -26
- package/dist/web-search-tool.js +25 -15
- package/package.json +7 -4
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor-native SwitchMode ⇄ OpenCode `plan_enter` / `plan_exit`.
|
|
3
|
+
*
|
|
4
|
+
* Cursor raises `switch_mode_request_query` (#4) with SwitchModeArgs and blocks
|
|
5
|
+
* until the client replies approved{} or rejected{reason} (no async variant).
|
|
6
|
+
* Cursor CLI prompts the user, then flips unifiedMode and injects a
|
|
7
|
+
* `<system_reminder>` describing how to behave in that mode.
|
|
8
|
+
* Display `switch_mode_tool_call` (#25) is a separate transcript record and is
|
|
9
|
+
* not replayed as a host tool.
|
|
10
|
+
*
|
|
11
|
+
* Host mapping (advertisement-gated):
|
|
12
|
+
* - plan, spec → plan_enter
|
|
13
|
+
* - every other non-empty target → plan_exit (leave plan for OpenCode build),
|
|
14
|
+
* then inject the Cursor CLI-shaped mode reminder for that target
|
|
15
|
+
*
|
|
16
|
+
* Upstream OpenCode's plan tools are not mode flags. Each one asks the user
|
|
17
|
+
* through the Question service and, on "Yes", injects a synthetic user message
|
|
18
|
+
* carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a
|
|
19
|
+
* `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
|
|
20
|
+
* `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
|
|
21
|
+
* observable halves — the approval gate and the behavioural contract — are both
|
|
22
|
+
* reachable, so the bridge degrades instead of refusing when the host tool is
|
|
23
|
+
* missing (`plan_enter` is commented out upstream and `plan_exit` is gated
|
|
24
|
+
* behind OPENCODE_EXPERIMENTAL_PLAN_MODE + CLI client):
|
|
25
|
+
*
|
|
26
|
+
* - entering plan/spec needs no host tool at all — approve immediately and let
|
|
27
|
+
* the injected <system_reminder> carry the contract, exactly as Cursor CLI's
|
|
28
|
+
* own plan mode is prompt-enforced.
|
|
29
|
+
* - leaving plan mode is the gate the user must actually see, so it falls back
|
|
30
|
+
* to the host `question` tool and only rejects when that is absent too.
|
|
31
|
+
*
|
|
32
|
+
* Resolution is keyed solely on the advertised catalog under canonical tool
|
|
33
|
+
* names — never on host identity or model id. A compatibility layer may restate
|
|
34
|
+
* alternate host vocabulary under canonical names before the provider sees the
|
|
35
|
+
* catalog, so the bridge itself remains OpenCode-shaped.
|
|
36
|
+
*
|
|
37
|
+
* CLI reject reason for user declines: "Mode switch rejected by user"
|
|
38
|
+
* (chunk-7076/dist/ui.js onSwitchModeReject).
|
|
39
|
+
*/
|
|
40
|
+
import { type OpencodeQuestionInput } from "./ask-question.js";
|
|
41
|
+
/**
|
|
42
|
+
* `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
|
|
43
|
+
* Continuation writes an InteractionResponse, not an ExecClientMessage.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SWITCH_MODE_RESULT_FIELD = "switch_mode_request_response";
|
|
46
|
+
/** CLI verbatim when the user rejects the mode switch. */
|
|
47
|
+
export declare const USER_REJECTED_REASON = "Mode switch rejected by user";
|
|
48
|
+
export declare const MISSING_QUERY_REASON = "Missing switch-mode query";
|
|
49
|
+
export declare const MISSING_ARGS_REASON = "Missing switch-mode arguments";
|
|
50
|
+
export declare const MISSING_TARGET_REASON = "Missing targetModeId";
|
|
51
|
+
/**
|
|
52
|
+
* A turn that cannot call host tools is a lifecycle turn (title generation,
|
|
53
|
+
* compaction, summarization). OpenCode opens one alongside the real Run, and it
|
|
54
|
+
* must not mutate session state — approving there flips plan mode twice and
|
|
55
|
+
* lets the throwaway turn write its own plan file.
|
|
56
|
+
*
|
|
57
|
+
* Soft-ack with `approved{}` instead of `rejected{reason}`: a hard reject lands
|
|
58
|
+
* in the transcript and the real turn narrates "mode switches are blocked"
|
|
59
|
+
* even though the next Run will succeed. CreatePlan already soft-acks the same
|
|
60
|
+
* case; SwitchMode must match.
|
|
61
|
+
*/
|
|
62
|
+
export declare const PLAN_EXIT_UNAVAILABLE_REASON: string;
|
|
63
|
+
/** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */
|
|
64
|
+
export declare const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?";
|
|
65
|
+
export declare const SWITCH_MODE_EXIT_HEADER = "Build Agent";
|
|
66
|
+
export type SwitchModeHostTool = "plan_enter" | "plan_exit";
|
|
67
|
+
/**
|
|
68
|
+
* How a SwitchMode query is satisfied this turn.
|
|
69
|
+
*
|
|
70
|
+
* - `native` the host advertises the matching plan tool; bridge to it.
|
|
71
|
+
* - `approve` entering plan mode needs no host tool — approve immediately.
|
|
72
|
+
* - `ack` lifecycle turn (`allowTools=false`): approve on the wire without
|
|
73
|
+
* mutating session mode (same soft-ack pattern as CreatePlan).
|
|
74
|
+
* - `question` leaving plan mode falls back to the host `question` prompt.
|
|
75
|
+
* - `reject` nothing can satisfy it; `reason` names the real cause.
|
|
76
|
+
*/
|
|
77
|
+
export type SwitchModeBridge = {
|
|
78
|
+
kind: "native";
|
|
79
|
+
toolName: SwitchModeHostTool;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "approve";
|
|
82
|
+
} | {
|
|
83
|
+
kind: "ack";
|
|
84
|
+
} | {
|
|
85
|
+
kind: "question";
|
|
86
|
+
input: OpencodeQuestionInput;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "reject";
|
|
89
|
+
reason: string;
|
|
90
|
+
};
|
|
91
|
+
export type CursorSwitchModeArgs = {
|
|
92
|
+
targetModeId: string;
|
|
93
|
+
explanation: string;
|
|
94
|
+
toolCallId: string;
|
|
95
|
+
};
|
|
96
|
+
export type DecodedSwitchModeQuery = {
|
|
97
|
+
args: CursorSwitchModeArgs;
|
|
98
|
+
toolCallId: string;
|
|
99
|
+
};
|
|
100
|
+
export type SwitchModeMapping = {
|
|
101
|
+
ok: true;
|
|
102
|
+
toolName: SwitchModeHostTool;
|
|
103
|
+
} | {
|
|
104
|
+
ok: false;
|
|
105
|
+
reason: string;
|
|
106
|
+
};
|
|
107
|
+
/** Normalize Cursor mode ids for comparison. */
|
|
108
|
+
export declare function normalizeSwitchModeId(targetModeId: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* Map a Cursor unified-mode id onto an OpenCode plan enter/exit tool.
|
|
111
|
+
* Does not check advertisement — callers gate on the host catalog.
|
|
112
|
+
*
|
|
113
|
+
* plan/spec enter plan; every other non-empty target leaves plan via plan_exit
|
|
114
|
+
* (host-portable primary switch) and relies on {@link cursorModeSystemReminder}
|
|
115
|
+
* for CLI-shaped behavioral guidance.
|
|
116
|
+
*/
|
|
117
|
+
export declare function mapSwitchModeTarget(targetModeId: string): SwitchModeMapping;
|
|
118
|
+
/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
|
|
119
|
+
export declare function switchModeExitQuestionInput(): OpencodeQuestionInput;
|
|
120
|
+
/**
|
|
121
|
+
* Resolve how this SwitchMode target is satisfied, keyed only on the advertised
|
|
122
|
+
* catalog under canonical tool names.
|
|
123
|
+
*
|
|
124
|
+
* A missing plan tool is not a refusal: entering plan mode needs no host tool,
|
|
125
|
+
* and leaving it degrades to the `question` prompt so the user still approves
|
|
126
|
+
* before any execution starts.
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolveSwitchModeBridge(targetModeId: string, options: {
|
|
129
|
+
allowTools: boolean;
|
|
130
|
+
advertised: ReadonlySet<string> | Iterable<string>;
|
|
131
|
+
/** Cursor unified mode currently recorded for this session, if any. */
|
|
132
|
+
activeModeId?: string;
|
|
133
|
+
}): SwitchModeBridge;
|
|
134
|
+
/** Decode a `switch_mode_request_query` body, or undefined when unusable. */
|
|
135
|
+
export declare function decodeSwitchModeQuery(queryBytes: Uint8Array): DecodedSwitchModeQuery | undefined;
|
|
136
|
+
/** OpenCode plan_enter / plan_exit advertise empty input. */
|
|
137
|
+
export declare function switchModeToolInput(): Record<string, never>;
|
|
138
|
+
/** Shape `approved{}` / `rejected{reason}` for encodeMessage. */
|
|
139
|
+
export declare function switchModeApprovedResult(): Record<string, unknown>;
|
|
140
|
+
export declare function switchModeRejectedResult(reason: string): Record<string, unknown>;
|
|
141
|
+
/**
|
|
142
|
+
* Host tool outcome → Cursor SwitchModeRequestResponse.
|
|
143
|
+
* Question/permission declines and empty failures use the CLI user-reject string.
|
|
144
|
+
*/
|
|
145
|
+
export declare function switchModeResultFromToolOutput(output: string, isError: boolean): Record<string, unknown>;
|
|
146
|
+
/**
|
|
147
|
+
* Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated
|
|
148
|
+
* plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt
|
|
149
|
+
* keeps the model in plan mode rather than silently starting execution.
|
|
150
|
+
*/
|
|
151
|
+
export declare function switchModeResultFromQuestionOutput(output: string, isError: boolean): Record<string, unknown>;
|
|
152
|
+
/** Record the approved Cursor mode for later system-reminder injection. */
|
|
153
|
+
export declare function setActiveCursorMode(sessionKey: string | undefined, targetModeId: string, options?: {
|
|
154
|
+
bridgedPlanEntered?: boolean;
|
|
155
|
+
}): void;
|
|
156
|
+
export declare function getActiveCursorMode(sessionKey: string | undefined): string | undefined;
|
|
157
|
+
/** True while the provider has an approved Cursor plan/spec mode for this session. */
|
|
158
|
+
export declare function isCursorPlanModeActive(sessionKey: string | undefined): boolean;
|
|
159
|
+
/** True after a bridged plan/spec Run has observed plan_enter removed by the host. */
|
|
160
|
+
export declare function isBridgedCursorPlanModeActive(sessionKey: string | undefined): boolean;
|
|
161
|
+
export declare function clearActiveCursorMode(sessionKey: string | undefined): void;
|
|
162
|
+
export declare function resetActiveCursorModesForTests(): void;
|
|
163
|
+
/**
|
|
164
|
+
* Cursor CLI-shaped mode reminder for the active unified mode.
|
|
165
|
+
* Tool names are adapted to OpenCode (`task`, `question`) where the CLI names
|
|
166
|
+
* Cursor-only tools; the behavioral contract matches the CLI reminders.
|
|
167
|
+
*/
|
|
168
|
+
export declare function cursorModeSystemReminder(targetModeId: string, options?: {
|
|
169
|
+
firstTurn?: boolean;
|
|
170
|
+
planExitAdvertised?: boolean;
|
|
171
|
+
}): string | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* Consume the active-mode reminder for this OpenCode session.
|
|
174
|
+
* Marks the mode as no longer first-turn after the first successful read.
|
|
175
|
+
*/
|
|
176
|
+
export declare function takeActiveCursorModeReminder(sessionKey: string | undefined, options?: {
|
|
177
|
+
advertisedTools?: ReadonlySet<string> | Iterable<string>;
|
|
178
|
+
}): string | undefined;
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor-native SwitchMode ⇄ OpenCode `plan_enter` / `plan_exit`.
|
|
3
|
+
*
|
|
4
|
+
* Cursor raises `switch_mode_request_query` (#4) with SwitchModeArgs and blocks
|
|
5
|
+
* until the client replies approved{} or rejected{reason} (no async variant).
|
|
6
|
+
* Cursor CLI prompts the user, then flips unifiedMode and injects a
|
|
7
|
+
* `<system_reminder>` describing how to behave in that mode.
|
|
8
|
+
* Display `switch_mode_tool_call` (#25) is a separate transcript record and is
|
|
9
|
+
* not replayed as a host tool.
|
|
10
|
+
*
|
|
11
|
+
* Host mapping (advertisement-gated):
|
|
12
|
+
* - plan, spec → plan_enter
|
|
13
|
+
* - every other non-empty target → plan_exit (leave plan for OpenCode build),
|
|
14
|
+
* then inject the Cursor CLI-shaped mode reminder for that target
|
|
15
|
+
*
|
|
16
|
+
* Upstream OpenCode's plan tools are not mode flags. Each one asks the user
|
|
17
|
+
* through the Question service and, on "Yes", injects a synthetic user message
|
|
18
|
+
* carrying `agent: "plan" | "build"`, which `createUserMessage` turns into a
|
|
19
|
+
* `setAgentModel` primary-agent switch (opencode `tool/plan.ts`,
|
|
20
|
+
* `session/prompt.ts`). A provider cannot reach `setAgentModel`, but the two
|
|
21
|
+
* observable halves — the approval gate and the behavioural contract — are both
|
|
22
|
+
* reachable, so the bridge degrades instead of refusing when the host tool is
|
|
23
|
+
* missing (`plan_enter` is commented out upstream and `plan_exit` is gated
|
|
24
|
+
* behind OPENCODE_EXPERIMENTAL_PLAN_MODE + CLI client):
|
|
25
|
+
*
|
|
26
|
+
* - entering plan/spec needs no host tool at all — approve immediately and let
|
|
27
|
+
* the injected <system_reminder> carry the contract, exactly as Cursor CLI's
|
|
28
|
+
* own plan mode is prompt-enforced.
|
|
29
|
+
* - leaving plan mode is the gate the user must actually see, so it falls back
|
|
30
|
+
* to the host `question` tool and only rejects when that is absent too.
|
|
31
|
+
*
|
|
32
|
+
* Resolution is keyed solely on the advertised catalog under canonical tool
|
|
33
|
+
* names — never on host identity or model id. A compatibility layer may restate
|
|
34
|
+
* alternate host vocabulary under canonical names before the provider sees the
|
|
35
|
+
* catalog, so the bridge itself remains OpenCode-shaped.
|
|
36
|
+
*
|
|
37
|
+
* CLI reject reason for user declines: "Mode switch rejected by user"
|
|
38
|
+
* (chunk-7076/dist/ui.js onSwitchModeReject).
|
|
39
|
+
*/
|
|
40
|
+
import { parseAnswerSegments, } from "./ask-question.js";
|
|
41
|
+
import { decodeMessageSparse } from "./messages.js";
|
|
42
|
+
/**
|
|
43
|
+
* `PendingExec.resultField` for a held-open SwitchMode InteractionQuery.
|
|
44
|
+
* Continuation writes an InteractionResponse, not an ExecClientMessage.
|
|
45
|
+
*/
|
|
46
|
+
export const SWITCH_MODE_RESULT_FIELD = "switch_mode_request_response";
|
|
47
|
+
/** CLI verbatim when the user rejects the mode switch. */
|
|
48
|
+
export const USER_REJECTED_REASON = "Mode switch rejected by user";
|
|
49
|
+
export const MISSING_QUERY_REASON = "Missing switch-mode query";
|
|
50
|
+
export const MISSING_ARGS_REASON = "Missing switch-mode arguments";
|
|
51
|
+
export const MISSING_TARGET_REASON = "Missing targetModeId";
|
|
52
|
+
/**
|
|
53
|
+
* A turn that cannot call host tools is a lifecycle turn (title generation,
|
|
54
|
+
* compaction, summarization). OpenCode opens one alongside the real Run, and it
|
|
55
|
+
* must not mutate session state — approving there flips plan mode twice and
|
|
56
|
+
* lets the throwaway turn write its own plan file.
|
|
57
|
+
*
|
|
58
|
+
* Soft-ack with `approved{}` instead of `rejected{reason}`: a hard reject lands
|
|
59
|
+
* in the transcript and the real turn narrates "mode switches are blocked"
|
|
60
|
+
* even though the next Run will succeed. CreatePlan already soft-acks the same
|
|
61
|
+
* case; SwitchMode must match.
|
|
62
|
+
*/
|
|
63
|
+
export const PLAN_EXIT_UNAVAILABLE_REASON = "Neither the OpenCode `plan_exit` tool nor the `question` tool is available to the " +
|
|
64
|
+
"current agent, so leaving plan mode cannot be approved this turn.";
|
|
65
|
+
/** Upstream `PlanExitTool` wording, minus a plan path the provider cannot verify. */
|
|
66
|
+
export const SWITCH_MODE_EXIT_QUESTION = "Planning is complete. Would you like to switch to the build agent and start implementing?";
|
|
67
|
+
export const SWITCH_MODE_EXIT_HEADER = "Build Agent";
|
|
68
|
+
const SWITCH_MODE_EXIT_YES = "Yes";
|
|
69
|
+
const SWITCH_MODE_EXIT_NO = "No";
|
|
70
|
+
const activeCursorModeBySession = new Map();
|
|
71
|
+
function asRecord(value) {
|
|
72
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
73
|
+
? value
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
function str(value) {
|
|
77
|
+
return typeof value === "string" ? value : "";
|
|
78
|
+
}
|
|
79
|
+
/** Normalize Cursor mode ids for comparison. */
|
|
80
|
+
export function normalizeSwitchModeId(targetModeId) {
|
|
81
|
+
return targetModeId.trim().toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Map a Cursor unified-mode id onto an OpenCode plan enter/exit tool.
|
|
85
|
+
* Does not check advertisement — callers gate on the host catalog.
|
|
86
|
+
*
|
|
87
|
+
* plan/spec enter plan; every other non-empty target leaves plan via plan_exit
|
|
88
|
+
* (host-portable primary switch) and relies on {@link cursorModeSystemReminder}
|
|
89
|
+
* for CLI-shaped behavioral guidance.
|
|
90
|
+
*/
|
|
91
|
+
export function mapSwitchModeTarget(targetModeId) {
|
|
92
|
+
const id = normalizeSwitchModeId(targetModeId);
|
|
93
|
+
if (!id)
|
|
94
|
+
return { ok: false, reason: MISSING_TARGET_REASON };
|
|
95
|
+
if (id === "plan" || id === "spec") {
|
|
96
|
+
return { ok: true, toolName: "plan_enter" };
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, toolName: "plan_exit" };
|
|
99
|
+
}
|
|
100
|
+
/** The synthetic AskQuestion item backing a question-emulated plan exit. */
|
|
101
|
+
function switchModeExitQuestionItem() {
|
|
102
|
+
return {
|
|
103
|
+
id: "switch_mode_exit",
|
|
104
|
+
prompt: SWITCH_MODE_EXIT_QUESTION,
|
|
105
|
+
options: [
|
|
106
|
+
{ id: "yes", label: SWITCH_MODE_EXIT_YES },
|
|
107
|
+
{ id: "no", label: SWITCH_MODE_EXIT_NO },
|
|
108
|
+
],
|
|
109
|
+
allowMultiple: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Host `question` input mirroring upstream `PlanExitTool`'s own prompt. */
|
|
113
|
+
export function switchModeExitQuestionInput() {
|
|
114
|
+
return {
|
|
115
|
+
questions: [
|
|
116
|
+
{
|
|
117
|
+
question: SWITCH_MODE_EXIT_QUESTION,
|
|
118
|
+
header: SWITCH_MODE_EXIT_HEADER,
|
|
119
|
+
options: [
|
|
120
|
+
{
|
|
121
|
+
label: SWITCH_MODE_EXIT_YES,
|
|
122
|
+
description: "Switch to build agent and start implementing the plan",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: SWITCH_MODE_EXIT_NO,
|
|
126
|
+
description: "Stay with plan agent to continue refining the plan",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve how this SwitchMode target is satisfied, keyed only on the advertised
|
|
135
|
+
* catalog under canonical tool names.
|
|
136
|
+
*
|
|
137
|
+
* A missing plan tool is not a refusal: entering plan mode needs no host tool,
|
|
138
|
+
* and leaving it degrades to the `question` prompt so the user still approves
|
|
139
|
+
* before any execution starts.
|
|
140
|
+
*/
|
|
141
|
+
export function resolveSwitchModeBridge(targetModeId, options) {
|
|
142
|
+
const mapped = mapSwitchModeTarget(targetModeId);
|
|
143
|
+
if (!mapped.ok)
|
|
144
|
+
return { kind: "reject", reason: mapped.reason };
|
|
145
|
+
// Lifecycle turn: soft-ack on the wire, no session mutation. A hard reject
|
|
146
|
+
// would enter the transcript and make the real turn narrate a blocked switch.
|
|
147
|
+
if (!options.allowTools)
|
|
148
|
+
return { kind: "ack" };
|
|
149
|
+
// Already in the target mode: nothing to approve. Cursor's own IDE handler
|
|
150
|
+
// auto-approves here too, and without this a model that asks to leave plan
|
|
151
|
+
// mode after the plan write already prompted would ask the user twice.
|
|
152
|
+
if (options.activeModeId
|
|
153
|
+
&& normalizeSwitchModeId(options.activeModeId) === normalizeSwitchModeId(targetModeId)) {
|
|
154
|
+
return { kind: "approve" };
|
|
155
|
+
}
|
|
156
|
+
const names = options.advertised instanceof Set
|
|
157
|
+
? options.advertised
|
|
158
|
+
: new Set(options.advertised);
|
|
159
|
+
if (names.has(mapped.toolName))
|
|
160
|
+
return { kind: "native", toolName: mapped.toolName };
|
|
161
|
+
// Entering plan mode is provider-owned: the injected <system_reminder> is the
|
|
162
|
+
// whole contract, exactly as Cursor CLI's own plan mode is prompt-enforced.
|
|
163
|
+
if (mapped.toolName === "plan_enter")
|
|
164
|
+
return { kind: "approve" };
|
|
165
|
+
// Leaving plan mode starts real work, so it keeps a user-visible gate.
|
|
166
|
+
if (names.has("question")) {
|
|
167
|
+
return { kind: "question", input: switchModeExitQuestionInput() };
|
|
168
|
+
}
|
|
169
|
+
return { kind: "reject", reason: PLAN_EXIT_UNAVAILABLE_REASON };
|
|
170
|
+
}
|
|
171
|
+
/** Decode a `switch_mode_request_query` body, or undefined when unusable. */
|
|
172
|
+
export function decodeSwitchModeQuery(queryBytes) {
|
|
173
|
+
let decoded;
|
|
174
|
+
try {
|
|
175
|
+
decoded = decodeMessageSparse("SwitchModeRequestQuery", queryBytes);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const argsRecord = asRecord(decoded.args);
|
|
181
|
+
if (!argsRecord)
|
|
182
|
+
return undefined;
|
|
183
|
+
const targetModeId = str(argsRecord.target_mode_id);
|
|
184
|
+
if (!targetModeId.trim())
|
|
185
|
+
return undefined;
|
|
186
|
+
const toolCallId = str(argsRecord.tool_call_id) || str(decoded.tool_call_id);
|
|
187
|
+
return {
|
|
188
|
+
args: {
|
|
189
|
+
targetModeId,
|
|
190
|
+
explanation: str(argsRecord.explanation),
|
|
191
|
+
toolCallId,
|
|
192
|
+
},
|
|
193
|
+
toolCallId,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** OpenCode plan_enter / plan_exit advertise empty input. */
|
|
197
|
+
export function switchModeToolInput() {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
/** Shape `approved{}` / `rejected{reason}` for encodeMessage. */
|
|
201
|
+
export function switchModeApprovedResult() {
|
|
202
|
+
return { approved: {} };
|
|
203
|
+
}
|
|
204
|
+
export function switchModeRejectedResult(reason) {
|
|
205
|
+
return { rejected: { reason } };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Host tool outcome → Cursor SwitchModeRequestResponse.
|
|
209
|
+
* Question/permission declines and empty failures use the CLI user-reject string.
|
|
210
|
+
*/
|
|
211
|
+
export function switchModeResultFromToolOutput(output, isError) {
|
|
212
|
+
if (!isError)
|
|
213
|
+
return switchModeApprovedResult();
|
|
214
|
+
const trimmed = output.trim();
|
|
215
|
+
// OpenCode plan tools throw Question.RejectedError on "No" — treat as user reject.
|
|
216
|
+
if (!trimmed
|
|
217
|
+
|| /reject/i.test(trimmed)
|
|
218
|
+
|| /denied/i.test(trimmed)
|
|
219
|
+
|| /permission/i.test(trimmed)
|
|
220
|
+
|| /cancelled|canceled|dismissed/i.test(trimmed)) {
|
|
221
|
+
return switchModeRejectedResult(USER_REJECTED_REASON);
|
|
222
|
+
}
|
|
223
|
+
return switchModeRejectedResult(trimmed);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Host `question` outcome → Cursor SwitchModeRequestResponse, for the emulated
|
|
227
|
+
* plan exit. Only an explicit "Yes" approves; an unanswered or dismissed prompt
|
|
228
|
+
* keeps the model in plan mode rather than silently starting execution.
|
|
229
|
+
*/
|
|
230
|
+
export function switchModeResultFromQuestionOutput(output, isError) {
|
|
231
|
+
if (isError)
|
|
232
|
+
return switchModeResultFromToolOutput(output, true);
|
|
233
|
+
const [segment] = parseAnswerSegments([switchModeExitQuestionItem()], output);
|
|
234
|
+
const answer = (segment ?? "").trim().toLowerCase();
|
|
235
|
+
if (answer === SWITCH_MODE_EXIT_YES.toLowerCase())
|
|
236
|
+
return switchModeApprovedResult();
|
|
237
|
+
return switchModeRejectedResult(USER_REJECTED_REASON);
|
|
238
|
+
}
|
|
239
|
+
/** Record the approved Cursor mode for later system-reminder injection. */
|
|
240
|
+
export function setActiveCursorMode(sessionKey, targetModeId, options = {}) {
|
|
241
|
+
if (!sessionKey)
|
|
242
|
+
return;
|
|
243
|
+
const modeId = normalizeSwitchModeId(targetModeId);
|
|
244
|
+
if (!modeId)
|
|
245
|
+
return;
|
|
246
|
+
activeCursorModeBySession.set(sessionKey, {
|
|
247
|
+
modeId,
|
|
248
|
+
firstTurn: true,
|
|
249
|
+
bridgedPlanEntered: options.bridgedPlanEntered === true,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
export function getActiveCursorMode(sessionKey) {
|
|
253
|
+
if (!sessionKey)
|
|
254
|
+
return undefined;
|
|
255
|
+
return activeCursorModeBySession.get(sessionKey)?.modeId;
|
|
256
|
+
}
|
|
257
|
+
/** True while the provider has an approved Cursor plan/spec mode for this session. */
|
|
258
|
+
export function isCursorPlanModeActive(sessionKey) {
|
|
259
|
+
const mode = getActiveCursorMode(sessionKey);
|
|
260
|
+
return mode === "plan" || mode === "spec";
|
|
261
|
+
}
|
|
262
|
+
/** True after a bridged plan/spec Run has observed plan_enter removed by the host. */
|
|
263
|
+
export function isBridgedCursorPlanModeActive(sessionKey) {
|
|
264
|
+
if (!sessionKey)
|
|
265
|
+
return false;
|
|
266
|
+
const state = activeCursorModeBySession.get(sessionKey);
|
|
267
|
+
return (state?.modeId === "plan" || state?.modeId === "spec") && state.bridgedPlanEntered;
|
|
268
|
+
}
|
|
269
|
+
export function clearActiveCursorMode(sessionKey) {
|
|
270
|
+
if (!sessionKey)
|
|
271
|
+
return;
|
|
272
|
+
activeCursorModeBySession.delete(sessionKey);
|
|
273
|
+
}
|
|
274
|
+
export function resetActiveCursorModesForTests() {
|
|
275
|
+
activeCursorModeBySession.clear();
|
|
276
|
+
}
|
|
277
|
+
function wrapReminder(body) {
|
|
278
|
+
return `<system_reminder>\n${body.trim()}\n</system_reminder>`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Cursor CLI-shaped mode reminder for the active unified mode.
|
|
282
|
+
* Tool names are adapted to OpenCode (`task`, `question`) where the CLI names
|
|
283
|
+
* Cursor-only tools; the behavioral contract matches the CLI reminders.
|
|
284
|
+
*/
|
|
285
|
+
export function cursorModeSystemReminder(targetModeId, options = {}) {
|
|
286
|
+
const id = normalizeSwitchModeId(targetModeId);
|
|
287
|
+
if (!id)
|
|
288
|
+
return undefined;
|
|
289
|
+
const first = options.firstTurn !== false;
|
|
290
|
+
// Without a host plan_exit, recording the plan is itself the gate: the
|
|
291
|
+
// provider persists it and then asks the user whether to start implementing.
|
|
292
|
+
// Naming an unavailable tool would strand the model.
|
|
293
|
+
const leavePlan = options.planExitAdvertised === false
|
|
294
|
+
? "record the finished plan (Cursor CreatePlan). Writing it needs no approval, and the user is then asked whether to start implementing; if they decline, refine the plan and record it again"
|
|
295
|
+
: "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode";
|
|
296
|
+
if (id === "plan" || id === "spec") {
|
|
297
|
+
return wrapReminder(first
|
|
298
|
+
? `Plan mode is active. The user does not want execution yet -- you MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction.
|
|
299
|
+
|
|
300
|
+
1. Research enough to make an accurate plan.
|
|
301
|
+
2. Before finishing, resolve decisions that would materially change the implementation path, touched files, architecture, user-visible behavior, data model, or validation strategy. If investigation cannot resolve one, ask clarifying questions in small batches (use the OpenCode \`question\` tool when available).
|
|
302
|
+
3. Do not put choices in the plan for the user to resolve. The plan must present one recommended approach, not unresolved questions or "choose A or B" options.
|
|
303
|
+
4. When ready, ${leavePlan}.
|
|
304
|
+
5. Do not execute the plan until the user confirms it.`
|
|
305
|
+
: `Plan mode is still active. You MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction.`);
|
|
306
|
+
}
|
|
307
|
+
if (id === "chat") {
|
|
308
|
+
return wrapReminder(first
|
|
309
|
+
? `Ask mode is active. The user wants you to answer questions about their codebase or coding in general. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
|
|
310
|
+
|
|
311
|
+
Your role in Ask mode:
|
|
312
|
+
|
|
313
|
+
1. Answer the user's questions comprehensively and accurately. Focus on providing clear, detailed explanations.
|
|
314
|
+
2. Use readonly tools to explore the codebase and gather information needed to answer the user's questions.
|
|
315
|
+
3. Provide code examples and references when helpful, citing specific file paths and line numbers.
|
|
316
|
+
4. If you need more information, ask the user for clarification (OpenCode \`question\` when available).
|
|
317
|
+
5. You may provide suggestions about how to implement something, but you MUST NOT actually implement it yourself.
|
|
318
|
+
6. If the user asks you to make changes or implement something, politely remind them that you're in Ask mode and can only provide information and guidance. Suggest they switch to Agent mode (OpenCode \`plan_exit\` / SwitchMode target agent) if they want you to make changes.`
|
|
319
|
+
: `Ask mode is still active. You MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).`);
|
|
320
|
+
}
|
|
321
|
+
if (id === "debug") {
|
|
322
|
+
return wrapReminder(first
|
|
323
|
+
? `You are now in **DEBUG MODE**. You must debug with **runtime evidence**.
|
|
324
|
+
|
|
325
|
+
**Why this approach:** Traditional AI agents jump to fixes claiming high confidence, but fail due to lacking runtime information. You cannot and must NOT fix bugs from code-only guesses — you need actual runtime data.
|
|
326
|
+
|
|
327
|
+
**Your systematic workflow:**
|
|
328
|
+
1. Generate 3-5 precise hypotheses about WHY the bug occurs.
|
|
329
|
+
2. Instrument code with logs to test hypotheses in parallel.
|
|
330
|
+
3. Ask the user to reproduce the bug. Conclude with a <reproduction_steps>...</reproduction_steps> block when the user must run something (mandatory unless the issue is fully confirmed fixed).
|
|
331
|
+
4. Analyze logs, accept/reject hypotheses, and only then implement a fix.
|
|
332
|
+
5. Verify the fix with runtime evidence before claiming success.
|
|
333
|
+
|
|
334
|
+
Prefer reproduction, runtime logs, and end-to-end verification over speculative refactors.`
|
|
335
|
+
: `Debug mode is still active. You must debug with **runtime evidence**.
|
|
336
|
+
|
|
337
|
+
**During fixes:** Do NOT remove instrumentation until post-fix verification logs prove success or the user explicitly asks you to remove it.
|
|
338
|
+
**Testing:** Prefer reproduction, runtime logs, and end-to-end verification; run tests when they directly exercise a hypothesis or confirm the final fix.
|
|
339
|
+
**Reproduction steps (MANDATORY):** Unless the issue is fully confirmed fixed, conclude with a <reproduction_steps>...</reproduction_steps> block when the user must reproduce.
|
|
340
|
+
**If fix failed:** Generate NEW hypotheses from different subsystems and add more instrumentation.`);
|
|
341
|
+
}
|
|
342
|
+
if (id === "multitask") {
|
|
343
|
+
return wrapReminder(`Multitask Mode is active. You are a coordinator who pushes meaningful work to asynchronous workers through the OpenCode \`task\` tool (prefer background=true when the host supports it).
|
|
344
|
+
|
|
345
|
+
- For most non-trivial requests, launch or resume one coherent worker and let that worker handle the investigation/implementation.
|
|
346
|
+
- After delegating the only coherent worker task, do not redo the same work in the foreground. Only coordinate, answer a new independent question, or synthesize after multiple workers return.
|
|
347
|
+
- NEVER await or sleep while waiting for a running subagent — end your response; you will be notified when it completes.
|
|
348
|
+
- Do NOT aggressively decompose small or medium tasks into many sibling agents. Multitask Mode is about moving substantial work out of the foreground, not maximizing parallel agents.
|
|
349
|
+
- For trivial requests (zero or one tool call), fulfill them directly and disregard these Multitask instructions.`);
|
|
350
|
+
}
|
|
351
|
+
if (id === "triage") {
|
|
352
|
+
return wrapReminder(first
|
|
353
|
+
? `Triage mode is active. Your job is to coordinate long-horizon, multi-step work by delegating to subagents and integrating their progress.
|
|
354
|
+
|
|
355
|
+
1. Break the user's task into well-scoped subtasks and launch subagents with the OpenCode \`task\` tool. Provide clear objectives and context so each subagent can make measurable progress.
|
|
356
|
+
2. Routinely inspect subagent output and synthesize results.
|
|
357
|
+
3. Decide next steps and iterate: launch additional agents, request revisions, or merge work when ready.
|
|
358
|
+
4. Throughout triage mode, maintain a global plan, document your decisions, and ensure the combined work moves the user toward their goal.`
|
|
359
|
+
: `Triage mode is still active. You must continue to coordinate long-horizon, multi-step work by delegating to subagents and integrating their progress.`);
|
|
360
|
+
}
|
|
361
|
+
if (id === "project") {
|
|
362
|
+
return wrapReminder(`You are Project Agent Mode: a long-running, high-level planner and orchestrator for complex software projects.
|
|
363
|
+
|
|
364
|
+
Your mandate is to convert user intent into a correct, high-quality implementation by delegating nearly all work to subagents and coordinating them safely over long horizons.
|
|
365
|
+
|
|
366
|
+
## Non-Negotiable Rules
|
|
367
|
+
1) Orchestrate, don't execute — you are NOT an implementer. Workspace modifications MUST be performed by OpenCode \`task\` subagents.
|
|
368
|
+
2) Task-first for everything — default to spawning subagents for research, exploration, implementation, validation, and review. Use your own read-only tools only for quick triage and synthesizing plans.
|
|
369
|
+
3) No work without clarity — if ambiguity remains, stop and ask clarifying questions (OpenCode \`question\` when available).
|
|
370
|
+
4) Phase-gated workflow — Clarify → Research → Plan → User Review → Implement → Review → Iterate → Finalize. Do not skip phases.
|
|
371
|
+
5) Externalize state — assume chat context may be condensed; keep durable progress notes so work can resume from written artifacts.`);
|
|
372
|
+
}
|
|
373
|
+
if (id === "background") {
|
|
374
|
+
return wrapReminder(`Cloud / background mode intent is active. Prefer long-running and environment/browser automation work via OpenCode \`task\` subagents (background=true when advertised) rather than blocking the foreground turn.
|
|
375
|
+
|
|
376
|
+
- Useful when the task needs browser automation, multi-service environments, or should keep running if the local session is interrupted.
|
|
377
|
+
- After launching background work, end your response promptly; do not busy-wait.
|
|
378
|
+
- For short local unit/lint/typecheck work, stay in ordinary agent execution instead.`);
|
|
379
|
+
}
|
|
380
|
+
// agent / build / edit / unknown leave-plan targets: implementation mode.
|
|
381
|
+
return wrapReminder(first
|
|
382
|
+
? `Agent mode is active (OpenCode build / agents). You have left plan mode and may implement: edit files, run tools, and execute the agreed approach.
|
|
383
|
+
|
|
384
|
+
- Prefer making progress with the advertised host tools.
|
|
385
|
+
- If the task is large or ambiguous again, switch back to plan mode (OpenCode \`plan_enter\` / SwitchMode target plan) before a large rewrite.`
|
|
386
|
+
: `Agent mode is still active. Continue implementing with the advertised host tools. Switch back to plan mode only when a new large/ambiguous design decision appears.`);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Consume the active-mode reminder for this OpenCode session.
|
|
390
|
+
* Marks the mode as no longer first-turn after the first successful read.
|
|
391
|
+
*/
|
|
392
|
+
export function takeActiveCursorModeReminder(sessionKey, options = {}) {
|
|
393
|
+
if (!sessionKey)
|
|
394
|
+
return undefined;
|
|
395
|
+
const state = activeCursorModeBySession.get(sessionKey);
|
|
396
|
+
if (!state)
|
|
397
|
+
return undefined;
|
|
398
|
+
const advertised = options.advertisedTools
|
|
399
|
+
? options.advertisedTools instanceof Set
|
|
400
|
+
? options.advertisedTools
|
|
401
|
+
: new Set(options.advertisedTools)
|
|
402
|
+
: undefined;
|
|
403
|
+
const isPlanMode = state.modeId === "plan" || state.modeId === "spec";
|
|
404
|
+
if (isPlanMode && state.bridgedPlanEntered && advertised) {
|
|
405
|
+
if (advertised.has("plan_enter")) {
|
|
406
|
+
activeCursorModeBySession.delete(sessionKey);
|
|
407
|
+
return cursorModeSystemReminder("agent", { firstTurn: true });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const reminder = cursorModeSystemReminder(state.modeId, {
|
|
411
|
+
firstTurn: state.firstTurn,
|
|
412
|
+
...(advertised ? { planExitAdvertised: advertised.has("plan_exit") } : {}),
|
|
413
|
+
});
|
|
414
|
+
if (state.firstTurn)
|
|
415
|
+
state.firstTurn = false;
|
|
416
|
+
return reminder;
|
|
417
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mapCursorArgsToOpencode, mapCursorSubagentTypeToOpenCode, mcpRealToolName } from "./tools.js";
|
|
2
2
|
import { decodeStructEntriesToJson } from "./struct.js";
|
|
3
|
+
import { mapSwitchModeTarget } from "./switch-mode.js";
|
|
3
4
|
const TODO_STATUS = {
|
|
4
5
|
0: "pending",
|
|
5
6
|
1: "pending",
|
|
@@ -127,8 +128,8 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
// create_plan_tool_call here is display-state mirroring into todowrite.
|
|
130
|
-
// Separately, InteractionQuery create_plan_request_query
|
|
131
|
-
// protocol/
|
|
131
|
+
// Separately, InteractionQuery create_plan_request_query writes a host plan
|
|
132
|
+
// file via protocol/create-plan.ts (hostPlansDir) — that write is not an
|
|
132
133
|
// execution of this display payload.
|
|
133
134
|
if (variant === "update_todos_tool_call" || variant === "create_plan_tool_call") {
|
|
134
135
|
const result = asRecord(payload.result);
|
|
@@ -237,14 +238,18 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
if (variant === "switch_mode_tool_call") {
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
// Display-path mirror of InteractionQuery #4. Same mapping as switch-mode.ts:
|
|
242
|
+
// plan/spec → plan_enter; every other non-empty target → plan_exit.
|
|
243
|
+
// Display completions stay non-replayed regardless of mapping.
|
|
244
|
+
const target = typeof args.target_mode_id === "string" ? args.target_mode_id : "";
|
|
245
|
+
const mapped = mapSwitchModeTarget(target);
|
|
242
246
|
return {
|
|
243
247
|
callId,
|
|
244
248
|
variant,
|
|
245
|
-
preferredToolName:
|
|
249
|
+
preferredToolName: mapped.ok ? mapped.toolName : "plan_exit",
|
|
246
250
|
// OpenCode plan_enter / plan_exit both advertise an empty input schema.
|
|
247
251
|
args: {},
|
|
252
|
+
bridgeable: mapped.ok,
|
|
248
253
|
};
|
|
249
254
|
}
|
|
250
255
|
// F11: display-path delete → bash `rm -f` (OpenCode has no delete tool).
|