ai-usage-analyzer 0.3.2 → 0.5.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 +30 -15
- package/bin/ai-usage.js +66 -35
- package/package.json +8 -6
- package/src/aggregate.js +48 -21
- package/src/live.js +132 -0
- package/src/loaders.js +259 -114
- package/src/markdown.js +133 -70
- package/src/pricing.js +49 -0
- package/src/quota.js +194 -0
- package/src/render.js +512 -189
- package/src/server.js +67 -0
- package/src/tools.js +5 -5
- package/src/tui.js +326 -0
- package/web/app.js +952 -0
- package/web/favicon.svg +1 -0
- package/web/fonts/OFL.txt +93 -0
- package/web/fonts/manrope.woff2 +0 -0
- package/web/icons/claude.svg +1 -0
- package/web/icons/openai.svg +1 -0
- package/web/icons/opencode.svg +1 -0
- package/web/index.html +199 -0
- package/web/style.css +324 -0
package/src/render.js
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// TUI renderer using chalk + cli-table3 + boxen + gradient-string.
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import gradient from
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
3
|
+
import boxen from "boxen";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import Table from "cli-table3";
|
|
6
|
+
import gradient from "gradient-string";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
MONTH_NAMES,
|
|
13
|
+
overall,
|
|
14
|
+
perMonth,
|
|
15
|
+
perProject,
|
|
16
|
+
perTool,
|
|
17
|
+
perToolPerMonth,
|
|
18
|
+
perWeek,
|
|
19
|
+
tokenBreakdown,
|
|
20
|
+
topSessions,
|
|
21
|
+
} from "./aggregate.js";
|
|
22
|
+
import { getToolColor } from "./tools.js";
|
|
16
23
|
|
|
17
24
|
const VERSION = JSON.parse(
|
|
18
|
-
readFileSync(
|
|
25
|
+
readFileSync(
|
|
26
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
27
|
+
"utf8",
|
|
28
|
+
),
|
|
19
29
|
).version;
|
|
20
30
|
|
|
21
31
|
// ---------------------------------------------------------------------------
|
|
@@ -34,8 +44,8 @@ function detectWidth() {
|
|
|
34
44
|
}
|
|
35
45
|
// 3. tput cols (POSIX)
|
|
36
46
|
try {
|
|
37
|
-
const { execSync } = require(
|
|
38
|
-
const out = execSync(
|
|
47
|
+
const { execSync } = require("node:child_process");
|
|
48
|
+
const out = execSync("tput cols 2>/dev/null", { encoding: "utf8" });
|
|
39
49
|
const n = parseInt(out.trim(), 10);
|
|
40
50
|
if (Number.isFinite(n) && n > 0) return n;
|
|
41
51
|
} catch {}
|
|
@@ -51,39 +61,54 @@ export const NARROW = TERM_WIDTH < 110;
|
|
|
51
61
|
// ---------------------------------------------------------------------------
|
|
52
62
|
|
|
53
63
|
export function fmtInt(n) {
|
|
54
|
-
return Number(n).toLocaleString(
|
|
64
|
+
return Number(n).toLocaleString("en-US");
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
export function fmtCompact(n) {
|
|
58
68
|
const a = Math.abs(n);
|
|
59
|
-
if (a >= 1e9) return (n / 1e9).toFixed(2) +
|
|
60
|
-
if (a >= 1e6) return (n / 1e6).toFixed(2) +
|
|
61
|
-
if (a >= 1e3) return (n / 1e3).toFixed(1) +
|
|
69
|
+
if (a >= 1e9) return (n / 1e9).toFixed(2) + "B";
|
|
70
|
+
if (a >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
71
|
+
if (a >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
62
72
|
return String(n);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
export function fmtCost(n) {
|
|
66
|
-
if (
|
|
67
|
-
|
|
75
|
+
export function fmtCost(n, status) {
|
|
76
|
+
if (status === "unavailable") return "n/a";
|
|
77
|
+
if (!n) return "—";
|
|
78
|
+
return (status === "estimated" || status === "partial" ? "~" : "") + "$" + Number(n).toFixed(2) + (status === "partial" ? "*" : "");
|
|
79
|
+
}
|
|
80
|
+
function fmtDuration(ms) {
|
|
81
|
+
if (!ms || ms <= 0) return chalk.dim("—");
|
|
82
|
+
const s = Math.round(ms / 1000);
|
|
83
|
+
if (s < 60) return `${s}s`;
|
|
84
|
+
const m = Math.floor(s / 60);
|
|
85
|
+
const rem = s % 60;
|
|
86
|
+
if (m < 60) return rem > 0 ? `${m}m${rem}s` : `${m}m`;
|
|
87
|
+
const h = Math.floor(m / 60);
|
|
88
|
+
const rm = m % 60;
|
|
89
|
+
return rm > 0 ? `${h}h${rm}m` : `${h}h`;
|
|
68
90
|
}
|
|
69
|
-
|
|
70
91
|
// ---------------------------------------------------------------------------
|
|
71
92
|
// Color helpers
|
|
72
93
|
// ---------------------------------------------------------------------------
|
|
73
94
|
|
|
74
|
-
export function toolColor(t) {
|
|
75
|
-
|
|
95
|
+
export function toolColor(t) {
|
|
96
|
+
return getToolColor(t);
|
|
97
|
+
}
|
|
98
|
+
export function colorize(t, c) {
|
|
99
|
+
return chalk.hex(c)(t);
|
|
100
|
+
}
|
|
76
101
|
|
|
77
102
|
// Cool-to-warm gradient: small bars feel subtle, large bars pop.
|
|
78
|
-
const BAR_GRADIENT = gradient([
|
|
103
|
+
const BAR_GRADIENT = gradient(["#3b82f6", "#14b8a6", "#f1fa8c", "#ff9e64"]);
|
|
79
104
|
|
|
80
105
|
function bar(value, max, width) {
|
|
81
|
-
if (max <= 0) return
|
|
106
|
+
if (max <= 0) return " ".repeat(width);
|
|
82
107
|
const pct = Math.max(0, Math.min(1, value / max));
|
|
83
108
|
const filled = Math.round(pct * width);
|
|
84
109
|
const empty = width - filled;
|
|
85
|
-
if (filled === 0) return chalk.gray(
|
|
86
|
-
return BAR_GRADIENT(
|
|
110
|
+
if (filled === 0) return chalk.gray("░".repeat(width));
|
|
111
|
+
return BAR_GRADIENT("█".repeat(filled)) + chalk.gray("░".repeat(empty));
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
// Stacked bar — one segment per tool, widths proportional to that tool's
|
|
@@ -92,73 +117,87 @@ function bar(value, max, width) {
|
|
|
92
117
|
// colored segment; no change in feel.
|
|
93
118
|
function stackedBar(byTool, width) {
|
|
94
119
|
const total = Object.values(byTool).reduce((a, b) => a + b, 0);
|
|
95
|
-
if (total <= 0 || width <= 0) return
|
|
120
|
+
if (total <= 0 || width <= 0) return " ".repeat(Math.max(0, width));
|
|
96
121
|
const entries = Object.entries(byTool)
|
|
97
122
|
.filter(([, v]) => v > 0)
|
|
98
123
|
.sort((a, b) => b[1] - a[1]);
|
|
99
|
-
let out =
|
|
124
|
+
let out = "";
|
|
100
125
|
let x = 0;
|
|
101
126
|
for (let i = 0; i < entries.length; i++) {
|
|
102
127
|
const [tool, value] = entries[i];
|
|
103
128
|
// Last segment fills the remaining width so rounding doesn't leave
|
|
104
129
|
// a visible gap or overshoot.
|
|
105
|
-
const segW =
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
const segW =
|
|
131
|
+
i === entries.length - 1
|
|
132
|
+
? width - x
|
|
133
|
+
: Math.round((value / total) * width);
|
|
108
134
|
if (segW <= 0) continue;
|
|
109
|
-
out += chalk.hex(toolColor(tool))(
|
|
135
|
+
out += chalk.hex(toolColor(tool))("█".repeat(segW));
|
|
110
136
|
x += segW;
|
|
111
137
|
}
|
|
112
138
|
return out;
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
function shortModel(m) {
|
|
116
|
-
if (!m) return
|
|
117
|
-
if (typeof m ===
|
|
142
|
+
if (!m) return "—";
|
|
143
|
+
if (typeof m === "string" && m.trim().startsWith("{")) {
|
|
118
144
|
try {
|
|
119
145
|
const d = JSON.parse(m);
|
|
120
146
|
const id = d.id || d.model || m;
|
|
121
147
|
const prov = d.providerId || d.provider;
|
|
122
|
-
return id + (prov ? ` (${prov})` :
|
|
123
|
-
} catch {
|
|
148
|
+
return id + (prov ? ` (${prov})` : "");
|
|
149
|
+
} catch {
|
|
150
|
+
return m.slice(0, 30);
|
|
151
|
+
}
|
|
124
152
|
}
|
|
125
153
|
return m;
|
|
126
154
|
}
|
|
127
155
|
|
|
128
156
|
function truncMiddle(s, w) {
|
|
129
|
-
if (!s) return
|
|
157
|
+
if (!s) return "";
|
|
130
158
|
if (s.length <= w) return s;
|
|
131
159
|
if (w <= 1) return s.slice(0, w);
|
|
132
160
|
const head = Math.ceil((w - 1) / 2);
|
|
133
161
|
const tail = Math.floor((w - 1) / 2);
|
|
134
|
-
return s.slice(0, head) +
|
|
162
|
+
return s.slice(0, head) + "…" + s.slice(s.length - tail);
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
function truncEnd(s, w) {
|
|
138
|
-
if (!s) return
|
|
166
|
+
if (!s) return "";
|
|
139
167
|
if (s.length <= w) return s;
|
|
140
|
-
return s.slice(0, w - 1) +
|
|
168
|
+
return s.slice(0, w - 1) + "…";
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
// ---------------------------------------------------------------------------
|
|
144
172
|
// Header
|
|
145
173
|
// ---------------------------------------------------------------------------
|
|
146
174
|
|
|
147
|
-
export function renderHeader({
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
export function renderHeader({
|
|
176
|
+
totalSessions,
|
|
177
|
+
totalTokens,
|
|
178
|
+
totalCost,
|
|
179
|
+
costStatus,
|
|
180
|
+
dateRange,
|
|
181
|
+
}) {
|
|
182
|
+
const title = gradient.pastel.multiline("◆ AI TOKEN ANALYZER ◆");
|
|
183
|
+
const sub =
|
|
184
|
+
chalk.dim(`${totalSessions} sessions`) +
|
|
185
|
+
" • " +
|
|
186
|
+
chalk.bold.cyan(`${fmtCompact(totalTokens)} total tokens`) +
|
|
187
|
+
(totalCost
|
|
188
|
+
? " • " + chalk.bold.yellow(`${fmtCost(totalCost, costStatus)} USD`)
|
|
189
|
+
: "");
|
|
190
|
+
const range =
|
|
191
|
+
dateRange[0] && dateRange[1]
|
|
192
|
+
? chalk.dim.italic(`\n range: ${dateRange[0]} → ${dateRange[1]}`)
|
|
193
|
+
: "";
|
|
155
194
|
return boxen(`${title}\n\n${sub}${range}`, {
|
|
156
195
|
title: chalk.dim(`v${VERSION}`),
|
|
157
|
-
titleAlignment:
|
|
158
|
-
borderStyle:
|
|
159
|
-
borderColor:
|
|
196
|
+
titleAlignment: "right",
|
|
197
|
+
borderStyle: "round",
|
|
198
|
+
borderColor: "magenta",
|
|
160
199
|
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
161
|
-
align:
|
|
200
|
+
align: "center",
|
|
162
201
|
});
|
|
163
202
|
}
|
|
164
203
|
|
|
@@ -170,38 +209,71 @@ export function renderDetections(detections) {
|
|
|
170
209
|
// For narrow terminals, simplify to Tool + Status + Count + Tokens (drop Path)
|
|
171
210
|
if (NARROW) {
|
|
172
211
|
const t = new Table({
|
|
173
|
-
head: [
|
|
212
|
+
head: [
|
|
213
|
+
chalk.bold("Tool"),
|
|
214
|
+
chalk.bold("Status"),
|
|
215
|
+
chalk.bold("Count"),
|
|
216
|
+
chalk.bold("Tokens"),
|
|
217
|
+
],
|
|
174
218
|
style: { head: [], border: [] },
|
|
175
219
|
});
|
|
176
220
|
for (const d of detections) {
|
|
177
|
-
const status =
|
|
178
|
-
|
|
179
|
-
|
|
221
|
+
const status =
|
|
222
|
+
d.status === "present"
|
|
223
|
+
? chalk.green("● present")
|
|
224
|
+
: chalk.red("○ absent");
|
|
225
|
+
const count = d.count ? fmtInt(d.count) : chalk.dim("—");
|
|
226
|
+
const tok = d.hasTokens
|
|
227
|
+
? d.count
|
|
228
|
+
? chalk.cyan("yes")
|
|
229
|
+
: chalk.dim("—")
|
|
230
|
+
: chalk.dim("n/a");
|
|
180
231
|
t.push([colorize(d.name, toolColor(d.key)), status, count, tok]);
|
|
181
232
|
}
|
|
182
|
-
return boxen(
|
|
183
|
-
|
|
184
|
-
|
|
233
|
+
return boxen(
|
|
234
|
+
t.toString() +
|
|
235
|
+
"\n" +
|
|
236
|
+
chalk.dim(
|
|
237
|
+
"(paths hidden in narrow mode — use $AI_USAGE_PATHS_JSON to inspect)",
|
|
238
|
+
),
|
|
239
|
+
{
|
|
240
|
+
title: chalk.bold("AI Tools Detected"),
|
|
241
|
+
borderStyle: "round",
|
|
242
|
+
borderColor: "cyan",
|
|
243
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
244
|
+
},
|
|
245
|
+
);
|
|
185
246
|
}
|
|
186
247
|
|
|
187
248
|
const t = new Table({
|
|
188
|
-
head: [
|
|
249
|
+
head: [
|
|
250
|
+
chalk.bold("Tool"),
|
|
251
|
+
chalk.bold("Status"),
|
|
252
|
+
chalk.bold("Path"),
|
|
253
|
+
chalk.bold("Count"),
|
|
254
|
+
chalk.bold("Tokens"),
|
|
255
|
+
],
|
|
189
256
|
style: { head: [], border: [] },
|
|
190
257
|
});
|
|
191
258
|
|
|
192
259
|
for (const d of detections) {
|
|
193
|
-
const status =
|
|
194
|
-
|
|
195
|
-
const
|
|
260
|
+
const status =
|
|
261
|
+
d.status === "present" ? chalk.green("● present") : chalk.red("○ absent");
|
|
262
|
+
const path = d.path
|
|
263
|
+
? truncEnd(d.path.replace(process.env.HOME || "", "~"), 50)
|
|
264
|
+
: chalk.dim("—");
|
|
265
|
+
const count = d.count ? fmtInt(d.count) : chalk.dim("—");
|
|
196
266
|
const tok = d.hasTokens
|
|
197
|
-
?
|
|
198
|
-
|
|
267
|
+
? d.count
|
|
268
|
+
? chalk.cyan("yes")
|
|
269
|
+
: chalk.dim("—")
|
|
270
|
+
: chalk.dim("n/a");
|
|
199
271
|
t.push([colorize(d.name, toolColor(d.key)), status, path, count, tok]);
|
|
200
272
|
}
|
|
201
273
|
return boxen(t.toString(), {
|
|
202
|
-
title: chalk.bold(
|
|
203
|
-
borderStyle:
|
|
204
|
-
borderColor:
|
|
274
|
+
title: chalk.bold("AI Tools Detected"),
|
|
275
|
+
borderStyle: "round",
|
|
276
|
+
borderColor: "cyan",
|
|
205
277
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
206
278
|
});
|
|
207
279
|
}
|
|
@@ -216,23 +288,49 @@ export function renderOverview(records, detections) {
|
|
|
216
288
|
|
|
217
289
|
// Token breakdown sub-table
|
|
218
290
|
const bt = new Table({
|
|
219
|
-
style: { border: [
|
|
291
|
+
style: { border: ["gray"] },
|
|
220
292
|
colWidths: [14, 12, 12],
|
|
221
293
|
});
|
|
222
294
|
bt.push(
|
|
223
|
-
[chalk.bold(
|
|
224
|
-
[
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
[
|
|
295
|
+
[chalk.bold("Type"), chalk.bold("Tokens"), chalk.bold("Share")],
|
|
296
|
+
[
|
|
297
|
+
chalk.cyan("Input"),
|
|
298
|
+
fmtCompact(breakdown.input),
|
|
299
|
+
pct(breakdown.ratios.input),
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
chalk.green("Output"),
|
|
303
|
+
fmtCompact(breakdown.output),
|
|
304
|
+
pct(breakdown.ratios.output),
|
|
305
|
+
],
|
|
306
|
+
[
|
|
307
|
+
chalk.cyan("Cache Read"),
|
|
308
|
+
fmtCompact(breakdown.cacheRead),
|
|
309
|
+
pct(breakdown.ratios.cacheRead),
|
|
310
|
+
],
|
|
311
|
+
[
|
|
312
|
+
chalk.cyan("Cache Write"),
|
|
313
|
+
fmtCompact(breakdown.cacheWrite),
|
|
314
|
+
pct(breakdown.ratios.cacheWrite),
|
|
315
|
+
],
|
|
316
|
+
[
|
|
317
|
+
chalk.yellow("Reasoning"),
|
|
318
|
+
fmtCompact(breakdown.reasoning),
|
|
319
|
+
pct(breakdown.ratios.reasoning),
|
|
320
|
+
],
|
|
321
|
+
[chalk.bold("Total"), chalk.bold(fmtCompact(breakdown.total)), "100.0%"],
|
|
230
322
|
);
|
|
231
323
|
|
|
232
324
|
// Per-tool mini summary
|
|
233
325
|
const pt = new Table({
|
|
234
|
-
head: [
|
|
235
|
-
|
|
326
|
+
head: [
|
|
327
|
+
chalk.bold("Tool"),
|
|
328
|
+
chalk.bold("n"),
|
|
329
|
+
chalk.bold("Total"),
|
|
330
|
+
chalk.bold("Avg/sess"),
|
|
331
|
+
chalk.bold("Cost"),
|
|
332
|
+
],
|
|
333
|
+
style: { head: [], border: ["gray"] },
|
|
236
334
|
colWidths: [14, 5, 10, 11, 9],
|
|
237
335
|
});
|
|
238
336
|
const byTool = perTool(records);
|
|
@@ -242,49 +340,76 @@ export function renderOverview(records, detections) {
|
|
|
242
340
|
fmtInt(p.n),
|
|
243
341
|
fmtCompact(p.tokensTotal),
|
|
244
342
|
fmtCompact(p.avg),
|
|
245
|
-
fmtCost(p.cost),
|
|
343
|
+
fmtCost(p.cost, p.costStatus),
|
|
246
344
|
]);
|
|
247
345
|
}
|
|
248
346
|
pt.push([
|
|
249
|
-
chalk.bold(
|
|
347
|
+
chalk.bold("TOTAL"),
|
|
250
348
|
chalk.bold(fmtInt(records.length)),
|
|
251
349
|
chalk.bold(fmtCompact(t.tokensTotal)),
|
|
252
350
|
chalk.bold(fmtCompact(t.avg)),
|
|
253
|
-
chalk.bold(fmtCost(t.cost)),
|
|
351
|
+
chalk.bold(fmtCost(t.cost, t.costStatus)),
|
|
254
352
|
]);
|
|
255
353
|
|
|
256
|
-
const body =
|
|
354
|
+
const body =
|
|
355
|
+
pt.toString() +
|
|
356
|
+
"\n\n" +
|
|
357
|
+
chalk.bold.underline("Token Breakdown") +
|
|
358
|
+
"\n" +
|
|
359
|
+
bt.toString() +
|
|
360
|
+
"\n\n" + chalk.dim("~ estimated API cost; * partial; n/a unavailable") +
|
|
361
|
+
(t.unknownModels.length ? "\n" + chalk.dim(`Unpriced models: ${t.unknownModels.map(m => String(m).replace(/[\x00-\x1f\x7f-\x9f]/g, "")).join(", ")}`) : "");
|
|
257
362
|
return boxen(body, {
|
|
258
|
-
title: chalk.bold(
|
|
259
|
-
borderStyle:
|
|
260
|
-
borderColor:
|
|
363
|
+
title: chalk.bold("Overview"),
|
|
364
|
+
borderStyle: "round",
|
|
365
|
+
borderColor: "blue",
|
|
261
366
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
262
367
|
});
|
|
263
368
|
}
|
|
264
369
|
|
|
265
|
-
function pct(x) {
|
|
370
|
+
function pct(x) {
|
|
371
|
+
return (x * 100).toFixed(1) + "%";
|
|
372
|
+
}
|
|
266
373
|
|
|
267
374
|
// ---------------------------------------------------------------------------
|
|
268
375
|
// Per Project
|
|
269
376
|
// ---------------------------------------------------------------------------
|
|
270
377
|
|
|
378
|
+
function fmtNByTool(nByTool) {
|
|
379
|
+
return Object.entries(nByTool)
|
|
380
|
+
.sort((a, b) => b[1] - a[1])
|
|
381
|
+
.map(([tool, count]) => chalk.hex(toolColor(tool))(`${tool}×${count}`))
|
|
382
|
+
.join(chalk.dim(" "));
|
|
383
|
+
}
|
|
384
|
+
|
|
271
385
|
export function renderPerProject(records) {
|
|
272
386
|
const items = perProject(records);
|
|
273
|
-
if (items.length === 0) return
|
|
387
|
+
if (items.length === 0) return "";
|
|
274
388
|
const barW = NARROW ? 10 : 18;
|
|
275
389
|
|
|
276
390
|
// Narrow mode: drop In/Out/Cache columns, just show n + Total + Dist
|
|
277
391
|
const head = NARROW
|
|
278
|
-
? [
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
392
|
+
? [
|
|
393
|
+
chalk.bold("Project"),
|
|
394
|
+
chalk.bold("n"),
|
|
395
|
+
chalk.bold("Total"),
|
|
396
|
+
chalk.bold("Dist"),
|
|
397
|
+
]
|
|
398
|
+
: [
|
|
399
|
+
chalk.bold("Project"),
|
|
400
|
+
chalk.bold("Sessions by tool"),
|
|
401
|
+
chalk.bold("In"),
|
|
402
|
+
chalk.bold("Out"),
|
|
403
|
+
chalk.bold("Cache"),
|
|
404
|
+
chalk.bold("Total"),
|
|
405
|
+
chalk.bold("Dist"),
|
|
406
|
+
];
|
|
283
407
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
284
408
|
|
|
285
409
|
for (const p of items) {
|
|
286
410
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
287
|
-
const
|
|
411
|
+
const sessionCell = NARROW ? fmtInt(p.n) : fmtNByTool(p.nByTool);
|
|
412
|
+
const row = [truncEnd(p.project, NARROW ? 28 : 40), sessionCell];
|
|
288
413
|
if (!NARROW) {
|
|
289
414
|
row.push(
|
|
290
415
|
fmtCompact(p.tokensInput),
|
|
@@ -296,9 +421,141 @@ export function renderPerProject(records) {
|
|
|
296
421
|
t.push(row);
|
|
297
422
|
}
|
|
298
423
|
return boxen(t.toString(), {
|
|
299
|
-
title: chalk.bold(
|
|
300
|
-
borderStyle:
|
|
301
|
-
borderColor:
|
|
424
|
+
title: chalk.bold("Per Project"),
|
|
425
|
+
borderStyle: "round",
|
|
426
|
+
borderColor: "cyan",
|
|
427
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Cursor-aware variant used by the interactive TUI.
|
|
432
|
+
// The selected row is highlighted; a zero-width marker lets tui.js locate it.
|
|
433
|
+
const CURSOR_MARK = "\u2060";
|
|
434
|
+
export { CURSOR_MARK };
|
|
435
|
+
|
|
436
|
+
export function renderPerProjectCursor(records, cursorIdx) {
|
|
437
|
+
const items = perProject(records);
|
|
438
|
+
if (items.length === 0) return "";
|
|
439
|
+
const barW = NARROW ? 10 : 18;
|
|
440
|
+
|
|
441
|
+
const head = NARROW
|
|
442
|
+
? [
|
|
443
|
+
chalk.bold("Project"),
|
|
444
|
+
chalk.bold("n"),
|
|
445
|
+
chalk.bold("Total"),
|
|
446
|
+
chalk.bold("Dist"),
|
|
447
|
+
]
|
|
448
|
+
: [
|
|
449
|
+
chalk.bold("Project"),
|
|
450
|
+
chalk.bold("Sessions by tool"),
|
|
451
|
+
chalk.bold("In"),
|
|
452
|
+
chalk.bold("Out"),
|
|
453
|
+
chalk.bold("Cache"),
|
|
454
|
+
chalk.bold("Total"),
|
|
455
|
+
chalk.bold("Dist"),
|
|
456
|
+
];
|
|
457
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
458
|
+
|
|
459
|
+
items.forEach((p, i) => {
|
|
460
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
461
|
+
const sessionCell = NARROW ? fmtInt(p.n) : fmtNByTool(p.nByTool);
|
|
462
|
+
const hl = i === cursorIdx;
|
|
463
|
+
const hi = (s) => (hl ? chalk.bgWhite.black(s) : s);
|
|
464
|
+
const projLabel =
|
|
465
|
+
(hl ? CURSOR_MARK : "") + truncEnd(p.project, NARROW ? 28 : 40);
|
|
466
|
+
const row = [
|
|
467
|
+
hl ? chalk.bgWhite.black(projLabel) : projLabel,
|
|
468
|
+
hi(sessionCell),
|
|
469
|
+
];
|
|
470
|
+
if (!NARROW)
|
|
471
|
+
row.push(
|
|
472
|
+
hi(fmtCompact(p.tokensInput)),
|
|
473
|
+
hi(fmtCompact(p.tokensOutput)),
|
|
474
|
+
hi(fmtCompact(cache)),
|
|
475
|
+
);
|
|
476
|
+
row.push(
|
|
477
|
+
hi(fmtCompact(p.tokensTotal)),
|
|
478
|
+
hl ? hi(stackedBar(p.byTool, barW)) : stackedBar(p.byTool, barW),
|
|
479
|
+
);
|
|
480
|
+
t.push(row);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return boxen(t.toString(), {
|
|
484
|
+
title:
|
|
485
|
+
chalk.bold("Per Project") + chalk.dim(" ↑/↓ navigate · Enter select"),
|
|
486
|
+
borderStyle: "round",
|
|
487
|
+
borderColor: "cyan",
|
|
488
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function renderProjectSessions(records, projectName) {
|
|
493
|
+
const sessions = records
|
|
494
|
+
.filter((r) => r.project === projectName)
|
|
495
|
+
.sort((a, b) => b.ts - a.ts);
|
|
496
|
+
|
|
497
|
+
if (sessions.length === 0) return chalk.dim(" (no sessions)");
|
|
498
|
+
|
|
499
|
+
const head = NARROW
|
|
500
|
+
? [
|
|
501
|
+
chalk.bold("#"),
|
|
502
|
+
chalk.bold("Tool"),
|
|
503
|
+
chalk.bold("Dur"),
|
|
504
|
+
chalk.bold("Total"),
|
|
505
|
+
chalk.bold("Cost"),
|
|
506
|
+
chalk.bold("Title"),
|
|
507
|
+
]
|
|
508
|
+
: [
|
|
509
|
+
chalk.bold("#"),
|
|
510
|
+
chalk.bold("Date"),
|
|
511
|
+
chalk.bold("Tool"),
|
|
512
|
+
chalk.bold("Duration"),
|
|
513
|
+
chalk.bold("Total"),
|
|
514
|
+
chalk.bold("In"),
|
|
515
|
+
chalk.bold("Out"),
|
|
516
|
+
chalk.bold("Cache"),
|
|
517
|
+
chalk.bold("Cost"),
|
|
518
|
+
chalk.bold("Title"),
|
|
519
|
+
];
|
|
520
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
521
|
+
|
|
522
|
+
sessions.forEach((r, i) => {
|
|
523
|
+
const date = r.ts ? new Date(r.ts).toISOString().slice(0, 10) : "—";
|
|
524
|
+
const cache = (r.tokensCacheRead || 0) + (r.tokensCacheWrite || 0);
|
|
525
|
+
const dur = fmtDuration(r.durationMs);
|
|
526
|
+
const titleStr = truncEnd(r.title || chalk.dim("—"), NARROW ? 28 : 42);
|
|
527
|
+
if (NARROW) {
|
|
528
|
+
t.push([
|
|
529
|
+
String(i + 1),
|
|
530
|
+
colorize(r.tool, toolColor(r.tool)),
|
|
531
|
+
dur,
|
|
532
|
+
fmtCompact(r.tokensTotal),
|
|
533
|
+
fmtCost(r.cost, r.costStatus),
|
|
534
|
+
titleStr,
|
|
535
|
+
]);
|
|
536
|
+
} else {
|
|
537
|
+
t.push([
|
|
538
|
+
String(i + 1),
|
|
539
|
+
date,
|
|
540
|
+
colorize(r.tool, toolColor(r.tool)),
|
|
541
|
+
dur,
|
|
542
|
+
chalk.bold(fmtCompact(r.tokensTotal)),
|
|
543
|
+
fmtCompact(r.tokensInput),
|
|
544
|
+
fmtCompact(r.tokensOutput),
|
|
545
|
+
fmtCompact(cache),
|
|
546
|
+
fmtCost(r.cost, r.costStatus),
|
|
547
|
+
titleStr,
|
|
548
|
+
]);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return boxen(t.toString(), {
|
|
553
|
+
title:
|
|
554
|
+
chalk.bold("Sessions ") +
|
|
555
|
+
chalk.cyan(truncEnd(projectName, 60)) +
|
|
556
|
+
chalk.dim(" Esc back"),
|
|
557
|
+
borderStyle: "round",
|
|
558
|
+
borderColor: "yellow",
|
|
302
559
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
303
560
|
});
|
|
304
561
|
}
|
|
@@ -309,24 +566,35 @@ export function renderPerProject(records) {
|
|
|
309
566
|
|
|
310
567
|
export function renderPerTool(records) {
|
|
311
568
|
const items = perTool(records);
|
|
312
|
-
if (items.length === 0) return
|
|
569
|
+
if (items.length === 0) return "";
|
|
313
570
|
const max = items[0].tokensTotal;
|
|
314
571
|
const barW = NARROW ? 10 : 18;
|
|
315
572
|
|
|
316
573
|
const head = NARROW
|
|
317
|
-
? [
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
574
|
+
? [
|
|
575
|
+
chalk.bold("Tool"),
|
|
576
|
+
chalk.bold("n"),
|
|
577
|
+
chalk.bold("Total"),
|
|
578
|
+
chalk.bold("Cost"),
|
|
579
|
+
chalk.bold("Avg/sess"),
|
|
580
|
+
chalk.bold("Dist"),
|
|
581
|
+
]
|
|
582
|
+
: [
|
|
583
|
+
chalk.bold("Tool"),
|
|
584
|
+
chalk.bold("n"),
|
|
585
|
+
chalk.bold("Input"),
|
|
586
|
+
chalk.bold("Output"),
|
|
587
|
+
chalk.bold("Cache"),
|
|
588
|
+
chalk.bold("Total"),
|
|
589
|
+
chalk.bold("Cost"),
|
|
590
|
+
chalk.bold("Avg/sess"),
|
|
591
|
+
chalk.bold("Dist"),
|
|
592
|
+
];
|
|
322
593
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
323
594
|
|
|
324
595
|
for (const p of items) {
|
|
325
596
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
326
|
-
const row = [
|
|
327
|
-
colorize(p.tool, toolColor(p.tool)),
|
|
328
|
-
fmtInt(p.n),
|
|
329
|
-
];
|
|
597
|
+
const row = [colorize(p.tool, toolColor(p.tool)), fmtInt(p.n)];
|
|
330
598
|
if (!NARROW) {
|
|
331
599
|
row.push(
|
|
332
600
|
fmtCompact(p.tokensInput),
|
|
@@ -336,16 +604,16 @@ export function renderPerTool(records) {
|
|
|
336
604
|
}
|
|
337
605
|
row.push(
|
|
338
606
|
fmtCompact(p.tokensTotal),
|
|
339
|
-
fmtCost(p.cost),
|
|
607
|
+
fmtCost(p.cost, p.costStatus),
|
|
340
608
|
fmtCompact(p.avg),
|
|
341
609
|
bar(p.tokensTotal, max, barW),
|
|
342
610
|
);
|
|
343
611
|
t.push(row);
|
|
344
612
|
}
|
|
345
613
|
return boxen(t.toString(), {
|
|
346
|
-
title: chalk.bold(
|
|
347
|
-
borderStyle:
|
|
348
|
-
borderColor:
|
|
614
|
+
title: chalk.bold("Per Tool"),
|
|
615
|
+
borderStyle: "round",
|
|
616
|
+
borderColor: "magenta",
|
|
349
617
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
350
618
|
});
|
|
351
619
|
}
|
|
@@ -356,16 +624,28 @@ export function renderPerTool(records) {
|
|
|
356
624
|
|
|
357
625
|
export function renderPerToolPerMonth(records) {
|
|
358
626
|
const items = perToolPerMonth(records);
|
|
359
|
-
if (items.length === 0) return
|
|
627
|
+
if (items.length === 0) return "";
|
|
360
628
|
const max = items[0].tokensTotal;
|
|
361
629
|
const barW = NARROW ? 8 : 16;
|
|
362
630
|
|
|
363
631
|
const head = NARROW
|
|
364
|
-
? [
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
632
|
+
? [
|
|
633
|
+
chalk.bold("Tool"),
|
|
634
|
+
chalk.bold("Month"),
|
|
635
|
+
chalk.bold("n"),
|
|
636
|
+
chalk.bold("Total"),
|
|
637
|
+
chalk.bold("Dist"),
|
|
638
|
+
]
|
|
639
|
+
: [
|
|
640
|
+
chalk.bold("Tool"),
|
|
641
|
+
chalk.bold("Month"),
|
|
642
|
+
chalk.bold("n"),
|
|
643
|
+
chalk.bold("Input"),
|
|
644
|
+
chalk.bold("Output"),
|
|
645
|
+
chalk.bold("Cache"),
|
|
646
|
+
chalk.bold("Total"),
|
|
647
|
+
chalk.bold("Dist"),
|
|
648
|
+
];
|
|
369
649
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
370
650
|
|
|
371
651
|
for (const p of items) {
|
|
@@ -385,16 +665,13 @@ export function renderPerToolPerMonth(records) {
|
|
|
385
665
|
fmtCompact(cache),
|
|
386
666
|
);
|
|
387
667
|
}
|
|
388
|
-
row.push(
|
|
389
|
-
fmtCompact(p.tokensTotal),
|
|
390
|
-
bar(p.tokensTotal, max, barW),
|
|
391
|
-
);
|
|
668
|
+
row.push(fmtCompact(p.tokensTotal), bar(p.tokensTotal, max, barW));
|
|
392
669
|
t.push(row);
|
|
393
670
|
}
|
|
394
671
|
return boxen(t.toString(), {
|
|
395
|
-
title: chalk.bold(
|
|
396
|
-
borderStyle:
|
|
397
|
-
borderColor:
|
|
672
|
+
title: chalk.bold("Per Tool per Month"),
|
|
673
|
+
borderStyle: "round",
|
|
674
|
+
borderColor: "cyan",
|
|
398
675
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
399
676
|
});
|
|
400
677
|
}
|
|
@@ -405,13 +682,16 @@ export function renderPerToolPerMonth(records) {
|
|
|
405
682
|
|
|
406
683
|
export function renderPerMonth(records) {
|
|
407
684
|
const items = perMonth(records);
|
|
408
|
-
if (items.length === 0) return
|
|
685
|
+
if (items.length === 0) return "";
|
|
409
686
|
const barW = NARROW ? 10 : 16;
|
|
410
687
|
|
|
411
688
|
const head = [
|
|
412
|
-
chalk.bold(
|
|
413
|
-
chalk.bold(
|
|
414
|
-
chalk.bold(
|
|
689
|
+
chalk.bold("Month"),
|
|
690
|
+
chalk.bold("n"),
|
|
691
|
+
chalk.bold("Input"),
|
|
692
|
+
chalk.bold("Output"),
|
|
693
|
+
chalk.bold("Total"),
|
|
694
|
+
chalk.bold("Dist"),
|
|
415
695
|
];
|
|
416
696
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
417
697
|
|
|
@@ -432,18 +712,18 @@ export function renderPerMonth(records) {
|
|
|
432
712
|
// TOTAL row
|
|
433
713
|
const tot = overall(records);
|
|
434
714
|
t.push([
|
|
435
|
-
chalk.bgGray.white.bold(
|
|
715
|
+
chalk.bgGray.white.bold(" TOTAL "),
|
|
436
716
|
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
437
717
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
438
718
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
439
719
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
440
|
-
|
|
720
|
+
"",
|
|
441
721
|
]);
|
|
442
722
|
|
|
443
723
|
return boxen(t.toString(), {
|
|
444
|
-
title: chalk.bold(
|
|
445
|
-
borderStyle:
|
|
446
|
-
borderColor:
|
|
724
|
+
title: chalk.bold("Per Month"),
|
|
725
|
+
borderStyle: "round",
|
|
726
|
+
borderColor: "green",
|
|
447
727
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
448
728
|
});
|
|
449
729
|
}
|
|
@@ -454,13 +734,16 @@ export function renderPerMonth(records) {
|
|
|
454
734
|
|
|
455
735
|
export function renderPerWeek(records) {
|
|
456
736
|
const items = perWeek(records);
|
|
457
|
-
if (items.length === 0) return
|
|
737
|
+
if (items.length === 0) return "";
|
|
458
738
|
const barW = NARROW ? 10 : 14;
|
|
459
739
|
|
|
460
740
|
const head = [
|
|
461
|
-
chalk.bold(
|
|
462
|
-
chalk.bold(
|
|
463
|
-
chalk.bold(
|
|
741
|
+
chalk.bold("ISO Week"),
|
|
742
|
+
chalk.bold("n"),
|
|
743
|
+
chalk.bold("Input"),
|
|
744
|
+
chalk.bold("Output"),
|
|
745
|
+
chalk.bold("Total"),
|
|
746
|
+
chalk.bold("Dist"),
|
|
464
747
|
];
|
|
465
748
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
466
749
|
|
|
@@ -477,18 +760,18 @@ export function renderPerWeek(records) {
|
|
|
477
760
|
// TOTAL row
|
|
478
761
|
const tot = overall(records);
|
|
479
762
|
t.push([
|
|
480
|
-
chalk.bgGray.white.bold(
|
|
763
|
+
chalk.bgGray.white.bold(" TOTAL "),
|
|
481
764
|
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
482
765
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
483
766
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
484
767
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
485
|
-
|
|
768
|
+
"",
|
|
486
769
|
]);
|
|
487
770
|
|
|
488
771
|
return boxen(t.toString(), {
|
|
489
|
-
title: chalk.bold(
|
|
490
|
-
borderStyle:
|
|
491
|
-
borderColor:
|
|
772
|
+
title: chalk.bold("Per Week"),
|
|
773
|
+
borderStyle: "round",
|
|
774
|
+
borderColor: "magenta",
|
|
492
775
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
493
776
|
});
|
|
494
777
|
}
|
|
@@ -499,12 +782,17 @@ export function renderPerWeek(records) {
|
|
|
499
782
|
|
|
500
783
|
export function renderTopSessions(records, n = 5) {
|
|
501
784
|
const top = topSessions(records, n);
|
|
502
|
-
if (top.length === 0) return
|
|
785
|
+
if (top.length === 0) return "";
|
|
503
786
|
|
|
504
787
|
if (NARROW) {
|
|
505
788
|
const t = new Table({
|
|
506
|
-
head: [
|
|
507
|
-
|
|
789
|
+
head: [
|
|
790
|
+
chalk.bold("#"),
|
|
791
|
+
chalk.bold("Total"),
|
|
792
|
+
chalk.bold("In/Out"),
|
|
793
|
+
chalk.bold("Cost"),
|
|
794
|
+
chalk.bold("Title"),
|
|
795
|
+
],
|
|
508
796
|
style: { head: [], border: [] },
|
|
509
797
|
});
|
|
510
798
|
top.forEach((r, i) => {
|
|
@@ -512,22 +800,31 @@ export function renderTopSessions(records, n = 5) {
|
|
|
512
800
|
chalk.bold(String(i + 1)),
|
|
513
801
|
chalk.bold.yellow(fmtCompact(r.tokensTotal)),
|
|
514
802
|
`${fmtCompact(r.tokensInput)}/${fmtCompact(r.tokensOutput)}`,
|
|
515
|
-
r.cost
|
|
516
|
-
truncEnd(r.title, 30) ||
|
|
803
|
+
fmtCost(r.cost, r.costStatus),
|
|
804
|
+
truncEnd(r.title, 30) || "—",
|
|
517
805
|
]);
|
|
518
806
|
});
|
|
519
|
-
return boxen(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
807
|
+
return boxen(
|
|
808
|
+
t.toString() + "\n" + chalk.dim("(full project + model in wide mode)"),
|
|
809
|
+
{
|
|
810
|
+
title: chalk.bold.yellow(`Top ${n} Heaviest Sessions`),
|
|
811
|
+
borderStyle: "round",
|
|
812
|
+
borderColor: "yellow",
|
|
813
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
814
|
+
},
|
|
815
|
+
);
|
|
523
816
|
}
|
|
524
817
|
|
|
525
818
|
const t = new Table({
|
|
526
819
|
head: [
|
|
527
|
-
chalk.bold(
|
|
528
|
-
chalk.bold(
|
|
529
|
-
chalk.bold(
|
|
530
|
-
chalk.bold(
|
|
820
|
+
chalk.bold("#"),
|
|
821
|
+
chalk.bold("Total"),
|
|
822
|
+
chalk.bold("Input"),
|
|
823
|
+
chalk.bold("Output"),
|
|
824
|
+
chalk.bold("Cost"),
|
|
825
|
+
chalk.bold("Model"),
|
|
826
|
+
chalk.bold("Project"),
|
|
827
|
+
chalk.bold("Title"),
|
|
531
828
|
],
|
|
532
829
|
style: { head: [], border: [] },
|
|
533
830
|
});
|
|
@@ -537,16 +834,16 @@ export function renderTopSessions(records, n = 5) {
|
|
|
537
834
|
chalk.bold.yellow(fmtCompact(r.tokensTotal)),
|
|
538
835
|
fmtCompact(r.tokensInput),
|
|
539
836
|
fmtCompact(r.tokensOutput),
|
|
540
|
-
r.cost
|
|
837
|
+
fmtCost(r.cost, r.costStatus),
|
|
541
838
|
shortModel(r.model),
|
|
542
839
|
truncEnd(r.project, 30),
|
|
543
|
-
truncEnd(r.title, 40) ||
|
|
840
|
+
truncEnd(r.title, 40) || "—",
|
|
544
841
|
]);
|
|
545
842
|
});
|
|
546
843
|
return boxen(t.toString(), {
|
|
547
844
|
title: chalk.bold.yellow(`Top ${n} Heaviest Sessions`),
|
|
548
|
-
borderStyle:
|
|
549
|
-
borderColor:
|
|
845
|
+
borderStyle: "round",
|
|
846
|
+
borderColor: "yellow",
|
|
550
847
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
551
848
|
});
|
|
552
849
|
}
|
|
@@ -557,53 +854,79 @@ export function renderTopSessions(records, n = 5) {
|
|
|
557
854
|
|
|
558
855
|
export function renderNotes(detections, errors) {
|
|
559
856
|
const lines = [];
|
|
560
|
-
lines.push(chalk.
|
|
561
|
-
lines.push(
|
|
562
|
-
lines.push(
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
lines.push(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
lines.push(
|
|
569
|
-
|
|
857
|
+
lines.push(chalk.dim("Costs: ~ estimated API rates; * partial, excludes unpriced models; n/a unavailable."));
|
|
858
|
+
lines.push(chalk.bold.underline("Token definitions"));
|
|
859
|
+
lines.push(
|
|
860
|
+
` • ${chalk.cyan("Input")} — prompt tokens (what was sent to the model)`,
|
|
861
|
+
);
|
|
862
|
+
lines.push(
|
|
863
|
+
` • ${chalk.green("Output")} — completion tokens (what the model generated)`,
|
|
864
|
+
);
|
|
865
|
+
lines.push(
|
|
866
|
+
` • ${chalk.cyan("Cache Read")} — prompt tokens served from provider cache (cheap)`,
|
|
867
|
+
);
|
|
868
|
+
lines.push(
|
|
869
|
+
` • ${chalk.cyan("Cache Write")} — prompt tokens cached for future use`,
|
|
870
|
+
);
|
|
871
|
+
lines.push(
|
|
872
|
+
` • ${chalk.yellow("Reasoning")} — extended thinking / chain-of-thought tokens`,
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
lines.push("");
|
|
876
|
+
lines.push(chalk.bold.underline("Detected tools with token data"));
|
|
877
|
+
const withTokens = detections.filter(
|
|
878
|
+
(d) => d.hasTokens && d.status === "present",
|
|
879
|
+
);
|
|
570
880
|
if (withTokens.length === 0) {
|
|
571
|
-
lines.push(
|
|
881
|
+
lines.push(" " + chalk.dim("(none — only presence info available)"));
|
|
572
882
|
} else {
|
|
573
883
|
for (const d of withTokens) {
|
|
574
|
-
lines.push(
|
|
884
|
+
lines.push(
|
|
885
|
+
` ${chalk.green("●")} ${colorize(d.name, toolColor(d.key))} ${chalk.dim(d.description)}`,
|
|
886
|
+
);
|
|
575
887
|
}
|
|
576
888
|
}
|
|
577
889
|
|
|
578
|
-
lines.push(
|
|
579
|
-
lines.push(
|
|
580
|
-
|
|
890
|
+
lines.push("");
|
|
891
|
+
lines.push(
|
|
892
|
+
chalk.bold.underline("Detected tools without token data (presence only)"),
|
|
893
|
+
);
|
|
894
|
+
const noTokens = detections.filter(
|
|
895
|
+
(d) => !d.hasTokens && d.status === "present",
|
|
896
|
+
);
|
|
581
897
|
if (noTokens.length === 0) {
|
|
582
|
-
lines.push(
|
|
898
|
+
lines.push(" " + chalk.dim("(none)"));
|
|
583
899
|
} else {
|
|
584
900
|
for (const d of noTokens) {
|
|
585
|
-
lines.push(
|
|
901
|
+
lines.push(
|
|
902
|
+
` ${chalk.green("●")} ${colorize(d.name, toolColor(d.key))} ${chalk.dim(d.description)}`,
|
|
903
|
+
);
|
|
586
904
|
}
|
|
587
905
|
}
|
|
588
906
|
|
|
589
|
-
lines.push(
|
|
590
|
-
lines.push(chalk.bold.underline(
|
|
591
|
-
lines.push(
|
|
592
|
-
|
|
593
|
-
|
|
907
|
+
lines.push("");
|
|
908
|
+
lines.push(chalk.bold.underline("Env overrides"));
|
|
909
|
+
lines.push(
|
|
910
|
+
" $CLAUDE_HOME $CODEX_HOME $OPENCODE_HOME $MIMOCODE_HOME",
|
|
911
|
+
);
|
|
912
|
+
lines.push(" $COPILOT_HOME $ANTIGRAVITY_HOME $GEMINI_HOME");
|
|
913
|
+
lines.push(
|
|
914
|
+
" $AI_USAGE_PATHS_JSON = " + chalk.italic('\'{"codex":"/custom/path"}\''),
|
|
915
|
+
);
|
|
594
916
|
|
|
595
917
|
if (errors && errors.length) {
|
|
596
|
-
lines.push(
|
|
918
|
+
lines.push("");
|
|
597
919
|
lines.push(chalk.bold.underline(chalk.red(`Errors (${errors.length})`)));
|
|
598
920
|
for (const e of errors.slice(0, 5)) {
|
|
599
|
-
lines.push(
|
|
921
|
+
lines.push(" " + chalk.red("! ") + e);
|
|
600
922
|
}
|
|
601
|
-
if (errors.length > 5)
|
|
923
|
+
if (errors.length > 5)
|
|
924
|
+
lines.push(" " + chalk.dim(`... and ${errors.length - 5} more`));
|
|
602
925
|
}
|
|
603
926
|
|
|
604
|
-
return boxen(lines.join(
|
|
605
|
-
borderStyle:
|
|
606
|
-
borderColor:
|
|
927
|
+
return boxen(lines.join("\n"), {
|
|
928
|
+
borderStyle: "round",
|
|
929
|
+
borderColor: "gray",
|
|
607
930
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
608
931
|
});
|
|
609
932
|
}
|