@goodandready/dsh-clinebot 0.4.4 → 0.4.6

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,25 @@ 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.4.6] - 2026-09-25
9
+
10
+ ### Fixed
11
+ - **Obsolete Settings Service Dependency Removal (#119)**: Removed ` settings` from `inject` and eradicated legacy `ctx.inject([settings], ...)` and `sctx.settings.register(...)` calls, adapting to DSH 0.1.7 kernel architecture where the Cordis `settings` service was removed.
12
+ - **Config Reader Export (#119)**: Exported `configReader(config)` returning `() => config` to provide standard DSH configuration introspection.
13
+ - **Defensive SettingsForms Adapter (#119)**: Implemented lightweight `settingsApi` adapter providing `get()`, `replace()`, `update()`, and `watch()` without declaring `settings` in `inject`, accessing `ctx?.get?.(settings)` defensively.
14
+ - **Failover & Settings Regression Tests (#119)**: Added 5 strict DoD regression tests verifying `inject`, absence of `settings.register`, proxy property safety, config reader, and strict proxy boot.
15
+
16
+ ## [0.4.5] - 2026-09-25
17
+
18
+ ### Fixed
19
+ - **Obsolete Model ID Migration & Canonical Normalization (#103)**: Automatically migrate historical model identifiers lacking dots (e.g., `deepseek-v41-flash` -> `deepseek-v4.1-flash`, `qwen38-max` -> `qwen3.8-max`, `glm-53` -> `glm-5.3`, `glm-53-flash` -> `glm-5.3-flash`, `muse-spark-13-contributor` -> `muse-spark-1.3-contributor`) across disk cache, DSH settings, and auto-discovery, eliminating stuck legacy IDs.
20
+ - **Config Schema Test Assertion for Schemastery 3.18.4 (#112)**: Aligned `test/config-schema.test.js` with Schemastery 3.18.4 volatile getter behavior using `plainConfig(validated).enabled`.
21
+ - **UI Color Compliance & Token Hardening (#116)**: Replaced hardcoded `rgba()` fallbacks in `accounts-section.js` and `models-section.js` with canonical DSH `color-mix()` and design tokens.
22
+ - **Decomposition Compliance (#117)**: Extracted credentials helpers into `lib/credential-refs.js`, reducing `lib/cline-client.js` from 590 to 511 lines (well below the 600-line ceiling).
23
+ - **Client Localization Pruning (#115)**: Removed 6 obsolete dead locale keys from `src/client/locales.js` in both `en` and `zh`.
24
+ - **Route Policy & Packaging Contract (#113)**: Documented mandatory «Политика маршрутов» (16 registered endpoints) and «Что публикуется» sections in `docs/design/DESIGN.md`.
25
+ - **Visual Verification Evidence (#114)**: Added dual-theme production visual verification artifact `media/visual-verification.png` and embedded under `## Visual verification` in `README.md`.
26
+
8
27
  ## [0.4.4] - 2026-09-25
9
28
 
10
29
  ### Added
package/README.md CHANGED
@@ -206,6 +206,14 @@ All endpoints are registered under `/dsh-clinebot/*` and protected against untru
206
206
 
207
207
  ---
208
208
 
209
+ ## Visual verification
210
+
211
+ Settings card in DeepSeek Harness, Dark and Light themes side by side:
212
+
213
+ ![dsh-clinebot visual verification](media/visual-verification.png)
214
+
215
+ ---
216
+
209
217
  ## 🧪 Testing
210
218
 
211
219
  Run the automated test suite:
package/lib/client.js CHANGED
@@ -48,7 +48,6 @@ const en = {
48
48
  'key.save': 'Save Key',
49
49
  'key.saving': 'Saving…',
50
50
  'key.login_fast': '🔑 Open API Keys ↗',
51
- 'key.login_waiting': 'Paste key below for instant verification.',
52
51
  'key.verifying': 'Validating key with Cline…',
53
52
  'key.verified': '✓ Valid key for {email} ({plan})',
54
53
  'key.invalid': '✗ Key validation failed: {error}',
@@ -66,10 +65,8 @@ const en = {
66
65
  'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
67
66
  'accounts.delete_btn': 'Delete',
68
67
  'accounts.delete_confirm': 'Are you sure you want to delete account "{label}"?',
69
- 'accounts.delete_secret': 'Delete secret from credentials',
70
68
  'accounts.save_btn': 'Save Account',
71
69
  'accounts.cancel_btn': 'Cancel',
72
- 'accounts.key_label': 'API Key',
73
70
  'accounts.quota_used': '{pct}% used',
74
71
  'accounts.last_failover': 'Last failover: {from} → {to} ({reason})',
75
72
  'quota.title': '📊 ClinePass Quota & Rate Limits',
@@ -81,7 +78,6 @@ const en = {
81
78
  'quota.window_5h': '⏱ 5-Hour Rolling Limit',
82
79
  'quota.window_weekly': '📅 Weekly Window',
83
80
  'quota.used': '{pct}% used',
84
- 'quota.remaining': 'Remaining: {pct}%',
85
81
  'quota.reset_at': 'Reset: {time}',
86
82
  'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
87
83
  'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
@@ -114,7 +110,6 @@ const en = {
114
110
  'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
115
111
  'models.ctx_default': 'Default ({ctx})',
116
112
  'models.ctx_original': 'Original ({ctx} · {provider})',
117
- 'models.ctx_custom': 'Custom…',
118
113
  'models.ctx_edit': 'Edit Context Window',
119
114
  'models.ctx_custom_placeholder': 'Tokens (e.g. 2000000)',
120
115
  'models.ctx_save': 'Save',
@@ -131,7 +126,6 @@ const en = {
131
126
  'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test or force re-sync.',
132
127
  'diag.smoke_btn': 'Run Smoke Test (Ping)',
133
128
  'diag.smoke_testing': 'Testing…',
134
- 'diag.smoke_ok': 'Smoke test passed! Latency: {latency} ms',
135
129
  'diag.resync_btn': 'Force Re-sync to DSH',
136
130
  'diag.resyncing': 'Syncing…',
137
131
  'diag.resynced_msg': 'Provider re-synced to DSH Models ({count} models)',
@@ -170,7 +164,6 @@ const en = {
170
164
  'key.save': '保存密钥',
171
165
  'key.saving': '正在保存…',
172
166
  'key.login_fast': '🔑 打开 API 密钥页 ↗',
173
- 'key.login_waiting': '请在下方粘贴密钥,系统将自动进行有效性校验。',
174
167
  'key.verifying': '正在连接 Cline 验证密钥…',
175
168
  'key.verified': '✓ 密钥有效:{email} ({plan})',
176
169
  'key.invalid': '✗ 密钥验证未通过:{error}',
@@ -188,10 +181,8 @@ const en = {
188
181
  'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
189
182
  'accounts.delete_btn': '删除',
190
183
  'accounts.delete_confirm': '确定要删除账号“{label}”吗?',
191
- 'accounts.delete_secret': '同时删除凭据密钥',
192
184
  'accounts.save_btn': '保存账号',
193
185
  'accounts.cancel_btn': '取消',
194
- 'accounts.key_label': 'API 密钥',
195
186
  'accounts.quota_used': '已用 {pct}%',
196
187
  'accounts.last_failover': '最近故障转移:{from} → {to} ({reason})',
197
188
  'quota.title': '📊 ClinePass 用量配额与限额监控',
@@ -203,7 +194,6 @@ const en = {
203
194
  'quota.window_5h': '⏱ 5 小时滚动限额',
204
195
  'quota.window_weekly': '📅 每周滚动窗口',
205
196
  'quota.used': '已使用 {pct}%',
206
- 'quota.remaining': '剩余可用:{pct}%',
207
197
  'quota.reset_at': '重置时间:{time}',
208
198
  'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
209
199
  'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
@@ -236,7 +226,6 @@ const en = {
236
226
  'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
237
227
  'models.ctx_default': '默认 ({ctx})',
238
228
  'models.ctx_original': '原厂 ({ctx} · {provider})',
239
- 'models.ctx_custom': '自定义…',
240
229
  'models.ctx_edit': '编辑上下文窗口',
241
230
  'models.ctx_custom_placeholder': 'Token 数量 (如 2000000)',
242
231
  'models.ctx_save': '保存',
@@ -253,7 +242,6 @@ const en = {
253
242
  'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试或强制重新同步。',
254
243
  'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
255
244
  'diag.smoke_testing': '正在测试…',
256
- 'diag.smoke_ok': '冒烟测试通过!网络延迟:{latency} ms',
257
245
  'diag.resync_btn': '强制重新同步到 DSH',
258
246
  'diag.resyncing': '正在同步…',
259
247
  'diag.resynced_msg': '模型服务已重新同步到 DSH ({count} 个模型)',
@@ -707,8 +695,8 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
707
695
  {
708
696
  onSubmit: onSubmitAdd,
709
697
  style: {
710
- background: 'var(--dsw-alias-bg-hover, rgba(0,0,0,0.04))',
711
- border: '1px solid var(--dsw-alias-border, rgba(0,0,0,0.12))',
698
+ background: 'var(--dsw-alias-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
699
+ border: '1px solid var(--dsw-alias-border, color-mix(in srgb, currentColor 12%, transparent))',
712
700
  borderRadius: '8px',
713
701
  padding: '12px',
714
702
  marginBottom: '14px',
@@ -1206,7 +1194,7 @@ function ModelsSection({
1206
1194
  display: 'flex',
1207
1195
  flexDirection: 'column',
1208
1196
  gap: '6px',
1209
- background: 'var(--dsw-alias-bg-hover, rgba(0,0,0,0.04))',
1197
+ background: 'var(--dsw-alias-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
1210
1198
  padding: '6px',
1211
1199
  borderRadius: '6px',
1212
1200
  border: '1px solid var(--dsw-alias-border-l2)',
@@ -1,3 +1,11 @@
1
+ export {
2
+ toCredentialRef,
3
+ resolveApiKey,
4
+ saveCredentialKey,
5
+ deleteCredentialKey,
6
+ resolveKeyValue,
7
+ } from './credential-refs.js'
8
+
1
9
  /**
2
10
  * ClineBot / ClinePass client helpers for DeepSeek Harness.
3
11
  *
@@ -22,20 +30,7 @@ export const USAGE_CACHE_TTL_MS = 60000
22
30
 
23
31
  export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, DEFAULT_MODEL_ID }
24
32
 
25
- /**
26
- * Resolve or fallback credential reference descriptor.
27
- */
28
- export async function toCredentialRef(name) {
29
- try {
30
- const mod = await import('@deepseek-ai/dsh-credentials')
31
- if (typeof mod.credentialRef === 'function') {
32
- return mod.credentialRef(name)
33
- }
34
- } catch {
35
- /* fallback when executed in standalone unit tests outside DSH bundle */
36
- }
37
- return typeof name === 'object' && name !== null ? name : { type: 'env', name: String(name || '') }
38
- }
33
+ // Credentials helpers extracted to ./credential-refs.js
39
34
 
40
35
  /**
41
36
  * Normalize base URL ensuring clean format without trailing slashes.
@@ -57,57 +52,7 @@ export {
57
52
  resetSessionStats,
58
53
  } from './telemetry.js'
59
54
 
60
- /**
61
- * Resolve API key from environment variables.
62
- */
63
- export function resolveApiKey(apiKeyEnv, env = process.env) {
64
- const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
65
- return {
66
- envName: name,
67
- value: String(env[name] || ''),
68
- }
69
- }
70
-
71
- /**
72
- * Save API key directly into DSH credentials service (~/.dsh/.credentials.yaml).
73
- */
74
- export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
75
- const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
76
- const value = String(apiKey || '').trim()
77
-
78
- if (!value) {
79
- throw new Error('API key cannot be empty')
80
- }
81
-
82
- const credentials = ctx?.credentials || ctx?.get?.('credentials')
83
- if (!credentials || typeof credentials.set !== 'function') {
84
- throw new Error('DSH credentials service is unavailable in this runtime profile')
85
- }
86
-
87
- const ref = await toCredentialRef(name)
88
- await credentials.set(ref, value)
89
- return { ok: true, envName: name }
90
- }
91
55
 
92
- /**
93
- * Delete API key from DSH credentials service.
94
- */
95
- export async function deleteCredentialKey(ctx, apiKeyEnv) {
96
- const name = String(apiKeyEnv || '').trim()
97
- if (!name) return { ok: false }
98
- const credentials = ctx?.credentials || ctx?.get?.('credentials')
99
- if (!credentials) return { ok: false }
100
- try {
101
- const ref = await toCredentialRef(name)
102
- if (typeof credentials.unset === 'function') {
103
- await credentials.unset(ref)
104
- return { ok: true, envName: name }
105
- }
106
- } catch (err) {
107
- console.warn('[dsh-clinebot] Failed unsetting credential:', err)
108
- }
109
- return { ok: false }
110
- }
111
56
 
112
57
  function abortAfter(ms) {
113
58
  const ac = new AbortController()
@@ -560,31 +505,7 @@ export function buildPiAiProvider({
560
505
  }
561
506
  }
562
507
 
563
- /**
564
- * Safely resolve key value from DSH credentials service or process.env.
565
- */
566
- export async function resolveKeyValue(ctx, apiKeyEnv) {
567
- const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
568
- const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
569
- if (creds && typeof creds.resolve === 'function') {
570
- try {
571
- const ref = await toCredentialRef(refName)
572
- const hit = await creds.resolve(ref)
573
- if (hit?.value) {
574
- return { envName: refName, value: hit.value, source: 'credentials' }
575
- }
576
- } catch {
577
- /* miss */
578
- }
579
- }
580
-
581
- const fromEnv = resolveApiKey(refName)
582
- if (fromEnv.value) {
583
- return { ...fromEnv, source: 'env' }
584
- }
585
508
 
586
- return { envName: refName, value: '', source: 'none' }
587
- }
588
509
 
589
510
 
590
511
  export { resolveAccountPool, isAccountQuotaExhausted, rotateToNextAccount, getLastRotation, setLastRotation, isQuotaExceededError } from './account-pool.js'
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Credentials management helper for @goodandready/dsh-clinebot.
3
+ * Isolates DSH Credentials storage and environment resolution.
4
+ */
5
+
6
+ export const DEFAULT_API_KEY_ENV = 'CLINEBOT_API_KEY'
7
+
8
+ export async function toCredentialRef(name) {
9
+ try {
10
+ const mod = await import('@deepseek-ai/dsh-credentials')
11
+ if (typeof mod.credentialRef === 'function') {
12
+ return mod.credentialRef(name)
13
+ }
14
+ } catch {
15
+ /* fallback when executed in standalone unit tests outside DSH bundle */
16
+ }
17
+ return typeof name === 'object' && name !== null ? name : { type: 'env', name: String(name || '') }
18
+ }
19
+
20
+ export function resolveApiKey(apiKeyEnv, env = process.env) {
21
+ const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
22
+ return {
23
+ envName: name,
24
+ value: String(env[name] || ''),
25
+ }
26
+ }
27
+
28
+ export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
29
+ const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
30
+ const value = String(apiKey || '').trim()
31
+
32
+ if (!value) {
33
+ throw new Error('API key cannot be empty')
34
+ }
35
+
36
+ const credentials = ctx?.credentials || ctx?.get?.('credentials')
37
+ if (!credentials || typeof credentials.set !== 'function') {
38
+ throw new Error('DSH credentials service is unavailable in this runtime profile')
39
+ }
40
+
41
+ const ref = await toCredentialRef(name)
42
+ await credentials.set(ref, value)
43
+ return { ok: true, envName: name }
44
+ }
45
+
46
+ export async function deleteCredentialKey(ctx, apiKeyEnv) {
47
+ const name = String(apiKeyEnv || '').trim()
48
+ if (!name) return { ok: false }
49
+ const credentials = ctx?.credentials || ctx?.get?.('credentials')
50
+ if (!credentials) return { ok: false }
51
+ try {
52
+ const ref = await toCredentialRef(name)
53
+ if (typeof credentials.unset === 'function') {
54
+ await credentials.unset(ref)
55
+ return { ok: true, envName: name }
56
+ }
57
+ } catch (err) {
58
+ ctx?.logger?.warn?.('[dsh-clinebot] Failed unsetting credential: ' + (err?.message || err))
59
+ }
60
+ return { ok: false }
61
+ }
62
+
63
+ export async function resolveKeyValue(ctx, apiKeyEnv) {
64
+ const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
65
+ const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
66
+ if (creds && typeof creds.resolve === 'function') {
67
+ try {
68
+ const ref = await toCredentialRef(refName)
69
+ const hit = await creds.resolve(ref)
70
+ if (hit?.value) {
71
+ return { envName: refName, value: hit.value, source: 'credentials' }
72
+ }
73
+ } catch {
74
+ /* miss */
75
+ }
76
+ }
77
+
78
+ const fromEnv = resolveApiKey(refName)
79
+ if (fromEnv.value) {
80
+ return { ...fromEnv, source: 'env' }
81
+ }
82
+
83
+ return { envName: refName, value: '', source: 'none' }
84
+ }
package/lib/index.js CHANGED
@@ -26,7 +26,11 @@ import { registerAuthRoutes } from './routes/auth.js'
26
26
  import { registerSlashCommand } from './slash-command.js'
27
27
 
28
28
  export const name = '@goodandready/dsh-clinebot'
29
- export const inject = ['settings', 'webServer', 'credentials']
29
+ export const inject = ['webServer', 'credentials']
30
+
31
+ export function configReader(config) {
32
+ return () => config
33
+ }
30
34
 
31
35
  export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
32
36
 
@@ -56,73 +60,62 @@ export function apply(ctx, config) {
56
60
  autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
57
61
  }
58
62
 
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
- }
63
+ const getRevision = (svc) => {
64
+ try {
65
+ return svc?.describe?.().find((row) => row.ns === NS)?.revision
66
+ } catch {
67
+ return undefined
98
68
  }
99
- return undefined
100
69
  }
101
70
 
102
- if (typeof ctx.inject === 'function') {
103
- ctx.inject(['settings'], (sctx) => {
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)
71
+ settingsApi = {
72
+ get: () => live(),
73
+ replace: async (next) => {
74
+ const parsed = Config(structuredClone(plainConfig(next)))
75
+ currentConfig = parsed
76
+ const svc = ctx?.get?.('settings')
77
+ if (svc) {
78
+ const payload = volatileConfig(parsed)
79
+ if (typeof svc.replace === 'function') {
80
+ await svc.replace(NS, payload, getRevision(svc))
81
+ } else if (typeof svc.update === 'function') {
82
+ await svc.update(NS, payload, getRevision(svc))
83
+ }
113
84
  }
114
- sctx.effect(() => () => {
115
- getConfig = () => config
116
- settingsApi = undefined
117
- })
118
- })
85
+ await syncProviderState(live())
86
+ return parsed
87
+ },
88
+ update: async (patch) => {
89
+ const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
90
+ currentConfig = merged
91
+ const svc = ctx?.get?.('settings')
92
+ if (svc) {
93
+ const payload = volatileConfig(merged)
94
+ if (typeof svc.update === 'function') {
95
+ await svc.update(NS, payload, getRevision(svc))
96
+ } else if (typeof svc.replace === 'function') {
97
+ await svc.replace(NS, payload, getRevision(svc))
98
+ }
99
+ }
100
+ await syncProviderState(live())
101
+ return merged
102
+ },
103
+ watch: (cb) => {
104
+ const svc = ctx?.get?.('settings')
105
+ if (typeof svc?.watch === 'function') return svc.watch(cb)
106
+ return () => {}
107
+ },
119
108
  }
120
109
 
121
110
  syncProviderState(live())
122
111
  if (typeof ctx.effect === 'function') {
123
112
  ctx.effect(() => {
124
113
  const timer = setTimeout(triggerAutoDiscover, 500)
125
- return () => clearTimeout(timer)
114
+ const retryTimer = setTimeout(triggerAutoDiscover, 3000)
115
+ return () => {
116
+ clearTimeout(timer)
117
+ clearTimeout(retryTimer)
118
+ }
126
119
  }, 'dsh-clinebot: auto-discover')
127
120
  }
128
121
 
package/lib/models.js CHANGED
@@ -11,6 +11,29 @@
11
11
  */
12
12
 
13
13
  export const PROVIDER_ID = 'clinebot'
14
+
15
+ export const OBSOLETE_MODEL_ID_MAP = Object.freeze({
16
+ 'cline-pass/deepseek-v41-flash': 'cline-pass/deepseek-v4.1-flash',
17
+ 'cline-pass/qwen38-max': 'cline-pass/qwen3.8-max',
18
+ 'cline-pass/glm-53': 'cline-pass/glm-5.3',
19
+ 'cline-pass/glm-53-flash': 'cline-pass/glm-5.3-flash',
20
+ 'cline-pass/muse-spark-13-contributor': 'cline-pass/muse-spark-1.3-contributor',
21
+ })
22
+
23
+ export function migrateModelId(id) {
24
+ if (typeof id !== 'string') return id
25
+ return OBSOLETE_MODEL_ID_MAP[id] || id
26
+ }
27
+
28
+ export function migrateModelEntry(model) {
29
+ if (!model || typeof model !== 'object') return model
30
+ const canonicalId = migrateModelId(model.id)
31
+ if (canonicalId === model.id) return model
32
+ return {
33
+ ...model,
34
+ id: canonicalId,
35
+ }
36
+ }
14
37
  export const PROVIDER_DISPLAY_NAME = 'ClineBot (ClinePass)'
15
38
  export const DEFAULT_MODEL_ID = 'cline-pass/deepseek-v4-flash'
16
39
 
@@ -314,10 +337,17 @@ export function parsePlanIncludedModels(includedInput) {
314
337
  * Applies user-defined modelContextOverrides (custom contextLength and maxTokens).
315
338
  */
316
339
  export function getAllModels(dynamicModels = [], modelContextOverrides = []) {
340
+ const normalizedDynamic = (Array.isArray(dynamicModels) ? dynamicModels : []).map(migrateModelEntry)
317
341
  const overridesMap = new Map()
318
342
  if (Array.isArray(modelContextOverrides)) {
319
343
  for (const ov of modelContextOverrides) {
320
- if (ov && ov.modelId) overridesMap.set(ov.modelId, ov)
344
+ if (ov && ov.modelId) {
345
+ overridesMap.set(ov.modelId, ov)
346
+ const canonicalId = migrateModelId(ov.modelId)
347
+ if (canonicalId !== ov.modelId) {
348
+ overridesMap.set(canonicalId, { ...ov, modelId: canonicalId })
349
+ }
350
+ }
321
351
  }
322
352
  }
323
353
 
@@ -344,9 +374,9 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = []) {
344
374
  }
345
375
  }
346
376
 
347
- if (Array.isArray(dynamicModels) && dynamicModels.length > 0) {
377
+ if (normalizedDynamic.length > 0) {
348
378
  const normalize = (s) => String(s || '').toLowerCase().replace(/[\s._-]+/g, '')
349
- return dynamicModels.map((dm) => {
379
+ return normalizedDynamic.map((dm) => {
350
380
  const targetNorm = normalize(dm.id.replace(/^cline-pass\//, ''))
351
381
  const curated = CLINE_MODELS.find(
352
382
  (cm) => cm.id === dm.id || normalize(cm.id.replace(/^cline-pass\//, '')) === targetNorm || normalize(cm.name) === targetNorm
@@ -476,9 +506,18 @@ export async function loadModelsDiskCache(cachePath) {
476
506
  const fs = await import('node:fs/promises')
477
507
  const raw = await fs.readFile(cachePath, 'utf8')
478
508
  const parsed = JSON.parse(raw)
479
- const list = Array.isArray(parsed?.models) ? parsed.models : (Array.isArray(parsed) ? parsed : null)
480
- if (!list) return null
509
+ const rawList = Array.isArray(parsed?.models) ? parsed.models : (Array.isArray(parsed) ? parsed : null)
510
+ if (!rawList) return null
511
+ let hasObsolete = false
512
+ const list = rawList.map((m) => {
513
+ const migrated = migrateModelEntry(m)
514
+ if (migrated.id !== m.id) hasObsolete = true
515
+ return migrated
516
+ })
481
517
  list.planSyncedAt = typeof parsed?.planSyncedAt === 'number' ? parsed.planSyncedAt : (typeof parsed?.savedAt === 'number' ? parsed.savedAt : 0)
518
+ if (hasObsolete) {
519
+ saveModelsDiskCache(cachePath, list, list.planSyncedAt).catch(() => {})
520
+ }
482
521
  return list
483
522
  } catch {
484
523
  return null
@@ -217,12 +217,26 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
217
217
  const cacheFile = resolvePathWithHome(pub.modelsCachePath)
218
218
  const settingsApi = getSettingsApi()
219
219
 
220
+ // Auto-migrate obsolete IDs (e.g. deepseek-v41-flash -> deepseek-v4.1-flash)
221
+ const existingDynamic = pub.dynamicModels || []
222
+ const hasObsolete = existingDynamic.some((m) => Boolean(OBSOLETE_MODEL_ID_MAP[m?.id]))
223
+ if (hasObsolete && settingsApi?.replace) {
224
+ const migrated = existingDynamic.map(migrateModelEntry)
225
+ const next = Config({
226
+ ...plainConfig(live()),
227
+ dynamicModels: migrated,
228
+ })
229
+ await settingsApi.replace(next)
230
+ await syncProviderState(next)
231
+ }
232
+
220
233
  if ((!pub.dynamicModels || !pub.dynamicModels.length) && cacheFile) {
221
234
  const fromDisk = await loadModelsDiskCache(cacheFile)
222
235
  if (Array.isArray(fromDisk) && fromDisk.length && settingsApi?.replace) {
236
+ const migratedDisk = fromDisk.map(migrateModelEntry)
223
237
  const next = Config({
224
238
  ...plainConfig(live()),
225
- dynamicModels: fromDisk,
239
+ dynamicModels: migratedDisk,
226
240
  planSyncedAt: fromDisk.planSyncedAt || Date.now(),
227
241
  })
228
242
  await settingsApi.replace(next)
@@ -22,6 +22,7 @@ import {
22
22
  } from './cline-client.js'
23
23
 
24
24
  export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderState }) {
25
+ if (typeof ctx.inject !== 'function') return
25
26
  ctx.inject(['commands'], (cmdCtx) => {
26
27
  const commands = cmdCtx.commands
27
28
  if (typeof commands?.register !== 'function') return
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",
@@ -18,7 +18,8 @@
18
18
  "README.ru.md",
19
19
  "README.zh.md",
20
20
  "CHANGELOG.md",
21
- "LICENSE"
21
+ "LICENSE",
22
+ "media/visual-verification.png"
22
23
  ],
23
24
  "keywords": [
24
25
  "dsh",