@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 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.2",
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",
@@ -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
- // ── Docker ────────────────────────────────────────────────────────────────
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 is not installed or not running. Install: https://docs.docker.com/get-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', 'Docker is available');
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 — skipping');
97
+ report.skip('memos-postgres container', 'Already running');
57
98
  } else {
58
99
  const containerExists = (() => {
59
- try {
60
- shSync(`docker inspect ${env.memosContainer}`, { timeout: 5000 });
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 stopped container');
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 and started postgres:15-alpine');
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 to be ready (up to 30s)
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
- shSync(`docker exec ${env.memosContainer} pg_isready -U ${env.memosUser}`, { timeout: 3000 });
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 is ready');
142
+ report.ok('memos-postgres', 'PostgreSQL ready');
108
143
 
109
- // ── memos DB ─────────────────────────────────────────────────────────────
144
+ // ── memos DB ────────────────────────────────────────────────────────────
110
145
  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
- }
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
- summary TEXT NOT NULL,
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
- content TEXT NOT NULL,
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 testCount = sqlite('SELECT COUNT(*) FROM memu_memory_items;', env.memukPath).trim();
159
- report.ok('memuK SQLite', `Schema initialized · ${testCount} memory_items`);
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 = path.join(env.skillDir, 'memory-triple-recall');
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 today = new Date().toISOString().split('T')[0];
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 (!fs.existsSync(skillFile)) {
215
- fs.writeFileSync(skillFile, skillContent);
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', 'Already existsnot overwriting');
198
+ report.skip('memory-triple-recall SKILL', 'Preserving existingbundle 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 cronContent = `\
226
- # memos memuK sync every 15 minutes
227
- */15 * * * * root /var/lib/openclaw/.openclaw/workspace/scripts/sync_memos_to_memuk.py >> /var/lib/openclaw/.openclaw/workspace/logs/memuk-sync.log 2>&1
228
- `.trim();
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
- fs.writeFileSync(cronPath, cronContent + '\n', { mode: 0o644 });
234
- report.ok('Sync cron', `Installed ${cronPath}`);
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 "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 -`,
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 via user crontab');
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
- // ── MCPorter config ────────────────────────────────────────────────────────
248
- const pkgName = 'memos-memu-local-memory-tools-for-agent';
249
- const mcporterConfigPath = path.join(env.workspaceDir, 'mcporter.json');
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 check ───────────────────────────────────────────────────────────
234
+ // ── Final checks ───────────────────────────────────────────────────────
280
235
  try {
281
236
  psql('SELECT 1;');
282
- report.ok('Final memos check', 'Responds OK');
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', 'Responds OK');
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
- type: 'text',
298
- text: `🛠️ memory-system install complete\n\n${report.toText()}`,
299
- },
300
- ],
250
+ content: [{
251
+ type: 'text',
252
+ text: `🛠️ memory-system install complete\n\n${report.toText()}`,
253
+ }],
301
254
  };
302
255
  }