@magpie-community/opencode-cursor-auth 0.2.5 → 0.2.6

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 (3) hide show
  1. package/README.md +7 -0
  2. package/index.mjs +71 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -131,6 +131,13 @@ Accounts with a Grok Bot allowance also show its usage and reset time;
131
131
  active trials are labelled separately. A spent Bot allowance doesn't stop
132
132
  Cursor requests.
133
133
 
134
+ An account that may spend on-demand once its included usage is gone (a
135
+ team or personal spend limit, as `cursor-agent`'s usage view shows it) also
136
+ shows On-demand: its spend, and its limit when it has one. Cursor keeps
137
+ serving such an account past the included usage, so magpie doesn't count
138
+ it used up when Cursor Models or Other Models reach 100%; it does when the
139
+ on-demand spend reaches its limit.
140
+
134
141
  Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
135
142
  subscription grants Bot access without a paid Cursor plan; see
136
143
  [Grok Bot plans and billing](https://cursor.com/help/grok-bot/plans).
package/index.mjs CHANGED
@@ -380,16 +380,67 @@ function firstParty(model) {
380
380
  return ["grok-4.5", "grok-4.6", "grok-4.7"].some((b) => model === b || model.startsWith(b + "-"))
381
381
  }
382
382
 
383
+ // onDemandOf is what Cursor still serves once the included usage is spent,
384
+ // as cursor-agent's own usage view tells it (its On-demand line): from the
385
+ // period's spendLimitUsage (cents) and, where that sets no limit of the
386
+ // user's, GetHardLimit's reply (dollars; undefined when it couldn't be
387
+ // read). kind is "fixed" (up to limitDollars), "unlimited", "disabled" or
388
+ // "unavailable" (not known).
389
+ function onDemandOf(data, hard) {
390
+ const s = data?.spendLimitUsage
391
+ const usedDollars = Number(s?.individualUsed ?? 0) / 100
392
+ const own =
393
+ s?.individualLimit === undefined
394
+ ? undefined
395
+ : Number(s.individualLimit) > 0
396
+ ? { kind: "fixed", usedDollars, limitDollars: Number(s.individualLimit) / 100 }
397
+ : { kind: "disabled", usedDollars }
398
+ if (s?.limitType === "team") {
399
+ if (own) return own
400
+ if (hard) return hard.noUsageBasedAllowed || !(Number(hard.hardLimit) > 0) ? { kind: "disabled", usedDollars } : { kind: "unlimited", usedDollars }
401
+ return Number(s.pooledLimit ?? 0) > 0 ? { kind: "unlimited", usedDollars } : { kind: "unavailable", usedDollars }
402
+ }
403
+ if (!hard) return own ?? { kind: "unavailable", usedDollars }
404
+ if (hard.noUsageBasedAllowed) return { kind: "disabled", usedDollars }
405
+ const limit = Number(hard.hardLimit ?? 0)
406
+ if (limit >= 2147483647) return { kind: "unlimited", usedDollars }
407
+ return limit > 0 ? { kind: "fixed", usedDollars, limitDollars: limit } : { kind: "disabled", usedDollars }
408
+ }
409
+
410
+ // hardLimit is GetHardLimit's reply, undefined when it couldn't be read.
411
+ async function hardLimit(tok) {
412
+ try {
413
+ const res = await fetch(API + "/aiserver.v1.DashboardService/GetHardLimit", {
414
+ method: "POST",
415
+ headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
416
+ body: "{}",
417
+ signal: AbortSignal.timeout(15_000),
418
+ })
419
+ return res.ok ? ((await res.json()) ?? undefined) : undefined
420
+ } catch {
421
+ return undefined
422
+ }
423
+ }
424
+
425
+ const dollars = (n) => "$" + n.toFixed(2)
426
+
383
427
  // usage is the account's windows this billing period; an enterprise plan
384
428
  // reports spend instead, and gets none. ids are the models it can be
385
- // asked for, each counted by the pool Cursor bills it to.
429
+ // asked for, each counted by the pool Cursor bills it to. An account that
430
+ // may spend on-demand once its included usage is gone (fottencity on
431
+ // Discord: a team that pays for more) is still served when the pools are
432
+ // spent, so they are shown aside, and its On-demand spend is what can run
433
+ // out: at its limit, or never when it has none.
386
434
  async function usage(tok, ids) {
387
- const res = await fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
388
- method: "POST",
389
- headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
390
- body: "{}",
391
- signal: AbortSignal.timeout(15_000),
392
- })
435
+ const [res, hard] = await Promise.all([
436
+ fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
437
+ method: "POST",
438
+ headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
439
+ body: "{}",
440
+ signal: AbortSignal.timeout(15_000),
441
+ }),
442
+ hardLimit(tok),
443
+ ])
393
444
  if (!res.ok) return { error: statusText(res.status), windows: [] }
394
445
  const data = await res.json()
395
446
  const u = data?.planUsage
@@ -412,11 +463,20 @@ async function usage(tok, ids) {
412
463
  const asked = ids.flatMap((id) => [id, id.replace(/@[^@]*$/, "")])
413
464
  const pool = [...new Set(["auto", ...asked, ...(data.autoBucketModels ?? [])])].filter(inPool)
414
465
  const num = (v) => (typeof v === "number" ? v : 0)
466
+ const od = onDemandOf(data, hard)
467
+ const pooled = od.kind === "fixed" || od.kind === "unlimited" ? { aside: true } : {}
468
+ const onDemand =
469
+ od.kind === "fixed"
470
+ ? [{ name: "On-demand", used: Math.max(0, Math.min(100, (100 * od.usedDollars) / od.limitDollars)), display: `${dollars(od.usedDollars)} / ${dollars(od.limitDollars)}`, ...resets }]
471
+ : od.kind === "unlimited"
472
+ ? [{ name: "On-demand", used: 0, display: dollars(od.usedDollars), ...resets, aside: true }]
473
+ : []
415
474
  // the two pools fit the line; the total goes in its tooltip
416
475
  return {
417
476
  windows: [
418
- { name: "Cursor Models", used: num(u.autoPercentUsed), ...resets, models: pool },
419
- { name: "Other Models", used: num(u.apiPercentUsed), ...resets, notModels: pool },
477
+ { name: "Cursor Models", used: num(u.autoPercentUsed), ...resets, models: pool, ...pooled },
478
+ { name: "Other Models", used: num(u.apiPercentUsed), ...resets, notModels: pool, ...pooled },
479
+ ...onDemand,
420
480
  { name: "Total", used: num(u.totalPercentUsed), ...resets, aside: true },
421
481
  ],
422
482
  }
@@ -2197,7 +2257,7 @@ export async function CursorAuthPlugin() {
2197
2257
  }
2198
2258
  const [u, plan, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok), botUsage(tok)])
2199
2259
  // Put Bot before Total for magpie's three menu-bar rings.
2200
- const windows = [...u.windows.filter((w) => !w.aside), ...bot, ...u.windows.filter((w) => w.aside)]
2260
+ const windows = [...u.windows.filter((w) => w.name !== "Total"), ...bot, ...u.windows.filter((w) => w.name === "Total")]
2201
2261
  return { ...u, windows, ...(plan ? { plan } : {}), signIn: "kept" }
2202
2262
  },
2203
2263
  },
@@ -2240,4 +2300,4 @@ export async function CursorAuthPlugin() {
2240
2300
  }
2241
2301
 
2242
2302
  // for tests
2243
- export const _internal = { resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, STEP_WAIT, stepUsage, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
2303
+ export const _internal = { onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, STEP_WAIT, stepUsage, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, 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.2.5",
3
+ "version": "0.2.6",
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",