Add memory-viewer from silicondawn

- Web UI for browsing and editing OpenClaw memory files
- Cloned from https://github.com/silicondawn/memory-viewer
- Built and running on port 8901
- Features: file tree, markdown rendering, search, live reload
- Full access to MEMORY.md, daily notes, skills, automations
This commit is contained in:
Krilly
2026-02-21 02:28:57 +00:00
parent 690a2e31b3
commit c9acf0c4da
78 changed files with 18899 additions and 1 deletions

View File

@@ -0,0 +1,50 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { app } from './index.js';
describe('Server API', () => {
it('GET /api/info returns bot info', async () => {
// Note: This relies on actual file system unless mocked.
// Ideally we mock fs, but for integration test on CI it's fine.
const res = await app.request('/api/info');
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty('version');
});
it('GET /api/agent/status returns structure', async () => {
const res = await app.request('/api/agent/status');
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty('config');
expect(data).toHaveProperty('gateway');
});
});
describe('Backlinks API', () => {
// These tests rely on the actual WORKSPACE directory (~/clawd by default)
// They test the API structure and basic behavior
it('GET /api/resolve-wikilink returns 400 without link', async () => {
const res = await app.request('/api/resolve-wikilink');
expect(res.status).toBe(400);
});
it('GET /api/resolve-wikilink resolves existing file', async () => {
// MEMORY.md should exist in the workspace
const res = await app.request('/api/resolve-wikilink?link=MEMORY');
expect(res.status).toBe(200);
const data = await res.json() as any;
expect(data).toHaveProperty('found');
expect(data).toHaveProperty('path');
});
it('GET /api/resolve-wikilink returns not found for nonexistent', async () => {
const res = await app.request('/api/resolve-wikilink?link=nonexistent-file-that-does-not-exist-12345');
expect(res.status).toBe(200);
const data = await res.json() as any;
expect(data.found).toBe(false);
});
});

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env node
// Trigger a cron job via OpenClaw gateway WebSocket
// Usage: node cron-trigger.mjs <jobId>
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { randomUUID } from "crypto";
import WebSocket from "ws";
const jobId = process.argv[2];
if (!jobId) { console.log(JSON.stringify({success:false, error:"missing jobId"})); process.exit(1); }
const configPath = join(homedir(), ".openclaw", "openclaw.json");
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const port = config.gateway?.port || 18789;
const token = config.gateway?.auth?.token || "";
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
let connectNonce = null;
let connected = false;
const pending = new Map();
const timeout = setTimeout(() => {
console.log(JSON.stringify({success:false, error:"timeout"}));
ws.close();
process.exit(1);
}, 10000);
function send(obj) { ws.send(JSON.stringify(obj)); }
function request(method, params) {
const id = randomUUID();
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
send({ type: "req", id, method, params });
});
}
ws.on("message", (data) => {
try {
const msg = JSON.parse(data.toString());
// Event frame
if (msg.type === "event") {
if (msg.event === "connect.challenge") {
connectNonce = msg.payload?.nonce;
// Send connect request
request("connect", {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "gateway-client",
displayName: "Memory Viewer",
version: "1.0.0",
platform: "linux",
mode: "backend",
},
caps: [],
auth: { token },
role: "operator",
scopes: ["operator.admin"],
}).then(() => {
connected = true;
// Send cron.run — it's async, so fire and consider it triggered
const cronId = randomUUID();
ws.send(JSON.stringify({type:'req', id:cronId, method:'cron.run', params:{id: jobId}}));
// Give it a moment to dispatch, then report success
setTimeout(() => {
clearTimeout(timeout);
console.log(JSON.stringify({success:true, result:'triggered'}));
ws.close();
process.exit(0);
}, 500);
}).catch((err) => {
clearTimeout(timeout);
console.log(JSON.stringify({success:false, error: err.message}));
ws.close();
process.exit(1);
});
}
return;
}
// Response frame
if (msg.type === "res") {
const p = pending.get(msg.id);
if (!p) return;
pending.delete(msg.id);
if (msg.ok) p.resolve(msg.payload);
else p.reject(new Error(msg.error?.message || "unknown error"));
}
} catch {}
});
ws.on("error", (err) => {
clearTimeout(timeout);
console.log(JSON.stringify({success:false, error: err.message}));
process.exit(1);
});

File diff suppressed because it is too large Load Diff