pi-memoria 0.2.0 → 0.3.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/AGENTS.md +110 -0
- package/README.md +126 -166
- package/index.ts +301 -204
- package/package.json +29 -69
- package/src/aliases.ts +104 -0
- package/src/config.ts +22 -184
- package/src/database.ts +301 -0
- package/src/errors.ts +55 -0
- package/src/files.ts +29 -0
- package/src/hot-memory.ts +231 -0
- package/src/results.ts +264 -0
- package/src/sessions.ts +246 -843
- package/src/text.ts +48 -0
- package/skills/memoria/SKILL.md +0 -77
- package/skills/memoria-sessions/SKILL.md +0 -69
- package/src/commands.ts +0 -398
- package/src/frontmatter.ts +0 -318
- package/src/index-engine.ts +0 -1071
- package/src/recall.ts +0 -211
- package/src/rerank.ts +0 -85
- package/src/runtime.ts +0 -1469
- package/src/search.ts +0 -499
- package/src/similarity.ts +0 -374
- package/src/store.ts +0 -1724
- package/src/synonyms.ts +0 -105
- package/src/timeexpr.ts +0 -145
- package/src/tokenize.ts +0 -295
- package/src/tools.ts +0 -813
- package/src/types.ts +0 -541
- package/src/usage.ts +0 -188
- package/src/util.ts +0 -141
package/src/frontmatter.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal, dependency-free YAML-subset frontmatter parser + serializer.
|
|
3
|
-
*
|
|
4
|
-
* Supported: `key: scalar`, quoted strings, booleans, numbers, null, inline
|
|
5
|
-
* arrays `[a, b]`, block arrays (`- item`), one level of nested maps, and
|
|
6
|
-
* literal/folded block scalars (`|`, `|-`, `>`, `>-`).
|
|
7
|
-
*
|
|
8
|
-
* The parser is intentionally forgiving: unparseable lines are kept as raw text
|
|
9
|
-
* so that a malformed memory never blocks indexing.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface FrontmatterSplit {
|
|
13
|
-
frontmatter: Record<string, unknown>;
|
|
14
|
-
body: string;
|
|
15
|
-
/** True when the file started with a `---` fence. */
|
|
16
|
-
hasFrontmatter: boolean;
|
|
17
|
-
/** Raw frontmatter source, for round-tripping via `parseFrontmatter` reuse. */
|
|
18
|
-
raw: string;
|
|
19
|
-
/** Line offset where the body starts (used for line-number reporting). */
|
|
20
|
-
bodyLineOffset: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const FENCE = /^---[ \t]*$/;
|
|
24
|
-
|
|
25
|
-
function unquote(value: string): string {
|
|
26
|
-
const trimmed = value.trim();
|
|
27
|
-
if (trimmed.length >= 2) {
|
|
28
|
-
const first = trimmed[0];
|
|
29
|
-
const last = trimmed[trimmed.length - 1];
|
|
30
|
-
if (first === '"' && last === '"') {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(trimmed) as string;
|
|
33
|
-
} catch {
|
|
34
|
-
return trimmed.slice(1, -1);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (first === "'" && last === "'") {
|
|
38
|
-
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseInlineArray(value: string): unknown[] {
|
|
45
|
-
const inner = value.slice(1, -1).trim();
|
|
46
|
-
if (!inner) return [];
|
|
47
|
-
const items: string[] = [];
|
|
48
|
-
let current = "";
|
|
49
|
-
let quote: string | undefined;
|
|
50
|
-
for (let i = 0; i < inner.length; i += 1) {
|
|
51
|
-
const ch = inner[i];
|
|
52
|
-
if (quote) {
|
|
53
|
-
current += ch;
|
|
54
|
-
if (ch === quote && inner[i - 1] !== "\\") quote = undefined;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (ch === '"' || ch === "'") {
|
|
58
|
-
quote = ch;
|
|
59
|
-
current += ch;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (ch === ",") {
|
|
63
|
-
items.push(current);
|
|
64
|
-
current = "";
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
current += ch;
|
|
68
|
-
}
|
|
69
|
-
items.push(current);
|
|
70
|
-
return items.map((item) => parseScalar(item.trim()));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseScalar(value: string): unknown {
|
|
74
|
-
const trimmed = value.trim();
|
|
75
|
-
if (trimmed === "" || trimmed === "~" || trimmed === "null") return null;
|
|
76
|
-
if (trimmed === "true") return true;
|
|
77
|
-
if (trimmed === "false") return false;
|
|
78
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return parseInlineArray(trimmed);
|
|
79
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
80
|
-
const map: Record<string, unknown> = {};
|
|
81
|
-
for (const pair of splitTopLevel(trimmed.slice(1, -1))) {
|
|
82
|
-
const idx = pair.indexOf(":");
|
|
83
|
-
if (idx === -1) continue;
|
|
84
|
-
map[unquote(pair.slice(0, idx))] = parseScalar(pair.slice(idx + 1));
|
|
85
|
-
}
|
|
86
|
-
return map;
|
|
87
|
-
}
|
|
88
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
89
|
-
if (trimmed.startsWith('"') || trimmed.startsWith("'")) return unquote(trimmed);
|
|
90
|
-
return trimmed;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function splitTopLevel(input: string): string[] {
|
|
94
|
-
const parts: string[] = [];
|
|
95
|
-
let current = "";
|
|
96
|
-
let depth = 0;
|
|
97
|
-
let quote: string | undefined;
|
|
98
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
99
|
-
const ch = input[i];
|
|
100
|
-
if (quote) {
|
|
101
|
-
current += ch;
|
|
102
|
-
if (ch === quote && input[i - 1] !== "\\") quote = undefined;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (ch === '"' || ch === "'") {
|
|
106
|
-
quote = ch;
|
|
107
|
-
current += ch;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (ch === "[" || ch === "{") depth += 1;
|
|
111
|
-
if (ch === "]" || ch === "}") depth -= 1;
|
|
112
|
-
if (ch === "," && depth === 0) {
|
|
113
|
-
parts.push(current);
|
|
114
|
-
current = "";
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
current += ch;
|
|
118
|
-
}
|
|
119
|
-
if (current.trim()) parts.push(current);
|
|
120
|
-
return parts;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function indentOf(line: string): number {
|
|
124
|
-
let i = 0;
|
|
125
|
-
while (i < line.length && line[i] === " ") i += 1;
|
|
126
|
-
return i;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function parseBlockScalar(lines: string[], startIndex: number, baseIndent: number, folded: boolean): { value: string; nextIndex: number } {
|
|
130
|
-
const collected: string[] = [];
|
|
131
|
-
let i = startIndex;
|
|
132
|
-
let blockIndent = -1;
|
|
133
|
-
for (; i < lines.length; i += 1) {
|
|
134
|
-
const line = lines[i];
|
|
135
|
-
if (line.trim() === "") {
|
|
136
|
-
collected.push("");
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const indent = indentOf(line);
|
|
140
|
-
if (indent <= baseIndent) break;
|
|
141
|
-
if (blockIndent === -1) blockIndent = indent;
|
|
142
|
-
collected.push(line.slice(Math.min(blockIndent, indent)));
|
|
143
|
-
}
|
|
144
|
-
while (collected.length > 0 && collected[collected.length - 1] === "") collected.pop();
|
|
145
|
-
const value = folded ? collected.join(" ").replace(/\s+/g, " ").trim() : collected.join("\n");
|
|
146
|
-
return { value, nextIndex: i };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Split a markdown document into frontmatter + body.
|
|
151
|
-
*/
|
|
152
|
-
export function splitFrontmatter(source: string): FrontmatterSplit {
|
|
153
|
-
const text = source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
|
|
154
|
-
const lines = text.split(/\r?\n/);
|
|
155
|
-
if (lines.length === 0 || !FENCE.test(lines[0])) {
|
|
156
|
-
return { frontmatter: {}, body: text, hasFrontmatter: false, raw: "", bodyLineOffset: 0 };
|
|
157
|
-
}
|
|
158
|
-
let end = -1;
|
|
159
|
-
for (let i = 1; i < lines.length; i += 1) {
|
|
160
|
-
if (FENCE.test(lines[i])) {
|
|
161
|
-
end = i;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (end === -1) {
|
|
166
|
-
return { frontmatter: {}, body: text, hasFrontmatter: false, raw: "", bodyLineOffset: 0 };
|
|
167
|
-
}
|
|
168
|
-
const rawLines = lines.slice(1, end);
|
|
169
|
-
const body = lines.slice(end + 1).join("\n").replace(/^\n/, "");
|
|
170
|
-
return {
|
|
171
|
-
frontmatter: parseFrontmatterLines(rawLines),
|
|
172
|
-
body,
|
|
173
|
-
hasFrontmatter: true,
|
|
174
|
-
raw: rawLines.join("\n"),
|
|
175
|
-
bodyLineOffset: end + 1,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Parse raw frontmatter lines into an object. */
|
|
180
|
-
export function parseFrontmatterLines(lines: string[]): Record<string, unknown> {
|
|
181
|
-
const result: Record<string, unknown> = {};
|
|
182
|
-
let i = 0;
|
|
183
|
-
while (i < lines.length) {
|
|
184
|
-
const line = lines[i];
|
|
185
|
-
if (line.trim() === "" || line.trimStart().startsWith("#")) {
|
|
186
|
-
i += 1;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const indent = indentOf(line);
|
|
190
|
-
if (indent > 0) {
|
|
191
|
-
// Orphan nested content without a parent key: skip.
|
|
192
|
-
i += 1;
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const colon = line.indexOf(":");
|
|
196
|
-
if (colon === -1) {
|
|
197
|
-
i += 1;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const key = unquote(line.slice(0, colon));
|
|
201
|
-
if (!key) {
|
|
202
|
-
i += 1;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const rest = line.slice(colon + 1).trim();
|
|
206
|
-
if (rest === "" ) {
|
|
207
|
-
// Could be a nested map or a block array on following lines.
|
|
208
|
-
const nested: Record<string, unknown> = {};
|
|
209
|
-
const items: unknown[] = [];
|
|
210
|
-
let j = i + 1;
|
|
211
|
-
let sawNested = false;
|
|
212
|
-
let sawList = false;
|
|
213
|
-
for (; j < lines.length; j += 1) {
|
|
214
|
-
const next = lines[j];
|
|
215
|
-
if (next.trim() === "") continue;
|
|
216
|
-
const nextIndent = indentOf(next);
|
|
217
|
-
if (nextIndent === 0) break;
|
|
218
|
-
const stripped = next.trim();
|
|
219
|
-
if (stripped.startsWith("- ")) {
|
|
220
|
-
sawList = true;
|
|
221
|
-
items.push(parseScalar(stripped.slice(2)));
|
|
222
|
-
} else if (stripped === "-") {
|
|
223
|
-
sawList = true;
|
|
224
|
-
items.push(null);
|
|
225
|
-
} else {
|
|
226
|
-
const nestedColon = stripped.indexOf(":");
|
|
227
|
-
if (nestedColon === -1) continue;
|
|
228
|
-
sawNested = true;
|
|
229
|
-
nested[unquote(stripped.slice(0, nestedColon))] = parseScalar(stripped.slice(nestedColon + 1));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (sawList) result[key] = items;
|
|
233
|
-
else if (sawNested) result[key] = nested;
|
|
234
|
-
else result[key] = null;
|
|
235
|
-
i = j;
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (rest === "|" || rest === "|-" || rest === ">" || rest === ">-") {
|
|
239
|
-
const folded = rest.startsWith(">");
|
|
240
|
-
const { value, nextIndex } = parseBlockScalar(lines, i + 1, indent, folded);
|
|
241
|
-
result[key] = value;
|
|
242
|
-
i = nextIndex;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
if (rest.startsWith("&") || rest.startsWith("*") || rest.startsWith("!")) {
|
|
246
|
-
// Anchors, aliases and tags are unsupported: keep as raw text.
|
|
247
|
-
result[key] = rest;
|
|
248
|
-
i += 1;
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
result[key] = parseScalar(rest);
|
|
252
|
-
i += 1;
|
|
253
|
-
}
|
|
254
|
-
return result;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const BARE_SAFE = /^[A-Za-z0-9_./+@-]+( [A-Za-z0-9_./+@-]+)*$/;
|
|
258
|
-
|
|
259
|
-
function yamlScalar(value: unknown): string {
|
|
260
|
-
if (value === null || value === undefined) return "null";
|
|
261
|
-
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
262
|
-
const text = String(value);
|
|
263
|
-
if (text === "") return '""';
|
|
264
|
-
if (/^[a-z]+:\/\//i.test(text) || /^\d{4}-\d{2}-\d{2}T/.test(text)) return text;
|
|
265
|
-
if (BARE_SAFE.test(text) && !/^(true|false|null|~|yes|no|on|off)$/i.test(text) && !/^-?\d/.test(text)) return text;
|
|
266
|
-
return JSON.stringify(text);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Serialize an object back into frontmatter lines (without the `---` fences). */
|
|
270
|
-
export function serializeFrontmatter(data: Record<string, unknown>): string {
|
|
271
|
-
const lines: string[] = [];
|
|
272
|
-
for (const [key, value] of Object.entries(data)) {
|
|
273
|
-
if (value === undefined) continue;
|
|
274
|
-
if (Array.isArray(value)) {
|
|
275
|
-
if (value.length === 0) {
|
|
276
|
-
lines.push(`${key}: []`);
|
|
277
|
-
} else if (value.every((item) => typeof item === "string" && BARE_SAFE.test(item as string))) {
|
|
278
|
-
lines.push(`${key}: [${value.join(", ")}]`);
|
|
279
|
-
} else {
|
|
280
|
-
lines.push(`${key}:`);
|
|
281
|
-
for (const item of value) lines.push(` - ${yamlScalar(item)}`);
|
|
282
|
-
}
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (value !== null && typeof value === "object") {
|
|
286
|
-
lines.push(`${key}:`);
|
|
287
|
-
for (const [nestedKey, nestedValue] of Object.entries(value as Record<string, unknown>)) {
|
|
288
|
-
lines.push(` ${nestedKey}: ${yamlScalar(nestedValue)}`);
|
|
289
|
-
}
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
const text = String(value);
|
|
293
|
-
if (text.includes("\n")) {
|
|
294
|
-
lines.push(`${key}: |-`);
|
|
295
|
-
for (const line of text.split("\n")) lines.push(` ${line}`);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
lines.push(`${key}: ${yamlScalar(value)}`);
|
|
299
|
-
}
|
|
300
|
-
return lines.join("\n");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Render a full markdown file with frontmatter. */
|
|
304
|
-
export function stringifyWithFrontmatter(data: Record<string, unknown>, body: string): string {
|
|
305
|
-
const fm = serializeFrontmatter(data);
|
|
306
|
-
const trimmedBody = body.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
307
|
-
return `---\n${fm}\n---\n\n${trimmedBody}\n`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Parse a frontmatter date-ish value into epoch ms. */
|
|
311
|
-
export function parseDateValue(value: unknown): number {
|
|
312
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
313
|
-
if (typeof value !== "string" || !value.trim()) return 0;
|
|
314
|
-
const normalized = value.trim().replace(" ", "T");
|
|
315
|
-
const parsed = Date.parse(normalized);
|
|
316
|
-
if (Number.isNaN(parsed)) return 0;
|
|
317
|
-
return parsed;
|
|
318
|
-
}
|