@tokz/cli 0.2.7 → 0.2.9
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 +20 -7
- package/dist/{Root-ZPDATMNQ.js → Root-ZJO6F6LM.js} +404 -215
- package/dist/cli.js +34 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# tokz
|
|
2
2
|
|
|
3
|
+
[](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
|
-
|
|
|
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
|
-
²
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
31
|
-
import { homedir as
|
|
32
|
-
import { join as
|
|
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/
|
|
35
|
-
import { access
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
512
|
+
await access2(join3(antigravityRoot(home), "conversations"));
|
|
343
513
|
return true;
|
|
344
514
|
} catch {
|
|
345
515
|
try {
|
|
346
|
-
await
|
|
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
|
|
358
|
-
import { homedir as
|
|
359
|
-
import { join as
|
|
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 =
|
|
534
|
+
async detect(home = homedir4()) {
|
|
365
535
|
try {
|
|
366
|
-
await
|
|
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
|
|
377
|
-
import { homedir as
|
|
378
|
-
import { join as
|
|
379
|
-
import { glob as
|
|
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 ??
|
|
475
|
-
return CHANNELS.map((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
|
|
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
|
|
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
|
|
622
|
+
import { access as access5 } from "fs/promises";
|
|
543
623
|
import { createInterface } from "readline";
|
|
544
|
-
import { homedir as
|
|
545
|
-
import { join as
|
|
546
|
-
import { glob as
|
|
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
|
|
570
|
-
return process.env.CODEX_HOME ??
|
|
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
|
|
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
|
|
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
|
|
679
|
-
import { homedir as
|
|
680
|
-
import { basename as basename2, join as
|
|
681
|
-
import { glob as
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
736
|
-
import { homedir as
|
|
737
|
-
import { join as
|
|
738
|
-
import { glob as
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
802
|
-
import { homedir as
|
|
803
|
-
import { join as
|
|
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 [
|
|
987
|
-
const h = home ??
|
|
988
|
-
return GOOSE_DB_CANDIDATES.map((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
|
|
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
|
|
1037
|
-
import { homedir as
|
|
1038
|
-
import { join as
|
|
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() :
|
|
1042
|
-
return
|
|
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
|
|
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
|
|
1078
|
-
import { homedir as
|
|
1079
|
-
import { join as
|
|
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 ??
|
|
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 =
|
|
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
|
|
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
|
|
1137
|
-
import { homedir as
|
|
1138
|
-
import { join as
|
|
1139
|
-
import { glob as
|
|
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 ??
|
|
1146
|
-
return KIMI_DIRS.map((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
|
|
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
|
|
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
|
|
1200
|
-
import { homedir as
|
|
1201
|
-
import { join as
|
|
1202
|
-
import { glob as
|
|
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 ??
|
|
1208
|
-
return OPENCLAW_DIRS.map((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
|
|
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
|
|
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
|
|
1270
|
-
import { homedir as
|
|
1271
|
-
import { join as
|
|
1272
|
-
import { glob as
|
|
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
|
|
1296
|
-
return process.env.OPENCODE_DATA_DIR ??
|
|
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 =
|
|
1308
|
-
const messageFiles = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1383
|
-
import { homedir as
|
|
1384
|
-
import { join as
|
|
1385
|
-
import { glob as
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
1435
|
-
import { homedir as
|
|
1436
|
-
import { join as
|
|
1437
|
-
import { glob as
|
|
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 ??
|
|
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
|
|
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
|
|
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 =
|
|
1676
|
+
async detect(home = homedir18()) {
|
|
1497
1677
|
for (const parts of pathCandidates) {
|
|
1498
1678
|
try {
|
|
1499
|
-
await
|
|
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
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
|
|
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 offline — surfaced 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(
|
|
@@ -1547,7 +1726,6 @@ import { Box as Box9, Text as Text10, useApp, useInput as useInput4 } from "ink"
|
|
|
1547
1726
|
|
|
1548
1727
|
// src/timeframe.ts
|
|
1549
1728
|
var TIMEFRAMES = [
|
|
1550
|
-
{ id: "all", label: "All time" },
|
|
1551
1729
|
{ id: "today", label: "Today" },
|
|
1552
1730
|
{ id: "yesterday", label: "Yesterday" },
|
|
1553
1731
|
{ id: "7d", label: "Last 7 days" },
|
|
@@ -1563,8 +1741,6 @@ function nextTimeframe(id) {
|
|
|
1563
1741
|
}
|
|
1564
1742
|
function timeframeRange(id, now = Date.now()) {
|
|
1565
1743
|
switch (id) {
|
|
1566
|
-
case "all":
|
|
1567
|
-
return void 0;
|
|
1568
1744
|
case "today":
|
|
1569
1745
|
return { from: isoDay(0, now), to: isoDay(0, now) };
|
|
1570
1746
|
case "yesterday":
|
|
@@ -2302,7 +2478,7 @@ function Dashboard({
|
|
|
2302
2478
|
] }) : null
|
|
2303
2479
|
] }),
|
|
2304
2480
|
/* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
2305
|
-
timeframe
|
|
2481
|
+
timeframe ? /* @__PURE__ */ jsxs7(Text8, { color: "yellow", children: [
|
|
2306
2482
|
timeframe,
|
|
2307
2483
|
" \xB7 "
|
|
2308
2484
|
] }) : null,
|
|
@@ -2333,7 +2509,7 @@ var KEYS = [
|
|
|
2333
2509
|
["\u2190 \u2192 or 1\u20136", "switch dashboard tab"],
|
|
2334
2510
|
["/", "filter project list (esc clears)"],
|
|
2335
2511
|
["s", "cycle project sort: cost \xB7 recent \xB7 name"],
|
|
2336
|
-
["t", "cycle timeframe:
|
|
2512
|
+
["t", "cycle timeframe: today \xB7 yesterday \xB7 7d \xB7 30d"],
|
|
2337
2513
|
["g", "Activity tab: group by day \xB7 week \xB7 month"],
|
|
2338
2514
|
["esc", "go back"],
|
|
2339
2515
|
["?", "toggle this help"],
|
|
@@ -2383,10 +2559,23 @@ function App({
|
|
|
2383
2559
|
initialSelected = 0
|
|
2384
2560
|
}) {
|
|
2385
2561
|
const { exit } = useApp();
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2562
|
+
const rawAgents = useMemo2(() => agents ?? wrapProjects(projects ?? []), [agents, projects]);
|
|
2563
|
+
const agentList = useMemo2(() => {
|
|
2564
|
+
const withData = rawAgents.filter((a) => a.projects.length > 0);
|
|
2565
|
+
if (withData.length <= 1) return rawAgents;
|
|
2566
|
+
const combined = {
|
|
2567
|
+
adapter: {
|
|
2568
|
+
id: "__all_agents__",
|
|
2569
|
+
name: "All agents",
|
|
2570
|
+
supported: true,
|
|
2571
|
+
detect: async () => true,
|
|
2572
|
+
loadProjects: async () => []
|
|
2573
|
+
},
|
|
2574
|
+
detected: true,
|
|
2575
|
+
projects: withData.flatMap((a) => a.projects)
|
|
2576
|
+
};
|
|
2577
|
+
return [combined, ...rawAgents];
|
|
2578
|
+
}, [rawAgents]);
|
|
2390
2579
|
const multiAgent = agentList.length > 1;
|
|
2391
2580
|
const firstWithData = Math.max(0, agentList.findIndex((a) => a.projects.length > 0));
|
|
2392
2581
|
const [agentIdx, setAgentIdx] = useState5(firstWithData);
|
|
@@ -2398,7 +2587,7 @@ function App({
|
|
|
2398
2587
|
);
|
|
2399
2588
|
const [help, setHelp] = useState5(false);
|
|
2400
2589
|
const [filterCapture, setFilterCapture] = useState5(false);
|
|
2401
|
-
const [timeframe, setTimeframe] = useState5("
|
|
2590
|
+
const [timeframe, setTimeframe] = useState5("30d");
|
|
2402
2591
|
const scoped = useMemo2(
|
|
2403
2592
|
() => applyTimeframe(activeProjects, timeframeRange(timeframe)),
|
|
2404
2593
|
[activeProjects, timeframe]
|
|
@@ -2498,7 +2687,7 @@ function App({
|
|
|
2498
2687
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", flexGrow: 1, children: [
|
|
2499
2688
|
/* @__PURE__ */ jsx10(Box9, { flexDirection: "column", flexGrow: 1, children: content }),
|
|
2500
2689
|
/* @__PURE__ */ jsx10(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: help ? "any key to close help" : view === "agents" ? HINTS.agents : /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
2501
|
-
/* @__PURE__ */ jsxs9(Text10, { color:
|
|
2690
|
+
/* @__PURE__ */ jsxs9(Text10, { color: "yellow", children: [
|
|
2502
2691
|
"\u23F1 ",
|
|
2503
2692
|
tfLabel
|
|
2504
2693
|
] }),
|
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/
|
|
27
|
-
import {
|
|
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 ??
|
|
58
|
-
const file =
|
|
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) :
|
|
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
|
|
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-
|
|
279
|
+
import("./Root-ZJO6F6LM.js"),
|
|
255
280
|
import("./Fullscreen-GK2ZYXHV.js")
|
|
256
281
|
]);
|
|
257
282
|
render(React.createElement(Fullscreen, null, React.createElement(Root)));
|