@mingxy/cerebro-claude-code 0.3.7 → 0.3.9
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/marketplace.json +0 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +2 -1
- package/README.md +0 -0
- package/config.json +0 -0
- package/hooks/apply-dream.mjs +11 -1
- package/hooks/common.mjs +36 -6
- package/hooks/common.sh +0 -0
- package/hooks/dream.mjs +0 -0
- package/hooks/flush-detached.mjs +0 -0
- package/hooks/hooks.json +0 -0
- package/hooks/post-compact.mjs +0 -0
- package/hooks/pre-compact.mjs +0 -0
- package/hooks/recall-approve.mjs +0 -0
- package/hooks/session-end.mjs +0 -0
- package/hooks/session-start.mjs +2 -2
- package/hooks/stop.mjs +0 -0
- package/hooks/user-prompt-submit.mjs +0 -0
- package/package.json +1 -1
- package/scripts/memory-profile.sh +0 -0
- package/scripts/memory-save.sh +0 -0
- package/scripts/memory-search.sh +0 -0
- package/scripts/web-server.mjs +0 -0
- package/skills/apply-dream/SKILL.md +0 -0
- package/skills/dream/SKILL.md +0 -0
- package/skills/memory-profile/SKILL.md +0 -0
- package/skills/memory-save/SKILL.md +0 -0
- package/skills/memory-search/SKILL.md +0 -0
- package/tests/common.test.mjs +0 -0
- package/tests/hooks.test.mjs +37 -0
- package/tests/test_smoke.sh +0 -0
- package/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/web/assets/index-DVguCuEA.css +0 -0
- package/web/assets/index-zOJZZn-i.js +0 -0
- package/web/favicon.svg +0 -0
- package/web/icons.svg +0 -0
- package/web/index.html +0 -0
|
File without changes
|
package/.mcp.json
CHANGED
package/README.md
CHANGED
|
File without changes
|
package/config.json
CHANGED
|
File without changes
|
package/hooks/apply-dream.mjs
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// unknown kept name → surfaced as `unknown`, NEVER silently dropped (that is memory evaporation)
|
|
6
6
|
// dropped → removal candidate, listed for review; applied only with --apply
|
|
7
7
|
// merged/updated/added → LLM version wins (content is allowed to change there)
|
|
8
|
+
// except: added onto an existing local file is a conflict (LLM relabeled an
|
|
9
|
+
// existing memory as new) — skipped and surfaced, never blindly overwrites
|
|
8
10
|
// stats.total still counts kept entries — the ledger is the server's, not ours to re-derive
|
|
9
11
|
//
|
|
10
12
|
// Read-only review by default (prints the diff); `--apply` writes after user approval.
|
|
@@ -88,6 +90,7 @@ let entries = JSON.parse(readFileSync(archivePath, "utf8")).entries || [];
|
|
|
88
90
|
// ─── merge ────────────────────────────────────────────────────────────────────
|
|
89
91
|
const unknown = []; // kept names missing from the old archive — LLM renamed, memory at risk
|
|
90
92
|
const empty = []; // content actions with empty body+description — never written, surfaced
|
|
93
|
+
const conflict = []; // added but a local file with that name exists — LLM rewrite mislabeled as new
|
|
91
94
|
const report = { keep: 0, write: [], drop: [], dropCount: 0 };
|
|
92
95
|
for (let e of entries) {
|
|
93
96
|
// normalize BEFORE any branch: an LLM-emitted Chinese name that the MEMORY.md
|
|
@@ -105,6 +108,11 @@ for (let e of entries) {
|
|
|
105
108
|
// 60-byte frontmatter shell. merged/updated skip = old content survives;
|
|
106
109
|
// added skip = surfaced below, not silently dropped.
|
|
107
110
|
if (!(e.body || "").trim() && !(e.description || "").trim()) { empty.push(`${e.name} (${act})`); continue; }
|
|
111
|
+
// added onto an existing local file = the LLM relabeled a rewrite as new
|
|
112
|
+
// (deepseek tic, cf. the 2026-08-20 archive where 6 existing entries came
|
|
113
|
+
// back as added with condensed bodies and zero new info). Its content
|
|
114
|
+
// would trade a hand-written file for a stub — skip, surface, human decides.
|
|
115
|
+
if (act === "added" && oldFiles.has(e.name)) { conflict.push(e.name); continue; }
|
|
108
116
|
report.write.push(e); // LLM content is authoritative for these actions
|
|
109
117
|
} else {
|
|
110
118
|
unknown.push(`${e.name} (action=${act || "?"})`);
|
|
@@ -114,15 +122,17 @@ for (let e of entries) {
|
|
|
114
122
|
// ─── review output ────────────────────────────────────────────────────────────
|
|
115
123
|
const lines = [
|
|
116
124
|
`apply-dream review · archive ${archivePath}`,
|
|
117
|
-
`kept ${report.keep} / write ${report.write.length} / drop ${report.dropCount} / unknown ${unknown.length}`,
|
|
125
|
+
`kept ${report.keep} / write ${report.write.length} / drop ${report.dropCount} / unknown ${unknown.length} / conflict ${conflict.length}`,
|
|
118
126
|
];
|
|
119
127
|
for (const e of report.write) lines.push(` ${e.source || e.action} ${e.name} — ${(e.description || "").slice(0, 60)}`);
|
|
120
128
|
for (const n of report.drop) lines.push(` drop ${n}`);
|
|
121
129
|
for (const n of unknown) lines.push(` ? ${n} ← surfaced, not dropped`);
|
|
122
130
|
for (const n of empty) lines.push(` ~ ${n} ← empty stub skipped, not written`);
|
|
131
|
+
for (const n of conflict) lines.push(` ! ${n} ← added but exists locally, skipped — review manually`);
|
|
123
132
|
if (orphanFiles.length) lines.push(` (unparsed old files left untouched: ${orphanFiles.length})`);
|
|
124
133
|
console.log(lines.join("\n"));
|
|
125
134
|
if (unknown.length) console.log("\n⚠ URGENT: unknown kept names above — surface to the user BEFORE applying; they may be renames the LLM invented.");
|
|
135
|
+
if (conflict.length) console.log("\n⚠ CONFLICT: added-but-exists above — old file kept, dream content discarded; merge by hand if the dream version carries new info.");
|
|
126
136
|
if (!APPLY) { console.log("\ndry run — pass --apply to write"); process.exit(0); }
|
|
127
137
|
|
|
128
138
|
// ─── write phase (--apply) ─────────────────────────────────────────────────────
|
package/hooks/common.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const DEF = {
|
|
|
26
26
|
requestTimeout: 15,
|
|
27
27
|
recentCount: 8,
|
|
28
28
|
searchCount: 8,
|
|
29
|
+
globalCount: 3,
|
|
29
30
|
maxContent: 3000,
|
|
30
31
|
maxQueryLength: 200,
|
|
31
32
|
logDir: join(HOME, ".config/cerebro/logs"),
|
|
@@ -75,6 +76,7 @@ function loadConfig() {
|
|
|
75
76
|
requestTimeout: num("MEM_REQUEST_TIMEOUT", c.requestTimeoutMs ? c.requestTimeoutMs / 1000 : null, DEF.requestTimeout),
|
|
76
77
|
recentCount: num("MEM_RECENT_COUNT", i.recentCount, DEF.recentCount),
|
|
77
78
|
searchCount: num("MEM_SEARCH_COUNT", i.searchCount, DEF.searchCount),
|
|
79
|
+
globalCount: num("MEM_GLOBAL_COUNT", i.globalCount, DEF.globalCount),
|
|
78
80
|
maxContent: num("MEM_MAX_CONTENT", ct.maxContentLength || ct.maxContentChars, DEF.maxContent),
|
|
79
81
|
maxQueryLength: num("MEM_MAX_QUERY_LENGTH", ct.maxQueryLength, DEF.maxQueryLength),
|
|
80
82
|
logDir: (process.env.MEM_LOG_DIR || lg.logDir || DEF.logDir).replace(/^~/, HOME),
|
|
@@ -499,7 +501,7 @@ export async function flushSessionIngest(transcriptPath, sessionId, timeoutSec =
|
|
|
499
501
|
const agentId = process.env.OMEM_AGENT_ID || "claude-code";
|
|
500
502
|
|
|
501
503
|
if (messages.length > 0) {
|
|
502
|
-
const body = { messages, agent_id: agentId };
|
|
504
|
+
const body = { messages, agent_id: agentId, home_path: HOME };
|
|
503
505
|
if (sessionId) body.session_id = sessionId;
|
|
504
506
|
if (pn) body.project_name = pn;
|
|
505
507
|
if (pp) body.project_path = pp;
|
|
@@ -570,12 +572,13 @@ export function truncateAtBoundary(text, maxLength) {
|
|
|
570
572
|
}
|
|
571
573
|
|
|
572
574
|
// GET /v1/memories/search — 单路语义搜索
|
|
573
|
-
export async function searchMemories(query, limit, projectPath) {
|
|
575
|
+
export async function searchMemories(query, limit, projectPath, excludeGlobal) {
|
|
574
576
|
limit = limit || config.searchCount;
|
|
575
577
|
const safeQ = truncateQuery(query);
|
|
576
578
|
if (!safeQ) return [];
|
|
577
579
|
const params = new URLSearchParams({ q: safeQ, limit: String(limit) });
|
|
578
580
|
if (projectPath) params.set("project_path", projectPath);
|
|
581
|
+
if (excludeGlobal) params.set("exclude_global", "1");
|
|
579
582
|
try {
|
|
580
583
|
const resp = await fetch(`${config.apiUrl}/v1/memories/search?${params}`, {
|
|
581
584
|
headers: { "X-API-Key": config.apiKey, Accept: "application/json" },
|
|
@@ -589,18 +592,23 @@ export async function searchMemories(query, limit, projectPath) {
|
|
|
589
592
|
}
|
|
590
593
|
|
|
591
594
|
// buildMemoryInjection — 对标 opencode hooks.ts:246-329
|
|
592
|
-
//
|
|
595
|
+
// 四路并发:profile + global + recent + search(query)。query 为空跳过 search。
|
|
596
|
+
// 拍板(issue #3):项目 recent/search 路带 exclude_global(专区已单列,项目路不混全局);
|
|
597
|
+
// 全局专区 globalCount 条(SessionStart q 为空,走 list 按时间取最新全局记忆)。
|
|
593
598
|
export async function buildMemoryInjection(query, projectPath, options = {}) {
|
|
594
599
|
const profileEnabled = options.profileEnabled !== false;
|
|
595
600
|
const recentEnabled = options.recentEnabled !== false;
|
|
601
|
+
const globalEnabled = options.globalEnabled !== false;
|
|
596
602
|
const hdrs = { "X-API-Key": config.apiKey, Accept: "application/json" };
|
|
597
603
|
const recentCount = config.recentCount;
|
|
598
604
|
const searchCount = config.searchCount;
|
|
605
|
+
const globalCount = config.globalCount;
|
|
599
606
|
const profileQs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
|
|
600
|
-
const recentQs = `?limit=${recentCount}&offset=0&sort=updated_at&order=desc${projectPath ? `&project_path=${encodeURIComponent(projectPath)}` : ""}`;
|
|
607
|
+
const recentQs = `?limit=${recentCount}&offset=0&sort=updated_at&order=desc&exclude_global=1${projectPath ? `&project_path=${encodeURIComponent(projectPath)}` : ""}`;
|
|
608
|
+
const globalQs = `?limit=${globalCount}&offset=0&sort=updated_at&order=desc&global_only=1`;
|
|
601
609
|
const safeQ = truncateQuery(query);
|
|
602
610
|
|
|
603
|
-
const [profileResp, recentResp, searchResp] = await Promise.all([
|
|
611
|
+
const [profileResp, recentResp, globalResp, searchResp] = await Promise.all([
|
|
604
612
|
profileEnabled
|
|
605
613
|
? fetch(`${config.apiUrl}/v2/profile/inject${profileQs}`, { headers: hdrs, signal: AbortSignal.timeout(config.profileTimeoutMs) })
|
|
606
614
|
.then((r) => r.text()).catch(() => "")
|
|
@@ -609,8 +617,12 @@ export async function buildMemoryInjection(query, projectPath, options = {}) {
|
|
|
609
617
|
? fetch(`${config.apiUrl}/v1/memories${recentQs}`, { headers: hdrs, signal: AbortSignal.timeout(config.recentTimeoutMs) })
|
|
610
618
|
.then((r) => (r.ok ? r.text() : null)).catch(() => null)
|
|
611
619
|
: Promise.resolve(""),
|
|
620
|
+
globalEnabled
|
|
621
|
+
? fetch(`${config.apiUrl}/v1/memories${globalQs}`, { headers: hdrs, signal: AbortSignal.timeout(config.recentTimeoutMs) })
|
|
622
|
+
.then((r) => (r.ok ? r.text() : null)).catch(() => null)
|
|
623
|
+
: Promise.resolve(""),
|
|
612
624
|
safeQ
|
|
613
|
-
? fetch(`${config.apiUrl}/v1/memories/search?q=${encodeURIComponent(safeQ)}&limit=${searchCount}${projectPath ? `&project_path=${encodeURIComponent(projectPath)}` : ""}`, { headers: hdrs, signal: AbortSignal.timeout(5000) })
|
|
625
|
+
? fetch(`${config.apiUrl}/v1/memories/search?q=${encodeURIComponent(safeQ)}&limit=${searchCount}&exclude_global=1${projectPath ? `&project_path=${encodeURIComponent(projectPath)}` : ""}`, { headers: hdrs, signal: AbortSignal.timeout(5000) })
|
|
614
626
|
.then((r) => r.text()).catch(() => "")
|
|
615
627
|
: Promise.resolve(""),
|
|
616
628
|
]);
|
|
@@ -634,6 +646,13 @@ export async function buildMemoryInjection(query, projectPath, options = {}) {
|
|
|
634
646
|
} catch {}
|
|
635
647
|
}
|
|
636
648
|
|
|
649
|
+
// parse global (最新全局记忆,跨项目)
|
|
650
|
+
let globalMemories = [];
|
|
651
|
+
try {
|
|
652
|
+
const gd = JSON.parse(globalResp);
|
|
653
|
+
if (gd && !gd.error) globalMemories = gd.memories || [];
|
|
654
|
+
} catch {}
|
|
655
|
+
|
|
637
656
|
// parse search
|
|
638
657
|
let searchResults = [];
|
|
639
658
|
try {
|
|
@@ -650,6 +669,16 @@ export async function buildMemoryInjection(query, projectPath, options = {}) {
|
|
|
650
669
|
}
|
|
651
670
|
|
|
652
671
|
const seenIds = new Set();
|
|
672
|
+
if (globalMemories.length > 0) {
|
|
673
|
+
sections.push("## Global Memories");
|
|
674
|
+
for (const m of globalMemories) {
|
|
675
|
+
if (m.id) seenIds.add(m.id);
|
|
676
|
+
const age = formatRelativeAge(m.updated_at || m.created_at);
|
|
677
|
+
sections.push(`- (${age}) ${m.content || ""}`);
|
|
678
|
+
}
|
|
679
|
+
sections.push("");
|
|
680
|
+
}
|
|
681
|
+
|
|
653
682
|
if (projectMemories.length > 0) {
|
|
654
683
|
sections.push("## Recent Project Activity");
|
|
655
684
|
for (const m of projectMemories) {
|
|
@@ -682,6 +711,7 @@ export async function buildMemoryInjection(query, projectPath, options = {}) {
|
|
|
682
711
|
return {
|
|
683
712
|
text,
|
|
684
713
|
profileCount: profileContent ? 1 : 0,
|
|
714
|
+
globalCount: globalMemories.length,
|
|
685
715
|
projectMemoryCount: projectMemories.length,
|
|
686
716
|
searchCount: dedupedResults.length,
|
|
687
717
|
recentFailed,
|
package/hooks/common.sh
CHANGED
|
File without changes
|
package/hooks/dream.mjs
CHANGED
|
File without changes
|
package/hooks/flush-detached.mjs
CHANGED
|
File without changes
|
package/hooks/hooks.json
CHANGED
|
File without changes
|
package/hooks/post-compact.mjs
CHANGED
|
File without changes
|
package/hooks/pre-compact.mjs
CHANGED
|
File without changes
|
package/hooks/recall-approve.mjs
CHANGED
|
File without changes
|
package/hooks/session-end.mjs
CHANGED
|
File without changes
|
package/hooks/session-start.mjs
CHANGED
|
@@ -158,9 +158,9 @@ if (startSource === "clear") {
|
|
|
158
158
|
await postRecallEvent({
|
|
159
159
|
sessionId: sid,
|
|
160
160
|
recallType: "session_start",
|
|
161
|
-
queryText: `Session Start · ${injection.projectMemoryCount}
|
|
161
|
+
queryText: `Session Start · ${injection.globalCount} global · ${injection.projectMemoryCount} project · ${injection.profileCount > 0 ? "profile" : "no profile"}`,
|
|
162
162
|
profileInjected: injection.profileCount > 0,
|
|
163
|
-
keptCount: injection.projectMemoryCount,
|
|
163
|
+
keptCount: injection.globalCount + injection.projectMemoryCount,
|
|
164
164
|
injectedContent: out,
|
|
165
165
|
failureReason: injection.recentFailed ? "recent fetch failed/timeout" : "",
|
|
166
166
|
});
|
package/hooks/stop.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/package.json
CHANGED
|
File without changes
|
package/scripts/memory-save.sh
CHANGED
|
File without changes
|
package/scripts/memory-search.sh
CHANGED
|
File without changes
|
package/scripts/web-server.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/skills/dream/SKILL.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/tests/common.test.mjs
CHANGED
|
File without changes
|
package/tests/hooks.test.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
6
8
|
|
|
7
9
|
const HOOKS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "hooks");
|
|
8
10
|
|
|
@@ -109,3 +111,38 @@ describe("session-end.mjs", () => {
|
|
|
109
111
|
assert.deepEqual(out, {});
|
|
110
112
|
});
|
|
111
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("apply-dream.mjs", () => {
|
|
116
|
+
test("added onto existing local file is a conflict, not an overwrite", async () => {
|
|
117
|
+
const tmp = mkdtempSync(join(tmpdir(), "apply-dream-"));
|
|
118
|
+
try {
|
|
119
|
+
const memDir = join(tmp, "memory");
|
|
120
|
+
mkdirSync(memDir);
|
|
121
|
+
const existing = "---\nname: cc-x\ndescription: hand-written\nmetadata:\n type: feedback\n---\n\nprecise body\n";
|
|
122
|
+
writeFileSync(join(memDir, "cc-x.md"), existing);
|
|
123
|
+
writeFileSync(join(memDir, "MEMORY.md"), "- [cc-x](cc-x.md) — hand-written\n");
|
|
124
|
+
const outDir = join(tmp, "dream", "output");
|
|
125
|
+
mkdirSync(outDir, { recursive: true });
|
|
126
|
+
writeFileSync(join(outDir, "20260820.json"), JSON.stringify({ entries: [
|
|
127
|
+
{ name: "cc-x", action: "added", body: "condensed stub rewrite" }, // mislabeled rewrite
|
|
128
|
+
{ name: "cc-new", action: "added", body: "genuinely new" }, // real addition
|
|
129
|
+
] }));
|
|
130
|
+
|
|
131
|
+
const res = await new Promise((resolve) => {
|
|
132
|
+
const child = spawn(process.execPath, [join(HOOKS_DIR, "apply-dream.mjs"), "--apply"], {
|
|
133
|
+
env: { ...process.env, OMEM_DREAM_DIR: join(tmp, "dream"), OMEM_DREAM_MEMORY_DIR: memDir },
|
|
134
|
+
});
|
|
135
|
+
let stdout = "";
|
|
136
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
137
|
+
child.on("close", (code) => resolve({ stdout, code }));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.ok(res.stdout.includes("conflict 1"), `expected one conflict, got: ${res.stdout}`);
|
|
141
|
+
assert.ok(res.stdout.includes("! cc-x"), "conflicting name must be listed");
|
|
142
|
+
assert.equal(readFileSync(join(memDir, "cc-x.md"), "utf8"), existing, "existing file must stay untouched");
|
|
143
|
+
assert.ok(readFileSync(join(memDir, "cc-new.md"), "utf8").includes("genuinely new"), "genuinely new entry must be written");
|
|
144
|
+
} finally {
|
|
145
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
package/tests/test_smoke.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/web/favicon.svg
CHANGED
|
File without changes
|
package/web/icons.svg
CHANGED
|
File without changes
|
package/web/index.html
CHANGED
|
File without changes
|