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