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
|
@@ -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
|
+
}
|
|
@@ -174,6 +174,12 @@ export function createMessageTypes() {
|
|
|
174
174
|
addType(root, "AskQuestionArgs", [
|
|
175
175
|
{ id: 1, name: "title", type: "string" },
|
|
176
176
|
{ id: 2, name: "questions", type: "AskQuestionItem", repeated: true },
|
|
177
|
+
// #5/#6 drive Cursor's non-blocking AskQuestion: the client answers the
|
|
178
|
+
// interaction with `async` right away and delivers the real answers later
|
|
179
|
+
// through ConversationAction.async_ask_question_completion_action, keyed by
|
|
180
|
+
// the originating tool call id. See protocol/ask-question.ts.
|
|
181
|
+
{ id: 5, name: "run_async", type: "bool" },
|
|
182
|
+
{ id: 6, name: "async_original_tool_call_id", type: "string" },
|
|
177
183
|
]);
|
|
178
184
|
addType(root, "AskQuestionToolCall", [{ id: 1, name: "args", type: "AskQuestionArgs" }]);
|
|
179
185
|
addType(root, "FetchToolArgs", [
|
|
@@ -187,7 +193,23 @@ export function createMessageTypes() {
|
|
|
187
193
|
{ id: 2, name: "explanation", type: "string" },
|
|
188
194
|
{ id: 3, name: "tool_call_id", type: "string" },
|
|
189
195
|
]);
|
|
190
|
-
|
|
196
|
+
// Display ToolCall.result (CLI success hardcodes from_mode_id=""); distinct from
|
|
197
|
+
// InteractionResponse approved/rejected, which is the consent gate.
|
|
198
|
+
addType(root, "SwitchModeSuccess", [
|
|
199
|
+
{ id: 1, name: "from_mode_id", type: "string" },
|
|
200
|
+
{ id: 2, name: "to_mode_id", type: "string" },
|
|
201
|
+
]);
|
|
202
|
+
addType(root, "SwitchModeError", [{ id: 1, name: "error", type: "string" }]);
|
|
203
|
+
addType(root, "SwitchModeRejected", [{ id: 1, name: "reason", type: "string" }]);
|
|
204
|
+
addType(root, "SwitchModeResult", [
|
|
205
|
+
{ id: 1, name: "success", type: "SwitchModeSuccess" },
|
|
206
|
+
{ id: 2, name: "error", type: "SwitchModeError" },
|
|
207
|
+
{ id: 3, name: "rejected", type: "SwitchModeRejected" },
|
|
208
|
+
], [{ name: "result", fields: ["success", "error", "rejected"] }]);
|
|
209
|
+
addType(root, "SwitchModeToolCall", [
|
|
210
|
+
{ id: 1, name: "args", type: "SwitchModeToolArgs" },
|
|
211
|
+
{ id: 2, name: "result", type: "SwitchModeResult" },
|
|
212
|
+
]);
|
|
191
213
|
addType(root, "PiReadToolArgs", [
|
|
192
214
|
{ id: 1, name: "path", type: "string" },
|
|
193
215
|
{ id: 2, name: "offset", type: "int32" },
|
|
@@ -258,7 +280,27 @@ export function createMessageTypes() {
|
|
|
258
280
|
{ id: 2, name: "file_path", type: "string" },
|
|
259
281
|
{ id: 5, name: "reference_image_paths", type: "string", repeated: true },
|
|
260
282
|
]);
|
|
261
|
-
|
|
283
|
+
// Cursor generates the image server-side and returns it on the display
|
|
284
|
+
// tool_call frame. `image_data` is a base64 string, not bytes.
|
|
285
|
+
addType(root, "GenerateImageSuccess", [
|
|
286
|
+
{ id: 1, name: "file_path", type: "string" },
|
|
287
|
+
{ id: 2, name: "image_data", type: "string" },
|
|
288
|
+
]);
|
|
289
|
+
addType(root, "GenerateImageError", [{ id: 1, name: "error", type: "string" }]);
|
|
290
|
+
addType(root, "GenerateImageResult", [
|
|
291
|
+
{ id: 1, name: "success", type: "GenerateImageSuccess" },
|
|
292
|
+
{ id: 2, name: "error", type: "GenerateImageError" },
|
|
293
|
+
], [{ name: "result", fields: ["success", "error"] }]);
|
|
294
|
+
addType(root, "GenerateImageToolCall", [
|
|
295
|
+
{ id: 1, name: "args", type: "GenerateImageToolArgs" },
|
|
296
|
+
{ id: 2, name: "result", type: "GenerateImageResult" },
|
|
297
|
+
]);
|
|
298
|
+
// InteractionQuery keeps query bodies opaque; decoded on demand like
|
|
299
|
+
// AskQuestionInteractionQuery.
|
|
300
|
+
addType(root, "GenerateImageRequestQuery", [
|
|
301
|
+
{ id: 1, name: "args", type: "GenerateImageToolArgs" },
|
|
302
|
+
{ id: 2, name: "tool_call_id", type: "string" },
|
|
303
|
+
]);
|
|
262
304
|
addType(root, "ToolCall", [
|
|
263
305
|
{ id: 57, name: "tool_call_id", type: "string" },
|
|
264
306
|
{ id: 1, name: "shell_tool_call", type: "ShellToolCall" },
|
|
@@ -455,10 +497,20 @@ export function createMessageTypes() {
|
|
|
455
497
|
{ id: 1, name: "path", type: "string" },
|
|
456
498
|
{ id: 2, name: "error", type: "string" },
|
|
457
499
|
]);
|
|
500
|
+
// Cursor's own executor reports a refused write with this variant rather than
|
|
501
|
+
// a generic error, and its agent reads `operation` / `is_readonly` back.
|
|
502
|
+
addType(root, "WritePermissionDenied", [
|
|
503
|
+
{ id: 1, name: "path", type: "string" },
|
|
504
|
+
{ id: 2, name: "directory", type: "string" },
|
|
505
|
+
{ id: 3, name: "operation", type: "string" },
|
|
506
|
+
{ id: 4, name: "error", type: "string" },
|
|
507
|
+
{ id: 5, name: "is_readonly", type: "bool" },
|
|
508
|
+
]);
|
|
458
509
|
addType(root, "WriteResult", [
|
|
459
510
|
{ id: 1, name: "success", type: "WriteSuccess" },
|
|
511
|
+
{ id: 3, name: "permission_denied", type: "WritePermissionDenied" },
|
|
460
512
|
{ id: 5, name: "error", type: "WriteError" },
|
|
461
|
-
], [{ name: "result", fields: ["success", "error"] }]);
|
|
513
|
+
], [{ name: "result", fields: ["success", "permission_denied", "error"] }]);
|
|
462
514
|
// agent.v1 Pi exec results. Cursor places Pi requests at ExecServerMessage
|
|
463
515
|
// fields #45..#51 and their corresponding ExecClientMessage results at
|
|
464
516
|
// #46..#52 (the result fields are deliberately offset by one).
|
|
@@ -1069,11 +1121,29 @@ export function createMessageTypes() {
|
|
|
1069
1121
|
addType(root, "CancelAction", [
|
|
1070
1122
|
{ id: 1, name: "conversation_id", type: "string" },
|
|
1071
1123
|
]);
|
|
1124
|
+
// Deferred answers for an AskQuestion the client already replied to with
|
|
1125
|
+
// `async`. `original_args` is kept as raw bytes so the exact AskQuestionArgs
|
|
1126
|
+
// Cursor sent — including fields this schema does not model — is echoed back
|
|
1127
|
+
// verbatim rather than re-encoded from a lossy decode.
|
|
1128
|
+
addType(root, "AsyncAskQuestionCompletionAction", [
|
|
1129
|
+
{ id: 1, name: "original_tool_call_id", type: "string" },
|
|
1130
|
+
{ id: 2, name: "original_args", type: "bytes" },
|
|
1131
|
+
{ id: 3, name: "result", type: "AskQuestionResult" },
|
|
1132
|
+
]);
|
|
1072
1133
|
addType(root, "ConversationAction", [
|
|
1073
1134
|
{ id: 1, name: "user_message_action", type: "UserMessageAction" },
|
|
1074
1135
|
{ id: 2, name: "resume_action", type: "ResumeAction" },
|
|
1075
1136
|
{ id: 3, name: "cancel_action", type: "CancelAction" },
|
|
1076
|
-
|
|
1137
|
+
{ id: 8, name: "async_ask_question_completion_action", type: "AsyncAskQuestionCompletionAction" },
|
|
1138
|
+
], [{
|
|
1139
|
+
name: "action",
|
|
1140
|
+
fields: [
|
|
1141
|
+
"user_message_action",
|
|
1142
|
+
"resume_action",
|
|
1143
|
+
"cancel_action",
|
|
1144
|
+
"async_ask_question_completion_action",
|
|
1145
|
+
],
|
|
1146
|
+
}]);
|
|
1077
1147
|
// Seed ConversationStateStructure for turn 1 (system prompt as JSON strings
|
|
1078
1148
|
// in #1). After the first conversation_checkpoint_update we echo opaque
|
|
1079
1149
|
// server bytes instead — CLI's live structure uses blob-id bytes in #1/#8.
|
|
@@ -1172,18 +1242,42 @@ export function createMessageTypes() {
|
|
|
1172
1242
|
{ id: 2, name: "rejected", type: "WebSearchRequestRejected" },
|
|
1173
1243
|
], [{ name: "result", fields: ["approved", "rejected"] }]);
|
|
1174
1244
|
addType(root, "AskQuestionRejected", [{ id: 1, name: "reason", type: "string" }]);
|
|
1245
|
+
addType(root, "AskQuestionError", [{ id: 1, name: "error_message", type: "string" }]);
|
|
1246
|
+
addType(root, "AskQuestionAsync", []);
|
|
1247
|
+
addType(root, "AskQuestionSuccessAnswer", [
|
|
1248
|
+
{ id: 1, name: "question_id", type: "string" },
|
|
1249
|
+
{ id: 2, name: "selected_option_ids", type: "string", repeated: true },
|
|
1250
|
+
{ id: 3, name: "freeform_text", type: "string" },
|
|
1251
|
+
]);
|
|
1252
|
+
addType(root, "AskQuestionSuccess", [
|
|
1253
|
+
{ id: 1, name: "answers", type: "AskQuestionSuccessAnswer", repeated: true },
|
|
1254
|
+
]);
|
|
1175
1255
|
addType(root, "AskQuestionResult", [
|
|
1256
|
+
{ id: 1, name: "success", type: "AskQuestionSuccess" },
|
|
1257
|
+
{ id: 2, name: "error", type: "AskQuestionError" },
|
|
1176
1258
|
{ id: 3, name: "rejected", type: "AskQuestionRejected" },
|
|
1177
|
-
|
|
1259
|
+
{ id: 4, name: "async", type: "AskQuestionAsync" },
|
|
1260
|
+
], [{ name: "result", fields: ["success", "error", "rejected", "async"] }]);
|
|
1178
1261
|
addType(root, "AskQuestionInteractionResponse", [
|
|
1179
1262
|
{ id: 1, name: "result", type: "AskQuestionResult" },
|
|
1180
1263
|
]);
|
|
1264
|
+
// InteractionQuery keeps query bodies opaque (see the InteractionQuery
|
|
1265
|
+
// comment); this type decodes only the ask-question body on demand.
|
|
1266
|
+
addType(root, "AskQuestionInteractionQuery", [
|
|
1267
|
+
{ id: 1, name: "args", type: "AskQuestionArgs" },
|
|
1268
|
+
{ id: 2, name: "tool_call_id", type: "string" },
|
|
1269
|
+
]);
|
|
1181
1270
|
addType(root, "SwitchModeRequestApproved", []);
|
|
1182
1271
|
addType(root, "SwitchModeRequestRejected", [{ id: 1, name: "reason", type: "string" }]);
|
|
1183
1272
|
addType(root, "SwitchModeRequestResponse", [
|
|
1184
1273
|
{ id: 1, name: "approved", type: "SwitchModeRequestApproved" },
|
|
1185
1274
|
{ id: 2, name: "rejected", type: "SwitchModeRequestRejected" },
|
|
1186
1275
|
], [{ name: "result", fields: ["approved", "rejected"] }]);
|
|
1276
|
+
// InteractionQuery keeps query bodies opaque; decoded on demand like
|
|
1277
|
+
// AskQuestionInteractionQuery / CreatePlanRequestQuery.
|
|
1278
|
+
addType(root, "SwitchModeRequestQuery", [
|
|
1279
|
+
{ id: 1, name: "args", type: "SwitchModeToolArgs" },
|
|
1280
|
+
]);
|
|
1187
1281
|
addType(root, "CreatePlanSuccess", []);
|
|
1188
1282
|
addType(root, "CreatePlanError", [{ id: 1, name: "error", type: "string" }]);
|
|
1189
1283
|
addType(root, "CreatePlanResult", [
|
|
@@ -1194,6 +1288,12 @@ export function createMessageTypes() {
|
|
|
1194
1288
|
addType(root, "CreatePlanRequestResponse", [
|
|
1195
1289
|
{ id: 1, name: "result", type: "CreatePlanResult" },
|
|
1196
1290
|
]);
|
|
1291
|
+
// InteractionQuery keeps query bodies opaque; decoded on demand like
|
|
1292
|
+
// AskQuestionInteractionQuery / GenerateImageRequestQuery.
|
|
1293
|
+
addType(root, "CreatePlanRequestQuery", [
|
|
1294
|
+
{ id: 1, name: "args", type: "CreatePlanArgs" },
|
|
1295
|
+
{ id: 2, name: "tool_call_id", type: "string" },
|
|
1296
|
+
]);
|
|
1197
1297
|
addType(root, "SetupVmEnvironmentSuccess", []);
|
|
1198
1298
|
addType(root, "SetupVmEnvironmentResult", [
|
|
1199
1299
|
{ id: 1, name: "success", type: "SetupVmEnvironmentSuccess" },
|
|
@@ -1294,10 +1394,22 @@ export function createMessageTypes() {
|
|
|
1294
1394
|
{ id: 1, name: "run_request", type: "AgentRunRequest" },
|
|
1295
1395
|
{ id: 2, name: "exec_client_message", type: "ExecClientMessage" },
|
|
1296
1396
|
{ id: 3, name: "kv_client_message", type: "KvClientMessage" },
|
|
1397
|
+
{ id: 4, name: "conversation_action", type: "ConversationAction" },
|
|
1297
1398
|
{ id: 5, name: "exec_client_control_message", type: "ExecClientControlMessage" },
|
|
1298
1399
|
{ id: 6, name: "interaction_response", type: "InteractionResponse" },
|
|
1299
1400
|
{ id: 7, name: "client_heartbeat", type: "ClientHeartbeat" },
|
|
1300
|
-
], [{
|
|
1401
|
+
], [{
|
|
1402
|
+
name: "message",
|
|
1403
|
+
fields: [
|
|
1404
|
+
"run_request",
|
|
1405
|
+
"exec_client_message",
|
|
1406
|
+
"kv_client_message",
|
|
1407
|
+
"conversation_action",
|
|
1408
|
+
"exec_client_control_message",
|
|
1409
|
+
"interaction_response",
|
|
1410
|
+
"client_heartbeat",
|
|
1411
|
+
],
|
|
1412
|
+
}]);
|
|
1301
1413
|
// ── AgentServerMessage ──
|
|
1302
1414
|
addType(root, "AgentServerMessage", [
|
|
1303
1415
|
{ id: 1, name: "interaction_update", type: "InteractionUpdate" },
|
package/dist/protocol/struct.js
CHANGED
|
@@ -249,7 +249,7 @@ export function encodeJsonAsValue(v) {
|
|
|
249
249
|
}
|
|
250
250
|
else if (typeof v === "object") {
|
|
251
251
|
const st = [];
|
|
252
|
-
for (const [key, val] of Object.entries(v)) {
|
|
252
|
+
for (const [key, val] of Object.entries(v).sort(([left], [right]) => left.localeCompare(right))) {
|
|
253
253
|
if (val === undefined)
|
|
254
254
|
continue;
|
|
255
255
|
const entry = [];
|