memory-eternal 0.10.0 → 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.
- 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 +16 -5
- package/README.md +20 -5
- package/bin/dsh-memory.mjs +318 -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 +197 -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
|
@@ -1,59 +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)。
|
|
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)。
|
package/hooks/capture.mjs
CHANGED
|
@@ -1,77 +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 静默失败 */ })
|
|
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 静默失败 */ })
|
package/hooks/claude-session.js
CHANGED
|
@@ -1,56 +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 静默失败 */ })
|
|
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 静默失败 */ })
|
package/hooks/hooks.claude.json
CHANGED
|
@@ -1,12 +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
|
-
}
|
|
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
|
+
}
|
package/hooks/hooks.codex.json
CHANGED
|
@@ -1,15 +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
|
-
}
|
|
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
|
+
}
|
package/hooks/hooks.cursor.json
CHANGED
|
@@ -1,11 +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
|
-
}
|
|
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
|
+
}
|