pi-tokenburn 0.6.0 → 0.7.1
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 +5 -3
- package/extensions/core/aggregate.ts +4 -5
- package/extensions/core/config.ts +7 -3
- package/extensions/core/parser.ts +1 -1
- package/extensions/core/select.ts +9 -0
- package/extensions/features/breakdown/query.ts +4 -5
- package/extensions/features/report/chart.ts +11 -4
- package/extensions/features/status-line/status.ts +1 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -232,15 +232,17 @@ CI scripts are [nushell](https://www.nushell.sh) (`scripts/`), the same ones loc
|
|
|
232
232
|
| Workflow | When | What |
|
|
233
233
|
|---|---|---|
|
|
234
234
|
| **CI** | push / PR | quality gate on Linux; tests on macOS and Windows, and in extreme time zones |
|
|
235
|
-
| **Auto-merge library updates** | CI finished on a Dependabot PR | **patch and minor** updates (GitHub Actions) are merged automatically, but only **after CI is green** on that exact commit; a **major** update waits for you.
|
|
236
|
-
| **
|
|
235
|
+
| **Auto-merge library updates** | CI finished on a Dependabot PR | **patch and minor** updates (GitHub Actions) are merged automatically, but only **after CI is green** on that exact commit; a **major** update waits for you. Needs the `GH_PAT` secret: action bumps change `.github/workflows/*`, which `GITHUB_TOKEN` may not merge |
|
|
236
|
+
| **Gitea Actions Update** | Mondays 03:15 UTC | Gitea has no Dependabot: moves the sha pins in `.gitea/workflows` to the newest release of the **same major** and commits `ci(deps)`; a new major is only reported |
|
|
237
|
+
| **Nightly Dependency Update** | every night (GitHub 02:00 UTC) and after each auto-merge | `bun update` within ranges, verify on all platforms, commit `chore(deps)`; then **build, tag and publish a new patch** whenever a library was upgraded or merged, or `feat`/`fix`/`perf` commits are waiting since the last tag |
|
|
238
|
+
| **Mirror GitHub** (Gitea) | hourly | Gitea has no nightly of its own: fast-forwards its `main` to GitHub's and pushes the new tags (starting Gitea's release for each), so the hosts never diverge; fails loudly if they did |
|
|
237
239
|
| **Release** | tag `vX.Y.Z` | validates the tag against `package.json`, runs the gate, `npm publish` (idempotent, with provenance), creates the release |
|
|
238
240
|
|
|
239
241
|
So library updates need no human: they are merged once CI passes, built, versioned and published as a patch. A downgrade, a failing check, or a major update stops the chain and waits for review.
|
|
240
242
|
|
|
241
243
|
Manual release: `just release patch` (or `minor` / `major` / `X.Y.Z`) bumps, tags and pushes to GitHub; `just release-all patch` pushes to every configured remote; `just push-release-all` pushes an existing bump.
|
|
242
244
|
|
|
243
|
-
**Secrets** (repo settings): `NPM_TOKEN` is an npm *granular access token* with publish rights and "bypass 2FA" (required for CI publishing).
|
|
245
|
+
**Secrets** (repo settings): `NPM_TOKEN` is an npm *granular access token* with publish rights and "bypass 2FA" (required for CI publishing). `GH_PAT` (classic PAT, `repo` + `workflow`: needed to auto-merge GitHub Actions bumps; also lets a tag push trigger the release itself). Optional: `GITEA_TOKEN` (Gitea).
|
|
244
246
|
|
|
245
247
|
Commits follow [Conventional Commits](https://www.conventionalcommits.org); the changelog is generated by git-cliff.
|
|
246
248
|
|
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
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
|
-
|
|
144
|
-
await
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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", `${
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|