@psg2/pi-costs 1.0.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli.js +393 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# pi-costs
|
|
2
|
+
|
|
3
|
+
Analyze cost and token usage from [pi coding agent](https://github.com/badlogic/pi-mono) sessions.
|
|
4
|
+
|
|
5
|
+
Reads the JSONL session logs stored in `~/.pi/agent/sessions/` and produces a summary of costs, token usage, and breakdowns by model, project, day, or individual session.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Run directly with `bunx` (no install needed):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bunx @psg2/pi-costs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install globally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun install -g @psg2/pi-costs
|
|
19
|
+
pi-costs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Options
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
pi-costs All projects, last 7 days
|
|
26
|
+
pi-costs --days 30 Last 30 days
|
|
27
|
+
pi-costs --days 0 All time
|
|
28
|
+
pi-costs --project my-app Filter by project (substring match)
|
|
29
|
+
pi-costs --sessions Show per-session breakdown
|
|
30
|
+
pi-costs --daily Show per-day breakdown
|
|
31
|
+
pi-costs --dir <path> Custom sessions directory
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Example output
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
======================================================================
|
|
38
|
+
Pi Session Costs — last 7 days
|
|
39
|
+
======================================================================
|
|
40
|
+
|
|
41
|
+
Total cost: $42.5100
|
|
42
|
+
Sessions: 18
|
|
43
|
+
LLM requests: 320
|
|
44
|
+
Input tokens: 50.2K ($0.2510)
|
|
45
|
+
Output tokens: 1.2M ($30.0000)
|
|
46
|
+
Cache read: 15.0M ($9.0000)
|
|
47
|
+
Cache write: 800.0K ($3.2590)
|
|
48
|
+
|
|
49
|
+
──────────────────────────────────────────────────────────────────
|
|
50
|
+
By Model:
|
|
51
|
+
Model Cost Requests
|
|
52
|
+
──────────────────────────────────────────────────────────────────
|
|
53
|
+
claude-opus-4-5 $30.1200 210
|
|
54
|
+
claude-sonnet-4-5 $12.3900 110
|
|
55
|
+
|
|
56
|
+
──────────────────────────────────────────────────────────────────
|
|
57
|
+
By Project:
|
|
58
|
+
Project Cost Requests
|
|
59
|
+
──────────────────────────────────────────────────────────────────
|
|
60
|
+
my-web-app $28.4500 200
|
|
61
|
+
my-api-server $14.0600 120
|
|
62
|
+
|
|
63
|
+
======================================================================
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
Pi stores session logs as JSONL files in `~/.pi/agent/sessions/<project>/`. Each assistant message includes a `usage` field with token counts and pre-calculated costs in USD:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"type": "message",
|
|
73
|
+
"message": {
|
|
74
|
+
"role": "assistant",
|
|
75
|
+
"model": "claude-opus-4-5",
|
|
76
|
+
"usage": {
|
|
77
|
+
"input": 3183,
|
|
78
|
+
"output": 104,
|
|
79
|
+
"cacheRead": 50000,
|
|
80
|
+
"cacheWrite": 4000,
|
|
81
|
+
"cost": {
|
|
82
|
+
"input": 0.0159,
|
|
83
|
+
"output": 0.0026,
|
|
84
|
+
"cacheRead": 0.025,
|
|
85
|
+
"cacheWrite": 0.025,
|
|
86
|
+
"total": 0.0685
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See the [pi session format docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/session.md) for full details.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bun install
|
|
99
|
+
bun test # Run tests
|
|
100
|
+
bun run dev # Run from source
|
|
101
|
+
bun run build # Build for distribution
|
|
102
|
+
bun run lint # Lint
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join as join2, resolve } from "path";
|
|
8
|
+
|
|
9
|
+
// src/format.ts
|
|
10
|
+
function fmtCost(v) {
|
|
11
|
+
return `$${v.toFixed(4)}`;
|
|
12
|
+
}
|
|
13
|
+
function fmtTokens(v) {
|
|
14
|
+
if (v >= 1e6)
|
|
15
|
+
return `${(v / 1e6).toFixed(1)}M`;
|
|
16
|
+
if (v >= 1000)
|
|
17
|
+
return `${(v / 1000).toFixed(1)}K`;
|
|
18
|
+
return String(v);
|
|
19
|
+
}
|
|
20
|
+
function pad(s, width, align = "left") {
|
|
21
|
+
if (align === "right")
|
|
22
|
+
return s.padStart(width);
|
|
23
|
+
return s.padEnd(width);
|
|
24
|
+
}
|
|
25
|
+
var W = 70;
|
|
26
|
+
var SEP = "\u2500".repeat(W - 4);
|
|
27
|
+
function renderReport(opts) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
const ln = (s = "") => {
|
|
30
|
+
lines.push(s);
|
|
31
|
+
};
|
|
32
|
+
const { totals, perModel, perProject, perDay, sessionRows } = opts;
|
|
33
|
+
ln();
|
|
34
|
+
ln("=".repeat(W));
|
|
35
|
+
ln(` Pi Session Costs \u2014 ${opts.period}`);
|
|
36
|
+
if (opts.projectFilter)
|
|
37
|
+
ln(` Filter: '${opts.projectFilter}'`);
|
|
38
|
+
ln("=".repeat(W));
|
|
39
|
+
ln();
|
|
40
|
+
ln(` Total cost: ${fmtCost(totals.totalCost)}`);
|
|
41
|
+
ln(` Sessions: ${opts.sessionCount}`);
|
|
42
|
+
ln(` LLM requests: ${totals.requests}`);
|
|
43
|
+
ln(` Input tokens: ${fmtTokens(totals.inputTokens)} ($${totals.costInput.toFixed(4)})`);
|
|
44
|
+
ln(` Output tokens: ${fmtTokens(totals.outputTokens)} ($${totals.costOutput.toFixed(4)})`);
|
|
45
|
+
ln(` Cache read: ${fmtTokens(totals.cacheReadTokens)} ($${totals.costCacheRead.toFixed(4)})`);
|
|
46
|
+
ln(` Cache write: ${fmtTokens(totals.cacheWriteTokens)} ($${totals.costCacheWrite.toFixed(4)})`);
|
|
47
|
+
if (perModel.size > 0) {
|
|
48
|
+
ln();
|
|
49
|
+
ln(` ${SEP}`);
|
|
50
|
+
ln(" By Model:");
|
|
51
|
+
ln(` ${pad("Model", 45)}${pad("Cost", 10, "right")}${pad("Requests", 10, "right")}`);
|
|
52
|
+
ln(` ${SEP}`);
|
|
53
|
+
const sorted = [...perModel.entries()].sort((a, b) => b[1].cost - a[1].cost);
|
|
54
|
+
for (const [model, { requests, cost }] of sorted) {
|
|
55
|
+
ln(` ${pad(model, 45)}${pad(fmtCost(cost), 10, "right")}${pad(String(requests), 10, "right")}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (perProject.size > 1) {
|
|
59
|
+
ln();
|
|
60
|
+
ln(` ${SEP}`);
|
|
61
|
+
ln(" By Project:");
|
|
62
|
+
ln(` ${pad("Project", 45)}${pad("Cost", 10, "right")}${pad("Requests", 10, "right")}`);
|
|
63
|
+
ln(` ${SEP}`);
|
|
64
|
+
const sorted = [...perProject.entries()].sort((a, b) => b[1].totalCost - a[1].totalCost);
|
|
65
|
+
for (const [proj, stats] of sorted) {
|
|
66
|
+
let short = proj.split("/").filter(Boolean).pop() ?? proj;
|
|
67
|
+
if (short.length > 43)
|
|
68
|
+
short = `\u2026${short.slice(-42)}`;
|
|
69
|
+
ln(` ${pad(short, 45)}${pad(fmtCost(stats.totalCost), 10, "right")}${pad(String(stats.requests), 10, "right")}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (opts.showDaily && perDay.size > 0) {
|
|
73
|
+
ln();
|
|
74
|
+
ln(` ${SEP}`);
|
|
75
|
+
ln(" By Day:");
|
|
76
|
+
ln(` ${pad("Date", 15)}${pad("Cost", 10, "right")}${pad("Requests", 10, "right")}${pad("Input", 10, "right")}${pad("Output", 10, "right")}`);
|
|
77
|
+
ln(` ${SEP}`);
|
|
78
|
+
const sorted = [...perDay.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
79
|
+
for (const [day, stats] of sorted) {
|
|
80
|
+
const totalIn = stats.inputTokens + stats.cacheReadTokens + stats.cacheWriteTokens;
|
|
81
|
+
ln(` ${pad(day, 15)}${pad(fmtCost(stats.totalCost), 10, "right")}${pad(String(stats.requests), 10, "right")}${pad(fmtTokens(totalIn), 10, "right")}${pad(fmtTokens(stats.outputTokens), 10, "right")}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (opts.showSessions && sessionRows.length > 0) {
|
|
85
|
+
const SW = 90;
|
|
86
|
+
const sSep = "\u2500".repeat(SW);
|
|
87
|
+
ln();
|
|
88
|
+
ln(` ${sSep}`);
|
|
89
|
+
ln(" Sessions:");
|
|
90
|
+
ln(` ${pad("Time", 18)}${pad("Project", 20)}${pad("Reqs", 6, "right")}${pad("Cost", 10, "right")}${pad("In Tok", 10, "right")}${pad("Out Tok", 10, "right")} Model`);
|
|
91
|
+
ln(` ${sSep}`);
|
|
92
|
+
for (const row of sessionRows) {
|
|
93
|
+
let proj = row.project;
|
|
94
|
+
if (proj.length > 18)
|
|
95
|
+
proj = `\u2026${proj.slice(-17)}`;
|
|
96
|
+
ln(` ${pad(row.time, 18)}${pad(proj, 20)}${pad(String(row.requests), 6, "right")}${pad(fmtCost(row.cost), 10, "right")}${pad(fmtTokens(row.inputTokens), 10, "right")}${pad(fmtTokens(row.outputTokens), 10, "right")} ${row.models}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
ln();
|
|
100
|
+
ln("=".repeat(W));
|
|
101
|
+
ln();
|
|
102
|
+
return lines.join(`
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/parser.ts
|
|
107
|
+
import { readFile, readdir } from "fs/promises";
|
|
108
|
+
import { join } from "path";
|
|
109
|
+
|
|
110
|
+
// src/stats.ts
|
|
111
|
+
function createStats() {
|
|
112
|
+
return {
|
|
113
|
+
totalCost: 0,
|
|
114
|
+
costInput: 0,
|
|
115
|
+
costOutput: 0,
|
|
116
|
+
costCacheRead: 0,
|
|
117
|
+
costCacheWrite: 0,
|
|
118
|
+
inputTokens: 0,
|
|
119
|
+
outputTokens: 0,
|
|
120
|
+
cacheReadTokens: 0,
|
|
121
|
+
cacheWriteTokens: 0,
|
|
122
|
+
requests: 0,
|
|
123
|
+
models: new Map
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function addUsage(stats, usage, model) {
|
|
127
|
+
const cost = usage.cost ?? {};
|
|
128
|
+
const total = cost.total ?? 0;
|
|
129
|
+
stats.totalCost += total;
|
|
130
|
+
stats.costInput += cost.input ?? 0;
|
|
131
|
+
stats.costOutput += cost.output ?? 0;
|
|
132
|
+
stats.costCacheRead += cost.cacheRead ?? 0;
|
|
133
|
+
stats.costCacheWrite += cost.cacheWrite ?? 0;
|
|
134
|
+
stats.inputTokens += usage.input ?? 0;
|
|
135
|
+
stats.outputTokens += usage.output ?? 0;
|
|
136
|
+
stats.cacheReadTokens += usage.cacheRead ?? 0;
|
|
137
|
+
stats.cacheWriteTokens += usage.cacheWrite ?? 0;
|
|
138
|
+
stats.requests += 1;
|
|
139
|
+
const entry = stats.models.get(model) ?? { requests: 0, cost: 0 };
|
|
140
|
+
entry.requests += 1;
|
|
141
|
+
entry.cost += total;
|
|
142
|
+
stats.models.set(model, entry);
|
|
143
|
+
}
|
|
144
|
+
function mergeStats(target, source) {
|
|
145
|
+
target.totalCost += source.totalCost;
|
|
146
|
+
target.costInput += source.costInput;
|
|
147
|
+
target.costOutput += source.costOutput;
|
|
148
|
+
target.costCacheRead += source.costCacheRead;
|
|
149
|
+
target.costCacheWrite += source.costCacheWrite;
|
|
150
|
+
target.inputTokens += source.inputTokens;
|
|
151
|
+
target.outputTokens += source.outputTokens;
|
|
152
|
+
target.cacheReadTokens += source.cacheReadTokens;
|
|
153
|
+
target.cacheWriteTokens += source.cacheWriteTokens;
|
|
154
|
+
target.requests += source.requests;
|
|
155
|
+
for (const [model, { requests, cost }] of source.models) {
|
|
156
|
+
const entry = target.models.get(model) ?? { requests: 0, cost: 0 };
|
|
157
|
+
entry.requests += requests;
|
|
158
|
+
entry.cost += cost;
|
|
159
|
+
target.models.set(model, entry);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/parser.ts
|
|
164
|
+
function parseSessionTimestamp(filename) {
|
|
165
|
+
const tsRaw = filename.split("_")[0];
|
|
166
|
+
if (!tsRaw)
|
|
167
|
+
return null;
|
|
168
|
+
const parts = tsRaw.replace("Z", "").split("T");
|
|
169
|
+
if (parts.length !== 2)
|
|
170
|
+
return null;
|
|
171
|
+
const [datePart, timeRaw] = parts;
|
|
172
|
+
const timePieces = timeRaw?.split("-");
|
|
173
|
+
if (timePieces.length < 3)
|
|
174
|
+
return null;
|
|
175
|
+
const timeStr = `${timePieces[0]}:${timePieces[1]}:${timePieces[2]}`;
|
|
176
|
+
const date = new Date(`${datePart}T${timeStr}Z`);
|
|
177
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
178
|
+
}
|
|
179
|
+
function decodeProjectName(dirname) {
|
|
180
|
+
return dirname.replace(/--/g, "/").replace(/^\/|\/$/g, "");
|
|
181
|
+
}
|
|
182
|
+
function shortProjectName(project, maxLen = 43) {
|
|
183
|
+
const segments = project.split("/").filter(Boolean);
|
|
184
|
+
let short = segments[segments.length - 1] ?? project;
|
|
185
|
+
if (short.length > maxLen) {
|
|
186
|
+
short = `\u2026${short.slice(-(maxLen - 1))}`;
|
|
187
|
+
}
|
|
188
|
+
return short;
|
|
189
|
+
}
|
|
190
|
+
async function discoverSessions(opts) {
|
|
191
|
+
const sessions = [];
|
|
192
|
+
const cutoff = opts.days > 0 ? new Date(Date.now() - opts.days * 86400000) : new Date(0);
|
|
193
|
+
let projectDirs;
|
|
194
|
+
try {
|
|
195
|
+
projectDirs = await readdir(opts.sessionsDir);
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
for (const dir of projectDirs.sort()) {
|
|
200
|
+
const projectPath = join(opts.sessionsDir, dir);
|
|
201
|
+
const project = decodeProjectName(dir);
|
|
202
|
+
if (opts.projectFilter && !project.toLowerCase().includes(opts.projectFilter.toLowerCase())) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let files;
|
|
206
|
+
try {
|
|
207
|
+
files = await readdir(projectPath);
|
|
208
|
+
} catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
for (const file of files.sort()) {
|
|
212
|
+
if (!file.endsWith(".jsonl"))
|
|
213
|
+
continue;
|
|
214
|
+
const ts = parseSessionTimestamp(file);
|
|
215
|
+
if (!ts || ts < cutoff)
|
|
216
|
+
continue;
|
|
217
|
+
sessions.push({
|
|
218
|
+
filepath: join(projectPath, file),
|
|
219
|
+
project,
|
|
220
|
+
timestamp: ts
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return sessions;
|
|
225
|
+
}
|
|
226
|
+
async function parseSession(filepath) {
|
|
227
|
+
const stats = createStats();
|
|
228
|
+
const text = await readFile(filepath, "utf-8");
|
|
229
|
+
for (const line of text.split(`
|
|
230
|
+
`)) {
|
|
231
|
+
if (!line.trim())
|
|
232
|
+
continue;
|
|
233
|
+
let obj;
|
|
234
|
+
try {
|
|
235
|
+
obj = JSON.parse(line);
|
|
236
|
+
} catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (obj.type !== "message")
|
|
240
|
+
continue;
|
|
241
|
+
const msg = obj.message;
|
|
242
|
+
if (!msg || msg.role !== "assistant")
|
|
243
|
+
continue;
|
|
244
|
+
const usage = msg.usage;
|
|
245
|
+
if (!usage)
|
|
246
|
+
continue;
|
|
247
|
+
const model = msg.model ?? "unknown";
|
|
248
|
+
addUsage(stats, usage, model);
|
|
249
|
+
}
|
|
250
|
+
return stats;
|
|
251
|
+
}
|
|
252
|
+
async function analyzeSessions(sessions, showSessions) {
|
|
253
|
+
const totals = createStats();
|
|
254
|
+
const perProject = new Map;
|
|
255
|
+
const perDay = new Map;
|
|
256
|
+
const sessionRows = [];
|
|
257
|
+
let sessionCount = 0;
|
|
258
|
+
for (const { filepath, project, timestamp } of sessions) {
|
|
259
|
+
const sessionStats = await parseSession(filepath);
|
|
260
|
+
if (sessionStats.requests === 0)
|
|
261
|
+
continue;
|
|
262
|
+
sessionCount++;
|
|
263
|
+
mergeStats(totals, sessionStats);
|
|
264
|
+
const projectStats = perProject.get(project) ?? createStats();
|
|
265
|
+
mergeStats(projectStats, sessionStats);
|
|
266
|
+
perProject.set(project, projectStats);
|
|
267
|
+
const dayKey = timestamp.toISOString().slice(0, 10);
|
|
268
|
+
const dayStats = perDay.get(dayKey) ?? createStats();
|
|
269
|
+
mergeStats(dayStats, sessionStats);
|
|
270
|
+
perDay.set(dayKey, dayStats);
|
|
271
|
+
if (showSessions) {
|
|
272
|
+
const short = shortProjectName(project, 18);
|
|
273
|
+
const modelsStr = [...sessionStats.models.entries()].sort((a, b) => b[1].requests - a[1].requests).map(([m]) => m).join(", ");
|
|
274
|
+
sessionRows.push({
|
|
275
|
+
time: formatTimestamp(timestamp),
|
|
276
|
+
project: short,
|
|
277
|
+
requests: sessionStats.requests,
|
|
278
|
+
cost: sessionStats.totalCost,
|
|
279
|
+
inputTokens: sessionStats.inputTokens + sessionStats.cacheReadTokens + sessionStats.cacheWriteTokens,
|
|
280
|
+
outputTokens: sessionStats.outputTokens,
|
|
281
|
+
models: modelsStr
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { totals, perProject, perDay, perModel: totals.models, sessionRows, sessionCount };
|
|
286
|
+
}
|
|
287
|
+
function formatTimestamp(date) {
|
|
288
|
+
const y = date.getUTCFullYear();
|
|
289
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
290
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
291
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
292
|
+
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
|
293
|
+
return `${y}-${m}-${d} ${h}:${min}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/cli.ts
|
|
297
|
+
var HELP = `pi-costs \u2014 Analyze cost and token usage from pi coding agent sessions
|
|
298
|
+
|
|
299
|
+
Usage:
|
|
300
|
+
pi-costs All projects, last 7 days
|
|
301
|
+
pi-costs --days 30 Last 30 days
|
|
302
|
+
pi-costs --days 0 All time
|
|
303
|
+
pi-costs --project finances Filter by project (substring match)
|
|
304
|
+
pi-costs --sessions Show per-session breakdown
|
|
305
|
+
pi-costs --daily Show per-day breakdown
|
|
306
|
+
|
|
307
|
+
Options:
|
|
308
|
+
--days <n> Time window in days (default: 7, use 0 for all time)
|
|
309
|
+
--project <name> Filter projects by substring (case-insensitive)
|
|
310
|
+
--sessions Show individual session breakdown
|
|
311
|
+
--daily Show per-day breakdown
|
|
312
|
+
--dir <path> Custom sessions directory (default: ~/.pi/agent/sessions)
|
|
313
|
+
-h, --help Show this help
|
|
314
|
+
-v, --version Show version
|
|
315
|
+
`;
|
|
316
|
+
function parseArgs(args) {
|
|
317
|
+
const opts = {
|
|
318
|
+
days: 7,
|
|
319
|
+
projectFilter: "",
|
|
320
|
+
showSessions: false,
|
|
321
|
+
showDaily: false,
|
|
322
|
+
sessionsDir: join2(homedir(), ".pi", "agent", "sessions")
|
|
323
|
+
};
|
|
324
|
+
for (let i = 0;i < args.length; i++) {
|
|
325
|
+
const arg = args[i];
|
|
326
|
+
switch (arg) {
|
|
327
|
+
case "--days":
|
|
328
|
+
opts.days = Number.parseInt(args[++i] ?? "7", 10);
|
|
329
|
+
break;
|
|
330
|
+
case "--project":
|
|
331
|
+
opts.projectFilter = args[++i] ?? "";
|
|
332
|
+
break;
|
|
333
|
+
case "--sessions":
|
|
334
|
+
opts.showSessions = true;
|
|
335
|
+
break;
|
|
336
|
+
case "--daily":
|
|
337
|
+
opts.showDaily = true;
|
|
338
|
+
break;
|
|
339
|
+
case "--dir":
|
|
340
|
+
opts.sessionsDir = args[++i] ?? opts.sessionsDir;
|
|
341
|
+
break;
|
|
342
|
+
case "-h":
|
|
343
|
+
case "--help":
|
|
344
|
+
console.log(HELP);
|
|
345
|
+
process.exit(0);
|
|
346
|
+
break;
|
|
347
|
+
case "-v":
|
|
348
|
+
case "--version": {
|
|
349
|
+
const pkgPath = resolve(import.meta.dirname ?? ".", "..", "package.json");
|
|
350
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
351
|
+
console.log(`pi-costs v${pkg.version}`);
|
|
352
|
+
process.exit(0);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
default:
|
|
356
|
+
console.error(`Unknown option: ${arg}
|
|
357
|
+
Run 'pi-costs --help' for usage.`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return opts;
|
|
362
|
+
}
|
|
363
|
+
async function main() {
|
|
364
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
365
|
+
const sessions = await discoverSessions(opts);
|
|
366
|
+
if (sessions.length === 0) {
|
|
367
|
+
const period2 = opts.days > 0 ? `last ${opts.days} days` : "all time";
|
|
368
|
+
const filter = opts.projectFilter ? ` matching '${opts.projectFilter}'` : "";
|
|
369
|
+
console.log(`No sessions found for ${period2}${filter}`);
|
|
370
|
+
console.log(`
|
|
371
|
+
Searched: ${opts.sessionsDir}`);
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
374
|
+
const result = await analyzeSessions(sessions, opts.showSessions);
|
|
375
|
+
const period = opts.days > 0 ? `last ${opts.days} days` : "all time";
|
|
376
|
+
const report = renderReport({
|
|
377
|
+
period,
|
|
378
|
+
projectFilter: opts.projectFilter,
|
|
379
|
+
totals: result.totals,
|
|
380
|
+
sessionCount: result.sessionCount,
|
|
381
|
+
perModel: result.perModel,
|
|
382
|
+
perProject: result.perProject,
|
|
383
|
+
perDay: result.perDay,
|
|
384
|
+
showDaily: opts.showDaily,
|
|
385
|
+
sessionRows: result.sessionRows,
|
|
386
|
+
showSessions: opts.showSessions
|
|
387
|
+
});
|
|
388
|
+
console.log(report);
|
|
389
|
+
}
|
|
390
|
+
main().catch((err) => {
|
|
391
|
+
console.error("Error:", err.message);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@psg2/pi-costs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Analyze cost and token usage from pi coding agent sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pi-costs": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "bun build src/cli.ts --outdir dist --target bun",
|
|
11
|
+
"dev": "bun run src/cli.ts",
|
|
12
|
+
"lint": "bunx --bun biome check src/",
|
|
13
|
+
"lint:fix": "bunx --bun biome check --write src/",
|
|
14
|
+
"format": "bunx --bun biome format --write src/",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"prepublishOnly": "bun run build"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/psg2/pi-costs.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/psg2/pi-costs#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/psg2/pi-costs/issues"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"pi",
|
|
36
|
+
"pi-coding-agent",
|
|
37
|
+
"claude",
|
|
38
|
+
"llm",
|
|
39
|
+
"cost",
|
|
40
|
+
"usage",
|
|
41
|
+
"tokens"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "^1.9.4",
|
|
46
|
+
"@types/bun": "^1.2.4"
|
|
47
|
+
}
|
|
48
|
+
}
|