opencode-mempalace-persistence 2.0.1 → 2.1.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/dist/index.js +68 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { execSync,
|
|
1
|
+
import { execSync, execFileSync, execFile, spawnSync } from "child_process";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmdirSync, unlinkSync, appendFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
6
|
const HOME = homedir();
|
|
7
|
-
const VENV_PYTHON = join(HOME, ".local/share/pipx/venvs/mempalace/bin/python3");
|
|
8
7
|
const MEMPALACE_BIN = join(HOME, ".local/bin/mempalace");
|
|
9
8
|
const OPENCODE_DB = join(HOME, ".local/share/opencode/opencode.db");
|
|
10
9
|
const STATE_FILE = join(HOME, ".mempalace/sync_state.json");
|
|
@@ -38,6 +37,45 @@ function hookLog(msg) {
|
|
|
38
37
|
}
|
|
39
38
|
catch { }
|
|
40
39
|
}
|
|
40
|
+
// Errors are never silent: hook.log is always written (unlike the
|
|
41
|
+
// DEBUG-gated log), so a broken pipeline is visible by default.
|
|
42
|
+
function errLog(msg) {
|
|
43
|
+
log("ERROR: " + msg);
|
|
44
|
+
hookLog("ERROR: " + msg);
|
|
45
|
+
}
|
|
46
|
+
// Probe for a working Python interpreter at startup instead of hardcoding
|
|
47
|
+
// one installer layout (pipx vs uv tool vs system). runPython only needs
|
|
48
|
+
// stdlib (sqlite3/json), so any python3 works. Priority: explicit env
|
|
49
|
+
// override, legacy pipx venv, uv tool venv, PATH fallback.
|
|
50
|
+
let resolvedPython = undefined;
|
|
51
|
+
function resolvePython() {
|
|
52
|
+
if (resolvedPython !== undefined)
|
|
53
|
+
return resolvedPython;
|
|
54
|
+
const candidates = [
|
|
55
|
+
process.env.MEMPALACE_PYTHON,
|
|
56
|
+
join(HOME, ".local/share/pipx/venvs/mempalace/bin/python3"),
|
|
57
|
+
join(HOME, ".local/share/uv/tools/mempalace/bin/python3"),
|
|
58
|
+
].filter((p) => !!p && existsSync(p));
|
|
59
|
+
if (candidates.length > 0) {
|
|
60
|
+
resolvedPython = candidates[0];
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
try {
|
|
64
|
+
execSync("python3 --version", { encoding: "utf-8", timeout: 10000 });
|
|
65
|
+
resolvedPython = "python3";
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
resolvedPython = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (resolvedPython) {
|
|
72
|
+
log("using python: " + resolvedPython);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
errLog("no working Python interpreter found (tried MEMPALACE_PYTHON, pipx venv, uv tool venv, PATH python3) — DB export disabled");
|
|
76
|
+
}
|
|
77
|
+
return resolvedPython;
|
|
78
|
+
}
|
|
41
79
|
let miningLock = false;
|
|
42
80
|
let lastSyncTs = 0;
|
|
43
81
|
let wakeupDone = false;
|
|
@@ -46,12 +84,19 @@ let wakeupDone = false;
|
|
|
46
84
|
// official Stop hook: the hook decides WHEN, the model decides WHAT).
|
|
47
85
|
let pendingCheckpoint = null;
|
|
48
86
|
function runPython(code) {
|
|
87
|
+
const python = resolvePython();
|
|
88
|
+
if (!python)
|
|
89
|
+
throw new Error("no working Python interpreter (see hook.log)");
|
|
49
90
|
writeFileSync(TMP_SCRIPT, code);
|
|
50
91
|
try {
|
|
51
|
-
|
|
92
|
+
// argv array, no shell (see PR #2): paths here are fixed, never user input.
|
|
93
|
+
return execFileSync(python, [TMP_SCRIPT], { encoding: "utf-8", timeout: 30000 }).trim();
|
|
52
94
|
}
|
|
53
95
|
finally {
|
|
54
|
-
|
|
96
|
+
try {
|
|
97
|
+
unlinkSync(TMP_SCRIPT);
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
55
100
|
}
|
|
56
101
|
}
|
|
57
102
|
function hasText(parts) {
|
|
@@ -99,7 +144,8 @@ function persistCounters(counters) {
|
|
|
99
144
|
}
|
|
100
145
|
function mempalaceWakeup() {
|
|
101
146
|
try {
|
|
102
|
-
|
|
147
|
+
// argv array, no shell.
|
|
148
|
+
const out = execFileSync(MEMPALACE_BIN, ["wake-up"], { encoding: "utf-8", timeout: 15000 }).trim();
|
|
103
149
|
if (!out)
|
|
104
150
|
return "";
|
|
105
151
|
return out.slice(0, MAX_WAKEUP_CHARS);
|
|
@@ -133,7 +179,9 @@ function readIdentity() {
|
|
|
133
179
|
}
|
|
134
180
|
function mempalaceSearch(query) {
|
|
135
181
|
try {
|
|
136
|
-
|
|
182
|
+
// argv array, no shell (see PR #2): the query is raw user message
|
|
183
|
+
// text, so it must never pass through /bin/sh. No manual escaping needed.
|
|
184
|
+
const out = execFileSync(MEMPALACE_BIN, ["search", query, "--results", String(MAX_SEARCH_RESULTS)], {
|
|
137
185
|
encoding: "utf-8",
|
|
138
186
|
timeout: 15000,
|
|
139
187
|
}).trim();
|
|
@@ -162,7 +210,7 @@ function dbSync() {
|
|
|
162
210
|
doDbSync();
|
|
163
211
|
}
|
|
164
212
|
catch (e) {
|
|
165
|
-
|
|
213
|
+
errLog("sync err: " + String(e));
|
|
166
214
|
}
|
|
167
215
|
}
|
|
168
216
|
function backfillRequested() {
|
|
@@ -270,8 +318,10 @@ function markSynced(now) {
|
|
|
270
318
|
// Official classification: decisions, preferences, milestones, problems,
|
|
271
319
|
// emotional context. Agent tag keeps opencode-mined drawers attributable.
|
|
272
320
|
// One wing per project (official multi-project pattern).
|
|
273
|
-
|
|
274
|
-
|
|
321
|
+
// Argv array, no shell (see PR #2): wing names are sanitized, but the
|
|
322
|
+
// spawn path stays shell-free regardless.
|
|
323
|
+
function mineArgs(wingDir, wing) {
|
|
324
|
+
return ["mine", wingDir, "--mode", "convos", "--extract", "general", "--agent", "opencode", "--wing", wing];
|
|
275
325
|
}
|
|
276
326
|
function cleanupExport(wings) {
|
|
277
327
|
for (const files of wings.values()) {
|
|
@@ -323,13 +373,16 @@ function doDbSync() {
|
|
|
323
373
|
return;
|
|
324
374
|
}
|
|
325
375
|
const [wing, files] = entries[i];
|
|
326
|
-
|
|
376
|
+
// No timeout here by design (see PR #4): Node would kill only the
|
|
377
|
+
// wrapper shell and orphan the python mine process, which keeps
|
|
378
|
+
// holding the palace lock while the next mine piles up. miningLock
|
|
379
|
+
// already serializes concurrent mines; long mines run to completion.
|
|
380
|
+
execFile(MEMPALACE_BIN, mineArgs(join(OUT_DIR, wing), wing), {
|
|
327
381
|
encoding: "utf-8",
|
|
328
|
-
timeout: 300000,
|
|
329
382
|
}, (err) => {
|
|
330
383
|
if (err) {
|
|
331
384
|
miningLock = false;
|
|
332
|
-
|
|
385
|
+
errLog(`mine err (${wing}): ${err.message}`);
|
|
333
386
|
return;
|
|
334
387
|
}
|
|
335
388
|
log(`mined wing ${wing} (${files.length} sessions)`);
|
|
@@ -347,12 +400,12 @@ function exitSync() {
|
|
|
347
400
|
return;
|
|
348
401
|
for (const [wing] of wings) {
|
|
349
402
|
log(`exit save: mining wing ${wing}`);
|
|
350
|
-
const res = spawnSync(MEMPALACE_BIN,
|
|
403
|
+
const res = spawnSync(MEMPALACE_BIN, mineArgs(join(OUT_DIR, wing), wing), {
|
|
351
404
|
encoding: "utf-8",
|
|
352
405
|
timeout: 60000,
|
|
353
406
|
});
|
|
354
407
|
if (res.error || res.status !== 0) {
|
|
355
|
-
|
|
408
|
+
errLog(`exit mine err (${wing}): ${String(res.error || res.status)}`);
|
|
356
409
|
return;
|
|
357
410
|
}
|
|
358
411
|
}
|
|
@@ -361,7 +414,7 @@ function exitSync() {
|
|
|
361
414
|
log("exit save done");
|
|
362
415
|
}
|
|
363
416
|
catch (e) {
|
|
364
|
-
|
|
417
|
+
errLog("exit save err: " + String(e));
|
|
365
418
|
}
|
|
366
419
|
}
|
|
367
420
|
export default (async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-mempalace-persistence",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "OpenCode plugin — auto-sync conversations to MemPalace memory in real-time. No forced wings, KG extraction via MCP tools.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|