@goodandready/dsh-clinebot 0.6.1 → 0.6.3
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 +14 -0
- package/README.md +10 -14
- package/README.ru.md +6 -13
- package/README.zh.md +6 -13
- package/lib/adapter.js +6 -1
- package/lib/budget.js +37 -0
- package/lib/client.js +86 -142
- package/lib/cline-client.js +0 -102
- package/lib/config.js +6 -8
- package/lib/index.js +7 -201
- package/lib/provider-sync.js +2 -141
- package/lib/routes/accounts.js +1 -37
- package/lib/stats-storage.js +15 -0
- package/lib/transport/cline-transport.js +1 -1
- package/lib/tui-settings.js +24 -0
- package/package.json +1 -1
- package/lib/proxy-server.js +0 -138
- package/lib/proxy-token.js +0 -187
- package/lib/routes/proxy.js +0 -578
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Config, publicConfig, plainConfig, volatileConfig, isVolatileRef, NS
|
|
1
|
+
import { Config, publicConfig, plainConfig, volatileConfig, isVolatileRef, NS } from './config.js'
|
|
2
2
|
import { registerPluginUpdater } from './updater.js'
|
|
3
3
|
import {
|
|
4
4
|
sessionStats,
|
|
@@ -7,28 +7,20 @@ import {
|
|
|
7
7
|
clearUsageCache,
|
|
8
8
|
clearProbeCache,
|
|
9
9
|
} from './cline-client.js'
|
|
10
|
-
import {
|
|
11
|
-
rotateToNextAccount,
|
|
12
|
-
isQuotaExceededError,
|
|
13
|
-
} from './account-pool.js'
|
|
10
|
+
import { rotateToNextAccount } from './account-pool.js'
|
|
14
11
|
import { PROVIDER_ID } from './models.js'
|
|
15
12
|
import {
|
|
16
|
-
upsertPiAiProvider,
|
|
17
|
-
removePiAiProvider,
|
|
18
13
|
autoDiscoverPlanModels,
|
|
19
|
-
resolveActiveAccountKey,
|
|
20
14
|
} from './provider-sync.js'
|
|
21
15
|
import { registerSettingsRoutes } from './routes/settings.js'
|
|
22
16
|
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
23
17
|
import { registerModelsRoutes } from './routes/models.js'
|
|
24
18
|
import { registerAuthRoutes } from './routes/auth.js'
|
|
25
|
-
import { registerProxyRoutes } from './routes/proxy.js'
|
|
26
|
-
import { startLoopbackProxyServer, stopLoopbackProxyServer } from './proxy-server.js'
|
|
27
19
|
import { ClineBotAdapter } from './adapter.js'
|
|
28
20
|
import { registerSlashCommand } from './slash-command.js'
|
|
29
21
|
import { ClineBotSearchProvider, CLINEBOT_SEARCH_PROVIDER_ID } from './web-search.js'
|
|
30
22
|
import { resolveAccountPool } from './account-pool.js'
|
|
31
|
-
import {
|
|
23
|
+
import { loadStats, flushStats, resolvePath, switchStatsStorage } from './stats-storage.js'
|
|
32
24
|
import { service, migrateLegacyPiAiProvider } from './services.js'
|
|
33
25
|
import { applyTuiSettings } from './tui-settings.js'
|
|
34
26
|
|
|
@@ -39,7 +31,7 @@ export function configReader(config) {
|
|
|
39
31
|
return () => config
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
export { NS,
|
|
34
|
+
export { NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
43
35
|
|
|
44
36
|
export function apply(ctx, config) {
|
|
45
37
|
let currentConfig = config || {}
|
|
@@ -86,26 +78,8 @@ export function apply(ctx, config) {
|
|
|
86
78
|
const syncProviderState = async (cfg) => {
|
|
87
79
|
try {
|
|
88
80
|
const pub = publicConfig(cfg)
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
// llm-pi-ai mutation and no loopback proxy are involved.
|
|
92
|
-
if (adapterRegistration) {
|
|
93
|
-
adapterRegistration.replace(pub.enabled ? [PROVIDER_ID] : [])
|
|
94
|
-
}
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
if (!pub.enabled) {
|
|
98
|
-
await removePiAiProvider(ctx)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
if (pub.proxyMode !== false && (service(ctx, 'webServer') || typeof ctx?.get === 'function')) {
|
|
102
|
-
await startLoopbackProxyServer(ctx, routeEnv)
|
|
103
|
-
} else {
|
|
104
|
-
await stopLoopbackProxyServer()
|
|
105
|
-
}
|
|
106
|
-
const activeKey = await resolveActiveAccountKey(ctx, cfg)
|
|
107
|
-
if (activeKey.value) {
|
|
108
|
-
await upsertPiAiProvider(ctx, cfg, pub.enabledModels)
|
|
81
|
+
if (adapterRegistration) {
|
|
82
|
+
adapterRegistration.replace(pub.enabled ? [PROVIDER_ID] : [])
|
|
109
83
|
}
|
|
110
84
|
} catch {
|
|
111
85
|
/* ignore transient settings unavailable */
|
|
@@ -284,7 +258,6 @@ export function apply(ctx, config) {
|
|
|
284
258
|
}
|
|
285
259
|
|
|
286
260
|
const ensureAdapter = () => {
|
|
287
|
-
if (publicConfig(live()).transport === 'pi-ai') return
|
|
288
261
|
if (typeof ctx.inject !== 'function') return
|
|
289
262
|
ctx.inject(['llm'], (llmCtx) => {
|
|
290
263
|
const llm = llmCtx?.llm || llmCtx?.get?.('llm')
|
|
@@ -322,20 +295,12 @@ export function apply(ctx, config) {
|
|
|
322
295
|
ensureAdapter()
|
|
323
296
|
applyTuiSettings(ctx, { live })
|
|
324
297
|
|
|
325
|
-
if (typeof ctx.inject === 'function'
|
|
298
|
+
if (typeof ctx.inject === 'function') {
|
|
326
299
|
ctx.inject(['settings'], () => {
|
|
327
300
|
void migrateLegacyPiAiProvider(ctx)
|
|
328
301
|
})
|
|
329
302
|
}
|
|
330
303
|
|
|
331
|
-
if (typeof ctx.effect === 'function') {
|
|
332
|
-
ctx.effect(() => {
|
|
333
|
-
return () => {
|
|
334
|
-
stopLoopbackProxyServer()
|
|
335
|
-
}
|
|
336
|
-
}, 'dsh-clinebot: loopback-proxy-server')
|
|
337
|
-
}
|
|
338
|
-
|
|
339
304
|
syncProviderState(live())
|
|
340
305
|
if (typeof ctx.effect === 'function') {
|
|
341
306
|
ctx.effect(() => {
|
|
@@ -363,7 +328,6 @@ export function apply(ctx, config) {
|
|
|
363
328
|
registerAccountsRoutes(webCtx, routeEnv)
|
|
364
329
|
registerModelsRoutes(webCtx, routeEnv)
|
|
365
330
|
registerAuthRoutes(webCtx, routeEnv)
|
|
366
|
-
registerProxyRoutes(webCtx, routeEnv)
|
|
367
331
|
}
|
|
368
332
|
|
|
369
333
|
if (service(ctx, 'webServer')?.register) {
|
|
@@ -380,164 +344,6 @@ export function apply(ctx, config) {
|
|
|
380
344
|
syncProviderState,
|
|
381
345
|
})
|
|
382
346
|
|
|
383
|
-
let lastFailoverAt = 0
|
|
384
|
-
|
|
385
|
-
const handleStream429 = async () => {
|
|
386
|
-
const now = Date.now()
|
|
387
|
-
if (now - lastFailoverAt < 30000) {
|
|
388
|
-
ctx?.logger?.warn?.('[dsh-clinebot] Suppressing rapid failover rotation within 30s storm window')
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
lastFailoverAt = now
|
|
392
|
-
try {
|
|
393
|
-
const failover = await rotateToNextAccount(ctx, live(), 'stream_429', settingsApi)
|
|
394
|
-
if (failover?.rotated) {
|
|
395
|
-
await syncProviderState(live())
|
|
396
|
-
}
|
|
397
|
-
} catch (err) {
|
|
398
|
-
ctx?.logger?.warn?.('[dsh-clinebot] Auto-failover rotation failed: ' + (err?.message || err))
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (typeof ctx.on === 'function' && publicConfig(live()).transport === 'pi-ai') {
|
|
403
|
-
const unlisten = ctx.on('llm/stream', (options, next) => {
|
|
404
|
-
const isCline = options?.provider === PROVIDER_ID
|
|
405
|
-
const stream = next()
|
|
406
|
-
const startTime = Date.now()
|
|
407
|
-
let promptTokens = 0
|
|
408
|
-
let completionTokens = 0
|
|
409
|
-
let finished = false
|
|
410
|
-
|
|
411
|
-
return (async function* () {
|
|
412
|
-
try {
|
|
413
|
-
for await (const chunk of stream) {
|
|
414
|
-
if (isCline && chunk?.type === 'usage' && chunk?.usage) {
|
|
415
|
-
const u = chunk.usage
|
|
416
|
-
promptTokens += (Number(u.inputTokens) || 0) + (Number(u.cacheReadTokens) || 0) + (Number(u.cacheWriteTokens) || 0)
|
|
417
|
-
completionTokens += Number(u.outputTokens) || 0
|
|
418
|
-
}
|
|
419
|
-
if (isCline && chunk?.type === 'finish' && chunk?.reason) {
|
|
420
|
-
finished = true
|
|
421
|
-
const latencyMs = Date.now() - startTime
|
|
422
|
-
const reason = chunk.reason
|
|
423
|
-
const failure = reason.failure || {}
|
|
424
|
-
const isError = reason.kind === 'error'
|
|
425
|
-
const isAborted = reason.kind === 'aborted'
|
|
426
|
-
const is429 = failure.status === 429 || failure.code === 429 || failure.code === 'rate_limit_exceeded'
|
|
427
|
-
const isExceeded = isQuotaExceededError(`${failure.message || ''} ${failure.code || ''}`)
|
|
428
|
-
|
|
429
|
-
const isProxy = publicConfig(live()).proxyMode !== false
|
|
430
|
-
if (isError) {
|
|
431
|
-
recordSessionRequest({
|
|
432
|
-
latencyMs,
|
|
433
|
-
ok: false,
|
|
434
|
-
error: failure.message || 'Stream error',
|
|
435
|
-
promptTokens,
|
|
436
|
-
completionTokens,
|
|
437
|
-
model: options?.model,
|
|
438
|
-
})
|
|
439
|
-
if (!isProxy) {
|
|
440
|
-
recordUsage({
|
|
441
|
-
model: options?.model,
|
|
442
|
-
promptTokens,
|
|
443
|
-
completionTokens,
|
|
444
|
-
totalTokens: promptTokens + completionTokens,
|
|
445
|
-
isError: true,
|
|
446
|
-
is429,
|
|
447
|
-
statsPath: publicConfig(live()).statsPath,
|
|
448
|
-
})
|
|
449
|
-
}
|
|
450
|
-
if (is429 || isExceeded) {
|
|
451
|
-
handleStream429()
|
|
452
|
-
}
|
|
453
|
-
} else if (isAborted) {
|
|
454
|
-
recordSessionRequest({
|
|
455
|
-
latencyMs,
|
|
456
|
-
ok: true,
|
|
457
|
-
aborted: true,
|
|
458
|
-
promptTokens,
|
|
459
|
-
completionTokens,
|
|
460
|
-
model: options?.model,
|
|
461
|
-
})
|
|
462
|
-
} else {
|
|
463
|
-
recordSessionRequest({
|
|
464
|
-
latencyMs,
|
|
465
|
-
ok: true,
|
|
466
|
-
promptTokens,
|
|
467
|
-
completionTokens,
|
|
468
|
-
model: options?.model,
|
|
469
|
-
})
|
|
470
|
-
if (!isProxy) {
|
|
471
|
-
recordUsage({
|
|
472
|
-
model: options?.model,
|
|
473
|
-
promptTokens,
|
|
474
|
-
completionTokens,
|
|
475
|
-
totalTokens: promptTokens + completionTokens,
|
|
476
|
-
statsPath: publicConfig(live()).statsPath,
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
yield chunk
|
|
482
|
-
}
|
|
483
|
-
if (isCline && !finished) {
|
|
484
|
-
recordSessionRequest({
|
|
485
|
-
latencyMs: Date.now() - startTime,
|
|
486
|
-
ok: true,
|
|
487
|
-
promptTokens,
|
|
488
|
-
completionTokens,
|
|
489
|
-
model: options?.model,
|
|
490
|
-
})
|
|
491
|
-
const isProxy = publicConfig(live()).proxyMode !== false
|
|
492
|
-
if (!isProxy) {
|
|
493
|
-
recordUsage({
|
|
494
|
-
model: options?.model,
|
|
495
|
-
promptTokens,
|
|
496
|
-
completionTokens,
|
|
497
|
-
totalTokens: promptTokens + completionTokens,
|
|
498
|
-
statsPath: publicConfig(live()).statsPath,
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
} catch (err) {
|
|
503
|
-
if (isCline) {
|
|
504
|
-
const latencyMs = Date.now() - startTime
|
|
505
|
-
const status = err?.status || err?.statusCode
|
|
506
|
-
const msg = String(err?.message || err)
|
|
507
|
-
recordSessionRequest({
|
|
508
|
-
latencyMs,
|
|
509
|
-
ok: false,
|
|
510
|
-
error: msg,
|
|
511
|
-
promptTokens,
|
|
512
|
-
completionTokens,
|
|
513
|
-
model: options?.model,
|
|
514
|
-
})
|
|
515
|
-
const isProxy = publicConfig(live()).proxyMode !== false
|
|
516
|
-
if (!isProxy) {
|
|
517
|
-
recordUsage({
|
|
518
|
-
model: options?.model,
|
|
519
|
-
promptTokens,
|
|
520
|
-
completionTokens,
|
|
521
|
-
totalTokens: promptTokens + completionTokens,
|
|
522
|
-
isError: true,
|
|
523
|
-
is429: status === 429,
|
|
524
|
-
statsPath: publicConfig(live()).statsPath,
|
|
525
|
-
})
|
|
526
|
-
}
|
|
527
|
-
if (status === 429 || isQuotaExceededError(msg)) {
|
|
528
|
-
handleStream429()
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
throw err
|
|
532
|
-
}
|
|
533
|
-
})()
|
|
534
|
-
}, { global: true, prepend: true })
|
|
535
|
-
|
|
536
|
-
if (typeof ctx.effect === 'function') {
|
|
537
|
-
ctx.effect(() => () => unlisten?.(), 'dsh-clinebot: llm/stream failover')
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
347
|
// Native DSH WebSearchProvider over ctx.web (#233)
|
|
542
348
|
if (typeof ctx.inject === 'function') {
|
|
543
349
|
ctx.inject(['web'], (webCtx) => {
|
package/lib/provider-sync.js
CHANGED
|
@@ -15,17 +15,12 @@ import {
|
|
|
15
15
|
probeHealth,
|
|
16
16
|
fetchUsageLimits,
|
|
17
17
|
sessionStats,
|
|
18
|
-
usageCache,
|
|
19
|
-
buildPiAiProvider,
|
|
20
18
|
DEFAULT_API_KEY_ENV,
|
|
21
19
|
} from './cline-client.js'
|
|
22
|
-
import { publicConfig, plainConfig, Config
|
|
20
|
+
import { publicConfig, plainConfig, Config } from './config.js'
|
|
23
21
|
import { publicUsage } from './http.js'
|
|
24
22
|
import { resolveKeyValue } from './credential-refs.js'
|
|
25
23
|
import { resolveAccountPool, getLastRotation } from './account-pool.js'
|
|
26
|
-
import { getLocalProxyToken, LOCAL_PROXY_KEY_ENV } from './proxy-token.js'
|
|
27
|
-
import { saveCredentialKey } from './credential-refs.js'
|
|
28
|
-
import { getProxyPort, detectHostWebServerFence } from './proxy-server.js'
|
|
29
24
|
|
|
30
25
|
export function resolvePathWithHome(p) {
|
|
31
26
|
let guard = 0
|
|
@@ -48,28 +43,6 @@ export function resolvePathWithHome(p) {
|
|
|
48
43
|
return p
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
export async function checkRegisteredInPiAi(ctx) {
|
|
52
|
-
const settings = ctx?.get?.('settings')
|
|
53
|
-
try {
|
|
54
|
-
if (typeof settings?.describe === 'function') {
|
|
55
|
-
const descriptors = settings.describe()
|
|
56
|
-
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
57
|
-
if (piAiDesc?.value?.providers?.[PROVIDER_ID]) return true
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const llm = ctx?.get?.('llm')
|
|
61
|
-
if (typeof llm?.listProviders === 'function') {
|
|
62
|
-
const list = await llm.listProviders()
|
|
63
|
-
if (Array.isArray(list) && list.some((p) => (p?.id || p) === PROVIDER_ID)) {
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
46
|
export async function checkRegisteredInLlm(ctx) {
|
|
74
47
|
try {
|
|
75
48
|
const llm = ctx?.get?.('llm')
|
|
@@ -110,14 +83,12 @@ export async function buildStatus(ctx, cfg) {
|
|
|
110
83
|
const pub = publicConfig(cfg)
|
|
111
84
|
const probeTimeout = Math.min(2500, pub.timeoutMs || 2500)
|
|
112
85
|
|
|
113
|
-
const [key, pool,
|
|
86
|
+
const [key, pool, isRegistered, health] = await Promise.all([
|
|
114
87
|
resolveKeyValue(ctx, pub.apiKeyEnv),
|
|
115
88
|
resolveAccountPool(ctx, cfg),
|
|
116
|
-
checkRegisteredInPiAi(ctx),
|
|
117
89
|
checkRegisteredInLlm(ctx),
|
|
118
90
|
probeHealth(pub.baseUrl, { timeoutMs: probeTimeout }),
|
|
119
91
|
])
|
|
120
|
-
const isRegistered = isRegisteredInLlm || isRegisteredInPiAi
|
|
121
92
|
|
|
122
93
|
const activeAcc = await resolveActiveAccountKey(ctx, cfg, pool)
|
|
123
94
|
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
@@ -210,116 +181,6 @@ export async function buildStatus(ctx, cfg) {
|
|
|
210
181
|
}
|
|
211
182
|
}
|
|
212
183
|
|
|
213
|
-
export async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
|
|
214
|
-
const settings = ctx?.get?.('settings')
|
|
215
|
-
if (!settings?.mutate) {
|
|
216
|
-
throw new Error('DSH settings service unavailable')
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const pub = publicConfig(cfg)
|
|
220
|
-
const allModels = getAllModels(pub.dynamicModels, pub.modelContextOverrides, pub.customModels)
|
|
221
|
-
const allowedSet = new Set(activeModelIds || pub.enabledModels)
|
|
222
|
-
const modelsToRegister = allModels.filter((m) => allowedSet.has(m.id))
|
|
223
|
-
|
|
224
|
-
const activeAcc = await resolveActiveAccountKey(ctx, cfg)
|
|
225
|
-
|
|
226
|
-
let isProxy = pub.proxyMode !== false
|
|
227
|
-
let effectiveBaseUrl = pub.baseUrl
|
|
228
|
-
let proxyPort = getProxyPort()
|
|
229
|
-
|
|
230
|
-
if (isProxy) {
|
|
231
|
-
if (!proxyPort) {
|
|
232
|
-
proxyPort = ctx?.webServer?.port || ctx?.get?.('webServer')?.port || process.env.PORT || 3080
|
|
233
|
-
const isFenced = await detectHostWebServerFence(proxyPort)
|
|
234
|
-
if (isFenced) {
|
|
235
|
-
ctx?.logger?.warn?.(
|
|
236
|
-
`[dsh-clinebot] DesktopWebServer security fence detected on port ${proxyPort} and dedicated loopback proxy is not active. Falling back to direct upstream mode.`
|
|
237
|
-
)
|
|
238
|
-
isProxy = false
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (isProxy && proxyPort) {
|
|
244
|
-
effectiveBaseUrl = `http://127.0.0.1:${proxyPort}/dsh-clinebot/v1`
|
|
245
|
-
} else {
|
|
246
|
-
isProxy = false
|
|
247
|
-
effectiveBaseUrl = pub.baseUrl
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const localToken = isProxy ? getLocalProxyToken() : undefined
|
|
251
|
-
if (isProxy && localToken) {
|
|
252
|
-
try {
|
|
253
|
-
await saveCredentialKey(ctx, LOCAL_PROXY_KEY_ENV, localToken)
|
|
254
|
-
} catch {
|
|
255
|
-
/* credentials service might be absent/read-only in tests; process.env is set */
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
let existingModels = []
|
|
260
|
-
try {
|
|
261
|
-
if (typeof settings.describe === 'function') {
|
|
262
|
-
const descriptors = settings.describe()
|
|
263
|
-
const piAiDesc = Array.isArray(descriptors) ? descriptors.find((d) => d?.ns === LLM_PI_AI_NS) : null
|
|
264
|
-
const existingProv = piAiDesc?.value?.providers?.[PROVIDER_ID]
|
|
265
|
-
if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
|
|
266
|
-
} else if (typeof settings.get === 'function') {
|
|
267
|
-
const piAiVal = settings.get(LLM_PI_AI_NS)
|
|
268
|
-
const existingProv = piAiVal?.providers?.[PROVIDER_ID]
|
|
269
|
-
if (Array.isArray(existingProv?.models)) existingModels = existingProv.models
|
|
270
|
-
}
|
|
271
|
-
} catch {
|
|
272
|
-
/* ignore read error */
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const providerObj = buildPiAiProvider({
|
|
276
|
-
baseUrl: effectiveBaseUrl,
|
|
277
|
-
apiKeyEnv: isProxy ? LOCAL_PROXY_KEY_ENV : (activeAcc?.apiKeyEnv || pub.apiKeyEnv),
|
|
278
|
-
models: modelsToRegister.length ? modelsToRegister : allModels,
|
|
279
|
-
dynamicModels: pub.dynamicModels,
|
|
280
|
-
modelContextOverrides: pub.modelContextOverrides,
|
|
281
|
-
customModels: pub.customModels,
|
|
282
|
-
existingModels,
|
|
283
|
-
displayName: PROVIDER_DISPLAY_NAME,
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
await settings.mutate(LLM_PI_AI_NS, [
|
|
287
|
-
{
|
|
288
|
-
op: 'set',
|
|
289
|
-
path: ['providers', PROVIDER_ID],
|
|
290
|
-
value: providerObj,
|
|
291
|
-
},
|
|
292
|
-
])
|
|
293
|
-
|
|
294
|
-
ctx?.logger?.info?.(
|
|
295
|
-
`[dsh-clinebot] Synchronized llm-pi-ai provider "${PROVIDER_ID}" with ${providerObj.models.length} models.`
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
return providerObj
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export async function removePiAiProvider(ctx) {
|
|
302
|
-
const settings = ctx?.get?.('settings')
|
|
303
|
-
if (!settings?.mutate) {
|
|
304
|
-
throw new Error('DSH settings service unavailable')
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
await settings.mutate(LLM_PI_AI_NS, [
|
|
309
|
-
{
|
|
310
|
-
op: 'remove',
|
|
311
|
-
path: ['providers', PROVIDER_ID],
|
|
312
|
-
},
|
|
313
|
-
])
|
|
314
|
-
} catch (err) {
|
|
315
|
-
const msg = String(err?.message || err).toLowerCase()
|
|
316
|
-
if (!msg.includes('not found') && !msg.includes('absent') && !msg.includes('enoent') && !msg.includes('does not exist')) {
|
|
317
|
-
throw err
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return { ok: true }
|
|
321
|
-
}
|
|
322
|
-
|
|
323
184
|
export function formatProgressBar(pct, totalWidth = 10) {
|
|
324
185
|
const clamped = Math.max(0, Math.min(100, pct || 0))
|
|
325
186
|
const filled = Math.round((clamped / 100) * totalWidth)
|
package/lib/routes/accounts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { writeJson, readBody, publicUsage } from '../http.js'
|
|
|
2
2
|
import { discoverLocalClineKeys, consumeDiscoveredKey } from '../key-discovery.js'
|
|
3
3
|
import { assertTrustedSettingsRequest } from '../access.js'
|
|
4
4
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
5
|
-
import {
|
|
5
|
+
import { resolveActiveAccountKey } from '../provider-sync.js'
|
|
6
6
|
import { resolveAccountPool } from '../account-pool.js'
|
|
7
7
|
import {
|
|
8
8
|
clearUsageCache,
|
|
@@ -313,42 +313,6 @@ export function registerAccountsRoutes(ctx, { live, syncProviderState, getSettin
|
|
|
313
313
|
},
|
|
314
314
|
}), 'dsh-clinebot: /accounts/active')
|
|
315
315
|
|
|
316
|
-
// POST /dsh-clinebot/register — upsert into DSH llm-pi-ai
|
|
317
|
-
ctx.effect(() => ctx.webServer.register({
|
|
318
|
-
kind: 'exact',
|
|
319
|
-
path: '/dsh-clinebot/register',
|
|
320
|
-
handler: async (req, res) => {
|
|
321
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
322
|
-
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
323
|
-
try {
|
|
324
|
-
const bodyBuf = await readBody(req)
|
|
325
|
-
let body = {}
|
|
326
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
|
|
327
|
-
const activeModels = body.models || publicConfig(live()).enabledModels
|
|
328
|
-
const result = await upsertPiAiProvider(ctx, live(), activeModels)
|
|
329
|
-
writeJson(res, 200, { ok: true, provider: result })
|
|
330
|
-
} catch (err) {
|
|
331
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
}), 'dsh-clinebot: /register')
|
|
335
|
-
|
|
336
|
-
// POST /dsh-clinebot/unregister — remove from DSH
|
|
337
|
-
ctx.effect(() => ctx.webServer.register({
|
|
338
|
-
kind: 'exact',
|
|
339
|
-
path: '/dsh-clinebot/unregister',
|
|
340
|
-
handler: async (req, res) => {
|
|
341
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
342
|
-
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
343
|
-
try {
|
|
344
|
-
await removePiAiProvider(ctx)
|
|
345
|
-
writeJson(res, 200, { ok: true })
|
|
346
|
-
} catch (err) {
|
|
347
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
}), 'dsh-clinebot: /unregister')
|
|
351
|
-
|
|
352
316
|
// GET /dsh-clinebot/keys/discovered — scan local Cline CLI and VS Code extension keys (#232, #246)
|
|
353
317
|
ctx.effect(() => ctx.webServer.register({
|
|
354
318
|
kind: 'exact',
|
package/lib/stats-storage.js
CHANGED
|
@@ -361,6 +361,21 @@ export function getSessionStats(sessionId) {
|
|
|
361
361
|
return bucket ? JSON.parse(JSON.stringify(bucket)) : null
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Current UTC day/month estimated spend for budget guards.
|
|
366
|
+
* @param {number} [now]
|
|
367
|
+
*/
|
|
368
|
+
export function getBudgetSpend(now = Date.now()) {
|
|
369
|
+
const dayKey = new Date(now).toISOString().slice(0, 10)
|
|
370
|
+
const monthKey = dayKey.slice(0, 7)
|
|
371
|
+
return {
|
|
372
|
+
dayKey,
|
|
373
|
+
monthKey,
|
|
374
|
+
dayCost: Number(inMemoryStats.byDay?.[dayKey]?.costUsd || 0),
|
|
375
|
+
monthCost: Number(inMemoryStats.byMonth?.[monthKey]?.costUsd || 0),
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
364
379
|
export function resetStats(customPath) {
|
|
365
380
|
inMemoryStats = createEmptyStats()
|
|
366
381
|
saveStatsSync(customPath)
|
package/lib/tui-settings.js
CHANGED
|
@@ -114,6 +114,30 @@ export function buildTuiSection({ live }) {
|
|
|
114
114
|
return Number.isInteger(value) && value >= -1 ? { kind: 'set', value } : undefined
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
|
+
{
|
|
118
|
+
path: ['dailyBudgetUsd'],
|
|
119
|
+
group: 'advanced',
|
|
120
|
+
kind: 'number',
|
|
121
|
+
label: 'Daily budget (USD)',
|
|
122
|
+
hint: 'Estimated spend guard; 0 disables it.',
|
|
123
|
+
format: (value) => (Number(value) > 0 ? String(Number(value)) : ''),
|
|
124
|
+
parse: (text) => {
|
|
125
|
+
const value = Number(text)
|
|
126
|
+
return Number.isFinite(value) && value >= 0 ? { kind: 'set', value } : { kind: 'clear' }
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
path: ['monthlyBudgetUsd'],
|
|
131
|
+
group: 'advanced',
|
|
132
|
+
kind: 'number',
|
|
133
|
+
label: 'Monthly budget (USD)',
|
|
134
|
+
hint: 'Estimated spend guard; 0 disables it.',
|
|
135
|
+
format: (value) => (Number(value) > 0 ? String(Number(value)) : ''),
|
|
136
|
+
parse: (text) => {
|
|
137
|
+
const value = Number(text)
|
|
138
|
+
return Number.isFinite(value) && value >= 0 ? { kind: 'set', value } : { kind: 'clear' }
|
|
139
|
+
},
|
|
140
|
+
},
|
|
117
141
|
]
|
|
118
142
|
|
|
119
143
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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",
|