@magpie-community/opencode-cursor-auth 0.1.0 → 0.1.2

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 +93 -7
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -25,6 +25,7 @@
25
25
  // OpenCode speaks chat completions to it (@ai-sdk/openai-compatible); the
26
26
  // fetch here answers them.
27
27
  import http2 from "node:http2"
28
+ import { STATUS_CODES } from "node:http"
28
29
  import { gunzipSync } from "node:zlib"
29
30
  import { createHash, randomBytes, randomUUID } from "node:crypto"
30
31
  import { execFile } from "node:child_process"
@@ -267,7 +268,7 @@ async function browserSignIn() {
267
268
  try {
268
269
  res = await fetch(`${API}/auth/poll?uuid=${uuid}&verifier=${verifier}`, { headers, signal: AbortSignal.timeout(15_000) })
269
270
  } catch {
270
- if (++errors >= 3) return { type: "failed" }
271
+ if (++errors >= 3) return { type: "failed", error: "couldn't reach Cursor" }
271
272
  await sleep(wait)
272
273
  continue
273
274
  }
@@ -277,12 +278,12 @@ async function browserSignIn() {
277
278
  continue
278
279
  }
279
280
  if (!res.ok) {
280
- if (res.status === 403 || ++errors >= 3) return { type: "failed" }
281
+ if (res.status === 403 || ++errors >= 3) return { type: "failed", error: res.status === 403 ? "Cursor refused the sign-in (403)" : `Cursor's sign-in answered HTTP ${res.status}` }
281
282
  await sleep(wait)
282
283
  continue
283
284
  }
284
285
  const j = await res.json().catch(() => null)
285
- if (!j?.accessToken || !("refreshToken" in j)) return { type: "failed" }
286
+ if (!j?.accessToken || !("refreshToken" in j)) return { type: "failed", error: "Cursor sent back no token" }
286
287
  const who = await whoIs(j.accessToken)
287
288
  return {
288
289
  type: "success",
@@ -293,7 +294,7 @@ async function browserSignIn() {
293
294
  ...(who.plan ? { plan: who.plan } : {}),
294
295
  }
295
296
  }
296
- return { type: "failed" }
297
+ return { type: "failed", error: "the sign-in timed out" }
297
298
  },
298
299
  }
299
300
  }
@@ -308,9 +309,10 @@ async function cliSignIn() {
308
309
  async callback() {
309
310
  const tok = await cliToken()
310
311
  const exp = expiry(tok)
311
- if (!tok || (exp && exp <= Date.now())) return { type: "failed" }
312
+ if (!tok) return { type: "failed", error: "cursor-agent isn't signed in: run `cursor-agent login`" }
313
+ if (exp && exp <= Date.now()) return { type: "failed", error: "cursor-agent's sign-in has run out: run `cursor-agent login` again" }
312
314
  const who = await whoIs(tok)
313
- if (!who.email) return { type: "failed" }
315
+ if (!who.email) return { type: "failed", error: "Cursor couldn't say which account is signed in" }
314
316
  return { type: "success", access: "", refresh: CLI_MARK, expires: 0, accountId: who.email, ...(who.plan ? { plan: who.plan } : {}) }
315
317
  },
316
318
  }
@@ -318,6 +320,79 @@ async function cliSignIn() {
318
320
 
319
321
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
320
322
 
323
+ // ---- usage ------------------------------------------------------------------------
324
+ //
325
+ // How much of the plan's included usage is gone, as magpie's built-in Cursor
326
+ // account shows it (internal/provider/cursor_usage.go): the dashboard's
327
+ // current period, split into the Cursor Models and Other Models pools.
328
+
329
+ // statusText is a refused request's status as magpie says it (Go's
330
+ // http.StatusText).
331
+ const statusText = (s) =>
332
+ ({ 413: "Request Entity Too Large", 414: "Request URI Too Long", 416: "Requested Range Not Satisfiable", 418: "I'm a teapot", 509: "" })[s] ?? STATUS_CODES[s] ?? ""
333
+
334
+ const POOL_VARIANT = /-(fast|none|low|medium|high|xhigh|extra-high|max|thinking)$/
335
+
336
+ // poolBase names a model's family: lower case, without cursor- and the
337
+ // effort and speed the CLI adds to it.
338
+ function poolBase(model) {
339
+ model = String(model).trim().toLowerCase()
340
+ if (model.startsWith("cursor-")) model = model.slice("cursor-".length)
341
+ for (;;) {
342
+ const b = model.replace(POOL_VARIANT, "")
343
+ if (b === model) return model
344
+ model = b
345
+ }
346
+ }
347
+
348
+ // Cursor's autoBucketModels can lag model releases: it still omitted Grok
349
+ // 4.6/4.7 when the published Cursor Models pool already included them.
350
+ // Keep those documented families alongside the server's exact model list.
351
+ // See https://cursor.com/docs/models-and-pricing.
352
+ function firstParty(model) {
353
+ if (model.startsWith("cursor-")) model = model.slice("cursor-".length)
354
+ if (model === "default" || model === "composer" || model.startsWith("composer-")) return true
355
+ return ["grok-4.5", "grok-4.6", "grok-4.7"].some((b) => model === b || model.startsWith(b + "-"))
356
+ }
357
+
358
+ // usage is the account's windows this billing period; an enterprise plan
359
+ // reports spend instead, and gets none. ids are the models it can be
360
+ // asked for, each counted by the pool Cursor bills it to.
361
+ async function usage(tok, ids) {
362
+ const res = await fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
363
+ method: "POST",
364
+ headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
365
+ body: "{}",
366
+ signal: AbortSignal.timeout(15_000),
367
+ })
368
+ if (!res.ok) return { error: statusText(res.status), windows: [] }
369
+ const data = await res.json()
370
+ const u = data?.planUsage
371
+ if (!u) return { windows: [] }
372
+ const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
373
+ const resets = ms > 0 ? { resetsAt: new Date(ms).toISOString() } : {}
374
+ const bucket = new Set((data.autoBucketModels ?? []).map(poolBase))
375
+ const inPool = (model) => {
376
+ model = poolBase(model)
377
+ if (model === "auto") model = "default" // the CLI's Auto is default in Cursor's API
378
+ // the server names a family (grok-4.8); the CLI asks for one at an
379
+ // effort or speed (grok-4.8-high-fast)
380
+ return bucket.has(model) || firstParty(model)
381
+ }
382
+ // Auto is always one, so the pool's list is never empty (which would
383
+ // count every model)
384
+ const pool = [...new Set(["auto", ...ids, ...(data.autoBucketModels ?? [])])].filter(inPool)
385
+ const num = (v) => (typeof v === "number" ? v : 0)
386
+ // the two pools fit the line; the total goes in its tooltip
387
+ return {
388
+ windows: [
389
+ { name: "Cursor Models", used: num(u.autoPercentUsed), ...resets, models: pool },
390
+ { name: "Other Models", used: num(u.apiPercentUsed), ...resets, notModels: pool },
391
+ { name: "Total", used: num(u.totalPercentUsed), ...resets, aside: true },
392
+ ],
393
+ }
394
+ }
395
+
321
396
  // ---- models ----------------------------------------------------------------------
322
397
  //
323
398
  // Cursor lists a model once for each effort and speed it serves it at
@@ -1381,6 +1456,17 @@ export async function CursorAuthPlugin() {
1381
1456
  { type: "oauth", label: "cursor-agent's sign-in", authorize: cliSignIn },
1382
1457
  { type: "api", label: "Cursor API key (cursor.com/dashboard → Integrations)" },
1383
1458
  ],
1459
+ // magpie's: how much of the plan's included usage is gone (the plan
1460
+ // is the one the sign-in read)
1461
+ async usage(getAuth, provider) {
1462
+ let tok
1463
+ try {
1464
+ tok = await tokenOf(await getAuth())
1465
+ } catch (e) {
1466
+ return { error: e.message, windows: [] }
1467
+ }
1468
+ return usage(tok, Object.keys(provider?.models ?? {}))
1469
+ },
1384
1470
  },
1385
1471
  async config(config) {
1386
1472
  config.provider ??= {}
@@ -1414,4 +1500,4 @@ export async function CursorAuthPlugin() {
1414
1500
  }
1415
1501
 
1416
1502
  // for tests
1417
- export const _internal = { splitID, families, byEffort, offered, modelID, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
1503
+ export const _internal = { poolBase, splitID, families, byEffort, offered, modelID, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",