@ilya-lesikov/pi-pi 0.1.0 → 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.
@@ -0,0 +1,306 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export interface ModelUsage {
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ cacheReadTokens: number;
9
+ cacheWriteTokens: number;
10
+ turns: number;
11
+ }
12
+
13
+ export interface UsageTracker {
14
+ recordTurn(modelId: string, provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void;
15
+ recordSubagentCompletion(tokens: { input?: number; output?: number; total?: number }, cost?: number, meta?: { description?: string; modelId?: string; durationMs?: number; toolUses?: number }): void;
16
+ loadFromSummary(summary: Record<string, unknown>): void;
17
+ getTotalInputTokens(): number;
18
+ getTotalOutputTokens(): number;
19
+ getTotalCacheReadTokens(): number;
20
+ getTotalCacheWriteTokens(): number;
21
+ getTotalCost(): number;
22
+ getCacheHitRate(): number;
23
+ getPerModelUsage(): Record<string, ModelUsage>;
24
+ getSubagentTotals(): { inputTokens: number; outputTokens: number; cost: number };
25
+ getSubagentList(): SubagentUsage[];
26
+ toSummary(): object;
27
+ reset(): void;
28
+ }
29
+
30
+ export interface SubagentUsage {
31
+ description: string;
32
+ modelId: string;
33
+ inputTokens: number;
34
+ outputTokens: number;
35
+ cacheReadTokens: number;
36
+ cacheWriteTokens: number;
37
+ cost: number;
38
+ durationMs: number;
39
+ toolUses: number;
40
+ }
41
+
42
+ interface TrackerState {
43
+ startedAt: string;
44
+ totalInputTokens: number;
45
+ totalOutputTokens: number;
46
+ totalCacheReadTokens: number;
47
+ totalCacheWriteTokens: number;
48
+ totalCost: number;
49
+ totalTurns: number;
50
+ subagentInputTokens: number;
51
+ subagentOutputTokens: number;
52
+ subagentCost: number;
53
+ models: Map<string, ModelUsage>;
54
+ subagents: SubagentUsage[];
55
+ }
56
+
57
+ function toFiniteNumber(value: unknown): number {
58
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
59
+ }
60
+
61
+ function resolveAgentDir(): string {
62
+ const envKey = "PI_CODING_AGENT_DIR";
63
+ const envDir = process.env[envKey];
64
+ if (envDir) {
65
+ if (envDir === "~") return homedir();
66
+ if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
67
+ return envDir;
68
+ }
69
+ return join(homedir(), ".pi", "agent");
70
+ }
71
+
72
+ function createInitialState(): TrackerState {
73
+ return {
74
+ startedAt: new Date().toISOString(),
75
+ totalInputTokens: 0,
76
+ totalOutputTokens: 0,
77
+ totalCacheReadTokens: 0,
78
+ totalCacheWriteTokens: 0,
79
+ totalCost: 0,
80
+ totalTurns: 0,
81
+ subagentInputTokens: 0,
82
+ subagentOutputTokens: 0,
83
+ subagentCost: 0,
84
+ models: new Map<string, ModelUsage>(),
85
+ subagents: [],
86
+ };
87
+ }
88
+
89
+ export function createUsageTracker(): UsageTracker {
90
+ const state = createInitialState();
91
+
92
+ return {
93
+ recordTurn(modelId: string, _provider: string, input: number, output: number, cacheRead: number, cacheWrite: number, cost: number): void {
94
+ const safeInput = toFiniteNumber(input);
95
+ const safeOutput = toFiniteNumber(output);
96
+ const safeCacheRead = toFiniteNumber(cacheRead);
97
+ const safeCacheWrite = toFiniteNumber(cacheWrite);
98
+ const safeCost = toFiniteNumber(cost);
99
+
100
+ state.totalInputTokens += safeInput;
101
+ state.totalOutputTokens += safeOutput;
102
+ state.totalCacheReadTokens += safeCacheRead;
103
+ state.totalCacheWriteTokens += safeCacheWrite;
104
+ state.totalCost += safeCost;
105
+ state.totalTurns += 1;
106
+
107
+ const key = modelId || "unknown-model";
108
+ const usage = state.models.get(key) ?? {
109
+ inputTokens: 0,
110
+ outputTokens: 0,
111
+ cacheReadTokens: 0,
112
+ cacheWriteTokens: 0,
113
+ turns: 0,
114
+ };
115
+
116
+ usage.inputTokens += safeInput;
117
+ usage.outputTokens += safeOutput;
118
+ usage.cacheReadTokens += safeCacheRead;
119
+ usage.cacheWriteTokens += safeCacheWrite;
120
+ usage.turns += 1;
121
+ state.models.set(key, usage);
122
+ },
123
+
124
+ recordSubagentCompletion(
125
+ tokens: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number; cost?: number },
126
+ cost?: number,
127
+ meta?: { description?: string; modelId?: string; durationMs?: number; toolUses?: number },
128
+ ): void {
129
+ const safeInput = toFiniteNumber(tokens.input);
130
+ const safeOutput = toFiniteNumber(tokens.output);
131
+ const safeTotal = toFiniteNumber(tokens.total);
132
+ const safeCacheRead = toFiniteNumber(tokens.cacheRead);
133
+ const safeCacheWrite = toFiniteNumber(tokens.cacheWrite);
134
+ const safeCost = toFiniteNumber(tokens.cost ?? cost);
135
+
136
+ const effectiveInput = safeInput === 0 && safeOutput === 0 ? safeTotal : safeInput;
137
+
138
+ state.subagentInputTokens += effectiveInput;
139
+ state.subagentOutputTokens += safeOutput;
140
+ state.subagentCost += safeCost;
141
+
142
+ state.subagents.push({
143
+ description: meta?.description ?? "unknown",
144
+ modelId: meta?.modelId ?? "unknown",
145
+ inputTokens: effectiveInput,
146
+ outputTokens: safeOutput,
147
+ cacheReadTokens: safeCacheRead,
148
+ cacheWriteTokens: safeCacheWrite,
149
+ cost: safeCost,
150
+ durationMs: toFiniteNumber(meta?.durationMs),
151
+ toolUses: toFiniteNumber(meta?.toolUses),
152
+ });
153
+ },
154
+
155
+ loadFromSummary(summary: Record<string, unknown>): void {
156
+ const totals = summary.totals as Record<string, unknown> | undefined;
157
+ const subagents = summary.subagents as Record<string, unknown> | undefined;
158
+ const models = summary.models as Record<string, Record<string, unknown>> | undefined;
159
+ if (totals) {
160
+ state.totalInputTokens = toFiniteNumber(totals.inputTokens);
161
+ state.totalOutputTokens = toFiniteNumber(totals.outputTokens);
162
+ state.totalCacheReadTokens = toFiniteNumber(totals.cacheReadTokens);
163
+ state.totalCacheWriteTokens = toFiniteNumber(totals.cacheWriteTokens);
164
+ state.totalCost = toFiniteNumber(totals.cost);
165
+ state.totalTurns = toFiniteNumber(totals.turns);
166
+ }
167
+ if (Array.isArray(summary.subagents)) {
168
+ for (const sa of summary.subagents as Record<string, unknown>[]) {
169
+ const entry: SubagentUsage = {
170
+ description: typeof sa.description === "string" ? sa.description : "unknown",
171
+ modelId: typeof sa.modelId === "string" ? sa.modelId : "unknown",
172
+ inputTokens: toFiniteNumber(sa.inputTokens),
173
+ outputTokens: toFiniteNumber(sa.outputTokens),
174
+ cacheReadTokens: toFiniteNumber(sa.cacheReadTokens),
175
+ cacheWriteTokens: toFiniteNumber(sa.cacheWriteTokens),
176
+ cost: toFiniteNumber(sa.cost),
177
+ durationMs: toFiniteNumber(sa.durationMs),
178
+ toolUses: toFiniteNumber(sa.toolUses),
179
+ };
180
+ state.subagents.push(entry);
181
+ state.subagentInputTokens += entry.inputTokens;
182
+ state.subagentOutputTokens += entry.outputTokens;
183
+ state.subagentCost += entry.cost;
184
+ }
185
+ }
186
+ if (models) {
187
+ for (const [modelId, usage] of Object.entries(models)) {
188
+ state.models.set(modelId, {
189
+ inputTokens: toFiniteNumber(usage.inputTokens),
190
+ outputTokens: toFiniteNumber(usage.outputTokens),
191
+ cacheReadTokens: toFiniteNumber(usage.cacheReadTokens),
192
+ cacheWriteTokens: toFiniteNumber(usage.cacheWriteTokens),
193
+ turns: toFiniteNumber(usage.turns),
194
+ });
195
+ }
196
+ }
197
+ if (typeof summary.startedAt === "string") {
198
+ state.startedAt = summary.startedAt;
199
+ }
200
+ },
201
+
202
+ getTotalInputTokens(): number {
203
+ return state.totalInputTokens;
204
+ },
205
+
206
+ getTotalOutputTokens(): number {
207
+ return state.totalOutputTokens;
208
+ },
209
+
210
+ getTotalCacheReadTokens(): number {
211
+ return state.totalCacheReadTokens;
212
+ },
213
+
214
+ getTotalCacheWriteTokens(): number {
215
+ return state.totalCacheWriteTokens;
216
+ },
217
+
218
+ getTotalCost(): number {
219
+ return state.totalCost;
220
+ },
221
+
222
+ getCacheHitRate(): number {
223
+ const denominator = state.totalCacheReadTokens + state.totalInputTokens;
224
+ if (denominator <= 0) return 0;
225
+ return state.totalCacheReadTokens / denominator;
226
+ },
227
+
228
+ getPerModelUsage(): Record<string, ModelUsage> {
229
+ const out: Record<string, ModelUsage> = {};
230
+ for (const [model, usage] of state.models.entries()) {
231
+ out[model] = { ...usage };
232
+ }
233
+ return out;
234
+ },
235
+
236
+ getSubagentTotals(): { inputTokens: number; outputTokens: number; cost: number } {
237
+ return {
238
+ inputTokens: state.subagentInputTokens,
239
+ outputTokens: state.subagentOutputTokens,
240
+ cost: state.subagentCost,
241
+ };
242
+ },
243
+
244
+ getSubagentList(): SubagentUsage[] {
245
+ return [...state.subagents];
246
+ },
247
+
248
+ toSummary(): object {
249
+ return {
250
+ startedAt: state.startedAt,
251
+ endedAt: new Date().toISOString(),
252
+ totals: {
253
+ inputTokens: state.totalInputTokens,
254
+ outputTokens: state.totalOutputTokens,
255
+ cacheReadTokens: state.totalCacheReadTokens,
256
+ cacheWriteTokens: state.totalCacheWriteTokens,
257
+ cost: Number(state.totalCost.toFixed(6)),
258
+ turns: state.totalTurns,
259
+ },
260
+ subagents: [...state.subagents],
261
+ models: this.getPerModelUsage(),
262
+ };
263
+ },
264
+
265
+ reset(): void {
266
+ const next = createInitialState();
267
+ state.startedAt = next.startedAt;
268
+ state.totalInputTokens = 0;
269
+ state.totalOutputTokens = 0;
270
+ state.totalCacheReadTokens = 0;
271
+ state.totalCacheWriteTokens = 0;
272
+ state.totalCost = 0;
273
+ state.totalTurns = 0;
274
+ state.subagentInputTokens = 0;
275
+ state.subagentOutputTokens = 0;
276
+ state.subagentCost = 0;
277
+ state.models.clear();
278
+ state.subagents = [];
279
+ },
280
+ };
281
+ }
282
+
283
+ export function loadUsageSummary(sessionId: string): Record<string, unknown> | null {
284
+ const usageDir = join(resolveAgentDir(), "extensions", "pp", "usage");
285
+ const filePath = join(usageDir, `${sessionId}.json`);
286
+ if (!existsSync(filePath)) return null;
287
+ try {
288
+ return JSON.parse(readFileSync(filePath, "utf-8")) as Record<string, unknown>;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ export function dumpUsageSummary(tracker: UsageTracker, sessionId: string): void {
295
+ const usageDir = join(resolveAgentDir(), "extensions", "pp", "usage");
296
+ mkdirSync(usageDir, { recursive: true });
297
+
298
+ const summary = tracker.toSummary() as Record<string, unknown>;
299
+ const payload = {
300
+ sessionId,
301
+ ...summary,
302
+ };
303
+
304
+ const outPath = join(usageDir, `${sessionId}.json`);
305
+ writeFileSync(outPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
306
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",