@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 CHANGED
@@ -1,11 +1,22 @@
1
1
  {
2
2
  "name": "@mingxy/cerebro",
3
- "version": "1.5.11",
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, "🧠 Omem Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
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, "🧠 Omem Server Error", cleanMsg.substring(0, 200), "error");
253
+ showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
254
254
  } else if (cleanMsg.includes("timed out")) {
255
- showToast(tui, "🧠 Omem Service Timeout", cleanMsg.substring(0, 100), "error");
255
+ showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
256
256
  } else {
257
- showToast(tui, "🧠 Omem Error", cleanMsg.substring(0, 150), "error");
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, "🧠 Omem Service Unavailable", "Network error · check API connection", "error");
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
- const messages = sessionMessages.get(input.sessionID)!;
312
- if (messages.length > 0) {
313
- try {
314
- const result = await client.ingestMessages(messages, {
315
- mode: ingestMode,
316
- tags: [...containerTags, "auto-capture"],
317
- sessionId: input.sessionID,
318
- });
319
- if (result === null) {
320
- showToast(tui, "🔴 Archive Failed", "Session archive blocked · check spiritual realm status", "error");
321
- } else {
322
- showToast(tui, "📦 Session Archived", `${messages.length} residual dialogues archived · merged into the realm`, "success");
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
- } catch {
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
- function showToast(tui: any, title: string, message: string, variant: string = "info", duration: number = 5000) {
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
- `🧠 Omem v${pluginVersion} · Server Error`,
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
- `🧠 Omem v${pluginVersion} · Connection Failed`,
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
  }