@goodandready/dsh-clinebot 0.5.33 → 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.
@@ -234,7 +234,8 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
234
234
  timeoutMs: pub.timeoutMs,
235
235
  bypassCache: true,
236
236
  })
237
- writeJson(res, usageData.ok ? 200 : 502, publicUsage(usageData))
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
  }
@@ -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
- const csv = exportStatsAsCsv()
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
  }
@@ -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
+ }