@playwo/opencode-cursor-oauth 0.0.0-dev.0cb3e1517254
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 +31 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +117 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +306 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +142 -0
- package/dist/models.d.ts +13 -0
- package/dist/models.js +206 -0
- package/dist/pkce.d.ts +4 -0
- package/dist/pkce.js +9 -0
- package/dist/proto/agent_pb.d.ts +13022 -0
- package/dist/proto/agent_pb.js +3250 -0
- package/dist/proxy.d.ts +20 -0
- package/dist/proxy.js +1688 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# opencode-cursor-oauth
|
|
2
|
+
|
|
3
|
+
Use Cursor models (Claude, GPT, Gemini, etc.) inside [OpenCode](https://opencode.ai).
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
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
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Add to your `opencode.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"plugin": ["@playwo/opencode-cursor-oauth"]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then authenticate via the OpenCode UI (Settings → Providers → Cursor → Login).
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Cursor account with API access
|
|
27
|
+
- OpenCode 1.2+
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface CursorAuthParams {
|
|
2
|
+
verifier: string;
|
|
3
|
+
challenge: string;
|
|
4
|
+
uuid: string;
|
|
5
|
+
loginUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CursorCredentials {
|
|
8
|
+
access: string;
|
|
9
|
+
refresh: string;
|
|
10
|
+
expires: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function generateCursorAuthParams(): Promise<CursorAuthParams>;
|
|
13
|
+
export declare function pollCursorAuth(uuid: string, verifier: string): Promise<{
|
|
14
|
+
accessToken: string;
|
|
15
|
+
refreshToken: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function refreshCursorToken(refreshToken: string): Promise<CursorCredentials>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract JWT expiry with 5-minute safety margin.
|
|
20
|
+
* Falls back to 1 hour from now if token can't be parsed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getTokenExpiry(token: string): number;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { generatePKCE } from "./pkce";
|
|
2
|
+
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
3
|
+
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
4
|
+
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
5
|
+
const CURSOR_REFRESH_URL = process.env.CURSOR_REFRESH_URL ??
|
|
6
|
+
"https://api2.cursor.sh/auth/exchange_user_api_key";
|
|
7
|
+
const POLL_MAX_ATTEMPTS = 150;
|
|
8
|
+
const POLL_BASE_DELAY = 1000;
|
|
9
|
+
const POLL_MAX_DELAY = 10_000;
|
|
10
|
+
const POLL_BACKOFF_MULTIPLIER = 1.2;
|
|
11
|
+
export async function generateCursorAuthParams() {
|
|
12
|
+
const { verifier, challenge } = await generatePKCE();
|
|
13
|
+
const uuid = crypto.randomUUID();
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
challenge,
|
|
16
|
+
uuid,
|
|
17
|
+
mode: "login",
|
|
18
|
+
redirectTarget: "cli",
|
|
19
|
+
});
|
|
20
|
+
const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
|
|
21
|
+
return { verifier, challenge, uuid, loginUrl };
|
|
22
|
+
}
|
|
23
|
+
export async function pollCursorAuth(uuid, verifier) {
|
|
24
|
+
let delay = POLL_BASE_DELAY;
|
|
25
|
+
let consecutiveErrors = 0;
|
|
26
|
+
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
27
|
+
await Bun.sleep(delay);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
|
|
30
|
+
if (response.status === 404) {
|
|
31
|
+
consecutiveErrors = 0;
|
|
32
|
+
delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const data = (await response.json());
|
|
37
|
+
return {
|
|
38
|
+
accessToken: data.accessToken,
|
|
39
|
+
refreshToken: data.refreshToken,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Poll failed: ${response.status}`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
consecutiveErrors++;
|
|
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
|
+
});
|
|
54
|
+
throw new Error("Too many consecutive errors during Cursor auth polling");
|
|
55
|
+
}
|
|
56
|
+
logPluginWarn("Cursor auth polling attempt failed", {
|
|
57
|
+
stage: "oauth_poll",
|
|
58
|
+
uuid,
|
|
59
|
+
attempt: attempt + 1,
|
|
60
|
+
consecutiveErrors,
|
|
61
|
+
...errorDetails(error),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
logPluginError("Cursor authentication polling timed out", {
|
|
66
|
+
stage: "oauth_poll",
|
|
67
|
+
uuid,
|
|
68
|
+
attempts: POLL_MAX_ATTEMPTS,
|
|
69
|
+
});
|
|
70
|
+
throw new Error("Cursor authentication polling timeout");
|
|
71
|
+
}
|
|
72
|
+
export async function refreshCursorToken(refreshToken) {
|
|
73
|
+
const response = await fetch(CURSOR_REFRESH_URL, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
body: "{}",
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = await response.text();
|
|
83
|
+
logPluginError("Cursor token refresh failed", {
|
|
84
|
+
stage: "token_refresh",
|
|
85
|
+
status: response.status,
|
|
86
|
+
responseBody: error,
|
|
87
|
+
});
|
|
88
|
+
throw new Error(`Cursor token refresh failed: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
const data = (await response.json());
|
|
91
|
+
return {
|
|
92
|
+
access: data.accessToken,
|
|
93
|
+
refresh: data.refreshToken || refreshToken,
|
|
94
|
+
expires: getTokenExpiry(data.accessToken),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Extract JWT expiry with 5-minute safety margin.
|
|
99
|
+
* Falls back to 1 hour from now if token can't be parsed.
|
|
100
|
+
*/
|
|
101
|
+
export function getTokenExpiry(token) {
|
|
102
|
+
try {
|
|
103
|
+
const parts = token.split(".");
|
|
104
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
105
|
+
return Date.now() + 3600 * 1000;
|
|
106
|
+
}
|
|
107
|
+
const decoded = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
108
|
+
if (decoded &&
|
|
109
|
+
typeof decoded === "object" &&
|
|
110
|
+
typeof decoded.exp === "number") {
|
|
111
|
+
return decoded.exp * 1000 - 5 * 60 * 1000;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
}
|
|
116
|
+
return Date.now() + 3600 * 1000;
|
|
117
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Cursor Auth Plugin
|
|
3
|
+
*
|
|
4
|
+
* Enables using Cursor models (Claude, GPT, etc.) inside OpenCode via:
|
|
5
|
+
* 1. Browser-based OAuth login to Cursor
|
|
6
|
+
* 2. Local proxy translating OpenAI format → Cursor gRPC protocol
|
|
7
|
+
*/
|
|
8
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
9
|
+
/**
|
|
10
|
+
* OpenCode plugin that provides Cursor authentication and model access.
|
|
11
|
+
* Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
|
|
12
|
+
*/
|
|
13
|
+
export declare const CursorAuthPlugin: Plugin;
|
|
14
|
+
export default CursorAuthPlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
|
|
2
|
+
import { configurePluginLogger, errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
3
|
+
import { getCursorModels } from "./models";
|
|
4
|
+
import { startProxy, stopProxy, } from "./proxy";
|
|
5
|
+
const CURSOR_PROVIDER_ID = "cursor";
|
|
6
|
+
let lastModelDiscoveryError = null;
|
|
7
|
+
/**
|
|
8
|
+
* OpenCode plugin that provides Cursor authentication and model access.
|
|
9
|
+
* Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
|
|
10
|
+
*/
|
|
11
|
+
export const CursorAuthPlugin = async (input) => {
|
|
12
|
+
configurePluginLogger(input);
|
|
13
|
+
return {
|
|
14
|
+
auth: {
|
|
15
|
+
provider: CURSOR_PROVIDER_ID,
|
|
16
|
+
async loader(getAuth, provider) {
|
|
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;
|
|
37
|
+
models = await getCursorModels(accessToken);
|
|
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
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const message = error instanceof Error
|
|
89
|
+
? error.message
|
|
90
|
+
: "Cursor model discovery failed.";
|
|
91
|
+
logPluginError("Cursor auth loader failed", {
|
|
92
|
+
stage: "loader",
|
|
93
|
+
providerID: CURSOR_PROVIDER_ID,
|
|
94
|
+
...errorDetails(error),
|
|
95
|
+
});
|
|
96
|
+
stopProxy();
|
|
97
|
+
if (provider) {
|
|
98
|
+
provider.models = {};
|
|
99
|
+
}
|
|
100
|
+
if (message !== lastModelDiscoveryError) {
|
|
101
|
+
lastModelDiscoveryError = message;
|
|
102
|
+
await showDiscoveryFailureToast(input, message);
|
|
103
|
+
}
|
|
104
|
+
return buildDisabledProviderConfig(message);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
methods: [
|
|
108
|
+
{
|
|
109
|
+
type: "oauth",
|
|
110
|
+
label: "Login with Cursor",
|
|
111
|
+
async authorize() {
|
|
112
|
+
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
113
|
+
return {
|
|
114
|
+
url: loginUrl,
|
|
115
|
+
instructions: "Complete login in your browser. This window will close automatically.",
|
|
116
|
+
method: "auto",
|
|
117
|
+
async callback() {
|
|
118
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
119
|
+
return {
|
|
120
|
+
type: "success",
|
|
121
|
+
refresh: refreshToken,
|
|
122
|
+
access: accessToken,
|
|
123
|
+
expires: getTokenExpiry(accessToken),
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
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
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
function buildCursorProviderModels(models, port) {
|
|
143
|
+
return Object.fromEntries(models.map((model) => [
|
|
144
|
+
model.id,
|
|
145
|
+
{
|
|
146
|
+
id: model.id,
|
|
147
|
+
providerID: CURSOR_PROVIDER_ID,
|
|
148
|
+
api: {
|
|
149
|
+
id: model.id,
|
|
150
|
+
url: `http://localhost:${port}/v1`,
|
|
151
|
+
npm: "@ai-sdk/openai-compatible",
|
|
152
|
+
},
|
|
153
|
+
name: model.name,
|
|
154
|
+
capabilities: {
|
|
155
|
+
temperature: true,
|
|
156
|
+
reasoning: model.reasoning,
|
|
157
|
+
attachment: false,
|
|
158
|
+
toolcall: true,
|
|
159
|
+
input: {
|
|
160
|
+
text: true,
|
|
161
|
+
audio: false,
|
|
162
|
+
image: false,
|
|
163
|
+
video: false,
|
|
164
|
+
pdf: false,
|
|
165
|
+
},
|
|
166
|
+
output: {
|
|
167
|
+
text: true,
|
|
168
|
+
audio: false,
|
|
169
|
+
image: false,
|
|
170
|
+
video: false,
|
|
171
|
+
pdf: false,
|
|
172
|
+
},
|
|
173
|
+
interleaved: false,
|
|
174
|
+
},
|
|
175
|
+
cost: estimateModelCost(model.id),
|
|
176
|
+
limit: {
|
|
177
|
+
context: model.contextWindow,
|
|
178
|
+
output: model.maxTokens,
|
|
179
|
+
},
|
|
180
|
+
status: "active",
|
|
181
|
+
options: {},
|
|
182
|
+
headers: {},
|
|
183
|
+
release_date: "",
|
|
184
|
+
variants: {},
|
|
185
|
+
},
|
|
186
|
+
]));
|
|
187
|
+
}
|
|
188
|
+
async function showDiscoveryFailureToast(input, message) {
|
|
189
|
+
try {
|
|
190
|
+
await input.client.tui.showToast({
|
|
191
|
+
body: {
|
|
192
|
+
title: "Cursor plugin disabled",
|
|
193
|
+
message,
|
|
194
|
+
variant: "error",
|
|
195
|
+
duration: 8_000,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
logPluginWarn("Failed to display Cursor plugin toast", {
|
|
201
|
+
title: "Cursor plugin disabled",
|
|
202
|
+
message,
|
|
203
|
+
...errorDetails(error),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function buildDisabledProviderConfig(message) {
|
|
208
|
+
return {
|
|
209
|
+
baseURL: "http://127.0.0.1/cursor-disabled/v1",
|
|
210
|
+
apiKey: "cursor-disabled",
|
|
211
|
+
async fetch() {
|
|
212
|
+
return new Response(JSON.stringify({
|
|
213
|
+
error: {
|
|
214
|
+
message,
|
|
215
|
+
type: "server_error",
|
|
216
|
+
code: "cursor_model_discovery_failed",
|
|
217
|
+
},
|
|
218
|
+
}), {
|
|
219
|
+
status: 503,
|
|
220
|
+
headers: { "Content-Type": "application/json" },
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// $/M token rates from cursor.com/docs/models-and-pricing
|
|
226
|
+
const MODEL_COST_TABLE = {
|
|
227
|
+
// Anthropic
|
|
228
|
+
"claude-4-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
229
|
+
"claude-4-sonnet-1m": { input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } },
|
|
230
|
+
"claude-4.5-haiku": { input: 1, output: 5, cache: { read: 0.1, write: 1.25 } },
|
|
231
|
+
"claude-4.5-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
|
|
232
|
+
"claude-4.5-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
233
|
+
"claude-4.6-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
|
|
234
|
+
"claude-4.6-opus-fast": { input: 30, output: 150, cache: { read: 3, write: 37.5 } },
|
|
235
|
+
"claude-4.6-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
236
|
+
// Cursor
|
|
237
|
+
"composer-1": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
238
|
+
"composer-1.5": { input: 3.5, output: 17.5, cache: { read: 0.35, write: 0 } },
|
|
239
|
+
"composer-2": { input: 0.5, output: 2.5, cache: { read: 0.2, write: 0 } },
|
|
240
|
+
"composer-2-fast": { input: 1.5, output: 7.5, cache: { read: 0.2, write: 0 } },
|
|
241
|
+
// Google
|
|
242
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5, cache: { read: 0.03, write: 0 } },
|
|
243
|
+
"gemini-3-flash": { input: 0.5, output: 3, cache: { read: 0.05, write: 0 } },
|
|
244
|
+
"gemini-3-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
245
|
+
"gemini-3-pro-image": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
246
|
+
"gemini-3.1-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
247
|
+
// OpenAI
|
|
248
|
+
"gpt-5": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
249
|
+
"gpt-5-fast": { input: 2.5, output: 20, cache: { read: 0.25, write: 0 } },
|
|
250
|
+
"gpt-5-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
|
|
251
|
+
"gpt-5-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
252
|
+
"gpt-5.1-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
253
|
+
"gpt-5.1-codex-max": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
254
|
+
"gpt-5.1-codex-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
|
|
255
|
+
"gpt-5.2": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
256
|
+
"gpt-5.2-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
257
|
+
"gpt-5.3-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
258
|
+
"gpt-5.4": { input: 2.5, output: 15, cache: { read: 0.25, write: 0 } },
|
|
259
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cache: { read: 0.075, write: 0 } },
|
|
260
|
+
"gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } },
|
|
261
|
+
// xAI
|
|
262
|
+
"grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } },
|
|
263
|
+
// Moonshot
|
|
264
|
+
"kimi-k2.5": { input: 0.6, output: 3, cache: { read: 0.1, write: 0 } },
|
|
265
|
+
};
|
|
266
|
+
// Most-specific first
|
|
267
|
+
const MODEL_COST_PATTERNS = [
|
|
268
|
+
{ match: (id) => /claude.*opus.*fast/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus-fast"] },
|
|
269
|
+
{ match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"] },
|
|
270
|
+
{ match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"] },
|
|
271
|
+
{ match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
|
|
272
|
+
{ match: (id) => /claude/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
|
|
273
|
+
{ match: (id) => /composer-?2/i.test(id), cost: MODEL_COST_TABLE["composer-2"] },
|
|
274
|
+
{ match: (id) => /composer-?1\.5/i.test(id), cost: MODEL_COST_TABLE["composer-1.5"] },
|
|
275
|
+
{ match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] },
|
|
276
|
+
{ match: (id) => /gpt-5\.4.*nano/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-nano"] },
|
|
277
|
+
{ match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"] },
|
|
278
|
+
{ match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] },
|
|
279
|
+
{ match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] },
|
|
280
|
+
{ match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] },
|
|
281
|
+
{ match: (id) => /gpt-5\.1.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex-mini"] },
|
|
282
|
+
{ match: (id) => /gpt-5\.1/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex"] },
|
|
283
|
+
{ match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] },
|
|
284
|
+
{ match: (id) => /gpt-5.*fast/i.test(id), cost: MODEL_COST_TABLE["gpt-5-fast"] },
|
|
285
|
+
{ match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] },
|
|
286
|
+
{ match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
|
|
287
|
+
{ match: (id) => /gemini.*3.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-3-flash"] },
|
|
288
|
+
{ match: (id) => /gemini.*3/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] },
|
|
289
|
+
{ match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"] },
|
|
290
|
+
{ match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
|
|
291
|
+
{ match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] },
|
|
292
|
+
{ match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] },
|
|
293
|
+
];
|
|
294
|
+
const DEFAULT_COST = { input: 3, output: 15, cache: { read: 0.3, write: 0 } };
|
|
295
|
+
function estimateModelCost(modelId) {
|
|
296
|
+
const normalized = modelId.toLowerCase();
|
|
297
|
+
const exact = MODEL_COST_TABLE[normalized];
|
|
298
|
+
if (exact)
|
|
299
|
+
return exact;
|
|
300
|
+
const stripped = normalized.replace(/-(high|medium|low|preview|thinking|spark-preview)$/g, "");
|
|
301
|
+
const strippedMatch = MODEL_COST_TABLE[stripped];
|
|
302
|
+
if (strippedMatch)
|
|
303
|
+
return strippedMatch;
|
|
304
|
+
return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
|
|
305
|
+
}
|
|
306
|
+
export default CursorAuthPlugin;
|
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.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CursorModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
reasoning: boolean;
|
|
5
|
+
contextWindow: number;
|
|
6
|
+
maxTokens: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class CursorModelDiscoveryError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function getCursorModels(apiKey: string): Promise<CursorModel[]>;
|
|
12
|
+
/** @internal Test-only. */
|
|
13
|
+
export declare function clearModelCache(): void;
|