@polderlabs/bizar 4.4.2 → 4.4.4

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.
@@ -29,10 +29,10 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-B3RgW6FS.js"></script>
33
- <link rel="modulepreload" crossorigin href="/assets/mobile-Dvq2d53Y.js">
32
+ <script type="module" crossorigin src="/assets/main-C4Dq4wTz.js"></script>
33
+ <link rel="modulepreload" crossorigin href="/assets/mobile-CaZ0gEeJ.js">
34
34
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
35
- <link rel="stylesheet" crossorigin href="/assets/main-DlJ8wgLR.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/main-Nq8Dq3VR.css">
36
36
  </head>
37
37
  <body>
38
38
  <div id="root"></div>
@@ -12,8 +12,8 @@
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
14
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
15
- <script type="module" crossorigin src="/assets/mobile-DbxIw8BG.js"></script>
16
- <link rel="modulepreload" crossorigin href="/assets/mobile-Dvq2d53Y.js">
15
+ <script type="module" crossorigin src="/assets/mobile-CQTXbuHq.js"></script>
16
+ <link rel="modulepreload" crossorigin href="/assets/mobile-CaZ0gEeJ.js">
17
17
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
18
18
  </head>
19
19
  <body>
@@ -193,9 +193,24 @@ async function startDashboard({ port, projectRoot, opencodeConfigDir, bizarRoot,
193
193
  // Background mode: do NOT launch a browser (the caller already detached),
194
194
  // do NOT block on a signal. The caller (detachAfterBoot) decides how to
195
195
  // keep this process alive (or not) once we return.
196
+ // v4.4.3 — Pin the server + close callback at module scope so they
197
+ // are not garbage-collected when the caller discards the return
198
+ // value. Without this, the listening socket is closed by GC, the
199
+ // event loop drains, and the process exits immediately after
200
+ // startDashboard resolves.
201
+ if (bg) {
202
+ backgroundHandle = { url, port: usePort, close };
203
+ }
196
204
  return { url, port: usePort, close };
197
205
  }
198
206
 
207
+ /**
208
+ * v4.4.3 — Module-scope handle for the background-mode dashboard.
209
+ * Prevents the express `server` from being garbage-collected after
210
+ * the caller discards startDashboard's return value.
211
+ */
212
+ let backgroundHandle = null;
213
+
199
214
  /**
200
215
  * Called after startDashboard returns in background mode. The server is
201
216
  * listening and the PID/PORT files are written. We intentionally keep
package/cli/bin.mjs CHANGED
@@ -695,6 +695,35 @@ async function main() {
695
695
  const result = await runDoctor();
696
696
  if (result.failed > 0) process.exit(1);
697
697
  }
698
+ } else if (args[0] === 'repair') {
699
+ // v4.4.3 — One-shot repair for stale `bizar` bin symlinks.
700
+ // Symptom: after `npm i -g @polderlabs/bizar`, the package is
701
+ // installed under `npm root -g` but the `bizar` symlink on PATH
702
+ // still points at a legacy install path (e.g. ~/.local/lib/...).
703
+ // Calls of `bizar ...` then run the old code with the old bugs.
704
+ if (isHelpRequest) {
705
+ console.log(`
706
+ bizar repair — Fix common install issues
707
+
708
+ Usage:
709
+ bizar repair Diagnose + fix stale bin symlinks and mismatched versions
710
+ bizar repair --dry-run Show what would change without modifying anything
711
+ bizar repair --bin-only Only fix the bin symlink; skip version checks
712
+ `);
713
+ } else {
714
+ const { runRepair } = await import('./repair.mjs');
715
+ const dryRun = args.includes('--dry-run');
716
+ const binOnly = args.includes('--bin-only');
717
+ const result = await runRepair({ dryRun, binOnly });
718
+ if (!result.ok) {
719
+ for (const n of result.notes) console.log(` ${n}`);
720
+ process.exit(1);
721
+ }
722
+ for (const n of result.notes) console.log(` ${n}`);
723
+ if (result.fixed.length > 0) {
724
+ console.log(chalk.green('\n Repair complete. Re-run your shell or `hash -r` to pick up the new path.'));
725
+ }
726
+ }
698
727
  } else if (args[0] === 'heads-up') {
699
728
  // v3.21.0 — Pre-push / pre-release heads-ups
700
729
  await runHeadsUp(args[1], args.slice(2));
@@ -702,7 +731,23 @@ async function main() {
702
731
  await runArtifact(args.slice(1), {});
703
732
  } else if (args[0] === 'install') {
704
733
  if (isHelpRequest) showInstallHelp();
705
- else await runInstaller();
734
+ else {
735
+ await runInstaller();
736
+ // v4.4.3 — After install, repair any stale bin symlinks so the
737
+ // user picks up the new code (the installer itself may have
738
+ // been running from a stale install path).
739
+ try {
740
+ const { runRepair } = await import('./repair.mjs');
741
+ const r = await runRepair({});
742
+ if (r.fixed.length > 0) {
743
+ console.log(chalk.cyan('\n Repair: repointed stale bin symlinks:'));
744
+ for (const f of r.fixed) console.log(` ${f}`);
745
+ console.log(chalk.dim(' Re-run your shell or `hash -r` to pick up the new path.'));
746
+ }
747
+ } catch (err) {
748
+ console.log(chalk.dim(` Repair skipped: ${err.message}`));
749
+ }
750
+ }
706
751
  } else if (args[0] === 'service') {
707
752
  if (isHelpRequest) showServiceHelp();
708
753
  else await runServiceCommand(args[1]);
@@ -832,19 +877,11 @@ async function runDash(dashArgs) {
832
877
 
833
878
  switch (sub) {
834
879
  case 'start':
835
- // v4.4.0 — When `--bg` is passed, spawn the dashboard as a
836
- // detached child process via the dash module's startInBackground
837
- // helper, then return. Without this, runDash calls startDashboard
838
- // in-process; once the bin's main() resolves, the bin process
839
- // exits and takes the dashboard's HTTP server with it. The
840
- // downstream effect is "(intermediate value)(intermediate value)
841
- // (intermediate value) is not a function or its return value is
842
- // not iterable" — a pre-existing crash signature in this codepath.
843
880
  if (subOpts.bg) {
844
- // Spawn the dashboard as a detached child process. The child's
845
- // argv is `node cli.mjs start --bg` so the dash module's own
846
- // CLI dispatch hits the background branch and runs
847
- // startDashboard in the child, not the parent.
881
+ // v4.4.0 — Background mode: spawn the dashboard as a detached
882
+ // child process via dashModule.startInBackground(). The child
883
+ // runs `node cli.mjs start --bg` which keeps itself alive via
884
+ // the bg-* pollers and the (v4.4.3) module-scope backgroundHandle.
848
885
  await dashModule.startInBackground(['start', ...(subOpts.subArgs || [])]);
849
886
  } else {
850
887
  await dashModule.start(subOpts);
package/cli/repair.mjs ADDED
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli/repair.mjs
4
+ *
5
+ * v4.4.3 — One-shot repair for common install issues.
6
+ *
7
+ * Symptom this fixes: after `npm i -g @polderlabs/bizar`, the package is
8
+ * installed under the npm-global location (e.g. ~/.local/npm/lib/...) but
9
+ * the `bizar` symlink on PATH still points at a legacy install path
10
+ * (e.g. ~/.local/lib/...). Calls of `bizar ...` then run the old code
11
+ * with the old bugs. The fix is to repoint the bin symlink at the
12
+ * currently-resolved package.
13
+ *
14
+ * Exports:
15
+ * runRepair({ dryRun, binOnly }):
16
+ * Returns { ok: boolean, fixed: string[], notes: string[] }
17
+ */
18
+ import {
19
+ existsSync,
20
+ readlinkSync,
21
+ lstatSync,
22
+ symlinkSync,
23
+ unlinkSync,
24
+ readFileSync,
25
+ realpathSync,
26
+ } from 'node:fs';
27
+ import { join, dirname, resolve as pathResolve } from 'node:path';
28
+ import { homedir } from 'node:os';
29
+ import { execFileSync } from 'node:child_process';
30
+
31
+ const HOME = homedir();
32
+
33
+ function npmRootGlobal() {
34
+ try {
35
+ return execFileSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000 }).trim();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function findPackageRootFrom(startDir, targetName) {
42
+ let dir = startDir;
43
+ // Walk up at most 10 levels to avoid infinite loops on weird filesystems.
44
+ for (let i = 0; i < 10; i++) {
45
+ const pkgPath = join(dir, 'package.json');
46
+ if (existsSync(pkgPath)) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
49
+ if (pkg.name === targetName) return dir;
50
+ } catch {
51
+ /* skip */
52
+ }
53
+ }
54
+ const parent = pathResolve(dir, '..');
55
+ if (parent === dir) return null;
56
+ dir = parent;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Resolve the package directory the currently-running `bizar` came from.
63
+ * Walks `process.argv[1]` (the bin entry) up to the package root via the
64
+ * nearest package.json with `"name": "@polderlabs/bizar"`.
65
+ */
66
+ function resolveCurrentPackageRoot() {
67
+ const candidates = [
68
+ process.argv[1],
69
+ // eslint-disable-next-line no-undef
70
+ typeof import.meta !== 'undefined' ? import.meta.dirname : null,
71
+ ].filter(Boolean);
72
+
73
+ for (const c of candidates) {
74
+ let dir;
75
+ try {
76
+ dir = dirname(realpathSync(c));
77
+ } catch {
78
+ dir = dirname(c);
79
+ }
80
+ const root = findPackageRootFrom(dir, '@polderlabs/bizar');
81
+ if (root) return root;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Given a symlink target like `../lib/node_modules/@polderlabs/bizar/cli/bin.mjs`,
88
+ * resolve the package root it points at (the directory containing package.json
89
+ * with `"name": "@polderlabs/bizar"`).
90
+ */
91
+ function packageRootFromLink(linkPath) {
92
+ let dir;
93
+ try {
94
+ dir = dirname(realpathSync(linkPath));
95
+ } catch {
96
+ return null;
97
+ }
98
+ return findPackageRootFrom(dir, '@polderlabs/bizar');
99
+ }
100
+
101
+ /**
102
+ * Find any existing `bizar` symlink on PATH that points at a different
103
+ * `@polderlabs/bizar` package root than the one currently installed.
104
+ */
105
+ function findStaleBinLinks(expectedPkgRoot) {
106
+ const out = [];
107
+ const npmGlobal = npmRootGlobal();
108
+ const binDirs = [
109
+ join(HOME, '.local', 'bin'),
110
+ join(HOME, '.npm-global', 'bin'),
111
+ '/usr/local/bin',
112
+ npmGlobal ? join(npmGlobal, '..', '..', 'bin') : null,
113
+ '/usr/bin',
114
+ ].filter((p) => p && existsSync(p));
115
+
116
+ for (const dir of binDirs) {
117
+ const link = join(dir, 'bizar');
118
+ if (!existsSync(link)) continue;
119
+ let st;
120
+ try {
121
+ st = lstatSync(link);
122
+ } catch {
123
+ continue;
124
+ }
125
+ if (!st.isSymbolicLink()) continue;
126
+
127
+ const pkgRoot = packageRootFromLink(link);
128
+ if (pkgRoot && pathResolve(pkgRoot) !== pathResolve(expectedPkgRoot)) {
129
+ out.push({
130
+ linkPath: link,
131
+ currentPkgRoot: pkgRoot,
132
+ expectedPkgRoot,
133
+ });
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+
139
+ function fixBinLink(linkPath, expectedPkgRoot) {
140
+ const newTarget = join(expectedPkgRoot, 'cli', 'bin.mjs');
141
+ try {
142
+ unlinkSync(linkPath);
143
+ } catch (err) {
144
+ return { ok: false, error: `unlink failed: ${err.message}` };
145
+ }
146
+ try {
147
+ symlinkSync(newTarget, linkPath);
148
+ } catch (err) {
149
+ return { ok: false, error: `symlink failed: ${err.message}` };
150
+ }
151
+ return { ok: true, newTarget };
152
+ }
153
+
154
+ export async function runRepair({ dryRun = false, binOnly = false } = {}) {
155
+ const notes = [];
156
+ const fixed = [];
157
+
158
+ // The "expected" package root is the npm-global install. We repair
159
+ // any bin symlinks that point at a different @polderlabs/bizar
160
+ // installation. When running from a source checkout, we still do the
161
+ // repair (the npm-global install is the canonical one).
162
+ const npmGlobal = npmRootGlobal();
163
+ let expectedPkgRoot = null;
164
+ if (npmGlobal) {
165
+ const candidate = join(npmGlobal, '@polderlabs', 'bizar');
166
+ if (existsSync(join(candidate, 'package.json'))) {
167
+ expectedPkgRoot = candidate;
168
+ }
169
+ }
170
+
171
+ if (!expectedPkgRoot) {
172
+ return {
173
+ ok: false,
174
+ fixed,
175
+ notes: [
176
+ 'Could not locate the npm-global @polderlabs/bizar package.',
177
+ 'Is the package installed? `npm i -g @polderlabs/bizar`',
178
+ ],
179
+ };
180
+ }
181
+
182
+ // Report current state.
183
+ notes.push(`Expected package root: ${expectedPkgRoot}`);
184
+ try {
185
+ const pkg = JSON.parse(readFileSync(join(expectedPkgRoot, 'package.json'), 'utf8'));
186
+ notes.push(`Expected package version: ${pkg.version}`);
187
+ } catch { /* ignore */ }
188
+
189
+ // Find stale bin symlinks.
190
+ const stale = findStaleBinLinks(expectedPkgRoot);
191
+ if (stale.length === 0) {
192
+ notes.push('No stale bin symlinks detected.');
193
+ } else {
194
+ for (const s of stale) {
195
+ notes.push(`Stale: ${s.linkPath} -> ${s.currentPkgRoot}`);
196
+ notes.push(` Expected: ${s.expectedPkgRoot}`);
197
+ if (!dryRun) {
198
+ const r = fixBinLink(s.linkPath, s.expectedPkgRoot);
199
+ if (r.ok) {
200
+ fixed.push(`${s.linkPath} -> ${r.newTarget}`);
201
+ } else {
202
+ notes.push(` Failed to fix ${s.linkPath}: ${r.error}`);
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ return { ok: true, fixed, notes, dryRun };
209
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "4.4.2",
3
+ "version": "4.4.4",
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": {
@@ -19,10 +19,12 @@
19
19
  "scripts": {
20
20
  "typecheck": "tsc --noEmit",
21
21
  "build:sdk": "tsc -p packages/sdk/tsconfig.json",
22
+ "build:dash": "vite build",
22
23
  "test:sdk": "node_modules/.bin/vitest run --root packages/sdk",
23
24
  "test:sdk:watch": "node_modules/.bin/vitest --root packages/sdk",
24
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
- "build": "npm run build:sdk"
26
+ "build": "npm run build:sdk && npm run build:dash",
27
+ "prepublishOnly": "npm run build"
26
28
  },
27
29
  "keywords": [
28
30
  "opencode",