@polderlabs/bizar 4.5.0 → 4.5.2

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 (77) hide show
  1. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  2. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-B4OfGAwz.js +361 -0
  4. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +1 -0
  5. package/bizar-dash/dist/assets/main-DAlLdW8I.css +1 -0
  6. package/bizar-dash/dist/assets/{mobile--0FBIKX3.js → mobile-BRhoDOUz.js} +1 -2
  7. package/bizar-dash/dist/assets/{mobile--0FBIKX3.js.map → mobile-BRhoDOUz.js.map} +1 -1
  8. package/bizar-dash/dist/assets/{mobile-OgRp8VIb.js → mobile-lbH6szyX.js} +2 -3
  9. package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +1 -0
  10. package/bizar-dash/dist/index.html +3 -3
  11. package/bizar-dash/dist/mobile.html +2 -2
  12. package/bizar-dash/skills/bizar/SKILL.md +20 -0
  13. package/bizar-dash/skills/headroom/SKILL.md +94 -0
  14. package/bizar-dash/skills/lightrag/SKILL.md +11 -0
  15. package/bizar-dash/skills/obsidian/SKILL.md +13 -0
  16. package/bizar-dash/src/server/api.mjs +6 -0
  17. package/bizar-dash/src/server/headroom.mjs +647 -0
  18. package/bizar-dash/src/server/memory-lightrag.mjs +281 -6
  19. package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
  20. package/bizar-dash/src/server/memory-store.mjs +151 -0
  21. package/bizar-dash/src/server/mods-loader.mjs +13 -5
  22. package/bizar-dash/src/server/providers-store.mjs +73 -1
  23. package/bizar-dash/src/server/routes/_shared.mjs +17 -0
  24. package/bizar-dash/src/server/routes/chat.mjs +38 -0
  25. package/bizar-dash/src/server/routes/headroom.mjs +126 -0
  26. package/bizar-dash/src/server/routes/lightrag.mjs +6 -2
  27. package/bizar-dash/src/server/routes/memory.mjs +439 -4
  28. package/bizar-dash/src/server/schedules-runner.mjs +4 -4
  29. package/bizar-dash/src/server/server.mjs +55 -37
  30. package/bizar-dash/src/server/watcher.mjs +2 -2
  31. package/bizar-dash/src/web/App.tsx +42 -6
  32. package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
  33. package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
  34. package/bizar-dash/src/web/components/Toast.tsx +1 -1
  35. package/bizar-dash/src/web/components/Topbar.tsx +3 -1
  36. package/bizar-dash/src/web/lib/api.ts +12 -11
  37. package/bizar-dash/src/web/lib/types.ts +16 -0
  38. package/bizar-dash/src/web/main.tsx +1 -0
  39. package/bizar-dash/src/web/styles/chat.css +2 -0
  40. package/bizar-dash/src/web/styles/main.css +52 -23
  41. package/bizar-dash/src/web/styles/memory.css +955 -0
  42. package/bizar-dash/src/web/styles/tasks.css +2 -0
  43. package/bizar-dash/src/web/views/Memory.tsx +141 -0
  44. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +3 -2
  45. package/bizar-dash/src/web/views/Overview.tsx +7 -2
  46. package/bizar-dash/src/web/views/Settings.tsx +32 -1
  47. package/bizar-dash/src/web/views/Skills.tsx +3 -2
  48. package/bizar-dash/src/web/views/Tasks.tsx +4 -3
  49. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
  50. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
  51. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
  52. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +356 -0
  53. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
  54. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
  55. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
  56. package/bizar-dash/tests/cli-bugfixes.test.mjs +151 -0
  57. package/bizar-dash/tests/frontend-bugfixes.test.mjs +151 -0
  58. package/bizar-dash/tests/headroom-install.test.mjs +173 -0
  59. package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
  60. package/bizar-dash/tests/headroom-status.test.mjs +117 -0
  61. package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
  62. package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
  63. package/bizar-dash/tests/memory-tab.test.mjs +322 -0
  64. package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
  65. package/bizar-dash/tests/server-bugfixes.test.mjs +318 -0
  66. package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
  67. package/cli/artifact.mjs +11 -4
  68. package/cli/bin.mjs +211 -12
  69. package/cli/doctor.mjs +5 -3
  70. package/cli/memory.mjs +65 -21
  71. package/cli/provision.mjs +8 -2
  72. package/install.sh +1 -2
  73. package/package.json +3 -3
  74. package/bizar-dash/dist/assets/main-CDFKHzBg.css +0 -1
  75. package/bizar-dash/dist/assets/main-NYFpS2wY.js +0 -312
  76. package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +0 -1
  77. package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +0 -1
@@ -46,7 +46,7 @@ function makeProjectRoot() {
46
46
  }
47
47
 
48
48
  function seedArtifact(projectRoot, slug, { comments = [], answers = [], status = 'draft' } = {}) {
49
- const dir = join(projectRoot, 'artifacts', slug);
49
+ const dir = join(projectRoot, '.bizar', 'artifacts', slug);
50
50
  mkdirSync(dir, { recursive: true });
51
51
  const meta = {
52
52
  slug,
@@ -85,7 +85,7 @@ test('submitFeedback creates feedback.md', () => {
85
85
  const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
86
86
  assert.equal(result.ok, true);
87
87
  assert.equal(result.slug, slug);
88
- const feedbackPath = join(root, 'artifacts', slug, 'feedback.md');
88
+ const feedbackPath = join(root, '.bizar', 'artifacts', slug, 'feedback.md');
89
89
  assert.ok(existsSync(feedbackPath), 'feedback.md should be created');
90
90
  const content = readFileSync(feedbackPath, 'utf8');
91
91
  assert.ok(content.includes('glyph: ' + slug), 'frontmatter glyph line');
@@ -107,7 +107,7 @@ test('submitFeedback updates meta.json status to "review"', () => {
107
107
  seedArtifact(root, slug, { status: 'draft' });
108
108
  const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
109
109
  assert.equal(result.ok, true);
110
- const meta = JSON.parse(readFileSync(join(root, 'artifacts', slug, 'meta.json'), 'utf8'));
110
+ const meta = JSON.parse(readFileSync(join(root, '.bizar', 'artifacts', slug, 'meta.json'), 'utf8'));
111
111
  assert.equal(meta.status, 'review', 'meta.status should be "review"');
112
112
  } finally {
113
113
  rmSync(root, { recursive: true, force: true });
@@ -123,7 +123,7 @@ test('submitFeedback updates meta.json lastEdited', () => {
123
123
  const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
124
124
  const after = Date.now();
125
125
  assert.equal(result.ok, true);
126
- const meta = JSON.parse(readFileSync(join(root, 'artifacts', slug, 'meta.json'), 'utf8'));
126
+ const meta = JSON.parse(readFileSync(join(root, '.bizar', 'artifacts', slug, 'meta.json'), 'utf8'));
127
127
  const ts = Date.parse(meta.lastEdited);
128
128
  assert.ok(Number.isFinite(ts), 'lastEdited should be a parseable timestamp');
129
129
  assert.ok(ts >= before && ts <= after, `lastEdited ${meta.lastEdited} should be in [${before}, ${after}]`);
@@ -208,7 +208,7 @@ test('submitFeedback falls back to comments.json when canvas has none', () => {
208
208
  const slug = 'glyph-comments-json-fallback';
209
209
  // Seed canvas with empty comments array, comments.json with one comment.
210
210
  seedArtifact(root, slug);
211
- const dir = join(root, 'artifacts', slug);
211
+ const dir = join(root, '.bizar', 'artifacts', slug);
212
212
  // Replace plan.json with empty comments, populate comments.json.
213
213
  const canvas = JSON.parse(readFileSync(join(dir, 'plan.json'), 'utf8'));
214
214
  canvas.comments = [];
@@ -339,7 +339,7 @@ test('POST /api/artifacts/:slug/submit persists feedback.md on disk', async () =
339
339
  body: JSON.stringify({ answers: [] }),
340
340
  });
341
341
  assert.equal(res.status, 200);
342
- const feedbackPath = join(root, 'artifacts', slug, 'feedback.md');
342
+ const feedbackPath = join(root, '.bizar', 'artifacts', slug, 'feedback.md');
343
343
  assert.ok(existsSync(feedbackPath), 'feedback.md should exist on disk');
344
344
  const content = readFileSync(feedbackPath, 'utf8');
345
345
  assert.ok(content.startsWith('---'), 'starts with YAML frontmatter');
package/cli/artifact.mjs CHANGED
@@ -1885,13 +1885,22 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
1885
1885
 
1886
1886
  // ─── Browser opening ─────────────────────────────────────────────────────────
1887
1887
 
1888
+ function isWSL() {
1889
+ try {
1890
+ const version = readFileSync('/proc/version', 'utf8');
1891
+ if (version.toLowerCase().includes('microsoft')) return true;
1892
+ } catch { /* not WSL */ }
1893
+ return !!process.env.WSL_INTEROP;
1894
+ }
1895
+
1888
1896
  function openBrowser(url) {
1889
1897
  const platform = process.platform;
1898
+ const wsl = isWSL();
1890
1899
  let cmd, args;
1891
1900
  if (platform === 'darwin') {
1892
1901
  cmd = 'open';
1893
1902
  args = [url];
1894
- } else if (platform === 'win32') {
1903
+ } else if (platform === 'win32' || wsl) {
1895
1904
  cmd = 'cmd';
1896
1905
  args = ['/c', 'start', '""', url];
1897
1906
  } else {
@@ -1900,9 +1909,7 @@ function openBrowser(url) {
1900
1909
  }
1901
1910
 
1902
1911
  // Check if the command exists
1903
- const probe = platform === 'win32'
1904
- ? spawnSync('where', [cmd], { stdio: 'ignore' })
1905
- : spawnSync('which', [cmd], { stdio: 'ignore' });
1912
+ const probe = spawnSync('which', [cmd], { stdio: 'ignore' });
1906
1913
  if (probe.status !== 0) {
1907
1914
  console.log(` ℹ Open ${url} in your browser (no ${cmd} available)`);
1908
1915
  return false;
package/cli/bin.mjs CHANGED
@@ -22,6 +22,13 @@ import { fileURLToPath } from 'node:url';
22
22
 
23
23
  const HOME = homedir();
24
24
  const BIZAR_HOME = join(HOME, '.config', 'bizar');
25
+
26
+ // Exit codes
27
+ const EXIT_OK = 0;
28
+ const EXIT_ERROR = 1;
29
+ const EXIT_USAGE = 2;
30
+ const EXIT_MISSING_DEP = 3;
31
+ const EXIT_TIMEOUT = 4;
25
32
  import chalk from 'chalk';
26
33
  import { runInstaller } from './install.mjs';
27
34
  import { runAudit } from './audit.mjs';
@@ -35,6 +42,17 @@ import { ensureSetup, checkSetupStatus } from './bootstrap.mjs';
35
42
  const args = process.argv.slice(2);
36
43
  const isHelpRequest = args.includes('--help') || args.includes('-h');
37
44
  const isVersionRequest = args.includes('--version') || args.includes('-v');
45
+ const wantJson = args.includes('--json');
46
+ const wantDebug = args.includes('--debug');
47
+
48
+ if (wantDebug) {
49
+ process.env.DEBUG = 'bizar:*';
50
+ process.env.BIZAR_DEBUG = '1';
51
+ }
52
+
53
+ function dbg(...msg) {
54
+ if (wantDebug) console.error('[DEBUG]', ...msg);
55
+ }
38
56
 
39
57
  // ── Bootstrap ─────────────────────────────────────────────────────────────────
40
58
  // Every bin command checks setup status on first invocation.
@@ -91,6 +109,7 @@ function showHelp() {
91
109
  service Manage the background service daemon
92
110
  bg <subcommand> Manage background agents (list/view/kill/logs)
93
111
  memory <subcommand> Manage project memory (Bizar Memory Service)
112
+ headroom <subcommand> Manage Headroom context compression
94
113
  dash <subcommand> Manage the dashboard (start/stop/status/cleanup/tui)
95
114
  browser-harness-up Start Chromium for browser-harness (start/stop/status/restart)
96
115
  dev-link [src] Symlink the local plugin source into opencode's plugin dir
@@ -758,7 +777,7 @@ function showUsageHelp() {
758
777
  `);
759
778
  }
760
779
 
761
- async function runUsageCommand(args) {
780
+ async function runUsageCommand(args, wantJson = false) {
762
781
  const range = (args[0] && ['24h', '7d', '30d'].includes(args[0])) ? args[0] : '24h';
763
782
  const { port, secret } = readDashboardConn();
764
783
  const url = `http://127.0.0.1:${port}/api/usage?range=${range}`;
@@ -773,6 +792,10 @@ async function runUsageCommand(args) {
773
792
  console.error(chalk.red(` ✗ Failed to load usage data: ${data?.message ?? resp.statusText}`));
774
793
  process.exit(1);
775
794
  }
795
+ if (wantJson) {
796
+ process.stdout.write(JSON.stringify(data) + '\n');
797
+ return;
798
+ }
776
799
  const t = data.totals;
777
800
  console.log('');
778
801
  console.log(chalk.bold(` Usage summary — ${range} (from JSONL store)`));
@@ -809,6 +832,168 @@ async function runUsageCommand(args) {
809
832
  }
810
833
  }
811
834
 
835
+ function showHeadroomHelp() {
836
+ console.log(`
837
+ bizar headroom — Manage Headroom context compression
838
+
839
+ Usage:
840
+ bizar headroom status Show live status (installed, proxy, wrapped)
841
+ bizar headroom stats Show compression stats for the last 24h
842
+ bizar headroom install Install headroom via pip or npm
843
+ bizar headroom wrap Wrap opencode to route through the proxy
844
+ bizar headroom unwrap Unwrap opencode
845
+ bizar headroom start Start the proxy server
846
+ bizar headroom stop Stop the proxy server
847
+ bizar headroom doctor Run headroom doctor health check
848
+
849
+ Examples:
850
+ bizar headroom status
851
+ bizar headroom stats
852
+ bizar headroom install
853
+ bizar headroom wrap
854
+ `);
855
+ }
856
+
857
+ async function runHeadroomCommand(headroomArgs) {
858
+ const sub = headroomArgs[0];
859
+ const flags = headroomArgs.slice(1).filter((a) => a.startsWith('-'));
860
+ const positional = headroomArgs.slice(1).filter((a) => !a.startsWith('-'));
861
+
862
+ if (!sub || sub === '--help' || sub === '-h' || flags.includes('--help') || flags.includes('-h')) {
863
+ showHeadroomHelp();
864
+ return;
865
+ }
866
+
867
+ // Delegate to dashboard API
868
+ const { port, secret } = readDashboardConn();
869
+ const baseUrl = `http://127.0.0.1:${port}`;
870
+
871
+ async function apiGet(path) {
872
+ const url = `${baseUrl}${path}`;
873
+ const headers = { accept: 'application/json' };
874
+ if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
875
+ const res = await fetch(url, { method: 'GET', headers });
876
+ const text = await res.text();
877
+ let data = null;
878
+ try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
879
+ if (!res.ok) throw new Error(`${path}: ${data?.message || data?.error || res.statusText}`);
880
+ return data;
881
+ }
882
+
883
+ async function apiPost(path, body = {}) {
884
+ const url = `${baseUrl}${path}`;
885
+ const headers = { 'content-type': 'application/json', accept: 'application/json' };
886
+ if (secret) headers.authorization = `Basic ${Buffer.from(`opencode:${secret}`).toString('base64')}`;
887
+ const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
888
+ const text = await res.text();
889
+ let data = null;
890
+ try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
891
+ if (!res.ok) throw new Error(`${path}: ${data?.message || data?.error || res.statusText}`);
892
+ return data;
893
+ }
894
+
895
+ if (sub === 'status') {
896
+ const s = await apiGet('/api/headroom/status');
897
+ console.log('');
898
+ console.log(chalk.bold(' Headroom status'));
899
+ console.log('');
900
+ console.log(` ${chalk.dim('installed:')} ${s.installed ? chalk.green('yes') : chalk.red('no')} ${s.version || ''}`);
901
+ console.log(` ${chalk.dim('proxy:')} ${s.proxyRunning ? chalk.green('running') : chalk.yellow('stopped')} ${s.proxyPort ? `@ ${s.proxyPort}` : ''} ${s.proxyPid ? `(PID ${s.proxyPid})` : ''}`);
902
+ console.log(` ${chalk.dim('wrapped:')} ${s.wrapped ? chalk.green('yes') : chalk.yellow('no')}`);
903
+ console.log(` ${chalk.dim('healthy:')} ${s.healthy === 'ok' ? chalk.green('ok') : s.healthy === 'warn' ? chalk.yellow('warn') : chalk.red('fail')}`);
904
+ if (s.messages && s.messages.length > 0) {
905
+ console.log('');
906
+ for (const m of s.messages) {
907
+ console.log(` ${m}`);
908
+ }
909
+ }
910
+ console.log('');
911
+ return;
912
+ }
913
+
914
+ if (sub === 'stats') {
915
+ const hours = parseInt(positional[0], 10) || 24;
916
+ const st = await apiGet(`/api/headroom/stats?hours=${hours}`);
917
+ console.log('');
918
+ console.log(chalk.bold(` Headroom stats (${hours}h)`));
919
+ console.log('');
920
+ if (st.error) {
921
+ console.log(chalk.yellow(` ${st.error}`));
922
+ } else {
923
+ console.log(` ${chalk.dim('tokens saved:')} ${(st.tokensSaved || 0).toLocaleString()}`);
924
+ console.log(` ${chalk.dim('compression:')} ${st.compressionRatio ? `${Math.round(st.compressionRatio * 100)}%` : '—'}`);
925
+ console.log(` ${chalk.dim('cache hits:')} ${st.cacheHits ?? '—'}`);
926
+ console.log(` ${chalk.dim('transforms:')} ${st.transforms ?? '—'}`);
927
+ }
928
+ console.log('');
929
+ return;
930
+ }
931
+
932
+ if (sub === 'install') {
933
+ const r = await apiPost('/api/headroom/install', { force: true });
934
+ if (r.installed) {
935
+ console.log(chalk.green(` ✓ Headroom installed via ${r.method}${r.version ? ` (${r.version})` : ''}`));
936
+ } else {
937
+ console.log(chalk.red(' ✗ Install failed. Try: pip install "headroom-ai[all]"'));
938
+ }
939
+ return;
940
+ }
941
+
942
+ if (sub === 'wrap') {
943
+ const port = parseInt(positional[0], 10) || 8787;
944
+ const r = await apiPost('/api/headroom/wrap', { port });
945
+ if (r.ok) {
946
+ console.log(chalk.green(` ✓ opencode wrapped with Headroom on port ${port}`));
947
+ } else {
948
+ console.log(chalk.red(` ✗ Wrap failed: ${r.log || 'unknown error'}`));
949
+ }
950
+ return;
951
+ }
952
+
953
+ if (sub === 'unwrap') {
954
+ const r = await apiPost('/api/headroom/unwrap');
955
+ if (r.ok) {
956
+ console.log(chalk.green(' ✓ opencode unwrapped from Headroom'));
957
+ } else {
958
+ console.log(chalk.red(' ✗ Unwrap failed'));
959
+ }
960
+ return;
961
+ }
962
+
963
+ if (sub === 'start') {
964
+ const port = parseInt(positional[0], 10) || 8787;
965
+ const r = await apiPost('/api/headroom/proxy/start', { port, host: '127.0.0.1' });
966
+ if (r.ok) {
967
+ console.log(chalk.green(` ✓ Proxy started on 127.0.0.1:${port} (PID ${r.pid})`));
968
+ } else {
969
+ console.log(chalk.red(' ✗ Proxy start failed'));
970
+ }
971
+ return;
972
+ }
973
+
974
+ if (sub === 'stop') {
975
+ const r = await apiPost('/api/headroom/proxy/stop');
976
+ if (r.ok) {
977
+ console.log(chalk.green(' ✓ Proxy stopped'));
978
+ } else {
979
+ console.log(chalk.red(' ✗ Proxy stop failed'));
980
+ }
981
+ return;
982
+ }
983
+
984
+ if (sub === 'doctor') {
985
+ // Run `headroom doctor` locally
986
+ const { spawn } = await import('node:child_process');
987
+ const child = spawn('headroom', ['doctor'], { stdio: 'inherit' });
988
+ await new Promise((resolve) => child.on('close', resolve));
989
+ return;
990
+ }
991
+
992
+ console.error(chalk.red(` ✗ Unknown headroom subcommand: ${sub}`));
993
+ showHeadroomHelp();
994
+ process.exit(1);
995
+ }
996
+
812
997
  function showDashHelp() {
813
998
  console.log(`
814
999
  bizar dash — Manage the Bizar dashboard
@@ -920,11 +1105,15 @@ function parseFlag(name) {
920
1105
  * Returns `null` if the flag isn't present (the provisioner's
921
1106
  * "don't touch mods" default), or a string[] of mod ids if it is.
922
1107
  */
923
- function parseWithModsFlag(subargs) {
1108
+ export function parseWithModsFlag(subargs) {
924
1109
  const idx = subargs.indexOf('--with-mods');
925
1110
  if (idx === -1) return null;
926
1111
  const raw = subargs[idx + 1];
927
- if (!raw || raw.startsWith('--')) return [];
1112
+ if (!raw || raw.startsWith('--')) {
1113
+ console.error('bizar: --with-mods requires a value (e.g. --with-mods a,b,c)');
1114
+ console.error('Usage: bizar install --with-mods <mod-id,mod-id>');
1115
+ process.exit(2);
1116
+ }
928
1117
  return raw
929
1118
  .split(',')
930
1119
  .map((s) => s.trim())
@@ -1018,8 +1207,10 @@ async function main() {
1018
1207
  if (isHelpRequest && !args[1]) showMemoryHelp();
1019
1208
  else {
1020
1209
  const { runMemory } = await import('./memory.mjs');
1021
- await runMemory(args[1], args.slice(2));
1210
+ await runMemory(args[1], args.slice(2), { wantJson });
1022
1211
  }
1212
+ } else if (args[0] === 'headroom') {
1213
+ await runHeadroomCommand(args.slice(1));
1023
1214
  } else if (args[0] === 'export') {
1024
1215
  if (isHelpRequest) showExportHelp();
1025
1216
  else await runExport(parseFlag('--target'));
@@ -1064,8 +1255,11 @@ async function main() {
1064
1255
  if (isHelpRequest) showDoctorHelp();
1065
1256
  else {
1066
1257
  const { runDoctor } = await import('./doctor.mjs');
1067
- const result = await runDoctor();
1068
- if (result.failed > 0) process.exit(1);
1258
+ const result = await runDoctor({ silent: wantJson, json: wantJson });
1259
+ if (wantJson) {
1260
+ process.stdout.write(JSON.stringify(result) + '\n');
1261
+ }
1262
+ if (result.failed > 0) process.exit(EXIT_ERROR);
1069
1263
  }
1070
1264
  } else if (args[0] === 'repair') {
1071
1265
  // v4.4.3 — One-shot repair for stale `bizar` bin symlinks.
@@ -1163,11 +1357,11 @@ async function main() {
1163
1357
  await runMinimaxCommand(args.slice(1));
1164
1358
  } else if (args[0] === 'usage') {
1165
1359
  // v4.6.0 — Compact usage analytics summary from the JSONL store.
1166
- await runUsageCommand(args.slice(1));
1360
+ await runUsageCommand(args.slice(1), wantJson);
1167
1361
  } else if (args[0] === 'dash' || args[0] === 'dashboard') {
1168
1362
  // `bizar dashboard` is a deprecated alias for `bizar dash`
1169
1363
  if (args[0] === 'dashboard') {
1170
- console.warn(chalk.yellow(' ⚠ `bizar dashboard` is deprecated, use `bizar dash` instead.'));
1364
+ process.stdout.write(chalk.yellow(' ⚠ `bizar dashboard` is deprecated, use `bizar dash` instead.\n'));
1171
1365
  }
1172
1366
  const dashArgs = args.slice(1); // everything after 'dash' or 'dashboard'
1173
1367
  if (dashArgs.length === 0 || isHelpRequest) {
@@ -1293,7 +1487,12 @@ async function runDash(dashArgs) {
1293
1487
 
1294
1488
  // ── Main ──────────────────────────────────────────────────────────────────────
1295
1489
 
1296
- await main().catch((err) => {
1297
- console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
1298
- process.exit(1);
1299
- });
1490
+ // Only run main() when bin.mjs is executed directly (not imported as a module in tests)
1491
+ const thisFile = fileURLToPath(import.meta.url);
1492
+ const isMainModule = process.argv[1] === thisFile;
1493
+ if (isMainModule) {
1494
+ await main().catch((err) => {
1495
+ console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
1496
+ process.exit(1);
1497
+ });
1498
+ }
package/cli/doctor.mjs CHANGED
@@ -269,10 +269,12 @@ const CHECKS = [
269
269
 
270
270
  /**
271
271
  * Run all health checks. Prints per-check output unless `opts.silent`.
272
- * Always prints a final summary line. Returns `{ passed, failed, results }`.
272
+ * Prints a final summary line unless `opts.json` is true.
273
+ * Returns `{ passed, failed, results }`.
273
274
  */
274
275
  export async function runDoctor(opts = {}) {
275
276
  const silent = !!opts.silent;
277
+ const jsonMode = !!opts.json;
276
278
  const results = [];
277
279
  for (const [name, fn] of CHECKS) {
278
280
  const r = await runCheck(name, fn);
@@ -288,8 +290,8 @@ export async function runDoctor(opts = {}) {
288
290
  const passed = results.filter((r) => r.ok).length;
289
291
  const failed = results.length - passed;
290
292
 
291
- // Always print a summary line if there were failures, or if not silent.
292
- if (failed > 0 || !silent) {
293
+ // Print summary line only when not in JSON mode
294
+ if (!jsonMode && (failed > 0 || !silent)) {
293
295
  console.log('');
294
296
  if (failed === 0) {
295
297
  console.log(
package/cli/memory.mjs CHANGED
@@ -755,7 +755,8 @@ function printSetupResult({ mode, remote, vaultPath }) {
755
755
  /**
756
756
  * `bizar memory status`
757
757
  */
758
- async function cmdStatus(_args) {
758
+ async function cmdStatus(_args, opts = {}) {
759
+ const { wantJson = false } = opts;
759
760
  const projectRoot = getProjectRoot();
760
761
  const { loadConfig, resolveVault, initVault, listNotes } = memoryStore;
761
762
  const { isGitInstalled, status: gitStatus } = memoryGit;
@@ -768,19 +769,15 @@ async function cmdStatus(_args) {
768
769
 
769
770
  const { vaultRoot, mode, projectId, branch } = resolveVault(projectRoot);
770
771
 
771
- console.log(chalk.bold('\n Bizar Memory Status'));
772
- console.log(' ─────────────────────────────');
773
- kv('mode', mode);
774
- kv('projectId', projectId);
775
- kv('vault', vaultRoot);
776
- kv('branch', branch);
772
+ const out = { mode, projectId, vault: vaultRoot, branch };
777
773
 
778
774
  // Ensure vault exists
779
775
  if (!existsSync(vaultRoot)) {
780
- warn(`vault directory does not exist — run \`bizar memory init\` or \`bizar memory sync\``);
776
+ out.vaultExists = false;
781
777
  } else {
778
+ out.vaultExists = true;
782
779
  const notes = listNotes(projectRoot);
783
- kv('noteCount', String(notes.length));
780
+ out.noteCount = notes.length;
784
781
  }
785
782
 
786
783
  // Git status if managed
@@ -788,14 +785,14 @@ async function cmdStatus(_args) {
788
785
  if (isGitInstalled()) {
789
786
  const gs = gitStatus(vaultRoot);
790
787
  if (gs.ok) {
791
- kv('git', `${gs.branch} (${gs.clean ? chalk.green('clean') : chalk.red('dirty')})`);
792
- if (gs.ahead > 0) kv('ahead', String(gs.ahead));
793
- if (gs.behind > 0) kv('behind', String(gs.behind));
788
+ out.git = { branch: gs.branch, clean: gs.clean };
789
+ if (gs.ahead > 0) out.git.ahead = gs.ahead;
790
+ if (gs.behind > 0) out.git.behind = gs.behind;
794
791
  } else {
795
- kv('git', chalk.yellow(`not a git repo`));
792
+ out.git = { error: 'not a git repo' };
796
793
  }
797
794
  } else {
798
- kv('git', chalk.yellow(`git not installed`));
795
+ out.git = { error: 'git not installed' };
799
796
  }
800
797
  }
801
798
 
@@ -804,15 +801,15 @@ async function cmdStatus(_args) {
804
801
  const { isLightRAGRunning, isLightRAGInstalled, resolveLightRAGConfig } = memoryStore;
805
802
  const cfg = resolveLightRAGConfig(projectRoot);
806
803
  if (!cfg.enabled) {
807
- kv('lightrag', chalk.gray('disabled'));
804
+ out.lightrag = { enabled: false };
808
805
  } else if (!(await isLightRAGInstalled())) {
809
- kv('lightrag', chalk.yellow('not installed `uv tool install "lightrag-hku[api]"`'));
806
+ out.lightrag = { enabled: true, installed: false };
810
807
  } else {
811
808
  const running = await isLightRAGRunning(cfg);
812
- kv('lightrag', running ? chalk.green(`${cfg.host}:${cfg.port} (running)`) : chalk.yellow(`${cfg.host}:${cfg.port} (not running — run \`bizar memory reindex\`)`));
809
+ out.lightrag = { enabled: true, installed: true, running, host: cfg.host, port: cfg.port };
813
810
  }
814
811
  } catch (err) {
815
- kv('lightrag', chalk.gray(`status unavailable (${err.message})`));
812
+ out.lightrag = { error: err.message };
816
813
  }
817
814
 
818
815
  // Last reindex attempt
@@ -820,10 +817,55 @@ async function cmdStatus(_args) {
820
817
  if (existsSync(reindexMarker)) {
821
818
  try {
822
819
  const { finishedAt, ok, inserted, failed, noteCount } = JSON.parse(readFileSync(reindexMarker, 'utf8'));
823
- kv('lastReindex', `${new Date(finishedAt).toLocaleString()} — ${ok ? chalk.green(`${inserted}/${noteCount} ok`) : chalk.red(`${failed} failed`)}`);
820
+ out.lastReindex = { finishedAt, ok, inserted, failed, noteCount };
824
821
  } catch { /* ignore */ }
825
822
  }
826
823
 
824
+ if (wantJson) {
825
+ process.stdout.write(JSON.stringify(out) + '\n');
826
+ return;
827
+ }
828
+
829
+ console.log(chalk.bold('\n Bizar Memory Status'));
830
+ console.log(' ─────────────────────────────');
831
+ kv('mode', mode);
832
+ kv('projectId', projectId);
833
+ kv('vault', vaultRoot);
834
+ kv('branch', branch);
835
+ if (!out.vaultExists) {
836
+ warn(`vault directory does not exist — run \`bizar memory init\` or \`bizar memory sync\``);
837
+ } else {
838
+ kv('noteCount', String(out.noteCount));
839
+ }
840
+ if (out.git) {
841
+ if (out.git.error) {
842
+ kv('git', chalk.yellow(out.git.error));
843
+ } else {
844
+ const cleanStr = out.git.clean ? chalk.green('clean') : chalk.red('dirty');
845
+ kv('git', `${out.git.branch} (${cleanStr})`);
846
+ if (out.git.ahead) kv('ahead', String(out.git.ahead));
847
+ if (out.git.behind) kv('behind', String(out.git.behind));
848
+ }
849
+ }
850
+ if (out.lightrag) {
851
+ if (out.lightrag.error) {
852
+ kv('lightrag', chalk.gray(`status unavailable (${out.lightrag.error})`));
853
+ } else if (!out.lightrag.enabled) {
854
+ kv('lightrag', chalk.gray('disabled'));
855
+ } else if (!out.lightrag.installed) {
856
+ kv('lightrag', chalk.yellow('not installed — `uv tool install "lightrag-hku[api]"`'));
857
+ } else {
858
+ const runningStr = out.lightrag.running
859
+ ? chalk.green(`${out.lightrag.host}:${out.lightrag.port} (running)`)
860
+ : chalk.yellow(`${out.lightrag.host}:${out.lightrag.port} (not running)`);
861
+ kv('lightrag', runningStr);
862
+ }
863
+ }
864
+ if (out.lastReindex) {
865
+ const lr = out.lastReindex;
866
+ const lrStr = `${new Date(lr.finishedAt).toLocaleString()} — ${lr.ok ? chalk.green(`${lr.inserted}/${lr.noteCount} ok`) : chalk.red(`${lr.failed} failed`)}`;
867
+ kv('lastReindex', lrStr);
868
+ }
827
869
  console.log();
828
870
  }
829
871
 
@@ -1606,8 +1648,10 @@ async function cmdDelete(args) {
1606
1648
  /**
1607
1649
  * @param {string} subcommand
1608
1650
  * @param {string[]} args
1651
+ * @param {{ wantJson?: boolean }} opts
1609
1652
  */
1610
- export async function runMemory(subcommand, args) {
1653
+ export async function runMemory(subcommand, args, opts = {}) {
1654
+ const { wantJson = false } = opts;
1611
1655
  switch (subcommand) {
1612
1656
  case 'init':
1613
1657
  await cmdInit(args);
@@ -1616,7 +1660,7 @@ export async function runMemory(subcommand, args) {
1616
1660
  await cmdSetup(args);
1617
1661
  break;
1618
1662
  case 'status':
1619
- await cmdStatus(args);
1663
+ await cmdStatus(args, { wantJson });
1620
1664
  break;
1621
1665
  case 'link':
1622
1666
  await cmdLink(args);
package/cli/provision.mjs CHANGED
@@ -446,7 +446,10 @@ export async function ensureNpmPackage(pkg, { mode, dryRun, force }) {
446
446
  // processes are reading files inside the npm-global install dir. We
447
447
  // can't replace those files atomically while they're open. The caller
448
448
  // is expected to have already killed them via ensureInstancesKilled().
449
- const r = spawnSync('npm', ['install', '-g', `${pkg}@latest`], { stdio: 'inherit' });
449
+ const r = spawnSync('npm', ['install', '-g', `${pkg}@latest`], { stdio: 'inherit', timeout: 600000 });
450
+ if (r.status === null && r.error?.code === 'ETIMEDOUT') {
451
+ return { ok: false, message: `${pkg} install timed out after 10 minutes`, installed: current };
452
+ }
450
453
  if (r.status !== 0) {
451
454
  return { ok: false, message: `${pkg} install failed`, installed: current };
452
455
  }
@@ -471,7 +474,10 @@ export async function updateOpencodeCli({ dryRun, force }) {
471
474
  return { ok: true, message: 'opencode updated via `opencode upgrade`' };
472
475
  }
473
476
  console.log(chalk.dim(' opencode upgrade not available; falling back to npm'));
474
- const r2 = spawnSync('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit' });
477
+ const r2 = spawnSync('npm', ['install', '-g', 'opencode-ai@latest'], { stdio: 'inherit', timeout: 600000 });
478
+ if (r2.status === null && r2.error?.code === 'ETIMEDOUT') {
479
+ return { ok: false, message: 'opencode install timed out after 10 minutes' };
480
+ }
475
481
  if (r2.status === 0) {
476
482
  return { ok: true, message: 'opencode updated via npm' };
477
483
  }
package/install.sh CHANGED
@@ -322,8 +322,6 @@ main() {
322
322
  ;;
323
323
  esac
324
324
 
325
- print_banner
326
-
327
325
  # ── Hand off to the unified provisioner (Node) ─────────────────────────
328
326
  # All cross-OS work (agent files, plugin copy, opencode.json patching,
329
327
  # skills install, doctor check) lives in `cli/provision.mjs:runProvision`.
@@ -350,6 +348,7 @@ main() {
350
348
  warn "provisioner exited with code $rc"
351
349
  exit "$rc"
352
350
  fi
351
+ print_banner
353
352
  else
354
353
  warn "cli/provision.mjs not found — agent files / plugin / opencode.json"
355
354
  warn " were NOT synced. Run \`bizar install\` from a checkout to fix."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "build:dash": "vite build",
23
23
  "test:sdk": "node_modules/.bin/vitest run --root packages/sdk",
24
24
  "test:sdk:watch": "node_modules/.bin/vitest --root packages/sdk",
25
- "test": "npm run typecheck && npm run test:sdk && bun test plugins/bizar/tests/loop.test.ts plugins/bizar/tests/block.test.ts plugins/bizar/tests/stall-think.test.ts plugins/bizar/tests/tools/bg-get-comments.test.ts plugins/bizar/tests/tools/bg-spawn-delegation.test.ts plugins/bizar/tests/tools/opencode-runner.test.ts plugins/bizar/tests/settings.test.ts plugins/bizar/tests/commands.test.ts plugins/bizar/tests/commands-impl.test.ts plugins/bizar/tests/tools/plan-action.test.ts plugins/bizar/tests/tools/wait-for-feedback.test.ts plugins/bizar/tests/tools/read-glyph-feedback.test.ts plugins/bizar/tests/reasoning-clean.test.ts plugins/bizar/tests/key-rotation.test.ts && node bizar-dash/tests/smoke-v2.mjs && node --test bizar-dash/tests/path-safe.test.mjs bizar-dash/tests/tmux-wrap.test.mjs bizar-dash/tests/opencode-sessions-detail.test.mjs bizar-dash/tests/opencode-runner.test.mjs bizar-dash/tests/mod-instructions.node.test.mjs bizar-dash/tests/mod-upgrade.node.test.mjs bizar-dash/tests/graphify-mod-spawn.node.test.mjs bizar-dash/tests/no-agent-browser.node.test.mjs bizar-dash/tests/providers-store-backup-keys.node.test.mjs bizar-dash/tests/dashboard-ports.test.mjs bizar-dash/tests/submit-feedback.test.mjs bizar-dash/tests/yaml.test.mjs bizar-dash/tests/memory-store.test.mjs bizar-dash/tests/memory-schema.test.mjs bizar-dash/tests/memory-secrets.test.mjs bizar-dash/tests/memory-git.test.mjs bizar-dash/tests/memory-sync.test.mjs bizar-dash/tests/obsidian-back-compat.test.mjs bizar-dash/tests/memory-lightrag.test.mjs bizar-dash/tests/memory-config.test.mjs bizar-dash/tests/memory-cli.test.mjs bizar-dash/tests/memory-cli-readlistdelete.test.mjs bizar-dash/tests/memory-cli-setup.test.mjs bizar-dash/tests/memory-conflicts.test.mjs bizar-dash/tests/memory-namespace.test.mjs bizar-dash/tests/memory-path-safety.test.mjs bizar-dash/tests/memory-protocol-drift.test.mjs bizar-dash/tests/memory-roundtrip.test.mjs",
25
+ "test": "npm run typecheck && npm run test:sdk && bun test plugins/bizar/tests/loop.test.ts plugins/bizar/tests/block.test.ts plugins/bizar/tests/stall-think.test.ts plugins/bizar/tests/tools/bg-get-comments.test.ts plugins/bizar/tests/tools/bg-spawn-delegation.test.ts plugins/bizar/tests/tools/opencode-runner.test.ts plugins/bizar/tests/settings.test.ts plugins/bizar/tests/commands.test.ts plugins/bizar/tests/commands-impl.test.ts plugins/bizar/tests/tools/plan-action.test.ts plugins/bizar/tests/tools/wait-for-feedback.test.ts plugins/bizar/tests/tools/read-glyph-feedback.test.ts plugins/bizar/tests/reasoning-clean.test.ts plugins/bizar/tests/key-rotation.test.ts && node bizar-dash/tests/smoke-v2.mjs && node --test bizar-dash/tests/path-safe.test.mjs bizar-dash/tests/tmux-wrap.test.mjs bizar-dash/tests/opencode-sessions-detail.test.mjs bizar-dash/tests/opencode-runner.test.mjs bizar-dash/tests/mod-instructions.node.test.mjs bizar-dash/tests/mod-upgrade.node.test.mjs bizar-dash/tests/graphify-mod-spawn.node.test.mjs bizar-dash/tests/no-agent-browser.node.test.mjs bizar-dash/tests/providers-store-backup-keys.node.test.mjs bizar-dash/tests/dashboard-ports.test.mjs bizar-dash/tests/submit-feedback.test.mjs bizar-dash/tests/yaml.test.mjs bizar-dash/tests/memory-store.test.mjs bizar-dash/tests/memory-schema.test.mjs bizar-dash/tests/memory-secrets.test.mjs bizar-dash/tests/memory-git.test.mjs bizar-dash/tests/memory-sync.test.mjs bizar-dash/tests/obsidian-back-compat.test.mjs bizar-dash/tests/memory-lightrag.test.mjs bizar-dash/tests/memory-config.test.mjs bizar-dash/tests/memory-cli.test.mjs bizar-dash/tests/memory-cli-readlistdelete.test.mjs bizar-dash/tests/memory-cli-setup.test.mjs bizar-dash/tests/memory-conflicts.test.mjs bizar-dash/tests/memory-namespace.test.mjs bizar-dash/tests/memory-path-safety.test.mjs bizar-dash/tests/memory-protocol-drift.test.mjs bizar-dash/tests/memory-roundtrip.test.mjs bizar-dash/tests/cli-bugfixes.test.mjs bizar-dash/tests/server-bugfixes.test.mjs bizar-dash/tests/frontend-bugfixes.test.mjs",
26
26
  "build": "npm run build:sdk && npm run build:dash",
27
27
  "prepublishOnly": "npm run build"
28
28
  },
@@ -85,7 +85,7 @@
85
85
  "@vitejs/plugin-react": "^4.3.0",
86
86
  "typescript": "^5.6.0",
87
87
  "vite": "^5.0.0",
88
- "vitest": "^2.1.0"
88
+ "vitest": "^4.1.9"
89
89
  },
90
90
  "allowScripts": {
91
91
  "better-sqlite3@12.11.1": true