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

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 +86 -1
  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"
@@ -318,6 +319,79 @@ async function cliSignIn() {
318
319
 
319
320
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
320
321
 
322
+ // ---- usage ------------------------------------------------------------------------
323
+ //
324
+ // How much of the plan's included usage is gone, as magpie's built-in Cursor
325
+ // account shows it (internal/provider/cursor_usage.go): the dashboard's
326
+ // current period, split into the Cursor Models and Other Models pools.
327
+
328
+ // statusText is a refused request's status as magpie says it (Go's
329
+ // http.StatusText).
330
+ const statusText = (s) =>
331
+ ({ 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] ?? ""
332
+
333
+ const POOL_VARIANT = /-(fast|none|low|medium|high|xhigh|extra-high|max|thinking)$/
334
+
335
+ // poolBase names a model's family: lower case, without cursor- and the
336
+ // effort and speed the CLI adds to it.
337
+ function poolBase(model) {
338
+ model = String(model).trim().toLowerCase()
339
+ if (model.startsWith("cursor-")) model = model.slice("cursor-".length)
340
+ for (;;) {
341
+ const b = model.replace(POOL_VARIANT, "")
342
+ if (b === model) return model
343
+ model = b
344
+ }
345
+ }
346
+
347
+ // Cursor's autoBucketModels can lag model releases: it still omitted Grok
348
+ // 4.6/4.7 when the published Cursor Models pool already included them.
349
+ // Keep those documented families alongside the server's exact model list.
350
+ // See https://cursor.com/docs/models-and-pricing.
351
+ function firstParty(model) {
352
+ if (model.startsWith("cursor-")) model = model.slice("cursor-".length)
353
+ if (model === "default" || model === "composer" || model.startsWith("composer-")) return true
354
+ return ["grok-4.5", "grok-4.6", "grok-4.7"].some((b) => model === b || model.startsWith(b + "-"))
355
+ }
356
+
357
+ // usage is the account's windows this billing period; an enterprise plan
358
+ // reports spend instead, and gets none. ids are the models it can be
359
+ // asked for, each counted by the pool Cursor bills it to.
360
+ async function usage(tok, ids) {
361
+ const res = await fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
362
+ method: "POST",
363
+ headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
364
+ body: "{}",
365
+ signal: AbortSignal.timeout(15_000),
366
+ })
367
+ if (!res.ok) return { error: statusText(res.status), windows: [] }
368
+ const data = await res.json()
369
+ const u = data?.planUsage
370
+ if (!u) return { windows: [] }
371
+ const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
372
+ const resets = ms > 0 ? { resetsAt: new Date(ms).toISOString() } : {}
373
+ const bucket = new Set((data.autoBucketModels ?? []).map(poolBase))
374
+ const inPool = (model) => {
375
+ model = poolBase(model)
376
+ if (model === "auto") model = "default" // the CLI's Auto is default in Cursor's API
377
+ // the server names a family (grok-4.8); the CLI asks for one at an
378
+ // effort or speed (grok-4.8-high-fast)
379
+ return bucket.has(model) || firstParty(model)
380
+ }
381
+ // Auto is always one, so the pool's list is never empty (which would
382
+ // count every model)
383
+ const pool = [...new Set(["auto", ...ids, ...(data.autoBucketModels ?? [])])].filter(inPool)
384
+ const num = (v) => (typeof v === "number" ? v : 0)
385
+ // the two pools fit the line; the total goes in its tooltip
386
+ return {
387
+ windows: [
388
+ { name: "Cursor Models", used: num(u.autoPercentUsed), ...resets, models: pool },
389
+ { name: "Other Models", used: num(u.apiPercentUsed), ...resets, notModels: pool },
390
+ { name: "Total", used: num(u.totalPercentUsed), ...resets, aside: true },
391
+ ],
392
+ }
393
+ }
394
+
321
395
  // ---- models ----------------------------------------------------------------------
322
396
  //
323
397
  // Cursor lists a model once for each effort and speed it serves it at
@@ -1381,6 +1455,17 @@ export async function CursorAuthPlugin() {
1381
1455
  { type: "oauth", label: "cursor-agent's sign-in", authorize: cliSignIn },
1382
1456
  { type: "api", label: "Cursor API key (cursor.com/dashboard → Integrations)" },
1383
1457
  ],
1458
+ // magpie's: how much of the plan's included usage is gone (the plan
1459
+ // is the one the sign-in read)
1460
+ async usage(getAuth, provider) {
1461
+ let tok
1462
+ try {
1463
+ tok = await tokenOf(await getAuth())
1464
+ } catch (e) {
1465
+ return { error: e.message, windows: [] }
1466
+ }
1467
+ return usage(tok, Object.keys(provider?.models ?? {}))
1468
+ },
1384
1469
  },
1385
1470
  async config(config) {
1386
1471
  config.provider ??= {}
@@ -1414,4 +1499,4 @@ export async function CursorAuthPlugin() {
1414
1499
  }
1415
1500
 
1416
1501
  // for tests
1417
- export const _internal = { splitID, families, byEffort, offered, modelID, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
1502
+ 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.1",
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",