@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.
- package/.github/FUNDING.yml +1 -0
- package/.github/workflows/publish.yml +46 -0
- package/README.md +14 -0
- package/extensions/index.ts +100 -33
- package/extensions/reflect.ts +243 -70
- package/package.json +1 -1
- package/release.sh +39 -0
- package/tests/apply-edits.test.ts +51 -0
- package/tests/batching.test.ts +1 -1
- 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,18 +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
94
|
if (choice === undefined || choice === null) return;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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}. **${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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");
|