@polderlabs/bizar 4.5.1 → 4.7.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 (94) hide show
  1. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  2. package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
  3. package/bizar-dash/dist/assets/main-DGGq-iZI.js +361 -0
  4. package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
  5. package/bizar-dash/dist/assets/{mobile-O6ANdD4W.js → mobile-CWqPoGaT.js} +18 -19
  6. package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js → mobile-i4Uv9eW8.js} +1 -2
  8. package/bizar-dash/dist/assets/{mobile-DQLFCjwJ.js.map → mobile-i4Uv9eW8.js.map} +1 -1
  9. package/bizar-dash/dist/index.html +3 -3
  10. package/bizar-dash/dist/mobile.html +2 -2
  11. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  12. package/bizar-dash/src/server/headroom.mjs +37 -35
  13. package/bizar-dash/src/server/logger.mjs +71 -0
  14. package/bizar-dash/src/server/memory-lightrag.mjs +84 -37
  15. package/bizar-dash/src/server/metrics.mjs +193 -0
  16. package/bizar-dash/src/server/mods-loader.mjs +13 -5
  17. package/bizar-dash/src/server/providers-store.mjs +73 -1
  18. package/bizar-dash/src/server/routes/chat.mjs +41 -0
  19. package/bizar-dash/src/server/routes/lightrag.mjs +7 -2
  20. package/bizar-dash/src/server/routes/memory.mjs +13 -4
  21. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  22. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  23. package/bizar-dash/src/server/schedules-runner.mjs +4 -4
  24. package/bizar-dash/src/server/server.mjs +101 -37
  25. package/bizar-dash/src/server/watcher.mjs +2 -2
  26. package/bizar-dash/src/web/App.tsx +40 -6
  27. package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
  28. package/bizar-dash/src/web/components/Toast.tsx +1 -1
  29. package/bizar-dash/src/web/components/Topbar.tsx +1 -1
  30. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  31. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  32. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  33. package/bizar-dash/src/web/lib/api.ts +12 -11
  34. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  35. package/bizar-dash/src/web/locales/en.json +52 -0
  36. package/bizar-dash/src/web/main.tsx +5 -0
  37. package/bizar-dash/src/web/styles/chat.css +2 -0
  38. package/bizar-dash/src/web/styles/main.css +52 -23
  39. package/bizar-dash/src/web/styles/tasks.css +2 -0
  40. package/bizar-dash/src/web/views/Activity.tsx +24 -17
  41. package/bizar-dash/src/web/views/History.tsx +90 -75
  42. package/bizar-dash/src/web/views/Memory.tsx +4 -3
  43. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
  44. package/bizar-dash/src/web/views/Overview.tsx +15 -10
  45. package/bizar-dash/src/web/views/Settings.tsx +9 -2
  46. package/bizar-dash/src/web/views/Skills.tsx +3 -2
  47. package/bizar-dash/src/web/views/Tasks.tsx +4 -3
  48. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +8 -6
  49. package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
  50. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  51. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  52. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  53. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  54. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  55. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  56. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  57. package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
  58. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  59. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  60. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  61. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  62. package/bizar-dash/tests/logger.test.mjs +207 -0
  63. package/bizar-dash/tests/metrics.test.mjs +183 -0
  64. package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
  65. package/bizar-dash/tests/setup.ts +7 -0
  66. package/bizar-dash/vitest.config.ts +13 -0
  67. package/cli/artifact-cli.mjs +605 -0
  68. package/cli/artifact-render.mjs +621 -0
  69. package/cli/artifact-server.mjs +847 -0
  70. package/cli/artifact.mjs +38 -2089
  71. package/cli/bg.mjs +5 -13
  72. package/cli/bin.mjs +170 -1348
  73. package/cli/commands/artifact.mjs +20 -0
  74. package/cli/commands/dash.mjs +160 -0
  75. package/cli/commands/headroom.mjs +204 -0
  76. package/cli/commands/install.mjs +169 -0
  77. package/cli/commands/memory.mjs +25 -0
  78. package/cli/commands/minimax.mjs +265 -0
  79. package/cli/commands/mod.mjs +185 -0
  80. package/cli/commands/service.mjs +65 -0
  81. package/cli/commands/usage.mjs +109 -0
  82. package/cli/commands/util.mjs +306 -0
  83. package/cli/doctor.mjs +6 -16
  84. package/cli/memory.mjs +65 -21
  85. package/cli/provision.mjs +10 -15
  86. package/cli/service-controller.mjs +1 -11
  87. package/cli/service.mjs +1 -11
  88. package/cli/utils.mjs +41 -1
  89. package/install.sh +1 -2
  90. package/package.json +8 -3
  91. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +0 -1
  92. package/bizar-dash/dist/assets/main-usWhlPWa.js +0 -362
  93. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +0 -1
  94. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +0 -1
@@ -1,2 +1 @@
1
- import{al as r,j as o,r as e,am as n}from"./mobile-O6ANdD4W.js";const t=document.getElementById("root");if(!t)throw new Error("Root element #root not found");r(t).render(o.jsx(e.StrictMode,{children:o.jsx(n,{})}));
2
- //# sourceMappingURL=mobile-DQLFCjwJ.js.map
1
+ import{am as r,j as o,r as e,an as n}from"./mobile-CWqPoGaT.js";const t=document.getElementById("root");if(!t)throw new Error("Root element #root not found");r(t).render(o.jsx(e.StrictMode,{children:o.jsx(n,{})}));
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-DQLFCjwJ.js","sources":["../../src/web/mobile.tsx"],"sourcesContent":["// src/mobile.tsx — entry point for mobile dashboard.\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { MobileApp } from './MobileApp';\nimport './styles/mobile.css';\n\nconst root = document.getElementById('root');\nif (!root) throw new Error('Root element #root not found');\ncreateRoot(root).render(\n <StrictMode>\n <MobileApp />\n </StrictMode>,\n);\n"],"names":["root","createRoot","jsx","StrictMode","MobileApp"],"mappings":"gEAMA,MAAMA,EAAO,SAAS,eAAe,MAAM,EAC3C,GAAI,CAACA,EAAM,MAAM,IAAI,MAAM,8BAA8B,EACzDC,EAAWD,CAAI,EAAE,OACfE,EAAAA,IAACC,EAAAA,WAAA,CACC,SAAAD,EAAAA,IAACE,EAAA,CAAA,CAAU,CAAA,CACb,CACF"}
1
+ {"version":3,"file":"mobile-i4Uv9eW8.js","sources":["../../src/web/mobile.tsx"],"sourcesContent":["// src/mobile.tsx — entry point for mobile dashboard.\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { MobileApp } from './MobileApp';\nimport './styles/mobile.css';\n\nconst root = document.getElementById('root');\nif (!root) throw new Error('Root element #root not found');\ncreateRoot(root).render(\n <StrictMode>\n <MobileApp />\n </StrictMode>,\n);\n"],"names":["root","createRoot","jsx","StrictMode","MobileApp"],"mappings":"gEAMA,MAAMA,EAAO,SAAS,eAAe,MAAM,EAC3C,GAAI,CAACA,EAAM,MAAM,IAAI,MAAM,8BAA8B,EACzDC,EAAWD,CAAI,EAAE,OACfE,EAAAA,IAACC,EAAAA,WAAA,CACC,SAAAD,EAAAA,IAACE,EAAA,CAAA,CAAU,CAAA,CACb,CACF"}
@@ -29,10 +29,10 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-usWhlPWa.js"></script>
33
- <link rel="modulepreload" crossorigin href="/assets/mobile-O6ANdD4W.js">
32
+ <script type="module" crossorigin src="/assets/main-DGGq-iZI.js"></script>
33
+ <link rel="modulepreload" crossorigin href="/assets/mobile-CWqPoGaT.js">
34
34
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
35
- <link rel="stylesheet" crossorigin href="/assets/main-eWZ4NlCL.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/main-DAlLdW8I.css">
36
36
  </head>
37
37
  <body>
38
38
  <div id="root"></div>
@@ -12,8 +12,8 @@
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
14
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
15
- <script type="module" crossorigin src="/assets/mobile-DQLFCjwJ.js"></script>
16
- <link rel="modulepreload" crossorigin href="/assets/mobile-O6ANdD4W.js">
15
+ <script type="module" crossorigin src="/assets/mobile-i4Uv9eW8.js"></script>
16
+ <link rel="modulepreload" crossorigin href="/assets/mobile-CWqPoGaT.js">
17
17
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
18
18
  </head>
19
19
  <body>
@@ -0,0 +1 @@
1
+ {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":20.207488999999953,"failed":false}],[":tests/components/Button.test.tsx",{"duration":65.99081000000001,"failed":false}],[":tests/components/Card.test.tsx",{"duration":24.894168999999977,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":91.24809300000004,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":112.40757200000002,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":113.472669,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":3.547411000000011,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":25.80152300000003,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":74.72992599999998,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":43.51148800000004,"failed":false}]]}
@@ -115,42 +115,44 @@ async function findProxyPid(port) {
115
115
  }
116
116
  } catch { /* ignore */ }
117
117
 
118
- // Try to find process listening on the port via /proc/net/tcp (Linux)
118
+ // Try to find process listening on the port via /proc/net/tcp (Linux only).
119
+ // On macOS/Windows /proc does not exist — return null early to avoid noisy errors.
120
+ if (process.platform !== 'linux') {
121
+ return null;
122
+ }
119
123
  try {
120
- if (process.platform === 'linux') {
121
- const netTcp = readFileSync('/proc/net/tcp', 'utf8');
122
- const portHex = port.toString(16).toUpperCase().padStart(4, '0');
123
- const lines = netTcp.split('\n');
124
- for (const line of lines.slice(1)) {
125
- // Format: sl local_address rem_address st tx_queue rx_queue ... inode
126
- const parts = line.trim().split(/\s+/);
127
- if (parts.length < 10) continue;
128
- const local = parts[1];
129
- const localPortHex = local.split(':')[1];
130
- if (localPortHex?.toUpperCase() === portHex) {
131
- // Try to get PID from the socket inode
132
- const inode = parts[9];
133
- if (inode && inode !== '0') {
134
- // Scan /proc/*/fd/* for socket inodes
135
- const { readdirSync, readlinkSync } = await import('node:fs');
136
- const procDir = '/proc';
137
- const pids = readdirSync(procDir).filter(
138
- (n) => /^\d+$/.test(n) && !isNaN(parseInt(n, 10)),
139
- );
140
- for (const pid of pids) {
141
- try {
142
- const fdDir = join(procDir, pid, 'fd');
143
- const fds = readdirSync(fdDir);
144
- for (const fd of fds) {
145
- try {
146
- const link = readlinkSync(join(fdDir, fd));
147
- if (link.includes(`socket:[${inode}]`)) {
148
- return parseInt(pid, 10);
149
- }
150
- } catch { /* skip */ }
151
- }
152
- } catch { /* skip */ }
153
- }
124
+ const netTcp = readFileSync('/proc/net/tcp', 'utf8');
125
+ const portHex = port.toString(16).toUpperCase().padStart(4, '0');
126
+ const lines = netTcp.split('\n');
127
+ for (const line of lines.slice(1)) {
128
+ // Format: sl local_address rem_address st tx_queue rx_queue ... inode
129
+ const parts = line.trim().split(/\s+/);
130
+ if (parts.length < 10) continue;
131
+ const local = parts[1];
132
+ const localPortHex = local.split(':')[1];
133
+ if (localPortHex?.toUpperCase() === portHex) {
134
+ // Try to get PID from the socket inode
135
+ const inode = parts[9];
136
+ if (inode && inode !== '0') {
137
+ // Scan /proc/*/fd/* for socket inodes
138
+ const { readdirSync, readlinkSync } = await import('node:fs');
139
+ const procDir = '/proc';
140
+ const pids = readdirSync(procDir).filter(
141
+ (n) => /^\d+$/.test(n) && !isNaN(parseInt(n, 10)),
142
+ );
143
+ for (const pid of pids) {
144
+ try {
145
+ const fdDir = join(procDir, pid, 'fd');
146
+ const fds = readdirSync(fdDir);
147
+ for (const fd of fds) {
148
+ try {
149
+ const link = readlinkSync(join(fdDir, fd));
150
+ if (link.includes(`socket:[${inode}]`)) {
151
+ return parseInt(pid, 10);
152
+ }
153
+ } catch { /* skip */ }
154
+ }
155
+ } catch { /* skip */ }
154
156
  }
155
157
  }
156
158
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * src/server/logger.mjs
3
+ *
4
+ * v4.7.0 — Structured JSON logger with level filtering.
5
+ *
6
+ * Replaces ad-hoc `console.log('[module] message')` calls across the
7
+ * server with a single, level-gated logger that emits one JSON object
8
+ * per line. Output goes to the matching console.* stream (log/warn/
9
+ * error) so existing terminal capture keeps working, but the body is
10
+ * machine-parseable.
11
+ *
12
+ * Level selection: BIZAR_LOG_LEVEL env var (debug | info | warn | error).
13
+ * Default `info` matches the existing behaviour — everything but debug
14
+ * is visible. Set `debug` to surface verbose tracing during development.
15
+ *
16
+ * Usage:
17
+ * import { info, warn, error, child } from './logger.mjs';
18
+ * info('server started', { port: 4317 });
19
+ * warn('rate limited', { module: 'lightrag', err: e.message });
20
+ * const log = child({ module: 'overview' });
21
+ * log.info('snapshot built', { size: 1234 });
22
+ */
23
+
24
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
25
+ const ENV_LEVEL = process.env.BIZAR_LOG_LEVEL;
26
+ const currentLevel = LEVELS[ENV_LEVEL] !== undefined ? ENV_LEVEL : 'info';
27
+
28
+ function format(level, msg, ctx) {
29
+ const entry = {
30
+ ts: new Date().toISOString(),
31
+ level,
32
+ msg,
33
+ ...(ctx && typeof ctx === 'object' ? ctx : {}),
34
+ };
35
+ return JSON.stringify(entry);
36
+ }
37
+
38
+ function shouldLog(level) {
39
+ return LEVELS[currentLevel] <= LEVELS[level];
40
+ }
41
+
42
+ export function debug(msg, ctx) {
43
+ if (shouldLog('debug')) console.log(format('debug', msg, ctx));
44
+ }
45
+
46
+ export function info(msg, ctx) {
47
+ if (shouldLog('info')) console.log(format('info', msg, ctx));
48
+ }
49
+
50
+ export function warn(msg, ctx) {
51
+ if (shouldLog('warn')) console.warn(format('warn', msg, ctx));
52
+ }
53
+
54
+ export function error(msg, ctx) {
55
+ if (shouldLog('error')) console.error(format('error', msg, ctx));
56
+ }
57
+
58
+ /**
59
+ * Build a child logger that binds a context object to every log call.
60
+ * Useful for tagging a series of related entries with a module/session id.
61
+ */
62
+ export function child(ctx) {
63
+ return {
64
+ debug: (m, c) => debug(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
65
+ info: (m, c) => info(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
66
+ warn: (m, c) => warn(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
67
+ error: (m, c) => error(m, { ...ctx, ...(c && typeof c === 'object' ? c : {}) }),
68
+ };
69
+ }
70
+
71
+ export { currentLevel };
@@ -52,9 +52,32 @@ import https from 'node:https';
52
52
 
53
53
  import { atomicWriteJson } from './routes/_shared.mjs';
54
54
  import { parseFrontmatter } from './yaml.mjs';
55
+ import { warn as logWarn } from './logger.mjs';
55
56
 
56
57
  // ── Helpers ──────────────────────────────────────────────────────────────────
57
58
 
59
+ /**
60
+ * v4.7.0 — Per-site log rate limiter for hot-path catches.
61
+ *
62
+ * Several empty catches below sit on stdout/stderr `data` handlers
63
+ * that can fire dozens of times per second when LightRAG is chatty
64
+ * (or the disk is slow). Without rate-limiting, a transient disk
65
+ * hiccup floods the log with thousands of identical warnings and
66
+ * hides the real problem underneath.
67
+ *
68
+ * Returns true the first time it's called for a key within `intervalMs`,
69
+ * then false until the window passes. Default 60s — matches the
70
+ * "if it's still broken, say it again a minute later" pattern.
71
+ */
72
+ const _lastLog = new Map();
73
+ function _rateLimit(key, intervalMs = 60_000) {
74
+ const now = Date.now();
75
+ const last = _lastLog.get(key) || 0;
76
+ if (now - last < intervalMs) return false;
77
+ _lastLog.set(key, now);
78
+ return true;
79
+ }
80
+
58
81
  function deepMerge(target, source) {
59
82
  const out = { ...target };
60
83
  if (source === null || source === undefined) return out;
@@ -84,7 +107,8 @@ function readLogTail(filePath, n = 30) {
84
107
  const content = readFileSync(filePath, 'utf8');
85
108
  const lines = content.split('\n');
86
109
  return lines.slice(-n);
87
- } catch {
110
+ } catch (err) {
111
+ console.warn('[lightrag] swallowed in readLogTail:', err?.message || err);
88
112
  return [];
89
113
  }
90
114
  }
@@ -206,7 +230,8 @@ export function resolveLightRAGConfig(projectRoot) {
206
230
  if (existsSync(path)) {
207
231
  try {
208
232
  userCfg = JSON.parse(readFileSync(path, 'utf8')).lightrag || {};
209
- } catch {
233
+ } catch (err) {
234
+ console.warn('[lightrag] swallowed in resolveLightRAGConfig.readMemoryJson:', err?.message || err);
210
235
  userCfg = {};
211
236
  }
212
237
  }
@@ -269,8 +294,9 @@ export function findLightragBinary() {
269
294
  timeout: 3000,
270
295
  }).trim();
271
296
  if (path) return path;
272
- } catch {
297
+ } catch (err) {
273
298
  // command returned non-zero — fall through
299
+ console.warn('[lightrag] swallowed in findLightragBinary.which:', err?.message || err);
274
300
  }
275
301
  return null;
276
302
  }
@@ -336,7 +362,8 @@ export async function isRunning(config) {
336
362
  try {
337
363
  const res = await httpGet(`http://${config.host}:${config.port}/health`, 3000);
338
364
  return res.status === 200;
339
- } catch {
365
+ } catch (err) {
366
+ console.warn('[lightrag] swallowed in isRunning.healthCheck:', err?.message || err);
340
367
  return false;
341
368
  }
342
369
  }
@@ -361,7 +388,8 @@ function readPidAlive(pidFile) {
361
388
  if (!Number.isFinite(pid) || pid <= 0) return { pid: null, alive: false };
362
389
  const alive = killPid(pid, 0); // signal 0 = probe
363
390
  return { pid, alive };
364
- } catch {
391
+ } catch (err) {
392
+ console.warn('[lightrag] swallowed in readPidAlive:', err?.message || err);
365
393
  return { pid: null, alive: false };
366
394
  }
367
395
  }
@@ -387,7 +415,9 @@ export async function startServer(config, { logger } = {}) {
387
415
  if (existing && !existingAlive) {
388
416
  try {
389
417
  unlinkSync(pidFile);
390
- } catch {}
418
+ } catch (err) {
419
+ console.warn('[lightrag] swallowed in startServer.unlinkStalePid:', err?.message || err);
420
+ }
391
421
  }
392
422
 
393
423
  if (!findLightragBinary()) {
@@ -437,11 +467,23 @@ export async function startServer(config, { logger } = {}) {
437
467
  let fd = -1;
438
468
  try {
439
469
  fd = openSync(logFile, 'a');
440
- child.stdout?.on('data', (d) => { try { writeSync(fd, d); } catch {} });
441
- child.stderr?.on('data', (d) => { try { writeSync(fd, d); } catch {} });
470
+ child.stdout?.on('data', (d) => {
471
+ try { writeSync(fd, d); } catch (err) {
472
+ if (_rateLimit('lightrag-write-stdout')) logWarn('lightrag stream write failed (stdout)', { module: 'lightrag', err: err?.message || String(err) });
473
+ }
474
+ });
475
+ child.stderr?.on('data', (d) => {
476
+ try { writeSync(fd, d); } catch (err) {
477
+ if (_rateLimit('lightrag-write-stderr')) logWarn('lightrag stream write failed (stderr)', { module: 'lightrag', err: err?.message || String(err) });
478
+ }
479
+ });
442
480
  child.on('exit', (code, signal) => {
443
- try { closeSync(fd); } catch {}
444
- try { unlinkSync(pidFile); } catch {}
481
+ try { closeSync(fd); } catch (err) {
482
+ if (_rateLimit('lightrag-close-fd')) logWarn('lightrag close fd failed', { module: 'lightrag', err: err?.message || String(err) });
483
+ }
484
+ try { unlinkSync(pidFile); } catch (err) {
485
+ if (_rateLimit('lightrag-exit-unlink')) logWarn('lightrag pid unlink failed', { module: 'lightrag', err: err?.message || String(err) });
486
+ }
445
487
  log(`lightrag-server exited (code=${code}, signal=${signal})`);
446
488
  });
447
489
  } catch (openErr) {
@@ -476,7 +518,9 @@ export async function stopServer(config, { logger } = {}) {
476
518
  const pidFile = join(config.workingDir, 'lightrag.pid');
477
519
  const { pid, alive } = readPidAlive(pidFile);
478
520
  if (!pid || !alive) {
479
- try { unlinkSync(pidFile); } catch {}
521
+ try { unlinkSync(pidFile); } catch (err) {
522
+ if (_rateLimit('lightrag-stop-notrunning-unlink')) logWarn('lightrag pid unlink failed (not running)', { module: 'lightrag', err: err?.message || String(err) });
523
+ }
480
524
  return { ok: true, message: 'not running' };
481
525
  }
482
526
  log(`stopping lightrag-server (pid ${pid})`);
@@ -486,7 +530,9 @@ export async function stopServer(config, { logger } = {}) {
486
530
  log(`escalating to SIGKILL (pid ${pid})`);
487
531
  killPid(pid, 'SIGKILL');
488
532
  }
489
- try { unlinkSync(pidFile); } catch {}
533
+ try { unlinkSync(pidFile); } catch (err) {
534
+ if (_rateLimit('lightrag-stop-unlink')) logWarn('lightrag pid unlink failed (stop)', { module: 'lightrag', err: err?.message || String(err) });
535
+ }
490
536
  return { ok: true, message: 'stopped' };
491
537
  }
492
538
 
@@ -818,7 +864,8 @@ export async function stats(projectRoot) {
818
864
  try {
819
865
  process.kill(parsed, 0);
820
866
  pid = parsed;
821
- } catch {
867
+ } catch (err) {
868
+ console.warn('[lightrag] swallowed in stats.pidProbe:', err?.message || err);
822
869
  pid = null;
823
870
  }
824
871
  }
@@ -835,8 +882,8 @@ export async function stats(projectRoot) {
835
882
  lastReindexOk = m.ok === true;
836
883
  lastReindexInserted = typeof m.inserted === 'number' ? m.inserted : null;
837
884
  lastReindexFailed = typeof m.failed === 'number' ? m.failed : null;
838
- } catch {
839
- /* marker corrupt ignore */
885
+ } catch (err) {
886
+ console.warn('[lightrag] swallowed in stats.markerRead:', err?.message || err);
840
887
  }
841
888
  }
842
889
 
@@ -853,13 +900,13 @@ export async function stats(projectRoot) {
853
900
  try {
854
901
  const obj = JSON.parse(readFileSync(p, 'utf8'));
855
902
  indexedApprox += Object.keys(obj || {}).length;
856
- } catch {
857
- /* skip */
903
+ } catch (err) {
904
+ console.warn('[lightrag] swallowed in stats.kvStoreRead:', err?.message || err);
858
905
  }
859
906
  }
860
907
  }
861
- } catch {
862
- /* ignore */
908
+ } catch (err) {
909
+ console.warn('[lightrag] swallowed in stats.kvStoreScan:', err?.message || err);
863
910
  }
864
911
 
865
912
  // Note count = listNotes from the vault.
@@ -895,8 +942,8 @@ export async function stats(projectRoot) {
895
942
  walk(vaultRoot);
896
943
  }
897
944
  }
898
- } catch {
899
- /* ignore leave noteCount at 0 */
945
+ } catch (err) {
946
+ console.warn('[lightrag] swallowed in stats.noteCount:', err?.message || err);
900
947
  }
901
948
 
902
949
  // Query stats: best-effort — LightRAG's /query endpoint doesn't expose
@@ -920,12 +967,12 @@ export async function stats(projectRoot) {
920
967
  counted++;
921
968
  }
922
969
  }
923
- } catch {
924
- /* skip */
970
+ } catch (err) {
971
+ console.warn('[lightrag] swallowed in stats.queryLogLine:', err?.message || err);
925
972
  }
926
973
  }
927
- } catch {
928
- /* ignore */
974
+ } catch (err) {
975
+ console.warn('[lightrag] swallowed in stats.queryLogRead:', err?.message || err);
929
976
  }
930
977
  }
931
978
  const avgResponseMs = counted > 0 ? Math.round(totalResponseMs / counted) : null;
@@ -974,24 +1021,24 @@ export function recordQuery(projectRoot, durationMs) {
974
1021
  const keep = lines.slice(-50000).join('\n');
975
1022
  writeFileSync(path, keep);
976
1023
  }
977
- } catch {
978
- /* ignore */
1024
+ } catch (err) {
1025
+ console.warn('[lightrag] swallowed in recordQuery.truncate:', err?.message || err);
979
1026
  }
980
- } catch {
981
- /* never throw */
1027
+ } catch (err) {
1028
+ console.warn('[lightrag] swallowed in recordQuery:', err?.message || err);
982
1029
  }
983
1030
  }
984
1031
 
985
1032
  function appendFileSafe(path, content) {
986
1033
  try {
987
1034
  appendFileSync(path, content);
988
- } catch {
989
- /* fall back to manual write */
1035
+ } catch (err) {
1036
+ console.warn('[lightrag] appendFileSync failed, falling back to read+write:', err?.message || err);
990
1037
  try {
991
1038
  const cur = existsSync(path) ? readFileSync(path, 'utf8') : '';
992
1039
  writeFileSync(path, cur + content);
993
- } catch {
994
- /* swallow stats are best-effort */
1040
+ } catch (err2) {
1041
+ console.warn('[lightrag] swallowed in appendFileSafe.manualWrite:', err2?.message || err2);
995
1042
  }
996
1043
  }
997
1044
  }
@@ -1024,13 +1071,13 @@ export async function rebuildGraph(projectRoot, opts = {}) {
1024
1071
  for (const e of readdirSync(config.workingDir)) {
1025
1072
  try {
1026
1073
  unlinkSync(join(config.workingDir, e));
1027
- } catch {
1028
- /* skip */
1074
+ } catch (err) {
1075
+ console.warn('[lightrag] swallowed in rebuildGraph.unlinkEntry:', err?.message || err);
1029
1076
  }
1030
1077
  }
1031
1078
  }
1032
- } catch {
1033
- /* ignore */
1079
+ } catch (err) {
1080
+ console.warn('[lightrag] swallowed in rebuildGraph.wipe:', err?.message || err);
1034
1081
  }
1035
1082
  // Reindex from scratch.
1036
1083
  return reindexVault(projectRoot, opts);
@@ -0,0 +1,193 @@
1
+ /**
2
+ * src/server/metrics.mjs
3
+ *
4
+ * v4.7.0 — Tiny Prometheus-style metrics registry.
5
+ *
6
+ * Implements the three primitive metric types we actually use:
7
+ * - counter(name, help) → { inc(labels) }
8
+ * - gauge(name, help) → { set(value, labels) }
9
+ * - histogram(name, help, opts) → { observe(value, labels) }
10
+ *
11
+ * Storage is in-process: counters/gauges/histograms are kept in Maps
12
+ * keyed by metric name → label-set signature → values. Labels are
13
+ * JSON-stringified to produce a stable key (sorted-keys would be
14
+ * nicer but JSON.stringify of a literal object preserves insertion
15
+ * order, which is good enough for our use — every caller writes the
16
+ * same label shape for the same metric).
17
+ *
18
+ * `render()` returns the Prometheus text exposition format
19
+ * (Content-Type: text/plain; version=0.0.4) so it can be scraped by
20
+ * Prometheus or read by humans. The output includes the `# HELP` and
21
+ * `# TYPE` headers for every metric.
22
+ *
23
+ * `reset()` clears the registry — only intended for tests.
24
+ */
25
+
26
+ const counters = new Map();
27
+ const gauges = new Map();
28
+ const histograms = new Map();
29
+
30
+ // Default Prometheus-style histogram buckets in seconds (suitable for
31
+ // HTTP latency). Override per-metric via opts.buckets.
32
+ const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
33
+
34
+ function labelsKey(labels) {
35
+ if (!labels || typeof labels !== 'object') return '{}';
36
+ const keys = Object.keys(labels);
37
+ if (keys.length === 0) return '{}';
38
+ const ordered = {};
39
+ for (const k of keys.sort()) ordered[k] = labels[k];
40
+ return JSON.stringify(ordered);
41
+ }
42
+
43
+ function labelsToString(labels) {
44
+ if (!labels || typeof labels !== 'object') return '';
45
+ const keys = Object.keys(labels);
46
+ if (keys.length === 0) return '';
47
+ return keys
48
+ .sort()
49
+ .map((k) => `${k}="${escapeLabelValue(String(labels[k]))}"`)
50
+ .join(',');
51
+ }
52
+
53
+ function escapeLabelValue(v) {
54
+ return v.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
55
+ }
56
+
57
+ export function counter(name, help) {
58
+ if (!name) throw new Error('counter(name, help): name is required');
59
+ return {
60
+ inc(labels = {}) {
61
+ const key = labelsKey(labels);
62
+ let entry = counters.get(name);
63
+ if (!entry) {
64
+ entry = { help: help || '', values: new Map() };
65
+ counters.set(name, entry);
66
+ }
67
+ const v = entry.values.get(key) || { labels, value: 0 };
68
+ v.value += 1;
69
+ entry.values.set(key, v);
70
+ },
71
+ };
72
+ }
73
+
74
+ export function gauge(name, help) {
75
+ if (!name) throw new Error('gauge(name, help): name is required');
76
+ return {
77
+ set(value, labels = {}) {
78
+ const key = labelsKey(labels);
79
+ let entry = gauges.get(name);
80
+ if (!entry) {
81
+ entry = { help: help || '', values: new Map() };
82
+ gauges.set(name, entry);
83
+ }
84
+ entry.values.set(key, { labels, value: Number(value) });
85
+ },
86
+ };
87
+ }
88
+
89
+ export function histogram(name, help, opts = {}) {
90
+ if (!name) throw new Error('histogram(name, help, opts): name is required');
91
+ const buckets = Array.isArray(opts.buckets) && opts.buckets.length > 0
92
+ ? [...opts.buckets].sort((a, b) => a - b)
93
+ : DEFAULT_BUCKETS;
94
+ return {
95
+ observe(value, labels = {}) {
96
+ const key = labelsKey(labels);
97
+ let entry = histograms.get(name);
98
+ if (!entry) {
99
+ entry = {
100
+ help: help || '',
101
+ buckets,
102
+ values: new Map(),
103
+ };
104
+ histograms.set(name, entry);
105
+ }
106
+ const v = entry.values.get(key) || {
107
+ labels,
108
+ count: 0,
109
+ sum: 0,
110
+ bucketCounts: buckets.map(() => 0),
111
+ };
112
+ v.count += 1;
113
+ v.sum += Number(value);
114
+ for (let i = 0; i < buckets.length; i += 1) {
115
+ if (Number(value) <= buckets[i]) v.bucketCounts[i] += 1;
116
+ }
117
+ entry.values.set(key, v);
118
+ },
119
+ };
120
+ }
121
+
122
+ function renderCounters() {
123
+ const lines = [];
124
+ for (const [name, entry] of counters) {
125
+ lines.push(`# HELP ${name} ${entry.help}`);
126
+ lines.push(`# TYPE ${name} counter`);
127
+ for (const v of entry.values.values()) {
128
+ const labels = labelsToString(v.labels);
129
+ lines.push(`${name}${labels ? '{' + labels + '}' : ''} ${v.value}`);
130
+ }
131
+ }
132
+ return lines;
133
+ }
134
+
135
+ function renderGauges() {
136
+ const lines = [];
137
+ for (const [name, entry] of gauges) {
138
+ lines.push(`# HELP ${name} ${entry.help}`);
139
+ lines.push(`# TYPE ${name} gauge`);
140
+ for (const v of entry.values.values()) {
141
+ const labels = labelsToString(v.labels);
142
+ lines.push(`${name}${labels ? '{' + labels + '}' : ''} ${v.value}`);
143
+ }
144
+ }
145
+ return lines;
146
+ }
147
+
148
+ function renderHistograms() {
149
+ const lines = [];
150
+ for (const [name, entry] of histograms) {
151
+ lines.push(`# HELP ${name} ${entry.help}`);
152
+ lines.push(`# TYPE ${name} histogram`);
153
+ for (const v of entry.values.values()) {
154
+ const baseLabels = labelsToString(v.labels);
155
+ for (let i = 0; i < entry.buckets.length; i += 1) {
156
+ const le = entry.buckets[i];
157
+ const bucketLabels = baseLabels
158
+ ? `${baseLabels},le="${le}"`
159
+ : `le="${le}"`;
160
+ lines.push(`${name}_bucket{${bucketLabels}} ${v.bucketCounts[i]}`);
161
+ }
162
+ const infLabels = baseLabels ? `${baseLabels},le="+Inf"` : 'le="+Inf"';
163
+ lines.push(`${name}_bucket{${infLabels}} ${v.count}`);
164
+ const sumLabels = baseLabels;
165
+ lines.push(
166
+ `${name}_sum${sumLabels ? '{' + sumLabels + '}' : ''} ${v.sum}`,
167
+ );
168
+ lines.push(
169
+ `${name}_count${sumLabels ? '{' + sumLabels + '}' : ''} ${v.count}`,
170
+ );
171
+ }
172
+ }
173
+ return lines;
174
+ }
175
+
176
+ export function render() {
177
+ const parts = [
178
+ ...renderCounters(),
179
+ ...renderGauges(),
180
+ ...renderHistograms(),
181
+ ];
182
+ return parts.length === 0 ? '' : `${parts.join('\n')}\n`;
183
+ }
184
+
185
+ /**
186
+ * Reset every metric. Intended for tests — never call this from a
187
+ * long-running server.
188
+ */
189
+ export function reset() {
190
+ counters.clear();
191
+ gauges.clear();
192
+ histograms.clear();
193
+ }