@goodandready/dsh-clinebot 0.5.16 → 0.5.17
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 +10 -0
- package/lib/index.js +5 -9
- package/lib/provider-sync.js +2 -5
- package/lib/routes/proxy.js +24 -21
- package/lib/slash-command.js +4 -4
- package/lib/stats-storage.js +16 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.17] - 2026-10-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Cosmokit Volatile Getters Unwrapping in Live Config (Issues #194, #200)**: Resolved issue where `live()` returned raw Cosmokit `Volatile` `{ get }` object boxes for volatile schema fields, causing downstream boolean checks (such as `!pub.enabled` in `syncProviderState`) to always evaluate as truthy objects regardless of actual boolean value. Fully unwrapped `live()` return values via `plainConfig(Config(plainConfig(...)))` and guaranteed robust fallback defaults when `apply()` receives null/undefined config.
|
|
12
|
+
- **Proxy Body Read Timeout & Payload Size Bounds (Issues #198, #202)**: Switched non-streaming upstream body read timeouts from `streamIdleTimeoutMs` (30s) to `bodyTimeoutMs = Math.max(50, Number(pub.timeoutMs) || 15000)`. Added strict guard against unbounded payload responses (`MAX_BODY_BYTES = 10 * 1024 * 1024`) returning 502 Bad Gateway if upstream response exceeds 10MB.
|
|
13
|
+
- **Dead Configuration Keys & Timeout Cleanup (Issue #197)**: Removed non-existent configuration keys (`streamTotalTimeoutMs`, `connectTimeoutMs`, `idleTimeoutMs`) from `lib/routes/proxy.js` and eliminated dead timer logic.
|
|
14
|
+
- **DSH Non-Existent Service API Calls Removal (Issue #199)**: Removed dead `settings?.get` and `settingsApi.watch` calls on DSH services that lack those methods.
|
|
15
|
+
- **Corrupted Stats Backup Pruning (Issue #201)**: Added automatic rotation and pruning for `.corrupt.*` backup files in `lib/stats-storage.js`, retaining at most the 3 latest diagnostic copies to prevent unbounded disk usage.
|
|
16
|
+
- **Version Literals & Slash Command Cleanup (Issue #202)**: Synchronized hardcoded fallback version literals to `'0.5.17'`, cleaned up unused `invocation` argument in `slash-command.js`, and updated fallback port detection (`ctx?.webServer?.port || ctx?.get?.('webServer')?.port || process.env.PORT || 3080`).
|
|
17
|
+
|
|
8
18
|
## [0.5.16] - 2026-10-04
|
|
9
19
|
|
|
10
20
|
### Fixed
|
package/lib/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export function configReader(config) {
|
|
|
37
37
|
export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
38
38
|
|
|
39
39
|
export function apply(ctx, config) {
|
|
40
|
-
let currentConfig = config
|
|
40
|
+
let currentConfig = config || {}
|
|
41
41
|
const overrides = {}
|
|
42
42
|
const getConfig = () => {
|
|
43
43
|
if (!currentConfig) return currentConfig
|
|
@@ -73,7 +73,7 @@ export function apply(ctx, config) {
|
|
|
73
73
|
}
|
|
74
74
|
return currentConfig
|
|
75
75
|
}
|
|
76
|
-
const live = () => (
|
|
76
|
+
const live = () => plainConfig(Config(plainConfig(getConfig() || config || {})))
|
|
77
77
|
let settingsApi
|
|
78
78
|
|
|
79
79
|
const syncProviderState = async (cfg) => {
|
|
@@ -219,7 +219,7 @@ export function apply(ctx, config) {
|
|
|
219
219
|
applyConfigMutations(currentConfig, parsed)
|
|
220
220
|
syncStatsStorage(live())
|
|
221
221
|
await syncProviderState(live())
|
|
222
|
-
return parsed
|
|
222
|
+
return plainConfig(parsed)
|
|
223
223
|
},
|
|
224
224
|
update: async (patch) => {
|
|
225
225
|
const svc = ctx?.get?.('settings')
|
|
@@ -248,13 +248,9 @@ export function apply(ctx, config) {
|
|
|
248
248
|
applyConfigMutations(currentConfig, merged)
|
|
249
249
|
syncStatsStorage(live())
|
|
250
250
|
await syncProviderState(live())
|
|
251
|
-
return merged
|
|
252
|
-
},
|
|
253
|
-
watch: (cb) => {
|
|
254
|
-
const svc = ctx?.get?.('settings')
|
|
255
|
-
if (typeof svc?.watch === 'function') return svc.watch(cb)
|
|
256
|
-
return () => {}
|
|
251
|
+
return plainConfig(merged)
|
|
257
252
|
},
|
|
253
|
+
|
|
258
254
|
}
|
|
259
255
|
|
|
260
256
|
if (typeof ctx.effect === 'function') {
|
package/lib/provider-sync.js
CHANGED
|
@@ -56,10 +56,7 @@ export async function checkRegisteredInPiAi(ctx) {
|
|
|
56
56
|
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
57
57
|
if (piAiDesc?.value?.providers?.[PROVIDER_ID]) return true
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
const piAi = settings.get(LLM_PI_AI_NS)
|
|
61
|
-
if (piAi?.providers?.[PROVIDER_ID]) return true
|
|
62
|
-
}
|
|
59
|
+
|
|
63
60
|
const llm = ctx?.get?.('llm')
|
|
64
61
|
if (typeof llm?.listProviders === 'function') {
|
|
65
62
|
const list = await llm.listProviders()
|
|
@@ -215,7 +212,7 @@ export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
|
|
|
215
212
|
|
|
216
213
|
if (isProxy) {
|
|
217
214
|
if (!proxyPort) {
|
|
218
|
-
proxyPort = ctx?.webServer?.port || process.env.PORT || 3080
|
|
215
|
+
proxyPort = ctx?.webServer?.port || ctx?.get?.('webServer')?.port || process.env.PORT || 3080
|
|
219
216
|
const isFenced = await detectHostWebServerFence(proxyPort)
|
|
220
217
|
if (isFenced) {
|
|
221
218
|
ctx?.logger?.warn?.(
|
package/lib/routes/proxy.js
CHANGED
|
@@ -24,9 +24,9 @@ import { getAllModels, DEFAULT_MODEL_ID } from '../models.js'
|
|
|
24
24
|
import { getLocalProxyToken } from '../proxy-token.js'
|
|
25
25
|
|
|
26
26
|
const require = createRequire(import.meta.url)
|
|
27
|
-
let packageVersion = '0.5.
|
|
27
|
+
let packageVersion = '0.5.17'
|
|
28
28
|
try {
|
|
29
|
-
packageVersion = require('../../package.json').version || '0.5.
|
|
29
|
+
packageVersion = require('../../package.json').version || '0.5.17'
|
|
30
30
|
} catch (err) {
|
|
31
31
|
/* fallback to default version if manifest is unreadable */
|
|
32
32
|
}
|
|
@@ -161,8 +161,8 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
161
161
|
'User-Agent': USER_AGENT
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const connectTimeoutMs = Math.max(50, Number(pub.
|
|
165
|
-
const streamIdleTimeoutMs = Math.max(50, Number(pub.streamIdleTimeoutMs
|
|
164
|
+
const connectTimeoutMs = Math.max(50, Number(pub.timeoutMs) || 15000)
|
|
165
|
+
const streamIdleTimeoutMs = Math.max(50, Number(pub.streamIdleTimeoutMs) || 30000)
|
|
166
166
|
|
|
167
167
|
const connectAbortCtrl = new AbortController()
|
|
168
168
|
const bodyAbortCtrl = new AbortController()
|
|
@@ -283,16 +283,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
if (pub.streamTotalTimeoutMs && Number(pub.streamTotalTimeoutMs) > 0) {
|
|
288
|
-
const totalMs = Number(pub.streamTotalTimeoutMs)
|
|
289
|
-
totalTimer = setTimeout(() => {
|
|
290
|
-
streamError = new Error(`Stream total timeout after ${totalMs}ms`)
|
|
291
|
-
try {
|
|
292
|
-
reader.cancel('stream total timeout').catch(() => {})
|
|
293
|
-
} catch { /* reader already cancelled or closed */ }
|
|
294
|
-
}, totalMs)
|
|
295
|
-
}
|
|
286
|
+
|
|
296
287
|
|
|
297
288
|
try {
|
|
298
289
|
resetIdleTimer()
|
|
@@ -341,7 +332,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
341
332
|
ctx?.logger?.warn?.('[dsh-clinebot:proxy] Stream read error: ' + (streamErr?.message || streamErr))
|
|
342
333
|
} finally {
|
|
343
334
|
clearIdleTimer()
|
|
344
|
-
|
|
335
|
+
|
|
345
336
|
clientAbortCtrl.signal.removeEventListener('abort', onStreamAbort)
|
|
346
337
|
cleanupClientListeners()
|
|
347
338
|
try {
|
|
@@ -379,17 +370,26 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
379
370
|
return
|
|
380
371
|
}
|
|
381
372
|
|
|
382
|
-
// Non-streaming JSON response with deadline and fail-closed error handling (#185)
|
|
383
|
-
const
|
|
373
|
+
// Non-streaming JSON response with deadline and fail-closed error handling (#185, #198, #202)
|
|
374
|
+
const bodyTimeoutMs = Math.max(50, Number(pub.timeoutMs) || 15000)
|
|
384
375
|
let bodyTimer = null
|
|
385
376
|
const bodyTimeoutPromise = new Promise((_, reject) => {
|
|
386
377
|
bodyTimer = setTimeout(() => {
|
|
387
378
|
bodyTimedOut = true
|
|
388
|
-
bodyAbortCtrl.abort(new Error(`Upstream body read timed out after ${
|
|
389
|
-
reject(new Error(`Upstream body read timed out after ${
|
|
390
|
-
},
|
|
379
|
+
bodyAbortCtrl.abort(new Error(`Upstream body read timed out after ${bodyTimeoutMs}ms`))
|
|
380
|
+
reject(new Error(`Upstream body read timed out after ${bodyTimeoutMs}ms`))
|
|
381
|
+
}, bodyTimeoutMs)
|
|
391
382
|
})
|
|
392
383
|
|
|
384
|
+
const MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
385
|
+
const contentLength = Number(upstreamRes.headers.get('content-length'))
|
|
386
|
+
if (contentLength > MAX_BODY_BYTES) {
|
|
387
|
+
clearTimeout(bodyTimer)
|
|
388
|
+
cleanupClientListeners()
|
|
389
|
+
writeJson(res, 502, { ok: false, error: 'Upstream response exceeded maximum allowed size' })
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
393
|
let resText = ''
|
|
394
394
|
let readError = null
|
|
395
395
|
try {
|
|
@@ -397,6 +397,9 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
397
397
|
upstreamRes.text(),
|
|
398
398
|
bodyTimeoutPromise,
|
|
399
399
|
])
|
|
400
|
+
if (resText.length > MAX_BODY_BYTES) {
|
|
401
|
+
throw new Error('Upstream response exceeded maximum allowed size')
|
|
402
|
+
}
|
|
400
403
|
} catch (err) {
|
|
401
404
|
readError = err
|
|
402
405
|
} finally {
|
|
@@ -419,7 +422,7 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
419
422
|
})
|
|
420
423
|
const statusCode = isTimeout ? 504 : 502
|
|
421
424
|
const errMessage = isTimeout
|
|
422
|
-
? `Gateway Timeout: upstream body read timed out after ${
|
|
425
|
+
? `Gateway Timeout: upstream body read timed out after ${bodyTimeoutMs}ms`
|
|
423
426
|
: `Bad Gateway: upstream response body read error: ${readError?.message || readError}`
|
|
424
427
|
return writeJson(res, statusCode, {
|
|
425
428
|
error: {
|
package/lib/slash-command.js
CHANGED
|
@@ -26,7 +26,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
26
26
|
const commands = cmdCtx.commands
|
|
27
27
|
if (typeof commands?.register !== 'function') return
|
|
28
28
|
|
|
29
|
-
const executeCommand = async (rawArgs
|
|
29
|
+
const executeCommand = async (rawArgs) => {
|
|
30
30
|
try {
|
|
31
31
|
const pub = publicConfig(live())
|
|
32
32
|
const subcmd = String(rawArgs || '').trim().toLowerCase().split(/\s+/)[0] || 'quota'
|
|
@@ -102,12 +102,12 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
102
102
|
if (settingsApi?.replace) {
|
|
103
103
|
const curr = plainConfig(live())
|
|
104
104
|
const isDifferent = String(param || "").trim() !== String(curr.activeAccount || "").trim()
|
|
105
|
-
const next = Config({
|
|
105
|
+
const next = plainConfig(Config({
|
|
106
106
|
...curr,
|
|
107
107
|
activeAccount: param,
|
|
108
108
|
dynamicModels: isDifferent ? [] : (curr.dynamicModels || []),
|
|
109
109
|
planSyncedAt: isDifferent ? 0 : Number(curr.planSyncedAt || 0),
|
|
110
|
-
})
|
|
110
|
+
}))
|
|
111
111
|
await settingsApi.replace(next)
|
|
112
112
|
await syncProviderState(next)
|
|
113
113
|
return `Active account switched to ${param}`
|
|
@@ -276,7 +276,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
276
276
|
? invocation
|
|
277
277
|
: (invocation?.rawInput || invocation?.input || invocation?.text || invocation?.args || '')
|
|
278
278
|
try {
|
|
279
|
-
const output = await executeCommand(rawArgs
|
|
279
|
+
const output = await executeCommand(rawArgs)
|
|
280
280
|
return {
|
|
281
281
|
kind: 'success',
|
|
282
282
|
text: output,
|
package/lib/stats-storage.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Survives dsh-web service restarts and tracks historical consumption by day, month, model, and account.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, copyFileSync } from 'node:fs'
|
|
8
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync, copyFileSync, readdirSync, unlinkSync } from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
|
|
@@ -104,11 +104,25 @@ export function loadStats(customPath) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
} catch (err) {
|
|
107
|
-
// If reading fails or file is corrupted, preserve corrupted file for diagnostics
|
|
107
|
+
// If reading fails or file is corrupted, preserve corrupted file for diagnostics (#158, #201)
|
|
108
108
|
try {
|
|
109
109
|
if (existsSync(filePath)) {
|
|
110
110
|
const corruptBackup = `${filePath}.corrupt.${Date.now()}`
|
|
111
111
|
copyFileSync(filePath, corruptBackup)
|
|
112
|
+
const dir = path.dirname(filePath)
|
|
113
|
+
const base = path.basename(filePath)
|
|
114
|
+
const prefix = `${base}.corrupt.`
|
|
115
|
+
const existing = readdirSync(dir)
|
|
116
|
+
.filter((name) => name.startsWith(prefix))
|
|
117
|
+
.sort()
|
|
118
|
+
while (existing.length > 3) {
|
|
119
|
+
const oldest = existing.shift()
|
|
120
|
+
try {
|
|
121
|
+
unlinkSync(path.join(dir, oldest))
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore deletion failure */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
112
126
|
}
|
|
113
127
|
} catch {
|
|
114
128
|
/* ignore backup failure */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
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",
|