@lebronj/pi-suite 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lebronj/pi-suite",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "JHP's Pi extension suite for team coding workflows",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -85,6 +85,7 @@ Memory versioning:
85
85
  - Versioning mirror and snapshots live at `~/.pi/agent/evolution` by default.
86
86
  - No remote is configured by default; memory evolution stays local per user/machine unless the user adds a personal private remote.
87
87
  - Automatic local snapshot + commit is enabled by default; automatic push is disabled unless `PI_EVOLUTION_AUTO_PUSH=1`.
88
+ - Snapshots use a sliding window: keep the latest 100 by default, and delete the oldest snapshot directory and manifest when a new snapshot exceeds the limit.
88
89
  - Snapshots run before mutating memory tools, curator runs, learning approve/reject, session summaries/handoffs, compact handoffs, restore, and external curator `run-once`.
89
90
  - Slash commands: `/memory-version-status`, `/memory-version-snapshot [reason]`, `/memory-version-list`, `/memory-version-restore <snapshot-id> [memory|skill-drafts|all]`, `/memory-version-push`.
90
91
  - Restore always writes a pre-restore snapshot first, then restores selected files and commits the restored state.
@@ -126,6 +127,7 @@ Useful memory environment variables:
126
127
  - `PI_EVOLUTION_BRANCH`: override branch; default `main`.
127
128
  - `PI_EVOLUTION_AUTO_COMMIT=0`: disable automatic local commits.
128
129
  - `PI_EVOLUTION_AUTO_PUSH=1`: push automatically after commits.
130
+ - `PI_EVOLUTION_MAX_SNAPSHOTS`: maximum local snapshots to keep; default `100`.
129
131
 
130
132
  ## Web And Research
131
133
 
@@ -196,7 +196,7 @@ Pi-memory mirrors the authoritative runtime directories into a local evolution r
196
196
 
197
197
  Authoritative runtime data remains `~/.pi/agent/memory` and `~/.pi/agent/skill-drafts`; `~/.pi/agent/evolution` is a versioned mirror and backup repo.
198
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`. `memory_curate` also scans yesterday's daily log into `REVIEW.md` when learning is enabled and that daily file changed since the last scan. Read-only operations do not snapshot.
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`. `memory_curate` also scans yesterday's daily log into `REVIEW.md` when learning is enabled and that daily file changed since the last scan. Read-only operations do not snapshot. Snapshots use a sliding window and keep the latest 100 snapshots by default; creating snapshot 101 deletes the oldest snapshot directory and manifest.
200
200
 
201
201
  Tools and slash commands:
202
202
 
@@ -253,6 +253,7 @@ The controller uses a systemd user timer when available and falls back to cron.
253
253
  | `PI_EVOLUTION_BRANCH` | branch | `main` | Local branch used for init/clone |
254
254
  | `PI_EVOLUTION_AUTO_COMMIT` | `0`, `1`, `true`, `false` | `1` | Commit sync/snapshot changes automatically |
255
255
  | `PI_EVOLUTION_AUTO_PUSH` | `0`, `1`, `true`, `false` | `0` | Push after commits automatically |
256
+ | `PI_EVOLUTION_MAX_SNAPSHOTS` | positive integer | `100` | Maximum snapshots to keep in the local sliding window |
256
257
 
257
258
  ## Development
258
259
 
@@ -1573,6 +1573,7 @@ function formatEvolutionStatusText(status: ReturnType<typeof getEvolutionGitStat
1573
1573
  `dirty: ${status.dirty}`,
1574
1574
  `autoCommit: ${status.autoCommit}`,
1575
1575
  `autoPush: ${status.autoPush}`,
1576
+ `snapshotLimit: ${status.maxSnapshots}`,
1576
1577
  `lastCommit: ${status.lastCommit || "n/a"}`,
1577
1578
  status.status ? `status:\n${status.status}` : "status: clean",
1578
1579
  ].join("\n");
@@ -4,6 +4,7 @@ export interface EvolutionConfig {
4
4
  enabled: boolean;
5
5
  autoCommit: boolean;
6
6
  autoPush: boolean;
7
+ maxSnapshots: number;
7
8
  repoDir: string;
8
9
  remote: string | null;
9
10
  branch: string;
@@ -19,6 +20,7 @@ type EvolutionEnv = Partial<
19
20
  | "PI_EVOLUTION_ENABLED"
20
21
  | "PI_EVOLUTION_AUTO_COMMIT"
21
22
  | "PI_EVOLUTION_AUTO_PUSH"
23
+ | "PI_EVOLUTION_MAX_SNAPSHOTS"
22
24
  | "HOME"
23
25
  | "USERPROFILE"
24
26
  | "HOMEDRIVE"
@@ -30,6 +32,7 @@ type EvolutionEnv = Partial<
30
32
  export const DEFAULT_EVOLUTION_REMOTE = "";
31
33
  export const LEGACY_SHARED_EVOLUTION_REMOTE = "https://github.com/LRM-Teams/pi-evolution.git";
32
34
  export const DEFAULT_EVOLUTION_BRANCH = "main";
35
+ export const DEFAULT_EVOLUTION_MAX_SNAPSHOTS = 100;
33
36
 
34
37
  function homeDir(env: EvolutionEnv): string {
35
38
  return env.HOME ?? env.USERPROFILE ?? (env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined) ?? "~";
@@ -43,6 +46,12 @@ function truthy(value: string | undefined, fallback: boolean): boolean {
43
46
  return fallback;
44
47
  }
45
48
 
49
+ function positiveInteger(value: string | undefined, fallback: number): number {
50
+ if (value === undefined) return fallback;
51
+ const parsed = Number.parseInt(value.trim(), 10);
52
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
53
+ }
54
+
46
55
  function expandHome(input: string, env: EvolutionEnv): string {
47
56
  if (input === "~") return homeDir(env);
48
57
  if (input.startsWith("~/")) return path.join(homeDir(env), input.slice(2));
@@ -55,6 +64,7 @@ export function resolveEvolutionConfig(memoryDir: string, env: EvolutionEnv = pr
55
64
  enabled: truthy(env.PI_EVOLUTION_ENABLED, true),
56
65
  autoCommit: truthy(env.PI_EVOLUTION_AUTO_COMMIT, true),
57
66
  autoPush: truthy(env.PI_EVOLUTION_AUTO_PUSH, false),
67
+ maxSnapshots: positiveInteger(env.PI_EVOLUTION_MAX_SNAPSHOTS, DEFAULT_EVOLUTION_MAX_SNAPSHOTS),
58
68
  repoDir: path.resolve(expandHome(env.PI_EVOLUTION_DIR || path.join(agentDir, "evolution"), env)),
59
69
  remote: env.PI_EVOLUTION_REMOTE?.trim() || null,
60
70
  branch: env.PI_EVOLUTION_BRANCH || DEFAULT_EVOLUTION_BRANCH,
@@ -14,6 +14,7 @@ export interface GitStatus {
14
14
  lastCommit: string | null;
15
15
  autoPush: boolean;
16
16
  autoCommit: boolean;
17
+ maxSnapshots: number;
17
18
  enabled: boolean;
18
19
  }
19
20
 
@@ -104,6 +105,7 @@ export function getEvolutionGitStatus(config: EvolutionConfig): GitStatus {
104
105
  lastCommit: null,
105
106
  autoPush: config.autoPush,
106
107
  autoCommit: config.autoCommit,
108
+ maxSnapshots: config.maxSnapshots,
107
109
  enabled: config.enabled,
108
110
  };
109
111
  }
@@ -118,6 +120,7 @@ export function getEvolutionGitStatus(config: EvolutionConfig): GitStatus {
118
120
  lastCommit: runGit(config.repoDir, ["log", "-1", "--oneline"], { allowFailure: true }) || null,
119
121
  autoPush: config.autoPush,
120
122
  autoCommit: config.autoCommit,
123
+ maxSnapshots: config.maxSnapshots,
121
124
  enabled: config.enabled,
122
125
  };
123
126
  }
@@ -1,4 +1,4 @@
1
- export { DEFAULT_EVOLUTION_BRANCH, DEFAULT_EVOLUTION_REMOTE, LEGACY_SHARED_EVOLUTION_REMOTE, resolveEvolutionConfig, type EvolutionConfig } from "./config.ts";
1
+ export { DEFAULT_EVOLUTION_BRANCH, DEFAULT_EVOLUTION_MAX_SNAPSHOTS, DEFAULT_EVOLUTION_REMOTE, LEGACY_SHARED_EVOLUTION_REMOTE, resolveEvolutionConfig, type EvolutionConfig } from "./config.ts";
2
2
  export { commitEvolutionChanges, ensureEvolutionRepo, getEvolutionGitStatus, pushEvolution, type GitCommitResult, type GitStatus } from "./git.ts";
3
3
  export { buildManifest, createSnapshotId, listManifests, readManifest, writeManifest, type EvolutionManifest } from "./manifest.ts";
4
4
  export { restoreEvolutionSnapshot, type RestoreResult, type RestoreTarget } from "./restore.ts";
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { EvolutionConfig } from "./config.ts";
4
- import { countFiles } from "./file-utils.ts";
4
+ import { countFiles, pathExists } from "./file-utils.ts";
5
5
 
6
6
  export interface EvolutionManifest {
7
7
  id: string;
@@ -57,12 +57,27 @@ export function readManifest(config: EvolutionConfig, id: string): EvolutionMani
57
57
  }
58
58
 
59
59
  export function listManifests(config: EvolutionConfig, limit = 20): EvolutionManifest[] {
60
+ return readAllManifests(config).slice(0, limit);
61
+ }
62
+
63
+ export function pruneOldSnapshots(config: EvolutionConfig): EvolutionManifest[] {
64
+ const manifests = readAllManifests(config);
65
+ if (manifests.length <= config.maxSnapshots) return [];
66
+
67
+ const removed = manifests.slice(config.maxSnapshots);
68
+ for (const manifest of removed) {
69
+ fs.rmSync(path.join(config.repoDir, "snapshots", manifest.id), { recursive: true, force: true });
70
+ fs.rmSync(path.join(config.repoDir, "manifests", `${manifest.id}.json`), { force: true });
71
+ }
72
+ return removed;
73
+ }
74
+
75
+ function readAllManifests(config: EvolutionConfig): EvolutionManifest[] {
60
76
  const dir = path.join(config.repoDir, "manifests");
61
77
  if (!fs.existsSync(dir)) return [];
62
78
  return fs.readdirSync(dir)
63
79
  .filter((file) => file.endsWith(".json"))
64
- .sort()
65
- .reverse()
66
- .slice(0, limit)
67
- .map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8")) as EvolutionManifest);
80
+ .map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8")) as EvolutionManifest)
81
+ .filter((manifest) => pathExists(path.join(config.repoDir, "snapshots", manifest.id)))
82
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
68
83
  }
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
  import type { EvolutionConfig } from "./config.ts";
3
3
  import { copyDirContents, emptyDir } from "./file-utils.ts";
4
4
  import { commitEvolutionChanges, ensureEvolutionRepo, type GitCommitResult } from "./git.ts";
5
- import { buildManifest, createSnapshotId, writeManifest, type EvolutionManifest } from "./manifest.ts";
5
+ import { buildManifest, createSnapshotId, pruneOldSnapshots, writeManifest, type EvolutionManifest } from "./manifest.ts";
6
6
  import { syncCurrentToEvolution } from "./sync.ts";
7
7
 
8
8
  export interface SnapshotOptions {
@@ -29,6 +29,7 @@ export function createEvolutionSnapshot(config: EvolutionConfig, options: Snapsh
29
29
  copyDirContents(config.skillDraftsDir, path.join(snapshotDir, "skill-drafts"));
30
30
  const manifest = buildManifest(config, id, options.reason, options.trigger || "manual", options.sessionId);
31
31
  writeManifest(config, manifest);
32
+ pruneOldSnapshots(config);
32
33
  syncCurrentToEvolution(config);
33
34
  const commit = commitEvolutionChanges(config, options.commitMessage || `memory: snapshot ${id}`);
34
35
  return { manifest, commit };
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { test } from "node:test";
@@ -51,6 +51,24 @@ test("creates snapshot, manifest, current mirrors, and git commit", () => {
51
51
  assert.match(getEvolutionGitStatus(config).lastCommit || "", /memory: test snapshot/);
52
52
  });
53
53
 
54
+ test("prunes old snapshots beyond the configured window", () => {
55
+ const { memoryDir, repoDir, config } = testConfig();
56
+ config.maxSnapshots = 3;
57
+ mkdirSync(memoryDir, { recursive: true });
58
+
59
+ const ids: string[] = [];
60
+ for (let i = 0; i < 4; i += 1) {
61
+ writeFileSync(join(memoryDir, "MEMORY.md"), `version ${i}\n`, "utf-8");
62
+ const result = createEvolutionSnapshot(config, { reason: `snapshot ${i}`, trigger: "test", commitMessage: `memory: snapshot ${i}` });
63
+ ids.push(result.manifest?.id || "");
64
+ }
65
+
66
+ assert.equal(existsSync(join(repoDir, "snapshots", ids[0])), false);
67
+ assert.equal(existsSync(join(repoDir, "manifests", `${ids[0]}.json`)), false);
68
+ assert.equal(readdirSync(join(repoDir, "snapshots")).length, 3);
69
+ assert.deepEqual(listManifests(config, 10).map((manifest) => manifest.id), ids.slice(1).reverse());
70
+ });
71
+
54
72
  test("local-only config removes the legacy shared team remote", () => {
55
73
  const { config, repoDir } = testConfig();
56
74
  mkdirSync(repoDir, { recursive: true });