@playwo/opencode-cursor-oauth 0.0.0-dev.c1bf17c092a2 → 0.0.0-dev.c1f285cb4d7e
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/index.js +3 -0
- package/dist/proxy.js +469 -68
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -133,6 +133,9 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
133
133
|
return;
|
|
134
134
|
output.headers["x-opencode-session-id"] = incoming.sessionID;
|
|
135
135
|
output.headers["x-session-id"] = incoming.sessionID;
|
|
136
|
+
if (incoming.agent) {
|
|
137
|
+
output.headers["x-opencode-agent"] = incoming.agent;
|
|
138
|
+
}
|
|
136
139
|
},
|
|
137
140
|
};
|
|
138
141
|
};
|
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";
|
|
@@ -41,6 +41,31 @@ function evictStaleConversations() {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
function normalizeAgentKey(agentKey) {
|
|
45
|
+
const trimmed = agentKey?.trim();
|
|
46
|
+
return trimmed ? trimmed : "default";
|
|
47
|
+
}
|
|
48
|
+
function hashString(value) {
|
|
49
|
+
return createHash("sha256").update(value).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
function createStoredConversation() {
|
|
52
|
+
return {
|
|
53
|
+
conversationId: crypto.randomUUID(),
|
|
54
|
+
checkpoint: null,
|
|
55
|
+
blobStore: new Map(),
|
|
56
|
+
lastAccessMs: Date.now(),
|
|
57
|
+
systemPromptHash: "",
|
|
58
|
+
completedTurnsFingerprint: "",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function resetStoredConversation(stored) {
|
|
62
|
+
stored.conversationId = crypto.randomUUID();
|
|
63
|
+
stored.checkpoint = null;
|
|
64
|
+
stored.blobStore = new Map();
|
|
65
|
+
stored.lastAccessMs = Date.now();
|
|
66
|
+
stored.systemPromptHash = "";
|
|
67
|
+
stored.completedTurnsFingerprint = "";
|
|
68
|
+
}
|
|
44
69
|
/** Connect protocol frame: [1-byte flags][4-byte BE length][payload] */
|
|
45
70
|
function frameConnectMessage(data, flags = 0) {
|
|
46
71
|
const frame = Buffer.alloc(5 + data.length);
|
|
@@ -49,6 +74,26 @@ function frameConnectMessage(data, flags = 0) {
|
|
|
49
74
|
frame.set(data, 5);
|
|
50
75
|
return frame;
|
|
51
76
|
}
|
|
77
|
+
function decodeConnectUnaryBody(payload) {
|
|
78
|
+
if (payload.length < 5)
|
|
79
|
+
return null;
|
|
80
|
+
let offset = 0;
|
|
81
|
+
while (offset + 5 <= payload.length) {
|
|
82
|
+
const flags = payload[offset];
|
|
83
|
+
const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
|
|
84
|
+
const messageLength = view.getUint32(1, false);
|
|
85
|
+
const frameEnd = offset + 5 + messageLength;
|
|
86
|
+
if (frameEnd > payload.length)
|
|
87
|
+
return null;
|
|
88
|
+
if ((flags & 0b0000_0001) !== 0)
|
|
89
|
+
return null;
|
|
90
|
+
if ((flags & CONNECT_END_STREAM_FLAG) === 0) {
|
|
91
|
+
return payload.subarray(offset + 5, frameEnd);
|
|
92
|
+
}
|
|
93
|
+
offset = frameEnd;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
52
97
|
function buildCursorHeaders(options, contentType, extra = {}) {
|
|
53
98
|
const headers = new Headers(buildCursorHeaderValues(options, contentType, extra));
|
|
54
99
|
return headers;
|
|
@@ -474,7 +519,8 @@ export async function startProxy(getAccessToken, models = []) {
|
|
|
474
519
|
const sessionId = req.headers.get("x-opencode-session-id")
|
|
475
520
|
?? req.headers.get("x-session-id")
|
|
476
521
|
?? undefined;
|
|
477
|
-
|
|
522
|
+
const agentKey = req.headers.get("x-opencode-agent") ?? undefined;
|
|
523
|
+
return handleChatCompletion(body, accessToken, { sessionId, agentKey });
|
|
478
524
|
}
|
|
479
525
|
catch (err) {
|
|
480
526
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -513,9 +559,24 @@ export function stopProxy() {
|
|
|
513
559
|
conversationStates.clear();
|
|
514
560
|
}
|
|
515
561
|
function handleChatCompletion(body, accessToken, context = {}) {
|
|
516
|
-
const
|
|
562
|
+
const parsed = parseMessages(body.messages);
|
|
563
|
+
const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
|
|
517
564
|
const modelId = body.model;
|
|
518
|
-
const
|
|
565
|
+
const normalizedAgentKey = normalizeAgentKey(context.agentKey);
|
|
566
|
+
const isTitleAgent = normalizedAgentKey === "title";
|
|
567
|
+
if (isTitleAgent) {
|
|
568
|
+
const titleSourceText = buildTitleSourceText(userText, turns, pendingAssistantSummary, toolResults);
|
|
569
|
+
if (!titleSourceText) {
|
|
570
|
+
return new Response(JSON.stringify({
|
|
571
|
+
error: {
|
|
572
|
+
message: "No title source text found",
|
|
573
|
+
type: "invalid_request_error",
|
|
574
|
+
},
|
|
575
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
576
|
+
}
|
|
577
|
+
return handleTitleGenerationRequest(titleSourceText, accessToken, modelId, body.stream !== false);
|
|
578
|
+
}
|
|
579
|
+
const tools = selectToolsForChoice(body.tools ?? [], body.tool_choice);
|
|
519
580
|
if (!userText && toolResults.length === 0) {
|
|
520
581
|
return new Response(JSON.stringify({
|
|
521
582
|
error: {
|
|
@@ -524,16 +585,24 @@ function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
524
585
|
},
|
|
525
586
|
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
526
587
|
}
|
|
527
|
-
// bridgeKey:
|
|
588
|
+
// bridgeKey: session/agent-scoped, for active tool-call bridges
|
|
528
589
|
// convKey: model-independent, for conversation state that survives model switches
|
|
529
|
-
const bridgeKey = deriveBridgeKey(modelId, body.messages, context.sessionId);
|
|
530
|
-
const convKey = deriveConversationKey(body.messages, context.sessionId);
|
|
590
|
+
const bridgeKey = deriveBridgeKey(modelId, body.messages, context.sessionId, context.agentKey);
|
|
591
|
+
const convKey = deriveConversationKey(body.messages, context.sessionId, context.agentKey);
|
|
531
592
|
const activeBridge = activeBridges.get(bridgeKey);
|
|
532
593
|
if (activeBridge && toolResults.length > 0) {
|
|
533
594
|
activeBridges.delete(bridgeKey);
|
|
534
595
|
if (activeBridge.bridge.alive) {
|
|
596
|
+
if (activeBridge.modelId !== modelId) {
|
|
597
|
+
logPluginWarn("Resuming pending Cursor tool call on original model after model switch", {
|
|
598
|
+
requestedModelId: modelId,
|
|
599
|
+
resumedModelId: activeBridge.modelId,
|
|
600
|
+
convKey,
|
|
601
|
+
bridgeKey,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
535
604
|
// Resume the live bridge with tool results
|
|
536
|
-
return handleToolResultResume(activeBridge, toolResults,
|
|
605
|
+
return handleToolResultResume(activeBridge, toolResults, bridgeKey, convKey);
|
|
537
606
|
}
|
|
538
607
|
// Bridge died (timeout, server disconnect, etc.).
|
|
539
608
|
// Clean up and fall through to start a fresh bridge.
|
|
@@ -548,28 +617,49 @@ function handleChatCompletion(body, accessToken, context = {}) {
|
|
|
548
617
|
}
|
|
549
618
|
let stored = conversationStates.get(convKey);
|
|
550
619
|
if (!stored) {
|
|
551
|
-
stored =
|
|
552
|
-
conversationId: crypto.randomUUID(),
|
|
553
|
-
checkpoint: null,
|
|
554
|
-
blobStore: new Map(),
|
|
555
|
-
lastAccessMs: Date.now(),
|
|
556
|
-
};
|
|
620
|
+
stored = createStoredConversation();
|
|
557
621
|
conversationStates.set(convKey, stored);
|
|
558
622
|
}
|
|
623
|
+
const systemPromptHash = hashString(systemPrompt);
|
|
624
|
+
if (stored.checkpoint
|
|
625
|
+
&& (stored.systemPromptHash !== systemPromptHash
|
|
626
|
+
|| (turns.length > 0 && stored.completedTurnsFingerprint !== completedTurnsFingerprint))) {
|
|
627
|
+
resetStoredConversation(stored);
|
|
628
|
+
}
|
|
629
|
+
stored.systemPromptHash = systemPromptHash;
|
|
630
|
+
stored.completedTurnsFingerprint = completedTurnsFingerprint;
|
|
559
631
|
stored.lastAccessMs = Date.now();
|
|
560
632
|
evictStaleConversations();
|
|
561
633
|
// Build the request. When tool results are present but the bridge died,
|
|
562
634
|
// we must still include the last user text so Cursor has context.
|
|
563
635
|
const mcpTools = buildMcpToolDefinitions(tools);
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
636
|
+
const needsInitialHandoff = !stored.checkpoint && (turns.length > 0 || pendingAssistantSummary || toolResults.length > 0);
|
|
637
|
+
const replayTurns = needsInitialHandoff ? [] : turns;
|
|
638
|
+
let effectiveUserText = needsInitialHandoff
|
|
639
|
+
? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
|
|
640
|
+
: toolResults.length > 0
|
|
641
|
+
? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
|
|
642
|
+
: userText;
|
|
643
|
+
const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
|
|
568
644
|
payload.mcpTools = mcpTools;
|
|
569
645
|
if (body.stream === false) {
|
|
570
|
-
return handleNonStreamingResponse(payload, accessToken, modelId, convKey
|
|
646
|
+
return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
|
|
647
|
+
systemPrompt,
|
|
648
|
+
systemPromptHash,
|
|
649
|
+
completedTurnsFingerprint,
|
|
650
|
+
turns,
|
|
651
|
+
userText,
|
|
652
|
+
agentKey: normalizedAgentKey,
|
|
653
|
+
});
|
|
571
654
|
}
|
|
572
|
-
return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey
|
|
655
|
+
return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, {
|
|
656
|
+
systemPrompt,
|
|
657
|
+
systemPromptHash,
|
|
658
|
+
completedTurnsFingerprint,
|
|
659
|
+
turns,
|
|
660
|
+
userText,
|
|
661
|
+
agentKey: normalizedAgentKey,
|
|
662
|
+
});
|
|
573
663
|
}
|
|
574
664
|
/** Normalize OpenAI message content to a plain string. */
|
|
575
665
|
function textContent(content) {
|
|
@@ -584,8 +674,6 @@ function textContent(content) {
|
|
|
584
674
|
}
|
|
585
675
|
function parseMessages(messages) {
|
|
586
676
|
let systemPrompt = "You are a helpful assistant.";
|
|
587
|
-
const pairs = [];
|
|
588
|
-
const toolResults = [];
|
|
589
677
|
// Collect system messages
|
|
590
678
|
const systemParts = messages
|
|
591
679
|
.filter((m) => m.role === "system")
|
|
@@ -593,40 +681,194 @@ function parseMessages(messages) {
|
|
|
593
681
|
if (systemParts.length > 0) {
|
|
594
682
|
systemPrompt = systemParts.join("\n");
|
|
595
683
|
}
|
|
596
|
-
// Separate tool results from conversation turns
|
|
597
684
|
const nonSystem = messages.filter((m) => m.role !== "system");
|
|
598
|
-
|
|
685
|
+
const parsedTurns = [];
|
|
686
|
+
let currentTurn;
|
|
599
687
|
for (const msg of nonSystem) {
|
|
600
|
-
if (msg.role === "
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
688
|
+
if (msg.role === "user") {
|
|
689
|
+
if (currentTurn)
|
|
690
|
+
parsedTurns.push(currentTurn);
|
|
691
|
+
currentTurn = {
|
|
692
|
+
userText: textContent(msg.content),
|
|
693
|
+
segments: [],
|
|
694
|
+
};
|
|
695
|
+
continue;
|
|
605
696
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
pairs.push({ userText: pendingUser, assistantText: "" });
|
|
609
|
-
}
|
|
610
|
-
pendingUser = textContent(msg.content);
|
|
697
|
+
if (!currentTurn) {
|
|
698
|
+
currentTurn = { userText: "", segments: [] };
|
|
611
699
|
}
|
|
612
|
-
|
|
613
|
-
// Skip assistant messages that are just tool_calls with no text
|
|
700
|
+
if (msg.role === "assistant") {
|
|
614
701
|
const text = textContent(msg.content);
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
702
|
+
if (text) {
|
|
703
|
+
currentTurn.segments.push({ kind: "assistantText", text });
|
|
704
|
+
}
|
|
705
|
+
if (msg.tool_calls?.length) {
|
|
706
|
+
currentTurn.segments.push({
|
|
707
|
+
kind: "assistantToolCalls",
|
|
708
|
+
toolCalls: msg.tool_calls,
|
|
709
|
+
});
|
|
618
710
|
}
|
|
711
|
+
continue;
|
|
619
712
|
}
|
|
713
|
+
if (msg.role === "tool") {
|
|
714
|
+
currentTurn.segments.push({
|
|
715
|
+
kind: "toolResult",
|
|
716
|
+
result: {
|
|
717
|
+
toolCallId: msg.tool_call_id ?? "",
|
|
718
|
+
content: textContent(msg.content),
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (currentTurn)
|
|
724
|
+
parsedTurns.push(currentTurn);
|
|
725
|
+
let userText = "";
|
|
726
|
+
let toolResults = [];
|
|
727
|
+
let pendingAssistantSummary = "";
|
|
728
|
+
let completedTurnStates = parsedTurns;
|
|
729
|
+
const lastTurn = parsedTurns.at(-1);
|
|
730
|
+
if (lastTurn) {
|
|
731
|
+
const trailingSegments = splitTrailingToolResults(lastTurn.segments);
|
|
732
|
+
const hasAssistantSummary = trailingSegments.base.length > 0;
|
|
733
|
+
if (trailingSegments.trailing.length > 0 && hasAssistantSummary) {
|
|
734
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
735
|
+
userText = lastTurn.userText;
|
|
736
|
+
toolResults = trailingSegments.trailing.map((segment) => segment.result);
|
|
737
|
+
pendingAssistantSummary = summarizeTurnSegments(trailingSegments.base);
|
|
738
|
+
}
|
|
739
|
+
else if (lastTurn.userText && lastTurn.segments.length === 0) {
|
|
740
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
741
|
+
userText = lastTurn.userText;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const turns = completedTurnStates
|
|
745
|
+
.map((turn) => ({
|
|
746
|
+
userText: turn.userText,
|
|
747
|
+
assistantText: summarizeTurnSegments(turn.segments),
|
|
748
|
+
}))
|
|
749
|
+
.filter((turn) => turn.userText || turn.assistantText);
|
|
750
|
+
return {
|
|
751
|
+
systemPrompt,
|
|
752
|
+
userText,
|
|
753
|
+
turns,
|
|
754
|
+
toolResults,
|
|
755
|
+
pendingAssistantSummary,
|
|
756
|
+
completedTurnsFingerprint: buildCompletedTurnsFingerprint(systemPrompt, turns),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function splitTrailingToolResults(segments) {
|
|
760
|
+
let index = segments.length;
|
|
761
|
+
while (index > 0 && segments[index - 1]?.kind === "toolResult") {
|
|
762
|
+
index -= 1;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
base: segments.slice(0, index),
|
|
766
|
+
trailing: segments.slice(index).filter((segment) => segment.kind === "toolResult"),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function summarizeTurnSegments(segments) {
|
|
770
|
+
const parts = [];
|
|
771
|
+
for (const segment of segments) {
|
|
772
|
+
if (segment.kind === "assistantText") {
|
|
773
|
+
const trimmed = segment.text.trim();
|
|
774
|
+
if (trimmed)
|
|
775
|
+
parts.push(trimmed);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (segment.kind === "assistantToolCalls") {
|
|
779
|
+
const summary = segment.toolCalls.map(formatToolCallSummary).join("\n\n");
|
|
780
|
+
if (summary)
|
|
781
|
+
parts.push(summary);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
parts.push(formatToolResultSummary(segment.result));
|
|
785
|
+
}
|
|
786
|
+
return parts.join("\n\n").trim();
|
|
787
|
+
}
|
|
788
|
+
function formatToolCallSummary(call) {
|
|
789
|
+
const args = call.function.arguments?.trim();
|
|
790
|
+
return args
|
|
791
|
+
? `[assistant requested tool ${call.function.name} id=${call.id}]\n${args}`
|
|
792
|
+
: `[assistant requested tool ${call.function.name} id=${call.id}]`;
|
|
793
|
+
}
|
|
794
|
+
function formatToolResultSummary(result) {
|
|
795
|
+
const label = result.toolCallId
|
|
796
|
+
? `[tool result id=${result.toolCallId}]`
|
|
797
|
+
: "[tool result]";
|
|
798
|
+
const content = result.content.trim();
|
|
799
|
+
return content ? `${label}\n${content}` : label;
|
|
800
|
+
}
|
|
801
|
+
function buildCompletedTurnsFingerprint(systemPrompt, turns) {
|
|
802
|
+
return hashString(JSON.stringify({ systemPrompt, turns }));
|
|
803
|
+
}
|
|
804
|
+
function buildToolResumePrompt(userText, pendingAssistantSummary, toolResults) {
|
|
805
|
+
const parts = [userText.trim()];
|
|
806
|
+
if (pendingAssistantSummary.trim()) {
|
|
807
|
+
parts.push(`[previous assistant tool activity]\n${pendingAssistantSummary.trim()}`);
|
|
808
|
+
}
|
|
809
|
+
if (toolResults.length > 0) {
|
|
810
|
+
parts.push(toolResults.map(formatToolResultSummary).join("\n\n"));
|
|
811
|
+
}
|
|
812
|
+
return parts.filter(Boolean).join("\n\n");
|
|
813
|
+
}
|
|
814
|
+
function buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults) {
|
|
815
|
+
const transcript = turns.map((turn, index) => {
|
|
816
|
+
const sections = [`Turn ${index + 1}`];
|
|
817
|
+
if (turn.userText.trim())
|
|
818
|
+
sections.push(`User: ${turn.userText.trim()}`);
|
|
819
|
+
if (turn.assistantText.trim())
|
|
820
|
+
sections.push(`Assistant: ${turn.assistantText.trim()}`);
|
|
821
|
+
return sections.join("\n");
|
|
822
|
+
});
|
|
823
|
+
const inProgress = buildToolResumePrompt("", pendingAssistantSummary, toolResults).trim();
|
|
824
|
+
const history = [
|
|
825
|
+
...transcript,
|
|
826
|
+
...(inProgress ? [`In-progress turn\n${inProgress}`] : []),
|
|
827
|
+
].join("\n\n").trim();
|
|
828
|
+
if (!history)
|
|
829
|
+
return userText;
|
|
830
|
+
return [
|
|
831
|
+
"[OpenCode session handoff]",
|
|
832
|
+
"You are continuing an existing session that previously ran on another provider/model.",
|
|
833
|
+
"Treat the transcript below as prior conversation history before answering the latest user message.",
|
|
834
|
+
"",
|
|
835
|
+
"<previous-session-transcript>",
|
|
836
|
+
history,
|
|
837
|
+
"</previous-session-transcript>",
|
|
838
|
+
"",
|
|
839
|
+
"Latest user message:",
|
|
840
|
+
userText.trim(),
|
|
841
|
+
].filter(Boolean).join("\n");
|
|
842
|
+
}
|
|
843
|
+
function buildTitleSourceText(userText, turns, pendingAssistantSummary, toolResults) {
|
|
844
|
+
const history = turns.map((turn) => [turn.userText.trim(), turn.assistantText.trim()].filter(Boolean).join("\n")).filter(Boolean);
|
|
845
|
+
if (pendingAssistantSummary.trim()) {
|
|
846
|
+
history.push(pendingAssistantSummary.trim());
|
|
620
847
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
lastUserText = pendingUser;
|
|
848
|
+
if (toolResults.length > 0) {
|
|
849
|
+
history.push(toolResults.map(formatToolResultSummary).join("\n\n"));
|
|
624
850
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
851
|
+
if (userText.trim()) {
|
|
852
|
+
history.push(userText.trim());
|
|
853
|
+
}
|
|
854
|
+
return history.join("\n\n").trim();
|
|
855
|
+
}
|
|
856
|
+
function selectToolsForChoice(tools, toolChoice) {
|
|
857
|
+
if (!tools.length)
|
|
858
|
+
return [];
|
|
859
|
+
if (toolChoice === undefined || toolChoice === null || toolChoice === "auto" || toolChoice === "required") {
|
|
860
|
+
return tools;
|
|
861
|
+
}
|
|
862
|
+
if (toolChoice === "none") {
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
if (typeof toolChoice === "object") {
|
|
866
|
+
const choice = toolChoice;
|
|
867
|
+
if (choice.type === "function" && typeof choice.function?.name === "string") {
|
|
868
|
+
return tools.filter((tool) => tool.function.name === choice.function.name);
|
|
869
|
+
}
|
|
628
870
|
}
|
|
629
|
-
return
|
|
871
|
+
return tools;
|
|
630
872
|
}
|
|
631
873
|
/** Convert OpenAI tool definitions to Cursor's MCP tool protobuf format. */
|
|
632
874
|
function buildMcpToolDefinitions(tools) {
|
|
@@ -1087,31 +1329,34 @@ function sendExecResult(execMsg, messageCase, value, sendFrame) {
|
|
|
1087
1329
|
});
|
|
1088
1330
|
sendFrame(toBinary(AgentClientMessageSchema, clientMessage));
|
|
1089
1331
|
}
|
|
1090
|
-
/** Derive a key for active bridge lookup (tool-call continuations).
|
|
1091
|
-
function deriveBridgeKey(modelId, messages, sessionId) {
|
|
1332
|
+
/** Derive a key for active bridge lookup (tool-call continuations). */
|
|
1333
|
+
function deriveBridgeKey(modelId, messages, sessionId, agentKey) {
|
|
1092
1334
|
if (sessionId) {
|
|
1335
|
+
const normalizedAgent = normalizeAgentKey(agentKey);
|
|
1093
1336
|
return createHash("sha256")
|
|
1094
|
-
.update(`bridge:${sessionId}:${
|
|
1337
|
+
.update(`bridge:${sessionId}:${normalizedAgent}`)
|
|
1095
1338
|
.digest("hex")
|
|
1096
1339
|
.slice(0, 16);
|
|
1097
1340
|
}
|
|
1098
1341
|
const firstUserMsg = messages.find((m) => m.role === "user");
|
|
1099
1342
|
const firstUserText = firstUserMsg ? textContent(firstUserMsg.content) : "";
|
|
1343
|
+
const normalizedAgent = normalizeAgentKey(agentKey);
|
|
1100
1344
|
return createHash("sha256")
|
|
1101
|
-
.update(`bridge:${modelId}:${firstUserText.slice(0, 200)}`)
|
|
1345
|
+
.update(`bridge:${normalizedAgent}:${modelId}:${firstUserText.slice(0, 200)}`)
|
|
1102
1346
|
.digest("hex")
|
|
1103
1347
|
.slice(0, 16);
|
|
1104
1348
|
}
|
|
1105
1349
|
/** Derive a key for conversation state. Model-independent so context survives model switches. */
|
|
1106
|
-
function deriveConversationKey(messages, sessionId) {
|
|
1350
|
+
function deriveConversationKey(messages, sessionId, agentKey) {
|
|
1107
1351
|
if (sessionId) {
|
|
1352
|
+
const normalizedAgent = normalizeAgentKey(agentKey);
|
|
1108
1353
|
return createHash("sha256")
|
|
1109
|
-
.update(`session:${sessionId}`)
|
|
1354
|
+
.update(`session:${sessionId}:${normalizedAgent}`)
|
|
1110
1355
|
.digest("hex")
|
|
1111
1356
|
.slice(0, 16);
|
|
1112
1357
|
}
|
|
1113
1358
|
return createHash("sha256")
|
|
1114
|
-
.update(buildConversationFingerprint(messages))
|
|
1359
|
+
.update(`${normalizeAgentKey(agentKey)}:${buildConversationFingerprint(messages)}`)
|
|
1115
1360
|
.digest("hex")
|
|
1116
1361
|
.slice(0, 16);
|
|
1117
1362
|
}
|
|
@@ -1121,8 +1366,114 @@ function buildConversationFingerprint(messages) {
|
|
|
1121
1366
|
return `${message.role}:${textContent(message.content)}:${message.tool_call_id ?? ""}:${toolCallIDs}`;
|
|
1122
1367
|
}).join("\n---\n");
|
|
1123
1368
|
}
|
|
1369
|
+
function updateStoredConversationAfterCompletion(convKey, metadata, assistantText) {
|
|
1370
|
+
const stored = conversationStates.get(convKey);
|
|
1371
|
+
if (!stored)
|
|
1372
|
+
return;
|
|
1373
|
+
const nextTurns = metadata.userText
|
|
1374
|
+
? [...metadata.turns, { userText: metadata.userText, assistantText: assistantText.trim() }]
|
|
1375
|
+
: metadata.turns;
|
|
1376
|
+
stored.systemPromptHash = metadata.systemPromptHash;
|
|
1377
|
+
stored.completedTurnsFingerprint = buildCompletedTurnsFingerprint(metadata.systemPrompt, nextTurns);
|
|
1378
|
+
stored.lastAccessMs = Date.now();
|
|
1379
|
+
}
|
|
1380
|
+
function deriveFallbackTitle(text) {
|
|
1381
|
+
const cleaned = text
|
|
1382
|
+
.replace(/<[^>]+>/g, " ")
|
|
1383
|
+
.replace(/\[[^\]]+\]/g, " ")
|
|
1384
|
+
.replace(/[^\p{L}\p{N}'’\-\s]+/gu, " ")
|
|
1385
|
+
.replace(/\s+/g, " ")
|
|
1386
|
+
.trim();
|
|
1387
|
+
if (!cleaned)
|
|
1388
|
+
return "";
|
|
1389
|
+
const words = cleaned.split(" ").filter(Boolean).slice(0, 6);
|
|
1390
|
+
return finalizeTitle(words.map(titleCaseWord).join(" "));
|
|
1391
|
+
}
|
|
1392
|
+
function titleCaseWord(word) {
|
|
1393
|
+
if (!word)
|
|
1394
|
+
return word;
|
|
1395
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
1396
|
+
}
|
|
1397
|
+
function finalizeTitle(value) {
|
|
1398
|
+
return value
|
|
1399
|
+
.replace(/^#{1,6}\s*/, "")
|
|
1400
|
+
.replace(/[.!?,:;]+$/g, "")
|
|
1401
|
+
.replace(/\s+/g, " ")
|
|
1402
|
+
.trim()
|
|
1403
|
+
.slice(0, 80)
|
|
1404
|
+
.trim();
|
|
1405
|
+
}
|
|
1406
|
+
function createBufferedSSETextResponse(modelId, text, usage) {
|
|
1407
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1408
|
+
const created = Math.floor(Date.now() / 1000);
|
|
1409
|
+
const payload = [
|
|
1410
|
+
{
|
|
1411
|
+
id: completionId,
|
|
1412
|
+
object: "chat.completion.chunk",
|
|
1413
|
+
created,
|
|
1414
|
+
model: modelId,
|
|
1415
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
id: completionId,
|
|
1419
|
+
object: "chat.completion.chunk",
|
|
1420
|
+
created,
|
|
1421
|
+
model: modelId,
|
|
1422
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
id: completionId,
|
|
1426
|
+
object: "chat.completion.chunk",
|
|
1427
|
+
created,
|
|
1428
|
+
model: modelId,
|
|
1429
|
+
choices: [],
|
|
1430
|
+
usage,
|
|
1431
|
+
},
|
|
1432
|
+
].map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("") + "data: [DONE]\n\n";
|
|
1433
|
+
return new Response(payload, { headers: SSE_HEADERS });
|
|
1434
|
+
}
|
|
1435
|
+
async function handleTitleGenerationRequest(sourceText, accessToken, modelId, stream) {
|
|
1436
|
+
const requestBody = toBinary(NameAgentRequestSchema, create(NameAgentRequestSchema, {
|
|
1437
|
+
userMessage: sourceText,
|
|
1438
|
+
}));
|
|
1439
|
+
const response = await callCursorUnaryRpc({
|
|
1440
|
+
accessToken,
|
|
1441
|
+
rpcPath: "/agent.v1.AgentService/NameAgent",
|
|
1442
|
+
requestBody,
|
|
1443
|
+
timeoutMs: 5_000,
|
|
1444
|
+
});
|
|
1445
|
+
if (response.timedOut) {
|
|
1446
|
+
throw new Error("Cursor title generation timed out");
|
|
1447
|
+
}
|
|
1448
|
+
if (response.exitCode !== 0) {
|
|
1449
|
+
throw new Error(`Cursor title generation failed with HTTP ${response.exitCode}`);
|
|
1450
|
+
}
|
|
1451
|
+
const payload = decodeConnectUnaryBody(response.body) ?? response.body;
|
|
1452
|
+
const decoded = fromBinary(NameAgentResponseSchema, payload);
|
|
1453
|
+
const title = finalizeTitle(decoded.name) || deriveFallbackTitle(sourceText) || "Untitled Session";
|
|
1454
|
+
const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
1455
|
+
if (stream) {
|
|
1456
|
+
return createBufferedSSETextResponse(modelId, title, usage);
|
|
1457
|
+
}
|
|
1458
|
+
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1459
|
+
const created = Math.floor(Date.now() / 1000);
|
|
1460
|
+
return new Response(JSON.stringify({
|
|
1461
|
+
id: completionId,
|
|
1462
|
+
object: "chat.completion",
|
|
1463
|
+
created,
|
|
1464
|
+
model: modelId,
|
|
1465
|
+
choices: [
|
|
1466
|
+
{
|
|
1467
|
+
index: 0,
|
|
1468
|
+
message: { role: "assistant", content: title },
|
|
1469
|
+
finish_reason: "stop",
|
|
1470
|
+
},
|
|
1471
|
+
],
|
|
1472
|
+
usage,
|
|
1473
|
+
}), { headers: { "Content-Type": "application/json" } });
|
|
1474
|
+
}
|
|
1124
1475
|
/** Create an SSE streaming Response that reads from a live bridge. */
|
|
1125
|
-
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey) {
|
|
1476
|
+
function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
|
|
1126
1477
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1127
1478
|
const created = Math.floor(Date.now() / 1000);
|
|
1128
1479
|
const stream = new ReadableStream({
|
|
@@ -1170,6 +1521,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1170
1521
|
totalTokens: 0,
|
|
1171
1522
|
};
|
|
1172
1523
|
const tagFilter = createThinkingTagFilter();
|
|
1524
|
+
let assistantText = metadata.assistantSeedText ?? "";
|
|
1173
1525
|
let mcpExecReceived = false;
|
|
1174
1526
|
let endStreamError = null;
|
|
1175
1527
|
const processChunk = createConnectFrameParser((messageBytes) => {
|
|
@@ -1183,8 +1535,10 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1183
1535
|
const { content, reasoning } = tagFilter.process(text);
|
|
1184
1536
|
if (reasoning)
|
|
1185
1537
|
sendSSE(makeChunk({ reasoning_content: reasoning }));
|
|
1186
|
-
if (content)
|
|
1538
|
+
if (content) {
|
|
1539
|
+
assistantText += content;
|
|
1187
1540
|
sendSSE(makeChunk({ content }));
|
|
1541
|
+
}
|
|
1188
1542
|
}
|
|
1189
1543
|
},
|
|
1190
1544
|
// onMcpExec — the model wants to execute a tool.
|
|
@@ -1194,8 +1548,21 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1194
1548
|
const flushed = tagFilter.flush();
|
|
1195
1549
|
if (flushed.reasoning)
|
|
1196
1550
|
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
1197
|
-
if (flushed.content)
|
|
1551
|
+
if (flushed.content) {
|
|
1552
|
+
assistantText += flushed.content;
|
|
1198
1553
|
sendSSE(makeChunk({ content: flushed.content }));
|
|
1554
|
+
}
|
|
1555
|
+
const assistantSeedText = [
|
|
1556
|
+
assistantText.trim(),
|
|
1557
|
+
formatToolCallSummary({
|
|
1558
|
+
id: exec.toolCallId,
|
|
1559
|
+
type: "function",
|
|
1560
|
+
function: {
|
|
1561
|
+
name: exec.toolName,
|
|
1562
|
+
arguments: exec.decodedArgs,
|
|
1563
|
+
},
|
|
1564
|
+
}),
|
|
1565
|
+
].filter(Boolean).join("\n\n");
|
|
1199
1566
|
const toolCallIndex = state.toolCallIndex++;
|
|
1200
1567
|
sendSSE(makeChunk({
|
|
1201
1568
|
tool_calls: [{
|
|
@@ -1215,6 +1582,11 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1215
1582
|
blobStore,
|
|
1216
1583
|
mcpTools,
|
|
1217
1584
|
pendingExecs: state.pendingExecs,
|
|
1585
|
+
modelId,
|
|
1586
|
+
metadata: {
|
|
1587
|
+
...metadata,
|
|
1588
|
+
assistantSeedText,
|
|
1589
|
+
},
|
|
1218
1590
|
});
|
|
1219
1591
|
sendSSE(makeChunk({}, "tool_calls"));
|
|
1220
1592
|
sendDone();
|
|
@@ -1263,8 +1635,11 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
|
|
|
1263
1635
|
const flushed = tagFilter.flush();
|
|
1264
1636
|
if (flushed.reasoning)
|
|
1265
1637
|
sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
|
|
1266
|
-
if (flushed.content)
|
|
1638
|
+
if (flushed.content) {
|
|
1639
|
+
assistantText += flushed.content;
|
|
1267
1640
|
sendSSE(makeChunk({ content: flushed.content }));
|
|
1641
|
+
}
|
|
1642
|
+
updateStoredConversationAfterCompletion(convKey, metadata, assistantText);
|
|
1268
1643
|
sendSSE(makeChunk({}, "stop"));
|
|
1269
1644
|
sendSSE(makeUsageChunk());
|
|
1270
1645
|
sendDone();
|
|
@@ -1298,13 +1673,20 @@ async function startBridge(accessToken, requestBytes) {
|
|
|
1298
1673
|
const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), 5_000);
|
|
1299
1674
|
return { bridge, heartbeatTimer };
|
|
1300
1675
|
}
|
|
1301
|
-
async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey) {
|
|
1676
|
+
async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
|
|
1302
1677
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
1303
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey);
|
|
1678
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
|
|
1304
1679
|
}
|
|
1305
1680
|
/** Resume a paused bridge by sending MCP results and continuing to stream. */
|
|
1306
|
-
function handleToolResultResume(active, toolResults,
|
|
1307
|
-
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs } = active;
|
|
1681
|
+
function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
|
|
1682
|
+
const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata } = active;
|
|
1683
|
+
const resumeMetadata = {
|
|
1684
|
+
...metadata,
|
|
1685
|
+
assistantSeedText: [
|
|
1686
|
+
metadata.assistantSeedText?.trim() ?? "",
|
|
1687
|
+
toolResults.map(formatToolResultSummary).join("\n\n"),
|
|
1688
|
+
].filter(Boolean).join("\n\n"),
|
|
1689
|
+
};
|
|
1308
1690
|
// Send mcpResult for each pending exec that has a matching tool result
|
|
1309
1691
|
for (const exec of pendingExecs) {
|
|
1310
1692
|
const result = toolResults.find((r) => r.toolCallId === exec.toolCallId);
|
|
@@ -1344,12 +1726,15 @@ function handleToolResultResume(active, toolResults, modelId, bridgeKey, convKey
|
|
|
1344
1726
|
});
|
|
1345
1727
|
bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
|
|
1346
1728
|
}
|
|
1347
|
-
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey);
|
|
1729
|
+
return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
|
|
1348
1730
|
}
|
|
1349
|
-
async function handleNonStreamingResponse(payload, accessToken, modelId, convKey) {
|
|
1731
|
+
async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
1350
1732
|
const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
|
|
1351
1733
|
const created = Math.floor(Date.now() / 1000);
|
|
1352
|
-
const { text, usage } = await collectFullResponse(payload, accessToken, modelId, convKey);
|
|
1734
|
+
const { text, usage, finishReason, toolCalls } = await collectFullResponse(payload, accessToken, modelId, convKey, metadata);
|
|
1735
|
+
const message = finishReason === "tool_calls"
|
|
1736
|
+
? { role: "assistant", content: null, tool_calls: toolCalls }
|
|
1737
|
+
: { role: "assistant", content: text };
|
|
1353
1738
|
return new Response(JSON.stringify({
|
|
1354
1739
|
id: completionId,
|
|
1355
1740
|
object: "chat.completion",
|
|
@@ -1358,17 +1743,18 @@ async function handleNonStreamingResponse(payload, accessToken, modelId, convKey
|
|
|
1358
1743
|
choices: [
|
|
1359
1744
|
{
|
|
1360
1745
|
index: 0,
|
|
1361
|
-
message
|
|
1362
|
-
finish_reason:
|
|
1746
|
+
message,
|
|
1747
|
+
finish_reason: finishReason,
|
|
1363
1748
|
},
|
|
1364
1749
|
],
|
|
1365
1750
|
usage,
|
|
1366
1751
|
}), { headers: { "Content-Type": "application/json" } });
|
|
1367
1752
|
}
|
|
1368
|
-
async function collectFullResponse(payload, accessToken, modelId, convKey) {
|
|
1753
|
+
async function collectFullResponse(payload, accessToken, modelId, convKey, metadata) {
|
|
1369
1754
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
1370
1755
|
let fullText = "";
|
|
1371
1756
|
let endStreamError = null;
|
|
1757
|
+
const pendingToolCalls = [];
|
|
1372
1758
|
const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
|
|
1373
1759
|
const state = {
|
|
1374
1760
|
toolCallIndex: 0,
|
|
@@ -1385,7 +1771,17 @@ async function collectFullResponse(payload, accessToken, modelId, convKey) {
|
|
|
1385
1771
|
return;
|
|
1386
1772
|
const { content } = tagFilter.process(text);
|
|
1387
1773
|
fullText += content;
|
|
1388
|
-
}, () => {
|
|
1774
|
+
}, (exec) => {
|
|
1775
|
+
pendingToolCalls.push({
|
|
1776
|
+
id: exec.toolCallId,
|
|
1777
|
+
type: "function",
|
|
1778
|
+
function: {
|
|
1779
|
+
name: exec.toolName,
|
|
1780
|
+
arguments: exec.decodedArgs,
|
|
1781
|
+
},
|
|
1782
|
+
});
|
|
1783
|
+
scheduleBridgeEnd(bridge);
|
|
1784
|
+
}, (checkpointBytes) => {
|
|
1389
1785
|
const stored = conversationStates.get(convKey);
|
|
1390
1786
|
if (stored) {
|
|
1391
1787
|
stored.checkpoint = checkpointBytes;
|
|
@@ -1421,10 +1817,15 @@ async function collectFullResponse(payload, accessToken, modelId, convKey) {
|
|
|
1421
1817
|
reject(endStreamError);
|
|
1422
1818
|
return;
|
|
1423
1819
|
}
|
|
1820
|
+
if (pendingToolCalls.length === 0) {
|
|
1821
|
+
updateStoredConversationAfterCompletion(convKey, metadata, fullText);
|
|
1822
|
+
}
|
|
1424
1823
|
const usage = computeUsage(state);
|
|
1425
1824
|
resolve({
|
|
1426
1825
|
text: fullText,
|
|
1427
1826
|
usage,
|
|
1827
|
+
finishReason: pendingToolCalls.length > 0 ? "tool_calls" : "stop",
|
|
1828
|
+
toolCalls: pendingToolCalls,
|
|
1428
1829
|
});
|
|
1429
1830
|
});
|
|
1430
1831
|
return promise;
|
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.c1f285cb4d7e",
|
|
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",
|