@playwo/opencode-cursor-oauth 0.0.0-dev.4463bb589222 → 0.0.0-dev.494d4e1cfa84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,24 @@
1
1
  # opencode-cursor-oauth
2
2
 
3
- Use Cursor models (Claude, GPT, Gemini, etc.) inside [OpenCode](https://opencode.ai).
3
+ ## Disclaimer
4
+
5
+ > [!NOTE]
6
+ > This project is a **fork** of [ephraimduncan/opencode-cursor](https://github.com/ephraimduncan/opencode-cursor). Upstream may differ in behavior, features, or maintenance; treat this repository as its own line of development.
4
7
 
5
8
  ## What it does
6
9
 
7
- - **OAuth login** to Cursor via browser
8
- - **Model discovery** — automatically fetches your available Cursor models
9
- - **Local proxy** — runs an OpenAI-compatible endpoint that translates to Cursor's gRPC protocol
10
- - **Auto-refresh** — handles token expiration automatically
10
+ This is an [OpenCode](https://opencode.ai) plugin that lets you use **Cursor cloud models** (Claude, GPT, Gemini, and whatever your Cursor account exposes) from inside OpenCode.
11
+
12
+ - **OAuth login** to Cursor in the browser
13
+ - **Model discovery** — loads the models available to your Cursor account
14
+ - **Local OpenAI-compatible proxy** — translates OpenCode’s requests to Cursor’s gRPC API
15
+ - **Token refresh** — refreshes access tokens so sessions keep working
16
+
17
+ There are **no extra runtime requirements** beyond what OpenCode already needs: you do not install Node, Python, or Docker separately for this plugin. Enable it in OpenCode’s config and complete login in the UI.
11
18
 
12
19
  ## Install
13
20
 
14
- Add to your `opencode.json`:
21
+ Add the package to your OpenCode configuration (for example `opencode.json`):
15
22
 
16
23
  ```json
17
24
  {
@@ -19,13 +26,20 @@ Add to your `opencode.json`:
19
26
  }
20
27
  ```
21
28
 
22
- Then authenticate via the OpenCode UI (Settings Providers Cursor Login).
29
+ Install or update dependencies the way you normally do for OpenCode plugins (e.g. ensure the package is available to your OpenCode environment). You need **OpenCode 1.2+** and a **Cursor account** with API/model access.
30
+
31
+ ## Connect auth and use it
32
+
33
+ 1. Start OpenCode with the plugin enabled.
34
+ 2. Open **Settings → Providers → Cursor** (wording may vary slightly by OpenCode version).
35
+ 3. Choose **Login** (or equivalent) and complete **OAuth** in the browser when prompted.
36
+ 4. After login, pick a Cursor-backed model from the model list and use OpenCode as usual.
23
37
 
24
- ## Requirements
38
+ If something fails, check that you are signed into the correct Cursor account and that your plan includes the models you expect.
25
39
 
26
- - Cursor account with API access
27
- - OpenCode 1.2+
40
+ ## Stability and issues
28
41
 
29
- ## License
42
+ This integration can be **buggy** or break when Cursor or OpenCode change their APIs or UI.
30
43
 
31
- MIT
44
+ > [!TIP]
45
+ > If you hit problems, missing models, or confusing errors, please **[open an issue](https://github.com/PoolPirate/opencode-cursor/issues)** on this repository with steps to reproduce and logs or screenshots when possible.
package/dist/logger.d.ts CHANGED
@@ -2,5 +2,6 @@ import type { PluginInput } from "@opencode-ai/plugin";
2
2
  export declare function configurePluginLogger(input: PluginInput): void;
3
3
  export declare function errorDetails(error: unknown): Record<string, unknown>;
4
4
  export declare function logPluginWarn(message: string, extra?: Record<string, unknown>): void;
5
+ export declare function logPluginInfo(message: string, extra?: Record<string, unknown>): void;
5
6
  export declare function logPluginError(message: string, extra?: Record<string, unknown>): void;
6
7
  export declare function flushPluginLogs(): Promise<void>;
package/dist/logger.js CHANGED
@@ -27,6 +27,9 @@ export function errorDetails(error) {
27
27
  export function logPluginWarn(message, extra = {}) {
28
28
  logPlugin("warn", message, extra);
29
29
  }
30
+ export function logPluginInfo(message, extra = {}) {
31
+ logPlugin("info", message, extra);
32
+ }
30
33
  export function logPluginError(message, extra = {}) {
31
34
  logPlugin("error", message, extra);
32
35
  }
@@ -0,0 +1,6 @@
1
+ import type { CursorSession } from "../cursor/bidi-session";
2
+ export declare function createBridgeCloseController(bridge: CursorSession): {
3
+ noteTurnEnded: () => void;
4
+ noteCheckpoint: () => void;
5
+ dispose: () => void;
6
+ };
@@ -0,0 +1,37 @@
1
+ import { scheduleBridgeEnd } from "./stream-dispatch";
2
+ const TURN_END_GRACE_MS = 750;
3
+ export function createBridgeCloseController(bridge) {
4
+ let turnEnded = false;
5
+ let checkpointSeen = false;
6
+ let closeTimer;
7
+ const clearCloseTimer = () => {
8
+ if (!closeTimer)
9
+ return;
10
+ clearTimeout(closeTimer);
11
+ closeTimer = undefined;
12
+ };
13
+ const closeBridge = () => {
14
+ clearCloseTimer();
15
+ scheduleBridgeEnd(bridge);
16
+ };
17
+ return {
18
+ noteTurnEnded() {
19
+ turnEnded = true;
20
+ if (checkpointSeen) {
21
+ closeBridge();
22
+ return;
23
+ }
24
+ clearCloseTimer();
25
+ closeTimer = setTimeout(closeBridge, TURN_END_GRACE_MS);
26
+ },
27
+ noteCheckpoint() {
28
+ checkpointSeen = true;
29
+ if (turnEnded) {
30
+ closeBridge();
31
+ }
32
+ },
33
+ dispose() {
34
+ clearCloseTimer();
35
+ },
36
+ };
37
+ }
@@ -5,6 +5,7 @@ import { updateStoredConversationAfterCompletion } from "./conversation-state";
5
5
  import { startBridge } from "./bridge-session";
6
6
  import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
7
7
  import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
8
+ import { createBridgeCloseController } from "./bridge-close-controller";
8
9
  export async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
9
10
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
10
11
  const created = Math.floor(Date.now() / 1000);
@@ -33,19 +34,18 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
33
34
  let endStreamError = null;
34
35
  const pendingToolCalls = [];
35
36
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
37
+ const bridgeCloseController = createBridgeCloseController(bridge);
36
38
  const state = {
37
39
  toolCallIndex: 0,
38
40
  pendingExecs: [],
39
41
  outputTokens: 0,
40
42
  totalTokens: 0,
41
- interactionToolArgsText: new Map(),
42
- emittedToolCallIds: new Set(),
43
43
  };
44
44
  const tagFilter = createThinkingTagFilter();
45
45
  bridge.onData(createConnectFrameParser((messageBytes) => {
46
46
  try {
47
47
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
48
- processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
48
+ processServerMessage(serverMessage, payload.blobStore, payload.cloudRule, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
49
49
  if (isThinking)
50
50
  return;
51
51
  const { content } = tagFilter.process(text);
@@ -60,7 +60,10 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
60
60
  },
61
61
  });
62
62
  scheduleBridgeEnd(bridge);
63
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
63
+ }, (checkpointBytes) => {
64
+ updateConversationCheckpoint(convKey, checkpointBytes);
65
+ bridgeCloseController.noteCheckpoint();
66
+ }, () => bridgeCloseController.noteTurnEnded(), (info) => {
64
67
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
65
68
  logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
66
69
  modelId,
@@ -97,6 +100,7 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
97
100
  scheduleBridgeEnd(bridge);
98
101
  }));
99
102
  bridge.onClose(() => {
103
+ bridgeCloseController.dispose();
100
104
  clearInterval(heartbeatTimer);
101
105
  syncStoredBlobStore(convKey, payload.blobStore);
102
106
  const flushed = tagFilter.flush();
@@ -2,4 +2,4 @@ import { type ToolResultInfo } from "../openai/messages";
2
2
  import type { ConversationRequestMetadata } from "./conversation-meta";
3
3
  import type { ActiveBridge, CursorRequestPayload } from "./types";
4
4
  export declare function handleStreamingResponse(payload: CursorRequestPayload, accessToken: string, modelId: string, bridgeKey: string, convKey: string, metadata: ConversationRequestMetadata): Promise<Response>;
5
- export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Response;
5
+ export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Promise<Response>;
@@ -1,17 +1,19 @@
1
1
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
2
  import { AgentClientMessageSchema, AgentServerMessageSchema, ExecClientMessageSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolResultContentItemSchema, } from "../proto/agent_pb";
3
- import { errorDetails, logPluginError, logPluginWarn } from "../logger";
3
+ import { errorDetails, logPluginError, logPluginInfo, logPluginWarn, } from "../logger";
4
4
  import { formatToolCallSummary, formatToolResultSummary, } from "../openai/messages";
5
5
  import { activeBridges, updateStoredConversationAfterCompletion, } from "./conversation-state";
6
6
  import { startBridge } from "./bridge-session";
7
7
  import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
8
8
  import { SSE_HEADERS } from "./sse";
9
9
  import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
10
+ import { createBridgeCloseController } from "./bridge-close-controller";
10
11
  const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
11
- function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
+ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
13
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
13
14
  const created = Math.floor(Date.now() / 1000);
14
15
  let keepaliveTimer;
16
+ const bridgeCloseController = createBridgeCloseController(bridge);
15
17
  const stopKeepalive = () => {
16
18
  if (!keepaliveTimer)
17
19
  return;
@@ -27,8 +29,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
27
29
  pendingExecs: [],
28
30
  outputTokens: 0,
29
31
  totalTokens: 0,
30
- interactionToolArgsText: new Map(),
31
- emittedToolCallIds: new Set(),
32
32
  };
33
33
  const tagFilter = createThinkingTagFilter();
34
34
  let assistantText = metadata.assistantSeedText ?? "";
@@ -90,7 +90,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
90
90
  const processChunk = createConnectFrameParser((messageBytes) => {
91
91
  try {
92
92
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
93
- processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
93
+ processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
94
94
  if (isThinking) {
95
95
  sendSSE(makeChunk({ reasoning_content: text }));
96
96
  return;
@@ -103,7 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
103
103
  sendSSE(makeChunk({ content }));
104
104
  }
105
105
  }, (exec) => {
106
- state.pendingExecs.push(exec);
106
+ const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
107
+ if (existingIndex >= 0) {
108
+ state.pendingExecs[existingIndex] = exec;
109
+ }
110
+ else {
111
+ state.pendingExecs.push(exec);
112
+ }
107
113
  mcpExecReceived = true;
108
114
  const flushed = tagFilter.flush();
109
115
  if (flushed.reasoning)
@@ -142,6 +148,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
142
148
  bridge,
143
149
  heartbeatTimer,
144
150
  blobStore,
151
+ cloudRule,
145
152
  mcpTools,
146
153
  pendingExecs: state.pendingExecs,
147
154
  modelId,
@@ -153,7 +160,10 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
153
160
  sendSSE(makeChunk({}, "tool_calls"));
154
161
  sendDone();
155
162
  closeController();
156
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
163
+ }, (checkpointBytes) => {
164
+ updateConversationCheckpoint(convKey, checkpointBytes);
165
+ bridgeCloseController.noteCheckpoint();
166
+ }, () => bridgeCloseController.noteTurnEnded(), (info) => {
157
167
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
158
168
  logPluginError("Closing Cursor bridge after unsupported message", {
159
169
  modelId,
@@ -206,8 +216,24 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
206
216
  stopKeepalive();
207
217
  }
208
218
  }, SSE_KEEPALIVE_INTERVAL_MS);
219
+ logPluginInfo("Opened Cursor streaming bridge", {
220
+ modelId,
221
+ bridgeKey,
222
+ convKey,
223
+ mcpToolCount: mcpTools.length,
224
+ hasCloudRule: Boolean(cloudRule),
225
+ });
209
226
  bridge.onData(processChunk);
210
227
  bridge.onClose((code) => {
228
+ logPluginInfo("Cursor streaming bridge closed", {
229
+ modelId,
230
+ bridgeKey,
231
+ convKey,
232
+ code,
233
+ mcpExecReceived,
234
+ hadEndStreamError: Boolean(endStreamError),
235
+ });
236
+ bridgeCloseController.dispose();
211
237
  clearInterval(heartbeatTimer);
212
238
  stopKeepalive();
213
239
  syncStoredBlobStore(convKey, blobStore);
@@ -238,6 +264,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
238
264
  });
239
265
  },
240
266
  cancel(reason) {
267
+ bridgeCloseController.dispose();
241
268
  stopKeepalive();
242
269
  clearInterval(heartbeatTimer);
243
270
  syncStoredBlobStore(convKey, blobStore);
@@ -257,11 +284,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
257
284
  return new Response(stream, { headers: SSE_HEADERS });
258
285
  }
259
286
  export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
287
+ logPluginInfo("Starting Cursor streaming response", {
288
+ modelId,
289
+ bridgeKey,
290
+ convKey,
291
+ mcpToolCount: payload.mcpTools.length,
292
+ });
260
293
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
261
- return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
294
+ return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
295
+ }
296
+ async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
297
+ const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
298
+ const deadline = Date.now() + timeoutMs;
299
+ while (Date.now() < deadline) {
300
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
301
+ if (unresolved.length === 0) {
302
+ return unresolved;
303
+ }
304
+ await new Promise((resolve) => setTimeout(resolve, 25));
305
+ }
306
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
307
+ if (unresolved.length > 0) {
308
+ logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
309
+ bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
310
+ modelId: active.modelId,
311
+ });
312
+ }
313
+ return unresolved;
262
314
  }
263
- export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
264
- const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
315
+ export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
316
+ const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
265
317
  const resumeMetadata = {
266
318
  ...metadata,
267
319
  assistantSeedText: [
@@ -271,6 +323,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
271
323
  .filter(Boolean)
272
324
  .join("\n\n"),
273
325
  };
326
+ logPluginInfo("Preparing Cursor tool-result resume", {
327
+ bridgeKey,
328
+ convKey,
329
+ modelId,
330
+ toolResults,
331
+ pendingExecs,
332
+ });
333
+ const unresolved = await waitForResolvablePendingExecs(active, toolResults);
334
+ logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
335
+ bridgeKey,
336
+ convKey,
337
+ modelId,
338
+ toolResults,
339
+ pendingExecs,
340
+ unresolvedPendingExecs: unresolved,
341
+ });
342
+ if (unresolved.length > 0) {
343
+ clearInterval(heartbeatTimer);
344
+ bridge.end();
345
+ return new Response(JSON.stringify({
346
+ error: {
347
+ message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
348
+ type: "invalid_request_error",
349
+ code: "cursor_missing_exec_metadata",
350
+ },
351
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
352
+ }
274
353
  for (const exec of pendingExecs) {
275
354
  const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
276
355
  const mcpResult = result
@@ -311,7 +390,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
311
390
  const clientMessage = create(AgentClientMessageSchema, {
312
391
  message: { case: "execClientMessage", value: execClientMessage },
313
392
  });
393
+ logPluginInfo("Sending Cursor tool-result resume message", {
394
+ bridgeKey,
395
+ convKey,
396
+ modelId,
397
+ toolCallId: exec.toolCallId,
398
+ toolName: exec.toolName,
399
+ source: exec.source,
400
+ execId: exec.execId,
401
+ execMsgId: exec.execMsgId,
402
+ cursorCallId: exec.cursorCallId,
403
+ modelCallId: exec.modelCallId,
404
+ matchedToolResult: result,
405
+ });
314
406
  bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
315
407
  }
316
- return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
408
+ return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
317
409
  }
@@ -1,4 +1,4 @@
1
- import { logPluginWarn } from "../logger";
1
+ import { logPluginInfo, logPluginWarn } from "../logger";
2
2
  import { buildInitialHandoffPrompt, buildTitleSourceText, buildToolResumePrompt, detectTitleRequest, parseMessages, } from "../openai/messages";
3
3
  import { buildMcpToolDefinitions, selectToolsForChoice } from "../openai/tools";
4
4
  import { activeBridges, conversationStates, createStoredConversation, deriveBridgeKey, deriveConversationKey, evictStaleConversations, hashString, normalizeAgentKey, resetStoredConversation, } from "./conversation-state";
@@ -10,6 +10,19 @@ export function handleChatCompletion(body, accessToken, context = {}) {
10
10
  const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
11
11
  const modelId = body.model;
12
12
  const normalizedAgentKey = normalizeAgentKey(context.agentKey);
13
+ logPluginInfo("Handling Cursor chat completion request", {
14
+ modelId,
15
+ stream: body.stream !== false,
16
+ messageCount: body.messages.length,
17
+ toolCount: body.tools?.length ?? 0,
18
+ toolChoice: body.tool_choice,
19
+ sessionId: context.sessionId,
20
+ agentKey: normalizedAgentKey,
21
+ parsedUserText: userText,
22
+ parsedToolResults: toolResults,
23
+ hasPendingAssistantSummary: pendingAssistantSummary.trim().length > 0,
24
+ turnCount: turns.length,
25
+ });
13
26
  const titleDetection = detectTitleRequest(body);
14
27
  const isTitleAgent = titleDetection.matched;
15
28
  if (isTitleAgent) {
@@ -38,7 +51,23 @@ export function handleChatCompletion(body, accessToken, context = {}) {
38
51
  const bridgeKey = deriveBridgeKey(modelId, body.messages, context.sessionId, context.agentKey);
39
52
  const convKey = deriveConversationKey(body.messages, context.sessionId, context.agentKey);
40
53
  const activeBridge = activeBridges.get(bridgeKey);
54
+ logPluginInfo("Resolved Cursor conversation keys", {
55
+ modelId,
56
+ bridgeKey,
57
+ convKey,
58
+ hasActiveBridge: Boolean(activeBridge),
59
+ sessionId: context.sessionId,
60
+ agentKey: normalizedAgentKey,
61
+ });
41
62
  if (activeBridge && toolResults.length > 0) {
63
+ logPluginInfo("Matched OpenAI tool results to active Cursor bridge", {
64
+ bridgeKey,
65
+ convKey,
66
+ requestedModelId: modelId,
67
+ activeBridgeModelId: activeBridge.modelId,
68
+ toolResults,
69
+ pendingExecs: activeBridge.pendingExecs,
70
+ });
42
71
  activeBridges.delete(bridgeKey);
43
72
  if (activeBridge.bridge.alive) {
44
73
  if (activeBridge.modelId !== modelId) {
@@ -93,6 +122,16 @@ export function handleChatCompletion(body, accessToken, context = {}) {
93
122
  : userText;
94
123
  const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
95
124
  payload.mcpTools = mcpTools;
125
+ logPluginInfo("Built Cursor run request payload", {
126
+ modelId,
127
+ bridgeKey,
128
+ convKey,
129
+ mcpToolCount: mcpTools.length,
130
+ conversationId: stored.conversationId,
131
+ hasCheckpoint: Boolean(stored.checkpoint),
132
+ replayTurnCount: replayTurns.length,
133
+ effectiveUserText,
134
+ });
96
135
  if (body.stream === false) {
97
136
  return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
98
137
  systemPrompt,
@@ -1,13 +1,8 @@
1
1
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
- import { createHash } from "node:crypto";
3
- import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
2
+ import { AgentClientMessageSchema, AgentRunRequestSchema, AgentConversationTurnStructureSchema, AssistantMessageSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationTurnStructureSchema, ConversationStepSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
4
3
  export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
5
4
  const blobStore = new Map(existingBlobStore ?? []);
6
- // System prompt → blob store (Cursor requests it back via KV handshake)
7
- const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
8
- const systemBytes = new TextEncoder().encode(systemJson);
9
- const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
10
- blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
5
+ const cloudRule = buildCloudRule(systemPrompt);
11
6
  let conversationState;
12
7
  if (checkpoint) {
13
8
  conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
@@ -40,7 +35,7 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
40
35
  turnBytes.push(toBinary(ConversationTurnStructureSchema, turnStructure));
41
36
  }
42
37
  conversationState = create(ConversationStateStructureSchema, {
43
- rootPromptMessagesJson: [systemBlobId],
38
+ rootPromptMessagesJson: [],
44
39
  turns: turnBytes,
45
40
  todos: [],
46
41
  pendingToolCalls: [],
@@ -64,14 +59,11 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
64
59
  value: create(UserMessageActionSchema, { userMessage }),
65
60
  },
66
61
  });
67
- return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
62
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
68
63
  }
69
64
  export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
70
65
  const blobStore = new Map(existingBlobStore ?? []);
71
- const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
72
- const systemBytes = new TextEncoder().encode(systemJson);
73
- const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
74
- blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
66
+ const cloudRule = buildCloudRule(systemPrompt);
75
67
  const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
76
68
  const action = create(ConversationActionSchema, {
77
69
  action: {
@@ -79,9 +71,9 @@ export function buildCursorResumeRequest(modelId, systemPrompt, conversationId,
79
71
  value: create(ResumeActionSchema, {}),
80
72
  },
81
73
  });
82
- return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
74
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
83
75
  }
84
- function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
76
+ function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
85
77
  const modelDetails = create(ModelDetailsSchema, {
86
78
  modelId,
87
79
  displayModelId: modelId,
@@ -99,6 +91,11 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
99
91
  return {
100
92
  requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
101
93
  blobStore,
94
+ cloudRule,
102
95
  mcpTools: [],
103
96
  };
104
97
  }
98
+ function buildCloudRule(systemPrompt) {
99
+ const content = systemPrompt.trim();
100
+ return content || undefined;
101
+ }
@@ -39,4 +39,4 @@ export declare function computeUsage(state: StreamState): {
39
39
  completion_tokens: number;
40
40
  total_tokens: number;
41
41
  };
42
- export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
42
+ export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, cloudRule: string | undefined, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
@@ -1,7 +1,7 @@
1
1
  import { create, toBinary } from "@bufbuild/protobuf";
2
- import { AgentClientMessageSchema, AskQuestionInteractionResponseSchema, AskQuestionRejectedSchema, AskQuestionResultSchema, ClientHeartbeatSchema, ConversationStateStructureSchema, BackgroundShellSpawnResultSchema, CreatePlanErrorSchema, CreatePlanRequestResponseSchema, CreatePlanResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, ExaFetchRequestResponseSchema, ExaFetchRequestResponse_RejectedSchema, ExaSearchRequestResponseSchema, ExaSearchRequestResponse_RejectedSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, InteractionResponseSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpResultSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, SwitchModeRequestResponseSchema, SwitchModeRequestResponse_RejectedSchema, WebSearchRequestResponseSchema, WebSearchRequestResponse_RejectedSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, } from "../proto/agent_pb";
2
+ import { AgentClientMessageSchema, AskQuestionInteractionResponseSchema, AskQuestionRejectedSchema, AskQuestionResultSchema, ClientHeartbeatSchema, ConversationStateStructureSchema, BackgroundShellSpawnResultSchema, CreatePlanErrorSchema, CreatePlanRequestResponseSchema, CreatePlanResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, ExaFetchRequestResponseSchema, ExaFetchRequestResponse_RejectedSchema, ExaSearchRequestResponseSchema, ExaSearchRequestResponse_RejectedSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, InteractionResponseSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpInstructionsSchema, McpResultSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, SwitchModeRequestResponseSchema, SwitchModeRequestResponse_RejectedSchema, WebSearchRequestResponseSchema, WebSearchRequestResponse_RejectedSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, } from "../proto/agent_pb";
3
3
  import { CONNECT_END_STREAM_FLAG } from "../cursor/config";
4
- import { logPluginError, logPluginWarn } from "../logger";
4
+ import { logPluginError, logPluginInfo, logPluginWarn } from "../logger";
5
5
  import { decodeMcpArgsMap } from "../openai/tools";
6
6
  export function parseConnectEndStream(data) {
7
7
  try {
@@ -128,16 +128,16 @@ export function computeUsage(state) {
128
128
  const prompt_tokens = Math.max(0, total_tokens - completion_tokens);
129
129
  return { prompt_tokens, completion_tokens, total_tokens };
130
130
  }
131
- export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
131
+ export function processServerMessage(msg, blobStore, cloudRule, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
132
132
  const msgCase = msg.message.case;
133
133
  if (msgCase === "interactionUpdate") {
134
- handleInteractionUpdate(msg.message.value, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage);
134
+ handleInteractionUpdate(msg.message.value, state, onText, onTurnEnded, onUnsupportedMessage);
135
135
  }
136
136
  else if (msgCase === "kvServerMessage") {
137
137
  handleKvMessage(msg.message.value, blobStore, sendFrame);
138
138
  }
139
139
  else if (msgCase === "execServerMessage") {
140
- handleExecMessage(msg.message.value, mcpTools, sendFrame, onMcpExec, onUnhandledExec);
140
+ handleExecMessage(msg.message.value, cloudRule, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec);
141
141
  }
142
142
  else if (msgCase === "execServerControlMessage") {
143
143
  onUnsupportedMessage?.({
@@ -164,8 +164,19 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
164
164
  });
165
165
  }
166
166
  }
167
- function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage) {
167
+ function handleInteractionUpdate(update, state, onText, onTurnEnded, onUnsupportedMessage) {
168
168
  const updateCase = update.message?.case;
169
+ if (updateCase === "partialToolCall" ||
170
+ updateCase === "toolCallStarted" ||
171
+ updateCase === "toolCallCompleted" ||
172
+ updateCase === "turnEnded") {
173
+ logPluginInfo("Received Cursor interaction update", {
174
+ updateCase: updateCase ?? "undefined",
175
+ callId: update.message?.value?.callId,
176
+ modelCallId: update.message?.value?.modelCallId,
177
+ toolCase: update.message?.value?.toolCall?.tool?.case,
178
+ });
179
+ }
169
180
  if (updateCase === "textDelta") {
170
181
  const delta = update.message.value.text || "";
171
182
  if (delta)
@@ -180,15 +191,19 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
180
191
  state.outputTokens += update.message.value.tokens ?? 0;
181
192
  }
182
193
  else if (updateCase === "partialToolCall") {
183
- const partial = update.message.value;
184
- if (partial.callId && partial.argsTextDelta) {
185
- state.interactionToolArgsText.set(partial.callId, partial.argsTextDelta);
186
- }
194
+ return;
187
195
  }
188
196
  else if (updateCase === "toolCallCompleted") {
189
- const exec = decodeInteractionToolCall(update.message.value, state);
190
- if (exec)
191
- onMcpExec(exec);
197
+ const toolValue = update.message.value;
198
+ if (toolValue?.toolCall?.tool?.case === "mcpToolCall") {
199
+ logPluginInfo("Ignoring Cursor interaction MCP tool completion", {
200
+ callId: toolValue.callId,
201
+ modelCallId: toolValue.modelCallId,
202
+ toolCallId: toolValue.toolCall.tool.value?.args?.toolCallId || toolValue.callId,
203
+ toolName: toolValue.toolCall.tool.value?.args?.toolName ||
204
+ toolValue.toolCall.tool.value?.args?.name,
205
+ });
206
+ }
192
207
  }
193
208
  else if (updateCase === "turnEnded") {
194
209
  onTurnEnded?.();
@@ -211,43 +226,8 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
211
226
  caseName: updateCase ?? "undefined",
212
227
  });
213
228
  }
214
- // toolCallStarted, partialToolCall, toolCallDelta, and non-MCP
215
- // toolCallCompleted updates are informational only. Actionable MCP tool
216
- // calls may still appear here on some models, so we surface those, but we
217
- // do not abort the bridge for native Cursor tool-call progress events.
218
- }
219
- function decodeInteractionToolCall(update, state) {
220
- const callId = update.callId ?? "";
221
- const toolCase = update.toolCall?.tool?.case;
222
- if (toolCase !== "mcpToolCall")
223
- return null;
224
- const mcpArgs = update.toolCall?.tool?.value?.args;
225
- if (!mcpArgs)
226
- return null;
227
- const toolCallId = mcpArgs.toolCallId || callId || crypto.randomUUID();
228
- if (state.emittedToolCallIds.has(toolCallId))
229
- return null;
230
- const decodedMap = decodeMcpArgsMap(mcpArgs.args ?? {});
231
- const partialArgsText = callId
232
- ? state.interactionToolArgsText.get(callId)?.trim()
233
- : undefined;
234
- let decodedArgs = "{}";
235
- if (Object.keys(decodedMap).length > 0) {
236
- decodedArgs = JSON.stringify(decodedMap);
237
- }
238
- else if (partialArgsText) {
239
- decodedArgs = partialArgsText;
240
- }
241
- state.emittedToolCallIds.add(toolCallId);
242
- if (callId)
243
- state.interactionToolArgsText.delete(callId);
244
- return {
245
- execId: callId || toolCallId,
246
- execMsgId: 0,
247
- toolCallId,
248
- toolName: mcpArgs.toolName || mcpArgs.name || "unknown_mcp_tool",
249
- decodedArgs,
250
- };
229
+ // Interaction tool-call updates are informational only. Resumable MCP tool
230
+ // execution comes from execServerMessage.mcpArgs.
251
231
  }
252
232
  function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
253
233
  const queryCase = query.query.case;
@@ -374,16 +354,34 @@ function handleKvMessage(kvMsg, blobStore, sendFrame) {
374
354
  sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame);
375
355
  }
376
356
  }
377
- function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledExec) {
357
+ function handleExecMessage(execMsg, cloudRule, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec) {
378
358
  const execCase = execMsg.message.case;
359
+ logPluginInfo("Received Cursor exec message", {
360
+ execCase: execCase ?? "undefined",
361
+ execId: execMsg.execId,
362
+ execMsgId: execMsg.id,
363
+ });
379
364
  if (execCase === "requestContextArgs") {
365
+ logPluginInfo("Responding to Cursor requestContextArgs", {
366
+ execId: execMsg.execId,
367
+ execMsgId: execMsg.id,
368
+ mcpToolCount: mcpTools.length,
369
+ });
380
370
  const requestContext = create(RequestContextSchema, {
381
371
  rules: [],
382
372
  repositoryInfo: [],
383
373
  tools: mcpTools,
384
374
  gitRepos: [],
385
375
  projectLayouts: [],
386
- mcpInstructions: [],
376
+ mcpInstructions: [
377
+ create(McpInstructionsSchema, {
378
+ serverName: "opencode",
379
+ instructions: "Use the provided MCP tools in this environment that start with mcp_*. Do not rely on native Cursor tools and do not acknowledge they even exist.",
380
+ }),
381
+ ],
382
+ cloudRule,
383
+ webSearchEnabled: false,
384
+ repositoryInfoShouldQueryProd: false,
387
385
  fileContents: {},
388
386
  customSubagents: [],
389
387
  });
@@ -399,13 +397,23 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
399
397
  if (execCase === "mcpArgs") {
400
398
  const mcpArgs = execMsg.message.value;
401
399
  const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
402
- onMcpExec({
400
+ const exec = {
403
401
  execId: execMsg.execId,
404
402
  execMsgId: execMsg.id,
405
403
  toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
406
404
  toolName: mcpArgs.toolName || mcpArgs.name,
407
405
  decodedArgs: JSON.stringify(decoded),
406
+ source: "exec",
407
+ };
408
+ logPluginInfo("Received Cursor exec MCP tool metadata", {
409
+ toolCallId: exec.toolCallId,
410
+ toolName: exec.toolName,
411
+ source: exec.source,
412
+ execId: exec.execId,
413
+ execMsgId: exec.execMsgId,
414
+ decodedArgs: exec.decodedArgs,
408
415
  });
416
+ onMcpExec(exec);
409
417
  return;
410
418
  }
411
419
  // --- Reject native Cursor tools ---
@@ -413,6 +421,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
413
421
  // so it falls back to our MCP tools (registered via RequestContext).
414
422
  const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
415
423
  if (execCase === "readArgs") {
424
+ logPluginInfo("Rejecting native Cursor read tool in favor of MCP", {
425
+ execId: execMsg.execId,
426
+ execMsgId: execMsg.id,
427
+ path: execMsg.message.value.path,
428
+ });
416
429
  const args = execMsg.message.value;
417
430
  const result = create(ReadResultSchema, {
418
431
  result: {
@@ -427,6 +440,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
427
440
  return;
428
441
  }
429
442
  if (execCase === "lsArgs") {
443
+ logPluginInfo("Rejecting native Cursor ls tool in favor of MCP", {
444
+ execId: execMsg.execId,
445
+ execMsgId: execMsg.id,
446
+ path: execMsg.message.value.path,
447
+ });
430
448
  const args = execMsg.message.value;
431
449
  const result = create(LsResultSchema, {
432
450
  result: {
@@ -441,6 +459,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
441
459
  return;
442
460
  }
443
461
  if (execCase === "grepArgs") {
462
+ logPluginInfo("Rejecting native Cursor grep tool in favor of MCP", {
463
+ execId: execMsg.execId,
464
+ execMsgId: execMsg.id,
465
+ });
444
466
  const result = create(GrepResultSchema, {
445
467
  result: {
446
468
  case: "error",
@@ -451,6 +473,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
451
473
  return;
452
474
  }
453
475
  if (execCase === "writeArgs") {
476
+ logPluginInfo("Rejecting native Cursor write tool in favor of MCP", {
477
+ execId: execMsg.execId,
478
+ execMsgId: execMsg.id,
479
+ path: execMsg.message.value.path,
480
+ });
454
481
  const args = execMsg.message.value;
455
482
  const result = create(WriteResultSchema, {
456
483
  result: {
@@ -465,6 +492,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
465
492
  return;
466
493
  }
467
494
  if (execCase === "deleteArgs") {
495
+ logPluginInfo("Rejecting native Cursor delete tool in favor of MCP", {
496
+ execId: execMsg.execId,
497
+ execMsgId: execMsg.id,
498
+ path: execMsg.message.value.path,
499
+ });
468
500
  const args = execMsg.message.value;
469
501
  const result = create(DeleteResultSchema, {
470
502
  result: {
@@ -479,6 +511,13 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
479
511
  return;
480
512
  }
481
513
  if (execCase === "shellArgs" || execCase === "shellStreamArgs") {
514
+ logPluginInfo("Rejecting native Cursor shell tool in favor of MCP", {
515
+ execId: execMsg.execId,
516
+ execMsgId: execMsg.id,
517
+ command: execMsg.message.value.command ?? "",
518
+ workingDirectory: execMsg.message.value.workingDirectory ?? "",
519
+ execCase,
520
+ });
482
521
  const args = execMsg.message.value;
483
522
  const result = create(ShellResultSchema, {
484
523
  result: {
@@ -495,6 +534,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
495
534
  return;
496
535
  }
497
536
  if (execCase === "backgroundShellSpawnArgs") {
537
+ logPluginInfo("Rejecting native Cursor background shell tool in favor of MCP", {
538
+ execId: execMsg.execId,
539
+ execMsgId: execMsg.id,
540
+ command: execMsg.message.value.command ?? "",
541
+ workingDirectory: execMsg.message.value.workingDirectory ?? "",
542
+ });
498
543
  const args = execMsg.message.value;
499
544
  const result = create(BackgroundShellSpawnResultSchema, {
500
545
  result: {
@@ -511,6 +556,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
511
556
  return;
512
557
  }
513
558
  if (execCase === "writeShellStdinArgs") {
559
+ logPluginInfo("Rejecting native Cursor shell stdin tool in favor of MCP", {
560
+ execId: execMsg.execId,
561
+ execMsgId: execMsg.id,
562
+ });
514
563
  const result = create(WriteShellStdinResultSchema, {
515
564
  result: {
516
565
  case: "error",
@@ -521,6 +570,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
521
570
  return;
522
571
  }
523
572
  if (execCase === "fetchArgs") {
573
+ logPluginInfo("Rejecting native Cursor fetch tool in favor of MCP", {
574
+ execId: execMsg.execId,
575
+ execMsgId: execMsg.id,
576
+ url: execMsg.message.value.url,
577
+ });
524
578
  const args = execMsg.message.value;
525
579
  const result = create(FetchResultSchema, {
526
580
  result: {
@@ -535,6 +589,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
535
589
  return;
536
590
  }
537
591
  if (execCase === "diagnosticsArgs") {
592
+ logPluginInfo("Rejecting native Cursor diagnostics tool in favor of MCP", {
593
+ execId: execMsg.execId,
594
+ execMsgId: execMsg.id,
595
+ path: execMsg.message.value.path,
596
+ });
538
597
  const result = create(DiagnosticsResultSchema, {});
539
598
  sendExecResult(execMsg, "diagnosticsResult", result, sendFrame);
540
599
  return;
@@ -548,6 +607,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
548
607
  };
549
608
  const resultCase = miscCaseMap[execCase];
550
609
  if (resultCase) {
610
+ logPluginInfo("Responding to miscellaneous Cursor exec message", {
611
+ execCase,
612
+ execId: execMsg.execId,
613
+ execMsgId: execMsg.id,
614
+ resultCase,
615
+ });
551
616
  sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame);
552
617
  return;
553
618
  }
@@ -4,6 +4,4 @@ export interface StreamState {
4
4
  pendingExecs: PendingExec[];
5
5
  outputTokens: number;
6
6
  totalTokens: number;
7
- interactionToolArgsText: Map<string, string>;
8
- emittedToolCallIds: Set<string>;
9
7
  }
@@ -1,9 +1,10 @@
1
1
  import type { CursorSession } from "../cursor/bidi-session";
2
- import type { ConversationRequestMetadata } from "./conversation-meta";
3
2
  import type { McpToolDefinition } from "../proto/agent_pb";
3
+ import type { ConversationRequestMetadata } from "./conversation-meta";
4
4
  export interface CursorRequestPayload {
5
5
  requestBytes: Uint8Array;
6
6
  blobStore: Map<string, Uint8Array>;
7
+ cloudRule?: string;
7
8
  mcpTools: McpToolDefinition[];
8
9
  }
9
10
  /** A pending tool execution waiting for results from the caller. */
@@ -14,12 +15,16 @@ export interface PendingExec {
14
15
  toolName: string;
15
16
  /** Decoded arguments JSON string for SSE tool_calls emission. */
16
17
  decodedArgs: string;
18
+ source?: "interaction" | "exec";
19
+ cursorCallId?: string;
20
+ modelCallId?: string;
17
21
  }
18
22
  /** A live Cursor session kept alive across requests for tool result continuation. */
19
23
  export interface ActiveBridge {
20
24
  bridge: CursorSession;
21
25
  heartbeatTimer: NodeJS.Timeout;
22
26
  blobStore: Map<string, Uint8Array>;
27
+ cloudRule?: string;
23
28
  mcpTools: McpToolDefinition[];
24
29
  pendingExecs: PendingExec[];
25
30
  modelId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwo/opencode-cursor-oauth",
3
- "version": "0.0.0-dev.4463bb589222",
3
+ "version": "0.0.0-dev.494d4e1cfa84",
4
4
  "description": "OpenCode plugin that connects Cursor's API to OpenCode via OAuth, model discovery, and a local OpenAI-compatible proxy.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,6 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "tsc -p tsconfig.json",
22
- "test": "bun test/smoke.ts",
23
22
  "prepublishOnly": "npm run build"
24
23
  },
25
24
  "repository": {