@chrysb/alphaclaw 0.6.0 → 0.6.2-beta.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.
Files changed (53) hide show
  1. package/lib/public/css/agents.css +1 -1
  2. package/lib/public/css/cron.css +535 -0
  3. package/lib/public/css/theme.css +72 -0
  4. package/lib/public/js/app.js +45 -10
  5. package/lib/public/js/components/action-button.js +26 -20
  6. package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
  7. package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
  8. package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
  9. package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
  10. package/lib/public/js/components/agents-tab/index.js +4 -0
  11. package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
  12. package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
  13. package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
  14. package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
  15. package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
  16. package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
  17. package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
  18. package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
  19. package/lib/public/js/components/cron-tab/index.js +100 -0
  20. package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
  21. package/lib/public/js/components/doctor/summary-cards.js +5 -11
  22. package/lib/public/js/components/google/gmail-setup-wizard.js +30 -30
  23. package/lib/public/js/components/google/index.js +1 -1
  24. package/lib/public/js/components/icons.js +13 -0
  25. package/lib/public/js/components/pill-tabs.js +33 -0
  26. package/lib/public/js/components/pop-actions.js +58 -0
  27. package/lib/public/js/components/routes/agents-route.js +4 -0
  28. package/lib/public/js/components/routes/cron-route.js +9 -0
  29. package/lib/public/js/components/routes/index.js +1 -0
  30. package/lib/public/js/components/segmented-control.js +15 -9
  31. package/lib/public/js/components/summary-stat-card.js +17 -0
  32. package/lib/public/js/components/tooltip.js +50 -4
  33. package/lib/public/js/components/watchdog-tab.js +46 -1
  34. package/lib/public/js/lib/api.js +94 -0
  35. package/lib/public/js/lib/app-navigation.js +2 -0
  36. package/lib/public/js/lib/storage-keys.js +1 -0
  37. package/lib/public/setup.html +1 -0
  38. package/lib/server/agents/agents.js +15 -0
  39. package/lib/server/constants.js +1 -0
  40. package/lib/server/cost-utils.js +312 -0
  41. package/lib/server/cron-service.js +461 -0
  42. package/lib/server/db/usage/index.js +100 -1
  43. package/lib/server/db/usage/pricing.js +1 -83
  44. package/lib/server/db/usage/sessions.js +4 -1
  45. package/lib/server/db/usage/shared.js +2 -1
  46. package/lib/server/db/usage/summary.js +5 -1
  47. package/lib/server/gmail-watch.js +0 -1
  48. package/lib/server/onboarding/index.js +39 -5
  49. package/lib/server/onboarding/openclaw.js +25 -19
  50. package/lib/server/onboarding/validation.js +28 -0
  51. package/lib/server/routes/cron.js +148 -0
  52. package/lib/server.js +13 -0
  53. package/package.json +1 -1
@@ -0,0 +1,461 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { parseJsonValueFromNoisyOutput } = require("./utils/json");
4
+ const { deriveCostBreakdown } = require("./cost-utils");
5
+
6
+ const kCronStoreFile = "jobs.json";
7
+ const kCronRunsDir = "runs";
8
+ const kMaxRunsLimit = 200;
9
+ const kDefaultRunsLimit = 20;
10
+
11
+ const toFiniteNumber = (value, fallback = 0) => {
12
+ const parsed = Number(value);
13
+ return Number.isFinite(parsed) ? parsed : fallback;
14
+ };
15
+
16
+ const sanitizeCronJobId = (jobId = "") => {
17
+ const trimmed = String(jobId || "").trim();
18
+ if (!trimmed) throw new Error("Job id is required");
19
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("\0")) {
20
+ throw new Error("Invalid job id");
21
+ }
22
+ return trimmed;
23
+ };
24
+
25
+ const normalizeRunStatus = (value = "all") => {
26
+ const normalized = String(value || "all").trim().toLowerCase();
27
+ if (["ok", "error", "skipped", "all"].includes(normalized)) return normalized;
28
+ return "all";
29
+ };
30
+
31
+ const normalizeDeliveryStatus = (value = "all") => {
32
+ const normalized = String(value || "all").trim().toLowerCase();
33
+ if (
34
+ ["delivered", "not-delivered", "unknown", "not-requested", "all"].includes(
35
+ normalized,
36
+ )
37
+ ) {
38
+ return normalized;
39
+ }
40
+ return "all";
41
+ };
42
+
43
+ const readJsonFile = (filePath) => {
44
+ try {
45
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
46
+ } catch {
47
+ return null;
48
+ }
49
+ };
50
+
51
+ const normalizeJobs = (storeValue) => {
52
+ if (!storeValue || typeof storeValue !== "object") return [];
53
+ if (!Array.isArray(storeValue.jobs)) return [];
54
+ return storeValue.jobs
55
+ .filter((job) => job && typeof job === "object")
56
+ .map((job) => ({
57
+ ...job,
58
+ id: String(job.id || "").trim(),
59
+ name: String(job.name || "").trim(),
60
+ enabled: job.enabled !== false,
61
+ state: job.state && typeof job.state === "object" ? job.state : {},
62
+ payload: job.payload && typeof job.payload === "object" ? job.payload : {},
63
+ delivery: job.delivery && typeof job.delivery === "object" ? job.delivery : {},
64
+ schedule: job.schedule && typeof job.schedule === "object" ? job.schedule : {},
65
+ }))
66
+ .filter((job) => job.id);
67
+ };
68
+
69
+ const readCronStore = ({ cronDir }) => {
70
+ const storePath = path.join(cronDir, kCronStoreFile);
71
+ const parsed = readJsonFile(storePath);
72
+ return {
73
+ storePath,
74
+ version: 1,
75
+ jobs: normalizeJobs(parsed),
76
+ };
77
+ };
78
+
79
+ const sortJobs = (jobs = [], { sortBy = "nextRunAtMs", sortDir = "asc" } = {}) => {
80
+ const direction = String(sortDir || "asc").toLowerCase() === "desc" ? -1 : 1;
81
+ const readSortable = (job) => {
82
+ if (sortBy === "name") return String(job?.name || "").toLowerCase();
83
+ if (sortBy === "updatedAtMs") return toFiniteNumber(job?.updatedAtMs, 0);
84
+ return toFiniteNumber(job?.state?.nextRunAtMs, Number.MAX_SAFE_INTEGER);
85
+ };
86
+ return [...jobs].sort((a, b) => {
87
+ const aValue = readSortable(a);
88
+ const bValue = readSortable(b);
89
+ if (aValue === bValue) return 0;
90
+ return aValue > bValue ? direction : -direction;
91
+ });
92
+ };
93
+
94
+ const paginate = (items = [], { limit = 200, offset = 0 } = {}) => {
95
+ const safeLimit = Math.max(1, Math.min(200, Number.parseInt(String(limit), 10) || 200));
96
+ const safeOffset = Math.max(0, Number.parseInt(String(offset), 10) || 0);
97
+ const total = items.length;
98
+ const entries = items.slice(safeOffset, safeOffset + safeLimit);
99
+ const nextOffset = safeOffset + entries.length;
100
+ return {
101
+ entries,
102
+ total,
103
+ offset: safeOffset,
104
+ limit: safeLimit,
105
+ hasMore: nextOffset < total,
106
+ nextOffset: nextOffset < total ? nextOffset : null,
107
+ };
108
+ };
109
+
110
+ const parseRunLogLine = (line, jobId) => {
111
+ if (!line) return null;
112
+ try {
113
+ const value = JSON.parse(line);
114
+ if (!value || typeof value !== "object") return null;
115
+ if (String(value.action || "") !== "finished") return null;
116
+ if (String(value.jobId || "") !== jobId) return null;
117
+ const ts = toFiniteNumber(value.ts, 0);
118
+ if (!ts) return null;
119
+ return {
120
+ ts,
121
+ jobId,
122
+ action: "finished",
123
+ status: value.status,
124
+ error: value.error,
125
+ summary: value.summary,
126
+ delivered:
127
+ typeof value.delivered === "boolean" ? value.delivered : undefined,
128
+ deliveryStatus: value.deliveryStatus,
129
+ deliveryError: value.deliveryError,
130
+ sessionId: value.sessionId,
131
+ sessionKey: value.sessionKey,
132
+ runAtMs: value.runAtMs,
133
+ durationMs: value.durationMs,
134
+ nextRunAtMs: value.nextRunAtMs,
135
+ model: value.model,
136
+ provider: value.provider,
137
+ usage:
138
+ value.usage && typeof value.usage === "object" ? value.usage : undefined,
139
+ };
140
+ } catch {
141
+ return null;
142
+ }
143
+ };
144
+
145
+ const readTokenValue = (source = {}, keys = []) => {
146
+ for (const key of keys) {
147
+ const numericValue = Number(source?.[key]);
148
+ if (Number.isFinite(numericValue) && numericValue >= 0) {
149
+ return numericValue;
150
+ }
151
+ }
152
+ return 0;
153
+ };
154
+
155
+ const enrichRunEntryEstimatedCost = (entry = {}) => {
156
+ const usage = entry?.usage;
157
+ if (!usage || typeof usage !== "object") return entry;
158
+ const existingEstimatedCost = Number(
159
+ usage?.estimatedCost ?? usage?.estimated_cost ?? entry?.estimatedCost ?? entry?.estimated_cost,
160
+ );
161
+ if (Number.isFinite(existingEstimatedCost) && existingEstimatedCost >= 0) {
162
+ return {
163
+ ...entry,
164
+ estimatedCost: existingEstimatedCost,
165
+ usage: {
166
+ ...usage,
167
+ estimatedCost: existingEstimatedCost,
168
+ },
169
+ };
170
+ }
171
+ const inputTokens = readTokenValue(usage, ["input_tokens", "inputTokens"]);
172
+ const outputTokens = readTokenValue(usage, ["output_tokens", "outputTokens"]);
173
+ const cacheReadTokens = readTokenValue(usage, ["cache_read_tokens", "cacheReadTokens"]);
174
+ const cacheWriteTokens = readTokenValue(usage, ["cache_write_tokens", "cacheWriteTokens"]);
175
+ if (inputTokens <= 0 && outputTokens <= 0 && cacheReadTokens <= 0 && cacheWriteTokens <= 0) {
176
+ return entry;
177
+ }
178
+ const model = String(entry?.model || usage?.model || "").trim();
179
+ if (!model) return entry;
180
+ const breakdown = deriveCostBreakdown({
181
+ inputTokens,
182
+ outputTokens,
183
+ cacheReadTokens,
184
+ cacheWriteTokens,
185
+ provider: String(entry?.provider || "").trim(),
186
+ model,
187
+ });
188
+ if (!breakdown.pricingFound) {
189
+ return {
190
+ ...entry,
191
+ usage: {
192
+ ...usage,
193
+ pricingFound: false,
194
+ },
195
+ };
196
+ }
197
+ return {
198
+ ...entry,
199
+ estimatedCost: breakdown.totalCost,
200
+ usage: {
201
+ ...usage,
202
+ estimatedCost: breakdown.totalCost,
203
+ pricingFound: true,
204
+ },
205
+ };
206
+ };
207
+
208
+ const readJobRuns = ({
209
+ runsDir,
210
+ jobId,
211
+ limit = kDefaultRunsLimit,
212
+ offset = 0,
213
+ status = "all",
214
+ deliveryStatus = "all",
215
+ sortDir = "desc",
216
+ query = "",
217
+ }) => {
218
+ const safeJobId = sanitizeCronJobId(jobId);
219
+ const runLogPath = path.join(runsDir, `${safeJobId}.jsonl`);
220
+ const raw = fs.existsSync(runLogPath) ? fs.readFileSync(runLogPath, "utf8") : "";
221
+ const lines = String(raw || "")
222
+ .split("\n")
223
+ .map((line) => line.trim())
224
+ .filter(Boolean);
225
+ const entries = lines
226
+ .map((line) => parseRunLogLine(line, safeJobId))
227
+ .filter(Boolean);
228
+
229
+ const normalizedStatus = normalizeRunStatus(status);
230
+ const normalizedDeliveryStatus = normalizeDeliveryStatus(deliveryStatus);
231
+ const queryText = String(query || "").trim().toLowerCase();
232
+
233
+ const filtered = entries.filter((entry) => {
234
+ if (normalizedStatus !== "all" && String(entry.status || "") !== normalizedStatus) {
235
+ return false;
236
+ }
237
+ const entryDelivery = String(entry.deliveryStatus || "not-requested");
238
+ if (
239
+ normalizedDeliveryStatus !== "all" &&
240
+ entryDelivery !== normalizedDeliveryStatus
241
+ ) {
242
+ return false;
243
+ }
244
+ if (!queryText) return true;
245
+ const searchable = [
246
+ String(entry.summary || ""),
247
+ String(entry.error || ""),
248
+ String(entry.model || ""),
249
+ String(entry.provider || ""),
250
+ ]
251
+ .join(" ")
252
+ .toLowerCase();
253
+ return searchable.includes(queryText);
254
+ });
255
+
256
+ filtered.sort((a, b) => {
257
+ if (sortDir === "asc") return a.ts - b.ts;
258
+ return b.ts - a.ts;
259
+ });
260
+
261
+ const page = paginate(filtered, {
262
+ limit: Math.max(1, Math.min(kMaxRunsLimit, Number.parseInt(String(limit), 10) || kDefaultRunsLimit)),
263
+ offset,
264
+ });
265
+ return {
266
+ runLogPath,
267
+ entries: page.entries,
268
+ total: page.total,
269
+ offset: page.offset,
270
+ limit: page.limit,
271
+ hasMore: page.hasMore,
272
+ nextOffset: page.nextOffset,
273
+ };
274
+ };
275
+
276
+ const shellEscapeArg = (value) => `'${String(value || "").replace(/'/g, `'\\''`)}'`;
277
+
278
+ const parseCommandJson = (rawOutput) => {
279
+ const parsed = parseJsonValueFromNoisyOutput(rawOutput);
280
+ if (parsed && typeof parsed === "object") return parsed;
281
+ return null;
282
+ };
283
+
284
+ const createCronService = ({
285
+ clawCmd,
286
+ OPENCLAW_DIR,
287
+ getSessionUsageByKeyPattern,
288
+ }) => {
289
+ const cronDir = path.join(OPENCLAW_DIR, "cron");
290
+ const runsDir = path.join(cronDir, kCronRunsDir);
291
+
292
+ const listJobs = ({ sortBy = "nextRunAtMs", sortDir = "asc" } = {}) => {
293
+ const store = readCronStore({ cronDir });
294
+ const jobs = sortJobs(store.jobs, { sortBy, sortDir });
295
+ return {
296
+ storePath: store.storePath,
297
+ jobs,
298
+ };
299
+ };
300
+
301
+ const getStatus = () => {
302
+ const { storePath, jobs } = listJobs({ sortBy: "nextRunAtMs", sortDir: "asc" });
303
+ const enabledJobs = jobs.filter((job) => job.enabled !== false);
304
+ const nextWakeAtMs = enabledJobs.reduce((lowestValue, job) => {
305
+ const candidate = toFiniteNumber(job?.state?.nextRunAtMs, 0);
306
+ if (!candidate) return lowestValue;
307
+ if (!lowestValue) return candidate;
308
+ return Math.min(lowestValue, candidate);
309
+ }, 0);
310
+ return {
311
+ enabled: true,
312
+ storePath,
313
+ jobs: jobs.length,
314
+ enabledJobs: enabledJobs.length,
315
+ nextWakeAtMs: nextWakeAtMs || null,
316
+ };
317
+ };
318
+
319
+ const runCommand = async (command, { timeoutMs = 30000 } = {}) => {
320
+ const result = await clawCmd(command, { quiet: true, timeoutMs });
321
+ if (!result?.ok) {
322
+ const message = String(result?.stderr || result?.stdout || "Command failed").trim();
323
+ throw new Error(message || "Command failed");
324
+ }
325
+ return {
326
+ raw: result.stdout || "",
327
+ parsed: parseCommandJson(result.stdout || ""),
328
+ };
329
+ };
330
+
331
+ const runJobNow = async (jobId) => {
332
+ const safeJobId = sanitizeCronJobId(jobId);
333
+ const command = `cron run ${shellEscapeArg(safeJobId)} --json`;
334
+ return runCommand(command, { timeoutMs: 600000 });
335
+ };
336
+
337
+ const setJobEnabled = async ({ jobId, enabled }) => {
338
+ const safeJobId = sanitizeCronJobId(jobId);
339
+ const action = enabled ? "enable" : "disable";
340
+ const command = `cron ${action} ${shellEscapeArg(safeJobId)} --json`;
341
+ return runCommand(command, { timeoutMs: 60000 });
342
+ };
343
+
344
+ const updateJobPrompt = async ({ jobId, message }) => {
345
+ const safeJobId = sanitizeCronJobId(jobId);
346
+ const command = `cron edit ${shellEscapeArg(safeJobId)} --message ${shellEscapeArg(message || "")} --json`;
347
+ return runCommand(command, { timeoutMs: 60000 });
348
+ };
349
+
350
+ const getJobRuns = ({
351
+ jobId,
352
+ limit = kDefaultRunsLimit,
353
+ offset = 0,
354
+ status = "all",
355
+ deliveryStatus = "all",
356
+ sortDir = "desc",
357
+ query = "",
358
+ }) => {
359
+ const runs = readJobRuns({
360
+ runsDir,
361
+ jobId,
362
+ limit,
363
+ offset,
364
+ status,
365
+ deliveryStatus,
366
+ sortDir,
367
+ query,
368
+ });
369
+ return {
370
+ ...runs,
371
+ entries: runs.entries.map((entry) => enrichRunEntryEstimatedCost(entry)),
372
+ };
373
+ };
374
+
375
+ const getJobUsage = ({ jobId, sinceMs = 0 }) => {
376
+ const safeJobId = sanitizeCronJobId(jobId);
377
+ const keyPattern = `%:cron:${safeJobId}%`;
378
+ return getSessionUsageByKeyPattern({
379
+ keyPattern,
380
+ sinceMs: toFiniteNumber(sinceMs, 0),
381
+ });
382
+ };
383
+
384
+ const getBulkJobUsage = ({ sinceMs = 0 } = {}) => {
385
+ const { jobs } = listJobs({ sortBy: "name", sortDir: "asc" });
386
+ const safeSinceMs = toFiniteNumber(sinceMs, 0);
387
+ const byJobId = {};
388
+ jobs.forEach((job) => {
389
+ const usage = getJobUsage({ jobId: job.id, sinceMs: safeSinceMs }) || {};
390
+ const totals = usage?.totals || {};
391
+ const runCount = toFiniteNumber(totals.runCount, 0);
392
+ const totalTokens = toFiniteNumber(totals.totalTokens, 0);
393
+ const totalCost = toFiniteNumber(totals.totalCost, 0);
394
+ byJobId[job.id] = {
395
+ totalTokens,
396
+ totalCost,
397
+ runCount,
398
+ avgTokensPerRun: runCount > 0 ? Math.round(totalTokens / runCount) : 0,
399
+ };
400
+ });
401
+ return {
402
+ sinceMs: safeSinceMs,
403
+ byJobId,
404
+ };
405
+ };
406
+
407
+ const getBulkJobRuns = ({
408
+ sinceMs = 0,
409
+ limitPerJob = kDefaultRunsLimit,
410
+ status = "all",
411
+ deliveryStatus = "all",
412
+ sortDir = "desc",
413
+ query = "",
414
+ } = {}) => {
415
+ const { jobs } = listJobs({ sortBy: "name", sortDir: "asc" });
416
+ const safeSinceMs = toFiniteNumber(sinceMs, 0);
417
+ const safeLimitPerJob = Math.max(
418
+ 1,
419
+ Math.min(kMaxRunsLimit, Number.parseInt(String(limitPerJob), 10) || kDefaultRunsLimit),
420
+ );
421
+ const byJobId = {};
422
+ jobs.forEach((job) => {
423
+ const runs = getJobRuns({
424
+ jobId: job.id,
425
+ limit: safeLimitPerJob,
426
+ offset: 0,
427
+ status,
428
+ deliveryStatus,
429
+ sortDir,
430
+ query,
431
+ });
432
+ const filteredEntries = safeSinceMs > 0
433
+ ? runs.entries.filter((entry) => toFiniteNumber(entry?.ts, 0) >= safeSinceMs)
434
+ : runs.entries;
435
+ byJobId[job.id] = {
436
+ entries: filteredEntries,
437
+ total: filteredEntries.length,
438
+ };
439
+ });
440
+ return {
441
+ sinceMs: safeSinceMs,
442
+ byJobId,
443
+ };
444
+ };
445
+
446
+ return {
447
+ listJobs,
448
+ getStatus,
449
+ runJobNow,
450
+ setJobEnabled,
451
+ updateJobPrompt,
452
+ getJobRuns,
453
+ getJobUsage,
454
+ getBulkJobUsage,
455
+ getBulkJobRuns,
456
+ };
457
+ };
458
+
459
+ module.exports = {
460
+ createCronService,
461
+ };
@@ -1,11 +1,11 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { DatabaseSync } = require("node:sqlite");
4
+ const { kGlobalModelPricing, deriveCostBreakdown } = require("../../cost-utils");
4
5
  const { ensureSchema } = require("./schema");
5
6
  const { getDailySummary } = require("./summary");
6
7
  const { getSessionsList, getSessionDetail } = require("./sessions");
7
8
  const { getSessionTimeSeries } = require("./timeseries");
8
- const { kGlobalModelPricing } = require("./pricing");
9
9
 
10
10
  let db = null;
11
11
  let usageDbPath = "";
@@ -24,6 +24,104 @@ const initUsageDb = ({ rootDir }) => {
24
24
  return { path: usageDbPath };
25
25
  };
26
26
 
27
+ const getSessionUsageByKeyPattern = ({ keyPattern = "", sinceMs = 0 } = {}) => {
28
+ const database = ensureDb();
29
+ const normalizedPattern = String(keyPattern || "").trim();
30
+ if (!normalizedPattern) {
31
+ return {
32
+ totals: {
33
+ inputTokens: 0,
34
+ outputTokens: 0,
35
+ cacheReadTokens: 0,
36
+ cacheWriteTokens: 0,
37
+ totalTokens: 0,
38
+ totalCost: 0,
39
+ eventCount: 0,
40
+ runCount: 0,
41
+ },
42
+ modelBreakdown: [],
43
+ };
44
+ }
45
+
46
+ const rows = database
47
+ .prepare(
48
+ `
49
+ SELECT
50
+ COALESCE(model, '') AS model,
51
+ COALESCE(provider, '') AS provider,
52
+ COUNT(*) AS event_count,
53
+ COUNT(DISTINCT COALESCE(NULLIF(session_key, ''), NULLIF(session_id, ''))) AS run_count,
54
+ SUM(COALESCE(input_tokens, 0)) AS input_tokens,
55
+ SUM(COALESCE(output_tokens, 0)) AS output_tokens,
56
+ SUM(COALESCE(cache_read_tokens, 0)) AS cache_read_tokens,
57
+ SUM(COALESCE(cache_write_tokens, 0)) AS cache_write_tokens,
58
+ SUM(COALESCE(total_tokens, 0)) AS total_tokens
59
+ FROM usage_events
60
+ WHERE session_key LIKE $keyPattern
61
+ AND ($sinceMs <= 0 OR timestamp >= $sinceMs)
62
+ GROUP BY model, provider
63
+ ORDER BY total_tokens DESC
64
+ `,
65
+ )
66
+ .all({
67
+ $keyPattern: normalizedPattern,
68
+ $sinceMs: Number.isFinite(Number(sinceMs)) ? Number(sinceMs) : 0,
69
+ });
70
+ const modelBreakdown = rows.map((row) => {
71
+ const inputTokens = Number(row.input_tokens || 0);
72
+ const outputTokens = Number(row.output_tokens || 0);
73
+ const cacheReadTokens = Number(row.cache_read_tokens || 0);
74
+ const cacheWriteTokens = Number(row.cache_write_tokens || 0);
75
+ const totalTokens = Number(row.total_tokens || 0);
76
+ const costBreakdown = deriveCostBreakdown({
77
+ inputTokens,
78
+ outputTokens,
79
+ cacheReadTokens,
80
+ cacheWriteTokens,
81
+ provider: String(row.provider || ""),
82
+ model: String(row.model || ""),
83
+ });
84
+ return {
85
+ model: String(row.model || ""),
86
+ provider: String(row.provider || ""),
87
+ eventCount: Number(row.event_count || 0),
88
+ runCount: Number(row.run_count || 0),
89
+ inputTokens,
90
+ outputTokens,
91
+ cacheReadTokens,
92
+ cacheWriteTokens,
93
+ totalTokens,
94
+ totalCost: costBreakdown.totalCost,
95
+ pricingFound: costBreakdown.pricingFound,
96
+ };
97
+ });
98
+
99
+ const totals = modelBreakdown.reduce(
100
+ (accumulator, row) => ({
101
+ inputTokens: accumulator.inputTokens + row.inputTokens,
102
+ outputTokens: accumulator.outputTokens + row.outputTokens,
103
+ cacheReadTokens: accumulator.cacheReadTokens + row.cacheReadTokens,
104
+ cacheWriteTokens: accumulator.cacheWriteTokens + row.cacheWriteTokens,
105
+ totalTokens: accumulator.totalTokens + row.totalTokens,
106
+ totalCost: accumulator.totalCost + row.totalCost,
107
+ eventCount: accumulator.eventCount + row.eventCount,
108
+ runCount: accumulator.runCount + row.runCount,
109
+ }),
110
+ {
111
+ inputTokens: 0,
112
+ outputTokens: 0,
113
+ cacheReadTokens: 0,
114
+ cacheWriteTokens: 0,
115
+ totalTokens: 0,
116
+ totalCost: 0,
117
+ eventCount: 0,
118
+ runCount: 0,
119
+ },
120
+ );
121
+
122
+ return { totals, modelBreakdown };
123
+ };
124
+
27
125
  module.exports = {
28
126
  initUsageDb,
29
127
  getDailySummary: (options = {}) => getDailySummary({ database: ensureDb(), ...options }),
@@ -31,5 +129,6 @@ module.exports = {
31
129
  getSessionDetail: (options = {}) => getSessionDetail({ database: ensureDb(), ...options }),
32
130
  getSessionTimeSeries: (options = {}) =>
33
131
  getSessionTimeSeries({ database: ensureDb(), ...options }),
132
+ getSessionUsageByKeyPattern,
34
133
  kGlobalModelPricing,
35
134
  };
@@ -1,83 +1 @@
1
- const kTokensPerMillion = 1_000_000;
2
- const kLongContextThresholdTokens = 200_000;
3
- const kGlobalModelPricing = {
4
- "claude-opus-4-6": {
5
- input: (tokens) => (tokens > kLongContextThresholdTokens ? 10.0 : 5.0),
6
- output: (tokens) => (tokens > kLongContextThresholdTokens ? 37.5 : 25.0),
7
- },
8
- "claude-sonnet-4-6": { input: 3.0, output: 15.0 },
9
- "claude-haiku-4-6": { input: 0.8, output: 4.0 },
10
- "gpt-5": { input: 1.25, output: 10.0 },
11
- "gpt-5.4": { input: 2.5, output: 10.0 },
12
- "gpt-5.1-codex": { input: 2.5, output: 10.0 },
13
- "gpt-5.3-codex": { input: 2.5, output: 10.0 },
14
- "gpt-4.1": { input: 2.0, output: 8.0 },
15
- "gpt-4o": { input: 2.5, output: 10.0 },
16
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
17
- "gemini-3.1-pro-preview": { input: 2.0, output: 12.0 },
18
- "gemini-3-flash-preview": { input: 0.5, output: 3.0 },
19
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
20
- };
21
-
22
- const toInt = (value, fallbackValue = 0) => {
23
- const parsed = Number.parseInt(String(value ?? ""), 10);
24
- return Number.isFinite(parsed) ? parsed : fallbackValue;
25
- };
26
-
27
- const resolvePricing = (model) => {
28
- const normalized = String(model || "").toLowerCase();
29
- if (!normalized) return null;
30
- const exact = kGlobalModelPricing[normalized];
31
- if (exact) return exact;
32
- const matchKey = Object.keys(kGlobalModelPricing).find((key) =>
33
- normalized.includes(key),
34
- );
35
- return matchKey ? kGlobalModelPricing[matchKey] : null;
36
- };
37
-
38
- const resolvePerMillionRate = (rate, tokens) => {
39
- if (typeof rate === "function") {
40
- return Number(rate(toInt(tokens)));
41
- }
42
- return Number(rate || 0);
43
- };
44
-
45
- const deriveCostBreakdown = ({
46
- inputTokens = 0,
47
- outputTokens = 0,
48
- cacheReadTokens = 0,
49
- cacheWriteTokens = 0,
50
- model = "",
51
- }) => {
52
- const pricing = resolvePricing(model);
53
- if (!pricing) {
54
- return {
55
- inputCost: 0,
56
- outputCost: 0,
57
- cacheReadCost: 0,
58
- cacheWriteCost: 0,
59
- totalCost: 0,
60
- pricingFound: false,
61
- };
62
- }
63
- const inputRate = resolvePerMillionRate(pricing.input, inputTokens);
64
- const outputRate = resolvePerMillionRate(pricing.output, outputTokens);
65
- const inputCost = (inputTokens / kTokensPerMillion) * inputRate;
66
- const outputCost = (outputTokens / kTokensPerMillion) * outputRate;
67
- const cacheReadCost = 0;
68
- const cacheWriteRate = resolvePerMillionRate(pricing.input, cacheWriteTokens);
69
- const cacheWriteCost = (cacheWriteTokens / kTokensPerMillion) * cacheWriteRate;
70
- return {
71
- inputCost,
72
- outputCost,
73
- cacheReadCost,
74
- cacheWriteCost,
75
- totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
76
- pricingFound: true,
77
- };
78
- };
79
-
80
- module.exports = {
81
- kGlobalModelPricing,
82
- deriveCostBreakdown,
83
- };
1
+ module.exports = require("../../cost-utils");
@@ -6,7 +6,10 @@ const {
6
6
  getUsageMetricsFromEventRow,
7
7
  } = require("./shared");
8
8
 
9
- const getSessionsList = ({ database, limit = kDefaultSessionLimit } = {}) => {
9
+ const getSessionsList = ({
10
+ database,
11
+ limit = kDefaultSessionLimit,
12
+ } = {}) => {
10
13
  const safeLimit = clampInt(limit, 1, kMaxSessionLimit, kDefaultSessionLimit);
11
14
  const rows = database
12
15
  .prepare(`
@@ -1,4 +1,4 @@
1
- const { deriveCostBreakdown } = require("./pricing");
1
+ const { deriveCostBreakdown } = require("../../cost-utils");
2
2
 
3
3
  const kDefaultSessionLimit = 50;
4
4
  const kMaxSessionLimit = 200;
@@ -76,6 +76,7 @@ const getUsageMetricsFromEventRow = (row) => {
76
76
  outputTokens,
77
77
  cacheReadTokens,
78
78
  cacheWriteTokens,
79
+ provider: row.provider,
79
80
  model: row.model,
80
81
  });
81
82
  return {
@@ -124,7 +124,11 @@ const getAgentCostDistribution = ({
124
124
  };
125
125
  };
126
126
 
127
- const getDailySummary = ({ database, days = kDefaultDays, timeZone = kUtcTimeZone } = {}) => {
127
+ const getDailySummary = ({
128
+ database,
129
+ days = kDefaultDays,
130
+ timeZone = kUtcTimeZone,
131
+ } = {}) => {
128
132
  const { now, safeDays, startDay, timeZone: normalizedTimeZone } = getPeriodRange(
129
133
  days,
130
134
  timeZone,