@lebronj/pi-suite 0.1.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/LICENSE +21 -0
- package/README.md +86 -0
- package/extensions/pet.ts +1033 -0
- package/extensions/prompt-url-widget.ts +158 -0
- package/extensions/redraws.ts +24 -0
- package/extensions/snake.ts +343 -0
- package/extensions/tps.ts +47 -0
- package/package.json +69 -0
- package/prompts/cl.md +54 -0
- package/prompts/is.md +25 -0
- package/prompts/pr.md +37 -0
- package/prompts/wr.md +35 -0
- package/scripts/bootstrap.sh +95 -0
- package/skills/add-llm-provider.md +57 -0
- package/skills/image-to-editable-ppt-slide/SKILL.md +113 -0
- package/skills/image-to-editable-ppt-slide/scripts/generate_spec_template.py +91 -0
- package/skills/image-to-editable-ppt-slide/scripts/pptx_rebuilder.py +181 -0
- package/skills/leetcode-array/SKILL.md +40 -0
- package/skills/leetcode-array/problems/best_time_to_buy_and_sell_stock.py +19 -0
- package/skills/leetcode-array/problems/product_of_array_except_self.py +22 -0
- package/skills/leetcode-array/problems/two_sum.py +19 -0
- package/skills/pi-skill/SKILL.md +154 -0
- package/skills/weather.md +49 -0
- package/vendor/pi-memory/LICENSE +21 -0
- package/vendor/pi-memory/README.md +223 -0
- package/vendor/pi-memory/index.ts +2367 -0
- package/vendor/pi-memory/package.json +68 -0
- package/vendor/pi-memory/scripts/postinstall.cjs +44 -0
- package/vendor/pi-memory/src/cli.ts +79 -0
- package/vendor/pi-memory/src/curator-core/audit.ts +45 -0
- package/vendor/pi-memory/src/curator-core/curate.ts +90 -0
- package/vendor/pi-memory/src/curator-core/metadata.ts +55 -0
- package/vendor/pi-memory/src/curator-core/patch.ts +24 -0
- package/vendor/pi-memory/src/curator-core/policy.ts +77 -0
- package/vendor/pi-memory/src/curator-store/file-store.ts +51 -0
- package/vendor/pi-memory/src/curator-store/types.ts +21 -0
- package/vendor/pi-memory/src/index.ts +35 -0
- package/vendor/pi-memory/src/learning/candidates.ts +205 -0
- package/vendor/pi-memory/src/learning/memory.ts +144 -0
- package/vendor/pi-memory/src/learning/skills.ts +200 -0
- package/vendor/pi-memory/src/service-controller.ts +248 -0
- package/vendor/pi-memory/test/curate.test.ts +68 -0
- package/vendor/pi-memory/test/learning-candidates.test.ts +107 -0
- package/vendor/pi-memory/test/memory-promotions.test.ts +44 -0
- package/vendor/pi-memory/test/metadata.test.ts +17 -0
- package/vendor/pi-memory/test/skill-drafts.test.ts +57 -0
- package/vendor/pi-memory/test/transition-handoff.test.ts +86 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jhp/pi-memory",
|
|
3
|
+
"version": "0.3.12",
|
|
4
|
+
"description": "Pi coding agent extension for structured time-aware memory with qmd-powered search and curator support",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jhp-pi-memory-curator": "./src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi",
|
|
12
|
+
"pi-coding-agent",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"memory",
|
|
15
|
+
"search",
|
|
16
|
+
"qmd",
|
|
17
|
+
"curator",
|
|
18
|
+
"time-aware-memory",
|
|
19
|
+
"scratchpad",
|
|
20
|
+
"daily-log"
|
|
21
|
+
],
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"author": "JHP",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/jhp/pi-memory.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/jhp/pi-memory/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/jhp/pi-memory#readme",
|
|
37
|
+
"files": [
|
|
38
|
+
"index.ts",
|
|
39
|
+
"src",
|
|
40
|
+
"scripts",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
49
|
+
"build": "tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"lint": "biome check .",
|
|
51
|
+
"test": "tsx --test test/*.test.ts",
|
|
52
|
+
"pack:check": "npm pack --dry-run"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "2.4.0",
|
|
56
|
+
"tsx": "4.0.0",
|
|
57
|
+
"typescript": "5.9.3"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@earendil-works/pi-ai": "*",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
62
|
+
"typebox": "*"
|
|
63
|
+
},
|
|
64
|
+
"overrides": {
|
|
65
|
+
"rimraf": "6.1.3",
|
|
66
|
+
"glob": "13.0.3"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { spawnSync } = require("node:child_process");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
function hasQmd() {
|
|
5
|
+
const result = spawnSync("qmd", ["status"], {
|
|
6
|
+
stdio: "ignore",
|
|
7
|
+
shell: process.platform === "win32",
|
|
8
|
+
});
|
|
9
|
+
return result.status === 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function memoryDir() {
|
|
13
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
14
|
+
return path.join(home, ".pi", "agent", "memory");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function configureGitHooks() {
|
|
18
|
+
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
19
|
+
stdio: "ignore",
|
|
20
|
+
shell: process.platform === "win32",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (result.status !== 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spawnSync("git", ["config", "core.hooksPath", ".githooks"], {
|
|
28
|
+
stdio: "ignore",
|
|
29
|
+
shell: process.platform === "win32",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
configureGitHooks();
|
|
34
|
+
|
|
35
|
+
if (!hasQmd()) {
|
|
36
|
+
const dir = memoryDir();
|
|
37
|
+
console.log("\npi-memory: qmd not found (required for `memory_search`).\n");
|
|
38
|
+
console.log("Install qmd (requires Bun):");
|
|
39
|
+
console.log(" bun install -g https://github.com/tobi/qmd");
|
|
40
|
+
console.log(" # ensure ~/.bun/bin is in your PATH\n");
|
|
41
|
+
console.log("Then set up the collection (one-time):");
|
|
42
|
+
console.log(` qmd collection add ${dir} --name pi-memory`);
|
|
43
|
+
console.log(" qmd embed\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { JsonlAuditLog } from "./curator-core/audit.ts";
|
|
3
|
+
import { runMemoryCuratorOnce } from "./curator-core/curate.ts";
|
|
4
|
+
import { FileMemoryStore } from "./curator-store/file-store.ts";
|
|
5
|
+
import { disableCuratorService, enableCuratorService, getCuratorServiceStatus, resolveMemoryDir } from "./service-controller.ts";
|
|
6
|
+
|
|
7
|
+
function cliPath(): string {
|
|
8
|
+
return new URL(import.meta.url).pathname;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readOption(args: string[], name: string): string | undefined {
|
|
12
|
+
const index = args.indexOf(name);
|
|
13
|
+
if (index < 0) return undefined;
|
|
14
|
+
return args[index + 1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasFlag(args: string[], name: string): boolean {
|
|
18
|
+
return args.includes(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function usage(): string {
|
|
22
|
+
return [
|
|
23
|
+
"Usage:",
|
|
24
|
+
" jhp-pi-memory-curator run-once [--memory-dir <path>] [--reason <text>] [--dry-run] [--json]",
|
|
25
|
+
" jhp-pi-memory-curator enable [--memory-dir <path>] [--schedule HH:MM]",
|
|
26
|
+
" jhp-pi-memory-curator disable [--memory-dir <path>]",
|
|
27
|
+
" jhp-pi-memory-curator status [--memory-dir <path>]",
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main(): Promise<void> {
|
|
32
|
+
const [command, ...args] = process.argv.slice(2);
|
|
33
|
+
const memoryDir = readOption(args, "--memory-dir") || resolveMemoryDir();
|
|
34
|
+
|
|
35
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
36
|
+
console.log(usage());
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (command === "run-once") {
|
|
41
|
+
const result = await runMemoryCuratorOnce({
|
|
42
|
+
memoryStore: new FileMemoryStore(memoryDir),
|
|
43
|
+
auditLog: new JsonlAuditLog(memoryDir),
|
|
44
|
+
reason: readOption(args, "--reason") || "cli",
|
|
45
|
+
dryRun: hasFlag(args, "--dry-run"),
|
|
46
|
+
});
|
|
47
|
+
if (hasFlag(args, "--json")) console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
else console.log(result.summary);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === "enable" || command === "install-service") {
|
|
53
|
+
const result = enableCuratorService({ memoryDir, cliPath: cliPath(), schedule: readOption(args, "--schedule") });
|
|
54
|
+
console.log(result.message);
|
|
55
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (command === "disable" || command === "uninstall-service") {
|
|
60
|
+
const result = disableCuratorService({ memoryDir, cliPath: cliPath() });
|
|
61
|
+
console.log(result.message);
|
|
62
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (command === "status") {
|
|
67
|
+
const result = getCuratorServiceStatus({ memoryDir, cliPath: cliPath() });
|
|
68
|
+
console.log(result.message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.error(`Unknown command: ${command}\n\n${usage()}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
main().catch((error: unknown) => {
|
|
77
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { MemoryPatch } from "./patch.ts";
|
|
4
|
+
import type { MemoryTarget } from "../curator-store/types.ts";
|
|
5
|
+
|
|
6
|
+
export interface AuditLog {
|
|
7
|
+
write(entry: AuditEntry): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type AuditEntry = {
|
|
11
|
+
runId: string;
|
|
12
|
+
target: MemoryTarget;
|
|
13
|
+
operation: MemoryPatch["operation"];
|
|
14
|
+
old?: string;
|
|
15
|
+
new?: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
reviewedAt: string;
|
|
18
|
+
actor: "pi-memory-curator";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class JsonlAuditLog implements AuditLog {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
|
|
24
|
+
constructor(memoryDir: string, auditPath?: string) {
|
|
25
|
+
this.path = auditPath || join(memoryDir, "audit", "curator.jsonl");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async write(entry: AuditEntry): Promise<void> {
|
|
29
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
30
|
+
writeFileSync(this.path, `${JSON.stringify(entry)}\n`, { encoding: "utf-8", flag: "a" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function auditEntryFromPatch(runId: string, patch: MemoryPatch, reviewedAt: string): AuditEntry {
|
|
35
|
+
return {
|
|
36
|
+
runId,
|
|
37
|
+
target: patch.target,
|
|
38
|
+
operation: patch.operation,
|
|
39
|
+
old: patch.oldText,
|
|
40
|
+
new: patch.newText,
|
|
41
|
+
reason: patch.reason,
|
|
42
|
+
reviewedAt,
|
|
43
|
+
actor: "pi-memory-curator",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { auditEntryFromPatch, type AuditLog } from "./audit.ts";
|
|
2
|
+
import type { CuratorPolicy } from "./policy.ts";
|
|
3
|
+
import { createLifecyclePatches } from "./policy.ts";
|
|
4
|
+
import type { MemoryPatch } from "./patch.ts";
|
|
5
|
+
import { validateMemoryPatch } from "./patch.ts";
|
|
6
|
+
import { MEMORY_TARGETS, type MemoryStore, type MemoryTarget } from "../curator-store/types.ts";
|
|
7
|
+
|
|
8
|
+
export type RunMemoryCuratorOnceOptions = {
|
|
9
|
+
memoryStore: MemoryStore;
|
|
10
|
+
auditLog?: AuditLog;
|
|
11
|
+
policy?: CuratorPolicy;
|
|
12
|
+
now?: () => Date;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CuratorRunResult = {
|
|
18
|
+
runId: string;
|
|
19
|
+
changed: number;
|
|
20
|
+
reviewed: number;
|
|
21
|
+
deduped: number;
|
|
22
|
+
patches: MemoryPatch[];
|
|
23
|
+
summary: string;
|
|
24
|
+
dryRun: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function runMemoryCuratorOnce(options: RunMemoryCuratorOnceOptions): Promise<CuratorRunResult> {
|
|
28
|
+
const now = options.now?.() || new Date();
|
|
29
|
+
const runId = now.toISOString();
|
|
30
|
+
const entriesByTarget = new Map<MemoryTarget, string[]>();
|
|
31
|
+
const patches: MemoryPatch[] = [];
|
|
32
|
+
let deduped = 0;
|
|
33
|
+
|
|
34
|
+
for (const target of MEMORY_TARGETS) {
|
|
35
|
+
if (target === "review") continue;
|
|
36
|
+
const entries = await options.memoryStore.readEntries(target);
|
|
37
|
+
entriesByTarget.set(target, entries);
|
|
38
|
+
const dedupedEntries = dedupeEntries(entries);
|
|
39
|
+
if (dedupedEntries.length !== entries.length) {
|
|
40
|
+
deduped += entries.length - dedupedEntries.length;
|
|
41
|
+
patches.push({ target, operation: "dedupe", reason: "duplicate exact entries", confidence: "high" });
|
|
42
|
+
}
|
|
43
|
+
patches.push(...createLifecyclePatches(target, dedupedEntries, now, options.policy));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const errors = patches.flatMap((patch) => validateMemoryPatch(patch).map((error) => `${patch.operation}: ${error}`));
|
|
47
|
+
if (errors.length) throw new Error(`Invalid curator patch: ${errors.join("; ")}`);
|
|
48
|
+
|
|
49
|
+
if (!options.dryRun && patches.length > 0) {
|
|
50
|
+
await applyPatches(options.memoryStore, entriesByTarget, patches);
|
|
51
|
+
const reviewedAt = now.toISOString();
|
|
52
|
+
for (const patch of patches) await options.auditLog?.write(auditEntryFromPatch(runId, patch, reviewedAt));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const changed = patches.filter((patch) => patch.operation === "replace").length + deduped;
|
|
56
|
+
const reviewed = patches.filter((patch) => patch.operation === "append_review").length;
|
|
57
|
+
const summary = `Curated memory: ${changed} updated, ${reviewed} review item(s), ${runId}`;
|
|
58
|
+
if (!options.dryRun && patches.length > 0) await options.memoryStore.saveState({ lastRunAt: runId, lastRunSummary: summary });
|
|
59
|
+
return { runId, changed, reviewed, deduped, patches, summary, dryRun: Boolean(options.dryRun) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function applyPatches(memoryStore: MemoryStore, entriesByTarget: Map<MemoryTarget, string[]>, patches: MemoryPatch[]): Promise<void> {
|
|
63
|
+
for (const patch of patches) {
|
|
64
|
+
if (patch.operation === "append_review") {
|
|
65
|
+
const storedReviewEntries = entriesByTarget.get("review");
|
|
66
|
+
const entries = storedReviewEntries || await memoryStore.readEntries("review");
|
|
67
|
+
if (patch.newText && !entries.includes(patch.newText)) entries.push(patch.newText);
|
|
68
|
+
entriesByTarget.set("review", entries);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const storedEntries = entriesByTarget.get(patch.target);
|
|
73
|
+
const entries = storedEntries || await memoryStore.readEntries(patch.target);
|
|
74
|
+
if (patch.operation === "replace") {
|
|
75
|
+
const index = entries.findIndex((entry) => entry === patch.oldText);
|
|
76
|
+
if (index < 0) throw new Error(`Patch target entry not found in ${patch.target}`);
|
|
77
|
+
entries[index] = patch.newText || "";
|
|
78
|
+
} else if (patch.operation === "dedupe") {
|
|
79
|
+
entriesByTarget.set(patch.target, dedupeEntries(entries));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
entriesByTarget.set(patch.target, entries);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const [target, entries] of entriesByTarget) await memoryStore.writeEntries(target, dedupeEntries(entries));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function dedupeEntries(entries: string[]): string[] {
|
|
89
|
+
return [...new Set(entries.map((entry) => entry.trim()).filter(Boolean))];
|
|
90
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type MemoryMetadata = Record<string, string>;
|
|
2
|
+
|
|
3
|
+
export type ParsedEntry = {
|
|
4
|
+
metadata: MemoryMetadata;
|
|
5
|
+
body: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
hasMetadata: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const ORDERED_METADATA_KEYS = ["type", "provider", "status", "date", "reset", "month", "used", "limit", "ttlDays"];
|
|
11
|
+
|
|
12
|
+
export function parseMetadata(line: string): MemoryMetadata | undefined {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return undefined;
|
|
15
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
16
|
+
if (!inner) return {};
|
|
17
|
+
const metadata: MemoryMetadata = {};
|
|
18
|
+
for (const part of inner.split(/\s+/)) {
|
|
19
|
+
const index = part.indexOf(":");
|
|
20
|
+
if (index <= 0) continue;
|
|
21
|
+
const key = part.slice(0, index).trim();
|
|
22
|
+
const value = part.slice(index + 1).trim();
|
|
23
|
+
if (key && value) metadata[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return metadata;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function serializeMetadata(metadata: MemoryMetadata): string {
|
|
29
|
+
const keys = [
|
|
30
|
+
...ORDERED_METADATA_KEYS.filter((key) => metadata[key] !== undefined),
|
|
31
|
+
...Object.keys(metadata).filter((key) => !ORDERED_METADATA_KEYS.includes(key)).sort(),
|
|
32
|
+
];
|
|
33
|
+
return `[${keys.map((key) => `${key}:${metadata[key]}`).join(" ")}]`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseEntry(raw: string): ParsedEntry {
|
|
37
|
+
const trimmed = raw.trim();
|
|
38
|
+
const lines = trimmed.split("\n");
|
|
39
|
+
const metadata = parseMetadata(lines[0]);
|
|
40
|
+
if (metadata === undefined) return { metadata: {}, body: trimmed, raw: trimmed, hasMetadata: false };
|
|
41
|
+
return { metadata, body: lines.slice(1).join("\n").trim(), raw: trimmed, hasMetadata: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderEntry(entry: ParsedEntry): string {
|
|
45
|
+
if (!entry.hasMetadata || Object.keys(entry.metadata).length === 0) return entry.body.trim();
|
|
46
|
+
return `${serializeMetadata(entry.metadata)}\n${entry.body.trim()}`.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function todayUtc(now: Date): string {
|
|
50
|
+
return now.toISOString().slice(0, 10);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function currentMonth(now: Date): string {
|
|
54
|
+
return now.toISOString().slice(0, 7);
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MEMORY_TARGETS, type MemoryTarget } from "../curator-store/types.ts";
|
|
2
|
+
|
|
3
|
+
export const MEMORY_PATCH_OPERATIONS = ["replace", "append_review", "dedupe"] as const;
|
|
4
|
+
|
|
5
|
+
export type MemoryPatchOperation = (typeof MEMORY_PATCH_OPERATIONS)[number];
|
|
6
|
+
|
|
7
|
+
export interface MemoryPatch {
|
|
8
|
+
target: MemoryTarget;
|
|
9
|
+
operation: MemoryPatchOperation;
|
|
10
|
+
oldText?: string;
|
|
11
|
+
newText?: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
confidence: "high" | "medium" | "low";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateMemoryPatch(patch: MemoryPatch): string[] {
|
|
17
|
+
const errors: string[] = [];
|
|
18
|
+
if (!MEMORY_TARGETS.includes(patch.target)) errors.push(`invalid target '${patch.target}'`);
|
|
19
|
+
if (!MEMORY_PATCH_OPERATIONS.includes(patch.operation)) errors.push(`invalid operation '${patch.operation}'`);
|
|
20
|
+
if (!patch.reason.trim()) errors.push("reason is required");
|
|
21
|
+
if (patch.operation === "replace" && (!patch.oldText?.trim() || !patch.newText?.trim())) errors.push("replace requires oldText and newText");
|
|
22
|
+
if (patch.operation === "append_review" && !patch.newText?.trim()) errors.push("append_review requires newText");
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { currentMonth, parseEntry, renderEntry, todayUtc } from "./metadata.ts";
|
|
2
|
+
import type { MemoryPatch } from "./patch.ts";
|
|
3
|
+
import type { MemoryTarget } from "../curator-store/types.ts";
|
|
4
|
+
|
|
5
|
+
export type CuratorPolicy = {
|
|
6
|
+
markTodayEvents?: boolean;
|
|
7
|
+
reviewExpiredTemporary?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CURATOR_POLICY: Required<CuratorPolicy> = {
|
|
11
|
+
markTodayEvents: true,
|
|
12
|
+
reviewExpiredTemporary: true,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createLifecyclePatches(target: MemoryTarget, rawEntries: string[], now: Date, policy: CuratorPolicy = {}): MemoryPatch[] {
|
|
16
|
+
const resolvedPolicy = { ...DEFAULT_CURATOR_POLICY, ...policy };
|
|
17
|
+
const patches: MemoryPatch[] = [];
|
|
18
|
+
const today = todayUtc(now);
|
|
19
|
+
const month = currentMonth(now);
|
|
20
|
+
|
|
21
|
+
for (const raw of rawEntries) {
|
|
22
|
+
const parsed = parseEntry(raw);
|
|
23
|
+
if (!parsed.hasMetadata) continue;
|
|
24
|
+
|
|
25
|
+
if (parsed.metadata.type === "event") {
|
|
26
|
+
const date = parsed.metadata.date;
|
|
27
|
+
const status = parsed.metadata.status;
|
|
28
|
+
if (date && date < today && (status === "planned" || status === "today")) {
|
|
29
|
+
const next = parseEntry(raw);
|
|
30
|
+
next.metadata.status = "past";
|
|
31
|
+
next.body = rewritePastEventBody(next.body);
|
|
32
|
+
patches.push({ target, operation: "replace", oldText: raw, newText: renderEntry(next), reason: "event date passed", confidence: "high" });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (resolvedPolicy.markTodayEvents && date === today && status === "planned") {
|
|
36
|
+
const next = parseEntry(raw);
|
|
37
|
+
next.metadata.status = "today";
|
|
38
|
+
patches.push({ target, operation: "replace", oldText: raw, newText: renderEntry(next), reason: "event date is today", confidence: "high" });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (parsed.metadata.type === "temporary" && resolvedPolicy.reviewExpiredTemporary && parsed.metadata.date && parsed.metadata.date < today) {
|
|
44
|
+
patches.push({
|
|
45
|
+
target: "review",
|
|
46
|
+
operation: "append_review",
|
|
47
|
+
newText: `[type:review source:${target} reason:expired-temporary]\nTemporary memory may be stale: ${raw}`,
|
|
48
|
+
reason: "temporary memory date passed",
|
|
49
|
+
confidence: "high",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (parsed.metadata.type === "quota") {
|
|
54
|
+
const reset = parsed.metadata.reset;
|
|
55
|
+
const resetReached = reset ? Date.parse(reset) <= now.getTime() : false;
|
|
56
|
+
if ((parsed.metadata.month && parsed.metadata.month !== month) || (parsed.metadata.status === "exhausted" && resetReached)) {
|
|
57
|
+
const next = parseEntry(raw);
|
|
58
|
+
next.metadata.month = month;
|
|
59
|
+
next.metadata.status = "active";
|
|
60
|
+
next.metadata.used = "0";
|
|
61
|
+
const provider = next.metadata.provider || "provider";
|
|
62
|
+
next.body = `${provider} search quota is active for ${month}.`;
|
|
63
|
+
patches.push({ target, operation: "replace", oldText: raw, newText: renderEntry(next), reason: "quota reset date reached", confidence: "high" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return patches;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function rewritePastEventBody(body: string): string {
|
|
72
|
+
const rewritten = body
|
|
73
|
+
.replace(/\bUser is planning\b/i, "User had")
|
|
74
|
+
.replace(/\bUser plans\b/i, "User had planned")
|
|
75
|
+
.replace(/\bUser planned\b/i, "User had planned");
|
|
76
|
+
return /completion status unknown/i.test(rewritten) ? rewritten : `${rewritten} Completion status unknown.`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { ENTRY_DELIMITER, type CuratorState, type MemoryStore, type MemoryTarget } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MEMORY_DIR = join(process.env.HOME || process.cwd(), ".pi", "agent", "memory");
|
|
6
|
+
|
|
7
|
+
export class FileMemoryStore implements MemoryStore {
|
|
8
|
+
readonly memoryDir: string;
|
|
9
|
+
|
|
10
|
+
constructor(memoryDir = DEFAULT_MEMORY_DIR) {
|
|
11
|
+
this.memoryDir = memoryDir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async readEntries(target: MemoryTarget): Promise<string[]> {
|
|
15
|
+
const path = this.pathForTarget(target);
|
|
16
|
+
if (!existsSync(path)) return [];
|
|
17
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
18
|
+
if (!raw) return [];
|
|
19
|
+
return raw.split(ENTRY_DELIMITER).map((entry) => entry.trim()).filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async writeEntries(target: MemoryTarget, entries: string[]): Promise<void> {
|
|
23
|
+
this.atomicWrite(this.pathForTarget(target), entries.map((entry) => entry.trim()).filter(Boolean).join(ENTRY_DELIMITER));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async loadState(): Promise<CuratorState> {
|
|
27
|
+
const path = join(this.memoryDir, ".curator-state.json");
|
|
28
|
+
if (!existsSync(path)) return {};
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(readFileSync(path, "utf-8")) as CuratorState;
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async saveState(state: CuratorState): Promise<void> {
|
|
37
|
+
this.atomicWrite(join(this.memoryDir, ".curator-state.json"), `${JSON.stringify(state, null, 2)}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pathForTarget(target: MemoryTarget): string {
|
|
41
|
+
const name = target === "memory" ? "MEMORY" : target === "user" ? "USER" : target === "state" ? "STATE" : "REVIEW";
|
|
42
|
+
return join(this.memoryDir, `${name}.md`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private atomicWrite(path: string, content: string): void {
|
|
46
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
47
|
+
const tmpPath = join(dirname(path), `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
48
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
49
|
+
renameSync(tmpPath, path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const ENTRY_DELIMITER = "\n§\n";
|
|
2
|
+
export const MEMORY_TARGETS = ["memory", "user", "state", "review"] as const;
|
|
3
|
+
|
|
4
|
+
export type MemoryTarget = (typeof MEMORY_TARGETS)[number];
|
|
5
|
+
|
|
6
|
+
export type CuratorState = {
|
|
7
|
+
lastRunAt?: string;
|
|
8
|
+
lastRunSummary?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface MemoryStore {
|
|
12
|
+
readEntries(target: MemoryTarget): Promise<string[]>;
|
|
13
|
+
writeEntries(target: MemoryTarget, entries: string[]): Promise<void>;
|
|
14
|
+
loadState(): Promise<CuratorState>;
|
|
15
|
+
saveState(state: CuratorState): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeMemoryTarget(value: string | undefined, fallback: MemoryTarget = "memory"): MemoryTarget {
|
|
19
|
+
const normalized = (value || fallback).trim().toLowerCase();
|
|
20
|
+
return MEMORY_TARGETS.includes(normalized as MemoryTarget) ? (normalized as MemoryTarget) : fallback;
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { auditEntryFromPatch, JsonlAuditLog, type AuditEntry, type AuditLog } from "./curator-core/audit.ts";
|
|
2
|
+
export { runMemoryCuratorOnce, type CuratorRunResult, type RunMemoryCuratorOnceOptions } from "./curator-core/curate.ts";
|
|
3
|
+
export { parseEntry, parseMetadata, renderEntry, serializeMetadata, todayUtc } from "./curator-core/metadata.ts";
|
|
4
|
+
export {
|
|
5
|
+
createReviewCandidateId,
|
|
6
|
+
normalizeCandidateSignature,
|
|
7
|
+
parseReviewCandidate,
|
|
8
|
+
renderReviewCandidate,
|
|
9
|
+
upsertReviewCandidate,
|
|
10
|
+
validateReviewCandidateInput,
|
|
11
|
+
type ParsedReviewCandidate,
|
|
12
|
+
type ReviewCandidateInput,
|
|
13
|
+
} from "./learning/candidates.ts";
|
|
14
|
+
export {
|
|
15
|
+
applyReviewLifecycle,
|
|
16
|
+
approveMemoryPromotion,
|
|
17
|
+
proposeMemoryPromotions,
|
|
18
|
+
rejectReviewItem,
|
|
19
|
+
type MemoryApprovalResult,
|
|
20
|
+
type MemoryPromotionResult,
|
|
21
|
+
type ReviewLifecycleResult,
|
|
22
|
+
} from "./learning/memory.ts";
|
|
23
|
+
export {
|
|
24
|
+
approveSkillDraft,
|
|
25
|
+
listSkillDraftProposals,
|
|
26
|
+
proposeSkillDrafts,
|
|
27
|
+
type SkillApprovalResult,
|
|
28
|
+
type SkillProposal,
|
|
29
|
+
type SkillProposalResult,
|
|
30
|
+
} from "./learning/skills.ts";
|
|
31
|
+
export { DEFAULT_CURATOR_POLICY, createLifecyclePatches, type CuratorPolicy } from "./curator-core/policy.ts";
|
|
32
|
+
export { validateMemoryPatch, type MemoryPatch } from "./curator-core/patch.ts";
|
|
33
|
+
export { DEFAULT_MEMORY_DIR, FileMemoryStore } from "./curator-store/file-store.ts";
|
|
34
|
+
export { ENTRY_DELIMITER, MEMORY_TARGETS, normalizeMemoryTarget, type CuratorState, type MemoryStore, type MemoryTarget } from "./curator-store/types.ts";
|
|
35
|
+
export { disableCuratorService, enableCuratorService, getCuratorServiceStatus, resolveMemoryDir, type CuratorServiceBackend, type CuratorServiceResult, type CuratorServiceState } from "./service-controller.ts";
|