@magpie-community/opencode-cursor-auth 0.2.9 → 0.2.11

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 +11 -4
  2. package/index.mjs +73 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -76,9 +76,13 @@ plugin's `fetch` answers them on Cursor's agent API:
76
76
  usage events, by the Run's conversation id: it shows in about 2.5 s,
77
77
  and is looked for up to 6 s. Far from Cursor it shows later than that
78
78
  (yetone/magpie#1053), so the step says 0 used and the conversation owes
79
- it: its next steps collect the event in the background and count it
80
- with their own, and a step that ends the turn waits for what is still
81
- owed, so the conversation adds up to what Cursor counted. An account
79
+ it: its next steps collect the event in the background, and a step
80
+ that ends the turn waits for what is still owed, so the conversation
81
+ adds up to what Cursor counted. What a step collected of earlier steps
82
+ is not added to its own count, which a client reads as how full its
83
+ context is: it is `usage.magpie_deferred` (`prompt_tokens`,
84
+ `completion_tokens`, `cached_tokens`, `cache_write_tokens`), which
85
+ magpie bills with the step and clients ignore. An account
82
86
  whose events miss the wait 3 steps in a row stops waiting for them.
83
87
  With nothing naming the session (so no later step to collect it), or
84
88
  an account that can't read its usage events, the step is estimated as
@@ -140,7 +144,10 @@ team or personal spend limit, as `cursor-agent`'s usage view shows it) also
140
144
  shows On-demand: its spend, and its limit when it has one. Cursor keeps
141
145
  serving such an account past the included usage, so magpie doesn't count
142
146
  it used up when Cursor Models or Other Models reach 100%; it does when the
143
- on-demand spend reaches its limit.
147
+ on-demand spend reaches its limit. A team member with no limit of their
148
+ own spends the team's pool: On-demand shows the pool's spend and limit,
149
+ with the member's own spend beside it, and the account is used up when
150
+ the pool is.
144
151
 
145
152
  Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
146
153
  subscription grants Bot access without a paid Cursor plan; see
package/index.mjs CHANGED
@@ -385,7 +385,10 @@ function firstParty(model) {
385
385
  // period's spendLimitUsage (cents) and, where that sets no limit of the
386
386
  // user's, GetHardLimit's reply (dollars; undefined when it couldn't be
387
387
  // read). kind is "fixed" (up to limitDollars), "unlimited", "disabled" or
388
- // "unavailable" (not known).
388
+ // "unavailable" (not known). A team member with no limit of their own
389
+ // spends the team's pool, which cursor-agent's view calls unlimited but
390
+ // which runs out at pooledLimit for the whole team (magpie #1868): that is
391
+ // a fixed limit on the pool's spend, with the member's own in mineDollars.
389
392
  function onDemandOf(data, hard) {
390
393
  const s = data?.spendLimitUsage
391
394
  const usedDollars = Number(s?.individualUsed ?? 0) / 100
@@ -397,8 +400,11 @@ function onDemandOf(data, hard) {
397
400
  : { kind: "disabled", usedDollars }
398
401
  if (s?.limitType === "team") {
399
402
  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 }
403
+ if (hard && (hard.noUsageBasedAllowed || !(Number(hard.hardLimit) > 0))) return { kind: "disabled", usedDollars }
404
+ // pooledLimit and pooledRemaining come as int64 strings, pooledUsed as a number
405
+ const pool = Number(s.pooledLimit ?? 0)
406
+ if (pool > 0) return { kind: "fixed", usedDollars: Number(s.pooledUsed ?? 0) / 100, limitDollars: pool / 100, mineDollars: usedDollars }
407
+ return hard ? { kind: "unlimited", usedDollars } : { kind: "unavailable", usedDollars }
402
408
  }
403
409
  if (!hard) return own ?? { kind: "unavailable", usedDollars }
404
410
  if (hard.noUsageBasedAllowed) return { kind: "disabled", usedDollars }
@@ -422,7 +428,8 @@ async function hardLimit(tok) {
422
428
  }
423
429
  }
424
430
 
425
- const dollars = (n) => "$" + n.toFixed(2)
431
+ // dollars, thousands grouped: a team's pool runs to six figures ($451,200.00)
432
+ const dollars = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
426
433
 
427
434
  // usage is the account's windows this billing period; an enterprise plan
428
435
  // reports spend instead, and gets none. ids are the models it can be
@@ -430,7 +437,8 @@ const dollars = (n) => "$" + n.toFixed(2)
430
437
  // may spend on-demand once its included usage is gone (fottencity on
431
438
  // Discord: a team that pays for more) is still served when the pools are
432
439
  // 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.
440
+ // out: at its limit (a team member with none of their own: the team's
441
+ // pool), or never when it has none.
434
442
  async function usage(tok, ids) {
435
443
  const [res, hard] = await Promise.all([
436
444
  fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
@@ -467,7 +475,7 @@ async function usage(tok, ids) {
467
475
  const pooled = od.kind === "fixed" || od.kind === "unlimited" ? { aside: true } : {}
468
476
  const onDemand =
469
477
  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 }]
478
+ ? [{ 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 }]
471
479
  : od.kind === "unlimited"
472
480
  ? [{ name: "On-demand", used: 0, display: dollars(od.usedDollars), ...resets, aside: true }]
473
481
  : []
@@ -1400,7 +1408,8 @@ function owe(conv, { since = Date.now(), n = 0, ready = Date.now(), carry = null
1400
1408
 
1401
1409
  // usageEvents are the dashboard's events of conversation conv from since
1402
1410
  // on, the `want` oldest not counted yet, summed: usage (null when none),
1403
- // how many were found (got), and refused when the account can't read them.
1411
+ // each with its time (events), how many were found (got), and refused
1412
+ // when the account can't read them.
1404
1413
  // Polling, they are looked for as STEP_WAIT says, from `first` on, until
1405
1414
  // all show; else asked for once.
1406
1415
  async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_WAIT.first, signal } = {}) {
@@ -1413,6 +1422,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1413
1422
  const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0)
1414
1423
  let usage = null
1415
1424
  let got = 0
1425
+ const events = []
1416
1426
  if (poll) await wait(first)
1417
1427
  while (!signal?.aborted) {
1418
1428
  try {
@@ -1425,7 +1435,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1425
1435
  if (res.status >= 400 && res.status < 500 && res.status !== 429) {
1426
1436
  // not this account's to read
1427
1437
  unreadable.set(tokKey(tok), Date.now())
1428
- return { usage, got, refused: true }
1438
+ return { usage, events, got, refused: true }
1429
1439
  }
1430
1440
  const evs = res.ok ? ((await res.json())?.usageEventsDisplay ?? []) : []
1431
1441
  const found = evs
@@ -1435,7 +1445,9 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1435
1445
  for (const ev of found) {
1436
1446
  claimed.set(conv + "\0" + ev.timestamp, Date.now())
1437
1447
  const t = ev.tokenUsage
1438
- usage = addUsage(usage ?? ZERO(), { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens) })
1448
+ const u = { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
1449
+ usage = addUsage(usage ?? ZERO(), u)
1450
+ events.push({ at: num(ev.timestamp), usage: u })
1439
1451
  got++
1440
1452
  }
1441
1453
  if (got >= want) break
@@ -1443,7 +1455,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1443
1455
  if (!poll || Date.now() + STEP_WAIT.every > end) break
1444
1456
  await wait(STEP_WAIT.every)
1445
1457
  }
1446
- return { usage, got, refused: false }
1458
+ return { usage, events, got, refused: false }
1447
1459
  }
1448
1460
 
1449
1461
  // stepUsage is the usage of the Run of conversation conv started at since,
@@ -1453,22 +1465,24 @@ async function stepUsage(tok, conv, since, signal) {
1453
1465
  }
1454
1466
 
1455
1467
  // collect takes conversation conv's debt and asks for its events (polling
1456
- // when poll): what was carried and what was found (usage), and whether
1457
- // any is still owed (short), which is owed on. What comes back once the
1458
- // Run that asked is over (over() true) is carried to the next instead.
1468
+ // when poll): what was carried and what was found (usage), what was
1469
+ // carried alone (carry) and the events found (events), and whether any is
1470
+ // still owed (short), which is owed on. What comes back once the Run that
1471
+ // asked is over (over() true) is carried to the next instead.
1459
1472
  function collect(tok, conv, { poll = false, first, signal, over = () => false } = {}) {
1460
1473
  const d = owed.get(conv)
1461
- if (!d) return Promise.resolve({ usage: null, short: false })
1474
+ if (!d) return Promise.resolve({ usage: null, carry: null, events: [], short: false })
1462
1475
  owed.delete(conv)
1463
1476
  const p = (async () => {
1464
- let r = { usage: null, got: 0 }
1477
+ let r = { usage: null, events: [], got: 0 }
1465
1478
  if (d.n > 0 && canRead(tok)) r = await usageEvents(tok, conv, d.since, d.n, { poll, first, signal }).catch(() => r)
1466
1479
  const short = d.n > r.got && canRead(tok)
1467
1480
  if (short) owe(conv, { since: d.since, n: d.n - r.got, ready: d.ready })
1481
+ const carry = d.carry && { ...d.carry }
1468
1482
  const usage = addUsage(d.carry, r.usage)
1469
- if (!over()) return { usage, short }
1483
+ if (!over()) return { usage, carry, events: r.events ?? [], short }
1470
1484
  owe(conv, { carry: usage })
1471
- return { usage: null, short }
1485
+ return { usage: null, carry: null, events: [], short }
1472
1486
  })()
1473
1487
  const set = flights.get(conv) ?? new Set()
1474
1488
  flights.set(conv, set.add(p))
@@ -1490,22 +1504,41 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false }
1490
1504
  // read its events). owed(final) is what the Run collected otherwise,
1491
1505
  // all that can be had when final. done() ends the Run: what was collected
1492
1506
  // and not given out goes back.
1507
+ //
1508
+ // What a Run collects is told apart by its event's time: Cursor stamps a
1509
+ // Run's event as the Run begins (1.4–3.4 s after since), so one from since
1510
+ // on (less the second usageEvents allows) is this Run's own (mine), and one
1511
+ // before it, or a count carried, an earlier Run's (theirs). own() gives out only mine, as the step's usage,
1512
+ // which a client reads as how full its context is; theirs goes out by
1513
+ // owed(), apart. A Run of its conversation begins only once the one
1514
+ // before has ended, so the two never meet; were the time off, the step's
1515
+ // count would be told in the wrong field, never twice nor lost.
1493
1516
  function ledger(tok, conv, since, signal, kept) {
1494
1517
  const key = tokKey(tok)
1495
- let pending = null // collected, not given out yet
1518
+ let mine = null // this Run's own, collected, not given out yet
1519
+ let theirs = null // earlier Runs', collected, not given out yet
1496
1520
  let over = false
1497
1521
  const gather = (opts) =>
1498
1522
  collect(tok, conv, { ...opts, over: () => over }).then((r) => {
1499
- pending = addUsage(pending, r.usage)
1523
+ theirs = addUsage(theirs, r.carry)
1524
+ for (const e of r.events ?? []) {
1525
+ if (e.at >= since - 1000) mine = addUsage(mine, e.usage)
1526
+ else theirs = addUsage(theirs, e.usage)
1527
+ }
1500
1528
  return r
1501
1529
  })
1502
1530
  let collecting = null
1503
1531
  const debt = kept ? owed.get(conv) : undefined
1504
1532
  const timer = debt ? setTimeout(() => (collecting = gather()), Math.max(0, debt.ready - Date.now())) : null
1505
1533
  timer?.unref?.()
1534
+ const takeMine = () => {
1535
+ const u = mine
1536
+ mine = null
1537
+ return u
1538
+ }
1506
1539
  const take = () => {
1507
- const u = pending
1508
- pending = null
1540
+ const u = addUsage(theirs, mine)
1541
+ mine = theirs = null
1509
1542
  return u
1510
1543
  }
1511
1544
  return {
@@ -1526,7 +1559,7 @@ function ledger(tok, conv, since, signal, kept) {
1526
1559
  if (r.short) misses.set(key, m + 1)
1527
1560
  else misses.delete(key)
1528
1561
  } else misses.set(key, m + 1)
1529
- return take() ?? false
1562
+ return takeMine() ?? false
1530
1563
  },
1531
1564
  async owed(final) {
1532
1565
  if (!kept) return null
@@ -1540,7 +1573,8 @@ function ledger(tok, conv, since, signal, kept) {
1540
1573
  done() {
1541
1574
  over = true
1542
1575
  clearTimeout(timer)
1543
- if (pending) owe(conv, { carry: take() })
1576
+ const u = take()
1577
+ if (u) owe(conv, { carry: u })
1544
1578
  },
1545
1579
  }
1546
1580
  }
@@ -1910,16 +1944,19 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
1910
1944
  // with it or by a later step (ledger), never guessed
1911
1945
  let counted = false
1912
1946
  // finish is the step's end, with what earlier steps owed (more), which
1913
- // is no reason not to guess the step's own
1947
+ // is no reason not to guess the step's own. The step's usage is its own
1948
+ // Run's alone: a client reads its prompt_tokens as how full the context
1949
+ // is (pi, OpenCode), and an earlier step's prompt added to it read twice
1950
+ // the context. What earlier steps owed goes apart, as magpie_deferred,
1951
+ // which magpie counts with the step and no client reads.
1914
1952
  const finish = (more) => {
1915
1953
  if (!counted && !usage.output) usage.output = Math.floor((said + 3) / 4)
1916
1954
  // a guess only when Cursor counted nothing: a prompt read whole from
1917
1955
  // the cache leaves no uncached rest, and is no reason to guess; nor is
1918
1956
  // a prompt counted from the events
1919
1957
  if (!counted && !usage.input && !usage.cacheRead && !usage.cacheWrite) usage.input = estimate
1920
- // the events' output is their steps', counted here as their prompt is
1921
- if (more) for (const k of ["input", "output", "cacheRead", "cacheWrite"]) usage[k] += more[k]
1922
1958
  const prompt = usage.input + usage.cacheRead + usage.cacheWrite
1959
+ const deferred = more && more.input + more.output + more.cacheRead + more.cacheWrite > 0
1923
1960
  return {
1924
1961
  stop: calls > 0 ? "tool_calls" : "stop",
1925
1962
  usage: {
@@ -1928,6 +1965,16 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
1928
1965
  total_tokens: prompt + usage.output,
1929
1966
  prompt_tokens_details: { cached_tokens: usage.cacheRead, cache_write_tokens: usage.cacheWrite },
1930
1967
  ...(usage.reasoning ? { completion_tokens_details: { reasoning_tokens: usage.reasoning } } : {}),
1968
+ ...(deferred
1969
+ ? {
1970
+ magpie_deferred: {
1971
+ prompt_tokens: more.input + more.cacheRead + more.cacheWrite,
1972
+ completion_tokens: more.output,
1973
+ cached_tokens: more.cacheRead,
1974
+ cache_write_tokens: more.cacheWrite,
1975
+ },
1976
+ }
1977
+ : {}),
1931
1978
  },
1932
1979
  }
1933
1980
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",