@magpie-community/opencode-cursor-auth 0.2.31 → 0.2.33
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 +20 -2
- package/index.mjs +155 -18
- 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.
|
|
@@ -254,6 +259,19 @@ own spends the team's pool: On-demand shows the pool's spend and limit,
|
|
|
254
259
|
with the member's own spend beside it, and the account is used up when
|
|
255
260
|
the pool is.
|
|
256
261
|
|
|
262
|
+
Free credit shows too, as Cursor's own IDE reads it. The plan's bonus
|
|
263
|
+
("+$X free usage on us", `GetCurrentPeriodUsage`'s `bonusSpend` and
|
|
264
|
+
`remainingBonus`) is Free usage: what was spent from it this period, and
|
|
265
|
+
whether more is left; Cursor gives no amount for it. Each credit grant
|
|
266
|
+
(`GetUsageLimitStatusAndActiveGrants`' `activeGrants`, the IDE's "Use your
|
|
267
|
+
$X free credits") is a window of its own, named by its campaign: spent of
|
|
268
|
+
total, when it expires, and its limits. A grant limited to some models
|
|
269
|
+
lists them; one only Cursor's Cloud Agents (or another client) may spend
|
|
270
|
+
says so in its name, since magpie talks as `cursor-agent`. While a bonus
|
|
271
|
+
or a grant magpie can spend has some left, a spent included pool doesn't
|
|
272
|
+
count the account used up for the models it pays for. With "Included
|
|
273
|
+
usage only" on, free credit is shown only.
|
|
274
|
+
|
|
257
275
|
An Enterprise account's period has no Cursor Models, Other Models or Total
|
|
258
276
|
in `GetCurrentPeriodUsage`: Cursor reports its overall allowance through
|
|
259
277
|
the website's `usage-summary` instead. Magpie shows that as Enterprise,
|
|
@@ -273,8 +291,8 @@ In magpie, open the Cursor provider, turn on **Included usage only** under
|
|
|
273
291
|
applies to every Cursor account, and takes effect at each account's next
|
|
274
292
|
usage reading.
|
|
275
293
|
|
|
276
|
-
When on, Cursor Models and Other Models (
|
|
277
|
-
are what keep an account in routing: once one is used up, magpie moves its
|
|
294
|
+
When on, Cursor Models and Other Models (a merged plan's Weekly, an
|
|
295
|
+
Enterprise account's allowance) are what keep an account in routing: once one is used up, magpie moves its
|
|
278
296
|
models to the next account until the period resets, even when on-demand
|
|
279
297
|
could still pay for them. On-demand and Total are still shown but never
|
|
280
298
|
keep an account in routing. Use it when on-demand is a shared or paid pool
|
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
|
|
331
|
+
if (had && Date.now() - had.at < PLAN_TTL) return had
|
|
325
332
|
try {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
339
|
+
return had ?? { plan: "" }
|
|
331
340
|
}
|
|
332
341
|
}
|
|
333
342
|
|
|
@@ -532,6 +541,99 @@ 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
|
+
// ---- free credit ------------------------------------------------------------------
|
|
545
|
+
//
|
|
546
|
+
// Cursor gives free usage two ways (CryptoSarom on X, "赠金"), as Cursor
|
|
547
|
+
// 3.x's own client reads them:
|
|
548
|
+
//
|
|
549
|
+
// - the plan's bonus, "+$X free usage on us" under the included usage in
|
|
550
|
+
// the IDE's usage tooltip: GetCurrentPeriodUsage's planUsage.bonusSpend
|
|
551
|
+
// (cents spent from it this period), remainingBonus (whether any is
|
|
552
|
+
// left; no amount is given) and bonusTooltip ("We work with model
|
|
553
|
+
// providers to give you free usage beyond what you've purchased. Amounts
|
|
554
|
+
// may vary."). It isn't tied to a model.
|
|
555
|
+
// - credit grants, the IDE's "Use your $X free … credits" banner:
|
|
556
|
+
// DashboardService/GetUsageLimitStatusAndActiveGrants' activeGrants, each
|
|
557
|
+
// with totalCents and remainingCents (int64 strings), expiresAtMs, and
|
|
558
|
+
// its limits: allowedModelIds, allowedModelTags and allowedModelSelections
|
|
559
|
+
// (the models it may be spent on; none listed is every model) and
|
|
560
|
+
// allowedClientTypes (the clients that may spend it; none listed is any;
|
|
561
|
+
// "glass" without "ide" is the IDE's Cloud Agent credits). magpie talks
|
|
562
|
+
// as cursor-agent, clientType "cli".
|
|
563
|
+
//
|
|
564
|
+
// creditGrants is that reply's activeGrants, [] when it couldn't be read.
|
|
565
|
+
async function creditGrants(tok) {
|
|
566
|
+
try {
|
|
567
|
+
const res = await fetch(API + "/aiserver.v1.DashboardService/GetUsageLimitStatusAndActiveGrants", {
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1", ...await teamHeader(tok) },
|
|
570
|
+
body: "{}",
|
|
571
|
+
signal: AbortSignal.timeout(15_000),
|
|
572
|
+
})
|
|
573
|
+
if (!res.ok) return []
|
|
574
|
+
const g = (await res.json())?.activeGrants
|
|
575
|
+
return Array.isArray(g) ? g : []
|
|
576
|
+
} catch {
|
|
577
|
+
return []
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// tagOf is a model name as the IDE matches a grant's allowedModelTags to
|
|
582
|
+
// it: lower case, "-" read as "."; a tag matches a name it is part of.
|
|
583
|
+
const tagOf = (s) => String(s).toLowerCase().replace(/-/g, ".")
|
|
584
|
+
|
|
585
|
+
// credits are the windows of the account's free credit, and what it lets
|
|
586
|
+
// Cursor go on serving once the included usage is spent: all (a bonus or
|
|
587
|
+
// grant magpie can spend on every model still has some left) and covers
|
|
588
|
+
// (the account's model ids a grant limited to some models still pays
|
|
589
|
+
// for). Every credit window is shown only: what stops an account is the
|
|
590
|
+
// included usage it outlasts, which usage() sets aside. plan is
|
|
591
|
+
// planUsage, resets its period's end, ids the account's model ids.
|
|
592
|
+
function credits(plan, grants, ids, resets, now = Date.now()) {
|
|
593
|
+
const windows = []
|
|
594
|
+
const covers = new Set()
|
|
595
|
+
let all = false
|
|
596
|
+
const bonus = Number(plan?.bonusSpend ?? 0) / 100
|
|
597
|
+
if (bonus > 0 || plan?.remainingBonus === true) {
|
|
598
|
+
windows.push({ name: "Free usage", used: 0, display: `+${dollars(bonus)} on us` + (plan?.remainingBonus === true ? ", more left" : ""), ...resets, aside: true })
|
|
599
|
+
if (plan?.remainingBonus === true) all = true
|
|
600
|
+
}
|
|
601
|
+
for (const g of grants) {
|
|
602
|
+
const total = Number(g?.totalCents ?? 0) / 100
|
|
603
|
+
const left = Math.max(0, Number(g?.remainingCents ?? 0) / 100)
|
|
604
|
+
if (!(total > 0) || !Number.isFinite(total) || !Number.isFinite(left)) continue
|
|
605
|
+
const exp = Number(g.expiresAtMs ?? 0)
|
|
606
|
+
if (exp > 0 && exp <= now) continue
|
|
607
|
+
const clients = (g.allowedClientTypes ?? []).map(String)
|
|
608
|
+
const mine = clients.length === 0 || clients.includes("cli")
|
|
609
|
+
const named = [...(g.allowedModelIds ?? []), ...(g.allowedModelSelections ?? []).map((m) => m?.modelId).filter(Boolean)].map(String)
|
|
610
|
+
const tags = (g.allowedModelTags ?? []).map(String)
|
|
611
|
+
const limited = named.length > 0 || tags.length > 0
|
|
612
|
+
const bases = new Set(named.map((m) => (poolBase(m) === "auto" ? "default" : poolBase(m))))
|
|
613
|
+
const fits = (id) => {
|
|
614
|
+
const b = poolBase(id) === "auto" ? "default" : poolBase(id)
|
|
615
|
+
return bases.has(b) || tags.some((t) => tagOf(id).includes(tagOf(t)))
|
|
616
|
+
}
|
|
617
|
+
const matched = limited ? ids.filter(fits) : []
|
|
618
|
+
const name = (String(g.campaignName ?? "").trim() || "Free credit") + (mine ? "" : clients.includes("glass") && !clients.includes("ide") ? " (Cloud Agents only)" : ` (${clients.join(", ")} only)`)
|
|
619
|
+
windows.push({
|
|
620
|
+
name,
|
|
621
|
+
used: Math.max(0, Math.min(100, (100 * (total - left)) / total)),
|
|
622
|
+
display: `${dollars(total - left)} / ${dollars(total)}`,
|
|
623
|
+
amount: total - left,
|
|
624
|
+
limit: total,
|
|
625
|
+
unit: "USD",
|
|
626
|
+
...(exp > 0 ? { runsOutAt: new Date(exp).toISOString(), runsOut: left } : {}),
|
|
627
|
+
...(limited ? { models: matched.length ? matched : [...named, ...tags] } : {}),
|
|
628
|
+
aside: true,
|
|
629
|
+
})
|
|
630
|
+
if (!mine || !(left > 0)) continue
|
|
631
|
+
if (!limited) all = true
|
|
632
|
+
else for (const id of matched) covers.add(id)
|
|
633
|
+
}
|
|
634
|
+
return { windows, all, covers }
|
|
635
|
+
}
|
|
636
|
+
|
|
535
637
|
// INCLUDED_ONLY is the switch (magpie's plugin options, off by default)
|
|
536
638
|
// that keeps an account in routing only while its included usage lasts
|
|
537
639
|
// (magpie-community/plugins#485): on, the included pools (Cursor Models and
|
|
@@ -617,7 +719,9 @@ async function enterpriseUsage(tok, includedOnly = false) {
|
|
|
617
719
|
// the pools are the allowance whatever on-demand allows, and On-demand is
|
|
618
720
|
// shown only. An Enterprise account's period has no planUsage; its
|
|
619
721
|
// allowance comes from usage-summary instead.
|
|
620
|
-
|
|
722
|
+
// plan is planOf's answer, asked alongside (a promise): a merged plan's
|
|
723
|
+
// included usage is one window, Weekly on a weekly plan, for every model.
|
|
724
|
+
async function usage(tok, ids, includedOnly = false, plan = undefined) {
|
|
621
725
|
const [res, hard] = await Promise.all([
|
|
622
726
|
fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
|
|
623
727
|
method: "POST",
|
|
@@ -627,13 +731,14 @@ async function usage(tok, ids, includedOnly = false) {
|
|
|
627
731
|
}),
|
|
628
732
|
hardLimit(tok),
|
|
629
733
|
])
|
|
734
|
+
const grants = creditGrants(tok)
|
|
630
735
|
if (!res.ok) return { error: statusText(res.status), windows: [] }
|
|
631
736
|
const data = await res.json()
|
|
632
737
|
const u = data?.planUsage
|
|
633
738
|
if (!u) {
|
|
634
739
|
// Prefer measured Enterprise usage before any reset-date-only fallback.
|
|
635
740
|
const windows = await enterpriseUsage(tok, includedOnly)
|
|
636
|
-
return
|
|
741
|
+
return { windows: windows ?? [], credits: credits(undefined, await grants, ids, {}).windows }
|
|
637
742
|
}
|
|
638
743
|
const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
|
|
639
744
|
const resets = ms > 0 ? { resetsAt: new Date(ms).toISOString() } : {}
|
|
@@ -654,21 +759,50 @@ async function usage(tok, ids, includedOnly = false) {
|
|
|
654
759
|
const pool = [...new Set(["auto", ...asked, ...(data.autoBucketModels ?? [])])].filter(inPool)
|
|
655
760
|
const num = (v) => (typeof v === "number" ? v : 0)
|
|
656
761
|
const od = onDemandOf(data, hard)
|
|
657
|
-
|
|
762
|
+
// free credit that magpie can spend on every model keeps the account
|
|
763
|
+
// served past its included usage, as on-demand that can pay does; one
|
|
764
|
+
// limited to some models keeps those models served past a spent pool.
|
|
765
|
+
// Neither does with includedOnly.
|
|
766
|
+
const free = credits(u, await grants, [...new Set(asked)], resets)
|
|
767
|
+
const pooled = !includedOnly && (od.kind === "fixed" || od.kind === "unlimited" || free.all) ? { aside: true } : {}
|
|
768
|
+
const covers = includedOnly ? new Set() : free.covers
|
|
658
769
|
const onDemand =
|
|
659
770
|
od.kind === "fixed"
|
|
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 } : {}) }]
|
|
771
|
+
? [{ 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 || free.all ? { aside: true } : {}) }]
|
|
661
772
|
: od.kind === "unlimited"
|
|
662
773
|
? [{ name: "On-demand", used: 0, display: dollars(od.usedDollars), ...resets, aside: true }]
|
|
663
774
|
: []
|
|
664
|
-
//
|
|
775
|
+
// A merged plan has no pools: its included usage is one allowance every
|
|
776
|
+
// model draws on, which the dashboard shows as the period's
|
|
777
|
+
// totalPercentUsed, autoPercentUsed the same and apiPercentUsed 0 always.
|
|
778
|
+
const info = await plan
|
|
779
|
+
// spent is whether a pool no longer pays: then the models a credit still
|
|
780
|
+
// pays for are left out of it
|
|
781
|
+
const spent = (used) => covers.size > 0 && !pooled.aside && used >= 100
|
|
782
|
+
if (info?.merged === true) {
|
|
783
|
+
const weekly = info.period === "weekly"
|
|
784
|
+
const used = num(u.totalPercentUsed)
|
|
785
|
+
return {
|
|
786
|
+
windows: [
|
|
787
|
+
{ name: weekly ? "Weekly" : info.period === "monthly" ? "Monthly" : "Included", used, ...resets, ...(weekly ? { span: 604800 } : {}), ...(spent(used) ? { notModels: [...covers] } : {}), ...pooled },
|
|
788
|
+
...onDemand,
|
|
789
|
+
],
|
|
790
|
+
credits: free.windows,
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// the two pools fit the line; the total goes in its tooltip. A spent
|
|
794
|
+
// Cursor Models pool every model of which a credit pays for is aside:
|
|
795
|
+
// an empty models list would count every model.
|
|
796
|
+
const auto = num(u.autoPercentUsed), api = num(u.apiPercentUsed)
|
|
797
|
+
const cursorModels = spent(auto) ? pool.filter((m) => !covers.has(m)) : pool
|
|
665
798
|
return {
|
|
666
799
|
windows: [
|
|
667
|
-
{ name: "Cursor Models", used:
|
|
668
|
-
{ name: "Other Models", used:
|
|
800
|
+
{ name: "Cursor Models", used: auto, ...resets, models: cursorModels.length ? cursorModels : pool, ...(cursorModels.length ? pooled : { aside: true }) },
|
|
801
|
+
{ name: "Other Models", used: api, ...resets, notModels: spent(api) ? [...new Set([...pool, ...covers])] : pool, ...pooled },
|
|
669
802
|
...onDemand,
|
|
670
803
|
{ name: "Total", used: num(u.totalPercentUsed), ...resets, aside: true },
|
|
671
804
|
],
|
|
805
|
+
credits: free.windows,
|
|
672
806
|
}
|
|
673
807
|
}
|
|
674
808
|
|
|
@@ -3233,7 +3367,7 @@ export async function CursorAuthPlugin(input) {
|
|
|
3233
3367
|
key: INCLUDED_ONLY,
|
|
3234
3368
|
label: "Included usage only",
|
|
3235
3369
|
description:
|
|
3236
|
-
"Keeps an account in routing only while its included usage lasts: once Cursor Models
|
|
3370
|
+
"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 or free credit could pay.",
|
|
3237
3371
|
},
|
|
3238
3372
|
],
|
|
3239
3373
|
// magpie's: how much of the plan's included usage is gone (the plan
|
|
@@ -3246,14 +3380,17 @@ export async function CursorAuthPlugin(input) {
|
|
|
3246
3380
|
} catch (e) {
|
|
3247
3381
|
return { error: e.message, windows: [], signIn: "kept" }
|
|
3248
3382
|
}
|
|
3249
|
-
const
|
|
3383
|
+
const info = planOf(tok)
|
|
3384
|
+
const [u, { plan }, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {}), includedOnly(), info), info, botUsage(tok)])
|
|
3250
3385
|
// Bot goes among magpie's three menu-bar rings: before Total, and
|
|
3251
3386
|
// before an On-demand with no limit, which is only a dollar figure on
|
|
3252
3387
|
// a ring that never fills (magpie #2213, Morganwan90). An On-demand
|
|
3253
3388
|
// with a limit stays ahead of it: that is what can stop the account.
|
|
3389
|
+
// Free credit goes after them, with Total: it is shown only.
|
|
3254
3390
|
const last = (w) => w.name === "Total" || (w.name === "On-demand" && w.aside)
|
|
3255
|
-
const
|
|
3256
|
-
|
|
3391
|
+
const { credits: free = [], ...read } = u
|
|
3392
|
+
const windows = [...read.windows.filter((w) => !last(w)), ...bot, ...free, ...read.windows.filter(last)]
|
|
3393
|
+
return { ...read, windows, ...(plan ? { plan } : {}), signIn: "kept" }
|
|
3257
3394
|
},
|
|
3258
3395
|
},
|
|
3259
3396
|
// the session the request is part of, for the conversation_id
|
|
@@ -3295,4 +3432,4 @@ export async function CursorAuthPlugin(input) {
|
|
|
3295
3432
|
}
|
|
3296
3433
|
|
|
3297
3434
|
// for tests
|
|
3298
|
-
export const _internal = { kvReply, listFor, lists, keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, 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 }
|
|
3435
|
+
export const _internal = { credits, kvReply, listFor, lists, keychain, keychainToken, nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, resetTeam: () => { teamRead.at = 0; teamRead.info = null }, expireTeam: () => (teamRead.at = 0),H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, 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.
|
|
3
|
+
"version": "0.2.33",
|
|
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",
|