@firstpick/pi-extension-stats 0.1.7 → 0.1.9

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 +19 -2
  2. package/index.ts +238 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@ Token and cost analytics for Pi session history.
9
9
  - Parses local Pi session `.jsonl` files for the current workspace.
10
10
  - Aggregates usage by UTC day.
11
11
  - Displays compact daily token bars and cost bars with totals.
12
- - Shows input/output/cache breakdown, prompt-injection estimate (`PI: X tok`) with source split-up, cache hit rate, estimated cache savings, cost burn rate, and top model usage.
12
+ - Shows input/output/cache breakdown, estimated initial prompt input (`PI: X tok`) with source split-up, cache hit rate, estimated cache savings, cost burn rate, and top model usage.
13
13
  - Highlights highest-cost day, projected 30-day cost, most expensive sessions, and model cost efficiency.
14
14
 
15
15
  ## Install
@@ -26,13 +26,30 @@ No required configuration.
26
26
 
27
27
  - `/stats [days|all]` — show token usage dashboard (default: last 14 days).
28
28
  - `/stats tokens` — show current context token breakdown by source/type.
29
- - `/stats-pi` — show prompt-injection token breakdown.
29
+ - `/stats-pi` — show estimated initial prompt input token breakdown. It counts Pi's system prompt text, active provider-level tool schemas, framing overhead, and optional historical calibration.
30
+ - `/calibrate` — start an isolated calibration session with a fixed probe prompt, then update `/stats-pi` and the footer `PI: X tok` estimate from the first assistant response usage. `/calibrate current` reuses the current branch if it already has a suitable first-turn usage sample.
30
31
  - `/stats-last [days|all]` — show non-zero daily usage graph.
31
32
  - `/stats-most-expense [days|all]` — show most expensive sessions.
32
33
  - `/stats-model-compare [days|all]` — show model token/cost comparison.
33
34
  - `/stats-cost-trend [days|all]` — show cost trend and projections.
34
35
  - `/stats-cache [days|all]` — show cache efficiency and token mix.
35
36
 
37
+ ## Prompt input estimate
38
+
39
+ `/stats-pi` and the `PI: ~X tok` value in `/stats` estimate the full initial model input, not just raw prompt text. `/stats-pi` can be run before any LLM prompt in a fresh session.
40
+
41
+ The calculation is intentionally provider-agnostic:
42
+
43
+ ```text
44
+ promptTextTokens = weighted text estimate of ctx.getSystemPrompt()
45
+ toolSchemaTokens = weighted text estimate of active tool definitions JSON
46
+ framingTokens = conservative message/request framing allowance
47
+ baseEstimate = promptTextTokens + toolSchemaTokens + framingTokens
48
+ estimatedInitialInput = baseEstimate × historicalCalibrationMultiplier
49
+ ```
50
+
51
+ The historical multiplier is learned opportunistically from future sessions by comparing the pre-call estimate with the provider-reported first assistant `usage.input + usage.cacheRead + usage.cacheWrite` after subtracting the first user prompt estimate. `/calibrate` performs the same calculation on demand by opening an isolated session and sending a fixed probe prompt; `/calibrate current` can reuse the current branch once its first assistant response has usage data. Without samples, `/stats-pi` reports an uncalibrated estimate and a conservative range. Provider-reported usage in Pi session JSONL remains the authoritative post-call value.
52
+
36
53
  ## Tools
37
54
 
38
55
  None.
package/index.ts CHANGED
@@ -2,7 +2,18 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { buildSessionContext, formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
4
4
  import type { BuildSystemPromptOptions, ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
5
- import { estimatePromptInjectionTokens, estimateTokensFromCharCount, formatTokens } from "@firstpick/pi-utils";
5
+ import {
6
+ appendInitialPromptCalibrationRecord,
7
+ buildInitialPromptCalibrationRecord,
8
+ collectInitialPromptCalibration,
9
+ estimateInitialPromptInput,
10
+ estimatePromptInjectionTokens,
11
+ estimateTokensFromCharCount,
12
+ formatTokens,
13
+ type InitialPromptCalibration,
14
+ type InitialPromptInputEstimate,
15
+ type InitialPromptToolInfo,
16
+ } from "@firstpick/pi-utils";
6
17
 
7
18
  type DayUsage = {
8
19
  input: number;
@@ -31,6 +42,12 @@ type UsageRecord = {
31
42
 
32
43
  type Totals = DayUsage;
33
44
 
45
+ type PendingInitialPromptMeasurement = {
46
+ estimate: InitialPromptInputEstimate;
47
+ firstUserTokens: number;
48
+ skipReason?: string;
49
+ };
50
+
34
51
  type PromptInjectionSource = {
35
52
  label: string;
36
53
  chars: number;
@@ -44,6 +61,8 @@ type TokenBreakdownSource = {
44
61
  const DEFAULT_DAYS = 14;
45
62
  const MAX_BAR_WIDTH = 24;
46
63
  const COST_BAR_WIDTH = 10;
64
+ const FIRST_USER_MESSAGE_FRAMING_TOKENS = 16;
65
+ const CALIBRATION_PROMPT = "Calibration probe: reply with exactly `calibration-ok` and no other text.";
47
66
 
48
67
  function addPromptSource(sources: PromptInjectionSource[], label: string, content: string | undefined): number {
49
68
  if (!content) return 0;
@@ -172,22 +191,65 @@ function buildPromptInjectionSources(systemPrompt: string, options: BuildSystemP
172
191
  return sources;
173
192
  }
174
193
 
175
- function formatPromptInjectionLines(systemPrompt: string, options: BuildSystemPromptOptions | null): string[] {
176
- const sources = buildPromptInjectionSources(systemPrompt, options)
177
- .map((source) => ({ ...source, tokens: estimateTokensFromCharCount(source.chars) }))
194
+ function formatTokenCell(tokens: number): string {
195
+ return tokens < 0 ? `-${formatTokens(Math.abs(tokens))}` : formatTokens(tokens);
196
+ }
197
+
198
+ function formatCalibrationSummary(estimate: InitialPromptInputEstimate): string {
199
+ if (estimate.calibrationSamples <= 0) return "uncalibrated";
200
+ const sampleLabel = estimate.calibrationSamples === 1 ? "sample" : "samples";
201
+ return `learned scale ×${estimate.calibrationMultiplier.toFixed(2)} from ${estimate.calibrationSamples} ${sampleLabel}`;
202
+ }
203
+
204
+ function distributeCalibratedTokens<T extends { tokens: number }>(sources: T[], calibratedTotal: number): T[] {
205
+ const uncalibratedTotal = sources.reduce((sum, source) => sum + source.tokens, 0);
206
+ if (uncalibratedTotal <= 0 || calibratedTotal <= 0) return sources.map((source) => ({ ...source, tokens: 0 }));
207
+
208
+ const exact = sources.map((source, index) => {
209
+ const scaled = (source.tokens / uncalibratedTotal) * calibratedTotal;
210
+ const tokens = Math.floor(scaled);
211
+ return { index, tokens, remainder: scaled - tokens };
212
+ });
213
+ let remaining = calibratedTotal - exact.reduce((sum, source) => sum + source.tokens, 0);
214
+ for (const source of [...exact].sort((a, b) => b.remainder - a.remainder || a.index - b.index)) {
215
+ if (remaining <= 0) break;
216
+ source.tokens += 1;
217
+ remaining -= 1;
218
+ }
219
+
220
+ return sources.map((source, index) => ({ ...source, tokens: exact[index]?.tokens ?? 0 }));
221
+ }
222
+
223
+ function formatPromptInjectionLines(systemPrompt: string, options: BuildSystemPromptOptions | null, estimate: InitialPromptInputEstimate): string[] {
224
+ const promptSources = buildPromptInjectionSources(systemPrompt, options)
225
+ .map((source) => ({
226
+ ...source,
227
+ tokens: systemPrompt.length > 0 ? Math.round((source.chars / systemPrompt.length) * estimate.promptText) : estimateTokensFromCharCount(source.chars),
228
+ }))
178
229
  .sort((a, b) => b.tokens - a.tokens || b.chars - a.chars);
179
- const totalTokens = estimatePromptInjectionTokens(systemPrompt);
230
+ const uncalibratedSources = [
231
+ estimate.toolSchemas > 0
232
+ ? { label: `Active tool schemas (${estimate.toolCount})`, chars: 0, tokens: estimate.toolSchemas }
233
+ : null,
234
+ estimate.framing > 0 ? { label: "Provider/request framing", chars: 0, tokens: estimate.framing } : null,
235
+ ...promptSources,
236
+ ].filter((source): source is { label: string; chars: number; tokens: number } => !!source && source.tokens !== 0);
237
+ const sources = distributeCalibratedTokens(uncalibratedSources, estimate.total)
238
+ .filter((source) => source.tokens !== 0)
239
+ .sort((a, b) => Math.abs(b.tokens) - Math.abs(a.tokens) || b.chars - a.chars);
180
240
  const labelWidth = Math.max("Source".length, ...sources.map((source) => source.label.length));
181
- const tokenWidth = Math.max("Tokens".length, ...sources.map((source) => formatTokens(source.tokens).length));
241
+ const tokenWidth = Math.max("Tokens".length, ...sources.map((source) => formatTokenCell(source.tokens).length));
182
242
  const percentWidth = "%".length;
183
243
  const separator = `├${"─".repeat(labelWidth + 2)}┼${"─".repeat(tokenWidth + 2)}┼${"─".repeat(percentWidth + 6)}┤`;
184
244
  const rows = sources.map((source) => {
185
- const percent = totalTokens > 0 ? `${((source.tokens / totalTokens) * 100).toFixed(1)}%` : "0.0%";
186
- return `│ ${source.label.padEnd(labelWidth)} │ ${formatTokens(source.tokens).padStart(tokenWidth)} │ ${percent.padStart(percentWidth + 4)} │`;
245
+ const percent = estimate.total > 0 ? `${((source.tokens / estimate.total) * 100).toFixed(1)}%` : "0.0%";
246
+ return `│ ${source.label.padEnd(labelWidth)} │ ${formatTokenCell(source.tokens).padStart(tokenWidth)} │ ${percent.padStart(percentWidth + 4)} │`;
187
247
  });
248
+ const range = estimate.low !== estimate.high ? ` · range ${formatTokens(estimate.low)}–${formatTokens(estimate.high)}` : "";
188
249
 
189
250
  return [
190
- `Prompt injection: PI: ${formatTokens(totalTokens)} tok`,
251
+ `Prompt injection: PI: ~${formatTokens(estimate.total)} tok initial input (${estimate.confidence}${range})`,
252
+ `Unscaled basis: prompt text ${formatTokens(estimate.promptText)} + tool schemas ${formatTokens(estimate.toolSchemas)} + framing ${formatTokens(estimate.framing)} · displayed rows are proportionally scaled (${formatCalibrationSummary(estimate)})`,
191
253
  `┌${"─".repeat(labelWidth + 2)}┬${"─".repeat(tokenWidth + 2)}┬${"─".repeat(percentWidth + 6)}┐`,
192
254
  `│ ${"Source".padEnd(labelWidth)} │ ${"Tokens".padStart(tokenWidth)} │ ${"%".padStart(percentWidth + 4)} │`,
193
255
  separator,
@@ -606,9 +668,129 @@ function buildCacheEfficiencyLines(totals: Totals): string[] {
606
668
 
607
669
  export default function statsExtension(pi: ExtensionAPI) {
608
670
  let latestSystemPromptOptions: BuildSystemPromptOptions | null = null;
671
+ let pendingInitialPromptMeasurement: PendingInitialPromptMeasurement | null = null;
672
+
673
+ const getToolEstimateInputs = (): { activeTools: string[]; allTools: InitialPromptToolInfo[] } => {
674
+ let activeTools: string[] = [];
675
+ let allTools: InitialPromptToolInfo[] = [];
609
676
 
610
- pi.on("before_agent_start", async (event) => {
677
+ try {
678
+ activeTools = pi.getActiveTools();
679
+ } catch {
680
+ activeTools = [];
681
+ }
682
+
683
+ try {
684
+ allTools = pi.getAllTools().map((tool) => ({
685
+ name: tool.name,
686
+ description: tool.description,
687
+ parameters: tool.parameters,
688
+ }));
689
+ } catch {
690
+ allTools = [];
691
+ }
692
+
693
+ return { activeTools, allTools };
694
+ };
695
+
696
+ const getPromptCalibration = (ctx: ExtensionCommandContext): InitialPromptCalibration | null => {
697
+ return collectInitialPromptCalibration(ctx.sessionManager.getSessionDir());
698
+ };
699
+
700
+ const estimateInitialPromptForContext = (systemPrompt: string, calibration?: InitialPromptCalibration | null): InitialPromptInputEstimate => {
701
+ const { activeTools, allTools } = getToolEstimateInputs();
702
+ return estimateInitialPromptInput({ systemPrompt, activeTools, allTools, calibration });
703
+ };
704
+
705
+ const branchHasAssistantUsage = (ctx: { sessionManager: { getBranch(): unknown[] } }): boolean => {
706
+ try {
707
+ return ctx.sessionManager.getBranch().some((entry) => {
708
+ const record = (entry && typeof entry === "object" ? entry : {}) as Record<string, any>;
709
+ return record.type === "message" && record.message?.role === "assistant" && !!record.message?.usage;
710
+ });
711
+ } catch {
712
+ return true;
713
+ }
714
+ };
715
+
716
+ const calibrateFromCurrentBranch = (ctx: ExtensionCommandContext): { ok: true; record: NonNullable<ReturnType<typeof buildInitialPromptCalibrationRecord>> } | { ok: false; reason: string } => {
717
+ let firstUserTokens: number | null = null;
718
+ let firstAssistantWithUsage: Record<string, any> | null = null;
719
+
720
+ for (const entry of ctx.sessionManager.getBranch()) {
721
+ const record = (entry && typeof entry === "object" ? entry : {}) as Record<string, any>;
722
+ if (record.type !== "message") continue;
723
+
724
+ const message = (record.message && typeof record.message === "object" ? record.message : {}) as Record<string, any>;
725
+ if (message.role === "user" && firstUserTokens === null) {
726
+ firstUserTokens = estimatePromptInjectionTokens(stringifyContextValue(message.content)) + FIRST_USER_MESSAGE_FRAMING_TOKENS;
727
+ }
728
+ if (message.role === "assistant" && message.usage) {
729
+ firstAssistantWithUsage = message;
730
+ break;
731
+ }
732
+ }
733
+
734
+ if (firstUserTokens === null) return { ok: false, reason: "No initial user message found in the current branch." };
735
+ if (!firstAssistantWithUsage) return { ok: false, reason: "No assistant response with usage data found yet. Run /calibrate after the first assistant response finishes." };
736
+
737
+ const usage = firstAssistantWithUsage.usage as Record<string, unknown>;
738
+ const actualInitialInputTokens =
739
+ (Number(usage.input ?? 0) || 0) + (Number(usage.cacheRead ?? 0) || 0) + (Number(usage.cacheWrite ?? 0) || 0);
740
+ if (actualInitialInputTokens <= 0) return { ok: false, reason: "The first assistant response has no input/cache token usage to calibrate from." };
741
+
742
+ const estimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), null);
743
+ const record = buildInitialPromptCalibrationRecord({
744
+ estimate,
745
+ actualInitialInputTokens,
746
+ firstUserTokens,
747
+ provider: String(firstAssistantWithUsage.provider ?? ctx.model?.provider ?? "unknown"),
748
+ model: String(firstAssistantWithUsage.responseModel ?? firstAssistantWithUsage.model ?? ctx.model?.id ?? "unknown"),
749
+ });
750
+ if (!record) return { ok: false, reason: "Calibration sample was outside the accepted sanity range (0.25×–4×)." };
751
+ return { ok: true, record };
752
+ };
753
+
754
+ pi.on("session_start", async () => {
755
+ pendingInitialPromptMeasurement = null;
756
+ });
757
+
758
+ pi.on("before_agent_start", async (event, ctx) => {
611
759
  latestSystemPromptOptions = event.systemPromptOptions;
760
+
761
+ if (!branchHasAssistantUsage(ctx)) {
762
+ pendingInitialPromptMeasurement = {
763
+ estimate: estimateInitialPromptForContext(event.systemPrompt, null),
764
+ firstUserTokens: estimatePromptInjectionTokens(event.prompt) + FIRST_USER_MESSAGE_FRAMING_TOKENS,
765
+ skipReason: event.images && event.images.length > 0 ? "image prompt" : undefined,
766
+ };
767
+ }
768
+ });
769
+
770
+ pi.on("agent_start", async (_event, ctx) => {
771
+ if (!pendingInitialPromptMeasurement || pendingInitialPromptMeasurement.skipReason) return;
772
+ pendingInitialPromptMeasurement.estimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), null);
773
+ });
774
+
775
+ pi.on("message_end", async (event) => {
776
+ if (!pendingInitialPromptMeasurement) return;
777
+ const pending = pendingInitialPromptMeasurement;
778
+ const message = (event.message && typeof event.message === "object" ? event.message : {}) as Record<string, any>;
779
+ if (message.role !== "assistant" || !message.usage) return;
780
+ pendingInitialPromptMeasurement = null;
781
+ if (pending.skipReason) return;
782
+
783
+ const usage = message.usage as Record<string, unknown>;
784
+ const actualInitialInputTokens =
785
+ (Number(usage.input ?? 0) || 0) + (Number(usage.cacheRead ?? 0) || 0) + (Number(usage.cacheWrite ?? 0) || 0);
786
+ const record = buildInitialPromptCalibrationRecord({
787
+ estimate: pending.estimate,
788
+ actualInitialInputTokens,
789
+ firstUserTokens: pending.firstUserTokens,
790
+ provider: String(message.provider ?? "unknown"),
791
+ model: String(message.responseModel ?? message.model ?? "unknown"),
792
+ });
793
+ if (record) appendInitialPromptCalibrationRecord(pi.appendEntry, record);
612
794
  });
613
795
 
614
796
  const showCurrentContextTokens = (ctx: ExtensionCommandContext) => {
@@ -633,8 +815,9 @@ export default function statsExtension(pi: ExtensionAPI) {
633
815
  const byDay = aggregateUsageByDay(records);
634
816
  const dayKeys = getScopeDayKeys(byDay, parsedArgs);
635
817
  const totals = sumUsage(byDay, dayKeys);
818
+ const calibration = collectInitialPromptCalibration(sessionDir);
636
819
  const scopeLabel = parsedArgs.mode === "all" ? "all days" : `last ${parsedArgs.days} days`;
637
- return { files, records, byDay, dayKeys, totals, scopeLabel };
820
+ return { files, records, byDay, dayKeys, totals, calibration, scopeLabel };
638
821
  };
639
822
 
640
823
  const parseStatsCommandArgs = (args: string, ctx: ExtensionCommandContext) => {
@@ -702,14 +885,51 @@ export default function statsExtension(pi: ExtensionAPI) {
702
885
  );
703
886
 
704
887
  registerScopedStatsCommand("stats-last", "Show non-zero daily usage graph. Usage: /stats-last [days|all]", (data, ctx) => {
705
- const promptInjectionTokens = estimatePromptInjectionTokens(ctx.getSystemPrompt());
706
- return [`📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI: ${formatTokens(promptInjectionTokens)} tok`, "", ...buildGraphLines(data.byDay, data.dayKeys, true)];
888
+ const promptEstimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), data.calibration);
889
+ return [`📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI: ~${formatTokens(promptEstimate.total)} tok`, "", ...buildGraphLines(data.byDay, data.dayKeys, true)];
707
890
  });
708
891
 
709
892
  pi.registerCommand("stats-pi", {
710
- description: "Show prompt-injection token breakdown.",
893
+ description: "Show estimated initial prompt input token breakdown.",
711
894
  handler: async (_args, ctx) => {
712
- ctx.ui.notify(formatPromptInjectionLines(ctx.getSystemPrompt(), latestSystemPromptOptions).join("\n"), "info");
895
+ const systemPrompt = ctx.getSystemPrompt();
896
+ const promptEstimate = estimateInitialPromptForContext(systemPrompt, getPromptCalibration(ctx));
897
+ ctx.ui.notify(formatPromptInjectionLines(systemPrompt, latestSystemPromptOptions, promptEstimate).join("\n"), "info");
898
+ },
899
+ });
900
+
901
+ pi.registerCommand("calibrate", {
902
+ description: "Start an isolated calibration turn to calibrate PI initial prompt token estimates.",
903
+ handler: async (args, ctx) => {
904
+ const mode = args.trim().toLowerCase();
905
+ if (mode === "current" || mode === "here") {
906
+ const result = calibrateFromCurrentBranch(ctx);
907
+ if (!result.ok) {
908
+ ctx.ui.notify(`Calibration failed: ${result.reason}`, "warning");
909
+ return;
910
+ }
911
+
912
+ appendInitialPromptCalibrationRecord(pi.appendEntry, result.record);
913
+ const calibration = getPromptCalibration(ctx);
914
+ const estimate = estimateInitialPromptForContext(ctx.getSystemPrompt(), calibration);
915
+ ctx.ui.notify(
916
+ `Calibrated PI estimate: ~${formatTokens(estimate.total)} tok (scale ×${estimate.calibrationMultiplier.toFixed(2)}, ${estimate.calibrationSamples} samples). Run /stats-pi for details.`,
917
+ "info",
918
+ );
919
+ return;
920
+ }
921
+
922
+ if (!ctx.isIdle()) {
923
+ ctx.ui.notify("Calibration needs an idle agent so it can start a clean probe turn.", "warning");
924
+ return;
925
+ }
926
+
927
+ ctx.ui.notify("Starting isolated calibration session…", "info");
928
+ await ctx.newSession({
929
+ withSession: async (newCtx) => {
930
+ await newCtx.sendUserMessage(CALIBRATION_PROMPT);
931
+ },
932
+ });
713
933
  },
714
934
  });
715
935
 
@@ -726,9 +946,9 @@ export default function statsExtension(pi: ExtensionAPI) {
726
946
  if (!data) return;
727
947
 
728
948
  const systemPrompt = ctx.getSystemPrompt();
729
- const promptInjectionTokens = estimatePromptInjectionTokens(systemPrompt);
949
+ const promptEstimate = estimateInitialPromptForContext(systemPrompt, data.calibration);
730
950
  const graphLines = buildGraphLines(data.byDay, data.dayKeys, true);
731
- const promptInjectionLines = formatPromptInjectionLines(systemPrompt, latestSystemPromptOptions);
951
+ const promptInjectionLines = formatPromptInjectionLines(systemPrompt, latestSystemPromptOptions, promptEstimate);
732
952
  const modelLines = formatModelComparisonLines(data.records, data.dayKeys, data.totals).slice(0, 7);
733
953
  const sessionLines = formatExpensiveSessionLines(data.records, data.dayKeys).slice(0, 7);
734
954
  const commandLines = [
@@ -737,7 +957,7 @@ export default function statsExtension(pi: ExtensionAPI) {
737
957
  ];
738
958
 
739
959
  ctx.ui.notify(
740
- `📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI: ${formatTokens(promptInjectionTokens)} tok\n\n${graphLines.join("\n")}\n\n${promptInjectionLines.join("\n")}\n\n${buildCostTrendLines(data.byDay, data.dayKeys).join("\n")}\n${buildCacheEfficiencyLines(data.totals).join("\n")}\n\n${modelLines.join("\n")}\n\n${sessionLines.join("\n")}\n\n${commandLines.join("\n")}`,
960
+ `📊 Token stats (${data.scopeLabel}, ${data.files.length} sessions) · PI: ~${formatTokens(promptEstimate.total)} tok\n\n${graphLines.join("\n")}\n\n${promptInjectionLines.join("\n")}\n\n${buildCostTrendLines(data.byDay, data.dayKeys).join("\n")}\n${buildCacheEfficiencyLines(data.totals).join("\n")}\n\n${modelLines.join("\n")}\n\n${sessionLines.join("\n")}\n\n${commandLines.join("\n")}`,
741
961
  "info",
742
962
  );
743
963
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-extension-stats",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Token and cost usage analytics command for Pi session history.",
5
5
  "license": "MIT",
6
6
  "keywords": [