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.
- package/.claude-plugin/plugin.json +2 -16
- package/.codex-plugin/plugin.json +2 -16
- package/.cursor-plugin/plugin.json +2 -16
- package/.mcp.json +9 -9
- package/LICENSE +21 -21
- package/README.en.md +19 -5
- package/README.md +23 -5
- package/bin/dsh-memory.mjs +343 -188
- package/examples/mcp-memory/memory-eternal.cordis.yml +59 -59
- package/hooks/capture.mjs +77 -77
- package/hooks/claude-session.js +56 -56
- package/hooks/hooks.claude.json +12 -12
- package/hooks/hooks.codex.json +15 -15
- package/hooks/hooks.cursor.json +11 -11
- package/index.js +177 -30
- package/lib/api.js +48 -14
- package/lib/capture-log.js +65 -59
- package/lib/capture.js +112 -7
- package/lib/client.js +13 -13
- package/lib/config-sync.js +161 -90
- package/lib/db.js +359 -359
- package/lib/feedback.js +214 -205
- package/lib/llm-openai.js +84 -84
- package/lib/mcp.js +207 -166
- package/lib/node-bin.js +99 -99
- package/lib/setup.js +470 -470
- package/lib/sse.js +51 -51
- package/lib/stall.js +30 -30
- package/lib/sweep.js +56 -56
- package/lib/vault-resolve.js +84 -84
- package/lib/vault.js +23 -6
- package/lib/watchdog.js +527 -392
- package/lib/web-assets.js +41 -0
- package/lib/web.js +316 -176
- package/package.json +12 -8
- package/scripts/release.mjs +204 -0
- package/skills/memory-recall/SKILL.md +14 -14
- package/web/app.js +12 -12
- package/web/index.html +46 -46
package/bin/dsh-memory.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const out = []
|
|
34
|
-
for (let i =
|
|
35
|
-
const a = argv[i]
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const {
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
case '
|
|
107
|
-
const {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const {
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
})
|