cursor-oauth-opencode 0.1.2 → 0.1.3
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/dist/index.js +54 -27
- package/dist/models.d.ts +5 -1
- package/dist/models.js +54 -6
- package/dist/proxy.d.ts +4 -0
- package/dist/proxy.js +15 -2
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
|
|
2
|
-
import { getCursorModels } from "./models";
|
|
3
|
-
import { startProxy } from "./proxy";
|
|
2
|
+
import { getCachedCursorModels, getCursorModels } from "./models";
|
|
3
|
+
import { startProxy, updateProxyModels } from "./proxy";
|
|
4
4
|
const CURSOR_PROVIDER_ID = "cursor";
|
|
5
5
|
/**
|
|
6
6
|
* OpenCode plugin that provides Cursor authentication and model access.
|
|
7
7
|
* Register in opencode.json: { "plugin": ["cursor-oauth-opencode"] }
|
|
8
8
|
*/
|
|
9
9
|
export const CursorAuthPlugin = async (input) => {
|
|
10
|
+
async function persistRefreshedAuth(refreshed) {
|
|
11
|
+
await input.client.auth.set({
|
|
12
|
+
path: { id: CURSOR_PROVIDER_ID },
|
|
13
|
+
body: {
|
|
14
|
+
type: "oauth",
|
|
15
|
+
refresh: refreshed.refresh,
|
|
16
|
+
access: refreshed.access,
|
|
17
|
+
expires: refreshed.expires,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function refreshCursorTokenWithTimeout(refreshToken, timeoutMs) {
|
|
22
|
+
let timeout;
|
|
23
|
+
try {
|
|
24
|
+
return await Promise.race([
|
|
25
|
+
refreshCursorToken(refreshToken),
|
|
26
|
+
new Promise((_, reject) => {
|
|
27
|
+
timeout = setTimeout(() => reject(new Error("Cursor token refresh timed out")), timeoutMs);
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
if (timeout)
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function refreshModelsInBackground(provider, port, accessToken) {
|
|
37
|
+
if (!accessToken)
|
|
38
|
+
return;
|
|
39
|
+
void getCursorModels(accessToken, { timeoutMs: 2_000 })
|
|
40
|
+
.then((models) => {
|
|
41
|
+
updateProxyModels(models);
|
|
42
|
+
if (provider) {
|
|
43
|
+
provider.models = buildCursorProviderModels(models, port);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.catch(() => { });
|
|
47
|
+
}
|
|
10
48
|
return {
|
|
11
49
|
auth: {
|
|
12
50
|
provider: CURSOR_PROVIDER_ID,
|
|
@@ -14,22 +52,8 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
14
52
|
const auth = await getAuth();
|
|
15
53
|
if (!auth || auth.type !== "oauth")
|
|
16
54
|
return {};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!accessToken || auth.expires < Date.now()) {
|
|
20
|
-
const refreshed = await refreshCursorToken(auth.refresh);
|
|
21
|
-
await input.client.auth.set({
|
|
22
|
-
path: { id: CURSOR_PROVIDER_ID },
|
|
23
|
-
body: {
|
|
24
|
-
type: "oauth",
|
|
25
|
-
refresh: refreshed.refresh,
|
|
26
|
-
access: refreshed.access,
|
|
27
|
-
expires: refreshed.expires,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
accessToken = refreshed.access;
|
|
31
|
-
}
|
|
32
|
-
const models = await getCursorModels(accessToken);
|
|
55
|
+
const accessToken = auth.access;
|
|
56
|
+
const models = getCachedCursorModels(accessToken);
|
|
33
57
|
const port = await startProxy(async () => {
|
|
34
58
|
const currentAuth = await getAuth();
|
|
35
59
|
if (currentAuth.type !== "oauth") {
|
|
@@ -37,15 +61,7 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
37
61
|
}
|
|
38
62
|
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
39
63
|
const refreshed = await refreshCursorToken(currentAuth.refresh);
|
|
40
|
-
await
|
|
41
|
-
path: { id: CURSOR_PROVIDER_ID },
|
|
42
|
-
body: {
|
|
43
|
-
type: "oauth",
|
|
44
|
-
refresh: refreshed.refresh,
|
|
45
|
-
access: refreshed.access,
|
|
46
|
-
expires: refreshed.expires,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
64
|
+
await persistRefreshedAuth(refreshed);
|
|
49
65
|
return refreshed.access;
|
|
50
66
|
}
|
|
51
67
|
return currentAuth.access;
|
|
@@ -53,6 +69,17 @@ export const CursorAuthPlugin = async (input) => {
|
|
|
53
69
|
if (provider) {
|
|
54
70
|
provider.models = buildCursorProviderModels(models, port);
|
|
55
71
|
}
|
|
72
|
+
if (accessToken && auth.expires >= Date.now()) {
|
|
73
|
+
refreshModelsInBackground(provider, port, accessToken);
|
|
74
|
+
}
|
|
75
|
+
else if (auth.refresh) {
|
|
76
|
+
void refreshCursorTokenWithTimeout(auth.refresh, 1_500)
|
|
77
|
+
.then(async (refreshed) => {
|
|
78
|
+
await persistRefreshedAuth(refreshed);
|
|
79
|
+
refreshModelsInBackground(provider, port, refreshed.access);
|
|
80
|
+
})
|
|
81
|
+
.catch(() => { });
|
|
82
|
+
}
|
|
56
83
|
return {
|
|
57
84
|
baseURL: `http://localhost:${port}/v1`,
|
|
58
85
|
apiKey: "cursor-proxy",
|
package/dist/models.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface CursorModel {
|
|
|
5
5
|
contextWindow: number;
|
|
6
6
|
maxTokens: number;
|
|
7
7
|
}
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function getCachedCursorModels(apiKey?: string): CursorModel[];
|
|
9
|
+
export declare function getCursorModels(apiKey: string, options?: {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
allowStale?: boolean;
|
|
12
|
+
}): Promise<CursorModel[]>;
|
|
9
13
|
/** @internal Test-only. */
|
|
10
14
|
export declare function clearModelCache(): void;
|
package/dist/models.js
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* when discovery fails.
|
|
5
5
|
*/
|
|
6
6
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { callCursorUnaryRpc } from "./proxy";
|
|
9
10
|
import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb";
|
|
10
11
|
const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
|
|
11
12
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
13
|
const DEFAULT_MAX_TOKENS = 64_000;
|
|
14
|
+
const MODEL_CACHE_TTL_MS = 10 * 60_000;
|
|
15
|
+
const DEFAULT_DISCOVERY_TIMEOUT_MS = 2_000;
|
|
13
16
|
const CursorModelDetailsSchema = z.object({
|
|
14
17
|
modelId: z.string(),
|
|
15
18
|
displayName: z.string().optional().catch(undefined),
|
|
@@ -40,7 +43,7 @@ const FALLBACK_MODELS = [
|
|
|
40
43
|
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 64_000 },
|
|
41
44
|
{ id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 },
|
|
42
45
|
];
|
|
43
|
-
async function fetchCursorUsableModels(apiKey) {
|
|
46
|
+
async function fetchCursorUsableModels(apiKey, timeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS) {
|
|
44
47
|
try {
|
|
45
48
|
const requestPayload = create(GetUsableModelsRequestSchema, {});
|
|
46
49
|
const requestBody = toBinary(GetUsableModelsRequestSchema, requestPayload);
|
|
@@ -48,6 +51,7 @@ async function fetchCursorUsableModels(apiKey) {
|
|
|
48
51
|
accessToken: apiKey,
|
|
49
52
|
rpcPath: GET_USABLE_MODELS_PATH,
|
|
50
53
|
requestBody,
|
|
54
|
+
timeoutMs,
|
|
51
55
|
});
|
|
52
56
|
if (response.timedOut || response.exitCode !== 0 || response.body.length === 0) {
|
|
53
57
|
return null;
|
|
@@ -63,16 +67,60 @@ async function fetchCursorUsableModels(apiKey) {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
let cachedModels = null;
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
let cachedModelKey = "";
|
|
71
|
+
let cachedModelTime = 0;
|
|
72
|
+
let inFlightModelKey = "";
|
|
73
|
+
let inFlightModels = null;
|
|
74
|
+
function modelCacheKey(apiKey) {
|
|
75
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
76
|
+
}
|
|
77
|
+
function isCacheFresh(cacheKey, now = Date.now()) {
|
|
78
|
+
return Boolean(cachedModels && cachedModelKey === cacheKey && now - cachedModelTime < MODEL_CACHE_TTL_MS);
|
|
79
|
+
}
|
|
80
|
+
export function getCachedCursorModels(apiKey) {
|
|
81
|
+
if (!apiKey)
|
|
82
|
+
return FALLBACK_MODELS;
|
|
83
|
+
const cacheKey = modelCacheKey(apiKey);
|
|
84
|
+
if (cachedModels && cachedModelKey === cacheKey)
|
|
85
|
+
return cachedModels;
|
|
86
|
+
return FALLBACK_MODELS;
|
|
87
|
+
}
|
|
88
|
+
export async function getCursorModels(apiKey, options = {}) {
|
|
89
|
+
const cacheKey = modelCacheKey(apiKey);
|
|
90
|
+
if (isCacheFresh(cacheKey))
|
|
91
|
+
return cachedModels;
|
|
92
|
+
if (inFlightModels && inFlightModelKey === cacheKey)
|
|
93
|
+
return inFlightModels;
|
|
94
|
+
inFlightModelKey = cacheKey;
|
|
95
|
+
inFlightModels = (async () => {
|
|
96
|
+
try {
|
|
97
|
+
const discovered = await fetchCursorUsableModels(apiKey, options.timeoutMs);
|
|
98
|
+
cachedModels = discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS;
|
|
99
|
+
cachedModelKey = cacheKey;
|
|
100
|
+
cachedModelTime = Date.now();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
if (!(options.allowStale ?? true) || cachedModelKey !== cacheKey || !cachedModels) {
|
|
104
|
+
cachedModels = FALLBACK_MODELS;
|
|
105
|
+
cachedModelKey = cacheKey;
|
|
106
|
+
cachedModelTime = Date.now();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
inFlightModelKey = "";
|
|
111
|
+
inFlightModels = null;
|
|
112
|
+
}
|
|
68
113
|
return cachedModels;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return cachedModels;
|
|
114
|
+
})();
|
|
115
|
+
return inFlightModels;
|
|
72
116
|
}
|
|
73
117
|
/** @internal Test-only. */
|
|
74
118
|
export function clearModelCache() {
|
|
75
119
|
cachedModels = null;
|
|
120
|
+
cachedModelKey = "";
|
|
121
|
+
cachedModelTime = 0;
|
|
122
|
+
inFlightModelKey = "";
|
|
123
|
+
inFlightModels = null;
|
|
76
124
|
}
|
|
77
125
|
function decodeGetUsableModelsResponse(payload) {
|
|
78
126
|
try {
|
package/dist/proxy.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export declare function callCursorUnaryRpc(options: CursorUnaryRpcOptions): Prom
|
|
|
11
11
|
timedOut: boolean;
|
|
12
12
|
}>;
|
|
13
13
|
export declare function getProxyPort(): number | undefined;
|
|
14
|
+
export declare function updateProxyModels(models: ReadonlyArray<{
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}>): void;
|
|
14
18
|
export declare function startProxy(getAccessToken: () => Promise<string>, models?: ReadonlyArray<{
|
|
15
19
|
id: string;
|
|
16
20
|
name: string;
|
package/dist/proxy.js
CHANGED
|
@@ -170,6 +170,7 @@ export async function callCursorUnaryRpc(options) {
|
|
|
170
170
|
}
|
|
171
171
|
let proxyServer;
|
|
172
172
|
let proxyPort;
|
|
173
|
+
let proxyStartPromise;
|
|
173
174
|
let proxyAccessTokenProvider;
|
|
174
175
|
let proxyModels = [];
|
|
175
176
|
function buildOpenAIModelList(models) {
|
|
@@ -183,14 +184,25 @@ function buildOpenAIModelList(models) {
|
|
|
183
184
|
export function getProxyPort() {
|
|
184
185
|
return proxyPort;
|
|
185
186
|
}
|
|
186
|
-
export
|
|
187
|
-
proxyAccessTokenProvider = getAccessToken;
|
|
187
|
+
export function updateProxyModels(models) {
|
|
188
188
|
proxyModels = models.map((model) => ({
|
|
189
189
|
id: model.id,
|
|
190
190
|
name: model.name,
|
|
191
191
|
}));
|
|
192
|
+
}
|
|
193
|
+
export async function startProxy(getAccessToken, models = []) {
|
|
194
|
+
proxyAccessTokenProvider = getAccessToken;
|
|
195
|
+
updateProxyModels(models);
|
|
192
196
|
if (proxyServer && proxyPort)
|
|
193
197
|
return proxyPort;
|
|
198
|
+
if (proxyStartPromise)
|
|
199
|
+
return proxyStartPromise;
|
|
200
|
+
proxyStartPromise = startProxyInner().finally(() => {
|
|
201
|
+
proxyStartPromise = undefined;
|
|
202
|
+
});
|
|
203
|
+
return proxyStartPromise;
|
|
204
|
+
}
|
|
205
|
+
async function startProxyInner() {
|
|
194
206
|
proxyServer = Bun.serve({
|
|
195
207
|
port: 0,
|
|
196
208
|
idleTimeout: 255, // max — Cursor responses can take 30s+
|
|
@@ -231,6 +243,7 @@ export function stopProxy() {
|
|
|
231
243
|
proxyServer.stop();
|
|
232
244
|
proxyServer = undefined;
|
|
233
245
|
proxyPort = undefined;
|
|
246
|
+
proxyStartPromise = undefined;
|
|
234
247
|
proxyAccessTokenProvider = undefined;
|
|
235
248
|
proxyModels = [];
|
|
236
249
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-oauth-opencode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenCode plugin that connects Cursor's API to OpenCode via OAuth, model discovery, and a local OpenAI-compatible proxy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json && node scripts/copy-runtime.mjs && bun build ./bin/setup.ts --target=node --format=esm --outfile=bin/setup.js --external=jsonc-parser",
|
|
28
|
+
"bench": "bun scripts/bench.ts",
|
|
28
29
|
"test": "bun test/smoke.ts",
|
|
29
30
|
"prepack": "npm run build",
|
|
30
31
|
"prepublishOnly": "npm run build"
|