@tokz/cli 0.2.3 → 0.2.5

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.
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ findMcpServers
4
+ } from "./chunk-65BQE6TI.js";
2
5
  import {
3
6
  addUsage,
4
7
  buildReport,
@@ -6,9 +9,10 @@ import {
6
9
  cacheSavings,
7
10
  compact,
8
11
  costUsd,
12
+ dayKey,
9
13
  duration,
10
14
  emptyUsage,
11
- findMcpServers,
15
+ groupDaily,
12
16
  parseTranscript,
13
17
  pct,
14
18
  pct1,
@@ -16,19 +20,19 @@ import {
16
20
  sanitizeProjectPath,
17
21
  shortModel,
18
22
  usd
19
- } from "./chunk-JAZOB7NC.js";
23
+ } from "./chunk-Z5W5QH2Z.js";
20
24
 
21
25
  // src/ui/Root.tsx
22
26
  import { useEffect as useEffect2, useState as useState6 } from "react";
23
27
  import { Box as Box10, Text as Text11, useApp as useApp2 } from "ink";
24
28
 
25
29
  // 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";
30
+ import { access as access15 } from "fs/promises";
31
+ import { homedir as homedir16 } from "os";
32
+ import { join as join16 } from "path";
29
33
 
30
- // src/agents/claude.ts
31
- import { access } from "fs/promises";
34
+ // src/agents/antigravity.ts
35
+ import { access, readFile as readFile2, readdir } from "fs/promises";
32
36
  import { homedir as homedir2 } from "os";
33
37
  import { join as join2 } from "path";
34
38
 
@@ -47,6 +51,24 @@ function baseName(p) {
47
51
  const parts = p.replace(/\\/g, "/").split("/").filter(Boolean);
48
52
  return parts.at(-1) ?? p;
49
53
  }
54
+ var UNKNOWN_PROJECT = "(unknown project)";
55
+ function groupSessionsByCwd(agentId, sessions) {
56
+ const byCwd = /* @__PURE__ */ new Map();
57
+ for (const s of sessions) {
58
+ if (Object.keys(s.usageByModel).length === 0) continue;
59
+ const key = s.cwd ?? UNKNOWN_PROJECT;
60
+ (byCwd.get(key) ?? byCwd.set(key, []).get(key)).push(s);
61
+ }
62
+ return [...byCwd].map(([cwd, group]) => ({
63
+ id: `${agentId}:${cwd}`,
64
+ name: cwd,
65
+ label: cwd === UNKNOWN_PROJECT ? cwd : baseName(cwd),
66
+ realPath: cwd === UNKNOWN_PROJECT ? void 0 : cwd,
67
+ report: buildReport(group, []),
68
+ sessions: group,
69
+ serverList: []
70
+ })).sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
71
+ }
50
72
  var DAY_MS = 864e5;
51
73
  function aggregate(projects) {
52
74
  const usageByModel = {};
@@ -175,14 +197,167 @@ async function loadProjects(home = homedir(), onProgress) {
175
197
  return out;
176
198
  }
177
199
 
200
+ // src/agents/antigravity.ts
201
+ var ESTIMATED_CHARS_PER_TOKEN = 4;
202
+ var INPUT_SHARE = 0.85;
203
+ function antigravityRoot(home) {
204
+ return join2(home ?? homedir2(), ".gemini", "antigravity-cli");
205
+ }
206
+ function modelIdFromDisplayName(name) {
207
+ const bare = name.replace(/\s*\(.*\)\s*$/, "").trim();
208
+ const slug = bare.toLowerCase().replace(/\s+/g, "-");
209
+ return slug.startsWith("claude") ? slug.replace(/\./g, "-") : slug;
210
+ }
211
+ var MODEL_NAME_RX = /(Gemini|Claude|GPT)[- ][0-9][0-9A-Za-z. ]{0,25}(\((Low|Medium|High|Max|Thinking)\))?/;
212
+ function scanConversation(buf) {
213
+ const text = buf.toString("latin1");
214
+ const turnsByModel = {};
215
+ const anchor = /used_claude_conservative/g;
216
+ let m;
217
+ while (m = anchor.exec(text)) {
218
+ const window = text.slice(m.index + 24, m.index + 250);
219
+ const name = window.match(MODEL_NAME_RX);
220
+ const key = name ? modelIdFromDisplayName(name[0]) : "";
221
+ turnsByModel[key] = (turnsByModel[key] ?? 0) + 1;
222
+ }
223
+ let textChars = 0;
224
+ let start = -1;
225
+ for (let i = 0; i <= buf.length; i++) {
226
+ const c = i < buf.length ? buf[i] : 0;
227
+ const printable = c >= 32 && c < 127;
228
+ if (printable && start < 0) start = i;
229
+ if (!printable && start >= 0) {
230
+ if (i - start >= 20) textChars += i - start;
231
+ start = -1;
232
+ }
233
+ }
234
+ return { turnsByModel, textChars };
235
+ }
236
+ async function readHistory(root) {
237
+ try {
238
+ const raw = await readFile2(join2(root, "history.jsonl"), "utf8");
239
+ const out = [];
240
+ for (const line of raw.split("\n")) {
241
+ if (!line.trim()) continue;
242
+ try {
243
+ out.push(JSON.parse(line));
244
+ } catch {
245
+ }
246
+ }
247
+ return out;
248
+ } catch {
249
+ return [];
250
+ }
251
+ }
252
+ async function loadAntigravityProjects(home, onProgress) {
253
+ const root = antigravityRoot(home);
254
+ const convDir = join2(root, "conversations");
255
+ let dbFiles;
256
+ try {
257
+ dbFiles = (await readdir(convDir)).filter((f) => f.endsWith(".db"));
258
+ } catch {
259
+ return [];
260
+ }
261
+ if (dbFiles.length === 0) return [];
262
+ const history = await readHistory(root);
263
+ const byConversation = /* @__PURE__ */ new Map();
264
+ for (const e of history) {
265
+ if (!e.conversationId || !e.timestamp) continue;
266
+ const c = byConversation.get(e.conversationId) ?? {};
267
+ c.workspace ??= e.workspace;
268
+ if (!c.firstTs || e.timestamp < c.firstTs) c.firstTs = e.timestamp;
269
+ if (!c.lastTs || e.timestamp > c.lastTs) c.lastTs = e.timestamp;
270
+ byConversation.set(e.conversationId, c);
271
+ }
272
+ const sessions = [];
273
+ let parsed = 0;
274
+ for (const file of dbFiles) {
275
+ onProgress?.({ parsed, total: dbFiles.length, currentProject: "Antigravity conversations" });
276
+ let scan;
277
+ try {
278
+ scan = scanConversation(await readFile2(join2(convDir, file)));
279
+ } catch {
280
+ parsed += 1;
281
+ continue;
282
+ }
283
+ parsed += 1;
284
+ onProgress?.({ parsed, total: dbFiles.length, currentProject: "Antigravity conversations" });
285
+ const named = Object.entries(scan.turnsByModel).filter(([k]) => k !== "");
286
+ named.sort((a, b) => b[1] - a[1]);
287
+ const fallback = named[0]?.[0] ?? "antigravity-unknown";
288
+ const turnsByModel = {};
289
+ for (const [model, turns] of Object.entries(scan.turnsByModel)) {
290
+ const key = model === "" ? fallback : model;
291
+ turnsByModel[key] = (turnsByModel[key] ?? 0) + turns;
292
+ }
293
+ const totalTurns = Object.values(turnsByModel).reduce((s, n) => s + n, 0);
294
+ if (totalTurns === 0) continue;
295
+ const conversationId = file.replace(/\.db$/, "");
296
+ const meta = byConversation.get(conversationId);
297
+ const firstTs = meta?.firstTs ? new Date(meta.firstTs).toISOString() : void 0;
298
+ const lastTs = meta?.lastTs ? new Date(meta.lastTs).toISOString() : void 0;
299
+ const day = (lastTs ?? firstTs)?.slice(0, 10);
300
+ const estTokens = Math.round(scan.textChars / ESTIMATED_CHARS_PER_TOKEN);
301
+ const stats = {
302
+ file: conversationId,
303
+ cwd: meta?.workspace,
304
+ firstTs,
305
+ lastTs,
306
+ usageByModel: {},
307
+ toolCalls: {},
308
+ toolCostUsd: {},
309
+ dailyUsage: {}
310
+ };
311
+ for (const [model, turns] of Object.entries(turnsByModel)) {
312
+ const share = turns / totalTurns;
313
+ const u = stats.usageByModel[model] ??= emptyUsage();
314
+ u.inputTokens += Math.round(estTokens * share * INPUT_SHARE);
315
+ u.outputTokens += Math.round(estTokens * share * (1 - INPUT_SHARE));
316
+ u.turns += turns;
317
+ if (day) {
318
+ const d = (stats.dailyUsage[day] ??= {})[model] ??= emptyUsage();
319
+ d.inputTokens += u.inputTokens;
320
+ d.outputTokens += u.outputTokens;
321
+ d.turns += turns;
322
+ }
323
+ }
324
+ sessions.push(stats);
325
+ }
326
+ return groupSessionsByCwd("antigravity", sessions);
327
+ }
328
+ var antigravityAdapter = {
329
+ id: "antigravity",
330
+ name: "Antigravity",
331
+ supported: true,
332
+ estimated: true,
333
+ estimateNote: "no token counts on disk \u2014 tokens/costs estimated from conversation size",
334
+ async detect(home) {
335
+ try {
336
+ await access(join2(antigravityRoot(home), "conversations"));
337
+ return true;
338
+ } catch {
339
+ try {
340
+ await access(join2(home ?? homedir2(), ".gemini", "antigravity", "conversations"));
341
+ return true;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+ },
347
+ loadProjects: loadAntigravityProjects
348
+ };
349
+
178
350
  // src/agents/claude.ts
351
+ import { access as access2 } from "fs/promises";
352
+ import { homedir as homedir3 } from "os";
353
+ import { join as join3 } from "path";
179
354
  var claudeAdapter = {
180
355
  id: "claude",
181
356
  name: "Claude Code",
182
357
  supported: true,
183
- async detect(home = homedir2()) {
358
+ async detect(home = homedir3()) {
184
359
  try {
185
- await access(join2(home, ".claude", "projects"));
360
+ await access2(join3(home, ".claude", "projects"));
186
361
  return true;
187
362
  } catch {
188
363
  return false;
@@ -191,13 +366,178 @@ var claudeAdapter = {
191
366
  loadProjects: (home, onProgress) => loadProjects(home, onProgress)
192
367
  };
193
368
 
369
+ // src/agents/codebuff.ts
370
+ import { access as access3, stat } from "fs/promises";
371
+ import { homedir as homedir4 } from "os";
372
+ import { join as join4 } from "path";
373
+ import { glob as glob2 } from "tinyglobby";
374
+
375
+ // src/agents/usage.ts
376
+ import { readFile as readFile3 } from "fs/promises";
377
+ async function readJson(file) {
378
+ try {
379
+ return JSON.parse(await readFile3(file, "utf8"));
380
+ } catch {
381
+ return void 0;
382
+ }
383
+ }
384
+ async function readJsonl(file) {
385
+ let raw;
386
+ try {
387
+ raw = await readFile3(file, "utf8");
388
+ } catch {
389
+ return [];
390
+ }
391
+ const out = [];
392
+ for (const line of raw.split("\n")) {
393
+ if (!line.trim()) continue;
394
+ try {
395
+ out.push(JSON.parse(line));
396
+ } catch {
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+ function sessionFromRecords(file, cwd, records) {
402
+ const stats = { file, cwd, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
403
+ for (const r of records) {
404
+ if (r.input + r.output + r.cacheRead + r.cacheWrite === 0) continue;
405
+ if (r.ts) {
406
+ if (!stats.firstTs || r.ts < stats.firstTs) stats.firstTs = r.ts;
407
+ if (!stats.lastTs || r.ts > stats.lastTs) stats.lastTs = r.ts;
408
+ }
409
+ const accs = [stats.usageByModel[r.model] ??= emptyUsage()];
410
+ if (r.ts) {
411
+ const day = stats.dailyUsage[r.ts.slice(0, 10)] ??= {};
412
+ accs.push(day[r.model] ??= emptyUsage());
413
+ }
414
+ for (const u of accs) {
415
+ u.inputTokens += r.input;
416
+ u.outputTokens += r.output;
417
+ u.cacheReadTokens += r.cacheRead;
418
+ u.cacheCreationTokens += r.cacheWrite;
419
+ u.turns += 1;
420
+ }
421
+ }
422
+ return stats;
423
+ }
424
+ function pickNum(obj, keys) {
425
+ if (!obj || typeof obj !== "object") return 0;
426
+ const rec = obj;
427
+ for (const k of keys) {
428
+ const v = rec[k];
429
+ const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
430
+ if (Number.isFinite(n)) return Math.max(0, Math.trunc(n));
431
+ }
432
+ return 0;
433
+ }
434
+ function str(obj, key) {
435
+ if (!obj || typeof obj !== "object") return void 0;
436
+ const v = obj[key];
437
+ return typeof v === "string" && v.length > 0 ? v : void 0;
438
+ }
439
+ function toIso(value) {
440
+ if (typeof value === "string") {
441
+ const t = Date.parse(value);
442
+ return Number.isFinite(t) ? new Date(t).toISOString() : void 0;
443
+ }
444
+ if (typeof value === "number" || typeof value === "bigint") {
445
+ let ms = Number(value);
446
+ if (!Number.isFinite(ms) || ms <= 0) return void 0;
447
+ if (ms < 1e12) ms *= 1e3;
448
+ return new Date(ms).toISOString();
449
+ }
450
+ return void 0;
451
+ }
452
+ function deepFind(node, keys, depth = 6) {
453
+ if (!node || typeof node !== "object" || depth < 0) return void 0;
454
+ const rec = node;
455
+ if (!Array.isArray(node) && keys.some((k) => k in rec)) return rec;
456
+ for (const v of Object.values(rec)) {
457
+ const hit = deepFind(v, keys, depth - 1);
458
+ if (hit) return hit;
459
+ }
460
+ return void 0;
461
+ }
462
+
463
+ // src/agents/codebuff.ts
464
+ var CHANNELS = ["manicode", "manicode-dev", "manicode-staging"];
465
+ function codebuffRoots(home) {
466
+ const env = process.env.CODEBUFF_DATA_DIR;
467
+ if (env) return env.split(",").map((s) => s.trim()).filter(Boolean);
468
+ const h = home ?? homedir4();
469
+ return CHANNELS.map((c) => join4(h, ".config", c));
470
+ }
471
+ function isAssistant(msg) {
472
+ const role = str(msg, "variant") ?? str(msg, "role");
473
+ return role === "ai" || role === "agent" || role === "assistant";
474
+ }
475
+ async function parseFile(file) {
476
+ const arr = await readJson(file);
477
+ if (!Array.isArray(arr)) return sessionFromRecords(file, void 0, []);
478
+ let fileTs;
479
+ try {
480
+ fileTs = (await stat(file)).mtime.toISOString();
481
+ } catch {
482
+ }
483
+ const records = [];
484
+ for (const msg of arr) {
485
+ if (!isAssistant(msg)) continue;
486
+ const meta = msg.metadata;
487
+ const usage = meta?.usage;
488
+ if (!usage) continue;
489
+ records.push({
490
+ model: str(meta, "model") ?? "codebuff-unknown",
491
+ ts: str(msg, "timestamp") ?? fileTs,
492
+ input: pickNum(usage, ["inputTokens", "input_tokens"]),
493
+ output: pickNum(usage, ["outputTokens", "output_tokens"]),
494
+ cacheRead: pickNum(usage, ["cacheReadInputTokens", "cache_read_input_tokens", "cachedTokens"]),
495
+ cacheWrite: pickNum(usage, ["cacheCreationInputTokens", "cache_creation_input_tokens"])
496
+ });
497
+ }
498
+ return sessionFromRecords(file, void 0, records);
499
+ }
500
+ async function loadCodebuffProjects(home, onProgress) {
501
+ const files = [];
502
+ for (const root of codebuffRoots(home)) {
503
+ files.push(
504
+ ...await glob2(["**/chat-messages.json"], { cwd: root, absolute: true }).catch(() => [])
505
+ );
506
+ }
507
+ const sessions = [];
508
+ let parsed = 0;
509
+ for (const f of files) {
510
+ onProgress?.({ parsed, total: files.length, currentProject: "Codebuff chats" });
511
+ sessions.push(await parseFile(f));
512
+ parsed += 1;
513
+ onProgress?.({ parsed, total: files.length, currentProject: "Codebuff chats" });
514
+ }
515
+ return groupSessionsByCwd("codebuff", sessions);
516
+ }
517
+ var codebuffAdapter = {
518
+ id: "codebuff",
519
+ name: "Codebuff",
520
+ supported: true,
521
+ async detect(home) {
522
+ for (const root of codebuffRoots(home)) {
523
+ try {
524
+ await access3(root);
525
+ return true;
526
+ } catch {
527
+ }
528
+ }
529
+ return false;
530
+ },
531
+ loadProjects: loadCodebuffProjects
532
+ };
533
+
194
534
  // src/agents/codex.ts
195
535
  import { createReadStream } from "fs";
196
- import { access as access2 } from "fs/promises";
536
+ import { access as access4 } from "fs/promises";
197
537
  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";
538
+ import { homedir as homedir5 } from "os";
539
+ import { join as join5 } from "path";
540
+ import { glob as glob3 } from "tinyglobby";
201
541
  import { z } from "zod";
202
542
  var TokenTotals = z.object({
203
543
  input_tokens: z.number().catch(0).default(0),
@@ -220,8 +560,8 @@ var Line = z.object({
220
560
  }).passthrough().optional()
221
561
  });
222
562
  function codexHome(home) {
223
- if (home) return join3(home, ".codex");
224
- return process.env.CODEX_HOME ?? join3(homedir3(), ".codex");
563
+ if (home) return join5(home, ".codex");
564
+ return process.env.CODEX_HOME ?? join5(homedir5(), ".codex");
225
565
  }
226
566
  var DEFAULT_MODEL = "gpt-5";
227
567
  async function parseCodexRollout(file) {
@@ -240,19 +580,19 @@ async function parseCodexRollout(file) {
240
580
  }
241
581
  const parsed = Line.safeParse(obj);
242
582
  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;
583
+ const { timestamp, type, payload: payload2 } = parsed.data;
584
+ if (!stats.cwd) stats.cwd = payload2?.cwd ?? parsed.data.cwd;
585
+ if (payload2?.model) model = payload2.model;
246
586
  if (timestamp) {
247
587
  if (!stats.firstTs) stats.firstTs = timestamp;
248
588
  stats.lastTs = timestamp;
249
589
  }
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;
590
+ const toolName = payload2?.type === "function_call" || payload2?.type === "custom_tool_call" ? payload2.name : payload2?.type === "local_shell_call" ? "shell" : type === "function_call" ? parsed.data.name : void 0;
251
591
  if (toolName) {
252
592
  stats.toolCalls[toolName] = (stats.toolCalls[toolName] ?? 0) + 1;
253
593
  turnTools.push(toolName);
254
594
  }
255
- const totals = payload?.type === "token_count" ? payload.info?.total_token_usage : void 0;
595
+ const totals = payload2?.type === "token_count" ? payload2.info?.total_token_usage : void 0;
256
596
  if (!totals) continue;
257
597
  const cur = {
258
598
  input: totals.input_tokens,
@@ -298,38 +638,20 @@ async function parseCodexRollout(file) {
298
638
  }
299
639
  async function loadCodexProjects(home, onProgress) {
300
640
  const root = codexHome(home);
301
- const files = await glob2(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
641
+ const files = await glob3(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
302
642
  cwd: root,
303
643
  absolute: true
304
644
  }).catch(() => []);
305
645
  if (files.length === 0) return [];
306
- const byCwd = /* @__PURE__ */ new Map();
646
+ const sessions = [];
307
647
  let parsed = 0;
308
648
  for (const f of files) {
309
649
  onProgress?.({ parsed, total: files.length, currentProject: "Codex sessions" });
310
- const s = await parseCodexRollout(f);
650
+ sessions.push(await parseCodexRollout(f));
311
651
  parsed += 1;
312
652
  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
653
  }
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;
654
+ return groupSessionsByCwd("codex", sessions);
333
655
  }
334
656
  var codexAdapter = {
335
657
  id: "codex",
@@ -337,7 +659,7 @@ var codexAdapter = {
337
659
  supported: true,
338
660
  async detect(home) {
339
661
  try {
340
- await access2(join3(codexHome(home), "sessions"));
662
+ await access4(join5(codexHome(home), "sessions"));
341
663
  return true;
342
664
  } catch {
343
665
  return false;
@@ -346,11 +668,602 @@ var codexAdapter = {
346
668
  loadProjects: loadCodexProjects
347
669
  };
348
670
 
671
+ // src/agents/droid.ts
672
+ import { access as access5, stat as stat2 } from "fs/promises";
673
+ import { homedir as homedir6 } from "os";
674
+ import { basename as basename2, join as join6 } from "path";
675
+ import { glob as glob4 } from "tinyglobby";
676
+ function droidRoot(home) {
677
+ return process.env.DROID_SESSIONS_DIR ?? join6(home ?? homedir6(), ".factory", "sessions");
678
+ }
679
+ async function parseFile2(file) {
680
+ const settings = await readJson(file);
681
+ const usage = settings?.tokenUsage;
682
+ if (!usage) return sessionFromRecords(file, void 0, []);
683
+ let ts = str(settings, "updatedAt") ?? str(settings, "createdAt");
684
+ if (!ts) {
685
+ try {
686
+ ts = (await stat2(file)).mtime.toISOString();
687
+ } catch {
688
+ }
689
+ }
690
+ return sessionFromRecords(file, void 0, [
691
+ {
692
+ model: str(settings, "model") ?? "droid-unknown",
693
+ ts,
694
+ input: pickNum(usage, ["inputTokens"]),
695
+ output: pickNum(usage, ["outputTokens"]) + pickNum(usage, ["thinkingTokens"]),
696
+ cacheRead: pickNum(usage, ["cacheReadTokens"]),
697
+ cacheWrite: pickNum(usage, ["cacheCreationTokens"])
698
+ }
699
+ ]);
700
+ }
701
+ async function loadDroidProjects(home, onProgress) {
702
+ const files = (await glob4(["**/*.settings.json"], { cwd: droidRoot(home), absolute: true }).catch(() => [])).filter((f) => basename2(f).endsWith(".settings.json"));
703
+ const sessions = [];
704
+ let parsed = 0;
705
+ for (const f of files) {
706
+ onProgress?.({ parsed, total: files.length, currentProject: "Droid sessions" });
707
+ sessions.push(await parseFile2(f));
708
+ parsed += 1;
709
+ onProgress?.({ parsed, total: files.length, currentProject: "Droid sessions" });
710
+ }
711
+ return groupSessionsByCwd("droid", sessions);
712
+ }
713
+ var droidAdapter = {
714
+ id: "droid",
715
+ name: "Droid (Factory)",
716
+ supported: true,
717
+ async detect(home) {
718
+ try {
719
+ await access5(droidRoot(home));
720
+ return true;
721
+ } catch {
722
+ return false;
723
+ }
724
+ },
725
+ loadProjects: loadDroidProjects
726
+ };
727
+
728
+ // src/agents/gemini.ts
729
+ import { access as access6 } from "fs/promises";
730
+ import { homedir as homedir7 } from "os";
731
+ import { join as join7 } from "path";
732
+ import { glob as glob5 } from "tinyglobby";
733
+ function geminiRoot(home) {
734
+ return process.env.GEMINI_DATA_DIR ?? join7(home ?? homedir7(), ".gemini", "tmp");
735
+ }
736
+ var IN = ["input", "prompt", "input_tokens", "prompt_tokens"];
737
+ var OUT = ["output", "candidates", "output_tokens", "candidates_tokens"];
738
+ function recordFrom(node, model, ts) {
739
+ const tokens = node.tokens;
740
+ if (!tokens) return void 0;
741
+ const reasoning = pickNum(tokens, ["thoughts", "reasoning", "thoughts_tokens", "reasoning_tokens"]);
742
+ return {
743
+ model: str(node, "model") ?? model ?? "gemini-unknown",
744
+ ts: str(node, "timestamp") ?? ts,
745
+ input: pickNum(tokens, IN),
746
+ output: pickNum(tokens, OUT) + reasoning,
747
+ cacheRead: pickNum(tokens, ["cached", "cached_tokens"]),
748
+ cacheWrite: 0
749
+ };
750
+ }
751
+ async function parseFile3(file) {
752
+ const records = [];
753
+ const push = (r) => r && records.push(r);
754
+ if (file.endsWith(".jsonl")) {
755
+ for (const line of await readJsonl(file)) push(recordFrom(line, str(line, "model"), void 0));
756
+ } else {
757
+ const doc = await readJson(file);
758
+ const model = str(doc, "model");
759
+ const messages = doc?.messages;
760
+ if (Array.isArray(messages)) for (const m of messages) push(recordFrom(m, model, void 0));
761
+ push(recordFrom(doc, model, void 0));
762
+ }
763
+ return sessionFromRecords(file, void 0, records);
764
+ }
765
+ async function loadGeminiProjects(home, onProgress) {
766
+ const files = await glob5(["**/*.json", "**/*.jsonl"], { cwd: geminiRoot(home), absolute: true }).catch(
767
+ () => []
768
+ );
769
+ const sessions = [];
770
+ let parsed = 0;
771
+ for (const f of files) {
772
+ onProgress?.({ parsed, total: files.length, currentProject: "Gemini logs" });
773
+ sessions.push(await parseFile3(f));
774
+ parsed += 1;
775
+ onProgress?.({ parsed, total: files.length, currentProject: "Gemini logs" });
776
+ }
777
+ return groupSessionsByCwd("gemini", sessions);
778
+ }
779
+ var geminiAdapter = {
780
+ id: "gemini",
781
+ name: "Gemini CLI",
782
+ supported: true,
783
+ async detect(home) {
784
+ try {
785
+ await access6(geminiRoot(home));
786
+ return true;
787
+ } catch {
788
+ return false;
789
+ }
790
+ },
791
+ loadProjects: loadGeminiProjects
792
+ };
793
+
794
+ // src/agents/goose.ts
795
+ import { access as access7 } from "fs/promises";
796
+ import { homedir as homedir8 } from "os";
797
+ import { join as join8 } from "path";
798
+
799
+ // src/sqlite.ts
800
+ import { readFile as readFile4 } from "fs/promises";
801
+ function u8(b, o) {
802
+ return b[o];
803
+ }
804
+ function u16(b, o) {
805
+ return b[o] << 8 | b[o + 1];
806
+ }
807
+ function u32(b, o) {
808
+ return b[o] * 16777216 + (b[o + 1] << 16) + (b[o + 2] << 8) + b[o + 3];
809
+ }
810
+ function varint(b, o) {
811
+ let result = 0n;
812
+ for (let i = 0; i < 8; i++) {
813
+ const byte = b[o + i];
814
+ result = result << 7n | BigInt(byte & 127);
815
+ if ((byte & 128) === 0) return [result, i + 1];
816
+ }
817
+ result = result << 8n | BigInt(b[o + 8]);
818
+ return [result, 9];
819
+ }
820
+ function page(db, n) {
821
+ const start = (n - 1) * db.pageSize;
822
+ return db.buf.subarray(start, start + db.pageSize);
823
+ }
824
+ function payload(db, p, cellContentOffset, payloadLen) {
825
+ const U = db.usable;
826
+ const X = U - 35;
827
+ if (payloadLen <= X) return p.subarray(cellContentOffset, cellContentOffset + payloadLen);
828
+ const M = Math.floor((U - 12) * 32 / 255) - 23;
829
+ const K = M + (payloadLen - M) % (U - 4);
830
+ const local = K <= X ? K : M;
831
+ const parts = [p.subarray(cellContentOffset, cellContentOffset + local)];
832
+ let next = u32(p, cellContentOffset + local);
833
+ let remaining = payloadLen - local;
834
+ while (next !== 0 && remaining > 0) {
835
+ const ov = page(db, next);
836
+ next = u32(ov, 0);
837
+ const take = Math.min(remaining, U - 4);
838
+ parts.push(ov.subarray(4, 4 + take));
839
+ remaining -= take;
840
+ }
841
+ return Buffer.concat(parts);
842
+ }
843
+ function decodeRecord(rec, rowid) {
844
+ const [headerLen, hlBytes] = varint(rec, 0);
845
+ const serials = [];
846
+ let o = hlBytes;
847
+ while (o < Number(headerLen)) {
848
+ const [s, n] = varint(rec, o);
849
+ serials.push(s);
850
+ o += n;
851
+ }
852
+ const values = [];
853
+ let body = Number(headerLen);
854
+ for (const st of serials) {
855
+ const t = Number(st);
856
+ if (t === 0) {
857
+ values.push(rowid);
858
+ } else if (t >= 1 && t <= 6) {
859
+ const len = t <= 4 ? t : t === 5 ? 6 : 8;
860
+ let v = 0n;
861
+ for (let i = 0; i < len; i++) v = v << 8n | BigInt(rec[body + i]);
862
+ const bits = BigInt(len * 8);
863
+ if (v >> bits - 1n) v -= 1n << bits;
864
+ values.push(v >= -9007199254740991n && v <= 9007199254740991n ? Number(v) : v);
865
+ body += len;
866
+ } else if (t === 7) {
867
+ values.push(rec.readDoubleBE(body));
868
+ body += 8;
869
+ } else if (t === 8) {
870
+ values.push(0);
871
+ } else if (t === 9) {
872
+ values.push(1);
873
+ } else if (t >= 12 && t % 2 === 0) {
874
+ const len = (t - 12) / 2;
875
+ values.push(Uint8Array.from(rec.subarray(body, body + len)));
876
+ body += len;
877
+ } else if (t >= 13) {
878
+ const len = (t - 13) / 2;
879
+ values.push(rec.toString("utf8", body, body + len));
880
+ body += len;
881
+ } else {
882
+ values.push(null);
883
+ }
884
+ }
885
+ return values;
886
+ }
887
+ function walkTable(db, rootPage, out) {
888
+ const p = page(db, rootPage);
889
+ const headerOffset = rootPage === 1 ? 100 : 0;
890
+ const type = u8(p, headerOffset);
891
+ const cellCount = u16(p, headerOffset + 3);
892
+ const interior = type === 5;
893
+ const cellPtrBase = headerOffset + (interior ? 12 : 8);
894
+ for (let i = 0; i < cellCount; i++) {
895
+ const cellOffset = u16(p, cellPtrBase + i * 2);
896
+ if (interior) {
897
+ const child = u32(p, cellOffset);
898
+ walkTable(db, child, out);
899
+ } else if (type === 13) {
900
+ const [plen, n1] = varint(p, cellOffset);
901
+ const [rowid, n2] = varint(p, cellOffset + n1);
902
+ const rec = payload(db, p, cellOffset + n1 + n2, Number(plen));
903
+ out.push([rowid, decodeRecord(rec, rowid)]);
904
+ }
905
+ }
906
+ if (interior) {
907
+ const rightMost = u32(p, headerOffset + 8);
908
+ if (rightMost) walkTable(db, rightMost, out);
909
+ }
910
+ }
911
+ function columnNames(sql) {
912
+ const open = sql.indexOf("(");
913
+ const close = sql.lastIndexOf(")");
914
+ if (open < 0 || close < 0) return [];
915
+ const body = sql.slice(open + 1, close);
916
+ const cols = [];
917
+ let depth = 0;
918
+ let cur = "";
919
+ const parts = [];
920
+ for (const ch of body) {
921
+ if (ch === "(") depth++;
922
+ else if (ch === ")") depth--;
923
+ if (ch === "," && depth === 0) {
924
+ parts.push(cur);
925
+ cur = "";
926
+ } else cur += ch;
927
+ }
928
+ if (cur.trim()) parts.push(cur);
929
+ const CONSTRAINTS = /* @__PURE__ */ new Set(["primary", "unique", "check", "foreign", "constraint"]);
930
+ for (const part of parts) {
931
+ const token = part.trim().replace(/^["'`\[]|["'`\]]$/g, "");
932
+ const name = token.split(/[\s(]/)[0].replace(/["'`\[\]]/g, "");
933
+ if (name && !CONSTRAINTS.has(name.toLowerCase())) cols.push(name);
934
+ }
935
+ return cols;
936
+ }
937
+ async function readTable(file, table) {
938
+ let buf;
939
+ try {
940
+ buf = await readFile4(file);
941
+ } catch {
942
+ return [];
943
+ }
944
+ if (buf.length < 100 || buf.toString("latin1", 0, 16) !== "SQLite format 3\0") return [];
945
+ let pageSize = u16(buf, 16);
946
+ if (pageSize === 1) pageSize = 65536;
947
+ const reserved = u8(buf, 20);
948
+ const db = { buf, pageSize, usable: pageSize - reserved };
949
+ const master = [];
950
+ walkTable(db, 1, master);
951
+ let rootPage = 0;
952
+ let createSql = "";
953
+ for (const [, cols] of master) {
954
+ if (cols[0] === "table" && cols[1] === table) {
955
+ rootPage = Number(cols[3]);
956
+ createSql = typeof cols[4] === "string" ? cols[4] : "";
957
+ break;
958
+ }
959
+ }
960
+ if (!rootPage) return [];
961
+ const names = columnNames(createSql);
962
+ const rows = [];
963
+ walkTable(db, rootPage, rows);
964
+ return rows.map(([rowid, values]) => {
965
+ const row = {};
966
+ names.forEach((name, i) => row[name] = values[i] ?? null);
967
+ row.rowid ??= rowid <= 9007199254740991n ? Number(rowid) : rowid;
968
+ return row;
969
+ });
970
+ }
971
+
972
+ // src/agents/goose.ts
973
+ var GOOSE_DB_CANDIDATES = [
974
+ [".local", "share", "goose", "sessions", "sessions.db"],
975
+ ["Library", "Application Support", "goose", "sessions", "sessions.db"],
976
+ [".local", "share", "Block", "goose", "sessions", "sessions.db"]
977
+ ];
978
+ function gooseDbCandidates(home) {
979
+ const root = process.env.GOOSE_PATH_ROOT;
980
+ if (root) return [join8(root, "data", "sessions", "sessions.db")];
981
+ const h = home ?? homedir8();
982
+ return GOOSE_DB_CANDIDATES.map((parts) => join8(h, ...parts));
983
+ }
984
+ async function loadGooseProjects(home, onProgress) {
985
+ const records = [];
986
+ for (const db of gooseDbCandidates(home)) {
987
+ const rows = await readTable(db, "sessions").catch(() => []);
988
+ for (const row of rows) {
989
+ onProgress?.({ parsed: records.length, total: rows.length, currentProject: "Goose sessions" });
990
+ const input = pickNum(row, ["accumulated_input_tokens"]) || pickNum(row, ["input_tokens"]);
991
+ const output = pickNum(row, ["accumulated_output_tokens"]) || pickNum(row, ["output_tokens"]);
992
+ const total = pickNum(row, ["accumulated_total_tokens"]) || pickNum(row, ["total_tokens"]);
993
+ const reasoning = Math.max(0, total - input - output);
994
+ let model = "goose-unknown";
995
+ try {
996
+ model = JSON.parse(String(row.model_config_json ?? "{}")).model_name || model;
997
+ } catch {
998
+ }
999
+ records.push({
1000
+ model,
1001
+ ts: toIso(row.created_at),
1002
+ input,
1003
+ output: output + reasoning,
1004
+ cacheRead: 0,
1005
+ cacheWrite: 0
1006
+ });
1007
+ }
1008
+ }
1009
+ const session = sessionFromRecords("goose", void 0, records);
1010
+ return groupSessionsByCwd("goose", [session]);
1011
+ }
1012
+ var gooseAdapter = {
1013
+ id: "goose",
1014
+ name: "Goose",
1015
+ supported: true,
1016
+ async detect(home) {
1017
+ for (const db of gooseDbCandidates(home)) {
1018
+ try {
1019
+ await access7(db);
1020
+ return true;
1021
+ } catch {
1022
+ }
1023
+ }
1024
+ return false;
1025
+ },
1026
+ loadProjects: loadGooseProjects
1027
+ };
1028
+
1029
+ // src/agents/hermes.ts
1030
+ import { access as access8 } from "fs/promises";
1031
+ import { homedir as homedir9 } from "os";
1032
+ import { join as join9 } from "path";
1033
+ function hermesDb(home) {
1034
+ const env = process.env.HERMES_HOME;
1035
+ const root = env ? env.split(",")[0].trim() : join9(home ?? homedir9(), ".hermes");
1036
+ return join9(root, "state.db");
1037
+ }
1038
+ async function loadHermesProjects(home, onProgress) {
1039
+ const rows = await readTable(hermesDb(home), "sessions").catch(() => []);
1040
+ const records = [];
1041
+ for (const row of rows) {
1042
+ onProgress?.({ parsed: records.length, total: rows.length, currentProject: "Hermes sessions" });
1043
+ records.push({
1044
+ model: str(row, "model") ?? "hermes-unknown",
1045
+ ts: toIso(row.started_at),
1046
+ input: pickNum(row, ["input_tokens"]),
1047
+ output: pickNum(row, ["output_tokens"]) + pickNum(row, ["reasoning_tokens"]),
1048
+ cacheRead: pickNum(row, ["cache_read_tokens"]),
1049
+ cacheWrite: pickNum(row, ["cache_write_tokens"])
1050
+ });
1051
+ }
1052
+ const session = sessionFromRecords("hermes", void 0, records);
1053
+ return groupSessionsByCwd("hermes", [session]);
1054
+ }
1055
+ var hermesAdapter = {
1056
+ id: "hermes",
1057
+ name: "Hermes",
1058
+ supported: true,
1059
+ async detect(home) {
1060
+ try {
1061
+ await access8(hermesDb(home));
1062
+ return true;
1063
+ } catch {
1064
+ return false;
1065
+ }
1066
+ },
1067
+ loadProjects: loadHermesProjects
1068
+ };
1069
+
1070
+ // src/agents/kilo.ts
1071
+ import { access as access9 } from "fs/promises";
1072
+ import { homedir as homedir10 } from "os";
1073
+ import { join as join10 } from "path";
1074
+ function kiloDir(home) {
1075
+ return process.env.KILO_DATA_DIR ?? join10(home ?? homedir10(), ".local", "share", "kilo");
1076
+ }
1077
+ async function loadKiloProjects(home, onProgress) {
1078
+ const db = join10(kiloDir(home), "kilo.db");
1079
+ let rows;
1080
+ try {
1081
+ rows = await readTable(db, "message");
1082
+ } catch {
1083
+ return [];
1084
+ }
1085
+ const bySession = /* @__PURE__ */ new Map();
1086
+ let parsed = 0;
1087
+ for (const row of rows) {
1088
+ onProgress?.({ parsed, total: rows.length, currentProject: "Kilo messages" });
1089
+ parsed += 1;
1090
+ if (typeof row.data !== "string") continue;
1091
+ let m;
1092
+ try {
1093
+ m = JSON.parse(row.data);
1094
+ } catch {
1095
+ continue;
1096
+ }
1097
+ if (m.role !== "assistant" || !m.tokens) continue;
1098
+ const session = m.sessionID ?? String(row.session_id ?? "kilo");
1099
+ const list = bySession.get(session) ?? [];
1100
+ list.push({
1101
+ model: m.modelID ?? "kilo-unknown",
1102
+ ts: m.time?.created ? new Date(m.time.created).toISOString() : void 0,
1103
+ input: m.tokens.input ?? 0,
1104
+ output: (m.tokens.output ?? 0) + (m.tokens.reasoning ?? 0),
1105
+ cacheRead: m.tokens.cache?.read ?? 0,
1106
+ cacheWrite: m.tokens.cache?.write ?? 0
1107
+ });
1108
+ bySession.set(session, list);
1109
+ }
1110
+ const sessions = [];
1111
+ for (const [id, records] of bySession) sessions.push(sessionFromRecords(id, void 0, records));
1112
+ return groupSessionsByCwd("kilo", sessions);
1113
+ }
1114
+ var kiloAdapter = {
1115
+ id: "kilo",
1116
+ name: "Kilo",
1117
+ supported: true,
1118
+ async detect(home) {
1119
+ try {
1120
+ await access9(join10(kiloDir(home), "kilo.db"));
1121
+ return true;
1122
+ } catch {
1123
+ return false;
1124
+ }
1125
+ },
1126
+ loadProjects: loadKiloProjects
1127
+ };
1128
+
1129
+ // src/agents/kimi.ts
1130
+ import { access as access10 } from "fs/promises";
1131
+ import { homedir as homedir11 } from "os";
1132
+ import { join as join11 } from "path";
1133
+ import { glob as glob6 } from "tinyglobby";
1134
+ var KIMI_DIRS = [".kimi", ".kimi-code"];
1135
+ var USAGE_KEYS = ["inputOther", "input_other", "inputCacheRead", "input_cache_read"];
1136
+ function kimiRoots(home) {
1137
+ const env = process.env.KIMI_DATA_DIR;
1138
+ if (env) return env.split(",").map((s) => s.trim()).filter(Boolean);
1139
+ const h = home ?? homedir11();
1140
+ return KIMI_DIRS.map((d) => join11(h, d));
1141
+ }
1142
+ async function parseFile4(file) {
1143
+ const records = [];
1144
+ for (const line of await readJsonl(file)) {
1145
+ const usage = deepFind(line, USAGE_KEYS);
1146
+ if (!usage) continue;
1147
+ records.push({
1148
+ model: str(line, "model") ?? "kimi-unknown",
1149
+ ts: str(line, "timestamp"),
1150
+ input: pickNum(usage, ["inputOther", "input_other"]),
1151
+ output: pickNum(usage, ["output"]),
1152
+ cacheRead: pickNum(usage, ["inputCacheRead", "input_cache_read"]),
1153
+ cacheWrite: pickNum(usage, ["inputCacheCreation", "input_cache_creation"])
1154
+ });
1155
+ }
1156
+ return sessionFromRecords(file, void 0, records);
1157
+ }
1158
+ async function loadKimiProjects(home, onProgress) {
1159
+ const files = [];
1160
+ for (const root of kimiRoots(home)) {
1161
+ files.push(
1162
+ ...await glob6(["sessions/**/wire.jsonl"], { cwd: root, absolute: true }).catch(() => [])
1163
+ );
1164
+ }
1165
+ const sessions = [];
1166
+ let parsed = 0;
1167
+ for (const f of files) {
1168
+ onProgress?.({ parsed, total: files.length, currentProject: "Kimi sessions" });
1169
+ sessions.push(await parseFile4(f));
1170
+ parsed += 1;
1171
+ onProgress?.({ parsed, total: files.length, currentProject: "Kimi sessions" });
1172
+ }
1173
+ return groupSessionsByCwd("kimi", sessions);
1174
+ }
1175
+ var kimiAdapter = {
1176
+ id: "kimi",
1177
+ name: "Kimi CLI",
1178
+ supported: true,
1179
+ async detect(home) {
1180
+ for (const root of kimiRoots(home)) {
1181
+ try {
1182
+ await access10(join11(root, "sessions"));
1183
+ return true;
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ return false;
1188
+ },
1189
+ loadProjects: loadKimiProjects
1190
+ };
1191
+
1192
+ // src/agents/openclaw.ts
1193
+ import { access as access11 } from "fs/promises";
1194
+ import { homedir as homedir12 } from "os";
1195
+ import { join as join12 } from "path";
1196
+ import { glob as glob7 } from "tinyglobby";
1197
+ var OPENCLAW_DIRS = [".openclaw", ".clawdbot", ".moltbot", ".moldbot"];
1198
+ function openclawRoots(home) {
1199
+ const env = process.env.OPENCLAW_DIR;
1200
+ if (env) return env.split(",").map((s) => s.trim()).filter(Boolean);
1201
+ const h = home ?? homedir12();
1202
+ return OPENCLAW_DIRS.map((d) => join12(h, d));
1203
+ }
1204
+ function modelFrom(obj) {
1205
+ return str(obj, "modelId") ?? str(obj, "model");
1206
+ }
1207
+ async function parseFile5(file) {
1208
+ const records = [];
1209
+ let currentModel;
1210
+ for (const line of await readJsonl(file)) {
1211
+ const l = line;
1212
+ if (l.type === "model_change" || l.type === "model-snapshot") {
1213
+ currentModel = modelFrom(l.data) ?? modelFrom(l) ?? currentModel;
1214
+ continue;
1215
+ }
1216
+ const usage = l.message?.usage;
1217
+ if (!usage) continue;
1218
+ const model = modelFrom(l.message) ?? currentModel ?? "openclaw-unknown";
1219
+ records.push({
1220
+ model,
1221
+ ts: str(l.message, "timestamp") ?? str(l, "timestamp"),
1222
+ input: pickNum(usage, ["input"]),
1223
+ output: pickNum(usage, ["output"]),
1224
+ cacheRead: pickNum(usage, ["cacheRead"]),
1225
+ cacheWrite: pickNum(usage, ["cacheWrite"])
1226
+ });
1227
+ }
1228
+ return sessionFromRecords(file, void 0, records);
1229
+ }
1230
+ async function loadOpenclawProjects(home, onProgress) {
1231
+ const files = [];
1232
+ for (const root of openclawRoots(home)) {
1233
+ files.push(...await glob7(["**/*.jsonl", "**/*.jsonl.*"], { cwd: root, absolute: true }).catch(() => []));
1234
+ }
1235
+ const sessions = [];
1236
+ let parsed = 0;
1237
+ for (const f of files) {
1238
+ onProgress?.({ parsed, total: files.length, currentProject: "OpenClaw sessions" });
1239
+ sessions.push(await parseFile5(f));
1240
+ parsed += 1;
1241
+ onProgress?.({ parsed, total: files.length, currentProject: "OpenClaw sessions" });
1242
+ }
1243
+ return groupSessionsByCwd("openclaw", sessions);
1244
+ }
1245
+ var openclawAdapter = {
1246
+ id: "openclaw",
1247
+ name: "OpenClaw",
1248
+ supported: true,
1249
+ async detect(home) {
1250
+ for (const root of openclawRoots(home)) {
1251
+ try {
1252
+ await access11(root);
1253
+ return true;
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ return false;
1258
+ },
1259
+ loadProjects: loadOpenclawProjects
1260
+ };
1261
+
349
1262
  // 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";
1263
+ import { access as access12, readFile as readFile5 } from "fs/promises";
1264
+ import { homedir as homedir13 } from "os";
1265
+ import { join as join13 } from "path";
1266
+ import { glob as glob8 } from "tinyglobby";
354
1267
  import { z as z2 } from "zod";
355
1268
  var Message = z2.object({
356
1269
  sessionID: z2.string(),
@@ -373,34 +1286,34 @@ var Session = z2.object({
373
1286
  });
374
1287
  var Project = z2.object({ id: z2.string(), worktree: z2.string().optional() });
375
1288
  function opencodeRoot(home) {
376
- if (home) return join4(home, ".local", "share", "opencode");
377
- return process.env.OPENCODE_DATA_DIR ?? join4(homedir4(), ".local", "share", "opencode");
1289
+ if (home) return join13(home, ".local", "share", "opencode");
1290
+ return process.env.OPENCODE_DATA_DIR ?? join13(homedir13(), ".local", "share", "opencode");
378
1291
  }
379
- async function readJson(file) {
1292
+ async function readJson2(file) {
380
1293
  try {
381
- return JSON.parse(await readFile2(file, "utf8"));
1294
+ return JSON.parse(await readFile5(file, "utf8"));
382
1295
  } catch {
383
1296
  return void 0;
384
1297
  }
385
1298
  }
386
1299
  async function loadOpencodeProjects(home, onProgress) {
387
1300
  const root = opencodeRoot(home);
388
- const storage = join4(root, "storage");
389
- const messageFiles = await glob3(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
1301
+ const storage = join13(root, "storage");
1302
+ const messageFiles = await glob8(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
390
1303
  () => []
391
1304
  );
392
1305
  if (messageFiles.length === 0) return [];
393
1306
  const sessionDir = /* @__PURE__ */ new Map();
394
1307
  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));
1308
+ for (const f of await glob8(["project/*.json"], { cwd: storage, absolute: true }).catch(() => [])) {
1309
+ const p = Project.safeParse(await readJson2(f));
397
1310
  if (p.success && p.data.worktree) projectWorktree.set(p.data.id, p.data.worktree);
398
1311
  }
399
- const sessionFiles = await glob3(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
1312
+ const sessionFiles = await glob8(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
400
1313
  () => []
401
1314
  );
402
1315
  for (const f of sessionFiles) {
403
- const s = Session.safeParse(await readJson(f));
1316
+ const s = Session.safeParse(await readJson2(f));
404
1317
  if (!s.success) continue;
405
1318
  const dir = s.data.directory ?? (s.data.projectID ? projectWorktree.get(s.data.projectID) : void 0);
406
1319
  if (dir) sessionDir.set(s.data.id, dir);
@@ -409,7 +1322,7 @@ async function loadOpencodeProjects(home, onProgress) {
409
1322
  let parsed = 0;
410
1323
  for (const f of messageFiles) {
411
1324
  onProgress?.({ parsed, total: messageFiles.length, currentProject: "OpenCode messages" });
412
- const m = Message.safeParse(await readJson(f));
1325
+ const m = Message.safeParse(await readJson2(f));
413
1326
  parsed += 1;
414
1327
  onProgress?.({ parsed, total: messageFiles.length, currentProject: "OpenCode messages" });
415
1328
  if (!m.success || m.data.role !== "assistant" || !m.data.tokens) continue;
@@ -442,28 +1355,7 @@ async function loadOpencodeProjects(home, onProgress) {
442
1355
  u.turns += 1;
443
1356
  }
444
1357
  }
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;
1358
+ return groupSessionsByCwd("opencode", [...bySession.values()]);
467
1359
  }
468
1360
  var opencodeAdapter = {
469
1361
  id: "opencode",
@@ -471,7 +1363,7 @@ var opencodeAdapter = {
471
1363
  supported: true,
472
1364
  async detect(home) {
473
1365
  try {
474
- await access3(join4(opencodeRoot(home), "storage", "message"));
1366
+ await access12(join13(opencodeRoot(home), "storage", "message"));
475
1367
  return true;
476
1368
  } catch {
477
1369
  return false;
@@ -480,19 +1372,130 @@ var opencodeAdapter = {
480
1372
  loadProjects: loadOpencodeProjects
481
1373
  };
482
1374
 
1375
+ // src/agents/pi.ts
1376
+ import { access as access13 } from "fs/promises";
1377
+ import { homedir as homedir14 } from "os";
1378
+ import { join as join14 } from "path";
1379
+ import { glob as glob9 } from "tinyglobby";
1380
+ function piRoot(home) {
1381
+ return process.env.PI_AGENT_DIR ?? join14(home ?? homedir14(), ".pi", "agent", "sessions");
1382
+ }
1383
+ async function parseFile6(file) {
1384
+ const records = [];
1385
+ for (const line of await readJsonl(file)) {
1386
+ const msg = line.message;
1387
+ const usage = msg?.usage;
1388
+ if (!usage) continue;
1389
+ records.push({
1390
+ model: str(msg, "model") ?? "pi-unknown",
1391
+ ts: str(line, "timestamp"),
1392
+ input: pickNum(usage, ["input"]),
1393
+ output: pickNum(usage, ["output"]),
1394
+ cacheRead: pickNum(usage, ["cacheRead"]),
1395
+ cacheWrite: pickNum(usage, ["cacheWrite"])
1396
+ });
1397
+ }
1398
+ return sessionFromRecords(file, void 0, records);
1399
+ }
1400
+ async function loadPiProjects(home, onProgress) {
1401
+ const files = await glob9(["**/*.jsonl"], { cwd: piRoot(home), absolute: true }).catch(() => []);
1402
+ const sessions = [];
1403
+ let parsed = 0;
1404
+ for (const f of files) {
1405
+ onProgress?.({ parsed, total: files.length, currentProject: "pi sessions" });
1406
+ sessions.push(await parseFile6(f));
1407
+ parsed += 1;
1408
+ onProgress?.({ parsed, total: files.length, currentProject: "pi sessions" });
1409
+ }
1410
+ return groupSessionsByCwd("pi", sessions);
1411
+ }
1412
+ var piAdapter = {
1413
+ id: "pi",
1414
+ name: "pi-agent",
1415
+ supported: true,
1416
+ async detect(home) {
1417
+ try {
1418
+ await access13(piRoot(home));
1419
+ return true;
1420
+ } catch {
1421
+ return false;
1422
+ }
1423
+ },
1424
+ loadProjects: loadPiProjects
1425
+ };
1426
+
1427
+ // src/agents/qwen.ts
1428
+ import { access as access14 } from "fs/promises";
1429
+ import { homedir as homedir15 } from "os";
1430
+ import { join as join15 } from "path";
1431
+ import { glob as glob10 } from "tinyglobby";
1432
+ function qwenRoot(home) {
1433
+ return process.env.QWEN_DATA_DIR ?? join15(home ?? homedir15(), ".qwen");
1434
+ }
1435
+ async function parseFile7(file) {
1436
+ const records = [];
1437
+ for (const line of await readJsonl(file)) {
1438
+ const meta = line.usageMetadata;
1439
+ if (!meta) continue;
1440
+ const prompt = pickNum(meta, ["promptTokenCount"]);
1441
+ const cached = pickNum(meta, ["cachedContentTokenCount"]);
1442
+ const reasoning = pickNum(meta, ["thoughtsTokenCount"]);
1443
+ records.push({
1444
+ model: str(line, "model") ?? "qwen-unknown",
1445
+ ts: str(line, "timestamp"),
1446
+ input: Math.max(0, prompt - cached),
1447
+ output: pickNum(meta, ["candidatesTokenCount"]) + reasoning,
1448
+ cacheRead: cached,
1449
+ cacheWrite: 0
1450
+ });
1451
+ }
1452
+ return sessionFromRecords(file, void 0, records);
1453
+ }
1454
+ async function loadQwenProjects(home, onProgress) {
1455
+ const files = await glob10(["projects/**/*.jsonl"], { cwd: qwenRoot(home), absolute: true }).catch(
1456
+ () => []
1457
+ );
1458
+ const sessions = [];
1459
+ let parsed = 0;
1460
+ for (const f of files) {
1461
+ onProgress?.({ parsed, total: files.length, currentProject: "Qwen sessions" });
1462
+ sessions.push(await parseFile7(f));
1463
+ parsed += 1;
1464
+ onProgress?.({ parsed, total: files.length, currentProject: "Qwen sessions" });
1465
+ }
1466
+ return groupSessionsByCwd("qwen", sessions);
1467
+ }
1468
+ var qwenAdapter = {
1469
+ id: "qwen",
1470
+ name: "Qwen Code",
1471
+ supported: true,
1472
+ async detect(home) {
1473
+ try {
1474
+ await access14(join15(qwenRoot(home), "projects"));
1475
+ return true;
1476
+ } catch {
1477
+ return false;
1478
+ }
1479
+ },
1480
+ loadProjects: loadQwenProjects
1481
+ };
1482
+
483
1483
  // src/agents/index.ts
484
- function detectOnly(id, name, ...pathParts) {
1484
+ function detectOnly(id, name, reason, ...pathCandidates) {
485
1485
  return {
486
1486
  id,
487
1487
  name,
488
1488
  supported: false,
489
- async detect(home = homedir5()) {
490
- try {
491
- await access4(join5(home, ...pathParts));
492
- return true;
493
- } catch {
494
- return false;
1489
+ unsupportedReason: reason,
1490
+ async detect(home = homedir16()) {
1491
+ for (const parts of pathCandidates) {
1492
+ try {
1493
+ await access15(join16(home, ...parts));
1494
+ return true;
1495
+ } catch {
1496
+ }
495
1497
  }
1498
+ return false;
496
1499
  },
497
1500
  loadProjects: async () => []
498
1501
  };
@@ -501,8 +1504,23 @@ var ADAPTERS = [
501
1504
  claudeAdapter,
502
1505
  codexAdapter,
503
1506
  opencodeAdapter,
504
- detectOnly("gemini", "Gemini CLI", ".gemini", "tmp"),
505
- detectOnly("cursor", "Cursor CLI", ".cursor", "chats")
1507
+ geminiAdapter,
1508
+ qwenAdapter,
1509
+ droidAdapter,
1510
+ codebuffAdapter,
1511
+ openclawAdapter,
1512
+ kimiAdapter,
1513
+ kiloAdapter,
1514
+ gooseAdapter,
1515
+ hermesAdapter,
1516
+ piAdapter,
1517
+ antigravityAdapter,
1518
+ // Detected but not parsed: Copilot uses OpenTelemetry spans, Amp a usage
1519
+ // ledger (formats not wired yet). Cursor is intentionally absent — it stores
1520
+ // no token counts on disk (usage is server-side, reachable only with auth),
1521
+ // which is out of scope for an offline tool.
1522
+ detectOnly("copilot", "GitHub Copilot CLI", "usage is OpenTelemetry spans \u2014 parsing not wired yet", [".copilot", "otel"]),
1523
+ detectOnly("amp", "Amp", "usage-ledger thread format \u2014 parsing not wired yet", [".local", "share", "amp"])
506
1524
  ];
507
1525
  async function loadAllAgents(home, onProgress) {
508
1526
  const out = [];
@@ -529,7 +1547,7 @@ var TIMEFRAMES = [
529
1547
  { id: "7d", label: "Last 7 days" },
530
1548
  { id: "30d", label: "Last 30 days" }
531
1549
  ];
532
- var isoDay = (offsetDays, now) => new Date(now - offsetDays * 864e5).toISOString().slice(0, 10);
1550
+ var isoDay = (offsetDays, now) => dayKey(now - offsetDays * 864e5);
533
1551
  function timeframeLabel(id) {
534
1552
  return TIMEFRAMES.find((t) => t.id === id)?.label ?? id;
535
1553
  }
@@ -589,11 +1607,15 @@ var ART = [
589
1607
  " \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
590
1608
  " \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 "
591
1609
  ];
592
- function Banner({ subtitle }) {
1610
+ function bannerHeight(mode) {
1611
+ return mode === "full" ? 7 : mode === "tiny" ? 3 : 0;
1612
+ }
1613
+ function Banner({ subtitle, mode }) {
593
1614
  const { cols, rows } = useTerminalSize();
594
- const tiny = cols < 40 || rows < 18;
1615
+ const resolved = mode ?? (cols < 40 || rows < 18 ? "tiny" : "full");
1616
+ if (resolved === "none") return null;
595
1617
  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)),
1618
+ resolved === "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
1619
  subtitle ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: subtitle }) : null
598
1620
  ] });
599
1621
  }
@@ -619,15 +1641,28 @@ function modelColor(modelId) {
619
1641
  return "white";
620
1642
  }
621
1643
 
1644
+ // src/ui/viewport.ts
1645
+ function windowOffset(cursor, count, height) {
1646
+ if (count <= height) return 0;
1647
+ const centered = cursor - Math.floor(height / 2);
1648
+ return Math.max(0, Math.min(centered, count - height));
1649
+ }
1650
+
622
1651
  // src/ui/AgentPicker.tsx
623
1652
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
624
1653
  function status(a) {
625
1654
  if (a.projects.length > 0) {
626
1655
  const cost = a.projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
627
1656
  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)}` };
1657
+ const est = a.adapter.estimated ? `~` : "";
1658
+ const tail = a.adapter.estimated ? " \xB7 estimated" : "";
1659
+ return { text: `${a.projects.length} projects \xB7 ${sessions} sessions \xB7 ${est}${usd(cost)}${tail}` };
629
1660
  }
630
- if (a.detected && !a.adapter.supported) return { text: "detected \xB7 parsing not supported yet", color: theme.warn };
1661
+ if (a.detected && !a.adapter.supported)
1662
+ return {
1663
+ text: `detected \xB7 ${a.adapter.unsupportedReason ?? "parsing not supported yet"}`,
1664
+ color: theme.warn
1665
+ };
631
1666
  if (a.detected) return { text: "detected \xB7 no usage data", dim: true };
632
1667
  return { text: "not detected", dim: true };
633
1668
  }
@@ -638,33 +1673,43 @@ function AgentPicker({
638
1673
  const selectable = agents.map((a, i) => ({ a, i })).filter(({ a }) => a.projects.length > 0).map(({ i }) => i);
639
1674
  const [cursor, setCursor] = useState2(0);
640
1675
  const clamped = Math.min(cursor, Math.max(0, selectable.length - 1));
1676
+ const { cols, rows } = useTerminalSize();
641
1677
  useInput((_input, key) => {
642
1678
  if (key.upArrow) setCursor(() => Math.max(0, clamped - 1));
643
1679
  if (key.downArrow) setCursor(() => Math.min(selectable.length - 1, clamped + 1));
644
1680
  if (key.return && selectable[clamped] !== void 0) onSelect(selectable[clamped]);
645
1681
  });
646
1682
  const nameW = Math.max(...agents.map((a) => a.adapter.name.length)) + 2;
1683
+ const FIXED_CHROME = 9;
1684
+ const LIST_MIN = 3;
1685
+ const forcedTiny = cols < 40;
1686
+ const bannerBudget = rows - FIXED_CHROME - LIST_MIN;
1687
+ const bannerMode = !forcedTiny && bannerBudget >= 7 ? "full" : bannerBudget >= 3 ? "tiny" : "none";
1688
+ const visibleRows = Math.max(1, rows - FIXED_CHROME - bannerHeight(bannerMode));
1689
+ const selectedDisplayIdx = selectable[clamped] ?? 0;
1690
+ const offset = windowOffset(selectedDisplayIdx, agents.length, visibleRows);
1691
+ const visible = agents.slice(offset, offset + visibleRows);
1692
+ const hiddenBelow = agents.length - offset - visible.length;
647
1693
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
648
- /* @__PURE__ */ jsx2(Banner, { subtitle: "where your agents' tokens and dollars go \xB7 100% offline" }),
1694
+ /* @__PURE__ */ jsx2(Banner, { subtitle: "where your agents' tokens and dollars actually go", mode: bannerMode }),
649
1695
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accent, children: "Pick an agent" }),
650
1696
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [
651
- agents.map((a, i) => {
1697
+ visible.map((a, vi) => {
1698
+ const i = offset + vi;
652
1699
  const st = status(a);
653
1700
  const selected = selectable[clamped] === i;
654
1701
  const selectableRow = a.projects.length > 0;
655
1702
  return /* @__PURE__ */ jsxs2(Text2, { bold: selected, children: [
656
1703
  /* @__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
- ),
1704
+ /* @__PURE__ */ jsx2(Text2, { color: selected ? theme.accent : void 0, dimColor: !selectableRow, children: a.adapter.name.padEnd(nameW) }),
665
1705
  /* @__PURE__ */ jsx2(Text2, { color: st.color, dimColor: st.dim, children: st.text })
666
1706
  ] }, a.adapter.id);
667
1707
  }),
1708
+ agents.length > visibleRows ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1709
+ offset > 0 ? `\u2191 ${offset} above` : "",
1710
+ offset > 0 && hiddenBelow > 0 ? " \xB7 " : "",
1711
+ hiddenBelow > 0 ? `\u2193 ${hiddenBelow} below` : ""
1712
+ ] }) : null,
668
1713
  selectable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no agent usage data found on this machine" }) : null
669
1714
  ] })
670
1715
  ] });
@@ -740,7 +1785,7 @@ function Menu({
740
1785
  /* @__PURE__ */ jsx5(
741
1786
  Banner,
742
1787
  {
743
- subtitle: `${agentName ? `${agentName} \xB7 ` : ""}where your agent's tokens and dollars go \xB7 100% offline`
1788
+ subtitle: `${agentName ? `${agentName} \xB7 ` : ""}where your agent's tokens and dollars go`
744
1789
  }
745
1790
  ),
746
1791
  /* @__PURE__ */ jsx5(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx5(
@@ -891,12 +1936,13 @@ function ProjectList({
891
1936
  const fixed = COST_W + (showSess ? SESS_W : 0) + (showWhen ? WHEN_W : 0) + (showBar ? BAR_W + 2 : 0);
892
1937
  const nameW = Math.max(10, Math.min(42, avail - fixed - 2));
893
1938
  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));
1939
+ const CHROME_ROWS = 11;
1940
+ const visibleRows = Math.max(3, rows - CHROME_ROWS);
895
1941
  const clip = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "\u2026" : s;
896
1942
  const total = projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
897
1943
  const max = Math.max(0, ...projects.map((p) => p.report.totalCostUsd));
898
1944
  const projCursor = clamped - (pinnedShown ? 1 : 0);
899
- const offset = Math.max(0, Math.min(projCursor - Math.floor(visibleRows / 2), filtered.length - visibleRows));
1945
+ const offset = windowOffset(Math.max(0, projCursor), filtered.length, visibleRows);
900
1946
  const visible = filtered.slice(offset, offset + visibleRows);
901
1947
  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
1948
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, children: [
@@ -1188,24 +2234,33 @@ function Sessions({ r, cols }) {
1188
2234
  ] }) : null
1189
2235
  ] });
1190
2236
  }
1191
- function Activity({ r, chartW, cols }) {
1192
- const days = lastDays(r, 21).filter((d, i, all) => d.costUsd > 0 || i >= all.length - 14);
2237
+ function Activity({
2238
+ r,
2239
+ chartW,
2240
+ cols,
2241
+ grouping
2242
+ }) {
2243
+ if (r.daily.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no dated activity" });
1193
2244
  const wide = cols >= 66;
1194
- const rows = days.slice(-16).map((d) => ({
1195
- label: `${d.date} ${relativeDate(d.date) === "today" ? "\u25C2" : " "}`,
2245
+ const source = grouping === "day" ? lastDays(r, 21).filter((d, i, all) => d.costUsd > 0 || i >= all.length - 14) : groupDaily(r.daily, grouping);
2246
+ const rows = source.slice(-16).map((d) => ({
2247
+ label: grouping === "day" ? `${d.date} ${relativeDate(d.date) === "today" ? "\u25C2" : " "}` : grouping === "week" ? `wk ${d.date}` : d.date,
1196
2248
  value: d.costUsd,
1197
2249
  display: d.costUsd > 0 ? wide ? `${usd(d.costUsd)} \xB7 ${compact(d.turns)} turns` : usd(d.costUsd) : "\u2014"
1198
2250
  }));
1199
- if (r.daily.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no dated activity" });
1200
2251
  const avg = r.totalCostUsd / Math.max(1, r.daily.length);
1201
2252
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1202
2253
  /* @__PURE__ */ jsx8(BarChart, { rows, width: chartW }),
1203
2254
  /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
2255
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "g" }),
2256
+ " group: ",
2257
+ grouping,
2258
+ " \xB7 ",
1204
2259
  r.daily.length,
1205
- " active days \xB7 avg ",
1206
- usd(avg),
1207
- "/active day \xB7 projected",
2260
+ " active days \xB7 avg",
1208
2261
  " ",
2262
+ usd(avg),
2263
+ "/active day \xB7 projected ",
1209
2264
  usd(r.monthlyProjectionUsd),
1210
2265
  "/mo"
1211
2266
  ] }) })
@@ -1217,12 +2272,15 @@ function Dashboard({
1217
2272
  timeframe
1218
2273
  }) {
1219
2274
  const [tab, setTab] = useState4(initialTab);
2275
+ const [grouping, setGrouping] = useState4("day");
1220
2276
  const { cols } = useTerminalSize();
1221
2277
  useInput3((input, key) => {
1222
2278
  const n = Number.parseInt(input, 10);
1223
2279
  if (n >= 1 && n <= TABS.length) setTab(n - 1);
1224
2280
  if (key.rightArrow) setTab((t) => (t + 1) % TABS.length);
1225
2281
  if (key.leftArrow) setTab((t) => (t + TABS.length - 1) % TABS.length);
2282
+ if (input === "g")
2283
+ setGrouping((g) => g === "day" ? "week" : g === "week" ? "month" : "day");
1226
2284
  });
1227
2285
  const r = project.report;
1228
2286
  const span = r.spanStart && r.spanEnd ? `${r.spanStart} \u2192 ${r.spanEnd}` : `${r.spanDays}d`;
@@ -1255,7 +2313,7 @@ function Dashboard({
1255
2313
  tab === 2 ? /* @__PURE__ */ jsx8(Tools, { r, chartW, cols }) : null,
1256
2314
  tab === 3 ? /* @__PURE__ */ jsx8(Servers, { r, cols }) : null,
1257
2315
  tab === 4 ? /* @__PURE__ */ jsx8(Sessions, { r, cols }) : null,
1258
- tab === 5 ? /* @__PURE__ */ jsx8(Activity, { r, chartW, cols }) : null
2316
+ tab === 5 ? /* @__PURE__ */ jsx8(Activity, { r, chartW, cols, grouping }) : null
1259
2317
  ] })
1260
2318
  ] });
1261
2319
  }
@@ -1270,6 +2328,7 @@ var KEYS = [
1270
2328
  ["/", "filter project list (esc clears)"],
1271
2329
  ["s", "cycle project sort: cost \xB7 recent \xB7 name"],
1272
2330
  ["t", "cycle timeframe: all \xB7 today \xB7 yesterday \xB7 7d \xB7 30d"],
2331
+ ["g", "Activity tab: group by day \xB7 week \xB7 month"],
1273
2332
  ["esc", "go back"],
1274
2333
  ["?", "toggle this help"],
1275
2334
  ["q", "quit"]
@@ -1339,7 +2398,8 @@ function App({
1339
2398
  [activeProjects, timeframe]
1340
2399
  );
1341
2400
  const totals = useMemo2(() => aggregate(scoped), [scoped]);
1342
- const agentName = agentList[agentIdx]?.adapter.name ?? "";
2401
+ const activeAdapter = agentList[agentIdx]?.adapter;
2402
+ const agentName = activeAdapter ? activeAdapter.name + (activeAdapter.estimated ? " (estimated)" : "") : "";
1343
2403
  useInput4((input, key) => {
1344
2404
  if (help) {
1345
2405
  setHelp(false);
@@ -1472,7 +2532,7 @@ function Root() {
1472
2532
  }, [empty, exit]);
1473
2533
  if (!agents) {
1474
2534
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
1475
- /* @__PURE__ */ jsx11(Banner, { subtitle: "where your agents' tokens and dollars go \xB7 100% offline" }),
2535
+ /* @__PURE__ */ jsx11(Banner, { subtitle: "where your agents' tokens and dollars go" }),
1476
2536
  /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
1477
2537
  /* @__PURE__ */ jsxs10(Text11, { children: [
1478
2538
  /* @__PURE__ */ jsx11(Text11, { color: theme.accent, children: SPINNER[frame % SPINNER.length] }),