#!/usr/bin/env node /** * ruflo H7 patch re-apply tool. * * H7: `ruflo memory store` routes through the AgentDB-v3 bridge * (memory-bridge.js:bridgeStoreEntry) which inserts into an in-RAM sql.js DB * and never flushes to disk — entries are lost between CLI invocations. * * Fix: force getBridge() in @claude-flow/cli's memory-initializer.js to * return null, so memory ops fall through to the raw sql.js path that DOES * persist (db.export() + writeFileRestricted). * * Both operations live in the GLOBAL npm install and are overwritten by * `ruflo update` — re-run this script after any ruflo upgrade. * Each apply/revert/check now covers BOTH the getBridge patch AND the * onnxruntime-node dedupe (disabling the nested @xenova/transformers copy). * * Usage: * node tools/ruflo-h7-patch.mjs apply both fixes (idempotent) * node tools/ruflo-h7-patch.mjs --check exit 0 if both applied, 1 if not * node tools/ruflo-h7-patch.mjs --revert remove both fixes * * See docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md §4 */ import { execSync } from 'node:child_process'; import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; export const PATCH_MARKER = 'LIDERRA-H7-PATCH'; export const PATCH_LINE = ' return null; /* LIDERRA-H7-PATCH: AgentDB bridge writes in-RAM sql.js, never flushes — force raw persisting path */'; const ANCHOR = 'async function getBridge() {'; /** True if the file content already contains the patch. */ export function isPatched(content) { return content.includes(PATCH_MARKER); } /** * Insert the patch line as the first statement of getBridge(). * Idempotent. Throws if the anchor is absent (ruflo upstream changed). */ export function applyPatch(content) { if (isPatched(content)) return { content, changed: false }; const idx = content.indexOf(ANCHOR); if (idx === -1) { throw new Error( `anchor "${ANCHOR}" not found — ruflo upstream changed getBridge(); revise this patch manually`, ); } const insertAt = idx + ANCHOR.length; const patched = content.slice(0, insertAt) + '\n' + PATCH_LINE + content.slice(insertAt); return { content: patched, changed: true }; } /** Remove the patch line (the line carrying PATCH_MARKER). */ export function revertPatch(content) { if (!isPatched(content)) return { content, changed: false }; const lines = content.split('\n'); const kept = lines.filter((l) => !l.includes(PATCH_MARKER)); return { content: kept.join('\n'), changed: true }; } /** * Resolve the path to @claude-flow/cli's memory-initializer.js inside the * global npm tree. Handles both nested (under ruflo/) and hoisted layouts. */ export function resolveTargetPath(npmRoot) { const tail = ['@claude-flow', 'cli', 'dist', 'src', 'memory', 'memory-initializer.js']; const candidates = [ join(npmRoot, 'ruflo', 'node_modules', ...tail), join(npmRoot, ...tail), ]; return candidates.find((c) => existsSync(c)) ?? null; } // --- onnxruntime-node dedupe (H7 fix, part 2) --- // ruflo's dependency tree carries two incompatible onnxruntime-node versions: // ruflo/node_modules/onnxruntime-node (1.24.3) // ruflo/node_modules/@xenova/transformers/node_modules/onnxruntime-node (1.14.0) // Both ship onnxruntime.dll; loading both into one process => DLL name // collision => ERR_DLOPEN_FAILED. Disabling the nested duplicate makes // @xenova/transformers resolve to the single hoisted 1.24.3. const DEDUPE_DISABLED_SUFFIX = '.h7disabled'; /** Resolve { active, disabled } paths for the duplicate nested onnxruntime-node. */ export function resolveDedupePaths(npmRoot) { const active = join( npmRoot, 'ruflo', 'node_modules', '@xenova', 'transformers', 'node_modules', 'onnxruntime-node', ); return { active, disabled: active + DEDUPE_DISABLED_SUFFIX }; } /** * Dedupe status: * 'deduped' — disabled dir exists, active does not (fix applied) * 'not-deduped' — active dir exists, disabled does not (fix not applied) * 'absent' — neither exists (no nested duplicate — nothing to dedupe) * 'inconsistent' — both exist (manual intervention required) */ export function dedupeStatus(active, disabled) { const a = existsSync(active); const d = existsSync(disabled); if (a && d) return 'inconsistent'; if (d) return 'deduped'; if (a) return 'not-deduped'; return 'absent'; } /** Disable the nested duplicate (rename active -> disabled). Idempotent. */ export function applyDedupe(active, disabled) { const status = dedupeStatus(active, disabled); if (status === 'deduped' || status === 'absent') return { changed: false, status }; if (status === 'inconsistent') { throw new Error( `dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`, ); } renameSync(active, disabled); return { changed: true, status: 'deduped' }; } /** Re-enable the nested duplicate (rename disabled -> active). */ export function revertDedupe(active, disabled) { const status = dedupeStatus(active, disabled); if (status === 'not-deduped' || status === 'absent') return { changed: false, status }; if (status === 'inconsistent') { throw new Error( `dedupe inconsistent — both "${active}" and "${disabled}" exist; resolve manually`, ); } renameSync(disabled, active); return { changed: true, status: 'not-deduped' }; } function main() { const mode = process.argv[2]; const VALID_MODES = [undefined, '--check', '--revert']; if (!VALID_MODES.includes(mode)) { console.error(`[ruflo-h7-patch] unknown flag: ${mode}`); console.error('Usage: node tools/ruflo-h7-patch.mjs [--check | --revert]'); process.exit(2); } let npmRoot; try { npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim(); } catch (e) { console.error('[ruflo-h7-patch] failed to run `npm root -g`: ' + e.message); process.exit(2); } const target = resolveTargetPath(npmRoot); if (!target) { console.error('[ruflo-h7-patch] memory-initializer.js not found under ' + npmRoot); process.exit(2); } const { active, disabled } = resolveDedupePaths(npmRoot); if (mode === '--check') { const patched = isPatched(readFileSync(target, 'utf8')); const dStatus = dedupeStatus(active, disabled); const dedupeOk = dStatus === 'deduped' || dStatus === 'absent'; console.log(`[ruflo-h7-patch] ${target}`); console.log(` getBridge patch: ${patched ? 'PATCHED' : 'UNPATCHED'}`); console.log(` onnxruntime dedupe: ${dStatus}`); process.exit(patched && dedupeOk ? 0 : 1); } if (mode === '--revert') { const r1 = revertPatch(readFileSync(target, 'utf8')); if (r1.changed) writeFileSync(target, r1.content); console.log(`[ruflo-h7-patch] revert getBridge: ${r1.changed ? 'patch removed' : 'was not patched'}`); try { const r2 = revertDedupe(active, disabled); console.log(`[ruflo-h7-patch] revert dedupe: ${r2.changed ? 're-enabled nested onnxruntime-node' : r2.status}`); } catch (e) { console.error('[ruflo-h7-patch] ' + e.message); process.exit(3); } process.exit(0); } // default: apply try { const r1 = applyPatch(readFileSync(target, 'utf8')); if (r1.changed) writeFileSync(target, r1.content); console.log(`[ruflo-h7-patch] apply getBridge: ${r1.changed ? 'patched' : 'already patched'}`); const r2 = applyDedupe(active, disabled); console.log(`[ruflo-h7-patch] apply dedupe: ${r2.changed ? 'disabled nested onnxruntime-node' : r2.status}`); process.exit(0); } catch (e) { console.error('[ruflo-h7-patch] ' + e.message); process.exit(3); } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main(); }