ai-usage-analyzer 0.3.1 → 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/README.md +9 -7
- 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 +507 -180
- package/src/tui.js +310 -0
package/src/render.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
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
|
|
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";
|
|
8
11
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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";
|
|
23
|
+
|
|
24
|
+
const VERSION = JSON.parse(
|
|
25
|
+
readFileSync(
|
|
26
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
27
|
+
"utf8",
|
|
28
|
+
),
|
|
29
|
+
).version;
|
|
13
30
|
|
|
14
31
|
// ---------------------------------------------------------------------------
|
|
15
32
|
// Terminal width detection
|
|
@@ -27,8 +44,8 @@ function detectWidth() {
|
|
|
27
44
|
}
|
|
28
45
|
// 3. tput cols (POSIX)
|
|
29
46
|
try {
|
|
30
|
-
const { execSync } = require(
|
|
31
|
-
const out = execSync(
|
|
47
|
+
const { execSync } = require("node:child_process");
|
|
48
|
+
const out = execSync("tput cols 2>/dev/null", { encoding: "utf8" });
|
|
32
49
|
const n = parseInt(out.trim(), 10);
|
|
33
50
|
if (Number.isFinite(n) && n > 0) return n;
|
|
34
51
|
} catch {}
|
|
@@ -44,39 +61,53 @@ export const NARROW = TERM_WIDTH < 110;
|
|
|
44
61
|
// ---------------------------------------------------------------------------
|
|
45
62
|
|
|
46
63
|
export function fmtInt(n) {
|
|
47
|
-
return Number(n).toLocaleString(
|
|
64
|
+
return Number(n).toLocaleString("en-US");
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
export function fmtCompact(n) {
|
|
51
68
|
const a = Math.abs(n);
|
|
52
|
-
if (a >= 1e9) return (n / 1e9).toFixed(2) +
|
|
53
|
-
if (a >= 1e6) return (n / 1e6).toFixed(2) +
|
|
54
|
-
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";
|
|
55
72
|
return String(n);
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
export function fmtCost(n) {
|
|
59
|
-
if (!n) return
|
|
60
|
-
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`;
|
|
61
89
|
}
|
|
62
|
-
|
|
63
90
|
// ---------------------------------------------------------------------------
|
|
64
91
|
// Color helpers
|
|
65
92
|
// ---------------------------------------------------------------------------
|
|
66
93
|
|
|
67
|
-
export function toolColor(t) {
|
|
68
|
-
|
|
94
|
+
export function toolColor(t) {
|
|
95
|
+
return getToolColor(t);
|
|
96
|
+
}
|
|
97
|
+
export function colorize(t, c) {
|
|
98
|
+
return chalk.hex(c)(t);
|
|
99
|
+
}
|
|
69
100
|
|
|
70
101
|
// Cool-to-warm gradient: small bars feel subtle, large bars pop.
|
|
71
|
-
const BAR_GRADIENT = gradient([
|
|
102
|
+
const BAR_GRADIENT = gradient(["#3b82f6", "#14b8a6", "#f1fa8c", "#ff9e64"]);
|
|
72
103
|
|
|
73
104
|
function bar(value, max, width) {
|
|
74
|
-
if (max <= 0) return
|
|
105
|
+
if (max <= 0) return " ".repeat(width);
|
|
75
106
|
const pct = Math.max(0, Math.min(1, value / max));
|
|
76
107
|
const filled = Math.round(pct * width);
|
|
77
108
|
const empty = width - filled;
|
|
78
|
-
if (filled === 0) return chalk.gray(
|
|
79
|
-
return BAR_GRADIENT(
|
|
109
|
+
if (filled === 0) return chalk.gray("░".repeat(width));
|
|
110
|
+
return BAR_GRADIENT("█".repeat(filled)) + chalk.gray("░".repeat(empty));
|
|
80
111
|
}
|
|
81
112
|
|
|
82
113
|
// Stacked bar — one segment per tool, widths proportional to that tool's
|
|
@@ -85,71 +116,86 @@ function bar(value, max, width) {
|
|
|
85
116
|
// colored segment; no change in feel.
|
|
86
117
|
function stackedBar(byTool, width) {
|
|
87
118
|
const total = Object.values(byTool).reduce((a, b) => a + b, 0);
|
|
88
|
-
if (total <= 0 || width <= 0) return
|
|
119
|
+
if (total <= 0 || width <= 0) return " ".repeat(Math.max(0, width));
|
|
89
120
|
const entries = Object.entries(byTool)
|
|
90
121
|
.filter(([, v]) => v > 0)
|
|
91
122
|
.sort((a, b) => b[1] - a[1]);
|
|
92
|
-
let out =
|
|
123
|
+
let out = "";
|
|
93
124
|
let x = 0;
|
|
94
125
|
for (let i = 0; i < entries.length; i++) {
|
|
95
126
|
const [tool, value] = entries[i];
|
|
96
127
|
// Last segment fills the remaining width so rounding doesn't leave
|
|
97
128
|
// a visible gap or overshoot.
|
|
98
|
-
const segW =
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
const segW =
|
|
130
|
+
i === entries.length - 1
|
|
131
|
+
? width - x
|
|
132
|
+
: Math.round((value / total) * width);
|
|
101
133
|
if (segW <= 0) continue;
|
|
102
|
-
out += chalk.hex(toolColor(tool))(
|
|
134
|
+
out += chalk.hex(toolColor(tool))("█".repeat(segW));
|
|
103
135
|
x += segW;
|
|
104
136
|
}
|
|
105
137
|
return out;
|
|
106
138
|
}
|
|
107
139
|
|
|
108
140
|
function shortModel(m) {
|
|
109
|
-
if (!m) return
|
|
110
|
-
if (typeof m ===
|
|
141
|
+
if (!m) return "—";
|
|
142
|
+
if (typeof m === "string" && m.trim().startsWith("{")) {
|
|
111
143
|
try {
|
|
112
144
|
const d = JSON.parse(m);
|
|
113
145
|
const id = d.id || d.model || m;
|
|
114
146
|
const prov = d.providerId || d.provider;
|
|
115
|
-
return id + (prov ? ` (${prov})` :
|
|
116
|
-
} catch {
|
|
147
|
+
return id + (prov ? ` (${prov})` : "");
|
|
148
|
+
} catch {
|
|
149
|
+
return m.slice(0, 30);
|
|
150
|
+
}
|
|
117
151
|
}
|
|
118
152
|
return m;
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
function truncMiddle(s, w) {
|
|
122
|
-
if (!s) return
|
|
156
|
+
if (!s) return "";
|
|
123
157
|
if (s.length <= w) return s;
|
|
124
158
|
if (w <= 1) return s.slice(0, w);
|
|
125
159
|
const head = Math.ceil((w - 1) / 2);
|
|
126
160
|
const tail = Math.floor((w - 1) / 2);
|
|
127
|
-
return s.slice(0, head) +
|
|
161
|
+
return s.slice(0, head) + "…" + s.slice(s.length - tail);
|
|
128
162
|
}
|
|
129
163
|
|
|
130
164
|
function truncEnd(s, w) {
|
|
131
|
-
if (!s) return
|
|
165
|
+
if (!s) return "";
|
|
132
166
|
if (s.length <= w) return s;
|
|
133
|
-
return s.slice(0, w - 1) +
|
|
167
|
+
return s.slice(0, w - 1) + "…";
|
|
134
168
|
}
|
|
135
169
|
|
|
136
170
|
// ---------------------------------------------------------------------------
|
|
137
171
|
// Header
|
|
138
172
|
// ---------------------------------------------------------------------------
|
|
139
173
|
|
|
140
|
-
export function renderHeader({
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
: "";
|
|
148
192
|
return boxen(`${title}\n\n${sub}${range}`, {
|
|
149
|
-
|
|
150
|
-
|
|
193
|
+
title: chalk.dim(`v${VERSION}`),
|
|
194
|
+
titleAlignment: "right",
|
|
195
|
+
borderStyle: "round",
|
|
196
|
+
borderColor: "magenta",
|
|
151
197
|
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
152
|
-
align:
|
|
198
|
+
align: "center",
|
|
153
199
|
});
|
|
154
200
|
}
|
|
155
201
|
|
|
@@ -161,38 +207,71 @@ export function renderDetections(detections) {
|
|
|
161
207
|
// For narrow terminals, simplify to Tool + Status + Count + Tokens (drop Path)
|
|
162
208
|
if (NARROW) {
|
|
163
209
|
const t = new Table({
|
|
164
|
-
head: [
|
|
210
|
+
head: [
|
|
211
|
+
chalk.bold("Tool"),
|
|
212
|
+
chalk.bold("Status"),
|
|
213
|
+
chalk.bold("Count"),
|
|
214
|
+
chalk.bold("Tokens"),
|
|
215
|
+
],
|
|
165
216
|
style: { head: [], border: [] },
|
|
166
217
|
});
|
|
167
218
|
for (const d of detections) {
|
|
168
|
-
const status =
|
|
169
|
-
|
|
170
|
-
|
|
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");
|
|
171
229
|
t.push([colorize(d.name, toolColor(d.key)), status, count, tok]);
|
|
172
230
|
}
|
|
173
|
-
return boxen(
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
);
|
|
176
244
|
}
|
|
177
245
|
|
|
178
246
|
const t = new Table({
|
|
179
|
-
head: [
|
|
247
|
+
head: [
|
|
248
|
+
chalk.bold("Tool"),
|
|
249
|
+
chalk.bold("Status"),
|
|
250
|
+
chalk.bold("Path"),
|
|
251
|
+
chalk.bold("Count"),
|
|
252
|
+
chalk.bold("Tokens"),
|
|
253
|
+
],
|
|
180
254
|
style: { head: [], border: [] },
|
|
181
255
|
});
|
|
182
256
|
|
|
183
257
|
for (const d of detections) {
|
|
184
|
-
const status =
|
|
185
|
-
|
|
186
|
-
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("—");
|
|
187
264
|
const tok = d.hasTokens
|
|
188
|
-
?
|
|
189
|
-
|
|
265
|
+
? d.count
|
|
266
|
+
? chalk.cyan("yes")
|
|
267
|
+
: chalk.dim("—")
|
|
268
|
+
: chalk.dim("n/a");
|
|
190
269
|
t.push([colorize(d.name, toolColor(d.key)), status, path, count, tok]);
|
|
191
270
|
}
|
|
192
271
|
return boxen(t.toString(), {
|
|
193
|
-
title: chalk.bold(
|
|
194
|
-
borderStyle:
|
|
195
|
-
borderColor:
|
|
272
|
+
title: chalk.bold("AI Tools Detected"),
|
|
273
|
+
borderStyle: "round",
|
|
274
|
+
borderColor: "cyan",
|
|
196
275
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
197
276
|
});
|
|
198
277
|
}
|
|
@@ -207,23 +286,49 @@ export function renderOverview(records, detections) {
|
|
|
207
286
|
|
|
208
287
|
// Token breakdown sub-table
|
|
209
288
|
const bt = new Table({
|
|
210
|
-
style: { border: [
|
|
289
|
+
style: { border: ["gray"] },
|
|
211
290
|
colWidths: [14, 12, 12],
|
|
212
291
|
});
|
|
213
292
|
bt.push(
|
|
214
|
-
[chalk.bold(
|
|
215
|
-
[
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
[
|
|
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%"],
|
|
221
320
|
);
|
|
222
321
|
|
|
223
322
|
// Per-tool mini summary
|
|
224
323
|
const pt = new Table({
|
|
225
|
-
head: [
|
|
226
|
-
|
|
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"] },
|
|
227
332
|
colWidths: [14, 5, 10, 11, 9],
|
|
228
333
|
});
|
|
229
334
|
const byTool = perTool(records);
|
|
@@ -237,45 +342,70 @@ export function renderOverview(records, detections) {
|
|
|
237
342
|
]);
|
|
238
343
|
}
|
|
239
344
|
pt.push([
|
|
240
|
-
chalk.bold(
|
|
345
|
+
chalk.bold("TOTAL"),
|
|
241
346
|
chalk.bold(fmtInt(records.length)),
|
|
242
347
|
chalk.bold(fmtCompact(t.tokensTotal)),
|
|
243
348
|
chalk.bold(fmtCompact(t.avg)),
|
|
244
349
|
chalk.bold(fmtCost(t.cost)),
|
|
245
350
|
]);
|
|
246
351
|
|
|
247
|
-
const body =
|
|
352
|
+
const body =
|
|
353
|
+
pt.toString() +
|
|
354
|
+
"\n\n" +
|
|
355
|
+
chalk.bold.underline("Token Breakdown") +
|
|
356
|
+
"\n" +
|
|
357
|
+
bt.toString();
|
|
248
358
|
return boxen(body, {
|
|
249
|
-
title: chalk.bold(
|
|
250
|
-
borderStyle:
|
|
251
|
-
borderColor:
|
|
359
|
+
title: chalk.bold("Overview"),
|
|
360
|
+
borderStyle: "round",
|
|
361
|
+
borderColor: "blue",
|
|
252
362
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
253
363
|
});
|
|
254
364
|
}
|
|
255
365
|
|
|
256
|
-
function pct(x) {
|
|
366
|
+
function pct(x) {
|
|
367
|
+
return (x * 100).toFixed(1) + "%";
|
|
368
|
+
}
|
|
257
369
|
|
|
258
370
|
// ---------------------------------------------------------------------------
|
|
259
371
|
// Per Project
|
|
260
372
|
// ---------------------------------------------------------------------------
|
|
261
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
|
+
|
|
262
381
|
export function renderPerProject(records) {
|
|
263
382
|
const items = perProject(records);
|
|
264
|
-
if (items.length === 0) return
|
|
383
|
+
if (items.length === 0) return "";
|
|
265
384
|
const barW = NARROW ? 10 : 18;
|
|
266
385
|
|
|
267
386
|
// Narrow mode: drop In/Out/Cache columns, just show n + Total + Dist
|
|
268
387
|
const head = NARROW
|
|
269
|
-
? [
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
];
|
|
274
403
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
275
404
|
|
|
276
405
|
for (const p of items) {
|
|
277
406
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
278
|
-
const
|
|
407
|
+
const sessionCell = NARROW ? fmtInt(p.n) : fmtNByTool(p.nByTool);
|
|
408
|
+
const row = [truncEnd(p.project, NARROW ? 28 : 40), sessionCell];
|
|
279
409
|
if (!NARROW) {
|
|
280
410
|
row.push(
|
|
281
411
|
fmtCompact(p.tokensInput),
|
|
@@ -287,9 +417,141 @@ export function renderPerProject(records) {
|
|
|
287
417
|
t.push(row);
|
|
288
418
|
}
|
|
289
419
|
return boxen(t.toString(), {
|
|
290
|
-
title: chalk.bold(
|
|
291
|
-
borderStyle:
|
|
292
|
-
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",
|
|
293
555
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
294
556
|
});
|
|
295
557
|
}
|
|
@@ -300,24 +562,35 @@ export function renderPerProject(records) {
|
|
|
300
562
|
|
|
301
563
|
export function renderPerTool(records) {
|
|
302
564
|
const items = perTool(records);
|
|
303
|
-
if (items.length === 0) return
|
|
565
|
+
if (items.length === 0) return "";
|
|
304
566
|
const max = items[0].tokensTotal;
|
|
305
567
|
const barW = NARROW ? 10 : 18;
|
|
306
568
|
|
|
307
569
|
const head = NARROW
|
|
308
|
-
? [
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
];
|
|
313
589
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
314
590
|
|
|
315
591
|
for (const p of items) {
|
|
316
592
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
317
|
-
const row = [
|
|
318
|
-
colorize(p.tool, toolColor(p.tool)),
|
|
319
|
-
fmtInt(p.n),
|
|
320
|
-
];
|
|
593
|
+
const row = [colorize(p.tool, toolColor(p.tool)), fmtInt(p.n)];
|
|
321
594
|
if (!NARROW) {
|
|
322
595
|
row.push(
|
|
323
596
|
fmtCompact(p.tokensInput),
|
|
@@ -334,9 +607,9 @@ export function renderPerTool(records) {
|
|
|
334
607
|
t.push(row);
|
|
335
608
|
}
|
|
336
609
|
return boxen(t.toString(), {
|
|
337
|
-
title: chalk.bold(
|
|
338
|
-
borderStyle:
|
|
339
|
-
borderColor:
|
|
610
|
+
title: chalk.bold("Per Tool"),
|
|
611
|
+
borderStyle: "round",
|
|
612
|
+
borderColor: "magenta",
|
|
340
613
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
341
614
|
});
|
|
342
615
|
}
|
|
@@ -347,16 +620,28 @@ export function renderPerTool(records) {
|
|
|
347
620
|
|
|
348
621
|
export function renderPerToolPerMonth(records) {
|
|
349
622
|
const items = perToolPerMonth(records);
|
|
350
|
-
if (items.length === 0) return
|
|
623
|
+
if (items.length === 0) return "";
|
|
351
624
|
const max = items[0].tokensTotal;
|
|
352
625
|
const barW = NARROW ? 8 : 16;
|
|
353
626
|
|
|
354
627
|
const head = NARROW
|
|
355
|
-
? [
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
];
|
|
360
645
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
361
646
|
|
|
362
647
|
for (const p of items) {
|
|
@@ -376,16 +661,13 @@ export function renderPerToolPerMonth(records) {
|
|
|
376
661
|
fmtCompact(cache),
|
|
377
662
|
);
|
|
378
663
|
}
|
|
379
|
-
row.push(
|
|
380
|
-
fmtCompact(p.tokensTotal),
|
|
381
|
-
bar(p.tokensTotal, max, barW),
|
|
382
|
-
);
|
|
664
|
+
row.push(fmtCompact(p.tokensTotal), bar(p.tokensTotal, max, barW));
|
|
383
665
|
t.push(row);
|
|
384
666
|
}
|
|
385
667
|
return boxen(t.toString(), {
|
|
386
|
-
title: chalk.bold(
|
|
387
|
-
borderStyle:
|
|
388
|
-
borderColor:
|
|
668
|
+
title: chalk.bold("Per Tool per Month"),
|
|
669
|
+
borderStyle: "round",
|
|
670
|
+
borderColor: "cyan",
|
|
389
671
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
390
672
|
});
|
|
391
673
|
}
|
|
@@ -396,13 +678,16 @@ export function renderPerToolPerMonth(records) {
|
|
|
396
678
|
|
|
397
679
|
export function renderPerMonth(records) {
|
|
398
680
|
const items = perMonth(records);
|
|
399
|
-
if (items.length === 0) return
|
|
681
|
+
if (items.length === 0) return "";
|
|
400
682
|
const barW = NARROW ? 10 : 16;
|
|
401
683
|
|
|
402
684
|
const head = [
|
|
403
|
-
chalk.bold(
|
|
404
|
-
chalk.bold(
|
|
405
|
-
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"),
|
|
406
691
|
];
|
|
407
692
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
408
693
|
|
|
@@ -423,18 +708,18 @@ export function renderPerMonth(records) {
|
|
|
423
708
|
// TOTAL row
|
|
424
709
|
const tot = overall(records);
|
|
425
710
|
t.push([
|
|
426
|
-
chalk.bgGray.white.bold(
|
|
711
|
+
chalk.bgGray.white.bold(" TOTAL "),
|
|
427
712
|
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
428
713
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
429
714
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
430
715
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
431
|
-
|
|
716
|
+
"",
|
|
432
717
|
]);
|
|
433
718
|
|
|
434
719
|
return boxen(t.toString(), {
|
|
435
|
-
title: chalk.bold(
|
|
436
|
-
borderStyle:
|
|
437
|
-
borderColor:
|
|
720
|
+
title: chalk.bold("Per Month"),
|
|
721
|
+
borderStyle: "round",
|
|
722
|
+
borderColor: "green",
|
|
438
723
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
439
724
|
});
|
|
440
725
|
}
|
|
@@ -445,13 +730,16 @@ export function renderPerMonth(records) {
|
|
|
445
730
|
|
|
446
731
|
export function renderPerWeek(records) {
|
|
447
732
|
const items = perWeek(records);
|
|
448
|
-
if (items.length === 0) return
|
|
733
|
+
if (items.length === 0) return "";
|
|
449
734
|
const barW = NARROW ? 10 : 14;
|
|
450
735
|
|
|
451
736
|
const head = [
|
|
452
|
-
chalk.bold(
|
|
453
|
-
chalk.bold(
|
|
454
|
-
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"),
|
|
455
743
|
];
|
|
456
744
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
457
745
|
|
|
@@ -468,18 +756,18 @@ export function renderPerWeek(records) {
|
|
|
468
756
|
// TOTAL row
|
|
469
757
|
const tot = overall(records);
|
|
470
758
|
t.push([
|
|
471
|
-
chalk.bgGray.white.bold(
|
|
759
|
+
chalk.bgGray.white.bold(" TOTAL "),
|
|
472
760
|
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
473
761
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
474
762
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
475
763
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
476
|
-
|
|
764
|
+
"",
|
|
477
765
|
]);
|
|
478
766
|
|
|
479
767
|
return boxen(t.toString(), {
|
|
480
|
-
title: chalk.bold(
|
|
481
|
-
borderStyle:
|
|
482
|
-
borderColor:
|
|
768
|
+
title: chalk.bold("Per Week"),
|
|
769
|
+
borderStyle: "round",
|
|
770
|
+
borderColor: "magenta",
|
|
483
771
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
484
772
|
});
|
|
485
773
|
}
|
|
@@ -490,12 +778,17 @@ export function renderPerWeek(records) {
|
|
|
490
778
|
|
|
491
779
|
export function renderTopSessions(records, n = 5) {
|
|
492
780
|
const top = topSessions(records, n);
|
|
493
|
-
if (top.length === 0) return
|
|
781
|
+
if (top.length === 0) return "";
|
|
494
782
|
|
|
495
783
|
if (NARROW) {
|
|
496
784
|
const t = new Table({
|
|
497
|
-
head: [
|
|
498
|
-
|
|
785
|
+
head: [
|
|
786
|
+
chalk.bold("#"),
|
|
787
|
+
chalk.bold("Total"),
|
|
788
|
+
chalk.bold("In/Out"),
|
|
789
|
+
chalk.bold("Cost"),
|
|
790
|
+
chalk.bold("Title"),
|
|
791
|
+
],
|
|
499
792
|
style: { head: [], border: [] },
|
|
500
793
|
});
|
|
501
794
|
top.forEach((r, i) => {
|
|
@@ -503,22 +796,31 @@ export function renderTopSessions(records, n = 5) {
|
|
|
503
796
|
chalk.bold(String(i + 1)),
|
|
504
797
|
chalk.bold.yellow(fmtCompact(r.tokensTotal)),
|
|
505
798
|
`${fmtCompact(r.tokensInput)}/${fmtCompact(r.tokensOutput)}`,
|
|
506
|
-
r.cost ?
|
|
507
|
-
truncEnd(r.title, 30) ||
|
|
799
|
+
r.cost ? "$" + r.cost.toFixed(4) : "—",
|
|
800
|
+
truncEnd(r.title, 30) || "—",
|
|
508
801
|
]);
|
|
509
802
|
});
|
|
510
|
-
return boxen(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
);
|
|
514
812
|
}
|
|
515
813
|
|
|
516
814
|
const t = new Table({
|
|
517
815
|
head: [
|
|
518
|
-
chalk.bold(
|
|
519
|
-
chalk.bold(
|
|
520
|
-
chalk.bold(
|
|
521
|
-
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"),
|
|
522
824
|
],
|
|
523
825
|
style: { head: [], border: [] },
|
|
524
826
|
});
|
|
@@ -528,16 +830,16 @@ export function renderTopSessions(records, n = 5) {
|
|
|
528
830
|
chalk.bold.yellow(fmtCompact(r.tokensTotal)),
|
|
529
831
|
fmtCompact(r.tokensInput),
|
|
530
832
|
fmtCompact(r.tokensOutput),
|
|
531
|
-
r.cost ?
|
|
833
|
+
r.cost ? "$" + r.cost.toFixed(4) : "—",
|
|
532
834
|
shortModel(r.model),
|
|
533
835
|
truncEnd(r.project, 30),
|
|
534
|
-
truncEnd(r.title, 40) ||
|
|
836
|
+
truncEnd(r.title, 40) || "—",
|
|
535
837
|
]);
|
|
536
838
|
});
|
|
537
839
|
return boxen(t.toString(), {
|
|
538
840
|
title: chalk.bold.yellow(`Top ${n} Heaviest Sessions`),
|
|
539
|
-
borderStyle:
|
|
540
|
-
borderColor:
|
|
841
|
+
borderStyle: "round",
|
|
842
|
+
borderColor: "yellow",
|
|
541
843
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
542
844
|
});
|
|
543
845
|
}
|
|
@@ -548,53 +850,78 @@ export function renderTopSessions(records, n = 5) {
|
|
|
548
850
|
|
|
549
851
|
export function renderNotes(detections, errors) {
|
|
550
852
|
const lines = [];
|
|
551
|
-
lines.push(chalk.bold.underline(
|
|
552
|
-
lines.push(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
lines.push(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
lines.push(
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
);
|
|
561
875
|
if (withTokens.length === 0) {
|
|
562
|
-
lines.push(
|
|
876
|
+
lines.push(" " + chalk.dim("(none — only presence info available)"));
|
|
563
877
|
} else {
|
|
564
878
|
for (const d of withTokens) {
|
|
565
|
-
lines.push(
|
|
879
|
+
lines.push(
|
|
880
|
+
` ${chalk.green("●")} ${colorize(d.name, toolColor(d.key))} ${chalk.dim(d.description)}`,
|
|
881
|
+
);
|
|
566
882
|
}
|
|
567
883
|
}
|
|
568
884
|
|
|
569
|
-
lines.push(
|
|
570
|
-
lines.push(
|
|
571
|
-
|
|
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
|
+
);
|
|
572
892
|
if (noTokens.length === 0) {
|
|
573
|
-
lines.push(
|
|
893
|
+
lines.push(" " + chalk.dim("(none)"));
|
|
574
894
|
} else {
|
|
575
895
|
for (const d of noTokens) {
|
|
576
|
-
lines.push(
|
|
896
|
+
lines.push(
|
|
897
|
+
` ${chalk.green("●")} ${colorize(d.name, toolColor(d.key))} ${chalk.dim(d.description)}`,
|
|
898
|
+
);
|
|
577
899
|
}
|
|
578
900
|
}
|
|
579
901
|
|
|
580
|
-
lines.push(
|
|
581
|
-
lines.push(chalk.bold.underline(
|
|
582
|
-
lines.push(
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
);
|
|
585
911
|
|
|
586
912
|
if (errors && errors.length) {
|
|
587
|
-
lines.push(
|
|
913
|
+
lines.push("");
|
|
588
914
|
lines.push(chalk.bold.underline(chalk.red(`Errors (${errors.length})`)));
|
|
589
915
|
for (const e of errors.slice(0, 5)) {
|
|
590
|
-
lines.push(
|
|
916
|
+
lines.push(" " + chalk.red("! ") + e);
|
|
591
917
|
}
|
|
592
|
-
if (errors.length > 5)
|
|
918
|
+
if (errors.length > 5)
|
|
919
|
+
lines.push(" " + chalk.dim(`... and ${errors.length - 5} more`));
|
|
593
920
|
}
|
|
594
921
|
|
|
595
|
-
return boxen(lines.join(
|
|
596
|
-
borderStyle:
|
|
597
|
-
borderColor:
|
|
922
|
+
return boxen(lines.join("\n"), {
|
|
923
|
+
borderStyle: "round",
|
|
924
|
+
borderColor: "gray",
|
|
598
925
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
599
926
|
});
|
|
600
927
|
}
|