@mingxy/cerebro 1.18.20 → 1.19.1

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.18.20",
3
+ "version": "1.19.1",
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/client.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { logInfo, logWarn, logError } from "./logger.js";
2
- import type { OmemPluginConfig } from "./config.js";
1
+ import { logWarn, logError } from "./logger.js";
2
+ import type { CerebroPluginConfig } from "./config.js";
3
3
 
4
4
  function sanitizeContent(text: string, maxLen: number): string {
5
5
  let clean = text.replace(/<[\w-]+[^>]*>[\s\S]*?<\/[\w-]+>/g, "");
@@ -42,25 +42,6 @@ export interface ListResponse {
42
42
  offset: number;
43
43
  }
44
44
 
45
-
46
- export interface DiscardedItem {
47
- memory_id: string;
48
- content: string;
49
- score: number;
50
- refine_relevance?: string;
51
- refine_reasoning?: string;
52
- }
53
-
54
- export interface ShouldRecallResponse {
55
- should_recall: boolean;
56
- query?: string;
57
- reason?: string;
58
- similarity_score?: number;
59
- confidence?: number;
60
- memories?: SearchResult[];
61
- discarded?: DiscardedItem[];
62
- }
63
-
64
45
  export interface PreferenceDto {
65
46
  id: string;
66
47
  slot: string;
@@ -101,16 +82,16 @@ export class CerebroClient {
101
82
  constructor(
102
83
  private baseUrl: string,
103
84
  private apiKey: string,
104
- private config?: Partial<OmemPluginConfig>,
85
+ private config?: Partial<CerebroPluginConfig>,
105
86
  ) {
106
87
  this.baseUrl = baseUrl.replace(/\/+$/, "");
107
88
  }
108
89
 
109
- private getCfg<S extends keyof OmemPluginConfig, K extends string & keyof OmemPluginConfig[S]>(
110
- section: S, key: K, fallback: OmemPluginConfig[S][K],
111
- ): OmemPluginConfig[S][K] {
90
+ private getCfg<S extends keyof CerebroPluginConfig, K extends string & keyof CerebroPluginConfig[S]>(
91
+ section: S, key: K, fallback: CerebroPluginConfig[S][K],
92
+ ): CerebroPluginConfig[S][K] {
112
93
  const sec = this.config?.[section] as Record<string, unknown> | undefined;
113
- return (sec?.[key] ?? fallback) as OmemPluginConfig[S][K];
94
+ return (sec?.[key] ?? fallback) as CerebroPluginConfig[S][K];
114
95
  }
115
96
 
116
97
  private async request<T>(
@@ -202,7 +183,7 @@ export class CerebroClient {
202
183
  category?: string,
203
184
  projectPath?: string,
204
185
  ): Promise<MemoryDto | null> {
205
- const safeContent = sanitizeContent(content, this.getCfg("content", "maxContentChars", 30000));
186
+ const safeContent = sanitizeContent(content, this.getCfg("content", "maxContentLength", 3000));
206
187
  return this.post<MemoryDto>("/v1/memories", {
207
188
  content: safeContent,
208
189
  tags,
@@ -261,7 +242,7 @@ export class CerebroClient {
261
242
  ): Promise<unknown> {
262
243
  const safeMessages = messages.map(m => ({
263
244
  role: m.role,
264
- content: sanitizeContent(m.content, this.getCfg("content", "maxContentChars", 30000)),
245
+ content: sanitizeContent(m.content, this.getCfg("content", "maxContentLength", 3000)),
265
246
  }));
266
247
  return this.post("/v1/memories", {
267
248
  messages: safeMessages,
@@ -362,49 +343,6 @@ export class CerebroClient {
362
343
  );
363
344
  }
364
345
 
365
- async shouldRecall(
366
- query_text: string,
367
- last_query_text: string | undefined,
368
- session_id: string,
369
- similarity_threshold?: number,
370
- max_results?: number,
371
- project_tags?: string[],
372
- conversation_context?: string[],
373
- recall_overrides?: {
374
- fetch_multiplier?: number;
375
- topk_cap_multiplier?: number;
376
- mmr_jaccard_threshold?: number;
377
- mmr_penalty_factor?: number;
378
- phase2_multiplier?: number;
379
- llm_max_eval?: number;
380
- refine_strategy?: string;
381
- skip_llm_gate?: boolean;
382
- },
383
- projectPath?: string,
384
- ): Promise<ShouldRecallResponse | null> {
385
- const res = await this.post<ShouldRecallResponse>("/v1/should-recall", {
386
- query_text,
387
- last_query_text,
388
- session_id,
389
- similarity_threshold,
390
- max_results,
391
- project_tags,
392
- conversation_context,
393
- ...recall_overrides,
394
- project_path: projectPath,
395
- }, 20_000);
396
- logInfo("shouldRecall raw response", {
397
- should_recall: res?.should_recall,
398
- memCount: res?.memories?.length ?? 0,
399
- discardedCount: res?.discarded?.length ?? 0,
400
- confidence: res?.confidence,
401
- reason: res?.reason,
402
- rawKeys: res ? Object.keys(res) : "null",
403
- rawJSON: JSON.stringify(res).slice(0, 500),
404
- });
405
- return res;
406
- }
407
-
408
346
  async updateProfileInjected(
409
347
  event_id: string,
410
348
  profile_injected: boolean,
package/src/config.ts CHANGED
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
 
5
5
  // ── Nested config interface ──────────────────────────────────────────
6
6
 
7
- export interface OmemPluginConfig {
7
+ export interface CerebroPluginConfig {
8
8
  connection: {
9
9
  apiUrl: string;
10
10
  apiKey: string;
@@ -15,21 +15,16 @@ export interface OmemPluginConfig {
15
15
  maxContentChars: number;
16
16
  maxContentLength: number;
17
17
  };
18
+ injection: {
19
+ recentCount: number;
20
+ searchCount: number;
21
+ recentTruncateChars: number;
22
+ searchTruncateChars: number;
23
+ };
18
24
  ingest: {
19
25
  autoCaptureThreshold: number;
20
26
  ingestMode: "smart" | "raw";
21
27
  };
22
- recall: {
23
- similarityThreshold: number;
24
- maxRecallResults: number;
25
- fetchMultiplier: number;
26
- topkCapMultiplier: number;
27
- mmrJaccardThreshold: number;
28
- mmrPenaltyFactor: number;
29
- phase2Multiplier: number;
30
- llmMaxEval: number;
31
- refineStrategy: "strict" | "balanced" | "loose";
32
- };
33
28
  logging: {
34
29
  logEnabled: boolean;
35
30
  logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
@@ -42,16 +37,13 @@ export interface OmemPluginConfig {
42
37
  enabled?: boolean;
43
38
  port?: number;
44
39
  };
45
- profile?: {
46
- ttlMs?: number;
47
- };
48
40
  agentMemoryPolicy?: Record<string, "none" | "readonly" | "readwrite">;
49
41
  defaultPolicy?: "none" | "readonly" | "readwrite";
50
42
  }
51
43
 
52
44
  // ── Defaults ─────────────────────────────────────────────────────────
53
45
 
54
- const DEFAULTS: OmemPluginConfig = {
46
+ const DEFAULTS: CerebroPluginConfig = {
55
47
  connection: {
56
48
  apiUrl: "https://www.mengxy.cc",
57
49
  apiKey: "",
@@ -60,23 +52,18 @@ const DEFAULTS: OmemPluginConfig = {
60
52
  content: {
61
53
  maxQueryLength: 200,
62
54
  maxContentChars: 30000,
63
- maxContentLength: 500,
55
+ maxContentLength: 3000,
56
+ },
57
+ injection: {
58
+ recentCount: 5,
59
+ searchCount: 10,
60
+ recentTruncateChars: 0, // 0 = 不截断
61
+ searchTruncateChars: 0, // 0 = 不截断
64
62
  },
65
63
  ingest: {
66
64
  autoCaptureThreshold: 5,
67
65
  ingestMode: "smart",
68
66
  },
69
- recall: {
70
- similarityThreshold: 0.4,
71
- maxRecallResults: 10,
72
- fetchMultiplier: 3,
73
- topkCapMultiplier: 2,
74
- mmrJaccardThreshold: 0.85,
75
- mmrPenaltyFactor: 0.5,
76
- phase2Multiplier: 2,
77
- llmMaxEval: 15,
78
- refineStrategy: "loose",
79
- },
80
67
  logging: {
81
68
  logEnabled: true,
82
69
  logLevel: "INFO",
@@ -88,9 +75,6 @@ const DEFAULTS: OmemPluginConfig = {
88
75
  web: {
89
76
  enabled: true,
90
77
  },
91
- profile: {
92
- ttlMs: 300000,
93
- },
94
78
  };
95
79
 
96
80
  // ── Flat-to-nested migration ─────────────────────────────────────────
@@ -105,8 +89,6 @@ interface FlatConfig {
105
89
  maxContentLength?: number;
106
90
  autoCaptureThreshold?: number;
107
91
  ingestMode?: "smart" | "raw";
108
- similarityThreshold?: number;
109
- maxRecallResults?: number;
110
92
  toastDelayMs?: number;
111
93
  logEnabled?: boolean;
112
94
  logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR";
@@ -119,7 +101,7 @@ function isFlatConfig(cfg: Record<string, unknown>): boolean {
119
101
  return "apiUrl" in cfg && !("connection" in cfg);
120
102
  }
121
103
 
122
- function migrateFlatToNested(flat: FlatConfig): OmemPluginConfig {
104
+ function migrateFlatToNested(flat: FlatConfig): CerebroPluginConfig {
123
105
  return {
124
106
  connection: {
125
107
  apiUrl: flat.apiUrl ?? DEFAULTS.connection.apiUrl,
@@ -131,21 +113,11 @@ function migrateFlatToNested(flat: FlatConfig): OmemPluginConfig {
131
113
  maxContentChars: flat.maxContentChars ?? DEFAULTS.content.maxContentChars,
132
114
  maxContentLength: flat.maxContentLength ?? DEFAULTS.content.maxContentLength,
133
115
  },
116
+ injection: { ...DEFAULTS.injection },
134
117
  ingest: {
135
118
  autoCaptureThreshold: flat.autoCaptureThreshold ?? DEFAULTS.ingest.autoCaptureThreshold,
136
119
  ingestMode: flat.ingestMode ?? DEFAULTS.ingest.ingestMode,
137
120
  },
138
- recall: {
139
- similarityThreshold: flat.similarityThreshold ?? DEFAULTS.recall.similarityThreshold,
140
- maxRecallResults: flat.maxRecallResults ?? DEFAULTS.recall.maxRecallResults,
141
- fetchMultiplier: DEFAULTS.recall.fetchMultiplier,
142
- topkCapMultiplier: DEFAULTS.recall.topkCapMultiplier,
143
- mmrJaccardThreshold: DEFAULTS.recall.mmrJaccardThreshold,
144
- mmrPenaltyFactor: DEFAULTS.recall.mmrPenaltyFactor,
145
- phase2Multiplier: DEFAULTS.recall.phase2Multiplier,
146
- llmMaxEval: DEFAULTS.recall.llmMaxEval,
147
- refineStrategy: DEFAULTS.recall.refineStrategy,
148
- },
149
121
  logging: {
150
122
  logEnabled: flat.logEnabled ?? DEFAULTS.logging.logEnabled,
151
123
  logLevel: flat.logLevel ?? DEFAULTS.logging.logLevel,
@@ -162,17 +134,16 @@ function migrateFlatToNested(flat: FlatConfig): OmemPluginConfig {
162
134
  type IngestMode = "smart" | "raw";
163
135
  const INGEST_MODES: ReadonlySet<string> = new Set<IngestMode>(["smart", "raw"]);
164
136
 
165
- function deepMerge(base: OmemPluginConfig, overrides: Partial<OmemPluginConfig>): OmemPluginConfig {
166
- const result: OmemPluginConfig = {
137
+ function deepMerge(base: CerebroPluginConfig, overrides: Partial<CerebroPluginConfig>): CerebroPluginConfig {
138
+ const result: CerebroPluginConfig = {
167
139
  connection: { ...base.connection, ...overrides.connection },
168
140
  content: { ...base.content, ...overrides.content },
141
+ injection: { ...base.injection, ...overrides.injection },
169
142
  ingest: { ...base.ingest, ...overrides.ingest },
170
- recall: { ...base.recall, ...overrides.recall },
171
143
  logging: { ...base.logging, ...overrides.logging },
172
144
  ui: { ...base.ui, ...overrides.ui },
173
145
  };
174
146
  result.web = { ...base.web!, ...overrides.web };
175
- result.profile = { ...base.profile!, ...overrides.profile };
176
147
  if (overrides.agentMemoryPolicy) result.agentMemoryPolicy = overrides.agentMemoryPolicy;
177
148
  if (overrides.defaultPolicy) result.defaultPolicy = overrides.defaultPolicy;
178
149
  return result;
@@ -218,8 +189,8 @@ function configLog(message: string, fields?: Record<string, unknown>, level: str
218
189
  }
219
190
  }
220
191
 
221
- export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPluginConfig {
222
- let config: OmemPluginConfig = structuredClone(DEFAULTS);
192
+ export function loadPluginConfig(overrides?: Partial<CerebroPluginConfig>): CerebroPluginConfig {
193
+ let config: CerebroPluginConfig = structuredClone(DEFAULTS);
223
194
 
224
195
  // Try loading from config file
225
196
  try {
@@ -227,7 +198,7 @@ export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPlu
227
198
  const raw = JSON.parse(readFileSync(cfgPath, "utf-8")) as Record<string, unknown>;
228
199
 
229
200
  // Auto-migrate flat format
230
- const parsed: OmemPluginConfig = isFlatConfig(raw) ? migrateFlatToNested(raw as FlatConfig) : raw as unknown as OmemPluginConfig;
201
+ const parsed: CerebroPluginConfig = isFlatConfig(raw) ? migrateFlatToNested(raw as FlatConfig) : raw as unknown as CerebroPluginConfig;
231
202
 
232
203
  // Merge nested groups with defaults for safety
233
204
  config = deepMerge(config, parsed);
@@ -252,12 +223,6 @@ export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPlu
252
223
  if (INGEST_MODES.has(process.env.OMEM_INGEST_MODE ?? "")) {
253
224
  config.ingest.ingestMode = process.env.OMEM_INGEST_MODE as IngestMode;
254
225
  }
255
- if (process.env.OMEM_SIMILARITY_THRESHOLD) {
256
- config.recall.similarityThreshold = parseFloat(process.env.OMEM_SIMILARITY_THRESHOLD) || DEFAULTS.recall.similarityThreshold;
257
- }
258
- if (process.env.OMEM_MAX_RECALL_RESULTS) {
259
- config.recall.maxRecallResults = parseInt(process.env.OMEM_MAX_RECALL_RESULTS, 10) || DEFAULTS.recall.maxRecallResults;
260
- }
261
226
 
262
227
  if (process.env.OMEM_WEB_ENABLED === "false" || process.env.OMEM_WEB_ENABLED === "0") {
263
228
  config.web = { ...config.web!, enabled: false };
@@ -280,7 +245,7 @@ export type AgentPolicy = "none" | "readonly" | "readwrite";
280
245
 
281
246
  export function resolveAgentPolicy(
282
247
  agentName: string,
283
- config: Partial<OmemPluginConfig>,
248
+ config: Partial<CerebroPluginConfig>,
284
249
  ): AgentPolicy {
285
250
  const policies = config.agentMemoryPolicy;
286
251
  if (policies) {
package/src/hooks.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Model, UserMessage, Part } from "@opencode-ai/sdk";
2
2
  import type { CerebroClient, SearchResult } from "./client.js";
3
- import { type OmemPluginConfig, resolveAgentPolicy } from "./config.js";
3
+ import { type CerebroPluginConfig, DEFAULTS, resolveAgentPolicy } from "./config.js";
4
4
  import { logDebug, logInfo, logError as logErr } from "./logger.js";
5
5
  import { readFile } from "node:fs/promises";
6
6
 
@@ -111,15 +111,23 @@ async function detectProjectName(rootPath: string): Promise<string | undefined>
111
111
  return result;
112
112
  }
113
113
 
114
- export function showToast(tui: any, title: string, message: string, variant: string = "info", delayMs: number = 7000) {
114
+ export function showToast(tui: any, title: string, message: string, variant: string = "info", delayMs?: number) {
115
115
  if (!tui) return;
116
+ const effectiveDelay = delayMs ?? DEFAULTS.ui.toastDelayMs;
116
117
  setTimeout(() => {
117
118
  try {
118
119
  tui.showToast({ body: { title, message, variant, duration: 5000 } });
119
120
  } catch (err) {
120
121
  logErr("showToast failed", { error: String(err) });
121
122
  }
122
- }, delayMs);
123
+ }, effectiveDelay);
124
+ }
125
+
126
+ export function createToast(config: Partial<CerebroPluginConfig>) {
127
+ const defaultDelay = config.ui?.toastDelayMs ?? DEFAULTS.ui.toastDelayMs;
128
+ return (tui: any, title: string, message: string, variant: string = "info", delayMs?: number) => {
129
+ showToast(tui, title, message, variant, delayMs ?? defaultDelay);
130
+ };
123
131
  }
124
132
 
125
133
  const SYSTEM_INJECTION_PATTERNS: RegExp[] = [
@@ -211,7 +219,7 @@ const FETCH_POLICY = [
211
219
  "</cerebro-fetch-policy>",
212
220
  ].join("\n");
213
221
 
214
- const INJECTION_MAX_CHARS_FALLBACK = 4000;
222
+ const MAX_INJECTION_CHARS_FALLBACK = DEFAULTS.content.maxContentChars;
215
223
 
216
224
  interface InjectionResult {
217
225
  text: string;
@@ -224,9 +232,14 @@ export async function buildMemoryInjection(
224
232
  client: CerebroClient,
225
233
  projectPath: string | undefined,
226
234
  query: string,
227
- config: Partial<OmemPluginConfig>,
235
+ config: Partial<CerebroPluginConfig>,
228
236
  ): Promise<InjectionResult> {
229
- const maxChars = config.content?.maxContentLength ?? INJECTION_MAX_CHARS_FALLBACK;
237
+ const maxChars = config.content?.maxContentChars ?? MAX_INJECTION_CHARS_FALLBACK;
238
+ const ic = config.injection ?? DEFAULTS.injection;
239
+ const recentCount = ic.recentCount || DEFAULTS.injection.recentCount;
240
+ const searchCount = ic.searchCount || DEFAULTS.injection.searchCount;
241
+ const recentTruncate = ic.recentTruncateChars || 0; // 0 = 不截断
242
+ const searchTruncate = ic.searchTruncateChars || 0; // 0 = 不截断
230
243
 
231
244
  const [profile, projectMemories, searchResults] = await Promise.all([
232
245
  Promise.race([
@@ -234,12 +247,12 @@ export async function buildMemoryInjection(
234
247
  new Promise<null>((resolve) => setTimeout(() => resolve(null), 1000)),
235
248
  ]).catch(() => null),
236
249
  Promise.race([
237
- client.listRecent(5, projectPath),
250
+ client.listRecent(recentCount, projectPath),
238
251
  new Promise<never[]>((resolve) => setTimeout(() => resolve([]), 1000)),
239
252
  ]).catch(() => []),
240
253
  query
241
254
  ? Promise.race([
242
- client.searchMemories(query, 10, undefined, undefined, projectPath),
255
+ client.searchMemories(query, searchCount, undefined, undefined, projectPath),
243
256
  new Promise<never[]>((resolve) => setTimeout(() => resolve([]), 1500)),
244
257
  ]).catch(() => [])
245
258
  : Promise.resolve([]),
@@ -259,7 +272,7 @@ export async function buildMemoryInjection(
259
272
  for (const m of projectMemories) {
260
273
  seenIds.add(m.id);
261
274
  const age = formatRelativeAge(m.updated_at || m.created_at) || "unknown";
262
- const content = truncate(m.content, 200);
275
+ const content = recentTruncate > 0 ? truncate(m.content, recentTruncate) : m.content;
263
276
  sections.push(`- (${age}) ${content}`);
264
277
  }
265
278
  sections.push("");
@@ -270,7 +283,7 @@ export async function buildMemoryInjection(
270
283
  sections.push("## Relevant Memories");
271
284
  for (const r of dedupedResults) {
272
285
  const age = formatRelativeAge(r.memory.created_at) || "unknown";
273
- const content = truncate(r.memory.content, 300);
286
+ const content = searchTruncate > 0 ? truncate(r.memory.content, searchTruncate) : r.memory.content;
274
287
  sections.push(`- (${age}) ${content}`);
275
288
  }
276
289
  sections.push("");
@@ -298,7 +311,7 @@ export function chatMessageRecallHook(
298
311
  client: CerebroClient,
299
312
  _containerTags: string[],
300
313
  tui: any,
301
- config: Partial<OmemPluginConfig> = {},
314
+ config: Partial<CerebroPluginConfig> = {},
302
315
  getAgentName?: () => string,
303
316
  directory?: string,
304
317
  ) {
@@ -435,7 +448,7 @@ export function createCerebroCompactionPrompt(
435
448
  return sections.join("\n");
436
449
  }
437
450
 
438
- export function compactingHook(client: CerebroClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart", isAutoStoreEnabled?: (sessionId: string | undefined) => boolean, getMainSessionId?: () => string | undefined, sdkClient?: any, config: Partial<OmemPluginConfig> = {}, agentId?: string, directory?: string) {
451
+ export function compactingHook(client: CerebroClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart", isAutoStoreEnabled?: (sessionId: string | undefined) => boolean, getMainSessionId?: () => string | undefined, sdkClient?: any, config: Partial<CerebroPluginConfig> = {}, agentId?: string, directory?: string) {
439
452
  const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
440
453
  return async (
441
454
  input: { sessionID?: string },
@@ -568,7 +581,7 @@ export function autocontinueHook(
568
581
  isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
569
582
  getMainSessionId?: () => string | undefined,
570
583
  sdkClient?: any,
571
- config: Partial<OmemPluginConfig> = {},
584
+ config: Partial<CerebroPluginConfig> = {},
572
585
  agentId?: string,
573
586
  directory?: string,
574
587
  ) {
@@ -683,7 +696,7 @@ export function sessionIdleHook(
683
696
  getMainSessionId?: () => string | undefined,
684
697
  isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
685
698
  agentId?: string,
686
- config: Partial<OmemPluginConfig> = {},
699
+ config: Partial<CerebroPluginConfig> = {},
687
700
  onAgentResolved?: (name: string) => void,
688
701
  directory?: string,
689
702
  ) {
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import type { Server } from "node:http";
7
7
  import { CerebroClient } from "./client.js";
8
- import { chatMessageRecallHook, autocontinueHook, compactingHook, sessionIdleHook, showToast as hooksShowToast, sessionMessages, firstMessages } from "./hooks.js";
8
+ import { chatMessageRecallHook, autocontinueHook, compactingHook, sessionIdleHook, createToast, sessionMessages, firstMessages } from "./hooks.js";
9
9
  import { detectSaveKeyword, detectRecallKeyword, KEYWORD_NUDGE, RECALL_NUDGE } from "./keywords.js";
10
10
  import { getUserTag, getProjectTag } from "./tags.js";
11
11
  import { buildTools } from "./tools.js";
@@ -64,33 +64,30 @@ const OmemPlugin: Plugin = async (input) => {
64
64
  } catch {}
65
65
 
66
66
  const config = loadPluginConfig(overrides as any);
67
+ const toast = createToast(config);
67
68
 
68
69
  const cerebroClient = new CerebroClient(config.connection.apiUrl, config.connection.apiKey, config);
69
70
 
70
- // 启动时检测连接状态
71
71
  try {
72
72
  await cerebroClient.getStats();
73
- hooksShowToast(tui, "🧠 Cerebro · Connected", `Version v${pluginVersion}`, "success", 6000);
74
73
  logInfo(`Connected to ${config.connection.apiUrl}`);
75
74
  } catch (err) {
76
75
  const errMsg = err instanceof Error ? err.message : String(err);
77
76
  logError(`Connection failed: ${errMsg}`);
78
77
  if (errMsg.includes("[cerebro]")) {
79
78
  const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
80
- hooksShowToast(
79
+ toast(
81
80
  tui,
82
81
  `🧠 Cerebro v${pluginVersion} · Server Error`,
83
82
  cleanMsg.substring(0, 150),
84
- "error",
85
- 8000
83
+ "error"
86
84
  );
87
85
  } else {
88
- hooksShowToast(
86
+ toast(
89
87
  tui,
90
88
  `🧠 Cerebro v${pluginVersion} · Connection Failed`,
91
89
  `Unable to reach ${config.connection.apiUrl}`,
92
- "error",
93
- 8000
90
+ "error"
94
91
  );
95
92
  }
96
93
  }
@@ -108,6 +105,7 @@ const OmemPlugin: Plugin = async (input) => {
108
105
 
109
106
  let webServer: Server | null = null;
110
107
  const webEnabled = config.web?.enabled !== false;
108
+ let webPort: number | undefined;
111
109
  if (webEnabled) {
112
110
  try {
113
111
  webServer = await startWebServer({
@@ -116,15 +114,20 @@ const OmemPlugin: Plugin = async (input) => {
116
114
  });
117
115
  if (webServer) {
118
116
  const addr = webServer.address();
119
- const actualPort = typeof addr === "object" && addr ? addr.port : config.web?.port || 5212;
120
- hooksShowToast(tui, "🌐 Cerebro Web", `http://localhost:${actualPort}`, "info", 8000);
121
- logInfo(`Web UI available at http://localhost:${actualPort}`);
117
+ webPort = typeof addr === "object" && addr ? addr.port : config.web?.port || 5212;
118
+ logInfo(`Web UI available at http://localhost:${webPort}`);
122
119
  }
123
120
  } catch (err) {
124
121
  logError(`Web server start failed: ${err instanceof Error ? err.message : String(err)}`);
125
122
  }
126
123
  }
127
124
 
125
+ if (webPort) {
126
+ toast(tui, `🧠 Cerebro Connected · v${pluginVersion}`, `\n🌐 Open in browser http://localhost:${webPort}`, "success");
127
+ } else {
128
+ toast(tui, "🧠 Connected", `v${pluginVersion}`, "success");
129
+ }
130
+
128
131
  const shutdown = async () => {
129
132
  try {
130
133
  if (webServer) {