cursor-opencode-provider 0.7.6 → 0.8.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +36 -15
  3. package/SECURITY.md +43 -7
  4. package/dist/agent-url.js +2 -2
  5. package/dist/auth-renewal.d.ts +85 -0
  6. package/dist/auth-renewal.js +284 -0
  7. package/dist/auth.d.ts +38 -22
  8. package/dist/auth.js +79 -96
  9. package/dist/classic-tools.d.ts +15 -0
  10. package/dist/classic-tools.js +45 -0
  11. package/dist/context/build.d.ts +62 -7
  12. package/dist/context/build.js +127 -95
  13. package/dist/context/dynamic-catalog.d.ts +2 -58
  14. package/dist/context/dynamic-catalog.js +16 -161
  15. package/dist/context/epoch.d.ts +6 -4
  16. package/dist/context/epoch.js +22 -10
  17. package/dist/context/frozen.js +42 -15
  18. package/dist/context/index.d.ts +3 -1
  19. package/dist/context/index.js +3 -1
  20. package/dist/context/overlay.d.ts +2 -23
  21. package/dist/context/overlay.js +6 -30
  22. package/dist/context/paths.d.ts +51 -21
  23. package/dist/context/paths.js +78 -27
  24. package/dist/context/rules.d.ts +3 -15
  25. package/dist/context/rules.js +10 -186
  26. package/dist/context/skills-bridge.d.ts +22 -0
  27. package/dist/context/skills-bridge.js +20 -0
  28. package/dist/context/skills.d.ts +31 -8
  29. package/dist/context/skills.js +187 -86
  30. package/dist/debug.d.ts +2 -0
  31. package/dist/debug.js +4 -0
  32. package/dist/errors.d.ts +13 -0
  33. package/dist/errors.js +27 -3
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +119 -15
  36. package/dist/host-plan-file.d.ts +28 -0
  37. package/dist/host-plan-file.js +76 -0
  38. package/dist/image-input.d.ts +18 -0
  39. package/dist/image-input.js +119 -28
  40. package/dist/image-save.d.ts +10 -2
  41. package/dist/image-save.js +35 -23
  42. package/dist/index.d.ts +10 -0
  43. package/dist/language-model.d.ts +86 -25
  44. package/dist/language-model.js +1295 -369
  45. package/dist/model-config.d.ts +9 -0
  46. package/dist/model-config.js +28 -1
  47. package/dist/opencode2/catalog.d.ts +1 -0
  48. package/dist/opencode2/catalog.js +2 -0
  49. package/dist/opencode2/image-save-tool.d.ts +8 -0
  50. package/dist/opencode2/image-save-tool.js +84 -0
  51. package/dist/opencode2/integration.d.ts +30 -7
  52. package/dist/opencode2/integration.js +75 -46
  53. package/dist/opencode2/types.d.ts +15 -0
  54. package/dist/parallel-step.d.ts +65 -0
  55. package/dist/parallel-step.js +102 -0
  56. package/dist/plugin-opencode2.js +58 -56
  57. package/dist/plugin.d.ts +0 -10
  58. package/dist/plugin.js +207 -127
  59. package/dist/pricing-data.d.ts +14 -1
  60. package/dist/pricing-data.js +14 -1
  61. package/dist/protocol/ask-question.d.ts +12 -1
  62. package/dist/protocol/ask-question.js +77 -3
  63. package/dist/protocol/blob-store.js +3 -2
  64. package/dist/protocol/conversation-bind.js +1 -1
  65. package/dist/protocol/conversation-persistence.d.ts +2 -0
  66. package/dist/protocol/conversation-persistence.js +11 -1
  67. package/dist/protocol/conversation-state.d.ts +2 -0
  68. package/dist/protocol/conversation-state.js +19 -0
  69. package/dist/protocol/create-plan.d.ts +47 -25
  70. package/dist/protocol/create-plan.js +35 -24
  71. package/dist/protocol/interactions.d.ts +26 -17
  72. package/dist/protocol/interactions.js +80 -31
  73. package/dist/protocol/messages.js +96 -18
  74. package/dist/protocol/request.d.ts +16 -9
  75. package/dist/protocol/request.js +17 -25
  76. package/dist/protocol/switch-mode.d.ts +66 -27
  77. package/dist/protocol/switch-mode.js +147 -73
  78. package/dist/protocol/token-details.d.ts +2 -0
  79. package/dist/protocol/token-details.js +11 -1
  80. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  81. package/dist/protocol/tool-call-bridge.js +14 -0
  82. package/dist/protocol/tools.d.ts +33 -11
  83. package/dist/protocol/tools.js +165 -103
  84. package/dist/protocol/turn-provenance.d.ts +52 -0
  85. package/dist/protocol/turn-provenance.js +174 -0
  86. package/dist/replay-safety.d.ts +6 -0
  87. package/dist/replay-safety.js +56 -6
  88. package/dist/session.d.ts +44 -2
  89. package/dist/session.js +45 -1
  90. package/dist/shared.d.ts +7 -0
  91. package/dist/shared.js +7 -0
  92. package/dist/todo-store.d.ts +2 -0
  93. package/dist/todo-store.js +19 -0
  94. package/dist/transport/connect.d.ts +2 -0
  95. package/dist/transport/connect.js +23 -6
  96. package/dist/usage.js +23 -6
  97. package/package.json +3 -2
  98. package/dist/context/agents.d.ts +0 -12
  99. package/dist/context/agents.js +0 -136
  100. package/dist/plan-execution-kickoff.d.ts +0 -57
  101. package/dist/plan-execution-kickoff.js +0 -186
@@ -14,6 +14,8 @@ export type HandledInteraction = {
14
14
  variantField: number;
15
15
  variantName: string;
16
16
  outcome: "rejected" | "acknowledged" | "failed" | "bridged" | "approved";
17
+ /** Decoded model call id, including refusals and lifecycle acknowledgments. */
18
+ toolCallId?: string;
17
19
  /**
18
20
  * Immediate reply for this query. Absent for bridged *synchronous* AskQuestion
19
21
  * or SwitchMode: Cursor blocks until the host tool returns, exactly as its own
@@ -31,24 +33,29 @@ export type HandledInteraction = {
31
33
  */
32
34
  switchMode?: DecodedSwitchModeQuery & {
33
35
  bridge: SwitchModeBridge;
34
- toolName?: SwitchModeHostTool | "question";
36
+ toolName?: SwitchModeHostTool;
35
37
  };
36
38
  /**
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.
39
+ * Present when CreatePlan needs a host tool: a host plan-stage tool that
40
+ * owns the write *and* the review, the host's `plan_exit` review of the
41
+ * plan file the provider just wrote (`planUri` / `planPath` set), or the
42
+ * emulated `question` prompt after that write.
41
43
  */
42
44
  createPlan?: DecodedCreatePlanQuery & {
43
45
  bridge: CreatePlanBridge;
44
- toolName: typeof CURSOR_PLAN_STAGE_TOOL | "question";
46
+ toolName: typeof CURSOR_PLAN_STAGE_TOOL | "plan_exit" | "question";
45
47
  planUri?: string;
46
- /** Filesystem path of the plan the provider just wrote (emulated path). */
48
+ /** Filesystem path of the plan the provider just wrote. */
47
49
  planPath?: string;
48
50
  questionInput?: ReturnType<typeof createPlanApprovalQuestionInput>;
49
- /** The plan to show the user before they approve it (emulated path only). */
51
+ /** The plan to show the user before the host review. */
50
52
  planReview?: string;
51
53
  };
54
+ /**
55
+ * Cursor tool call id of a CreatePlan the host plan agent will record
56
+ * instead. Nothing was written, so its display must not be mirrored as a plan.
57
+ */
58
+ deferredCreatePlanToolCallId?: string;
52
59
  /** Present when an image generation was approved: the target to expect. */
53
60
  generateImage?: DecodedGenerateImageQuery;
54
61
  };
@@ -81,16 +88,18 @@ export type HandleInteractionQueryOptions = {
81
88
  workspaceRoot?: string;
82
89
  /** True when the advertised host plan-stage tool is available. */
83
90
  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
- */
91
+ /** An approved switch into the host `plan` agent waits for this Run to end. */
92
+ hostPlanEntryPending?: boolean;
93
+ /** Host primary agent this Run executes under, when the host reported it. */
94
+ hostAgent?: string;
95
+ /** Provider-recorded Cursor plan/spec mode for the CreatePlan execution gate. */
89
96
  planModeActive?: boolean;
97
+ /** The host's own plan file for this session, when the host defines one. */
98
+ hostPlanFile?: string;
90
99
  /**
91
100
  * Cursor unified mode currently recorded for this session. A SwitchMode to
92
101
  * the mode already in effect needs no approval, matching Cursor's own IDE
93
- * handler and keeping the plan-approval prompt from firing twice.
102
+ * handler and keeping the host's plan review from running twice.
94
103
  */
95
104
  activeCursorModeId?: string;
96
105
  };
@@ -112,9 +121,9 @@ export declare function inspectInteractionQueryWire(agentServerPayload: Uint8Arr
112
121
  * Bridged / persisted exceptions:
113
122
  * - AskQuestion (#3) → OpenCode `question` tool
114
123
  * - 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
124
+ * - CreatePlan (#7) → the host's plan file (session `Session.plan` file when
125
+ * known, else a new file under hostPlansDir); empty args still get the CLI
126
+ * empty-`plan_uri` success ack
118
127
  * - GenerateImage (#12) → approve when `cursor_image_save` is advertised
119
128
  */
120
129
  export declare function handleInteractionQuery(query: Record<string, unknown>, agentServerPayload: Uint8Array, options?: HandleInteractionQueryOptions): HandledInteraction;
@@ -2,7 +2,7 @@ import { encodeMessage } from "./messages.js";
2
2
  import { readAllFields } from "./struct.js";
3
3
  import { decodeAskQuestionQuery, MISSING_ARGS_REASON, MISSING_QUERY_REASON, } from "./ask-question.js";
4
4
  import { decodeGenerateImageQuery, } from "./generate-image.js";
5
- import { CURSOR_PLAN_STAGE_TOOL, createPlanApprovalQuestionInput, decodeCreatePlanQuery, renderPlanReviewMessage, resolveCreatePlanBridge, writeOpencodePlanFile, } from "./create-plan.js";
5
+ import { CURSOR_PLAN_STAGE_TOOL, decodeCreatePlanQuery, createPlanApprovalQuestionInput, renderPlanReviewMessage, resolveCreatePlanBridge, writeOpencodePlanFile, } from "./create-plan.js";
6
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";
7
7
  const HEADLESS_REASON = "This Cursor interaction requires UI approval and is not available through the OpenCode provider.";
8
8
  /**
@@ -76,9 +76,9 @@ export function inspectInteractionQueryWire(agentServerPayload) {
76
76
  * Bridged / persisted exceptions:
77
77
  * - AskQuestion (#3) → OpenCode `question` tool
78
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
79
+ * - CreatePlan (#7) → the host's plan file (session `Session.plan` file when
80
+ * known, else a new file under hostPlansDir); empty args still get the CLI
81
+ * empty-`plan_uri` success ack
82
82
  * - GenerateImage (#12) → approve when `cursor_image_save` is advertised
83
83
  */
84
84
  export function handleInteractionQuery(query, agentServerPayload, options = {}) {
@@ -160,7 +160,7 @@ export function handleInteractionQuery(query, agentServerPayload, options = {})
160
160
  * non-blocking prompt, and otherwise hold the query open until the user picks.
161
161
  */
162
162
  function handleAskQuestionQuery(id, variantBytes, options) {
163
- const base = { id, variantField: 3, variantName: "ask_question_interaction_query" };
163
+ const base = { id, variantField: 3, variantName: "ask_question_interaction_query", toolCallId: undefined };
164
164
  const reject = (reason) => ({
165
165
  ...base,
166
166
  outcome: "rejected",
@@ -171,6 +171,7 @@ function handleAskQuestionQuery(id, variantBytes, options) {
171
171
  const decoded = decodeAskQuestionQuery(variantBytes);
172
172
  if (!decoded)
173
173
  return reject(MISSING_ARGS_REASON);
174
+ base.toolCallId = decoded.toolCallId;
174
175
  if (!options.canBridgeAskQuestion)
175
176
  return reject(ASK_QUESTION_UNAVAILABLE_REASON);
176
177
  return {
@@ -200,7 +201,7 @@ export function buildAskQuestionInteractionReply(id, result) {
200
201
  * until the host tool returns, then reply approved{} or rejected{reason}.
201
202
  */
202
203
  function handleSwitchModeQuery(id, variantBytes, options) {
203
- const base = { id, variantField: 4, variantName: "switch_mode_request_query" };
204
+ const base = { id, variantField: 4, variantName: "switch_mode_request_query", toolCallId: undefined };
204
205
  const reject = (reason) => ({
205
206
  ...base,
206
207
  outcome: "rejected",
@@ -211,10 +212,12 @@ function handleSwitchModeQuery(id, variantBytes, options) {
211
212
  const decoded = decodeSwitchModeQuery(variantBytes);
212
213
  if (!decoded)
213
214
  return reject(SWITCH_MODE_MISSING_ARGS_REASON);
215
+ base.toolCallId = decoded.toolCallId;
214
216
  const bridge = resolveSwitchModeBridge(decoded.args.targetModeId, {
215
217
  allowTools: options.allowTools === true,
216
218
  advertised: options.advertisedTools ?? [],
217
219
  ...(options.activeCursorModeId ? { activeModeId: options.activeCursorModeId } : {}),
220
+ ...(options.hostAgent ? { hostAgent: options.hostAgent } : {}),
218
221
  });
219
222
  if (bridge.kind === "reject")
220
223
  return reject(bridge.reason);
@@ -243,10 +246,10 @@ function handleSwitchModeQuery(id, variantBytes, options) {
243
246
  switchMode: {
244
247
  ...decoded,
245
248
  bridge,
246
- toolName: bridge.kind === "native" ? bridge.toolName : "question",
249
+ toolName: bridge.toolName,
247
250
  },
248
- // Keep Cursor waiting until the host tool returns, matching CLI blocking on
249
- // the mode-switch approval prompt.
251
+ // Keep Cursor waiting until the host plan tool returns: the host owns the
252
+ // mode-switch approval.
250
253
  reply: undefined,
251
254
  };
252
255
  }
@@ -298,7 +301,7 @@ export function buildAsyncAskQuestionCompletion(originalToolCallId, originalArgs
298
301
  * written but not accepted, so the model keeps planning.
299
302
  */
300
303
  function handleCreatePlanQuery(id, variantBytes, options) {
301
- const base = { id, variantField: 7, variantName: "create_plan_request_query" };
304
+ const base = { id, variantField: 7, variantName: "create_plan_request_query", toolCallId: undefined };
302
305
  const reply = (result) => ({
303
306
  ...base,
304
307
  outcome: result.error ? "failed" : "acknowledged",
@@ -315,13 +318,46 @@ function handleCreatePlanQuery(id, variantBytes, options) {
315
318
  const decoded = decodeCreatePlanQuery(variantBytes);
316
319
  if (!decoded)
317
320
  return reply({ success: {}, plan_uri: "" });
321
+ base.toolCallId = decoded.toolCallId;
318
322
  const bridge = resolveCreatePlanBridge({
319
323
  allowTools: options.allowTools === true,
320
324
  canStage: options.canBridgeCreatePlan === true,
321
- planModeActive: options.planModeActive === true,
322
325
  advertised: options.advertisedTools ?? [],
326
+ hostPlanEntryPending: options.hostPlanEntryPending === true,
327
+ ...(options.hostAgent ? { hostAgent: options.hostAgent } : {}),
328
+ planModeActive: options.planModeActive === true,
329
+ ...(options.hostPlanFile ? { hostPlanFile: options.hostPlanFile } : {}),
323
330
  });
324
- // The host plan-stage tool owns the write and the approval prompt together.
331
+ // The host's plan agent records and reviews the plan; write nothing here.
332
+ if (bridge.kind === "defer") {
333
+ return {
334
+ ...reply({ error: { error: bridge.reason }, plan_uri: "" }),
335
+ ...(decoded.toolCallId ? { deferredCreatePlanToolCallId: decoded.toolCallId } : {}),
336
+ };
337
+ }
338
+ // The host plan agent reviews its own plan file: record the plan there and
339
+ // hand it to the host plan_exit, which asks the user and, on approval,
340
+ // moves the session to its build agent.
341
+ if (bridge.kind === "exit") {
342
+ const written = writeOpencodePlanFile(decoded.args, options.workspaceRoot ?? "", Date.now(), bridge.planPath);
343
+ if (!written.ok) {
344
+ return reply({ error: { error: written.error }, plan_uri: "" });
345
+ }
346
+ return {
347
+ ...base,
348
+ outcome: "bridged",
349
+ createPlan: {
350
+ ...decoded,
351
+ bridge,
352
+ toolName: "plan_exit",
353
+ planUri: written.planUri,
354
+ planPath: written.planPath,
355
+ planReview: renderPlanReviewMessage(written.markdown, written.planPath),
356
+ },
357
+ reply: undefined,
358
+ };
359
+ }
360
+ // The host plan-stage tool owns the write and the review together.
325
361
  if (bridge.kind === "stage") {
326
362
  return {
327
363
  ...base,
@@ -330,25 +366,18 @@ function handleCreatePlanQuery(id, variantBytes, options) {
330
366
  reply: undefined,
331
367
  };
332
368
  }
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
369
  if (bridge.kind === "approve") {
370
+ const workspaceRoot = options.workspaceRoot?.trim();
371
+ if (!workspaceRoot) {
372
+ return reply({
373
+ error: { error: "CreatePlan requires a workspace root to write the plan file" },
374
+ plan_uri: "",
375
+ });
376
+ }
377
+ const written = writeOpencodePlanFile(decoded.args, workspaceRoot, Date.now(), options.hostPlanFile);
378
+ if (!written.ok) {
379
+ return reply({ error: { error: written.error }, plan_uri: "" });
380
+ }
352
381
  return {
353
382
  ...base,
354
383
  outcome: "bridged",
@@ -364,6 +393,25 @@ function handleCreatePlanQuery(id, variantBytes, options) {
364
393
  reply: undefined,
365
394
  };
366
395
  }
396
+ // A lifecycle turn (title generation, compaction) runs alongside the real one
397
+ // and would otherwise write a second, throwaway plan file for the same
398
+ // request. Ack it the CLI way instead of persisting anything.
399
+ if (options.allowTools !== true)
400
+ return reply({ success: {}, plan_uri: "" });
401
+ const workspaceRoot = options.workspaceRoot?.trim();
402
+ if (!workspaceRoot) {
403
+ return reply({
404
+ error: { error: "CreatePlan requires a workspace root to write the plan file" },
405
+ plan_uri: "",
406
+ });
407
+ }
408
+ // The session's own plan file when known, as the host's plan agent uses it.
409
+ const written = writeOpencodePlanFile(decoded.args, workspaceRoot, Date.now(), options.hostPlanFile);
410
+ if (!written.ok) {
411
+ return reply({ error: { error: written.error }, plan_uri: "" });
412
+ }
413
+ // Nothing advertised can ask: the plan is recorded where the host keeps
414
+ // plans. Execution starts only if the user later switches agents.
367
415
  return reply({ success: {}, plan_uri: written.planUri });
368
416
  }
369
417
  /**
@@ -375,7 +423,7 @@ function handleCreatePlanQuery(id, variantBytes, options) {
375
423
  * nowhere to go.
376
424
  */
377
425
  function handleGenerateImageQuery(id, variantBytes, options) {
378
- const base = { id, variantField: 12, variantName: "generate_image_request_query" };
426
+ const base = { id, variantField: 12, variantName: "generate_image_request_query", toolCallId: undefined };
379
427
  const reject = (reason) => ({
380
428
  ...base,
381
429
  outcome: "rejected",
@@ -388,6 +436,7 @@ function handleGenerateImageQuery(id, variantBytes, options) {
388
436
  const decoded = decodeGenerateImageQuery(variantBytes);
389
437
  if (!decoded)
390
438
  return reject("Missing generate image arguments");
439
+ base.toolCallId = decoded.toolCallId;
391
440
  if (!options.canSaveGeneratedImage) {
392
441
  return reject("This OpenCode agent cannot save a generated image, so generating one would "
393
442
  + "produce nothing. Describe the image in your reply instead.");
@@ -28,8 +28,8 @@ export function createMessageTypes() {
28
28
  ]);
29
29
  addType(root, "Heartbeat", []);
30
30
  // Display ToolCall (interaction_update.tool_call_*) — agent.v1 oneof, not
31
- // {tool_name,args} strings. Args-only wrappers are enough to bridge into
32
- // OpenCode; result payloads are ignored on decode.
31
+ // {tool_name,args} strings. Decode finalized todo state for mirroring and
32
+ // MCP errors for diagnostics; other completion results are not replayed.
33
33
  root.add(new protobuf.Enum("TodoStatus", {
34
34
  TODO_STATUS_UNSPECIFIED: 0,
35
35
  TODO_STATUS_PENDING: 1,
@@ -79,7 +79,10 @@ export function createMessageTypes() {
79
79
  ]);
80
80
  addType(root, "EditToolCall", [{ id: 1, name: "args", type: "EditToolArgs" }]);
81
81
  addType(root, "LsToolCall", [{ id: 1, name: "args", type: "LsArgs" }]);
82
- addType(root, "McpToolCall", [{ id: 1, name: "args", type: "McpArgs" }]);
82
+ addType(root, "McpToolCall", [
83
+ { id: 1, name: "args", type: "McpArgs" },
84
+ { id: 2, name: "result", type: "McpResult" },
85
+ ]);
83
86
  addType(root, "CreatePlanArgs", [
84
87
  { id: 1, name: "plan", type: "string" },
85
88
  { id: 2, name: "todos", type: "TodoItem", repeated: true },
@@ -389,6 +392,13 @@ export function createMessageTypes() {
389
392
  { id: 3, name: "args_text_delta", type: "string" },
390
393
  { id: 4, name: "model_call_id", type: "string" },
391
394
  ]);
395
+ // agent.v1 ToolCallDeltaUpdate: keep the nested delta opaque; the pump
396
+ // needs its presence as generation progress, not its display-only content.
397
+ addType(root, "ToolCallDeltaUpdate", [
398
+ { id: 1, name: "call_id", type: "string" },
399
+ { id: 2, name: "tool_call_delta", type: "bytes" },
400
+ { id: 3, name: "model_call_id", type: "string" },
401
+ ]);
392
402
  // agent.v1 StepStartedUpdate / StepCompletedUpdate: step_id is uint64 (T:4),
393
403
  // not string — wrong type made every step_completed frame throw
394
404
  // "index out of range" in decodeMessage.
@@ -399,6 +409,12 @@ export function createMessageTypes() {
399
409
  { id: 1, name: "step_id", type: "uint64" },
400
410
  { id: 2, name: "step_duration_ms", type: "int64" },
401
411
  ]);
412
+ // agent.v1 ToolRequestsListedUpdate — Cursor lists how many tool calls this
413
+ // generation will send so the client can place its step boundary (CLI does
414
+ // not need this; OpenCode's AI SDK step does). Field 27 on InteractionUpdate.
415
+ addType(root, "ToolRequestsListedUpdate", [
416
+ { id: 1, name: "call_count", type: "uint32" },
417
+ ]);
402
418
  // InteractionUpdate — the core streaming update message
403
419
  addType(root, "InteractionUpdate", [
404
420
  { id: 1, name: "text_delta", type: "TextDeltaUpdate" },
@@ -408,9 +424,11 @@ export function createMessageTypes() {
408
424
  { id: 7, name: "partial_tool_call", type: "PartialToolCall" },
409
425
  { id: 13, name: "heartbeat", type: "Heartbeat" },
410
426
  { id: 14, name: "turn_ended", type: "TurnEnded" },
427
+ { id: 15, name: "tool_call_delta", type: "ToolCallDeltaUpdate" },
411
428
  { id: 16, name: "step_started", type: "StepStarted" },
412
429
  { id: 17, name: "step_completed", type: "StepCompleted" },
413
- ], [{ name: "update", fields: ["text_delta", "tool_call_started", "tool_call_completed", "thinking_delta", "partial_tool_call", "heartbeat", "turn_ended", "step_started", "step_completed"] }]);
430
+ { id: 27, name: "tool_requests_listed", type: "ToolRequestsListedUpdate" },
431
+ ], [{ name: "update", fields: ["text_delta", "tool_call_started", "tool_call_completed", "thinking_delta", "partial_tool_call", "heartbeat", "turn_ended", "tool_call_delta", "step_started", "step_completed", "tool_requests_listed"] }]);
414
432
  // ── Exec channel ──
415
433
  // Field numbers match agent.v1. Extra fields we don't use are still declared
416
434
  // so protobufjs doesn't drop them on decode.
@@ -934,12 +952,25 @@ export function createMessageTypes() {
934
952
  { id: 5, name: "tool_name", type: "string" },
935
953
  ]);
936
954
  addType(root, "McpTextContent", [{ id: 1, name: "text", type: "string" }]);
937
- addType(root, "McpToolResultContentItem", [{ id: 1, name: "text", type: "McpTextContent" }], [{ name: "content", fields: ["text"] }]);
955
+ // Cursor CLI 2026.10.01 `agent/v1/mcp_exec_pb.js`: "McpImageContent|1 data 12|2 mime_type 9",
956
+ // "McpToolResultContentItem|1 text #0 content|2 image #1 content".
957
+ addType(root, "McpImageContent", [
958
+ { id: 1, name: "data", type: "bytes" },
959
+ { id: 2, name: "mime_type", type: "string" },
960
+ ]);
961
+ addType(root, "McpToolResultContentItem", [
962
+ { id: 1, name: "text", type: "McpTextContent" },
963
+ { id: 2, name: "image", type: "McpImageContent" },
964
+ ], [{ name: "content", fields: ["text", "image"] }]);
938
965
  addType(root, "McpSuccess", [
939
966
  { id: 1, name: "content", type: "McpToolResultContentItem", repeated: true },
940
967
  { id: 2, name: "is_error", type: "bool" },
941
968
  ]);
942
- addType(root, "McpError", [{ id: 1, name: "error", type: "string" }]);
969
+ addType(root, "McpError", [
970
+ { id: 1, name: "error", type: "string" },
971
+ // Server-side CallDynamicTool validation uses a title (#1) and detail (#2).
972
+ { id: 2, name: "detail", type: "string" },
973
+ ]);
943
974
  addType(root, "McpResult", [
944
975
  { id: 1, name: "success", type: "McpSuccess" },
945
976
  { id: 2, name: "error", type: "McpError" },
@@ -1062,9 +1093,27 @@ export function createMessageTypes() {
1062
1093
  { id: 20, name: "is_working_dir_home_dir", type: "bool" },
1063
1094
  { id: 21, name: "process_working_directory", type: "string" },
1064
1095
  ]);
1096
+ // Cursor applies a rule by its type: `global` is the CLI's `alwaysApply: true`
1097
+ // (its AGENTS.md / CLAUDE.md default). An untyped rule is only attachable on
1098
+ // request, so its content never reaches the model on its own.
1099
+ addType(root, "CursorRuleTypeGlobal", []);
1100
+ addType(root, "CursorRuleTypeFileGlobs", [
1101
+ { id: 1, name: "globs", type: "string", repeated: true },
1102
+ ]);
1103
+ addType(root, "CursorRuleTypeAgentFetched", [
1104
+ { id: 1, name: "description", type: "string" },
1105
+ ]);
1106
+ addType(root, "CursorRuleTypeManuallyAttached", []);
1107
+ addType(root, "CursorRuleType", [
1108
+ { id: 1, name: "global", type: "CursorRuleTypeGlobal" },
1109
+ { id: 2, name: "file_globbed", type: "CursorRuleTypeFileGlobs" },
1110
+ { id: 3, name: "agent_fetched", type: "CursorRuleTypeAgentFetched" },
1111
+ { id: 4, name: "manually_attached", type: "CursorRuleTypeManuallyAttached" },
1112
+ ], [{ name: "type", fields: ["global", "file_globbed", "agent_fetched", "manually_attached"] }]);
1065
1113
  addType(root, "CursorRule", [
1066
1114
  { id: 1, name: "full_path", type: "string" },
1067
1115
  { id: 2, name: "content", type: "string" },
1116
+ { id: 3, name: "type", type: "CursorRuleType" },
1068
1117
  ]);
1069
1118
  addType(root, "RepositoryIndexingInfo", [
1070
1119
  { id: 1, name: "relative_workspace_path", type: "string" },
@@ -1151,8 +1200,8 @@ export function createMessageTypes() {
1151
1200
  { id: 1, name: "success", type: "RequestContextSuccess" },
1152
1201
  ]);
1153
1202
  // Cursor probes MCP server availability before emitting an MCP-backed tool
1154
- // call. OpenCode owns those servers, so answer from the descriptors already
1155
- // advertised in RequestContext rather than surfacing this as a user tool.
1203
+ // call. OpenCode owns those servers, so answer from the live host catalog
1204
+ // rather than surfacing this as a user tool.
1156
1205
  addType(root, "McpStateExecArgs", [
1157
1206
  { id: 1, name: "server_identifiers", type: "string", repeated: true },
1158
1207
  { id: 2, name: "kick_only", type: "bool" },
@@ -1329,13 +1378,26 @@ export function createMessageTypes() {
1329
1378
  addType(root, "SelectedContext", [
1330
1379
  { id: 1, name: "selected_images", type: "SelectedImage", repeated: true },
1331
1380
  ]);
1381
+ // agent.v1.AgentMode — the client's current mode, sent on every user message.
1382
+ root.add(new protobuf.Enum("AgentMode", {
1383
+ AGENT_MODE_UNSPECIFIED: 0,
1384
+ AGENT_MODE_AGENT: 1,
1385
+ AGENT_MODE_ASK: 2,
1386
+ AGENT_MODE_PLAN: 3,
1387
+ AGENT_MODE_DEBUG: 4,
1388
+ AGENT_MODE_TRIAGE: 5,
1389
+ AGENT_MODE_PROJECT: 6,
1390
+ AGENT_MODE_MULTITASK: 7,
1391
+ AGENT_MODE_CUSTOM: 8,
1392
+ }));
1332
1393
  addType(root, "UserMessage", [
1333
1394
  { id: 1, name: "text", type: "string" },
1334
1395
  { id: 2, name: "message_id", type: "string" },
1335
1396
  { id: 3, name: "selected_context", type: "SelectedContext" },
1397
+ { id: 4, name: "mode", type: "AgentMode" },
1336
1398
  ]);
1337
- // RequestContext — UserMessageAction #2. Live per-turn tools go here
1338
- // (AgentRunRequest.mcp_tools #4 is prewarm-only / empty on real turns).
1399
+ // RequestContext — UserMessageAction #2. Slim mcp_meta_tool_options names
1400
+ // the advertised catalog; AgentRunRequest.mcp_tools (#4) is empty on real turns.
1339
1401
  addType(root, "RequestContext", [
1340
1402
  { id: 2, name: "rules", type: "CursorRule", repeated: true },
1341
1403
  { id: 4, name: "env", type: "RequestContextEnv" },
@@ -1372,7 +1434,8 @@ export function createMessageTypes() {
1372
1434
  { id: 2, name: "request_context", type: "RequestContext" },
1373
1435
  ]);
1374
1436
  addType(root, "CancelAction", [
1375
- { id: 1, name: "conversation_id", type: "string" },
1437
+ // Cursor CLI CancelAction field #1 is the interruption reason, not an id.
1438
+ { id: 1, name: "reason", type: "string" },
1376
1439
  ]);
1377
1440
  // Deferred answers for an AskQuestion the client already replied to with
1378
1441
  // `async`. `original_args` is kept as raw bytes so the exact AskQuestionArgs
@@ -1383,11 +1446,23 @@ export function createMessageTypes() {
1383
1446
  { id: 2, name: "original_args", type: "bytes" },
1384
1447
  { id: 3, name: "result", type: "AskQuestionResult" },
1385
1448
  ]);
1449
+ // Cursor CLI 2026.10.01 descriptors: system context is
1450
+ // injected into the current Run without submitting another user turn.
1451
+ addType(root, "SystemContextInjection", [
1452
+ { id: 1, name: "producer", type: "string" },
1453
+ { id: 2, name: "content", type: "string" },
1454
+ ]);
1455
+ addType(root, "InjectContextAction", [
1456
+ { id: 1, name: "injection_id", type: "string" },
1457
+ { id: 2, name: "expected_run_id", type: "string" },
1458
+ { id: 4, name: "system_context", type: "SystemContextInjection" },
1459
+ ]);
1386
1460
  addType(root, "ConversationAction", [
1387
1461
  { id: 1, name: "user_message_action", type: "UserMessageAction" },
1388
1462
  { id: 2, name: "resume_action", type: "ResumeAction" },
1389
1463
  { id: 3, name: "cancel_action", type: "CancelAction" },
1390
1464
  { id: 8, name: "async_ask_question_completion_action", type: "AsyncAskQuestionCompletionAction" },
1465
+ { id: 19, name: "inject_context_action", type: "InjectContextAction" },
1391
1466
  ], [{
1392
1467
  name: "action",
1393
1468
  fields: [
@@ -1395,6 +1470,7 @@ export function createMessageTypes() {
1395
1470
  "resume_action",
1396
1471
  "cancel_action",
1397
1472
  "async_ask_question_completion_action",
1473
+ "inject_context_action",
1398
1474
  ],
1399
1475
  }]);
1400
1476
  // Seed ConversationStateStructure for turn 1 (system prompt as JSON strings
@@ -1683,11 +1759,11 @@ export function createMessageTypes() {
1683
1759
  { id: 2, name: "value", type: "string" },
1684
1760
  ]);
1685
1761
  addType(root, "AvailableModelVariant", [
1686
- { id: 1, name: "display_name", type: "string" },
1687
- { id: 2, name: "is_max_mode", type: "bool" },
1688
- { id: 3, name: "is_default_max_config", type: "bool" },
1689
- { id: 4, name: "is_default_non_max_config", type: "bool" },
1690
- { id: 5, name: "parameter_values", type: "AvailableModelParameterValue", repeated: true },
1762
+ { id: 1, name: "parameter_values", type: "AvailableModelParameterValue", repeated: true },
1763
+ { id: 2, name: "display_name", type: "string" },
1764
+ { id: 3, name: "is_max_mode", type: "bool" },
1765
+ { id: 4, name: "is_default_max_config", type: "bool" },
1766
+ { id: 5, name: "is_default_non_max_config", type: "bool" },
1691
1767
  ]);
1692
1768
  addType(root, "AvailableModelEntry", [
1693
1769
  { id: 1, name: "name", type: "string" },
@@ -1696,14 +1772,16 @@ export function createMessageTypes() {
1696
1772
  { id: 9, name: "supports_thinking", type: "bool" },
1697
1773
  { id: 10, name: "supports_images", type: "bool" },
1698
1774
  { id: 14, name: "supports_max_mode", type: "bool" },
1699
- { id: 15, name: "context_token_limit", type: "uint32" },
1775
+ { id: 15, name: "context_token_limit", type: "int32" },
1776
+ { id: 16, name: "context_token_limit_for_max_mode", type: "int32" },
1700
1777
  { id: 17, name: "client_display_name", type: "string" },
1701
1778
  { id: 18, name: "server_model_name", type: "string" },
1702
1779
  { id: 29, name: "parameter_definitions", type: "AvailableModelParameterDefinition", repeated: true },
1703
1780
  { id: 30, name: "variants", type: "AvailableModelVariant", repeated: true },
1704
1781
  ]);
1705
1782
  addType(root, "AvailableModelsResponse", [
1706
- { id: 1, name: "models", type: "AvailableModelEntry", repeated: true },
1783
+ { id: 1, name: "model_names", type: "string", repeated: true },
1784
+ { id: 2, name: "models", type: "AvailableModelEntry", repeated: true },
1707
1785
  ]);
1708
1786
  return root;
1709
1787
  }
@@ -1,4 +1,4 @@
1
- import { type OpencodeToolDef } from "./tools.js";
1
+ import type { OpencodeToolDef } from "./tools.js";
2
2
  import type { CursorImageInput } from "../image-input.js";
3
3
  export type SeedHistoryMessage = {
4
4
  role: "system" | "user" | "assistant";
@@ -11,7 +11,6 @@ export type RunRequestInput = {
11
11
  conversationId: string;
12
12
  /** Stable parent group; unlike conversationId, this survives compaction/rebase. */
13
13
  conversationGroupId?: string;
14
- systemPrompt?: string;
15
14
  /**
16
15
  * Prior chat turns for a seed ConversationStateStructure (no checkpoint).
17
16
  * Tool outputs, when required for compaction/recovery, are represented as
@@ -22,7 +21,7 @@ export type RunRequestInput = {
22
21
  * Opaque ConversationStateStructure bytes from the last
23
22
  * conversation_checkpoint_update for this conversation_id. When set, echoed
24
23
  * as AgentRunRequest.conversation_state (CLI parity). When absent, a seed
25
- * state with the system prompt (or empty) is built for turn 1.
24
+ * state carrying only `history` (or empty) is built for turn 1.
26
25
  */
27
26
  conversationState?: Uint8Array;
28
27
  parameterValues?: Array<{
@@ -38,23 +37,31 @@ export type RunRequestInput = {
38
37
  requestContext?: Record<string, unknown>;
39
38
  /** Resume the supplied checkpoint instead of submitting another user turn. */
40
39
  action?: "user" | "resume";
40
+ /**
41
+ * Cursor mode of this user turn (`UserMessage.mode`, an `agent.v1.AgentMode`
42
+ * value). Cursor CLI sends its current mode on every user message.
43
+ */
44
+ mode?: number;
41
45
  };
42
46
  /**
43
47
  * Seed ConversationStateStructure for the first turn (no checkpoint yet).
44
48
  *
45
- * OpenCode needs a system prompt channel; we put it in root_prompt_messages_json
46
- * as a JSON chat message. After the first checkpoint arrives we stop inventing
47
- * state and echo the server's opaque structure instead (CLI behavior).
49
+ * After the first checkpoint arrives we stop inventing state and echo the
50
+ * server's opaque structure instead (CLI behavior). Compaction resets and
51
+ * rebases also use this seed, with `history` carrying OpenCode's prompt turns
52
+ * so Cursor can continue without the old checkpoint.
48
53
  *
49
- * Compaction resets also use this seed, with `history` carrying OpenCode's
50
- * compacted prompt turns so Cursor can summarize without the old checkpoint.
54
+ * No `system` entry is seeded: Cursor does not follow a client-authored
55
+ * `system` root message, and neither Cursor client writes
56
+ * `root_prompt_messages_json`. Host system context travels as the
57
+ * system-instructions rule in RequestContext (`systemInstructionsRule`), so
58
+ * `system` history entries are dropped here rather than duplicated.
51
59
  *
52
60
  * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
53
61
  * field is the internal `--system-prompt` CLI override and the server rejects
54
62
  * it for normal accounts.
55
63
  */
56
64
  export declare function buildSeedConversationState(input?: {
57
- systemPrompt?: string;
58
65
  history?: SeedHistoryMessage[];
59
66
  }): Uint8Array;
60
67
  /**