@goodandready/dsh-subscriptions 0.6.4 → 0.6.6

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/README.md CHANGED
@@ -186,6 +186,13 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
186
186
  * **Frosted Panel**: Hovering reveals a `backdrop-filter: blur(28px)` panel listing every account with usage bars. Clicking the bubble refreshes quota data; data also refreshes every 60 s.
187
187
  * **Settings Card Polish (`v0.4.18`)**: Chevron switched to the core `IconChevronDownOutline14` primitive; explicit settings snapshot states (loading / unavailable + Retry) guard against phantom input.
188
188
 
189
+ ### 16. ⚡ ClineBot-Inspired UI & Hardened Telemetry (`v0.6.6`)
190
+ * **Live Status Badge Bar**: Instant latency check to the host (`Host online (XX ms)`), real-time connected account indicators, and pool size at a glance.
191
+ * **One-Click Smoke Test (Ping)**: Test live connectivity to the active upstream subscription provider and measure real round-trip latency.
192
+ * **Session Telemetry Dashboard**: Visual stat cards displaying successful/total requests, average latency, session success rate, and last request activity.
193
+ * **Curated Models Catalog**: Quick preview of supported model families with context windows and capability tags (Vision, Reasoning, Hybrid).
194
+ * **Non-Blocking Storage**: `HistoryStore` debounces disk persistence asynchronously to prevent blocking the Node.js event loop during high-throughput streaming.
195
+
189
196
  ---
190
197
 
191
198
  ## 📦 Quick Installation
package/README.ru.md CHANGED
@@ -162,6 +162,13 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
162
162
  * **Reasoning vs Standard**: 429 на reasoning-модели (claude `*thinking*`, grok `*reasoning*`, все codex-модели) охлаждает только семейство reasoning этого аккаунта — standard-модели того же аккаунта продолжают работать. Legacy-кулдауны (от старых версий) по-прежнему блокируют весь аккаунт до истечения.
163
163
  * **Скрыть устаревшие модели (`hideDeprecatedModels`)**: убирает из нативного селектора id с `test`/`preview`/`dev`/`alpha`/`beta`/`legacy` (действует и на живые каталоги, и на статичный фолбэк).
164
164
 
165
+ ### 12. ⚡ Дизайн в стиле ClineBot и неблокирующая телеметрия (`v0.6.6`)
166
+ * **Строка статуса с бейджами**: Мгновенный замер задержки хоста (`Host online (XX ms)`), количество активных подключений и размер пула.
167
+ * **Интерактивный Smoke Test (Ping)**: Проверка связи с провайдером в один клик с измерением реального времени отклика в миллисекундах.
168
+ * **Панель телеметрии сессии**: 4 карточки метрик (успешных/всего запросов, средняя задержка, процент успешности, последняя активность).
169
+ * **Каталог моделей подписок**: Компактный список поддерживаемых моделей с размером контекста (128k, 200k, 1M) и тегами возможностей.
170
+ * **Асинхронная история**: Неблокирующий дебаунсинг записи `HistoryStore`, исключающий просадки event loop при интенсивном стриминге.
171
+
165
172
  ---
166
173
 
167
174
  ## 📦 Быстрая установка
package/lib/blob.js CHANGED
@@ -37,11 +37,3 @@ export function parseBlob(text) {
37
37
  }
38
38
  }
39
39
 
40
- export function publicAccountView(blob, extra) {
41
- const label = (blob && (blob.label || blob.email)) || ''
42
- return {
43
- label,
44
- email: (blob && blob.email) || '',
45
- ...(extra || {}),
46
- }
47
- }
package/lib/client.js CHANGED
@@ -54,6 +54,10 @@ window.__ModuleLoader__.load({
54
54
  '.dsub-chevOpen{transform:rotate(180deg)}' +
55
55
  '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
56
56
  '.dsub-block{margin-top:10px}' +
57
+ '.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}' +
58
+ '.dsub-group[open]{padding-bottom:6px}' +
59
+ '.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}' +
60
+ '.dsub-groupHead::-webkit-details-marker{display:none}' +
57
61
  '.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
58
62
  '.dsub-grow{flex:1 1 180px;min-width:140px}' +
59
63
  '.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
@@ -107,6 +111,27 @@ window.__ModuleLoader__.load({
107
111
  '.dsub-details{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2)}' +
108
112
  '.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}' +
109
113
  '.dsub-summary:hover{color:var(--dsw-alias-label-primary)}' +
114
+ '.dsub-header-bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:8px 0 12px;margin-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
115
+ '.dsub-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}' +
116
+ '.dsub-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
117
+ '.dsub-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
118
+ '.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
119
+ '.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}' +
120
+ '.dsub-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;margin-top:12px}' +
121
+ '.dsub-section-title{font-size:14.5px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}' +
122
+ '.dsub-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
123
+ '.dsub-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:10px}' +
124
+ '.dsub-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}' +
125
+ '.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}' +
126
+ '.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
127
+ '.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:rgba(16,185,129,0.09);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid rgba(16,185,129,0.25)}' +
128
+ '.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:rgba(239,68,68,0.09);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid rgba(239,68,68,0.25)}' +
129
+ '.dsub-btn-primary{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;border-color:transparent !important;font-weight:600}' +
130
+ '.dsub-btn-primary:hover:not(:disabled){opacity:0.9}' +
131
+ '.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}' +
132
+ '.dsub-model-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-size:11.5px;color:var(--dsw-alias-label-primary)}' +
133
+ '.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}' +
134
+ '.dsub-model-tag{font-size:10px;padding:1px 4px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-weight:600}' +
110
135
  '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
111
136
  '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
112
137
  '.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
@@ -310,6 +335,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
310
335
  'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
311
336
  'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
312
337
  'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
338
+ 'closeModal': 'Close',
339
+ 'groupAuth': 'Sign-in and redirect',
340
+ 'groupPrivacy': 'Privacy and model list',
341
+ 'groupRotation': 'Rotation and limits',
342
+ 'groupProvider': 'Provider behaviour',
343
+ 'groupFallback': 'Local fallback',
313
344
  'privacyMask': 'Mask emails and account identifiers',
314
345
  'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
315
346
  'autoLoopback': 'Auto Loopback',
@@ -343,6 +374,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
343
374
  'probeIntervalMinHint': 'Interval for background checks to refresh account status and quota balance.',
344
375
  'notifyLimits': 'Notify on rate limits',
345
376
  'notifyLimitsHint': 'Show notification alerts when account usage reaches quota ceiling.',
377
+ 'hostOnline': 'Host online',
378
+ 'hostOffline': 'Host unreachable',
379
+ 'statsTitle': 'Session Telemetry',
380
+ 'statsRequests': 'Successful / Total Requests',
381
+ 'statsLatency': 'Avg Response Latency',
382
+ 'statsSuccessRate': 'Session Success Rate',
383
+ 'statsLastActivity': 'Last Activity',
384
+ 'smokeTitle': 'Connection Smoke Test (Ping)',
385
+ 'smokeDesc': 'Test live upstream connectivity and measure round-trip latency to the subscription provider.',
386
+ 'runSmoke': '⚡ Run Smoke Test (Ping)',
387
+ 'smokeTesting': 'Pinging…',
388
+ 'smokeOk': 'Smoke test passed',
389
+ 'smokeFail': 'Smoke test failed',
390
+ 'modelsCatalogTitle': 'Curated Subscription Models',
391
+ 'modelsCatalogDesc': 'Available model catalog across active subscriptions in DeepSeek Harness.',
346
392
  'resetCredits': 'Reset credits',
347
393
  'resetCreditsHint': 'ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.',
348
394
  'resetCreditsAction': 'Reset ChatGPT Limit',
@@ -539,6 +585,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
539
585
  function badgeFor(account, t) {
540
586
  if (!account || !account.configured) return t('notConnected')
541
587
  if (account.validationUrl) return t('verifyAccount')
588
+ // #303: a quarantined account says why it is parked.
589
+ if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
590
+ return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
591
+ }
542
592
  if (account.cooldownUntil && account.cooldownUntil > Date.now()) return t('coolingDown')
543
593
  if (account.usagePercent != null && account.usagePercent >= 100) return t('usageFull')
544
594
  return t('connected')
@@ -636,6 +686,32 @@ const cssId = 'dsh-subscriptions/settings.module.css'
636
686
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
637
687
  }
638
688
  const [checkRes, setCheckRes] = React.useState({})
689
+ const [hostPing, setHostPing] = React.useState({ ok: true, latencyMs: null })
690
+ const [telemetry, setTelemetry] = React.useState(null)
691
+ const [smoke, setSmoke] = React.useState({ testing: false, result: null })
692
+
693
+ const reloadTelemetry = () => {
694
+ fetch('/dsh-subscriptions/telemetry', { cache: 'no-store' })
695
+ .then((r) => r.json())
696
+ .then((d) => { if (d && d.ok && d.telemetry) setTelemetry(d.telemetry) })
697
+ .catch(() => {})
698
+ }
699
+
700
+ const runSmokeTest = async (provider, index) => {
701
+ setSmoke({ testing: true, result: null })
702
+ try {
703
+ const res = await fetch('/dsh-subscriptions/smoke', {
704
+ method: 'POST',
705
+ headers: { 'Content-Type': 'application/json' },
706
+ body: JSON.stringify({ provider, index }),
707
+ })
708
+ const d = await res.json()
709
+ setSmoke({ testing: false, result: d })
710
+ reloadTelemetry()
711
+ } catch (e) {
712
+ setSmoke({ testing: false, result: { ok: false, error: { message: String(e && e.message || e) } } })
713
+ }
714
+ }
639
715
  const [checking, setChecking] = React.useState({})
640
716
  const [saved, setSaved] = React.useState(false)
641
717
  const [tokenDraft, setTokenDraft] = React.useState({})
@@ -649,9 +725,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
649
725
  setProviders((data && data.providers) || [])
650
726
  }
651
727
 
652
- const reload = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
653
- .then((res) => res.json())
654
- .then(applyPayload)
728
+ const reload = () => {
729
+ const t0 = Date.now()
730
+ return fetch('/dsh-subscriptions/config', { cache: 'no-store' })
731
+ .then((res) => res.json())
732
+ .then((data) => {
733
+ setHostPing({ ok: true, latencyMs: Date.now() - t0 })
734
+ applyPayload(data)
735
+ reloadTelemetry()
736
+ return data
737
+ })
738
+ .catch((err) => {
739
+ setHostPing({ ok: false, latencyMs: null })
740
+ throw err
741
+ })
742
+ }
655
743
 
656
744
  React.useEffect(() => {
657
745
  let alive = true
@@ -857,11 +945,65 @@ const cssId = 'dsh-subscriptions/settings.module.css'
857
945
  { id: 'antigravity', name: 'Antigravity' },
858
946
  ]
859
947
 
948
+ const connectedCount = accounts.filter((a) => a.configured).length
949
+
860
950
  return React.createElement('div', { className: 'dsub-wrap' },
951
+ React.createElement('div', { className: 'dsub-header-bar' },
952
+ React.createElement('div', { className: 'dsub-row', style: { marginTop: 0 } },
953
+ React.createElement('span', {
954
+ className: 'dsub-badge ' + (hostPing.ok ? 'dsub-badge-ok' : 'dsub-badge-bad')
955
+ }, hostPing.ok
956
+ ? (hostPing.latencyMs != null ? 'Host online (' + hostPing.latencyMs + ' ms)' : t('hostOnline'))
957
+ : t('hostOffline')
958
+ ),
959
+ React.createElement('span', {
960
+ className: 'dsub-badge ' + (connectedCount > 0 ? 'dsub-badge-ok' : 'dsub-badge-warn')
961
+ }, connectedCount > 0
962
+ ? (connectedCount + ' ' + t('subsLogged'))
963
+ : t('subsNotLogged')
964
+ ),
965
+ React.createElement('span', {
966
+ className: 'dsub-badge dsub-badge-neutral'
967
+ }, 'Pool: ' + accounts.length + ' accounts'),
968
+ ),
969
+ React.createElement('button', {
970
+ type: 'button',
971
+ className: 'dsub-mini dsub-btn-primary',
972
+ disabled: smoke.testing,
973
+ onClick: () => runSmokeTest(),
974
+ }, smoke.testing ? t('smokeTesting') : t('runSmoke')),
975
+ ),
976
+ smoke.result ? React.createElement('div', {
977
+ className: smoke.result.ok ? 'dsub-alert-ok' : 'dsub-alert-bad',
978
+ style: { marginBottom: '12px' }
979
+ }, smoke.result.ok
980
+ ? ('🟢 ' + t('smokeOk') + ': ' + (smoke.result.provider || 'Provider').toUpperCase() + ' · ' + (smoke.result.latencyMs != null ? smoke.result.latencyMs + ' ms' : 'OK') + (smoke.result.label ? ' (' + smoke.result.label + ')' : ''))
981
+ : ('❌ ' + t('smokeFail') + ': ' + (smoke.result.error && (smoke.result.error.message || smoke.result.error.code) || 'Unknown error'))
982
+ ) : null,
983
+ telemetry && telemetry.totalRequests > 0 ? React.createElement('div', { className: 'dsub-grid-2', style: { marginBottom: '14px' } },
984
+ React.createElement('div', { className: 'dsub-stat-box' },
985
+ React.createElement('div', { className: 'dsub-stat-val' }, telemetry.successRequests + ' / ' + telemetry.totalRequests),
986
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsRequests')),
987
+ ),
988
+ React.createElement('div', { className: 'dsub-stat-box' },
989
+ React.createElement('div', { className: 'dsub-stat-val' }, (telemetry.avgLatencyMs || 0) + ' ms'),
990
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLatency')),
991
+ ),
992
+ React.createElement('div', { className: 'dsub-stat-box' },
993
+ React.createElement('div', { className: 'dsub-stat-val' }, (telemetry.successRate || 100) + '%'),
994
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsSuccessRate')),
995
+ ),
996
+ React.createElement('div', { className: 'dsub-stat-box' },
997
+ React.createElement('div', { className: 'dsub-stat-val' }, telemetry.lastRequestAt ? Math.round((Date.now() - telemetry.lastRequestAt) / 60000) + 'm ago' : '—'),
998
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLastActivity')),
999
+ ),
1000
+ ) : null,
861
1001
  React.createElement('div', { className: 'dsub-block' },
862
1002
  React.createElement('div', { className: 'dsub-h' }, t('title')),
863
1003
  React.createElement('div', { className: 'dsub-sub' },
864
1004
  t('intro')),
1005
+ React.createElement('details', { className: 'dsub-group', open: true },
1006
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupAuth')),
865
1007
  React.createElement('label', { className: 'dsub-row' },
866
1008
  React.createElement('input', {
867
1009
  type: 'checkbox',
@@ -875,23 +1017,26 @@ const cssId = 'dsh-subscriptions/settings.module.css'
875
1017
  React.createElement('label', { className: 'dsub-row' },
876
1018
  React.createElement('input', {
877
1019
  type: 'checkbox',
878
- checked: !!draft.privacyMask,
879
- onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
1020
+ checked: !!draft.autoLoopback,
1021
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
880
1022
  }),
881
- React.createElement('span', null, t('privacyMask')),
1023
+ React.createElement('span', null, t('autoLoopback')),
882
1024
  ),
883
1025
  React.createElement('div', { className: 'dsub-sub' },
884
- t('privacyMaskHint')),
1026
+ t('autoLoopbackHint')),
1027
+ ),
1028
+ React.createElement('details', { className: 'dsub-group', open: true },
1029
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupPrivacy')),
885
1030
  React.createElement('label', { className: 'dsub-row' },
886
1031
  React.createElement('input', {
887
1032
  type: 'checkbox',
888
- checked: !!draft.autoLoopback,
889
- onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
1033
+ checked: !!draft.privacyMask,
1034
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
890
1035
  }),
891
- React.createElement('span', null, t('autoLoopback')),
1036
+ React.createElement('span', null, t('privacyMask')),
892
1037
  ),
893
1038
  React.createElement('div', { className: 'dsub-sub' },
894
- t('autoLoopbackHint')),
1039
+ t('privacyMaskHint')),
895
1040
  React.createElement('label', { className: 'dsub-row' },
896
1041
  React.createElement('input', {
897
1042
  type: 'checkbox',
@@ -902,6 +1047,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
902
1047
  ),
903
1048
  React.createElement('div', { className: 'dsub-sub' },
904
1049
  t('hideDeprecatedHint')),
1050
+ ),
1051
+ React.createElement('details', { className: 'dsub-group', open: true },
1052
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupRotation')),
905
1053
  React.createElement('div', { className: 'dsub-row' },
906
1054
  React.createElement('span', { className: 'dsub-h' }, t('composerQuota')),
907
1055
  React.createElement('select', {
@@ -931,6 +1079,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
931
1079
  ),
932
1080
  React.createElement('div', { className: 'dsub-sub' },
933
1081
  t('expiryNotifyDaysHint')),
1082
+ ),
1083
+ React.createElement('details', { className: 'dsub-group', open: true },
1084
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupProvider')),
934
1085
  React.createElement('label', { className: 'dsub-row' },
935
1086
  React.createElement('input', {
936
1087
  type: 'checkbox',
@@ -956,6 +1107,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
956
1107
  ),
957
1108
  React.createElement('div', { className: 'dsub-sub' },
958
1109
  t('codexVerbosityHint')),
1110
+ ),
1111
+ React.createElement('details', { className: 'dsub-group', open: true },
1112
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupFallback')),
959
1113
  React.createElement('label', { className: 'dsub-row' },
960
1114
  React.createElement('input', {
961
1115
  type: 'checkbox',
@@ -982,6 +1136,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
982
1136
  onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallbackModel: e.target.value })),
983
1137
  }),
984
1138
  ) : null,
1139
+ ),
1140
+ React.createElement('details', { className: 'dsub-group' },
1141
+ React.createElement('summary', { className: 'dsub-groupHead' }, '🎯 ' + t('modelsCatalogTitle')),
1142
+ React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('modelsCatalogDesc')),
1143
+ React.createElement('div', { className: 'dsub-model-grid' },
1144
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'gpt-4o'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Vision')),
1145
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'o1 / o3-mini'), React.createElement('span', { className: 'dsub-model-tag' }, '200k · Reasoning')),
1146
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'claude-3-7-sonnet'), React.createElement('span', { className: 'dsub-model-tag' }, '200k · Hybrid')),
1147
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'claude-3-5-sonnet'), React.createElement('span', { className: 'dsub-model-tag' }, '200k · Coding')),
1148
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'grok-beta'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Speed')),
1149
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'gemini-2.5-pro'), React.createElement('span', { className: 'dsub-model-tag' }, '1M · Context')),
1150
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'kimi-k1.5'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Math')),
1151
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'glm-4-plus'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Chinese')),
1152
+ ),
1153
+ ),
985
1154
  React.createElement('details', { className: 'dsub-details' },
986
1155
  React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
987
1156
  React.createElement('div', { className: 'dsub-block' },
@@ -1691,15 +1860,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1691
1860
  const rows = state.accounts.map((a, i) => {
1692
1861
  const isLogged = state.logged.includes(a.provider)
1693
1862
  const cooled = a.cooldownUntil && a.cooldownUntil > now
1863
+ const quarantined = Boolean(a.quarantineReason) && Number(a.quarantineUntil || 0) > now
1694
1864
  const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
1695
1865
  const b = brandBadge(a.provider)
1696
1866
  const isActiveThis = state.active && state.active.provider === a.provider && state.active.index === a.index
1697
1867
 
1698
1868
  let statusClass = 'dsub-statusOff'
1699
1869
  let statusText = t('subsNotLogged')
1700
- if (cooled) {
1870
+ if (quarantined) {
1871
+ statusClass = 'dsub-statusBad'
1872
+ statusText = a.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(a.quarantineReason)
1873
+ } else if (cooled) {
1701
1874
  statusClass = 'dsub-statusWarn'
1702
- statusText = t('subsCooldown')
1875
+ // Only some model families may be cooled down - say which ones.
1876
+ statusText = t('subsCooldown') + (Array.isArray(a.cooldownFamilies) && a.cooldownFamilies.length ? ' · ' + a.cooldownFamilies.join('/') : '')
1703
1877
  } else if (isLogged) {
1704
1878
  if (pct >= 90) { statusClass = 'dsub-statusBad'; statusText = '90%+ ' + t('subsUsed') }
1705
1879
  else if (pct >= 50) { statusClass = 'dsub-statusWarn'; statusText = pct + '% ' + t('subsUsed') }
@@ -1715,7 +1889,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1715
1889
  isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent)', color: 'var(--dsw-alias-state-success-primary)' } }, 'ACTIVE') : null,
1716
1890
  a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
1717
1891
  ),
1718
- React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
1892
+ React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass, role: 'status', 'aria-live': 'polite' }, statusText),
1719
1893
  ),
1720
1894
  (isLogged && pct != null) ? React.createElement('div', { className: 'dsub-barTrack', style: { marginTop: 4, height: 4 } },
1721
1895
  React.createElement('div', {
@@ -1746,7 +1920,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1746
1920
  className: 'dsub-modalWrap',
1747
1921
  onClick: (e) => { if (e.target === e.currentTarget) setState((s) => Object.assign({}, s, { open: false })) },
1748
1922
  },
1749
- React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-label': t('subsModalTitle') },
1923
+ React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('subsModalTitle') },
1750
1924
  React.createElement('div', { className: 'dsub-modalHead' },
1751
1925
  React.createElement('div', { className: 'dsub-modalTitleWrap' },
1752
1926
  React.createElement('div', { className: 'dsub-modalIcon' }, '⚡'),
@@ -1765,6 +1939,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1765
1939
  React.createElement('button', {
1766
1940
  type: 'button',
1767
1941
  className: 'dsub-mini',
1942
+ autoFocus: true,
1943
+ 'aria-label': t('closeModal'),
1768
1944
  onClick: () => setState((s) => Object.assign({}, s, { open: false })),
1769
1945
  }, '✕'),
1770
1946
  ),
package/lib/history.js CHANGED
@@ -4,17 +4,24 @@ import { homedir } from 'node:os'
4
4
 
5
5
  // #65: request and cost history. Stored in
6
6
  // ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
7
- // All IO is best-effort and synchronous: a write failure must never crash the harness.
7
+ // All IO is best-effort: a write failure must never crash the harness.
8
8
 
9
9
  export const DEFAULT_HISTORY_DIR = join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
10
10
 
11
11
  export class HistoryStore {
12
- constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000) {
12
+ constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0) {
13
13
  this.dir = dir
14
14
  this.ttlMs = ttlMs
15
+ this.debounceMs = debounceMs
15
16
  this.path = join(dir, 'history.json')
16
17
  this.rows = []
18
+ this._persistTimer = null
17
19
  this._load()
20
+
21
+ if (typeof process !== 'undefined' && typeof process.on === 'function') {
22
+ this._exitHandler = () => this.flush()
23
+ process.on('beforeExit', this._exitHandler)
24
+ }
18
25
  }
19
26
 
20
27
  _load() {
@@ -36,6 +43,24 @@ export class HistoryStore {
36
43
  }
37
44
 
38
45
  _persist() {
46
+ if (this.debounceMs > 0) {
47
+ if (this._persistTimer) return
48
+ this._persistTimer = setTimeout(() => {
49
+ this._persistTimer = null
50
+ this.flush()
51
+ }, this.debounceMs)
52
+ if (typeof this._persistTimer.unref === 'function') this._persistTimer.unref()
53
+ return
54
+ }
55
+ this.flush()
56
+ }
57
+
58
+ /** Write pending updates to disk immediately. */
59
+ flush() {
60
+ if (this._persistTimer) {
61
+ clearTimeout(this._persistTimer)
62
+ this._persistTimer = null
63
+ }
39
64
  try {
40
65
  mkdirSync(this.dir, { recursive: true })
41
66
  writeFileSync(this.path, JSON.stringify(this.rows))
@@ -63,4 +88,55 @@ export class HistoryStore {
63
88
  size() {
64
89
  return this.rows.length
65
90
  }
91
+
92
+ /** Summarize session and 24-hour telemetry for UI stat cards. */
93
+ telemetrySummary() {
94
+ const total = this.rows.length
95
+ if (total === 0) {
96
+ return {
97
+ totalRequests: 0,
98
+ requests24h: 0,
99
+ successRequests: 0,
100
+ errorRequests: 0,
101
+ successRate: 100,
102
+ avgLatencyMs: 0,
103
+ lastLatencyMs: 0,
104
+ lastRequestAt: null,
105
+ lastProvider: null,
106
+ lastModel: null,
107
+ }
108
+ }
109
+ let success = 0
110
+ let error = 0
111
+ let totalMs = 0
112
+ let latencyCount = 0
113
+ const now = Date.now()
114
+ const last24h = now - 24 * 60 * 60 * 1000
115
+ let count24h = 0
116
+
117
+ for (const r of this.rows) {
118
+ if (r.ts >= last24h) count24h++
119
+ const st = Number(r.status || 0)
120
+ if (st >= 200 && st < 400) success++
121
+ else if (st >= 400) error++
122
+ if (typeof r.ms === 'number' && r.ms > 0) {
123
+ totalMs += r.ms
124
+ latencyCount++
125
+ }
126
+ }
127
+
128
+ const first = this.rows[0] // newest first
129
+ return {
130
+ totalRequests: total,
131
+ requests24h: count24h,
132
+ successRequests: success,
133
+ errorRequests: error,
134
+ successRate: total > 0 ? Math.round((success / total) * 100) : 100,
135
+ avgLatencyMs: latencyCount > 0 ? Math.round(totalMs / latencyCount) : 0,
136
+ lastLatencyMs: (first && typeof first.ms === 'number') ? first.ms : 0,
137
+ lastRequestAt: (first && first.ts) || null,
138
+ lastProvider: (first && first.provider) || null,
139
+ lastModel: (first && first.model) || null,
140
+ }
141
+ }
66
142
  }
package/lib/http.js CHANGED
@@ -30,7 +30,10 @@ export function readBody(req, maxBytes = 64 * 1024) {
30
30
  })
31
31
  }
32
32
 
33
- /** Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
33
+ /**
34
+ * @param {import('node:http').IncomingMessage} request
35
+ * @returns {boolean}
36
+ * Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
34
37
  export function isTrustedSettingsRequest(request) {
35
38
  return request.headers['sec-fetch-site'] !== 'cross-site'
36
39
  }
package/lib/index.js CHANGED
@@ -440,6 +440,11 @@ export function apply(ctx, config) {
440
440
  configured: info.configured,
441
441
  writable: info.writable,
442
442
  cooldownUntil: info.cooldownUntil,
443
+ // #303: expose the cooldown scope and quarantine so the card can
444
+ // explain why only some models are blocked.
445
+ cooldownFamilies: info.cooldownFamilies || null,
446
+ quarantineUntil: info.quarantineUntil || 0,
447
+ quarantineReason: info.quarantineReason || null,
443
448
  usagePercent: info.usagePercent,
444
449
  quota: info.quota || null,
445
450
  usage: info.usage || null,
package/lib/quarantine.js CHANGED
@@ -29,10 +29,3 @@ export function isQuarantined(account, nowMs = Date.now()) {
29
29
  return Number(account.quarantineUntil) > Number(nowMs)
30
30
  }
31
31
 
32
- export function releaseFromQuarantine(account) {
33
- const next = { ...account }
34
- delete next.quarantineReason
35
- delete next.quarantineUntil
36
- if (next.status === 'quarantine') next.status = 'active'
37
- return next
38
- }
package/lib/ratelimit.js CHANGED
@@ -208,11 +208,3 @@ export function quotaSnapshot(provider, headers, bodyJson, nowMs = Date.now()) {
208
208
  return { remaining, limit, resetAt: resetAt ?? null, usedPercent, measuredAt: nowMs }
209
209
  }
210
210
 
211
- export function formatQuota(q) {
212
- if (!q) return ""
213
- const parts = []
214
- if (q.remaining!=null && q.limit!=null) parts.push(q.remaining+"/"+q.limit)
215
- else if (q.remaining!=null) parts.push("left "+q.remaining)
216
- if (q.resetAt) parts.push("reset "+new Date(q.resetAt).toLocaleString())
217
- return parts.join(" · ")
218
- }
package/lib/refs.js CHANGED
@@ -61,6 +61,11 @@ export function displayName(provider) {
61
61
  return DISPLAY[provider] || displayExtra[provider] || provider
62
62
  }
63
63
 
64
+ /**
65
+ * @param {string} provider
66
+ * @param {number} index
67
+ * @returns {string} provider-scoped credential ref, e.g. CODEX_OAUTH_1
68
+ */
64
69
  export function oauthRef(provider, index) {
65
70
  const n = Number(index)
66
71
  if (!Number.isInteger(n) || n < 1) throw new Error('account index must be an integer >= 1')
@@ -26,6 +26,12 @@ function fill(template, n) {
26
26
  return String(template).replace('{n}', String(n))
27
27
  }
28
28
 
29
+ /**
30
+ * @param {number} resetAt epoch ms of the quota window reset
31
+ * @param {string} [lang]
32
+ * @param {number} [now]
33
+ * @returns {string} human-readable remaining time, down to the minute
34
+ */
29
35
  export function formatRelativeReset(resetAt, lang = 'ru', now = Date.now()) {
30
36
  if (typeof resetAt !== 'number' || !Number.isFinite(resetAt) || resetAt <= 0) return ''
31
37
  const delta = resetAt - now
@@ -32,7 +32,7 @@ export function registerStatusRoutes(ctx, state) {
32
32
  ctx.effect(() => ctx.webServer.register({
33
33
  kind: 'exact',
34
34
  path: '/dsh-subscriptions/config',
35
- handler: async (req, res) => {
35
+ handler: safeJsonHandler(async (req, res) => {
36
36
  if (req.method === 'GET') {
37
37
  writeJson(res, 200, await configResponse())
38
38
  return
@@ -67,10 +67,7 @@ export function registerStatusRoutes(ctx, state) {
67
67
  } catch (e) {
68
68
  writeJson(res, 400, { ok: false, error: { code: 'save', message: String(e && e.message || e) } })
69
69
  }
70
- },
71
-
72
-
73
-
70
+ }),
74
71
  }), 'dsh-subscriptions: /config')
75
72
 
76
73
 
@@ -239,7 +236,7 @@ export function registerStatusRoutes(ctx, state) {
239
236
  ctx.effect(() => ctx.webServer.register({
240
237
  kind: 'exact',
241
238
  path: '/dsh-subscriptions/history',
242
- handler: async (req, res) => {
239
+ handler: safeJsonHandler(async (req, res) => {
243
240
  if (req.method !== 'GET') {
244
241
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
245
242
  return
@@ -251,6 +248,127 @@ export function registerStatusRoutes(ctx, state) {
251
248
  }
252
249
  const limit = Math.min(Number(queryOf(req).get('limit') || '10'), 100)
253
250
  writeJson(res, 200, { ok: true, total: history.size(), items: history.recent(limit) })
254
- },
251
+ }),
255
252
  }), 'dsh-subscriptions: /history')
253
+
254
+ // Telemetry summary route for UI stat cards (requests, latency, success rate)
255
+ ctx.effect(() => ctx.webServer.register({
256
+ kind: 'exact',
257
+ path: '/dsh-subscriptions/telemetry',
258
+ handler: safeJsonHandler(async (req, res) => {
259
+ if (req.method !== 'GET') {
260
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
261
+ return
262
+ }
263
+ const summary = typeof history.telemetrySummary === 'function'
264
+ ? history.telemetrySummary()
265
+ : { totalRequests: history.size(), requests24h: 0, successRequests: 0, errorRequests: 0, successRate: 100, avgLatencyMs: 0 }
266
+ writeJson(res, 200, { ok: true, telemetry: summary })
267
+ }),
268
+ }), 'dsh-subscriptions: /telemetry')
269
+
270
+ // Live smoke ping test for active subscription providers
271
+ ctx.effect(() => ctx.webServer.register({
272
+ kind: 'exact',
273
+ path: '/dsh-subscriptions/smoke',
274
+ handler: safeJsonHandler(async (req, res) => {
275
+ if (req.method !== 'POST') {
276
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
277
+ return
278
+ }
279
+ let payload = {}
280
+ try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch {}
281
+
282
+ let provider = payload.provider
283
+ let index = payload.index || 1
284
+
285
+ if (!provider) {
286
+ const accounts = await accountsView()
287
+ const connected = accounts.find((a) => a.configured)
288
+ if (connected) {
289
+ provider = connected.provider
290
+ index = connected.index || 1
291
+ }
292
+ }
293
+
294
+ if (!provider || !isProvider(provider)) {
295
+ writeJson(res, 400, { ok: false, error: { code: 'no_connected_accounts', message: 'No connected subscription account found to test' } })
296
+ return
297
+ }
298
+
299
+ const ref = oauthRef(provider, index)
300
+ const info = await store.describeRef(ref)
301
+ if (!info.configured) {
302
+ writeJson(res, 400, { ok: false, provider, index, ref, error: { code: 'not_connected', message: 'Account is not connected' } })
303
+ return
304
+ }
305
+
306
+ const t0 = Date.now()
307
+ let blob
308
+ try {
309
+ blob = await store.loadBlob(ref)
310
+ blob = await store.ensureFresh(provider, blob, ref)
311
+ } catch (e) {
312
+ const latencyMs = Date.now() - t0
313
+ history.add({ provider, ref, path: "/smoke", status: 401, ms: latencyMs })
314
+ writeJson(res, 502, { ok: false, provider, index, ref, latencyMs, error: { code: 'auth_failed', message: String(e && e.message || e) } })
315
+ return
316
+ }
317
+
318
+ const vendor = getVendor(provider)
319
+ if (typeof vendor.check === 'function') {
320
+ let capturedQuota = null
321
+ const probeFetch = async (url, init) => {
322
+ const res2 = await fetch(url, init)
323
+ try {
324
+ const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
325
+ if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
326
+ } catch {}
327
+ return res2
328
+ }
329
+ try {
330
+ await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
331
+ const latencyMs = Date.now() - t0
332
+ history.add({ provider, ref, path: "/smoke", status: 200, ms: latencyMs })
333
+ writeJson(res, 200, {
334
+ ok: true,
335
+ provider,
336
+ index,
337
+ ref,
338
+ latencyMs,
339
+ label: pmL(blob.label),
340
+ email: pmE(blob.email),
341
+ quota: capturedQuota || info.quota || null,
342
+ usagePercent: info.usagePercent ?? null,
343
+ })
344
+ return
345
+ } catch (e) {
346
+ const latencyMs = Date.now() - t0
347
+ history.add({ provider, ref, path: "/smoke", status: 502, ms: latencyMs })
348
+ writeJson(res, 502, {
349
+ ok: false,
350
+ provider,
351
+ index,
352
+ ref,
353
+ latencyMs,
354
+ error: { code: e && e.code ? e.code : 'VENDOR', message: String(e && e.message || e).slice(0, 300) },
355
+ })
356
+ return
357
+ }
358
+ }
359
+
360
+ const latencyMs = Date.now() - t0
361
+ history.add({ provider, ref, path: "/smoke", status: 200, ms: latencyMs })
362
+ writeJson(res, 200, {
363
+ ok: true,
364
+ provider,
365
+ index,
366
+ ref,
367
+ latencyMs,
368
+ label: pmL(blob.label),
369
+ email: pmE(blob.email),
370
+ quota: info.quota || null,
371
+ })
372
+ }),
373
+ }), 'dsh-subscriptions: /smoke')
256
374
  }
@@ -1,6 +1,13 @@
1
1
  const sessionPins = new Map()
2
2
  const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
3
3
 
4
+ /**
5
+ * Bind a session id to an account ref for ttlMs (default 30 minutes).
6
+ * @param {string|undefined} sessionId
7
+ * @param {string|undefined} accountRef
8
+ * @param {number} [ttlMs]
9
+ * @returns {void}
10
+ */
4
11
  export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
5
12
  if (!sessionId || !accountRef) return
6
13
  sessionPins.set(sessionId, {
@@ -9,6 +16,10 @@ export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
9
16
  })
10
17
  }
11
18
 
19
+ /**
20
+ * @param {string|undefined} sessionId
21
+ * @returns {string|null} the bound account ref, or null when absent/expired
22
+ */
12
23
  export function getPinnedAccountRef(sessionId) {
13
24
  if (!sessionId) return null
14
25
  const entry = sessionPins.get(sessionId)
@@ -150,7 +150,9 @@ export async function usage(blob, _cfg, fetchImpl) {
150
150
  }
151
151
 
152
152
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, saveBlob }) {
153
- const ready = await withProject(blob, fetchImpl, saveBlob)
153
+ // Abort must cover the project pre-flight as well as the stream.
154
+ const preflight = (u, i) => fetchImpl(u, { ...(i || {}), signal })
155
+ const ready = await withProject(blob, preflight, saveBlob)
154
156
  const { envelope, sessionId } = streamEnvelope({
155
157
  projectId: ready.projectId,
156
158
  model: options.model,
@@ -236,7 +236,8 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
236
236
  if (!body.instructions) delete body.instructions
237
237
  // grok manages reasoning itself (cli catalog aware); drop the codex-level field
238
238
  delete body.reasoning
239
- const catalog = await cliCatalogCached(blob, config, fetchImpl).catch(() => new Map())
239
+ // Thread the caller AbortSignal through the catalog pre-flight too.
240
+ const catalog = await cliCatalogCached(blob, config, (u, i) => fetchImpl(u, { ...(i || {}), signal })).catch(() => new Map())
240
241
  const reasoning = grokReasoningBody(options.model, options.reasoningEffort, catalog)
241
242
  if (reasoning) body.reasoning = reasoning
242
243
  body = grokBodyForModel(options.model, body)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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",
@@ -34,9 +34,6 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
- "scripts": {
38
- "test": "eslint lib/ && node --test test/*.test.mjs"
39
- },
40
37
  "dsh": {
41
38
  "bundle": {
42
39
  "patch": "./cordis.patch.yml"
@@ -62,6 +59,12 @@
62
59
  "undici": "^8.10.1"
63
60
  },
64
61
  "devDependencies": {
65
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8"
62
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
63
+ "@types/node": "^22.20.2",
64
+ "typescript": "^7.0.2"
65
+ },
66
+ "scripts": {
67
+ "test": "eslint lib/ && node --test test/*.test.mjs",
68
+ "typecheck": "tsc -p tsconfig.checkjs.json"
66
69
  }
67
- }
70
+ }
@@ -1,24 +0,0 @@
1
- export function parseRateLimitHeaders(headers) {
2
- if (!headers || typeof headers !== 'object') return null
3
-
4
- const get = (key) => {
5
- const k = Object.keys(headers).find((h) => h.toLowerCase() === key.toLowerCase())
6
- return k ? headers[k] : null
7
- }
8
-
9
- const remainingTokens = get('x-ratelimit-remaining-tokens')
10
- const resetTokens = get('x-ratelimit-reset-tokens')
11
- const remainingRequests = get('x-ratelimit-remaining-requests')
12
- const resetRequests = get('x-ratelimit-reset-requests')
13
-
14
- if (!remainingTokens && !resetTokens && !remainingRequests && !resetRequests) {
15
- return null
16
- }
17
-
18
- return {
19
- remainingTokens: remainingTokens != null ? Number(remainingTokens) : null,
20
- resetTokensTime: resetTokens || null,
21
- remainingRequests: remainingRequests != null ? Number(remainingRequests) : null,
22
- resetRequestsTime: resetRequests || null
23
- }
24
- }
package/lib/ui-styles.js DELETED
@@ -1,119 +0,0 @@
1
- export const CSS =
2
- '.dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}' +
3
- '.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}' +
4
- '.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
5
- '.dsub-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:18px 20px;display:flex}' +
6
- '.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}' +
7
- '.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
8
- '.dsub-name{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:1.4}' +
9
- '.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}' +
10
- '.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}' +
11
- '.dsub-chevOpen{transform:rotate(180deg)}' +
12
- '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 20px;padding-bottom:12px}' +
13
- '.dsub-block{margin-top:10px}' +
14
- '.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
15
- '.dsub-grow{flex:1 1 180px;min-width:140px}' +
16
- '.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
17
- '.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}' +
18
- '.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}' +
19
- '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary)}' +
20
- '.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
21
- '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
22
- '.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
23
- '.dsub-mini{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
24
- '.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
25
- '.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
26
- '.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
27
- '.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
28
- '.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
29
- '.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
30
- '.dsub-inp{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;min-width:180px}' +
31
- '.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
32
- '.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:12px 14px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
33
- '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:#000;font-size:11px;font-weight:600}' +
34
- '.dsub-bar{position:relative;height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
35
- '.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
36
- '.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
37
- '.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}' +
38
- '.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
39
- '.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}' +
40
- '.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
41
- '.dsub-verify a{color:var(--dsw-alias-brand-primary)}' +
42
- '.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.15),inset 0 1px 0 rgba(255,255,255,.06);transition:all .16s ease;user-select:none}' +
43
- '.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,255,255,.1)}' +
44
- '.dsub-pill:active{transform:translateY(0)}' +
45
- '.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
46
- '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
47
- '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}' +
48
- '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warning-primary) 70%,transparent)}' +
49
- '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}' +
50
- '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
51
- '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:rgba(0,0,0,.65);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
52
- '.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.07);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
53
- '@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}' +
54
- '@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}' +
55
- '.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
56
- '.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}' +
57
- '.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}' +
58
- '.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
59
- '.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 rgba(255,255,255,.05)}' +
60
- '.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
61
- '.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
62
- '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
63
- '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
64
- '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
65
- '.dsub-barTrack{height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
66
- '.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
67
- '.dsub-panel-box{margin-top:10px;padding:14px 16px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
68
- '.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 5%,transparent)}' +
69
- '.dsub-panel-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
70
- '.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
71
- '.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
72
- '.dsub-usage-card{display:flex;flex-direction:column;gap:5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
73
- '.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}' +
74
- '.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
75
- '.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
76
- '.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
77
- '.dsub-tag-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;border-style:solid;border-width:1px}' +
78
- '.dsub-tag-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;border-style:solid;border-width:1px}' +
79
- '.dsub-tag-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;border-style:solid;border-width:1px}' +
80
- '.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
81
- '.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
82
- '.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
83
- '.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
84
- '.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
85
- '.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
86
- '.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
87
- '.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
88
- '.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}' +
89
- '.dsub-brandCodex{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)}' +
90
- '.dsub-brandClaude{background:rgba(245,158,11,.15);color:#f59e0b;border:1px solid rgba(245,158,11,.3)}' +
91
- '.dsub-brandGrok{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3)}' +
92
- '.dsub-brandAgy{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3)}' +
93
- '.dsub-brandOllama{background:rgba(107,114,128,.15);color:#9ca3af;border:1px solid rgba(107,114,128,.3)}' +
94
- '.dsub-brandKimi{background:rgba(236,72,153,.15);color:#f472b6;border:1px solid rgba(236,72,153,.3)}' +
95
- '.dsub-brandGlm{background:rgba(20,184,166,.15);color:#2dd4bf;border:1px solid rgba(20,184,166,.3)}' +
96
- '.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:rgba(99,102,241,.15);color:#818cf8;border:1px solid rgba(99,102,241,.3);margin-left:6px}' +
97
-
98
- '.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
99
- '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
100
- '.dsub-accName{font-size:13px;font-weight:600}' +
101
- '.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
102
- '.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}' +
103
- '.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary)}' +
104
- '.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
105
- '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
106
- '.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}' +
107
- '.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary,#3b82f6);color:#fff;border:0;cursor:pointer;transition:opacity .15s}' +
108
- '.dsub-btnPrimary:hover{opacity:.9}' +
109
- '.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}' +
110
- '.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}' +
111
- '.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}' +
112
- '.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
113
- '.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}' +
114
- '.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warning-primary)}' +
115
- '.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}' +
116
- '.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}' +
117
- '.dsub-diag{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}'
118
-
119
- const cssId = 'dsh-subscriptions/settings.module.css'