@mingxy/cerebro 1.5.11 → 1.6.2
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 +17 -3
- package/src/hooks.ts +28 -21
- package/src/index.ts +45 -12
- package/src/tools.ts +27 -0
- package/src/tui.tsx +72 -0
- package/tsconfig.json +3 -1
package/package.json
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mingxy/cerebro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Cerebro persistent memory plugin for OpenCode — auto-recall, auto-capture, 9 memory tools with clustering",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./tui": {
|
|
13
|
+
"types": "./src/tui.tsx",
|
|
14
|
+
"default": "./src/tui.tsx"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"oc-plugin": [
|
|
8
|
-
"server"
|
|
18
|
+
"server",
|
|
19
|
+
"tui"
|
|
9
20
|
],
|
|
10
21
|
"keywords": [
|
|
11
22
|
"opencode",
|
|
@@ -24,7 +35,10 @@
|
|
|
24
35
|
"directory": "plugins/opencode"
|
|
25
36
|
},
|
|
26
37
|
"dependencies": {
|
|
27
|
-
"@opencode-ai/plugin": "^1.0.162"
|
|
38
|
+
"@opencode-ai/plugin": "^1.0.162",
|
|
39
|
+
"@opentui/core": "^0.1.92",
|
|
40
|
+
"@opentui/solid": "^0.1.92",
|
|
41
|
+
"solid-js": "^1.9.10"
|
|
28
42
|
},
|
|
29
43
|
"devDependencies": {
|
|
30
44
|
"@types/node": "^25.5.0",
|
package/src/hooks.ts
CHANGED
|
@@ -143,7 +143,7 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
143
143
|
const shouldRecallRes = await client.shouldRecall(query_text, last_query_text, input.sessionID, similarityThreshold, maxRecallResults, projectTags.length > 0 ? projectTags : undefined);
|
|
144
144
|
|
|
145
145
|
if (!shouldRecallRes) {
|
|
146
|
-
showToast(tui, "🧠
|
|
146
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -250,14 +250,14 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
250
250
|
// Server returned error (500, etc.) with details
|
|
251
251
|
const cleanMsg = errMsg.replace(/^\[omem\]\s*/, "");
|
|
252
252
|
if (cleanMsg.startsWith("500")) {
|
|
253
|
-
showToast(tui, "🧠
|
|
253
|
+
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
254
254
|
} else if (cleanMsg.includes("timed out")) {
|
|
255
|
-
showToast(tui, "🧠
|
|
255
|
+
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
256
256
|
} else {
|
|
257
|
-
showToast(tui, "🧠
|
|
257
|
+
showToast(tui, "🧠 Cerebro Error", cleanMsg.substring(0, 150), "error");
|
|
258
258
|
}
|
|
259
259
|
} else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
260
|
-
showToast(tui, "🧠
|
|
260
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
261
261
|
} else {
|
|
262
262
|
showToast(tui, "🧠 Memory Recall Error", errMsg.substring(0, 100), "error");
|
|
263
263
|
}
|
|
@@ -302,29 +302,33 @@ export function keywordDetectionHook(_client: OmemClient, _containerTags: string
|
|
|
302
302
|
};
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
export function compactingHook(client: OmemClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart") {
|
|
305
|
+
export function compactingHook(client: OmemClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart", isAutoStoreEnabled?: (sessionId: string | undefined) => boolean) {
|
|
306
306
|
return async (
|
|
307
307
|
input: { sessionID?: string },
|
|
308
308
|
output: { context: string[]; prompt?: string },
|
|
309
309
|
) => {
|
|
310
310
|
if (input.sessionID && sessionMessages.has(input.sessionID)) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
311
|
+
if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
|
|
312
|
+
sessionMessages.delete(input.sessionID);
|
|
313
|
+
} else {
|
|
314
|
+
const messages = sessionMessages.get(input.sessionID)!;
|
|
315
|
+
if (messages.length > 0) {
|
|
316
|
+
try {
|
|
317
|
+
const result = await client.ingestMessages(messages, {
|
|
318
|
+
mode: ingestMode,
|
|
319
|
+
tags: [...containerTags, "auto-capture"],
|
|
320
|
+
sessionId: input.sessionID,
|
|
321
|
+
});
|
|
322
|
+
if (result === null) {
|
|
323
|
+
showToast(tui, "🔴 Archive Failed", "Session archive blocked · check spiritual realm status", "error");
|
|
324
|
+
} else {
|
|
325
|
+
showToast(tui, "📦 Session Archived", `${messages.length} residual dialogues archived · merged into the realm`, "success");
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
showToast(tui, "🔴 Archive Failed", "Session archive blocked · spiritual pulse anomaly", "error");
|
|
323
329
|
}
|
|
324
|
-
|
|
325
|
-
showToast(tui, "🔴 Archive Failed", "Session archive blocked · spiritual pulse anomaly", "error");
|
|
330
|
+
sessionMessages.delete(input.sessionID);
|
|
326
331
|
}
|
|
327
|
-
sessionMessages.delete(input.sessionID);
|
|
328
332
|
}
|
|
329
333
|
}
|
|
330
334
|
|
|
@@ -350,6 +354,7 @@ export function sessionIdleHook(
|
|
|
350
354
|
_ingestMode: "smart" | "raw" = "smart",
|
|
351
355
|
threshold: number = 0,
|
|
352
356
|
getMainSessionId?: () => string | undefined,
|
|
357
|
+
isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
|
|
353
358
|
) {
|
|
354
359
|
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
355
360
|
let isCapturing = false;
|
|
@@ -360,6 +365,8 @@ export function sessionIdleHook(
|
|
|
360
365
|
const sessionID = input.event.properties?.sessionID;
|
|
361
366
|
if (!sessionID) return;
|
|
362
367
|
|
|
368
|
+
if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID)) return;
|
|
369
|
+
|
|
363
370
|
if (getMainSessionId) {
|
|
364
371
|
const mainId = getMainSessionId();
|
|
365
372
|
if (mainId && sessionID !== mainId) return;
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,22 @@ try {
|
|
|
20
20
|
}
|
|
21
21
|
} catch {}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Per-session auto-store toggle: sessionId → enabled (default: true = auto-store on)
|
|
24
|
+
const autoStoreSessions = new Map<string, boolean>();
|
|
25
|
+
|
|
26
|
+
export function isAutoStoreEnabled(sessionId: string | undefined): boolean {
|
|
27
|
+
if (!sessionId) return true;
|
|
28
|
+
return autoStoreSessions.get(sessionId) ?? true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setAutoStoreEnabled(sessionId: string, enabled: boolean): void {
|
|
32
|
+
autoStoreSessions.set(sessionId, enabled);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Bridge for TUI plugin (same process, different module graph)
|
|
36
|
+
(globalThis as any).__cerebro_autoStore = isAutoStoreEnabled;
|
|
37
|
+
|
|
38
|
+
function showToast(tui: any, title: string, message?: string, variant: string = "info", duration: number = 5000) {
|
|
24
39
|
if (!tui) return;
|
|
25
40
|
setTimeout(() => {
|
|
26
41
|
try {
|
|
@@ -48,13 +63,7 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
48
63
|
// 启动时检测连接状态
|
|
49
64
|
try {
|
|
50
65
|
await omemClient.getStats();
|
|
51
|
-
showToast(
|
|
52
|
-
tui,
|
|
53
|
-
`🧠 Omem v${pluginVersion} · Connected`,
|
|
54
|
-
`${config.apiUrl.replace(/^https?:\/\//, "")}`,
|
|
55
|
-
"success",
|
|
56
|
-
6000
|
|
57
|
-
);
|
|
66
|
+
showToast(tui, `🧠 Cerebro v${pluginVersion} · Connected`, undefined, "success", 6000);
|
|
58
67
|
logInfo(`Connected to ${config.apiUrl}`);
|
|
59
68
|
} catch (err) {
|
|
60
69
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -63,7 +72,7 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
63
72
|
const cleanMsg = errMsg.replace(/^\[omem\]\s*/, "");
|
|
64
73
|
showToast(
|
|
65
74
|
tui,
|
|
66
|
-
`🧠
|
|
75
|
+
`🧠 Cerebro v${pluginVersion} · Server Error`,
|
|
67
76
|
cleanMsg.substring(0, 150),
|
|
68
77
|
"error",
|
|
69
78
|
8000
|
|
@@ -71,7 +80,7 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
71
80
|
} else {
|
|
72
81
|
showToast(
|
|
73
82
|
tui,
|
|
74
|
-
`🧠
|
|
83
|
+
`🧠 Cerebro v${pluginVersion} · Connection Failed`,
|
|
75
84
|
`Unable to reach ${config.apiUrl}`,
|
|
76
85
|
"error",
|
|
77
86
|
8000
|
|
@@ -89,14 +98,36 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
89
98
|
const recallHook = autoRecallHook(omemClient, containerTags, tui, config);
|
|
90
99
|
|
|
91
100
|
return {
|
|
101
|
+
config: async (cfg: any) => {
|
|
102
|
+
cfg.command ??= {};
|
|
103
|
+
cfg.command["memory-toggle"] = {
|
|
104
|
+
template: "/memory-toggle <on|off>",
|
|
105
|
+
description: "Toggle Cerebro auto-store ON or OFF for current session",
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
"command.execute.before": async (input: { command: string; sessionID: string; arguments: string }, output: { parts: any[] }) => {
|
|
109
|
+
if (input.command !== "memory-toggle") return;
|
|
110
|
+
const arg = input.arguments.trim().toLowerCase();
|
|
111
|
+
const sessionId = input.sessionID;
|
|
112
|
+
if (arg === "off") {
|
|
113
|
+
setAutoStoreEnabled(sessionId, false);
|
|
114
|
+
output.parts = [{ type: "text", text: "⏸️ Cerebro auto-store: OFF — manual memory_store still works" }];
|
|
115
|
+
} else if (arg === "on") {
|
|
116
|
+
setAutoStoreEnabled(sessionId, true);
|
|
117
|
+
output.parts = [{ type: "text", text: "✅ Cerebro auto-store: ON" }];
|
|
118
|
+
} else {
|
|
119
|
+
const current = isAutoStoreEnabled(sessionId);
|
|
120
|
+
output.parts = [{ type: "text", text: `Cerebro auto-store: ${current ? "✅ ON" : "⏸️ OFF"}\nUsage: /memory-toggle on | off` }];
|
|
121
|
+
}
|
|
122
|
+
},
|
|
92
123
|
"experimental.chat.system.transform": async (input: any, output: any) => {
|
|
93
124
|
if (input.sessionID) currentSessionId = input.sessionID;
|
|
94
125
|
return recallHook(input, output);
|
|
95
126
|
},
|
|
96
127
|
"chat.message": keywordDetectionHook(omemClient, containerTags, config.autoCaptureThreshold, tui, config.ingestMode),
|
|
97
|
-
"experimental.session.compacting": compactingHook(omemClient, containerTags, tui, config.ingestMode),
|
|
128
|
+
"experimental.session.compacting": compactingHook(omemClient, containerTags, tui, config.ingestMode, isAutoStoreEnabled),
|
|
98
129
|
tool: buildTools(omemClient, containerTags, { agentId, getSessionId: () => currentSessionId }),
|
|
99
|
-
event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode, config.autoCaptureThreshold, () => currentSessionId),
|
|
130
|
+
event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode, config.autoCaptureThreshold, () => currentSessionId, isAutoStoreEnabled),
|
|
100
131
|
"shell.env": async (_input: any, output: any) => {
|
|
101
132
|
if (directory) {
|
|
102
133
|
output.env.OMEM_PROJECT_DIR = directory;
|
|
@@ -107,6 +138,8 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
107
138
|
|
|
108
139
|
export { OmemPlugin };
|
|
109
140
|
|
|
141
|
+
export { default as tui } from "./tui.js";
|
|
142
|
+
|
|
110
143
|
export default {
|
|
111
144
|
id: "ourmem",
|
|
112
145
|
server: OmemPlugin,
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import type { OmemClient } from "./client.js";
|
|
3
|
+
import { isAutoStoreEnabled, setAutoStoreEnabled } from "./index.js";
|
|
3
4
|
|
|
4
5
|
function extractMemoryIds(result: unknown): string[] {
|
|
5
6
|
if (!result) return [];
|
|
@@ -373,5 +374,31 @@ export function buildTools(client: OmemClient, containerTags: string[], context:
|
|
|
373
374
|
return JSON.stringify({ ok: true, result });
|
|
374
375
|
},
|
|
375
376
|
}),
|
|
377
|
+
|
|
378
|
+
memory_toggle: tool({
|
|
379
|
+
description:
|
|
380
|
+
"Toggle Cerebro auto-store ON or OFF for current session. Does NOT affect manual memory_store calls.",
|
|
381
|
+
args: {
|
|
382
|
+
state: tool.schema
|
|
383
|
+
.string()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("Set to 'on' or 'off'. Omit to check current status."),
|
|
386
|
+
},
|
|
387
|
+
async execute(args) {
|
|
388
|
+
const sessionId = context.getSessionId();
|
|
389
|
+
if (!sessionId) return JSON.stringify({ ok: false, error: "No active session" });
|
|
390
|
+
|
|
391
|
+
if (args.state === "on") {
|
|
392
|
+
setAutoStoreEnabled(sessionId, true);
|
|
393
|
+
return JSON.stringify({ ok: true, auto_store: true, message: "Cerebro auto-store: ON" });
|
|
394
|
+
} else if (args.state === "off") {
|
|
395
|
+
setAutoStoreEnabled(sessionId, false);
|
|
396
|
+
return JSON.stringify({ ok: true, auto_store: false, message: "Cerebro auto-store: OFF" });
|
|
397
|
+
} else {
|
|
398
|
+
const current = isAutoStoreEnabled(sessionId);
|
|
399
|
+
return JSON.stringify({ ok: true, auto_store: current, message: `Cerebro auto-store: ${current ? "ON" : "OFF"}` });
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
376
403
|
};
|
|
377
404
|
}
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-nocheck — TUI JSX is resolved at runtime by opencode (same as quota plugin)
|
|
2
|
+
/** @jsxImportSource @opentui/solid */
|
|
3
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
4
|
+
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
5
|
+
|
|
6
|
+
const id = "@mingxy/cerebro";
|
|
7
|
+
const SIDEBAR_ORDER = 160;
|
|
8
|
+
|
|
9
|
+
function SidebarContentView(props: {
|
|
10
|
+
api: TuiPluginApi;
|
|
11
|
+
sessionID: string;
|
|
12
|
+
}) {
|
|
13
|
+
const [autoStore, setAutoStore] = createSignal(true);
|
|
14
|
+
|
|
15
|
+
const unsubscribers = [
|
|
16
|
+
props.api.event.on("session.updated", () => {
|
|
17
|
+
setAutoStore(globalThis.__cerebro_autoStore?.(props.sessionID) ?? true);
|
|
18
|
+
}),
|
|
19
|
+
props.api.event.on("tui.session.select", (event) => {
|
|
20
|
+
if (event.properties?.sessionID === props.sessionID) {
|
|
21
|
+
setAutoStore(globalThis.__cerebro_autoStore?.(props.sessionID) ?? true);
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
createEffect(() => {
|
|
27
|
+
props.sessionID;
|
|
28
|
+
setAutoStore(globalThis.__cerebro_autoStore?.(props.sessionID) ?? true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const interval = setInterval(() => {
|
|
32
|
+
setAutoStore(globalThis.__cerebro_autoStore?.(props.sessionID) ?? true);
|
|
33
|
+
}, 2000);
|
|
34
|
+
|
|
35
|
+
onCleanup(() => {
|
|
36
|
+
clearInterval(interval);
|
|
37
|
+
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const enabled = autoStore();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<box gap={0}>
|
|
44
|
+
<text fg={props.api.theme.current.text}>
|
|
45
|
+
<b>Cerebro</b>
|
|
46
|
+
</text>
|
|
47
|
+
<box gap={0}>
|
|
48
|
+
<text fg={props.api.theme.current.text} wrapMode="none">
|
|
49
|
+
{enabled ? "✅ Auto-store: ON" : "⏸️ Auto-store: OFF"}
|
|
50
|
+
</text>
|
|
51
|
+
</box>
|
|
52
|
+
</box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tui: TuiPlugin = async (api) => {
|
|
57
|
+
api.slots.register({
|
|
58
|
+
order: SIDEBAR_ORDER,
|
|
59
|
+
slots: {
|
|
60
|
+
sidebar_content(_ctx, props: { session_id: string }) {
|
|
61
|
+
return <SidebarContentView api={api} sessionID={props.session_id} />;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const pluginModule: TuiPluginModule & { id: string } = {
|
|
68
|
+
id,
|
|
69
|
+
tui,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default pluginModule;
|
package/tsconfig.json
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
"noFallthroughCasesInSwitch": true,
|
|
18
18
|
"resolveJsonModule": true,
|
|
19
19
|
"isolatedModules": true,
|
|
20
|
+
"jsx": "react-jsx",
|
|
21
|
+
"jsxImportSource": "@opentui/solid",
|
|
20
22
|
"types": ["node"]
|
|
21
23
|
},
|
|
22
|
-
"include": ["src/**/*.ts"],
|
|
24
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
23
25
|
"exclude": ["node_modules", "dist"]
|
|
24
26
|
}
|