memory-eternal 0.10.6 → 0.10.8
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 +1 -1
- package/.codex-plugin/plugin.json +10 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.mcp.json +9 -9
- package/LICENSE +21 -21
- package/README.en.md +37 -1
- package/README.md +30 -1
- package/bin/dsh-memory.mjs +31 -45
- 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 +23 -12
- package/hooks/hooks.codex.json +15 -15
- package/hooks/hooks.cursor.json +11 -11
- package/hooks/secret-guard.mjs +82 -0
- package/index.js +137 -21
- package/lib/api.js +148 -17
- package/lib/client.js +17 -17
- package/lib/config-sync.js +99 -0
- package/lib/db.js +359 -359
- package/lib/host-heartbeat.js +76 -72
- package/lib/llm-openai.js +84 -84
- package/lib/mcp.js +3 -1
- package/lib/node-bin.js +99 -99
- package/lib/setup.js +515 -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/watchdog.js +241 -11
- package/lib/web-assets.js +41 -41
- package/lib/web.js +2 -0
- package/package.json +2 -2
- package/skills/memory-recall/SKILL.md +19 -14
- package/web/app.js +15 -15
- 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,23 @@
|
|
|
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
|
+
"PreToolUse": [
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Read|Bash|Grep|Glob",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/secret-guard.mjs\""
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 记忆核心 · 凭据读取守卫(Claude Code PreToolUse hook,纯 node、静默失败)
|
|
3
|
+
//
|
|
4
|
+
// 为什么需要它:一旦 agent 知道「本机有密钥」,它很可能直接 `cat ~/.dsh/.credentials.yaml`
|
|
5
|
+
// 或 Read 那个文件 —— 值就进了它的上下文(再经 provider / 日志 / 蒸馏扩散出去)。
|
|
6
|
+
// 「知道去哪取」与「不许直接读」必须成对存在,否则索引反而放大了泄漏面。
|
|
7
|
+
//
|
|
8
|
+
// 生效条件(**默认不打扰任何人**):本机存在密钥加载器
|
|
9
|
+
// ~/.config/memory-eternal/secrets.py
|
|
10
|
+
// 没有它(其它用户 / 其它机器)这个 hook 直接放行,什么也不做。
|
|
11
|
+
//
|
|
12
|
+
// 命中判定:工具参数里出现下列文件(Read 的 file_path / Bash 的命令行 / Grep 的 pattern 等)
|
|
13
|
+
// .credentials.y(a)ml、~/.dsh/.env、gitee-token
|
|
14
|
+
// 命中即以 PreToolUse permissionDecision=deny 拒绝,并指路到 secrets.py --names / --run。
|
|
15
|
+
//
|
|
16
|
+
// 由 hooks/hooks.claude.json 注册(PreToolUse)。也可以手工测试:
|
|
17
|
+
// echo '{"tool_name":"Read","tool_input":{"file_path":"/x/.credentials.yaml"}}' | node hooks/secret-guard.mjs
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import os from 'node:os'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
|
|
23
|
+
const LOADER = path.join(os.homedir(), '.config', 'memory-eternal', 'secrets.py')
|
|
24
|
+
const SENSITIVE = [
|
|
25
|
+
/\.credentials\.ya?ml/i,
|
|
26
|
+
/[\\/]\.dsh[\\/]\.env(?:$|[\s"'])/i,
|
|
27
|
+
/[\\/]\.env(?:$|["'\s])/i, // 通用 .env(含值)——只在本机装了加载器时才拦
|
|
28
|
+
/gitee-token/i,
|
|
29
|
+
]
|
|
30
|
+
const REASON = [
|
|
31
|
+
'这道读取被「记忆核心」的凭据守卫拦下了:该文件里是密钥**明文**,读进上下文就等于泄漏',
|
|
32
|
+
'(值经 provider / 日志 / 自动蒸馏会扩散到多处,且删除也无法收回)。',
|
|
33
|
+
'',
|
|
34
|
+
'正确做法:',
|
|
35
|
+
' 1) 先列名字(不输出值):python ~/.config/memory-eternal/secrets.py --names',
|
|
36
|
+
' 2) 再带值执行命令(值只进子进程环境变量):python ~/.config/memory-eternal/secrets.py --require <KEY> --run -- <命令>',
|
|
37
|
+
' 3) 需要「有哪些键、放在哪、干什么用」的说明:memory_recall("密钥 目录")',
|
|
38
|
+
'',
|
|
39
|
+
'确实需要人工查看时,请在终端里自己打开该文件 —— 不要经由 Agent 的工具读取。',
|
|
40
|
+
].join('\n')
|
|
41
|
+
|
|
42
|
+
async function readStdin() {
|
|
43
|
+
try {
|
|
44
|
+
return await new Promise((resolve) => {
|
|
45
|
+
let buf = ''
|
|
46
|
+
process.stdin.setEncoding('utf8')
|
|
47
|
+
process.stdin.on('data', (c) => { buf += c })
|
|
48
|
+
process.stdin.on('end', () => resolve(buf))
|
|
49
|
+
setTimeout(() => resolve(buf), 1500) // hook 不能挂住宿主
|
|
50
|
+
})
|
|
51
|
+
} catch { return '' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function targetsCredential(input) {
|
|
55
|
+
if (!input || typeof input !== 'object') return false
|
|
56
|
+
const fields = ['file_path', 'path', 'notebook_path', 'command', 'pattern', 'glob', 'query', 'url', 'content']
|
|
57
|
+
const haystack = fields
|
|
58
|
+
.map((f) => (typeof input[f] === 'string' ? input[f] : ''))
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join('\n')
|
|
61
|
+
if (!haystack) return false
|
|
62
|
+
return SENSITIVE.some((re) => re.test(haystack))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
if (!fs.existsSync(LOADER)) return // 没装加载器 → 不打扰
|
|
67
|
+
const raw = await readStdin()
|
|
68
|
+
let payload = null
|
|
69
|
+
try { payload = JSON.parse(raw || '{}') } catch { return }
|
|
70
|
+
const tool = String(payload?.tool_name || '')
|
|
71
|
+
if (!tool) return
|
|
72
|
+
if (!targetsCredential(payload.tool_input)) return
|
|
73
|
+
process.stdout.write(JSON.stringify({
|
|
74
|
+
hookSpecificOutput: {
|
|
75
|
+
hookEventName: 'PreToolUse',
|
|
76
|
+
permissionDecision: 'deny',
|
|
77
|
+
permissionDecisionReason: REASON,
|
|
78
|
+
},
|
|
79
|
+
}))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch(() => { /* hook 静默失败:绝不因为守卫崩了而挡住正常工具 */ })
|