@askjo/pi-reflect 1.1.0 → 1.2.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.
@@ -0,0 +1 @@
1
+ github: skyfallsin
@@ -0,0 +1,46 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ concurrency:
9
+ group: npm-publish
10
+ cancel-in-progress: false
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 24
20
+ cache: npm
21
+ - run: npm ci
22
+ - run: npm test
23
+
24
+ publish:
25
+ needs: test
26
+ runs-on: ubuntu-latest
27
+ permissions:
28
+ contents: read
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version: 24
35
+ registry-url: "https://registry.npmjs.org"
36
+ - name: Verify tag matches package.json version
37
+ run: |
38
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
39
+ PKG_VERSION=$(node -p "require('./package.json').version")
40
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
41
+ echo "::error::Tag version ($TAG_VERSION) != package.json version ($PKG_VERSION)"
42
+ exit 1
43
+ fi
44
+ - run: npm ci --ignore-scripts
45
+ - name: Publish with provenance
46
+ run: npm publish --provenance --access public
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
3
+ <a href="https://github.com/jo-inc/pi-reflect/stargazers"><img src="https://img.shields.io/github/stars/jo-inc/pi-reflect" alt="GitHub stars" /></a>
4
+ </p>
1
5
  <p align="center">
2
6
  <picture>
3
7
  <source media="(prefers-color-scheme: dark)" srcset="logo-dark.png" width="300">
@@ -165,3 +169,13 @@ pi -e ./extensions/index.ts # test locally without installing
165
169
  ## License
166
170
 
167
171
  MIT
172
+
173
+ ## Pi Ecosystem
174
+
175
+ | Package | Description |
176
+ |---------|-------------|
177
+ | [pi-mem](https://github.com/jo-inc/pi-mem) | Persistent markdown memory for coding agents |
178
+ | [pi-boss](https://github.com/skyfallsin/pi-boss) | Multi-agent orchestration via tmux |
179
+ | [pi-room](https://github.com/skyfallsin/pi-room) | Multi-agent awareness and coordination |
180
+ | [pi-vertex-anthropic](https://github.com/skyfallsin/pi-vertex-anthropic) | Claude via Google Cloud Vertex AI |
181
+ | [pi-skill-posthog](https://github.com/skyfallsin/pi-skill-posthog) | PostHog analytics skill for pi agents |
@@ -14,6 +14,13 @@
14
14
 
15
15
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
16
  import * as path from "node:path";
17
+ import * as fs from "node:fs";
18
+
19
+ /** Display a target path as "parentDir/filename" for compact but unambiguous listing */
20
+ function targetLabel(filePath: string): string {
21
+ const dir = path.basename(path.dirname(filePath));
22
+ return `${dir}/${path.basename(filePath)}`;
23
+ }
17
24
 
18
25
  import {
19
26
  type ReflectTarget,
@@ -28,6 +35,7 @@ import {
28
35
  runReflection,
29
36
  collectTranscriptsForDate,
30
37
  getAvailableSessionDates,
38
+ computeFileMetrics,
31
39
  CONFIG_FILE,
32
40
  } from "./reflect.js";
33
41
 
@@ -44,14 +52,14 @@ export default function (pi: ExtensionAPI) {
44
52
  modelRegistryRef = ctx.modelRegistry;
45
53
  const targetPath = args?.trim();
46
54
 
47
- let target: ReflectTarget;
55
+ let targets: ReflectTarget[];
48
56
 
49
57
  if (targetPath) {
50
58
  const config = loadConfig();
51
59
  const existing = config.targets.find(
52
60
  (t) => resolvePath(t.path) === resolvePath(targetPath),
53
61
  );
54
- target = existing ?? { ...DEFAULT_TARGET, path: targetPath };
62
+ targets = [existing ?? { ...DEFAULT_TARGET, path: targetPath }];
55
63
  } else {
56
64
  const config = loadConfig();
57
65
  if (config.targets.length === 0) {
@@ -60,14 +68,14 @@ export default function (pi: ExtensionAPI) {
60
68
  "No targets configured. Enter path to a markdown file to reflect on:",
61
69
  );
62
70
  if (!filePath) return;
63
- target = { ...DEFAULT_TARGET, path: filePath };
71
+ targets = [{ ...DEFAULT_TARGET, path: filePath }];
64
72
 
65
73
  const save = await ctx.ui.confirm(
66
74
  "Save target?",
67
75
  `Save ${filePath} as a reflection target for next time?`,
68
76
  );
69
77
  if (save) {
70
- config.targets.push(target);
78
+ config.targets.push(targets[0]);
71
79
  saveConfig(config);
72
80
  ctx.ui.notify("Saved to reflect.json", "info");
73
81
  }
@@ -76,18 +84,23 @@ export default function (pi: ExtensionAPI) {
76
84
  return;
77
85
  }
78
86
  } else if (config.targets.length === 1) {
79
- target = config.targets[0];
87
+ targets = [config.targets[0]];
80
88
  } else if (ctx.hasUI) {
89
+ const allTargetsLabel = "All targets";
81
90
  const choice = await ctx.ui.select(
82
91
  "Which target?",
83
- config.targets.map((t) => path.basename(t.path)),
92
+ [...config.targets.map((t) => targetLabel(t.path)), allTargetsLabel],
84
93
  );
85
94
  if (choice === undefined || choice === null) return;
86
- const chosenTarget = config.targets.find((t) => path.basename(t.path) === choice);
87
- if (!chosenTarget) return;
88
- target = chosenTarget;
95
+ if (choice === allTargetsLabel) {
96
+ targets = config.targets;
97
+ } else {
98
+ const chosenTarget = config.targets.find((t) => targetLabel(t.path) === choice);
99
+ if (!chosenTarget) return;
100
+ targets = [chosenTarget];
101
+ }
89
102
  } else {
90
- target = config.targets[0];
103
+ targets = [config.targets[0]];
91
104
  }
92
105
  }
93
106
 
@@ -95,26 +108,28 @@ export default function (pi: ExtensionAPI) {
95
108
  ? (msg, level) => ctx.ui.notify(msg, level)
96
109
  : (msg, level) => console.log(`[reflect] [${level}] ${msg}`);
97
110
 
98
- // Use the current session model if available
99
- let currentModel: any;
100
- let currentModelApiKey: string | undefined;
101
- if (ctx.model) {
102
- const key = await ctx.modelRegistry.getApiKey(ctx.model);
103
- if (key) {
104
- currentModel = ctx.model;
105
- currentModelApiKey = key;
111
+ // Run configured targets sequentially to avoid provider concurrency spikes,
112
+ // and persist each successful result immediately.
113
+ let completed = 0;
114
+ let failed = 0;
115
+ for (const target of targets) {
116
+ try {
117
+ const run = await runReflection(target, modelRegistryRef, notify, undefined, {});
118
+ if (run) {
119
+ const history = loadHistory();
120
+ history.push(run);
121
+ saveHistory(history);
122
+ completed++;
123
+ } else {
124
+ failed++;
125
+ }
126
+ } catch (error) {
127
+ failed++;
128
+ notify(`${targetLabel(target.path)} failed: ${error instanceof Error ? error.message : String(error)}`, "error");
106
129
  }
107
130
  }
108
-
109
- const run = await runReflection(target, modelRegistryRef, notify, undefined, {
110
- currentModel,
111
- currentModelApiKey,
112
- });
113
-
114
- if (run) {
115
- const history = loadHistory();
116
- history.push(run);
117
- saveHistory(history);
131
+ if (targets.length > 1) {
132
+ notify(`All targets finished: ${completed} completed, ${failed} failed or skipped`, failed ? "warning" : "info");
118
133
  }
119
134
  },
120
135
  });
@@ -135,7 +150,7 @@ export default function (pi: ExtensionAPI) {
135
150
  }
136
151
 
137
152
  const lines = config.targets.map((t, i) => {
138
- return `${i + 1}. **${path.basename(t.path)}** — ${t.schedule}, ${t.model}, ${t.lookbackDays}d lookback\n ${t.path}`;
153
+ return `${i + 1}. **${targetLabel(t.path)}** — ${t.schedule}, ${t.model}, ${t.lookbackDays}d lookback\n ${t.path}`;
139
154
  });
140
155
 
141
156
  ctx.ui.notify(
@@ -160,7 +175,7 @@ export default function (pi: ExtensionAPI) {
160
175
  const recent = history.slice(-10).reverse();
161
176
  const lines = recent.map((r) => {
162
177
  const date = r.timestamp.slice(0, 16).replace("T", " ");
163
- const file = path.basename(r.targetPath);
178
+ const file = targetLabel(r.targetPath);
164
179
  return `- **${date}** ${file}: ${r.editsApplied} edits, ${r.correctionsFound} corrections (${r.sessionsAnalyzed} sessions)\n ${r.summary}`;
165
180
  });
166
181
 
@@ -197,16 +212,68 @@ export default function (pi: ExtensionAPI) {
197
212
  byTarget.set(key, list);
198
213
  }
199
214
 
215
+ // If multiple targets tracked, let user pick one (or show all)
216
+ if (byTarget.size > 1 && ctx.hasUI) {
217
+ const options = ["All targets", ...Array.from(byTarget.keys()).map((p) => targetLabel(p))];
218
+ const choice = await ctx.ui.select("Show stats for which target?", options);
219
+ if (choice === undefined || choice === null) return;
220
+ if (choice !== "All targets") {
221
+ // Filter to just the chosen target
222
+ const chosenPath = Array.from(byTarget.keys()).find((p) => targetLabel(p) === choice);
223
+ if (chosenPath) {
224
+ const runs = byTarget.get(chosenPath)!;
225
+ byTarget.clear();
226
+ byTarget.set(chosenPath, runs);
227
+ }
228
+ }
229
+ }
230
+
200
231
  const output: string[] = [];
201
232
  let targetIdx = 0;
202
233
 
203
234
  for (const [targetPath, runs] of byTarget) {
204
- const fileName = path.basename(targetPath);
235
+ const fileName = targetLabel(targetPath);
205
236
  if (targetIdx > 0) output.push("", "---", "");
206
237
  output.push(`# ${fileName}`);
207
238
  output.push(`_${targetPath}_`);
208
239
  output.push("");
209
240
 
241
+ // --- Current File Size ---
242
+ const resolvedPath = resolvePath(targetPath);
243
+ if (fs.existsSync(resolvedPath)) {
244
+ const current = computeFileMetrics(fs.readFileSync(resolvedPath, "utf-8"));
245
+ output.push(`### Current Size`);
246
+ output.push(`${current.chars.toLocaleString()} chars · ${current.words.toLocaleString()} words · ${current.lines.toLocaleString()} lines · ~${current.estTokens.toLocaleString()} tokens`);
247
+ output.push("");
248
+ }
249
+
250
+ // --- File Size Trend ---
251
+ const runsWithSize = runs.filter((r) => r.fileSize).sort((a, b) => getSourceDate(a).localeCompare(getSourceDate(b)));
252
+ if (runsWithSize.length >= 2) {
253
+ output.push("### File Size Trend");
254
+ output.push("");
255
+ for (const r of runsWithSize) {
256
+ const sz = r.fileSize!;
257
+ const date = getSourceDate(r);
258
+ const bar = "\u2588".repeat(Math.round(sz.estTokens / 1000));
259
+ output.push(`${date} ${sz.chars.toLocaleString().padStart(7)} chars ${sz.words.toLocaleString().padStart(6)} words ~${sz.estTokens.toLocaleString().padStart(6)} tok ${bar}`);
260
+ }
261
+
262
+ const first = runsWithSize[0].fileSize!;
263
+ const last = runsWithSize[runsWithSize.length - 1].fileSize!;
264
+ const charDelta = last.chars - first.chars;
265
+ const pct = first.chars > 0 ? ((charDelta / first.chars) * 100).toFixed(0) : "N/A";
266
+ output.push("");
267
+ if (charDelta > 0) {
268
+ output.push(`Trend: \u2191 grew ${charDelta.toLocaleString()} chars (+${pct}%) over ${runsWithSize.length} runs`);
269
+ } else if (charDelta < 0) {
270
+ output.push(`Trend: \u2193 shrank ${Math.abs(charDelta).toLocaleString()} chars (${pct}%) over ${runsWithSize.length} runs`);
271
+ } else {
272
+ output.push(`Trend: \u2194 unchanged`);
273
+ }
274
+ output.push("");
275
+ }
276
+
210
277
  // --- Correction Rate Trend ---
211
278
  output.push("### Correction Rate (corrections per session)");
212
279
  output.push("");
@@ -391,7 +458,7 @@ export default function (pi: ExtensionAPI) {
391
458
  planLines.push("**Backfill plan (dry run — no file edits):**");
392
459
  planLines.push("");
393
460
  for (const p of plan) {
394
- const fileName = path.basename(p.target.path);
461
+ const fileName = targetLabel(p.target.path);
395
462
  planLines.push(`- **${fileName}**: ${p.dates.length} date(s) [${p.dates[0]} \u2192 ${p.dates[p.dates.length - 1]}]`);
396
463
  }
397
464
  planLines.push("");
@@ -415,7 +482,7 @@ export default function (pi: ExtensionAPI) {
415
482
  const updatedHistory = loadHistory();
416
483
 
417
484
  for (const p of plan) {
418
- const fileName = path.basename(p.target.path);
485
+ const fileName = targetLabel(p.target.path);
419
486
 
420
487
  for (const date of p.dates) {
421
488
  ctx.ui.notify(`[${completed + failed + 1}/${totalCalls}] ${fileName} — ${date}...`, "info");