cursor-opencode-provider 0.4.3 → 0.6.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 +33 -23
- 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 +11 -1
- package/dist/context/build.js +76 -25
- package/dist/context/frozen.d.ts +29 -0
- package/dist/context/frozen.js +168 -0
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -2
- package/dist/context/paths.d.ts +34 -53
- package/dist/context/paths.js +54 -118
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +2 -2
- 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 +13 -11
- package/dist/language-model.js +957 -150
- 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/blob-reachability.d.ts +15 -0
- package/dist/protocol/blob-reachability.js +140 -0
- package/dist/protocol/blob-store.d.ts +37 -0
- package/dist/protocol/blob-store.js +115 -0
- package/dist/protocol/conversation-bind.d.ts +13 -0
- package/dist/protocol/conversation-bind.js +27 -1
- package/dist/protocol/conversation-persistence.d.ts +32 -0
- package/dist/protocol/conversation-persistence.js +504 -0
- package/dist/protocol/conversation-state.d.ts +16 -0
- package/dist/protocol/conversation-state.js +62 -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/kv.d.ts +1 -0
- package/dist/protocol/kv.js +2 -0
- package/dist/protocol/messages.d.ts +2 -0
- package/dist/protocol/messages.js +148 -8
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/switch-mode.d.ts +178 -0
- package/dist/protocol/switch-mode.js +417 -0
- package/dist/protocol/token-details.d.ts +33 -0
- package/dist/protocol/token-details.js +155 -0
- package/dist/protocol/tool-call-bridge.js +10 -5
- package/dist/protocol/tools.d.ts +44 -11
- package/dist/protocol/tools.js +171 -41
- package/dist/session.d.ts +39 -11
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +3 -0
- package/dist/usage.d.ts +42 -37
- package/dist/usage.js +201 -70
- package/dist/web-search-tool.d.ts +22 -26
- package/dist/web-search-tool.js +25 -15
- package/package.json +7 -4
|
@@ -1,14 +1,98 @@
|
|
|
1
|
+
import { type AskQuestionResultMessage, type DecodedAskQuestionQuery } from "./ask-question.js";
|
|
2
|
+
import { type DecodedGenerateImageQuery } from "./generate-image.js";
|
|
3
|
+
import { CURSOR_PLAN_STAGE_TOOL, type CreatePlanBridge, type DecodedCreatePlanQuery, createPlanApprovalQuestionInput } from "./create-plan.js";
|
|
4
|
+
import { type DecodedSwitchModeQuery, type SwitchModeBridge, type SwitchModeHostTool } from "./switch-mode.js";
|
|
1
5
|
export type InteractionQueryWireInfo = {
|
|
2
6
|
id?: number;
|
|
3
7
|
variantField?: number;
|
|
4
8
|
variantName?: string;
|
|
9
|
+
/** Raw bytes of the selected variant sub-message, when it is length-delimited. */
|
|
10
|
+
variantBytes?: Uint8Array;
|
|
5
11
|
};
|
|
6
12
|
export type HandledInteraction = {
|
|
7
13
|
id: number;
|
|
8
14
|
variantField: number;
|
|
9
15
|
variantName: string;
|
|
10
|
-
outcome: "rejected" | "acknowledged" | "failed";
|
|
11
|
-
|
|
16
|
+
outcome: "rejected" | "acknowledged" | "failed" | "bridged" | "approved";
|
|
17
|
+
/**
|
|
18
|
+
* Immediate reply for this query. Absent for bridged *synchronous* AskQuestion
|
|
19
|
+
* or SwitchMode: Cursor blocks until the host tool returns, exactly as its own
|
|
20
|
+
* CLI does while the user is choosing / approving the mode switch.
|
|
21
|
+
*/
|
|
22
|
+
reply?: Uint8Array;
|
|
23
|
+
/** Present when `outcome === "bridged"`: the question set to hand OpenCode. */
|
|
24
|
+
askQuestion?: DecodedAskQuestionQuery;
|
|
25
|
+
/**
|
|
26
|
+
* Present when SwitchMode should mutate session mode — bridged to a host
|
|
27
|
+
* tool, or approved outright when entering plan mode needs none. Absent on
|
|
28
|
+
* lifecycle soft-acks (`ack`): those reply `approved{}` without recording
|
|
29
|
+
* a mode. The caller reads `bridge` to decide whether a tool call is emitted,
|
|
30
|
+
* and `args.targetModeId` to record the active Cursor mode when present.
|
|
31
|
+
*/
|
|
32
|
+
switchMode?: DecodedSwitchModeQuery & {
|
|
33
|
+
bridge: SwitchModeBridge;
|
|
34
|
+
toolName?: SwitchModeHostTool | "question";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Present when CreatePlan needs a host tool: either a native plan stage that
|
|
38
|
+
* owns the write *and* the approval, or the emulated `question` prompt the
|
|
39
|
+
* provider raises after writing the plan itself. `planUri` is set only for
|
|
40
|
+
* the emulated path, where the provider already knows the file it wrote.
|
|
41
|
+
*/
|
|
42
|
+
createPlan?: DecodedCreatePlanQuery & {
|
|
43
|
+
bridge: CreatePlanBridge;
|
|
44
|
+
toolName: typeof CURSOR_PLAN_STAGE_TOOL | "question";
|
|
45
|
+
planUri?: string;
|
|
46
|
+
/** Filesystem path of the plan the provider just wrote (emulated path). */
|
|
47
|
+
planPath?: string;
|
|
48
|
+
questionInput?: ReturnType<typeof createPlanApprovalQuestionInput>;
|
|
49
|
+
/** The plan to show the user before they approve it (emulated path only). */
|
|
50
|
+
planReview?: string;
|
|
51
|
+
};
|
|
52
|
+
/** Present when an image generation was approved: the target to expect. */
|
|
53
|
+
generateImage?: DecodedGenerateImageQuery;
|
|
54
|
+
};
|
|
55
|
+
export type HandleInteractionQueryOptions = {
|
|
56
|
+
/**
|
|
57
|
+
* True when the host advertises `question` on this turn and tool calls are
|
|
58
|
+
* allowed, i.e. the provider can actually surface the prompt to the user.
|
|
59
|
+
*/
|
|
60
|
+
canBridgeAskQuestion?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* True when tool calls are allowed this turn (needed before advertising can
|
|
63
|
+
* be checked for plan_enter / plan_exit).
|
|
64
|
+
*/
|
|
65
|
+
allowTools?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Host tool names advertised this turn. Used to resolve SwitchMode →
|
|
68
|
+
* plan_enter / plan_exit only when the matching tool is present.
|
|
69
|
+
*/
|
|
70
|
+
advertisedTools?: ReadonlySet<string> | Iterable<string>;
|
|
71
|
+
/**
|
|
72
|
+
* True when `cursor_image_save` is advertised, i.e. a generated image can be
|
|
73
|
+
* committed to disk under OpenCode's permission. Approving generation the
|
|
74
|
+
* provider cannot then save would spend the user's Cursor quota for nothing.
|
|
75
|
+
*/
|
|
76
|
+
canSaveGeneratedImage?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Session workspace root used to place CreatePlan files via {@link hostPlansDir}
|
|
79
|
+
* (host project-config `plans/` in a git worktree, else host global data/plans).
|
|
80
|
+
*/
|
|
81
|
+
workspaceRoot?: string;
|
|
82
|
+
/** True when the advertised host plan-stage tool is available. */
|
|
83
|
+
canBridgeCreatePlan?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* True when the provider has recorded an approved Cursor plan/spec mode for
|
|
86
|
+
* this session. A written plan then ends with an execution-approval prompt,
|
|
87
|
+
* which is the transition out of planning; outside plan mode there is none.
|
|
88
|
+
*/
|
|
89
|
+
planModeActive?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Cursor unified mode currently recorded for this session. A SwitchMode to
|
|
92
|
+
* the mode already in effect needs no approval, matching Cursor's own IDE
|
|
93
|
+
* handler and keeping the plan-approval prompt from firing twice.
|
|
94
|
+
*/
|
|
95
|
+
activeCursorModeId?: string;
|
|
12
96
|
};
|
|
13
97
|
export declare class UnsupportedInteractionQueryError extends Error {
|
|
14
98
|
constructor(info: InteractionQueryWireInfo);
|
|
@@ -20,11 +104,29 @@ export declare class UnsupportedInteractionQueryError extends Error {
|
|
|
20
104
|
*/
|
|
21
105
|
export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Array): InteractionQueryWireInfo;
|
|
22
106
|
/**
|
|
23
|
-
* Build the
|
|
107
|
+
* Build the typed response required by Cursor's Run RPC.
|
|
108
|
+
*
|
|
109
|
+
* OpenCode has no Cursor UI callbacks, so UI-bound queries get a conservative
|
|
110
|
+
* headless policy (reject; ack a few no-UI cases).
|
|
24
111
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
112
|
+
* Bridged / persisted exceptions:
|
|
113
|
+
* - AskQuestion (#3) → OpenCode `question` tool
|
|
114
|
+
* - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised
|
|
115
|
+
* - CreatePlan (#7) → host-calculated plan file via hostPlansDir (project-config
|
|
116
|
+
* `plans/` in a git worktree, else host global data/plans); empty args still
|
|
117
|
+
* get the CLI empty-`plan_uri` success ack
|
|
118
|
+
* - GenerateImage (#12) → approve when `cursor_image_save` is advertised
|
|
119
|
+
*/
|
|
120
|
+
export declare function handleInteractionQuery(query: Record<string, unknown>, agentServerPayload: Uint8Array, options?: HandleInteractionQueryOptions): HandledInteraction;
|
|
121
|
+
/** `AgentClientMessage{interaction_response{ask_question_interaction_response}}`. */
|
|
122
|
+
export declare function buildAskQuestionInteractionReply(id: number, result: AskQuestionResultMessage): Uint8Array;
|
|
123
|
+
/** `AgentClientMessage{interaction_response{switch_mode_request_response}}`. */
|
|
124
|
+
export declare function buildSwitchModeInteractionReply(id: number, result: Record<string, unknown>): Uint8Array;
|
|
125
|
+
/** `AgentClientMessage{interaction_response{create_plan_request_response}}`. */
|
|
126
|
+
export declare function buildCreatePlanInteractionReply(id: number, result: Record<string, unknown>): Uint8Array;
|
|
127
|
+
/**
|
|
128
|
+
* Deferred answers for a query already acknowledged with `async`.
|
|
129
|
+
* `originalArgs` must be the server's own `AskQuestionArgs` bytes so Cursor can
|
|
130
|
+
* correlate the completion with the tool call it raised.
|
|
29
131
|
*/
|
|
30
|
-
export declare function
|
|
132
|
+
export declare function buildAsyncAskQuestionCompletion(originalToolCallId: string, originalArgs: Uint8Array, result: AskQuestionResultMessage): Uint8Array;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { encodeMessage } from "./messages.js";
|
|
2
2
|
import { readAllFields } from "./struct.js";
|
|
3
|
+
import { decodeAskQuestionQuery, MISSING_ARGS_REASON, MISSING_QUERY_REASON, } from "./ask-question.js";
|
|
4
|
+
import { decodeGenerateImageQuery, } from "./generate-image.js";
|
|
5
|
+
import { CURSOR_PLAN_STAGE_TOOL, createPlanApprovalQuestionInput, decodeCreatePlanQuery, renderPlanReviewMessage, resolveCreatePlanBridge, writeOpencodePlanFile, } from "./create-plan.js";
|
|
6
|
+
import { decodeSwitchModeQuery, MISSING_ARGS_REASON as SWITCH_MODE_MISSING_ARGS_REASON, MISSING_QUERY_REASON as SWITCH_MODE_MISSING_QUERY_REASON, resolveSwitchModeBridge, switchModeApprovedResult, } from "./switch-mode.js";
|
|
3
7
|
const HEADLESS_REASON = "This Cursor interaction requires UI approval and is not available through the OpenCode provider.";
|
|
8
|
+
/**
|
|
9
|
+
* Rejection used when AskQuestion cannot be bridged because the current
|
|
10
|
+
* OpenCode agent does not advertise `question` (subagents deny it by default,
|
|
11
|
+
* and compaction turns advertise no tools at all). Naming the actual reason
|
|
12
|
+
* keeps the model from concluding that asking is impossible in general.
|
|
13
|
+
*/
|
|
14
|
+
const ASK_QUESTION_UNAVAILABLE_REASON = "The OpenCode `question` tool is not available to the current agent, so questions "
|
|
15
|
+
+ "cannot be shown to the user this turn. State the question in your reply instead.";
|
|
4
16
|
const variantNames = {
|
|
5
17
|
2: "web_search_request_query",
|
|
6
18
|
3: "ask_question_interaction_query",
|
|
@@ -39,27 +51,37 @@ export function inspectInteractionQueryWire(agentServerPayload) {
|
|
|
39
51
|
// zero, not a malformed/missing correlation id.
|
|
40
52
|
let id = 0;
|
|
41
53
|
let variantField;
|
|
54
|
+
let variantBytes;
|
|
42
55
|
for (const field of readAllFields(queryBytes)) {
|
|
43
56
|
if (field.fn === 1 && field.wt === 0)
|
|
44
57
|
id = field.varint;
|
|
45
|
-
else if (field.wt === 2 && variantField === undefined)
|
|
58
|
+
else if (field.wt === 2 && variantField === undefined) {
|
|
46
59
|
variantField = field.fn;
|
|
60
|
+
variantBytes = field.bytes;
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
return {
|
|
49
64
|
id,
|
|
50
65
|
variantField,
|
|
51
66
|
variantName: variantField === undefined ? undefined : variantNames[variantField],
|
|
67
|
+
variantBytes,
|
|
52
68
|
};
|
|
53
69
|
}
|
|
54
70
|
/**
|
|
55
|
-
* Build the
|
|
71
|
+
* Build the typed response required by Cursor's Run RPC.
|
|
56
72
|
*
|
|
57
|
-
* OpenCode has no Cursor UI callbacks, so
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
73
|
+
* OpenCode has no Cursor UI callbacks, so UI-bound queries get a conservative
|
|
74
|
+
* headless policy (reject; ack a few no-UI cases).
|
|
75
|
+
*
|
|
76
|
+
* Bridged / persisted exceptions:
|
|
77
|
+
* - AskQuestion (#3) → OpenCode `question` tool
|
|
78
|
+
* - SwitchMode (#4) → OpenCode `plan_enter` / `plan_exit` when advertised
|
|
79
|
+
* - CreatePlan (#7) → host-calculated plan file via hostPlansDir (project-config
|
|
80
|
+
* `plans/` in a git worktree, else host global data/plans); empty args still
|
|
81
|
+
* get the CLI empty-`plan_uri` success ack
|
|
82
|
+
* - GenerateImage (#12) → approve when `cursor_image_save` is advertised
|
|
61
83
|
*/
|
|
62
|
-
export function handleInteractionQuery(query, agentServerPayload) {
|
|
84
|
+
export function handleInteractionQuery(query, agentServerPayload, options = {}) {
|
|
63
85
|
const info = inspectInteractionQueryWire(agentServerPayload);
|
|
64
86
|
if (info.id === undefined || info.variantField === undefined || !info.variantName) {
|
|
65
87
|
throw new UnsupportedInteractionQueryError(info);
|
|
@@ -67,6 +89,18 @@ export function handleInteractionQuery(query, agentServerPayload) {
|
|
|
67
89
|
if (typeof query.id === "number" && query.id !== info.id) {
|
|
68
90
|
throw new Error(`Cursor interaction query id mismatch: decoded=${query.id} wire=${info.id}`);
|
|
69
91
|
}
|
|
92
|
+
if (info.variantField === 3) {
|
|
93
|
+
return handleAskQuestionQuery(info.id, info.variantBytes, options);
|
|
94
|
+
}
|
|
95
|
+
if (info.variantField === 4) {
|
|
96
|
+
return handleSwitchModeQuery(info.id, info.variantBytes, options);
|
|
97
|
+
}
|
|
98
|
+
if (info.variantField === 7) {
|
|
99
|
+
return handleCreatePlanQuery(info.id, info.variantBytes, options);
|
|
100
|
+
}
|
|
101
|
+
if (info.variantField === 12) {
|
|
102
|
+
return handleGenerateImageQuery(info.id, info.variantBytes, options);
|
|
103
|
+
}
|
|
70
104
|
let response;
|
|
71
105
|
let outcome;
|
|
72
106
|
switch (info.variantField) {
|
|
@@ -74,30 +108,6 @@ export function handleInteractionQuery(query, agentServerPayload) {
|
|
|
74
108
|
response = { web_search_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
75
109
|
outcome = "rejected";
|
|
76
110
|
break;
|
|
77
|
-
case 3:
|
|
78
|
-
response = {
|
|
79
|
-
ask_question_interaction_response: {
|
|
80
|
-
result: { rejected: { reason: "Questions must be asked through OpenCode's question tool." } },
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
outcome = "rejected";
|
|
84
|
-
break;
|
|
85
|
-
case 4:
|
|
86
|
-
response = { switch_mode_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
87
|
-
outcome = "rejected";
|
|
88
|
-
break;
|
|
89
|
-
case 7:
|
|
90
|
-
// F14: create_plan_request_query auto-ack (Cursor CLI headless parity).
|
|
91
|
-
// Cursor CLI's headless fallback acknowledges plan creation without a
|
|
92
|
-
// client-side URI (`success` + empty `plan_uri`); the plan remains in
|
|
93
|
-
// conversation state / checkpoints. This provider mirrors that reply so
|
|
94
|
-
// the Run RPC does not deadlock waiting for UI approval. Impact: Cursor
|
|
95
|
-
// may treat the plan as accepted without an OpenCode UI confirm; local
|
|
96
|
-
// tool execution is still gated by OpenCode permissions. Do not change
|
|
97
|
-
// this to reject/prompt without CLI parity evidence.
|
|
98
|
-
response = { create_plan_request_response: { result: { success: {}, plan_uri: "" } } };
|
|
99
|
-
outcome = "acknowledged";
|
|
100
|
-
break;
|
|
101
111
|
case 8:
|
|
102
112
|
// OpenCode owns the local environment; there is no Cursor VM to create.
|
|
103
113
|
response = { setup_vm_environment_result: { success: {} } };
|
|
@@ -115,10 +125,6 @@ export function handleInteractionQuery(query, agentServerPayload) {
|
|
|
115
125
|
response = { mcp_auth_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
116
126
|
outcome = "rejected";
|
|
117
127
|
break;
|
|
118
|
-
case 12:
|
|
119
|
-
response = { generate_image_request_response: { rejected: { reason: HEADLESS_REASON } } };
|
|
120
|
-
outcome = "rejected";
|
|
121
|
-
break;
|
|
122
128
|
case 13:
|
|
123
129
|
response = {
|
|
124
130
|
replace_env_result: {
|
|
@@ -147,3 +153,257 @@ export function handleInteractionQuery(query, agentServerPayload) {
|
|
|
147
153
|
}),
|
|
148
154
|
};
|
|
149
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* AskQuestion is the one interaction OpenCode can genuinely satisfy, so it is
|
|
158
|
+
* translated instead of refused. Cursor CLI's own handler is mirrored: reject
|
|
159
|
+
* a malformed query, answer `async` immediately when the server asked for a
|
|
160
|
+
* non-blocking prompt, and otherwise hold the query open until the user picks.
|
|
161
|
+
*/
|
|
162
|
+
function handleAskQuestionQuery(id, variantBytes, options) {
|
|
163
|
+
const base = { id, variantField: 3, variantName: "ask_question_interaction_query" };
|
|
164
|
+
const reject = (reason) => ({
|
|
165
|
+
...base,
|
|
166
|
+
outcome: "rejected",
|
|
167
|
+
reply: buildAskQuestionInteractionReply(id, { rejected: { reason } }),
|
|
168
|
+
});
|
|
169
|
+
if (!variantBytes)
|
|
170
|
+
return reject(MISSING_QUERY_REASON);
|
|
171
|
+
const decoded = decodeAskQuestionQuery(variantBytes);
|
|
172
|
+
if (!decoded)
|
|
173
|
+
return reject(MISSING_ARGS_REASON);
|
|
174
|
+
if (!options.canBridgeAskQuestion)
|
|
175
|
+
return reject(ASK_QUESTION_UNAVAILABLE_REASON);
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
outcome: "bridged",
|
|
179
|
+
askQuestion: decoded,
|
|
180
|
+
// Async queries are unblocked now and answered later through a
|
|
181
|
+
// ConversationAction; synchronous ones keep Cursor waiting, which is what
|
|
182
|
+
// its CLI does while the user is still choosing.
|
|
183
|
+
reply: decoded.args.runAsync
|
|
184
|
+
? buildAskQuestionInteractionReply(id, { async: {} })
|
|
185
|
+
: undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** `AgentClientMessage{interaction_response{ask_question_interaction_response}}`. */
|
|
189
|
+
export function buildAskQuestionInteractionReply(id, result) {
|
|
190
|
+
return encodeMessage("AgentClientMessage", {
|
|
191
|
+
interaction_response: {
|
|
192
|
+
id,
|
|
193
|
+
ask_question_interaction_response: { result },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* SwitchMode is bridged to OpenCode `plan_enter` / `plan_exit` when advertised.
|
|
199
|
+
* Cursor CLI blocks until the user approves or rejects; we hold the query open
|
|
200
|
+
* until the host tool returns, then reply approved{} or rejected{reason}.
|
|
201
|
+
*/
|
|
202
|
+
function handleSwitchModeQuery(id, variantBytes, options) {
|
|
203
|
+
const base = { id, variantField: 4, variantName: "switch_mode_request_query" };
|
|
204
|
+
const reject = (reason) => ({
|
|
205
|
+
...base,
|
|
206
|
+
outcome: "rejected",
|
|
207
|
+
reply: buildSwitchModeInteractionReply(id, { rejected: { reason } }),
|
|
208
|
+
});
|
|
209
|
+
if (!variantBytes)
|
|
210
|
+
return reject(SWITCH_MODE_MISSING_QUERY_REASON);
|
|
211
|
+
const decoded = decodeSwitchModeQuery(variantBytes);
|
|
212
|
+
if (!decoded)
|
|
213
|
+
return reject(SWITCH_MODE_MISSING_ARGS_REASON);
|
|
214
|
+
const bridge = resolveSwitchModeBridge(decoded.args.targetModeId, {
|
|
215
|
+
allowTools: options.allowTools === true,
|
|
216
|
+
advertised: options.advertisedTools ?? [],
|
|
217
|
+
...(options.activeCursorModeId ? { activeModeId: options.activeCursorModeId } : {}),
|
|
218
|
+
});
|
|
219
|
+
if (bridge.kind === "reject")
|
|
220
|
+
return reject(bridge.reason);
|
|
221
|
+
// Lifecycle turn: approve on the wire so the model never sees a rejection, but
|
|
222
|
+
// do not attach switchMode — the real turn owns session mode.
|
|
223
|
+
if (bridge.kind === "ack") {
|
|
224
|
+
return {
|
|
225
|
+
...base,
|
|
226
|
+
outcome: "acknowledged",
|
|
227
|
+
reply: buildSwitchModeInteractionReply(id, switchModeApprovedResult()),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (bridge.kind === "approve") {
|
|
231
|
+
// Entering plan mode needs no host tool, so Cursor is released at once and
|
|
232
|
+
// the caller records the mode for the reminder injected on later Runs.
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
outcome: "approved",
|
|
236
|
+
switchMode: { ...decoded, bridge },
|
|
237
|
+
reply: buildSwitchModeInteractionReply(id, switchModeApprovedResult()),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
outcome: "bridged",
|
|
243
|
+
switchMode: {
|
|
244
|
+
...decoded,
|
|
245
|
+
bridge,
|
|
246
|
+
toolName: bridge.kind === "native" ? bridge.toolName : "question",
|
|
247
|
+
},
|
|
248
|
+
// Keep Cursor waiting until the host tool returns, matching CLI blocking on
|
|
249
|
+
// the mode-switch approval prompt.
|
|
250
|
+
reply: undefined,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** `AgentClientMessage{interaction_response{switch_mode_request_response}}`. */
|
|
254
|
+
export function buildSwitchModeInteractionReply(id, result) {
|
|
255
|
+
return encodeMessage("AgentClientMessage", {
|
|
256
|
+
interaction_response: {
|
|
257
|
+
id,
|
|
258
|
+
switch_mode_request_response: result,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** `AgentClientMessage{interaction_response{create_plan_request_response}}`. */
|
|
263
|
+
export function buildCreatePlanInteractionReply(id, result) {
|
|
264
|
+
return encodeMessage("AgentClientMessage", {
|
|
265
|
+
interaction_response: {
|
|
266
|
+
id,
|
|
267
|
+
create_plan_request_response: { result },
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Deferred answers for a query already acknowledged with `async`.
|
|
273
|
+
* `originalArgs` must be the server's own `AskQuestionArgs` bytes so Cursor can
|
|
274
|
+
* correlate the completion with the tool call it raised.
|
|
275
|
+
*/
|
|
276
|
+
export function buildAsyncAskQuestionCompletion(originalToolCallId, originalArgs, result) {
|
|
277
|
+
return encodeMessage("AgentClientMessage", {
|
|
278
|
+
conversation_action: {
|
|
279
|
+
async_ask_question_completion_action: {
|
|
280
|
+
original_tool_call_id: originalToolCallId,
|
|
281
|
+
original_args: originalArgs,
|
|
282
|
+
result,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* CreatePlan persistence and execution approval. Cursor CLI writes
|
|
289
|
+
* `~/.cursor/plans/*.plan.md` with YAML frontmatter; this provider writes plain
|
|
290
|
+
* markdown under the host's calculated plans dir (`hostPlansDir` / path bridge)
|
|
291
|
+
* so switching models and hosts stays coherent. Empty / missing args keep the
|
|
292
|
+
* CLI empty-`plan_uri` success ack (nothing to write).
|
|
293
|
+
*
|
|
294
|
+
* Writing the plan never asks; executing it always does. Which channel raises
|
|
295
|
+
* that prompt is resolved from the advertised catalog by
|
|
296
|
+
* {@link resolveCreatePlanBridge}, and every channel reports the same outcome:
|
|
297
|
+
* `success` means the user approved execution, `error` means the plan was
|
|
298
|
+
* written but not accepted, so the model keeps planning.
|
|
299
|
+
*/
|
|
300
|
+
function handleCreatePlanQuery(id, variantBytes, options) {
|
|
301
|
+
const base = { id, variantField: 7, variantName: "create_plan_request_query" };
|
|
302
|
+
const reply = (result) => ({
|
|
303
|
+
...base,
|
|
304
|
+
outcome: result.error ? "failed" : "acknowledged",
|
|
305
|
+
reply: encodeMessage("AgentClientMessage", {
|
|
306
|
+
interaction_response: {
|
|
307
|
+
id,
|
|
308
|
+
create_plan_request_response: { result },
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
// Missing / empty args: CLI headless fallback — success with empty plan_uri.
|
|
313
|
+
if (!variantBytes)
|
|
314
|
+
return reply({ success: {}, plan_uri: "" });
|
|
315
|
+
const decoded = decodeCreatePlanQuery(variantBytes);
|
|
316
|
+
if (!decoded)
|
|
317
|
+
return reply({ success: {}, plan_uri: "" });
|
|
318
|
+
const bridge = resolveCreatePlanBridge({
|
|
319
|
+
allowTools: options.allowTools === true,
|
|
320
|
+
canStage: options.canBridgeCreatePlan === true,
|
|
321
|
+
planModeActive: options.planModeActive === true,
|
|
322
|
+
advertised: options.advertisedTools ?? [],
|
|
323
|
+
});
|
|
324
|
+
// The host plan-stage tool owns the write and the approval prompt together.
|
|
325
|
+
if (bridge.kind === "stage") {
|
|
326
|
+
return {
|
|
327
|
+
...base,
|
|
328
|
+
outcome: "bridged",
|
|
329
|
+
createPlan: { ...decoded, bridge, toolName: CURSOR_PLAN_STAGE_TOOL },
|
|
330
|
+
reply: undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// A lifecycle turn (title generation, compaction) runs alongside the real one
|
|
334
|
+
// and would otherwise write a second, throwaway plan file for the same
|
|
335
|
+
// request. Ack it the CLI way instead of persisting anything.
|
|
336
|
+
if (options.allowTools !== true)
|
|
337
|
+
return reply({ success: {}, plan_uri: "" });
|
|
338
|
+
const workspaceRoot = options.workspaceRoot?.trim();
|
|
339
|
+
if (!workspaceRoot) {
|
|
340
|
+
return reply({
|
|
341
|
+
error: { error: "CreatePlan requires a workspace root to write the plan file" },
|
|
342
|
+
plan_uri: "",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const written = writeOpencodePlanFile(decoded.args, workspaceRoot);
|
|
346
|
+
if (!written.ok) {
|
|
347
|
+
return reply({ error: { error: written.error }, plan_uri: "" });
|
|
348
|
+
}
|
|
349
|
+
// The plan is on disk either way; only execution needs the user. Hold Cursor's
|
|
350
|
+
// query open while the host asks, exactly as the CLI blocks on its own prompt.
|
|
351
|
+
if (bridge.kind === "approve") {
|
|
352
|
+
return {
|
|
353
|
+
...base,
|
|
354
|
+
outcome: "bridged",
|
|
355
|
+
createPlan: {
|
|
356
|
+
...decoded,
|
|
357
|
+
bridge,
|
|
358
|
+
toolName: "question",
|
|
359
|
+
planUri: written.planUri,
|
|
360
|
+
planPath: written.planPath,
|
|
361
|
+
questionInput: createPlanApprovalQuestionInput(written.planPath),
|
|
362
|
+
planReview: renderPlanReviewMessage(written.markdown, written.planPath),
|
|
363
|
+
},
|
|
364
|
+
reply: undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return reply({ success: {}, plan_uri: written.planUri });
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* GenerateImage approval. Cursor renders its own confirmation in the CLI; here
|
|
371
|
+
* the meaningful gate is OpenCode's `edit` permission raised by
|
|
372
|
+
* `cursor_image_save` when the finished bytes are written, so generation itself
|
|
373
|
+
* is approved whenever the provider can actually commit the result. Approving
|
|
374
|
+
* without a commit path would spend Cursor quota on an image that then has
|
|
375
|
+
* nowhere to go.
|
|
376
|
+
*/
|
|
377
|
+
function handleGenerateImageQuery(id, variantBytes, options) {
|
|
378
|
+
const base = { id, variantField: 12, variantName: "generate_image_request_query" };
|
|
379
|
+
const reject = (reason) => ({
|
|
380
|
+
...base,
|
|
381
|
+
outcome: "rejected",
|
|
382
|
+
reply: encodeMessage("AgentClientMessage", {
|
|
383
|
+
interaction_response: { id, generate_image_request_response: { rejected: { reason } } },
|
|
384
|
+
}),
|
|
385
|
+
});
|
|
386
|
+
if (!variantBytes)
|
|
387
|
+
return reject("Missing generate image query");
|
|
388
|
+
const decoded = decodeGenerateImageQuery(variantBytes);
|
|
389
|
+
if (!decoded)
|
|
390
|
+
return reject("Missing generate image arguments");
|
|
391
|
+
if (!options.canSaveGeneratedImage) {
|
|
392
|
+
return reject("This OpenCode agent cannot save a generated image, so generating one would "
|
|
393
|
+
+ "produce nothing. Describe the image in your reply instead.");
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
...base,
|
|
397
|
+
outcome: "acknowledged",
|
|
398
|
+
generateImage: decoded,
|
|
399
|
+
reply: encodeMessage("AgentClientMessage", {
|
|
400
|
+
interaction_response: {
|
|
401
|
+
id,
|
|
402
|
+
// Cursor's CLI returns the description the user may have edited at the
|
|
403
|
+
// prompt. There is no text field on OpenCode's permission, so the
|
|
404
|
+
// model's own description is passed through unchanged.
|
|
405
|
+
generate_image_request_response: { approved: { description: decoded.description } },
|
|
406
|
+
},
|
|
407
|
+
}),
|
|
408
|
+
};
|
|
409
|
+
}
|
package/dist/protocol/kv.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { CursorSession } from "../session.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function handleKvServerMessage(ksm: Record<string, unknown>, session: CursorSession): {
|
|
18
18
|
reply: Uint8Array;
|
|
19
|
+
replyBlobBytes: number;
|
|
19
20
|
kind: "set" | "get";
|
|
20
21
|
id: number;
|
|
21
22
|
blobIdHex: string;
|
package/dist/protocol/kv.js
CHANGED
|
@@ -38,6 +38,7 @@ export function handleKvServerMessage(ksm, session) {
|
|
|
38
38
|
id,
|
|
39
39
|
blobIdHex: key,
|
|
40
40
|
found: true,
|
|
41
|
+
replyBlobBytes: 0,
|
|
41
42
|
reply: encodeMessage("AgentClientMessage", {
|
|
42
43
|
kv_client_message: { id, set_blob_result: {} },
|
|
43
44
|
}),
|
|
@@ -72,6 +73,7 @@ export function handleKvServerMessage(ksm, session) {
|
|
|
72
73
|
blobIdHex: key,
|
|
73
74
|
found,
|
|
74
75
|
echoed,
|
|
76
|
+
replyBlobBytes: blobData.length,
|
|
75
77
|
reply: encodeMessage("AgentClientMessage", {
|
|
76
78
|
kv_client_message: { id, get_blob_result: { blob_data: blobData } },
|
|
77
79
|
}),
|
|
@@ -3,6 +3,8 @@ export declare function createMessageTypes(): protobuf.Root;
|
|
|
3
3
|
export declare function getMessageTypes(): protobuf.Root;
|
|
4
4
|
export declare function encodeMessage(typeName: string, message: Record<string, unknown>): Uint8Array;
|
|
5
5
|
export declare function decodeMessage<T = Record<string, unknown>>(typeName: string, data: Uint8Array): T;
|
|
6
|
+
/** Decode without inventing absent proto defaults (used for byte-stable snapshots). */
|
|
7
|
+
export declare function decodeMessageSparse<T = Record<string, unknown>>(typeName: string, data: Uint8Array): T;
|
|
6
8
|
/**
|
|
7
9
|
* Diagnostic-only: walk AgentServerMessage.interaction_update(1).turn_ended(14)
|
|
8
10
|
* without going through our hand-written TurnEnded schema, so a `cache_write`/
|