@tmustier/pi-usage-extension 0.3.2 → 0.5.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/index.ts CHANGED
@@ -5,89 +5,17 @@
5
5
  * - Tab cycles: Today → This Week → Last Week → All Time
6
6
  * - Arrow keys navigate providers
7
7
  * - Enter expands/collapses to show models
8
+ *
9
+ * Data collection and caching live in ./data.ts.
8
10
  */
9
11
 
10
12
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
11
13
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
12
14
  import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
13
- import { readdir, readFile } from "node:fs/promises";
14
- import { join } from "node:path";
15
- import { homedir } from "node:os";
16
-
17
- // =============================================================================
18
- // Types
19
- // =============================================================================
20
-
21
- interface TokenStats {
22
- total: number;
23
- input: number;
24
- output: number;
25
- cacheRead: number;
26
- cacheWrite: number;
27
- }
28
-
29
- interface BaseStats {
30
- messages: number;
31
- cost: number;
32
- tokens: TokenStats;
33
- }
34
-
35
- interface ModelStats extends BaseStats {
36
- sessions: Set<string>;
37
- }
38
15
 
39
- interface ProviderStats extends BaseStats {
40
- sessions: Set<string>;
41
- models: Map<string, ModelStats>;
42
- }
43
-
44
- interface TotalStats extends BaseStats {
45
- sessions: number;
46
- }
47
-
48
- interface Insight {
49
- percent: number; // 0-100
50
- headline: string;
51
- advice: string;
52
- }
53
-
54
- interface PeriodInsights {
55
- insights: Insight[];
56
- }
57
-
58
- interface RawMessage {
59
- sessionId: string;
60
- timestamp: number;
61
- cost: number;
62
- input: number;
63
- cacheRead: number;
64
- cacheWrite: number;
65
- }
16
+ import { collectUsageData, TAB_ORDER } from "./data";
17
+ import type { BaseStats, TabName, UsageData } from "./data";
66
18
 
67
- interface PeriodRawData {
68
- messages: RawMessage[];
69
- sessionCosts: Map<string, number>;
70
- }
71
-
72
- interface GlobalSessionSpan {
73
- startMs: number;
74
- endMs: number;
75
- }
76
-
77
- interface TimeFilteredStats {
78
- providers: Map<string, ProviderStats>;
79
- totals: TotalStats;
80
- insights: PeriodInsights;
81
- }
82
-
83
- interface UsageData {
84
- today: TimeFilteredStats;
85
- thisWeek: TimeFilteredStats;
86
- lastWeek: TimeFilteredStats;
87
- allTime: TimeFilteredStats;
88
- }
89
-
90
- type TabName = "today" | "thisWeek" | "lastWeek" | "allTime";
91
19
  type ViewMode = "table" | "insights";
92
20
 
93
21
  // =============================================================================
@@ -182,482 +110,6 @@ const TABLE_LAYOUTS: TableLayoutCandidate[] = [
182
110
  { columns: [COST_COLUMN], minNameWidth: 8, compact: true },
183
111
  ];
184
112
 
185
- // =============================================================================
186
- // Data Collection
187
- // =============================================================================
188
-
189
- function getSessionsDir(): string {
190
- // Replicate Pi's logic: respect PI_CODING_AGENT_DIR env var
191
- const agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
192
- return join(agentDir, "sessions");
193
- }
194
-
195
- async function collectSessionFilesRecursively(dir: string, files: string[], signal?: AbortSignal): Promise<void> {
196
- try {
197
- const entries = await readdir(dir, { withFileTypes: true });
198
- for (const entry of entries) {
199
- if (signal?.aborted) return;
200
- const entryPath = join(dir, entry.name);
201
- if (entry.isDirectory()) {
202
- await collectSessionFilesRecursively(entryPath, files, signal);
203
- } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
204
- files.push(entryPath);
205
- }
206
- }
207
- } catch {
208
- // Skip directories we can't read
209
- }
210
- }
211
-
212
- async function getAllSessionFiles(signal?: AbortSignal): Promise<string[]> {
213
- const files: string[] = [];
214
- await collectSessionFilesRecursively(getSessionsDir(), files, signal);
215
- files.sort();
216
- return files;
217
- }
218
-
219
- interface SessionMessage {
220
- provider: string;
221
- model: string;
222
- cost: number;
223
- input: number;
224
- output: number;
225
- cacheRead: number;
226
- cacheWrite: number;
227
- timestamp: number;
228
- }
229
-
230
- interface ParsedSessionFile {
231
- sessionId: string;
232
- messages: SessionMessage[];
233
- }
234
-
235
- async function parseSessionFile(
236
- filePath: string,
237
- seenHashes: Set<string>,
238
- signal?: AbortSignal
239
- ): Promise<ParsedSessionFile | null> {
240
- try {
241
- const content = await readFile(filePath, "utf8");
242
- if (signal?.aborted) return null;
243
- const lines = content.trim().split("\n");
244
- const messages: SessionMessage[] = [];
245
- let sessionId = "";
246
-
247
- for (let i = 0; i < lines.length; i++) {
248
- if (signal?.aborted) return null;
249
- if (i % 500 === 0) {
250
- await new Promise<void>((resolve) => setImmediate(resolve));
251
- }
252
- const line = lines[i]!;
253
- if (!line.trim()) continue;
254
- try {
255
- const entry = JSON.parse(line);
256
-
257
- if (entry.type === "session") {
258
- sessionId = entry.id;
259
- } else if (entry.type === "message" && entry.message?.role === "assistant") {
260
- const msg = entry.message;
261
- if (msg.usage && msg.provider && msg.model) {
262
- const input = msg.usage.input || 0;
263
- const output = msg.usage.output || 0;
264
- const cacheRead = msg.usage.cacheRead || 0;
265
- const cacheWrite = msg.usage.cacheWrite || 0;
266
- const fallbackTs = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
267
- const timestamp = msg.timestamp || (Number.isNaN(fallbackTs) ? 0 : fallbackTs);
268
-
269
- // Deduplicate copied history across branched session files.
270
- // Keep the existing ccusage-style hash so current totals remain comparable.
271
- const totalTokens = input + output + cacheRead + cacheWrite;
272
- const hash = `${timestamp}:${totalTokens}`;
273
- if (seenHashes.has(hash)) continue;
274
- seenHashes.add(hash);
275
-
276
- messages.push({
277
- provider: msg.provider,
278
- model: msg.model,
279
- cost: msg.usage.cost?.total || 0,
280
- input,
281
- output,
282
- cacheRead,
283
- cacheWrite,
284
- timestamp,
285
- });
286
- }
287
- }
288
- } catch {
289
- // Skip malformed lines
290
- }
291
- }
292
-
293
- return sessionId ? { sessionId, messages } : null;
294
- } catch {
295
- return null;
296
- }
297
- }
298
-
299
- // Helper to accumulate stats into a target
300
- function accumulateStats(
301
- target: BaseStats,
302
- cost: number,
303
- tokens: { total: number; input: number; output: number; cacheRead: number; cacheWrite: number }
304
- ): void {
305
- target.messages++;
306
- target.cost += cost;
307
- target.tokens.total += tokens.total;
308
- target.tokens.input += tokens.input;
309
- target.tokens.output += tokens.output;
310
- target.tokens.cacheRead += tokens.cacheRead;
311
- target.tokens.cacheWrite += tokens.cacheWrite;
312
- }
313
-
314
- function emptyTokens(): TokenStats {
315
- return { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
316
- }
317
-
318
- function emptyModelStats(): ModelStats {
319
- return { sessions: new Set(), messages: 0, cost: 0, tokens: emptyTokens() };
320
- }
321
-
322
- function emptyProviderStats(): ProviderStats {
323
- return { sessions: new Set(), messages: 0, cost: 0, tokens: emptyTokens(), models: new Map() };
324
- }
325
-
326
- function emptyTimeFilteredStats(): TimeFilteredStats {
327
- return {
328
- providers: new Map(),
329
- totals: { sessions: 0, messages: 0, cost: 0, tokens: emptyTokens() },
330
- insights: { insights: [] },
331
- };
332
- }
333
-
334
- function emptyPeriodRawData(): PeriodRawData {
335
- return { messages: [], sessionCosts: new Map() };
336
- }
337
-
338
- function emptyUsageData(): UsageData {
339
- return {
340
- today: emptyTimeFilteredStats(),
341
- thisWeek: emptyTimeFilteredStats(),
342
- lastWeek: emptyTimeFilteredStats(),
343
- allTime: emptyTimeFilteredStats(),
344
- };
345
- }
346
-
347
- function getPeriodsForTimestamp(timestamp: number, todayMs: number, weekStartMs: number, lastWeekStartMs: number): TabName[] {
348
- const periods: TabName[] = ["allTime"];
349
- if (timestamp >= todayMs) periods.push("today");
350
- if (timestamp >= weekStartMs) {
351
- periods.push("thisWeek");
352
- } else if (timestamp >= lastWeekStartMs) {
353
- periods.push("lastWeek");
354
- }
355
- return periods;
356
- }
357
-
358
- function addMessagesToUsageData(
359
- data: UsageData,
360
- sessionId: string,
361
- messages: SessionMessage[],
362
- todayMs: number,
363
- weekStartMs: number,
364
- lastWeekStartMs: number,
365
- rawByPeriod: Record<TabName, PeriodRawData>,
366
- globalSessionSpans: Map<string, GlobalSessionSpan>
367
- ): void {
368
- const sessionContributed = { today: false, thisWeek: false, lastWeek: false, allTime: false };
369
-
370
- for (const msg of messages) {
371
- // Track real per-session lifetime across every message we see, regardless of
372
- // which period the message falls into. Used later for the "8h+ session" insight.
373
- if (msg.timestamp > 0) {
374
- const span = globalSessionSpans.get(sessionId);
375
- if (!span) {
376
- globalSessionSpans.set(sessionId, { startMs: msg.timestamp, endMs: msg.timestamp });
377
- } else {
378
- if (msg.timestamp < span.startMs) span.startMs = msg.timestamp;
379
- if (msg.timestamp > span.endMs) span.endMs = msg.timestamp;
380
- }
381
- }
382
-
383
- const periods = getPeriodsForTimestamp(msg.timestamp, todayMs, weekStartMs, lastWeekStartMs);
384
- const tokens = {
385
- // Count fresh tokens processed this turn.
386
- // Include cacheWrite because those prompt tokens were newly written and billed.
387
- // Exclude cacheRead because repeated cache hits would otherwise dominate totals.
388
- total: msg.input + msg.output + msg.cacheWrite,
389
- input: msg.input,
390
- output: msg.output,
391
- cacheRead: msg.cacheRead,
392
- cacheWrite: msg.cacheWrite,
393
- };
394
-
395
- for (const period of periods) {
396
- const stats = data[period];
397
-
398
- let providerStats = stats.providers.get(msg.provider);
399
- if (!providerStats) {
400
- providerStats = emptyProviderStats();
401
- stats.providers.set(msg.provider, providerStats);
402
- }
403
-
404
- let modelStats = providerStats.models.get(msg.model);
405
- if (!modelStats) {
406
- modelStats = emptyModelStats();
407
- providerStats.models.set(msg.model, modelStats);
408
- }
409
-
410
- modelStats.sessions.add(sessionId);
411
- accumulateStats(modelStats, msg.cost, tokens);
412
-
413
- providerStats.sessions.add(sessionId);
414
- accumulateStats(providerStats, msg.cost, tokens);
415
-
416
- accumulateStats(stats.totals, msg.cost, tokens);
417
- sessionContributed[period] = true;
418
-
419
- const raw = rawByPeriod[period];
420
- raw.messages.push({
421
- sessionId,
422
- timestamp: msg.timestamp,
423
- cost: msg.cost,
424
- input: msg.input,
425
- cacheRead: msg.cacheRead,
426
- cacheWrite: msg.cacheWrite,
427
- });
428
- raw.sessionCosts.set(sessionId, (raw.sessionCosts.get(sessionId) ?? 0) + msg.cost);
429
- }
430
- }
431
-
432
- if (sessionContributed.today) data.today.totals.sessions++;
433
- if (sessionContributed.thisWeek) data.thisWeek.totals.sessions++;
434
- if (sessionContributed.lastWeek) data.lastWeek.totals.sessions++;
435
- if (sessionContributed.allTime) data.allTime.totals.sessions++;
436
- }
437
-
438
- async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null> {
439
- const startOfToday = new Date();
440
- startOfToday.setHours(0, 0, 0, 0);
441
- const todayMs = startOfToday.getTime();
442
-
443
- // Start of current week (Monday 00:00)
444
- const startOfWeek = new Date();
445
- const dayOfWeek = startOfWeek.getDay(); // 0 = Sunday, 1 = Monday, ...
446
- const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
447
- startOfWeek.setDate(startOfWeek.getDate() - daysSinceMonday);
448
- startOfWeek.setHours(0, 0, 0, 0);
449
- const weekStartMs = startOfWeek.getTime();
450
-
451
- // Start of last week (previous Monday 00:00)
452
- const startOfLastWeek = new Date(startOfWeek);
453
- startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
454
- const lastWeekStartMs = startOfLastWeek.getTime();
455
-
456
- const data = emptyUsageData();
457
- const rawByPeriod: Record<TabName, PeriodRawData> = {
458
- today: emptyPeriodRawData(),
459
- thisWeek: emptyPeriodRawData(),
460
- lastWeek: emptyPeriodRawData(),
461
- allTime: emptyPeriodRawData(),
462
- };
463
- const globalSessionSpans = new Map<string, GlobalSessionSpan>();
464
-
465
- const sessionFiles = await getAllSessionFiles(signal);
466
- if (signal?.aborted) return null;
467
- const seenHashes = new Set<string>();
468
-
469
- for (const filePath of sessionFiles) {
470
- if (signal?.aborted) return null;
471
- const parsed = await parseSessionFile(filePath, seenHashes, signal);
472
- if (signal?.aborted) return null;
473
- if (!parsed) continue;
474
-
475
- addMessagesToUsageData(
476
- data,
477
- parsed.sessionId,
478
- parsed.messages,
479
- todayMs,
480
- weekStartMs,
481
- lastWeekStartMs,
482
- rawByPeriod,
483
- globalSessionSpans
484
- );
485
-
486
- await new Promise<void>((resolve) => setImmediate(resolve));
487
- }
488
-
489
- // Classify sessions that are globally long-running once, then reuse across periods.
490
- const longSessionIds = new Set<string>();
491
- for (const [id, span] of globalSessionSpans) {
492
- if (span.endMs - span.startMs >= LONG_SESSION_MS) longSessionIds.add(id);
493
- }
494
-
495
- for (const period of TAB_ORDER) {
496
- data[period].insights = computeInsights(rawByPeriod[period], longSessionIds);
497
- }
498
-
499
- return data;
500
- }
501
-
502
- // =============================================================================
503
- // Insights
504
- // =============================================================================
505
-
506
- const PARALLEL_WINDOW_MS = 2 * 60_000; // exact ±N milliseconds around each message
507
- const PARALLEL_SESSION_THRESHOLD = 4;
508
- const LARGE_CONTEXT_THRESHOLD = 150_000;
509
- const LARGE_CACHE_MISS_THRESHOLD = 100_000;
510
- const LONG_SESSION_MS = 8 * 60 * 60 * 1000;
511
- const TOP_SESSION_COUNT = 5;
512
- const MIN_MESSAGES_FOR_PARALLEL_INSIGHT = 10;
513
- const MIN_PERCENT_TO_SHOW = 1;
514
-
515
- /**
516
- * Insights are weighted by recorded API cost. Periods with zero total cost produce
517
- * an empty `insights` list — the UI renders a distinct empty-state for that case.
518
- * Long-running-session classification is passed in from a global pass so that a
519
- * session's real lifetime is used rather than the slice visible inside this period.
520
- */
521
- function computeInsights(raw: PeriodRawData, longSessionIds: Set<string>): PeriodInsights {
522
- if (raw.messages.length === 0) {
523
- return { insights: [] };
524
- }
525
-
526
- const total = raw.messages.reduce((sum, m) => sum + m.cost, 0);
527
- if (total <= 0) {
528
- return { insights: [] };
529
- }
530
-
531
- const candidates: Insight[] = [];
532
-
533
- // 1. Parallel sessions — ≥ N unique sessions active within an exact ±W ms window.
534
- const parallelWeight = computeParallelCostWeight(raw.messages);
535
- if (parallelWeight !== null) {
536
- candidates.push({
537
- percent: (parallelWeight / total) * 100,
538
- headline: `of your cost was while ${PARALLEL_SESSION_THRESHOLD}+ sessions ran in parallel`,
539
- advice:
540
- "All sessions share one rate limit. If you don't need them all at once, queueing uses capacity more evenly.",
541
- });
542
- }
543
-
544
- // 2. Large context — input + cacheRead + cacheWrite > threshold.
545
- const largeContextWeight = raw.messages
546
- .filter((m) => m.input + m.cacheRead + m.cacheWrite > LARGE_CONTEXT_THRESHOLD)
547
- .reduce((sum, m) => sum + m.cost, 0);
548
- candidates.push({
549
- percent: (largeContextWeight / total) * 100,
550
- headline: `of your cost was at >${formatThresholdTokens(LARGE_CONTEXT_THRESHOLD)} context`,
551
- advice:
552
- "Longer sessions are more expensive even when cached. /compact mid-task, /clear when switching to new tasks.",
553
- });
554
-
555
- // 3. Large uncached prompt — fresh (non-cached) input > threshold, per the v0.2.0 formula.
556
- const uncachedWeight = raw.messages
557
- .filter((m) => m.input + m.cacheWrite > LARGE_CACHE_MISS_THRESHOLD)
558
- .reduce((sum, m) => sum + m.cost, 0);
559
- candidates.push({
560
- percent: (uncachedWeight / total) * 100,
561
- headline: `of your cost came from >${formatThresholdTokens(LARGE_CACHE_MISS_THRESHOLD)}-token uncached prompts`,
562
- advice:
563
- "Uncached input is expensive, and often happens when sending a message to a session that has gone idle. /compact before stepping away keeps the cold-start small.",
564
- });
565
-
566
- // 4. Long-running sessions — classification comes from the global pass so we use
567
- // true session lifetime, not just the span visible inside this period slice.
568
- const longWeight = raw.messages
569
- .filter((m) => longSessionIds.has(m.sessionId))
570
- .reduce((sum, m) => sum + m.cost, 0);
571
- if (longWeight > 0) {
572
- candidates.push({
573
- percent: (longWeight / total) * 100,
574
- headline: `of your cost came from sessions active for ${LONG_SESSION_MS / 3_600_000}+ hours`,
575
- advice:
576
- "These are often background/loop sessions. Continuous usage can add up quickly so make sure it is intentional.",
577
- });
578
- }
579
-
580
- // 5. Top-N session concentration.
581
- if (raw.sessionCosts.size > TOP_SESSION_COUNT) {
582
- const sortedSessions = Array.from(raw.sessionCosts.values()).sort((a, b) => b - a);
583
- const topN = Math.min(TOP_SESSION_COUNT, sortedSessions.length);
584
- const topWeight = sortedSessions.slice(0, topN).reduce((sum, c) => sum + c, 0);
585
- candidates.push({
586
- percent: (topWeight / total) * 100,
587
- headline: `of your cost came from your top ${topN} session${topN === 1 ? "" : "s"}`,
588
- advice:
589
- "A small number of sessions drives most of your spend. The table view can help pinpoint which ones.",
590
- });
591
- }
592
-
593
- const insights = candidates.filter((i) => i.percent >= MIN_PERCENT_TO_SHOW).sort((a, b) => b.percent - a.percent);
594
- return { insights };
595
- }
596
-
597
- /**
598
- * Two-pointer sweep of messages sorted by timestamp. For each message, count the
599
- * number of distinct session IDs whose messages fall within an exact ± window.
600
- * Returns the total cost attributable to moments when ≥ threshold sessions were
601
- * active, or null if the period has too few sessions/messages to call it.
602
- *
603
- * Messages with missing/invalid timestamps (parsed as 0) are filtered out first —
604
- * otherwise they would collapse into a single synthetic instant and inflate the
605
- * parallel count on older or incomplete logs.
606
- */
607
- function computeParallelCostWeight(messages: RawMessage[]): number | null {
608
- const timed = messages.filter((m) => m.timestamp > 0);
609
- if (timed.length < MIN_MESSAGES_FOR_PARALLEL_INSIGHT) return null;
610
- const distinctSessions = new Set(timed.map((m) => m.sessionId));
611
- if (distinctSessions.size < PARALLEL_SESSION_THRESHOLD) return null;
612
-
613
- const sorted = timed.slice().sort((a, b) => a.timestamp - b.timestamp);
614
- const sidCount = new Map<string, number>();
615
- let uniqueCount = 0;
616
- let left = 0;
617
- let right = 0;
618
- let parallelCost = 0;
619
-
620
- for (let i = 0; i < sorted.length; i++) {
621
- const current = sorted[i]!;
622
- const high = current.timestamp + PARALLEL_WINDOW_MS;
623
- const low = current.timestamp - PARALLEL_WINDOW_MS;
624
-
625
- while (right < sorted.length && sorted[right]!.timestamp <= high) {
626
- const sid = sorted[right]!.sessionId;
627
- const next = (sidCount.get(sid) ?? 0) + 1;
628
- sidCount.set(sid, next);
629
- if (next === 1) uniqueCount++;
630
- right++;
631
- }
632
- while (left < right && sorted[left]!.timestamp < low) {
633
- const sid = sorted[left]!.sessionId;
634
- const next = (sidCount.get(sid) ?? 0) - 1;
635
- if (next === 0) {
636
- sidCount.delete(sid);
637
- uniqueCount--;
638
- } else {
639
- sidCount.set(sid, next);
640
- }
641
- left++;
642
- }
643
-
644
- if (uniqueCount >= PARALLEL_SESSION_THRESHOLD) parallelCost += current.cost;
645
- }
646
-
647
- return parallelCost;
648
- }
649
-
650
- function formatThresholdTokens(n: number): string {
651
- if (n >= 1_000_000) return `${n / 1_000_000}M`;
652
- if (n >= 1_000) return `${n / 1_000}k`;
653
- return String(n);
654
- }
655
-
656
- function formatInsightPercent(p: number): string {
657
- if (p >= 10) return `${Math.round(p)}%`;
658
- return `${Math.round(p * 10) / 10}%`;
659
- }
660
-
661
113
  // =============================================================================
662
114
  // Formatting Helpers
663
115
  // =============================================================================
@@ -685,6 +137,11 @@ function formatNumber(n: number): string {
685
137
  return n.toLocaleString();
686
138
  }
687
139
 
140
+ function formatInsightPercent(p: number): string {
141
+ if (p >= 10) return `${Math.round(p)}%`;
142
+ return `${Math.round(p * 10) / 10}%`;
143
+ }
144
+
688
145
  function padLeft(s: string, len: number): string {
689
146
  const vis = visibleWidth(s);
690
147
  if (vis >= len) return s;
@@ -753,11 +210,10 @@ const TAB_LABELS: Record<TabName, string> = {
753
210
  today: "Today",
754
211
  thisWeek: "This Week",
755
212
  lastWeek: "Last Week",
213
+ last30Days: "Last 30 Days",
756
214
  allTime: "All Time",
757
215
  };
758
216
 
759
- const TAB_ORDER: TabName[] = ["today", "thisWeek", "lastWeek", "allTime"];
760
-
761
217
  class UsageComponent {
762
218
  private activeTab: TabName = "allTime";
763
219
  private viewMode: ViewMode = "table";
@@ -1090,7 +546,7 @@ export default function (pi: ExtensionAPI) {
1090
546
 
1091
547
  loader.onAbort = () => finish(null);
1092
548
 
1093
- collectUsageData(loader.signal)
549
+ collectUsageData({ signal: loader.signal })
1094
550
  .then(finish)
1095
551
  .catch(() => finish(null));
1096
552
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-usage-extension",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Usage statistics dashboard for Pi sessions.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",
@@ -25,7 +25,7 @@
25
25
  "@earendil-works/pi-tui": "*"
26
26
  },
27
27
  "devDependencies": {
28
- "@earendil-works/pi-coding-agent": "^0.74.0",
29
- "@earendil-works/pi-tui": "^0.74.0"
28
+ "@earendil-works/pi-coding-agent": "^0.80.3",
29
+ "@earendil-works/pi-tui": "^0.80.3"
30
30
  }
31
31
  }