@playwo/opencode-cursor-oauth 0.0.0-dev.0cb3e1517254 → 0.0.0-dev.194e3412ea47
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/proxy.js +166 -2
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { create, fromBinary, fromJson, toBinary, toJson } from "@bufbuild/protobuf";
|
|
16
16
|
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
17
|
-
import { AgentClientMessageSchema, AgentRunRequestSchema, AgentServerMessageSchema, BidiRequestIdSchema, ClientHeartbeatSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolDefinitionSchema, McpToolResultContentItemSchema, ModelDetailsSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, UserMessageActionSchema, UserMessageSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, } from "./proto/agent_pb";
|
|
17
|
+
import { AgentClientMessageSchema, AgentRunRequestSchema, AgentServerMessageSchema, BidiRequestIdSchema, ClientHeartbeatSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, BackgroundShellSpawnResultSchema, DeleteResultSchema, DeleteRejectedSchema, DiagnosticsResultSchema, ExecClientMessageSchema, FetchErrorSchema, FetchResultSchema, GetBlobResultSchema, GrepErrorSchema, GrepResultSchema, KvClientMessageSchema, LsRejectedSchema, LsResultSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolDefinitionSchema, McpToolResultContentItemSchema, ModelDetailsSchema, NameAgentRequestSchema, NameAgentResponseSchema, ReadRejectedSchema, ReadResultSchema, RequestContextResultSchema, RequestContextSchema, RequestContextSuccessSchema, SetBlobResultSchema, ShellRejectedSchema, ShellResultSchema, UserMessageActionSchema, UserMessageSchema, WriteRejectedSchema, WriteResultSchema, WriteShellStdinErrorSchema, WriteShellStdinResultSchema, } from "./proto/agent_pb";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { connect as connectHttp2 } from "node:http2";
|
|
20
20
|
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
@@ -22,6 +22,7 @@ const CURSOR_API_URL = process.env.CURSOR_API_URL ?? "https://api2.cursor.sh";
|
|
|
22
22
|
const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
|
|
23
23
|
const CURSOR_CONNECT_PROTOCOL_VERSION = "1";
|
|
24
24
|
const CONNECT_END_STREAM_FLAG = 0b00000010;
|
|
25
|
+
const OPENCODE_TITLE_REQUEST_MARKER = "Generate a title for this conversation:";
|
|
25
26
|
const SSE_HEADERS = {
|
|
26
27
|
"Content-Type": "text/event-stream",
|
|
27
28
|
"Cache-Control": "no-cache",
|
|
@@ -74,6 +75,26 @@ function frameConnectMessage(data, flags = 0) {
|
|
|
74
75
|
frame.set(data, 5);
|
|
75
76
|
return frame;
|
|
76
77
|
}
|
|
78
|
+
function decodeConnectUnaryBody(payload) {
|
|
79
|
+
if (payload.length < 5)
|
|
80
|
+
return null;
|
|
81
|
+
let offset = 0;
|
|
82
|
+
while (offset + 5 <= payload.length) {
|
|
83
|
+
const flags = payload[offset];
|
|
84
|
+
const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
|
|
85
|
+
const messageLength = view.getUint32(1, false);
|
|
86
|
+
const frameEnd = offset + 5 + messageLength;
|
|
87
|
+
if (frameEnd > payload.length)
|
|
88
|
+
return null;
|
|
89
|
+
if ((flags & 0b0000_0001) !== 0)
|
|
90
|
+
return null;
|
|
91
|
+
if ((flags & CONNECT_END_STREAM_FLAG) === 0) {
|
|
92
|
+
return payload.subarray(offset + 5, frameEnd);
|
|
93
|
+
}
|
|
94
|
+
offset = frameEnd;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
77
98
|
function buildCursorHeaders(options, contentType, extra = {}) {
|
|
78
99
|
const headers = new Headers(buildCursorHeaderValues(options, contentType, extra));
|
|
79
100
|
return headers;
|
|
@@ -542,6 +563,21 @@ function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
542
563
|
const parsed = parseMessages(body.messages);
|
|
543
564
|
const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
|
|
544
565
|
const modelId = body.model;
|
|
566
|
+
const normalizedAgentKey = normalizeAgentKey(context.agentKey);
|
|
567
|
+
const titleDetection = detectTitleRequest(body);
|
|
568
|
+
const isTitleAgent = titleDetection.matched;
|
|
569
|
+
if (isTitleAgent) {
|
|
570
|
+
const titleSourceText = buildTitleSourceText(userText, turns, pendingAssistantSummary, toolResults);
|
|
571
|
+
if (!titleSourceText) {
|
|
572
|
+
return new Response(JSON.stringify({
|
|
573
|
+
error: {
|
|
574
|
+
message: "No title source text found",
|
|
575
|
+
type: "invalid_request_error",
|
|
576
|
+
},
|
|
577
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
578
|
+
}
|
|
579
|
+
return handleTitleGenerationRequest(titleSourceText, accessToken, modelId, body.stream !== false);
|
|
580
|
+
}
|
|
545
581
|
const tools = selectToolsForChoice(body.tools ?? [], body.tool_choice);
|
|
546
582
|
if (!userText && toolResults.length === 0) {
|
|
547
583
|
return new Response(JSON.stringify({
|
|
@@ -601,7 +637,7 @@ function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
601
637
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
602
638
|
const needsInitialHandoff = !stored.checkpoint && (turns.length > 0 || pendingAssistantSummary || toolResults.length > 0);
|
|
603
639
|
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
604
|
-
|
|
640
|
+
let effectiveUserText = needsInitialHandoff
|
|
605
641
|
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
606
642
|
: toolResults.length > 0
|
|
607
643
|
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
@@ -615,6 +651,7 @@ function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
615
651
|
completedTurnsFingerprint,
|
|
616
652
|
turns,
|
|
617
653
|
userText,
|
|
654
|
+
agentKey: normalizedAgentKey,
|
|
618
655
|
});
|
|
619
656
|
}
|
|
620
657
|
return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, {
|
|
@@ -623,6 +660,7 @@ function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
623
660
|
completedTurnsFingerprint,
|
|
624
661
|
turns,
|
|
625
662
|
userText,
|
|
663
|
+
agentKey: normalizedAgentKey,
|
|
626
664
|
});
|
|
627
665
|
}
|
|
628
666
|
/** Normalize OpenAI message content to a plain string. */
|
|
@@ -804,6 +842,37 @@ function buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, too
|
|
|
804
842
|
userText.trim(),
|
|
805
843
|
].filter(Boolean).join("\n");
|
|
806
844
|
}
|
|
845
|
+
function buildTitleSourceText(userText, turns, pendingAssistantSummary, toolResults) {
|
|
846
|
+
const history = turns
|
|
847
|
+
.map((turn) => [
|
|
848
|
+
isTitleRequestMarker(turn.userText) ? "" : turn.userText.trim(),
|
|
849
|
+
turn.assistantText.trim(),
|
|
850
|
+
].filter(Boolean).join("\n"))
|
|
851
|
+
.filter(Boolean);
|
|
852
|
+
if (pendingAssistantSummary.trim()) {
|
|
853
|
+
history.push(pendingAssistantSummary.trim());
|
|
854
|
+
}
|
|
855
|
+
if (toolResults.length > 0) {
|
|
856
|
+
history.push(toolResults.map(formatToolResultSummary).join("\n\n"));
|
|
857
|
+
}
|
|
858
|
+
if (userText.trim() && !isTitleRequestMarker(userText)) {
|
|
859
|
+
history.push(userText.trim());
|
|
860
|
+
}
|
|
861
|
+
return history.join("\n\n").trim();
|
|
862
|
+
}
|
|
863
|
+
function detectTitleRequest(body) {
|
|
864
|
+
if ((body.tools?.length ?? 0) > 0) {
|
|
865
|
+
return { matched: false, reason: "tools-present" };
|
|
866
|
+
}
|
|
867
|
+
const firstNonSystem = body.messages.find((message) => message.role !== "system");
|
|
868
|
+
if (firstNonSystem?.role === "user" && isTitleRequestMarker(textContent(firstNonSystem.content))) {
|
|
869
|
+
return { matched: true, reason: "opencode-title-marker" };
|
|
870
|
+
}
|
|
871
|
+
return { matched: false, reason: "no-title-marker" };
|
|
872
|
+
}
|
|
873
|
+
function isTitleRequestMarker(text) {
|
|
874
|
+
return text.trim() === OPENCODE_TITLE_REQUEST_MARKER;
|
|
875
|
+
}
|
|
807
876
|
function selectToolsForChoice(tools, toolChoice) {
|
|
808
877
|
if (!tools.length)
|
|
809
878
|
return [];
|
|
@@ -1328,6 +1397,101 @@ function updateStoredConversationAfterCompletion(convKey, metadata, assistantTex
|
|
|
1328
1397
|
stored.completedTurnsFingerprint = buildCompletedTurnsFingerprint(metadata.systemPrompt, nextTurns);
|
|
1329
1398
|
stored.lastAccessMs = Date.now();
|
|
1330
1399
|
}
|
|
1400
|
+
function deriveFallbackTitle(text) {
|
|
1401
|
+
const cleaned = text
|
|
1402
|
+
.replace(/<[^>]+>/g, " ")
|
|
1403
|
+
.replace(/\[[^\]]+\]/g, " ")
|
|
1404
|
+
.replace(/[^\p{L}\p{N}'’\-\s]+/gu, " ")
|
|
1405
|
+
.replace(/\s+/g, " ")
|
|
1406
|
+
.trim();
|
|
1407
|
+
if (!cleaned)
|
|
1408
|
+
return "";
|
|
1409
|
+
const words = cleaned.split(" ").filter(Boolean).slice(0, 6);
|
|
1410
|
+
return finalizeTitle(words.map(titleCaseWord).join(" "));
|
|
1411
|
+
}
|
|
1412
|
+
function titleCaseWord(word) {
|
|
1413
|
+
if (!word)
|
|
1414
|
+
return word;
|
|
1415
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
1416
|
+
}
|
|
1417
|
+
function finalizeTitle(value) {
|
|
1418
|
+
return value
|
|
1419
|
+
.replace(/^#{1,6}\s*/, "")
|
|
1420
|
+
.replace(/[.!?,:;]+$/g, "")
|
|
1421
|
+
.replace(/\s+/g, " ")
|
|
1422
|
+
.trim()
|
|
1423
|
+
.slice(0, 80)
|
|
1424
|
+
.trim();
|
|
1425
|
+
}
|
|
1426
|
+
function createBufferedSSETextResponse(modelId, text, usage) {
|
|
1427
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1428
|
+
const created = Math.floor(Date.now() / 1000);
|
|
1429
|
+
const payload = [
|
|
1430
|
+
{
|
|
1431
|
+
id: completionId,
|
|
1432
|
+
object: "chat.completion.chunk",
|
|
1433
|
+
created,
|
|
1434
|
+
model: modelId,
|
|
1435
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
id: completionId,
|
|
1439
|
+
object: "chat.completion.chunk",
|
|
1440
|
+
created,
|
|
1441
|
+
model: modelId,
|
|
1442
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
id: completionId,
|
|
1446
|
+
object: "chat.completion.chunk",
|
|
1447
|
+
created,
|
|
1448
|
+
model: modelId,
|
|
1449
|
+
choices: [],
|
|
1450
|
+
usage,
|
|
1451
|
+
},
|
|
1452
|
+
].map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("") + "data: [DONE]\n\n";
|
|
1453
|
+
return new Response(payload, { headers: SSE_HEADERS });
|
|
1454
|
+
}
|
|
1455
|
+
async function handleTitleGenerationRequest(sourceText, accessToken, modelId, stream) {
|
|
1456
|
+
const requestBody = toBinary(NameAgentRequestSchema, create(NameAgentRequestSchema, {
|
|
1457
|
+
userMessage: sourceText,
|
|
1458
|
+
}));
|
|
1459
|
+
const response = await callCursorUnaryRpc({
|
|
1460
|
+
accessToken,
|
|
1461
|
+
rpcPath: "/agent.v1.AgentService/NameAgent",
|
|
1462
|
+
requestBody,
|
|
1463
|
+
timeoutMs: 5_000,
|
|
1464
|
+
});
|
|
1465
|
+
if (response.timedOut) {
|
|
1466
|
+
throw new Error("Cursor title generation timed out");
|
|
1467
|
+
}
|
|
1468
|
+
if (response.exitCode !== 0) {
|
|
1469
|
+
throw new Error(`Cursor title generation failed with HTTP ${response.exitCode}`);
|
|
1470
|
+
}
|
|
1471
|
+
const payload = decodeConnectUnaryBody(response.body) ?? response.body;
|
|
1472
|
+
const decoded = fromBinary(NameAgentResponseSchema, payload);
|
|
1473
|
+
const title = finalizeTitle(decoded.name) || deriveFallbackTitle(sourceText) || "Untitled Session";
|
|
1474
|
+
const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
1475
|
+
if (stream) {
|
|
1476
|
+
return createBufferedSSETextResponse(modelId, title, usage);
|
|
1477
|
+
}
|
|
1478
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1479
|
+
const created = Math.floor(Date.now() / 1000);
|
|
1480
|
+
return new Response(JSON.stringify({
|
|
1481
|
+
id: completionId,
|
|
1482
|
+
object: "chat.completion",
|
|
1483
|
+
created,
|
|
1484
|
+
model: modelId,
|
|
1485
|
+
choices: [
|
|
1486
|
+
{
|
|
1487
|
+
index: 0,
|
|
1488
|
+
message: { role: "assistant", content: title },
|
|
1489
|
+
finish_reason: "stop",
|
|
1490
|
+
},
|
|
1491
|
+
],
|
|
1492
|
+
usage,
|
|
1493
|
+
}), { headers: { "Content-Type": "application/json" } });
|
|
1494
|
+
}
|
|
1331
1495
|
/** Create an SSE streaming Response that reads from a live bridge. */
|
|
1332
1496
|
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
1333
1497
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
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.194e3412ea47",
|
|
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",
|