@tokz/cli 0.0.1 → 0.2.0
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 +78 -0
- package/dist/Fullscreen-GK2ZYXHV.js +26 -0
- package/dist/Root-T6AZELSZ.js +1499 -0
- package/dist/chunk-MO4Y7IF6.js +404 -0
- package/dist/cli.js +108 -0
- package/package.json +35 -8
- package/cli.js +0 -1
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addUsage,
|
|
4
|
+
buildReport,
|
|
5
|
+
cacheHitRate,
|
|
6
|
+
cacheSavings,
|
|
7
|
+
compact,
|
|
8
|
+
costUsd,
|
|
9
|
+
duration,
|
|
10
|
+
emptyUsage,
|
|
11
|
+
findMcpServers,
|
|
12
|
+
parseTranscript,
|
|
13
|
+
pct,
|
|
14
|
+
pct1,
|
|
15
|
+
relativeDate,
|
|
16
|
+
sanitizeProjectPath,
|
|
17
|
+
shortModel,
|
|
18
|
+
usd
|
|
19
|
+
} from "./chunk-MO4Y7IF6.js";
|
|
20
|
+
|
|
21
|
+
// src/ui/Root.tsx
|
|
22
|
+
import { useEffect as useEffect2, useState as useState6 } from "react";
|
|
23
|
+
import { Box as Box10, Text as Text11, useApp as useApp2 } from "ink";
|
|
24
|
+
|
|
25
|
+
// src/agents/index.ts
|
|
26
|
+
import { access as access4 } from "fs/promises";
|
|
27
|
+
import { homedir as homedir5 } from "os";
|
|
28
|
+
import { join as join5 } from "path";
|
|
29
|
+
|
|
30
|
+
// src/agents/claude.ts
|
|
31
|
+
import { access } from "fs/promises";
|
|
32
|
+
import { homedir as homedir2 } from "os";
|
|
33
|
+
import { join as join2 } from "path";
|
|
34
|
+
|
|
35
|
+
// src/projects.ts
|
|
36
|
+
import { readFile } from "fs/promises";
|
|
37
|
+
import { homedir } from "os";
|
|
38
|
+
import { join, relative, sep } from "path";
|
|
39
|
+
import { glob } from "tinyglobby";
|
|
40
|
+
function applyTimeframe(projects, range) {
|
|
41
|
+
if (!range) return projects;
|
|
42
|
+
return projects.map(
|
|
43
|
+
(p) => p.sessions ? { ...p, report: buildReport(p.sessions, p.serverList ?? [], range) } : p
|
|
44
|
+
).filter((p) => !p.sessions || p.report.sessionCount > 0).sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
|
|
45
|
+
}
|
|
46
|
+
function baseName(p) {
|
|
47
|
+
const parts = p.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
48
|
+
return parts.at(-1) ?? p;
|
|
49
|
+
}
|
|
50
|
+
var DAY_MS = 864e5;
|
|
51
|
+
function aggregate(projects) {
|
|
52
|
+
const usageByModel = {};
|
|
53
|
+
const toolCalls = {};
|
|
54
|
+
const toolCostUsd = {};
|
|
55
|
+
const serverByName = /* @__PURE__ */ new Map();
|
|
56
|
+
const dailyByDate = /* @__PURE__ */ new Map();
|
|
57
|
+
const sessions = [];
|
|
58
|
+
let sessionCount = 0;
|
|
59
|
+
let totalTurns = 0;
|
|
60
|
+
let start;
|
|
61
|
+
let end;
|
|
62
|
+
for (const { report } of projects) {
|
|
63
|
+
sessionCount += report.sessionCount;
|
|
64
|
+
totalTurns += report.totalTurns ?? 0;
|
|
65
|
+
for (const [m, u] of Object.entries(report.usageByModel)) {
|
|
66
|
+
addUsage(usageByModel[m] ??= emptyUsage(), u);
|
|
67
|
+
}
|
|
68
|
+
for (const [t, n] of Object.entries(report.toolCalls)) toolCalls[t] = (toolCalls[t] ?? 0) + n;
|
|
69
|
+
for (const [t, c] of Object.entries(report.toolCostUsd ?? {})) {
|
|
70
|
+
toolCostUsd[t] = (toolCostUsd[t] ?? 0) + c;
|
|
71
|
+
}
|
|
72
|
+
for (const s of report.servers) {
|
|
73
|
+
const acc = serverByName.get(s.name);
|
|
74
|
+
if (!acc) {
|
|
75
|
+
serverByName.set(s.name, { ...s });
|
|
76
|
+
} else {
|
|
77
|
+
acc.callsObserved += s.callsObserved;
|
|
78
|
+
acc.estCostUsd += s.estCostUsd ?? 0;
|
|
79
|
+
acc.unused = acc.callsObserved === 0;
|
|
80
|
+
acc.configured = acc.configured || s.configured;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const d of report.daily ?? []) {
|
|
84
|
+
const acc = dailyByDate.get(d.date);
|
|
85
|
+
if (!acc) {
|
|
86
|
+
dailyByDate.set(d.date, { ...d });
|
|
87
|
+
} else {
|
|
88
|
+
acc.costUsd += d.costUsd;
|
|
89
|
+
acc.inputTokens += d.inputTokens;
|
|
90
|
+
acc.cacheReadTokens += d.cacheReadTokens;
|
|
91
|
+
acc.cacheCreationTokens += d.cacheCreationTokens;
|
|
92
|
+
acc.outputTokens += d.outputTokens;
|
|
93
|
+
acc.turns += d.turns;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
sessions.push(...report.sessions ?? []);
|
|
97
|
+
if (report.spanStart && (!start || report.spanStart < start)) start = report.spanStart;
|
|
98
|
+
if (report.spanEnd && (!end || report.spanEnd > end)) end = report.spanEnd;
|
|
99
|
+
}
|
|
100
|
+
const costByModel = {};
|
|
101
|
+
let totalCostUsd = 0;
|
|
102
|
+
for (const [m, u] of Object.entries(usageByModel)) {
|
|
103
|
+
costByModel[m] = costUsd(u, m);
|
|
104
|
+
totalCostUsd += costByModel[m].total;
|
|
105
|
+
}
|
|
106
|
+
const spanDays = start && end ? Math.max(1, Math.round((Date.parse(end) - Date.parse(start)) / DAY_MS)) : 1;
|
|
107
|
+
return {
|
|
108
|
+
sessionCount,
|
|
109
|
+
spanDays,
|
|
110
|
+
spanStart: start,
|
|
111
|
+
spanEnd: end,
|
|
112
|
+
usageByModel,
|
|
113
|
+
costByModel,
|
|
114
|
+
totalCostUsd,
|
|
115
|
+
monthlyProjectionUsd: totalCostUsd / spanDays * 30,
|
|
116
|
+
toolCalls,
|
|
117
|
+
toolCostUsd,
|
|
118
|
+
servers: [...serverByName.values()],
|
|
119
|
+
daily: [...dailyByDate.values()].sort((a, b) => a.date < b.date ? -1 : 1),
|
|
120
|
+
sessions: sessions.sort((a, b) => b.costUsd - a.costUsd),
|
|
121
|
+
cacheSavingsUsd: cacheSavings(usageByModel),
|
|
122
|
+
cacheHitRate: cacheHitRate(usageByModel),
|
|
123
|
+
totalTurns
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function realPathsBySanitized(home) {
|
|
127
|
+
const map = /* @__PURE__ */ new Map();
|
|
128
|
+
try {
|
|
129
|
+
const cfg = JSON.parse(await readFile(join(home, ".claude.json"), "utf8"));
|
|
130
|
+
const projects = cfg?.projects;
|
|
131
|
+
if (projects && typeof projects === "object") {
|
|
132
|
+
for (const p of Object.keys(projects)) map.set(sanitizeProjectPath(p), p);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
return map;
|
|
137
|
+
}
|
|
138
|
+
async function loadProjects(home = homedir(), onProgress) {
|
|
139
|
+
const root = join(home, ".claude", "projects");
|
|
140
|
+
const files = await glob(["**/*.jsonl"], { cwd: root, absolute: true }).catch(() => []);
|
|
141
|
+
if (files.length === 0) return [];
|
|
142
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
143
|
+
for (const f of files) {
|
|
144
|
+
const dir = relative(root, f).split(sep)[0];
|
|
145
|
+
const list = byDir.get(dir) ?? [];
|
|
146
|
+
list.push(f);
|
|
147
|
+
byDir.set(dir, list);
|
|
148
|
+
}
|
|
149
|
+
const realMap = await realPathsBySanitized(home);
|
|
150
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
151
|
+
const seenToolIds = /* @__PURE__ */ new Set();
|
|
152
|
+
let parsed = 0;
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const [dir, dirFiles] of byDir) {
|
|
155
|
+
onProgress?.({ parsed, total: files.length, currentProject: realMap.get(dir) ?? dir });
|
|
156
|
+
const sessions = [];
|
|
157
|
+
for (const f of dirFiles) {
|
|
158
|
+
sessions.push(await parseTranscript(f, seenMessageIds, seenToolIds));
|
|
159
|
+
parsed += 1;
|
|
160
|
+
onProgress?.({ parsed, total: files.length, currentProject: realMap.get(dir) ?? dir });
|
|
161
|
+
}
|
|
162
|
+
const realPath = realMap.get(dir) ?? sessions.find((s) => s.cwd)?.cwd;
|
|
163
|
+
const servers = realPath ? await findMcpServers(realPath, home) : [];
|
|
164
|
+
out.push({
|
|
165
|
+
id: dir,
|
|
166
|
+
name: realPath ?? dir,
|
|
167
|
+
label: realPath ? baseName(realPath) : dir,
|
|
168
|
+
realPath,
|
|
169
|
+
report: buildReport(sessions, servers),
|
|
170
|
+
sessions,
|
|
171
|
+
serverList: servers
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/agents/claude.ts
|
|
179
|
+
var claudeAdapter = {
|
|
180
|
+
id: "claude",
|
|
181
|
+
name: "Claude Code",
|
|
182
|
+
supported: true,
|
|
183
|
+
async detect(home = homedir2()) {
|
|
184
|
+
try {
|
|
185
|
+
await access(join2(home, ".claude", "projects"));
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
loadProjects: (home, onProgress) => loadProjects(home, onProgress)
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/agents/codex.ts
|
|
195
|
+
import { createReadStream } from "fs";
|
|
196
|
+
import { access as access2 } from "fs/promises";
|
|
197
|
+
import { createInterface } from "readline";
|
|
198
|
+
import { homedir as homedir3 } from "os";
|
|
199
|
+
import { join as join3 } from "path";
|
|
200
|
+
import { glob as glob2 } from "tinyglobby";
|
|
201
|
+
import { z } from "zod";
|
|
202
|
+
var TokenTotals = z.object({
|
|
203
|
+
input_tokens: z.number().catch(0).default(0),
|
|
204
|
+
cached_input_tokens: z.number().catch(0).default(0),
|
|
205
|
+
output_tokens: z.number().catch(0).default(0),
|
|
206
|
+
reasoning_output_tokens: z.number().catch(0).default(0),
|
|
207
|
+
total_tokens: z.number().catch(0).default(0)
|
|
208
|
+
});
|
|
209
|
+
var Line = z.object({
|
|
210
|
+
timestamp: z.string().optional(),
|
|
211
|
+
type: z.string().optional(),
|
|
212
|
+
// old-format meta lines carry cwd at the top level
|
|
213
|
+
cwd: z.string().optional(),
|
|
214
|
+
payload: z.object({
|
|
215
|
+
type: z.string().optional(),
|
|
216
|
+
cwd: z.string().optional(),
|
|
217
|
+
model: z.string().optional(),
|
|
218
|
+
name: z.string().optional(),
|
|
219
|
+
info: z.object({ total_token_usage: TokenTotals.optional() }).nullish()
|
|
220
|
+
}).passthrough().optional()
|
|
221
|
+
});
|
|
222
|
+
function codexHome(home) {
|
|
223
|
+
if (home) return join3(home, ".codex");
|
|
224
|
+
return process.env.CODEX_HOME ?? join3(homedir3(), ".codex");
|
|
225
|
+
}
|
|
226
|
+
var DEFAULT_MODEL = "gpt-5";
|
|
227
|
+
async function parseCodexRollout(file) {
|
|
228
|
+
const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
|
|
229
|
+
const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
|
|
230
|
+
let model = DEFAULT_MODEL;
|
|
231
|
+
let prev = { input: 0, cached: 0, output: 0, total: 0 };
|
|
232
|
+
let turnTools = [];
|
|
233
|
+
for await (const raw of rl) {
|
|
234
|
+
if (!raw.trim()) continue;
|
|
235
|
+
let obj;
|
|
236
|
+
try {
|
|
237
|
+
obj = JSON.parse(raw);
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const parsed = Line.safeParse(obj);
|
|
242
|
+
if (!parsed.success) continue;
|
|
243
|
+
const { timestamp, type, payload } = parsed.data;
|
|
244
|
+
if (!stats.cwd) stats.cwd = payload?.cwd ?? parsed.data.cwd;
|
|
245
|
+
if (payload?.model) model = payload.model;
|
|
246
|
+
if (timestamp) {
|
|
247
|
+
if (!stats.firstTs) stats.firstTs = timestamp;
|
|
248
|
+
stats.lastTs = timestamp;
|
|
249
|
+
}
|
|
250
|
+
const toolName = payload?.type === "function_call" || payload?.type === "custom_tool_call" ? payload.name : payload?.type === "local_shell_call" ? "shell" : type === "function_call" ? parsed.data.name : void 0;
|
|
251
|
+
if (toolName) {
|
|
252
|
+
stats.toolCalls[toolName] = (stats.toolCalls[toolName] ?? 0) + 1;
|
|
253
|
+
turnTools.push(toolName);
|
|
254
|
+
}
|
|
255
|
+
const totals = payload?.type === "token_count" ? payload.info?.total_token_usage : void 0;
|
|
256
|
+
if (!totals) continue;
|
|
257
|
+
const cur = {
|
|
258
|
+
input: totals.input_tokens,
|
|
259
|
+
cached: totals.cached_input_tokens,
|
|
260
|
+
output: totals.output_tokens,
|
|
261
|
+
total: totals.total_tokens
|
|
262
|
+
};
|
|
263
|
+
if (cur.total < prev.total) prev = { input: 0, cached: 0, output: 0, total: 0 };
|
|
264
|
+
const dInput = Math.max(0, cur.input - prev.input);
|
|
265
|
+
const dCached = Math.max(0, cur.cached - prev.cached);
|
|
266
|
+
const dOutput = Math.max(0, cur.output - prev.output);
|
|
267
|
+
prev = cur;
|
|
268
|
+
if (dInput + dOutput === 0) continue;
|
|
269
|
+
const delta = {
|
|
270
|
+
// Codex's input_tokens INCLUDES cached tokens; split them out.
|
|
271
|
+
inputTokens: Math.max(0, dInput - dCached),
|
|
272
|
+
cacheReadTokens: dCached,
|
|
273
|
+
cacheCreationTokens: 0,
|
|
274
|
+
outputTokens: dOutput,
|
|
275
|
+
turns: 1
|
|
276
|
+
};
|
|
277
|
+
const accs = [stats.usageByModel[model] ??= emptyUsage()];
|
|
278
|
+
if (timestamp) {
|
|
279
|
+
const day = stats.dailyUsage[timestamp.slice(0, 10)] ??= {};
|
|
280
|
+
accs.push(day[model] ??= emptyUsage());
|
|
281
|
+
}
|
|
282
|
+
for (const u of accs) {
|
|
283
|
+
u.inputTokens += delta.inputTokens;
|
|
284
|
+
u.cacheReadTokens += delta.cacheReadTokens;
|
|
285
|
+
u.outputTokens += delta.outputTokens;
|
|
286
|
+
u.turns += 1;
|
|
287
|
+
}
|
|
288
|
+
const turnCost = costUsd(delta, model).total;
|
|
289
|
+
if (turnCost > 0 && turnTools.length > 0) {
|
|
290
|
+
const share = turnCost / turnTools.length;
|
|
291
|
+
for (const name of turnTools) {
|
|
292
|
+
stats.toolCostUsd[name] = (stats.toolCostUsd[name] ?? 0) + share;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
turnTools = [];
|
|
296
|
+
}
|
|
297
|
+
return stats;
|
|
298
|
+
}
|
|
299
|
+
async function loadCodexProjects(home, onProgress) {
|
|
300
|
+
const root = codexHome(home);
|
|
301
|
+
const files = await glob2(["sessions/**/*.jsonl", "archived_sessions/**/*.jsonl"], {
|
|
302
|
+
cwd: root,
|
|
303
|
+
absolute: true
|
|
304
|
+
}).catch(() => []);
|
|
305
|
+
if (files.length === 0) return [];
|
|
306
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
307
|
+
let parsed = 0;
|
|
308
|
+
for (const f of files) {
|
|
309
|
+
onProgress?.({ parsed, total: files.length, currentProject: "Codex sessions" });
|
|
310
|
+
const s = await parseCodexRollout(f);
|
|
311
|
+
parsed += 1;
|
|
312
|
+
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
|
+
}
|
|
319
|
+
const out = [];
|
|
320
|
+
for (const [cwd, sessions] of byCwd) {
|
|
321
|
+
out.push({
|
|
322
|
+
id: `codex:${cwd}`,
|
|
323
|
+
name: cwd,
|
|
324
|
+
label: cwd === "(unknown project)" ? cwd : baseName(cwd),
|
|
325
|
+
realPath: cwd,
|
|
326
|
+
report: buildReport(sessions, []),
|
|
327
|
+
sessions,
|
|
328
|
+
serverList: []
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
var codexAdapter = {
|
|
335
|
+
id: "codex",
|
|
336
|
+
name: "OpenAI Codex",
|
|
337
|
+
supported: true,
|
|
338
|
+
async detect(home) {
|
|
339
|
+
try {
|
|
340
|
+
await access2(join3(codexHome(home), "sessions"));
|
|
341
|
+
return true;
|
|
342
|
+
} catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
loadProjects: loadCodexProjects
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/agents/opencode.ts
|
|
350
|
+
import { access as access3, readFile as readFile2 } from "fs/promises";
|
|
351
|
+
import { homedir as homedir4 } from "os";
|
|
352
|
+
import { join as join4 } from "path";
|
|
353
|
+
import { glob as glob3 } from "tinyglobby";
|
|
354
|
+
import { z as z2 } from "zod";
|
|
355
|
+
var Message = z2.object({
|
|
356
|
+
sessionID: z2.string(),
|
|
357
|
+
role: z2.string(),
|
|
358
|
+
modelID: z2.string().optional(),
|
|
359
|
+
providerID: z2.string().optional(),
|
|
360
|
+
time: z2.object({ created: z2.number().optional(), completed: z2.number().optional() }).optional(),
|
|
361
|
+
tokens: z2.object({
|
|
362
|
+
input: z2.number().catch(0).default(0),
|
|
363
|
+
output: z2.number().catch(0).default(0),
|
|
364
|
+
reasoning: z2.number().catch(0).default(0),
|
|
365
|
+
cache: z2.object({ read: z2.number().catch(0).default(0), write: z2.number().catch(0).default(0) }).optional()
|
|
366
|
+
}).optional()
|
|
367
|
+
});
|
|
368
|
+
var Session = z2.object({
|
|
369
|
+
id: z2.string(),
|
|
370
|
+
directory: z2.string().optional(),
|
|
371
|
+
projectID: z2.string().optional(),
|
|
372
|
+
time: z2.object({ created: z2.number().optional() }).optional()
|
|
373
|
+
});
|
|
374
|
+
var Project = z2.object({ id: z2.string(), worktree: z2.string().optional() });
|
|
375
|
+
function opencodeRoot(home) {
|
|
376
|
+
if (home) return join4(home, ".local", "share", "opencode");
|
|
377
|
+
return process.env.OPENCODE_DATA_DIR ?? join4(homedir4(), ".local", "share", "opencode");
|
|
378
|
+
}
|
|
379
|
+
async function readJson(file) {
|
|
380
|
+
try {
|
|
381
|
+
return JSON.parse(await readFile2(file, "utf8"));
|
|
382
|
+
} catch {
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function loadOpencodeProjects(home, onProgress) {
|
|
387
|
+
const root = opencodeRoot(home);
|
|
388
|
+
const storage = join4(root, "storage");
|
|
389
|
+
const messageFiles = await glob3(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
|
|
390
|
+
() => []
|
|
391
|
+
);
|
|
392
|
+
if (messageFiles.length === 0) return [];
|
|
393
|
+
const sessionDir = /* @__PURE__ */ new Map();
|
|
394
|
+
const projectWorktree = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const f of await glob3(["project/*.json"], { cwd: storage, absolute: true }).catch(() => [])) {
|
|
396
|
+
const p = Project.safeParse(await readJson(f));
|
|
397
|
+
if (p.success && p.data.worktree) projectWorktree.set(p.data.id, p.data.worktree);
|
|
398
|
+
}
|
|
399
|
+
const sessionFiles = await glob3(["session/**/*.json"], { cwd: storage, absolute: true }).catch(
|
|
400
|
+
() => []
|
|
401
|
+
);
|
|
402
|
+
for (const f of sessionFiles) {
|
|
403
|
+
const s = Session.safeParse(await readJson(f));
|
|
404
|
+
if (!s.success) continue;
|
|
405
|
+
const dir = s.data.directory ?? (s.data.projectID ? projectWorktree.get(s.data.projectID) : void 0);
|
|
406
|
+
if (dir) sessionDir.set(s.data.id, dir);
|
|
407
|
+
}
|
|
408
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
409
|
+
let parsed = 0;
|
|
410
|
+
for (const f of messageFiles) {
|
|
411
|
+
onProgress?.({ parsed, total: messageFiles.length, currentProject: "OpenCode messages" });
|
|
412
|
+
const m = Message.safeParse(await readJson(f));
|
|
413
|
+
parsed += 1;
|
|
414
|
+
onProgress?.({ parsed, total: messageFiles.length, currentProject: "OpenCode messages" });
|
|
415
|
+
if (!m.success || m.data.role !== "assistant" || !m.data.tokens) continue;
|
|
416
|
+
const { sessionID, modelID, tokens, time } = m.data;
|
|
417
|
+
const stats = bySession.get(sessionID) ?? {
|
|
418
|
+
file: sessionID,
|
|
419
|
+
cwd: sessionDir.get(sessionID),
|
|
420
|
+
usageByModel: {},
|
|
421
|
+
toolCalls: {},
|
|
422
|
+
toolCostUsd: {},
|
|
423
|
+
dailyUsage: {}
|
|
424
|
+
};
|
|
425
|
+
bySession.set(sessionID, stats);
|
|
426
|
+
const model = modelID ?? "unknown";
|
|
427
|
+
const ts = time?.created ? new Date(time.created).toISOString() : void 0;
|
|
428
|
+
if (ts) {
|
|
429
|
+
if (!stats.firstTs || ts < stats.firstTs) stats.firstTs = ts;
|
|
430
|
+
if (!stats.lastTs || ts > stats.lastTs) stats.lastTs = ts;
|
|
431
|
+
}
|
|
432
|
+
const accs = [stats.usageByModel[model] ??= emptyUsage()];
|
|
433
|
+
if (ts) {
|
|
434
|
+
const day = stats.dailyUsage[ts.slice(0, 10)] ??= {};
|
|
435
|
+
accs.push(day[model] ??= emptyUsage());
|
|
436
|
+
}
|
|
437
|
+
for (const u of accs) {
|
|
438
|
+
u.inputTokens += tokens.input;
|
|
439
|
+
u.cacheReadTokens += tokens.cache?.read ?? 0;
|
|
440
|
+
u.cacheCreationTokens += tokens.cache?.write ?? 0;
|
|
441
|
+
u.outputTokens += tokens.output + tokens.reasoning;
|
|
442
|
+
u.turns += 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const s of bySession.values()) {
|
|
447
|
+
if (Object.keys(s.usageByModel).length === 0) continue;
|
|
448
|
+
const key = s.cwd ?? "(unknown project)";
|
|
449
|
+
const list = byDir.get(key) ?? [];
|
|
450
|
+
list.push(s);
|
|
451
|
+
byDir.set(key, list);
|
|
452
|
+
}
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const [dir, sessions] of byDir) {
|
|
455
|
+
out.push({
|
|
456
|
+
id: `opencode:${dir}`,
|
|
457
|
+
name: dir,
|
|
458
|
+
label: dir === "(unknown project)" ? dir : baseName(dir),
|
|
459
|
+
realPath: dir,
|
|
460
|
+
report: buildReport(sessions, []),
|
|
461
|
+
sessions,
|
|
462
|
+
serverList: []
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
var opencodeAdapter = {
|
|
469
|
+
id: "opencode",
|
|
470
|
+
name: "OpenCode",
|
|
471
|
+
supported: true,
|
|
472
|
+
async detect(home) {
|
|
473
|
+
try {
|
|
474
|
+
await access3(join4(opencodeRoot(home), "storage", "message"));
|
|
475
|
+
return true;
|
|
476
|
+
} catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
loadProjects: loadOpencodeProjects
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/agents/index.ts
|
|
484
|
+
function detectOnly(id, name, ...pathParts) {
|
|
485
|
+
return {
|
|
486
|
+
id,
|
|
487
|
+
name,
|
|
488
|
+
supported: false,
|
|
489
|
+
async detect(home = homedir5()) {
|
|
490
|
+
try {
|
|
491
|
+
await access4(join5(home, ...pathParts));
|
|
492
|
+
return true;
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
loadProjects: async () => []
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
var ADAPTERS = [
|
|
501
|
+
claudeAdapter,
|
|
502
|
+
codexAdapter,
|
|
503
|
+
opencodeAdapter,
|
|
504
|
+
detectOnly("gemini", "Gemini CLI", ".gemini", "tmp"),
|
|
505
|
+
detectOnly("cursor", "Cursor CLI", ".cursor", "chats")
|
|
506
|
+
];
|
|
507
|
+
async function loadAllAgents(home, onProgress) {
|
|
508
|
+
const out = [];
|
|
509
|
+
for (const adapter of ADAPTERS) {
|
|
510
|
+
const detected = await adapter.detect(home);
|
|
511
|
+
let projects = [];
|
|
512
|
+
if (detected && adapter.supported) {
|
|
513
|
+
projects = await adapter.loadProjects(home, (p) => onProgress?.(adapter.name, p)).catch(() => []);
|
|
514
|
+
}
|
|
515
|
+
out.push({ adapter, detected, projects });
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/ui/App.tsx
|
|
521
|
+
import { useMemo as useMemo2, useState as useState5 } from "react";
|
|
522
|
+
import { Box as Box9, Text as Text10, useApp, useInput as useInput4 } from "ink";
|
|
523
|
+
|
|
524
|
+
// src/timeframe.ts
|
|
525
|
+
var TIMEFRAMES = [
|
|
526
|
+
{ id: "all", label: "All time" },
|
|
527
|
+
{ id: "today", label: "Today" },
|
|
528
|
+
{ id: "yesterday", label: "Yesterday" },
|
|
529
|
+
{ id: "7d", label: "Last 7 days" },
|
|
530
|
+
{ id: "30d", label: "Last 30 days" }
|
|
531
|
+
];
|
|
532
|
+
var isoDay = (offsetDays, now) => new Date(now - offsetDays * 864e5).toISOString().slice(0, 10);
|
|
533
|
+
function timeframeLabel(id) {
|
|
534
|
+
return TIMEFRAMES.find((t) => t.id === id)?.label ?? id;
|
|
535
|
+
}
|
|
536
|
+
function nextTimeframe(id) {
|
|
537
|
+
const i = TIMEFRAMES.findIndex((t) => t.id === id);
|
|
538
|
+
return TIMEFRAMES[(i + 1) % TIMEFRAMES.length].id;
|
|
539
|
+
}
|
|
540
|
+
function timeframeRange(id, now = Date.now()) {
|
|
541
|
+
switch (id) {
|
|
542
|
+
case "all":
|
|
543
|
+
return void 0;
|
|
544
|
+
case "today":
|
|
545
|
+
return { from: isoDay(0, now), to: isoDay(0, now) };
|
|
546
|
+
case "yesterday":
|
|
547
|
+
return { from: isoDay(1, now), to: isoDay(1, now) };
|
|
548
|
+
case "7d":
|
|
549
|
+
return { from: isoDay(6, now), to: isoDay(0, now) };
|
|
550
|
+
case "30d":
|
|
551
|
+
return { from: isoDay(29, now), to: isoDay(0, now) };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/ui/AgentPicker.tsx
|
|
556
|
+
import { useState as useState2 } from "react";
|
|
557
|
+
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
558
|
+
|
|
559
|
+
// src/ui/Banner.tsx
|
|
560
|
+
import { Box, Text } from "ink";
|
|
561
|
+
|
|
562
|
+
// src/ui/useTerminalSize.ts
|
|
563
|
+
import { useEffect, useState } from "react";
|
|
564
|
+
import { useStdout } from "ink";
|
|
565
|
+
function useTerminalSize() {
|
|
566
|
+
const { stdout } = useStdout();
|
|
567
|
+
const [size, setSize] = useState({
|
|
568
|
+
cols: stdout?.columns ?? 80,
|
|
569
|
+
rows: stdout?.rows ?? 24
|
|
570
|
+
});
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
if (!stdout) return;
|
|
573
|
+
const onResize = () => setSize({ cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 });
|
|
574
|
+
onResize();
|
|
575
|
+
stdout.on("resize", onResize);
|
|
576
|
+
return () => {
|
|
577
|
+
stdout.off("resize", onResize);
|
|
578
|
+
};
|
|
579
|
+
}, [stdout]);
|
|
580
|
+
return size;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/ui/Banner.tsx
|
|
584
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
585
|
+
var ART = [
|
|
586
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
587
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
|
|
588
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 ",
|
|
589
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
|
|
590
|
+
" \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 "
|
|
591
|
+
];
|
|
592
|
+
function Banner({ subtitle }) {
|
|
593
|
+
const { cols, rows } = useTerminalSize();
|
|
594
|
+
const tiny = cols < 40 || rows < 18;
|
|
595
|
+
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)),
|
|
597
|
+
subtitle ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: subtitle }) : null
|
|
598
|
+
] });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/ui/theme.ts
|
|
602
|
+
var theme = {
|
|
603
|
+
accent: "cyan",
|
|
604
|
+
good: "green",
|
|
605
|
+
bad: "red",
|
|
606
|
+
warn: "yellow",
|
|
607
|
+
mcp: "magenta"
|
|
608
|
+
};
|
|
609
|
+
var MODEL_COLORS = [
|
|
610
|
+
["fable", "magenta"],
|
|
611
|
+
["opus", "cyan"],
|
|
612
|
+
["sonnet", "blue"],
|
|
613
|
+
["haiku", "green"]
|
|
614
|
+
];
|
|
615
|
+
function modelColor(modelId) {
|
|
616
|
+
for (const [key, color] of MODEL_COLORS) {
|
|
617
|
+
if (modelId.includes(key)) return color;
|
|
618
|
+
}
|
|
619
|
+
return "white";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/ui/AgentPicker.tsx
|
|
623
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
624
|
+
function status(a) {
|
|
625
|
+
if (a.projects.length > 0) {
|
|
626
|
+
const cost = a.projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
|
|
627
|
+
const sessions = a.projects.reduce((s, p) => s + p.report.sessionCount, 0);
|
|
628
|
+
return { text: `${a.projects.length} projects \xB7 ${sessions} sessions \xB7 ${usd(cost)}` };
|
|
629
|
+
}
|
|
630
|
+
if (a.detected && !a.adapter.supported) return { text: "detected \xB7 parsing not supported yet", color: theme.warn };
|
|
631
|
+
if (a.detected) return { text: "detected \xB7 no usage data", dim: true };
|
|
632
|
+
return { text: "not detected", dim: true };
|
|
633
|
+
}
|
|
634
|
+
function AgentPicker({
|
|
635
|
+
agents,
|
|
636
|
+
onSelect
|
|
637
|
+
}) {
|
|
638
|
+
const selectable = agents.map((a, i) => ({ a, i })).filter(({ a }) => a.projects.length > 0).map(({ i }) => i);
|
|
639
|
+
const [cursor, setCursor] = useState2(0);
|
|
640
|
+
const clamped = Math.min(cursor, Math.max(0, selectable.length - 1));
|
|
641
|
+
useInput((_input, key) => {
|
|
642
|
+
if (key.upArrow) setCursor(() => Math.max(0, clamped - 1));
|
|
643
|
+
if (key.downArrow) setCursor(() => Math.min(selectable.length - 1, clamped + 1));
|
|
644
|
+
if (key.return && selectable[clamped] !== void 0) onSelect(selectable[clamped]);
|
|
645
|
+
});
|
|
646
|
+
const nameW = Math.max(...agents.map((a) => a.adapter.name.length)) + 2;
|
|
647
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
648
|
+
/* @__PURE__ */ jsx2(Banner, { subtitle: "where your agents' tokens and dollars go \xB7 100% offline" }),
|
|
649
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accent, children: "Pick an agent" }),
|
|
650
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [
|
|
651
|
+
agents.map((a, i) => {
|
|
652
|
+
const st = status(a);
|
|
653
|
+
const selected = selectable[clamped] === i;
|
|
654
|
+
const selectableRow = a.projects.length > 0;
|
|
655
|
+
return /* @__PURE__ */ jsxs2(Text2, { bold: selected, children: [
|
|
656
|
+
/* @__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
|
+
),
|
|
665
|
+
/* @__PURE__ */ jsx2(Text2, { color: st.color, dimColor: st.dim, children: st.text })
|
|
666
|
+
] }, a.adapter.id);
|
|
667
|
+
}),
|
|
668
|
+
selectable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no agent usage data found on this machine" }) : null
|
|
669
|
+
] })
|
|
670
|
+
] });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/ui/Menu.tsx
|
|
674
|
+
import { Box as Box4, Text as Text5 } from "ink";
|
|
675
|
+
import SelectInput from "ink-select-input";
|
|
676
|
+
|
|
677
|
+
// src/ui/Sparkline.tsx
|
|
678
|
+
import { Text as Text3 } from "ink";
|
|
679
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
680
|
+
var LEVELS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
681
|
+
function sparkline(values) {
|
|
682
|
+
const max = Math.max(0, ...values);
|
|
683
|
+
if (max <= 0) return LEVELS[0].repeat(values.length);
|
|
684
|
+
return values.map((v) => LEVELS[v <= 0 ? 0 : Math.min(7, Math.max(1, Math.round(v / max * 7)))]).join("");
|
|
685
|
+
}
|
|
686
|
+
function Sparkline({ values, color = "cyan" }) {
|
|
687
|
+
return /* @__PURE__ */ jsx3(Text3, { color, children: sparkline(values) });
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/ui/StatCards.tsx
|
|
691
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
692
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
693
|
+
function StatCards({ stats }) {
|
|
694
|
+
return /* @__PURE__ */ jsx4(Box3, { flexWrap: "wrap", children: stats.map((s) => /* @__PURE__ */ jsxs3(
|
|
695
|
+
Box3,
|
|
696
|
+
{
|
|
697
|
+
borderStyle: "round",
|
|
698
|
+
borderColor: "gray",
|
|
699
|
+
flexDirection: "column",
|
|
700
|
+
paddingX: 1,
|
|
701
|
+
marginRight: 1,
|
|
702
|
+
children: [
|
|
703
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: s.label }),
|
|
704
|
+
/* @__PURE__ */ jsxs3(Text4, { bold: true, color: s.color, children: [
|
|
705
|
+
s.value,
|
|
706
|
+
s.hint ? /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
707
|
+
" ",
|
|
708
|
+
s.hint
|
|
709
|
+
] }) : null
|
|
710
|
+
] })
|
|
711
|
+
]
|
|
712
|
+
},
|
|
713
|
+
s.label
|
|
714
|
+
)) });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/ui/Menu.tsx
|
|
718
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
719
|
+
function Indicator({ isSelected }) {
|
|
720
|
+
return /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: isSelected ? "\u25B8 " : " " });
|
|
721
|
+
}
|
|
722
|
+
function Button({ isSelected, label }) {
|
|
723
|
+
return /* @__PURE__ */ jsx5(Text5, { color: isSelected ? "black" : theme.accent, backgroundColor: isSelected ? theme.accent : void 0, bold: true, children: ` ${label} ` });
|
|
724
|
+
}
|
|
725
|
+
function Menu({
|
|
726
|
+
projects,
|
|
727
|
+
totals,
|
|
728
|
+
agentName,
|
|
729
|
+
onSelect
|
|
730
|
+
}) {
|
|
731
|
+
const range = totals.spanStart && totals.spanEnd ? `${totals.spanStart} \u2192 ${totals.spanEnd}` : "no dated activity";
|
|
732
|
+
const last30 = totals.daily.slice(-30).map((d) => d.costUsd);
|
|
733
|
+
const unused = totals.servers.filter((s) => s.unused).length;
|
|
734
|
+
const items = [
|
|
735
|
+
{ key: "list", label: "Browse projects", value: "list" },
|
|
736
|
+
{ key: "aggregate", label: "All projects (aggregate)", value: "aggregate" },
|
|
737
|
+
{ key: "quit", label: "Quit", value: "quit" }
|
|
738
|
+
];
|
|
739
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
740
|
+
/* @__PURE__ */ jsx5(
|
|
741
|
+
Banner,
|
|
742
|
+
{
|
|
743
|
+
subtitle: `${agentName ? `${agentName} \xB7 ` : ""}where your agent's tokens and dollars go \xB7 100% offline`
|
|
744
|
+
}
|
|
745
|
+
),
|
|
746
|
+
/* @__PURE__ */ jsx5(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx5(
|
|
747
|
+
StatCards,
|
|
748
|
+
{
|
|
749
|
+
stats: [
|
|
750
|
+
{ label: "Total cost", value: usd(totals.totalCostUsd), color: theme.accent },
|
|
751
|
+
{ label: "Projected", value: usd(totals.monthlyProjectionUsd), hint: "/mo" },
|
|
752
|
+
{ label: "Projects", value: String(projects.length) },
|
|
753
|
+
{ label: "Sessions", value: String(totals.sessionCount) },
|
|
754
|
+
{ label: "Turns", value: compact(totals.totalTurns) },
|
|
755
|
+
{
|
|
756
|
+
label: "Cache hit",
|
|
757
|
+
value: pct1(totals.cacheHitRate),
|
|
758
|
+
color: totals.cacheHitRate > 0.8 ? theme.good : theme.warn
|
|
759
|
+
}
|
|
760
|
+
]
|
|
761
|
+
}
|
|
762
|
+
) }),
|
|
763
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
764
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
765
|
+
/* @__PURE__ */ jsx5(Sparkline, { values: last30.length > 0 ? last30 : [0] }),
|
|
766
|
+
/* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
|
|
767
|
+
" activity (last ",
|
|
768
|
+
Math.max(1, last30.length),
|
|
769
|
+
" active days)"
|
|
770
|
+
] })
|
|
771
|
+
] }),
|
|
772
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: range }),
|
|
773
|
+
unused > 0 ? /* @__PURE__ */ jsxs4(Text5, { color: theme.bad, children: [
|
|
774
|
+
"\u26A0 ",
|
|
775
|
+
unused,
|
|
776
|
+
" configured MCP server",
|
|
777
|
+
unused > 1 ? "s" : "",
|
|
778
|
+
" never called"
|
|
779
|
+
] }) : null
|
|
780
|
+
] }),
|
|
781
|
+
/* @__PURE__ */ jsx5(SelectInput, { items, onSelect: (item) => onSelect(item.value), indicatorComponent: Indicator, itemComponent: Button })
|
|
782
|
+
] });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/ui/ProjectList.tsx
|
|
786
|
+
import { useMemo, useState as useState3 } from "react";
|
|
787
|
+
import { Box as Box5, Text as Text6, useInput as useInput2 } from "ink";
|
|
788
|
+
|
|
789
|
+
// src/ui/bars.ts
|
|
790
|
+
function bar(value, max, width = 20) {
|
|
791
|
+
if (max <= 0 || value <= 0) return "";
|
|
792
|
+
return "\u2588".repeat(Math.max(1, Math.round(value / max * width)));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/ui/ProjectList.tsx
|
|
796
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
797
|
+
var COST_W = 10;
|
|
798
|
+
var SESS_W = 6;
|
|
799
|
+
var WHEN_W = 11;
|
|
800
|
+
var BAR_W = 14;
|
|
801
|
+
var SORT_CYCLE = ["cost", "recent", "name"];
|
|
802
|
+
function sortProjects(projects, mode) {
|
|
803
|
+
const copy = [...projects];
|
|
804
|
+
if (mode === "cost") copy.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
|
|
805
|
+
if (mode === "recent") copy.sort((a, b) => (b.report.spanEnd ?? "").localeCompare(a.report.spanEnd ?? ""));
|
|
806
|
+
if (mode === "name") copy.sort((a, b) => a.label.localeCompare(b.label));
|
|
807
|
+
return copy;
|
|
808
|
+
}
|
|
809
|
+
function displayLabels(projects) {
|
|
810
|
+
const counts = /* @__PURE__ */ new Map();
|
|
811
|
+
for (const p of projects) counts.set(p.label, (counts.get(p.label) ?? 0) + 1);
|
|
812
|
+
const out = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const p of projects) {
|
|
814
|
+
if ((counts.get(p.label) ?? 0) > 1) {
|
|
815
|
+
const parts = p.name.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
816
|
+
const parent = parts.at(-2);
|
|
817
|
+
out.set(p.id, parent ? `${p.label} (${parent})` : p.label);
|
|
818
|
+
} else {
|
|
819
|
+
out.set(p.id, p.label);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function ProjectList({
|
|
825
|
+
projects,
|
|
826
|
+
agentName,
|
|
827
|
+
onSelect,
|
|
828
|
+
onAggregate,
|
|
829
|
+
onFilteringChange
|
|
830
|
+
}) {
|
|
831
|
+
const [cursor, setCursor] = useState3(0);
|
|
832
|
+
const [filter, setFilter] = useState3("");
|
|
833
|
+
const [filtering, setFilteringRaw] = useState3(false);
|
|
834
|
+
const [sort, setSort] = useState3("cost");
|
|
835
|
+
const { cols, rows } = useTerminalSize();
|
|
836
|
+
const setFiltering = (v) => {
|
|
837
|
+
setFilteringRaw(v);
|
|
838
|
+
onFilteringChange?.(v);
|
|
839
|
+
};
|
|
840
|
+
const labels = useMemo(() => displayLabels(projects), [projects]);
|
|
841
|
+
const filtered = useMemo(() => {
|
|
842
|
+
const sorted = sortProjects(projects, sort);
|
|
843
|
+
if (!filter) return sorted;
|
|
844
|
+
const q = filter.toLowerCase();
|
|
845
|
+
return sorted.filter(
|
|
846
|
+
(p) => p.label.toLowerCase().includes(q) || p.name.toLowerCase().includes(q)
|
|
847
|
+
);
|
|
848
|
+
}, [projects, sort, filter]);
|
|
849
|
+
const pinnedShown = !filter;
|
|
850
|
+
const rowCount = filtered.length + (pinnedShown ? 1 : 0);
|
|
851
|
+
const clamped = Math.min(cursor, Math.max(0, rowCount - 1));
|
|
852
|
+
useInput2((input, key) => {
|
|
853
|
+
if (filtering) {
|
|
854
|
+
if (key.return) setFiltering(false);
|
|
855
|
+
else if (key.escape) {
|
|
856
|
+
setFiltering(false);
|
|
857
|
+
setFilter("");
|
|
858
|
+
} else if (key.backspace || key.delete) setFilter((f) => f.slice(0, -1));
|
|
859
|
+
else if (input && !key.ctrl && !key.meta) setFilter((f) => f + input);
|
|
860
|
+
setCursor(0);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (input === "/") {
|
|
864
|
+
setFiltering(true);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (input === "a") {
|
|
868
|
+
onAggregate();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (input === "s") {
|
|
872
|
+
setSort((m) => SORT_CYCLE[(SORT_CYCLE.indexOf(m) + 1) % SORT_CYCLE.length]);
|
|
873
|
+
setCursor(0);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (key.upArrow) setCursor(() => Math.max(0, clamped - 1));
|
|
877
|
+
if (key.downArrow) setCursor(() => Math.min(rowCount - 1, clamped + 1));
|
|
878
|
+
if (key.return) {
|
|
879
|
+
if (pinnedShown && clamped === 0) onAggregate();
|
|
880
|
+
else {
|
|
881
|
+
const p = filtered[clamped - (pinnedShown ? 1 : 0)];
|
|
882
|
+
if (p) onSelect(p);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
const overhead = 8;
|
|
887
|
+
const avail = cols - overhead;
|
|
888
|
+
const showBar = avail >= 24 + SESS_W + WHEN_W + COST_W + 2 + 8;
|
|
889
|
+
const showWhen = avail >= 20 + SESS_W + WHEN_W + COST_W;
|
|
890
|
+
const showSess = avail >= 16 + SESS_W + COST_W;
|
|
891
|
+
const fixed = COST_W + (showSess ? SESS_W : 0) + (showWhen ? WHEN_W : 0) + (showBar ? BAR_W + 2 : 0);
|
|
892
|
+
const nameW = Math.max(10, Math.min(42, avail - fixed - 2));
|
|
893
|
+
const barW = showBar ? Math.min(BAR_W + Math.max(0, avail - fixed - 2 - nameW), 30) : 0;
|
|
894
|
+
const visibleRows = Math.max(4, Math.min(20, rows - 10));
|
|
895
|
+
const clip = (s) => s.length > nameW ? s.slice(0, nameW - 1) + "\u2026" : s;
|
|
896
|
+
const total = projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
|
|
897
|
+
const max = Math.max(0, ...projects.map((p) => p.report.totalCostUsd));
|
|
898
|
+
const projCursor = clamped - (pinnedShown ? 1 : 0);
|
|
899
|
+
const offset = Math.max(0, Math.min(projCursor - Math.floor(visibleRows / 2), filtered.length - visibleRows));
|
|
900
|
+
const visible = filtered.slice(offset, offset + visibleRows);
|
|
901
|
+
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
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, children: [
|
|
903
|
+
/* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, flexDirection: "column", children: [
|
|
904
|
+
/* @__PURE__ */ jsxs5(Text6, { bold: true, color: theme.accent, children: [
|
|
905
|
+
"Projects",
|
|
906
|
+
agentName ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
907
|
+
" \u2014 ",
|
|
908
|
+
agentName
|
|
909
|
+
] }) : null
|
|
910
|
+
] }),
|
|
911
|
+
/* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
912
|
+
filtered.length,
|
|
913
|
+
filter ? `/${projects.length}` : "",
|
|
914
|
+
" project",
|
|
915
|
+
filtered.length === 1 && !filter ? "" : "s",
|
|
916
|
+
" \xB7",
|
|
917
|
+
" ",
|
|
918
|
+
usd(total),
|
|
919
|
+
" total \xB7 sorted by ",
|
|
920
|
+
sort,
|
|
921
|
+
filtering || filter ? /* @__PURE__ */ jsxs5(Text6, { children: [
|
|
922
|
+
" \xB7 filter: ",
|
|
923
|
+
/* @__PURE__ */ jsx6(Text6, { color: theme.warn, children: filter || "\u2026" }),
|
|
924
|
+
filtering ? /* @__PURE__ */ jsx6(Text6, { color: theme.warn, children: "\u258C" }) : null
|
|
925
|
+
] }) : null
|
|
926
|
+
] })
|
|
927
|
+
] }),
|
|
928
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " + "PROJECT".padEnd(nameW) + (showSess ? "SESS".padStart(SESS_W) : "") + (showWhen ? "LAST".padStart(WHEN_W) : "") + "COST".padStart(COST_W) + (showBar ? " SHARE" : "") }),
|
|
929
|
+
/* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [
|
|
930
|
+
pinnedShown ? /* @__PURE__ */ jsx6(Text6, { color: clamped === 0 ? theme.accent : void 0, bold: clamped === 0, children: line("All projects", String(projects.reduce((s, p) => s + p.report.sessionCount, 0)), "", usd(total), "", clamped === 0) }) : null,
|
|
931
|
+
filtered.length === 0 ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
932
|
+
'no projects match "',
|
|
933
|
+
filter,
|
|
934
|
+
'"'
|
|
935
|
+
] }) : null,
|
|
936
|
+
visible.map((p, i) => {
|
|
937
|
+
const idx = offset + i;
|
|
938
|
+
const selected = idx === projCursor;
|
|
939
|
+
return /* @__PURE__ */ jsx6(Text6, { color: selected ? theme.accent : void 0, bold: selected, children: line(
|
|
940
|
+
labels.get(p.id) ?? p.label,
|
|
941
|
+
String(p.report.sessionCount),
|
|
942
|
+
relativeDate(p.report.spanEnd),
|
|
943
|
+
usd(p.report.totalCostUsd),
|
|
944
|
+
bar(p.report.totalCostUsd, max, barW),
|
|
945
|
+
selected
|
|
946
|
+
) }, p.id);
|
|
947
|
+
}),
|
|
948
|
+
filtered.length > visibleRows ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
|
|
949
|
+
"\u2026",
|
|
950
|
+
offset + visibleRows < filtered.length ? ` ${filtered.length - offset - visibleRows} below` : "",
|
|
951
|
+
offset > 0 ? ` \xB7 ${offset} above` : ""
|
|
952
|
+
] }) : null
|
|
953
|
+
] })
|
|
954
|
+
] });
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/ui/Dashboard.tsx
|
|
958
|
+
import { useState as useState4 } from "react";
|
|
959
|
+
import { Box as Box7, Text as Text8, useInput as useInput3 } from "ink";
|
|
960
|
+
|
|
961
|
+
// src/ui/BarChart.tsx
|
|
962
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
963
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
964
|
+
function BarChart({
|
|
965
|
+
rows,
|
|
966
|
+
width = 22,
|
|
967
|
+
showShare = false,
|
|
968
|
+
maxLabel = 34
|
|
969
|
+
}) {
|
|
970
|
+
const max = Math.max(0, ...rows.map((r) => r.value));
|
|
971
|
+
const total = rows.reduce((s, r) => s + r.value, 0);
|
|
972
|
+
const clip = (s) => s.length > maxLabel ? "\u2026" + s.slice(-(maxLabel - 1)) : s;
|
|
973
|
+
const labelW = Math.min(maxLabel, Math.max(0, ...rows.map((r) => r.label.length)));
|
|
974
|
+
const dispW = Math.max(0, ...rows.map((r) => (r.display ?? String(r.value)).length));
|
|
975
|
+
return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", children: rows.map((r) => /* @__PURE__ */ jsxs6(Text7, { children: [
|
|
976
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: clip(r.label).padEnd(labelW) }),
|
|
977
|
+
" ",
|
|
978
|
+
/* @__PURE__ */ jsx7(Text7, { color: r.color ?? "cyan", children: bar(r.value, max, width).padEnd(width) }),
|
|
979
|
+
" ",
|
|
980
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: (r.display ?? String(r.value)).padStart(dispW) }),
|
|
981
|
+
showShare && total > 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${pct(r.value / total).padStart(4)}` }) : null
|
|
982
|
+
] }, r.label)) });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/ui/Dashboard.tsx
|
|
986
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
987
|
+
var TABS = ["Overview", "Models", "Tools", "Servers", "Sessions", "Activity"];
|
|
988
|
+
function TabBar({ tab, narrow }) {
|
|
989
|
+
return /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: TABS.map((t, i) => /* @__PURE__ */ jsx8(
|
|
990
|
+
Text8,
|
|
991
|
+
{
|
|
992
|
+
color: i === tab ? "black" : "gray",
|
|
993
|
+
backgroundColor: i === tab ? theme.accent : void 0,
|
|
994
|
+
bold: i === tab,
|
|
995
|
+
children: narrow ? ` ${i + 1}${i === tab ? ` ${t}` : ""} ` : ` ${i + 1} ${t} `
|
|
996
|
+
},
|
|
997
|
+
t
|
|
998
|
+
)) });
|
|
999
|
+
}
|
|
1000
|
+
function lastDays(r, n) {
|
|
1001
|
+
const byDate = new Map(r.daily.map((d) => [d.date, d]));
|
|
1002
|
+
const out = [];
|
|
1003
|
+
const today = r.spanEnd ? Date.parse(r.spanEnd) : Date.now();
|
|
1004
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1005
|
+
const date = new Date(today - i * 864e5).toISOString().slice(0, 10);
|
|
1006
|
+
const d = byDate.get(date);
|
|
1007
|
+
out.push({ date, costUsd: d?.costUsd ?? 0, turns: d?.turns ?? 0 });
|
|
1008
|
+
}
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
1011
|
+
function Section({ title, children }) {
|
|
1012
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
|
|
1013
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, dimColor: true, children: title.toUpperCase() }),
|
|
1014
|
+
children
|
|
1015
|
+
] });
|
|
1016
|
+
}
|
|
1017
|
+
function Overview({ r, chartW }) {
|
|
1018
|
+
const days = lastDays(r, 30);
|
|
1019
|
+
const peak = days.reduce((a, b) => b.costUsd > a.costUsd ? b : a, days[0]);
|
|
1020
|
+
const models = Object.entries(r.costByModel).sort(([, a], [, b]) => b.total - a.total).slice(0, 4).map(([m, c]) => ({ label: shortModel(m), value: c.total, display: usd(c.total), color: modelColor(m) }));
|
|
1021
|
+
const tools = Object.entries(r.toolCalls).sort(([, a], [, b]) => b - a).slice(0, 5).map(([n, c]) => ({ label: n, value: c, display: compact(c), color: n.startsWith("mcp__") ? theme.mcp : theme.accent }));
|
|
1022
|
+
const unused = r.servers.filter((s) => s.unused).length;
|
|
1023
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1024
|
+
/* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx8(
|
|
1025
|
+
StatCards,
|
|
1026
|
+
{
|
|
1027
|
+
stats: [
|
|
1028
|
+
{ label: "Total cost", value: usd(r.totalCostUsd), color: theme.accent },
|
|
1029
|
+
{ label: "Projected", value: usd(r.monthlyProjectionUsd), hint: "/mo" },
|
|
1030
|
+
{ label: "Sessions", value: String(r.sessionCount) },
|
|
1031
|
+
{ label: "Turns", value: compact(r.totalTurns) },
|
|
1032
|
+
{ label: "Cache hit", value: pct1(r.cacheHitRate), color: r.cacheHitRate > 0.8 ? theme.good : theme.warn },
|
|
1033
|
+
{ label: "Cache saved", value: usd(r.cacheSavingsUsd), color: theme.good }
|
|
1034
|
+
]
|
|
1035
|
+
}
|
|
1036
|
+
) }),
|
|
1037
|
+
/* @__PURE__ */ jsx8(Section, { title: "last 30 days", children: /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1038
|
+
/* @__PURE__ */ jsx8(Sparkline, { values: days.map((d) => d.costUsd) }),
|
|
1039
|
+
peak && peak.costUsd > 0 ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1040
|
+
" peak ",
|
|
1041
|
+
usd(peak.costUsd),
|
|
1042
|
+
" on ",
|
|
1043
|
+
peak.date
|
|
1044
|
+
] }) : null
|
|
1045
|
+
] }) }),
|
|
1046
|
+
models.length > 0 ? /* @__PURE__ */ jsx8(Section, { title: "cost by model", children: /* @__PURE__ */ jsx8(BarChart, { rows: models, width: chartW, showShare: true }) }) : null,
|
|
1047
|
+
tools.length > 0 ? /* @__PURE__ */ jsx8(Section, { title: "top tools", children: /* @__PURE__ */ jsx8(BarChart, { rows: tools, width: chartW }) }) : null,
|
|
1048
|
+
unused > 0 ? /* @__PURE__ */ jsxs7(Text8, { color: theme.bad, children: [
|
|
1049
|
+
"\u26A0 ",
|
|
1050
|
+
unused,
|
|
1051
|
+
" MCP server",
|
|
1052
|
+
unused > 1 ? "s" : "",
|
|
1053
|
+
" configured but never called \u2014 see tab 4"
|
|
1054
|
+
] }) : null
|
|
1055
|
+
] });
|
|
1056
|
+
}
|
|
1057
|
+
function Models({ r, cols }) {
|
|
1058
|
+
const entries = Object.entries(r.usageByModel).sort(
|
|
1059
|
+
([a], [b]) => r.costByModel[b].total - r.costByModel[a].total
|
|
1060
|
+
);
|
|
1061
|
+
if (entries.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no usage recorded" });
|
|
1062
|
+
const nameW = Math.max(...entries.map(([m]) => shortModel(m).length), 5);
|
|
1063
|
+
const col = (s) => s.padStart(10);
|
|
1064
|
+
const full = cols >= nameW + 2 + 10 * 6 + 7;
|
|
1065
|
+
const mid = cols >= nameW + 2 + 10 * 3 + 7;
|
|
1066
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1067
|
+
/* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1068
|
+
"MODEL".padEnd(nameW + 2),
|
|
1069
|
+
full ? col("INPUT") + col("CACHE RD") + col("CACHE WR") : "",
|
|
1070
|
+
mid ? col("OUTPUT") + col("TURNS") : "",
|
|
1071
|
+
col("COST"),
|
|
1072
|
+
" SHARE"
|
|
1073
|
+
] }),
|
|
1074
|
+
entries.map(([m, u]) => {
|
|
1075
|
+
const c = r.costByModel[m];
|
|
1076
|
+
const share = r.totalCostUsd > 0 ? c.total / r.totalCostUsd : 0;
|
|
1077
|
+
return /* @__PURE__ */ jsxs7(Text8, { children: [
|
|
1078
|
+
/* @__PURE__ */ jsx8(Text8, { color: modelColor(m), bold: true, children: shortModel(m).padEnd(nameW + 2) }),
|
|
1079
|
+
full ? col(compact(u.inputTokens)) + col(compact(u.cacheReadTokens)) + col(compact(u.cacheCreationTokens)) : "",
|
|
1080
|
+
mid ? col(compact(u.outputTokens)) + col(compact(u.turns)) : "",
|
|
1081
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: col(usd(c.total)) }),
|
|
1082
|
+
/* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${pct(share).padStart(4)}` })
|
|
1083
|
+
] }, m);
|
|
1084
|
+
}),
|
|
1085
|
+
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1086
|
+
"cost split \u2014 in ",
|
|
1087
|
+
usd(entries.reduce((s, [m]) => s + r.costByModel[m].input, 0)),
|
|
1088
|
+
" \xB7 cache rd",
|
|
1089
|
+
" ",
|
|
1090
|
+
usd(entries.reduce((s, [m]) => s + r.costByModel[m].cacheRead, 0)),
|
|
1091
|
+
" \xB7 cache wr",
|
|
1092
|
+
" ",
|
|
1093
|
+
usd(entries.reduce((s, [m]) => s + r.costByModel[m].cacheWrite, 0)),
|
|
1094
|
+
" \xB7 out",
|
|
1095
|
+
" ",
|
|
1096
|
+
usd(entries.reduce((s, [m]) => s + r.costByModel[m].output, 0))
|
|
1097
|
+
] }) })
|
|
1098
|
+
] });
|
|
1099
|
+
}
|
|
1100
|
+
function Tools({ r, chartW, cols }) {
|
|
1101
|
+
const costs = r.toolCostUsd ?? {};
|
|
1102
|
+
const anyCost = Object.values(costs).some((c) => c > 0);
|
|
1103
|
+
const rows = Object.entries(r.toolCalls).map(([n, calls]) => ({ name: n, calls, cost: costs[n] ?? 0 })).sort((a, b) => anyCost ? b.cost - a.cost : b.calls - a.calls).slice(0, 15).map((t) => ({
|
|
1104
|
+
label: t.name,
|
|
1105
|
+
value: anyCost ? t.cost : t.calls,
|
|
1106
|
+
display: anyCost ? `${usd(t.cost)} \xB7 ${compact(t.calls)} calls` : compact(t.calls),
|
|
1107
|
+
color: t.name.startsWith("mcp__") ? theme.mcp : theme.accent
|
|
1108
|
+
}));
|
|
1109
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no tool calls" });
|
|
1110
|
+
const maxLabel = Math.max(12, Math.min(40, cols - chartW - (anyCost ? 30 : 16)));
|
|
1111
|
+
const totalCalls = Object.values(r.toolCalls).reduce((s, n) => s + n, 0);
|
|
1112
|
+
const mcpCalls = Object.entries(r.toolCalls).filter(([n]) => n.startsWith("mcp__")).reduce((s, [, n]) => s + n, 0);
|
|
1113
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1114
|
+
/* @__PURE__ */ jsx8(BarChart, { rows, width: chartW, showShare: true, maxLabel }),
|
|
1115
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
1116
|
+
/* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1117
|
+
compact(totalCalls),
|
|
1118
|
+
" calls across ",
|
|
1119
|
+
Object.keys(r.toolCalls).length,
|
|
1120
|
+
" tools \xB7",
|
|
1121
|
+
" ",
|
|
1122
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.mcp, children: "magenta = MCP" }),
|
|
1123
|
+
" (",
|
|
1124
|
+
pct(totalCalls > 0 ? mcpCalls / totalCalls : 0),
|
|
1125
|
+
" of calls)"
|
|
1126
|
+
] }),
|
|
1127
|
+
anyCost ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "est. cost = each turn's bill split across the tools that turn called" }) : null
|
|
1128
|
+
] })
|
|
1129
|
+
] });
|
|
1130
|
+
}
|
|
1131
|
+
function Servers({ r, cols }) {
|
|
1132
|
+
if (r.servers.length === 0)
|
|
1133
|
+
return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no MCP servers configured or observed for this project" });
|
|
1134
|
+
const pretty = (s) => !s.configured && s.name.startsWith("plugin_") ? s.name.replace(/^plugin_/, "").replace("_", ":") : s.name;
|
|
1135
|
+
const nameW = Math.min(30, Math.max(...r.servers.map((s) => pretty(s).length), 6));
|
|
1136
|
+
const sorted = [...r.servers].sort((a, b) => (b.estCostUsd ?? 0) - (a.estCostUsd ?? 0));
|
|
1137
|
+
const clipName = (n) => n.length > nameW ? n.slice(0, nameW - 1) + "\u2026" : n;
|
|
1138
|
+
const wide = cols >= 80;
|
|
1139
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1140
|
+
/* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1141
|
+
"SERVER".padEnd(nameW),
|
|
1142
|
+
"CALLS".padStart(8),
|
|
1143
|
+
"EST COST".padStart(10),
|
|
1144
|
+
" STATUS ",
|
|
1145
|
+
wide ? "SOURCE" : ""
|
|
1146
|
+
] }),
|
|
1147
|
+
sorted.map((s) => /* @__PURE__ */ jsxs7(Text8, { children: [
|
|
1148
|
+
/* @__PURE__ */ jsx8(Text8, { color: s.configured ? theme.accent : theme.mcp, children: clipName(pretty(s)).padEnd(nameW) }),
|
|
1149
|
+
compact(s.callsObserved).padStart(8),
|
|
1150
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: usd(s.estCostUsd ?? 0).padStart(10) }),
|
|
1151
|
+
" ",
|
|
1152
|
+
s.unused ? /* @__PURE__ */ jsx8(Text8, { color: theme.bad, children: "\u25CF UNUSED" }) : /* @__PURE__ */ jsx8(Text8, { color: theme.good, children: "\u25CF used " }),
|
|
1153
|
+
wide ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + (s.configured ? s.source.replace(/\\/g, "/") : "plugin / external config") }) : null
|
|
1154
|
+
] }, s.name)),
|
|
1155
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
1156
|
+
sorted.some((s) => s.unused) ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Unused servers still load their tool schemas into every request \u2014 dead weight in the context window. Consider removing them." }) : null,
|
|
1157
|
+
sorted.some((s) => !s.configured) ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1158
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.mcp, children: "magenta" }),
|
|
1159
|
+
" servers were seen in transcripts but live outside .mcp.json / ~/.claude.json (plugins, managed configs)."
|
|
1160
|
+
] }) : null,
|
|
1161
|
+
sorted.some((s) => (s.estCostUsd ?? 0) > 0) ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "est. cost = each turn's bill split across the tools that turn called" }) : null
|
|
1162
|
+
] })
|
|
1163
|
+
] });
|
|
1164
|
+
}
|
|
1165
|
+
function Sessions({ r, cols }) {
|
|
1166
|
+
const rows = r.sessions.slice(0, 12);
|
|
1167
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no sessions" });
|
|
1168
|
+
const wide = cols >= 60;
|
|
1169
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1170
|
+
/* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1171
|
+
"WHEN".padEnd(12),
|
|
1172
|
+
"LENGTH".padEnd(9),
|
|
1173
|
+
wide ? "TURNS".padStart(6) + "TOOLS".padStart(7) : "",
|
|
1174
|
+
"COST".padStart(9),
|
|
1175
|
+
" MODEL"
|
|
1176
|
+
] }),
|
|
1177
|
+
rows.map((s) => /* @__PURE__ */ jsxs7(Text8, { children: [
|
|
1178
|
+
relativeDate(s.start).padEnd(12),
|
|
1179
|
+
duration(s.start, s.end).padEnd(9),
|
|
1180
|
+
wide ? compact(s.turns).padStart(6) + compact(s.toolCallCount).padStart(7) : "",
|
|
1181
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, children: usd(s.costUsd).padStart(9) }),
|
|
1182
|
+
/* @__PURE__ */ jsx8(Text8, { color: s.models[0] ? modelColor(s.models[0]) : void 0, children: " " + (s.models[0] ? shortModel(s.models[0]) : "\u2014") })
|
|
1183
|
+
] }, s.file)),
|
|
1184
|
+
r.sessions.length > rows.length ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1185
|
+
"\u2026 ",
|
|
1186
|
+
r.sessions.length - rows.length,
|
|
1187
|
+
" more (sorted by cost)"
|
|
1188
|
+
] }) : null
|
|
1189
|
+
] });
|
|
1190
|
+
}
|
|
1191
|
+
function Activity({ r, chartW, cols }) {
|
|
1192
|
+
const days = lastDays(r, 21).filter((d, i, all) => d.costUsd > 0 || i >= all.length - 14);
|
|
1193
|
+
const wide = cols >= 66;
|
|
1194
|
+
const rows = days.slice(-16).map((d) => ({
|
|
1195
|
+
label: `${d.date} ${relativeDate(d.date) === "today" ? "\u25C2" : " "}`,
|
|
1196
|
+
value: d.costUsd,
|
|
1197
|
+
display: d.costUsd > 0 ? wide ? `${usd(d.costUsd)} \xB7 ${compact(d.turns)} turns` : usd(d.costUsd) : "\u2014"
|
|
1198
|
+
}));
|
|
1199
|
+
if (r.daily.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no dated activity" });
|
|
1200
|
+
const avg = r.totalCostUsd / Math.max(1, r.daily.length);
|
|
1201
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1202
|
+
/* @__PURE__ */ jsx8(BarChart, { rows, width: chartW }),
|
|
1203
|
+
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1204
|
+
r.daily.length,
|
|
1205
|
+
" active days \xB7 avg ",
|
|
1206
|
+
usd(avg),
|
|
1207
|
+
"/active day \xB7 projected",
|
|
1208
|
+
" ",
|
|
1209
|
+
usd(r.monthlyProjectionUsd),
|
|
1210
|
+
"/mo"
|
|
1211
|
+
] }) })
|
|
1212
|
+
] });
|
|
1213
|
+
}
|
|
1214
|
+
function Dashboard({
|
|
1215
|
+
project,
|
|
1216
|
+
initialTab = 0,
|
|
1217
|
+
timeframe
|
|
1218
|
+
}) {
|
|
1219
|
+
const [tab, setTab] = useState4(initialTab);
|
|
1220
|
+
const { cols } = useTerminalSize();
|
|
1221
|
+
useInput3((input, key) => {
|
|
1222
|
+
const n = Number.parseInt(input, 10);
|
|
1223
|
+
if (n >= 1 && n <= TABS.length) setTab(n - 1);
|
|
1224
|
+
if (key.rightArrow) setTab((t) => (t + 1) % TABS.length);
|
|
1225
|
+
if (key.leftArrow) setTab((t) => (t + TABS.length - 1) % TABS.length);
|
|
1226
|
+
});
|
|
1227
|
+
const r = project.report;
|
|
1228
|
+
const span = r.spanStart && r.spanEnd ? `${r.spanStart} \u2192 ${r.spanEnd}` : `${r.spanDays}d`;
|
|
1229
|
+
const fullPath = project.name.replace(/\\/g, "/");
|
|
1230
|
+
const chartW = Math.max(10, Math.min(26, cols - 54));
|
|
1231
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, children: [
|
|
1232
|
+
/* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
|
|
1233
|
+
/* @__PURE__ */ jsxs7(Text8, { children: [
|
|
1234
|
+
/* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accent, children: project.label }),
|
|
1235
|
+
project.label !== fullPath && cols >= project.label.length + fullPath.length + 6 ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1236
|
+
" ",
|
|
1237
|
+
fullPath
|
|
1238
|
+
] }) : null
|
|
1239
|
+
] }),
|
|
1240
|
+
/* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1241
|
+
timeframe && timeframe !== "All time" ? /* @__PURE__ */ jsxs7(Text8, { color: "yellow", children: [
|
|
1242
|
+
timeframe,
|
|
1243
|
+
" \xB7 "
|
|
1244
|
+
] }) : null,
|
|
1245
|
+
r.sessionCount,
|
|
1246
|
+
" sessions \xB7 ",
|
|
1247
|
+
span,
|
|
1248
|
+
cols >= 70 ? " \xB7 API-equivalent cost, computed offline" : ""
|
|
1249
|
+
] })
|
|
1250
|
+
] }),
|
|
1251
|
+
/* @__PURE__ */ jsx8(TabBar, { tab, narrow: cols < 72 }),
|
|
1252
|
+
/* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1253
|
+
tab === 0 ? /* @__PURE__ */ jsx8(Overview, { r, chartW }) : null,
|
|
1254
|
+
tab === 1 ? /* @__PURE__ */ jsx8(Models, { r, cols }) : null,
|
|
1255
|
+
tab === 2 ? /* @__PURE__ */ jsx8(Tools, { r, chartW, cols }) : null,
|
|
1256
|
+
tab === 3 ? /* @__PURE__ */ jsx8(Servers, { r, cols }) : null,
|
|
1257
|
+
tab === 4 ? /* @__PURE__ */ jsx8(Sessions, { r, cols }) : null,
|
|
1258
|
+
tab === 5 ? /* @__PURE__ */ jsx8(Activity, { r, chartW, cols }) : null
|
|
1259
|
+
] })
|
|
1260
|
+
] });
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/ui/HelpOverlay.tsx
|
|
1264
|
+
import { Box as Box8, Text as Text9 } from "ink";
|
|
1265
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1266
|
+
var KEYS = [
|
|
1267
|
+
["\u2191 \u2193", "move selection"],
|
|
1268
|
+
["\u23CE", "open / select"],
|
|
1269
|
+
["\u2190 \u2192 or 1\u20136", "switch dashboard tab"],
|
|
1270
|
+
["/", "filter project list (esc clears)"],
|
|
1271
|
+
["s", "cycle project sort: cost \xB7 recent \xB7 name"],
|
|
1272
|
+
["t", "cycle timeframe: all \xB7 today \xB7 yesterday \xB7 7d \xB7 30d"],
|
|
1273
|
+
["esc", "go back"],
|
|
1274
|
+
["?", "toggle this help"],
|
|
1275
|
+
["q", "quit"]
|
|
1276
|
+
];
|
|
1277
|
+
function HelpOverlay() {
|
|
1278
|
+
return /* @__PURE__ */ jsxs8(Box8, { borderStyle: "double", borderColor: "cyan", flexDirection: "column", paddingX: 2, paddingY: 1, alignSelf: "center", children: [
|
|
1279
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Keyboard shortcuts" }),
|
|
1280
|
+
/* @__PURE__ */ jsx9(Text9, { children: " " }),
|
|
1281
|
+
KEYS.map(([key, desc]) => /* @__PURE__ */ jsxs8(Text9, { children: [
|
|
1282
|
+
/* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: key.padEnd(14) }),
|
|
1283
|
+
/* @__PURE__ */ jsx9(Text9, { children: desc })
|
|
1284
|
+
] }, key)),
|
|
1285
|
+
/* @__PURE__ */ jsx9(Text9, { children: " " }),
|
|
1286
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "All data is read locally from ~/.claude \u2014 nothing leaves this machine." })
|
|
1287
|
+
] });
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/ui/App.tsx
|
|
1291
|
+
import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1292
|
+
var HINTS = {
|
|
1293
|
+
agents: "\u2191\u2193 navigate \xB7 \u23CE select \xB7 ? help \xB7 q quit",
|
|
1294
|
+
menu: "\u2191\u2193 navigate \xB7 \u23CE select \xB7 esc agents \xB7 ? help \xB7 q quit",
|
|
1295
|
+
list: "\u2191\u2193 move \xB7 \u23CE open \xB7 / filter \xB7 s sort \xB7 a all \xB7 esc back \xB7 ? help \xB7 q quit",
|
|
1296
|
+
project: "1\u20136 \u2190\u2192 tabs \xB7 esc back \xB7 ? help \xB7 q quit",
|
|
1297
|
+
aggregate: "1\u20136 \u2190\u2192 tabs \xB7 esc back \xB7 ? help \xB7 q quit"
|
|
1298
|
+
};
|
|
1299
|
+
function wrapProjects(projects) {
|
|
1300
|
+
return [
|
|
1301
|
+
{
|
|
1302
|
+
adapter: {
|
|
1303
|
+
id: "claude",
|
|
1304
|
+
name: "Claude Code",
|
|
1305
|
+
supported: true,
|
|
1306
|
+
detect: async () => true,
|
|
1307
|
+
loadProjects: async () => projects
|
|
1308
|
+
},
|
|
1309
|
+
detected: true,
|
|
1310
|
+
projects
|
|
1311
|
+
}
|
|
1312
|
+
];
|
|
1313
|
+
}
|
|
1314
|
+
function App({
|
|
1315
|
+
agents,
|
|
1316
|
+
projects,
|
|
1317
|
+
initialView,
|
|
1318
|
+
initialSelected = 0
|
|
1319
|
+
}) {
|
|
1320
|
+
const { exit } = useApp();
|
|
1321
|
+
const agentList = useMemo2(
|
|
1322
|
+
() => agents ?? wrapProjects(projects ?? []),
|
|
1323
|
+
[agents, projects]
|
|
1324
|
+
);
|
|
1325
|
+
const multiAgent = agentList.length > 1;
|
|
1326
|
+
const firstWithData = Math.max(0, agentList.findIndex((a) => a.projects.length > 0));
|
|
1327
|
+
const [agentIdx, setAgentIdx] = useState5(firstWithData);
|
|
1328
|
+
const [view, setView] = useState5(initialView ?? (multiAgent ? "agents" : "menu"));
|
|
1329
|
+
const [aggFrom, setAggFrom] = useState5("menu");
|
|
1330
|
+
const activeProjects = agentList[agentIdx]?.projects ?? [];
|
|
1331
|
+
const [selected, setSelected] = useState5(
|
|
1332
|
+
activeProjects[initialSelected]
|
|
1333
|
+
);
|
|
1334
|
+
const [help, setHelp] = useState5(false);
|
|
1335
|
+
const [filterCapture, setFilterCapture] = useState5(false);
|
|
1336
|
+
const [timeframe, setTimeframe] = useState5("all");
|
|
1337
|
+
const scoped = useMemo2(
|
|
1338
|
+
() => applyTimeframe(activeProjects, timeframeRange(timeframe)),
|
|
1339
|
+
[activeProjects, timeframe]
|
|
1340
|
+
);
|
|
1341
|
+
const totals = useMemo2(() => aggregate(scoped), [scoped]);
|
|
1342
|
+
const agentName = agentList[agentIdx]?.adapter.name ?? "";
|
|
1343
|
+
useInput4((input, key) => {
|
|
1344
|
+
if (help) {
|
|
1345
|
+
setHelp(false);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (filterCapture) return;
|
|
1349
|
+
if (input === "q") exit();
|
|
1350
|
+
if (input === "?") setHelp(true);
|
|
1351
|
+
if (input === "t" && view !== "agents") setTimeframe((tf) => nextTimeframe(tf));
|
|
1352
|
+
if (key.escape) {
|
|
1353
|
+
if (view === "project") setView("list");
|
|
1354
|
+
else if (view === "aggregate") setView(aggFrom);
|
|
1355
|
+
else if (view === "list") setView("menu");
|
|
1356
|
+
else if (view === "menu" && multiAgent) setView("agents");
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
const anyData = agentList.some((a) => a.projects.length > 0);
|
|
1360
|
+
if (!anyData) return /* @__PURE__ */ jsx10(Text10, { children: "No agent usage data found (Claude Code, Codex, OpenCode\u2026)." });
|
|
1361
|
+
const tfLabel = timeframeLabel(timeframe);
|
|
1362
|
+
let content;
|
|
1363
|
+
if (help) {
|
|
1364
|
+
content = /* @__PURE__ */ jsx10(HelpOverlay, {});
|
|
1365
|
+
} else if (view === "agents") {
|
|
1366
|
+
content = /* @__PURE__ */ jsx10(
|
|
1367
|
+
AgentPicker,
|
|
1368
|
+
{
|
|
1369
|
+
agents: agentList,
|
|
1370
|
+
onSelect: (i) => {
|
|
1371
|
+
setAgentIdx(i);
|
|
1372
|
+
setSelected(void 0);
|
|
1373
|
+
setView("menu");
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
} else if (view === "menu") {
|
|
1378
|
+
content = /* @__PURE__ */ jsx10(
|
|
1379
|
+
Menu,
|
|
1380
|
+
{
|
|
1381
|
+
projects: scoped,
|
|
1382
|
+
totals,
|
|
1383
|
+
agentName,
|
|
1384
|
+
onSelect: (action) => {
|
|
1385
|
+
if (action === "quit") exit();
|
|
1386
|
+
else {
|
|
1387
|
+
if (action === "aggregate") setAggFrom("menu");
|
|
1388
|
+
setView(action);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
} else if (view === "aggregate") {
|
|
1394
|
+
content = /* @__PURE__ */ jsx10(
|
|
1395
|
+
Dashboard,
|
|
1396
|
+
{
|
|
1397
|
+
project: {
|
|
1398
|
+
id: "__all__",
|
|
1399
|
+
name: `All ${agentName} projects`,
|
|
1400
|
+
label: `All ${agentName} projects`,
|
|
1401
|
+
report: totals
|
|
1402
|
+
},
|
|
1403
|
+
timeframe: tfLabel
|
|
1404
|
+
}
|
|
1405
|
+
);
|
|
1406
|
+
} else if (view === "project" && selected) {
|
|
1407
|
+
const current = scoped.find((p) => p.id === selected.id);
|
|
1408
|
+
content = current ? /* @__PURE__ */ jsx10(Dashboard, { project: current, timeframe: tfLabel }) : /* @__PURE__ */ jsx10(Box9, { paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
|
|
1409
|
+
selected.label,
|
|
1410
|
+
": no activity ",
|
|
1411
|
+
tfLabel.toLowerCase(),
|
|
1412
|
+
" \u2014 press t to change the timeframe or esc to go back."
|
|
1413
|
+
] }) });
|
|
1414
|
+
} else {
|
|
1415
|
+
content = /* @__PURE__ */ jsx10(
|
|
1416
|
+
ProjectList,
|
|
1417
|
+
{
|
|
1418
|
+
projects: scoped,
|
|
1419
|
+
agentName,
|
|
1420
|
+
onSelect: (p) => {
|
|
1421
|
+
setSelected(p);
|
|
1422
|
+
setView("project");
|
|
1423
|
+
},
|
|
1424
|
+
onAggregate: () => {
|
|
1425
|
+
setAggFrom("list");
|
|
1426
|
+
setView("aggregate");
|
|
1427
|
+
},
|
|
1428
|
+
onFilteringChange: setFilterCapture
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", flexGrow: 1, children: [
|
|
1433
|
+
/* @__PURE__ */ jsx10(Box9, { flexDirection: "column", flexGrow: 1, children: content }),
|
|
1434
|
+
/* @__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: [
|
|
1435
|
+
/* @__PURE__ */ jsxs9(Text10, { color: timeframe === "all" ? void 0 : "yellow", children: [
|
|
1436
|
+
"\u23F1 ",
|
|
1437
|
+
tfLabel
|
|
1438
|
+
] }),
|
|
1439
|
+
" \xB7 t cycle \xB7 ",
|
|
1440
|
+
HINTS[view]
|
|
1441
|
+
] }) }) })
|
|
1442
|
+
] });
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// src/ui/Root.tsx
|
|
1446
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1447
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1448
|
+
function ProgressBar({ done, total, width = 30 }) {
|
|
1449
|
+
const filled = total > 0 ? Math.round(done / total * width) : 0;
|
|
1450
|
+
return /* @__PURE__ */ jsxs10(Text11, { children: [
|
|
1451
|
+
/* @__PURE__ */ jsx11(Text11, { color: theme.accent, children: "\u2588".repeat(filled) }),
|
|
1452
|
+
/* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "\u2591".repeat(width - filled) })
|
|
1453
|
+
] });
|
|
1454
|
+
}
|
|
1455
|
+
function Root() {
|
|
1456
|
+
const { exit } = useApp2();
|
|
1457
|
+
const [agents, setAgents] = useState6();
|
|
1458
|
+
const [agentName, setAgentName] = useState6("");
|
|
1459
|
+
const [progress, setProgress] = useState6({ parsed: 0, total: 0 });
|
|
1460
|
+
const [frame, setFrame] = useState6(0);
|
|
1461
|
+
useEffect2(() => {
|
|
1462
|
+
const timer = setInterval(() => setFrame((f) => f + 1), 80);
|
|
1463
|
+
loadAllAgents(void 0, (agent, p) => {
|
|
1464
|
+
setAgentName(agent);
|
|
1465
|
+
setProgress(p);
|
|
1466
|
+
}).then(setAgents).catch(() => setAgents([])).finally(() => clearInterval(timer));
|
|
1467
|
+
return () => clearInterval(timer);
|
|
1468
|
+
}, []);
|
|
1469
|
+
const empty = agents !== void 0 && !agents.some((a) => a.projects.length > 0);
|
|
1470
|
+
useEffect2(() => {
|
|
1471
|
+
if (empty) exit();
|
|
1472
|
+
}, [empty, exit]);
|
|
1473
|
+
if (!agents) {
|
|
1474
|
+
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
|
|
1475
|
+
/* @__PURE__ */ jsx11(Banner, { subtitle: "where your agents' tokens and dollars go \xB7 100% offline" }),
|
|
1476
|
+
/* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
|
|
1477
|
+
/* @__PURE__ */ jsxs10(Text11, { children: [
|
|
1478
|
+
/* @__PURE__ */ jsx11(Text11, { color: theme.accent, children: SPINNER[frame % SPINNER.length] }),
|
|
1479
|
+
" Parsing",
|
|
1480
|
+
agentName ? ` ${agentName}` : "",
|
|
1481
|
+
"\u2026",
|
|
1482
|
+
" ",
|
|
1483
|
+
/* @__PURE__ */ jsxs10(Text11, { bold: true, children: [
|
|
1484
|
+
progress.parsed,
|
|
1485
|
+
"/",
|
|
1486
|
+
progress.total || "?"
|
|
1487
|
+
] })
|
|
1488
|
+
] }),
|
|
1489
|
+
/* @__PURE__ */ jsx11(ProgressBar, { done: progress.parsed, total: progress.total }),
|
|
1490
|
+
progress.currentProject ? /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: progress.currentProject.replace(/\\/g, "/") }) : null
|
|
1491
|
+
] })
|
|
1492
|
+
] });
|
|
1493
|
+
}
|
|
1494
|
+
if (empty) return /* @__PURE__ */ jsx11(Text11, { children: "No agent usage data found (Claude Code, Codex, OpenCode\u2026)." });
|
|
1495
|
+
return /* @__PURE__ */ jsx11(App, { agents });
|
|
1496
|
+
}
|
|
1497
|
+
export {
|
|
1498
|
+
Root
|
|
1499
|
+
};
|