@mingxy/cerebro 1.16.10 → 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.
Files changed (58) hide show
  1. package/package.json +6 -1
  2. package/src/config.ts +19 -4
  3. package/src/index.ts +30 -0
  4. package/src/web-server.ts +180 -0
  5. package/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  6. package/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  7. package/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  8. package/web/assets/index-B-0ucKQF.js +119 -0
  9. package/web/assets/index-Dxd5Um3O.css +1 -0
  10. package/web/favicon.svg +1 -0
  11. package/web/icons.svg +24 -0
  12. package/web/index.html +15 -0
  13. package/.omo/evidence/f1-verification.txt +0 -44
  14. package/INJECTION_FLOW.md +0 -434
  15. package/cerebro.example.jsonc +0 -72
  16. package/dist/client.d.ts +0 -165
  17. package/dist/client.d.ts.map +0 -1
  18. package/dist/client.js +0 -222
  19. package/dist/client.js.map +0 -1
  20. package/dist/config.d.ts +0 -46
  21. package/dist/config.d.ts.map +0 -1
  22. package/dist/config.js +0 -201
  23. package/dist/config.js.map +0 -1
  24. package/dist/hooks.d.ts +0 -41
  25. package/dist/hooks.d.ts.map +0 -1
  26. package/dist/hooks.js +0 -1078
  27. package/dist/hooks.js.map +0 -1
  28. package/dist/index.d.ts +0 -11
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/index.js +0 -118
  31. package/dist/index.js.map +0 -1
  32. package/dist/keywords.d.ts +0 -3
  33. package/dist/keywords.d.ts.map +0 -1
  34. package/dist/keywords.js +0 -21
  35. package/dist/keywords.js.map +0 -1
  36. package/dist/logger.d.ts +0 -5
  37. package/dist/logger.d.ts.map +0 -1
  38. package/dist/logger.js +0 -62
  39. package/dist/logger.js.map +0 -1
  40. package/dist/privacy.d.ts +0 -3
  41. package/dist/privacy.d.ts.map +0 -1
  42. package/dist/privacy.js +0 -10
  43. package/dist/privacy.js.map +0 -1
  44. package/dist/tags.d.ts +0 -3
  45. package/dist/tags.d.ts.map +0 -1
  46. package/dist/tags.js +0 -13
  47. package/dist/tags.js.map +0 -1
  48. package/dist/tools.d.ts +0 -209
  49. package/dist/tools.d.ts.map +0 -1
  50. package/dist/tools.js +0 -344
  51. package/dist/tools.js.map +0 -1
  52. package/dist/tui.d.ts +0 -7
  53. package/dist/tui.d.ts.map +0 -1
  54. package/dist/tui.js +0 -63
  55. package/dist/tui.js.map +0 -1
  56. package/mingxy-omem-0.1.6.tgz +0 -0
  57. package/schema.json +0 -225
  58. package/tsconfig.json +0 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/cerebro",
3
- "version": "1.16.10",
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 environment variable overrides (flat OMEM_* → nested paths)
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
- // Apply explicit overrides (from opencode.json)
250
- if (overrides) {
251
- config = deepMerge(config, overrides);
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/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
+ }