@playwo/opencode-cursor-oauth 0.0.0-dev.2c48be2f48c9 → 0.0.0-dev.762b07a81479

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.
@@ -13,6 +13,7 @@ interface ParsedMessages {
13
13
  toolResults: ToolResultInfo[];
14
14
  pendingAssistantSummary: string;
15
15
  completedTurnsFingerprint: string;
16
+ assistantContinuation: boolean;
16
17
  }
17
18
  /** Normalize OpenAI message content to a plain string. */
18
19
  export declare function textContent(content: OpenAIMessage["content"]): string;
@@ -64,6 +64,7 @@ export function parseMessages(messages) {
64
64
  let userText = "";
65
65
  let toolResults = [];
66
66
  let pendingAssistantSummary = "";
67
+ let assistantContinuation = false;
67
68
  let completedTurnStates = parsedTurns;
68
69
  const lastTurn = parsedTurns.at(-1);
69
70
  if (lastTurn) {
@@ -79,6 +80,12 @@ export function parseMessages(messages) {
79
80
  completedTurnStates = parsedTurns.slice(0, -1);
80
81
  userText = lastTurn.userText;
81
82
  }
83
+ else if (lastTurn.userText && hasAssistantSummary) {
84
+ completedTurnStates = parsedTurns.slice(0, -1);
85
+ userText = lastTurn.userText;
86
+ pendingAssistantSummary = summarizeTurnSegments(lastTurn.segments);
87
+ assistantContinuation = true;
88
+ }
82
89
  }
83
90
  const turns = completedTurnStates
84
91
  .map((turn) => ({
@@ -93,6 +100,7 @@ export function parseMessages(messages) {
93
100
  toolResults,
94
101
  pendingAssistantSummary,
95
102
  completedTurnsFingerprint: buildCompletedTurnsFingerprint(systemPrompt, turns),
103
+ assistantContinuation,
96
104
  };
97
105
  }
98
106
  function splitTrailingToolResults(segments) {
@@ -38,6 +38,8 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
38
38
  pendingExecs: [],
39
39
  outputTokens: 0,
40
40
  totalTokens: 0,
41
+ interactionToolArgsText: new Map(),
42
+ emittedToolCallIds: new Set(),
41
43
  };
42
44
  const tagFilter = createThinkingTagFilter();
43
45
  bridge.onData(createConnectFrameParser((messageBytes) => {
@@ -58,7 +60,17 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
58
60
  },
59
61
  });
60
62
  scheduleBridgeEnd(bridge);
61
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), (info) => {
63
+ }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
64
+ endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
65
+ logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
66
+ modelId,
67
+ convKey,
68
+ category: info.category,
69
+ caseName: info.caseName,
70
+ detail: info.detail,
71
+ });
72
+ scheduleBridgeEnd(bridge);
73
+ }, (info) => {
62
74
  endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
63
75
  logPluginError("Closing non-streaming Cursor bridge after unsupported exec", {
64
76
  modelId,
@@ -27,6 +27,8 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
27
27
  pendingExecs: [],
28
28
  outputTokens: 0,
29
29
  totalTokens: 0,
30
+ interactionToolArgsText: new Map(),
31
+ emittedToolCallIds: new Set(),
30
32
  };
31
33
  const tagFilter = createThinkingTagFilter();
32
34
  let assistantText = metadata.assistantSeedText ?? "";
@@ -138,7 +140,18 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
138
140
  sendSSE(makeChunk({}, "tool_calls"));
139
141
  sendDone();
140
142
  closeController();
141
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), (info) => {
143
+ }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
144
+ endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
145
+ logPluginError("Closing Cursor bridge after unsupported message", {
146
+ modelId,
147
+ bridgeKey,
148
+ convKey,
149
+ category: info.category,
150
+ caseName: info.caseName,
151
+ detail: info.detail,
152
+ });
153
+ scheduleBridgeEnd(bridge);
154
+ }, (info) => {
142
155
  endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
143
156
  logPluginError("Closing Cursor bridge after unsupported exec", {
144
157
  modelId,
@@ -7,7 +7,7 @@ import { handleNonStreamingResponse, handleStreamingResponse, handleToolResultRe
7
7
  import { handleTitleGenerationRequest } from "./title";
8
8
  export function handleChatCompletion(body, accessToken, context = {}) {
9
9
  const parsed = parseMessages(body.messages);
10
- const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
10
+ const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, assistantContinuation, } = parsed;
11
11
  const modelId = body.model;
12
12
  const normalizedAgentKey = normalizeAgentKey(context.agentKey);
13
13
  const titleDetection = detectTitleRequest(body);
@@ -79,6 +79,14 @@ export function handleChatCompletion(body, accessToken, context = {}) {
79
79
  stored.completedTurnsFingerprint = completedTurnsFingerprint;
80
80
  stored.lastAccessMs = Date.now();
81
81
  evictStaleConversations();
82
+ if (assistantContinuation) {
83
+ return new Response(JSON.stringify({
84
+ error: {
85
+ message: "Assistant-last continuation is not supported by the Cursor provider",
86
+ type: "invalid_request_error",
87
+ },
88
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
89
+ }
82
90
  // Build the request. When tool results are present but the bridge died,
83
91
  // we must still include the last user text so Cursor has context.
84
92
  const mcpTools = buildMcpToolDefinitions(tools);
@@ -3,3 +3,4 @@ export declare function buildCursorRequest(modelId: string, systemPrompt: string
3
3
  userText: string;
4
4
  assistantText: string;
5
5
  }>, conversationId: string, checkpoint: Uint8Array | null, existingBlobStore?: Map<string, Uint8Array>): CursorRequestPayload;
6
+ export declare function buildCursorResumeRequest(modelId: string, systemPrompt: string, conversationId: string, checkpoint: Uint8Array, existingBlobStore?: Map<string, Uint8Array>): CursorRequestPayload;
@@ -1,6 +1,6 @@
1
1
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
2
  import { createHash } from "node:crypto";
3
- import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
3
+ import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
4
4
  export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
5
5
  const blobStore = new Map(existingBlobStore ?? []);
6
6
  // System prompt → blob store (Cursor requests it back via KV handshake)
@@ -64,6 +64,24 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
64
64
  value: create(UserMessageActionSchema, { userMessage }),
65
65
  },
66
66
  });
67
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
68
+ }
69
+ export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
70
+ const blobStore = new Map(existingBlobStore ?? []);
71
+ const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
72
+ const systemBytes = new TextEncoder().encode(systemJson);
73
+ const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
74
+ blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
75
+ const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
76
+ const action = create(ConversationActionSchema, {
77
+ action: {
78
+ case: "resumeAction",
79
+ value: create(ResumeActionSchema, {}),
80
+ },
81
+ });
82
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
83
+ }
84
+ function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
67
85
  const modelDetails = create(ModelDetailsSchema, {
68
86
  modelId,
69
87
  displayModelId: modelId,
@@ -7,6 +7,11 @@ export interface UnhandledExecInfo {
7
7
  execId: string;
8
8
  execMsgId: number;
9
9
  }
10
+ export interface UnsupportedServerMessageInfo {
11
+ category: "agentMessage" | "interactionUpdate" | "interactionQuery" | "execServerControl" | "toolCall";
12
+ caseName: string;
13
+ detail?: string;
14
+ }
10
15
  export declare function parseConnectEndStream(data: Uint8Array): Error | null;
11
16
  export declare function makeHeartbeatBytes(): Uint8Array;
12
17
  export declare function scheduleBridgeEnd(bridge: CursorSession): void;
@@ -34,4 +39,4 @@ export declare function computeUsage(state: StreamState): {
34
39
  completion_tokens: number;
35
40
  total_tokens: number;
36
41
  };
37
- export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
42
+ export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
@@ -128,10 +128,10 @@ export function computeUsage(state) {
128
128
  const prompt_tokens = Math.max(0, total_tokens - completion_tokens);
129
129
  return { prompt_tokens, completion_tokens, total_tokens };
130
130
  }
131
- export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onUnhandledExec) {
131
+ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state, onText, onMcpExec, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
132
132
  const msgCase = msg.message.case;
133
133
  if (msgCase === "interactionUpdate") {
134
- handleInteractionUpdate(msg.message.value, state, onText);
134
+ handleInteractionUpdate(msg.message.value, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage);
135
135
  }
136
136
  else if (msgCase === "kvServerMessage") {
137
137
  handleKvMessage(msg.message.value, blobStore, sendFrame);
@@ -139,6 +139,18 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
139
139
  else if (msgCase === "execServerMessage") {
140
140
  handleExecMessage(msg.message.value, mcpTools, sendFrame, onMcpExec, onUnhandledExec);
141
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
+ onUnsupportedMessage?.({
150
+ category: "interactionQuery",
151
+ caseName: msg.message.value.query.case ?? "undefined",
152
+ });
153
+ }
142
154
  else if (msgCase === "conversationCheckpointUpdate") {
143
155
  const stateStructure = msg.message.value;
144
156
  if (stateStructure.tokenDetails) {
@@ -148,8 +160,14 @@ export function processServerMessage(msg, blobStore, mcpTools, sendFrame, state,
148
160
  onCheckpoint(toBinary(ConversationStateStructureSchema, stateStructure));
149
161
  }
150
162
  }
163
+ else {
164
+ onUnsupportedMessage?.({
165
+ category: "agentMessage",
166
+ caseName: msgCase ?? "undefined",
167
+ });
168
+ }
151
169
  }
152
- function handleInteractionUpdate(update, state, onText) {
170
+ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded, onUnsupportedMessage) {
153
171
  const updateCase = update.message?.case;
154
172
  if (updateCase === "textDelta") {
155
173
  const delta = update.message.value.text || "";
@@ -164,10 +182,82 @@ function handleInteractionUpdate(update, state, onText) {
164
182
  else if (updateCase === "tokenDelta") {
165
183
  state.outputTokens += update.message.value.tokens ?? 0;
166
184
  }
185
+ else if (updateCase === "partialToolCall") {
186
+ const partial = update.message.value;
187
+ if (partial.callId && partial.argsTextDelta) {
188
+ state.interactionToolArgsText.set(partial.callId, partial.argsTextDelta);
189
+ }
190
+ }
191
+ else if (updateCase === "toolCallCompleted") {
192
+ const exec = decodeInteractionToolCall(update.message.value, state);
193
+ if (exec)
194
+ onMcpExec(exec);
195
+ else {
196
+ onUnsupportedMessage?.({
197
+ category: "toolCall",
198
+ caseName: update.message.value?.toolCall?.tool?.case ?? "undefined",
199
+ detail: "toolCallCompleted",
200
+ });
201
+ }
202
+ }
203
+ else if (updateCase === "turnEnded") {
204
+ onTurnEnded?.();
205
+ }
206
+ else if (updateCase === "toolCallStarted" ||
207
+ updateCase === "toolCallDelta" ||
208
+ updateCase === "thinkingCompleted" ||
209
+ updateCase === "userMessageAppended" ||
210
+ updateCase === "summary" ||
211
+ updateCase === "summaryStarted" ||
212
+ updateCase === "summaryCompleted" ||
213
+ updateCase === "heartbeat" ||
214
+ updateCase === "stepStarted" ||
215
+ updateCase === "stepCompleted") {
216
+ return;
217
+ }
218
+ else {
219
+ onUnsupportedMessage?.({
220
+ category: "interactionUpdate",
221
+ caseName: updateCase ?? "undefined",
222
+ });
223
+ }
167
224
  // toolCallStarted, partialToolCall, toolCallDelta, toolCallCompleted
168
225
  // are intentionally ignored. MCP tool calls flow through the exec
169
226
  // message path (mcpArgs → mcpResult), not interaction updates.
170
227
  }
228
+ function decodeInteractionToolCall(update, state) {
229
+ const callId = update.callId ?? "";
230
+ const toolCase = update.toolCall?.tool?.case;
231
+ if (toolCase !== "mcpToolCall")
232
+ return null;
233
+ const mcpArgs = update.toolCall?.tool?.value?.args;
234
+ if (!mcpArgs)
235
+ return null;
236
+ const toolCallId = mcpArgs.toolCallId || callId || crypto.randomUUID();
237
+ if (state.emittedToolCallIds.has(toolCallId))
238
+ return null;
239
+ const decodedMap = decodeMcpArgsMap(mcpArgs.args ?? {});
240
+ const partialArgsText = callId
241
+ ? state.interactionToolArgsText.get(callId)?.trim()
242
+ : undefined;
243
+ let decodedArgs = "{}";
244
+ if (Object.keys(decodedMap).length > 0) {
245
+ decodedArgs = JSON.stringify(decodedMap);
246
+ }
247
+ else if (partialArgsText) {
248
+ decodedArgs = partialArgsText;
249
+ }
250
+ state.emittedToolCallIds.add(toolCallId);
251
+ if (callId)
252
+ state.interactionToolArgsText.delete(callId);
253
+ return {
254
+ execId: callId || toolCallId,
255
+ execMsgId: 0,
256
+ toolCallId,
257
+ toolName: mcpArgs.toolName || mcpArgs.name || "unknown_mcp_tool",
258
+ decodedArgs,
259
+ };
260
+ }
171
261
  /** Send a KV client response back to Cursor. */
172
262
  function sendKvResponse(kvMsg, messageCase, value, sendFrame) {
173
263
  const response = create(KvClientMessageSchema, {
@@ -4,4 +4,6 @@ export interface StreamState {
4
4
  pendingExecs: PendingExec[];
5
5
  outputTokens: number;
6
6
  totalTokens: number;
7
+ interactionToolArgsText: Map<string, string>;
8
+ emittedToolCallIds: Set<string>;
7
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwo/opencode-cursor-oauth",
3
- "version": "0.0.0-dev.2c48be2f48c9",
3
+ "version": "0.0.0-dev.762b07a81479",
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",