@hank-warren/pi-stats 0.4.1 → 0.4.2
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/CHANGELOG.md +6 -0
- package/README.md +4 -0
- package/cache.ts +1 -1
- package/package.json +2 -2
- package/stats.ts +2 -2
- package/types.ts +1 -1
- package/widget.ts +43 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @hank-warren/pi-stats
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Label the month the heatmap grid ends in. The axis keyed each month change off its column's Monday and dropped a label that no longer fit at the right edge, so the current month was missing from the axis on 56% of days. Columns are now named for the month holding their Thursday, and the rightmost label is clamped left instead of dropped.
|
|
8
|
+
|
|
3
9
|
## 0.4.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -99,3 +99,7 @@ Sessions remain authoritative. For fast repeat opens, the extension stores one d
|
|
|
99
99
|
The location follows `PI_CODING_AGENT_DIR`. The cache contains file stat identity, session IDs/timestamps/working directories, model names, tool names with call and error counts, usage counters, and de-duplication fingerprints. It never stores prompts, responses, tool arguments, tool output, or other conversation content. It is written atomically with mode `0600` and can be deleted at any time; the next `/stats` invocation rebuilds it.
|
|
100
100
|
|
|
101
101
|
No network or billing API is used. Displayed costs are the estimates already recorded by Pi.
|
|
102
|
+
|
|
103
|
+
## Changelog
|
|
104
|
+
|
|
105
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
package/cache.ts
CHANGED
|
@@ -11,7 +11,7 @@ const SKIP_DIRECTORIES = new Set(["subagent-artifacts"]);
|
|
|
11
11
|
/** Extensions record out-of-transcript model usage in <agentDir>/<extension>/usage.jsonl. */
|
|
12
12
|
const USAGE_SIDECAR_NAMES = new Set(["usage.jsonl", "usage.jsonl.1"]);
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
interface ScanOptions {
|
|
15
15
|
agentDir: string;
|
|
16
16
|
activeSessionDir?: string;
|
|
17
17
|
env?: NodeJS.ProcessEnv;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-stats",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A compact /stats dashboard for all-time Pi token usage, including persisted subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/hank-warren/pi-extensions/tree/main/packages/pi-stats#readme",
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=
|
|
26
|
+
"node": ">=22.19.0"
|
|
27
27
|
},
|
|
28
28
|
"pi": {
|
|
29
29
|
"extensions": [
|
package/stats.ts
CHANGED
|
@@ -43,7 +43,7 @@ function firstNumber(source: Record<string, unknown>, names: string[]): number {
|
|
|
43
43
|
return 0;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
function normalizeUsage(value: unknown): UsageTotals | undefined {
|
|
47
47
|
const usage = object(value);
|
|
48
48
|
if (!usage) return undefined;
|
|
49
49
|
const cost = object(usage.cost);
|
|
@@ -375,7 +375,7 @@ function hasPersistedSubagentChild(record: UsageRecord, sessionPaths: ReadonlySe
|
|
|
375
375
|
return record.toolName === "subagent" && Boolean(record.childSessionFiles?.some((file) => sessionPaths.has(resolve(file))));
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
|
|
378
|
+
function deduplicateToolCalls(sessions: readonly SessionRecord[]): ToolCallRecord[] {
|
|
379
379
|
const seen = new Set<string>();
|
|
380
380
|
const result: ToolCallRecord[] = [];
|
|
381
381
|
for (const session of sessions) {
|
package/types.ts
CHANGED
package/widget.ts
CHANGED
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
type UsageTotals,
|
|
11
11
|
} from "./types.ts";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
type WidgetState =
|
|
14
14
|
| { kind: "loading"; completed: number; total: number }
|
|
15
15
|
| { kind: "error"; message: string }
|
|
16
16
|
| { kind: "ready"; snapshot: StatsSnapshot };
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
interface StatsWidgetOptions {
|
|
19
19
|
theme: Theme;
|
|
20
20
|
requestRender: () => void;
|
|
21
21
|
onClose: () => void;
|
|
@@ -37,7 +37,7 @@ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "
|
|
|
37
37
|
const RANGES: StatsRange[] = ["all", "7d", "30d"];
|
|
38
38
|
const RANGE_LABELS: Record<StatsRange, string> = { all: "All time", "7d": "Last 7 days", "30d": "Last 30 days" };
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
type StatsTab = "overview" | "models" | "tools" | "projects";
|
|
41
41
|
const TABS: StatsTab[] = ["overview", "models", "tools", "projects"];
|
|
42
42
|
const TAB_LABELS: Record<StatsTab, string> = { overview: "Overview", models: "Models", tools: "Tools", projects: "Projects" };
|
|
43
43
|
|
|
@@ -76,6 +76,45 @@ function localDay(time: number): string {
|
|
|
76
76
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The heatmap's month label row: one character per week column, `weeks` wide.
|
|
81
|
+
*
|
|
82
|
+
* A column belongs to the month holding its Thursday, so it is named for whichever month
|
|
83
|
+
* owns four or more of its seven days. Labels are placed right to left because only the
|
|
84
|
+
* right edge is constrained: each one sits at its month's first column unless its
|
|
85
|
+
* neighbour crowds it, and shifts left just enough to stay clear.
|
|
86
|
+
*/
|
|
87
|
+
export function monthAxis(start: Date, weeks: number): string {
|
|
88
|
+
const monthOf = (week: number): number => {
|
|
89
|
+
const thursday = new Date(start);
|
|
90
|
+
thursday.setDate(thursday.getDate() + week * 7 + 3);
|
|
91
|
+
return thursday.getMonth();
|
|
92
|
+
};
|
|
93
|
+
/** Three characters plus a space, so neighbouring labels never touch. */
|
|
94
|
+
const spacing = 4;
|
|
95
|
+
|
|
96
|
+
const boundaries: { column: number; month: number }[] = [];
|
|
97
|
+
for (let week = 0; week < weeks; week++) {
|
|
98
|
+
const month = monthOf(week);
|
|
99
|
+
if (month !== monthOf(week - 1)) boundaries.push({ column: week, month });
|
|
100
|
+
}
|
|
101
|
+
// A grid too short to leave its month has no boundary to label, so name it after its
|
|
102
|
+
// last column like every other grid.
|
|
103
|
+
if (boundaries.length === 0) boundaries.push({ column: 0, month: monthOf(weeks - 1) });
|
|
104
|
+
|
|
105
|
+
const placed: { column: number; month: number }[] = [];
|
|
106
|
+
let rightmost = weeks - 3;
|
|
107
|
+
for (let index = boundaries.length - 1; index >= 0 && rightmost >= 0; index--) {
|
|
108
|
+
const column = Math.min(boundaries[index]!.column, rightmost);
|
|
109
|
+
placed.unshift({ column, month: boundaries[index]!.month });
|
|
110
|
+
rightmost = column - spacing;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let row = "";
|
|
114
|
+
for (const { column, month } of placed) row = row.padEnd(column, " ") + MONTHS[month]!;
|
|
115
|
+
return row.padEnd(weeks, " ").slice(0, weeks);
|
|
116
|
+
}
|
|
117
|
+
|
|
79
118
|
function padRight(value: string, width: number): string {
|
|
80
119
|
const clipped = truncateToWidth(value, Math.max(0, width), "");
|
|
81
120
|
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
@@ -424,22 +463,7 @@ export class StatsWidget implements Component {
|
|
|
424
463
|
const gap = Math.max(1, width - visibleWidth(heading) - visibleWidth(legend) - 1);
|
|
425
464
|
const lines = [`${heading}${" ".repeat(gap)}${theme.fg("dim", legend)}`];
|
|
426
465
|
|
|
427
|
-
|
|
428
|
-
let lastLabel = -4;
|
|
429
|
-
for (let week = 0; week < weeks; week++) {
|
|
430
|
-
const date = new Date(start);
|
|
431
|
-
date.setDate(date.getDate() + week * 7);
|
|
432
|
-
const previous = new Date(date);
|
|
433
|
-
previous.setDate(previous.getDate() - 7);
|
|
434
|
-
if ((week === 0 || date.getMonth() !== previous.getMonth()) && week - lastLabel >= 4 && week + 3 <= weeks) {
|
|
435
|
-
months += MONTHS[date.getMonth()]!;
|
|
436
|
-
lastLabel = week;
|
|
437
|
-
week += 2;
|
|
438
|
-
} else {
|
|
439
|
-
months += " ";
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
lines.push(theme.fg("dim", months));
|
|
466
|
+
lines.push(theme.fg("dim", " ".repeat(HEATMAP_LABEL_WIDTH) + monthAxis(start, weeks)));
|
|
443
467
|
|
|
444
468
|
for (let weekday = 0; weekday < 7; weekday++) {
|
|
445
469
|
const label = weekday === 0 ? "Mon" : weekday === 2 ? "Wed" : weekday === 4 ? "Fri" : " ";
|