@goodandready/dsh-clinebot 0.6.12 → 0.6.14

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 CHANGED
@@ -5,6 +5,25 @@ 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.6.14] - 2026-10-21
9
+
10
+ ### Fixed
11
+ - **Settings source gate**: remote clients can no longer pass by spoofing `Sec-Fetch-Site`; `X-Forwarded-Host` is trusted only from a loopback reverse proxy. Added regression cases for both bypasses.
12
+ - **Theme regression**: replaced 58 hardcoded colors in the client stylesheet with DSH theme tokens and `color-mix`, restoring light/dark compatibility and matching `DESIGN.md`.
13
+
14
+ ### Changed
15
+ - Extracted attempt telemetry into `lib/adapter-telemetry.js`; `lib/adapter.js` is back under the 600-line budget.
16
+ - The decomposition test now scans every server module instead of a hand-maintained allowlist.
17
+
18
+ ### Removed
19
+ - Dead/test-only exports `clearAccountEvents`, `getDiscoveredKey` and `clearDiscoveredKeysCache` (with tests adjusted to the production API).
20
+ - Annotated intentional best-effort catches and removed stale `.tgz` artifacts from the workspace.
21
+
22
+ ## [0.6.13] - 2026-10-21
23
+
24
+ ### Added
25
+ - **Automatic price refresh for new models**: model sync and auto-discovery compare the newly added IDs with the public catalog and fetch models.dev only when coverage is missing. Repeated misses are throttled, and the model-sync response includes a `priceSync` summary.
26
+
8
27
  ## [0.6.12] - 2026-10-21
9
28
 
10
29
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>Native ClineBot / ClinePass Provider Companion for DeepSeek Harness</h3>
6
6
 
7
- <p><b>0.6.12</b> adds model/account/error filters and a custom date range to analytics. Chat runs on the native <code>ctx.llm</code> adapter and requires DSH <code>0.2.1-rc.2</code>; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
7
+ <p><b>0.6.14</b> hardens the settings source gate, restores token-only theming and cleans up the audit findings. Chat runs on the native <code>ctx.llm</code> adapter and requires DSH <code>0.2.1-rc.2</code>; the legacy <code>llm-pi-ai</code>, loopback proxy and <code>llm/stream</code> path are removed.</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
package/README.ru.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
6
6
 
7
- <p><b>0.6.12</b> добавляет фильтры аналитики по модели, аккаунту и ошибкам, а также произвольный диапазон дат. Чат работает через нативный адаптер <code>ctx.llm</code>, требуется DSH <code>0.2.1-rc.2</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
7
+ <p><b>0.6.14</b> закрывает находки аудита: усилен source gate настроек, тема снова использует только токены DSH, мёртвый код и артефакты убраны. Чат работает через нативный адаптер <code>ctx.llm</code>, требуется DSH <code>0.2.1-rc.2</code>; старые <code>llm-pi-ai</code>, loopback-прокси и <code>llm/stream</code> удалены.</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h3>适用于 DeepSeek Harness 的 ClineBot / ClinePass 原生模型提供商伴侣插件</h3>
6
6
 
7
- <p><b>0.6.12</b> 为分析添加模型/账户/错误筛选和自定义日期范围。聊天使用原生 <code>ctx.llm</code> 适配器,需要 DSH <code>0.2.1-rc.2</code>;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
7
+ <p><b>0.6.14</b> 修复审计发现:加固设置 source gate,恢复仅使用 DSH 主题令牌的样式,并清理死代码和残留文件。聊天使用原生 <code>ctx.llm</code> 适配器,需要 DSH <code>0.2.1-rc.2</code>;旧的 <code>llm-pi-ai</code>、回环代理和 <code>llm/stream</code> 已删除。</p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
@@ -79,7 +79,3 @@ export function flushAccountEvents() {
79
79
  }
80
80
  }
81
81
 
82
- export function clearAccountEvents() {
83
- events = []
84
- flushAccountEvents()
85
- }
@@ -0,0 +1,74 @@
1
+ import { estimateCost } from './model-prices.js'
2
+ import { recordSessionRequest } from './telemetry.js'
3
+ import { recordUsage } from './stats-storage.js'
4
+
5
+ /**
6
+ * Record one attempt's session and persistent telemetry.
7
+ * Kept out of adapter.js so the adapter stays within the decomposition budget.
8
+ */
9
+ export function recordAttemptTelemetry({
10
+ pub,
11
+ accountId,
12
+ model,
13
+ usage,
14
+ isError,
15
+ is429 = false,
16
+ ttftMs,
17
+ latencyMs,
18
+ aborted = false,
19
+ options,
20
+ }) {
21
+ const promptTokens = Number(usage?.promptTokens || 0)
22
+ const completionTokens = Number(usage?.completionTokens || 0)
23
+ const reasoningTokens = Number(usage?.reasoningTokens || 0)
24
+ const totalTokens = Number(usage?.totalTokens || (promptTokens + completionTokens))
25
+ const cost = estimateCost({
26
+ model,
27
+ promptTokens,
28
+ completionTokens,
29
+ reasoningTokens,
30
+ cacheReadTokens: usage?.cacheReadTokens,
31
+ cacheWriteTokens: usage?.cacheWriteTokens,
32
+ overrides: pub.priceOverrides || {},
33
+ })
34
+
35
+ try {
36
+ recordSessionRequest({
37
+ latencyMs,
38
+ ok: !isError,
39
+ aborted,
40
+ promptTokens,
41
+ completionTokens,
42
+ reasoningTokens,
43
+ model,
44
+ ...(isError ? { error: is429 ? 'rate limit' : 'stream error' } : {}),
45
+ })
46
+ } catch {
47
+ /* telemetry must never break inference */
48
+ }
49
+
50
+ try {
51
+ recordUsage({
52
+ model,
53
+ accountId,
54
+ promptTokens,
55
+ completionTokens,
56
+ reasoningTokens,
57
+ totalTokens,
58
+ isError,
59
+ is429,
60
+ statsPath: pub.statsPath,
61
+ sessionId: options.sessionId ? String(options.sessionId) : undefined,
62
+ purpose: options.purpose,
63
+ ttftMs: ttftMs ?? undefined,
64
+ latencyMs,
65
+ cacheReadTokens: usage?.cacheReadTokens,
66
+ cacheWriteTokens: usage?.cacheWriteTokens,
67
+ cacheSavingsUsd: cost.cacheSavingsUsd,
68
+ costUsd: cost.usd,
69
+ priced: cost.priced,
70
+ })
71
+ } catch {
72
+ /* persistent telemetry is best-effort */
73
+ }
74
+ }
package/lib/adapter.js CHANGED
@@ -11,10 +11,9 @@ import { publicConfig } from './config.js'
11
11
  import { getAllModels, findModel, formatModelDescription } from './models.js'
12
12
  import { resolveAccountPool } from './account-pool.js'
13
13
  import { resolveSessionAccount, markAccountCooldown } from './session-router.js'
14
- import { recordSessionRequest } from './telemetry.js'
15
- import { recordUsage, getBudgetSpend } from './stats-storage.js'
14
+ import { recordAttemptTelemetry } from './adapter-telemetry.js'
15
+ import { getBudgetSpend } from './stats-storage.js'
16
16
  import { budgetStatus, budgetErrorMessage } from './budget.js'
17
- import { estimateCost } from './model-prices.js'
18
17
  import { buildOpenAiPayload } from './adapter-messages.js'
19
18
  import { streamJsonCompletion, streamOpenAiChunks } from './adapter-stream.js'
20
19
  import {
@@ -549,72 +548,8 @@ export class ClineBotAdapter extends LlmAdapter {
549
548
  * options: Record<string, unknown>,
550
549
  * }} entry
551
550
  */
552
- recordAttempt({
553
- pub,
554
- accountId,
555
- model,
556
- usage,
557
- isError,
558
- is429 = false,
559
- ttftMs,
560
- latencyMs,
561
- aborted = false,
562
- options,
563
- }) {
564
- const promptTokens = Number(usage?.promptTokens || 0)
565
- const completionTokens = Number(usage?.completionTokens || 0)
566
- const reasoningTokens = Number(usage?.reasoningTokens || 0)
567
- const totalTokens = Number(usage?.totalTokens || (promptTokens + completionTokens))
568
- const cost = estimateCost({
569
- model,
570
- promptTokens,
571
- completionTokens,
572
- reasoningTokens,
573
- cacheReadTokens: usage?.cacheReadTokens,
574
- cacheWriteTokens: usage?.cacheWriteTokens,
575
- overrides: pub.priceOverrides || {},
576
- })
577
-
578
- try {
579
- recordSessionRequest({
580
- latencyMs,
581
- ok: !isError,
582
- aborted,
583
- promptTokens,
584
- completionTokens,
585
- reasoningTokens,
586
- model,
587
- ...(isError ? { error: is429 ? 'rate limit' : 'stream error' } : {}),
588
- })
589
- } catch {
590
- /* telemetry must never break inference */
591
- }
592
-
593
- try {
594
- recordUsage({
595
- model,
596
- accountId,
597
- promptTokens,
598
- completionTokens,
599
- reasoningTokens,
600
- totalTokens,
601
- isError,
602
- is429,
603
- statsPath: pub.statsPath,
604
- // Extended fields consumed by stats v2 once available.
605
- sessionId: options.sessionId ? String(options.sessionId) : undefined,
606
- purpose: options.purpose,
607
- ttftMs: ttftMs ?? undefined,
608
- latencyMs,
609
- cacheReadTokens: usage?.cacheReadTokens,
610
- cacheWriteTokens: usage?.cacheWriteTokens,
611
- cacheSavingsUsd: cost.cacheSavingsUsd,
612
- costUsd: cost.usd,
613
- priced: cost.priced,
614
- })
615
- } catch {
616
- /* persistent telemetry is best-effort */
617
- }
551
+ recordAttempt(entry) {
552
+ recordAttemptTelemetry(entry)
618
553
  }
619
554
  }
620
555
 
package/lib/client.js CHANGED
@@ -325,59 +325,58 @@ function ensureCss(){if(typeof document==="undefined")return;if(document.getElem
325
325
  .cb-analytics-head span:nth-child(4),.cb-analytics-head span:nth-child(5),.cb-analytics-row span:nth-child(4),.cb-analytics-row span:nth-child(5){display:none}
326
326
  }
327
327
 
328
- /* Opencode-inspired visual language: neutral rgba surfaces, 7-10px radii and
329
- the same green/amber/red accents. Text still follows the harness theme. */
328
+ /* OpenCode-inspired proportions, rendered exclusively with DSH theme tokens. */
330
329
  .cb-page{gap:14px;padding:4px 2px 24px}
331
330
  .cb-page-title{font-size:18px;font-weight:650;line-height:1.4}
332
331
  .cb-panel{max-width:760px;padding:18px}
333
- .cb-section-card,.cb-dialogCard,.cb-provider-card,.cb-progress-card,.cb-stat-box,.cb-account-add,.cb-discovered,.cb-update-banner,.cb-notice,.cb-savebar-inner,.cb-account-menu{border:1px solid rgba(127,127,127,.25);border-radius:10px;background:rgba(127,127,127,.06)}
332
+ .cb-section-card,.cb-dialogCard,.cb-provider-card,.cb-progress-card,.cb-stat-box,.cb-account-add,.cb-discovered,.cb-update-banner,.cb-notice,.cb-savebar-inner,.cb-account-menu{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
334
333
  .cb-section-card,.cb-dialogCard{padding:12px 14px;gap:10px}
335
334
  .cb-section-title,.cb-settings-group-title{font-size:13px;font-weight:650;line-height:1.5}
336
335
  .cb-section-desc,.cb-settings-group-desc,.cb-settings-row-desc,.cb-hint{opacity:.72;line-height:1.5}
337
- .cb-input,.cb-select{height:auto;min-height:32px;padding:6px 8px;border:1px solid rgba(127,127,127,.35);border-radius:7px;background:rgba(127,127,127,.08);font-size:13px;line-height:1.4}
338
- .cb-input:focus,.cb-select:focus{border-color:rgba(80,140,255,.6);outline:2px solid rgba(80,140,255,.25);outline-offset:1px}
339
- .cb-btn{height:auto;min-height:30px;padding:6px 11px;border:1px solid rgba(127,127,127,.35);border-radius:7px;background:rgba(127,127,127,.1);font-size:13px;line-height:1.4}
340
- .cb-btn:hover:not(:disabled){background:rgba(127,127,127,.18)}
341
- .cb-btn-primary{border-color:rgba(80,140,255,.6);background:rgba(80,140,255,.18);color:inherit}
342
- .cb-btn-primary:hover:not(:disabled){background:rgba(80,140,255,.26)}
343
- .cb-btn-danger{border-color:rgba(220,80,80,.5);color:#e07a7a}
344
- .cb-badge{padding:2px 7px;border:1px solid rgba(127,127,127,.3);border-radius:999px;background:rgba(127,127,127,.1);font-size:11px;line-height:1.4}
345
- .cb-badge-ok{color:#4fbf86;border-color:rgba(60,180,110,.5);background:rgba(60,180,110,.10)}
346
- .cb-badge-warn{color:#d9a24a;border-color:rgba(220,160,60,.5);background:rgba(220,160,60,.10)}
347
- .cb-badge-bad{color:#e07a7a;border-color:rgba(220,80,80,.5);background:rgba(220,80,80,.10)}
336
+ .cb-input,.cb-select{height:auto;min-height:32px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l3);border-radius:7px;background:var(--dsw-alias-bg-layer-1);font-size:13px;line-height:1.4}
337
+ .cb-input:focus,.cb-select:focus{border-color:var(--dsw-alias-brand-primary);outline:2px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,transparent);outline-offset:1px}
338
+ .cb-btn{height:auto;min-height:30px;padding:6px 11px;border:1px solid var(--dsw-alias-border-l3);border-radius:7px;background:var(--dsw-alias-bg-layer-2);font-size:13px;line-height:1.4}
339
+ .cb-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}
340
+ .cb-btn-primary{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);color:inherit}
341
+ .cb-btn-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-brand-primary) 26%,transparent)}
342
+ .cb-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 50%,transparent);color:var(--dsw-alias-state-error-primary)}
343
+ .cb-badge{padding:2px 7px;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-layer-2);font-size:11px;line-height:1.4}
344
+ .cb-badge-ok{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 50%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}
345
+ .cb-badge-warn{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 50%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}
346
+ .cb-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 50%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
348
347
  .cb-stat-box{padding:8px 10px;border-radius:8px}
349
348
  .cb-stat-val{font-size:15px;font-weight:600;line-height:1.4}
350
349
  .cb-stat-lbl{font-size:11px;line-height:1.5}
351
350
  .cb-table{font-size:12px}
352
- .cb-table th{padding:5px 8px;border-bottom:1px solid rgba(127,127,127,.18);font-size:11px;font-weight:600;opacity:.8}
353
- .cb-table td{padding:5px 8px;border-bottom:1px solid rgba(127,127,127,.18);font-size:12px}
354
- .cb-model-group td{padding:8px 8px 4px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;background:rgba(127,127,127,.08)}
355
- .cb-progress-track,.cb-bar-track{height:8px;border-radius:999px;background:rgba(127,127,127,.2)}
356
- .cb-progress-bar-fill,.cb-bar-fill,.cb-footFill,.cb-meter-fill{background:#4fbf86}
357
- .cb-progress-bar-warn{background:#d9a24a}
358
- .cb-progress-bar-bad,.cb-footFillWarn,.cb-meter-fill-warn{background:#e07a7a}
359
- .cb-progress-meta-bad,.cb-progress-low,.cb-footPctWarn{color:#e07a7a}
360
- .cb-footBar,.cb-meter{background:rgba(127,127,127,.2)}
361
- .cb-account-tab,.cb-seg{border:1px solid rgba(127,127,127,.3);border-radius:999px;background:rgba(127,127,127,.1)}
351
+ .cb-table th{padding:5px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:11px;font-weight:600;opacity:.8}
352
+ .cb-table td{padding:5px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px}
353
+ .cb-model-group td{padding:8px 8px 4px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;background:var(--dsw-alias-bg-layer-2)}
354
+ .cb-progress-track,.cb-bar-track{height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-2)}
355
+ .cb-progress-bar-fill,.cb-bar-fill,.cb-footFill,.cb-meter-fill{background:var(--dsw-alias-state-success-primary)}
356
+ .cb-progress-bar-warn{background:var(--dsw-alias-state-warn-primary)}
357
+ .cb-progress-bar-bad,.cb-footFillWarn,.cb-meter-fill-warn{background:var(--dsw-alias-state-error-primary)}
358
+ .cb-progress-meta-bad,.cb-progress-low,.cb-footPctWarn{color:var(--dsw-alias-state-error-primary)}
359
+ .cb-footBar,.cb-meter{background:var(--dsw-alias-bg-layer-2)}
360
+ .cb-account-tab,.cb-seg{border:1px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-layer-1)}
362
361
  .cb-seg{border-radius:7px;overflow:hidden}
363
- .cb-seg-item{border:0;border-left:1px solid rgba(127,127,127,.2);border-radius:0;background:transparent}
362
+ .cb-seg-item{border:0;border-left:1px solid var(--dsw-alias-border-l2);border-radius:0;background:transparent}
364
363
  .cb-seg-item:first-child{border-left:0}
365
- .cb-seg-item-active,.cb-account-tab-active{border-color:rgba(80,140,255,.6);background:rgba(80,140,255,.18);color:inherit}
364
+ .cb-seg-item-active,.cb-account-tab-active{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);color:inherit}
366
365
  .cb-tab{border:0;border-radius:0;background:transparent;border-bottom:1.5px solid transparent}
367
- .cb-tab-active{border-bottom-color:#4fbf86;background:transparent}
368
- .cb-heat-l0{background:rgba(127,127,127,.10)}
369
- .cb-heat-l1{background:rgba(79,191,134,.234)}
370
- .cb-heat-l2{background:rgba(79,191,134,.382)}
371
- .cb-heat-l3{background:rgba(79,191,134,.530)}
372
- .cb-heat-l4{background:rgba(79,191,134,.826)}
366
+ .cb-tab-active{border-bottom-color:var(--dsw-alias-state-success-primary);background:transparent}
367
+ .cb-heat-l0{background:var(--dsw-alias-bg-layer-2)}
368
+ .cb-heat-l1{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 18%,var(--dsw-alias-bg-layer-2))}
369
+ .cb-heat-l2{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 36%,var(--dsw-alias-bg-layer-2))}
370
+ .cb-heat-l3{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 54%,var(--dsw-alias-bg-layer-2))}
371
+ .cb-heat-l4{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 82%,var(--dsw-alias-bg-layer-2))}
373
372
  .cb-heat-cell,.cb-heat-swatch{border-radius:3px}
374
- .cb-analytics-head,.cb-analytics-row{padding:5px 0;border-bottom:1px solid rgba(127,127,127,.18)}
375
- .cb-settings-row,.cb-price-section{border-color:rgba(127,127,127,.18)}
376
- .cb-alert-ok{background:rgba(60,180,110,.10);color:#4fbf86}
377
- .cb-alert-bad,.cb-alert-err{background:rgba(220,80,80,.10);color:#e07a7a}
378
- .cb-banner-warning{background:rgba(220,160,60,.10);border-color:rgba(220,160,60,.5);color:#d9a24a}
379
- .cb-banner-exhausted{background:rgba(220,80,80,.10);border-color:rgba(220,80,80,.5);color:#e07a7a}
380
- .cb-account-route-row,.cb-model-row,.cb-preview,.cb-account-add{background:rgba(127,127,127,.06)}
373
+ .cb-analytics-head,.cb-analytics-row{padding:5px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
374
+ .cb-settings-row,.cb-price-section{border-color:var(--dsw-alias-border-l2)}
375
+ .cb-alert-ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary)}
376
+ .cb-alert-bad,.cb-alert-err{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
377
+ .cb-banner-warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 50%,transparent);color:var(--dsw-alias-state-warn-primary)}
378
+ .cb-banner-exhausted{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 50%,transparent);color:var(--dsw-alias-state-error-primary)}
379
+ .cb-account-route-row,.cb-model-row,.cb-preview,.cb-account-add{background:var(--dsw-alias-bg-layer-1)}
381
380
 
382
381
  `;document.head.appendChild(style)}
383
382
 
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
@@ -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/price-sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
3
  import path from 'node:path'
4
- import { normalizeModelKey } from './model-prices.js'
4
+ import { normalizeModelKey, getPublicPriceCatalog, setPublicPriceCatalog } from './model-prices.js'
5
5
 
6
6
  const DEFAULT_URL = 'https://models.dev/api.json'
7
7
  const PROVIDER_PRIORITY = {
@@ -79,6 +79,51 @@ export function loadPublicPriceCache(cachePath) {
79
79
  }
80
80
  }
81
81
 
82
+ let lastMissingSignature = ''
83
+ let lastAttemptAt = 0
84
+
85
+ export function resetPriceSyncState() {
86
+ lastMissingSignature = ''
87
+ lastAttemptAt = 0
88
+ }
89
+
90
+ /**
91
+ * Refresh the public catalog only when one of the active models is missing.
92
+ * A repeated missing set is retried at most once per interval, so an unknown
93
+ * custom model cannot hammer models.dev on every sync.
94
+ */
95
+ export async function ensurePublicPricesForModels(modelIds, {
96
+ url = DEFAULT_URL,
97
+ cachePath = '~/.dsh/clinebot-prices.json',
98
+ fetchImpl,
99
+ timeoutMs = 8000,
100
+ minIntervalMs = 6 * 60 * 60 * 1000,
101
+ now = Date.now(),
102
+ } = {}) {
103
+ const ids = [...new Set((Array.isArray(modelIds) ? modelIds : []).map((id) => String(id || '').trim()).filter(Boolean))]
104
+ const missing = ids.filter((id) => !getPublicPriceCatalog()[normalizeModelKey(id)])
105
+ if (missing.length === 0) return { synced: false, missing: [], count: 0 }
106
+ if (!fetchImpl && process.env.CLINEBOT_DISABLE_PRICE_AUTOSYNC === '1') {
107
+ return { synced: false, skipped: true, missing, count: 0 }
108
+ }
109
+ const signature = missing.slice().sort().join('|')
110
+ if (signature === lastMissingSignature && now - lastAttemptAt < minIntervalMs) {
111
+ return { synced: false, skipped: true, missing, count: 0 }
112
+ }
113
+ lastMissingSignature = signature
114
+ lastAttemptAt = now
115
+ const result = await syncPublicPrices({ url, cachePath, fetchImpl, timeoutMs })
116
+ setPublicPriceCatalog(result.catalog)
117
+ const stillMissing = ids.filter((id) => !result.catalog[normalizeModelKey(id)])
118
+ return {
119
+ synced: true,
120
+ count: result.count,
121
+ syncedAt: result.syncedAt,
122
+ source: result.source,
123
+ missing: stillMissing,
124
+ }
125
+ }
126
+
82
127
  /**
83
128
  * Fetch and cache the public catalog. The caller owns applying it to
84
129
  * model-prices; keeping that side effect out of this helper makes it testable.
@@ -11,6 +11,7 @@ import {
11
11
  migrateModelEntry,
12
12
  isCatalogDifferent,
13
13
  } from './models.js'
14
+ import { ensurePublicPricesForModels } from './price-sync.js'
14
15
  import {
15
16
  probeHealth,
16
17
  fetchUsageLimits,
@@ -243,6 +244,12 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
243
244
  if (cacheFile) {
244
245
  await saveModelsDiskCache(cacheFile, usageData.dynamicModels, now, cacheIdentity)
245
246
  }
247
+ if (isDifferent) {
248
+ await ensurePublicPricesForModels(usageData.dynamicModels.map((model) => model.id), {
249
+ url: pub.priceCatalogUrl,
250
+ cachePath: pub.priceCatalogPath,
251
+ }).catch(() => {})
252
+ }
246
253
  return
247
254
  }
248
255
 
@@ -5,6 +5,7 @@ import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.j
5
5
  import { getAllModels, saveModelsDiskCache, getOriginalModelContext, getOriginalModelMaxTokens } from '../models.js'
6
6
  import { fetchUsageLimits, smokeChat } from '../cline-client.js'
7
7
  import { publicUsage } from '../http.js'
8
+ import { ensurePublicPricesForModels } from '../price-sync.js'
8
9
 
9
10
  export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
10
11
  // POST /dsh-clinebot/models/sync — dynamically load models from plan
@@ -39,6 +40,8 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
39
40
  return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
40
41
  }
41
42
 
43
+ const previousIds = new Set((pub.dynamicModels || []).map((model) => model?.id).filter(Boolean))
44
+ const addedModels = dynamicModels.filter((model) => model?.id && !previousIds.has(model.id))
42
45
  const now = Date.now()
43
46
  const next = plainConfig(Config({
44
47
  ...plainConfig(live()),
@@ -57,12 +60,25 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
57
60
  })
58
61
  }
59
62
 
63
+ let priceSync = null
64
+ if (addedModels.length > 0) {
65
+ try {
66
+ priceSync = await ensurePublicPricesForModels(addedModels.map((model) => model.id), {
67
+ url: pub.priceCatalogUrl,
68
+ cachePath: pub.priceCatalogPath,
69
+ })
70
+ } catch {
71
+ /* price catalog refresh is best-effort */
72
+ }
73
+ }
74
+
60
75
  return writeJson(res, 200, {
61
76
  ok: true,
62
77
  plan: usageData.plan,
63
78
  discoveredCount: dynamicModels.length,
64
79
  totalModelsCount: allModels.length,
65
80
  models: allModels,
81
+ priceSync,
66
82
  })
67
83
  } catch (err) {
68
84
  return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
@@ -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)
@@ -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
 
@@ -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'
12
+ let packageVersion = '0.6.14'
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.12",
3
+ "version": "0.6.14",
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",