@goodandready/dsh-clinebot 0.3.24 → 0.3.25

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,14 @@ 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.25] - 2026-09-24
9
+
10
+ ### Fixed
11
+
12
+ - **SettingsPage "Settings Unavailable" Banner Removal (#96 / GitHub #7)**: Removed the blocking `snapshotStatus === 'unavailable'` render gate in `SettingsPage`. When DSH serves no native settings form or when the page is accessed over non-loopback connections (`persistence === 'memory'`), the page no longer displays the dead-end warning banner and instead smoothly renders its fully functional standalone REST UI from `/dsh-clinebot/status` and `/dsh-clinebot/config`.
13
+ - **Modern DSH `SettingsForms` Adapter**: In `lib/index.js`, implemented a robust adapter for modern DSH `SettingsForms` (`svc.replace` / `svc.update` / `svc.describe`). DSH 0.1.7 removed `sctx.settings.register`, which previously caused `PUT /dsh-clinebot/config` to fail with HTTP 503 `settings not ready` and prevented key changes, account switching, and model toggles from persisting to DSH.
14
+ - **Volatile Field Projection (`volatileConfig`)**: Marked only user-editable settings as `.volatile()`, leaving internal derived fields (`dynamicModels` and deprecated `enabledModels`) non-volatile. Added `volatileConfig` helper to strip non-volatile fields before passing payloads to `SettingsForms.replace`, preventing DSH from rejecting configuration writes with `Config field is not volatile`.
15
+
8
16
  ## [0.3.24] - 2026-09-24
9
17
 
10
18
  ### Fixed
package/lib/client.js CHANGED
@@ -1199,13 +1199,6 @@ function SettingsPage(props) {
1199
1199
  }
1200
1200
 
1201
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
- }
1209
1202
  if (err) {
1210
1203
  return React.createElement(
1211
1204
  'div',
package/lib/config.js CHANGED
@@ -38,7 +38,7 @@ export const Config = z.object({
38
38
  disabledModels: z.array(z.string()).default([])
39
39
  .description('List of model IDs explicitly disabled by the user (new models are enabled automatically).').volatile(),
40
40
  enabledModels: z.array(z.string()).default([])
41
- .description('Deprecated: preserved for backwards compatibility with earlier versions.').volatile(),
41
+ .description('Deprecated: preserved for backwards compatibility with earlier versions.'),
42
42
  dynamicModels: z.array(z.object({
43
43
  id: z.string(),
44
44
  name: z.string(),
@@ -50,7 +50,7 @@ export const Config = z.object({
50
50
  isCustom: z.boolean().default(false),
51
51
  reasoningEfforts: z.any().default(undefined),
52
52
  })).default([])
53
- .description('Models automatically discovered from the official ClinePass subscription plan.').volatile(),
53
+ .description('Models automatically discovered from the official ClinePass subscription plan.'),
54
54
  timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
55
55
  .description('HTTP probe timeout in milliseconds.').volatile(),
56
56
  smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
@@ -84,6 +84,18 @@ export function plainConfig(cfg) {
84
84
  return out
85
85
  }
86
86
 
87
+ export function volatileConfig(cfg) {
88
+ const plain = plainConfig(cfg)
89
+ if (!plain || typeof plain !== 'object') return plain
90
+ const out = {}
91
+ for (const [key, field] of Object.entries(Config.dict || {})) {
92
+ if (field?.meta?.volatile && Object.hasOwn(plain, key)) {
93
+ out[key] = plain[key]
94
+ }
95
+ }
96
+ return out
97
+ }
98
+
87
99
  export function publicConfig(cfg) {
88
100
  cfg = plainConfig(cfg)
89
101
  const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Config, publicConfig, plainConfig, NS, LLM_PI_AI_NS } from './config.js'
1
+ import { Config, publicConfig, plainConfig, volatileConfig, NS, LLM_PI_AI_NS } from './config.js'
2
2
  import { registerPluginUpdater } from './updater.js'
3
3
  import {
4
4
  sessionStats,
@@ -29,7 +29,8 @@ export const inject = ['settings', 'webServer', 'credentials']
29
29
  export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
30
30
 
31
31
  export function apply(ctx, config) {
32
- let getConfig = () => config
32
+ let currentConfig = config
33
+ let getConfig = () => currentConfig
33
34
  const live = () => (getConfig() ? Config(structuredClone(plainConfig(getConfig()))) : config)
34
35
  let settingsApi
35
36
 
@@ -55,14 +56,61 @@ export function apply(ctx, config) {
55
56
  autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
56
57
  }
57
58
 
59
+ const createSettingsAdapter = (svc) => {
60
+ if (!svc) return undefined
61
+ if (typeof svc.replace === 'function' || typeof svc.update === 'function' || typeof svc.write === 'function') {
62
+ const getRevision = () => {
63
+ try {
64
+ return svc.describe?.().find((row) => row.ns === NS)?.revision
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+ return {
70
+ get: () => live(),
71
+ replace: async (next) => {
72
+ const parsed = Config(structuredClone(plainConfig(next)))
73
+ currentConfig = parsed
74
+ const payload = volatileConfig(parsed)
75
+ if (typeof svc.replace === 'function') {
76
+ await svc.replace(NS, payload, getRevision())
77
+ } else if (typeof svc.update === 'function') {
78
+ await svc.update(NS, payload, getRevision())
79
+ }
80
+ return parsed
81
+ },
82
+ update: async (patch) => {
83
+ const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
84
+ currentConfig = merged
85
+ const payload = volatileConfig(merged)
86
+ if (typeof svc.update === 'function') {
87
+ await svc.update(NS, payload, getRevision())
88
+ } else if (typeof svc.replace === 'function') {
89
+ await svc.replace(NS, payload, getRevision())
90
+ }
91
+ return merged
92
+ },
93
+ watch: (cb) => {
94
+ if (typeof svc.watch === 'function') return svc.watch(cb)
95
+ return () => {}
96
+ },
97
+ }
98
+ }
99
+ return undefined
100
+ }
101
+
58
102
  if (typeof ctx.inject === 'function') {
59
103
  ctx.inject(['settings'], (sctx) => {
60
- const scope = sctx.settings.register(NS, Config, { base: config })
61
- settingsApi = scope
62
- getConfig = () => (scope?.get?.() ?? config) ?? config
63
- sctx.effect(() => scope.watch((next) => {
64
- syncProviderState(live())
65
- }), 'dsh-clinebot: settings')
104
+ if (typeof sctx.settings?.register === 'function') {
105
+ const scope = sctx.settings.register(NS, Config, { base: config })
106
+ settingsApi = scope
107
+ getConfig = () => (scope?.get?.() ?? config) ?? config
108
+ sctx.effect(() => scope.watch((next) => {
109
+ syncProviderState(live())
110
+ }), 'dsh-clinebot: settings')
111
+ } else {
112
+ settingsApi = createSettingsAdapter(sctx.settings)
113
+ }
66
114
  sctx.effect(() => () => {
67
115
  getConfig = () => config
68
116
  settingsApi = undefined
@@ -79,6 +127,8 @@ export function apply(ctx, config) {
79
127
  syncProviderState(live())
80
128
  }), 'dsh-clinebot: settings')
81
129
  }
130
+ } else {
131
+ settingsApi = createSettingsAdapter(settingsService)
82
132
  }
83
133
  }
84
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
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",