@playwo/opencode-cursor-oauth 0.0.0-dev.4463bb589222 → 0.0.0-dev.628837adf8c9

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/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
  }
@@ -38,14 +38,12 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
38
38
  pendingExecs: [],
39
39
  outputTokens: 0,
40
40
  totalTokens: 0,
41
- interactionToolArgsText: new Map(),
42
- emittedToolCallIds: new Set(),
43
41
  };
44
42
  const tagFilter = createThinkingTagFilter();
45
43
  bridge.onData(createConnectFrameParser((messageBytes) => {
46
44
  try {
47
45
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
48
- processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
46
+ processServerMessage(serverMessage, payload.blobStore, payload.rules, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
49
47
  if (isThinking)
50
48
  return;
51
49
  const { content } = tagFilter.process(text);
@@ -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,6 +1,6 @@
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";
@@ -8,7 +8,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
10
  const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
11
- function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
11
+ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, rules, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
12
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
13
13
  const created = Math.floor(Date.now() / 1000);
14
14
  let keepaliveTimer;
@@ -27,8 +27,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
27
27
  pendingExecs: [],
28
28
  outputTokens: 0,
29
29
  totalTokens: 0,
30
- interactionToolArgsText: new Map(),
31
- emittedToolCallIds: new Set(),
32
30
  };
33
31
  const tagFilter = createThinkingTagFilter();
34
32
  let assistantText = metadata.assistantSeedText ?? "";
@@ -90,7 +88,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
90
88
  const processChunk = createConnectFrameParser((messageBytes) => {
91
89
  try {
92
90
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
93
- processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
91
+ processServerMessage(serverMessage, blobStore, rules, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
94
92
  if (isThinking) {
95
93
  sendSSE(makeChunk({ reasoning_content: text }));
96
94
  return;
@@ -103,7 +101,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
103
101
  sendSSE(makeChunk({ content }));
104
102
  }
105
103
  }, (exec) => {
106
- state.pendingExecs.push(exec);
104
+ const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
105
+ if (existingIndex >= 0) {
106
+ state.pendingExecs[existingIndex] = exec;
107
+ }
108
+ else {
109
+ state.pendingExecs.push(exec);
110
+ }
107
111
  mcpExecReceived = true;
108
112
  const flushed = tagFilter.flush();
109
113
  if (flushed.reasoning)
@@ -142,6 +146,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
142
146
  bridge,
143
147
  heartbeatTimer,
144
148
  blobStore,
149
+ rules,
145
150
  mcpTools,
146
151
  pendingExecs: state.pendingExecs,
147
152
  modelId,
@@ -206,8 +211,23 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
206
211
  stopKeepalive();
207
212
  }
208
213
  }, SSE_KEEPALIVE_INTERVAL_MS);
214
+ logPluginInfo("Opened Cursor streaming bridge", {
215
+ modelId,
216
+ bridgeKey,
217
+ convKey,
218
+ mcpToolCount: mcpTools.length,
219
+ ruleCount: rules.length,
220
+ });
209
221
  bridge.onData(processChunk);
210
222
  bridge.onClose((code) => {
223
+ logPluginInfo("Cursor streaming bridge closed", {
224
+ modelId,
225
+ bridgeKey,
226
+ convKey,
227
+ code,
228
+ mcpExecReceived,
229
+ hadEndStreamError: Boolean(endStreamError),
230
+ });
211
231
  clearInterval(heartbeatTimer);
212
232
  stopKeepalive();
213
233
  syncStoredBlobStore(convKey, blobStore);
@@ -257,11 +277,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
257
277
  return new Response(stream, { headers: SSE_HEADERS });
258
278
  }
259
279
  export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
280
+ logPluginInfo("Starting Cursor streaming response", {
281
+ modelId,
282
+ bridgeKey,
283
+ convKey,
284
+ mcpToolCount: payload.mcpTools.length,
285
+ });
260
286
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
261
- return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
287
+ return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.rules, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
288
+ }
289
+ async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
290
+ const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
291
+ const deadline = Date.now() + timeoutMs;
292
+ while (Date.now() < deadline) {
293
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
294
+ if (unresolved.length === 0) {
295
+ return unresolved;
296
+ }
297
+ await new Promise((resolve) => setTimeout(resolve, 25));
298
+ }
299
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
300
+ if (unresolved.length > 0) {
301
+ logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
302
+ bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
303
+ modelId: active.modelId,
304
+ });
305
+ }
306
+ return unresolved;
262
307
  }
263
- export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
264
- const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
308
+ export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
309
+ const { bridge, heartbeatTimer, blobStore, rules, mcpTools, pendingExecs, modelId, metadata, } = active;
265
310
  const resumeMetadata = {
266
311
  ...metadata,
267
312
  assistantSeedText: [
@@ -271,6 +316,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
271
316
  .filter(Boolean)
272
317
  .join("\n\n"),
273
318
  };
319
+ logPluginInfo("Preparing Cursor tool-result resume", {
320
+ bridgeKey,
321
+ convKey,
322
+ modelId,
323
+ toolResults,
324
+ pendingExecs,
325
+ });
326
+ const unresolved = await waitForResolvablePendingExecs(active, toolResults);
327
+ logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
328
+ bridgeKey,
329
+ convKey,
330
+ modelId,
331
+ toolResults,
332
+ pendingExecs,
333
+ unresolvedPendingExecs: unresolved,
334
+ });
335
+ if (unresolved.length > 0) {
336
+ clearInterval(heartbeatTimer);
337
+ bridge.end();
338
+ return new Response(JSON.stringify({
339
+ error: {
340
+ message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
341
+ type: "invalid_request_error",
342
+ code: "cursor_missing_exec_metadata",
343
+ },
344
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
345
+ }
274
346
  for (const exec of pendingExecs) {
275
347
  const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
276
348
  const mcpResult = result
@@ -311,7 +383,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
311
383
  const clientMessage = create(AgentClientMessageSchema, {
312
384
  message: { case: "execClientMessage", value: execClientMessage },
313
385
  });
386
+ logPluginInfo("Sending Cursor tool-result resume message", {
387
+ bridgeKey,
388
+ convKey,
389
+ modelId,
390
+ toolCallId: exec.toolCallId,
391
+ toolName: exec.toolName,
392
+ source: exec.source,
393
+ execId: exec.execId,
394
+ execMsgId: exec.execMsgId,
395
+ cursorCallId: exec.cursorCallId,
396
+ modelCallId: exec.modelCallId,
397
+ matchedToolResult: result,
398
+ });
314
399
  bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
315
400
  }
316
- return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
401
+ return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, rules, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
317
402
  }
@@ -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, CursorRuleSchema, CursorRuleTypeAgentFetchedSchema, CursorRuleTypeSchema, 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 rules = buildCursorRules(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, rules);
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 rules = buildCursorRules(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, rules);
83
75
  }
84
- function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
76
+ function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, rules) {
85
77
  const modelDetails = create(ModelDetailsSchema, {
86
78
  modelId,
87
79
  displayModelId: modelId,
@@ -99,6 +91,27 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
99
91
  return {
100
92
  requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
101
93
  blobStore,
94
+ rules,
102
95
  mcpTools: [],
103
96
  };
104
97
  }
98
+ function buildCursorRules(systemPrompt) {
99
+ const content = systemPrompt.trim();
100
+ if (!content)
101
+ return [];
102
+ return [
103
+ create(CursorRuleSchema, {
104
+ fullPath: "/opencode/system-prompt.md",
105
+ content,
106
+ type: create(CursorRuleTypeSchema, {
107
+ type: {
108
+ case: "agentFetched",
109
+ value: create(CursorRuleTypeAgentFetchedSchema, {
110
+ description: "OpenCode system prompt",
111
+ }),
112
+ },
113
+ }),
114
+ source: 0,
115
+ }),
116
+ ];
117
+ }
@@ -1,4 +1,4 @@
1
- import { type AgentServerMessage, type McpToolDefinition } from "../proto/agent_pb";
1
+ import { type AgentServerMessage, type CursorRule, type McpToolDefinition } from "../proto/agent_pb";
2
2
  import type { CursorSession } from "../cursor/bidi-session";
3
3
  import type { StreamState } from "./stream-state";
4
4
  import type { PendingExec } from "./types";
@@ -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>, rules: CursorRule[], 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
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";
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, rules, 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, rules, 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,11 +354,21 @@ 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, rules, 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
- rules: [],
371
+ rules,
382
372
  repositoryInfo: [],
383
373
  tools: mcpTools,
384
374
  gitRepos: [],
@@ -399,13 +389,23 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
399
389
  if (execCase === "mcpArgs") {
400
390
  const mcpArgs = execMsg.message.value;
401
391
  const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
402
- onMcpExec({
392
+ const exec = {
403
393
  execId: execMsg.execId,
404
394
  execMsgId: execMsg.id,
405
395
  toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
406
396
  toolName: mcpArgs.toolName || mcpArgs.name,
407
397
  decodedArgs: JSON.stringify(decoded),
398
+ source: "exec",
399
+ };
400
+ logPluginInfo("Received Cursor exec MCP tool metadata", {
401
+ toolCallId: exec.toolCallId,
402
+ toolName: exec.toolName,
403
+ source: exec.source,
404
+ execId: exec.execId,
405
+ execMsgId: exec.execMsgId,
406
+ decodedArgs: exec.decodedArgs,
408
407
  });
408
+ onMcpExec(exec);
409
409
  return;
410
410
  }
411
411
  // --- Reject native Cursor tools ---
@@ -413,6 +413,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
413
413
  // so it falls back to our MCP tools (registered via RequestContext).
414
414
  const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
415
415
  if (execCase === "readArgs") {
416
+ logPluginInfo("Rejecting native Cursor read tool in favor of MCP", {
417
+ execId: execMsg.execId,
418
+ execMsgId: execMsg.id,
419
+ path: execMsg.message.value.path,
420
+ });
416
421
  const args = execMsg.message.value;
417
422
  const result = create(ReadResultSchema, {
418
423
  result: {
@@ -427,6 +432,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
427
432
  return;
428
433
  }
429
434
  if (execCase === "lsArgs") {
435
+ logPluginInfo("Rejecting native Cursor ls tool in favor of MCP", {
436
+ execId: execMsg.execId,
437
+ execMsgId: execMsg.id,
438
+ path: execMsg.message.value.path,
439
+ });
430
440
  const args = execMsg.message.value;
431
441
  const result = create(LsResultSchema, {
432
442
  result: {
@@ -441,6 +451,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
441
451
  return;
442
452
  }
443
453
  if (execCase === "grepArgs") {
454
+ logPluginInfo("Rejecting native Cursor grep tool in favor of MCP", {
455
+ execId: execMsg.execId,
456
+ execMsgId: execMsg.id,
457
+ });
444
458
  const result = create(GrepResultSchema, {
445
459
  result: {
446
460
  case: "error",
@@ -451,6 +465,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
451
465
  return;
452
466
  }
453
467
  if (execCase === "writeArgs") {
468
+ logPluginInfo("Rejecting native Cursor write tool in favor of MCP", {
469
+ execId: execMsg.execId,
470
+ execMsgId: execMsg.id,
471
+ path: execMsg.message.value.path,
472
+ });
454
473
  const args = execMsg.message.value;
455
474
  const result = create(WriteResultSchema, {
456
475
  result: {
@@ -465,6 +484,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
465
484
  return;
466
485
  }
467
486
  if (execCase === "deleteArgs") {
487
+ logPluginInfo("Rejecting native Cursor delete tool in favor of MCP", {
488
+ execId: execMsg.execId,
489
+ execMsgId: execMsg.id,
490
+ path: execMsg.message.value.path,
491
+ });
468
492
  const args = execMsg.message.value;
469
493
  const result = create(DeleteResultSchema, {
470
494
  result: {
@@ -479,6 +503,13 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
479
503
  return;
480
504
  }
481
505
  if (execCase === "shellArgs" || execCase === "shellStreamArgs") {
506
+ logPluginInfo("Rejecting native Cursor shell tool in favor of MCP", {
507
+ execId: execMsg.execId,
508
+ execMsgId: execMsg.id,
509
+ command: execMsg.message.value.command ?? "",
510
+ workingDirectory: execMsg.message.value.workingDirectory ?? "",
511
+ execCase,
512
+ });
482
513
  const args = execMsg.message.value;
483
514
  const result = create(ShellResultSchema, {
484
515
  result: {
@@ -495,6 +526,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
495
526
  return;
496
527
  }
497
528
  if (execCase === "backgroundShellSpawnArgs") {
529
+ logPluginInfo("Rejecting native Cursor background shell tool in favor of MCP", {
530
+ execId: execMsg.execId,
531
+ execMsgId: execMsg.id,
532
+ command: execMsg.message.value.command ?? "",
533
+ workingDirectory: execMsg.message.value.workingDirectory ?? "",
534
+ });
498
535
  const args = execMsg.message.value;
499
536
  const result = create(BackgroundShellSpawnResultSchema, {
500
537
  result: {
@@ -511,6 +548,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
511
548
  return;
512
549
  }
513
550
  if (execCase === "writeShellStdinArgs") {
551
+ logPluginInfo("Rejecting native Cursor shell stdin tool in favor of MCP", {
552
+ execId: execMsg.execId,
553
+ execMsgId: execMsg.id,
554
+ });
514
555
  const result = create(WriteShellStdinResultSchema, {
515
556
  result: {
516
557
  case: "error",
@@ -521,6 +562,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
521
562
  return;
522
563
  }
523
564
  if (execCase === "fetchArgs") {
565
+ logPluginInfo("Rejecting native Cursor fetch tool in favor of MCP", {
566
+ execId: execMsg.execId,
567
+ execMsgId: execMsg.id,
568
+ url: execMsg.message.value.url,
569
+ });
524
570
  const args = execMsg.message.value;
525
571
  const result = create(FetchResultSchema, {
526
572
  result: {
@@ -535,6 +581,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
535
581
  return;
536
582
  }
537
583
  if (execCase === "diagnosticsArgs") {
584
+ logPluginInfo("Rejecting native Cursor diagnostics tool in favor of MCP", {
585
+ execId: execMsg.execId,
586
+ execMsgId: execMsg.id,
587
+ path: execMsg.message.value.path,
588
+ });
538
589
  const result = create(DiagnosticsResultSchema, {});
539
590
  sendExecResult(execMsg, "diagnosticsResult", result, sendFrame);
540
591
  return;
@@ -548,6 +599,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
548
599
  };
549
600
  const resultCase = miscCaseMap[execCase];
550
601
  if (resultCase) {
602
+ logPluginInfo("Responding to miscellaneous Cursor exec message", {
603
+ execCase,
604
+ execId: execMsg.execId,
605
+ execMsgId: execMsg.id,
606
+ resultCase,
607
+ });
551
608
  sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame);
552
609
  return;
553
610
  }
@@ -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 { CursorRule, McpToolDefinition } from "../proto/agent_pb";
2
3
  import type { ConversationRequestMetadata } from "./conversation-meta";
3
- import type { McpToolDefinition } from "../proto/agent_pb";
4
4
  export interface CursorRequestPayload {
5
5
  requestBytes: Uint8Array;
6
6
  blobStore: Map<string, Uint8Array>;
7
+ rules: CursorRule[];
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
+ rules: CursorRule[];
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.628837adf8c9",
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": {