cursor-oauth-opencode 0.1.1 → 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 +8 -0
- package/dist/proxy.js +29 -4
- 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,9 +11,17 @@ 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;
|
|
17
21
|
}>): Promise<number>;
|
|
18
22
|
export declare function stopProxy(): void;
|
|
23
|
+
declare function normalizeMcpArgsForOpenCode(toolName: string, args: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
export declare const __test: {
|
|
25
|
+
normalizeMcpArgsForOpenCode: typeof normalizeMcpArgsForOpenCode;
|
|
26
|
+
};
|
|
19
27
|
export {};
|
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
|
}
|
|
@@ -392,6 +405,14 @@ function decodeMcpArgsMap(args) {
|
|
|
392
405
|
}
|
|
393
406
|
return decoded;
|
|
394
407
|
}
|
|
408
|
+
function normalizeMcpArgsForOpenCode(toolName, args) {
|
|
409
|
+
if (toolName !== "read" || typeof args.filePath === "string")
|
|
410
|
+
return args;
|
|
411
|
+
if (typeof args.path !== "string")
|
|
412
|
+
return args;
|
|
413
|
+
const { path, ...rest } = args;
|
|
414
|
+
return { ...rest, filePath: path };
|
|
415
|
+
}
|
|
395
416
|
function blobKey(blobId) {
|
|
396
417
|
return Buffer.from(blobId).toString("hex");
|
|
397
418
|
}
|
|
@@ -731,12 +752,13 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec) {
|
|
|
731
752
|
}
|
|
732
753
|
if (execCase === "mcpArgs") {
|
|
733
754
|
const mcpArgs = execMsg.message.value;
|
|
734
|
-
const
|
|
755
|
+
const toolName = mcpArgs.toolName || mcpArgs.name;
|
|
756
|
+
const decoded = normalizeMcpArgsForOpenCode(toolName, decodeMcpArgsMap(mcpArgs.args ?? {}));
|
|
735
757
|
onMcpExec({
|
|
736
758
|
execId: execMsg.execId,
|
|
737
759
|
execMsgId: execMsg.id,
|
|
738
760
|
toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
|
|
739
|
-
toolName
|
|
761
|
+
toolName,
|
|
740
762
|
decodedArgs: JSON.stringify(decoded),
|
|
741
763
|
});
|
|
742
764
|
return;
|
|
@@ -851,6 +873,9 @@ function handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec) {
|
|
|
851
873
|
// Unknown exec type — log and ignore
|
|
852
874
|
console.error(`[proxy] unhandled exec: ${execCase}`);
|
|
853
875
|
}
|
|
876
|
+
export const __test = {
|
|
877
|
+
normalizeMcpArgsForOpenCode,
|
|
878
|
+
};
|
|
854
879
|
/** Send an exec client message back to Cursor. */
|
|
855
880
|
function sendExecResult(execMsg, messageCase, value, sendFrame) {
|
|
856
881
|
const execClientMessage = create(ExecClientMessageSchema, {
|
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"
|