@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/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 host = header(request, 'x-forwarded-host') || header(request, 'host')
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) && isLoopbackAddress(request?.socket?.remoteAddress)) {
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) && isLoopbackAddress(request?.socket?.remoteAddress)) {
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
- if (isLoopbackAddress(request?.socket?.remoteAddress)) {
124
- return true
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) {
@@ -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
  }
@@ -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
@@ -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 */
@@ -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)
@@ -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
  */
@@ -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.13'
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
  }
@@ -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.13",
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",