memops v0.1: doctor + switch + backup-plan

This commit is contained in:
2026-03-07 12:43:18 +10:00
commit 47bc93aacd
7 changed files with 331 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
/dist
*.log
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Tao Deng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+37
View File
@@ -0,0 +1,37 @@
# openclaw-memops
Memory Ops Kit for OpenClaw.
Goals:
- Make OpenClaw memory *operationally reliable* (no more silent failures when embeddings billing breaks).
- Keep recovery simple: restore agents (SOUL + config + memory) on a fresh machine.
- Prefer OpenClaw built-in memory-core; provide optional glue and guardrails.
## Features (v0.1)
- `memops doctor`: sanity checks for memory + embeddings provider health.
- `memops switch openai|ollama`: switch `agents.defaults.memorySearch.provider` and restart gateway.
- `memops backup-plan`: print/emit backup include list for disaster recovery.
## Non-goals
- Replacing OpenClaw memory-core.
- Uploading any user memory content to Git.
## Requirements
- OpenClaw CLI installed on the same machine.
- Node.js 20+.
## Usage (planned)
```bash
memops doctor
memops switch ollama
memops switch openai
memops backup-plan
```
## Safety
This tool edits `~/.openclaw/openclaw.json`.
- Always makes a timestamped backup before modifying.
- Never prints secrets; reports only whether keys are present.
## License
MIT
+63
View File
@@ -0,0 +1,63 @@
{
"name": "openclaw-memops",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openclaw-memops",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"zod": "^3.24.2"
},
"bin": {
"memops": "dist/cli.js"
},
"devDependencies": {
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "openclaw-memops",
"version": "0.1.0",
"description": "Memory Ops Kit for OpenClaw: doctor + provider switch (OpenAI/Ollama) + backup plan + recovery helpers.",
"license": "MIT",
"type": "module",
"bin": {
"memops": "./dist/cli.js"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "node --enable-source-maps dist/cli.js",
"lint": "node -c dist/cli.js || true",
"prepack": "npm run build"
},
"dependencies": {
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
}
}
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
type Json = any;
const HOME = process.env.HOME || '';
const OPENCLAW_CONFIG = process.env.OPENCLAW_CONFIG_PATH || join(HOME, '.openclaw', 'openclaw.json');
function die(msg: string): never {
console.error(msg);
process.exit(1);
}
function loadConfig(): Json {
if (!existsSync(OPENCLAW_CONFIG)) die(`Config not found: ${OPENCLAW_CONFIG}`);
return JSON.parse(readFileSync(OPENCLAW_CONFIG, 'utf8'));
}
function saveConfig(cfg: Json): void {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const bak = `${OPENCLAW_CONFIG}.bak.${ts}`;
copyFileSync(OPENCLAW_CONFIG, bak);
writeFileSync(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 1) + '\n', 'utf8');
console.error(`Saved config. Backup: ${bak}`);
}
function get(obj: any, path: string[]): any {
return path.reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), obj);
}
function set(obj: any, path: string[], value: any): void {
let cur = obj;
for (let i = 0; i < path.length - 1; i++) {
const k = path[i];
if (!cur[k] || typeof cur[k] !== 'object') cur[k] = {};
cur = cur[k];
}
cur[path[path.length - 1]] = value;
}
function del(obj: any, path: string[]): void {
const parent = get(obj, path.slice(0, -1));
if (parent && typeof parent === 'object') delete parent[path[path.length - 1]];
}
function run(cmd: string, args: string[], opts: { quiet?: boolean } = {}): string {
const out = execFileSync(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
const s = out.toString('utf8');
if (!opts.quiet) process.stdout.write(s);
return s;
}
function restartGateway() {
try {
run('openclaw', ['gateway', 'restart'], { quiet: true });
} catch (e: any) {
die(`Failed to restart gateway: ${e?.message || e}`);
}
}
function doctor() {
let cfg: any;
try {
cfg = loadConfig();
} catch (e: any) {
die(String(e?.message || e));
}
const ms = get(cfg, ['agents', 'defaults', 'memorySearch']);
const provider = ms?.provider;
const enabled = ms?.enabled;
const hasOpenAIKey = !!ms?.remote?.apiKey;
console.log(`config: ${OPENCLAW_CONFIG}`);
console.log(`memorySearch.enabled: ${enabled}`);
console.log(`memorySearch.provider: ${provider}`);
console.log(`memorySearch.remote.apiKey.present: ${hasOpenAIKey}`);
// OpenClaw memory status (best effort)
try {
const raw = run('openclaw', ['memory', 'status', '--json'], { quiet: true });
const arr = JSON.parse(raw);
const main = arr.find((x: any) => x.agentId === 'main') || arr[0];
if (main?.status) {
console.log(`memory.backend: ${main.status.backend}`);
console.log(`memory.dbPath: ${main.status.dbPath}`);
console.log(`memory.fts.available: ${main.status.fts?.available}`);
console.log(`memory.vector.available: ${main.status.vector?.available}`);
console.log(`memory.provider(requested): ${main.status.requestedProvider}`);
console.log(`memory.model: ${main.status.model}`);
}
} catch {
console.log('memory.status: unavailable');
}
// Provider-specific health checks
if (provider === 'ollama') {
try {
// minimal tags call
run('curl', ['-sS', '-m', '3', 'http://127.0.0.1:11434/api/tags'], { quiet: true });
console.log('ollama: reachable');
} catch {
console.log('ollama: NOT reachable at http://127.0.0.1:11434');
}
}
if (provider === 'openai') {
console.log('openai: configured (note: this tool does not call OpenAI APIs)');
}
}
function backupPlan() {
console.log('Backup include list (recommended):');
console.log('- ~/.openclaw/openclaw.json');
console.log('- ~/.openclaw/.env (if used)');
console.log('- ~/.openclaw/credentials');
console.log('- ~/.openclaw/agents');
console.log('- ~/.openclaw/memory (per-agent sqlite)');
console.log('- ~/.openclaw/workspaces');
console.log('- ~/.openclaw/workspace/main');
console.log('- ~/.openclaw/cron');
console.log('Weekly full add-on:');
console.log('- ~/.openclaw/agents/*/sessions');
}
function switchProvider(to: 'openai' | 'ollama') {
const cfg = loadConfig();
const path = ['agents', 'defaults', 'memorySearch'];
const ms = get(cfg, path) || {};
if (to === 'ollama') {
set(cfg, [...path, 'provider'], 'ollama');
// remove openai key from memorySearch section to avoid confusion
del(cfg, [...path, 'remote']);
} else {
set(cfg, [...path, 'provider'], 'openai');
// keep existing remote.apiKey if present; do not prompt
if (!ms?.remote?.apiKey) {
console.error('WARN: memorySearch.remote.apiKey is not set. Configure it in openclaw.json first.');
}
}
saveConfig(cfg);
restartGateway();
console.log(`Switched memorySearch.provider -> ${to} and restarted gateway.`);
}
const [,, cmd, ...args] = process.argv;
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
console.log('openclaw-memops');
console.log('Commands:');
console.log(' memops doctor');
console.log(' memops switch openai|ollama');
console.log(' memops backup-plan');
process.exit(0);
}
if (cmd === 'doctor') doctor();
else if (cmd === 'backup-plan') backupPlan();
else if (cmd === 'switch') {
const to = args[0];
if (to !== 'openai' && to !== 'ollama') die('Usage: memops switch openai|ollama');
switchProvider(to);
} else {
die(`Unknown command: ${cmd}`);
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}