@goodandready/dsh-clinebot 0.6.13 → 0.6.15
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 +26 -0
- package/README.md +8 -2
- package/README.ru.md +2 -1
- package/README.zh.md +2 -1
- package/lib/access.js +12 -0
- package/lib/account-events.js +0 -4
- package/lib/adapter-telemetry.js +76 -0
- package/lib/adapter.js +17 -72
- package/lib/client.js +47 -45
- package/lib/config.js +3 -0
- package/lib/http.js +10 -14
- package/lib/index.js +4 -4
- package/lib/key-discovery.js +2 -13
- package/lib/models.js +0 -4
- package/lib/price-sync.js +2 -5
- package/lib/provider-sync.js +1 -0
- package/lib/routes/models.js +1 -0
- package/lib/routes/settings.js +1 -1
- package/lib/session-router.js +0 -23
- package/lib/stats-storage.js +3 -3
- package/lib/telemetry.js +4 -0
- package/lib/transport/cline-transport.js +5 -5
- package/lib/tui-settings.js +1 -1
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -62,6 +62,8 @@ export const Config = z.object({
|
|
|
62
62
|
.description('Public model price catalog URL used by the optional price sync.').volatile(),
|
|
63
63
|
priceCatalogPath: z.string().default('~/.dsh/clinebot-prices.json')
|
|
64
64
|
.description('Local cache for the public price catalog.').volatile(),
|
|
65
|
+
priceAutoSync: z.boolean().default(true)
|
|
66
|
+
.description('Automatically fetch the public price catalog when a newly discovered model has no price.').volatile(),
|
|
65
67
|
eventLogPath: z.string().default('~/.dsh/clinebot-events.json')
|
|
66
68
|
.description('Local ring buffer for account cooldown, recovery and routing events.').volatile(),
|
|
67
69
|
enabledModels: z.array(z.string()).default([])
|
|
@@ -252,6 +254,7 @@ export function publicConfig(cfg) {
|
|
|
252
254
|
statsPath: String(cfg?.statsPath || '~/.dsh/clinebot-stats.json'),
|
|
253
255
|
priceCatalogUrl: String(cfg?.priceCatalogUrl || 'https://models.dev/api.json'),
|
|
254
256
|
priceCatalogPath: String(cfg?.priceCatalogPath || '~/.dsh/clinebot-prices.json'),
|
|
257
|
+
priceAutoSync: cfg?.priceAutoSync !== false,
|
|
255
258
|
eventLogPath: String(cfg?.eventLogPath || '~/.dsh/clinebot-events.json'),
|
|
256
259
|
statsRetentionDays: Number.isFinite(Number(cfg?.statsRetentionDays))
|
|
257
260
|
? Math.max(0, Math.min(3650, Number(cfg.statsRetentionDays)))
|
package/lib/http.js
CHANGED
|
@@ -86,7 +86,11 @@ export function isTrustedSettingsRequest(request, ctx = null) {
|
|
|
86
86
|
return false
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
const
|
|
89
|
+
const remoteLoopback = isLoopbackAddress(request?.socket?.remoteAddress)
|
|
90
|
+
// A forwarding header is only trusted when the actual peer is the local
|
|
91
|
+
// reverse proxy; never trust it from a LAN/remote client.
|
|
92
|
+
const forwardedHost = header(request, 'x-forwarded-host')
|
|
93
|
+
const host = remoteLoopback ? (forwardedHost || header(request, 'host')) : header(request, 'host')
|
|
90
94
|
const origin = header(request, 'origin')
|
|
91
95
|
if (origin) {
|
|
92
96
|
try {
|
|
@@ -94,7 +98,7 @@ export function isTrustedSettingsRequest(request, ctx = null) {
|
|
|
94
98
|
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
95
99
|
return true
|
|
96
100
|
}
|
|
97
|
-
if (isLoopbackAddress(url.hostname) &&
|
|
101
|
+
if (isLoopbackAddress(url.hostname) && remoteLoopback) {
|
|
98
102
|
return true
|
|
99
103
|
}
|
|
100
104
|
return false
|
|
@@ -110,7 +114,7 @@ export function isTrustedSettingsRequest(request, ctx = null) {
|
|
|
110
114
|
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
111
115
|
return true
|
|
112
116
|
}
|
|
113
|
-
if (isLoopbackAddress(url.hostname) &&
|
|
117
|
+
if (isLoopbackAddress(url.hostname) && remoteLoopback) {
|
|
114
118
|
return true
|
|
115
119
|
}
|
|
116
120
|
return false
|
|
@@ -119,17 +123,9 @@ export function isTrustedSettingsRequest(request, ctx = null) {
|
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
// Requests without origin/referer (e.g. curl or internal requests): allow if loopback
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// If sec-fetch-site is explicitly same-origin or same-site, allow
|
|
128
|
-
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
|
|
129
|
-
return true
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return false
|
|
126
|
+
// Requests without origin/referer (e.g. curl or internal requests): allow if loopback.
|
|
127
|
+
// Sec-Fetch-Site alone is spoofable by non-browser clients and is not an auth signal.
|
|
128
|
+
return remoteLoopback
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
export function publicUsage(usage) {
|
package/lib/index.js
CHANGED
|
@@ -38,7 +38,7 @@ export function configReader(config) {
|
|
|
38
38
|
export { NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
39
39
|
|
|
40
40
|
export function apply(ctx, config) {
|
|
41
|
-
let currentConfig = config || {}
|
|
41
|
+
let currentConfig = configReader(config)() || {}
|
|
42
42
|
const overrides = {}
|
|
43
43
|
const getConfig = () => {
|
|
44
44
|
if (!currentConfig) return currentConfig
|
|
@@ -300,8 +300,8 @@ export function apply(ctx, config) {
|
|
|
300
300
|
}])
|
|
301
301
|
}
|
|
302
302
|
ctx.effect(() => () => {
|
|
303
|
-
try { adapterDirectory?.() } catch {}
|
|
304
|
-
try { adapterRegistration?.() } catch {}
|
|
303
|
+
try { adapterDirectory?.() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
304
|
+
try { adapterRegistration?.() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
305
305
|
adapterDirectory = null
|
|
306
306
|
adapterRegistration = null
|
|
307
307
|
}, 'dsh-clinebot: llm adapter')
|
|
@@ -383,7 +383,7 @@ export function apply(ctx, config) {
|
|
|
383
383
|
const unreg = web.registerSearchProvider(CLINEBOT_SEARCH_PROVIDER_ID, searchProvider)
|
|
384
384
|
if (typeof ctx.effect === 'function') {
|
|
385
385
|
ctx.effect(() => () => {
|
|
386
|
-
try { if (typeof unreg === 'function') unreg() } catch {}
|
|
386
|
+
try { if (typeof unreg === 'function') unreg() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
387
387
|
}, 'dsh-clinebot: web search provider')
|
|
388
388
|
}
|
|
389
389
|
} catch (e) {
|
package/lib/key-discovery.js
CHANGED
|
@@ -18,13 +18,6 @@ function cleanupExpired() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function getDiscoveredKey(id) {
|
|
22
|
-
if (!id || typeof id !== 'string') return null
|
|
23
|
-
cleanupExpired()
|
|
24
|
-
const item = discoveredKeysCache.get(id)
|
|
25
|
-
return item?.key || null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
export function consumeDiscoveredKey(id) {
|
|
29
22
|
if (!id || typeof id !== 'string') return null
|
|
30
23
|
cleanupExpired()
|
|
@@ -34,10 +27,6 @@ export function consumeDiscoveredKey(id) {
|
|
|
34
27
|
return item.key
|
|
35
28
|
}
|
|
36
29
|
|
|
37
|
-
export function clearDiscoveredKeysCache() {
|
|
38
|
-
discoveredKeysCache.clear()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
30
|
export function maskApiKey(key) {
|
|
42
31
|
if (!key || typeof key !== 'string') return ''
|
|
43
32
|
if (key.length <= 12) return '••••••••'
|
|
@@ -162,10 +151,10 @@ export function discoverLocalClineKeys() {
|
|
|
162
151
|
const hits = scanFileForKeys(full, 'Cline CLI (~/.cline)')
|
|
163
152
|
for (const h of hits) addKey(h)
|
|
164
153
|
}
|
|
165
|
-
} catch {}
|
|
154
|
+
} catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
166
155
|
}
|
|
167
156
|
}
|
|
168
|
-
} catch {}
|
|
157
|
+
} catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
169
158
|
|
|
170
159
|
return discovered
|
|
171
160
|
}
|
package/lib/models.js
CHANGED
|
@@ -414,10 +414,6 @@ export function isSupportedModel(id, dynamicModels = [], modelContextOverrides =
|
|
|
414
414
|
return findModel(id, dynamicModels, modelContextOverrides, customModels) !== null
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
export function getDefaultModelIds(dynamicModels = [], modelContextOverrides = [], customModels = []) {
|
|
418
|
-
return getAllModels(dynamicModels, modelContextOverrides, customModels).map((m) => m.id)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
417
|
/**
|
|
422
418
|
* Filter out models explicitly disabled by user.
|
|
423
419
|
* Any newly introduced model (not in disabledModelIds) is active by default.
|
package/lib/price-sync.js
CHANGED
|
@@ -82,11 +82,6 @@ export function loadPublicPriceCache(cachePath) {
|
|
|
82
82
|
let lastMissingSignature = ''
|
|
83
83
|
let lastAttemptAt = 0
|
|
84
84
|
|
|
85
|
-
export function resetPriceSyncState() {
|
|
86
|
-
lastMissingSignature = ''
|
|
87
|
-
lastAttemptAt = 0
|
|
88
|
-
}
|
|
89
|
-
|
|
90
85
|
/**
|
|
91
86
|
* Refresh the public catalog only when one of the active models is missing.
|
|
92
87
|
* A repeated missing set is retried at most once per interval, so an unknown
|
|
@@ -96,6 +91,7 @@ export async function ensurePublicPricesForModels(modelIds, {
|
|
|
96
91
|
url = DEFAULT_URL,
|
|
97
92
|
cachePath = '~/.dsh/clinebot-prices.json',
|
|
98
93
|
fetchImpl,
|
|
94
|
+
enabled = true,
|
|
99
95
|
timeoutMs = 8000,
|
|
100
96
|
minIntervalMs = 6 * 60 * 60 * 1000,
|
|
101
97
|
now = Date.now(),
|
|
@@ -103,6 +99,7 @@ export async function ensurePublicPricesForModels(modelIds, {
|
|
|
103
99
|
const ids = [...new Set((Array.isArray(modelIds) ? modelIds : []).map((id) => String(id || '').trim()).filter(Boolean))]
|
|
104
100
|
const missing = ids.filter((id) => !getPublicPriceCatalog()[normalizeModelKey(id)])
|
|
105
101
|
if (missing.length === 0) return { synced: false, missing: [], count: 0 }
|
|
102
|
+
if (enabled === false) return { synced: false, skipped: true, missing, count: 0 }
|
|
106
103
|
if (!fetchImpl && process.env.CLINEBOT_DISABLE_PRICE_AUTOSYNC === '1') {
|
|
107
104
|
return { synced: false, skipped: true, missing, count: 0 }
|
|
108
105
|
}
|
package/lib/provider-sync.js
CHANGED
|
@@ -248,6 +248,7 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
|
|
|
248
248
|
await ensurePublicPricesForModels(usageData.dynamicModels.map((model) => model.id), {
|
|
249
249
|
url: pub.priceCatalogUrl,
|
|
250
250
|
cachePath: pub.priceCatalogPath,
|
|
251
|
+
enabled: pub.priceAutoSync !== false,
|
|
251
252
|
}).catch(() => {})
|
|
252
253
|
}
|
|
253
254
|
return
|
package/lib/routes/models.js
CHANGED
|
@@ -66,6 +66,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
66
66
|
priceSync = await ensurePublicPricesForModels(addedModels.map((model) => model.id), {
|
|
67
67
|
url: pub.priceCatalogUrl,
|
|
68
68
|
cachePath: pub.priceCatalogPath,
|
|
69
|
+
enabled: pub.priceAutoSync !== false,
|
|
69
70
|
})
|
|
70
71
|
} catch {
|
|
71
72
|
/* price catalog refresh is best-effort */
|
package/lib/routes/settings.js
CHANGED
|
@@ -280,7 +280,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
280
280
|
let query = {}
|
|
281
281
|
try {
|
|
282
282
|
query = Object.fromEntries(new URL(req.url, 'http://127.0.0.1').searchParams.entries())
|
|
283
|
-
} catch {}
|
|
283
|
+
} catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
284
284
|
const parseTime = (value) => {
|
|
285
285
|
if (value === undefined || value === '') return undefined
|
|
286
286
|
const numeric = Number(value)
|
package/lib/session-router.js
CHANGED
|
@@ -112,18 +112,6 @@ export function markAccountCooldown(accountId, durationMs = 60000, resetsAt = nu
|
|
|
112
112
|
emitAccountEvent({ type: 'cooldown', accountId, reason, until })
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/**
|
|
116
|
-
* Clear cooldown for an account manually.
|
|
117
|
-
*
|
|
118
|
-
* @param {string} accountId
|
|
119
|
-
*/
|
|
120
|
-
export function clearAccountCooldown(accountId) {
|
|
121
|
-
if (accountId && cooldowns.has(accountId)) {
|
|
122
|
-
cooldowns.delete(accountId)
|
|
123
|
-
emitAccountEvent({ type: 'recover', accountId, reason: 'manual' })
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
115
|
export function matchesAccount(acc, identifier) {
|
|
128
116
|
if (!acc || !identifier) return false
|
|
129
117
|
return acc.id === identifier || acc.apiKeyEnv === identifier || acc.label === identifier
|
|
@@ -362,17 +350,6 @@ export function resolveSessionAccount(sessionId, accounts = [], defaultActiveId
|
|
|
362
350
|
}
|
|
363
351
|
}
|
|
364
352
|
|
|
365
|
-
/**
|
|
366
|
-
* Explicitly release a session.
|
|
367
|
-
*
|
|
368
|
-
* @param {string} sessionId
|
|
369
|
-
*/
|
|
370
|
-
export function releaseSession(sessionId) {
|
|
371
|
-
if (sessionId) {
|
|
372
|
-
sessions.delete(sessionId)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
353
|
/**
|
|
377
354
|
* Clear all sessions and cooldowns (for test environments).
|
|
378
355
|
*/
|
package/lib/stats-storage.js
CHANGED
|
@@ -154,10 +154,10 @@ export function loadStats(customPath) {
|
|
|
154
154
|
const existing = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort()
|
|
155
155
|
while (existing.length > 3) {
|
|
156
156
|
const oldest = existing.shift()
|
|
157
|
-
try { unlinkSync(path.join(dir, oldest)) } catch {}
|
|
157
|
+
try { unlinkSync(path.join(dir, oldest)) } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
} catch {}
|
|
160
|
+
} catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
161
161
|
}
|
|
162
162
|
inMemoryStats = createEmptyStats()
|
|
163
163
|
pruneRetentionOnStats(inMemoryStats, statsRetentionDays)
|
|
@@ -202,7 +202,7 @@ function queueSaveStats(customPath, debounceMs = 500) {
|
|
|
202
202
|
if (saveTimer) clearTimeout(saveTimer)
|
|
203
203
|
saveTimer = setTimeout(() => {
|
|
204
204
|
saveTimer = null
|
|
205
|
-
try { saveStatsSync(customPath) } catch {}
|
|
205
|
+
try { saveStatsSync(customPath) } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
206
206
|
}, debounceMs)
|
|
207
207
|
}
|
|
208
208
|
|
package/lib/telemetry.js
CHANGED
|
@@ -10,6 +10,7 @@ export const sessionStats = {
|
|
|
10
10
|
promptTokensEst: 0,
|
|
11
11
|
completionTokensEst: 0,
|
|
12
12
|
reasoningTokensEst: 0,
|
|
13
|
+
offloadedImages: 0,
|
|
13
14
|
totalTokensEst: 0,
|
|
14
15
|
lastLatencyMs: null,
|
|
15
16
|
lastRequestAt: null,
|
|
@@ -26,6 +27,7 @@ export function recordSessionRequest({
|
|
|
26
27
|
promptTokens = 0,
|
|
27
28
|
completionTokens = 0,
|
|
28
29
|
reasoningTokens = 0,
|
|
30
|
+
offloadedImages = 0,
|
|
29
31
|
model,
|
|
30
32
|
}) {
|
|
31
33
|
sessionStats.totalRequests += 1
|
|
@@ -43,6 +45,7 @@ export function recordSessionRequest({
|
|
|
43
45
|
sessionStats.promptTokensEst += Number(promptTokens) || 0
|
|
44
46
|
sessionStats.completionTokensEst += Number(completionTokens) || 0
|
|
45
47
|
sessionStats.reasoningTokensEst = (sessionStats.reasoningTokensEst || 0) + (Number(reasoningTokens) || 0)
|
|
48
|
+
sessionStats.offloadedImages = (sessionStats.offloadedImages || 0) + (Number(offloadedImages) || 0)
|
|
46
49
|
sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
|
|
47
50
|
|
|
48
51
|
if (model) {
|
|
@@ -74,6 +77,7 @@ export function resetSessionStats() {
|
|
|
74
77
|
sessionStats.promptTokensEst = 0
|
|
75
78
|
sessionStats.completionTokensEst = 0
|
|
76
79
|
sessionStats.reasoningTokensEst = 0
|
|
80
|
+
sessionStats.offloadedImages = 0
|
|
77
81
|
sessionStats.totalTokensEst = 0
|
|
78
82
|
sessionStats.lastLatencyMs = null
|
|
79
83
|
sessionStats.lastRequestAt = null
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { createRequire } from 'node:module'
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url)
|
|
12
|
-
let packageVersion = '0.6.
|
|
12
|
+
let packageVersion = '0.6.15'
|
|
13
13
|
try {
|
|
14
14
|
packageVersion = require('../../package.json').version || packageVersion
|
|
15
15
|
} catch {
|
|
@@ -144,7 +144,7 @@ export async function readResponseText(response, { maxBytes = DEFAULT_MAX_BODY_B
|
|
|
144
144
|
out += decoder.decode()
|
|
145
145
|
return out
|
|
146
146
|
} finally {
|
|
147
|
-
try { reader.releaseLock() } catch {}
|
|
147
|
+
try { reader.releaseLock() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -285,7 +285,7 @@ export async function* readSseEvents(response, {
|
|
|
285
285
|
try {
|
|
286
286
|
while (true) {
|
|
287
287
|
if (signal?.aborted) {
|
|
288
|
-
try { await reader.cancel('aborted') } catch {}
|
|
288
|
+
try { await reader.cancel('aborted') } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
289
289
|
throw abortError()
|
|
290
290
|
}
|
|
291
291
|
const { done, value } = await readWithIdle()
|
|
@@ -305,10 +305,10 @@ export async function* readSseEvents(response, {
|
|
|
305
305
|
const tail = parseSseBlock(buffer)
|
|
306
306
|
if (tail !== undefined) yield tail
|
|
307
307
|
} catch (error) {
|
|
308
|
-
try { await reader.cancel(error?.message || 'stream error') } catch {}
|
|
308
|
+
try { await reader.cancel(error?.message || 'stream error') } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
309
309
|
if (signal?.aborted) throw abortError()
|
|
310
310
|
throw error
|
|
311
311
|
} finally {
|
|
312
|
-
try { reader.releaseLock() } catch {}
|
|
312
|
+
try { reader.releaseLock() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
313
313
|
}
|
|
314
314
|
}
|
package/lib/tui-settings.js
CHANGED
|
@@ -184,7 +184,7 @@ export function applyTuiSettings(ctx, { live }) {
|
|
|
184
184
|
const dispose = service.register(buildTuiSection({ live }))
|
|
185
185
|
if (typeof ctx.effect === 'function') {
|
|
186
186
|
ctx.effect(() => () => {
|
|
187
|
-
try { dispose() } catch {}
|
|
187
|
+
try { dispose() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
188
188
|
}, 'dsh-clinebot: tui settings')
|
|
189
189
|
}
|
|
190
190
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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",
|