@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 +11 -1
- package/lib/account-pool.js +12 -4
- package/lib/client.js +14 -13
- package/lib/config.js +41 -12
- package/lib/index.js +2 -2
- package/package.json +1 -1
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
|
|
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
|
|
package/lib/account-pool.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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,
|
|
546
|
-
React.createElement('td', null, React.createElement('code', null,
|
|
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(
|
|
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 (
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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.
|
|
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",
|