@goodandready/dsh-subscriptions 0.6.3 → 0.6.5

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
@@ -12,6 +12,8 @@ export function normalizeSlots(slots) {
12
12
  const out = []
13
13
  const seen = new Set()
14
14
  for (const slot of Array.isArray(slots) ? slots : []) {
15
+ // User-editable config: a null or non-object entry must be skipped, not crash the plugin.
16
+ if (!slot || typeof slot !== 'object') continue
15
17
  if (!isProvider(slot.provider)) continue
16
18
  const index = Number(slot.index)
17
19
  if (!Number.isInteger(index) || index < 1) continue
@@ -105,10 +107,14 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
105
107
  quarantines.delete(ref)
106
108
  writeQueues.delete(ref)
107
109
  for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
108
- for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
109
110
  refreshFailures.delete(ref)
110
111
  usage.delete(ref)
111
112
  usageFetched.delete(ref)
113
+ // Counters and health must not outlive the account, otherwise a
114
+ // re-added slot inherits stale rotation statistics.
115
+ requestCounts.delete(ref)
116
+ health.delete(ref)
117
+ windows.delete(ref)
112
118
  }
113
119
 
114
120
  async function describeRef(ref) {
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}' +
@@ -310,6 +314,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
310
314
  '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
315
  'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
312
316
  '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.',
317
+ 'closeModal': 'Close',
318
+ 'groupAuth': 'Sign-in and redirect',
319
+ 'groupPrivacy': 'Privacy and model list',
320
+ 'groupRotation': 'Rotation and limits',
321
+ 'groupProvider': 'Provider behaviour',
322
+ 'groupFallback': 'Local fallback',
313
323
  'privacyMask': 'Mask emails and account identifiers',
314
324
  'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
315
325
  'autoLoopback': 'Auto Loopback',
@@ -539,6 +549,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
539
549
  function badgeFor(account, t) {
540
550
  if (!account || !account.configured) return t('notConnected')
541
551
  if (account.validationUrl) return t('verifyAccount')
552
+ // #303: a quarantined account says why it is parked.
553
+ if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
554
+ return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
555
+ }
542
556
  if (account.cooldownUntil && account.cooldownUntil > Date.now()) return t('coolingDown')
543
557
  if (account.usagePercent != null && account.usagePercent >= 100) return t('usageFull')
544
558
  return t('connected')
@@ -862,6 +876,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
862
876
  React.createElement('div', { className: 'dsub-h' }, t('title')),
863
877
  React.createElement('div', { className: 'dsub-sub' },
864
878
  t('intro')),
879
+ React.createElement('details', { className: 'dsub-group', open: true },
880
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupAuth')),
865
881
  React.createElement('label', { className: 'dsub-row' },
866
882
  React.createElement('input', {
867
883
  type: 'checkbox',
@@ -875,23 +891,26 @@ const cssId = 'dsh-subscriptions/settings.module.css'
875
891
  React.createElement('label', { className: 'dsub-row' },
876
892
  React.createElement('input', {
877
893
  type: 'checkbox',
878
- checked: !!draft.privacyMask,
879
- onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
894
+ checked: !!draft.autoLoopback,
895
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
880
896
  }),
881
- React.createElement('span', null, t('privacyMask')),
897
+ React.createElement('span', null, t('autoLoopback')),
882
898
  ),
883
899
  React.createElement('div', { className: 'dsub-sub' },
884
- t('privacyMaskHint')),
900
+ t('autoLoopbackHint')),
901
+ ),
902
+ React.createElement('details', { className: 'dsub-group', open: true },
903
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupPrivacy')),
885
904
  React.createElement('label', { className: 'dsub-row' },
886
905
  React.createElement('input', {
887
906
  type: 'checkbox',
888
- checked: !!draft.autoLoopback,
889
- onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
907
+ checked: !!draft.privacyMask,
908
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
890
909
  }),
891
- React.createElement('span', null, t('autoLoopback')),
910
+ React.createElement('span', null, t('privacyMask')),
892
911
  ),
893
912
  React.createElement('div', { className: 'dsub-sub' },
894
- t('autoLoopbackHint')),
913
+ t('privacyMaskHint')),
895
914
  React.createElement('label', { className: 'dsub-row' },
896
915
  React.createElement('input', {
897
916
  type: 'checkbox',
@@ -902,6 +921,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
902
921
  ),
903
922
  React.createElement('div', { className: 'dsub-sub' },
904
923
  t('hideDeprecatedHint')),
924
+ ),
925
+ React.createElement('details', { className: 'dsub-group', open: true },
926
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupRotation')),
905
927
  React.createElement('div', { className: 'dsub-row' },
906
928
  React.createElement('span', { className: 'dsub-h' }, t('composerQuota')),
907
929
  React.createElement('select', {
@@ -931,6 +953,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
931
953
  ),
932
954
  React.createElement('div', { className: 'dsub-sub' },
933
955
  t('expiryNotifyDaysHint')),
956
+ ),
957
+ React.createElement('details', { className: 'dsub-group', open: true },
958
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupProvider')),
934
959
  React.createElement('label', { className: 'dsub-row' },
935
960
  React.createElement('input', {
936
961
  type: 'checkbox',
@@ -956,6 +981,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
956
981
  ),
957
982
  React.createElement('div', { className: 'dsub-sub' },
958
983
  t('codexVerbosityHint')),
984
+ ),
985
+ React.createElement('details', { className: 'dsub-group', open: true },
986
+ React.createElement('summary', { className: 'dsub-groupHead' }, t('groupFallback')),
959
987
  React.createElement('label', { className: 'dsub-row' },
960
988
  React.createElement('input', {
961
989
  type: 'checkbox',
@@ -982,6 +1010,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
982
1010
  onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallbackModel: e.target.value })),
983
1011
  }),
984
1012
  ) : null,
1013
+ ),
985
1014
  React.createElement('details', { className: 'dsub-details' },
986
1015
  React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
987
1016
  React.createElement('div', { className: 'dsub-block' },
@@ -1691,15 +1720,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1691
1720
  const rows = state.accounts.map((a, i) => {
1692
1721
  const isLogged = state.logged.includes(a.provider)
1693
1722
  const cooled = a.cooldownUntil && a.cooldownUntil > now
1723
+ const quarantined = Boolean(a.quarantineReason) && Number(a.quarantineUntil || 0) > now
1694
1724
  const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
1695
1725
  const b = brandBadge(a.provider)
1696
1726
  const isActiveThis = state.active && state.active.provider === a.provider && state.active.index === a.index
1697
1727
 
1698
1728
  let statusClass = 'dsub-statusOff'
1699
1729
  let statusText = t('subsNotLogged')
1700
- if (cooled) {
1730
+ if (quarantined) {
1731
+ statusClass = 'dsub-statusBad'
1732
+ statusText = a.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(a.quarantineReason)
1733
+ } else if (cooled) {
1701
1734
  statusClass = 'dsub-statusWarn'
1702
- statusText = t('subsCooldown')
1735
+ // Only some model families may be cooled down - say which ones.
1736
+ statusText = t('subsCooldown') + (Array.isArray(a.cooldownFamilies) && a.cooldownFamilies.length ? ' · ' + a.cooldownFamilies.join('/') : '')
1703
1737
  } else if (isLogged) {
1704
1738
  if (pct >= 90) { statusClass = 'dsub-statusBad'; statusText = '90%+ ' + t('subsUsed') }
1705
1739
  else if (pct >= 50) { statusClass = 'dsub-statusWarn'; statusText = pct + '% ' + t('subsUsed') }
@@ -1715,7 +1749,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1715
1749
  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
1750
  a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
1717
1751
  ),
1718
- React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
1752
+ React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass, role: 'status', 'aria-live': 'polite' }, statusText),
1719
1753
  ),
1720
1754
  (isLogged && pct != null) ? React.createElement('div', { className: 'dsub-barTrack', style: { marginTop: 4, height: 4 } },
1721
1755
  React.createElement('div', {
@@ -1746,7 +1780,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1746
1780
  className: 'dsub-modalWrap',
1747
1781
  onClick: (e) => { if (e.target === e.currentTarget) setState((s) => Object.assign({}, s, { open: false })) },
1748
1782
  },
1749
- React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-label': t('subsModalTitle') },
1783
+ React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('subsModalTitle') },
1750
1784
  React.createElement('div', { className: 'dsub-modalHead' },
1751
1785
  React.createElement('div', { className: 'dsub-modalTitleWrap' },
1752
1786
  React.createElement('div', { className: 'dsub-modalIcon' }, '⚡'),
@@ -1765,6 +1799,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1765
1799
  React.createElement('button', {
1766
1800
  type: 'button',
1767
1801
  className: 'dsub-mini',
1802
+ autoFocus: true,
1803
+ 'aria-label': t('closeModal'),
1768
1804
  onClick: () => setState((s) => Object.assign({}, s, { open: false })),
1769
1805
  }, '✕'),
1770
1806
  ),
package/lib/http.js CHANGED
@@ -12,7 +12,7 @@ export function writeHtml(res, code, html) {
12
12
  } catch { /* socket closed */ }
13
13
  }
14
14
 
15
- export function readBody(req, maxBytes) {
15
+ export function readBody(req, maxBytes = 64 * 1024) {
16
16
  return new Promise((resolve, reject) => {
17
17
  const chunks = []
18
18
  let size = 0
@@ -30,7 +30,10 @@ export function readBody(req, maxBytes) {
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
  }
@@ -4,8 +4,12 @@ import { join } from 'node:path'
4
4
  import { readClaudeCredentials } from './vendors/claude-cli.js'
5
5
  import { discoverCodingAgentConfigs } from './importers/coding-agents.js'
6
6
 
7
+ // Test seam: a fixture root overrides the real home for the duration of a
8
+ // public call. Undefined (the default) keeps production behaviour.
9
+ let activeHome = null
10
+
7
11
  function homeFile(...parts) {
8
- return join(homedir(), ...parts)
12
+ return join(activeHome || homedir(), ...parts)
9
13
  }
10
14
 
11
15
  async function readJson(path) {
@@ -17,7 +21,17 @@ async function readJson(path) {
17
21
  }
18
22
  }
19
23
 
20
- export async function discoverLocalCliSessions() {
24
+ export async function discoverLocalCliSessions(options = {}) {
25
+ const prevHome = activeHome
26
+ activeHome = options && options.home ? String(options.home) : null
27
+ try {
28
+ return await discoverLocalCliSessionsInner()
29
+ } finally {
30
+ activeHome = prevHome
31
+ }
32
+ }
33
+
34
+ async function discoverLocalCliSessionsInner() {
21
35
  const detected = {}
22
36
 
23
37
  // 1. Codex CLI
@@ -144,7 +158,7 @@ export async function discoverLocalCliSessions() {
144
158
  }
145
159
 
146
160
  // 8. Claude CLI Direct Import
147
- const claudeCreds = await readClaudeCredentials()
161
+ const claudeCreds = await readClaudeCredentials(activeHome)
148
162
  if (claudeCreds) {
149
163
  detected['claude-cli'] = {
150
164
  provider: 'claude',
@@ -186,13 +200,13 @@ export async function discoverLocalCliSessions() {
186
200
  return detected
187
201
  }
188
202
 
189
- export async function loadLocalCliBlob(provider) {
190
- const discovered = await discoverLocalCliSessions()
203
+ export async function loadLocalCliBlob(provider, options = {}) {
204
+ const discovered = await discoverLocalCliSessions(options)
191
205
  const info = discovered[provider]
192
206
  if (!info) throw new Error(`no local CLI session found for ${provider}`)
193
207
 
194
208
  if (provider === 'claude-cli' || (provider === 'claude' && info.provider === 'claude')) {
195
- const creds = await readClaudeCredentials()
209
+ const creds = await readClaudeCredentials(options && options.home)
196
210
  if (!creds) throw new Error('Claude CLI credentials not found')
197
211
  return {
198
212
  accessToken: creds.accessToken,
package/lib/index.js CHANGED
@@ -141,7 +141,7 @@ export function apply(ctx, config) {
141
141
  if (!cfg.ollamaFallback || !models.length) throw err
142
142
  const model = cfg.ollamaFallbackModel || models[0].id
143
143
  try { ctx.emit && ctx.emit('subscriptions.ollama-fallback', { provider, model, reason: err && err.code || 'EXHAUSTED' }) } catch {}
144
- try { ctx.log && ctx.log.warn && ctx.log.warn(`[dsh-subscriptions] ${provider}: all accounts exhausted (${err && err.code || 'EXHAUSTED'}), откат на ollama/${model}`) } catch {}
144
+ try { ctx.log && ctx.log.warn && ctx.log.warn(`[dsh-subscriptions] ${provider}: all accounts exhausted (${err && err.code || 'EXHAUSTED'}), falling back to ollama/${model}`) } catch {}
145
145
  try {
146
146
  recordHistory({
147
147
  provider: 'ollama',
@@ -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
- }
@@ -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/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
@@ -17,7 +17,6 @@ export function registerAccountsRoutes(ctx, state) {
17
17
  refreshModels,
18
18
  refForSlot,
19
19
  resetCredits,
20
- subscriptions,
21
20
  } = state
22
21
 
23
22
 
@@ -19,7 +19,6 @@ export function registerOauthRoutes(ctx, state) {
19
19
  completeOAuth,
20
20
  fetchForRef,
21
21
  sweepPending,
22
- subscriptions,
23
22
  pmL,
24
23
  } = state
25
24
 
@@ -16,7 +16,6 @@ export function registerStatusRoutes(ctx, state) {
16
16
  store,
17
17
  history,
18
18
  diagnosticsReport,
19
- subscriptions,
20
19
  pmL,
21
20
  pmE,
22
21
  } = state
package/lib/routes.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { publicConfig } from './config-schema.js'
2
2
  import { PROVIDERS, displayName } from './refs.js'
3
- import { writeHtml, writeJson } from './http.js'
3
+ import { escapeHtml, writeHtml, writeJson } from './http.js'
4
4
  import { registerStatusRoutes } from './routes/status.js'
5
5
  import { registerOauthRoutes } from './routes/oauth.js'
6
6
  import { registerAccountsRoutes } from './routes/accounts.js'
@@ -11,7 +11,6 @@ export function registerRoutes(ctx, state) {
11
11
  accountsView,
12
12
  history,
13
13
  live,
14
- subscriptions,
15
14
  } = state
16
15
 
17
16
  async function configResponse() {
@@ -101,10 +100,3 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&amp;').replace(/</g,'
101
100
  registerAccountsRoutes(ctx, state)
102
101
  registerProxyRoutes(ctx, state)
103
102
  }
104
-
105
- export function escapeHtml(text) {
106
- return String(text)
107
- .replace(/&/g, '&amp;')
108
- .replace(/</g, '&lt;')
109
- .replace(/>/g, '&gt;')
110
- }
@@ -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,
@@ -2,8 +2,12 @@ import { readFile } from 'node:fs/promises'
2
2
  import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
 
5
- export async function readClaudeCredentials() {
6
- const p = join(homedir(), '.claude', 'credentials.json')
5
+ /**
6
+ * Read Claude Code CLI credentials. `home` is a test seam: when omitted the
7
+ * real user home is used, so production behaviour is unchanged.
8
+ */
9
+ export async function readClaudeCredentials(home) {
10
+ const p = join(home || homedir(), '.claude', 'credentials.json')
7
11
  try {
8
12
  const text = await readFile(p, 'utf8')
9
13
  const json = JSON.parse(text)
@@ -16,6 +16,11 @@ export const CODY_MODELS = [
16
16
  }
17
17
  ]
18
18
 
19
+ /** Catalog id -> Sourcegraph model name. */
20
+ export const CODY_MODEL_ALIASES = {
21
+ 'cody-claude-3.5-sonnet': 'claude-3-5-sonnet',
22
+ }
23
+
19
24
  export function providerInfo() {
20
25
  return { id, name: 'Sourcegraph Cody' }
21
26
  }
@@ -50,8 +55,14 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
50
55
  if (!token) throw new LlmError('Sourcegraph Cody not authenticated', 'AUTH')
51
56
 
52
57
  const url = `${(config && config.apiBase) || CODY_API_BASE}/chat/completions`
58
+ // Catalog ids carry a cody- prefix and dotted versions; the Sourcegraph
59
+ // API expects the bare dashed name (cody-claude-3.5-sonnet -> claude-3-5-sonnet).
60
+ const requested = String(options.model || '').trim()
61
+ const mapped = CODY_MODEL_ALIASES[requested]
62
+ || requested.replace(/^cody-/, '').replace(/\./g, '-')
63
+ || 'claude-3-5-sonnet'
53
64
  const body = {
54
- model: 'claude-3-5-sonnet',
65
+ model: mapped,
55
66
  messages: openaiMessages(options),
56
67
  stream: true
57
68
  }
@@ -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.3",
3
+ "version": "0.6.5",
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",
@@ -35,7 +35,8 @@
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
37
  "scripts": {
38
- "test": "eslint lib/ && node --test test/*.test.mjs"
38
+ "test": "eslint lib/ && node --test test/*.test.mjs",
39
+ "typecheck": "tsc -p tsconfig.checkjs.json"
39
40
  },
40
41
  "dsh": {
41
42
  "bundle": {
@@ -62,6 +63,8 @@
62
63
  "undici": "^8.10.1"
63
64
  },
64
65
  "devDependencies": {
65
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8"
66
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
67
+ "@types/node": "^22.20.2",
68
+ "typescript": "^7.0.2"
66
69
  }
67
70
  }