@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.
- package/README.md +26 -0
- package/index.mjs +111 -11
- 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
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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",
|