@playwo/opencode-cursor-oauth 0.0.0-dev.d7836f7ad39f → 0.0.0-dev.dfb269562f0c

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.
@@ -1,5 +1,4 @@
1
1
  import { type CursorBaseRequestOptions } from "./headers";
2
- export declare function encodeBidiAppendRequest(dataHex: string, requestId: string, appendSeqno: number): Uint8Array;
3
2
  export interface CursorSession {
4
3
  write: (data: Uint8Array) => void;
5
4
  end: () => void;
@@ -8,6 +7,6 @@ export interface CursorSession {
8
7
  readonly alive: boolean;
9
8
  }
10
9
  export interface CreateCursorSessionOptions extends CursorBaseRequestOptions {
11
- requestId: string;
10
+ initialRequestBytes: Uint8Array;
12
11
  }
13
12
  export declare function createCursorSession(options: CreateCursorSessionOptions): Promise<CursorSession>;
@@ -1,149 +1,164 @@
1
- import { create, toBinary } from "@bufbuild/protobuf";
2
- import { BidiRequestIdSchema } from "../proto/agent_pb";
3
- import { CURSOR_API_URL } from "./config";
4
- import { concatBytes, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, frameConnectMessage, toFetchBody, } from "./connect-framing";
5
- import { buildCursorHeaders } from "./headers";
6
- import { errorDetails, logPluginError, logPluginWarn } from "../logger";
7
- export function encodeBidiAppendRequest(dataHex, requestId, appendSeqno) {
8
- const requestIdBytes = toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId }));
9
- return concatBytes([
10
- encodeProtoStringField(1, dataHex),
11
- encodeProtoMessageField(2, requestIdBytes),
12
- encodeProtoVarintField(3, appendSeqno),
13
- ]);
14
- }
1
+ import { connect as connectHttp2, } from "node:http2";
2
+ import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
3
+ import { frameConnectMessage } from "./connect-framing";
4
+ import { buildCursorHeaderValues, } from "./headers";
5
+ import { errorDetails, logPluginError } from "../logger";
6
+ const CURSOR_BIDI_RUN_PATH = "/agent.v1.AgentService/Run";
15
7
  export async function createCursorSession(options) {
16
- const response = await fetch(new URL("/agent.v1.AgentService/RunSSE", options.url ?? CURSOR_API_URL), {
17
- method: "POST",
18
- headers: buildCursorHeaders(options, "application/connect+proto", {
19
- accept: "text/event-stream",
20
- "connect-protocol-version": "1",
21
- }),
22
- body: toFetchBody(frameConnectMessage(toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId: options.requestId })))),
23
- });
24
- if (!response.ok || !response.body) {
25
- const errorBody = await response.text().catch(() => "");
26
- logPluginError("Cursor RunSSE request failed", {
27
- requestId: options.requestId,
28
- status: response.status,
29
- responseBody: errorBody,
30
- });
31
- throw new Error(`RunSSE failed: ${response.status}${errorBody ? ` ${errorBody}` : ""}`);
8
+ if (options.initialRequestBytes.length === 0) {
9
+ throw new Error("Cursor sessions require an initial request message");
32
10
  }
33
- const cbs = {
34
- data: null,
35
- close: null,
36
- };
37
- const abortController = new AbortController();
38
- const reader = response.body.getReader();
39
- let appendSeqno = 0;
40
- let alive = true;
41
- let closeCode = 0;
42
- let writeChain = Promise.resolve();
43
- const pendingChunks = [];
44
- const finish = (code) => {
45
- if (!alive)
46
- return;
47
- alive = false;
48
- closeCode = code;
49
- cbs.close?.(code);
50
- };
51
- const append = async (data) => {
52
- const requestBody = encodeBidiAppendRequest(Buffer.from(data).toString("hex"), options.requestId, appendSeqno++);
53
- const appendResponse = await fetch(new URL("/aiserver.v1.BidiService/BidiAppend", options.url ?? CURSOR_API_URL), {
54
- method: "POST",
55
- headers: buildCursorHeaders(options, "application/proto"),
56
- body: toFetchBody(requestBody),
57
- signal: abortController.signal,
58
- });
59
- if (!appendResponse.ok) {
60
- const errorBody = await appendResponse.text().catch(() => "");
61
- logPluginError("Cursor BidiAppend request failed", {
62
- requestId: options.requestId,
63
- appendSeqno: appendSeqno - 1,
64
- status: appendResponse.status,
65
- responseBody: errorBody,
66
- });
67
- throw new Error(`BidiAppend failed: ${appendResponse.status}${errorBody ? ` ${errorBody}` : ""}`);
68
- }
69
- await appendResponse.arrayBuffer().catch(() => undefined);
70
- };
71
- (async () => {
72
- try {
73
- while (true) {
74
- const { done, value } = await reader.read();
75
- if (done) {
76
- finish(0);
77
- break;
78
- }
79
- if (value && value.length > 0) {
80
- const chunk = Buffer.from(value);
81
- if (cbs.data) {
82
- cbs.data(chunk);
83
- }
84
- else {
85
- pendingChunks.push(chunk);
86
- }
87
- }
11
+ const target = new URL(CURSOR_BIDI_RUN_PATH, options.url ?? CURSOR_API_URL);
12
+ const authority = `${target.protocol}//${target.host}`;
13
+ const requestId = crypto.randomUUID();
14
+ return new Promise((resolve, reject) => {
15
+ const cbs = {
16
+ data: null,
17
+ close: null,
18
+ };
19
+ let session;
20
+ let stream;
21
+ let alive = true;
22
+ let closeCode = 0;
23
+ let opened = false;
24
+ let settled = false;
25
+ let statusCode = 0;
26
+ const pendingChunks = [];
27
+ const errorChunks = [];
28
+ const closeTransport = () => {
29
+ try {
30
+ stream?.close();
88
31
  }
89
- }
90
- catch (error) {
91
- logPluginWarn("Cursor stream reader closed with error", {
92
- requestId: options.requestId,
32
+ catch { }
33
+ try {
34
+ session?.close();
35
+ }
36
+ catch { }
37
+ };
38
+ const finish = (code) => {
39
+ if (!alive)
40
+ return;
41
+ alive = false;
42
+ closeCode = code;
43
+ cbs.close?.(code);
44
+ closeTransport();
45
+ };
46
+ const rejectOpen = (error) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ alive = false;
51
+ closeTransport();
52
+ reject(error);
53
+ };
54
+ const resolveOpen = (sessionHandle) => {
55
+ if (settled)
56
+ return;
57
+ settled = true;
58
+ opened = true;
59
+ resolve(sessionHandle);
60
+ };
61
+ const handleTransportError = (message, error) => {
62
+ logPluginError(message, {
63
+ requestId,
64
+ url: target.toString(),
93
65
  ...errorDetails(error),
94
66
  });
95
- finish(alive ? 1 : closeCode);
96
- }
97
- })();
98
- return {
99
- get alive() {
100
- return alive;
101
- },
102
- write(data) {
103
- if (!alive)
67
+ if (!opened) {
68
+ rejectOpen(new Error(error instanceof Error ? error.message : String(error ?? message)));
104
69
  return;
105
- writeChain = writeChain
106
- .then(() => append(data))
107
- .catch((error) => {
108
- logPluginError("Cursor stream append failed", {
109
- requestId: options.requestId,
110
- ...errorDetails(error),
111
- });
70
+ }
71
+ finish(1);
72
+ };
73
+ const sessionHandle = {
74
+ get alive() {
75
+ return alive;
76
+ },
77
+ write(data) {
78
+ if (!alive || !stream)
79
+ return;
112
80
  try {
113
- abortController.abort();
81
+ stream.write(frameConnectMessage(data));
114
82
  }
115
- catch { }
116
- try {
117
- reader.cancel();
83
+ catch (error) {
84
+ handleTransportError("Cursor HTTP/2 write failed", error);
85
+ }
86
+ },
87
+ end() {
88
+ finish(0);
89
+ },
90
+ onData(cb) {
91
+ cbs.data = cb;
92
+ while (pendingChunks.length > 0) {
93
+ cb(pendingChunks.shift());
94
+ }
95
+ },
96
+ onClose(cb) {
97
+ if (!alive) {
98
+ queueMicrotask(() => cb(closeCode));
118
99
  }
119
- catch { }
120
- finish(1);
100
+ else {
101
+ cbs.close = cb;
102
+ }
103
+ },
104
+ };
105
+ try {
106
+ session = connectHttp2(authority);
107
+ session.once("error", (error) => {
108
+ handleTransportError("Cursor HTTP/2 session failed", error);
121
109
  });
122
- },
123
- end() {
124
- try {
125
- abortController.abort();
126
- }
127
- catch { }
128
- try {
129
- reader.cancel();
130
- }
131
- catch { }
132
- finish(0);
133
- },
134
- onData(cb) {
135
- cbs.data = cb;
136
- while (pendingChunks.length > 0) {
137
- cb(pendingChunks.shift());
138
- }
139
- },
140
- onClose(cb) {
141
- if (!alive) {
142
- queueMicrotask(() => cb(closeCode));
143
- }
144
- else {
145
- cbs.close = cb;
146
- }
147
- },
148
- };
110
+ const headers = {
111
+ ":method": "POST",
112
+ ":path": `${target.pathname}${target.search}`,
113
+ ...buildCursorHeaderValues(options, "application/connect+proto", {
114
+ accept: "application/connect+proto",
115
+ "connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
116
+ }),
117
+ };
118
+ stream = session.request(headers);
119
+ stream.once("response", (responseHeaders) => {
120
+ const statusHeader = responseHeaders[":status"];
121
+ statusCode =
122
+ typeof statusHeader === "number"
123
+ ? statusHeader
124
+ : Number(statusHeader ?? 0);
125
+ if (statusCode >= 200 && statusCode < 300) {
126
+ resolveOpen(sessionHandle);
127
+ }
128
+ });
129
+ stream.on("data", (chunk) => {
130
+ const buffer = Buffer.from(chunk);
131
+ if (!opened && statusCode >= 400) {
132
+ errorChunks.push(buffer);
133
+ return;
134
+ }
135
+ if (cbs.data) {
136
+ cbs.data(buffer);
137
+ }
138
+ else {
139
+ pendingChunks.push(buffer);
140
+ }
141
+ });
142
+ stream.once("end", () => {
143
+ if (!opened) {
144
+ const errorBody = Buffer.concat(errorChunks).toString("utf8").trim();
145
+ logPluginError("Cursor HTTP/2 Run request failed", {
146
+ requestId,
147
+ status: statusCode,
148
+ responseBody: errorBody,
149
+ });
150
+ rejectOpen(new Error(`Run failed: ${statusCode || 1}${errorBody ? ` ${errorBody}` : ""}`));
151
+ return;
152
+ }
153
+ finish(statusCode >= 200 && statusCode < 300 ? 0 : statusCode || 1);
154
+ });
155
+ stream.once("error", (error) => {
156
+ handleTransportError("Cursor HTTP/2 stream failed", error);
157
+ });
158
+ stream.write(frameConnectMessage(options.initialRequestBytes));
159
+ }
160
+ catch (error) {
161
+ handleTransportError("Cursor HTTP/2 transport setup failed", error);
162
+ }
163
+ });
149
164
  }
@@ -1,5 +1,5 @@
1
1
  export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
2
2
  export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
3
3
  export { buildCursorHeaders, buildCursorHeaderValues, type CursorBaseRequestOptions, } from "./headers";
4
- export { createCursorSession, encodeBidiAppendRequest, type CreateCursorSessionOptions, type CursorSession, } from "./bidi-session";
4
+ export { createCursorSession, type CreateCursorSessionOptions, type CursorSession, } from "./bidi-session";
5
5
  export { callCursorUnaryRpc, type CursorUnaryRpcOptions } from "./unary-rpc";
@@ -1,5 +1,5 @@
1
1
  export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
2
2
  export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
3
3
  export { buildCursorHeaders, buildCursorHeaderValues, } from "./headers";
4
- export { createCursorSession, encodeBidiAppendRequest, } from "./bidi-session";
4
+ export { createCursorSession, } from "./bidi-session";
5
5
  export { callCursorUnaryRpc } from "./unary-rpc";
@@ -4,7 +4,6 @@ export interface CursorUnaryRpcOptions {
4
4
  requestBody: Uint8Array;
5
5
  url?: string;
6
6
  timeoutMs?: number;
7
- transport?: "auto" | "fetch" | "http2";
8
7
  }
9
8
  export declare function callCursorUnaryRpc(options: CursorUnaryRpcOptions): Promise<{
10
9
  body: Uint8Array;
@@ -1,67 +1,10 @@
1
1
  import { connect as connectHttp2, } from "node:http2";
2
2
  import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
3
- import { toFetchBody } from "./connect-framing";
4
- import { buildCursorHeaders, buildCursorHeaderValues } from "./headers";
3
+ import { buildCursorHeaderValues } from "./headers";
5
4
  import { errorDetails, logPluginError } from "../logger";
6
5
  export async function callCursorUnaryRpc(options) {
7
6
  const target = new URL(options.rpcPath, options.url ?? CURSOR_API_URL);
8
- const transport = options.transport ?? "auto";
9
- if (transport === "http2" ||
10
- (transport === "auto" && target.protocol === "https:")) {
11
- const http2Result = await callCursorUnaryRpcOverHttp2(options, target);
12
- if (transport === "http2" ||
13
- http2Result.timedOut ||
14
- http2Result.exitCode !== 1) {
15
- return http2Result;
16
- }
17
- }
18
- return callCursorUnaryRpcOverFetch(options, target);
19
- }
20
- async function callCursorUnaryRpcOverFetch(options, target) {
21
- let timedOut = false;
22
- const timeoutMs = options.timeoutMs ?? 5_000;
23
- const controller = new AbortController();
24
- const timeout = timeoutMs > 0
25
- ? setTimeout(() => {
26
- timedOut = true;
27
- controller.abort();
28
- }, timeoutMs)
29
- : undefined;
30
- try {
31
- const response = await fetch(target, {
32
- method: "POST",
33
- headers: buildCursorHeaders(options, "application/proto", {
34
- accept: "application/proto, application/json",
35
- "connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
36
- "connect-timeout-ms": String(timeoutMs),
37
- }),
38
- body: toFetchBody(options.requestBody),
39
- signal: controller.signal,
40
- });
41
- const body = new Uint8Array(await response.arrayBuffer());
42
- return {
43
- body,
44
- exitCode: response.ok ? 0 : response.status,
45
- timedOut,
46
- };
47
- }
48
- catch {
49
- logPluginError("Cursor unary fetch transport failed", {
50
- rpcPath: options.rpcPath,
51
- url: target.toString(),
52
- timeoutMs,
53
- timedOut,
54
- });
55
- return {
56
- body: new Uint8Array(),
57
- exitCode: timedOut ? 124 : 1,
58
- timedOut,
59
- };
60
- }
61
- finally {
62
- if (timeout)
63
- clearTimeout(timeout);
64
- }
7
+ return callCursorUnaryRpcOverHttp2(options, target);
65
8
  }
66
9
  async function callCursorUnaryRpcOverHttp2(options, target) {
67
10
  const timeoutMs = options.timeoutMs ?? 5_000;
@@ -13,7 +13,6 @@ interface ParsedMessages {
13
13
  toolResults: ToolResultInfo[];
14
14
  pendingAssistantSummary: string;
15
15
  completedTurnsFingerprint: string;
16
- assistantContinuation: boolean;
17
16
  }
18
17
  /** Normalize OpenAI message content to a plain string. */
19
18
  export declare function textContent(content: OpenAIMessage["content"]): string;
@@ -64,7 +64,6 @@ export function parseMessages(messages) {
64
64
  let userText = "";
65
65
  let toolResults = [];
66
66
  let pendingAssistantSummary = "";
67
- let assistantContinuation = false;
68
67
  let completedTurnStates = parsedTurns;
69
68
  const lastTurn = parsedTurns.at(-1);
70
69
  if (lastTurn) {
@@ -84,7 +83,6 @@ export function parseMessages(messages) {
84
83
  completedTurnStates = parsedTurns.slice(0, -1);
85
84
  userText = lastTurn.userText;
86
85
  pendingAssistantSummary = summarizeTurnSegments(lastTurn.segments);
87
- assistantContinuation = true;
88
86
  }
89
87
  }
90
88
  const turns = completedTurnStates
@@ -100,7 +98,6 @@ export function parseMessages(messages) {
100
98
  toolResults,
101
99
  pendingAssistantSummary,
102
100
  completedTurnsFingerprint: buildCompletedTurnsFingerprint(systemPrompt, turns),
103
- assistantContinuation,
104
101
  };
105
102
  }
106
103
  function splitTrailingToolResults(segments) {
@@ -110,7 +110,6 @@ export const CursorAuthPlugin = async (input) => {
110
110
  async "chat.headers"(incoming, output) {
111
111
  if (incoming.model.providerID !== CURSOR_PROVIDER_ID)
112
112
  return;
113
- output.headers["x-opencode-session-id"] = incoming.sessionID;
114
113
  output.headers["x-session-id"] = incoming.sessionID;
115
114
  if (incoming.agent) {
116
115
  output.headers["x-opencode-agent"] = incoming.agent;
@@ -2,12 +2,10 @@ import { createCursorSession } from "../cursor/bidi-session";
2
2
  import { makeHeartbeatBytes } from "./stream-dispatch";
3
3
  const HEARTBEAT_INTERVAL_MS = 5_000;
4
4
  export async function startBridge(accessToken, requestBytes) {
5
- const requestId = crypto.randomUUID();
6
5
  const bridge = await createCursorSession({
7
6
  accessToken,
8
- requestId,
7
+ initialRequestBytes: requestBytes,
9
8
  });
10
- bridge.write(requestBytes);
11
9
  const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), HEARTBEAT_INTERVAL_MS);
12
10
  return { bridge, heartbeatTimer };
13
11
  }
@@ -49,6 +49,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
49
49
  return;
50
50
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
51
51
  };
52
+ const failStream = (message, code) => {
53
+ if (closed)
54
+ return;
55
+ sendSSE({
56
+ error: {
57
+ message,
58
+ type: "server_error",
59
+ ...(code ? { code } : {}),
60
+ },
61
+ });
62
+ sendDone();
63
+ closeController();
64
+ };
52
65
  const closeController = () => {
53
66
  if (closed)
54
67
  return;
@@ -200,10 +213,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
200
213
  syncStoredBlobStore(convKey, blobStore);
201
214
  if (endStreamError) {
202
215
  activeBridges.delete(bridgeKey);
203
- if (!closed) {
204
- closed = true;
205
- controller.error(endStreamError);
206
- }
216
+ failStream(endStreamError.message, "cursor_bridge_closed");
207
217
  return;
208
218
  }
209
219
  if (!mcpExecReceived) {
@@ -223,11 +233,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
223
233
  }
224
234
  activeBridges.delete(bridgeKey);
225
235
  if (code !== 0 && !closed) {
226
- sendSSE(makeChunk({ content: "\n[Error: bridge connection lost]" }));
227
- sendSSE(makeChunk({}, "stop"));
228
- sendSSE(makeUsageChunk());
229
- sendDone();
230
- closeController();
236
+ failStream("Cursor bridge connection lost", "cursor_bridge_closed");
231
237
  }
232
238
  });
233
239
  },
@@ -2,12 +2,12 @@ import { logPluginWarn } from "../logger";
2
2
  import { buildInitialHandoffPrompt, buildTitleSourceText, buildToolResumePrompt, detectTitleRequest, parseMessages, } from "../openai/messages";
3
3
  import { buildMcpToolDefinitions, selectToolsForChoice } from "../openai/tools";
4
4
  import { activeBridges, conversationStates, createStoredConversation, deriveBridgeKey, deriveConversationKey, evictStaleConversations, hashString, normalizeAgentKey, resetStoredConversation, } from "./conversation-state";
5
- import { buildCursorRequest, buildCursorResumeRequest } from "./cursor-request";
5
+ import { buildCursorRequest } from "./cursor-request";
6
6
  import { handleNonStreamingResponse, handleStreamingResponse, handleToolResultResume, } from "./bridge";
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, assistantContinuation, } = parsed;
10
+ const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
11
11
  const modelId = body.model;
12
12
  const normalizedAgentKey = normalizeAgentKey(context.agentKey);
13
13
  const titleDetection = detectTitleRequest(body);
@@ -79,40 +79,16 @@ export function handleChatCompletion(body, accessToken, context = {}) {
79
79
  stored.completedTurnsFingerprint = completedTurnsFingerprint;
80
80
  stored.lastAccessMs = Date.now();
81
81
  evictStaleConversations();
82
- if (assistantContinuation) {
83
- if (!stored.checkpoint) {
84
- return new Response(JSON.stringify({
85
- error: {
86
- message: "Assistant-last continuation requires an existing Cursor checkpoint",
87
- type: "invalid_request_error",
88
- },
89
- }), { status: 400, headers: { "Content-Type": "application/json" } });
90
- }
91
- const payload = buildCursorResumeRequest(modelId, systemPrompt, stored.conversationId, stored.checkpoint, stored.blobStore);
92
- payload.mcpTools = buildMcpToolDefinitions(tools);
93
- const metadata = {
94
- systemPrompt,
95
- systemPromptHash,
96
- completedTurnsFingerprint,
97
- turns,
98
- userText,
99
- assistantSeedText: pendingAssistantSummary,
100
- agentKey: normalizedAgentKey,
101
- };
102
- if (body.stream === false) {
103
- return handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata);
104
- }
105
- return handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata);
106
- }
107
82
  // Build the request. When tool results are present but the bridge died,
108
83
  // we must still include the last user text so Cursor has context.
109
84
  const mcpTools = buildMcpToolDefinitions(tools);
85
+ const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
110
86
  const needsInitialHandoff = !stored.checkpoint &&
111
- (turns.length > 0 || pendingAssistantSummary || toolResults.length > 0);
87
+ (turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
112
88
  const replayTurns = needsInitialHandoff ? [] : turns;
113
89
  let effectiveUserText = needsInitialHandoff
114
90
  ? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
115
- : toolResults.length > 0
91
+ : toolResults.length > 0 || hasPendingAssistantSummary
116
92
  ? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
117
93
  : userText;
118
94
  const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
@@ -1,4 +1,4 @@
1
- import { errorDetails, logPluginError } from "../logger";
1
+ import { errorDetails, logPluginError, logPluginWarn } from "../logger";
2
2
  import { handleChatCompletion } from "./chat-completion";
3
3
  import { activeBridges, conversationStates } from "./conversation-state";
4
4
  let proxyServer;
@@ -42,14 +42,32 @@ export async function startProxy(getAccessToken, models = []) {
42
42
  throw new Error("Cursor proxy access token provider not configured");
43
43
  }
44
44
  const accessToken = await proxyAccessTokenProvider();
45
- const sessionId = req.headers.get("x-opencode-session-id") ??
46
- req.headers.get("x-session-id") ??
47
- undefined;
45
+ const sessionId = req.headers.get("x-session-id") ?? undefined;
48
46
  const agentKey = req.headers.get("x-opencode-agent") ?? undefined;
49
- return handleChatCompletion(body, accessToken, {
47
+ const response = await handleChatCompletion(body, accessToken, {
50
48
  sessionId,
51
49
  agentKey,
52
50
  });
51
+ if (response.status >= 400) {
52
+ let responseBody = "";
53
+ try {
54
+ responseBody = await response.clone().text();
55
+ }
56
+ catch (error) {
57
+ responseBody = `Failed to read rejected response body: ${error instanceof Error ? error.message : String(error)}`;
58
+ }
59
+ logPluginWarn("Rejected Cursor chat completion", {
60
+ path: url.pathname,
61
+ method: req.method,
62
+ sessionId,
63
+ agentKey,
64
+ status: response.status,
65
+ requestBody: body,
66
+ requestBodyText: JSON.stringify(body),
67
+ responseBody,
68
+ });
69
+ }
70
+ return response;
53
71
  }
54
72
  catch (err) {
55
73
  const message = err instanceof Error ? err.message : String(err);
@@ -192,13 +192,6 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
192
192
  const exec = decodeInteractionToolCall(update.message.value, state);
193
193
  if (exec)
194
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
195
  }
203
196
  else if (updateCase === "turnEnded") {
204
197
  onTurnEnded?.();
@@ -221,9 +214,10 @@ function handleInteractionUpdate(update, state, onText, onMcpExec, onTurnEnded,
221
214
  caseName: updateCase ?? "undefined",
222
215
  });
223
216
  }
224
- // toolCallStarted, partialToolCall, toolCallDelta, toolCallCompleted
225
- // are intentionally ignored. MCP tool calls flow through the exec
226
- // message path (mcpArgs mcpResult), not interaction updates.
217
+ // toolCallStarted, partialToolCall, toolCallDelta, and non-MCP
218
+ // toolCallCompleted updates are informational only. Actionable MCP tool
219
+ // calls may still appear here on some models, so we surface those, but we
220
+ // do not abort the bridge for native Cursor tool-call progress events.
227
221
  }
228
222
  function decodeInteractionToolCall(update, state) {
229
223
  const callId = update.callId ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playwo/opencode-cursor-oauth",
3
- "version": "0.0.0-dev.d7836f7ad39f",
3
+ "version": "0.0.0-dev.dfb269562f0c",
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",