@playwo/opencode-cursor-oauth 0.0.0-dev.4258a6733133 → 0.0.0-dev.6338d5591e37
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/proxy/bridge-session.js +1 -3
- package/dist/proxy/bridge-streaming.js +0 -1
- package/dist/proxy/stream-dispatch.js +149 -14
- 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;
|
|
@@ -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
|
}
|
|
@@ -103,7 +103,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
103
103
|
sendSSE(makeChunk({ content }));
|
|
104
104
|
}
|
|
105
105
|
}, (exec) => {
|
|
106
|
-
state.pendingExecs.push(exec);
|
|
107
106
|
mcpExecReceived = true;
|
|
108
107
|
const flushed = tagFilter.flush();
|
|
109
108
|
if (flushed.reasoning)
|
|
@@ -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,6 +128,51 @@ 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
|
+
function getPendingExecKey(exec) {
|
|
132
|
+
return exec.toolCallId || `${exec.toolName}:${exec.decodedArgs}`;
|
|
133
|
+
}
|
|
134
|
+
function replacePendingExec(state, exec) {
|
|
135
|
+
const execKey = getPendingExecKey(exec);
|
|
136
|
+
const existingIndex = state.pendingExecs.findIndex((candidate) => getPendingExecKey(candidate) === execKey);
|
|
137
|
+
if (existingIndex >= 0) {
|
|
138
|
+
state.pendingExecs[existingIndex] = exec;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
state.pendingExecs.push(exec);
|
|
142
|
+
}
|
|
143
|
+
function hasUsableDecodedArgs(decodedArgs) {
|
|
144
|
+
const trimmed = decodedArgs.trim();
|
|
145
|
+
return trimmed !== "" && trimmed !== "{}";
|
|
146
|
+
}
|
|
147
|
+
function mergePendingExec(existing, incoming) {
|
|
148
|
+
const incomingHasExecMetadata = incoming.execMsgId !== 0;
|
|
149
|
+
const existingHasExecMetadata = existing.execMsgId !== 0;
|
|
150
|
+
return {
|
|
151
|
+
execId: incomingHasExecMetadata || !existing.execId ? incoming.execId : existing.execId,
|
|
152
|
+
execMsgId: incomingHasExecMetadata || !existingHasExecMetadata
|
|
153
|
+
? incoming.execMsgId
|
|
154
|
+
: existing.execMsgId,
|
|
155
|
+
toolCallId: existing.toolCallId || incoming.toolCallId,
|
|
156
|
+
toolName: incoming.toolName && incoming.toolName !== "unknown_mcp_tool"
|
|
157
|
+
? incoming.toolName
|
|
158
|
+
: existing.toolName,
|
|
159
|
+
decodedArgs: hasUsableDecodedArgs(incoming.decodedArgs)
|
|
160
|
+
? incoming.decodedArgs
|
|
161
|
+
: existing.decodedArgs,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function emitPendingExec(exec, state, onMcpExec) {
|
|
165
|
+
const execKey = getPendingExecKey(exec);
|
|
166
|
+
const existing = state.pendingExecs.find((candidate) => getPendingExecKey(candidate) === execKey);
|
|
167
|
+
const nextExec = existing ? mergePendingExec(existing, exec) : exec;
|
|
168
|
+
if (state.emittedToolCallIds.has(execKey)) {
|
|
169
|
+
replacePendingExec(state, nextExec);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
state.emittedToolCallIds.add(execKey);
|
|
173
|
+
replacePendingExec(state, nextExec);
|
|
174
|
+
onMcpExec(nextExec);
|
|
175
|
+
}
|
|
131
176
|
export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
|
|
132
177
|
const msgCase = msg.message.case;
|
|
133
178
|
if (msgCase === "interactionUpdate") {
|
|
@@ -137,7 +182,7 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
|
|
|
137
182
|
handleKvMessage(msg.message.value, blobStore, sendFrame);
|
|
138
183
|
}
|
|
139
184
|
else if (msgCase === "execServerMessage") {
|
|
140
|
-
handleExecMessage(msg.message.value, mcpTools, sendFrame, onMcpExec, onUnhandledExec);
|
|
185
|
+
handleExecMessage(msg.message.value, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec);
|
|
141
186
|
}
|
|
142
187
|
else if (msgCase === "execServerControlMessage") {
|
|
143
188
|
onUnsupportedMessage?.({
|
|
@@ -146,10 +191,7 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
|
|
|
146
191
|
});
|
|
147
192
|
}
|
|
148
193
|
else if (msgCase === "interactionQuery") {
|
|
149
|
-
onUnsupportedMessage
|
|
150
|
-
category: "interactionQuery",
|
|
151
|
-
caseName: msg.message.value.query.case ?? "undefined",
|
|
152
|
-
});
|
|
194
|
+
handleInteractionQuery(msg.message.value, sendFrame, onUnsupportedMessage);
|
|
153
195
|
}
|
|
154
196
|
else if (msgCase === "conversationCheckpointUpdate") {
|
|
155
197
|
const stateStructure = msg.message.value;
|
|
@@ -185,13 +227,14 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
|
|
|
185
227
|
else if (updateCase === "partialToolCall") {
|
|
186
228
|
const partial = update.message.value;
|
|
187
229
|
if (partial.callId && partial.argsTextDelta) {
|
|
188
|
-
state.interactionToolArgsText.
|
|
230
|
+
const existing = state.interactionToolArgsText.get(partial.callId) ?? "";
|
|
231
|
+
state.interactionToolArgsText.set(partial.callId, `${existing}${partial.argsTextDelta}`);
|
|
189
232
|
}
|
|
190
233
|
}
|
|
191
234
|
else if (updateCase === "toolCallCompleted") {
|
|
192
235
|
const exec = decodeInteractionToolCall(update.message.value, state);
|
|
193
236
|
if (exec)
|
|
194
|
-
|
|
237
|
+
emitPendingExec(exec, state, onMcpExec);
|
|
195
238
|
}
|
|
196
239
|
else if (updateCase === "turnEnded") {
|
|
197
240
|
onTurnEnded?.();
|
|
@@ -228,8 +271,6 @@ function decodeInteractionToolCall(update, state) {
|
|
|
228
271
|
if (!mcpArgs)
|
|
229
272
|
return null;
|
|
230
273
|
const toolCallId = mcpArgs.toolCallId || callId || crypto.randomUUID();
|
|
231
|
-
if (state.emittedToolCallIds.has(toolCallId))
|
|
232
|
-
return null;
|
|
233
274
|
const decodedMap = decodeMcpArgsMap(mcpArgs.args ?? {});
|
|
234
275
|
const partialArgsText = callId
|
|
235
276
|
? state.interactionToolArgsText.get(callId)?.trim()
|
|
@@ -241,7 +282,6 @@ function decodeInteractionToolCall(update, state) {
|
|
|
241
282
|
else if (partialArgsText) {
|
|
242
283
|
decodedArgs = partialArgsText;
|
|
243
284
|
}
|
|
244
|
-
state.emittedToolCallIds.add(toolCallId);
|
|
245
285
|
if (callId)
|
|
246
286
|
state.interactionToolArgsText.delete(callId);
|
|
247
287
|
return {
|
|
@@ -252,6 +292,90 @@ function decodeInteractionToolCall(update, state) {
|
|
|
252
292
|
decodedArgs,
|
|
253
293
|
};
|
|
254
294
|
}
|
|
295
|
+
function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
|
|
296
|
+
const queryCase = query.query.case;
|
|
297
|
+
if (queryCase === "webSearchRequestQuery") {
|
|
298
|
+
const response = create(WebSearchRequestResponseSchema, {
|
|
299
|
+
result: {
|
|
300
|
+
case: "rejected",
|
|
301
|
+
value: create(WebSearchRequestResponse_RejectedSchema, {
|
|
302
|
+
reason: "Native Cursor web search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
sendInteractionResponse(query.id, "webSearchRequestResponse", response, sendFrame);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (queryCase === "askQuestionInteractionQuery") {
|
|
310
|
+
const response = create(AskQuestionInteractionResponseSchema, {
|
|
311
|
+
result: create(AskQuestionResultSchema, {
|
|
312
|
+
result: {
|
|
313
|
+
case: "rejected",
|
|
314
|
+
value: create(AskQuestionRejectedSchema, {
|
|
315
|
+
reason: "Native Cursor question prompts are not available in this environment. Use the provided MCP tool `question` instead.",
|
|
316
|
+
}),
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
sendInteractionResponse(query.id, "askQuestionInteractionResponse", response, sendFrame);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (queryCase === "switchModeRequestQuery") {
|
|
324
|
+
const response = create(SwitchModeRequestResponseSchema, {
|
|
325
|
+
result: {
|
|
326
|
+
case: "rejected",
|
|
327
|
+
value: create(SwitchModeRequestResponse_RejectedSchema, {
|
|
328
|
+
reason: "Cursor mode switching is not available in this environment. Continue using the current agent and the provided MCP tools.",
|
|
329
|
+
}),
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
sendInteractionResponse(query.id, "switchModeRequestResponse", response, sendFrame);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (queryCase === "exaSearchRequestQuery") {
|
|
336
|
+
const response = create(ExaSearchRequestResponseSchema, {
|
|
337
|
+
result: {
|
|
338
|
+
case: "rejected",
|
|
339
|
+
value: create(ExaSearchRequestResponse_RejectedSchema, {
|
|
340
|
+
reason: "Native Cursor Exa search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
sendInteractionResponse(query.id, "exaSearchRequestResponse", response, sendFrame);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (queryCase === "exaFetchRequestQuery") {
|
|
348
|
+
const response = create(ExaFetchRequestResponseSchema, {
|
|
349
|
+
result: {
|
|
350
|
+
case: "rejected",
|
|
351
|
+
value: create(ExaFetchRequestResponse_RejectedSchema, {
|
|
352
|
+
reason: "Native Cursor Exa fetch is not available in this environment. Use the provided MCP tools `websearch` and `webfetch` instead.",
|
|
353
|
+
}),
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
sendInteractionResponse(query.id, "exaFetchRequestResponse", response, sendFrame);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (queryCase === "createPlanRequestQuery") {
|
|
360
|
+
const response = create(CreatePlanRequestResponseSchema, {
|
|
361
|
+
result: create(CreatePlanResultSchema, {
|
|
362
|
+
planUri: "",
|
|
363
|
+
result: {
|
|
364
|
+
case: "error",
|
|
365
|
+
value: create(CreatePlanErrorSchema, {
|
|
366
|
+
error: "Native Cursor plan creation is not available in this environment. Use the provided MCP planning tools instead.",
|
|
367
|
+
}),
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
sendInteractionResponse(query.id, "createPlanRequestResponse", response, sendFrame);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
onUnsupportedMessage?.({
|
|
375
|
+
category: "interactionQuery",
|
|
376
|
+
caseName: queryCase ?? "undefined",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
255
379
|
/** Send a KV client response back to Cursor. */
|
|
256
380
|
function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
257
381
|
const response = create(KvClientMessageSchema, {
|
|
@@ -263,6 +387,16 @@ function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
|
263
387
|
});
|
|
264
388
|
sendFrame(toBinary(AgentClientMessageSchema, clientMsg));
|
|
265
389
|
}
|
|
390
|
+
function sendInteractionResponse(queryId, messageCase, value, sendFrame) {
|
|
391
|
+
const response = create(InteractionResponseSchema, {
|
|
392
|
+
id: queryId,
|
|
393
|
+
result: { case: messageCase, value: value },
|
|
394
|
+
});
|
|
395
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
396
|
+
message: { case: "interactionResponse", value: response },
|
|
397
|
+
});
|
|
398
|
+
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
399
|
+
}
|
|
266
400
|
function handleKvMessage(kvMsg, blobStore, sendFrame) {
|
|
267
401
|
const kvCase = kvMsg.message.case;
|
|
268
402
|
if (kvCase === "getBlobArgs") {
|
|
@@ -283,7 +417,7 @@ function handleKvMessage(kvMsg, blobStore, sendFrame) {
|
|
|
283
417
|
sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame);
|
|
284
418
|
}
|
|
285
419
|
}
|
|
286
|
-
function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledExec) {
|
|
420
|
+
function handleExecMessage(execMsg, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec) {
|
|
287
421
|
const execCase = execMsg.message.case;
|
|
288
422
|
if (execCase === "requestContextArgs") {
|
|
289
423
|
const requestContext = create(RequestContextSchema, {
|
|
@@ -308,13 +442,14 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec, onUnhandledE
|
|
|
308
442
|
if (execCase === "mcpArgs") {
|
|
309
443
|
const mcpArgs = execMsg.message.value;
|
|
310
444
|
const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
|
|
311
|
-
|
|
445
|
+
const exec = {
|
|
312
446
|
execId: execMsg.execId,
|
|
313
447
|
execMsgId: execMsg.id,
|
|
314
448
|
toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
|
|
315
449
|
toolName: mcpArgs.toolName || mcpArgs.name,
|
|
316
450
|
decodedArgs: JSON.stringify(decoded),
|
|
317
|
-
}
|
|
451
|
+
};
|
|
452
|
+
emitPendingExec(exec, state, onMcpExec);
|
|
318
453
|
return;
|
|
319
454
|
}
|
|
320
455
|
// --- Reject native Cursor tools ---
|
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.6338d5591e37",
|
|
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",
|