@playwo/opencode-cursor-oauth 0.0.0-dev.17eadae36ea6

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,103 @@
1
+ # @playwo/opencode-cursor-oauth
2
+
3
+ OpenCode plugin that connects to Cursor's API, giving you access to Cursor
4
+ models inside OpenCode with full tool-calling support.
5
+
6
+ ## Install in OpenCode
7
+
8
+ Add this to `~/.config/opencode/opencode.json`:
9
+
10
+ ```jsonc
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.
55
+
56
+ ## Architecture
57
+
58
+ ```
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
+
90
+ GitHub Actions publishes this package with `.github/workflows/publish-npm.yml`.
91
+
92
+ - branch pushes publish a `dev` build as `0.0.0-dev.<sha>`
93
+ - versioned releases publish `latest` using the `package.json` version and upload the packed `.tgz` to the GitHub release
94
+
95
+ Repository secrets required:
96
+
97
+ - `NPM_TOKEN` for npm publish access
98
+
99
+ ## Requirements
100
+
101
+ - [OpenCode](https://opencode.ai)
102
+ - [Bun](https://bun.sh)
103
+ - Active [Cursor](https://cursor.com) subscription
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,92 @@
1
+ import { generatePKCE } from "./pkce";
2
+ const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
3
+ const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
4
+ const CURSOR_REFRESH_URL = process.env.CURSOR_REFRESH_URL ??
5
+ "https://api2.cursor.sh/auth/exchange_user_api_key";
6
+ const POLL_MAX_ATTEMPTS = 150;
7
+ const POLL_BASE_DELAY = 1000;
8
+ const POLL_MAX_DELAY = 10_000;
9
+ const POLL_BACKOFF_MULTIPLIER = 1.2;
10
+ export async function generateCursorAuthParams() {
11
+ const { verifier, challenge } = await generatePKCE();
12
+ const uuid = crypto.randomUUID();
13
+ const params = new URLSearchParams({
14
+ challenge,
15
+ uuid,
16
+ mode: "login",
17
+ redirectTarget: "cli",
18
+ });
19
+ const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
20
+ return { verifier, challenge, uuid, loginUrl };
21
+ }
22
+ export async function pollCursorAuth(uuid, verifier) {
23
+ let delay = POLL_BASE_DELAY;
24
+ let consecutiveErrors = 0;
25
+ for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
26
+ await Bun.sleep(delay);
27
+ try {
28
+ const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
29
+ if (response.status === 404) {
30
+ consecutiveErrors = 0;
31
+ delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY);
32
+ continue;
33
+ }
34
+ if (response.ok) {
35
+ const data = (await response.json());
36
+ return {
37
+ accessToken: data.accessToken,
38
+ refreshToken: data.refreshToken,
39
+ };
40
+ }
41
+ throw new Error(`Poll failed: ${response.status}`);
42
+ }
43
+ catch {
44
+ consecutiveErrors++;
45
+ if (consecutiveErrors >= 3) {
46
+ throw new Error("Too many consecutive errors during Cursor auth polling");
47
+ }
48
+ }
49
+ }
50
+ throw new Error("Cursor authentication polling timeout");
51
+ }
52
+ export async function refreshCursorToken(refreshToken) {
53
+ const response = await fetch(CURSOR_REFRESH_URL, {
54
+ method: "POST",
55
+ headers: {
56
+ Authorization: `Bearer ${refreshToken}`,
57
+ "Content-Type": "application/json",
58
+ },
59
+ body: "{}",
60
+ });
61
+ if (!response.ok) {
62
+ const error = await response.text();
63
+ throw new Error(`Cursor token refresh failed: ${error}`);
64
+ }
65
+ const data = (await response.json());
66
+ return {
67
+ access: data.accessToken,
68
+ refresh: data.refreshToken || refreshToken,
69
+ expires: getTokenExpiry(data.accessToken),
70
+ };
71
+ }
72
+ /**
73
+ * Extract JWT expiry with 5-minute safety margin.
74
+ * Falls back to 1 hour from now if token can't be parsed.
75
+ */
76
+ export function getTokenExpiry(token) {
77
+ try {
78
+ const parts = token.split(".");
79
+ if (parts.length !== 3 || !parts[1]) {
80
+ return Date.now() + 3600 * 1000;
81
+ }
82
+ const decoded = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
83
+ if (decoded &&
84
+ typeof decoded === "object" &&
85
+ typeof decoded.exp === "number") {
86
+ return decoded.exp * 1000 - 5 * 60 * 1000;
87
+ }
88
+ }
89
+ catch {
90
+ }
91
+ return Date.now() + 3600 * 1000;
92
+ }
@@ -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,279 @@
1
+ import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
2
+ import { getCursorModels } from "./models";
3
+ import { startProxy, stopProxy } from "./proxy";
4
+ const CURSOR_PROVIDER_ID = "cursor";
5
+ let lastModelDiscoveryError = null;
6
+ /**
7
+ * OpenCode plugin that provides Cursor authentication and model access.
8
+ * Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
9
+ */
10
+ export const CursorAuthPlugin = async (input) => {
11
+ return {
12
+ auth: {
13
+ provider: CURSOR_PROVIDER_ID,
14
+ 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
+ try {
35
+ models = await getCursorModels(accessToken);
36
+ lastModelDiscoveryError = null;
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error
40
+ ? error.message
41
+ : "Cursor model discovery failed.";
42
+ stopProxy();
43
+ if (provider) {
44
+ provider.models = {};
45
+ }
46
+ if (message !== lastModelDiscoveryError) {
47
+ lastModelDiscoveryError = message;
48
+ await showDiscoveryFailureToast(input, message);
49
+ }
50
+ return buildDisabledProviderConfig(message);
51
+ }
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
+ },
95
+ methods: [
96
+ {
97
+ type: "oauth",
98
+ label: "Login with Cursor",
99
+ async authorize() {
100
+ const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
101
+ return {
102
+ url: loginUrl,
103
+ instructions: "Complete login in your browser. This window will close automatically.",
104
+ method: "auto",
105
+ async callback() {
106
+ const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
107
+ return {
108
+ type: "success",
109
+ refresh: refreshToken,
110
+ access: accessToken,
111
+ expires: getTokenExpiry(accessToken),
112
+ };
113
+ },
114
+ };
115
+ },
116
+ },
117
+ ],
118
+ },
119
+ };
120
+ };
121
+ function buildCursorProviderModels(models, port) {
122
+ return Object.fromEntries(models.map((model) => [
123
+ model.id,
124
+ {
125
+ id: model.id,
126
+ providerID: CURSOR_PROVIDER_ID,
127
+ api: {
128
+ id: model.id,
129
+ url: `http://localhost:${port}/v1`,
130
+ npm: "@ai-sdk/openai-compatible",
131
+ },
132
+ name: model.name,
133
+ capabilities: {
134
+ temperature: true,
135
+ reasoning: model.reasoning,
136
+ attachment: false,
137
+ toolcall: true,
138
+ input: {
139
+ text: true,
140
+ audio: false,
141
+ image: false,
142
+ video: false,
143
+ pdf: false,
144
+ },
145
+ output: {
146
+ text: true,
147
+ audio: false,
148
+ image: false,
149
+ video: false,
150
+ pdf: false,
151
+ },
152
+ interleaved: false,
153
+ },
154
+ cost: estimateModelCost(model.id),
155
+ limit: {
156
+ context: model.contextWindow,
157
+ output: model.maxTokens,
158
+ },
159
+ status: "active",
160
+ options: {},
161
+ headers: {},
162
+ release_date: "",
163
+ variants: {},
164
+ },
165
+ ]));
166
+ }
167
+ async function showDiscoveryFailureToast(input, message) {
168
+ try {
169
+ await input.client.tui.showToast({
170
+ body: {
171
+ title: "Cursor plugin disabled",
172
+ message,
173
+ variant: "error",
174
+ duration: 8_000,
175
+ },
176
+ });
177
+ }
178
+ catch { }
179
+ }
180
+ function buildDisabledProviderConfig(message) {
181
+ return {
182
+ baseURL: "http://127.0.0.1/cursor-disabled/v1",
183
+ apiKey: "cursor-disabled",
184
+ async fetch() {
185
+ return new Response(JSON.stringify({
186
+ error: {
187
+ message,
188
+ type: "server_error",
189
+ code: "cursor_model_discovery_failed",
190
+ },
191
+ }), {
192
+ status: 503,
193
+ headers: { "Content-Type": "application/json" },
194
+ });
195
+ },
196
+ };
197
+ }
198
+ // $/M token rates from cursor.com/docs/models-and-pricing
199
+ const MODEL_COST_TABLE = {
200
+ // Anthropic
201
+ "claude-4-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
202
+ "claude-4-sonnet-1m": { input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } },
203
+ "claude-4.5-haiku": { input: 1, output: 5, cache: { read: 0.1, write: 1.25 } },
204
+ "claude-4.5-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
205
+ "claude-4.5-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
206
+ "claude-4.6-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
207
+ "claude-4.6-opus-fast": { input: 30, output: 150, cache: { read: 3, write: 37.5 } },
208
+ "claude-4.6-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
209
+ // Cursor
210
+ "composer-1": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
211
+ "composer-1.5": { input: 3.5, output: 17.5, cache: { read: 0.35, write: 0 } },
212
+ "composer-2": { input: 0.5, output: 2.5, cache: { read: 0.2, write: 0 } },
213
+ "composer-2-fast": { input: 1.5, output: 7.5, cache: { read: 0.2, write: 0 } },
214
+ // Google
215
+ "gemini-2.5-flash": { input: 0.3, output: 2.5, cache: { read: 0.03, write: 0 } },
216
+ "gemini-3-flash": { input: 0.5, output: 3, cache: { read: 0.05, write: 0 } },
217
+ "gemini-3-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
218
+ "gemini-3-pro-image": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
219
+ "gemini-3.1-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
220
+ // OpenAI
221
+ "gpt-5": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
222
+ "gpt-5-fast": { input: 2.5, output: 20, cache: { read: 0.25, write: 0 } },
223
+ "gpt-5-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
224
+ "gpt-5-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
225
+ "gpt-5.1-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
226
+ "gpt-5.1-codex-max": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
227
+ "gpt-5.1-codex-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
228
+ "gpt-5.2": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
229
+ "gpt-5.2-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
230
+ "gpt-5.3-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
231
+ "gpt-5.4": { input: 2.5, output: 15, cache: { read: 0.25, write: 0 } },
232
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cache: { read: 0.075, write: 0 } },
233
+ "gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } },
234
+ // xAI
235
+ "grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } },
236
+ // Moonshot
237
+ "kimi-k2.5": { input: 0.6, output: 3, cache: { read: 0.1, write: 0 } },
238
+ };
239
+ // Most-specific first
240
+ const MODEL_COST_PATTERNS = [
241
+ { match: (id) => /claude.*opus.*fast/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus-fast"] },
242
+ { match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"] },
243
+ { match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"] },
244
+ { match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
245
+ { match: (id) => /claude/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
246
+ { match: (id) => /composer-?2/i.test(id), cost: MODEL_COST_TABLE["composer-2"] },
247
+ { match: (id) => /composer-?1\.5/i.test(id), cost: MODEL_COST_TABLE["composer-1.5"] },
248
+ { match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] },
249
+ { match: (id) => /gpt-5\.4.*nano/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-nano"] },
250
+ { match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"] },
251
+ { match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] },
252
+ { match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] },
253
+ { match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] },
254
+ { match: (id) => /gpt-5\.1.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex-mini"] },
255
+ { match: (id) => /gpt-5\.1/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex"] },
256
+ { match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] },
257
+ { match: (id) => /gpt-5.*fast/i.test(id), cost: MODEL_COST_TABLE["gpt-5-fast"] },
258
+ { match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] },
259
+ { match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
260
+ { match: (id) => /gemini.*3.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-3-flash"] },
261
+ { match: (id) => /gemini.*3/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] },
262
+ { match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"] },
263
+ { match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
264
+ { match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] },
265
+ { match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] },
266
+ ];
267
+ const DEFAULT_COST = { input: 3, output: 15, cache: { read: 0.3, write: 0 } };
268
+ function estimateModelCost(modelId) {
269
+ const normalized = modelId.toLowerCase();
270
+ const exact = MODEL_COST_TABLE[normalized];
271
+ if (exact)
272
+ return exact;
273
+ const stripped = normalized.replace(/-(high|medium|low|preview|thinking|spark-preview)$/g, "");
274
+ const strippedMatch = MODEL_COST_TABLE[stripped];
275
+ if (strippedMatch)
276
+ return strippedMatch;
277
+ return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
278
+ }
279
+ export default CursorAuthPlugin;
@@ -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;