@goodandready/dsh-clinebot 0.3.23 → 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,10 +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.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
+
8
14
  ## [0.3.23] - 2026-09-24
9
15
 
10
16
  ### Fixed
11
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.
12
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.
13
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.
14
24
  - `/cline models` prints the model total in English.
@@ -194,7 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
194
204
  ## [0.3.4] - 2026-09-08
195
205
 
196
206
  ### Fixed
197
- - **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.
198
208
 
199
209
  ## [0.3.3] - 2026-09-08
200
210
 
@@ -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
@@ -538,12 +538,14 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
538
538
  'tbody',
539
539
  null,
540
540
  status.accounts.map((acc) => {
541
- 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')
542
544
  return React.createElement(
543
545
  'tr',
544
546
  { key: acc.id },
545
- React.createElement('td', null, React.createElement('strong', null, acc.label)),
546
- 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)),
547
549
  React.createElement(
548
550
  'td',
549
551
  null,
@@ -565,7 +567,7 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
565
567
  className: 'cb-btn',
566
568
  style: { padding: '4px 8px', fontSize: '11px' },
567
569
  disabled: !!busy,
568
- onClick: () => handlePinAccount(acc.apiKeyEnv),
570
+ onClick: () => handlePinAccount(envName),
569
571
  },
570
572
  t('accounts.pin_btn')
571
573
  )
@@ -1196,15 +1198,14 @@ function SettingsPage(props) {
1196
1198
  debounceSaveDisabledModels(nextDisabled)
1197
1199
  }
1198
1200
 
1199
- if (snapshotStatus === 'unavailable') {
1200
- return React.createElement(
1201
- 'div',
1202
- { className: 'cb-page' },
1203
- React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
1204
- )
1205
- }
1206
-
1207
- 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
+ }
1208
1209
  if (err) {
1209
1210
  return React.createElement(
1210
1211
  'div',
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/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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.23",
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",