@magpie-community/opencode-cursor-auth 0.2.30 → 0.2.32

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 +20 -0
  2. package/index.mjs +73 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -240,6 +240,11 @@ go without the list for 5 minutes instead of each waiting for it again.
240
240
  ## Usage
241
241
 
242
242
  magpie shows Cursor Models, Other Models and Total for the billing period.
243
+ A plan of Cursor's merged billing (`GetPlanInfo`'s `isMerged`, the newer
244
+ plans) has no Other Models pool: its included usage is one allowance every
245
+ model draws on, as the dashboard's "Weekly usage limit" shows it. magpie
246
+ shows that as one window, Weekly (Monthly on a monthly plan), for every
247
+ model.
243
248
  Accounts with a Grok Bot allowance also show its usage and reset time;
244
249
  active trials are labelled separately. A spent Bot allowance doesn't stop
245
250
  Cursor requests.
@@ -265,6 +270,21 @@ reaching its included allowance doesn't count the account as used up.
265
270
  On-demand also shows the team's spend when known, with its limit when it
266
271
  has one. A reached on-demand limit counts the account as used up.
267
272
 
273
+ ### Included usage only (optional)
274
+
275
+ In magpie, open the Cursor provider, turn on **Included usage only** under
276
+ **Plugin options**, and save (or set `"pluginOptions": {"cursor":
277
+ {"included_only": true}}` in magpie's settings.json). It is off by default,
278
+ applies to every Cursor account, and takes effect at each account's next
279
+ usage reading.
280
+
281
+ When on, Cursor Models and Other Models (a merged plan's Weekly, an
282
+ Enterprise account's allowance) are what keep an account in routing: once one is used up, magpie moves its
283
+ models to the next account until the period resets, even when on-demand
284
+ could still pay for them. On-demand and Total are still shown but never
285
+ keep an account in routing. Use it when on-demand is a shared or paid pool
286
+ you don't want spent. When off, on-demand works as described above.
287
+
268
288
  Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
269
289
  subscription grants Bot access without a paid Cursor plan; see
270
290
  [Grok Bot plans and billing](https://cursor.com/help/grok-bot/plans).
package/index.mjs CHANGED
@@ -316,18 +316,27 @@ async function whoIs(tok) {
316
316
 
317
317
  // plans are the plan each token's account is on, as `cursor-agent about`
318
318
  // names it (GetPlanInfo's planName, magpie's card title), asked again after
319
- // an hour.
319
+ // an hour. merged is GetPlanInfo's isMerged: a plan of Cursor's merged
320
+ // (xAI) billing, whose included usage is one allowance for every model,
321
+ // most of them a week long (includedUsagePeriod), and has no Other Models
322
+ // pool (majiang_ai on X). The dashboard's usage page shows such a plan one
323
+ // meter, the period's totalPercentUsed. period is "weekly", "monthly" or
324
+ // "" (not said). A reply that couldn't be read keeps the last one, else
325
+ // says nothing: undefined merged, which the pools are kept for.
320
326
  const plans = new Map()
321
327
  const PLAN_TTL = 3600_000
328
+ const PERIODS = { INCLUDED_USAGE_PERIOD_WEEKLY: "weekly", INCLUDED_USAGE_PERIOD_MONTHLY: "monthly", 1: "monthly", 2: "weekly" }
322
329
  async function planOf(tok) {
323
330
  const had = plans.get(tok)
324
- if (had && Date.now() - had.at < PLAN_TTL) return had.plan
331
+ if (had && Date.now() - had.at < PLAN_TTL) return had
325
332
  try {
326
- const plan = String((await unary(API, "aiserver.v1.DashboardService/GetPlanInfo", tok))?.planInfo?.planName ?? "").trim()
327
- if (plan) plans.set(tok, { plan, at: Date.now() })
328
- return plan || had?.plan || ""
333
+ const info = await unary(API, "aiserver.v1.DashboardService/GetPlanInfo", tok)
334
+ const plan = String(info?.planInfo?.planName ?? "").trim()
335
+ const got = { plan: plan || had?.plan || "", merged: info?.isMerged === true, period: PERIODS[info?.planInfo?.includedUsagePeriod] ?? "", at: Date.now() }
336
+ if (plan || got.merged) plans.set(tok, got)
337
+ return got
329
338
  } catch {
330
- return had?.plan ?? ""
339
+ return had ?? { plan: "" }
331
340
  }
332
341
  }
333
342
 
@@ -532,13 +541,21 @@ async function hardLimit(tok) {
532
541
  // dollars, thousands grouped: a team's pool runs to six figures ($451,200.00)
533
542
  const dollars = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
534
543
 
544
+ // INCLUDED_ONLY is the switch (magpie's plugin options, off by default)
545
+ // that keeps an account in routing only while its included usage lasts
546
+ // (magpie-community/plugins#485): on, the included pools (Cursor Models and
547
+ // Other Models; an Enterprise account's allowance) are what stops it, and
548
+ // On-demand and Total are shown only, a budget with room or none at all
549
+ // keeping nothing in routing. Off, on-demand that can still pay keeps it.
550
+ const INCLUDED_ONLY = "included_only"
551
+
535
552
  // enterpriseUsage is the allowance GetCurrentPeriodUsage leaves out on an
536
553
  // Enterprise account. That call answers with only the billing cycle there;
537
554
  // the website's usage-summary has individualUsage.overall, in cents, and
538
555
  // the account's membership. The CLI JWT is also the website's dashboard
539
556
  // session, as botUsage builds it. undefined when the summary says nothing
540
557
  // this can show, so a failed read stays an empty card, as before.
541
- async function enterpriseUsage(tok) {
558
+ async function enterpriseUsage(tok, includedOnly = false) {
542
559
  try {
543
560
  const subject = JSON.parse(Buffer.from(tok.split(".")[1], "base64url").toString()).sub
544
561
  if (typeof subject !== "string") return undefined
@@ -569,8 +586,9 @@ async function enterpriseUsage(tok) {
569
586
  const unlimited = od?.enabled === true && od.limit === null
570
587
  const fixed = od?.enabled === true && typeof od.limit === "number" && Number.isFinite(od.limit) && od.limit > 0 && spent
571
588
  // Only an explicitly unlimited budget or a measured budget with room
572
- // lets a spent included allowance stay available to magpie's routing.
573
- const aside = unlimited || (fixed && od.used < od.limit)
589
+ // lets a spent included allowance stay available to magpie's routing,
590
+ // and neither does with INCLUDED_ONLY on.
591
+ const aside = !includedOnly && (unlimited || (fixed && od.used < od.limit))
574
592
  const windows = [{
575
593
  name: "Enterprise",
576
594
  used: Math.max(0, Math.min(100, (100 * used) / limit)),
@@ -589,6 +607,7 @@ async function enterpriseUsage(tok) {
589
607
  limit: od.limit / 100,
590
608
  unit: "USD",
591
609
  ...resets,
610
+ ...(includedOnly ? { aside: true } : {}),
592
611
  })
593
612
  else if (unlimited && spent) windows.push({ name: "On-demand", used: 0, display: dollars(od.used / 100), ...resets, aside: true })
594
613
  return windows
@@ -603,9 +622,13 @@ async function enterpriseUsage(tok) {
603
622
  // Discord: a team that pays for more) is still served when the pools are
604
623
  // spent, so they are shown aside, and its On-demand spend is what can run
605
624
  // out: at its limit (a team member with none of their own: the team's
606
- // pool), or never when it has none. An Enterprise account's period has no
607
- // planUsage; its allowance comes from usage-summary instead.
608
- async function usage(tok, ids) {
625
+ // pool), or never when it has none. With includedOnly (INCLUDED_ONLY on)
626
+ // the pools are the allowance whatever on-demand allows, and On-demand is
627
+ // shown only. An Enterprise account's period has no planUsage; its
628
+ // allowance comes from usage-summary instead.
629
+ // plan is planOf's answer, asked alongside (a promise): a merged plan's
630
+ // included usage is one window, Weekly on a weekly plan, for every model.
631
+ async function usage(tok, ids, includedOnly = false, plan = undefined) {
609
632
  const [res, hard] = await Promise.all([
610
633
  fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
611
634
  method: "POST",
@@ -620,7 +643,7 @@ async function usage(tok, ids) {
620
643
  const u = data?.planUsage
621
644
  if (!u) {
622
645
  // Prefer measured Enterprise usage before any reset-date-only fallback.
623
- const windows = await enterpriseUsage(tok)
646
+ const windows = await enterpriseUsage(tok, includedOnly)
624
647
  return windows ? { windows } : { windows: [] }
625
648
  }
626
649
  const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
@@ -642,13 +665,26 @@ async function usage(tok, ids) {
642
665
  const pool = [...new Set(["auto", ...asked, ...(data.autoBucketModels ?? [])])].filter(inPool)
643
666
  const num = (v) => (typeof v === "number" ? v : 0)
644
667
  const od = onDemandOf(data, hard)
645
- const pooled = od.kind === "fixed" || od.kind === "unlimited" ? { aside: true } : {}
668
+ const pooled = !includedOnly && (od.kind === "fixed" || od.kind === "unlimited") ? { aside: true } : {}
646
669
  const onDemand =
647
670
  od.kind === "fixed"
648
- ? [{ name: "On-demand", used: Math.max(0, Math.min(100, (100 * od.usedDollars) / od.limitDollars)), display: `${dollars(od.usedDollars)} / ${dollars(od.limitDollars)}` + (od.mineDollars === undefined ? "" : ` (you: ${dollars(od.mineDollars)})`), ...resets }]
671
+ ? [{ name: "On-demand", used: Math.max(0, Math.min(100, (100 * od.usedDollars) / od.limitDollars)), display: `${dollars(od.usedDollars)} / ${dollars(od.limitDollars)}` + (od.mineDollars === undefined ? "" : ` (you: ${dollars(od.mineDollars)})`), ...resets, ...(includedOnly ? { aside: true } : {}) }]
649
672
  : od.kind === "unlimited"
650
673
  ? [{ name: "On-demand", used: 0, display: dollars(od.usedDollars), ...resets, aside: true }]
651
674
  : []
675
+ // A merged plan has no pools: its included usage is one allowance every
676
+ // model draws on, which the dashboard shows as the period's
677
+ // totalPercentUsed, autoPercentUsed the same and apiPercentUsed 0 always.
678
+ const info = await plan
679
+ if (info?.merged === true) {
680
+ const weekly = info.period === "weekly"
681
+ return {
682
+ windows: [
683
+ { name: weekly ? "Weekly" : info.period === "monthly" ? "Monthly" : "Included", used: num(u.totalPercentUsed), ...resets, ...(weekly ? { span: 604800 } : {}), ...pooled },
684
+ ...onDemand,
685
+ ],
686
+ }
687
+ }
652
688
  // the two pools fit the line; the total goes in its tooltip
653
689
  return {
654
690
  windows: [
@@ -3172,7 +3208,16 @@ async function answerOf(auth, chat, signal, session = "") {
3172
3208
 
3173
3209
  // ---- the plugin ---------------------------------------------------------------------------
3174
3210
 
3175
- export async function CursorAuthPlugin() {
3211
+ export async function CursorAuthPlugin(input) {
3212
+ // includedOnly is the user's INCLUDED_ONLY as magpie has it now, read at
3213
+ // each usage reading so a change needs no restart; false outside magpie
3214
+ const includedOnly = () => {
3215
+ try {
3216
+ return input?.magpie?.option?.(ID, INCLUDED_ONLY) === true
3217
+ } catch {
3218
+ return false
3219
+ }
3220
+ }
3176
3221
  return {
3177
3222
  auth: {
3178
3223
  provider: ID,
@@ -3205,6 +3250,16 @@ export async function CursorAuthPlugin() {
3205
3250
  { type: "oauth", label: "cursor-agent's sign-in", authorize: cliSignIn },
3206
3251
  { type: "api", label: "Cursor API key (cursor.com/dashboard → Integrations)" },
3207
3252
  ],
3253
+ // magpie's own switches (input.magpie.option), read at each usage
3254
+ // reading
3255
+ options: [
3256
+ {
3257
+ key: INCLUDED_ONLY,
3258
+ label: "Included usage only",
3259
+ description:
3260
+ "Keeps an account in routing only while its included usage lasts: once an included pool (Cursor Models, Other Models, a newer plan's Weekly) is used up, magpie moves its models to the next account until it resets, even if on-demand could pay. On-demand and Total never keep an account in routing.",
3261
+ },
3262
+ ],
3208
3263
  // magpie's: how much of the plan's included usage is gone (the plan
3209
3264
  // is the one the sign-in read). The built-in's read, clean or not,
3210
3265
  // neither marked the account nor cleared it.
@@ -3215,7 +3270,8 @@ export async function CursorAuthPlugin() {
3215
3270
  } catch (e) {
3216
3271
  return { error: e.message, windows: [], signIn: "kept" }
3217
3272
  }
3218
- const [u, plan, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok), botUsage(tok)])
3273
+ const info = planOf(tok)
3274
+ const [u, { plan }, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {}), includedOnly(), info), info, botUsage(tok)])
3219
3275
  // Bot goes among magpie's three menu-bar rings: before Total, and
3220
3276
  // before an On-demand with no limit, which is only a dollar figure on
3221
3277
  // a ring that never fills (magpie #2213, Morganwan90). An On-demand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
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",