memory-eternal 0.10.0 → 0.10.3

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,343 @@
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, countCards } = 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
+ // 状态别名:all = 「审核队列」的全部(pending + rejected)。
182
+ // approved 是已出队状态、deleted 归回收中心,都要显式指定(issue #20 的问号)。
183
+ const STATUS = { pending: ['pending'], rejected: ['rejected'], deleted: ['deleted'], approved: ['approved'], all: ['pending', 'rejected'], queue: ['pending', 'rejected'] }
184
+ const wanted = String(argOf('--status') || 'pending').toLowerCase()
185
+ const status = STATUS[wanted] || [wanted]
186
+ const kind = argOf('--kind') || undefined
187
+ // issue #20:总数必须与 --limit 解耦。
188
+ // 旧实现把 listCards(limit) 的**截断后条数**当成「共 N 张」,且默认 limit=50 静默截断 ——
189
+ // 队列规模被系统性低估(报告者因此把 32 张待审看成 5 张)。
190
+ // 现在:不给 --limit(或 --limit 0)= 全部;给了只限制**显示条数**,总数永远是匹配总数。
191
+ const rawLimit = argOf('--limit')
192
+ const wantedLimit = Number(rawLimit)
193
+ const limit = rawLimit === null || !Number.isFinite(wantedLimit) || wantedLimit <= 0
194
+ ? null
195
+ : Math.min(Math.floor(wantedLimit), 5000)
196
+ const total = await countCards(root, { status, kind })
197
+ const cards = await listCards(root, { status, kind, limit: limit ?? undefined, sort: 'recent' })
198
+ const returned = cards.length
199
+ const hidden = Math.max(0, total - returned)
200
+ if (asJson) {
201
+ console.log(JSON.stringify({
202
+ ok: true, vaultDir: root, status: wanted,
203
+ total, // 匹配总数(与 --limit 无关)
204
+ returned, // 本次实际返回条数
205
+ count: total, // 兼容别名:0.10.2 起曾误等于 returned,属 bug(issue #20),现与 total 一致
206
+ limit: limit ?? null, // 生效的显示上限;null = 全部
207
+ hasMore: hidden > 0,
208
+ 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 })),
209
+ }, null, 2))
210
+ return
211
+ }
212
+ if (!total) { console.log(`没有 ${wanted} 状态的卡片(库:${root})`); return }
213
+ console.log(hidden > 0
214
+ ? `${wanted} 共 ${total} 张,显示前 ${returned} 张(库:${root}):`
215
+ : `${wanted} 共 ${total} 张(库:${root}):`)
216
+ for (const c of cards) console.log(`- [${c.status}] ${c.kind} | ${c.title}\n ${c.path}`)
217
+ if (hidden > 0) console.log(`… 还有 ${hidden} 张未显示(用 --limit N 调整,--limit 0 = 全部)`)
218
+ return
219
+ }
220
+ if (sub === 'approve' || sub === 'reject') {
221
+ const paths = auditPaths()
222
+ if (!paths.length) {
223
+ console.error(`用法:dsh-memory audit ${sub} <卡片路径...> [--reason "原因"] [--vault DIR]`)
224
+ process.exitCode = 1
225
+ return
226
+ }
227
+ const status = sub === 'approve' ? 'approved' : 'rejected'
228
+ const reason = argOf('--reason') || (sub === 'approve' ? 'CLI 人工批准' : 'CLI 人工驳回')
229
+ const changedBy = argOf('--by') || 'cli'
230
+ const results = []
231
+ for (const p of paths) {
232
+ try {
233
+ await setCardStatus(root, p, status, { changedBy, reason })
234
+ results.push({ path: p, ok: true, status })
235
+ if (!asJson) console.log(`✓ ${status} ${p}`)
236
+ } catch (error) {
237
+ results.push({ path: p, ok: false, error: String(error?.message || error) })
238
+ if (!asJson) console.error(`✗ ${p}:${String(error?.message || error)}`)
239
+ }
240
+ }
241
+ if (asJson) console.log(JSON.stringify({ ok: results.every((r) => r.ok), vaultDir: root, results }, null, 2))
242
+ else console.log(`完成:${results.filter((r) => r.ok).length}/${results.length}(库:${root})`)
243
+ process.exitCode = results.some((r) => !r.ok) ? 1 : 0
244
+ return
245
+ }
246
+ console.error(`用法:
247
+ dsh-memory audit list [--status pending|rejected|approved|deleted|all] [--kind KIND] [--limit N] [--json] [--vault DIR]
248
+ dsh-memory audit approve <卡片路径...> [--reason "原因"] [--vault DIR]
249
+ dsh-memory audit reject <卡片路径...> [--reason "原因"] [--vault DIR]
250
+
251
+ 说明:审核写入仍走 setCardStatus(DB 层守卫 + 不可变 audit_log 都不绕过)。
252
+ --status all = 审核队列(pending + rejected),不含已出队的 approved / 回收中心的 deleted。
253
+ --limit 只限制「显示条数」(默认全部显示,--limit 0 同义);总数始终是匹配总数,不会被 --limit 改写。
254
+ --json 字段:total(匹配总数)/ returned(本次返回条数)/ count(= total 的兼容别名)
255
+ / limit(生效上限,null = 全部)/ hasMore / cards
256
+ MCP 侧只提供只读的 memory_audit_list —— 批准/驳回必须由人显式发起。`)
257
+ process.exitCode = 1
258
+ return
259
+ }
260
+ case 'watchdog': {
261
+ const { startWatchdog, reapStaleWatchdogs } = await import('../lib/watchdog.js')
262
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
263
+ const port = Number(argOf('--port')) || 7999
264
+ if (has('--reap')) {
265
+ const out = await reapStaleWatchdogs({ port, keepPid: 0 })
266
+ console.log(`扫描到 ${out.scanned} 个 watchdog 进程,清理孤儿 ${out.killed.length} 个${out.killed.length ? '(pid ' + out.killed.join(', ') + ')' : ''},保留 ${out.skipped.length} 个(锁中登记的活跃实例 / 其它端口不会被动)`)
267
+ return
268
+ }
269
+ const interval = Number(argOf('--interval')) || 5000
270
+ const maxRestart = Number(argOf('--max-restart')) || 10
271
+ const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
272
+ const autoStart = !has('--no-restart')
273
+ console.log(`dsh-memory watchdog 启动 → 目标端口 ${port},间隔 ${interval}ms,最大重拉 ${maxRestart} 次${autoStart ? '' : '(--no-restart)'}`)
274
+ console.log(`vault: ${vaultRoot}\n按 Ctrl+C 退出`)
275
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
276
+ return
277
+ }
278
+ case 'setup': {
279
+ const { runSetup } = await import('../lib/setup.js')
280
+ const only = []
281
+ if (has('--claude-only')) only.push('claude')
282
+ if (has('--codex-only')) only.push('codex')
283
+ if (has('--cursor-only')) only.push('cursor')
284
+ const out = await runSetup({ only, withHooks: !has('--no-hooks'), dryRun: has('--dry-run') })
285
+ const done = out.results.filter((r) => r.ok).length
286
+ console.log(`\n记忆库目录:默认 ~/.dsh/memory-vault(MEMORY_VAULT_DIR 可改)`)
287
+ console.log(`完成:${done}/${out.results.length} 项。Web UI: dsh-memory open`)
288
+ return
289
+ }
290
+ case 'connect': {
291
+ // 会话结束自动沉淀 hook:不依赖 plugin(Codex Desktop 也适用),写用户级 hooks.json
292
+ const agent = positional()[0] || ''
293
+ if (!agent) {
294
+ console.error('用法:dsh-memory connect <claude|codex|cursor> [--dry-run]')
295
+ process.exitCode = 1
296
+ return
297
+ }
298
+ const { connectAgentHooks } = await import('../lib/setup.js')
299
+ const out = await connectAgentHooks(agent, { dryRun: has('--dry-run'), log: (m) => console.log(m) })
300
+ if (out && out.ok) console.log(`\n✓ ${agent} 会话结束自动沉淀 hook 已就绪(写入统一 ~/.dsh/memory-vault,新卡进待审核)`)
301
+ else { console.error(`✗ ${agent} 连接失败:` + JSON.stringify(out)); process.exitCode = 1 }
302
+ return
303
+ }
304
+ case 'sweep': {
305
+ const dir = argv[1]
306
+ if (!dir || dir.startsWith('--')) {
307
+ console.error('用法:dsh-memory sweep <dir>(如 ~/.claude/projects)')
308
+ process.exitCode = 1
309
+ return
310
+ }
311
+ const { sweepSessions } = await import('../lib/sweep.js')
312
+ const out = await sweepSessions(dir)
313
+ console.log(JSON.stringify(out, null, 2))
314
+ return
315
+ }
316
+ default:
317
+ console.log(`dsh-memory — 记忆核心 CLI
318
+
319
+ 用法:
320
+ dsh-memory recall <query> [--limit N] 检索知识卡
321
+ dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
322
+ dsh-memory serve [--port N] Web UI(前台)
323
+ dsh-memory open 确保 Web 存活并打开浏览器
324
+ dsh-memory mcp MCP stdio server
325
+ dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
326
+ dsh-memory connect <claude|codex|cursor> 写会话结束自动沉淀 hook(不依赖 plugin,含 Codex Desktop)
327
+ dsh-memory sweep <dir> 挖掘会话 JSONL
328
+ dsh-memory watchdog [--port N] [--interval MS] [--max-restart N] 看门狗保活 web server
329
+ dsh-memory watchdog --reap [--port N] 清理不在锁里的孤儿看门狗
330
+ dsh-memory status [--port N] [--json] 查看常驻看门狗(pid/端口/版本/存活)
331
+ dsh-memory stop [--port N] [--json] 停止常驻看门狗(显式命令,配置变更不会自动停)
332
+ dsh-memory restart [--port N] [--vault DIR] 停止并重新拉起看门狗(应用新版本)
333
+ dsh-memory audit list [--status pending] [--kind K] [--json] 列出待审/驳回卡片
334
+ dsh-memory audit approve <path...> | reject <path...> [--reason "原因"] 人工批量审批
335
+
336
+ 环境变量:MEMORY_VAULT_DIR / MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL`)
337
+ }
338
+ }
339
+
340
+ main().catch((error) => {
341
+ console.error(String(error?.stack || error))
342
+ process.exit(1)
343
+ })