cursor-opencode-provider 0.7.1 → 0.7.3

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 (50) hide show
  1. package/README.md +8 -5
  2. package/dist/context/agents.js +1 -1
  3. package/dist/context/build.d.ts +2 -0
  4. package/dist/context/build.js +22 -10
  5. package/dist/context/epoch.d.ts +59 -0
  6. package/dist/context/epoch.js +224 -0
  7. package/dist/context/frozen.d.ts +4 -2
  8. package/dist/context/frozen.js +31 -11
  9. package/dist/context/index.d.ts +2 -0
  10. package/dist/context/index.js +2 -0
  11. package/dist/context/overlay.d.ts +44 -0
  12. package/dist/context/overlay.js +96 -0
  13. package/dist/debug.d.ts +9 -1
  14. package/dist/debug.js +46 -5
  15. package/dist/host-event-bridge.d.ts +22 -0
  16. package/dist/host-event-bridge.js +22 -0
  17. package/dist/image-input.js +36 -11
  18. package/dist/index.d.ts +3 -0
  19. package/dist/language-model.d.ts +51 -3
  20. package/dist/language-model.js +569 -140
  21. package/dist/model-config.js +5 -1
  22. package/dist/plugin-opencode2.js +10 -4
  23. package/dist/plugin.js +26 -15
  24. package/dist/pricing-data.d.ts +40 -0
  25. package/dist/pricing-data.js +40 -0
  26. package/dist/protocol/ask-question.d.ts +8 -9
  27. package/dist/protocol/ask-question.js +8 -9
  28. package/dist/protocol/conversation-bind.js +18 -8
  29. package/dist/protocol/create-plan.d.ts +6 -7
  30. package/dist/protocol/create-plan.js +4 -5
  31. package/dist/protocol/messages.js +18 -1
  32. package/dist/protocol/struct.js +1 -1
  33. package/dist/protocol/switch-mode.d.ts +3 -4
  34. package/dist/protocol/switch-mode.js +21 -12
  35. package/dist/protocol/tool-call-bridge.d.ts +4 -1
  36. package/dist/protocol/tool-call-bridge.js +27 -5
  37. package/dist/protocol/tools.d.ts +4 -2
  38. package/dist/protocol/tools.js +346 -55
  39. package/dist/session-directory.d.ts +23 -4
  40. package/dist/session-directory.js +36 -4
  41. package/dist/session.d.ts +23 -3
  42. package/dist/session.js +68 -8
  43. package/dist/shared.d.ts +2 -0
  44. package/dist/shared.js +2 -0
  45. package/dist/transport/connect.js +78 -10
  46. package/dist/transport/https-proxy.d.ts +56 -0
  47. package/dist/transport/https-proxy.js +348 -0
  48. package/dist/usage.d.ts +11 -0
  49. package/dist/usage.js +33 -7
  50. package/package.json +1 -1
@@ -154,9 +154,13 @@ export function modelInfoToConfig(mi, options = {}) {
154
154
  // tier is a variant parameter. Long-context choices are therefore emitted as
155
155
  // separate OpenCode entries by modelsToConfig.
156
156
  const documentedContext = getDocumentedCursorModelContext(mi.id);
157
+ // Cursor Auto (`default`) has no static maxContext on the model list, but
158
+ // live checkpoints report 256k for the base tier — the previous 200k
159
+ // fallback made the host context meter disagree with Cursor.
160
+ const fallbackContext = mi.id === "default" ? 256_000 : 200_000;
157
161
  const context = contextTier === "long"
158
162
  ? (mi.maxContextForMaxMode ?? documentedContext?.maxContextForMaxMode ?? 1_000_000)
159
- : (mi.maxContext ?? documentedContext?.maxContext ?? 200_000);
163
+ : (mi.maxContext ?? documentedContext?.maxContext ?? fallbackContext);
160
164
  // OpenCode's overflow/compaction/UI use limit.context; generation and
161
165
  // thinking budgets use limit.output. models.dev 1M peers advertise
162
166
  // 64k–128k output — a tiny cap makes long-context sessions feel broken
@@ -9,6 +9,7 @@ import { captureCursorShellResult, cursorShellEnvForCommand, prepareCursorShellA
9
9
  import { applyCursorProviderInventory, CURSOR_INTEGRATION_ID } from "./opencode2/catalog.js";
10
10
  import { applyCursorIntegration, resolveCursorAccessToken } from "./opencode2/integration.js";
11
11
  import { registerTodoTools } from "./opencode2/todo-tools.js";
12
+ import { OPENCODE_2_TOOL_DIALECT } from "./protocol/tools.js";
12
13
  import { clearSessionTodos } from "./todo-store.js";
13
14
  import { markCompactionSession } from "./compaction-marker.js";
14
15
  import { markSessionDirectory } from "./session-directory.js";
@@ -213,11 +214,14 @@ const plugin = {
213
214
  ...(token ? { accessToken: token } : {}),
214
215
  // Static fallback only. This hook fires once per model/package, not
215
216
  // per session, and 2.0 runs one daemon across many projects — the
216
- // real per-request directory comes from the session.context hook
217
- // below via `getSessionDirectory`, which `language-model.ts` prefers.
217
+ // real per-request directory comes from `x-opencode-directory` and
218
+ // the session.context hook below via `getSessionDirectory`.
218
219
  workspaceRoot,
219
220
  cacheDir,
220
221
  ...event.options,
222
+ // Keep after `event.options` so the OC2 plugin always selects the
223
+ // `path`/`shell` dialect when advertised schemas are opaque.
224
+ defaultDialect: OPENCODE_2_TOOL_DIALECT,
221
225
  });
222
226
  }));
223
227
  await track(ctx.aisdk.hook("language", (event) => {
@@ -324,8 +328,10 @@ const plugin = {
324
328
  }
325
329
  const rememberSessionDirectory = async (sessionID) => {
326
330
  try {
327
- const info = await ctx.session.get({ sessionID });
328
- markSessionDirectory(sessionID, info.location?.directory);
331
+ const info = (await ctx.session.get({ sessionID }));
332
+ // OpenCode 2.0 stable exposes a flat `directory`; older shapes nest it
333
+ // under `location.directory`. Prefer the flat field when both exist.
334
+ markSessionDirectory(sessionID, info.directory ?? info.location?.directory);
329
335
  }
330
336
  catch {
331
337
  // Best effort — falls back to the static workspaceRoot above.
package/dist/plugin.js CHANGED
@@ -17,6 +17,7 @@ import { sessionActivity } from "./activity.js";
17
17
  import { createOpenCodeWebSearchTool, openCodeWebSearchTool } from "./web-search-tool.js";
18
18
  import { createCursorImageSaveTool, cursorImageSaveTool } from "./image-save-tool.js";
19
19
  import { createPlanExecutionKickoffText, setPlanExecutionKickoff, } from "./plan-execution-kickoff.js";
20
+ import { dispatchHostEventBridge } from "./host-event-bridge.js";
20
21
  const MODULE_URL = new URL("./index.js", import.meta.url).href;
21
22
  export async function loadClassicTools(options = {}) {
22
23
  const configDir = options.configDirs?.[0] ?? opencodeGlobalConfigDirs()[0];
@@ -56,21 +57,25 @@ export async function CursorPlugin(input) {
56
57
  const cacheDir = opencodeGlobalCacheDir();
57
58
  const apiBaseURL = cursorApiBaseURL();
58
59
  const classicTools = await loadClassicTools();
59
- // After CreatePlan Yes, queue OpenCode's plan_exit-shaped synthetic turn so
60
- // implementation starts. Failures stay in the kickoff module (trace only).
61
- setPlanExecutionKickoff(async ({ sessionID, planPath }) => {
62
- await input.client.session.promptAsync({
63
- path: { id: sessionID },
64
- body: {
65
- agent: "build",
66
- parts: [{
67
- type: "text",
68
- text: createPlanExecutionKickoffText(planPath),
69
- synthetic: true,
70
- }],
71
- },
72
- });
73
- });
60
+ // Install the OpenCode plan-exit-shaped kickoff only when the OpenCode client
61
+ // exposes that structural API. Without it, no synthetic kickoff is registered.
62
+ const sessionClient = input.client?.session;
63
+ const promptAsync = sessionClient?.promptAsync;
64
+ setPlanExecutionKickoff(typeof promptAsync === "function"
65
+ ? async ({ sessionID, planPath }) => {
66
+ await promptAsync.call(sessionClient, {
67
+ path: { id: sessionID },
68
+ body: {
69
+ agent: "build",
70
+ parts: [{
71
+ type: "text",
72
+ text: createPlanExecutionKickoffText(planPath),
73
+ synthetic: true,
74
+ }],
75
+ },
76
+ });
77
+ }
78
+ : undefined);
74
79
  // Last access token successfully resolved in this plugin instance. Config's
75
80
  // loadModels can only read OpenCode's durable store (auth.json /
76
81
  // OPENCODE_AUTH_CONTENT); auth.loader gets live credentials via getAuth().
@@ -217,6 +222,12 @@ export async function CursorPlugin(input) {
217
222
  sessionActivity.recordActivity(event.properties.part.sessionID);
218
223
  break;
219
224
  }
225
+ await dispatchHostEventBridge({
226
+ event,
227
+ client: input.client,
228
+ directory: input.directory,
229
+ serverUrl: input.serverUrl,
230
+ });
220
231
  },
221
232
  async "tool.execute.before"(hookInput, output) {
222
233
  if (hookInput.tool !== "bash")
@@ -55,6 +55,12 @@ export declare const CURSOR_MODEL_COSTS: {
55
55
  readonly cache_read: 0.5;
56
56
  readonly cache_write: 6.25;
57
57
  };
58
+ readonly "claude-opus-5-5": {
59
+ readonly input: 4;
60
+ readonly output: 20;
61
+ readonly cache_read: 0.2;
62
+ readonly cache_write: 5;
63
+ };
58
64
  readonly "claude-sonnet-4": {
59
65
  readonly input: 3;
60
66
  readonly output: 15;
@@ -241,6 +247,26 @@ export declare const CURSOR_MODEL_COSTS: {
241
247
  readonly output: 12;
242
248
  readonly cache_read: 1;
243
249
  };
250
+ readonly "grok-4.7": {
251
+ readonly input: 2;
252
+ readonly output: 6;
253
+ readonly cache_read: 0.5;
254
+ readonly context_over_200k: {
255
+ readonly input: 4;
256
+ readonly output: 12;
257
+ readonly cache_read: 1;
258
+ };
259
+ };
260
+ readonly "grok-4.7-fast": {
261
+ readonly input: 4;
262
+ readonly output: 12;
263
+ readonly cache_read: 1;
264
+ readonly context_over_200k: {
265
+ readonly input: 6;
266
+ readonly output: 18;
267
+ readonly cache_read: 1.5;
268
+ };
269
+ };
244
270
  readonly "kimi-k2.7-code": {
245
271
  readonly input: 0.95;
246
272
  readonly output: 4;
@@ -289,6 +315,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
289
315
  readonly maxContext: 300000;
290
316
  readonly maxContextForMaxMode: 1000000;
291
317
  };
318
+ readonly "claude-opus-5-5": {
319
+ readonly maxContext: 300000;
320
+ readonly maxContextForMaxMode: 1000000;
321
+ };
292
322
  readonly "claude-sonnet-4": {
293
323
  readonly maxContext: 200000;
294
324
  readonly maxContextForMaxMode: 1000000;
@@ -383,6 +413,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
383
413
  readonly "grok-4.6": {
384
414
  readonly maxContext: 256000;
385
415
  };
416
+ readonly "grok-4.7": {
417
+ readonly maxContext: 256000;
418
+ readonly maxContextForMaxMode: 500000;
419
+ };
386
420
  readonly "kimi-k2.7-code": {
387
421
  readonly maxContext: 262000;
388
422
  };
@@ -420,6 +454,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
420
454
  readonly "claude-opus-5": {
421
455
  readonly supportsImages: true;
422
456
  };
457
+ readonly "claude-opus-5-5": {
458
+ readonly supportsImages: true;
459
+ };
423
460
  readonly "claude-sonnet-4": {
424
461
  readonly supportsImages: true;
425
462
  };
@@ -498,6 +535,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
498
535
  readonly "grok-4.6": {
499
536
  readonly supportsImages: false;
500
537
  };
538
+ readonly "grok-4.7": {
539
+ readonly supportsImages: false;
540
+ };
501
541
  readonly "kimi-k2.7-code": {
502
542
  readonly supportsImages: true;
503
543
  };
@@ -55,6 +55,12 @@ export const CURSOR_MODEL_COSTS = {
55
55
  "cache_read": 0.5,
56
56
  "cache_write": 6.25
57
57
  },
58
+ "claude-opus-5-5": {
59
+ "input": 4,
60
+ "output": 20,
61
+ "cache_read": 0.2,
62
+ "cache_write": 5
63
+ },
58
64
  "claude-sonnet-4": {
59
65
  "input": 3,
60
66
  "output": 15,
@@ -241,6 +247,26 @@ export const CURSOR_MODEL_COSTS = {
241
247
  "output": 12,
242
248
  "cache_read": 1
243
249
  },
250
+ "grok-4.7": {
251
+ "input": 2,
252
+ "output": 6,
253
+ "cache_read": 0.5,
254
+ "context_over_200k": {
255
+ "input": 4,
256
+ "output": 12,
257
+ "cache_read": 1
258
+ }
259
+ },
260
+ "grok-4.7-fast": {
261
+ "input": 4,
262
+ "output": 12,
263
+ "cache_read": 1,
264
+ "context_over_200k": {
265
+ "input": 6,
266
+ "output": 18,
267
+ "cache_read": 1.5
268
+ }
269
+ },
244
270
  "kimi-k2.7-code": {
245
271
  "input": 0.95,
246
272
  "output": 4,
@@ -289,6 +315,10 @@ export const CURSOR_MODEL_CONTEXTS = {
289
315
  "maxContext": 300000,
290
316
  "maxContextForMaxMode": 1000000
291
317
  },
318
+ "claude-opus-5-5": {
319
+ "maxContext": 300000,
320
+ "maxContextForMaxMode": 1000000
321
+ },
292
322
  "claude-sonnet-4": {
293
323
  "maxContext": 200000,
294
324
  "maxContextForMaxMode": 1000000
@@ -383,6 +413,10 @@ export const CURSOR_MODEL_CONTEXTS = {
383
413
  "grok-4.6": {
384
414
  "maxContext": 256000
385
415
  },
416
+ "grok-4.7": {
417
+ "maxContext": 256000,
418
+ "maxContextForMaxMode": 500000
419
+ },
386
420
  "kimi-k2.7-code": {
387
421
  "maxContext": 262000
388
422
  },
@@ -420,6 +454,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
420
454
  "claude-opus-5": {
421
455
  "supportsImages": true
422
456
  },
457
+ "claude-opus-5-5": {
458
+ "supportsImages": true
459
+ },
423
460
  "claude-sonnet-4": {
424
461
  "supportsImages": true
425
462
  },
@@ -498,6 +535,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
498
535
  "grok-4.6": {
499
536
  "supportsImages": false
500
537
  },
538
+ "grok-4.7": {
539
+ "supportsImages": false
540
+ },
501
541
  "kimi-k2.7-code": {
502
542
  "supportsImages": true
503
543
  },
@@ -9,7 +9,7 @@
9
9
  * and fall back to prose instead of ever calling the advertised host tool.
10
10
  *
11
11
  * Every rule below mirrors Cursor CLI, decompiled at
12
- * `~/Projects/cursor-mock-server/cursor/cli{,-local}`:
12
+ * `<cursor-cli-checkout>/cursor/cli{,-local}`:
13
13
  *
14
14
  * - `src/utils/interaction-utils.ts` — `BU` (freeform option id), `Q7` (display
15
15
  * options), `iX` (selection → success), `N$` (rejection + default reason).
@@ -98,19 +98,18 @@ export declare function rejectedResult(reason?: string): AskQuestionResultMessag
98
98
  /** `AskQuestionResult{async}` — the immediate reply for `run_async` queries. */
99
99
  export declare function asyncResult(): AskQuestionResultMessage;
100
100
  /**
101
- * Pull each question's answer text out of OpenCode's `question` tool output.
101
+ * Pull each question's answer text out of a host `question` tool output.
102
102
  *
103
- * The tool returns prose, not structured data (`metadata.answers` does not
104
- * cross the AI SDK boundary):
103
+ * OpenCode returns prose (`metadata.answers` does not cross the AI SDK
104
+ * boundary):
105
105
  *
106
106
  * User has answered your questions: "<q1>"="<a, b>", "<q2>"="Unanswered". You
107
107
  * can now continue with the user's answers in mind.
108
108
  *
109
- * The provider authored the question strings, so each answer is located by its
110
- * own `"<question>"="` anchor scanning forward — robust against commas, quotes
111
- * and `"="` inside question or answer text, and against duplicate questions.
112
- * Returns undefined for a question whose anchor is absent, which is treated as
113
- * unanswered rather than guessed at.
109
+ * The prose is located by its `"<question>"="` anchor — robust
110
+ * against commas, quotes and `"="` inside question or answer text, and against
111
+ * duplicate questions. Returns undefined for a question whose anchor is
112
+ * absent, which is treated as unanswered rather than guessed at.
114
113
  */
115
114
  export declare function parseAnswerSegments(questions: readonly CursorAskQuestionItem[], output: string): Array<string | undefined>;
116
115
  /**
@@ -9,7 +9,7 @@
9
9
  * and fall back to prose instead of ever calling the advertised host tool.
10
10
  *
11
11
  * Every rule below mirrors Cursor CLI, decompiled at
12
- * `~/Projects/cursor-mock-server/cursor/cli{,-local}`:
12
+ * `<cursor-cli-checkout>/cursor/cli{,-local}`:
13
13
  *
14
14
  * - `src/utils/interaction-utils.ts` — `BU` (freeform option id), `Q7` (display
15
15
  * options), `iX` (selection → success), `N$` (rejection + default reason).
@@ -218,19 +218,18 @@ export function asyncResult() {
218
218
  return { async: {} };
219
219
  }
220
220
  /**
221
- * Pull each question's answer text out of OpenCode's `question` tool output.
221
+ * Pull each question's answer text out of a host `question` tool output.
222
222
  *
223
- * The tool returns prose, not structured data (`metadata.answers` does not
224
- * cross the AI SDK boundary):
223
+ * OpenCode returns prose (`metadata.answers` does not cross the AI SDK
224
+ * boundary):
225
225
  *
226
226
  * User has answered your questions: "<q1>"="<a, b>", "<q2>"="Unanswered". You
227
227
  * can now continue with the user's answers in mind.
228
228
  *
229
- * The provider authored the question strings, so each answer is located by its
230
- * own `"<question>"="` anchor scanning forward — robust against commas, quotes
231
- * and `"="` inside question or answer text, and against duplicate questions.
232
- * Returns undefined for a question whose anchor is absent, which is treated as
233
- * unanswered rather than guessed at.
229
+ * The prose is located by its `"<question>"="` anchor — robust
230
+ * against commas, quotes and `"="` inside question or answer text, and against
231
+ * duplicate questions. Returns undefined for a question whose anchor is
232
+ * absent, which is treated as unanswered rather than guessed at.
234
233
  */
235
234
  export function parseAnswerSegments(questions, output) {
236
235
  const answers = [];
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { clearCheckpoint } from "./checkpoint.js";
3
3
  import { clearConversationBlobs } from "./blob-store.js";
4
- import { clearFrozenRequestContext, transferFrozenRequestContext, } from "../context/frozen.js";
4
+ import { transferFrozenRequestContext, } from "../context/frozen.js";
5
5
  /**
6
6
  * OpenCode session key → active Cursor conversation_id.
7
7
  *
@@ -11,8 +11,15 @@ import { clearFrozenRequestContext, transferFrozenRequestContext, } from "../con
11
11
  * cached conversation against OpenCode's newly compacted prompt.
12
12
  */
13
13
  const activeBySession = new Map();
14
+ // A compaction id cannot be reconstructed from its host session key. Keep its
15
+ // tiny binding even when the recent-session index soft-evicts that session.
16
+ const remintedBySession = new Map();
14
17
  export const MAX_ACTIVE_CONVERSATION_BINDINGS = 256;
15
18
  function rememberConversation(sessionKey, conversationId) {
19
+ if (conversationId === sessionIdToUuid(sessionKey))
20
+ remintedBySession.delete(sessionKey);
21
+ else
22
+ remintedBySession.set(sessionKey, conversationId);
16
23
  // Map insertion order is the LRU order. Refresh existing sessions on use.
17
24
  activeBySession.delete(sessionKey);
18
25
  activeBySession.set(sessionKey, conversationId);
@@ -21,19 +28,21 @@ function rememberConversation(sessionKey, conversationId) {
21
28
  if (!oldest)
22
29
  break;
23
30
  activeBySession.delete(oldest[0]);
24
- clearCheckpoint(oldest[1]);
25
- clearConversationBlobs(oldest[1]);
26
- clearFrozenRequestContext(oldest[1]);
31
+ // Evict only the lookup pointer. The checkpoint may still be needed by a
32
+ // held Run, and a returning session can hydrate its durable binding before
33
+ // resolving another turn. Compaction/reset owns deletion of opaque state.
27
34
  }
28
35
  }
29
36
  export function resetConversationBindingsForTests() {
30
37
  activeBySession.clear();
38
+ remintedBySession.clear();
31
39
  }
32
40
  export function hasConversationBinding(sessionKey) {
33
- return activeBySession.has(sessionKey);
41
+ return activeBySession.has(sessionKey) || remintedBySession.has(sessionKey);
34
42
  }
35
43
  export function isActiveConversationBinding(sessionKey, conversationId) {
36
- return activeBySession.get(sessionKey) === conversationId;
44
+ return (activeBySession.get(sessionKey) ?? remintedBySession.get(sessionKey)
45
+ ?? sessionIdToUuid(sessionKey)) === conversationId;
37
46
  }
38
47
  /** Restore a validated durable binding before resolving the next Run. */
39
48
  export function restoreConversationBinding(sessionKey, conversationId) {
@@ -63,7 +72,7 @@ export function resolveConversationGroupId(sessionKey, conversationId) {
63
72
  }
64
73
  /** Current Cursor conversation_id for an OpenCode session key, creating the default binding if needed. */
65
74
  export function peekConversationId(sessionKey) {
66
- let id = activeBySession.get(sessionKey);
75
+ let id = activeBySession.get(sessionKey) ?? remintedBySession.get(sessionKey);
67
76
  if (!id) {
68
77
  id = sessionIdToUuid(sessionKey);
69
78
  }
@@ -83,7 +92,8 @@ export function bindConversationId(sessionKey, opts) {
83
92
  return { conversationId: crypto.randomUUID(), reset: !!opts?.reset };
84
93
  }
85
94
  if (opts?.reset) {
86
- const previousId = activeBySession.get(sessionKey) ?? sessionIdToUuid(sessionKey);
95
+ const previousId = activeBySession.get(sessionKey) ?? remintedBySession.get(sessionKey)
96
+ ?? sessionIdToUuid(sessionKey);
87
97
  const conversationId = crypto.randomUUID();
88
98
  clearCheckpoint(previousId);
89
99
  clearConversationBlobs(previousId);
@@ -54,9 +54,10 @@ export type CursorPlanStageInput = {
54
54
  * How a CreatePlan query is satisfied this turn. Keyed only on the advertised
55
55
  * catalog and the provider's own plan-mode state — never on host identity.
56
56
  *
57
- * - `stage` a host plan-stage tool is advertised; the host writes the plan
58
- * *and* owns the execution-approval prompt, and its tool result is
59
- * the approval outcome (success = approved, error = not approved).
57
+ * - `stage` a host plan-stage tool is advertised; the host writes the plan.
58
+ * Its tool result is the approval outcome (success = approved,
59
+ * error = not approved). The result may name the host's own
60
+ * follow-up approval call. This provider does not invent that prompt.
60
61
  * - `approve` no such tool, but the host advertises `question`: the provider
61
62
  * writes the plan itself and then asks the same thing with upstream
62
63
  * `PlanExitTool`'s own prompt.
@@ -95,10 +96,8 @@ export declare function createPlanApprovalQuestionInput(planLabel: string): Open
95
96
  /**
96
97
  * True when the emulated approval prompt came back as an explicit "Yes".
97
98
  * An unanswered, dismissed, or failed prompt keeps the model planning rather
98
- * than silently starting execution.
99
- *
100
- * `question` must be the exact prompt that was asked — the host echoes it back
101
- * verbatim, and it is the anchor the answer is parsed from.
99
+ * than silently starting execution. OpenCode prose is anchored on the exact
100
+ * prompt text. Every other result shape fails closed.
102
101
  */
103
102
  export declare function createPlanApproved(output: string, isError: boolean, question: string): boolean;
104
103
  /** Decode a `create_plan_request_query` body, or undefined when unusable. */
@@ -156,16 +156,15 @@ export function createPlanApprovalQuestionInput(planLabel) {
156
156
  /**
157
157
  * True when the emulated approval prompt came back as an explicit "Yes".
158
158
  * An unanswered, dismissed, or failed prompt keeps the model planning rather
159
- * than silently starting execution.
160
- *
161
- * `question` must be the exact prompt that was asked — the host echoes it back
162
- * verbatim, and it is the anchor the answer is parsed from.
159
+ * than silently starting execution. OpenCode prose is anchored on the exact
160
+ * prompt text. Every other result shape fails closed.
163
161
  */
164
162
  export function createPlanApproved(output, isError, question) {
165
163
  if (isError)
166
164
  return false;
167
165
  const [segment] = parseAnswerSegments([createPlanApprovalItem(question)], output);
168
- return (segment ?? "").trim().toLowerCase() === CREATE_PLAN_APPROVAL_YES.toLowerCase();
166
+ const answer = (segment ?? "").trim().toLowerCase();
167
+ return answer === CREATE_PLAN_APPROVAL_YES.toLowerCase();
169
168
  }
170
169
  function asRecord(value) {
171
170
  return value && typeof value === "object" && !Array.isArray(value)
@@ -287,7 +287,22 @@ export function createMessageTypes() {
287
287
  { id: 3, name: "pattern", type: "string" },
288
288
  { id: 4, name: "tool_call_id", type: "string" },
289
289
  ]);
290
- addType(root, "GetMcpToolsToolCall", [{ id: 1, name: "args", type: "GetMcpToolsArgs" }]);
290
+ // agent.v1 GetMcpToolsToolCall is args + result. Cursor's agent fills result
291
+ // locally (GetDynamicTools / GetMcpTools); large catalogs spill through
292
+ // write_args and put the spill path on GetMcpToolsSuccess.output_file_path.
293
+ addType(root, "GetMcpToolsSuccess", [
294
+ { id: 1, name: "content", type: "string" },
295
+ { id: 2, name: "output_file_path", type: "string" },
296
+ ]);
297
+ addType(root, "GetMcpToolsError", [{ id: 1, name: "error", type: "string" }]);
298
+ addType(root, "GetMcpToolsAgentResult", [
299
+ { id: 1, name: "success", type: "GetMcpToolsSuccess" },
300
+ { id: 2, name: "error", type: "GetMcpToolsError" },
301
+ ], [{ name: "result", fields: ["success", "error"] }]);
302
+ addType(root, "GetMcpToolsToolCall", [
303
+ { id: 1, name: "args", type: "GetMcpToolsArgs" },
304
+ { id: 2, name: "result", type: "GetMcpToolsAgentResult" },
305
+ ]);
291
306
  addType(root, "GenerateImageToolArgs", [
292
307
  { id: 1, name: "description", type: "string" },
293
308
  { id: 2, name: "file_path", type: "string" },
@@ -472,6 +487,8 @@ export function createMessageTypes() {
472
487
  { id: 1, name: "matches", type: "GrepFileMatch", repeated: true },
473
488
  { id: 2, name: "total_lines", type: "int32" },
474
489
  { id: 3, name: "total_matched_lines", type: "int32" },
490
+ { id: 4, name: "client_truncated", type: "bool" },
491
+ { id: 5, name: "ripgrep_truncated", type: "bool" },
475
492
  ]);
476
493
  addType(root, "GrepUnionResult", [
477
494
  { id: 2, name: "files", type: "GrepFilesResult" },
@@ -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).sort(([left], [right]) => left.localeCompare(right))) {
252
+ for (const [key, val] of Object.entries(v).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) {
253
253
  if (val === undefined)
254
254
  continue;
255
255
  const entry = [];
@@ -30,10 +30,8 @@
30
30
  * - leaving plan mode is the gate the user must actually see, so it falls back
31
31
  * to the host `question` tool and only rejects when that is absent too.
32
32
  *
33
- * Resolution is keyed solely on the advertised catalog under canonical tool
34
- * names — never on host identity or model id. A compatibility layer may restate
35
- * alternate host vocabulary under canonical names before the provider sees the
36
- * catalog, so the bridge itself remains OpenCode-shaped.
33
+ * Resolution is keyed solely on the advertised catalog under canonical
34
+ * OpenCode tool names — never on any external identity or model id.
37
35
  *
38
36
  * CLI reject reason for user declines: "Mode switch rejected by user"
39
37
  * (chunk-7076/dist/ui.js onSwitchModeReject).
@@ -169,6 +167,7 @@ export declare function resetActiveCursorModesForTests(): void;
169
167
  export declare function cursorModeSystemReminder(targetModeId: string, options?: {
170
168
  firstTurn?: boolean;
171
169
  planExitAdvertised?: boolean;
170
+ planStageAdvertised?: boolean;
172
171
  }): string | undefined;
173
172
  /**
174
173
  * Consume the active-mode reminder for this OpenCode session.
@@ -30,10 +30,8 @@
30
30
  * - leaving plan mode is the gate the user must actually see, so it falls back
31
31
  * to the host `question` tool and only rejects when that is absent too.
32
32
  *
33
- * Resolution is keyed solely on the advertised catalog under canonical tool
34
- * names — never on host identity or model id. A compatibility layer may restate
35
- * alternate host vocabulary under canonical names before the provider sees the
36
- * catalog, so the bridge itself remains OpenCode-shaped.
33
+ * Resolution is keyed solely on the advertised catalog under canonical
34
+ * OpenCode tool names — never on any external identity or model id.
37
35
  *
38
36
  * CLI reject reason for user declines: "Mode switch rejected by user"
39
37
  * (chunk-7076/dist/ui.js onSwitchModeReject).
@@ -295,12 +293,15 @@ export function cursorModeSystemReminder(targetModeId, options = {}) {
295
293
  if (!id)
296
294
  return undefined;
297
295
  const first = options.firstTurn !== false;
298
- // Without a host plan_exit, recording the plan is itself the gate: the
299
- // provider persists it and then asks the user whether to start implementing.
300
- // Naming an unavailable tool would strand the model.
301
- const leavePlan = options.planExitAdvertised === false
302
- ? "record the finished plan (Cursor CreatePlan). Writing it needs no approval, and the user is then asked whether to start implementing; if they decline, refine the plan and record it again"
303
- : "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode";
296
+ // A host plan-stage tool writes the artifact and its result names the
297
+ // follow-up that requests approval. plan_exit only leaves plan mode.
298
+ // Without either tool, recording the plan is the gate and the provider asks
299
+ // whether to start implementing. Naming an unavailable tool would strand the model.
300
+ const leavePlan = options.planStageAdvertised
301
+ ? "record the finished plan with Cursor CreatePlan. The host stage tool waits for the host plan review and does not return until the user accepts or declines. Do not call `plan_exit` to submit or skip that review, and do not implement until the tool returns success"
302
+ : options.planExitAdvertised === false
303
+ ? "record the finished plan (Cursor CreatePlan). Writing it needs no approval, and the user is then asked whether to start implementing; if they decline, refine the plan and record it again"
304
+ : "record the finished plan, then call OpenCode `plan_exit` so the user can approve leaving plan mode";
304
305
  if (id === "plan" || id === "spec") {
305
306
  return wrapReminder(first
306
307
  ? `Plan mode is active. The user does not want execution yet -- you MUST NOT make edits, run non-readonly tools (including changing configs or making commits), or otherwise modify system state. This supersedes any conflicting instruction.
@@ -389,9 +390,12 @@ Your mandate is to convert user intent into a correct, high-quality implementati
389
390
  return wrapReminder(first
390
391
  ? `Agent mode is active (OpenCode build / agents). You have left plan mode and may implement: edit files, run tools, and execute the agreed approach.
391
392
 
393
+ The plan-mode restriction is over. An earlier reminder that forbade edits no longer applies.
394
+
395
+ - Change file contents with the advertised \`write\` and \`edit\` tools.
392
396
  - Prefer making progress with the advertised host tools.
393
397
  - If the task is large or ambiguous again, switch back to plan mode (OpenCode \`plan_enter\` / SwitchMode target plan) before a large rewrite.`
394
- : `Agent mode is still active. Continue implementing with the advertised host tools. Switch back to plan mode only when a new large/ambiguous design decision appears.`);
398
+ : `Agent mode is still active. Continue implementing with the advertised host tools, and change file contents with \`write\` and \`edit\`. Switch back to plan mode only when a new large/ambiguous design decision appears.`);
395
399
  }
396
400
  /**
397
401
  * Consume the active-mode reminder for this OpenCode session.
@@ -417,7 +421,12 @@ export function takeActiveCursorModeReminder(sessionKey, options = {}) {
417
421
  }
418
422
  const reminder = cursorModeSystemReminder(state.modeId, {
419
423
  firstTurn: state.firstTurn,
420
- ...(advertised ? { planExitAdvertised: advertised.has("plan_exit") } : {}),
424
+ ...(advertised
425
+ ? {
426
+ planExitAdvertised: advertised.has("plan_exit"),
427
+ planStageAdvertised: advertised.has("cursor_plan_stage"),
428
+ }
429
+ : {}),
421
430
  });
422
431
  if (state.firstTurn)
423
432
  state.firstTurn = false;
@@ -1,3 +1,4 @@
1
+ import { type HostToolDialect } from "./tools.js";
1
2
  /**
2
3
  * Cursor's interaction_update.tool_call_* carries a typed ToolCall oneof
3
4
  * (shell_tool_call, update_todos_tool_call, mcp_tool_call, …). Some of those
@@ -19,6 +20,8 @@ export type BridgedOpenCodeToolCall = {
19
20
  callId: string;
20
21
  variant: string;
21
22
  };
23
+ /** Display variants Cursor executes itself (mcp_state + optional write spill). */
24
+ export declare function isNativeDisplayToolCall(variant: string): boolean;
22
25
  /**
23
26
  * Normalize a full todo snapshot for `mirroredTodos` storage. Returns
24
27
  * undefined when the input is not a todo array (nothing to learn). Drops the
@@ -55,7 +58,7 @@ export declare function parseDisplayToolCall(callId: string, toolCall: Record<st
55
58
  * Pick an advertised OpenCode tool for a display call, remapping args as needed.
56
59
  * Returns undefined when nothing compatible is advertised this turn.
57
60
  */
58
- export declare function resolveBridgedOpenCodeToolCall(display: DisplayToolCall, advertised: Iterable<string>): BridgedOpenCodeToolCall | undefined;
61
+ export declare function resolveBridgedOpenCodeToolCall(display: DisplayToolCall, advertised: Iterable<string>, dialect?: HostToolDialect): BridgedOpenCodeToolCall | undefined;
59
62
  /** Advertised OpenCode tool ids from Cursor McpToolDefinition descriptors. */
60
63
  export declare function advertisedToolNamesFromDescriptors(descriptors: Array<Record<string, unknown>>): string[];
61
64
  /** Walk a protobuf message and return top-level field numbers present. */