@goodandready/dsh-subscriptions 0.6.51 → 0.6.53

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/lib/accounts.js CHANGED
@@ -13,10 +13,9 @@ function loadQuarantinesFromFile(map) {
13
13
  const p = resolveQuarantineFilePath()
14
14
  const content = readFileSync(p, 'utf8')
15
15
  const json = JSON.parse(content)
16
- const now = Date.now()
17
16
  if (json && typeof json === 'object') {
18
17
  for (const [k, v] of Object.entries(json)) {
19
- if (v && Number(v.until) > now) {
18
+ if (v && typeof v === 'object' && v.until) {
20
19
  map.set(k, v)
21
20
  }
22
21
  }
@@ -29,9 +28,8 @@ function saveQuarantinesToFile(map) {
29
28
  const p = resolveQuarantineFilePath()
30
29
  mkdirSync(join(p, '..'), { recursive: true })
31
30
  const obj = {}
32
- const now = Date.now()
33
31
  for (const [k, v] of map.entries()) {
34
- if (v && Number(v.until) > now) {
32
+ if (v && typeof v === 'object' && v.until) {
35
33
  obj[k] = v
36
34
  }
37
35
  }
@@ -452,12 +450,25 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
452
450
  },
453
451
  rememberQuota(ref, snap) {
454
452
  if (snap) {
453
+ const prevSnap = quotas.get(ref)
455
454
  quotas.set(ref, snap)
456
455
  if (snap.usedPercent != null) {
457
456
  const now = snap.measuredAt || Date.now()
458
- const samples = quotaSamples.get(ref) || []
457
+ let samples = (quotaSamples.get(ref) || []).slice()
459
458
  const remainingPercent = snap.remainingPercent ?? (100 - Number(snap.usedPercent))
460
- samples.push({ at: now, pct: remainingPercent })
459
+ const lastSample = samples[samples.length - 1]
460
+
461
+ const prevReset = prevSnap?.resetAt ?? lastSample?.resetAt ?? null
462
+ const currReset = snap.resetAt ?? null
463
+ const resetChanged = (prevReset !== null && currReset !== null && Math.abs(Number(prevReset) - Number(currReset)) > 300) ||
464
+ (prevSnap !== undefined && (prevReset === null) !== (currReset === null))
465
+ const increased = lastSample !== undefined && remainingPercent > lastSample.pct + 0.5
466
+
467
+ if (resetChanged || increased) {
468
+ samples = []
469
+ }
470
+
471
+ samples.push({ at: now, pct: remainingPercent, resetAt: currReset })
461
472
  while (samples.length > 20) samples.shift()
462
473
  quotaSamples.set(ref, samples)
463
474
  snap.samples = [...samples]
@@ -483,12 +494,14 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
483
494
  saveQuarantinesToFile(quarantines)
484
495
  if (health.has(ref)) health.set(ref, null)
485
496
  },
486
- rememberQuarantine(ref, reason, until, attempts = 1) {
497
+ rememberQuarantine(ref, reason, until, attempts) {
487
498
  if (!ref) return
499
+ const existing = quarantines.get(ref)
500
+ const finalAttempts = attempts !== undefined ? Number(attempts) : (existing?.attempts ? existing.attempts + 1 : 1)
488
501
  quarantines.set(ref, {
489
- reason: reason || 'RATE_LIMIT',
490
- until: Number(until) || (Date.now() + 3600000),
491
- attempts: Number(attempts) || 1,
502
+ reason: reason || existing?.reason || 'RATE_LIMIT',
503
+ until: Number(until) || existing?.until || (Date.now() + 3600000),
504
+ attempts: Number(finalAttempts) || 1,
492
505
  status: STATUS_QUARANTINE,
493
506
  })
494
507
  saveQuarantinesToFile(quarantines)
@@ -510,12 +523,14 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
510
523
  return { success: false, reason: 'NOT_EXPIRED' }
511
524
  }
512
525
  quarantines.set(ref, enterProbing(acc))
526
+ saveQuarantinesToFile(quarantines)
513
527
  try {
514
528
  if (typeof probeFn === 'function') {
515
529
  await probeFn(ref)
516
530
  }
517
531
  resolveWarmupSuccess(acc)
518
532
  quarantines.delete(ref)
533
+ saveQuarantinesToFile(quarantines)
519
534
  return { success: true }
520
535
  } catch (err) {
521
536
  const updated = resolveWarmupFailure(acc, err?.message || q.reason)
@@ -525,6 +540,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
525
540
  attempts: updated.quarantineAttempts,
526
541
  status: updated.status,
527
542
  })
543
+ saveQuarantinesToFile(quarantines)
528
544
  return { success: false, account: updated, error: err }
529
545
  }
530
546
  },
@@ -44,7 +44,7 @@ export function createAdapterManager({
44
44
  autoPacing: () => live().autoPacing,
45
45
  switchAtRemaining: () => live().switchAtRemaining,
46
46
  rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
47
- rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
47
+ rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
48
48
  getQuarantine: (ref) => store.getQuarantine(ref),
49
49
  probeWarmup: (ref, fn) => store.probeWarmup(ref, fn),
50
50
  rememberQuota: (ref, snap) => store.rememberQuota(ref, snap),
package/lib/adapter.js CHANGED
@@ -133,7 +133,12 @@ export class SubscriptionAdapter extends LlmAdapter {
133
133
  onCooldown: (account) => {
134
134
  if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
135
135
  if (typeof deps.rememberQuarantine === 'function') {
136
- deps.rememberQuarantine(account.ref, account.quarantineReason || 'RATE_LIMIT', account.quarantineUntil || account.cooldownUntil)
136
+ const currentQ = typeof deps.getQuarantine === 'function' ? deps.getQuarantine(account.ref) : null
137
+ if (currentQ && currentQ.attempts && currentQ.attempts > 1) {
138
+ deps.rememberQuarantine(account.ref, currentQ.reason, currentQ.until, currentQ.attempts)
139
+ } else {
140
+ deps.rememberQuarantine(account.ref, account.quarantineReason || 'RATE_LIMIT', account.quarantineUntil || account.cooldownUntil, account.quarantineAttempts || 1)
141
+ }
137
142
  }
138
143
  if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
139
144
  },
@@ -267,15 +272,21 @@ export class SubscriptionAdapter extends LlmAdapter {
267
272
  if (typeof deps.probeWarmup === "function") {
268
273
  const qUntil = Number(account.quarantineUntil || 0)
269
274
  if (qUntil > 0 && qUntil <= Date.now()) {
275
+ const probeFetch = typeof deps.fetchForRef === 'function'
276
+ ? deps.fetchForRef(account.ref)
277
+ : (typeof deps.fetchFor === 'function' ? deps.fetchFor(account.ref) : (deps.fetchImpl || fetch))
278
+ const probeCfg = typeof deps.vendorConfig === 'function' ? (deps.vendorConfig(provider) || {}) : {}
270
279
  const probeResult = await deps.probeWarmup(account.ref, async () => {
271
280
  if (vendor && typeof vendor.check === "function") {
272
281
  const probeBlob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
273
- await vendor.check(probeBlob, { config: deps.vendorConfig(provider), signal: opts && opts.signal })
282
+ await vendor.check(probeBlob, probeCfg, probeFetch)
274
283
  }
275
284
  })
276
285
  if (probeResult && !probeResult.success) {
277
286
  const err = probeResult.error || new Error("warmup probe failed")
278
287
  err.code = "QUOTA"
288
+ err.isWarmupProbeFailure = true
289
+ err.warmupAccount = probeResult.account
279
290
  throw err
280
291
  }
281
292
  }
package/lib/client.js CHANGED
@@ -2362,47 +2362,101 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2362
2362
  return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
2363
2363
  }
2364
2364
 
2365
- const PLUGIN_LABELS = new Set(['Plugins', '插件'])
2366
- function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms)) {
2365
+ const PLUGIN_LABELS = new Set(['plugins', '插件', 'plugin'])
2366
+
2367
+ function resolvePluginLabels(customCtx) {
2368
+ const labels = new Set(PLUGIN_LABELS)
2369
+ const loc = (customCtx && customCtx.locale) || (typeof window !== 'undefined' && window.__dshCtx && window.__dshCtx.locale)
2370
+ if (loc && typeof loc.bind === 'function') {
2371
+ try {
2372
+ const sp = loc.bind('settings.plugins')
2373
+ const nav = typeof sp === 'function' ? sp('nav') : null
2374
+ if (nav && typeof nav === 'string') labels.add(nav.trim().toLowerCase())
2375
+ } catch { /* bestEffort */ }
2376
+ try {
2377
+ const pm = loc.bind('pluginManager')
2378
+ const panel = typeof pm === 'function' ? (pm('panel') || pm('title')) : null
2379
+ if (panel && typeof panel === 'string') labels.add(panel.trim().toLowerCase())
2380
+ } catch { /* bestEffort */ }
2381
+ }
2382
+ return labels
2383
+ }
2384
+
2385
+ function matchesPluginLabel(el, customLabels) {
2386
+ if (!el) return false
2387
+ const dataTab = (el.getAttribute('data-tab') || '').trim().toLowerCase()
2388
+ const dataSection = (el.getAttribute('data-section') || '').trim().toLowerCase()
2389
+ const dataKey = (el.getAttribute('data-key') || '').trim().toLowerCase()
2390
+ const id = (el.getAttribute('id') || '').trim().toLowerCase()
2391
+ if (dataTab === 'plugins' || dataSection === 'plugins' || dataKey === 'plugins' || id === 'plugins') return true
2392
+
2393
+ if (el.querySelector && el.querySelector('path[d*="M10.3232"]')) return true
2394
+
2395
+ const txt = (el.textContent || '').trim().toLowerCase()
2396
+ const aria = (el.getAttribute('aria-label') || '').trim().toLowerCase()
2397
+ const title = (el.getAttribute('title') || '').trim().toLowerCase()
2398
+ const labels = customLabels || PLUGIN_LABELS
2399
+ return labels.has(txt) || labels.has(aria) || labels.has(title) || labels.has(dataTab)
2400
+ }
2401
+
2402
+ function findAndClickCard(doc) {
2403
+ for (const key of [PKG, 'dsh-subscriptions', '@goodandready/dsh-subscriptions']) {
2404
+ const card = doc.querySelector(`[data-plugin-package="${key}"]`) || doc.querySelector(`[data-plugin-item="${ROW_ID}"]`)
2405
+ if (card) {
2406
+ const open = [...card.querySelectorAll('button')].filter((b) => b.getAttribute('role') !== 'switch')
2407
+ const target = open.length >= 1 ? open[0] : card
2408
+ if (typeof target.click === 'function') {
2409
+ target.click()
2410
+ return true
2411
+ }
2412
+ }
2413
+ }
2414
+ return false
2415
+ }
2416
+
2417
+ function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms), navCtx) {
2367
2418
  try {
2368
- const buttons = [...doc.querySelectorAll('button')]
2369
- const panel = buttons.find((b) => PLUGIN_LABELS.has(b.getAttribute('aria-label') || ''))
2370
- || buttons.find((b) => PLUGIN_LABELS.has(b.textContent.trim()))
2419
+ if (findAndClickCard(doc)) return
2420
+
2421
+ const activeLabels = resolvePluginLabels(navCtx)
2422
+ const checkEl = (el) => matchesPluginLabel(el, activeLabels)
2423
+
2424
+ const buttons = [...doc.querySelectorAll('button, a, div[role="button"], div[role="tab"]')]
2425
+ const panel = buttons.find(checkEl)
2371
2426
  if (panel) {
2372
2427
  panel.click()
2373
2428
  const poll = (left) => {
2374
- try {
2375
- for (const key of [PKG, 'dsh-subscriptions', '@goodandready/dsh-subscriptions']) {
2376
- const card = doc.querySelector(`[data-plugin-package="${key}"]`) || doc.querySelector(`[data-plugin-item="${ROW_ID}"]`)
2377
- if (card) {
2378
- const open = [...card.querySelectorAll('button')].filter((b) => b.getAttribute('role') !== 'switch')
2379
- const target = open.length >= 1 ? open[0] : card
2380
- if (typeof target.click === 'function') {
2381
- target.click()
2382
- return
2383
- }
2384
- }
2385
- }
2386
- } catch {}
2429
+ if (findAndClickCard(doc)) return
2387
2430
  if (left > 0) schedule(() => poll(left - 1), 100)
2388
2431
  }
2389
- schedule(() => poll(40), 150)
2432
+ schedule(() => poll(40), 100)
2390
2433
  return
2391
2434
  }
2435
+
2392
2436
  const triggers = buttons.filter((b) => b.getAttribute('aria-haspopup') === 'dialog' && b.hasAttribute('aria-expanded'))
2393
2437
  if (triggers.length > 0) {
2394
2438
  const trigger = triggers.find((b) => b.getAttribute('aria-expanded') !== 'true') || triggers[0]
2395
2439
  if (trigger && typeof trigger.click === 'function') {
2396
2440
  trigger.click()
2397
- schedule(() => {
2441
+ const pollSection = (left) => {
2398
2442
  try {
2399
- const section = [...doc.querySelectorAll('button')].find((b) => PLUGIN_LABELS.has(b.textContent.trim()))
2400
- if (section && typeof section.click === 'function') section.click()
2401
- } catch {}
2402
- }, 80)
2443
+ const section = [...doc.querySelectorAll('button, a, div[role="tab"], [role="button"]')].find(checkEl)
2444
+ if (section && typeof section.click === 'function') {
2445
+ section.click()
2446
+ const poll = (cLeft) => {
2447
+ if (findAndClickCard(doc)) return
2448
+ if (cLeft > 0) schedule(() => poll(cLeft - 1), 100)
2449
+ }
2450
+ schedule(() => poll(40), 100)
2451
+ return
2452
+ }
2453
+ } catch { /* bestEffort */ }
2454
+ if (left > 0) schedule(() => pollSection(left - 1), 80)
2455
+ }
2456
+ schedule(() => pollSection(20), 80)
2403
2457
  }
2404
2458
  }
2405
- } catch {}
2459
+ } catch { /* bestEffort */ }
2406
2460
  }
2407
2461
  function SubsPill(props) {
2408
2462
  const t = (props && props.t) || ((k) => k)
@@ -2621,16 +2675,16 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2621
2675
  if (nav && typeof nav.openBundle === 'function') {
2622
2676
  nav.openBundle(PKG)
2623
2677
  }
2624
- } catch {}
2678
+ } catch { /* bestEffort */ }
2625
2679
 
2626
2680
  try {
2627
2681
  const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
2628
2682
  if (layout && typeof layout.selectPanel === 'function') {
2629
2683
  layout.selectPanel('plugins')
2630
2684
  }
2631
- } catch {}
2685
+ } catch { /* bestEffort */ }
2632
2686
 
2633
- openPluginSettings(document)
2687
+ openPluginSettings(document, undefined, clientCtx)
2634
2688
  },
2635
2689
  }, t('subsOpenSettings')),
2636
2690
  ),
@@ -2734,6 +2788,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2734
2788
 
2735
2789
  exports.inject = ['slots', 'locale', 'sessions']
2736
2790
  exports.apply = function apply(ctx) {
2791
+ if (typeof window !== 'undefined') {
2792
+ window.__dshCtx = ctx
2793
+ }
2737
2794
  setT(ctx.locale.bind(NS))
2738
2795
  try {
2739
2796
  ctx.inject(['pluginNavigation'], (navCtx) => {
package/lib/forecast.js CHANGED
@@ -73,8 +73,22 @@ export function estimateForecast(state, key, remainingPercent, resetsAt, now) {
73
73
  // #352: Burn-rate calculation & Pacing risk estimator
74
74
  export function calculateBurnRatePerHour(samples, now = Date.now()) {
75
75
  if (!Array.isArray(samples) || samples.length < 2) return 0
76
- const recent = samples.filter((s) => s.at >= now - 6 * HOUR_MS)
76
+ let recent = samples.filter((s) => s.at >= now - 6 * HOUR_MS)
77
77
  if (recent.length < 2) return 0
78
+
79
+ let windowStartIdx = 0
80
+ for (let i = 1; i < recent.length; i++) {
81
+ const prev = recent[i - 1]
82
+ const curr = recent[i]
83
+ const resetChanged = prev.resetAt != null && curr.resetAt != null && Math.abs(Number(prev.resetAt) - Number(curr.resetAt)) > 300
84
+ const increased = curr.pct > prev.pct + 0.5
85
+ if (resetChanged || increased) {
86
+ windowStartIdx = i
87
+ }
88
+ }
89
+ recent = recent.slice(windowStartIdx)
90
+ if (recent.length < 2) return 0
91
+
78
92
  const first = recent[0]
79
93
  const last = recent[recent.length - 1]
80
94
  const hours = (last.at - first.at) / HOUR_MS
package/lib/http.js CHANGED
@@ -157,13 +157,13 @@ export function isTrustedSettingsRequest(request, targetCtx) {
157
157
  try {
158
158
  const u = new URL(origin)
159
159
  if (u.host.toLowerCase() === host.toLowerCase()) return true
160
- } catch {}
160
+ } catch { /* bestEffort */ }
161
161
  }
162
162
  if (referer && host) {
163
163
  try {
164
164
  const u = new URL(referer)
165
165
  if (u.host.toLowerCase() === host.toLowerCase()) return true
166
- } catch {}
166
+ } catch { /* bestEffort */ }
167
167
  }
168
168
  }
169
169
 
package/lib/index.js CHANGED
@@ -142,7 +142,7 @@ export function apply(ctx, rawConfig) {
142
142
  syncCustomVendors()
143
143
  } catch (err) {
144
144
  cachedConfig = prevConfig
145
- try { syncCustomVendors() } catch {}
145
+ try { syncCustomVendors() } catch { /* bestEffort */ }
146
146
  throw err
147
147
  }
148
148
  return parsed
@@ -316,7 +316,7 @@ export function apply(ctx, rawConfig) {
316
316
  cooldownMs: () => live().cooldownMs,
317
317
  switchAtRemaining: () => live().switchAtRemaining,
318
318
  rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
319
- rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
319
+ rememberQuarantine: (ref, reason, until, attempts) => store.rememberQuarantine(ref, reason, until, attempts),
320
320
  recordSuccess: (ref) => store.recordSuccess(ref),
321
321
  getHealth: (ref) => store.getHealth(ref),
322
322
  recordSwitch: (ref) => store.recordSwitch(ref),
@@ -100,9 +100,17 @@ export async function* streamWithRotation({
100
100
  ? REASON_HARD_LIMIT
101
101
  : REASON_RATE_LIMIT
102
102
 
103
- const quarantined = putInQuarantine(account, reason, nowMs())
104
- account.quarantineUntil = quarantined.quarantineUntil
105
- account.quarantineReason = quarantined.quarantineReason
103
+ if (err && err.isWarmupProbeFailure && err.warmupAccount) {
104
+ account.quarantineUntil = err.warmupAccount.quarantineUntil
105
+ account.quarantineReason = err.warmupAccount.quarantineReason
106
+ account.quarantineAttempts = err.warmupAccount.quarantineAttempts
107
+ account.status = err.warmupAccount.status
108
+ } else {
109
+ const quarantined = putInQuarantine(account, reason, nowMs())
110
+ account.quarantineUntil = quarantined.quarantineUntil
111
+ account.quarantineReason = quarantined.quarantineReason
112
+ account.quarantineAttempts = (account.quarantineAttempts || 0) + 1
113
+ }
106
114
 
107
115
  if (reason === REASON_REVOKED) {
108
116
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.51",
3
+ "version": "0.6.53",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",