@bsbofmusic/memos-memu-local-memory-tools-for-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # memos-memu-local-memory-tools-for-agent
2
+
3
+ **One-shot install + query tools for a local memos + memuK memory system, powered by MCP (Model Context Protocol).**
4
+
5
+ Designed for [OpenClaw](https://openclaw.ai) agents that need persistent, queryable memory beyond Markdown files — a three-layer stack:
6
+
7
+ | Layer | Store | What it's for |
8
+ |-------|-------|--------------|
9
+ | File brain | `MEMORY.md` / `memory/*.md` | Stable facts, rules, decisions |
10
+ | memos | PostgreSQL (Docker) | Original quotes, timing, commitments |
11
+ | memuK | SQLite | Topic summaries, cross-session recall |
12
+
13
+ ---
14
+
15
+ ## Quick start
16
+
17
+ ### 1. Add to MCPorter config (`~/.openclaw/mcporter.json`)
18
+
19
+ ```json
20
+ {
21
+ "servers": {
22
+ "memos-memu-local-memory-tools-for-agent": {
23
+ "command": "npx",
24
+ "args": ["-y", "@bsbofmusic/memos-memu-local-memory-tools-for-agent"],
25
+ "type": "stdio"
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ### 2. Tell your agent
32
+
33
+ > "Install the memory system using `install_memory_system` tool."
34
+
35
+ The agent will call `install_memory_system` — and the MCP server will set up everything automatically.
36
+
37
+ ### 3. Verify
38
+
39
+ ```bash
40
+ npx @bsbofmusic/memos-memu-local-memory-tools-for-agent
41
+ # then ask your agent to call verify_memory_system
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Tools
47
+
48
+ ### `install_memory_system`
49
+ One-shot install of the full memory system. Safe to re-run — detects existing components and skips them.
50
+
51
+ What it sets up:
52
+ - **Docker `memos-postgres` container** (PostgreSQL 15 alpine + memos schema)
53
+ - **memuK SQLite database** (`~/.openclaw/memu.db`) with three tables
54
+ - **`memory-triple-recall` SKILL** for OpenClaw agents
55
+ - **Sync cron** (`*/15 * * * *`) that syncs memos → memuK every 15 minutes
56
+ - **MCPorter MCP entry** (merged into your existing `mcporter.json`)
57
+
58
+ ### `verify_memory_system`
59
+ Smoke-test all installed components. Returns PASS/FAIL for each:
60
+ - Docker availability
61
+ - memos-postgres container status
62
+ - memos PostgreSQL queryability
63
+ - memuK SQLite accessibility
64
+ - memory-triple-recall SKILL presence
65
+ - MCPorter MCP config entry
66
+ - Sync cron installation
67
+
68
+ ### `memos_query`
69
+ Search memos PostgreSQL by keyword.
70
+
71
+ ```
72
+ query: string // required — case-insensitive keywords
73
+ limit?: number // default 10, max 20
74
+ ```
75
+
76
+ ### `memuk_search`
77
+ Search memuK SQLite memory summaries.
78
+
79
+ ```
80
+ query: string // required — topic / keywords
81
+ limit?: number // default 5, max 10
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Requirements
87
+
88
+ | Dependency | Version | Notes |
89
+ |------------|---------|-------|
90
+ | Node.js | ≥ 18 | Built-in on most systems |
91
+ | Docker | any | Required for memos PostgreSQL |
92
+ | sqlite3 CLI | any | For memuK access |
93
+
94
+ ### Docker setup (if not already installed)
95
+
96
+ ```bash
97
+ # Linux
98
+ curl -fsSL https://get.docker.com | sh
99
+
100
+ # macOS
101
+ brew install --cask docker
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Architecture
107
+
108
+ ```
109
+ ┌─────────────────────────────────────────────────────────┐
110
+ │ OpenClaw Agent │
111
+ │ (K / any OpenClaw agent) │
112
+ └────────────────┬────────────────────────────────────────┘
113
+ │ MCP stdio
114
+
115
+ ┌─────────────────────────────────────────────────────────┐
116
+ │ MCP Server │
117
+ │ memos-memu-local-memory-tools-for-agent │
118
+ │ (this package) │
119
+ │ │
120
+ │ ┌──────────────┐ ┌──────────────┐ │
121
+ │ │ install_* │ │ memos_query │ │
122
+ │ │ verify_* │ │ memuk_search │ │
123
+ │ └──────────────┘ └──────────────┘ │
124
+ └────────┬─────────────────┬──────────────────────────────┘
125
+ │ docker exec │ sqlite3 CLI
126
+ ▼ ▼
127
+ ┌─────────────────┐ ┌─────────────────┐
128
+ │ memos-postgres │ │ ~/.openclaw/ │
129
+ │ (Docker PG) │ │ memu.db │
130
+ │ │ │ (SQLite) │
131
+ │ memo table │ │ │
132
+ │ raw history │ │ memu_memory_ │
133
+ │ │ │ items │
134
+ └─────────────────┘ └─────────────────┘
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Sync mechanism
140
+
141
+ Every 15 minutes, a cron job runs `sync_memos_to_memuk.py` which:
142
+
143
+ 1. Reads the last synced `memo.id` from `memu_sync_checkpoint`
144
+ 2. Fetches all newer memos from PostgreSQL
145
+ 3. Inserts summaries into `memu_memory_items`
146
+ 4. Updates the checkpoint
147
+
148
+ This keeps memuK as a lightweight, queryable cache of memos history without touching the DB on every recall.
149
+
150
+ ---
151
+
152
+ ## Environment variables
153
+
154
+ | Variable | Default | Description |
155
+ |----------|---------|-------------|
156
+ | `MEMOS_CONTAINER` | `memos-postgres` | Docker container name |
157
+ | `MEMOS_DB` | `memos` | PostgreSQL database name |
158
+ | `MEMOS_USER` | `memos` | PostgreSQL username |
159
+ | `MEMOS_PASSWORD` | `memos_local_20260312` | PostgreSQL password |
160
+ | `MEMUK_PATH` | `~/.openclaw/memu.db` | memuK SQLite file path |
161
+ | `OPENCLAW_STATE_DIR` | `~/.openclaw` | OpenClaw state directory |
162
+
163
+ ---
164
+
165
+ ## Uninstall / reset
166
+
167
+ ```bash
168
+ # Remove Docker containers
169
+ docker stop memos-postgres && docker rm memos-postgres
170
+
171
+ # Remove memuK SQLite
172
+ rm ~/.openclaw/memu.db
173
+
174
+ # Remove SKILL
175
+ rm -rf ~/.openclaw/workspace/skills/memory-triple-recall
176
+
177
+ # Remove cron
178
+ sudo rm /etc/cron.d/memuk-sync || crontab -e # delete memuk-sync line
179
+
180
+ # Remove MCPorter entry (edit mcporter.json and delete the server entry)
181
+ ```
182
+
183
+ ---
184
+
185
+ ## License
186
+
187
+ MIT — use freely, modify freely.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@bsbofmusic/memos-memu-local-memory-tools-for-agent",
3
+ "version": "1.0.0",
4
+ "description": "MCP server — one-shot install + query for memos (PostgreSQL) and memuK (SQLite) local memory. Designed for OpenClaw agents.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "memos-memu-local-memory-tools-for-agent": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "test": "node src/tools/verify.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "mcp-server",
17
+ "openclaw",
18
+ "memos",
19
+ "memory",
20
+ "local-memory",
21
+ "sqlite",
22
+ "postgresql"
23
+ ],
24
+ "author": "K / bsbofmusic",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ }
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,138 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+
8
+ import { install_memory_system } from './tools/install.js';
9
+ import { memos_query } from './tools/memos.js';
10
+ import { memuk_search } from './tools/memuk.js';
11
+ import { verify_memory_system } from './tools/verify.js';
12
+
13
+ const SERVER_NAME = 'memos-memu-local-memory-tools-for-agent';
14
+ const SERVER_VERSION = '1.0.0';
15
+
16
+ export const tools = [
17
+ {
18
+ name: 'install_memory_system',
19
+ description:
20
+ 'One-shot install of the full memos + memuK local memory system. ' +
21
+ 'Sets up: Docker memos-postgres container, PostgreSQL DB, memuK SQLite schema, ' +
22
+ 'memory-triple-recall SKILL, sync cron, and mcporter MCP config. ' +
23
+ 'Safe to re-run — detects existing components and skips what\'s already there. ' +
24
+ 'Returns a step-by-step status report.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {},
28
+ },
29
+ },
30
+ {
31
+ name: 'verify_memory_system',
32
+ description:
33
+ 'Smoke-test the installed memory system. Checks: memos PostgreSQL connectivity, ' +
34
+ 'memuK SQLite connectivity, cron sync status, and mcporter config presence. ' +
35
+ 'Returns PASS/FAIL for each component plus a summary.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {},
39
+ },
40
+ },
41
+ {
42
+ name: 'memos_query',
43
+ description:
44
+ 'Query the memos PostgreSQL database for raw conversation history, original wording, ' +
45
+ 'and past context. Use for: original quotes, exact wording, timing of events, commitments. ' +
46
+ 'Returns formatted memo entries with content and metadata.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ query: {
51
+ type: 'string',
52
+ description: 'Keywords to search in memo content (case-insensitive)',
53
+ },
54
+ limit: {
55
+ type: 'number',
56
+ description: 'Max results to return (default 10, max 20)',
57
+ default: 10,
58
+ },
59
+ },
60
+ required: ['query'],
61
+ },
62
+ },
63
+ {
64
+ name: 'memuk_search',
65
+ description:
66
+ 'Search the memuK SQLite memory store for topic summaries and cross-session recall. ' +
67
+ 'Use for: "did we talk about X", topic-level memory lookups. ' +
68
+ 'Returns memory summaries with type and timestamp.',
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ query: {
73
+ type: 'string',
74
+ description: 'Topic / keywords to search in memory summaries',
75
+ },
76
+ limit: {
77
+ type: 'number',
78
+ description: 'Max results to return (default 5, max 10)',
79
+ default: 5,
80
+ },
81
+ },
82
+ required: ['query'],
83
+ },
84
+ },
85
+ ];
86
+
87
+ const server = new Server(
88
+ { name: SERVER_NAME, version: SERVER_VERSION },
89
+ { capabilities: { tools: {} } }
90
+ );
91
+
92
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
93
+ return { tools };
94
+ });
95
+
96
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
97
+ const { name, arguments: args } = request.params;
98
+
99
+ try {
100
+ switch (name) {
101
+ case 'install_memory_system':
102
+ return await install_memory_system(args);
103
+ case 'verify_memory_system':
104
+ return await verify_memory_system(args);
105
+ case 'memos_query':
106
+ return await memos_query(args);
107
+ case 'memuk_search':
108
+ return await memuk_search(args);
109
+ default:
110
+ return {
111
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
112
+ isError: true,
113
+ };
114
+ }
115
+ } catch (err) {
116
+ return {
117
+ content: [
118
+ {
119
+ type: 'text',
120
+ text: `❌ Tool error [${name}]: ${err.message}\n\n${err.stack}`,
121
+ },
122
+ ],
123
+ isError: true,
124
+ };
125
+ }
126
+ });
127
+
128
+ async function main() {
129
+ const transport = new StdioServerTransport();
130
+ await server.connect(transport);
131
+ }
132
+
133
+ main().catch((err) => {
134
+ console.error('Server fatal error:', err);
135
+ process.exit(1);
136
+ });
137
+
138
+ export default server;
@@ -0,0 +1,114 @@
1
+ import { execSync, exec } from 'child_process';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ export function detectEnv() {
10
+ const home = os.homedir();
11
+ const openclawState =
12
+ process.env.OPENCLAW_STATE_DIR ||
13
+ path.join(home, '.openclaw');
14
+
15
+ return {
16
+ memosContainer: process.env.MEMOS_CONTAINER || 'memos-postgres',
17
+ memosDb: process.env.MEMOS_DB || 'memos',
18
+ memosUser: process.env.MEMOS_USER || 'memos',
19
+ memosPassword: process.env.MEMOS_PASSWORD || 'memos_local_20260312',
20
+ memukPath: process.env.MEMUK_PATH || path.join(openclawState, 'memu.db'),
21
+ workspaceDir: openclawState,
22
+ skillDir: path.join(openclawState, 'workspace', 'skills'),
23
+ };
24
+ }
25
+
26
+ export function psql(sql, container) {
27
+ const env = detectEnv();
28
+ const c = container || env.memosContainer;
29
+ const safeSql = sql.replace(/"/g, '\\"');
30
+ try {
31
+ const out = execSync(
32
+ `docker exec ${c} psql -U ${env.memosUser} -d ${env.memosDb} -t -A -c "${safeSql}"`,
33
+ { timeout: 15000, maxBuffer: 512 * 1024 }
34
+ ).toString();
35
+ return out;
36
+ } catch (err) {
37
+ throw new Error(`psql failed: ${err.message}`);
38
+ }
39
+ }
40
+
41
+ export function sqlite(sql, dbPath) {
42
+ const env = detectEnv();
43
+ const db = dbPath || env.memukPath;
44
+ const safeSql = sql.replace(/"/g, '""');
45
+ try {
46
+ const out = execSync(
47
+ `/usr/bin/sqlite3 "${db}" "${safeSql}"`,
48
+ { timeout: 10000 }
49
+ ).toString();
50
+ return out;
51
+ } catch (err) {
52
+ throw new Error(`sqlite3 failed: ${err.message}`);
53
+ }
54
+ }
55
+
56
+ export function sh(cmd, opts = {}) {
57
+ return new Promise((resolve, reject) => {
58
+ exec(cmd, { timeout: opts.timeout || 300000 }, (err, stdout, stderr) => {
59
+ if (err) reject(new Error(`${cmd}\n→ ${err.message}`));
60
+ else resolve(stdout.toString());
61
+ });
62
+ });
63
+ }
64
+
65
+ export function shSync(cmd, opts = {}) {
66
+ try {
67
+ return execSync(cmd, { timeout: opts.timeout || 300000, ...opts }).toString();
68
+ } catch (err) {
69
+ throw new Error(`shSync failed: ${err.message}`);
70
+ }
71
+ }
72
+
73
+ export function dockerAvailable() {
74
+ try {
75
+ execSync('docker info', { timeout: 5000 });
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ export function dockerContainerRunning(name) {
83
+ try {
84
+ const out = execSync(
85
+ `docker inspect -f '{{.State.Running}}' ${name} 2>/dev/null`,
86
+ { timeout: 5000 }
87
+ ).toString().trim();
88
+ return out === 'true';
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ export function parseSqlite(output) {
95
+ const lines = output.split('\n').filter(l => l.trim() && !l.startsWith('--'));
96
+ return lines.map(line => {
97
+ const pipe = line.split('|');
98
+ return {
99
+ id: pipe[0]?.trim() ?? '',
100
+ summary: pipe[1]?.trim() ?? '',
101
+ memory_type: pipe[2]?.trim() ?? '',
102
+ happened_at: pipe[3]?.trim() ?? '',
103
+ };
104
+ });
105
+ }
106
+
107
+ export function parsePsqlJson(output) {
108
+ try {
109
+ const rows = JSON.parse(output.trim());
110
+ return Array.isArray(rows) ? rows : [];
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
@@ -0,0 +1,37 @@
1
+ export const STEPS = [
2
+ {
3
+ id: 'docker',
4
+ name: 'Docker availability',
5
+ description: 'Checks if Docker is installed and running.',
6
+ },
7
+ {
8
+ id: 'memos-container',
9
+ name: 'memos-postgres Docker container',
10
+ description: 'Creates or starts the PostgreSQL + memos Docker container.',
11
+ },
12
+ {
13
+ id: 'memos-db',
14
+ name: 'memos PostgreSQL schema',
15
+ description: 'Verifies the memo table is accessible.',
16
+ },
17
+ {
18
+ id: 'memuk-db',
19
+ name: 'memuK SQLite database',
20
+ description: 'Creates the memuK SQLite schema with three tables.',
21
+ },
22
+ {
23
+ id: 'skill',
24
+ name: 'memory-triple-recall SKILL',
25
+ description: 'Installs the SKILL.md into the OpenClaw workspace skills directory.',
26
+ },
27
+ {
28
+ id: 'cron',
29
+ name: 'Sync cron job',
30
+ description: 'Installs a */15 cron that syncs memos → memuK.',
31
+ },
32
+ {
33
+ id: 'mcporter-config',
34
+ name: 'MCPorter MCP server entry',
35
+ description: 'Merges the MCP server config into ~/.openclaw/mcporter.json.',
36
+ },
37
+ ];
@@ -0,0 +1,303 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import {
5
+ detectEnv,
6
+ sh,
7
+ shSync,
8
+ dockerAvailable,
9
+ dockerContainerRunning,
10
+ psql,
11
+ sqlite,
12
+ } from '../shared/db.js';
13
+
14
+ const indent = s => s.split('\n').map(l => ` ${l}`).join('\n');
15
+
16
+ class InstallReport {
17
+ constructor() {
18
+ this.steps = [];
19
+ this.summary = { passed: 0, skipped: 0, failed: 0 };
20
+ }
21
+ ok(name, detail = '') {
22
+ this.steps.push({ status: '✅', name, detail });
23
+ this.summary.passed++;
24
+ }
25
+ skip(name, reason) {
26
+ this.steps.push({ status: '⏭️', name, detail: reason });
27
+ this.summary.skipped++;
28
+ }
29
+ fail(name, reason) {
30
+ this.steps.push({ status: '❌', name, detail: reason });
31
+ this.summary.failed++;
32
+ }
33
+ toText() {
34
+ const lines = this.steps.map(
35
+ s => `${s.status} ${s.name}${s.detail ? '\n' + indent(s.detail) : ''}`
36
+ );
37
+ const t = this.summary.passed + this.summary.skipped + this.summary.failed;
38
+ lines.push(`\n─── ${this.summary.passed}/${t} passed · ${this.summary.skipped} skipped · ${this.summary.failed} failed ───`);
39
+ return lines.join('\n');
40
+ }
41
+ }
42
+
43
+ export async function install_memory_system() {
44
+ const env = detectEnv();
45
+ const report = new InstallReport();
46
+
47
+ // ── Docker ────────────────────────────────────────────────────────────────
48
+ if (!dockerAvailable()) {
49
+ report.fail('Docker', 'Docker is not installed or not running. Install: https://docs.docker.com/get-docker/');
50
+ return { content: [{ type: 'text', text: report.toText() }] };
51
+ }
52
+ report.ok('Docker', 'Docker is available');
53
+
54
+ // ── Container ────────────────────────────────────────────────────────────
55
+ if (dockerContainerRunning(env.memosContainer)) {
56
+ report.skip('memos-postgres container', 'Already running — skipping');
57
+ } else {
58
+ const containerExists = (() => {
59
+ try {
60
+ shSync(`docker inspect ${env.memosContainer}`, { timeout: 5000 });
61
+ return true;
62
+ } catch { return false; }
63
+ })();
64
+
65
+ if (containerExists) {
66
+ try {
67
+ shSync(`docker start ${env.memosContainer}`, { timeout: 15000 });
68
+ report.ok('memos-postgres container', 'Started existing stopped container');
69
+ } catch (err) {
70
+ report.fail('memos-postgres container', `docker start failed: ${err.message}`);
71
+ return { content: [{ type: 'text', text: report.toText() }] };
72
+ }
73
+ } else {
74
+ try {
75
+ // Pull and start postgres alpine
76
+ shSync(
77
+ `docker run -d --name ${env.memosContainer} ` +
78
+ `-e POSTGRES_USER=${env.memosUser} ` +
79
+ `-e POSTGRES_PASSWORD=${env.memosPassword} ` +
80
+ `-e POSTGRES_DB=${env.memosDb} ` +
81
+ `-v memos-pgdata:/var/lib/postgresql/data ` +
82
+ `postgres:15-alpine`,
83
+ { timeout: 60000 }
84
+ );
85
+ report.ok('memos-postgres container', 'Created and started postgres:15-alpine');
86
+ } catch (err) {
87
+ report.fail('memos-postgres container', `docker run failed: ${err.message}`);
88
+ return { content: [{ type: 'text', text: report.toText() }] };
89
+ }
90
+ }
91
+ }
92
+
93
+ // Wait for postgres to be ready (up to 30s)
94
+ let pgReady = false;
95
+ for (let i = 0; i < 20; i++) {
96
+ await new Promise(r => setTimeout(r, 1500));
97
+ try {
98
+ shSync(`docker exec ${env.memosContainer} pg_isready -U ${env.memosUser}`, { timeout: 3000 });
99
+ pgReady = true;
100
+ break;
101
+ } catch {}
102
+ }
103
+ if (!pgReady) {
104
+ report.fail('memos-postgres', 'PostgreSQL did not become ready in 30s');
105
+ return { content: [{ type: 'text', text: report.toText() }] };
106
+ }
107
+ report.ok('memos-postgres', 'PostgreSQL is ready');
108
+
109
+ // ── memos DB ─────────────────────────────────────────────────────────────
110
+ try {
111
+ const testQ = psql('SELECT 1 AS ok;');
112
+ if (testQ.includes('1')) {
113
+ report.ok('memos PostgreSQL', 'Database is accessible');
114
+ } else {
115
+ throw new Error('Unexpected query result');
116
+ }
117
+ } catch (err) {
118
+ report.fail('memos PostgreSQL', err.message);
119
+ return { content: [{ type: 'text', text: report.toText() }] };
120
+ }
121
+
122
+ // ── memuK SQLite ─────────────────────────────────────────────────────────
123
+ const memukDir = path.dirname(env.memukPath);
124
+ if (!fs.existsSync(memukDir)) {
125
+ fs.mkdirSync(memukDir, { recursive: true });
126
+ }
127
+
128
+ const schema = `
129
+ CREATE TABLE IF NOT EXISTS memu_memory_items (
130
+ id TEXT PRIMARY KEY,
131
+ summary TEXT NOT NULL,
132
+ memory_type TEXT DEFAULT 'imported',
133
+ happened_at TEXT,
134
+ user_id TEXT DEFAULT 'default',
135
+ created_at TEXT DEFAULT (datetime('now', 'localtime'))
136
+ );
137
+ CREATE TABLE IF NOT EXISTS memu_sync_checkpoint (
138
+ key TEXT PRIMARY KEY,
139
+ value TEXT NOT NULL,
140
+ updated_at TEXT DEFAULT (datetime('now', 'localtime'))
141
+ );
142
+ CREATE TABLE IF NOT EXISTS memu_raw_memos (
143
+ id INTEGER PRIMARY KEY,
144
+ content TEXT NOT NULL,
145
+ raw_json TEXT,
146
+ created_ts INTEGER,
147
+ synced_at TEXT DEFAULT (datetime('now', 'localtime'))
148
+ );
149
+ `.trim();
150
+
151
+ try {
152
+ if (fs.existsSync(env.memukPath)) {
153
+ const fd = fs.openSync(env.memukPath, 'a');
154
+ fs.writeSync(fd, '\n' + schema + '\n');
155
+ fs.closeSync(fd);
156
+ } else {
157
+ fs.writeFileSync(env.memukPath, schema + '\n');
158
+ }
159
+ const testCount = sqlite('SELECT COUNT(*) FROM memu_memory_items;', env.memukPath).trim();
160
+ report.ok('memuK SQLite', `Schema initialized · ${testCount} memory_items`);
161
+ } catch (err) {
162
+ report.fail('memuK SQLite', err.message);
163
+ return { content: [{ type: 'text', text: report.toText() }] };
164
+ }
165
+
166
+ // ── SKILL ────────────────────────────────────────────────────────────────
167
+ const skillDir = path.join(env.skillDir, 'memory-triple-recall');
168
+ const skillFile = path.join(skillDir, 'SKILL.md');
169
+ const today = new Date().toISOString().split('T')[0];
170
+
171
+ const skillContent = `---
172
+ name: memory-triple-recall
173
+ description: |
174
+ Three-layer memory recall stack: MEMORY.md → memos (PostgreSQL) → memuK (SQLite).
175
+ Use for: original quotes, timing, commitments, topic recall, historical context.
176
+ triggers: 回忆|回想|记得|原话|哪天|什么时候|答应过|之前|历史|进展
177
+ version: 1.0.0
178
+ updated: ${today}
179
+ ---
180
+
181
+ # Memory Triple Recall
182
+
183
+ ## Architecture
184
+ | Layer | Store | Access |
185
+ |-------|-------|--------|
186
+ | File brain | MEMORY.md / memory/*.md | File system |
187
+ | memos | PostgreSQL Docker | memos_query MCP tool |
188
+ | memuK | SQLite | memuk_search MCP tool |
189
+
190
+ ## Fast path
191
+ Do NOT trigger layered recall for atomic facts in USER.md / IDENTITY.md / injected context.
192
+
193
+ ## NEVER fast-path (always do Layer 1+2)
194
+ - 喜欢、最爱、最喜欢的X
195
+ - 女友、crush、个人偏好、私人事实
196
+
197
+ ## Default order
198
+ 1. File brain: stable facts, rules, decisions
199
+ 2. memos: original wording, timing, commitments (memos_query)
200
+ 3. memuK: fuzzy topic recall (memuk_search)
201
+
202
+ ## Output shape
203
+ - 已确认事实
204
+ - 原话·证据
205
+ - 仍不确定点
206
+
207
+ ## Zero-evidence fallback
208
+ 已确认事实 → [空]
209
+ 原话·证据 → [空]
210
+ 仍不确定点 → 此话题在三层记忆中均无记录。
211
+ `;
212
+
213
+ try {
214
+ fs.mkdirSync(skillDir, { recursive: true });
215
+ if (!fs.existsSync(skillFile)) {
216
+ fs.writeFileSync(skillFile, skillContent);
217
+ report.ok('memory-triple-recall SKILL', `Installed → ${skillFile}`);
218
+ } else {
219
+ report.skip('memory-triple-recall SKILL', 'Already exists — not overwriting');
220
+ }
221
+ } catch (err) {
222
+ report.fail('memory-triple-recall SKILL', err.message);
223
+ }
224
+
225
+ // ── Cron ─────────────────────────────────────────────────────────────────
226
+ const cronContent = `\
227
+ # memos → memuK sync every 15 minutes
228
+ */15 * * * * root /var/lib/openclaw/.openclaw/workspace/scripts/sync_memos_to_memuk.py >> /var/lib/openclaw/.openclaw/workspace/logs/memuk-sync.log 2>&1
229
+ `.trim();
230
+
231
+ const cronPath = '/etc/cron.d/memuk-sync';
232
+ try {
233
+ fs.mkdirSync(path.dirname(cronPath), { recursive: true });
234
+ fs.writeFileSync(cronPath, cronContent + '\n', { mode: 0o644 });
235
+ report.ok('Sync cron', `Installed → ${cronPath}`);
236
+ } catch {
237
+ try {
238
+ await sh(
239
+ `(crontab -l 2>/dev/null | grep -v "memuk-sync"; echo "*/15 * * * * /var/lib/openclaw/.openclaw/workspace/scripts/sync_memos_to_memuk.py >> /var/lib/openclaw/.openclaw/workspace/logs/memuk-sync.log 2>&1") | crontab -`,
240
+ { timeout: 5000 }
241
+ );
242
+ report.ok('Sync cron', 'Installed via user crontab');
243
+ } catch (err) {
244
+ report.fail('Sync cron', err.message);
245
+ }
246
+ }
247
+
248
+ // ── MCPorter config ────────────────────────────────────────────────────────
249
+ const pkgName = 'memos-memu-local-memory-tools-for-agent';
250
+ const mcporterConfigPath = path.join(env.workspaceDir, 'mcporter.json');
251
+ let existingConfig = {};
252
+ if (fs.existsSync(mcporterConfigPath)) {
253
+ try {
254
+ existingConfig = JSON.parse(fs.readFileSync(mcporterConfigPath, 'utf8'));
255
+ } catch {}
256
+ }
257
+
258
+ const newEntry = {
259
+ args: ['-y', `@bsbofmusic/${pkgName}`],
260
+ command: 'npx',
261
+ type: 'stdio',
262
+ };
263
+
264
+ const merged = {
265
+ ...existingConfig,
266
+ servers: {
267
+ ...(existingConfig.servers || {}),
268
+ [pkgName]: newEntry,
269
+ },
270
+ };
271
+
272
+ try {
273
+ fs.mkdirSync(path.dirname(mcporterConfigPath), { recursive: true });
274
+ fs.writeFileSync(mcporterConfigPath, JSON.stringify(merged, null, 2) + '\n');
275
+ report.ok('MCPorter MCP config', `Updated ${mcporterConfigPath}`);
276
+ } catch (err) {
277
+ report.fail('MCPorter MCP config', err.message);
278
+ }
279
+
280
+ // ── Final check ───────────────────────────────────────────────────────────
281
+ try {
282
+ psql('SELECT 1;');
283
+ report.ok('Final memos check', 'Responds OK');
284
+ } catch (err) {
285
+ report.fail('Final memos check', err.message);
286
+ }
287
+
288
+ try {
289
+ sqlite('SELECT 1;', env.memukPath);
290
+ report.ok('Final memuK check', 'Responds OK');
291
+ } catch (err) {
292
+ report.fail('Final memuK check', err.message);
293
+ }
294
+
295
+ return {
296
+ content: [
297
+ {
298
+ type: 'text',
299
+ text: `🛠️ memory-system install complete\n\n${report.toText()}`,
300
+ },
301
+ ],
302
+ };
303
+ }
@@ -0,0 +1,61 @@
1
+ import { detectEnv, psql, parsePsqlJson } from '../shared/db.js';
2
+
3
+ export async function memos_query({ query, limit = 10 }) {
4
+ if (!query || typeof query !== 'string') {
5
+ return {
6
+ content: [{ type: 'text', text: '❌ memos_query requires a non-empty "query" string.' }],
7
+ isError: true,
8
+ };
9
+ }
10
+
11
+ const safe = s => s.replace(/'/g, "''");
12
+ const lim = Math.min(parseInt(limit) || 10, 20);
13
+
14
+ const sql = `
15
+ SELECT jsonb_agg(q ORDER BY created_ts DESC)
16
+ FROM (
17
+ SELECT
18
+ id,
19
+ substr(content, 1, 500) AS content,
20
+ created_ts,
21
+ visibility
22
+ FROM memo
23
+ WHERE content ILIKE '%${safe(query)}%'
24
+ ORDER BY created_ts DESC
25
+ LIMIT ${lim}
26
+ ) q;`.trim();
27
+
28
+ let raw;
29
+ try {
30
+ raw = psql(sql);
31
+ } catch (err) {
32
+ return {
33
+ content: [{
34
+ type: 'text',
35
+ text: `❌ memos PostgreSQL query failed: ${err.message}\n\nIs the memos-postgres container running?`,
36
+ }],
37
+ };
38
+ }
39
+
40
+ const rows = parsePsqlJson(raw);
41
+
42
+ if (!rows.length) {
43
+ return {
44
+ content: [{ type: 'text', text: `✅ memos query "${query}" returned 0 results.` }],
45
+ };
46
+ }
47
+
48
+ const lines = rows.map((r, i) => {
49
+ const ts = new Date(Number(r.created_ts)).toLocaleString('zh-CN', {
50
+ timeZone: 'Asia/Shanghai',
51
+ });
52
+ return `─── ${i + 1}. [id:${r.id}] ${ts} (${r.visibility}) ───\n${r.content}`;
53
+ });
54
+
55
+ return {
56
+ content: [{
57
+ type: 'text',
58
+ text: `📋 memos results for "${query}" (${rows.length}):\n\n${lines.join('\n\n')}`,
59
+ }],
60
+ };
61
+ }
@@ -0,0 +1,55 @@
1
+ import { detectEnv, sqlite, parseSqlite } from '../shared/db.js';
2
+
3
+ export async function memuk_search({ query, limit = 5 }) {
4
+ if (!query || typeof query !== 'string') {
5
+ return {
6
+ content: [{ type: 'text', text: '❌ memuk_search requires a non-empty "query" string.' }],
7
+ isError: true,
8
+ };
9
+ }
10
+
11
+ const safe = s => s.replace(/'/g, "''");
12
+ const lim = Math.min(parseInt(limit) || 5, 10);
13
+ const env = detectEnv();
14
+
15
+ const sql = `
16
+ SELECT
17
+ id,
18
+ substr(summary, 1, 300) AS summary,
19
+ memory_type,
20
+ happened_at
21
+ FROM memu_memory_items
22
+ WHERE summary LIKE '%${safe(query)}%' OR memory_type LIKE '%${safe(query)}%'
23
+ ORDER BY happened_at DESC
24
+ LIMIT ${lim};`.trim();
25
+
26
+ let rows;
27
+ try {
28
+ const out = sqlite(sql, env.memukPath);
29
+ rows = parseSqlite(out);
30
+ } catch (err) {
31
+ return {
32
+ content: [{
33
+ type: 'text',
34
+ text: `❌ memuK SQLite search failed: ${err.message}\n\nIs ${env.memukPath} accessible?`,
35
+ }],
36
+ };
37
+ }
38
+
39
+ if (!rows.length) {
40
+ return {
41
+ content: [{ type: 'text', text: `✅ memuK search "${query}" returned 0 results.` }],
42
+ };
43
+ }
44
+
45
+ const lines = rows.map(
46
+ (r, i) => `─── ${i + 1}. [${r.memory_type}] ${r.happened_at || 'n/a'} ───\n${r.summary}`
47
+ );
48
+
49
+ return {
50
+ content: [{
51
+ type: 'text',
52
+ text: `🧠 memuK results for "${query}" (${rows.length}):\n\n${lines.join('\n\n')}`,
53
+ }],
54
+ };
55
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import {
5
+ detectEnv,
6
+ psql,
7
+ sqlite,
8
+ dockerAvailable,
9
+ dockerContainerRunning,
10
+ } from '../shared/db.js';
11
+
12
+ export async function verify_memory_system() {
13
+ const env = detectEnv();
14
+ const results = [];
15
+
16
+ const check = (component, fn) => {
17
+ try {
18
+ const result = fn();
19
+ results.push(result);
20
+ } catch (err) {
21
+ results.push({ component, status: 'FAIL', detail: err.message });
22
+ }
23
+ };
24
+
25
+ // 1. Docker
26
+ check('Docker', () => ({
27
+ component: 'Docker',
28
+ status: dockerAvailable() ? 'PASS' : 'FAIL',
29
+ detail: dockerAvailable() ? 'Docker is available' : 'Docker is not available',
30
+ }));
31
+
32
+ // 2. Container
33
+ check('memos-postgres container', () => {
34
+ const running = dockerContainerRunning(env.memosContainer);
35
+ return {
36
+ component: 'memos-postgres container',
37
+ status: running ? 'PASS' : 'FAIL',
38
+ detail: running ? `Container "${env.memosContainer}" is running` : `Container "${env.memosContainer}" is not running`,
39
+ };
40
+ });
41
+
42
+ // 3. memos PostgreSQL
43
+ check('memos PostgreSQL', () => {
44
+ const out = psql('SELECT COUNT(*) FROM memo;').trim();
45
+ return { component: 'memos PostgreSQL', status: 'PASS', detail: `${out} memos in DB` };
46
+ });
47
+
48
+ // 4. memuK SQLite
49
+ check('memuK SQLite', () => {
50
+ if (!fs.existsSync(env.memukPath)) {
51
+ return { component: 'memuK SQLite', status: 'FAIL', detail: `DB not found at ${env.memukPath}` };
52
+ }
53
+ try {
54
+ const count = sqlite('SELECT COUNT(*) FROM memu_memory_items;', env.memukPath).trim();
55
+ let checkpoint = 'unset';
56
+ try {
57
+ checkpoint = sqlite("SELECT value FROM memu_sync_checkpoint WHERE key='last_memo_id';", env.memukPath).trim() || 'unset';
58
+ } catch { /* checkpoint table may not exist yet */ }
59
+ return {
60
+ component: 'memuK SQLite',
61
+ status: 'PASS',
62
+ detail: `${count} memory items · checkpoint: ${checkpoint}`,
63
+ };
64
+ } catch (err) {
65
+ return { component: 'memuK SQLite', status: 'FAIL', detail: err.message };
66
+ }
67
+ });
68
+
69
+ // 5. SKILL
70
+ check('memory-triple-recall SKILL', () => {
71
+ const skillFile = path.join(env.skillDir, 'memory-triple-recall', 'SKILL.md');
72
+ return {
73
+ component: 'memory-triple-recall SKILL',
74
+ status: fs.existsSync(skillFile) ? 'PASS' : 'FAIL',
75
+ detail: fs.existsSync(skillFile) ? skillFile : `SKILL.md not found at ${skillFile}`,
76
+ };
77
+ });
78
+
79
+ // 6. MCPorter config
80
+ check('MCPorter config', () => {
81
+ const cfgPath = path.join(env.workspaceDir, 'mcporter.json');
82
+ if (!fs.existsSync(cfgPath)) {
83
+ return { component: 'MCPorter config', status: 'FAIL', detail: `${cfgPath} not found` };
84
+ }
85
+ try {
86
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
87
+ const hasEntry = cfg?.servers?.['memos-memu-local-memory-tools-for-agent'];
88
+ return {
89
+ component: 'MCPorter config',
90
+ status: hasEntry ? 'PASS' : 'FAIL',
91
+ detail: hasEntry ? 'MCP server entry found' : 'MCP server entry missing',
92
+ };
93
+ } catch (err) {
94
+ return { component: 'MCPorter config', status: 'FAIL', detail: `JSON parse error: ${err.message}` };
95
+ }
96
+ });
97
+
98
+ // 7. Cron
99
+ check('Sync cron', () => {
100
+ try {
101
+ const out = execSync(
102
+ 'crontab -l 2>/dev/null | grep memuk-sync; cat /etc/cron.d/memuk-sync 2>/dev/null || echo ""',
103
+ { timeout: 5000 }
104
+ ).toString();
105
+ return {
106
+ component: 'Sync cron',
107
+ status: out.includes('sync_memos') ? 'PASS' : 'FAIL',
108
+ detail: out.includes('sync_memos') ? 'Cron entry found' : 'No sync cron entry found',
109
+ };
110
+ } catch {
111
+ return { component: 'Sync cron', status: 'FAIL', detail: 'Cron check failed' };
112
+ }
113
+ });
114
+
115
+ const passed = results.filter(r => r.status === 'PASS').length;
116
+ const failed = results.filter(r => r.status === 'FAIL').length;
117
+ const summary = failed === 0
118
+ ? `✅ All checks passed (${passed}/${passed})`
119
+ : `⚠️ ${passed} passed · ${failed} failed`;
120
+
121
+ const lines = results.map(
122
+ r => `${r.status} ${r.component}\n → ${r.detail}`
123
+ );
124
+
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: `🔬 memory-system verify\n\n${lines.join('\n\n')}\n\n${'─'.repeat(50)}\n${summary}`,
130
+ },
131
+ ],
132
+ };
133
+ }