@mingxy/cerebro 1.20.0 → 1.20.2

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.20.0",
3
+ "version": "1.20.2",
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/config.ts CHANGED
@@ -177,7 +177,7 @@ function configLog(message: string, fields?: Record<string, unknown>, level: str
177
177
  if (lvl < CONFIGURED_MIN_LEVEL) return;
178
178
  try {
179
179
  const logDir = join(homedir(), ".config", "cerebro", "logs");
180
- const logPath = join(logDir, "plugin.log");
180
+ const logPath = join(logDir, "cerebro.log");
181
181
  const ts = new Date().toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
182
182
  const parts = [`${level.padEnd(5)} ${ts} service=cerebro ${message}`];
183
183
  if (fields) {
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ import { chatMessageRecallHook, autocontinueHook, compactingHook, sessionIdleHoo
8
8
  import { detectSaveKeyword, detectRecallKeyword, KEYWORD_NUDGE, RECALL_NUDGE } from "./keywords.js";
9
9
  import { getUserTag, getProjectTag } from "./tags.js";
10
10
  import { buildTools } from "./tools.js";
11
- import { logInfo, logDebug, logError } from "./logger.js";
11
+ import { logInfo, logDebug, logError, setOpencodeClient } from "./logger.js";
12
12
  import { loadPluginConfig, resolveAgentPolicy } from "./config.js";
13
13
  import { checkAndUpdate } from "./updater.js";
14
14
  import { startWebServer, stopWebServer, type WebServerHandle } from "./web-server.js";
@@ -66,6 +66,8 @@ const OmemPlugin: Plugin = async (input) => {
66
66
  const config = loadPluginConfig(overrides as any);
67
67
  const toast = createToast(config);
68
68
 
69
+ setOpencodeClient(client);
70
+
69
71
  const cerebroClient = new CerebroClient(config.connection.apiUrl, config.connection.apiKey, config);
70
72
 
71
73
  try {
package/src/logger.ts CHANGED
@@ -33,8 +33,12 @@ function getConfig() {
33
33
  return cachedConfig;
34
34
  }
35
35
 
36
- function getLogFilePath(): string {
37
- return join(getConfig().logging.logDir, "plugin.log");
36
+ function getLogFilePath(sessionId?: string): string {
37
+ const base = getConfig().logging.logDir;
38
+ if (sessionId) {
39
+ return join(base, `cerebro-${sessionId}.log`);
40
+ }
41
+ return join(base, "cerebro.log");
38
42
  }
39
43
 
40
44
  function getMinLevel(): number {
@@ -109,7 +113,8 @@ function writeLog(level: string, message: string, fields?: Record<string, unknow
109
113
  if (lvl < getMinLevel()) return;
110
114
 
111
115
  const cfg = getConfig();
112
- const logFile = getLogFilePath();
116
+ const sid = (fields?.sessionId ?? fields?.sessionID) as string | undefined;
117
+ const logFile = getLogFilePath(sid);
113
118
  ensureLogDir(cfg.logging.logDir);
114
119
 
115
120
  const now = new Date();
package/src/updater.ts CHANGED
@@ -5,6 +5,7 @@ import { join, dirname } from "node:path";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { openSync, closeSync, unlinkSync, statSync, writeSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
7
7
  import { loadPluginConfig } from "./config.js";
8
+ import { logInfo, logDebug, logError } from "./logger.js";
8
9
 
9
10
  const execFileAsync = promisify(execFile);
10
11
  const require = createRequire(import.meta.url);
@@ -14,8 +15,12 @@ const require = createRequire(import.meta.url);
14
15
  async function getLatestVersion(): Promise<string | null> {
15
16
  try {
16
17
  const { stdout } = await execFileAsync("npm", ["view", "@mingxy/cerebro", "version"], { timeout: 10000 });
18
+ logDebug("updater: fetched latest version", { version: stdout.trim() });
17
19
  return stdout.trim();
18
- } catch { return null; }
20
+ } catch {
21
+ logError("updater: failed to fetch latest version");
22
+ return null;
23
+ }
19
24
  }
20
25
 
21
26
  // ── Semantic version comparison ─────────────────────────────────────
@@ -45,6 +50,7 @@ function getInstallDir(): string {
45
50
 
46
51
  async function installUpdate(targetDir: string): Promise<boolean> {
47
52
  const tmpDir = mkdtempSync(join(tmpdir(), "cerebro-update-"));
53
+ logInfo("updater: downloading update", { targetDir, tmpDir });
48
54
  try {
49
55
  // 1. Download tgz
50
56
  await execFileAsync("npm", ["pack", "@mingxy/cerebro@latest", "--pack-destination", tmpDir], { timeout: 60000 });
@@ -52,13 +58,18 @@ async function installUpdate(targetDir: string): Promise<boolean> {
52
58
  // 2. Find the tgz file
53
59
  const files = readdirSync(tmpDir);
54
60
  const tgz = files.find(f => f.endsWith(".tgz"));
55
- if (!tgz) return false;
61
+ if (!tgz) {
62
+ logError("updater: no tgz found after npm pack", { tmpDir, files: files.join(",") });
63
+ return false;
64
+ }
56
65
 
57
66
  // 3. Extract to target dir (strip the "package/" prefix from tgz)
58
67
  await execFileAsync("tar", ["-xzf", join(tmpDir, tgz), "-C", targetDir, "--strip-components=1", "--no-same-owner", "--no-same-permissions"], { timeout: 30000 });
59
68
 
69
+ logInfo("updater: update installed successfully", { targetDir, tgz });
60
70
  return true;
61
- } catch {
71
+ } catch (err) {
72
+ logError("updater: install failed", { error: err instanceof Error ? err.message : String(err) });
62
73
  return false;
63
74
  } finally {
64
75
  try { rmSync(tmpDir, { recursive: true }); } catch {}
@@ -78,13 +89,18 @@ function acquireLock(): boolean {
78
89
  const s = statSync(LOCK_FILE);
79
90
  if (Date.now() - s.mtimeMs > STALE_LOCK_MS) {
80
91
  unlinkSync(LOCK_FILE);
92
+ logDebug("updater: cleaned stale lock", { ageMs: Date.now() - s.mtimeMs });
81
93
  }
82
94
  } catch { /* lock file doesn't exist, normal */ }
83
95
 
84
96
  lockFd = openSync(LOCK_FILE, "wx");
85
97
  writeSync(lockFd, String(process.pid));
98
+ logDebug("updater: lock acquired", { pid: process.pid });
86
99
  return true;
87
- } catch { return false; }
100
+ } catch {
101
+ logDebug("updater: lock acquisition failed (another process updating?)");
102
+ return false;
103
+ }
88
104
  }
89
105
 
90
106
  function releaseLock(): void {
@@ -92,6 +108,7 @@ function releaseLock(): void {
92
108
  try { closeSync(lockFd); } catch {}
93
109
  lockFd = null;
94
110
  try { unlinkSync(LOCK_FILE); } catch {}
111
+ logDebug("updater: lock released");
95
112
  }
96
113
  }
97
114
 
@@ -99,25 +116,38 @@ function releaseLock(): void {
99
116
 
100
117
  export async function checkAndUpdate(tui: any, currentVersion: string): Promise<void> {
101
118
  const config = loadPluginConfig();
102
- if (!config.autoUpdate) return;
119
+ if (!config.autoUpdate) {
120
+ logDebug("updater: autoUpdate disabled, skipping");
121
+ return;
122
+ }
123
+
124
+ logInfo("updater: checking for updates", { currentVersion });
103
125
 
104
126
  const latest = await getLatestVersion();
105
127
  if (!latest) return;
106
128
 
107
- if (compareVersions(latest, currentVersion) <= 0) return; // already up to date
129
+ if (compareVersions(latest, currentVersion) <= 0) {
130
+ logInfo("updater: already up to date", { currentVersion, latest });
131
+ return;
132
+ }
133
+
134
+ logInfo("updater: new version available", { currentVersion, latest });
108
135
 
109
- if (!acquireLock()) return; // another process is updating
136
+ if (!acquireLock()) return;
110
137
 
111
138
  try {
112
139
  const targetDir = getInstallDir();
113
140
  const success = await installUpdate(targetDir);
114
141
 
115
142
  if (success) {
143
+ logInfo("updater: update completed", { from: currentVersion, to: latest });
116
144
  try {
117
145
  tui?.showToast?.({
118
146
  body: { message: `Cerebro updated to v${latest} — restart opencode to apply`, variant: "info" }
119
147
  });
120
148
  } catch {}
149
+ } else {
150
+ logError("updater: update failed", { targetDir });
121
151
  }
122
152
  } finally {
123
153
  releaseLock();
@@ -10,6 +10,32 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import * as os from "node:os";
12
12
 
13
+ // ── Lightweight file logger for child process ──────────────────────────
14
+ // Cannot import logger.ts (no opencodeClient + circular dependency risk).
15
+ // Writes to cerebro-web-child.log in the configured log directory.
16
+
17
+ const CHILD_LOG_FILE = path.join(
18
+ process.env.OMEM_LOG_DIR || path.join(os.homedir(), ".config", "cerebro", "logs"),
19
+ "cerebro-web-child.log",
20
+ );
21
+
22
+ function childLog(level: string, message: string, fields?: Record<string, unknown>): void {
23
+ try {
24
+ const dir = path.dirname(CHILD_LOG_FILE);
25
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
26
+ const now = new Date();
27
+ const ts = now.toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
28
+ const parts = [`${level.padEnd(5)} ${ts} service=cerebro-web-child`];
29
+ if (fields) {
30
+ for (const [k, v] of Object.entries(fields)) {
31
+ parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
32
+ }
33
+ }
34
+ parts.push(message);
35
+ fs.appendFileSync(CHILD_LOG_FILE, parts.join(" ") + "\n");
36
+ } catch { /* best effort */ }
37
+ }
38
+
13
39
  // ── Types ────────────────────────────────────────────────────────────────
14
40
 
15
41
  interface ChildConfig {
@@ -138,14 +164,14 @@ function startServer(config: ChildConfig): void {
138
164
  try {
139
165
  fs.writeFileSync(pidFilePath, String(process.pid));
140
166
  } catch {
141
- console.error("[cerebro:web-child] Failed to write PID file");
167
+ childLog("ERROR", "Failed to write PID file");
142
168
  }
143
169
 
144
170
  // 初始 touch 心跳文件
145
171
  try {
146
172
  fs.writeFileSync(heartbeatFilePath, "");
147
173
  } catch {
148
- console.error("[cerebro:web-child] Failed to create heartbeat file");
174
+ childLog("ERROR", "Failed to create heartbeat file");
149
175
  }
150
176
 
151
177
  server = http.createServer(
@@ -199,15 +225,13 @@ function startServer(config: ChildConfig): void {
199
225
  );
200
226
 
201
227
  server.on("error", (err: NodeJS.ErrnoException) => {
202
- console.error(`[cerebro:web-child] Server error: ${err.message}`);
228
+ childLog("ERROR", "Server error", { error: err.message });
203
229
  process.send?.({ type: "error", message: err.message });
204
230
  cleanup();
205
231
  });
206
232
 
207
233
  server.listen(port, "127.0.0.1", () => {
208
- console.log(
209
- `[cerebro:web-child] Server listening on http://localhost:${port}`,
210
- );
234
+ childLog("INFO", "Server listening", { port });
211
235
  process.send?.({ type: "ready", port });
212
236
  });
213
237
 
@@ -216,9 +240,7 @@ function startServer(config: ChildConfig): void {
216
240
  try {
217
241
  const stat = fs.statSync(heartbeatFilePath);
218
242
  if (Date.now() - stat.mtimeMs > 60_000) {
219
- console.log(
220
- "[cerebro:web-child] Heartbeat expired, shutting down",
221
- );
243
+ childLog("INFO", "Heartbeat expired, shutting down");
222
244
  cleanup();
223
245
  }
224
246
  } catch {
package/src/web-server.ts CHANGED
@@ -10,6 +10,7 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import * as os from "node:os";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { logInfo, logWarn, logError } from "./logger.js";
13
14
 
14
15
  const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = path.dirname(__filename);
@@ -75,7 +76,7 @@ export async function startWebServer(
75
76
 
76
77
  // ── Step 1: 检查端口是否已有 cerebro server ──
77
78
  if (await probeExistingServer(port)) {
78
- console.log(`[cerebro:web] Reusing existing server on port ${port}`);
79
+ logInfo("web-server: reusing existing server", { port });
79
80
  return createHandle(port, heartbeatFilePath);
80
81
  }
81
82
 
@@ -87,9 +88,7 @@ export async function startWebServer(
87
88
  // 有其他进程正在 fork 或运行,等待 200ms 后重试
88
89
  await new Promise((r) => setTimeout(r, 200));
89
90
  if (await probeExistingServer(port)) {
90
- console.log(
91
- `[cerebro:web] Reusing server after PID check on port ${port}`,
92
- );
91
+ logInfo("web-server: reusing server after PID check", { port });
93
92
  return createHandle(port, heartbeatFilePath);
94
93
  }
95
94
  } else {
@@ -101,15 +100,11 @@ export async function startWebServer(
101
100
  // ── Step 3: 检查 web 目录 ──
102
101
  const webDir = path.resolve(__dirname, "..", "web");
103
102
  if (!fs.existsSync(webDir)) {
104
- console.warn(
105
- `[cerebro:web] Web directory not found: ${webDir}, skipping server start`,
106
- );
103
+ logWarn("web-server: web directory not found, skipping", { webDir });
107
104
  return null;
108
105
  }
109
106
  if (!fs.existsSync(path.join(webDir, "index.html"))) {
110
- console.warn(
111
- `[cerebro:web] index.html not found in ${webDir}, skipping server start`,
112
- );
107
+ logWarn("web-server: index.html not found, skipping", { webDir });
113
108
  return null;
114
109
  }
115
110
 
@@ -119,11 +114,18 @@ export async function startWebServer(
119
114
  } catch { /* ignore */ }
120
115
 
121
116
  // ── Step 5: Fork 子进程 ──
122
- const childPath = path.resolve(__dirname, "web-server-child.js");
117
+ // TS runtime: .ts exists; compiled: .js exists
118
+ const childTs = path.resolve(__dirname, "web-server-child.ts");
119
+ const childJs = path.resolve(__dirname, "web-server-child.js");
120
+ const childPath = fs.existsSync(childTs) ? childTs : childJs;
123
121
 
122
+ // Use the same executor as the parent process (tsx, node, etc.)
123
+ // so the child can run .ts files. Also inherit execArgv (--import tsx, etc.)
124
124
  const child = fork(childPath, [], {
125
125
  detached: true,
126
126
  stdio: ["pipe", "pipe", "pipe", "ipc"],
127
+ execPath: process.execPath,
128
+ execArgv: process.execArgv,
127
129
  });
128
130
 
129
131
  // Drain stdout/stderr to prevent pipe buffer from blocking the child
@@ -161,17 +163,13 @@ export async function startWebServer(
161
163
  });
162
164
 
163
165
  if (!ready) {
164
- console.warn(
165
- `[cerebro:web] Failed to start web server child process on port ${port}`,
166
- );
166
+ logError("web-server: failed to start child process", { port });
167
167
  try { child.kill(); } catch { /* ignore */ }
168
168
  try { fs.unlinkSync(pidFilePath); } catch { /* ignore */ }
169
169
  return null;
170
170
  }
171
171
 
172
- console.log(
173
- `[cerebro:web] Web server child process started on port ${port}`,
174
- );
172
+ logInfo("web-server: child process started", { port, pid: child.pid });
175
173
  return createHandle(port, heartbeatFilePath);
176
174
  }
177
175