@goodandready/dsh-clinebot 0.5.32 → 0.6.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/CHANGELOG.md +31 -0
- package/README.md +63 -36
- package/README.ru.md +3 -1
- package/README.zh.md +3 -1
- package/lib/adapter-messages.js +237 -0
- package/lib/adapter-stream.js +292 -0
- package/lib/adapter.js +616 -0
- package/lib/client.js +2564 -924
- package/lib/cline-client.js +15 -6
- package/lib/config.js +266 -233
- package/lib/credential-refs.js +104 -84
- package/lib/index.js +569 -502
- package/lib/model-prices.js +117 -0
- package/lib/provider-sync.js +404 -387
- package/lib/proxy-token.js +187 -140
- package/lib/routes/accounts.js +523 -403
- package/lib/routes/models.js +2 -1
- package/lib/routes/proxy.js +8 -27
- package/lib/routes/settings.js +63 -2
- package/lib/services.js +55 -0
- package/lib/stats-storage.js +364 -166
- package/lib/transport/cline-transport.js +314 -0
- package/lib/tui-settings.js +158 -0
- package/lib/updater.js +5 -1
- package/package.json +113 -77
package/lib/routes/models.js
CHANGED
|
@@ -234,7 +234,8 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
234
234
|
timeoutMs: pub.timeoutMs,
|
|
235
235
|
bypassCache: true,
|
|
236
236
|
})
|
|
237
|
-
|
|
237
|
+
const accountLabel = activeKey.label || activeKey.apiKeyEnv || activeKey.id || ''
|
|
238
|
+
writeJson(res, usageData.ok ? 200 : 502, { ...(publicUsage(usageData) || {}), account: accountLabel })
|
|
238
239
|
} catch (err) {
|
|
239
240
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
240
241
|
}
|
package/lib/routes/proxy.js
CHANGED
|
@@ -15,19 +15,19 @@
|
|
|
15
15
|
* 7. Streaming SSE token usage interception & persistent telemetry recording.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { writeJson, readBody
|
|
18
|
+
import { writeJson, readBody } from '../http.js'
|
|
19
19
|
import { publicConfig, plainConfig } from '../config.js'
|
|
20
|
-
import { resolveAccountPool } from '../account-pool.js'
|
|
21
20
|
import { resolveSessionAccount, markAccountCooldown } from '../session-router.js'
|
|
22
21
|
import { recordUsage } from '../stats-storage.js'
|
|
23
22
|
import { getAllModels, DEFAULT_MODEL_ID } from '../models.js'
|
|
24
23
|
import { optimizePromptCacheAndBudget } from '../multimodal-budget.js'
|
|
25
24
|
import {
|
|
26
25
|
getLocalProxyToken, hasDisallowedBrowserOrigin, isAuthorizedProxyRequest,
|
|
26
|
+
assertAuthorizedProxyRequest,
|
|
27
27
|
USER_AGENT, sendProxyTerminalError, extractUsageMetrics
|
|
28
28
|
} from '../proxy-token.js'
|
|
29
29
|
|
|
30
|
-
export { hasDisallowedBrowserOrigin, isAuthorizedProxyRequest, USER_AGENT }
|
|
30
|
+
export { hasDisallowedBrowserOrigin, isAuthorizedProxyRequest, assertAuthorizedProxyRequest, USER_AGENT }
|
|
31
31
|
|
|
32
32
|
export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
33
33
|
return async (req, res) => {
|
|
@@ -35,18 +35,9 @@ export function createChatCompletionsHandler(ctx, { live, getSettingsApi, syncPr
|
|
|
35
35
|
return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (hasDisallowedBrowserOrigin(req)) {
|
|
43
|
-
return writeJson(res, 403, { ok: false, error: 'Cross-site or browser requests to proxy are forbidden' })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const pool = await resolveAccountPool(ctx, live())
|
|
47
|
-
if (!isAuthorizedProxyRequest(req, pool)) {
|
|
48
|
-
return writeJson(res, 401, { ok: false, error: 'Unauthorized: missing or invalid bearer token' })
|
|
49
|
-
}
|
|
38
|
+
const auth = await assertAuthorizedProxyRequest(ctx, req, res, live)
|
|
39
|
+
if (!auth.ok) return
|
|
40
|
+
const pool = auth.pool
|
|
50
41
|
|
|
51
42
|
let bodyBuf
|
|
52
43
|
try {
|
|
@@ -543,18 +534,8 @@ export function createModelsHandler(ctx, { live }) {
|
|
|
543
534
|
return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
544
535
|
}
|
|
545
536
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (hasDisallowedBrowserOrigin(req)) {
|
|
551
|
-
return writeJson(res, 403, { ok: false, error: 'Cross-site or browser requests to proxy are forbidden' })
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const pool = await resolveAccountPool(ctx, live())
|
|
555
|
-
if (!isAuthorizedProxyRequest(req, pool)) {
|
|
556
|
-
return writeJson(res, 401, { ok: false, error: 'Unauthorized: missing or invalid bearer token' })
|
|
557
|
-
}
|
|
537
|
+
const auth = await assertAuthorizedProxyRequest(ctx, req, res, live)
|
|
538
|
+
if (!auth.ok) return
|
|
558
539
|
|
|
559
540
|
const pub = publicConfig(live())
|
|
560
541
|
const dynamic = Array.isArray(pub.dynamicModels) ? pub.dynamicModels : []
|
package/lib/routes/settings.js
CHANGED
|
@@ -3,7 +3,8 @@ import { assertTrustedSettingsRequest } from '../access.js'
|
|
|
3
3
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { buildStatus } from '../provider-sync.js'
|
|
5
5
|
import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
|
|
6
|
-
import { getStatsSummary, resetStats, exportStatsAsCsv } from '../stats-storage.js'
|
|
6
|
+
import { getStatsSummary, resetStats, exportStatsAsCsv, getAnalytics } from '../stats-storage.js'
|
|
7
|
+
import { priceTable } from '../model-prices.js'
|
|
7
8
|
|
|
8
9
|
export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProviderState, triggerAutoDiscover }) {
|
|
9
10
|
// 1. GET /dsh-clinebot/status
|
|
@@ -221,7 +222,13 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
221
222
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
222
223
|
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
223
224
|
try {
|
|
224
|
-
|
|
225
|
+
let detailed = false
|
|
226
|
+
try {
|
|
227
|
+
detailed = new URL(req.url, 'http://127.0.0.1').searchParams.get('detailed') === '1'
|
|
228
|
+
} catch {
|
|
229
|
+
detailed = false
|
|
230
|
+
}
|
|
231
|
+
const csv = exportStatsAsCsv(detailed)
|
|
225
232
|
res.statusCode = 200
|
|
226
233
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8')
|
|
227
234
|
res.setHeader('Content-Disposition', 'attachment; filename="clinebot-stats.csv"')
|
|
@@ -231,4 +238,58 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
231
238
|
}
|
|
232
239
|
},
|
|
233
240
|
}), 'dsh-clinebot: /stats/export')
|
|
241
|
+
|
|
242
|
+
// 7. GET /dsh-clinebot/analytics — aggregated session/day/model/account report
|
|
243
|
+
ctx.effect(() => ctx.webServer.register({
|
|
244
|
+
kind: 'exact',
|
|
245
|
+
path: '/dsh-clinebot/analytics',
|
|
246
|
+
handler: async (req, res) => {
|
|
247
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
248
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
249
|
+
try {
|
|
250
|
+
let query = {}
|
|
251
|
+
try {
|
|
252
|
+
query = Object.fromEntries(new URL(req.url, 'http://127.0.0.1').searchParams.entries())
|
|
253
|
+
} catch {}
|
|
254
|
+
const parseTime = (value) => {
|
|
255
|
+
if (value === undefined || value === '') return undefined
|
|
256
|
+
const numeric = Number(value)
|
|
257
|
+
return Number.isFinite(numeric) ? numeric : Date.parse(value)
|
|
258
|
+
}
|
|
259
|
+
const analytics = getAnalytics({
|
|
260
|
+
from: parseTime(query.from),
|
|
261
|
+
to: parseTime(query.to),
|
|
262
|
+
groupBy: query.groupBy,
|
|
263
|
+
limit: query.limit,
|
|
264
|
+
model: query.model,
|
|
265
|
+
account: query.account,
|
|
266
|
+
heatmapDays: query.heatmapDays,
|
|
267
|
+
heatmapModel: query.heatmapModel,
|
|
268
|
+
})
|
|
269
|
+
writeJson(res, 200, { ok: true, ...analytics })
|
|
270
|
+
} catch (err) {
|
|
271
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
}), 'dsh-clinebot: /analytics')
|
|
275
|
+
|
|
276
|
+
// 8. GET /dsh-clinebot/prices — local cost-estimate table
|
|
277
|
+
ctx.effect(() => ctx.webServer.register({
|
|
278
|
+
kind: 'exact',
|
|
279
|
+
path: '/dsh-clinebot/prices',
|
|
280
|
+
handler: async (req, res) => {
|
|
281
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
282
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
283
|
+
try {
|
|
284
|
+
const pub = publicConfig(live())
|
|
285
|
+
writeJson(res, 200, {
|
|
286
|
+
ok: true,
|
|
287
|
+
...priceTable(pub.priceOverrides || {}, pub.enabledModels || []),
|
|
288
|
+
updatedAt: new Date().toISOString(),
|
|
289
|
+
})
|
|
290
|
+
} catch (err) {
|
|
291
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
}), 'dsh-clinebot: /prices')
|
|
234
295
|
}
|
package/lib/services.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional-service helpers and one-time legacy lifecycle migration.
|
|
3
|
+
*
|
|
4
|
+
* The plugin no longer hard-injects `webServer`/`credentials`, so every read
|
|
5
|
+
* must survive Cordis's strict-proxy guard and a headless profile that mounts
|
|
6
|
+
* neither service.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LLM_PI_AI_NS } from './config.js'
|
|
10
|
+
import { deleteCredentialKey } from './credential-refs.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read an optional service without tripping Cordis's strict-proxy guard.
|
|
14
|
+
* @param {object | undefined} ctx
|
|
15
|
+
* @param {string} name
|
|
16
|
+
* @returns {unknown}
|
|
17
|
+
*/
|
|
18
|
+
export function service(ctx, name) {
|
|
19
|
+
try {
|
|
20
|
+
const viaGet = typeof ctx?.get === 'function' ? ctx.get(name) : undefined
|
|
21
|
+
if (viaGet) return viaGet
|
|
22
|
+
} catch {
|
|
23
|
+
/* strict context refuses get() for a service the fiber does not inject */
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return ctx ? ctx[name] : undefined
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove the legacy `llm-pi-ai.providers.clinebot` entry and its local proxy
|
|
34
|
+
* token once the native adapter owns the route. Idempotent and best-effort:
|
|
35
|
+
* a failed cleanup is logged, never fatal.
|
|
36
|
+
* @param {object} ctx
|
|
37
|
+
*/
|
|
38
|
+
export async function migrateLegacyPiAiProvider(ctx) {
|
|
39
|
+
try {
|
|
40
|
+
const svc = service(ctx, 'settings')
|
|
41
|
+
if (!svc || typeof svc.describe !== 'function' || typeof svc.mutate !== 'function') return
|
|
42
|
+
const descriptor = svc.describe().find((entry) => entry?.ns === LLM_PI_AI_NS)
|
|
43
|
+
if (descriptor?.value?.providers?.clinebot) {
|
|
44
|
+
await svc.mutate(LLM_PI_AI_NS, [{ op: 'remove', path: ['providers', 'clinebot'] }])
|
|
45
|
+
ctx?.logger?.info?.('[dsh-clinebot] Removed legacy llm-pi-ai clinebot provider after adapter migration')
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Legacy llm-pi-ai provider cleanup skipped: ' + (error?.message || error))
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await deleteCredentialKey(ctx, 'CLINEBOT_LOCAL_PROXY_TOKEN')
|
|
52
|
+
} catch {
|
|
53
|
+
/* the credential may never have existed */
|
|
54
|
+
}
|
|
55
|
+
}
|