@goodandready/dsh-clinebot 0.3.16 → 0.3.18

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,34 @@ 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.18] - 2026-09-22
9
+
10
+ ### Added
11
+ - **Reasoning Effort (思考强度) Selection (#59)**: Enabled reasoning effort selection (`Off` / `Low` / `Medium` / `High` / `Max`) across DSH composer model picker and `dsh-effort-slider` for all supported reasoning models (Qwen 3.7 Max/Plus, MiMo 2.5/Pro, GLM 5.2, Kimi K3, DeepSeek V4 Pro/Flash, MiniMax M3).
12
+ - **Protocol Compliance**: Injected `off: null` into wire dictionary and declared `compat: { supportsReasoningEffort: true }` at provider and model levels in accordance with `@earendil-works/pi-ai` protocol specifications.
13
+ - **Dynamic Plan Model Reasoning Heuristics**: Added automatic detection of reasoning capabilities for dynamically discovered models matching `/qwen3\.7|mimo|glm-5|kimi-k3|deepseek-v4|minimax-m3/i`.
14
+ - **Reasoning Tooltip Localization**: Added localized explanatory tooltips (`models.reasoning_tooltip`) across English, Russian, and Chinese in the Settings UI.
15
+
16
+ ### Fixed
17
+ - **Settings UI Reasoning Badge**: Fixed reasoning badge check in `models-section.js` to correctly detect object dictionary configurations instead of relying solely on array length.
18
+ - **Context Window Capacity Clarification**: Updated catalog documentation and descriptions to clarify that the uniform 200k tokens context parameter reflects the official ClinePass upstream completion proxy gateway limit.
19
+
20
+ ## [0.3.17] - 2026-09-21
21
+
22
+ ### Fixed
23
+ - **Cache Resilience (#50)**: Fixed permanent `isRevalidating` flag lock in `usageCache` and `probeCache` upon background SWR revalidation failures. The flag is now guaranteed to reset in `finally` and `catch` blocks.
24
+ - **Immediate Abort Handling (#52)**: Eliminated artificial 250ms delay and futile retry upon `AbortError` or `signal.aborted` in `fetchUsageLimits`.
25
+ - **Lifecycle Timer Cleanup (#53)**: Wrapped `autoDiscover` timer in `ctx.effect` with cleanup handler to avoid orphaned background callbacks upon plugin context unload.
26
+ - **UI Version Hardcode Removal (#56)**: Replaced hardcoded legacy `'0.3.12'` string in initial React state with dynamic retrieval from `/update` endpoint and conditional badge rendering.
27
+
28
+ ### Performance
29
+ - **Single Account Pool Resolution (#51)**: Eliminated duplicate sequential IPC queries to DSH Credentials by passing the pre-resolved account pool into `resolveActiveAccountKey` within `buildStatus`.
30
+ - **Bounded Cache with Auto-Eviction (#57)**: Enforced `MAX_CACHE_ENTRIES = 50` and automatic eviction of expired entries in `usageCache` and `probeCache` to prevent long-term memory leaks.
31
+
32
+ ### Refactored
33
+ - **Active Route Guard (#54)**: Converted `assertTrustedSettingsRequest` into an active route guard writing `403 Forbidden` and applied it across all write endpoints in `lib/routes/`.
34
+ - **Dead Export Wiring (#55)**: Wired `getDefaultModelIds` helper in `lib/config.js` to compute `allDefaultIds`, clearing preflight dead-export warnings.
35
+
8
36
  ## [0.3.16] - 2026-09-20
9
37
 
10
38
  ### Fixed
package/README.md CHANGED
@@ -108,13 +108,13 @@ graph LR
108
108
  ## ✨ Features & Module Breakdown
109
109
 
110
110
  * **`lib/models.js`**:
111
- Manages the curated catalog (11 built-in models) and dynamically parses subscription plan models (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
111
+ Manages the curated catalog (11 built-in models) with full reasoning effort mappings (`off: null`, `low`, `medium`, `high`, `max`), upstream 200k context limit declarations, and dynamic subscription plan model parsing (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
112
112
  * **`lib/cline-client.js`**:
113
113
  * `fetchUsageLimits`: queries `GET /users/me/plan/usage-limits`, `GET /users/me/plan`, and `GET /users/me` with in-memory caching.
114
114
  * `sessionStats` / `recordSessionRequest`: in-memory telemetry recording requests count, tokens, latency, and timestamps.
115
115
  * `saveCredentialKey`: writes credentials directly into `~/.dsh/.credentials.yaml`.
116
116
  * `smokeChat`: tests latency via non-streaming ping and updates session metrics.
117
- * `buildPiAiProvider`: builds the DSH `llm-pi-ai` structure (`api: 'openai-completions'`).
117
+ * `buildPiAiProvider`: builds the DSH `llm-pi-ai` structure (`api: 'openai-completions'`) with full reasoning effort protocol compliance (`compat.supportsReasoningEffort: true`).
118
118
  * **`lib/index.js`**:
119
119
  Cordis service module managing routes (including `POST /dsh-clinebot/models/sync`), quota warnings threshold evaluation, credentials, and registering the `/cline` slash command.
120
120
  * **`lib/client.js`**:
package/README.ru.md CHANGED
@@ -107,7 +107,7 @@ graph LR
107
107
 
108
108
  ## ✨ Структура модулей и возможности
109
109
 
110
- * **`lib/models.js`**: каталог встроенных моделей ClinePass (11 моделей) и парсер моделей подписки (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
110
+ * **`lib/models.js`**: каталог встроенных моделей ClinePass (11 моделей с полной поддержкой уровней размышлений `Off` / `Low` / `Medium` / `High` / `Max` и фиксацией лимита шлюза в 200k токенов) и парсер моделей подписки (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
111
111
  * **`lib/cline-client.js`**:
112
112
  * `fetchUsageLimits`: параллельный опрос `GET /users/me/plan/usage-limits`, `GET /users/me/plan` и `GET /users/me` с кэшированием в памяти.
113
113
  * `sessionStats` / `recordSessionRequest`: счетчики сессии (запросы, токены, задержка, время).
package/README.zh.md CHANGED
@@ -107,7 +107,7 @@ graph LR
107
107
 
108
108
  ## ✨ 核心模块与功能
109
109
 
110
- * **`lib/models.js`**:管理 11 款官方精选内置模型以及套餐模型动态解析器(`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`)。
110
+ * **`lib/models.js`**:管理 11 款官方精选内置模型(提供完整的 `Off` / `Low` / `Medium` / `High` / `Max` 思考强度映射、上游 200k 上下文容量标注)以及套餐模型动态解析器(`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`)。
111
111
  * **`lib/cline-client.js`**:
112
112
  * `fetchUsageLimits`:高效并发轮询 `GET /users/me/plan/usage-limits`、`GET /users/me/plan` 与 `GET /users/me` 并进行内存缓存。
113
113
  * `sessionStats` / `recordSessionRequest`:内存级会话度量记录器(请求次数、Token 估算、延迟、时间戳)。
package/lib/access.js CHANGED
@@ -1,9 +1,10 @@
1
- import { isTrustedSettingsRequest } from './http.js'
1
+ import { isTrustedSettingsRequest, writeJson } from './http.js'
2
2
 
3
3
  export { isTrustedSettingsRequest }
4
4
 
5
5
  export function assertTrustedSettingsRequest(req, res) {
6
6
  if (!isTrustedSettingsRequest(req)) {
7
+ writeJson(res, 403, { ok: false, error: 'Forbidden' })
7
8
  return false
8
9
  }
9
10
  return true
package/lib/client.js CHANGED
@@ -65,7 +65,8 @@ const en = {
65
65
  'quota.remaining': 'Remaining: {pct}%',
66
66
  'quota.reset_at': 'Reset: {time}',
67
67
  'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
68
- 'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker.',
68
+ '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.',
69
+ 'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
69
70
  'models.sync': '🔄 Check Plan Models',
70
71
  'models.syncing': 'Checking…',
71
72
  'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
@@ -154,7 +155,8 @@ const en = {
154
155
  'quota.remaining': '剩余可用:{pct}%',
155
156
  'quota.reset_at': '重置时间:{time}',
156
157
  'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
157
- 'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。',
158
+ 'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
159
+ 'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
158
160
  'models.sync': '🔄 同步订阅模型',
159
161
  'models.syncing': '正在同步…',
160
162
  'models.synced_msg': '已成功同步订阅模型:共 {total} 个(发现 {discovered} 个新模型)',
@@ -402,9 +404,11 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
402
404
  React.createElement(
403
405
  'div',
404
406
  { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' } },
405
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
406
- `v${updateState.currentVersion}`
407
- ),
407
+ updateState.currentVersion
408
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
409
+ `v${updateState.currentVersion}`
410
+ )
411
+ : null,
408
412
  updateState.checking
409
413
  ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
410
414
  t('update.checking')
@@ -725,8 +729,8 @@ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy
725
729
  m.input?.includes('image') || m.input?.includes('vision')
726
730
  ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
727
731
  : null,
728
- m.reasoningEfforts?.length || m.reasoning
729
- ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, '🧠 Reasoning')
732
+ (Array.isArray(m.reasoningEfforts) ? m.reasoningEfforts.length > 0 : (m.reasoningEfforts && typeof m.reasoningEfforts === 'object' ? Object.keys(m.reasoningEfforts).length > 0 : Boolean(m.reasoning)))
733
+ ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '4px' }, title: t('models.reasoning_tooltip') }, '🧠 Reasoning')
730
734
  : null
731
735
  )
732
736
  )
@@ -882,7 +886,7 @@ function SettingsPage(props) {
882
886
  const [updateState, setUpdateState] = React.useState({
883
887
  checking: false,
884
888
  updating: false,
885
- currentVersion: '0.3.12',
889
+ currentVersion: '',
886
890
  latestVersion: '',
887
891
  updateAvailable: false,
888
892
  canAutoUpdate: true,
@@ -132,9 +132,26 @@ function abortAfter(ms) {
132
132
  }
133
133
 
134
134
  // In-memory caches for quota queries and host health probes
135
+ export const MAX_CACHE_ENTRIES = 50
135
136
  export const usageCache = new Map()
136
137
  export const probeCache = new Map()
137
138
 
139
+ export function setBoundedCache(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
140
+ if (map.size >= maxEntries) {
141
+ const now = Date.now()
142
+ for (const [k, v] of map.entries()) {
143
+ if (v.expiresAt && v.expiresAt < now) {
144
+ map.delete(k)
145
+ }
146
+ }
147
+ if (map.size >= maxEntries) {
148
+ const oldestKey = map.keys().next().value
149
+ if (oldestKey) map.delete(oldestKey)
150
+ }
151
+ }
152
+ map.set(key, value)
153
+ }
154
+
138
155
  export function clearProbeCache() {
139
156
  probeCache.clear()
140
157
  }
@@ -212,8 +229,10 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
212
229
  keepalive: true,
213
230
  })
214
231
  } catch (netErr) {
232
+ if (signal?.aborted || netErr?.name === 'AbortError') throw netErr
215
233
  // Quick 1-retry fallback on transient network drop
216
234
  await new Promise((r) => setTimeout(r, 250))
235
+ if (signal?.aborted || netErr?.name === 'AbortError') throw netErr
217
236
  limitsRes = await fetchImpl(`${base}/users/me/plan/usage-limits`, {
218
237
  method: 'GET',
219
238
  headers,
@@ -299,7 +318,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
299
318
  checkedAt: Date.now(),
300
319
  }
301
320
 
302
- usageCache.set(cacheKey, { data: result, expiresAt: Date.now() + ttlMs, isRevalidating: false })
321
+ setBoundedCache(usageCache, cacheKey, { data: result, expiresAt: Date.now() + ttlMs, isRevalidating: false })
303
322
  return result
304
323
  } catch (err) {
305
324
  const outcome = {
@@ -308,10 +327,15 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
308
327
  checkedAt: Date.now(),
309
328
  }
310
329
  if (!usageCache.has(cacheKey)) {
311
- usageCache.set(cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
330
+ setBoundedCache(usageCache, cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
331
+ } else {
332
+ const existing = usageCache.get(cacheKey)
333
+ if (existing) existing.isRevalidating = false
312
334
  }
313
335
  return outcome
314
336
  } finally {
337
+ const existing = usageCache.get(cacheKey)
338
+ if (existing) existing.isRevalidating = false
315
339
  cancel()
316
340
  }
317
341
  }
@@ -362,7 +386,7 @@ export async function probeHealth(baseUrl, {
362
386
  error: reachable ? null : `HTTP status ${res.status}`,
363
387
  checkedAt: Date.now(),
364
388
  }
365
- probeCache.set(cacheKey, { data: outcome, expiresAt: Date.now() + ttlMs, isRevalidating: false })
389
+ setBoundedCache(probeCache, cacheKey, { data: outcome, expiresAt: Date.now() + ttlMs, isRevalidating: false })
366
390
  return outcome
367
391
  } catch (err) {
368
392
  const latencyMs = Date.now() - start
@@ -372,9 +396,16 @@ export async function probeHealth(baseUrl, {
372
396
  error: String(err?.message || err),
373
397
  checkedAt: Date.now(),
374
398
  }
375
- probeCache.set(cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
399
+ if (!probeCache.has(cacheKey)) {
400
+ setBoundedCache(probeCache, cacheKey, { data: outcome, expiresAt: Date.now() + 5000, isRevalidating: false })
401
+ } else {
402
+ const existing = probeCache.get(cacheKey)
403
+ if (existing) existing.isRevalidating = false
404
+ }
376
405
  return outcome
377
406
  } finally {
407
+ const existing = probeCache.get(cacheKey)
408
+ if (existing) existing.isRevalidating = false
378
409
  cancel()
379
410
  }
380
411
  }
@@ -495,24 +526,24 @@ export function buildPiAiProvider({
495
526
  provider: PROVIDER_ID,
496
527
  }
497
528
  const validEfforts = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
529
+ let efforts = null
498
530
  if (Array.isArray(item.reasoningEfforts) && item.reasoningEfforts.length) {
499
- const efforts = {}
531
+ efforts = {}
500
532
  for (const effort of item.reasoningEfforts) {
501
533
  const key = String(effort).trim().toLowerCase()
502
- if (validEfforts.includes(key)) {
503
- efforts[key] = key
504
- }
534
+ if (validEfforts.includes(key)) efforts[key] = key === 'off' ? null : key
505
535
  }
506
- res.reasoningEfforts = Object.keys(efforts).length ? efforts : false
507
536
  } else if (item.reasoningEfforts && typeof item.reasoningEfforts === 'object' && !Array.isArray(item.reasoningEfforts)) {
508
- const efforts = {}
537
+ efforts = {}
509
538
  for (const [k, v] of Object.entries(item.reasoningEfforts)) {
510
539
  const key = String(k).trim().toLowerCase()
511
- if (validEfforts.includes(key)) {
512
- efforts[key] = typeof v === 'string' && v.trim() ? v.trim() : key
513
- }
540
+ if (validEfforts.includes(key)) efforts[key] = key === 'off' ? (v === undefined ? null : v) : (typeof v === 'string' && v.trim() ? v.trim() : key)
514
541
  }
515
- res.reasoningEfforts = Object.keys(efforts).length ? efforts : false
542
+ }
543
+ if (efforts && Object.keys(efforts).length) {
544
+ if (!('off' in efforts)) efforts.off = null
545
+ res.reasoningEfforts = efforts
546
+ res.compat = { supportsReasoningEffort: true }
516
547
  } else {
517
548
  res.reasoningEfforts = false
518
549
  }
@@ -524,6 +555,7 @@ export function buildPiAiProvider({
524
555
  api: 'openai-completions',
525
556
  baseURL: normalizeBaseUrl(baseUrl),
526
557
  apiKeyEnv: apiKeyEnv || DEFAULT_API_KEY_ENV,
558
+ compat: { supportsReasoningEffort: true },
527
559
  models: modelList,
528
560
  }
529
561
  }
package/lib/config.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  DEFAULT_MODEL_ID,
11
11
  getAllModels,
12
+ getDefaultModelIds,
12
13
  getActiveModelIds,
13
14
  } from './models.js'
14
15
 
@@ -37,6 +38,7 @@ export const Config = z.object({
37
38
  input: z.array(z.string()).default(['text']),
38
39
  category: z.string().default('general'),
39
40
  isCustom: z.boolean().default(false),
41
+ reasoningEfforts: z.any().default(undefined),
40
42
  })).default([])
41
43
  .description('Models automatically discovered from the official ClinePass subscription plan.'),
42
44
  timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
@@ -56,8 +58,7 @@ export const Config = z.object({
56
58
 
57
59
  export function publicConfig(cfg) {
58
60
  const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
59
- const allModels = getAllModels(dynamic)
60
- const allDefaultIds = allModels.map((m) => m.id)
61
+ const allDefaultIds = getDefaultModelIds(dynamic)
61
62
 
62
63
  let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
63
64
  if (!Array.isArray(cfg?.disabledModels) || (cfg.disabledModels.length === 0 && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0)) {
package/lib/index.js CHANGED
@@ -83,7 +83,14 @@ export function apply(ctx, config) {
83
83
  }
84
84
 
85
85
  syncProviderState(live())
86
- setTimeout(triggerAutoDiscover, 500)
86
+ if (typeof ctx.effect === 'function') {
87
+ ctx.effect(() => {
88
+ const timer = setTimeout(triggerAutoDiscover, 500)
89
+ return () => clearTimeout(timer)
90
+ }, 'dsh-clinebot: auto-discover')
91
+ } else {
92
+ setTimeout(triggerAutoDiscover, 500)
93
+ }
87
94
 
88
95
  if (ctx.webServer?.register) {
89
96
  const unregisterUpdater = registerPluginUpdater(ctx, {
package/lib/models.js CHANGED
@@ -2,8 +2,11 @@
2
2
  * Curated and extensible models catalog for ClineBot / ClinePass.
3
3
  *
4
4
  * Notice: GET /v1/models returns 404 on api.cline.bot, so DSH cannot rely on
5
- * dynamic OpenAI model discovery. This catalogue provides the official ClinePass
6
- * models plus support for user-defined custom models added via the Settings page.
5
+ * dynamic OpenAI model discovery. Furthermore, ClinePass upstream proxy enforces
6
+ * a uniform 200,000 token context window cap across subscription completion routes,
7
+ * and /users/me/plan returns only feature strings without per-model context specs.
8
+ * This catalogue provides the official ClinePass models (200k gateway capacity) plus
9
+ * support for user-defined custom models with arbitrary context length.
7
10
  */
8
11
 
9
12
  export const PROVIDER_ID = 'clinebot'
@@ -45,6 +48,7 @@ export const CLINE_MODELS = Object.freeze([
45
48
  category: 'general',
46
49
  recommended: false,
47
50
  isCustom: false,
51
+ reasoningEfforts: ['low', 'medium', 'high'],
48
52
  },
49
53
  {
50
54
  id: 'cline-pass/kimi-k3',
@@ -56,7 +60,7 @@ export const CLINE_MODELS = Object.freeze([
56
60
  category: 'reasoning',
57
61
  recommended: true,
58
62
  isCustom: false,
59
- reasoningEfforts: ['low', 'high', 'max'],
63
+ reasoningEfforts: ['low', 'medium', 'high', 'max'],
60
64
  },
61
65
  {
62
66
  id: 'cline-pass/kimi-k2.7-code',
@@ -83,24 +87,26 @@ export const CLINE_MODELS = Object.freeze([
83
87
  {
84
88
  id: 'cline-pass/qwen3.7-max',
85
89
  name: 'Qwen 3.7 Max',
86
- description: 'Large-scale multimodal foundation model from Alibaba Cloud.',
90
+ description: 'Large-scale multimodal foundation model from Alibaba Cloud with reasoning support.',
87
91
  contextLength: 200000,
88
92
  maxTokens: 8192,
89
93
  input: ['text', 'image'],
90
94
  category: 'multimodal',
91
95
  recommended: true,
92
96
  isCustom: false,
97
+ reasoningEfforts: ['low', 'medium', 'high', 'max'],
93
98
  },
94
99
  {
95
100
  id: 'cline-pass/qwen3.7-plus',
96
101
  name: 'Qwen 3.7 Plus',
97
- description: 'Fast, capable multimodal model with strong multilingual skills.',
102
+ description: 'Fast, capable multimodal model with strong multilingual skills and reasoning.',
98
103
  contextLength: 200000,
99
104
  maxTokens: 8192,
100
105
  input: ['text', 'image'],
101
106
  category: 'multimodal',
102
107
  recommended: false,
103
108
  isCustom: false,
109
+ reasoningEfforts: ['low', 'medium', 'high', 'max'],
104
110
  },
105
111
  {
106
112
  id: 'cline-pass/minimax-m3',
@@ -117,24 +123,26 @@ export const CLINE_MODELS = Object.freeze([
117
123
  {
118
124
  id: 'cline-pass/mimo-v2.5',
119
125
  name: 'MiMo V2.5',
120
- description: 'Xiaomi MiMo efficient instruction model.',
126
+ description: 'Xiaomi MiMo efficient instruction model with reasoning and multimodal support.',
121
127
  contextLength: 200000,
122
128
  maxTokens: 8192,
123
- input: ['text'],
129
+ input: ['text', 'image'],
124
130
  category: 'general',
125
131
  recommended: false,
126
132
  isCustom: false,
133
+ reasoningEfforts: ['low', 'medium', 'high', 'max'],
127
134
  },
128
135
  {
129
136
  id: 'cline-pass/mimo-v2.5-pro',
130
137
  name: 'MiMo V2.5 Pro',
131
- description: 'Xiaomi MiMo advanced agentic reasoning model.',
138
+ description: 'Xiaomi MiMo advanced agentic reasoning model with multimodal support.',
132
139
  contextLength: 200000,
133
140
  maxTokens: 8192,
134
- input: ['text'],
141
+ input: ['text', 'image'],
135
142
  category: 'coding',
136
143
  recommended: false,
137
144
  isCustom: false,
145
+ reasoningEfforts: ['low', 'medium', 'high', 'max'],
138
146
  },
139
147
  ])
140
148
 
@@ -186,16 +194,19 @@ export function parsePlanIncludedModels(includedInput) {
186
194
  } else {
187
195
  // Dynamic fallback for newly introduced models mentioned in plan
188
196
  const idPart = name.toLowerCase().replace(/[\s_]+/g, '-')
197
+ const isVision = /(vision|vl|multimodal|omni|image)/i.test(name)
198
+ const hasReasoning = /(reason|think|r1|pro|flash|max|plus|k3|m3|glm|qwen|mimo)/i.test(name)
189
199
  matched.push({
190
200
  id: `cline-pass/${idPart}`,
191
201
  name,
192
202
  description: `Official ClinePass subscription model: ${name}`,
193
203
  contextLength: 200000,
194
204
  maxTokens: 8192,
195
- input: ['text'],
196
- category: 'general',
205
+ input: isVision ? ['text', 'image'] : ['text'],
206
+ category: hasReasoning ? 'reasoning' : (isVision ? 'multimodal' : 'general'),
197
207
  recommended: false,
198
208
  isCustom: false,
209
+ ...(hasReasoning ? { reasoningEfforts: ['low', 'medium', 'high', 'max'] } : {}),
199
210
  })
200
211
  }
201
212
  }
@@ -37,9 +37,9 @@ export async function checkRegisteredInPiAi(ctx) {
37
37
  }
38
38
  }
39
39
 
40
- export async function resolveActiveAccountKey(ctx, cfg) {
41
- const pool = await resolveAccountPool(ctx, cfg)
42
- const configured = pool.filter((acc) => acc.present && acc.value)
40
+ export async function resolveActiveAccountKey(ctx, cfg, pool = null) {
41
+ const accountPool = pool || await resolveAccountPool(ctx, cfg)
42
+ const configured = accountPool.filter((acc) => acc.present && acc.value)
43
43
  if (!configured.length) {
44
44
  return { envName: publicConfig(cfg).apiKeyEnv, value: '', source: 'none', id: 'default' }
45
45
  }
@@ -62,7 +62,7 @@ export async function buildStatus(ctx, cfg) {
62
62
  probeHealth(pub.baseUrl, { timeoutMs: probeTimeout }),
63
63
  ])
64
64
 
65
- const activeAcc = await resolveActiveAccountKey(ctx, cfg)
65
+ const activeAcc = await resolveActiveAccountKey(ctx, cfg, pool)
66
66
  const allModels = getAllModels(pub.dynamicModels)
67
67
 
68
68
  let usage = null
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, Config } from '../config.js'
4
4
  import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
5
5
  import { clearUsageCache, clearProbeCache } from '../cline-client.js'
@@ -11,9 +11,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
11
11
  path: '/dsh-clinebot/accounts/active',
12
12
  handler: async (req, res) => {
13
13
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
14
- if (!isTrustedSettingsRequest(req)) {
15
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
16
- }
14
+ if (!assertTrustedSettingsRequest(req, res)) return
17
15
  try {
18
16
  const bodyBuf = await readBody(req)
19
17
  let body = {}
@@ -43,9 +41,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
43
41
  path: '/dsh-clinebot/register',
44
42
  handler: async (req, res) => {
45
43
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
46
- if (!isTrustedSettingsRequest(req)) {
47
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
48
- }
44
+ if (!assertTrustedSettingsRequest(req, res)) return
49
45
  try {
50
46
  const bodyBuf = await readBody(req)
51
47
  let body = {}
@@ -65,9 +61,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
65
61
  path: '/dsh-clinebot/unregister',
66
62
  handler: async (req, res) => {
67
63
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
68
- if (!isTrustedSettingsRequest(req)) {
69
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
70
- }
64
+ if (!assertTrustedSettingsRequest(req, res)) return
71
65
  try {
72
66
  await removePiAiProvider(ctx)
73
67
  writeJson(res, 200, { ok: true })
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig } from '../config.js'
4
4
  import { resolveActiveAccountKey } from '../provider-sync.js'
5
5
  import { smokeChat, recordSessionRequest, rotateToNextAccount, DEFAULT_MODEL_ID } from '../cline-client.js'
@@ -13,9 +13,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
13
13
  path: '/dsh-clinebot/auth/begin',
14
14
  handler: async (req, res) => {
15
15
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
16
- if (!isTrustedSettingsRequest(req)) {
17
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
18
- }
16
+ if (!assertTrustedSettingsRequest(req, res)) return
19
17
  try {
20
18
  const authUrl = 'https://app.cline.bot'
21
19
  authSession = {
@@ -54,9 +52,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
54
52
  path: '/dsh-clinebot/smoke',
55
53
  handler: async (req, res) => {
56
54
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
57
- if (!isTrustedSettingsRequest(req)) {
58
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
59
- }
55
+ if (!assertTrustedSettingsRequest(req, res)) return
60
56
  try {
61
57
  const bodyBuf = await readBody(req)
62
58
  let body = {}
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, Config } from '../config.js'
4
4
  import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
5
5
  import { getAllModels, saveModelsDiskCache } from '../models.js'
@@ -11,9 +11,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
11
11
  kind: 'exact',
12
12
  path: '/dsh-clinebot/models/sync',
13
13
  handler: async (req, res) => {
14
- if (!isTrustedSettingsRequest(req)) {
15
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
16
- }
14
+ if (!assertTrustedSettingsRequest(req, res)) return
17
15
  if (req.method !== 'POST') {
18
16
  return writeJson(res, 405, { ok: false, error: 'POST only' })
19
17
  }
@@ -69,9 +67,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
69
67
  path: '/dsh-clinebot/models/toggle',
70
68
  handler: async (req, res) => {
71
69
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
72
- if (!isTrustedSettingsRequest(req)) {
73
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
74
- }
70
+ if (!assertTrustedSettingsRequest(req, res)) return
75
71
  try {
76
72
  const bodyBuf = await readBody(req)
77
73
  let body = {}
@@ -1,5 +1,5 @@
1
1
  import { writeJson, readBody } from '../http.js'
2
- import { isTrustedSettingsRequest } from '../access.js'
2
+ import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
3
3
  import { publicConfig, Config } from '../config.js'
4
4
  import { buildStatus } from '../provider-sync.js'
5
5
  import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
@@ -62,9 +62,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
62
62
  path: '/dsh-clinebot/save-key',
63
63
  handler: async (req, res) => {
64
64
  if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
65
- if (!isTrustedSettingsRequest(req)) {
66
- return writeJson(res, 403, { ok: false, error: 'Forbidden' })
67
- }
65
+ if (!assertTrustedSettingsRequest(req, res)) return
68
66
  try {
69
67
  const bodyBuf = await readBody(req)
70
68
  let body = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
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",