@mingxy/cerebro 1.5.10 → 1.6.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 +3 -2
- package/src/client.ts +12 -0
- package/src/hooks.ts +130 -73
- package/src/index.ts +44 -12
- package/src/tools.ts +27 -0
- package/src/tui.tsx +72 -0
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mingxy/cerebro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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
7
|
"oc-plugin": [
|
|
8
|
-
"server"
|
|
8
|
+
"server",
|
|
9
|
+
"tui"
|
|
9
10
|
],
|
|
10
11
|
"keywords": [
|
|
11
12
|
"opencode",
|
package/src/client.ts
CHANGED
|
@@ -359,4 +359,16 @@ export class OmemClient {
|
|
|
359
359
|
);
|
|
360
360
|
return res?.recalls ?? [];
|
|
361
361
|
}
|
|
362
|
+
|
|
363
|
+
async sessionIngest(
|
|
364
|
+
messages: Array<{ role: string; content: string }>,
|
|
365
|
+
sessionId?: string,
|
|
366
|
+
agentId?: string,
|
|
367
|
+
): Promise<unknown> {
|
|
368
|
+
return this.post("/v1/memories/session-ingest", {
|
|
369
|
+
messages,
|
|
370
|
+
session_id: sessionId,
|
|
371
|
+
agent_id: agentId,
|
|
372
|
+
}, 60000);
|
|
373
|
+
}
|
|
362
374
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -12,31 +12,17 @@ function showToast(tui: any, title: string, message: string, variant: string = "
|
|
|
12
12
|
}, delayMs);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function extractUserRequest(content: string): string {
|
|
16
|
+
const match = content.match(/<user-request>([\s\S]*?)<\/user-request>/);
|
|
17
|
+
return match ? match[1].trim() : content;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const keywordDetectedSessions = new Set<string>();
|
|
16
21
|
const injectedMemoryIds = new Map<string, Set<string>>();
|
|
17
22
|
const firstMessages = new Map<string, string>();
|
|
18
23
|
const sessionMessages = new Map<string, Array<{ role: string; content: string }>>();
|
|
19
24
|
const profileInjectedSessions = new Set<string>();
|
|
20
25
|
|
|
21
|
-
function extractMemoryIds(result: unknown): string[] {
|
|
22
|
-
if (!result) return [];
|
|
23
|
-
if (Array.isArray(result)) {
|
|
24
|
-
return (result as Array<{ id?: string }>).map((m) => m.id).filter(Boolean) as string[];
|
|
25
|
-
}
|
|
26
|
-
if (typeof result === "object" && result !== null) {
|
|
27
|
-
const r = result as Record<string, unknown>;
|
|
28
|
-
if (Array.isArray(r.memories)) {
|
|
29
|
-
return (r.memories as Array<{ id?: string }>).map((m) => m.id).filter(Boolean) as string[];
|
|
30
|
-
}
|
|
31
|
-
if (Array.isArray(r.results)) {
|
|
32
|
-
return (r.results as Array<{ id?: string; memory?: { id?: string } }>)
|
|
33
|
-
.map((m) => m.id ?? m.memory?.id)
|
|
34
|
-
.filter(Boolean) as string[];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
26
|
function formatRelativeAge(isoDate: string): string {
|
|
41
27
|
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
42
28
|
const minutes = Math.floor(diffMs / 60_000);
|
|
@@ -149,14 +135,15 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
149
135
|
try {
|
|
150
136
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
151
137
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
152
|
-
const
|
|
138
|
+
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
139
|
+
const query_text = extractUserRequest(rawQuery);
|
|
153
140
|
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
154
141
|
|
|
155
142
|
const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
|
|
156
143
|
const shouldRecallRes = await client.shouldRecall(query_text, last_query_text, input.sessionID, similarityThreshold, maxRecallResults, projectTags.length > 0 ? projectTags : undefined);
|
|
157
144
|
|
|
158
145
|
if (!shouldRecallRes) {
|
|
159
|
-
showToast(tui, "🧠
|
|
146
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
|
|
160
147
|
return;
|
|
161
148
|
}
|
|
162
149
|
|
|
@@ -263,14 +250,14 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
263
250
|
// Server returned error (500, etc.) with details
|
|
264
251
|
const cleanMsg = errMsg.replace(/^\[omem\]\s*/, "");
|
|
265
252
|
if (cleanMsg.startsWith("500")) {
|
|
266
|
-
showToast(tui, "🧠
|
|
253
|
+
showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
|
|
267
254
|
} else if (cleanMsg.includes("timed out")) {
|
|
268
|
-
showToast(tui, "🧠
|
|
255
|
+
showToast(tui, "🧠 Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
|
|
269
256
|
} else {
|
|
270
|
-
showToast(tui, "🧠
|
|
257
|
+
showToast(tui, "🧠 Cerebro Error", cleanMsg.substring(0, 150), "error");
|
|
271
258
|
}
|
|
272
259
|
} else if (errMsg.includes("fetch") || errMsg.includes("network")) {
|
|
273
|
-
showToast(tui, "🧠
|
|
260
|
+
showToast(tui, "🧠 Cerebro Service Unavailable", "Network error · check API connection", "error");
|
|
274
261
|
} else {
|
|
275
262
|
showToast(tui, "🧠 Memory Recall Error", errMsg.substring(0, 100), "error");
|
|
276
263
|
}
|
|
@@ -278,7 +265,7 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
278
265
|
};
|
|
279
266
|
}
|
|
280
267
|
|
|
281
|
-
export function keywordDetectionHook(
|
|
268
|
+
export function keywordDetectionHook(_client: OmemClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart") {
|
|
282
269
|
return async (
|
|
283
270
|
input: { sessionID: string; messageID?: string },
|
|
284
271
|
output: { message: UserMessage; parts: Part[] },
|
|
@@ -307,65 +294,41 @@ export function keywordDetectionHook(client: OmemClient, containerTags: string[]
|
|
|
307
294
|
});
|
|
308
295
|
|
|
309
296
|
const messages = sessionMessages.get(input.sessionID)!;
|
|
297
|
+
// Ingest is now handled by sessionIdleHook (session.idle → sessionIngest API).
|
|
298
|
+
// This hook only collects messages and detects keywords for recall.
|
|
310
299
|
if (messages.length >= threshold) {
|
|
311
|
-
|
|
312
|
-
const result = await client.ingestMessages(messages, {
|
|
313
|
-
mode: ingestMode,
|
|
314
|
-
tags: [...containerTags, "auto-capture"],
|
|
315
|
-
sessionId: input.sessionID,
|
|
316
|
-
});
|
|
317
|
-
if (result === null) {
|
|
318
|
-
showToast(tui, "🔴 Capture Failed", `Memory capture blocked · check API Key and spiritual connection`, "error");
|
|
319
|
-
} else {
|
|
320
|
-
showToast(tui, "🧠 Memory Sealed", `${messages.length} dialogues captured · entrusted to the heavens for refinement`, "success");
|
|
321
|
-
const memoryIds = extractMemoryIds(result);
|
|
322
|
-
if (memoryIds.length > 0) {
|
|
323
|
-
const recordResult = await client.recordSessionRecall(
|
|
324
|
-
input.sessionID,
|
|
325
|
-
memoryIds,
|
|
326
|
-
"auto",
|
|
327
|
-
firstMessages.get(input.sessionID) || "",
|
|
328
|
-
0,
|
|
329
|
-
0,
|
|
330
|
-
);
|
|
331
|
-
if (recordResult) {
|
|
332
|
-
showToast(tui, "📦 Capture Recorded", `${memoryIds.length} memory(s) saved to session history`, "success");
|
|
333
|
-
} else {
|
|
334
|
-
showToast(tui, "🔴 Capture Record Failed", `Failed to save capture record · check API connection`, "error");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
sessionMessages.delete(input.sessionID);
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
340
|
-
showToast(tui, "🔴 Capture Failed", "Memory capture blocked · spiritual pulse anomaly", "error");
|
|
341
|
-
}
|
|
300
|
+
// Threshold reached — messages will be processed on next session.idle
|
|
342
301
|
}
|
|
343
302
|
};
|
|
344
303
|
}
|
|
345
304
|
|
|
346
|
-
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) {
|
|
347
306
|
return async (
|
|
348
307
|
input: { sessionID?: string },
|
|
349
308
|
output: { context: string[]; prompt?: string },
|
|
350
309
|
) => {
|
|
351
310
|
if (input.sessionID && sessionMessages.has(input.sessionID)) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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");
|
|
364
329
|
}
|
|
365
|
-
|
|
366
|
-
showToast(tui, "🔴 Archive Failed", "Session archive blocked · spiritual pulse anomaly", "error");
|
|
330
|
+
sessionMessages.delete(input.sessionID);
|
|
367
331
|
}
|
|
368
|
-
sessionMessages.delete(input.sessionID);
|
|
369
332
|
}
|
|
370
333
|
}
|
|
371
334
|
|
|
@@ -379,3 +342,97 @@ export function compactingHook(client: OmemClient, containerTags: string[], tui:
|
|
|
379
342
|
}
|
|
380
343
|
};
|
|
381
344
|
}
|
|
345
|
+
|
|
346
|
+
const processedMessageIds = new Set<string>();
|
|
347
|
+
const pluginStartTime = Date.now();
|
|
348
|
+
|
|
349
|
+
export function sessionIdleHook(
|
|
350
|
+
omemClient: OmemClient,
|
|
351
|
+
_containerTags: string[],
|
|
352
|
+
tui: any,
|
|
353
|
+
sdkClient: any,
|
|
354
|
+
_ingestMode: "smart" | "raw" = "smart",
|
|
355
|
+
threshold: number = 0,
|
|
356
|
+
getMainSessionId?: () => string | undefined,
|
|
357
|
+
isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
|
|
358
|
+
) {
|
|
359
|
+
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
360
|
+
let isCapturing = false;
|
|
361
|
+
|
|
362
|
+
return async (input: { event: { type: string; properties?: any } }) => {
|
|
363
|
+
if (input.event.type !== "session.idle") return;
|
|
364
|
+
|
|
365
|
+
const sessionID = input.event.properties?.sessionID;
|
|
366
|
+
if (!sessionID) return;
|
|
367
|
+
|
|
368
|
+
if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID)) return;
|
|
369
|
+
|
|
370
|
+
if (getMainSessionId) {
|
|
371
|
+
const mainId = getMainSessionId();
|
|
372
|
+
if (mainId && sessionID !== mainId) return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (idleTimeout) clearTimeout(idleTimeout);
|
|
376
|
+
|
|
377
|
+
idleTimeout = setTimeout(async () => {
|
|
378
|
+
if (isCapturing) return;
|
|
379
|
+
isCapturing = true;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const response = await sdkClient.session.messages({ path: { id: sessionID } });
|
|
383
|
+
if (!response?.data) return;
|
|
384
|
+
|
|
385
|
+
const messages = response.data;
|
|
386
|
+
const conversationMessages: Array<{ role: string; content: string }> = [];
|
|
387
|
+
const newMessageIds: string[] = [];
|
|
388
|
+
let hasNewMessages = false;
|
|
389
|
+
|
|
390
|
+
for (const msg of messages) {
|
|
391
|
+
const msgId = msg.info?.id;
|
|
392
|
+
if (!msgId || processedMessageIds.has(msgId)) continue;
|
|
393
|
+
|
|
394
|
+
// Skip messages created before this plugin instance started
|
|
395
|
+
// (prevents replaying entire session history on restart)
|
|
396
|
+
const msgTime = msg.info?.createdAt ? new Date(msg.info.createdAt).getTime() : 0;
|
|
397
|
+
if (msgTime > 0 && msgTime < pluginStartTime) continue;
|
|
398
|
+
|
|
399
|
+
const role = msg.info?.role;
|
|
400
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
401
|
+
|
|
402
|
+
const textParts = (msg.parts || [])
|
|
403
|
+
.filter((p: any) => p.type === "text" && p.text)
|
|
404
|
+
.map((p: any) => p.text);
|
|
405
|
+
const text = textParts.join("\n").trim();
|
|
406
|
+
if (!text) continue;
|
|
407
|
+
|
|
408
|
+
hasNewMessages = true;
|
|
409
|
+
newMessageIds.push(msgId);
|
|
410
|
+
conversationMessages.push({ role, content: text });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!hasNewMessages || conversationMessages.length === 0) return;
|
|
414
|
+
|
|
415
|
+
if (threshold > 1 && conversationMessages.length < threshold) {
|
|
416
|
+
// Log that we're waiting for more messages
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
await omemClient.sessionIngest(conversationMessages, sessionID);
|
|
422
|
+
for (const id of newMessageIds) {
|
|
423
|
+
processedMessageIds.add(id);
|
|
424
|
+
}
|
|
425
|
+
showToast(tui, "🧠 Memory Sealed", `${conversationMessages.length} dialogues captured · entrusted to the heavens for refinement`, "success");
|
|
426
|
+
} catch (err) {
|
|
427
|
+
showToast(tui, "🔴 Session Capture Failed", String(err).substring(0, 100), "error");
|
|
428
|
+
}
|
|
429
|
+
} catch (err) {
|
|
430
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
431
|
+
showToast(tui, "🔴 Idle Capture Error", errMsg.substring(0, 100), "error");
|
|
432
|
+
} finally {
|
|
433
|
+
isCapturing = false;
|
|
434
|
+
idleTimeout = null;
|
|
435
|
+
}
|
|
436
|
+
}, 10000);
|
|
437
|
+
};
|
|
438
|
+
}
|
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";
|
|
@@ -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,13 +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 }),
|
|
130
|
+
event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode, config.autoCaptureThreshold, () => currentSessionId, isAutoStoreEnabled),
|
|
99
131
|
"shell.env": async (_input: any, output: any) => {
|
|
100
132
|
if (directory) {
|
|
101
133
|
output.env.OMEM_PROJECT_DIR = directory;
|
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;
|