@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.
Files changed (2) hide show
  1. package/index.mjs +42 -20
  2. 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("Cursor: couldn't reach the API: " + e.message), { status: 502 })
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(`Cursor: exchange_user_api_key: HTTP ${res.status}`), { status: 502 })
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
- const out = (status, message) => ({ status, message: "Cursor: " + message })
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.includes("expired")) return out(401, msg + " — sign in to Cursor again")
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: "Cursor: " + (e?.message ?? String(e)) } })
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: "Cursor: an empty reply" } }
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: "Cursor: an empty reply" } }
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: "Cursor: the reply broke off: EOF" } }
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: "Cursor: the reply broke off: " + (e?.message ?? e) } }
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.startsWith("Cursor") ? e.message : "Cursor: " + 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: "Cursor: only chat completions are served" })
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: "Cursor: a request that isn't JSON" })
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
- return usage(tok, Object.keys(provider?.models ?? {}))
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
- try {
1492
- const tok = await tokenOf(auth)
1493
- return Object.fromEntries(offered(await usable(tok)).map((m) => [m.id, runtimeModel(m)]))
1494
- } catch {
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.2",
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",