@goodandready/dsh-subscriptions 0.7.21 → 0.7.23

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/alerts.js CHANGED
@@ -56,11 +56,19 @@ export function toNotifyPayload(body) {
56
56
  }
57
57
  const DEDUP_WINDOW_MS = 10 * 60 * 1000 // 10 minutes cooldown per (ref + type)
58
58
 
59
+ /**
60
+ * Last billing status announced per account. A condition that simply
61
+ * persists — the provider page still says `past_due` — is news the first
62
+ * time and noise on every later dedupe window.
63
+ */
64
+ const lastBillingStatus = new Map()
65
+
59
66
  const alertBuffer = []
60
67
  const lastAlertTime = new Map()
61
68
 
62
69
  export function clearAlerts() {
63
70
  alertBuffer.length = 0
71
+ lastBillingStatus.clear()
64
72
  lastAlertTime.clear()
65
73
  }
66
74
 
@@ -309,6 +317,11 @@ export function notifyBillingAlert({
309
317
  ? `Billing alert: ${provider || 'Account'} (${ref || 'slot'}) has payment issue (${status}).`
310
318
  : `Subscription renewal alert: ${provider || 'Account'} (${ref || 'slot'}) renewal is overdue.`
311
319
 
320
+ // Announce a billing state once, and re-arm when it clears.
321
+ const billingKey = `${provider || "account"}:${ref || "slot"}`
322
+ if (lastBillingStatus.get(billingKey) === status) return null
323
+ lastBillingStatus.set(billingKey, status)
324
+
312
325
  const alert = recordAlert({
313
326
  type: 'billing_status',
314
327
  ref,
package/lib/client.css CHANGED
@@ -5,3 +5,5 @@
5
5
  .dsub-tabs{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}.dsub-tab{appearance:none;font:inherit;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:5px 12px}.dsub-tab:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}.dsub-tabActive{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground)}.dsub-wrap[data-tab=overview]>:not([data-pane=overview]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap),.dsub-wrap[data-tab=accounts]>:not([data-pane=accounts]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap),.dsub-wrap[data-tab=routing]>:not([data-pane=routing]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap),.dsub-wrap[data-tab=models]>:not([data-pane=models]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap),.dsub-wrap[data-tab=vault]>:not([data-pane=vault]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap),.dsub-wrap[data-tab=notifications]>:not([data-pane=notifications]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap),.dsub-wrap[data-tab=advanced]>:not([data-pane=advanced]):not(.dsub-header-bar):not(.dsub-tabs):not(.dsub-foot):not(.dsub-modalWrap){display:none}
6
6
 
7
7
  .dsub-wrap[data-tab=overview]>[data-pane]:not([data-pane=overview]),.dsub-wrap[data-tab=accounts]>[data-pane]:not([data-pane=accounts]),.dsub-wrap[data-tab=routing]>[data-pane]:not([data-pane=routing]),.dsub-wrap[data-tab=models]>[data-pane]:not([data-pane=models]),.dsub-wrap[data-tab=vault]>[data-pane]:not([data-pane=vault]),.dsub-wrap[data-tab=notifications]>[data-pane]:not([data-pane=notifications]),.dsub-wrap[data-tab=advanced]>[data-pane]:not([data-pane=advanced]),.dsub-wrap[data-tab=overview] [data-pane]:not([data-pane=overview]),.dsub-wrap[data-tab=accounts] [data-pane]:not([data-pane=accounts]),.dsub-wrap[data-tab=routing] [data-pane]:not([data-pane=routing]),.dsub-wrap[data-tab=models] [data-pane]:not([data-pane=models]),.dsub-wrap[data-tab=vault] [data-pane]:not([data-pane=vault]),.dsub-wrap[data-tab=notifications] [data-pane]:not([data-pane=notifications]),.dsub-wrap[data-tab=advanced] [data-pane]:not([data-pane=advanced]),.dsub-wrap[data-tab=overview]>.dsub-block:not([data-pane]),.dsub-wrap[data-tab=accounts]>.dsub-block:not([data-pane]){display:none}
8
+
9
+ .dsub-accountCardAction{cursor:pointer}.dsub-accountCardAction:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
package/lib/client.js CHANGED
@@ -3585,6 +3585,7 @@ h('div', { className: 'dsub-row' },
3585
3585
  const [state, setState] = React.useState({ logged: [], usage: 0, accounts: [], open: false, active: null, loading: false })
3586
3586
  const [toasts, setToasts] = React.useState([])
3587
3587
  const lastAlertTsRef = React.useRef(Date.now() - 30000)
3588
+ const lastErrorRef = React.useRef({ text: '', at: 0 })
3588
3589
 
3589
3590
  const dismissToast = (id) => {
3590
3591
  setToasts((prev) => prev.filter((t) => t.id !== id))
@@ -3592,11 +3593,23 @@ h('div', { className: 'dsub-row' },
3592
3593
  const reportError = (message) => {
3593
3594
  const text = cleanErrorMessage(message)
3594
3595
  if (!text) return
3596
+ const now = Date.now()
3597
+ const last = lastErrorRef.current
3598
+ if (last.text === text && now - last.at < 60000) return
3599
+ lastErrorRef.current = { text: text, at: now }
3600
+ const id = 'err_' + now
3595
3601
  setToasts((prev) => prev.some((toast) => toast.message === text)
3596
3602
  ? prev
3597
- : [{ id: 'err_' + Date.now(), message: text, cls: 'dsub-toast-bad' }, ...prev].slice(0, 3))
3603
+ : [{ id: id, message: text, cls: 'dsub-toast-bad' }, ...prev].slice(0, 3))
3604
+ setTimeout(() => dismissToast(id), 12000)
3598
3605
  }
3599
3606
 
3607
+ const hostDown = (() => { let last = 0; return (msg) => { const now = Date.now(); if (now - last < 120000) return; last = now; reportError(msg) } })()
3608
+ const fetchConfig = (attempt) => fetch('/dsh-subscriptions/config', { cache: 'no-store' }).catch((e) => {
3609
+ if ((attempt || 0) < 1) return new Promise((res) => setTimeout(res, 700)).then(() => fetchConfig((attempt || 0) + 1))
3610
+ throw e
3611
+ })
3612
+
3600
3613
  const pollAlerts = () => {
3601
3614
  fetch('/dsh-subscriptions/alerts?limit=5', { cache: 'no-store' })
3602
3615
  .then((res) => res.json())
@@ -3615,25 +3628,25 @@ h('div', { className: 'dsub-row' },
3615
3628
  }
3616
3629
  }
3617
3630
  })
3618
- .catch((e) => reportError(t('hostOffline') + ': ' + String(e && e.message || e)))
3631
+ .catch((e) => hostDown(t('hostOffline') + ': ' + String(e && e.message || e)))
3619
3632
  }
3620
3633
 
3621
3634
  const pull = () => {
3622
3635
  setState((s) => Object.assign({}, s, { loading: true }))
3623
3636
  refreshLoggedIn().then((r) => {
3624
- if (r.error) reportError(t('hostOffline') + ': ' + r.error)
3637
+ if (r.error) hostDown(t('hostOffline') + ': ' + r.error)
3625
3638
  const logged = r.logged || []
3626
- fetch('/dsh-subscriptions/config', { cache: 'no-store' })
3639
+ fetchConfig(0)
3627
3640
  .then((res) => res.json())
3628
3641
  .then((cfg) => {
3629
3642
  setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: (cfg && cfg.accounts) || [], active: r.active, loading: false }))
3630
3643
  })
3631
3644
  .catch((e) => {
3632
- reportError(t('hostOffline') + ': ' + String(e && e.message || e))
3645
+ hostDown(t('hostOffline') + ': ' + String(e && e.message || e))
3633
3646
  setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: [], active: r.active, loading: false }))
3634
3647
  })
3635
3648
  }).catch((e) => {
3636
- reportError(t('hostOffline') + ': ' + String(e && e.message || e))
3649
+ hostDown(t('hostOffline') + ': ' + String(e && e.message || e))
3637
3650
  setState((s) => Object.assign({}, s, { loading: false }))
3638
3651
  })
3639
3652
  }
@@ -3747,6 +3760,25 @@ h('div', { className: 'dsub-row' },
3747
3760
  }, [state.accounts, now])
3748
3761
 
3749
3762
  // Accounts list
3763
+ const openSettings = () => {
3764
+ setState((s) => Object.assign({}, s, { open: false }))
3765
+ const clientCtx = props.ctx || window.__dshCtx
3766
+
3767
+ try {
3768
+ const nav = (clientCtx && clientCtx.pluginNavigation) || (window.__dshCtx && window.__dshCtx.pluginNavigation)
3769
+ if (nav && typeof nav.openBundle === 'function') {
3770
+ nav.openBundle(PKG)
3771
+ }
3772
+ } catch { /* bestEffort */ }
3773
+
3774
+ try {
3775
+ const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
3776
+ if (layout && typeof layout.selectPanel === 'function') {
3777
+ layout.selectPanel('plugins')
3778
+ }
3779
+ } catch { /* bestEffort */ }
3780
+ }
3781
+
3750
3782
  const rows = state.accounts.map((a, i) => {
3751
3783
  const isLogged = state.logged.includes(a.provider)
3752
3784
  const cooled = a.cooldownUntil && a.cooldownUntil > now
@@ -3771,7 +3803,20 @@ h('div', { className: 'dsub-row' },
3771
3803
  else { statusClass = 'dsub-statusOk'; statusText = (pct != null ? pct + '% · ' : '') + t('subsLogged') }
3772
3804
  }
3773
3805
 
3774
- return h('div', { className: 'dsub-accountCard', key: i },
3806
+ return h('div', {
3807
+ className: 'dsub-accountCard dsub-accountCardAction',
3808
+ key: i,
3809
+ role: 'button',
3810
+ tabIndex: 0,
3811
+ title: t('subsOpenSettings'),
3812
+ onClick: openSettings,
3813
+ onKeyDown: (e) => {
3814
+ if (e.key === 'Enter' || e.key === ' ') {
3815
+ e.preventDefault()
3816
+ openSettings()
3817
+ }
3818
+ },
3819
+ },
3775
3820
  h('div', { className: 'dsub-brandBadge ' + b.cls }, b.icon),
3776
3821
  h('div', { className: 'dsub-accInfo' },
3777
3822
  h('div', { className: 'dsub-accNameRow' },
@@ -3856,22 +3901,7 @@ h('div', { className: 'dsub-row' },
3856
3901
  href: '#',
3857
3902
  onClick: (e) => {
3858
3903
  e.preventDefault()
3859
- setState((s) => Object.assign({}, s, { open: false }))
3860
- const clientCtx = props.ctx || window.__dshCtx
3861
-
3862
- try {
3863
- const nav = (clientCtx && clientCtx.pluginNavigation) || (window.__dshCtx && window.__dshCtx.pluginNavigation)
3864
- if (nav && typeof nav.openBundle === 'function') {
3865
- nav.openBundle(PKG)
3866
- }
3867
- } catch { /* bestEffort */ }
3868
-
3869
- try {
3870
- const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
3871
- if (layout && typeof layout.selectPanel === 'function') {
3872
- layout.selectPanel('plugins')
3873
- }
3874
- } catch { /* bestEffort */ }
3904
+ openSettings()
3875
3905
  },
3876
3906
  }, t('subsOpenSettings')),
3877
3907
  ),
@@ -255,7 +255,7 @@ async function discoverLocalCliSessionsInner() {
255
255
  const claudeEntry = {
256
256
  provider: 'claude',
257
257
  source: 'file',
258
- path: homeFile('.claude', 'credentials.json'),
258
+ path: claudeCreds.sourcePath || homeFile('.claude', '.credentials.json'),
259
259
  email: claudeCreds.email || 'Claude Code CLI User',
260
260
  hasRefreshToken: !!claudeCreds.refreshToken,
261
261
  }
@@ -17,12 +17,12 @@ export const INSTRUCTIONS = {
17
17
  title: "Claude Code",
18
18
  stepsEn: [
19
19
  "1. Run \"claude login\" in server console.",
20
- "2. Click \"📥 From CLI\" — plugin will import ~/.claude/credentials.json.",
20
+ "2. Click \"📥 From CLI\" — plugin will import ~/.claude/.credentials.json.",
21
21
  "3. Or paste your Session Token / API Key from Anthropic Console and click \"Save token\".",
22
22
  ],
23
23
  stepsZh: [
24
24
  "1. 在服务器控制台运行 \"claude login\"。",
25
- "2. 点击 \"📥 从 CLI 导入\" — 插件将自动导入 ~/.claude/credentials.json。",
25
+ "2. 点击 \"📥 从 CLI 导入\" — 插件将自动导入 ~/.claude/.credentials.json。",
26
26
  "3. 或者从 Anthropic 控制台复制您的 Session Token 或 API Key 并点击 \"保存令牌\"。",
27
27
  ],
28
28
  link: "https://console.anthropic.com/settings/keys",
package/lib/messages.js CHANGED
@@ -338,7 +338,7 @@ export function anthropicPayload(options, extraSystem) {
338
338
  input: safeJson(call.arguments),
339
339
  })
340
340
  }
341
- messages.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
341
+ messages.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: " " }] })
342
342
  continue
343
343
  }
344
344
  if (message.role === "tool") {
@@ -385,7 +385,7 @@ export function anthropicPayload(options, extraSystem) {
385
385
  content: content || "",
386
386
  })
387
387
  }
388
- messages.push({ role: "user", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
388
+ messages.push({ role: "user", content: blocks.length ? blocks : [{ type: "text", text: " " }] })
389
389
  }
390
390
 
391
391
  const useCacheControl = options.cacheControl !== false
@@ -425,11 +425,26 @@ export function anthropicPayload(options, extraSystem) {
425
425
  const targetIdx = userTurnIndices[userTurnIndices.length - 2]
426
426
  const targetMsg = messages[targetIdx]
427
427
  if (Array.isArray(targetMsg.content) && targetMsg.content.length > 0) {
428
- const lastBlockIdx = targetMsg.content.length - 1
429
- const lastBlock = targetMsg.content[lastBlockIdx]
430
- targetMsg.content[lastBlockIdx] = {
431
- ...lastBlock,
432
- cache_control: { type: "ephemeral" },
428
+ // Anthropic rejects cache_control on an empty text block, so stamp
429
+ // the last block that can carry it: non-empty text, or an image.
430
+ let targetBlockIdx = -1
431
+ for (let i = targetMsg.content.length - 1; i >= 0; i -= 1) {
432
+ const block = targetMsg.content[i]
433
+ if (block === null || block === undefined) continue
434
+ if (block.type === "image") {
435
+ targetBlockIdx = i
436
+ break
437
+ }
438
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim() !== "") {
439
+ targetBlockIdx = i
440
+ break
441
+ }
442
+ }
443
+ if (targetBlockIdx >= 0) {
444
+ targetMsg.content[targetBlockIdx] = {
445
+ ...targetMsg.content[targetBlockIdx],
446
+ cache_control: { type: "ephemeral" },
447
+ }
433
448
  }
434
449
  }
435
450
  }
@@ -442,7 +457,9 @@ export function anthropicPayload(options, extraSystem) {
442
457
  ...(system ? { system } : {}),
443
458
  messages,
444
459
  ...(tools && tools.length ? { tools } : {}),
445
- ...(options.temperature != null ? { temperature: options.temperature } : {}),
460
+ // temperature is deliberately not sent to Anthropic: the API accepts
461
+ // only 1 while extended thinking is enabled, so forwarding the session
462
+ // value turns every other setting into a 400.
446
463
  }
447
464
  }
448
465
 
@@ -3,13 +3,20 @@ import { homedir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
 
5
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.
6
+ * Credential file names Claude Code has used, newest first. Current releases
7
+ * write `.credentials.json` (dotted); the undotted name is kept as a fallback so
8
+ * machines with an older or hand-made file keep working.
8
9
  */
9
- export async function readClaudeCredentials(home) {
10
- const p = join(home || homedir(), '.claude', 'credentials.json')
10
+ export const CLAUDE_CREDENTIAL_FILES = ['.credentials.json', 'credentials.json']
11
+
12
+ /** Absolute candidate paths for the Claude Code OAuth grant, newest first. */
13
+ export function claudeCredentialsPaths(home) {
14
+ return CLAUDE_CREDENTIAL_FILES.map((file) => join(home || homedir(), '.claude', file))
15
+ }
16
+
17
+ async function readGrantFrom(file) {
11
18
  try {
12
- const text = await readFile(p, 'utf8')
19
+ const text = await readFile(file, 'utf8')
13
20
  const json = JSON.parse(text)
14
21
  if (!json) return null
15
22
  const oauth = json.claudeAiOauth || json.oauth || json
@@ -20,6 +27,7 @@ export async function readClaudeCredentials(home) {
20
27
  expiresAt: oauth.expiresAt || (oauth.expires_in ? Date.now() + oauth.expires_in * 1000 : Date.now() + 3600000),
21
28
  subscriptionType: oauth.subscriptionType || 'Claude Code',
22
29
  email: oauth.email || json.email || 'Claude Code CLI User',
30
+ sourcePath: file,
23
31
  }
24
32
  }
25
33
  return null
@@ -27,3 +35,18 @@ export async function readClaudeCredentials(home) {
27
35
  return null
28
36
  }
29
37
  }
38
+
39
+ /**
40
+ * Read Claude Code CLI credentials. `home` is a test seam: when omitted the
41
+ * real user home is used, so production behaviour is unchanged.
42
+ *
43
+ * Candidates are tried newest first, because Claude Code moved from
44
+ * `~/.claude/credentials.json` to the dotted `~/.claude/.credentials.json`.
45
+ */
46
+ export async function readClaudeCredentials(home) {
47
+ for (const file of claudeCredentialsPaths(home)) {
48
+ const grant = await readGrantFrom(file)
49
+ if (grant) return grant
50
+ }
51
+ return null
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.21",
3
+ "version": "0.7.23",
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",