@magpie-community/opencode-cursor-auth 0.2.29 → 0.2.31

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 +26 -0
  2. package/index.mjs +111 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -254,6 +254,32 @@ own spends the team's pool: On-demand shows the pool's spend and limit,
254
254
  with the member's own spend beside it, and the account is used up when
255
255
  the pool is.
256
256
 
257
+ An Enterprise account's period has no Cursor Models, Other Models or Total
258
+ in `GetCurrentPeriodUsage`: Cursor reports its overall allowance through
259
+ the website's `usage-summary` instead. Magpie shows that as Enterprise,
260
+ for example `49.45 / 800 USD · 6.2% used`, or
261
+ `750.55 / 800 USD · 93.8% left` with Magpie's Used / Left choice, reset at
262
+ the billing cycle's end. When the summary says team on-demand is enabled
263
+ and unlimited or still has room, the Enterprise window is informational:
264
+ reaching its included allowance doesn't count the account as used up.
265
+ On-demand also shows the team's spend when known, with its limit when it
266
+ has one. A reached on-demand limit counts the account as used up.
267
+
268
+ ### Included usage only (optional)
269
+
270
+ In magpie, open the Cursor provider, turn on **Included usage only** under
271
+ **Plugin options**, and save (or set `"pluginOptions": {"cursor":
272
+ {"included_only": true}}` in magpie's settings.json). It is off by default,
273
+ applies to every Cursor account, and takes effect at each account's next
274
+ usage reading.
275
+
276
+ When on, Cursor Models and Other Models (an Enterprise account's allowance)
277
+ are what keep an account in routing: once one is used up, magpie moves its
278
+ models to the next account until the period resets, even when on-demand
279
+ could still pay for them. On-demand and Total are still shown but never
280
+ keep an account in routing. Use it when on-demand is a shared or paid pool
281
+ you don't want spent. When off, on-demand works as described above.
282
+
257
283
  Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
258
284
  subscription grants Bot access without a paid Cursor plan; see
259
285
  [Grok Bot plans and billing](https://cursor.com/help/grok-bot/plans).
package/index.mjs CHANGED
@@ -532,15 +532,92 @@ async function hardLimit(tok) {
532
532
  // dollars, thousands grouped: a team's pool runs to six figures ($451,200.00)
533
533
  const dollars = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
534
534
 
535
- // usage is the account's windows this billing period; an enterprise plan
536
- // reports spend instead, and gets none. ids are the models it can be
537
- // asked for, each counted by the pool Cursor bills it to. An account that
538
- // may spend on-demand once its included usage is gone (fottencity on
535
+ // INCLUDED_ONLY is the switch (magpie's plugin options, off by default)
536
+ // that keeps an account in routing only while its included usage lasts
537
+ // (magpie-community/plugins#485): on, the included pools (Cursor Models and
538
+ // Other Models; an Enterprise account's allowance) are what stops it, and
539
+ // On-demand and Total are shown only, a budget with room or none at all
540
+ // keeping nothing in routing. Off, on-demand that can still pay keeps it.
541
+ const INCLUDED_ONLY = "included_only"
542
+
543
+ // enterpriseUsage is the allowance GetCurrentPeriodUsage leaves out on an
544
+ // Enterprise account. That call answers with only the billing cycle there;
545
+ // the website's usage-summary has individualUsage.overall, in cents, and
546
+ // the account's membership. The CLI JWT is also the website's dashboard
547
+ // session, as botUsage builds it. undefined when the summary says nothing
548
+ // this can show, so a failed read stays an empty card, as before.
549
+ async function enterpriseUsage(tok, includedOnly = false) {
550
+ try {
551
+ const subject = JSON.parse(Buffer.from(tok.split(".")[1], "base64url").toString()).sub
552
+ if (typeof subject !== "string") return undefined
553
+ const user = subject.split("|").filter(Boolean).at(-1)
554
+ if (!user || !/^[a-zA-Z0-9._-]+$/.test(user)) return undefined
555
+ const res = await fetch(WEBSITE + "/api/usage-summary", {
556
+ method: "GET",
557
+ headers: {
558
+ Accept: "application/json",
559
+ Origin: WEBSITE,
560
+ Cookie: "WorkosCursorSessionToken=" + encodeURIComponent(user + "::" + tok),
561
+ },
562
+ redirect: "error",
563
+ signal: AbortSignal.timeout(15_000),
564
+ })
565
+ if (!res.ok) return undefined
566
+ const data = await res.json()
567
+ if (String(data?.membershipType).toLowerCase() !== "enterprise") return undefined
568
+ const overall = data?.individualUsage?.overall
569
+ if (overall?.enabled !== true) return undefined
570
+ const { used, limit } = overall
571
+ if (typeof used !== "number" || !Number.isFinite(used) || used < 0) return undefined
572
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) return undefined
573
+ const end = Date.parse(data.billingCycleEnd ?? "")
574
+ const resets = Number.isFinite(end) ? { resetsAt: new Date(end).toISOString() } : {}
575
+ const od = data.teamUsage?.onDemand
576
+ const spent = typeof od?.used === "number" && Number.isFinite(od.used) && od.used >= 0
577
+ const unlimited = od?.enabled === true && od.limit === null
578
+ const fixed = od?.enabled === true && typeof od.limit === "number" && Number.isFinite(od.limit) && od.limit > 0 && spent
579
+ // Only an explicitly unlimited budget or a measured budget with room
580
+ // lets a spent included allowance stay available to magpie's routing,
581
+ // and neither does with INCLUDED_ONLY on.
582
+ const aside = !includedOnly && (unlimited || (fixed && od.used < od.limit))
583
+ const windows = [{
584
+ name: "Enterprise",
585
+ used: Math.max(0, Math.min(100, (100 * used) / limit)),
586
+ display: `${dollars(used / 100)} / ${dollars(limit / 100)}`,
587
+ amount: used / 100,
588
+ limit: limit / 100,
589
+ unit: "USD",
590
+ ...resets,
591
+ ...(aside ? { aside: true } : {}),
592
+ }]
593
+ if (fixed) windows.push({
594
+ name: "On-demand",
595
+ used: Math.min(100, (100 * od.used) / od.limit),
596
+ display: `${dollars(od.used / 100)} / ${dollars(od.limit / 100)}`,
597
+ amount: od.used / 100,
598
+ limit: od.limit / 100,
599
+ unit: "USD",
600
+ ...resets,
601
+ ...(includedOnly ? { aside: true } : {}),
602
+ })
603
+ else if (unlimited && spent) windows.push({ name: "On-demand", used: 0, display: dollars(od.used / 100), ...resets, aside: true })
604
+ return windows
605
+ } catch {
606
+ return undefined
607
+ }
608
+ }
609
+
610
+ // usage is the account's windows this billing period. ids are the models it
611
+ // can be asked for, each counted by the pool Cursor bills it to. An account
612
+ // that may spend on-demand once its included usage is gone (fottencity on
539
613
  // Discord: a team that pays for more) is still served when the pools are
540
614
  // spent, so they are shown aside, and its On-demand spend is what can run
541
615
  // out: at its limit (a team member with none of their own: the team's
542
- // pool), or never when it has none.
543
- async function usage(tok, ids) {
616
+ // pool), or never when it has none. With includedOnly (INCLUDED_ONLY on)
617
+ // the pools are the allowance whatever on-demand allows, and On-demand is
618
+ // shown only. An Enterprise account's period has no planUsage; its
619
+ // allowance comes from usage-summary instead.
620
+ async function usage(tok, ids, includedOnly = false) {
544
621
  const [res, hard] = await Promise.all([
545
622
  fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
546
623
  method: "POST",
@@ -553,7 +630,11 @@ async function usage(tok, ids) {
553
630
  if (!res.ok) return { error: statusText(res.status), windows: [] }
554
631
  const data = await res.json()
555
632
  const u = data?.planUsage
556
- if (!u) return { windows: [] }
633
+ if (!u) {
634
+ // Prefer measured Enterprise usage before any reset-date-only fallback.
635
+ const windows = await enterpriseUsage(tok, includedOnly)
636
+ return windows ? { windows } : { windows: [] }
637
+ }
557
638
  const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
558
639
  const resets = ms > 0 ? { resetsAt: new Date(ms).toISOString() } : {}
559
640
  const bucket = new Set((data.autoBucketModels ?? []).map(poolBase))
@@ -573,10 +654,10 @@ async function usage(tok, ids) {
573
654
  const pool = [...new Set(["auto", ...asked, ...(data.autoBucketModels ?? [])])].filter(inPool)
574
655
  const num = (v) => (typeof v === "number" ? v : 0)
575
656
  const od = onDemandOf(data, hard)
576
- const pooled = od.kind === "fixed" || od.kind === "unlimited" ? { aside: true } : {}
657
+ const pooled = !includedOnly && (od.kind === "fixed" || od.kind === "unlimited") ? { aside: true } : {}
577
658
  const onDemand =
578
659
  od.kind === "fixed"
579
- ? [{ 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 }]
660
+ ? [{ 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 } : {}) }]
580
661
  : od.kind === "unlimited"
581
662
  ? [{ name: "On-demand", used: 0, display: dollars(od.usedDollars), ...resets, aside: true }]
582
663
  : []
@@ -3103,7 +3184,16 @@ async function answerOf(auth, chat, signal, session = "") {
3103
3184
 
3104
3185
  // ---- the plugin ---------------------------------------------------------------------------
3105
3186
 
3106
- export async function CursorAuthPlugin() {
3187
+ export async function CursorAuthPlugin(input) {
3188
+ // includedOnly is the user's INCLUDED_ONLY as magpie has it now, read at
3189
+ // each usage reading so a change needs no restart; false outside magpie
3190
+ const includedOnly = () => {
3191
+ try {
3192
+ return input?.magpie?.option?.(ID, INCLUDED_ONLY) === true
3193
+ } catch {
3194
+ return false
3195
+ }
3196
+ }
3107
3197
  return {
3108
3198
  auth: {
3109
3199
  provider: ID,
@@ -3136,6 +3226,16 @@ export async function CursorAuthPlugin() {
3136
3226
  { type: "oauth", label: "cursor-agent's sign-in", authorize: cliSignIn },
3137
3227
  { type: "api", label: "Cursor API key (cursor.com/dashboard → Integrations)" },
3138
3228
  ],
3229
+ // magpie's own switches (input.magpie.option), read at each usage
3230
+ // reading
3231
+ options: [
3232
+ {
3233
+ key: INCLUDED_ONLY,
3234
+ label: "Included usage only",
3235
+ description:
3236
+ "Keeps an account in routing only while its included usage lasts: once Cursor Models or Other Models is used up, magpie moves those models to the next account until the period resets, even if on-demand could pay. On-demand and Total are still shown but never keep an account in routing.",
3237
+ },
3238
+ ],
3139
3239
  // magpie's: how much of the plan's included usage is gone (the plan
3140
3240
  // is the one the sign-in read). The built-in's read, clean or not,
3141
3241
  // neither marked the account nor cleared it.
@@ -3146,7 +3246,7 @@ export async function CursorAuthPlugin() {
3146
3246
  } catch (e) {
3147
3247
  return { error: e.message, windows: [], signIn: "kept" }
3148
3248
  }
3149
- const [u, plan, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok), botUsage(tok)])
3249
+ const [u, plan, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {}), includedOnly()), planOf(tok), botUsage(tok)])
3150
3250
  // Bot goes among magpie's three menu-bar rings: before Total, and
3151
3251
  // before an On-demand with no limit, which is only a dollar figure on
3152
3252
  // 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.29",
3
+ "version": "0.2.31",
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",