@goodandready/dsh-clinebot 0.5.11 → 0.5.12
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 +8 -0
- package/lib/account-pool.js +17 -3
- package/lib/routes/accounts.js +12 -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,14 @@ 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.12] - 2026-10-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
8
16
|
## [0.5.11] - 2026-10-01
|
|
9
17
|
|
|
10
18
|
### 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/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 {
|
|
@@ -207,10 +210,13 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
207
210
|
if (!freshAccounts.some((a) => a && a.apiKeyEnv === targetEnv)) {
|
|
208
211
|
freshAccounts.push(accountToDelete)
|
|
209
212
|
}
|
|
213
|
+
const shouldRestoreActive = (curr.activeAccount === targetEnv && (!fresh.activeAccount || fresh.activeAccount === targetEnv))
|
|
214
|
+
? targetEnv
|
|
215
|
+
: fresh.activeAccount
|
|
210
216
|
const restoredConfig = plainConfig(Config({
|
|
211
217
|
...fresh,
|
|
212
218
|
accounts: freshAccounts,
|
|
213
|
-
activeAccount:
|
|
219
|
+
activeAccount: shouldRestoreActive,
|
|
214
220
|
}))
|
|
215
221
|
await settingsApi.replace(restoredConfig)
|
|
216
222
|
await syncProviderState(restoredConfig)
|
|
@@ -229,6 +235,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
229
235
|
})
|
|
230
236
|
}
|
|
231
237
|
|
|
238
|
+
pendingSecretCleanupAccounts.add(targetEnv)
|
|
232
239
|
return writeJson(res, 500, {
|
|
233
240
|
ok: false,
|
|
234
241
|
partial: true,
|
|
@@ -239,10 +246,11 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
|
|
249
|
+
pendingSecretCleanupAccounts.delete(targetEnv)
|
|
242
250
|
clearUsageCache()
|
|
243
251
|
clearProbeCache()
|
|
244
252
|
|
|
245
|
-
writeJson(res, 200, { ok: true, removed: targetEnv })
|
|
253
|
+
writeJson(res, 200, { ok: true, removed: targetEnv, secretDeleted: Boolean(body.deleteSecret && delRes?.ok) })
|
|
246
254
|
} catch (err) {
|
|
247
255
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
248
256
|
}
|
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.12",
|
|
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",
|