@goodandready/dsh-clinebot 0.2.0

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/lib/index.js ADDED
@@ -0,0 +1,573 @@
1
+ import z from '@deepseek-ai/schemastery'
2
+ import { credentialRef } from '@deepseek-ai/dsh-credentials'
3
+ import { writeJson, readBody, isTrustedSettingsRequest } from './http.js'
4
+ import {
5
+ CLINE_MODELS,
6
+ DEFAULT_MODEL_ID,
7
+ PROVIDER_ID,
8
+ PROVIDER_DISPLAY_NAME,
9
+ getAllModels,
10
+ getDefaultModelIds,
11
+ validateCustomModel,
12
+ } from './models.js'
13
+ import {
14
+ DEFAULT_BASE_URL,
15
+ DEFAULT_API_KEY_ENV,
16
+ DEFAULT_TIMEOUT_MS,
17
+ DEFAULT_SMOKE_TIMEOUT_MS,
18
+ normalizeBaseUrl,
19
+ resolveApiKey,
20
+ saveCredentialKey,
21
+ fetchUsageLimits,
22
+ probeHealth,
23
+ smokeChat,
24
+ buildPiAiProvider,
25
+ } from './cline-client.js'
26
+
27
+ export const name = '@goodandready/dsh-clinebot'
28
+ export const inject = ['settings', 'webServer', 'credentials']
29
+
30
+ export const NS = 'dsh-clinebot'
31
+ export const LLM_PI_AI_NS = 'llm-pi-ai'
32
+
33
+ export const Config = z.object({
34
+ enabled: z.boolean().default(true)
35
+ .description('When true, ClineBot is registered as a model provider in DSH.'),
36
+ baseUrl: z.string().default(DEFAULT_BASE_URL)
37
+ .description('Base API URL (default: https://api.cline.bot/api/v1).'),
38
+ apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV)
39
+ .description('Credential / env name containing the ClinePass API key (never store key directly here).'),
40
+ defaultModel: z.string().default(DEFAULT_MODEL_ID)
41
+ .description('Default model ID for chat and smoke tests.'),
42
+ enabledModels: z.array(z.string()).default(getDefaultModelIds())
43
+ .description('List of model IDs enabled for selection in DSH.'),
44
+ customModels: z.array(z.object({
45
+ id: z.string(),
46
+ name: z.string(),
47
+ description: z.string().default(''),
48
+ contextLength: z.number().default(200000),
49
+ maxTokens: z.number().default(8192),
50
+ input: z.array(z.string()).default(['text']),
51
+ category: z.string().default('general'),
52
+ isCustom: z.boolean().default(true),
53
+ })).default([])
54
+ .description('User-added custom models from the ClinePass subscription.'),
55
+ timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
56
+ .description('HTTP probe timeout in milliseconds.'),
57
+ smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
58
+ .description('Timeout for smoke chat completions in milliseconds.'),
59
+ })
60
+
61
+ function publicConfig(cfg) {
62
+ const custom = Array.isArray(cfg?.customModels) ? cfg.customModels : []
63
+ const allDefaultIds = getDefaultModelIds(custom)
64
+ return {
65
+ enabled: !!cfg?.enabled,
66
+ baseUrl: normalizeBaseUrl(cfg?.baseUrl),
67
+ apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
68
+ defaultModel: cfg?.defaultModel || DEFAULT_MODEL_ID,
69
+ customModels: custom,
70
+ enabledModels: Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length
71
+ ? cfg.enabledModels
72
+ : allDefaultIds,
73
+ timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
74
+ smokeTimeoutMs: Number(cfg?.smokeTimeoutMs) || DEFAULT_SMOKE_TIMEOUT_MS,
75
+ }
76
+ }
77
+
78
+ async function resolveKeyValue(ctx, apiKeyEnv) {
79
+ const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
80
+ if (ctx?.credentials && typeof ctx.credentials.resolve === 'function') {
81
+ try {
82
+ const hit = await ctx.credentials.resolve(credentialRef(refName))
83
+ if (hit?.value) {
84
+ return { envName: refName, value: hit.value, source: 'credentials' }
85
+ }
86
+ } catch {
87
+ /* credentials service miss, fall through */
88
+ }
89
+ }
90
+
91
+ const fromEnv = resolveApiKey(refName)
92
+ if (fromEnv.value) {
93
+ return { ...fromEnv, source: 'env' }
94
+ }
95
+
96
+ return { envName: refName, value: '', source: 'none' }
97
+ }
98
+
99
+ async function checkRegisteredInPiAi(ctx) {
100
+ const settings = ctx?.get?.('settings')
101
+ if (!settings?.get) return false
102
+ try {
103
+ const piAi = settings.get(LLM_PI_AI_NS)
104
+ return !!piAi?.providers?.[PROVIDER_ID]
105
+ } catch {
106
+ return false
107
+ }
108
+ }
109
+
110
+ async function buildStatus(ctx, cfg) {
111
+ const pub = publicConfig(cfg)
112
+ const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
113
+ const health = await probeHealth(pub.baseUrl, { timeoutMs: pub.timeoutMs })
114
+ const isRegistered = await checkRegisteredInPiAi(ctx)
115
+ const allModels = getAllModels(pub.customModels)
116
+
117
+ let usage = null
118
+ if (key.value) {
119
+ usage = await fetchUsageLimits(pub.baseUrl, key.value, { timeoutMs: pub.timeoutMs }).catch(() => null)
120
+ }
121
+
122
+ return {
123
+ ok: true,
124
+ providerId: PROVIDER_ID,
125
+ displayName: PROVIDER_DISPLAY_NAME,
126
+ config: pub,
127
+ key: {
128
+ envName: key.envName,
129
+ present: !!key.value,
130
+ source: key.source,
131
+ },
132
+ health,
133
+ usage,
134
+ isRegistered,
135
+ availableModels: allModels,
136
+ }
137
+ }
138
+
139
+ async function upsertPiAiProvider(ctx, cfg, customModelIds) {
140
+ const settings = ctx?.get?.('settings')
141
+ if (!settings?.mutate) {
142
+ throw new Error('DSH settings service unavailable')
143
+ }
144
+
145
+ const pub = publicConfig(cfg)
146
+ const allModels = getAllModels(pub.customModels)
147
+ const selectedIds = new Set(customModelIds || pub.enabledModels)
148
+ const modelsToRegister = allModels.filter((m) => selectedIds.has(m.id))
149
+
150
+ const providerObj = buildPiAiProvider({
151
+ baseUrl: pub.baseUrl,
152
+ apiKeyEnv: pub.apiKeyEnv,
153
+ models: modelsToRegister.length ? modelsToRegister : allModels,
154
+ customModels: pub.customModels,
155
+ displayName: PROVIDER_DISPLAY_NAME,
156
+ })
157
+
158
+ await settings.mutate(LLM_PI_AI_NS, [
159
+ {
160
+ op: 'set',
161
+ path: ['providers', PROVIDER_ID],
162
+ value: providerObj,
163
+ },
164
+ ])
165
+
166
+ return providerObj
167
+ }
168
+
169
+ async function removePiAiProvider(ctx) {
170
+ const settings = ctx?.get?.('settings')
171
+ if (!settings?.mutate) {
172
+ throw new Error('DSH settings service unavailable')
173
+ }
174
+
175
+ await settings.mutate(LLM_PI_AI_NS, [
176
+ {
177
+ op: 'remove',
178
+ path: ['providers', PROVIDER_ID],
179
+ },
180
+ ])
181
+ return { ok: true }
182
+ }
183
+
184
+ function formatProgressBar(pct, totalWidth = 10) {
185
+ const clamped = Math.max(0, Math.min(100, pct || 0))
186
+ const filled = Math.round((clamped / 100) * totalWidth)
187
+ const empty = Math.max(0, totalWidth - filled)
188
+ return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${clamped}%`
189
+ }
190
+
191
+ export function apply(ctx, config) {
192
+ let liveCfg = Config(structuredClone(config || {}))
193
+ let settingsApi
194
+
195
+ const settingsService = ctx.get('settings')
196
+ if (typeof settingsService?.register === 'function') {
197
+ const scope = settingsService.register(NS, Config, { base: config })
198
+ settingsApi = scope
199
+ liveCfg = Config(scope.get() ?? config)
200
+ ctx.effect(() => scope.watch((next) => {
201
+ liveCfg = Config(next ?? config)
202
+ }), 'dsh-clinebot: settings')
203
+ }
204
+
205
+ const live = () => liveCfg
206
+
207
+ // Web server HTTP route handlers
208
+ if (ctx.webServer?.register) {
209
+ // 1. GET /dsh-clinebot/status
210
+ ctx.effect(() => ctx.webServer.register({
211
+ kind: 'exact',
212
+ path: '/dsh-clinebot/status',
213
+ handler: async (req, res) => {
214
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
215
+ try {
216
+ const st = await buildStatus(ctx, live())
217
+ writeJson(res, 200, st)
218
+ } catch (err) {
219
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
220
+ }
221
+ },
222
+ }), 'dsh-clinebot: /status')
223
+
224
+ // 2. GET & PUT /dsh-clinebot/config
225
+ ctx.effect(() => ctx.webServer.register({
226
+ kind: 'exact',
227
+ path: '/dsh-clinebot/config',
228
+ handler: async (req, res) => {
229
+ if (req.method === 'GET') {
230
+ return writeJson(res, 200, { ok: true, config: publicConfig(live()) })
231
+ }
232
+ if (req.method !== 'PUT') {
233
+ return writeJson(res, 405, { ok: false, error: 'GET or PUT' })
234
+ }
235
+ if (!isTrustedSettingsRequest(req)) {
236
+ return writeJson(res, 403, { ok: false, error: 'same-origin only' })
237
+ }
238
+ if (!settingsApi) {
239
+ return writeJson(res, 503, { ok: false, error: 'settings not ready' })
240
+ }
241
+ let payload
242
+ try {
243
+ payload = JSON.parse((await readBody(req)).toString('utf8') || '{}')
244
+ } catch {
245
+ return writeJson(res, 400, { ok: false, error: 'invalid json' })
246
+ }
247
+ if (payload && typeof payload.config === 'object') payload = payload.config
248
+ try {
249
+ const parsed = Config({ ...publicConfig(live()), ...payload })
250
+ await settingsApi.replace(parsed)
251
+ writeJson(res, 200, { ok: true, config: publicConfig(live()) })
252
+ } catch (e) {
253
+ writeJson(res, 400, { ok: false, error: String(e?.message || e) })
254
+ }
255
+ },
256
+ }), 'dsh-clinebot: /config')
257
+
258
+ // 3. POST /dsh-clinebot/save-key — direct saving into DSH credentials service
259
+ ctx.effect(() => ctx.webServer.register({
260
+ kind: 'exact',
261
+ path: '/dsh-clinebot/save-key',
262
+ handler: async (req, res) => {
263
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
264
+ if (!isTrustedSettingsRequest(req)) {
265
+ return writeJson(res, 403, { ok: false, error: 'Forbidden' })
266
+ }
267
+ try {
268
+ const bodyBuf = await readBody(req)
269
+ let body = {}
270
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
271
+
272
+ const apiKey = String(body.apiKey || '').trim()
273
+ if (!apiKey) {
274
+ return writeJson(res, 400, { ok: false, error: 'API key cannot be empty' })
275
+ }
276
+
277
+ const pub = publicConfig(live())
278
+ const targetEnvName = String(body.apiKeyEnv || pub.apiKeyEnv || DEFAULT_API_KEY_ENV).trim()
279
+ await saveCredentialKey(ctx, targetEnvName, apiKey)
280
+
281
+ // Run validation probe with the newly saved key
282
+ const validation = await smokeChat(pub.baseUrl, apiKey, {
283
+ model: pub.defaultModel,
284
+ timeoutMs: 15000,
285
+ })
286
+
287
+ writeJson(res, 200, {
288
+ ok: true,
289
+ envName: targetEnvName,
290
+ validated: validation.ok,
291
+ latencyMs: validation.latencyMs,
292
+ validationError: validation.ok ? null : validation.error,
293
+ })
294
+ } catch (err) {
295
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
296
+ }
297
+ },
298
+ }), 'dsh-clinebot: /save-key')
299
+
300
+ // 4. GET /dsh-clinebot/usage — direct fresh usage limit query
301
+ ctx.effect(() => ctx.webServer.register({
302
+ kind: 'exact',
303
+ path: '/dsh-clinebot/usage',
304
+ handler: async (req, res) => {
305
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
306
+ try {
307
+ const pub = publicConfig(live())
308
+ const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
309
+ if (!key.value) {
310
+ return writeJson(res, 400, { ok: false, error: 'API key not found' })
311
+ }
312
+ const usageData = await fetchUsageLimits(pub.baseUrl, key.value, {
313
+ timeoutMs: pub.timeoutMs,
314
+ bypassCache: true,
315
+ })
316
+ writeJson(res, usageData.ok ? 200 : 502, usageData)
317
+ } catch (err) {
318
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
319
+ }
320
+ },
321
+ }), 'dsh-clinebot: /usage')
322
+
323
+ // 5. POST /dsh-clinebot/register — upsert into DSH llm-pi-ai
324
+ ctx.effect(() => ctx.webServer.register({
325
+ kind: 'exact',
326
+ path: '/dsh-clinebot/register',
327
+ handler: async (req, res) => {
328
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
329
+ if (!isTrustedSettingsRequest(req)) {
330
+ return writeJson(res, 403, { ok: false, error: 'Forbidden' })
331
+ }
332
+ try {
333
+ const bodyBuf = await readBody(req)
334
+ let body = {}
335
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
336
+ const result = await upsertPiAiProvider(ctx, live(), body.models)
337
+ writeJson(res, 200, { ok: true, provider: result })
338
+ } catch (err) {
339
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
340
+ }
341
+ },
342
+ }), 'dsh-clinebot: /register')
343
+
344
+ // 6. POST /dsh-clinebot/unregister — remove from DSH
345
+ ctx.effect(() => ctx.webServer.register({
346
+ kind: 'exact',
347
+ path: '/dsh-clinebot/unregister',
348
+ handler: async (req, res) => {
349
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
350
+ if (!isTrustedSettingsRequest(req)) {
351
+ return writeJson(res, 403, { ok: false, error: 'Forbidden' })
352
+ }
353
+ try {
354
+ await removePiAiProvider(ctx)
355
+ writeJson(res, 200, { ok: true })
356
+ } catch (err) {
357
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
358
+ }
359
+ },
360
+ }), 'dsh-clinebot: /unregister')
361
+
362
+ // 7. POST /dsh-clinebot/smoke — live ping test
363
+ ctx.effect(() => ctx.webServer.register({
364
+ kind: 'exact',
365
+ path: '/dsh-clinebot/smoke',
366
+ handler: async (req, res) => {
367
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
368
+ if (!isTrustedSettingsRequest(req)) {
369
+ return writeJson(res, 403, { ok: false, error: 'Forbidden' })
370
+ }
371
+ try {
372
+ const bodyBuf = await readBody(req)
373
+ let body = {}
374
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
375
+
376
+ const pub = publicConfig(live())
377
+ const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
378
+ if (!key.value) {
379
+ return writeJson(res, 400, {
380
+ ok: false,
381
+ error: `API key not found. Ensure ${key.envName} is added to DSH credentials or environment.`,
382
+ })
383
+ }
384
+
385
+ const modelToTest = body.model || pub.defaultModel || DEFAULT_MODEL_ID
386
+ const outcome = await smokeChat(pub.baseUrl, key.value, {
387
+ model: modelToTest,
388
+ timeoutMs: pub.smokeTimeoutMs,
389
+ })
390
+ writeJson(res, outcome.ok ? 200 : 502, outcome)
391
+ } catch (err) {
392
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
393
+ }
394
+ },
395
+ }), 'dsh-clinebot: /smoke')
396
+
397
+ // 8. POST & DELETE /dsh-clinebot/models/custom — add/update or remove custom model
398
+ ctx.effect(() => ctx.webServer.register({
399
+ kind: 'exact',
400
+ path: '/dsh-clinebot/models/custom',
401
+ handler: async (req, res) => {
402
+ if (!isTrustedSettingsRequest(req)) {
403
+ return writeJson(res, 403, { ok: false, error: 'Forbidden' })
404
+ }
405
+ if (req.method === 'POST') {
406
+ try {
407
+ const bodyBuf = await readBody(req)
408
+ let body = {}
409
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
410
+
411
+ const validated = validateCustomModel(body)
412
+ if (!validated.ok) {
413
+ return writeJson(res, 400, { ok: false, error: validated.error })
414
+ }
415
+
416
+ const currentCustom = Array.isArray(live().customModels) ? [...live().customModels] : []
417
+ const existingIdx = currentCustom.findIndex((m) => m.id === validated.model.id)
418
+ if (existingIdx >= 0) {
419
+ currentCustom[existingIdx] = validated.model
420
+ } else {
421
+ currentCustom.push(validated.model)
422
+ }
423
+
424
+ const currentEnabled = new Set(live().enabledModels || getDefaultModelIds())
425
+ currentEnabled.add(validated.model.id)
426
+
427
+ if (settingsApi?.replace) {
428
+ const next = Config({
429
+ ...live(),
430
+ customModels: currentCustom,
431
+ enabledModels: Array.from(currentEnabled),
432
+ })
433
+ await settingsApi.replace(next)
434
+ if (await checkRegisteredInPiAi(ctx)) {
435
+ await upsertPiAiProvider(ctx, next, Array.from(currentEnabled))
436
+ }
437
+ }
438
+
439
+ return writeJson(res, 200, { ok: true, model: validated.model, customModels: currentCustom })
440
+ } catch (err) {
441
+ return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
442
+ }
443
+ }
444
+
445
+ if (req.method === 'DELETE') {
446
+ try {
447
+ const bodyBuf = await readBody(req)
448
+ let body = {}
449
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
450
+
451
+ const modelId = String(body.id || '').trim()
452
+ if (!modelId) {
453
+ return writeJson(res, 400, { ok: false, error: 'Missing model ID' })
454
+ }
455
+
456
+ const currentCustom = (live().customModels || []).filter((m) => m.id !== modelId)
457
+ const currentEnabled = (live().enabledModels || []).filter((id) => id !== modelId)
458
+
459
+ if (settingsApi?.replace) {
460
+ const next = Config({
461
+ ...live(),
462
+ customModels: currentCustom,
463
+ enabledModels: currentEnabled,
464
+ })
465
+ await settingsApi.replace(next)
466
+ if (await checkRegisteredInPiAi(ctx)) {
467
+ await upsertPiAiProvider(ctx, next, currentEnabled)
468
+ }
469
+ }
470
+
471
+ return writeJson(res, 200, { ok: true, removed: modelId, customModels: currentCustom })
472
+ } catch (err) {
473
+ return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
474
+ }
475
+ }
476
+
477
+ return writeJson(res, 405, { ok: false, error: 'POST or DELETE' })
478
+ },
479
+ }), 'dsh-clinebot: /models/custom')
480
+
481
+ // 9. POST /dsh-clinebot/models/toggle — toggle enabled status in picker
482
+ ctx.effect(() => ctx.webServer.register({
483
+ kind: 'exact',
484
+ path: '/dsh-clinebot/models/toggle',
485
+ handler: async (req, res) => {
486
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
487
+ if (!isTrustedSettingsRequest(req)) {
488
+ return writeJson(res, 403, { ok: false, error: 'Forbidden' })
489
+ }
490
+ try {
491
+ const bodyBuf = await readBody(req)
492
+ let body = {}
493
+ try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
494
+
495
+ if (Array.isArray(body.enabledModels) && settingsApi?.replace) {
496
+ const patch = { enabledModels: body.enabledModels }
497
+ if (body.defaultModel) patch.defaultModel = body.defaultModel
498
+ const next = Config({ ...live(), ...patch })
499
+ await settingsApi.replace(next)
500
+ if (await checkRegisteredInPiAi(ctx)) {
501
+ await upsertPiAiProvider(ctx, next, body.enabledModels)
502
+ }
503
+ }
504
+ writeJson(res, 200, { ok: true, enabledModels: body.enabledModels })
505
+ } catch (err) {
506
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
507
+ }
508
+ },
509
+ }), 'dsh-clinebot: /models/toggle')
510
+ }
511
+
512
+ // Register /cline chat slash-command if commands service is present
513
+ ctx.inject(['commands'], (cmdCtx) => {
514
+ const commands = cmdCtx.commands
515
+ if (typeof commands?.register !== 'function') return
516
+
517
+ const unregister = commands.register({
518
+ name: 'cline',
519
+ description: 'Check ClinePass subscription quota, rate limits and latency',
520
+ execute: async () => {
521
+ const pub = publicConfig(live())
522
+ const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
523
+ if (!key.value) {
524
+ return '⚠️ **ClineBot**: API-ключ не настроен. Откройте **Настройки → ClineBot** и сохраните ключ.'
525
+ }
526
+
527
+ const [health, usage] = await Promise.all([
528
+ probeHealth(pub.baseUrl, { timeoutMs: 5000 }),
529
+ fetchUsageLimits(pub.baseUrl, key.value, { timeoutMs: 8000 }),
530
+ ])
531
+
532
+ const fiveHour = usage?.windows?.fiveHour
533
+ const weekly = usage?.windows?.weekly
534
+ const reset5h = fiveHour?.resetsAt ? new Date(fiveHour.resetsAt).toLocaleTimeString() : 'н/д'
535
+ const resetWk = weekly?.resetsAt ? new Date(weekly.resetsAt).toLocaleDateString() : 'н/д'
536
+
537
+ const lines = [
538
+ `### 🤖 ClinePass Status (${usage?.plan || 'ClinePass'})`,
539
+ `* **Пинг хоста**: ${health.ok ? `✅ ${health.latencyMs} мс` : '❌ Недоступен'}`,
540
+ `* **Активный ключ**: ${key.envName} (${key.source})`,
541
+ `* **Модель по умолчанию**: \`${pub.defaultModel}\``,
542
+ '',
543
+ `**⏱ 5-часовое окно**: ${formatProgressBar(fiveHour?.percentUsed)} (сброс: ${reset5h})`,
544
+ `**📅 Недельное окно**: ${formatProgressBar(weekly?.percentUsed)} (сброс: ${resetWk})`,
545
+ ]
546
+
547
+ if (usage?.user?.email) {
548
+ lines.push(`* **Аккаунт**: \`${usage.user.email}\``)
549
+ }
550
+
551
+ return lines.join('\n')
552
+ },
553
+ })
554
+
555
+ ctx.effect(() => () => unregister?.(), 'dsh-clinebot: slash-command')
556
+ })
557
+
558
+ return {
559
+ getStatus: () => buildStatus(ctx, live()),
560
+ registerProvider: (models) => upsertPiAiProvider(ctx, live(), models),
561
+ unregisterProvider: () => removePiAiProvider(ctx),
562
+ getUsageLimits: async () => {
563
+ const pub = publicConfig(live())
564
+ const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
565
+ return fetchUsageLimits(pub.baseUrl, key.value)
566
+ },
567
+ runSmokeTest: async (model) => {
568
+ const pub = publicConfig(live())
569
+ const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
570
+ return smokeChat(pub.baseUrl, key.value, { model: model || pub.defaultModel })
571
+ },
572
+ }
573
+ }