@playwo/opencode-cursor-oauth 0.0.0-dev.4463bb589222 → 0.0.0-dev.494d4e1cfa84
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 +26 -12
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -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.js +8 -4
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +103 -11
- package/dist/proxy/chat-completion.js +40 -1
- package/dist/proxy/cursor-request.js +12 -15
- package/dist/proxy/stream-dispatch.d.ts +1 -1
- package/dist/proxy/stream-dispatch.js +118 -53
- package/dist/proxy/stream-state.d.ts +0 -2
- package/dist/proxy/types.d.ts +6 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
# opencode-cursor-oauth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Disclaimer
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> This project is a **fork** of [ephraimduncan/opencode-cursor](https://github.com/ephraimduncan/opencode-cursor). Upstream may differ in behavior, features, or maintenance; treat this repository as its own line of development.
|
|
4
7
|
|
|
5
8
|
## What it does
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
10
|
+
This is an [OpenCode](https://opencode.ai) plugin that lets you use **Cursor cloud models** (Claude, GPT, Gemini, and whatever your Cursor account exposes) from inside OpenCode.
|
|
11
|
+
|
|
12
|
+
- **OAuth login** to Cursor in the browser
|
|
13
|
+
- **Model discovery** — loads the models available to your Cursor account
|
|
14
|
+
- **Local OpenAI-compatible proxy** — translates OpenCode’s requests to Cursor’s gRPC API
|
|
15
|
+
- **Token refresh** — refreshes access tokens so sessions keep working
|
|
16
|
+
|
|
17
|
+
There are **no extra runtime requirements** beyond what OpenCode already needs: you do not install Node, Python, or Docker separately for this plugin. Enable it in OpenCode’s config and complete login in the UI.
|
|
11
18
|
|
|
12
19
|
## Install
|
|
13
20
|
|
|
14
|
-
Add to your `opencode.json
|
|
21
|
+
Add the package to your OpenCode configuration (for example `opencode.json`):
|
|
15
22
|
|
|
16
23
|
```json
|
|
17
24
|
{
|
|
@@ -19,13 +26,20 @@ Add to your `opencode.json`:
|
|
|
19
26
|
}
|
|
20
27
|
```
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
Install or update dependencies the way you normally do for OpenCode plugins (e.g. ensure the package is available to your OpenCode environment). You need **OpenCode 1.2+** and a **Cursor account** with API/model access.
|
|
30
|
+
|
|
31
|
+
## Connect auth and use it
|
|
32
|
+
|
|
33
|
+
1. Start OpenCode with the plugin enabled.
|
|
34
|
+
2. Open **Settings → Providers → Cursor** (wording may vary slightly by OpenCode version).
|
|
35
|
+
3. Choose **Login** (or equivalent) and complete **OAuth** in the browser when prompted.
|
|
36
|
+
4. After login, pick a Cursor-backed model from the model list and use OpenCode as usual.
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
If something fails, check that you are signed into the correct Cursor account and that your plan includes the models you expect.
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
- OpenCode 1.2+
|
|
40
|
+
## Stability and issues
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
This integration can be **buggy** or break when Cursor or OpenCode change their APIs or UI.
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
> [!TIP]
|
|
45
|
+
> If you hit problems, missing models, or confusing errors, please **[open an issue](https://github.com/PoolPirate/opencode-cursor/issues)** on this repository with steps to reproduce and logs or screenshots when possible.
|
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
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { scheduleBridgeEnd } from "./stream-dispatch";
|
|
2
|
+
const TURN_END_GRACE_MS = 750;
|
|
3
|
+
export function createBridgeCloseController(bridge) {
|
|
4
|
+
let turnEnded = false;
|
|
5
|
+
let checkpointSeen = false;
|
|
6
|
+
let closeTimer;
|
|
7
|
+
const clearCloseTimer = () => {
|
|
8
|
+
if (!closeTimer)
|
|
9
|
+
return;
|
|
10
|
+
clearTimeout(closeTimer);
|
|
11
|
+
closeTimer = undefined;
|
|
12
|
+
};
|
|
13
|
+
const closeBridge = () => {
|
|
14
|
+
clearCloseTimer();
|
|
15
|
+
scheduleBridgeEnd(bridge);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
noteTurnEnded() {
|
|
19
|
+
turnEnded = true;
|
|
20
|
+
if (checkpointSeen) {
|
|
21
|
+
closeBridge();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
clearCloseTimer();
|
|
25
|
+
closeTimer = setTimeout(closeBridge, TURN_END_GRACE_MS);
|
|
26
|
+
},
|
|
27
|
+
noteCheckpoint() {
|
|
28
|
+
checkpointSeen = true;
|
|
29
|
+
if (turnEnded) {
|
|
30
|
+
closeBridge();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
dispose() {
|
|
34
|
+
clearCloseTimer();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -5,6 +5,7 @@ import { updateStoredConversationAfterCompletion } from "./conversation-state";
|
|
|
5
5
|
import { startBridge } from "./bridge-session";
|
|
6
6
|
import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
|
|
7
7
|
import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
|
|
8
|
+
import { createBridgeCloseController } from "./bridge-close-controller";
|
|
8
9
|
export async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
9
10
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
10
11
|
const created = Math.floor(Date.now() / 1000);
|
|
@@ -33,19 +34,18 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
33
34
|
let endStreamError = null;
|
|
34
35
|
const pendingToolCalls = [];
|
|
35
36
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
37
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
36
38
|
const state = {
|
|
37
39
|
toolCallIndex: 0,
|
|
38
40
|
pendingExecs: [],
|
|
39
41
|
outputTokens: 0,
|
|
40
42
|
totalTokens: 0,
|
|
41
|
-
interactionToolArgsText: new Map(),
|
|
42
|
-
emittedToolCallIds: new Set(),
|
|
43
43
|
};
|
|
44
44
|
const tagFilter = createThinkingTagFilter();
|
|
45
45
|
bridge.onData(createConnectFrameParser((messageBytes) => {
|
|
46
46
|
try {
|
|
47
47
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
48
|
-
processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
48
|
+
processServerMessage(serverMessage, payload.blobStore, payload.cloudRule, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
49
49
|
if (isThinking)
|
|
50
50
|
return;
|
|
51
51
|
const { content } = tagFilter.process(text);
|
|
@@ -60,7 +60,10 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
scheduleBridgeEnd(bridge);
|
|
63
|
-
}, (checkpointBytes) =>
|
|
63
|
+
}, (checkpointBytes) => {
|
|
64
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
65
|
+
bridgeCloseController.noteCheckpoint();
|
|
66
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
64
67
|
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
65
68
|
logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
|
|
66
69
|
modelId,
|
|
@@ -97,6 +100,7 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
97
100
|
scheduleBridgeEnd(bridge);
|
|
98
101
|
}));
|
|
99
102
|
bridge.onClose(() => {
|
|
103
|
+
bridgeCloseController.dispose();
|
|
100
104
|
clearInterval(heartbeatTimer);
|
|
101
105
|
syncStoredBlobStore(convKey, payload.blobStore);
|
|
102
106
|
const flushed = tagFilter.flush();
|
|
@@ -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,17 +1,19 @@
|
|
|
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";
|
|
7
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
|
+
import { createBridgeCloseController } from "./bridge-close-controller";
|
|
10
11
|
const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
11
|
-
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
13
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
13
14
|
const created = Math.floor(Date.now() / 1000);
|
|
14
15
|
let keepaliveTimer;
|
|
16
|
+
const bridgeCloseController = createBridgeCloseController(bridge);
|
|
15
17
|
const stopKeepalive = () => {
|
|
16
18
|
if (!keepaliveTimer)
|
|
17
19
|
return;
|
|
@@ -27,8 +29,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
27
29
|
pendingExecs: [],
|
|
28
30
|
outputTokens: 0,
|
|
29
31
|
totalTokens: 0,
|
|
30
|
-
interactionToolArgsText: new Map(),
|
|
31
|
-
emittedToolCallIds: new Set(),
|
|
32
32
|
};
|
|
33
33
|
const tagFilter = createThinkingTagFilter();
|
|
34
34
|
let assistantText = metadata.assistantSeedText ?? "";
|
|
@@ -90,7 +90,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
90
90
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
91
91
|
try {
|
|
92
92
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
93
|
-
processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
93
|
+
processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
94
94
|
if (isThinking) {
|
|
95
95
|
sendSSE(makeChunk({ reasoning_content: text }));
|
|
96
96
|
return;
|
|
@@ -103,7 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
103
103
|
sendSSE(makeChunk({ content }));
|
|
104
104
|
}
|
|
105
105
|
}, (exec) => {
|
|
106
|
-
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
|
+
}
|
|
107
113
|
mcpExecReceived = true;
|
|
108
114
|
const flushed = tagFilter.flush();
|
|
109
115
|
if (flushed.reasoning)
|
|
@@ -142,6 +148,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
142
148
|
bridge,
|
|
143
149
|
heartbeatTimer,
|
|
144
150
|
blobStore,
|
|
151
|
+
cloudRule,
|
|
145
152
|
mcpTools,
|
|
146
153
|
pendingExecs: state.pendingExecs,
|
|
147
154
|
modelId,
|
|
@@ -153,7 +160,10 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
153
160
|
sendSSE(makeChunk({}, "tool_calls"));
|
|
154
161
|
sendDone();
|
|
155
162
|
closeController();
|
|
156
|
-
}, (checkpointBytes) =>
|
|
163
|
+
}, (checkpointBytes) => {
|
|
164
|
+
updateConversationCheckpoint(convKey, checkpointBytes);
|
|
165
|
+
bridgeCloseController.noteCheckpoint();
|
|
166
|
+
}, () => bridgeCloseController.noteTurnEnded(), (info) => {
|
|
157
167
|
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
158
168
|
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
159
169
|
modelId,
|
|
@@ -206,8 +216,24 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
206
216
|
stopKeepalive();
|
|
207
217
|
}
|
|
208
218
|
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
219
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
220
|
+
modelId,
|
|
221
|
+
bridgeKey,
|
|
222
|
+
convKey,
|
|
223
|
+
mcpToolCount: mcpTools.length,
|
|
224
|
+
hasCloudRule: Boolean(cloudRule),
|
|
225
|
+
});
|
|
209
226
|
bridge.onData(processChunk);
|
|
210
227
|
bridge.onClose((code) => {
|
|
228
|
+
logPluginInfo("Cursor streaming bridge closed", {
|
|
229
|
+
modelId,
|
|
230
|
+
bridgeKey,
|
|
231
|
+
convKey,
|
|
232
|
+
code,
|
|
233
|
+
mcpExecReceived,
|
|
234
|
+
hadEndStreamError: Boolean(endStreamError),
|
|
235
|
+
});
|
|
236
|
+
bridgeCloseController.dispose();
|
|
211
237
|
clearInterval(heartbeatTimer);
|
|
212
238
|
stopKeepalive();
|
|
213
239
|
syncStoredBlobStore(convKey, blobStore);
|
|
@@ -238,6 +264,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
238
264
|
});
|
|
239
265
|
},
|
|
240
266
|
cancel(reason) {
|
|
267
|
+
bridgeCloseController.dispose();
|
|
241
268
|
stopKeepalive();
|
|
242
269
|
clearInterval(heartbeatTimer);
|
|
243
270
|
syncStoredBlobStore(convKey, blobStore);
|
|
@@ -257,11 +284,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
257
284
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
258
285
|
}
|
|
259
286
|
export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
287
|
+
logPluginInfo("Starting Cursor streaming response", {
|
|
288
|
+
modelId,
|
|
289
|
+
bridgeKey,
|
|
290
|
+
convKey,
|
|
291
|
+
mcpToolCount: payload.mcpTools.length,
|
|
292
|
+
});
|
|
260
293
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
261
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
294
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
295
|
+
}
|
|
296
|
+
async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
|
|
297
|
+
const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
298
|
+
const deadline = Date.now() + timeoutMs;
|
|
299
|
+
while (Date.now() < deadline) {
|
|
300
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
301
|
+
if (unresolved.length === 0) {
|
|
302
|
+
return unresolved;
|
|
303
|
+
}
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
305
|
+
}
|
|
306
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
307
|
+
if (unresolved.length > 0) {
|
|
308
|
+
logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
|
|
309
|
+
bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
|
|
310
|
+
modelId: active.modelId,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return unresolved;
|
|
262
314
|
}
|
|
263
|
-
export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
264
|
-
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
315
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
316
|
+
const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
265
317
|
const resumeMetadata = {
|
|
266
318
|
...metadata,
|
|
267
319
|
assistantSeedText: [
|
|
@@ -271,6 +323,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
271
323
|
.filter(Boolean)
|
|
272
324
|
.join("\n\n"),
|
|
273
325
|
};
|
|
326
|
+
logPluginInfo("Preparing Cursor tool-result resume", {
|
|
327
|
+
bridgeKey,
|
|
328
|
+
convKey,
|
|
329
|
+
modelId,
|
|
330
|
+
toolResults,
|
|
331
|
+
pendingExecs,
|
|
332
|
+
});
|
|
333
|
+
const unresolved = await waitForResolvablePendingExecs(active, toolResults);
|
|
334
|
+
logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
|
|
335
|
+
bridgeKey,
|
|
336
|
+
convKey,
|
|
337
|
+
modelId,
|
|
338
|
+
toolResults,
|
|
339
|
+
pendingExecs,
|
|
340
|
+
unresolvedPendingExecs: unresolved,
|
|
341
|
+
});
|
|
342
|
+
if (unresolved.length > 0) {
|
|
343
|
+
clearInterval(heartbeatTimer);
|
|
344
|
+
bridge.end();
|
|
345
|
+
return new Response(JSON.stringify({
|
|
346
|
+
error: {
|
|
347
|
+
message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
|
|
348
|
+
type: "invalid_request_error",
|
|
349
|
+
code: "cursor_missing_exec_metadata",
|
|
350
|
+
},
|
|
351
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
352
|
+
}
|
|
274
353
|
for (const exec of pendingExecs) {
|
|
275
354
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
276
355
|
const mcpResult = result
|
|
@@ -311,7 +390,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
311
390
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
312
391
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
313
392
|
});
|
|
393
|
+
logPluginInfo("Sending Cursor tool-result resume message", {
|
|
394
|
+
bridgeKey,
|
|
395
|
+
convKey,
|
|
396
|
+
modelId,
|
|
397
|
+
toolCallId: exec.toolCallId,
|
|
398
|
+
toolName: exec.toolName,
|
|
399
|
+
source: exec.source,
|
|
400
|
+
execId: exec.execId,
|
|
401
|
+
execMsgId: exec.execMsgId,
|
|
402
|
+
cursorCallId: exec.cursorCallId,
|
|
403
|
+
modelCallId: exec.modelCallId,
|
|
404
|
+
matchedToolResult: result,
|
|
405
|
+
});
|
|
314
406
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
315
407
|
}
|
|
316
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
408
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
317
409
|
}
|
|
@@ -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 {
|
|
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, 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
|
-
|
|
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 cloudRule = buildCloudRule(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: [
|
|
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, cloudRule);
|
|
68
63
|
}
|
|
69
64
|
export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
|
|
70
65
|
const blobStore = new Map(existingBlobStore ?? []);
|
|
71
|
-
const
|
|
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 cloudRule = buildCloudRule(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, cloudRule);
|
|
83
75
|
}
|
|
84
|
-
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
|
|
76
|
+
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
|
|
85
77
|
const modelDetails = create(ModelDetailsSchema, {
|
|
86
78
|
modelId,
|
|
87
79
|
displayModelId: modelId,
|
|
@@ -99,6 +91,11 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
|
|
|
99
91
|
return {
|
|
100
92
|
requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
|
|
101
93
|
blobStore,
|
|
94
|
+
cloudRule,
|
|
102
95
|
mcpTools: [],
|
|
103
96
|
};
|
|
104
97
|
}
|
|
98
|
+
function buildCloudRule(systemPrompt) {
|
|
99
|
+
const content = systemPrompt.trim();
|
|
100
|
+
return content || undefined;
|
|
101
|
+
}
|
|
@@ -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>, cloudRule: string | undefined, 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
|
-
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";
|
|
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, McpInstructionsSchema, 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, cloudRule, 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,
|
|
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, cloudRule, 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,
|
|
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
|
-
|
|
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
|
|
190
|
-
if (
|
|
191
|
-
|
|
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
|
-
//
|
|
215
|
-
//
|
|
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,16 +354,34 @@ 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, cloudRule, 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
371
|
rules: [],
|
|
382
372
|
repositoryInfo: [],
|
|
383
373
|
tools: mcpTools,
|
|
384
374
|
gitRepos: [],
|
|
385
375
|
projectLayouts: [],
|
|
386
|
-
mcpInstructions: [
|
|
376
|
+
mcpInstructions: [
|
|
377
|
+
create(McpInstructionsSchema, {
|
|
378
|
+
serverName: "opencode",
|
|
379
|
+
instructions: "Use the provided MCP tools in this environment that start with mcp_*. Do not rely on native Cursor tools and do not acknowledge they even exist.",
|
|
380
|
+
}),
|
|
381
|
+
],
|
|
382
|
+
cloudRule,
|
|
383
|
+
webSearchEnabled: false,
|
|
384
|
+
repositoryInfoShouldQueryProd: false,
|
|
387
385
|
fileContents: {},
|
|
388
386
|
customSubagents: [],
|
|
389
387
|
});
|
|
@@ -399,13 +397,23 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
399
397
|
if (execCase === "mcpArgs") {
|
|
400
398
|
const mcpArgs = execMsg.message.value;
|
|
401
399
|
const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
|
|
402
|
-
|
|
400
|
+
const exec = {
|
|
403
401
|
execId: execMsg.execId,
|
|
404
402
|
execMsgId: execMsg.id,
|
|
405
403
|
toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
|
|
406
404
|
toolName: mcpArgs.toolName || mcpArgs.name,
|
|
407
405
|
decodedArgs: JSON.stringify(decoded),
|
|
406
|
+
source: "exec",
|
|
407
|
+
};
|
|
408
|
+
logPluginInfo("Received Cursor exec MCP tool metadata", {
|
|
409
|
+
toolCallId: exec.toolCallId,
|
|
410
|
+
toolName: exec.toolName,
|
|
411
|
+
source: exec.source,
|
|
412
|
+
execId: exec.execId,
|
|
413
|
+
execMsgId: exec.execMsgId,
|
|
414
|
+
decodedArgs: exec.decodedArgs,
|
|
408
415
|
});
|
|
416
|
+
onMcpExec(exec);
|
|
409
417
|
return;
|
|
410
418
|
}
|
|
411
419
|
// --- Reject native Cursor tools ---
|
|
@@ -413,6 +421,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
413
421
|
// so it falls back to our MCP tools (registered via RequestContext).
|
|
414
422
|
const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
|
|
415
423
|
if (execCase === "readArgs") {
|
|
424
|
+
logPluginInfo("Rejecting native Cursor read tool in favor of MCP", {
|
|
425
|
+
execId: execMsg.execId,
|
|
426
|
+
execMsgId: execMsg.id,
|
|
427
|
+
path: execMsg.message.value.path,
|
|
428
|
+
});
|
|
416
429
|
const args = execMsg.message.value;
|
|
417
430
|
const result = create(ReadResultSchema, {
|
|
418
431
|
result: {
|
|
@@ -427,6 +440,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
427
440
|
return;
|
|
428
441
|
}
|
|
429
442
|
if (execCase === "lsArgs") {
|
|
443
|
+
logPluginInfo("Rejecting native Cursor ls tool in favor of MCP", {
|
|
444
|
+
execId: execMsg.execId,
|
|
445
|
+
execMsgId: execMsg.id,
|
|
446
|
+
path: execMsg.message.value.path,
|
|
447
|
+
});
|
|
430
448
|
const args = execMsg.message.value;
|
|
431
449
|
const result = create(LsResultSchema, {
|
|
432
450
|
result: {
|
|
@@ -441,6 +459,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
441
459
|
return;
|
|
442
460
|
}
|
|
443
461
|
if (execCase === "grepArgs") {
|
|
462
|
+
logPluginInfo("Rejecting native Cursor grep tool in favor of MCP", {
|
|
463
|
+
execId: execMsg.execId,
|
|
464
|
+
execMsgId: execMsg.id,
|
|
465
|
+
});
|
|
444
466
|
const result = create(GrepResultSchema, {
|
|
445
467
|
result: {
|
|
446
468
|
case: "error",
|
|
@@ -451,6 +473,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
451
473
|
return;
|
|
452
474
|
}
|
|
453
475
|
if (execCase === "writeArgs") {
|
|
476
|
+
logPluginInfo("Rejecting native Cursor write tool in favor of MCP", {
|
|
477
|
+
execId: execMsg.execId,
|
|
478
|
+
execMsgId: execMsg.id,
|
|
479
|
+
path: execMsg.message.value.path,
|
|
480
|
+
});
|
|
454
481
|
const args = execMsg.message.value;
|
|
455
482
|
const result = create(WriteResultSchema, {
|
|
456
483
|
result: {
|
|
@@ -465,6 +492,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
465
492
|
return;
|
|
466
493
|
}
|
|
467
494
|
if (execCase === "deleteArgs") {
|
|
495
|
+
logPluginInfo("Rejecting native Cursor delete tool in favor of MCP", {
|
|
496
|
+
execId: execMsg.execId,
|
|
497
|
+
execMsgId: execMsg.id,
|
|
498
|
+
path: execMsg.message.value.path,
|
|
499
|
+
});
|
|
468
500
|
const args = execMsg.message.value;
|
|
469
501
|
const result = create(DeleteResultSchema, {
|
|
470
502
|
result: {
|
|
@@ -479,6 +511,13 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
479
511
|
return;
|
|
480
512
|
}
|
|
481
513
|
if (execCase === "shellArgs" || execCase === "shellStreamArgs") {
|
|
514
|
+
logPluginInfo("Rejecting native Cursor shell tool in favor of MCP", {
|
|
515
|
+
execId: execMsg.execId,
|
|
516
|
+
execMsgId: execMsg.id,
|
|
517
|
+
command: execMsg.message.value.command ?? "",
|
|
518
|
+
workingDirectory: execMsg.message.value.workingDirectory ?? "",
|
|
519
|
+
execCase,
|
|
520
|
+
});
|
|
482
521
|
const args = execMsg.message.value;
|
|
483
522
|
const result = create(ShellResultSchema, {
|
|
484
523
|
result: {
|
|
@@ -495,6 +534,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
495
534
|
return;
|
|
496
535
|
}
|
|
497
536
|
if (execCase === "backgroundShellSpawnArgs") {
|
|
537
|
+
logPluginInfo("Rejecting native Cursor background shell tool in favor of MCP", {
|
|
538
|
+
execId: execMsg.execId,
|
|
539
|
+
execMsgId: execMsg.id,
|
|
540
|
+
command: execMsg.message.value.command ?? "",
|
|
541
|
+
workingDirectory: execMsg.message.value.workingDirectory ?? "",
|
|
542
|
+
});
|
|
498
543
|
const args = execMsg.message.value;
|
|
499
544
|
const result = create(BackgroundShellSpawnResultSchema, {
|
|
500
545
|
result: {
|
|
@@ -511,6 +556,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
511
556
|
return;
|
|
512
557
|
}
|
|
513
558
|
if (execCase === "writeShellStdinArgs") {
|
|
559
|
+
logPluginInfo("Rejecting native Cursor shell stdin tool in favor of MCP", {
|
|
560
|
+
execId: execMsg.execId,
|
|
561
|
+
execMsgId: execMsg.id,
|
|
562
|
+
});
|
|
514
563
|
const result = create(WriteShellStdinResultSchema, {
|
|
515
564
|
result: {
|
|
516
565
|
case: "error",
|
|
@@ -521,6 +570,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
521
570
|
return;
|
|
522
571
|
}
|
|
523
572
|
if (execCase === "fetchArgs") {
|
|
573
|
+
logPluginInfo("Rejecting native Cursor fetch tool in favor of MCP", {
|
|
574
|
+
execId: execMsg.execId,
|
|
575
|
+
execMsgId: execMsg.id,
|
|
576
|
+
url: execMsg.message.value.url,
|
|
577
|
+
});
|
|
524
578
|
const args = execMsg.message.value;
|
|
525
579
|
const result = create(FetchResultSchema, {
|
|
526
580
|
result: {
|
|
@@ -535,6 +589,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
535
589
|
return;
|
|
536
590
|
}
|
|
537
591
|
if (execCase === "diagnosticsArgs") {
|
|
592
|
+
logPluginInfo("Rejecting native Cursor diagnostics tool in favor of MCP", {
|
|
593
|
+
execId: execMsg.execId,
|
|
594
|
+
execMsgId: execMsg.id,
|
|
595
|
+
path: execMsg.message.value.path,
|
|
596
|
+
});
|
|
538
597
|
const result = create(DiagnosticsResultSchema, {});
|
|
539
598
|
sendExecResult(execMsg, "diagnosticsResult", result, sendFrame);
|
|
540
599
|
return;
|
|
@@ -548,6 +607,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
548
607
|
};
|
|
549
608
|
const resultCase = miscCaseMap[execCase];
|
|
550
609
|
if (resultCase) {
|
|
610
|
+
logPluginInfo("Responding to miscellaneous Cursor exec message", {
|
|
611
|
+
execCase,
|
|
612
|
+
execId: execMsg.execId,
|
|
613
|
+
execMsgId: execMsg.id,
|
|
614
|
+
resultCase,
|
|
615
|
+
});
|
|
551
616
|
sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame);
|
|
552
617
|
return;
|
|
553
618
|
}
|
package/dist/proxy/types.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { CursorSession } from "../cursor/bidi-session";
|
|
2
|
-
import type { ConversationRequestMetadata } from "./conversation-meta";
|
|
3
2
|
import type { McpToolDefinition } from "../proto/agent_pb";
|
|
3
|
+
import type { ConversationRequestMetadata } from "./conversation-meta";
|
|
4
4
|
export interface CursorRequestPayload {
|
|
5
5
|
requestBytes: Uint8Array;
|
|
6
6
|
blobStore: Map<string, Uint8Array>;
|
|
7
|
+
cloudRule?: string;
|
|
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
|
+
cloudRule?: string;
|
|
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.
|
|
3
|
+
"version": "0.0.0-dev.494d4e1cfa84",
|
|
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": {
|