memory-eternal 0.9.23 → 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 +27 -7
- package/README.md +40 -7
- package/bin/dsh-memory.mjs +318 -188
- package/examples/mcp-memory/memory-eternal.cordis.yml +59 -0
- 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 +187 -32
- package/lib/api.js +592 -536
- package/lib/capture-log.js +65 -59
- package/lib/capture-run.js +134 -126
- package/lib/capture.js +278 -14
- package/lib/client.js +20 -17
- package/lib/config-sync.js +161 -89
- package/lib/db.js +359 -212
- 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 +347 -106
- package/lib/watchdog.js +527 -392
- package/lib/web-assets.js +41 -0
- package/lib/web.js +316 -176
- package/package.json +12 -7
- package/scripts/release.mjs +204 -0
- package/skills/memory-recall/SKILL.md +14 -14
- package/web/app.js +20 -17
- package/web/index.html +46 -46
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
|
+
}
|