@playwo/opencode-cursor-oauth 0.0.0-dev.4258a6733133 → 0.0.0-dev.628837adf8c9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cursor/bidi-session.d.ts +1 -2
- package/dist/cursor/bidi-session.js +153 -138
- package/dist/cursor/index.d.ts +1 -1
- package/dist/cursor/index.js +1 -1
- package/dist/cursor/unary-rpc.d.ts +0 -1
- package/dist/cursor/unary-rpc.js +2 -59
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- package/dist/proxy/bridge-non-streaming.js +1 -3
- package/dist/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.d.ts +1 -1
- package/dist/proxy/bridge-streaming.js +95 -10
- package/dist/proxy/chat-completion.js +40 -1
- package/dist/proxy/cursor-request.js +28 -15
- package/dist/proxy/stream-dispatch.d.ts +2 -2
- package/dist/proxy/stream-dispatch.js +204 -56
- package/dist/proxy/stream-state.d.ts +0 -2
- package/dist/proxy/types.d.ts +6 -1
- package/package.json +1 -2
|
@@ -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/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
|
}
|
|
@@ -38,14 +38,12 @@ 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(),
|
|
43
41
|
};
|
|
44
42
|
const tagFilter = createThinkingTagFilter();
|
|
45
43
|
bridge.onData(createConnectFrameParser((messageBytes) => {
|
|
46
44
|
try {
|
|
47
45
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
48
|
-
processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
46
|
+
processServerMessage(serverMessage, payload.blobStore, payload.rules, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
49
47
|
if (isThinking)
|
|
50
48
|
return;
|
|
51
49
|
const { content } = tagFilter.process(text);
|
|
@@ -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
|
}
|
|
@@ -2,4 +2,4 @@ import { type ToolResultInfo } from "../openai/messages";
|
|
|
2
2
|
import type { ConversationRequestMetadata } from "./conversation-meta";
|
|
3
3
|
import type { ActiveBridge, CursorRequestPayload } from "./types";
|
|
4
4
|
export declare function handleStreamingResponse(payload: CursorRequestPayload, accessToken: string, modelId: string, bridgeKey: string, convKey: string, metadata: ConversationRequestMetadata): Promise<Response>;
|
|
5
|
-
export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Response
|
|
5
|
+
export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Promise<Response>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
2
|
import { AgentClientMessageSchema, AgentServerMessageSchema, ExecClientMessageSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolResultContentItemSchema, } from "../proto/agent_pb";
|
|
3
|
-
import { errorDetails, logPluginError, logPluginWarn } from "../logger";
|
|
3
|
+
import { errorDetails, logPluginError, logPluginInfo, logPluginWarn, } from "../logger";
|
|
4
4
|
import { formatToolCallSummary, formatToolResultSummary, } from "../openai/messages";
|
|
5
5
|
import { activeBridges, updateStoredConversationAfterCompletion, } from "./conversation-state";
|
|
6
6
|
import { startBridge } from "./bridge-session";
|
|
@@ -8,7 +8,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
10
|
const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
11
|
-
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
11
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, rules, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
12
12
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
13
13
|
const created = Math.floor(Date.now() / 1000);
|
|
14
14
|
let keepaliveTimer;
|
|
@@ -27,8 +27,6 @@ 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(),
|
|
32
30
|
};
|
|
33
31
|
const tagFilter = createThinkingTagFilter();
|
|
34
32
|
let assistantText = metadata.assistantSeedText ?? "";
|
|
@@ -90,7 +88,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
90
88
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
91
89
|
try {
|
|
92
90
|
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
93
|
-
processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
91
|
+
processServerMessage(serverMessage, blobStore, rules, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
|
|
94
92
|
if (isThinking) {
|
|
95
93
|
sendSSE(makeChunk({ reasoning_content: text }));
|
|
96
94
|
return;
|
|
@@ -103,7 +101,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
103
101
|
sendSSE(makeChunk({ content }));
|
|
104
102
|
}
|
|
105
103
|
}, (exec) => {
|
|
106
|
-
state.pendingExecs.
|
|
104
|
+
const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
|
|
105
|
+
if (existingIndex >= 0) {
|
|
106
|
+
state.pendingExecs[existingIndex] = exec;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
state.pendingExecs.push(exec);
|
|
110
|
+
}
|
|
107
111
|
mcpExecReceived = true;
|
|
108
112
|
const flushed = tagFilter.flush();
|
|
109
113
|
if (flushed.reasoning)
|
|
@@ -142,6 +146,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
142
146
|
bridge,
|
|
143
147
|
heartbeatTimer,
|
|
144
148
|
blobStore,
|
|
149
|
+
rules,
|
|
145
150
|
mcpTools,
|
|
146
151
|
pendingExecs: state.pendingExecs,
|
|
147
152
|
modelId,
|
|
@@ -206,8 +211,23 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
206
211
|
stopKeepalive();
|
|
207
212
|
}
|
|
208
213
|
}, SSE_KEEPALIVE_INTERVAL_MS);
|
|
214
|
+
logPluginInfo("Opened Cursor streaming bridge", {
|
|
215
|
+
modelId,
|
|
216
|
+
bridgeKey,
|
|
217
|
+
convKey,
|
|
218
|
+
mcpToolCount: mcpTools.length,
|
|
219
|
+
ruleCount: rules.length,
|
|
220
|
+
});
|
|
209
221
|
bridge.onData(processChunk);
|
|
210
222
|
bridge.onClose((code) => {
|
|
223
|
+
logPluginInfo("Cursor streaming bridge closed", {
|
|
224
|
+
modelId,
|
|
225
|
+
bridgeKey,
|
|
226
|
+
convKey,
|
|
227
|
+
code,
|
|
228
|
+
mcpExecReceived,
|
|
229
|
+
hadEndStreamError: Boolean(endStreamError),
|
|
230
|
+
});
|
|
211
231
|
clearInterval(heartbeatTimer);
|
|
212
232
|
stopKeepalive();
|
|
213
233
|
syncStoredBlobStore(convKey, blobStore);
|
|
@@ -257,11 +277,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
257
277
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
258
278
|
}
|
|
259
279
|
export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
280
|
+
logPluginInfo("Starting Cursor streaming response", {
|
|
281
|
+
modelId,
|
|
282
|
+
bridgeKey,
|
|
283
|
+
convKey,
|
|
284
|
+
mcpToolCount: payload.mcpTools.length,
|
|
285
|
+
});
|
|
260
286
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
261
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
287
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.rules, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
288
|
+
}
|
|
289
|
+
async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
|
|
290
|
+
const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
|
|
291
|
+
const deadline = Date.now() + timeoutMs;
|
|
292
|
+
while (Date.now() < deadline) {
|
|
293
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
294
|
+
if (unresolved.length === 0) {
|
|
295
|
+
return unresolved;
|
|
296
|
+
}
|
|
297
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
298
|
+
}
|
|
299
|
+
const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
|
|
300
|
+
if (unresolved.length > 0) {
|
|
301
|
+
logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
|
|
302
|
+
bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
|
|
303
|
+
modelId: active.modelId,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return unresolved;
|
|
262
307
|
}
|
|
263
|
-
export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
264
|
-
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
308
|
+
export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
309
|
+
const { bridge, heartbeatTimer, blobStore, rules, mcpTools, pendingExecs, modelId, metadata, } = active;
|
|
265
310
|
const resumeMetadata = {
|
|
266
311
|
...metadata,
|
|
267
312
|
assistantSeedText: [
|
|
@@ -271,6 +316,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
271
316
|
.filter(Boolean)
|
|
272
317
|
.join("\n\n"),
|
|
273
318
|
};
|
|
319
|
+
logPluginInfo("Preparing Cursor tool-result resume", {
|
|
320
|
+
bridgeKey,
|
|
321
|
+
convKey,
|
|
322
|
+
modelId,
|
|
323
|
+
toolResults,
|
|
324
|
+
pendingExecs,
|
|
325
|
+
});
|
|
326
|
+
const unresolved = await waitForResolvablePendingExecs(active, toolResults);
|
|
327
|
+
logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
|
|
328
|
+
bridgeKey,
|
|
329
|
+
convKey,
|
|
330
|
+
modelId,
|
|
331
|
+
toolResults,
|
|
332
|
+
pendingExecs,
|
|
333
|
+
unresolvedPendingExecs: unresolved,
|
|
334
|
+
});
|
|
335
|
+
if (unresolved.length > 0) {
|
|
336
|
+
clearInterval(heartbeatTimer);
|
|
337
|
+
bridge.end();
|
|
338
|
+
return new Response(JSON.stringify({
|
|
339
|
+
error: {
|
|
340
|
+
message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
|
|
341
|
+
type: "invalid_request_error",
|
|
342
|
+
code: "cursor_missing_exec_metadata",
|
|
343
|
+
},
|
|
344
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
345
|
+
}
|
|
274
346
|
for (const exec of pendingExecs) {
|
|
275
347
|
const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
|
|
276
348
|
const mcpResult = result
|
|
@@ -311,7 +383,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
|
|
|
311
383
|
const clientMessage = create(AgentClientMessageSchema, {
|
|
312
384
|
message: { case: "execClientMessage", value: execClientMessage },
|
|
313
385
|
});
|
|
386
|
+
logPluginInfo("Sending Cursor tool-result resume message", {
|
|
387
|
+
bridgeKey,
|
|
388
|
+
convKey,
|
|
389
|
+
modelId,
|
|
390
|
+
toolCallId: exec.toolCallId,
|
|
391
|
+
toolName: exec.toolName,
|
|
392
|
+
source: exec.source,
|
|
393
|
+
execId: exec.execId,
|
|
394
|
+
execMsgId: exec.execMsgId,
|
|
395
|
+
cursorCallId: exec.cursorCallId,
|
|
396
|
+
modelCallId: exec.modelCallId,
|
|
397
|
+
matchedToolResult: result,
|
|
398
|
+
});
|
|
314
399
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
315
400
|
}
|
|
316
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
401
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, rules, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
317
402
|
}
|
|
@@ -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, CursorRuleSchema, CursorRuleTypeAgentFetchedSchema, CursorRuleTypeSchema, 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 rules = buildCursorRules(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, rules);
|
|
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 rules = buildCursorRules(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, rules);
|
|
83
75
|
}
|
|
84
|
-
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
|
|
76
|
+
function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, rules) {
|
|
85
77
|
const modelDetails = create(ModelDetailsSchema, {
|
|
86
78
|
modelId,
|
|
87
79
|
displayModelId: modelId,
|
|
@@ -99,6 +91,27 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
|
|
|
99
91
|
return {
|
|
100
92
|
requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
|
|
101
93
|
blobStore,
|
|
94
|
+
rules,
|
|
102
95
|
mcpTools: [],
|
|
103
96
|
};
|
|
104
97
|
}
|
|
98
|
+
function buildCursorRules(systemPrompt) {
|
|
99
|
+
const content = systemPrompt.trim();
|
|
100
|
+
if (!content)
|
|
101
|
+
return [];
|
|
102
|
+
return [
|
|
103
|
+
create(CursorRuleSchema, {
|
|
104
|
+
fullPath: "/opencode/system-prompt.md",
|
|
105
|
+
content,
|
|
106
|
+
type: create(CursorRuleTypeSchema, {
|
|
107
|
+
type: {
|
|
108
|
+
case: "agentFetched",
|
|
109
|
+
value: create(CursorRuleTypeAgentFetchedSchema, {
|
|
110
|
+
description: "OpenCode system prompt",
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
source: 0,
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentServerMessage, type McpToolDefinition } from "../proto/agent_pb";
|
|
1
|
+
import { type AgentServerMessage, type CursorRule, type McpToolDefinition } from "../proto/agent_pb";
|
|
2
2
|
import type { CursorSession } from "../cursor/bidi-session";
|
|
3
3
|
import type { StreamState } from "./stream-state";
|
|
4
4
|
import type { PendingExec } from "./types";
|
|
@@ -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>, rules: CursorRule[], 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, 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
|
-
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, rules, 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, rules, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec);
|
|
141
141
|
}
|
|
142
142
|
else if (msgCase === "execServerControlMessage") {
|
|
143
143
|
onUnsupportedMessage?.({
|
|
@@ -146,10 +146,7 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
else if (msgCase === "interactionQuery") {
|
|
149
|
-
onUnsupportedMessage
|
|
150
|
-
category: "interactionQuery",
|
|
151
|
-
caseName: msg.message.value.query.case ?? "undefined",
|
|
152
|
-
});
|
|
149
|
+
handleInteractionQuery(msg.message.value, sendFrame, onUnsupportedMessage);
|
|
153
150
|
}
|
|
154
151
|
else if (msgCase === "conversationCheckpointUpdate") {
|
|
155
152
|
const stateStructure = msg.message.value;
|
|
@@ -167,8 +164,19 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
|
|
|
167
164
|
});
|
|
168
165
|
}
|
|
169
166
|
}
|
|
170
|
-
function handleInteractionUpdate(update, state, onText,
|
|
167
|
+
function handleInteractionUpdate(update, state, onText, onTurnEnded, onUnsupportedMessage) {
|
|
171
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
|
+
}
|
|
172
180
|
if (updateCase === "textDelta") {
|
|
173
181
|
const delta = update.message.value.text || "";
|
|
174
182
|
if (delta)
|
|
@@ -183,15 +191,19 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
|
|
|
183
191
|
state.outputTokens += update.message.value.tokens ?? 0;
|
|
184
192
|
}
|
|
185
193
|
else if (updateCase === "partialToolCall") {
|
|
186
|
-
|
|
187
|
-
if (partial.callId && partial.argsTextDelta) {
|
|
188
|
-
state.interactionToolArgsText.set(partial.callId, partial.argsTextDelta);
|
|
189
|
-
}
|
|
194
|
+
return;
|
|
190
195
|
}
|
|
191
196
|
else if (updateCase === "toolCallCompleted") {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
|
|
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
|
+
}
|
|
195
207
|
}
|
|
196
208
|
else if (updateCase === "turnEnded") {
|
|
197
209
|
onTurnEnded?.();
|
|
@@ -214,43 +226,92 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
|
|
|
214
226
|
caseName: updateCase ?? "undefined",
|
|
215
227
|
});
|
|
216
228
|
}
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// calls may still appear here on some models, so we surface those, but we
|
|
220
|
-
// do not abort the bridge for native Cursor tool-call progress events.
|
|
229
|
+
// Interaction tool-call updates are informational only. Resumable MCP tool
|
|
230
|
+
// execution comes from execServerMessage.mcpArgs.
|
|
221
231
|
}
|
|
222
|
-
function
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
232
|
+
function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
|
|
233
|
+
const queryCase = query.query.case;
|
|
234
|
+
if (queryCase === "webSearchRequestQuery") {
|
|
235
|
+
const response = create(WebSearchRequestResponseSchema, {
|
|
236
|
+
result: {
|
|
237
|
+
case: "rejected",
|
|
238
|
+
value: create(WebSearchRequestResponse_RejectedSchema, {
|
|
239
|
+
reason: "Native Cursor web search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
sendInteractionResponse(query.id, "webSearchRequestResponse", response, sendFrame);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (queryCase === "askQuestionInteractionQuery") {
|
|
247
|
+
const response = create(AskQuestionInteractionResponseSchema, {
|
|
248
|
+
result: create(AskQuestionResultSchema, {
|
|
249
|
+
result: {
|
|
250
|
+
case: "rejected",
|
|
251
|
+
value: create(AskQuestionRejectedSchema, {
|
|
252
|
+
reason: "Native Cursor question prompts are not available in this environment. Use the provided MCP tool `question` instead.",
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
sendInteractionResponse(query.id, "askQuestionInteractionResponse", response, sendFrame);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (queryCase === "switchModeRequestQuery") {
|
|
261
|
+
const response = create(SwitchModeRequestResponseSchema, {
|
|
262
|
+
result: {
|
|
263
|
+
case: "rejected",
|
|
264
|
+
value: create(SwitchModeRequestResponse_RejectedSchema, {
|
|
265
|
+
reason: "Cursor mode switching is not available in this environment. Continue using the current agent and the provided MCP tools.",
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
sendInteractionResponse(query.id, "switchModeRequestResponse", response, sendFrame);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (queryCase === "exaSearchRequestQuery") {
|
|
273
|
+
const response = create(ExaSearchRequestResponseSchema, {
|
|
274
|
+
result: {
|
|
275
|
+
case: "rejected",
|
|
276
|
+
value: create(ExaSearchRequestResponse_RejectedSchema, {
|
|
277
|
+
reason: "Native Cursor Exa search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
sendInteractionResponse(query.id, "exaSearchRequestResponse", response, sendFrame);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (queryCase === "exaFetchRequestQuery") {
|
|
285
|
+
const response = create(ExaFetchRequestResponseSchema, {
|
|
286
|
+
result: {
|
|
287
|
+
case: "rejected",
|
|
288
|
+
value: create(ExaFetchRequestResponse_RejectedSchema, {
|
|
289
|
+
reason: "Native Cursor Exa fetch is not available in this environment. Use the provided MCP tools `websearch` and `webfetch` instead.",
|
|
290
|
+
}),
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
sendInteractionResponse(query.id, "exaFetchRequestResponse", response, sendFrame);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (queryCase === "createPlanRequestQuery") {
|
|
297
|
+
const response = create(CreatePlanRequestResponseSchema, {
|
|
298
|
+
result: create(CreatePlanResultSchema, {
|
|
299
|
+
planUri: "",
|
|
300
|
+
result: {
|
|
301
|
+
case: "error",
|
|
302
|
+
value: create(CreatePlanErrorSchema, {
|
|
303
|
+
error: "Native Cursor plan creation is not available in this environment. Use the provided MCP planning tools instead.",
|
|
304
|
+
}),
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
sendInteractionResponse(query.id, "createPlanRequestResponse", response, sendFrame);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
onUnsupportedMessage?.({
|
|
312
|
+
category: "interactionQuery",
|
|
313
|
+
caseName: queryCase ?? "undefined",
|
|
314
|
+
});
|
|
254
315
|
}
|
|
255
316
|
/** Send a KV client response back to Cursor. */
|
|
256
317
|
function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
@@ -263,6 +324,16 @@ function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
|
263
324
|
});
|
|
264
325
|
sendFrame(toBinary(AgentClientMessageSchema, clientMsg));
|
|
265
326
|
}
|
|
327
|
+
function sendInteractionResponse(queryId, messageCase, value, sendFrame) {
|
|
328
|
+
const response = create(InteractionResponseSchema, {
|
|
329
|
+
id: queryId,
|
|
330
|
+
result: { case: messageCase, value: value },
|
|
331
|
+
});
|
|
332
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
333
|
+
message: { case: "interactionResponse", value: response },
|
|
334
|
+
});
|
|
335
|
+
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
336
|
+
}
|
|
266
337
|
function handleKvMessage(kvMsg, blobStore, sendFrame) {
|
|
267
338
|
const kvCase = kvMsg.message.case;
|
|
268
339
|
if (kvCase === "getBlobArgs") {
|
|
@@ -283,11 +354,21 @@ function handleKvMessage(kvMsg, blobStore, sendFrame) {
|
|
|
283
354
|
sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame);
|
|
284
355
|
}
|
|
285
356
|
}
|
|
286
|
-
function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledExec) {
|
|
357
|
+
function handleExecMessage(execMsg, rules, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec) {
|
|
287
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
|
+
});
|
|
288
364
|
if (execCase === "requestContextArgs") {
|
|
365
|
+
logPluginInfo("Responding to Cursor requestContextArgs", {
|
|
366
|
+
execId: execMsg.execId,
|
|
367
|
+
execMsgId: execMsg.id,
|
|
368
|
+
mcpToolCount: mcpTools.length,
|
|
369
|
+
});
|
|
289
370
|
const requestContext = create(RequestContextSchema, {
|
|
290
|
-
rules
|
|
371
|
+
rules,
|
|
291
372
|
repositoryInfo: [],
|
|
292
373
|
tools: mcpTools,
|
|
293
374
|
gitRepos: [],
|
|
@@ -308,13 +389,23 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
308
389
|
if (execCase === "mcpArgs") {
|
|
309
390
|
const mcpArgs = execMsg.message.value;
|
|
310
391
|
const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
|
|
311
|
-
|
|
392
|
+
const exec = {
|
|
312
393
|
execId: execMsg.execId,
|
|
313
394
|
execMsgId: execMsg.id,
|
|
314
395
|
toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
|
|
315
396
|
toolName: mcpArgs.toolName || mcpArgs.name,
|
|
316
397
|
decodedArgs: JSON.stringify(decoded),
|
|
398
|
+
source: "exec",
|
|
399
|
+
};
|
|
400
|
+
logPluginInfo("Received Cursor exec MCP tool metadata", {
|
|
401
|
+
toolCallId: exec.toolCallId,
|
|
402
|
+
toolName: exec.toolName,
|
|
403
|
+
source: exec.source,
|
|
404
|
+
execId: exec.execId,
|
|
405
|
+
execMsgId: exec.execMsgId,
|
|
406
|
+
decodedArgs: exec.decodedArgs,
|
|
317
407
|
});
|
|
408
|
+
onMcpExec(exec);
|
|
318
409
|
return;
|
|
319
410
|
}
|
|
320
411
|
// --- Reject native Cursor tools ---
|
|
@@ -322,6 +413,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
322
413
|
// so it falls back to our MCP tools (registered via RequestContext).
|
|
323
414
|
const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
|
|
324
415
|
if (execCase === "readArgs") {
|
|
416
|
+
logPluginInfo("Rejecting native Cursor read tool in favor of MCP", {
|
|
417
|
+
execId: execMsg.execId,
|
|
418
|
+
execMsgId: execMsg.id,
|
|
419
|
+
path: execMsg.message.value.path,
|
|
420
|
+
});
|
|
325
421
|
const args = execMsg.message.value;
|
|
326
422
|
const result = create(ReadResultSchema, {
|
|
327
423
|
result: {
|
|
@@ -336,6 +432,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
336
432
|
return;
|
|
337
433
|
}
|
|
338
434
|
if (execCase === "lsArgs") {
|
|
435
|
+
logPluginInfo("Rejecting native Cursor ls tool in favor of MCP", {
|
|
436
|
+
execId: execMsg.execId,
|
|
437
|
+
execMsgId: execMsg.id,
|
|
438
|
+
path: execMsg.message.value.path,
|
|
439
|
+
});
|
|
339
440
|
const args = execMsg.message.value;
|
|
340
441
|
const result = create(LsResultSchema, {
|
|
341
442
|
result: {
|
|
@@ -350,6 +451,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
350
451
|
return;
|
|
351
452
|
}
|
|
352
453
|
if (execCase === "grepArgs") {
|
|
454
|
+
logPluginInfo("Rejecting native Cursor grep tool in favor of MCP", {
|
|
455
|
+
execId: execMsg.execId,
|
|
456
|
+
execMsgId: execMsg.id,
|
|
457
|
+
});
|
|
353
458
|
const result = create(GrepResultSchema, {
|
|
354
459
|
result: {
|
|
355
460
|
case: "error",
|
|
@@ -360,6 +465,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
360
465
|
return;
|
|
361
466
|
}
|
|
362
467
|
if (execCase === "writeArgs") {
|
|
468
|
+
logPluginInfo("Rejecting native Cursor write tool in favor of MCP", {
|
|
469
|
+
execId: execMsg.execId,
|
|
470
|
+
execMsgId: execMsg.id,
|
|
471
|
+
path: execMsg.message.value.path,
|
|
472
|
+
});
|
|
363
473
|
const args = execMsg.message.value;
|
|
364
474
|
const result = create(WriteResultSchema, {
|
|
365
475
|
result: {
|
|
@@ -374,6 +484,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
374
484
|
return;
|
|
375
485
|
}
|
|
376
486
|
if (execCase === "deleteArgs") {
|
|
487
|
+
logPluginInfo("Rejecting native Cursor delete tool in favor of MCP", {
|
|
488
|
+
execId: execMsg.execId,
|
|
489
|
+
execMsgId: execMsg.id,
|
|
490
|
+
path: execMsg.message.value.path,
|
|
491
|
+
});
|
|
377
492
|
const args = execMsg.message.value;
|
|
378
493
|
const result = create(DeleteResultSchema, {
|
|
379
494
|
result: {
|
|
@@ -388,6 +503,13 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
388
503
|
return;
|
|
389
504
|
}
|
|
390
505
|
if (execCase === "shellArgs" || execCase === "shellStreamArgs") {
|
|
506
|
+
logPluginInfo("Rejecting native Cursor shell tool in favor of MCP", {
|
|
507
|
+
execId: execMsg.execId,
|
|
508
|
+
execMsgId: execMsg.id,
|
|
509
|
+
command: execMsg.message.value.command ?? "",
|
|
510
|
+
workingDirectory: execMsg.message.value.workingDirectory ?? "",
|
|
511
|
+
execCase,
|
|
512
|
+
});
|
|
391
513
|
const args = execMsg.message.value;
|
|
392
514
|
const result = create(ShellResultSchema, {
|
|
393
515
|
result: {
|
|
@@ -404,6 +526,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
404
526
|
return;
|
|
405
527
|
}
|
|
406
528
|
if (execCase === "backgroundShellSpawnArgs") {
|
|
529
|
+
logPluginInfo("Rejecting native Cursor background shell tool in favor of MCP", {
|
|
530
|
+
execId: execMsg.execId,
|
|
531
|
+
execMsgId: execMsg.id,
|
|
532
|
+
command: execMsg.message.value.command ?? "",
|
|
533
|
+
workingDirectory: execMsg.message.value.workingDirectory ?? "",
|
|
534
|
+
});
|
|
407
535
|
const args = execMsg.message.value;
|
|
408
536
|
const result = create(BackgroundShellSpawnResultSchema, {
|
|
409
537
|
result: {
|
|
@@ -420,6 +548,10 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
420
548
|
return;
|
|
421
549
|
}
|
|
422
550
|
if (execCase === "writeShellStdinArgs") {
|
|
551
|
+
logPluginInfo("Rejecting native Cursor shell stdin tool in favor of MCP", {
|
|
552
|
+
execId: execMsg.execId,
|
|
553
|
+
execMsgId: execMsg.id,
|
|
554
|
+
});
|
|
423
555
|
const result = create(WriteShellStdinResultSchema, {
|
|
424
556
|
result: {
|
|
425
557
|
case: "error",
|
|
@@ -430,6 +562,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
430
562
|
return;
|
|
431
563
|
}
|
|
432
564
|
if (execCase === "fetchArgs") {
|
|
565
|
+
logPluginInfo("Rejecting native Cursor fetch tool in favor of MCP", {
|
|
566
|
+
execId: execMsg.execId,
|
|
567
|
+
execMsgId: execMsg.id,
|
|
568
|
+
url: execMsg.message.value.url,
|
|
569
|
+
});
|
|
433
570
|
const args = execMsg.message.value;
|
|
434
571
|
const result = create(FetchResultSchema, {
|
|
435
572
|
result: {
|
|
@@ -444,6 +581,11 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
444
581
|
return;
|
|
445
582
|
}
|
|
446
583
|
if (execCase === "diagnosticsArgs") {
|
|
584
|
+
logPluginInfo("Rejecting native Cursor diagnostics tool in favor of MCP", {
|
|
585
|
+
execId: execMsg.execId,
|
|
586
|
+
execMsgId: execMsg.id,
|
|
587
|
+
path: execMsg.message.value.path,
|
|
588
|
+
});
|
|
447
589
|
const result = create(DiagnosticsResultSchema, {});
|
|
448
590
|
sendExecResult(execMsg, "diagnosticsResult", result, sendFrame);
|
|
449
591
|
return;
|
|
@@ -457,6 +599,12 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
457
599
|
};
|
|
458
600
|
const resultCase = miscCaseMap[execCase];
|
|
459
601
|
if (resultCase) {
|
|
602
|
+
logPluginInfo("Responding to miscellaneous Cursor exec message", {
|
|
603
|
+
execCase,
|
|
604
|
+
execId: execMsg.execId,
|
|
605
|
+
execMsgId: execMsg.id,
|
|
606
|
+
resultCase,
|
|
607
|
+
});
|
|
460
608
|
sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame);
|
|
461
609
|
return;
|
|
462
610
|
}
|
package/dist/proxy/types.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { CursorSession } from "../cursor/bidi-session";
|
|
2
|
+
import type { CursorRule, McpToolDefinition } from "../proto/agent_pb";
|
|
2
3
|
import type { ConversationRequestMetadata } from "./conversation-meta";
|
|
3
|
-
import type { McpToolDefinition } from "../proto/agent_pb";
|
|
4
4
|
export interface CursorRequestPayload {
|
|
5
5
|
requestBytes: Uint8Array;
|
|
6
6
|
blobStore: Map<string, Uint8Array>;
|
|
7
|
+
rules: CursorRule[];
|
|
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
|
+
rules: CursorRule[];
|
|
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.628837adf8c9",
|
|
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": {
|