@goodandready/dsh-subscriptions 0.6.1 → 0.6.3

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
@@ -195,10 +195,15 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
195
195
  ```
196
196
 
197
197
  > [!IMPORTANT]
198
- > Restart DSH Web UI after installation (`systemctl --user restart dsh-web`) and navigate to **Settings → Subscriptions** to link your accounts.
198
+ > Restart DSH Web UI after installation (`systemctl --user restart dsh-web`) and navigate to **Settings → Plugins → Plugin Settings → Subscriptions** to link your accounts.
199
199
 
200
200
  ---
201
201
 
202
+
203
+ > [!TIP]
204
+ > **UI Settings Card vs YAML Overrides**:
205
+ > All common options (account slots, autoLoopback, privacyMask, composerQuota, expiryNotifyDays, codexFastMode, codexVerbosity, ollamaFallback/baseUrl/model, and cooldown/probe intervals) can be managed directly in the Web UI card (**Settings → Plugins → Plugin Settings → Subscriptions**). Low-level parameters such as OAuth client IDs/redirect URIs, API base URL overrides, and custom vendors are configured in `settings.yaml`.
206
+
202
207
  ## ⚙️ Configuration Reference (`settings.yaml`)
203
208
 
204
209
  ```yaml
package/README.ru.md CHANGED
@@ -172,6 +172,11 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
172
172
 
173
173
  ---
174
174
 
175
+
176
+ > [!TIP]
177
+ > **Карточка настроек в UI vs Настройки в YAML**:
178
+ > Все основные параметры (слоты аккаунтов, перехват autoLoopback, privacyMask, индикатор composerQuota, оповещение expiryNotifyDays, codexFastMode, codexVerbosity, ollamaFallback/baseUrl/model, интервалы cooldown и probe) настраиваются напрямую в Web UI: **«Настройки → Плагины → Настройки плагинов → Subscriptions»**. Низкоуровневые оверрайды (собственные OAuth Client ID / Redirect URI, Base URL для прокси, кастомные вендоры) задаются в `settings.yaml`.
179
+
175
180
  ## ⚙️ Пример конфигурации (`settings.yaml`)
176
181
 
177
182
  ```yaml
package/lib/client.js CHANGED
@@ -103,6 +103,10 @@ window.__ModuleLoader__.load({
103
103
  '.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
104
104
  '.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
105
105
  '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
106
+ '.dsub-select{appearance:none;height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 24px 0 8px;font-size:12px;cursor:pointer}' +
107
+ '.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
+ '.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}' +
109
+ '.dsub-summary:hover{color:var(--dsw-alias-label-primary)}' +
106
110
  '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
107
111
  '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
108
112
  '.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
@@ -308,6 +312,37 @@ const cssId = 'dsh-subscriptions/settings.module.css'
308
312
  '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.',
309
313
  'privacyMask': 'Mask emails and account identifiers',
310
314
  'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
315
+ 'autoLoopback': 'Auto Loopback',
316
+ 'autoLoopbackHint': 'Listen on local callback port to catch OAuth tokens automatically without manual copy-paste.',
317
+ 'hideDeprecated': 'Hide deprecated models',
318
+ 'hideDeprecatedHint': 'Hide older or superseded model variants from model dropdowns.',
319
+ 'composerQuota': 'Composer quota indicator',
320
+ 'composerQuotaHint': 'Display subscription usage indicator directly in the composer toolbar.',
321
+ 'composerQuotaOff': 'Disabled',
322
+ 'composerQuotaPercent': 'Percentage',
323
+ 'composerQuotaBar': 'Progress Bar',
324
+ 'composerQuotaForecast': 'Remaining Forecast',
325
+ 'expiryNotifyDays': 'Expiry warning threshold (days)',
326
+ 'expiryNotifyDaysHint': 'Notify when subscription renewal or token expiration is within this many days (0 to disable).',
327
+ 'codexFastMode': 'Codex fast mode (1.5x)',
328
+ 'codexFastModeHint': 'Enable high-throughput mode for OpenAI Codex models if supported by subscription.',
329
+ 'codexVerbosity': 'Codex streaming verbosity',
330
+ 'codexVerbosityHint': 'Control streaming chunking detail level from the Codex provider.',
331
+ 'codexVerbosityDefault': 'Default',
332
+ 'codexVerbosityLow': 'Low',
333
+ 'codexVerbosityMedium': 'Medium',
334
+ 'codexVerbosityHigh': 'High',
335
+ 'ollamaFallback': 'Ollama local fallback',
336
+ 'ollamaFallbackHint': 'Automatically route queries to a local Ollama instance when cloud quota is exhausted.',
337
+ 'ollamaBaseUrl': 'Ollama Base URL',
338
+ 'ollamaFallbackModel': 'Ollama Fallback Model',
339
+ 'advancedSettings': 'Advanced Settings & Monitoring',
340
+ 'cooldownMs': 'Failure cooldown (minutes)',
341
+ 'cooldownMsHint': 'How long to pause a failing or rate-limited account before retrying.',
342
+ 'probeIntervalMin': 'Health probe interval (minutes)',
343
+ 'probeIntervalMinHint': 'Interval for background checks to refresh account status and quota balance.',
344
+ 'notifyLimits': 'Notify on rate limits',
345
+ 'notifyLimitsHint': 'Show notification alerts when account usage reaches quota ceiling.',
311
346
  'resetCredits': 'Reset credits',
312
347
  'resetCreditsHint': 'ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.',
313
348
  'resetCreditsAction': 'Reset ChatGPT Limit',
@@ -847,6 +882,146 @@ const cssId = 'dsh-subscriptions/settings.module.css'
847
882
  ),
848
883
  React.createElement('div', { className: 'dsub-sub' },
849
884
  t('privacyMaskHint')),
885
+ React.createElement('label', { className: 'dsub-row' },
886
+ React.createElement('input', {
887
+ type: 'checkbox',
888
+ checked: !!draft.autoLoopback,
889
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
890
+ }),
891
+ React.createElement('span', null, t('autoLoopback')),
892
+ ),
893
+ React.createElement('div', { className: 'dsub-sub' },
894
+ t('autoLoopbackHint')),
895
+ React.createElement('label', { className: 'dsub-row' },
896
+ React.createElement('input', {
897
+ type: 'checkbox',
898
+ checked: !!draft.hideDeprecatedModels,
899
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { hideDeprecatedModels: e.target.checked })),
900
+ }),
901
+ React.createElement('span', null, t('hideDeprecated')),
902
+ ),
903
+ React.createElement('div', { className: 'dsub-sub' },
904
+ t('hideDeprecatedHint')),
905
+ React.createElement('div', { className: 'dsub-row' },
906
+ React.createElement('span', { className: 'dsub-h' }, t('composerQuota')),
907
+ React.createElement('select', {
908
+ className: 'dsub-select',
909
+ value: draft.composerQuota || 'percent',
910
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { composerQuota: e.target.value })),
911
+ },
912
+ React.createElement('option', { value: 'off' }, t('composerQuotaOff')),
913
+ React.createElement('option', { value: 'percent' }, t('composerQuotaPercent')),
914
+ React.createElement('option', { value: 'bar' }, t('composerQuotaBar')),
915
+ React.createElement('option', { value: 'forecast' }, t('composerQuotaForecast')),
916
+ ),
917
+ ),
918
+ React.createElement('div', { className: 'dsub-sub' },
919
+ t('composerQuotaHint')),
920
+ React.createElement('div', { className: 'dsub-row' },
921
+ React.createElement('span', { className: 'dsub-h' }, t('expiryNotifyDays')),
922
+ React.createElement('input', {
923
+ type: 'number',
924
+ className: 'dsub-inp',
925
+ style: { width: '80px', minWidth: '80px' },
926
+ min: 0,
927
+ max: 90,
928
+ value: draft.expiryNotifyDays != null ? draft.expiryNotifyDays : 7,
929
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { expiryNotifyDays: Number(e.target.value) || 0 })),
930
+ }),
931
+ ),
932
+ React.createElement('div', { className: 'dsub-sub' },
933
+ t('expiryNotifyDaysHint')),
934
+ React.createElement('label', { className: 'dsub-row' },
935
+ React.createElement('input', {
936
+ type: 'checkbox',
937
+ checked: !!draft.codexFastMode,
938
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { codexFastMode: e.target.checked })),
939
+ }),
940
+ React.createElement('span', null, t('codexFastMode')),
941
+ ),
942
+ React.createElement('div', { className: 'dsub-sub' },
943
+ t('codexFastModeHint')),
944
+ React.createElement('div', { className: 'dsub-row' },
945
+ React.createElement('span', { className: 'dsub-h' }, t('codexVerbosity')),
946
+ React.createElement('select', {
947
+ className: 'dsub-select',
948
+ value: draft.codexVerbosity || 'default',
949
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { codexVerbosity: e.target.value })),
950
+ },
951
+ React.createElement('option', { value: 'default' }, t('codexVerbosityDefault')),
952
+ React.createElement('option', { value: 'low' }, t('codexVerbosityLow')),
953
+ React.createElement('option', { value: 'medium' }, t('codexVerbosityMedium')),
954
+ React.createElement('option', { value: 'high' }, t('codexVerbosityHigh')),
955
+ ),
956
+ ),
957
+ React.createElement('div', { className: 'dsub-sub' },
958
+ t('codexVerbosityHint')),
959
+ React.createElement('label', { className: 'dsub-row' },
960
+ React.createElement('input', {
961
+ type: 'checkbox',
962
+ checked: !!draft.ollamaFallback,
963
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallback: e.target.checked })),
964
+ }),
965
+ React.createElement('span', null, t('ollamaFallback')),
966
+ ),
967
+ React.createElement('div', { className: 'dsub-sub' },
968
+ t('ollamaFallbackHint')),
969
+ draft.ollamaFallback ? React.createElement('div', { className: 'dsub-row' },
970
+ React.createElement('input', {
971
+ type: 'text',
972
+ className: 'dsub-inp dsub-grow',
973
+ placeholder: 'http://localhost:11434',
974
+ value: draft.ollamaBaseUrl || '',
975
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaBaseUrl: e.target.value })),
976
+ }),
977
+ React.createElement('input', {
978
+ type: 'text',
979
+ className: 'dsub-inp dsub-grow',
980
+ placeholder: 'llama3:latest',
981
+ value: draft.ollamaFallbackModel || '',
982
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallbackModel: e.target.value })),
983
+ }),
984
+ ) : null,
985
+ React.createElement('details', { className: 'dsub-details' },
986
+ React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
987
+ React.createElement('div', { className: 'dsub-block' },
988
+ React.createElement('div', { className: 'dsub-row' },
989
+ React.createElement('span', { className: 'dsub-h' }, t('cooldownMs')),
990
+ React.createElement('input', {
991
+ type: 'number',
992
+ className: 'dsub-inp',
993
+ style: { width: '80px', minWidth: '80px' },
994
+ min: 1,
995
+ max: 120,
996
+ value: Math.round((draft.cooldownMs || 600000) / 60000),
997
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { cooldownMs: (Number(e.target.value) || 10) * 60000 })),
998
+ }),
999
+ ),
1000
+ React.createElement('div', { className: 'dsub-sub' }, t('cooldownMsHint')),
1001
+ React.createElement('div', { className: 'dsub-row' },
1002
+ React.createElement('span', { className: 'dsub-h' }, t('probeIntervalMin')),
1003
+ React.createElement('input', {
1004
+ type: 'number',
1005
+ className: 'dsub-inp',
1006
+ style: { width: '80px', minWidth: '80px' },
1007
+ min: 1,
1008
+ max: 1440,
1009
+ value: draft.probeIntervalMin != null ? draft.probeIntervalMin : 15,
1010
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { probeIntervalMin: Number(e.target.value) || 15 })),
1011
+ }),
1012
+ ),
1013
+ React.createElement('div', { className: 'dsub-sub' }, t('probeIntervalMinHint')),
1014
+ React.createElement('label', { className: 'dsub-row' },
1015
+ React.createElement('input', {
1016
+ type: 'checkbox',
1017
+ checked: draft.notifyLimits !== false,
1018
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { notifyLimits: e.target.checked })),
1019
+ }),
1020
+ React.createElement('span', null, t('notifyLimits')),
1021
+ ),
1022
+ React.createElement('div', { className: 'dsub-sub' }, t('notifyLimitsHint')),
1023
+ ),
1024
+ ),
850
1025
  ),
851
1026
  React.createElement('div', { className: 'dsub-block' },
852
1027
  React.createElement('div', { className: 'dsub-row' },
@@ -1258,9 +1433,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1258
1433
  // The canonical place is the Plugins tab: a card with its own header
1259
1434
  // and collapse instead of a row in the side list. The registration key must
1260
1435
  // equal the settings namespace, otherwise the tab silently hides the slot.
1261
- let moved = false
1262
1436
  try {
1263
- moved = !!ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
1437
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
1264
1438
  {
1265
1439
  name: 'settings.plugin.item',
1266
1440
  key: NS,
@@ -1269,27 +1443,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1269
1443
  },
1270
1444
  PluginCard,
1271
1445
  ))
1272
- } catch { moved = false }
1273
- if (moved) return
1274
- // #244: Fallback for harness cores before settings.plugin.item slot was
1275
- // introduced (< 0.1.2-rc.1). Once all production profiles run a core
1276
- // that has settings.plugin.item, this block can be safely removed.
1277
- // Tracked in Gitea #244 — do not remove without verifying minimum core.
1278
- try {
1279
- ctx.slots.inject('settings.section', () => ctx.slots.register(
1280
- {
1281
- name: 'settings.section',
1282
- id: '@goodandready/dsh-subscriptions',
1283
- order: 28,
1284
- locale: NS,
1285
- label: () => t('title'),
1286
- inject: () => ({ ctx: ctx }),
1287
- },
1288
- SubsSection,
1289
- ))
1290
1446
  } catch (e) {
1291
- // Neither slot system available — settings panel will not render.
1292
- // This is expected on headless / non-web profiles.
1447
+ // Slot system not available — expected on headless / non-web profiles.
1293
1448
  }
1294
1449
  }
1295
1450
 
package/lib/ui-styles.js CHANGED
@@ -2,14 +2,14 @@ export const CSS =
2
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
3
  '.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}' +
4
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:14px 16px;display:flex}' +
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
6
  '.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}' +
7
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:15px;font-weight:600;line-height:1.4}' +
8
+ '.dsub-name{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:1.4}' +
9
9
  '.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}' +
10
10
  '.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}' +
11
11
  '.dsub-chevOpen{transform:rotate(180deg)}' +
12
- '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
12
+ '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 20px;padding-bottom:12px}' +
13
13
  '.dsub-block{margin-top:10px}' +
14
14
  '.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
15
15
  '.dsub-grow{flex:1 1 180px;min-width:140px}' +
@@ -25,13 +25,13 @@ export const CSS =
25
25
  '.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
26
26
  '.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
27
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:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
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
29
  '.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
30
- '.dsub-inp{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;min-width:180px}' +
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
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:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
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
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:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
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
35
  '.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
36
36
  '.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
37
37
  '.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}' +
@@ -62,11 +62,11 @@ export const CSS =
62
62
  '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
63
63
  '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
64
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:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
65
+ '.dsub-barTrack{height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
66
66
  '.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
67
- '.dsub-panel-box{margin-top:10px;padding:12px 14px;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}' +
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
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:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
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
70
  '.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
71
71
  '.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
72
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)}' +
@@ -74,9 +74,9 @@ export const CSS =
74
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
75
  '.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
76
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:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
78
- '.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
79
- '.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
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
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
81
  '.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
82
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}' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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",
@@ -1,35 +0,0 @@
1
- import { open, unlink } from 'node:fs/promises'
2
-
3
- export async function withFileLock(filePath, fn, timeoutMs = 5000) {
4
- const lockPath = `${filePath}.lock`
5
- const start = Date.now()
6
- let handle = null
7
-
8
- while (Date.now() - start < timeoutMs) {
9
- try {
10
- handle = await open(lockPath, 'wx')
11
- break
12
- } catch (err) {
13
- if (err.code === 'EEXIST') {
14
- await new Promise((r) => setTimeout(r, 40))
15
- continue
16
- }
17
- throw err
18
- }
19
- }
20
-
21
- if (!handle) {
22
- throw new Error(`Failed to acquire lock on ${lockPath} after ${timeoutMs}ms`)
23
- }
24
-
25
- try {
26
- return await fn()
27
- } finally {
28
- try {
29
- await handle.close()
30
- await unlink(lockPath)
31
- } catch {
32
- // ignore unlock errors
33
- }
34
- }
35
- }
@@ -1,23 +0,0 @@
1
- export function estimateTokens(text) {
2
- if (!text || typeof text !== 'string') return 0
3
- // Average English: ~4 chars per token; CJK/Code: ~1.5 - 2 chars per token
4
- const hasCjk = /[\u4e00-\u9fa5]/.test(text)
5
- const ratio = hasCjk ? 2 : 3.8
6
- return Math.max(1, Math.ceil(text.length / ratio))
7
- }
8
-
9
- export function validateContextLimit(messages, contextWindowLimit = 128000) {
10
- let totalChars = 0
11
- for (const m of messages || []) {
12
- const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')
13
- totalChars += content.length
14
- }
15
- const estimated = estimateTokens(totalChars > 0 ? 'x'.repeat(totalChars) : '')
16
- const safe = estimated <= contextWindowLimit * 0.95
17
- return {
18
- estimatedTokens: estimated,
19
- limit: contextWindowLimit,
20
- isSafe: safe,
21
- marginTokens: Math.max(0, Math.floor(contextWindowLimit * 0.95 - estimated))
22
- }
23
- }
package/lib/revocation.js DELETED
@@ -1,25 +0,0 @@
1
- const REVOKED_ERRORS = new Set([
2
- 'invalid_grant',
3
- 'token_revoked',
4
- 'account_deactivated',
5
- 'session_expired'
6
- ])
7
-
8
- export function isTokenRevokedError(err) {
9
- if (!err) return false
10
- const msg = String(err.message || err.error || err.code || '').toLowerCase()
11
- for (const pattern of REVOKED_ERRORS) {
12
- if (msg.includes(pattern)) return true
13
- }
14
- return false
15
- }
16
-
17
- export function handleAccountRevocation(account) {
18
- return {
19
- ...account,
20
- hasToken: false,
21
- status: 'revoked',
22
- needsReauth: true,
23
- revokedAt: Date.now()
24
- }
25
- }