pi-tokenburn 0.2.0 → 0.3.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 +34 -7
- package/extensions/tokenburn.ts +236 -90
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Totals match the `tokenburn` CLI exactly.
|
|
|
10
10
|
|
|
11
11
|
- **Status bar:** `🔥 1.20M ($52.79) [today]`, turns red with `OVER BUDGET` past your limit.
|
|
12
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
|
-
- **
|
|
13
|
+
- **Views:** `/tokenburn day|week|month|year|all`. Status bar period: `/tokenburn status <window>` or `/tokenburn cycle`.
|
|
14
14
|
- **Window panel (optional):** Today / Week / Month / Total below the editor, with in/out tokens and cost.
|
|
15
15
|
- **Budgets** per day, week and month, in tokens and/or dollars.
|
|
16
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.
|
|
@@ -27,15 +27,21 @@ pi install git:github.com/sorinirimies/pi-tokenburn
|
|
|
27
27
|
|
|
28
28
|
| Command | What it does |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `/tokenburn`
|
|
31
|
-
| `/tokenburn
|
|
32
|
-
| `/tokenburn
|
|
33
|
-
| `/tokenburn
|
|
30
|
+
| `/tokenburn` · `day` · `today` | Report + **daily** chart (last 14 days) |
|
|
31
|
+
| `/tokenburn week` | Report + **weekly** chart (last 8 weeks) |
|
|
32
|
+
| `/tokenburn month` | Report + **monthly** chart (last 6 months) |
|
|
33
|
+
| `/tokenburn year` | Report + **yearly** chart (last 5 years) |
|
|
34
|
+
| `/tokenburn all` | Report + whole history (monthly columns up to 18 months, yearly beyond) |
|
|
35
|
+
| `/tokenburn report <view>` · `chart <view>` | Same as above, explicit form |
|
|
36
|
+
| `/tokenburn status <today\|week\|month\|year\|total>` | Set the status-bar period |
|
|
37
|
+
| `/tokenburn cycle` | Rotate the status-bar period: today → week → month → year → total |
|
|
34
38
|
| `/tokenburn window` | Toggle the panel below the editor |
|
|
35
39
|
| `/tokenburn budget <day\|week\|month> <tokens>` | Token budget, e.g. `budget day 5000000` |
|
|
36
40
|
| `/tokenburn budget <day\|week\|month> $<cost>` | Dollar budget, e.g. `budget week $50` |
|
|
37
41
|
| `/tokenburn cache` | Toggle counting cache read/write tokens |
|
|
38
42
|
|
|
43
|
+
Tab-completion works for all of the above. Unknown input prints usage instead of failing.
|
|
44
|
+
|
|
39
45
|
Chart example (`/tokenburn`, with a 600M day budget):
|
|
40
46
|
|
|
41
47
|
```
|
|
@@ -75,12 +81,33 @@ Weeks start on Monday, in local time.
|
|
|
75
81
|
|
|
76
82
|
Environment overrides for the session directory: `TOKENBURN_PI_SESSIONS`, `PI_CODING_AGENT_SESSION_DIR`, `PI_CODING_AGENT_DIR`.
|
|
77
83
|
|
|
78
|
-
##
|
|
84
|
+
## Development
|
|
79
85
|
|
|
80
86
|
```bash
|
|
81
|
-
bun
|
|
87
|
+
bun install
|
|
88
|
+
just check # typecheck + tests + pack check + nushell tests (what CI runs)
|
|
89
|
+
just test # bun test only
|
|
82
90
|
```
|
|
83
91
|
|
|
92
|
+
CI scripts are [nushell](https://www.nushell.sh) (`scripts/`), the same ones locally and in GitHub / Gitea Actions.
|
|
93
|
+
`just --list` shows every task.
|
|
94
|
+
|
|
95
|
+
## Releases (automatic)
|
|
96
|
+
|
|
97
|
+
| Workflow | When | What |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| **CI** | push / PR | quality gate on Linux; tests on macOS, Windows and extreme time zones |
|
|
100
|
+
| **Nightly Dependency Update** | every night (GitHub 02:00 UTC, Gitea 02:30) | `bun update` within ranges, verify on all platforms, commit `chore(deps)`; then **publish a patch** when a *runtime* dependency was upgraded or `feat`/`fix`/`perf` commits are waiting since the last tag |
|
|
101
|
+
| **Release** | tag `vX.Y.Z` | validates the tag against `package.json`, runs the gate, `npm publish` (idempotent), creates the release |
|
|
102
|
+
|
|
103
|
+
Dev-tooling-only bumps are committed but never released on their own. A downgrade blocks the whole update.
|
|
104
|
+
|
|
105
|
+
Manual release: `just bump patch` (or `minor` / `major` / `X.Y.Z`), then `just release-push`.
|
|
106
|
+
|
|
107
|
+
**Secrets** (repo settings): `NPM_TOKEN` — npm *granular access token* with publish rights and "bypass 2FA" (required for CI publishing). Optional: `GH_PAT` (lets a tag push trigger the release itself), `GITEA_TOKEN` (Gitea).
|
|
108
|
+
|
|
109
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org); the changelog is generated by git-cliff.
|
|
110
|
+
|
|
84
111
|
## Scope
|
|
85
112
|
|
|
86
113
|
Pi sessions only. For Zed, Claude Code, Codex, Gemini and more, use the Rust [tokenburn](https://github.com/sorinirimies/tokenburn).
|
package/extensions/tokenburn.ts
CHANGED
|
@@ -44,9 +44,13 @@ export interface TokenBurnReport {
|
|
|
44
44
|
today: PeriodStats;
|
|
45
45
|
week: PeriodStats;
|
|
46
46
|
month: PeriodStats;
|
|
47
|
+
year: PeriodStats;
|
|
47
48
|
total: PeriodStats;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export const STATUS_WINDOWS = ["today", "week", "month", "year", "total"] as const;
|
|
52
|
+
export type Window = (typeof STATUS_WINDOWS)[number];
|
|
53
|
+
|
|
50
54
|
export interface TokenBurnConfig {
|
|
51
55
|
/** Count cache read/write tokens in totals (matches the tokenburn CLI). */
|
|
52
56
|
includeCache: boolean;
|
|
@@ -54,7 +58,7 @@ export interface TokenBurnConfig {
|
|
|
54
58
|
chartColor: boolean;
|
|
55
59
|
showStatus: boolean;
|
|
56
60
|
showWidget: boolean;
|
|
57
|
-
statusWindow:
|
|
61
|
+
statusWindow: Window;
|
|
58
62
|
budgets: {
|
|
59
63
|
dayTokens?: number;
|
|
60
64
|
weekTokens?: number;
|
|
@@ -95,16 +99,21 @@ export async function loadConfig(): Promise<TokenBurnConfig> {
|
|
|
95
99
|
chartColor: typeof parsed.chartColor === "boolean" ? parsed.chartColor : DEFAULT_CONFIG.chartColor,
|
|
96
100
|
showStatus: typeof parsed.showStatus === "boolean" ? parsed.showStatus : DEFAULT_CONFIG.showStatus,
|
|
97
101
|
showWidget: typeof parsed.showWidget === "boolean" ? parsed.showWidget : DEFAULT_CONFIG.showWidget,
|
|
98
|
-
statusWindow:
|
|
102
|
+
statusWindow: (STATUS_WINDOWS as readonly string[]).includes(parsed.statusWindow)
|
|
99
103
|
? parsed.statusWindow
|
|
100
104
|
: DEFAULT_CONFIG.statusWindow,
|
|
101
|
-
budgets: typeof parsed.budgets === "object" && parsed.budgets !== null ? parsed.budgets : {},
|
|
105
|
+
budgets: typeof parsed.budgets === "object" && parsed.budgets !== null ? { ...parsed.budgets } : {},
|
|
102
106
|
};
|
|
103
107
|
} catch {
|
|
104
|
-
return
|
|
108
|
+
return freshConfig();
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
/** A config that shares no mutable state with DEFAULT_CONFIG. */
|
|
113
|
+
export function freshConfig(): TokenBurnConfig {
|
|
114
|
+
return { ...DEFAULT_CONFIG, budgets: {} };
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
export async function saveConfig(cfg: TokenBurnConfig): Promise<void> {
|
|
109
118
|
await mkdir(getConfigDir(), { recursive: true });
|
|
110
119
|
await writeFile(join(getConfigDir(), "tokenburn.json"), JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
@@ -228,11 +237,13 @@ export async function collectUsage(
|
|
|
228
237
|
const startOfDay = bucketStart(now.getTime(), "day");
|
|
229
238
|
const startOfWeek = bucketStart(now.getTime(), "week");
|
|
230
239
|
const startOfMonth = bucketStart(now.getTime(), "month");
|
|
240
|
+
const startOfYear = bucketStart(now.getTime(), "year");
|
|
231
241
|
|
|
232
242
|
const report: TokenBurnReport = {
|
|
233
243
|
today: emptyStats(),
|
|
234
244
|
week: emptyStats(),
|
|
235
245
|
month: emptyStats(),
|
|
246
|
+
year: emptyStats(),
|
|
236
247
|
total: emptyStats(),
|
|
237
248
|
};
|
|
238
249
|
|
|
@@ -250,11 +261,13 @@ export async function collectUsage(
|
|
|
250
261
|
for (const t of turns) {
|
|
251
262
|
if (t.ts > maxTs) maxTs = t.ts;
|
|
252
263
|
add(report.total, t);
|
|
264
|
+
if (t.ts >= startOfYear) add(report.year, t);
|
|
253
265
|
if (t.ts >= startOfMonth) add(report.month, t);
|
|
254
266
|
if (t.ts >= startOfWeek) add(report.week, t);
|
|
255
267
|
if (t.ts >= startOfDay) add(report.today, t);
|
|
256
268
|
}
|
|
257
269
|
if (turns.length > 0) report.total.sessions++;
|
|
270
|
+
if (maxTs >= startOfYear) report.year.sessions++;
|
|
258
271
|
if (maxTs >= startOfMonth) report.month.sessions++;
|
|
259
272
|
if (maxTs >= startOfWeek) report.week.sessions++;
|
|
260
273
|
if (maxTs >= startOfDay) report.today.sessions++;
|
|
@@ -267,7 +280,7 @@ export async function collectUsage(
|
|
|
267
280
|
// Time series + chart
|
|
268
281
|
// ---------------------------------------------------------------------------
|
|
269
282
|
|
|
270
|
-
export type Unit = "day" | "week" | "month";
|
|
283
|
+
export type Unit = "day" | "week" | "month" | "year";
|
|
271
284
|
|
|
272
285
|
/** Local-time start of the day / Monday-week / month containing `ts`. */
|
|
273
286
|
export function bucketStart(ts: number, unit: Unit): number {
|
|
@@ -277,6 +290,7 @@ export function bucketStart(ts: number, unit: Unit): number {
|
|
|
277
290
|
const dow = (d.getDay() + 6) % 7; // Monday = 0
|
|
278
291
|
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow).getTime();
|
|
279
292
|
}
|
|
293
|
+
if (unit === "year") return new Date(d.getFullYear(), 0, 1).getTime();
|
|
280
294
|
return new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
281
295
|
}
|
|
282
296
|
|
|
@@ -284,6 +298,7 @@ const pad2 = (n: number) => String(n).padStart(2, "0");
|
|
|
284
298
|
|
|
285
299
|
export function bucketLabel(start: number, unit: Unit): string {
|
|
286
300
|
const d = new Date(start);
|
|
301
|
+
if (unit === "year") return String(d.getFullYear());
|
|
287
302
|
if (unit === "month") return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`;
|
|
288
303
|
const base = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
289
304
|
return unit === "day" ? `${base} ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]}` : `${base} wk`;
|
|
@@ -296,7 +311,7 @@ export interface SeriesPoint {
|
|
|
296
311
|
cost: number;
|
|
297
312
|
}
|
|
298
313
|
|
|
299
|
-
export const DEFAULT_COUNT: Record<Unit, number> = { day: 14, week: 8, month: 6 };
|
|
314
|
+
export const DEFAULT_COUNT: Record<Unit, number> = { day: 14, week: 8, month: 6, year: 5 };
|
|
300
315
|
|
|
301
316
|
/** Last `count` buckets ending at `now`, oldest first, empty buckets zero-filled. */
|
|
302
317
|
export async function collectSeries(
|
|
@@ -316,7 +331,9 @@ export async function collectSeries(
|
|
|
316
331
|
? new Date(y, m, day - i)
|
|
317
332
|
: unit === "week"
|
|
318
333
|
? new Date(y, m, day - i * 7)
|
|
319
|
-
:
|
|
334
|
+
: unit === "year"
|
|
335
|
+
? new Date(y - i, 0, 1)
|
|
336
|
+
: new Date(y, m - i, 1);
|
|
320
337
|
starts.push(bucketStart(anchor.getTime(), unit));
|
|
321
338
|
}
|
|
322
339
|
const points = new Map<number, SeriesPoint>(
|
|
@@ -333,6 +350,27 @@ export async function collectSeries(
|
|
|
333
350
|
return starts.map((s) => points.get(s)!);
|
|
334
351
|
}
|
|
335
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Whole history: monthly columns while it spans <= 18 months, yearly after that.
|
|
355
|
+
* Always at least 2 columns so the chart has a shape.
|
|
356
|
+
*/
|
|
357
|
+
export async function collectAllSeries(
|
|
358
|
+
sessionsRoot: string,
|
|
359
|
+
now: Date = new Date(),
|
|
360
|
+
includeCache = true,
|
|
361
|
+
): Promise<{ unit: Unit; series: SeriesPoint[] }> {
|
|
362
|
+
let first = now.getTime();
|
|
363
|
+
for (const turns of await loadAll(sessionsRoot)) for (const t of turns) if (t.ts < first) first = t.ts;
|
|
364
|
+
const f = new Date(first);
|
|
365
|
+
const months = (now.getFullYear() - f.getFullYear()) * 12 + (now.getMonth() - f.getMonth()) + 1;
|
|
366
|
+
if (months <= 18) {
|
|
367
|
+
const unit: Unit = "month";
|
|
368
|
+
return { unit, series: await collectSeries(sessionsRoot, unit, Math.max(2, months), now, includeCache) };
|
|
369
|
+
}
|
|
370
|
+
const years = now.getFullYear() - f.getFullYear() + 1;
|
|
371
|
+
return { unit: "year", series: await collectSeries(sessionsRoot, "year", years, now, includeCache) };
|
|
372
|
+
}
|
|
373
|
+
|
|
336
374
|
const EIGHTHS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
|
|
337
375
|
const SPARK = "▁▂▃▄▅▆▇█";
|
|
338
376
|
|
|
@@ -350,8 +388,8 @@ export function sparkline(values: number[]): string {
|
|
|
350
388
|
return values.map((v) => SPARK[Math.min(7, Math.round((v / max) * 7))]).join("");
|
|
351
389
|
}
|
|
352
390
|
|
|
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" };
|
|
391
|
+
const UNIT_TITLE: Record<Unit, string> = { day: "Daily", week: "Weekly", month: "Monthly", year: "Yearly" };
|
|
392
|
+
const UNIT_NOUN: Record<Unit, string> = { day: "day", week: "week", month: "month", year: "year" };
|
|
355
393
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
356
394
|
|
|
357
395
|
export type Paint = (kind: "bar" | "now" | "over" | "axis" | "budget" | "dim" | "title", text: string) => string;
|
|
@@ -362,6 +400,7 @@ function columnLabels(p: SeriesPoint, unit: Unit): [string, string] {
|
|
|
362
400
|
const d = new Date(p.start);
|
|
363
401
|
if (unit === "day") return [pad2(d.getDate()), ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][d.getDay()]];
|
|
364
402
|
if (unit === "week") return [`${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`, "wk"];
|
|
403
|
+
if (unit === "year") return [String(d.getFullYear()), ""];
|
|
365
404
|
return [MONTHS[d.getMonth()], String(d.getFullYear())];
|
|
366
405
|
}
|
|
367
406
|
|
|
@@ -372,7 +411,7 @@ function columnLabels(p: SeriesPoint, unit: Unit): [string, string] {
|
|
|
372
411
|
export function renderChart(
|
|
373
412
|
series: SeriesPoint[],
|
|
374
413
|
unit: Unit,
|
|
375
|
-
opts: { height?: number; budget?: { tokens?: number; cost?: number }; paint?: Paint } = {},
|
|
414
|
+
opts: { height?: number; budget?: { tokens?: number; cost?: number }; paint?: Paint; title?: string } = {},
|
|
376
415
|
): string[] {
|
|
377
416
|
const paint = opts.paint ?? plain;
|
|
378
417
|
const height = opts.height ?? 8;
|
|
@@ -398,8 +437,8 @@ export function renderChart(
|
|
|
398
437
|
|
|
399
438
|
const lines: string[] = [];
|
|
400
439
|
lines.push(
|
|
401
|
-
paint("title", `${UNIT_TITLE[unit]} tokens`) +
|
|
402
|
-
paint("dim", ` · last ${series.length} ${UNIT_NOUN[unit]}s`) +
|
|
440
|
+
paint("title", opts.title ?? `${UNIT_TITLE[unit]} tokens`) +
|
|
441
|
+
paint("dim", ` · ${opts.title ? "" : "last "}${series.length} ${UNIT_NOUN[unit]}${series.length === 1 ? "" : "s"}`) +
|
|
403
442
|
(budgetTokens ? paint("budget", ` ┄ budget ${formatNum(budgetTokens)}`) : ""),
|
|
404
443
|
);
|
|
405
444
|
|
|
@@ -443,8 +482,6 @@ export function clearCache(): void {
|
|
|
443
482
|
FILE_CACHE.clear();
|
|
444
483
|
}
|
|
445
484
|
|
|
446
|
-
export type Window = "today" | "week" | "month" | "total";
|
|
447
|
-
|
|
448
485
|
export function budgetsFor(cfg: TokenBurnConfig, w: Window): { tokens?: number; cost?: number } {
|
|
449
486
|
const b = cfg.budgets;
|
|
450
487
|
if (w === "today") return { tokens: b.dayTokens, cost: b.dayCost };
|
|
@@ -472,7 +509,7 @@ export function themePaint(theme: { fg: (c: any, t: string) => string }): Paint
|
|
|
472
509
|
}
|
|
473
510
|
|
|
474
511
|
export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
475
|
-
let config: TokenBurnConfig =
|
|
512
|
+
let config: TokenBurnConfig = freshConfig();
|
|
476
513
|
|
|
477
514
|
const updateUI = async (ctx: ExtensionContext) => {
|
|
478
515
|
if (!ctx.hasUI) return;
|
|
@@ -510,6 +547,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
510
547
|
line("Today", report.today, budgetsFor(config, "today")),
|
|
511
548
|
line("Week", report.week, budgetsFor(config, "week")),
|
|
512
549
|
line("Month", report.month, budgetsFor(config, "month")),
|
|
550
|
+
line("Year", report.year),
|
|
513
551
|
line("Total", report.total),
|
|
514
552
|
`└${"─".repeat(46)}┘`,
|
|
515
553
|
];
|
|
@@ -531,91 +569,199 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
531
569
|
});
|
|
532
570
|
|
|
533
571
|
pi.registerCommand("tokenburn", {
|
|
534
|
-
description: "TokenBurn
|
|
535
|
-
getArgumentCompletions: (prefix: string) =>
|
|
536
|
-
const opts = ["report", "day", "week", "month", "today", "total", "cycle", "window", "cache", "status <today|week|month|total>", "budget <day|week|month> <tokens|$cost>"];
|
|
537
|
-
return opts.filter((o) => o.startsWith(prefix)).map((value) => ({ value }));
|
|
538
|
-
},
|
|
572
|
+
description: "TokenBurn: /tokenburn [day|week|month|year|all] chart, status <window>, cycle, window, budget, cache",
|
|
573
|
+
getArgumentCompletions: (prefix: string) => completions(prefix),
|
|
539
574
|
handler: async (args, ctx) => {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
if (cmd === "window" || cmd === "widget") {
|
|
543
|
-
config.showWidget = !config.showWidget;
|
|
575
|
+
const cmd = parseCommand(args);
|
|
576
|
+
const persist = async () => {
|
|
544
577
|
await saveConfig(config);
|
|
545
578
|
await updateUI(ctx);
|
|
546
|
-
ctx.ui.notify(`tokenburn widget: ${config.showWidget ? "enabled" : "hidden"}`, "info");
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const WINDOWS = ["today", "week", "month", "total"] as const;
|
|
551
|
-
const setWindow = async (w: (typeof WINDOWS)[number]) => {
|
|
552
|
-
config.statusWindow = w;
|
|
553
|
-
await saveConfig(config);
|
|
554
|
-
await updateUI(ctx);
|
|
555
|
-
ctx.ui.notify(`tokenburn status window: ${w}`, "info");
|
|
556
579
|
};
|
|
557
580
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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]);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
581
|
+
switch (cmd.kind) {
|
|
582
|
+
case "window":
|
|
583
|
+
config.showWidget = !config.showWidget;
|
|
584
|
+
await persist();
|
|
585
|
+
ctx.ui.notify(`tokenburn widget: ${config.showWidget ? "enabled" : "hidden"}`, "info");
|
|
586
|
+
return;
|
|
572
587
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
588
|
+
case "status":
|
|
589
|
+
config.statusWindow = cmd.window;
|
|
590
|
+
await persist();
|
|
591
|
+
ctx.ui.notify(`tokenburn status window: ${cmd.window}`, "info");
|
|
592
|
+
return;
|
|
580
593
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
ctx.ui.notify(
|
|
594
|
+
case "cycle": {
|
|
595
|
+
const next = STATUS_WINDOWS[(STATUS_WINDOWS.indexOf(config.statusWindow) + 1) % STATUS_WINDOWS.length];
|
|
596
|
+
config.statusWindow = next;
|
|
597
|
+
await persist();
|
|
598
|
+
ctx.ui.notify(`tokenburn status window: ${next}`, "info");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case "cache":
|
|
603
|
+
config.includeCache = !config.includeCache;
|
|
604
|
+
await persist();
|
|
605
|
+
ctx.ui.notify(`tokenburn: cache tokens ${config.includeCache ? "included" : "excluded"}`, "info");
|
|
606
|
+
return;
|
|
607
|
+
|
|
608
|
+
case "budget": {
|
|
609
|
+
const key = `${cmd.period}${cmd.isCost ? "Cost" : "Tokens"}` as keyof TokenBurnConfig["budgets"];
|
|
610
|
+
config.budgets[key] = cmd.amount;
|
|
611
|
+
await persist();
|
|
612
|
+
ctx.ui.notify(
|
|
613
|
+
`tokenburn ${cmd.period} budget: ${cmd.isCost ? formatCost(cmd.amount) : formatNum(cmd.amount) + " tokens"}`,
|
|
614
|
+
"info",
|
|
615
|
+
);
|
|
586
616
|
return;
|
|
587
617
|
}
|
|
588
|
-
const key = `${p1}${isCost ? "Cost" : "Tokens"}` as keyof TokenBurnConfig["budgets"];
|
|
589
|
-
config.budgets[key] = val;
|
|
590
|
-
await saveConfig(config);
|
|
591
|
-
await updateUI(ctx);
|
|
592
|
-
ctx.ui.notify(`tokenburn ${p1} budget: ${isCost ? formatCost(val) : formatNum(val) + " tokens"}`, "info");
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
618
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
+
case "invalid":
|
|
620
|
+
ctx.ui.notify(cmd.message, "warning");
|
|
621
|
+
return;
|
|
622
|
+
|
|
623
|
+
case "report": {
|
|
624
|
+
const root = getSessionsDir();
|
|
625
|
+
const now = new Date();
|
|
626
|
+
const report = await collectUsage(root, now, config.includeCache);
|
|
627
|
+
const paint = config.chartColor ? themePaint(ctx.ui.theme) : undefined;
|
|
628
|
+
|
|
629
|
+
let chart: string[];
|
|
630
|
+
if (cmd.view === "all") {
|
|
631
|
+
const all = await collectAllSeries(root, now, config.includeCache);
|
|
632
|
+
chart = renderChart(all.series, all.unit, { paint, title: "All time" });
|
|
633
|
+
} else {
|
|
634
|
+
const series = await collectSeries(root, cmd.view, DEFAULT_COUNT[cmd.view], now, config.includeCache);
|
|
635
|
+
const budgetWindow: Window | undefined =
|
|
636
|
+
cmd.view === "day" ? "today" : cmd.view === "year" ? undefined : cmd.view;
|
|
637
|
+
chart = renderChart(series, cmd.view, {
|
|
638
|
+
paint,
|
|
639
|
+
budget: budgetWindow ? budgetsFor(config, budgetWindow) : undefined,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const row = (name: string, p: PeriodStats) =>
|
|
644
|
+
`${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`;
|
|
645
|
+
const rule = "─".repeat(60);
|
|
646
|
+
ctx.ui.notify(
|
|
647
|
+
[
|
|
648
|
+
"🔥 TokenBurn Report",
|
|
649
|
+
rule,
|
|
650
|
+
row("Today", report.today),
|
|
651
|
+
row("This Week", report.week),
|
|
652
|
+
row("This Month", report.month),
|
|
653
|
+
row("This Year", report.year),
|
|
654
|
+
row("All Time", report.total),
|
|
655
|
+
rule,
|
|
656
|
+
...chart,
|
|
657
|
+
rule,
|
|
658
|
+
"/tokenburn day|week|month|year|all · status <window> · cycle · window · budget · cache",
|
|
659
|
+
].join("\n"),
|
|
660
|
+
"info",
|
|
661
|
+
);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
619
665
|
},
|
|
620
666
|
});
|
|
621
667
|
}
|
|
668
|
+
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
// Command parsing + completions (pure, unit-tested)
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
|
|
673
|
+
export const REPORT_VIEWS = ["day", "week", "month", "year", "all"] as const;
|
|
674
|
+
export type ReportView = (typeof REPORT_VIEWS)[number];
|
|
675
|
+
export const BUDGET_PERIODS = ["day", "week", "month"] as const;
|
|
676
|
+
|
|
677
|
+
export type Command =
|
|
678
|
+
| { kind: "report"; view: ReportView }
|
|
679
|
+
| { kind: "status"; window: Window }
|
|
680
|
+
| { kind: "cycle" }
|
|
681
|
+
| { kind: "window" }
|
|
682
|
+
| { kind: "cache" }
|
|
683
|
+
| { kind: "budget"; period: (typeof BUDGET_PERIODS)[number]; amount: number; isCost: boolean }
|
|
684
|
+
| { kind: "invalid"; message: string };
|
|
685
|
+
|
|
686
|
+
export const USAGE =
|
|
687
|
+
"Usage: /tokenburn [day|week|month|year|all] · status <today|week|month|year|total> · cycle · window · budget <day|week|month> <tokens|$cost> · cache";
|
|
688
|
+
|
|
689
|
+
const asView = (w: string | undefined): ReportView | undefined =>
|
|
690
|
+
w === "today" ? "day" : (REPORT_VIEWS as readonly string[]).includes(w ?? "") ? (w as ReportView) : undefined;
|
|
691
|
+
|
|
692
|
+
export function parseCommand(args: string | undefined): Command {
|
|
693
|
+
const [cmd, p1, p2] = (args ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
694
|
+
if (!cmd) return { kind: "report", view: "day" };
|
|
695
|
+
|
|
696
|
+
const view = asView(cmd);
|
|
697
|
+
if (view) return { kind: "report", view };
|
|
698
|
+
|
|
699
|
+
switch (cmd) {
|
|
700
|
+
case "report":
|
|
701
|
+
case "chart": {
|
|
702
|
+
if (p1 === undefined) return { kind: "report", view: "day" };
|
|
703
|
+
const v = asView(p1);
|
|
704
|
+
return v ? { kind: "report", view: v } : { kind: "invalid", message: `Unknown view "${p1}". ${USAGE}` };
|
|
705
|
+
}
|
|
706
|
+
case "status":
|
|
707
|
+
return (STATUS_WINDOWS as readonly string[]).includes(p1 ?? "")
|
|
708
|
+
? { kind: "status", window: p1 as Window }
|
|
709
|
+
: { kind: "invalid", message: `Usage: /tokenburn status <${STATUS_WINDOWS.join("|")}>` };
|
|
710
|
+
case "cycle":
|
|
711
|
+
return { kind: "cycle" };
|
|
712
|
+
case "window":
|
|
713
|
+
case "widget":
|
|
714
|
+
return { kind: "window" };
|
|
715
|
+
case "cache":
|
|
716
|
+
return { kind: "cache" };
|
|
717
|
+
case "budget": {
|
|
718
|
+
const isCost = (p2 ?? "").startsWith("$");
|
|
719
|
+
const amount = Number((p2 ?? "").replace(/^\$/, ""));
|
|
720
|
+
if (!(BUDGET_PERIODS as readonly string[]).includes(p1 ?? "") || !Number.isFinite(amount) || amount <= 0) {
|
|
721
|
+
return { kind: "invalid", message: "Usage: /tokenburn budget <day|week|month> <tokens | $cost>" };
|
|
722
|
+
}
|
|
723
|
+
return { kind: "budget", period: p1 as (typeof BUDGET_PERIODS)[number], amount, isCost };
|
|
724
|
+
}
|
|
725
|
+
default:
|
|
726
|
+
return { kind: "invalid", message: `Unknown option "${cmd}". ${USAGE}` };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export interface CompletionItem {
|
|
731
|
+
value: string;
|
|
732
|
+
/** Required by pi: it calls `label.endsWith(...)` when applying a completion. */
|
|
733
|
+
label: string;
|
|
734
|
+
description?: string;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const TOP_LEVEL: CompletionItem[] = [
|
|
738
|
+
{ value: "day", label: "day", description: "Report + daily chart (last 14 days)" },
|
|
739
|
+
{ value: "week", label: "week", description: "Report + weekly chart (last 8 weeks)" },
|
|
740
|
+
{ value: "month", label: "month", description: "Report + monthly chart (last 6 months)" },
|
|
741
|
+
{ value: "year", label: "year", description: "Report + yearly chart (last 5 years)" },
|
|
742
|
+
{ value: "all", label: "all", description: "Report + whole history" },
|
|
743
|
+
{ value: "status ", label: "status", description: "Set the status-bar period" },
|
|
744
|
+
{ value: "cycle", label: "cycle", description: "Rotate the status-bar period" },
|
|
745
|
+
{ value: "window", label: "window", description: "Toggle the panel below the editor" },
|
|
746
|
+
{ value: "budget ", label: "budget", description: "Set a day/week/month budget" },
|
|
747
|
+
{ value: "cache", label: "cache", description: "Toggle counting cache tokens" },
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
/** `prefix` is the whole argument text typed so far (pi replaces it with `value`). */
|
|
751
|
+
export function completions(prefix: string): CompletionItem[] | null {
|
|
752
|
+
const text = (prefix ?? "").toLowerCase().replace(/^\s+/, "");
|
|
753
|
+
const sub = (head: string, words: readonly string[], desc: (w: string) => string): CompletionItem[] =>
|
|
754
|
+
words.map((w) => ({ value: `${head} ${w}`, label: w, description: desc(w) }));
|
|
755
|
+
|
|
756
|
+
let pool: CompletionItem[];
|
|
757
|
+
if (text.startsWith("status ")) pool = sub("status", STATUS_WINDOWS, (w) => `Show ${w} in the status bar`);
|
|
758
|
+
else if (text.startsWith("budget ") && !/^budget \S+ /.test(text))
|
|
759
|
+
pool = sub("budget", BUDGET_PERIODS, (w) => `Set the ${w} budget (tokens or $cost)`);
|
|
760
|
+
else if (/^(report|chart) /.test(text)) {
|
|
761
|
+
const head = text.split(" ")[0];
|
|
762
|
+
pool = sub(head, REPORT_VIEWS, (w) => `${head} for ${w}`);
|
|
763
|
+
} else pool = TOP_LEVEL;
|
|
764
|
+
|
|
765
|
+
const out = pool.filter((i) => i.value.startsWith(text));
|
|
766
|
+
return out.length > 0 ? out : null;
|
|
767
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tokenburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://github.com/sorinirimies/pi-tokenburn#readme",
|
|
13
13
|
"files": ["extensions", "README.md", "LICENSE"],
|
|
14
|
-
"pi": {
|
|
15
|
-
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": ["./extensions/tokenburn.ts"]
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"check": "nu scripts/ci/quality_gate.nu"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
27
|
+
"@types/bun": "^1.4.2",
|
|
28
|
+
"typescript": "^7.0.2"
|
|
29
|
+
}
|
|
16
30
|
}
|