@pi-unipi/memory 2.0.13 → 2.1.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 +51 -7
- package/bridge/mempalace_bridge.py +556 -0
- package/index.ts +7 -4
- package/mempalace.ts +260 -0
- package/package.json +8 -5
- package/storage.ts +253 -22
- package/tools.ts +0 -0
package/mempalace.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/memory — MemPalace backend client
|
|
3
|
+
*
|
|
4
|
+
* Detects and auto-installs MemPalace (via uv), then invokes the bundled
|
|
5
|
+
* Python bridge (bridge/mempalace_bridge.py) once per operation using the
|
|
6
|
+
* MemPalace venv python. Each call is a synchronous spawnSync that prints
|
|
7
|
+
* one JSON line.
|
|
8
|
+
*
|
|
9
|
+
* If MemPalace or uv is unavailable, all operations return null so the
|
|
10
|
+
* storage layer can fall back to the legacy SQLite path. Memory must never
|
|
11
|
+
* hard-fail because the backend is missing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
/** Default MemPalace palace path. */
|
|
21
|
+
export const DEFAULT_PALACE = path.join(os.homedir(), ".mempalace", "palace");
|
|
22
|
+
|
|
23
|
+
const INSTALL_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-install");
|
|
24
|
+
const MIGRATED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-migrated");
|
|
25
|
+
/** Flag written after a successful ping, so subsequent sessions can skip
|
|
26
|
+
* the ~0.5s Python cold-start sanity check. Stale after PING_VERIFIED_TTL_MS. */
|
|
27
|
+
const PING_VERIFIED_FLAG = path.join(os.homedir(), ".unipi", "memory", ".mempalace-ping-verified");
|
|
28
|
+
const PING_VERIFIED_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
29
|
+
|
|
30
|
+
/** Path to the bundled bridge script. */
|
|
31
|
+
const BRIDGE_PATH = path.join(dirname(fileURLToPath(import.meta.url)), "bridge", "mempalace_bridge.py");
|
|
32
|
+
|
|
33
|
+
function dirname(p: string): string {
|
|
34
|
+
return path.dirname(p);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BridgeResponse<T> {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
result?: T;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MempalaceRecord {
|
|
44
|
+
id: string;
|
|
45
|
+
title: string;
|
|
46
|
+
content: string;
|
|
47
|
+
tags: string[];
|
|
48
|
+
project: string;
|
|
49
|
+
type: "preference" | "decision" | "pattern" | "summary";
|
|
50
|
+
created: string;
|
|
51
|
+
updated: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MempalaceSearchResult extends MempalaceRecord {
|
|
55
|
+
score: number;
|
|
56
|
+
snippet: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MempalaceListItem {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
type: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface MempalaceListItemAll extends MempalaceListItem {
|
|
66
|
+
project: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface MempalaceInstall {
|
|
70
|
+
python: string;
|
|
71
|
+
version: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Check whether a binary is on PATH. */
|
|
75
|
+
function which(bin: string): string | null {
|
|
76
|
+
try {
|
|
77
|
+
const res = spawnSync(bin, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
78
|
+
if (res.status === 0 || res.stdout || res.stderr) return bin;
|
|
79
|
+
} catch { /* ignore */ }
|
|
80
|
+
// Fallback: `which`
|
|
81
|
+
try {
|
|
82
|
+
const res = spawnSync("which", [bin], { encoding: "utf-8" });
|
|
83
|
+
if (res.status === 0) return res.stdout.trim() || null;
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Locate the MemPalace venv python after a `uv tool install mempalace`.
|
|
90
|
+
* Uses `uv tool dir` to find the venv root.
|
|
91
|
+
*/
|
|
92
|
+
function findVenvPython(): string | null {
|
|
93
|
+
try {
|
|
94
|
+
const res = spawnSync("uv", ["tool", "dir"], { encoding: "utf-8", timeout: 5000 });
|
|
95
|
+
if (res.status !== 0 || !res.stdout.trim()) return null;
|
|
96
|
+
const candidate = path.join(res.stdout.trim(), "mempalace", "bin", "python");
|
|
97
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
98
|
+
// Some platforms use Scripts/ on Windows — not relevant here but be safe.
|
|
99
|
+
const win = path.join(res.stdout.trim(), "mempalace", "Scripts", "python.exe");
|
|
100
|
+
if (fs.existsSync(win)) return win;
|
|
101
|
+
} catch { /* ignore */ }
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Read a cached install record. */
|
|
106
|
+
function readCachedInstall(): MempalaceInstall | null {
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(INSTALL_FLAG)) {
|
|
109
|
+
const parsed = JSON.parse(fs.readFileSync(INSTALL_FLAG, "utf-8"));
|
|
110
|
+
if (parsed && parsed.python && fs.existsSync(parsed.python)) {
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch { /* ignore */ }
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Persist an install record so we don't re-detect every session. */
|
|
119
|
+
function writeCachedInstall(install: MempalaceInstall): void {
|
|
120
|
+
try {
|
|
121
|
+
fs.mkdirSync(path.dirname(INSTALL_FLAG), { recursive: true });
|
|
122
|
+
fs.writeFileSync(INSTALL_FLAG, JSON.stringify(install, null, 2), "utf-8");
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Detect mempalace version via the venv python. */
|
|
127
|
+
function detectVersion(python: string): string {
|
|
128
|
+
try {
|
|
129
|
+
const res = spawnSync(python, ["-c", "import mempalace; print(getattr(mempalace,'__version__','unknown'))"], { encoding: "utf-8", timeout: 5000 });
|
|
130
|
+
return (res.stdout || "").trim() || "unknown";
|
|
131
|
+
} catch {
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ensure MemPalace is installed and return the venv python path.
|
|
138
|
+
* Auto-installs via `uv tool install mempalace` if missing and uv is
|
|
139
|
+
* available. Returns null if MemPalace cannot be made available (caller
|
|
140
|
+
* should fall back to legacy SQLite storage).
|
|
141
|
+
*/
|
|
142
|
+
export function ensureMempalace(): MempalaceInstall | null {
|
|
143
|
+
const cached = readCachedInstall();
|
|
144
|
+
if (cached) return cached;
|
|
145
|
+
|
|
146
|
+
// 1. Already installed via uv tool? Locate venv python.
|
|
147
|
+
let python = findVenvPython();
|
|
148
|
+
|
|
149
|
+
// 2. If not, and uv is available, install it.
|
|
150
|
+
if (!python && which("uv")) {
|
|
151
|
+
try {
|
|
152
|
+
const res = spawnSync("uv", ["tool", "install", "mempalace"], {
|
|
153
|
+
encoding: "utf-8",
|
|
154
|
+
timeout: 180_000, // first install downloads deps + embedding model
|
|
155
|
+
});
|
|
156
|
+
if (res.status === 0) {
|
|
157
|
+
python = findVenvPython();
|
|
158
|
+
}
|
|
159
|
+
} catch { /* ignore — fall back */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!python) return null;
|
|
163
|
+
|
|
164
|
+
const version = detectVersion(python);
|
|
165
|
+
const install = { python, version };
|
|
166
|
+
writeCachedInstall(install);
|
|
167
|
+
return install;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Drop the cached install record (forces re-detection next session). */
|
|
171
|
+
export function invalidateInstallCache(): void {
|
|
172
|
+
try { if (fs.existsSync(INSTALL_FLAG)) fs.unlinkSync(INSTALL_FLAG); } catch { /* ignore */ }
|
|
173
|
+
invalidatePingVerified();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Was the palace ping-verified recently enough to trust without re-pinging? */
|
|
177
|
+
export function isPingVerified(): boolean {
|
|
178
|
+
try {
|
|
179
|
+
if (!fs.existsSync(PING_VERIFIED_FLAG)) return false;
|
|
180
|
+
const ts = Number.parseInt(fs.readFileSync(PING_VERIFIED_FLAG, "utf-8").trim(), 10);
|
|
181
|
+
if (!Number.isFinite(ts)) return false;
|
|
182
|
+
return Date.now() - ts < PING_VERIFIED_TTL_MS;
|
|
183
|
+
} catch { return false; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Mark the palace as ping-verified (written after a successful ping). */
|
|
187
|
+
export function markPingVerified(): void {
|
|
188
|
+
try {
|
|
189
|
+
fs.mkdirSync(path.dirname(PING_VERIFIED_FLAG), { recursive: true });
|
|
190
|
+
fs.writeFileSync(PING_VERIFIED_FLAG, String(Date.now()), "utf-8");
|
|
191
|
+
} catch { /* ignore */ }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Drop the ping-verified flag (forces a real ping next session). */
|
|
195
|
+
export function invalidatePingVerified(): void {
|
|
196
|
+
try { if (fs.existsSync(PING_VERIFIED_FLAG)) fs.unlinkSync(PING_VERIFIED_FLAG); } catch { /* ignore */ }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Has the one-way legacy migration been completed? */
|
|
200
|
+
export function isMigrated(): boolean {
|
|
201
|
+
return fs.existsSync(MIGRATED_FLAG);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Mark the one-way legacy migration complete. */
|
|
205
|
+
export function markMigrated(): void {
|
|
206
|
+
try {
|
|
207
|
+
fs.mkdirSync(path.dirname(MIGRATED_FLAG), { recursive: true });
|
|
208
|
+
fs.writeFileSync(MIGRATED_FLAG, new Date().toISOString(), "utf-8");
|
|
209
|
+
} catch { /* ignore */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Force re-migration by clearing the flag. */
|
|
213
|
+
export function clearMigratedFlag(): void {
|
|
214
|
+
try { if (fs.existsSync(MIGRATED_FLAG)) fs.unlinkSync(MIGRATED_FLAG); } catch { /* ignore */ }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Run one bridge command synchronously. Returns the parsed result, or null
|
|
219
|
+
* on any failure (timeout, non-zero exit, bad JSON, ok=false).
|
|
220
|
+
*/
|
|
221
|
+
export function runBridge<T = unknown>(
|
|
222
|
+
install: MempalaceInstall,
|
|
223
|
+
palace: string,
|
|
224
|
+
cmd: string,
|
|
225
|
+
args: Record<string, unknown> = {},
|
|
226
|
+
): T | null {
|
|
227
|
+
let argsJson: string;
|
|
228
|
+
try {
|
|
229
|
+
argsJson = JSON.stringify(args);
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
let res;
|
|
234
|
+
try {
|
|
235
|
+
res = spawnSync(install.python, [BRIDGE_PATH, palace, cmd, argsJson], {
|
|
236
|
+
encoding: "utf-8",
|
|
237
|
+
timeout: 60_000,
|
|
238
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
if (res.error || res.status !== 0) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const out = (res.stdout || "").trim();
|
|
247
|
+
if (!out) return null;
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(out) as BridgeResponse<T>;
|
|
250
|
+
if (!parsed.ok) return null;
|
|
251
|
+
return (parsed.result ?? null) as T | null;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Ping the bridge — returns true if the backend is alive. */
|
|
258
|
+
export function ping(install: MempalaceInstall, palace: string): boolean {
|
|
259
|
+
return runBridge<string>(install, palace, "ping") === "pong";
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/memory",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Persistent cross-session memory with
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"pi-coding-agent",
|
|
22
22
|
"unipi",
|
|
23
23
|
"memory",
|
|
24
|
+
"mempalace",
|
|
24
25
|
"vector-search",
|
|
25
26
|
"sqlite-vec"
|
|
26
27
|
],
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
"settings.ts",
|
|
33
34
|
"tools.ts",
|
|
34
35
|
"commands.ts",
|
|
36
|
+
"mempalace.ts",
|
|
37
|
+
"bridge/mempalace_bridge.py",
|
|
35
38
|
"tui/**/*",
|
|
36
39
|
"skills/**/*",
|
|
37
40
|
"README.md"
|
|
@@ -40,11 +43,11 @@
|
|
|
40
43
|
"better-sqlite3": "^12.9.0",
|
|
41
44
|
"sqlite-vec": "^0.1.9",
|
|
42
45
|
"js-yaml": "^4.1.0",
|
|
43
|
-
"@pi-unipi/core": "2.
|
|
44
|
-
"@pi-unipi/info-screen": "2.
|
|
46
|
+
"@pi-unipi/core": "2.1.1",
|
|
47
|
+
"@pi-unipi/info-screen": "2.1.1"
|
|
45
48
|
},
|
|
46
49
|
"peerDependencies": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
50
|
+
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
48
51
|
"typebox": "^1.1.38"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|