@polderlabs/bizar 4.4.13 → 4.5.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 (118) hide show
  1. package/bizar-dash/CHANGELOG.md +37 -276
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
  3. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  4. package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
  5. package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
  6. package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
  7. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
  8. package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
  9. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
  10. package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
  11. package/bizar-dash/dist/index.html +3 -3
  12. package/bizar-dash/dist/mobile.html +2 -2
  13. package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
  14. package/bizar-dash/skills/bizar/SKILL.md +116 -0
  15. package/bizar-dash/skills/chat/SKILL.md +74 -0
  16. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  17. package/bizar-dash/skills/lightrag/SKILL.md +86 -0
  18. package/bizar-dash/skills/minimax/SKILL.md +80 -0
  19. package/bizar-dash/skills/obsidian/SKILL.md +68 -0
  20. package/bizar-dash/skills/providers/SKILL.md +75 -0
  21. package/bizar-dash/skills/sdk/SKILL.md +138 -0
  22. package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
  23. package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
  24. package/bizar-dash/skills/usage/SKILL.md +62 -0
  25. package/bizar-dash/src/server/api.mjs +18 -0
  26. package/bizar-dash/src/server/headroom.mjs +645 -0
  27. package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
  28. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  29. package/bizar-dash/src/server/memory-store.mjs +189 -0
  30. package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
  31. package/bizar-dash/src/server/minimax.mjs +196 -5
  32. package/bizar-dash/src/server/providers-store.mjs +956 -0
  33. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  34. package/bizar-dash/src/server/routes/config.mjs +52 -1
  35. package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
  36. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  37. package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
  38. package/bizar-dash/src/server/routes/memory.mjs +668 -1
  39. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
  40. package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
  41. package/bizar-dash/src/server/routes/providers.mjs +266 -5
  42. package/bizar-dash/src/server/routes/skills.mjs +32 -43
  43. package/bizar-dash/src/server/routes/update.mjs +340 -0
  44. package/bizar-dash/src/server/routes/usage.mjs +136 -0
  45. package/bizar-dash/src/server/serve-info.mjs +135 -4
  46. package/bizar-dash/src/server/server.mjs +20 -0
  47. package/bizar-dash/src/server/skills-store.mjs +152 -262
  48. package/bizar-dash/src/web/App.tsx +120 -29
  49. package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
  50. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  51. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  52. package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
  53. package/bizar-dash/src/web/components/Topbar.tsx +2 -1
  54. package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
  55. package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
  56. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
  57. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
  58. package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
  59. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
  60. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
  61. package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
  62. package/bizar-dash/src/web/lib/api.ts +43 -0
  63. package/bizar-dash/src/web/lib/types.ts +16 -0
  64. package/bizar-dash/src/web/main.tsx +2 -0
  65. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
  66. package/bizar-dash/src/web/styles/chat.css +135 -1
  67. package/bizar-dash/src/web/styles/main.css +46 -0
  68. package/bizar-dash/src/web/styles/memory.css +955 -0
  69. package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
  70. package/bizar-dash/src/web/styles/settings.css +418 -0
  71. package/bizar-dash/src/web/styles/skills.css +302 -0
  72. package/bizar-dash/src/web/styles/tasks.css +288 -0
  73. package/bizar-dash/src/web/views/Chat.tsx +276 -48
  74. package/bizar-dash/src/web/views/Config.tsx +3 -2065
  75. package/bizar-dash/src/web/views/Memory.tsx +140 -0
  76. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
  77. package/bizar-dash/src/web/views/Overview.tsx +3 -0
  78. package/bizar-dash/src/web/views/Settings.tsx +36 -0
  79. package/bizar-dash/src/web/views/Skills.tsx +208 -260
  80. package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
  81. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  82. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  83. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  84. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
  85. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  86. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  87. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  88. package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
  89. package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
  90. package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
  91. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  92. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  93. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  94. package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
  95. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  96. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  97. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  98. package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
  99. package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
  100. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  101. package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
  102. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
  103. package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
  104. package/bizar-dash/tests/skills-list.test.mjs +232 -0
  105. package/bizar-dash/tests/skills-search.test.mjs +222 -0
  106. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  107. package/bizar-dash/tests/tasks-create.test.mjs +187 -0
  108. package/bizar-dash/tests/update-check.test.mjs +127 -0
  109. package/bizar-dash/tests/update-run.test.mjs +266 -0
  110. package/cli/bin.mjs +247 -1
  111. package/cli/provision.mjs +118 -4
  112. package/config/agents/_shared/SKILLS.md +109 -0
  113. package/package.json +1 -1
  114. package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
  115. package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
  116. package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
  117. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
  118. package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
@@ -148,6 +148,22 @@ export const DEFAULT_SETTINGS = {
148
148
  baseUrl: 'https://www.minimax.io',
149
149
  chatBaseUrl: 'https://api.minimax.io/v1',
150
150
  },
151
+ // v5.0.0 — Headroom context compression integration.
152
+ // Headroom sits between opencode and LLM providers, compressing tool
153
+ // outputs, logs, RAG chunks, and conversation history by 60–95%.
154
+ headroom: {
155
+ enabled: true,
156
+ autoInstall: true,
157
+ port: 8787,
158
+ host: '127.0.0.1',
159
+ outputShaper: false,
160
+ telemetry: false,
161
+ budget: 0,
162
+ backend: 'anthropic',
163
+ autoStart: true,
164
+ autoWrap: true,
165
+ routeAllProviders: true,
166
+ },
151
167
  };
152
168
 
153
169
  /**
@@ -174,6 +190,7 @@ export function mergeSettings(existing) {
174
190
  merged.agents = { ...DEFAULT_SETTINGS.agents, ...(existing.agents || {}) };
175
191
  merged.systemLlm = { ...DEFAULT_SETTINGS.systemLlm, ...(existing.systemLlm || {}) };
176
192
  merged.minimax = { ...DEFAULT_SETTINGS.minimax, ...(existing.minimax || {}) };
193
+ merged.headroom = { ...DEFAULT_SETTINGS.headroom, ...(existing.headroom || {}) };
177
194
  // Always use the package version — never let user settings override it
178
195
  merged.about.version = DEFAULT_SETTINGS.about.version;
179
196
  return merged;
@@ -8,6 +8,8 @@
8
8
  * /api/config/providers (POST) — add
9
9
  * /api/config/providers/:id (PUT) — update
10
10
  * /api/config/providers/:id (DELETE) — remove
11
+ * /api/config/providers/auto (POST) — add with key, auto-discover models
12
+ * + apply systemLlm config
11
13
  * /api/config/mcps — list MCP servers
12
14
  * /api/config/mcps (POST) — add
13
15
  * /api/config/mcps/:id (PUT) — update
@@ -22,7 +24,12 @@
22
24
  import { Router } from 'express';
23
25
  import { existsSync, mkdirSync } from 'node:fs';
24
26
  import { dirname } from 'node:path';
25
- import { providersStore, mcpsStore } from '../providers-store.mjs';
27
+ import {
28
+ providersStore,
29
+ mcpsStore,
30
+ loadConfig,
31
+ saveConfig,
32
+ } from '../providers-store.mjs';
26
33
  import { OPENCODE_JSON, atomicWriteJson, safeReadJSON, wrap } from './_shared.mjs';
27
34
 
28
35
  /**
@@ -101,11 +108,55 @@ export function createConfigRouter({ state, watcher }) {
101
108
  res.status(204).end();
102
109
  }));
103
110
 
111
+ // v4.4.14 — "Add with key" — the user pastes a key, we figure out the
112
+ // rest. Auto-discovers the live model list (probes /v1/models), writes
113
+ // the full provider block, and (if requested) applies the system LLM
114
+ // config so the rest of the dashboard's /api/llm/* calls hit this
115
+ // provider+model.
116
+ router.post('/config/providers/auto', wrap(async (req, res) => {
117
+ const body = req.body || {};
118
+ const result = await providersStore.addWithAuto({
119
+ id: typeof body.id === 'string' ? body.id.trim() : undefined,
120
+ name: typeof body.name === 'string' ? body.name.trim() : undefined,
121
+ apiKey: typeof body.apiKey === 'string' ? body.apiKey : '',
122
+ groupId: typeof body.groupId === 'string' ? body.groupId : 'default',
123
+ setAsSystemLlm: body.setAsSystemLlm !== false,
124
+ preferredModel: typeof body.preferredModel === 'string' ? body.preferredModel : undefined,
125
+ systemLlmModel: typeof body.systemLlmModel === 'string' ? body.systemLlmModel : undefined,
126
+ });
127
+ if (!result.ok) {
128
+ res.status(result.error === 'unknown_provider' ? 400 : 400).json(result);
129
+ return;
130
+ }
131
+ res.status(201).json(result);
132
+ }));
133
+
104
134
  // ── /api/config/mcps ───────────────────────────────────────────────────
105
135
  router.get('/config/mcps', wrap(async (_req, res) => {
106
136
  res.json({ mcps: mcpsStore.list() });
107
137
  }));
108
138
 
139
+ // v4.4.14 — System LLM settings. The dashboard reads / writes the
140
+ // opencode.json#systemLlm block via this thin surface. The block
141
+ // controls which provider+model is used for the dashboard's own
142
+ // /api/llm/* calls (auto-title, enhance-prompt, summarization).
143
+ router.get('/llm/system-llm', wrap(async (_req, res) => {
144
+ const cfg = loadConfig();
145
+ res.json(cfg.systemLlm || { enabled: false, provider: null, model: null });
146
+ }));
147
+
148
+ router.put('/llm/system-llm', wrap(async (req, res) => {
149
+ const cfg = loadConfig();
150
+ const body = req.body || {};
151
+ cfg.systemLlm = {
152
+ enabled: body.enabled !== false,
153
+ provider: typeof body.provider === 'string' ? body.provider : (cfg.systemLlm?.provider || null),
154
+ model: typeof body.model === 'string' ? body.model : (cfg.systemLlm?.model || null),
155
+ };
156
+ saveConfig(cfg);
157
+ res.json(cfg.systemLlm);
158
+ }));
159
+
109
160
  router.post('/config/mcps', wrap(async (req, res) => {
110
161
  const mcp = mcpsStore.add(req.body || {});
111
162
  res.status(201).json(mcp);
@@ -0,0 +1,165 @@
1
+ /**
2
+ * src/server/routes/env-vars.mjs
3
+ *
4
+ * v1 — Bizar env vars stored in ~/.config/bizar/env.json (mode 0600).
5
+ *
6
+ * Endpoints:
7
+ * GET /api/env-vars — list all (masked values)
8
+ * POST /api/env-vars — create {name, value}
9
+ * PUT /api/env-vars/:name — update {value}
10
+ * DELETE /api/env-vars/:name — remove + delete from process.env
11
+ * POST /api/env-vars/:name/test — {referenced, inProcessEnv}
12
+ *
13
+ * Name must match /^BIZAR_[A-Z0-9_]+$/.
14
+ * On startup, loadEnvJson() sets process.env[name] = value for every entry.
15
+ */
16
+ import { Router } from 'express';
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { wrap } from './_shared.mjs';
21
+
22
+ const HOME = homedir();
23
+ const BIZAR_DIR = join(HOME, '.config', 'bizar');
24
+ const ENV_FILE = join(BIZAR_DIR, 'env.json');
25
+
26
+ // Lazy-load store
27
+ let _store = null;
28
+
29
+ function readStore() {
30
+ if (_store !== null) return _store;
31
+ if (!existsSync(ENV_FILE)) {
32
+ _store = {};
33
+ return _store;
34
+ }
35
+ try {
36
+ _store = JSON.parse(readFileSync(ENV_FILE, 'utf8')) || {};
37
+ } catch {
38
+ _store = {};
39
+ }
40
+ return _store;
41
+ }
42
+
43
+ function writeStore(store) {
44
+ mkdirSync(BIZAR_DIR, { recursive: true });
45
+ const tmp = `${ENV_FILE}.tmp.${process.pid}`;
46
+ writeFileSync(tmp, JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
47
+ renameSync(tmp, ENV_FILE);
48
+ _store = store;
49
+ }
50
+
51
+ /**
52
+ * Load env.json into process.env at startup.
53
+ * Called once from server.mjs after createApiRouter().
54
+ */
55
+ export function loadEnvJson() {
56
+ const store = readStore();
57
+ for (const [name, entry] of Object.entries(store)) {
58
+ if (name.startsWith('BIZAR_')) {
59
+ process.env[name] = entry.value;
60
+ }
61
+ }
62
+ }
63
+
64
+ /** Reset in-memory store — used by tests to ensure isolation between test cases. */
65
+ export function resetStore() {
66
+ _store = null;
67
+ }
68
+
69
+ /** Mask a value — return last 4 chars, prefixed with ***. */
70
+ function mask(value) {
71
+ if (!value || value.length <= 4) return '****';
72
+ return '*'.repeat(Math.max(0, value.length - 4)) + value.slice(-4);
73
+ }
74
+
75
+ const NAME_RE = /^BIZAR_[A-Z0-9_]+$/;
76
+
77
+ export function createEnvVarsRouter() {
78
+ const router = Router();
79
+
80
+ // GET /api/env-vars — list all (masked)
81
+ router.get('/env-vars', wrap(async (_req, res) => {
82
+ const store = readStore();
83
+ const list = Object.entries(store).map(([name, entry]) => ({
84
+ name,
85
+ value: mask(entry.value),
86
+ createdAt: entry.createdAt,
87
+ source: entry.source,
88
+ }));
89
+ res.json(list);
90
+ }));
91
+
92
+ // POST /api/env-vars — create
93
+ router.post('/env-vars', wrap(async (req, res) => {
94
+ const { name, value } = req.body || {};
95
+ if (!name || typeof name !== 'string') {
96
+ res.status(400).json({ error: 'name_required', message: 'name is required' });
97
+ return;
98
+ }
99
+ if (!NAME_RE.test(name)) {
100
+ res.status(400).json({
101
+ error: 'invalid_name',
102
+ message: 'name must match /^BIZAR_[A-Z0-9_]+$/',
103
+ });
104
+ return;
105
+ }
106
+ if (!value || typeof value !== 'string') {
107
+ res.status(400).json({ error: 'value_required', message: 'value is required' });
108
+ return;
109
+ }
110
+ const store = readStore();
111
+ if (store[name]) {
112
+ res.status(409).json({ error: 'already_exists', message: `${name} already exists` });
113
+ return;
114
+ }
115
+ const entry = { value, createdAt: new Date().toISOString(), source: 'dashboard' };
116
+ store[name] = entry;
117
+ writeStore(store);
118
+ process.env[name] = value;
119
+ res.status(201).json({ name, value: mask(value), createdAt: entry.createdAt, source: entry.source });
120
+ }));
121
+
122
+ // PUT /api/env-vars/:name — update
123
+ router.put('/env-vars/:name', wrap(async (req, res) => {
124
+ const { name } = req.params;
125
+ const { value } = req.body || {};
126
+ if (!value || typeof value !== 'string') {
127
+ res.status(400).json({ error: 'value_required', message: 'value is required' });
128
+ return;
129
+ }
130
+ const store = readStore();
131
+ if (!store[name]) {
132
+ res.status(404).json({ error: 'not_found', message: `${name} not found` });
133
+ return;
134
+ }
135
+ store[name] = { ...store[name], value };
136
+ writeStore(store);
137
+ process.env[name] = value;
138
+ res.json({ name, value: mask(value), createdAt: store[name].createdAt, source: store[name].source });
139
+ }));
140
+
141
+ // DELETE /api/env-vars/:name — remove
142
+ router.delete('/env-vars/:name', wrap(async (req, res) => {
143
+ const { name } = req.params;
144
+ const store = readStore();
145
+ if (!store[name]) {
146
+ res.status(404).json({ error: 'not_found', message: `${name} not found` });
147
+ return;
148
+ }
149
+ delete store[name];
150
+ writeStore(store);
151
+ delete process.env[name];
152
+ res.json({ ok: true });
153
+ }));
154
+
155
+ // POST /api/env-vars/:name/test — check if referenced
156
+ router.post('/env-vars/:name/test', wrap(async (req, res) => {
157
+ const { name } = req.params;
158
+ const store = readStore();
159
+ const inProcessEnv = name in process.env;
160
+ // Conservative: we don't statically analyze code, so referenced = inProcessEnv for now
161
+ res.json({ referenced: inProcessEnv, inProcessEnv });
162
+ }));
163
+
164
+ return router;
165
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * src/server/routes/headroom.mjs
3
+ *
4
+ * v1.0.0 — REST endpoints for Headroom management.
5
+ *
6
+ * Mounted at /api/headroom/* by api.mjs.
7
+ */
8
+ import { Router } from 'express';
9
+ import {
10
+ getHeadroomStatus,
11
+ getHeadroomStats,
12
+ installHeadroom,
13
+ wrapOpencode,
14
+ unwrapOpencode,
15
+ startProxy,
16
+ stopProxy,
17
+ getOpencodeConfig,
18
+ headroomStartupHook,
19
+ } from '../headroom.mjs';
20
+ import { wrap } from './_shared.mjs';
21
+
22
+ export function createHeadroomRouter() {
23
+ const router = Router();
24
+
25
+ // GET /api/headroom/status
26
+ router.get('/status', wrap(async (_req, res) => {
27
+ const status = await getHeadroomStatus();
28
+ res.json(status);
29
+ }));
30
+
31
+ // GET /api/headroom/stats?hours=24
32
+ router.get('/stats', wrap(async (req, res) => {
33
+ const hours = Math.max(1, Math.min(parseInt(req.query.hours, 10) || 24, 720));
34
+ const stats = await getHeadroomStats({ hours });
35
+ res.json(stats);
36
+ }));
37
+
38
+ // POST /api/headroom/install
39
+ router.post('/install', wrap(async (req, res) => {
40
+ const { force = false } = req.body || {};
41
+ const result = await installHeadroom({ force: Boolean(force) });
42
+ res.json(result);
43
+ }));
44
+
45
+ // POST /api/headroom/wrap
46
+ router.post('/wrap', wrap(async (req, res) => {
47
+ const { port } = req.body || {};
48
+ const result = await wrapOpencode({
49
+ port: port ? Math.max(1, Math.min(parseInt(port, 10), 65535)) : 8787,
50
+ });
51
+ res.json(result);
52
+ }));
53
+
54
+ // POST /api/headroom/unwrap
55
+ router.post('/unwrap', wrap(async (_req, res) => {
56
+ const result = await unwrapOpencode();
57
+ res.json(result);
58
+ }));
59
+
60
+ // POST /api/headroom/proxy/start
61
+ router.post('/proxy/start', wrap(async (req, res) => {
62
+ const { port, host } = req.body || {};
63
+ const result = await startProxy({
64
+ port: port ? Math.max(1, Math.min(parseInt(port, 10), 65535)) : 8787,
65
+ host: typeof host === 'string' && host ? host : '127.0.0.1',
66
+ });
67
+ res.json(result);
68
+ }));
69
+
70
+ // POST /api/headroom/proxy/stop
71
+ router.post('/proxy/stop', wrap(async (_req, res) => {
72
+ const result = await stopProxy();
73
+ res.json(result);
74
+ }));
75
+
76
+ // GET /api/headroom/opencode-config
77
+ router.get('/opencode-config', wrap(async (_req, res) => {
78
+ const config = await getOpencodeConfig();
79
+ res.json(config);
80
+ }));
81
+
82
+ // POST /api/headroom/auto-route — configure all providers to route through Headroom
83
+ router.post('/auto-route', wrap(async (_req, res) => {
84
+ const { loadConfig, saveConfig } = await import('../providers-store.mjs');
85
+ const cfg = loadConfig();
86
+ const headroomPort = 8787;
87
+ const proxyUrl = `http://127.0.0.1:${headroomPort}/v1`;
88
+
89
+ // For each provider, prepend the headroom proxy URL to baseURL
90
+ const providers = cfg.provider || {};
91
+ let changed = 0;
92
+ for (const [id, p] of Object.entries(providers)) {
93
+ if (!p || typeof p !== 'object') continue;
94
+ const baseURL = p.baseURL || p.options?.baseURL || '';
95
+ // Only update if not already routed
96
+ if (baseURL && !baseURL.includes('127.0.0.1:8787') && !baseURL.includes('localhost:8787')) {
97
+ const newBaseURL = `${proxyUrl}/${baseURL.replace(/^https?:\/\//, '')}`;
98
+ if (p.options) {
99
+ providers[id] = { ...p, options: { ...p.options, baseURL: newBaseURL } };
100
+ } else {
101
+ providers[id] = { ...p, baseURL: newBaseURL };
102
+ }
103
+ changed++;
104
+ }
105
+ }
106
+ cfg.provider = providers;
107
+ saveConfig(cfg);
108
+
109
+ res.json({ ok: true, changed, message: `Updated ${changed} provider(s) to route through Headroom proxy.` });
110
+ }));
111
+
112
+ // POST /api/headroom/startup-hook — trigger the startup hook programmatically
113
+ router.post('/startup-hook', wrap(async (_req, res) => {
114
+ const { readSettings } = await import('./_shared.mjs');
115
+ const settings = readSettings();
116
+ const headroomSettings = settings?.data?.headroom;
117
+ if (!headroomSettings) {
118
+ res.status(400).json({ error: 'bad_request', message: 'Headroom settings not found in settings.json' });
119
+ return;
120
+ }
121
+ const result = await headroomStartupHook(headroomSettings);
122
+ res.json(result);
123
+ }));
124
+
125
+ return router;
126
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * src/server/routes/lightrag.mjs
3
+ *
4
+ * v4.6.0 — LightRAG settings endpoints.
5
+ *
6
+ * Mounted at /api/lightrag/*. Used by the Settings view (handled by
7
+ * the sibling thor-settings agent) to surface and override the
8
+ * free-tier opencode Zen defaults that lightrag starts with.
9
+ *
10
+ * Endpoints:
11
+ * GET /lightrag/defaults — current default models + provenance
12
+ * PUT /lightrag/defaults — set env-var overrides (process-scoped, not persisted)
13
+ * GET /lightrag/status — current running config (delegates to resolveLightRAGConfig)
14
+ *
15
+ * The PUT endpoint mutates `process.env` for the lifetime of the
16
+ * dashboard server process. We do NOT persist this to disk because:
17
+ * 1. env-var overrides are conventionally process-scoped, and
18
+ * 2. the proper way to persist is via PUT /api/memory/config which
19
+ * writes to .bizar/memory.json (see routes/memory.mjs).
20
+ */
21
+ import { Router } from 'express';
22
+ import { existsSync, readFileSync, statSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import {
25
+ getDefaultLightRAGConfig,
26
+ LIGHTRAG_DEFAULT_LLM,
27
+ LIGHTRAG_DEFAULT_EMBEDDING,
28
+ } from '../memory-store.mjs';
29
+ import { resolveLightRAGConfig } from '../memory-lightrag.mjs';
30
+ import { wrap } from './_shared.mjs';
31
+
32
+ /**
33
+ * @param {object} deps
34
+ * @param {string} deps.projectRoot
35
+ * @returns {import('express').Router}
36
+ */
37
+ export function createLightragRouter({ projectRoot }) {
38
+ const router = Router();
39
+
40
+ // GET /api/lightrag/defaults — current defaults + where they came from.
41
+ router.get('/lightrag/defaults', wrap(async (_req, res) => {
42
+ const cfg = getDefaultLightRAGConfig();
43
+ res.json({
44
+ llm: cfg.llm,
45
+ embedding: cfg.embedding,
46
+ source: cfg.source,
47
+ llmSource: cfg.llmSource,
48
+ embeddingSource: cfg.embeddingSource,
49
+ builtin: {
50
+ llm: LIGHTRAG_DEFAULT_LLM,
51
+ embedding: LIGHTRAG_DEFAULT_EMBEDDING,
52
+ },
53
+ envVars: {
54
+ BIZAR_LIGHTRAG_LLM: process.env.BIZAR_LIGHTRAG_LLM || null,
55
+ BIZAR_LIGHTRAG_EMBEDDING: process.env.BIZAR_LIGHTRAG_EMBEDDING || null,
56
+ },
57
+ });
58
+ }));
59
+
60
+ // PUT /api/lightrag/defaults — set env-var overrides.
61
+ // Body: { llm?: string, embedding?: string }
62
+ // Both fields are optional; only the supplied ones are set. To clear
63
+ // an override, send an empty string ''.
64
+ router.put('/lightrag/defaults', wrap(async (req, res) => {
65
+ const body = req.body || {};
66
+ const updates = {};
67
+ if (Object.prototype.hasOwnProperty.call(body, 'llm')) {
68
+ const v = typeof body.llm === 'string' ? body.llm.trim() : '';
69
+ if (v) {
70
+ process.env.BIZAR_LIGHTRAG_LLM = v;
71
+ updates.llm = v;
72
+ } else {
73
+ delete process.env.BIZAR_LIGHTRAG_LLM;
74
+ updates.llmCleared = true;
75
+ }
76
+ }
77
+ if (Object.prototype.hasOwnProperty.call(body, 'embedding')) {
78
+ const v = typeof body.embedding === 'string' ? body.embedding.trim() : '';
79
+ if (v) {
80
+ process.env.BIZAR_LIGHTRAG_EMBEDDING = v;
81
+ updates.embedding = v;
82
+ } else {
83
+ delete process.env.BIZAR_LIGHTRAG_EMBEDDING;
84
+ updates.embeddingCleared = true;
85
+ }
86
+ }
87
+ const next = getDefaultLightRAGConfig();
88
+ res.json({
89
+ ok: true,
90
+ applied: updates,
91
+ llm: next.llm,
92
+ embedding: next.embedding,
93
+ source: next.source,
94
+ });
95
+ }));
96
+
97
+ // GET /api/lightrag/status — current resolved config + server status.
98
+ // Mirrors /api/memory/lightrag/status but is the canonical surface
99
+ // for the Settings view.
100
+ router.get('/lightrag/status', wrap(async (_req, res) => {
101
+ const cfg = resolveLightRAGConfig(projectRoot);
102
+ const pidFile = join(cfg.workingDir, 'lightrag.pid');
103
+ let pid = null;
104
+ let alive = false;
105
+ if (existsSync(pidFile)) {
106
+ try {
107
+ pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
108
+ if (Number.isFinite(pid) && pid > 0) {
109
+ try {
110
+ process.kill(pid, 0);
111
+ alive = true;
112
+ } catch {
113
+ alive = false;
114
+ }
115
+ } else {
116
+ pid = null;
117
+ }
118
+ } catch {
119
+ pid = null;
120
+ }
121
+ }
122
+
123
+ const logFile = join(cfg.workingDir, 'lightrag.log');
124
+ const logTail = [];
125
+ if (existsSync(logFile)) {
126
+ try {
127
+ const content = readFileSync(logFile, 'utf8');
128
+ const lines = content.split('\n');
129
+ logTail.push(...lines.slice(-30));
130
+ } catch { /* ignore */ }
131
+ }
132
+
133
+ let logSize = 0;
134
+ try {
135
+ if (existsSync(logFile)) logSize = statSync(logFile).size;
136
+ } catch { /* ignore */ }
137
+
138
+ res.json({
139
+ running: alive,
140
+ pid: alive ? pid : null,
141
+ host: cfg.host,
142
+ port: cfg.port,
143
+ llmBinding: cfg.llmBinding,
144
+ embeddingBinding: cfg.embeddingBinding,
145
+ llmModel: cfg.llmModel,
146
+ embeddingModel: cfg.embeddingModel,
147
+ workingDir: cfg.workingDir,
148
+ logSize,
149
+ logTail,
150
+ });
151
+ }));
152
+
153
+ return router;
154
+ }