@goodandready/dsh-clinebot 0.4.0 → 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.
@@ -26,10 +26,8 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
26
26
  const commands = cmdCtx.commands
27
27
  if (typeof commands?.register !== 'function') return
28
28
 
29
- const unregister = commands.register({
30
- name: 'cline',
31
- description: 'Check ClinePass subscription quota, models, accounts and test connectivity (/cline [quota|models|accounts|switch <name>|test [model]|ping|rotate])',
32
- execute: async (rawArgs) => {
29
+ const executeCommand = async (rawArgs, invocation = null) => {
30
+ try {
33
31
  const pub = publicConfig(live())
34
32
  const subcmd = String(rawArgs || '').trim().toLowerCase().split(/\s+/)[0] || 'quota'
35
33
  const param = String(rawArgs || '').trim().split(/\s+/)[1] || ''
@@ -38,23 +36,27 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
38
36
  if (subcmd === 'models') {
39
37
  const allModels = getAllModels(pub.dynamicModels)
40
38
  const disabledSet = new Set(pub.disabledModels || [])
39
+ const activeCount = allModels.filter((m) => !disabledSet.has(m.id)).length
41
40
  const syncStatus = pub.planSynced
42
- ? `✅ Verified with plan (${pub.planSyncedAt ? new Date(pub.planSyncedAt).toLocaleDateString() : 'synced'})`
43
- : '⚠️ Not verified with plan (fallback catalog)'
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'}`
44
44
  const lines = [
45
- '### 🎯 ClinePass Models Catalog',
46
- `* **Plan Sync**: ${syncStatus}`,
47
- `* **Total models**: ${allModels.length} (${allModels.filter((m) => !disabledSet.has(m.id)).length} active)`,
45
+ summary,
46
+ '',
47
+ `Plan Sync: ${syncStatus}`,
48
+ `Active Models: ${activeCount} of ${allModels.length}`,
48
49
  ]
49
50
  if (pub.defaultModelWarning) {
50
- lines.push(`* ⚠️ **Warning**: ${pub.defaultModelWarning}`)
51
+ lines.push(`Warning: ${pub.defaultModelWarning}`)
51
52
  }
52
53
  lines.push('')
53
54
  for (const m of allModels) {
54
- const active = !disabledSet.has(m.id) ? '✅' : '❌'
55
- const isVis = isVisionModel(m.id, pub.dynamicModels) ? '📷 Vision' : '📝 Text'
56
- const efforts = Array.isArray(m.reasoningEfforts) ? `🧠 [${m.reasoningEfforts.join(', ')}]` : ''
57
- lines.push(`* ${active} **${m.name}** (\`${m.id}\`) — ${isVis} · ${formatModelContext(m.contextLength)} ${efforts}`)
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}`)
58
60
  }
59
61
  return lines.join('\n')
60
62
  }
@@ -62,31 +64,37 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
62
64
  // 2. Subcommand /cline accounts
63
65
  if (subcmd === 'accounts') {
64
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}`
65
71
  const lines = [
66
- '### 🔑 ClinePass Accounts Pool',
67
- `* **Active Account**: \`${pub.activeAccount || pub.apiKeyEnv}\``,
72
+ summary,
73
+ '',
74
+ `Active Account: ${activeName}`,
68
75
  '',
69
76
  ]
70
77
  for (const acc of pool) {
71
- const pinBadge = acc.isPinned ? '📌 [Pinned]' : ''
72
- const statusBadge = acc.present ? '✅ Configured' : '⚠️ Missing Key'
78
+ const pinStr = acc.isPinned ? ' [Pinned]' : ''
79
+ const statusStr = acc.present ? 'Configured' : 'Missing Key'
73
80
  const cacheKey = `cline:usage:${(acc.value || '').slice(-8)}`
74
81
  const cached = usageCache.get(cacheKey)?.data
75
- const usageInfo = cached?.windows?.fiveHour ? ` · ⏱ ${cached.windows.fiveHour.percentUsed}% used` : ''
76
- lines.push(`* **${acc.label}** (\`${acc.apiKeyEnv}\`): ${statusBadge} ${pinBadge}${usageInfo}`)
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}`)
77
85
  }
78
86
  const lastRot = getLastRotation()
79
87
  if (lastRot) {
80
- lines.push('', `* **Last Failover**: \`${lastRot.from}\` → \`${lastRot.to}\` (${lastRot.reason})`)
88
+ lines.push('', `Last Failover: ${lastRot.from} -> ${lastRot.to} (${lastRot.reason})`)
81
89
  }
82
- lines.push('', 'Switch active account: `/cline switch <env_variable_name>`')
90
+ lines.push('', 'Switch active account: /cline switch <KEY_NAME>')
83
91
  return lines.join('\n')
84
92
  }
85
93
 
86
94
  // 3. Subcommand /cline switch <account>
87
95
  if (subcmd === 'switch') {
88
96
  if (!param) {
89
- return '⚠️ Please specify account: `/cline switch <CLINEBOT_API_KEY_2>`'
97
+ return 'Please specify account: /cline switch <KEY_NAME>'
90
98
  }
91
99
  clearUsageCache()
92
100
  clearProbeCache()
@@ -95,9 +103,9 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
95
103
  const next = Config({ ...plainConfig(live()), activeAccount: param })
96
104
  await settingsApi.replace(next)
97
105
  await syncProviderState(next)
98
- return `✅ Active account switched to \`${param}\``
106
+ return `Active account switched to ${param}`
99
107
  }
100
- return `⚠️ Could not apply setting (settings service unavailable).`
108
+ return 'Could not apply setting (settings service unavailable).'
101
109
  }
102
110
 
103
111
  // 4. Subcommand /cline rotate (smart failover next)
@@ -105,42 +113,44 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
105
113
  const res = await rotateToNextAccount(ctx, live(), 'slash_command', getSettingsApi())
106
114
  if (res.rotated) {
107
115
  await syncProviderState(live())
108
- return `🔄 **Account Rotated**: switched from \`${res.previousAccount}\` to \`${res.activeAccount}\`. DSH provider updated!`
116
+ return `Account rotated: switched from ${res.previousAccount} to ${res.activeAccount}. DSH provider updated.`
109
117
  }
110
- return `⚠️ Rotation skipped: ${res.message || 'no alternative configured accounts in pool'}.`
118
+ return `Rotation skipped: ${res.message || 'no alternative configured accounts in pool'}.`
111
119
  }
112
120
 
113
121
  // 5. Subcommand /cline ping (fresh host reachability probe)
114
122
  if (subcmd === 'ping') {
115
123
  const health = await probeHealth(pub.baseUrl, { bypassCache: true, timeoutMs: 5000 })
116
124
  if (health.ok) {
117
- return `🏓 **Cline API Pong**: \`${pub.baseUrl}\` is reachable (latency: **${health.latencyMs} ms**, HTTP ${health.status})`
125
+ return `Cline API Pong: ${pub.baseUrl} is reachable (latency: ${health.latencyMs} ms, HTTP ${health.status})`
118
126
  }
119
- return `❌ **Cline API Ping Failed**: ${health.error || 'Host unreachable'}`
127
+ return `Cline API Ping Failed: ${health.error || 'Host unreachable'}`
120
128
  }
121
129
 
122
130
  // 6. Subcommand /cline stats (real DSH stream metrics)
123
131
  if (subcmd === 'stats' || subcmd === 'telemetry') {
132
+ const summary = `ClineBot Telemetry · ${sessionStats.successfulRequests}/${sessionStats.totalRequests} reqs · ~${sessionStats.totalTokensEst} tokens`
124
133
  const lines = [
125
- '### 📈 DSH ClineBot Stream Telemetry',
126
- `* **Requests**: ${sessionStats.successfulRequests} successful / ${sessionStats.totalRequests} total${sessionStats.abortedRequests ? ` (${sessionStats.abortedRequests} aborted)` : ''}${sessionStats.failedRequests ? ` (${sessionStats.failedRequests} failed)` : ''}`,
127
- `* **Estimated Tokens**: ~${sessionStats.totalTokensEst} total (~${sessionStats.promptTokensEst} prompt, ~${sessionStats.completionTokensEst} completion)`,
128
- `* **Last Stream Latency**: ${sessionStats.lastLatencyMs !== null ? `${sessionStats.lastLatencyMs} ms` : '—'}`,
129
- `* **Last Request At**: ${sessionStats.lastRequestAt ? new Date(sessionStats.lastRequestAt).toLocaleTimeString() : '—'}`,
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() : '—'}`,
130
140
  ]
131
141
  if (sessionStats.lastError) {
132
- lines.push(`* **Last Stream Error**: ${sessionStats.lastError}`)
142
+ lines.push(`Last Error: ${sessionStats.lastError}`)
133
143
  }
134
144
  if (sessionStats.byModel && Object.keys(sessionStats.byModel).length) {
135
- lines.push('', '**By Model:**')
145
+ lines.push('', 'By Model:')
136
146
  for (const [mId, mStats] of Object.entries(sessionStats.byModel)) {
137
- lines.push(`* \`${mId}\`: ${mStats.requests} reqs, ~${mStats.promptTokens + mStats.completionTokens} tokens`)
147
+ lines.push(` ${mId}: ${mStats.requests} reqs, ~${mStats.promptTokens + mStats.completionTokens} tokens`)
138
148
  }
139
149
  }
140
150
  if (sessionStats.lastSmoke) {
141
151
  const smoke = sessionStats.lastSmoke
142
152
  const smokeTime = new Date(smoke.at).toLocaleTimeString()
143
- lines.push('', `* **Last Smoke Test**: ${smoke.ok ? '✅ Passed' : '❌ Failed'} (${smoke.latencyMs} ms at ${smokeTime}${smoke.model ? `, model: \`${smoke.model}\`` : ''})`)
153
+ lines.push('', `Last Smoke Test: ${smoke.ok ? 'Passed' : 'Failed'} (${smoke.latencyMs} ms at ${smokeTime}${smoke.model ? `, model: ${smoke.model}` : ''})`)
144
154
  }
145
155
  return lines.join('\n')
146
156
  }
@@ -148,11 +158,11 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
148
158
  // 7. Subcommand /cline test [model] / smoke
149
159
  if (subcmd === 'test' || subcmd === 'smoke') {
150
160
  if (param && !isSupportedModel(param, pub.dynamicModels)) {
151
- return `⚠️ **ClineBot**: Model \`${param}\` is not recognized. Use \`/cline models\` to see available models.`
161
+ return `ClineBot: Model "${param}" is not recognized. Use /cline models to see available models.`
152
162
  }
153
163
  const activeKey = await resolveActiveAccountKey(ctx, live())
154
164
  if (!activeKey.value) {
155
- return '⚠️ **ClineBot**: API key is not configured. Open **Settings → Plugins → ClineBot**.'
165
+ return 'ClineBot: API key is not configured. Open Settings -> Plugins -> ClineBot.'
156
166
  }
157
167
  const modelToTest = param || pub.defaultModel || DEFAULT_MODEL_ID
158
168
  const outcome = await smokeChat(pub.baseUrl, activeKey.value, {
@@ -171,25 +181,28 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
171
181
  const failover = await rotateToNextAccount(ctx, live(), 'smoke_429', getSettingsApi())
172
182
  if (failover.rotated) {
173
183
  await syncProviderState(live())
174
- failoverNotice = `\n🔄 **Auto-failover**: HTTP 429 detected! Active account automatically rotated to \`${failover.activeAccount}\`.`
184
+ failoverNotice = `\nAuto-failover: HTTP 429 detected! Active account automatically rotated to ${failover.activeAccount}.`
175
185
  }
176
186
  }
177
187
 
178
188
  if (outcome.ok) {
189
+ const summary = `Smoke Test Passed · ${outcome.model} · ${outcome.latencyMs} ms`
179
190
  return [
180
- `### 🟢 Smoke Test Passed: \`${outcome.model}\``,
181
- `* **Latency**: ${outcome.latencyMs} ms`,
182
- `* **Tokens**: prompt: ${outcome.promptTokens}, completion: ${outcome.completionTokens} (total: ${outcome.totalTokens})`,
183
- `* **Preview**: _"${outcome.preview}"_`,
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}"`,
184
197
  ].join('\n')
185
198
  }
186
- return `❌ **Smoke Test Failed**: ${outcome.error} (HTTP ${outcome.status || 'timeout'})${failoverNotice}`
199
+ return `Smoke Test Failed: ${outcome.error} (HTTP ${outcome.status || 'timeout'})${failoverNotice}`
187
200
  }
188
201
 
189
- // 7. Subcommand /cline quota or /cline balance
202
+ // 8. Subcommand /cline quota or /cline balance
190
203
  const activeKey = await resolveActiveAccountKey(ctx, live())
191
204
  if (!activeKey.value) {
192
- return '⚠️ **ClineBot**: API key is not configured. Open **Settings → Plugins → ClineBot**.'
205
+ return 'ClineBot: API key is not configured. Open Settings -> Plugins -> ClineBot.'
193
206
  }
194
207
 
195
208
  const [health, usage] = await Promise.all([
@@ -199,40 +212,91 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
199
212
 
200
213
  const fiveHour = usage?.windows?.fiveHour
201
214
  const weekly = usage?.windows?.weekly
202
- const reset5h = fiveHour?.resetsAt ? new Date(fiveHour.resetsAt).toLocaleTimeString() : 'N/A'
203
- 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'
204
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, ' ')
205
230
  const lines = [
206
- `### 🤖 ClinePass Status (${usage?.plan || 'ClinePass'})`,
207
- `* **Host Ping**: ${health.ok ? `✅ ${health.latencyMs} ms` : '❌ Unreachable'}`,
208
- `* **Active Key**: \`${activeKey.envName}\` (${activeKey.source})`,
209
- `* **Default Model**: \`${pub.defaultModel}\``,
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'}`,
210
237
  ]
238
+ if (usage?.user?.email) {
239
+ lines.splice(3, 0, `${pad('Account')}${usage.user.email}`)
240
+ }
211
241
  if (pub.defaultModelWarning) {
212
- lines.push(`* ⚠️ **Warning**: ${pub.defaultModelWarning}`)
242
+ lines.push(`${pad('Warning')}${pub.defaultModelWarning}`)
213
243
  }
244
+
214
245
  lines.push(
215
246
  '',
216
- `**⏱ 5-Hour Rolling Limit**: ${formatProgressBar(fiveHour?.percentUsed)} (reset: ${reset5h})`,
217
- `**📅 Weekly Window**: ${formatProgressBar(weekly?.percentUsed)} (reset: ${resetWk})`,
247
+ `${pad('5-Hour Quota')}${formatProgressBar(fiveHour?.percentUsed)} (resets: ${reset5h})`,
248
+ `${pad('Weekly Quota')}${formatProgressBar(weekly?.percentUsed)} (resets: ${resetWkFull})`,
218
249
  )
219
250
 
220
251
  if (fiveHour?.percentUsed >= 95) {
221
- lines.push('', '🚨 **CRITICAL LIMIT**: 5-hour quota is 95%+ consumed! New requests may be throttled until reset.')
252
+ lines.push('', 'CRITICAL: 5-hour quota is 95%+ consumed! New requests may be throttled until reset.')
222
253
  } else if (fiveHour?.percentUsed >= 80) {
223
- lines.push('', `⚠️ **Warning**: 5-hour quota is ${fiveHour.percentUsed}% consumed.`)
254
+ lines.push('', `Warning: 5-hour quota is ${fiveHour.percentUsed}% consumed.`)
224
255
  }
225
256
 
226
257
  if (sessionStats.totalRequests > 0) {
227
- lines.push('', `**📊 Session Telemetry**: ${sessionStats.successfulRequests}/${sessionStats.totalRequests} successful requests, ~${sessionStats.totalTokensEst} total tokens`)
258
+ lines.push('', `Session Stats: ${sessionStats.successfulRequests}/${sessionStats.totalRequests} successful requests, ~${sessionStats.totalTokensEst} total tokens`)
228
259
  }
229
260
 
230
- if (usage?.user?.email) {
231
- lines.push(`* **Account**: \`${usage.user.email}\``)
261
+ return lines.join('\n')
262
+ } catch (err) {
263
+ return `ClineBot error: ${String(err?.message || err)}`
264
+ }
265
+ }
266
+
267
+ const commandHandler = async (invocation) => {
268
+ const rawArgs = typeof invocation === 'string'
269
+ ? invocation
270
+ : (invocation?.rawInput || invocation?.input || invocation?.text || invocation?.args || '')
271
+ try {
272
+ const output = await executeCommand(rawArgs, invocation)
273
+ return {
274
+ kind: 'success',
275
+ text: output,
276
+ // Support toString() for callers that treat result directly as string
277
+ toString() { return output },
278
+ }
279
+ } catch (err) {
280
+ return {
281
+ kind: 'error',
282
+ text: `ClineBot error: ${String(err?.message || err)}`,
283
+ toString() { return `ClineBot error: ${String(err?.message || err)}` },
232
284
  }
285
+ }
286
+ }
233
287
 
234
- return lines.join('\n')
235
- },
288
+ const commandExecute = async (rawArgs, invocation = null) => {
289
+ const result = await commandHandler(invocation || rawArgs)
290
+ return result.text
291
+ }
292
+
293
+ const unregister = commands.register({
294
+ definitionId: '@goodandready/dsh-clinebot',
295
+ name: 'cline',
296
+ description: 'Check ClinePass subscription quota, models, accounts and test connectivity (/cline [quota|models|accounts|switch <name>|test [model]|ping|rotate])',
297
+ input: { hint: '[quota|models|accounts|switch <name>|test [model]|ping|rotate]' },
298
+ handler: commandHandler,
299
+ execute: commandExecute,
236
300
  })
237
301
 
238
302
  ctx.effect(() => () => unregister?.(), 'dsh-clinebot: slash-command')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.4.0",
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",