@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 +19 -0
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/account-events.js +0 -4
- package/lib/adapter-telemetry.js +74 -0
- package/lib/adapter.js +4 -69
- package/lib/client.js +38 -39
- package/lib/http.js +10 -14
- package/lib/index.js +3 -3
- package/lib/key-discovery.js +2 -13
- package/lib/price-sync.js +46 -1
- package/lib/provider-sync.js +7 -0
- package/lib/routes/models.js +16 -0
- package/lib/routes/settings.js +1 -1
- package/lib/stats-storage.js +3 -3
- package/lib/transport/cline-transport.js +5 -5
- package/lib/tui-settings.js +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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>
|
package/lib/account-events.js
CHANGED
|
@@ -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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
|
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
|
|
338
|
-
.cb-input:focus,.cb-select:focus{border-color:
|
|
339
|
-
.cb-btn{height:auto;min-height:30px;padding:6px 11px;border:1px solid
|
|
340
|
-
.cb-btn:hover:not(:disabled){background:
|
|
341
|
-
.cb-btn-primary{border-color:
|
|
342
|
-
.cb-btn-primary:hover:not(:disabled){background:
|
|
343
|
-
.cb-btn-danger{border-color:
|
|
344
|
-
.cb-badge{padding:2px 7px;border:1px solid
|
|
345
|
-
.cb-badge-ok{color
|
|
346
|
-
.cb-badge-warn{color
|
|
347
|
-
.cb-badge-bad{color
|
|
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
|
|
353
|
-
.cb-table td{padding:5px 8px;border-bottom:1px solid
|
|
354
|
-
.cb-model-group td{padding:8px 8px 4px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;background:
|
|
355
|
-
.cb-progress-track,.cb-bar-track{height:8px;border-radius:999px;background:
|
|
356
|
-
.cb-progress-bar-fill,.cb-bar-fill,.cb-footFill,.cb-meter-fill{background
|
|
357
|
-
.cb-progress-bar-warn{background
|
|
358
|
-
.cb-progress-bar-bad,.cb-footFillWarn,.cb-meter-fill-warn{background
|
|
359
|
-
.cb-progress-meta-bad,.cb-progress-low,.cb-footPctWarn{color
|
|
360
|
-
.cb-footBar,.cb-meter{background:
|
|
361
|
-
.cb-account-tab,.cb-seg{border:1px solid
|
|
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
|
|
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:
|
|
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
|
|
368
|
-
.cb-heat-l0{background:
|
|
369
|
-
.cb-heat-l1{background:
|
|
370
|
-
.cb-heat-l2{background:
|
|
371
|
-
.cb-heat-l3{background:
|
|
372
|
-
.cb-heat-l4{background:
|
|
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
|
|
375
|
-
.cb-settings-row,.cb-price-section{border-color:
|
|
376
|
-
.cb-alert-ok{background:
|
|
377
|
-
.cb-alert-bad,.cb-alert-err{background:
|
|
378
|
-
.cb-banner-warning{background:
|
|
379
|
-
.cb-banner-exhausted{background:
|
|
380
|
-
.cb-account-route-row,.cb-model-row,.cb-preview,.cb-account-add{background:
|
|
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
|
|
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
|
@@ -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/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.
|
package/lib/provider-sync.js
CHANGED
|
@@ -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
|
|
package/lib/routes/models.js
CHANGED
|
@@ -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) })
|
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/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
|
|
|
@@ -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.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
|
}
|
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.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",
|