pi-extensions 0.1.22 → 0.1.24

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,19 @@
1
+ name: Apply Skill Optimization
2
+ on:
3
+ issue_comment:
4
+ types: [created]
5
+
6
+ jobs:
7
+ apply:
8
+ if: >-
9
+ github.event.issue.pull_request &&
10
+ contains(github.event.comment.body, '/apply-optimize')
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ pull-requests: write
14
+ contents: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: tesslio/skill-review-and-optimize@d81583861aaf29d1da7f10e6539efef4e27b0dd5
18
+ with:
19
+ mode: 'apply'
@@ -0,0 +1,17 @@
1
+ name: Skill Review & Optimize
2
+ on:
3
+ pull_request:
4
+ paths: ['**/SKILL.md']
5
+
6
+ jobs:
7
+ review:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ pull-requests: write
11
+ contents: read
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: tesslio/skill-review-and-optimize@d81583861aaf29d1da7f10e6539efef4e27b0dd5
15
+ with:
16
+ optimize: 'true'
17
+ tessl-token: ${{ secrets.TESSL_API_TOKEN }}
@@ -1,30 +1,61 @@
1
1
  ---
2
2
  name: extending-pi
3
- description: Guide for extending Pi — decide between skills, extensions, prompt templates, themes, context files, or custom models, then create and package them. Use when someone wants to extend Pi, add capabilities, create a skill, build an extension, or make a Pi package.
3
+ description: Guide for extending Pi — decide between skills, extensions, prompt templates, themes, context files, or custom models, then scaffold files, configure manifests, and package them. Use when someone wants to extend Pi, add capabilities, create a skill, build an extension, make a Pi package, scaffold extension files, or configure a manifest.json.
4
4
  ---
5
5
 
6
6
  # Extending Pi
7
7
 
8
- Help the user decide what to build and where to find guidance.
8
+ Help the user decide what to build, scaffold the right files, and point to detailed guidance.
9
9
 
10
10
  ## What to build
11
11
 
12
- | Goal | Build a… | Where |
13
- |------|----------|-------|
14
- | Teach Pi a workflow or how to use a tool/API/CLI | **Skill** | Read `skill-creator/SKILL.md` for detailed guidance |
15
- | Give Pi a new tool, command, or runtime behavior | **Extension** | Read Pi docs: `docs/extensions.md` |
16
- | Reuse a prompt pattern with variables | **Prompt template** | Read Pi docs: `docs/prompt-templates.md` |
17
- | Set project-wide coding guidelines | **Context file** | `AGENTS.md` in project root or `.pi/agent/` — just markdown |
18
- | Change Pi's appearance | **Theme** | Read Pi docs: `docs/themes.md` |
19
- | Add a model or provider | **Custom model** | Read Pi docs: `docs/models.md` (JSON) or `docs/custom-provider.md` (extension) |
20
- | Share any of the above | **Package** | Read Pi docs: `docs/packages.md` |
12
+ | Goal | Build a… | Key files to create | Where |
13
+ |------|----------|-------------------|-------|
14
+ | Teach Pi a workflow or how to use a tool/API/CLI | **Skill** | `SKILL.md` with YAML frontmatter + markdown body | Read `skill-creator/SKILL.md` for detailed guidance |
15
+ | Give Pi a new tool, command, or runtime behavior | **Extension** | `manifest.json` + `src/index.ts` entry point | Read Pi docs: `docs/extensions.md` |
16
+ | Reuse a prompt pattern with variables | **Prompt template** | `.md` file with `{{variable}}` placeholders | Read Pi docs: `docs/prompt-templates.md` |
17
+ | Set project-wide coding guidelines | **Context file** | `AGENTS.md` in project root or `.pi/agent/` — just markdown | No extra docs needed |
18
+ | Change Pi's appearance | **Theme** | `theme.json` with color and font definitions | Read Pi docs: `docs/themes.md` |
19
+ | Add a model or provider | **Custom model** | `models.json` or extension with provider registration | Read Pi docs: `docs/models.md` (JSON) or `docs/custom-provider.md` (extension) |
20
+ | Share any of the above | **Package** | `manifest.json` with dependencies and entry points | Read Pi docs: `docs/packages.md` |
21
21
 
22
22
  ## Skill vs Extension — the fuzzy boundary
23
23
 
24
- If `bash` + instructions can do it, prefer a **skill** (simpler, no code to maintain). If you need event hooks, typed tools, UI components, or policy enforcement, use an **extension**.
24
+ If `bash` + instructions can do it, prefer a **Skill** (simpler, no code to maintain). If you need event hooks, typed tools, UI components, or policy enforcement, use an **Extension**.
25
25
 
26
26
  Examples:
27
27
  - "Pi should know our deploy process" → **Skill** (workflow instructions)
28
28
  - "Pi should confirm before `rm -rf`" → **Extension** (event interception)
29
29
  - "Pi should use Brave Search" → **Skill** (instructions + CLI scripts)
30
30
  - "Pi should have a structured `db_query` tool" → **Extension** (registerTool)
31
+
32
+ ## Minimal working examples
33
+
34
+ **Skill** — place in `.pi/skills/my-skill/SKILL.md`:
35
+ ```markdown
36
+ ---
37
+ name: my-skill
38
+ description: Does X when the user asks to Y.
39
+ ---
40
+ # My Skill
41
+ Step 1: ...
42
+ Step 2: ...
43
+ ```
44
+
45
+ **Extension** — create `manifest.json` + `src/index.ts`:
46
+ ```json
47
+ { "name": "my-extension", "version": "0.1.0", "entry": "src/index.ts" }
48
+ ```
49
+ ```typescript
50
+ import { registerTool } from "@anthropic/pi-sdk";
51
+ registerTool("my_tool", { description: "..." }, async (input) => { /* ... */ });
52
+ ```
53
+
54
+ ## Quick-start steps
55
+
56
+ 1. **Pick the artifact type** from the table above.
57
+ 2. **Scaffold the files** — create the key files using the minimal examples above.
58
+ 3. **Validate locally**:
59
+ - Skills: place `SKILL.md` in `.pi/skills/<name>/` and invoke the skill — if it doesn't trigger, check that `name` and `description` in frontmatter are set correctly.
60
+ - Extensions: run `pi ext install .` — if it fails with "missing entry", verify the `entry` path in `manifest.json` points to an existing file.
61
+ 4. **Package and share** — follow `docs/packages.md` to bundle and publish.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensions",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "keywords": [
@@ -15,15 +15,11 @@
15
15
  "./arcade/tetris.ts",
16
16
  "./arcade/mario-not/mario-not.ts",
17
17
  "./code-actions/index.ts",
18
- "./control/control.ts",
19
18
  "./files-widget/index.ts",
20
19
  "./raw-paste/index.ts",
21
20
  "./ralph-wiggum/index.ts",
22
- "./review/review.ts",
23
- "./relaunch/index.ts",
24
21
  "./tab-status/tab-status.ts",
25
- "./usage-extension/index.ts",
26
- "./ready-status/ready-status.ts"
22
+ "./usage-extension/index.ts"
27
23
  ],
28
24
  "skills": [
29
25
  "./extending-pi/SKILL.md",
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 - 2026-04-09
4
+ - Keep recursive subagent session scanning in `/usage`
5
+ - Remove the deduped/raw mode toggle and keep the deduped view as the default behavior
6
+
7
+ ## 0.1.4 - 2026-04-09
8
+ - Scan session files recursively so nested subagent runs are included in `/usage`
9
+ - Add deduped vs raw counting modes to compare copied branch history against raw file totals
10
+
3
11
  ## 0.1.3 - 2026-02-03
4
12
  - Add preview image metadata for the extension listing.
5
13
 
@@ -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-01-13
10
+ - **Last updated:** 2026-04-09
11
11
 
12
12
  ## Installation
13
13
 
@@ -111,7 +111,9 @@ The "Cache" column combines both read and write tokens.
111
111
 
112
112
  ## Data Source
113
113
 
114
- Statistics are parsed from session files in `~/.pi/agent/sessions/`. Each session is a JSONL file containing message entries with usage data. Assistant messages duplicated across branched session files are deduplicated by timestamp + total tokens (matching ccusage).
114
+ Statistics are parsed recursively from session files in `~/.pi/agent/sessions/`, including nested subagent runs such as `run-0/` directories. Each session is a JSONL file containing message entries with usage data.
115
+
116
+ Assistant messages duplicated across branched session files are deduplicated by timestamp + total tokens, matching the extension's previous behavior while still including recursive subagent sessions.
115
117
 
116
118
  Respects the `PI_CODING_AGENT_DIR` environment variable if set.
117
119
 
@@ -96,31 +96,27 @@ function getSessionsDir(): string {
96
96
  return join(agentDir, "sessions");
97
97
  }
98
98
 
99
- async function getAllSessionFiles(signal?: AbortSignal): Promise<string[]> {
100
- const sessionsDir = getSessionsDir();
101
- const files: string[] = [];
102
-
99
+ async function collectSessionFilesRecursively(dir: string, files: string[], signal?: AbortSignal): Promise<void> {
103
100
  try {
104
- const cwdDirs = await readdir(sessionsDir, { withFileTypes: true });
105
- for (const dir of cwdDirs) {
106
- if (signal?.aborted) return files;
107
- if (!dir.isDirectory()) continue;
108
- const cwdPath = join(sessionsDir, dir.name);
109
- try {
110
- const sessionFiles = await readdir(cwdPath);
111
- for (const file of sessionFiles) {
112
- if (file.endsWith(".jsonl")) {
113
- files.push(join(cwdPath, file));
114
- }
115
- }
116
- } catch {
117
- // Skip directories we can't read
101
+ const entries = await readdir(dir, { withFileTypes: true });
102
+ for (const entry of entries) {
103
+ if (signal?.aborted) return;
104
+ const entryPath = join(dir, entry.name);
105
+ if (entry.isDirectory()) {
106
+ await collectSessionFilesRecursively(entryPath, files, signal);
107
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
108
+ files.push(entryPath);
118
109
  }
119
110
  }
120
111
  } catch {
121
- // Return empty if we can't read sessions dir
112
+ // Skip directories we can't read
122
113
  }
114
+ }
123
115
 
116
+ async function getAllSessionFiles(signal?: AbortSignal): Promise<string[]> {
117
+ const files: string[] = [];
118
+ await collectSessionFilesRecursively(getSessionsDir(), files, signal);
119
+ files.sort();
124
120
  return files;
125
121
  }
126
122
 
@@ -135,11 +131,16 @@ interface SessionMessage {
135
131
  timestamp: number;
136
132
  }
137
133
 
134
+ interface ParsedSessionFile {
135
+ sessionId: string;
136
+ messages: SessionMessage[];
137
+ }
138
+
138
139
  async function parseSessionFile(
139
140
  filePath: string,
140
141
  seenHashes: Set<string>,
141
142
  signal?: AbortSignal
142
- ): Promise<{ sessionId: string; messages: SessionMessage[] } | null> {
143
+ ): Promise<ParsedSessionFile | null> {
143
144
  try {
144
145
  const content = await readFile(filePath, "utf8");
145
146
  if (signal?.aborted) return null;
@@ -169,8 +170,8 @@ async function parseSessionFile(
169
170
  const fallbackTs = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
170
171
  const timestamp = msg.timestamp || (Number.isNaN(fallbackTs) ? 0 : fallbackTs);
171
172
 
172
- // Deduplicate by timestamp + total tokens (same as ccusage)
173
- // Session files contain many duplicate entries
173
+ // Deduplicate copied history across branched session files.
174
+ // Keep the existing ccusage-style hash so current totals remain comparable.
174
175
  const totalTokens = input + output + cacheRead + cacheWrite;
175
176
  const hash = `${timestamp}:${totalTokens}`;
176
177
  if (seenHashes.has(hash)) continue;
@@ -232,6 +233,73 @@ function emptyTimeFilteredStats(): TimeFilteredStats {
232
233
  };
233
234
  }
234
235
 
236
+ function emptyUsageData(): UsageData {
237
+ return {
238
+ today: emptyTimeFilteredStats(),
239
+ thisWeek: emptyTimeFilteredStats(),
240
+ allTime: emptyTimeFilteredStats(),
241
+ };
242
+ }
243
+
244
+ function getPeriodsForTimestamp(timestamp: number, todayMs: number, weekStartMs: number): TabName[] {
245
+ const periods: TabName[] = ["allTime"];
246
+ if (timestamp >= todayMs) periods.push("today");
247
+ if (timestamp >= weekStartMs) periods.push("thisWeek");
248
+ return periods;
249
+ }
250
+
251
+ function addMessagesToUsageData(
252
+ data: UsageData,
253
+ sessionId: string,
254
+ messages: SessionMessage[],
255
+ todayMs: number,
256
+ weekStartMs: number
257
+ ): void {
258
+ const sessionContributed = { today: false, thisWeek: false, allTime: false };
259
+
260
+ for (const msg of messages) {
261
+ const periods = getPeriodsForTimestamp(msg.timestamp, todayMs, weekStartMs);
262
+ const tokens = {
263
+ // Total = input + output only. cacheRead/cacheWrite are tracked separately.
264
+ // cacheRead tokens were already counted when first sent, so including them
265
+ // would double-count and massively inflate totals (cache hits repeat every message).
266
+ total: msg.input + msg.output,
267
+ input: msg.input,
268
+ output: msg.output,
269
+ cache: msg.cacheRead + msg.cacheWrite,
270
+ };
271
+
272
+ for (const period of periods) {
273
+ const stats = data[period];
274
+
275
+ let providerStats = stats.providers.get(msg.provider);
276
+ if (!providerStats) {
277
+ providerStats = emptyProviderStats();
278
+ stats.providers.set(msg.provider, providerStats);
279
+ }
280
+
281
+ let modelStats = providerStats.models.get(msg.model);
282
+ if (!modelStats) {
283
+ modelStats = emptyModelStats();
284
+ providerStats.models.set(msg.model, modelStats);
285
+ }
286
+
287
+ modelStats.sessions.add(sessionId);
288
+ accumulateStats(modelStats, msg.cost, tokens);
289
+
290
+ providerStats.sessions.add(sessionId);
291
+ accumulateStats(providerStats, msg.cost, tokens);
292
+
293
+ accumulateStats(stats.totals, msg.cost, tokens);
294
+ sessionContributed[period] = true;
295
+ }
296
+ }
297
+
298
+ if (sessionContributed.today) data.today.totals.sessions++;
299
+ if (sessionContributed.thisWeek) data.thisWeek.totals.sessions++;
300
+ if (sessionContributed.allTime) data.allTime.totals.sessions++;
301
+ }
302
+
235
303
  async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null> {
236
304
  const startOfToday = new Date();
237
305
  startOfToday.setHours(0, 0, 0, 0);
@@ -245,15 +313,11 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null>
245
313
  startOfWeek.setHours(0, 0, 0, 0);
246
314
  const weekStartMs = startOfWeek.getTime();
247
315
 
248
- const data: UsageData = {
249
- today: emptyTimeFilteredStats(),
250
- thisWeek: emptyTimeFilteredStats(),
251
- allTime: emptyTimeFilteredStats(),
252
- };
316
+ const data = emptyUsageData();
253
317
 
254
318
  const sessionFiles = await getAllSessionFiles(signal);
255
319
  if (signal?.aborted) return null;
256
- const seenHashes = new Set<string>(); // Deduplicate across all files
320
+ const seenHashes = new Set<string>();
257
321
 
258
322
  for (const filePath of sessionFiles) {
259
323
  if (signal?.aborted) return null;
@@ -261,59 +325,7 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null>
261
325
  if (signal?.aborted) return null;
262
326
  if (!parsed) continue;
263
327
 
264
- const { sessionId, messages } = parsed;
265
- const sessionContributed = { today: false, thisWeek: false, allTime: false };
266
-
267
- for (const msg of messages) {
268
- if (signal?.aborted) return null;
269
- const periods: TabName[] = ["allTime"];
270
- if (msg.timestamp >= todayMs) periods.push("today");
271
- if (msg.timestamp >= weekStartMs) periods.push("thisWeek");
272
-
273
- const tokens = {
274
- // Total = input + output only. cacheRead/cacheWrite are tracked separately.
275
- // cacheRead tokens were already counted when first sent, so including them
276
- // would double-count and massively inflate totals (cache hits repeat every message).
277
- total: msg.input + msg.output,
278
- input: msg.input,
279
- output: msg.output,
280
- cache: msg.cacheRead + msg.cacheWrite,
281
- };
282
-
283
- for (const period of periods) {
284
- const stats = data[period];
285
-
286
- // Get or create provider stats
287
- let providerStats = stats.providers.get(msg.provider);
288
- if (!providerStats) {
289
- providerStats = emptyProviderStats();
290
- stats.providers.set(msg.provider, providerStats);
291
- }
292
-
293
- // Get or create model stats
294
- let modelStats = providerStats.models.get(msg.model);
295
- if (!modelStats) {
296
- modelStats = emptyModelStats();
297
- providerStats.models.set(msg.model, modelStats);
298
- }
299
-
300
- // Accumulate stats at all levels
301
- modelStats.sessions.add(sessionId);
302
- accumulateStats(modelStats, msg.cost, tokens);
303
-
304
- providerStats.sessions.add(sessionId);
305
- accumulateStats(providerStats, msg.cost, tokens);
306
-
307
- accumulateStats(stats.totals, msg.cost, tokens);
308
-
309
- sessionContributed[period] = true;
310
- }
311
- }
312
-
313
- // Count unique sessions per period
314
- if (sessionContributed.today) data.today.totals.sessions++;
315
- if (sessionContributed.thisWeek) data.thisWeek.totals.sessions++;
316
- if (sessionContributed.allTime) data.allTime.totals.sessions++;
328
+ addMessagesToUsageData(data, parsed.sessionId, parsed.messages, todayMs, weekStartMs);
317
329
 
318
330
  await new Promise<void>((resolve) => setImmediate(resolve));
319
331
  }
@@ -463,7 +475,7 @@ class UsageComponent {
463
475
  const label = TAB_LABELS[tab];
464
476
  return tab === this.activeTab ? th.fg("accent", `[${label}]`) : th.fg("dim", ` ${label} `);
465
477
  }).join(" ");
466
- return [tabs, ""];
478
+ return [tabs, th.fg("dim", "Dedupes copied branched-history messages. Recursive subagent sessions included."), ""];
467
479
  }
468
480
 
469
481
  private renderHeader(): string[] {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-usage-extension",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Usage statistics dashboard for Pi sessions.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",