@yandy0725/pi-memory 0.3.1 → 1.0.1
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 +110 -16
- package/README.zh.md +120 -28
- package/index.ts +134 -18
- package/package.json +3 -2
- package/src/agent-types.ts +44 -0
- package/src/config.ts +36 -3
- package/src/dream.ts +116 -101
- package/src/extract.ts +110 -0
- package/src/index-file.ts +66 -57
- package/src/inject.ts +233 -1
- package/src/memory-tool.ts +162 -55
- package/src/nudge.ts +9 -6
- package/src/paths.ts +3 -6
- package/src/session-search.ts +19 -4
- package/src/topic-file.ts +87 -64
package/src/session-search.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { SessionManager, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { basename } from "node:path";
|
|
3
|
+
import { SessionManager, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: session content parsing
|
|
5
6
|
function extractText(content: any): string {
|
|
6
7
|
if (typeof content === "string") return content;
|
|
7
8
|
if (Array.isArray(content)) {
|
|
8
|
-
return
|
|
9
|
+
return (
|
|
10
|
+
content
|
|
11
|
+
// biome-ignore lint/suspicious/noExplicitAny: content block filter
|
|
12
|
+
.filter((b: any) => b.type === "text")
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: content block map
|
|
14
|
+
.map((b: any) => b.text)
|
|
15
|
+
.join(" ")
|
|
16
|
+
);
|
|
9
17
|
}
|
|
10
18
|
return "";
|
|
11
19
|
}
|
|
@@ -24,8 +32,13 @@ export async function searchSessions(
|
|
|
24
32
|
// gather message texts in order for context windows
|
|
25
33
|
const msgs: { text: string; idx: number }[] = [];
|
|
26
34
|
for (const [i, line] of lines.entries()) {
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: JSON parse entry
|
|
27
36
|
let entry: any;
|
|
28
|
-
try {
|
|
37
|
+
try {
|
|
38
|
+
entry = JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
29
42
|
if (entry.type === "message" && entry.message) {
|
|
30
43
|
const t = extractText(entry.message.content);
|
|
31
44
|
if (t) msgs.push({ text: t, idx: i });
|
|
@@ -37,7 +50,9 @@ export async function searchSessions(
|
|
|
37
50
|
.filter((x) => Math.abs(x.idx - m.idx) <= 50)
|
|
38
51
|
.map((x) => ` ${x.text.slice(0, 200)}`)
|
|
39
52
|
.join("\n");
|
|
40
|
-
hits.push(
|
|
53
|
+
hits.push(
|
|
54
|
+
`## Session ${basename(s.path)} (${s.modified.toISOString().slice(0, 10)})\n…matched: "${m.text.slice(0, 150)}"…\ncontext:\n${ctx}`,
|
|
55
|
+
);
|
|
41
56
|
if (hits.length >= config.maxMatches) break;
|
|
42
57
|
}
|
|
43
58
|
}
|
package/src/topic-file.ts
CHANGED
|
@@ -1,91 +1,114 @@
|
|
|
1
1
|
export interface TopicMeta {
|
|
2
|
-
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
type: string;
|
|
5
|
+
updated: string;
|
|
3
6
|
}
|
|
4
7
|
|
|
8
|
+
export const ALLOWED_TYPES = ["user", "feedback", "project", "reference"] as const;
|
|
9
|
+
|
|
5
10
|
export function buildFrontmatter(meta: TopicMeta): string {
|
|
6
|
-
|
|
11
|
+
return [
|
|
12
|
+
"---",
|
|
13
|
+
`name: ${meta.name}`,
|
|
14
|
+
`description: ${meta.description}`,
|
|
15
|
+
`type: ${meta.type}`,
|
|
16
|
+
`updated: ${meta.updated}`,
|
|
17
|
+
"---",
|
|
18
|
+
"",
|
|
19
|
+
"",
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseFrontmatter(raw: string): TopicMeta | null {
|
|
24
|
+
if (!raw.startsWith("---\n")) return null;
|
|
25
|
+
const endIdx = raw.indexOf("\n---\n", 4);
|
|
26
|
+
if (endIdx === -1) return null;
|
|
27
|
+
const block = raw.slice(4, endIdx);
|
|
28
|
+
const meta: Record<string, string> = {};
|
|
29
|
+
for (const line of block.split("\n")) {
|
|
30
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
31
|
+
if (m) meta[m[1]] = m[2].trim();
|
|
32
|
+
}
|
|
33
|
+
if (!meta.name || !meta.description || !meta.type || !meta.updated) return null;
|
|
34
|
+
if (!(ALLOWED_TYPES as readonly string[]).includes(meta.type)) return null;
|
|
35
|
+
return meta as unknown as TopicMeta;
|
|
7
36
|
}
|
|
8
37
|
|
|
9
|
-
export function appendContent(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
): string {
|
|
14
|
-
const section = `## ${entryTitle}\n\n${content}`;
|
|
15
|
-
if (!existing || existing.trim() === "") return section;
|
|
16
|
-
return `${existing.trimEnd()}\n\n${section}\n`;
|
|
38
|
+
export function appendContent(existing: string | null, entryTitle: string, content: string): string {
|
|
39
|
+
const section = `## ${entryTitle}\n\n${content}`;
|
|
40
|
+
if (!existing || existing.trim() === "") return section;
|
|
41
|
+
return `${existing.trimEnd()}\n\n${section}\n`;
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
export function updateFrontmatterDate(raw: string, date: string): string {
|
|
20
|
-
|
|
45
|
+
return raw.replace(/^(---\n(?:.*\n)*?)updated: .+(\n---)/m, `$1updated: ${date}$2`);
|
|
21
46
|
}
|
|
22
47
|
|
|
23
48
|
export function removeEntrySection(raw: string, title: string): string {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
: raw.indexOf(marker);
|
|
29
|
-
if (startIdx === -1) throw new Error(`Entry "${title}" not found`);
|
|
49
|
+
const marker = `## ${title}`;
|
|
50
|
+
// find start of this entry block
|
|
51
|
+
const startIdx = raw.indexOf(`\n${marker}\n`) !== -1 ? raw.indexOf(`\n${marker}\n`) : raw.indexOf(marker);
|
|
52
|
+
if (startIdx === -1) throw new Error(`Entry "${title}" not found`);
|
|
30
53
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
// find start of next ## or EOF
|
|
55
|
+
const afterHeader = raw.indexOf("\n", startIdx + marker.length);
|
|
56
|
+
const nextH2 = raw.indexOf("\n## ", afterHeader + 1);
|
|
57
|
+
const endIdx = nextH2 === -1 ? raw.length : nextH2;
|
|
35
58
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
// remove including preceding blank lines
|
|
60
|
+
let cutStart = startIdx;
|
|
61
|
+
while (cutStart > 0 && raw[cutStart - 1] === "\n") cutStart--;
|
|
62
|
+
// also strip one more \n if present (the blank line separator)
|
|
63
|
+
if (cutStart > 0 && raw[cutStart - 1] === "\n") cutStart--;
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
let result = raw.slice(0, cutStart) + raw.slice(endIdx);
|
|
66
|
+
// ensure exactly one trailing newline
|
|
67
|
+
result = result.replace(/\n{3,}$/, "\n\n").replace(/\n{2,}$/, "\n");
|
|
68
|
+
if (!result.endsWith("\n")) result += "\n";
|
|
69
|
+
return result;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
export function hasEntries(raw: string): boolean {
|
|
50
|
-
|
|
73
|
+
return /^## /m.test(raw);
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
export interface ParsedEntry {
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
title: string;
|
|
78
|
+
content: string;
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
export function parseEntries(raw: string): ParsedEntry[] {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
const entries: ParsedEntry[] = [];
|
|
83
|
+
const lines = raw.split("\n");
|
|
84
|
+
let currentTitle = "";
|
|
85
|
+
let currentContent: string[] = [];
|
|
86
|
+
let inEntry = false;
|
|
87
|
+
let inFrontmatter = false;
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (line === "---") {
|
|
91
|
+
inFrontmatter = !inFrontmatter;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (inFrontmatter) continue;
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
const h2 = line.match(/^## (.+)$/);
|
|
97
|
+
if (h2) {
|
|
98
|
+
if (inEntry) {
|
|
99
|
+
entries.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
100
|
+
}
|
|
101
|
+
currentTitle = h2[1];
|
|
102
|
+
currentContent = [];
|
|
103
|
+
inEntry = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (inEntry) {
|
|
107
|
+
currentContent.push(line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (inEntry) {
|
|
111
|
+
entries.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
112
|
+
}
|
|
113
|
+
return entries;
|
|
91
114
|
}
|