@goodandready/dsh-clinebot 0.5.33 → 0.6.1

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, getSessionStats, 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
@@ -196,6 +197,24 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
196
197
  },
197
198
  }), 'dsh-clinebot: /stats')
198
199
 
200
+ // 4b. GET /dsh-clinebot/stats/session — ClineBot-only aggregate for one DSH session
201
+ ctx.effect(() => ctx.webServer.register({
202
+ kind: 'exact',
203
+ path: '/dsh-clinebot/stats/session',
204
+ handler: async (req, res) => {
205
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
206
+ if (!assertTrustedSettingsRequest(req, res, ctx)) return
207
+ try {
208
+ const url = new URL(req.url || '/', 'http://localhost')
209
+ const sessionId = String(url.searchParams.get('sessionId') || '').trim().slice(0, 200)
210
+ if (!sessionId) return writeJson(res, 400, { ok: false, error: 'sessionId required' })
211
+ writeJson(res, 200, { ok: true, session: getSessionStats(sessionId) })
212
+ } catch (err) {
213
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
214
+ }
215
+ },
216
+ }), 'dsh-clinebot: /stats/session')
217
+
199
218
  // 5. POST /dsh-clinebot/stats/reset — reset persistent stats (Issue #129)
200
219
  ctx.effect(() => ctx.webServer.register({
201
220
  kind: 'exact',
@@ -221,7 +240,13 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
221
240
  if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
222
241
  if (!assertTrustedSettingsRequest(req, res, ctx)) return
223
242
  try {
224
- const csv = exportStatsAsCsv()
243
+ let detailed = false
244
+ try {
245
+ detailed = new URL(req.url, 'http://127.0.0.1').searchParams.get('detailed') === '1'
246
+ } catch {
247
+ detailed = false
248
+ }
249
+ const csv = exportStatsAsCsv(detailed)
225
250
  res.statusCode = 200
226
251
  res.setHeader('Content-Type', 'text/csv; charset=utf-8')
227
252
  res.setHeader('Content-Disposition', 'attachment; filename="clinebot-stats.csv"')
@@ -231,4 +256,58 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
231
256
  }
232
257
  },
233
258
  }), 'dsh-clinebot: /stats/export')
259
+
260
+ // 7. GET /dsh-clinebot/analytics — aggregated session/day/model/account report
261
+ ctx.effect(() => ctx.webServer.register({
262
+ kind: 'exact',
263
+ path: '/dsh-clinebot/analytics',
264
+ handler: async (req, res) => {
265
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
266
+ if (!assertTrustedSettingsRequest(req, res, ctx)) return
267
+ try {
268
+ let query = {}
269
+ try {
270
+ query = Object.fromEntries(new URL(req.url, 'http://127.0.0.1').searchParams.entries())
271
+ } catch {}
272
+ const parseTime = (value) => {
273
+ if (value === undefined || value === '') return undefined
274
+ const numeric = Number(value)
275
+ return Number.isFinite(numeric) ? numeric : Date.parse(value)
276
+ }
277
+ const analytics = getAnalytics({
278
+ from: parseTime(query.from),
279
+ to: parseTime(query.to),
280
+ groupBy: query.groupBy,
281
+ limit: query.limit,
282
+ model: query.model,
283
+ account: query.account,
284
+ heatmapDays: query.heatmapDays,
285
+ heatmapModel: query.heatmapModel,
286
+ })
287
+ writeJson(res, 200, { ok: true, ...analytics })
288
+ } catch (err) {
289
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
290
+ }
291
+ },
292
+ }), 'dsh-clinebot: /analytics')
293
+
294
+ // 8. GET /dsh-clinebot/prices — local cost-estimate table
295
+ ctx.effect(() => ctx.webServer.register({
296
+ kind: 'exact',
297
+ path: '/dsh-clinebot/prices',
298
+ handler: async (req, res) => {
299
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
300
+ if (!assertTrustedSettingsRequest(req, res, ctx)) return
301
+ try {
302
+ const pub = publicConfig(live())
303
+ writeJson(res, 200, {
304
+ ok: true,
305
+ ...priceTable(pub.priceOverrides || {}, pub.enabledModels || []),
306
+ updatedAt: new Date().toISOString(),
307
+ })
308
+ } catch (err) {
309
+ writeJson(res, 500, { ok: false, error: String(err?.message || err) })
310
+ }
311
+ },
312
+ }), 'dsh-clinebot: /prices')
234
313
  }
@@ -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
+ }