@magpie-community/opencode-cursor-auth 0.2.5 → 0.2.7

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 +12 -1
  2. package/index.mjs +80 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,11 @@ plugin's `fetch` answers them on Cursor's agent API:
63
63
  so subagents under one session are conversations of their own. With
64
64
  nothing naming the session, each Run has a new one.
65
65
  - Text, thinking (`reasoning_content`) and token usage come back streamed
66
- or not. Cursor's `input_tokens` counts the cached prompt too: the cache
66
+ or not. Replayed assistant `reasoning_content` (or `reasoning`) is kept as
67
+ AI SDK reasoning parts in the next Run's transcript, including when a
68
+ caller splits parallel tool calls across assistant messages. It stays
69
+ separate from reply text; no thinking signature is invented.
70
+ Cursor's `input_tokens` counts the cached prompt too: the cache
67
71
  read and written is taken out of it, and given as `cached_tokens` and
68
72
  `cache_write_tokens`, so it is counted once; its reasoning tokens are
69
73
  `reasoning_tokens`. A step that calls tools gets no usage in its Run
@@ -131,6 +135,13 @@ Accounts with a Grok Bot allowance also show its usage and reset time;
131
135
  active trials are labelled separately. A spent Bot allowance doesn't stop
132
136
  Cursor requests.
133
137
 
138
+ An account that may spend on-demand once its included usage is gone (a
139
+ team or personal spend limit, as `cursor-agent`'s usage view shows it) also
140
+ shows On-demand: its spend, and its limit when it has one. Cursor keeps
141
+ serving such an account past the included usage, so magpie doesn't count
142
+ it used up when Cursor Models or Other Models reach 100%; it does when the
143
+ on-demand spend reaches its limit.
144
+
134
145
  Sign in to the Cursor account linked in Grok Bot. A linked SuperGrok
135
146
  subscription grants Bot access without a paid Cursor plan; see
136
147
  [Grok Bot plans and billing](https://cursor.com/help/grok-bot/plans).
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 fetch(API + "/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
388
- method: "POST",
389
- headers: { Authorization: `Bearer ${tok}`, "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
390
- body: "{}",
391
- signal: AbortSignal.timeout(15_000),
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,11 +463,20 @@ 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
  }
@@ -1021,6 +1081,10 @@ function statusOf(status, code, msg) {
1021
1081
  const textOf = (c) =>
1022
1082
  typeof c === "string" ? c : Array.isArray(c) ? c.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("") : ""
1023
1083
 
1084
+ // Chat clients replay thinking beside content. Cursor's transcript blobs
1085
+ // use AI SDK reasoning parts; putting it in text would change its role.
1086
+ const reasoningOf = (m) => [m.reasoning_content, m.reasoning].find((s) => typeof s === "string" && s.trim()) ?? ""
1087
+
1024
1088
  // joinSplitCalls joins an assistant message onto the assistant message
1025
1089
  // before it when that one made tool calls: Anthropic takes consecutive
1026
1090
  // assistant messages as one turn, and an agent can send a turn's parallel
@@ -1033,7 +1097,8 @@ function joinSplitCalls(msgs) {
1033
1097
  const prev = out.at(-1)
1034
1098
  if (m?.role === "assistant" && prev?.role === "assistant" && prev.tool_calls?.length) {
1035
1099
  const content = [textOf(prev.content), textOf(m.content)].filter(Boolean).join("\n\n")
1036
- out[out.length - 1] = { ...prev, content, tool_calls: [...prev.tool_calls, ...(m.tool_calls ?? [])] }
1100
+ const reasoning_content = [reasoningOf(prev), reasoningOf(m)].filter(Boolean).join("\n\n")
1101
+ out[out.length - 1] = { ...prev, content, reasoning_content, tool_calls: [...prev.tool_calls, ...(m.tool_calls ?? [])] }
1037
1102
  continue
1038
1103
  }
1039
1104
  out.push(m)
@@ -1138,9 +1203,11 @@ function conversation(chat, tools) {
1138
1203
  if (m.role === "assistant") {
1139
1204
  answer()
1140
1205
  const content = []
1206
+ const reasoning = reasoningOf(m)
1207
+ if (reasoning) content.push({ type: "reasoning", text: reasoning })
1141
1208
  const text = textOf(m.content)
1142
1209
  if (text) content.push({ type: "text", text })
1143
- size += text.length
1210
+ size += reasoning.length + text.length
1144
1211
  for (const c of m.tool_calls ?? []) {
1145
1212
  if (c?.type && c.type !== "function") continue
1146
1213
  const args = parseArgs(c.function?.arguments)
@@ -2197,7 +2264,7 @@ export async function CursorAuthPlugin() {
2197
2264
  }
2198
2265
  const [u, plan, bot] = await Promise.all([usage(tok, Object.keys(provider?.models ?? {})), planOf(tok), botUsage(tok)])
2199
2266
  // Put Bot before Total for magpie's three menu-bar rings.
2200
- const windows = [...u.windows.filter((w) => !w.aside), ...bot, ...u.windows.filter((w) => w.aside)]
2267
+ const windows = [...u.windows.filter((w) => w.name !== "Total"), ...bot, ...u.windows.filter((w) => w.name === "Total")]
2201
2268
  return { ...u, windows, ...(plan ? { plan } : {}), signIn: "kept" }
2202
2269
  },
2203
2270
  },
@@ -2240,4 +2307,4 @@ export async function CursorAuthPlugin() {
2240
2307
  }
2241
2308
 
2242
2309
  // for tests
2243
- 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 }
2310
+ 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.5",
3
+ "version": "0.2.7",
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",