@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.
Files changed (2) hide show
  1. package/dist/proxy.js +166 -2
  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
- const effectiveUserText = needsInitialHandoff
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.0cb3e1517254",
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",