@polderlabs/bizar 5.0.0 → 5.0.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.
Files changed (96) hide show
  1. package/bizar-dash/dist/assets/{icons-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
  2. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
  4. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
  5. package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
  6. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
  7. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
  8. package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
  9. package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
  10. package/bizar-dash/dist/index.html +4 -4
  11. package/bizar-dash/dist/mobile.html +3 -3
  12. package/bizar-dash/node_modules/.package-lock.json +6 -0
  13. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  14. package/bizar-dash/package-lock.json +6 -0
  15. package/bizar-dash/src/server/api.mjs +7 -0
  16. package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
  17. package/bizar-dash/src/server/memory-store.mjs +46 -0
  18. package/bizar-dash/src/server/providers-store.mjs +11 -4
  19. package/bizar-dash/src/server/routes/_shared.mjs +2 -2
  20. package/bizar-dash/src/server/routes/doctor.mjs +71 -0
  21. package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
  22. package/bizar-dash/src/server/routes/memory.mjs +3 -0
  23. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
  24. package/bizar-dash/src/server/routes/schedules.mjs +55 -0
  25. package/bizar-dash/src/server/serve-info.mjs +172 -0
  26. package/bizar-dash/src/web/App.tsx +48 -11
  27. package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
  28. package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
  29. package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
  30. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
  31. package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
  32. package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
  33. package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
  34. package/bizar-dash/src/web/components/Topbar.tsx +11 -0
  35. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
  36. package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
  37. package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
  38. package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
  39. package/bizar-dash/src/web/lib/types.ts +134 -0
  40. package/bizar-dash/src/web/styles/chat.css +8 -5
  41. package/bizar-dash/src/web/styles/main.css +77 -2
  42. package/bizar-dash/src/web/styles/settings.css +265 -0
  43. package/bizar-dash/src/web/views/Chat.tsx +15 -1
  44. package/bizar-dash/src/web/views/Doctor.tsx +317 -0
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
  46. package/bizar-dash/src/web/views/Schedules.tsx +94 -12
  47. package/bizar-dash/src/web/views/Settings.tsx +76 -46
  48. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
  49. package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
  50. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
  51. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
  52. package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
  53. package/bizar-dash/tests/autosave.test.tsx +276 -0
  54. package/bizar-dash/tests/chat-composer.test.tsx +140 -0
  55. package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
  56. package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
  57. package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
  58. package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
  59. package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
  60. package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
  61. package/bizar-dash/tests/minimax-models.test.mjs +123 -0
  62. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
  63. package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
  64. package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
  65. package/bizar-dash/tests/settings-layout.test.tsx +129 -0
  66. package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
  67. package/bizar-dash/tests/settings-nav.test.tsx +126 -0
  68. package/cli/commands/minimax.mjs +5 -2
  69. package/cli/tests/minimax-cli.test.mjs +79 -0
  70. package/config/agents/frigg.md +1 -1
  71. package/config/agents/heimdall.md +1 -1
  72. package/config/agents/mimir.md +1 -1
  73. package/config/agents/quick.md +1 -1
  74. package/config/agents/semble-search.md +1 -1
  75. package/config/agents/vor.md +1 -1
  76. package/config/opencode.json +28 -21
  77. package/config/opencode.json.template +8 -1
  78. package/package.json +1 -1
  79. package/plugins/bizar/index.ts +77 -0
  80. package/plugins/bizar/src/compaction.d.mts +48 -0
  81. package/plugins/bizar/src/compaction.mjs +192 -0
  82. package/plugins/bizar/tests/compaction.test.ts +264 -0
  83. package/templates/schedules/daily-backup.json +12 -0
  84. package/templates/schedules/daily-cleanup.json +12 -0
  85. package/templates/schedules/hourly-health-check.json +12 -0
  86. package/templates/schedules/webhook-on-push.json +13 -0
  87. package/templates/schedules/weekly-digest.json +13 -0
  88. package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +0 -1
  89. package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
  90. package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
  91. package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
  92. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
  93. package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +0 -1
  94. package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
  95. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
  96. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
@@ -1 +1 @@
1
- import{m as r}from"./mobile-C2gysFOZ.js";import{b as e,j as o,r as m}from"./react-vendor-DZRUXSPQ.js";import"./icons-CFqu2M-c.js";import"./markdown-DIquRulQ.js";const t=document.getElementById("root");if(!t)throw new Error("Root element #root not found");e(t).render(o.jsx(m.StrictMode,{children:o.jsx(r,{})}));
1
+ import{m as r}from"./mobile-BtxQJftK.js";import{b as e,j as o,r as m}from"./react-vendor-DZRUXSPQ.js";import"./icons-Bo0iH9EC.js";import"./markdown-DIquRulQ.js";const t=document.getElementById("root");if(!t)throw new Error("Root element #root not found");e(t).render(o.jsx(m.StrictMode,{children:o.jsx(r,{})}));
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-DHXXbn1A.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":"iKAMA,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-DD-FZrTC.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":"iKAMA,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,13 +29,13 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-Dl8yY5_H.js"></script>
32
+ <script type="module" crossorigin src="/assets/main-C1cpttnv.js"></script>
33
33
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-DZRUXSPQ.js">
34
- <link rel="modulepreload" crossorigin href="/assets/icons-CFqu2M-c.js">
34
+ <link rel="modulepreload" crossorigin href="/assets/icons-Bo0iH9EC.js">
35
35
  <link rel="modulepreload" crossorigin href="/assets/markdown-DIquRulQ.js">
36
- <link rel="modulepreload" crossorigin href="/assets/mobile-C2gysFOZ.js">
36
+ <link rel="modulepreload" crossorigin href="/assets/mobile-BtxQJftK.js">
37
37
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
38
- <link rel="stylesheet" crossorigin href="/assets/main-ZAfGKENE.css">
38
+ <link rel="stylesheet" crossorigin href="/assets/main-DTkNlLrw.css">
39
39
  </head>
40
40
  <body>
41
41
  <div id="root"></div>
@@ -12,11 +12,11 @@
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-DHXXbn1A.js"></script>
15
+ <script type="module" crossorigin src="/assets/mobile-DD-FZrTC.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-DZRUXSPQ.js">
17
- <link rel="modulepreload" crossorigin href="/assets/icons-CFqu2M-c.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/icons-Bo0iH9EC.js">
18
18
  <link rel="modulepreload" crossorigin href="/assets/markdown-DIquRulQ.js">
19
- <link rel="modulepreload" crossorigin href="/assets/mobile-C2gysFOZ.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/mobile-BtxQJftK.js">
20
20
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
21
21
  </head>
22
22
  <body>
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "bizar-dash",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
@@ -1 +1 @@
1
- {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":24.136259999999993,"failed":false}],[":tests/components/Button.test.tsx",{"duration":83.28092300000003,"failed":false}],[":tests/components/Card.test.tsx",{"duration":29.62249799999995,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":108.65311200000008,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":127.17467799999997,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":129.80008299999997,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":3.760281999999961,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":33.259794000000056,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":87.20143099999996,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":55.38087500000006,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":148.675974,"failed":false}],[":tests/a11y.test.tsx",{"duration":638.991055,"failed":false}],[":tests/lib/search-fuzzy.test.ts",{"duration":8.135348000000022,"failed":false}],[":tests/components/settings-search.test.tsx",{"duration":693.5110860000001,"failed":false}],[":tests/memory-graph-view.test.tsx",{"duration":44.16916100000003,"failed":false}],[":tests/components/workspace-selector.test.tsx",{"duration":103.51388600000007,"failed":false}],[":tests/components/screenshot-ocr.test.tsx",{"duration":37.170334000000025,"failed":false}],[":tests/voice-recorder.test.tsx",{"duration":99.73518000000001,"failed":false}]]}
1
+ {"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":26.432379999999966,"failed":false}],[":tests/components/Button.test.tsx",{"duration":112.34220400000004,"failed":false}],[":tests/components/Card.test.tsx",{"duration":35.56367599999999,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":157.37005299999998,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":173.31895800000007,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":185.367477,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":2.9995610000000283,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":46.117966000000024,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":109.72244999999998,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":63.391606000000024,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":199.5516550000001,"failed":false}],[":tests/a11y.test.tsx",{"duration":705.7359089999999,"failed":false}],[":tests/lib/search-fuzzy.test.ts",{"duration":4.982475999999963,"failed":false}],[":tests/components/settings-search.test.tsx",{"duration":709.4941419999999,"failed":false}],[":tests/memory-graph-view.test.tsx",{"duration":44.654364999999984,"failed":false}],[":tests/components/workspace-selector.test.tsx",{"duration":138.400938,"failed":false}],[":tests/components/screenshot-ocr.test.tsx",{"duration":58.98421100000007,"failed":false}],[":tests/voice-recorder.test.tsx",{"duration":134.03825800000004,"failed":false}],[":tests/autosave.test.tsx",{"duration":255.54418999999984,"failed":false}],[":tests/settings-layout.test.tsx",{"duration":228.89731000000006,"failed":false}],[":tests/settings-nav.test.tsx",{"duration":280.74951,"failed":false}],[":tests/components/doctor-panel.test.tsx",{"duration":87.87421399999994,"failed":false}],[":tests/chat-composer.test.tsx",{"duration":208.14527199999998,"failed":false}],[":tests/settings-mode-wiring.test.tsx",{"duration":213.08016700000007,"failed":false}],[":tests/minimax-bar.test.ts",{"duration":0,"failed":true}],[":tests/minimax-bar.test.tsx",{"duration":39.83727500000009,"failed":false}]]}
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "bizar-dash",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
@@ -46,6 +46,7 @@ import { createDialogsRouter } from './routes/dialogs.mjs';
46
46
  import { createSkillsRouter } from './routes/skills.mjs';
47
47
  import { createObsidianRouter } from './routes/obsidian.mjs';
48
48
  import { createDiagnosticsRouter } from './routes/diagnostics.mjs';
49
+ import { createDoctorRouter } from './routes/doctor.mjs';
49
50
  import { createPairRouter } from './routes/pair.mjs';
50
51
  import { createThemesRouter } from './routes/themes.mjs';
51
52
  import { createNotificationsRouter } from './routes/notifications.mjs';
@@ -144,6 +145,12 @@ export async function createApiRouter({
144
145
  // v5.0.0 — Voice notes (record, transcribe, list, delete, stream audio).
145
146
  router.use(createVoiceRouter({}));
146
147
  router.use(createDiagnosticsRouter());
148
+ // v6.0.0 — Doctor page API. Mounted right after diagnostics so the
149
+ // /doctor/* paths live next to the legacy /diagnostics/* surface
150
+ // they conceptually extend. Each route inside is at its bare path
151
+ // (e.g. '/health'), which becomes '/api/doctor/health' at the top
152
+ // level once /api is prepended by server.mjs.
153
+ router.use('/doctor', createDoctorRouter());
147
154
  router.use(createPairRouter({ state, broadcast }));
148
155
  router.use(createThemesRouter({ state }));
149
156
  router.use(createNotificationsRouter({ broadcast }));
@@ -2,8 +2,14 @@
2
2
  * src/server/diagnostics-store.mjs
3
3
  *
4
4
  * v3.0.0 — Health check, version info, and last-N errors.
5
+ * v6.0.0 — Doctor surface: full system snapshot (`collectDiagnostics()`),
6
+ * per-check dispatch (`runCheck(name)`), and a timestamped
7
+ * recent-errors feed (`getRecentErrors({ since })`) for the
8
+ * Doctor page in the dashboard.
5
9
  *
6
- * Returns a single snapshot for the diagnostics card in the dashboard.
10
+ * The legacy `snapshot()` and `health()` methods are preserved so the
11
+ * v3 `/api/diagnostics` routes keep working unchanged. The Doctor routes
12
+ * call the richer `collectDiagnostics()` / `runCheck()` / `health()`.
7
13
  */
8
14
  import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
9
15
  import { join, dirname } from 'node:path';
@@ -77,6 +83,450 @@ function serviceStatus() {
77
83
  }
78
84
  }
79
85
 
86
+ /**
87
+ * v6.0.0 — Timestamped error feed. Reads the recent service log and
88
+ * returns entries whose `[ts]` prefix (if any) is >= `since`. Each
89
+ * entry carries a parsed epoch ms to make filtering cheap for the
90
+ * UI's "last hour" widget.
91
+ *
92
+ * Capped at 200 entries to keep the response bounded — the dashboard
93
+ * only ever displays ~30. Returns `[]` on any read failure so the UI
94
+ * can render an empty state instead of crashing.
95
+ *
96
+ * @param {{ since?: number, limit?: number }} opts
97
+ * @returns {Array<{ line: string, ts: string | null, tsMs: number | null }>}
98
+ */
99
+ export function getRecentErrors({ since = 0, limit = 200 } = {}) {
100
+ if (!existsSync(SERVICE_LOG)) return [];
101
+ try {
102
+ const text = readFileSync(SERVICE_LOG, 'utf8');
103
+ const lines = text.split(/\r?\n/);
104
+ const out = [];
105
+ for (let i = lines.length - 1; i >= 0; i--) {
106
+ const line = lines[i];
107
+ if (!line.trim()) continue;
108
+ if (!/\b(failed|error|err)\b/i.test(line)) continue;
109
+ const tsStr = parseLogTs(line);
110
+ const tsMs = tsStr ? Date.parse(tsStr) : null;
111
+ if (since && tsMs !== null && tsMs < since) continue;
112
+ out.push({ line, ts: tsStr, tsMs });
113
+ if (out.length >= limit) break;
114
+ }
115
+ return out;
116
+ } catch {
117
+ return [];
118
+ }
119
+ }
120
+
121
+ /**
122
+ * v6.0.0 — Probe the local headroom proxy on `host:port`. Returns a
123
+ * `{ running, port?, error? }` shape so the Doctor page can show a
124
+ * "Headroom proxy on 8787 ✓" line without forcing the route to
125
+ * swallow a 1.5s timeout on every poll.
126
+ *
127
+ * @param {string} host
128
+ * @param {number} port
129
+ * @param {number} timeoutMs
130
+ * @returns {Promise<{ running: boolean, port?: number, error?: string }>}
131
+ */
132
+ async function probeTcp(host, port, timeoutMs = 800) {
133
+ // Lazy import so unit tests that don't need TCP probing still load
134
+ // the module under environments that can't resolve `node:net`.
135
+ const { connect } = await import('node:net');
136
+ return new Promise((resolve) => {
137
+ let settled = false;
138
+ const finish = (ok, error) => {
139
+ if (settled) return;
140
+ settled = true;
141
+ resolve(ok ? { running: true, port } : { running: false, error: error || 'unreachable' });
142
+ };
143
+ const sock = connect(port, host);
144
+ const timer = setTimeout(() => {
145
+ sock.destroy();
146
+ finish(false, 'timeout');
147
+ }, timeoutMs);
148
+ sock.once('connect', () => {
149
+ clearTimeout(timer);
150
+ sock.destroy();
151
+ finish(true);
152
+ });
153
+ sock.once('error', (err) => {
154
+ clearTimeout(timer);
155
+ finish(false, err.message);
156
+ });
157
+ });
158
+ }
159
+
160
+ /**
161
+ * v6.0.0 — LightRAG port probe. The dashboard doesn't always have a
162
+ * lightrag server running, so the probe is best-effort. Returns the
163
+ * port from settings if known, plus a `running` boolean.
164
+ *
165
+ * @returns {Promise<{ running: boolean, port?: number, error?: string }>}
166
+ */
167
+ async function probeLightRAG() {
168
+ try {
169
+ // Avoid a hard dependency on the memory router module: just read
170
+ // the same `.bizar/memory.json` it would consult. Fall back to
171
+ // a no-port default.
172
+ const cfgPath = join(HOME, '.config', 'bizar', 'memory.json');
173
+ let port = 0;
174
+ if (existsSync(cfgPath)) {
175
+ try {
176
+ const parsed = JSON.parse(readFileSync(cfgPath, 'utf8'));
177
+ port = Number(parsed?.lightrag?.port || 0);
178
+ } catch { /* malformed JSON — ignore */ }
179
+ }
180
+ if (!port) return { running: false, port: undefined, error: 'port unknown' };
181
+ return await probeTcp('127.0.0.1', port, 800);
182
+ } catch (err) {
183
+ return { running: false, error: err.message };
184
+ }
185
+ }
186
+
187
+ async function probeHeadroom() {
188
+ // Headroom's default port — matches headroom.mjs's
189
+ // DEFAULT_HEADROOM_PORT. Settings override takes precedence.
190
+ let port = 8787;
191
+ let host = '127.0.0.1';
192
+ try {
193
+ const settingsPath = join(HOME, '.config', 'bizar', 'settings.json');
194
+ if (existsSync(settingsPath)) {
195
+ const parsed = JSON.parse(readFileSync(settingsPath, 'utf8'));
196
+ port = Number(parsed?.headroom?.port) || port;
197
+ host = String(parsed?.headroom?.host) || host;
198
+ }
199
+ } catch { /* ignore */ }
200
+ const result = await probeTcp(host, port, 800);
201
+ return { ...result, port, host };
202
+ }
203
+
204
+ /**
205
+ * v6.0.0 — Probe whether the opencode plugin's `serve-info.json` is
206
+ * reachable on disk and parseable. The dashboard reads serve-info to
207
+ * dispatch agent tasks; if it's missing or stale, the Doctor page
208
+ * flags it as a warning (not a failure) since some installs don't use
209
+ * the plugin.
210
+ */
211
+ function opencodeServeInfo() {
212
+ const candidates = [
213
+ join(HOME, '.cache', 'bizar', 'serve.json'),
214
+ join(HOME, '.cache', 'bizar', 'serve-info.json'),
215
+ ];
216
+ for (const path of candidates) {
217
+ if (!existsSync(path)) continue;
218
+ try {
219
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
220
+ const port = Number(raw?.port || 0);
221
+ return { reachable: true, path, port: port || undefined, baseUrl: raw?.baseUrl || null };
222
+ } catch (err) {
223
+ return { reachable: false, path, error: err.message };
224
+ }
225
+ }
226
+ return { reachable: false, path: null };
227
+ }
228
+
229
+ /**
230
+ * v6.0.0 — Aggregate counts across every dashboard store. The Doctor
231
+ * page surfaces this as a one-line-per-row table. All counts return
232
+ * 0 (never throw) when the underlying store is empty or unreachable,
233
+ * so a partial outage still gives the operator a useful readout.
234
+ */
235
+ function collectCounts() {
236
+ const projects = projectsStore.list();
237
+ const active = projectsStore.active();
238
+ const tasks = active ? tasksStore.loadTasks(active.id) : [];
239
+ const schedules = active ? schedulesStore.list(active.id) : [];
240
+ let workspacesCount = 0;
241
+ try {
242
+ // listWorkspaces is async and requires a userId — fall back to the
243
+ // index file size if we can't resolve one cheaply.
244
+ const indexPath = join(HOME, '.config', 'bizar', 'workspaces', 'index.json');
245
+ if (existsSync(indexPath)) {
246
+ const parsed = JSON.parse(readFileSync(indexPath, 'utf8'));
247
+ workspacesCount = Array.isArray(parsed?.workspaces) ? parsed.workspaces.length : 0;
248
+ }
249
+ } catch { /* ignore */ }
250
+ let voiceNotes = 0;
251
+ try {
252
+ const voiceDir = join(HOME, '.config', 'bizar', 'voice');
253
+ if (existsSync(voiceDir)) {
254
+ voiceNotes = readdirSync(voiceDir).filter((f) => f.endsWith('.json')).length;
255
+ }
256
+ } catch { /* ignore */ }
257
+ let evalRuns = 0;
258
+ try {
259
+ const evalDir = join(HOME, '.config', 'bizar', 'eval');
260
+ if (existsSync(evalDir)) {
261
+ evalRuns = readdirSync(evalDir).filter((f) => f.endsWith('.json')).length;
262
+ }
263
+ } catch { /* ignore */ }
264
+ let backups = 0;
265
+ try {
266
+ const backupDir = join(HOME, '.local', 'share', 'bizar', 'backups');
267
+ if (existsSync(backupDir)) {
268
+ backups = readdirSync(backupDir).filter((f) => f.startsWith('bizar-')).length;
269
+ }
270
+ } catch { /* ignore */ }
271
+ return {
272
+ tasks: tasks.length,
273
+ schedules: schedules.length,
274
+ mods: modsLoader.list().length,
275
+ providers: providersStore.list().length,
276
+ mcps: mcpsStore.list().length,
277
+ agents: agentsStore.list().length,
278
+ projects: projects.projects.length,
279
+ activeProject: active?.id || null,
280
+ workspaces: workspacesCount,
281
+ voiceNotes,
282
+ evalRuns,
283
+ backups,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * v6.0.0 — Static config-health checks. Each check is a name +
289
+ * `{ status, message }` pair. `status` is `ok` / `warn` / `fail`,
290
+ * matching the `DoctorCheck` shape consumed by the frontend.
291
+ *
292
+ * Used by `health()` and exposed individually via `runCheck(name)`.
293
+ */
294
+ async function runConfigChecks() {
295
+ const opencodeConfig = providersStore.OPENCODE_JSON;
296
+ const agentsDir = agentsStore.AGENTS_DIR;
297
+ const checks = [];
298
+ checks.push({
299
+ name: 'opencode-config',
300
+ status: existsSync(opencodeConfig) ? 'ok' : 'fail',
301
+ message: existsSync(opencodeConfig) ? `present at ${opencodeConfig}` : `${opencodeConfig} not found`,
302
+ });
303
+ checks.push({
304
+ name: 'opencode-agents',
305
+ status: existsSync(agentsDir) ? 'ok' : 'warn',
306
+ message: existsSync(agentsDir) ? `${agentsDir} present` : `${agentsDir} missing — agents will fall back to defaults`,
307
+ });
308
+ const bizarHome = join(HOME, '.config', 'bizar');
309
+ checks.push({
310
+ name: 'bizar-home',
311
+ status: existsSync(bizarHome) ? 'ok' : 'fail',
312
+ message: existsSync(bizarHome) ? bizarHome : `${bizarHome} missing`,
313
+ });
314
+ const memoryConfig = join(HOME, '.config', 'bizar', 'memory.json');
315
+ checks.push({
316
+ name: 'memory-config',
317
+ status: existsSync(memoryConfig) ? 'ok' : 'warn',
318
+ message: existsSync(memoryConfig) ? 'memory.json configured' : 'no memory.json — running with defaults',
319
+ });
320
+ return checks;
321
+ }
322
+
323
+ /**
324
+ * v6.0.0 — Service health checks. TCP probes for headroom and
325
+ * lightrag (both best-effort), opencode plugin file probe, and the
326
+ * dashboard itself (always `ok` — we're running because we got here).
327
+ */
328
+ async function runServiceChecks() {
329
+ const headroom = await probeHeadroom();
330
+ const lightrag = await probeLightRAG();
331
+ const serveInfo = opencodeServeInfo();
332
+ const checks = [
333
+ {
334
+ name: 'dashboard',
335
+ status: 'ok',
336
+ message: `running on pid ${process.pid}, up ${Math.floor(process.uptime())}s`,
337
+ },
338
+ {
339
+ name: 'headroom',
340
+ status: headroom.running ? 'ok' : 'warn',
341
+ message: headroom.running
342
+ ? `proxy on ${headroom.host}:${headroom.port}`
343
+ : `not running on ${headroom.host}:${headroom.port}${headroom.error ? ` (${headroom.error})` : ''}`,
344
+ },
345
+ {
346
+ name: 'lightrag',
347
+ status: lightrag.running ? 'ok' : 'warn',
348
+ message: lightrag.running
349
+ ? `running on port ${lightrag.port}`
350
+ : `not running${lightrag.port ? ` on port ${lightrag.port}` : ''}${lightrag.error ? ` (${lightrag.error})` : ''}`,
351
+ },
352
+ {
353
+ name: 'opencode',
354
+ status: serveInfo.reachable ? 'ok' : 'warn',
355
+ message: serveInfo.reachable
356
+ ? `serve-info at ${serveInfo.path}${serveInfo.port ? ` (port ${serveInfo.port})` : ''}`
357
+ : `serve-info missing${serveInfo.error ? `: ${serveInfo.error}` : ''} — plugin may not be running`,
358
+ },
359
+ ];
360
+ return checks;
361
+ }
362
+
363
+ /**
364
+ * v6.0.0 — Overall system checks. Pure: no I/O, no probes. Reports
365
+ * Node version, platform, arch, and uptime.
366
+ */
367
+ function runSystemChecks() {
368
+ return [
369
+ {
370
+ name: 'node',
371
+ status: 'ok',
372
+ message: `${process.version} on ${process.platform}/${process.arch}`,
373
+ },
374
+ {
375
+ name: 'uptime',
376
+ status: 'ok',
377
+ message: `${Math.floor(process.uptime())}s`,
378
+ },
379
+ {
380
+ name: 'memory',
381
+ status: process.memoryUsage().rss < 512 * 1024 * 1024 ? 'ok' : 'warn',
382
+ message: `rss ${(process.memoryUsage().rss / 1024 / 1024).toFixed(1)} MB`,
383
+ },
384
+ ];
385
+ }
386
+
387
+ /**
388
+ * v6.0.0 — Run a single named check. Returns a single
389
+ * `{ name, status, message }` entry, or `null` if the name is unknown.
390
+ * Used by `POST /api/doctor/check`.
391
+ *
392
+ * @param {string} name
393
+ * @returns {Promise<{ name: string, status: string, message: string } | { name: string, status: 'fail', message: string, error: string }>}
394
+ */
395
+ export async function runCheck(name) {
396
+ if (!name || typeof name !== 'string') {
397
+ return { name: String(name || ''), status: 'fail', message: 'unknown check', error: 'checkName required' };
398
+ }
399
+ const system = runSystemChecks();
400
+ const config = await runConfigChecks();
401
+ const services = await runServiceChecks();
402
+ const all = [...system, ...config, ...services];
403
+ const hit = all.find((c) => c.name === name);
404
+ if (!hit) {
405
+ return {
406
+ name,
407
+ status: 'fail',
408
+ message: 'unknown check',
409
+ error: `no check named "${name}". Available: ${all.map((c) => c.name).join(', ')}`,
410
+ };
411
+ }
412
+ return hit;
413
+ }
414
+
415
+ /**
416
+ * v6.0.0 — Top-level health rollup. Aggregates every check group,
417
+ * collapses to a single `ok | warn | fail` status, and returns the
418
+ * structured list of issues so the UI can show "3 warnings" without
419
+ * having to re-derive the math client-side.
420
+ *
421
+ * @returns {Promise<{ status: 'ok'|'warn'|'fail', issues: Array<{ name: string, status: string, message: string }>, groups: Record<string, Array<{ name: string, status: string, message: string }>>, ts: string }>}
422
+ */
423
+ export async function health() {
424
+ const system = runSystemChecks();
425
+ const config = await runConfigChecks();
426
+ const services = await runServiceChecks();
427
+ const groups = { system, config, services };
428
+ const flat = [...system, ...config, ...services];
429
+ const issues = flat.filter((c) => c.status !== 'ok');
430
+ let status = 'ok';
431
+ if (issues.some((c) => c.status === 'fail')) status = 'fail';
432
+ else if (issues.length > 0) status = 'warn';
433
+ return { ts: new Date().toISOString(), status, issues, groups };
434
+ }
435
+
436
+ /**
437
+ * v6.0.0 — Full Doctor snapshot. One call collects everything the
438
+ * Doctor page renders, with bounded response size so the dashboard
439
+ * auto-refresh (every 30s) stays cheap.
440
+ *
441
+ * @returns {Promise<{
442
+ * timestamp: string,
443
+ * bizarVersion: string,
444
+ * nodeVersion: string,
445
+ * platform: string,
446
+ * arch: string,
447
+ * uptime: number,
448
+ * memory: NodeJS.MemoryUsage,
449
+ * disk: { exists: boolean, path: string, sizeBytes: number },
450
+ * services: { dashboard: object, headroom: object, lightrag: object, opencode: object },
451
+ * counts: ReturnType<typeof collectCounts>,
452
+ * recentErrors: ReturnType<typeof getRecentErrors>,
453
+ * configHealth: Array<{ name: string, status: string, message: string }>,
454
+ * opencode: { configExists: boolean, agentsDir: string, agentFiles: string[], dashboardConnected: boolean },
455
+ * checks: { system: Array, config: Array, services: Array },
456
+ * health: { status: string, issues: Array }
457
+ * }>}
458
+ */
459
+ export async function collectDiagnostics() {
460
+ const counts = collectCounts();
461
+ const system = runSystemChecks();
462
+ const config = await runConfigChecks();
463
+ const services = await runServiceChecks();
464
+ const rollup = { system, config, services };
465
+ const flat = [...system, ...config, ...services];
466
+ const issues = flat.filter((c) => c.status !== 'ok');
467
+ let status = 'ok';
468
+ if (issues.some((c) => c.status === 'fail')) status = 'fail';
469
+ else if (issues.length > 0) status = 'warn';
470
+ const headroom = await probeHeadroom();
471
+ const lightrag = await probeLightRAG();
472
+ const serveInfo = opencodeServeInfo();
473
+ const opencodeConfig = providersStore.OPENCODE_JSON;
474
+ const agentsDir = agentsStore.AGENTS_DIR;
475
+ let agentFiles = [];
476
+ try {
477
+ if (existsSync(agentsDir)) {
478
+ agentFiles = readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
479
+ }
480
+ } catch { /* ignore */ }
481
+ const dashboardLogPath = join(HOME, '.config', 'bizar', 'dashboard.log');
482
+ const disk = {
483
+ path: join(HOME, '.config'),
484
+ exists: existsSync(join(HOME, '.config')),
485
+ sizeBytes: 0,
486
+ };
487
+ try {
488
+ if (disk.exists) disk.sizeBytes = statSync(join(HOME, '.config')).size;
489
+ } catch { /* ignore */ }
490
+ const dashboardLogExists = existsSync(dashboardLogPath);
491
+ return {
492
+ timestamp: new Date().toISOString(),
493
+ bizarVersion: dashboardVersion(),
494
+ nodeVersion: process.version,
495
+ platform: process.platform,
496
+ arch: process.arch,
497
+ uptime: Math.floor(process.uptime()),
498
+ memory: process.memoryUsage(),
499
+ disk,
500
+ services: {
501
+ dashboard: {
502
+ running: true,
503
+ pid: process.pid,
504
+ uptime: Math.floor(process.uptime()),
505
+ logPath: dashboardLogPath,
506
+ logExists: dashboardLogExists,
507
+ },
508
+ headroom,
509
+ lightrag,
510
+ opencode: serveInfo,
511
+ },
512
+ counts,
513
+ recentErrors: getRecentErrors({ since: Date.now() - 3600_000, limit: 50 }),
514
+ configHealth: config,
515
+ opencode: {
516
+ configExists: existsSync(opencodeConfig),
517
+ configPath: opencodeConfig,
518
+ agentsDir,
519
+ agentFiles,
520
+ dashboardConnected: serveInfo.reachable,
521
+ },
522
+ checks: rollup,
523
+ health: {
524
+ status,
525
+ issues,
526
+ },
527
+ };
528
+ }
529
+
80
530
  export const diagnosticsStore = {
81
531
  /** Build the full diagnostics snapshot. */
82
532
  snapshot(extra = {}) {
@@ -145,4 +595,4 @@ export const diagnosticsStore = {
145
595
  };
146
596
 
147
597
  // Add MOD_DIR alias for older access
148
- modsLoader.MOD_DIR = modsLoader.MOD_DIR || modsLoader.MODS_DIR;
598
+ modsLoader.MOD_DIR = modsLoader.MOD_DIR || modsLoader.MODS_DIR;
@@ -20,10 +20,56 @@ import { validateNote } from './memory-schema.mjs';
20
20
  import { scan as scanSecrets, hasHighFindings } from './memory-secrets.mjs';
21
21
  import { atomicWriteJson, safeReadJSON, safeReadText } from '../../../cli/atomic.mjs';
22
22
  import * as memoryGit from './memory-git.mjs';
23
+ import { warn as loggerWarn } from './logger.mjs';
23
24
 
24
25
  const HOME = homedir();
26
+
27
+ /**
28
+ * Default vault root — auto-created on first use.
29
+ * Override with BIZAR_MEMORY_VAULT env var.
30
+ */
31
+ export const DEFAULT_MEMORY_VAULT = join(HOME, '.local', 'share', 'bizar', 'memory');
32
+
33
+ /**
34
+ * Default git remote — set via BIZAR_MEMORY_GIT_REMOTE to enable sync.
35
+ */
36
+ export const DEFAULT_GIT_REMOTE = process.env.BIZAR_MEMORY_GIT_REMOTE || null;
37
+
25
38
  const BIZAR_MEMORY_ROOT = join(HOME, '.local', 'share', 'bizar', 'memory');
26
39
 
40
+ /**
41
+ * Return the effective vault root for the default memory vault.
42
+ * Override with BIZAR_MEMORY_VAULT env var.
43
+ *
44
+ * @returns {string}
45
+ */
46
+ export function currentVault() {
47
+ return process.env.BIZAR_MEMORY_VAULT || DEFAULT_MEMORY_VAULT;
48
+ }
49
+
50
+ /**
51
+ * Ensure the default memory vault directory exists and is git-initialised.
52
+ * Idempotent — calling multiple times is safe.
53
+ *
54
+ * @returns {string} the vault path that was ensured
55
+ */
56
+ export function ensureVaultExists() {
57
+ const vault = currentVault();
58
+ if (!existsSync(vault)) {
59
+ mkdirSync(vault, { recursive: true, mode: 0o700 });
60
+ if (!existsSync(join(vault, '.git'))) {
61
+ try {
62
+ execFileSync('git', ['init'], { cwd: vault, stdio: 'pipe' });
63
+ execFileSync('git', ['config', 'user.email', 'bizar@localhost'], { cwd: vault, stdio: 'pipe' });
64
+ execFileSync('git', ['config', 'user.name', 'BizarHarness'], { cwd: vault, stdio: 'pipe' });
65
+ } catch (err) {
66
+ loggerWarn('failed to init git in vault', { vault, err: err.message });
67
+ }
68
+ }
69
+ }
70
+ return vault;
71
+ }
72
+
27
73
  /**
28
74
  * Path safety: reject any relPath that would escape the vault root.
29
75
  *
@@ -326,6 +326,11 @@ export function syncLegacyKeys(p) {
326
326
  return next;
327
327
  }
328
328
 
329
+ // ── MiniMax model constants ──────────────────────────────────────────────────
330
+ //
331
+ // Cheap/fast everyday model — agents that need a lightweight workhorse.
332
+ export const MINIMAX_DEFAULT = 'MiniMax-M2.7';
333
+
329
334
  // ── v4.6.0 Provider catalog ─────────────────────────────────────────────────
330
335
  //
331
336
  // Curated list of well-known providers. The dashboard's "Add provider"
@@ -406,9 +411,10 @@ export const PROVIDER_CATALOG = Object.freeze([
406
411
  keyHint: 'sk-cp-…, sk-ant-…, or sk-or-…',
407
412
  docs: 'https://api.minimax.io',
408
413
  models: [
409
- 'MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed',
410
- 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1',
411
- 'MiniMax-M2.1-highspeed', 'MiniMax-M2',
414
+ Object.freeze({ id: 'MiniMax-M2.7-Flash', name: 'MiniMax M2.7 Flash', tier: 'cheap', recommended: true }),
415
+ Object.freeze({ id: 'MiniMax-M2.7', name: 'MiniMax M2.7', tier: 'cheap' }),
416
+ Object.freeze({ id: 'MiniMax-M3', name: 'MiniMax M3', tier: 'mid' }),
417
+ Object.freeze({ id: 'MiniMax-M3-Reasoning', name: 'MiniMax M3 Reasoning', tier: 'premium' }),
412
418
  ],
413
419
  requiresKey: true,
414
420
  }),
@@ -1543,7 +1549,8 @@ export const providersStore = {
1543
1549
  keyPattern: /^sk-(cp|ant|or)-[A-Za-z0-9_-]{20,}$/,
1544
1550
  defaultModel: 'MiniMax-M3',
1545
1551
  defaultModels: [
1546
- 'MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed',
1552
+ 'MiniMax-M2.7-Flash', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed',
1553
+ 'MiniMax-M3', 'MiniMax-M3-Reasoning',
1547
1554
  'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1',
1548
1555
  'MiniMax-M2.1-highspeed', 'MiniMax-M2',
1549
1556
  ],