@playwo/opencode-cursor-oauth 0.0.0-dev.c80ebcb27754 → 0.0.0-dev.d7836f7ad39f
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 +19 -91
- package/dist/auth.js +27 -3
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/cursor/bidi-session.d.ts +13 -0
- package/dist/cursor/bidi-session.js +149 -0
- package/dist/cursor/config.d.ts +4 -0
- package/dist/cursor/config.js +4 -0
- package/dist/cursor/connect-framing.d.ts +10 -0
- package/dist/cursor/connect-framing.js +80 -0
- package/dist/cursor/headers.d.ts +6 -0
- package/dist/cursor/headers.js +16 -0
- package/dist/cursor/index.d.ts +5 -0
- package/dist/cursor/index.js +5 -0
- package/dist/cursor/unary-rpc.d.ts +13 -0
- package/dist/cursor/unary-rpc.js +181 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -229
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +147 -0
- package/dist/models.d.ts +3 -0
- package/dist/models.js +80 -54
- package/dist/openai/index.d.ts +3 -0
- package/dist/openai/index.js +3 -0
- package/dist/openai/messages.d.ts +40 -0
- package/dist/openai/messages.js +231 -0
- package/dist/openai/tools.d.ts +7 -0
- package/dist/openai/tools.js +58 -0
- package/dist/openai/types.d.ts +41 -0
- package/dist/openai/types.js +1 -0
- package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
- package/dist/plugin/cursor-auth-plugin.js +140 -0
- package/dist/proto/agent_pb.js +637 -319
- package/dist/provider/index.d.ts +2 -0
- package/dist/provider/index.js +2 -0
- package/dist/provider/model-cost.d.ts +9 -0
- package/dist/provider/model-cost.js +206 -0
- package/dist/provider/models.d.ts +8 -0
- package/dist/provider/models.js +86 -0
- package/dist/proxy/bridge-non-streaming.d.ts +3 -0
- package/dist/proxy/bridge-non-streaming.js +119 -0
- package/dist/proxy/bridge-session.d.ts +5 -0
- package/dist/proxy/bridge-session.js +13 -0
- package/dist/proxy/bridge-streaming.d.ts +5 -0
- package/dist/proxy/bridge-streaming.js +311 -0
- package/dist/proxy/bridge.d.ts +3 -0
- package/dist/proxy/bridge.js +3 -0
- package/dist/proxy/chat-completion.d.ts +2 -0
- package/dist/proxy/chat-completion.js +138 -0
- package/dist/proxy/conversation-meta.d.ts +12 -0
- package/dist/proxy/conversation-meta.js +1 -0
- package/dist/proxy/conversation-state.d.ts +35 -0
- package/dist/proxy/conversation-state.js +95 -0
- package/dist/proxy/cursor-request.d.ts +6 -0
- package/dist/proxy/cursor-request.js +104 -0
- package/dist/proxy/index.d.ts +12 -0
- package/dist/proxy/index.js +12 -0
- package/dist/proxy/server.d.ts +6 -0
- package/dist/proxy/server.js +89 -0
- package/dist/proxy/sse.d.ts +5 -0
- package/dist/proxy/sse.js +5 -0
- package/dist/proxy/state-sync.d.ts +2 -0
- package/dist/proxy/state-sync.js +17 -0
- package/dist/proxy/stream-dispatch.d.ts +42 -0
- package/dist/proxy/stream-dispatch.js +491 -0
- package/dist/proxy/stream-state.d.ts +9 -0
- package/dist/proxy/stream-state.js +1 -0
- package/dist/proxy/title.d.ts +1 -0
- package/dist/proxy/title.js +103 -0
- package/dist/proxy/types.d.ts +27 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy.d.ts +2 -19
- package/dist/proxy.js +2 -1221
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { connect as connectHttp2, } from "node:http2";
|
|
2
|
+
import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
|
|
3
|
+
import { toFetchBody } from "./connect-framing";
|
|
4
|
+
import { buildCursorHeaders, buildCursorHeaderValues } from "./headers";
|
|
5
|
+
import { errorDetails, logPluginError } from "../logger";
|
|
6
|
+
export async function callCursorUnaryRpc(options) {
|
|
7
|
+
const target = new URL(options.rpcPath, options.url ?? CURSOR_API_URL);
|
|
8
|
+
const transport = options.transport ?? "auto";
|
|
9
|
+
if (transport === "http2" ||
|
|
10
|
+
(transport === "auto" && target.protocol === "https:")) {
|
|
11
|
+
const http2Result = await callCursorUnaryRpcOverHttp2(options, target);
|
|
12
|
+
if (transport === "http2" ||
|
|
13
|
+
http2Result.timedOut ||
|
|
14
|
+
http2Result.exitCode !== 1) {
|
|
15
|
+
return http2Result;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return callCursorUnaryRpcOverFetch(options, target);
|
|
19
|
+
}
|
|
20
|
+
async function callCursorUnaryRpcOverFetch(options, target) {
|
|
21
|
+
let timedOut = false;
|
|
22
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeout = timeoutMs > 0
|
|
25
|
+
? setTimeout(() => {
|
|
26
|
+
timedOut = true;
|
|
27
|
+
controller.abort();
|
|
28
|
+
}, timeoutMs)
|
|
29
|
+
: undefined;
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(target, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: buildCursorHeaders(options, "application/proto", {
|
|
34
|
+
accept: "application/proto, application/json",
|
|
35
|
+
"connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
|
|
36
|
+
"connect-timeout-ms": String(timeoutMs),
|
|
37
|
+
}),
|
|
38
|
+
body: toFetchBody(options.requestBody),
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
42
|
+
return {
|
|
43
|
+
body,
|
|
44
|
+
exitCode: response.ok ? 0 : response.status,
|
|
45
|
+
timedOut,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
logPluginError("Cursor unary fetch transport failed", {
|
|
50
|
+
rpcPath: options.rpcPath,
|
|
51
|
+
url: target.toString(),
|
|
52
|
+
timeoutMs,
|
|
53
|
+
timedOut,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
body: new Uint8Array(),
|
|
57
|
+
exitCode: timedOut ? 124 : 1,
|
|
58
|
+
timedOut,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (timeout)
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function callCursorUnaryRpcOverHttp2(options, target) {
|
|
67
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
68
|
+
const authority = `${target.protocol}//${target.host}`;
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
let settled = false;
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
let session;
|
|
73
|
+
let stream;
|
|
74
|
+
const finish = (result) => {
|
|
75
|
+
if (settled)
|
|
76
|
+
return;
|
|
77
|
+
settled = true;
|
|
78
|
+
if (timeout)
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
try {
|
|
81
|
+
stream?.close();
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
try {
|
|
85
|
+
session?.close();
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
resolve(result);
|
|
89
|
+
};
|
|
90
|
+
const timeout = timeoutMs > 0
|
|
91
|
+
? setTimeout(() => {
|
|
92
|
+
timedOut = true;
|
|
93
|
+
finish({
|
|
94
|
+
body: new Uint8Array(),
|
|
95
|
+
exitCode: 124,
|
|
96
|
+
timedOut: true,
|
|
97
|
+
});
|
|
98
|
+
}, timeoutMs)
|
|
99
|
+
: undefined;
|
|
100
|
+
try {
|
|
101
|
+
session = connectHttp2(authority);
|
|
102
|
+
session.once("error", (error) => {
|
|
103
|
+
logPluginError("Cursor unary HTTP/2 session failed", {
|
|
104
|
+
rpcPath: options.rpcPath,
|
|
105
|
+
url: target.toString(),
|
|
106
|
+
timedOut,
|
|
107
|
+
...errorDetails(error),
|
|
108
|
+
});
|
|
109
|
+
finish({
|
|
110
|
+
body: new Uint8Array(),
|
|
111
|
+
exitCode: timedOut ? 124 : 1,
|
|
112
|
+
timedOut,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const headers = {
|
|
116
|
+
":method": "POST",
|
|
117
|
+
":path": `${target.pathname}${target.search}`,
|
|
118
|
+
...buildCursorHeaderValues(options, "application/proto", {
|
|
119
|
+
accept: "application/proto, application/json",
|
|
120
|
+
"connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
|
|
121
|
+
"connect-timeout-ms": String(timeoutMs),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
stream = session.request(headers);
|
|
125
|
+
let statusCode = 0;
|
|
126
|
+
const chunks = [];
|
|
127
|
+
stream.once("response", (responseHeaders) => {
|
|
128
|
+
const statusHeader = responseHeaders[":status"];
|
|
129
|
+
statusCode =
|
|
130
|
+
typeof statusHeader === "number"
|
|
131
|
+
? statusHeader
|
|
132
|
+
: Number(statusHeader ?? 0);
|
|
133
|
+
});
|
|
134
|
+
stream.on("data", (chunk) => {
|
|
135
|
+
chunks.push(Buffer.from(chunk));
|
|
136
|
+
});
|
|
137
|
+
stream.once("end", () => {
|
|
138
|
+
const body = new Uint8Array(Buffer.concat(chunks));
|
|
139
|
+
finish({
|
|
140
|
+
body,
|
|
141
|
+
exitCode: statusCode >= 200 && statusCode < 300 ? 0 : statusCode || 1,
|
|
142
|
+
timedOut,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
stream.once("error", (error) => {
|
|
146
|
+
logPluginError("Cursor unary HTTP/2 stream failed", {
|
|
147
|
+
rpcPath: options.rpcPath,
|
|
148
|
+
url: target.toString(),
|
|
149
|
+
timedOut,
|
|
150
|
+
...errorDetails(error),
|
|
151
|
+
});
|
|
152
|
+
finish({
|
|
153
|
+
body: new Uint8Array(),
|
|
154
|
+
exitCode: timedOut ? 124 : 1,
|
|
155
|
+
timedOut,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
// Bun's node:http2 client currently breaks on end(Buffer.alloc(0)) against
|
|
159
|
+
// Cursor's HTTPS endpoint, but a header-only end() succeeds for empty unary bodies.
|
|
160
|
+
if (options.requestBody.length > 0) {
|
|
161
|
+
stream.end(Buffer.from(options.requestBody));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
stream.end();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
logPluginError("Cursor unary HTTP/2 setup failed", {
|
|
169
|
+
rpcPath: options.rpcPath,
|
|
170
|
+
url: target.toString(),
|
|
171
|
+
timedOut,
|
|
172
|
+
...errorDetails(error),
|
|
173
|
+
});
|
|
174
|
+
finish({
|
|
175
|
+
body: new Uint8Array(),
|
|
176
|
+
exitCode: timedOut ? 124 : 1,
|
|
177
|
+
timedOut,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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;
|
|
1
|
+
export { CursorAuthPlugin } from "./plugin/cursor-auth-plugin";
|
|
2
|
+
export { CursorAuthPlugin as default } from "./plugin/cursor-auth-plugin";
|
package/dist/index.js
CHANGED
|
@@ -1,229 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
// Ensure we have a valid access token, refreshing if expired
|
|
18
|
-
let accessToken = auth.access;
|
|
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);
|
|
33
|
-
const port = await startProxy(async () => {
|
|
34
|
-
const currentAuth = await getAuth();
|
|
35
|
-
if (currentAuth.type !== "oauth") {
|
|
36
|
-
throw new Error("Cursor auth not configured");
|
|
37
|
-
}
|
|
38
|
-
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
39
|
-
const refreshed = await refreshCursorToken(currentAuth.refresh);
|
|
40
|
-
await input.client.auth.set({
|
|
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
|
-
});
|
|
49
|
-
return refreshed.access;
|
|
50
|
-
}
|
|
51
|
-
return currentAuth.access;
|
|
52
|
-
}, models);
|
|
53
|
-
if (provider) {
|
|
54
|
-
provider.models = buildCursorProviderModels(models, port);
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
baseURL: `http://localhost:${port}/v1`,
|
|
58
|
-
apiKey: "cursor-proxy",
|
|
59
|
-
async fetch(requestInput, init) {
|
|
60
|
-
if (init?.headers) {
|
|
61
|
-
if (init.headers instanceof Headers) {
|
|
62
|
-
init.headers.delete("authorization");
|
|
63
|
-
}
|
|
64
|
-
else if (Array.isArray(init.headers)) {
|
|
65
|
-
init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization");
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
delete init.headers["authorization"];
|
|
69
|
-
delete init.headers["Authorization"];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return fetch(requestInput, init);
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
methods: [
|
|
77
|
-
{
|
|
78
|
-
type: "oauth",
|
|
79
|
-
label: "Login with Cursor",
|
|
80
|
-
async authorize() {
|
|
81
|
-
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
82
|
-
return {
|
|
83
|
-
url: loginUrl,
|
|
84
|
-
instructions: "Complete login in your browser. This window will close automatically.",
|
|
85
|
-
method: "auto",
|
|
86
|
-
async callback() {
|
|
87
|
-
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
88
|
-
return {
|
|
89
|
-
type: "success",
|
|
90
|
-
refresh: refreshToken,
|
|
91
|
-
access: accessToken,
|
|
92
|
-
expires: getTokenExpiry(accessToken),
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
};
|
|
102
|
-
function buildCursorProviderModels(models, port) {
|
|
103
|
-
return Object.fromEntries(models.map((model) => [
|
|
104
|
-
model.id,
|
|
105
|
-
{
|
|
106
|
-
id: model.id,
|
|
107
|
-
providerID: CURSOR_PROVIDER_ID,
|
|
108
|
-
api: {
|
|
109
|
-
id: model.id,
|
|
110
|
-
url: `http://localhost:${port}/v1`,
|
|
111
|
-
npm: "@ai-sdk/openai-compatible",
|
|
112
|
-
},
|
|
113
|
-
name: model.name,
|
|
114
|
-
capabilities: {
|
|
115
|
-
temperature: true,
|
|
116
|
-
reasoning: model.reasoning,
|
|
117
|
-
attachment: false,
|
|
118
|
-
toolcall: true,
|
|
119
|
-
input: {
|
|
120
|
-
text: true,
|
|
121
|
-
audio: false,
|
|
122
|
-
image: false,
|
|
123
|
-
video: false,
|
|
124
|
-
pdf: false,
|
|
125
|
-
},
|
|
126
|
-
output: {
|
|
127
|
-
text: true,
|
|
128
|
-
audio: false,
|
|
129
|
-
image: false,
|
|
130
|
-
video: false,
|
|
131
|
-
pdf: false,
|
|
132
|
-
},
|
|
133
|
-
interleaved: false,
|
|
134
|
-
},
|
|
135
|
-
cost: estimateModelCost(model.id),
|
|
136
|
-
limit: {
|
|
137
|
-
context: model.contextWindow,
|
|
138
|
-
output: model.maxTokens,
|
|
139
|
-
},
|
|
140
|
-
status: "active",
|
|
141
|
-
options: {},
|
|
142
|
-
headers: {},
|
|
143
|
-
release_date: "",
|
|
144
|
-
variants: {},
|
|
145
|
-
},
|
|
146
|
-
]));
|
|
147
|
-
}
|
|
148
|
-
// $/M token rates from cursor.com/docs/models-and-pricing
|
|
149
|
-
const MODEL_COST_TABLE = {
|
|
150
|
-
// Anthropic
|
|
151
|
-
"claude-4-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
152
|
-
"claude-4-sonnet-1m": { input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } },
|
|
153
|
-
"claude-4.5-haiku": { input: 1, output: 5, cache: { read: 0.1, write: 1.25 } },
|
|
154
|
-
"claude-4.5-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
|
|
155
|
-
"claude-4.5-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
156
|
-
"claude-4.6-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
|
|
157
|
-
"claude-4.6-opus-fast": { input: 30, output: 150, cache: { read: 3, write: 37.5 } },
|
|
158
|
-
"claude-4.6-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
159
|
-
// Cursor
|
|
160
|
-
"composer-1": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
161
|
-
"composer-1.5": { input: 3.5, output: 17.5, cache: { read: 0.35, write: 0 } },
|
|
162
|
-
"composer-2": { input: 0.5, output: 2.5, cache: { read: 0.2, write: 0 } },
|
|
163
|
-
"composer-2-fast": { input: 1.5, output: 7.5, cache: { read: 0.2, write: 0 } },
|
|
164
|
-
// Google
|
|
165
|
-
"gemini-2.5-flash": { input: 0.3, output: 2.5, cache: { read: 0.03, write: 0 } },
|
|
166
|
-
"gemini-3-flash": { input: 0.5, output: 3, cache: { read: 0.05, write: 0 } },
|
|
167
|
-
"gemini-3-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
168
|
-
"gemini-3-pro-image": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
169
|
-
"gemini-3.1-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
170
|
-
// OpenAI
|
|
171
|
-
"gpt-5": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
172
|
-
"gpt-5-fast": { input: 2.5, output: 20, cache: { read: 0.25, write: 0 } },
|
|
173
|
-
"gpt-5-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
|
|
174
|
-
"gpt-5-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
175
|
-
"gpt-5.1-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
176
|
-
"gpt-5.1-codex-max": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
177
|
-
"gpt-5.1-codex-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
|
|
178
|
-
"gpt-5.2": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
179
|
-
"gpt-5.2-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
180
|
-
"gpt-5.3-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
181
|
-
"gpt-5.4": { input: 2.5, output: 15, cache: { read: 0.25, write: 0 } },
|
|
182
|
-
"gpt-5.4-mini": { input: 0.75, output: 4.5, cache: { read: 0.075, write: 0 } },
|
|
183
|
-
"gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } },
|
|
184
|
-
// xAI
|
|
185
|
-
"grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } },
|
|
186
|
-
// Moonshot
|
|
187
|
-
"kimi-k2.5": { input: 0.6, output: 3, cache: { read: 0.1, write: 0 } },
|
|
188
|
-
};
|
|
189
|
-
// Most-specific first
|
|
190
|
-
const MODEL_COST_PATTERNS = [
|
|
191
|
-
{ match: (id) => /claude.*opus.*fast/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus-fast"] },
|
|
192
|
-
{ match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"] },
|
|
193
|
-
{ match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"] },
|
|
194
|
-
{ match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
|
|
195
|
-
{ match: (id) => /claude/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
|
|
196
|
-
{ match: (id) => /composer-?2/i.test(id), cost: MODEL_COST_TABLE["composer-2"] },
|
|
197
|
-
{ match: (id) => /composer-?1\.5/i.test(id), cost: MODEL_COST_TABLE["composer-1.5"] },
|
|
198
|
-
{ match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] },
|
|
199
|
-
{ match: (id) => /gpt-5\.4.*nano/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-nano"] },
|
|
200
|
-
{ match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"] },
|
|
201
|
-
{ match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] },
|
|
202
|
-
{ match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] },
|
|
203
|
-
{ match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] },
|
|
204
|
-
{ match: (id) => /gpt-5\.1.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex-mini"] },
|
|
205
|
-
{ match: (id) => /gpt-5\.1/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex"] },
|
|
206
|
-
{ match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] },
|
|
207
|
-
{ match: (id) => /gpt-5.*fast/i.test(id), cost: MODEL_COST_TABLE["gpt-5-fast"] },
|
|
208
|
-
{ match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] },
|
|
209
|
-
{ match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
|
|
210
|
-
{ match: (id) => /gemini.*3.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-3-flash"] },
|
|
211
|
-
{ match: (id) => /gemini.*3/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] },
|
|
212
|
-
{ match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"] },
|
|
213
|
-
{ match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
|
|
214
|
-
{ match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] },
|
|
215
|
-
{ match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] },
|
|
216
|
-
];
|
|
217
|
-
const DEFAULT_COST = { input: 3, output: 15, cache: { read: 0.3, write: 0 } };
|
|
218
|
-
function estimateModelCost(modelId) {
|
|
219
|
-
const normalized = modelId.toLowerCase();
|
|
220
|
-
const exact = MODEL_COST_TABLE[normalized];
|
|
221
|
-
if (exact)
|
|
222
|
-
return exact;
|
|
223
|
-
const stripped = normalized.replace(/-(high|medium|low|preview|thinking|spark-preview)$/g, "");
|
|
224
|
-
const strippedMatch = MODEL_COST_TABLE[stripped];
|
|
225
|
-
if (strippedMatch)
|
|
226
|
-
return strippedMatch;
|
|
227
|
-
return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
|
|
228
|
-
}
|
|
229
|
-
export default CursorAuthPlugin;
|
|
1
|
+
export { CursorAuthPlugin } from "./plugin/cursor-auth-plugin";
|
|
2
|
+
export { CursorAuthPlugin as default } from "./plugin/cursor-auth-plugin";
|
package/dist/logger.d.ts
ADDED
|
@@ -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,147 @@
|
|
|
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
|
|
107
|
+
.slice(0, MAX_ARRAY_LENGTH)
|
|
108
|
+
.map((entry) => serializeValue(entry, depth + 1, seen));
|
|
109
|
+
}
|
|
110
|
+
if (typeof value === "object") {
|
|
111
|
+
if (seen.has(value))
|
|
112
|
+
return "[circular]";
|
|
113
|
+
seen.add(value);
|
|
114
|
+
if (depth >= 3) {
|
|
115
|
+
return `[object ${value.constructor?.name || "Object"}]`;
|
|
116
|
+
}
|
|
117
|
+
const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
|
|
118
|
+
return Object.fromEntries(entries.map(([key, entry]) => [
|
|
119
|
+
key,
|
|
120
|
+
serializeValue(entry, depth + 1, seen),
|
|
121
|
+
]));
|
|
122
|
+
}
|
|
123
|
+
return String(value);
|
|
124
|
+
}
|
|
125
|
+
function serializeBinary(value) {
|
|
126
|
+
const text = new TextDecoder().decode(value);
|
|
127
|
+
const printable = /^[\x09\x0a\x0d\x20-\x7e]*$/.test(text);
|
|
128
|
+
if (printable) {
|
|
129
|
+
return {
|
|
130
|
+
type: "uint8array",
|
|
131
|
+
length: value.length,
|
|
132
|
+
text: truncateString(text),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
type: "uint8array",
|
|
137
|
+
length: value.length,
|
|
138
|
+
base64: truncateString(Buffer.from(value).toString("base64")),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function truncateString(value) {
|
|
142
|
+
if (value === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
if (value.length <= MAX_STRING_LENGTH)
|
|
145
|
+
return value;
|
|
146
|
+
return `${value.slice(0, MAX_STRING_LENGTH - 3)}...`;
|
|
147
|
+
}
|
package/dist/models.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export interface CursorModel {
|
|
|
5
5
|
contextWindow: number;
|
|
6
6
|
maxTokens: number;
|
|
7
7
|
}
|
|
8
|
+
export declare class CursorModelDiscoveryError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
8
11
|
export declare function getCursorModels(apiKey: string): Promise<CursorModel[]>;
|
|
9
12
|
/** @internal Test-only. */
|
|
10
13
|
export declare function clearModelCache(): void;
|