mneme-ai 2.75.0 → 2.75.2

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.
@@ -36,6 +36,29 @@
36
36
  * (preinstall runs before deps are installed) — node builtins only. It is
37
37
  * ALSO importable: `require()` exposes the pure functions for the test suite
38
38
  * without running the IO side effects (guarded by require.main === module).
39
+ *
40
+ * RELATIONSHIP TO THE SHIPPED preinstall. This module is the unit-tested +
41
+ * SUPER-QUAN-probed REFERENCE implementation of the FULL reaper (incl. the
42
+ * cmdline-match daemon kill), AND a runtime helper for `mneme upgrade` (which
43
+ * runs in a normal node context, free of the cmd.exe constraints below).
44
+ *
45
+ * It is NOT what npm runs at preinstall. The shipped `scripts.preinstall` is a
46
+ * LEAN inline `node -e` that does the cmd-SAFE subset: trail + image-kill +
47
+ * heartbeat-PID reap (which already covers `node.exe …mneme.js`, since the
48
+ * daemon registers a .beat) + Handle-Oracle DLL gate + sweep. It deliberately
49
+ * OMITS the cmdline-match OS-process query, because that needs PowerShell/wmic
50
+ * with pipes/quotes that DO NOT survive `cmd /d /s /c "node -e \"…\""`:
51
+ * • v2.75.0 — generated the inline from this whole 18.5 KB file → exceeded
52
+ * the Windows cmd.exe ~8191-char limit → "command line is too long".
53
+ * • v2.75.1 — a leaner inline still embedded a PowerShell `… | Select-Object`
54
+ * with literal double-quotes; the `"` broke cmd quoting and exposed `|` to
55
+ * cmd → "'Select-Object' is not recognized" → uninstallable on Windows.
56
+ * Lesson: the inline must be < 8000 chars AND contain ZERO literal `"`. Guards:
57
+ * probe.preinstall.reaps_node_daemon (length + no-double-quote + markers) and
58
+ * P7 (incl. a faithful `cmd /d /s /c` smoke). Referencing a package file from
59
+ * preinstall is also out (v2.19.48/49 scar: file may not exist pre-extract).
60
+ * The cmdline-match safety-net therefore lives HERE, for the `mneme upgrade`
61
+ * path, not in the npm preinstall.
39
62
  */
40
63
 
41
64
  "use strict";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mneme-ai",
3
- "version": "2.75.0",
3
+ "version": "2.75.2",
4
4
  "mcpName": "io.github.patsa2561-art/mneme-ai",
5
5
  "description": "Mneme — the memory layer for your codebase. Knows the WHY, the WHAT, the WHERE-IT-BREAKS.",
6
6
  "type": "module",
@@ -29,13 +29,13 @@
29
29
  "build": "tsc -b",
30
30
  "clean": "tsc -b --clean",
31
31
  "postinstall": "node bin/postinstall-mneme-lite.cjs",
32
- "preinstall": "node -e \"/**\\n * v2.75.0 — MNEME preinstall reaper (HANDLE-ORACLE + PID-LEASE + CMDLINE-MATCH).\\n *\\n * THE BUG THIS CLOSES. The old inline preinstall did `taskkill /F /IM\\n * mneme.exe /T` — but the Mneme daemon on Windows runs as\\n * `node.exe …\\\\bin\\\\mneme.js nucleus daemon`, whose IMAGE NAME is\\n * `node.exe`, NOT `mneme.exe`. So image-name kill never touched it. A\\n * daemon that loaded a native module (libvips-42.dll via the OPTIONAL\\n * @huggingface/transformers→sharp path) keeps that DLL memory-mapped and\\n * locked → `npm i -g mneme-ai` fails with EBUSY when it tries to\\n * overwrite the DLL.\\n *\\n * THE FIX — three deterministic, pure-JS layers (no native, no deps):\\n *\\n * 1. PID-LEASE read ~/.mneme-global/heartbeats/*.beat — every\\n * live Mneme process leases its {pid, holdsPaths}\\n * there. Authoritative for daemons that registered.\\n *\\n * 2. CMDLINE-MATCH query the OS process table (wmic → PowerShell on\\n * Windows; `ps` on POSIX) and kill any process whose\\n * COMMAND LINE looks like a Mneme daemon\\n * (`mneme.js`/`nucleus daemon`) even if it never\\n * wrote a heartbeat (old version, crashed registry,\\n * deleted beat file). This is the real node.exe fix.\\n *\\n * 3. HANDLE-ORACLE replace the old blind `wait(300)`/`wait(500)` with\\n * a DETERMINISTIC gate: loop `fs.openSync(dll,'r+')`\\n * (exclusive on Windows) and proceed the instant the\\n * OS confirms the handle is free — proof, not hope.\\n * Falls back to the v2.19.61 rename-sideways trick if\\n * the handle never frees.\\n *\\n * CONTRACT: this script must NEVER block an install. Every path is wrapped\\n * best-effort and the process ALWAYS exits 0. It depends on ZERO node_modules\\n * (preinstall runs before deps are installed) — node builtins only. It is\\n * ALSO importable: `require()` exposes the pure functions for the test suite\\n * without running the IO side effects (guarded by require.main === module).\\n */\\n\\n\\\"use strict\\\";\\n\\nconst fs = require(\\\"node:fs\\\");\\nconst path = require(\\\"node:path\\\");\\nconst os = require(\\\"node:os\\\");\\nconst { spawnSync } = require(\\\"node:child_process\\\");\\nconst crypto = require(\\\"node:crypto\\\");\\n\\nconst IS_WIN = process.platform === \\\"win32\\\";\\n\\n/* ── tiny helpers ─────────────────────────────────────────────────────── */\\n\\n/** Bounded synchronous sleep. preinstall must be synchronous (npm waits on\\n * the process), so we busy-wait — but the HANDLE-ORACLE exits early the\\n * instant the lock frees, so total spin time is normally a few ms. */\\nfunction busyWait(ms) {\\n const end = Date.now() + Math.max(0, ms);\\n while (Date.now() < end) { /* spin */ }\\n}\\n\\nfunction organDir() { return path.join(os.homedir(), \\\".mneme-global\\\"); }\\nfunction heartbeatDir() { return path.join(organDir(), \\\"heartbeats\\\"); }\\n\\n/* ── PURE FUNCTION 1: does a command line look like a Mneme daemon? ────────\\n *\\n * The daemon is spawned as `node …/bin/mneme.js nucleus daemon [--detach]`\\n * (see nucleus_daemon.ts). We match the SCRIPT path + the daemon subcommand,\\n * NOT a bare \\\"mneme\\\" substring (which would false-positive on, say, an editor\\n * opened on a file called mneme.ts, or this very preinstall process). */\\nfunction matchesMnemeDaemonCmdline(cmdline) {\\n if (typeof cmdline !== \\\"string\\\" || cmdline.length === 0) return false;\\n const c = cmdline.toLowerCase();\\n // Never match the installer/preinstall itself.\\n if (c.includes(\\\"preinstall-mneme\\\") || c.includes(\\\"postinstall-mneme\\\")) return false;\\n // Must reference the mneme CLI entrypoint (bin/mneme.js or a `mneme` shim\\n // invoking a .js) AND a daemon-shaped subcommand.\\n const refsMnemeJs = /(^|[\\\\\\\\/\\\\s\\\"'])mneme\\\\.js([\\\\s\\\"']|$)/.test(c) || c.includes(\\\"bin\\\\\\\\mneme.js\\\") || c.includes(\\\"bin/mneme.js\\\") || c.includes(\\\"mneme-ai\\\");\\n const isDaemonish = c.includes(\\\"nucleus\\\") || c.includes(\\\"daemon\\\") || c.includes(\\\"--detach\\\");\\n return refsMnemeJs && isDaemonish;\\n}\\n\\n/* ── PURE FUNCTION 2: parse the Windows `wmic … /format:csv` table ────────\\n *\\n * wmic CSV columns are: Node,CommandLine,ProcessId (header order varies, so\\n * we locate columns by header name). CommandLine itself can contain commas,\\n * so we DON'T naive-split: we take the FIRST field (Node=hostname) and the\\n * LAST field (ProcessId) and treat everything between as the command line. */\\nfunction parseWmicCsv(stdout) {\\n if (typeof stdout !== \\\"string\\\") return [];\\n const lines = stdout.split(/\\\\r?\\\\n/).map((l) => l.trim()).filter(Boolean);\\n if (lines.length < 2) return [];\\n // Find header row (contains \\\"ProcessId\\\").\\n let headerIdx = lines.findIndex((l) => /ProcessId/i.test(l) && l.includes(\\\",\\\"));\\n if (headerIdx < 0) return [];\\n const out = [];\\n for (let i = headerIdx + 1; i < lines.length; i++) {\\n const row = lines[i];\\n const first = row.indexOf(\\\",\\\");\\n const last = row.lastIndexOf(\\\",\\\");\\n if (first < 0 || last <= first) continue;\\n const pid = parseInt(row.slice(last + 1).trim(), 10);\\n const cmdline = row.slice(first + 1, last).trim();\\n if (Number.isFinite(pid) && pid > 0) out.push({ pid, cmdline });\\n }\\n return out;\\n}\\n\\n/* ── PURE FUNCTION 3: parse PowerShell CSV (ProcessId,CommandLine) ──────── */\\nfunction parsePowershellCsv(stdout) {\\n if (typeof stdout !== \\\"string\\\") return [];\\n const lines = stdout.split(/\\\\r?\\\\n/).map((l) => l.trim()).filter(Boolean);\\n const out = [];\\n for (const line of lines) {\\n // \\\"<pid>\\\",\\\"<cmdline maybe with commas>\\\"\\n const m = line.match(/^\\\"?(\\\\d+)\\\"?\\\\s*,\\\\s*\\\"?(.*?)\\\"?$/);\\n if (m) {\\n const pid = parseInt(m[1], 10);\\n if (Number.isFinite(pid) && pid > 0) out.push({ pid, cmdline: m[2] || \\\"\\\" });\\n }\\n }\\n return out;\\n}\\n\\n/* ── PURE FUNCTION 4: parse POSIX `ps -eo pid=,args=` ───────────────────── */\\nfunction parsePosixPs(stdout) {\\n if (typeof stdout !== \\\"string\\\") return [];\\n const out = [];\\n for (const line of stdout.split(/\\\\r?\\\\n/)) {\\n const t = line.trim();\\n if (!t) continue;\\n const m = t.match(/^(\\\\d+)\\\\s+(.*)$/);\\n if (m) {\\n const pid = parseInt(m[1], 10);\\n if (Number.isFinite(pid) && pid > 0) out.push({ pid, cmdline: m[2] });\\n }\\n }\\n return out;\\n}\\n\\n/* ── PURE FUNCTION 5: select daemon PIDs to kill (exclude self) ──────────── */\\nfunction selectDaemonPids(procs, selfPid) {\\n if (!Array.isArray(procs)) return [];\\n const seen = new Set();\\n const out = [];\\n for (const p of procs) {\\n if (!p || typeof p.pid !== \\\"number\\\" || p.pid <= 0) continue;\\n if (p.pid === selfPid) continue;\\n if (!matchesMnemeDaemonCmdline(p.cmdline)) continue;\\n if (seen.has(p.pid)) continue;\\n seen.add(p.pid);\\n out.push(p.pid);\\n }\\n return out;\\n}\\n\\n/* ── PURE FUNCTION 6: known libvips/native DLL candidate paths ───────────── */\\nfunction libvipsDllCandidates(npmGlobalPkgDir, isWindows) {\\n if (!isWindows) return [];\\n const nm = path.join(npmGlobalPkgDir, \\\"node_modules\\\");\\n return [\\n path.join(nm, \\\"@img\\\", \\\"sharp-libvips-win32-x64\\\", \\\"lib\\\", \\\"libvips-42.dll\\\"),\\n path.join(nm, \\\"@img\\\", \\\"sharp-libvips-win32-x64\\\", \\\"lib\\\", \\\"libvips-cpp-8.17.3.dll\\\"),\\n path.join(nm, \\\"sharp\\\", \\\"build\\\", \\\"Release\\\", \\\"sharp-win32-x64.node\\\"),\\n ];\\n}\\n\\n/* ── HANDLE-ORACLE: deterministic \\\"is this file unlocked?\\\" ────────────────\\n *\\n * On Windows, fs.openSync(path,'r+') fails with EBUSY/EPERM while another\\n * process holds the file mapped. Success = nobody holds it = safe to\\n * overwrite. We inject openFn for testing. Returns true if free / absent. */\\nfunction tryExclusiveOpen(filePath, openFn) {\\n const open = openFn || ((p) => { const fd = fs.openSync(p, \\\"r+\\\"); fs.closeSync(fd); });\\n try {\\n if (!fs.existsSync(filePath)) return true; // nothing to lock\\n } catch { /* fall through to open attempt */ }\\n try { open(filePath); return true; }\\n catch { return false; }\\n}\\n\\n/** Loop tryExclusiveOpen until released or tries exhausted. Deterministic\\n * proof-of-release; returns the moment the OS frees the handle. */\\nfunction waitForHandleRelease(filePath, opts) {\\n const o = opts || {};\\n const tries = typeof o.tries === \\\"number\\\" ? o.tries : 40;\\n const intervalMs = typeof o.intervalMs === \\\"number\\\" ? o.intervalMs : 50;\\n const openFn = o.openFn;\\n const sleep = o.sleep || busyWait;\\n for (let i = 1; i <= tries; i++) {\\n if (tryExclusiveOpen(filePath, openFn)) return { released: true, attempts: i };\\n if (i < tries) sleep(intervalMs);\\n }\\n return { released: false, attempts: tries };\\n}\\n\\n/* ── IO: read PID leases from the heartbeat registry ─────────────────────── */\\nfunction readHeartbeatLeases(dir) {\\n const d = dir || heartbeatDir();\\n const out = [];\\n let entries;\\n try { entries = fs.readdirSync(d); } catch { return out; }\\n for (const f of entries) {\\n if (!f.endsWith(\\\".beat\\\")) continue;\\n try {\\n const beat = JSON.parse(fs.readFileSync(path.join(d, f), \\\"utf8\\\"));\\n if (typeof beat.pid === \\\"number\\\" && beat.pid > 0) {\\n out.push({ pid: beat.pid, holdsPaths: Array.isArray(beat.holdsPaths) ? beat.holdsPaths : [], beatFile: path.join(d, f) });\\n }\\n } catch { /* corrupt — skip */ }\\n }\\n return out;\\n}\\n\\n/* ── IO: query the OS process table (best-effort) ────────────────────────── */\\nfunction queryProcessTable() {\\n try {\\n if (IS_WIN) {\\n // Prefer wmic (fast, present on most Windows); fall back to PowerShell\\n // CIM (wmic is removed on some Win11 builds).\\n const w = spawnSync(\\\"wmic\\\", [\\\"process\\\", \\\"get\\\", \\\"Name,ProcessId,CommandLine\\\", \\\"/format:csv\\\"],\\n { shell: true, windowsHide: true, timeout: 6000, encoding: \\\"utf8\\\" });\\n if (w.status === 0 && typeof w.stdout === \\\"string\\\" && /ProcessId/i.test(w.stdout)) {\\n return parseWmicCsv(w.stdout);\\n }\\n const ps = spawnSync(\\\"powershell\\\",\\n [\\\"-NoProfile\\\", \\\"-NonInteractive\\\", \\\"-Command\\\",\\n \\\"Get-CimInstance Win32_Process -Filter \\\\\\\"Name='node.exe'\\\\\\\" | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation\\\"],\\n { shell: false, windowsHide: true, timeout: 8000, encoding: \\\"utf8\\\" });\\n if (typeof ps.stdout === \\\"string\\\") return parsePowershellCsv(ps.stdout);\\n return [];\\n }\\n const r = spawnSync(\\\"ps\\\", [\\\"-eo\\\", \\\"pid=,args=\\\"], { timeout: 6000, encoding: \\\"utf8\\\" });\\n if (typeof r.stdout === \\\"string\\\") return parsePosixPs(r.stdout);\\n return [];\\n } catch { return []; }\\n}\\n\\n/* ── IO: kill a list of PIDs (Windows-correct taskkill /F /PID) ──────────── */\\nfunction killPids(pids, isWindows) {\\n const win = typeof isWindows === \\\"boolean\\\" ? isWindows : IS_WIN;\\n const out = [];\\n for (const pid of pids || []) {\\n if (typeof pid !== \\\"number\\\" || pid <= 0 || pid === process.pid) { out.push({ pid, killed: false, reason: \\\"skip\\\" }); continue; }\\n try {\\n if (win) {\\n const r = spawnSync(\\\"taskkill\\\", [\\\"/F\\\", \\\"/PID\\\", String(pid), \\\"/T\\\"], { shell: true, windowsHide: true, timeout: 4000, stdio: \\\"ignore\\\" });\\n out.push({ pid, killed: r.status === 0 });\\n } else {\\n try { process.kill(pid, \\\"SIGTERM\\\"); } catch { /* */ }\\n busyWait(120);\\n try { process.kill(pid, \\\"SIGKILL\\\"); } catch { /* */ }\\n out.push({ pid, killed: true });\\n }\\n } catch { out.push({ pid, killed: false, reason: \\\"throw\\\" }); }\\n }\\n return out;\\n}\\n\\n/* ── HMAC-chained trail (tamper-evident install breadcrumb) ──────────────── */\\nfunction makeTrail(organ, version) {\\n const trailPath = path.join(organ, \\\"preinstall-trail.jsonl\\\");\\n const secret = process.env[\\\"MNEME_PREINSTALL_TRAIL_SECRET\\\"] || \\\"mneme-preinstall-trail-v1\\\";\\n const lastSig = () => {\\n try {\\n if (!fs.existsSync(trailPath)) return \\\"genesis\\\";\\n const lines = fs.readFileSync(trailPath, \\\"utf8\\\").trim().split(\\\"\\\\n\\\").filter(Boolean);\\n if (lines.length === 0) return \\\"genesis\\\";\\n const last = JSON.parse(lines[lines.length - 1]);\\n return typeof last.sig === \\\"string\\\" ? last.sig : \\\"genesis\\\";\\n } catch { return \\\"genesis\\\"; }\\n };\\n return (step, ok, details) => {\\n try {\\n const prevSig = lastSig();\\n const body = { v: 2, ts: new Date().toISOString(), version, step, ok, ...(details ? { details } : {}), pid: process.pid, prevSig };\\n const sig = crypto.createHmac(\\\"sha256\\\", secret).update(prevSig + \\\"::\\\" + JSON.stringify(body)).digest(\\\"hex\\\");\\n fs.appendFileSync(trailPath, JSON.stringify({ ...body, sig }) + \\\"\\\\n\\\", \\\"utf8\\\");\\n } catch { /* trail is best-effort */ }\\n };\\n}\\n\\n/* ── ORCHESTRATOR ───────────────────────────────────────────────────────── */\\nfunction runPreinstall(opts) {\\n const o = opts || {};\\n const home = os.homedir();\\n const organ = o.organDir || organDir();\\n const beatDir = o.heartbeatDir || heartbeatDir();\\n // Injection points (default to real IO; tests override for hermeticity).\\n const queryProcs = o.queryProcs || queryProcessTable;\\n const killFn = o.killFn || killPids;\\n const version = process.env[\\\"npm_package_version\\\"] || \\\"unknown\\\";\\n const result = { ok: true, version, killedPids: [], handleOracle: [], renamed: 0, swept: 0, leasePids: [], cmdlinePids: [] };\\n try { if (!fs.existsSync(organ)) fs.mkdirSync(organ, { recursive: true, mode: 0o700 }); } catch { /* */ }\\n const trail = makeTrail(organ, version);\\n trail(\\\"preinstall-start\\\", true, { v: \\\"2.75.0\\\", reaper: true });\\n\\n // 1. announce incoming install (other Mneme processes back off on seeing this).\\n try {\\n fs.writeFileSync(path.join(organ, \\\"install-incoming.flag\\\"),\\n JSON.stringify({ v: 2, announcedAt: new Date().toISOString(), announcerPid: process.pid, reason: \\\"preinstall-hook\\\" }),\\n { encoding: \\\"utf8\\\", mode: 0o600 });\\n trail(\\\"flag-written\\\", true);\\n } catch { trail(\\\"flag-written\\\", false); }\\n\\n // 2. image-name kill (legacy; catches a real mneme.exe shim if one exists).\\n if (IS_WIN && o.imageKill !== false) {\\n try { spawnSync(\\\"taskkill\\\", [\\\"/F\\\", \\\"/IM\\\", \\\"mneme.exe\\\", \\\"/T\\\"], { shell: true, windowsHide: true, timeout: 5000, stdio: \\\"ignore\\\" }); } catch { /* */ }\\n }\\n\\n // 3. PID-LEASE: kill every daemon that leased a heartbeat (authoritative).\\n const leases = readHeartbeatLeases(beatDir);\\n const leasePids = leases.map((l) => l.pid).filter((p) => p !== process.pid);\\n result.leasePids = leasePids.slice();\\n for (const l of leases) { if (l.beatFile) { try { fs.unlinkSync(l.beatFile); } catch { /* */ } } }\\n const killedLease = killFn(leasePids);\\n\\n // 4. CMDLINE-MATCH: the real node.exe fix — kill daemons NOT in the\\n // registry (old version / crashed / deleted beat) by matching cmdline.\\n const procs = queryProcs();\\n const cmdlinePids = selectDaemonPids(procs, process.pid).filter((p) => !leasePids.includes(p));\\n result.cmdlinePids = cmdlinePids.slice();\\n const killedCmd = killFn(cmdlinePids);\\n result.killedPids = killedLease.concat(killedCmd).filter((k) => k.killed).map((k) => k.pid);\\n trail(\\\"daemons-reaped\\\", true, { lease: leasePids.length, cmdline: cmdlinePids.length, killed: result.killedPids.length });\\n\\n // 5. HANDLE-ORACLE: deterministically wait for the native DLLs to free,\\n // then rename-sideways only if the lock genuinely persists.\\n let renamed = 0;\\n const dllPrefixes = o.npmGlobalDirs || defaultNpmGlobalDirs(home);\\n for (const pkgDir of dllPrefixes) {\\n for (const dll of libvipsDllCandidates(pkgDir, IS_WIN)) {\\n let present = false;\\n try { present = fs.existsSync(dll); } catch { /* */ }\\n if (!present) continue;\\n const gate = waitForHandleRelease(dll, { tries: 40, intervalMs: 50 });\\n result.handleOracle.push({ dll, released: gate.released, attempts: gate.attempts });\\n if (!gate.released) {\\n // Lock never freed — fall back to the v2.19.61 rename-sideways trick\\n // so npm can lay down the new DLL beside the locked (orphaned) one.\\n try { fs.renameSync(dll, dll + \\\".locked-\\\" + Date.now() + \\\"-\\\" + process.pid); renamed++; } catch { /* */ }\\n }\\n }\\n }\\n result.renamed = renamed;\\n trail(\\\"handle-oracle\\\", true, { checked: result.handleOracle.length, renamed });\\n\\n // 6. sweep abandoned npm staging dirs (.mneme-ai-* leftovers).\\n let swept = 0;\\n try {\\n for (const npmParent of (o.sweepDirs || defaultNpmNodeModules(home))) {\\n let entries;\\n try { entries = fs.readdirSync(npmParent); } catch { continue; }\\n for (const entry of entries) {\\n if (entry.startsWith(\\\".mneme-ai-\\\")) {\\n try { fs.rmSync(path.join(npmParent, entry), { recursive: true, force: true }); swept++; } catch { /* */ }\\n }\\n }\\n }\\n } catch { /* */ }\\n result.swept = swept;\\n trail(\\\"staging-swept\\\", true, { swept });\\n trail(\\\"preinstall-end\\\", true);\\n return result;\\n}\\n\\n/* ── npm global location heuristics (Windows + POSIX, multi-version-mgr) ──── */\\nfunction defaultNpmGlobalDirs(home) {\\n const dirs = [];\\n const push = (p) => { if (p) dirs.push(path.join(p, \\\"mneme-ai\\\")); };\\n if (IS_WIN) {\\n push(path.join(home, \\\"AppData\\\", \\\"Roaming\\\", \\\"npm\\\", \\\"node_modules\\\"));\\n push(path.join(path.dirname(process.execPath), \\\"node_modules\\\"));\\n } else {\\n push(\\\"/usr/local/lib/node_modules\\\");\\n push(\\\"/usr/lib/node_modules\\\");\\n push(path.join(home, \\\".npm-global\\\", \\\"node_modules\\\"));\\n }\\n return dirs;\\n}\\nfunction defaultNpmNodeModules(home) {\\n if (IS_WIN) {\\n return [path.join(home, \\\"AppData\\\", \\\"Roaming\\\", \\\"npm\\\", \\\"node_modules\\\"), path.join(path.dirname(process.execPath), \\\"node_modules\\\")];\\n }\\n return [\\\"/usr/local/lib/node_modules\\\", path.join(home, \\\".npm-global\\\", \\\"node_modules\\\")];\\n}\\n\\n/* ── exports (for the test suite) ───────────────────────────────────────── */\\nmodule.exports = {\\n matchesMnemeDaemonCmdline,\\n parseWmicCsv,\\n parsePowershellCsv,\\n parsePosixPs,\\n selectDaemonPids,\\n libvipsDllCandidates,\\n tryExclusiveOpen,\\n waitForHandleRelease,\\n readHeartbeatLeases,\\n queryProcessTable,\\n killPids,\\n runPreinstall,\\n};\\n\\n\\ntry { runPreinstall(); } catch (_e) { /* never block install */ }\\nprocess.exit(0);\\n\""
32
+ "preinstall": "node -e \"try{const fs=require('node:fs');const path=require('node:path');const os=require('node:os');const{spawnSync}=require('node:child_process');const crypto=require('node:crypto');const w=process.platform==='win32';const home=os.homedir();const organ=path.join(home,'.mneme-global');const trailPath=path.join(organ,'preinstall-trail.jsonl');const trailSecret=process.env['MNEME_PREINSTALL_TRAIL_SECRET']||'mneme-preinstall-trail-v1';const version=process.env['npm_package_version']||'unknown';try{if(!fs.existsSync(organ))fs.mkdirSync(organ,{recursive:true,mode:0o700})}catch(e){}const lastSig=()=>{try{if(!fs.existsSync(trailPath))return'genesis';const lines=fs.readFileSync(trailPath,'utf8').trim().split('\\\\n').filter(Boolean);if(lines.length===0)return'genesis';const last=JSON.parse(lines[lines.length-1]);return typeof last?.sig==='string'?last.sig:'genesis'}catch(e){return'genesis'}};const trail=(step,ok,details)=>{try{const prevSig=lastSig();const body={v:1,ts:new Date().toISOString(),version,step,ok,...(details?{details}:{}),pid:process.pid,prevSig};const sig=crypto.createHmac('sha256',trailSecret).update(prevSig+'::'+JSON.stringify(body)).digest('hex');fs.appendFileSync(trailPath,JSON.stringify({...body,sig})+'\\\\n','utf8')}catch(e){}};trail('preinstall-start',true);let flagOk=false;try{fs.writeFileSync(path.join(organ,'install-incoming.flag'),JSON.stringify({v:1,announcedAt:new Date().toISOString(),announcerPid:process.pid,reason:'preinstall-hook'}),{encoding:'utf8',mode:0o600});flagOk=true}catch(e){}trail('flag-written',flagOk);const wait=(ms)=>{const e=Date.now()+ms;while(Date.now()<e){}};wait(300);if(w){const r=spawnSync('taskkill',['/F','/IM','mneme.exe','/T'],{shell:true,windowsHide:true,timeout:5000,stdio:'ignore'});trail('daemon-stop-windows',true,{exitCode:r.status});let reaped=0;try{const beatDir=path.join(organ,'heartbeats');if(fs.existsSync(beatDir)){for(const f of fs.readdirSync(beatDir)){const m=f.match(/^(\\\\d+)\\\\.beat$/);if(m){const pid=parseInt(m[1]);if(pid>0&&pid!==process.pid){spawnSync('taskkill',['/F','/PID',pid.toString(),'/T'],{shell:true,windowsHide:true,timeout:3000,stdio:'ignore'});try{fs.unlinkSync(path.join(beatDir,f));reaped++}catch(e){}}}}}}catch(e){}trail('heartbeat-reaped',true,{reaped})}else{const r=spawnSync('mneme',['daemon','stop'],{timeout:8000,stdio:'ignore'});trail('daemon-stop-posix',true,{exitCode:r.status});let reaped=0;try{const beatDir=path.join(organ,'heartbeats');if(fs.existsSync(beatDir)){for(const f of fs.readdirSync(beatDir)){const m=f.match(/^(\\\\d+)\\\\.beat$/);if(m){const pid=parseInt(m[1]);if(pid>0&&pid!==process.pid){try{process.kill(pid,'SIGTERM')}catch(e){}wait(100);try{process.kill(pid,'SIGKILL')}catch(e){}try{fs.unlinkSync(path.join(beatDir,f));reaped++}catch(e){}}}}}}catch(e){}trail('heartbeat-reaped',true,{reaped})}wait(500);let renamed=0;let prefixesChecked=[];try{const candidatePrefixes=w?[path.join(home,'AppData','Roaming','npm'),path.dirname(process.execPath),'C:\\\\\\\\nvm4w\\\\\\\\nodejs',path.join(home,'AppData','Local','nvm')]:['/usr/local/lib','/usr/lib',path.join(home,'.npm-global'),path.join(home,'.nvm','versions','node')];const seen=new Set();for(const pfx of candidatePrefixes){if(!fs.existsSync(pfx))continue;let nodeModulesBases=[];if(fs.existsSync(path.join(pfx,'node_modules')))nodeModulesBases.push(path.join(pfx,'node_modules'));try{for(const entry of fs.readdirSync(pfx)){const sub=path.join(pfx,entry,'node_modules');if(fs.existsSync(sub))nodeModulesBases.push(sub);const sub2=path.join(pfx,entry,'nodejs','node_modules');if(fs.existsSync(sub2))nodeModulesBases.push(sub2)}}catch(e){}for(const nm of nodeModulesBases){if(seen.has(nm))continue;seen.add(nm);prefixesChecked.push(nm);const npmGlobal=path.join(nm,'mneme-ai');if(!fs.existsSync(npmGlobal))continue;const dllPaths=w?[path.join(npmGlobal,'node_modules','@img','sharp-libvips-win32-x64','lib','libvips-42.dll'),path.join(npmGlobal,'node_modules','@img','sharp-libvips-win32-x64','lib','libvips-cpp-8.17.3.dll'),path.join(npmGlobal,'node_modules','sharp','build','Release','sharp-win32-x64.node')]:[];for(const dll of dllPaths){if(!fs.existsSync(dll))continue;let freed=false;for(let i=0;i<40;i++){try{const fd=fs.openSync(dll,'r+');fs.closeSync(fd);freed=true;break}catch(e2){wait(50)}}if(!freed){try{fs.renameSync(dll,dll+'.locked-'+Date.now()+'-'+process.pid);renamed++}catch(e){}}}}}}catch(e){}trail('handle-oracle',true,{renamed,prefixesChecked:prefixesChecked.length});let swept=0;try{const candidates=w?[path.join(home,'AppData','Roaming','npm','node_modules'),path.join(path.dirname(process.execPath),'node_modules')]:['/usr/local/lib/node_modules',path.join(home,'.npm-global','node_modules')];for(const npmParent of candidates){if(!fs.existsSync(npmParent))continue;try{for(const entry of fs.readdirSync(npmParent)){if(entry.startsWith('.mneme-ai-')){try{fs.rmSync(path.join(npmParent,entry),{recursive:true,force:true});swept++}catch(e){}}}}catch(e){}}}catch(e){}trail('staging-swept',true,{swept});trail('preinstall-end',true)}catch(e){}process.exit(0)\""
33
33
  },
34
34
  "dependencies": {
35
- "@mneme-ai/core": "2.75.0",
36
- "@mneme-ai/correlator": "2.75.0",
37
- "@mneme-ai/embeddings": "2.75.0",
38
- "@mneme-ai/mcp": "2.75.0",
35
+ "@mneme-ai/core": "2.75.2",
36
+ "@mneme-ai/correlator": "2.75.2",
37
+ "@mneme-ai/embeddings": "2.75.2",
38
+ "@mneme-ai/mcp": "2.75.2",
39
39
  "commander": "^14.0.3",
40
40
  "kleur": "^4.1.5"
41
41
  },