@tmustier/pi-usage-extension 0.9.0 → 0.9.1
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 +6 -0
- package/README.md +10 -2
- package/data.ts +1 -1
- package/export.ts +34 -0
- package/index.ts +23 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.1] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Exports now save to `/tmp` (or the OS temp dir where `/tmp` doesn't exist) instead of the pi session's cwd, so they never litter a repo or home directory. The `✓ Saved` note shows the full path.
|
|
7
|
+
- The export directory is configurable: `{ "usage-extension": { "exportDir": "~/Downloads" } }` in `~/.pi/agent/settings.json` (`~` expands; the directory is created if missing).
|
|
8
|
+
|
|
3
9
|
## [0.9.0] - 2026-07-17
|
|
4
10
|
|
|
5
11
|
### Added
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A Pi extension that displays aggregated usage statistics across all sessions.
|
|
|
7
7
|
## Compatibility
|
|
8
8
|
|
|
9
9
|
- **Pi version:** 0.42.4+
|
|
10
|
-
- **Last updated:** 2026-07-17 (0.9.
|
|
10
|
+
- **Last updated:** 2026-07-17 (0.9.1)
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
@@ -107,7 +107,7 @@ A note on reading them: cache-miss and context numbers are **observed cost, not
|
|
|
107
107
|
|
|
108
108
|
### Export
|
|
109
109
|
|
|
110
|
-
Press `e` in any view to write the **current slice** to the
|
|
110
|
+
Press `e` in any view to write the **current slice** to `/tmp` (or the OS temp dir where `/tmp` doesn't exist) — exports never litter your repo or home directory, and the `✓ Saved` note shows the full path so you can grab the file if you want to keep it:
|
|
111
111
|
|
|
112
112
|
| View | File | Contents |
|
|
113
113
|
|---|---|---|
|
|
@@ -117,6 +117,14 @@ Press `e` in any view to write the **current slice** to the current working dire
|
|
|
117
117
|
|
|
118
118
|
A `✓ Saved …` note confirms the write (or reports the error) above the help line.
|
|
119
119
|
|
|
120
|
+
To export somewhere permanent instead, set an export directory in `~/.pi/agent/settings.json` (created if missing, `~` expands):
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"usage-extension": { "exportDir": "~/Downloads" }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
120
128
|
### Graph explorer
|
|
121
129
|
|
|
122
130
|
The **Graphs** view plots usage over time for the active period as a braille line chart, with a legend showing per-series totals and shares.
|
package/data.ts
CHANGED
|
@@ -198,7 +198,7 @@ export interface ParsedSessionFile {
|
|
|
198
198
|
// Paths
|
|
199
199
|
// =============================================================================
|
|
200
200
|
|
|
201
|
-
function getAgentDir(): string {
|
|
201
|
+
export function getAgentDir(): string {
|
|
202
202
|
// Replicate Pi's logic: respect PI_CODING_AGENT_DIR env var
|
|
203
203
|
return process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
204
204
|
}
|
package/export.ts
CHANGED
|
@@ -7,9 +7,43 @@
|
|
|
7
7
|
* component owns file naming and disk writes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { join } from "node:path";
|
|
10
11
|
import type { Insight, ProviderStats, TotalStats } from "./data.ts";
|
|
11
12
|
import type { GraphModel } from "./graph.ts";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Read the configured export directory from settings.json content, if any.
|
|
16
|
+
* Config shape: `{ "usage-extension": { "exportDir": "~/Downloads" } }`.
|
|
17
|
+
*/
|
|
18
|
+
export function parseExportDirSetting(settingsJson: string): string | null {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(settingsJson) as { "usage-extension"?: { exportDir?: unknown } };
|
|
21
|
+
const dir = parsed["usage-extension"]?.exportDir;
|
|
22
|
+
return typeof dir === "string" && dir.trim() !== "" ? dir.trim() : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pick the export directory. A configured dir wins (with `~` expanded);
|
|
30
|
+
* otherwise exports go to /tmp so they never litter a repo or home
|
|
31
|
+
* directory, falling back to the OS temp dir where /tmp doesn't exist.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveExportDir(
|
|
34
|
+
configured: string | null,
|
|
35
|
+
home: string,
|
|
36
|
+
slashTmpExists: boolean,
|
|
37
|
+
fallbackTmp: string,
|
|
38
|
+
): string {
|
|
39
|
+
if (configured !== null) {
|
|
40
|
+
if (configured === "~") return home;
|
|
41
|
+
if (configured.startsWith("~/")) return join(home, configured.slice(2));
|
|
42
|
+
return configured;
|
|
43
|
+
}
|
|
44
|
+
return slashTmpExists ? "/tmp" : fallbackTmp;
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
/** Quote a CSV field only when it needs it (comma, quote, or newline). */
|
|
14
48
|
function csvField(value: string | number): string {
|
|
15
49
|
const s = String(value);
|
package/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-wor
|
|
|
13
13
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
15
15
|
|
|
16
|
-
import { collectUsageData, TAB_ORDER } from "./data";
|
|
16
|
+
import { collectUsageData, getAgentDir, TAB_ORDER } from "./data";
|
|
17
17
|
import type { CollectProgress } from "./data";
|
|
18
18
|
import type { BaseStats, ProviderStats, TabName, TotalStats, UsageData } from "./data";
|
|
19
19
|
import {
|
|
@@ -26,8 +26,16 @@ import {
|
|
|
26
26
|
TOTAL_SERIES_KEY,
|
|
27
27
|
} from "./graph";
|
|
28
28
|
import type { GraphGroupBy, GraphMetric, GraphModel } from "./graph";
|
|
29
|
-
import {
|
|
30
|
-
|
|
29
|
+
import {
|
|
30
|
+
buildGraphCsv,
|
|
31
|
+
buildInsightsJson,
|
|
32
|
+
buildTableCsv,
|
|
33
|
+
exportFileName,
|
|
34
|
+
parseExportDirSetting,
|
|
35
|
+
resolveExportDir,
|
|
36
|
+
} from "./export";
|
|
37
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { homedir, tmpdir } from "node:os";
|
|
31
39
|
import { join } from "node:path";
|
|
32
40
|
|
|
33
41
|
type ViewMode = "table" | "insights" | "graph";
|
|
@@ -524,9 +532,19 @@ class UsageComponent {
|
|
|
524
532
|
content = buildTableCsv(visible.providers, visible.totals);
|
|
525
533
|
}
|
|
526
534
|
try {
|
|
527
|
-
|
|
535
|
+
let configured: string | null = null;
|
|
536
|
+
try {
|
|
537
|
+
configured = parseExportDirSetting(readFileSync(join(getAgentDir(), "settings.json"), "utf8"));
|
|
538
|
+
} catch {
|
|
539
|
+
// No settings file or unreadable: fall through to the default dir.
|
|
540
|
+
}
|
|
541
|
+
const home = homedir();
|
|
542
|
+
const dir = resolveExportDir(configured, home, existsSync("/tmp"), tmpdir());
|
|
543
|
+
mkdirSync(dir, { recursive: true });
|
|
544
|
+
const path = join(dir, name);
|
|
528
545
|
writeFileSync(path, content);
|
|
529
|
-
|
|
546
|
+
const shown = path.startsWith(home + "/") ? "~" + path.slice(home.length) : path;
|
|
547
|
+
this.exportNote = { text: `Saved ${shown}`, ok: true };
|
|
530
548
|
} catch (err) {
|
|
531
549
|
this.exportNote = { text: `Export failed: ${err instanceof Error ? err.message : String(err)}`, ok: false };
|
|
532
550
|
}
|