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.
- package/.claude-plugin/plugin.json +34 -0
- package/.codex-plugin/plugin.json +29 -0
- package/.cursor-plugin/plugin.json +28 -0
- package/.mcp.json +9 -0
- package/LICENSE +21 -0
- package/README.en.md +224 -0
- package/README.md +236 -0
- package/bin/dsh-memory.mjs +183 -0
- package/cordis.patch.yml +15 -0
- package/hooks/capture.mjs +77 -0
- package/hooks/claude-session.js +56 -0
- package/hooks/hooks.claude.json +12 -0
- package/hooks/hooks.codex.json +15 -0
- package/hooks/hooks.cursor.json +11 -0
- package/index.js +565 -0
- package/lib/api.js +321 -0
- package/lib/capture-run.js +98 -0
- package/lib/capture.js +267 -0
- package/lib/client.js +186 -0
- package/lib/llm-openai.js +84 -0
- package/lib/mcp.js +166 -0
- package/lib/setup.js +470 -0
- package/lib/sweep.js +56 -0
- package/lib/vault.js +802 -0
- package/lib/watchdog.js +201 -0
- package/lib/web.js +149 -0
- package/package.json +101 -0
- package/skills/memory-recall/SKILL.md +14 -0
- package/web/app.js +230 -0
- package/web/index.html +46 -0
|
@@ -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
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -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 静默失败 */ })
|