@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 +187 -0
- package/package.json +36 -0
- package/src/index.js +138 -0
- package/src/shared/db.js +114 -0
- package/src/shared/install-steps.js +37 -0
- package/src/tools/install.js +303 -0
- package/src/tools/memos.js +61 -0
- package/src/tools/memuk.js +55 -0
- package/src/tools/verify.js +133 -0
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;
|
package/src/shared/db.js
ADDED
|
@@ -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
|
+
}
|