@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 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
+ }
@@ -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;
@@ -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
+ }
@@ -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;