@magpie-community/opencode-cursor-auth 0.2.28 → 0.2.30
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 +15 -2
- package/index.mjs +118 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,8 +51,10 @@ plugin's `fetch` answers them on Cursor's agent API:
|
|
|
51
51
|
(refused, no network) or a stream the server refused leaves the next Run
|
|
52
52
|
to try HTTP/2 again. A region error over HTTP/1.1 sends the Run back to
|
|
53
53
|
HTTP/2 at the region's agent host. BidiAppends go up numbered in order,
|
|
54
|
-
up to 16 at a time as cursor-agent sends them
|
|
55
|
-
|
|
54
|
+
up to 16 at a time as cursor-agent sends them. A native session's Run
|
|
55
|
+
sends only its KV replies (the history's blobs) that way; a tool result
|
|
56
|
+
or anything else waits for the appends before it, goes alone, and is
|
|
57
|
+
written once Cursor answered it, so it is never uncertain. A BidiAppend
|
|
56
58
|
with no answer in 60 s (more for a large one) ends the Run, as
|
|
57
59
|
cursor-agent does.
|
|
58
60
|
- A new Run imports the whole conversation as AI SDK messages kept as blobs
|
|
@@ -252,6 +254,17 @@ own spends the team's pool: On-demand shows the pool's spend and limit,
|
|
|
252
254
|
with the member's own spend beside it, and the account is used up when
|
|
253
255
|
the pool is.
|
|
254
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
|
+
|
|
255
268
|
Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
|
|
256
269
|
subscription grants Bot access without a paid Cursor plan; see
|
|
257
270
|
[Grok Bot plans and billing](https://cursor.com/help/grok-bot/plans).
|
package/index.mjs
CHANGED
|
@@ -532,14 +532,79 @@ 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
|
+
// enterpriseUsage is the allowance GetCurrentPeriodUsage leaves out on an
|
|
536
|
+
// Enterprise account. That call answers with only the billing cycle there;
|
|
537
|
+
// the website's usage-summary has individualUsage.overall, in cents, and
|
|
538
|
+
// the account's membership. The CLI JWT is also the website's dashboard
|
|
539
|
+
// session, as botUsage builds it. undefined when the summary says nothing
|
|
540
|
+
// this can show, so a failed read stays an empty card, as before.
|
|
541
|
+
async function enterpriseUsage(tok) {
|
|
542
|
+
try {
|
|
543
|
+
const subject = JSON.parse(Buffer.from(tok.split(".")[1], "base64url").toString()).sub
|
|
544
|
+
if (typeof subject !== "string") return undefined
|
|
545
|
+
const user = subject.split("|").filter(Boolean).at(-1)
|
|
546
|
+
if (!user || !/^[a-zA-Z0-9._-]+$/.test(user)) return undefined
|
|
547
|
+
const res = await fetch(WEBSITE + "/api/usage-summary", {
|
|
548
|
+
method: "GET",
|
|
549
|
+
headers: {
|
|
550
|
+
Accept: "application/json",
|
|
551
|
+
Origin: WEBSITE,
|
|
552
|
+
Cookie: "WorkosCursorSessionToken=" + encodeURIComponent(user + "::" + tok),
|
|
553
|
+
},
|
|
554
|
+
redirect: "error",
|
|
555
|
+
signal: AbortSignal.timeout(15_000),
|
|
556
|
+
})
|
|
557
|
+
if (!res.ok) return undefined
|
|
558
|
+
const data = await res.json()
|
|
559
|
+
if (String(data?.membershipType).toLowerCase() !== "enterprise") return undefined
|
|
560
|
+
const overall = data?.individualUsage?.overall
|
|
561
|
+
if (overall?.enabled !== true) return undefined
|
|
562
|
+
const { used, limit } = overall
|
|
563
|
+
if (typeof used !== "number" || !Number.isFinite(used) || used < 0) return undefined
|
|
564
|
+
if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) return undefined
|
|
565
|
+
const end = Date.parse(data.billingCycleEnd ?? "")
|
|
566
|
+
const resets = Number.isFinite(end) ? { resetsAt: new Date(end).toISOString() } : {}
|
|
567
|
+
const od = data.teamUsage?.onDemand
|
|
568
|
+
const spent = typeof od?.used === "number" && Number.isFinite(od.used) && od.used >= 0
|
|
569
|
+
const unlimited = od?.enabled === true && od.limit === null
|
|
570
|
+
const fixed = od?.enabled === true && typeof od.limit === "number" && Number.isFinite(od.limit) && od.limit > 0 && spent
|
|
571
|
+
// 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)
|
|
574
|
+
const windows = [{
|
|
575
|
+
name: "Enterprise",
|
|
576
|
+
used: Math.max(0, Math.min(100, (100 * used) / limit)),
|
|
577
|
+
display: `${dollars(used / 100)} / ${dollars(limit / 100)}`,
|
|
578
|
+
amount: used / 100,
|
|
579
|
+
limit: limit / 100,
|
|
580
|
+
unit: "USD",
|
|
581
|
+
...resets,
|
|
582
|
+
...(aside ? { aside: true } : {}),
|
|
583
|
+
}]
|
|
584
|
+
if (fixed) windows.push({
|
|
585
|
+
name: "On-demand",
|
|
586
|
+
used: Math.min(100, (100 * od.used) / od.limit),
|
|
587
|
+
display: `${dollars(od.used / 100)} / ${dollars(od.limit / 100)}`,
|
|
588
|
+
amount: od.used / 100,
|
|
589
|
+
limit: od.limit / 100,
|
|
590
|
+
unit: "USD",
|
|
591
|
+
...resets,
|
|
592
|
+
})
|
|
593
|
+
else if (unlimited && spent) windows.push({ name: "On-demand", used: 0, display: dollars(od.used / 100), ...resets, aside: true })
|
|
594
|
+
return windows
|
|
595
|
+
} catch {
|
|
596
|
+
return undefined
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// usage is the account's windows this billing period. ids are the models it
|
|
601
|
+
// can be asked for, each counted by the pool Cursor bills it to. An account
|
|
602
|
+
// that may spend on-demand once its included usage is gone (fottencity on
|
|
539
603
|
// Discord: a team that pays for more) is still served when the pools are
|
|
540
604
|
// spent, so they are shown aside, and its On-demand spend is what can run
|
|
541
605
|
// out: at its limit (a team member with none of their own: the team's
|
|
542
|
-
// pool), or never when it has none.
|
|
606
|
+
// pool), or never when it has none. An Enterprise account's period has no
|
|
607
|
+
// planUsage; its allowance comes from usage-summary instead.
|
|
543
608
|
async function usage(tok, ids) {
|
|
544
609
|
const [res, hard] = await Promise.all([
|
|
545
610
|
fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
|
|
@@ -553,7 +618,11 @@ async function usage(tok, ids) {
|
|
|
553
618
|
if (!res.ok) return { error: statusText(res.status), windows: [] }
|
|
554
619
|
const data = await res.json()
|
|
555
620
|
const u = data?.planUsage
|
|
556
|
-
if (!u)
|
|
621
|
+
if (!u) {
|
|
622
|
+
// Prefer measured Enterprise usage before any reset-date-only fallback.
|
|
623
|
+
const windows = await enterpriseUsage(tok)
|
|
624
|
+
return windows ? { windows } : { windows: [] }
|
|
625
|
+
}
|
|
557
626
|
const ms = /^[+-]?\d+$/.test(data.billingCycleEnd ?? "") ? Number(data.billingCycleEnd) : 0
|
|
558
627
|
const resets = ms > 0 ? { resetsAt: new Date(ms).toISOString() } : {}
|
|
559
628
|
const bucket = new Set((data.autoBucketModels ?? []).map(poolBase))
|
|
@@ -2038,10 +2107,11 @@ async function agentURL(tok, fresh) {
|
|
|
2038
2107
|
// H2.retryAfter, or until a region error sends one back (answerOf); a
|
|
2039
2108
|
// connection that failed (refused, no network) or a stream the server
|
|
2040
2109
|
// refused leaves the next Run to try HTTP/2 again. atOnce is how many
|
|
2041
|
-
// BidiAppends openH1 may have on their way at once
|
|
2110
|
+
// BidiAppends openH1 may have on their way at once, overlaps which messages
|
|
2111
|
+
// may be among them (openH1).
|
|
2042
2112
|
let h1Until = 0
|
|
2043
2113
|
const cancelledOpen = () => ({ error: { status: 410, message: "the request was cancelled" }, h2Failed: false })
|
|
2044
|
-
async function open(base, headers, signal, atOnce = H1.atOnce) {
|
|
2114
|
+
async function open(base, headers, signal, atOnce = H1.atOnce, overlaps) {
|
|
2045
2115
|
if (signal?.aborted) return cancelledOpen()
|
|
2046
2116
|
if (Date.now() >= h1Until) {
|
|
2047
2117
|
const o = await openH2(base, headers, signal)
|
|
@@ -2055,7 +2125,7 @@ async function open(base, headers, signal, atOnce = H1.atOnce) {
|
|
|
2055
2125
|
if (o.blocked) h1Until = Date.now() + H2.retryAfter
|
|
2056
2126
|
}
|
|
2057
2127
|
if (signal?.aborted) return cancelledOpen()
|
|
2058
|
-
return openH1(headers, signal, atOnce)
|
|
2128
|
+
return openH1(headers, signal, atOnce, overlaps)
|
|
2059
2129
|
}
|
|
2060
2130
|
|
|
2061
2131
|
const H2 = {
|
|
@@ -2128,10 +2198,19 @@ const H1 = {
|
|
|
2128
2198
|
// keeps them: one at a time, a new Run's history (a blob a message,
|
|
2129
2199
|
// hundreds for a long conversation, all asked for at once) took a round
|
|
2130
2200
|
// trip to Cursor each before the model could start. A native session's
|
|
2131
|
-
// Run
|
|
2201
|
+
// Run sends only its KV replies that way (kvReply); each of its other
|
|
2202
|
+
// messages waits for every append before it, and goes alone (runNative).
|
|
2132
2203
|
atOnce: 16,
|
|
2133
2204
|
}
|
|
2134
2205
|
|
|
2206
|
+
// kvReply is whether a client message (an AgentClientMessage, unframed) is
|
|
2207
|
+
// a KV reply, kv_client_message: a blob Cursor asked for, which nothing
|
|
2208
|
+
// else waits on. Cursor asks for a new Run's whole history at once.
|
|
2209
|
+
function kvReply(msg) {
|
|
2210
|
+
const b = Buffer.isBuffer(msg) ? msg : Buffer.from(msg ?? EMPTY)
|
|
2211
|
+
return b.length > 0 && b[0] === 0x1a // field 3, length-delimited
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2135
2214
|
// connectCode is Connect's code for an HTTP status that came with no
|
|
2136
2215
|
// Connect error of its own
|
|
2137
2216
|
function connectCode(status) {
|
|
@@ -2144,8 +2223,11 @@ function connectCode(status) {
|
|
|
2144
2223
|
// the stream it reads, Connect's frames as the HTTP/2 Run's. Its response
|
|
2145
2224
|
// is a 200 at once; an error Cursor says later, or a call that fails,
|
|
2146
2225
|
// ends the stream with Connect's end frame, which runOnce reads as the
|
|
2147
|
-
// Run's own.
|
|
2148
|
-
|
|
2226
|
+
// Run's own. overlaps says of a message whether it may go up while appends
|
|
2227
|
+
// before it are on their way (up to atOnce of them); one that may not waits
|
|
2228
|
+
// for all of them, goes alone, and its write's callback is its own append
|
|
2229
|
+
// answered.
|
|
2230
|
+
function openH1(headers, signal, atOnce = H1.atOnce, overlaps = () => true) {
|
|
2149
2231
|
if (signal?.aborted) return Promise.resolve(cancelledOpen())
|
|
2150
2232
|
const ac = new AbortController()
|
|
2151
2233
|
const id = headers["x-request-id"]
|
|
@@ -2159,11 +2241,20 @@ function openH1(headers, signal, atOnce = H1.atOnce) {
|
|
|
2159
2241
|
write(chunk, _enc, cb) {
|
|
2160
2242
|
// runOnce writes one whole frame each time
|
|
2161
2243
|
const msg = Buffer.from(chunk).subarray(5)
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2244
|
+
const go = () => {
|
|
2245
|
+
const p = append(msg, seq++)
|
|
2246
|
+
going.add(p)
|
|
2247
|
+
const gone = () => going.delete(p)
|
|
2248
|
+
p.then(gone, gone)
|
|
2249
|
+
return p
|
|
2250
|
+
}
|
|
2251
|
+
if (!overlaps(msg)) {
|
|
2252
|
+
// alone: after every append before it is answered, and answered
|
|
2253
|
+
// itself before the next write
|
|
2254
|
+
Promise.allSettled([...going]).then(go).then(() => cb(), (e) => cb(e))
|
|
2255
|
+
return
|
|
2256
|
+
}
|
|
2257
|
+
go()
|
|
2167
2258
|
// Up to atOnce appends go at once; past that the write stays
|
|
2168
2259
|
// buffered until one finishes, so callbacks and drain reflect the
|
|
2169
2260
|
// pending upload rather than an unbounded promise chain. At one,
|
|
@@ -2432,9 +2523,15 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
|
|
|
2432
2523
|
"x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
|
|
2433
2524
|
},
|
|
2434
2525
|
native.ac.signal,
|
|
2435
|
-
//
|
|
2436
|
-
//
|
|
2437
|
-
|
|
2526
|
+
// Over HTTP/1.1 the history's blobs go up H1.atOnce at a time, as
|
|
2527
|
+
// cursor-agent sends them; one at a time, a Run with a long history
|
|
2528
|
+
// waited a round trip to api2 a blob before its first token (ysm119 on
|
|
2529
|
+
// Discord: 135 blobs, 37 s against 3 s over HTTP/2). Everything else
|
|
2530
|
+
// goes alone, after the appends before it were answered, and counts as
|
|
2531
|
+
// written only once Cursor answered it, so a tool result is never
|
|
2532
|
+
// uncertain.
|
|
2533
|
+
H1.atOnce,
|
|
2534
|
+
kvReply,
|
|
2438
2535
|
)
|
|
2439
2536
|
if (o.error) {
|
|
2440
2537
|
native.fail(o.error)
|
|
@@ -3167,4 +3264,4 @@ export async function CursorAuthPlugin() {
|
|
|
3167
3264
|
}
|
|
3168
3265
|
|
|
3169
3266
|
// for tests
|
|
3170
|
-
export const _internal = { 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 }
|
|
3267
|
+
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 }
|
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.30",
|
|
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",
|