@playwo/opencode-cursor-oauth 0.0.0-dev.2c48be2f48c9 → 0.0.0-dev.4463bb589222
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/openai/messages.js +5 -0
- package/dist/plugin/cursor-auth-plugin.js +0 -1
- package/dist/proxy/bridge-non-streaming.js +13 -1
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.js +29 -10
- package/dist/proxy/chat-completion.js +3 -2
- package/dist/proxy/cursor-request.d.ts +1 -0
- package/dist/proxy/cursor-request.js +19 -1
- package/dist/proxy/server.js +23 -5
- package/dist/proxy/stream-dispatch.d.ts +6 -1
- package/dist/proxy/stream-dispatch.js +182 -7
- package/dist/proxy/stream-state.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type CursorBaseRequestOptions } from "./headers";
|
|
2
|
-
export declare function encodeBidiAppendRequest(dataHex: string, requestId: string, appendSeqno: number): Uint8Array;
|
|
3
2
|
export interface CursorSession {
|
|
4
3
|
write: (data: Uint8Array) => void;
|
|
5
4
|
end: () => void;
|
|
@@ -8,6 +7,6 @@ export interface CursorSession {
|
|
|
8
7
|
readonly alive: boolean;
|
|
9
8
|
}
|
|
10
9
|
export interface CreateCursorSessionOptions extends CursorBaseRequestOptions {
|
|
11
|
-
|
|
10
|
+
initialRequestBytes: Uint8Array;
|
|
12
11
|
}
|
|
13
12
|
export declare function createCursorSession(options: CreateCursorSessionOptions): Promise<CursorSession>;
|
|
@@ -1,149 +1,164 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
export function encodeBidiAppendRequest(dataHex, requestId, appendSeqno) {
|
|
8
|
-
const requestIdBytes = toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId }));
|
|
9
|
-
return concatBytes([
|
|
10
|
-
encodeProtoStringField(1, dataHex),
|
|
11
|
-
encodeProtoMessageField(2, requestIdBytes),
|
|
12
|
-
encodeProtoVarintField(3, appendSeqno),
|
|
13
|
-
]);
|
|
14
|
-
}
|
|
1
|
+
import { connect as connectHttp2, } from "node:http2";
|
|
2
|
+
import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
|
|
3
|
+
import { frameConnectMessage } from "./connect-framing";
|
|
4
|
+
import { buildCursorHeaderValues, } from "./headers";
|
|
5
|
+
import { errorDetails, logPluginError } from "../logger";
|
|
6
|
+
const CURSOR_BIDI_RUN_PATH = "/agent.v1.AgentService/Run";
|
|
15
7
|
export async function createCursorSession(options) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
headers: buildCursorHeaders(options, "application/connect+proto", {
|
|
19
|
-
accept: "text/event-stream",
|
|
20
|
-
"connect-protocol-version": "1",
|
|
21
|
-
}),
|
|
22
|
-
body: toFetchBody(frameConnectMessage(toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId: options.requestId })))),
|
|
23
|
-
});
|
|
24
|
-
if (!response.ok || !response.body) {
|
|
25
|
-
const errorBody = await response.text().catch(() => "");
|
|
26
|
-
logPluginError("Cursor RunSSE request failed", {
|
|
27
|
-
requestId: options.requestId,
|
|
28
|
-
status: response.status,
|
|
29
|
-
responseBody: errorBody,
|
|
30
|
-
});
|
|
31
|
-
throw new Error(`RunSSE failed: ${response.status}${errorBody ? ` ${errorBody}` : ""}`);
|
|
8
|
+
if (options.initialRequestBytes.length === 0) {
|
|
9
|
+
throw new Error("Cursor sessions require an initial request message");
|
|
32
10
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const appendResponse = await fetch(new URL("/aiserver.v1.BidiService/BidiAppend", options.url ?? CURSOR_API_URL), {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: buildCursorHeaders(options, "application/proto"),
|
|
56
|
-
body: toFetchBody(requestBody),
|
|
57
|
-
signal: abortController.signal,
|
|
58
|
-
});
|
|
59
|
-
if (!appendResponse.ok) {
|
|
60
|
-
const errorBody = await appendResponse.text().catch(() => "");
|
|
61
|
-
logPluginError("Cursor BidiAppend request failed", {
|
|
62
|
-
requestId: options.requestId,
|
|
63
|
-
appendSeqno: appendSeqno - 1,
|
|
64
|
-
status: appendResponse.status,
|
|
65
|
-
responseBody: errorBody,
|
|
66
|
-
});
|
|
67
|
-
throw new Error(`BidiAppend failed: ${appendResponse.status}${errorBody ? ` ${errorBody}` : ""}`);
|
|
68
|
-
}
|
|
69
|
-
await appendResponse.arrayBuffer().catch(() => undefined);
|
|
70
|
-
};
|
|
71
|
-
(async () => {
|
|
72
|
-
try {
|
|
73
|
-
while (true) {
|
|
74
|
-
const { done, value } = await reader.read();
|
|
75
|
-
if (done) {
|
|
76
|
-
finish(0);
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
if (value && value.length > 0) {
|
|
80
|
-
const chunk = Buffer.from(value);
|
|
81
|
-
if (cbs.data) {
|
|
82
|
-
cbs.data(chunk);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
pendingChunks.push(chunk);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
11
|
+
const target = new URL(CURSOR_BIDI_RUN_PATH, options.url ?? CURSOR_API_URL);
|
|
12
|
+
const authority = `${target.protocol}//${target.host}`;
|
|
13
|
+
const requestId = crypto.randomUUID();
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const cbs = {
|
|
16
|
+
data: null,
|
|
17
|
+
close: null,
|
|
18
|
+
};
|
|
19
|
+
let session;
|
|
20
|
+
let stream;
|
|
21
|
+
let alive = true;
|
|
22
|
+
let closeCode = 0;
|
|
23
|
+
let opened = false;
|
|
24
|
+
let settled = false;
|
|
25
|
+
let statusCode = 0;
|
|
26
|
+
const pendingChunks = [];
|
|
27
|
+
const errorChunks = [];
|
|
28
|
+
const closeTransport = () => {
|
|
29
|
+
try {
|
|
30
|
+
stream?.close();
|
|
88
31
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
32
|
+
catch { }
|
|
33
|
+
try {
|
|
34
|
+
session?.close();
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
};
|
|
38
|
+
const finish = (code) => {
|
|
39
|
+
if (!alive)
|
|
40
|
+
return;
|
|
41
|
+
alive = false;
|
|
42
|
+
closeCode = code;
|
|
43
|
+
cbs.close?.(code);
|
|
44
|
+
closeTransport();
|
|
45
|
+
};
|
|
46
|
+
const rejectOpen = (error) => {
|
|
47
|
+
if (settled)
|
|
48
|
+
return;
|
|
49
|
+
settled = true;
|
|
50
|
+
alive = false;
|
|
51
|
+
closeTransport();
|
|
52
|
+
reject(error);
|
|
53
|
+
};
|
|
54
|
+
const resolveOpen = (sessionHandle) => {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
58
|
+
opened = true;
|
|
59
|
+
resolve(sessionHandle);
|
|
60
|
+
};
|
|
61
|
+
const handleTransportError = (message, error) => {
|
|
62
|
+
logPluginError(message, {
|
|
63
|
+
requestId,
|
|
64
|
+
url: target.toString(),
|
|
93
65
|
...errorDetails(error),
|
|
94
66
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})();
|
|
98
|
-
return {
|
|
99
|
-
get alive() {
|
|
100
|
-
return alive;
|
|
101
|
-
},
|
|
102
|
-
write(data) {
|
|
103
|
-
if (!alive)
|
|
67
|
+
if (!opened) {
|
|
68
|
+
rejectOpen(new Error(error instanceof Error ? error.message : String(error ?? message)));
|
|
104
69
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
70
|
+
}
|
|
71
|
+
finish(1);
|
|
72
|
+
};
|
|
73
|
+
const sessionHandle = {
|
|
74
|
+
get alive() {
|
|
75
|
+
return alive;
|
|
76
|
+
},
|
|
77
|
+
write(data) {
|
|
78
|
+
if (!alive || !stream)
|
|
79
|
+
return;
|
|
112
80
|
try {
|
|
113
|
-
|
|
81
|
+
stream.write(frameConnectMessage(data));
|
|
114
82
|
}
|
|
115
|
-
catch {
|
|
116
|
-
|
|
117
|
-
|
|
83
|
+
catch (error) {
|
|
84
|
+
handleTransportError("Cursor HTTP/2 write failed", error);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
end() {
|
|
88
|
+
finish(0);
|
|
89
|
+
},
|
|
90
|
+
onData(cb) {
|
|
91
|
+
cbs.data = cb;
|
|
92
|
+
while (pendingChunks.length > 0) {
|
|
93
|
+
cb(pendingChunks.shift());
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onClose(cb) {
|
|
97
|
+
if (!alive) {
|
|
98
|
+
queueMicrotask(() => cb(closeCode));
|
|
118
99
|
}
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
else {
|
|
101
|
+
cbs.close = cb;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
session = connectHttp2(authority);
|
|
107
|
+
session.once("error", (error) => {
|
|
108
|
+
handleTransportError("Cursor HTTP/2 session failed", error);
|
|
121
109
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
110
|
+
const headers = {
|
|
111
|
+
":method": "POST",
|
|
112
|
+
":path": `${target.pathname}${target.search}`,
|
|
113
|
+
...buildCursorHeaderValues(options, "application/connect+proto", {
|
|
114
|
+
accept: "application/connect+proto",
|
|
115
|
+
"connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
stream = session.request(headers);
|
|
119
|
+
stream.once("response", (responseHeaders) => {
|
|
120
|
+
const statusHeader = responseHeaders[":status"];
|
|
121
|
+
statusCode =
|
|
122
|
+
typeof statusHeader === "number"
|
|
123
|
+
? statusHeader
|
|
124
|
+
: Number(statusHeader ?? 0);
|
|
125
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
126
|
+
resolveOpen(sessionHandle);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
stream.on("data", (chunk) => {
|
|
130
|
+
const buffer = Buffer.from(chunk);
|
|
131
|
+
if (!opened && statusCode >= 400) {
|
|
132
|
+
errorChunks.push(buffer);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (cbs.data) {
|
|
136
|
+
cbs.data(buffer);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
pendingChunks.push(buffer);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
stream.once("end", () => {
|
|
143
|
+
if (!opened) {
|
|
144
|
+
const errorBody = Buffer.concat(errorChunks).toString("utf8").trim();
|
|
145
|
+
logPluginError("Cursor HTTP/2 Run request failed", {
|
|
146
|
+
requestId,
|
|
147
|
+
status: statusCode,
|
|
148
|
+
responseBody: errorBody,
|
|
149
|
+
});
|
|
150
|
+
rejectOpen(new Error(`Run failed: ${statusCode || 1}${errorBody ? ` ${errorBody}` : ""}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
finish(statusCode >= 200 && statusCode < 300 ? 0 : statusCode || 1);
|
|
154
|
+
});
|
|
155
|
+
stream.once("error", (error) => {
|
|
156
|
+
handleTransportError("Cursor HTTP/2 stream failed", error);
|
|
157
|
+
});
|
|
158
|
+
stream.write(frameConnectMessage(options.initialRequestBytes));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
handleTransportError("Cursor HTTP/2 transport setup failed", error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
149
164
|
}
|
package/dist/cursor/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
|
|
2
2
|
export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
|
|
3
3
|
export { buildCursorHeaders, buildCursorHeaderValues, type CursorBaseRequestOptions, } from "./headers";
|
|
4
|
-
export { createCursorSession,
|
|
4
|
+
export { createCursorSession, type CreateCursorSessionOptions, type CursorSession, } from "./bidi-session";
|
|
5
5
|
export { callCursorUnaryRpc, type CursorUnaryRpcOptions } from "./unary-rpc";
|
package/dist/cursor/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
|
|
2
2
|
export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
|
|
3
3
|
export { buildCursorHeaders, buildCursorHeaderValues, } from "./headers";
|
|
4
|
-
export { createCursorSession,
|
|
4
|
+
export { createCursorSession, } from "./bidi-session";
|
|
5
5
|
export { callCursorUnaryRpc } from "./unary-rpc";
|
package/dist/cursor/unary-rpc.js
CHANGED
|
@@ -1,67 +1,10 @@
|
|
|
1
1
|
import { connect as connectHttp2, } from "node:http2";
|
|
2
2
|
import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
|
|
3
|
-
import {
|
|
4
|
-
import { buildCursorHeaders, buildCursorHeaderValues } from "./headers";
|
|
3
|
+
import { buildCursorHeaderValues } from "./headers";
|
|
5
4
|
import { errorDetails, logPluginError } from "../logger";
|
|
6
5
|
export async function callCursorUnaryRpc(options) {
|
|
7
6
|
const target = new URL(options.rpcPath, options.url ?? CURSOR_API_URL);
|
|
8
|
-
|
|
9
|
-
if (transport === "http2" ||
|
|
10
|
-
(transport === "auto" && target.protocol === "https:")) {
|
|
11
|
-
const http2Result = await callCursorUnaryRpcOverHttp2(options, target);
|
|
12
|
-
if (transport === "http2" ||
|
|
13
|
-
http2Result.timedOut ||
|
|
14
|
-
http2Result.exitCode !== 1) {
|
|
15
|
-
return http2Result;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return callCursorUnaryRpcOverFetch(options, target);
|
|
19
|
-
}
|
|
20
|
-
async function callCursorUnaryRpcOverFetch(options, target) {
|
|
21
|
-
let timedOut = false;
|
|
22
|
-
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
23
|
-
const controller = new AbortController();
|
|
24
|
-
const timeout = timeoutMs > 0
|
|
25
|
-
? setTimeout(() => {
|
|
26
|
-
timedOut = true;
|
|
27
|
-
controller.abort();
|
|
28
|
-
}, timeoutMs)
|
|
29
|
-
: undefined;
|
|
30
|
-
try {
|
|
31
|
-
const response = await fetch(target, {
|
|
32
|
-
method: "POST",
|
|
33
|
-
headers: buildCursorHeaders(options, "application/proto", {
|
|
34
|
-
accept: "application/proto, application/json",
|
|
35
|
-
"connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
|
|
36
|
-
"connect-timeout-ms": String(timeoutMs),
|
|
37
|
-
}),
|
|
38
|
-
body: toFetchBody(options.requestBody),
|
|
39
|
-
signal: controller.signal,
|
|
40
|
-
});
|
|
41
|
-
const body = new Uint8Array(await response.arrayBuffer());
|
|
42
|
-
return {
|
|
43
|
-
body,
|
|
44
|
-
exitCode: response.ok ? 0 : response.status,
|
|
45
|
-
timedOut,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
logPluginError("Cursor unary fetch transport failed", {
|
|
50
|
-
rpcPath: options.rpcPath,
|
|
51
|
-
url: target.toString(),
|
|
52
|
-
timeoutMs,
|
|
53
|
-
timedOut,
|
|
54
|
-
});
|
|
55
|
-
return {
|
|
56
|
-
body: new Uint8Array(),
|
|
57
|
-
exitCode: timedOut ? 124 : 1,
|
|
58
|
-
timedOut,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
if (timeout)
|
|
63
|
-
clearTimeout(timeout);
|
|
64
|
-
}
|
|
7
|
+
return callCursorUnaryRpcOverHttp2(options, target);
|
|
65
8
|
}
|
|
66
9
|
async function callCursorUnaryRpcOverHttp2(options, target) {
|
|
67
10
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
package/dist/openai/messages.js
CHANGED
|
@@ -79,6 +79,11 @@ export function parseMessages(messages) {
|
|
|
79
79
|
completedTurnStates = parsedTurns.slice(0, -1);
|
|
80
80
|
userText = lastTurn.userText;
|
|
81
81
|
}
|
|
82
|
+
else if (lastTurn.userText && hasAssistantSummary) {
|
|
83
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
84
|
+
userText = lastTurn.userText;
|
|
85
|
+
pendingAssistantSummary = summarizeTurnSegments(lastTurn.segments);
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
const turns = completedTurnStates
|
|
84
89
|
.map((turn) => ({
|
|
@@ -110,7 +110,6 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
110
110
|
async "chat.headers"(incoming, output) {
|
|
111
111
|
if (incoming.model.providerID !== CURSOR_PROVIDER_ID)
|
|
112
112
|
return;
|
|
113
|
-
output.headers["x-opencode-session-id"] = incoming.sessionID;
|
|
114
113
|
output.headers["x-session-id"] = incoming.sessionID;
|
|
115
114
|
if (incoming.agent) {
|
|
116
115
|
output.headers["x-opencode-agent"] = incoming.agent;
|
|
@@ -38,6 +38,8 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
38
38
|
pendingExecs: [],
|
|
39
39
|
outputTokens: 0,
|
|
40
40
|
totalTokens: 0,
|
|
41
|
+
interactionToolArgsText: new Map(),
|
|
42
|
+
emittedToolCallIds: new Set(),
|
|
41
43
|
};
|
|
42
44
|
const tagFilter = createThinkingTagFilter();
|
|
43
45
|
bridge.onData(createConnectFrameParser((messageBytes) => {
|
|
@@ -58,7 +60,17 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
|
|
|
58
60
|
},
|
|
59
61
|
});
|
|
60
62
|
scheduleBridgeEnd(bridge);
|
|
61
|
-
}, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), (info) => {
|
|
63
|
+
}, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
|
|
64
|
+
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
65
|
+
logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
|
|
66
|
+
modelId,
|
|
67
|
+
convKey,
|
|
68
|
+
category: info.category,
|
|
69
|
+
caseName: info.caseName,
|
|
70
|
+
detail: info.detail,
|
|
71
|
+
});
|
|
72
|
+
scheduleBridgeEnd(bridge);
|
|
73
|
+
}, (info) => {
|
|
62
74
|
endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
|
|
63
75
|
logPluginError("Closing non-streaming Cursor bridge after unsupported exec", {
|
|
64
76
|
modelId,
|
|
@@ -2,12 +2,10 @@ import { createCursorSession } from "../cursor/bidi-session";
|
|
|
2
2
|
import { makeHeartbeatBytes } from "./stream-dispatch";
|
|
3
3
|
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
4
4
|
export async function startBridge(accessToken, requestBytes) {
|
|
5
|
-
const requestId = crypto.randomUUID();
|
|
6
5
|
const bridge = await createCursorSession({
|
|
7
6
|
accessToken,
|
|
8
|
-
|
|
7
|
+
initialRequestBytes: requestBytes,
|
|
9
8
|
});
|
|
10
|
-
bridge.write(requestBytes);
|
|
11
9
|
const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), HEARTBEAT_INTERVAL_MS);
|
|
12
10
|
return { bridge, heartbeatTimer };
|
|
13
11
|
}
|
|
@@ -27,6 +27,8 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
27
27
|
pendingExecs: [],
|
|
28
28
|
outputTokens: 0,
|
|
29
29
|
totalTokens: 0,
|
|
30
|
+
interactionToolArgsText: new Map(),
|
|
31
|
+
emittedToolCallIds: new Set(),
|
|
30
32
|
};
|
|
31
33
|
const tagFilter = createThinkingTagFilter();
|
|
32
34
|
let assistantText = metadata.assistantSeedText ?? "";
|
|
@@ -47,6 +49,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
47
49
|
return;
|
|
48
50
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
49
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
|
+
};
|
|
50
65
|
const closeController = () => {
|
|
51
66
|
if (closed)
|
|
52
67
|
return;
|
|
@@ -138,7 +153,18 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
138
153
|
sendSSE(makeChunk({}, "tool_calls"));
|
|
139
154
|
sendDone();
|
|
140
155
|
closeController();
|
|
141
|
-
}, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), (info) => {
|
|
156
|
+
}, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
|
|
157
|
+
endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
|
|
158
|
+
logPluginError("Closing Cursor bridge after unsupported message", {
|
|
159
|
+
modelId,
|
|
160
|
+
bridgeKey,
|
|
161
|
+
convKey,
|
|
162
|
+
category: info.category,
|
|
163
|
+
caseName: info.caseName,
|
|
164
|
+
detail: info.detail,
|
|
165
|
+
});
|
|
166
|
+
scheduleBridgeEnd(bridge);
|
|
167
|
+
}, (info) => {
|
|
142
168
|
endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
|
|
143
169
|
logPluginError("Closing Cursor bridge after unsupported exec", {
|
|
144
170
|
modelId,
|
|
@@ -187,10 +213,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
187
213
|
syncStoredBlobStore(convKey, blobStore);
|
|
188
214
|
if (endStreamError) {
|
|
189
215
|
activeBridges.delete(bridgeKey);
|
|
190
|
-
|
|
191
|
-
closed = true;
|
|
192
|
-
controller.error(endStreamError);
|
|
193
|
-
}
|
|
216
|
+
failStream(endStreamError.message, "cursor_bridge_closed");
|
|
194
217
|
return;
|
|
195
218
|
}
|
|
196
219
|
if (!mcpExecReceived) {
|
|
@@ -210,11 +233,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
210
233
|
}
|
|
211
234
|
activeBridges.delete(bridgeKey);
|
|
212
235
|
if (code !== 0 && !closed) {
|
|
213
|
-
|
|
214
|
-
sendSSE(makeChunk({}, "stop"));
|
|
215
|
-
sendSSE(makeUsageChunk());
|
|
216
|
-
sendDone();
|
|
217
|
-
closeController();
|
|
236
|
+
failStream("Cursor bridge connection lost", "cursor_bridge_closed");
|
|
218
237
|
}
|
|
219
238
|
});
|
|
220
239
|
},
|
|
@@ -82,12 +82,13 @@ export function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
82
82
|
// Build the request. When tool results are present but the bridge died,
|
|
83
83
|
// we must still include the last user text so Cursor has context.
|
|
84
84
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
85
|
+
const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
|
|
85
86
|
const needsInitialHandoff = !stored.checkpoint &&
|
|
86
|
-
(turns.length > 0 ||
|
|
87
|
+
(turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
|
|
87
88
|
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
88
89
|
let effectiveUserText = needsInitialHandoff
|
|
89
90
|
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
90
|
-
: toolResults.length > 0
|
|
91
|
+
: toolResults.length > 0 || hasPendingAssistantSummary
|
|
91
92
|
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
92
93
|
: userText;
|
|
93
94
|
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
@@ -3,3 +3,4 @@ export declare function buildCursorRequest(modelId: string, systemPrompt: string
|
|
|
3
3
|
userText: string;
|
|
4
4
|
assistantText: string;
|
|
5
5
|
}>, conversationId: string, checkpoint: Uint8Array | null, existingBlobStore?: Map<string, Uint8Array>): CursorRequestPayload;
|
|
6
|
+
export declare function buildCursorResumeRequest(modelId: string, systemPrompt: string, conversationId: string, checkpoint: Uint8Array, existingBlobStore?: Map<string, Uint8Array>): CursorRequestPayload;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
|
|
3
|
+
import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
|
|
4
4
|
export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
|
|
5
5
|
const blobStore = new Map(existingBlobStore ?? []);
|
|
6
6
|
// System prompt → blob store (Cursor requests it back via KV handshake)
|
|
@@ -64,6 +64,24 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
|
|
|
64
64
|
value: create(UserMessageActionSchema, { userMessage }),
|
|
65
65
|
},
|
|
66
66
|
});
|
|
67
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
|
|
68
|
+
}
|
|
69
|
+
export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
|
|
70
|
+
const blobStore = new Map(existingBlobStore ?? []);
|
|
71
|
+
const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
|
|
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);
|
|
75
|
+
const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
|
|
76
|
+
const action = create(ConversationActionSchema, {
|
|
77
|
+
action: {
|
|
78
|
+
case: "resumeAction",
|
|
79
|
+
value: create(ResumeActionSchema, {}),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
|
|
83
|
+
}
|
|
84
|
+
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
|
|
67
85
|
const modelDetails = create(ModelDetailsSchema, {
|
|
68
86
|
modelId,
|
|
69
87
|
displayModelId: modelId,
|
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);
|
|
@@ -7,6 +7,11 @@ export interface UnhandledExecInfo {
|
|
|
7
7
|
execId: string;
|
|
8
8
|
execMsgId: number;
|
|
9
9
|
}
|
|
10
|
+
export interface UnsupportedServerMessageInfo {
|
|
11
|
+
category: "agentMessage" | "interactionUpdate" | "interactionQuery" | "execServerControl" | "toolCall";
|
|
12
|
+
caseName: string;
|
|
13
|
+
detail?: string;
|
|
14
|
+
}
|
|
10
15
|
export declare function parseConnectEndStream(data: Uint8Array): Error | null;
|
|
11
16
|
export declare function makeHeartbeatBytes(): Uint8Array;
|
|
12
17
|
export declare function scheduleBridgeEnd(bridge: CursorSession): void;
|
|
@@ -34,4 +39,4 @@ export declare function computeUsage(state: StreamState): {
|
|
|
34
39
|
completion_tokens: number;
|
|
35
40
|
total_tokens: number;
|
|
36
41
|
};
|
|
37
|
-
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, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create, toBinary } from "@bufbuild/protobuf";
|
|
2
|
-
import { AgentClientMessageSchema, ClientHeartbeatSchema, ConversationStateStructureSchema, BackgroundShellSpawnResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpResultSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, 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, 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
4
|
import { logPluginError, logPluginWarn } from "../logger";
|
|
5
5
|
import { decodeMcpArgsMap } from "../openai/tools";
|
|
@@ -128,10 +128,10 @@ 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, onUnhandledExec) {
|
|
131
|
+
export function processServerMessage(msg, blobStore, 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, onMcpExec, onTurnEnded, onUnsupportedMessage);
|
|
135
135
|
}
|
|
136
136
|
else if (msgCase === "kvServerMessage") {
|
|
137
137
|
handleKvMessage(msg.message.value, blobStore, sendFrame);
|
|
@@ -139,6 +139,15 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
|
|
|
139
139
|
else if (msgCase === "execServerMessage") {
|
|
140
140
|
handleExecMessage(msg.message.value, mcpTools, sendFrame, onMcpExec, onUnhandledExec);
|
|
141
141
|
}
|
|
142
|
+
else if (msgCase === "execServerControlMessage") {
|
|
143
|
+
onUnsupportedMessage?.({
|
|
144
|
+
category: "execServerControl",
|
|
145
|
+
caseName: msg.message.value.message.case ?? "undefined",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else if (msgCase === "interactionQuery") {
|
|
149
|
+
handleInteractionQuery(msg.message.value, sendFrame, onUnsupportedMessage);
|
|
150
|
+
}
|
|
142
151
|
else if (msgCase === "conversationCheckpointUpdate") {
|
|
143
152
|
const stateStructure = msg.message.value;
|
|
144
153
|
if (stateStructure.tokenDetails) {
|
|
@@ -148,8 +157,14 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
|
|
|
148
157
|
onCheckpoint(toBinary(ConversationStateStructureSchema, stateStructure));
|
|
149
158
|
}
|
|
150
159
|
}
|
|
160
|
+
else {
|
|
161
|
+
onUnsupportedMessage?.({
|
|
162
|
+
category: "agentMessage",
|
|
163
|
+
caseName: msgCase ?? "undefined",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
151
166
|
}
|
|
152
|
-
function handleInteractionUpdate(update, state, onText) {
|
|
167
|
+
function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage) {
|
|
153
168
|
const updateCase = update.message?.case;
|
|
154
169
|
if (updateCase === "textDelta") {
|
|
155
170
|
const delta = update.message.value.text || "";
|
|
@@ -164,9 +179,159 @@ function handleInteractionUpdate(update, state, onText) {
|
|
|
164
179
|
else if (updateCase === "tokenDelta") {
|
|
165
180
|
state.outputTokens += update.message.value.tokens ?? 0;
|
|
166
181
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
else if (updateCase === "partialToolCall") {
|
|
183
|
+
const partial = update.message.value;
|
|
184
|
+
if (partial.callId && partial.argsTextDelta) {
|
|
185
|
+
state.interactionToolArgsText.set(partial.callId, partial.argsTextDelta);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (updateCase === "toolCallCompleted") {
|
|
189
|
+
const exec = decodeInteractionToolCall(update.message.value, state);
|
|
190
|
+
if (exec)
|
|
191
|
+
onMcpExec(exec);
|
|
192
|
+
}
|
|
193
|
+
else if (updateCase === "turnEnded") {
|
|
194
|
+
onTurnEnded?.();
|
|
195
|
+
}
|
|
196
|
+
else if (updateCase === "toolCallStarted" ||
|
|
197
|
+
updateCase === "toolCallDelta" ||
|
|
198
|
+
updateCase === "thinkingCompleted" ||
|
|
199
|
+
updateCase === "userMessageAppended" ||
|
|
200
|
+
updateCase === "summary" ||
|
|
201
|
+
updateCase === "summaryStarted" ||
|
|
202
|
+
updateCase === "summaryCompleted" ||
|
|
203
|
+
updateCase === "heartbeat" ||
|
|
204
|
+
updateCase === "stepStarted" ||
|
|
205
|
+
updateCase === "stepCompleted") {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
onUnsupportedMessage?.({
|
|
210
|
+
category: "interactionUpdate",
|
|
211
|
+
caseName: updateCase ?? "undefined",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// toolCallStarted, partialToolCall, toolCallDelta, and non-MCP
|
|
215
|
+
// toolCallCompleted updates are informational only. Actionable MCP tool
|
|
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
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
|
|
253
|
+
const queryCase = query.query.case;
|
|
254
|
+
if (queryCase === "webSearchRequestQuery") {
|
|
255
|
+
const response = create(WebSearchRequestResponseSchema, {
|
|
256
|
+
result: {
|
|
257
|
+
case: "rejected",
|
|
258
|
+
value: create(WebSearchRequestResponse_RejectedSchema, {
|
|
259
|
+
reason: "Native Cursor web search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
sendInteractionResponse(query.id, "webSearchRequestResponse", response, sendFrame);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (queryCase === "askQuestionInteractionQuery") {
|
|
267
|
+
const response = create(AskQuestionInteractionResponseSchema, {
|
|
268
|
+
result: create(AskQuestionResultSchema, {
|
|
269
|
+
result: {
|
|
270
|
+
case: "rejected",
|
|
271
|
+
value: create(AskQuestionRejectedSchema, {
|
|
272
|
+
reason: "Native Cursor question prompts are not available in this environment. Use the provided MCP tool `question` instead.",
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
sendInteractionResponse(query.id, "askQuestionInteractionResponse", response, sendFrame);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (queryCase === "switchModeRequestQuery") {
|
|
281
|
+
const response = create(SwitchModeRequestResponseSchema, {
|
|
282
|
+
result: {
|
|
283
|
+
case: "rejected",
|
|
284
|
+
value: create(SwitchModeRequestResponse_RejectedSchema, {
|
|
285
|
+
reason: "Cursor mode switching is not available in this environment. Continue using the current agent and the provided MCP tools.",
|
|
286
|
+
}),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
sendInteractionResponse(query.id, "switchModeRequestResponse", response, sendFrame);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (queryCase === "exaSearchRequestQuery") {
|
|
293
|
+
const response = create(ExaSearchRequestResponseSchema, {
|
|
294
|
+
result: {
|
|
295
|
+
case: "rejected",
|
|
296
|
+
value: create(ExaSearchRequestResponse_RejectedSchema, {
|
|
297
|
+
reason: "Native Cursor Exa search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
sendInteractionResponse(query.id, "exaSearchRequestResponse", response, sendFrame);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (queryCase === "exaFetchRequestQuery") {
|
|
305
|
+
const response = create(ExaFetchRequestResponseSchema, {
|
|
306
|
+
result: {
|
|
307
|
+
case: "rejected",
|
|
308
|
+
value: create(ExaFetchRequestResponse_RejectedSchema, {
|
|
309
|
+
reason: "Native Cursor Exa fetch is not available in this environment. Use the provided MCP tools `websearch` and `webfetch` instead.",
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
sendInteractionResponse(query.id, "exaFetchRequestResponse", response, sendFrame);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (queryCase === "createPlanRequestQuery") {
|
|
317
|
+
const response = create(CreatePlanRequestResponseSchema, {
|
|
318
|
+
result: create(CreatePlanResultSchema, {
|
|
319
|
+
planUri: "",
|
|
320
|
+
result: {
|
|
321
|
+
case: "error",
|
|
322
|
+
value: create(CreatePlanErrorSchema, {
|
|
323
|
+
error: "Native Cursor plan creation is not available in this environment. Use the provided MCP planning tools instead.",
|
|
324
|
+
}),
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
sendInteractionResponse(query.id, "createPlanRequestResponse", response, sendFrame);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
onUnsupportedMessage?.({
|
|
332
|
+
category: "interactionQuery",
|
|
333
|
+
caseName: queryCase ?? "undefined",
|
|
334
|
+
});
|
|
170
335
|
}
|
|
171
336
|
/** Send a KV client response back to Cursor. */
|
|
172
337
|
function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
@@ -179,6 +344,16 @@ function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
|
179
344
|
});
|
|
180
345
|
sendFrame(toBinary(AgentClientMessageSchema, clientMsg));
|
|
181
346
|
}
|
|
347
|
+
function sendInteractionResponse(queryId, messageCase, value, sendFrame) {
|
|
348
|
+
const response = create(InteractionResponseSchema, {
|
|
349
|
+
id: queryId,
|
|
350
|
+
result: { case: messageCase, value: value },
|
|
351
|
+
});
|
|
352
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
353
|
+
message: { case: "interactionResponse", value: response },
|
|
354
|
+
});
|
|
355
|
+
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
356
|
+
}
|
|
182
357
|
function handleKvMessage(kvMsg, blobStore, sendFrame) {
|
|
183
358
|
const kvCase = kvMsg.message.case;
|
|
184
359
|
if (kvCase === "getBlobArgs") {
|
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.4463bb589222",
|
|
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",
|