@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.
Files changed (32) hide show
  1. package/bizar-dash/src/server/api.mjs +2 -6
  2. package/bizar-dash/src/server/diagnostics-store.mjs +5 -35
  3. package/bizar-dash/src/server/mod-security.mjs +1 -2
  4. package/bizar-dash/src/server/providers-store.mjs +1 -1
  5. package/bizar-dash/src/server/routes/_shared.mjs +0 -17
  6. package/bizar-dash/src/server/routes/model-router.mjs +103 -0
  7. package/bizar-dash/src/server/routes/model-router.test.mjs +76 -0
  8. package/bizar-dash/src/server/server.mjs +1 -27
  9. package/bizar-dash/tests/cli-bugfixes.test.mjs +1 -1
  10. package/bizar-dash/tests/cli-error-visibility.test.mjs +1 -1
  11. package/bizar-dash/tests/cli-refactor.test.mjs +0 -1
  12. package/bizar-dash/tests/diagnostics-store.test.mjs +0 -1
  13. package/cli/bin.mjs +1 -15
  14. package/cli/cli-commands-validation.test.mjs +1 -1
  15. package/cli/commands/lightrag.mjs +1 -1
  16. package/cli/commands/util.mjs +1 -1
  17. package/cli/copy.mjs +1 -47
  18. package/cli/doctor.mjs +3 -3
  19. package/cli/install.mjs +3 -37
  20. package/cli/service-controller.mjs +0 -192
  21. package/cli/service-env.mjs +2 -9
  22. package/cli/utils.mjs +0 -7
  23. package/package.json +1 -1
  24. package/packages/sdk/package.json +1 -1
  25. package/scripts/check-deps.mjs +0 -24
  26. package/bizar-dash/skills/headroom/SKILL.md +0 -94
  27. package/bizar-dash/src/server/headroom.mjs +0 -649
  28. package/bizar-dash/src/server/routes/headroom.mjs +0 -126
  29. package/bizar-dash/tests/headroom-install.test.mjs +0 -173
  30. package/bizar-dash/tests/headroom-settings.test.mjs +0 -126
  31. package/bizar-dash/tests/headroom-status.test.mjs +0 -117
  32. package/cli/commands/headroom.mjs +0 -204
@@ -1,649 +0,0 @@
1
- /**
2
- * src/server/headroom.mjs
3
- *
4
- * v1.0.0 — Headroom CLI integration for the Bizar dashboard.
5
- *
6
- * Headroom (headroom-ai) is a context compression layer that sits between
7
- * cline and LLM providers. It compresses tool outputs, logs, RAG chunks,
8
- * and conversation history by 60–95% before they reach the model.
9
- *
10
- * This module wraps the `headroom` CLI and exposes:
11
- * - getHeadroomStatus() — live status (installed, version, proxy, wrapped)
12
- * - getHeadroomStats() — compression statistics
13
- * - installHeadroom() — pip/npm install
14
- * - wrapCline() — run `headroom wrap cline`
15
- * - unwrapCline() — run `headroom unwrap cline`
16
- * - startProxy() — spawn `headroom proxy` as detached child
17
- * - stopProxy() — kill the proxy process
18
- * - getClineConfig() — read cline.json headroom status
19
- * - withHeadroomProxy(url) — prepend Headroom proxy URL if enabled
20
- * - headroomStartupHook() — auto-install/start/wrap on dashboard boot
21
- */
22
- import { spawn } from 'node:child_process';
23
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
24
- import { join, dirname } from 'node:path';
25
- import { homedir } from 'node:os';
26
- import { fileURLToPath } from 'node:url';
27
- import { readSettings } from './routes/_shared.mjs';
28
-
29
- // ── Constants ────────────────────────────────────────────────────────────────
30
-
31
- const HOME = homedir();
32
- const BIZAR_CACHE = join(HOME, '.cache', 'bizar');
33
- const HEADROOM_PORT_FILE = join(BIZAR_CACHE, 'headroom.port');
34
- const CLINE_JSON = join(HOME, '.config', 'cline', 'cline.json');
35
- const DEFAULT_HEADROOM_PORT = 8787;
36
- const DEFAULT_HEADROOM_HOST = '127.0.0.1';
37
-
38
- // ── Helpers ─────────────────────────────────────────────────────────────────
39
-
40
- /**
41
- * Run a command, capture stdout + stderr, resolve with exit code.
42
- *
43
- * @param {string} cmd
44
- * @param {string[]} args
45
- * @param {{ timeout?: number, cwd?: string, env?: Record<string, string> }} [opts]
46
- * @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
47
- */
48
- function runCmd(cmd, args, opts = {}) {
49
- return new Promise((resolve) => {
50
- const { timeout = 30000, cwd = HOME, env = process.env } = opts;
51
- // shell:false avoids DEP0190 deprecation warning and is safe because
52
- // args are already a proper array (not interpolated into a command string).
53
- const child = spawn(cmd, args, { cwd, env, shell: false });
54
- let stdout = '';
55
- let stderr = '';
56
- const timer = setTimeout(() => {
57
- child.kill('SIGTERM');
58
- }, timeout);
59
- child.stdout?.on('data', (d) => { stdout += d.toString(); });
60
- child.stderr?.on('data', (d) => { stderr += d.toString(); });
61
- child.on('close', (code) => {
62
- clearTimeout(timer);
63
- resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code ?? 0 });
64
- });
65
- child.on('error', (err) => {
66
- clearTimeout(timer);
67
- resolve({ stdout, stderr: err.message, exitCode: 1 });
68
- });
69
- });
70
- }
71
-
72
- /**
73
- * Safe read of Headroom port from the cached file.
74
- * @returns {number | null}
75
- */
76
- function readHeadroomPort() {
77
- try {
78
- if (!existsSync(HEADROOM_PORT_FILE)) return null;
79
- const port = parseInt(readFileSync(HEADROOM_PORT_FILE, 'utf8').trim(), 10);
80
- return Number.isFinite(port) && port > 0 ? port : null;
81
- } catch {
82
- return null;
83
- }
84
- }
85
-
86
- /**
87
- * Check if a TCP port is in use (quick probe).
88
- * @param {number} port
89
- * @param {string} [host]
90
- * @returns {Promise<boolean>}
91
- */
92
- async function isPortInUse(port, host = '127.0.0.1') {
93
- const { connect } = await import('node:net');
94
- return new Promise((resolve) => {
95
- const s = connect({ port, host, timeout: 1000 }, () => {
96
- s.destroy();
97
- resolve(true);
98
- });
99
- s.on('error', () => resolve(false));
100
- s.on('timeout', () => { s.destroy(); resolve(false); });
101
- });
102
- }
103
-
104
- /**
105
- * Read the proxy PID from the port file comment (if any), or try to find
106
- * it via /proc. Returns null if not determinable.
107
- * @param {number} port
108
- * @returns {Promise<number | null>}
109
- */
110
- async function findProxyPid(port) {
111
- // Try reading from the pid file (same dir as port file)
112
- const pidFile = join(BIZAR_CACHE, 'headroom.pid');
113
- try {
114
- if (existsSync(pidFile)) {
115
- const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
116
- if (Number.isFinite(pid) && pid > 0) return pid;
117
- }
118
- } catch { /* ignore */ }
119
-
120
- // Try to find process listening on the port via /proc/net/tcp (Linux only).
121
- // On macOS/Windows /proc does not exist — return null early to avoid noisy errors.
122
- if (process.platform !== 'linux') {
123
- return null;
124
- }
125
- try {
126
- const netTcp = readFileSync('/proc/net/tcp', 'utf8');
127
- const portHex = port.toString(16).toUpperCase().padStart(4, '0');
128
- const lines = netTcp.split('\n');
129
- for (const line of lines.slice(1)) {
130
- // Format: sl local_address rem_address st tx_queue rx_queue ... inode
131
- const parts = line.trim().split(/\s+/);
132
- if (parts.length < 10) continue;
133
- const local = parts[1];
134
- const localPortHex = local.split(':')[1];
135
- if (localPortHex?.toUpperCase() === portHex) {
136
- // Try to get PID from the socket inode
137
- const inode = parts[9];
138
- if (inode && inode !== '0') {
139
- // Scan /proc/*/fd/* for socket inodes
140
- const { readdirSync, readlinkSync } = await import('node:fs');
141
- const procDir = '/proc';
142
- const pids = readdirSync(procDir).filter(
143
- (n) => /^\d+$/.test(n) && !isNaN(parseInt(n, 10)),
144
- );
145
- for (const pid of pids) {
146
- try {
147
- const fdDir = join(procDir, pid, 'fd');
148
- const fds = readdirSync(fdDir);
149
- for (const fd of fds) {
150
- try {
151
- const link = readlinkSync(join(fdDir, fd));
152
- if (link.includes(`socket:[${inode}]`)) {
153
- return parseInt(pid, 10);
154
- }
155
- } catch { /* skip */ }
156
- }
157
- } catch { /* skip */ }
158
- }
159
- }
160
- }
161
- }
162
- } catch { /* ignore */ }
163
-
164
- return null;
165
- }
166
-
167
- // ── Status ──────────────────────────────────────────────────────────────────
168
-
169
- /**
170
- * Get the live Headroom status.
171
- *
172
- * @returns {Promise<{
173
- * installed: boolean,
174
- * version: string | null,
175
- * proxyRunning: boolean,
176
- * proxyPort: number | null,
177
- * proxyPid: number | null,
178
- * wrapped: boolean,
179
- * configPath: string | null,
180
- * healthy: 'ok' | 'warn' | 'fail',
181
- * messages: string[],
182
- * }>}
183
- */
184
- export async function getHeadroomStatus() {
185
- const messages = [];
186
- let healthy = 'ok';
187
-
188
- // 1. Check if headroom binary is on PATH
189
- const { stdout: versionOut, exitCode: versionCode } = await runCmd('headroom', ['--version']);
190
- const installed = versionCode === 0 && versionOut.length > 0;
191
- const version = installed ? versionOut.replace(/^headroom\s+/i, '').trim() : null;
192
-
193
- if (!installed) {
194
- messages.push('Headroom is not installed. Run `pip install "headroom-ai[all]"` or enable auto-install in Settings.');
195
- healthy = 'warn';
196
- }
197
-
198
- // 2. Check proxy port
199
- const proxyPort = readHeadroomPort();
200
- let proxyRunning = false;
201
- let proxyPid = null;
202
-
203
- if (proxyPort) {
204
- proxyRunning = await isPortInUse(proxyPort);
205
- if (proxyRunning) {
206
- proxyPid = await findProxyPid(proxyPort);
207
- messages.push(`Proxy running on ${proxyPort}${proxyPid ? ` (PID ${proxyPid})` : ''}.`);
208
- } else {
209
- messages.push(`Proxy port ${proxyPort} is not in use (stale port file).`);
210
- healthy = healthy === 'ok' ? 'warn' : healthy;
211
- }
212
- } else {
213
- messages.push('Proxy not started yet.');
214
- }
215
-
216
- // 3. Check if cline is wrapped (cline.json has headroom provider)
217
- const { configPath, hasHeadroomProvider } = await getClineConfig();
218
- const wrapped = hasHeadroomProvider;
219
-
220
- if (!wrapped && installed) {
221
- messages.push('cline is not wrapped with Headroom. Run `headroom wrap cline` or use Settings.');
222
- healthy = healthy === 'ok' ? 'warn' : healthy;
223
- } else if (wrapped) {
224
- messages.push('cline is wrapped with Headroom.');
225
- }
226
-
227
- if (healthy === 'ok') {
228
- messages.push('Headroom is healthy.');
229
- }
230
-
231
- return {
232
- installed,
233
- version,
234
- proxyRunning,
235
- proxyPort,
236
- proxyPid,
237
- wrapped,
238
- configPath,
239
- healthy,
240
- messages,
241
- };
242
- }
243
-
244
- // ── Stats ───────────────────────────────────────────────────────────────────
245
-
246
- /**
247
- * Get Headroom compression statistics.
248
- *
249
- * @param {{ hours?: number }} [opts]
250
- * @returns {Promise<{
251
- * tokensSaved: number,
252
- * compressionRatio: number,
253
- * cacheHits: number,
254
- * transforms: number,
255
- * raw: object,
256
- * } | { error: string }>}
257
- */
258
- export async function getHeadroomStats({ hours = 24 } = {}) {
259
- const { stdout, stderr, exitCode } = await runCmd(
260
- 'headroom',
261
- ['perf', '--hours', String(hours), '--format', 'json'],
262
- { timeout: 15000 },
263
- );
264
-
265
- if (exitCode !== 0) {
266
- return { error: stderr || `headroom perf failed with exit code ${exitCode}` };
267
- }
268
-
269
- try {
270
- const raw = JSON.parse(stdout);
271
- return {
272
- tokensSaved: raw.tokens_saved ?? raw.tokensSaved ?? 0,
273
- compressionRatio: raw.compression_ratio ?? raw.compressionRatio ?? 0,
274
- cacheHits: raw.cache_hits ?? raw.cacheHits ?? 0,
275
- transforms: raw.transforms ?? 0,
276
- raw,
277
- };
278
- } catch {
279
- return { error: `Failed to parse headroom perf JSON: ${stdout.slice(0, 200)}` };
280
- }
281
- }
282
-
283
- // ── Install ─────────────────────────────────────────────────────────────────
284
-
285
- /**
286
- * Install Headroom via pip (preferred) or npm fallback.
287
- *
288
- * @param {{ force?: boolean }} [opts]
289
- * @returns {Promise<{ ok: boolean, installed: boolean, version: string | null, method: string }>}
290
- */
291
- export async function installHeadroom({ force = false } = {}) {
292
- // Check if already installed
293
- if (!force) {
294
- const { exitCode } = await runCmd('headroom', ['--version'], { timeout: 5000 });
295
- if (exitCode === 0) {
296
- const { stdout } = await runCmd('headroom', ['--version'], { timeout: 5000 });
297
- return { ok: true, installed: true, version: stdout.trim() || null, method: 'already-installed' };
298
- }
299
- }
300
-
301
- // Try pip first
302
- try {
303
- const { stdout: pipOut, exitCode: pipCode } = await runCmd(
304
- 'pip',
305
- ['install', '--user', 'headroom-ai[all]'],
306
- { timeout: 120000 },
307
- );
308
- if (pipCode === 0) {
309
- const { stdout: verOut } = await runCmd('headroom', ['--version'], { timeout: 5000 });
310
- return {
311
- ok: true,
312
- installed: true,
313
- version: verOut.trim() || null,
314
- method: 'pip',
315
- };
316
- }
317
- } catch { /* fall through */ }
318
-
319
- // Fall back to npm
320
- try {
321
- const { stdout: npmOut, exitCode: npmCode } = await runCmd(
322
- 'npm',
323
- ['install', '-g', 'headroom-ai'],
324
- { timeout: 120000 },
325
- );
326
- if (npmCode === 0) {
327
- const { stdout: verOut } = await runCmd('headroom', ['--version'], { timeout: 5000 });
328
- return {
329
- ok: true,
330
- installed: true,
331
- version: verOut.trim() || null,
332
- method: 'npm',
333
- };
334
- }
335
- } catch { /* fall through */ }
336
-
337
- return { ok: false, installed: false, version: null, method: 'none' };
338
- }
339
-
340
- // ── Wrap / Unwrap ───────────────────────────────────────────────────────────
341
-
342
- /**
343
- * Run `headroom wrap cline --port <port>` and persist the port.
344
- *
345
- * @param {{ port?: number }} [opts]
346
- * @returns {Promise<{ ok: boolean, port: number, pid?: number, log: string }>}
347
- */
348
- export async function wrapCline({ port = DEFAULT_HEADROOM_PORT } = {}) {
349
- mkdirSync(BIZAR_CACHE, { recursive: true });
350
- writeFileSync(HEADROOM_PORT_FILE, String(port), 'utf8');
351
-
352
- const { stdout, stderr, exitCode } = await runCmd(
353
- 'headroom',
354
- ['wrap', 'cline', '--port', String(port)],
355
- { timeout: 30000 },
356
- );
357
-
358
- const ok = exitCode === 0;
359
- return {
360
- ok,
361
- port,
362
- log: ok ? stdout : `${stdout}\n${stderr}`.trim(),
363
- };
364
- }
365
-
366
- /**
367
- * Run `headroom unwrap cline`.
368
- *
369
- * @returns {Promise<{ ok: boolean, log: string }>}
370
- */
371
- export async function unwrapCline() {
372
- const { stdout, stderr, exitCode } = await runCmd(
373
- 'headroom',
374
- ['unwrap', 'cline'],
375
- { timeout: 30000 },
376
- );
377
-
378
- return { ok: exitCode === 0, log: `${stdout}\n${stderr}`.trim() };
379
- }
380
-
381
- // ── Proxy process management ────────────────────────────────────────────────
382
-
383
- /** @type {Map<number, { pid: number }>} */
384
- const _proxyProcesses = new Map();
385
-
386
- /**
387
- * Start the Headroom proxy as a detached child process.
388
- *
389
- * @param {{ port?: number, host?: string }} [opts]
390
- * @returns {Promise<{ ok: boolean, pid: number | null, port: number, logPath: string }>}
391
- */
392
- export async function startProxy({ port = DEFAULT_HEADROOM_PORT, host = DEFAULT_HEADROOM_HOST } = {}) {
393
- mkdirSync(BIZAR_CACHE, { recursive: true });
394
- writeFileSync(HEADROOM_PORT_FILE, String(port), 'utf8');
395
-
396
- const logPath = join(BIZAR_CACHE, `headroom-proxy-${port}.log`);
397
-
398
- return new Promise((resolve) => {
399
- const child = spawn(
400
- 'headroom',
401
- ['proxy', '--port', String(port), '--host', host],
402
- {
403
- detached: true,
404
- stdio: ['ignore', 'pipe', 'pipe'],
405
- env: process.env,
406
- cwd: HOME,
407
- },
408
- );
409
-
410
- let logWritten = false;
411
- function writeLog(text) {
412
- if (!logWritten) {
413
- try {
414
- writeFileSync(logPath, text, 'utf8');
415
- logWritten = true;
416
- } catch { /* ignore */ }
417
- }
418
- }
419
-
420
- child.stdout?.on('data', (d) => writeLog(d.toString()));
421
- child.stderr?.on('data', (d) => writeLog(d.toString()));
422
-
423
- child.on('error', (err) => {
424
- writeLog(`spawn error: ${err.message}`);
425
- resolve({ ok: false, pid: null, port, logPath });
426
- });
427
-
428
- // Give it a moment to fail
429
- setTimeout(() => {
430
- const pid = child.pid;
431
- if (pid) {
432
- _proxyProcesses.set(port, { pid });
433
- // Write PID file
434
- try {
435
- writeFileSync(join(BIZAR_CACHE, 'headroom.pid'), String(pid), 'utf8');
436
- } catch { /* ignore */ }
437
- resolve({ ok: true, pid, port, logPath });
438
- } else {
439
- resolve({ ok: false, pid: null, port, logPath });
440
- }
441
- }, 2000);
442
- });
443
- }
444
-
445
- /**
446
- * Stop the Headroom proxy process.
447
- *
448
- * @returns {Promise<{ ok: boolean, killed: number[] }>}
449
- */
450
- export async function stopProxy() {
451
- const port = readHeadroomPort() || DEFAULT_HEADROOM_PORT;
452
- const killed = [];
453
-
454
- // Try PID file first
455
- const pidFile = join(BIZAR_CACHE, 'headroom.pid');
456
- try {
457
- if (existsSync(pidFile)) {
458
- const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
459
- if (Number.isFinite(pid) && pid > 0) {
460
- try {
461
- process.kill(pid, 'SIGTERM');
462
- killed.push(pid);
463
- } catch { /* ignore */ }
464
- }
465
- }
466
- } catch { /* ignore */ }
467
-
468
- // Also kill by port lookup
469
- const pid = await findProxyPid(port);
470
- if (pid && !killed.includes(pid)) {
471
- try {
472
- process.kill(pid, 'SIGTERM');
473
- killed.push(pid);
474
- } catch { /* ignore */ }
475
- }
476
-
477
- // Try SIGKILL if SIGTERM didn't work
478
- for (const p of killed) {
479
- try {
480
- process.kill(p, 0); // check if still alive
481
- } catch {
482
- // process is gone
483
- }
484
- }
485
-
486
- // Clean up PID file
487
- try {
488
- if (existsSync(pidFile)) {
489
- const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
490
- try { process.kill(pid, 'SIGTERM'); } catch { /* ignore */ }
491
- try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ }
492
- }
493
- } catch { /* ignore */ }
494
-
495
- try { require('node:fs').unlinkSync(pidFile); } catch { /* ignore */ }
496
- _proxyProcesses.delete(port);
497
-
498
- return { ok: killed.length > 0, killed };
499
- }
500
-
501
- // ── Cline config ─────────────────────────────────────────────────────────
502
-
503
- /**
504
- * Read cline.json and check for Headroom provider entry.
505
- *
506
- * @returns {Promise<{ configPath: string, hasHeadroomProvider: boolean, baseURL: string | null }>}
507
- */
508
- export async function getClineConfig() {
509
- const configPath = CLINE_JSON;
510
- if (!existsSync(configPath)) {
511
- return { configPath, hasHeadroomProvider: false, baseURL: null };
512
- }
513
-
514
- try {
515
- const raw = JSON.parse(readFileSync(configPath, 'utf8'));
516
- const providers = raw?.provider || raw?.providers || {};
517
- const headroomProvider = providers?.headroom;
518
- const hasHeadroomProvider = Boolean(headroomProvider);
519
- const baseURL = headroomProvider?.baseURL || headroomProvider?.options?.baseURL || null;
520
-
521
- return { configPath, hasHeadroomProvider, baseURL };
522
- } catch {
523
- return { configPath, hasHeadroomProvider: false, baseURL: null };
524
- }
525
- }
526
-
527
- // ── Headroom proxy URL helper ───────────────────────────────────────────────
528
-
529
- /**
530
- * If Headroom is enabled and the proxy is running, return the proxy URL
531
- * prepended to the given original URL. Otherwise returns the URL unchanged.
532
- *
533
- * Usage:
534
- * const url = withHeadroomProxy('https://api.anthropic.com/v1/messages');
535
- * // if headroom proxy is on 8787 → 'http://127.0.0.1:8787/v1/https://api.anthropic.com/v1/messages'
536
- *
537
- * This is a URL transformation: headroom proxy expects the target URL as
538
- * the path. We just return the transformed URL string; callers apply it.
539
- *
540
- * @param {string} url
541
- * @param {{ port?: number }} [opts]
542
- * @returns {string}
543
- */
544
- export function withHeadroomProxy(url, { port = DEFAULT_HEADROOM_PORT } = {}) {
545
- // eslint-disable-next-line no-sync
546
- const settings = (() => {
547
- try {
548
- const s = readSettings();
549
- return s?.data?.headroom;
550
- } catch {
551
- return null;
552
- }
553
- })();
554
-
555
- if (!settings?.enabled) return url;
556
- if (!url) return url;
557
-
558
- // Only proxy OpenAI-compatible URLs (most providers use this)
559
- try {
560
- const parsed = new URL(url);
561
- // Don't re-proxy already-proxied URLs
562
- if (parsed.host === '127.0.0.1' || parsed.host === 'localhost') return url;
563
- const proxyBase = `http://${DEFAULT_HEADROOM_HOST}:${port}`;
564
- return `${proxyBase}/v1/${parsed.protocol}//${parsed.host}${parsed.pathname}${parsed.search}`;
565
- } catch {
566
- return url;
567
- }
568
- }
569
-
570
- // ── Startup hook ───────────────────────────────────────────────────────────
571
-
572
- /**
573
- * Headroom startup hook — called by server.mjs on dashboard boot.
574
- *
575
- * If `headroom.autoInstall` is true and Headroom is missing: install.
576
- * If `headroom.autoStart` is true and proxy isn't running: start proxy.
577
- * If `headroom.autoWrap` is true and cline isn't wrapped: wrap.
578
- * If `headroom.routeAllProviders` is true: configure providers to use proxy.
579
- *
580
- * All errors are caught and logged — startup must not fail if Headroom
581
- * has issues.
582
- *
583
- * @param {import('../web/lib/types').HeadroomSettings} headroomSettings
584
- * @returns {Promise<{ installed: boolean, wrapped: boolean, proxied: boolean }>}
585
- */
586
- export async function headroomStartupHook(headroomSettings) {
587
- const result = { installed: false, wrapped: false, proxied: false };
588
-
589
- try {
590
- const status = await getHeadroomStatus();
591
-
592
- // 1. Auto-install
593
- if (headroomSettings.autoInstall && !status.installed) {
594
- try {
595
- const inst = await installHeadroom();
596
- result.installed = inst.installed;
597
- if (inst.installed) {
598
- console.log('[headroom] Installed via', inst.method, inst.version || '');
599
- }
600
- } catch (err) {
601
- console.warn('[headroom] Auto-install failed:', err?.message || err);
602
- }
603
- } else {
604
- result.installed = status.installed;
605
- }
606
-
607
- // Re-check status after potential install
608
- const statusAfter = await getHeadroomStatus();
609
-
610
- // 2. Auto-start proxy
611
- if (headroomSettings.autoStart && !statusAfter.proxyRunning) {
612
- try {
613
- const port = headroomSettings.port || DEFAULT_HEADROOM_PORT;
614
- const host = headroomSettings.host || DEFAULT_HEADROOM_HOST;
615
- await startProxy({ port, host });
616
- console.log(`[headroom] Proxy started on ${host}:${port}`);
617
- } catch (err) {
618
- console.warn('[headroom] Auto-start proxy failed:', err?.message || err);
619
- }
620
- }
621
-
622
- // 3. Auto-wrap cline
623
- if (headroomSettings.autoWrap && !statusAfter.wrapped) {
624
- try {
625
- const port = headroomSettings.port || DEFAULT_HEADROOM_PORT;
626
- const wrapped = await wrapCline({ port });
627
- result.wrapped = wrapped.ok;
628
- if (wrapped.ok) {
629
- console.log('[headroom] cline wrapped on port', port);
630
- }
631
- } catch (err) {
632
- console.warn('[headroom] Auto-wrap failed:', err?.message || err);
633
- }
634
- } else {
635
- result.wrapped = statusAfter.wrapped;
636
- }
637
-
638
- // 4. Route all providers (handled at the provider level — mark flag)
639
- result.proxied = headroomSettings.routeAllProviders;
640
- if (headroomSettings.routeAllProviders) {
641
- console.log('[headroom] routeAllProviders enabled — configure provider baseURLs to point at the proxy');
642
- }
643
- } catch (err) {
644
- // Never let headroom failures crash the dashboard startup
645
- console.warn('[headroom] Startup hook error:', err?.message || err);
646
- }
647
-
648
- return result;
649
- }