@playwo/opencode-cursor-oauth 0.0.0-dev.762b07a81479 → 0.0.0-dev.7fe465ca080f
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/cursor/bidi-session.d.ts +1 -2
- package/dist/cursor/bidi-session.js +153 -138
- package/dist/cursor/index.d.ts +1 -1
- package/dist/cursor/index.js +1 -1
- package/dist/cursor/unary-rpc.d.ts +0 -1
- package/dist/cursor/unary-rpc.js +2 -59
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- package/dist/openai/messages.d.ts +0 -1
- package/dist/openai/messages.js +0 -3
- package/dist/plugin/cursor-auth-plugin.js +0 -1
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +103 -12
- package/dist/proxy/chat-completion.js +44 -12
- package/dist/proxy/server.js +23 -5
- package/dist/proxy/stream-dispatch.js +289 -23
- package/dist/proxy/types.d.ts +3 -0
- package/package.json +1 -2
|
@@ -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";
|
|
@@ -49,6 +49,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
49
49
|
return;
|
|
50
50
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
51
51
|
};
|
|
52
|
+
const failStream = (message, code) => {
|
|
53
|
+
if (closed)
|
|
54
|
+
return;
|
|
55
|
+
sendSSE({
|
|
56
|
+
error: {
|
|
57
|
+
message,
|
|
58
|
+
type: "server_error",
|
|
59
|
+
...(code ? { code } : {}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
sendDone();
|
|
63
|
+
closeController();
|
|
64
|
+
};
|
|
52
65
|
const closeController = () => {
|
|
53
66
|
if (closed)
|
|
54
67
|
return;
|
|
@@ -90,7 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
90
103
|
sendSSE(makeChunk({ content }));
|
|
91
104
|
}
|
|
92
105
|
}, (exec) => {
|
|
93
|
-
state.pendingExecs.
|
|
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
|
+
}
|
|
94
113
|
mcpExecReceived = true;
|
|
95
114
|
const flushed = tagFilter.flush();
|
|
96
115
|
if (flushed.reasoning)
|
|
@@ -193,17 +212,28 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
193
212
|
stopKeepalive();
|
|
194
213
|
}
|
|
195
214
|
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
215
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
216
|
+
modelId,
|
|
217
|
+
bridgeKey,
|
|
218
|
+
convKey,
|
|
219
|
+
mcpToolCount: mcpTools.length,
|
|
220
|
+
});
|
|
196
221
|
bridge.onData(processChunk);
|
|
197
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
|
+
});
|
|
198
231
|
clearInterval(heartbeatTimer);
|
|
199
232
|
stopKeepalive();
|
|
200
233
|
syncStoredBlobStore(convKey, blobStore);
|
|
201
234
|
if (endStreamError) {
|
|
202
235
|
activeBridges.delete(bridgeKey);
|
|
203
|
-
|
|
204
|
-
closed = true;
|
|
205
|
-
controller.error(endStreamError);
|
|
206
|
-
}
|
|
236
|
+
failStream(endStreamError.message, "cursor_bridge_closed");
|
|
207
237
|
return;
|
|
208
238
|
}
|
|
209
239
|
if (!mcpExecReceived) {
|
|
@@ -223,11 +253,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
223
253
|
}
|
|
224
254
|
activeBridges.delete(bridgeKey);
|
|
225
255
|
if (code !== 0 && !closed) {
|
|
226
|
-
|
|
227
|
-
sendSSE(makeChunk({}, "stop"));
|
|
228
|
-
sendSSE(makeUsageChunk());
|
|
229
|
-
sendDone();
|
|
230
|
-
closeController();
|
|
256
|
+
failStream("Cursor bridge connection lost", "cursor_bridge_closed");
|
|
231
257
|
}
|
|
232
258
|
});
|
|
233
259
|
},
|
|
@@ -251,10 +277,35 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
251
277
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
252
278
|
}
|
|
253
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
|
+
});
|
|
254
286
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
255
287
|
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
256
288
|
}
|
|
257
|
-
|
|
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;
|
|
307
|
+
}
|
|
308
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
258
309
|
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
259
310
|
const resumeMetadata = {
|
|
260
311
|
...metadata,
|
|
@@ -265,6 +316,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
265
316
|
.filter(Boolean)
|
|
266
317
|
.join("\n\n"),
|
|
267
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: 409, headers: { "Content-Type": "application/json" } });
|
|
345
|
+
}
|
|
268
346
|
for (const exec of pendingExecs) {
|
|
269
347
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
270
348
|
const mcpResult = result
|
|
@@ -305,6 +383,19 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
305
383
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
306
384
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
307
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
|
+
});
|
|
308
399
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
309
400
|
}
|
|
310
401
|
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";
|
|
@@ -7,9 +7,22 @@ import { handleNonStreamingResponse, handleStreamingResponse, handleToolResultRe
|
|
|
7
7
|
import { handleTitleGenerationRequest } from "./title";
|
|
8
8
|
export function handleChatCompletion(body, accessToken, context = {}) {
|
|
9
9
|
const parsed = parseMessages(body.messages);
|
|
10
|
-
const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint,
|
|
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) {
|
|
@@ -79,27 +108,30 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
79
108
|
stored.completedTurnsFingerprint = completedTurnsFingerprint;
|
|
80
109
|
stored.lastAccessMs = Date.now();
|
|
81
110
|
evictStaleConversations();
|
|
82
|
-
if (assistantContinuation) {
|
|
83
|
-
return new Response(JSON.stringify({
|
|
84
|
-
error: {
|
|
85
|
-
message: "Assistant-last continuation is not supported by the Cursor provider",
|
|
86
|
-
type: "invalid_request_error",
|
|
87
|
-
},
|
|
88
|
-
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
89
|
-
}
|
|
90
111
|
// Build the request. When tool results are present but the bridge died,
|
|
91
112
|
// we must still include the last user text so Cursor has context.
|
|
92
113
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
114
|
+
const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
|
|
93
115
|
const needsInitialHandoff = !stored.checkpoint &&
|
|
94
|
-
(turns.length > 0 ||
|
|
116
|
+
(turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
|
|
95
117
|
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
96
118
|
let effectiveUserText = needsInitialHandoff
|
|
97
119
|
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
98
|
-
: toolResults.length > 0
|
|
120
|
+
: toolResults.length > 0 || hasPendingAssistantSummary
|
|
99
121
|
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
100
122
|
: userText;
|
|
101
123
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
102
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
|
+
});
|
|
103
135
|
if (body.stream === false) {
|
|
104
136
|
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
105
137
|
systemPrompt,
|
package/dist/proxy/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorDetails, logPluginError } from "../logger";
|
|
1
|
+
import { errorDetails, logPluginError, logPluginWarn } from "../logger";
|
|
2
2
|
import { handleChatCompletion } from "./chat-completion";
|
|
3
3
|
import { activeBridges, conversationStates } from "./conversation-state";
|
|
4
4
|
let proxyServer;
|
|
@@ -42,14 +42,32 @@ export async function startProxy(getAccessToken, models = []) {
|
|
|
42
42
|
throw new Error("Cursor proxy access token provider not configured");
|
|
43
43
|
}
|
|
44
44
|
const accessToken = await proxyAccessTokenProvider();
|
|
45
|
-
const sessionId = req.headers.get("x-
|
|
46
|
-
req.headers.get("x-session-id") ??
|
|
47
|
-
undefined;
|
|
45
|
+
const sessionId = req.headers.get("x-session-id") ?? undefined;
|
|
48
46
|
const agentKey = req.headers.get("x-opencode-agent") ?? undefined;
|
|
49
|
-
|
|
47
|
+
const response = await handleChatCompletion(body, accessToken, {
|
|
50
48
|
sessionId,
|
|
51
49
|
agentKey,
|
|
52
50
|
});
|
|
51
|
+
if (response.status >= 400) {
|
|
52
|
+
let responseBody = "";
|
|
53
|
+
try {
|
|
54
|
+
responseBody = await response.clone().text();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
responseBody = `Failed to read rejected response body: ${error instanceof Error ? error.message : String(error)}`;
|
|
58
|
+
}
|
|
59
|
+
logPluginWarn("Rejected Cursor chat completion", {
|
|
60
|
+
path: url.pathname,
|
|
61
|
+
method: req.method,
|
|
62
|
+
sessionId,
|
|
63
|
+
agentKey,
|
|
64
|
+
status: response.status,
|
|
65
|
+
requestBody: body,
|
|
66
|
+
requestBodyText: JSON.stringify(body),
|
|
67
|
+
responseBody,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return response;
|
|
53
71
|
}
|
|
54
72
|
catch (err) {
|
|
55
73
|
const message = err instanceof Error ? err.message : String(err);
|