pi-tokenburn 0.3.0 → 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 +18 -12
- package/extensions/tokenburn.ts +84 -36
- package/package.json +1 -1
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
|
+
- **Status line:** `/tokenburn day|week|month|year|all` (or `cycle`) changes the period shown at the bottom. Charts live under `/tokenburn chart <view>`.
|
|
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,22 +27,19 @@ 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
|
|
34
|
-
| `/tokenburn
|
|
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 |
|
|
30
|
+
| `/tokenburn day\|week\|month\|year\|all` | **Switch the bottom status line**: today, this week, this month, this year, or the all-time total |
|
|
31
|
+
| `/tokenburn chart <view>` | Report + **chart**. Views: `day` (14 days), `week` (8 weeks), `month` (6 months), `year` (5 years), `all` (whole history; monthly columns up to 18 months, yearly beyond) |
|
|
32
|
+
| `/tokenburn` | Report + daily chart (same as `/tokenburn chart day`) |
|
|
33
|
+
| `/tokenburn cycle` | Rotate the status line: today → week → month → year → total |
|
|
34
|
+
| `/tokenburn status <today\|week\|month\|year\|total>` | Explicit form of the first row |
|
|
38
35
|
| `/tokenburn window` | Toggle the panel below the editor |
|
|
39
36
|
| `/tokenburn budget <day\|week\|month> <tokens>` | Token budget, e.g. `budget day 5000000` |
|
|
40
37
|
| `/tokenburn budget <day\|week\|month> $<cost>` | Dollar budget, e.g. `budget week $50` |
|
|
41
38
|
| `/tokenburn cache` | Toggle counting cache read/write tokens |
|
|
42
39
|
|
|
43
|
-
Tab-completion works for all of
|
|
40
|
+
Tab-completion works for all of these. Unknown input prints usage instead of failing. `report <view>` is kept as an alias of `chart <view>`.
|
|
44
41
|
|
|
45
|
-
Chart example (`/tokenburn`, with a 600M day budget):
|
|
42
|
+
Chart example (`/tokenburn chart day`, with a 600M day budget):
|
|
46
43
|
|
|
47
44
|
```
|
|
48
45
|
Daily tokens · last 14 days ┄ budget 600.00M
|
|
@@ -81,14 +78,23 @@ Weeks start on Monday, in local time.
|
|
|
81
78
|
|
|
82
79
|
Environment overrides for the session directory: `TOKENBURN_PI_SESSIONS`, `PI_CODING_AGENT_SESSION_DIR`, `PI_CODING_AGENT_DIR`.
|
|
83
80
|
|
|
81
|
+
## Security notes
|
|
82
|
+
|
|
83
|
+
- **Local and read-only.** It reads pi's session logs and its own `tokenburn.json`. Nothing leaves your machine: no network, no telemetry, no shell commands, **zero runtime dependencies**.
|
|
84
|
+
- **Never breaks pi.** Stats refreshes are fire-and-forget and swallow their own errors, so a failure here cannot interrupt a turn or a session start.
|
|
85
|
+
- **Hand-edited config is sanitised.** Unknown or non-numeric budget values are dropped.
|
|
86
|
+
|
|
84
87
|
## Development
|
|
85
88
|
|
|
86
89
|
```bash
|
|
87
90
|
bun install
|
|
88
91
|
just check # typecheck + tests + pack check + nushell tests (what CI runs)
|
|
89
|
-
just test # bun test only
|
|
92
|
+
just test # bun test only (with coverage)
|
|
93
|
+
just coverage # tests + a coverage table
|
|
90
94
|
```
|
|
91
95
|
|
|
96
|
+
**Coverage:** every `bun test` collects coverage (`bunfig.toml`) and **fails below 95% lines / 95% functions**. CI shows the table on the run page and uploads `lcov.info`.
|
|
97
|
+
|
|
92
98
|
CI scripts are [nushell](https://www.nushell.sh) (`scripts/`), the same ones locally and in GitHub / Gitea Actions.
|
|
93
99
|
`just --list` shows every task.
|
|
94
100
|
|
package/extensions/tokenburn.ts
CHANGED
|
@@ -78,18 +78,24 @@ const DEFAULT_CONFIG: TokenBurnConfig = {
|
|
|
78
78
|
budgets: {},
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
return join(
|
|
81
|
+
/** Where pi keeps its config: PI_CODING_AGENT_DIR, then $XDG_CONFIG_HOME/pi/agent, then ~/.pi/agent. */
|
|
82
|
+
export function resolveAgentDir(env: Record<string, string | undefined>, home: string): string {
|
|
83
|
+
if (env.PI_CODING_AGENT_DIR) return env.PI_CODING_AGENT_DIR;
|
|
84
|
+
if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "pi", "agent");
|
|
85
|
+
return join(home, ".pi", "agent");
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
88
|
+
/** Pi session logs: TOKENBURN_PI_SESSIONS, PI_CODING_AGENT_SESSION_DIR, then <agent dir>/sessions. */
|
|
89
|
+
export function resolveSessionsDir(env: Record<string, string | undefined>, home: string): string {
|
|
90
|
+
if (env.TOKENBURN_PI_SESSIONS) return env.TOKENBURN_PI_SESSIONS;
|
|
91
|
+
if (env.PI_CODING_AGENT_SESSION_DIR) return env.PI_CODING_AGENT_SESSION_DIR;
|
|
92
|
+
return join(resolveAgentDir(env, home), "sessions");
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
const getConfigDir = () => resolveAgentDir(process.env, homedir());
|
|
96
|
+
|
|
97
|
+
const getSessionsDir = () => resolveSessionsDir(process.env, homedir());
|
|
98
|
+
|
|
93
99
|
export async function loadConfig(): Promise<TokenBurnConfig> {
|
|
94
100
|
try {
|
|
95
101
|
const raw = await readFile(join(getConfigDir(), "tokenburn.json"), "utf8");
|
|
@@ -102,13 +108,26 @@ export async function loadConfig(): Promise<TokenBurnConfig> {
|
|
|
102
108
|
statusWindow: (STATUS_WINDOWS as readonly string[]).includes(parsed.statusWindow)
|
|
103
109
|
? parsed.statusWindow
|
|
104
110
|
: DEFAULT_CONFIG.statusWindow,
|
|
105
|
-
budgets:
|
|
111
|
+
budgets: sanitizeBudgets(parsed.budgets),
|
|
106
112
|
};
|
|
107
113
|
} catch {
|
|
108
114
|
return freshConfig();
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
const BUDGET_KEYS = ["dayTokens", "weekTokens", "monthTokens", "dayCost", "weekCost", "monthCost"] as const;
|
|
119
|
+
|
|
120
|
+
/** Keep only known budget keys holding positive, finite numbers (the file is hand-editable). */
|
|
121
|
+
export function sanitizeBudgets(value: unknown): TokenBurnConfig["budgets"] {
|
|
122
|
+
const out: TokenBurnConfig["budgets"] = {};
|
|
123
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return out;
|
|
124
|
+
for (const key of BUDGET_KEYS) {
|
|
125
|
+
const n = (value as Record<string, unknown>)[key];
|
|
126
|
+
if (typeof n === "number" && Number.isFinite(n) && n > 0) out[key] = n;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
/** A config that shares no mutable state with DEFAULT_CONFIG. */
|
|
113
132
|
export function freshConfig(): TokenBurnConfig {
|
|
114
133
|
return { ...DEFAULT_CONFIG, budgets: {} };
|
|
@@ -172,6 +191,16 @@ async function parseFile(file: string): Promise<Turn[]> {
|
|
|
172
191
|
return turns;
|
|
173
192
|
}
|
|
174
193
|
|
|
194
|
+
/** Cache key for a file: mtime + size. `undefined` when it cannot be stat'ed. */
|
|
195
|
+
export function fileSignature(file: string): string | undefined {
|
|
196
|
+
try {
|
|
197
|
+
const st = statSync(file);
|
|
198
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
199
|
+
} catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
function listSessionFiles(root: string): string[] {
|
|
176
205
|
const files: string[] = [];
|
|
177
206
|
const walk = (dir: string) => {
|
|
@@ -209,13 +238,8 @@ async function loadAll(sessionsRoot: string): Promise<Turn[][]> {
|
|
|
209
238
|
|
|
210
239
|
const out: Turn[][] = [];
|
|
211
240
|
for (const file of files) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const st = statSync(file);
|
|
215
|
-
sig = `${st.mtimeMs}:${st.size}`;
|
|
216
|
-
} catch {
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
241
|
+
const sig = fileSignature(file);
|
|
242
|
+
if (sig === undefined) continue; // vanished between listing and stat
|
|
219
243
|
let entry = FILE_CACHE.get(file);
|
|
220
244
|
if (!entry || entry.sig !== sig) {
|
|
221
245
|
entry = { sig, turns: await parseFile(file) };
|
|
@@ -560,7 +584,8 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
560
584
|
|
|
561
585
|
pi.on("session_start", async (_event, ctx) => {
|
|
562
586
|
config = await loadConfig();
|
|
563
|
-
|
|
587
|
+
// A failing refresh must never break session start.
|
|
588
|
+
await updateUI(ctx).catch(() => {});
|
|
564
589
|
});
|
|
565
590
|
|
|
566
591
|
// Fire-and-forget: never block the agent loop on a stats refresh.
|
|
@@ -569,7 +594,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
569
594
|
});
|
|
570
595
|
|
|
571
596
|
pi.registerCommand("tokenburn", {
|
|
572
|
-
description: "TokenBurn: /tokenburn [day|week|month|year|all]
|
|
597
|
+
description: "TokenBurn: /tokenburn [day|week|month|year|all] sets the status bar; /tokenburn chart <day|week|month|year|all> shows the chart",
|
|
573
598
|
getArgumentCompletions: (prefix: string) => completions(prefix),
|
|
574
599
|
handler: async (args, ctx) => {
|
|
575
600
|
const cmd = parseCommand(args);
|
|
@@ -655,7 +680,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
655
680
|
rule,
|
|
656
681
|
...chart,
|
|
657
682
|
rule,
|
|
658
|
-
"/tokenburn day|week|month|year|all ·
|
|
683
|
+
"/tokenburn chart <day|week|month|year|all> · /tokenburn <day|week|month|year|all> sets the status bar · cycle · window · budget · cache",
|
|
659
684
|
].join("\n"),
|
|
660
685
|
"info",
|
|
661
686
|
);
|
|
@@ -684,24 +709,37 @@ export type Command =
|
|
|
684
709
|
| { kind: "invalid"; message: string };
|
|
685
710
|
|
|
686
711
|
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";
|
|
712
|
+
"Usage: /tokenburn [day|week|month|year|all] (status-bar period) · chart [day|week|month|year|all] · status <today|week|month|year|total> · cycle · window · budget <day|week|month> <tokens|$cost> · cache";
|
|
713
|
+
|
|
714
|
+
/** Words that pick the status-bar period: `day` shows today, `all` shows the all-time total. */
|
|
715
|
+
const WORD_TO_WINDOW: Record<string, Window> = {
|
|
716
|
+
day: "today",
|
|
717
|
+
today: "today",
|
|
718
|
+
week: "week",
|
|
719
|
+
month: "month",
|
|
720
|
+
year: "year",
|
|
721
|
+
all: "total",
|
|
722
|
+
total: "total",
|
|
723
|
+
};
|
|
688
724
|
|
|
725
|
+
/** Views the chart can show. */
|
|
689
726
|
const asView = (w: string | undefined): ReportView | undefined =>
|
|
690
|
-
w === "today" ? "day" : (REPORT_VIEWS as readonly string[]).includes(w ?? "") ? (w as ReportView) : undefined;
|
|
727
|
+
w === "today" ? "day" : w === "total" ? "all" : (REPORT_VIEWS as readonly string[]).includes(w ?? "") ? (w as ReportView) : undefined;
|
|
691
728
|
|
|
692
729
|
export function parseCommand(args: string | undefined): Command {
|
|
693
730
|
const [cmd, p1, p2] = (args ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
694
731
|
if (!cmd) return { kind: "report", view: "day" };
|
|
695
732
|
|
|
696
|
-
|
|
697
|
-
|
|
733
|
+
// /tokenburn day|week|month|year|all → what the bottom status line shows
|
|
734
|
+
const window = WORD_TO_WINDOW[cmd];
|
|
735
|
+
if (window) return { kind: "status", window };
|
|
698
736
|
|
|
699
737
|
switch (cmd) {
|
|
700
|
-
case "
|
|
701
|
-
case "
|
|
738
|
+
case "chart":
|
|
739
|
+
case "report": {
|
|
702
740
|
if (p1 === undefined) return { kind: "report", view: "day" };
|
|
703
741
|
const v = asView(p1);
|
|
704
|
-
return v ? { kind: "report", view: v } : { kind: "invalid", message: `Unknown
|
|
742
|
+
return v ? { kind: "report", view: v } : { kind: "invalid", message: `Unknown chart "${p1}". ${USAGE}` };
|
|
705
743
|
}
|
|
706
744
|
case "status":
|
|
707
745
|
return (STATUS_WINDOWS as readonly string[]).includes(p1 ?? "")
|
|
@@ -734,13 +772,22 @@ export interface CompletionItem {
|
|
|
734
772
|
description?: string;
|
|
735
773
|
}
|
|
736
774
|
|
|
775
|
+
const CHART_SPAN: Record<ReportView, string> = {
|
|
776
|
+
day: "last 14 days",
|
|
777
|
+
week: "last 8 weeks",
|
|
778
|
+
month: "last 6 months",
|
|
779
|
+
year: "last 5 years",
|
|
780
|
+
all: "whole history",
|
|
781
|
+
};
|
|
782
|
+
|
|
737
783
|
const TOP_LEVEL: CompletionItem[] = [
|
|
738
|
-
{ value: "day", label: "day", description: "
|
|
739
|
-
{ value: "week", label: "week", description: "
|
|
740
|
-
{ value: "month", label: "month", description: "
|
|
741
|
-
{ value: "year", label: "year", description: "
|
|
742
|
-
{ value: "all", label: "all", description: "
|
|
743
|
-
{ value: "
|
|
784
|
+
{ value: "day", label: "day", description: "Status bar: today" },
|
|
785
|
+
{ value: "week", label: "week", description: "Status bar: this week" },
|
|
786
|
+
{ value: "month", label: "month", description: "Status bar: this month" },
|
|
787
|
+
{ value: "year", label: "year", description: "Status bar: this year" },
|
|
788
|
+
{ value: "all", label: "all", description: "Status bar: all-time total" },
|
|
789
|
+
{ value: "chart ", label: "chart", description: "Report + chart: day, week, month, year or all" },
|
|
790
|
+
{ value: "status ", label: "status", description: "Set the status-bar period explicitly" },
|
|
744
791
|
{ value: "cycle", label: "cycle", description: "Rotate the status-bar period" },
|
|
745
792
|
{ value: "window", label: "window", description: "Toggle the panel below the editor" },
|
|
746
793
|
{ value: "budget ", label: "budget", description: "Set a day/week/month budget" },
|
|
@@ -750,16 +797,17 @@ const TOP_LEVEL: CompletionItem[] = [
|
|
|
750
797
|
/** `prefix` is the whole argument text typed so far (pi replaces it with `value`). */
|
|
751
798
|
export function completions(prefix: string): CompletionItem[] | null {
|
|
752
799
|
const text = (prefix ?? "").toLowerCase().replace(/^\s+/, "");
|
|
753
|
-
|
|
754
|
-
|
|
800
|
+
// `tail` = " " when the completed text still needs another word (a budget amount).
|
|
801
|
+
const sub = (head: string, words: readonly string[], desc: (w: string) => string, tail = ""): CompletionItem[] =>
|
|
802
|
+
words.map((w) => ({ value: `${head} ${w}${tail}`, label: w, description: desc(w) }));
|
|
755
803
|
|
|
756
804
|
let pool: CompletionItem[];
|
|
757
805
|
if (text.startsWith("status ")) pool = sub("status", STATUS_WINDOWS, (w) => `Show ${w} in the status bar`);
|
|
758
806
|
else if (text.startsWith("budget ") && !/^budget \S+ /.test(text))
|
|
759
|
-
pool = sub("budget", BUDGET_PERIODS, (w) => `Set the ${w} budget (tokens or $cost)
|
|
807
|
+
pool = sub("budget", BUDGET_PERIODS, (w) => `Set the ${w} budget (tokens or $cost)`, " ");
|
|
760
808
|
else if (/^(report|chart) /.test(text)) {
|
|
761
809
|
const head = text.split(" ")[0];
|
|
762
|
-
pool = sub(head, REPORT_VIEWS, (w) =>
|
|
810
|
+
pool = sub(head, REPORT_VIEWS, (w) => `Chart: ${CHART_SPAN[w as ReportView]}`);
|
|
763
811
|
} else pool = TOP_LEVEL;
|
|
764
812
|
|
|
765
813
|
const out = pool.filter((i) => i.value.startsWith(text));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tokenburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|