@magpie-community/opencode-cursor-auth 0.2.4 → 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.
- package/README.md +18 -1
- package/index.mjs +111 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,9 +124,26 @@ minutes), the way pi-cursor-sdk lists Cursor's catalog:
|
|
|
124
124
|
and `service_tier` counting as for a listed model. The list doesn't show
|
|
125
125
|
these ids: pick the `@<size>` model for a window that is the model's.
|
|
126
126
|
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
magpie shows Cursor Models, Other Models and Total for the billing period.
|
|
130
|
+
Accounts with a Grok Bot allowance also show its usage and reset time;
|
|
131
|
+
active trials are labelled separately. A spent Bot allowance doesn't stop
|
|
132
|
+
Cursor requests.
|
|
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
|
+
|
|
141
|
+
Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
|
|
142
|
+
subscription grants Bot access without a paid Cursor plan; see
|
|
143
|
+
[Grok Bot plans and billing](https://cursor.com/help/grok-bot/plans).
|
|
144
|
+
|
|
127
145
|
## Not included
|
|
128
146
|
|
|
129
|
-
- Usage and plan limits.
|
|
130
147
|
- Web search: Cursor's own web search is refused like its other tools.
|
|
131
148
|
- Switching between several accounts. OpenCode keeps one sign-in per
|
|
132
149
|
provider.
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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,16 +463,62 @@ 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
|
}
|
|
423
483
|
}
|
|
424
484
|
|
|
485
|
+
// Grok Bot's separate allowance, using the CLI JWT as a dashboard session.
|
|
486
|
+
async function botUsage(tok) {
|
|
487
|
+
try {
|
|
488
|
+
const subject = JSON.parse(Buffer.from(tok.split(".")[1], "base64url").toString()).sub
|
|
489
|
+
if (typeof subject !== "string") return []
|
|
490
|
+
const user = subject.split("|").filter(Boolean).at(-1)
|
|
491
|
+
if (!user || !/^[a-zA-Z0-9._-]+$/.test(user)) return []
|
|
492
|
+
const res = await fetch(WEBSITE + "/api/dashboard/get-sand-usage-status", {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers: {
|
|
495
|
+
"Content-Type": "application/json",
|
|
496
|
+
Accept: "application/json",
|
|
497
|
+
Origin: WEBSITE,
|
|
498
|
+
Cookie: "WorkosCursorSessionToken=" + encodeURIComponent(user + "::" + tok),
|
|
499
|
+
},
|
|
500
|
+
body: "{}",
|
|
501
|
+
redirect: "error",
|
|
502
|
+
signal: AbortSignal.timeout(5_000),
|
|
503
|
+
})
|
|
504
|
+
if (!res.ok) return []
|
|
505
|
+
const data = await res.json()
|
|
506
|
+
const paid = typeof data?.includedLimitZero === "boolean"
|
|
507
|
+
? !data.includedLimitZero : data?.hasNonZeroIncludedLimit === true
|
|
508
|
+
const trial = !paid && typeof data?.sandTrialExpiresAt === "string" && Date.parse(data.sandTrialExpiresAt) > Date.now()
|
|
509
|
+
if ((!paid && !trial) || typeof data?.usagePercent !== "number" || !Number.isFinite(data.usagePercent)) return []
|
|
510
|
+
const reset = paid && typeof data.nextResetTimestampUtc === "string" ? Date.parse(data.nextResetTimestampUtc) : NaN
|
|
511
|
+
return [{
|
|
512
|
+
name: trial ? "Grok Bot (trial)" : "Grok Bot",
|
|
513
|
+
used: Math.max(0, Math.min(100, data.usagePercent)),
|
|
514
|
+
aside: true, // Bot exhaustion must not stop Cursor model requests.
|
|
515
|
+
...(Number.isFinite(reset) ? { resetsAt: new Date(reset).toISOString(), span: 604800 } : {}),
|
|
516
|
+
}]
|
|
517
|
+
} catch {
|
|
518
|
+
return []
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
425
522
|
// ---- models ----------------------------------------------------------------------
|
|
426
523
|
//
|
|
427
524
|
// The list is Cursor's model picker (AiService/AvailableModels, asked as the
|
|
@@ -2158,8 +2255,10 @@ export async function CursorAuthPlugin() {
|
|
|
2158
2255
|
} catch (e) {
|
|
2159
2256
|
return { error: e.message, windows: [], signIn: "kept" }
|
|
2160
2257
|
}
|
|
2161
|
-
const [u, plan] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok)])
|
|
2162
|
-
|
|
2258
|
+
const [u, plan, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok), botUsage(tok)])
|
|
2259
|
+
// Put Bot before Total for magpie's three menu-bar rings.
|
|
2260
|
+
const windows = [...u.windows.filter((w) => w.name !== "Total"), ...bot, ...u.windows.filter((w) => w.name === "Total")]
|
|
2261
|
+
return { ...u, windows, ...(plan ? { plan } : {}), signIn: "kept" }
|
|
2163
2262
|
},
|
|
2164
2263
|
},
|
|
2165
2264
|
// the session the request is part of, for the conversation_id
|
|
@@ -2201,4 +2300,4 @@ export async function CursorAuthPlugin() {
|
|
|
2201
2300
|
}
|
|
2202
2301
|
|
|
2203
2302
|
// for tests
|
|
2204
|
-
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.
|
|
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",
|