@goodandready/dsh-clinebot 0.3.22 → 0.3.24

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,24 @@ 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.24] - 2026-09-24
9
+
10
+ ### Fixed
11
+
12
+ - The settings card opens. Host settings are published as a namespace, copied to plain values before use, and account credential names are read as text, so the form renders instead of a React error.
13
+
14
+ ## [0.3.23] - 2026-09-24
15
+
16
+ ### Fixed
17
+ - **React Error #185 Infinite Loop Fix (#74 / GitHub #6)**: Restored stable reference constant `SNAPSHOT_READY` in `SettingsPage.getSnapshot` for `useSyncExternalStore`. Returning an inline object literal caused `Object.is` mismatch on every render, triggering an infinite update depth loop (`Maximum update depth exceeded`). The snapshot fallback now returns frozen `SNAPSHOT_READY`, guaranteeing reference stability while keeping the page unblocked in standalone mode.
18
+ - The settings schema marks the user-editable fields volatile, so DSH includes the `dsh-clinebot` namespace and the configuration page is no longer stuck on "host namespace is not ready".
19
+ - Nested fields inside an already volatile array stay plain. The plugin copies host volatile references to plain values before `structuredClone`, so startup does not reject the config or fail to clone a getter.
20
+ - The configuration page renders its own status payload when the host form snapshot is still loading. A missing host namespace still shows the unavailable banner until that payload arrives.
21
+ - Account credential names are read as text. A host volatile reference was sent through as an empty object, and React error #31 replaced the settings card.
22
+ - The settings page binds `configForms.get('dsh-clinebot')` on DSH 0.1.7. The removed `settingsScope` and `lanSettings` services are no longer consulted, so the plugin configuration page can render after install.
23
+ - `GET /dsh-clinebot/status`, `/config`, `/usage`, and `/auth/status` use the same trusted-request check as the write routes. Usage responses keep the quota fields the card shows and omit the raw provider payload.
24
+ - `/cline models` prints the model total in English.
25
+
8
26
  ## [0.3.22] - 2026-09-24
9
27
 
10
28
  ### Fixed
@@ -186,7 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
186
204
  ## [0.3.4] - 2026-09-08
187
205
 
188
206
  ### Fixed
189
- - **Cache Path Resolution**: Corrected POSIX home directory expansion (`~/`) in `resolvePathWithHome` so `~/.dsh/clinebot-models-cache.json` resolves cleanly to `/home/vadim/.dsh/...` instead of root-level paths.
207
+ - **Cache Path Resolution**: Corrected POSIX home directory expansion (`~/`) in `resolvePathWithHome` so `~/.dsh/clinebot-models-cache.json` resolves under the user home directory instead of a root-level path.
190
208
 
191
209
  ## [0.3.3] - 2026-09-08
192
210
 
package/README.md CHANGED
@@ -50,7 +50,7 @@ Integrating ClinePass into DeepSeek Harness (DSH) natively poses key challenges:
50
50
  * 🚀 **One-Click In-App Updater**: Upgrade `@goodandready/dsh-clinebot` directly from the DSH UI or trigger secure loopback updates via `/dsh-clinebot/update`.
51
51
  * ⚡ **SWR Quota & Health Caching**: Instantaneous response time (<2ms) on status queries with background revalidation.
52
52
  * 🔀 **Smart Quota-Aware Failover**: Automatic multi-account rotation skipping exhausted accounts and recovering when `resetsAt` is reached.
53
- * 🖥️ **Dedicated Settings Page**: Full-width page in DSH Settings (`Settings → ClineBot`).
53
+ * 🖥️ **Plugin configuration page**: Open the installed ClineBot plugin and choose configure. The page shows the credential name, models, quota, and accounts. It is not a separate sidebar section.
54
54
  * 🔄 **Dynamic Subscription Model Sync**: Automatically pulls real models included in your ClinePass plan directly from `GET /api/v1/users/me/plan` with one-click DSH provider sync.
55
55
  * ⚠️ **Quota Exhaustion Alerts**: Real-time visual warning banners when 5-hour rolling limit reaches 80% (warning) and 95% (exhausted), complete with countdown to reset.
56
56
  * 📈 **Session Metrics Telemetry**: Live dashboard tracking request counts, token consumption estimates, latency, and last-request timestamp.
@@ -118,7 +118,7 @@ graph LR
118
118
  * **`lib/index.js`**:
119
119
  Cordis service module managing routes (including `POST /dsh-clinebot/models/sync`), quota warnings threshold evaluation, credentials, and registering the `/cline` slash command.
120
120
  * **`lib/client.js`**:
121
- Full-featured dedicated Settings section (`settings.section`, order 28) with live quota bars, exhaustion warning banner, session metrics telemetry card, one-click plan sync button, and plugin accordion (`settings.plugin.item`).
121
+ Configuration page on the plugin row (`plugins.row.config`) with live quota bars, an exhaustion warning, session metrics, one-click plan sync, and a fallback plugins card (`settings.plugin.item`). There is no `settings.section` sidebar entry.
122
122
 
123
123
  ---
124
124
 
package/README.ru.md CHANGED
@@ -50,7 +50,7 @@
50
50
  * 🚀 **Обновление в один клик**: Обновление плагина прямо из интерфейса DSH или через защищённый loopback-эндпоинт `/dsh-clinebot/update`.
51
51
  * ⚡ **SWR-кэширование квот и здоровья**: Мгновенный ответ (<2 мс) на запросы статуса с фоновым обновлением данных без блокировки UI.
52
52
  * 🔀 **Smart Quota-Aware Failover**: Интеллектуальная ротация аккаунтов с обходом исчерпанных лимитов и авто-восстановлением при наступлении `resetsAt`.
53
- * 🖥️ **Отдельная страница в Настройках**: собственная полноэкранная страница в меню Настроек DSH (`Настройки → ClineBot`).
53
+ * 🖥️ **Страница конфигурации плагина**: откройте установленный ClineBot и перейдите в настройки плагина. Там имя учётных данных, модели, квота и аккаунты. Отдельного пункта бокового меню нет.
54
54
  * 🔄 **Динамическая синхронизация моделей подписки**: автоматическое получение реального списка моделей из `GET /api/v1/users/me/plan` и мгновенное обновление провайдера DSH в один клик.
55
55
  * ⚠️ **Предупреждения об исчерпании квоты**: баннеры предупреждения при достижении 80% (внимание) и 95% (исчерпано) 5-часового лимита с таймером сброса.
56
56
  * 📈 **Метрики сессии**: учет количества запросов, расчетных токенов (Prompt / Completion), задержки и времени последнего вызова.
@@ -115,7 +115,7 @@ graph LR
115
115
  * `smokeChat`: замер задержки и тестовый пинг с фиксацией статистики.
116
116
  * `buildPiAiProvider`: генерация конфигурации провайдера для `llm-pi-ai` (`api: 'openai-completions'`).
117
117
  * **`lib/index.js`**: сервис Cordis, регистрация системных маршрутов (включая `/dsh-clinebot/models/sync`), расчет порогов предупреждения квоты и слэш-команды `/cline`.
118
- * **`lib/client.js`**: полнофункциональный раздел настроек (`settings.section`, order 28) с баннерами предупреждений, карточкой статистики сессии, кнопкой синхронизации моделей плана и аккордеоном плагина (`settings.plugin.item`).
118
+ * **`lib/client.js`**: страница конфигурации строки плагина (`plugins.row.config`) с предупреждениями, статистикой сессии, синхронизацией моделей и запасной карточкой (`settings.plugin.item`). Пункта `settings.section` в боковом меню нет.
119
119
 
120
120
  ---
121
121
 
package/README.zh.md CHANGED
@@ -50,7 +50,7 @@
50
50
  * 🚀 **应用内一键更新**:直接在 DSH 界面检查并升级插件,或通过受保护的 `/dsh-clinebot/update` 进行本地安全更新。
51
51
  * ⚡ **SWR 配额与健康状态缓存**:状态查询毫秒级响应(<2ms),并在后台静默更新,不阻塞前端渲染。
52
52
  * 🔀 **智能配额故障转移 (Smart Failover)**:多账号池自动轮询,避开耗尽账号并在 `resetsAt` 到达后自动恢复。
53
- * 🖥️ **专属设置大页**:在 DSH 设置中提供全宽独立页面(`设置 → ClineBot`)。
53
+ * 🖥️ **插件配置页**:打开已安装的 ClineBot,进入配置。页面包含凭据名、模型、配额和账号,不单独占用侧边栏。
54
54
  * 🔄 **订阅模型动态同步**:从官方 `GET /api/v1/users/me/plan` 自动提取真实包含模型,一键原子级同步至 DSH 提供商配置,无需等待插件更新。
55
55
  * ⚠️ **额度耗尽实时预警**:当 5 小时滑动窗口达到 80%(警告黄色)和 95%(即将耗尽红色)时展示醒目预警横幅与重置倒计时。
56
56
  * 📈 **会话统计与指标看板**:实时追踪请求调用次数、预估 Token(Prompt / Completion)、最近延迟及最后调用时间。
@@ -115,7 +115,7 @@ graph LR
115
115
  * `smokeChat`:毫秒级网络探活与非流式延迟测试,并记录会话指标。
116
116
  * `buildPiAiProvider`:构建 DSH `llm-pi-ai` 兼容的服务商定义 (`api: 'openai-completions'`)。
117
117
  * **`lib/index.js`**:Cordis 插件主生命周期服务,注册后端 REST API 路由(含 `/dsh-clinebot/models/sync`)、额度预警计算与 `/cline` 聊天斜杠指令。
118
- * **`lib/client.js`**:前端设置面板(`settings.section` 序号 28),内含预警横幅、会话指标卡片、一键模型同步按钮及插件折叠卡片。
118
+ * **`lib/client.js`**:插件行配置页(`plugins.row.config`),含预警横幅、会话指标、一键模型同步,以及后备插件卡片(`settings.plugin.item`)。没有 `settings.section` 侧边栏项。
119
119
 
120
120
  ---
121
121
 
@@ -3,8 +3,16 @@ import { resolveKeyValue, usageCache, clearUsageCache, clearProbeCache, DEFAULT_
3
3
  /**
4
4
  * Resolve all accounts in pool with their status and keys.
5
5
  */
6
+ function textSetting(value) {
7
+ let current = value
8
+ while (current && typeof current === 'object' && !Array.isArray(current) && typeof current.get === 'function') {
9
+ current = current.get()
10
+ }
11
+ return typeof current === 'string' ? current.trim() : ''
12
+ }
13
+
6
14
  export async function resolveAccountPool(ctx, cfg) {
7
- const apiKeyEnv = cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV
15
+ const apiKeyEnv = textSetting(cfg?.apiKeyEnv) || DEFAULT_API_KEY_ENV
8
16
  const defaultSlot = {
9
17
  id: 'default',
10
18
  label: 'Default',
@@ -12,16 +20,16 @@ export async function resolveAccountPool(ctx, cfg) {
12
20
  }
13
21
  const accounts = Array.isArray(cfg?.accounts) ? cfg.accounts : []
14
22
  const allSlots = [defaultSlot, ...accounts]
15
- const activeAccount = String(cfg?.activeAccount || '')
23
+ const activeAccount = textSetting(cfg?.activeAccount)
16
24
  const resolved = []
17
25
 
18
26
  for (let i = 0; i < allSlots.length; i++) {
19
27
  const slot = allSlots[i]
20
- const envName = slot.apiKeyEnv || (i === 0 ? apiKeyEnv : `CLINEBOT_API_KEY_${i + 1}`)
28
+ const envName = textSetting(slot.apiKeyEnv) || (i === 0 ? apiKeyEnv : `CLINEBOT_API_KEY_${i + 1}`)
21
29
  const keyInfo = await resolveKeyValue(ctx, envName)
22
30
  resolved.push({
23
31
  id: slot.id || (i === 0 ? 'default' : `account-${i + 1}`),
24
- label: slot.label || (i === 0 ? 'Default' : `Account ${i + 1}`),
32
+ label: textSetting(slot.label) || (i === 0 ? 'Default' : `Account ${i + 1}`),
25
33
  apiKeyEnv: envName,
26
34
  present: Boolean(keyInfo.value),
27
35
  source: keyInfo.source,
package/lib/client.js CHANGED
@@ -14,7 +14,11 @@ const NS = 'dsh-clinebot'
14
14
  const ROW_ID = 'dsh-clinebot'
15
15
  const ROW_CONFIG_KEY = '@goodandready/dsh-clinebot#' + ROW_ID
16
16
  const ROUTE_PREFIX = '/dsh-clinebot'
17
- const SNAPSHOT_LOADING = { status: 'loading', view: null }
17
+ const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', view: null })
18
+ // Stable (frozen) fallback reference: useSyncExternalStore compares snapshots by
19
+ // identity (Object.is), so returning a fresh object literal causes an infinite render loop
20
+ // (React error #185 "Maximum update depth exceeded"). Keep this module-level.
21
+ const SNAPSHOT_READY = Object.freeze({ status: 'ready', view: null })
18
22
 
19
23
  // --- src/client/locales.js ---
20
24
  const en = {
@@ -534,12 +538,14 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
534
538
  'tbody',
535
539
  null,
536
540
  status.accounts.map((acc) => {
537
- const isActive = status.activeAccount === acc.apiKeyEnv || (!status.activeAccount && acc.id === 'default')
541
+ const envName = typeof acc.apiKeyEnv === 'string' ? acc.apiKeyEnv : ''
542
+ const label = typeof acc.label === 'string' ? acc.label : ''
543
+ const isActive = status.activeAccount === envName || (!status.activeAccount && acc.id === 'default')
538
544
  return React.createElement(
539
545
  'tr',
540
546
  { key: acc.id },
541
- React.createElement('td', null, React.createElement('strong', null, acc.label)),
542
- React.createElement('td', null, React.createElement('code', null, acc.apiKeyEnv)),
547
+ React.createElement('td', null, React.createElement('strong', null, label)),
548
+ React.createElement('td', null, React.createElement('code', null, envName)),
543
549
  React.createElement(
544
550
  'td',
545
551
  null,
@@ -561,7 +567,7 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
561
567
  className: 'cb-btn',
562
568
  style: { padding: '4px 8px', fontSize: '11px' },
563
569
  disabled: !!busy,
564
- onClick: () => handlePinAccount(acc.apiKeyEnv),
570
+ onClick: () => handlePinAccount(envName),
565
571
  },
566
572
  t('accounts.pin_btn')
567
573
  )
@@ -834,36 +840,29 @@ Response: ${smokeResult.preview || '(empty)'}`
834
840
  }
835
841
 
836
842
  // --- src/client/settings-page.js ---
843
+ function readConfigForms(ctx) {
844
+ if (!ctx || typeof ctx.get !== 'function') return undefined
845
+ try {
846
+ return ctx.get('configForms') || undefined
847
+ } catch (err) {
848
+ console.warn('[dsh-clinebot] configForms is not available:', err)
849
+ return undefined
850
+ }
851
+ }
852
+
837
853
  function SettingsPage(props) {
838
854
  const ctx = props?.ctx
839
855
  const t = props?.t || makeT(en, en)
840
856
 
841
857
  const scope = React.useMemo(() => {
842
- let s = null
843
- try {
844
- if (typeof ctx?.get === 'function') {
845
- s = ctx.get('lanSettings') || ctx.get('settingsScope')
846
- }
847
- } catch (_) {
848
- s = null
849
- }
850
- if (!s) {
851
- try {
852
- if (ctx && typeof ctx === 'object') {
853
- s = ctx.lanSettings || ctx.settingsScope || null
854
- }
855
- } catch (_) {
856
- s = null
857
- }
858
- }
859
- if (!s) return undefined
858
+ const svc = readConfigForms(ctx)
859
+ if (!svc || typeof svc.get !== 'function') return undefined
860
860
  try {
861
- if (typeof s.bind === 'function') return s.bind({ namespace: NS })
862
- if (typeof s.get === 'function') return s.get(NS)
863
- } catch (_) {
861
+ return svc.get(NS)
862
+ } catch (err) {
863
+ console.warn('[dsh-clinebot] configForms.get failed:', err)
864
864
  return undefined
865
865
  }
866
- return undefined
867
866
  }, [ctx])
868
867
 
869
868
  const subscribe = React.useMemo(() => {
@@ -878,18 +877,19 @@ function SettingsPage(props) {
878
877
  }, [scope])
879
878
 
880
879
  const getSnapshot = React.useCallback(() => {
881
- if (!scope?.getSnapshot) return { status: 'ready', view: null }
880
+ if (!scope?.getSnapshot) return SNAPSHOT_READY
882
881
  try {
883
- return scope.getSnapshot() || { status: 'ready', view: null }
884
- } catch (_) {
885
- return { status: 'ready', view: null }
882
+ return scope.getSnapshot() || SNAPSHOT_READY
883
+ } catch (err) {
884
+ console.warn('[dsh-clinebot] settings snapshot failed:', err)
885
+ return SNAPSHOT_READY
886
886
  }
887
887
  }, [scope])
888
888
 
889
889
  const snapshot = React.useSyncExternalStore(
890
890
  subscribe,
891
891
  getSnapshot,
892
- React.useCallback(() => SNAPSHOT_LOADING, [])
892
+ React.useCallback(() => SNAPSHOT_READY, [])
893
893
  )
894
894
  const snapshotStatus = snapshot?.status || 'loading'
895
895
 
@@ -1198,15 +1198,14 @@ function SettingsPage(props) {
1198
1198
  debounceSaveDisabledModels(nextDisabled)
1199
1199
  }
1200
1200
 
1201
- if (snapshotStatus === 'unavailable') {
1202
- return React.createElement(
1203
- 'div',
1204
- { className: 'cb-page' },
1205
- React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
1206
- )
1207
- }
1208
-
1209
- if (!status || !draft || snapshotStatus === 'loading') {
1201
+ if (!status || !draft) {
1202
+ if (snapshotStatus === 'unavailable') {
1203
+ return React.createElement(
1204
+ 'div',
1205
+ { className: 'cb-page' },
1206
+ React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
1207
+ )
1208
+ }
1210
1209
  if (err) {
1211
1210
  return React.createElement(
1212
1211
  'div',
@@ -1386,49 +1385,14 @@ function PluginCard(props) {
1386
1385
  }
1387
1386
 
1388
1387
  // --- src/client/entry.js ---
1389
- function refreshMirrorUntilVisible(ctx) {
1390
- const getSettingsService = () => {
1391
- try {
1392
- if (typeof ctx?.get === 'function') {
1393
- const s = ctx.get('lanSettings') || ctx.get('settingsScope')
1394
- if (s) return s
1395
- }
1396
- } catch (_) {}
1397
- try {
1398
- if (ctx && typeof ctx === 'object') {
1399
- return ctx.lanSettings || ctx.settingsScope || null
1400
- }
1401
- } catch (_) {}
1402
- return null
1403
- }
1404
-
1405
- const s = getSettingsService()
1406
- if (!s) return
1407
-
1408
- const visible = () => {
1409
- try {
1410
- const current = getSettingsService()
1411
- const view = current?.describe?.()?.getSnapshot?.()?.view
1412
- return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1413
- } catch (_) {
1414
- return false
1415
- }
1388
+ function ensureSettingsForm(ctx) {
1389
+ const svc = readConfigForms(ctx)
1390
+ if (!svc || typeof svc.get !== 'function') return
1391
+ try {
1392
+ svc.get(NS)
1393
+ } catch (err) {
1394
+ console.warn('[dsh-clinebot] configForms.get failed:', err)
1416
1395
  }
1417
- if (visible()) return
1418
-
1419
- if (typeof setInterval !== 'function') return
1420
-
1421
- let tries = 0
1422
- const timer = setInterval(() => {
1423
- if (visible() || tries >= 15) { clearInterval(timer); return }
1424
- tries += 1
1425
- try {
1426
- const current = getSettingsService()
1427
- current?.describe?.()?.load?.()
1428
- } catch (e) {
1429
- console.debug?.('[dsh-clinebot] Polling settings mirror:', e)
1430
- }
1431
- }, 1000)
1432
1396
  }
1433
1397
 
1434
1398
  function apply(ctx) {
@@ -1453,8 +1417,8 @@ function apply(ctx) {
1453
1417
 
1454
1418
  if (typeof ctx.effect === 'function') {
1455
1419
  ctx.effect(
1456
- () => refreshMirrorUntilVisible(ctx),
1457
- 'dsh-clinebot: re-read the settings mirror until our namespace appears',
1420
+ () => { ensureSettingsForm(ctx) },
1421
+ 'dsh-clinebot: open the settings form for this namespace',
1458
1422
  )
1459
1423
  }
1460
1424
 
@@ -1532,7 +1496,7 @@ function apply(ctx) {
1532
1496
  )
1533
1497
  }
1534
1498
 
1535
- module.exports = { apply, inject: ['slots', 'locale'] }
1499
+ module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
1536
1500
 
1537
1501
  return module.exports
1538
1502
  },
package/lib/config.js CHANGED
@@ -13,22 +13,32 @@ import {
13
13
  getActiveModelIds,
14
14
  } from './models.js'
15
15
 
16
+
17
+ // DSH settings.describe only publishes fields marked volatile. Without them the
18
+ // namespace is omitted and the configuration page stays unavailable.
19
+ if (typeof z.prototype?.volatile !== 'function') {
20
+ z.prototype.volatile = function volatile() {
21
+ if (this.meta && this.meta.volatile) throw new TypeError('volatile schema is already wrapped')
22
+ return typeof this.extra === 'function' ? this.extra('volatile', true) : this
23
+ }
24
+ }
25
+
16
26
  export const NS = 'dsh-clinebot'
17
27
  export const LLM_PI_AI_NS = 'llm-pi-ai'
18
28
 
19
29
  export const Config = z.object({
20
30
  enabled: z.boolean().default(true)
21
- .description('When true, ClineBot is registered as a model provider in DSH.'),
31
+ .description('When true, ClineBot is registered as a model provider in DSH.').volatile(),
22
32
  baseUrl: z.string().default(DEFAULT_BASE_URL)
23
- .description('Base API URL (default: https://api.cline.bot/api/v1).'),
33
+ .description('Base API URL (default: https://api.cline.bot/api/v1).').volatile(),
24
34
  apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV)
25
- .description('Credential / env name containing the ClinePass API key (never store key directly here).'),
35
+ .description('Credential / env name containing the ClinePass API key (never store key directly here).').volatile(),
26
36
  defaultModel: z.string().default(DEFAULT_MODEL_ID)
27
- .description('Default model ID for chat and smoke tests.'),
37
+ .description('Default model ID for chat and smoke tests.').volatile(),
28
38
  disabledModels: z.array(z.string()).default([])
29
- .description('List of model IDs explicitly disabled by the user (new models are enabled automatically).'),
39
+ .description('List of model IDs explicitly disabled by the user (new models are enabled automatically).').volatile(),
30
40
  enabledModels: z.array(z.string()).default([])
31
- .description('Deprecated: preserved for backwards compatibility with earlier versions.'),
41
+ .description('Deprecated: preserved for backwards compatibility with earlier versions.').volatile(),
32
42
  dynamicModels: z.array(z.object({
33
43
  id: z.string(),
34
44
  name: z.string(),
@@ -40,23 +50,42 @@ export const Config = z.object({
40
50
  isCustom: z.boolean().default(false),
41
51
  reasoningEfforts: z.any().default(undefined),
42
52
  })).default([])
43
- .description('Models automatically discovered from the official ClinePass subscription plan.'),
53
+ .description('Models automatically discovered from the official ClinePass subscription plan.').volatile(),
44
54
  timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
45
- .description('HTTP probe timeout in milliseconds.'),
55
+ .description('HTTP probe timeout in milliseconds.').volatile(),
46
56
  smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
47
- .description('Timeout for smoke chat completions in milliseconds.'),
57
+ .description('Timeout for smoke chat completions in milliseconds.').volatile(),
48
58
  modelsCachePath: z.string().default('~/.dsh/clinebot-models-cache.json')
49
- .description('Local on-disk cache path for models snapshot.'),
59
+ .description('Local on-disk cache path for models snapshot.').volatile(),
50
60
  accounts: z.array(z.object({
51
61
  label: z.string().default(''),
52
62
  apiKeyEnv: z.string(),
53
63
  })).default([])
54
- .description('Additional accounts for multi-account failover and rate limit rotation.'),
64
+ .description('Additional accounts for multi-account failover and rate limit rotation.').volatile(),
55
65
  activeAccount: z.string().default('')
56
- .description('Manually pinned active account envName or empty for auto/default.'),
66
+ .description('Manually pinned active account envName or empty for auto/default.').volatile(),
57
67
  })
58
68
 
69
+
70
+ function isVolatileRef(value) {
71
+ return !!value && typeof value === 'object' && !Array.isArray(value) && typeof value.get === 'function'
72
+ }
73
+
74
+ // DSH stores each volatile field as { get }. structuredClone cannot copy that
75
+ // getter, so callers need a plain snapshot before validation.
76
+ export function plainConfig(cfg) {
77
+ if (isVolatileRef(cfg)) return plainConfig(cfg.get())
78
+ if (!cfg || typeof cfg !== 'object') return cfg
79
+ const out = {}
80
+ for (const key of Object.keys(cfg)) {
81
+ const value = cfg[key]
82
+ out[key] = isVolatileRef(value) ? value.get() : value
83
+ }
84
+ return out
85
+ }
86
+
59
87
  export function publicConfig(cfg) {
88
+ cfg = plainConfig(cfg)
60
89
  const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
61
90
  const allDefaultIds = getDefaultModelIds(dynamic)
62
91
 
package/lib/http.js CHANGED
@@ -92,3 +92,34 @@ export function isTrustedSettingsRequest(request) {
92
92
 
93
93
  return false
94
94
  }
95
+
96
+ export function publicUsage(usage) {
97
+ if (!usage || typeof usage !== 'object') return null
98
+ const windowView = (window) => {
99
+ if (!window || typeof window !== 'object') return null
100
+ return {
101
+ percentUsed: window.percentUsed,
102
+ remainingPercent: window.remainingPercent,
103
+ resetsAt: window.resetsAt || null,
104
+ }
105
+ }
106
+ if (usage.ok === false) {
107
+ return {
108
+ ok: false,
109
+ error: typeof usage.error === 'string' ? usage.error : 'usage unavailable',
110
+ checkedAt: usage.checkedAt,
111
+ }
112
+ }
113
+ return {
114
+ ok: true,
115
+ plan: usage.plan || null,
116
+ user: { email: usage.user?.email || null },
117
+ windows: {
118
+ fiveHour: windowView(usage.windows?.fiveHour),
119
+ weekly: windowView(usage.windows?.weekly),
120
+ monthly: windowView(usage.windows?.monthly),
121
+ },
122
+ checkedAt: usage.checkedAt,
123
+ }
124
+ }
125
+
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Config, publicConfig, NS, LLM_PI_AI_NS } from './config.js'
1
+ import { Config, publicConfig, plainConfig, NS, LLM_PI_AI_NS } from './config.js'
2
2
  import { registerPluginUpdater } from './updater.js'
3
3
  import {
4
4
  sessionStats,
@@ -30,7 +30,7 @@ export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSess
30
30
 
31
31
  export function apply(ctx, config) {
32
32
  let getConfig = () => config
33
- const live = () => (getConfig() ? Config(structuredClone(getConfig())) : config)
33
+ const live = () => (getConfig() ? Config(structuredClone(plainConfig(getConfig()))) : config)
34
34
  let settingsApi
35
35
 
36
36
  const syncProviderState = async (cfg) => {
@@ -1,6 +1,7 @@
1
1
  import os from 'node:os'
2
2
  import path from 'node:path'
3
3
  import { publicConfig, Config, LLM_PI_AI_NS } from './config.js'
4
+ import { publicUsage } from './http.js'
4
5
  import {
5
6
  PROVIDER_ID,
6
7
  PROVIDER_DISPLAY_NAME,
@@ -109,7 +110,7 @@ export async function buildStatus(ctx, cfg) {
109
110
  })),
110
111
  activeAccount: activeAcc.apiKeyEnv,
111
112
  health,
112
- usage,
113
+ usage: publicUsage(usage),
113
114
  quotaWarning,
114
115
  sessionStats: { ...sessionStats },
115
116
  isRegistered,
@@ -38,6 +38,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
38
38
  path: '/dsh-clinebot/auth/status',
39
39
  handler: async (req, res) => {
40
40
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
41
+ if (!assertTrustedSettingsRequest(req, res)) return
41
42
  writeJson(res, 200, {
42
43
  ok: true,
43
44
  status: authSession?.state || 'idle',
@@ -4,6 +4,7 @@ import { publicConfig, Config } from '../config.js'
4
4
  import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
5
5
  import { getAllModels, saveModelsDiskCache } from '../models.js'
6
6
  import { fetchUsageLimits } from '../cline-client.js'
7
+ import { publicUsage } from '../http.js'
7
8
 
8
9
  export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
9
10
  // POST /dsh-clinebot/models/sync — dynamically load models from plan
@@ -103,6 +104,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
103
104
  path: '/dsh-clinebot/usage',
104
105
  handler: async (req, res) => {
105
106
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
107
+ if (!assertTrustedSettingsRequest(req, res)) return
106
108
  try {
107
109
  const pub = publicConfig(live())
108
110
  const activeKey = await resolveActiveAccountKey(ctx, live())
@@ -113,7 +115,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
113
115
  timeoutMs: pub.timeoutMs,
114
116
  bypassCache: true,
115
117
  })
116
- writeJson(res, usageData.ok ? 200 : 502, usageData)
118
+ writeJson(res, usageData.ok ? 200 : 502, publicUsage(usageData))
117
119
  } catch (err) {
118
120
  writeJson(res, 500, { ok: false, error: String(err?.message || err) })
119
121
  }
@@ -11,6 +11,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
11
11
  path: '/dsh-clinebot/status',
12
12
  handler: async (req, res) => {
13
13
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
14
+ if (!assertTrustedSettingsRequest(req, res)) return
14
15
  try {
15
16
  const st = await buildStatus(ctx, live())
16
17
  writeJson(res, 200, st)
@@ -26,6 +27,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
26
27
  path: '/dsh-clinebot/config',
27
28
  handler: async (req, res) => {
28
29
  if (req.method === 'GET') {
30
+ if (!assertTrustedSettingsRequest(req, res)) return
29
31
  return writeJson(res, 200, { ok: true, config: publicConfig(live()) })
30
32
  }
31
33
  if (req.method !== 'PUT') {
@@ -39,7 +39,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
39
39
  const disabledSet = new Set(pub.disabledModels || [])
40
40
  const lines = [
41
41
  '### 🎯 ClinePass Models Catalog',
42
- `* **Всего моделей**: ${allModels.length} (${allModels.filter((m) => !disabledSet.has(m.id)).length} активно)`,
42
+ `* **Total models**: ${allModels.length} (${allModels.filter((m) => !disabledSet.has(m.id)).length} active)`,
43
43
  '',
44
44
  ]
45
45
  for (const m of allModels) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
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",
@@ -49,7 +49,11 @@
49
49
  },
50
50
  "client": {
51
51
  "platform": "web",
52
- "inject": []
52
+ "inject": [
53
+ "@deepseek-ai/dsh-client-ui-slots",
54
+ "@deepseek-ai/dsh-client-locale",
55
+ "@deepseek-ai/dsh-client-ui-settings"
56
+ ]
53
57
  }
54
58
  },
55
59
  "peerDependencies": {