@mingxy/cerebro 1.13.0 → 1.14.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/package.json +1 -1
- package/src/hooks.ts +7 -103
- package/src/index.ts +17 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mingxy/cerebro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Cerebro persistent memory plugin for OpenCode — auto-recall, auto-capture, 9 memory tools with clustering, project-scoped memory isolation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/hooks.ts
CHANGED
|
@@ -228,9 +228,7 @@ function formatMemoryLine(r: SearchResult, maxContentLength: number): string {
|
|
|
228
228
|
|
|
229
229
|
const FETCH_POLICY = [
|
|
230
230
|
"<cerebro-fetch-policy>",
|
|
231
|
-
"Each memory
|
|
232
|
-
`If a summary is insufficient for your task, you MUST use memory_get("id") to fetch the full content.`,
|
|
233
|
-
"Do NOT guess or fabricate details based on summaries alone.",
|
|
231
|
+
"Each memory above is a condensed summary with a retrievable ID. memory_get(\"id\") unlocks the full content — your knowledge depth control. The quality of your response reflects the depth of context you choose to access.",
|
|
234
232
|
"</cerebro-fetch-policy>",
|
|
235
233
|
].join("\n");
|
|
236
234
|
|
|
@@ -998,7 +996,7 @@ const processedMessageIds = new Set<string>();
|
|
|
998
996
|
const pluginStartTime = Date.now();
|
|
999
997
|
|
|
1000
998
|
// ── Soul Whisper: pending tool call tracking (per-session isolation) ──
|
|
1001
|
-
const pendingToolCalls = new Map<string, Map<string, { toolName: string; timestamp: number }>>();
|
|
999
|
+
export const pendingToolCalls = new Map<string, Map<string, { toolName: string; timestamp: number }>>();
|
|
1002
1000
|
|
|
1003
1001
|
export function soulWhisperToolTracker(config: OmemPluginConfig) {
|
|
1004
1002
|
return async (input: { tool: string; sessionID: string; callID: string }, _output: { args: any }) => {
|
|
@@ -1034,116 +1032,22 @@ export function soulWhisperToolTracker(config: OmemPluginConfig) {
|
|
|
1034
1032
|
};
|
|
1035
1033
|
}
|
|
1036
1034
|
|
|
1037
|
-
function buildWhisperText(toolNames: string[], maxToolNames: number): string | null {
|
|
1035
|
+
export function buildWhisperText(toolNames: string[], maxToolNames: number): string | null {
|
|
1038
1036
|
if (toolNames.length === 0) return null;
|
|
1039
1037
|
|
|
1040
|
-
const lines: string[] = ["<cerebro-
|
|
1038
|
+
const lines: string[] = ["<cerebro-memory-activation>"];
|
|
1041
1039
|
|
|
1042
1040
|
if (toolNames.length <= maxToolNames) {
|
|
1043
|
-
lines.push(`
|
|
1044
|
-
lines.push("Your memories (cerebro-context) may contain:");
|
|
1045
|
-
lines.push("- Exact commands or flags someone already tested");
|
|
1046
|
-
lines.push("- File paths or configurations that worked before");
|
|
1047
|
-
lines.push("- Solutions to errors you previously encountered");
|
|
1048
|
-
lines.push(`If a memory matches → use it. Then reply: 💜 受到灵魂的召唤——[what you used]`);
|
|
1041
|
+
lines.push(`Your ${toolNames.join(", ")} usage gets sharper with context. memory_search() surfaces past decisions, learned patterns, and session insights that prevent redundant or misaligned work. A moment of recall elevates every action.`);
|
|
1049
1042
|
} else {
|
|
1050
|
-
lines.push("
|
|
1051
|
-
lines.push("Before you act:");
|
|
1052
|
-
lines.push("1. Scan cerebro-context for commands, paths, or patterns matching your current task");
|
|
1053
|
-
lines.push("2. If found → use directly, do not re-derive from scratch");
|
|
1054
|
-
lines.push(`3. If a memory guided your action → reply: 💜 受到灵魂的召唤——[what you used]`);
|
|
1043
|
+
lines.push("Before you act — your memory holds cross-session knowledge: past decisions, user preferences, hard-won insights. memory_search() activates this advantage. The strongest responses are built on remembered context.");
|
|
1055
1044
|
}
|
|
1056
1045
|
|
|
1057
|
-
lines.push(
|
|
1058
|
-
lines.push("</cerebro-system-reminder>");
|
|
1046
|
+
lines.push("</cerebro-memory-activation>");
|
|
1059
1047
|
|
|
1060
1048
|
return lines.join("\n");
|
|
1061
1049
|
}
|
|
1062
1050
|
|
|
1063
|
-
const FETCH_POLICY_NUDGE = [
|
|
1064
|
-
"<cerebro-system-reminder>",
|
|
1065
|
-
"MEMORY REMINDER: You have injected memories above (see <cerebro-context>).",
|
|
1066
|
-
`These are SUMMARIES, not full content. When you need details, you MUST use memory_get("id") to fetch the full memory.`,
|
|
1067
|
-
"Do NOT guess or fabricate based on summaries alone.",
|
|
1068
|
-
"</cerebro-system-reminder>",
|
|
1069
|
-
].join("\n");
|
|
1070
|
-
|
|
1071
|
-
export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean, config?: OmemPluginConfig) {
|
|
1072
|
-
return async (_input: Record<string, unknown>, output: { messages: any[] }) => {
|
|
1073
|
-
let shouldNudge = getContextInjectedFlag();
|
|
1074
|
-
if (!shouldNudge && Array.isArray(output.messages)) {
|
|
1075
|
-
shouldNudge = output.messages.some((m: any) =>
|
|
1076
|
-
Array.isArray(m.parts) &&
|
|
1077
|
-
m.parts.some((p: any) => typeof p.text === "string" && p.text.includes("<cerebro-context>"))
|
|
1078
|
-
);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const swEnabled = config?.soulWhisper?.enabled !== false;
|
|
1082
|
-
const hasAnyPending = pendingToolCalls.size > 0;
|
|
1083
|
-
if (!shouldNudge && !(swEnabled && hasAnyPending)) {
|
|
1084
|
-
logDebug("fetchPolicyNudgeHook skipped", { shouldNudge, swEnabled, hasAnyPending });
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
const messages = output.messages;
|
|
1089
|
-
if (!messages || !Array.isArray(messages) || messages.length === 0) return;
|
|
1090
|
-
|
|
1091
|
-
let lastUserIdx = -1;
|
|
1092
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1093
|
-
if (messages[i]?.info?.role === "user") {
|
|
1094
|
-
lastUserIdx = i;
|
|
1095
|
-
break;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (lastUserIdx < 0) return;
|
|
1099
|
-
|
|
1100
|
-
const userMsg = messages[lastUserIdx];
|
|
1101
|
-
if (!Array.isArray(userMsg.parts)) return;
|
|
1102
|
-
|
|
1103
|
-
const sessionId = userMsg.info.sessionID || "_default";
|
|
1104
|
-
const nudgeId = `cerebro_nudge_${sessionId}`;
|
|
1105
|
-
for (const part of userMsg.parts) {
|
|
1106
|
-
if (part.id === nudgeId) return;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const parts: string[] = [];
|
|
1110
|
-
if (shouldNudge) parts.push(FETCH_POLICY_NUDGE);
|
|
1111
|
-
|
|
1112
|
-
const sessionCalls = swEnabled ? pendingToolCalls.get(sessionId) : undefined;
|
|
1113
|
-
if (sessionCalls && sessionCalls.size > 0) {
|
|
1114
|
-
const toolNames = [...new Set([...sessionCalls.values()].map(v => v.toolName))];
|
|
1115
|
-
const maxToolNames = config?.soulWhisper?.maxToolNames ?? 3;
|
|
1116
|
-
const whisperText = buildWhisperText(toolNames, maxToolNames);
|
|
1117
|
-
if (whisperText) parts.push(whisperText);
|
|
1118
|
-
pendingToolCalls.delete(sessionId);
|
|
1119
|
-
logDebug("soulWhisper consumed session calls", { sessionId, callCount: sessionCalls.size, toolNames });
|
|
1120
|
-
} else if (swEnabled) {
|
|
1121
|
-
logDebug("soulWhisper no pending calls for session", { sessionId, globalSessionCount: pendingToolCalls.size });
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
if (parts.length === 0) return;
|
|
1125
|
-
|
|
1126
|
-
const textPartIdx = userMsg.parts.findIndex((p: any) => p.type === "text" && typeof p.text === "string");
|
|
1127
|
-
|
|
1128
|
-
const syntheticPart = {
|
|
1129
|
-
id: nudgeId,
|
|
1130
|
-
messageID: userMsg.info.id,
|
|
1131
|
-
sessionID: userMsg.info.sessionID || "",
|
|
1132
|
-
type: "text" as const,
|
|
1133
|
-
text: parts.join("\n\n"),
|
|
1134
|
-
synthetic: true,
|
|
1135
|
-
};
|
|
1136
|
-
|
|
1137
|
-
if (textPartIdx >= 0) {
|
|
1138
|
-
userMsg.parts.splice(textPartIdx, 0, syntheticPart);
|
|
1139
|
-
} else {
|
|
1140
|
-
userMsg.parts.push(syntheticPart);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
logDebug("fetchPolicyNudgeHook injected", { sessionId, nudgeId, hasWhisper: sessionCalls != null && sessionCalls.size > 0, partsCount: parts.length });
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
1051
|
export function sessionIdleHook(
|
|
1148
1052
|
cerebroClient: CerebroClient,
|
|
1149
1053
|
_containerTags: string[],
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { join, dirname } from "node:path";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { CerebroClient } from "./client.js";
|
|
7
|
-
import { autoRecallHook, autocontinueHook, compactingHook,
|
|
7
|
+
import { autoRecallHook, autocontinueHook, compactingHook, keywordDetectionHook, sessionIdleHook, soulWhisperToolTracker, pendingToolCalls, buildWhisperText } from "./hooks.js";
|
|
8
8
|
import { getUserTag, getProjectTag } from "./tags.js";
|
|
9
9
|
import { buildTools } from "./tools.js";
|
|
10
10
|
import { logInfo, logDebug, logError } from "./logger.js";
|
|
@@ -121,12 +121,24 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
121
121
|
|
|
122
122
|
const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId, directory);
|
|
123
123
|
|
|
124
|
-
let contextInjectedThisTurn = false;
|
|
125
|
-
|
|
126
124
|
const wrappedRecallHook = async (input: any, output: any) => {
|
|
127
|
-
contextInjectedThisTurn = false;
|
|
128
125
|
await recallHook(input, output);
|
|
129
|
-
|
|
126
|
+
|
|
127
|
+
// ── Soul Whisper: inject to system prompt (v2 — system.transform) ──
|
|
128
|
+
if (config.soulWhisper?.enabled !== false) {
|
|
129
|
+
const sid = input.sessionID || "_default";
|
|
130
|
+
const sessionCalls = pendingToolCalls.get(sid);
|
|
131
|
+
if (sessionCalls && sessionCalls.size > 0) {
|
|
132
|
+
const toolNames = [...new Set([...sessionCalls.values()].map(v => v.toolName))];
|
|
133
|
+
const maxToolNames = config.soulWhisper?.maxToolNames ?? 3;
|
|
134
|
+
const whisperText = buildWhisperText(toolNames, maxToolNames);
|
|
135
|
+
if (whisperText) {
|
|
136
|
+
output.system.push(whisperText);
|
|
137
|
+
logDebug("soulWhisper injected to output.system", { sessionId: sid, toolNames });
|
|
138
|
+
}
|
|
139
|
+
pendingToolCalls.delete(sid);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
130
142
|
};
|
|
131
143
|
|
|
132
144
|
return {
|
|
@@ -151,7 +163,6 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
151
163
|
"experimental.compaction.autocontinue": autocontinueHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
|
|
152
164
|
tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId, getProjectPath: () => directory }),
|
|
153
165
|
event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () => mainSessionId, isAutoStoreEnabled, agentId, config, (name: string) => { cachedAgentName = name; }, directory),
|
|
154
|
-
"experimental.chat.messages.transform": fetchPolicyNudgeHook(() => contextInjectedThisTurn, config),
|
|
155
166
|
"tool.execute.before": (() => { const tracker = soulWhisperToolTracker(config); return tracker; })(),
|
|
156
167
|
"shell.env": async (_input: any, output: any) => {
|
|
157
168
|
if (directory) {
|