cognitive-enzyme-hook 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/README.md +76 -0
- package/capture.mjs +133 -0
- package/index.mjs +110 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# cognitive-enzyme-hook
|
|
2
|
+
|
|
3
|
+
A [Claude Code](https://claude.com/claude-code) `UserPromptSubmit` hook that turns
|
|
4
|
+
[Cognitive Enzyme](../../README.md) into a **System 1 push layer**: before every
|
|
5
|
+
prompt reaches the model, it calls the Cognitive Enzyme server's `/api/prime` and
|
|
6
|
+
injects the activated reflexes as context.
|
|
7
|
+
|
|
8
|
+
> MCP's `prime()` is a *pull* (the agent must decide to call it). This hook makes it a
|
|
9
|
+
> *push*: the right intuitions appear automatically, every turn.
|
|
10
|
+
|
|
11
|
+
**It never blocks your prompt.** If the server is down, unreachable, times out, or
|
|
12
|
+
returns an error, the hook silently exits without injecting anything.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
From a clone of this repository (the package is not published to the npm
|
|
17
|
+
registry yet):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i -g ./hooks/npm
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Once published: `npm i -g cognitive-enzyme-hook`.
|
|
24
|
+
|
|
25
|
+
## Configure
|
|
26
|
+
|
|
27
|
+
Add to `~/.claude/settings.json` (or a project `.claude/settings.json`):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"hooks": {
|
|
32
|
+
"UserPromptSubmit": [
|
|
33
|
+
{ "hooks": [{ "type": "command", "command": "ce-prime-hook", "timeout": 10 }] }
|
|
34
|
+
],
|
|
35
|
+
"SessionEnd": [
|
|
36
|
+
{ "hooks": [{ "type": "command", "command": "ce-capture-hook", "timeout": 30 }] }
|
|
37
|
+
],
|
|
38
|
+
"PreCompact": [
|
|
39
|
+
{ "hooks": [{ "type": "command", "command": "ce-capture-hook", "timeout": 30 }] }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"env": {
|
|
43
|
+
"CE_API_URL": "http://localhost:8420",
|
|
44
|
+
"CE_API_TOKEN": "<your machine token>"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`ce-capture-hook` distills the finished session's transcript into reusable
|
|
50
|
+
memories via the server's `/api/capture` (System 2 write path); it fires on
|
|
51
|
+
`SessionEnd` and `PreCompact` and never blocks.
|
|
52
|
+
|
|
53
|
+
Then open `/hooks` once (or restart Claude Code) to load it.
|
|
54
|
+
|
|
55
|
+
> Zero-install alternative: use `"command": "npx", "args": ["-y", "cognitive-enzyme-hook"]`.
|
|
56
|
+
> Convenient, but `npx` resolves the package on **every** prompt — adds noticeable latency
|
|
57
|
+
> on the prompt critical path. Prefer the global install above.
|
|
58
|
+
|
|
59
|
+
## Environment variables
|
|
60
|
+
|
|
61
|
+
| Variable | Default | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `CE_API_URL` | `http://localhost:8420` | Cognitive Enzyme REST base URL |
|
|
64
|
+
| `CE_API_TOKEN` | — | Static machine token (required when the server has auth enabled) |
|
|
65
|
+
| `CE_PRIME_TIMEOUT` | `3` | Request timeout, seconds (kept short — it's on the prompt path) |
|
|
66
|
+
| `CE_PRIME_MAX` | `6` | Max reflexes injected per prompt (token control) |
|
|
67
|
+
| `CE_PRIME_MIN_CONF` | `0.4` | Minimum enzyme confidence to inject |
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- Node.js ≥ 18 (uses built-in `fetch`)
|
|
72
|
+
- A running Cognitive Enzyme server (`python -m app`)
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/capture.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cognitive-enzyme-capture — Claude Code `SessionEnd` / `PreCompact` / `Stop` hook.
|
|
4
|
+
*
|
|
5
|
+
* System 2 写路径(与 index.mjs 读路径相对):会话结束或上下文压缩前,读取
|
|
6
|
+
* Claude Code 的 transcript,POST 到 CE 的 REST `/api/capture`。CE 端异步做
|
|
7
|
+
* 「提取候选 → 撞库 → 对账(ADD/UPDATE/REINFORCE/INVALIDATE)」,本 hook 立即返回。
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:**绝不阻塞、绝不报错给用户**。CE 没起/超时/异常一律静默退出 0。
|
|
10
|
+
* 零依赖:仅用 Node 18+ 内置 fetch / AbortController / fs。
|
|
11
|
+
*
|
|
12
|
+
* 可配置 env:
|
|
13
|
+
* CE_API_URL REST 基址,默认 http://localhost:8420
|
|
14
|
+
* CE_API_TOKEN 静态机器令牌(服务端开启鉴权时必填)
|
|
15
|
+
* CE_CAPTURE_TIMEOUT 请求超时(秒),默认 5
|
|
16
|
+
* CE_CAPTURE_MAX_CHARS transcript 摘要上限,默认 16000
|
|
17
|
+
* CE_CAPTURE_TURNS 最近纳入多少轮,默认 40
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
const API_URL = (process.env.CE_API_URL || "http://localhost:8420").replace(/\/+$/, "");
|
|
23
|
+
const API_TOKEN = (process.env.CE_API_TOKEN || "").trim();
|
|
24
|
+
const TIMEOUT_MS = Math.round((Number(process.env.CE_CAPTURE_TIMEOUT) || 5) * 1000);
|
|
25
|
+
const MAX_CHARS = Number(process.env.CE_CAPTURE_MAX_CHARS) || 16000;
|
|
26
|
+
const MAX_TURNS = Number(process.env.CE_CAPTURE_TURNS) || 40;
|
|
27
|
+
|
|
28
|
+
function bail() {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readStdin() {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
35
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// prime 注入块(哨兵定界或旧格式)压缩为一行 ⟦CE:SEEN id,...⟧:
|
|
39
|
+
// 省摘要预算,同时保住酶 id 供服务端做被动效果评估。
|
|
40
|
+
const INJECT_RE = /⟦CE:BEGIN⟧[\s\S]*?⟦CE:END⟧/g;
|
|
41
|
+
const LEGACY_RE = /\[认知酶 · System 1 直觉\][\s\S]*?(?:主动检索。|以强化\/纠正它。)/g;
|
|
42
|
+
const ID_RE = /‹enzyme_id=([^›\s]+)›/g;
|
|
43
|
+
|
|
44
|
+
function compressInjections(text) {
|
|
45
|
+
const repl = (m) => {
|
|
46
|
+
const ids = [...new Set([...m.matchAll(ID_RE)].map((x) => x[1]))];
|
|
47
|
+
return ids.length ? `⟦CE:SEEN ${ids.join(",")}⟧` : "";
|
|
48
|
+
};
|
|
49
|
+
return text.replace(INJECT_RE, repl).replace(LEGACY_RE, repl);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** message.content 可能是字符串或 block 列表,统一抽成文本。 */
|
|
53
|
+
function textFromContent(content) {
|
|
54
|
+
if (typeof content === "string") return content;
|
|
55
|
+
if (Array.isArray(content)) {
|
|
56
|
+
return content
|
|
57
|
+
.filter((b) => b && b.type === "text")
|
|
58
|
+
.map((b) => b.text || "")
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 读取 transcript JSONL,拼出最近若干轮 user/assistant 的纯文本。 */
|
|
65
|
+
function readTranscript(path) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = readFileSync(path, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
const turns = [];
|
|
73
|
+
for (const line of raw.split("\n")) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed) continue;
|
|
76
|
+
let obj;
|
|
77
|
+
try {
|
|
78
|
+
obj = JSON.parse(trimmed);
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const msg = obj.message || obj;
|
|
83
|
+
const role = msg.role || obj.type;
|
|
84
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
85
|
+
const text = compressInjections(textFromContent(msg.content || "")).trim();
|
|
86
|
+
if (text) turns.push(`[${role}] ${text}`);
|
|
87
|
+
}
|
|
88
|
+
if (turns.length === 0) return "";
|
|
89
|
+
return turns.slice(-MAX_TURNS).join("\n\n").slice(-MAX_CHARS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
if (typeof fetch !== "function") bail(); // Node <18
|
|
94
|
+
|
|
95
|
+
let payload;
|
|
96
|
+
try {
|
|
97
|
+
payload = JSON.parse(await readStdin());
|
|
98
|
+
} catch {
|
|
99
|
+
bail();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const transcriptPath = payload.transcript_path || "";
|
|
103
|
+
if (!transcriptPath) bail();
|
|
104
|
+
|
|
105
|
+
const transcript = readTranscript(transcriptPath);
|
|
106
|
+
if (!transcript || transcript.length < 200) bail(); // 太短不值得跑 LLM
|
|
107
|
+
|
|
108
|
+
const headers = { "Content-Type": "application/json" };
|
|
109
|
+
if (API_TOKEN) headers.Authorization = `Bearer ${API_TOKEN}`;
|
|
110
|
+
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
113
|
+
try {
|
|
114
|
+
await fetch(`${API_URL}/api/capture`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers,
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
transcript,
|
|
119
|
+
session_id: payload.session_id || "",
|
|
120
|
+
source: (payload.hook_event_name || "capture").toLowerCase(),
|
|
121
|
+
domain: "",
|
|
122
|
+
}),
|
|
123
|
+
signal: controller.signal,
|
|
124
|
+
});
|
|
125
|
+
} catch {
|
|
126
|
+
// 服务不可用 / 超时 → 静默放过
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
bail();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch(() => process.exit(0));
|
package/index.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cognitive-enzyme-hook — Claude Code `UserPromptSubmit` hook.
|
|
4
|
+
*
|
|
5
|
+
* 把 Cognitive Enzyme 的反射作为 System 1 直觉注入当前 prompt:读取 Claude Code
|
|
6
|
+
* 在 stdin 给的 hook 输入(含 prompt),调 CE 的 REST `/api/prime`,把命中的酶
|
|
7
|
+
* 格式化成一段上下文,通过 additionalContext 注回模型。
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:**绝不阻塞 prompt**。CE 没起、超时、报错——一律静默退出 0、不注入。
|
|
10
|
+
* 零依赖:仅用 Node 18+ 内置 fetch / AbortController / process.stdin。
|
|
11
|
+
*
|
|
12
|
+
* 可配置 env:
|
|
13
|
+
* CE_API_URL REST 基址,默认 http://localhost:8420
|
|
14
|
+
* CE_API_TOKEN 静态机器令牌(服务端开启鉴权时必填)
|
|
15
|
+
* CE_PRIME_TIMEOUT 请求超时(秒),默认 3
|
|
16
|
+
* CE_PRIME_MAX 注入条数上限,默认 6
|
|
17
|
+
* CE_PRIME_MIN_CONF 置信度下限,默认 0.4
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const API_URL = (process.env.CE_API_URL || "http://localhost:8420").replace(/\/+$/, "");
|
|
21
|
+
const API_TOKEN = (process.env.CE_API_TOKEN || "").trim();
|
|
22
|
+
const TIMEOUT_MS = Math.round((Number(process.env.CE_PRIME_TIMEOUT) || 3) * 1000);
|
|
23
|
+
const MAX_RESULTS = Number(process.env.CE_PRIME_MAX) || 6;
|
|
24
|
+
const MIN_CONFIDENCE = Number(process.env.CE_PRIME_MIN_CONF) || 0.4;
|
|
25
|
+
|
|
26
|
+
const SEV_ICON = { critical: "🔴", warning: "🟡", info: "🟢" };
|
|
27
|
+
|
|
28
|
+
/** 输出 UserPromptSubmit 的 JSON 并退出;空串=不注入。永远 exit 0。 */
|
|
29
|
+
function emit(additionalContext) {
|
|
30
|
+
if (additionalContext) {
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
hookSpecificOutput: {
|
|
34
|
+
hookEventName: "UserPromptSubmit",
|
|
35
|
+
additionalContext,
|
|
36
|
+
},
|
|
37
|
+
suppressOutput: true,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readStdin() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
47
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
if (typeof fetch !== "function") emit(""); // Node <18:放弃注入,不报错
|
|
52
|
+
|
|
53
|
+
let payload;
|
|
54
|
+
try {
|
|
55
|
+
payload = JSON.parse(await readStdin());
|
|
56
|
+
} catch {
|
|
57
|
+
emit("");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const context = String(payload.prompt || "").trim();
|
|
61
|
+
if (!context) emit("");
|
|
62
|
+
|
|
63
|
+
const headers = { "Content-Type": "application/json" };
|
|
64
|
+
if (API_TOKEN) headers.Authorization = `Bearer ${API_TOKEN}`;
|
|
65
|
+
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
68
|
+
let data;
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${API_URL}/api/prime`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers,
|
|
73
|
+
body: JSON.stringify({ context, max_results: MAX_RESULTS }),
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) emit(""); // 401 / 5xx 等:静默放过
|
|
77
|
+
data = await res.json();
|
|
78
|
+
} catch {
|
|
79
|
+
emit(""); // 服务不可用 / 超时
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const matches = (data.matches || []).filter(
|
|
85
|
+
(m) => (m.confidence || 0) >= MIN_CONFIDENCE,
|
|
86
|
+
);
|
|
87
|
+
if (matches.length === 0) emit("");
|
|
88
|
+
|
|
89
|
+
// ⟦CE:BEGIN⟧/⟦CE:END⟧ 哨兵供 capture 端识别注入块:剥离防回声 + 被动效果评估
|
|
90
|
+
const lines = [
|
|
91
|
+
"⟦CE:BEGIN⟧",
|
|
92
|
+
"[认知酶 · System 1 直觉] 以下反射被当前上下文激活,供参考(无关可忽略):",
|
|
93
|
+
];
|
|
94
|
+
for (const m of matches) {
|
|
95
|
+
const icon = SEV_ICON[m.severity] || "⚪";
|
|
96
|
+
const conf = Math.round((m.confidence || 0) * 100);
|
|
97
|
+
let line = `${icon} [${conf}%] ${m.suggestion || ""}`;
|
|
98
|
+
if (m.reason) line += `(因为:${m.reason})`;
|
|
99
|
+
line += ` ‹enzyme_id=${m.id}›`;
|
|
100
|
+
lines.push(line);
|
|
101
|
+
}
|
|
102
|
+
lines.push(
|
|
103
|
+
"若已连接 cognitive-enzyme MCP:验证后请调 report(enzyme_ids=[上列 id 按序], success) " +
|
|
104
|
+
"反馈以强化/纠正;任务中途需要更多相关经验可调 search(query) 主动检索。",
|
|
105
|
+
);
|
|
106
|
+
lines.push("⟦CE:END⟧");
|
|
107
|
+
emit(lines.join("\n"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch(() => process.exit(0));
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cognitive-enzyme-hook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code hooks for Cognitive Enzyme: inject System-1 reflexes on prompt (prime) and distill memories on session end (capture).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ce-prime-hook": "index.mjs",
|
|
8
|
+
"ce-capture-hook": "capture.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/7-e1even/Cognitive-Enzyme.git",
|
|
16
|
+
"directory": "hooks/npm"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/7-e1even/Cognitive-Enzyme#readme",
|
|
19
|
+
"files": [
|
|
20
|
+
"index.mjs",
|
|
21
|
+
"capture.mjs",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"claude-code",
|
|
26
|
+
"claude",
|
|
27
|
+
"hook",
|
|
28
|
+
"cognitive-enzyme",
|
|
29
|
+
"mcp",
|
|
30
|
+
"ai-agent",
|
|
31
|
+
"system-1"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|