@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 +1 -0
- package/dist/logger.js +3 -0
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +68 -2
- package/dist/proxy/chat-completion.js +9 -1
- package/dist/proxy/stream-dispatch.js +66 -11
- package/dist/proxy/types.d.ts +3 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/dist/proxy/types.d.ts
CHANGED
|
@@ -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.
|
|
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",
|