@qmxme/pi-stats 0.2.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.
Files changed (3) hide show
  1. package/README.md +55 -0
  2. package/package.json +41 -0
  3. package/stats.ts +65 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @qmxme/pi-stats
2
+
3
+ Stats widget extension for [pi](https://github.com/badlogic/pi) that displays token usage, throughput, and response duration.
4
+
5
+ ## Features
6
+
7
+ - **Token throughput**: Tokens per second during response
8
+ - **Token usage**: Input → Output tokens per message
9
+ - **Cache tracking**: Cache read (↓) and write (↑) when applicable
10
+ - **Total tokens**: Cumulative token count
11
+ - **Duration**: Response time in seconds or minutes
12
+
13
+ ## Widget Display
14
+
15
+ Example output in the UI status bar:
16
+
17
+ ```
18
+ 12.3 tok/s | 1.2k → 450 | cache 500↓ 300↑ | total 2.1k | 3.2s
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pi install npm:@qmxme/pi-stats
25
+ ```
26
+
27
+ With a pinned version:
28
+
29
+ ```bash
30
+ pi install npm:@qmxme/pi-stats@0.1.0
31
+ ```
32
+
33
+ Project-local installation:
34
+
35
+ ```bash
36
+ pi install npm:@qmxme/pi-stats -l
37
+ ```
38
+
39
+ Try without installing:
40
+
41
+ ```bash
42
+ pi -e npm:@qmxme/pi-stats
43
+ ```
44
+
45
+ ## Development
46
+
47
+ ```bash
48
+ npm install # install build dependencies
49
+ npm run build # compile to dist/
50
+ npm run dev # watch mode
51
+ ```
52
+
53
+ ## License
54
+
55
+ MIT
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@qmxme/pi-stats",
3
+ "version": "0.2.0",
4
+ "description": "Stats widget extension for pi - shows token throughput, usage, and duration",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/qmx/pi-stats"
8
+ },
9
+ "type": "module",
10
+ "main": "./stats.ts",
11
+ "types": "./stats.ts",
12
+ "files": [
13
+ "stats.ts",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "pi",
18
+ "pi-package",
19
+ "extension",
20
+ "stats"
21
+ ],
22
+ "pi": {
23
+ "extensions": [
24
+ "./stats.ts"
25
+ ]
26
+ },
27
+ "scripts": {
28
+ "typecheck": "tsc --noEmit",
29
+ "dev": "tsc --noEmit --watch",
30
+ "prepublishOnly": "npm run typecheck"
31
+ },
32
+ "peerDependencies": {
33
+ "@mariozechner/pi-coding-agent": "*"
34
+ },
35
+ "devDependencies": {
36
+ "@mariozechner/pi-coding-agent": "^0.63.0",
37
+ "@types/node": "^22.10.7",
38
+ "typescript": "^5.7.3"
39
+ },
40
+ "license": "MIT"
41
+ }
package/stats.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ export default function (pi: ExtensionAPI) {
4
+ let streamingStart: number | undefined;
5
+ let initialOutputTokens: number = 0;
6
+ let widgetKey = "streaming-stats";
7
+
8
+ // Format numbers with K/M suffix
9
+ function formatNum(n: number): string {
10
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
11
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
12
+ return n.toString();
13
+ }
14
+
15
+ // Format duration
16
+ function formatDuration(ms: number): string {
17
+ const s = ms / 1000;
18
+ if (s < 60) return `${s.toFixed(1)}s`;
19
+ const m = Math.floor(s / 60);
20
+ const rem = (s % 60).toFixed(1);
21
+ return `${m}m${rem}s`;
22
+ }
23
+
24
+ pi.on("message_start", async (event, ctx) => {
25
+ if (event.message.role === "assistant") {
26
+ streamingStart = Date.now();
27
+ initialOutputTokens = event.message.usage.output;
28
+ ctx.ui.setWidget(widgetKey, undefined);
29
+ }
30
+ });
31
+
32
+ pi.on("message_end", async (event, ctx) => {
33
+ if (event.message.role === "assistant" && streamingStart) {
34
+ const elapsed = Date.now() - streamingStart;
35
+ const usage = event.message.usage;
36
+
37
+ // Calculate total output tokens generated in this turn
38
+ const outputTokens = usage.output - initialOutputTokens;
39
+ const tps = elapsed > 0 ? (outputTokens / elapsed) * 1000 : 0;
40
+
41
+ const total = usage.output + usage.input + usage.cacheRead + usage.cacheWrite;
42
+
43
+ // Build cache string, only showing non-zero values
44
+ let cacheStr = "";
45
+ const cacheParts: string[] = [];
46
+ if (usage.cacheRead > 0) cacheParts.push(`${formatNum(usage.cacheRead)}↓`);
47
+ if (usage.cacheWrite > 0) cacheParts.push(`${formatNum(usage.cacheWrite)}↑`);
48
+ if (cacheParts.length > 0) {
49
+ cacheStr = ` | cache ${cacheParts.join(" ")}`;
50
+ }
51
+
52
+ const stats = `${tps.toFixed(1)} tok/s | ${formatNum(usage.input)} → ${formatNum(usage.output)}${cacheStr} | total ${formatNum(total)} | ${formatDuration(elapsed)}`;
53
+
54
+ ctx.ui.setWidget(widgetKey, (tui, theme) => {
55
+ const lines = [theme.fg("dim", stats)];
56
+ return {
57
+ render: () => lines,
58
+ invalidate: () => {},
59
+ };
60
+ });
61
+
62
+ streamingStart = undefined;
63
+ }
64
+ });
65
+ }