commit 47bc93aacdc1a9dedde6b11ad62b19982eaca04c Author: dtzp555 Date: Sat Mar 7 12:43:18 2026 +1000 memops v0.1: doctor + switch + backup-plan diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9029ec1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +/dist +*.log +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..48443a2 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcb425e --- /dev/null +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..628b680 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e979f21 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..be16b8c --- /dev/null +++ b/src/cli.ts @@ -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}`); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8b0ee87 --- /dev/null +++ b/tsconfig.json @@ -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"] +}