@goodandready/dsh-subscriptions 0.7.21 → 0.7.22
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 +13 -0
- package/lib/client.css +2 -0
- package/lib/client.js +53 -23
- package/lib/import-auth.js +1 -1
- package/lib/instructions.js +2 -2
- package/lib/messages.js +25 -8
- package/lib/vendors/claude-cli.js +28 -5
- package/package.json +1 -1
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:
|
|
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) =>
|
|
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)
|
|
3637
|
+
if (r.error) hostDown(t('hostOffline') + ': ' + r.error)
|
|
3625
3638
|
const logged = r.logged || []
|
|
3626
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', {
|
|
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
|
-
|
|
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
|
),
|
package/lib/import-auth.js
CHANGED
|
@@ -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
|
}
|
package/lib/instructions.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
10
|
-
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.7.22",
|
|
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",
|