@polderlabs/bizar 4.4.13 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.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 +96 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/lightrag/SKILL.md +75 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +55 -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 +12 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
- package/bizar-dash/src/server/memory-store.mjs +38 -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/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +241 -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 +4 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +118 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +0 -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/main.tsx +1 -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/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/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Settings.tsx +6 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- 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/lightrag-defaults.node.test.mjs +118 -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/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/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 +82 -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
|
@@ -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,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
|
+
}
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { Router } from 'express';
|
|
13
13
|
import { join, dirname } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
16
17
|
|
|
17
18
|
const SERVER_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
19
|
const memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) => m);
|
|
@@ -654,5 +655,244 @@ export function createMemoryRouter({ projectRoot }) {
|
|
|
654
655
|
}
|
|
655
656
|
}));
|
|
656
657
|
|
|
658
|
+
// ── v4.6.0 Global memory-config surface ─────────────────────────────────
|
|
659
|
+
//
|
|
660
|
+
// A second config file at `~/.config/bizar/memory-config.json` holds
|
|
661
|
+
// operator-facing settings that aren't tied to a single project —
|
|
662
|
+
// e.g. the global default LightRAG URL, the global obsidian vault
|
|
663
|
+
// path, and git push defaults. The Settings view (handled by the
|
|
664
|
+
// sibling thor-settings agent) reads + writes this file via these
|
|
665
|
+
// endpoints.
|
|
666
|
+
//
|
|
667
|
+
// Distinct from the per-project `.bizar/memory.json` (the existing
|
|
668
|
+
// /memory/config surfaces that). We use a separate path to keep the
|
|
669
|
+
// two systems cleanly separated.
|
|
670
|
+
|
|
671
|
+
const GLOBAL_MEMORY_CONFIG_PATH = join(
|
|
672
|
+
process.env.HOME || '/tmp',
|
|
673
|
+
'.config',
|
|
674
|
+
'bizar',
|
|
675
|
+
'memory-config.json',
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
function loadGlobalMemoryConfig() {
|
|
679
|
+
try {
|
|
680
|
+
if (!existsSync(GLOBAL_MEMORY_CONFIG_PATH)) {
|
|
681
|
+
return {
|
|
682
|
+
exists: false,
|
|
683
|
+
config: {
|
|
684
|
+
lightrag: { enabled: false, url: 'http://127.0.0.1:9621', llm: '', embedding: '' },
|
|
685
|
+
obsidian: { vaultPath: '', syncInterval: 300 },
|
|
686
|
+
git: { repoPath: '', remoteUrl: '', branch: 'main', autoSync: false },
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const raw = JSON.parse(readFileSync(GLOBAL_MEMORY_CONFIG_PATH, 'utf8'));
|
|
691
|
+
return { exists: true, config: raw };
|
|
692
|
+
} catch {
|
|
693
|
+
return {
|
|
694
|
+
exists: false,
|
|
695
|
+
error: 'corrupt_json',
|
|
696
|
+
config: {
|
|
697
|
+
lightrag: { enabled: false, url: 'http://127.0.0.1:9621', llm: '', embedding: '' },
|
|
698
|
+
obsidian: { vaultPath: '', syncInterval: 300 },
|
|
699
|
+
git: { repoPath: '', remoteUrl: '', branch: 'main', autoSync: false },
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function saveGlobalMemoryConfig(config) {
|
|
706
|
+
const dir = dirname(GLOBAL_MEMORY_CONFIG_PATH);
|
|
707
|
+
if (!existsSync(dir)) {
|
|
708
|
+
mkdirSync(dir, { recursive: true });
|
|
709
|
+
}
|
|
710
|
+
atomicWriteJson(GLOBAL_MEMORY_CONFIG_PATH, config);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// GET /memory/config/global — grouped shape (lightrag/obsidian/git).
|
|
714
|
+
router.get('/memory/config/global', wrap(async (_req, res) => {
|
|
715
|
+
const { exists, config, error } = loadGlobalMemoryConfig();
|
|
716
|
+
res.json({
|
|
717
|
+
exists,
|
|
718
|
+
path: GLOBAL_MEMORY_CONFIG_PATH,
|
|
719
|
+
config,
|
|
720
|
+
...(error ? { error } : {}),
|
|
721
|
+
});
|
|
722
|
+
}));
|
|
723
|
+
|
|
724
|
+
// PUT /memory/config/global — partial update. Validates each block
|
|
725
|
+
// and merges on top of the existing config.
|
|
726
|
+
router.put('/memory/config/global', wrap(async (req, res) => {
|
|
727
|
+
const body = req.body || {};
|
|
728
|
+
const { exists, config: existing } = loadGlobalMemoryConfig();
|
|
729
|
+
|
|
730
|
+
// Light validation — no zod import (keep surface small). Each
|
|
731
|
+
// block is shallow-merged; if a block is present, we shallow-merge
|
|
732
|
+
// its top-level fields, rejecting unknown top-level keys.
|
|
733
|
+
const next = JSON.parse(JSON.stringify(existing));
|
|
734
|
+
|
|
735
|
+
const VALID_TOP_KEYS = new Set(['lightrag', 'obsidian', 'git']);
|
|
736
|
+
const EXTRA_KEYS = Object.keys(body).filter((k) => !VALID_TOP_KEYS.has(k));
|
|
737
|
+
if (EXTRA_KEYS.length > 0) {
|
|
738
|
+
res.status(400).json({
|
|
739
|
+
error: 'bad_request',
|
|
740
|
+
message: `unknown top-level keys: ${EXTRA_KEYS.join(', ')}`,
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (body.lightrag !== undefined) {
|
|
746
|
+
if (typeof body.lightrag !== 'object' || body.lightrag === null || Array.isArray(body.lightrag)) {
|
|
747
|
+
res.status(400).json({ error: 'bad_request', message: 'lightrag must be an object' });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const l = body.lightrag;
|
|
751
|
+
if ('enabled' in l && typeof l.enabled !== 'boolean') {
|
|
752
|
+
res.status(400).json({ error: 'bad_request', message: 'lightrag.enabled must be boolean' });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if ('url' in l && typeof l.url !== 'string') {
|
|
756
|
+
res.status(400).json({ error: 'bad_request', message: 'lightrag.url must be a string' });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if ('llm' in l && typeof l.llm !== 'string') {
|
|
760
|
+
res.status(400).json({ error: 'bad_request', message: 'lightrag.llm must be a string' });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if ('embedding' in l && typeof l.embedding !== 'string') {
|
|
764
|
+
res.status(400).json({ error: 'bad_request', message: 'lightrag.embedding must be a string' });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
next.lightrag = { ...next.lightrag, ...l };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (body.obsidian !== undefined) {
|
|
771
|
+
if (typeof body.obsidian !== 'object' || body.obsidian === null || Array.isArray(body.obsidian)) {
|
|
772
|
+
res.status(400).json({ error: 'bad_request', message: 'obsidian must be an object' });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const o = body.obsidian;
|
|
776
|
+
if ('vaultPath' in o && typeof o.vaultPath !== 'string') {
|
|
777
|
+
res.status(400).json({ error: 'bad_request', message: 'obsidian.vaultPath must be a string' });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if ('syncInterval' in o) {
|
|
781
|
+
const n = Number(o.syncInterval);
|
|
782
|
+
if (!Number.isFinite(n) || n < 0 || n > 86400 * 30) {
|
|
783
|
+
res.status(400).json({ error: 'bad_request', message: 'obsidian.syncInterval must be 0..2592000 seconds' });
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
next.obsidian.syncInterval = n;
|
|
787
|
+
}
|
|
788
|
+
if ('vaultPath' in o) next.obsidian.vaultPath = o.vaultPath;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (body.git !== undefined) {
|
|
792
|
+
if (typeof body.git !== 'object' || body.git === null || Array.isArray(body.git)) {
|
|
793
|
+
res.status(400).json({ error: 'bad_request', message: 'git must be an object' });
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const g = body.git;
|
|
797
|
+
if ('repoPath' in g && typeof g.repoPath !== 'string') {
|
|
798
|
+
res.status(400).json({ error: 'bad_request', message: 'git.repoPath must be a string' });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if ('remoteUrl' in g && typeof g.remoteUrl !== 'string') {
|
|
802
|
+
res.status(400).json({ error: 'bad_request', message: 'git.remoteUrl must be a string' });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if ('branch' in g) {
|
|
806
|
+
if (typeof g.branch !== 'string' || !/^[A-Za-z0-9._/-]{1,200}$/.test(g.branch)) {
|
|
807
|
+
res.status(400).json({ error: 'bad_request', message: 'git.branch must be a valid git ref name' });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
next.git.branch = g.branch;
|
|
811
|
+
}
|
|
812
|
+
if ('autoSync' in g && typeof g.autoSync !== 'boolean') {
|
|
813
|
+
res.status(400).json({ error: 'bad_request', message: 'git.autoSync must be boolean' });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if ('repoPath' in g) next.git.repoPath = g.repoPath;
|
|
817
|
+
if ('remoteUrl' in g) next.git.remoteUrl = g.remoteUrl;
|
|
818
|
+
if ('autoSync' in g) next.git.autoSync = g.autoSync;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
saveGlobalMemoryConfig(next);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
res.status(500).json({ error: 'write_failed', message: err.message });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
res.json({ ok: true, exists: true, path: GLOBAL_MEMORY_CONFIG_PATH, config: next });
|
|
828
|
+
}));
|
|
829
|
+
|
|
830
|
+
// POST /memory/test-git — test the configured git repo. Returns:
|
|
831
|
+
// { ok: bool, checks: [...], message?: string }
|
|
832
|
+
// Steps performed:
|
|
833
|
+
// 1. Path exists?
|
|
834
|
+
// 2. Is a directory?
|
|
835
|
+
// 3. Is a valid git repo (has .git/)?
|
|
836
|
+
// 4. (Optional) Has a configured remote?
|
|
837
|
+
// 5. (Optional) Can we push (skipped unless ?push=1)?
|
|
838
|
+
router.post('/memory/test-git', wrap(async (req, res) => {
|
|
839
|
+
const { config } = loadGlobalMemoryConfig();
|
|
840
|
+
const repoPath = config?.git?.repoPath || '';
|
|
841
|
+
const remoteUrl = config?.git?.remoteUrl || '';
|
|
842
|
+
const wantPush = req.query.push === '1' || req.body?.push === true;
|
|
843
|
+
|
|
844
|
+
const checks = [];
|
|
845
|
+
if (!repoPath) {
|
|
846
|
+
checks.push({ name: 'repo_path_set', pass: false, detail: 'git.repoPath is empty — set it in Settings first' });
|
|
847
|
+
res.json({ ok: false, checks, message: 'repo path not configured' });
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
checks.push({
|
|
852
|
+
name: 'repo_path_exists',
|
|
853
|
+
pass: existsSync(repoPath),
|
|
854
|
+
detail: existsSync(repoPath) ? repoPath : `${repoPath} does not exist`,
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
let isGit = false;
|
|
858
|
+
let isDir = false;
|
|
859
|
+
try {
|
|
860
|
+
const s = statSync(repoPath);
|
|
861
|
+
isDir = s.isDirectory();
|
|
862
|
+
checks.push({ name: 'repo_is_directory', pass: isDir, detail: isDir ? 'yes' : 'not a directory' });
|
|
863
|
+
} catch (err) {
|
|
864
|
+
checks.push({ name: 'repo_is_directory', pass: false, detail: err.message });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (isDir) {
|
|
868
|
+
isGit = existsSync(join(repoPath, '.git'));
|
|
869
|
+
checks.push({ name: 'is_git_repo', pass: isGit, detail: isGit ? 'yes' : 'no .git/ directory' });
|
|
870
|
+
} else {
|
|
871
|
+
checks.push({ name: 'is_git_repo', pass: false, detail: 'skipped (not a directory)' });
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (remoteUrl) {
|
|
875
|
+
checks.push({ name: 'remote_url_set', pass: true, detail: remoteUrl });
|
|
876
|
+
} else {
|
|
877
|
+
checks.push({ name: 'remote_url_set', pass: false, detail: 'no remote configured' });
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (wantPush && isGit && remoteUrl) {
|
|
881
|
+
try {
|
|
882
|
+
const out = execFileSync('git', ['ls-remote', '--heads', remoteUrl], { timeout: 5000 }).toString();
|
|
883
|
+
checks.push({ name: 'remote_reachable', pass: true, detail: `${out.split('\n').filter(Boolean).length} heads` });
|
|
884
|
+
} catch (err) {
|
|
885
|
+
checks.push({ name: 'remote_reachable', pass: false, detail: err.message });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const ok = checks.every((c) => c.pass);
|
|
890
|
+
res.json({
|
|
891
|
+
ok,
|
|
892
|
+
checks,
|
|
893
|
+
message: ok ? 'git config looks healthy' : 'one or more checks failed',
|
|
894
|
+
});
|
|
895
|
+
}));
|
|
896
|
+
|
|
657
897
|
return router;
|
|
658
898
|
}
|