@mingxy/cerebro 1.8.3 → 1.10.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/dist/client.d.ts +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -2
- package/dist/client.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +135 -5
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -25
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +2 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +32 -9
- package/dist/tools.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +20 -4
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +386 -380
- package/src/hooks.ts +575 -453
- package/src/index.ts +151 -151
- package/src/tools.ts +35 -7
package/src/index.ts
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, dirname } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { OmemClient } from "./client.js";
|
|
7
|
-
import { autoRecallHook, compactingHook, keywordDetectionHook, sessionIdleHook } from "./hooks.js";
|
|
8
|
-
import { getUserTag, getProjectTag } from "./tags.js";
|
|
9
|
-
import { buildTools } from "./tools.js";
|
|
10
|
-
import { logInfo, logError } from "./logger.js";
|
|
11
|
-
import { loadPluginConfig } from "./config.js";
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
|
|
16
|
-
let pluginVersion = "unknown";
|
|
17
|
-
try {
|
|
18
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
19
|
-
if (pkg?.version && typeof pkg.version === "string") {
|
|
20
|
-
pluginVersion = pkg.version;
|
|
21
|
-
}
|
|
22
|
-
} catch {}
|
|
23
|
-
|
|
24
|
-
// Per-session auto-store toggle: sessionId → enabled (default: true = auto-store on)
|
|
25
|
-
const autoStoreSessions = new Map<string, boolean>();
|
|
26
|
-
|
|
27
|
-
function getStateFilePath(sessionId: string): string {
|
|
28
|
-
return join(tmpdir(), `cerebro_autostore_${sessionId}.json`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function isAutoStoreEnabled(sessionId: string | undefined): boolean {
|
|
32
|
-
if (!sessionId) return true;
|
|
33
|
-
return autoStoreSessions.get(sessionId) ?? true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function setAutoStoreEnabled(sessionId: string, enabled: boolean): void {
|
|
37
|
-
autoStoreSessions.set(sessionId, enabled);
|
|
38
|
-
try {
|
|
39
|
-
writeFileSync(getStateFilePath(sessionId), JSON.stringify({ enabled }));
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
(globalThis as any).__cerebro_autoStoreMap = autoStoreSessions;
|
|
44
|
-
|
|
45
|
-
function showToast(tui: any, title: string, message?: string, variant: string = "info", duration: number = 5000) {
|
|
46
|
-
if (!tui) return;
|
|
47
|
-
setTimeout(() => {
|
|
48
|
-
try {
|
|
49
|
-
const body: any = { variant, duration };
|
|
50
|
-
if (message) {
|
|
51
|
-
body.title = title;
|
|
52
|
-
body.message = message;
|
|
53
|
-
} else {
|
|
54
|
-
body.message = title;
|
|
55
|
-
}
|
|
56
|
-
tui.showToast({ body });
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.error("[cerebro] showToast failed:", err);
|
|
59
|
-
}
|
|
60
|
-
}, 3000);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const OmemPlugin: Plugin = async (input) => {
|
|
64
|
-
const { directory, client } = input;
|
|
65
|
-
// Proxy: dynamically resolve client.tui on each access so toast works
|
|
66
|
-
// even if client.tui isn't ready yet at plugin init time
|
|
67
|
-
const tui = new Proxy({} as any, {
|
|
68
|
-
get(_, prop) {
|
|
69
|
-
return (client as any)?.tui?.[prop];
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Load overrides from opencode.json plugin_config
|
|
74
|
-
let overrides: Record<string, unknown> = {};
|
|
75
|
-
try {
|
|
76
|
-
const ocCfg = JSON.parse(readFileSync(join(directory, "opencode.json"), "utf-8"));
|
|
77
|
-
const pc = ocCfg?.plugin_config?.["@mingxy/omem"] || ocCfg?.plugin_config?.["@ourmem/opencode"];
|
|
78
|
-
if (pc) overrides = pc;
|
|
79
|
-
} catch {}
|
|
80
|
-
|
|
81
|
-
const config = loadPluginConfig(overrides as any);
|
|
82
|
-
|
|
83
|
-
const omemClient = new OmemClient(config.apiUrl, config.apiKey, config);
|
|
84
|
-
|
|
85
|
-
// 启动时检测连接状态
|
|
86
|
-
try {
|
|
87
|
-
await omemClient.getStats();
|
|
88
|
-
showToast(tui, "🧠 Cerebro · Connected", `Version v${pluginVersion}`, "success", 6000);
|
|
89
|
-
logInfo(`Connected to ${config.apiUrl}`);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
92
|
-
logError(`Connection failed: ${errMsg}`);
|
|
93
|
-
if (errMsg.includes("[omem]")) {
|
|
94
|
-
const cleanMsg = errMsg.replace(/^\[omem\]\s*/, "");
|
|
95
|
-
showToast(
|
|
96
|
-
tui,
|
|
97
|
-
`🧠 Cerebro v${pluginVersion} · Server Error`,
|
|
98
|
-
cleanMsg.substring(0, 150),
|
|
99
|
-
"error",
|
|
100
|
-
8000
|
|
101
|
-
);
|
|
102
|
-
} else {
|
|
103
|
-
showToast(
|
|
104
|
-
tui,
|
|
105
|
-
`🧠 Cerebro v${pluginVersion} · Connection Failed`,
|
|
106
|
-
`Unable to reach ${config.apiUrl}`,
|
|
107
|
-
"error",
|
|
108
|
-
8000
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const email = process.env.GIT_AUTHOR_EMAIL || process.env.USER || "unknown";
|
|
114
|
-
const cwd = directory || process.cwd();
|
|
115
|
-
const containerTags = [getUserTag(email), getProjectTag(cwd)];
|
|
116
|
-
const agentId = process.env.OMEM_AGENT_ID || "opencode";
|
|
117
|
-
|
|
118
|
-
let currentSessionId: string | undefined;
|
|
119
|
-
|
|
120
|
-
const recallHook = autoRecallHook(omemClient, containerTags, tui, config);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
config: async (cfg: any) => {
|
|
124
|
-
cfg.command ??= {};
|
|
125
|
-
cfg.command["memory-toggle"] = {
|
|
126
|
-
template: "Use the memory_toggle tool with state='$ARGUMENTS' to toggle Cerebro auto-store for this session. You MUST call the memory_toggle tool, do not just acknowledge.",
|
|
127
|
-
description: "Toggle Cerebro auto-store ON or OFF for current session",
|
|
128
|
-
};
|
|
129
|
-
},
|
|
130
|
-
"experimental.chat.system.transform": async (input: any, output: any) => {
|
|
131
|
-
if (input.sessionID) currentSessionId = input.sessionID;
|
|
132
|
-
return recallHook(input, output);
|
|
133
|
-
},
|
|
134
|
-
"chat.message": keywordDetectionHook(omemClient, containerTags, config.autoCaptureThreshold, tui, config.ingestMode),
|
|
135
|
-
"experimental.session.compacting": compactingHook(omemClient, containerTags, tui, config.ingestMode, isAutoStoreEnabled),
|
|
136
|
-
tool: buildTools(omemClient, containerTags, { agentId, getSessionId: () => currentSessionId }),
|
|
137
|
-
event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode, config.autoCaptureThreshold, () => currentSessionId, isAutoStoreEnabled, agentId),
|
|
138
|
-
"shell.env": async (_input: any, output: any) => {
|
|
139
|
-
if (directory) {
|
|
140
|
-
output.env.OMEM_PROJECT_DIR = directory;
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export { OmemPlugin };
|
|
147
|
-
|
|
148
|
-
export default {
|
|
149
|
-
id: "ourmem",
|
|
150
|
-
server: OmemPlugin,
|
|
151
|
-
};
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { OmemClient } from "./client.js";
|
|
7
|
+
import { autoRecallHook, compactingHook, keywordDetectionHook, sessionIdleHook } from "./hooks.js";
|
|
8
|
+
import { getUserTag, getProjectTag } from "./tags.js";
|
|
9
|
+
import { buildTools } from "./tools.js";
|
|
10
|
+
import { logInfo, logError } from "./logger.js";
|
|
11
|
+
import { loadPluginConfig } from "./config.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
let pluginVersion = "unknown";
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
19
|
+
if (pkg?.version && typeof pkg.version === "string") {
|
|
20
|
+
pluginVersion = pkg.version;
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
// Per-session auto-store toggle: sessionId → enabled (default: true = auto-store on)
|
|
25
|
+
const autoStoreSessions = new Map<string, boolean>();
|
|
26
|
+
|
|
27
|
+
function getStateFilePath(sessionId: string): string {
|
|
28
|
+
return join(tmpdir(), `cerebro_autostore_${sessionId}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isAutoStoreEnabled(sessionId: string | undefined): boolean {
|
|
32
|
+
if (!sessionId) return true;
|
|
33
|
+
return autoStoreSessions.get(sessionId) ?? true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setAutoStoreEnabled(sessionId: string, enabled: boolean): void {
|
|
37
|
+
autoStoreSessions.set(sessionId, enabled);
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(getStateFilePath(sessionId), JSON.stringify({ enabled }));
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
(globalThis as any).__cerebro_autoStoreMap = autoStoreSessions;
|
|
44
|
+
|
|
45
|
+
function showToast(tui: any, title: string, message?: string, variant: string = "info", duration: number = 5000) {
|
|
46
|
+
if (!tui) return;
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
try {
|
|
49
|
+
const body: any = { variant, duration };
|
|
50
|
+
if (message) {
|
|
51
|
+
body.title = title;
|
|
52
|
+
body.message = message;
|
|
53
|
+
} else {
|
|
54
|
+
body.message = title;
|
|
55
|
+
}
|
|
56
|
+
tui.showToast({ body });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("[cerebro] showToast failed:", err);
|
|
59
|
+
}
|
|
60
|
+
}, 3000);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const OmemPlugin: Plugin = async (input) => {
|
|
64
|
+
const { directory, client } = input;
|
|
65
|
+
// Proxy: dynamically resolve client.tui on each access so toast works
|
|
66
|
+
// even if client.tui isn't ready yet at plugin init time
|
|
67
|
+
const tui = new Proxy({} as any, {
|
|
68
|
+
get(_, prop) {
|
|
69
|
+
return (client as any)?.tui?.[prop];
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Load overrides from opencode.json plugin_config
|
|
74
|
+
let overrides: Record<string, unknown> = {};
|
|
75
|
+
try {
|
|
76
|
+
const ocCfg = JSON.parse(readFileSync(join(directory, "opencode.json"), "utf-8"));
|
|
77
|
+
const pc = ocCfg?.plugin_config?.["@mingxy/omem"] || ocCfg?.plugin_config?.["@ourmem/opencode"];
|
|
78
|
+
if (pc) overrides = pc;
|
|
79
|
+
} catch {}
|
|
80
|
+
|
|
81
|
+
const config = loadPluginConfig(overrides as any);
|
|
82
|
+
|
|
83
|
+
const omemClient = new OmemClient(config.apiUrl, config.apiKey, config);
|
|
84
|
+
|
|
85
|
+
// 启动时检测连接状态
|
|
86
|
+
try {
|
|
87
|
+
await omemClient.getStats();
|
|
88
|
+
showToast(tui, "🧠 Cerebro · Connected", `Version v${pluginVersion}`, "success", 6000);
|
|
89
|
+
logInfo(`Connected to ${config.apiUrl}`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
logError(`Connection failed: ${errMsg}`);
|
|
93
|
+
if (errMsg.includes("[omem]")) {
|
|
94
|
+
const cleanMsg = errMsg.replace(/^\[omem\]\s*/, "");
|
|
95
|
+
showToast(
|
|
96
|
+
tui,
|
|
97
|
+
`🧠 Cerebro v${pluginVersion} · Server Error`,
|
|
98
|
+
cleanMsg.substring(0, 150),
|
|
99
|
+
"error",
|
|
100
|
+
8000
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
showToast(
|
|
104
|
+
tui,
|
|
105
|
+
`🧠 Cerebro v${pluginVersion} · Connection Failed`,
|
|
106
|
+
`Unable to reach ${config.apiUrl}`,
|
|
107
|
+
"error",
|
|
108
|
+
8000
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const email = process.env.GIT_AUTHOR_EMAIL || process.env.USER || "unknown";
|
|
114
|
+
const cwd = directory || process.cwd();
|
|
115
|
+
const containerTags = [getUserTag(email), getProjectTag(cwd)];
|
|
116
|
+
const agentId = process.env.OMEM_AGENT_ID || "opencode";
|
|
117
|
+
|
|
118
|
+
let currentSessionId: string | undefined;
|
|
119
|
+
|
|
120
|
+
const recallHook = autoRecallHook(omemClient, containerTags, tui, config);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
config: async (cfg: any) => {
|
|
124
|
+
cfg.command ??= {};
|
|
125
|
+
cfg.command["memory-toggle"] = {
|
|
126
|
+
template: "Use the memory_toggle tool with state='$ARGUMENTS' to toggle Cerebro auto-store for this session. You MUST call the memory_toggle tool, do not just acknowledge.",
|
|
127
|
+
description: "Toggle Cerebro auto-store ON or OFF for current session",
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
"experimental.chat.system.transform": async (input: any, output: any) => {
|
|
131
|
+
if (input.sessionID) currentSessionId = input.sessionID;
|
|
132
|
+
return recallHook(input, output);
|
|
133
|
+
},
|
|
134
|
+
"chat.message": keywordDetectionHook(omemClient, containerTags, config.autoCaptureThreshold, tui, config.ingestMode),
|
|
135
|
+
"experimental.session.compacting": compactingHook(omemClient, containerTags, tui, config.ingestMode, isAutoStoreEnabled, () => currentSessionId, client),
|
|
136
|
+
tool: buildTools(omemClient, containerTags, { agentId, getSessionId: () => currentSessionId }),
|
|
137
|
+
event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode, config.autoCaptureThreshold, () => currentSessionId, isAutoStoreEnabled, agentId),
|
|
138
|
+
"shell.env": async (_input: any, output: any) => {
|
|
139
|
+
if (directory) {
|
|
140
|
+
output.env.OMEM_PROJECT_DIR = directory;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export { OmemPlugin };
|
|
147
|
+
|
|
148
|
+
export default {
|
|
149
|
+
id: "ourmem",
|
|
150
|
+
server: OmemPlugin,
|
|
151
|
+
};
|
package/src/tools.ts
CHANGED
|
@@ -32,28 +32,56 @@ export function buildTools(client: OmemClient, containerTags: string[], context:
|
|
|
32
32
|
description:
|
|
33
33
|
"Store a new memory in the user's long-term memory. " +
|
|
34
34
|
"Use when the user explicitly asks to remember something, " +
|
|
35
|
-
"or when you identify important preferences, facts, or decisions worth preserving."
|
|
35
|
+
"or when you identify important preferences, facts, or decisions worth preserving. " +
|
|
36
|
+
"IMPORTANT: Before calling, you MUST analyze: (1) Which category fits best? (2) Is this project-specific or cross-project? (3) Does it contain sensitive data? (4) Are tags accurate and descriptive? " +
|
|
37
|
+
"Every memory MUST have a correct category and at least 1 meaningful tag.",
|
|
36
38
|
args: {
|
|
37
|
-
content: tool.schema.string().describe(
|
|
39
|
+
content: tool.schema.string().describe(
|
|
40
|
+
"The information to remember. MUST be: atomic (one fact per memory), complete (self-contained without context), and precise (no ambiguity). " +
|
|
41
|
+
"BAD: 'fixed some bugs'. GOOD: 'Fixed memory_type validation bug in memory.rs:1480 - LLM returns illegal \"pinned\" value, added match guard to normalize to WORK/EMOTIONAL fallback'."
|
|
42
|
+
),
|
|
38
43
|
tags: tool.schema
|
|
39
44
|
.array(tool.schema.string())
|
|
40
45
|
.optional()
|
|
41
|
-
.describe(
|
|
46
|
+
.describe(
|
|
47
|
+
"REQUIRED. At least 1 tag in snake_case. Tags describe the memory's topic/domain for future retrieval. " +
|
|
48
|
+
"Examples: rust_backend, memory_system, bug_fix, user_preference, project_config. " +
|
|
49
|
+
"NEVER leave empty — if unsure, use a broad tag like the project name or topic area."
|
|
50
|
+
),
|
|
42
51
|
source: tool.schema
|
|
43
52
|
.string()
|
|
44
|
-
.describe("Origin context, e.g. 'conversation', 'code-review', 'user-input'"),
|
|
53
|
+
.describe("Origin context, e.g. 'conversation', 'code-review', 'user-input', 'debugging', 'architecture-decision'"),
|
|
45
54
|
scope: tool.schema
|
|
46
55
|
.string()
|
|
47
56
|
.optional()
|
|
48
|
-
.describe(
|
|
57
|
+
.describe(
|
|
58
|
+
"'project' (default) = only visible in current project context. 'global' = visible across all projects. " +
|
|
59
|
+
"Rule: if the memory applies generally (user preferences, general knowledge, cross-project patterns) use 'global'. " +
|
|
60
|
+
"If it's specific to one project's code/architecture, use 'project'."
|
|
61
|
+
),
|
|
49
62
|
visibility: tool.schema
|
|
50
63
|
.string()
|
|
51
64
|
.optional()
|
|
52
|
-
.describe(
|
|
65
|
+
.describe(
|
|
66
|
+
"'global' (default) = all agents can see and recall this memory. 'private' = ONLY the current agent can see it. " +
|
|
67
|
+
"MUST use 'private' when content contains: passwords, API keys, tokens, database credentials, SSH keys, personal information (phone, email, address), " +
|
|
68
|
+
"internal company details, or anything the user would NOT want other agents to access. " +
|
|
69
|
+
"WARNING: private memories are invisible to ALL other agents — if in doubt, ask the user. " +
|
|
70
|
+
"Do NOT overuse 'private' for normal work notes — default 'global' is correct for most cases."
|
|
71
|
+
),
|
|
53
72
|
category: tool.schema
|
|
54
73
|
.string()
|
|
55
74
|
.optional()
|
|
56
|
-
.describe(
|
|
75
|
+
.describe(
|
|
76
|
+
"MUST be one of (choose the BEST fit): " +
|
|
77
|
+
"'cases' (default) = work records, bug fixes, architecture decisions, implementation notes, meeting conclusions; " +
|
|
78
|
+
"'preferences' = user likes/dislikes, coding style preferences, tool choices (e.g. 'prefers Vim over VSCode'); " +
|
|
79
|
+
"'entities' = projects, tools, people, concepts — defining what something IS (e.g. 'omem-server: Rust memory backend using LanceDB'); " +
|
|
80
|
+
"'events' = time-bound milestones (deployments, releases, incidents); " +
|
|
81
|
+
"'profile' = user identity traits (role, skills, team membership); " +
|
|
82
|
+
"'patterns' = workflows, methodologies, best practices, recurring solutions. " +
|
|
83
|
+
"When in doubt, use 'cases'."
|
|
84
|
+
),
|
|
57
85
|
},
|
|
58
86
|
async execute(args) {
|
|
59
87
|
const allTags = [...containerTags, ...(args.tags ?? [])];
|