Wraps the global fs functions with a 1s TTL memo, scoped via path whitelist to plugin-discovery paths only. Workaround for upstream openclaw issue #86791: `loadPluginMetadataSnapshot()`'s cache-validity check re-runs `hashWatchedFiles` on every lookup, which walks every plugin's package.json + manifest + source via realpathSync -> ancestor lstat chain. On prod t2 with ~100 plugins, one cache-check pass is ~6 400 lstat + ~400 stat (~6-7s CPU per call). Fires on every agent turn, every loadConfig() call, every channel routing decision. This plugin doesn't fix the upstream design; it just absorbs the repeated stats within a 1s window so the same paths aren't re-statted 6× per second during a discovery walk. Verified on prod t2 (2026-05-27): - Cache hit ratio: 92.1-98.2% (stable across windows) - Idle baseline (0 turn, 0 push): 0.6-3.7% CPU (was 25%+ pre-fix) - Per-turn cost: notably reduced; previously 100% sustained per turn Path whitelist: - /openclaw/dist/extensions/ - /.openclaw/plugins/ - /node_modules/@openclaw/ - /openclaw/plugin-sdk/ All other paths pass through to original fs functions unchanged. Manifest requires `activation.onStartup: true` so openclaw register()s the plugin even though it exposes no tools/contracts (otherwise jiti caches the module without ever calling register). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
3.8 KiB
JavaScript
112 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Install / uninstall / update OpenClaw Perf Cache plugin.
|
|
*
|
|
* Usage:
|
|
* node scripts/install.mjs --install # build + copy + register in openclaw.json
|
|
* node scripts/install.mjs --uninstall # remove plugin + unregister
|
|
* node scripts/install.mjs --update # rebuild + copy (no config touch)
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { execSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const projRoot = path.resolve(__dirname, '..');
|
|
const pluginSrcDir = path.join(projRoot, 'plugin');
|
|
|
|
const PLUGIN_ID = 'openclaw-perf-cache';
|
|
const ocDir = path.join(process.env.HOME || '/root', '.openclaw');
|
|
const pluginsDir = path.join(ocDir, 'plugins');
|
|
const installDir = path.join(pluginsDir, PLUGIN_ID);
|
|
const configPath = path.join(ocDir, 'openclaw.json');
|
|
|
|
const action = process.argv[2];
|
|
if (!action || !['--install', '--uninstall', '--update'].includes(action)) {
|
|
console.log('Usage: node scripts/install.mjs --install | --uninstall | --update');
|
|
process.exit(1);
|
|
}
|
|
|
|
function build() {
|
|
console.log('Building plugin TypeScript...');
|
|
execSync('npx tsc -p tsconfig.json', { cwd: pluginSrcDir, stdio: 'inherit' });
|
|
}
|
|
|
|
function copyPluginFiles() {
|
|
if (fs.existsSync(installDir)) fs.rmSync(installDir, { recursive: true });
|
|
fs.mkdirSync(installDir, { recursive: true });
|
|
// Copy compiled output + manifest + package.json. Skip the .ts source so
|
|
// jiti uses the .js (no transpile overhead at load time) and skip tsconfig.
|
|
for (const f of ['index.js', 'openclaw.plugin.json', 'package.json']) {
|
|
const src = path.join(pluginSrcDir, f);
|
|
const dst = path.join(installDir, f);
|
|
if (!fs.existsSync(src)) {
|
|
console.error(`missing ${src}; run build first`);
|
|
process.exit(1);
|
|
}
|
|
fs.copyFileSync(src, dst);
|
|
}
|
|
console.log(`Copied to ${installDir}`);
|
|
}
|
|
|
|
function readConfig() {
|
|
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
}
|
|
|
|
function writeConfig(cfg) {
|
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
}
|
|
|
|
function registerInConfig() {
|
|
const cfg = readConfig();
|
|
const plugins = (cfg.plugins ??= {});
|
|
const allow = (plugins.allow ??= []);
|
|
const loadPaths = ((plugins.load ??= {}).paths ??= []);
|
|
const entries = (plugins.entries ??= {});
|
|
|
|
if (!allow.includes(PLUGIN_ID)) allow.push(PLUGIN_ID);
|
|
if (!loadPaths.includes(installDir)) loadPaths.push(installDir);
|
|
|
|
const entry = (entries[PLUGIN_ID] ??= {});
|
|
if (entry.enabled === undefined) entry.enabled = true;
|
|
|
|
writeConfig(cfg);
|
|
console.log(`Registered ${PLUGIN_ID} in ${configPath}`);
|
|
}
|
|
|
|
function unregisterFromConfig() {
|
|
const cfg = readConfig();
|
|
const plugins = cfg.plugins ?? {};
|
|
if (Array.isArray(plugins.allow)) {
|
|
plugins.allow = plugins.allow.filter((id) => id !== PLUGIN_ID);
|
|
}
|
|
const loadPaths = plugins.load?.paths;
|
|
if (Array.isArray(loadPaths)) {
|
|
plugins.load.paths = loadPaths.filter((p) => p !== installDir);
|
|
}
|
|
if (plugins.entries && plugins.entries[PLUGIN_ID]) {
|
|
delete plugins.entries[PLUGIN_ID];
|
|
}
|
|
writeConfig(cfg);
|
|
console.log(`Unregistered ${PLUGIN_ID} from ${configPath}`);
|
|
}
|
|
|
|
if (action === '--install') {
|
|
build();
|
|
copyPluginFiles();
|
|
registerInConfig();
|
|
console.log('Done. Restart gateway to load: systemctl --user restart openclaw-gateway');
|
|
} else if (action === '--update') {
|
|
build();
|
|
copyPluginFiles();
|
|
console.log('Updated. Restart gateway to reload: systemctl --user restart openclaw-gateway');
|
|
} else if (action === '--uninstall') {
|
|
if (fs.existsSync(installDir)) {
|
|
fs.rmSync(installDir, { recursive: true });
|
|
console.log(`Removed ${installDir}`);
|
|
}
|
|
unregisterFromConfig();
|
|
console.log('Done. Restart gateway to drop: systemctl --user restart openclaw-gateway');
|
|
}
|