pi-tokenburn 0.1.0 โ 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/extensions/tokenburn.ts +258 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ Totals match the `tokenburn` CLI exactly.
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Status bar:** `๐ฅ 1.20M ($52.79) [today]`, turns red with `OVER BUDGET` past your limit.
|
|
12
|
+
- **Report chart:** `/tokenburn` shows the summary plus a vertical column chart (y-axis, budget line, current period marked, over-budget columns in red, peak/avg footer). Daily (14 days), weekly (8 weeks) or monthly (6 months).
|
|
13
|
+
- **Switch periods fast:** `/tokenburn week`, `/tokenburn month`, `/tokenburn cycle`.
|
|
12
14
|
- **Window panel (optional):** Today / Week / Month / Total below the editor, with in/out tokens and cost.
|
|
13
15
|
- **Budgets** per day, week and month, in tokens and/or dollars.
|
|
14
16
|
- **Fast:** per-file cache keyed on mtime+size. First scan of ~400 MB of sessions takes under a second; refreshes after that take milliseconds. Refresh never blocks the agent loop.
|
|
@@ -25,13 +27,31 @@ pi install git:github.com/sorinirimies/pi-tokenburn
|
|
|
25
27
|
|
|
26
28
|
| Command | What it does |
|
|
27
29
|
|---|---|
|
|
28
|
-
| `/tokenburn` |
|
|
30
|
+
| `/tokenburn` | Report: Today / Week / Month / All-time + **daily** chart |
|
|
31
|
+
| `/tokenburn report week` ยท `report month` | Same report with a **weekly** / **monthly** chart (also `chart month`) |
|
|
32
|
+
| `/tokenburn status <today\|week\|month\|total>` | Set the status-bar period (shortcut: `/tokenburn total`) |
|
|
33
|
+
| `/tokenburn cycle` | Rotate the status-bar period: today โ week โ month โ total |
|
|
29
34
|
| `/tokenburn window` | Toggle the panel below the editor |
|
|
30
|
-
| `/tokenburn status <today\|week\|month\|total>` | Period shown in the status bar |
|
|
31
35
|
| `/tokenburn budget <day\|week\|month> <tokens>` | Token budget, e.g. `budget day 5000000` |
|
|
32
36
|
| `/tokenburn budget <day\|week\|month> $<cost>` | Dollar budget, e.g. `budget week $50` |
|
|
33
37
|
| `/tokenburn cache` | Toggle counting cache read/write tokens |
|
|
34
38
|
|
|
39
|
+
Chart example (`/tokenburn`, with a 600M day budget):
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Daily tokens ยท last 14 days โ budget 600.00M
|
|
43
|
+
1.13B โ โโ
|
|
44
|
+
โ โโ
|
|
45
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
46
|
+
565.16M โ โโ โโ โโ โโ
|
|
47
|
+
โ โ
โ
โโ โโ โโ โโ โโ โโ โโ โโ โโ
|
|
48
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
49
|
+
26 27 28 29 30 01 02 03 04 05 06 07 08 09
|
|
50
|
+
Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr
|
|
51
|
+
โฒ
|
|
52
|
+
ฮฃ 3.83B ($1264.30) ยท avg 273.80M/day ยท peak 1.13B (09-29 Tue)
|
|
53
|
+
```
|
|
54
|
+
|
|
35
55
|
Weeks start on Monday, in local time.
|
|
36
56
|
|
|
37
57
|
## Config
|
|
@@ -41,6 +61,7 @@ Weeks start on Monday, in local time.
|
|
|
41
61
|
```json
|
|
42
62
|
{
|
|
43
63
|
"includeCache": true,
|
|
64
|
+
"chartColor": true,
|
|
44
65
|
"showStatus": true,
|
|
45
66
|
"showWidget": false,
|
|
46
67
|
"statusWindow": "today",
|
|
@@ -50,6 +71,8 @@ Weeks start on Monday, in local time.
|
|
|
50
71
|
|
|
51
72
|
- `includeCache` (default `true`): count cache read/write tokens in totals, like the `tokenburn` CLI. Cache reads dominate (often 95%+ of tokens) but are billed far cheaper; set `false` to budget on input + output only.
|
|
52
73
|
|
|
74
|
+
- `chartColor` (default `true`): color the chart using your pi theme (current period green, over-budget red, budget line yellow). Set `false` for plain text.
|
|
75
|
+
|
|
53
76
|
Environment overrides for the session directory: `TOKENBURN_PI_SESSIONS`, `PI_CODING_AGENT_SESSION_DIR`, `PI_CODING_AGENT_DIR`.
|
|
54
77
|
|
|
55
78
|
## Develop
|
package/extensions/tokenburn.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface TokenBurnReport {
|
|
|
50
50
|
export interface TokenBurnConfig {
|
|
51
51
|
/** Count cache read/write tokens in totals (matches the tokenburn CLI). */
|
|
52
52
|
includeCache: boolean;
|
|
53
|
+
/** Color the report chart (set false if your terminal garbles ANSI in notifications). */
|
|
54
|
+
chartColor: boolean;
|
|
53
55
|
showStatus: boolean;
|
|
54
56
|
showWidget: boolean;
|
|
55
57
|
statusWindow: "today" | "week" | "month" | "total";
|
|
@@ -65,6 +67,7 @@ export interface TokenBurnConfig {
|
|
|
65
67
|
|
|
66
68
|
const DEFAULT_CONFIG: TokenBurnConfig = {
|
|
67
69
|
includeCache: true,
|
|
70
|
+
chartColor: true,
|
|
68
71
|
showStatus: true,
|
|
69
72
|
showWidget: false,
|
|
70
73
|
statusWindow: "today",
|
|
@@ -89,6 +92,7 @@ export async function loadConfig(): Promise<TokenBurnConfig> {
|
|
|
89
92
|
const parsed = JSON.parse(raw);
|
|
90
93
|
return {
|
|
91
94
|
includeCache: typeof parsed.includeCache === "boolean" ? parsed.includeCache : DEFAULT_CONFIG.includeCache,
|
|
95
|
+
chartColor: typeof parsed.chartColor === "boolean" ? parsed.chartColor : DEFAULT_CONFIG.chartColor,
|
|
92
96
|
showStatus: typeof parsed.showStatus === "boolean" ? parsed.showStatus : DEFAULT_CONFIG.showStatus,
|
|
93
97
|
showWidget: typeof parsed.showWidget === "boolean" ? parsed.showWidget : DEFAULT_CONFIG.showWidget,
|
|
94
98
|
statusWindow: ["today", "week", "month", "total"].includes(parsed.statusWindow)
|
|
@@ -188,15 +192,42 @@ const emptyStats = (): PeriodStats => ({
|
|
|
188
192
|
sessions: 0,
|
|
189
193
|
});
|
|
190
194
|
|
|
195
|
+
/** Refresh the per-file cache and return every session file's parsed turns. */
|
|
196
|
+
async function loadAll(sessionsRoot: string): Promise<Turn[][]> {
|
|
197
|
+
const files = listSessionFiles(sessionsRoot);
|
|
198
|
+
const live = new Set(files);
|
|
199
|
+
for (const key of FILE_CACHE.keys()) if (!live.has(key)) FILE_CACHE.delete(key);
|
|
200
|
+
|
|
201
|
+
const out: Turn[][] = [];
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
let sig: string;
|
|
204
|
+
try {
|
|
205
|
+
const st = statSync(file);
|
|
206
|
+
sig = `${st.mtimeMs}:${st.size}`;
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
let entry = FILE_CACHE.get(file);
|
|
211
|
+
if (!entry || entry.sig !== sig) {
|
|
212
|
+
entry = { sig, turns: await parseFile(file) };
|
|
213
|
+
FILE_CACHE.set(file, entry);
|
|
214
|
+
}
|
|
215
|
+
out.push(entry.turns);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const turnTokens = (t: Turn, includeCache: boolean) =>
|
|
221
|
+
t.input + t.output + (includeCache ? t.cacheRead + t.cacheWrite : 0);
|
|
222
|
+
|
|
191
223
|
export async function collectUsage(
|
|
192
224
|
sessionsRoot: string,
|
|
193
225
|
now: Date = new Date(),
|
|
194
226
|
includeCache = true,
|
|
195
227
|
): Promise<TokenBurnReport> {
|
|
196
|
-
const startOfDay =
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
|
|
228
|
+
const startOfDay = bucketStart(now.getTime(), "day");
|
|
229
|
+
const startOfWeek = bucketStart(now.getTime(), "week");
|
|
230
|
+
const startOfMonth = bucketStart(now.getTime(), "month");
|
|
200
231
|
|
|
201
232
|
const report: TokenBurnReport = {
|
|
202
233
|
today: emptyStats(),
|
|
@@ -205,12 +236,8 @@ export async function collectUsage(
|
|
|
205
236
|
total: emptyStats(),
|
|
206
237
|
};
|
|
207
238
|
|
|
208
|
-
const files = listSessionFiles(sessionsRoot);
|
|
209
|
-
const live = new Set(files);
|
|
210
|
-
for (const key of FILE_CACHE.keys()) if (!live.has(key)) FILE_CACHE.delete(key);
|
|
211
|
-
|
|
212
239
|
const add = (p: PeriodStats, t: Turn) => {
|
|
213
|
-
p.tokens += t
|
|
240
|
+
p.tokens += turnTokens(t, includeCache);
|
|
214
241
|
p.input += t.input;
|
|
215
242
|
p.output += t.output;
|
|
216
243
|
p.cacheRead += t.cacheRead;
|
|
@@ -218,29 +245,16 @@ export async function collectUsage(
|
|
|
218
245
|
p.cost += t.cost;
|
|
219
246
|
};
|
|
220
247
|
|
|
221
|
-
for (const
|
|
222
|
-
let sig: string;
|
|
223
|
-
try {
|
|
224
|
-
const st = statSync(file);
|
|
225
|
-
sig = `${st.mtimeMs}:${st.size}`;
|
|
226
|
-
} catch {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
let entry = FILE_CACHE.get(file);
|
|
230
|
-
if (!entry || entry.sig !== sig) {
|
|
231
|
-
entry = { sig, turns: await parseFile(file) };
|
|
232
|
-
FILE_CACHE.set(file, entry);
|
|
233
|
-
}
|
|
234
|
-
|
|
248
|
+
for (const turns of await loadAll(sessionsRoot)) {
|
|
235
249
|
let maxTs = 0;
|
|
236
|
-
for (const t of
|
|
250
|
+
for (const t of turns) {
|
|
237
251
|
if (t.ts > maxTs) maxTs = t.ts;
|
|
238
252
|
add(report.total, t);
|
|
239
253
|
if (t.ts >= startOfMonth) add(report.month, t);
|
|
240
254
|
if (t.ts >= startOfWeek) add(report.week, t);
|
|
241
255
|
if (t.ts >= startOfDay) add(report.today, t);
|
|
242
256
|
}
|
|
243
|
-
if (
|
|
257
|
+
if (turns.length > 0) report.total.sessions++;
|
|
244
258
|
if (maxTs >= startOfMonth) report.month.sessions++;
|
|
245
259
|
if (maxTs >= startOfWeek) report.week.sessions++;
|
|
246
260
|
if (maxTs >= startOfDay) report.today.sessions++;
|
|
@@ -249,6 +263,181 @@ export async function collectUsage(
|
|
|
249
263
|
return report;
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Time series + chart
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
export type Unit = "day" | "week" | "month";
|
|
271
|
+
|
|
272
|
+
/** Local-time start of the day / Monday-week / month containing `ts`. */
|
|
273
|
+
export function bucketStart(ts: number, unit: Unit): number {
|
|
274
|
+
const d = new Date(ts);
|
|
275
|
+
if (unit === "day") return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
276
|
+
if (unit === "week") {
|
|
277
|
+
const dow = (d.getDay() + 6) % 7; // Monday = 0
|
|
278
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow).getTime();
|
|
279
|
+
}
|
|
280
|
+
return new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const pad2 = (n: number) => String(n).padStart(2, "0");
|
|
284
|
+
|
|
285
|
+
export function bucketLabel(start: number, unit: Unit): string {
|
|
286
|
+
const d = new Date(start);
|
|
287
|
+
if (unit === "month") return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`;
|
|
288
|
+
const base = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
289
|
+
return unit === "day" ? `${base} ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]}` : `${base} wk`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface SeriesPoint {
|
|
293
|
+
start: number;
|
|
294
|
+
label: string;
|
|
295
|
+
tokens: number;
|
|
296
|
+
cost: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const DEFAULT_COUNT: Record<Unit, number> = { day: 14, week: 8, month: 6 };
|
|
300
|
+
|
|
301
|
+
/** Last `count` buckets ending at `now`, oldest first, empty buckets zero-filled. */
|
|
302
|
+
export async function collectSeries(
|
|
303
|
+
sessionsRoot: string,
|
|
304
|
+
unit: Unit,
|
|
305
|
+
count = DEFAULT_COUNT[unit],
|
|
306
|
+
now: Date = new Date(),
|
|
307
|
+
includeCache = true,
|
|
308
|
+
): Promise<SeriesPoint[]> {
|
|
309
|
+
const starts: number[] = [];
|
|
310
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
311
|
+
const y = now.getFullYear();
|
|
312
|
+
const m = now.getMonth();
|
|
313
|
+
const day = now.getDate();
|
|
314
|
+
const anchor =
|
|
315
|
+
unit === "day"
|
|
316
|
+
? new Date(y, m, day - i)
|
|
317
|
+
: unit === "week"
|
|
318
|
+
? new Date(y, m, day - i * 7)
|
|
319
|
+
: new Date(y, m - i, 1);
|
|
320
|
+
starts.push(bucketStart(anchor.getTime(), unit));
|
|
321
|
+
}
|
|
322
|
+
const points = new Map<number, SeriesPoint>(
|
|
323
|
+
starts.map((start) => [start, { start, label: bucketLabel(start, unit), tokens: 0, cost: 0 }]),
|
|
324
|
+
);
|
|
325
|
+
for (const turns of await loadAll(sessionsRoot)) {
|
|
326
|
+
for (const t of turns) {
|
|
327
|
+
const pt = points.get(bucketStart(t.ts, unit));
|
|
328
|
+
if (!pt) continue;
|
|
329
|
+
pt.tokens += turnTokens(t, includeCache);
|
|
330
|
+
pt.cost += t.cost;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return starts.map((s) => points.get(s)!);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const EIGHTHS = ["", "โ", "โ", "โ", "โ", "โ", "โ", "โ"];
|
|
337
|
+
const SPARK = "โโโโโ
โโโ";
|
|
338
|
+
|
|
339
|
+
/** Horizontal bar of `value/max` over `width` cells, with 1/8-cell precision. */
|
|
340
|
+
export function bar(value: number, max: number, width: number): string {
|
|
341
|
+
if (max <= 0 || value <= 0) return "";
|
|
342
|
+
const eighths = Math.max(1, Math.round((value / max) * width * 8));
|
|
343
|
+
const full = Math.floor(eighths / 8);
|
|
344
|
+
return "โ".repeat(full) + EIGHTHS[eighths % 8];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function sparkline(values: number[]): string {
|
|
348
|
+
const max = Math.max(...values, 0);
|
|
349
|
+
if (max <= 0) return SPARK[0].repeat(values.length);
|
|
350
|
+
return values.map((v) => SPARK[Math.min(7, Math.round((v / max) * 7))]).join("");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const UNIT_TITLE: Record<Unit, string> = { day: "Daily", week: "Weekly", month: "Monthly" };
|
|
354
|
+
const UNIT_NOUN: Record<Unit, string> = { day: "day", week: "week", month: "month" };
|
|
355
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
356
|
+
|
|
357
|
+
export type Paint = (kind: "bar" | "now" | "over" | "axis" | "budget" | "dim" | "title", text: string) => string;
|
|
358
|
+
const plain: Paint = (_k, t) => t;
|
|
359
|
+
|
|
360
|
+
/** Two header rows for a column: [top, bottom]. */
|
|
361
|
+
function columnLabels(p: SeriesPoint, unit: Unit): [string, string] {
|
|
362
|
+
const d = new Date(p.start);
|
|
363
|
+
if (unit === "day") return [pad2(d.getDate()), ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][d.getDay()]];
|
|
364
|
+
if (unit === "week") return [`${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`, "wk"];
|
|
365
|
+
return [MONTHS[d.getMonth()], String(d.getFullYear())];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Vertical column chart: y-axis, 1/8-row precision, optional budget line,
|
|
370
|
+
* highlighted current bucket and over-budget columns, summary footer.
|
|
371
|
+
*/
|
|
372
|
+
export function renderChart(
|
|
373
|
+
series: SeriesPoint[],
|
|
374
|
+
unit: Unit,
|
|
375
|
+
opts: { height?: number; budget?: { tokens?: number; cost?: number }; paint?: Paint } = {},
|
|
376
|
+
): string[] {
|
|
377
|
+
const paint = opts.paint ?? plain;
|
|
378
|
+
const height = opts.height ?? 8;
|
|
379
|
+
const colW = unit === "day" ? 3 : 6; // bar cells + 1 gap
|
|
380
|
+
const barW = colW - 1;
|
|
381
|
+
|
|
382
|
+
const peakPoint = series.reduce((m, p) => (p.tokens > m.tokens ? p : m), series[0]);
|
|
383
|
+
const dataMax = peakPoint?.tokens ?? 0;
|
|
384
|
+
const budgetTokens = opts.budget?.tokens;
|
|
385
|
+
const scaleMax = Math.max(dataMax, budgetTokens ?? 0);
|
|
386
|
+
const total = series.reduce((a, p) => a + p.tokens, 0);
|
|
387
|
+
const totalCost = series.reduce((a, p) => a + p.cost, 0);
|
|
388
|
+
|
|
389
|
+
const yLabels = [formatNum(scaleMax), formatNum(scaleMax / 2), "0"];
|
|
390
|
+
const yW = Math.max(...yLabels.map((l) => l.length)) + 1;
|
|
391
|
+
const labelFor = (row: number) => (row === height ? yLabels[0] : row === Math.ceil(height / 2) ? yLabels[1] : "");
|
|
392
|
+
const budgetRow = budgetTokens && scaleMax > 0 ? Math.max(1, Math.ceil((budgetTokens / scaleMax) * height)) : 0;
|
|
393
|
+
|
|
394
|
+
const isOver = (p: SeriesPoint) =>
|
|
395
|
+
opts.budget ? isOverBudget({ tokens: p.tokens, cost: p.cost } as PeriodStats, opts.budget) : false;
|
|
396
|
+
const kindOf = (i: number): "bar" | "now" | "over" =>
|
|
397
|
+
isOver(series[i]) ? "over" : i === series.length - 1 ? "now" : "bar";
|
|
398
|
+
|
|
399
|
+
const lines: string[] = [];
|
|
400
|
+
lines.push(
|
|
401
|
+
paint("title", `${UNIT_TITLE[unit]} tokens`) +
|
|
402
|
+
paint("dim", ` ยท last ${series.length} ${UNIT_NOUN[unit]}s`) +
|
|
403
|
+
(budgetTokens ? paint("budget", ` โ budget ${formatNum(budgetTokens)}`) : ""),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
for (let row = height; row >= 1; row--) {
|
|
407
|
+
let line = paint("axis", `${labelFor(row).padStart(yW)} โ`);
|
|
408
|
+
for (let i = 0; i < series.length; i++) {
|
|
409
|
+
const h = scaleMax > 0 ? (series[i].tokens / scaleMax) * height : 0;
|
|
410
|
+
let cell: string;
|
|
411
|
+
if (h >= row) cell = "โ";
|
|
412
|
+
else if (h > row - 1 && series[i].tokens > 0) cell = SPARK[Math.min(7, Math.max(0, Math.floor((h - (row - 1)) * 8) - 1))];
|
|
413
|
+
else if (row === 1 && series[i].tokens > 0) cell = "โ";
|
|
414
|
+
else cell = row === budgetRow ? "โ" : " ";
|
|
415
|
+
const filled = cell !== " " && cell !== "โ";
|
|
416
|
+
const body = cell.repeat(barW);
|
|
417
|
+
line += filled ? paint(kindOf(i), body) : cell === "โ" ? paint("budget", body) : body;
|
|
418
|
+
line += row === budgetRow ? paint("budget", "โ") : " ";
|
|
419
|
+
}
|
|
420
|
+
lines.push(line);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
lines.push(paint("axis", `${"".padStart(yW)} โ${"โ".repeat(series.length * colW)}`));
|
|
424
|
+
const pad = " ".repeat(yW + 2);
|
|
425
|
+
const cols = series.map((p, i) => ({ l: columnLabels(p, unit), now: i === series.length - 1 }));
|
|
426
|
+
lines.push(pad + cols.map((c) => c.l[0].padEnd(colW)).join(""));
|
|
427
|
+
lines.push(paint("dim", pad + cols.map((c) => c.l[1].padEnd(colW)).join("")));
|
|
428
|
+
lines.push(pad + cols.map((c) => (c.now ? paint("now", "โฒ".padEnd(colW)) : " ".repeat(colW))).join(""));
|
|
429
|
+
|
|
430
|
+
const avg = series.length ? total / series.length : 0;
|
|
431
|
+
lines.push(
|
|
432
|
+
paint(
|
|
433
|
+
"dim",
|
|
434
|
+
`ฮฃ ${formatNum(total)} (${formatCost(totalCost)}) ยท avg ${formatNum(avg)}/${UNIT_NOUN[unit]}` +
|
|
435
|
+
(dataMax > 0 ? ` ยท peak ${formatNum(dataMax)} (${bucketLabel(peakPoint.start, unit)})` : ""),
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
return lines;
|
|
439
|
+
}
|
|
440
|
+
|
|
252
441
|
/** Clear the per-file cache (tests). */
|
|
253
442
|
export function clearCache(): void {
|
|
254
443
|
FILE_CACHE.clear();
|
|
@@ -268,6 +457,20 @@ export function isOverBudget(p: PeriodStats, b: { tokens?: number; cost?: number
|
|
|
268
457
|
return (b.tokens !== undefined && p.tokens > b.tokens) || (b.cost !== undefined && p.cost > b.cost);
|
|
269
458
|
}
|
|
270
459
|
|
|
460
|
+
/** Map chart roles to pi theme colors. */
|
|
461
|
+
export function themePaint(theme: { fg: (c: any, t: string) => string }): Paint {
|
|
462
|
+
const color: Record<string, string> = {
|
|
463
|
+
bar: "accent",
|
|
464
|
+
now: "success",
|
|
465
|
+
over: "error",
|
|
466
|
+
axis: "dim",
|
|
467
|
+
budget: "warning",
|
|
468
|
+
dim: "dim",
|
|
469
|
+
title: "accent",
|
|
470
|
+
};
|
|
471
|
+
return (kind, text) => theme.fg(color[kind], text);
|
|
472
|
+
}
|
|
473
|
+
|
|
271
474
|
export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
272
475
|
let config: TokenBurnConfig = { ...DEFAULT_CONFIG };
|
|
273
476
|
|
|
@@ -330,7 +533,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
330
533
|
pi.registerCommand("tokenburn", {
|
|
331
534
|
description: "TokenBurn stats: show report, toggle widget, or set budgets",
|
|
332
535
|
getArgumentCompletions: (prefix: string) => {
|
|
333
|
-
const opts = ["report", "window", "cache", "status <today|week|month|total>", "budget <day|week|month> <tokens|$cost>"];
|
|
536
|
+
const opts = ["report", "day", "week", "month", "today", "total", "cycle", "window", "cache", "status <today|week|month|total>", "budget <day|week|month> <tokens|$cost>"];
|
|
334
537
|
return opts.filter((o) => o.startsWith(prefix)).map((value) => ({ value }));
|
|
335
538
|
},
|
|
336
539
|
handler: async (args, ctx) => {
|
|
@@ -344,11 +547,26 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
344
547
|
return;
|
|
345
548
|
}
|
|
346
549
|
|
|
347
|
-
|
|
348
|
-
|
|
550
|
+
const WINDOWS = ["today", "week", "month", "total"] as const;
|
|
551
|
+
const setWindow = async (w: (typeof WINDOWS)[number]) => {
|
|
552
|
+
config.statusWindow = w;
|
|
349
553
|
await saveConfig(config);
|
|
350
554
|
await updateUI(ctx);
|
|
351
|
-
ctx.ui.notify(`tokenburn status window
|
|
555
|
+
ctx.ui.notify(`tokenburn status window: ${w}`, "info");
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// /tokenburn today|week|month|total โ shortcut for the status bar period
|
|
559
|
+
if ((WINDOWS as readonly string[]).includes(cmd ?? "")) {
|
|
560
|
+
await setWindow(cmd as (typeof WINDOWS)[number]);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (cmd === "status" && (WINDOWS as readonly string[]).includes(p1 ?? "")) {
|
|
564
|
+
await setWindow(p1 as (typeof WINDOWS)[number]);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// /tokenburn cycle โ today โ week โ month โ total โ today
|
|
568
|
+
if (cmd === "cycle") {
|
|
569
|
+
await setWindow(WINDOWS[(WINDOWS.indexOf(config.statusWindow) + 1) % WINDOWS.length]);
|
|
352
570
|
return;
|
|
353
571
|
}
|
|
354
572
|
|
|
@@ -375,20 +593,26 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
375
593
|
return;
|
|
376
594
|
}
|
|
377
595
|
|
|
378
|
-
//
|
|
596
|
+
// /tokenburn [report] [day|week|month] โ summary + chart
|
|
597
|
+
const unitArg = (cmd === "report" || cmd === "chart" ? p1 : cmd) as string | undefined;
|
|
598
|
+
const unit: Unit = unitArg === "week" || unitArg === "month" ? unitArg : "day";
|
|
379
599
|
const report = await collectUsage(getSessionsDir(), new Date(), config.includeCache);
|
|
600
|
+
const series = await collectSeries(getSessionsDir(), unit, DEFAULT_COUNT[unit], new Date(), config.includeCache);
|
|
601
|
+
const budgetKey = unit === "day" ? "today" : unit === "week" ? "week" : "month";
|
|
380
602
|
const formatRow = (name: string, p: PeriodStats) =>
|
|
381
|
-
`${name.padEnd(
|
|
603
|
+
`${name.padEnd(10)} ${formatNum(p.tokens).padStart(8)} tokens ${formatCost(p.cost).padStart(9)} ยท in:${formatNum(p.input)} out:${formatNum(p.output)} cache:${formatNum(p.cacheRead + p.cacheWrite)} ยท ${p.sessions} sess`;
|
|
382
604
|
|
|
383
605
|
const output = [
|
|
384
606
|
"๐ฅ TokenBurn Report",
|
|
385
|
-
"
|
|
607
|
+
"โ".repeat(60),
|
|
386
608
|
formatRow("Today", report.today),
|
|
387
609
|
formatRow("This Week", report.week),
|
|
388
610
|
formatRow("This Month", report.month),
|
|
389
611
|
formatRow("All Time", report.total),
|
|
390
|
-
"
|
|
391
|
-
|
|
612
|
+
"โ".repeat(60),
|
|
613
|
+
...renderChart(series, unit, { budget: budgetsFor(config, budgetKey as Window), paint: config.chartColor ? themePaint(ctx.ui.theme) : undefined }),
|
|
614
|
+
"โ".repeat(60),
|
|
615
|
+
"/tokenburn [day|week|month] chart ยท today|week|month|total|cycle status bar ยท window ยท budget ยท cache",
|
|
392
616
|
].join("\n");
|
|
393
617
|
|
|
394
618
|
ctx.ui.notify(output, "info");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tokenburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi extension for live token usage monitoring and budget tracking (Day / Week / Month / Total). 0 prompt tokens.",
|
|
5
5
|
"keywords": ["pi-package", "pi-extension", "tokenburn", "tokens", "analytics", "budget", "stats"],
|
|
6
6
|
"license": "MIT",
|