@goodandready/dsh-clinebot 0.3.21 → 0.3.23

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,20 @@ 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.23] - 2026-09-24
9
+
10
+ ### Fixed
11
+ - **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.
12
+ - 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.
13
+ - `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.
14
+ - `/cline models` prints the model total in English.
15
+
16
+ ## [0.3.22] - 2026-09-24
17
+
18
+ ### Fixed
19
+ - **SettingsPage Crash on Cordis Strict Context (#67 / GitHub #4)**: Fixed unhandled `TypeError: cannot get property "settingsScope" without inject` thrown when accessing `ctx?.settingsScope` under DSH 0.17+ / Cordis v3+ strict proxy contexts. Replaced bare property access with defensive `ctx.get('lanSettings') || ctx.get('settingsScope')` inside `try/catch` across both `settings-page.js` and `entry.js`.
20
+ - **Standalone Unblocked UI Rendering**: When `settingsScope` / `lanSettings` service is absent or not injected in the client fiber, `SettingsPage` snapshot now defaults to `{ status: 'ready', view: null }` instead of permanently hanging on `status: 'loading'`. The settings page is entirely functional via its own HTTP REST endpoints (`/dsh-clinebot/status`, `/save-key`, `/models/toggle`, etc.).
21
+
8
22
  ## [0.3.21] - 2026-09-23
9
23
 
10
24
  ### Fixed
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
 
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 = {
@@ -834,16 +838,27 @@ Response: ${smokeResult.preview || '(empty)'}`
834
838
  }
835
839
 
836
840
  // --- src/client/settings-page.js ---
841
+ function readConfigForms(ctx) {
842
+ if (!ctx || typeof ctx.get !== 'function') return undefined
843
+ try {
844
+ return ctx.get('configForms') || undefined
845
+ } catch (err) {
846
+ console.warn('[dsh-clinebot] configForms is not available:', err)
847
+ return undefined
848
+ }
849
+ }
850
+
837
851
  function SettingsPage(props) {
838
852
  const ctx = props?.ctx
839
853
  const t = props?.t || makeT(en, en)
840
854
 
841
855
  const scope = React.useMemo(() => {
842
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
843
- if (!s?.bind) return undefined
856
+ const svc = readConfigForms(ctx)
857
+ if (!svc || typeof svc.get !== 'function') return undefined
844
858
  try {
845
- return s.bind({ namespace: NS })
846
- } catch (_) {
859
+ return svc.get(NS)
860
+ } catch (err) {
861
+ console.warn('[dsh-clinebot] configForms.get failed:', err)
847
862
  return undefined
848
863
  }
849
864
  }, [ctx])
@@ -860,18 +875,19 @@ function SettingsPage(props) {
860
875
  }, [scope])
861
876
 
862
877
  const getSnapshot = React.useCallback(() => {
863
- if (!scope?.getSnapshot) return SNAPSHOT_LOADING
878
+ if (!scope?.getSnapshot) return SNAPSHOT_READY
864
879
  try {
865
- return scope.getSnapshot() || SNAPSHOT_LOADING
866
- } catch (_) {
867
- return SNAPSHOT_LOADING
880
+ return scope.getSnapshot() || SNAPSHOT_READY
881
+ } catch (err) {
882
+ console.warn('[dsh-clinebot] settings snapshot failed:', err)
883
+ return SNAPSHOT_READY
868
884
  }
869
885
  }, [scope])
870
886
 
871
887
  const snapshot = React.useSyncExternalStore(
872
888
  subscribe,
873
889
  getSnapshot,
874
- React.useCallback(() => SNAPSHOT_LOADING, [])
890
+ React.useCallback(() => SNAPSHOT_READY, [])
875
891
  )
876
892
  const snapshotStatus = snapshot?.status || 'loading'
877
893
 
@@ -1368,29 +1384,14 @@ function PluginCard(props) {
1368
1384
  }
1369
1385
 
1370
1386
  // --- src/client/entry.js ---
1371
- function refreshMirrorUntilVisible(ctx) {
1372
- const visible = () => {
1373
- try {
1374
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1375
- const view = s?.describe?.()?.getSnapshot?.()?.view
1376
- return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1377
- } catch (_) {
1378
- return false
1379
- }
1387
+ function ensureSettingsForm(ctx) {
1388
+ const svc = readConfigForms(ctx)
1389
+ if (!svc || typeof svc.get !== 'function') return
1390
+ try {
1391
+ svc.get(NS)
1392
+ } catch (err) {
1393
+ console.warn('[dsh-clinebot] configForms.get failed:', err)
1380
1394
  }
1381
- if (visible()) return () => {}
1382
- let tries = 0
1383
- const timer = setInterval(() => {
1384
- if (visible() || tries >= 15) { clearInterval(timer); return }
1385
- tries += 1
1386
- try {
1387
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
1388
- s?.describe?.()?.load?.()
1389
- } catch (e) {
1390
- console.debug?.('[dsh-clinebot] Polling settings mirror:', e)
1391
- }
1392
- }, 1000)
1393
- return () => clearInterval(timer)
1394
1395
  }
1395
1396
 
1396
1397
  function apply(ctx) {
@@ -1415,8 +1416,8 @@ function apply(ctx) {
1415
1416
 
1416
1417
  if (typeof ctx.effect === 'function') {
1417
1418
  ctx.effect(
1418
- () => refreshMirrorUntilVisible(ctx),
1419
- 'dsh-clinebot: re-read the settings mirror until our namespace appears',
1419
+ () => { ensureSettingsForm(ctx) },
1420
+ 'dsh-clinebot: open the settings form for this namespace',
1420
1421
  )
1421
1422
  }
1422
1423
 
@@ -1494,7 +1495,7 @@ function apply(ctx) {
1494
1495
  )
1495
1496
  }
1496
1497
 
1497
- module.exports = { apply, inject: ['slots', 'locale'] }
1498
+ module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
1498
1499
 
1499
1500
  return module.exports
1500
1501
  },
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
+
@@ -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.21",
3
+ "version": "0.3.23",
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": {