@magpie-community/opencode-cursor-auth 0.2.12 → 0.2.13

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 (2) hide show
  1. package/index.mjs +95 -46
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1358,17 +1358,18 @@ function usageOf(uf) {
1358
1358
  // event: its step looks for it as before, and is guessed without it.
1359
1359
  const STEP_WAIT = { first: 1500, every: 500, until: 6000, ask: 5000, skipAfter: 3, retryEvery: 10, owedAt: 5000, owedFor: 600_000 }
1360
1360
 
1361
- // claimed are the usage events already counted, by conversation and time,
1362
- // so that two steps of one conversation never count the same one.
1361
+ // claimed are the usage events already counted, by account, conversation
1362
+ // and time, so that two steps of one conversation never count the same one.
1363
1363
  const claimed = new Map()
1364
1364
 
1365
- // owed are what each conversation still owes, by conversation_id: n Runs
1366
- // whose events are to be counted, the earliest started at since, looked
1367
- // for from ready on; and carry, a count collected that no step gave out.
1365
+ // owed are what each account's conversation still owes, by credential
1366
+ // fingerprint and conversation_id: n Runs whose events are to be counted,
1367
+ // the earliest started at since, looked for from ready on; and carry, a
1368
+ // count collected that no step gave out.
1368
1369
  const owed = new Map()
1369
1370
 
1370
- // flights are each conversation's collections still going, which a step
1371
- // that ends the turn waits for.
1371
+ // flights are each account's conversation collections still going, which
1372
+ // a step that ends the turn waits for.
1372
1373
  const flights = new Map()
1373
1374
 
1374
1375
  // unreadable are the accounts (by token) whose usage events Cursor won't
@@ -1378,6 +1379,7 @@ const flights = new Map()
1378
1379
  const unreadable = new Map()
1379
1380
  const misses = new Map()
1380
1381
  const tokKey = (tok) => createHash("sha256").update(tok).digest("hex")
1382
+ const usageKey = (tok, conv) => tokKey(tok) + "\0" + conv
1381
1383
  function canRead(tok) {
1382
1384
  const at = unreadable.get(tokKey(tok))
1383
1385
  if (at === undefined) return true
@@ -1394,12 +1396,12 @@ function addUsage(a, b) {
1394
1396
  return a
1395
1397
  }
1396
1398
 
1397
- // owe adds to conversation conv's debt.
1398
- function owe(conv, { since = Date.now(), n = 0, ready = Date.now(), carry = null }) {
1399
+ // owe adds to the account/conversation's debt.
1400
+ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null }) {
1399
1401
  for (const [k, d] of owed) if (Date.now() - d.since > STEP_WAIT.owedFor) owed.delete(k)
1400
1402
  if (n <= 0 && !carry) return
1401
- const d = owed.get(conv)
1402
- if (!d) return void owed.set(conv, { since, n, ready, carry: carry && { ...carry } })
1403
+ const d = owed.get(key)
1404
+ if (!d) return void owed.set(key, { since, n, ready, carry: carry && { ...carry } })
1403
1405
  d.since = Math.min(d.since, since)
1404
1406
  d.n += n
1405
1407
  d.ready = Math.min(d.ready, ready)
@@ -1413,6 +1415,7 @@ function owe(conv, { since = Date.now(), n = 0, ready = Date.now(), carry = null
1413
1415
  // Polling, they are looked for as STEP_WAIT says, from `first` on, until
1414
1416
  // all show; else asked for once.
1415
1417
  async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_WAIT.first, signal } = {}) {
1418
+ const key = usageKey(tok, conv)
1416
1419
  const end = Date.now() + STEP_WAIT.until
1417
1420
  const wait = (ms) => new Promise((r) => {
1418
1421
  const t = setTimeout(r, ms)
@@ -1439,11 +1442,11 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1439
1442
  }
1440
1443
  const evs = res.ok ? ((await res.json())?.usageEventsDisplay ?? []) : []
1441
1444
  const found = evs
1442
- .filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(conv + "\0" + e.timestamp))
1445
+ .filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(key + "\0" + e.timestamp))
1443
1446
  .sort((a, b) => num(a.timestamp) - num(b.timestamp))
1444
1447
  .slice(0, want - got)
1445
1448
  for (const ev of found) {
1446
- claimed.set(conv + "\0" + ev.timestamp, Date.now())
1449
+ claimed.set(key + "\0" + ev.timestamp, Date.now())
1447
1450
  const t = ev.tokenUsage
1448
1451
  const u = { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
1449
1452
  usage = addUsage(usage ?? ZERO(), u)
@@ -1464,31 +1467,32 @@ async function stepUsage(tok, conv, since, signal) {
1464
1467
  return (await usageEvents(tok, conv, since, 1, { poll: true, signal })).usage
1465
1468
  }
1466
1469
 
1467
- // collect takes conversation conv's debt and asks for its events (polling
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
+ // collect takes the account's debt for conversation conv and asks for its
1471
+ // events (polling when poll): what was carried and what was found (usage),
1472
+ // what was carried alone (carry) and the events found (events), and whether any is
1470
1473
  // still owed (short), which is owed on. What comes back once the Run that
1471
1474
  // asked is over (over() true) is carried to the next instead.
1472
1475
  function collect(tok, conv, { poll = false, first, signal, over = () => false } = {}) {
1473
- const d = owed.get(conv)
1476
+ const key = usageKey(tok, conv)
1477
+ const d = owed.get(key)
1474
1478
  if (!d) return Promise.resolve({ usage: null, carry: null, events: [], short: false })
1475
- owed.delete(conv)
1479
+ owed.delete(key)
1476
1480
  const p = (async () => {
1477
1481
  let r = { usage: null, events: [], got: 0 }
1478
1482
  if (d.n > 0 && canRead(tok)) r = await usageEvents(tok, conv, d.since, d.n, { poll, first, signal }).catch(() => r)
1479
1483
  const short = d.n > r.got && canRead(tok)
1480
- if (short) owe(conv, { since: d.since, n: d.n - r.got, ready: d.ready })
1484
+ if (short) owe(key, { since: d.since, n: d.n - r.got, ready: d.ready })
1481
1485
  const carry = d.carry && { ...d.carry }
1482
1486
  const usage = addUsage(d.carry, r.usage)
1483
1487
  if (!over()) return { usage, carry, events: r.events ?? [], short }
1484
- owe(conv, { carry: usage })
1488
+ owe(key, { carry: usage })
1485
1489
  return { usage: null, carry: null, events: [], short }
1486
1490
  })()
1487
- const set = flights.get(conv) ?? new Set()
1488
- flights.set(conv, set.add(p))
1491
+ const set = flights.get(key) ?? new Set()
1492
+ flights.set(key, set.add(p))
1489
1493
  p.finally(() => {
1490
1494
  set.delete(p)
1491
- if (!set.size && flights.get(conv) === set) flights.delete(conv)
1495
+ if (!set.size && flights.get(key) === set) flights.delete(key)
1492
1496
  })
1493
1497
  return p
1494
1498
  }
@@ -1515,6 +1519,7 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false }
1515
1519
  // count would be told in the wrong field, never twice nor lost.
1516
1520
  function ledger(tok, conv, since, signal, kept) {
1517
1521
  const key = tokKey(tok)
1522
+ const debtKey = usageKey(tok, conv)
1518
1523
  let mine = null // this Run's own, collected, not given out yet
1519
1524
  let theirs = null // earlier Runs', collected, not given out yet
1520
1525
  let over = false
@@ -1528,7 +1533,7 @@ function ledger(tok, conv, since, signal, kept) {
1528
1533
  return r
1529
1534
  })
1530
1535
  let collecting = null
1531
- const debt = kept ? owed.get(conv) : undefined
1536
+ const debt = kept ? owed.get(debtKey) : undefined
1532
1537
  const timer = debt ? setTimeout(() => (collecting = gather()), Math.max(0, debt.ready - Date.now())) : null
1533
1538
  timer?.unref?.()
1534
1539
  const takeMine = () => {
@@ -1552,7 +1557,7 @@ function ledger(tok, conv, since, signal, kept) {
1552
1557
  else if (!r.refused) misses.set(key, m + 1)
1553
1558
  return r.usage ?? null
1554
1559
  }
1555
- owe(conv, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
1560
+ owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
1556
1561
  if (waits) {
1557
1562
  const r = await gather({ poll: true, signal })
1558
1563
  if (!canRead(tok)) return null
@@ -1565,8 +1570,8 @@ function ledger(tok, conv, since, signal, kept) {
1565
1570
  if (!kept) return null
1566
1571
  if (final) {
1567
1572
  await collecting
1568
- await Promise.all([...(flights.get(conv) ?? [])])
1569
- if (owed.has(conv)) await gather({ poll: owed.get(conv).n > 0, first: 0, signal })
1573
+ await Promise.all([...(flights.get(debtKey) ?? [])])
1574
+ if (owed.has(debtKey)) await gather({ poll: owed.get(debtKey).n > 0, first: 0, signal })
1570
1575
  }
1571
1576
  return take()
1572
1577
  },
@@ -1574,7 +1579,7 @@ function ledger(tok, conv, since, signal, kept) {
1574
1579
  over = true
1575
1580
  clearTimeout(timer)
1576
1581
  const u = take()
1577
- if (u) owe(conv, { carry: u })
1582
+ if (u) owe(debtKey, { carry: u })
1578
1583
  },
1579
1584
  }
1580
1585
  }
@@ -1666,12 +1671,21 @@ async function agentURL(tok, fresh) {
1666
1671
  // connection that failed (refused, no network) or a stream the server
1667
1672
  // refused leaves the next Run to try HTTP/2 again.
1668
1673
  let h1Until = 0
1674
+ const cancelledOpen = () => ({ error: { status: 410, message: "the request was cancelled" }, h2Failed: false })
1669
1675
  async function open(base, headers, signal) {
1676
+ if (signal?.aborted) return cancelledOpen()
1670
1677
  if (Date.now() >= h1Until) {
1671
1678
  const o = await openH2(base, headers, signal)
1679
+ // The head can arrive just before abort, before this continuation owns it.
1680
+ if (signal?.aborted) {
1681
+ o.req?.close(http2.constants.NGHTTP2_CANCEL)
1682
+ o.session?.destroy()
1683
+ return cancelledOpen()
1684
+ }
1672
1685
  if (!o.h2Failed) return o
1673
1686
  if (o.blocked) h1Until = Date.now() + H2.retryAfter
1674
1687
  }
1688
+ if (signal?.aborted) return cancelledOpen()
1675
1689
  return openH1(headers, signal)
1676
1690
  }
1677
1691
 
@@ -1702,6 +1716,7 @@ function h2Blocked(e) {
1702
1716
  // than by being cancelled, says h2Failed, and blocked when h2Blocked says
1703
1717
  // so of the failure.
1704
1718
  function openH2(base, headers, signal) {
1719
+ if (signal?.aborted) return Promise.resolve(cancelledOpen())
1705
1720
  return new Promise((resolve) => {
1706
1721
  const u = new URL(base)
1707
1722
  const session = http2.connect(u.origin)
@@ -1710,9 +1725,11 @@ function openH2(base, headers, signal) {
1710
1725
  if (done) return
1711
1726
  done = true
1712
1727
  clearTimeout(timer)
1728
+ signal?.removeEventListener("abort", abort)
1713
1729
  session.destroy()
1714
- resolve({ error: { status: 502, message: e?.message ?? String(e) }, h2Failed, blocked: h2Failed && h2Blocked(e) })
1730
+ resolve({ error: { status: h2Failed ? 502 : 410, message: e?.message ?? String(e) }, h2Failed, blocked: h2Failed && h2Blocked(e) })
1715
1731
  }
1732
+ const abort = () => fail(new Error("the request was cancelled"), false)
1716
1733
  const timer = setTimeout(() => fail(Object.assign(new Error(`no answer over HTTP/2 in ${H2.wait / 1000}s`), { h2Timeout: true })), H2.wait)
1717
1734
  session.on("error", fail)
1718
1735
  const req = session.request({
@@ -1728,9 +1745,11 @@ function openH2(base, headers, signal) {
1728
1745
  if (done) return
1729
1746
  done = true
1730
1747
  clearTimeout(timer)
1748
+ signal?.removeEventListener("abort", abort)
1731
1749
  resolve({ session, req, status: Number(h[":status"]) })
1732
1750
  })
1733
- signal?.addEventListener("abort", () => fail(new Error("the request was cancelled"), false), { once: true })
1751
+ signal?.addEventListener("abort", abort, { once: true })
1752
+ if (signal?.aborted) abort()
1734
1753
  })
1735
1754
  }
1736
1755
 
@@ -1750,23 +1769,29 @@ function connectCode(status) {
1750
1769
  // ends the stream with Connect's end frame, which runOnce reads as the
1751
1770
  // Run's own.
1752
1771
  function openH1(headers, signal) {
1772
+ if (signal?.aborted) return Promise.resolve(cancelledOpen())
1753
1773
  const ac = new AbortController()
1754
- signal?.addEventListener("abort", () => ac.abort(), { once: true })
1755
1774
  const id = headers["x-request-id"]
1756
1775
  const h = { ...headers, "accept-encoding": "identity", "x-cursor-streaming": "true" }
1757
1776
  let ended = false
1758
1777
  let seq = 0
1759
- let chain = Promise.resolve()
1778
+ let reader
1760
1779
  const req = new Duplex({
1761
1780
  read() {},
1762
1781
  write(chunk, _enc, cb) {
1763
1782
  // runOnce writes one whole frame each time
1764
1783
  const msg = Buffer.from(chunk).subarray(5)
1765
1784
  const n = seq++
1766
- chain = chain.then(() => (ended ? undefined : append(msg, n)))
1767
- cb()
1785
+ // Keep the write buffered until its append finishes, so callbacks and
1786
+ // drain reflect the pending upload rather than an unbounded promise chain.
1787
+ append(msg, n).then(() => cb(), (e) => cb(e))
1768
1788
  },
1769
1789
  })
1790
+ const cleanup = () => {
1791
+ signal?.removeEventListener("abort", close)
1792
+ ac.abort()
1793
+ reader?.cancel().catch(() => {})
1794
+ }
1770
1795
  const end = (body) => {
1771
1796
  if (ended || req.destroyed) return
1772
1797
  ended = true
@@ -1774,7 +1799,7 @@ function openH1(headers, signal) {
1774
1799
  f[0] = 2
1775
1800
  req.push(f)
1776
1801
  req.push(null)
1777
- ac.abort()
1802
+ cleanup()
1778
1803
  }
1779
1804
  const failed = async (res) => {
1780
1805
  const text = await res.text().catch(() => "")
@@ -1788,6 +1813,7 @@ function openH1(headers, signal) {
1788
1813
  // (more time for a large one) ends the Run, as cursor-agent's
1789
1814
  // bidi_append_deadline_exceeded does: the messages after it wait on it.
1790
1815
  const append = async (msg, n) => {
1816
+ if (ended || ac.signal.aborted) return
1791
1817
  const body = pb().str(1, msg.toString("hex")).bytes(2, pb().str(1, id).done()).varint(3, n).done()
1792
1818
  const ms = H1.appendWait + Math.ceil(body.length / 65536) * 1000
1793
1819
  const deadline = AbortSignal.timeout(ms)
@@ -1798,6 +1824,7 @@ function openH1(headers, signal) {
1798
1824
  body,
1799
1825
  signal: AbortSignal.any([ac.signal, deadline]),
1800
1826
  })
1827
+ if (ended || ac.signal.aborted) { await res.body?.cancel().catch(() => {}); return }
1801
1828
  if (!res.ok) return failed(res)
1802
1829
  await res.arrayBuffer()
1803
1830
  } catch (e) {
@@ -1806,6 +1833,16 @@ function openH1(headers, signal) {
1806
1833
  end(JSON.stringify({ error: { code: "unavailable", message: `BidiAppend: ${e?.message ?? e}` } }))
1807
1834
  }
1808
1835
  }
1836
+ const close = () => {
1837
+ ended = true
1838
+ cleanup()
1839
+ req.destroy()
1840
+ }
1841
+ signal?.addEventListener("abort", close, { once: true })
1842
+ if (signal?.aborted) {
1843
+ close()
1844
+ return Promise.resolve(cancelledOpen())
1845
+ }
1809
1846
  ;(async () => {
1810
1847
  try {
1811
1848
  const res = await fetch(API + "/agent.v1.AgentService/RunSSE", {
@@ -1814,25 +1851,29 @@ function openH1(headers, signal) {
1814
1851
  body: frame(pb().str(1, id).done()),
1815
1852
  signal: ac.signal,
1816
1853
  })
1854
+ if (ended || ac.signal.aborted) { await res.body?.cancel().catch(() => {}); return }
1817
1855
  if (!res.ok) return failed(res)
1818
- for await (const c of res.body) {
1819
- if (ended || req.destroyed) return
1820
- req.push(Buffer.from(c))
1856
+ reader = res.body.getReader()
1857
+ try {
1858
+ while (!ended && !req.destroyed) {
1859
+ const { value, done } = await reader.read()
1860
+ if (done) break
1861
+ if (!ended && !req.destroyed) req.push(Buffer.from(value))
1862
+ }
1863
+ } finally {
1864
+ reader.releaseLock()
1865
+ reader = undefined
1821
1866
  }
1822
1867
  if (!ended && !req.destroyed) {
1823
1868
  ended = true
1824
1869
  req.push(null)
1870
+ cleanup()
1825
1871
  }
1826
1872
  } catch (e) {
1827
1873
  if (!ac.signal.aborted) end(JSON.stringify({ error: { code: "unavailable", message: `RunSSE: ${e?.message ?? e}` } }))
1828
1874
  }
1829
1875
  })()
1830
- const close = () => {
1831
- ended = true
1832
- ac.abort()
1833
- req.destroy()
1834
- }
1835
- return Promise.resolve({ session: { close, destroy: close }, req: Object.assign(req, { close }), status: 200, h1: true })
1876
+ return Promise.resolve(signal?.aborted ? cancelledOpen() : { session: { close, destroy: close }, req: Object.assign(req, { close }), status: 200, h1: true })
1836
1877
  }
1837
1878
 
1838
1879
  // runOnce is one Run of the chat on the agent API at base: an async
@@ -1865,14 +1906,15 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
1865
1906
  if (o.error) return book.done(), o
1866
1907
  const { session, req } = o
1867
1908
  let closed = false
1909
+ let heartbeat
1868
1910
  const send = (msg) => {
1869
1911
  if (!closed && !req.destroyed && req.writable) req.write(frame(msg))
1870
1912
  }
1871
- const heartbeat = setInterval(() => send(pb().bytes(7, EMPTY).done()), 5000) // the client is still there, as the CLI says
1872
1913
  const close = () => {
1873
1914
  if (closed) return
1874
1915
  closed = true
1875
1916
  clearInterval(heartbeat)
1917
+ signal?.removeEventListener("abort", close)
1876
1918
  try {
1877
1919
  req.close(http2.constants.NGHTTP2_CANCEL)
1878
1920
  } catch {}
@@ -1880,6 +1922,13 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
1880
1922
  setTimeout(() => session.destroy(), 1000).unref?.()
1881
1923
  }
1882
1924
  signal?.addEventListener("abort", close, { once: true })
1925
+ // Abort can win after open's head check but before this Run takes ownership.
1926
+ if (signal?.aborted) {
1927
+ close()
1928
+ book.done()
1929
+ return cancelledOpen()
1930
+ }
1931
+ heartbeat = setInterval(() => send(pb().bytes(7, EMPTY).done()), 5000) // the client is still there, as the CLI says
1883
1932
  send(first)
1884
1933
  if (o.status < 200 || o.status > 299) {
1885
1934
  const chunks = []
@@ -2397,4 +2446,4 @@ export async function CursorAuthPlugin() {
2397
2446
  }
2398
2447
 
2399
2448
  // for tests
2400
- 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 }
2449
+ export const _internal = { onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, 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.12",
3
+ "version": "0.2.13",
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",