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