@playwo/opencode-cursor-oauth 0.0.0-dev.b8e6dd72a8b6 → 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/README.md +19 -91
- package/dist/auth.js +26 -1
- package/dist/index.js +90 -63
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +142 -0
- package/dist/models.js +21 -0
- package/dist/proxy.js +607 -104
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,103 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-cursor-oauth
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
models inside OpenCode with full tool-calling support.
|
|
3
|
+
Use Cursor models (Claude, GPT, Gemini, etc.) inside [OpenCode](https://opencode.ai).
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## What it does
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- **OAuth login** to Cursor via browser
|
|
8
|
+
- **Model discovery** — automatically fetches your available Cursor models
|
|
9
|
+
- **Local proxy** — runs an OpenAI-compatible endpoint that translates to Cursor's gRPC protocol
|
|
10
|
+
- **Auto-refresh** — handles token expiration automatically
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
"$schema": "https://opencode.ai/config.json",
|
|
13
|
-
"plugin": [
|
|
14
|
-
"@playwo/opencode-cursor-oauth"
|
|
15
|
-
],
|
|
16
|
-
"provider": {
|
|
17
|
-
"cursor": {
|
|
18
|
-
"name": "Cursor"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
The `cursor` provider stub is required because OpenCode drops providers that do
|
|
25
|
-
not already exist in its bundled provider catalog.
|
|
26
|
-
|
|
27
|
-
OpenCode installs npm plugins automatically at startup, so users do not need to
|
|
28
|
-
clone this repository.
|
|
29
|
-
|
|
30
|
-
## Authenticate
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
opencode auth login --provider cursor
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
This opens Cursor OAuth in the browser. Tokens are stored in
|
|
37
|
-
`~/.local/share/opencode/auth.json` and refreshed automatically.
|
|
38
|
-
|
|
39
|
-
## Use
|
|
40
|
-
|
|
41
|
-
Start OpenCode and select any Cursor model. The plugin starts a local
|
|
42
|
-
OpenAI-compatible proxy on demand and routes requests through Cursor's gRPC API.
|
|
43
|
-
|
|
44
|
-
## How it works
|
|
45
|
-
|
|
46
|
-
1. OAuth — browser-based login to Cursor via PKCE.
|
|
47
|
-
2. Model discovery — queries Cursor's gRPC API for all available models; if discovery fails, the plugin disables the Cursor provider for that load and shows a visible error toast instead of crashing OpenCode.
|
|
48
|
-
3. Local proxy — translates `POST /v1/chat/completions` into Cursor's
|
|
49
|
-
protobuf/Connect protocol.
|
|
50
|
-
4. Native tool routing — rejects Cursor's built-in filesystem/shell tools and
|
|
51
|
-
exposes OpenCode's tool surface via Cursor MCP instead.
|
|
52
|
-
|
|
53
|
-
Cursor agent streaming uses Cursor's `RunSSE` + `BidiAppend` transport, so the
|
|
54
|
-
plugin runs entirely inside OpenCode without a Node sidecar.
|
|
12
|
+
## Install
|
|
55
13
|
|
|
56
|
-
|
|
14
|
+
Add to your `opencode.json`:
|
|
57
15
|
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"plugin": ["@playwo/opencode-cursor-oauth"]
|
|
19
|
+
}
|
|
58
20
|
```
|
|
59
|
-
OpenCode --> /v1/chat/completions --> Bun.serve (proxy)
|
|
60
|
-
|
|
|
61
|
-
RunSSE stream + BidiAppend writes
|
|
62
|
-
|
|
|
63
|
-
Cursor Connect/SSE transport
|
|
64
|
-
|
|
|
65
|
-
api2.cursor.sh gRPC
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Tool call flow
|
|
69
|
-
|
|
70
|
-
```
|
|
71
|
-
1. Cursor model receives OpenAI tools via RequestContext (as MCP tool defs)
|
|
72
|
-
2. Model tries native tools (readArgs, shellArgs, etc.)
|
|
73
|
-
3. Proxy rejects each with typed error (ReadRejected, ShellRejected, etc.)
|
|
74
|
-
4. Model falls back to MCP tool -> mcpArgs exec message
|
|
75
|
-
5. Proxy emits OpenAI tool_calls SSE chunk, pauses the Cursor stream
|
|
76
|
-
6. OpenCode executes tool, sends result in follow-up request
|
|
77
|
-
7. Proxy resumes the Cursor stream with mcpResult and continues streaming
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Develop locally
|
|
81
|
-
|
|
82
|
-
```sh
|
|
83
|
-
bun install
|
|
84
|
-
bun run build
|
|
85
|
-
bun test/smoke.ts
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Publish
|
|
89
21
|
|
|
90
|
-
|
|
22
|
+
Then authenticate via the OpenCode UI (Settings → Providers → Cursor → Login).
|
|
91
23
|
|
|
92
|
-
|
|
93
|
-
- versioned releases publish `latest` using the `package.json` version and upload the packed `.tgz` to the GitHub release
|
|
94
|
-
|
|
95
|
-
Repository secrets required:
|
|
24
|
+
## Requirements
|
|
96
25
|
|
|
97
|
-
-
|
|
26
|
+
- Cursor account with API access
|
|
27
|
+
- OpenCode 1.2+
|
|
98
28
|
|
|
99
|
-
##
|
|
29
|
+
## License
|
|
100
30
|
|
|
101
|
-
|
|
102
|
-
- [Bun](https://bun.sh)
|
|
103
|
-
- Active [Cursor](https://cursor.com) subscription
|
|
31
|
+
MIT
|
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,6 +1,7 @@
|
|
|
1
1
|
import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
|
|
2
|
+
import { configurePluginLogger, errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
2
3
|
import { getCursorModels } from "./models";
|
|
3
|
-
import { startProxy, stopProxy } from "./proxy";
|
|
4
|
+
import { startProxy, stopProxy, } from "./proxy";
|
|
4
5
|
const CURSOR_PROVIDER_ID = "cursor";
|
|
5
6
|
let lastModelDiscoveryError = null;
|
|
6
7
|
/**
|
|
@@ -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
|
{
|
|
@@ -116,6 +128,15 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
116
128
|
},
|
|
117
129
|
],
|
|
118
130
|
},
|
|
131
|
+
async "chat.headers"(incoming, output) {
|
|
132
|
+
if (incoming.model.providerID !== CURSOR_PROVIDER_ID)
|
|
133
|
+
return;
|
|
134
|
+
output.headers["x-opencode-session-id"] = incoming.sessionID;
|
|
135
|
+
output.headers["x-session-id"] = incoming.sessionID;
|
|
136
|
+
if (incoming.agent) {
|
|
137
|
+
output.headers["x-opencode-agent"] = incoming.agent;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
119
140
|
};
|
|
120
141
|
};
|
|
121
142
|
function buildCursorProviderModels(models, port) {
|
|
@@ -175,7 +196,13 @@ async function showDiscoveryFailureToast(input, message) {
|
|
|
175
196
|
},
|
|
176
197
|
});
|
|
177
198
|
}
|
|
178
|
-
catch {
|
|
199
|
+
catch (error) {
|
|
200
|
+
logPluginWarn("Failed to display Cursor plugin toast", {
|
|
201
|
+
title: "Cursor plugin disabled",
|
|
202
|
+
message,
|
|
203
|
+
...errorDetails(error),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
179
206
|
}
|
|
180
207
|
function buildDisabledProviderConfig(message) {
|
|
181
208
|
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
|
}
|