@bsbofmusic/memos-memu-local-memory-tools-for-agent 1.3.2 → 1.3.4
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/SKILL.md +115 -0
- package/SKILL.md.bak +115 -0
- package/package.json +1 -1
- package/src/tools/install.js +97 -144
package/SKILL.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-triple-recall
|
|
3
|
+
description: |
|
|
4
|
+
三层记忆召回:File Brain → memos + memuK(MCP,同时查询)。
|
|
5
|
+
默认调用:mcporter call memos-memu-local-memory-tools-for-agent memos_memuk_query query="<关键词>"
|
|
6
|
+
Layer 2 强制并行,不允许串行分开查。
|
|
7
|
+
triggers: 回忆|回想|记得|原话|哪天|什么时候|答应过|之前|历史|进展|有没有说过|问过你
|
|
8
|
+
version: 2.2.0
|
|
9
|
+
updated: 2026-03-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# 三层记忆召回 — Memory Triple Recall
|
|
13
|
+
|
|
14
|
+
## 架构
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Layer 1 File Brain → 稳定事实、规则、拍板决定
|
|
18
|
+
Layer 2 memos + memuK ← 必须同时触发,不用等
|
|
19
|
+
↓ memos(原话、原始内容)+ memuK(摘要、跨session)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 调用规则(强制)
|
|
25
|
+
|
|
26
|
+
### Layer 1:File Brain
|
|
27
|
+
- 直接读 `MEMORY.md` / `memory/*.md`
|
|
28
|
+
- 适用:系统规则、拍板结论、阶段总结、配置拍板
|
|
29
|
+
|
|
30
|
+
### Layer 2:memos + memuK(MCP,必须同时触发)
|
|
31
|
+
```
|
|
32
|
+
mcporter call memos-memu-local-memory-tools-for-agent memos_memuk_query query="<关键词>" limit=10
|
|
33
|
+
```
|
|
34
|
+
- **关键词提取规则(必须执行)**:
|
|
35
|
+
1. 先把问题拆成短句,去掉语气词(吗、呢、吧、啊、呀)、标点符号
|
|
36
|
+
2. 提取核心名词、动词短语、人名、地名、项目名
|
|
37
|
+
3. 多个关键词都要试,不要只试一个
|
|
38
|
+
4. 常见例子:
|
|
39
|
+
- "我喜欢的女星是谁" → 优先试"Leah Gotti",然后"喜欢的女星",然后"女星"
|
|
40
|
+
- "之前我们有没有聊过npm token的事" → 优先试"npm token",然后"npm",然后"token"
|
|
41
|
+
- "3月9日有什么重要的事" → 优先试"3月9日",然后"重要的事"
|
|
42
|
+
- "穿孔枕项目最近进展" → 优先试"穿孔枕",然后"piercing pillow",然后"项目进展"
|
|
43
|
+
- 适用:**所有个人事实类问题**,包括但不限于:
|
|
44
|
+
- 喜欢什么、最爱谁
|
|
45
|
+
- 女友、crush、女星
|
|
46
|
+
- 个人偏好、私人事实
|
|
47
|
+
- 具体承诺、时间点
|
|
48
|
+
- "之前说过"、"原话"
|
|
49
|
+
- **禁止**:凭记忆直接答,必须调 MCP 工具查
|
|
50
|
+
- 如果 Layer 1 已经明确记载,直接答;Layer 1 没有的,必须走 Layer 2(用合并的工具同时查两个数据库)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 问题路由表
|
|
55
|
+
|
|
56
|
+
| 问题类型 | 示例 | 路由 |
|
|
57
|
+
|---------|------|------|
|
|
58
|
+
| 系统规则/配置 | "我的 MCP 有哪些工具" | Layer 1(MEMORY.md)|
|
|
59
|
+
| 个人偏好/事实 | "我喜欢什么"、"我女友是谁" | **Layer 2 必须** |
|
|
60
|
+
| 承诺/时间点 | "我什么时候答应过" | **Layer 2 必须** |
|
|
61
|
+
| 原话 | "我当时怎么说的" | **Layer 2 必须** |
|
|
62
|
+
| 模糊主题 | "我们聊过 XXX 吗" | Layer 2 → Layer 3 |
|
|
63
|
+
| 稳定结论 | "上次排障结论是什么" | Layer 1 已有则直接答 |
|
|
64
|
+
| 最新 session | 最近 15 分钟的对话 | Layer 2(还没被 ingest 则走 session)|
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 输出格式
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
已确认事实(来自 Layer X):
|
|
72
|
+
- ...
|
|
73
|
+
|
|
74
|
+
原话·证据(来自 memos id:xxx):
|
|
75
|
+
> ...
|
|
76
|
+
|
|
77
|
+
仍不确定点:
|
|
78
|
+
- ...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
三层全空时:
|
|
82
|
+
```
|
|
83
|
+
已确认事实 → [空]
|
|
84
|
+
原话·证据 → [空]
|
|
85
|
+
仍不确定点 → 此话题在三层记忆中均无记录。
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 强制执行约束
|
|
91
|
+
|
|
92
|
+
1. **Layer 2 必须并行**:任何个人事实类问题,第一动作必须是 `memos_memuk_query` 并行同时查两个库,不允许先查一个再查另一个
|
|
93
|
+
2. **不制造记忆**:memos + memuK 都查不到就说查不到,禁止编造
|
|
94
|
+
3. **证据要引用**:从返回结果里引用原文,不要自己总结
|
|
95
|
+
4. **版本时效**:memos ingest 延迟 ~15 分钟,超短窗口内的问题可标注"可能尚未 ingest"
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## MCP 工具参考
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Layer 2 查询(memos + memuK 同时并行,禁止分开)
|
|
103
|
+
mcporter call memos-memu-local-memory-tools-for-agent memos_memuk_query query="<关键词>" limit=10
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 零证据时的标准回复
|
|
109
|
+
|
|
110
|
+
当三层全空时,使用以下标准回复,不要自行发挥:
|
|
111
|
+
|
|
112
|
+
> 已确认事实 → [空]
|
|
113
|
+
> 原话·证据 → [空]
|
|
114
|
+
> 仍不确定点 → 此话题在三层记忆中均无记录。建议直接告诉我。
|
|
115
|
+
|
package/SKILL.md.bak
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-triple-recall
|
|
3
|
+
description: |
|
|
4
|
+
三层记忆召回:File Brain → memos + memuK(MCP,同时查询)。
|
|
5
|
+
默认调用:mcporter call memos-memu-local-memory-tools-for-agent memos_memuk_query query="<关键词>"
|
|
6
|
+
Layer 2 强制并行,不允许串行分开查。
|
|
7
|
+
triggers: 回忆|回想|记得|原话|哪天|什么时候|答应过|之前|历史|进展|有没有说过|问过你
|
|
8
|
+
version: 2.2.0
|
|
9
|
+
updated: 2026-03-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# 三层记忆召回 — Memory Triple Recall
|
|
13
|
+
|
|
14
|
+
## 架构
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Layer 1 File Brain → 稳定事实、规则、拍板决定
|
|
18
|
+
Layer 2 memos + memuK ← 必须同时触发,不用等
|
|
19
|
+
↓ memos(原话、原始内容)+ memuK(摘要、跨session)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 调用规则(强制)
|
|
25
|
+
|
|
26
|
+
### Layer 1:File Brain
|
|
27
|
+
- 直接读 `MEMORY.md` / `memory/*.md`
|
|
28
|
+
- 适用:系统规则、拍板结论、阶段总结、配置拍板
|
|
29
|
+
|
|
30
|
+
### Layer 2:memos + memuK(MCP,必须同时触发)
|
|
31
|
+
```
|
|
32
|
+
mcporter call memos-memu-local-memory-tools-for-agent memos_memuk_query query="<关键词>" limit=10
|
|
33
|
+
```
|
|
34
|
+
- **关键词提取规则(必须执行)**:
|
|
35
|
+
1. 先把问题拆成短句,去掉语气词(吗、呢、吧、啊、呀)、标点符号
|
|
36
|
+
2. 提取核心名词、动词短语、人名、地名、项目名
|
|
37
|
+
3. 多个关键词都要试,不要只试一个
|
|
38
|
+
4. 常见例子:
|
|
39
|
+
- "我喜欢的女星是谁" → 优先试"Leah Gotti",然后"喜欢的女星",然后"女星"
|
|
40
|
+
- "之前我们有没有聊过npm token的事" → 优先试"npm token",然后"npm",然后"token"
|
|
41
|
+
- "3月9日有什么重要的事" → 优先试"3月9日",然后"重要的事"
|
|
42
|
+
- "穿孔枕项目最近进展" → 优先试"穿孔枕",然后"piercing pillow",然后"项目进展"
|
|
43
|
+
- 适用:**所有个人事实类问题**,包括但不限于:
|
|
44
|
+
- 喜欢什么、最爱谁
|
|
45
|
+
- 女友、crush、女星
|
|
46
|
+
- 个人偏好、私人事实
|
|
47
|
+
- 具体承诺、时间点
|
|
48
|
+
- "之前说过"、"原话"
|
|
49
|
+
- **禁止**:凭记忆直接答,必须调 MCP 工具查
|
|
50
|
+
- 如果 Layer 1 已经明确记载,直接答;Layer 1 没有的,必须走 Layer 2(用合并的工具同时查两个数据库)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 问题路由表
|
|
55
|
+
|
|
56
|
+
| 问题类型 | 示例 | 路由 |
|
|
57
|
+
|---------|------|------|
|
|
58
|
+
| 系统规则/配置 | "我的 MCP 有哪些工具" | Layer 1(MEMORY.md)|
|
|
59
|
+
| 个人偏好/事实 | "我喜欢什么"、"我女友是谁" | **Layer 2 必须** |
|
|
60
|
+
| 承诺/时间点 | "我什么时候答应过" | **Layer 2 必须** |
|
|
61
|
+
| 原话 | "我当时怎么说的" | **Layer 2 必须** |
|
|
62
|
+
| 模糊主题 | "我们聊过 XXX 吗" | Layer 2 → Layer 3 |
|
|
63
|
+
| 稳定结论 | "上次排障结论是什么" | Layer 1 已有则直接答 |
|
|
64
|
+
| 最新 session | 最近 15 分钟的对话 | Layer 2(还没被 ingest 则走 session)|
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 输出格式
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
已确认事实(来自 Layer X):
|
|
72
|
+
- ...
|
|
73
|
+
|
|
74
|
+
原话·证据(来自 memos id:xxx):
|
|
75
|
+
> ...
|
|
76
|
+
|
|
77
|
+
仍不确定点:
|
|
78
|
+
- ...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
三层全空时:
|
|
82
|
+
```
|
|
83
|
+
已确认事实 → [空]
|
|
84
|
+
原话·证据 → [空]
|
|
85
|
+
仍不确定点 → 此话题在三层记忆中均无记录。
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 强制执行约束
|
|
91
|
+
|
|
92
|
+
1. **Layer 2 必须并行**:任何个人事实类问题,第一动作必须是 `memos_memuk_query` 并行同时查两个库,不允许先查一个再查另一个
|
|
93
|
+
2. **不制造记忆**:memos + memuK 都查不到就说查不到,禁止编造
|
|
94
|
+
3. **证据要引用**:从返回结果里引用原文,不要自己总结
|
|
95
|
+
4. **版本时效**:memos ingest 延迟 ~15 分钟,超短窗口内的问题可标注"可能尚未 ingest"
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## MCP 工具参考
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Layer 2 查询(memos + memuK 同时并行,禁止分开)
|
|
103
|
+
mcporter call memos-memu-local-memory-tools-for-agent memos_memuk_query query="<关键词>" limit=10
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 零证据时的标准回复
|
|
109
|
+
|
|
110
|
+
当三层全空时,使用以下标准回复,不要自行发挥:
|
|
111
|
+
|
|
112
|
+
> 已确认事实 → [空]
|
|
113
|
+
> 原话·证据 → [空]
|
|
114
|
+
> 仍不确定点 → 此话题在三层记忆中均无记录。建议直接告诉我。
|
|
115
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsbofmusic/memos-memu-local-memory-tools-for-agent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "MCP server — one-shot install + query for memos (PostgreSQL) and memuK (SQLite) local memory. Designed for OpenClaw agents.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
package/src/tools/install.js
CHANGED
|
@@ -40,39 +40,77 @@ class InstallReport {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Managed cron entry header — marks this file as managed by install_memory_system.
|
|
45
|
+
* Re-running install will overwrite this file with a fresh header + entry.
|
|
46
|
+
*/
|
|
47
|
+
function buildCronEntry(env, syncScript, syncLog, envFile) {
|
|
48
|
+
const python = process.env.HOME
|
|
49
|
+
? `${process.env.HOME}/.openclaw/workspace/memuk-venv/bin/python`
|
|
50
|
+
: 'python3';
|
|
51
|
+
|
|
52
|
+
return `# ─────────────────────────────────────────────────────────────────────
|
|
53
|
+
# Managed by: install_memory_system (memos-memu-local-memory-tools-for-agent)
|
|
54
|
+
# Manual edits to this file will be overwritten on next install.
|
|
55
|
+
# To remove: delete this file or run: rm /etc/cron.d/memusync
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
57
|
+
# Source credentials — do not hardcode secrets in cron entries
|
|
58
|
+
${envFile ? `source ${envFile}` : ''}
|
|
59
|
+
|
|
60
|
+
# memos → memuK sync every 15 minutes
|
|
61
|
+
*/15 * * * * ${process.env.USER || 'root'} ${python} ${syncScript} \\
|
|
62
|
+
--batch-size 50 >> ${syncLog} 2>&1
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
export async function install_memory_system() {
|
|
44
67
|
const env = detectEnv();
|
|
45
68
|
const report = new InstallReport();
|
|
46
69
|
|
|
47
|
-
// ──
|
|
70
|
+
// ── .env 凭证文件 ─────────────────────────────────────────────────────
|
|
71
|
+
const secretsDir = path.join(env.stateDir, 'secrets');
|
|
72
|
+
const envFile = path.join(secretsDir, 'memusync.env');
|
|
73
|
+
const envContent = [
|
|
74
|
+
`# Managed by install_memory_system — do not commit this file`,
|
|
75
|
+
`MEMOS_DB_PASSWORD=${env.memosPassword}`,
|
|
76
|
+
`OPENAI_API_KEY=${process.env.OPENAI_API_KEY || ''}`,
|
|
77
|
+
].join('\n') + '\n';
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
fs.mkdirSync(secretsDir, { recursive: true });
|
|
81
|
+
fs.writeFileSync(envFile, envContent, { mode: 0o600 }); // owner-only read
|
|
82
|
+
report.ok('.env credentials', `Written → ${envFile} (mode 0600)`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
report.fail('.env credentials', err.message);
|
|
85
|
+
return { content: [{ type: 'text', text: report.toText() }] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Docker ──────────────────────────────────────────────────────────────
|
|
48
89
|
if (!dockerAvailable()) {
|
|
49
|
-
report.fail('Docker', 'Docker
|
|
90
|
+
report.fail('Docker', 'Docker not available. Install: https://docs.docker.com/get-docker/');
|
|
50
91
|
return { content: [{ type: 'text', text: report.toText() }] };
|
|
51
92
|
}
|
|
52
|
-
report.ok('Docker', '
|
|
93
|
+
report.ok('Docker', 'available');
|
|
53
94
|
|
|
54
|
-
// ── Container
|
|
95
|
+
// ── Container ──────────────────────────────────────────────────────────
|
|
55
96
|
if (dockerContainerRunning(env.memosContainer)) {
|
|
56
|
-
report.skip('memos-postgres container', 'Already running
|
|
97
|
+
report.skip('memos-postgres container', 'Already running');
|
|
57
98
|
} else {
|
|
58
99
|
const containerExists = (() => {
|
|
59
|
-
try {
|
|
60
|
-
|
|
61
|
-
return true;
|
|
62
|
-
} catch { return false; }
|
|
100
|
+
try { shSync(`docker inspect ${env.memosContainer}`, { timeout: 5000 }); return true; }
|
|
101
|
+
catch { return false; }
|
|
63
102
|
})();
|
|
64
103
|
|
|
65
104
|
if (containerExists) {
|
|
66
105
|
try {
|
|
67
106
|
shSync(`docker start ${env.memosContainer}`, { timeout: 15000 });
|
|
68
|
-
report.ok('memos-postgres container', 'Started existing
|
|
107
|
+
report.ok('memos-postgres container', 'Started existing container');
|
|
69
108
|
} catch (err) {
|
|
70
109
|
report.fail('memos-postgres container', `docker start failed: ${err.message}`);
|
|
71
110
|
return { content: [{ type: 'text', text: report.toText() }] };
|
|
72
111
|
}
|
|
73
112
|
} else {
|
|
74
113
|
try {
|
|
75
|
-
// Pull and start postgres alpine
|
|
76
114
|
shSync(
|
|
77
115
|
`docker run -d --name ${env.memosContainer} ` +
|
|
78
116
|
`-e POSTGRES_USER=${env.memosUser} ` +
|
|
@@ -82,7 +120,7 @@ export async function install_memory_system() {
|
|
|
82
120
|
`postgres:15-alpine`,
|
|
83
121
|
{ timeout: 60000 }
|
|
84
122
|
);
|
|
85
|
-
report.ok('memos-postgres container', 'Created
|
|
123
|
+
report.ok('memos-postgres container', 'Created + started postgres:15-alpine');
|
|
86
124
|
} catch (err) {
|
|
87
125
|
report.fail('memos-postgres container', `docker run failed: ${err.message}`);
|
|
88
126
|
return { content: [{ type: 'text', text: report.toText() }] };
|
|
@@ -90,213 +128,128 @@ export async function install_memory_system() {
|
|
|
90
128
|
}
|
|
91
129
|
}
|
|
92
130
|
|
|
93
|
-
// Wait for postgres
|
|
131
|
+
// Wait for postgres ready (up to 30s)
|
|
94
132
|
let pgReady = false;
|
|
95
133
|
for (let i = 0; i < 20; i++) {
|
|
96
134
|
await new Promise(r => setTimeout(r, 1500));
|
|
97
|
-
try {
|
|
98
|
-
|
|
99
|
-
pgReady = true;
|
|
100
|
-
break;
|
|
101
|
-
} catch {}
|
|
135
|
+
try { shSync(`docker exec ${env.memosContainer} pg_isready -U ${env.memosUser}`, { timeout: 3000 }); pgReady = true; break; }
|
|
136
|
+
catch {}
|
|
102
137
|
}
|
|
103
138
|
if (!pgReady) {
|
|
104
139
|
report.fail('memos-postgres', 'PostgreSQL did not become ready in 30s');
|
|
105
140
|
return { content: [{ type: 'text', text: report.toText() }] };
|
|
106
141
|
}
|
|
107
|
-
report.ok('memos-postgres', 'PostgreSQL
|
|
142
|
+
report.ok('memos-postgres', 'PostgreSQL ready');
|
|
108
143
|
|
|
109
|
-
// ── memos DB
|
|
144
|
+
// ── memos DB ────────────────────────────────────────────────────────────
|
|
110
145
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
report.ok('memos PostgreSQL', 'Database is accessible');
|
|
114
|
-
} else {
|
|
115
|
-
throw new Error('Unexpected query result');
|
|
116
|
-
}
|
|
146
|
+
if (psql('SELECT 1 AS ok;').includes('1')) report.ok('memos PostgreSQL', 'Accessible');
|
|
147
|
+
else throw new Error('Unexpected query result');
|
|
117
148
|
} catch (err) {
|
|
118
149
|
report.fail('memos PostgreSQL', err.message);
|
|
119
150
|
return { content: [{ type: 'text', text: report.toText() }] };
|
|
120
151
|
}
|
|
121
152
|
|
|
122
|
-
// ── memuK SQLite
|
|
153
|
+
// ── memuK SQLite ────────────────────────────────────────────────────────
|
|
123
154
|
const memukDir = path.dirname(env.memukPath);
|
|
124
|
-
if (!fs.existsSync(memukDir)) {
|
|
125
|
-
fs.mkdirSync(memukDir, { recursive: true });
|
|
126
|
-
}
|
|
155
|
+
if (!fs.existsSync(memukDir)) fs.mkdirSync(memukDir, { recursive: true });
|
|
127
156
|
|
|
128
|
-
// Always initialize as a fresh SQLite DB — overwrite if exists to avoid
|
|
129
|
-
// schema corruption from partial appends.
|
|
130
157
|
const schema = `
|
|
131
158
|
CREATE TABLE IF NOT EXISTS memu_memory_items (
|
|
132
|
-
id TEXT PRIMARY KEY,
|
|
133
|
-
|
|
134
|
-
memory_type TEXT DEFAULT 'imported',
|
|
135
|
-
happened_at TEXT,
|
|
159
|
+
id TEXT PRIMARY KEY, summary TEXT NOT NULL,
|
|
160
|
+
memory_type TEXT DEFAULT 'imported', happened_at TEXT,
|
|
136
161
|
user_id TEXT DEFAULT 'default',
|
|
137
162
|
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
138
163
|
);
|
|
139
164
|
CREATE TABLE IF NOT EXISTS memu_sync_checkpoint (
|
|
140
|
-
key TEXT PRIMARY KEY,
|
|
141
|
-
value TEXT NOT NULL,
|
|
165
|
+
key TEXT PRIMARY KEY, value TEXT NOT NULL,
|
|
142
166
|
updated_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
143
167
|
);
|
|
144
168
|
CREATE TABLE IF NOT EXISTS memu_raw_memos (
|
|
145
|
-
id INTEGER PRIMARY KEY,
|
|
146
|
-
|
|
147
|
-
raw_json TEXT,
|
|
148
|
-
created_ts INTEGER,
|
|
169
|
+
id INTEGER PRIMARY KEY, content TEXT NOT NULL,
|
|
170
|
+
raw_json TEXT, created_ts INTEGER,
|
|
149
171
|
synced_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
150
172
|
);
|
|
151
|
-
-- Initialize checkpoint with a placeholder so table is not empty
|
|
152
173
|
INSERT OR IGNORE INTO memu_sync_checkpoint (key, value) VALUES ('last_memo_id', '0');
|
|
153
174
|
`.trim();
|
|
154
175
|
|
|
155
176
|
try {
|
|
156
|
-
// Always write fresh schema — avoids partial schema from previous installs
|
|
157
177
|
fs.writeFileSync(env.memukPath, schema + '\n');
|
|
158
|
-
const
|
|
159
|
-
report.ok('memuK SQLite', `Schema
|
|
178
|
+
const count = sqlite('SELECT COUNT(*) FROM memu_memory_items;', env.memukPath).trim();
|
|
179
|
+
report.ok('memuK SQLite', `Schema ready · ${count} items`);
|
|
160
180
|
} catch (err) {
|
|
161
181
|
report.fail('memuK SQLite', err.message);
|
|
162
182
|
return { content: [{ type: 'text', text: report.toText() }] };
|
|
163
183
|
}
|
|
164
184
|
|
|
165
|
-
// ── SKILL
|
|
166
|
-
const skillDir
|
|
185
|
+
// ── SKILL.md ───────────────────────────────────────────────────────────
|
|
186
|
+
const skillDir = path.join(env.workspaceDir, 'skills', 'memory-triple-recall');
|
|
167
187
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const skillContent = `---
|
|
171
|
-
name: memory-triple-recall
|
|
172
|
-
description: |
|
|
173
|
-
Three-layer memory recall stack: MEMORY.md → memos (PostgreSQL) → memuK (SQLite).
|
|
174
|
-
Use for: original quotes, timing, commitments, topic recall, historical context.
|
|
175
|
-
triggers: 回忆|回想|记得|原话|哪天|什么时候|答应过|之前|历史|进展
|
|
176
|
-
version: 1.0.0
|
|
177
|
-
updated: ${today}
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
# Memory Triple Recall
|
|
181
|
-
|
|
182
|
-
## Architecture
|
|
183
|
-
| Layer | Store | Access |
|
|
184
|
-
|-------|-------|--------|
|
|
185
|
-
| File brain | MEMORY.md / memory/*.md | File system |
|
|
186
|
-
| memos | PostgreSQL Docker | memos_query MCP tool |
|
|
187
|
-
| memuK | SQLite | memuk_search MCP tool |
|
|
188
|
-
|
|
189
|
-
## Fast path
|
|
190
|
-
Do NOT trigger layered recall for atomic facts in USER.md / IDENTITY.md / injected context.
|
|
191
|
-
|
|
192
|
-
## NEVER fast-path (always do Layer 1+2)
|
|
193
|
-
- 喜欢、最爱、最喜欢的X
|
|
194
|
-
- 女友、crush、个人偏好、私人事实
|
|
195
|
-
|
|
196
|
-
## Default order
|
|
197
|
-
1. File brain: stable facts, rules, decisions
|
|
198
|
-
2. memos: original wording, timing, commitments (memos_query)
|
|
199
|
-
3. memuK: fuzzy topic recall (memuk_search)
|
|
200
|
-
|
|
201
|
-
## Output shape
|
|
202
|
-
- 已确认事实
|
|
203
|
-
- 原话·证据
|
|
204
|
-
- 仍不确定点
|
|
205
|
-
|
|
206
|
-
## Zero-evidence fallback
|
|
207
|
-
已确认事实 → [空]
|
|
208
|
-
原话·证据 → [空]
|
|
209
|
-
仍不确定点 → 此话题在三层记忆中均无记录。
|
|
210
|
-
`;
|
|
188
|
+
const bundledSkill = path.join(__dirname, '..', '..', 'SKILL.md');
|
|
211
189
|
|
|
212
190
|
try {
|
|
213
191
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
214
|
-
if (
|
|
215
|
-
fs.
|
|
192
|
+
if (fs.existsSync(bundledSkill)) {
|
|
193
|
+
fs.copyFileSync(bundledSkill, skillFile);
|
|
216
194
|
report.ok('memory-triple-recall SKILL', `Installed → ${skillFile}`);
|
|
195
|
+
} else if (!fs.existsSync(skillFile)) {
|
|
196
|
+
report.fail('memory-triple-recall SKILL', `Bundled SKILL.md not found at ${bundledSkill}`);
|
|
217
197
|
} else {
|
|
218
|
-
report.skip('memory-triple-recall SKILL', '
|
|
198
|
+
report.skip('memory-triple-recall SKILL', 'Preserving existing — bundle missing');
|
|
219
199
|
}
|
|
220
200
|
} catch (err) {
|
|
221
201
|
report.fail('memory-triple-recall SKILL', err.message);
|
|
222
202
|
}
|
|
223
203
|
|
|
224
|
-
// ── Cron
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
204
|
+
// ── Cron(幂等 /etc/cron.d/ + managed header)─────────────────────────
|
|
205
|
+
const syncScript = path.join(env.workspaceDir, 'sync_memos_to_memuk.py');
|
|
206
|
+
const syncLog = path.join(env.workspaceDir, 'logs', 'memuk', 'sync.log');
|
|
207
|
+
const cronPath = '/etc/cron.d/memusync';
|
|
208
|
+
const cronContent = buildCronEntry(env, syncScript, syncLog, envFile);
|
|
229
209
|
|
|
230
|
-
const cronPath = '/etc/cron.d/memuk-sync';
|
|
231
210
|
try {
|
|
232
211
|
fs.mkdirSync(path.dirname(cronPath), { recursive: true });
|
|
233
|
-
|
|
234
|
-
|
|
212
|
+
// Idempotent: always overwrite with fresh managed header
|
|
213
|
+
fs.writeFileSync(cronPath, cronContent, { mode: 0o644 });
|
|
214
|
+
report.ok('Sync cron (/etc/cron.d)', `Installed → ${cronPath}`);
|
|
235
215
|
} catch {
|
|
216
|
+
// Fallback to user crontab if /etc/cron.d not writable
|
|
236
217
|
try {
|
|
237
218
|
await sh(
|
|
238
|
-
`(crontab -l 2>/dev/null | grep -v "
|
|
219
|
+
`(crontab -l 2>/dev/null | grep -v "memusync"; echo "` +
|
|
220
|
+
buildCronEntry(env, syncScript, syncLog, envFile).replace(/\n/g, ' \\\n') +
|
|
221
|
+
`") | crontab -`,
|
|
239
222
|
{ timeout: 5000 }
|
|
240
223
|
);
|
|
241
|
-
report.ok('Sync cron', 'Installed
|
|
224
|
+
report.ok('Sync cron (user crontab)', 'Installed with managed header');
|
|
242
225
|
} catch (err) {
|
|
243
226
|
report.fail('Sync cron', err.message);
|
|
244
227
|
}
|
|
245
228
|
}
|
|
246
229
|
|
|
247
|
-
// ──
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
let existingConfig = {};
|
|
251
|
-
if (fs.existsSync(mcporterConfigPath)) {
|
|
252
|
-
try {
|
|
253
|
-
existingConfig = JSON.parse(fs.readFileSync(mcporterConfigPath, 'utf8'));
|
|
254
|
-
} catch {}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const newEntry = {
|
|
258
|
-
args: ['-y', `@bsbofmusic/${pkgName}`],
|
|
259
|
-
command: 'npx',
|
|
260
|
-
type: 'stdio',
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const merged = {
|
|
264
|
-
...existingConfig,
|
|
265
|
-
servers: {
|
|
266
|
-
...(existingConfig.servers || {}),
|
|
267
|
-
[pkgName]: newEntry,
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
fs.mkdirSync(path.dirname(mcporterConfigPath), { recursive: true });
|
|
273
|
-
fs.writeFileSync(mcporterConfigPath, JSON.stringify(merged, null, 2) + '\n');
|
|
274
|
-
report.ok('MCPorter MCP config', `Updated ${mcporterConfigPath}`);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
report.fail('MCPorter MCP config', err.message);
|
|
277
|
-
}
|
|
230
|
+
// ── mcporter.json(已移除)─────────────────────────────────────────────
|
|
231
|
+
// mcporter auto-discovers MCP tools from globally installed npm packages.
|
|
232
|
+
// No manual config write needed. This step is intentionally absent.
|
|
278
233
|
|
|
279
|
-
// ── Final
|
|
234
|
+
// ── Final checks ───────────────────────────────────────────────────────
|
|
280
235
|
try {
|
|
281
236
|
psql('SELECT 1;');
|
|
282
|
-
report.ok('Final memos check', '
|
|
237
|
+
report.ok('Final memos check', 'OK');
|
|
283
238
|
} catch (err) {
|
|
284
239
|
report.fail('Final memos check', err.message);
|
|
285
240
|
}
|
|
286
241
|
|
|
287
242
|
try {
|
|
288
243
|
sqlite('SELECT 1;', env.memukPath);
|
|
289
|
-
report.ok('Final memuK check', '
|
|
244
|
+
report.ok('Final memuK check', 'OK');
|
|
290
245
|
} catch (err) {
|
|
291
246
|
report.fail('Final memuK check', err.message);
|
|
292
247
|
}
|
|
293
248
|
|
|
294
249
|
return {
|
|
295
|
-
content: [
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
},
|
|
300
|
-
],
|
|
250
|
+
content: [{
|
|
251
|
+
type: 'text',
|
|
252
|
+
text: `🛠️ memory-system install complete\n\n${report.toText()}`,
|
|
253
|
+
}],
|
|
301
254
|
};
|
|
302
255
|
}
|