pi-tokenburn 0.6.0 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  /** Aggregation of parsed turns into period totals and time series. */
2
2
  import { loadAll, type Turn } from "./parser.ts";
3
- import { selectTurns, type UsageOptions } from "./select.ts";
3
+ import { cacheValue, selectTurns, type UsageOptions } from "./select.ts";
4
4
  import { bucketLabel, bucketStart, type Unit } from "./time.ts";
5
5
  import type { PeriodStats, TokenBurnReport } from "./types.ts";
6
6
 
@@ -56,10 +56,9 @@ export async function collectUsage(
56
56
  p.cacheRead += t.cacheRead;
57
57
  p.cacheWrite += t.cacheWrite;
58
58
  p.cost += t.cost;
59
- if (t.input > 0 && t.costInput > 0) {
60
- p.cacheBase += (t.cacheRead * t.costInput) / t.input;
61
- p.cacheActual += t.costCacheRead;
62
- }
59
+ const cv = cacheValue(t);
60
+ p.cacheBase += cv.base;
61
+ p.cacheActual += cv.actual;
63
62
  };
64
63
 
65
64
  for (const turns of await loadSelected(sessionsRoot, opts)) {
@@ -1,7 +1,7 @@
1
1
  /** Config file (tokenburn.json), path resolution, budgets. */
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { readFile, writeFile, mkdir } from "node:fs/promises";
4
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
5
5
  import { sanitizeFilter, sanitizePrices, type PriceMode, type PriceTable, type UsageFilter } from "./select.ts";
6
6
  import { STATUS_WINDOWS, type PeriodStats, type Window } from "./types.ts";
7
7
 
@@ -140,8 +140,12 @@ export function freshConfig(): TokenBurnConfig {
140
140
  }
141
141
 
142
142
  export async function saveConfig(cfg: TokenBurnConfig): Promise<void> {
143
- await mkdir(getConfigDir(), { recursive: true });
144
- await writeFile(join(getConfigDir(), "tokenburn.json"), JSON.stringify(cfg, null, 2) + "\n", "utf8");
143
+ const dir = getConfigDir();
144
+ await mkdir(dir, { recursive: true });
145
+ // Write beside, then rename: a crash or a second pi session never leaves half a file behind.
146
+ const tmp = join(dir, `tokenburn.json.${process.pid}.tmp`);
147
+ await writeFile(tmp, JSON.stringify(cfg, null, 2) + "\n", "utf8");
148
+ await rename(tmp, join(dir, "tokenburn.json"));
145
149
  }
146
150
 
147
151
  export function budgetsFor(cfg: TokenBurnConfig, w: Window): { tokens?: number; cost?: number } {
@@ -64,13 +64,13 @@ const GUARD_BYTES = 64;
64
64
  const HEAD_BYTES = 256;
65
65
  export const CHUNK_BYTES = 1 << 20;
66
66
 
67
- /** A Turn from one JSONL line, or undefined when it carries no usage. */
68
67
  /** A finite number, else 0 (logs can hold 1e999, strings, junk). */
69
68
  const finite = (v: unknown): number => {
70
69
  const n = Number(v);
71
70
  return Number.isFinite(n) ? n : 0;
72
71
  };
73
72
 
73
+ /** A Turn from one JSONL line, or undefined when it carries no usage. */
74
74
  function turnFromLine(line: Buffer, meta: Meta): Turn | undefined {
75
75
  if (meta.cwd === undefined && line.includes('"type":"session"')) {
76
76
  try {
@@ -103,3 +103,12 @@ export function selectTurns(turns: Turn[], opts: UsageOptions | undefined): Turn
103
103
  }
104
104
  return out;
105
105
  }
106
+
107
+ /**
108
+ * What the turn's cached tokens would have cost as plain input (`base`) and what they did cost
109
+ * (`actual`). Both 0 when the log has no input price for the turn (then no savings are claimed).
110
+ */
111
+ export function cacheValue(t: Turn): { base: number; actual: number } {
112
+ if (!(t.input > 0 && t.costInput > 0)) return { base: 0, actual: 0 };
113
+ return { base: (t.cacheRead * t.costInput) / t.input, actual: t.costCacheRead };
114
+ }
@@ -1,6 +1,6 @@
1
1
  /** Grouped usage queries: per model, provider, project and session, over a period. */
2
2
  import { HOUR_MS, loadSelected, turnTokens } from "../../core/aggregate.ts";
3
- import type { UsageOptions } from "../../core/select.ts";
3
+ import { cacheValue, type UsageOptions } from "../../core/select.ts";
4
4
  import { bucketStart } from "../../core/time.ts";
5
5
 
6
6
  export const PERIODS = ["hour", "day", "week", "month", "year", "all"] as const;
@@ -106,10 +106,9 @@ export async function collectGroups(
106
106
  r.turns++;
107
107
  if (t.ts < r.first) r.first = t.ts;
108
108
  if (t.ts > r.last) r.last = t.ts;
109
- if (t.input > 0 && t.costInput > 0) {
110
- r.cacheBase += (t.cacheRead * t.costInput) / t.input;
111
- r.cacheActual += t.costCacheRead;
112
- }
109
+ const cv = cacheValue(t);
110
+ r.cacheBase += cv.base;
111
+ r.cacheActual += cv.actual;
113
112
  if (!seen.has(key)) {
114
113
  seen.add(key);
115
114
  r.sessions++;
@@ -68,21 +68,28 @@ export function renderChart(
68
68
  if (h >= row) cell = "█";
69
69
  else if (h > row - 1 && series[i].tokens > 0) cell = SPARK[Math.min(7, Math.max(0, Math.floor((h - (row - 1)) * 8) - 1))];
70
70
  else if (row === 1 && series[i].tokens > 0) cell = "▁";
71
+ else if (row === 1 && budgetRow !== 1) cell = "·"; // an empty bucket, not a missing one
71
72
  else cell = row === budgetRow ? "┄" : " ";
72
- const filled = cell !== " " && cell !== "┄";
73
+ const filled = cell !== " " && cell !== "┄" && cell !== "·";
73
74
  const body = cell.repeat(barW);
74
- line += filled ? paint(kindOf(i), body) : cell === "┄" ? paint("budget", body) : body;
75
+ line += filled ? paint(kindOf(i), body) : cell === "┄" ? paint("budget", body) : cell === "·" ? paint("axis", body) : body;
75
76
  line += row === budgetRow ? paint("budget", "┄") : " ";
76
77
  }
77
78
  lines.push(line);
78
79
  }
79
80
 
80
- lines.push(paint("axis", `${"".padStart(yW)} └${"─".repeat(series.length * colW)}`));
81
+ lines.push(paint("axis", `${yLabels[2].padStart(yW)} └${"─".repeat(series.length * colW)}`));
81
82
  const pad = " ".repeat(yW + 2);
82
83
  const cols = series.map((p, i) => ({ l: columnLabels(p, unit), now: i === series.length - 1 }));
83
84
  lines.push(pad + cols.map((c) => c.l[0].padEnd(colW)).join(""));
84
85
  lines.push(paint("dim", pad + cols.map((c) => c.l[1].padEnd(colW)).join("")));
85
- lines.push(pad + cols.map((c) => (c.now ? paint("now", "▲".padEnd(colW)) : " ".repeat(colW))).join(""));
86
+ // The marker under the current bucket, with its value so you need not read it off the axis.
87
+ const latest = series[series.length - 1];
88
+ lines.push(
89
+ pad +
90
+ cols.map((c) => (c.now ? paint("now", "▲".padEnd(colW)) : " ".repeat(colW))).join("") +
91
+ (latest ? paint("now", `now ${formatNum(latest.tokens)} (${formatCost(latest.cost)})`) : ""),
92
+ );
86
93
 
87
94
  const avg = series.length ? total / series.length : 0;
88
95
  lines.push(
@@ -1,12 +1,11 @@
1
1
  /** Status line: window start, burn rate / cache hit / savings extras for the status window (pure). */
2
2
  import type { TokenBurnConfig } from "../../core/config.ts";
3
3
  import { formatCost, formatNum } from "../../core/format.ts";
4
+ import { HOUR_MS as HOUR } from "../../core/aggregate.ts";
4
5
  import { bucketStart } from "../../core/time.ts";
5
6
  import type { PeriodStats, Rate, TokenBurnReport, Window } from "../../core/types.ts";
6
7
  import { cacheHitRatio } from "../breakdown/query.ts";
7
8
 
8
- const HOUR = 3_600_000;
9
-
10
9
  /** Start of a status window (`hour` = rolling last 60 minutes, `total` = 0). */
11
10
  export function windowStart(w: Window, now: number): number {
12
11
  if (w === "hour") return now - HOUR;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tokenburn",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
@@ -24,6 +24,9 @@
24
24
  "peerDependencies": {
25
25
  "@earendil-works/pi-coding-agent": "*"
26
26
  },
27
+ "overrides": {
28
+ "undici": "8.12.3"
29
+ },
27
30
  "devDependencies": {
28
31
  "@earendil-works/pi-coding-agent": "^1.4.0",
29
32
  "@types/bun": "^1.4.4",