pi-tokenburn 0.1.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 +54 -4
- package/extensions/tokenburn.ts +457 -87
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ Totals match the `tokenburn` CLI exactly.
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Status bar:** `๐ฅ 1.20M ($52.79) [today]`, turns red with `OVER BUDGET` past your limit.
|
|
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`.
|
|
12
14
|
- **Window panel (optional):** Today / Week / Month / Total below the editor, with in/out tokens and cost.
|
|
13
15
|
- **Budgets** per day, week and month, in tokens and/or dollars.
|
|
14
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.
|
|
@@ -25,13 +27,37 @@ pi install git:github.com/sorinirimies/pi-tokenburn
|
|
|
25
27
|
|
|
26
28
|
| Command | What it does |
|
|
27
29
|
|---|---|
|
|
28
|
-
| `/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 |
|
|
29
38
|
| `/tokenburn window` | Toggle the panel below the editor |
|
|
30
|
-
| `/tokenburn status <today\|week\|month\|total>` | Period shown in the status bar |
|
|
31
39
|
| `/tokenburn budget <day\|week\|month> <tokens>` | Token budget, e.g. `budget day 5000000` |
|
|
32
40
|
| `/tokenburn budget <day\|week\|month> $<cost>` | Dollar budget, e.g. `budget week $50` |
|
|
33
41
|
| `/tokenburn cache` | Toggle counting cache read/write tokens |
|
|
34
42
|
|
|
43
|
+
Tab-completion works for all of the above. Unknown input prints usage instead of failing.
|
|
44
|
+
|
|
45
|
+
Chart example (`/tokenburn`, with a 600M day budget):
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Daily tokens ยท last 14 days โ budget 600.00M
|
|
49
|
+
1.13B โ โโ
|
|
50
|
+
โ โโ
|
|
51
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
52
|
+
565.16M โ โโ โโ โโ โโ
|
|
53
|
+
โ โ
โ
โโ โโ โโ โโ โโ โโ โโ โโ โโ
|
|
54
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
55
|
+
26 27 28 29 30 01 02 03 04 05 06 07 08 09
|
|
56
|
+
Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr
|
|
57
|
+
โฒ
|
|
58
|
+
ฮฃ 3.83B ($1264.30) ยท avg 273.80M/day ยท peak 1.13B (09-29 Tue)
|
|
59
|
+
```
|
|
60
|
+
|
|
35
61
|
Weeks start on Monday, in local time.
|
|
36
62
|
|
|
37
63
|
## Config
|
|
@@ -41,6 +67,7 @@ Weeks start on Monday, in local time.
|
|
|
41
67
|
```json
|
|
42
68
|
{
|
|
43
69
|
"includeCache": true,
|
|
70
|
+
"chartColor": true,
|
|
44
71
|
"showStatus": true,
|
|
45
72
|
"showWidget": false,
|
|
46
73
|
"statusWindow": "today",
|
|
@@ -50,14 +77,37 @@ Weeks start on Monday, in local time.
|
|
|
50
77
|
|
|
51
78
|
- `includeCache` (default `true`): count cache read/write tokens in totals, like the `tokenburn` CLI. Cache reads dominate (often 95%+ of tokens) but are billed far cheaper; set `false` to budget on input + output only.
|
|
52
79
|
|
|
80
|
+
- `chartColor` (default `true`): color the chart using your pi theme (current period green, over-budget red, budget line yellow). Set `false` for plain text.
|
|
81
|
+
|
|
53
82
|
Environment overrides for the session directory: `TOKENBURN_PI_SESSIONS`, `PI_CODING_AGENT_SESSION_DIR`, `PI_CODING_AGENT_DIR`.
|
|
54
83
|
|
|
55
|
-
##
|
|
84
|
+
## Development
|
|
56
85
|
|
|
57
86
|
```bash
|
|
58
|
-
bun
|
|
87
|
+
bun install
|
|
88
|
+
just check # typecheck + tests + pack check + nushell tests (what CI runs)
|
|
89
|
+
just test # bun test only
|
|
59
90
|
```
|
|
60
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
|
+
|
|
61
111
|
## Scope
|
|
62
112
|
|
|
63
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,15 +44,21 @@ 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;
|
|
57
|
+
/** Color the report chart (set false if your terminal garbles ANSI in notifications). */
|
|
58
|
+
chartColor: boolean;
|
|
53
59
|
showStatus: boolean;
|
|
54
60
|
showWidget: boolean;
|
|
55
|
-
statusWindow:
|
|
61
|
+
statusWindow: Window;
|
|
56
62
|
budgets: {
|
|
57
63
|
dayTokens?: number;
|
|
58
64
|
weekTokens?: number;
|
|
@@ -65,6 +71,7 @@ export interface TokenBurnConfig {
|
|
|
65
71
|
|
|
66
72
|
const DEFAULT_CONFIG: TokenBurnConfig = {
|
|
67
73
|
includeCache: true,
|
|
74
|
+
chartColor: true,
|
|
68
75
|
showStatus: true,
|
|
69
76
|
showWidget: false,
|
|
70
77
|
statusWindow: "today",
|
|
@@ -89,18 +96,24 @@ export async function loadConfig(): Promise<TokenBurnConfig> {
|
|
|
89
96
|
const parsed = JSON.parse(raw);
|
|
90
97
|
return {
|
|
91
98
|
includeCache: typeof parsed.includeCache === "boolean" ? parsed.includeCache : DEFAULT_CONFIG.includeCache,
|
|
99
|
+
chartColor: typeof parsed.chartColor === "boolean" ? parsed.chartColor : DEFAULT_CONFIG.chartColor,
|
|
92
100
|
showStatus: typeof parsed.showStatus === "boolean" ? parsed.showStatus : DEFAULT_CONFIG.showStatus,
|
|
93
101
|
showWidget: typeof parsed.showWidget === "boolean" ? parsed.showWidget : DEFAULT_CONFIG.showWidget,
|
|
94
|
-
statusWindow:
|
|
102
|
+
statusWindow: (STATUS_WINDOWS as readonly string[]).includes(parsed.statusWindow)
|
|
95
103
|
? parsed.statusWindow
|
|
96
104
|
: DEFAULT_CONFIG.statusWindow,
|
|
97
|
-
budgets: typeof parsed.budgets === "object" && parsed.budgets !== null ? parsed.budgets : {},
|
|
105
|
+
budgets: typeof parsed.budgets === "object" && parsed.budgets !== null ? { ...parsed.budgets } : {},
|
|
98
106
|
};
|
|
99
107
|
} catch {
|
|
100
|
-
return
|
|
108
|
+
return freshConfig();
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
/** A config that shares no mutable state with DEFAULT_CONFIG. */
|
|
113
|
+
export function freshConfig(): TokenBurnConfig {
|
|
114
|
+
return { ...DEFAULT_CONFIG, budgets: {} };
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
export async function saveConfig(cfg: TokenBurnConfig): Promise<void> {
|
|
105
118
|
await mkdir(getConfigDir(), { recursive: true });
|
|
106
119
|
await writeFile(join(getConfigDir(), "tokenburn.json"), JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
@@ -188,29 +201,54 @@ const emptyStats = (): PeriodStats => ({
|
|
|
188
201
|
sessions: 0,
|
|
189
202
|
});
|
|
190
203
|
|
|
204
|
+
/** Refresh the per-file cache and return every session file's parsed turns. */
|
|
205
|
+
async function loadAll(sessionsRoot: string): Promise<Turn[][]> {
|
|
206
|
+
const files = listSessionFiles(sessionsRoot);
|
|
207
|
+
const live = new Set(files);
|
|
208
|
+
for (const key of FILE_CACHE.keys()) if (!live.has(key)) FILE_CACHE.delete(key);
|
|
209
|
+
|
|
210
|
+
const out: Turn[][] = [];
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
let sig: string;
|
|
213
|
+
try {
|
|
214
|
+
const st = statSync(file);
|
|
215
|
+
sig = `${st.mtimeMs}:${st.size}`;
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
let entry = FILE_CACHE.get(file);
|
|
220
|
+
if (!entry || entry.sig !== sig) {
|
|
221
|
+
entry = { sig, turns: await parseFile(file) };
|
|
222
|
+
FILE_CACHE.set(file, entry);
|
|
223
|
+
}
|
|
224
|
+
out.push(entry.turns);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const turnTokens = (t: Turn, includeCache: boolean) =>
|
|
230
|
+
t.input + t.output + (includeCache ? t.cacheRead + t.cacheWrite : 0);
|
|
231
|
+
|
|
191
232
|
export async function collectUsage(
|
|
192
233
|
sessionsRoot: string,
|
|
193
234
|
now: Date = new Date(),
|
|
194
235
|
includeCache = true,
|
|
195
236
|
): Promise<TokenBurnReport> {
|
|
196
|
-
const startOfDay =
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
237
|
+
const startOfDay = bucketStart(now.getTime(), "day");
|
|
238
|
+
const startOfWeek = bucketStart(now.getTime(), "week");
|
|
239
|
+
const startOfMonth = bucketStart(now.getTime(), "month");
|
|
240
|
+
const startOfYear = bucketStart(now.getTime(), "year");
|
|
200
241
|
|
|
201
242
|
const report: TokenBurnReport = {
|
|
202
243
|
today: emptyStats(),
|
|
203
244
|
week: emptyStats(),
|
|
204
245
|
month: emptyStats(),
|
|
246
|
+
year: emptyStats(),
|
|
205
247
|
total: emptyStats(),
|
|
206
248
|
};
|
|
207
249
|
|
|
208
|
-
const files = listSessionFiles(sessionsRoot);
|
|
209
|
-
const live = new Set(files);
|
|
210
|
-
for (const key of FILE_CACHE.keys()) if (!live.has(key)) FILE_CACHE.delete(key);
|
|
211
|
-
|
|
212
250
|
const add = (p: PeriodStats, t: Turn) => {
|
|
213
|
-
p.tokens += t
|
|
251
|
+
p.tokens += turnTokens(t, includeCache);
|
|
214
252
|
p.input += t.input;
|
|
215
253
|
p.output += t.output;
|
|
216
254
|
p.cacheRead += t.cacheRead;
|
|
@@ -218,29 +256,18 @@ export async function collectUsage(
|
|
|
218
256
|
p.cost += t.cost;
|
|
219
257
|
};
|
|
220
258
|
|
|
221
|
-
for (const
|
|
222
|
-
let sig: string;
|
|
223
|
-
try {
|
|
224
|
-
const st = statSync(file);
|
|
225
|
-
sig = `${st.mtimeMs}:${st.size}`;
|
|
226
|
-
} catch {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
let entry = FILE_CACHE.get(file);
|
|
230
|
-
if (!entry || entry.sig !== sig) {
|
|
231
|
-
entry = { sig, turns: await parseFile(file) };
|
|
232
|
-
FILE_CACHE.set(file, entry);
|
|
233
|
-
}
|
|
234
|
-
|
|
259
|
+
for (const turns of await loadAll(sessionsRoot)) {
|
|
235
260
|
let maxTs = 0;
|
|
236
|
-
for (const t of
|
|
261
|
+
for (const t of turns) {
|
|
237
262
|
if (t.ts > maxTs) maxTs = t.ts;
|
|
238
263
|
add(report.total, t);
|
|
264
|
+
if (t.ts >= startOfYear) add(report.year, t);
|
|
239
265
|
if (t.ts >= startOfMonth) add(report.month, t);
|
|
240
266
|
if (t.ts >= startOfWeek) add(report.week, t);
|
|
241
267
|
if (t.ts >= startOfDay) add(report.today, t);
|
|
242
268
|
}
|
|
243
|
-
if (
|
|
269
|
+
if (turns.length > 0) report.total.sessions++;
|
|
270
|
+
if (maxTs >= startOfYear) report.year.sessions++;
|
|
244
271
|
if (maxTs >= startOfMonth) report.month.sessions++;
|
|
245
272
|
if (maxTs >= startOfWeek) report.week.sessions++;
|
|
246
273
|
if (maxTs >= startOfDay) report.today.sessions++;
|
|
@@ -249,13 +276,212 @@ export async function collectUsage(
|
|
|
249
276
|
return report;
|
|
250
277
|
}
|
|
251
278
|
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Time series + chart
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
export type Unit = "day" | "week" | "month" | "year";
|
|
284
|
+
|
|
285
|
+
/** Local-time start of the day / Monday-week / month containing `ts`. */
|
|
286
|
+
export function bucketStart(ts: number, unit: Unit): number {
|
|
287
|
+
const d = new Date(ts);
|
|
288
|
+
if (unit === "day") return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
289
|
+
if (unit === "week") {
|
|
290
|
+
const dow = (d.getDay() + 6) % 7; // Monday = 0
|
|
291
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow).getTime();
|
|
292
|
+
}
|
|
293
|
+
if (unit === "year") return new Date(d.getFullYear(), 0, 1).getTime();
|
|
294
|
+
return new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const pad2 = (n: number) => String(n).padStart(2, "0");
|
|
298
|
+
|
|
299
|
+
export function bucketLabel(start: number, unit: Unit): string {
|
|
300
|
+
const d = new Date(start);
|
|
301
|
+
if (unit === "year") return String(d.getFullYear());
|
|
302
|
+
if (unit === "month") return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`;
|
|
303
|
+
const base = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
304
|
+
return unit === "day" ? `${base} ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]}` : `${base} wk`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface SeriesPoint {
|
|
308
|
+
start: number;
|
|
309
|
+
label: string;
|
|
310
|
+
tokens: number;
|
|
311
|
+
cost: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const DEFAULT_COUNT: Record<Unit, number> = { day: 14, week: 8, month: 6, year: 5 };
|
|
315
|
+
|
|
316
|
+
/** Last `count` buckets ending at `now`, oldest first, empty buckets zero-filled. */
|
|
317
|
+
export async function collectSeries(
|
|
318
|
+
sessionsRoot: string,
|
|
319
|
+
unit: Unit,
|
|
320
|
+
count = DEFAULT_COUNT[unit],
|
|
321
|
+
now: Date = new Date(),
|
|
322
|
+
includeCache = true,
|
|
323
|
+
): Promise<SeriesPoint[]> {
|
|
324
|
+
const starts: number[] = [];
|
|
325
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
326
|
+
const y = now.getFullYear();
|
|
327
|
+
const m = now.getMonth();
|
|
328
|
+
const day = now.getDate();
|
|
329
|
+
const anchor =
|
|
330
|
+
unit === "day"
|
|
331
|
+
? new Date(y, m, day - i)
|
|
332
|
+
: unit === "week"
|
|
333
|
+
? new Date(y, m, day - i * 7)
|
|
334
|
+
: unit === "year"
|
|
335
|
+
? new Date(y - i, 0, 1)
|
|
336
|
+
: new Date(y, m - i, 1);
|
|
337
|
+
starts.push(bucketStart(anchor.getTime(), unit));
|
|
338
|
+
}
|
|
339
|
+
const points = new Map<number, SeriesPoint>(
|
|
340
|
+
starts.map((start) => [start, { start, label: bucketLabel(start, unit), tokens: 0, cost: 0 }]),
|
|
341
|
+
);
|
|
342
|
+
for (const turns of await loadAll(sessionsRoot)) {
|
|
343
|
+
for (const t of turns) {
|
|
344
|
+
const pt = points.get(bucketStart(t.ts, unit));
|
|
345
|
+
if (!pt) continue;
|
|
346
|
+
pt.tokens += turnTokens(t, includeCache);
|
|
347
|
+
pt.cost += t.cost;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return starts.map((s) => points.get(s)!);
|
|
351
|
+
}
|
|
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
|
+
|
|
374
|
+
const EIGHTHS = ["", "โ", "โ", "โ", "โ", "โ", "โ", "โ"];
|
|
375
|
+
const SPARK = "โโโโโ
โโโ";
|
|
376
|
+
|
|
377
|
+
/** Horizontal bar of `value/max` over `width` cells, with 1/8-cell precision. */
|
|
378
|
+
export function bar(value: number, max: number, width: number): string {
|
|
379
|
+
if (max <= 0 || value <= 0) return "";
|
|
380
|
+
const eighths = Math.max(1, Math.round((value / max) * width * 8));
|
|
381
|
+
const full = Math.floor(eighths / 8);
|
|
382
|
+
return "โ".repeat(full) + EIGHTHS[eighths % 8];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function sparkline(values: number[]): string {
|
|
386
|
+
const max = Math.max(...values, 0);
|
|
387
|
+
if (max <= 0) return SPARK[0].repeat(values.length);
|
|
388
|
+
return values.map((v) => SPARK[Math.min(7, Math.round((v / max) * 7))]).join("");
|
|
389
|
+
}
|
|
390
|
+
|
|
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" };
|
|
393
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
394
|
+
|
|
395
|
+
export type Paint = (kind: "bar" | "now" | "over" | "axis" | "budget" | "dim" | "title", text: string) => string;
|
|
396
|
+
const plain: Paint = (_k, t) => t;
|
|
397
|
+
|
|
398
|
+
/** Two header rows for a column: [top, bottom]. */
|
|
399
|
+
function columnLabels(p: SeriesPoint, unit: Unit): [string, string] {
|
|
400
|
+
const d = new Date(p.start);
|
|
401
|
+
if (unit === "day") return [pad2(d.getDate()), ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][d.getDay()]];
|
|
402
|
+
if (unit === "week") return [`${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`, "wk"];
|
|
403
|
+
if (unit === "year") return [String(d.getFullYear()), ""];
|
|
404
|
+
return [MONTHS[d.getMonth()], String(d.getFullYear())];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Vertical column chart: y-axis, 1/8-row precision, optional budget line,
|
|
409
|
+
* highlighted current bucket and over-budget columns, summary footer.
|
|
410
|
+
*/
|
|
411
|
+
export function renderChart(
|
|
412
|
+
series: SeriesPoint[],
|
|
413
|
+
unit: Unit,
|
|
414
|
+
opts: { height?: number; budget?: { tokens?: number; cost?: number }; paint?: Paint; title?: string } = {},
|
|
415
|
+
): string[] {
|
|
416
|
+
const paint = opts.paint ?? plain;
|
|
417
|
+
const height = opts.height ?? 8;
|
|
418
|
+
const colW = unit === "day" ? 3 : 6; // bar cells + 1 gap
|
|
419
|
+
const barW = colW - 1;
|
|
420
|
+
|
|
421
|
+
const peakPoint = series.reduce((m, p) => (p.tokens > m.tokens ? p : m), series[0]);
|
|
422
|
+
const dataMax = peakPoint?.tokens ?? 0;
|
|
423
|
+
const budgetTokens = opts.budget?.tokens;
|
|
424
|
+
const scaleMax = Math.max(dataMax, budgetTokens ?? 0);
|
|
425
|
+
const total = series.reduce((a, p) => a + p.tokens, 0);
|
|
426
|
+
const totalCost = series.reduce((a, p) => a + p.cost, 0);
|
|
427
|
+
|
|
428
|
+
const yLabels = [formatNum(scaleMax), formatNum(scaleMax / 2), "0"];
|
|
429
|
+
const yW = Math.max(...yLabels.map((l) => l.length)) + 1;
|
|
430
|
+
const labelFor = (row: number) => (row === height ? yLabels[0] : row === Math.ceil(height / 2) ? yLabels[1] : "");
|
|
431
|
+
const budgetRow = budgetTokens && scaleMax > 0 ? Math.max(1, Math.ceil((budgetTokens / scaleMax) * height)) : 0;
|
|
432
|
+
|
|
433
|
+
const isOver = (p: SeriesPoint) =>
|
|
434
|
+
opts.budget ? isOverBudget({ tokens: p.tokens, cost: p.cost } as PeriodStats, opts.budget) : false;
|
|
435
|
+
const kindOf = (i: number): "bar" | "now" | "over" =>
|
|
436
|
+
isOver(series[i]) ? "over" : i === series.length - 1 ? "now" : "bar";
|
|
437
|
+
|
|
438
|
+
const lines: string[] = [];
|
|
439
|
+
lines.push(
|
|
440
|
+
paint("title", opts.title ?? `${UNIT_TITLE[unit]} tokens`) +
|
|
441
|
+
paint("dim", ` ยท ${opts.title ? "" : "last "}${series.length} ${UNIT_NOUN[unit]}${series.length === 1 ? "" : "s"}`) +
|
|
442
|
+
(budgetTokens ? paint("budget", ` โ budget ${formatNum(budgetTokens)}`) : ""),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
for (let row = height; row >= 1; row--) {
|
|
446
|
+
let line = paint("axis", `${labelFor(row).padStart(yW)} โ`);
|
|
447
|
+
for (let i = 0; i < series.length; i++) {
|
|
448
|
+
const h = scaleMax > 0 ? (series[i].tokens / scaleMax) * height : 0;
|
|
449
|
+
let cell: string;
|
|
450
|
+
if (h >= row) cell = "โ";
|
|
451
|
+
else if (h > row - 1 && series[i].tokens > 0) cell = SPARK[Math.min(7, Math.max(0, Math.floor((h - (row - 1)) * 8) - 1))];
|
|
452
|
+
else if (row === 1 && series[i].tokens > 0) cell = "โ";
|
|
453
|
+
else cell = row === budgetRow ? "โ" : " ";
|
|
454
|
+
const filled = cell !== " " && cell !== "โ";
|
|
455
|
+
const body = cell.repeat(barW);
|
|
456
|
+
line += filled ? paint(kindOf(i), body) : cell === "โ" ? paint("budget", body) : body;
|
|
457
|
+
line += row === budgetRow ? paint("budget", "โ") : " ";
|
|
458
|
+
}
|
|
459
|
+
lines.push(line);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
lines.push(paint("axis", `${"".padStart(yW)} โ${"โ".repeat(series.length * colW)}`));
|
|
463
|
+
const pad = " ".repeat(yW + 2);
|
|
464
|
+
const cols = series.map((p, i) => ({ l: columnLabels(p, unit), now: i === series.length - 1 }));
|
|
465
|
+
lines.push(pad + cols.map((c) => c.l[0].padEnd(colW)).join(""));
|
|
466
|
+
lines.push(paint("dim", pad + cols.map((c) => c.l[1].padEnd(colW)).join("")));
|
|
467
|
+
lines.push(pad + cols.map((c) => (c.now ? paint("now", "โฒ".padEnd(colW)) : " ".repeat(colW))).join(""));
|
|
468
|
+
|
|
469
|
+
const avg = series.length ? total / series.length : 0;
|
|
470
|
+
lines.push(
|
|
471
|
+
paint(
|
|
472
|
+
"dim",
|
|
473
|
+
`ฮฃ ${formatNum(total)} (${formatCost(totalCost)}) ยท avg ${formatNum(avg)}/${UNIT_NOUN[unit]}` +
|
|
474
|
+
(dataMax > 0 ? ` ยท peak ${formatNum(dataMax)} (${bucketLabel(peakPoint.start, unit)})` : ""),
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
return lines;
|
|
478
|
+
}
|
|
479
|
+
|
|
252
480
|
/** Clear the per-file cache (tests). */
|
|
253
481
|
export function clearCache(): void {
|
|
254
482
|
FILE_CACHE.clear();
|
|
255
483
|
}
|
|
256
484
|
|
|
257
|
-
export type Window = "today" | "week" | "month" | "total";
|
|
258
|
-
|
|
259
485
|
export function budgetsFor(cfg: TokenBurnConfig, w: Window): { tokens?: number; cost?: number } {
|
|
260
486
|
const b = cfg.budgets;
|
|
261
487
|
if (w === "today") return { tokens: b.dayTokens, cost: b.dayCost };
|
|
@@ -268,8 +494,22 @@ export function isOverBudget(p: PeriodStats, b: { tokens?: number; cost?: number
|
|
|
268
494
|
return (b.tokens !== undefined && p.tokens > b.tokens) || (b.cost !== undefined && p.cost > b.cost);
|
|
269
495
|
}
|
|
270
496
|
|
|
497
|
+
/** Map chart roles to pi theme colors. */
|
|
498
|
+
export function themePaint(theme: { fg: (c: any, t: string) => string }): Paint {
|
|
499
|
+
const color: Record<string, string> = {
|
|
500
|
+
bar: "accent",
|
|
501
|
+
now: "success",
|
|
502
|
+
over: "error",
|
|
503
|
+
axis: "dim",
|
|
504
|
+
budget: "warning",
|
|
505
|
+
dim: "dim",
|
|
506
|
+
title: "accent",
|
|
507
|
+
};
|
|
508
|
+
return (kind, text) => theme.fg(color[kind], text);
|
|
509
|
+
}
|
|
510
|
+
|
|
271
511
|
export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
272
|
-
let config: TokenBurnConfig =
|
|
512
|
+
let config: TokenBurnConfig = freshConfig();
|
|
273
513
|
|
|
274
514
|
const updateUI = async (ctx: ExtensionContext) => {
|
|
275
515
|
if (!ctx.hasUI) return;
|
|
@@ -307,6 +547,7 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
307
547
|
line("Today", report.today, budgetsFor(config, "today")),
|
|
308
548
|
line("Week", report.week, budgetsFor(config, "week")),
|
|
309
549
|
line("Month", report.month, budgetsFor(config, "month")),
|
|
550
|
+
line("Year", report.year),
|
|
310
551
|
line("Total", report.total),
|
|
311
552
|
`โ${"โ".repeat(46)}โ`,
|
|
312
553
|
];
|
|
@@ -328,70 +569,199 @@ export default function tokenBurnExtension(pi: ExtensionAPI) {
|
|
|
328
569
|
});
|
|
329
570
|
|
|
330
571
|
pi.registerCommand("tokenburn", {
|
|
331
|
-
description: "TokenBurn
|
|
332
|
-
getArgumentCompletions: (prefix: string) =>
|
|
333
|
-
const opts = ["report", "window", "cache", "status <today|week|month|total>", "budget <day|week|month> <tokens|$cost>"];
|
|
334
|
-
return opts.filter((o) => o.startsWith(prefix)).map((value) => ({ value }));
|
|
335
|
-
},
|
|
572
|
+
description: "TokenBurn: /tokenburn [day|week|month|year|all] chart, status <window>, cycle, window, budget, cache",
|
|
573
|
+
getArgumentCompletions: (prefix: string) => completions(prefix),
|
|
336
574
|
handler: async (args, ctx) => {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
if (cmd === "window" || cmd === "widget") {
|
|
340
|
-
config.showWidget = !config.showWidget;
|
|
575
|
+
const cmd = parseCommand(args);
|
|
576
|
+
const persist = async () => {
|
|
341
577
|
await saveConfig(config);
|
|
342
578
|
await updateUI(ctx);
|
|
343
|
-
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
579
|
+
};
|
|
346
580
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
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;
|
|
354
587
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
588
|
+
case "status":
|
|
589
|
+
config.statusWindow = cmd.window;
|
|
590
|
+
await persist();
|
|
591
|
+
ctx.ui.notify(`tokenburn status window: ${cmd.window}`, "info");
|
|
592
|
+
return;
|
|
362
593
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
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
|
+
);
|
|
368
616
|
return;
|
|
369
617
|
}
|
|
370
|
-
const key = `${p1}${isCost ? "Cost" : "Tokens"}` as keyof TokenBurnConfig["budgets"];
|
|
371
|
-
config.budgets[key] = val;
|
|
372
|
-
await saveConfig(config);
|
|
373
|
-
await updateUI(ctx);
|
|
374
|
-
ctx.ui.notify(`tokenburn ${p1} budget: ${isCost ? formatCost(val) : formatNum(val) + " tokens"}`, "info");
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
618
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
}
|
|
395
665
|
},
|
|
396
666
|
});
|
|
397
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
|
}
|