@mingxy/cerebro 1.4.0 → 1.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/cerebro",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
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",
package/src/client.ts CHANGED
@@ -84,6 +84,7 @@ export interface MemoryDto {
84
84
  source?: string;
85
85
  tenant_id: string;
86
86
  agent_id?: string;
87
+ importance: number;
87
88
  created_at: string;
88
89
  updated_at: string;
89
90
  }
@@ -171,6 +172,7 @@ export class OmemClient {
171
172
  scope?: string,
172
173
  agentId?: string,
173
174
  sessionId?: string,
175
+ visibility?: string,
174
176
  ): Promise<MemoryDto | null> {
175
177
  const safeContent = sanitizeContent(content, this.getCfg("maxContentChars", 30000));
176
178
  return this.post<MemoryDto>("/v1/memories", {
@@ -180,6 +182,7 @@ export class OmemClient {
180
182
  scope,
181
183
  agent_id: agentId,
182
184
  session_id: sessionId,
185
+ visibility,
183
186
  });
184
187
  }
185
188
 
@@ -356,4 +359,16 @@ export class OmemClient {
356
359
  );
357
360
  return res?.recalls ?? [];
358
361
  }
362
+
363
+ async sessionIngest(
364
+ summaries: Array<{ topic: string; content: string }>,
365
+ sessionId?: string,
366
+ agentId?: string,
367
+ ): Promise<unknown> {
368
+ return this.post("/v1/memories/session-ingest", {
369
+ summaries,
370
+ session_id: sessionId,
371
+ agent_id: agentId,
372
+ });
373
+ }
359
374
  }
package/src/hooks.ts CHANGED
@@ -102,13 +102,15 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
102
102
  if (clustered.cluster_summaries.length > 0) {
103
103
  sections.push("## 📋 主题簇(聚合记忆)");
104
104
  for (const cs of clustered.cluster_summaries) {
105
- sections.push(`\n### ${cs.title} (整合自${cs.member_count}条记忆)`);
105
+ const scoreIndicator = cs.relevance_score >= 0.8 ? "★★★" : cs.relevance_score >= 0.6 ? "★★" : "★";
106
+ sections.push(`\n### ${cs.title} (整合自${cs.member_count}条记忆) ${scoreIndicator}`);
106
107
  sections.push(`> ${cs.summary}`);
107
108
  if (cs.key_memories.length > 0) {
108
109
  sections.push("**核心要点:**");
109
110
  for (const mem of cs.key_memories) {
110
111
  const content = truncate(mem.content, maxContentLength);
111
- sections.push(`- ${content}`);
112
+ const importanceBar = mem.importance >= 0.7 ? "●" : mem.importance >= 0.4 ? "◐" : "○";
113
+ sections.push(`- ${importanceBar} ${content}`);
112
114
  }
113
115
  }
114
116
  }
@@ -341,6 +343,105 @@ export function keywordDetectionHook(client: OmemClient, containerTags: string[]
341
343
  };
342
344
  }
343
345
 
346
+ const processedMessageIds = new Set<string>();
347
+
348
+ export function sessionIdleHook(
349
+ omemClient: OmemClient,
350
+ containerTags: string[],
351
+ tui: any,
352
+ sdkClient: any,
353
+ ingestMode: "smart" | "raw" = "smart",
354
+ ) {
355
+ let idleTimeout: ReturnType<typeof setTimeout> | null = null;
356
+ let isCapturing = false;
357
+
358
+ return async (input: { event: { type: string; properties?: any } }) => {
359
+ if (input.event.type !== "session.idle") return;
360
+
361
+ const sessionID = input.event.properties?.sessionID;
362
+ if (!sessionID) return;
363
+
364
+ if (idleTimeout) clearTimeout(idleTimeout);
365
+
366
+ idleTimeout = setTimeout(async () => {
367
+ if (isCapturing) return;
368
+ isCapturing = true;
369
+
370
+ try {
371
+ const response = await sdkClient.session.messages({ path: { id: sessionID } });
372
+ if (!response?.data) return;
373
+
374
+ const messages = response.data;
375
+ const conversationMessages: Array<{ role: string; content: string }> = [];
376
+ const dcpSummaries: Array<{ topic: string; content: string }> = [];
377
+ let hasNewMessages = false;
378
+
379
+ for (const msg of messages) {
380
+ const msgId = msg.info?.id;
381
+ if (!msgId || processedMessageIds.has(msgId)) continue;
382
+
383
+ const role = msg.info?.role;
384
+ if (role !== "user" && role !== "assistant") continue;
385
+
386
+ const textParts = (msg.parts || [])
387
+ .filter((p: any) => p.type === "text" && p.text)
388
+ .map((p: any) => p.text);
389
+ const text = textParts.join("\n").trim();
390
+ if (!text) continue;
391
+
392
+ hasNewMessages = true;
393
+ processedMessageIds.add(msgId);
394
+
395
+ if (text.includes("[Compressed conversation section]")) {
396
+ const blockRegex = /\[Compressed conversation section\]\n([\s\S]*?)(?=<dcp-block|$)/g;
397
+ let match;
398
+ while ((match = blockRegex.exec(text)) !== null) {
399
+ const summary = match[1].trim();
400
+ if (summary) {
401
+ dcpSummaries.push({ topic: "session-compress", content: summary });
402
+ }
403
+ }
404
+ }
405
+
406
+ conversationMessages.push({ role, content: text });
407
+ }
408
+
409
+ if (!hasNewMessages) return;
410
+
411
+ if (dcpSummaries.length > 0) {
412
+ try {
413
+ await omemClient.sessionIngest(dcpSummaries, sessionID);
414
+ showToast(tui, "🧠 DCP Summary Captured", `${dcpSummaries.length} compress summaries stored (zero-LLM)`, "success");
415
+ } catch (err) {
416
+ showToast(tui, "🔴 DCP Capture Failed", String(err).substring(0, 100), "error");
417
+ }
418
+ }
419
+
420
+ if (conversationMessages.length > 0) {
421
+ try {
422
+ const result = await omemClient.ingestMessages(conversationMessages, {
423
+ mode: ingestMode,
424
+ tags: [...containerTags, "session-idle"],
425
+ sessionId: sessionID,
426
+ });
427
+ if (result) {
428
+ showToast(tui, "🧠 Session Captured", `${conversationMessages.length} messages captured via session.idle`, "success");
429
+ }
430
+ } catch (err) {
431
+ showToast(tui, "🔴 Session Capture Failed", String(err).substring(0, 100), "error");
432
+ }
433
+ }
434
+ } catch (err) {
435
+ const errMsg = err instanceof Error ? err.message : String(err);
436
+ showToast(tui, "🔴 Idle Capture Error", errMsg.substring(0, 100), "error");
437
+ } finally {
438
+ isCapturing = false;
439
+ idleTimeout = null;
440
+ }
441
+ }, 10000);
442
+ };
443
+ }
444
+
344
445
  export function compactingHook(client: OmemClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart") {
345
446
  return async (
346
447
  input: { sessionID?: string },
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { join, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { OmemClient } from "./client.js";
6
- import { autoRecallHook, compactingHook, keywordDetectionHook } from "./hooks.js";
6
+ import { autoRecallHook, compactingHook, keywordDetectionHook, sessionIdleHook } from "./hooks.js";
7
7
  import { getUserTag, getProjectTag } from "./tags.js";
8
8
  import { buildTools } from "./tools.js";
9
9
  import { logInfo, logError } from "./logger.js";
@@ -96,6 +96,7 @@ const OmemPlugin: Plugin = async (input) => {
96
96
  "chat.message": keywordDetectionHook(omemClient, containerTags, config.autoCaptureThreshold, tui, config.ingestMode),
97
97
  "experimental.session.compacting": compactingHook(omemClient, containerTags, tui, config.ingestMode),
98
98
  tool: buildTools(omemClient, containerTags, { agentId, getSessionId: () => currentSessionId }),
99
+ event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode),
99
100
  "shell.env": async (_input: any, output: any) => {
100
101
  if (directory) {
101
102
  output.env.OMEM_PROJECT_DIR = directory;
package/src/tools.ts CHANGED
@@ -45,6 +45,10 @@ export function buildTools(client: OmemClient, containerTags: string[], context:
45
45
  .string()
46
46
  .optional()
47
47
  .describe("Memory scope: 'project' (default, only visible in this project) or 'global' (visible across all projects)"),
48
+ visibility: tool.schema
49
+ .string()
50
+ .optional()
51
+ .describe("Memory visibility: 'global' (default, visible to all agents) or 'private' (only visible to the storing agent). Use 'private' for sensitive data like credentials, personal info, or anything the user wouldn't want shared."),
48
52
  },
49
53
  async execute(args) {
50
54
  const allTags = [...containerTags, ...(args.tags ?? [])];
@@ -55,6 +59,7 @@ export function buildTools(client: OmemClient, containerTags: string[], context:
55
59
  args.scope ?? "project",
56
60
  context.agentId,
57
61
  context.getSessionId(),
62
+ args.visibility,
58
63
  );
59
64
  if (!result) return JSON.stringify({ ok: false, error: "The omem server may be unavailable." });
60
65
  return JSON.stringify({ ok: true, id: result.id, tags: result.tags });