cursor-opencode-provider 0.7.2 → 0.7.4

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 (53) 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/opencode2/mcp-direct.d.ts +26 -0
  23. package/dist/opencode2/mcp-direct.js +60 -0
  24. package/dist/opencode2/types.d.ts +24 -0
  25. package/dist/plugin-opencode2.js +24 -4
  26. package/dist/plugin.js +26 -15
  27. package/dist/pricing-data.d.ts +40 -0
  28. package/dist/pricing-data.js +40 -0
  29. package/dist/protocol/ask-question.d.ts +8 -9
  30. package/dist/protocol/ask-question.js +8 -9
  31. package/dist/protocol/conversation-bind.js +18 -8
  32. package/dist/protocol/create-plan.d.ts +6 -7
  33. package/dist/protocol/create-plan.js +4 -5
  34. package/dist/protocol/messages.js +16 -1
  35. package/dist/protocol/struct.js +1 -1
  36. package/dist/protocol/switch-mode.d.ts +3 -4
  37. package/dist/protocol/switch-mode.js +21 -12
  38. package/dist/protocol/tool-call-bridge.d.ts +4 -1
  39. package/dist/protocol/tool-call-bridge.js +27 -5
  40. package/dist/protocol/tools.d.ts +4 -2
  41. package/dist/protocol/tools.js +247 -51
  42. package/dist/session-directory.d.ts +23 -4
  43. package/dist/session-directory.js +36 -4
  44. package/dist/session.d.ts +23 -3
  45. package/dist/session.js +68 -8
  46. package/dist/shared.d.ts +2 -0
  47. package/dist/shared.js +2 -0
  48. package/dist/transport/connect.js +78 -10
  49. package/dist/transport/https-proxy.d.ts +56 -0
  50. package/dist/transport/https-proxy.js +348 -0
  51. package/dist/usage.d.ts +11 -0
  52. package/dist/usage.js +33 -7
  53. 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
@@ -0,0 +1,26 @@
1
+ import type { ToolDraft } from "./types.js";
2
+ /**
3
+ * OpenCode 2 puts an MCP server's tools in Code Mode unless that server's
4
+ * config sets `codemode: false` (`packages/schema/src/mcp.ts`, applied in
5
+ * `packages/core/src/tool/mcp.ts`). Only `options.codemode === false` joins
6
+ * the AI SDK catalog (`packages/core/src/tool.ts`). Cursor can call a tool
7
+ * only when it is in that catalog.
8
+ *
9
+ * `config.codemode` is also the remote-connection switch: while it is not
10
+ * false, OpenCode appends `?codemode=false` so servers that bundle their own
11
+ * Code Mode return individual tools (`packages/core/src/mcp/client.ts`).
12
+ * This module therefore leaves server config alone and clears the tool option
13
+ * instead. An explicit server `codemode: true` stays in Code Mode. OpenCode's
14
+ * own namespaced tools (the `opencode` namespace) are not MCP servers and are
15
+ * left alone.
16
+ *
17
+ * The tool registry is location-scoped, so this placement is shared by every
18
+ * provider in the process. `"codemode": true` on a server is the per-server
19
+ * way to keep that server inside `execute`.
20
+ */
21
+ export declare function mcpServerNamespace(server: string): string;
22
+ export declare function rememberDirectMcpNamespaces(target: Set<string>, servers: readonly (readonly [string, {
23
+ readonly codemode?: boolean;
24
+ }])[]): void;
25
+ /** Move tools from the recorded MCP namespaces onto the direct catalog. */
26
+ export declare function exposeDirectMcpTools(editor: ToolDraft, namespaces: ReadonlySet<string>): void;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * OpenCode 2 puts an MCP server's tools in Code Mode unless that server's
3
+ * config sets `codemode: false` (`packages/schema/src/mcp.ts`, applied in
4
+ * `packages/core/src/tool/mcp.ts`). Only `options.codemode === false` joins
5
+ * the AI SDK catalog (`packages/core/src/tool.ts`). Cursor can call a tool
6
+ * only when it is in that catalog.
7
+ *
8
+ * `config.codemode` is also the remote-connection switch: while it is not
9
+ * false, OpenCode appends `?codemode=false` so servers that bundle their own
10
+ * Code Mode return individual tools (`packages/core/src/mcp/client.ts`).
11
+ * This module therefore leaves server config alone and clears the tool option
12
+ * instead. An explicit server `codemode: true` stays in Code Mode. OpenCode's
13
+ * own namespaced tools (the `opencode` namespace) are not MCP servers and are
14
+ * left alone.
15
+ *
16
+ * The tool registry is location-scoped, so this placement is shared by every
17
+ * provider in the process. `"codemode": true` on a server is the per-server
18
+ * way to keep that server inside `execute`.
19
+ */
20
+ export function mcpServerNamespace(server) {
21
+ return server.replace(/[^a-zA-Z0-9_-]/g, "_");
22
+ }
23
+ export function rememberDirectMcpNamespaces(target, servers) {
24
+ target.clear();
25
+ // The editor exposes normalized namespaces, not server ownership. Keep
26
+ // ambiguous namespaces in their host-selected placement: otherwise a server
27
+ // named `opencode` moves builtins too, or `my.docs` overrides an explicit
28
+ // Code Mode choice on `my_docs`. Exclusions must win regardless of order.
29
+ const excluded = new Set(["opencode"]);
30
+ for (const [name, config] of servers) {
31
+ const namespace = mcpServerNamespace(name);
32
+ if (config.codemode === true)
33
+ excluded.add(namespace);
34
+ else
35
+ target.add(namespace);
36
+ }
37
+ for (const namespace of excluded)
38
+ target.delete(namespace);
39
+ }
40
+ /** Move tools from the recorded MCP namespaces onto the direct catalog. */
41
+ export function exposeDirectMcpTools(editor, namespaces) {
42
+ if (namespaces.size === 0 || !editor.list || !editor.update)
43
+ return;
44
+ for (const tool of editor.list()) {
45
+ const namespace = tool.options?.namespace;
46
+ if (!namespace || !namespaces.has(namespace))
47
+ continue;
48
+ if (tool.options?.codemode === false)
49
+ continue;
50
+ editor.update(tool.id, (draft) => {
51
+ const options = {};
52
+ if (draft.options?.namespace !== undefined)
53
+ options.namespace = draft.options.namespace;
54
+ if (draft.options?.permission !== undefined)
55
+ options.permission = draft.options.permission;
56
+ options.codemode = false;
57
+ draft.options = options;
58
+ });
59
+ }
60
+ }
@@ -223,6 +223,15 @@ export type ToolDraft = {
223
223
  list?(): readonly (ToolDefinition & {
224
224
  readonly id: string;
225
225
  })[];
226
+ /** Host ToolEditor.update — missing ids are ignored. */
227
+ update?(id: string, update: (tool: {
228
+ options?: {
229
+ namespace?: string;
230
+ permission?: string;
231
+ codemode?: boolean;
232
+ pinned?: boolean;
233
+ };
234
+ }) => void): void;
226
235
  };
227
236
  export type ToolHookBaseFields = {
228
237
  readonly tool: string;
@@ -368,6 +377,20 @@ export type WebSearchDomain = {
368
377
  readonly transform: Transform<WebSearchEditor>;
369
378
  readonly reload: () => Promise<void>;
370
379
  };
380
+ /**
381
+ * MCP config reader this plugin uses. Server `codemode` is observed and not
382
+ * written: writing it also changes the remote raw-tool URL. The host
383
+ * `MCPEditor` is a superset.
384
+ */
385
+ export type McpServerConfig = {
386
+ readonly codemode?: boolean;
387
+ };
388
+ export type McpEditor = {
389
+ list(): readonly (readonly [string, McpServerConfig])[];
390
+ };
391
+ export type McpDomain = {
392
+ readonly transform: Transform<McpEditor>;
393
+ };
371
394
  export type PluginLocation = {
372
395
  readonly directory: string;
373
396
  };
@@ -381,6 +404,7 @@ export type PluginContext = {
381
404
  readonly location?: PluginLocation;
382
405
  readonly shell?: ShellDomain;
383
406
  readonly websearch?: WebSearchDomain;
407
+ readonly mcp?: McpDomain;
384
408
  };
385
409
  export type Cleanup = () => Promise<void> | void;
386
410
  export type Plugin2 = {
@@ -8,7 +8,9 @@ import { fetchOpenCodeWebSearchText, parseExaWebSearchResults, } from "./web-too
8
8
  import { captureCursorShellResult, cursorShellEnvForCommand, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
9
9
  import { applyCursorProviderInventory, CURSOR_INTEGRATION_ID } from "./opencode2/catalog.js";
10
10
  import { applyCursorIntegration, resolveCursorAccessToken } from "./opencode2/integration.js";
11
+ import { exposeDirectMcpTools, rememberDirectMcpNamespaces } from "./opencode2/mcp-direct.js";
11
12
  import { registerTodoTools } from "./opencode2/todo-tools.js";
13
+ import { OPENCODE_2_TOOL_DIALECT } from "./protocol/tools.js";
12
14
  import { clearSessionTodos } from "./todo-store.js";
13
15
  import { markCompactionSession } from "./compaction-marker.js";
14
16
  import { markSessionDirectory } from "./session-directory.js";
@@ -213,11 +215,14 @@ const plugin = {
213
215
  ...(token ? { accessToken: token } : {}),
214
216
  // Static fallback only. This hook fires once per model/package, not
215
217
  // 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.
218
+ // real per-request directory comes from `x-opencode-directory` and
219
+ // the session.context hook below via `getSessionDirectory`.
218
220
  workspaceRoot,
219
221
  cacheDir,
220
222
  ...event.options,
223
+ // Keep after `event.options` so the OC2 plugin always selects the
224
+ // `path`/`shell` dialect when advertised schemas are opaque.
225
+ defaultDialect: OPENCODE_2_TOOL_DIALECT,
221
226
  });
222
227
  }));
223
228
  await track(ctx.aisdk.hook("language", (event) => {
@@ -246,12 +251,25 @@ const plugin = {
246
251
  });
247
252
  }));
248
253
  }
254
+ // Namespaces whose tools belong on the direct catalog. Filled by the MCP
255
+ // transform (config is not written) and read when tool transforms replay,
256
+ // including after a later MCP discovery reload.
257
+ const directMcpNamespaces = new Set();
258
+ if (ctx.mcp) {
259
+ await track(ctx.mcp.transform((editor) => {
260
+ rememberDirectMcpNamespaces(directMcpNamespaces, editor.list());
261
+ }));
262
+ }
249
263
  await track(ctx.tool.transform((draft) => {
250
264
  // OpenCode 2 dropped host todowrite/todoread. Off by default
251
265
  // (`CURSOR_OPENCODE2_TODOS=1`/`true` force-enables). When on, register
252
266
  // them as direct catalog tools (`codemode: false` + output schema) if
253
267
  // the host does not already own those names. When off, register none.
254
268
  registerTodoTools(draft);
269
+ // MCP tools default into Code Mode. Move every server that did not
270
+ // explicitly opt in onto the direct catalog so Cursor can call them
271
+ // by name. See `opencode2/mcp-direct.ts`.
272
+ exposeDirectMcpTools(draft, directMcpNamespaces);
255
273
  }));
256
274
  // ── Shell timeout wrapper ────────────────────────────────
257
275
  await track(ctx.tool.hook("execute.before", (event) => {
@@ -324,8 +342,10 @@ const plugin = {
324
342
  }
325
343
  const rememberSessionDirectory = async (sessionID) => {
326
344
  try {
327
- const info = await ctx.session.get({ sessionID });
328
- markSessionDirectory(sessionID, info.location?.directory);
345
+ const info = (await ctx.session.get({ sessionID }));
346
+ // OpenCode 2.0 stable exposes a flat `directory`; older shapes nest it
347
+ // under `location.directory`. Prefer the flat field when both exist.
348
+ markSessionDirectory(sessionID, info.directory ?? info.location?.directory);
329
349
  }
330
350
  catch {
331
351
  // 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" },