@playwo/opencode-cursor-oauth 0.0.0-dev.4696faa690e4 → 0.0.0-dev.4a26f78d4622

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.
@@ -6,7 +6,6 @@ import { startBridge } from "./bridge-session";
6
6
  import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
7
7
  import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
8
8
  import { createBridgeCloseController } from "./bridge-close-controller";
9
- const MCP_TOOL_BATCH_WINDOW_MS = 150;
10
9
  export async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
11
10
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
12
11
  const created = Math.floor(Date.now() / 1000);
@@ -34,19 +33,8 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
34
33
  let fullText = "";
35
34
  let endStreamError = null;
36
35
  const pendingToolCalls = [];
37
- let toolCallEndTimer;
38
36
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
39
37
  const bridgeCloseController = createBridgeCloseController(bridge);
40
- const stopToolCallEndTimer = () => {
41
- if (!toolCallEndTimer)
42
- return;
43
- clearTimeout(toolCallEndTimer);
44
- toolCallEndTimer = undefined;
45
- };
46
- const scheduleToolCallBridgeEnd = () => {
47
- stopToolCallEndTimer();
48
- toolCallEndTimer = setTimeout(() => scheduleBridgeEnd(bridge), MCP_TOOL_BATCH_WINDOW_MS);
49
- };
50
38
  const state = {
51
39
  toolCallIndex: 0,
52
40
  pendingExecs: [],
@@ -78,11 +66,18 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
78
66
  else {
79
67
  pendingToolCalls.push(toolCall);
80
68
  }
81
- scheduleToolCallBridgeEnd();
82
- }, (_info) => { }, (checkpointBytes) => {
69
+ }, (_info) => { }, undefined, (checkpointBytes) => {
83
70
  updateConversationCheckpoint(convKey, checkpointBytes);
84
71
  bridgeCloseController.noteCheckpoint();
85
- }, () => bridgeCloseController.noteTurnEnded(), (info) => {
72
+ if (pendingToolCalls.length > 0) {
73
+ scheduleBridgeEnd(bridge);
74
+ }
75
+ }, () => {
76
+ bridgeCloseController.noteTurnEnded();
77
+ if (pendingToolCalls.length > 0) {
78
+ scheduleBridgeEnd(bridge);
79
+ }
80
+ }, (info) => {
86
81
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
87
82
  logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
88
83
  modelId,
@@ -120,7 +115,6 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
120
115
  }));
121
116
  bridge.onClose(() => {
122
117
  bridgeCloseController.dispose();
123
- stopToolCallEndTimer();
124
118
  clearInterval(heartbeatTimer);
125
119
  syncStoredBlobStore(convKey, payload.blobStore);
126
120
  const flushed = tagFilter.flush();
@@ -9,7 +9,6 @@ import { SSE_HEADERS } from "./sse";
9
9
  import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
10
10
  import { createBridgeCloseController } from "./bridge-close-controller";
11
11
  const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
12
- const MCP_TOOL_BATCH_WINDOW_MS = 150;
13
12
  function sortedIds(values) {
14
13
  return [...values].sort();
15
14
  }
@@ -17,7 +16,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
17
16
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
18
17
  const created = Math.floor(Date.now() / 1000);
19
18
  let keepaliveTimer;
20
- let toolCallFlushTimer;
21
19
  const bridgeCloseController = createBridgeCloseController(bridge);
22
20
  const stopKeepalive = () => {
23
21
  if (!keepaliveTimer)
@@ -25,12 +23,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
25
23
  clearInterval(keepaliveTimer);
26
24
  keepaliveTimer = undefined;
27
25
  };
28
- const stopToolCallFlushTimer = () => {
29
- if (!toolCallFlushTimer)
30
- return;
31
- clearTimeout(toolCallFlushTimer);
32
- toolCallFlushTimer = undefined;
33
- };
34
26
  const stream = new ReadableStream({
35
27
  start(controller) {
36
28
  const encoder = new TextEncoder();
@@ -80,7 +72,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
80
72
  return;
81
73
  closed = true;
82
74
  stopKeepalive();
83
- stopToolCallFlushTimer();
84
75
  controller.close();
85
76
  };
86
77
  const makeChunk = (delta, finishReason = null) => ({
@@ -147,20 +138,20 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
147
138
  ],
148
139
  }));
149
140
  };
150
- const publishPendingToolCalls = () => {
141
+ const publishPendingToolCalls = (source) => {
151
142
  if (closed || toolCallsFlushed || state.pendingExecs.length === 0)
152
143
  return;
153
144
  logPluginInfo("Evaluating Cursor tool-call publish", {
154
145
  modelId,
155
146
  bridgeKey,
156
147
  convKey,
148
+ source,
157
149
  pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
158
150
  toolCallsFlushed,
159
151
  mcpExecReceived,
160
152
  nowMs: Date.now(),
161
153
  });
162
154
  toolCallsFlushed = true;
163
- stopToolCallFlushTimer();
164
155
  const flushed = tagFilter.flush();
165
156
  if (flushed.reasoning)
166
157
  sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
@@ -206,6 +197,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
206
197
  modelId,
207
198
  bridgeKey,
208
199
  convKey,
200
+ source,
209
201
  pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
210
202
  emittedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
211
203
  assistantSeedTextLength: assistantSeedText.length,
@@ -235,19 +227,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
235
227
  sendDone();
236
228
  closeController();
237
229
  };
238
- const schedulePendingToolCallPublish = () => {
239
- if (toolCallsFlushed)
240
- return;
241
- stopToolCallFlushTimer();
242
- logPluginInfo("Scheduling Cursor tool-call publish", {
243
- modelId,
244
- bridgeKey,
245
- convKey,
246
- delayMs: MCP_TOOL_BATCH_WINDOW_MS,
247
- pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
248
- });
249
- toolCallFlushTimer = setTimeout(publishPendingToolCalls, MCP_TOOL_BATCH_WINDOW_MS);
250
- };
251
230
  const processChunk = createConnectFrameParser((messageBytes) => {
252
231
  try {
253
232
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
@@ -309,7 +288,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
309
288
  ? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
310
289
  : [],
311
290
  });
312
- schedulePendingToolCallPublish();
313
291
  }, (info) => {
314
292
  if (toolCallsFlushed) {
315
293
  logPluginWarn("Received Cursor MCP interaction update after tool-call envelope was already published", {
@@ -335,20 +313,62 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
335
313
  lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
336
314
  };
337
315
  }
338
- logPluginInfo("Tracking Cursor MCP interaction state in streaming bridge", {
316
+ }, (info) => {
317
+ const existingActiveBridge = activeBridges.get(bridgeKey);
318
+ if (existingActiveBridge) {
319
+ existingActiveBridge.diagnostics = {
320
+ announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
321
+ publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
322
+ lastMcpUpdate: info.updateCase === "stepCompleted"
323
+ ? `${info.updateCase}:${info.stepId}:${info.stepDurationMs ?? "unknown"}`
324
+ : `${info.updateCase}:${info.stepId}`,
325
+ publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
326
+ lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
327
+ };
328
+ }
329
+ logPluginInfo("Tracking Cursor step boundary in streaming bridge", {
339
330
  modelId,
340
331
  bridgeKey,
341
332
  convKey,
342
333
  updateCase: info.updateCase,
343
- toolCallId: info.toolCallId,
334
+ stepId: info.stepId,
335
+ stepDurationMs: info.stepDurationMs,
336
+ toolCallsFlushed,
337
+ mcpExecReceived,
344
338
  pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
339
+ streamedToolCallIds: sortedIds(streamedToolCalls.keys()),
345
340
  hasStoredActiveBridge: Boolean(existingActiveBridge),
341
+ storedActiveBridgePendingExecToolCallIds: existingActiveBridge
342
+ ? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
343
+ : [],
346
344
  storedActiveBridgeDiagnostics: existingActiveBridge?.diagnostics,
347
345
  });
348
346
  }, (checkpointBytes) => {
347
+ logPluginInfo("Received Cursor conversation checkpoint", {
348
+ modelId,
349
+ bridgeKey,
350
+ convKey,
351
+ toolCallsFlushed,
352
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
353
+ });
349
354
  updateConversationCheckpoint(convKey, checkpointBytes);
350
355
  bridgeCloseController.noteCheckpoint();
351
- }, () => bridgeCloseController.noteTurnEnded(), (info) => {
356
+ if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
357
+ publishPendingToolCalls("checkpoint");
358
+ }
359
+ }, () => {
360
+ logPluginInfo("Received Cursor turn-ended signal", {
361
+ modelId,
362
+ bridgeKey,
363
+ convKey,
364
+ toolCallsFlushed,
365
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
366
+ });
367
+ bridgeCloseController.noteTurnEnded();
368
+ if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
369
+ publishPendingToolCalls("turnEnded");
370
+ }
371
+ }, (info) => {
352
372
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
353
373
  logPluginError("Closing Cursor bridge after unsupported message", {
354
374
  modelId,
@@ -376,6 +396,15 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
376
396
  // Skip unparseable messages.
377
397
  }
378
398
  }, (endStreamBytes) => {
399
+ logPluginInfo("Received Cursor end-of-stream signal", {
400
+ modelId,
401
+ bridgeKey,
402
+ convKey,
403
+ byteLength: endStreamBytes.length,
404
+ });
405
+ if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
406
+ publishPendingToolCalls("endStream");
407
+ }
379
408
  endStreamError = parseConnectEndStream(endStreamBytes);
380
409
  if (endStreamError) {
381
410
  logPluginError("Cursor stream returned Connect end-stream error", {
@@ -423,7 +452,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
423
452
  bridgeCloseController.dispose();
424
453
  clearInterval(heartbeatTimer);
425
454
  stopKeepalive();
426
- stopToolCallFlushTimer();
427
455
  syncStoredBlobStore(convKey, blobStore);
428
456
  if (endStreamError) {
429
457
  activeBridges.delete(bridgeKey);
@@ -454,7 +482,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule
454
482
  cancel(reason) {
455
483
  bridgeCloseController.dispose();
456
484
  stopKeepalive();
457
- stopToolCallFlushTimer();
458
485
  clearInterval(heartbeatTimer);
459
486
  syncStoredBlobStore(convKey, blobStore);
460
487
  const active = activeBridges.get(bridgeKey);
@@ -18,6 +18,11 @@ export interface McpToolCallUpdateInfo {
18
18
  modelCallId?: string;
19
19
  toolName?: string;
20
20
  }
21
+ export interface StepUpdateInfo {
22
+ updateCase: "stepStarted" | "stepCompleted";
23
+ stepId: string;
24
+ stepDurationMs?: string;
25
+ }
21
26
  export declare function parseConnectEndStream(data: Uint8Array): Error | null;
22
27
  export declare function makeHeartbeatBytes(): Uint8Array;
23
28
  export declare function scheduleBridgeEnd(bridge: CursorSession): void;
@@ -45,4 +50,4 @@ export declare function computeUsage(state: StreamState): {
45
50
  completion_tokens: number;
46
51
  total_tokens: number;
47
52
  };
48
- export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, cloudRule: string | undefined, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onMcpToolCallUpdate?: (info: McpToolCallUpdateInfo) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
53
+ export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, cloudRule: string | undefined, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onMcpToolCallUpdate?: (info: McpToolCallUpdateInfo) => void, onStepUpdate?: (info: StepUpdateInfo) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
@@ -128,10 +128,25 @@ 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, cloudRule, mcpTools, sendFrame, state, onText, onMcpExec, onMcpToolCallUpdate, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
131
+ export function processServerMessage(msg, blobStore, cloudRule, mcpTools, sendFrame, state, onText, onMcpExec, onMcpToolCallUpdate, onStepUpdate, onCheckpoint, onTurnEnded, onUnsupportedMessage, onUnhandledExec) {
132
132
  const msgCase = msg.message.case;
133
+ logPluginInfo("Received Cursor server signal", {
134
+ messageCase: msgCase ?? "undefined",
135
+ interactionCase: msgCase === "interactionUpdate"
136
+ ? msg.message.value.message?.case ?? "undefined"
137
+ : undefined,
138
+ execCase: msgCase === "execServerMessage"
139
+ ? msg.message.value.message?.case ?? "undefined"
140
+ : undefined,
141
+ interactionQueryCase: msgCase === "interactionQuery"
142
+ ? msg.message.value.query?.case ?? "undefined"
143
+ : undefined,
144
+ kvCase: msgCase === "kvServerMessage"
145
+ ? msg.message.value.message?.case ?? "undefined"
146
+ : undefined,
147
+ });
133
148
  if (msgCase === "interactionUpdate") {
134
- handleInteractionUpdate(msg.message.value, state, onText, onMcpToolCallUpdate, onTurnEnded, onUnsupportedMessage);
149
+ handleInteractionUpdate(msg.message.value, state, onText, onMcpToolCallUpdate, onStepUpdate, onTurnEnded, onUnsupportedMessage);
135
150
  }
136
151
  else if (msgCase === "kvServerMessage") {
137
152
  handleKvMessage(msg.message.value, blobStore, sendFrame);
@@ -164,19 +179,8 @@ export function processServerMessage(msg, blobStore, cloudRule, mcpTools, sendFr
164
179
  });
165
180
  }
166
181
  }
167
- function handleInteractionUpdate(update, state, onText, onMcpToolCallUpdate, onTurnEnded, onUnsupportedMessage) {
182
+ function handleInteractionUpdate(update, state, onText, onMcpToolCallUpdate, onStepUpdate, onTurnEnded, onUnsupportedMessage) {
168
183
  const updateCase = update.message?.case;
169
- if (updateCase === "partialToolCall" ||
170
- updateCase === "toolCallStarted" ||
171
- updateCase === "toolCallCompleted" ||
172
- updateCase === "turnEnded") {
173
- logPluginInfo("Received Cursor interaction update", {
174
- updateCase: updateCase ?? "undefined",
175
- callId: update.message?.value?.callId,
176
- modelCallId: update.message?.value?.modelCallId,
177
- toolCase: update.message?.value?.toolCall?.tool?.case,
178
- });
179
- }
180
184
  if ((updateCase === "partialToolCall" ||
181
185
  updateCase === "toolCallStarted" ||
182
186
  updateCase === "toolCallCompleted") &&
@@ -193,6 +197,19 @@ function handleInteractionUpdate(update, state, onText, onMcpToolCallUpdate, onT
193
197
  });
194
198
  }
195
199
  }
200
+ if (updateCase === "stepStarted" || updateCase === "stepCompleted") {
201
+ const stepValue = update.message?.value;
202
+ const stepId = stepValue?.stepId;
203
+ if (stepId !== undefined && stepId !== null) {
204
+ onStepUpdate?.({
205
+ updateCase,
206
+ stepId: String(stepId),
207
+ stepDurationMs: updateCase === "stepCompleted" && stepValue?.stepDurationMs !== undefined
208
+ ? String(stepValue.stepDurationMs)
209
+ : undefined,
210
+ });
211
+ }
212
+ }
196
213
  if (updateCase === "textDelta") {
197
214
  const delta = update.message.value.text || "";
198
215
  if (delta)
@@ -372,11 +389,6 @@ function handleKvMessage(kvMsg, blobStore, sendFrame) {
372
389
  }
373
390
  function handleExecMessage(execMsg, cloudRule, mcpTools, sendFrame, state, onMcpExec, onUnhandledExec) {
374
391
  const execCase = execMsg.message.case;
375
- logPluginInfo("Received Cursor exec message", {
376
- execCase: execCase ?? "undefined",
377
- execId: execMsg.execId,
378
- execMsgId: execMsg.id,
379
- });
380
392
  if (execCase === "requestContextArgs") {
381
393
  logPluginInfo("Responding to Cursor requestContextArgs", {
382
394
  execId: execMsg.execId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwo/opencode-cursor-oauth",
3
- "version": "0.0.0-dev.4696faa690e4",
3
+ "version": "0.0.0-dev.4a26f78d4622",
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",