@tokz/cli 0.2.7 → 0.2.8

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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # tokz
2
2
 
3
+ [![CI](https://github.com/tokz-dev/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/tokz-dev/cli/actions/workflows/ci.yml)
4
+
3
5
  **See where your coding agents' tokens — and dollars — actually go.**
4
6
 
5
7
  tokz reads the session logs your coding agents already write to disk and turns
@@ -39,20 +41,31 @@ tokz auto-detects each agent from its local data directory. No configuration.
39
41
  | Kilo | `~/.local/share/kilo/kilo.db` | ✅ parsed (SQLite) |
40
42
  | Goose | `~/.local/share/goose` | ✅ parsed (SQLite) |
41
43
  | Hermes | `~/.hermes/state.db` | ✅ parsed (SQLite) |
44
+ | GitHub Copilot CLI | `~/.copilot/otel` | ✅ parsed (OpenTelemetry) |
45
+ | Amp | `~/.local/share/amp` | ✅ parsed |
42
46
  | Antigravity | `~/.gemini/antigravity-cli` | ≈ estimated¹ |
43
- | GitHub Copilot CLI | `~/.copilot/otel` | 🔍 detected, not parsed² |
44
- | Amp | `~/.local/share/amp` | 🔍 detected, not parsed² |
47
+ | Cursor | `~/.cursor` | 🔍 detected, not parsed² |
45
48
 
46
49
  ¹ Antigravity stores no token counts on disk. tokz reads real per-model turn
47
50
  counts from its conversation databases and estimates tokens from content size
48
51
  (~4 chars/token); the UI labels every Antigravity number **estimated**.
49
52
 
50
- ² Detected and listed with the reason, but not yet parsed: Copilot stores usage
51
- as OpenTelemetry spans, Amp as a usage ledger. (Cursor is intentionally left
52
- out — it keeps no token counts locally; usage lives server-side behind auth.)
53
+ ² Cursor keeps no token counts locally its usage lives server-side behind
54
+ auth so it's surfaced with that reason rather than parsed.
55
+
56
+ The SQLite-backed agents are read with a small built-in pure-JS SQLite reader,
57
+ so there's no native dependency and `npx` just works. When more than one agent
58
+ has data, the picker adds an **All agents** row that merges every agent's
59
+ projects into one cross-agent total.
60
+
61
+ ## Config file (optional)
53
62
 
54
- The three SQLite-backed agents are read with a small built-in pure-JS SQLite
55
- reader, so there's no native dependency and `npx` just works.
63
+ Drop defaults in `~/.tokz/config.json` so you don't retype flags; any CLI flag
64
+ still overrides it:
65
+
66
+ ```json
67
+ { "timezone": "local", "offline": false, "costSource": "auto", "days": 30 }
68
+ ```
56
69
 
57
70
  ## Interactive TUI
58
71
 
@@ -27,14 +27,15 @@ import { useEffect as useEffect2, useState as useState6 } from "react";
27
27
  import { Box as Box10, Text as Text11, useApp as useApp2 } from "ink";
28
28
 
29
29
  // src/agents/index.ts
30
- import { access as access15 } from "fs/promises";
31
- import { homedir as homedir16 } from "os";
32
- import { join as join16 } from "path";
30
+ import { access as access17 } from "fs/promises";
31
+ import { homedir as homedir18 } from "os";
32
+ import { join as join18 } from "path";
33
33
 
34
- // src/agents/antigravity.ts
35
- import { access, readFile as readFile2, readdir } from "fs/promises";
34
+ // src/agents/amp.ts
35
+ import { access } from "fs/promises";
36
36
  import { homedir as homedir2 } from "os";
37
37
  import { join as join2 } from "path";
38
+ import { glob as glob2 } from "tinyglobby";
38
39
 
39
40
  // src/projects.ts
40
41
  import { readFile } from "fs/promises";
@@ -203,11 +204,180 @@ async function loadProjects(home = homedir(), onProgress) {
203
204
  return out;
204
205
  }
205
206
 
207
+ // src/agents/usage.ts
208
+ import { readFile as readFile2 } from "fs/promises";
209
+ async function readJson(file) {
210
+ try {
211
+ return JSON.parse(await readFile2(file, "utf8"));
212
+ } catch {
213
+ return void 0;
214
+ }
215
+ }
216
+ async function readJsonl(file) {
217
+ let raw;
218
+ try {
219
+ raw = await readFile2(file, "utf8");
220
+ } catch {
221
+ return [];
222
+ }
223
+ const out = [];
224
+ for (const line of raw.split("\n")) {
225
+ if (!line.trim()) continue;
226
+ try {
227
+ out.push(JSON.parse(line));
228
+ } catch {
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+ function sessionFromRecords(file, cwd, records) {
234
+ const stats = { file, cwd, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
235
+ for (const r of records) {
236
+ if (r.input + r.output + r.cacheRead + r.cacheWrite === 0) continue;
237
+ if (r.ts) {
238
+ if (!stats.firstTs || r.ts < stats.firstTs) stats.firstTs = r.ts;
239
+ if (!stats.lastTs || r.ts > stats.lastTs) stats.lastTs = r.ts;
240
+ }
241
+ const accs = [stats.usageByModel[r.model] ??= emptyUsage()];
242
+ if (r.ts) {
243
+ const day = stats.dailyUsage[r.ts.slice(0, 10)] ??= {};
244
+ accs.push(day[r.model] ??= emptyUsage());
245
+ }
246
+ for (const u of accs) {
247
+ u.inputTokens += r.input;
248
+ u.outputTokens += r.output;
249
+ u.cacheReadTokens += r.cacheRead;
250
+ u.cacheCreationTokens += r.cacheWrite;
251
+ u.turns += 1;
252
+ }
253
+ }
254
+ return stats;
255
+ }
256
+ function pickNum(obj, keys) {
257
+ if (!obj || typeof obj !== "object") return 0;
258
+ const rec = obj;
259
+ for (const k of keys) {
260
+ const v = rec[k];
261
+ const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
262
+ if (Number.isFinite(n)) return Math.max(0, Math.trunc(n));
263
+ }
264
+ return 0;
265
+ }
266
+ function str(obj, key) {
267
+ if (!obj || typeof obj !== "object") return void 0;
268
+ const v = obj[key];
269
+ return typeof v === "string" && v.length > 0 ? v : void 0;
270
+ }
271
+ function toIso(value) {
272
+ if (typeof value === "string") {
273
+ const t = Date.parse(value);
274
+ return Number.isFinite(t) ? new Date(t).toISOString() : void 0;
275
+ }
276
+ if (typeof value === "number" || typeof value === "bigint") {
277
+ let ms = Number(value);
278
+ if (!Number.isFinite(ms) || ms <= 0) return void 0;
279
+ if (ms < 1e12) ms *= 1e3;
280
+ return new Date(ms).toISOString();
281
+ }
282
+ return void 0;
283
+ }
284
+ function deepFind(node, keys, depth = 6) {
285
+ if (!node || typeof node !== "object" || depth < 0) return void 0;
286
+ const rec = node;
287
+ if (!Array.isArray(node) && keys.some((k) => k in rec)) return rec;
288
+ for (const v of Object.values(rec)) {
289
+ const hit = deepFind(v, keys, depth - 1);
290
+ if (hit) return hit;
291
+ }
292
+ return void 0;
293
+ }
294
+
295
+ // src/agents/amp.ts
296
+ function ampRoot(home) {
297
+ const env = process.env.AMP_DATA_DIR;
298
+ return env ? env.split(",")[0].trim() : join2(home ?? homedir2(), ".local", "share", "amp");
299
+ }
300
+ function parseThread(thread) {
301
+ const messages = thread?.messages ?? [];
302
+ const events = thread?.usageLedger?.events;
303
+ const records = [];
304
+ if (Array.isArray(events)) {
305
+ const cacheByMsg = /* @__PURE__ */ new Map();
306
+ for (const m of messages) {
307
+ if (typeof m.messageId === "number" && m.usage) {
308
+ cacheByMsg.set(m.messageId, {
309
+ cw: m.usage.cacheCreationInputTokens ?? 0,
310
+ cr: m.usage.cacheReadInputTokens ?? 0
311
+ });
312
+ }
313
+ }
314
+ for (const e of events) {
315
+ const tokens = e.tokens;
316
+ const toMsg = e.toMessageId;
317
+ const cache = typeof toMsg === "number" ? cacheByMsg.get(toMsg) : void 0;
318
+ records.push({
319
+ model: str(e, "model") ?? "amp-unknown",
320
+ ts: toIso(str(e, "timestamp")),
321
+ input: pickNum(tokens, ["input"]),
322
+ output: pickNum(tokens, ["output"]),
323
+ cacheRead: cache?.cr ?? 0,
324
+ cacheWrite: cache?.cw ?? 0
325
+ });
326
+ }
327
+ return records;
328
+ }
329
+ for (const m of messages) {
330
+ if (m.role !== "assistant" || !m.usage) continue;
331
+ records.push({
332
+ model: m.model ?? "amp-unknown",
333
+ ts: toIso(m.timestamp),
334
+ input: m.usage.inputTokens ?? 0,
335
+ output: m.usage.outputTokens ?? 0,
336
+ cacheRead: m.usage.cacheReadInputTokens ?? 0,
337
+ cacheWrite: m.usage.cacheCreationInputTokens ?? 0
338
+ });
339
+ }
340
+ return records;
341
+ }
342
+ async function loadAmpProjects(home, onProgress) {
343
+ const files = await glob2(["threads/**/*.json"], { cwd: ampRoot(home), absolute: true }).catch(
344
+ () => []
345
+ );
346
+ const sessions = [];
347
+ let parsed = 0;
348
+ for (const f of files) {
349
+ onProgress?.({ parsed, total: files.length, currentProject: "Amp threads" });
350
+ const thread = await readJson(f);
351
+ const id = str(thread, "id") ?? f;
352
+ sessions.push(sessionFromRecords(id, void 0, parseThread(thread)));
353
+ parsed += 1;
354
+ onProgress?.({ parsed, total: files.length, currentProject: "Amp threads" });
355
+ }
356
+ return groupSessionsByCwd("amp", sessions);
357
+ }
358
+ var ampAdapter = {
359
+ id: "amp",
360
+ name: "Amp",
361
+ supported: true,
362
+ async detect(home) {
363
+ try {
364
+ await access(join2(ampRoot(home), "threads"));
365
+ return true;
366
+ } catch {
367
+ return false;
368
+ }
369
+ },
370
+ loadProjects: loadAmpProjects
371
+ };
372
+
206
373
  // src/agents/antigravity.ts
374
+ import { access as access2, readFile as readFile3, readdir } from "fs/promises";
375
+ import { homedir as homedir3 } from "os";
376
+ import { join as join3 } from "path";
207
377
  var ESTIMATED_CHARS_PER_TOKEN = 4;
208
378
  var INPUT_SHARE = 0.85;
209
379
  function antigravityRoot(home) {
210
- return join2(home ?? homedir2(), ".gemini", "antigravity-cli");
380
+ return join3(home ?? homedir3(), ".gemini", "antigravity-cli");
211
381
  }
212
382
  function modelIdFromDisplayName(name) {
213
383
  const bare = name.replace(/\s*\(.*\)\s*$/, "").trim();
@@ -241,7 +411,7 @@ function scanConversation(buf) {
241
411
  }
242
412
  async function readHistory(root) {
243
413
  try {
244
- const raw = await readFile2(join2(root, "history.jsonl"), "utf8");
414
+ const raw = await readFile3(join3(root, "history.jsonl"), "utf8");
245
415
  const out = [];
246
416
  for (const line of raw.split("\n")) {
247
417
  if (!line.trim()) continue;
@@ -257,7 +427,7 @@ async function readHistory(root) {
257
427
  }
258
428
  async function loadAntigravityProjects(home, onProgress) {
259
429
  const root = antigravityRoot(home);
260
- const convDir = join2(root, "conversations");
430
+ const convDir = join3(root, "conversations");
261
431
  let dbFiles;
262
432
  try {
263
433
  dbFiles = (await readdir(convDir)).filter((f) => f.endsWith(".db"));
@@ -281,7 +451,7 @@ async function loadAntigravityProjects(home, onProgress) {
281
451
  onProgress?.({ parsed, total: dbFiles.length, currentProject: "Antigravity conversations" });
282
452
  let scan;
283
453
  try {
284
- scan = scanConversation(await readFile2(join2(convDir, file)));
454
+ scan = scanConversation(await readFile3(join3(convDir, file)));
285
455
  } catch {
286
456
  parsed += 1;
287
457
  continue;
@@ -339,11 +509,11 @@ var antigravityAdapter = {
339
509
  estimateNote: "no token counts on disk \u2014 tokens/costs estimated from conversation size",
340
510
  async detect(home) {
341
511
  try {
342
- await access(join2(antigravityRoot(home), "conversations"));
512
+ await access2(join3(antigravityRoot(home), "conversations"));
343
513
  return true;
344
514
  } catch {
345
515
  try {
346
- await access(join2(home ?? homedir2(), ".gemini", "antigravity", "conversations"));
516
+ await access2(join3(home ?? homedir3(), ".gemini", "antigravity", "conversations"));
347
517
  return true;
348
518
  } catch {
349
519
  return false;
@@ -354,16 +524,16 @@ var antigravityAdapter = {
354
524
  };
355
525
 
356
526
  // src/agents/claude.ts
357
- import { access as access2 } from "fs/promises";
358
- import { homedir as homedir3 } from "os";
359
- import { join as join3 } from "path";
527
+ import { access as access3 } from "fs/promises";
528
+ import { homedir as homedir4 } from "os";
529
+ import { join as join4 } from "path";
360
530
  var claudeAdapter = {
361
531
  id: "claude",
362
532
  name: "Claude Code",
363
533
  supported: true,
364
- async detect(home = homedir3()) {
534
+ async detect(home = homedir4()) {
365
535
  try {
366
- await access2(join3(home, ".claude", "projects"));
536
+ await access3(join4(home, ".claude", "projects"));
367
537
  return true;
368
538
  } catch {
369
539
  return false;
@@ -373,106 +543,16 @@ var claudeAdapter = {
373
543
  };
374
544
 
375
545
  // src/agents/codebuff.ts
376
- import { access as access3, stat } from "fs/promises";
377
- import { homedir as homedir4 } from "os";
378
- import { join as join4 } from "path";
379
- import { glob as glob2 } from "tinyglobby";
380
-
381
- // src/agents/usage.ts
382
- import { readFile as readFile3 } from "fs/promises";
383
- async function readJson(file) {
384
- try {
385
- return JSON.parse(await readFile3(file, "utf8"));
386
- } catch {
387
- return void 0;
388
- }
389
- }
390
- async function readJsonl(file) {
391
- let raw;
392
- try {
393
- raw = await readFile3(file, "utf8");
394
- } catch {
395
- return [];
396
- }
397
- const out = [];
398
- for (const line of raw.split("\n")) {
399
- if (!line.trim()) continue;
400
- try {
401
- out.push(JSON.parse(line));
402
- } catch {
403
- }
404
- }
405
- return out;
406
- }
407
- function sessionFromRecords(file, cwd, records) {
408
- const stats = { file, cwd, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
409
- for (const r of records) {
410
- if (r.input + r.output + r.cacheRead + r.cacheWrite === 0) continue;
411
- if (r.ts) {
412
- if (!stats.firstTs || r.ts < stats.firstTs) stats.firstTs = r.ts;
413
- if (!stats.lastTs || r.ts > stats.lastTs) stats.lastTs = r.ts;
414
- }
415
- const accs = [stats.usageByModel[r.model] ??= emptyUsage()];
416
- if (r.ts) {
417
- const day = stats.dailyUsage[r.ts.slice(0, 10)] ??= {};
418
- accs.push(day[r.model] ??= emptyUsage());
419
- }
420
- for (const u of accs) {
421
- u.inputTokens += r.input;
422
- u.outputTokens += r.output;
423
- u.cacheReadTokens += r.cacheRead;
424
- u.cacheCreationTokens += r.cacheWrite;
425
- u.turns += 1;
426
- }
427
- }
428
- return stats;
429
- }
430
- function pickNum(obj, keys) {
431
- if (!obj || typeof obj !== "object") return 0;
432
- const rec = obj;
433
- for (const k of keys) {
434
- const v = rec[k];
435
- const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
436
- if (Number.isFinite(n)) return Math.max(0, Math.trunc(n));
437
- }
438
- return 0;
439
- }
440
- function str(obj, key) {
441
- if (!obj || typeof obj !== "object") return void 0;
442
- const v = obj[key];
443
- return typeof v === "string" && v.length > 0 ? v : void 0;
444
- }
445
- function toIso(value) {
446
- if (typeof value === "string") {
447
- const t = Date.parse(value);
448
- return Number.isFinite(t) ? new Date(t).toISOString() : void 0;
449
- }
450
- if (typeof value === "number" || typeof value === "bigint") {
451
- let ms = Number(value);
452
- if (!Number.isFinite(ms) || ms <= 0) return void 0;
453
- if (ms < 1e12) ms *= 1e3;
454
- return new Date(ms).toISOString();
455
- }
456
- return void 0;
457
- }
458
- function deepFind(node, keys, depth = 6) {
459
- if (!node || typeof node !== "object" || depth < 0) return void 0;
460
- const rec = node;
461
- if (!Array.isArray(node) && keys.some((k) => k in rec)) return rec;
462
- for (const v of Object.values(rec)) {
463
- const hit = deepFind(v, keys, depth - 1);
464
- if (hit) return hit;
465
- }
466
- return void 0;
467
- }
468
-
469
- // src/agents/codebuff.ts
546
+ import { access as access4, stat } from "fs/promises";
547
+ import { homedir as homedir5 } from "os";
548
+ import { join as join5 } from "path";
549
+ import { glob as glob3 } from "tinyglobby";
470
550
  var CHANNELS = ["manicode", "manicode-dev", "manicode-staging"];
471
551
  function codebuffRoots(home) {
472
552
  const env = process.env.CODEBUFF_DATA_DIR;
473
553
  if (env) return env.split(",").map((s) => s.trim()).filter(Boolean);
474
- const h = home ?? homedir4();
475
- return CHANNELS.map((c) => join4(h, ".config", c));
554
+ const h = home ?? homedir5();
555
+ return CHANNELS.map((c) => join5(h, ".config", c));
476
556
  }
477
557
  function isAssistant(msg) {
478
558
  const role = str(msg, "variant") ?? str(msg, "role");
@@ -507,7 +587,7 @@ async function loadCodebuffProjects(home, onProgress) {
507
587
  const files = [];
508
588
  for (const root of codebuffRoots(home)) {
509
589
  files.push(
510
- ...await glob2(["**/chat-messages.json"], { cwd: root, absolute: true }).catch(() => [])
590
+ ...await glob3(["**/chat-messages.json"], { cwd: root, absolute: true }).catch(() => [])
511
591
  );
512
592
  }
513
593
  const sessions = [];
@@ -527,7 +607,7 @@ var codebuffAdapter = {
527
607
  async detect(home) {
528
608
  for (const root of codebuffRoots(home)) {
529
609
  try {
530
- await access3(root);
610
+ await access4(root);
531
611
  return true;
532
612
  } catch {
533
613
  }
@@ -539,11 +619,11 @@ var codebuffAdapter = {
539
619
 
540
620
  // src/agents/codex.ts
541
621
  import { createReadStream } from "fs";
542
- import { access as access4 } from "fs/promises";
622
+ import { access as access5 } from "fs/promises";
543
623
  import { createInterface } from "readline";
544
- import { homedir as homedir5 } from "os";
545
- import { join as join5 } from "path";
546
- import { glob as glob3 } from "tinyglobby";
624
+ import { homedir as homedir6 } from "os";
625
+ import { join as join6 } from "path";
626
+ import { glob as glob4 } from "tinyglobby";
547
627
  import { z } from "zod";
548
628
  var TokenTotals = z.object({
549
629
  input_tokens: z.number().catch(0).default(0),
@@ -566,8 +646,8 @@ var Line = z.object({
566
646
  }).passthrough().optional()
567
647
  });
568
648
  function codexHome(home) {
569
- if (home) return join5(home, ".codex");
570
- return process.env.CODEX_HOME ?? join5(homedir5(), ".codex");
649
+ if (home) return join6(home, ".codex");
650
+ return process.env.CODEX_HOME ?? join6(homedir6(), ".codex");
571
651
  }
572
652
  var DEFAULT_MODEL = "gpt-5";
573
653
  async function parseCodexRollout(file) {
@@ -644,7 +724,7 @@ async function parseCodexRollout(file) {
644
724
  }
645
725
  async function loadCodexProjects(home, onProgress) {
646
726
  const root = codexHome(home);
647
- const files = await glob3(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
727
+ const files = await glob4(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
648
728
  cwd: root,
649
729
  absolute: true
650
730
  }).catch(() => []);
@@ -665,7 +745,7 @@ var codexAdapter = {
665
745
  supported: true,
666
746
  async detect(home) {
667
747
  try {
668
- await access4(join5(codexHome(home), "sessions"));
748
+ await access5(join6(codexHome(home), "sessions"));
669
749
  return true;
670
750
  } catch {
671
751
  return false;
@@ -674,13 +754,113 @@ var codexAdapter = {
674
754
  loadProjects: loadCodexProjects
675
755
  };
676
756
 
757
+ // src/agents/copilot.ts
758
+ import { access as access6 } from "fs/promises";
759
+ import { homedir as homedir7 } from "os";
760
+ import { join as join7 } from "path";
761
+ import { glob as glob5 } from "tinyglobby";
762
+ function copilotDir(home) {
763
+ return join7(home ?? homedir7(), ".copilot", "otel");
764
+ }
765
+ var MODEL_ATTRS = ["gen_ai.response.model", "gen_ai.request.model"];
766
+ var SESSION_ATTRS = [
767
+ "gen_ai.conversation.id",
768
+ "copilot_chat.session_id",
769
+ "copilot_chat.chat_session_id",
770
+ "session.id",
771
+ "github.copilot.interaction_id",
772
+ "gen_ai.response.id"
773
+ ];
774
+ function unwrap(v) {
775
+ if (v && typeof v === "object" && !Array.isArray(v)) {
776
+ const o = v;
777
+ return o.intValue ?? o.doubleValue ?? o.stringValue ?? o.value ?? v;
778
+ }
779
+ return v;
780
+ }
781
+ function attrNum(attrs, key) {
782
+ const v = unwrap(attrs[key]);
783
+ const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
784
+ return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0;
785
+ }
786
+ function attrStr(attrs, keys) {
787
+ for (const k of keys) {
788
+ const v = unwrap(attrs[k]);
789
+ if (typeof v === "string" && v.length > 0) return v;
790
+ if (typeof v === "number") return String(v);
791
+ }
792
+ return void 0;
793
+ }
794
+ function parseOtelFile(records) {
795
+ const bySession = /* @__PURE__ */ new Map();
796
+ const seen = /* @__PURE__ */ new Set();
797
+ for (const rec of records) {
798
+ const attrs = rec.attributes;
799
+ if (!attrs || typeof attrs !== "object") continue;
800
+ const input = attrNum(attrs, "gen_ai.usage.input_tokens");
801
+ const output = attrNum(attrs, "gen_ai.usage.output_tokens");
802
+ const cacheRead = attrNum(attrs, "gen_ai.usage.cache_read.input_tokens");
803
+ const cacheWrite = attrNum(attrs, "gen_ai.usage.cache_write.input_tokens") || attrNum(attrs, "gen_ai.usage.cache_creation.input_tokens");
804
+ const reasoning = attrNum(attrs, "gen_ai.usage.reasoning.output_tokens") || attrNum(attrs, "gen_ai.usage.reasoning_tokens");
805
+ if (input + output + cacheRead + cacheWrite === 0) continue;
806
+ const session = attrStr(attrs, SESSION_ATTRS) ?? "copilot";
807
+ const r = rec;
808
+ const dedup = attrStr(attrs, ["gen_ai.response.id"]) ?? `${String(unwrap(r.spanId) ?? "")}:${session}:${input}:${output}`;
809
+ if (seen.has(dedup)) continue;
810
+ seen.add(dedup);
811
+ const list = bySession.get(session) ?? [];
812
+ list.push({
813
+ model: attrStr(attrs, MODEL_ATTRS) ?? "copilot-unknown",
814
+ ts: toIso(r.timestamp) ?? toIso(r.startTime),
815
+ input,
816
+ output: output + reasoning,
817
+ cacheRead,
818
+ cacheWrite
819
+ });
820
+ bySession.set(session, list);
821
+ }
822
+ return bySession;
823
+ }
824
+ async function loadCopilotProjects(home, onProgress) {
825
+ const files = await glob5(["**/*.jsonl"], { cwd: copilotDir(home), absolute: true }).catch(() => []);
826
+ const bySession = /* @__PURE__ */ new Map();
827
+ let parsed = 0;
828
+ for (const f of files) {
829
+ onProgress?.({ parsed, total: files.length, currentProject: "Copilot otel" });
830
+ for (const [session, records] of parseOtelFile(await readJsonl(f))) {
831
+ const list = bySession.get(session) ?? [];
832
+ list.push(...records);
833
+ bySession.set(session, list);
834
+ }
835
+ parsed += 1;
836
+ onProgress?.({ parsed, total: files.length, currentProject: "Copilot otel" });
837
+ }
838
+ const sessions = [];
839
+ for (const [id, records] of bySession) sessions.push(sessionFromRecords(id, void 0, records));
840
+ return groupSessionsByCwd("copilot", sessions);
841
+ }
842
+ var copilotAdapter = {
843
+ id: "copilot",
844
+ name: "GitHub Copilot CLI",
845
+ supported: true,
846
+ async detect(home) {
847
+ try {
848
+ await access6(copilotDir(home));
849
+ return true;
850
+ } catch {
851
+ return false;
852
+ }
853
+ },
854
+ loadProjects: loadCopilotProjects
855
+ };
856
+
677
857
  // src/agents/droid.ts
678
- import { access as access5, stat as stat2 } from "fs/promises";
679
- import { homedir as homedir6 } from "os";
680
- import { basename as basename2, join as join6 } from "path";
681
- import { glob as glob4 } from "tinyglobby";
858
+ import { access as access7, stat as stat2 } from "fs/promises";
859
+ import { homedir as homedir8 } from "os";
860
+ import { basename as basename2, join as join8 } from "path";
861
+ import { glob as glob6 } from "tinyglobby";
682
862
  function droidRoot(home) {
683
- return process.env.DROID_SESSIONS_DIR ?? join6(home ?? homedir6(), ".factory", "sessions");
863
+ return process.env.DROID_SESSIONS_DIR ?? join8(home ?? homedir8(), ".factory", "sessions");
684
864
  }
685
865
  async function parseFile2(file) {
686
866
  const settings = await readJson(file);
@@ -705,7 +885,7 @@ async function parseFile2(file) {
705
885
  ]);
706
886
  }
707
887
  async function loadDroidProjects(home, onProgress) {
708
- const files = (await glob4(["**/*.settings.json"], { cwd: droidRoot(home), absolute: true }).catch(() => [])).filter((f) => basename2(f).endsWith(".settings.json"));
888
+ const files = (await glob6(["**/*.settings.json"], { cwd: droidRoot(home), absolute: true }).catch(() => [])).filter((f) => basename2(f).endsWith(".settings.json"));
709
889
  const sessions = [];
710
890
  let parsed = 0;
711
891
  for (const f of files) {
@@ -722,7 +902,7 @@ var droidAdapter = {
722
902
  supported: true,
723
903
  async detect(home) {
724
904
  try {
725
- await access5(droidRoot(home));
905
+ await access7(droidRoot(home));
726
906
  return true;
727
907
  } catch {
728
908
  return false;
@@ -732,12 +912,12 @@ var droidAdapter = {
732
912
  };
733
913
 
734
914
  // src/agents/gemini.ts
735
- import { access as access6 } from "fs/promises";
736
- import { homedir as homedir7 } from "os";
737
- import { join as join7 } from "path";
738
- import { glob as glob5 } from "tinyglobby";
915
+ import { access as access8 } from "fs/promises";
916
+ import { homedir as homedir9 } from "os";
917
+ import { join as join9 } from "path";
918
+ import { glob as glob7 } from "tinyglobby";
739
919
  function geminiRoot(home) {
740
- return process.env.GEMINI_DATA_DIR ?? join7(home ?? homedir7(), ".gemini", "tmp");
920
+ return process.env.GEMINI_DATA_DIR ?? join9(home ?? homedir9(), ".gemini", "tmp");
741
921
  }
742
922
  var IN = ["input", "prompt", "input_tokens", "prompt_tokens"];
743
923
  var OUT = ["output", "candidates", "output_tokens", "candidates_tokens"];
@@ -769,7 +949,7 @@ async function parseFile3(file) {
769
949
  return sessionFromRecords(file, void 0, records);
770
950
  }
771
951
  async function loadGeminiProjects(home, onProgress) {
772
- const files = await glob5(["**/*.json", "**/*.jsonl"], { cwd: geminiRoot(home), absolute: true }).catch(
952
+ const files = await glob7(["**/*.json", "**/*.jsonl"], { cwd: geminiRoot(home), absolute: true }).catch(
773
953
  () => []
774
954
  );
775
955
  const sessions = [];
@@ -788,7 +968,7 @@ var geminiAdapter = {
788
968
  supported: true,
789
969
  async detect(home) {
790
970
  try {
791
- await access6(geminiRoot(home));
971
+ await access8(geminiRoot(home));
792
972
  return true;
793
973
  } catch {
794
974
  return false;
@@ -798,9 +978,9 @@ var geminiAdapter = {
798
978
  };
799
979
 
800
980
  // src/agents/goose.ts
801
- import { access as access7 } from "fs/promises";
802
- import { homedir as homedir8 } from "os";
803
- import { join as join8 } from "path";
981
+ import { access as access9 } from "fs/promises";
982
+ import { homedir as homedir10 } from "os";
983
+ import { join as join10 } from "path";
804
984
 
805
985
  // src/sqlite.ts
806
986
  import { readFile as readFile4 } from "fs/promises";
@@ -983,9 +1163,9 @@ var GOOSE_DB_CANDIDATES = [
983
1163
  ];
984
1164
  function gooseDbCandidates(home) {
985
1165
  const root = process.env.GOOSE_PATH_ROOT;
986
- if (root) return [join8(root, "data", "sessions", "sessions.db")];
987
- const h = home ?? homedir8();
988
- return GOOSE_DB_CANDIDATES.map((parts) => join8(h, ...parts));
1166
+ if (root) return [join10(root, "data", "sessions", "sessions.db")];
1167
+ const h = home ?? homedir10();
1168
+ return GOOSE_DB_CANDIDATES.map((parts) => join10(h, ...parts));
989
1169
  }
990
1170
  async function loadGooseProjects(home, onProgress) {
991
1171
  const records = [];
@@ -1022,7 +1202,7 @@ var gooseAdapter = {
1022
1202
  async detect(home) {
1023
1203
  for (const db of gooseDbCandidates(home)) {
1024
1204
  try {
1025
- await access7(db);
1205
+ await access9(db);
1026
1206
  return true;
1027
1207
  } catch {
1028
1208
  }
@@ -1033,13 +1213,13 @@ var gooseAdapter = {
1033
1213
  };
1034
1214
 
1035
1215
  // src/agents/hermes.ts
1036
- import { access as access8 } from "fs/promises";
1037
- import { homedir as homedir9 } from "os";
1038
- import { join as join9 } from "path";
1216
+ import { access as access10 } from "fs/promises";
1217
+ import { homedir as homedir11 } from "os";
1218
+ import { join as join11 } from "path";
1039
1219
  function hermesDb(home) {
1040
1220
  const env = process.env.HERMES_HOME;
1041
- const root = env ? env.split(",")[0].trim() : join9(home ?? homedir9(), ".hermes");
1042
- return join9(root, "state.db");
1221
+ const root = env ? env.split(",")[0].trim() : join11(home ?? homedir11(), ".hermes");
1222
+ return join11(root, "state.db");
1043
1223
  }
1044
1224
  async function loadHermesProjects(home, onProgress) {
1045
1225
  const rows = await readTable(hermesDb(home), "sessions").catch(() => []);
@@ -1064,7 +1244,7 @@ var hermesAdapter = {
1064
1244
  supported: true,
1065
1245
  async detect(home) {
1066
1246
  try {
1067
- await access8(hermesDb(home));
1247
+ await access10(hermesDb(home));
1068
1248
  return true;
1069
1249
  } catch {
1070
1250
  return false;
@@ -1074,14 +1254,14 @@ var hermesAdapter = {
1074
1254
  };
1075
1255
 
1076
1256
  // src/agents/kilo.ts
1077
- import { access as access9 } from "fs/promises";
1078
- import { homedir as homedir10 } from "os";
1079
- import { join as join10 } from "path";
1257
+ import { access as access11 } from "fs/promises";
1258
+ import { homedir as homedir12 } from "os";
1259
+ import { join as join12 } from "path";
1080
1260
  function kiloDir(home) {
1081
- return process.env.KILO_DATA_DIR ?? join10(home ?? homedir10(), ".local", "share", "kilo");
1261
+ return process.env.KILO_DATA_DIR ?? join12(home ?? homedir12(), ".local", "share", "kilo");
1082
1262
  }
1083
1263
  async function loadKiloProjects(home, onProgress) {
1084
- const db = join10(kiloDir(home), "kilo.db");
1264
+ const db = join12(kiloDir(home), "kilo.db");
1085
1265
  let rows;
1086
1266
  try {
1087
1267
  rows = await readTable(db, "message");
@@ -1123,7 +1303,7 @@ var kiloAdapter = {
1123
1303
  supported: true,
1124
1304
  async detect(home) {
1125
1305
  try {
1126
- await access9(join10(kiloDir(home), "kilo.db"));
1306
+ await access11(join12(kiloDir(home), "kilo.db"));
1127
1307
  return true;
1128
1308
  } catch {
1129
1309
  return false;
@@ -1133,17 +1313,17 @@ var kiloAdapter = {
1133
1313
  };
1134
1314
 
1135
1315
  // src/agents/kimi.ts
1136
- import { access as access10 } from "fs/promises";
1137
- import { homedir as homedir11 } from "os";
1138
- import { join as join11 } from "path";
1139
- import { glob as glob6 } from "tinyglobby";
1316
+ import { access as access12 } from "fs/promises";
1317
+ import { homedir as homedir13 } from "os";
1318
+ import { join as join13 } from "path";
1319
+ import { glob as glob8 } from "tinyglobby";
1140
1320
  var KIMI_DIRS = [".kimi", ".kimi-code"];
1141
1321
  var USAGE_KEYS = ["inputOther", "input_other", "inputCacheRead", "input_cache_read"];
1142
1322
  function kimiRoots(home) {
1143
1323
  const env = process.env.KIMI_DATA_DIR;
1144
1324
  if (env) return env.split(",").map((s) => s.trim()).filter(Boolean);
1145
- const h = home ?? homedir11();
1146
- return KIMI_DIRS.map((d) => join11(h, d));
1325
+ const h = home ?? homedir13();
1326
+ return KIMI_DIRS.map((d) => join13(h, d));
1147
1327
  }
1148
1328
  async function parseFile4(file) {
1149
1329
  const records = [];
@@ -1165,7 +1345,7 @@ async function loadKimiProjects(home, onProgress) {
1165
1345
  const files = [];
1166
1346
  for (const root of kimiRoots(home)) {
1167
1347
  files.push(
1168
- ...await glob6(["sessions/**/wire.jsonl"], { cwd: root, absolute: true }).catch(() => [])
1348
+ ...await glob8(["sessions/**/wire.jsonl"], { cwd: root, absolute: true }).catch(() => [])
1169
1349
  );
1170
1350
  }
1171
1351
  const sessions = [];
@@ -1185,7 +1365,7 @@ var kimiAdapter = {
1185
1365
  async detect(home) {
1186
1366
  for (const root of kimiRoots(home)) {
1187
1367
  try {
1188
- await access10(join11(root, "sessions"));
1368
+ await access12(join13(root, "sessions"));
1189
1369
  return true;
1190
1370
  } catch {
1191
1371
  }
@@ -1196,16 +1376,16 @@ var kimiAdapter = {
1196
1376
  };
1197
1377
 
1198
1378
  // src/agents/openclaw.ts
1199
- import { access as access11 } from "fs/promises";
1200
- import { homedir as homedir12 } from "os";
1201
- import { join as join12 } from "path";
1202
- import { glob as glob7 } from "tinyglobby";
1379
+ import { access as access13 } from "fs/promises";
1380
+ import { homedir as homedir14 } from "os";
1381
+ import { join as join14 } from "path";
1382
+ import { glob as glob9 } from "tinyglobby";
1203
1383
  var OPENCLAW_DIRS = [".openclaw", ".clawdbot", ".moltbot", ".moldbot"];
1204
1384
  function openclawRoots(home) {
1205
1385
  const env = process.env.OPENCLAW_DIR;
1206
1386
  if (env) return env.split(",").map((s) => s.trim()).filter(Boolean);
1207
- const h = home ?? homedir12();
1208
- return OPENCLAW_DIRS.map((d) => join12(h, d));
1387
+ const h = home ?? homedir14();
1388
+ return OPENCLAW_DIRS.map((d) => join14(h, d));
1209
1389
  }
1210
1390
  function modelFrom(obj) {
1211
1391
  return str(obj, "modelId") ?? str(obj, "model");
@@ -1236,7 +1416,7 @@ async function parseFile5(file) {
1236
1416
  async function loadOpenclawProjects(home, onProgress) {
1237
1417
  const files = [];
1238
1418
  for (const root of openclawRoots(home)) {
1239
- files.push(...await glob7(["**/*.jsonl", "**/*.jsonl.*"], { cwd: root, absolute: true }).catch(() => []));
1419
+ files.push(...await glob9(["**/*.jsonl", "**/*.jsonl.*"], { cwd: root, absolute: true }).catch(() => []));
1240
1420
  }
1241
1421
  const sessions = [];
1242
1422
  let parsed = 0;
@@ -1255,7 +1435,7 @@ var openclawAdapter = {
1255
1435
  async detect(home) {
1256
1436
  for (const root of openclawRoots(home)) {
1257
1437
  try {
1258
- await access11(root);
1438
+ await access13(root);
1259
1439
  return true;
1260
1440
  } catch {
1261
1441
  }
@@ -1266,10 +1446,10 @@ var openclawAdapter = {
1266
1446
  };
1267
1447
 
1268
1448
  // src/agents/opencode.ts
1269
- import { access as access12, readFile as readFile5 } from "fs/promises";
1270
- import { homedir as homedir13 } from "os";
1271
- import { join as join13 } from "path";
1272
- import { glob as glob8 } from "tinyglobby";
1449
+ import { access as access14, readFile as readFile5 } from "fs/promises";
1450
+ import { homedir as homedir15 } from "os";
1451
+ import { join as join15 } from "path";
1452
+ import { glob as glob10 } from "tinyglobby";
1273
1453
  import { z as z2 } from "zod";
1274
1454
  var Message = z2.object({
1275
1455
  sessionID: z2.string(),
@@ -1292,8 +1472,8 @@ var Session = z2.object({
1292
1472
  });
1293
1473
  var Project = z2.object({ id: z2.string(), worktree: z2.string().optional() });
1294
1474
  function opencodeRoot(home) {
1295
- if (home) return join13(home, ".local", "share", "opencode");
1296
- return process.env.OPENCODE_DATA_DIR ?? join13(homedir13(), ".local", "share", "opencode");
1475
+ if (home) return join15(home, ".local", "share", "opencode");
1476
+ return process.env.OPENCODE_DATA_DIR ?? join15(homedir15(), ".local", "share", "opencode");
1297
1477
  }
1298
1478
  async function readJson2(file) {
1299
1479
  try {
@@ -1304,18 +1484,18 @@ async function readJson2(file) {
1304
1484
  }
1305
1485
  async function loadOpencodeProjects(home, onProgress) {
1306
1486
  const root = opencodeRoot(home);
1307
- const storage = join13(root, "storage");
1308
- const messageFiles = await glob8(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
1487
+ const storage = join15(root, "storage");
1488
+ const messageFiles = await glob10(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
1309
1489
  () => []
1310
1490
  );
1311
1491
  if (messageFiles.length === 0) return [];
1312
1492
  const sessionDir = /* @__PURE__ */ new Map();
1313
1493
  const projectWorktree = /* @__PURE__ */ new Map();
1314
- for (const f of await glob8(["project/*.json"], { cwd: storage, absolute: true }).catch(() => [])) {
1494
+ for (const f of await glob10(["project/*.json"], { cwd: storage, absolute: true }).catch(() => [])) {
1315
1495
  const p = Project.safeParse(await readJson2(f));
1316
1496
  if (p.success && p.data.worktree) projectWorktree.set(p.data.id, p.data.worktree);
1317
1497
  }
1318
- const sessionFiles = await glob8(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
1498
+ const sessionFiles = await glob10(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
1319
1499
  () => []
1320
1500
  );
1321
1501
  for (const f of sessionFiles) {
@@ -1369,7 +1549,7 @@ var opencodeAdapter = {
1369
1549
  supported: true,
1370
1550
  async detect(home) {
1371
1551
  try {
1372
- await access12(join13(opencodeRoot(home), "storage", "message"));
1552
+ await access14(join15(opencodeRoot(home), "storage", "message"));
1373
1553
  return true;
1374
1554
  } catch {
1375
1555
  return false;
@@ -1379,12 +1559,12 @@ var opencodeAdapter = {
1379
1559
  };
1380
1560
 
1381
1561
  // src/agents/pi.ts
1382
- import { access as access13 } from "fs/promises";
1383
- import { homedir as homedir14 } from "os";
1384
- import { join as join14 } from "path";
1385
- import { glob as glob9 } from "tinyglobby";
1562
+ import { access as access15 } from "fs/promises";
1563
+ import { homedir as homedir16 } from "os";
1564
+ import { join as join16 } from "path";
1565
+ import { glob as glob11 } from "tinyglobby";
1386
1566
  function piRoot(home) {
1387
- return process.env.PI_AGENT_DIR ?? join14(home ?? homedir14(), ".pi", "agent", "sessions");
1567
+ return process.env.PI_AGENT_DIR ?? join16(home ?? homedir16(), ".pi", "agent", "sessions");
1388
1568
  }
1389
1569
  async function parseFile6(file) {
1390
1570
  const records = [];
@@ -1404,7 +1584,7 @@ async function parseFile6(file) {
1404
1584
  return sessionFromRecords(file, void 0, records);
1405
1585
  }
1406
1586
  async function loadPiProjects(home, onProgress) {
1407
- const files = await glob9(["**/*.jsonl"], { cwd: piRoot(home), absolute: true }).catch(() => []);
1587
+ const files = await glob11(["**/*.jsonl"], { cwd: piRoot(home), absolute: true }).catch(() => []);
1408
1588
  const sessions = [];
1409
1589
  let parsed = 0;
1410
1590
  for (const f of files) {
@@ -1421,7 +1601,7 @@ var piAdapter = {
1421
1601
  supported: true,
1422
1602
  async detect(home) {
1423
1603
  try {
1424
- await access13(piRoot(home));
1604
+ await access15(piRoot(home));
1425
1605
  return true;
1426
1606
  } catch {
1427
1607
  return false;
@@ -1431,12 +1611,12 @@ var piAdapter = {
1431
1611
  };
1432
1612
 
1433
1613
  // src/agents/qwen.ts
1434
- import { access as access14 } from "fs/promises";
1435
- import { homedir as homedir15 } from "os";
1436
- import { join as join15 } from "path";
1437
- import { glob as glob10 } from "tinyglobby";
1614
+ import { access as access16 } from "fs/promises";
1615
+ import { homedir as homedir17 } from "os";
1616
+ import { join as join17 } from "path";
1617
+ import { glob as glob12 } from "tinyglobby";
1438
1618
  function qwenRoot(home) {
1439
- return process.env.QWEN_DATA_DIR ?? join15(home ?? homedir15(), ".qwen");
1619
+ return process.env.QWEN_DATA_DIR ?? join17(home ?? homedir17(), ".qwen");
1440
1620
  }
1441
1621
  async function parseFile7(file) {
1442
1622
  const records = [];
@@ -1458,7 +1638,7 @@ async function parseFile7(file) {
1458
1638
  return sessionFromRecords(file, void 0, records);
1459
1639
  }
1460
1640
  async function loadQwenProjects(home, onProgress) {
1461
- const files = await glob10(["projects/**/*.jsonl"], { cwd: qwenRoot(home), absolute: true }).catch(
1641
+ const files = await glob12(["projects/**/*.jsonl"], { cwd: qwenRoot(home), absolute: true }).catch(
1462
1642
  () => []
1463
1643
  );
1464
1644
  const sessions = [];
@@ -1477,7 +1657,7 @@ var qwenAdapter = {
1477
1657
  supported: true,
1478
1658
  async detect(home) {
1479
1659
  try {
1480
- await access14(join15(qwenRoot(home), "projects"));
1660
+ await access16(join17(qwenRoot(home), "projects"));
1481
1661
  return true;
1482
1662
  } catch {
1483
1663
  return false;
@@ -1493,10 +1673,10 @@ function detectOnly(id, name, reason, ...pathCandidates) {
1493
1673
  name,
1494
1674
  supported: false,
1495
1675
  unsupportedReason: reason,
1496
- async detect(home = homedir16()) {
1676
+ async detect(home = homedir18()) {
1497
1677
  for (const parts of pathCandidates) {
1498
1678
  try {
1499
- await access15(join16(home, ...parts));
1679
+ await access17(join18(home, ...parts));
1500
1680
  return true;
1501
1681
  } catch {
1502
1682
  }
@@ -1519,14 +1699,13 @@ var ADAPTERS = [
1519
1699
  kiloAdapter,
1520
1700
  gooseAdapter,
1521
1701
  hermesAdapter,
1702
+ copilotAdapter,
1703
+ ampAdapter,
1522
1704
  piAdapter,
1523
1705
  antigravityAdapter,
1524
- // Detected but not parsed: Copilot uses OpenTelemetry spans, Amp a usage
1525
- // ledger (formats not wired yet). Cursor is intentionally absent it stores
1526
- // no token counts on disk (usage is server-side, reachable only with auth),
1527
- // which is out of scope for an offline tool.
1528
- detectOnly("copilot", "GitHub Copilot CLI", "usage is OpenTelemetry spans \u2014 parsing not wired yet", [".copilot", "otel"]),
1529
- detectOnly("amp", "Amp", "usage-ledger thread format \u2014 parsing not wired yet", [".local", "share", "amp"])
1706
+ // Cursor keeps no token counts on disk (usage is server-side, reachable only
1707
+ // with auth), so there's nothing to parse offlinesurfaced with the reason.
1708
+ detectOnly("cursor", "Cursor", "no token counts stored locally \u2014 usage is server-side", [".cursor"])
1530
1709
  ];
1531
1710
  async function loadAllAgents(home, onProgress) {
1532
1711
  return Promise.all(
@@ -2383,10 +2562,23 @@ function App({
2383
2562
  initialSelected = 0
2384
2563
  }) {
2385
2564
  const { exit } = useApp();
2386
- const agentList = useMemo2(
2387
- () => agents ?? wrapProjects(projects ?? []),
2388
- [agents, projects]
2389
- );
2565
+ const rawAgents = useMemo2(() => agents ?? wrapProjects(projects ?? []), [agents, projects]);
2566
+ const agentList = useMemo2(() => {
2567
+ const withData = rawAgents.filter((a) => a.projects.length > 0);
2568
+ if (withData.length <= 1) return rawAgents;
2569
+ const combined = {
2570
+ adapter: {
2571
+ id: "__all_agents__",
2572
+ name: "All agents",
2573
+ supported: true,
2574
+ detect: async () => true,
2575
+ loadProjects: async () => []
2576
+ },
2577
+ detected: true,
2578
+ projects: withData.flatMap((a) => a.projects)
2579
+ };
2580
+ return [combined, ...rawAgents];
2581
+ }, [rawAgents]);
2390
2582
  const multiAgent = agentList.length > 1;
2391
2583
  const firstWithData = Math.max(0, agentList.findIndex((a) => a.projects.length > 0));
2392
2584
  const [agentIdx, setAgentIdx] = useState5(firstWithData);
package/dist/cli.js CHANGED
@@ -23,10 +23,33 @@ import {
23
23
  import { createRequire } from "module";
24
24
  import { Command } from "commander";
25
25
 
26
- // src/livePricing.ts
27
- import { mkdir, readFile, writeFile } from "fs/promises";
26
+ // src/config.ts
27
+ import { readFileSync } from "fs";
28
28
  import { homedir } from "os";
29
29
  import { join } from "path";
30
+ var COST_SOURCES = /* @__PURE__ */ new Set(["auto", "cc", "calc", "both"]);
31
+ function loadConfig(home = homedir()) {
32
+ let raw;
33
+ try {
34
+ raw = JSON.parse(readFileSync(join(home, ".tokz", "config.json"), "utf8"));
35
+ } catch {
36
+ return {};
37
+ }
38
+ if (!raw || typeof raw !== "object") return {};
39
+ const r = raw;
40
+ const cfg = {};
41
+ if (typeof r.timezone === "string") cfg.timezone = r.timezone;
42
+ if (typeof r.offline === "boolean") cfg.offline = r.offline;
43
+ if (typeof r.costSource === "string" && COST_SOURCES.has(r.costSource))
44
+ cfg.costSource = r.costSource;
45
+ if (typeof r.days === "number" && Number.isFinite(r.days) && r.days > 0) cfg.days = Math.trunc(r.days);
46
+ return cfg;
47
+ }
48
+
49
+ // src/livePricing.ts
50
+ import { mkdir, readFile, writeFile } from "fs/promises";
51
+ import { homedir as homedir2 } from "os";
52
+ import { join as join2 } from "path";
30
53
  var LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
31
54
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
32
55
  var FETCH_TIMEOUT_MS = 3500;
@@ -54,8 +77,8 @@ function mapLitellmPrices(raw) {
54
77
  return out;
55
78
  }
56
79
  async function initPricing(opts) {
57
- const dir = opts?.cacheDir ?? join(homedir(), ".tokz");
58
- const file = join(dir, "litellm-prices.json");
80
+ const dir = opts?.cacheDir ?? join2(homedir2(), ".tokz");
81
+ const file = join2(dir, "litellm-prices.json");
59
82
  let cached;
60
83
  try {
61
84
  cached = JSON.parse(await readFile(file, "utf8"));
@@ -149,10 +172,11 @@ function renderReport(report) {
149
172
  var { version } = createRequire(import.meta.url)("../package.json");
150
173
  var program = new Command();
151
174
  program.name("tokz").description("Audit where your coding agent's context window and API dollars go.").option("--offline", "don't fetch live pricing; use cached/built-in rates").option("--timezone <zone>", 'group days in this timezone: "utc" (default), "local", or an IANA name').version(version);
175
+ var config = loadConfig();
152
176
  function applyGlobals() {
153
177
  const opts = program.opts();
154
- setTimezone(opts.timezone);
155
- return { offline: opts.offline };
178
+ setTimezone(opts.timezone ?? config.timezone);
179
+ return { offline: opts.offline ?? config.offline };
156
180
  }
157
181
  async function parseAll(projectPath, events) {
158
182
  const transcripts = await findTranscripts(projectPath);
@@ -176,7 +200,7 @@ program.command("audit").argument("[project]", "project path (default: current d
176
200
  return;
177
201
  }
178
202
  const servers = opts.all ? [] : await findMcpServers(projectPath);
179
- const days = opts.days ? Number.parseInt(opts.days, 10) : void 0;
203
+ const days = opts.days ? Number.parseInt(opts.days, 10) : config.days;
180
204
  const isoDay = (offset) => new Date(Date.now() - offset * 864e5).toISOString().slice(0, 10);
181
205
  const since = parseDateArg(opts.since);
182
206
  const until = parseDateArg(opts.until);
@@ -232,7 +256,8 @@ program.command("statusline").description("compact usage line for Claude Code's
232
256
  } catch {
233
257
  }
234
258
  const valid = ["auto", "cc", "calc", "both"];
235
- const costSource = valid.includes(opts.costSource ?? "auto") ? opts.costSource ?? "auto" : "auto";
259
+ const requested = opts.costSource ?? config.costSource ?? "auto";
260
+ const costSource = valid.includes(requested) ? requested : "auto";
236
261
  console.log(await sl.statusline(input, Date.now(), void 0, { costSource }));
237
262
  });
238
263
  program.action(async () => {
@@ -251,7 +276,7 @@ program.action(async () => {
251
276
  const [{ render }, React, { Root }, { Fullscreen }] = await Promise.all([
252
277
  import("ink"),
253
278
  import("react"),
254
- import("./Root-ZPDATMNQ.js"),
279
+ import("./Root-7T6ZYTLC.js"),
255
280
  import("./Fullscreen-GK2ZYXHV.js")
256
281
  ]);
257
282
  render(React.createElement(Fullscreen, null, React.createElement(Root)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokz/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Audit where your coding agent's context window and API dollars go.",
5
5
  "type": "module",
6
6
  "license": "MIT",