@magpie-community/opencode-cursor-auth 0.1.2 → 0.1.4
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/index.mjs +42 -20
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -207,6 +207,23 @@ async function whoIs(tok) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// plans are the plan each token's account is on, as `cursor-agent about`
|
|
211
|
+
// names it (GetPlanInfo's planName, magpie's card title), asked again after
|
|
212
|
+
// an hour.
|
|
213
|
+
const plans = new Map()
|
|
214
|
+
const PLAN_TTL = 3600_000
|
|
215
|
+
async function planOf(tok) {
|
|
216
|
+
const had = plans.get(tok)
|
|
217
|
+
if (had && Date.now() - had.at < PLAN_TTL) return had.plan
|
|
218
|
+
try {
|
|
219
|
+
const plan = String((await unary(API, "aiserver.v1.DashboardService/GetPlanInfo", tok))?.planInfo?.planName ?? "").trim()
|
|
220
|
+
if (plan) plans.set(tok, { plan, at: Date.now() })
|
|
221
|
+
return plan || had?.plan || ""
|
|
222
|
+
} catch {
|
|
223
|
+
return had?.plan ?? ""
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
// exchanged are API keys' tokens, by the key's hash.
|
|
211
228
|
const exchanged = new Map()
|
|
212
229
|
|
|
@@ -222,9 +239,9 @@ async function keyToken(key) {
|
|
|
222
239
|
body: "{}",
|
|
223
240
|
signal: AbortSignal.timeout(15_000),
|
|
224
241
|
}).catch((e) => {
|
|
225
|
-
throw Object.assign(new Error("
|
|
242
|
+
throw Object.assign(new Error("couldn't reach Cursor's API: " + e.message), { status: 502 })
|
|
226
243
|
})
|
|
227
|
-
if (res.status >= 500) throw Object.assign(new Error(`
|
|
244
|
+
if (res.status >= 500) throw Object.assign(new Error(`exchange_user_api_key: HTTP ${res.status}`), { status: 502 })
|
|
228
245
|
const j = res.ok ? await res.json().catch(() => null) : null
|
|
229
246
|
if (!j?.accessToken) throw new AuthError("Cursor didn't take this API key")
|
|
230
247
|
exchanged.set(h, { tok: j.accessToken, exp: expiry(j.accessToken) })
|
|
@@ -830,15 +847,21 @@ function failure(status, text) {
|
|
|
830
847
|
return statusOf(status, f.code ?? "", msg)
|
|
831
848
|
}
|
|
832
849
|
|
|
850
|
+
// signInExpired is a message of a token or session run out; a trial, a
|
|
851
|
+
// plan or a link run out is no lapsed sign-in, and magpie marks the
|
|
852
|
+
// account lapsed on a 401.
|
|
853
|
+
const signInExpired = (low) => low.includes("expired") && /token|session|sign[- ]?in|log[- ]?in|auth|credential/.test(low)
|
|
854
|
+
|
|
833
855
|
// statusOf is the status and words for Cursor's error of this code and
|
|
834
856
|
// message. A region the team isn't served in says so, not to sign in.
|
|
835
857
|
function statusOf(status, code, msg) {
|
|
836
|
-
msg ||= `HTTP ${status}`
|
|
858
|
+
msg ||= statusText(status) || `HTTP ${status}`
|
|
837
859
|
const low = msg.toLowerCase()
|
|
838
|
-
|
|
860
|
+
// magpie names the provider before the message itself
|
|
861
|
+
const out = (status, message) => ({ status, message })
|
|
839
862
|
if (regional(msg)) return out(403, msg + " — Cursor serves your team only in some regions and turned this request away; signing in again won't change that")
|
|
840
863
|
if (code === "permission_denied") return out(403, msg)
|
|
841
|
-
if (code === "unauthenticated" || status === 401 || low
|
|
864
|
+
if (code === "unauthenticated" || status === 401 || signInExpired(low)) return out(401, msg + " — sign in to Cursor again")
|
|
842
865
|
if (code === "resource_exhausted" || low.includes("quota") || low.includes("rate limit") || low.includes("usage limit"))
|
|
843
866
|
return out(429, "usage limit reached: " + msg)
|
|
844
867
|
if (low.includes("too long") || low.includes("context length") || low.includes("too many tokens"))
|
|
@@ -1077,7 +1100,7 @@ function open(base, headers, signal) {
|
|
|
1077
1100
|
if (done) return
|
|
1078
1101
|
done = true
|
|
1079
1102
|
session.destroy()
|
|
1080
|
-
resolve({ error: { status: 502, message:
|
|
1103
|
+
resolve({ error: { status: 502, message: e?.message ?? String(e) } })
|
|
1081
1104
|
}
|
|
1082
1105
|
session.on("error", fail)
|
|
1083
1106
|
const req = session.request({
|
|
@@ -1152,7 +1175,7 @@ async function runOnce({ tok, base, id, tools, conv, signal }) {
|
|
|
1152
1175
|
// an error comes before anything of the answer: out of quota, a model the
|
|
1153
1176
|
// plan doesn't have — answered with its own status
|
|
1154
1177
|
const head = await wrapped.next()
|
|
1155
|
-
if (head.done) return { error: { status: 502, message: "
|
|
1178
|
+
if (head.done) return { error: { status: 502, message: "an empty reply" } }
|
|
1156
1179
|
if (head.value.error) {
|
|
1157
1180
|
await wrapped.return()
|
|
1158
1181
|
return { error: head.value.error }
|
|
@@ -1192,7 +1215,7 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1192
1215
|
if (f.end) {
|
|
1193
1216
|
const e = failure(200, f.data)
|
|
1194
1217
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
1195
|
-
else if (said === 0 && calls === 0) yield { error: { status: 502, message: "
|
|
1218
|
+
else if (said === 0 && calls === 0) yield { error: { status: 502, message: "an empty reply" } }
|
|
1196
1219
|
else yield finish()
|
|
1197
1220
|
return
|
|
1198
1221
|
}
|
|
@@ -1275,13 +1298,13 @@ async function* decode(stream, { send, blobs, tools, estimate }) {
|
|
|
1275
1298
|
yield finish()
|
|
1276
1299
|
return
|
|
1277
1300
|
}
|
|
1278
|
-
yield { error: { status: 502, message: "
|
|
1301
|
+
yield { error: { status: 502, message: "the reply broke off: EOF" } }
|
|
1279
1302
|
} catch (e) {
|
|
1280
1303
|
if (calls > 0 && /EOF/.test(e?.message ?? "")) {
|
|
1281
1304
|
yield finish()
|
|
1282
1305
|
return
|
|
1283
1306
|
}
|
|
1284
|
-
yield { error: { status: 502, message: "
|
|
1307
|
+
yield { error: { status: 502, message: "the reply broke off: " + (e?.message ?? e) } }
|
|
1285
1308
|
}
|
|
1286
1309
|
}
|
|
1287
1310
|
|
|
@@ -1347,7 +1370,7 @@ async function answer(auth, chat, signal) {
|
|
|
1347
1370
|
try {
|
|
1348
1371
|
tok = await tokenOf(auth)
|
|
1349
1372
|
} catch (e) {
|
|
1350
|
-
return errorResponse({ status: e.status ?? 401, message: e.message
|
|
1373
|
+
return errorResponse({ status: e.status ?? 401, message: e.message })
|
|
1351
1374
|
}
|
|
1352
1375
|
const model = String(chat.model ?? "")
|
|
1353
1376
|
let raw = []
|
|
@@ -1439,13 +1462,13 @@ export async function CursorAuthPlugin() {
|
|
|
1439
1462
|
async fetch(input, init = {}) {
|
|
1440
1463
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
|
1441
1464
|
if (!/\/chat\/completions$/.test(new URL(url).pathname))
|
|
1442
|
-
return errorResponse({ status: 404, message: "
|
|
1465
|
+
return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1443
1466
|
let chat
|
|
1444
1467
|
try {
|
|
1445
1468
|
const b = init.body ?? (input instanceof Request ? await input.clone().text() : undefined)
|
|
1446
1469
|
chat = JSON.parse(typeof b === "string" ? b : new TextDecoder().decode(b))
|
|
1447
1470
|
} catch {
|
|
1448
|
-
return errorResponse({ status: 400, message: "
|
|
1471
|
+
return errorResponse({ status: 400, message: "a request that isn't JSON" })
|
|
1449
1472
|
}
|
|
1450
1473
|
return answer((await getAuth()) ?? auth, chat, init.signal ?? (input instanceof Request ? input.signal : undefined))
|
|
1451
1474
|
},
|
|
@@ -1465,7 +1488,8 @@ export async function CursorAuthPlugin() {
|
|
|
1465
1488
|
} catch (e) {
|
|
1466
1489
|
return { error: e.message, windows: [] }
|
|
1467
1490
|
}
|
|
1468
|
-
|
|
1491
|
+
const [u, plan] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok)])
|
|
1492
|
+
return plan ? { ...u, plan } : u
|
|
1469
1493
|
},
|
|
1470
1494
|
},
|
|
1471
1495
|
async config(config) {
|
|
@@ -1488,12 +1512,10 @@ export async function CursorAuthPlugin() {
|
|
|
1488
1512
|
id: ID,
|
|
1489
1513
|
async models(provider, { auth } = {}) {
|
|
1490
1514
|
if (!auth) return provider.models
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
return provider.models
|
|
1496
|
-
}
|
|
1515
|
+
// a list Cursor couldn't give is a failure, not the few models
|
|
1516
|
+
// configured: magpie keeps the list it had
|
|
1517
|
+
const tok = await tokenOf(auth)
|
|
1518
|
+
return Object.fromEntries(offered(await usable(tok)).map((m) => [m.id, runtimeModel(m)]))
|
|
1497
1519
|
},
|
|
1498
1520
|
},
|
|
1499
1521
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Cursor subscriptions (Pro, Pro+, Ultra, Teams) in OpenCode and magpie, through the API cursor-agent talks to",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|