pi-memoria 0.2.0 → 0.3.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/AGENTS.md +53 -0
- package/README.md +126 -166
- package/index.ts +299 -204
- package/package.json +28 -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/package.json
CHANGED
|
@@ -1,71 +1,30 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"skills": [
|
|
31
|
-
"./skills"
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"index.ts",
|
|
36
|
-
"src",
|
|
37
|
-
"skills",
|
|
38
|
-
"README.md",
|
|
39
|
-
"LICENSE"
|
|
40
|
-
],
|
|
41
|
-
"scripts": {
|
|
42
|
-
"test": "node --test tests/*.test.ts",
|
|
43
|
-
"eval": "node tests/eval.ts",
|
|
44
|
-
"bench": "node tests/bench.ts",
|
|
45
|
-
"bench:real": "node tests/bench.ts 1000 5000 --real",
|
|
46
|
-
"typecheck": "tsc --noEmit"
|
|
47
|
-
},
|
|
48
|
-
"peerDependencies": {
|
|
49
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
50
|
-
"@earendil-works/pi-tui": "*",
|
|
51
|
-
"typebox": "*"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
55
|
-
"@earendil-works/pi-tui": "*",
|
|
56
|
-
"@types/node": "^24.0.0",
|
|
57
|
-
"typebox": "*",
|
|
58
|
-
"typescript": "^5.6.0"
|
|
59
|
-
},
|
|
60
|
-
"peerDependenciesMeta": {
|
|
61
|
-
"@earendil-works/pi-coding-agent": {
|
|
62
|
-
"optional": true
|
|
63
|
-
},
|
|
64
|
-
"@earendil-works/pi-tui": {
|
|
65
|
-
"optional": true
|
|
66
|
-
},
|
|
67
|
-
"typebox": {
|
|
68
|
-
"optional": true
|
|
69
|
-
}
|
|
70
|
-
}
|
|
2
|
+
"name": "pi-memoria",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Global working memory, SQLite FTS5 recall, and complete Pi session search",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": ["pi-package", "pi-extension", "memory", "sqlite", "fts5"],
|
|
8
|
+
"files": ["index.ts", "src", "README.md", "AGENTS.md", "LICENSE"],
|
|
9
|
+
"pi": { "extensions": ["./index.ts"] },
|
|
10
|
+
"engines": { "node": ">=22.19.0" },
|
|
11
|
+
"scripts": {
|
|
12
|
+
"check": "tsc --noEmit",
|
|
13
|
+
"test": "tsx --test test/*.test.ts",
|
|
14
|
+
"bench": "tsx scripts/benchmark.ts",
|
|
15
|
+
"verify": "npm run check && npm test"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": { "proper-lockfile": "^4.1.2" },
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
20
|
+
"typebox": "*"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
24
|
+
"@types/node": "^22.19.0",
|
|
25
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
26
|
+
"tsx": "^4.20.0",
|
|
27
|
+
"typebox": "1.3.27",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
}
|
|
71
30
|
}
|
package/src/aliases.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { isMissing } from "./files.ts";
|
|
3
|
+
|
|
4
|
+
export const MAX_ALIAS_FILE_BYTES = 64 * 1024;
|
|
5
|
+
export const MAX_ALIAS_KEYS = 256;
|
|
6
|
+
export const MAX_ALIAS_TARGETS = 8;
|
|
7
|
+
export const MAX_ALIAS_TOKEN_UNITS = 80;
|
|
8
|
+
export const MAX_ADDED_ALTERNATIVES = 128;
|
|
9
|
+
|
|
10
|
+
const TOKEN = /^[\p{L}\p{N}\p{M}_]+$/u;
|
|
11
|
+
|
|
12
|
+
/** Directional, one-hop terminology alternatives. User keys replace these entries wholesale. */
|
|
13
|
+
const BUILT_IN: ReadonlyArray<readonly [string, readonly string[]]> = [
|
|
14
|
+
["k8s", ["kubernetes"]],
|
|
15
|
+
["db", ["database"]],
|
|
16
|
+
["repo", ["repository"]],
|
|
17
|
+
["prefs", ["preferences"]],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export interface AliasConfiguration { aliases: Map<string, string[]>; warning?: string }
|
|
21
|
+
export interface QueryExpansion { expansions: Array<{ term: string; aliases: string[] }>; warning?: string }
|
|
22
|
+
|
|
23
|
+
function boundedRead(path: string): { text: string; bytes: number } {
|
|
24
|
+
const descriptor = openSync(path, "r");
|
|
25
|
+
try {
|
|
26
|
+
const buffer = Buffer.alloc(MAX_ALIAS_FILE_BYTES + 1);
|
|
27
|
+
const bytes = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
28
|
+
return { text: buffer.subarray(0, bytes).toString("utf8"), bytes };
|
|
29
|
+
} finally {
|
|
30
|
+
closeSync(descriptor);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function invalid(aliases: Map<string, string[]>, reason: string): AliasConfiguration {
|
|
35
|
+
return { aliases, warning: `${reason} Built-in aliases remain active; fix or remove the file and restart Pi or /reload.` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load optional `synonyms.json` from the global memory directory. A missing
|
|
40
|
+
* file is normal. Any malformed, unreadable, or oversized file is ignored
|
|
41
|
+
* whole so a partial configuration is never merged.
|
|
42
|
+
*/
|
|
43
|
+
export function loadAliases(path?: string): AliasConfiguration {
|
|
44
|
+
const aliases = new Map<string, string[]>(BUILT_IN.map(([key, targets]) => [key, [...targets]]));
|
|
45
|
+
if (!path) return { aliases };
|
|
46
|
+
let text: string;
|
|
47
|
+
try {
|
|
48
|
+
const read = boundedRead(path);
|
|
49
|
+
if (read.bytes > MAX_ALIAS_FILE_BYTES) return invalid(aliases, `synonyms.json is larger than ${MAX_ALIAS_FILE_BYTES} bytes and was ignored.`);
|
|
50
|
+
text = read.text;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isMissing(error)) return { aliases };
|
|
53
|
+
return invalid(aliases, `synonyms.json could not be read (${(error as Error).message}) and was ignored.`);
|
|
54
|
+
}
|
|
55
|
+
let value: unknown;
|
|
56
|
+
try { value = JSON.parse(text); }
|
|
57
|
+
catch { return invalid(aliases, "synonyms.json is not valid JSON and was ignored."); }
|
|
58
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return invalid(aliases, "synonyms.json must be a JSON object mapping single tokens to arrays of single tokens.");
|
|
60
|
+
}
|
|
61
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
62
|
+
if (entries.length > MAX_ALIAS_KEYS) return invalid(aliases, `synonyms.json has more than ${MAX_ALIAS_KEYS} keys and was ignored.`);
|
|
63
|
+
const user = new Map<string, string[]>();
|
|
64
|
+
for (const [key, targets] of entries) {
|
|
65
|
+
const normalized = key.trim().toLowerCase();
|
|
66
|
+
if (!TOKEN.test(normalized) || normalized.length > MAX_ALIAS_TOKEN_UNITS) {
|
|
67
|
+
return invalid(aliases, `synonyms.json key ${JSON.stringify(key)} is invalid: keys must be single tokens of at most ${MAX_ALIAS_TOKEN_UNITS} characters.`);
|
|
68
|
+
}
|
|
69
|
+
if (user.has(normalized)) return invalid(aliases, `synonyms.json contains duplicate key ${JSON.stringify(normalized)} after lowercasing and was ignored.`);
|
|
70
|
+
if (!Array.isArray(targets) || targets.length > MAX_ALIAS_TARGETS) {
|
|
71
|
+
return invalid(aliases, `synonyms.json key ${JSON.stringify(key)} must map to an array of at most ${MAX_ALIAS_TARGETS} tokens.`);
|
|
72
|
+
}
|
|
73
|
+
const list: string[] = [];
|
|
74
|
+
for (const target of targets) {
|
|
75
|
+
if (typeof target !== "string") {
|
|
76
|
+
return invalid(aliases, `synonyms.json key ${JSON.stringify(key)} contains a non-string alternative.`);
|
|
77
|
+
}
|
|
78
|
+
const normalizedTarget = target.trim().toLowerCase();
|
|
79
|
+
if (!TOKEN.test(normalizedTarget) || normalizedTarget.length > MAX_ALIAS_TOKEN_UNITS) {
|
|
80
|
+
return invalid(aliases, `synonyms.json alternative ${JSON.stringify(target)} is invalid: alternatives must be single tokens of at most ${MAX_ALIAS_TOKEN_UNITS} characters.`);
|
|
81
|
+
}
|
|
82
|
+
if (normalizedTarget !== normalized && !list.includes(normalizedTarget)) list.push(normalizedTarget);
|
|
83
|
+
}
|
|
84
|
+
user.set(normalized, list);
|
|
85
|
+
}
|
|
86
|
+
for (const [key, targets] of user) aliases.set(key, targets);
|
|
87
|
+
return { aliases };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One-hop expansion for the original query tokens, bounded in total added alternatives. */
|
|
91
|
+
export function expandQuery(terms: string[], aliases: Map<string, string[]>): QueryExpansion {
|
|
92
|
+
const expansions: Array<{ term: string; aliases: string[] }> = [];
|
|
93
|
+
let added = 0;
|
|
94
|
+
for (const term of terms) {
|
|
95
|
+
const alternatives = aliases.get(term.toLowerCase()) ?? [];
|
|
96
|
+
if (!alternatives.length) continue;
|
|
97
|
+
added += alternatives.length;
|
|
98
|
+
expansions.push({ term, aliases: alternatives });
|
|
99
|
+
}
|
|
100
|
+
if (added > MAX_ADDED_ALTERNATIVES) {
|
|
101
|
+
return { expansions: [], warning: `Alias expansion adds ${added} alternatives, over the ${MAX_ADDED_ALTERNATIVES}-term limit; the original terms were searched unexpanded.` };
|
|
102
|
+
}
|
|
103
|
+
return { expansions };
|
|
104
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,190 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration and path resolution for memoria.
|
|
3
|
-
*
|
|
4
|
-
* A store root has this shape:
|
|
5
|
-
*
|
|
6
|
-
* memoria/
|
|
7
|
-
* ├── MEMORY.md curated memory injected into every session (<= hotLimit chars)
|
|
8
|
-
* ├── config.json optional config overrides
|
|
9
|
-
* ├── .index/ derived index data (never indexed)
|
|
10
|
-
* ├── .trash/ soft-deleted memories
|
|
11
|
-
* └── library/ categorized markdown memories
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
import { homedir } from "node:os";
|
|
15
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
16
|
-
import { readFileOrUndefined } from "./util.ts";
|
|
17
|
-
import type { MemoriaConfig } from "./types.ts";
|
|
18
|
-
|
|
19
|
-
export const INDEX_DIR = ".index";
|
|
20
|
-
export const TRASH_DIR = ".trash";
|
|
21
|
-
export const LIBRARY_DIR = "library";
|
|
22
|
-
export const HOT_FILE = "MEMORY.md";
|
|
23
|
-
export const CONFIG_FILE = "config.json";
|
|
24
|
-
export const INDEX_FILE = "index.json";
|
|
25
|
-
export const INDEX_BIN_FILE = "index.bin";
|
|
26
|
-
export const USAGE_FILE = "usage.json";
|
|
27
|
-
export const SYNONYMS_FILE = "synonyms.json";
|
|
28
|
-
|
|
29
|
-
export const DEFAULT_CONFIG: MemoriaConfig = {
|
|
30
|
-
version: 1,
|
|
31
|
-
rootDir: "$AGENT_DIR/memoria",
|
|
32
|
-
projectRoot: "",
|
|
33
|
-
extraRoots: [],
|
|
34
|
-
hotLimit: 5000,
|
|
35
|
-
snippetChars: 280,
|
|
36
|
-
autoRecall: true,
|
|
37
|
-
autoRecallLimit: 3,
|
|
38
|
-
autoRecallMinRatio: 0.3,
|
|
39
|
-
autoRecallMinScore: 1.4,
|
|
40
|
-
autoRecallMaxChars: 2400,
|
|
41
|
-
autoRecallLastTurns: 1,
|
|
42
|
-
autoRecallPriorWeight: 0.4,
|
|
43
|
-
bodyCacheBytes: 8 * 1024 * 1024,
|
|
44
|
-
scanIntervalMs: 2000,
|
|
45
|
-
exclude: [],
|
|
46
|
-
defaultCategory: "inbox",
|
|
47
|
-
topicMaxWords: 6,
|
|
48
|
-
topicMaxChars: 8000,
|
|
49
|
-
usageTracking: true,
|
|
50
|
-
lastUsedWriteIntervalMs: 21_600_000,
|
|
51
|
-
staleAfterDays: 365,
|
|
52
|
-
promoteAfterWrites: 3,
|
|
53
|
-
dedupeThreshold: 0.62,
|
|
54
|
-
relatedHits: 3,
|
|
55
|
-
relatedBoost: 0.35,
|
|
56
|
-
synonyms: {},
|
|
57
|
-
synonymWeight: 0.6,
|
|
58
|
-
timeHints: true,
|
|
59
|
-
searchCacheSize: 32,
|
|
60
|
-
rerank: false,
|
|
61
|
-
rerankModel: "",
|
|
62
|
-
rerankTopK: 12,
|
|
63
|
-
rerankTimeoutMs: 2500,
|
|
64
|
-
watcherSettleMs: 300,
|
|
65
|
-
indexFormat: "auto",
|
|
66
|
-
sessionSearch: true,
|
|
67
|
-
sessionRoots: [],
|
|
68
|
-
sessionFallback: true,
|
|
69
|
-
sessionScanMs: 1500,
|
|
70
|
-
sessionExcerptChars: 400,
|
|
71
|
-
sessionCacheBytes: 32 * 1024 * 1024,
|
|
72
|
-
sessionIncludeTools: false,
|
|
73
|
-
sessionRipgrep: true,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** Filesystem context used to resolve store paths. */
|
|
77
|
-
export interface PathContext {
|
|
78
|
-
home: string;
|
|
79
|
-
/** Pi's user config directory (`~/.pi/agent` by default, or `PI_CODING_AGENT_DIR`). */
|
|
80
|
-
agentDir: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface ResolvedRoots {
|
|
84
|
-
/** The main user-level store: `<agent dir>/memoria` by default. */
|
|
85
|
-
primary: string;
|
|
86
|
-
/** Optional per-project store (writable). Empty string when disabled. */
|
|
87
|
-
project: string;
|
|
88
|
-
/** Additional read-only store roots declared in config. */
|
|
89
|
-
extras: string[];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Expand `$AGENT_DIR`, a leading `~`, absolute paths and cwd-relative paths.
|
|
94
|
-
* Returns undefined for an empty value.
|
|
95
|
-
*/
|
|
96
|
-
export function resolveStorePath(input: string, cwd: string, context: PathContext): string | undefined {
|
|
97
|
-
const value = (input ?? "").trim();
|
|
98
|
-
if (!value) return undefined;
|
|
99
|
-
let expanded = value.replace(/\$AGENT_DIR/g, context.agentDir);
|
|
100
|
-
if (expanded === "~") return context.home;
|
|
101
|
-
if (expanded.startsWith("~/") || expanded.startsWith("~\\")) expanded = join(context.home, expanded.slice(2));
|
|
102
|
-
return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Resolve the store roots for a working directory. */
|
|
106
|
-
export function resolveRoots(cwd: string, config: MemoriaConfig = DEFAULT_CONFIG, context?: Partial<PathContext>): ResolvedRoots {
|
|
107
|
-
const home = context?.home ?? homedir();
|
|
108
|
-
const pathContext: PathContext = { home, agentDir: context?.agentDir ?? join(home, ".pi", "agent") };
|
|
109
|
-
const primary = resolveStorePath(config.rootDir, cwd, pathContext) ?? join(pathContext.agentDir, "memoria");
|
|
110
|
-
const project = resolveStorePath(config.projectRoot ?? "", cwd, pathContext) ?? "";
|
|
111
|
-
const extras = config.extraRoots
|
|
112
|
-
.map((entry) => resolveStorePath(entry, cwd, pathContext))
|
|
113
|
-
.filter((entry): entry is string => Boolean(entry) && entry !== primary && entry !== project);
|
|
114
|
-
return { primary, project, extras: [...new Set(extras)] };
|
|
115
|
-
}
|
|
116
3
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const number = (value: unknown): number | undefined => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
|
|
122
|
-
const bool = (value: unknown): boolean | undefined => (typeof value === "boolean" ? value : undefined);
|
|
123
|
-
const str = (value: unknown): string | undefined => (typeof value === "string" && value.length > 0 ? value : undefined);
|
|
124
|
-
if (str(input.rootDir)) out.rootDir = input.rootDir as string;
|
|
125
|
-
if (Array.isArray(input.extraRoots)) out.extraRoots = input.extraRoots.filter((item): item is string => typeof item === "string");
|
|
126
|
-
if (Array.isArray(input.exclude)) out.exclude = input.exclude.filter((item): item is string => typeof item === "string");
|
|
127
|
-
if (number(input.hotLimit) !== undefined) out.hotLimit = Math.max(500, number(input.hotLimit)!);
|
|
128
|
-
if (number(input.snippetChars) !== undefined) out.snippetChars = Math.max(80, number(input.snippetChars)!);
|
|
129
|
-
if (bool(input.autoRecall) !== undefined) out.autoRecall = bool(input.autoRecall);
|
|
130
|
-
if (number(input.autoRecallLimit) !== undefined) out.autoRecallLimit = Math.max(0, Math.min(20, number(input.autoRecallLimit)!));
|
|
131
|
-
if (number(input.autoRecallMinRatio) !== undefined) out.autoRecallMinRatio = Math.max(0, Math.min(1, number(input.autoRecallMinRatio)!));
|
|
132
|
-
if (number(input.autoRecallMinScore) !== undefined) out.autoRecallMinScore = Math.max(0, number(input.autoRecallMinScore)!);
|
|
133
|
-
if (number(input.autoRecallMaxChars) !== undefined) out.autoRecallMaxChars = Math.max(200, number(input.autoRecallMaxChars)!);
|
|
134
|
-
if (number(input.autoRecallLastTurns) !== undefined) out.autoRecallLastTurns = Math.max(0, Math.min(5, number(input.autoRecallLastTurns)!));
|
|
135
|
-
if (number(input.bodyCacheBytes) !== undefined) out.bodyCacheBytes = Math.max(0, number(input.bodyCacheBytes)!);
|
|
136
|
-
if (number(input.scanIntervalMs) !== undefined) out.scanIntervalMs = Math.max(250, number(input.scanIntervalMs)!);
|
|
137
|
-
if (str(input.defaultCategory)) out.defaultCategory = input.defaultCategory as string;
|
|
138
|
-
if (number(input.topicMaxWords) !== undefined) out.topicMaxWords = Math.max(1, Math.min(12, number(input.topicMaxWords)!));
|
|
139
|
-
if (number(input.topicMaxChars) !== undefined) out.topicMaxChars = Math.max(500, number(input.topicMaxChars)!);
|
|
140
|
-
if (str(input.projectRoot)) out.projectRoot = input.projectRoot as string;
|
|
141
|
-
if (number(input.autoRecallPriorWeight) !== undefined) out.autoRecallPriorWeight = Math.max(0, Math.min(1, number(input.autoRecallPriorWeight)!));
|
|
142
|
-
if (bool(input.usageTracking) !== undefined) out.usageTracking = bool(input.usageTracking);
|
|
143
|
-
if (number(input.lastUsedWriteIntervalMs) !== undefined) out.lastUsedWriteIntervalMs = Math.max(60_000, number(input.lastUsedWriteIntervalMs)!);
|
|
144
|
-
if (number(input.staleAfterDays) !== undefined) out.staleAfterDays = Math.max(1, number(input.staleAfterDays)!);
|
|
145
|
-
if (number(input.promoteAfterWrites) !== undefined) out.promoteAfterWrites = Math.max(1, number(input.promoteAfterWrites)!);
|
|
146
|
-
if (number(input.dedupeThreshold) !== undefined) out.dedupeThreshold = Math.max(0.2, Math.min(0.98, number(input.dedupeThreshold)!));
|
|
147
|
-
if (number(input.relatedHits) !== undefined) out.relatedHits = Math.max(0, Math.min(10, number(input.relatedHits)!));
|
|
148
|
-
if (number(input.relatedBoost) !== undefined) out.relatedBoost = Math.max(0, Math.min(2, number(input.relatedBoost)!));
|
|
149
|
-
if (bool(input.timeHints) !== undefined) out.timeHints = bool(input.timeHints);
|
|
150
|
-
if (number(input.searchCacheSize) !== undefined) out.searchCacheSize = Math.max(0, Math.min(512, number(input.searchCacheSize)!));
|
|
151
|
-
if (bool(input.rerank) !== undefined) out.rerank = bool(input.rerank);
|
|
152
|
-
if (str(input.rerankModel)) out.rerankModel = input.rerankModel as string;
|
|
153
|
-
if (number(input.rerankTopK) !== undefined) out.rerankTopK = Math.max(1, Math.min(50, number(input.rerankTopK)!));
|
|
154
|
-
if (number(input.rerankTimeoutMs) !== undefined) out.rerankTimeoutMs = Math.max(250, Math.min(30_000, number(input.rerankTimeoutMs)!));
|
|
155
|
-
if (number(input.synonymWeight) !== undefined) out.synonymWeight = Math.max(0.1, Math.min(1, number(input.synonymWeight)!));
|
|
156
|
-
if (number(input.watcherSettleMs) !== undefined) out.watcherSettleMs = Math.max(0, Math.min(5000, number(input.watcherSettleMs)!));
|
|
157
|
-
if (str(input.indexFormat) && ["auto", "json", "binary"].includes(input.indexFormat as string)) out.indexFormat = input.indexFormat as string;
|
|
158
|
-
if (bool(input.sessionSearch) !== undefined) out.sessionSearch = bool(input.sessionSearch);
|
|
159
|
-
if (bool(input.sessionFallback) !== undefined) out.sessionFallback = bool(input.sessionFallback);
|
|
160
|
-
if (bool(input.sessionIncludeTools) !== undefined) out.sessionIncludeTools = bool(input.sessionIncludeTools);
|
|
161
|
-
if (number(input.sessionScanMs) !== undefined) out.sessionScanMs = Math.max(50, Math.min(60_000, number(input.sessionScanMs)!));
|
|
162
|
-
if (number(input.sessionExcerptChars) !== undefined) out.sessionExcerptChars = Math.max(80, Math.min(4000, number(input.sessionExcerptChars)!));
|
|
163
|
-
if (number(input.sessionCacheBytes) !== undefined) out.sessionCacheBytes = Math.max(0, number(input.sessionCacheBytes)!);
|
|
164
|
-
if (bool(input.sessionRipgrep) !== undefined) out.sessionRipgrep = bool(input.sessionRipgrep);
|
|
165
|
-
if (Array.isArray(input.sessionRoots)) out.sessionRoots = input.sessionRoots.filter((item): item is string => typeof item === "string");
|
|
166
|
-
if (input.synonyms && typeof input.synonyms === "object" && !Array.isArray(input.synonyms)) {
|
|
167
|
-
const table: Record<string, string[]> = {};
|
|
168
|
-
for (const [key, value] of Object.entries(input.synonyms as Record<string, unknown>)) {
|
|
169
|
-
const term = key.trim().toLowerCase();
|
|
170
|
-
if (!term) continue;
|
|
171
|
-
const list = Array.isArray(value) ? value : typeof value === "string" ? value.split(/[,\s]+/) : [];
|
|
172
|
-
const cleaned = list.map((entry) => String(entry).trim().toLowerCase()).filter((entry) => entry && entry !== term);
|
|
173
|
-
if (cleaned.length > 0) table[term] = [...new Set(cleaned)].slice(0, 12);
|
|
174
|
-
}
|
|
175
|
-
out.synonyms = table;
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
4
|
+
export function absolutePath(value: string, name: string): string {
|
|
5
|
+
const expanded = value === "~" ? homedir() : value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
|
|
6
|
+
if (!expanded || !isAbsolute(expanded)) throw new Error(`${name} must be an absolute path (or start with ~/).`);
|
|
7
|
+
return resolve(expanded);
|
|
178
8
|
}
|
|
179
9
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
10
|
+
export function configuration(agentDir: string, env: NodeJS.ProcessEnv = process.env) {
|
|
11
|
+
agentDir = absolutePath(agentDir, "Pi agent directory");
|
|
12
|
+
const directory = env.PI_MEMORIA_DIR ? absolutePath(env.PI_MEMORIA_DIR, "PI_MEMORIA_DIR") : join(agentDir, "memoria");
|
|
13
|
+
let additionalRoots: string[] = [];
|
|
14
|
+
if (env.PI_MEMORIA_SESSION_DIRS) {
|
|
15
|
+
const value: unknown = JSON.parse(env.PI_MEMORIA_SESSION_DIRS);
|
|
16
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
17
|
+
throw new Error("PI_MEMORIA_SESSION_DIRS must be a JSON array of absolute directory paths.");
|
|
18
|
+
}
|
|
19
|
+
additionalRoots = value.map((root: string) => absolutePath(root, "PI_MEMORIA_SESSION_DIRS entry"));
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
directory,
|
|
23
|
+
hotPath: join(directory, "MEMORY.md"),
|
|
24
|
+
databasePath: join(directory, "memoria.sqlite"),
|
|
25
|
+
aliasesPath: join(directory, "synonyms.json"),
|
|
26
|
+
sessionRoots: [join(agentDir, "sessions"), ...additionalRoots],
|
|
27
|
+
};
|
|
190
28
|
}
|