@goodandready/dsh-clinebot 0.3.6 → 0.3.7
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 +9 -0
- package/docs/design/DESIGN.md +7 -5
- package/lib/client.js +96 -44
- package/lib/index.js +34 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.7] - 2026-09-10
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Canonical Settings Namespace Registration** (Issue #16): Moved settings declaration in `lib/index.js` to `ctx.inject(['settings'], (sctx) => { sctx.settings.register(NS, Config, { base: config }) })`, guaranteeing synchronous/asynchronous namespace availability and reactive config watching via `live()`.
|
|
12
|
+
- **Top-Level Section Removal**: Removed the unauthorized fallback to `settings.section` in `lib/client.js`, strictly confining the plugin UI to the standard `settings.plugin.item` slot under Settings → Plugins → Plugin Settings.
|
|
13
|
+
- **Safe Service Resolution**: Replaced direct property access `ctx.credentials` with safe proxy lookup `(ctx?.get && ctx.get('credentials')) || ctx?.credentials`.
|
|
14
|
+
- **Client Mirror Invalidation & Scope Sync**: Added `refreshMirrorUntilVisible(ctx)` in `lib/client.js` to trigger settings mirror re-reads until the namespace is visible in the web client, and synchronized multi-account changes directly through `scope.set('activeAccount', accountEnv)`.
|
|
15
|
+
- **Duplicate-Safe Locales**: Wrapped client dictionary registration with duplicate-safe guards (`ctx.locale.register()`) preventing registration collision errors.
|
|
16
|
+
|
|
8
17
|
## [0.3.6] - 2026-09-09
|
|
9
18
|
|
|
10
19
|
### Fixed
|
package/docs/design/DESIGN.md
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
The plugin consists of two runtime boundaries conforming to DSH authoring standards:
|
|
8
8
|
|
|
9
9
|
### 2.1 Host Runtime (`lib/index.js`, `lib/cline-client.js`, `lib/models.js`, `lib/http.js`)
|
|
10
|
-
* **Cordis Service Registration**:
|
|
11
|
-
* **
|
|
10
|
+
* **Cordis Service Registration**: Declares `inject = ['settings', 'webServer', 'credentials']` and registers the settings namespace dynamically via `ctx.inject(['settings'], (sctx) => { sctx.settings.register(NS, Config, { base: config }) })`. This guarantees that the configuration schema, default values, and reactive watchers are declared and available to the host and client settingsScope without timing issues.
|
|
11
|
+
* **Safe Service Resolution**: Service lookups utilize defensive proxy resolution `(ctx?.get && ctx.get('credentials')) || ctx?.credentials` to prevent `undefined` properties on Cordis proxies.
|
|
12
|
+
* **Credential Isolation**: The plugin NEVER stores plain API keys in its configuration. The setting `apiKeyEnv` holds the credential identifier (default: `CLINEBOT_API_KEY`), resolved via `ctx.get('credentials').resolve()` or `process.env`.
|
|
12
13
|
* **State Synchronization & Auto-Registration**: Mutates the core `llm-pi-ai` settings space (`op: 'set', path: ['providers', 'clinebot']`) declaratively and automatically when enabled or key is saved.
|
|
13
14
|
* **Auto-Discovery & `disabledModels`**: Features automatic background polling of subscription plan models (`GET /api/v1/users/me/plan`). User preferences are tracked via `disabledModels: []`, ensuring newly added plan models appear enabled by default in the DSH chat picker without manual re-synchronization.
|
|
14
15
|
|
|
15
16
|
### 2.2 Client Runtime (`lib/client.js`)
|
|
16
17
|
* Self-registering module via `window.__ModuleLoader__.load({ id: '@goodandready/dsh-clinebot', factory })`.
|
|
17
18
|
* Injects `['slots', 'locale', 'settingsScope']`.
|
|
18
|
-
* Slots into `settings.plugin.item` (
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Slots strictly and exclusively into `settings.plugin.item` (`key: NS`, `locale: NS`). Standalone top-level `settings.section` registration is omitted to maintain clean primary navigation in DSH and prevent side-list pollution.
|
|
20
|
+
* Uses `refreshMirrorUntilVisible(ctx)` to invalidate and re-read the client settings mirror until the namespace is reported ready by the host.
|
|
21
|
+
* Registers localized `en` and `ru` dictionaries with duplicate-safe guards (`ctx.locale.register()`).
|
|
22
|
+
* Reactive binding via `((ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope).bind({ namespace: NS })` with `useSyncExternalStore` guarding against `unavailable` / `loading` snapshot states. Form mutations write directly to `scope.set()`.
|
|
21
23
|
* Uses native design tokens (`--dsw-alias-...`) with full dark/light theme support.
|
|
22
24
|
* Injects isolated style tag tagged with `data-dsh-plugin="dsh-clinebot"`.
|
|
23
25
|
|
package/lib/client.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// & Plugin Card (settings.plugin.item).
|
|
1
|
+
// Plugin Settings Card (settings.plugin.item).
|
|
3
2
|
window.__ModuleLoader__.load({
|
|
4
3
|
id: '@goodandready/dsh-clinebot',
|
|
5
4
|
factory: (require) => {
|
|
@@ -357,9 +356,10 @@ window.__ModuleLoader__.load({
|
|
|
357
356
|
const t = props?.t || makeT(ru, en)
|
|
358
357
|
|
|
359
358
|
const scope = React.useMemo(() => {
|
|
360
|
-
|
|
359
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
360
|
+
if (!s?.bind) return undefined
|
|
361
361
|
try {
|
|
362
|
-
return
|
|
362
|
+
return s.bind({ namespace: NS })
|
|
363
363
|
} catch (_) {
|
|
364
364
|
return undefined
|
|
365
365
|
}
|
|
@@ -377,11 +377,11 @@ window.__ModuleLoader__.load({
|
|
|
377
377
|
}, [scope])
|
|
378
378
|
|
|
379
379
|
const getSnapshot = React.useCallback(() => {
|
|
380
|
-
if (!scope?.getSnapshot) return
|
|
380
|
+
if (!scope?.getSnapshot) return SNAPSHOT_LOADING
|
|
381
381
|
try {
|
|
382
|
-
return scope.getSnapshot() ||
|
|
382
|
+
return scope.getSnapshot() || SNAPSHOT_LOADING
|
|
383
383
|
} catch (_) {
|
|
384
|
-
return
|
|
384
|
+
return SNAPSHOT_LOADING
|
|
385
385
|
}
|
|
386
386
|
}, [scope])
|
|
387
387
|
|
|
@@ -390,7 +390,7 @@ window.__ModuleLoader__.load({
|
|
|
390
390
|
getSnapshot,
|
|
391
391
|
React.useCallback(() => SNAPSHOT_LOADING, [])
|
|
392
392
|
)
|
|
393
|
-
const snapshotStatus = snapshot?.status || '
|
|
393
|
+
const snapshotStatus = snapshot?.status || 'loading'
|
|
394
394
|
|
|
395
395
|
const [status, setStatus] = React.useState(null)
|
|
396
396
|
const [draft, setDraft] = React.useState(null)
|
|
@@ -420,6 +420,12 @@ window.__ModuleLoader__.load({
|
|
|
420
420
|
load().catch((e) => setErr(String(e.message || e)))
|
|
421
421
|
}, [load])
|
|
422
422
|
|
|
423
|
+
React.useEffect(() => {
|
|
424
|
+
if (snapshotStatus === 'ready' && snapshot?.value && typeof snapshot.value === 'object') {
|
|
425
|
+
setDraft((prev) => (prev ? { ...prev, ...snapshot.value } : snapshot.value))
|
|
426
|
+
}
|
|
427
|
+
}, [snapshotStatus, snapshot?.value])
|
|
428
|
+
|
|
423
429
|
// Save Key handler
|
|
424
430
|
async function handleSaveKey() {
|
|
425
431
|
if (!apiKeyInput.trim()) {
|
|
@@ -570,6 +576,9 @@ window.__ModuleLoader__.load({
|
|
|
570
576
|
|
|
571
577
|
// Pin active account
|
|
572
578
|
async function handlePinAccount(accountEnv) {
|
|
579
|
+
if (scope && snapshotStatus === "ready") {
|
|
580
|
+
try { await scope.set("activeAccount", accountEnv) } catch (_) {}
|
|
581
|
+
}
|
|
573
582
|
setBusy(`pin-${accountEnv}`)
|
|
574
583
|
setErr('')
|
|
575
584
|
try {
|
|
@@ -1212,49 +1221,92 @@ window.__ModuleLoader__.load({
|
|
|
1212
1221
|
)
|
|
1213
1222
|
}
|
|
1214
1223
|
|
|
1224
|
+
function refreshMirrorUntilVisible(ctx) {
|
|
1225
|
+
const visible = () => {
|
|
1226
|
+
try {
|
|
1227
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
1228
|
+
const view = s?.describe?.()?.getSnapshot?.()?.view
|
|
1229
|
+
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
1230
|
+
} catch (_) {
|
|
1231
|
+
return false
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (visible()) return () => {}
|
|
1235
|
+
let tries = 0
|
|
1236
|
+
const timer = setInterval(() => {
|
|
1237
|
+
if (visible() || tries >= 15) { clearInterval(timer); return }
|
|
1238
|
+
tries += 1
|
|
1239
|
+
try {
|
|
1240
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
1241
|
+
s?.describe?.()?.load?.()
|
|
1242
|
+
} catch (_) {}
|
|
1243
|
+
}, 1000)
|
|
1244
|
+
return () => clearInterval(timer)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1215
1247
|
function apply(ctx) {
|
|
1248
|
+
const addLocale = (locale, dictionary) => {
|
|
1249
|
+
try {
|
|
1250
|
+
return ctx.locale.register(NS, locale, dictionary)
|
|
1251
|
+
} catch (_) {
|
|
1252
|
+
return () => {}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1216
1255
|
if (ctx.locale && ctx.locale.register) {
|
|
1217
|
-
|
|
1256
|
+
if (typeof ctx.effect === 'function') {
|
|
1257
|
+
ctx.effect(() => {
|
|
1258
|
+
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1259
|
+
return () => { for (const off of undo) off() }
|
|
1260
|
+
}, 'dsh-clinebot: dictionaries')
|
|
1261
|
+
} else {
|
|
1262
|
+
addLocale('en', en)
|
|
1263
|
+
addLocale('ru', ru)
|
|
1264
|
+
}
|
|
1218
1265
|
}
|
|
1219
|
-
const t = (ctx.locale && ctx.locale.bind) ? ctx.locale.bind(NS) : makeT(ru, en)
|
|
1220
1266
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
{
|
|
1227
|
-
name: 'settings.plugin.item',
|
|
1228
|
-
key: NS,
|
|
1229
|
-
locale: NS,
|
|
1230
|
-
inject: () => ({ ctx }),
|
|
1231
|
-
},
|
|
1232
|
-
(props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
1233
|
-
)
|
|
1234
|
-
})
|
|
1235
|
-
if (res === null) placed = false
|
|
1236
|
-
} catch (_) {
|
|
1237
|
-
placed = false
|
|
1267
|
+
if (typeof ctx.effect === 'function') {
|
|
1268
|
+
ctx.effect(
|
|
1269
|
+
() => refreshMirrorUntilVisible(ctx),
|
|
1270
|
+
'dsh-clinebot: re-read the settings mirror until our namespace appears',
|
|
1271
|
+
)
|
|
1238
1272
|
}
|
|
1239
1273
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
{
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1274
|
+
function registerSlotWhenReady(slotName, registerFn) {
|
|
1275
|
+
if (!ctx.slots) return
|
|
1276
|
+
if (typeof ctx.slots.inject === 'function') {
|
|
1277
|
+
try {
|
|
1278
|
+
ctx.slots.inject(slotName, () => {
|
|
1279
|
+
try {
|
|
1280
|
+
return registerFn()
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
console.warn('[dsh-clinebot] Error registering slot ' + slotName + ':', err)
|
|
1283
|
+
}
|
|
1284
|
+
})
|
|
1285
|
+
return
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
console.warn('[dsh-clinebot] Failed to inject slot ' + slotName + ':', err)
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (typeof ctx.slots.register === 'function') {
|
|
1291
|
+
try {
|
|
1292
|
+
registerFn()
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
console.warn('[dsh-clinebot] Failed direct registration for ' + slotName + ':', err)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1257
1297
|
}
|
|
1298
|
+
|
|
1299
|
+
registerSlotWhenReady('settings.plugin.item', () =>
|
|
1300
|
+
ctx.slots.register(
|
|
1301
|
+
{
|
|
1302
|
+
name: 'settings.plugin.item',
|
|
1303
|
+
key: NS,
|
|
1304
|
+
locale: NS,
|
|
1305
|
+
inject: () => ({ ctx }),
|
|
1306
|
+
},
|
|
1307
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
1308
|
+
)
|
|
1309
|
+
)
|
|
1258
1310
|
}
|
|
1259
1311
|
|
|
1260
1312
|
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
package/lib/index.js
CHANGED
|
@@ -122,9 +122,10 @@ function resolvePathWithHome(p) {
|
|
|
122
122
|
|
|
123
123
|
async function resolveKeyValue(ctx, apiKeyEnv) {
|
|
124
124
|
const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
125
|
-
|
|
125
|
+
const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
|
|
126
|
+
if (creds && typeof creds.resolve === 'function') {
|
|
126
127
|
try {
|
|
127
|
-
const hit = await
|
|
128
|
+
const hit = await creds.resolve(credentialRef(refName))
|
|
128
129
|
if (hit?.value) {
|
|
129
130
|
return { envName: refName, value: hit.value, source: 'credentials' }
|
|
130
131
|
}
|
|
@@ -323,7 +324,8 @@ function formatProgressBar(pct, totalWidth = 10) {
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
export function apply(ctx, config) {
|
|
326
|
-
let
|
|
327
|
+
let getConfig = () => config
|
|
328
|
+
const live = () => (getConfig() ? Config(structuredClone(getConfig())) : config)
|
|
327
329
|
let settingsApi
|
|
328
330
|
|
|
329
331
|
// Declarative sync helper: auto-registers or unregisters provider based on config & key availability
|
|
@@ -355,7 +357,7 @@ export function apply(ctx, config) {
|
|
|
355
357
|
if ((!pub.dynamicModels || !pub.dynamicModels.length) && cacheFile) {
|
|
356
358
|
const fromDisk = await loadModelsDiskCache(cacheFile)
|
|
357
359
|
if (Array.isArray(fromDisk) && fromDisk.length && settingsApi?.replace) {
|
|
358
|
-
const next = Config({ ...
|
|
360
|
+
const next = Config({ ...live(), dynamicModels: fromDisk })
|
|
359
361
|
await settingsApi.replace(next)
|
|
360
362
|
await syncProviderState(next)
|
|
361
363
|
}
|
|
@@ -376,7 +378,7 @@ export function apply(ctx, config) {
|
|
|
376
378
|
|
|
377
379
|
if (hasNew && settingsApi?.replace) {
|
|
378
380
|
const next = Config({
|
|
379
|
-
...
|
|
381
|
+
...live(),
|
|
380
382
|
dynamicModels: usageData.dynamicModels,
|
|
381
383
|
})
|
|
382
384
|
await settingsApi.replace(next)
|
|
@@ -391,23 +393,37 @@ export function apply(ctx, config) {
|
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
396
|
+
if (typeof ctx.inject === 'function') {
|
|
397
|
+
ctx.inject(['settings'], (sctx) => {
|
|
398
|
+
const scope = sctx.settings.register(NS, Config, { base: config })
|
|
399
|
+
settingsApi = scope
|
|
400
|
+
getConfig = () => (scope?.get?.() ?? config) ?? config
|
|
401
|
+
sctx.effect(() => scope.watch((next) => {
|
|
402
|
+
syncProviderState(live())
|
|
403
|
+
}), 'dsh-clinebot: settings')
|
|
404
|
+
sctx.effect(() => () => {
|
|
405
|
+
getConfig = () => config
|
|
406
|
+
settingsApi = undefined
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
} else {
|
|
410
|
+
const settingsService = (ctx?.get && ctx.get('settings')) || ctx?.settings
|
|
411
|
+
if (typeof settingsService?.register === 'function') {
|
|
412
|
+
const scope = settingsService.register(NS, Config, { base: config })
|
|
413
|
+
settingsApi = scope
|
|
414
|
+
getConfig = () => (scope?.get?.() ?? config) ?? config
|
|
415
|
+
if (typeof ctx.effect === 'function') {
|
|
416
|
+
ctx.effect(() => scope.watch((next) => {
|
|
417
|
+
syncProviderState(live())
|
|
418
|
+
}), 'dsh-clinebot: settings')
|
|
419
|
+
}
|
|
420
|
+
}
|
|
403
421
|
}
|
|
404
422
|
|
|
405
|
-
const live = () => liveCfg
|
|
406
|
-
|
|
407
423
|
// On startup: ensure provider is synced to llm-pi-ai if key is present
|
|
408
|
-
syncProviderState(
|
|
424
|
+
syncProviderState(live())
|
|
409
425
|
// Background discover plan models on startup
|
|
410
|
-
setTimeout(() => autoDiscoverPlanModels(
|
|
426
|
+
setTimeout(() => autoDiscoverPlanModels(live()), 500)
|
|
411
427
|
|
|
412
428
|
// Web server HTTP route handlers
|
|
413
429
|
if (ctx.webServer?.register) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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",
|