mirror of
https://github.com/dtzp555-max/clawkeeper.git
synced 2026-07-19 09:41:42 +00:00
memops v0.1: doctor + switch + backup-plan
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
Generated
+63
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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}`);
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user