@henryqw/pi-memory 0.1.0 → 0.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/extensions/memory.ts +26 -1
- package/package.json +2 -1
- package/src/store.ts +11 -5
package/extensions/memory.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readdir, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { join, sep } from "node:path";
|
|
3
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
4
|
import { getAgentDir, withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
5
6
|
import { lock } from "proper-lockfile";
|
|
6
7
|
import { Type } from "typebox";
|
|
7
8
|
import { loadMemoryConfig, type MemoryConfig } from "../src/config.ts";
|
|
@@ -14,6 +15,7 @@ const BACKUP_DIR = () => join(getAgentDir(), "memory-backups");
|
|
|
14
15
|
// Defense-in-depth against snapshot frame spoofing by poisoned on-disk entries.
|
|
15
16
|
const FRAME_TOKEN_LINE = /^\s*(?:═{3,}|MEMORY \(your personal notes|USER PROFILE \(who the user is)/;
|
|
16
17
|
const FRAME_TOKEN_REPLACEMENT = "[filtered frame token]";
|
|
18
|
+
const DISPLAY_CONTROL_CHARACTER = /[\p{Cc}\p{Cf}]/gu;
|
|
17
19
|
// @henryqw/pi-herdr-btw does not export internal/core.ts from its package root.
|
|
18
20
|
const BTW_CHILD_PAYLOAD_ARG = "--pi-herdr-btw-payload";
|
|
19
21
|
const CONSOLIDATION_FAILURE = /(?:exceed|over) the limit|would put memory|no entry matched|[Mm]ultiple entries matched|matched multiple distinct/i;
|
|
@@ -41,6 +43,16 @@ function sanitizeName(name: string): string {
|
|
|
41
43
|
return name.replace(/[\p{C}]/gu, "").slice(0, 120);
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function escapeDisplayControls(text: string): string {
|
|
47
|
+
return text.replace(DISPLAY_CONTROL_CHARACTER, (character) => {
|
|
48
|
+
if (character === "\n") return character;
|
|
49
|
+
const codePoint = character.codePointAt(0)!;
|
|
50
|
+
return codePoint <= 0xffff
|
|
51
|
+
? `\\u${codePoint.toString(16).padStart(4, "0")}`
|
|
52
|
+
: `\\u{${codePoint.toString(16)}}`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
function renderBlock(target: Target, entries: string[], config: MemoryConfig, warnings: string[]): string {
|
|
45
57
|
if (!entries.length) return "";
|
|
46
58
|
const limit = target === "user" ? config.userCharLimit : config.memoryCharLimit;
|
|
@@ -231,13 +243,26 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
231
243
|
message: "Write saved. This update is complete — do not repeat it.",
|
|
232
244
|
}),
|
|
233
245
|
}],
|
|
234
|
-
details: {},
|
|
246
|
+
details: { status: result.message ?? "Write saved.", entries: result.writtenEntries ?? [] },
|
|
235
247
|
};
|
|
236
248
|
} finally {
|
|
237
249
|
await release();
|
|
238
250
|
}
|
|
239
251
|
});
|
|
240
252
|
},
|
|
253
|
+
|
|
254
|
+
renderResult(result, _options, theme, _context) {
|
|
255
|
+
const details = result.details as { status: string; entries: string[] } | undefined;
|
|
256
|
+
if (!details) {
|
|
257
|
+
const content = result.content[0];
|
|
258
|
+
return new Text(content?.type === "text" ? content.text : "", 0, 0);
|
|
259
|
+
}
|
|
260
|
+
let text = theme.fg("success", `✓ ${details.status}`);
|
|
261
|
+
for (const entry of details.entries) {
|
|
262
|
+
text += `\n ${theme.fg("accent", escapeDisplayControls(entry).replaceAll("\n", "\n "))}`;
|
|
263
|
+
}
|
|
264
|
+
return new Text(text, 0, 0);
|
|
265
|
+
},
|
|
241
266
|
});
|
|
242
267
|
|
|
243
268
|
pi.on("before_agent_start", (event) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Auto-managed markdown memory for Pi: capped MEMORY.md/USER.md entry stores with frozen session snapshots.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@earendil-works/pi-ai": "^0.84.2",
|
|
31
31
|
"@earendil-works/pi-coding-agent": "^0.84.2",
|
|
32
|
+
"@earendil-works/pi-tui": "^0.84.2",
|
|
32
33
|
"typebox": "^1.3.15"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
package/src/store.ts
CHANGED
|
@@ -45,6 +45,7 @@ type Result = {
|
|
|
45
45
|
error?: string;
|
|
46
46
|
usage?: string;
|
|
47
47
|
entryCount?: number;
|
|
48
|
+
writtenEntries?: string[];
|
|
48
49
|
currentEntries?: string[];
|
|
49
50
|
matches?: string[];
|
|
50
51
|
done?: boolean;
|
|
@@ -127,7 +128,7 @@ export class MemoryStore {
|
|
|
127
128
|
return `${pct}% — ${current.toLocaleString()}/${limit.toLocaleString()} chars`;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
private successResponse(target: Target, message?: string): Result {
|
|
131
|
+
private successResponse(target: Target, message?: string, writtenEntries: string[] = []): Result {
|
|
131
132
|
this.resetOnSuccess();
|
|
132
133
|
return {
|
|
133
134
|
success: true,
|
|
@@ -136,6 +137,7 @@ export class MemoryStore {
|
|
|
136
137
|
message,
|
|
137
138
|
usage: this.usage(target),
|
|
138
139
|
entryCount: this.entries.get(target)!.length,
|
|
140
|
+
writtenEntries,
|
|
139
141
|
note: "Write saved. This update is complete — do not repeat it.",
|
|
140
142
|
};
|
|
141
143
|
}
|
|
@@ -392,7 +394,7 @@ export class MemoryStore {
|
|
|
392
394
|
const entries = this.entries.get(target)!;
|
|
393
395
|
|
|
394
396
|
if (entries.includes(text)) {
|
|
395
|
-
return this.successResponse(target, "Entry already exists (no duplicate added).");
|
|
397
|
+
return this.successResponse(target, "Entry already exists (no duplicate added).", [text]);
|
|
396
398
|
}
|
|
397
399
|
|
|
398
400
|
const newTotal = [...entries, text].join(ENTRY_DELIMITER).length;
|
|
@@ -408,7 +410,7 @@ export class MemoryStore {
|
|
|
408
410
|
|
|
409
411
|
entries.push(text);
|
|
410
412
|
await this.persist(target);
|
|
411
|
-
return this.successResponse(target, "Entry added.");
|
|
413
|
+
return this.successResponse(target, "Entry added.", [text]);
|
|
412
414
|
}
|
|
413
415
|
|
|
414
416
|
private unreadableAbort(target: Target): Result {
|
|
@@ -467,7 +469,7 @@ export class MemoryStore {
|
|
|
467
469
|
|
|
468
470
|
this.entries.set(target, deduped);
|
|
469
471
|
await this.persist(target);
|
|
470
|
-
return this.successResponse(target, "Entry replaced.");
|
|
472
|
+
return this.successResponse(target, "Entry replaced.", [text]);
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
async remove(target: Target, oldText: string): Promise<Result> {
|
|
@@ -557,6 +559,10 @@ export class MemoryStore {
|
|
|
557
559
|
|
|
558
560
|
this.entries.set(target, working);
|
|
559
561
|
await this.persist(target);
|
|
560
|
-
|
|
562
|
+
const writtenEntries = [...new Set(operations.flatMap((operation) => {
|
|
563
|
+
const content = normalize(operation.content ?? operation.new_text ?? "");
|
|
564
|
+
return (operation.action === "add" || operation.action === "replace") && working.includes(content) ? [content] : [];
|
|
565
|
+
}))];
|
|
566
|
+
return this.successResponse(target, `Applied ${operations.length} operation(s).`, writtenEntries);
|
|
561
567
|
}
|
|
562
568
|
}
|