@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.
- package/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +116 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +86 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +68 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +18 -0
- package/bizar-dash/src/server/headroom.mjs +645 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +189 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +668 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +20 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +120 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +2 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/Memory.tsx +140 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Overview.tsx +3 -0
- package/bizar-dash/src/web/views/Settings.tsx +36 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +247 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- 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 {
|
|
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
|
+
}
|