@tokz/cli 0.2.6 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Abdallah-Alwarawreh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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
 
@@ -13,28 +13,29 @@ import {
13
13
  duration,
14
14
  emptyUsage,
15
15
  groupDaily,
16
- parseTranscript,
16
+ parseTranscriptContent,
17
17
  pct,
18
18
  pct1,
19
19
  relativeDate,
20
20
  sanitizeProjectPath,
21
21
  shortModel,
22
22
  usd
23
- } from "./chunk-Z5W5QH2Z.js";
23
+ } from "./chunk-KO5GQL76.js";
24
24
 
25
25
  // src/ui/Root.tsx
26
26
  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";
@@ -172,12 +173,18 @@ async function loadProjects(home = homedir(), onProgress) {
172
173
  const seenMessageIds = /* @__PURE__ */ new Map();
173
174
  const seenToolIds = /* @__PURE__ */ new Set();
174
175
  let parsed = 0;
176
+ const contents = /* @__PURE__ */ new Map();
177
+ await Promise.all(
178
+ files.map(
179
+ (f) => readFile(f, "utf8").then((c) => void contents.set(f, c)).catch(() => void contents.set(f, ""))
180
+ )
181
+ );
175
182
  const out = [];
176
183
  for (const [dir, dirFiles] of byDir) {
177
184
  onProgress?.({ parsed, total: files.length, currentProject: realMap.get(dir) ?? dir });
178
185
  const sessions = [];
179
186
  for (const f of dirFiles) {
180
- sessions.push(await parseTranscript(f, seenMessageIds, seenToolIds));
187
+ sessions.push(parseTranscriptContent(contents.get(f) ?? "", f, seenMessageIds, seenToolIds));
181
188
  parsed += 1;
182
189
  onProgress?.({ parsed, total: files.length, currentProject: realMap.get(dir) ?? dir });
183
190
  }
@@ -197,11 +204,180 @@ async function loadProjects(home = homedir(), onProgress) {
197
204
  return out;
198
205
  }
199
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
+
200
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";
201
377
  var ESTIMATED_CHARS_PER_TOKEN = 4;
202
378
  var INPUT_SHARE = 0.85;
203
379
  function antigravityRoot(home) {
204
- return join2(home ?? homedir2(), ".gemini", "antigravity-cli");
380
+ return join3(home ?? homedir3(), ".gemini", "antigravity-cli");
205
381
  }
206
382
  function modelIdFromDisplayName(name) {
207
383
  const bare = name.replace(/\s*\(.*\)\s*$/, "").trim();
@@ -235,7 +411,7 @@ function scanConversation(buf) {
235
411
  }
236
412
  async function readHistory(root) {
237
413
  try {
238
- const raw = await readFile2(join2(root, "history.jsonl"), "utf8");
414
+ const raw = await readFile3(join3(root, "history.jsonl"), "utf8");
239
415
  const out = [];
240
416
  for (const line of raw.split("\n")) {
241
417
  if (!line.trim()) continue;
@@ -251,7 +427,7 @@ async function readHistory(root) {
251
427
  }
252
428
  async function loadAntigravityProjects(home, onProgress) {
253
429
  const root = antigravityRoot(home);
254
- const convDir = join2(root, "conversations");
430
+ const convDir = join3(root, "conversations");
255
431
  let dbFiles;
256
432
  try {
257
433
  dbFiles = (await readdir(convDir)).filter((f) => f.endsWith(".db"));
@@ -275,7 +451,7 @@ async function loadAntigravityProjects(home, onProgress) {
275
451
  onProgress?.({ parsed, total: dbFiles.length, currentProject: "Antigravity conversations" });
276
452
  let scan;
277
453
  try {
278
- scan = scanConversation(await readFile2(join2(convDir, file)));
454
+ scan = scanConversation(await readFile3(join3(convDir, file)));
279
455
  } catch {
280
456
  parsed += 1;
281
457
  continue;
@@ -333,11 +509,11 @@ var antigravityAdapter = {
333
509
  estimateNote: "no token counts on disk \u2014 tokens/costs estimated from conversation size",
334
510
  async detect(home) {
335
511
  try {
336
- await access(join2(antigravityRoot(home), "conversations"));
512
+ await access2(join3(antigravityRoot(home), "conversations"));
337
513
  return true;
338
514
  } catch {
339
515
  try {
340
- await access(join2(home ?? homedir2(), ".gemini", "antigravity", "conversations"));
516
+ await access2(join3(home ?? homedir3(), ".gemini", "antigravity", "conversations"));
341
517
  return true;
342
518
  } catch {
343
519
  return false;
@@ -348,16 +524,16 @@ var antigravityAdapter = {
348
524
  };
349
525
 
350
526
  // 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";
527
+ import { access as access3 } from "fs/promises";
528
+ import { homedir as homedir4 } from "os";
529
+ import { join as join4 } from "path";
354
530
  var claudeAdapter = {
355
531
  id: "claude",
356
532
  name: "Claude Code",
357
533
  supported: true,
358
- async detect(home = homedir3()) {
534
+ async detect(home = homedir4()) {
359
535
  try {
360
- await access2(join3(home, ".claude", "projects"));
536
+ await access3(join4(home, ".claude", "projects"));
361
537
  return true;
362
538
  } catch {
363
539
  return false;
@@ -367,106 +543,16 @@ var claudeAdapter = {
367
543
  };
368
544
 
369
545
  // 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
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";
464
550
  var CHANNELS = ["manicode", "manicode-dev", "manicode-staging"];
465
551
  function codebuffRoots(home) {
466
552
  const env = process.env.CODEBUFF_DATA_DIR;
467
553
  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));
554
+ const h = home ?? homedir5();
555
+ return CHANNELS.map((c) => join5(h, ".config", c));
470
556
  }
471
557
  function isAssistant(msg) {
472
558
  const role = str(msg, "variant") ?? str(msg, "role");
@@ -501,7 +587,7 @@ async function loadCodebuffProjects(home, onProgress) {
501
587
  const files = [];
502
588
  for (const root of codebuffRoots(home)) {
503
589
  files.push(
504
- ...await glob2(["**/chat-messages.json"], { cwd: root, absolute: true }).catch(() => [])
590
+ ...await glob3(["**/chat-messages.json"], { cwd: root, absolute: true }).catch(() => [])
505
591
  );
506
592
  }
507
593
  const sessions = [];
@@ -521,7 +607,7 @@ var codebuffAdapter = {
521
607
  async detect(home) {
522
608
  for (const root of codebuffRoots(home)) {
523
609
  try {
524
- await access3(root);
610
+ await access4(root);
525
611
  return true;
526
612
  } catch {
527
613
  }
@@ -533,11 +619,11 @@ var codebuffAdapter = {
533
619
 
534
620
  // src/agents/codex.ts
535
621
  import { createReadStream } from "fs";
536
- import { access as access4 } from "fs/promises";
622
+ import { access as access5 } from "fs/promises";
537
623
  import { createInterface } from "readline";
538
- import { homedir as homedir5 } from "os";
539
- import { join as join5 } from "path";
540
- 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";
541
627
  import { z } from "zod";
542
628
  var TokenTotals = z.object({
543
629
  input_tokens: z.number().catch(0).default(0),
@@ -560,8 +646,8 @@ var Line = z.object({
560
646
  }).passthrough().optional()
561
647
  });
562
648
  function codexHome(home) {
563
- if (home) return join5(home, ".codex");
564
- return process.env.CODEX_HOME ?? join5(homedir5(), ".codex");
649
+ if (home) return join6(home, ".codex");
650
+ return process.env.CODEX_HOME ?? join6(homedir6(), ".codex");
565
651
  }
566
652
  var DEFAULT_MODEL = "gpt-5";
567
653
  async function parseCodexRollout(file) {
@@ -638,7 +724,7 @@ async function parseCodexRollout(file) {
638
724
  }
639
725
  async function loadCodexProjects(home, onProgress) {
640
726
  const root = codexHome(home);
641
- const files = await glob3(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
727
+ const files = await glob4(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
642
728
  cwd: root,
643
729
  absolute: true
644
730
  }).catch(() => []);
@@ -659,7 +745,7 @@ var codexAdapter = {
659
745
  supported: true,
660
746
  async detect(home) {
661
747
  try {
662
- await access4(join5(codexHome(home), "sessions"));
748
+ await access5(join6(codexHome(home), "sessions"));
663
749
  return true;
664
750
  } catch {
665
751
  return false;
@@ -668,13 +754,113 @@ var codexAdapter = {
668
754
  loadProjects: loadCodexProjects
669
755
  };
670
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
+
671
857
  // 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";
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";
676
862
  function droidRoot(home) {
677
- return process.env.DROID_SESSIONS_DIR ?? join6(home ?? homedir6(), ".factory", "sessions");
863
+ return process.env.DROID_SESSIONS_DIR ?? join8(home ?? homedir8(), ".factory", "sessions");
678
864
  }
679
865
  async function parseFile2(file) {
680
866
  const settings = await readJson(file);
@@ -699,7 +885,7 @@ async function parseFile2(file) {
699
885
  ]);
700
886
  }
701
887
  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"));
888
+ const files = (await glob6(["**/*.settings.json"], { cwd: droidRoot(home), absolute: true }).catch(() => [])).filter((f) => basename2(f).endsWith(".settings.json"));
703
889
  const sessions = [];
704
890
  let parsed = 0;
705
891
  for (const f of files) {
@@ -716,7 +902,7 @@ var droidAdapter = {
716
902
  supported: true,
717
903
  async detect(home) {
718
904
  try {
719
- await access5(droidRoot(home));
905
+ await access7(droidRoot(home));
720
906
  return true;
721
907
  } catch {
722
908
  return false;
@@ -726,12 +912,12 @@ var droidAdapter = {
726
912
  };
727
913
 
728
914
  // 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";
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";
733
919
  function geminiRoot(home) {
734
- return process.env.GEMINI_DATA_DIR ?? join7(home ?? homedir7(), ".gemini", "tmp");
920
+ return process.env.GEMINI_DATA_DIR ?? join9(home ?? homedir9(), ".gemini", "tmp");
735
921
  }
736
922
  var IN = ["input", "prompt", "input_tokens", "prompt_tokens"];
737
923
  var OUT = ["output", "candidates", "output_tokens", "candidates_tokens"];
@@ -763,7 +949,7 @@ async function parseFile3(file) {
763
949
  return sessionFromRecords(file, void 0, records);
764
950
  }
765
951
  async function loadGeminiProjects(home, onProgress) {
766
- 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(
767
953
  () => []
768
954
  );
769
955
  const sessions = [];
@@ -782,7 +968,7 @@ var geminiAdapter = {
782
968
  supported: true,
783
969
  async detect(home) {
784
970
  try {
785
- await access6(geminiRoot(home));
971
+ await access8(geminiRoot(home));
786
972
  return true;
787
973
  } catch {
788
974
  return false;
@@ -792,9 +978,9 @@ var geminiAdapter = {
792
978
  };
793
979
 
794
980
  // 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";
981
+ import { access as access9 } from "fs/promises";
982
+ import { homedir as homedir10 } from "os";
983
+ import { join as join10 } from "path";
798
984
 
799
985
  // src/sqlite.ts
800
986
  import { readFile as readFile4 } from "fs/promises";
@@ -977,9 +1163,9 @@ var GOOSE_DB_CANDIDATES = [
977
1163
  ];
978
1164
  function gooseDbCandidates(home) {
979
1165
  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));
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));
983
1169
  }
984
1170
  async function loadGooseProjects(home, onProgress) {
985
1171
  const records = [];
@@ -1016,7 +1202,7 @@ var gooseAdapter = {
1016
1202
  async detect(home) {
1017
1203
  for (const db of gooseDbCandidates(home)) {
1018
1204
  try {
1019
- await access7(db);
1205
+ await access9(db);
1020
1206
  return true;
1021
1207
  } catch {
1022
1208
  }
@@ -1027,13 +1213,13 @@ var gooseAdapter = {
1027
1213
  };
1028
1214
 
1029
1215
  // 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";
1216
+ import { access as access10 } from "fs/promises";
1217
+ import { homedir as homedir11 } from "os";
1218
+ import { join as join11 } from "path";
1033
1219
  function hermesDb(home) {
1034
1220
  const env = process.env.HERMES_HOME;
1035
- const root = env ? env.split(",")[0].trim() : join9(home ?? homedir9(), ".hermes");
1036
- return join9(root, "state.db");
1221
+ const root = env ? env.split(",")[0].trim() : join11(home ?? homedir11(), ".hermes");
1222
+ return join11(root, "state.db");
1037
1223
  }
1038
1224
  async function loadHermesProjects(home, onProgress) {
1039
1225
  const rows = await readTable(hermesDb(home), "sessions").catch(() => []);
@@ -1058,7 +1244,7 @@ var hermesAdapter = {
1058
1244
  supported: true,
1059
1245
  async detect(home) {
1060
1246
  try {
1061
- await access8(hermesDb(home));
1247
+ await access10(hermesDb(home));
1062
1248
  return true;
1063
1249
  } catch {
1064
1250
  return false;
@@ -1068,14 +1254,14 @@ var hermesAdapter = {
1068
1254
  };
1069
1255
 
1070
1256
  // 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";
1257
+ import { access as access11 } from "fs/promises";
1258
+ import { homedir as homedir12 } from "os";
1259
+ import { join as join12 } from "path";
1074
1260
  function kiloDir(home) {
1075
- 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");
1076
1262
  }
1077
1263
  async function loadKiloProjects(home, onProgress) {
1078
- const db = join10(kiloDir(home), "kilo.db");
1264
+ const db = join12(kiloDir(home), "kilo.db");
1079
1265
  let rows;
1080
1266
  try {
1081
1267
  rows = await readTable(db, "message");
@@ -1117,7 +1303,7 @@ var kiloAdapter = {
1117
1303
  supported: true,
1118
1304
  async detect(home) {
1119
1305
  try {
1120
- await access9(join10(kiloDir(home), "kilo.db"));
1306
+ await access11(join12(kiloDir(home), "kilo.db"));
1121
1307
  return true;
1122
1308
  } catch {
1123
1309
  return false;
@@ -1127,17 +1313,17 @@ var kiloAdapter = {
1127
1313
  };
1128
1314
 
1129
1315
  // 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";
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";
1134
1320
  var KIMI_DIRS = [".kimi", ".kimi-code"];
1135
1321
  var USAGE_KEYS = ["inputOther", "input_other", "inputCacheRead", "input_cache_read"];
1136
1322
  function kimiRoots(home) {
1137
1323
  const env = process.env.KIMI_DATA_DIR;
1138
1324
  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));
1325
+ const h = home ?? homedir13();
1326
+ return KIMI_DIRS.map((d) => join13(h, d));
1141
1327
  }
1142
1328
  async function parseFile4(file) {
1143
1329
  const records = [];
@@ -1159,7 +1345,7 @@ async function loadKimiProjects(home, onProgress) {
1159
1345
  const files = [];
1160
1346
  for (const root of kimiRoots(home)) {
1161
1347
  files.push(
1162
- ...await glob6(["sessions/**/wire.jsonl"], { cwd: root, absolute: true }).catch(() => [])
1348
+ ...await glob8(["sessions/**/wire.jsonl"], { cwd: root, absolute: true }).catch(() => [])
1163
1349
  );
1164
1350
  }
1165
1351
  const sessions = [];
@@ -1179,7 +1365,7 @@ var kimiAdapter = {
1179
1365
  async detect(home) {
1180
1366
  for (const root of kimiRoots(home)) {
1181
1367
  try {
1182
- await access10(join11(root, "sessions"));
1368
+ await access12(join13(root, "sessions"));
1183
1369
  return true;
1184
1370
  } catch {
1185
1371
  }
@@ -1190,16 +1376,16 @@ var kimiAdapter = {
1190
1376
  };
1191
1377
 
1192
1378
  // 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";
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";
1197
1383
  var OPENCLAW_DIRS = [".openclaw", ".clawdbot", ".moltbot", ".moldbot"];
1198
1384
  function openclawRoots(home) {
1199
1385
  const env = process.env.OPENCLAW_DIR;
1200
1386
  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));
1387
+ const h = home ?? homedir14();
1388
+ return OPENCLAW_DIRS.map((d) => join14(h, d));
1203
1389
  }
1204
1390
  function modelFrom(obj) {
1205
1391
  return str(obj, "modelId") ?? str(obj, "model");
@@ -1230,7 +1416,7 @@ async function parseFile5(file) {
1230
1416
  async function loadOpenclawProjects(home, onProgress) {
1231
1417
  const files = [];
1232
1418
  for (const root of openclawRoots(home)) {
1233
- files.push(...await glob7(["**/*.jsonl", "**/*.jsonl.*"], { cwd: root, absolute: true }).catch(() => []));
1419
+ files.push(...await glob9(["**/*.jsonl", "**/*.jsonl.*"], { cwd: root, absolute: true }).catch(() => []));
1234
1420
  }
1235
1421
  const sessions = [];
1236
1422
  let parsed = 0;
@@ -1249,7 +1435,7 @@ var openclawAdapter = {
1249
1435
  async detect(home) {
1250
1436
  for (const root of openclawRoots(home)) {
1251
1437
  try {
1252
- await access11(root);
1438
+ await access13(root);
1253
1439
  return true;
1254
1440
  } catch {
1255
1441
  }
@@ -1260,10 +1446,10 @@ var openclawAdapter = {
1260
1446
  };
1261
1447
 
1262
1448
  // src/agents/opencode.ts
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";
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";
1267
1453
  import { z as z2 } from "zod";
1268
1454
  var Message = z2.object({
1269
1455
  sessionID: z2.string(),
@@ -1286,8 +1472,8 @@ var Session = z2.object({
1286
1472
  });
1287
1473
  var Project = z2.object({ id: z2.string(), worktree: z2.string().optional() });
1288
1474
  function opencodeRoot(home) {
1289
- if (home) return join13(home, ".local", "share", "opencode");
1290
- 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");
1291
1477
  }
1292
1478
  async function readJson2(file) {
1293
1479
  try {
@@ -1298,18 +1484,18 @@ async function readJson2(file) {
1298
1484
  }
1299
1485
  async function loadOpencodeProjects(home, onProgress) {
1300
1486
  const root = opencodeRoot(home);
1301
- const storage = join13(root, "storage");
1302
- 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(
1303
1489
  () => []
1304
1490
  );
1305
1491
  if (messageFiles.length === 0) return [];
1306
1492
  const sessionDir = /* @__PURE__ */ new Map();
1307
1493
  const projectWorktree = /* @__PURE__ */ new Map();
1308
- 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(() => [])) {
1309
1495
  const p = Project.safeParse(await readJson2(f));
1310
1496
  if (p.success && p.data.worktree) projectWorktree.set(p.data.id, p.data.worktree);
1311
1497
  }
1312
- const sessionFiles = await glob8(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
1498
+ const sessionFiles = await glob10(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
1313
1499
  () => []
1314
1500
  );
1315
1501
  for (const f of sessionFiles) {
@@ -1363,7 +1549,7 @@ var opencodeAdapter = {
1363
1549
  supported: true,
1364
1550
  async detect(home) {
1365
1551
  try {
1366
- await access12(join13(opencodeRoot(home), "storage", "message"));
1552
+ await access14(join15(opencodeRoot(home), "storage", "message"));
1367
1553
  return true;
1368
1554
  } catch {
1369
1555
  return false;
@@ -1373,12 +1559,12 @@ var opencodeAdapter = {
1373
1559
  };
1374
1560
 
1375
1561
  // 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";
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";
1380
1566
  function piRoot(home) {
1381
- 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");
1382
1568
  }
1383
1569
  async function parseFile6(file) {
1384
1570
  const records = [];
@@ -1398,7 +1584,7 @@ async function parseFile6(file) {
1398
1584
  return sessionFromRecords(file, void 0, records);
1399
1585
  }
1400
1586
  async function loadPiProjects(home, onProgress) {
1401
- const files = await glob9(["**/*.jsonl"], { cwd: piRoot(home), absolute: true }).catch(() => []);
1587
+ const files = await glob11(["**/*.jsonl"], { cwd: piRoot(home), absolute: true }).catch(() => []);
1402
1588
  const sessions = [];
1403
1589
  let parsed = 0;
1404
1590
  for (const f of files) {
@@ -1415,7 +1601,7 @@ var piAdapter = {
1415
1601
  supported: true,
1416
1602
  async detect(home) {
1417
1603
  try {
1418
- await access13(piRoot(home));
1604
+ await access15(piRoot(home));
1419
1605
  return true;
1420
1606
  } catch {
1421
1607
  return false;
@@ -1425,12 +1611,12 @@ var piAdapter = {
1425
1611
  };
1426
1612
 
1427
1613
  // 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";
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";
1432
1618
  function qwenRoot(home) {
1433
- return process.env.QWEN_DATA_DIR ?? join15(home ?? homedir15(), ".qwen");
1619
+ return process.env.QWEN_DATA_DIR ?? join17(home ?? homedir17(), ".qwen");
1434
1620
  }
1435
1621
  async function parseFile7(file) {
1436
1622
  const records = [];
@@ -1452,7 +1638,7 @@ async function parseFile7(file) {
1452
1638
  return sessionFromRecords(file, void 0, records);
1453
1639
  }
1454
1640
  async function loadQwenProjects(home, onProgress) {
1455
- 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(
1456
1642
  () => []
1457
1643
  );
1458
1644
  const sessions = [];
@@ -1471,7 +1657,7 @@ var qwenAdapter = {
1471
1657
  supported: true,
1472
1658
  async detect(home) {
1473
1659
  try {
1474
- await access14(join15(qwenRoot(home), "projects"));
1660
+ await access16(join17(qwenRoot(home), "projects"));
1475
1661
  return true;
1476
1662
  } catch {
1477
1663
  return false;
@@ -1487,10 +1673,10 @@ function detectOnly(id, name, reason, ...pathCandidates) {
1487
1673
  name,
1488
1674
  supported: false,
1489
1675
  unsupportedReason: reason,
1490
- async detect(home = homedir16()) {
1676
+ async detect(home = homedir18()) {
1491
1677
  for (const parts of pathCandidates) {
1492
1678
  try {
1493
- await access15(join16(home, ...parts));
1679
+ await access17(join18(home, ...parts));
1494
1680
  return true;
1495
1681
  } catch {
1496
1682
  }
@@ -1513,26 +1699,25 @@ var ADAPTERS = [
1513
1699
  kiloAdapter,
1514
1700
  gooseAdapter,
1515
1701
  hermesAdapter,
1702
+ copilotAdapter,
1703
+ ampAdapter,
1516
1704
  piAdapter,
1517
1705
  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"])
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"])
1524
1709
  ];
1525
1710
  async function loadAllAgents(home, onProgress) {
1526
- const out = [];
1527
- for (const adapter of ADAPTERS) {
1528
- const detected = await adapter.detect(home);
1529
- let projects = [];
1530
- if (detected && adapter.supported) {
1531
- projects = await adapter.loadProjects(home, (p) => onProgress?.(adapter.name, p)).catch(() => []);
1532
- }
1533
- out.push({ adapter, detected, projects });
1534
- }
1535
- return out;
1711
+ return Promise.all(
1712
+ ADAPTERS.map(async (adapter) => {
1713
+ const detected = await adapter.detect(home).catch(() => false);
1714
+ let projects = [];
1715
+ if (detected && adapter.supported) {
1716
+ projects = await adapter.loadProjects(home, (p) => onProgress?.(adapter.name, p)).catch(() => []);
1717
+ }
1718
+ return { adapter, detected, projects };
1719
+ })
1720
+ );
1536
1721
  }
1537
1722
 
1538
1723
  // src/ui/App.tsx
@@ -2377,10 +2562,23 @@ function App({
2377
2562
  initialSelected = 0
2378
2563
  }) {
2379
2564
  const { exit } = useApp();
2380
- const agentList = useMemo2(
2381
- () => agents ?? wrapProjects(projects ?? []),
2382
- [agents, projects]
2383
- );
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]);
2384
2582
  const multiAgent = agentList.length > 1;
2385
2583
  const firstWithData = Math.max(0, agentList.findIndex((a) => a.projects.length > 0));
2386
2584
  const [agentIdx, setAgentIdx] = useState5(firstWithData);
@@ -335,8 +335,7 @@ async function findTranscripts(projectPath, home = homedir()) {
335
335
  }
336
336
 
337
337
  // src/transcript.ts
338
- import { createReadStream } from "fs";
339
- import { createInterface } from "readline";
338
+ import { readFile } from "fs/promises";
340
339
  import { z } from "zod";
341
340
  var AssistantLine = z.object({
342
341
  type: z.literal("assistant"),
@@ -359,10 +358,18 @@ var AssistantLine = z.object({
359
358
  })
360
359
  });
361
360
  async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), seenToolIds = /* @__PURE__ */ new Set(), events) {
361
+ let content;
362
+ try {
363
+ content = await readFile(file, "utf8");
364
+ } catch {
365
+ content = "";
366
+ }
367
+ return parseTranscriptContent(content, file, seenMessages, seenToolIds, events);
368
+ }
369
+ function parseTranscriptContent(content, file, seenMessages = /* @__PURE__ */ new Map(), seenToolIds = /* @__PURE__ */ new Set(), events) {
362
370
  const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
363
- const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
364
- for await (const line of rl) {
365
- if (!line.trim()) continue;
371
+ for (const line of content.split("\n")) {
372
+ if (line.length < 2 || !line.includes("assistant")) continue;
366
373
  let raw;
367
374
  try {
368
375
  raw = JSON.parse(line);
@@ -467,5 +474,6 @@ export {
467
474
  parseDateArg,
468
475
  sanitizeProjectPath,
469
476
  findTranscripts,
470
- parseTranscript
477
+ parseTranscript,
478
+ parseTranscriptContent
471
479
  };
@@ -6,7 +6,7 @@ import {
6
6
  emptyUsage,
7
7
  shortModel,
8
8
  usd
9
- } from "./chunk-Z5W5QH2Z.js";
9
+ } from "./chunk-KO5GQL76.js";
10
10
 
11
11
  // src/blocks.ts
12
12
  var BLOCK_LENGTH_MS = 5 * 60 * 60 * 1e3;
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  buildBlocks,
4
4
  renderBlocksReport
5
- } from "./chunk-6Y6MKFHZ.js";
5
+ } from "./chunk-VJFLJJGX.js";
6
6
  import {
7
7
  findMcpServers
8
8
  } from "./chunk-65BQE6TI.js";
@@ -17,16 +17,39 @@ import {
17
17
  setTimezone,
18
18
  tok,
19
19
  usd
20
- } from "./chunk-Z5W5QH2Z.js";
20
+ } from "./chunk-KO5GQL76.js";
21
21
 
22
22
  // src/cli.ts
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);
@@ -210,7 +234,7 @@ program.command("blocks").description("Claude usage grouped into rolling 5-hour
210
234
  });
211
235
  program.command("statusline").description("compact usage line for Claude Code's statusLine hook (reads hook JSON on stdin)").argument("[action]", '"enable" writes the hook into ~/.claude/settings.json, "disable" removes it').option("--cost-source <mode>", "session cost source: auto | cc | calc | both", "auto").action(async (action, opts) => {
212
236
  const globals = applyGlobals();
213
- const sl = await import("./statusline-EXFMQYFF.js");
237
+ const sl = await import("./statusline-5SJKELOG.js");
214
238
  if (action === "enable" || action === "disable") {
215
239
  try {
216
240
  console.log(action === "enable" ? await sl.enableStatusline() : await sl.disableStatusline());
@@ -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-TUEV5MJB.js"),
279
+ import("./Root-7T6ZYTLC.js"),
255
280
  import("./Fullscreen-GK2ZYXHV.js")
256
281
  ]);
257
282
  render(React.createElement(Fullscreen, null, React.createElement(Root)));
@@ -3,7 +3,7 @@ import {
3
3
  buildBlocks,
4
4
  burnRate,
5
5
  fmtMs
6
- } from "./chunk-6Y6MKFHZ.js";
6
+ } from "./chunk-VJFLJJGX.js";
7
7
  import {
8
8
  compact,
9
9
  costUsd,
@@ -12,7 +12,7 @@ import {
12
12
  parseTranscript,
13
13
  shortModel,
14
14
  usd
15
- } from "./chunk-Z5W5QH2Z.js";
15
+ } from "./chunk-KO5GQL76.js";
16
16
 
17
17
  // src/statusline.ts
18
18
  import { mkdir, readFile, stat, writeFile } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokz/cli",
3
- "version": "0.2.6",
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",