@polderlabs/bizar 3.9.1 → 3.11.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/README.md CHANGED
@@ -143,7 +143,9 @@ npx bizar
143
143
 
144
144
  The interactive installer walks you through component selection, agent choice, install mode, API key setup, and auto-restarts opencode.
145
145
 
146
- ### Sourcegit clone (contributors)
146
+ > **Windows users:** the `npm install -g @polderlabs/bizar` command above is the recommended path on Windows. The installer uses `irm | iex` for uv, `py -m pip` for the pip fallback, `taskkill` for forced kills, and JS `setTimeout` instead of `sleep` so it works on both Windows PowerShell and POSIX shells. See the [Windows](#-windows) section below for prerequisites, known limitations, and the optional graph feature install.
147
+
148
+ ### Source — git clone (Linux/macOS contributors)
147
149
 
148
150
  ```bash
149
151
  git clone git@github.com:DrB0rk/BizarHarness.git
@@ -154,6 +156,50 @@ chmod +x install.sh
154
156
 
155
157
  Copies agent definitions and config to `~/.config/opencode/`, merges `opencode.json`, and prints next steps. Use this if you want to hack on BizarHarness itself.
156
158
 
159
+ > This `install.sh` script is bash-only and does **not** run on Windows natively. Windows contributors should use the npm path above instead.
160
+
161
+ ### Windows
162
+
163
+ The `bizar` CLI is cross-platform — the npm install runs a Node.js installer that handles `curl | sh` redirects, signal handling, and temp paths on Windows PowerShell automatically.
164
+
165
+ **Prerequisites**
166
+
167
+ - [Node.js 18+](https://nodejs.org/) on `PATH`
168
+ - [Python 3.10+](https://www.python.org/downloads/windows/) (only required for the optional `bizar graph` knowledge-graph feature)
169
+ - [Git for Windows](https://git-scm.com/download/win) — only needed if you want to run the bash `install.sh` from a Git Bash shell
170
+
171
+ **Install**
172
+
173
+ ```powershell
174
+ npm install -g @polderlabs/bizar
175
+ bizar
176
+ ```
177
+
178
+ **Optional: graph feature (knowledge graph + per-project .bizar/)**
179
+
180
+ The `bizar graph build` subcommand uses [graphify](https://github.com/bretbhomas/graphify). Install uv and graphify from PowerShell:
181
+
182
+ ```powershell
183
+ # Install uv (Python package manager) — Windows-native PowerShell installer
184
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
185
+
186
+ # Install graphify
187
+ uv tool install graphifyy
188
+ ```
189
+
190
+ Then build the project graph from any repo:
191
+
192
+ ```powershell
193
+ bizar graph build
194
+ ```
195
+
196
+ **Known Windows limitations**
197
+
198
+ - The `blessed` TUI library may render with quirks inside Windows Terminal; if you see garbled output in the TUI, use the browser UI via `bizar dash start --bg` and open http://localhost:4321 instead.
199
+ - Some shell scripts under `config/skills/embedded-esp-idf/` are bash-only and require Git Bash (or WSL).
200
+ - The bash `install.sh` does not run on Windows cmd/PowerShell; use the `npm install -g` path above.
201
+ - RTK (Rust Token Killer) ships a bash-only installer. On Windows, install it manually via [Cargo](https://github.com/rtk-ai/rtk#installation) or a [prebuilt binary](https://github.com/rtk-ai/rtk/releases), then run `rtk init -g --opencode`.
202
+
157
203
  ### Prerequisites
158
204
 
159
205
  - [opencode CLI](https://opencode.ai) installed and on `$PATH`
package/cli/bin.mjs CHANGED
@@ -2,20 +2,17 @@
2
2
  /**
3
3
  * cli/bin.mjs
4
4
  *
5
- * v3.0.0 — `bizar` runtime CLI.
5
+ * v3.10.0 — `bizar` runtime CLI.
6
6
  *
7
7
  * Architecture:
8
8
  * - `bizar` is the core runtime + installer + audit/init/export/update/plan
9
- * + service commands.
10
- * - The dashboard lives in a separate package, `@polderlabs/bizar-dash`.
11
- * If it's installed, `bizar dashboard` / `bizar --web*` will defer to it.
12
- * If not, the user is told to install it.
9
+ * + service + dash commands.
10
+ * - The dashboard lives in `@polderlabs/bizar-dash` as a library.
11
+ * Commands live under `bizar dash <subcommand>` (new canonical form).
12
+ * `bizar dashboard` is a deprecated alias (still works, prints warning).
13
13
  *
14
- * Subcommands (unchanged from v2.7.0):
15
- * install, audit, init, export, plan, update, test-gate, service
16
- *
17
- * Flags (unchanged):
18
- * --web / --no-web / --web-only / --bg / --detach
14
+ * Subcommands:
15
+ * install, audit, init, export, plan, update, test-gate, service, dash
19
16
  */
20
17
  import { existsSync } from 'node:fs';
21
18
  import { readFileSync } from 'node:fs';
@@ -77,36 +74,34 @@ function showHelp() {
77
74
  Bizar — Norse Pantheon Agent System for opencode
78
75
 
79
76
  Usage:
80
- bizar Launch the TUI dashboard (auto-runs first-time setup if needed)
81
- bizar --web Launch TUI + auto-open web dashboard
82
- bizar --no-web Launch TUI only (no browser)
83
- bizar --web-only Web dashboard only (no TUI, in browser)
84
- bizar --bg, --detach Launch web dashboard in background, return to shell
85
- bizar install Run the interactive installer
86
- bizar audit Run security audit on agent configuration
87
- bizar init Initialize .bizar/ in current project
88
- bizar export [target] Export agents/rules to another harness
89
- bizar plan <subcommand> Manage visual plans
90
- bizar graph Per-project knowledge graph (powered by graphify)
91
- bizar test-gate Detect & run the project's test suite
92
- bizar update Update opencode, bizar, and/or bizar-plugin
93
- bizar service Manage the background service daemon
94
- bizar dashboard Launch the web dashboard (uses bizar)
95
- bizar --setup Re-run setup manually (agents, plugin, RTK, Semble, Skills CLI)
96
- bizar --check Print setup status as JSON, exit 1 if setup needed
97
- bizar --version Show package version
98
- bizar --help Show this help
77
+ bizar <command> [options]
78
+
79
+ Commands:
80
+ install Run the interactive installer
81
+ audit Run security audit on agent configuration
82
+ init Initialize .bizar/ in current project
83
+ export [target] Export agents/rules to another harness
84
+ plan <subcommand> Manage visual plans
85
+ graph Per-project knowledge graph (powered by graphify)
86
+ test-gate Detect & run the project's test suite
87
+ update Update opencode, bizar, and/or bizar-plugin
88
+ service Manage the background service daemon
89
+ dash <subcommand> Manage the dashboard (start/stop/status/tui)
90
+
91
+ Examples:
92
+ bizar install
93
+ bizar audit
94
+ bizar dash start
95
+ bizar dash start --bg
96
+ bizar dash stop
97
+ bizar dash status
98
+
99
+ Run \`bizar <command> --help\` for per-command help.
99
100
 
100
101
  Install:
101
102
  npm install -g @polderlabs/bizar Install globally
102
- npm install -g @polderlabs/bizar-dash Optional companion dashboard
103
+ npm install -g @polderlabs/bizar-dash Optional dashboard package
103
104
  npm install -g @polderlabs/bizar-plugin Bizar opencode plugin
104
-
105
- Notes:
106
- The TUI is the default command — press 1-8 for tabs, q to quit.
107
- \`bizar --bg\` launches the dashboard detached; \`bizar dashboard stop\`
108
- terminates it. The web dashboard lives in the \`@polderlabs/bizar-dash\`
109
- package — if it's not installed, you'll be prompted to install it.
110
105
  `);
111
106
  }
112
107
 
@@ -181,7 +176,7 @@ function showInstallHelp() {
181
176
 
182
177
  function showUpdateHelp() {
183
178
  console.log(`
184
- bizar update — Update opencode, bizar, bizar-dash, and/or bizar-plugin
179
+ bizar update — Update opencode, bizar, and/or bizar-plugin
185
180
 
186
181
  Usage:
187
182
  bizar update Interactive prompt for components
@@ -207,7 +202,7 @@ function showUpdateHelp() {
207
202
  • Sends SIGTERM, waits up to 5s, escalates to SIGKILL if needed.
208
203
  • Re-runs the install script so the deployed plugin source matches
209
204
  the just-upgraded npm version (avoids the version-skew trap).
210
- • If the dashboard was running and bizar / bizar-dash were updated,
205
+ • If the dashboard was running and bizar or dash was updated,
211
206
  spawns a fresh detached dashboard process with the new code
212
207
  (skipped with --no-restart).
213
208
 
@@ -244,19 +239,35 @@ function showServiceHelp() {
244
239
  `);
245
240
  }
246
241
 
247
- function showDashboardHelp() {
242
+ function showDashHelp() {
248
243
  console.log(`
249
- bizar dashboardLaunch the web dashboard (uses @polderlabs/bizar-dash)
244
+ bizar dashManage the Bizar dashboard
250
245
 
251
246
  Usage:
252
- bizar dashboard Start the dashboard
253
- bizar dashboard start Same
254
- bizar dashboard stop Kill the running dashboard
255
- bizar dashboard status Show port + URL
247
+ bizar dash <subcommand> [options]
256
248
 
257
- Description:
258
- The dashboard lives in a separate package. If it's not installed,
259
- you'll see an install hint pointing at @polderlabs/bizar-dash.
249
+ Subcommands:
250
+ start [--bg] [--port N] Start the dashboard (default port 4321)
251
+ stop Stop the running dashboard
252
+ status Show dashboard port and URL
253
+ tui [--no-web] Launch the TUI
254
+
255
+ Options:
256
+ --bg Detach and run in background (for start)
257
+ --port N Override the default port
258
+ --no-web Skip launching the web UI (for tui)
259
+
260
+ Examples:
261
+ bizar dash start
262
+ bizar dash start --bg
263
+ bizar dash stop
264
+ bizar dash status
265
+ bizar dash tui
266
+ bizar dash tui --no-web
267
+
268
+ Note:
269
+ \`bizar dashboard\` is a deprecated alias for \`bizar dash\` and still
270
+ works, but new code should use \`bizar dash\`.
260
271
  `);
261
272
  }
262
273
 
@@ -420,35 +431,131 @@ async function main() {
420
431
  } else if (args[0] === 'service') {
421
432
  if (isHelpRequest) showServiceHelp();
422
433
  else await runServiceCommand(args[1]);
423
- } else if (args[0] === 'dashboard' || args[0] === 'start' || args[0] === 'stop' || args[0] === 'status' || args[0] === 'tui') {
424
- if (isHelpRequest) showDashboardHelp();
425
- else await delegateToDash(args.slice(1));
426
- } else if (args.includes('--bg') || args.includes('--detach')) {
427
- // Delegate to bizar-dash
428
- await delegateToDash(['--bg']);
429
- } else if (args.includes('--web-only')) {
430
- await delegateToDash(['--web-only']);
434
+ } else if (args[0] === 'dash' || args[0] === 'dashboard') {
435
+ // `bizar dashboard` is a deprecated alias for `bizar dash`
436
+ if (args[0] === 'dashboard') {
437
+ console.warn(chalk.yellow(' `bizar dashboard` is deprecated, use `bizar dash` instead.'));
438
+ }
439
+ const dashArgs = args.slice(1); // everything after 'dash' or 'dashboard'
440
+ if (dashArgs.length === 0 || isHelpRequest) {
441
+ showDashHelp();
442
+ } else {
443
+ await runDash(dashArgs);
444
+ }
431
445
  } else if (isHelpRequest) {
432
446
  showHelp();
433
447
  } else {
434
- // Default: launch the TUI dashboard. The TUI lives in bizar-dash.
435
- const dashPath = await findBizarDash();
436
- if (dashPath) {
437
- const skipWeb = args.includes('--no-web');
438
- const forceWeb = args.includes('--web');
439
- const settingAuto = await readAutoLaunchWeb();
440
- const launchWeb = !skipWeb && (forceWeb || settingAuto);
441
- await delegateToDash(['tui', ...(launchWeb ? [] : ['--no-web'])]);
448
+ // No args show help (breaking: previously launched TUI)
449
+ showHelp();
450
+ process.exit(1);
451
+ }
452
+ }
453
+
454
+ // ── Dashboard subcommand ───────────────────────────────────────────────────────
455
+
456
+ /**
457
+ * Parse dash-specific options from an array of args.
458
+ * Returns { opts, remaining } where opts have --bg / --port stripped.
459
+ */
460
+ function parseDashOpts(dashArgs) {
461
+ const opts = { bg: false, port: null, noWeb: false };
462
+ const remaining = [];
463
+ for (let i = 0; i < dashArgs.length; i++) {
464
+ const a = dashArgs[i];
465
+ if (a === '--bg') {
466
+ opts.bg = true;
467
+ } else if (a === '--no-web') {
468
+ opts.noWeb = true;
469
+ } else if (a === '--port' && i + 1 < dashArgs.length) {
470
+ opts.port = Number(dashArgs[i + 1]);
471
+ i++;
442
472
  } else {
443
- console.log('The Bizar dashboard is in a separate package:');
444
- console.log(chalk.cyan(' npm install -g @polderlabs/bizar-dash'));
445
- console.log('');
446
- console.log('Or run the installer to set up everything:');
447
- console.log(chalk.cyan(' npx -y @polderlabs/bizar install'));
473
+ remaining.push(a);
448
474
  }
449
475
  }
476
+ return { opts, remaining };
450
477
  }
451
478
 
479
+ /**
480
+ * Try to load the dashboard CLI module.
481
+ * Try 1: import via the package exports map (@polderlabs/bizar-dash/dash-cli)
482
+ * Try 2: file path probing for older installs or dev trees
483
+ */
484
+ async function loadDashCli() {
485
+ // Try 1: import via exports map (requires Tyr's package.json changes)
486
+ try {
487
+ const mod = await import('@polderlabs/bizar-dash/dash-cli');
488
+ return mod;
489
+ } catch (_e) {
490
+ // fall through to file probing
491
+ }
492
+
493
+ // Try 2: file path probing
494
+ const { join } = await import('node:path');
495
+ const { homedir } = await import('node:os');
496
+ const candidates = [
497
+ // npm global install
498
+ join(homedir(), '.npm-global', 'lib', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs'),
499
+ // nvm
500
+ join(homedir(), '.nvm', 'versions', 'node'),
501
+ // relative to current process
502
+ join(process.execPath, '..', '..', 'lib', 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs'),
503
+ // local node_modules
504
+ join(process.cwd(), 'node_modules', '@polderlabs', 'bizar-dash', 'src', 'cli.mjs'),
505
+ ];
506
+
507
+ const { pathToFileURL } = await import('node:url');
508
+ for (const p of candidates) {
509
+ try {
510
+ const url = pathToFileURL(p).href;
511
+ const mod = await import(url);
512
+ return mod;
513
+ } catch (_e) {
514
+ // try next
515
+ }
516
+ }
517
+ return null;
518
+ }
519
+
520
+ /**
521
+ * Dispatch a dashboard subcommand by loading the dashboard module and
522
+ * calling the appropriate exported function.
523
+ */
524
+ async function runDash(dashArgs) {
525
+ const { opts, remaining } = parseDashOpts(dashArgs);
526
+ const sub = remaining[0];
527
+ const subOpts = { ...opts, subArgs: remaining.slice(1) };
528
+
529
+ const dashModule = await loadDashCli();
530
+ if (!dashModule) {
531
+ console.error(chalk.red(' ✗ Dashboard not installed.'));
532
+ console.error(chalk.dim(' Run: npm install -g @polderlabs/bizar-dash'));
533
+ console.error(chalk.dim(' Or: npx -y @polderlabs/bizar install'));
534
+ process.exit(1);
535
+ }
536
+
537
+ switch (sub) {
538
+ case 'start':
539
+ await dashModule.start(subOpts);
540
+ break;
541
+ case 'stop':
542
+ await dashModule.stop(subOpts);
543
+ break;
544
+ case 'status':
545
+ await dashModule.status(subOpts);
546
+ break;
547
+ case 'tui':
548
+ await dashModule.tui(subOpts);
549
+ break;
550
+ default:
551
+ console.error(chalk.red(` ✗ Unknown subcommand: ${sub}`));
552
+ showDashHelp();
553
+ process.exit(1);
554
+ }
555
+ }
556
+
557
+ // ── Main ──────────────────────────────────────────────────────────────────────
558
+
452
559
  await main().catch((err) => {
453
560
  console.error(chalk.red(`bizar: ${err && err.message ? err.message : String(err)}`));
454
561
  process.exit(1);
package/cli/copy.mjs CHANGED
@@ -356,6 +356,9 @@ export async function installRtk() {
356
356
  const spinner = ora({ text: 'Installing RTK (Rust Token Killer)...', color: 'magenta' }).start();
357
357
 
358
358
  if (process.platform === 'win32') {
359
+ // RTK ships a bash-only installer; no PowerShell equivalent. On
360
+ // Windows the user must install RTK manually (e.g. via Cargo or
361
+ // prebuilt binary) and re-run `rtk init -g --opencode`.
359
362
  spinner.fail(chalk.red('Automatic RTK install not supported on Windows. Install manually from https://github.com/rtk-ai/rtk'));
360
363
  return false;
361
364
  }
@@ -392,13 +395,17 @@ export async function installSemble() {
392
395
  spinner.text = 'Installing uv (Python package manager)...';
393
396
  try {
394
397
  if (process.platform === 'win32') {
395
- spinner.fail(chalk.red('Automatic uv install not supported on Windows. Install from https://docs.astral.sh/uv'));
396
- return false;
398
+ // Windows: use the official PowerShell installer
399
+ execSync(
400
+ 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"',
401
+ { stdio: 'pipe', timeout: 60000 },
402
+ );
403
+ } else {
404
+ execSync(
405
+ 'curl -LsSf https://astral.sh/uv/install.sh | sh',
406
+ { stdio: 'pipe', timeout: 60000 },
407
+ );
397
408
  }
398
- execSync(
399
- 'curl -LsSf https://astral.sh/uv/install.sh | sh',
400
- { stdio: 'pipe', timeout: 60000 },
401
- );
402
409
  spinner.text = 'Setting up Semble...';
403
410
  } catch {
404
411
  spinner.fail(chalk.red('uv install failed. Install manually from https://docs.astral.sh/uv'));
package/cli/graph.mjs CHANGED
@@ -20,13 +20,18 @@ export const GRAPHIFY_OUT_ENV = 'GRAPHIFY_OUT';
20
20
  /**
21
21
  * Find a Python 3 interpreter on PATH.
22
22
  * Returns the absolute path string, or null if none found.
23
+ *
24
+ * On Windows the `py` launcher is the most reliable option; on Unix
25
+ * we prefer python3 then python.
23
26
  */
24
27
  export function findPython() {
25
- // Prefer python3, fall back to python
26
- for (const name of ['python3', 'python']) {
28
+ const candidates = process.platform === 'win32'
29
+ ? ['py', 'python', 'python3']
30
+ : ['python3', 'python'];
31
+ for (const name of candidates) {
27
32
  try {
28
33
  const result = spawnSync(name, ['--version'], { encoding: 'utf8', timeout: 5000 });
29
- if (result.status === 0 && result.stdout.includes('Python 3')) {
34
+ if (result.status === 0 && result.stdout.includes('Python')) {
30
35
  return name;
31
36
  }
32
37
  } catch {
@@ -37,10 +42,33 @@ export function findPython() {
37
42
  }
38
43
 
39
44
  /**
40
- * Check whether graphify is available via `python3 -m graphify --version`.
45
+ * Check whether graphify is available.
46
+ *
47
+ * Strategy:
48
+ * 1. Probe PATH for the `graphify` binary (uv tool install puts a shim at
49
+ * ~/.local/bin/graphify — invisible to the isolated python -m lookup).
50
+ * 2. Fall back to `python -m graphify --version` for pip/pipx installs.
51
+ *
41
52
  * Returns true if available, false otherwise.
42
53
  */
43
54
  function checkGraphify(python) {
55
+ // Try 1: check for the graphify binary on PATH (uv tool install shim).
56
+ // On Windows the Python launcher `py` is not on PATH by default, but the
57
+ // graphify shim is placed next to pip/pipx so a plain `where` works.
58
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
59
+ try {
60
+ const whichResult = spawnSync(whichCmd, ['graphify'], {
61
+ encoding: 'utf8',
62
+ timeout: 5000,
63
+ });
64
+ if (whichResult.status === 0 && (whichResult.stdout || '').trim().length > 0) {
65
+ return true;
66
+ }
67
+ } catch {
68
+ // fall through to python -m check
69
+ }
70
+
71
+ // Try 2: check via python -m (catches pip/pipx installs into system python)
44
72
  try {
45
73
  const result = spawnSync(python, ['-m', 'graphify', '--version'], {
46
74
  encoding: 'utf8',
@@ -219,19 +247,33 @@ async function cmdInstall() {
219
247
  return 1;
220
248
  }
221
249
 
222
- console.log(chalk.dim(' Installing graphify via pip...\n'));
250
+ // Prefer uv (handles PEP 668 isolated installs cleanly); fall back to pip.
251
+ const { detectUv } = await import('./utils.mjs');
252
+ const hasUv = await detectUv();
223
253
 
224
- // pip install graphifyy
225
- console.log(chalk.dim(' $ pip install graphifyy'));
226
- let result = spawnSync('pip', ['install', 'graphifyy'], { stdio: 'inherit' });
227
- if (result.status !== 0) {
228
- console.log(chalk.red(' ✗ pip install graphifyy failed.\n'));
229
- return 1;
254
+ if (hasUv) {
255
+ console.log(chalk.dim(' Installing graphify via uv...\n'));
256
+ const result = spawnSync('uv', ['tool', 'install', 'graphifyy'], { stdio: 'inherit' });
257
+ if (result.status !== 0) {
258
+ console.log(chalk.red(' ✗ uv tool install graphifyy failed.\n'));
259
+ return 1;
260
+ }
261
+ } else {
262
+ console.log(chalk.dim(' Installing graphify via pip...\n'));
263
+ // On Windows `pip` may not be on PATH; use `py -m pip` as the fallback.
264
+ const pipCmd = process.platform === 'win32'
265
+ ? [python, '-m', 'pip']
266
+ : ['pip'];
267
+ const result = spawnSync(pipCmd[0], pipCmd.slice(1).concat(['install', 'graphifyy']), { stdio: 'inherit' });
268
+ if (result.status !== 0) {
269
+ console.log(chalk.red(' ✗ pip install graphifyy failed.\n'));
270
+ return 1;
271
+ }
230
272
  }
231
273
 
232
274
  // graphify install --platform opencode --project
233
275
  console.log(chalk.dim('\n $ graphify install --platform opencode --project'));
234
- result = spawnSync('graphify', ['install', '--platform', 'opencode', '--project'], {
276
+ const result = spawnSync('graphify', ['install', '--platform', 'opencode', '--project'], {
235
277
  stdio: 'inherit',
236
278
  });
237
279
  if (result.status !== 0) {
package/cli/init.mjs CHANGED
@@ -154,12 +154,22 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
154
154
  // Soft step: never fails init. If graphify is missing or build errors,
155
155
  // the user can retry manually with `bizar graph build`.
156
156
  console.log(chalk.bold('\n--- Graph ---\n'));
157
- const detectGraphify = spawnSync('python3', ['-c', 'import graphify; print(graphify.__version__)'], {
158
- cwd,
159
- encoding: 'utf8',
160
- timeout: 5000,
161
- });
162
- const graphifyAvailable = detectGraphify.status === 0 && (detectGraphify.stdout || '').trim().length > 0;
157
+
158
+ // Check for the graphify binary on PATH first (uv tool install shim).
159
+ // Falls back to python -m for pip/pipx installs.
160
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
161
+ const whichCheck = spawnSync(whichCmd, ['graphify'], { encoding: 'utf8', timeout: 5000 });
162
+ let graphifyAvailable = whichCheck.status === 0 && (whichCheck.stdout || '').trim().length > 0;
163
+
164
+ if (!graphifyAvailable) {
165
+ const python = process.platform === 'win32' ? 'py' : 'python3';
166
+ const detectGraphify = spawnSync(python, ['-c', 'import graphify; print(graphify.__version__)'], {
167
+ cwd,
168
+ encoding: 'utf8',
169
+ timeout: 5000,
170
+ });
171
+ graphifyAvailable = detectGraphify.status === 0 && (detectGraphify.stdout || '').trim().length > 0;
172
+ }
163
173
 
164
174
  if (!graphifyAvailable) {
165
175
  console.log(chalk.yellow(' graphify not detected — skipping project graph build.'));
package/cli/install.mjs CHANGED
@@ -395,8 +395,12 @@ async function promptGraphifyInstall() {
395
395
  console.log();
396
396
  sectionHeading('Knowledge Graph (graphify)');
397
397
 
398
- // 1. Detect graphify already installed
399
- const detect = spawnSync('python3', ['-c', 'import graphify; print(graphify.__version__)'], {
398
+ // 1. Detect graphify already installed. Windows users typically have
399
+ // `py` on PATH (the Python launcher) rather than `python3`, so probe
400
+ // the right binary per platform. A failed probe just means we will
401
+ // prompt to install below.
402
+ const pythonBin = process.platform === 'win32' ? 'py' : 'python3';
403
+ const detect = spawnSync(pythonBin, ['-c', 'import graphify; print(graphify.__version__)'], {
400
404
  cwd: process.cwd(),
401
405
  encoding: 'utf8',
402
406
  timeout: 5000,
@@ -408,6 +412,26 @@ async function promptGraphifyInstall() {
408
412
  return;
409
413
  }
410
414
 
415
+ // Install graphify via pip as a fallback when uv is unavailable or the
416
+ // user declined the uv-based install. On Windows, `pip` is often not
417
+ // on PATH, so we invoke it as a `py -m pip` module instead. Returns
418
+ // the spawn result so the caller can log success/failure.
419
+ const installGraphifyWithPip = () => {
420
+ if (process.platform === 'win32') {
421
+ return spawnSync('py', ['-m', 'pip', 'install', 'graphifyy'], {
422
+ stdio: 'inherit',
423
+ timeout: 120000,
424
+ });
425
+ }
426
+ return spawnSync('pip', ['install', 'graphifyy'], {
427
+ stdio: 'inherit',
428
+ timeout: 120000,
429
+ });
430
+ };
431
+ const pipManualHint = process.platform === 'win32'
432
+ ? 'py -m pip install graphifyy'
433
+ : 'pip install graphifyy';
434
+
411
435
  // 2. Non-interactive: print hint and exit
412
436
  if (!stdin.isTTY || !stdout.isTTY) {
413
437
  console.log(chalk.dim(' graphify not detected. Install later with:'));
@@ -469,7 +493,12 @@ async function promptGraphifyInstall() {
469
493
 
470
494
  console.log(' Installing uv...');
471
495
  try {
472
- execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: 'inherit', timeout: 60000 });
496
+ if (process.platform === 'win32') {
497
+ // Windows: use the official PowerShell installer
498
+ execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: 'inherit', timeout: 60000 });
499
+ } else {
500
+ execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: 'inherit', timeout: 60000 });
501
+ }
473
502
  } catch (err) {
474
503
  console.log(chalk.red(` uv install failed: ${err.message}`));
475
504
  console.log(chalk.dim(' Install manually from https://docs.astral.sh/uv'));
@@ -480,12 +509,12 @@ async function promptGraphifyInstall() {
480
509
  rl2.close();
481
510
  if (pipAnswer === '' || pipAnswer.startsWith('y')) {
482
511
  console.log(' Installing graphify via pip...');
483
- const result = spawnSync('pip', ['install', 'graphifyy'], { stdio: 'inherit', timeout: 120000 });
512
+ const result = installGraphifyWithPip();
484
513
  if (result.status === 0) {
485
514
  console.log(chalk.green(' graphify installed. Run `bizar graph build` to populate .bizar/graph/.'));
486
515
  } else {
487
516
  console.log(chalk.red(` pip install failed (exit ${result.status}).`));
488
- console.log(chalk.dim(' Install manually: pip install graphifyy'));
517
+ console.log(chalk.dim(` Install manually: ${pipManualHint}`));
489
518
  }
490
519
  } else {
491
520
  console.log(chalk.dim(' Skipped.'));
@@ -513,12 +542,12 @@ async function promptGraphifyInstall() {
513
542
  rl2.close();
514
543
  if (pipAnswer === '' || pipAnswer.startsWith('y')) {
515
544
  console.log(' Installing graphify via pip...');
516
- const result = spawnSync('pip', ['install', 'graphifyy'], { stdio: 'inherit', timeout: 120000 });
545
+ const result = installGraphifyWithPip();
517
546
  if (result.status === 0) {
518
547
  console.log(chalk.green(' graphify installed. Run `bizar graph build` to populate .bizar/graph/.'));
519
548
  } else {
520
549
  console.log(chalk.red(` pip install failed (exit ${result.status}).`));
521
- console.log(chalk.dim(' Install manually: pip install graphifyy'));
550
+ console.log(chalk.dim(` Install manually: ${pipManualHint}`));
522
551
  }
523
552
  } else {
524
553
  console.log(chalk.dim(' Skipped.'));
@@ -578,12 +607,18 @@ export async function runPostInstall() {
578
607
  if (!rtkPresent) {
579
608
  console.log('BizarHarness: installing RTK (token optimizer)...');
580
609
  try {
581
- execSync(
582
- 'curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh',
583
- { stdio: 'pipe', timeout: 60000 },
584
- );
585
- execSync('rtk init -g --opencode', { stdio: 'pipe' });
586
- console.log('BizarHarness: RTK installed and configured.');
610
+ if (process.platform === 'win32') {
611
+ // RTK ships a bash-only installer; on Windows the user installs it
612
+ // manually (e.g. `cargo install --git https://github.com/rtk-ai/rtk`).
613
+ console.log('BizarHarness: RTK bash installer does not run on Windows. Install manually from https://github.com/rtk-ai/rtk');
614
+ } else {
615
+ execSync(
616
+ 'curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh',
617
+ { stdio: 'pipe', timeout: 60000 },
618
+ );
619
+ execSync('rtk init -g --opencode', { stdio: 'pipe' });
620
+ console.log('BizarHarness: RTK installed and configured.');
621
+ }
587
622
  } catch {
588
623
  console.log('BizarHarness: RTK install failed. Install manually from https://github.com/rtk-ai/rtk');
589
624
  }
@@ -601,13 +636,21 @@ export async function runPostInstall() {
601
636
  if (!semblePresent) {
602
637
  console.log('BizarHarness: installing Semble (code search)...');
603
638
  try {
604
- const hasUv = await detectUv();
605
- if (!hasUv) {
639
+ const hasUv = await detectUv();
640
+ if (!hasUv) {
641
+ if (process.platform === 'win32') {
642
+ // Windows: use the official PowerShell installer
643
+ execSync(
644
+ 'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"',
645
+ { stdio: 'pipe', timeout: 60000 },
646
+ );
647
+ } else {
606
648
  execSync(
607
649
  'curl -LsSf https://astral.sh/uv/install.sh | sh',
608
650
  { stdio: 'pipe', timeout: 60000 },
609
651
  );
610
652
  }
653
+ }
611
654
  execSync('uv tool install "semble[mcp]"', { stdio: 'pipe', timeout: 60000 });
612
655
  console.log('BizarHarness: Semble installed.');
613
656
  } catch {
package/cli/update.mjs CHANGED
@@ -121,23 +121,30 @@ export function readLivePid(pidFile) {
121
121
  * We treat any successful signal delivery as success; if SIGKILL is
122
122
  * sent, the original process is certainly dead (uncatchable).
123
123
  *
124
+ * Cross-platform note: Node.js 14+ maps `process.kill(pid)` without an
125
+ * explicit signal to the platform-appropriate default (`SIGTERM` on
126
+ * POSIX, `TerminateProcess` on Windows). We drop the signal argument so
127
+ * the same code works on both platforms. For the forced-kill phase on
128
+ * Windows we use `taskkill /F /PID <pid>` because `SIGKILL` is not
129
+ * delivered the same way there.
130
+ *
124
131
  * Exported for testability.
125
132
  */
126
- export function killAndWait(pid, { timeoutMs = 5000, label = 'process' } = {}) {
133
+ export async function killAndWait(pid, { timeoutMs = 5000, label = 'process' } = {}) {
127
134
  if (!pid) return true;
128
135
 
129
- // Phase 1: graceful SIGTERM
136
+ // Phase 1: graceful SIGTERM (or platform default on Windows)
130
137
  let sigtermOk = false;
131
138
  try {
132
- process.kill(pid, 'SIGTERM');
139
+ process.kill(pid);
133
140
  sigtermOk = true;
134
141
  } catch (err) {
135
142
  if (err.code === 'ESRCH') return true; // already dead
136
- console.log(chalk.yellow(` ! could not SIGTERM ${label} (pid ${pid}): ${err.message}`));
143
+ console.log(chalk.yellow(` ! could not signal ${label} (pid ${pid}): ${err.message}`));
137
144
  return false;
138
145
  }
139
146
 
140
- // Best-effort poll for graceful exit. Note: a SIGTERM-handling process
147
+ // Best-effort poll for graceful exit. Note: a signal-handling process
141
148
  // (Express dashboard, Node sleeper) usually exits within ~100ms. We
142
149
  // poll for up to `timeoutMs`; if anything responds to kill -0 it may be
143
150
  // a recycled PID, so we don't treat that as "still our process".
@@ -149,7 +156,7 @@ export function killAndWait(pid, { timeoutMs = 5000, label = 'process' } = {}) {
149
156
  } catch (err) {
150
157
  if (err.code === 'ESRCH') { sawExit = true; break; }
151
158
  }
152
- spawnSync('sleep', ['0.1']);
159
+ await new Promise((resolve) => setTimeout(resolve, 100));
153
160
  }
154
161
 
155
162
  if (sawExit) {
@@ -157,20 +164,26 @@ export function killAndWait(pid, { timeoutMs = 5000, label = 'process' } = {}) {
157
164
  return true;
158
165
  }
159
166
 
160
- // Phase 2: escalate to SIGKILL. Even if `kill -0` still succeeds (PID
161
- // recycled or process truly stuck), SIGKILL is uncatchable and the
162
- // original process — if it was still our PID — is now dead.
167
+ // Phase 2: escalate to forced kill. Even if `kill -0` still succeeds
168
+ // (PID recycled or process truly stuck), SIGKILL is uncatchable and
169
+ // the original process — if it was still our PID — is now dead.
170
+ // On Windows, use `taskkill /F` because POSIX SIGKILL semantics don't
171
+ // exist there.
163
172
  try {
164
- process.kill(pid, 'SIGKILL');
173
+ if (process.platform === 'win32') {
174
+ spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore' });
175
+ } else {
176
+ process.kill(pid, 'SIGKILL');
177
+ }
165
178
  if (sigtermOk) {
166
- console.log(chalk.yellow(` ! ${label} (pid ${pid}) did not exit gracefully; sent SIGKILL`));
179
+ console.log(chalk.yellow(` ! ${label} (pid ${pid}) did not exit gracefully; sent forced kill`));
167
180
  }
168
181
  // Brief settle for the kernel.
169
- spawnSync('sleep', ['0.2']);
182
+ await new Promise((resolve) => setTimeout(resolve, 200));
170
183
  return true;
171
184
  } catch (err) {
172
185
  if (err.code === 'ESRCH') return true;
173
- console.log(chalk.red(` ✗ could not SIGKILL ${label} (pid ${pid}): ${err.message}`));
186
+ console.log(chalk.red(` ✗ could not force-kill ${label} (pid ${pid}): ${err.message}`));
174
187
  return false;
175
188
  }
176
189
  }
@@ -272,9 +285,16 @@ function rerunInstallScript() {
272
285
  return { ok: false, message: 'setup rerun failed' };
273
286
  }
274
287
  const installSh = join(pkgRoot, 'install.sh');
275
- if (process.platform === 'win32' || !existsSync(installSh)) {
288
+ if (!existsSync(installSh)) {
276
289
  return { ok: false, message: 'could not locate a compatible setup script to re-run' };
277
290
  }
291
+ if (process.platform === 'win32') {
292
+ // On Windows, the bash install path doesn't apply. The plugin is
293
+ // already installed globally via the npm install
294
+ // (see cli/install.mjs:installPluginFromGlobal).
295
+ console.log(chalk.dim(' Skipping install.sh (Windows uses npm-based plugin install)'));
296
+ return { ok: true, message: 'install.sh skipped on Windows' };
297
+ }
278
298
  console.log(chalk.dim(`\n Re-running install script at ${installSh}...`));
279
299
  const r = spawnSync('bash', [installSh], { stdio: 'inherit' });
280
300
  if (r.status !== 0) {
@@ -349,17 +369,17 @@ async function confirmKill(instances, { assumeYes } = {}) {
349
369
  }
350
370
  }
351
371
 
352
- function killInstances(instances) {
372
+ async function killInstances(instances) {
353
373
  const out = [];
354
374
  if (instances.service) {
355
- const ok = killAndWait(instances.service.pid, { label: 'bizar service' });
375
+ const ok = await killAndWait(instances.service.pid, { label: 'bizar service' });
356
376
  out.push({ name: 'service', ok });
357
377
  if (ok) {
358
378
  try { rmSync(SERVICE_PID_FILE, { force: true }); } catch { /* ignore */ }
359
379
  }
360
380
  }
361
381
  if (instances.dashboard) {
362
- const ok = killAndWait(instances.dashboard.pid, { label: 'bizar-dash' });
382
+ const ok = await killAndWait(instances.dashboard.pid, { label: 'bizar-dash' });
363
383
  out.push({ name: 'dashboard', ok });
364
384
  if (ok) {
365
385
  try { rmSync(DASHBOARD_PID_FILE, { force: true }); } catch { /* ignore */ }
@@ -428,7 +448,7 @@ async function promptForUpdates(forceAll) {
428
448
  choices: [
429
449
  { name: 'opencode (the opencode CLI itself)', value: 'opencode', checked: true },
430
450
  { name: `bizar (${PKG_MAIN})`, value: 'bizar', checked: true },
431
- { name: `bizar-dash (${PKG_DASH}) — web dashboard`, value: 'dash', checked: true },
451
+ { name: `bizar dash (${PKG_DASH}) — web dashboard`, value: 'dash', checked: true },
432
452
  { name: `plugin (${PKG_PLUGIN})`, value: 'plugin', checked: true },
433
453
  ],
434
454
  },
@@ -460,14 +480,14 @@ export async function runUpdate(subargs = []) {
460
480
  process.exit(1);
461
481
  }
462
482
  console.log(chalk.cyan('\n Stopping running instances...'));
463
- const kills = killInstances(instances);
483
+ const kills = await killInstances(instances);
464
484
  for (const k of kills) {
465
485
  const marker = k.ok ? chalk.green('✓') : chalk.red('✗');
466
486
  console.log(` ${marker} ${k.name} stopped`);
467
487
  }
468
488
  // Give the kernel a moment to release any open file handles on the
469
489
  // npm-global directory before npm tries to replace files.
470
- spawnSync('sleep', ['0.5']);
490
+ await new Promise((resolve) => setTimeout(resolve, 500));
471
491
  } else {
472
492
  console.log(chalk.dim(' No running Bizar instances detected.'));
473
493
  }
@@ -565,7 +585,7 @@ export async function runUpdate(subargs = []) {
565
585
  console.log(chalk.green(` ✓ ${res.message}`));
566
586
  } else {
567
587
  console.log(chalk.yellow(` ⚠ ${res.message}`));
568
- console.log(chalk.dim(' Start it manually with `bizar-dash start --bg`.'));
588
+ console.log(chalk.dim(' Start it manually with `bizar dash start --bg`.'));
569
589
  }
570
590
  }
571
591
 
package/config/AGENTS.md CHANGED
@@ -334,7 +334,7 @@ This section is the single source of truth for every Bizar agent's behavior. It
334
334
  > | `bash_tool` | `bash` |
335
335
  > | `web_search` | `websearch` (opencode built-in) |
336
336
  > | `web_fetch` | `webfetch` (opencode built-in) |
337
- > | `present_files` | not applicable — Bizar delivers files via the dashboard (`bizar-dash/src/server/routes/artifacts.mjs`) or by writing to the workspace |
337
+ > | `present_files` | not applicable — Bizar delivers files via the dashboard (`@polderlabs/bizar-dash/src/server/routes/artifacts.mjs`) or by writing to the workspace |
338
338
  > | `image_search` / `places_*` / `weather_fetch` / `recipe_display_v0` / `fetch_sports_data` / `message_compose_v1` / `recommend_claude_apps` | not available in Bizar — do not assume these exist |
339
339
  > | `search_mcp_registry` / `suggest_connectors` | use the `skills` CLI (`skills add <owner/repo> -s <name>`) to discover and install skills instead |
340
340
  > | `ask_user_input_v0` | Bizar has a `question` tool — same shape, single high-value question |
@@ -13,6 +13,6 @@ If the user invoked `/bizar` with arguments, treat them as a request and route a
13
13
  - "dashboard" or "open dashboard" → `/bizar` (no args, will launch the dashboard)
14
14
  - Otherwise: ask one clarifying question
15
15
 
16
- If the user invoked `/bizar` with no arguments, the dashboard is launching in the background. Visit `http://localhost:<port>/` to access it. The plugin's `chat.message` hook spawns `bizar dashboard start` as a detached child process and surfaces the live URL in its response.
16
+ If the user invoked `/bizar` with no arguments, the dashboard is launching in the background. Visit `http://localhost:<port>/` to access it. The plugin's `chat.message` hook spawns `bizar dash start` as a detached child process and surfaces the live URL in its response.
17
17
 
18
- Common ports: 4321 is preferred; if it's taken, the launcher walks upward and picks the next free port. The PID and port are recorded under `~/.config/bizar/dashboard.{pid,port}` so `bizar dashboard stop` and `bizar dashboard status` can find the running instance.
18
+ Common ports: 4321 is preferred; if it's taken, the launcher walks upward and picks the next free port. The PID and port are recorded under `~/.config/bizar/dashboard.{pid,port}` so `bizar dash stop` and `bizar dash status` can find the running instance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "3.9.1",
3
+ "version": "3.11.0",
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.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  "ora": "^8.1.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@polderlabs/bizar-dash": ">=3.0.0"
36
+ "@polderlabs/bizar-dash": "^3.10.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@polderlabs/bizar-dash": {