pi-tokenburn 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 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
- - **Views:** `/tokenburn day|week|month|year|all`. Status bar period: `/tokenburn status <window>` or `/tokenburn cycle`.
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` · `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 |
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 the above. Unknown input prints usage instead of failing.
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,6 +78,12 @@ 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
@@ -108,13 +108,26 @@ export async function loadConfig(): Promise<TokenBurnConfig> {
108
108
  statusWindow: (STATUS_WINDOWS as readonly string[]).includes(parsed.statusWindow)
109
109
  ? parsed.statusWindow
110
110
  : DEFAULT_CONFIG.statusWindow,
111
- budgets: typeof parsed.budgets === "object" && parsed.budgets !== null ? { ...parsed.budgets } : {},
111
+ budgets: sanitizeBudgets(parsed.budgets),
112
112
  };
113
113
  } catch {
114
114
  return freshConfig();
115
115
  }
116
116
  }
117
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
+
118
131
  /** A config that shares no mutable state with DEFAULT_CONFIG. */
119
132
  export function freshConfig(): TokenBurnConfig {
120
133
  return { ...DEFAULT_CONFIG, budgets: {} };
@@ -571,7 +584,8 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
571
584
 
572
585
  pi.on("session_start", async (_event, ctx) => {
573
586
  config = await loadConfig();
574
- await updateUI(ctx);
587
+ // A failing refresh must never break session start.
588
+ await updateUI(ctx).catch(() => {});
575
589
  });
576
590
 
577
591
  // Fire-and-forget: never block the agent loop on a stats refresh.
@@ -580,7 +594,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
580
594
  });
581
595
 
582
596
  pi.registerCommand("tokenburn", {
583
- description: "TokenBurn: /tokenburn [day|week|month|year|all] chart, status <window>, cycle, window, budget, cache",
597
+ description: "TokenBurn: /tokenburn [day|week|month|year|all] sets the status bar; /tokenburn chart <day|week|month|year|all> shows the chart",
584
598
  getArgumentCompletions: (prefix: string) => completions(prefix),
585
599
  handler: async (args, ctx) => {
586
600
  const cmd = parseCommand(args);
@@ -666,7 +680,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
666
680
  rule,
667
681
  ...chart,
668
682
  rule,
669
- "/tokenburn day|week|month|year|all · status <window> · cycle · window · budget · cache",
683
+ "/tokenburn chart <day|week|month|year|all> · /tokenburn <day|week|month|year|all> sets the status bar · cycle · window · budget · cache",
670
684
  ].join("\n"),
671
685
  "info",
672
686
  );
@@ -695,24 +709,37 @@ export type Command =
695
709
  | { kind: "invalid"; message: string };
696
710
 
697
711
  export const USAGE =
698
- "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
+ };
699
724
 
725
+ /** Views the chart can show. */
700
726
  const asView = (w: string | undefined): ReportView | undefined =>
701
- 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;
702
728
 
703
729
  export function parseCommand(args: string | undefined): Command {
704
730
  const [cmd, p1, p2] = (args ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
705
731
  if (!cmd) return { kind: "report", view: "day" };
706
732
 
707
- const view = asView(cmd);
708
- if (view) return { kind: "report", view };
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 };
709
736
 
710
737
  switch (cmd) {
711
- case "report":
712
- case "chart": {
738
+ case "chart":
739
+ case "report": {
713
740
  if (p1 === undefined) return { kind: "report", view: "day" };
714
741
  const v = asView(p1);
715
- return v ? { kind: "report", view: v } : { kind: "invalid", message: `Unknown view "${p1}". ${USAGE}` };
742
+ return v ? { kind: "report", view: v } : { kind: "invalid", message: `Unknown chart "${p1}". ${USAGE}` };
716
743
  }
717
744
  case "status":
718
745
  return (STATUS_WINDOWS as readonly string[]).includes(p1 ?? "")
@@ -745,13 +772,22 @@ export interface CompletionItem {
745
772
  description?: string;
746
773
  }
747
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
+
748
783
  const TOP_LEVEL: CompletionItem[] = [
749
- { value: "day", label: "day", description: "Report + daily chart (last 14 days)" },
750
- { value: "week", label: "week", description: "Report + weekly chart (last 8 weeks)" },
751
- { value: "month", label: "month", description: "Report + monthly chart (last 6 months)" },
752
- { value: "year", label: "year", description: "Report + yearly chart (last 5 years)" },
753
- { value: "all", label: "all", description: "Report + whole history" },
754
- { value: "status ", label: "status", description: "Set the status-bar period" },
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" },
755
791
  { value: "cycle", label: "cycle", description: "Rotate the status-bar period" },
756
792
  { value: "window", label: "window", description: "Toggle the panel below the editor" },
757
793
  { value: "budget ", label: "budget", description: "Set a day/week/month budget" },
@@ -771,7 +807,7 @@ export function completions(prefix: string): CompletionItem[] | null {
771
807
  pool = sub("budget", BUDGET_PERIODS, (w) => `Set the ${w} budget (tokens or $cost)`, " ");
772
808
  else if (/^(report|chart) /.test(text)) {
773
809
  const head = text.split(" ")[0];
774
- pool = sub(head, REPORT_VIEWS, (w) => `${head} for ${w}`);
810
+ pool = sub(head, REPORT_VIEWS, (w) => `Chart: ${CHART_SPAN[w as ReportView]}`);
775
811
  } else pool = TOP_LEVEL;
776
812
 
777
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.1",
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",