@askjo/pi-reflect 1.0.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,16 +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
- if (choice === undefined) return;
86
- target = config.targets[choice];
94
+ if (choice === undefined || choice === null) return;
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
+ }
87
102
  } else {
88
- target = config.targets[0];
103
+ targets = [config.targets[0]];
89
104
  }
90
105
  }
91
106
 
@@ -93,12 +108,28 @@ export default function (pi: ExtensionAPI) {
93
108
  ? (msg, level) => ctx.ui.notify(msg, level)
94
109
  : (msg, level) => console.log(`[reflect] [${level}] ${msg}`);
95
110
 
96
- const run = await runReflection(target, modelRegistryRef, notify);
97
-
98
- if (run) {
99
- const history = loadHistory();
100
- history.push(run);
101
- saveHistory(history);
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");
129
+ }
130
+ }
131
+ if (targets.length > 1) {
132
+ notify(`All targets finished: ${completed} completed, ${failed} failed or skipped`, failed ? "warning" : "info");
102
133
  }
103
134
  },
104
135
  });
@@ -119,7 +150,7 @@ export default function (pi: ExtensionAPI) {
119
150
  }
120
151
 
121
152
  const lines = config.targets.map((t, i) => {
122
- 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}`;
123
154
  });
124
155
 
125
156
  ctx.ui.notify(
@@ -144,7 +175,7 @@ export default function (pi: ExtensionAPI) {
144
175
  const recent = history.slice(-10).reverse();
145
176
  const lines = recent.map((r) => {
146
177
  const date = r.timestamp.slice(0, 16).replace("T", " ");
147
- const file = path.basename(r.targetPath);
178
+ const file = targetLabel(r.targetPath);
148
179
  return `- **${date}** ${file}: ${r.editsApplied} edits, ${r.correctionsFound} corrections (${r.sessionsAnalyzed} sessions)\n ${r.summary}`;
149
180
  });
150
181
 
@@ -181,16 +212,68 @@ export default function (pi: ExtensionAPI) {
181
212
  byTarget.set(key, list);
182
213
  }
183
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
+
184
231
  const output: string[] = [];
185
232
  let targetIdx = 0;
186
233
 
187
234
  for (const [targetPath, runs] of byTarget) {
188
- const fileName = path.basename(targetPath);
235
+ const fileName = targetLabel(targetPath);
189
236
  if (targetIdx > 0) output.push("", "---", "");
190
237
  output.push(`# ${fileName}`);
191
238
  output.push(`_${targetPath}_`);
192
239
  output.push("");
193
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
+
194
277
  // --- Correction Rate Trend ---
195
278
  output.push("### Correction Rate (corrections per session)");
196
279
  output.push("");
@@ -375,7 +458,7 @@ export default function (pi: ExtensionAPI) {
375
458
  planLines.push("**Backfill plan (dry run — no file edits):**");
376
459
  planLines.push("");
377
460
  for (const p of plan) {
378
- const fileName = path.basename(p.target.path);
461
+ const fileName = targetLabel(p.target.path);
379
462
  planLines.push(`- **${fileName}**: ${p.dates.length} date(s) [${p.dates[0]} \u2192 ${p.dates[p.dates.length - 1]}]`);
380
463
  }
381
464
  planLines.push("");
@@ -399,7 +482,7 @@ export default function (pi: ExtensionAPI) {
399
482
  const updatedHistory = loadHistory();
400
483
 
401
484
  for (const p of plan) {
402
- const fileName = path.basename(p.target.path);
485
+ const fileName = targetLabel(p.target.path);
403
486
 
404
487
  for (const date of p.dates) {
405
488
  ctx.ui.notify(`[${completed + failed + 1}/${totalCalls}] ${fileName} — ${date}...`, "info");