@yandy0725/pi-memory 0.3.1 → 1.0.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.
@@ -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 content.filter((b: any) => b.type === "text").map((b: any) => b.text).join(" ");
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 { entry = JSON.parse(line); } catch { continue; }
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(`## Session ${basename(s.path)} (${s.modified.toISOString().slice(0, 10)})\n…matched: "${m.text.slice(0, 150)}"…\ncontext:\n${ctx}`);
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
- updated: string;
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
- return `---\nupdated: ${meta.updated}\n---\n\n`;
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
- existing: string | null,
11
- entryTitle: string,
12
- content: string,
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
- return raw.replace(/^(---\n)updated: .+(\n---)/m, `$1updated: ${date}$2`);
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
- const marker = `## ${title}`;
25
- // find start of this entry block
26
- const startIdx = raw.indexOf(`\n${marker}\n`) !== -1
27
- ? raw.indexOf(`\n${marker}\n`)
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
- // find start of next ## or EOF
32
- const afterHeader = raw.indexOf("\n", startIdx + marker.length);
33
- const nextH2 = raw.indexOf("\n## ", afterHeader + 1);
34
- const endIdx = nextH2 === -1 ? raw.length : nextH2;
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
- // remove including preceding blank lines
37
- let cutStart = startIdx;
38
- while (cutStart > 0 && raw[cutStart - 1] === "\n") cutStart--;
39
- // also strip one more \n if present (the blank line separator)
40
- if (cutStart > 0 && raw[cutStart - 1] === "\n") cutStart--;
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
- let result = raw.slice(0, cutStart) + raw.slice(endIdx);
43
- // ensure exactly one trailing newline
44
- result = result.replace(/\n{3,}$/, "\n\n").replace(/\n{2,}$/, "\n");
45
- if (!result.endsWith("\n")) result += "\n";
46
- return result;
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
- return /^## /m.test(raw);
73
+ return /^## /m.test(raw);
51
74
  }
52
75
 
53
76
  export interface ParsedEntry {
54
- title: string;
55
- content: string;
77
+ title: string;
78
+ content: string;
56
79
  }
57
80
 
58
81
  export function parseEntries(raw: string): ParsedEntry[] {
59
- const entries: ParsedEntry[] = [];
60
- const lines = raw.split("\n");
61
- let currentTitle = "";
62
- let currentContent: string[] = [];
63
- let inEntry = false;
64
- let inFrontmatter = false;
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
- for (const line of lines) {
67
- if (line === "---") {
68
- inFrontmatter = !inFrontmatter;
69
- continue;
70
- }
71
- if (inFrontmatter) continue;
89
+ for (const line of lines) {
90
+ if (line === "---") {
91
+ inFrontmatter = !inFrontmatter;
92
+ continue;
93
+ }
94
+ if (inFrontmatter) continue;
72
95
 
73
- const h2 = line.match(/^## (.+)$/);
74
- if (h2) {
75
- if (inEntry) {
76
- entries.push({ title: currentTitle, content: currentContent.join("\n").trim() });
77
- }
78
- currentTitle = h2[1];
79
- currentContent = [];
80
- inEntry = true;
81
- continue;
82
- }
83
- if (inEntry) {
84
- currentContent.push(line);
85
- }
86
- }
87
- if (inEntry) {
88
- entries.push({ title: currentTitle, content: currentContent.join("\n").trim() });
89
- }
90
- return entries;
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
  }