memory-eternal 0.1.0

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.
@@ -0,0 +1,183 @@
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 } = await import('../lib/watchdog.js')
113
+ const { defaultVaultDir } = await import('../lib/capture-run.js')
114
+ const port = Number(argOf('--port')) || 7999
115
+ const interval = Number(argOf('--interval')) || 5000
116
+ const maxRestart = Number(argOf('--max-restart')) || 10
117
+ const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
118
+ const autoStart = !has('--no-restart')
119
+ console.log(`dsh-memory watchdog 启动 → 目标端口 ${port},间隔 ${interval}ms,最大重拉 ${maxRestart} 次${autoStart ? '' : '(--no-restart)'}`)
120
+ console.log(`vault: ${vaultRoot}\n按 Ctrl+C 退出`)
121
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
122
+ return
123
+ }
124
+ case 'setup': {
125
+ const { runSetup } = await import('../lib/setup.js')
126
+ const only = []
127
+ if (has('--claude-only')) only.push('claude')
128
+ if (has('--codex-only')) only.push('codex')
129
+ if (has('--cursor-only')) only.push('cursor')
130
+ const out = await runSetup({ only, withHooks: !has('--no-hooks'), dryRun: has('--dry-run') })
131
+ const done = out.results.filter((r) => r.ok).length
132
+ console.log(`\n记忆库目录:默认 ~/.dsh/memory-vault(MEMORY_VAULT_DIR 可改)`)
133
+ console.log(`完成:${done}/${out.results.length} 项。Web UI: dsh-memory open`)
134
+ return
135
+ }
136
+ case 'connect': {
137
+ // 会话结束自动沉淀 hook:不依赖 plugin(Codex Desktop 也适用),写用户级 hooks.json
138
+ const agent = positional()[0] || ''
139
+ if (!agent) {
140
+ console.error('用法:dsh-memory connect <claude|codex|cursor> [--dry-run]')
141
+ process.exitCode = 1
142
+ return
143
+ }
144
+ const { connectAgentHooks } = await import('../lib/setup.js')
145
+ const out = await connectAgentHooks(agent, { dryRun: has('--dry-run'), log: (m) => console.log(m) })
146
+ if (out && out.ok) console.log(`\n✓ ${agent} 会话结束自动沉淀 hook 已就绪(写入统一 ~/.dsh/memory-vault,新卡进待审核)`)
147
+ else { console.error(`✗ ${agent} 连接失败:` + JSON.stringify(out)); process.exitCode = 1 }
148
+ return
149
+ }
150
+ case 'sweep': {
151
+ const dir = argv[1]
152
+ if (!dir || dir.startsWith('--')) {
153
+ console.error('用法:dsh-memory sweep <dir>(如 ~/.claude/projects)')
154
+ process.exitCode = 1
155
+ return
156
+ }
157
+ const { sweepSessions } = await import('../lib/sweep.js')
158
+ const out = await sweepSessions(dir)
159
+ console.log(JSON.stringify(out, null, 2))
160
+ return
161
+ }
162
+ default:
163
+ console.log(`dsh-memory — 记忆核心 CLI
164
+
165
+ 用法:
166
+ dsh-memory recall <query> [--limit N] 检索知识卡
167
+ dsh-memory capture <text | -> [--source T] 手动沉淀(- 读 stdin)
168
+ dsh-memory serve [--port N] Web UI(前台)
169
+ dsh-memory open 确保 Web 存活并打开浏览器
170
+ dsh-memory mcp MCP stdio server
171
+ dsh-memory setup [--dry-run] ... 自动挂载 MCP 到已装 agent
172
+ dsh-memory connect <claude|codex|cursor> 写会话结束自动沉淀 hook(不依赖 plugin,含 Codex Desktop)
173
+ dsh-memory sweep <dir> 挖掘会话 JSONL
174
+ dsh-memory watchdog [--port N] [--interval MS] [--max-restart N] 看门狗保活 web server
175
+
176
+ 环境变量:MEMORY_VAULT_DIR / MEMORY_LLM_BASE_URL / MEMORY_LLM_KEY / MEMORY_LLM_MODEL`)
177
+ }
178
+ }
179
+
180
+ main().catch((error) => {
181
+ console.error(String(error?.stack || error))
182
+ process.exit(1)
183
+ })
@@ -0,0 +1,15 @@
1
+ # 记忆核心(memory-eternal)bundle 补丁层。
2
+ # 安装后由 package.json 的 dsh.bundle.patch 自动生效;后层
3
+ # (profile 自己的 cordis.patch.yml / --patch)可按 id 覆盖或禁用本行。
4
+ #
5
+ # 客户端 UI(设置 → 记忆:统计 / 搜索 / 知识图谱)由 package.json 的
6
+ # dsh.client 元数据自动挂载,无需在此声明任何 client 行。
7
+
8
+ - insert:
9
+ - id: memory-eternal
10
+ name: memory-eternal
11
+ config:
12
+ enabled: true
13
+ autoCapture: true
14
+ autoRecall: true
15
+ vaultDir: ''
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // 记忆核心 · 通用「会话结束自动沉淀」hook(claude-code / codex / cursor 共用)。
3
+ //
4
+ // 用法:node capture.mjs <agent> agent ∈ claude-code | codex | cursor
5
+ // stdin(平台 hook 注入):
6
+ // { session_id, transcript_path, hook_event_name, cwd, last_assistant_message, prompt, ... }
7
+ //
8
+ // 行为:读 transcript(JSONL/JSON 兼容)或显式文本字段 → 调 runStandaloneCapture 蒸馏入库。
9
+ // 设计约束:
10
+ // - 静默失败(hook 崩溃不能影响宿主 agent)
11
+ // - 幂等(去重/追加在 runStandaloneCapture 内处理)
12
+ // - 署名 source = agent 名,便于智能体筛选;vault 由 MEMORY_VAULT_DIR 或 ~/.dsh/memory-vault 决定(与全库统一)
13
+ //
14
+ // Windows 友好:纯 node,无外部依赖(同 claude-session.js)。
15
+
16
+ import { promises as fs } from 'node:fs'
17
+
18
+ const agent = process.argv[2] || 'agent'
19
+
20
+ const readStdin = () => new Promise((resolve) => {
21
+ let buf = ''
22
+ process.stdin.setEncoding('utf8')
23
+ process.stdin.on('data', (c) => { buf += c })
24
+ process.stdin.on('end', () => resolve(buf))
25
+ setTimeout(() => resolve(buf), 2000)
26
+ })
27
+
28
+ /** 从 message 对象里抽可读文本(兼容 claude 的 {role,content} 与 codex/cursor 的 {text|content})。 */
29
+ function textOf(msg) {
30
+ if (!msg) return ''
31
+ if (typeof msg === 'string') return msg
32
+ const c = msg.content
33
+ if (typeof c === 'string') return c
34
+ if (Array.isArray(c)) return c.filter((x) => x && x.type === 'text').map((x) => x.text).join('\n')
35
+ return msg.text || msg.message || ''
36
+ }
37
+
38
+ /** 读 transcript 文件并抽取最近对话文本。优先 JSONL 行解析,退回整体文本。 */
39
+ async function extractTranscript(transcriptPath) {
40
+ let text
41
+ try { text = await fs.readFile(transcriptPath, 'utf8') } catch { return '' }
42
+ if (!text.trim()) return ''
43
+ text = text.replace(/^\uFEFF/, '') // 去掉可能存在的 UTF-8 BOM
44
+ const lines = text.split('\n').filter(Boolean)
45
+ const parts = []
46
+ let structured = 0
47
+ for (const line of lines.slice(-400)) {
48
+ let rec
49
+ try { rec = JSON.parse(line); structured++ } catch { continue }
50
+ const t = textOf(rec?.message)
51
+ if (t && t.trim().length > 4) {
52
+ const role = rec?.message?.role || rec?.role || 'msg'
53
+ parts.push(`${role}: ${t.trim()}`)
54
+ }
55
+ }
56
+ if (structured >= 1 && parts.length) return parts.slice(-30).join('\n\n')
57
+ // 非结构化:取尾部可读文本(去掉控制字符)
58
+ return text.replace(/\r/g, '').replace(/\u0000/g, '').slice(-6000)
59
+ }
60
+
61
+ async function main() {
62
+ let meta = {}
63
+ try { meta = JSON.parse((await readStdin()) || '{}') } catch { /* 静默 */ }
64
+
65
+ let conversation = ''
66
+ if (meta.transcript_path) { try { conversation = await extractTranscript(meta.transcript_path) } catch { /* 静默 */ } }
67
+ if (!conversation || conversation.trim().length < 120) {
68
+ conversation = meta.last_assistant_message || meta.prompt || meta.context || ''
69
+ }
70
+ if (!conversation || conversation.trim().length < 120) return
71
+
72
+ const { runStandaloneCapture, defaultVaultDir } = await import('../lib/capture-run.js')
73
+ const src = agent // 署名 = agent 名(可筛选);若想保留会话粒度可用 `${agent}:${session_id}`
74
+ await runStandaloneCapture(defaultVaultDir(), conversation, { source: src })
75
+ }
76
+
77
+ main().catch(() => { /* hook 静默失败 */ })
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // 记忆核心 · Claude Code SessionEnd/PreCompact hook(Windows 友好,纯 node)。
3
+ //
4
+ // Claude Code 在会话结束/压缩前触发,stdin 传入 JSON:
5
+ // { session_id, transcript_path, hook_event_name, cwd, ... }
6
+ // 本脚本解析 transcript JSONL,提取最近若干轮 user/assistant 文本,蒸馏入库。
7
+ // 由 lib/setup.js 自动注册到 ~/.claude/settings.json 的 hooks.SessionEnd。
8
+ //
9
+ // 特性:静默失败(hook 崩溃不能影响 Claude Code);幂等(重复跑同一段只追加)。
10
+
11
+ import { promises as fs } from 'node:fs'
12
+ import path from 'node:path'
13
+
14
+ async function main() {
15
+ let input = {}
16
+ try {
17
+ const raw = await new Promise((resolve) => {
18
+ let buf = ''
19
+ process.stdin.setEncoding('utf8')
20
+ process.stdin.on('data', (c) => { buf += c })
21
+ process.stdin.on('end', () => resolve(buf))
22
+ setTimeout(() => resolve(buf), 2000)
23
+ })
24
+ input = JSON.parse(raw || '{}')
25
+ } catch { /* 静默 */ }
26
+
27
+ const transcriptPath = input.transcript_path
28
+ if (!transcriptPath) return
29
+ let lines
30
+ try { lines = (await fs.readFile(transcriptPath, 'utf8')).split('\n').filter(Boolean) } catch { return }
31
+
32
+ // 提取尾部对话文本(user 与 assistant 的 message.content 文本部分)
33
+ const texts = []
34
+ for (const line of lines.slice(-400)) {
35
+ let rec
36
+ try { rec = JSON.parse(line) } catch { continue }
37
+ const msg = rec?.message
38
+ if (!msg || !msg.role) continue
39
+ const content = msg.content
40
+ let text = ''
41
+ if (typeof content === 'string') text = content
42
+ else if (Array.isArray(content)) {
43
+ text = content.filter((c) => c?.type === 'text').map((c) => c.text).join('\n')
44
+ }
45
+ if (text && text.trim().length > 4) texts.push(`${msg.role}: ${text.trim()}`)
46
+ }
47
+ const conversation = texts.slice(-30).join('\n\n')
48
+ if (conversation.trim().length < 120) return
49
+
50
+ const { runStandaloneCapture, defaultVaultDir } = await import('../lib/capture-run.js')
51
+ await runStandaloneCapture(defaultVaultDir(), conversation, {
52
+ source: `claude-code:${input.session_id || path.basename(transcriptPath)}`,
53
+ })
54
+ }
55
+
56
+ main().catch(() => { /* hook 静默失败 */ })
@@ -0,0 +1,12 @@
1
+ {
2
+ "SessionEnd": [
3
+ {
4
+ "hooks": [
5
+ {
6
+ "type": "command",
7
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/capture.mjs\" claude-code"
8
+ }
9
+ ]
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "Stop": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/capture.mjs\" codex",
9
+ "statusMessage": "memory-eternal: saving session"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "stop": [
5
+ { "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/capture.mjs\" cursor" }
6
+ ],
7
+ "sessionEnd": [
8
+ { "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/capture.mjs\" cursor" }
9
+ ]
10
+ }
11
+ }