@goodandready/dsh-clinebot 0.5.11 → 0.5.13
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/CHANGELOG.md +14 -0
- package/lib/account-pool.js +17 -3
- package/lib/index.js +21 -1
- package/lib/routes/accounts.js +9 -4
- package/lib/routes/settings.js +4 -0
- package/lib/slash-command.js +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.13] - 2026-10-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Native Volatile Account Switch Catalog & Plan Reset (Issue #168)**: In `loader/volatile-update`, actively detect when `activeAccount` changes via native Cosmokit volatile settings, resetting `dynamicModels = []`, `planSyncedAt = 0`, and clearing usage and probe caches (`clearUsageCache()`, `clearProbeCache()`) to guarantee stale catalogs from previous accounts are never marked verified.
|
|
12
|
+
- **Account Deletion Rollback Automatic Mode Preservation (Issue #187)**: Ensure surgical rollback of account deletion in `lib/routes/accounts.js` unconditionally preserves live `fresh.activeAccount`, preventing unintended restoration of deleted accounts or overriding concurrent selections of automatic mode (`activeAccount: ""`).
|
|
13
|
+
|
|
14
|
+
## [0.5.12] - 2026-10-02
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Credential Ownership Enforcement on Deletion Retry (Issue #155)**: Reject deletion of unowned or non-existent credentials with HTTP 404 and zero calls to the credentials store, preventing unconfirmed secrets deletion. Retries of previously failed secret cleanups are explicitly tracked in-memory to safely allow recovery without exposing arbitrary secret deletion.
|
|
18
|
+
- **Catalog Lifecycle Across All Account Switch Paths (Issue #168)**: Reset dynamic model catalog (`dynamicModels: []`) and plan synchronization state (`planSyncedAt: 0`) uniformly across slash-command switches (`/cline switch`), automatic account pool rotations, and manual configuration updates, preventing stale or cross-account models from being considered verified.
|
|
19
|
+
- **Rollback Concurrency & Active Account Selection Preservation (Issue #187)**: Ensure transactional account deletion rollback does not overwrite concurrent changes to `activeAccount`. Only restore the deleted account as active if no concurrent change has occurred, preserving external selection alongside other settings.
|
|
20
|
+
- **Test Hardening & Multi-Environment Portability (Issue #173)**: Hardened regression tests to detect surviving descendants across parent exits under genuine failure conditions, updated account deletion tests to assert honest partial state (`partial: true`), and replaced hardcoded path references with dynamic DSH adapter discovery and graceful skips across diverse environments.
|
|
21
|
+
|
|
8
22
|
## [0.5.11] - 2026-10-01
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/lib/account-pool.js
CHANGED
|
@@ -168,10 +168,19 @@ export async function rotateToNextAccount(ctx, cfg, reason = 'rate_limit', setti
|
|
|
168
168
|
// Fallback if all candidates are either exhausted or uncached: pick next in round-robin
|
|
169
169
|
const nextAcc = bestCandidate || candidates[currentIndex % candidates.length] || candidates[0]
|
|
170
170
|
|
|
171
|
+
const prevActive = String(cfg.activeAccount || cfg.apiKeyEnv || "").trim()
|
|
172
|
+
const nextActive = String(nextAcc.apiKeyEnv || "").trim()
|
|
173
|
+
const isDifferent = nextActive !== prevActive
|
|
174
|
+
|
|
171
175
|
let updated = false
|
|
172
176
|
if (settingsApi?.replace) {
|
|
173
177
|
try {
|
|
174
|
-
const next = {
|
|
178
|
+
const next = {
|
|
179
|
+
...toPlainSnapshot(cfg),
|
|
180
|
+
activeAccount: nextAcc.apiKeyEnv,
|
|
181
|
+
dynamicModels: isDifferent ? [] : (cfg.dynamicModels || []),
|
|
182
|
+
planSyncedAt: isDifferent ? 0 : Number(cfg.planSyncedAt || 0),
|
|
183
|
+
}
|
|
175
184
|
await settingsApi.replace(next)
|
|
176
185
|
updated = true
|
|
177
186
|
} catch (err) {
|
|
@@ -181,9 +190,14 @@ export async function rotateToNextAccount(ctx, cfg, reason = 'rate_limit', setti
|
|
|
181
190
|
const settings = (ctx?.get && ctx.get('settings')) || ctx?.settings
|
|
182
191
|
if (settings?.mutate) {
|
|
183
192
|
try {
|
|
184
|
-
|
|
193
|
+
const ops = [
|
|
185
194
|
{ op: 'set', path: ['activeAccount'], value: nextAcc.apiKeyEnv },
|
|
186
|
-
]
|
|
195
|
+
]
|
|
196
|
+
if (isDifferent) {
|
|
197
|
+
ops.push({ op: 'set', path: ['dynamicModels'], value: [] })
|
|
198
|
+
ops.push({ op: 'set', path: ['planSyncedAt'], value: 0 })
|
|
199
|
+
}
|
|
200
|
+
await settings.mutate('dsh-clinebot', ops)
|
|
187
201
|
updated = true
|
|
188
202
|
} catch (err) {
|
|
189
203
|
ctx?.logger?.warn?.('[dsh-clinebot] Failed to persist rotated activeAccount via settings.mutate: ' + (err?.message || err))
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
sessionStats,
|
|
5
5
|
recordSessionRequest,
|
|
6
6
|
resetSessionStats,
|
|
7
|
+
clearUsageCache,
|
|
8
|
+
clearProbeCache,
|
|
7
9
|
} from './cline-client.js'
|
|
8
10
|
import {
|
|
9
11
|
rotateToNextAccount,
|
|
@@ -104,9 +106,27 @@ export function apply(ctx, config) {
|
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
let lastActiveAccount = publicConfig(live()).activeAccount
|
|
107
110
|
if (typeof ctx?.on === 'function') {
|
|
108
111
|
ctx.on('loader/volatile-update', () => {
|
|
109
|
-
|
|
112
|
+
const liveCfg = live()
|
|
113
|
+
const currentActive = publicConfig(liveCfg).activeAccount
|
|
114
|
+
if (currentActive !== lastActiveAccount) {
|
|
115
|
+
lastActiveAccount = currentActive
|
|
116
|
+
try {
|
|
117
|
+
if (currentConfig && typeof currentConfig === 'object') {
|
|
118
|
+
currentConfig.dynamicModels = []
|
|
119
|
+
currentConfig.planSyncedAt = 0
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
/* ignore read-only or frozen */
|
|
123
|
+
}
|
|
124
|
+
overrides.dynamicModels = []
|
|
125
|
+
overrides.planSyncedAt = 0
|
|
126
|
+
clearUsageCache()
|
|
127
|
+
clearProbeCache()
|
|
128
|
+
}
|
|
129
|
+
syncStatsStorage(liveCfg)
|
|
110
130
|
})
|
|
111
131
|
}
|
|
112
132
|
|
package/lib/routes/accounts.js
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
DEFAULT_API_KEY_ENV,
|
|
12
12
|
} from '../cline-client.js'
|
|
13
13
|
|
|
14
|
+
const pendingSecretCleanupAccounts = new Set()
|
|
15
|
+
|
|
14
16
|
export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
15
17
|
// POST /dsh-clinebot/accounts — add or update account in pool
|
|
16
18
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -155,7 +157,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
155
157
|
const existingAccounts = Array.isArray(curr.accounts) ? curr.accounts : []
|
|
156
158
|
const accountToDelete = existingAccounts.find((a) => a && a.apiKeyEnv === targetEnv)
|
|
157
159
|
if (!accountToDelete) {
|
|
158
|
-
if (body.deleteSecret && ctx?.get?.('credentials')) {
|
|
160
|
+
if (body.deleteSecret && pendingSecretCleanupAccounts.has(targetEnv) && ctx?.get?.('credentials')) {
|
|
159
161
|
let retryDelRes = null
|
|
160
162
|
try {
|
|
161
163
|
retryDelRes = await deleteCredentialKey(ctx, targetEnv)
|
|
@@ -163,6 +165,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
163
165
|
retryDelRes = null
|
|
164
166
|
}
|
|
165
167
|
if (retryDelRes && retryDelRes.ok) {
|
|
168
|
+
pendingSecretCleanupAccounts.delete(targetEnv)
|
|
166
169
|
clearUsageCache()
|
|
167
170
|
clearProbeCache()
|
|
168
171
|
return writeJson(res, 200, { ok: true, removed: targetEnv, secretDeleted: true })
|
|
@@ -192,8 +195,8 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
192
195
|
await settingsApi.replace(next)
|
|
193
196
|
await syncProviderState(next)
|
|
194
197
|
|
|
198
|
+
let delRes = null
|
|
195
199
|
if (body.deleteSecret) {
|
|
196
|
-
let delRes = null
|
|
197
200
|
try {
|
|
198
201
|
delRes = await deleteCredentialKey(ctx, targetEnv)
|
|
199
202
|
} catch {
|
|
@@ -210,7 +213,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
210
213
|
const restoredConfig = plainConfig(Config({
|
|
211
214
|
...fresh,
|
|
212
215
|
accounts: freshAccounts,
|
|
213
|
-
activeAccount:
|
|
216
|
+
activeAccount: fresh.activeAccount,
|
|
214
217
|
}))
|
|
215
218
|
await settingsApi.replace(restoredConfig)
|
|
216
219
|
await syncProviderState(restoredConfig)
|
|
@@ -229,6 +232,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
229
232
|
})
|
|
230
233
|
}
|
|
231
234
|
|
|
235
|
+
pendingSecretCleanupAccounts.add(targetEnv)
|
|
232
236
|
return writeJson(res, 500, {
|
|
233
237
|
ok: false,
|
|
234
238
|
partial: true,
|
|
@@ -239,10 +243,11 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
239
243
|
}
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
pendingSecretCleanupAccounts.delete(targetEnv)
|
|
242
247
|
clearUsageCache()
|
|
243
248
|
clearProbeCache()
|
|
244
249
|
|
|
245
|
-
writeJson(res, 200, { ok: true, removed: targetEnv })
|
|
250
|
+
writeJson(res, 200, { ok: true, removed: targetEnv, secretDeleted: Boolean(body.deleteSecret && delRes?.ok) })
|
|
246
251
|
} catch (err) {
|
|
247
252
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
248
253
|
}
|
package/lib/routes/settings.js
CHANGED
|
@@ -93,6 +93,10 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
93
93
|
const base = plainConfig(live()) || {}
|
|
94
94
|
delete base.enabledModels
|
|
95
95
|
const merged = { ...base, ...rawPayload }
|
|
96
|
+
if (rawPayload.activeAccount !== undefined && String(rawPayload.activeAccount).trim() !== String(base.activeAccount || "").trim()) {
|
|
97
|
+
if (!("dynamicModels" in rawPayload)) merged.dynamicModels = []
|
|
98
|
+
if (!("planSyncedAt" in rawPayload)) merged.planSyncedAt = 0
|
|
99
|
+
}
|
|
96
100
|
const parsed = plainConfig(Config(merged))
|
|
97
101
|
await settingsApi.replace(parsed)
|
|
98
102
|
await syncProviderState(parsed)
|
package/lib/slash-command.js
CHANGED
|
@@ -100,7 +100,14 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
100
100
|
clearProbeCache()
|
|
101
101
|
const settingsApi = getSettingsApi()
|
|
102
102
|
if (settingsApi?.replace) {
|
|
103
|
-
const
|
|
103
|
+
const curr = plainConfig(live())
|
|
104
|
+
const isDifferent = String(param || "").trim() !== String(curr.activeAccount || "").trim()
|
|
105
|
+
const next = Config({
|
|
106
|
+
...curr,
|
|
107
|
+
activeAccount: param,
|
|
108
|
+
dynamicModels: isDifferent ? [] : (curr.dynamicModels || []),
|
|
109
|
+
planSyncedAt: isDifferent ? 0 : Number(curr.planSyncedAt || 0),
|
|
110
|
+
})
|
|
104
111
|
await settingsApi.replace(next)
|
|
105
112
|
await syncProviderState(next)
|
|
106
113
|
return `Active account switched to ${param}`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|