@magpie-community/opencode-workbuddy-auth 0.2.10 → 0.2.12

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 +20 -3
  2. package/index.mjs +134 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -344,13 +344,24 @@ client reads. The daily Free Plan slice isn't counted: it ends at the same
344
344
  midnight on every account. magpie's smart routing spends the account whose
345
345
  credits run out first.
346
346
 
347
+ The plan's own credits and the rest are two windows. "Credits" is the
348
+ packages whose credits come back each cycle, as WorkBuddy's client lists
349
+ them (Pro monthly or yearly, youth, advanced, flagship, the plan's bonus,
350
+ the free plan's monthly base, the Pro trials, or the subscription the
351
+ summary names), with the plan's cycle, so its pace line is the month's.
352
+ "Extra credits" is the packages that only lapse: gifted and activity
353
+ credits (赠送积分) and bought credit packs. Only one of the two stops the
354
+ account, since WorkBuddy serves it while any credits are left: the extra
355
+ window is set aside until the plan's credits are spent. A summary with
356
+ only one kind keeps the one "Credits" window (akic404 on Discord).
357
+
347
358
  An enterprise (team) account's resource summary lists no packages. Its
348
359
  credits are read from the enterprise meter
349
360
  (`/billing/meter/get-enterprise-user-usage`), the one WorkBuddy's own usage
350
361
  page reads: the cycle's credits used of its limit, and when the cycle
351
362
  resets (magpie #1904).
352
363
 
353
- ## The free model's daily window (WorkBuddy AI)
364
+ ## The free model's daily window (both builds)
354
365
 
355
366
  The credits above are not the only limit. `deepseek-v4.1-flash` is served at
356
367
  `x0.00` and costs the plan nothing, yet WorkBuddy AI refuses it long before
@@ -420,8 +431,14 @@ What is measured, and what is easy to read wrongly:
420
431
  rather than +08:00 guessed.
421
432
 
422
433
  A window that has lapsed reports nothing rather than 0%: nothing is
423
- exhausted, and the next request opens a fresh one. The China build is not
424
- covered; its limit has not been measured.
434
+ exhausted, and the next request opens a fresh one. The China build
435
+ (`copilot.tencent.com`) carries the same window: two Free-plan accounts
436
+ refused on 2026-11-02 at 200,389,043 and 200,485,923 tokens, so its cap is
437
+ measured at about 2e8 — and its refusals name the reset in Chinese
438
+ (`将在 … UTC+8 重置`), which reads as an edge the same way. Its card stops
439
+ at a **soft cap of 1.9e8**, 5% short of the measured one, so routing rests
440
+ the model before the vendor refuses it; the headroom absorbs the account's
441
+ spending on other machines, which this ledger cannot see.
425
442
 
426
443
  ## When free models work but a credit model does not
427
444
 
package/index.mjs CHANGED
@@ -2210,8 +2210,9 @@ function capacity(v) {
2210
2210
  // compact is a count as magpie says one: whole, else to two places.
2211
2211
  const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""))
2212
2212
 
2213
- // The window WorkBuddy AI rate limits its free models by, which its credit
2214
- // meter does not show.
2213
+ // The window WorkBuddy rate limits deepseek-v4.1-flash by, which its credit
2214
+ // meter does not show: WorkBuddy AI measured at about 1e8 over ten days, the
2215
+ // China build at about 2e8 on 2026-11-02 (see FREE_WINDOW_CN).
2215
2216
  //
2216
2217
  // A free account's usage() reports only the credits window, so a plan with
2217
2218
  // 160 credits untouched reads 0% while chats to deepseek-v4.1-flash are
@@ -2223,6 +2224,11 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
2223
2224
  // reset at 2026-10-20 19:47:41 UTC+8, alternatively, you can switch to
2224
2225
  // the other models to continue using it.
2225
2226
  //
2227
+ // The China build (copilot.tencent.com) says the same in its own words:
2228
+ //
2229
+ // 429 WorkBuddy: 您的使用量已超出频率限制,将在 2026-11-02 19:17:02
2230
+ // UTC+8 重置,您也可以切换其他模型继续使用。
2231
+ //
2226
2232
  // Measured on a free plan over ten days (one account, deepseek-v4.1-flash
2227
2233
  // only), counting in + cache_read + out:
2228
2234
  //
@@ -2244,6 +2250,12 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
2244
2250
  // were seen between a reset and the next request, each opening the new
2245
2251
  // window.
2246
2252
  //
2253
+ // The China build was measured after that, on 2026-11-02: two Free-plan
2254
+ // accounts on copilot.tencent.com, both spending the machine's ledger only
2255
+ // on this model, were refused at 200,389,043 and 200,485,923 tokens counted
2256
+ // the same way. Its cap sits at about 2e8 — and its refusals name the reset
2257
+ // in Chinese (将在 … UTC+8 重置), which windowReset reads as well.
2258
+ //
2247
2259
  // deepseek-v4.1-flash-sg is the same model under its paid id (the product
2248
2260
  // config's ModelRateLimitCap.lines[0] names it as paidId to this freeId,
2249
2261
  // allowPaidSwitch: true) and draws on the same window: it was refused in
@@ -2270,7 +2282,18 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
2270
2282
  // ~/.config/magpie otherwise. No endpoint reports the window; a ledger that
2271
2283
  // cannot be read yields no window rather than a made-up one.
2272
2284
 
2273
- const FREE_WINDOW_CAP = 1e8
2285
+ // A window: the build's ledger rows it counts (provider), the cap it reports
2286
+ // against, and how deep its display rounds. Two builds, two windows, two
2287
+ // caps: the same phone number can be signed in to both, and their ledgers
2288
+ // stay apart — one build's spending must not fill the other's window.
2289
+ //
2290
+ // The China cap is a soft one. Measured at about 2e8 (two accounts refusing
2291
+ // at 200.39M and 200.49M), the card stops the model at 1.9e8: 5% of headroom
2292
+ // for the account's spending on other machines (which this ledger cannot
2293
+ // see), so routing rests the model before the vendor's refusal does. A
2294
+ // refusal in force still reads the window full whatever the sum says.
2295
+ const FREE_WINDOW_CN = { provider: "workbuddy", cap: 1.9e8, short: "190M" }
2296
+ const FREE_WINDOW_AI = { provider: "workbuddy-ai", cap: 1e8, short: "100M" }
2274
2297
  const FREE_WINDOW_SPAN = 24 * 3600 * 1000
2275
2298
  // The lead: how far back of an edge the read goes on. A chain read from a row
2276
2299
  // that opens a window needs the window the span makes of the rows above it,
@@ -2287,8 +2310,6 @@ const FREE_WINDOW_GAP = FREE_WINDOW_SPAN
2287
2310
  const FREE_WINDOW_MODEL = "deepseek-v4.1-flash"
2288
2311
  // both ids, so magpie's exact match finds the -sg one too
2289
2312
  const FREE_WINDOW_MODELS = ["deepseek-v4.1-flash", "deepseek-v4.1-flash-sg"]
2290
- // the provider whose rows count
2291
- const FREE_WINDOW_PROVIDER = "workbuddy-ai"
2292
2313
 
2293
2314
  // windowOffset is the UTC offset a refusal spells out, in minutes, from
2294
2315
  // "UTC+8", "UTC+08:00", "UTC-5" or "GMT+8". null when it spells out none,
@@ -2304,10 +2325,15 @@ function windowOffset(text) {
2304
2325
  }
2305
2326
 
2306
2327
  // windowReset is the instant a refusal names, or NaN when it names none or
2307
- // names it in an offset that cannot be read.
2328
+ // names it in an offset that cannot be read. Both builds name it — WorkBuddy
2329
+ // AI in its English "will reset at …", the China build in its Chinese
2330
+ // "将在 … 重置" — with the time in the same shape, so the two patterns share
2331
+ // the group numbering and the offset below reads the "UTC+8" either way.
2308
2332
  function windowReset(text) {
2309
2333
  const s = String(text ?? "")
2310
- const at = /reset at (\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(s)
2334
+ const at =
2335
+ /reset at (\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(s) ||
2336
+ /将在\s*(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(s)
2311
2337
  if (!at) return NaN
2312
2338
  const off = windowOffset(s)
2313
2339
  if (off === null) return NaN
@@ -2316,17 +2342,18 @@ function windowReset(text) {
2316
2342
 
2317
2343
  // freeWindowOf is the window account is in now, from rows (magpie's usage
2318
2344
  // records), or null when it has lapsed — the next request opens a new one.
2319
- // now is passed in so the reading is testable.
2345
+ // now is passed in so the reading is testable; win is the build's window
2346
+ // (which build's rows count, what cap they fill), AI by default.
2320
2347
  //
2321
2348
  // The window is a chain: it begins at the first request answered after the
2322
2349
  // previous one ended, so where this window began depends on every window
2323
2350
  // before it. A window ends at a refusal's reset when the ledger holds one,
2324
2351
  // and otherwise a day after it began. readLedgerTail (which see) stops on
2325
2352
  // both, so that these rows read the same as the whole ledger.
2326
- function freeWindowOf(rows, account, now = Date.now()) {
2353
+ function freeWindowOf(rows, account, now = Date.now(), win = FREE_WINDOW_AI) {
2327
2354
  const mine = (Array.isArray(rows) ? rows : []).filter(
2328
2355
  (r) =>
2329
- r?.provider === FREE_WINDOW_PROVIDER &&
2356
+ r?.provider === win.provider &&
2330
2357
  r.providerAccount === account &&
2331
2358
  typeof r.model === "string" &&
2332
2359
  r.model.startsWith(FREE_WINDOW_MODEL) &&
@@ -2403,11 +2430,11 @@ function freeWindowOf(rows, account, now = Date.now()) {
2403
2430
 
2404
2431
  const card = (amount, resetsAt, refused) => ({
2405
2432
  name: "Free model tokens",
2406
- used: Math.min(100, (100 * amount) / FREE_WINDOW_CAP),
2433
+ used: Math.min(100, (100 * amount) / win.cap),
2407
2434
  amount,
2408
- limit: FREE_WINDOW_CAP,
2435
+ limit: win.cap,
2409
2436
  unit: "tokens",
2410
- display: `${tokensShort(amount)} / 100M`,
2437
+ display: `${tokensShort(amount)} / ${win.short}`,
2411
2438
  resetsAt: new Date(resetsAt).toISOString(),
2412
2439
  span: FREE_WINDOW_SPAN / 1000,
2413
2440
  models: FREE_WINDOW_MODELS.slice(),
@@ -2425,7 +2452,7 @@ function freeWindowOf(rows, account, now = Date.now()) {
2425
2452
  if (!Number.isFinite(t) || t <= now || t <= start) continue
2426
2453
  if (live === null || t > live) live = t
2427
2454
  }
2428
- if (live !== null) return card(FREE_WINDOW_CAP, live, true)
2455
+ if (live !== null) return card(win.cap, live, true)
2429
2456
 
2430
2457
  return card(used, end, false)
2431
2458
  }
@@ -2493,7 +2520,7 @@ function freeWindowLedger(cwd = process.cwd(), env = process.env, home = homedir
2493
2520
  // ledger mid-write, the one row holding the free window's start was
2494
2521
  // dropped without a word, and the panel showed no window at all until the
2495
2522
  // next read happened to succeed.
2496
- function freeWindowRows(account, now = Date.now(), cwd = process.cwd(), env = process.env, home = homedir()) {
2523
+ function freeWindowRows(account, now = Date.now(), cwd = process.cwd(), env = process.env, home = homedir(), win = FREE_WINDOW_AI) {
2497
2524
  if (!account) return null
2498
2525
  let found = null
2499
2526
  for (const path of freeWindowLedger(cwd, env, home)) {
@@ -2510,11 +2537,14 @@ function freeWindowRows(account, now = Date.now(), cwd = process.cwd(), env = pr
2510
2537
  // way this ledger is written; one rewritten in place (a rotation or a
2511
2538
  // compaction) moves mtime even when its size lands the same.
2512
2539
  const file = path + "|" + st.mtimeMs + ":" + st.size
2513
- const key = file + "|" + account
2540
+ // the key carries the account and the build: one account's rows are its
2541
+ // own, and the two builds' chains over one ledger must not answer for
2542
+ // each other (the same phone number can be signed in to both)
2543
+ const key = file + "|" + win.provider + "|" + account
2514
2544
  if (ledgerParsed?.key === key) return ledgerParsed.rows
2515
2545
  let got
2516
2546
  try {
2517
- got = readLedgerTail(path, st, now, undefined, account)
2547
+ got = readLedgerTail(path, st, now, undefined, account, win)
2518
2548
  } catch {
2519
2549
  return null
2520
2550
  }
@@ -2574,11 +2604,12 @@ function rowAccount(line) {
2574
2604
  }
2575
2605
 
2576
2606
  // ofAccount is whether a row belongs to the chain read for account: this
2577
- // account's own row of the free model, either id. A refusal of another
2578
- // account's, or of a paid model, says nothing about where this chain opens.
2579
- function ofAccount(row, account) {
2607
+ // account's own row of the build's free model, either id. A refusal of
2608
+ // another account's, of the other build, or of a paid model says nothing
2609
+ // about where this chain opens.
2610
+ function ofAccount(row, account, win = FREE_WINDOW_AI) {
2580
2611
  return (
2581
- row?.provider === FREE_WINDOW_PROVIDER &&
2612
+ row?.provider === win.provider &&
2582
2613
  row.providerAccount === account &&
2583
2614
  typeof row.model === "string" &&
2584
2615
  row.model.startsWith(FREE_WINDOW_MODEL)
@@ -2625,7 +2656,7 @@ function ofAccount(row, account) {
2625
2656
  // plugin_accounts.go). The label then no longer equals the accountId rows are
2626
2657
  // written under, no row is found for this account at all, and the card
2627
2658
  // reports no free window.
2628
- function readLedgerTail(path, st, now, chunks, account) {
2659
+ function readLedgerTail(path, st, now, chunks, account, win = FREE_WINDOW_AI) {
2629
2660
  const CHUNK = 1 << 16
2630
2661
  const lapsed = now - FREE_WINDOW_SPAN // a read past this holds no window now
2631
2662
  const fd = openSync(path, "r")
@@ -2684,7 +2715,7 @@ function readLedgerTail(path, st, now, chunks, account) {
2684
2715
  if (!(firstPass && tornEnd && i === lines.length - 1)) bad++
2685
2716
  continue
2686
2717
  }
2687
- if (!ofAccount(row, account)) continue
2718
+ if (!ofAccount(row, account, win)) continue
2688
2719
  rev.push(row)
2689
2720
  chain.push(row)
2690
2721
  if (Number(row.status) === 200) answered = true
@@ -2699,7 +2730,7 @@ function readLedgerTail(path, st, now, chunks, account) {
2699
2730
  if (!reach || !Number.isFinite(t) || t >= reach) {
2700
2731
  try {
2701
2732
  const row = JSON.parse(carry)
2702
- if (ofAccount(row, account)) rev.push(row)
2733
+ if (ofAccount(row, account, win)) rev.push(row)
2703
2734
  } catch {
2704
2735
  bad++
2705
2736
  }
@@ -2776,20 +2807,49 @@ function pinned(rows, account) {
2776
2807
  // CycleRemainCapacity at all leaves the window out. TotalCredits is not a
2777
2808
  // fallback: an account with no packages at all keeps its unknown allowance,
2778
2809
  // as before, so its free models are not skipped.
2779
- function usageOf(sum, plan) {
2810
+ //
2811
+ // The plan's own credits and the rest are two windows (akic404 on
2812
+ // Discord): summed, the gifted and activity packages' credits used long
2813
+ // ago put the card at most of its allowance on the first day of the
2814
+ // month, and the plan's pace line said nothing. "Credits" is the packages
2815
+ // whose credits come back each cycle — WorkBuddy's client's own
2816
+ // onlyRefreshCodes (PLAN_CODES), or the subscription the summary names —
2817
+ // and "Extra credits" the ones that only lapse (gift, activity, bought
2818
+ // credit packs, any code the client doesn't list). A summary with only
2819
+ // one kind keeps the one "Credits" window, as before.
2820
+ //
2821
+ // Only one of the two stops the account at a time: WorkBuddy serves it
2822
+ // while any credits are left, so the one set aside is the one whose
2823
+ // running out doesn't stop it — "Extra credits", until the plan's are
2824
+ // spent and extra ones are left.
2825
+ function usageOf(sum, plan, site) {
2780
2826
  const out = { plan: plan || (sum?.IsPaidUser ? "Pro" : "Free plan"), windows: [] }
2781
- let total = 0, used = 0, left = 0, bags = 0
2827
+ const ofPlan = planCodes(sum, site)
2828
+ const kinds = { plan: { total: 0, used: 0 }, extra: { total: 0, used: 0 } }
2829
+ let left = 0, bags = 0
2782
2830
  for (const p of sum?.Packages ?? []) {
2783
2831
  const hasTotal = p?.CycleTotalCapacity != null && p.CycleTotalCapacity !== ""
2784
2832
  if (hasTotal) {
2785
- total += capacity(p.CycleTotalCapacity)
2786
- used += capacity(p?.CycleUsedCapacity)
2833
+ const k = kinds[ofPlan(p?.PackageCode) ? "plan" : "extra"]
2834
+ k.total += capacity(p.CycleTotalCapacity)
2835
+ k.used += capacity(p?.CycleUsedCapacity)
2787
2836
  } else if (p?.CycleRemainCapacity != null && p.CycleRemainCapacity !== "") {
2788
2837
  left += capacity(p.CycleRemainCapacity)
2789
2838
  bags++
2790
2839
  }
2791
2840
  }
2792
- if (total > 0) out.windows.push({ name: "Credits", used: (100 * used) / total, display: `${compact(used)} / ${compact(total)}`, amount: used, limit: total, unit: "credits" })
2841
+ const window = (name, { used, total }) => ({ name, used: (100 * used) / total, display: `${compact(used)} / ${compact(total)}`, amount: used, limit: total, unit: "credits" })
2842
+ const { plan: mine, extra } = kinds
2843
+ if (mine.total > 0 && extra.total > 0) {
2844
+ const credits = window("Credits", mine), more = window("Extra credits", extra)
2845
+ if (mine.used >= mine.total && extra.used < extra.total) credits.aside = true
2846
+ else more.aside = true
2847
+ out.windows.push(credits, more)
2848
+ } else {
2849
+ const total = mine.total + extra.total, used = mine.used + extra.used
2850
+ if (total > 0) out.windows.push(window("Credits", { used, total }))
2851
+ }
2852
+ const total = mine.total + extra.total
2793
2853
  if (bags > 0 && (total === 0 || left > 0)) out.windows.push({ name: "Prepaid credits", balance: left, display: `${compact(left)} credits`, unit: "credits" })
2794
2854
  return out
2795
2855
  }
@@ -2845,6 +2905,26 @@ const FREE_CODES = [DAILY_CODE, CODE("008", "cfWoLwvjU4"), CODE("035", "ArVxJcGD
2845
2905
  // lost at the same midnight on every account, so they tell none apart
2846
2906
  const SLICE = "4"
2847
2907
 
2908
+ // PLAN_CODES are the packages whose credits come back each cycle, as
2909
+ // WorkBuddy's client (5.9.x) lists them (onlyRefreshCodes): Pro monthly,
2910
+ // Pro monthly plus, Pro yearly, youth, advanced, flagship, the plan's
2911
+ // bonus (bonus28; bonusIntl, "Plan Bonus Quota"), the international free
2912
+ // plan (freeMonIntl), the Pro trials, and on the China build alone the
2913
+ // free plan's monthly base (freeMon, 套餐基础积分). Every other code only
2914
+ // lapses: gift (006, "Bonus Pack" / Pro trial), activity (007, 赠送积分),
2915
+ // bonus29/30, and the credit packs (009, 036, 038).
2916
+ const PLAN_CODES = [CODE("002", "AkiJS3ZHF5"), CODE("005", "maRGyrHhw1"), CODE("003", "FAnt7lcmRT"), CODE("023", "4xbGhMrE6q"),
2917
+ CODE("026", "BaESVICNoi"), CODE("027", "0FCGVA6vSa"), CODE("028", "NtpWi0jzXs"), CODE("035", "ArVxJcGDsm"),
2918
+ CODE("039", "KRcQj7wUat"), CODE("040", "mi9rCYg46x"), CODE("037", "WxOD3MpI2o")]
2919
+ const FREE_MON = CODE("008", "cfWoLwvjU4")
2920
+
2921
+ // planCodes says whether a package of the summary is the plan's own: one
2922
+ // of PLAN_CODES on that site, or the subscription the summary names.
2923
+ function planCodes(sum, site) {
2924
+ const sub = sum?.SubscriptionPackageCode
2925
+ return (code) => !!code && (code === sub || PLAN_CODES.includes(code) || (!site?.ai && code === FREE_MON))
2926
+ }
2927
+
2848
2928
  // beijing is WorkBuddy's own time ("2026-10-13 00:39:27" is UTC+8, as its
2849
2929
  // DeductionEndTime for the same package says).
2850
2930
  const beijing = (ms) => new Date(ms + 8 * 3600_000).toISOString().slice(0, 19).replace("T", " ")
@@ -2887,11 +2967,12 @@ function endOf(p) {
2887
2967
  // packages are lost, and how many (cdredfox on X): smart routing spends
2888
2968
  // the account whose credits expire first. Only packages the summary
2889
2969
  // counts, with credits left, and not the daily slice; null when none.
2890
- function runsOutOf(sum, packages, now = Date.now()) {
2970
+ // only narrows it to the packages of one window (the split's, usageOf).
2971
+ function runsOutOf(sum, packages, now = Date.now(), only = () => true) {
2891
2972
  const counted = new Set((sum?.Packages ?? []).map((p) => p?.PackageCode))
2892
2973
  let at = 0, amount = 0
2893
2974
  for (const p of packages ?? []) {
2894
- if (!counted.has(p?.PackageCode) || p?.PackageCode === DAILY_CODE || String(p?.CapacityType) === SLICE) continue
2975
+ if (!counted.has(p?.PackageCode) || !only(p?.PackageCode) || p?.PackageCode === DAILY_CODE || String(p?.CapacityType) === SLICE) continue
2895
2976
  const left = capacity(p?.CycleCapacityRemainPrecise ?? p?.CycleCapacityRemain)
2896
2977
  const end = endOf(p)
2897
2978
  if (!(left > 0) || end <= now) continue
@@ -3626,7 +3707,7 @@ function makePlugin(site) {
3626
3707
  }
3627
3708
  said = "renewed"
3628
3709
  }
3629
- const out = usageOf(sum, auth.plan)
3710
+ const out = usageOf(sum, auth.plan, site)
3630
3711
  // an enterprise account's summary has no packages: its credits
3631
3712
  // are the enterprise meter's (#1904). A personal account's
3632
3713
  // refusal of it (10001 "uid or enterpriseID is empty") leaves
@@ -3641,22 +3722,36 @@ function makePlugin(site) {
3641
3722
  // when the plan's credits start again (cycleOf), and when the credits
3642
3723
  // left run out, for routing to spend them first (runsOutOf);
3643
3724
  // a list that can't be read leaves it unknown, the card stands
3725
+ //
3726
+ // Split (usageOf), the plan's cycle is the plan window's alone;
3727
+ // the soonest credits to lapse, of any package, stay on the window
3728
+ // that stops the account, as routing reads none set aside, and
3729
+ // the window set aside says its own.
3644
3730
  const credits = out.windows.find((w) => w.name === "Credits")
3731
+ const extra = out.windows.find((w) => w.name === "Extra credits")
3645
3732
  if (credits) {
3646
3733
  try {
3647
3734
  const packages = await packagesOf(site, a)
3648
- Object.assign(credits, cycleOf(sum, packages) ?? {}, runsOutOf(sum, packages) ?? {})
3735
+ Object.assign(credits, cycleOf(sum, packages) ?? {})
3736
+ const ofPlan = planCodes(sum, site)
3737
+ const stops = extra && !extra.aside ? extra : credits
3738
+ Object.assign(stops, runsOutOf(sum, packages) ?? {})
3739
+ const aside = extra ? (stops === credits ? extra : credits) : null
3740
+ if (aside) Object.assign(aside, runsOutOf(sum, packages, Date.now(), (code) => ofPlan(code) === (aside === credits)) ?? {})
3649
3741
  } catch {}
3650
3742
  }
3651
3743
  await spareFree(site, a, out.windows)
3652
3744
  // the free model's own window, which the credit meter above does
3653
3745
  // not show: what actually refuses a chat long before 160 credits
3654
- // are spent. WorkBuddy AI alone; the China build has no such cap
3655
- // measured (see freeWindowOf)
3656
- if (site.ai) {
3746
+ // are spent. Both builds: WorkBuddy AI at 1e8 (ten days of
3747
+ // refusals), the China build's refusals measured at about 2e8 on
3748
+ // 2026-11-02, its card stopping at the 1.9e8 soft cap (see the
3749
+ // window definitions above)
3750
+ {
3657
3751
  const account = a.accountId || a.user || a.uid || ""
3658
- const rows = freeWindowRows(account)
3659
- const w = rows && freeWindowOf(rows, account)
3752
+ const win = site.ai ? FREE_WINDOW_AI : FREE_WINDOW_CN
3753
+ const rows = freeWindowRows(account, Date.now(), process.cwd(), process.env, homedir(), win)
3754
+ const w = rows && freeWindowOf(rows, account, Date.now(), win)
3660
3755
  if (w) out.windows.push(w)
3661
3756
  }
3662
3757
  return { ...out, signIn: said }
@@ -3801,4 +3896,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
3801
3896
  export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
3802
3897
 
3803
3898
  // for tests
3804
- export const _internal = { idFor, OFF_TAKEN, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
3899
+ export const _internal = { planCodes, idFor, OFF_TAKEN, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_CN, FREE_WINDOW_AI, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-workbuddy-auth",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",