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:
50
memory-viewer/server/api.test.ts
Normal file
50
memory-viewer/server/api.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
99
memory-viewer/server/cron-trigger.mjs
Normal file
99
memory-viewer/server/cron-trigger.mjs
Normal 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);
|
||||
});
|
||||
1466
memory-viewer/server/index.ts
Normal file
1466
memory-viewer/server/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user