@goodandready/dsh-subscriptions 0.6.51 → 0.6.52

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,44 +2362,68 @@ 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', '插件'])
2365
+ const PLUGIN_LABELS = new Set(['plugins', 'плагины', '插件', 'plugin'])
2366
+ function matchesPluginLabel(el) {
2367
+ if (!el) return false
2368
+ const txt = (el.textContent || '').trim().toLowerCase()
2369
+ const aria = (el.getAttribute('aria-label') || '').trim().toLowerCase()
2370
+ const title = (el.getAttribute('title') || '').trim().toLowerCase()
2371
+ const dataTab = (el.getAttribute('data-tab') || '').trim().toLowerCase()
2372
+ return PLUGIN_LABELS.has(txt) || PLUGIN_LABELS.has(aria) || PLUGIN_LABELS.has(title) || PLUGIN_LABELS.has(dataTab)
2373
+ }
2374
+
2375
+ function findAndClickCard(doc) {
2376
+ for (const key of [PKG, 'dsh-subscriptions', '@goodandready/dsh-subscriptions']) {
2377
+ const card = doc.querySelector(`[data-plugin-package="${key}"]`) || doc.querySelector(`[data-plugin-item="${ROW_ID}"]`)
2378
+ if (card) {
2379
+ const open = [...card.querySelectorAll('button')].filter((b) => b.getAttribute('role') !== 'switch')
2380
+ const target = open.length >= 1 ? open[0] : card
2381
+ if (typeof target.click === 'function') {
2382
+ target.click()
2383
+ return true
2384
+ }
2385
+ }
2386
+ }
2387
+ return false
2388
+ }
2389
+
2366
2390
  function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms)) {
2367
2391
  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()))
2392
+ if (findAndClickCard(doc)) return
2393
+
2394
+ const buttons = [...doc.querySelectorAll('button, a, div[role="button"], div[role="tab"]')]
2395
+ const panel = buttons.find(matchesPluginLabel)
2371
2396
  if (panel) {
2372
2397
  panel.click()
2373
2398
  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 {}
2399
+ if (findAndClickCard(doc)) return
2387
2400
  if (left > 0) schedule(() => poll(left - 1), 100)
2388
2401
  }
2389
- schedule(() => poll(40), 150)
2402
+ schedule(() => poll(40), 100)
2390
2403
  return
2391
2404
  }
2405
+
2392
2406
  const triggers = buttons.filter((b) => b.getAttribute('aria-haspopup') === 'dialog' && b.hasAttribute('aria-expanded'))
2393
2407
  if (triggers.length > 0) {
2394
2408
  const trigger = triggers.find((b) => b.getAttribute('aria-expanded') !== 'true') || triggers[0]
2395
2409
  if (trigger && typeof trigger.click === 'function') {
2396
2410
  trigger.click()
2397
- schedule(() => {
2411
+ const pollSection = (left) => {
2398
2412
  try {
2399
- const section = [...doc.querySelectorAll('button')].find((b) => PLUGIN_LABELS.has(b.textContent.trim()))
2400
- if (section && typeof section.click === 'function') section.click()
2413
+ const section = [...doc.querySelectorAll('button, a, div[role="tab"], [role="button"]')].find(matchesPluginLabel)
2414
+ if (section && typeof section.click === 'function') {
2415
+ section.click()
2416
+ const poll = (cLeft) => {
2417
+ if (findAndClickCard(doc)) return
2418
+ if (cLeft > 0) schedule(() => poll(cLeft - 1), 100)
2419
+ }
2420
+ schedule(() => poll(40), 100)
2421
+ return
2422
+ }
2401
2423
  } catch {}
2402
- }, 80)
2424
+ if (left > 0) schedule(() => pollSection(left - 1), 80)
2425
+ }
2426
+ schedule(() => pollSection(20), 80)
2403
2427
  }
2404
2428
  }
2405
2429
  } catch {}
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/index.js CHANGED
@@ -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.52",
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",