@playwo/opencode-cursor-oauth 0.0.0-dev.2b58f52bd11a → 0.0.0-dev.36e1216df6cf
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/README.md +33 -12
- package/dist/AGENTS.md +8 -0
- package/dist/agent-rules.d.ts +1 -0
- package/dist/agent-rules.js +28 -0
- package/dist/auth.js +1 -2
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/cursor/bidi-session.d.ts +12 -0
- package/dist/cursor/bidi-session.js +164 -0
- package/dist/cursor/config.d.ts +4 -0
- package/dist/cursor/config.js +4 -0
- package/dist/cursor/connect-framing.d.ts +10 -0
- package/dist/cursor/connect-framing.js +80 -0
- package/dist/cursor/headers.d.ts +6 -0
- package/dist/cursor/headers.js +16 -0
- package/dist/cursor/index.d.ts +5 -0
- package/dist/cursor/index.js +5 -0
- package/dist/cursor/unary-rpc.d.ts +12 -0
- package/dist/cursor/unary-rpc.js +124 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -306
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +10 -2
- package/dist/models.js +1 -23
- package/dist/openai/index.d.ts +3 -0
- package/dist/openai/index.js +3 -0
- package/dist/openai/messages.d.ts +39 -0
- package/dist/openai/messages.js +228 -0
- package/dist/openai/tools.d.ts +7 -0
- package/dist/openai/tools.js +58 -0
- package/dist/openai/types.d.ts +41 -0
- package/dist/openai/types.js +1 -0
- package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
- package/dist/plugin/cursor-auth-plugin.js +139 -0
- package/dist/proto/agent_pb.js +637 -319
- package/dist/provider/index.d.ts +2 -0
- package/dist/provider/index.js +2 -0
- package/dist/provider/model-cost.d.ts +9 -0
- package/dist/provider/model-cost.js +206 -0
- package/dist/provider/models.d.ts +8 -0
- package/dist/provider/models.js +86 -0
- package/dist/proxy/bridge-close-controller.d.ts +6 -0
- package/dist/proxy/bridge-close-controller.js +37 -0
- package/dist/proxy/bridge-non-streaming.d.ts +3 -0
- package/dist/proxy/bridge-non-streaming.js +137 -0
- package/dist/proxy/bridge-session.d.ts +5 -0
- package/dist/proxy/bridge-session.js +11 -0
- package/dist/proxy/bridge-streaming.d.ts +5 -0
- package/dist/proxy/bridge-streaming.js +664 -0
- package/dist/proxy/bridge.d.ts +3 -0
- package/dist/proxy/bridge.js +3 -0
- package/dist/proxy/chat-completion.d.ts +2 -0
- package/dist/proxy/chat-completion.js +154 -0
- package/dist/proxy/conversation-meta.d.ts +12 -0
- package/dist/proxy/conversation-meta.js +1 -0
- package/dist/proxy/conversation-state.d.ts +35 -0
- package/dist/proxy/conversation-state.js +95 -0
- package/dist/proxy/cursor-request.d.ts +6 -0
- package/dist/proxy/cursor-request.js +102 -0
- package/dist/proxy/index.d.ts +12 -0
- package/dist/proxy/index.js +12 -0
- package/dist/proxy/server.d.ts +6 -0
- package/dist/proxy/server.js +107 -0
- package/dist/proxy/sse.d.ts +5 -0
- package/dist/proxy/sse.js +5 -0
- package/dist/proxy/state-sync.d.ts +2 -0
- package/dist/proxy/state-sync.js +17 -0
- package/dist/proxy/stream-dispatch.d.ts +53 -0
- package/dist/proxy/stream-dispatch.js +669 -0
- package/dist/proxy/stream-state.d.ts +7 -0
- package/dist/proxy/stream-state.js +1 -0
- package/dist/proxy/title.d.ts +1 -0
- package/dist/proxy/title.js +103 -0
- package/dist/proxy/types.d.ts +40 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy.d.ts +2 -20
- package/dist/proxy.js +2 -1852
- package/package.json +2 -3
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
|
+
import { AgentClientMessageSchema, AgentServerMessageSchema, ExecClientMessageSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolResultContentItemSchema, } from "../proto/agent_pb";
|
|
3
|
+
import { errorDetails, logPluginError, logPluginInfo, logPluginWarn, } from "../logger";
|
|
4
|
+
import { formatToolCallSummary, formatToolResultSummary, } from "../openai/messages";
|
|
5
|
+
import { activeBridges, updateStoredConversationAfterCompletion, } from "./conversation-state";
|
|
6
|
+
import { startBridge } from "./bridge-session";
|
|
7
|
+
import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
|
|
8
|
+
import { SSE_HEADERS } from "./sse";
|
|
9
|
+
import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
|
|
10
|
+
import { createBridgeCloseController } from "./bridge-close-controller";
|
|
11
|
+
const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
12
|
+
function sortedIds(values) {
|
|
13
|
+
return [...values].sort();
|
|
14
|
+
}
|
|
15
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
16
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
17
|
+
const created = Math.floor(Date.now() / 1000);
|
|
18
|
+
let keepaliveTimer;
|
|
19
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
20
|
+
const stopKeepalive = () => {
|
|
21
|
+
if (!keepaliveTimer)
|
|
22
|
+
return;
|
|
23
|
+
clearInterval(keepaliveTimer);
|
|
24
|
+
keepaliveTimer = undefined;
|
|
25
|
+
};
|
|
26
|
+
const stream = new ReadableStream({
|
|
27
|
+
start(controller) {
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
let closed = false;
|
|
30
|
+
const state = {
|
|
31
|
+
toolCallIndex: 0,
|
|
32
|
+
pendingExecs: [],
|
|
33
|
+
outputTokens: 0,
|
|
34
|
+
totalTokens: 0,
|
|
35
|
+
};
|
|
36
|
+
const tagFilter = createThinkingTagFilter();
|
|
37
|
+
let assistantText = metadata.assistantSeedText ?? "";
|
|
38
|
+
let mcpExecReceived = false;
|
|
39
|
+
let endStreamError = null;
|
|
40
|
+
let toolCallsFlushed = false;
|
|
41
|
+
const streamedToolCalls = new Map();
|
|
42
|
+
const sendSSE = (data) => {
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
46
|
+
};
|
|
47
|
+
const sendKeepalive = () => {
|
|
48
|
+
if (closed)
|
|
49
|
+
return;
|
|
50
|
+
controller.enqueue(encoder.encode(": keep-alive\n\n"));
|
|
51
|
+
};
|
|
52
|
+
const sendDone = () => {
|
|
53
|
+
if (closed)
|
|
54
|
+
return;
|
|
55
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
56
|
+
};
|
|
57
|
+
const failStream = (message, code) => {
|
|
58
|
+
if (closed)
|
|
59
|
+
return;
|
|
60
|
+
sendSSE({
|
|
61
|
+
error: {
|
|
62
|
+
message,
|
|
63
|
+
type: "server_error",
|
|
64
|
+
...(code ? { code } : {}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
sendDone();
|
|
68
|
+
closeController();
|
|
69
|
+
};
|
|
70
|
+
const closeController = () => {
|
|
71
|
+
if (closed)
|
|
72
|
+
return;
|
|
73
|
+
closed = true;
|
|
74
|
+
stopKeepalive();
|
|
75
|
+
controller.close();
|
|
76
|
+
};
|
|
77
|
+
const makeChunk = (delta, finishReason = null) => ({
|
|
78
|
+
id: completionId,
|
|
79
|
+
object: "chat.completion.chunk",
|
|
80
|
+
created,
|
|
81
|
+
model: modelId,
|
|
82
|
+
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
|
83
|
+
});
|
|
84
|
+
const makeUsageChunk = () => {
|
|
85
|
+
const { prompt_tokens, completion_tokens, total_tokens } = computeUsage(state);
|
|
86
|
+
return {
|
|
87
|
+
id: completionId,
|
|
88
|
+
object: "chat.completion.chunk",
|
|
89
|
+
created,
|
|
90
|
+
model: modelId,
|
|
91
|
+
choices: [],
|
|
92
|
+
usage: { prompt_tokens, completion_tokens, total_tokens },
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const ensureStreamedToolCall = (toolCallId, toolName) => {
|
|
96
|
+
const existing = streamedToolCalls.get(toolCallId);
|
|
97
|
+
if (existing) {
|
|
98
|
+
if (toolName && existing.toolName !== toolName) {
|
|
99
|
+
existing.toolName = toolName;
|
|
100
|
+
}
|
|
101
|
+
return existing;
|
|
102
|
+
}
|
|
103
|
+
const createdState = {
|
|
104
|
+
index: state.toolCallIndex++,
|
|
105
|
+
toolName,
|
|
106
|
+
started: false,
|
|
107
|
+
};
|
|
108
|
+
streamedToolCalls.set(toolCallId, createdState);
|
|
109
|
+
return createdState;
|
|
110
|
+
};
|
|
111
|
+
const emitPendingToolStart = (toolCallId, toolName, source) => {
|
|
112
|
+
if (!toolName)
|
|
113
|
+
return;
|
|
114
|
+
const toolCall = ensureStreamedToolCall(toolCallId, toolName);
|
|
115
|
+
if (toolCall.started)
|
|
116
|
+
return;
|
|
117
|
+
toolCall.started = true;
|
|
118
|
+
logPluginInfo("Streaming pending Cursor tool-call start", {
|
|
119
|
+
modelId,
|
|
120
|
+
bridgeKey,
|
|
121
|
+
convKey,
|
|
122
|
+
toolCallId,
|
|
123
|
+
toolName,
|
|
124
|
+
index: toolCall.index,
|
|
125
|
+
source,
|
|
126
|
+
});
|
|
127
|
+
sendSSE(makeChunk({
|
|
128
|
+
tool_calls: [
|
|
129
|
+
{
|
|
130
|
+
index: toolCall.index,
|
|
131
|
+
id: toolCallId,
|
|
132
|
+
type: "function",
|
|
133
|
+
function: {
|
|
134
|
+
name: toolCall.toolName,
|
|
135
|
+
arguments: "",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}));
|
|
140
|
+
};
|
|
141
|
+
const publishPendingToolCalls = (source) => {
|
|
142
|
+
if (closed || toolCallsFlushed || state.pendingExecs.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
logPluginInfo("Evaluating Cursor tool-call publish", {
|
|
145
|
+
modelId,
|
|
146
|
+
bridgeKey,
|
|
147
|
+
convKey,
|
|
148
|
+
source,
|
|
149
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
150
|
+
toolCallsFlushed,
|
|
151
|
+
mcpExecReceived,
|
|
152
|
+
nowMs: Date.now(),
|
|
153
|
+
});
|
|
154
|
+
toolCallsFlushed = true;
|
|
155
|
+
const flushed = tagFilter.flush();
|
|
156
|
+
if (flushed.reasoning)
|
|
157
|
+
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
158
|
+
if (flushed.content) {
|
|
159
|
+
assistantText += flushed.content;
|
|
160
|
+
sendSSE(makeChunk({ content: flushed.content }));
|
|
161
|
+
}
|
|
162
|
+
const assistantSeedText = [
|
|
163
|
+
assistantText.trim(),
|
|
164
|
+
state.pendingExecs
|
|
165
|
+
.map((exec) => formatToolCallSummary({
|
|
166
|
+
id: exec.toolCallId,
|
|
167
|
+
type: "function",
|
|
168
|
+
function: {
|
|
169
|
+
name: exec.toolName,
|
|
170
|
+
arguments: exec.decodedArgs,
|
|
171
|
+
},
|
|
172
|
+
}))
|
|
173
|
+
.join("\n\n"),
|
|
174
|
+
]
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join("\n\n");
|
|
177
|
+
activeBridges.set(bridgeKey, {
|
|
178
|
+
bridge,
|
|
179
|
+
heartbeatTimer,
|
|
180
|
+
blobStore,
|
|
181
|
+
cloudRule,
|
|
182
|
+
mcpTools,
|
|
183
|
+
pendingExecs: [...state.pendingExecs],
|
|
184
|
+
modelId,
|
|
185
|
+
metadata: {
|
|
186
|
+
...metadata,
|
|
187
|
+
assistantSeedText,
|
|
188
|
+
},
|
|
189
|
+
diagnostics: {
|
|
190
|
+
announcedToolCallIds: [],
|
|
191
|
+
publishedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
|
|
192
|
+
lastMcpUpdate: "publish",
|
|
193
|
+
publishedAtMs: Date.now(),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
logPluginInfo("Publishing Cursor tool-call envelope", {
|
|
197
|
+
modelId,
|
|
198
|
+
bridgeKey,
|
|
199
|
+
convKey,
|
|
200
|
+
source,
|
|
201
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
202
|
+
emittedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
|
|
203
|
+
assistantSeedTextLength: assistantSeedText.length,
|
|
204
|
+
});
|
|
205
|
+
for (const exec of state.pendingExecs) {
|
|
206
|
+
emitPendingToolStart(exec.toolCallId, exec.toolName, "publish");
|
|
207
|
+
const streamedToolCall = ensureStreamedToolCall(exec.toolCallId, exec.toolName);
|
|
208
|
+
sendSSE(makeChunk({
|
|
209
|
+
tool_calls: [
|
|
210
|
+
{
|
|
211
|
+
index: streamedToolCall.index,
|
|
212
|
+
function: {
|
|
213
|
+
arguments: exec.decodedArgs,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
logPluginInfo("Stored active Cursor bridge for tool-result resume", {
|
|
220
|
+
modelId,
|
|
221
|
+
bridgeKey,
|
|
222
|
+
convKey,
|
|
223
|
+
diagnostics: activeBridges.get(bridgeKey)?.diagnostics,
|
|
224
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
225
|
+
});
|
|
226
|
+
sendSSE(makeChunk({}, "tool_calls"));
|
|
227
|
+
sendDone();
|
|
228
|
+
closeController();
|
|
229
|
+
};
|
|
230
|
+
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
231
|
+
try {
|
|
232
|
+
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
233
|
+
processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
234
|
+
if (isThinking) {
|
|
235
|
+
sendSSE(makeChunk({ reasoning_content: text }));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const { content, reasoning } = tagFilter.process(text);
|
|
239
|
+
if (reasoning)
|
|
240
|
+
sendSSE(makeChunk({ reasoning_content: reasoning }));
|
|
241
|
+
if (content) {
|
|
242
|
+
assistantText += content;
|
|
243
|
+
sendSSE(makeChunk({ content }));
|
|
244
|
+
}
|
|
245
|
+
}, (exec) => {
|
|
246
|
+
if (toolCallsFlushed) {
|
|
247
|
+
logPluginWarn("Received Cursor MCP exec after tool-call envelope was already published", {
|
|
248
|
+
modelId,
|
|
249
|
+
bridgeKey,
|
|
250
|
+
convKey,
|
|
251
|
+
toolCallId: exec.toolCallId,
|
|
252
|
+
toolName: exec.toolName,
|
|
253
|
+
publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
|
|
254
|
+
[],
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
emitPendingToolStart(exec.toolCallId, exec.toolName, "exec");
|
|
258
|
+
const pendingBefore = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
|
|
259
|
+
const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
|
|
260
|
+
if (existingIndex >= 0) {
|
|
261
|
+
state.pendingExecs[existingIndex] = exec;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
state.pendingExecs.push(exec);
|
|
265
|
+
}
|
|
266
|
+
mcpExecReceived = true;
|
|
267
|
+
const pendingAfter = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
|
|
268
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
269
|
+
if (existingActiveBridge) {
|
|
270
|
+
existingActiveBridge.diagnostics = {
|
|
271
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
272
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
273
|
+
lastMcpUpdate: `exec:${exec.toolCallId}`,
|
|
274
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
275
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
logPluginInfo("Tracking Cursor MCP exec in streaming bridge", {
|
|
279
|
+
modelId,
|
|
280
|
+
bridgeKey,
|
|
281
|
+
convKey,
|
|
282
|
+
toolCallId: exec.toolCallId,
|
|
283
|
+
toolName: exec.toolName,
|
|
284
|
+
pendingBefore,
|
|
285
|
+
pendingAfter,
|
|
286
|
+
hasStoredActiveBridge: Boolean(existingActiveBridge),
|
|
287
|
+
storedActiveBridgePendingExecToolCallIds: existingActiveBridge
|
|
288
|
+
? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
|
|
289
|
+
: [],
|
|
290
|
+
});
|
|
291
|
+
}, (info) => {
|
|
292
|
+
if (toolCallsFlushed) {
|
|
293
|
+
logPluginWarn("Received Cursor MCP interaction update after tool-call envelope was already published", {
|
|
294
|
+
modelId,
|
|
295
|
+
bridgeKey,
|
|
296
|
+
convKey,
|
|
297
|
+
updateCase: info.updateCase,
|
|
298
|
+
toolCallId: info.toolCallId,
|
|
299
|
+
publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
|
|
300
|
+
[],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (info.updateCase !== "toolCallCompleted") {
|
|
304
|
+
emitPendingToolStart(info.toolCallId, info.toolName ?? "", "interaction");
|
|
305
|
+
}
|
|
306
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
307
|
+
if (existingActiveBridge) {
|
|
308
|
+
existingActiveBridge.diagnostics = {
|
|
309
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
310
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
311
|
+
lastMcpUpdate: `${info.updateCase}:${info.toolCallId}`,
|
|
312
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
313
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}, (info) => {
|
|
317
|
+
const existingActiveBridge = activeBridges.get(bridgeKey);
|
|
318
|
+
if (existingActiveBridge) {
|
|
319
|
+
existingActiveBridge.diagnostics = {
|
|
320
|
+
announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
|
|
321
|
+
publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
|
|
322
|
+
lastMcpUpdate: info.updateCase === "stepCompleted"
|
|
323
|
+
? `${info.updateCase}:${info.stepId}:${info.stepDurationMs ?? "unknown"}`
|
|
324
|
+
: `${info.updateCase}:${info.stepId}`,
|
|
325
|
+
publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
|
|
326
|
+
lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
logPluginInfo("Tracking Cursor step boundary in streaming bridge", {
|
|
330
|
+
modelId,
|
|
331
|
+
bridgeKey,
|
|
332
|
+
convKey,
|
|
333
|
+
updateCase: info.updateCase,
|
|
334
|
+
stepId: info.stepId,
|
|
335
|
+
stepDurationMs: info.stepDurationMs,
|
|
336
|
+
toolCallsFlushed,
|
|
337
|
+
mcpExecReceived,
|
|
338
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
339
|
+
streamedToolCallIds: sortedIds(streamedToolCalls.keys()),
|
|
340
|
+
hasStoredActiveBridge: Boolean(existingActiveBridge),
|
|
341
|
+
storedActiveBridgePendingExecToolCallIds: existingActiveBridge
|
|
342
|
+
? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
|
|
343
|
+
: [],
|
|
344
|
+
storedActiveBridgeDiagnostics: existingActiveBridge?.diagnostics,
|
|
345
|
+
});
|
|
346
|
+
}, (checkpointBytes) => {
|
|
347
|
+
logPluginInfo("Received Cursor conversation checkpoint", {
|
|
348
|
+
modelId,
|
|
349
|
+
bridgeKey,
|
|
350
|
+
convKey,
|
|
351
|
+
toolCallsFlushed,
|
|
352
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
353
|
+
});
|
|
354
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
355
|
+
bridgeCloseController.noteCheckpoint();
|
|
356
|
+
if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
|
|
357
|
+
publishPendingToolCalls("checkpoint");
|
|
358
|
+
}
|
|
359
|
+
}, () => {
|
|
360
|
+
logPluginInfo("Received Cursor turn-ended signal", {
|
|
361
|
+
modelId,
|
|
362
|
+
bridgeKey,
|
|
363
|
+
convKey,
|
|
364
|
+
toolCallsFlushed,
|
|
365
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
|
|
366
|
+
});
|
|
367
|
+
bridgeCloseController.noteTurnEnded();
|
|
368
|
+
if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
|
|
369
|
+
publishPendingToolCalls("turnEnded");
|
|
370
|
+
}
|
|
371
|
+
}, (info) => {
|
|
372
|
+
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
373
|
+
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
374
|
+
modelId,
|
|
375
|
+
bridgeKey,
|
|
376
|
+
convKey,
|
|
377
|
+
category: info.category,
|
|
378
|
+
caseName: info.caseName,
|
|
379
|
+
detail: info.detail,
|
|
380
|
+
});
|
|
381
|
+
scheduleBridgeEnd(bridge);
|
|
382
|
+
}, (info) => {
|
|
383
|
+
endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
|
|
384
|
+
logPluginError("Closing Cursor bridge after unsupported exec", {
|
|
385
|
+
modelId,
|
|
386
|
+
bridgeKey,
|
|
387
|
+
convKey,
|
|
388
|
+
execCase: info.execCase,
|
|
389
|
+
execId: info.execId,
|
|
390
|
+
execMsgId: info.execMsgId,
|
|
391
|
+
});
|
|
392
|
+
scheduleBridgeEnd(bridge);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Skip unparseable messages.
|
|
397
|
+
}
|
|
398
|
+
}, (endStreamBytes) => {
|
|
399
|
+
logPluginInfo("Received Cursor end-of-stream signal", {
|
|
400
|
+
modelId,
|
|
401
|
+
bridgeKey,
|
|
402
|
+
convKey,
|
|
403
|
+
byteLength: endStreamBytes.length,
|
|
404
|
+
});
|
|
405
|
+
if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
|
|
406
|
+
publishPendingToolCalls("endStream");
|
|
407
|
+
}
|
|
408
|
+
endStreamError = parseConnectEndStream(endStreamBytes);
|
|
409
|
+
if (endStreamError) {
|
|
410
|
+
logPluginError("Cursor stream returned Connect end-stream error", {
|
|
411
|
+
modelId,
|
|
412
|
+
bridgeKey,
|
|
413
|
+
convKey,
|
|
414
|
+
...errorDetails(endStreamError),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
scheduleBridgeEnd(bridge);
|
|
418
|
+
});
|
|
419
|
+
keepaliveTimer = setInterval(() => {
|
|
420
|
+
try {
|
|
421
|
+
sendKeepalive();
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
logPluginWarn("Failed to write SSE keepalive", {
|
|
425
|
+
modelId,
|
|
426
|
+
bridgeKey,
|
|
427
|
+
convKey,
|
|
428
|
+
...errorDetails(error),
|
|
429
|
+
});
|
|
430
|
+
stopKeepalive();
|
|
431
|
+
}
|
|
432
|
+
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
433
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
434
|
+
modelId,
|
|
435
|
+
bridgeKey,
|
|
436
|
+
convKey,
|
|
437
|
+
mcpToolCount: mcpTools.length,
|
|
438
|
+
hasCloudRule: Boolean(cloudRule),
|
|
439
|
+
});
|
|
440
|
+
bridge.onData(processChunk);
|
|
441
|
+
bridge.onClose((code) => {
|
|
442
|
+
logPluginInfo("Cursor streaming bridge closed", {
|
|
443
|
+
modelId,
|
|
444
|
+
bridgeKey,
|
|
445
|
+
convKey,
|
|
446
|
+
code,
|
|
447
|
+
mcpExecReceived,
|
|
448
|
+
hadEndStreamError: Boolean(endStreamError),
|
|
449
|
+
pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
|
|
450
|
+
storedActiveBridgeDiagnostics: activeBridges.get(bridgeKey)?.diagnostics,
|
|
451
|
+
});
|
|
452
|
+
bridgeCloseController.dispose();
|
|
453
|
+
clearInterval(heartbeatTimer);
|
|
454
|
+
stopKeepalive();
|
|
455
|
+
syncStoredBlobStore(convKey, blobStore);
|
|
456
|
+
if (endStreamError) {
|
|
457
|
+
activeBridges.delete(bridgeKey);
|
|
458
|
+
failStream(endStreamError.message, "cursor_bridge_closed");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!mcpExecReceived) {
|
|
462
|
+
const flushed = tagFilter.flush();
|
|
463
|
+
if (flushed.reasoning)
|
|
464
|
+
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
465
|
+
if (flushed.content) {
|
|
466
|
+
assistantText += flushed.content;
|
|
467
|
+
sendSSE(makeChunk({ content: flushed.content }));
|
|
468
|
+
}
|
|
469
|
+
updateStoredConversationAfterCompletion(convKey, metadata, assistantText);
|
|
470
|
+
sendSSE(makeChunk({}, "stop"));
|
|
471
|
+
sendSSE(makeUsageChunk());
|
|
472
|
+
sendDone();
|
|
473
|
+
closeController();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
activeBridges.delete(bridgeKey);
|
|
477
|
+
if (code !== 0 && !closed) {
|
|
478
|
+
failStream("Cursor bridge connection lost", "cursor_bridge_closed");
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
},
|
|
482
|
+
cancel(reason) {
|
|
483
|
+
bridgeCloseController.dispose();
|
|
484
|
+
stopKeepalive();
|
|
485
|
+
clearInterval(heartbeatTimer);
|
|
486
|
+
syncStoredBlobStore(convKey, blobStore);
|
|
487
|
+
const active = activeBridges.get(bridgeKey);
|
|
488
|
+
if (active?.bridge === bridge) {
|
|
489
|
+
activeBridges.delete(bridgeKey);
|
|
490
|
+
}
|
|
491
|
+
logPluginWarn("OpenCode client disconnected from Cursor SSE stream", {
|
|
492
|
+
modelId,
|
|
493
|
+
bridgeKey,
|
|
494
|
+
convKey,
|
|
495
|
+
reason: reason instanceof Error ? reason.message : String(reason ?? ""),
|
|
496
|
+
});
|
|
497
|
+
bridge.end();
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
return new Response(stream, { headers: SSE_HEADERS });
|
|
501
|
+
}
|
|
502
|
+
export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
503
|
+
logPluginInfo("Starting Cursor streaming response", {
|
|
504
|
+
modelId,
|
|
505
|
+
bridgeKey,
|
|
506
|
+
convKey,
|
|
507
|
+
mcpToolCount: payload.mcpTools.length,
|
|
508
|
+
});
|
|
509
|
+
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
510
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
511
|
+
}
|
|
512
|
+
async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
|
|
513
|
+
const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
514
|
+
const deadline = Date.now() + timeoutMs;
|
|
515
|
+
while (Date.now() < deadline) {
|
|
516
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
517
|
+
if (unresolved.length === 0) {
|
|
518
|
+
return unresolved;
|
|
519
|
+
}
|
|
520
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
521
|
+
}
|
|
522
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
523
|
+
if (unresolved.length > 0) {
|
|
524
|
+
logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
|
|
525
|
+
bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
|
|
526
|
+
modelId: active.modelId,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return unresolved;
|
|
530
|
+
}
|
|
531
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
532
|
+
const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
533
|
+
const resumeMetadata = {
|
|
534
|
+
...metadata,
|
|
535
|
+
assistantSeedText: [
|
|
536
|
+
metadata.assistantSeedText?.trim() ?? "",
|
|
537
|
+
toolResults.map(formatToolResultSummary).join("\n\n"),
|
|
538
|
+
]
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.join("\n\n"),
|
|
541
|
+
};
|
|
542
|
+
const pendingToolCallIds = new Set(pendingExecs.map((exec) => exec.toolCallId));
|
|
543
|
+
const toolResultIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
544
|
+
const missingToolResultIds = pendingExecs
|
|
545
|
+
.map((exec) => exec.toolCallId)
|
|
546
|
+
.filter((toolCallId) => !toolResultIds.has(toolCallId));
|
|
547
|
+
const unexpectedToolResultIds = toolResults
|
|
548
|
+
.map((result) => result.toolCallId)
|
|
549
|
+
.filter((toolCallId) => !pendingToolCallIds.has(toolCallId));
|
|
550
|
+
const matchedPendingExecs = pendingExecs.filter((exec) => toolResultIds.has(exec.toolCallId));
|
|
551
|
+
logPluginInfo("Preparing Cursor tool-result resume", {
|
|
552
|
+
bridgeKey,
|
|
553
|
+
convKey,
|
|
554
|
+
modelId,
|
|
555
|
+
toolResults,
|
|
556
|
+
pendingExecs,
|
|
557
|
+
bridgeAlive: bridge.alive,
|
|
558
|
+
diagnostics: active.diagnostics,
|
|
559
|
+
});
|
|
560
|
+
if (active.diagnostics) {
|
|
561
|
+
active.diagnostics.lastResumeAttemptAtMs = Date.now();
|
|
562
|
+
}
|
|
563
|
+
const unresolved = await waitForResolvablePendingExecs(active, toolResults);
|
|
564
|
+
logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
|
|
565
|
+
bridgeKey,
|
|
566
|
+
convKey,
|
|
567
|
+
modelId,
|
|
568
|
+
toolResults,
|
|
569
|
+
pendingExecs,
|
|
570
|
+
unresolvedPendingExecs: unresolved,
|
|
571
|
+
diagnostics: active.diagnostics,
|
|
572
|
+
});
|
|
573
|
+
if (unresolved.length > 0) {
|
|
574
|
+
clearInterval(heartbeatTimer);
|
|
575
|
+
bridge.end();
|
|
576
|
+
return new Response(JSON.stringify({
|
|
577
|
+
error: {
|
|
578
|
+
message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
|
|
579
|
+
type: "invalid_request_error",
|
|
580
|
+
code: "cursor_missing_exec_metadata",
|
|
581
|
+
},
|
|
582
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
583
|
+
}
|
|
584
|
+
if (missingToolResultIds.length > 0 || unexpectedToolResultIds.length > 0) {
|
|
585
|
+
logPluginError("Aborting Cursor tool-result resume because tool-call ids did not match", {
|
|
586
|
+
bridgeKey,
|
|
587
|
+
convKey,
|
|
588
|
+
modelId,
|
|
589
|
+
pendingToolCallIds: [...pendingToolCallIds],
|
|
590
|
+
toolResultIds: [...toolResultIds],
|
|
591
|
+
missingToolResultIds,
|
|
592
|
+
unexpectedToolResultIds,
|
|
593
|
+
});
|
|
594
|
+
clearInterval(heartbeatTimer);
|
|
595
|
+
bridge.end();
|
|
596
|
+
return new Response(JSON.stringify({
|
|
597
|
+
error: {
|
|
598
|
+
message: "Tool-result ids did not match the active Cursor MCP tool calls. Aborting instead of resuming a potentially stuck session.",
|
|
599
|
+
type: "invalid_request_error",
|
|
600
|
+
code: "cursor_tool_result_mismatch",
|
|
601
|
+
details: {
|
|
602
|
+
missingToolResultIds,
|
|
603
|
+
unexpectedToolResultIds,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
607
|
+
}
|
|
608
|
+
for (const exec of matchedPendingExecs) {
|
|
609
|
+
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
610
|
+
const mcpResult = result
|
|
611
|
+
? create(McpResultSchema, {
|
|
612
|
+
result: {
|
|
613
|
+
case: "success",
|
|
614
|
+
value: create(McpSuccessSchema, {
|
|
615
|
+
content: [
|
|
616
|
+
create(McpToolResultContentItemSchema, {
|
|
617
|
+
content: {
|
|
618
|
+
case: "text",
|
|
619
|
+
value: create(McpTextContentSchema, {
|
|
620
|
+
text: result.content,
|
|
621
|
+
}),
|
|
622
|
+
},
|
|
623
|
+
}),
|
|
624
|
+
],
|
|
625
|
+
isError: false,
|
|
626
|
+
}),
|
|
627
|
+
},
|
|
628
|
+
})
|
|
629
|
+
: create(McpResultSchema, {
|
|
630
|
+
result: {
|
|
631
|
+
case: "error",
|
|
632
|
+
value: create(McpErrorSchema, {
|
|
633
|
+
error: "Tool result not provided",
|
|
634
|
+
}),
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
const execClientMessage = create(ExecClientMessageSchema, {
|
|
638
|
+
id: exec.execMsgId,
|
|
639
|
+
execId: exec.execId,
|
|
640
|
+
message: {
|
|
641
|
+
case: "mcpResult",
|
|
642
|
+
value: mcpResult,
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
646
|
+
message: { case: "execClientMessage", value: execClientMessage },
|
|
647
|
+
});
|
|
648
|
+
logPluginInfo("Sending Cursor tool-result resume message", {
|
|
649
|
+
bridgeKey,
|
|
650
|
+
convKey,
|
|
651
|
+
modelId,
|
|
652
|
+
toolCallId: exec.toolCallId,
|
|
653
|
+
toolName: exec.toolName,
|
|
654
|
+
source: exec.source,
|
|
655
|
+
execId: exec.execId,
|
|
656
|
+
execMsgId: exec.execMsgId,
|
|
657
|
+
cursorCallId: exec.cursorCallId,
|
|
658
|
+
modelCallId: exec.modelCallId,
|
|
659
|
+
matchedToolResult: result,
|
|
660
|
+
});
|
|
661
|
+
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
662
|
+
}
|
|
663
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
664
|
+
}
|