@goodandready/dsh-clinebot 0.4.1 → 0.4.2
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 +7 -0
- package/lib/provider-sync.js +11 -3
- package/lib/slash-command.js +92 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.2] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Slash Command Plain Text Formatting (#107)**: Eliminated raw markdown formatting (`###`, `**`, backticks) in `/cline` command responses, preventing unrendered markdown clutter in DSH chat `<pre>` blocks.
|
|
12
|
+
- **Single-Line Collapsed Summary (#107)**: Added a concise first-line summary strictly `<= 120` characters across all subcommands (`quota`, `models`, `accounts`, `stats`, `test`), displaying cleanly in the collapsed command card without premature line breaks or truncation ellipses.
|
|
13
|
+
- **Active Key Name Resolution (#107)**: Fixed `resolveActiveAccountKey` to consistently set both `apiKeyEnv` and `envName` for pooled and default accounts, preventing `Active Key: undefined` in command output and status payloads.
|
|
14
|
+
|
|
8
15
|
## [0.4.1] - 2026-09-24
|
|
9
16
|
|
|
10
17
|
### Fixed
|
package/lib/provider-sync.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
sessionStats,
|
|
19
19
|
getLastRotation,
|
|
20
20
|
usageCache,
|
|
21
|
+
DEFAULT_API_KEY_ENV,
|
|
21
22
|
} from './cline-client.js'
|
|
22
23
|
|
|
23
24
|
export function resolvePathWithHome(p) {
|
|
@@ -58,14 +59,21 @@ export async function resolveActiveAccountKey(ctx, cfg, pool = null) {
|
|
|
58
59
|
const accountPool = pool || await resolveAccountPool(ctx, cfg)
|
|
59
60
|
const configured = accountPool.filter((acc) => acc.present && acc.value)
|
|
60
61
|
if (!configured.length) {
|
|
61
|
-
|
|
62
|
+
const fallbackEnv = publicConfig(cfg).apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
63
|
+
return { envName: fallbackEnv, apiKeyEnv: fallbackEnv, value: '', source: 'none', id: 'default' }
|
|
62
64
|
}
|
|
63
65
|
const pub = publicConfig(cfg)
|
|
66
|
+
let chosen = configured[0]
|
|
64
67
|
if (pub.activeAccount) {
|
|
65
68
|
const pinned = configured.find((acc) => acc.apiKeyEnv === pub.activeAccount || acc.id === pub.activeAccount)
|
|
66
|
-
if (pinned)
|
|
69
|
+
if (pinned) chosen = pinned
|
|
70
|
+
}
|
|
71
|
+
const keyEnv = chosen.apiKeyEnv || chosen.envName || DEFAULT_API_KEY_ENV
|
|
72
|
+
return {
|
|
73
|
+
...chosen,
|
|
74
|
+
envName: keyEnv,
|
|
75
|
+
apiKeyEnv: keyEnv,
|
|
67
76
|
}
|
|
68
|
-
return configured[0]
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export async function buildStatus(ctx, cfg) {
|
package/lib/slash-command.js
CHANGED
|
@@ -36,23 +36,27 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
36
36
|
if (subcmd === 'models') {
|
|
37
37
|
const allModels = getAllModels(pub.dynamicModels)
|
|
38
38
|
const disabledSet = new Set(pub.disabledModels || [])
|
|
39
|
+
const activeCount = allModels.filter((m) => !disabledSet.has(m.id)).length
|
|
39
40
|
const syncStatus = pub.planSynced
|
|
40
|
-
?
|
|
41
|
-
: '
|
|
41
|
+
? `Verified (${pub.planSyncedAt ? new Date(pub.planSyncedAt).toLocaleDateString([], { day: '2-digit', month: '2-digit', year: 'numeric' }) : 'synced'})`
|
|
42
|
+
: 'Unverified fallback'
|
|
43
|
+
const summary = `ClinePass Models · ${activeCount}/${allModels.length} active · ${pub.planSynced ? 'Verified with plan' : 'Fallback catalog'}`
|
|
42
44
|
const lines = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
summary,
|
|
46
|
+
'',
|
|
47
|
+
`Plan Sync: ${syncStatus}`,
|
|
48
|
+
`Active Models: ${activeCount} of ${allModels.length}`,
|
|
46
49
|
]
|
|
47
50
|
if (pub.defaultModelWarning) {
|
|
48
|
-
lines.push(
|
|
51
|
+
lines.push(`Warning: ${pub.defaultModelWarning}`)
|
|
49
52
|
}
|
|
50
53
|
lines.push('')
|
|
51
54
|
for (const m of allModels) {
|
|
52
|
-
const active = !disabledSet.has(m.id) ? '
|
|
53
|
-
const isVis = isVisionModel(m.id, pub.dynamicModels) ? '
|
|
54
|
-
const efforts = Array.isArray(m.reasoningEfforts) ?
|
|
55
|
-
lines.push(
|
|
55
|
+
const active = !disabledSet.has(m.id) ? '[OK]' : '[--]'
|
|
56
|
+
const isVis = isVisionModel(m.id, pub.dynamicModels) ? 'Vision' : 'Text'
|
|
57
|
+
const efforts = Array.isArray(m.reasoningEfforts) ? ` · Reasoning: ${m.reasoningEfforts.join(', ')}` : ''
|
|
58
|
+
lines.push(` ${active} ${m.name} (${m.id})`)
|
|
59
|
+
lines.push(` ${isVis} · ${formatModelContext(m.contextLength)}${efforts}`)
|
|
56
60
|
}
|
|
57
61
|
return lines.join('\n')
|
|
58
62
|
}
|
|
@@ -60,31 +64,37 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
60
64
|
// 2. Subcommand /cline accounts
|
|
61
65
|
if (subcmd === 'accounts') {
|
|
62
66
|
const pool = await resolveAccountPool(ctx, live())
|
|
67
|
+
const activeKey = await resolveActiveAccountKey(ctx, live(), pool)
|
|
68
|
+
const activeName = activeKey.apiKeyEnv || activeKey.envName || pub.apiKeyEnv
|
|
69
|
+
const configuredCount = pool.filter((a) => a.present).length
|
|
70
|
+
const summary = `ClinePass Accounts · ${configuredCount}/${pool.length} configured · Active: ${activeName}`
|
|
63
71
|
const lines = [
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
summary,
|
|
73
|
+
'',
|
|
74
|
+
`Active Account: ${activeName}`,
|
|
66
75
|
'',
|
|
67
76
|
]
|
|
68
77
|
for (const acc of pool) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
78
|
+
const pinStr = acc.isPinned ? ' [Pinned]' : ''
|
|
79
|
+
const statusStr = acc.present ? 'Configured' : 'Missing Key'
|
|
71
80
|
const cacheKey = `cline:usage:${(acc.value || '').slice(-8)}`
|
|
72
81
|
const cached = usageCache.get(cacheKey)?.data
|
|
73
|
-
const usageInfo = cached?.windows?.fiveHour ? ` ·
|
|
74
|
-
lines.push(
|
|
82
|
+
const usageInfo = cached?.windows?.fiveHour ? ` · 5h ${cached.windows.fiveHour.percentUsed}% used` : ''
|
|
83
|
+
lines.push(` ${acc.label} (${acc.apiKeyEnv})`)
|
|
84
|
+
lines.push(` Status: ${statusStr}${pinStr}${usageInfo}`)
|
|
75
85
|
}
|
|
76
86
|
const lastRot = getLastRotation()
|
|
77
87
|
if (lastRot) {
|
|
78
|
-
lines.push('',
|
|
88
|
+
lines.push('', `Last Failover: ${lastRot.from} -> ${lastRot.to} (${lastRot.reason})`)
|
|
79
89
|
}
|
|
80
|
-
lines.push('', 'Switch active account:
|
|
90
|
+
lines.push('', 'Switch active account: /cline switch <KEY_NAME>')
|
|
81
91
|
return lines.join('\n')
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
// 3. Subcommand /cline switch <account>
|
|
85
95
|
if (subcmd === 'switch') {
|
|
86
96
|
if (!param) {
|
|
87
|
-
return '
|
|
97
|
+
return 'Please specify account: /cline switch <KEY_NAME>'
|
|
88
98
|
}
|
|
89
99
|
clearUsageCache()
|
|
90
100
|
clearProbeCache()
|
|
@@ -93,9 +103,9 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
93
103
|
const next = Config({ ...plainConfig(live()), activeAccount: param })
|
|
94
104
|
await settingsApi.replace(next)
|
|
95
105
|
await syncProviderState(next)
|
|
96
|
-
return
|
|
106
|
+
return `Active account switched to ${param}`
|
|
97
107
|
}
|
|
98
|
-
return
|
|
108
|
+
return 'Could not apply setting (settings service unavailable).'
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
// 4. Subcommand /cline rotate (smart failover next)
|
|
@@ -103,42 +113,44 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
103
113
|
const res = await rotateToNextAccount(ctx, live(), 'slash_command', getSettingsApi())
|
|
104
114
|
if (res.rotated) {
|
|
105
115
|
await syncProviderState(live())
|
|
106
|
-
return
|
|
116
|
+
return `Account rotated: switched from ${res.previousAccount} to ${res.activeAccount}. DSH provider updated.`
|
|
107
117
|
}
|
|
108
|
-
return
|
|
118
|
+
return `Rotation skipped: ${res.message || 'no alternative configured accounts in pool'}.`
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
// 5. Subcommand /cline ping (fresh host reachability probe)
|
|
112
122
|
if (subcmd === 'ping') {
|
|
113
123
|
const health = await probeHealth(pub.baseUrl, { bypassCache: true, timeoutMs: 5000 })
|
|
114
124
|
if (health.ok) {
|
|
115
|
-
return
|
|
125
|
+
return `Cline API Pong: ${pub.baseUrl} is reachable (latency: ${health.latencyMs} ms, HTTP ${health.status})`
|
|
116
126
|
}
|
|
117
|
-
return
|
|
127
|
+
return `Cline API Ping Failed: ${health.error || 'Host unreachable'}`
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
// 6. Subcommand /cline stats (real DSH stream metrics)
|
|
121
131
|
if (subcmd === 'stats' || subcmd === 'telemetry') {
|
|
132
|
+
const summary = `ClineBot Telemetry · ${sessionStats.successfulRequests}/${sessionStats.totalRequests} reqs · ~${sessionStats.totalTokensEst} tokens`
|
|
122
133
|
const lines = [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
summary,
|
|
135
|
+
'',
|
|
136
|
+
`Requests: ${sessionStats.successfulRequests} successful / ${sessionStats.totalRequests} total${sessionStats.abortedRequests ? ` (${sessionStats.abortedRequests} aborted)` : ''}${sessionStats.failedRequests ? ` (${sessionStats.failedRequests} failed)` : ''}`,
|
|
137
|
+
`Tokens: ~${sessionStats.totalTokensEst} total (~${sessionStats.promptTokensEst} prompt, ~${sessionStats.completionTokensEst} completion)`,
|
|
138
|
+
`Last Latency: ${sessionStats.lastLatencyMs !== null ? `${sessionStats.lastLatencyMs} ms` : '—'}`,
|
|
139
|
+
`Last Request: ${sessionStats.lastRequestAt ? new Date(sessionStats.lastRequestAt).toLocaleTimeString() : '—'}`,
|
|
128
140
|
]
|
|
129
141
|
if (sessionStats.lastError) {
|
|
130
|
-
lines.push(
|
|
142
|
+
lines.push(`Last Error: ${sessionStats.lastError}`)
|
|
131
143
|
}
|
|
132
144
|
if (sessionStats.byModel && Object.keys(sessionStats.byModel).length) {
|
|
133
|
-
lines.push('', '
|
|
145
|
+
lines.push('', 'By Model:')
|
|
134
146
|
for (const [mId, mStats] of Object.entries(sessionStats.byModel)) {
|
|
135
|
-
lines.push(
|
|
147
|
+
lines.push(` ${mId}: ${mStats.requests} reqs, ~${mStats.promptTokens + mStats.completionTokens} tokens`)
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
if (sessionStats.lastSmoke) {
|
|
139
151
|
const smoke = sessionStats.lastSmoke
|
|
140
152
|
const smokeTime = new Date(smoke.at).toLocaleTimeString()
|
|
141
|
-
lines.push('',
|
|
153
|
+
lines.push('', `Last Smoke Test: ${smoke.ok ? 'Passed' : 'Failed'} (${smoke.latencyMs} ms at ${smokeTime}${smoke.model ? `, model: ${smoke.model}` : ''})`)
|
|
142
154
|
}
|
|
143
155
|
return lines.join('\n')
|
|
144
156
|
}
|
|
@@ -146,11 +158,11 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
146
158
|
// 7. Subcommand /cline test [model] / smoke
|
|
147
159
|
if (subcmd === 'test' || subcmd === 'smoke') {
|
|
148
160
|
if (param && !isSupportedModel(param, pub.dynamicModels)) {
|
|
149
|
-
return
|
|
161
|
+
return `ClineBot: Model "${param}" is not recognized. Use /cline models to see available models.`
|
|
150
162
|
}
|
|
151
163
|
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
152
164
|
if (!activeKey.value) {
|
|
153
|
-
return '
|
|
165
|
+
return 'ClineBot: API key is not configured. Open Settings -> Plugins -> ClineBot.'
|
|
154
166
|
}
|
|
155
167
|
const modelToTest = param || pub.defaultModel || DEFAULT_MODEL_ID
|
|
156
168
|
const outcome = await smokeChat(pub.baseUrl, activeKey.value, {
|
|
@@ -169,25 +181,28 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
169
181
|
const failover = await rotateToNextAccount(ctx, live(), 'smoke_429', getSettingsApi())
|
|
170
182
|
if (failover.rotated) {
|
|
171
183
|
await syncProviderState(live())
|
|
172
|
-
failoverNotice = `\
|
|
184
|
+
failoverNotice = `\nAuto-failover: HTTP 429 detected! Active account automatically rotated to ${failover.activeAccount}.`
|
|
173
185
|
}
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
if (outcome.ok) {
|
|
189
|
+
const summary = `Smoke Test Passed · ${outcome.model} · ${outcome.latencyMs} ms`
|
|
177
190
|
return [
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
summary,
|
|
192
|
+
'',
|
|
193
|
+
`Model: ${outcome.model}`,
|
|
194
|
+
`Latency: ${outcome.latencyMs} ms`,
|
|
195
|
+
`Tokens: prompt: ${outcome.promptTokens}, completion: ${outcome.completionTokens} (total: ${outcome.totalTokens})`,
|
|
196
|
+
`Preview: "${outcome.preview}"`,
|
|
182
197
|
].join('\n')
|
|
183
198
|
}
|
|
184
|
-
return
|
|
199
|
+
return `Smoke Test Failed: ${outcome.error} (HTTP ${outcome.status || 'timeout'})${failoverNotice}`
|
|
185
200
|
}
|
|
186
201
|
|
|
187
|
-
//
|
|
202
|
+
// 8. Subcommand /cline quota or /cline balance
|
|
188
203
|
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
189
204
|
if (!activeKey.value) {
|
|
190
|
-
return '
|
|
205
|
+
return 'ClineBot: API key is not configured. Open Settings -> Plugins -> ClineBot.'
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
const [health, usage] = await Promise.all([
|
|
@@ -197,41 +212,55 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
197
212
|
|
|
198
213
|
const fiveHour = usage?.windows?.fiveHour
|
|
199
214
|
const weekly = usage?.windows?.weekly
|
|
200
|
-
const reset5h = fiveHour?.resetsAt ? new Date(fiveHour.resetsAt).toLocaleTimeString() : 'N/A'
|
|
201
|
-
const resetWk = weekly?.resetsAt ? new Date(weekly.resetsAt).toLocaleDateString() : 'N/A'
|
|
215
|
+
const reset5h = fiveHour?.resetsAt ? new Date(fiveHour.resetsAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'N/A'
|
|
216
|
+
const resetWk = weekly?.resetsAt ? new Date(weekly.resetsAt).toLocaleDateString([], { day: '2-digit', month: '2-digit' }) : 'N/A'
|
|
217
|
+
const resetWkFull = weekly?.resetsAt ? new Date(weekly.resetsAt).toLocaleDateString([], { day: '2-digit', month: '2-digit', year: 'numeric' }) : 'N/A'
|
|
202
218
|
|
|
219
|
+
const planRaw = usage?.plan || 'ClinePass'
|
|
220
|
+
const planShort = planRaw.replace(/\s*\(\$\d+\.\d+\/mo\)/i, '').trim()
|
|
221
|
+
const wkPct = typeof weekly?.percentUsed === 'number' ? `${weekly.percentUsed}%` : 'N/A'
|
|
222
|
+
const fhPct = typeof fiveHour?.percentUsed === 'number' ? `${fiveHour.percentUsed}%` : 'N/A'
|
|
223
|
+
const pingStr = health.ok ? `${health.latencyMs} ms` : 'offline'
|
|
224
|
+
const keyName = activeKey.apiKeyEnv || activeKey.envName || pub.apiKeyEnv
|
|
225
|
+
|
|
226
|
+
// Line 1: Short summary for collapsed command row (strictly <= 120 chars)
|
|
227
|
+
const summary = `${planShort} · Weekly ${wkPct}${weekly?.resetsAt ? ` (reset ${resetWk})` : ''} · 5h ${fhPct} · Ping ${pingStr}`
|
|
228
|
+
|
|
229
|
+
const pad = (str, len = 15) => (str + ':').padEnd(len, ' ')
|
|
203
230
|
const lines = [
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
231
|
+
summary,
|
|
232
|
+
'',
|
|
233
|
+
`${pad('Plan')}${planRaw}`,
|
|
234
|
+
`${pad('Active Key')}${keyName} (${activeKey.source})`,
|
|
235
|
+
`${pad('Default Model')}${pub.defaultModel}`,
|
|
236
|
+
`${pad('Host Ping')}${health.ok ? `${health.latencyMs} ms (reachable)` : 'Unreachable'}`,
|
|
208
237
|
]
|
|
238
|
+
if (usage?.user?.email) {
|
|
239
|
+
lines.splice(3, 0, `${pad('Account')}${usage.user.email}`)
|
|
240
|
+
}
|
|
209
241
|
if (pub.defaultModelWarning) {
|
|
210
|
-
lines.push(
|
|
242
|
+
lines.push(`${pad('Warning')}${pub.defaultModelWarning}`)
|
|
211
243
|
}
|
|
244
|
+
|
|
212
245
|
lines.push(
|
|
213
246
|
'',
|
|
214
|
-
|
|
215
|
-
|
|
247
|
+
`${pad('5-Hour Quota')}${formatProgressBar(fiveHour?.percentUsed)} (resets: ${reset5h})`,
|
|
248
|
+
`${pad('Weekly Quota')}${formatProgressBar(weekly?.percentUsed)} (resets: ${resetWkFull})`,
|
|
216
249
|
)
|
|
217
250
|
|
|
218
251
|
if (fiveHour?.percentUsed >= 95) {
|
|
219
|
-
lines.push('', '
|
|
252
|
+
lines.push('', 'CRITICAL: 5-hour quota is 95%+ consumed! New requests may be throttled until reset.')
|
|
220
253
|
} else if (fiveHour?.percentUsed >= 80) {
|
|
221
|
-
lines.push('',
|
|
254
|
+
lines.push('', `Warning: 5-hour quota is ${fiveHour.percentUsed}% consumed.`)
|
|
222
255
|
}
|
|
223
256
|
|
|
224
257
|
if (sessionStats.totalRequests > 0) {
|
|
225
|
-
lines.push('',
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (usage?.user?.email) {
|
|
229
|
-
lines.push(`* **Account**: \`${usage.user.email}\``)
|
|
258
|
+
lines.push('', `Session Stats: ${sessionStats.successfulRequests}/${sessionStats.totalRequests} successful requests, ~${sessionStats.totalTokensEst} total tokens`)
|
|
230
259
|
}
|
|
231
260
|
|
|
232
261
|
return lines.join('\n')
|
|
233
262
|
} catch (err) {
|
|
234
|
-
return
|
|
263
|
+
return `ClineBot error: ${String(err?.message || err)}`
|
|
235
264
|
}
|
|
236
265
|
}
|
|
237
266
|
|
|
@@ -250,8 +279,8 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
250
279
|
} catch (err) {
|
|
251
280
|
return {
|
|
252
281
|
kind: 'error',
|
|
253
|
-
text:
|
|
254
|
-
toString() { return
|
|
282
|
+
text: `ClineBot error: ${String(err?.message || err)}`,
|
|
283
|
+
toString() { return `ClineBot error: ${String(err?.message || err)}` },
|
|
255
284
|
}
|
|
256
285
|
}
|
|
257
286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|