token-usage-dashboard 1.0.6

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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/bin/ai-usage-dashboard.js +92 -0
  4. package/build/client/_app/immutable/assets/0.fjR0gTLF.css +1 -0
  5. package/build/client/_app/immutable/assets/0.fjR0gTLF.css.br +0 -0
  6. package/build/client/_app/immutable/assets/0.fjR0gTLF.css.gz +0 -0
  7. package/build/client/_app/immutable/assets/2.BABRsmGA.css +1 -0
  8. package/build/client/_app/immutable/assets/2.BABRsmGA.css.br +0 -0
  9. package/build/client/_app/immutable/assets/2.BABRsmGA.css.gz +0 -0
  10. package/build/client/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  11. package/build/client/_app/immutable/chunks/Bjy-W4x2.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/Bjy-W4x2.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/DC6tN2WI.js +3 -0
  14. package/build/client/_app/immutable/chunks/DC6tN2WI.js.br +0 -0
  15. package/build/client/_app/immutable/chunks/DC6tN2WI.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/DRLSLo2t.js +2 -0
  17. package/build/client/_app/immutable/chunks/DRLSLo2t.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/DRLSLo2t.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/xihTtKlq.js +1 -0
  20. package/build/client/_app/immutable/chunks/xihTtKlq.js.br +1 -0
  21. package/build/client/_app/immutable/chunks/xihTtKlq.js.gz +0 -0
  22. package/build/client/_app/immutable/entry/app.C7Gg5PnI.js +2 -0
  23. package/build/client/_app/immutable/entry/app.C7Gg5PnI.js.br +0 -0
  24. package/build/client/_app/immutable/entry/app.C7Gg5PnI.js.gz +0 -0
  25. package/build/client/_app/immutable/entry/start.fbJ-_JRS.js +1 -0
  26. package/build/client/_app/immutable/entry/start.fbJ-_JRS.js.br +0 -0
  27. package/build/client/_app/immutable/entry/start.fbJ-_JRS.js.gz +0 -0
  28. package/build/client/_app/immutable/nodes/0.BNwlucmP.js +1 -0
  29. package/build/client/_app/immutable/nodes/0.BNwlucmP.js.br +0 -0
  30. package/build/client/_app/immutable/nodes/0.BNwlucmP.js.gz +0 -0
  31. package/build/client/_app/immutable/nodes/1.Dj3Fzim2.js +1 -0
  32. package/build/client/_app/immutable/nodes/1.Dj3Fzim2.js.br +0 -0
  33. package/build/client/_app/immutable/nodes/1.Dj3Fzim2.js.gz +0 -0
  34. package/build/client/_app/immutable/nodes/2.Csi9nKxw.js +2 -0
  35. package/build/client/_app/immutable/nodes/2.Csi9nKxw.js.br +0 -0
  36. package/build/client/_app/immutable/nodes/2.Csi9nKxw.js.gz +0 -0
  37. package/build/client/_app/version.json +1 -0
  38. package/build/client/_app/version.json.br +0 -0
  39. package/build/client/_app/version.json.gz +0 -0
  40. package/build/client/favicon.svg +1 -0
  41. package/build/client/favicon.svg.br +0 -0
  42. package/build/client/favicon.svg.gz +0 -0
  43. package/build/client/robots.txt +2 -0
  44. package/build/client/robots.txt.br +3 -0
  45. package/build/client/robots.txt.gz +0 -0
  46. package/build/env.js +70 -0
  47. package/build/handler.js +22 -0
  48. package/build/index.js +145 -0
  49. package/build/server/chunks/chunks/client.js-C1_n_a2s.js +28 -0
  50. package/build/server/chunks/chunks/env.js-l0sNRNKZ.js +2 -0
  51. package/build/server/chunks/chunks/exports.js-Dd48p7Ih.js +263 -0
  52. package/build/server/chunks/chunks/internal.js-b-1wrnfA.js +1201 -0
  53. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js +22 -0
  54. package/build/server/chunks/chunks/queries.js-aTxz7ubg.js +676 -0
  55. package/build/server/chunks/chunks/range.js-CLYFtcRE.js +117 -0
  56. package/build/server/chunks/chunks/refresh.js-DM1hnB2Y.js +107 -0
  57. package/build/server/chunks/chunks/request.js-B3j1_Icg.js +20 -0
  58. package/build/server/chunks/chunks/server.js-mTMwhapa.js +5374 -0
  59. package/build/server/chunks/chunks/shared.js-7DyfRqo3.js +255 -0
  60. package/build/server/chunks/chunks/utils.js-DzwqnkBT.js +1049 -0
  61. package/build/server/chunks/entries/endpoints/api/refresh/_server.ts.js-CJa6pKKD.js +21 -0
  62. package/build/server/chunks/entries/endpoints/api/settings/_server.ts.js-DQt7Bc2q.js +24 -0
  63. package/build/server/chunks/entries/endpoints/api/usage/_server.ts.js-kInjrhtE.js +17 -0
  64. package/build/server/chunks/entries/fallbacks/error.svelte.js-CVKDebWa.js +68 -0
  65. package/build/server/chunks/entries/hooks.server.js-BbojNy1X.js +16 -0
  66. package/build/server/chunks/entries/pages/_layout.svelte.js-C9kkUOBf.js +9 -0
  67. package/build/server/chunks/entries/pages/_page.server.ts.js-Oa3Sfml2.js +17 -0
  68. package/build/server/chunks/entries/pages/_page.svelte.js-D91hy9R4.js +1255 -0
  69. package/build/server/chunks/handler-br42Ds2q.js +1475 -0
  70. package/build/server/chunks/index.js-CBAFVV7O.js +3972 -0
  71. package/build/server/chunks/manifest.js-s70ajt69.js +63 -0
  72. package/build/server/chunks/nodes/0.js-Bzngkdxl.js +9 -0
  73. package/build/server/chunks/nodes/1.js-Cvu2_eGw.js +9 -0
  74. package/build/server/chunks/nodes/2.js-7xvomkGu.js +20 -0
  75. package/build/shims.js +33 -0
  76. package/package.json +64 -0
@@ -0,0 +1,22 @@
1
+ //#region node_modules/@sveltejs/kit/src/runtime/app/paths/internal/server.js
2
+ var base = "";
3
+ var assets = base;
4
+ var app_dir = "_app";
5
+ var initial = {
6
+ base,
7
+ assets
8
+ };
9
+ /**
10
+ * @param {{ base: string, assets: string }} paths
11
+ */
12
+ function override(paths) {
13
+ base = paths.base;
14
+ assets = paths.assets;
15
+ }
16
+ function reset() {
17
+ base = initial.base;
18
+ assets = initial.assets;
19
+ }
20
+
21
+ export { assets as a, base as b, app_dir as c, override as o, reset as r };
22
+ //# sourceMappingURL=internal2.js-BlXt2rfE.js.map
@@ -0,0 +1,676 @@
1
+ import { t as totalOf, c as costOf } from './range.js-CLYFtcRE.js';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
+ import { dirname, join, basename } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { mkdirSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+
8
+ //#region src/lib/server/config.ts
9
+ var home = homedir();
10
+ var env = (name, fallback) => (process.env[name] || "").trim() || fallback;
11
+ var dataHome = env("XDG_DATA_HOME", join(home, ".local", "share"));
12
+ var config = {
13
+ dbPath: env("AIUSAGE_DB", join(dataHome, "ai-usage-dashboard", "usage.db")),
14
+ claudeDir: env("AIUSAGE_CLAUDE_DIR", join(home, ".claude")),
15
+ codexDir: env("AIUSAGE_CODEX_DIR", join(home, ".codex")),
16
+ openCodeDir: env("AIUSAGE_OPENCODE_DIR", join(dataHome, "opencode")),
17
+ geminiDir: env("AIUSAGE_GEMINI_DIR", join(home, ".gemini")),
18
+ openRouterKey: env("OPENROUTER_API_KEY", ""),
19
+ scanIntervalMs: Number(env("AIUSAGE_SCAN_INTERVAL", "120")) * 1e3
20
+ };
21
+ //#endregion
22
+ //#region src/lib/server/db.ts
23
+ /** Bump to throw away scanned rows and read every source file again. */
24
+ var SCHEMA = 2;
25
+ var META = "create table if not exists meta (key text primary key, value text not null)";
26
+ var TABLES = `
27
+ create table if not exists events (
28
+ id text primary key,
29
+ tool text not null,
30
+ model text not null,
31
+ session text not null,
32
+ project text not null,
33
+ ts integer not null,
34
+ day text not null,
35
+ dow integer not null,
36
+ hour integer not null,
37
+ input integer not null default 0,
38
+ output integer not null default 0,
39
+ cache_read integer not null default 0,
40
+ cache_write integer not null default 0,
41
+ reasoning integer not null default 0
42
+ );
43
+ create index if not exists events_ts on events (ts);
44
+ create index if not exists events_day on events (day);
45
+ create table if not exists files (
46
+ path text primary key,
47
+ tool text not null,
48
+ size integer not null,
49
+ mtime integer not null,
50
+ events integer not null default 0,
51
+ scanned_at integer not null
52
+ );
53
+ create table if not exists windows (
54
+ id text primary key,
55
+ tool text not null,
56
+ label text not null,
57
+ used_percent real not null,
58
+ window_minutes integer,
59
+ resets_at integer,
60
+ detail text not null default '',
61
+ seen_at integer not null
62
+ );
63
+ `;
64
+ var handle = null;
65
+ function db() {
66
+ if (handle) return handle;
67
+ mkdirSync(dirname(config.dbPath), { recursive: true });
68
+ handle = new DatabaseSync(config.dbPath);
69
+ handle.exec("pragma journal_mode = wal");
70
+ handle.exec("pragma synchronous = normal");
71
+ handle.exec(META);
72
+ if (readMeta("schema") !== String(SCHEMA)) {
73
+ handle.exec("drop table if exists events; drop table if exists files");
74
+ writeMeta("schema", String(SCHEMA));
75
+ }
76
+ handle.exec(TABLES);
77
+ rebucket(handle);
78
+ return handle;
79
+ }
80
+ var REBUCKET = `
81
+ update events set
82
+ day = date(ts / 1000, 'unixepoch', 'localtime'),
83
+ dow = cast(strftime('%w', ts / 1000, 'unixepoch', 'localtime') as integer),
84
+ hour = cast(strftime('%H', ts / 1000, 'unixepoch', 'localtime') as integer)
85
+ `;
86
+ /** Move the machine and every stored day is an hour or a date out, so redo them once. */
87
+ function rebucket(handle) {
88
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
89
+ if (readMeta("timezone") === zone) return;
90
+ handle.exec(REBUCKET);
91
+ writeMeta("timezone", zone);
92
+ }
93
+ function readMeta(key) {
94
+ return db().prepare("select value from meta where key = ?").get(key)?.value ?? null;
95
+ }
96
+ function writeMeta(key, value) {
97
+ db().prepare("insert into meta (key, value) values (?, ?) on conflict(key) do update set value = excluded.value").run(key, value);
98
+ }
99
+ //#endregion
100
+ //#region src/lib/server/sources/types.ts
101
+ /** Lines that can't be parsed are skipped: a half-written tail is normal, not a fault. */
102
+ function* jsonLines(text, needle) {
103
+ for (const line of text.split("\n")) {
104
+ if (line.length < 2 || needle && !line.includes(needle)) continue;
105
+ try {
106
+ yield JSON.parse(line);
107
+ } catch {
108
+ continue;
109
+ }
110
+ }
111
+ }
112
+ var num = (value) => typeof value === "number" && value > 0 ? value : 0;
113
+ //#endregion
114
+ //#region src/lib/server/sources/claude.ts
115
+ /**
116
+ * A streaming message is rewritten in place under one `message.id`, so the caller keeps the
117
+ * highest value it has seen per field. Session comes off the line, never the filename, or a
118
+ * subagent's tokens land under the wrong session.
119
+ */
120
+ var claudeCode = {
121
+ id: "claude-code",
122
+ label: "Claude Code",
123
+ root: join(config.claudeDir, "projects"),
124
+ ext: ".jsonl",
125
+ parse(path, text) {
126
+ const events = [];
127
+ for (const line of jsonLines(text, "\"usage\"")) {
128
+ const message = line.message;
129
+ const usage = message?.usage;
130
+ if (line.type !== "assistant" || !usage || !message.id || !message.model) continue;
131
+ if (message.model.startsWith("<")) continue;
132
+ const ts = Date.parse(String(line.timestamp));
133
+ if (Number.isNaN(ts)) continue;
134
+ const details = usage.output_tokens_details;
135
+ events.push({
136
+ id: `claude-code:${message.id}`,
137
+ model: message.model,
138
+ session: String(line.sessionId || line.session_id || ""),
139
+ project: String(line.cwd || ""),
140
+ ts,
141
+ input: num(usage.input_tokens),
142
+ output: num(usage.output_tokens),
143
+ cache_read: num(usage.cache_read_input_tokens),
144
+ cache_write: num(usage.cache_creation_input_tokens),
145
+ reasoning: num(details?.thinking_tokens)
146
+ });
147
+ }
148
+ return { events };
149
+ }
150
+ };
151
+ //#endregion
152
+ //#region src/lib/server/sources/codex.ts
153
+ var label = (minutes) => {
154
+ if (minutes === 300) return "Codex 5-hour";
155
+ if (minutes === 10080) return "Codex weekly";
156
+ return minutes ? `Codex ${minutes}-minute` : "Codex";
157
+ };
158
+ function window(id, limit, plan, seenAt) {
159
+ if (!limit || typeof limit.used_percent !== "number") return null;
160
+ return {
161
+ id: `codex-${id}`,
162
+ label: label(limit.window_minutes),
163
+ used_percent: limit.used_percent,
164
+ window_minutes: limit.window_minutes ?? null,
165
+ resets_at: limit.resets_at ? limit.resets_at * 1e3 : null,
166
+ detail: plan ? `${plan} plan` : "",
167
+ seen_at: seenAt
168
+ };
169
+ }
170
+ //#endregion
171
+ //#region src/lib/server/sources/index.ts
172
+ var SOURCES = [
173
+ claudeCode,
174
+ {
175
+ id: "codex",
176
+ label: "Codex",
177
+ root: join(config.codexDir, "sessions"),
178
+ ext: ".jsonl",
179
+ parse(path, text) {
180
+ const events = [];
181
+ let windows = [];
182
+ const rollout = basename(path, ".jsonl");
183
+ let session = "";
184
+ let project = "";
185
+ let model = "unknown";
186
+ for (const line of jsonLines(text)) {
187
+ const payload = line.payload ?? {};
188
+ const kind = line.type === "event_msg" ? payload.type : line.type;
189
+ if (kind === "session_meta") {
190
+ session = String(payload.session_id || payload.id || "");
191
+ project = String(payload.cwd || "");
192
+ continue;
193
+ }
194
+ if (kind === "turn_context" && payload.model) {
195
+ model = String(payload.model);
196
+ continue;
197
+ }
198
+ if (kind !== "token_count") continue;
199
+ const info = payload.info;
200
+ const turn = info?.last_token_usage;
201
+ const running = info?.total_token_usage;
202
+ if (!turn || !running) continue;
203
+ const ts = Date.parse(String(line.timestamp));
204
+ if (Number.isNaN(ts)) continue;
205
+ events.push({
206
+ id: `codex:${rollout}:${num(running.total_tokens)}`,
207
+ model,
208
+ session: session || rollout,
209
+ project,
210
+ ts,
211
+ input: Math.max(0, num(turn.input_tokens) - num(turn.cached_input_tokens)),
212
+ output: num(turn.output_tokens),
213
+ cache_read: num(turn.cached_input_tokens),
214
+ cache_write: num(turn.cache_write_input_tokens),
215
+ reasoning: num(turn.reasoning_output_tokens)
216
+ });
217
+ const limits = payload.rate_limits;
218
+ if (!limits) continue;
219
+ const plan = typeof limits.plan_type === "string" ? limits.plan_type : "";
220
+ windows = [window("primary", limits.primary, plan, ts), window("secondary", limits.secondary, plan, ts)].filter((entry) => entry !== null);
221
+ }
222
+ return {
223
+ events,
224
+ windows
225
+ };
226
+ }
227
+ },
228
+ {
229
+ id: "opencode",
230
+ label: "OpenCode",
231
+ root: join(config.openCodeDir, "storage", "message"),
232
+ ext: ".json",
233
+ parse(path, text) {
234
+ let message;
235
+ try {
236
+ message = JSON.parse(text);
237
+ } catch {
238
+ return { events: [] };
239
+ }
240
+ const tokens = message.tokens;
241
+ const created = message.time?.created;
242
+ if (message.role !== "assistant" || !tokens || !created) return { events: [] };
243
+ const model = message.providerID ? `${message.providerID}/${message.modelID}` : message.modelID;
244
+ if (!model) return { events: [] };
245
+ return { events: [{
246
+ id: `opencode:${message.id || basename(path, ".json")}`,
247
+ model,
248
+ session: String(message.sessionID || ""),
249
+ project: String(message.path?.cwd || message.path?.root || ""),
250
+ ts: created < 0xe8d4a51000 ? created * 1e3 : created,
251
+ input: num(tokens.input),
252
+ output: num(tokens.output),
253
+ cache_read: num(tokens.cache?.read),
254
+ cache_write: num(tokens.cache?.write),
255
+ reasoning: num(tokens.reasoning)
256
+ }] };
257
+ }
258
+ }
259
+ ];
260
+ //#endregion
261
+ //#region src/lib/server/problems.ts
262
+ var seen = /* @__PURE__ */ new Map();
263
+ /** Keyed on id so a fault that repeats every scan stays one row in the panel. */
264
+ function report(problem) {
265
+ seen.set(problem.id, {
266
+ ...problem,
267
+ at: Date.now()
268
+ });
269
+ }
270
+ function resolve(id) {
271
+ seen.delete(id);
272
+ }
273
+ var problems = () => [...seen.values()].sort((a, b) => b.at - a.at);
274
+ var describe = (error) => error instanceof Error ? error.message : String(error);
275
+ //#endregion
276
+ //#region src/lib/server/scan.ts
277
+ var running = null;
278
+ var last = [];
279
+ var finishedAt = 0;
280
+ async function listFiles(source) {
281
+ return (await readdir(source.root, {
282
+ recursive: true,
283
+ withFileTypes: true
284
+ })).filter((entry) => entry.isFile() && entry.name.endsWith(source.ext)).map((entry) => join(entry.parentPath, entry.name));
285
+ }
286
+ var INSERT = "insert into events (id, tool, model, session, project, ts, day, dow, hour, input, output, cache_read, cache_write, reasoning) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) on conflict(id) do update set input = max(input, excluded.input), output = max(output, excluded.output), cache_read = max(cache_read, excluded.cache_read), cache_write = max(cache_write, excluded.cache_write), reasoning = max(reasoning, excluded.reasoning)";
287
+ var INSERT_WINDOW = "insert into windows (id, tool, label, used_percent, window_minutes, resets_at, detail, seen_at) values (?, ?, ?, ?, ?, ?, ?, ?) on conflict(id) do update set used_percent = excluded.used_percent, resets_at = excluded.resets_at, detail = excluded.detail, seen_at = excluded.seen_at where excluded.seen_at >= windows.seen_at";
288
+ async function scanSource(source) {
289
+ const state = {
290
+ id: source.id,
291
+ label: source.label,
292
+ root: source.root,
293
+ present: true,
294
+ files: 0,
295
+ events: 0
296
+ };
297
+ let paths;
298
+ try {
299
+ paths = await listFiles(source);
300
+ } catch {
301
+ state.present = false;
302
+ return state;
303
+ }
304
+ const handle = db();
305
+ const known = new Map(handle.prepare("select path, size, mtime, events from files where tool = ?").all(source.id).map((row) => [row.path, row]));
306
+ const insert = handle.prepare(INSERT);
307
+ const insertWindow = handle.prepare(INSERT_WINDOW);
308
+ const touch = handle.prepare("insert into files (path, tool, size, mtime, events, scanned_at) values (?, ?, ?, ?, ?, ?) on conflict(path) do update set size = excluded.size, mtime = excluded.mtime, events = excluded.events, scanned_at = excluded.scanned_at");
309
+ let failures = 0;
310
+ const now = Date.now();
311
+ for (const path of paths) {
312
+ state.files += 1;
313
+ let size = 0;
314
+ let mtime = 0;
315
+ try {
316
+ const info = await lstatOf(path);
317
+ size = info.size;
318
+ mtime = info.mtime;
319
+ } catch {
320
+ continue;
321
+ }
322
+ const cached = known.get(path);
323
+ if (cached && cached.size === size && cached.mtime === mtime) {
324
+ state.events += cached.events;
325
+ continue;
326
+ }
327
+ try {
328
+ const parsed = source.parse(path, await readFile(path, "utf8"));
329
+ handle.exec("begin");
330
+ for (const event of parsed.events) {
331
+ const at = new Date(event.ts);
332
+ insert.run(event.id, source.id, event.model, event.session, event.project, event.ts, localDay(at), at.getDay(), at.getHours(), event.input, event.output, event.cache_read, event.cache_write, event.reasoning);
333
+ }
334
+ for (const window of parsed.windows ?? []) insertWindow.run(window.id, source.id, window.label, window.used_percent, window.window_minutes, window.resets_at, window.detail, window.seen_at);
335
+ touch.run(path, source.id, size, mtime, parsed.events.length, now);
336
+ handle.exec("commit");
337
+ state.events += parsed.events.length;
338
+ } catch (error) {
339
+ try {
340
+ handle.exec("rollback");
341
+ } catch {}
342
+ failures += 1;
343
+ if (failures === 1) report({
344
+ id: `scan:${source.id}`,
345
+ level: "warn",
346
+ scope: source.label,
347
+ message: `Could not read ${path}`,
348
+ hint: describe(error)
349
+ });
350
+ }
351
+ }
352
+ if (!failures) resolve(`scan:${source.id}`);
353
+ if (state.files && !state.events) report({
354
+ id: `empty:${source.id}`,
355
+ level: "warn",
356
+ scope: source.label,
357
+ message: `Found ${state.files} files under ${source.root} but no usage in any of them`,
358
+ hint: "The format may have moved on. Open an issue with a sample and we will follow it."
359
+ });
360
+ else resolve(`empty:${source.id}`);
361
+ return state;
362
+ }
363
+ var pad = (value) => String(value).padStart(2, "0");
364
+ /** Local, not UTC, so a day on the calendar is the day the user had. */
365
+ var localDay = (at) => `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
366
+ async function lstatOf(path) {
367
+ const info = await stat(path);
368
+ return {
369
+ size: info.size,
370
+ mtime: Math.round(info.mtimeMs)
371
+ };
372
+ }
373
+ async function run() {
374
+ const stats = [];
375
+ for (const source of SOURCES) stats.push(await scanSource(source));
376
+ last = stats;
377
+ finishedAt = Date.now();
378
+ writeMeta("scanned_at", String(finishedAt));
379
+ return stats;
380
+ }
381
+ /**
382
+ * One scan at a time; a caller arriving mid-scan waits on the one already going. Rows are never
383
+ * removed when their file goes: outliving the tools' own pruning is what the database is for.
384
+ */
385
+ function scan() {
386
+ if (!running) running = run().finally(() => running = null);
387
+ return running;
388
+ }
389
+ var scanState = () => ({
390
+ sources: last,
391
+ scannedAt: finishedAt,
392
+ busy: running !== null
393
+ });
394
+ //#endregion
395
+ //#region src/lib/server/settings.ts
396
+ var DEFAULTS = {
397
+ theme: "auto",
398
+ range: "30d",
399
+ metric: "tokens",
400
+ scanIntervalSec: 120,
401
+ claudeWindows: true,
402
+ prices: {}
403
+ };
404
+ var cached = null;
405
+ function settings() {
406
+ if (cached) return cached;
407
+ let stored = {};
408
+ try {
409
+ stored = JSON.parse(readMeta("settings") ?? "{}");
410
+ } catch {
411
+ stored = {};
412
+ }
413
+ cached = {
414
+ ...DEFAULTS,
415
+ ...stored
416
+ };
417
+ return cached;
418
+ }
419
+ function saveSettings(patch) {
420
+ const next = {
421
+ ...settings(),
422
+ ...patch
423
+ };
424
+ writeMeta("settings", JSON.stringify(next));
425
+ cached = next;
426
+ return next;
427
+ }
428
+ var priceOverrides = () => settings().prices;
429
+ //#endregion
430
+ //#region src/lib/server/queries.ts
431
+ var empty = () => ({
432
+ input: 0,
433
+ output: 0,
434
+ cache_read: 0,
435
+ cache_write: 0
436
+ });
437
+ function where(filters, dated = true) {
438
+ const clauses = [];
439
+ const params = [];
440
+ if (dated) {
441
+ clauses.push("ts >= ?", "ts < ?");
442
+ params.push(filters.from, filters.to);
443
+ }
444
+ for (const [column, values] of [
445
+ ["tool", filters.tools],
446
+ ["model", filters.models],
447
+ ["project", filters.projects]
448
+ ]) {
449
+ if (!values.length) continue;
450
+ clauses.push(`${column} in (${values.map(() => "?").join(", ")})`);
451
+ params.push(...values);
452
+ }
453
+ return {
454
+ sql: clauses.length ? `where ${clauses.join(" and ")}` : "",
455
+ params
456
+ };
457
+ }
458
+ var SUMS = "sum(input) input, sum(output) output, sum(cache_read) cache_read, sum(cache_write) cache_write, sum(reasoning) reasoning, count(*) events, count(distinct tool || ':' || session) sessions";
459
+ /** Rows arrive split by model so each slice can be priced at its own rate. */
460
+ function fold(rows, overrides) {
461
+ const slices = /* @__PURE__ */ new Map();
462
+ const sessions = /* @__PURE__ */ new Map();
463
+ for (const row of rows) {
464
+ const key = String(row.key ?? "");
465
+ let slice = slices.get(key);
466
+ if (!slice) {
467
+ slice = {
468
+ key,
469
+ tokens: empty(),
470
+ total: 0,
471
+ reasoning: 0,
472
+ cost: 0,
473
+ events: 0,
474
+ sessions: 0
475
+ };
476
+ slices.set(key, slice);
477
+ sessions.set(key, /* @__PURE__ */ new Set());
478
+ }
479
+ const tokens = {
480
+ input: Number(row.input) || 0,
481
+ output: Number(row.output) || 0,
482
+ cache_read: Number(row.cache_read) || 0,
483
+ cache_write: Number(row.cache_write) || 0
484
+ };
485
+ for (const field of Object.keys(tokens)) slice.tokens[field] += tokens[field];
486
+ slice.total += totalOf(tokens);
487
+ slice.reasoning += Number(row.reasoning) || 0;
488
+ slice.events += Number(row.events) || 0;
489
+ slice.sessions += Number(row.sessions) || 0;
490
+ const cost = costOf(String(row.model ?? ""), tokens, overrides);
491
+ if (cost === null) slice.cost = null;
492
+ else if (slice.cost !== null) slice.cost += cost;
493
+ }
494
+ return slices;
495
+ }
496
+ var blank = (key) => ({
497
+ key,
498
+ tokens: empty(),
499
+ total: 0,
500
+ reasoning: 0,
501
+ cost: 0,
502
+ events: 0,
503
+ sessions: 0
504
+ });
505
+ function grouped(column, filters, overrides) {
506
+ const clause = where(filters);
507
+ return [...fold(db().prepare(`select ${column} key, model, ${SUMS} from events ${clause.sql} group by ${column}, model`).all(...clause.params), overrides).values()].sort((a, b) => b.total - a.total);
508
+ }
509
+ function totals(filters, overrides) {
510
+ const clause = where(filters);
511
+ const slice = fold(db().prepare(`select '' key, model, ${SUMS} from events ${clause.sql} group by model`).all(...clause.params), overrides).get("") ?? blank("");
512
+ const distinct = db().prepare(`select count(distinct tool || ':' || session) sessions from events ${clause.sql}`).get(...clause.params);
513
+ slice.sessions = Number(distinct?.sessions) || 0;
514
+ return slice;
515
+ }
516
+ function series(filters, column, overrides) {
517
+ const clause = where(filters);
518
+ const rows = db().prepare(`select day, ${column} key, model, ${SUMS} from events ${clause.sql} group by day, ${column}, model order by day`).all(...clause.params);
519
+ const points = /* @__PURE__ */ new Map();
520
+ for (const row of rows) {
521
+ const id = `${row.day}|${row.key}`;
522
+ const point = points.get(id) ?? {
523
+ day: String(row.day),
524
+ key: String(row.key ?? ""),
525
+ total: 0,
526
+ cost: 0
527
+ };
528
+ const tokens = {
529
+ input: Number(row.input) || 0,
530
+ output: Number(row.output) || 0,
531
+ cache_read: Number(row.cache_read) || 0,
532
+ cache_write: Number(row.cache_write) || 0
533
+ };
534
+ point.total += totalOf(tokens);
535
+ const cost = costOf(String(row.model ?? ""), tokens, overrides);
536
+ if (cost === null) point.cost = null;
537
+ else if (point.cost !== null) point.cost += cost;
538
+ points.set(id, point);
539
+ }
540
+ return [...points.values()];
541
+ }
542
+ /** The calendar ignores the date filter, since a year of squares is the whole point. */
543
+ function calendar(filters, overrides) {
544
+ const clause = where(filters, false);
545
+ const rows = db().prepare(`select day, model, ${SUMS} from events ${clause.sql} group by day, model order by day`).all(...clause.params);
546
+ const days = /* @__PURE__ */ new Map();
547
+ for (const row of rows) {
548
+ const day = String(row.day);
549
+ const entry = days.get(day) ?? {
550
+ day,
551
+ total: 0,
552
+ cost: 0,
553
+ events: 0
554
+ };
555
+ const tokens = {
556
+ input: Number(row.input) || 0,
557
+ output: Number(row.output) || 0,
558
+ cache_read: Number(row.cache_read) || 0,
559
+ cache_write: Number(row.cache_write) || 0
560
+ };
561
+ entry.total += totalOf(tokens);
562
+ entry.events += Number(row.events) || 0;
563
+ const cost = costOf(String(row.model ?? ""), tokens, overrides);
564
+ if (cost === null) entry.cost = null;
565
+ else if (entry.cost !== null) entry.cost += cost;
566
+ days.set(day, entry);
567
+ }
568
+ return [...days.values()];
569
+ }
570
+ function clock(filters) {
571
+ const clause = where(filters);
572
+ return db().prepare(`select dow day, hour, sum(input + output + cache_read + cache_write) total from events ${clause.sql} group by dow, hour`).all(...clause.params).map((row) => ({
573
+ day: Number(row.day),
574
+ hour: Number(row.hour),
575
+ total: Number(row.total) || 0
576
+ }));
577
+ }
578
+ function sessions(filters, overrides, limit = 60) {
579
+ const clause = where(filters);
580
+ const rows = db().prepare(`select session key, tool, project, model, min(ts) started, max(ts) ended, sum(input) input, sum(output) output, sum(cache_read) cache_read, sum(cache_write) cache_write, sum(reasoning) reasoning, count(*) events, count(distinct tool || ':' || session) sessions from events ${clause.sql} group by session, tool, project, model`).all(...clause.params);
581
+ const merged = /* @__PURE__ */ new Map();
582
+ for (const row of rows) {
583
+ const id = String(row.key);
584
+ const at = `${row.tool}:${id}`;
585
+ const entry = merged.get(at) ?? {
586
+ id,
587
+ tool: String(row.tool),
588
+ project: String(row.project),
589
+ models: /* @__PURE__ */ new Set(),
590
+ started: Number(row.started),
591
+ ended: Number(row.ended),
592
+ total: 0,
593
+ cost: 0
594
+ };
595
+ const tokens = {
596
+ input: Number(row.input) || 0,
597
+ output: Number(row.output) || 0,
598
+ cache_read: Number(row.cache_read) || 0,
599
+ cache_write: Number(row.cache_write) || 0
600
+ };
601
+ entry.models.add(String(row.model));
602
+ entry.started = Math.min(entry.started, Number(row.started));
603
+ entry.ended = Math.max(entry.ended, Number(row.ended));
604
+ entry.total += totalOf(tokens);
605
+ const cost = costOf(String(row.model), tokens, overrides);
606
+ if (cost === null) entry.cost = null;
607
+ else if (entry.cost !== null) entry.cost += cost;
608
+ merged.set(at, entry);
609
+ }
610
+ return [...merged.values()].sort((a, b) => b.ended - a.ended).slice(0, limit).map(({ models, ...row }) => ({
611
+ ...row,
612
+ models: [...models].sort().join(", ")
613
+ }));
614
+ }
615
+ var distinct = (column) => db().prepare(`select distinct ${column} value from events order by value`).all().map((row) => String(row.value)).filter(Boolean);
616
+ var memo = /* @__PURE__ */ new Map();
617
+ var memoFor = -1;
618
+ /** One rollup per filter set per scan, since nothing under it can move in between. */
619
+ var clearUsageMemo = () => memo.clear();
620
+ function usage(filters) {
621
+ const { scannedAt } = scanState();
622
+ if (scannedAt !== memoFor || memo.size > 32) {
623
+ memo.clear();
624
+ memoFor = scannedAt;
625
+ }
626
+ const key = JSON.stringify(filters);
627
+ const hit = memo.get(key);
628
+ if (hit) return {
629
+ ...hit,
630
+ problems: problems()
631
+ };
632
+ const fresh = rollup(filters);
633
+ memo.set(key, fresh);
634
+ return fresh;
635
+ }
636
+ function rollup(filters) {
637
+ const overrides = priceOverrides();
638
+ const span = filters.to - filters.from;
639
+ const previous = {
640
+ ...filters,
641
+ from: filters.from - span,
642
+ to: filters.from
643
+ };
644
+ const byModel = grouped("model", filters, overrides);
645
+ const oldest = db().prepare("select min(ts) ts from events").get()?.ts;
646
+ const state = scanState();
647
+ return {
648
+ generatedAt: Date.now(),
649
+ filters,
650
+ totals: totals(filters, overrides),
651
+ previous: totals(previous, overrides),
652
+ days: Math.max(1, Math.round(span / 864e5)),
653
+ byTool: grouped("tool", filters, overrides),
654
+ byModel,
655
+ byProject: grouped("project", filters, overrides),
656
+ series: series(filters, "tool", overrides),
657
+ seriesByModel: series(filters, "model", overrides),
658
+ calendar: calendar(filters, overrides),
659
+ clock: clock(filters),
660
+ sessions: sessions(filters, overrides),
661
+ windows: db().prepare("select * from windows order by window_minutes, tool").all(),
662
+ options: {
663
+ tools: distinct("tool"),
664
+ models: distinct("model"),
665
+ projects: distinct("project")
666
+ },
667
+ unpriced: byModel.filter((slice) => slice.cost === null).map((slice) => slice.key),
668
+ sources: state.sources,
669
+ scannedAt: Number(readMeta("scanned_at")) || state.scannedAt,
670
+ oldest: oldest === null || oldest === void 0 ? null : Number(oldest),
671
+ problems: problems()
672
+ };
673
+ }
674
+
675
+ export { scan as a, db as b, resolve as c, describe as d, config as e, saveSettings as f, clearUsageMemo as g, report as r, settings as s, usage as u };
676
+ //# sourceMappingURL=queries.js-aTxz7ubg.js.map