opencode-cursor-oauth 0.0.1
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 +94 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +98 -0
- package/dist/h2-bridge.mjs +140 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +157 -0
- package/dist/models.d.ts +22 -0
- package/dist/models.js +218 -0
- package/dist/pkce.d.ts +8 -0
- package/dist/pkce.js +13 -0
- package/dist/proto/agent_pb.d.ts +13022 -0
- package/dist/proto/agent_pb.js +3250 -0
- package/dist/proxy.d.ts +3 -0
- package/dist/proxy.js +992 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 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
|
+
"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.
|
|
48
|
+
3. Local proxy — translates `POST /v1/chat/completions` into Cursor's
|
|
49
|
+
protobuf/HTTP/2 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
|
+
HTTP/2 transport runs through a Node child process (`h2-bridge.mjs`) because
|
|
54
|
+
Bun's `node:http2` support is not reliable against Cursor's API.
|
|
55
|
+
|
|
56
|
+
## Architecture
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
OpenCode --> /v1/chat/completions --> Bun.serve (proxy)
|
|
60
|
+
|
|
|
61
|
+
Node child process (h2-bridge.mjs)
|
|
62
|
+
|
|
|
63
|
+
HTTP/2 Connect stream
|
|
64
|
+
|
|
|
65
|
+
api2.cursor.sh gRPC
|
|
66
|
+
/agent.v1.AgentService/Run
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Tool call flow
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
1. Cursor model receives OpenAI tools via RequestContext (as MCP tool defs)
|
|
73
|
+
2. Model tries native tools (readArgs, shellArgs, etc.)
|
|
74
|
+
3. Proxy rejects each with typed error (ReadRejected, ShellRejected, etc.)
|
|
75
|
+
4. Model falls back to MCP tool -> mcpArgs exec message
|
|
76
|
+
5. Proxy emits OpenAI tool_calls SSE chunk, pauses H2 stream
|
|
77
|
+
6. OpenCode executes tool, sends result in follow-up request
|
|
78
|
+
7. Proxy resumes H2 stream with mcpResult, streams continuation
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Develop locally
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
bun install
|
|
85
|
+
bun run build
|
|
86
|
+
bun test/smoke.ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- [OpenCode](https://opencode.ai)
|
|
92
|
+
- [Bun](https://bun.sh)
|
|
93
|
+
- [Node.js](https://nodejs.org) >= 18 for the HTTP/2 bridge process
|
|
94
|
+
- 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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor OAuth authentication.
|
|
3
|
+
* Handles PKCE-based login, polling, and token refresh.
|
|
4
|
+
*/
|
|
5
|
+
import { generatePKCE } from "./pkce";
|
|
6
|
+
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
7
|
+
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
8
|
+
const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
|
|
9
|
+
const POLL_MAX_ATTEMPTS = 150;
|
|
10
|
+
const POLL_BASE_DELAY = 1000;
|
|
11
|
+
const POLL_MAX_DELAY = 10_000;
|
|
12
|
+
const POLL_BACKOFF_MULTIPLIER = 1.2;
|
|
13
|
+
export async function generateCursorAuthParams() {
|
|
14
|
+
const { verifier, challenge } = await generatePKCE();
|
|
15
|
+
const uuid = crypto.randomUUID();
|
|
16
|
+
const params = new URLSearchParams({
|
|
17
|
+
challenge,
|
|
18
|
+
uuid,
|
|
19
|
+
mode: "login",
|
|
20
|
+
redirectTarget: "cli",
|
|
21
|
+
});
|
|
22
|
+
const loginUrl = `${CURSOR_LOGIN_URL}?${params.toString()}`;
|
|
23
|
+
return { verifier, challenge, uuid, loginUrl };
|
|
24
|
+
}
|
|
25
|
+
export async function pollCursorAuth(uuid, verifier) {
|
|
26
|
+
let delay = POLL_BASE_DELAY;
|
|
27
|
+
let consecutiveErrors = 0;
|
|
28
|
+
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
29
|
+
await Bun.sleep(delay);
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(`${CURSOR_POLL_URL}?uuid=${uuid}&verifier=${verifier}`);
|
|
32
|
+
if (response.status === 404) {
|
|
33
|
+
// User hasn't completed login yet
|
|
34
|
+
consecutiveErrors = 0;
|
|
35
|
+
delay = Math.min(delay * POLL_BACKOFF_MULTIPLIER, POLL_MAX_DELAY);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const data = (await response.json());
|
|
40
|
+
return {
|
|
41
|
+
accessToken: data.accessToken,
|
|
42
|
+
refreshToken: data.refreshToken,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Poll failed: ${response.status}`);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
consecutiveErrors++;
|
|
49
|
+
if (consecutiveErrors >= 3) {
|
|
50
|
+
throw new Error("Too many consecutive errors during Cursor auth polling");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Cursor authentication polling timeout");
|
|
55
|
+
}
|
|
56
|
+
export async function refreshCursorToken(refreshToken) {
|
|
57
|
+
const response = await fetch(CURSOR_REFRESH_URL, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
body: "{}",
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const error = await response.text();
|
|
67
|
+
throw new Error(`Cursor token refresh failed: ${error}`);
|
|
68
|
+
}
|
|
69
|
+
const data = (await response.json());
|
|
70
|
+
return {
|
|
71
|
+
access: data.accessToken,
|
|
72
|
+
refresh: data.refreshToken || refreshToken,
|
|
73
|
+
expires: getTokenExpiry(data.accessToken),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract JWT expiry with 5-minute safety margin.
|
|
78
|
+
* Falls back to 1 hour from now if token can't be parsed.
|
|
79
|
+
*/
|
|
80
|
+
export function getTokenExpiry(token) {
|
|
81
|
+
try {
|
|
82
|
+
const parts = token.split(".");
|
|
83
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
84
|
+
return Date.now() + 3600 * 1000;
|
|
85
|
+
}
|
|
86
|
+
const decoded = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
87
|
+
if (decoded &&
|
|
88
|
+
typeof decoded === "object" &&
|
|
89
|
+
typeof decoded.exp === "number") {
|
|
90
|
+
// 5-minute safety margin before actual expiry
|
|
91
|
+
return decoded.exp * 1000 - 5 * 60 * 1000;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Ignore parsing errors
|
|
96
|
+
}
|
|
97
|
+
return Date.now() + 3600 * 1000;
|
|
98
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dumb HTTP/2 bidirectional pipe for Cursor gRPC.
|
|
4
|
+
*
|
|
5
|
+
* Bun's node:http2 is broken. This Node script acts as a transparent
|
|
6
|
+
* HTTP/2 proxy: it opens a single bidirectional stream and ferries
|
|
7
|
+
* raw bytes between the parent process (via stdin/stdout) and Cursor.
|
|
8
|
+
*
|
|
9
|
+
* Protocol (length-prefixed framing over stdin/stdout):
|
|
10
|
+
* [4 bytes big-endian length][payload]
|
|
11
|
+
*
|
|
12
|
+
* First message on stdin is JSON config:
|
|
13
|
+
* { "accessToken": "...", "url": "...", "path": "..." }
|
|
14
|
+
*
|
|
15
|
+
* After config, subsequent stdin messages are raw bytes to write to the H2 stream.
|
|
16
|
+
* H2 response data is written to stdout using the same length-prefixed framing.
|
|
17
|
+
*/
|
|
18
|
+
import http2 from "node:http2";
|
|
19
|
+
import crypto from "node:crypto";
|
|
20
|
+
|
|
21
|
+
const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
|
|
22
|
+
|
|
23
|
+
/** Write one length-prefixed message to stdout. */
|
|
24
|
+
function writeMessage(data) {
|
|
25
|
+
const lenBuf = Buffer.alloc(4);
|
|
26
|
+
lenBuf.writeUInt32BE(data.length, 0);
|
|
27
|
+
process.stdout.write(lenBuf);
|
|
28
|
+
process.stdout.write(data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Buffered stdin reader ---
|
|
32
|
+
|
|
33
|
+
let stdinBuf = Buffer.alloc(0);
|
|
34
|
+
let stdinResolve = null;
|
|
35
|
+
let stdinEnded = false;
|
|
36
|
+
|
|
37
|
+
process.stdin.on("data", (chunk) => {
|
|
38
|
+
stdinBuf = Buffer.concat([stdinBuf, chunk]);
|
|
39
|
+
if (stdinResolve) {
|
|
40
|
+
const r = stdinResolve;
|
|
41
|
+
stdinResolve = null;
|
|
42
|
+
r();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
process.stdin.on("end", () => {
|
|
47
|
+
stdinEnded = true;
|
|
48
|
+
if (stdinResolve) {
|
|
49
|
+
const r = stdinResolve;
|
|
50
|
+
stdinResolve = null;
|
|
51
|
+
r();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function waitForData() {
|
|
56
|
+
return new Promise((resolve) => { stdinResolve = resolve; });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readExact(n) {
|
|
60
|
+
while (stdinBuf.length < n) {
|
|
61
|
+
if (stdinEnded) return null;
|
|
62
|
+
await waitForData();
|
|
63
|
+
}
|
|
64
|
+
const result = stdinBuf.subarray(0, n);
|
|
65
|
+
stdinBuf = stdinBuf.subarray(n);
|
|
66
|
+
return Buffer.from(result);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readMessage() {
|
|
70
|
+
const lenBuf = await readExact(4);
|
|
71
|
+
if (!lenBuf) return null;
|
|
72
|
+
const len = lenBuf.readUInt32BE(0);
|
|
73
|
+
if (len === 0) return Buffer.alloc(0);
|
|
74
|
+
return readExact(len);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Main ---
|
|
78
|
+
|
|
79
|
+
const configBuf = await readMessage();
|
|
80
|
+
if (!configBuf) process.exit(1);
|
|
81
|
+
|
|
82
|
+
const config = JSON.parse(configBuf.toString("utf8"));
|
|
83
|
+
const { accessToken, url, path: rpcPath } = config;
|
|
84
|
+
|
|
85
|
+
const client = http2.connect(url || "https://api2.cursor.sh");
|
|
86
|
+
|
|
87
|
+
const timeout = setTimeout(() => {
|
|
88
|
+
client.destroy();
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}, 120_000);
|
|
91
|
+
|
|
92
|
+
client.on("error", () => {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const h2Stream = client.request({
|
|
98
|
+
":method": "POST",
|
|
99
|
+
":path": rpcPath || "/agent.v1.AgentService/Run",
|
|
100
|
+
"content-type": "application/connect+proto",
|
|
101
|
+
"connect-protocol-version": "1",
|
|
102
|
+
te: "trailers",
|
|
103
|
+
authorization: `Bearer ${accessToken}`,
|
|
104
|
+
"x-ghost-mode": "true",
|
|
105
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
106
|
+
"x-cursor-client-type": "cli",
|
|
107
|
+
"x-request-id": crypto.randomUUID(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Forward H2 response data → stdout (length-prefixed)
|
|
111
|
+
h2Stream.on("data", (chunk) => {
|
|
112
|
+
writeMessage(chunk);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
h2Stream.on("end", () => {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
client.close();
|
|
118
|
+
// Give stdout time to flush
|
|
119
|
+
setTimeout(() => process.exit(0), 100);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
h2Stream.on("error", () => {
|
|
123
|
+
clearTimeout(timeout);
|
|
124
|
+
client.close();
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Forward stdin → H2 stream (after config message)
|
|
129
|
+
(async () => {
|
|
130
|
+
while (true) {
|
|
131
|
+
const msg = await readMessage();
|
|
132
|
+
if (!msg || msg.length === 0) {
|
|
133
|
+
// EOF or zero-length = done writing
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
137
|
+
h2Stream.write(msg);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
})();
|
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,157 @@
|
|
|
1
|
+
import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
|
|
2
|
+
import { getCursorModels } from "./models";
|
|
3
|
+
import { startProxy } from "./proxy";
|
|
4
|
+
const CURSOR_PROVIDER_ID = "cursor";
|
|
5
|
+
/**
|
|
6
|
+
* OpenCode plugin that provides Cursor authentication and model access.
|
|
7
|
+
* Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
|
|
8
|
+
*/
|
|
9
|
+
export const CursorAuthPlugin = async (input) => {
|
|
10
|
+
return {
|
|
11
|
+
auth: {
|
|
12
|
+
provider: CURSOR_PROVIDER_ID,
|
|
13
|
+
async loader(getAuth, provider) {
|
|
14
|
+
const auth = await getAuth();
|
|
15
|
+
if (!auth || auth.type !== "oauth")
|
|
16
|
+
return {};
|
|
17
|
+
// Start local proxy if not already running
|
|
18
|
+
const port = await startProxy(async () => {
|
|
19
|
+
const currentAuth = await getAuth();
|
|
20
|
+
if (currentAuth.type !== "oauth") {
|
|
21
|
+
throw new Error("Cursor auth not configured");
|
|
22
|
+
}
|
|
23
|
+
// Refresh token if expired
|
|
24
|
+
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
25
|
+
const refreshed = await refreshCursorToken(currentAuth.refresh);
|
|
26
|
+
await input.client.auth.set({
|
|
27
|
+
path: { id: CURSOR_PROVIDER_ID },
|
|
28
|
+
body: {
|
|
29
|
+
type: "oauth",
|
|
30
|
+
refresh: refreshed.refresh,
|
|
31
|
+
access: refreshed.access,
|
|
32
|
+
expires: refreshed.expires,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return refreshed.access;
|
|
36
|
+
}
|
|
37
|
+
return currentAuth.access;
|
|
38
|
+
});
|
|
39
|
+
// Discover models and inject into provider
|
|
40
|
+
if (provider) {
|
|
41
|
+
if (!provider.models)
|
|
42
|
+
provider.models = {};
|
|
43
|
+
try {
|
|
44
|
+
const models = await getCursorModels(auth.access);
|
|
45
|
+
for (const model of models) {
|
|
46
|
+
if (provider.models[model.id])
|
|
47
|
+
continue;
|
|
48
|
+
// Cast needed: OpenCode's internal Model type requires fields
|
|
49
|
+
// (interleaved, release_date, variants) not in the plugin SDK type.
|
|
50
|
+
provider.models[model.id] = {
|
|
51
|
+
id: model.id,
|
|
52
|
+
providerID: CURSOR_PROVIDER_ID,
|
|
53
|
+
api: {
|
|
54
|
+
id: model.id,
|
|
55
|
+
url: `http://localhost:${port}/v1`,
|
|
56
|
+
npm: "@ai-sdk/openai-compatible",
|
|
57
|
+
},
|
|
58
|
+
name: model.name,
|
|
59
|
+
capabilities: {
|
|
60
|
+
temperature: true,
|
|
61
|
+
reasoning: model.reasoning,
|
|
62
|
+
attachment: false,
|
|
63
|
+
toolcall: true,
|
|
64
|
+
input: {
|
|
65
|
+
text: true,
|
|
66
|
+
audio: false,
|
|
67
|
+
image: false,
|
|
68
|
+
video: false,
|
|
69
|
+
pdf: false,
|
|
70
|
+
},
|
|
71
|
+
output: {
|
|
72
|
+
text: true,
|
|
73
|
+
audio: false,
|
|
74
|
+
image: false,
|
|
75
|
+
video: false,
|
|
76
|
+
pdf: false,
|
|
77
|
+
},
|
|
78
|
+
interleaved: false,
|
|
79
|
+
},
|
|
80
|
+
cost: {
|
|
81
|
+
input: 0,
|
|
82
|
+
output: 0,
|
|
83
|
+
cache: { read: 0, write: 0 },
|
|
84
|
+
},
|
|
85
|
+
limit: {
|
|
86
|
+
context: model.contextWindow,
|
|
87
|
+
output: model.maxTokens,
|
|
88
|
+
},
|
|
89
|
+
status: "active",
|
|
90
|
+
options: {},
|
|
91
|
+
headers: {},
|
|
92
|
+
release_date: "",
|
|
93
|
+
variants: {},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Model discovery failed — proxy still works with direct model IDs
|
|
99
|
+
}
|
|
100
|
+
// Zero out costs for all Cursor models (included with subscription)
|
|
101
|
+
for (const model of Object.values(provider.models)) {
|
|
102
|
+
model.cost = {
|
|
103
|
+
input: 0,
|
|
104
|
+
output: 0,
|
|
105
|
+
cache: { read: 0, write: 0 },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
baseURL: `http://localhost:${port}/v1`,
|
|
111
|
+
apiKey: "cursor-proxy",
|
|
112
|
+
async fetch(requestInput, init) {
|
|
113
|
+
// Strip any dummy auth headers — proxy handles auth internally
|
|
114
|
+
if (init?.headers) {
|
|
115
|
+
if (init.headers instanceof Headers) {
|
|
116
|
+
init.headers.delete("authorization");
|
|
117
|
+
init.headers.delete("Authorization");
|
|
118
|
+
}
|
|
119
|
+
else if (Array.isArray(init.headers)) {
|
|
120
|
+
init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization");
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
delete init.headers["authorization"];
|
|
124
|
+
delete init.headers["Authorization"];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return fetch(requestInput, init);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
methods: [
|
|
132
|
+
{
|
|
133
|
+
type: "oauth",
|
|
134
|
+
label: "Login with Cursor",
|
|
135
|
+
async authorize() {
|
|
136
|
+
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
137
|
+
return {
|
|
138
|
+
url: loginUrl,
|
|
139
|
+
instructions: "Complete login in your browser. This window will close automatically.",
|
|
140
|
+
method: "auto",
|
|
141
|
+
async callback() {
|
|
142
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
143
|
+
return {
|
|
144
|
+
type: "success",
|
|
145
|
+
refresh: refreshToken,
|
|
146
|
+
access: accessToken,
|
|
147
|
+
expires: getTokenExpiry(accessToken),
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
export default CursorAuthPlugin;
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface CursorModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
reasoning: boolean;
|
|
5
|
+
contextWindow: number;
|
|
6
|
+
maxTokens: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CursorModelDiscoveryOptions {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
clientVersion?: string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fetch models from Cursor's GetUsableModels gRPC endpoint.
|
|
16
|
+
* Returns null on failure (caller should use fallback list).
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchCursorUsableModels(options: CursorModelDiscoveryOptions): Promise<CursorModel[] | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Get cursor models: try dynamic discovery, fall back to hardcoded list.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getCursorModels(apiKey: string): Promise<CursorModel[]>;
|