@playwo/opencode-cursor-oauth 0.0.0-dev.6338d5591e37 → 0.0.0-dev.67ecd4697583

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
  }
@@ -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";
@@ -103,6 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
103
103
  sendSSE(makeChunk({ content }));
104
104
  }
105
105
  }, (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
+ }
106
113
  mcpExecReceived = true;
107
114
  const flushed = tagFilter.flush();
108
115
  if (flushed.reasoning)
@@ -259,7 +266,26 @@ export async function handleStreamingResponse(payload, accessToken, modelId, bri
259
266
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
260
267
  return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
261
268
  }
262
- export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
269
+ async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
270
+ const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
271
+ const deadline = Date.now() + timeoutMs;
272
+ while (Date.now() < deadline) {
273
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
274
+ if (unresolved.length === 0) {
275
+ return unresolved;
276
+ }
277
+ await new Promise((resolve) => setTimeout(resolve, 25));
278
+ }
279
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
280
+ if (unresolved.length > 0) {
281
+ logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
282
+ bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
283
+ modelId: active.modelId,
284
+ });
285
+ }
286
+ return unresolved;
287
+ }
288
+ export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
263
289
  const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
264
290
  const resumeMetadata = {
265
291
  ...metadata,
@@ -270,6 +296,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
270
296
  .filter(Boolean)
271
297
  .join("\n\n"),
272
298
  };
299
+ logPluginInfo("Preparing Cursor tool-result resume", {
300
+ bridgeKey,
301
+ convKey,
302
+ modelId,
303
+ toolResults,
304
+ pendingExecs,
305
+ });
306
+ const unresolved = await waitForResolvablePendingExecs(active, toolResults);
307
+ logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
308
+ bridgeKey,
309
+ convKey,
310
+ modelId,
311
+ toolResults,
312
+ pendingExecs,
313
+ unresolvedPendingExecs: unresolved,
314
+ });
315
+ if (unresolved.length > 0) {
316
+ clearInterval(heartbeatTimer);
317
+ bridge.end();
318
+ return new Response(JSON.stringify({
319
+ error: {
320
+ message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
321
+ type: "invalid_request_error",
322
+ code: "cursor_missing_exec_metadata",
323
+ },
324
+ }), { status: 409, headers: { "Content-Type": "application/json" } });
325
+ }
273
326
  for (const exec of pendingExecs) {
274
327
  const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
275
328
  const mcpResult = result
@@ -310,6 +363,19 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
310
363
  const clientMessage = create(AgentClientMessageSchema, {
311
364
  message: { case: "execClientMessage", value: execClientMessage },
312
365
  });
366
+ logPluginInfo("Sending Cursor tool-result resume message", {
367
+ bridgeKey,
368
+ convKey,
369
+ modelId,
370
+ toolCallId: exec.toolCallId,
371
+ toolName: exec.toolName,
372
+ source: exec.source,
373
+ execId: exec.execId,
374
+ execMsgId: exec.execMsgId,
375
+ cursorCallId: exec.cursorCallId,
376
+ modelCallId: exec.modelCallId,
377
+ matchedToolResult: result,
378
+ });
313
379
  bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
314
380
  }
315
381
  return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
@@ -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";
@@ -39,6 +39,14 @@ export function handleChatCompletion(body, accessToken, context = {}) {
39
39
  const convKey = deriveConversationKey(body.messages, context.sessionId, context.agentKey);
40
40
  const activeBridge = activeBridges.get(bridgeKey);
41
41
  if (activeBridge && toolResults.length > 0) {
42
+ logPluginInfo("Matched OpenAI tool results to active Cursor bridge", {
43
+ bridgeKey,
44
+ convKey,
45
+ requestedModelId: modelId,
46
+ activeBridgeModelId: activeBridge.modelId,
47
+ toolResults,
48
+ pendingExecs: activeBridge.pendingExecs,
49
+ });
42
50
  activeBridges.delete(bridgeKey);
43
51
  if (activeBridge.bridge.alive) {
44
52
  if (activeBridge.modelId !== modelId) {
@@ -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,12 +128,8 @@ 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
- function getPendingExecKey(exec) {
132
- return exec.toolCallId || `${exec.toolName}:${exec.decodedArgs}`;
133
- }
134
131
  function replacePendingExec(state, exec) {
135
- const execKey = getPendingExecKey(exec);
136
- const existingIndex = state.pendingExecs.findIndex((candidate) => getPendingExecKey(candidate) === execKey);
132
+ const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
137
133
  if (existingIndex >= 0) {
138
134
  state.pendingExecs[existingIndex] = exec;
139
135
  return;
@@ -159,18 +155,54 @@ function mergePendingExec(existing, incoming) {
159
155
  decodedArgs: hasUsableDecodedArgs(incoming.decodedArgs)
160
156
  ? incoming.decodedArgs
161
157
  : existing.decodedArgs,
158
+ source: incomingHasExecMetadata ? incoming.source : existing.source ?? incoming.source,
159
+ cursorCallId: existing.cursorCallId || incoming.cursorCallId,
160
+ modelCallId: existing.modelCallId || incoming.modelCallId,
162
161
  };
163
162
  }
164
163
  function emitPendingExec(exec, state, onMcpExec) {
165
- const execKey = getPendingExecKey(exec);
166
- const existing = state.pendingExecs.find((candidate) => getPendingExecKey(candidate) === execKey);
164
+ const existing = state.pendingExecs.find((candidate) => candidate.toolCallId === exec.toolCallId);
167
165
  const nextExec = existing ? mergePendingExec(existing, exec) : exec;
168
- if (state.emittedToolCallIds.has(execKey)) {
166
+ const hadActionableMetadata = (existing?.execMsgId ?? 0) !== 0;
167
+ const hasActionableMetadata = nextExec.execMsgId !== 0;
168
+ if (state.emittedToolCallIds.has(nextExec.toolCallId)) {
169
169
  replacePendingExec(state, nextExec);
170
+ if (!hadActionableMetadata && hasActionableMetadata) {
171
+ logPluginInfo("Cursor MCP tool call metadata upgraded", {
172
+ toolCallId: nextExec.toolCallId,
173
+ toolName: nextExec.toolName,
174
+ source: nextExec.source,
175
+ execId: nextExec.execId,
176
+ execMsgId: nextExec.execMsgId,
177
+ cursorCallId: nextExec.cursorCallId,
178
+ modelCallId: nextExec.modelCallId,
179
+ });
180
+ }
181
+ else {
182
+ logPluginInfo("Ignored duplicate Cursor MCP tool call event", {
183
+ toolCallId: nextExec.toolCallId,
184
+ toolName: nextExec.toolName,
185
+ source: nextExec.source,
186
+ execId: nextExec.execId,
187
+ execMsgId: nextExec.execMsgId,
188
+ cursorCallId: nextExec.cursorCallId,
189
+ modelCallId: nextExec.modelCallId,
190
+ });
191
+ }
170
192
  return;
171
193
  }
172
- state.emittedToolCallIds.add(execKey);
194
+ state.emittedToolCallIds.add(nextExec.toolCallId);
173
195
  replacePendingExec(state, nextExec);
196
+ logPluginInfo("Emitting Cursor MCP tool call", {
197
+ toolCallId: nextExec.toolCallId,
198
+ toolName: nextExec.toolName,
199
+ source: nextExec.source,
200
+ execId: nextExec.execId,
201
+ execMsgId: nextExec.execMsgId,
202
+ cursorCallId: nextExec.cursorCallId,
203
+ modelCallId: nextExec.modelCallId,
204
+ decodedArgs: nextExec.decodedArgs,
205
+ });
174
206
  onMcpExec(nextExec);
175
207
  }
176
208
  export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
@@ -233,8 +265,19 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
233
265
  }
234
266
  else if (updateCase === "toolCallCompleted") {
235
267
  const exec = decodeInteractionToolCall(update.message.value, state);
236
- if (exec)
268
+ if (exec) {
269
+ logPluginInfo("Received Cursor interaction MCP tool call", {
270
+ toolCallId: exec.toolCallId,
271
+ toolName: exec.toolName,
272
+ source: exec.source,
273
+ execId: exec.execId,
274
+ execMsgId: exec.execMsgId,
275
+ cursorCallId: exec.cursorCallId,
276
+ modelCallId: exec.modelCallId,
277
+ decodedArgs: exec.decodedArgs,
278
+ });
237
279
  emitPendingExec(exec, state, onMcpExec);
280
+ }
238
281
  }
239
282
  else if (updateCase === "turnEnded") {
240
283
  onTurnEnded?.();
@@ -290,6 +333,9 @@ function decodeInteractionToolCall(update, state) {
290
333
  toolCallId,
291
334
  toolName: mcpArgs.toolName || mcpArgs.name || "unknown_mcp_tool",
292
335
  decodedArgs,
336
+ source: "interaction",
337
+ cursorCallId: callId || undefined,
338
+ modelCallId: update.modelCallId,
293
339
  };
294
340
  }
295
341
  function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
@@ -448,7 +494,16 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, state, onMcpExec, onUnh
448
494
  toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
449
495
  toolName: mcpArgs.toolName || mcpArgs.name,
450
496
  decodedArgs: JSON.stringify(decoded),
497
+ source: "exec",
451
498
  };
499
+ logPluginInfo("Received Cursor exec MCP tool metadata", {
500
+ toolCallId: exec.toolCallId,
501
+ toolName: exec.toolName,
502
+ source: exec.source,
503
+ execId: exec.execId,
504
+ execMsgId: exec.execMsgId,
505
+ decodedArgs: exec.decodedArgs,
506
+ });
452
507
  emitPendingExec(exec, state, onMcpExec);
453
508
  return;
454
509
  }
@@ -14,6 +14,9 @@ export interface PendingExec {
14
14
  toolName: string;
15
15
  /** Decoded arguments JSON string for SSE tool_calls emission. */
16
16
  decodedArgs: string;
17
+ source?: "interaction" | "exec";
18
+ cursorCallId?: string;
19
+ modelCallId?: string;
17
20
  }
18
21
  /** A live Cursor session kept alive across requests for tool result continuation. */
19
22
  export interface ActiveBridge {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwo/opencode-cursor-oauth",
3
- "version": "0.0.0-dev.6338d5591e37",
3
+ "version": "0.0.0-dev.67ecd4697583",
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",