@mingxy/cerebro-claude-code 0.3.3

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/hooks/stop.mjs ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // cerebro Stop hook — periodic session-ingest flush every N turns
3
+ // Does NOT fire on Ctrl+C interrupt (Claude Code design).
4
+ // SessionEnd detached process covers the interrupt case.
5
+ import { parseStdinJSON, emit, flushSessionIngest, stopCounterGet, stopCounterSet, injectionConfig } from "./common.mjs";
6
+
7
+ const input = parseStdinJSON();
8
+ const tp = input.transcript_path || "";
9
+ const sid = input.session_id || "";
10
+
11
+ const stopCfg = injectionConfig.stopFlush || {};
12
+ if (stopCfg.enabled === false || !tp || !sid) {
13
+ emit({});
14
+ process.exit(0);
15
+ }
16
+
17
+ const interval = stopCfg.interval || 5;
18
+ const counter = stopCounterGet(sid);
19
+ const newCount = counter + 1;
20
+
21
+ if (newCount < interval) {
22
+ stopCounterSet(sid, newCount);
23
+ emit({});
24
+ process.exit(0);
25
+ }
26
+
27
+ // Threshold reached — flush
28
+ stopCounterSet(sid, 0);
29
+ const result = await flushSessionIngest(tp, sid).catch(() => ({ ok: false, count: 0 }));
30
+
31
+ emit({
32
+ systemMessage: result.ok
33
+ ? `🧠 Cerebro · Auto-saved · ${result.count} messages ingested`
34
+ : `🧠 Cerebro · Auto-save failed (will retry next flush)`,
35
+ });
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // cerebro UserPromptSubmit hook — reasoned recall instruction + keyword nudge
3
+ // Injection content is config-driven (~/.claude/cerebro.json), not hardcoded.
4
+ import { parseStdinJSON, emit, postRecallEvent, injectionConfig } from "./common.mjs";
5
+
6
+ const input = parseStdinJSON();
7
+ const prompt = typeof input === "object" ? input.prompt || input.message || "" : "";
8
+ const sid = input.session_id || "";
9
+
10
+ // ─── Build injection from config ────────────────────────────────────────────
11
+ const parts = [];
12
+
13
+ // Recall instruction
14
+ const recallCfg = injectionConfig.recall || {};
15
+ if (recallCfg.enabled !== false && recallCfg.prompt) {
16
+ parts.push(`<cerebro-recall>\n${recallCfg.prompt}\n</cerebro-recall>`);
17
+ }
18
+
19
+ // Keyword nudge
20
+ const nudgeCfg = injectionConfig.nudge || {};
21
+ if (nudgeCfg.enabled !== false) {
22
+ const pLow = prompt.toLowerCase();
23
+ const nudges = [];
24
+ const saveKw = nudgeCfg.saveKeywords || [];
25
+ const recallKw = nudgeCfg.recallKeywords || [];
26
+
27
+ if (saveKw.some((kw) => pLow.includes(String(kw).toLowerCase())) && nudgeCfg.savePrompt) {
28
+ nudges.push(`<cerebro-nudge>${nudgeCfg.savePrompt}</cerebro-nudge>`);
29
+ }
30
+ if (recallKw.some((kw) => pLow.includes(String(kw).toLowerCase())) && nudgeCfg.recallPrompt) {
31
+ nudges.push(`<cerebro-nudge>${nudgeCfg.recallPrompt}</cerebro-nudge>`);
32
+ }
33
+ if (nudges.length) parts.push(nudges.join("\n"));
34
+ }
35
+
36
+ const injectionText = parts.join("\n\n");
37
+
38
+ // ─── POST recall-event (web Sessions page) ───────────────────────────────────
39
+ await postRecallEvent({
40
+ sessionId: sid,
41
+ recallType: "auto",
42
+ queryText: prompt,
43
+ profileInjected: false,
44
+ keptCount: 0,
45
+ injectedContent: injectionText,
46
+ maxScore: 0,
47
+ });
48
+
49
+ emit({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: injectionText } });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@mingxy/cerebro-claude-code",
3
+ "version": "0.3.3",
4
+ "description": "Persistent memory for Claude Code — memories survive across sessions, projects, and machines",
5
+ "author": {
6
+ "name": "mingxy-cerebro",
7
+ "email": "hi@cerebro.dev"
8
+ },
9
+ "license": "Apache-2.0",
10
+ "homepage": "https://github.com/mingxy-cerebro/cerebro-server",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/mingxy-cerebro/cerebro-server",
14
+ "directory": "plugins/claude-code"
15
+ },
16
+ "keywords": [
17
+ "claude-code",
18
+ "memory",
19
+ "persistent",
20
+ "agent-memory",
21
+ "context",
22
+ "recall",
23
+ "profile"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18"
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # cerebro Claude Code plugin — memory-profile skill
3
+ #
4
+ # Retrieve the synthesized user profile (preferences, patterns, identity traits)
5
+ # derived from stored memories. Mirrors plugins/opencode/src/tools.ts::memory_profile.
6
+ #
7
+ # Usage:
8
+ # bash memory-profile.sh
9
+ set -euo pipefail
10
+
11
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
12
+ if [[ -z "$PLUGIN_ROOT" ]]; then
13
+ PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)" || \
14
+ PLUGIN_ROOT="$(dirname "${BASH_SOURCE[0]:-$0}")/.."
15
+ fi
16
+ # shellcheck source=../hooks/common.sh
17
+ source "${PLUGIN_ROOT}/hooks/common.sh"
18
+
19
+ pp="$(detect_project_path || true)"
20
+
21
+ # ─── Build URL-encoded path ──────────────────────────────────────────────────
22
+ url_path="$(MEM_PP="$pp" python3 -c '
23
+ import os, urllib.parse, sys
24
+ pp = (os.environ.get("MEM_PP") or "").strip()
25
+ path = "/v2/profile"
26
+ if pp:
27
+ path += "?project_path=" + urllib.parse.quote(pp, safe="")
28
+ sys.stdout.write(path)
29
+ ')"
30
+
31
+ # ─── Fetch + format ──────────────────────────────────────────────────────────
32
+ resp="$(omem_get "$url_path")"
33
+
34
+ printf '%s' "$resp" | python3 -c '
35
+ import sys, json
36
+ raw = sys.stdin.read()
37
+ try:
38
+ data = json.loads(raw)
39
+ except Exception:
40
+ print("error: invalid JSON response from server")
41
+ sys.exit(0)
42
+ if isinstance(data, dict) and data.get("error"):
43
+ print("error: " + str(data["error"]))
44
+ sys.exit(0)
45
+ # Server may return either a bare PreferenceDto[] list or {preferences:[...]}.
46
+ if isinstance(data, list):
47
+ prefs = data
48
+ elif isinstance(data, dict):
49
+ prefs = data.get("preferences") or data.get("results") or []
50
+ else:
51
+ prefs = []
52
+ if not prefs:
53
+ print("no profile preferences")
54
+ sys.exit(0)
55
+ for p in prefs:
56
+ if not isinstance(p, dict):
57
+ continue
58
+ slot = p.get("slot", "?")
59
+ val = p.get("value", "")
60
+ conf = p.get("confidence", 0)
61
+ scp = p.get("scope", "")
62
+ try:
63
+ line = "%s: %s (conf=%.2f, scope=%s)" % (slot, val, float(conf), scp)
64
+ except (TypeError, ValueError):
65
+ line = "%s: %s (conf=%s, scope=%s)" % (slot, val, conf, scp)
66
+ print(line)
67
+ '
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ # cerebro Claude Code plugin — memory-save skill
3
+ #
4
+ # Persist a fact / decision / preference to long-term memory. Mirrors the
5
+ # LLM-facing contract of plugins/opencode/src/tools.ts::memory_store.
6
+ #
7
+ # Usage:
8
+ # bash memory-save.sh --content "..." [--tags "t1,t2"] [--category X] \
9
+ # [--visibility global|private] [--scope project|global]
10
+ # echo "..." | bash memory-save.sh # content from stdin
11
+ #
12
+ # Category enum (lowercase): cases | preferences | entities | events | profile | patterns
13
+ set -euo pipefail
14
+
15
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
16
+ if [[ -z "$PLUGIN_ROOT" ]]; then
17
+ PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)" || \
18
+ PLUGIN_ROOT="$(dirname "${BASH_SOURCE[0]:-$0}")/.."
19
+ fi
20
+ # shellcheck source=../hooks/common.sh
21
+ source "${PLUGIN_ROOT}/hooks/common.sh"
22
+
23
+ # ─── Arg parsing ─────────────────────────────────────────────────────────────
24
+ content=""
25
+ tags_arg=""
26
+ category=""
27
+ visibility="global"
28
+ scope="project"
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --content) content="${2:-}"; shift 2 ;;
33
+ --tags) tags_arg="${2:-}"; shift 2 ;;
34
+ --category) category="${2:-}"; shift 2 ;;
35
+ --visibility) visibility="${2:-}"; shift 2 ;;
36
+ --scope) scope="${2:-}"; shift 2 ;;
37
+ --help|-h)
38
+ sed -n '2,15p' "${BASH_SOURCE[0]:-$0}" >&2; exit 0 ;;
39
+ --*) echo "unknown option: $1" >&2; exit 2 ;;
40
+ *) content="$1"; shift ;;
41
+ esac
42
+ done
43
+
44
+ # stdin fallback when no --content / positional given
45
+ if [[ -z "$content" && ! -t 0 ]]; then
46
+ content="$(cat || true)"
47
+ fi
48
+
49
+ if [[ -z "$content" ]]; then
50
+ echo 'usage: memory-save.sh --content "..." [--tags t1,t2] [--category X] [--visibility global|private] [--scope project|global]' >&2
51
+ exit 1
52
+ fi
53
+
54
+ # Basic enum validation (fail fast with a friendly message, do not crash harder).
55
+ case "$visibility" in
56
+ global|private) : ;;
57
+ *) echo "error: --visibility must be 'global' or 'private' (got: $visibility)" >&2; exit 2 ;;
58
+ esac
59
+ case "$scope" in
60
+ project|global) : ;;
61
+ *) echo "error: --scope must be 'project' or 'global' (got: $scope)" >&2; exit 2 ;;
62
+ esac
63
+ if [[ -n "$category" ]]; then
64
+ case "$category" in
65
+ cases|preferences|entities|events|profile|patterns) : ;;
66
+ *) echo "error: --category must be one of cases|preferences|entities|events|profile|patterns (got: $category)" >&2; exit 2 ;;
67
+ esac
68
+ fi
69
+
70
+ # ─── Sanitize + assemble tags + project_path ─────────────────────────────────
71
+ content="$(printf '%s' "$content" | sanitize_content)"
72
+ container="$(container_tags || true)"
73
+ pp="$(detect_project_path || true)"
74
+
75
+ # ─── Build JSON body via python3 ─────────────────────────────────────────────
76
+ body="$(MEM_CONTENT="$content" MEM_CONTAINER="$container" MEM_TAGS="$tags_arg" \
77
+ MEM_CAT="$category" MEM_VIS="$visibility" MEM_SCOPE="$scope" MEM_PP="$pp" \
78
+ python3 -c '
79
+ import os, json, sys
80
+ content = os.environ["MEM_CONTENT"]
81
+ container = (os.environ.get("MEM_CONTAINER") or "").split()
82
+ user_tags = [t.strip() for t in (os.environ.get("MEM_TAGS") or "").split(",") if t.strip()]
83
+ tags = container + user_tags
84
+ body = {
85
+ "content": content,
86
+ "tags": tags,
87
+ "source": "claude-code",
88
+ "scope": os.environ.get("MEM_SCOPE") or "project",
89
+ "agent_id": "cerebro",
90
+ "visibility": os.environ.get("MEM_VIS") or "global",
91
+ }
92
+ pp = (os.environ.get("MEM_PP") or "").strip()
93
+ if pp:
94
+ body["project_path"] = pp
95
+ cat = (os.environ.get("MEM_CAT") or "").strip()
96
+ if cat:
97
+ body["category"] = cat
98
+ sys.stdout.write(json.dumps(body, ensure_ascii=False))
99
+ ')"
100
+
101
+ # ─── POST + format response ──────────────────────────────────────────────────
102
+ resp="$(omem_post "/v1/memories" "$body")"
103
+
104
+ printf '%s' "$resp" | python3 -c '
105
+ import sys, json
106
+ raw = sys.stdin.read()
107
+ try:
108
+ data = json.loads(raw)
109
+ except Exception:
110
+ print("error: invalid JSON response from server")
111
+ sys.exit(0)
112
+ if isinstance(data, dict) and data.get("error"):
113
+ print("error: " + str(data["error"]))
114
+ sys.exit(0)
115
+ if not isinstance(data, dict):
116
+ print("ok")
117
+ sys.exit(0)
118
+ mid = data.get("id")
119
+ extra = ""
120
+ tags = data.get("tags") or []
121
+ if isinstance(tags, list) and tags:
122
+ extra = " tags=" + ",".join(str(t) for t in tags)
123
+ if mid is not None:
124
+ print("ok id=" + str(mid) + extra)
125
+ else:
126
+ print("ok" + extra)
127
+ '
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # cerebro Claude Code plugin — memory-search skill
3
+ #
4
+ # Semantic search over long-term memory. Mirrors the LLM-facing contract of
5
+ # plugins/opencode/src/tools.ts::memory_search but shells out via common.sh.
6
+ #
7
+ # Usage:
8
+ # bash memory-search.sh "QUERY" [LIMIT]
9
+ # echo "auth flow" | bash memory-search.sh - [LIMIT]
10
+ set -euo pipefail
11
+
12
+ # Locate plugin root (CLAUDE_PLUGIN_ROOT when invoked by Claude Code; otherwise
13
+ # derive from this script's location: scripts/foo.sh -> .. = plugin root).
14
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
15
+ if [[ -z "$PLUGIN_ROOT" ]]; then
16
+ PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)" || \
17
+ PLUGIN_ROOT="$(dirname "${BASH_SOURCE[0]:-$0}")/.."
18
+ fi
19
+ # shellcheck source=../hooks/common.sh
20
+ source "${PLUGIN_ROOT}/hooks/common.sh"
21
+
22
+ # ─── Args ────────────────────────────────────────────────────────────────────
23
+ query="${1:-}"
24
+ limit="${2:-${MEM_SEARCH_COUNT:-8}}"
25
+
26
+ # Allow `echo "q" | memory-search.sh - [LIMIT]` for stdin-driven queries.
27
+ if [[ "$query" == "-" || (-z "$query" && ! -t 0) ]]; then
28
+ query=$(cat || true)
29
+ fi
30
+
31
+ if [[ -z "$query" ]]; then
32
+ echo "usage: memory-search.sh \"QUERY\" [LIMIT]" >&2
33
+ exit 1
34
+ fi
35
+
36
+ query="$(truncate_query "$query")"
37
+ tags="$(container_tags || true)"
38
+ pp="$(detect_project_path || true)"
39
+
40
+ # ─── Build URL-encoded GET path via python3 (avoid bash quoting traps) ───────
41
+ url_path="$(MEM_Q="$query" MEM_TAGS="$tags" MEM_PP="$pp" MEM_LIMIT="$limit" python3 -c '
42
+ import os, urllib.parse, sys
43
+ q = os.environ.get("MEM_Q", "")
44
+ limit = os.environ.get("MEM_LIMIT", "8")
45
+ parts = [
46
+ "q=" + urllib.parse.quote(q, safe=""),
47
+ "limit=" + urllib.parse.quote(str(limit), safe=""),
48
+ ]
49
+ tags_raw = (os.environ.get("MEM_TAGS") or "").strip()
50
+ if tags_raw:
51
+ comma = ",".join(t for t in tags_raw.split() if t)
52
+ if comma:
53
+ # comma is reserved-safe in query per RFC 3986; keep literal
54
+ parts.append("tags=" + ",".join(urllib.parse.quote(t, safe="") for t in comma.split(",")))
55
+ pp = (os.environ.get("MEM_PP") or "").strip()
56
+ if pp:
57
+ parts.append("project_path=" + urllib.parse.quote(pp, safe=""))
58
+ sys.stdout.write("/v1/memories/search?" + "&".join(parts))
59
+ ')"
60
+
61
+ # ─── Fetch + format ──────────────────────────────────────────────────────────
62
+ resp="$(omem_get "$url_path")"
63
+
64
+ printf '%s' "$resp" | python3 -c '
65
+ import sys, json
66
+ raw = sys.stdin.read()
67
+ try:
68
+ data = json.loads(raw)
69
+ except Exception:
70
+ print("error: invalid JSON response from server")
71
+ sys.exit(0)
72
+ if isinstance(data, dict) and data.get("error"):
73
+ print("error: " + str(data["error"]))
74
+ sys.exit(0)
75
+ results = data.get("results", []) if isinstance(data, dict) else []
76
+ if not results:
77
+ print("no memories")
78
+ sys.exit(0)
79
+ for r in results:
80
+ if not isinstance(r, dict):
81
+ continue
82
+ m = r.get("memory") or {}
83
+ if not isinstance(m, dict):
84
+ m = {}
85
+ score = r.get("score", 0.0)
86
+ mid = m.get("id", "?")
87
+ content = (m.get("content") or "")
88
+ snippet = content[:200]
89
+ try:
90
+ line = "[%.2f] %s: %s" % (float(score), mid, snippet)
91
+ except (TypeError, ValueError):
92
+ line = "[%s] %s: %s" % (score, mid, snippet)
93
+ print(line)
94
+ '
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // cerebro web-server — CC 插件独立进程(移植自 plugins/opencode/src/web-server.ts)
3
+ // 由 session-start.sh 经 setsid detach 拉起,多 CC session 共享端口 5212。
4
+ // 与 opencode 版的区别:去掉 takeover 定时器(SessionStart probe 已够),纯静态 serve。
5
+ import http from "node:http";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { writeFileSync, unlinkSync } from "node:fs";
10
+
11
+ const PID_FILE = path.join(process.env.HOME || process.env.USERPROFILE || "", ".config/cerebro/web-server.pid");
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const PORT = parseInt(process.env.OMEM_LOCAL_PORT || "", 10) || 5212;
15
+ const API_URL = process.env.OMEM_API_URL || "https://www.mengxy.cc";
16
+
17
+ // web 目录查找:env > 本地 web/ > 兄弟 opencode/web/
18
+ function findWebDir() {
19
+ const candidates = [
20
+ process.env.CEREBRO_WEB_DIR,
21
+ path.resolve(__dirname, "../web"),
22
+ path.resolve(__dirname, "../../opencode/web"),
23
+ ].filter(Boolean);
24
+ for (const d of candidates) {
25
+ if (fs.existsSync(path.join(d, "index.html"))) return d;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ const WEB_DIR = findWebDir();
31
+ if (!WEB_DIR) {
32
+ console.error("[cerebro web-server] no web directory found, exiting");
33
+ process.exit(0); // exit 0 — not an error, just nothing to serve
34
+ }
35
+
36
+ const COMMON = { "X-Content-Type-Options": "nosniff" };
37
+ const MIME = {
38
+ ".html": "text/html; charset=utf-8",
39
+ ".js": "application/javascript; charset=utf-8",
40
+ ".mjs": "application/javascript; charset=utf-8",
41
+ ".css": "text/css; charset=utf-8",
42
+ ".json": "application/json; charset=utf-8",
43
+ ".svg": "image/svg+xml",
44
+ ".png": "image/png",
45
+ ".jpg": "image/jpeg",
46
+ ".ico": "image/x-icon",
47
+ ".woff": "font/woff",
48
+ ".woff2": "font/woff2",
49
+ ".ttf": "font/ttf",
50
+ ".webp": "image/webp",
51
+ ".map": "application/json",
52
+ };
53
+
54
+ function resolveSafe(baseDir, pathname) {
55
+ const rel = pathname.startsWith("/") ? pathname.slice(1) : pathname;
56
+ const resolved = path.resolve(baseDir, rel || ".");
57
+ if (!resolved.startsWith(baseDir + path.sep) && resolved !== baseDir) return null;
58
+ return resolved;
59
+ }
60
+
61
+ function serveFile(res, filePath) {
62
+ const ext = path.extname(filePath).toLowerCase();
63
+ fs.readFile(filePath, (err, data) => {
64
+ if (err) {
65
+ res.writeHead(500, { ...COMMON, "Content-Type": "text/plain" });
66
+ res.end("Internal Server Error");
67
+ return;
68
+ }
69
+ let body = data;
70
+ if (ext === ".html" && data.includes("__OMEM_API_URL__")) {
71
+ body = data.toString("utf-8").replace(
72
+ /window\.__OMEM_API_URL__\s*=\s*["']__OMEM_API_URL__["']/,
73
+ `window.__OMEM_API_URL__ = ${JSON.stringify(API_URL)}`,
74
+ );
75
+ }
76
+ res.writeHead(200, {
77
+ ...COMMON,
78
+ "Content-Type": MIME[ext] || "application/octet-stream",
79
+ "Cache-Control": ext === ".html" ? "no-cache, no-store, must-revalidate" : "public, max-age=86400",
80
+ });
81
+ res.end(body);
82
+ });
83
+ }
84
+
85
+ const indexPath = path.join(WEB_DIR, "index.html");
86
+ const server = http.createServer((req, res) => {
87
+ if (req.url === "/health" || req.url === "/health/") {
88
+ res.writeHead(200, { ...COMMON, "Content-Type": "application/json" });
89
+ res.end(JSON.stringify({ status: "ok", service: "cerebro", port: PORT }));
90
+ return;
91
+ }
92
+ if (req.method !== "GET" && req.method !== "HEAD") {
93
+ res.writeHead(405, { ...COMMON, "Content-Type": "text/plain" });
94
+ res.end("Method Not Allowed");
95
+ return;
96
+ }
97
+ const url = new URL(req.url || "/", `http://localhost:${PORT}`);
98
+ const safePath = resolveSafe(WEB_DIR, url.pathname);
99
+ if (!safePath) {
100
+ res.writeHead(403, { ...COMMON, "Content-Type": "text/plain" });
101
+ res.end("Forbidden");
102
+ return;
103
+ }
104
+ fs.stat(safePath, (statErr, stats) => {
105
+ if (!statErr && stats.isFile()) { serveFile(res, safePath); return; }
106
+ fs.stat(indexPath, (idxErr, idxStats) => {
107
+ if (idxErr || !idxStats.isFile()) {
108
+ res.writeHead(404, { ...COMMON, "Content-Type": "text/plain" });
109
+ res.end("Not Found");
110
+ return;
111
+ }
112
+ serveFile(res, indexPath);
113
+ });
114
+ });
115
+ });
116
+
117
+ server.on("error", (err) => {
118
+ if (err.code === "EADDRINUSE") {
119
+ // 另一个 CC session / opencode 已占端口 — 正常,静默退出
120
+ process.exit(0);
121
+ }
122
+ console.error(`[cerebro web-server] error: ${err.message}`);
123
+ process.exit(1);
124
+ });
125
+
126
+ server.listen(PORT, "127.0.0.1", () => {
127
+ try { writeFileSync(PID_FILE, String(process.pid)); } catch {}
128
+ console.log(`[cerebro web-server] serving ${WEB_DIR} at http://localhost:${PORT} (pid=${process.pid})`);
129
+ });
130
+
131
+ process.on("SIGTERM", () => server.close(() => { try { unlinkSync(PID_FILE); } catch {} process.exit(0); }));
132
+ process.on("SIGINT", () => server.close(() => { try { unlinkSync(PID_FILE); } catch {} process.exit(0); }));
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: memory-profile
3
+ description: Retrieve the synthesized user profile (preferences, patterns, identity traits) induced from stored memories. Use at session start to ground yourself in the user's working style, tooling preferences, recurring workflows, and role context; or whenever adapting tone, format, or approach to match the user's established patterns would improve the interaction.
4
+ ---
5
+
6
+ # Memory Profile
7
+
8
+ Read the induced preference profile for the current user/project. Each preference is a `{slot, value, confidence, scope}` tuple synthesized from raw memories.
9
+
10
+ ## When to use
11
+
12
+ - Session bootstrap — load working-style context before doing work
13
+ - User asks "what do you know about me / my preferences / how I work"
14
+ - Adapting response format, tone, or tooling to the user's established patterns
15
+ - Checking whether a candidate action aligns with stored preferences
16
+
17
+ ## How to run
18
+
19
+ ```bash
20
+ bash "$CLAUDE_PLUGIN_ROOT/scripts/memory-profile.sh"
21
+ ```
22
+
23
+ No arguments. `project_path` is auto-detected from git toplevel and forwarded to the server.
24
+
25
+ ## Output format
26
+
27
+ One line per preference:
28
+
29
+ ```
30
+ preferred_language: rust (conf=0.92, scope=global)
31
+ review_style: terse, no praise (conf=0.85, scope=project)
32
+ indent_style: 2-space (conf=0.78, scope=global)
33
+ ```
34
+
35
+ `no profile preferences` when the profile is empty. `error: …` on transport/server failure.
36
+
37
+ ## Notes
38
+
39
+ - Confidence (`conf`) is the induction strength — treat values below ~0.5 as weak signals.
40
+ - `scope=global` preferences apply across projects; `scope=project` only to the current one.
41
+ - Profile is read-only here. To influence it, save memories via `memory-save` with `category=preferences` or `category=profile`; the server's induction pipeline will fold them in.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: memory-save
3
+ description: Persist a fact, decision, or preference to the user's long-term memory (Cerebro). Use when the user explicitly says "remember / save / store / don't forget / note this", or when you identify important information worth preserving across sessions — preferences, coding style, architecture decisions, bug fixes, project entities, milestones, workflows, or user identity traits. Each memory must be atomic (one fact), self-contained, and precise. Private secrets/credentials/personal data MUST use visibility=private; cross-project knowledge uses scope=global.
4
+ ---
5
+
6
+ # Memory Save
7
+
8
+ Store one atomic, self-contained memory. Before calling, decide:
9
+ 1. **category** — which of the 6 buckets does this belong to?
10
+ 2. **scope** — project-specific or cross-project?
11
+ 3. **visibility** — does it contain sensitive data?
12
+ 4. **tags** — at least one descriptive snake_case tag.
13
+
14
+ ## Category enum (lowercase, exact)
15
+
16
+ | Value | Use for |
17
+ |-------|---------|
18
+ | `cases` (default) | Work records, bug fixes, architecture decisions, troubleshooting notes |
19
+ | `preferences` | User likes/dislikes, coding style, tool choices, review habits |
20
+ | `entities` | Projects, tools, libraries, people, concepts worth remembering |
21
+ | `events` | Time-bound milestones — deployments, releases, incidents, deadlines |
22
+ | `profile` | User identity traits — role, skills, team membership, timezone |
23
+ | `patterns` | Workflows, methodologies, recurring best practices |
24
+
25
+ ## Visibility
26
+
27
+ - `global` (default) — all agents can see and recall. Correct for normal work notes.
28
+ - `private` — ONLY the current agent sees it. **MUST use for**: passwords, API keys, tokens, DB credentials, SSH keys, personal info (phone/email/address), internal company details, or anything the user would not want other agents to access. When in doubt, ask the user.
29
+
30
+ ## Scope
31
+
32
+ - `project` (default) — visible only in this project's context (auto `project_path`).
33
+ - `global` — visible across all projects. Use for user preferences, general knowledge, cross-project patterns.
34
+
35
+ ## How to run
36
+
37
+ ```bash
38
+ bash "$CLAUDE_PLUGIN_ROOT/scripts/memory-save.sh" \
39
+ --content "Fixed memory_type validation bug in memory.rs:1480 — LLM returned illegal 'pinned' value, added match guard normalizing to WORK/EMOTIONAL fallback" \
40
+ --tags "rust_backend,memory_system,bug_fix" \
41
+ --category cases \
42
+ --scope project \
43
+ --visibility global
44
+ ```
45
+
46
+ Content can also be piped:
47
+
48
+ ```bash
49
+ echo "user prefers terse caveman-style replies" | \
50
+ bash "$CLAUDE_PLUGIN_ROOT/scripts/memory-save.sh" --tags "communication,style" --category preferences --scope global
51
+ ```
52
+
53
+ ## Writing good content
54
+
55
+ - BAD: "fixed some bugs"
56
+ - GOOD: "Fixed refresh-token rotation bug in auth.rs:230 — replay window allowed token reuse; added nonce cache with 60s TTL."
57
+ - BAD: "user likes stuff"
58
+ - GOOD: "User prefers dark IDE theme, 2-space indent for Rust, tabs for Go."
59
+
60
+ ## Output
61
+
62
+ - Success: `ok id=mem_abc123 tags=omem_user_xxx,rust_backend`
63
+ - Failure: `error: <reason>`
64
+
65
+ ## Notes
66
+
67
+ - Content is sanitized (XML tags stripped, whitespace collapsed, truncated to `$MEM_MAX_CONTENT`).
68
+ - Container tags (`omem_user_<hash>`, `omem_project_<hash>`) are auto-prepended.
69
+ - `project_path` is auto-detected from git toplevel.
70
+ - `source` is hardcoded to `claude-code`, `agent_id` to `cerebro`.