@hank-warren/pi-stats 0.3.1 → 0.3.3
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 +24 -0
- package/README.md +2 -2
- package/index.ts +3 -2
- package/package.json +3 -2
- package/stats.ts +30 -4
- package/widget.ts +4 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @hank-warren/pi-stats
|
|
2
|
+
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cf12677: ship CHANGELOG.md in the published tarball
|
|
8
|
+
- 596888b: keep extension sidecar usage when the current session is not yet on disk
|
|
9
|
+
|
|
10
|
+
## 0.3.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 187e12c: Roll usage-free placeholder models into `Tools/summaries` and let the activity heatmap use the full terminal width.
|
|
15
|
+
|
|
16
|
+
Pi records placeholder assistant turns whose model is the literal `<synthetic>` (a missing model normalizes to `unknown`). With no tokens and no cost they produced their own empty model row; they are now attributed to the existing unattributed bucket, while a placeholder that did record spend keeps its own row.
|
|
17
|
+
|
|
18
|
+
The heatmap previously shrank to the recorded history, leaving most of a wide terminal blank. It now claims the available width up to a full year, drawing weeks before the first recorded session as empty cells that fill in over time.
|
|
19
|
+
|
|
20
|
+
## 0.3.1
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 67abcf5: expand the readme with try-it install, a configuration table, and troubleshooting
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Run `/stats` in interactive mode. The dashboard temporarily replaces the editor
|
|
|
27
27
|
- `u`: force a full rescan and rebuild the session index
|
|
28
28
|
- `Escape`: close
|
|
29
29
|
|
|
30
|
-
The Overview compares current-session, selected-range, and all-time usage in one aligned table covering input, output, cache read, cache write, reasoning, cache-hit rate, and recorded cost, followed by a month-labelled activity heatmap and all-time session, streak, and favourite-model highlights. Reasoning is shown as a subset of output and is never added to the total twice.
|
|
30
|
+
The Overview compares current-session, selected-range, and all-time usage in one aligned table covering input, output, cache read, cache write, reasoning, cache-hit rate, and recorded cost, followed by a month-labelled activity heatmap and all-time session, streak, and favourite-model highlights. The heatmap always spans the width the terminal offers, up to a full year; weeks before your first recorded session render as empty cells and fill in as history accumulates. Reasoning is shown as a subset of output and is never added to the total twice.
|
|
31
31
|
|
|
32
32
|
The Models tab ranks models for the selected range with share bars and a totals row that reconciles with the range total. It attributes assistant usage to the provider response model and keeps otherwise unattributed nested usage in `Tools/summaries`.
|
|
33
33
|
|
|
@@ -70,7 +70,7 @@ Each variable is matched exactly: `PI_STATS_DISABLE_USAGE_SIDECARS=true` does no
|
|
|
70
70
|
|
|
71
71
|
**Guardian or other extension usage is missing.** Sidecar reading is skipped entirely when `PI_STATS_DISABLE_USAGE_SIDECARS=1`. Otherwise confirm the writing extension is enabled and that `<agent dir>/<extension>/usage.jsonl` exists; sidecars are only discovered one level below the agent directory.
|
|
72
72
|
|
|
73
|
-
**A model shows under `Tools/summaries` instead of its own row.** That bucket holds nested usage that carries no attributable response model, such as some compaction and branch-summary records. It is counted in the totals either way.
|
|
73
|
+
**A model shows under `Tools/summaries` instead of its own row.** That bucket holds nested usage that carries no attributable response model, such as some compaction and branch-summary records. It also absorbs Pi's own placeholder turns — assistant messages whose model is `<synthetic>` or missing — but only when they recorded no tokens and no cost; a placeholder that was actually billed keeps its own row so the spend stays visible. Everything in the bucket is counted in the totals either way.
|
|
74
74
|
|
|
75
75
|
**Costs read `$0.00` or look wrong.** Displayed cost is whatever Pi already recorded in the session; `pi-stats` performs no pricing lookup and contacts no billing API. Providers used through a subscription or proxy commonly record zero cost.
|
|
76
76
|
|
package/index.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { buildSnapshot, makeIndex, usageFromEntries } from "./stats.ts";
|
|
|
4
4
|
import type { SessionRecord, StatsIndex, StatsRange } from "./types.ts";
|
|
5
5
|
import { StatsWidget } from "./widget.ts";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Exported for tests: the rebuild must keep sidecar usage, which no session record carries. */
|
|
8
|
+
export function includeInMemorySession(index: StatsIndex, ctx: ExtensionCommandContext): StatsIndex {
|
|
8
9
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
9
10
|
if (index.sessions.some((session) => session.sessionId === sessionId)) return index;
|
|
10
11
|
const entries = ctx.sessionManager.getEntries();
|
|
@@ -25,7 +26,7 @@ function includeInMemorySession(index: StatsIndex, ctx: ExtensionCommandContext)
|
|
|
25
26
|
source: "main",
|
|
26
27
|
usage,
|
|
27
28
|
};
|
|
28
|
-
return makeIndex([...index.sessions, session], index.diagnostics);
|
|
29
|
+
return makeIndex([...index.sessions, session], index.diagnostics, index.usage.filter((record) => record.kind === "sidecar"));
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export default function statsExtension(pi: ExtensionAPI): void {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-stats",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "A compact /stats dashboard for all-time Pi token usage, including persisted subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"types.ts",
|
|
38
38
|
"widget.ts",
|
|
39
39
|
"README.md",
|
|
40
|
-
"LICENSE"
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"CHANGELOG.md"
|
|
41
42
|
],
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"@earendil-works/pi-coding-agent": "*",
|
package/stats.ts
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
type UsageTotals,
|
|
15
15
|
} from "./types.ts";
|
|
16
16
|
|
|
17
|
+
/** Bucket for usage that carries no attributable response model. */
|
|
18
|
+
const SUMMARY_MODEL = "Tools/summaries";
|
|
19
|
+
|
|
17
20
|
interface ParseResult {
|
|
18
21
|
ignored: boolean;
|
|
19
22
|
malformedLines: number;
|
|
@@ -73,6 +76,29 @@ function detailsModel(details: unknown): string | undefined {
|
|
|
73
76
|
return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Pi records placeholder assistant messages for turns it generates itself; today they
|
|
81
|
+
* carry the literal model `<synthetic>`, and a missing model normalizes to `unknown`.
|
|
82
|
+
* Angle brackets are never valid in a provider model id, so they are a safe marker.
|
|
83
|
+
*/
|
|
84
|
+
function placeholderModel(model: string): boolean {
|
|
85
|
+
const bare = model.slice(model.lastIndexOf("/") + 1);
|
|
86
|
+
return bare === "unknown" || (bare.startsWith("<") && bare.endsWith(">"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unmetered(usage: UsageTotals): boolean {
|
|
90
|
+
return totalTokens(usage) === 0 && usage.reasoning === 0 && usage.cost === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Placeholder models that recorded no tokens and no cost would otherwise get their own
|
|
95
|
+
* zero-value model row. Roll them into the unattributed bucket so the call is still
|
|
96
|
+
* counted without inventing a model that was never billed.
|
|
97
|
+
*/
|
|
98
|
+
function attributedModel(model: string, usage: UsageTotals): string {
|
|
99
|
+
return placeholderModel(model) && unmetered(usage) ? SUMMARY_MODEL : model;
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
function nestedSubagentUsage(details: unknown): UsageTotals | undefined {
|
|
77
103
|
const record = object(details);
|
|
78
104
|
if (!record) return undefined;
|
|
@@ -135,7 +161,7 @@ function normalizeEntry(
|
|
|
135
161
|
if (role === "assistant") {
|
|
136
162
|
const usage = normalizeUsage(message.usage);
|
|
137
163
|
if (!usage) return undefined;
|
|
138
|
-
const model = modelName(message.provider, message.responseModel ?? message.model);
|
|
164
|
+
const model = attributedModel(modelName(message.provider, message.responseModel ?? message.model), usage);
|
|
139
165
|
return {
|
|
140
166
|
fingerprint: fingerprint([id, entry.timestamp, "assistant", model, usage]),
|
|
141
167
|
timestamp: timestamp(message.timestamp, entryTimestamp),
|
|
@@ -148,7 +174,7 @@ function normalizeEntry(
|
|
|
148
174
|
const toolName = typeof message.toolName === "string" ? message.toolName : "tool";
|
|
149
175
|
const usage = normalizeUsage(message.usage) ?? (toolName === "subagent" ? nestedSubagentUsage(message.details) : undefined);
|
|
150
176
|
if (!usage) return undefined;
|
|
151
|
-
const model = detailsModel(message.details) ??
|
|
177
|
+
const model = attributedModel(detailsModel(message.details) ?? SUMMARY_MODEL, usage);
|
|
152
178
|
const children = toolName === "subagent" ? childSessionFiles(message.details, sessionFile) : [];
|
|
153
179
|
return {
|
|
154
180
|
fingerprint: fingerprint([id, entry.timestamp, "tool", toolName, model, usage]),
|
|
@@ -167,9 +193,9 @@ function normalizeEntry(
|
|
|
167
193
|
const usage = normalizeUsage(entry.usage);
|
|
168
194
|
if (!usage) return undefined;
|
|
169
195
|
return {
|
|
170
|
-
fingerprint: fingerprint([id, entry.timestamp, entry.type,
|
|
196
|
+
fingerprint: fingerprint([id, entry.timestamp, entry.type, SUMMARY_MODEL, usage]),
|
|
171
197
|
timestamp: entryTimestamp,
|
|
172
|
-
model:
|
|
198
|
+
model: SUMMARY_MODEL,
|
|
173
199
|
usage,
|
|
174
200
|
kind: "summary",
|
|
175
201
|
};
|
package/widget.ts
CHANGED
|
@@ -374,10 +374,10 @@ export class StatsWidget implements Component {
|
|
|
374
374
|
today.setHours(12, 0, 0, 0);
|
|
375
375
|
const end = new Date(today);
|
|
376
376
|
end.setDate(end.getDate() + (6 - ((end.getDay() + 6) % 7)));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const weeks = Math.max(HEATMAP_MIN_WEEKS, Math.min(cells, HEATMAP_MAX_WEEKS
|
|
377
|
+
// Always claim the width the terminal offers, up to a year. Weeks before the first
|
|
378
|
+
// recorded day render as empty cells so a short history fills the grid in over time
|
|
379
|
+
// instead of leaving most of a wide row blank.
|
|
380
|
+
const weeks = Math.max(HEATMAP_MIN_WEEKS, Math.min(cells, HEATMAP_MAX_WEEKS));
|
|
381
381
|
const start = new Date(end);
|
|
382
382
|
start.setDate(start.getDate() - weeks * 7 + 1);
|
|
383
383
|
|