@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.
- package/.github/FUNDING.yml +1 -0
- package/.github/workflows/publish.yml +46 -0
- package/README.md +14 -0
- package/extensions/index.ts +103 -20
- package/extensions/reflect.ts +513 -139
- package/package.json +1 -1
- package/release.sh +39 -0
- package/tests/apply-edits.test.ts +51 -0
- package/tests/batching.test.ts +642 -0
- package/tests/context-files.test.ts +59 -0
- package/tests/helpers-and-utils.test.ts +29 -0
- package/tests/prompt-building.test.ts +9 -17
- package/tests/reflect.test.ts +539 -0
- package/tests/run-reflection.test.ts +33 -4
|
@@ -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 |
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
92
|
+
[...config.targets.map((t) => targetLabel(t.path)), allTargetsLabel],
|
|
84
93
|
);
|
|
85
|
-
if (choice === undefined) return;
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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}. **${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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");
|