@polderlabs/bizar 10.0.7 → 10.1.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/src/server/api.mjs +2 -6
- package/bizar-dash/src/server/diagnostics-store.mjs +5 -35
- package/bizar-dash/src/server/mod-security.mjs +1 -2
- package/bizar-dash/src/server/providers-store.mjs +1 -1
- package/bizar-dash/src/server/routes/_shared.mjs +0 -17
- package/bizar-dash/src/server/routes/model-router.mjs +103 -0
- package/bizar-dash/src/server/routes/model-router.test.mjs +76 -0
- package/bizar-dash/src/server/server.mjs +1 -27
- package/bizar-dash/tests/cli-bugfixes.test.mjs +1 -1
- package/bizar-dash/tests/cli-error-visibility.test.mjs +1 -1
- package/bizar-dash/tests/cli-refactor.test.mjs +0 -1
- package/bizar-dash/tests/diagnostics-store.test.mjs +0 -1
- package/cli/bin.mjs +1 -15
- package/cli/cli-commands-validation.test.mjs +1 -1
- package/cli/commands/lightrag.mjs +1 -1
- package/cli/commands/util.mjs +1 -1
- package/cli/copy.mjs +1 -47
- package/cli/doctor.mjs +3 -3
- package/cli/install.mjs +3 -37
- package/cli/service-controller.mjs +0 -192
- package/cli/service-env.mjs +2 -9
- package/cli/utils.mjs +0 -7
- package/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/scripts/check-deps.mjs +0 -24
- package/bizar-dash/skills/headroom/SKILL.md +0 -94
- package/bizar-dash/src/server/headroom.mjs +0 -649
- package/bizar-dash/src/server/routes/headroom.mjs +0 -126
- package/bizar-dash/tests/headroom-install.test.mjs +0 -173
- package/bizar-dash/tests/headroom-settings.test.mjs +0 -126
- package/bizar-dash/tests/headroom-status.test.mjs +0 -117
- package/cli/commands/headroom.mjs +0 -204
|
@@ -32,6 +32,7 @@ import { createArtifactsRouter } from './routes/artifacts.mjs';
|
|
|
32
32
|
import { createSchedulesRouter } from './routes/schedules.mjs';
|
|
33
33
|
import { createModsRouter } from './routes/mods.mjs';
|
|
34
34
|
import { createAgentsRouter } from './routes/agents.mjs';
|
|
35
|
+
import { createModelRouterRouter } from './routes/model-router.mjs';
|
|
35
36
|
import { createBackgroundRouter } from './routes/background.mjs';
|
|
36
37
|
import { createActivityRouter } from './routes/activity.mjs';
|
|
37
38
|
import { createHistoryRouter } from './routes/history.mjs';
|
|
@@ -58,7 +59,6 @@ import { createEnvVarsRouter } from './routes/env-vars.mjs';
|
|
|
58
59
|
import { createUpdateRouter } from './routes/update.mjs';
|
|
59
60
|
import { createSpawnRouter } from './routes/spawn.mjs';
|
|
60
61
|
import { createUsageRouter } from './routes/usage.mjs';
|
|
61
|
-
import { createHeadroomRouter } from './routes/headroom.mjs';
|
|
62
62
|
import { createEvalRouter } from './routes/eval.mjs';
|
|
63
63
|
import { createWorkspacesRouter } from './routes/workspaces.mjs';
|
|
64
64
|
import { createUsersRouter } from './routes/users.mjs';
|
|
@@ -125,6 +125,7 @@ export async function createApiRouter({
|
|
|
125
125
|
router.use(createSchedulesRouter({ broadcast }));
|
|
126
126
|
router.use(createModsRouter());
|
|
127
127
|
router.use(createAgentsRouter({ state, broadcast }));
|
|
128
|
+
router.use(createModelRouterRouter({ state }));
|
|
128
129
|
router.use(createBackgroundRouter({ broadcast }));
|
|
129
130
|
// Sprint S10 — live CC agents + SSE stream for the dashboard output panel.
|
|
130
131
|
router.use(createCCAgentsRouter({ broadcast }));
|
|
@@ -181,11 +182,6 @@ export async function createApiRouter({
|
|
|
181
182
|
router.use(createArtifactsRouter({ state, broadcast, projectRoot }));
|
|
182
183
|
router.use(createMinimaxRouter({ state, broadcast }));
|
|
183
184
|
router.use(createUsageRouter());
|
|
184
|
-
// v5.0.0 — Headroom context compression endpoints.
|
|
185
|
-
// Mounted at /api/headroom/* so the mount-prefix stripping works correctly.
|
|
186
|
-
// Each route handler inside the router is at its bare path (e.g. '/status'),
|
|
187
|
-
// which becomes '/api/headroom/status' at the top level.
|
|
188
|
-
router.use('/headroom', createHeadroomRouter());
|
|
189
185
|
// v5.0.0 — Eval framework endpoints.
|
|
190
186
|
router.use(createEvalRouter({ state, broadcast }));
|
|
191
187
|
// v5.0.0 — Workspace and user management endpoints.
|
|
@@ -119,10 +119,7 @@ export function getRecentErrors({ since = 0, limit = 200 } = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* v6.0.0 —
|
|
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.
|
|
122
|
+
* v6.0.0 — Generic TCP probe used by the Doctor page.
|
|
126
123
|
*
|
|
127
124
|
* @param {string} host
|
|
128
125
|
* @param {number} port
|
|
@@ -184,23 +181,6 @@ async function probeLightRAG() {
|
|
|
184
181
|
}
|
|
185
182
|
}
|
|
186
183
|
|
|
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
184
|
/**
|
|
205
185
|
* v6.0.0 — Probe whether the cline plugin's `serve-info.json` is
|
|
206
186
|
* reachable on disk and parseable. The dashboard reads serve-info to
|
|
@@ -321,12 +301,11 @@ async function runConfigChecks() {
|
|
|
321
301
|
}
|
|
322
302
|
|
|
323
303
|
/**
|
|
324
|
-
* v6.0.0 — Service health checks. TCP
|
|
325
|
-
*
|
|
326
|
-
*
|
|
304
|
+
* v6.0.0 — Service health checks. TCP probe for lightrag
|
|
305
|
+
* (best-effort), cline plugin file probe, and the dashboard itself
|
|
306
|
+
* (always `ok` — we're running because we got here).
|
|
327
307
|
*/
|
|
328
308
|
async function runServiceChecks() {
|
|
329
|
-
const headroom = await probeHeadroom();
|
|
330
309
|
const lightrag = await probeLightRAG();
|
|
331
310
|
const serveInfo = clineServeInfo();
|
|
332
311
|
const checks = [
|
|
@@ -335,13 +314,6 @@ async function runServiceChecks() {
|
|
|
335
314
|
status: 'ok',
|
|
336
315
|
message: `running on pid ${process.pid}, up ${Math.floor(process.uptime())}s`,
|
|
337
316
|
},
|
|
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
317
|
{
|
|
346
318
|
name: 'lightrag',
|
|
347
319
|
status: lightrag.running ? 'ok' : 'warn',
|
|
@@ -447,7 +419,7 @@ export async function health() {
|
|
|
447
419
|
* uptime: number,
|
|
448
420
|
* memory: NodeJS.MemoryUsage,
|
|
449
421
|
* disk: { exists: boolean, path: string, sizeBytes: number },
|
|
450
|
-
* services: { dashboard: object,
|
|
422
|
+
* services: { dashboard: object, lightrag: object, cline: object },
|
|
451
423
|
* counts: ReturnType<typeof collectCounts>,
|
|
452
424
|
* recentErrors: ReturnType<typeof getRecentErrors>,
|
|
453
425
|
* configHealth: Array<{ name: string, status: string, message: string }>,
|
|
@@ -467,7 +439,6 @@ export async function collectDiagnostics() {
|
|
|
467
439
|
let status = 'ok';
|
|
468
440
|
if (issues.some((c) => c.status === 'fail')) status = 'fail';
|
|
469
441
|
else if (issues.length > 0) status = 'warn';
|
|
470
|
-
const headroom = await probeHeadroom();
|
|
471
442
|
const lightrag = await probeLightRAG();
|
|
472
443
|
const serveInfo = clineServeInfo();
|
|
473
444
|
const clineConfig = providersStore.CLINE_JSON;
|
|
@@ -505,7 +476,6 @@ export async function collectDiagnostics() {
|
|
|
505
476
|
logPath: dashboardLogPath,
|
|
506
477
|
logExists: dashboardLogExists,
|
|
507
478
|
},
|
|
508
|
-
headroom,
|
|
509
479
|
lightrag,
|
|
510
480
|
cline: serveInfo,
|
|
511
481
|
},
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Path-traversal attacks (e.g. `../`) are blocked.
|
|
23
23
|
* 3. **Subprocess allowlist** — only whitelisted binaries
|
|
24
24
|
* (`bizar`, `cline`, `python3`, `graphify`, `git`, `node`,
|
|
25
|
-
* `npm`, `pip`, `pipx`, `uv
|
|
25
|
+
* `npm`, `pip`, `pipx`, `uv`) can be spawned. Custom
|
|
26
26
|
* binaries require an explicit `process:spawn:<bin>` permission.
|
|
27
27
|
* 4. **Audit log** — every privileged operation (fs read/write,
|
|
28
28
|
* process spawn, network fetch) is logged to
|
|
@@ -73,7 +73,6 @@ export const ALLOWED_BINARIES = new Set([
|
|
|
73
73
|
'npm',
|
|
74
74
|
'npx',
|
|
75
75
|
'git',
|
|
76
|
-
'headroom',
|
|
77
76
|
'jq',
|
|
78
77
|
]);
|
|
79
78
|
|
|
@@ -164,7 +164,7 @@ export { readClineJsonCached, invalidateClineJsonCache, CLINE_JSON_CACHE_TTL_MS
|
|
|
164
164
|
// MAX_ROTATION_ATTEMPTS: bound on the number of times `withKeyRotation`
|
|
165
165
|
// will retry the user's fn with a different key before throwing. With
|
|
166
166
|
// `cooldown` skipping, this matches the typical "1 primary + 1 backup"
|
|
167
|
-
// deployment (2 attempts) with
|
|
167
|
+
// deployment (2 attempts) with a 3rd emergency key.
|
|
168
168
|
//
|
|
169
169
|
// ROTATION_ERROR_PATTERNS: substring match against the lowercased error
|
|
170
170
|
// message — used as a fallback when no structured `status` is present
|
|
@@ -148,22 +148,6 @@ 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 cline 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
|
-
},
|
|
167
151
|
};
|
|
168
152
|
|
|
169
153
|
/**
|
|
@@ -190,7 +174,6 @@ export function mergeSettings(existing) {
|
|
|
190
174
|
merged.agents = { ...DEFAULT_SETTINGS.agents, ...(existing.agents || {}) };
|
|
191
175
|
merged.systemLlm = { ...DEFAULT_SETTINGS.systemLlm, ...(existing.systemLlm || {}) };
|
|
192
176
|
merged.minimax = { ...DEFAULT_SETTINGS.minimax, ...(existing.minimax || {}) };
|
|
193
|
-
merged.headroom = { ...DEFAULT_SETTINGS.headroom, ...(existing.headroom || {}) };
|
|
194
177
|
// Always use the package version — never let user settings override it
|
|
195
178
|
merged.about.version = DEFAULT_SETTINGS.about.version;
|
|
196
179
|
return merged;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/model-router.mjs
|
|
3
|
+
*
|
|
4
|
+
* Surfaces the local model-router configuration and live model list.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/model-router — full policy + agent→model map
|
|
7
|
+
* GET /api/model-router/models — live model list from the local router
|
|
8
|
+
* GET /api/model-router/agents — agent→model map only
|
|
9
|
+
*
|
|
10
|
+
* The local router URL defaults to ANTHROPIC_BASE_URL or
|
|
11
|
+
* BIZAR_MODEL_ROUTER_URL or `http://localhost:20128/v1`.
|
|
12
|
+
*/
|
|
13
|
+
import { Router } from 'express';
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { resolve, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { wrap } from './_shared.mjs';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
// .claude/model-router.json — repo root.
|
|
21
|
+
// From src/server/routes/ up to repo root is 3 levels (routes -> server -> src -> repo).
|
|
22
|
+
const ROUTER_POLICY_PATH = resolve(__dirname, '../../../../.claude/model-router.json');
|
|
23
|
+
|
|
24
|
+
const DEFAULT_ENDPOINT = 'http://localhost:20128/v1';
|
|
25
|
+
|
|
26
|
+
function resolveEndpoint(state) {
|
|
27
|
+
const fromEnv =
|
|
28
|
+
process.env.BIZAR_MODEL_ROUTER_URL ||
|
|
29
|
+
process.env.ANTHROPIC_BASE_URL ||
|
|
30
|
+
state?.config?.modelRouterUrl;
|
|
31
|
+
return (fromEnv || DEFAULT_ENDPOINT).replace(/\/+$/, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadPolicy() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readFile(ROUTER_POLICY_PATH, 'utf8');
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
return {
|
|
40
|
+
version: '0.0.0',
|
|
41
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
42
|
+
tiers: {},
|
|
43
|
+
agents: {},
|
|
44
|
+
policies: {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchLiveModels(endpoint) {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`${endpoint}/models`, {
|
|
52
|
+
signal: AbortSignal.timeout(2000),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) return { reachable: false, error: `HTTP ${res.status}` };
|
|
55
|
+
const body = await res.json();
|
|
56
|
+
return {
|
|
57
|
+
reachable: true,
|
|
58
|
+
endpoint,
|
|
59
|
+
models: Array.isArray(body?.data) ? body.data : [],
|
|
60
|
+
};
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
reachable: false,
|
|
64
|
+
endpoint,
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {object} deps
|
|
72
|
+
* @param {object} deps.state
|
|
73
|
+
* @returns {import('express').Router}
|
|
74
|
+
*/
|
|
75
|
+
export function createModelRouterRouter({ state }) {
|
|
76
|
+
const router = Router();
|
|
77
|
+
|
|
78
|
+
router.get('/model-router', wrap(async (_req, res) => {
|
|
79
|
+
const policy = await loadPolicy();
|
|
80
|
+
const endpoint = resolveEndpoint(state);
|
|
81
|
+
const live = await fetchLiveModels(endpoint);
|
|
82
|
+
res.json({
|
|
83
|
+
endpoint,
|
|
84
|
+
policy,
|
|
85
|
+
live,
|
|
86
|
+
agents: policy.agents || {},
|
|
87
|
+
tiers: policy.tiers || {},
|
|
88
|
+
});
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
router.get('/model-router/models', wrap(async (_req, res) => {
|
|
92
|
+
const endpoint = resolveEndpoint(state);
|
|
93
|
+
const live = await fetchLiveModels(endpoint);
|
|
94
|
+
res.json(live);
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
router.get('/model-router/agents', wrap(async (_req, res) => {
|
|
98
|
+
const policy = await loadPolicy();
|
|
99
|
+
res.json({ agents: policy.agents || {}, tiers: policy.tiers || {} });
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
return router;
|
|
103
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for /api/model-router. Verifies:
|
|
3
|
+
* - the route resolves and returns the agent→model map
|
|
4
|
+
* - live model probe gracefully degrades when the endpoint is unreachable
|
|
5
|
+
* - the policy file is read from `.claude/model-router.json`
|
|
6
|
+
*
|
|
7
|
+
* Run with: bun test src/server/routes/model-router.test.mjs
|
|
8
|
+
* or: node --test src/server/routes/model-router.test.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import express from 'express';
|
|
13
|
+
import { createModelRouterRouter } from './model-router.mjs';
|
|
14
|
+
|
|
15
|
+
function buildApp() {
|
|
16
|
+
const app = express();
|
|
17
|
+
// Mount under /api to match the dashboard's mount prefix.
|
|
18
|
+
app.use('/api', createModelRouterRouter({ state: {} }));
|
|
19
|
+
return app;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function getJson(app, path) {
|
|
23
|
+
const server = app.listen(0);
|
|
24
|
+
try {
|
|
25
|
+
const port = server.address().port;
|
|
26
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`);
|
|
27
|
+
return { status: res.status, body: await res.json() };
|
|
28
|
+
} finally {
|
|
29
|
+
await new Promise((r) => server.close(r));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('GET /api/model-router returns agent→model map from .claude/model-router.json', async () => {
|
|
34
|
+
const app = buildApp();
|
|
35
|
+
const { status, body } = await getJson(app, '/api/model-router');
|
|
36
|
+
assert.equal(status, 200);
|
|
37
|
+
assert.ok(typeof body.endpoint === 'string' && body.endpoint.length > 0, 'endpoint present');
|
|
38
|
+
assert.ok(body.policy, 'policy present');
|
|
39
|
+
assert.ok(body.agents, 'agents map present');
|
|
40
|
+
// Forseti / Baldr / Vidarr must be GPT (premium tier per the routing rules).
|
|
41
|
+
assert.match(body.agents.forseti.model, /gpt-5\.6-sol/, 'forseti uses premium GPT');
|
|
42
|
+
assert.match(body.agents.baldr.model, /gpt-5\.6-sol/, 'baldr uses premium GPT');
|
|
43
|
+
assert.match(body.agents.vidarr.model, /gpt-5\.6-sol/, 'vidarr uses premium GPT');
|
|
44
|
+
// Tyr + Odin use the high-tier terra model.
|
|
45
|
+
assert.match(body.agents.tyr.model, /gpt-5\.6-terra/, 'tyr uses high-tier GPT');
|
|
46
|
+
assert.match(body.agents.odin.model, /gpt-5\.6-terra/, 'odin uses high-tier GPT');
|
|
47
|
+
// Everyday default agents use MiniMax-M3.
|
|
48
|
+
assert.equal(body.agents.frigg.model, 'bizar/MiniMax-M3', 'frigg uses default');
|
|
49
|
+
assert.equal(body.agents.hermod.model, 'bizar/MiniMax-M3', 'hermod uses default');
|
|
50
|
+
// Vor uses the budget tier.
|
|
51
|
+
assert.match(body.agents.vor.model, /mimo|deepseek/, 'vor uses budget tier');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('GET /api/model-router/agents returns just the agents+tiers map', async () => {
|
|
55
|
+
const app = buildApp();
|
|
56
|
+
const { status, body } = await getJson(app, '/api/model-router/agents');
|
|
57
|
+
assert.equal(status, 200);
|
|
58
|
+
assert.ok(body.agents.thor, 'thor present');
|
|
59
|
+
assert.ok(body.tiers.default, 'default tier present');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('GET /api/model-router/models probe handles unreachable endpoint gracefully', async () => {
|
|
63
|
+
// Override env to a guaranteed-unreachable port.
|
|
64
|
+
const prev = process.env.BIZAR_MODEL_ROUTER_URL;
|
|
65
|
+
process.env.BIZAR_MODEL_ROUTER_URL = 'http://127.0.0.1:1';
|
|
66
|
+
try {
|
|
67
|
+
const app = buildApp();
|
|
68
|
+
const { status, body } = await getJson(app, '/api/model-router/models');
|
|
69
|
+
assert.equal(status, 200);
|
|
70
|
+
assert.equal(body.reachable, false);
|
|
71
|
+
assert.ok(typeof body.error === 'string', 'error string present');
|
|
72
|
+
} finally {
|
|
73
|
+
if (prev === undefined) delete process.env.BIZAR_MODEL_ROUTER_URL;
|
|
74
|
+
else process.env.BIZAR_MODEL_ROUTER_URL = prev;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
@@ -436,32 +436,6 @@ export async function createServer({
|
|
|
436
436
|
broadcast: localBroadcast,
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
-
// v5.0.0 — Headroom startup hook. Runs after api.mjs is loaded so the
|
|
440
|
-
// headroom routes are registered. Errors are caught and logged — startup
|
|
441
|
-
// must not fail if Headroom has issues.
|
|
442
|
-
//
|
|
443
|
-
// v10-S9 — opt-out via BIZAR_HEADROOM_AUTOSTART=0. The hook runs
|
|
444
|
-
// `npm install` + a child process spawn which can take 2-5s on cold
|
|
445
|
-
// boot. Real users hit this on every dashboard launch; tests hit it
|
|
446
|
-
// too. Setting the env var skips the install + child-process spawn
|
|
447
|
-
// entirely. Default behaviour is unchanged (settings.json
|
|
448
|
-
// `headroom.enabled` still controls).
|
|
449
|
-
if (/^(0|false|no|off)$/i.test((process.env.BIZAR_HEADROOM_AUTOSTART || '').trim())) {
|
|
450
|
-
console.log('[bizar-dash] headroom startup skipped (BIZAR_HEADROOM_AUTOSTART=0)');
|
|
451
|
-
} else {
|
|
452
|
-
const { headroomStartupHook } = await import('./headroom.mjs');
|
|
453
|
-
try {
|
|
454
|
-
const settings = readSettings();
|
|
455
|
-
if (settings?.data?.headroom) {
|
|
456
|
-
headroomStartupHook(settings.data.headroom).catch((err) => {
|
|
457
|
-
console.warn('[bizar-dash] headroomStartupHook error:', err?.message || err);
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
} catch (err) {
|
|
461
|
-
console.warn('[bizar-dash] headroom startup hook skipped:', err?.message || err);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
439
|
// v5.5.2 — Auto-migrate legacy git.repoPath from the old vault location
|
|
466
440
|
// to the new default. Runs before the lightrag hook so git-dependent
|
|
467
441
|
// checks are already correct.
|
|
@@ -479,7 +453,7 @@ export async function createServer({
|
|
|
479
453
|
console.warn('[bizar-dash] git.repoPath migration check failed:', err?.message || err);
|
|
480
454
|
}
|
|
481
455
|
|
|
482
|
-
// v5.x — LightRAG startup hook (issue #6).
|
|
456
|
+
// v5.x — LightRAG startup hook (issue #6).
|
|
483
457
|
// reads config from .bizar/memory.json + env vars, then calls
|
|
484
458
|
// lightragStartupHook() which respects `lightrag.enabled` and the
|
|
485
459
|
// BIZAR_LIGHTRAG_AUTOSTART env override. All errors are caught — the
|
|
@@ -86,7 +86,7 @@ describe('B7 — check-deps.mjs which() uses path.join()', () => {
|
|
|
86
86
|
const { checkDeps } = await import('../../scripts/check-deps.mjs');
|
|
87
87
|
const result = await checkDeps({ strict: false });
|
|
88
88
|
const names = [...result.present, ...result.missing].map(d => d.name);
|
|
89
|
-
const required = ['node', 'bun', 'cline', 'tmux', 'git', 'python3', 'pip', 'jq', 'gh', '
|
|
89
|
+
const required = ['node', 'bun', 'cline', 'tmux', 'git', 'python3', 'pip', 'jq', 'gh', 'semble', 'skills'];
|
|
90
90
|
for (const dep of required) {
|
|
91
91
|
assert.ok(names.includes(dep), `expected ${dep} to be checked`);
|
|
92
92
|
}
|
|
@@ -138,7 +138,7 @@ describe('command module null-check produces user-visible error', () => {
|
|
|
138
138
|
// Count `if (!mod)` checks that follow `importCommand`
|
|
139
139
|
const nullCheckCount = (binSrc.match(/if \(!mod\)/g) || []).length;
|
|
140
140
|
// We have 9 case groups that call importCommand: install/update, service, dash,
|
|
141
|
-
// minimax,
|
|
141
|
+
// minimax, mod, artifact, memory, usage, and util (utility commands)
|
|
142
142
|
assert.ok(
|
|
143
143
|
nullCheckCount >= 9,
|
|
144
144
|
`Expected at least 9 null checks after importCommand, found ${nullCheckCount}`,
|
|
@@ -151,7 +151,6 @@ describe('collectDiagnostics', () => {
|
|
|
151
151
|
assert.ok(snap.disk && typeof snap.disk.exists === 'boolean');
|
|
152
152
|
assert.ok(snap.services);
|
|
153
153
|
assert.ok(snap.services.dashboard);
|
|
154
|
-
assert.ok(snap.services.headroom);
|
|
155
154
|
assert.ok(snap.services.lightrag);
|
|
156
155
|
assert.ok(snap.services.cline);
|
|
157
156
|
assert.ok(snap.counts);
|
package/cli/bin.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Commands:
|
|
14
14
|
* install, audit, init, export, artifact, update, test-gate, service, dash,
|
|
15
|
-
* memory,
|
|
15
|
+
* memory, minimax, usage, mod, doctor, repair, dev-link, dev-unlink,
|
|
16
16
|
* heads-up, bg, agent-browser, agent-browser-up, providers, deploy, plugin,
|
|
17
17
|
* marketplace, plan, digest, backup, restore, clip, ocr, voice, workspace, eval
|
|
18
18
|
*/
|
|
@@ -133,7 +133,6 @@ function showHelp() {
|
|
|
133
133
|
service Manage the background service daemon
|
|
134
134
|
dash <subcommand> Manage the dashboard (start/stop/status/cleanup/tui)
|
|
135
135
|
memory <subcommand> Manage project memory (Bizar Memory Service)
|
|
136
|
-
headroom <subcommand> Manage Headroom context compression
|
|
137
136
|
lightrag <subcommand> Manage the LightRAG knowledge-graph server (status/start/autostart)
|
|
138
137
|
minimax <subcommand> Manage MiniMax Token Plan integration
|
|
139
138
|
tailscale <subcommand> Manage Tailscale integration (auth, serve, status)
|
|
@@ -347,19 +346,6 @@ async function main() {
|
|
|
347
346
|
break;
|
|
348
347
|
}
|
|
349
348
|
|
|
350
|
-
case 'headroom': {
|
|
351
|
-
const mod = await importCommand('headroom');
|
|
352
|
-
if (!mod) {
|
|
353
|
-
console.error(chalk.red(` ✗ Could not load headroom command module`));
|
|
354
|
-
process.exit(EXIT_ERROR);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
dbg('loaded command module:', 'headroom');
|
|
358
|
-
await mod.run(cmd, cmdArgs, isHelpRequest);
|
|
359
|
-
dbg('command returned:', cmd);
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
349
|
case 'lightrag': {
|
|
364
350
|
// v5.x — LightRAG management CLI (issue #6).
|
|
365
351
|
// Subcommands: status, start, autostart.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the current working directory's project. Used by
|
|
9
9
|
* `install.sh` and by operators who want to manually
|
|
10
10
|
* start LightRAG without opening the dashboard.
|
|
11
|
-
* start Alias for `autostart
|
|
11
|
+
* start Alias for `autostart`.
|
|
12
12
|
* status Print whether the LightRAG server is running.
|
|
13
13
|
*
|
|
14
14
|
* The dashboard server already auto-invokes `lightragStartupHook()` on
|
package/cli/commands/util.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export function showDoctorHelp() {
|
|
|
120
120
|
• plugin path resolves
|
|
121
121
|
• @polderlabs/bizar-plugin is installed globally
|
|
122
122
|
• core agent files are installed (odin, quick, thor, tyr)
|
|
123
|
-
•
|
|
123
|
+
• semble / skills on PATH (lenient — at least one)
|
|
124
124
|
• dashboard reachable (skipped if no port file)
|
|
125
125
|
• provider.minimax block + MiniMax model flags are sane
|
|
126
126
|
|
package/cli/copy.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { join, dirname } from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { repoPath, clineConfigDir, clineAgentsDir,
|
|
6
|
+
import { repoPath, clineConfigDir, clineAgentsDir, detectSemble, detectUv, detectSkillsCli } from './utils.mjs';
|
|
7
7
|
|
|
8
8
|
async function fileExists(path) {
|
|
9
9
|
try {
|
|
@@ -341,52 +341,6 @@ export async function installPluginBizar(projectRoot) {
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
export async function installHeadroom() {
|
|
345
|
-
const { execSync } = await import('node:child_process');
|
|
346
|
-
|
|
347
|
-
const already = await detectRtk();
|
|
348
|
-
if (already) {
|
|
349
|
-
const spinner = ora({ text: 'Configuring Headroom for cline...', color: 'magenta' }).start();
|
|
350
|
-
try {
|
|
351
|
-
execSync('headroom wrap cline', { stdio: 'pipe' });
|
|
352
|
-
spinner.succeed(chalk.green('Headroom configured for cline'));
|
|
353
|
-
} catch {
|
|
354
|
-
spinner.warn(chalk.yellow('Could not auto-configure Headroom — run `headroom wrap cline` manually'));
|
|
355
|
-
}
|
|
356
|
-
return true;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const spinner = ora({ text: 'Installing Headroom (context compressor)...', color: 'magenta' }).start();
|
|
360
|
-
|
|
361
|
-
if (process.platform === 'win32') {
|
|
362
|
-
spinner.fail(chalk.red('Automatic Headroom install not supported on Windows. Install manually: pip install "headroom-ai[all]"'));
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
execSync(
|
|
368
|
-
'pip install --user "headroom-ai[all]"',
|
|
369
|
-
{ stdio: 'pipe', timeout: 60000 },
|
|
370
|
-
);
|
|
371
|
-
spinner.text = 'Configuring Headroom for cline...';
|
|
372
|
-
execSync('headroom wrap cline', { stdio: 'pipe' });
|
|
373
|
-
spinner.succeed(chalk.green('Headroom installed and configured for cline'));
|
|
374
|
-
return true;
|
|
375
|
-
} catch {
|
|
376
|
-
// Fall back to npm
|
|
377
|
-
try {
|
|
378
|
-
execSync('npm install -g headroom-ai', { stdio: 'pipe', timeout: 60000 });
|
|
379
|
-
spinner.text = 'Configuring Headroom for cline...';
|
|
380
|
-
execSync('headroom wrap cline', { stdio: 'pipe' });
|
|
381
|
-
spinner.succeed(chalk.green('Headroom installed (npm) and configured for cline'));
|
|
382
|
-
return true;
|
|
383
|
-
} catch {
|
|
384
|
-
spinner.fail(chalk.red('Headroom install failed. Install manually: pip install "headroom-ai[all]" or npm install -g headroom-ai'));
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
344
|
export async function installSemble() {
|
|
391
345
|
const { execSync } = await import('node:child_process');
|
|
392
346
|
|
package/cli/doctor.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* hooks-wired: settings.json has PreToolUse/PostToolUse/etc
|
|
16
16
|
* agent-files-installed: agent .md files deployed
|
|
17
17
|
* skill-files-installed: SKILL.md files deployed
|
|
18
|
-
* tools-on-path: at least one of
|
|
18
|
+
* tools-on-path: at least one of semble/skills/claude
|
|
19
19
|
* memory-vault: BIZAR_HOME exists (memory subdir lazy)
|
|
20
20
|
* 9router-reachable: provider gateway responds
|
|
21
21
|
*
|
|
@@ -109,13 +109,13 @@ async function checkSkillFilesInstalled() {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Lenient: passes if at least one of
|
|
112
|
+
* Lenient: passes if at least one of semble/skills/claude is on PATH.
|
|
113
113
|
* These are informational — none of them are strictly required for
|
|
114
114
|
* `bizar doctor` to do its job, and missing them shouldn't fail the
|
|
115
115
|
* overall health report.
|
|
116
116
|
*/
|
|
117
117
|
async function checkToolsAvailable() {
|
|
118
|
-
const tools = ['
|
|
118
|
+
const tools = ['semble', 'skills', 'claude'];
|
|
119
119
|
const found = tools.filter(which);
|
|
120
120
|
if (found.length === 0) {
|
|
121
121
|
throw new Error(`none of ${tools.join('/')} on PATH`);
|
package/cli/install.mjs
CHANGED
|
@@ -15,8 +15,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
15
15
|
|
|
16
16
|
import { showBanner, showPantheon, sectionHeading } from './banner.mjs';
|
|
17
17
|
import { promptComponents, promptInstallMode, promptAgents, promptSkillPacks, promptApiKeys, promptConfirmInstall, promptRestartCline } from './prompts.mjs';
|
|
18
|
-
import { detectClaude,
|
|
19
|
-
import { installAgents, installAgentsMd, installSkill, installClineJson, installBizarFolder, installPluginBizar,
|
|
18
|
+
import { detectClaude, detectSemble, detectSkillsCli, detectUv, buildSummary, claudeAgentsDir, claudeConfigDir, repoPath } from './utils.mjs';
|
|
19
|
+
import { installAgents, installAgentsMd, installSkill, installClineJson, installBizarFolder, installPluginBizar, installSemble, installSkillsCli, installCuratedSkills, installRules, installHooks, installCommands, installCommandsBizar, mergeToolsIntoUserConfig } from './copy.mjs';
|
|
20
20
|
|
|
21
21
|
const AGENT_FILES = [
|
|
22
22
|
'odin.md', 'vor.md', 'frigg.md', 'quick.md',
|
|
@@ -514,7 +514,7 @@ export async function runPostInstall() {
|
|
|
514
514
|
// is a thin Claude Code-native bootstrap that:
|
|
515
515
|
// - copies `config/settings.json` template on first install
|
|
516
516
|
// - installs agents into `~/.claude/agents/`
|
|
517
|
-
// - probes for
|
|
517
|
+
// - probes for semble / skills-cli
|
|
518
518
|
// - installs Headroom via pip or npm
|
|
519
519
|
const { mkdirSync, copyFileSync, existsSync } = await import('node:fs');
|
|
520
520
|
const { execSync } = await import('node:child_process');
|
|
@@ -546,40 +546,6 @@ export async function runPostInstall() {
|
|
|
546
546
|
}
|
|
547
547
|
console.log('BizarHarness: agents installed.');
|
|
548
548
|
|
|
549
|
-
// Install Headroom
|
|
550
|
-
const headroomPresent = await detectHeadroom();
|
|
551
|
-
if (!headroomPresent) {
|
|
552
|
-
console.log('BizarHarness: installing Headroom (context compressor)...');
|
|
553
|
-
try {
|
|
554
|
-
if (process.platform === 'win32') {
|
|
555
|
-
console.log('BizarHarness: Automatic Headroom install not supported on Windows. Install manually: pip install "headroom-ai[all]"');
|
|
556
|
-
} else {
|
|
557
|
-
execSync(
|
|
558
|
-
'pip install --user "headroom-ai[all]"',
|
|
559
|
-
{ stdio: 'pipe', timeout: 60000 },
|
|
560
|
-
);
|
|
561
|
-
execSync('headroom wrap claude', { stdio: 'pipe' });
|
|
562
|
-
console.log('BizarHarness: Headroom installed and configured.');
|
|
563
|
-
}
|
|
564
|
-
} catch {
|
|
565
|
-
// Fall back to npm
|
|
566
|
-
try {
|
|
567
|
-
execSync('npm install -g headroom-ai', { stdio: 'pipe', timeout: 60000 });
|
|
568
|
-
execSync('headroom wrap claude', { stdio: 'pipe' });
|
|
569
|
-
console.log('BizarHarness: Headroom installed (npm) and configured.');
|
|
570
|
-
} catch {
|
|
571
|
-
console.log('BizarHarness: Headroom install failed. Install manually: pip install "headroom-ai[all]" or npm install -g headroom-ai');
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
} else {
|
|
575
|
-
try {
|
|
576
|
-
execSync('headroom wrap claude', { stdio: 'pipe' });
|
|
577
|
-
console.log('BizarHarness: Headroom configured for claude.');
|
|
578
|
-
} catch {
|
|
579
|
-
console.log('BizarHarness: could not configure Headroom. Run `headroom wrap claude` manually.');
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
549
|
// Install Semble
|
|
584
550
|
const semblePresent = await detectSemble();
|
|
585
551
|
if (!semblePresent) {
|