@goodandready/dsh-clinebot 0.3.1 → 0.3.2
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 +12 -0
- package/docs/design/DESIGN.md +2 -1
- package/lib/client.js +231 -85
- package/lib/index.js +106 -30
- package/lib/models.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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.3.2] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Automatic Plan Models Discovery**: Dynamic subscription plan models are parsed and registered automatically upon plugin startup and key configuration without requiring manual sync clicks.
|
|
12
|
+
- **`disabledModels` Selection Model**: Migrated model activation state to `disabledModels`. Any newly added models in the ClinePass subscription plan are enabled automatically by default, while user exclusions are reliably preserved.
|
|
13
|
+
- **Instant Non-Blocking Settings Status**: Limited background health check and quota ping timeout to 2500 ms in `buildStatus()`, rendering settings immediately and avoiding UI freezes on cold start or network hiccups.
|
|
14
|
+
- **Full DSH English & Russian Localization**: Complete translation dictionary coverage (`en` canonical and `ru`) across headers, badges, quotas, models table, metrics, and diagnostics.
|
|
15
|
+
- **Dynamic New Model Badges**: Newly discovered models received directly from the user's subscription plan now feature an informative `New` badge in the model picker.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **DSH Registration Automation**: Provider registration in `llm-pi-ai` is now fully declarative and synchronized automatically when API credentials or model choices change. Removed redundant manual registration requirement.
|
|
19
|
+
|
|
8
20
|
## [0.3.1] - 2026-09-07
|
|
9
21
|
|
|
10
22
|
### Fixed
|
package/docs/design/DESIGN.md
CHANGED
|
@@ -9,7 +9,8 @@ The plugin consists of two runtime boundaries conforming to DSH authoring standa
|
|
|
9
9
|
### 2.1 Host Runtime (`lib/index.js`, `lib/cline-client.js`, `lib/models.js`, `lib/http.js`)
|
|
10
10
|
* **Cordis Service Registration**: Injects `['settings', 'webServer', 'credentials']`.
|
|
11
11
|
* **Credential Isolation**: The plugin NEVER stores plain API keys in its configuration. The setting `apiKeyEnv` holds the credential identifier (default: `CLINEBOT_API_KEY`), resolved via `ctx.credentials.resolve()` or `process.env`.
|
|
12
|
-
* **State Synchronization**: Mutates the core `llm-pi-ai` settings space (`op: 'set', path: ['providers', 'clinebot']`) when enabled or
|
|
12
|
+
* **State Synchronization & Auto-Registration**: Mutates the core `llm-pi-ai` settings space (`op: 'set', path: ['providers', 'clinebot']`) declaratively and automatically when enabled or key is saved.
|
|
13
|
+
* **Auto-Discovery & `disabledModels`**: Features automatic background polling of subscription plan models (`GET /api/v1/users/me/plan`). User preferences are tracked via `disabledModels: []`, ensuring newly added plan models appear enabled by default in the DSH chat picker without manual re-synchronization.
|
|
13
14
|
|
|
14
15
|
### 2.2 Client Runtime (`lib/client.js`)
|
|
15
16
|
* Self-registering module via `window.__ModuleLoader__.load({ id: '@goodandready/dsh-clinebot', factory })`.
|
package/lib/client.js
CHANGED
|
@@ -14,14 +14,144 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
title: 'ClineBot',
|
|
15
15
|
subtitle: 'ClinePass subscription ($9.99/mo) with curated & custom models.',
|
|
16
16
|
'settings.loading': 'Loading ClineBot settings…',
|
|
17
|
+
'settings.retry': 'Retry',
|
|
17
18
|
'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
|
|
19
|
+
'header.title': 'ClineBot (ClinePass) Provider',
|
|
20
|
+
'header.sub': 'Connect models under ClinePass subscription ($9.99/mo). OpenAI API compatible, automated rolling limit tracking, and instant auto-registration in DSH.',
|
|
21
|
+
'badge.online': 'Host online ({latency} ms)',
|
|
22
|
+
'badge.offline': 'Host unreachable',
|
|
23
|
+
'badge.key_ok': 'Key ✓ ({source})',
|
|
24
|
+
'badge.key_missing': 'Key missing',
|
|
25
|
+
'badge.registered': 'DSH Registered ({count} models)',
|
|
26
|
+
'badge.not_registered': 'Not registered',
|
|
27
|
+
'key.title': '🔑 Authorization & API Key',
|
|
28
|
+
'key.desc': 'The key is saved directly to DSH Credentials storage (~/.dsh/.credentials.yaml) and never exposed in plaintext configuration files.',
|
|
29
|
+
'key.placeholder_has': '••••••••••••••••••••••••',
|
|
30
|
+
'key.placeholder_empty': 'Paste ClinePass key (cline_...)',
|
|
31
|
+
'key.show': 'Show',
|
|
32
|
+
'key.hide': 'Hide',
|
|
33
|
+
'key.save': 'Save Key',
|
|
34
|
+
'key.saving': 'Saving…',
|
|
35
|
+
'key.env_label': 'Credential environment name: ',
|
|
36
|
+
'key.get_key': 'Get key in app.cline.bot console ↗',
|
|
37
|
+
'key.saved_msg': 'Key saved to DSH Credentials. Validation: {status}',
|
|
38
|
+
'key.empty_err': 'Please enter an API key before saving',
|
|
39
|
+
'quota.title': '📊 ClinePass Quota & Rate Limits',
|
|
40
|
+
'quota.desc': 'Official rolling window request limits from ClinePass',
|
|
41
|
+
'quota.account': 'Account: {email} · Plan: {plan}',
|
|
42
|
+
'quota.refresh': 'Refresh Quotas',
|
|
43
|
+
'quota.refreshing': 'Refreshing…',
|
|
44
|
+
'quota.refreshed_msg': 'Subscription quotas refreshed',
|
|
45
|
+
'quota.window_5h': '⏱ 5-Hour Rolling Limit',
|
|
46
|
+
'quota.window_weekly': '📅 Weekly Window',
|
|
47
|
+
'quota.used': '{pct}% used',
|
|
48
|
+
'quota.remaining': 'Remaining: {pct}%',
|
|
49
|
+
'quota.reset_at': 'Reset: {time}',
|
|
50
|
+
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
51
|
+
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker.',
|
|
52
|
+
'models.sync': '🔄 Check Plan Models',
|
|
53
|
+
'models.syncing': 'Checking…',
|
|
54
|
+
'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
|
|
55
|
+
'models.all': 'All',
|
|
56
|
+
'models.vision': 'Vision Only',
|
|
57
|
+
'models.coding': 'Coding Only',
|
|
58
|
+
'models.recommended': 'Recommended',
|
|
59
|
+
'models.th_active': 'On',
|
|
60
|
+
'models.th_name': 'Model Name',
|
|
61
|
+
'models.th_id': 'Model ID',
|
|
62
|
+
'models.th_ctx': 'Context',
|
|
63
|
+
'models.th_caps': 'Capabilities',
|
|
64
|
+
'models.star': 'Star',
|
|
65
|
+
'models.auto_new': 'New',
|
|
66
|
+
'stats.title': '📈 Current DSH Session Telemetry',
|
|
67
|
+
'stats.desc': 'Local monitoring of requests, network latency, and estimated token usage via ClinePass.',
|
|
68
|
+
'stats.requests': 'Successful / Total Requests',
|
|
69
|
+
'stats.tokens': 'Estimated Session Tokens',
|
|
70
|
+
'stats.latency': 'Last Request Latency',
|
|
71
|
+
'stats.last_req': 'Last Request Time',
|
|
72
|
+
'diag.title': '⚡ Diagnostics & Auto-Registration',
|
|
73
|
+
'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test or force re-sync.',
|
|
74
|
+
'diag.smoke_btn': 'Run Smoke Test (Ping)',
|
|
75
|
+
'diag.smoke_testing': 'Testing…',
|
|
76
|
+
'diag.smoke_ok': 'Smoke test passed! Latency: {latency} ms',
|
|
77
|
+
'diag.resync_btn': 'Force Re-sync to DSH',
|
|
78
|
+
'diag.resyncing': 'Syncing…',
|
|
79
|
+
'diag.resynced_msg': 'Provider re-synced to DSH Models ({count} models)',
|
|
80
|
+
'diag.unregister_btn': 'Unregister from DSH',
|
|
81
|
+
'diag.unregistering': 'Removing…',
|
|
82
|
+
'diag.unregistered_msg': 'Provider removed from DSH Models',
|
|
18
83
|
}
|
|
19
84
|
|
|
20
85
|
const ru = {
|
|
21
86
|
title: 'ClineBot',
|
|
22
87
|
subtitle: 'Подписка ClinePass ($9.99/мес) с каталогом проверенных моделей.',
|
|
23
88
|
'settings.loading': 'Загрузка настроек ClineBot…',
|
|
89
|
+
'settings.retry': 'Повторить попытку',
|
|
24
90
|
'settings.unavailable': 'Настройки ClineBot недоступны (пространство хоста ещё не готово).',
|
|
91
|
+
'header.title': 'Провайдер ClineBot (ClinePass)',
|
|
92
|
+
'header.sub': 'Подключение моделей по подписке ClinePass ($9.99/мес). OpenAI API совместимый, автоматическое отслеживание лимитов и моментальная авторегистрация в DSH.',
|
|
93
|
+
'badge.online': 'Сервер доступен ({latency} мс)',
|
|
94
|
+
'badge.offline': 'Недоступен',
|
|
95
|
+
'badge.key_ok': 'Ключ ✓ ({source})',
|
|
96
|
+
'badge.key_missing': 'Ключ отсутствует',
|
|
97
|
+
'badge.registered': 'Зарегистрирован в DSH ({count} мод.)',
|
|
98
|
+
'badge.not_registered': 'Не зарегистрирован',
|
|
99
|
+
'key.title': '🔑 Авторизация и API-ключ',
|
|
100
|
+
'key.desc': 'Ключ сохраняется в системное хранилище DSH Credentials (~/.dsh/.credentials.yaml) и никогда не попадает в открытые файлы настроек.',
|
|
101
|
+
'key.placeholder_has': '••••••••••••••••••••••••',
|
|
102
|
+
'key.placeholder_empty': 'Вставьте ключ ClinePass (cline_...)',
|
|
103
|
+
'key.show': 'Показать',
|
|
104
|
+
'key.hide': 'Скрыть',
|
|
105
|
+
'key.save': 'Сохранить ключ',
|
|
106
|
+
'key.saving': 'Сохранение…',
|
|
107
|
+
'key.env_label': 'Переменная учётных данных: ',
|
|
108
|
+
'key.get_key': 'Получить ключ в консоли app.cline.bot ↗',
|
|
109
|
+
'key.saved_msg': 'Ключ сохранён в DSH Credentials. Валидация: {status}',
|
|
110
|
+
'key.empty_err': 'Введите API-ключ перед сохранением',
|
|
111
|
+
'quota.title': '📊 Остаток лимитов подписки (ClinePass Quota)',
|
|
112
|
+
'quota.desc': 'Официальные лимиты скользящих окон запросов ClinePass',
|
|
113
|
+
'quota.account': 'Аккаунт: {email} · Тариф: {plan}',
|
|
114
|
+
'quota.refresh': 'Обновить квоты',
|
|
115
|
+
'quota.refreshing': 'Обновление…',
|
|
116
|
+
'quota.refreshed_msg': 'Лимиты подписки обновлены',
|
|
117
|
+
'quota.window_5h': '⏱ 5-часовое скользящее окно (Rolling Limit)',
|
|
118
|
+
'quota.window_weekly': '📅 Недельное окно (Weekly Window)',
|
|
119
|
+
'quota.used': '{pct}% использовано',
|
|
120
|
+
'quota.remaining': 'Осталось: {pct}%',
|
|
121
|
+
'quota.reset_at': 'Сброс: {time}',
|
|
122
|
+
'models.title': '🎯 Модели в пикере DSH ({enabled} из {total} включено)',
|
|
123
|
+
'models.desc': 'Список синхронизируется напрямую из подписки ClinePass. Новые модели появляются автоматически. Снимите галочку, чтобы скрыть ненужную модель из чата.',
|
|
124
|
+
'models.sync': '🔄 Проверить модели плана',
|
|
125
|
+
'models.syncing': 'Проверка…',
|
|
126
|
+
'models.synced_msg': 'Синхронизировано моделей подписки: {total} ({discovered} получено напрямую из плана)',
|
|
127
|
+
'models.all': 'Все',
|
|
128
|
+
'models.vision': 'Только Vision',
|
|
129
|
+
'models.coding': 'Только Кодинг',
|
|
130
|
+
'models.recommended': 'Рекомендованные',
|
|
131
|
+
'models.th_active': 'Вкл',
|
|
132
|
+
'models.th_name': 'Название модели',
|
|
133
|
+
'models.th_id': 'Model ID',
|
|
134
|
+
'models.th_ctx': 'Контекст',
|
|
135
|
+
'models.th_caps': 'Возможности',
|
|
136
|
+
'models.star': 'Star',
|
|
137
|
+
'models.auto_new': 'Новая',
|
|
138
|
+
'stats.title': '📈 Статистика текущей сессии DSH',
|
|
139
|
+
'stats.desc': 'Локальный мониторинг запросов, задержек сети и ориентировочного расхода токенов через ClinePass.',
|
|
140
|
+
'stats.requests': 'Успешных запросов / Всего',
|
|
141
|
+
'stats.tokens': 'Оценка токенов сессии',
|
|
142
|
+
'stats.latency': 'Последняя задержка (Latency)',
|
|
143
|
+
'stats.last_req': 'Время последнего запроса',
|
|
144
|
+
'diag.title': '⚡ Диагностика и авторегистрация в DSH',
|
|
145
|
+
'diag.desc': 'ClineBot автоматически регистрируется в DSH Models при наличии ключа. Вы можете запустить проверку связи или принудительно пересинхронизировать модели.',
|
|
146
|
+
'diag.smoke_btn': 'Запустить Smoke Test (Ping)',
|
|
147
|
+
'diag.smoke_testing': 'Тестирование…',
|
|
148
|
+
'diag.smoke_ok': 'Smoke тест пройден! Задержка: {latency} мс',
|
|
149
|
+
'diag.resync_btn': 'Принудительно обновить в DSH',
|
|
150
|
+
'diag.resyncing': 'Обновление…',
|
|
151
|
+
'diag.resynced_msg': 'Провайдер обновлен в DSH Models ({count} моделей)',
|
|
152
|
+
'diag.unregister_btn': 'Удалить из DSH Models',
|
|
153
|
+
'diag.unregistering': 'Удаление…',
|
|
154
|
+
'diag.unregistered_msg': 'Провайдер удален из DSH Models',
|
|
25
155
|
}
|
|
26
156
|
|
|
27
157
|
function makeT(dict, fallback) {
|
|
@@ -85,7 +215,7 @@ window.__ModuleLoader__.load({
|
|
|
85
215
|
.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88;visibility:visible !important}
|
|
86
216
|
.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
87
217
|
.cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12) !important;border-color:rgba(239,68,68,0.5)}
|
|
88
|
-
.cb-btn
|
|
218
|
+
.cb-btn-disabled{opacity:0.5;cursor:not-allowed}
|
|
89
219
|
|
|
90
220
|
.cb-bar-container{display:flex;flex-direction:column;gap:6px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
91
221
|
.cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
@@ -111,7 +241,7 @@ window.__ModuleLoader__.load({
|
|
|
111
241
|
document.head.appendChild(style)
|
|
112
242
|
}
|
|
113
243
|
|
|
114
|
-
function ProgressBar({ label, percentUsed, remainingPercent, resetsAt }) {
|
|
244
|
+
function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
115
245
|
const used = Math.max(0, Math.min(100, percentUsed || 0))
|
|
116
246
|
let fillColor = 'var(--dsw-alias-state-success-primary)'
|
|
117
247
|
if (used > 75) fillColor = 'var(--dsw-alias-state-warning-primary)'
|
|
@@ -126,7 +256,7 @@ window.__ModuleLoader__.load({
|
|
|
126
256
|
'div',
|
|
127
257
|
{ className: 'cb-bar-head' },
|
|
128
258
|
React.createElement('span', null, label),
|
|
129
|
-
React.createElement('span', { style: { fontWeight: 600 } },
|
|
259
|
+
React.createElement('span', { style: { fontWeight: 600 } }, t('quota.used', { pct: used }))
|
|
130
260
|
),
|
|
131
261
|
React.createElement(
|
|
132
262
|
'div',
|
|
@@ -139,8 +269,8 @@ window.__ModuleLoader__.load({
|
|
|
139
269
|
React.createElement(
|
|
140
270
|
'div',
|
|
141
271
|
{ className: 'cb-bar-meta' },
|
|
142
|
-
React.createElement('span', null,
|
|
143
|
-
React.createElement('span', null,
|
|
272
|
+
React.createElement('span', null, t('quota.remaining', { pct: remainingPercent ?? (100 - used) })),
|
|
273
|
+
React.createElement('span', null, t('quota.reset_at', { time: resetStr }))
|
|
144
274
|
)
|
|
145
275
|
)
|
|
146
276
|
}
|
|
@@ -192,7 +322,7 @@ window.__ModuleLoader__.load({
|
|
|
192
322
|
// Save Key handler
|
|
193
323
|
async function handleSaveKey() {
|
|
194
324
|
if (!apiKeyInput.trim()) {
|
|
195
|
-
setErr('
|
|
325
|
+
setErr(t('key.empty_err'))
|
|
196
326
|
return
|
|
197
327
|
}
|
|
198
328
|
setBusy('save-key')
|
|
@@ -209,7 +339,7 @@ window.__ModuleLoader__.load({
|
|
|
209
339
|
})
|
|
210
340
|
const data = await res.json().catch(() => ({}))
|
|
211
341
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
212
|
-
setMsg(
|
|
342
|
+
setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : 'Error' }))
|
|
213
343
|
setApiKeyInput('')
|
|
214
344
|
await load()
|
|
215
345
|
} catch (e) {
|
|
@@ -229,7 +359,7 @@ window.__ModuleLoader__.load({
|
|
|
229
359
|
const data = await res.json().catch(() => ({}))
|
|
230
360
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
231
361
|
setStatus((prev) => ({ ...prev, usage: data }))
|
|
232
|
-
setMsg('
|
|
362
|
+
setMsg(t('quota.refreshed_msg'))
|
|
233
363
|
} catch (e) {
|
|
234
364
|
setErr(String(e.message || e))
|
|
235
365
|
} finally {
|
|
@@ -246,7 +376,7 @@ window.__ModuleLoader__.load({
|
|
|
246
376
|
const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
|
|
247
377
|
const data = await res.json().catch(() => ({}))
|
|
248
378
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
249
|
-
setMsg(
|
|
379
|
+
setMsg(t('models.synced_msg', { total: data.totalModelsCount, discovered: data.discoveredCount }))
|
|
250
380
|
await load()
|
|
251
381
|
} catch (e) {
|
|
252
382
|
setErr(String(e.message || e))
|
|
@@ -255,7 +385,7 @@ window.__ModuleLoader__.load({
|
|
|
255
385
|
}
|
|
256
386
|
}
|
|
257
387
|
|
|
258
|
-
// Register/
|
|
388
|
+
// Register/Force Re-sync Provider in DSH
|
|
259
389
|
async function handleRegister() {
|
|
260
390
|
setBusy('register')
|
|
261
391
|
setErr('')
|
|
@@ -268,7 +398,7 @@ window.__ModuleLoader__.load({
|
|
|
268
398
|
})
|
|
269
399
|
const data = await res.json().catch(() => ({}))
|
|
270
400
|
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
271
|
-
setMsg(
|
|
401
|
+
setMsg(t('diag.resynced_msg', { count: draft.enabledModels?.length || 0 }))
|
|
272
402
|
await load()
|
|
273
403
|
} catch (e) {
|
|
274
404
|
setErr(String(e.message || e))
|
|
@@ -286,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
286
416
|
const res = await fetch(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
|
|
287
417
|
const data = await res.json().catch(() => ({}))
|
|
288
418
|
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
289
|
-
setMsg('
|
|
419
|
+
setMsg(t('diag.unregistered_msg'))
|
|
290
420
|
await load()
|
|
291
421
|
} catch (e) {
|
|
292
422
|
setErr(String(e.message || e))
|
|
@@ -310,7 +440,7 @@ window.__ModuleLoader__.load({
|
|
|
310
440
|
const data = await res.json().catch(() => ({}))
|
|
311
441
|
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
312
442
|
setSmokeResult(data)
|
|
313
|
-
setMsg(
|
|
443
|
+
setMsg(t('diag.smoke_ok', { latency: data.latencyMs }))
|
|
314
444
|
await load()
|
|
315
445
|
} catch (e) {
|
|
316
446
|
setErr(String(e.message || e))
|
|
@@ -319,26 +449,31 @@ window.__ModuleLoader__.load({
|
|
|
319
449
|
}
|
|
320
450
|
}
|
|
321
451
|
|
|
322
|
-
// Toggle model
|
|
452
|
+
// Toggle model exclusion (disabledModels logic)
|
|
323
453
|
async function handleToggleModel(id) {
|
|
324
454
|
if (!draft) return
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
327
|
-
|
|
455
|
+
const currentDisabled = new Set(draft.disabledModels || [])
|
|
456
|
+
if (currentDisabled.has(id)) {
|
|
457
|
+
currentDisabled.delete(id) // Re-enable
|
|
328
458
|
} else {
|
|
329
|
-
|
|
459
|
+
currentDisabled.add(id) // Disable
|
|
330
460
|
}
|
|
331
|
-
const
|
|
332
|
-
|
|
461
|
+
const nextDisabled = Array.from(currentDisabled)
|
|
462
|
+
|
|
463
|
+
// Compute next enabled models
|
|
464
|
+
const allIds = (status?.availableModels || []).map((m) => m.id)
|
|
465
|
+
const nextEnabled = allIds.filter((mId) => !currentDisabled.has(mId))
|
|
466
|
+
|
|
467
|
+
setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
|
|
333
468
|
|
|
334
469
|
if (scope && snapshotStatus === 'ready') {
|
|
335
|
-
try { await scope.set('
|
|
470
|
+
try { await scope.set('disabledModels', nextDisabled) } catch (_) {}
|
|
336
471
|
}
|
|
337
472
|
try {
|
|
338
473
|
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
339
474
|
method: 'POST',
|
|
340
475
|
headers: { 'Content-Type': 'application/json' },
|
|
341
|
-
body: JSON.stringify({
|
|
476
|
+
body: JSON.stringify({ disabledModels: nextDisabled }),
|
|
342
477
|
})
|
|
343
478
|
} catch {}
|
|
344
479
|
}
|
|
@@ -346,26 +481,30 @@ window.__ModuleLoader__.load({
|
|
|
346
481
|
// Select all / filter models
|
|
347
482
|
async function handleSetModelsFilter(type) {
|
|
348
483
|
if (!status?.availableModels) return
|
|
349
|
-
|
|
484
|
+
const all = status.availableModels
|
|
485
|
+
let allowed = new Set()
|
|
350
486
|
if (type === 'all') {
|
|
351
|
-
|
|
487
|
+
allowed = new Set(all.map((m) => m.id))
|
|
352
488
|
} else if (type === 'vision') {
|
|
353
|
-
|
|
489
|
+
allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
|
|
354
490
|
} else if (type === 'coding') {
|
|
355
|
-
|
|
491
|
+
allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
356
492
|
} else if (type === 'recommended') {
|
|
357
|
-
|
|
493
|
+
allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
358
494
|
}
|
|
359
495
|
|
|
360
|
-
|
|
496
|
+
const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
|
|
497
|
+
const nextEnabled = Array.from(allowed)
|
|
498
|
+
|
|
499
|
+
setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
|
|
361
500
|
if (scope && snapshotStatus === 'ready') {
|
|
362
|
-
try { await scope.set('
|
|
501
|
+
try { await scope.set('disabledModels', nextDisabled) } catch (_) {}
|
|
363
502
|
}
|
|
364
503
|
try {
|
|
365
504
|
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
366
505
|
method: 'POST',
|
|
367
506
|
headers: { 'Content-Type': 'application/json' },
|
|
368
|
-
body: JSON.stringify({
|
|
507
|
+
body: JSON.stringify({ disabledModels: nextDisabled }),
|
|
369
508
|
})
|
|
370
509
|
} catch {}
|
|
371
510
|
}
|
|
@@ -383,30 +522,31 @@ window.__ModuleLoader__.load({
|
|
|
383
522
|
return React.createElement(
|
|
384
523
|
'div',
|
|
385
524
|
{ className: 'cb-page' },
|
|
386
|
-
React.createElement('div', { className: 'cb-alert cb-alert-err' },
|
|
525
|
+
React.createElement('div', { className: 'cb-alert cb-alert-err' }, `${t('settings.loading')}: ${err}`),
|
|
387
526
|
React.createElement(
|
|
388
527
|
'button',
|
|
389
528
|
{
|
|
390
529
|
type: 'button',
|
|
391
|
-
className: 'cb-btn
|
|
530
|
+
className: 'cb-btn',
|
|
392
531
|
style: { marginTop: '12px', alignSelf: 'flex-start' },
|
|
393
532
|
onClick: () => {
|
|
394
533
|
setErr('')
|
|
395
534
|
load().catch((e) => setErr(String(e.message || e)))
|
|
396
535
|
},
|
|
397
536
|
},
|
|
398
|
-
'
|
|
537
|
+
t('settings.retry')
|
|
399
538
|
)
|
|
400
539
|
)
|
|
401
540
|
}
|
|
402
|
-
return React.createElement('div', { className: 'cb-page' }, '
|
|
541
|
+
return React.createElement('div', { className: 'cb-page' }, t('settings.loading'))
|
|
403
542
|
}
|
|
404
543
|
|
|
405
544
|
const healthOk = !!status.health?.ok
|
|
406
545
|
const keyPresent = !!status.key?.present
|
|
407
546
|
const isRegistered = !!status.isRegistered
|
|
408
547
|
const modelsList = status.availableModels || []
|
|
409
|
-
const
|
|
548
|
+
const disabledSet = new Set(draft.disabledModels || [])
|
|
549
|
+
const enabledCount = modelsList.filter((m) => !disabledSet.has(m.id)).length
|
|
410
550
|
const usage = status.usage
|
|
411
551
|
|
|
412
552
|
return React.createElement(
|
|
@@ -420,27 +560,27 @@ window.__ModuleLoader__.load({
|
|
|
420
560
|
React.createElement(
|
|
421
561
|
'div',
|
|
422
562
|
{ className: 'cb-page-title' },
|
|
423
|
-
|
|
563
|
+
`🤖 ${t('header.title')}`,
|
|
424
564
|
React.createElement(
|
|
425
565
|
'span',
|
|
426
566
|
{ className: `cb-badge ${healthOk ? 'cb-badge-ok' : 'cb-badge-bad'}` },
|
|
427
|
-
healthOk ?
|
|
567
|
+
healthOk ? t('badge.online', { latency: status.health?.latencyMs }) : t('badge.offline')
|
|
428
568
|
),
|
|
429
569
|
React.createElement(
|
|
430
570
|
'span',
|
|
431
571
|
{ className: `cb-badge ${keyPresent ? 'cb-badge-ok' : 'cb-badge-warn'}` },
|
|
432
|
-
keyPresent ?
|
|
572
|
+
keyPresent ? t('badge.key_ok', { source: status.key?.source }) : t('badge.key_missing')
|
|
433
573
|
),
|
|
434
574
|
React.createElement(
|
|
435
575
|
'span',
|
|
436
576
|
{ className: `cb-badge ${isRegistered ? 'cb-badge-ok' : 'cb-badge-warn'}` },
|
|
437
|
-
isRegistered ? '
|
|
577
|
+
isRegistered ? t('badge.registered', { count: enabledCount }) : t('badge.not_registered')
|
|
438
578
|
)
|
|
439
579
|
),
|
|
440
580
|
React.createElement(
|
|
441
581
|
'div',
|
|
442
582
|
{ className: 'cb-page-sub' },
|
|
443
|
-
'
|
|
583
|
+
t('header.sub')
|
|
444
584
|
)
|
|
445
585
|
),
|
|
446
586
|
|
|
@@ -455,12 +595,12 @@ window.__ModuleLoader__.load({
|
|
|
455
595
|
React.createElement(
|
|
456
596
|
'div',
|
|
457
597
|
{ className: 'cb-section-title' },
|
|
458
|
-
'
|
|
598
|
+
t('key.title')
|
|
459
599
|
),
|
|
460
600
|
React.createElement(
|
|
461
601
|
'div',
|
|
462
602
|
{ className: 'cb-section-desc' },
|
|
463
|
-
'
|
|
603
|
+
t('key.desc')
|
|
464
604
|
),
|
|
465
605
|
React.createElement(
|
|
466
606
|
'div',
|
|
@@ -468,7 +608,7 @@ window.__ModuleLoader__.load({
|
|
|
468
608
|
React.createElement('input', {
|
|
469
609
|
className: 'cb-input',
|
|
470
610
|
type: showKey ? 'text' : 'password',
|
|
471
|
-
placeholder: keyPresent ? '
|
|
611
|
+
placeholder: keyPresent ? t('key.placeholder_has') : t('key.placeholder_empty'),
|
|
472
612
|
value: apiKeyInput,
|
|
473
613
|
onChange: (e) => setApiKeyInput(e.target.value),
|
|
474
614
|
}),
|
|
@@ -479,7 +619,7 @@ window.__ModuleLoader__.load({
|
|
|
479
619
|
className: 'cb-btn',
|
|
480
620
|
onClick: () => setShowKey((v) => !v),
|
|
481
621
|
},
|
|
482
|
-
showKey ? '
|
|
622
|
+
showKey ? t('key.hide') : t('key.show')
|
|
483
623
|
),
|
|
484
624
|
React.createElement(
|
|
485
625
|
'button',
|
|
@@ -489,13 +629,13 @@ window.__ModuleLoader__.load({
|
|
|
489
629
|
disabled: !!busy || !apiKeyInput.trim(),
|
|
490
630
|
onClick: handleSaveKey,
|
|
491
631
|
},
|
|
492
|
-
busy === 'save-key' ? '
|
|
632
|
+
busy === 'save-key' ? t('key.saving') : t('key.save')
|
|
493
633
|
)
|
|
494
634
|
),
|
|
495
635
|
React.createElement(
|
|
496
636
|
'div',
|
|
497
637
|
{ className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
498
|
-
React.createElement('span', null,
|
|
638
|
+
React.createElement('span', null, t('key.env_label')),
|
|
499
639
|
React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
|
|
500
640
|
React.createElement('span', null, ` · `),
|
|
501
641
|
React.createElement(
|
|
@@ -506,7 +646,7 @@ window.__ModuleLoader__.load({
|
|
|
506
646
|
rel: 'noreferrer',
|
|
507
647
|
style: { color: 'var(--dsw-alias-state-brand-primary)' },
|
|
508
648
|
},
|
|
509
|
-
'
|
|
649
|
+
t('key.get_key')
|
|
510
650
|
)
|
|
511
651
|
)
|
|
512
652
|
),
|
|
@@ -525,7 +665,7 @@ window.__ModuleLoader__.load({
|
|
|
525
665
|
? React.createElement(
|
|
526
666
|
'div',
|
|
527
667
|
{ style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
|
|
528
|
-
|
|
668
|
+
t('quota.reset_at', { time: new Date(status.quotaWarning.resetsAt).toLocaleTimeString() })
|
|
529
669
|
)
|
|
530
670
|
: null
|
|
531
671
|
)
|
|
@@ -540,7 +680,7 @@ window.__ModuleLoader__.load({
|
|
|
540
680
|
React.createElement(
|
|
541
681
|
'div',
|
|
542
682
|
{ className: 'cb-section-title' },
|
|
543
|
-
'
|
|
683
|
+
t('quota.title'),
|
|
544
684
|
React.createElement(
|
|
545
685
|
'button',
|
|
546
686
|
{
|
|
@@ -549,43 +689,45 @@ window.__ModuleLoader__.load({
|
|
|
549
689
|
disabled: !!busy,
|
|
550
690
|
onClick: handleRefreshQuota,
|
|
551
691
|
},
|
|
552
|
-
busy === 'refresh-quota' ? '
|
|
692
|
+
busy === 'refresh-quota' ? t('quota.refreshing') : t('quota.refresh')
|
|
553
693
|
)
|
|
554
694
|
),
|
|
555
695
|
React.createElement(
|
|
556
696
|
'div',
|
|
557
697
|
{ className: 'cb-section-desc' },
|
|
558
698
|
usage?.user?.email
|
|
559
|
-
?
|
|
560
|
-
: '
|
|
699
|
+
? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass ($9.99/mo)' })
|
|
700
|
+
: t('quota.desc')
|
|
561
701
|
),
|
|
562
702
|
React.createElement(
|
|
563
703
|
'div',
|
|
564
704
|
{ className: 'cb-grid-2' },
|
|
565
705
|
React.createElement(ProgressBar, {
|
|
566
|
-
label: '
|
|
706
|
+
label: t('quota.window_5h'),
|
|
567
707
|
percentUsed: usage?.windows?.fiveHour?.percentUsed || 0,
|
|
568
708
|
remainingPercent: usage?.windows?.fiveHour?.remainingPercent || 100,
|
|
569
709
|
resetsAt: usage?.windows?.fiveHour?.resetsAt,
|
|
710
|
+
t,
|
|
570
711
|
}),
|
|
571
712
|
React.createElement(ProgressBar, {
|
|
572
|
-
label: '
|
|
713
|
+
label: t('quota.window_weekly'),
|
|
573
714
|
percentUsed: usage?.windows?.weekly?.percentUsed || 0,
|
|
574
715
|
remainingPercent: usage?.windows?.weekly?.remainingPercent || 100,
|
|
575
716
|
resetsAt: usage?.windows?.weekly?.resetsAt,
|
|
717
|
+
t,
|
|
576
718
|
})
|
|
577
719
|
)
|
|
578
720
|
)
|
|
579
721
|
: null,
|
|
580
722
|
|
|
581
|
-
// Card 3: Model Picker Management with Official Plan Sync
|
|
723
|
+
// Card 3: Model Picker Management with Official Plan Sync & Auto-Enable
|
|
582
724
|
React.createElement(
|
|
583
725
|
'div',
|
|
584
726
|
{ className: 'cb-section-card' },
|
|
585
727
|
React.createElement(
|
|
586
728
|
'div',
|
|
587
729
|
{ className: 'cb-section-title' },
|
|
588
|
-
|
|
730
|
+
t('models.title', { enabled: enabledCount, total: modelsList.length }),
|
|
589
731
|
React.createElement(
|
|
590
732
|
'div',
|
|
591
733
|
{ className: 'cb-row' },
|
|
@@ -597,34 +739,34 @@ window.__ModuleLoader__.load({
|
|
|
597
739
|
disabled: !!busy || !keyPresent,
|
|
598
740
|
onClick: handleSyncPlanModels,
|
|
599
741
|
},
|
|
600
|
-
busy === 'sync-models' ? '
|
|
742
|
+
busy === 'sync-models' ? t('models.syncing') : t('models.sync')
|
|
601
743
|
),
|
|
602
744
|
React.createElement(
|
|
603
745
|
'button',
|
|
604
746
|
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
|
|
605
|
-
'
|
|
747
|
+
t('models.all')
|
|
606
748
|
),
|
|
607
749
|
React.createElement(
|
|
608
750
|
'button',
|
|
609
751
|
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('vision') },
|
|
610
|
-
'
|
|
752
|
+
t('models.vision')
|
|
611
753
|
),
|
|
612
754
|
React.createElement(
|
|
613
755
|
'button',
|
|
614
756
|
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('coding') },
|
|
615
|
-
'
|
|
757
|
+
t('models.coding')
|
|
616
758
|
),
|
|
617
759
|
React.createElement(
|
|
618
760
|
'button',
|
|
619
761
|
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('recommended') },
|
|
620
|
-
'
|
|
762
|
+
t('models.recommended')
|
|
621
763
|
)
|
|
622
764
|
)
|
|
623
765
|
),
|
|
624
766
|
React.createElement(
|
|
625
767
|
'div',
|
|
626
768
|
{ className: 'cb-section-desc' },
|
|
627
|
-
'
|
|
769
|
+
t('models.desc')
|
|
628
770
|
),
|
|
629
771
|
React.createElement(
|
|
630
772
|
'table',
|
|
@@ -635,18 +777,19 @@ window.__ModuleLoader__.load({
|
|
|
635
777
|
React.createElement(
|
|
636
778
|
'tr',
|
|
637
779
|
null,
|
|
638
|
-
React.createElement('th', { style: { width: '40px' } }, '
|
|
639
|
-
React.createElement('th', null, '
|
|
640
|
-
React.createElement('th', null, '
|
|
641
|
-
React.createElement('th', null, '
|
|
642
|
-
React.createElement('th', null, '
|
|
780
|
+
React.createElement('th', { style: { width: '40px' } }, t('models.th_active')),
|
|
781
|
+
React.createElement('th', null, t('models.th_name')),
|
|
782
|
+
React.createElement('th', null, t('models.th_id')),
|
|
783
|
+
React.createElement('th', null, t('models.th_ctx')),
|
|
784
|
+
React.createElement('th', null, t('models.th_caps'))
|
|
643
785
|
)
|
|
644
786
|
),
|
|
645
787
|
React.createElement(
|
|
646
788
|
'tbody',
|
|
647
789
|
null,
|
|
648
|
-
modelsList.map((m) =>
|
|
649
|
-
|
|
790
|
+
modelsList.map((m) => {
|
|
791
|
+
const isEnabled = !disabledSet.has(m.id)
|
|
792
|
+
return React.createElement(
|
|
650
793
|
'tr',
|
|
651
794
|
{ key: m.id },
|
|
652
795
|
React.createElement(
|
|
@@ -654,7 +797,7 @@ window.__ModuleLoader__.load({
|
|
|
654
797
|
null,
|
|
655
798
|
React.createElement('input', {
|
|
656
799
|
type: 'checkbox',
|
|
657
|
-
checked:
|
|
800
|
+
checked: isEnabled,
|
|
658
801
|
onChange: () => handleToggleModel(m.id),
|
|
659
802
|
})
|
|
660
803
|
),
|
|
@@ -663,7 +806,10 @@ window.__ModuleLoader__.load({
|
|
|
663
806
|
null,
|
|
664
807
|
React.createElement('strong', null, m.name),
|
|
665
808
|
m.recommended
|
|
666
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, '
|
|
809
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('models.star'))
|
|
810
|
+
: null,
|
|
811
|
+
m.isCustom
|
|
812
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '6px', opacity: 0.8 } }, t('models.auto_new'))
|
|
667
813
|
: null
|
|
668
814
|
),
|
|
669
815
|
React.createElement('td', null, React.createElement('code', null, m.id)),
|
|
@@ -677,7 +823,7 @@ window.__ModuleLoader__.load({
|
|
|
677
823
|
: null
|
|
678
824
|
)
|
|
679
825
|
)
|
|
680
|
-
)
|
|
826
|
+
})
|
|
681
827
|
)
|
|
682
828
|
)
|
|
683
829
|
),
|
|
@@ -689,12 +835,12 @@ window.__ModuleLoader__.load({
|
|
|
689
835
|
React.createElement(
|
|
690
836
|
'div',
|
|
691
837
|
{ className: 'cb-section-title' },
|
|
692
|
-
'
|
|
838
|
+
t('stats.title')
|
|
693
839
|
),
|
|
694
840
|
React.createElement(
|
|
695
841
|
'div',
|
|
696
842
|
{ className: 'cb-section-desc' },
|
|
697
|
-
'
|
|
843
|
+
t('stats.desc')
|
|
698
844
|
),
|
|
699
845
|
React.createElement(
|
|
700
846
|
'div',
|
|
@@ -703,25 +849,25 @@ window.__ModuleLoader__.load({
|
|
|
703
849
|
'div',
|
|
704
850
|
{ className: 'cb-stat-box' },
|
|
705
851
|
React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
|
|
706
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, '
|
|
852
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.requests'))
|
|
707
853
|
),
|
|
708
854
|
React.createElement(
|
|
709
855
|
'div',
|
|
710
856
|
{ className: 'cb-stat-box' },
|
|
711
857
|
React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
|
|
712
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, '
|
|
858
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.tokens'))
|
|
713
859
|
),
|
|
714
860
|
React.createElement(
|
|
715
861
|
'div',
|
|
716
862
|
{ className: 'cb-stat-box' },
|
|
717
|
-
React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs}
|
|
718
|
-
React.createElement('div', { className: 'cb-stat-lbl' },
|
|
863
|
+
React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} ms` : '—'),
|
|
864
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.latency'))
|
|
719
865
|
),
|
|
720
866
|
React.createElement(
|
|
721
867
|
'div',
|
|
722
868
|
{ className: 'cb-stat-box' },
|
|
723
869
|
React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
|
|
724
|
-
React.createElement('div', { className: 'cb-stat-lbl' }, '
|
|
870
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.last_req'))
|
|
725
871
|
)
|
|
726
872
|
)
|
|
727
873
|
),
|
|
@@ -733,12 +879,12 @@ window.__ModuleLoader__.load({
|
|
|
733
879
|
React.createElement(
|
|
734
880
|
'div',
|
|
735
881
|
{ className: 'cb-section-title' },
|
|
736
|
-
'
|
|
882
|
+
t('diag.title')
|
|
737
883
|
),
|
|
738
884
|
React.createElement(
|
|
739
885
|
'div',
|
|
740
886
|
{ className: 'cb-section-desc' },
|
|
741
|
-
'
|
|
887
|
+
t('diag.desc')
|
|
742
888
|
),
|
|
743
889
|
React.createElement(
|
|
744
890
|
'div',
|
|
@@ -751,7 +897,7 @@ window.__ModuleLoader__.load({
|
|
|
751
897
|
disabled: !!busy || !keyPresent,
|
|
752
898
|
onClick: handleSmoke,
|
|
753
899
|
},
|
|
754
|
-
busy === 'smoke' ? '
|
|
900
|
+
busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
|
|
755
901
|
),
|
|
756
902
|
isRegistered
|
|
757
903
|
? React.createElement(
|
|
@@ -762,7 +908,7 @@ window.__ModuleLoader__.load({
|
|
|
762
908
|
disabled: !!busy,
|
|
763
909
|
onClick: handleUnregister,
|
|
764
910
|
},
|
|
765
|
-
busy === 'unregister' ? '
|
|
911
|
+
busy === 'unregister' ? t('diag.unregistering') : t('diag.unregister_btn')
|
|
766
912
|
)
|
|
767
913
|
: null,
|
|
768
914
|
React.createElement(
|
|
@@ -774,14 +920,14 @@ window.__ModuleLoader__.load({
|
|
|
774
920
|
disabled: !!busy,
|
|
775
921
|
onClick: handleRegister,
|
|
776
922
|
},
|
|
777
|
-
busy === 'register' ? '
|
|
923
|
+
busy === 'register' ? t('diag.resyncing') : t('diag.resync_btn')
|
|
778
924
|
)
|
|
779
925
|
),
|
|
780
926
|
smokeResult
|
|
781
927
|
? React.createElement(
|
|
782
928
|
'div',
|
|
783
929
|
{ className: 'cb-preview' },
|
|
784
|
-
`✅
|
|
930
|
+
`✅ Latency: ${smokeResult.latencyMs} ms | Model: ${smokeResult.model}\nResponse: ${smokeResult.preview || '(empty)'}`
|
|
785
931
|
)
|
|
786
932
|
: null
|
|
787
933
|
)
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PROVIDER_DISPLAY_NAME,
|
|
9
9
|
getAllModels,
|
|
10
10
|
getDefaultModelIds,
|
|
11
|
+
getActiveModelIds,
|
|
11
12
|
parsePlanIncludedModels,
|
|
12
13
|
} from './models.js'
|
|
13
14
|
import {
|
|
@@ -44,8 +45,10 @@ export const Config = z.object({
|
|
|
44
45
|
.description('Credential / env name containing the ClinePass API key (never store key directly here).'),
|
|
45
46
|
defaultModel: z.string().default(DEFAULT_MODEL_ID)
|
|
46
47
|
.description('Default model ID for chat and smoke tests.'),
|
|
47
|
-
|
|
48
|
-
.description('List of model IDs
|
|
48
|
+
disabledModels: z.array(z.string()).default([])
|
|
49
|
+
.description('List of model IDs explicitly disabled by the user (new models are enabled automatically).'),
|
|
50
|
+
enabledModels: z.array(z.string()).default([])
|
|
51
|
+
.description('Deprecated: preserved for backwards compatibility with earlier versions.'),
|
|
49
52
|
dynamicModels: z.array(z.object({
|
|
50
53
|
id: z.string(),
|
|
51
54
|
name: z.string(),
|
|
@@ -65,16 +68,26 @@ export const Config = z.object({
|
|
|
65
68
|
|
|
66
69
|
function publicConfig(cfg) {
|
|
67
70
|
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
68
|
-
const
|
|
71
|
+
const allModels = getAllModels(dynamic)
|
|
72
|
+
const allDefaultIds = allModels.map((m) => m.id)
|
|
73
|
+
|
|
74
|
+
// Migration / compatibility: if disabledModels was not yet set, but enabledModels was provided
|
|
75
|
+
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
|
|
76
|
+
if (!Array.isArray(cfg?.disabledModels) || (cfg.disabledModels.length === 0 && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0)) {
|
|
77
|
+
const enabledSet = new Set(cfg.enabledModels)
|
|
78
|
+
disabledList = allDefaultIds.filter((id) => !enabledSet.has(id))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const activeIds = getActiveModelIds(allDefaultIds, disabledList)
|
|
82
|
+
|
|
69
83
|
return {
|
|
70
84
|
enabled: !!cfg?.enabled,
|
|
71
85
|
baseUrl: normalizeBaseUrl(cfg?.baseUrl),
|
|
72
86
|
apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
73
87
|
defaultModel: cfg?.defaultModel || DEFAULT_MODEL_ID,
|
|
74
88
|
dynamicModels: dynamic,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: allDefaultIds,
|
|
89
|
+
disabledModels: disabledList,
|
|
90
|
+
enabledModels: activeIds,
|
|
78
91
|
timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
|
|
79
92
|
smokeTimeoutMs: Number(cfg?.smokeTimeoutMs) || DEFAULT_SMOKE_TIMEOUT_MS,
|
|
80
93
|
}
|
|
@@ -115,13 +128,16 @@ async function checkRegisteredInPiAi(ctx) {
|
|
|
115
128
|
async function buildStatus(ctx, cfg) {
|
|
116
129
|
const pub = publicConfig(cfg)
|
|
117
130
|
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
118
|
-
|
|
131
|
+
// Non-blocking quick health probe with low timeout so settings page loads instantly
|
|
132
|
+
const probeTimeout = Math.min(2500, pub.timeoutMs || 2500)
|
|
133
|
+
const health = await probeHealth(pub.baseUrl, { timeoutMs: probeTimeout })
|
|
119
134
|
const isRegistered = await checkRegisteredInPiAi(ctx)
|
|
120
135
|
const allModels = getAllModels(pub.dynamicModels)
|
|
121
136
|
|
|
122
137
|
let usage = null
|
|
123
138
|
if (key.value) {
|
|
124
|
-
|
|
139
|
+
// Uses 60s cache; if cache miss, times out quickly
|
|
140
|
+
usage = await fetchUsageLimits(pub.baseUrl, key.value, { timeoutMs: probeTimeout }).catch(() => null)
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
// Evaluate warning state
|
|
@@ -162,7 +178,7 @@ async function buildStatus(ctx, cfg) {
|
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
async function upsertPiAiProvider(ctx, cfg,
|
|
181
|
+
async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
|
|
166
182
|
const settings = ctx?.get?.('settings')
|
|
167
183
|
if (!settings?.mutate) {
|
|
168
184
|
throw new Error('DSH settings service unavailable')
|
|
@@ -170,8 +186,8 @@ async function upsertPiAiProvider(ctx, cfg, dynamicModelIds) {
|
|
|
170
186
|
|
|
171
187
|
const pub = publicConfig(cfg)
|
|
172
188
|
const allModels = getAllModels(pub.dynamicModels)
|
|
173
|
-
const
|
|
174
|
-
const modelsToRegister = allModels.filter((m) =>
|
|
189
|
+
const allowedSet = new Set(activeModelIds || pub.enabledModels)
|
|
190
|
+
const modelsToRegister = allModels.filter((m) => allowedSet.has(m.id))
|
|
175
191
|
|
|
176
192
|
const providerObj = buildPiAiProvider({
|
|
177
193
|
baseUrl: pub.baseUrl,
|
|
@@ -218,6 +234,56 @@ export function apply(ctx, config) {
|
|
|
218
234
|
let liveCfg = Config(structuredClone(config || {}))
|
|
219
235
|
let settingsApi
|
|
220
236
|
|
|
237
|
+
// Declarative sync helper: auto-registers or unregisters provider based on config & key availability
|
|
238
|
+
const syncProviderState = async (cfg) => {
|
|
239
|
+
try {
|
|
240
|
+
const pub = publicConfig(cfg)
|
|
241
|
+
if (!pub.enabled) {
|
|
242
|
+
if (await checkRegisteredInPiAi(ctx)) {
|
|
243
|
+
await removePiAiProvider(ctx)
|
|
244
|
+
}
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
248
|
+
if (key.value) {
|
|
249
|
+
await upsertPiAiProvider(ctx, cfg, pub.enabledModels)
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
/* ignore transient settings unavailable */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Background check for subscription plan models (runs once on startup or key save)
|
|
257
|
+
const autoDiscoverPlanModels = async (cfg) => {
|
|
258
|
+
try {
|
|
259
|
+
const pub = publicConfig(cfg)
|
|
260
|
+
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
261
|
+
if (!key.value) return
|
|
262
|
+
|
|
263
|
+
const usageData = await fetchUsageLimits(pub.baseUrl, key.value, {
|
|
264
|
+
timeoutMs: Math.min(pub.timeoutMs, 5000),
|
|
265
|
+
bypassCache: true,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
if (usageData?.ok && Array.isArray(usageData.dynamicModels) && usageData.dynamicModels.length > 0) {
|
|
269
|
+
const existingDynamic = pub.dynamicModels || []
|
|
270
|
+
const existingIds = new Set(existingDynamic.map((m) => m.id))
|
|
271
|
+
const hasNew = usageData.dynamicModels.some((m) => !existingIds.has(m.id))
|
|
272
|
+
|
|
273
|
+
if (hasNew && settingsApi?.replace) {
|
|
274
|
+
const next = Config({
|
|
275
|
+
...liveCfg,
|
|
276
|
+
dynamicModels: usageData.dynamicModels,
|
|
277
|
+
})
|
|
278
|
+
await settingsApi.replace(next)
|
|
279
|
+
await syncProviderState(next)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
/* best-effort discovery */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
221
287
|
const settingsService = ctx.get('settings')
|
|
222
288
|
if (typeof settingsService?.register === 'function') {
|
|
223
289
|
const scope = settingsService.register(NS, Config, { base: config })
|
|
@@ -225,11 +291,17 @@ export function apply(ctx, config) {
|
|
|
225
291
|
liveCfg = Config(scope.get() ?? config)
|
|
226
292
|
ctx.effect(() => scope.watch((next) => {
|
|
227
293
|
liveCfg = Config(next ?? config)
|
|
294
|
+
syncProviderState(liveCfg)
|
|
228
295
|
}), 'dsh-clinebot: settings')
|
|
229
296
|
}
|
|
230
297
|
|
|
231
298
|
const live = () => liveCfg
|
|
232
299
|
|
|
300
|
+
// On startup: ensure provider is synced to llm-pi-ai if key is present
|
|
301
|
+
syncProviderState(liveCfg)
|
|
302
|
+
// Background discover plan models on startup
|
|
303
|
+
setTimeout(() => autoDiscoverPlanModels(liveCfg), 500)
|
|
304
|
+
|
|
233
305
|
// Web server HTTP route handlers
|
|
234
306
|
if (ctx.webServer?.register) {
|
|
235
307
|
// 1. GET /dsh-clinebot/status
|
|
@@ -274,6 +346,7 @@ export function apply(ctx, config) {
|
|
|
274
346
|
try {
|
|
275
347
|
const parsed = Config({ ...publicConfig(live()), ...payload })
|
|
276
348
|
await settingsApi.replace(parsed)
|
|
349
|
+
await syncProviderState(parsed)
|
|
277
350
|
writeJson(res, 200, { ok: true, config: publicConfig(live()) })
|
|
278
351
|
} catch (e) {
|
|
279
352
|
writeJson(res, 400, { ok: false, error: String(e?.message || e) })
|
|
@@ -304,6 +377,10 @@ export function apply(ctx, config) {
|
|
|
304
377
|
const targetEnvName = String(body.apiKeyEnv || pub.apiKeyEnv || DEFAULT_API_KEY_ENV).trim()
|
|
305
378
|
await saveCredentialKey(ctx, targetEnvName, apiKey)
|
|
306
379
|
|
|
380
|
+
// Auto-sync provider to DSH Models and discover plan models
|
|
381
|
+
await syncProviderState(live())
|
|
382
|
+
autoDiscoverPlanModels(live())
|
|
383
|
+
|
|
307
384
|
// Run validation probe with the newly saved key
|
|
308
385
|
const validation = await smokeChat(pub.baseUrl, apiKey, {
|
|
309
386
|
model: pub.defaultModel,
|
|
@@ -359,7 +436,8 @@ export function apply(ctx, config) {
|
|
|
359
436
|
const bodyBuf = await readBody(req)
|
|
360
437
|
let body = {}
|
|
361
438
|
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
362
|
-
const
|
|
439
|
+
const activeModels = body.models || publicConfig(live()).enabledModels
|
|
440
|
+
const result = await upsertPiAiProvider(ctx, live(), activeModels)
|
|
363
441
|
writeJson(res, 200, { ok: true, provider: result })
|
|
364
442
|
} catch (err) {
|
|
365
443
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
@@ -457,24 +535,14 @@ export function apply(ctx, config) {
|
|
|
457
535
|
|
|
458
536
|
const dynamicModels = Array.isArray(usageData.dynamicModels) ? usageData.dynamicModels : []
|
|
459
537
|
const allModels = getAllModels(dynamicModels)
|
|
460
|
-
const allIds = allModels.map((m) => m.id)
|
|
461
|
-
|
|
462
|
-
// Preserve currently enabled models, plus add any newly discovered ones
|
|
463
|
-
const existingEnabled = new Set(live().enabledModels || getDefaultModelIds())
|
|
464
|
-
for (const m of allModels) {
|
|
465
|
-
existingEnabled.add(m.id)
|
|
466
|
-
}
|
|
467
538
|
|
|
468
539
|
if (settingsApi?.replace) {
|
|
469
540
|
const next = Config({
|
|
470
541
|
...live(),
|
|
471
542
|
dynamicModels,
|
|
472
|
-
enabledModels: Array.from(existingEnabled),
|
|
473
543
|
})
|
|
474
544
|
await settingsApi.replace(next)
|
|
475
|
-
|
|
476
|
-
await upsertPiAiProvider(ctx, next, Array.from(existingEnabled))
|
|
477
|
-
}
|
|
545
|
+
await syncProviderState(next)
|
|
478
546
|
}
|
|
479
547
|
|
|
480
548
|
return writeJson(res, 200, {
|
|
@@ -490,7 +558,7 @@ export function apply(ctx, config) {
|
|
|
490
558
|
},
|
|
491
559
|
}), 'dsh-clinebot: /models/sync')
|
|
492
560
|
|
|
493
|
-
// 9. POST /dsh-clinebot/models/toggle — toggle enabled status in picker
|
|
561
|
+
// 9. POST /dsh-clinebot/models/toggle — toggle disabled/enabled status in picker
|
|
494
562
|
ctx.effect(() => ctx.webServer.register({
|
|
495
563
|
kind: 'exact',
|
|
496
564
|
path: '/dsh-clinebot/models/toggle',
|
|
@@ -504,16 +572,24 @@ export function apply(ctx, config) {
|
|
|
504
572
|
let body = {}
|
|
505
573
|
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
506
574
|
|
|
507
|
-
if (
|
|
508
|
-
const patch = {
|
|
575
|
+
if (settingsApi?.replace) {
|
|
576
|
+
const patch = {}
|
|
577
|
+
if (Array.isArray(body.disabledModels)) {
|
|
578
|
+
patch.disabledModels = body.disabledModels
|
|
579
|
+
} else if (Array.isArray(body.enabledModels)) {
|
|
580
|
+
// Convert legacy enabledModels toggle to disabledModels
|
|
581
|
+
const allModels = getAllModels(live().dynamicModels)
|
|
582
|
+
const enabledSet = new Set(body.enabledModels)
|
|
583
|
+
patch.disabledModels = allModels.map((m) => m.id).filter((id) => !enabledSet.has(id))
|
|
584
|
+
}
|
|
509
585
|
if (body.defaultModel) patch.defaultModel = body.defaultModel
|
|
510
586
|
const next = Config({ ...live(), ...patch })
|
|
511
587
|
await settingsApi.replace(next)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
588
|
+
await syncProviderState(next)
|
|
589
|
+
writeJson(res, 200, { ok: true, disabledModels: next.disabledModels, enabledModels: publicConfig(next).enabledModels })
|
|
590
|
+
} else {
|
|
591
|
+
writeJson(res, 200, { ok: true })
|
|
515
592
|
}
|
|
516
|
-
writeJson(res, 200, { ok: true, enabledModels: body.enabledModels })
|
|
517
593
|
} catch (err) {
|
|
518
594
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
519
595
|
}
|
package/lib/models.js
CHANGED
|
@@ -218,3 +218,14 @@ export function getDefaultModelIds(dynamicModels = []) {
|
|
|
218
218
|
return getAllModels(dynamicModels).map((m) => m.id)
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Filter out models explicitly disabled by user.
|
|
223
|
+
* Any newly introduced model (not in disabledModelIds) is active by default.
|
|
224
|
+
*/
|
|
225
|
+
export function getActiveModelIds(allModels = [], disabledModelIds = []) {
|
|
226
|
+
const disabledSet = new Set(Array.isArray(disabledModelIds) ? disabledModelIds : [])
|
|
227
|
+
return (Array.isArray(allModels) ? allModels : [])
|
|
228
|
+
.map((m) => (typeof m === 'string' ? m : m?.id))
|
|
229
|
+
.filter((id) => Boolean(id && !disabledSet.has(id)))
|
|
230
|
+
}
|
|
231
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|