@playwo/opencode-cursor-oauth 0.0.0-dev.4258a6733133 → 0.0.0-dev.4696faa690e4

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.
@@ -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,24 @@ 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
+ diagnostics: activeBridge.diagnostics,
71
+ });
42
72
  activeBridges.delete(bridgeKey);
43
73
  if (activeBridge.bridge.alive) {
44
74
  if (activeBridge.modelId !== modelId) {
@@ -93,6 +123,16 @@ export function handleChatCompletion(body, accessToken, context = {}) {
93
123
  : userText;
94
124
  const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
95
125
  payload.mcpTools = mcpTools;
126
+ logPluginInfo("Built Cursor run request payload", {
127
+ modelId,
128
+ bridgeKey,
129
+ convKey,
130
+ mcpToolCount: mcpTools.length,
131
+ conversationId: stored.conversationId,
132
+ hasCheckpoint: Boolean(stored.checkpoint),
133
+ replayTurnCount: replayTurns.length,
134
+ effectiveUserText,
135
+ });
96
136
  if (body.stream === false) {
97
137
  return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
98
138
  systemPrompt,
@@ -1,13 +1,9 @@
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";
3
+ import { appendBundledAgentsRule } from "../agent-rules";
4
4
  export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
5
5
  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);
6
+ const cloudRule = buildCloudRule(systemPrompt);
11
7
  let conversationState;
12
8
  if (checkpoint) {
13
9
  conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
@@ -40,7 +36,7 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
40
36
  turnBytes.push(toBinary(ConversationTurnStructureSchema, turnStructure));
41
37
  }
42
38
  conversationState = create(ConversationStateStructureSchema, {
43
- rootPromptMessagesJson: [systemBlobId],
39
+ rootPromptMessagesJson: [],
44
40
  turns: turnBytes,
45
41
  todos: [],
46
42
  pendingToolCalls: [],
@@ -64,14 +60,11 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
64
60
  value: create(UserMessageActionSchema, { userMessage }),
65
61
  },
66
62
  });
67
- return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
63
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
68
64
  }
69
65
  export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
70
66
  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);
67
+ const cloudRule = buildCloudRule(systemPrompt);
75
68
  const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
76
69
  const action = create(ConversationActionSchema, {
77
70
  action: {
@@ -79,9 +72,9 @@ export function buildCursorResumeRequest(modelId, systemPrompt, conversationId,
79
72
  value: create(ResumeActionSchema, {}),
80
73
  },
81
74
  });
82
- return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
75
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
83
76
  }
84
- function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
77
+ function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
85
78
  const modelDetails = create(ModelDetailsSchema, {
86
79
  modelId,
87
80
  displayModelId: modelId,
@@ -99,6 +92,11 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
99
92
  return {
100
93
  requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
101
94
  blobStore,
95
+ cloudRule,
102
96
  mcpTools: [],
103
97
  };
104
98
  }
99
+ function buildCloudRule(systemPrompt) {
100
+ const content = systemPrompt.trim();
101
+ return appendBundledAgentsRule(content || undefined);
102
+ }
@@ -12,6 +12,12 @@ export interface UnsupportedServerMessageInfo {
12
12
  caseName: string;
13
13
  detail?: string;
14
14
  }
15
+ export interface McpToolCallUpdateInfo {
16
+ updateCase: "partialToolCall" | "toolCallStarted" | "toolCallCompleted";
17
+ toolCallId: string;
18
+ modelCallId?: string;
19
+ toolName?: string;
20
+ }
15
21
  export declare function parseConnectEndStream(data: Uint8Array): Error | null;
16
22
  export declare function makeHeartbeatBytes(): Uint8Array;
17
23
  export declare function scheduleBridgeEnd(bridge: CursorSession): void;
@@ -39,4 +45,4 @@ export declare function computeUsage(state: StreamState): {
39
45
  completion_tokens: number;
40
46
  total_tokens: number;
41
47
  };
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;
48
+ 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, onMcpToolCallUpdate?: (info: McpToolCallUpdateInfo) => 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, ClientHeartbeatSchema, ConversationStateStructureSchema, BackgroundShellSpawnResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpResultSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, 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, onMcpToolCallUpdate, 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, onMcpToolCallUpdate, 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?.({
@@ -146,10 +146,7 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
146
146
  });
147
147
  }
148
148
  else if (msgCase === "interactionQuery") {
149
- onUnsupportedMessage?.({
150
- category: "interactionQuery",
151
- caseName: msg.message.value.query.case ?? "undefined",
152
- });
149
+ handleInteractionQuery(msg.message.value, sendFrame, onUnsupportedMessage);
153
150
  }
154
151
  else if (msgCase === "conversationCheckpointUpdate") {
155
152
  const stateStructure = msg.message.value;
@@ -167,8 +164,35 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
167
164
  });
168
165
  }
169
166
  }
170
- function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage) {
167
+ function handleInteractionUpdate(update, state, onText, onMcpToolCallUpdate, onTurnEnded, onUnsupportedMessage) {
171
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
+ }
180
+ if ((updateCase === "partialToolCall" ||
181
+ updateCase === "toolCallStarted" ||
182
+ updateCase === "toolCallCompleted") &&
183
+ update.message?.value?.toolCall?.tool?.case === "mcpToolCall") {
184
+ const toolValue = update.message.value;
185
+ const toolArgs = toolValue?.toolCall?.tool?.value?.args;
186
+ const toolCallId = toolArgs?.toolCallId || toolValue.callId;
187
+ if (toolCallId) {
188
+ onMcpToolCallUpdate?.({
189
+ updateCase,
190
+ toolCallId,
191
+ modelCallId: toolValue.modelCallId,
192
+ toolName: toolArgs?.toolName || toolArgs?.name,
193
+ });
194
+ }
195
+ }
172
196
  if (updateCase === "textDelta") {
173
197
  const delta = update.message.value.text || "";
174
198
  if (delta)
@@ -183,15 +207,19 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
183
207
  state.outputTokens += update.message.value.tokens ?? 0;
184
208
  }
185
209
  else if (updateCase === "partialToolCall") {
186
- const partial = update.message.value;
187
- if (partial.callId && partial.argsTextDelta) {
188
- state.interactionToolArgsText.set(partial.callId, partial.argsTextDelta);
189
- }
210
+ return;
190
211
  }
191
212
  else if (updateCase === "toolCallCompleted") {
192
- const exec = decodeInteractionToolCall(update.message.value, state);
193
- if (exec)
194
- onMcpExec(exec);
213
+ const toolValue = update.message.value;
214
+ if (toolValue?.toolCall?.tool?.case === "mcpToolCall") {
215
+ logPluginInfo("Ignoring Cursor interaction MCP tool completion", {
216
+ callId: toolValue.callId,
217
+ modelCallId: toolValue.modelCallId,
218
+ toolCallId: toolValue.toolCall.tool.value?.args?.toolCallId || toolValue.callId,
219
+ toolName: toolValue.toolCall.tool.value?.args?.toolName ||
220
+ toolValue.toolCall.tool.value?.args?.name,
221
+ });
222
+ }
195
223
  }
196
224
  else if (updateCase === "turnEnded") {
197
225
  onTurnEnded?.();
@@ -214,43 +242,92 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
214
242
  caseName: updateCase ?? "undefined",
215
243
  });
216
244
  }
217
- // toolCallStarted, partialToolCall, toolCallDelta, and non-MCP
218
- // toolCallCompleted updates are informational only. Actionable MCP tool
219
- // calls may still appear here on some models, so we surface those, but we
220
- // do not abort the bridge for native Cursor tool-call progress events.
245
+ // Interaction tool-call updates are informational only. Resumable MCP tool
246
+ // execution comes from execServerMessage.mcpArgs.
221
247
  }
222
- function decodeInteractionToolCall(update, state) {
223
- const callId = update.callId ?? "";
224
- const toolCase = update.toolCall?.tool?.case;
225
- if (toolCase !== "mcpToolCall")
226
- return null;
227
- const mcpArgs = update.toolCall?.tool?.value?.args;
228
- if (!mcpArgs)
229
- return null;
230
- const toolCallId = mcpArgs.toolCallId || callId || crypto.randomUUID();
231
- if (state.emittedToolCallIds.has(toolCallId))
232
- return null;
233
- const decodedMap = decodeMcpArgsMap(mcpArgs.args ?? {});
234
- const partialArgsText = callId
235
- ? state.interactionToolArgsText.get(callId)?.trim()
236
- : undefined;
237
- let decodedArgs = "{}";
238
- if (Object.keys(decodedMap).length > 0) {
239
- decodedArgs = JSON.stringify(decodedMap);
240
- }
241
- else if (partialArgsText) {
242
- decodedArgs = partialArgsText;
243
- }
244
- state.emittedToolCallIds.add(toolCallId);
245
- if (callId)
246
- state.interactionToolArgsText.delete(callId);
247
- return {
248
- execId: callId || toolCallId,
249
- execMsgId: 0,
250
- toolCallId,
251
- toolName: mcpArgs.toolName || mcpArgs.name || "unknown_mcp_tool",
252
- decodedArgs,
253
- };
248
+ function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
249
+ const queryCase = query.query.case;
250
+ if (queryCase === "webSearchRequestQuery") {
251
+ const response = create(WebSearchRequestResponseSchema, {
252
+ result: {
253
+ case: "rejected",
254
+ value: create(WebSearchRequestResponse_RejectedSchema, {
255
+ reason: "Native Cursor web search is not available in this environment. Use the provided MCP tool `websearch` instead.",
256
+ }),
257
+ },
258
+ });
259
+ sendInteractionResponse(query.id, "webSearchRequestResponse", response, sendFrame);
260
+ return;
261
+ }
262
+ if (queryCase === "askQuestionInteractionQuery") {
263
+ const response = create(AskQuestionInteractionResponseSchema, {
264
+ result: create(AskQuestionResultSchema, {
265
+ result: {
266
+ case: "rejected",
267
+ value: create(AskQuestionRejectedSchema, {
268
+ reason: "Native Cursor question prompts are not available in this environment. Use the provided MCP tool `question` instead.",
269
+ }),
270
+ },
271
+ }),
272
+ });
273
+ sendInteractionResponse(query.id, "askQuestionInteractionResponse", response, sendFrame);
274
+ return;
275
+ }
276
+ if (queryCase === "switchModeRequestQuery") {
277
+ const response = create(SwitchModeRequestResponseSchema, {
278
+ result: {
279
+ case: "rejected",
280
+ value: create(SwitchModeRequestResponse_RejectedSchema, {
281
+ reason: "Cursor mode switching is not available in this environment. Continue using the current agent and the provided MCP tools.",
282
+ }),
283
+ },
284
+ });
285
+ sendInteractionResponse(query.id, "switchModeRequestResponse", response, sendFrame);
286
+ return;
287
+ }
288
+ if (queryCase === "exaSearchRequestQuery") {
289
+ const response = create(ExaSearchRequestResponseSchema, {
290
+ result: {
291
+ case: "rejected",
292
+ value: create(ExaSearchRequestResponse_RejectedSchema, {
293
+ reason: "Native Cursor Exa search is not available in this environment. Use the provided MCP tool `websearch` instead.",
294
+ }),
295
+ },
296
+ });
297
+ sendInteractionResponse(query.id, "exaSearchRequestResponse", response, sendFrame);
298
+ return;
299
+ }
300
+ if (queryCase === "exaFetchRequestQuery") {
301
+ const response = create(ExaFetchRequestResponseSchema, {
302
+ result: {
303
+ case: "rejected",
304
+ value: create(ExaFetchRequestResponse_RejectedSchema, {
305
+ reason: "Native Cursor Exa fetch is not available in this environment. Use the provided MCP tools `websearch` and `webfetch` instead.",
306
+ }),
307
+ },
308
+ });
309
+ sendInteractionResponse(query.id, "exaFetchRequestResponse", response, sendFrame);
310
+ return;
311
+ }
312
+ if (queryCase === "createPlanRequestQuery") {
313
+ const response = create(CreatePlanRequestResponseSchema, {
314
+ result: create(CreatePlanResultSchema, {
315
+ planUri: "",
316
+ result: {
317
+ case: "error",
318
+ value: create(CreatePlanErrorSchema, {
319
+ error: "Native Cursor plan creation is not available in this environment. Use the provided MCP planning tools instead.",
320
+ }),
321
+ },
322
+ }),
323
+ });
324
+ sendInteractionResponse(query.id, "createPlanRequestResponse", response, sendFrame);
325
+ return;
326
+ }
327
+ onUnsupportedMessage?.({
328
+ category: "interactionQuery",
329
+ caseName: queryCase ?? "undefined",
330
+ });
254
331
  }
255
332
  /** Send a KV client response back to Cursor. */
256
333
  function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
@@ -263,6 +340,16 @@ function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
263
340
  });
264
341
  sendFrame(toBinary(AgentClientMessageSchema, clientMsg));
265
342
  }
343
+ function sendInteractionResponse(queryId, messageCase, value, sendFrame) {
344
+ const response = create(InteractionResponseSchema, {
345
+ id: queryId,
346
+ result: { case: messageCase, value: value },
347
+ });
348
+ const clientMessage = create(AgentClientMessageSchema, {
349
+ message: { case: "interactionResponse", value: response },
350
+ });
351
+ sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
352
+ }
266
353
  function handleKvMessage(kvMsg, blobStore, sendFrame) {
267
354
  const kvCase = kvMsg.message.case;
268
355
  if (kvCase === "getBlobArgs") {
@@ -283,16 +370,34 @@ function handleKvMessage(kvMsg, blobStore, sendFrame) {
283
370
  sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame);
284
371
  }
285
372
  }
286
- function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledExec) {
373
+ function handleExecMessage(execMsg, cloudRule, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec) {
287
374
  const execCase = execMsg.message.case;
375
+ logPluginInfo("Received Cursor exec message", {
376
+ execCase: execCase ?? "undefined",
377
+ execId: execMsg.execId,
378
+ execMsgId: execMsg.id,
379
+ });
288
380
  if (execCase === "requestContextArgs") {
381
+ logPluginInfo("Responding to Cursor requestContextArgs", {
382
+ execId: execMsg.execId,
383
+ execMsgId: execMsg.id,
384
+ mcpToolCount: mcpTools.length,
385
+ });
289
386
  const requestContext = create(RequestContextSchema, {
290
387
  rules: [],
291
388
  repositoryInfo: [],
292
389
  tools: mcpTools,
293
390
  gitRepos: [],
294
391
  projectLayouts: [],
295
- mcpInstructions: [],
392
+ mcpInstructions: [
393
+ create(McpInstructionsSchema, {
394
+ serverName: "opencode",
395
+ 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.",
396
+ }),
397
+ ],
398
+ cloudRule,
399
+ webSearchEnabled: false,
400
+ repositoryInfoShouldQueryProd: false,
296
401
  fileContents: {},
297
402
  customSubagents: [],
298
403
  });
@@ -308,13 +413,23 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
308
413
  if (execCase === "mcpArgs") {
309
414
  const mcpArgs = execMsg.message.value;
310
415
  const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
311
- onMcpExec({
416
+ const exec = {
312
417
  execId: execMsg.execId,
313
418
  execMsgId: execMsg.id,
314
419
  toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
315
420
  toolName: mcpArgs.toolName || mcpArgs.name,
316
421
  decodedArgs: JSON.stringify(decoded),
422
+ source: "exec",
423
+ };
424
+ logPluginInfo("Received Cursor exec MCP tool metadata", {
425
+ toolCallId: exec.toolCallId,
426
+ toolName: exec.toolName,
427
+ source: exec.source,
428
+ execId: exec.execId,
429
+ execMsgId: exec.execMsgId,
430
+ decodedArgs: exec.decodedArgs,
317
431
  });
432
+ onMcpExec(exec);
318
433
  return;
319
434
  }
320
435
  // --- Reject native Cursor tools ---
@@ -322,6 +437,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
322
437
  // so it falls back to our MCP tools (registered via RequestContext).
323
438
  const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
324
439
  if (execCase === "readArgs") {
440
+ logPluginInfo("Rejecting native Cursor read tool in favor of MCP", {
441
+ execId: execMsg.execId,
442
+ execMsgId: execMsg.id,
443
+ path: execMsg.message.value.path,
444
+ });
325
445
  const args = execMsg.message.value;
326
446
  const result = create(ReadResultSchema, {
327
447
  result: {
@@ -336,6 +456,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
336
456
  return;
337
457
  }
338
458
  if (execCase === "lsArgs") {
459
+ logPluginInfo("Rejecting native Cursor ls tool in favor of MCP", {
460
+ execId: execMsg.execId,
461
+ execMsgId: execMsg.id,
462
+ path: execMsg.message.value.path,
463
+ });
339
464
  const args = execMsg.message.value;
340
465
  const result = create(LsResultSchema, {
341
466
  result: {
@@ -350,6 +475,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
350
475
  return;
351
476
  }
352
477
  if (execCase === "grepArgs") {
478
+ logPluginInfo("Rejecting native Cursor grep tool in favor of MCP", {
479
+ execId: execMsg.execId,
480
+ execMsgId: execMsg.id,
481
+ });
353
482
  const result = create(GrepResultSchema, {
354
483
  result: {
355
484
  case: "error",
@@ -360,6 +489,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
360
489
  return;
361
490
  }
362
491
  if (execCase === "writeArgs") {
492
+ logPluginInfo("Rejecting native Cursor write tool in favor of MCP", {
493
+ execId: execMsg.execId,
494
+ execMsgId: execMsg.id,
495
+ path: execMsg.message.value.path,
496
+ });
363
497
  const args = execMsg.message.value;
364
498
  const result = create(WriteResultSchema, {
365
499
  result: {
@@ -374,6 +508,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
374
508
  return;
375
509
  }
376
510
  if (execCase === "deleteArgs") {
511
+ logPluginInfo("Rejecting native Cursor delete tool in favor of MCP", {
512
+ execId: execMsg.execId,
513
+ execMsgId: execMsg.id,
514
+ path: execMsg.message.value.path,
515
+ });
377
516
  const args = execMsg.message.value;
378
517
  const result = create(DeleteResultSchema, {
379
518
  result: {
@@ -388,6 +527,13 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
388
527
  return;
389
528
  }
390
529
  if (execCase === "shellArgs" || execCase === "shellStreamArgs") {
530
+ logPluginInfo("Rejecting native Cursor shell tool in favor of MCP", {
531
+ execId: execMsg.execId,
532
+ execMsgId: execMsg.id,
533
+ command: execMsg.message.value.command ?? "",
534
+ workingDirectory: execMsg.message.value.workingDirectory ?? "",
535
+ execCase,
536
+ });
391
537
  const args = execMsg.message.value;
392
538
  const result = create(ShellResultSchema, {
393
539
  result: {
@@ -404,6 +550,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
404
550
  return;
405
551
  }
406
552
  if (execCase === "backgroundShellSpawnArgs") {
553
+ logPluginInfo("Rejecting native Cursor background shell tool in favor of MCP", {
554
+ execId: execMsg.execId,
555
+ execMsgId: execMsg.id,
556
+ command: execMsg.message.value.command ?? "",
557
+ workingDirectory: execMsg.message.value.workingDirectory ?? "",
558
+ });
407
559
  const args = execMsg.message.value;
408
560
  const result = create(BackgroundShellSpawnResultSchema, {
409
561
  result: {
@@ -420,6 +572,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
420
572
  return;
421
573
  }
422
574
  if (execCase === "writeShellStdinArgs") {
575
+ logPluginInfo("Rejecting native Cursor shell stdin tool in favor of MCP", {
576
+ execId: execMsg.execId,
577
+ execMsgId: execMsg.id,
578
+ });
423
579
  const result = create(WriteShellStdinResultSchema, {
424
580
  result: {
425
581
  case: "error",
@@ -430,6 +586,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
430
586
  return;
431
587
  }
432
588
  if (execCase === "fetchArgs") {
589
+ logPluginInfo("Rejecting native Cursor fetch tool in favor of MCP", {
590
+ execId: execMsg.execId,
591
+ execMsgId: execMsg.id,
592
+ url: execMsg.message.value.url,
593
+ });
433
594
  const args = execMsg.message.value;
434
595
  const result = create(FetchResultSchema, {
435
596
  result: {
@@ -444,6 +605,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
444
605
  return;
445
606
  }
446
607
  if (execCase === "diagnosticsArgs") {
608
+ logPluginInfo("Rejecting native Cursor diagnostics tool in favor of MCP", {
609
+ execId: execMsg.execId,
610
+ execMsgId: execMsg.id,
611
+ path: execMsg.message.value.path,
612
+ });
447
613
  const result = create(DiagnosticsResultSchema, {});
448
614
  sendExecResult(execMsg, "diagnosticsResult", result, sendFrame);
449
615
  return;
@@ -457,6 +623,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
457
623
  };
458
624
  const resultCase = miscCaseMap[execCase];
459
625
  if (resultCase) {
626
+ logPluginInfo("Responding to miscellaneous Cursor exec message", {
627
+ execCase,
628
+ execId: execMsg.execId,
629
+ execMsgId: execMsg.id,
630
+ resultCase,
631
+ });
460
632
  sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame);
461
633
  return;
462
634
  }
@@ -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,14 +15,26 @@ 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;
21
+ }
22
+ export interface ActiveBridgeDiagnostics {
23
+ announcedToolCallIds: string[];
24
+ publishedToolCallIds: string[];
25
+ lastMcpUpdate?: string;
26
+ publishedAtMs?: number;
27
+ lastResumeAttemptAtMs?: number;
17
28
  }
18
29
  /** A live Cursor session kept alive across requests for tool result continuation. */
19
30
  export interface ActiveBridge {
20
31
  bridge: CursorSession;
21
32
  heartbeatTimer: NodeJS.Timeout;
22
33
  blobStore: Map<string, Uint8Array>;
34
+ cloudRule?: string;
23
35
  mcpTools: McpToolDefinition[];
24
36
  pendingExecs: PendingExec[];
25
37
  modelId: string;
26
38
  metadata: ConversationRequestMetadata;
39
+ diagnostics?: ActiveBridgeDiagnostics;
27
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwo/opencode-cursor-oauth",
3
- "version": "0.0.0-dev.4258a6733133",
3
+ "version": "0.0.0-dev.4696faa690e4",
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",
@@ -18,8 +18,7 @@
18
18
  "dist"
19
19
  ],
20
20
  "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "test": "bun test/smoke.ts",
21
+ "build": "node ./scripts/build.mjs",
23
22
  "prepublishOnly": "npm run build"
24
23
  },
25
24
  "repository": {