@playwo/opencode-cursor-oauth 0.0.0-dev.0cb3e1517254 → 0.0.0-dev.0e8f5d6c8379
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/auth.js +1 -2
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/cursor/bidi-session.d.ts +12 -0
- package/dist/cursor/bidi-session.js +164 -0
- package/dist/cursor/config.d.ts +4 -0
- package/dist/cursor/config.js +4 -0
- package/dist/cursor/connect-framing.d.ts +10 -0
- package/dist/cursor/connect-framing.js +80 -0
- package/dist/cursor/headers.d.ts +6 -0
- package/dist/cursor/headers.js +16 -0
- package/dist/cursor/index.d.ts +5 -0
- package/dist/cursor/index.js +5 -0
- package/dist/cursor/unary-rpc.d.ts +12 -0
- package/dist/cursor/unary-rpc.js +124 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -306
- package/dist/logger.js +7 -2
- package/dist/models.js +1 -23
- package/dist/openai/index.d.ts +3 -0
- package/dist/openai/index.js +3 -0
- package/dist/openai/messages.d.ts +39 -0
- package/dist/openai/messages.js +228 -0
- package/dist/openai/tools.d.ts +7 -0
- package/dist/openai/tools.js +58 -0
- package/dist/openai/types.d.ts +41 -0
- package/dist/openai/types.js +1 -0
- package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
- package/dist/plugin/cursor-auth-plugin.js +139 -0
- package/dist/proto/agent_pb.js +637 -319
- package/dist/provider/index.d.ts +2 -0
- package/dist/provider/index.js +2 -0
- package/dist/provider/model-cost.d.ts +9 -0
- package/dist/provider/model-cost.js +206 -0
- package/dist/provider/models.d.ts +8 -0
- package/dist/provider/models.js +86 -0
- package/dist/proxy/bridge-non-streaming.d.ts +3 -0
- package/dist/proxy/bridge-non-streaming.js +119 -0
- package/dist/proxy/bridge-session.d.ts +5 -0
- package/dist/proxy/bridge-session.js +11 -0
- package/dist/proxy/bridge-streaming.d.ts +5 -0
- package/dist/proxy/bridge-streaming.js +323 -0
- package/dist/proxy/bridge.d.ts +3 -0
- package/dist/proxy/bridge.js +3 -0
- package/dist/proxy/chat-completion.d.ts +2 -0
- package/dist/proxy/chat-completion.js +114 -0
- package/dist/proxy/conversation-meta.d.ts +12 -0
- package/dist/proxy/conversation-meta.js +1 -0
- package/dist/proxy/conversation-state.d.ts +35 -0
- package/dist/proxy/conversation-state.js +95 -0
- package/dist/proxy/cursor-request.d.ts +6 -0
- package/dist/proxy/cursor-request.js +104 -0
- package/dist/proxy/index.d.ts +12 -0
- package/dist/proxy/index.js +12 -0
- package/dist/proxy/server.d.ts +6 -0
- package/dist/proxy/server.js +107 -0
- package/dist/proxy/sse.d.ts +5 -0
- package/dist/proxy/sse.js +5 -0
- package/dist/proxy/state-sync.d.ts +2 -0
- package/dist/proxy/state-sync.js +17 -0
- package/dist/proxy/stream-dispatch.d.ts +42 -0
- package/dist/proxy/stream-dispatch.js +549 -0
- package/dist/proxy/stream-state.d.ts +9 -0
- package/dist/proxy/stream-state.js +1 -0
- package/dist/proxy/title.d.ts +1 -0
- package/dist/proxy/title.js +103 -0
- package/dist/proxy/types.d.ts +27 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy.d.ts +2 -20
- package/dist/proxy.js +2 -1688
- package/package.json +1 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { create, toBinary } from "@bufbuild/protobuf";
|
|
2
|
+
import { AgentClientMessageSchema, AskQuestionInteractionResponseSchema, AskQuestionRejectedSchema, AskQuestionResultSchema, ClientHeartbeatSchema, ConversationStateStructureSchema, BackgroundShellSpawnResultSchema, CreatePlanErrorSchema, CreatePlanRequestResponseSchema, CreatePlanResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, ExaFetchRequestResponseSchema, ExaFetchRequestResponse_RejectedSchema, ExaSearchRequestResponseSchema, ExaSearchRequestResponse_RejectedSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, InteractionResponseSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpResultSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, SwitchModeRequestResponseSchema, SwitchModeRequestResponse_RejectedSchema, WebSearchRequestResponseSchema, WebSearchRequestResponse_RejectedSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, } from "../proto/agent_pb";
|
|
3
|
+
import { CONNECT_END_STREAM_FLAG } from "../cursor/config";
|
|
4
|
+
import { logPluginError, logPluginWarn } from "../logger";
|
|
5
|
+
import { decodeMcpArgsMap } from "../openai/tools";
|
|
6
|
+
export function parseConnectEndStream(data) {
|
|
7
|
+
try {
|
|
8
|
+
const payload = JSON.parse(new TextDecoder().decode(data));
|
|
9
|
+
const error = payload?.error;
|
|
10
|
+
if (error) {
|
|
11
|
+
const code = error.code ?? "unknown";
|
|
12
|
+
const message = error.message ?? "Unknown error";
|
|
13
|
+
return new Error(`Connect error ${code}: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return new Error("Failed to parse Connect end stream");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function makeHeartbeatBytes() {
|
|
22
|
+
const heartbeat = create(AgentClientMessageSchema, {
|
|
23
|
+
message: {
|
|
24
|
+
case: "clientHeartbeat",
|
|
25
|
+
value: create(ClientHeartbeatSchema, {}),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return toBinary(AgentClientMessageSchema, heartbeat);
|
|
29
|
+
}
|
|
30
|
+
export function scheduleBridgeEnd(bridge) {
|
|
31
|
+
queueMicrotask(() => {
|
|
32
|
+
if (bridge.alive)
|
|
33
|
+
bridge.end();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a stateful parser for Connect protocol frames.
|
|
38
|
+
* Handles buffering partial data across chunks.
|
|
39
|
+
*/
|
|
40
|
+
export function createConnectFrameParser(onMessage, onEndStream) {
|
|
41
|
+
let pending = Buffer.alloc(0);
|
|
42
|
+
return (incoming) => {
|
|
43
|
+
pending = Buffer.concat([pending, incoming]);
|
|
44
|
+
while (pending.length >= 5) {
|
|
45
|
+
const flags = pending[0];
|
|
46
|
+
const msgLen = pending.readUInt32BE(1);
|
|
47
|
+
if (pending.length < 5 + msgLen)
|
|
48
|
+
break;
|
|
49
|
+
const messageBytes = pending.subarray(5, 5 + msgLen);
|
|
50
|
+
pending = pending.subarray(5 + msgLen);
|
|
51
|
+
if (flags & CONNECT_END_STREAM_FLAG) {
|
|
52
|
+
onEndStream(messageBytes);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
onMessage(messageBytes);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const THINKING_TAG_NAMES = [
|
|
61
|
+
"think",
|
|
62
|
+
"thinking",
|
|
63
|
+
"reasoning",
|
|
64
|
+
"thought",
|
|
65
|
+
"think_intent",
|
|
66
|
+
];
|
|
67
|
+
const MAX_THINKING_TAG_LEN = 16; // </think_intent> is 15 chars
|
|
68
|
+
/**
|
|
69
|
+
* Strip thinking tags from streamed text, routing tagged content to reasoning.
|
|
70
|
+
* Buffers partial tags across chunk boundaries.
|
|
71
|
+
*/
|
|
72
|
+
export function createThinkingTagFilter() {
|
|
73
|
+
let buffer = "";
|
|
74
|
+
let inThinking = false;
|
|
75
|
+
return {
|
|
76
|
+
process(text) {
|
|
77
|
+
const input = buffer + text;
|
|
78
|
+
buffer = "";
|
|
79
|
+
let content = "";
|
|
80
|
+
let reasoning = "";
|
|
81
|
+
let lastIdx = 0;
|
|
82
|
+
const re = new RegExp(`<(/?)(?:${THINKING_TAG_NAMES.join("|")})\\s*>`, "gi");
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = re.exec(input)) !== null) {
|
|
85
|
+
const before = input.slice(lastIdx, match.index);
|
|
86
|
+
if (inThinking)
|
|
87
|
+
reasoning += before;
|
|
88
|
+
else
|
|
89
|
+
content += before;
|
|
90
|
+
inThinking = match[1] !== "/";
|
|
91
|
+
lastIdx = re.lastIndex;
|
|
92
|
+
}
|
|
93
|
+
const rest = input.slice(lastIdx);
|
|
94
|
+
// Buffer a trailing '<' that could be the start of a thinking tag.
|
|
95
|
+
const ltPos = rest.lastIndexOf("<");
|
|
96
|
+
if (ltPos >= 0 &&
|
|
97
|
+
rest.length - ltPos < MAX_THINKING_TAG_LEN &&
|
|
98
|
+
/^<\/?[a-z_]*$/i.test(rest.slice(ltPos))) {
|
|
99
|
+
buffer = rest.slice(ltPos);
|
|
100
|
+
const before = rest.slice(0, ltPos);
|
|
101
|
+
if (inThinking)
|
|
102
|
+
reasoning += before;
|
|
103
|
+
else
|
|
104
|
+
content += before;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (inThinking)
|
|
108
|
+
reasoning += rest;
|
|
109
|
+
else
|
|
110
|
+
content += rest;
|
|
111
|
+
}
|
|
112
|
+
return { content, reasoning };
|
|
113
|
+
},
|
|
114
|
+
flush() {
|
|
115
|
+
const b = buffer;
|
|
116
|
+
buffer = "";
|
|
117
|
+
if (!b)
|
|
118
|
+
return { content: "", reasoning: "" };
|
|
119
|
+
return inThinking
|
|
120
|
+
? { content: "", reasoning: b }
|
|
121
|
+
: { content: b, reasoning: "" };
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function computeUsage(state) {
|
|
126
|
+
const completion_tokens = state.outputTokens;
|
|
127
|
+
const total_tokens = state.totalTokens || completion_tokens;
|
|
128
|
+
const prompt_tokens = Math.max(0, total_tokens - completion_tokens);
|
|
129
|
+
return { prompt_tokens, completion_tokens, total_tokens };
|
|
130
|
+
}
|
|
131
|
+
export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
|
|
132
|
+
const msgCase = msg.message.case;
|
|
133
|
+
if (msgCase === "interactionUpdate") {
|
|
134
|
+
handleInteractionUpdate(msg.message.value, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage);
|
|
135
|
+
}
|
|
136
|
+
else if (msgCase === "kvServerMessage") {
|
|
137
|
+
handleKvMessage(msg.message.value, blobStore, sendFrame);
|
|
138
|
+
}
|
|
139
|
+
else if (msgCase === "execServerMessage") {
|
|
140
|
+
handleExecMessage(msg.message.value, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec);
|
|
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
|
+
}
|
|
151
|
+
else if (msgCase === "conversationCheckpointUpdate") {
|
|
152
|
+
const stateStructure = msg.message.value;
|
|
153
|
+
if (stateStructure.tokenDetails) {
|
|
154
|
+
state.totalTokens = stateStructure.tokenDetails.usedTokens;
|
|
155
|
+
}
|
|
156
|
+
if (onCheckpoint) {
|
|
157
|
+
onCheckpoint(toBinary(ConversationStateStructureSchema, stateStructure));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
onUnsupportedMessage?.({
|
|
162
|
+
category: "agentMessage",
|
|
163
|
+
caseName: msgCase ?? "undefined",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage) {
|
|
168
|
+
const updateCase = update.message?.case;
|
|
169
|
+
if (updateCase === "textDelta") {
|
|
170
|
+
const delta = update.message.value.text || "";
|
|
171
|
+
if (delta)
|
|
172
|
+
onText(delta, false);
|
|
173
|
+
}
|
|
174
|
+
else if (updateCase === "thinkingDelta") {
|
|
175
|
+
const delta = update.message.value.text || "";
|
|
176
|
+
if (delta)
|
|
177
|
+
onText(delta, true);
|
|
178
|
+
}
|
|
179
|
+
else if (updateCase === "tokenDelta") {
|
|
180
|
+
state.outputTokens += update.message.value.tokens ?? 0;
|
|
181
|
+
}
|
|
182
|
+
else if (updateCase === "partialToolCall") {
|
|
183
|
+
const partial = update.message.value;
|
|
184
|
+
if (partial.callId && partial.argsTextDelta) {
|
|
185
|
+
const existing = state.interactionToolArgsText.get(partial.callId) ?? "";
|
|
186
|
+
state.interactionToolArgsText.set(partial.callId, `${existing}${partial.argsTextDelta}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (updateCase === "toolCallCompleted") {
|
|
190
|
+
const callId = update.message.value?.callId;
|
|
191
|
+
if (callId)
|
|
192
|
+
state.interactionToolArgsText.delete(callId);
|
|
193
|
+
}
|
|
194
|
+
else if (updateCase === "turnEnded") {
|
|
195
|
+
onTurnEnded?.();
|
|
196
|
+
}
|
|
197
|
+
else if (updateCase === "toolCallStarted" ||
|
|
198
|
+
updateCase === "toolCallDelta" ||
|
|
199
|
+
updateCase === "thinkingCompleted" ||
|
|
200
|
+
updateCase === "userMessageAppended" ||
|
|
201
|
+
updateCase === "summary" ||
|
|
202
|
+
updateCase === "summaryStarted" ||
|
|
203
|
+
updateCase === "summaryCompleted" ||
|
|
204
|
+
updateCase === "heartbeat" ||
|
|
205
|
+
updateCase === "stepStarted" ||
|
|
206
|
+
updateCase === "stepCompleted") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
onUnsupportedMessage?.({
|
|
211
|
+
category: "interactionUpdate",
|
|
212
|
+
caseName: updateCase ?? "undefined",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// toolCallStarted, partialToolCall, toolCallDelta, and toolCallCompleted are
|
|
216
|
+
// informational only. Actionable MCP tool execution must come from
|
|
217
|
+
// execServerMessage.mcpArgs so tool results can be resumed with the correct
|
|
218
|
+
// exec envelope.
|
|
219
|
+
}
|
|
220
|
+
function handleInteractionQuery(query, sendFrame, onUnsupportedMessage) {
|
|
221
|
+
const queryCase = query.query.case;
|
|
222
|
+
if (queryCase === "webSearchRequestQuery") {
|
|
223
|
+
const response = create(WebSearchRequestResponseSchema, {
|
|
224
|
+
result: {
|
|
225
|
+
case: "rejected",
|
|
226
|
+
value: create(WebSearchRequestResponse_RejectedSchema, {
|
|
227
|
+
reason: "Native Cursor web search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
sendInteractionResponse(query.id, "webSearchRequestResponse", response, sendFrame);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (queryCase === "askQuestionInteractionQuery") {
|
|
235
|
+
const response = create(AskQuestionInteractionResponseSchema, {
|
|
236
|
+
result: create(AskQuestionResultSchema, {
|
|
237
|
+
result: {
|
|
238
|
+
case: "rejected",
|
|
239
|
+
value: create(AskQuestionRejectedSchema, {
|
|
240
|
+
reason: "Native Cursor question prompts are not available in this environment. Use the provided MCP tool `question` instead.",
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
sendInteractionResponse(query.id, "askQuestionInteractionResponse", response, sendFrame);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (queryCase === "switchModeRequestQuery") {
|
|
249
|
+
const response = create(SwitchModeRequestResponseSchema, {
|
|
250
|
+
result: {
|
|
251
|
+
case: "rejected",
|
|
252
|
+
value: create(SwitchModeRequestResponse_RejectedSchema, {
|
|
253
|
+
reason: "Cursor mode switching is not available in this environment. Continue using the current agent and the provided MCP tools.",
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
sendInteractionResponse(query.id, "switchModeRequestResponse", response, sendFrame);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (queryCase === "exaSearchRequestQuery") {
|
|
261
|
+
const response = create(ExaSearchRequestResponseSchema, {
|
|
262
|
+
result: {
|
|
263
|
+
case: "rejected",
|
|
264
|
+
value: create(ExaSearchRequestResponse_RejectedSchema, {
|
|
265
|
+
reason: "Native Cursor Exa search is not available in this environment. Use the provided MCP tool `websearch` instead.",
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
sendInteractionResponse(query.id, "exaSearchRequestResponse", response, sendFrame);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (queryCase === "exaFetchRequestQuery") {
|
|
273
|
+
const response = create(ExaFetchRequestResponseSchema, {
|
|
274
|
+
result: {
|
|
275
|
+
case: "rejected",
|
|
276
|
+
value: create(ExaFetchRequestResponse_RejectedSchema, {
|
|
277
|
+
reason: "Native Cursor Exa fetch is not available in this environment. Use the provided MCP tools `websearch` and `webfetch` instead.",
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
sendInteractionResponse(query.id, "exaFetchRequestResponse", response, sendFrame);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (queryCase === "createPlanRequestQuery") {
|
|
285
|
+
const response = create(CreatePlanRequestResponseSchema, {
|
|
286
|
+
result: create(CreatePlanResultSchema, {
|
|
287
|
+
planUri: "",
|
|
288
|
+
result: {
|
|
289
|
+
case: "error",
|
|
290
|
+
value: create(CreatePlanErrorSchema, {
|
|
291
|
+
error: "Native Cursor plan creation is not available in this environment. Use the provided MCP planning tools instead.",
|
|
292
|
+
}),
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
sendInteractionResponse(query.id, "createPlanRequestResponse", response, sendFrame);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
onUnsupportedMessage?.({
|
|
300
|
+
category: "interactionQuery",
|
|
301
|
+
caseName: queryCase ?? "undefined",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/** Send a KV client response back to Cursor. */
|
|
305
|
+
function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
|
|
306
|
+
const response = create(KvClientMessageSchema, {
|
|
307
|
+
id: kvMsg.id,
|
|
308
|
+
message: { case: messageCase, value: value },
|
|
309
|
+
});
|
|
310
|
+
const clientMsg = create(AgentClientMessageSchema, {
|
|
311
|
+
message: { case: "kvClientMessage", value: response },
|
|
312
|
+
});
|
|
313
|
+
sendFrame(toBinary(AgentClientMessageSchema, clientMsg));
|
|
314
|
+
}
|
|
315
|
+
function sendInteractionResponse(queryId, messageCase, value, sendFrame) {
|
|
316
|
+
const response = create(InteractionResponseSchema, {
|
|
317
|
+
id: queryId,
|
|
318
|
+
result: { case: messageCase, value: value },
|
|
319
|
+
});
|
|
320
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
321
|
+
message: { case: "interactionResponse", value: response },
|
|
322
|
+
});
|
|
323
|
+
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
324
|
+
}
|
|
325
|
+
function handleKvMessage(kvMsg, blobStore, sendFrame) {
|
|
326
|
+
const kvCase = kvMsg.message.case;
|
|
327
|
+
if (kvCase === "getBlobArgs") {
|
|
328
|
+
const blobId = kvMsg.message.value.blobId;
|
|
329
|
+
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
330
|
+
const blobData = blobStore.get(blobIdKey);
|
|
331
|
+
if (!blobData) {
|
|
332
|
+
logPluginWarn("Cursor requested missing blob", {
|
|
333
|
+
blobId: blobIdKey,
|
|
334
|
+
knownBlobCount: blobStore.size,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
sendKvResponse(kvMsg, "getBlobResult", create(GetBlobResultSchema, blobData ? { blobData } : {}), sendFrame);
|
|
338
|
+
}
|
|
339
|
+
else if (kvCase === "setBlobArgs") {
|
|
340
|
+
const { blobId, blobData } = kvMsg.message.value;
|
|
341
|
+
blobStore.set(Buffer.from(blobId).toString("hex"), blobData);
|
|
342
|
+
sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function handleExecMessage(execMsg, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec) {
|
|
346
|
+
const execCase = execMsg.message.case;
|
|
347
|
+
if (execCase === "requestContextArgs") {
|
|
348
|
+
const requestContext = create(RequestContextSchema, {
|
|
349
|
+
rules: [],
|
|
350
|
+
repositoryInfo: [],
|
|
351
|
+
tools: mcpTools,
|
|
352
|
+
gitRepos: [],
|
|
353
|
+
projectLayouts: [],
|
|
354
|
+
mcpInstructions: [],
|
|
355
|
+
fileContents: {},
|
|
356
|
+
customSubagents: [],
|
|
357
|
+
});
|
|
358
|
+
const result = create(RequestContextResultSchema, {
|
|
359
|
+
result: {
|
|
360
|
+
case: "success",
|
|
361
|
+
value: create(RequestContextSuccessSchema, { requestContext }),
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
sendExecResult(execMsg, "requestContextResult", result, sendFrame);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (execCase === "mcpArgs") {
|
|
368
|
+
const mcpArgs = execMsg.message.value;
|
|
369
|
+
const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
|
|
370
|
+
const toolCallId = mcpArgs.toolCallId || crypto.randomUUID();
|
|
371
|
+
if (state.emittedToolCallIds.has(toolCallId)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
state.emittedToolCallIds.add(toolCallId);
|
|
375
|
+
onMcpExec({
|
|
376
|
+
execId: execMsg.execId,
|
|
377
|
+
execMsgId: execMsg.id,
|
|
378
|
+
toolCallId,
|
|
379
|
+
toolName: mcpArgs.toolName || mcpArgs.name,
|
|
380
|
+
decodedArgs: JSON.stringify(decoded),
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// --- Reject native Cursor tools ---
|
|
385
|
+
// The model tries these first. We must respond with rejection/error
|
|
386
|
+
// so it falls back to our MCP tools (registered via RequestContext).
|
|
387
|
+
const REJECT_REASON = "Tool not available in this environment. Use the MCP tools provided instead.";
|
|
388
|
+
if (execCase === "readArgs") {
|
|
389
|
+
const args = execMsg.message.value;
|
|
390
|
+
const result = create(ReadResultSchema, {
|
|
391
|
+
result: {
|
|
392
|
+
case: "rejected",
|
|
393
|
+
value: create(ReadRejectedSchema, {
|
|
394
|
+
path: args.path,
|
|
395
|
+
reason: REJECT_REASON,
|
|
396
|
+
}),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
sendExecResult(execMsg, "readResult", result, sendFrame);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (execCase === "lsArgs") {
|
|
403
|
+
const args = execMsg.message.value;
|
|
404
|
+
const result = create(LsResultSchema, {
|
|
405
|
+
result: {
|
|
406
|
+
case: "rejected",
|
|
407
|
+
value: create(LsRejectedSchema, {
|
|
408
|
+
path: args.path,
|
|
409
|
+
reason: REJECT_REASON,
|
|
410
|
+
}),
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
sendExecResult(execMsg, "lsResult", result, sendFrame);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (execCase === "grepArgs") {
|
|
417
|
+
const result = create(GrepResultSchema, {
|
|
418
|
+
result: {
|
|
419
|
+
case: "error",
|
|
420
|
+
value: create(GrepErrorSchema, { error: REJECT_REASON }),
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
sendExecResult(execMsg, "grepResult", result, sendFrame);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (execCase === "writeArgs") {
|
|
427
|
+
const args = execMsg.message.value;
|
|
428
|
+
const result = create(WriteResultSchema, {
|
|
429
|
+
result: {
|
|
430
|
+
case: "rejected",
|
|
431
|
+
value: create(WriteRejectedSchema, {
|
|
432
|
+
path: args.path,
|
|
433
|
+
reason: REJECT_REASON,
|
|
434
|
+
}),
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
sendExecResult(execMsg, "writeResult", result, sendFrame);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (execCase === "deleteArgs") {
|
|
441
|
+
const args = execMsg.message.value;
|
|
442
|
+
const result = create(DeleteResultSchema, {
|
|
443
|
+
result: {
|
|
444
|
+
case: "rejected",
|
|
445
|
+
value: create(DeleteRejectedSchema, {
|
|
446
|
+
path: args.path,
|
|
447
|
+
reason: REJECT_REASON,
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
sendExecResult(execMsg, "deleteResult", result, sendFrame);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (execCase === "shellArgs" || execCase === "shellStreamArgs") {
|
|
455
|
+
const args = execMsg.message.value;
|
|
456
|
+
const result = create(ShellResultSchema, {
|
|
457
|
+
result: {
|
|
458
|
+
case: "rejected",
|
|
459
|
+
value: create(ShellRejectedSchema, {
|
|
460
|
+
command: args.command ?? "",
|
|
461
|
+
workingDirectory: args.workingDirectory ?? "",
|
|
462
|
+
reason: REJECT_REASON,
|
|
463
|
+
isReadonly: false,
|
|
464
|
+
}),
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
sendExecResult(execMsg, "shellResult", result, sendFrame);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (execCase === "backgroundShellSpawnArgs") {
|
|
471
|
+
const args = execMsg.message.value;
|
|
472
|
+
const result = create(BackgroundShellSpawnResultSchema, {
|
|
473
|
+
result: {
|
|
474
|
+
case: "rejected",
|
|
475
|
+
value: create(ShellRejectedSchema, {
|
|
476
|
+
command: args.command ?? "",
|
|
477
|
+
workingDirectory: args.workingDirectory ?? "",
|
|
478
|
+
reason: REJECT_REASON,
|
|
479
|
+
isReadonly: false,
|
|
480
|
+
}),
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
sendExecResult(execMsg, "backgroundShellSpawnResult", result, sendFrame);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (execCase === "writeShellStdinArgs") {
|
|
487
|
+
const result = create(WriteShellStdinResultSchema, {
|
|
488
|
+
result: {
|
|
489
|
+
case: "error",
|
|
490
|
+
value: create(WriteShellStdinErrorSchema, { error: REJECT_REASON }),
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
sendExecResult(execMsg, "writeShellStdinResult", result, sendFrame);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (execCase === "fetchArgs") {
|
|
497
|
+
const args = execMsg.message.value;
|
|
498
|
+
const result = create(FetchResultSchema, {
|
|
499
|
+
result: {
|
|
500
|
+
case: "error",
|
|
501
|
+
value: create(FetchErrorSchema, {
|
|
502
|
+
url: args.url ?? "",
|
|
503
|
+
error: REJECT_REASON,
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
sendExecResult(execMsg, "fetchResult", result, sendFrame);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (execCase === "diagnosticsArgs") {
|
|
511
|
+
const result = create(DiagnosticsResultSchema, {});
|
|
512
|
+
sendExecResult(execMsg, "diagnosticsResult", result, sendFrame);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
// MCP resource/screen/computer exec types
|
|
516
|
+
const miscCaseMap = {
|
|
517
|
+
listMcpResourcesExecArgs: "listMcpResourcesExecResult",
|
|
518
|
+
readMcpResourceExecArgs: "readMcpResourceExecResult",
|
|
519
|
+
recordScreenArgs: "recordScreenResult",
|
|
520
|
+
computerUseArgs: "computerUseResult",
|
|
521
|
+
};
|
|
522
|
+
const resultCase = miscCaseMap[execCase];
|
|
523
|
+
if (resultCase) {
|
|
524
|
+
sendExecResult(execMsg, resultCase, create(McpResultSchema, {}), sendFrame);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
logPluginError("Unhandled Cursor exec type", {
|
|
528
|
+
execCase: execCase ?? "undefined",
|
|
529
|
+
execId: execMsg.execId,
|
|
530
|
+
execMsgId: execMsg.id,
|
|
531
|
+
});
|
|
532
|
+
onUnhandledExec?.({
|
|
533
|
+
execCase: execCase ?? "undefined",
|
|
534
|
+
execId: execMsg.execId,
|
|
535
|
+
execMsgId: execMsg.id,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
/** Send an exec client message back to Cursor. */
|
|
539
|
+
function sendExecResult(execMsg, messageCase, value, sendFrame) {
|
|
540
|
+
const execClientMessage = create(ExecClientMessageSchema, {
|
|
541
|
+
id: execMsg.id,
|
|
542
|
+
execId: execMsg.execId,
|
|
543
|
+
message: { case: messageCase, value: value },
|
|
544
|
+
});
|
|
545
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
546
|
+
message: { case: "execClientMessage", value: execClientMessage },
|
|
547
|
+
});
|
|
548
|
+
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
549
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PendingExec } from "./types";
|
|
2
|
+
export interface StreamState {
|
|
3
|
+
toolCallIndex: number;
|
|
4
|
+
pendingExecs: PendingExec[];
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
totalTokens: number;
|
|
7
|
+
interactionToolArgsText: Map<string, string>;
|
|
8
|
+
emittedToolCallIds: Set<string>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleTitleGenerationRequest(sourceText: string, accessToken: string, modelId: string, stream: boolean): Promise<Response>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
|
+
import { NameAgentRequestSchema, NameAgentResponseSchema, } from "../proto/agent_pb";
|
|
3
|
+
import { callCursorUnaryRpc, decodeConnectUnaryBody } from "../cursor";
|
|
4
|
+
import { SSE_HEADERS } from "./sse";
|
|
5
|
+
function deriveFallbackTitle(text) {
|
|
6
|
+
const cleaned = text
|
|
7
|
+
.replace(/<[^>]+>/g, " ")
|
|
8
|
+
.replace(/\[[^\]]+\]/g, " ")
|
|
9
|
+
.replace(/[^\p{L}\p{N}'’\-\s]+/gu, " ")
|
|
10
|
+
.replace(/\s+/g, " ")
|
|
11
|
+
.trim();
|
|
12
|
+
if (!cleaned)
|
|
13
|
+
return "";
|
|
14
|
+
const words = cleaned.split(" ").filter(Boolean).slice(0, 6);
|
|
15
|
+
return finalizeTitle(words.map(titleCaseWord).join(" "));
|
|
16
|
+
}
|
|
17
|
+
function titleCaseWord(word) {
|
|
18
|
+
if (!word)
|
|
19
|
+
return word;
|
|
20
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
21
|
+
}
|
|
22
|
+
function finalizeTitle(value) {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/^#{1,6}\s*/, "")
|
|
25
|
+
.replace(/[.!?,:;]+$/g, "")
|
|
26
|
+
.replace(/\s+/g, " ")
|
|
27
|
+
.trim()
|
|
28
|
+
.slice(0, 80)
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
function createBufferedSSETextResponse(modelId, text, usage) {
|
|
32
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
33
|
+
const created = Math.floor(Date.now() / 1000);
|
|
34
|
+
const payload = [
|
|
35
|
+
{
|
|
36
|
+
id: completionId,
|
|
37
|
+
object: "chat.completion.chunk",
|
|
38
|
+
created,
|
|
39
|
+
model: modelId,
|
|
40
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: completionId,
|
|
44
|
+
object: "chat.completion.chunk",
|
|
45
|
+
created,
|
|
46
|
+
model: modelId,
|
|
47
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: completionId,
|
|
51
|
+
object: "chat.completion.chunk",
|
|
52
|
+
created,
|
|
53
|
+
model: modelId,
|
|
54
|
+
choices: [],
|
|
55
|
+
usage,
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`)
|
|
59
|
+
.join("") + "data: [DONE]\n\n";
|
|
60
|
+
return new Response(payload, { headers: SSE_HEADERS });
|
|
61
|
+
}
|
|
62
|
+
export async function handleTitleGenerationRequest(sourceText, accessToken, modelId, stream) {
|
|
63
|
+
const requestBody = toBinary(NameAgentRequestSchema, create(NameAgentRequestSchema, {
|
|
64
|
+
userMessage: sourceText,
|
|
65
|
+
}));
|
|
66
|
+
const response = await callCursorUnaryRpc({
|
|
67
|
+
accessToken,
|
|
68
|
+
rpcPath: "/agent.v1.AgentService/NameAgent",
|
|
69
|
+
requestBody,
|
|
70
|
+
timeoutMs: 5_000,
|
|
71
|
+
});
|
|
72
|
+
if (response.timedOut) {
|
|
73
|
+
throw new Error("Cursor title generation timed out");
|
|
74
|
+
}
|
|
75
|
+
if (response.exitCode !== 0) {
|
|
76
|
+
throw new Error(`Cursor title generation failed with HTTP ${response.exitCode}`);
|
|
77
|
+
}
|
|
78
|
+
const payload = decodeConnectUnaryBody(response.body) ?? response.body;
|
|
79
|
+
const decoded = fromBinary(NameAgentResponseSchema, payload);
|
|
80
|
+
const title = finalizeTitle(decoded.name) ||
|
|
81
|
+
deriveFallbackTitle(sourceText) ||
|
|
82
|
+
"Untitled Session";
|
|
83
|
+
const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
84
|
+
if (stream) {
|
|
85
|
+
return createBufferedSSETextResponse(modelId, title, usage);
|
|
86
|
+
}
|
|
87
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
88
|
+
const created = Math.floor(Date.now() / 1000);
|
|
89
|
+
return new Response(JSON.stringify({
|
|
90
|
+
id: completionId,
|
|
91
|
+
object: "chat.completion",
|
|
92
|
+
created,
|
|
93
|
+
model: modelId,
|
|
94
|
+
choices: [
|
|
95
|
+
{
|
|
96
|
+
index: 0,
|
|
97
|
+
message: { role: "assistant", content: title },
|
|
98
|
+
finish_reason: "stop",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
usage,
|
|
102
|
+
}), { headers: { "Content-Type": "application/json" } });
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CursorSession } from "../cursor/bidi-session";
|
|
2
|
+
import type { ConversationRequestMetadata } from "./conversation-meta";
|
|
3
|
+
import type { McpToolDefinition } from "../proto/agent_pb";
|
|
4
|
+
export interface CursorRequestPayload {
|
|
5
|
+
requestBytes: Uint8Array;
|
|
6
|
+
blobStore: Map<string, Uint8Array>;
|
|
7
|
+
mcpTools: McpToolDefinition[];
|
|
8
|
+
}
|
|
9
|
+
/** A pending tool execution waiting for results from the caller. */
|
|
10
|
+
export interface PendingExec {
|
|
11
|
+
execId: string;
|
|
12
|
+
execMsgId: number;
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
/** Decoded arguments JSON string for SSE tool_calls emission. */
|
|
16
|
+
decodedArgs: string;
|
|
17
|
+
}
|
|
18
|
+
/** A live Cursor session kept alive across requests for tool result continuation. */
|
|
19
|
+
export interface ActiveBridge {
|
|
20
|
+
bridge: CursorSession;
|
|
21
|
+
heartbeatTimer: NodeJS.Timeout;
|
|
22
|
+
blobStore: Map<string, Uint8Array>;
|
|
23
|
+
mcpTools: McpToolDefinition[];
|
|
24
|
+
pendingExecs: PendingExec[];
|
|
25
|
+
modelId: string;
|
|
26
|
+
metadata: ConversationRequestMetadata;
|
|
27
|
+
}
|