@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.
- package/CHANGELOG.md +22 -0
- package/lib/client.js +415 -137
- package/lib/cline-client.js +20 -1
- package/lib/index.js +1 -3
- package/lib/models.js +10 -3
- package/lib/provider-sync.js +56 -20
- package/lib/routes/accounts.js +170 -2
- package/lib/routes/auth.js +15 -1
- package/lib/routes/models.js +1 -1
- package/lib/routes/settings.js +35 -3
- package/lib/slash-command.js +128 -64
- package/package.json +1 -1
package/lib/slash-command.js
CHANGED
|
@@ -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
|
|
30
|
-
|
|
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
|
-
?
|
|
43
|
-
: '
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
summary,
|
|
46
|
+
'',
|
|
47
|
+
`Plan Sync: ${syncStatus}`,
|
|
48
|
+
`Active Models: ${activeCount} of ${allModels.length}`,
|
|
48
49
|
]
|
|
49
50
|
if (pub.defaultModelWarning) {
|
|
50
|
-
lines.push(
|
|
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) ? '
|
|
56
|
-
const efforts = Array.isArray(m.reasoningEfforts) ?
|
|
57
|
-
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}`)
|
|
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
|
-
|
|
67
|
-
|
|
72
|
+
summary,
|
|
73
|
+
'',
|
|
74
|
+
`Active Account: ${activeName}`,
|
|
68
75
|
'',
|
|
69
76
|
]
|
|
70
77
|
for (const acc of pool) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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 ? ` ·
|
|
76
|
-
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}`)
|
|
77
85
|
}
|
|
78
86
|
const lastRot = getLastRotation()
|
|
79
87
|
if (lastRot) {
|
|
80
|
-
lines.push('',
|
|
88
|
+
lines.push('', `Last Failover: ${lastRot.from} -> ${lastRot.to} (${lastRot.reason})`)
|
|
81
89
|
}
|
|
82
|
-
lines.push('', 'Switch active account:
|
|
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 '
|
|
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
|
|
106
|
+
return `Active account switched to ${param}`
|
|
99
107
|
}
|
|
100
|
-
return
|
|
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
|
|
116
|
+
return `Account rotated: switched from ${res.previousAccount} to ${res.activeAccount}. DSH provider updated.`
|
|
109
117
|
}
|
|
110
|
-
return
|
|
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
|
|
125
|
+
return `Cline API Pong: ${pub.baseUrl} is reachable (latency: ${health.latencyMs} ms, HTTP ${health.status})`
|
|
118
126
|
}
|
|
119
|
-
return
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
142
|
+
lines.push(`Last Error: ${sessionStats.lastError}`)
|
|
133
143
|
}
|
|
134
144
|
if (sessionStats.byModel && Object.keys(sessionStats.byModel).length) {
|
|
135
|
-
lines.push('', '
|
|
145
|
+
lines.push('', 'By Model:')
|
|
136
146
|
for (const [mId, mStats] of Object.entries(sessionStats.byModel)) {
|
|
137
|
-
lines.push(
|
|
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('',
|
|
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
|
|
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 '
|
|
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 = `\
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
199
|
+
return `Smoke Test Failed: ${outcome.error} (HTTP ${outcome.status || 'timeout'})${failoverNotice}`
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
//
|
|
202
|
+
// 8. Subcommand /cline quota or /cline balance
|
|
190
203
|
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
191
204
|
if (!activeKey.value) {
|
|
192
|
-
return '
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
242
|
+
lines.push(`${pad('Warning')}${pub.defaultModelWarning}`)
|
|
213
243
|
}
|
|
244
|
+
|
|
214
245
|
lines.push(
|
|
215
246
|
'',
|
|
216
|
-
|
|
217
|
-
|
|
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('', '
|
|
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('',
|
|
254
|
+
lines.push('', `Warning: 5-hour quota is ${fiveHour.percentUsed}% consumed.`)
|
|
224
255
|
}
|
|
225
256
|
|
|
226
257
|
if (sessionStats.totalRequests > 0) {
|
|
227
|
-
lines.push('',
|
|
258
|
+
lines.push('', `Session Stats: ${sessionStats.successfulRequests}/${sessionStats.totalRequests} successful requests, ~${sessionStats.totalTokensEst} total tokens`)
|
|
228
259
|
}
|
|
229
260
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|