@playwo/opencode-cursor-oauth 0.0.0-dev.b8e6dd72a8b6 → 0.0.0-dev.de8f891a2e99
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/auth.js +26 -1
- package/dist/index.js +80 -62
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +142 -0
- package/dist/models.js +21 -0
- package/dist/proxy.js +62 -6
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generatePKCE } from "./pkce";
|
|
2
|
+
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
2
3
|
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
3
4
|
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
4
5
|
const CURSOR_REFRESH_URL = process.env.CURSOR_REFRESH_URL ??
|
|
@@ -40,13 +41,32 @@ export async function pollCursorAuth(uuid, verifier) {
|
|
|
40
41
|
}
|
|
41
42
|
throw new Error(`Poll failed: ${response.status}`);
|
|
42
43
|
}
|
|
43
|
-
catch {
|
|
44
|
+
catch (error) {
|
|
44
45
|
consecutiveErrors++;
|
|
45
46
|
if (consecutiveErrors >= 3) {
|
|
47
|
+
logPluginError("Cursor auth polling failed repeatedly", {
|
|
48
|
+
stage: "oauth_poll",
|
|
49
|
+
uuid,
|
|
50
|
+
attempts: attempt + 1,
|
|
51
|
+
consecutiveErrors,
|
|
52
|
+
...errorDetails(error),
|
|
53
|
+
});
|
|
46
54
|
throw new Error("Too many consecutive errors during Cursor auth polling");
|
|
47
55
|
}
|
|
56
|
+
logPluginWarn("Cursor auth polling attempt failed", {
|
|
57
|
+
stage: "oauth_poll",
|
|
58
|
+
uuid,
|
|
59
|
+
attempt: attempt + 1,
|
|
60
|
+
consecutiveErrors,
|
|
61
|
+
...errorDetails(error),
|
|
62
|
+
});
|
|
48
63
|
}
|
|
49
64
|
}
|
|
65
|
+
logPluginError("Cursor authentication polling timed out", {
|
|
66
|
+
stage: "oauth_poll",
|
|
67
|
+
uuid,
|
|
68
|
+
attempts: POLL_MAX_ATTEMPTS,
|
|
69
|
+
});
|
|
50
70
|
throw new Error("Cursor authentication polling timeout");
|
|
51
71
|
}
|
|
52
72
|
export async function refreshCursorToken(refreshToken) {
|
|
@@ -60,6 +80,11 @@ export async function refreshCursorToken(refreshToken) {
|
|
|
60
80
|
});
|
|
61
81
|
if (!response.ok) {
|
|
62
82
|
const error = await response.text();
|
|
83
|
+
logPluginError("Cursor token refresh failed", {
|
|
84
|
+
stage: "token_refresh",
|
|
85
|
+
status: response.status,
|
|
86
|
+
responseBody: error,
|
|
87
|
+
});
|
|
63
88
|
throw new Error(`Cursor token refresh failed: ${error}`);
|
|
64
89
|
}
|
|
65
90
|
const data = (await response.json());
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
|
|
2
|
+
import { configurePluginLogger, errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
2
3
|
import { getCursorModels } from "./models";
|
|
3
4
|
import { startProxy, stopProxy } from "./proxy";
|
|
4
5
|
const CURSOR_PROVIDER_ID = "cursor";
|
|
@@ -8,37 +9,90 @@ let lastModelDiscoveryError = null;
|
|
|
8
9
|
* Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
|
|
9
10
|
*/
|
|
10
11
|
export const CursorAuthPlugin = async (input) => {
|
|
12
|
+
configurePluginLogger(input);
|
|
11
13
|
return {
|
|
12
14
|
auth: {
|
|
13
15
|
provider: CURSOR_PROVIDER_ID,
|
|
14
16
|
async loader(getAuth, provider) {
|
|
15
|
-
const auth = await getAuth();
|
|
16
|
-
if (!auth || auth.type !== "oauth")
|
|
17
|
-
return {};
|
|
18
|
-
// Ensure we have a valid access token, refreshing if expired
|
|
19
|
-
let accessToken = auth.access;
|
|
20
|
-
if (!accessToken || auth.expires < Date.now()) {
|
|
21
|
-
const refreshed = await refreshCursorToken(auth.refresh);
|
|
22
|
-
await input.client.auth.set({
|
|
23
|
-
path: { id: CURSOR_PROVIDER_ID },
|
|
24
|
-
body: {
|
|
25
|
-
type: "oauth",
|
|
26
|
-
refresh: refreshed.refresh,
|
|
27
|
-
access: refreshed.access,
|
|
28
|
-
expires: refreshed.expires,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
accessToken = refreshed.access;
|
|
32
|
-
}
|
|
33
|
-
let models;
|
|
34
17
|
try {
|
|
18
|
+
const auth = await getAuth();
|
|
19
|
+
if (!auth || auth.type !== "oauth")
|
|
20
|
+
return {};
|
|
21
|
+
// Ensure we have a valid access token, refreshing if expired
|
|
22
|
+
let accessToken = auth.access;
|
|
23
|
+
if (!accessToken || auth.expires < Date.now()) {
|
|
24
|
+
const refreshed = await refreshCursorToken(auth.refresh);
|
|
25
|
+
await input.client.auth.set({
|
|
26
|
+
path: { id: CURSOR_PROVIDER_ID },
|
|
27
|
+
body: {
|
|
28
|
+
type: "oauth",
|
|
29
|
+
refresh: refreshed.refresh,
|
|
30
|
+
access: refreshed.access,
|
|
31
|
+
expires: refreshed.expires,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
accessToken = refreshed.access;
|
|
35
|
+
}
|
|
36
|
+
let models;
|
|
35
37
|
models = await getCursorModels(accessToken);
|
|
36
38
|
lastModelDiscoveryError = null;
|
|
39
|
+
const port = await startProxy(async () => {
|
|
40
|
+
const currentAuth = await getAuth();
|
|
41
|
+
if (currentAuth.type !== "oauth") {
|
|
42
|
+
const authError = new Error("Cursor auth not configured");
|
|
43
|
+
logPluginError("Cursor proxy access token lookup failed", {
|
|
44
|
+
stage: "proxy_access_token",
|
|
45
|
+
...errorDetails(authError),
|
|
46
|
+
});
|
|
47
|
+
throw authError;
|
|
48
|
+
}
|
|
49
|
+
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
50
|
+
const refreshed = await refreshCursorToken(currentAuth.refresh);
|
|
51
|
+
await input.client.auth.set({
|
|
52
|
+
path: { id: CURSOR_PROVIDER_ID },
|
|
53
|
+
body: {
|
|
54
|
+
type: "oauth",
|
|
55
|
+
refresh: refreshed.refresh,
|
|
56
|
+
access: refreshed.access,
|
|
57
|
+
expires: refreshed.expires,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return refreshed.access;
|
|
61
|
+
}
|
|
62
|
+
return currentAuth.access;
|
|
63
|
+
}, models);
|
|
64
|
+
if (provider) {
|
|
65
|
+
provider.models = buildCursorProviderModels(models, port);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
baseURL: `http://localhost:${port}/v1`,
|
|
69
|
+
apiKey: "cursor-proxy",
|
|
70
|
+
async fetch(requestInput, init) {
|
|
71
|
+
if (init?.headers) {
|
|
72
|
+
if (init.headers instanceof Headers) {
|
|
73
|
+
init.headers.delete("authorization");
|
|
74
|
+
}
|
|
75
|
+
else if (Array.isArray(init.headers)) {
|
|
76
|
+
init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
delete init.headers["authorization"];
|
|
80
|
+
delete init.headers["Authorization"];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return fetch(requestInput, init);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
37
86
|
}
|
|
38
87
|
catch (error) {
|
|
39
88
|
const message = error instanceof Error
|
|
40
89
|
? error.message
|
|
41
90
|
: "Cursor model discovery failed.";
|
|
91
|
+
logPluginError("Cursor auth loader failed", {
|
|
92
|
+
stage: "loader",
|
|
93
|
+
providerID: CURSOR_PROVIDER_ID,
|
|
94
|
+
...errorDetails(error),
|
|
95
|
+
});
|
|
42
96
|
stopProxy();
|
|
43
97
|
if (provider) {
|
|
44
98
|
provider.models = {};
|
|
@@ -49,48 +103,6 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
49
103
|
}
|
|
50
104
|
return buildDisabledProviderConfig(message);
|
|
51
105
|
}
|
|
52
|
-
const port = await startProxy(async () => {
|
|
53
|
-
const currentAuth = await getAuth();
|
|
54
|
-
if (currentAuth.type !== "oauth") {
|
|
55
|
-
throw new Error("Cursor auth not configured");
|
|
56
|
-
}
|
|
57
|
-
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
58
|
-
const refreshed = await refreshCursorToken(currentAuth.refresh);
|
|
59
|
-
await input.client.auth.set({
|
|
60
|
-
path: { id: CURSOR_PROVIDER_ID },
|
|
61
|
-
body: {
|
|
62
|
-
type: "oauth",
|
|
63
|
-
refresh: refreshed.refresh,
|
|
64
|
-
access: refreshed.access,
|
|
65
|
-
expires: refreshed.expires,
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
return refreshed.access;
|
|
69
|
-
}
|
|
70
|
-
return currentAuth.access;
|
|
71
|
-
}, models);
|
|
72
|
-
if (provider) {
|
|
73
|
-
provider.models = buildCursorProviderModels(models, port);
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
baseURL: `http://localhost:${port}/v1`,
|
|
77
|
-
apiKey: "cursor-proxy",
|
|
78
|
-
async fetch(requestInput, init) {
|
|
79
|
-
if (init?.headers) {
|
|
80
|
-
if (init.headers instanceof Headers) {
|
|
81
|
-
init.headers.delete("authorization");
|
|
82
|
-
}
|
|
83
|
-
else if (Array.isArray(init.headers)) {
|
|
84
|
-
init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization");
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
delete init.headers["authorization"];
|
|
88
|
-
delete init.headers["Authorization"];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return fetch(requestInput, init);
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
106
|
},
|
|
95
107
|
methods: [
|
|
96
108
|
{
|
|
@@ -175,7 +187,13 @@ async function showDiscoveryFailureToast(input, message) {
|
|
|
175
187
|
},
|
|
176
188
|
});
|
|
177
189
|
}
|
|
178
|
-
catch {
|
|
190
|
+
catch (error) {
|
|
191
|
+
logPluginWarn("Failed to display Cursor plugin toast", {
|
|
192
|
+
title: "Cursor plugin disabled",
|
|
193
|
+
message,
|
|
194
|
+
...errorDetails(error),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
179
197
|
}
|
|
180
198
|
function buildDisabledProviderConfig(message) {
|
|
181
199
|
return {
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
export declare function configurePluginLogger(input: PluginInput): void;
|
|
3
|
+
export declare function errorDetails(error: unknown): Record<string, unknown>;
|
|
4
|
+
export declare function logPluginWarn(message: string, extra?: Record<string, unknown>): void;
|
|
5
|
+
export declare function logPluginError(message: string, extra?: Record<string, unknown>): void;
|
|
6
|
+
export declare function flushPluginLogs(): Promise<void>;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const PLUGIN_LOG_SERVICE = "opencode-cursor-oauth";
|
|
2
|
+
const MAX_STRING_LENGTH = 1_500;
|
|
3
|
+
const MAX_ARRAY_LENGTH = 20;
|
|
4
|
+
const MAX_OBJECT_KEYS = 25;
|
|
5
|
+
let currentLogger;
|
|
6
|
+
let pendingLogWrites = Promise.resolve();
|
|
7
|
+
export function configurePluginLogger(input) {
|
|
8
|
+
currentLogger = {
|
|
9
|
+
client: input.client,
|
|
10
|
+
directory: input.directory,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function errorDetails(error) {
|
|
14
|
+
if (error instanceof Error) {
|
|
15
|
+
return {
|
|
16
|
+
errorName: error.name,
|
|
17
|
+
errorMessage: error.message,
|
|
18
|
+
errorStack: error.stack,
|
|
19
|
+
errorCause: serializeValue(error.cause, 1),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
errorType: typeof error,
|
|
24
|
+
errorValue: serializeValue(error, 1),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function logPluginWarn(message, extra = {}) {
|
|
28
|
+
logPlugin("warn", message, extra);
|
|
29
|
+
}
|
|
30
|
+
export function logPluginError(message, extra = {}) {
|
|
31
|
+
logPlugin("error", message, extra);
|
|
32
|
+
}
|
|
33
|
+
export function flushPluginLogs() {
|
|
34
|
+
return pendingLogWrites;
|
|
35
|
+
}
|
|
36
|
+
function logPlugin(level, message, extra) {
|
|
37
|
+
const serializedExtra = serializeValue(extra, 0);
|
|
38
|
+
writeConsoleLog(level, message, serializedExtra);
|
|
39
|
+
if (!currentLogger?.client?.app?.log) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
pendingLogWrites = pendingLogWrites
|
|
43
|
+
.catch(() => { })
|
|
44
|
+
.then(async () => {
|
|
45
|
+
try {
|
|
46
|
+
await currentLogger?.client.app.log({
|
|
47
|
+
query: { directory: currentLogger.directory },
|
|
48
|
+
body: {
|
|
49
|
+
service: PLUGIN_LOG_SERVICE,
|
|
50
|
+
level,
|
|
51
|
+
message,
|
|
52
|
+
extra: serializedExtra,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (logError) {
|
|
57
|
+
writeConsoleLog("warn", "Failed to forward plugin log to OpenCode", {
|
|
58
|
+
originalLevel: level,
|
|
59
|
+
originalMessage: message,
|
|
60
|
+
...errorDetails(logError),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function writeConsoleLog(level, message, extra) {
|
|
66
|
+
const prefix = `[${PLUGIN_LOG_SERVICE}] ${message}`;
|
|
67
|
+
const suffix = Object.keys(extra).length > 0 ? ` ${JSON.stringify(extra)}` : "";
|
|
68
|
+
if (level === "error") {
|
|
69
|
+
console.error(`${prefix}${suffix}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.warn(`${prefix}${suffix}`);
|
|
73
|
+
}
|
|
74
|
+
function serializeValue(value, depth, seen = new WeakSet()) {
|
|
75
|
+
if (value === null || value === undefined)
|
|
76
|
+
return value;
|
|
77
|
+
if (typeof value === "string")
|
|
78
|
+
return truncateString(value);
|
|
79
|
+
const valueType = typeof value;
|
|
80
|
+
if (valueType === "number" || valueType === "boolean")
|
|
81
|
+
return value;
|
|
82
|
+
if (valueType === "bigint")
|
|
83
|
+
return value.toString();
|
|
84
|
+
if (valueType === "symbol")
|
|
85
|
+
return String(value);
|
|
86
|
+
if (valueType === "function")
|
|
87
|
+
return `[function ${value.name || "anonymous"}]`;
|
|
88
|
+
if (value instanceof URL)
|
|
89
|
+
return value.toString();
|
|
90
|
+
if (value instanceof Headers)
|
|
91
|
+
return Object.fromEntries(value.entries());
|
|
92
|
+
if (value instanceof Error) {
|
|
93
|
+
return {
|
|
94
|
+
name: value.name,
|
|
95
|
+
message: value.message,
|
|
96
|
+
stack: truncateString(value.stack),
|
|
97
|
+
cause: serializeValue(value.cause, depth + 1, seen),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (value instanceof Uint8Array) {
|
|
101
|
+
return serializeBinary(value);
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
if (depth >= 3)
|
|
105
|
+
return `[array(${value.length})]`;
|
|
106
|
+
return value.slice(0, MAX_ARRAY_LENGTH).map((entry) => serializeValue(entry, depth + 1, seen));
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === "object") {
|
|
109
|
+
if (seen.has(value))
|
|
110
|
+
return "[circular]";
|
|
111
|
+
seen.add(value);
|
|
112
|
+
if (depth >= 3) {
|
|
113
|
+
return `[object ${value.constructor?.name || "Object"}]`;
|
|
114
|
+
}
|
|
115
|
+
const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
|
|
116
|
+
return Object.fromEntries(entries.map(([key, entry]) => [key, serializeValue(entry, depth + 1, seen)]));
|
|
117
|
+
}
|
|
118
|
+
return String(value);
|
|
119
|
+
}
|
|
120
|
+
function serializeBinary(value) {
|
|
121
|
+
const text = new TextDecoder().decode(value);
|
|
122
|
+
const printable = /^[\x09\x0a\x0d\x20-\x7e]*$/.test(text);
|
|
123
|
+
if (printable) {
|
|
124
|
+
return {
|
|
125
|
+
type: "uint8array",
|
|
126
|
+
length: value.length,
|
|
127
|
+
text: truncateString(text),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
type: "uint8array",
|
|
132
|
+
length: value.length,
|
|
133
|
+
base64: truncateString(Buffer.from(value).toString("base64")),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function truncateString(value) {
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
if (value.length <= MAX_STRING_LENGTH)
|
|
140
|
+
return value;
|
|
141
|
+
return `${value.slice(0, MAX_STRING_LENGTH - 3)}...`;
|
|
142
|
+
}
|
package/dist/models.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
3
4
|
import { callCursorUnaryRpc } from "./proxy";
|
|
4
5
|
import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb";
|
|
5
6
|
const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
|
|
@@ -35,16 +36,32 @@ async function fetchCursorUsableModels(apiKey) {
|
|
|
35
36
|
timeoutMs: MODEL_DISCOVERY_TIMEOUT_MS,
|
|
36
37
|
});
|
|
37
38
|
if (response.timedOut) {
|
|
39
|
+
logPluginError("Cursor model discovery timed out", {
|
|
40
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
41
|
+
timeoutMs: MODEL_DISCOVERY_TIMEOUT_MS,
|
|
42
|
+
});
|
|
38
43
|
throw new CursorModelDiscoveryError(`Cursor model discovery timed out after ${MODEL_DISCOVERY_TIMEOUT_MS}ms.`);
|
|
39
44
|
}
|
|
40
45
|
if (response.exitCode !== 0) {
|
|
46
|
+
logPluginError("Cursor model discovery HTTP failure", {
|
|
47
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
48
|
+
exitCode: response.exitCode,
|
|
49
|
+
responseBody: response.body,
|
|
50
|
+
});
|
|
41
51
|
throw new CursorModelDiscoveryError(buildDiscoveryHttpError(response.exitCode, response.body));
|
|
42
52
|
}
|
|
43
53
|
if (response.body.length === 0) {
|
|
54
|
+
logPluginWarn("Cursor model discovery returned an empty response", {
|
|
55
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
56
|
+
});
|
|
44
57
|
throw new CursorModelDiscoveryError("Cursor model discovery returned an empty response.");
|
|
45
58
|
}
|
|
46
59
|
const decoded = decodeGetUsableModelsResponse(response.body);
|
|
47
60
|
if (!decoded) {
|
|
61
|
+
logPluginError("Cursor model discovery returned an unreadable response", {
|
|
62
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
63
|
+
responseBody: response.body,
|
|
64
|
+
});
|
|
48
65
|
throw new CursorModelDiscoveryError("Cursor model discovery returned an unreadable response.");
|
|
49
66
|
}
|
|
50
67
|
const models = normalizeCursorModels(decoded.models);
|
|
@@ -56,6 +73,10 @@ async function fetchCursorUsableModels(apiKey) {
|
|
|
56
73
|
catch (error) {
|
|
57
74
|
if (error instanceof CursorModelDiscoveryError)
|
|
58
75
|
throw error;
|
|
76
|
+
logPluginError("Cursor model discovery crashed", {
|
|
77
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
78
|
+
...errorDetails(error),
|
|
79
|
+
});
|
|
59
80
|
throw new CursorModelDiscoveryError("Cursor model discovery failed.");
|
|
60
81
|
}
|
|
61
82
|
}
|
package/dist/proxy.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
|
17
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";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { connect as connectHttp2 } from "node:http2";
|
|
20
|
+
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
20
21
|
const CURSOR_API_URL = process.env.CURSOR_API_URL ?? "https://api2.cursor.sh";
|
|
21
22
|
const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
|
|
22
23
|
const CURSOR_CONNECT_PROTOCOL_VERSION = "1";
|
|
@@ -133,6 +134,11 @@ async function createCursorSession(options) {
|
|
|
133
134
|
});
|
|
134
135
|
if (!response.ok || !response.body) {
|
|
135
136
|
const errorBody = await response.text().catch(() => "");
|
|
137
|
+
logPluginError("Cursor RunSSE request failed", {
|
|
138
|
+
requestId: options.requestId,
|
|
139
|
+
status: response.status,
|
|
140
|
+
responseBody: errorBody,
|
|
141
|
+
});
|
|
136
142
|
throw new Error(`RunSSE failed: ${response.status}${errorBody ? ` ${errorBody}` : ""}`);
|
|
137
143
|
}
|
|
138
144
|
const cbs = {
|
|
@@ -163,6 +169,12 @@ async function createCursorSession(options) {
|
|
|
163
169
|
});
|
|
164
170
|
if (!appendResponse.ok) {
|
|
165
171
|
const errorBody = await appendResponse.text().catch(() => "");
|
|
172
|
+
logPluginError("Cursor BidiAppend request failed", {
|
|
173
|
+
requestId: options.requestId,
|
|
174
|
+
appendSeqno: appendSeqno - 1,
|
|
175
|
+
status: appendResponse.status,
|
|
176
|
+
responseBody: errorBody,
|
|
177
|
+
});
|
|
166
178
|
throw new Error(`BidiAppend failed: ${appendResponse.status}${errorBody ? ` ${errorBody}` : ""}`);
|
|
167
179
|
}
|
|
168
180
|
await appendResponse.arrayBuffer().catch(() => undefined);
|
|
@@ -186,7 +198,11 @@ async function createCursorSession(options) {
|
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
200
|
}
|
|
189
|
-
catch {
|
|
201
|
+
catch (error) {
|
|
202
|
+
logPluginWarn("Cursor stream reader closed with error", {
|
|
203
|
+
requestId: options.requestId,
|
|
204
|
+
...errorDetails(error),
|
|
205
|
+
});
|
|
190
206
|
finish(alive ? 1 : closeCode);
|
|
191
207
|
}
|
|
192
208
|
})();
|
|
@@ -199,7 +215,11 @@ async function createCursorSession(options) {
|
|
|
199
215
|
return;
|
|
200
216
|
writeChain = writeChain
|
|
201
217
|
.then(() => append(data))
|
|
202
|
-
.catch(() => {
|
|
218
|
+
.catch((error) => {
|
|
219
|
+
logPluginError("Cursor stream append failed", {
|
|
220
|
+
requestId: options.requestId,
|
|
221
|
+
...errorDetails(error),
|
|
222
|
+
});
|
|
203
223
|
try {
|
|
204
224
|
abortController.abort();
|
|
205
225
|
}
|
|
@@ -278,6 +298,12 @@ async function callCursorUnaryRpcOverFetch(options, target) {
|
|
|
278
298
|
};
|
|
279
299
|
}
|
|
280
300
|
catch {
|
|
301
|
+
logPluginError("Cursor unary fetch transport failed", {
|
|
302
|
+
rpcPath: options.rpcPath,
|
|
303
|
+
url: target.toString(),
|
|
304
|
+
timeoutMs,
|
|
305
|
+
timedOut,
|
|
306
|
+
});
|
|
281
307
|
return {
|
|
282
308
|
body: new Uint8Array(),
|
|
283
309
|
exitCode: timedOut ? 124 : 1,
|
|
@@ -325,7 +351,13 @@ async function callCursorUnaryRpcOverHttp2(options, target) {
|
|
|
325
351
|
: undefined;
|
|
326
352
|
try {
|
|
327
353
|
session = connectHttp2(authority);
|
|
328
|
-
session.once("error", () => {
|
|
354
|
+
session.once("error", (error) => {
|
|
355
|
+
logPluginError("Cursor unary HTTP/2 session failed", {
|
|
356
|
+
rpcPath: options.rpcPath,
|
|
357
|
+
url: target.toString(),
|
|
358
|
+
timedOut,
|
|
359
|
+
...errorDetails(error),
|
|
360
|
+
});
|
|
329
361
|
finish({
|
|
330
362
|
body: new Uint8Array(),
|
|
331
363
|
exitCode: timedOut ? 124 : 1,
|
|
@@ -361,16 +393,35 @@ async function callCursorUnaryRpcOverHttp2(options, target) {
|
|
|
361
393
|
timedOut,
|
|
362
394
|
});
|
|
363
395
|
});
|
|
364
|
-
stream.once("error", () => {
|
|
396
|
+
stream.once("error", (error) => {
|
|
397
|
+
logPluginError("Cursor unary HTTP/2 stream failed", {
|
|
398
|
+
rpcPath: options.rpcPath,
|
|
399
|
+
url: target.toString(),
|
|
400
|
+
timedOut,
|
|
401
|
+
...errorDetails(error),
|
|
402
|
+
});
|
|
365
403
|
finish({
|
|
366
404
|
body: new Uint8Array(),
|
|
367
405
|
exitCode: timedOut ? 124 : 1,
|
|
368
406
|
timedOut,
|
|
369
407
|
});
|
|
370
408
|
});
|
|
371
|
-
|
|
409
|
+
// Bun's node:http2 client currently breaks on end(Buffer.alloc(0)) against
|
|
410
|
+
// Cursor's HTTPS endpoint, but a header-only end() succeeds for empty unary bodies.
|
|
411
|
+
if (options.requestBody.length > 0) {
|
|
412
|
+
stream.end(Buffer.from(options.requestBody));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
stream.end();
|
|
416
|
+
}
|
|
372
417
|
}
|
|
373
|
-
catch {
|
|
418
|
+
catch (error) {
|
|
419
|
+
logPluginError("Cursor unary HTTP/2 setup failed", {
|
|
420
|
+
rpcPath: options.rpcPath,
|
|
421
|
+
url: target.toString(),
|
|
422
|
+
timedOut,
|
|
423
|
+
...errorDetails(error),
|
|
424
|
+
});
|
|
374
425
|
finish({
|
|
375
426
|
body: new Uint8Array(),
|
|
376
427
|
exitCode: timedOut ? 124 : 1,
|
|
@@ -424,6 +475,11 @@ export async function startProxy(getAccessToken, models = []) {
|
|
|
424
475
|
}
|
|
425
476
|
catch (err) {
|
|
426
477
|
const message = err instanceof Error ? err.message : String(err);
|
|
478
|
+
logPluginError("Cursor proxy request failed", {
|
|
479
|
+
path: url.pathname,
|
|
480
|
+
method: req.method,
|
|
481
|
+
...errorDetails(err),
|
|
482
|
+
});
|
|
427
483
|
return new Response(JSON.stringify({
|
|
428
484
|
error: { message, type: "server_error", code: "internal_error" },
|
|
429
485
|
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
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.de8f891a2e99",
|
|
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",
|