@tokz/cli 0.0.1 → 0.2.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.
@@ -0,0 +1,1499 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addUsage,
4
+ buildReport,
5
+ cacheHitRate,
6
+ cacheSavings,
7
+ compact,
8
+ costUsd,
9
+ duration,
10
+ emptyUsage,
11
+ findMcpServers,
12
+ parseTranscript,
13
+ pct,
14
+ pct1,
15
+ relativeDate,
16
+ sanitizeProjectPath,
17
+ shortModel,
18
+ usd
19
+ } from "./chunk-MO4Y7IF6.js";
20
+
21
+ // src/ui/Root.tsx
22
+ import { useEffect as useEffect2, useState as useState6 } from "react";
23
+ import { Box as Box10, Text as Text11, useApp as useApp2 } from "ink";
24
+
25
+ // src/agents/index.ts
26
+ import { access as access4 } from "fs/promises";
27
+ import { homedir as homedir5 } from "os";
28
+ import { join as join5 } from "path";
29
+
30
+ // src/agents/claude.ts
31
+ import { access } from "fs/promises";
32
+ import { homedir as homedir2 } from "os";
33
+ import { join as join2 } from "path";
34
+
35
+ // src/projects.ts
36
+ import { readFile } from "fs/promises";
37
+ import { homedir } from "os";
38
+ import { join, relative, sep } from "path";
39
+ import { glob } from "tinyglobby";
40
+ function applyTimeframe(projects, range) {
41
+ if (!range) return projects;
42
+ return projects.map(
43
+ (p) => p.sessions ? { ...p, report: buildReport(p.sessions, p.serverList ?? [], range) } : p
44
+ ).filter((p) => !p.sessions || p.report.sessionCount > 0).sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
45
+ }
46
+ function baseName(p) {
47
+ const parts = p.replace(/\\/g, "/").split("/").filter(Boolean);
48
+ return parts.at(-1) ?? p;
49
+ }
50
+ var DAY_MS = 864e5;
51
+ function aggregate(projects) {
52
+ const usageByModel = {};
53
+ const toolCalls = {};
54
+ const toolCostUsd = {};
55
+ const serverByName = /* @__PURE__ */ new Map();
56
+ const dailyByDate = /* @__PURE__ */ new Map();
57
+ const sessions = [];
58
+ let sessionCount = 0;
59
+ let totalTurns = 0;
60
+ let start;
61
+ let end;
62
+ for (const { report } of projects) {
63
+ sessionCount += report.sessionCount;
64
+ totalTurns += report.totalTurns ?? 0;
65
+ for (const [m, u] of Object.entries(report.usageByModel)) {
66
+ addUsage(usageByModel[m] ??= emptyUsage(), u);
67
+ }
68
+ for (const [t, n] of Object.entries(report.toolCalls)) toolCalls[t] = (toolCalls[t] ?? 0) + n;
69
+ for (const [t, c] of Object.entries(report.toolCostUsd ?? {})) {
70
+ toolCostUsd[t] = (toolCostUsd[t] ?? 0) + c;
71
+ }
72
+ for (const s of report.servers) {
73
+ const acc = serverByName.get(s.name);
74
+ if (!acc) {
75
+ serverByName.set(s.name, { ...s });
76
+ } else {
77
+ acc.callsObserved += s.callsObserved;
78
+ acc.estCostUsd += s.estCostUsd ?? 0;
79
+ acc.unused = acc.callsObserved === 0;
80
+ acc.configured = acc.configured || s.configured;
81
+ }
82
+ }
83
+ for (const d of report.daily ?? []) {
84
+ const acc = dailyByDate.get(d.date);
85
+ if (!acc) {
86
+ dailyByDate.set(d.date, { ...d });
87
+ } else {
88
+ acc.costUsd += d.costUsd;
89
+ acc.inputTokens += d.inputTokens;
90
+ acc.cacheReadTokens += d.cacheReadTokens;
91
+ acc.cacheCreationTokens += d.cacheCreationTokens;
92
+ acc.outputTokens += d.outputTokens;
93
+ acc.turns += d.turns;
94
+ }
95
+ }
96
+ sessions.push(...report.sessions ?? []);
97
+ if (report.spanStart && (!start || report.spanStart < start)) start = report.spanStart;
98
+ if (report.spanEnd && (!end || report.spanEnd > end)) end = report.spanEnd;
99
+ }
100
+ const costByModel = {};
101
+ let totalCostUsd = 0;
102
+ for (const [m, u] of Object.entries(usageByModel)) {
103
+ costByModel[m] = costUsd(u, m);
104
+ totalCostUsd += costByModel[m].total;
105
+ }
106
+ const spanDays = start && end ? Math.max(1, Math.round((Date.parse(end) - Date.parse(start)) / DAY_MS)) : 1;
107
+ return {
108
+ sessionCount,
109
+ spanDays,
110
+ spanStart: start,
111
+ spanEnd: end,
112
+ usageByModel,
113
+ costByModel,
114
+ totalCostUsd,
115
+ monthlyProjectionUsd: totalCostUsd / spanDays * 30,
116
+ toolCalls,
117
+ toolCostUsd,
118
+ servers: [...serverByName.values()],
119
+ daily: [...dailyByDate.values()].sort((a, b) => a.date < b.date ? -1 : 1),
120
+ sessions: sessions.sort((a, b) => b.costUsd - a.costUsd),
121
+ cacheSavingsUsd: cacheSavings(usageByModel),
122
+ cacheHitRate: cacheHitRate(usageByModel),
123
+ totalTurns
124
+ };
125
+ }
126
+ async function realPathsBySanitized(home) {
127
+ const map = /* @__PURE__ */ new Map();
128
+ try {
129
+ const cfg = JSON.parse(await readFile(join(home, ".claude.json"), "utf8"));
130
+ const projects = cfg?.projects;
131
+ if (projects && typeof projects === "object") {
132
+ for (const p of Object.keys(projects)) map.set(sanitizeProjectPath(p), p);
133
+ }
134
+ } catch {
135
+ }
136
+ return map;
137
+ }
138
+ async function loadProjects(home = homedir(), onProgress) {
139
+ const root = join(home, ".claude", "projects");
140
+ const files = await glob(["**/*.jsonl"], { cwd: root, absolute: true }).catch(() => []);
141
+ if (files.length === 0) return [];
142
+ const byDir = /* @__PURE__ */ new Map();
143
+ for (const f of files) {
144
+ const dir = relative(root, f).split(sep)[0];
145
+ const list = byDir.get(dir) ?? [];
146
+ list.push(f);
147
+ byDir.set(dir, list);
148
+ }
149
+ const realMap = await realPathsBySanitized(home);
150
+ const seenMessageIds = /* @__PURE__ */ new Set();
151
+ const seenToolIds = /* @__PURE__ */ new Set();
152
+ let parsed = 0;
153
+ const out = [];
154
+ for (const [dir, dirFiles] of byDir) {
155
+ onProgress?.({ parsed, total: files.length, currentProject: realMap.get(dir) ?? dir });
156
+ const sessions = [];
157
+ for (const f of dirFiles) {
158
+ sessions.push(await parseTranscript(f, seenMessageIds, seenToolIds));
159
+ parsed += 1;
160
+ onProgress?.({ parsed, total: files.length, currentProject: realMap.get(dir) ?? dir });
161
+ }
162
+ const realPath = realMap.get(dir) ?? sessions.find((s) => s.cwd)?.cwd;
163
+ const servers = realPath ? await findMcpServers(realPath, home) : [];
164
+ out.push({
165
+ id: dir,
166
+ name: realPath ?? dir,
167
+ label: realPath ? baseName(realPath) : dir,
168
+ realPath,
169
+ report: buildReport(sessions, servers),
170
+ sessions,
171
+ serverList: servers
172
+ });
173
+ }
174
+ out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
175
+ return out;
176
+ }
177
+
178
+ // src/agents/claude.ts
179
+ var claudeAdapter = {
180
+ id: "claude",
181
+ name: "Claude Code",
182
+ supported: true,
183
+ async detect(home = homedir2()) {
184
+ try {
185
+ await access(join2(home, ".claude", "projects"));
186
+ return true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ },
191
+ loadProjects: (home, onProgress) => loadProjects(home, onProgress)
192
+ };
193
+
194
+ // src/agents/codex.ts
195
+ import { createReadStream } from "fs";
196
+ import { access as access2 } from "fs/promises";
197
+ import { createInterface } from "readline";
198
+ import { homedir as homedir3 } from "os";
199
+ import { join as join3 } from "path";
200
+ import { glob as glob2 } from "tinyglobby";
201
+ import { z } from "zod";
202
+ var TokenTotals = z.object({
203
+ input_tokens: z.number().catch(0).default(0),
204
+ cached_input_tokens: z.number().catch(0).default(0),
205
+ output_tokens: z.number().catch(0).default(0),
206
+ reasoning_output_tokens: z.number().catch(0).default(0),
207
+ total_tokens: z.number().catch(0).default(0)
208
+ });
209
+ var Line = z.object({
210
+ timestamp: z.string().optional(),
211
+ type: z.string().optional(),
212
+ // old-format meta lines carry cwd at the top level
213
+ cwd: z.string().optional(),
214
+ payload: z.object({
215
+ type: z.string().optional(),
216
+ cwd: z.string().optional(),
217
+ model: z.string().optional(),
218
+ name: z.string().optional(),
219
+ info: z.object({ total_token_usage: TokenTotals.optional() }).nullish()
220
+ }).passthrough().optional()
221
+ });
222
+ function codexHome(home) {
223
+ if (home) return join3(home, ".codex");
224
+ return process.env.CODEX_HOME ?? join3(homedir3(), ".codex");
225
+ }
226
+ var DEFAULT_MODEL = "gpt-5";
227
+ async function parseCodexRollout(file) {
228
+ const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
229
+ const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
230
+ let model = DEFAULT_MODEL;
231
+ let prev = { input: 0, cached: 0, output: 0, total: 0 };
232
+ let turnTools = [];
233
+ for await (const raw of rl) {
234
+ if (!raw.trim()) continue;
235
+ let obj;
236
+ try {
237
+ obj = JSON.parse(raw);
238
+ } catch {
239
+ continue;
240
+ }
241
+ const parsed = Line.safeParse(obj);
242
+ if (!parsed.success) continue;
243
+ const { timestamp, type, payload } = parsed.data;
244
+ if (!stats.cwd) stats.cwd = payload?.cwd ?? parsed.data.cwd;
245
+ if (payload?.model) model = payload.model;
246
+ if (timestamp) {
247
+ if (!stats.firstTs) stats.firstTs = timestamp;
248
+ stats.lastTs = timestamp;
249
+ }
250
+ const toolName = payload?.type === "function_call" || payload?.type === "custom_tool_call" ? payload.name : payload?.type === "local_shell_call" ? "shell" : type === "function_call" ? parsed.data.name : void 0;
251
+ if (toolName) {
252
+ stats.toolCalls[toolName] = (stats.toolCalls[toolName] ?? 0) + 1;
253
+ turnTools.push(toolName);
254
+ }
255
+ const totals = payload?.type === "token_count" ? payload.info?.total_token_usage : void 0;
256
+ if (!totals) continue;
257
+ const cur = {
258
+ input: totals.input_tokens,
259
+ cached: totals.cached_input_tokens,
260
+ output: totals.output_tokens,
261
+ total: totals.total_tokens
262
+ };
263
+ if (cur.total < prev.total) prev = { input: 0, cached: 0, output: 0, total: 0 };
264
+ const dInput = Math.max(0, cur.input - prev.input);
265
+ const dCached = Math.max(0, cur.cached - prev.cached);
266
+ const dOutput = Math.max(0, cur.output - prev.output);
267
+ prev = cur;
268
+ if (dInput + dOutput === 0) continue;
269
+ const delta = {
270
+ // Codex's input_tokens INCLUDES cached tokens; split them out.
271
+ inputTokens: Math.max(0, dInput - dCached),
272
+ cacheReadTokens: dCached,
273
+ cacheCreationTokens: 0,
274
+ outputTokens: dOutput,
275
+ turns: 1
276
+ };
277
+ const accs = [stats.usageByModel[model] ??= emptyUsage()];
278
+ if (timestamp) {
279
+ const day = stats.dailyUsage[timestamp.slice(0, 10)] ??= {};
280
+ accs.push(day[model] ??= emptyUsage());
281
+ }
282
+ for (const u of accs) {
283
+ u.inputTokens += delta.inputTokens;
284
+ u.cacheReadTokens += delta.cacheReadTokens;
285
+ u.outputTokens += delta.outputTokens;
286
+ u.turns += 1;
287
+ }
288
+ const turnCost = costUsd(delta, model).total;
289
+ if (turnCost > 0 && turnTools.length > 0) {
290
+ const share = turnCost / turnTools.length;
291
+ for (const name of turnTools) {
292
+ stats.toolCostUsd[name] = (stats.toolCostUsd[name] ?? 0) + share;
293
+ }
294
+ }
295
+ turnTools = [];
296
+ }
297
+ return stats;
298
+ }
299
+ async function loadCodexProjects(home, onProgress) {
300
+ const root = codexHome(home);
301
+ const files = await glob2(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
302
+ cwd: root,
303
+ absolute: true
304
+ }).catch(() => []);
305
+ if (files.length === 0) return [];
306
+ const byCwd = /* @__PURE__ */ new Map();
307
+ let parsed = 0;
308
+ for (const f of files) {
309
+ onProgress?.({ parsed, total: files.length, currentProject: "Codex sessions" });
310
+ const s = await parseCodexRollout(f);
311
+ parsed += 1;
312
+ onProgress?.({ parsed, total: files.length, currentProject: "Codex sessions" });
313
+ if (Object.keys(s.usageByModel).length === 0) continue;
314
+ const key = s.cwd ?? "(unknown project)";
315
+ const list = byCwd.get(key) ?? [];
316
+ list.push(s);
317
+ byCwd.set(key, list);
318
+ }
319
+ const out = [];
320
+ for (const [cwd, sessions] of byCwd) {
321
+ out.push({
322
+ id: `codex:${cwd}`,
323
+ name: cwd,
324
+ label: cwd === "(unknown project)" ? cwd : baseName(cwd),
325
+ realPath: cwd,
326
+ report: buildReport(sessions, []),
327
+ sessions,
328
+ serverList: []
329
+ });
330
+ }
331
+ out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
332
+ return out;
333
+ }
334
+ var codexAdapter = {
335
+ id: "codex",
336
+ name: "OpenAI Codex",
337
+ supported: true,
338
+ async detect(home) {
339
+ try {
340
+ await access2(join3(codexHome(home), "sessions"));
341
+ return true;
342
+ } catch {
343
+ return false;
344
+ }
345
+ },
346
+ loadProjects: loadCodexProjects
347
+ };
348
+
349
+ // src/agents/opencode.ts
350
+ import { access as access3, readFile as readFile2 } from "fs/promises";
351
+ import { homedir as homedir4 } from "os";
352
+ import { join as join4 } from "path";
353
+ import { glob as glob3 } from "tinyglobby";
354
+ import { z as z2 } from "zod";
355
+ var Message = z2.object({
356
+ sessionID: z2.string(),
357
+ role: z2.string(),
358
+ modelID: z2.string().optional(),
359
+ providerID: z2.string().optional(),
360
+ time: z2.object({ created: z2.number().optional(), completed: z2.number().optional() }).optional(),
361
+ tokens: z2.object({
362
+ input: z2.number().catch(0).default(0),
363
+ output: z2.number().catch(0).default(0),
364
+ reasoning: z2.number().catch(0).default(0),
365
+ cache: z2.object({ read: z2.number().catch(0).default(0), write: z2.number().catch(0).default(0) }).optional()
366
+ }).optional()
367
+ });
368
+ var Session = z2.object({
369
+ id: z2.string(),
370
+ directory: z2.string().optional(),
371
+ projectID: z2.string().optional(),
372
+ time: z2.object({ created: z2.number().optional() }).optional()
373
+ });
374
+ var Project = z2.object({ id: z2.string(), worktree: z2.string().optional() });
375
+ function opencodeRoot(home) {
376
+ if (home) return join4(home, ".local", "share", "opencode");
377
+ return process.env.OPENCODE_DATA_DIR ?? join4(homedir4(), ".local", "share", "opencode");
378
+ }
379
+ async function readJson(file) {
380
+ try {
381
+ return JSON.parse(await readFile2(file, "utf8"));
382
+ } catch {
383
+ return void 0;
384
+ }
385
+ }
386
+ async function loadOpencodeProjects(home, onProgress) {
387
+ const root = opencodeRoot(home);
388
+ const storage = join4(root, "storage");
389
+ const messageFiles = await glob3(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
390
+ () => []
391
+ );
392
+ if (messageFiles.length === 0) return [];
393
+ const sessionDir = /* @__PURE__ */ new Map();
394
+ const projectWorktree = /* @__PURE__ */ new Map();
395
+ for (const f of await glob3(["project/*.json"], { cwd: storage, absolute: true }).catch(() => [])) {
396
+ const p = Project.safeParse(await readJson(f));
397
+ if (p.success && p.data.worktree) projectWorktree.set(p.data.id, p.data.worktree);
398
+ }
399
+ const sessionFiles = await glob3(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
400
+ () => []
401
+ );
402
+ for (const f of sessionFiles) {
403
+ const s = Session.safeParse(await readJson(f));
404
+ if (!s.success) continue;
405
+ const dir = s.data.directory ?? (s.data.projectID ? projectWorktree.get(s.data.projectID) : void 0);
406
+ if (dir) sessionDir.set(s.data.id, dir);
407
+ }
408
+ const bySession = /* @__PURE__ */ new Map();
409
+ let parsed = 0;
410
+ for (const f of messageFiles) {
411
+ onProgress?.({ parsed, total: messageFiles.length, currentProject: "OpenCode messages" });
412
+ const m = Message.safeParse(await readJson(f));
413
+ parsed += 1;
414
+ onProgress?.({ parsed, total: messageFiles.length, currentProject: "OpenCode messages" });
415
+ if (!m.success || m.data.role !== "assistant" || !m.data.tokens) continue;
416
+ const { sessionID, modelID, tokens, time } = m.data;
417
+ const stats = bySession.get(sessionID) ?? {
418
+ file: sessionID,
419
+ cwd: sessionDir.get(sessionID),
420
+ usageByModel: {},
421
+ toolCalls: {},
422
+ toolCostUsd: {},
423
+ dailyUsage: {}
424
+ };
425
+ bySession.set(sessionID, stats);
426
+ const model = modelID ?? "unknown";
427
+ const ts = time?.created ? new Date(time.created).toISOString() : void 0;
428
+ if (ts) {
429
+ if (!stats.firstTs || ts < stats.firstTs) stats.firstTs = ts;
430
+ if (!stats.lastTs || ts > stats.lastTs) stats.lastTs = ts;
431
+ }
432
+ const accs = [stats.usageByModel[model] ??= emptyUsage()];
433
+ if (ts) {
434
+ const day = stats.dailyUsage[ts.slice(0, 10)] ??= {};
435
+ accs.push(day[model] ??= emptyUsage());
436
+ }
437
+ for (const u of accs) {
438
+ u.inputTokens += tokens.input;
439
+ u.cacheReadTokens += tokens.cache?.read ?? 0;
440
+ u.cacheCreationTokens += tokens.cache?.write ?? 0;
441
+ u.outputTokens += tokens.output + tokens.reasoning;
442
+ u.turns += 1;
443
+ }
444
+ }
445
+ const byDir = /* @__PURE__ */ new Map();
446
+ for (const s of bySession.values()) {
447
+ if (Object.keys(s.usageByModel).length === 0) continue;
448
+ const key = s.cwd ?? "(unknown project)";
449
+ const list = byDir.get(key) ?? [];
450
+ list.push(s);
451
+ byDir.set(key, list);
452
+ }
453
+ const out = [];
454
+ for (const [dir, sessions] of byDir) {
455
+ out.push({
456
+ id: `opencode:${dir}`,
457
+ name: dir,
458
+ label: dir === "(unknown project)" ? dir : baseName(dir),
459
+ realPath: dir,
460
+ report: buildReport(sessions, []),
461
+ sessions,
462
+ serverList: []
463
+ });
464
+ }
465
+ out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
466
+ return out;
467
+ }
468
+ var opencodeAdapter = {
469
+ id: "opencode",
470
+ name: "OpenCode",
471
+ supported: true,
472
+ async detect(home) {
473
+ try {
474
+ await access3(join4(opencodeRoot(home), "storage", "message"));
475
+ return true;
476
+ } catch {
477
+ return false;
478
+ }
479
+ },
480
+ loadProjects: loadOpencodeProjects
481
+ };
482
+
483
+ // src/agents/index.ts
484
+ function detectOnly(id, name, ...pathParts) {
485
+ return {
486
+ id,
487
+ name,
488
+ supported: false,
489
+ async detect(home = homedir5()) {
490
+ try {
491
+ await access4(join5(home, ...pathParts));
492
+ return true;
493
+ } catch {
494
+ return false;
495
+ }
496
+ },
497
+ loadProjects: async () => []
498
+ };
499
+ }
500
+ var ADAPTERS = [
501
+ claudeAdapter,
502
+ codexAdapter,
503
+ opencodeAdapter,
504
+ detectOnly("gemini", "Gemini CLI", ".gemini", "tmp"),
505
+ detectOnly("cursor", "Cursor CLI", ".cursor", "chats")
506
+ ];
507
+ async function loadAllAgents(home, onProgress) {
508
+ const out = [];
509
+ for (const adapter of ADAPTERS) {
510
+ const detected = await adapter.detect(home);
511
+ let projects = [];
512
+ if (detected && adapter.supported) {
513
+ projects = await adapter.loadProjects(home, (p) => onProgress?.(adapter.name, p)).catch(() => []);
514
+ }
515
+ out.push({ adapter, detected, projects });
516
+ }
517
+ return out;
518
+ }
519
+
520
+ // src/ui/App.tsx
521
+ import { useMemo as useMemo2, useState as useState5 } from "react";
522
+ import { Box as Box9, Text as Text10, useApp, useInput as useInput4 } from "ink";
523
+
524
+ // src/timeframe.ts
525
+ var TIMEFRAMES = [
526
+ { id: "all", label: "All time" },
527
+ { id: "today", label: "Today" },
528
+ { id: "yesterday", label: "Yesterday" },
529
+ { id: "7d", label: "Last 7 days" },
530
+ { id: "30d", label: "Last 30 days" }
531
+ ];
532
+ var isoDay = (offsetDays, now) => new Date(now - offsetDays * 864e5).toISOString().slice(0, 10);
533
+ function timeframeLabel(id) {
534
+ return TIMEFRAMES.find((t) => t.id === id)?.label ?? id;
535
+ }
536
+ function nextTimeframe(id) {
537
+ const i = TIMEFRAMES.findIndex((t) => t.id === id);
538
+ return TIMEFRAMES[(i + 1) % TIMEFRAMES.length].id;
539
+ }
540
+ function timeframeRange(id, now = Date.now()) {
541
+ switch (id) {
542
+ case "all":
543
+ return void 0;
544
+ case "today":
545
+ return { from: isoDay(0, now), to: isoDay(0, now) };
546
+ case "yesterday":
547
+ return { from: isoDay(1, now), to: isoDay(1, now) };
548
+ case "7d":
549
+ return { from: isoDay(6, now), to: isoDay(0, now) };
550
+ case "30d":
551
+ return { from: isoDay(29, now), to: isoDay(0, now) };
552
+ }
553
+ }
554
+
555
+ // src/ui/AgentPicker.tsx
556
+ import { useState as useState2 } from "react";
557
+ import { Box as Box2, Text as Text2, useInput } from "ink";
558
+
559
+ // src/ui/Banner.tsx
560
+ import { Box, Text } from "ink";
561
+
562
+ // src/ui/useTerminalSize.ts
563
+ import { useEffect, useState } from "react";
564
+ import { useStdout } from "ink";
565
+ function useTerminalSize() {
566
+ const { stdout } = useStdout();
567
+ const [size, setSize] = useState({
568
+ cols: stdout?.columns ?? 80,
569
+ rows: stdout?.rows ?? 24
570
+ });
571
+ useEffect(() => {
572
+ if (!stdout) return;
573
+ const onResize = () => setSize({ cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
574
+ onResize();
575
+ stdout.on("resize", onResize);
576
+ return () => {
577
+ stdout.off("resize", onResize);
578
+ };
579
+ }, [stdout]);
580
+ return size;
581
+ }
582
+
583
+ // src/ui/Banner.tsx
584
+ import { jsx, jsxs } from "react/jsx-runtime";
585
+ var ART = [
586
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588",
587
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
588
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 ",
589
+ " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
590
+ " \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 "
591
+ ];
592
+ function Banner({ subtitle }) {
593
+ const { cols, rows } = useTerminalSize();
594
+ const tiny = cols < 40 || rows < 18;
595
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
596
+ tiny ? /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2590 TOKZ \u258C" }) : ART.map((line, i) => /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: line }, i)),
597
+ subtitle ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: subtitle }) : null
598
+ ] });
599
+ }
600
+
601
+ // src/ui/theme.ts
602
+ var theme = {
603
+ accent: "cyan",
604
+ good: "green",
605
+ bad: "red",
606
+ warn: "yellow",
607
+ mcp: "magenta"
608
+ };
609
+ var MODEL_COLORS = [
610
+ ["fable", "magenta"],
611
+ ["opus", "cyan"],
612
+ ["sonnet", "blue"],
613
+ ["haiku", "green"]
614
+ ];
615
+ function modelColor(modelId) {
616
+ for (const [key, color] of MODEL_COLORS) {
617
+ if (modelId.includes(key)) return color;
618
+ }
619
+ return "white";
620
+ }
621
+
622
+ // src/ui/AgentPicker.tsx
623
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
624
+ function status(a) {
625
+ if (a.projects.length > 0) {
626
+ const cost = a.projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
627
+ const sessions = a.projects.reduce((s, p) => s + p.report.sessionCount, 0);
628
+ return { text: `${a.projects.length} projects \xB7 ${sessions} sessions \xB7 ${usd(cost)}` };
629
+ }
630
+ if (a.detected && !a.adapter.supported) return { text: "detected \xB7 parsing not supported yet", color: theme.warn };
631
+ if (a.detected) return { text: "detected \xB7 no usage data", dim: true };
632
+ return { text: "not detected", dim: true };
633
+ }
634
+ function AgentPicker({
635
+ agents,
636
+ onSelect
637
+ }) {
638
+ const selectable = agents.map((a, i) => ({ a, i })).filter(({ a }) => a.projects.length > 0).map(({ i }) => i);
639
+ const [cursor, setCursor] = useState2(0);
640
+ const clamped = Math.min(cursor, Math.max(0, selectable.length - 1));
641
+ useInput((_input, key) => {
642
+ if (key.upArrow) setCursor(() => Math.max(0, clamped - 1));
643
+ if (key.downArrow) setCursor(() => Math.min(selectable.length - 1, clamped + 1));
644
+ if (key.return && selectable[clamped] !== void 0) onSelect(selectable[clamped]);
645
+ });
646
+ const nameW = Math.max(...agents.map((a) => a.adapter.name.length)) + 2;
647
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
648
+ /* @__PURE__ */ jsx2(Banner, { subtitle: "where your agents' tokens and dollars go \xB7 100% offline" }),
649
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accent, children: "Pick an agent" }),
650
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [
651
+ agents.map((a, i) => {
652
+ const st = status(a);
653
+ const selected = selectable[clamped] === i;
654
+ const selectableRow = a.projects.length > 0;
655
+ return /* @__PURE__ */ jsxs2(Text2, { bold: selected, children: [
656
+ /* @__PURE__ */ jsx2(Text2, { color: selected ? theme.accent : void 0, children: selected ? "\u25B8 " : " " }),
657
+ /* @__PURE__ */ jsx2(
658
+ Text2,
659
+ {
660
+ color: selected ? theme.accent : selectableRow ? void 0 : void 0,
661
+ dimColor: !selectableRow,
662
+ children: a.adapter.name.padEnd(nameW)
663
+ }
664
+ ),
665
+ /* @__PURE__ */ jsx2(Text2, { color: st.color, dimColor: st.dim, children: st.text })
666
+ ] }, a.adapter.id);
667
+ }),
668
+ selectable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no agent usage data found on this machine" }) : null
669
+ ] })
670
+ ] });
671
+ }
672
+
673
+ // src/ui/Menu.tsx
674
+ import { Box as Box4, Text as Text5 } from "ink";
675
+ import SelectInput from "ink-select-input";
676
+
677
+ // src/ui/Sparkline.tsx
678
+ import { Text as Text3 } from "ink";
679
+ import { jsx as jsx3 } from "react/jsx-runtime";
680
+ var LEVELS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
681
+ function sparkline(values) {
682
+ const max = Math.max(0, ...values);
683
+ if (max <= 0) return LEVELS[0].repeat(values.length);
684
+ return values.map((v) => LEVELS[v <= 0 ? 0 : Math.min(7, Math.max(1, Math.round(v / max * 7)))]).join("");
685
+ }
686
+ function Sparkline({ values, color = "cyan" }) {
687
+ return /* @__PURE__ */ jsx3(Text3, { color, children: sparkline(values) });
688
+ }
689
+
690
+ // src/ui/StatCards.tsx
691
+ import { Box as Box3, Text as Text4 } from "ink";
692
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
693
+ function StatCards({ stats }) {
694
+ return /* @__PURE__ */ jsx4(Box3, { flexWrap: "wrap", children: stats.map((s) => /* @__PURE__ */ jsxs3(
695
+ Box3,
696
+ {
697
+ borderStyle: "round",
698
+ borderColor: "gray",
699
+ flexDirection: "column",
700
+ paddingX: 1,
701
+ marginRight: 1,
702
+ children: [
703
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: s.label }),
704
+ /* @__PURE__ */ jsxs3(Text4, { bold: true, color: s.color, children: [
705
+ s.value,
706
+ s.hint ? /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
707
+ " ",
708
+ s.hint
709
+ ] }) : null
710
+ ] })
711
+ ]
712
+ },
713
+ s.label
714
+ )) });
715
+ }
716
+
717
+ // src/ui/Menu.tsx
718
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
719
+ function Indicator({ isSelected }) {
720
+ return /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: isSelected ? "\u25B8 " : " " });
721
+ }
722
+ function Button({ isSelected, label }) {
723
+ return /* @__PURE__ */ jsx5(Text5, { color: isSelected ? "black" : theme.accent, backgroundColor: isSelected ? theme.accent : void 0, bold: true, children: ` ${label} ` });
724
+ }
725
+ function Menu({
726
+ projects,
727
+ totals,
728
+ agentName,
729
+ onSelect
730
+ }) {
731
+ const range = totals.spanStart && totals.spanEnd ? `${totals.spanStart} \u2192 ${totals.spanEnd}` : "no dated activity";
732
+ const last30 = totals.daily.slice(-30).map((d) => d.costUsd);
733
+ const unused = totals.servers.filter((s) => s.unused).length;
734
+ const items = [
735
+ { key: "list", label: "Browse projects", value: "list" },
736
+ { key: "aggregate", label: "All projects (aggregate)", value: "aggregate" },
737
+ { key: "quit", label: "Quit", value: "quit" }
738
+ ];
739
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
740
+ /* @__PURE__ */ jsx5(
741
+ Banner,
742
+ {
743
+ subtitle: `${agentName ? `${agentName} \xB7 ` : ""}where your agent's tokens and dollars go \xB7 100% offline`
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsx5(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx5(
747
+ StatCards,
748
+ {
749
+ stats: [
750
+ { label: "Total cost", value: usd(totals.totalCostUsd), color: theme.accent },
751
+ { label: "Projected", value: usd(totals.monthlyProjectionUsd), hint: "/mo" },
752
+ { label: "Projects", value: String(projects.length) },
753
+ { label: "Sessions", value: String(totals.sessionCount) },
754
+ { label: "Turns", value: compact(totals.totalTurns) },
755
+ {
756
+ label: "Cache hit",
757
+ value: pct1(totals.cacheHitRate),
758
+ color: totals.cacheHitRate > 0.8 ? theme.good : theme.warn
759
+ }
760
+ ]
761
+ }
762
+ ) }),
763
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
764
+ /* @__PURE__ */ jsxs4(Text5, { children: [
765
+ /* @__PURE__ */ jsx5(Sparkline, { values: last30.length > 0 ? last30 : [0] }),
766
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
767
+ " activity (last ",
768
+ Math.max(1, last30.length),
769
+ " active days)"
770
+ ] })
771
+ ] }),
772
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: range }),
773
+ unused > 0 ? /* @__PURE__ */ jsxs4(Text5, { color: theme.bad, children: [
774
+ "\u26A0 ",
775
+ unused,
776
+ " configured MCP server",
777
+ unused > 1 ? "s" : "",
778
+ " never called"
779
+ ] }) : null
780
+ ] }),
781
+ /* @__PURE__ */ jsx5(SelectInput, { items, onSelect: (item) => onSelect(item.value), indicatorComponent: Indicator, itemComponent: Button })
782
+ ] });
783
+ }
784
+
785
+ // src/ui/ProjectList.tsx
786
+ import { useMemo, useState as useState3 } from "react";
787
+ import { Box as Box5, Text as Text6, useInput as useInput2 } from "ink";
788
+
789
+ // src/ui/bars.ts
790
+ function bar(value, max, width = 20) {
791
+ if (max <= 0 || value <= 0) return "";
792
+ return "\u2588".repeat(Math.max(1, Math.round(value / max * width)));
793
+ }
794
+
795
+ // src/ui/ProjectList.tsx
796
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
797
+ var COST_W = 10;
798
+ var SESS_W = 6;
799
+ var WHEN_W = 11;
800
+ var BAR_W = 14;
801
+ var SORT_CYCLE = ["cost", "recent", "name"];
802
+ function sortProjects(projects, mode) {
803
+ const copy = [...projects];
804
+ if (mode === "cost") copy.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
805
+ if (mode === "recent") copy.sort((a, b) => (b.report.spanEnd ?? "").localeCompare(a.report.spanEnd ?? ""));
806
+ if (mode === "name") copy.sort((a, b) => a.label.localeCompare(b.label));
807
+ return copy;
808
+ }
809
+ function displayLabels(projects) {
810
+ const counts = /* @__PURE__ */ new Map();
811
+ for (const p of projects) counts.set(p.label, (counts.get(p.label) ?? 0) + 1);
812
+ const out = /* @__PURE__ */ new Map();
813
+ for (const p of projects) {
814
+ if ((counts.get(p.label) ?? 0) > 1) {
815
+ const parts = p.name.replace(/\\/g, "/").split("/").filter(Boolean);
816
+ const parent = parts.at(-2);
817
+ out.set(p.id, parent ? `${p.label} (${parent})` : p.label);
818
+ } else {
819
+ out.set(p.id, p.label);
820
+ }
821
+ }
822
+ return out;
823
+ }
824
+ function ProjectList({
825
+ projects,
826
+ agentName,
827
+ onSelect,
828
+ onAggregate,
829
+ onFilteringChange
830
+ }) {
831
+ const [cursor, setCursor] = useState3(0);
832
+ const [filter, setFilter] = useState3("");
833
+ const [filtering, setFilteringRaw] = useState3(false);
834
+ const [sort, setSort] = useState3("cost");
835
+ const { cols, rows } = useTerminalSize();
836
+ const setFiltering = (v) => {
837
+ setFilteringRaw(v);
838
+ onFilteringChange?.(v);
839
+ };
840
+ const labels = useMemo(() => displayLabels(projects), [projects]);
841
+ const filtered = useMemo(() => {
842
+ const sorted = sortProjects(projects, sort);
843
+ if (!filter) return sorted;
844
+ const q = filter.toLowerCase();
845
+ return sorted.filter(
846
+ (p) => p.label.toLowerCase().includes(q) || p.name.toLowerCase().includes(q)
847
+ );
848
+ }, [projects, sort, filter]);
849
+ const pinnedShown = !filter;
850
+ const rowCount = filtered.length + (pinnedShown ? 1 : 0);
851
+ const clamped = Math.min(cursor, Math.max(0, rowCount - 1));
852
+ useInput2((input, key) => {
853
+ if (filtering) {
854
+ if (key.return) setFiltering(false);
855
+ else if (key.escape) {
856
+ setFiltering(false);
857
+ setFilter("");
858
+ } else if (key.backspace || key.delete) setFilter((f) => f.slice(0, -1));
859
+ else if (input && !key.ctrl && !key.meta) setFilter((f) => f + input);
860
+ setCursor(0);
861
+ return;
862
+ }
863
+ if (input === "/") {
864
+ setFiltering(true);
865
+ return;
866
+ }
867
+ if (input === "a") {
868
+ onAggregate();
869
+ return;
870
+ }
871
+ if (input === "s") {
872
+ setSort((m) => SORT_CYCLE[(SORT_CYCLE.indexOf(m) + 1) % SORT_CYCLE.length]);
873
+ setCursor(0);
874
+ return;
875
+ }
876
+ if (key.upArrow) setCursor(() => Math.max(0, clamped - 1));
877
+ if (key.downArrow) setCursor(() => Math.min(rowCount - 1, clamped + 1));
878
+ if (key.return) {
879
+ if (pinnedShown && clamped === 0) onAggregate();
880
+ else {
881
+ const p = filtered[clamped - (pinnedShown ? 1 : 0)];
882
+ if (p) onSelect(p);
883
+ }
884
+ }
885
+ });
886
+ const overhead = 8;
887
+ const avail = cols - overhead;
888
+ const showBar = avail >= 24 + SESS_W + WHEN_W + COST_W + 2 + 8;
889
+ const showWhen = avail >= 20 + SESS_W + WHEN_W + COST_W;
890
+ const showSess = avail >= 16 + SESS_W + COST_W;
891
+ const fixed = COST_W + (showSess ? SESS_W : 0) + (showWhen ? WHEN_W : 0) + (showBar ? BAR_W + 2 : 0);
892
+ const nameW = Math.max(10, Math.min(42, avail - fixed - 2));
893
+ const barW = showBar ? Math.min(BAR_W + Math.max(0, avail - fixed - 2 - nameW), 30) : 0;
894
+ const visibleRows = Math.max(4, Math.min(20, rows - 10));
895
+ const clip = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "\u2026" : s;
896
+ const total = projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
897
+ const max = Math.max(0, ...projects.map((p) => p.report.totalCostUsd));
898
+ const projCursor = clamped - (pinnedShown ? 1 : 0);
899
+ const offset = Math.max(0, Math.min(projCursor - Math.floor(visibleRows / 2), filtered.length - visibleRows));
900
+ const visible = filtered.slice(offset, offset + visibleRows);
901
+ const line = (label, sess, when, cost, share, selected) => (selected ? "\u25B8 " : " ") + clip(label).padEnd(nameW) + (showSess ? sess.padStart(SESS_W) : "") + (showWhen ? when.padStart(WHEN_W) : "") + cost.padStart(COST_W) + (showBar ? " " + share : "");
902
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, children: [
903
+ /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, flexDirection: "column", children: [
904
+ /* @__PURE__ */ jsxs5(Text6, { bold: true, color: theme.accent, children: [
905
+ "Projects",
906
+ agentName ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
907
+ " \u2014 ",
908
+ agentName
909
+ ] }) : null
910
+ ] }),
911
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
912
+ filtered.length,
913
+ filter ? `/${projects.length}` : "",
914
+ " project",
915
+ filtered.length === 1 && !filter ? "" : "s",
916
+ " \xB7",
917
+ " ",
918
+ usd(total),
919
+ " total \xB7 sorted by ",
920
+ sort,
921
+ filtering || filter ? /* @__PURE__ */ jsxs5(Text6, { children: [
922
+ " \xB7 filter: ",
923
+ /* @__PURE__ */ jsx6(Text6, { color: theme.warn, children: filter || "\u2026" }),
924
+ filtering ? /* @__PURE__ */ jsx6(Text6, { color: theme.warn, children: "\u258C" }) : null
925
+ ] }) : null
926
+ ] })
927
+ ] }),
928
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " + "PROJECT".padEnd(nameW) + (showSess ? "SESS".padStart(SESS_W) : "") + (showWhen ? "LAST".padStart(WHEN_W) : "") + "COST".padStart(COST_W) + (showBar ? " SHARE" : "") }),
929
+ /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [
930
+ pinnedShown ? /* @__PURE__ */ jsx6(Text6, { color: clamped === 0 ? theme.accent : void 0, bold: clamped === 0, children: line("All projects", String(projects.reduce((s, p) => s + p.report.sessionCount, 0)), "", usd(total), "", clamped === 0) }) : null,
931
+ filtered.length === 0 ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
932
+ 'no projects match "',
933
+ filter,
934
+ '"'
935
+ ] }) : null,
936
+ visible.map((p, i) => {
937
+ const idx = offset + i;
938
+ const selected = idx === projCursor;
939
+ return /* @__PURE__ */ jsx6(Text6, { color: selected ? theme.accent : void 0, bold: selected, children: line(
940
+ labels.get(p.id) ?? p.label,
941
+ String(p.report.sessionCount),
942
+ relativeDate(p.report.spanEnd),
943
+ usd(p.report.totalCostUsd),
944
+ bar(p.report.totalCostUsd, max, barW),
945
+ selected
946
+ ) }, p.id);
947
+ }),
948
+ filtered.length > visibleRows ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
949
+ "\u2026",
950
+ offset + visibleRows < filtered.length ? ` ${filtered.length - offset - visibleRows} below` : "",
951
+ offset > 0 ? ` \xB7 ${offset} above` : ""
952
+ ] }) : null
953
+ ] })
954
+ ] });
955
+ }
956
+
957
+ // src/ui/Dashboard.tsx
958
+ import { useState as useState4 } from "react";
959
+ import { Box as Box7, Text as Text8, useInput as useInput3 } from "ink";
960
+
961
+ // src/ui/BarChart.tsx
962
+ import { Box as Box6, Text as Text7 } from "ink";
963
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
964
+ function BarChart({
965
+ rows,
966
+ width = 22,
967
+ showShare = false,
968
+ maxLabel = 34
969
+ }) {
970
+ const max = Math.max(0, ...rows.map((r) => r.value));
971
+ const total = rows.reduce((s, r) => s + r.value, 0);
972
+ const clip = (s) => s.length > maxLabel ? "\u2026" + s.slice(-(maxLabel - 1)) : s;
973
+ const labelW = Math.min(maxLabel, Math.max(0, ...rows.map((r) => r.label.length)));
974
+ const dispW = Math.max(0, ...rows.map((r) => (r.display ?? String(r.value)).length));
975
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", children: rows.map((r) => /* @__PURE__ */ jsxs6(Text7, { children: [
976
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: clip(r.label).padEnd(labelW) }),
977
+ " ",
978
+ /* @__PURE__ */ jsx7(Text7, { color: r.color ?? "cyan", children: bar(r.value, max, width).padEnd(width) }),
979
+ " ",
980
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: (r.display ?? String(r.value)).padStart(dispW) }),
981
+ showShare && total > 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${pct(r.value / total).padStart(4)}` }) : null
982
+ ] }, r.label)) });
983
+ }
984
+
985
+ // src/ui/Dashboard.tsx
986
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
987
+ var TABS = ["Overview", "Models", "Tools", "Servers", "Sessions", "Activity"];
988
+ function TabBar({ tab, narrow }) {
989
+ return /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: TABS.map((t, i) => /* @__PURE__ */ jsx8(
990
+ Text8,
991
+ {
992
+ color: i === tab ? "black" : "gray",
993
+ backgroundColor: i === tab ? theme.accent : void 0,
994
+ bold: i === tab,
995
+ children: narrow ? ` ${i + 1}${i === tab ? ` ${t}` : ""} ` : ` ${i + 1} ${t} `
996
+ },
997
+ t
998
+ )) });
999
+ }
1000
+ function lastDays(r, n) {
1001
+ const byDate = new Map(r.daily.map((d) => [d.date, d]));
1002
+ const out = [];
1003
+ const today = r.spanEnd ? Date.parse(r.spanEnd) : Date.now();
1004
+ for (let i = n - 1; i >= 0; i--) {
1005
+ const date = new Date(today - i * 864e5).toISOString().slice(0, 10);
1006
+ const d = byDate.get(date);
1007
+ out.push({ date, costUsd: d?.costUsd ?? 0, turns: d?.turns ?? 0 });
1008
+ }
1009
+ return out;
1010
+ }
1011
+ function Section({ title, children }) {
1012
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
1013
+ /* @__PURE__ */ jsx8(Text8, { bold: true, dimColor: true, children: title.toUpperCase() }),
1014
+ children
1015
+ ] });
1016
+ }
1017
+ function Overview({ r, chartW }) {
1018
+ const days = lastDays(r, 30);
1019
+ const peak = days.reduce((a, b) => b.costUsd > a.costUsd ? b : a, days[0]);
1020
+ const models = Object.entries(r.costByModel).sort(([, a], [, b]) => b.total - a.total).slice(0, 4).map(([m, c]) => ({ label: shortModel(m), value: c.total, display: usd(c.total), color: modelColor(m) }));
1021
+ const tools = Object.entries(r.toolCalls).sort(([, a], [, b]) => b - a).slice(0, 5).map(([n, c]) => ({ label: n, value: c, display: compact(c), color: n.startsWith("mcp__") ? theme.mcp : theme.accent }));
1022
+ const unused = r.servers.filter((s) => s.unused).length;
1023
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1024
+ /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx8(
1025
+ StatCards,
1026
+ {
1027
+ stats: [
1028
+ { label: "Total cost", value: usd(r.totalCostUsd), color: theme.accent },
1029
+ { label: "Projected", value: usd(r.monthlyProjectionUsd), hint: "/mo" },
1030
+ { label: "Sessions", value: String(r.sessionCount) },
1031
+ { label: "Turns", value: compact(r.totalTurns) },
1032
+ { label: "Cache hit", value: pct1(r.cacheHitRate), color: r.cacheHitRate > 0.8 ? theme.good : theme.warn },
1033
+ { label: "Cache saved", value: usd(r.cacheSavingsUsd), color: theme.good }
1034
+ ]
1035
+ }
1036
+ ) }),
1037
+ /* @__PURE__ */ jsx8(Section, { title: "last 30 days", children: /* @__PURE__ */ jsxs7(Box7, { children: [
1038
+ /* @__PURE__ */ jsx8(Sparkline, { values: days.map((d) => d.costUsd) }),
1039
+ peak && peak.costUsd > 0 ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1040
+ " peak ",
1041
+ usd(peak.costUsd),
1042
+ " on ",
1043
+ peak.date
1044
+ ] }) : null
1045
+ ] }) }),
1046
+ models.length > 0 ? /* @__PURE__ */ jsx8(Section, { title: "cost by model", children: /* @__PURE__ */ jsx8(BarChart, { rows: models, width: chartW, showShare: true }) }) : null,
1047
+ tools.length > 0 ? /* @__PURE__ */ jsx8(Section, { title: "top tools", children: /* @__PURE__ */ jsx8(BarChart, { rows: tools, width: chartW }) }) : null,
1048
+ unused > 0 ? /* @__PURE__ */ jsxs7(Text8, { color: theme.bad, children: [
1049
+ "\u26A0 ",
1050
+ unused,
1051
+ " MCP server",
1052
+ unused > 1 ? "s" : "",
1053
+ " configured but never called \u2014 see tab 4"
1054
+ ] }) : null
1055
+ ] });
1056
+ }
1057
+ function Models({ r, cols }) {
1058
+ const entries = Object.entries(r.usageByModel).sort(
1059
+ ([a], [b]) => r.costByModel[b].total - r.costByModel[a].total
1060
+ );
1061
+ if (entries.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no usage recorded" });
1062
+ const nameW = Math.max(...entries.map(([m]) => shortModel(m).length), 5);
1063
+ const col = (s) => s.padStart(10);
1064
+ const full = cols >= nameW + 2 + 10 * 6 + 7;
1065
+ const mid = cols >= nameW + 2 + 10 * 3 + 7;
1066
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1067
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1068
+ "MODEL".padEnd(nameW + 2),
1069
+ full ? col("INPUT") + col("CACHE RD") + col("CACHE WR") : "",
1070
+ mid ? col("OUTPUT") + col("TURNS") : "",
1071
+ col("COST"),
1072
+ " SHARE"
1073
+ ] }),
1074
+ entries.map(([m, u]) => {
1075
+ const c = r.costByModel[m];
1076
+ const share = r.totalCostUsd > 0 ? c.total / r.totalCostUsd : 0;
1077
+ return /* @__PURE__ */ jsxs7(Text8, { children: [
1078
+ /* @__PURE__ */ jsx8(Text8, { color: modelColor(m), bold: true, children: shortModel(m).padEnd(nameW + 2) }),
1079
+ full ? col(compact(u.inputTokens)) + col(compact(u.cacheReadTokens)) + col(compact(u.cacheCreationTokens)) : "",
1080
+ mid ? col(compact(u.outputTokens)) + col(compact(u.turns)) : "",
1081
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: col(usd(c.total)) }),
1082
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${pct(share).padStart(4)}` })
1083
+ ] }, m);
1084
+ }),
1085
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1086
+ "cost split \u2014 in ",
1087
+ usd(entries.reduce((s, [m]) => s + r.costByModel[m].input, 0)),
1088
+ " \xB7 cache rd",
1089
+ " ",
1090
+ usd(entries.reduce((s, [m]) => s + r.costByModel[m].cacheRead, 0)),
1091
+ " \xB7 cache wr",
1092
+ " ",
1093
+ usd(entries.reduce((s, [m]) => s + r.costByModel[m].cacheWrite, 0)),
1094
+ " \xB7 out",
1095
+ " ",
1096
+ usd(entries.reduce((s, [m]) => s + r.costByModel[m].output, 0))
1097
+ ] }) })
1098
+ ] });
1099
+ }
1100
+ function Tools({ r, chartW, cols }) {
1101
+ const costs = r.toolCostUsd ?? {};
1102
+ const anyCost = Object.values(costs).some((c) => c > 0);
1103
+ const rows = Object.entries(r.toolCalls).map(([n, calls]) => ({ name: n, calls, cost: costs[n] ?? 0 })).sort((a, b) => anyCost ? b.cost - a.cost : b.calls - a.calls).slice(0, 15).map((t) => ({
1104
+ label: t.name,
1105
+ value: anyCost ? t.cost : t.calls,
1106
+ display: anyCost ? `${usd(t.cost)} \xB7 ${compact(t.calls)} calls` : compact(t.calls),
1107
+ color: t.name.startsWith("mcp__") ? theme.mcp : theme.accent
1108
+ }));
1109
+ if (rows.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no tool calls" });
1110
+ const maxLabel = Math.max(12, Math.min(40, cols - chartW - (anyCost ? 30 : 16)));
1111
+ const totalCalls = Object.values(r.toolCalls).reduce((s, n) => s + n, 0);
1112
+ const mcpCalls = Object.entries(r.toolCalls).filter(([n]) => n.startsWith("mcp__")).reduce((s, [, n]) => s + n, 0);
1113
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1114
+ /* @__PURE__ */ jsx8(BarChart, { rows, width: chartW, showShare: true, maxLabel }),
1115
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1116
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1117
+ compact(totalCalls),
1118
+ " calls across ",
1119
+ Object.keys(r.toolCalls).length,
1120
+ " tools \xB7",
1121
+ " ",
1122
+ /* @__PURE__ */ jsx8(Text8, { color: theme.mcp, children: "magenta = MCP" }),
1123
+ " (",
1124
+ pct(totalCalls > 0 ? mcpCalls / totalCalls : 0),
1125
+ " of calls)"
1126
+ ] }),
1127
+ anyCost ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "est. cost = each turn's bill split across the tools that turn called" }) : null
1128
+ ] })
1129
+ ] });
1130
+ }
1131
+ function Servers({ r, cols }) {
1132
+ if (r.servers.length === 0)
1133
+ return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no MCP servers configured or observed for this project" });
1134
+ const pretty = (s) => !s.configured && s.name.startsWith("plugin_") ? s.name.replace(/^plugin_/, "").replace("_", ":") : s.name;
1135
+ const nameW = Math.min(30, Math.max(...r.servers.map((s) => pretty(s).length), 6));
1136
+ const sorted = [...r.servers].sort((a, b) => (b.estCostUsd ?? 0) - (a.estCostUsd ?? 0));
1137
+ const clipName = (n) => n.length > nameW ? n.slice(0, nameW - 1) + "\u2026" : n;
1138
+ const wide = cols >= 80;
1139
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1140
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1141
+ "SERVER".padEnd(nameW),
1142
+ "CALLS".padStart(8),
1143
+ "EST COST".padStart(10),
1144
+ " STATUS ",
1145
+ wide ? "SOURCE" : ""
1146
+ ] }),
1147
+ sorted.map((s) => /* @__PURE__ */ jsxs7(Text8, { children: [
1148
+ /* @__PURE__ */ jsx8(Text8, { color: s.configured ? theme.accent : theme.mcp, children: clipName(pretty(s)).padEnd(nameW) }),
1149
+ compact(s.callsObserved).padStart(8),
1150
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: usd(s.estCostUsd ?? 0).padStart(10) }),
1151
+ " ",
1152
+ s.unused ? /* @__PURE__ */ jsx8(Text8, { color: theme.bad, children: "\u25CF UNUSED" }) : /* @__PURE__ */ jsx8(Text8, { color: theme.good, children: "\u25CF used " }),
1153
+ wide ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + (s.configured ? s.source.replace(/\\/g, "/") : "plugin / external config") }) : null
1154
+ ] }, s.name)),
1155
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1156
+ sorted.some((s) => s.unused) ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Unused servers still load their tool schemas into every request \u2014 dead weight in the context window. Consider removing them." }) : null,
1157
+ sorted.some((s) => !s.configured) ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1158
+ /* @__PURE__ */ jsx8(Text8, { color: theme.mcp, children: "magenta" }),
1159
+ " servers were seen in transcripts but live outside .mcp.json / ~/.claude.json (plugins, managed configs)."
1160
+ ] }) : null,
1161
+ sorted.some((s) => (s.estCostUsd ?? 0) > 0) ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "est. cost = each turn's bill split across the tools that turn called" }) : null
1162
+ ] })
1163
+ ] });
1164
+ }
1165
+ function Sessions({ r, cols }) {
1166
+ const rows = r.sessions.slice(0, 12);
1167
+ if (rows.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no sessions" });
1168
+ const wide = cols >= 60;
1169
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1170
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1171
+ "WHEN".padEnd(12),
1172
+ "LENGTH".padEnd(9),
1173
+ wide ? "TURNS".padStart(6) + "TOOLS".padStart(7) : "",
1174
+ "COST".padStart(9),
1175
+ " MODEL"
1176
+ ] }),
1177
+ rows.map((s) => /* @__PURE__ */ jsxs7(Text8, { children: [
1178
+ relativeDate(s.start).padEnd(12),
1179
+ duration(s.start, s.end).padEnd(9),
1180
+ wide ? compact(s.turns).padStart(6) + compact(s.toolCallCount).padStart(7) : "",
1181
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: usd(s.costUsd).padStart(9) }),
1182
+ /* @__PURE__ */ jsx8(Text8, { color: s.models[0] ? modelColor(s.models[0]) : void 0, children: " " + (s.models[0] ? shortModel(s.models[0]) : "\u2014") })
1183
+ ] }, s.file)),
1184
+ r.sessions.length > rows.length ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1185
+ "\u2026 ",
1186
+ r.sessions.length - rows.length,
1187
+ " more (sorted by cost)"
1188
+ ] }) : null
1189
+ ] });
1190
+ }
1191
+ function Activity({ r, chartW, cols }) {
1192
+ const days = lastDays(r, 21).filter((d, i, all) => d.costUsd > 0 || i >= all.length - 14);
1193
+ const wide = cols >= 66;
1194
+ const rows = days.slice(-16).map((d) => ({
1195
+ label: `${d.date} ${relativeDate(d.date) === "today" ? "\u25C2" : " "}`,
1196
+ value: d.costUsd,
1197
+ display: d.costUsd > 0 ? wide ? `${usd(d.costUsd)} \xB7 ${compact(d.turns)} turns` : usd(d.costUsd) : "\u2014"
1198
+ }));
1199
+ if (r.daily.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no dated activity" });
1200
+ const avg = r.totalCostUsd / Math.max(1, r.daily.length);
1201
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1202
+ /* @__PURE__ */ jsx8(BarChart, { rows, width: chartW }),
1203
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1204
+ r.daily.length,
1205
+ " active days \xB7 avg ",
1206
+ usd(avg),
1207
+ "/active day \xB7 projected",
1208
+ " ",
1209
+ usd(r.monthlyProjectionUsd),
1210
+ "/mo"
1211
+ ] }) })
1212
+ ] });
1213
+ }
1214
+ function Dashboard({
1215
+ project,
1216
+ initialTab = 0,
1217
+ timeframe
1218
+ }) {
1219
+ const [tab, setTab] = useState4(initialTab);
1220
+ const { cols } = useTerminalSize();
1221
+ useInput3((input, key) => {
1222
+ const n = Number.parseInt(input, 10);
1223
+ if (n >= 1 && n <= TABS.length) setTab(n - 1);
1224
+ if (key.rightArrow) setTab((t) => (t + 1) % TABS.length);
1225
+ if (key.leftArrow) setTab((t) => (t + TABS.length - 1) % TABS.length);
1226
+ });
1227
+ const r = project.report;
1228
+ const span = r.spanStart && r.spanEnd ? `${r.spanStart} \u2192 ${r.spanEnd}` : `${r.spanDays}d`;
1229
+ const fullPath = project.name.replace(/\\/g, "/");
1230
+ const chartW = Math.max(10, Math.min(26, cols - 54));
1231
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, children: [
1232
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
1233
+ /* @__PURE__ */ jsxs7(Text8, { children: [
1234
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accent, children: project.label }),
1235
+ project.label !== fullPath && cols >= project.label.length + fullPath.length + 6 ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1236
+ " ",
1237
+ fullPath
1238
+ ] }) : null
1239
+ ] }),
1240
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
1241
+ timeframe && timeframe !== "All time" ? /* @__PURE__ */ jsxs7(Text8, { color: "yellow", children: [
1242
+ timeframe,
1243
+ " \xB7 "
1244
+ ] }) : null,
1245
+ r.sessionCount,
1246
+ " sessions \xB7 ",
1247
+ span,
1248
+ cols >= 70 ? " \xB7 API-equivalent cost, computed offline" : ""
1249
+ ] })
1250
+ ] }),
1251
+ /* @__PURE__ */ jsx8(TabBar, { tab, narrow: cols < 72 }),
1252
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1253
+ tab === 0 ? /* @__PURE__ */ jsx8(Overview, { r, chartW }) : null,
1254
+ tab === 1 ? /* @__PURE__ */ jsx8(Models, { r, cols }) : null,
1255
+ tab === 2 ? /* @__PURE__ */ jsx8(Tools, { r, chartW, cols }) : null,
1256
+ tab === 3 ? /* @__PURE__ */ jsx8(Servers, { r, cols }) : null,
1257
+ tab === 4 ? /* @__PURE__ */ jsx8(Sessions, { r, cols }) : null,
1258
+ tab === 5 ? /* @__PURE__ */ jsx8(Activity, { r, chartW, cols }) : null
1259
+ ] })
1260
+ ] });
1261
+ }
1262
+
1263
+ // src/ui/HelpOverlay.tsx
1264
+ import { Box as Box8, Text as Text9 } from "ink";
1265
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1266
+ var KEYS = [
1267
+ ["\u2191 \u2193", "move selection"],
1268
+ ["\u23CE", "open / select"],
1269
+ ["\u2190 \u2192 or 1\u20136", "switch dashboard tab"],
1270
+ ["/", "filter project list (esc clears)"],
1271
+ ["s", "cycle project sort: cost \xB7 recent \xB7 name"],
1272
+ ["t", "cycle timeframe: all \xB7 today \xB7 yesterday \xB7 7d \xB7 30d"],
1273
+ ["esc", "go back"],
1274
+ ["?", "toggle this help"],
1275
+ ["q", "quit"]
1276
+ ];
1277
+ function HelpOverlay() {
1278
+ return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "double", borderColor: "cyan", flexDirection: "column", paddingX: 2, paddingY: 1, alignSelf: "center", children: [
1279
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Keyboard shortcuts" }),
1280
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
1281
+ KEYS.map(([key, desc]) => /* @__PURE__ */ jsxs8(Text9, { children: [
1282
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: key.padEnd(14) }),
1283
+ /* @__PURE__ */ jsx9(Text9, { children: desc })
1284
+ ] }, key)),
1285
+ /* @__PURE__ */ jsx9(Text9, { children: " " }),
1286
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "All data is read locally from ~/.claude \u2014 nothing leaves this machine." })
1287
+ ] });
1288
+ }
1289
+
1290
+ // src/ui/App.tsx
1291
+ import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1292
+ var HINTS = {
1293
+ agents: "\u2191\u2193 navigate \xB7 \u23CE select \xB7 ? help \xB7 q quit",
1294
+ menu: "\u2191\u2193 navigate \xB7 \u23CE select \xB7 esc agents \xB7 ? help \xB7 q quit",
1295
+ list: "\u2191\u2193 move \xB7 \u23CE open \xB7 / filter \xB7 s sort \xB7 a all \xB7 esc back \xB7 ? help \xB7 q quit",
1296
+ project: "1\u20136 \u2190\u2192 tabs \xB7 esc back \xB7 ? help \xB7 q quit",
1297
+ aggregate: "1\u20136 \u2190\u2192 tabs \xB7 esc back \xB7 ? help \xB7 q quit"
1298
+ };
1299
+ function wrapProjects(projects) {
1300
+ return [
1301
+ {
1302
+ adapter: {
1303
+ id: "claude",
1304
+ name: "Claude Code",
1305
+ supported: true,
1306
+ detect: async () => true,
1307
+ loadProjects: async () => projects
1308
+ },
1309
+ detected: true,
1310
+ projects
1311
+ }
1312
+ ];
1313
+ }
1314
+ function App({
1315
+ agents,
1316
+ projects,
1317
+ initialView,
1318
+ initialSelected = 0
1319
+ }) {
1320
+ const { exit } = useApp();
1321
+ const agentList = useMemo2(
1322
+ () => agents ?? wrapProjects(projects ?? []),
1323
+ [agents, projects]
1324
+ );
1325
+ const multiAgent = agentList.length > 1;
1326
+ const firstWithData = Math.max(0, agentList.findIndex((a) => a.projects.length > 0));
1327
+ const [agentIdx, setAgentIdx] = useState5(firstWithData);
1328
+ const [view, setView] = useState5(initialView ?? (multiAgent ? "agents" : "menu"));
1329
+ const [aggFrom, setAggFrom] = useState5("menu");
1330
+ const activeProjects = agentList[agentIdx]?.projects ?? [];
1331
+ const [selected, setSelected] = useState5(
1332
+ activeProjects[initialSelected]
1333
+ );
1334
+ const [help, setHelp] = useState5(false);
1335
+ const [filterCapture, setFilterCapture] = useState5(false);
1336
+ const [timeframe, setTimeframe] = useState5("all");
1337
+ const scoped = useMemo2(
1338
+ () => applyTimeframe(activeProjects, timeframeRange(timeframe)),
1339
+ [activeProjects, timeframe]
1340
+ );
1341
+ const totals = useMemo2(() => aggregate(scoped), [scoped]);
1342
+ const agentName = agentList[agentIdx]?.adapter.name ?? "";
1343
+ useInput4((input, key) => {
1344
+ if (help) {
1345
+ setHelp(false);
1346
+ return;
1347
+ }
1348
+ if (filterCapture) return;
1349
+ if (input === "q") exit();
1350
+ if (input === "?") setHelp(true);
1351
+ if (input === "t" && view !== "agents") setTimeframe((tf) => nextTimeframe(tf));
1352
+ if (key.escape) {
1353
+ if (view === "project") setView("list");
1354
+ else if (view === "aggregate") setView(aggFrom);
1355
+ else if (view === "list") setView("menu");
1356
+ else if (view === "menu" && multiAgent) setView("agents");
1357
+ }
1358
+ });
1359
+ const anyData = agentList.some((a) => a.projects.length > 0);
1360
+ if (!anyData) return /* @__PURE__ */ jsx10(Text10, { children: "No agent usage data found (Claude Code, Codex, OpenCode\u2026)." });
1361
+ const tfLabel = timeframeLabel(timeframe);
1362
+ let content;
1363
+ if (help) {
1364
+ content = /* @__PURE__ */ jsx10(HelpOverlay, {});
1365
+ } else if (view === "agents") {
1366
+ content = /* @__PURE__ */ jsx10(
1367
+ AgentPicker,
1368
+ {
1369
+ agents: agentList,
1370
+ onSelect: (i) => {
1371
+ setAgentIdx(i);
1372
+ setSelected(void 0);
1373
+ setView("menu");
1374
+ }
1375
+ }
1376
+ );
1377
+ } else if (view === "menu") {
1378
+ content = /* @__PURE__ */ jsx10(
1379
+ Menu,
1380
+ {
1381
+ projects: scoped,
1382
+ totals,
1383
+ agentName,
1384
+ onSelect: (action) => {
1385
+ if (action === "quit") exit();
1386
+ else {
1387
+ if (action === "aggregate") setAggFrom("menu");
1388
+ setView(action);
1389
+ }
1390
+ }
1391
+ }
1392
+ );
1393
+ } else if (view === "aggregate") {
1394
+ content = /* @__PURE__ */ jsx10(
1395
+ Dashboard,
1396
+ {
1397
+ project: {
1398
+ id: "__all__",
1399
+ name: `All ${agentName} projects`,
1400
+ label: `All ${agentName} projects`,
1401
+ report: totals
1402
+ },
1403
+ timeframe: tfLabel
1404
+ }
1405
+ );
1406
+ } else if (view === "project" && selected) {
1407
+ const current = scoped.find((p) => p.id === selected.id);
1408
+ content = current ? /* @__PURE__ */ jsx10(Dashboard, { project: current, timeframe: tfLabel }) : /* @__PURE__ */ jsx10(Box9, { paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
1409
+ selected.label,
1410
+ ": no activity ",
1411
+ tfLabel.toLowerCase(),
1412
+ " \u2014 press t to change the timeframe or esc to go back."
1413
+ ] }) });
1414
+ } else {
1415
+ content = /* @__PURE__ */ jsx10(
1416
+ ProjectList,
1417
+ {
1418
+ projects: scoped,
1419
+ agentName,
1420
+ onSelect: (p) => {
1421
+ setSelected(p);
1422
+ setView("project");
1423
+ },
1424
+ onAggregate: () => {
1425
+ setAggFrom("list");
1426
+ setView("aggregate");
1427
+ },
1428
+ onFilteringChange: setFilterCapture
1429
+ }
1430
+ );
1431
+ }
1432
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", flexGrow: 1, children: [
1433
+ /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", flexGrow: 1, children: content }),
1434
+ /* @__PURE__ */ jsx10(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: help ? "any key to close help" : view === "agents" ? HINTS.agents : /* @__PURE__ */ jsxs9(Fragment, { children: [
1435
+ /* @__PURE__ */ jsxs9(Text10, { color: timeframe === "all" ? void 0 : "yellow", children: [
1436
+ "\u23F1 ",
1437
+ tfLabel
1438
+ ] }),
1439
+ " \xB7 t cycle \xB7 ",
1440
+ HINTS[view]
1441
+ ] }) }) })
1442
+ ] });
1443
+ }
1444
+
1445
+ // src/ui/Root.tsx
1446
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1447
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1448
+ function ProgressBar({ done, total, width = 30 }) {
1449
+ const filled = total > 0 ? Math.round(done / total * width) : 0;
1450
+ return /* @__PURE__ */ jsxs10(Text11, { children: [
1451
+ /* @__PURE__ */ jsx11(Text11, { color: theme.accent, children: "\u2588".repeat(filled) }),
1452
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2591".repeat(width - filled) })
1453
+ ] });
1454
+ }
1455
+ function Root() {
1456
+ const { exit } = useApp2();
1457
+ const [agents, setAgents] = useState6();
1458
+ const [agentName, setAgentName] = useState6("");
1459
+ const [progress, setProgress] = useState6({ parsed: 0, total: 0 });
1460
+ const [frame, setFrame] = useState6(0);
1461
+ useEffect2(() => {
1462
+ const timer = setInterval(() => setFrame((f) => f + 1), 80);
1463
+ loadAllAgents(void 0, (agent, p) => {
1464
+ setAgentName(agent);
1465
+ setProgress(p);
1466
+ }).then(setAgents).catch(() => setAgents([])).finally(() => clearInterval(timer));
1467
+ return () => clearInterval(timer);
1468
+ }, []);
1469
+ const empty = agents !== void 0 && !agents.some((a) => a.projects.length > 0);
1470
+ useEffect2(() => {
1471
+ if (empty) exit();
1472
+ }, [empty, exit]);
1473
+ if (!agents) {
1474
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
1475
+ /* @__PURE__ */ jsx11(Banner, { subtitle: "where your agents' tokens and dollars go \xB7 100% offline" }),
1476
+ /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
1477
+ /* @__PURE__ */ jsxs10(Text11, { children: [
1478
+ /* @__PURE__ */ jsx11(Text11, { color: theme.accent, children: SPINNER[frame % SPINNER.length] }),
1479
+ " Parsing",
1480
+ agentName ? ` ${agentName}` : "",
1481
+ "\u2026",
1482
+ " ",
1483
+ /* @__PURE__ */ jsxs10(Text11, { bold: true, children: [
1484
+ progress.parsed,
1485
+ "/",
1486
+ progress.total || "?"
1487
+ ] })
1488
+ ] }),
1489
+ /* @__PURE__ */ jsx11(ProgressBar, { done: progress.parsed, total: progress.total }),
1490
+ progress.currentProject ? /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: progress.currentProject.replace(/\\/g, "/") }) : null
1491
+ ] })
1492
+ ] });
1493
+ }
1494
+ if (empty) return /* @__PURE__ */ jsx11(Text11, { children: "No agent usage data found (Claude Code, Codex, OpenCode\u2026)." });
1495
+ return /* @__PURE__ */ jsx11(App, { agents });
1496
+ }
1497
+ export {
1498
+ Root
1499
+ };