@bugabinga/pi-ext-cost 0.1.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/CHANGELOG.md +12 -0
- package/README.md +11 -0
- package/assets/footer_suite.gif +0 -0
- package/index.ts +390 -0
- package/package.json +15 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-05-21
|
|
4
|
+
|
|
5
|
+
- a40a427 prepare extensions for npm release
|
|
6
|
+
- 6a97080 add asciinema demo workflow
|
|
7
|
+
- 517ba54 pi: add extension footer fallbacks
|
|
8
|
+
- 133cb7d chore(pi): migrate extensions to earendil packages
|
|
9
|
+
- 5ca1296 Rework Pi agent extensions
|
|
10
|
+
- b87a61a feat(pi): monorepo workspace — all extensions are proper packages
|
|
11
|
+
- b408bba pi(ext): footer segment producers (context, timing, cost, runtime, git-status, pr-status, thinking)
|
|
12
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# cost
|
|
2
|
+
|
|
3
|
+
Pi model, token, and cost telemetry.
|
|
4
|
+
|
|
5
|
+
Shows model id, last-turn tokens, session cost, and monthly cost. Uses `footer:segment` with `@bugabinga/pi-ext-footer`; otherwise falls back to Pi status text.
|
|
6
|
+
|
|
7
|
+
## Demo
|
|
8
|
+
|
|
9
|
+
<!-- demo:footer_suite:start -->
|
|
10
|
+

|
|
11
|
+
<!-- demo:footer_suite:end -->
|
|
Binary file
|
package/index.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost, token, and model footer segments.
|
|
3
|
+
*
|
|
4
|
+
* Emits into the shared footer "llm" zone:
|
|
5
|
+
* - model: current model id
|
|
6
|
+
* - tokens: last assistant turn token usage
|
|
7
|
+
* - cost: turn/session/monthly spend when non-zero
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI, ExtensionContext, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { parseSessionEntries } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { readdirSync, readFileSync, type Dirent } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const FOOTER_KEY = Symbol.for("@bugabinga/pi-ext-footer");
|
|
18
|
+
const STATUS_ID = "cost";
|
|
19
|
+
const PRICE_UNIT_TOKENS = 1_000_000;
|
|
20
|
+
const COUNT_DECIMAL_THRESHOLD = 1_000;
|
|
21
|
+
const COUNT_ROUND_THRESHOLD = 10_000;
|
|
22
|
+
const LOW_COST_THRESHOLD = 0.01;
|
|
23
|
+
const MEDIUM_COST_THRESHOLD = 10;
|
|
24
|
+
const SESSION_EXT = ".jsonl";
|
|
25
|
+
const SESSION_MONTH_RE = /^(\d{4})-(\d{2})/;
|
|
26
|
+
|
|
27
|
+
const FOOTER = {
|
|
28
|
+
model: { id: "model", color: "syntaxKeyword", order: 0 },
|
|
29
|
+
tokens: { id: "tokens", color: "muted", order: 2 },
|
|
30
|
+
cost: { id: "cost", color: "success", order: 3 },
|
|
31
|
+
zone: "llm",
|
|
32
|
+
} as const satisfies Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
const UPDATE_EVENTS = ["agent_start", "agent_end", "tool_execution_end"] as const;
|
|
35
|
+
const RESET_SIGNATURE_EVENTS = ["model_select", "session_compact"] as const;
|
|
36
|
+
|
|
37
|
+
// ── Types ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
type PriceRow = {
|
|
40
|
+
input: number;
|
|
41
|
+
cacheRead: number;
|
|
42
|
+
output: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type TokenUsage = {
|
|
46
|
+
input: number;
|
|
47
|
+
output: number;
|
|
48
|
+
cacheRead: number;
|
|
49
|
+
cacheWrite: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type UsageWithCost = Partial<TokenUsage> & {
|
|
53
|
+
cost?: { total?: number };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type AssistantLikeMessage = {
|
|
57
|
+
role?: unknown;
|
|
58
|
+
usage?: UsageWithCost;
|
|
59
|
+
provider?: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type Branch = ReturnType<ExtensionContext["sessionManager"]["getBranch"]>;
|
|
64
|
+
|
|
65
|
+
type Snapshot = {
|
|
66
|
+
modelLabel: string;
|
|
67
|
+
usage: TokenUsage;
|
|
68
|
+
turnCost: number;
|
|
69
|
+
sessionCost: number;
|
|
70
|
+
monthlyCost: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type SegmentId = "model" | "tokens" | "cost";
|
|
74
|
+
|
|
75
|
+
// ── Footer capability ─────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function hasFooter(): boolean {
|
|
78
|
+
const footer = (globalThis as any)[FOOTER_KEY];
|
|
79
|
+
return footer?.loaded === true && footer.version >= 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Pricing ───────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
// Prices per 1M tokens. Used only when providers report $0 cost.
|
|
85
|
+
const PRICING: Record<string, Record<string, PriceRow>> = {
|
|
86
|
+
zai: {
|
|
87
|
+
"glm-5.1": { input: 1.4, cacheRead: 0.26, output: 4.4 },
|
|
88
|
+
"glm-5": { input: 1.0, cacheRead: 0.2, output: 4.0 },
|
|
89
|
+
"glm-5-turbo": { input: 0.5, cacheRead: 0.1, output: 2.0 },
|
|
90
|
+
"glm-4.7": { input: 1.0, cacheRead: 0.2, output: 4.0 },
|
|
91
|
+
"glm-4.7-flash": { input: 0.1, cacheRead: 0.02, output: 0.4 },
|
|
92
|
+
"glm-4.7-flashx": { input: 0.1, cacheRead: 0.02, output: 0.4 },
|
|
93
|
+
"glm-4.6": { input: 1.0, cacheRead: 0.2, output: 4.0 },
|
|
94
|
+
"glm-4.5": { input: 1.0, cacheRead: 0.2, output: 4.0 },
|
|
95
|
+
"glm-4.5-air": { input: 0.1, cacheRead: 0.02, output: 0.4 },
|
|
96
|
+
"glm-4.5-flash": { input: 0.1, cacheRead: 0.02, output: 0.4 },
|
|
97
|
+
},
|
|
98
|
+
minimax: {
|
|
99
|
+
"MiniMax-M2.7": { input: 1.0, cacheRead: 0.06, output: 4.0 },
|
|
100
|
+
"MiniMax-M2.7-highspeed": { input: 0.6, cacheRead: 0.06, output: 2.4 },
|
|
101
|
+
"MiniMax-M1": { input: 1.0, cacheRead: 0.06, output: 4.0 },
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function priceFor(provider: string, model: string): PriceRow | undefined {
|
|
106
|
+
const providerPrices = PRICING[provider];
|
|
107
|
+
if (!providerPrices) return undefined;
|
|
108
|
+
|
|
109
|
+
return providerPrices[model]
|
|
110
|
+
?? Object.entries(providerPrices).find(([prefix]) => model.startsWith(prefix))?.[1];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function manualTokenCost(provider: string, model: string, usage: TokenUsage): number {
|
|
114
|
+
const price = priceFor(provider, model);
|
|
115
|
+
if (!price) return 0;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
usage.input * price.input
|
|
119
|
+
+ usage.cacheRead * price.cacheRead
|
|
120
|
+
+ usage.output * price.output
|
|
121
|
+
) / PRICE_UNIT_TOKENS;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Message extraction ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function isAssistantMessageEntry(entry: unknown): entry is { type: "message"; message: AssistantLikeMessage } {
|
|
127
|
+
const candidate = entry as { type?: unknown; message?: AssistantLikeMessage } | undefined;
|
|
128
|
+
return candidate?.type === "message" && candidate.message?.role === "assistant";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeUsage(usage: UsageWithCost | undefined): TokenUsage {
|
|
132
|
+
return {
|
|
133
|
+
input: usage?.input ?? 0,
|
|
134
|
+
output: usage?.output ?? 0,
|
|
135
|
+
cacheRead: usage?.cacheRead ?? 0,
|
|
136
|
+
cacheWrite: usage?.cacheWrite ?? 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function messageCost(message: AssistantLikeMessage): number {
|
|
141
|
+
const usage = normalizeUsage(message.usage);
|
|
142
|
+
const reported = message.usage?.cost?.total ?? 0;
|
|
143
|
+
if (reported > 0) return reported;
|
|
144
|
+
|
|
145
|
+
return manualTokenCost(message.provider ?? "", message.model ?? "", usage);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sumMessageCosts(entries: readonly unknown[]): number {
|
|
149
|
+
let total = 0;
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (isAssistantMessageEntry(entry)) total += messageCost(entry.message);
|
|
152
|
+
}
|
|
153
|
+
return total;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function lastAssistantUsage(branch: Branch): TokenUsage {
|
|
157
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
158
|
+
const entry = branch[i];
|
|
159
|
+
if (!isAssistantMessageEntry(entry)) continue;
|
|
160
|
+
if (!entry.message.usage) continue;
|
|
161
|
+
return normalizeUsage(entry.message.usage);
|
|
162
|
+
}
|
|
163
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Session file scan ─────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function safeReadDir(path: string): Dirent[] {
|
|
169
|
+
try {
|
|
170
|
+
return readdirSync(path, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function currentMonthKey(date = new Date()): string {
|
|
177
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sessionFileMonth(name: string): string | undefined {
|
|
181
|
+
const match = name.match(SESSION_MONTH_RE);
|
|
182
|
+
return match ? `${match[1]}-${match[2]}` : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isSessionFileForMonth(file: Dirent, month: string): boolean {
|
|
186
|
+
return file.isFile() && file.name.endsWith(SESSION_EXT) && sessionFileMonth(file.name) === month;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sessionFilesForMonth(sessionDir: string, month: string): string[] {
|
|
190
|
+
const root = dirname(sessionDir);
|
|
191
|
+
const files: string[] = [];
|
|
192
|
+
|
|
193
|
+
for (const entry of safeReadDir(root)) {
|
|
194
|
+
if (entry.isFile() && isSessionFileForMonth(entry, month)) {
|
|
195
|
+
files.push(join(root, entry.name));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!entry.isDirectory()) continue;
|
|
200
|
+
const childDir = join(root, entry.name);
|
|
201
|
+
for (const file of safeReadDir(childDir)) {
|
|
202
|
+
if (isSessionFileForMonth(file, month)) files.push(join(childDir, file.name));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return files;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sumFileCosts(filePath: string): number {
|
|
210
|
+
try {
|
|
211
|
+
return sumMessageCosts(parseSessionEntries(readFileSync(filePath, "utf-8")));
|
|
212
|
+
} catch {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function calculateMonthlyCost(sessionDir: string): Promise<number> {
|
|
218
|
+
let total = 0;
|
|
219
|
+
for (const file of sessionFilesForMonth(sessionDir, currentMonthKey())) {
|
|
220
|
+
total += sumFileCosts(file);
|
|
221
|
+
}
|
|
222
|
+
return total;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Formatting ────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function formatCount(value: number): string {
|
|
228
|
+
if (value < COUNT_DECIMAL_THRESHOLD) return `${value}`;
|
|
229
|
+
if (value < COUNT_ROUND_THRESHOLD) return `${(value / COUNT_DECIMAL_THRESHOLD).toFixed(1)}k`;
|
|
230
|
+
return `${Math.round(value / COUNT_DECIMAL_THRESHOLD)}k`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatCost(value: number): string {
|
|
234
|
+
if (value < LOW_COST_THRESHOLD) return `$${value.toFixed(4)}`;
|
|
235
|
+
if (value < MEDIUM_COST_THRESHOLD) return `$${value.toFixed(2)}`;
|
|
236
|
+
return `$${value.toFixed(1)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function modelLabel(ctx: ExtensionContext): string {
|
|
240
|
+
const modelId = ctx.model?.id ?? "no-model";
|
|
241
|
+
return modelId.split("/").pop() ?? modelId;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function tokenLabel(usage: TokenUsage): string {
|
|
245
|
+
const totalPrompt = usage.input + usage.cacheRead;
|
|
246
|
+
if (totalPrompt === 0 && usage.output === 0) return "↑0";
|
|
247
|
+
if (usage.input === 0 && usage.cacheRead > 0) return `◐ ${formatCount(usage.cacheRead)}`;
|
|
248
|
+
if (usage.cacheRead === 0) return `↑${formatCount(usage.input)} ↓${formatCount(usage.output)}`;
|
|
249
|
+
|
|
250
|
+
const cachePercent = Math.round((usage.cacheRead / totalPrompt) * 100);
|
|
251
|
+
return `◐${formatCount(usage.input)} ↓${formatCount(usage.output)} (${cachePercent}% cached)`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function costLabel(snapshot: Snapshot): string | undefined {
|
|
255
|
+
const parts: string[] = [];
|
|
256
|
+
if (snapshot.turnCost > 0) parts.push(formatCost(snapshot.turnCost));
|
|
257
|
+
if (snapshot.sessionCost > 0) parts.push(`Σ${formatCost(snapshot.sessionCost)}`);
|
|
258
|
+
if (snapshot.monthlyCost > snapshot.sessionCost) parts.push(` ${formatCost(snapshot.monthlyCost)}`);
|
|
259
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Footer emission ───────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function emitSegment(pi: ExtensionAPI, id: SegmentId, text: string | undefined) {
|
|
265
|
+
if (text === undefined) {
|
|
266
|
+
pi.events.emit("footer:segment", { id: FOOTER[id].id, text: undefined });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
pi.events.emit("footer:segment", {
|
|
271
|
+
id: FOOTER[id].id,
|
|
272
|
+
text,
|
|
273
|
+
color: FOOTER[id].color as ThemeColor,
|
|
274
|
+
zone: FOOTER.zone,
|
|
275
|
+
order: FOOTER[id].order,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function statusLabel(snapshot: Snapshot): string {
|
|
280
|
+
return [snapshot.modelLabel, tokenLabel(snapshot.usage), costLabel(snapshot)]
|
|
281
|
+
.filter((part): part is string => !!part)
|
|
282
|
+
.join(" · ");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function publishSnapshot(pi: ExtensionAPI, ctx: ExtensionContext, snapshot: Snapshot) {
|
|
286
|
+
if (hasFooter()) {
|
|
287
|
+
ctx.ui.setStatus(STATUS_ID, undefined);
|
|
288
|
+
emitSegment(pi, "model", snapshot.modelLabel);
|
|
289
|
+
emitSegment(pi, "tokens", tokenLabel(snapshot.usage));
|
|
290
|
+
emitSegment(pi, "cost", costLabel(snapshot));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
clearSegments(pi);
|
|
295
|
+
ctx.ui.setStatus(STATUS_ID, statusLabel(snapshot));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function clearSegments(pi: ExtensionAPI) {
|
|
299
|
+
emitSegment(pi, "model", undefined);
|
|
300
|
+
emitSegment(pi, "tokens", undefined);
|
|
301
|
+
emitSegment(pi, "cost", undefined);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function clearPublished(pi: ExtensionAPI, ctx?: ExtensionContext) {
|
|
305
|
+
clearSegments(pi);
|
|
306
|
+
if (ctx?.hasUI) ctx.ui.setStatus(STATUS_ID, undefined);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Snapshot ──────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
function buildSnapshot(ctx: ExtensionContext, previousSessionCost: number, monthlyCost: number): Snapshot {
|
|
312
|
+
const branch = ctx.sessionManager.getBranch();
|
|
313
|
+
const sessionCost = sumMessageCosts(branch);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
modelLabel: modelLabel(ctx),
|
|
317
|
+
usage: lastAssistantUsage(branch),
|
|
318
|
+
turnCost: Math.max(0, sessionCost - previousSessionCost),
|
|
319
|
+
sessionCost,
|
|
320
|
+
monthlyCost,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function signature(snapshot: Snapshot): string {
|
|
325
|
+
const usage = snapshot.usage;
|
|
326
|
+
return [
|
|
327
|
+
snapshot.modelLabel,
|
|
328
|
+
usage.input,
|
|
329
|
+
usage.output,
|
|
330
|
+
usage.cacheRead,
|
|
331
|
+
usage.cacheWrite,
|
|
332
|
+
snapshot.sessionCost,
|
|
333
|
+
snapshot.monthlyCost,
|
|
334
|
+
].join("|");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Extension ─────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
export default function costExtension(pi: ExtensionAPI) {
|
|
340
|
+
let previousSessionCost = 0;
|
|
341
|
+
let monthlyCost = 0;
|
|
342
|
+
let lastSignature = "";
|
|
343
|
+
let hasUI = false;
|
|
344
|
+
|
|
345
|
+
async function sync(ctx: ExtensionContext) {
|
|
346
|
+
if (!hasUI) return;
|
|
347
|
+
|
|
348
|
+
const snapshot = buildSnapshot(ctx, previousSessionCost, monthlyCost);
|
|
349
|
+
previousSessionCost = snapshot.sessionCost;
|
|
350
|
+
|
|
351
|
+
const nextSignature = signature(snapshot);
|
|
352
|
+
if (nextSignature === lastSignature) return;
|
|
353
|
+
lastSignature = nextSignature;
|
|
354
|
+
|
|
355
|
+
publishSnapshot(pi, ctx, snapshot);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function syncBestEffort(ctx: ExtensionContext) {
|
|
359
|
+
try {
|
|
360
|
+
await sync(ctx);
|
|
361
|
+
} catch {
|
|
362
|
+
// Footer telemetry must never disturb the agent loop.
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
367
|
+
hasUI = ctx.hasUI;
|
|
368
|
+
if (!hasUI) return;
|
|
369
|
+
|
|
370
|
+
previousSessionCost = 0;
|
|
371
|
+
lastSignature = "";
|
|
372
|
+
monthlyCost = await calculateMonthlyCost(ctx.sessionManager.getSessionDir());
|
|
373
|
+
await syncBestEffort(ctx);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
for (const eventName of UPDATE_EVENTS) {
|
|
377
|
+
pi.on(eventName, async (_event, ctx) => syncBestEffort(ctx));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const eventName of RESET_SIGNATURE_EVENTS) {
|
|
381
|
+
pi.on(eventName, async (_event, ctx) => {
|
|
382
|
+
lastSignature = "";
|
|
383
|
+
await syncBestEffort(ctx);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
388
|
+
clearPublished(pi, ctx);
|
|
389
|
+
});
|
|
390
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bugabinga/pi-ext-cost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"description": "Model, token, and spend telemetry for Pi.",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi",
|
|
13
|
+
"pi-extension"
|
|
14
|
+
]
|
|
15
|
+
}
|