@lebronj/pi-suite 0.1.4 → 0.1.5
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/README.md +17 -3
- package/extensions/goal-mode.ts +6 -0
- package/extensions/pet.ts +134 -3
- package/extensions/simple-replies.ts +10 -0
- package/package.json +1 -1
- package/scripts/bootstrap.sh +37 -0
- package/skills/pi-skill/SKILL.md +137 -31
- package/vendor/pi-memory/README.md +50 -0
- package/vendor/pi-memory/index.ts +248 -0
- package/vendor/pi-memory/src/cli.ts +31 -2
- package/vendor/pi-memory/src/evolution/config.ts +63 -0
- package/vendor/pi-memory/src/evolution/file-utils.ts +70 -0
- package/vendor/pi-memory/src/evolution/git.ts +132 -0
- package/vendor/pi-memory/src/evolution/index.ts +6 -0
- package/vendor/pi-memory/src/evolution/manifest.ts +68 -0
- package/vendor/pi-memory/src/evolution/restore.ts +44 -0
- package/vendor/pi-memory/src/evolution/snapshot.ts +41 -0
- package/vendor/pi-memory/src/evolution/sync.ts +20 -0
- package/vendor/pi-memory/src/index.ts +23 -0
- package/vendor/pi-memory/test/evolution.test.ts +80 -0
|
@@ -12,6 +12,7 @@ Structured, time-aware memory extension for pi. It stores memory as plain Markdo
|
|
|
12
12
|
- Curator core for exact dedupe, event lifecycle updates, temporary review, quota reset, and audit logs.
|
|
13
13
|
- Review-first learning candidates that can become memory promotions or disabled skill drafts after approval.
|
|
14
14
|
- Optional external curator service via systemd user timer or cron so curation can run even when pi is closed.
|
|
15
|
+
- Snapshot + git versioning for `memory/` and disabled `skill-drafts/`, with restore support.
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
@@ -49,6 +50,11 @@ The extension auto-creates the `pi-memory` qmd collection and path contexts on s
|
|
|
49
50
|
daily/YYYY-MM-DD.md # Daily append-only logs
|
|
50
51
|
~/.pi/agent/skill-drafts/
|
|
51
52
|
<slug>/SKILL.md # Disabled skill drafts created after approval
|
|
53
|
+
~/.pi/agent/evolution/
|
|
54
|
+
memory/ # Current memory mirror
|
|
55
|
+
skill-drafts/ # Current skill draft mirror
|
|
56
|
+
snapshots/<id>/ # Point-in-time backup with manifest.json
|
|
57
|
+
manifests/<id>.json # Snapshot manifest index
|
|
52
58
|
```
|
|
53
59
|
|
|
54
60
|
Structured entries are separated by `§` and may start with metadata:
|
|
@@ -85,6 +91,11 @@ Metadata keys currently supported by tools and curator rules:
|
|
|
85
91
|
| `memory_curator_enable` | Enable the external daily curator service |
|
|
86
92
|
| `memory_curator_disable` | Disable the external daily curator service |
|
|
87
93
|
| `memory_curator_status` | Show service backend, schedule, and state |
|
|
94
|
+
| `memory_version_status` | Show local evolution git repo status |
|
|
95
|
+
| `memory_version_snapshot` | Manually snapshot memory and skill drafts |
|
|
96
|
+
| `memory_version_list` | List recent snapshots |
|
|
97
|
+
| `memory_version_restore` | Restore `memory`, `skill-drafts`, or `all` from a snapshot id |
|
|
98
|
+
| `memory_version_push` | Manually push the evolution repo to GitHub |
|
|
88
99
|
|
|
89
100
|
### memory_write Targets
|
|
90
101
|
|
|
@@ -168,6 +179,37 @@ Old candidates are lifecycle-managed without deletion. Low-confidence candidates
|
|
|
168
179
|
|
|
169
180
|
Current learning extraction is text-based: it reads user/assistant conversation messages and asks the active model for structured candidates. It does not yet inspect structured tool-call graphs directly. Curator patch audit remains in `audit/curator.jsonl`; learning approvals are tracked through `REVIEW.md` proposal metadata and status changes.
|
|
170
181
|
|
|
182
|
+
## Memory Versioning
|
|
183
|
+
|
|
184
|
+
Pi-memory mirrors the authoritative runtime directories into a local evolution repo and stores point-in-time snapshots before important changes:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
~/.pi/agent/evolution/
|
|
188
|
+
memory/
|
|
189
|
+
skill-drafts/
|
|
190
|
+
snapshots/<snapshot-id>/
|
|
191
|
+
memory/
|
|
192
|
+
skill-drafts/
|
|
193
|
+
manifest.json
|
|
194
|
+
manifests/<snapshot-id>.json
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Authoritative runtime data remains `~/.pi/agent/memory` and `~/.pi/agent/skill-drafts`; `~/.pi/agent/evolution` is a versioned mirror and backup repo.
|
|
198
|
+
|
|
199
|
+
Automatic hooks snapshot before and sync/commit after `memory_write`, mutating `memory_edit`, mutating `scratchpad`, `memory_curate`, learning approve/reject, session summary/handoff writes, compaction handoffs, and external `jhp-pi-memory-curator run-once`. Read-only operations do not snapshot.
|
|
200
|
+
|
|
201
|
+
Tools and slash commands:
|
|
202
|
+
|
|
203
|
+
- `memory_version_status` / `/memory-version-status`
|
|
204
|
+
- `memory_version_snapshot` / `/memory-version-snapshot [reason]`
|
|
205
|
+
- `memory_version_list` / `/memory-version-list`
|
|
206
|
+
- `memory_version_restore` / `/memory-version-restore <snapshot-id> [memory|skill-drafts|all]`
|
|
207
|
+
- `memory_version_push` / `/memory-version-push`
|
|
208
|
+
|
|
209
|
+
Restore always creates a pre-restore snapshot first, then restores the selected target, syncs the mirror, and commits `memory: restore snapshot <id>`.
|
|
210
|
+
|
|
211
|
+
Default remote is `https://github.com/LRM-Teams/pi-evolution.git`. Auto commit is on; auto push is off unless `PI_EVOLUTION_AUTO_PUSH=1` is set. The repo should be private because memory contents are committed in plaintext, including any secret accidentally written to memory.
|
|
212
|
+
|
|
171
213
|
## External Curator Service
|
|
172
214
|
|
|
173
215
|
`@jhp/pi-memory` includes a CLI and pi tools for an external daily service. The service is independent of the pi process, so curation can still run when pi is closed.
|
|
@@ -178,6 +220,8 @@ CLI:
|
|
|
178
220
|
jhp-pi-memory-curator enable --schedule 03:00
|
|
179
221
|
jhp-pi-memory-curator status
|
|
180
222
|
jhp-pi-memory-curator run-once
|
|
223
|
+
jhp-pi-memory-curator snapshot --reason "manual backup"
|
|
224
|
+
jhp-pi-memory-curator push
|
|
181
225
|
jhp-pi-memory-curator disable
|
|
182
226
|
```
|
|
183
227
|
|
|
@@ -203,6 +247,12 @@ The controller uses a systemd user timer when available and falls back to cron.
|
|
|
203
247
|
| `PI_MEMORY_SKILL_DRAFTS` | `off`, `review` | `review` | Allow curator to propose disabled skill drafts |
|
|
204
248
|
| `PI_MEMORY_AUTO_APPROVE_MEMORY` | `1`, `true`, `yes`, `on` | unset | YOLO mode for approving newly created memory proposals |
|
|
205
249
|
| `PI_MEMORY_AUTO_APPROVE_SKILL_DRAFTS` | `1`, `true`, `yes`, `on` | unset | YOLO mode for creating newly proposed disabled skill drafts |
|
|
250
|
+
| `PI_EVOLUTION_ENABLED` | `0`, `1`, `true`, `false` | `1` | Enable snapshot + git versioning |
|
|
251
|
+
| `PI_EVOLUTION_DIR` | path | `~/.pi/agent/evolution` | Local evolution repo directory |
|
|
252
|
+
| `PI_EVOLUTION_REMOTE` | URL | `https://github.com/LRM-Teams/pi-evolution.git` | Git remote for manual/optional push |
|
|
253
|
+
| `PI_EVOLUTION_BRANCH` | branch | `main` | Local branch used for init/clone |
|
|
254
|
+
| `PI_EVOLUTION_AUTO_COMMIT` | `0`, `1`, `true`, `false` | `1` | Commit sync/snapshot changes automatically |
|
|
255
|
+
| `PI_EVOLUTION_AUTO_PUSH` | `0`, `1`, `true`, `false` | `0` | Push after commits automatically |
|
|
206
256
|
|
|
207
257
|
## Development
|
|
208
258
|
|
|
@@ -50,6 +50,16 @@ import {
|
|
|
50
50
|
import { applyReviewLifecycle, approveMemoryPromotion, proposeMemoryPromotions, rejectReviewItem } from "./src/learning/memory.ts";
|
|
51
51
|
import { approveSkillDraft, listSkillDraftProposals, proposeSkillDrafts } from "./src/learning/skills.ts";
|
|
52
52
|
import { FileMemoryStore } from "./src/curator-store/file-store.ts";
|
|
53
|
+
import {
|
|
54
|
+
createEvolutionSnapshot,
|
|
55
|
+
getEvolutionGitStatus,
|
|
56
|
+
listManifests,
|
|
57
|
+
pushEvolution,
|
|
58
|
+
resolveEvolutionConfig,
|
|
59
|
+
restoreEvolutionSnapshot,
|
|
60
|
+
syncEvolutionAfterChange,
|
|
61
|
+
type RestoreTarget,
|
|
62
|
+
} from "./src/evolution/index.ts";
|
|
53
63
|
import { disableCuratorService, enableCuratorService, getCuratorServiceStatus } from "./src/service-controller.ts";
|
|
54
64
|
|
|
55
65
|
|
|
@@ -82,6 +92,10 @@ let SCRATCHPAD_FILE = path.join(MEMORY_DIR, "SCRATCHPAD.md");
|
|
|
82
92
|
let DAILY_DIR = path.join(MEMORY_DIR, "daily");
|
|
83
93
|
let SKILL_DRAFTS_DIR = path.join(path.dirname(MEMORY_DIR), "skill-drafts");
|
|
84
94
|
|
|
95
|
+
function currentEvolutionConfig() {
|
|
96
|
+
return resolveEvolutionConfig(MEMORY_DIR);
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
/** Override base directory (for testing). */
|
|
86
100
|
export function _setBaseDir(baseDir: string) {
|
|
87
101
|
MEMORY_DIR = baseDir;
|
|
@@ -313,12 +327,14 @@ const STRUCTURED_MEMORY_TARGETS = ["memory", "user", "state", "review"] as const
|
|
|
313
327
|
const MEMORY_WRITE_TARGETS = ["long_term", "daily", "state", "user", "review"] as const;
|
|
314
328
|
const MEMORY_READ_TARGETS = ["long_term", "scratchpad", "daily", "list", "user", "state", "review", "all"] as const;
|
|
315
329
|
const MEMORY_EDIT_ACTIONS = ["read", "add", "replace", "remove", "replace_all", "compact"] as const;
|
|
330
|
+
const MEMORY_VERSION_RESTORE_TARGETS = ["memory", "skill-drafts", "all"] as const;
|
|
316
331
|
const STRUCTURED_MEMORY_TYPES = ["fact", "preference", "event", "temporary", "quota", "review"] as const;
|
|
317
332
|
|
|
318
333
|
type StructuredMemoryTarget = (typeof STRUCTURED_MEMORY_TARGETS)[number];
|
|
319
334
|
type MemoryWriteTarget = (typeof MEMORY_WRITE_TARGETS)[number];
|
|
320
335
|
type MemoryReadTarget = (typeof MEMORY_READ_TARGETS)[number];
|
|
321
336
|
type MemoryEditAction = (typeof MEMORY_EDIT_ACTIONS)[number];
|
|
337
|
+
type MemoryVersionRestoreTarget = (typeof MEMORY_VERSION_RESTORE_TARGETS)[number];
|
|
322
338
|
|
|
323
339
|
type StructuredWriteOptions = {
|
|
324
340
|
target: StructuredMemoryTarget;
|
|
@@ -830,6 +846,7 @@ export function buildTransitionHandoff(ctx: ExtensionContext, reason: Transition
|
|
|
830
846
|
|
|
831
847
|
async function writeTransitionHandoff(ctx: ExtensionContext, reason: TransitionHandoffReason): Promise<boolean> {
|
|
832
848
|
ensureDirs();
|
|
849
|
+
await evolutionBeforeChange(`session transition handoff ${formatTransitionHandoffReason(reason)}`, "memory: snapshot before handoff", "session", shortSessionId(ctx.sessionManager.getSessionId()));
|
|
833
850
|
const sid = shortSessionId(ctx.sessionManager.getSessionId());
|
|
834
851
|
const ts = nowTimestamp();
|
|
835
852
|
const handoff = buildTransitionHandoff(ctx, reason, sid, ts);
|
|
@@ -840,6 +857,7 @@ async function writeTransitionHandoff(ctx: ExtensionContext, reason: TransitionH
|
|
|
840
857
|
fs.writeFileSync(filePath, existing + separator + handoff, "utf-8");
|
|
841
858
|
await ensureQmdAvailableForUpdate();
|
|
842
859
|
await runQmdUpdateNow();
|
|
860
|
+
await evolutionAfterChange("memory: sync after handoff");
|
|
843
861
|
return true;
|
|
844
862
|
}
|
|
845
863
|
|
|
@@ -1423,6 +1441,7 @@ function getSnapshotMode(): "stable" | "per-turn" {
|
|
|
1423
1441
|
|
|
1424
1442
|
async function runCurator(reason: string): Promise<string> {
|
|
1425
1443
|
ensureDirs();
|
|
1444
|
+
await evolutionBeforeChange(`curator before ${reason}`, "memory: snapshot before curate", "tool");
|
|
1426
1445
|
const store = new FileMemoryStore(MEMORY_DIR);
|
|
1427
1446
|
const result = await runMemoryCuratorOnce({
|
|
1428
1447
|
memoryStore: store,
|
|
@@ -1452,6 +1471,7 @@ async function runCurator(reason: string): Promise<string> {
|
|
|
1452
1471
|
await ensureQmdAvailableForUpdate();
|
|
1453
1472
|
scheduleQmdUpdate();
|
|
1454
1473
|
}
|
|
1474
|
+
await evolutionAfterChange("memory: sync after curate");
|
|
1455
1475
|
const notes = [
|
|
1456
1476
|
memoryResult.created > 0 ? `proposed ${memoryResult.created} memory promotion(s)` : "",
|
|
1457
1477
|
skillResult.created > 0 ? `proposed ${skillResult.created} skill draft(s)` : "",
|
|
@@ -1461,6 +1481,64 @@ async function runCurator(reason: string): Promise<string> {
|
|
|
1461
1481
|
return notes.length > 0 ? `${result.summary}; ${notes.join("; ")}` : result.summary;
|
|
1462
1482
|
}
|
|
1463
1483
|
|
|
1484
|
+
async function evolutionBeforeChange(reason: string, commitMessage: string, trigger = "tool", sessionId?: string): Promise<void> {
|
|
1485
|
+
try {
|
|
1486
|
+
createEvolutionSnapshot(currentEvolutionConfig(), { reason, trigger, sessionId, commitMessage });
|
|
1487
|
+
} catch {
|
|
1488
|
+
// Memory operations should not fail just because versioning is unavailable.
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async function evolutionAfterChange(commitMessage: string): Promise<void> {
|
|
1493
|
+
try {
|
|
1494
|
+
const config = currentEvolutionConfig();
|
|
1495
|
+
syncEvolutionAfterChange(config, commitMessage);
|
|
1496
|
+
if (config.autoPush) pushEvolution(config);
|
|
1497
|
+
} catch {
|
|
1498
|
+
// Best-effort only; the memory write itself remains authoritative.
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function formatEvolutionStatusText(status: ReturnType<typeof getEvolutionGitStatus>): string {
|
|
1503
|
+
return [
|
|
1504
|
+
`enabled: ${status.enabled}`,
|
|
1505
|
+
`repo: ${status.repoDir}`,
|
|
1506
|
+
`initialized: ${status.initialized}`,
|
|
1507
|
+
`branch: ${status.branch || "n/a"}`,
|
|
1508
|
+
`remote: ${status.remote || "n/a"}`,
|
|
1509
|
+
`dirty: ${status.dirty}`,
|
|
1510
|
+
`autoCommit: ${status.autoCommit}`,
|
|
1511
|
+
`autoPush: ${status.autoPush}`,
|
|
1512
|
+
`lastCommit: ${status.lastCommit || "n/a"}`,
|
|
1513
|
+
status.status ? `status:\n${status.status}` : "status: clean",
|
|
1514
|
+
].join("\n");
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function startupCuratorHintEnabled(): boolean {
|
|
1518
|
+
const value = process.env.PI_MEMORY_CURATOR_STARTUP_HINT?.trim().toLowerCase();
|
|
1519
|
+
return !value || !["0", "false", "off", "no"].includes(value);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function notifyDisabledCuratorService(ctx: ExtensionContext): void {
|
|
1523
|
+
if (!ctx.hasUI || !startupCuratorHintEnabled()) return;
|
|
1524
|
+
try {
|
|
1525
|
+
const result = getCuratorServiceStatus({ memoryDir: MEMORY_DIR, cliPath: new URL("./src/cli.ts", import.meta.url).pathname });
|
|
1526
|
+
if (result.state.enabled) return;
|
|
1527
|
+
ctx.ui.notify(
|
|
1528
|
+
[
|
|
1529
|
+
"Memory self-evolution curator is off.",
|
|
1530
|
+
"It can run daily outside pi to maintain memory lifecycle, review repeated learnings, and propose disabled skill drafts.",
|
|
1531
|
+
"Enable: /memory-curator-enable 03:00",
|
|
1532
|
+
"Status: /memory-curator-status",
|
|
1533
|
+
"Disable: /memory-curator-disable",
|
|
1534
|
+
].join("\n"),
|
|
1535
|
+
"info",
|
|
1536
|
+
);
|
|
1537
|
+
} catch {
|
|
1538
|
+
// Startup hints should never block memory initialization.
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1464
1542
|
/** Reset snapshot state (for testing). */
|
|
1465
1543
|
export function _resetMemorySnapshot() {
|
|
1466
1544
|
memorySnapshot = null;
|
|
@@ -1475,6 +1553,31 @@ export function _resetMemorySnapshot() {
|
|
|
1475
1553
|
// ---------------------------------------------------------------------------
|
|
1476
1554
|
|
|
1477
1555
|
export default function (pi: ExtensionAPI) {
|
|
1556
|
+
const versionedToolCommitMessages: Record<string, string> = {
|
|
1557
|
+
memory_write: "memory: sync after memory_write",
|
|
1558
|
+
memory_edit: "memory: sync after memory_edit",
|
|
1559
|
+
scratchpad: "memory: sync after scratchpad",
|
|
1560
|
+
memory_learning_approve: "memory: sync after learning approve",
|
|
1561
|
+
memory_learning_reject: "memory: sync after learning reject",
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
function shouldVersionToolCall(toolName: string, input: unknown): boolean {
|
|
1565
|
+
if (!(toolName in versionedToolCommitMessages)) return false;
|
|
1566
|
+
if (toolName === "scratchpad" && (input as { action?: string })?.action === "list") return false;
|
|
1567
|
+
if (toolName === "memory_edit" && (input as { action?: string })?.action === "read") return false;
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
1572
|
+
if (!shouldVersionToolCall(event.toolName, event.input)) return;
|
|
1573
|
+
await evolutionBeforeChange(`${event.toolName} tool`, `memory: snapshot before ${event.toolName}`, "tool", shortSessionId(ctx.sessionManager.getSessionId()));
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
pi.on("tool_result", async (event) => {
|
|
1577
|
+
if (event.isError || !shouldVersionToolCall(event.toolName, event.input)) return;
|
|
1578
|
+
await evolutionAfterChange(versionedToolCommitMessages[event.toolName]);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1478
1581
|
// --- session_start: detect qmd, auto-setup collection ---
|
|
1479
1582
|
pi.on("session_start", async (_event, ctx) => {
|
|
1480
1583
|
ensureDirs();
|
|
@@ -1492,6 +1595,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1492
1595
|
return undefined;
|
|
1493
1596
|
});
|
|
1494
1597
|
}
|
|
1598
|
+
notifyDisabledCuratorService(ctx);
|
|
1495
1599
|
|
|
1496
1600
|
qmdAvailable = await detectQmd();
|
|
1497
1601
|
if (!qmdAvailable) {
|
|
@@ -1548,6 +1652,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1548
1652
|
const result = await generateExitSummary(ctx);
|
|
1549
1653
|
if (result.hasMessages) {
|
|
1550
1654
|
const summary = result.summary ?? buildExitSummaryFallback(result.error);
|
|
1655
|
+
await evolutionBeforeChange("session shutdown summary", "memory: snapshot before session summary", "session", shortSessionId(ctx.sessionManager.getSessionId()));
|
|
1551
1656
|
const sid = shortSessionId(ctx.sessionManager.getSessionId());
|
|
1552
1657
|
const ts = nowTimestamp();
|
|
1553
1658
|
const entry = formatExitSummaryEntry(summary, reason, sid, ts);
|
|
@@ -1558,6 +1663,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1558
1663
|
await runSessionLearningExtractor(ctx);
|
|
1559
1664
|
await ensureQmdAvailableForUpdate();
|
|
1560
1665
|
await runQmdUpdateNow();
|
|
1666
|
+
await evolutionAfterChange("memory: sync after session summary");
|
|
1561
1667
|
}
|
|
1562
1668
|
}
|
|
1563
1669
|
} finally {
|
|
@@ -1660,6 +1766,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1660
1766
|
// source files) would keep being injected.
|
|
1661
1767
|
try {
|
|
1662
1768
|
if (parts.length === 0) return;
|
|
1769
|
+
await evolutionBeforeChange("session before compact handoff", "memory: snapshot before compact handoff", "session", sid);
|
|
1663
1770
|
|
|
1664
1771
|
const handoff = [`<!-- HANDOFF ${ts} [${sid}] -->`, "## Session Handoff", ...parts].join("\n");
|
|
1665
1772
|
|
|
@@ -1669,6 +1776,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1669
1776
|
fs.writeFileSync(filePath, existing + separator + handoff, "utf-8");
|
|
1670
1777
|
await ensureQmdAvailableForUpdate();
|
|
1671
1778
|
scheduleQmdUpdate();
|
|
1779
|
+
await evolutionAfterChange("memory: sync after compact handoff");
|
|
1672
1780
|
} finally {
|
|
1673
1781
|
refreshMemorySnapshot("session_before_compact");
|
|
1674
1782
|
}
|
|
@@ -2237,6 +2345,146 @@ export default function (pi: ExtensionAPI) {
|
|
|
2237
2345
|
},
|
|
2238
2346
|
});
|
|
2239
2347
|
|
|
2348
|
+
// --- memory versioning tools ---
|
|
2349
|
+
pi.registerTool({
|
|
2350
|
+
name: "memory_version_status",
|
|
2351
|
+
label: "Memory Version Status",
|
|
2352
|
+
description: "View evolution repo status, remote, branch, dirty state, last commit, and auto-push setting.",
|
|
2353
|
+
parameters: Type.Object({}),
|
|
2354
|
+
async execute() {
|
|
2355
|
+
try {
|
|
2356
|
+
const status = getEvolutionGitStatus(currentEvolutionConfig());
|
|
2357
|
+
return { content: [{ type: "text", text: formatEvolutionStatusText(status) }], details: status };
|
|
2358
|
+
} catch (error) {
|
|
2359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2360
|
+
return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
pi.registerTool({
|
|
2366
|
+
name: "memory_version_snapshot",
|
|
2367
|
+
label: "Memory Version Snapshot",
|
|
2368
|
+
description: "Manually create a memory + skill-drafts snapshot, sync current mirrors, and commit if changed.",
|
|
2369
|
+
parameters: Type.Object({
|
|
2370
|
+
reason: Type.Optional(Type.String({ description: "Snapshot reason" })),
|
|
2371
|
+
}),
|
|
2372
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2373
|
+
try {
|
|
2374
|
+
const result = createEvolutionSnapshot(currentEvolutionConfig(), {
|
|
2375
|
+
reason: params.reason || "manual snapshot",
|
|
2376
|
+
trigger: "tool",
|
|
2377
|
+
sessionId: shortSessionId(ctx.sessionManager.getSessionId()),
|
|
2378
|
+
commitMessage: "memory: manual snapshot",
|
|
2379
|
+
});
|
|
2380
|
+
if (currentEvolutionConfig().autoPush) pushEvolution(currentEvolutionConfig());
|
|
2381
|
+
return { content: [{ type: "text", text: result.manifest ? `Snapshot ${result.manifest.id}` : `Snapshot skipped: ${result.skipped}` }], details: result };
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2384
|
+
return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
pi.registerTool({
|
|
2390
|
+
name: "memory_version_list",
|
|
2391
|
+
label: "Memory Version List",
|
|
2392
|
+
description: "List recent memory evolution snapshots.",
|
|
2393
|
+
parameters: Type.Object({
|
|
2394
|
+
limit: Type.Optional(Type.Number({ description: "Max snapshots to list. Default: 20" })),
|
|
2395
|
+
}),
|
|
2396
|
+
async execute(_toolCallId, params) {
|
|
2397
|
+
try {
|
|
2398
|
+
const manifests = listManifests(currentEvolutionConfig(), params.limit || 20);
|
|
2399
|
+
const text = manifests.length === 0
|
|
2400
|
+
? "No snapshots found."
|
|
2401
|
+
: manifests.map((manifest) => `- ${manifest.id} ${manifest.createdAt} ${manifest.reason} (${manifest.files.memory} memory files, ${manifest.files.skillDrafts} skill files)`).join("\n");
|
|
2402
|
+
return { content: [{ type: "text", text }], details: { manifests } };
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2405
|
+
return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
|
|
2406
|
+
}
|
|
2407
|
+
},
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
pi.registerTool({
|
|
2411
|
+
name: "memory_version_restore",
|
|
2412
|
+
label: "Memory Version Restore",
|
|
2413
|
+
description: "Restore memory and/or skill drafts from a snapshot id. Creates a pre-restore snapshot first.",
|
|
2414
|
+
parameters: Type.Object({
|
|
2415
|
+
id: Type.String({ description: "Snapshot id" }),
|
|
2416
|
+
target: Type.Optional(StringEnum(MEMORY_VERSION_RESTORE_TARGETS, { description: "Restore target. Default: all" })),
|
|
2417
|
+
}),
|
|
2418
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2419
|
+
try {
|
|
2420
|
+
const target = (params.target || "all") as MemoryVersionRestoreTarget;
|
|
2421
|
+
const result = restoreEvolutionSnapshot(currentEvolutionConfig(), params.id, target as RestoreTarget, shortSessionId(ctx.sessionManager.getSessionId()));
|
|
2422
|
+
snapshotDirty = true;
|
|
2423
|
+
return { content: [{ type: "text", text: `Restored ${target} from snapshot ${result.restored.id}.` }], details: result };
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2426
|
+
return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
|
|
2427
|
+
}
|
|
2428
|
+
},
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
pi.registerTool({
|
|
2432
|
+
name: "memory_version_push",
|
|
2433
|
+
label: "Memory Version Push",
|
|
2434
|
+
description: "Manually push the local evolution repo to GitHub.",
|
|
2435
|
+
parameters: Type.Object({}),
|
|
2436
|
+
async execute() {
|
|
2437
|
+
try {
|
|
2438
|
+
const output = pushEvolution(currentEvolutionConfig());
|
|
2439
|
+
return { content: [{ type: "text", text: output || "Pushed evolution repo." }], details: {} };
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2442
|
+
return { content: [{ type: "text", text: message }], details: { error: message }, isError: true };
|
|
2443
|
+
}
|
|
2444
|
+
},
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
pi.registerCommand("memory-version-status", {
|
|
2448
|
+
description: "Show memory evolution repo status",
|
|
2449
|
+
handler: async (_args, ctx) => ctx.ui.notify(formatEvolutionStatusText(getEvolutionGitStatus(currentEvolutionConfig())), "info"),
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
pi.registerCommand("memory-version-snapshot", {
|
|
2453
|
+
description: "Create a memory evolution snapshot",
|
|
2454
|
+
handler: async (args, ctx) => {
|
|
2455
|
+
const result = createEvolutionSnapshot(currentEvolutionConfig(), { reason: args.trim() || "manual snapshot", trigger: "slash_command", sessionId: shortSessionId(ctx.sessionManager.getSessionId()), commitMessage: "memory: manual snapshot" });
|
|
2456
|
+
ctx.ui.notify(result.manifest ? `Snapshot ${result.manifest.id}` : `Snapshot skipped: ${result.skipped}`, "info");
|
|
2457
|
+
},
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
pi.registerCommand("memory-version-list", {
|
|
2461
|
+
description: "List memory evolution snapshots",
|
|
2462
|
+
handler: async (_args, ctx) => {
|
|
2463
|
+
const manifests = listManifests(currentEvolutionConfig(), 20);
|
|
2464
|
+
ctx.ui.notify(manifests.length === 0 ? "No snapshots found." : manifests.map((manifest) => `${manifest.id} ${manifest.reason}`).join("\n"), "info");
|
|
2465
|
+
},
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
pi.registerCommand("memory-version-restore", {
|
|
2469
|
+
description: "Restore memory evolution snapshot: /memory-version-restore <snapshot-id> [memory|skill-drafts|all]",
|
|
2470
|
+
handler: async (args, ctx) => {
|
|
2471
|
+
const [id, rawTarget] = args.trim().split(/\s+/);
|
|
2472
|
+
if (!id) {
|
|
2473
|
+
ctx.ui.notify("Usage: /memory-version-restore <snapshot-id> [memory|skill-drafts|all]", "error");
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const target = MEMORY_VERSION_RESTORE_TARGETS.includes(rawTarget as MemoryVersionRestoreTarget) ? rawTarget as MemoryVersionRestoreTarget : "all";
|
|
2477
|
+
const result = restoreEvolutionSnapshot(currentEvolutionConfig(), id, target as RestoreTarget, shortSessionId(ctx.sessionManager.getSessionId()));
|
|
2478
|
+
snapshotDirty = true;
|
|
2479
|
+
ctx.ui.notify(`Restored ${target} from snapshot ${result.restored.id}.`, "info");
|
|
2480
|
+
},
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2483
|
+
pi.registerCommand("memory-version-push", {
|
|
2484
|
+
description: "Push memory evolution repo",
|
|
2485
|
+
handler: async (_args, ctx) => ctx.ui.notify(pushEvolution(currentEvolutionConfig()) || "Pushed evolution repo.", "info"),
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2240
2488
|
// --- memory_search tool ---
|
|
2241
2489
|
pi.registerTool({
|
|
2242
2490
|
name: "memory_search",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { JsonlAuditLog } from "./curator-core/audit.ts";
|
|
3
3
|
import { runMemoryCuratorOnce } from "./curator-core/curate.ts";
|
|
4
4
|
import { FileMemoryStore } from "./curator-store/file-store.ts";
|
|
5
|
+
import { pushEvolution, resolveEvolutionConfig, syncEvolutionAfterChange, createEvolutionSnapshot } from "./evolution/index.ts";
|
|
5
6
|
import { disableCuratorService, enableCuratorService, getCuratorServiceStatus, resolveMemoryDir } from "./service-controller.ts";
|
|
6
7
|
|
|
7
8
|
function cliPath(): string {
|
|
@@ -22,6 +23,8 @@ function usage(): string {
|
|
|
22
23
|
return [
|
|
23
24
|
"Usage:",
|
|
24
25
|
" jhp-pi-memory-curator run-once [--memory-dir <path>] [--reason <text>] [--dry-run] [--json]",
|
|
26
|
+
" jhp-pi-memory-curator snapshot [--memory-dir <path>] [--reason <text>] [--json]",
|
|
27
|
+
" jhp-pi-memory-curator push [--memory-dir <path>]",
|
|
25
28
|
" jhp-pi-memory-curator enable [--memory-dir <path>] [--schedule HH:MM]",
|
|
26
29
|
" jhp-pi-memory-curator disable [--memory-dir <path>]",
|
|
27
30
|
" jhp-pi-memory-curator status [--memory-dir <path>]",
|
|
@@ -38,17 +41,43 @@ async function main(): Promise<void> {
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
if (command === "run-once") {
|
|
44
|
+
const reason = readOption(args, "--reason") || "cli";
|
|
45
|
+
const config = resolveEvolutionConfig(memoryDir);
|
|
46
|
+
if (!hasFlag(args, "--dry-run")) {
|
|
47
|
+
createEvolutionSnapshot(config, { reason: `curator before ${reason}`, trigger: "external_curator", commitMessage: "memory: snapshot before curator" });
|
|
48
|
+
}
|
|
41
49
|
const result = await runMemoryCuratorOnce({
|
|
42
50
|
memoryStore: new FileMemoryStore(memoryDir),
|
|
43
51
|
auditLog: new JsonlAuditLog(memoryDir),
|
|
44
|
-
reason
|
|
52
|
+
reason,
|
|
45
53
|
dryRun: hasFlag(args, "--dry-run"),
|
|
46
54
|
});
|
|
47
|
-
|
|
55
|
+
let evolutionCommit = null;
|
|
56
|
+
if (!hasFlag(args, "--dry-run")) {
|
|
57
|
+
evolutionCommit = syncEvolutionAfterChange(config, "memory: sync after external curator");
|
|
58
|
+
if (config.autoPush) pushEvolution(config);
|
|
59
|
+
}
|
|
60
|
+
if (hasFlag(args, "--json")) console.log(JSON.stringify({ ...result, evolutionCommit }, null, 2));
|
|
48
61
|
else console.log(result.summary);
|
|
49
62
|
return;
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
if (command === "snapshot") {
|
|
66
|
+
const result = createEvolutionSnapshot(resolveEvolutionConfig(memoryDir), {
|
|
67
|
+
reason: readOption(args, "--reason") || "cli snapshot",
|
|
68
|
+
trigger: "cli",
|
|
69
|
+
commitMessage: "memory: manual snapshot",
|
|
70
|
+
});
|
|
71
|
+
if (hasFlag(args, "--json")) console.log(JSON.stringify(result, null, 2));
|
|
72
|
+
else console.log(result.manifest ? `Snapshot ${result.manifest.id}` : `Snapshot skipped: ${result.skipped}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (command === "push") {
|
|
77
|
+
console.log(pushEvolution(resolveEvolutionConfig(memoryDir)) || "Pushed evolution repo.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
if (command === "enable" || command === "install-service") {
|
|
53
82
|
const result = enableCuratorService({ memoryDir, cliPath: cliPath(), schedule: readOption(args, "--schedule") });
|
|
54
83
|
console.log(result.message);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface EvolutionConfig {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
autoCommit: boolean;
|
|
6
|
+
autoPush: boolean;
|
|
7
|
+
repoDir: string;
|
|
8
|
+
remote: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
memoryDir: string;
|
|
11
|
+
skillDraftsDir: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type EvolutionEnv = Partial<
|
|
15
|
+
Record<
|
|
16
|
+
| "PI_EVOLUTION_DIR"
|
|
17
|
+
| "PI_EVOLUTION_REMOTE"
|
|
18
|
+
| "PI_EVOLUTION_BRANCH"
|
|
19
|
+
| "PI_EVOLUTION_ENABLED"
|
|
20
|
+
| "PI_EVOLUTION_AUTO_COMMIT"
|
|
21
|
+
| "PI_EVOLUTION_AUTO_PUSH"
|
|
22
|
+
| "HOME"
|
|
23
|
+
| "USERPROFILE"
|
|
24
|
+
| "HOMEDRIVE"
|
|
25
|
+
| "HOMEPATH",
|
|
26
|
+
string | undefined
|
|
27
|
+
>
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_EVOLUTION_REMOTE = "https://github.com/LRM-Teams/pi-evolution.git";
|
|
31
|
+
export const DEFAULT_EVOLUTION_BRANCH = "main";
|
|
32
|
+
|
|
33
|
+
function homeDir(env: EvolutionEnv): string {
|
|
34
|
+
return env.HOME ?? env.USERPROFILE ?? (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined) ?? "~";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function truthy(value: string | undefined, fallback: boolean): boolean {
|
|
38
|
+
if (value === undefined) return fallback;
|
|
39
|
+
const normalized = value.trim().toLowerCase();
|
|
40
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
41
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function expandHome(input: string, env: EvolutionEnv): string {
|
|
46
|
+
if (input === "~") return homeDir(env);
|
|
47
|
+
if (input.startsWith("~/")) return path.join(homeDir(env), input.slice(2));
|
|
48
|
+
return input;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = process.env): EvolutionConfig {
|
|
52
|
+
const agentDir = path.dirname(memoryDir);
|
|
53
|
+
return {
|
|
54
|
+
enabled: truthy(env.PI_EVOLUTION_ENABLED, true),
|
|
55
|
+
autoCommit: truthy(env.PI_EVOLUTION_AUTO_COMMIT, true),
|
|
56
|
+
autoPush: truthy(env.PI_EVOLUTION_AUTO_PUSH, false),
|
|
57
|
+
repoDir: path.resolve(expandHome(env.PI_EVOLUTION_DIR || path.join(agentDir, "evolution"), env)),
|
|
58
|
+
remote: env.PI_EVOLUTION_REMOTE || DEFAULT_EVOLUTION_REMOTE,
|
|
59
|
+
branch: env.PI_EVOLUTION_BRANCH || DEFAULT_EVOLUTION_BRANCH,
|
|
60
|
+
memoryDir,
|
|
61
|
+
skillDraftsDir: path.join(agentDir, "skill-drafts"),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function pathExists(filePath: string): boolean {
|
|
5
|
+
try {
|
|
6
|
+
fs.accessSync(filePath);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function emptyDir(dir: string): void {
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function copyDirContents(source: string, destination: string, options: { exclude?: (relativePath: string, isDirectory: boolean) => boolean } = {}): void {
|
|
19
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
20
|
+
if (!pathExists(source)) return;
|
|
21
|
+
const entries = fs.readdirSync(source, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const sourcePath = path.join(source, entry.name);
|
|
24
|
+
const destinationPath = path.join(destination, entry.name);
|
|
25
|
+
const relativePath = entry.name;
|
|
26
|
+
if (options.exclude?.(relativePath, entry.isDirectory())) continue;
|
|
27
|
+
copyPath(sourcePath, destinationPath, options, relativePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function copyPath(source: string, destination: string, options: { exclude?: (relativePath: string, isDirectory: boolean) => boolean }, relativePath: string): void {
|
|
32
|
+
const stat = fs.lstatSync(source);
|
|
33
|
+
if (options.exclude?.(relativePath, stat.isDirectory())) return;
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
36
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
37
|
+
copyPath(path.join(source, entry.name), path.join(destination, entry.name), options, path.join(relativePath, entry.name));
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (stat.isSymbolicLink()) {
|
|
42
|
+
try {
|
|
43
|
+
fs.symlinkSync(fs.readlinkSync(source), destination);
|
|
44
|
+
} catch {
|
|
45
|
+
// If symlink recreation fails (for example on Windows), copy target contents instead.
|
|
46
|
+
fs.copyFileSync(fs.realpathSync(source), destination);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (stat.isFile()) {
|
|
51
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
52
|
+
fs.copyFileSync(source, destination);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function replaceDirFrom(source: string, destination: string): void {
|
|
57
|
+
emptyDir(destination);
|
|
58
|
+
copyDirContents(source, destination);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function countFiles(dir: string): number {
|
|
62
|
+
if (!pathExists(dir)) return 0;
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
const entryPath = path.join(dir, entry.name);
|
|
66
|
+
if (entry.isDirectory()) count += countFiles(entryPath);
|
|
67
|
+
else if (entry.isFile() || entry.isSymbolicLink()) count += 1;
|
|
68
|
+
}
|
|
69
|
+
return count;
|
|
70
|
+
}
|