memory-eternal 0.9.23 → 0.10.2

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.
@@ -1,188 +1,318 @@
1
- #!/usr/bin/env node
2
- // 记忆核心 · CLI(多宿主统一入口)
3
- //
4
- // 用法:
5
- // dsh-memory recall <query> [--limit N] [--vault DIR] 检索知识卡
6
- // dsh-memory capture <text | - > [--source TAG] 手动沉淀(- 读 stdin)
7
- // dsh-memory serve [--port N] [--vault DIR] 启动 Web UI(前台常驻)
8
- // dsh-memory open [--port N] 确保 Web 存活并用浏览器打开
9
- // dsh-memory mcp MCP stdio server(各 agent 挂载)
10
- // dsh-memory setup [--claude-only|--codex-only|--cursor-only] [--dry-run] [--no-hooks]
11
- // 自动挂载 MCP 到已装的 agent
12
- // dsh-memory sweep <dir> 挖掘 Claude Code 会话 JSONL
13
- //
14
- // 环境变量:
15
- // MEMORY_VAULT_DIR vault 目录(默认 ~/.dsh/memory-vault)
16
- // MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL 蒸馏 LLM(OpenAI 兼容)
17
- // MEMORY_ETERNAL_SKIP_AUTO=1 setup 时不动外部配置
18
-
19
- import path from 'node:path'
20
- import process from 'node:process'
21
-
22
- const argv = process.argv.slice(2)
23
- const cmd = argv[0] || 'help'
24
- const argOf = (name) => {
25
- const i = argv.indexOf(name)
26
- return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
27
- }
28
- const has = (name) => argv.includes(name)
29
-
30
- /** 取位置参数(跳过子命令、flag 与 flag 的值) */
31
- const positional = () => {
32
- const isFlag = (s) => typeof s === 'string' && s.startsWith('--')
33
- const out = []
34
- for (let i = 1; i < argv.length; i++) {
35
- const a = argv[i]
36
- if (isFlag(a)) { i++; continue } // 跳过 flag + 它的值(任意 flag 都吃掉下一项)
37
- out.push(a)
38
- }
39
- return out
40
- }
41
-
42
- async function main() {
43
- switch (cmd) {
44
- case 'recall': {
45
- const query = positional().join(' ')
46
- const { search } = await import('../lib/vault.js')
47
- const { defaultVaultDir } = await import('../lib/capture-run.js')
48
- const root = path.resolve(argOf('--vault') || defaultVaultDir())
49
- const limit = Math.min(Math.max(Number(argOf('--limit')) || 5, 1), 20)
50
- const hits = await search(root, query, { limit, minScore: 2 })
51
- if (!hits.length) {
52
- console.log(JSON.stringify({ ok: true, hits: [], note: `记忆库中没有与「${query}」相关的内容` }, null, 2))
53
- return
54
- }
55
- console.log(JSON.stringify({ ok: true, vaultDir: root, hits }, null, 2))
56
- return
57
- }
58
- case 'capture': {
59
- const raw = positional().join(' ')
60
- let text = raw
61
- if (raw === '-' || (!raw && !process.stdin.isTTY)) {
62
- // 同步读 stdin 在 Windows 重定向下偶尔返回空(Node 22/24 行为差异)。
63
- // 改用流式读:先确保 readable 事件触发,再收集全部 data/end。
64
- text = await new Promise((resolve) => {
65
- let buf = ''
66
- const done = () => resolve(buf)
67
- process.stdin.setEncoding('utf8')
68
- process.stdin.on('data', (c) => { buf += c })
69
- process.stdin.on('end', done)
70
- process.stdin.on('error', done)
71
- // 兜底:若 stdin 已被消费完(同步场景),立即结束
72
- if (process.stdin.readableEnded) done()
73
- setTimeout(done, 3000)
74
- })
75
- }
76
- const { runStandaloneCapture, defaultVaultDir } = await import('../lib/capture-run.js')
77
- const root = path.resolve(defaultVaultDir())
78
- const out = await runStandaloneCapture(root, text, { source: argOf('--source') || 'cli' })
79
- console.log(JSON.stringify({ ...out, vaultDir: root }, null, 2))
80
- process.exitCode = out.ok ? 0 : 1
81
- return
82
- }
83
- case 'serve': {
84
- const { startWebServer, DEFAULT_WEB_PORT } = await import('../lib/web.js')
85
- const { defaultVaultDir } = await import('../lib/capture-run.js')
86
- const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
87
- const vault = path.resolve(argOf('--vault') || defaultVaultDir())
88
- await startWebServer({ port, vaultRoot: vault })
89
- process.on('SIGINT', () => process.exit(0))
90
- process.on('SIGTERM', () => process.exit(0))
91
- return
92
- }
93
- case 'open': {
94
- const { ensureWebServer, DEFAULT_WEB_PORT } = await import('../lib/web.js')
95
- const { defaultVaultDir } = await import('../lib/capture-run.js')
96
- const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
97
- const { url } = await ensureWebServer({ port, vaultRoot: path.resolve(defaultVaultDir()) })
98
- console.log(url)
99
- const plat = process.platform
100
- const { exec } = await import('node:child_process')
101
- if (plat === 'win32') exec(`start "" "${url}"`)
102
- else if (plat === 'darwin') exec(`open "${url}"`)
103
- else exec(`xdg-open "${url}"`)
104
- return
105
- }
106
- case 'mcp': {
107
- const { startMcpServer } = await import('../lib/mcp.js')
108
- startMcpServer()
109
- return
110
- }
111
- case 'watchdog': {
112
- const { startWatchdog, reapStaleWatchdogs } = await import('../lib/watchdog.js')
113
- const { defaultVaultDir } = await import('../lib/capture-run.js')
114
- const port = Number(argOf('--port')) || 7999
115
- if (has('--reap')) {
116
- const out = await reapStaleWatchdogs({ port, keepPid: 0 })
117
- console.log(`扫描到 ${out.scanned} 个 watchdog 进程,清理孤儿 ${out.killed.length} 个${out.killed.length ? '(pid ' + out.killed.join(', ') + ')' : ''},保留 ${out.skipped.length} 个(锁中登记的活跃实例 / 其它端口不会被动)`)
118
- return
119
- }
120
- const interval = Number(argOf('--interval')) || 5000
121
- const maxRestart = Number(argOf('--max-restart')) || 10
122
- const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
123
- const autoStart = !has('--no-restart')
124
- console.log(`dsh-memory watchdog 启动 → 目标端口 ${port},间隔 ${interval}ms,最大重拉 ${maxRestart} 次${autoStart ? '' : '(--no-restart)'}`)
125
- console.log(`vault: ${vaultRoot}\n按 Ctrl+C 退出`)
126
- startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
127
- return
128
- }
129
- case 'setup': {
130
- const { runSetup } = await import('../lib/setup.js')
131
- const only = []
132
- if (has('--claude-only')) only.push('claude')
133
- if (has('--codex-only')) only.push('codex')
134
- if (has('--cursor-only')) only.push('cursor')
135
- const out = await runSetup({ only, withHooks: !has('--no-hooks'), dryRun: has('--dry-run') })
136
- const done = out.results.filter((r) => r.ok).length
137
- console.log(`\n记忆库目录:默认 ~/.dsh/memory-vault(MEMORY_VAULT_DIR 可改)`)
138
- console.log(`完成:${done}/${out.results.length} 项。Web UI: dsh-memory open`)
139
- return
140
- }
141
- case 'connect': {
142
- // 会话结束自动沉淀 hook:不依赖 plugin(Codex Desktop 也适用),写用户级 hooks.json
143
- const agent = positional()[0] || ''
144
- if (!agent) {
145
- console.error('用法:dsh-memory connect <claude|codex|cursor> [--dry-run]')
146
- process.exitCode = 1
147
- return
148
- }
149
- const { connectAgentHooks } = await import('../lib/setup.js')
150
- const out = await connectAgentHooks(agent, { dryRun: has('--dry-run'), log: (m) => console.log(m) })
151
- if (out && out.ok) console.log(`\n✓ ${agent} 会话结束自动沉淀 hook 已就绪(写入统一 ~/.dsh/memory-vault,新卡进待审核)`)
152
- else { console.error(`✗ ${agent} 连接失败:` + JSON.stringify(out)); process.exitCode = 1 }
153
- return
154
- }
155
- case 'sweep': {
156
- const dir = argv[1]
157
- if (!dir || dir.startsWith('--')) {
158
- console.error('用法:dsh-memory sweep <dir>(如 ~/.claude/projects)')
159
- process.exitCode = 1
160
- return
161
- }
162
- const { sweepSessions } = await import('../lib/sweep.js')
163
- const out = await sweepSessions(dir)
164
- console.log(JSON.stringify(out, null, 2))
165
- return
166
- }
167
- default:
168
- console.log(`dsh-memory — 记忆核心 CLI
169
-
170
- 用法:
171
- dsh-memory recall <query> [--limit N] 检索知识卡
172
- dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
173
- dsh-memory serve [--port N] Web UI(前台)
174
- dsh-memory open 确保 Web 存活并打开浏览器
175
- dsh-memory mcp MCP stdio server
176
- dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
177
- dsh-memory connect <claude|codex|cursor> 写会话结束自动沉淀 hook(不依赖 plugin,含 Codex Desktop)
178
- dsh-memory sweep <dir> 挖掘会话 JSONL
179
- dsh-memory watchdog [--port N] [--interval MS] [--max-restart N] 看门狗保活 web server
180
-
181
- 环境变量:MEMORY_VAULT_DIR / MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL`)
182
- }
183
- }
184
-
185
- main().catch((error) => {
186
- console.error(String(error?.stack || error))
187
- process.exit(1)
188
- })
1
+ #!/usr/bin/env node
2
+ // 记忆核心 · CLI(多宿主统一入口)
3
+ //
4
+ // 用法:
5
+ // dsh-memory recall <query> [--limit N] [--vault DIR] 检索知识卡
6
+ // dsh-memory capture <text | - > [--source TAG] 手动沉淀(- 读 stdin)
7
+ // dsh-memory serve [--port N] [--vault DIR] 启动 Web UI(前台常驻)
8
+ // dsh-memory open [--port N] 确保 Web 存活并用浏览器打开
9
+ // dsh-memory mcp MCP stdio server(各 agent 挂载)
10
+ // dsh-memory setup [--claude-only|--codex-only|--cursor-only] [--dry-run] [--no-hooks]
11
+ // 自动挂载 MCP 到已装的 agent
12
+ // dsh-memory sweep <dir> 挖掘 Claude Code 会话 JSONL
13
+ //
14
+ // 环境变量:
15
+ // MEMORY_VAULT_DIR vault 目录(默认 ~/.dsh/memory-vault)
16
+ // MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL 蒸馏 LLM(OpenAI 兼容)
17
+ // MEMORY_ETERNAL_SKIP_AUTO=1 setup 时不动外部配置
18
+
19
+ import path from 'node:path'
20
+ import process from 'node:process'
21
+
22
+ const argv = process.argv.slice(2)
23
+ const cmd = argv[0] || 'help'
24
+ const argOf = (name) => {
25
+ const i = argv.indexOf(name)
26
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
27
+ }
28
+ const has = (name) => argv.includes(name)
29
+
30
+ /** audit 子命令的位置参数(= 卡片的 vault 相对路径),跳过 flag 与它的值 */
31
+ const auditPaths = () => {
32
+ const valueFlags = new Set(['--vault', '--kind', '--reason', '--by', '--limit', '--status'])
33
+ const out = []
34
+ for (let i = 2; i < argv.length; i++) {
35
+ const a = argv[i]
36
+ if (valueFlags.has(a)) { i++; continue }
37
+ if (typeof a === 'string' && a.startsWith('--')) continue
38
+ out.push(a)
39
+ }
40
+ return out
41
+ }
42
+
43
+ /** 取位置参数(跳过子命令、flag 与 flag 的值) */
44
+ const positional = () => {
45
+ const isFlag = (s) => typeof s === 'string' && s.startsWith('--')
46
+ const out = []
47
+ for (let i = 1; i < argv.length; i++) {
48
+ const a = argv[i]
49
+ if (isFlag(a)) { i++; continue } // 跳过 flag + 它的值(任意 flag 都吃掉下一项)
50
+ out.push(a)
51
+ }
52
+ return out
53
+ }
54
+
55
+ async function main() {
56
+ switch (cmd) {
57
+ case 'recall': {
58
+ const query = positional().join(' ')
59
+ const { search } = await import('../lib/vault.js')
60
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
61
+ const root = path.resolve(argOf('--vault') || defaultVaultDir())
62
+ const limit = Math.min(Math.max(Number(argOf('--limit')) || 5, 1), 20)
63
+ const hits = await search(root, query, { limit, minScore: 2 })
64
+ if (!hits.length) {
65
+ console.log(JSON.stringify({ ok: true, hits: [], note: `记忆库中没有与「${query}」相关的内容` }, null, 2))
66
+ return
67
+ }
68
+ console.log(JSON.stringify({ ok: true, vaultDir: root, hits }, null, 2))
69
+ return
70
+ }
71
+ case 'capture': {
72
+ const raw = positional().join(' ')
73
+ let text = raw
74
+ if (raw === '-' || (!raw && !process.stdin.isTTY)) {
75
+ // 同步读 stdin 在 Windows 重定向下偶尔返回空(Node 22/24 行为差异)。
76
+ // 改用流式读:先确保 readable 事件触发,再收集全部 data/end。
77
+ text = await new Promise((resolve) => {
78
+ let buf = ''
79
+ const done = () => resolve(buf)
80
+ process.stdin.setEncoding('utf8')
81
+ process.stdin.on('data', (c) => { buf += c })
82
+ process.stdin.on('end', done)
83
+ process.stdin.on('error', done)
84
+ // 兜底:若 stdin 已被消费完(同步场景),立即结束
85
+ if (process.stdin.readableEnded) done()
86
+ setTimeout(done, 3000)
87
+ })
88
+ }
89
+ const { runStandaloneCapture, defaultVaultDir } = await import('../lib/capture-run.js')
90
+ const root = path.resolve(defaultVaultDir())
91
+ const out = await runStandaloneCapture(root, text, { source: argOf('--source') || 'cli' })
92
+ console.log(JSON.stringify({ ...out, vaultDir: root }, null, 2))
93
+ process.exitCode = out.ok ? 0 : 1
94
+ return
95
+ }
96
+ case 'serve': {
97
+ const { startWebServer, DEFAULT_WEB_PORT } = await import('../lib/web.js')
98
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
99
+ const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
100
+ const vault = path.resolve(argOf('--vault') || defaultVaultDir())
101
+ await startWebServer({ port, vaultRoot: vault })
102
+ process.on('SIGINT', () => process.exit(0))
103
+ process.on('SIGTERM', () => process.exit(0))
104
+ return
105
+ }
106
+ case 'open': {
107
+ const { ensureWebServer, DEFAULT_WEB_PORT } = await import('../lib/web.js')
108
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
109
+ const port = Number(argOf('--port')) || DEFAULT_WEB_PORT
110
+ const { url } = await ensureWebServer({ port, vaultRoot: path.resolve(defaultVaultDir()) })
111
+ console.log(url)
112
+ const plat = process.platform
113
+ const { exec } = await import('node:child_process')
114
+ if (plat === 'win32') exec(`start "" "${url}"`)
115
+ else if (plat === 'darwin') exec(`open "${url}"`)
116
+ else exec(`xdg-open "${url}"`)
117
+ return
118
+ }
119
+ case 'mcp': {
120
+ const { startMcpServer } = await import('../lib/mcp.js')
121
+ startMcpServer()
122
+ return
123
+ }
124
+ case 'status': {
125
+ const { watchdogStatus } = await import('../lib/watchdog.js')
126
+ const port = argOf('--port')
127
+ const st = watchdogStatus({ port: port || undefined })
128
+ if (has('--json')) { console.log(JSON.stringify({ ok: true, ...st }, null, 2)); return }
129
+ console.log(`锁文件:${st.lockPath}`)
130
+ console.log(`本机包版本:${st.pkgVersion || '?'}`)
131
+ if (!st.watchdogs.length) { console.log('锁文件里没有登记的看门狗(没有常驻实例)。'); return }
132
+ for (const w of st.watchdogs) {
133
+ console.log(`- pid ${w.pid} · 端口 ${w.port} · ${w.alive ? '运行中' : '已退出(陈旧记录)'}${w.pkgVersion ? ' · v' + w.pkgVersion : ''}${w.startedAt ? ' · 启动于 ' + w.startedAt : ''}${w.vault ? ' · vault ' + w.vault : ''}`)
134
+ if (w.webPid) console.log(` web 子进程:pid ${w.webPid}(端口 ${w.webPort || w.port},${w.webAlive ? '运行中' : '已退出'})`)
135
+ if (w.versionMismatch) console.log(` ⚠ 常驻实例是旧版本(v${w.pkgVersion} ≠ 本机 v${st.pkgVersion})→ 执行 dsh-memory restart --port ${w.port} 替换`)
136
+ }
137
+ return
138
+ }
139
+ case 'stop': {
140
+ const { stopWatchdogs } = await import('../lib/watchdog.js')
141
+ const port = argOf('--port')
142
+ const out = await stopWatchdogs({ port: port || undefined })
143
+ if (has('--json')) { console.log(JSON.stringify({ ok: out.failed.length === 0, ...out }, null, 2)); return }
144
+ if (!out.stopped.length && !out.failed.length && !out.webStopped.length) console.log('没有需要停止的常驻看门狗(锁里没有活着的实例)。')
145
+ if (out.stopped.length) console.log(`已停止看门狗:pid ${out.stopped.join(', ')}`)
146
+ if (out.webStopped.length) console.log(`已停止它拉起的 web:pid ${out.webStopped.join(', ')}`)
147
+ if (out.failed.length) { console.error(`未能停止:pid ${out.failed.join(', ')}(可能无权限,请手动任务管理器 / kill 处理)`); process.exitCode = 1 }
148
+ return
149
+ }
150
+ case 'restart': {
151
+ // 显式替换常驻实例(#19):同端口已有实例时新进程只会让位,所以必须先停再起。
152
+ const { stopWatchdogs } = await import('../lib/watchdog.js')
153
+ const { nodeBinary, childEnv } = await import('../lib/node-bin.js')
154
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
155
+ const { spawn } = await import('node:child_process')
156
+ const { fileURLToPath } = await import('node:url')
157
+ const port = Number(argOf('--port')) || 7999
158
+ const interval = Number(argOf('--interval')) || 5000
159
+ const maxRestart = Number(argOf('--max-restart')) || 10
160
+ const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
161
+ const stopped = await stopWatchdogs({ port })
162
+ if (stopped.failed.length) { console.error(`旧实例未能停止:pid ${stopped.failed.join(', ')},放弃重启`); process.exitCode = 1; return }
163
+ if (stopped.stopped.length) console.log(`已停止旧实例:pid ${stopped.stopped.join(', ')}`)
164
+ if (stopped.webStopped && stopped.webStopped.length) console.log(`已停止旧实例拉起的 web:pid ${stopped.webStopped.join(', ')}`)
165
+ const bin = nodeBinary()
166
+ const script = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'watchdog.js')
167
+ const wd = spawn(bin, [script, '--port', String(port), '--interval', String(interval), '--max-restart', String(maxRestart), '--vault', vaultRoot], {
168
+ detached: true, stdio: 'ignore', env: childEnv({ MEMORY_VAULT_DIR: vaultRoot }), windowsHide: true,
169
+ })
170
+ wd.unref()
171
+ console.log(`已在后台启动新看门狗 pid=${wd.pid} port=${port} vault=${vaultRoot}`)
172
+ return
173
+ }
174
+ case 'audit': {
175
+ const sub = argv[1] || ''
176
+ const { listCards, setCardStatus } = await import('../lib/vault.js')
177
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
178
+ const root = path.resolve(argOf('--vault') || defaultVaultDir())
179
+ const asJson = has('--json')
180
+ if (sub === 'list') {
181
+ const STATUS = { pending: ['pending'], rejected: ['rejected'], deleted: ['deleted'], all: ['pending', 'rejected'], queue: ['pending', 'rejected'] }
182
+ const wanted = String(argOf('--status') || 'pending').toLowerCase()
183
+ const status = STATUS[wanted] || [wanted]
184
+ const kind = argOf('--kind') || undefined
185
+ const limit = Math.min(Math.max(Number(argOf('--limit')) || 50, 1), 500)
186
+ const cards = await listCards(root, { status, kind, limit, sort: 'recent' })
187
+ if (asJson) {
188
+ console.log(JSON.stringify({
189
+ ok: true, vaultDir: root, status: wanted, count: cards.length,
190
+ cards: cards.map((c) => ({ path: c.path, title: c.title, kind: c.kind, status: c.status, tags: c.tags, reason: c.reason, createdAt: c.createdAt, submittedBy: c.submittedBy })),
191
+ }, null, 2))
192
+ return
193
+ }
194
+ if (!cards.length) { console.log(`没有 ${wanted} 状态的卡片(库:${root})`); return }
195
+ console.log(`${wanted} 共 ${cards.length} 张(库:${root}):`)
196
+ for (const c of cards) console.log(`- [${c.status}] ${c.kind} | ${c.title}\n ${c.path}`)
197
+ return
198
+ }
199
+ if (sub === 'approve' || sub === 'reject') {
200
+ const paths = auditPaths()
201
+ if (!paths.length) {
202
+ console.error(`用法:dsh-memory audit ${sub} <卡片路径...> [--reason "原因"] [--vault DIR]`)
203
+ process.exitCode = 1
204
+ return
205
+ }
206
+ const status = sub === 'approve' ? 'approved' : 'rejected'
207
+ const reason = argOf('--reason') || (sub === 'approve' ? 'CLI 人工批准' : 'CLI 人工驳回')
208
+ const changedBy = argOf('--by') || 'cli'
209
+ const results = []
210
+ for (const p of paths) {
211
+ try {
212
+ await setCardStatus(root, p, status, { changedBy, reason })
213
+ results.push({ path: p, ok: true, status })
214
+ if (!asJson) console.log(`✓ ${status} ${p}`)
215
+ } catch (error) {
216
+ results.push({ path: p, ok: false, error: String(error?.message || error) })
217
+ if (!asJson) console.error(`✗ ${p}:${String(error?.message || error)}`)
218
+ }
219
+ }
220
+ if (asJson) console.log(JSON.stringify({ ok: results.every((r) => r.ok), vaultDir: root, results }, null, 2))
221
+ else console.log(`完成:${results.filter((r) => r.ok).length}/${results.length}(库:${root})`)
222
+ process.exitCode = results.some((r) => !r.ok) ? 1 : 0
223
+ return
224
+ }
225
+ console.error(`用法:
226
+ dsh-memory audit list [--status pending|rejected|deleted|all] [--kind KIND] [--limit N] [--json] [--vault DIR]
227
+ dsh-memory audit approve <卡片路径...> [--reason "原因"] [--vault DIR]
228
+ dsh-memory audit reject <卡片路径...> [--reason "原因"] [--vault DIR]
229
+
230
+ 说明:审核写入仍走 setCardStatus(DB 层守卫 + 不可变 audit_log 都不绕过)。
231
+ MCP 侧只提供只读的 memory_audit_list —— 批准/驳回必须由人显式发起。`)
232
+ process.exitCode = 1
233
+ return
234
+ }
235
+ case 'watchdog': {
236
+ const { startWatchdog, reapStaleWatchdogs } = await import('../lib/watchdog.js')
237
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
238
+ const port = Number(argOf('--port')) || 7999
239
+ if (has('--reap')) {
240
+ const out = await reapStaleWatchdogs({ port, keepPid: 0 })
241
+ console.log(`扫描到 ${out.scanned} 个 watchdog 进程,清理孤儿 ${out.killed.length} 个${out.killed.length ? '(pid ' + out.killed.join(', ') + ')' : ''},保留 ${out.skipped.length} 个(锁中登记的活跃实例 / 其它端口不会被动)`)
242
+ return
243
+ }
244
+ const interval = Number(argOf('--interval')) || 5000
245
+ const maxRestart = Number(argOf('--max-restart')) || 10
246
+ const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
247
+ const autoStart = !has('--no-restart')
248
+ console.log(`dsh-memory watchdog 启动 → 目标端口 ${port},间隔 ${interval}ms,最大重拉 ${maxRestart} 次${autoStart ? '' : '(--no-restart)'}`)
249
+ console.log(`vault: ${vaultRoot}\n按 Ctrl+C 退出`)
250
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
251
+ return
252
+ }
253
+ case 'setup': {
254
+ const { runSetup } = await import('../lib/setup.js')
255
+ const only = []
256
+ if (has('--claude-only')) only.push('claude')
257
+ if (has('--codex-only')) only.push('codex')
258
+ if (has('--cursor-only')) only.push('cursor')
259
+ const out = await runSetup({ only, withHooks: !has('--no-hooks'), dryRun: has('--dry-run') })
260
+ const done = out.results.filter((r) => r.ok).length
261
+ console.log(`\n记忆库目录:默认 ~/.dsh/memory-vault(MEMORY_VAULT_DIR 可改)`)
262
+ console.log(`完成:${done}/${out.results.length} 项。Web UI: dsh-memory open`)
263
+ return
264
+ }
265
+ case 'connect': {
266
+ // 会话结束自动沉淀 hook:不依赖 plugin(Codex Desktop 也适用),写用户级 hooks.json
267
+ const agent = positional()[0] || ''
268
+ if (!agent) {
269
+ console.error('用法:dsh-memory connect <claude|codex|cursor> [--dry-run]')
270
+ process.exitCode = 1
271
+ return
272
+ }
273
+ const { connectAgentHooks } = await import('../lib/setup.js')
274
+ const out = await connectAgentHooks(agent, { dryRun: has('--dry-run'), log: (m) => console.log(m) })
275
+ if (out && out.ok) console.log(`\n✓ ${agent} 会话结束自动沉淀 hook 已就绪(写入统一 ~/.dsh/memory-vault,新卡进待审核)`)
276
+ else { console.error(`✗ ${agent} 连接失败:` + JSON.stringify(out)); process.exitCode = 1 }
277
+ return
278
+ }
279
+ case 'sweep': {
280
+ const dir = argv[1]
281
+ if (!dir || dir.startsWith('--')) {
282
+ console.error('用法:dsh-memory sweep <dir>(如 ~/.claude/projects)')
283
+ process.exitCode = 1
284
+ return
285
+ }
286
+ const { sweepSessions } = await import('../lib/sweep.js')
287
+ const out = await sweepSessions(dir)
288
+ console.log(JSON.stringify(out, null, 2))
289
+ return
290
+ }
291
+ default:
292
+ console.log(`dsh-memory — 记忆核心 CLI
293
+
294
+ 用法:
295
+ dsh-memory recall <query> [--limit N] 检索知识卡
296
+ dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
297
+ dsh-memory serve [--port N] Web UI(前台)
298
+ dsh-memory open 确保 Web 存活并打开浏览器
299
+ dsh-memory mcp MCP stdio server
300
+ dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
301
+ dsh-memory connect <claude|codex|cursor> 写会话结束自动沉淀 hook(不依赖 plugin,含 Codex Desktop)
302
+ dsh-memory sweep <dir> 挖掘会话 JSONL
303
+ dsh-memory watchdog [--port N] [--interval MS] [--max-restart N] 看门狗保活 web server
304
+ dsh-memory watchdog --reap [--port N] 清理不在锁里的孤儿看门狗
305
+ dsh-memory status [--port N] [--json] 查看常驻看门狗(pid/端口/版本/存活)
306
+ dsh-memory stop [--port N] [--json] 停止常驻看门狗(显式命令,配置变更不会自动停)
307
+ dsh-memory restart [--port N] [--vault DIR] 停止并重新拉起看门狗(应用新版本)
308
+ dsh-memory audit list [--status pending] [--kind K] [--json] 列出待审/驳回卡片
309
+ dsh-memory audit approve <path...> | reject <path...> [--reason "原因"] 人工批量审批
310
+
311
+ 环境变量:MEMORY_VAULT_DIR / MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL`)
312
+ }
313
+ }
314
+
315
+ main().catch((error) => {
316
+ console.error(String(error?.stack || error))
317
+ process.exit(1)
318
+ })
@@ -0,0 +1,59 @@
1
+ # 记忆核心(memory-eternal)· MCP 对外通道示例 overlay
2
+ #
3
+ # 用途:把 memory-eternal 的 MCP stdio 服务挂进 DSH —— 让 **DSH 自己**也能通过官方
4
+ # `@deepseek-ai/dsh-mcp-client` 使用记忆库(与 Claude Code / Codex / Cursor 走同一条协议路径)。
5
+ #
6
+ # 什么时候需要它:
7
+ # · 你没装 memory-eternal 插件本体,只想用它的记忆库 → 用这个 overlay;
8
+ # · 你已经装了插件(推荐):插件是「一等公民」,这条 MCP 通道**不需要**,因为插件
9
+ # 自带 memory_recall 工具与系统提示段,还能拿到会话事件做自动沉淀。
10
+ #
11
+ # 用法(示例文件不在交付包里自动生效,必须显式 --patch 或合并进用户 patch 层):
12
+ # dsh web --patch "<仓库>/examples/mcp-memory/memory-eternal.cordis.yml"
13
+ # 或把下面的 insert 合并进 $DSH_HOME/cordis.patch.yml(本机所有 profile 生效)
14
+ # 或 $DSH_HOME/profiles/<name>/cordis.patch.yml(只对该 profile 生效)
15
+ #
16
+ # 前置条件(二进制已随包提供,无需额外安装):
17
+ # · Node 22+(memory-eternal engines 要求)
18
+ # · 全局安装或 profile 内安装 memory-eternal(提供 bin/dsh-memory.mjs)
19
+ # 全局:npm i -g memory-eternal
20
+ # profile 内:cd ~/.dsh/profiles/<name> && pnpm add memory-eternal
21
+ # 下面的 args 用的是「profile 内安装」路径;全局安装请改成
22
+ # args: ['dsh-memory', 'mcp'](Windows 下为 dsh-memory.cmd)。
23
+
24
+ - insert:
25
+ - id: memory-eternal-mcp
26
+ name: '@deepseek-ai/dsh-mcp-client'
27
+ config:
28
+ serverName: eternal
29
+ transport: stdio
30
+ command: node
31
+ args:
32
+ # 如果是全局安装,改成 ['dsh-memory', 'mcp']
33
+ - 'C:/Users/<你>/.dsh/profiles/web/node_modules/memory-eternal/bin/dsh-memory.mjs'
34
+ - 'mcp'
35
+ # 注意:stdio 桥接会主动**剥离名字像凭据的环境变量以及所有 DSH_* 变量**,其余继承。
36
+ # 因此:
37
+ # · 想让 MCP 进程用哪个库,显式在这里写 MEMORY_VAULT_DIR(不要依赖继承);
38
+ # · 想让 MCP 的 memory_capture 具备「LLM 蒸馏」能力,需要在这里显式传 MEMORY_LLM_*
39
+ # (这类变量名字含 KEY/TOKEN,很可能被桥接当凭据剥掉);
40
+ # · 不传就是「原文卡」降级模式,仍然可用,只是不压缩。
41
+ env:
42
+ # 留空 = 用插件自己的解析顺序(共享配置 memory-eternal-config.json → 默认 ~/.dsh/memory-vault)
43
+ MEMORY_VAULT_DIR: ''
44
+ # 可选:MCP 侧蒸馏用的 OpenAI 兼容端点
45
+ # MEMORY_LLM_BASE_URL: 'https://api.deepseek.com'
46
+ # MEMORY_LLM_MODEL: 'deepseek-chat'
47
+ # MEMORY_LLM_API_KEY: '<在此填 key>'
48
+ cwd: !!js process.cwd()
49
+
50
+ # 挂上之后 DSH 侧会看到这些工具(前缀由 serverName 决定):
51
+ # mcp__eternal__memory_recall —— 检索记忆(CJK 整词 + 字符 bigram,只读主库/approved)
52
+ # mcp__eternal__memory_capture —— 手动沉淀一段内容(走审核闸门,默认落隔离区 pending)
53
+ # mcp__eternal__memory_stats —— 记忆库概览
54
+ #
55
+ # 验证(与官方文档同一套三步骤):
56
+ # 1) 会话 A:让模型调用 mcp__eternal__memory_capture 写入一个唯一值;
57
+ # 2) 新建会话 B(不必重启宿主):问「我的验证饮料是什么?查记忆」→ 应调用 memory_recall 并返回该值;
58
+ # 3) 继续在 B 里用该偏好做一次推断。
59
+ # 本仓库 tests/e2e-recall.test.mjs 是同一套步骤的自动化版本(走 vault API)。