@mingxy/cerebro 1.16.9 → 1.17.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 +6 -1
- package/src/config.ts +19 -4
- package/src/hooks.ts +25 -13
- package/src/index.ts +30 -0
- package/src/web-server.ts +180 -0
- package/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/web/assets/index-B-0ucKQF.js +119 -0
- package/web/assets/index-Dxd5Um3O.css +1 -0
- package/web/favicon.svg +1 -0
- package/web/icons.svg +24 -0
- package/web/index.html +15 -0
- package/.omo/evidence/f1-verification.txt +0 -44
- package/INJECTION_FLOW.md +0 -434
- package/cerebro.example.jsonc +0 -72
- package/dist/client.d.ts +0 -165
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -222
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -46
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -201
- package/dist/config.js.map +0 -1
- package/dist/hooks.d.ts +0 -41
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -1066
- package/dist/hooks.js.map +0 -1
- package/dist/index.d.ts +0 -11
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -118
- package/dist/index.js.map +0 -1
- package/dist/keywords.d.ts +0 -3
- package/dist/keywords.d.ts.map +0 -1
- package/dist/keywords.js +0 -21
- package/dist/keywords.js.map +0 -1
- package/dist/logger.d.ts +0 -5
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -62
- package/dist/logger.js.map +0 -1
- package/dist/privacy.d.ts +0 -3
- package/dist/privacy.d.ts.map +0 -1
- package/dist/privacy.js +0 -10
- package/dist/privacy.js.map +0 -1
- package/dist/tags.d.ts +0 -3
- package/dist/tags.d.ts.map +0 -1
- package/dist/tags.js +0 -13
- package/dist/tags.js.map +0 -1
- package/dist/tools.d.ts +0 -209
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -344
- package/dist/tools.js.map +0 -1
- package/dist/tui.d.ts +0 -7
- package/dist/tui.d.ts.map +0 -1
- package/dist/tui.js +0 -63
- package/dist/tui.js.map +0 -1
- package/mingxy-omem-0.1.6.tgz +0 -0
- package/schema.json +0 -225
- package/tsconfig.json +0 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mingxy/cerebro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
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",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"server",
|
|
19
19
|
"tui"
|
|
20
20
|
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build:web": "bash ../../scripts/build-plugin-web.sh",
|
|
23
|
+
"prepublishOnly": "npm run build:web && npx tsc --noEmit"
|
|
24
|
+
},
|
|
21
25
|
"keywords": [
|
|
22
26
|
"opencode",
|
|
23
27
|
"memory",
|
|
@@ -28,6 +32,7 @@
|
|
|
28
32
|
"project-isolation"
|
|
29
33
|
],
|
|
30
34
|
"author": "mingxy-cerebro",
|
|
35
|
+
"files": ["src/", "web/"],
|
|
31
36
|
"license": "Apache-2.0",
|
|
32
37
|
"homepage": "https://github.com/mingxy-cerebro/cerebro-server",
|
|
33
38
|
"repository": {
|
package/src/config.ts
CHANGED
|
@@ -38,6 +38,10 @@ export interface OmemPluginConfig {
|
|
|
38
38
|
ui: {
|
|
39
39
|
toastDelayMs: number;
|
|
40
40
|
};
|
|
41
|
+
web?: {
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
port?: number;
|
|
44
|
+
};
|
|
41
45
|
profile?: {
|
|
42
46
|
ttlMs?: number;
|
|
43
47
|
};
|
|
@@ -81,6 +85,9 @@ const DEFAULTS: OmemPluginConfig = {
|
|
|
81
85
|
ui: {
|
|
82
86
|
toastDelayMs: 7000,
|
|
83
87
|
},
|
|
88
|
+
web: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
},
|
|
84
91
|
profile: {
|
|
85
92
|
ttlMs: 300000,
|
|
86
93
|
},
|
|
@@ -164,6 +171,7 @@ function deepMerge(base: OmemPluginConfig, overrides: Partial<OmemPluginConfig>)
|
|
|
164
171
|
logging: { ...base.logging, ...overrides.logging },
|
|
165
172
|
ui: { ...base.ui, ...overrides.ui },
|
|
166
173
|
};
|
|
174
|
+
result.web = { ...base.web!, ...overrides.web };
|
|
167
175
|
result.profile = { ...base.profile!, ...overrides.profile };
|
|
168
176
|
if (overrides.agentMemoryPolicy) result.agentMemoryPolicy = overrides.agentMemoryPolicy;
|
|
169
177
|
if (overrides.defaultPolicy) result.defaultPolicy = overrides.defaultPolicy;
|
|
@@ -227,7 +235,12 @@ export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPlu
|
|
|
227
235
|
configLog("config.json load failed, using defaults", { error: String(e) });
|
|
228
236
|
}
|
|
229
237
|
|
|
230
|
-
// Apply
|
|
238
|
+
// Apply explicit overrides (from opencode.json)
|
|
239
|
+
if (overrides) {
|
|
240
|
+
config = deepMerge(config, overrides);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Apply environment variable overrides last — env vars have highest priority
|
|
231
244
|
if (process.env.OMEM_API_URL) config.connection.apiUrl = process.env.OMEM_API_URL;
|
|
232
245
|
if (process.env.OMEM_API_KEY) config.connection.apiKey = process.env.OMEM_API_KEY;
|
|
233
246
|
if (process.env.OMEM_REQUEST_TIMEOUT_MS) {
|
|
@@ -246,9 +259,11 @@ export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPlu
|
|
|
246
259
|
config.recall.maxRecallResults = parseInt(process.env.OMEM_MAX_RECALL_RESULTS, 10) || DEFAULTS.recall.maxRecallResults;
|
|
247
260
|
}
|
|
248
261
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
262
|
+
if (process.env.OMEM_WEB_ENABLED === "false" || process.env.OMEM_WEB_ENABLED === "0") {
|
|
263
|
+
config.web = { ...config.web!, enabled: false };
|
|
264
|
+
}
|
|
265
|
+
if (process.env.OMEM_LOCAL_PORT) {
|
|
266
|
+
config.web = { ...config.web!, port: parseInt(process.env.OMEM_LOCAL_PORT, 10) || DEFAULTS.web!.port };
|
|
252
267
|
}
|
|
253
268
|
|
|
254
269
|
// Expand ~ to home directory in logDir
|
package/src/hooks.ts
CHANGED
|
@@ -494,24 +494,36 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
494
494
|
},
|
|
495
495
|
): Promise<string | undefined> => {
|
|
496
496
|
try {
|
|
497
|
+
const memoryLookup = new Map<string, SearchResult>();
|
|
498
|
+
if (shouldRecallRes.memories) {
|
|
499
|
+
for (const r of shouldRecallRes.memories) {
|
|
500
|
+
memoryLookup.set(r.memory.id, r);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
497
503
|
const items = clustered
|
|
498
504
|
? [
|
|
499
505
|
...clustered.cluster_summaries.flatMap((cs) =>
|
|
500
|
-
cs.key_memories.map((mem) =>
|
|
506
|
+
cs.key_memories.map((mem) => {
|
|
507
|
+
const sr = memoryLookup.get(mem.id ?? "");
|
|
508
|
+
return {
|
|
509
|
+
memory_id: mem.id ?? "",
|
|
510
|
+
score: cs.relevance_score,
|
|
511
|
+
refine_relevance: sr?.refine_relevance,
|
|
512
|
+
refine_reasoning: sr?.refine_reasoning,
|
|
513
|
+
is_kept: opts.injectedMemoryIds.includes(mem.id ?? ""),
|
|
514
|
+
};
|
|
515
|
+
})
|
|
516
|
+
),
|
|
517
|
+
...clustered.standalone_memories.map((mem) => {
|
|
518
|
+
const sr = memoryLookup.get(mem.id ?? "");
|
|
519
|
+
return {
|
|
501
520
|
memory_id: mem.id ?? "",
|
|
502
|
-
score:
|
|
503
|
-
refine_relevance:
|
|
504
|
-
refine_reasoning:
|
|
521
|
+
score: sr?.score ?? 0,
|
|
522
|
+
refine_relevance: sr?.refine_relevance,
|
|
523
|
+
refine_reasoning: sr?.refine_reasoning,
|
|
505
524
|
is_kept: opts.injectedMemoryIds.includes(mem.id ?? ""),
|
|
506
|
-
}
|
|
507
|
-
),
|
|
508
|
-
...clustered.standalone_memories.map((mem) => ({
|
|
509
|
-
memory_id: mem.id ?? "",
|
|
510
|
-
score: 0,
|
|
511
|
-
refine_relevance: undefined,
|
|
512
|
-
refine_reasoning: undefined,
|
|
513
|
-
is_kept: opts.injectedMemoryIds.includes(mem.id ?? ""),
|
|
514
|
-
})),
|
|
525
|
+
};
|
|
526
|
+
}),
|
|
515
527
|
]
|
|
516
528
|
: [
|
|
517
529
|
...(shouldRecallRes.memories?.map((r) => ({
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,14 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type { Server } from "node:http";
|
|
6
7
|
import { CerebroClient } from "./client.js";
|
|
7
8
|
import { autoRecallHook, autocontinueHook, compactingHook, keywordDetectionHook, sessionIdleHook, showToast as hooksShowToast } from "./hooks.js";
|
|
8
9
|
import { getUserTag, getProjectTag } from "./tags.js";
|
|
9
10
|
import { buildTools } from "./tools.js";
|
|
10
11
|
import { logInfo, logDebug, logError } from "./logger.js";
|
|
11
12
|
import { loadPluginConfig } from "./config.js";
|
|
13
|
+
import { startWebServer, stopWebServer } from "./web-server.js";
|
|
12
14
|
|
|
13
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
16
|
const __dirname = dirname(__filename);
|
|
@@ -103,6 +105,34 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
103
105
|
|
|
104
106
|
const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId, directory);
|
|
105
107
|
|
|
108
|
+
let webServer: Server | null = null;
|
|
109
|
+
const webEnabled = config.web?.enabled !== false;
|
|
110
|
+
if (webEnabled) {
|
|
111
|
+
try {
|
|
112
|
+
webServer = await startWebServer({
|
|
113
|
+
apiUrl: config.connection.apiUrl,
|
|
114
|
+
port: config.web?.port,
|
|
115
|
+
});
|
|
116
|
+
if (webServer) {
|
|
117
|
+
const addr = webServer.address();
|
|
118
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : config.web?.port || 5212;
|
|
119
|
+
hooksShowToast(tui, "🌐 Cerebro Web", `http://localhost:${actualPort}`, "info", 8000);
|
|
120
|
+
logInfo(`Web UI available at http://localhost:${actualPort}`);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logError(`Web server start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const shutdown = async () => {
|
|
128
|
+
if (webServer) {
|
|
129
|
+
await stopWebServer(webServer);
|
|
130
|
+
webServer = null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
process.on("SIGTERM", shutdown);
|
|
134
|
+
process.on("SIGINT", shutdown);
|
|
135
|
+
|
|
106
136
|
return {
|
|
107
137
|
config: async (cfg: any) => {
|
|
108
138
|
cfg.command ??= {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// ── Types ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface WebServerConfig {
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── MIME map ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const MIME_TYPES: Record<string, string> = {
|
|
19
|
+
".html": "text/html; charset=utf-8",
|
|
20
|
+
".js": "application/javascript; charset=utf-8",
|
|
21
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
22
|
+
".css": "text/css; charset=utf-8",
|
|
23
|
+
".json": "application/json; charset=utf-8",
|
|
24
|
+
".svg": "image/svg+xml",
|
|
25
|
+
".png": "image/png",
|
|
26
|
+
".jpg": "image/jpeg",
|
|
27
|
+
".jpeg": "image/jpeg",
|
|
28
|
+
".gif": "image/gif",
|
|
29
|
+
".ico": "image/x-icon",
|
|
30
|
+
".woff": "font/woff",
|
|
31
|
+
".woff2": "font/woff2",
|
|
32
|
+
".ttf": "font/ttf",
|
|
33
|
+
".eot": "application/vnd.ms-fontobject",
|
|
34
|
+
".webp": "image/webp",
|
|
35
|
+
".webmanifest": "application/manifest+json",
|
|
36
|
+
".map": "application/json",
|
|
37
|
+
".txt": "text/plain; charset=utf-8",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function getMimeType(ext: string): string {
|
|
41
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Safe path resolver (prevent traversal) ───────────────────────────────
|
|
45
|
+
|
|
46
|
+
function resolveSafe(baseDir: string, pathname: string): string | null {
|
|
47
|
+
const resolved = path.resolve(baseDir, pathname);
|
|
48
|
+
if (!resolved.startsWith(baseDir + path.sep) && resolved !== baseDir) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Start / Stop ─────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export function startWebServer(config: WebServerConfig): Promise<http.Server | null> {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const webDir = path.resolve(__dirname, "..", "web");
|
|
59
|
+
|
|
60
|
+
// Check web directory exists
|
|
61
|
+
if (!fs.existsSync(webDir)) {
|
|
62
|
+
console.warn(`[cerebro:web] Web directory not found: ${webDir}, skipping server start`);
|
|
63
|
+
resolve(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const indexPath = path.join(webDir, "index.html");
|
|
68
|
+
if (!fs.existsSync(indexPath)) {
|
|
69
|
+
console.warn(`[cerebro:web] index.html not found in ${webDir}, skipping server start`);
|
|
70
|
+
resolve(null);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const port = config.port || parseInt(process.env.OMEM_LOCAL_PORT || "", 10) || 5212;
|
|
75
|
+
|
|
76
|
+
const server = http.createServer(
|
|
77
|
+
(req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
78
|
+
// Only handle GET / HEAD
|
|
79
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
80
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
81
|
+
res.end("Method Not Allowed");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parse URL, strip query string
|
|
86
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
87
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
88
|
+
|
|
89
|
+
// Resolve safe file path
|
|
90
|
+
const safePath = resolveSafe(webDir, pathname);
|
|
91
|
+
|
|
92
|
+
if (!safePath) {
|
|
93
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
94
|
+
res.end("Forbidden");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Try to serve the file directly
|
|
99
|
+
fs.stat(safePath, (statErr, stats) => {
|
|
100
|
+
if (!statErr && stats.isFile()) {
|
|
101
|
+
serveFile(res, safePath, config.apiUrl);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// SPA fallback: serve index.html for non-file paths
|
|
106
|
+
fs.stat(indexPath, (idxErr, idxStats) => {
|
|
107
|
+
if (idxErr || !idxStats.isFile()) {
|
|
108
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
109
|
+
res.end("Not Found");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
serveFile(res, indexPath, config.apiUrl);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
119
|
+
if (err.code === "EADDRINUSE") {
|
|
120
|
+
console.warn(`[cerebro:web] Port ${port} already in use, web server not started`);
|
|
121
|
+
} else {
|
|
122
|
+
console.warn(`[cerebro:web] Server error: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
resolve(null);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
server.listen(port, "127.0.0.1", () => {
|
|
128
|
+
const addr = server.address();
|
|
129
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
130
|
+
console.log(`[cerebro:web] Static server listening on http://localhost:${actualPort}`);
|
|
131
|
+
resolve(server);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── File serving ─────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function serveFile(
|
|
139
|
+
res: http.ServerResponse,
|
|
140
|
+
filePath: string,
|
|
141
|
+
apiUrl: string,
|
|
142
|
+
): void {
|
|
143
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
144
|
+
const contentType = getMimeType(ext);
|
|
145
|
+
|
|
146
|
+
fs.readFile(filePath, (err, data) => {
|
|
147
|
+
if (err) {
|
|
148
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
149
|
+
res.end("Internal Server Error");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let body: Buffer | string = data;
|
|
154
|
+
|
|
155
|
+
// Config injection: replace placeholder in index.html
|
|
156
|
+
if (ext === ".html" && data.includes("__OMEM_API_URL__")) {
|
|
157
|
+
body = data.toString("utf-8").replace(/__OMEM_API_URL__/g, () => apiUrl);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
res.writeHead(200, {
|
|
161
|
+
"Content-Type": contentType,
|
|
162
|
+
"Cache-Control": ext === ".html" ? "no-cache, no-store, must-revalidate" : "public, max-age=86400",
|
|
163
|
+
});
|
|
164
|
+
res.end(body);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Graceful shutdown ────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
export function stopWebServer(server: http.Server): Promise<void> {
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
server.closeAllConnections?.();
|
|
173
|
+
const timer = setTimeout(resolve, 3000);
|
|
174
|
+
server.close(() => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
console.log("[cerebro:web] Server stopped");
|
|
177
|
+
resolve();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|