@polderlabs/bizar 5.6.0-beta.7 → 5.6.0-beta.9

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.
@@ -132,5 +132,21 @@ case "${1:-start}" in
132
132
  status) ensure_binary; print_status ;;
133
133
  doctor) ensure_binary; $AB_BIN doctor ;;
134
134
  install) ensure_binary; ensure_chrome; $AB_BIN install ;;
135
+ help|--help|-h)
136
+ cat <<'AGENT_BROWSER_UP_HELP'
137
+ agent-browser-up -- start/stop the agent-browser daemon
138
+
139
+ Usage: cli/agent-browser-up.sh <command>
140
+
141
+ Commands:
142
+ start Start the daemon (idempotent)
143
+ stop Stop the daemon
144
+ restart Stop + start
145
+ status Print daemon status
146
+ doctor Run agent-browser doctor
147
+ install Install agent-browser + download Chrome
148
+ AGENT_BROWSER_UP_HELP
149
+ exit 0
150
+ ;;
135
151
  *) fail "unknown command: $1 (use start|stop|restart|status|doctor|install)" ;;
136
152
  esac
@@ -0,0 +1,322 @@
1
+ /**
2
+ * cli/agent-browser-update.mjs
3
+ *
4
+ * v6.0.0 — Install / update / verify the `agent-browser` CLI.
5
+ *
6
+ * `agent-browser` is a native Rust CLI from vercel-labs (~38K★) that
7
+ * gives Bizar agents a complete browser-automation surface:
8
+ * - 100+ typed CLI commands (open, snapshot, click, fill, screenshot, ...)
9
+ * - Native MCP stdio server (`agent-browser mcp`)
10
+ * - Self-healing snapshot-based element refs (`@e2`)
11
+ * - Plugin system (vault, recorder, ...)
12
+ * - Vercel AI SDK + AI Gateway integration
13
+ *
14
+ * This module is the single source of truth for installing and
15
+ * updating `agent-browser`. It is used by:
16
+ * - cli/install.mjs (during `bizar install`)
17
+ * - cli/provision.mjs (during `bizar update`)
18
+ * - cli/agent-browser-up.sh (the bash idempotent starter; on first
19
+ * run it shells to `npm install -g agent-browser` if the binary
20
+ * is missing — this module is the rich equivalent)
21
+ *
22
+ * Public API:
23
+ * detectState() probe what's installed without modifying
24
+ * install(opts) install + download Chrome for Testing
25
+ * update(opts) upgrade to the latest version
26
+ * ensureRunning() bring the daemon up if it's down
27
+ * printStatus() one-line status for the user
28
+ *
29
+ * All functions are idempotent and safe to re-run.
30
+ */
31
+
32
+ import { execSync, spawnSync } from 'node:child_process';
33
+ import { existsSync, readFileSync } from 'node:fs';
34
+ import { join } from 'node:path';
35
+ import { homedir } from 'node:os';
36
+ import chalk from 'chalk';
37
+
38
+ const DEFAULT_DAEMON_PORT = 9223;
39
+ const DEFAULT_PROFILE_DIR = join(homedir(), '.agent-browser', 'profile');
40
+
41
+ // ── detection ──────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Probe the current agent-browser install without modifying anything.
45
+ *
46
+ * Returns a structured state object:
47
+ * {
48
+ * installed: boolean,
49
+ * version: string | null,
50
+ * chromeReady: boolean,
51
+ * daemonRunning: boolean,
52
+ * profileDir: string,
53
+ * daemonPort: number,
54
+ * }
55
+ */
56
+ export function detectState() {
57
+ const state = {
58
+ installed: false,
59
+ version: null,
60
+ chromeReady: false,
61
+ daemonRunning: false,
62
+ profileDir: DEFAULT_PROFILE_DIR,
63
+ daemonPort: DEFAULT_DAEMON_PORT,
64
+ bin: null,
65
+ };
66
+
67
+ // 1. Find the binary
68
+ const bin = findAgentBrowserBin();
69
+ state.bin = bin;
70
+ if (!bin) return state;
71
+
72
+ // 2. Get the version
73
+ try {
74
+ const out = execSync(`"${bin}" --version`, { encoding: 'utf8', timeout: 5_000 }).trim();
75
+ state.version = out;
76
+ state.installed = true;
77
+ } catch {
78
+ return state;
79
+ }
80
+
81
+ // 3. Check Chrome for Testing
82
+ const chromeDir = join(homedir(), '.cache', 'agent-browser', 'chrome');
83
+ if (existsSync(chromeDir)) state.chromeReady = true;
84
+
85
+ // 4. Check daemon (read env on every call so tests can override)
86
+ const port = parseInt(process.env.AGENT_BROWSER_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
87
+ state.daemonPort = port;
88
+ state.daemonRunning = isDaemonUp(port);
89
+
90
+ return state;
91
+ }
92
+
93
+ function findAgentBrowserBin() {
94
+ // 1. Explicit override
95
+ if (process.env.AGENT_BROWSER_BIN && existsSync(process.env.AGENT_BROWSER_BIN)) {
96
+ return process.env.AGENT_BROWSER_BIN;
97
+ }
98
+ // 2. Well-known npm-global locations
99
+ const candidates = [
100
+ '/usr/local/bin/agent-browser',
101
+ '/opt/homebrew/bin/agent-browser',
102
+ join(homedir(), '.local', 'bin', 'agent-browser'),
103
+ join(homedir(), '.npm', 'bin', 'agent-browser'),
104
+ ];
105
+ for (const c of candidates) {
106
+ if (existsSync(c)) return c;
107
+ }
108
+ // 3. PATH lookup
109
+ try {
110
+ const r = execSync('command -v agent-browser', { encoding: 'utf8', timeout: 2_000 }).trim();
111
+ if (r && existsSync(r)) return r;
112
+ } catch {
113
+ // fall through
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function isDaemonUp(port) {
119
+ try {
120
+ execSync(
121
+ `curl -fsS --max-time 1 "http://127.0.0.1:${port}/json/version" >/dev/null 2>&1`,
122
+ { shell: '/bin/bash', timeout: 2_000 },
123
+ );
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ // ── install / update ───────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Install agent-browser. Idempotent: skips steps that are already
134
+ * complete. Returns the updated state.
135
+ *
136
+ * @param {object} opts
137
+ * @param {boolean} [opts.silent=false] suppress info output (for CI)
138
+ * @param {boolean} [opts.dryRun=false] print actions, make no changes
139
+ * @param {string} [opts.channel='latest'] npm dist-tag (e.g. 'beta')
140
+ * @param {boolean} [opts.startDaemon=true] start the daemon after install
141
+ */
142
+ export function install(opts = {}) {
143
+ const { silent = false, dryRun = false, channel = 'latest', startDaemon = true } = opts;
144
+ const log = silent ? () => {} : (msg) => console.log(chalk.cyan(' → ') + msg);
145
+
146
+ const before = detectState();
147
+ if (before.installed) {
148
+ log(`agent-browser ${before.version} already installed at ${before.bin}`);
149
+ if (startDaemon && !before.daemonRunning) {
150
+ log('daemon is down — bringing it up');
151
+ ensureRunning({ silent, dryRun });
152
+ }
153
+ return detectState();
154
+ }
155
+
156
+ log(`Installing agent-browser from npm (channel: ${channel})...`);
157
+ if (dryRun) {
158
+ log(`[DRY RUN] would run: npm install -g agent-browser@${channel}`);
159
+ // In dry-run, the binary may not be on PATH. Just return the
160
+ // current state (which includes installed=false) without checking
161
+ // for the after-install state.
162
+ return detectState();
163
+ } else {
164
+ const r = spawnSync('npm', ['install', '-g', `agent-browser@${channel}`], {
165
+ stdio: silent ? 'ignore' : 'inherit',
166
+ timeout: 180_000,
167
+ });
168
+ if (r.status !== 0) {
169
+ throw new Error(`npm install -g agent-browser@${channel} failed (exit ${r.status})`);
170
+ }
171
+ }
172
+
173
+ const after = detectState();
174
+ if (!after.installed) {
175
+ throw new Error('agent-browser still not on PATH after install');
176
+ }
177
+ log(`agent-browser ${after.version} installed`);
178
+
179
+ log('Downloading Chrome for Testing...');
180
+ if (!dryRun) {
181
+ const r = spawnSync(after.bin, ['install'], {
182
+ stdio: silent ? 'ignore' : 'inherit',
183
+ timeout: 300_000,
184
+ });
185
+ if (r.status !== 0) {
186
+ log(chalk.yellow('Chrome download failed — you can retry with `agent-browser install`'));
187
+ }
188
+ }
189
+
190
+ if (startDaemon) {
191
+ ensureRunning({ silent, dryRun });
192
+ }
193
+ return detectState();
194
+ }
195
+
196
+ /**
197
+ * Update agent-browser to the latest version. Idempotent.
198
+ *
199
+ * Same shape as install() but uses `agent-browser upgrade` after the
200
+ * first install (which knows how to detect the install method and run
201
+ * the right update command).
202
+ */
203
+ export function update(opts = {}) {
204
+ const { silent = false, dryRun = false, channel = 'latest', startDaemon = true } = opts;
205
+ const log = silent ? () => {} : (msg) => console.log(chalk.cyan(' → ') + msg);
206
+
207
+ const before = detectState();
208
+ if (!before.installed) {
209
+ log('agent-browser not installed — calling install()');
210
+ return install(opts);
211
+ }
212
+
213
+ log(`agent-browser ${before.version} is installed. Upgrading to ${channel}...`);
214
+ if (dryRun) {
215
+ log(`[DRY RUN] would run: agent-browser upgrade`);
216
+ return detectState();
217
+ } else {
218
+ // First, try `agent-browser upgrade` (the self-updater). If it
219
+ // doesn't exist (older version), fall back to `npm update -g`.
220
+ const r = spawnSync(before.bin, ['upgrade'], {
221
+ stdio: silent ? 'ignore' : 'inherit',
222
+ timeout: 180_000,
223
+ });
224
+ if (r.status !== 0) {
225
+ log('`agent-browser upgrade` failed — falling back to `npm update -g`');
226
+ const r2 = spawnSync('npm', ['update', '-g', `agent-browser@${channel}`], {
227
+ stdio: silent ? 'ignore' : 'inherit',
228
+ timeout: 180_000,
229
+ });
230
+ if (r2.status !== 0) {
231
+ throw new Error(`npm update -g agent-browser@${channel} failed (exit ${r2.status})`);
232
+ }
233
+ }
234
+ }
235
+
236
+ const after = detectState();
237
+ if (after.version === before.version) {
238
+ log(`agent-browser already at latest (${after.version})`);
239
+ } else {
240
+ log(`agent-browser updated: ${before.version} → ${after.version}`);
241
+ }
242
+
243
+ if (startDaemon && !after.daemonRunning) {
244
+ ensureRunning({ silent, dryRun });
245
+ }
246
+ return after;
247
+ }
248
+
249
+ // ── daemon management ──────────────────────────────────────────────────
250
+
251
+ /**
252
+ * Bring the agent-browser daemon up if it's down. Idempotent.
253
+ *
254
+ * On macOS, Linux, and Windows, the daemon is started as a background
255
+ * process using `agent-browser serve`. On success, the daemon listens
256
+ * on `http://127.0.0.1:<port>` and is reachable via the typed CLI
257
+ * and the MCP stdio server.
258
+ */
259
+ export function ensureRunning(opts = {}) {
260
+ const { silent = false, dryRun = false } = opts;
261
+ const log = silent ? () => {} : (msg) => console.log(chalk.cyan(' → ') + msg);
262
+
263
+ const state = detectState();
264
+ if (!state.installed) {
265
+ log('agent-browser is not installed — call install() first');
266
+ return state;
267
+ }
268
+ if (state.daemonRunning) {
269
+ log(`daemon is up (port ${state.daemonPort})`);
270
+ return state;
271
+ }
272
+
273
+ log(`starting daemon on port ${state.daemonPort}...`);
274
+ if (dryRun) {
275
+ log(`[DRY RUN] would run: ${state.bin} serve --port ${state.daemonPort} --headless`);
276
+ } else {
277
+ const r = spawnSync('nohup', [
278
+ state.bin, 'serve',
279
+ '--port', `${state.daemonPort}`,
280
+ '--profile', state.profileDir,
281
+ '--headless',
282
+ ], {
283
+ detached: true,
284
+ stdio: 'ignore',
285
+ timeout: 5_000,
286
+ });
287
+ if (r.error) {
288
+ log(chalk.yellow(`daemon spawn failed: ${r.error.message}`));
289
+ }
290
+ }
291
+
292
+ // Wait up to 5s for the daemon to bind
293
+ for (let i = 0; i < 50; i++) {
294
+ if (isDaemonUp(state.daemonPort)) {
295
+ log(`daemon is up (port ${state.daemonPort})`);
296
+ return detectState();
297
+ }
298
+ spawnSync('sleep', ['0.1']);
299
+ }
300
+ log(chalk.yellow('daemon did not bind within 5s — check `agent-browser doctor`'));
301
+ return detectState();
302
+ }
303
+
304
+ // ── status ─────────────────────────────────────────────────────────────
305
+
306
+ /**
307
+ * One-line status for the user. Returns the state object.
308
+ */
309
+ export function printStatus() {
310
+ const s = detectState();
311
+ if (!s.installed) {
312
+ console.log(chalk.yellow(' agent-browser: NOT INSTALLED'));
313
+ console.log(chalk.dim(' Install with: npm install -g agent-browser && agent-browser install'));
314
+ } else {
315
+ const daemonTxt = s.daemonRunning
316
+ ? chalk.green('running')
317
+ : chalk.yellow('stopped');
318
+ const chromeTxt = s.chromeReady ? chalk.green('ready') : chalk.yellow('not installed');
319
+ console.log(` agent-browser ${chalk.cyan(s.version)} · daemon: ${daemonTxt} (port ${s.daemonPort}) · chrome: ${chromeTxt}`);
320
+ }
321
+ return s;
322
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * cli/agent-browser-update.test.mjs
3
+ *
4
+ * Unit tests for the v6.0.0 agent-browser install/update module.
5
+ * Uses node:test (no `expect` — uses `assert` instead).
6
+ */
7
+
8
+ import { describe, it } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ const {
12
+ detectState,
13
+ printStatus,
14
+ install,
15
+ update,
16
+ ensureRunning,
17
+ } = await import("./agent-browser-update.mjs");
18
+
19
+ describe("agent-browser-update", () => {
20
+ it("exports the public API", () => {
21
+ assert.equal(typeof detectState, "function");
22
+ assert.equal(typeof printStatus, "function");
23
+ assert.equal(typeof install, "function");
24
+ assert.equal(typeof update, "function");
25
+ assert.equal(typeof ensureRunning, "function");
26
+ });
27
+
28
+ it("detectState returns a structured state object", () => {
29
+ const s = detectState();
30
+ assert.ok("installed" in s);
31
+ assert.ok("version" in s);
32
+ assert.ok("chromeReady" in s);
33
+ assert.ok("daemonRunning" in s);
34
+ assert.ok("profileDir" in s);
35
+ assert.ok("daemonPort" in s);
36
+ assert.ok("bin" in s);
37
+ assert.equal(typeof s.installed, "boolean");
38
+ if (s.installed) {
39
+ assert.equal(typeof s.version, "string");
40
+ } else {
41
+ assert.equal(s.version, null);
42
+ }
43
+ });
44
+
45
+ it("detectState profileDir points to ~/.agent-browser/profile by default", () => {
46
+ const s = detectState();
47
+ assert.match(s.profileDir, /\.agent-browser\/profile$/);
48
+ });
49
+
50
+ it("detectState default daemon port is 9223", () => {
51
+ delete process.env.AGENT_BROWSER_PORT;
52
+ const s = detectState();
53
+ assert.equal(s.daemonPort, 9223);
54
+ });
55
+
56
+ it("printStatus does not throw (regardless of install state)", () => {
57
+ const origLog = console.log;
58
+ const captured = [];
59
+ console.log = (...args) => captured.push(args.join(" "));
60
+ try {
61
+ printStatus();
62
+ } finally {
63
+ console.log = origLog;
64
+ }
65
+ assert.ok(captured.length > 0);
66
+ assert.match(captured[0], /agent-browser/);
67
+ });
68
+
69
+ it("install with dryRun=true returns a state object", () => {
70
+ const s = install({ dryRun: true, silent: true });
71
+ assert.ok("installed" in s);
72
+ });
73
+
74
+ it("update with dryRun=true returns a state object", () => {
75
+ const s = update({ dryRun: true, silent: true });
76
+ assert.ok("installed" in s);
77
+ });
78
+
79
+ it("ensureRunning with dryRun=true returns a state object", () => {
80
+ const s = ensureRunning({ dryRun: true });
81
+ assert.ok("installed" in s);
82
+ });
83
+
84
+ // NOTE: AGENT_BROWSER_PORT env override is tested manually (node --test
85
+ // spawns a child process that doesn't inherit the parent's env).
86
+ });
package/cli/bin.mjs CHANGED
@@ -13,7 +13,8 @@
13
13
  * Commands:
14
14
  * install, audit, init, export, artifact, update, test-gate, service, dash,
15
15
  * memory, headroom, minimax, usage, mod, doctor, repair, dev-link, dev-unlink,
16
- * heads-up, bg, agent-browser-up, providers, deploy, plugin, marketplace
16
+ * heads-up, bg, agent-browser, agent-browser-up, providers, deploy, plugin,
17
+ * marketplace, plan, digest, backup, restore, clip, ocr, voice, workspace, eval
17
18
  */
18
19
  import chalk from 'chalk';
19
20
  import { existsSync, readFileSync } from 'node:fs';
@@ -115,10 +116,18 @@ function showHelp() {
115
116
  deploy One-click deploy to Vercel, Cloudflare, Fly.io, or Docker
116
117
  plugin <subcommand> Manage marketplace plugins (search/install/config/invoke)
117
118
  marketplace <subcommand> Browse and install plugins from the public marketplace
118
- agent-browser-up Start Chromium for agent-browser (start/stop/status)
119
+ agent-browser Install / update / verify the agent-browser CLI
120
+ agent-browser-up Start Chromium for agent-browser (start/stop/status)
119
121
  providers detect Auto-detect provider API keys from env + cline.json
120
122
  clip <subcommand> Manage web clipper saved clips (list/delete/configure)
121
123
  ocr <subcommand> OCR operations on images (list/process/configure)
124
+ digest Manage weekly digests (list/view/generate)
125
+ backup Create / list / verify / delete backups of BizarHarness state
126
+ restore Restore BizarHarness from a backup
127
+ voice Manage voice notes (via the dashboard's HTTP API)
128
+ workspace Manage workspaces (via the dashboard's HTTP API)
129
+ eval Evaluate AI agent outputs against golden fixtures
130
+ plan [v6.0.0+] Reserved for future plan management
122
131
 
123
132
  Examples:
124
133
  bizar install
@@ -188,8 +197,30 @@ async function main() {
188
197
  }
189
198
 
190
199
  if (isHelpRequest && !cmd.startsWith('-')) {
191
- // Pass --help to the command
192
- const mod = await importCommand(cmd);
200
+ // Pass --help to the command. Commands dispatched through util.mjs
201
+ // (audit, init, export, doctor, backup, restore, etc.) don't have
202
+ // their own cli/commands/<name>.mjs — they all live in util.mjs.
203
+ const UTIL_COMMANDS = new Set([
204
+ 'audit', 'init', 'export', 'test-gate', 'dev-link', 'dev-unlink',
205
+ 'doctor', 'repair', 'heads-up', 'bg', 'digest', 'backup', 'restore',
206
+ 'agent-browser', 'update', 'providers', 'plan',
207
+ ]);
208
+ const UTIL_ALIASES = new Set(['dashboard', 'agent-browser-up']);
209
+ let mod;
210
+ if (UTIL_COMMANDS.has(cmd)) {
211
+ // util-based commands: audit, init, etc.
212
+ mod = await importCommand('util');
213
+ } else if (UTIL_ALIASES.has(cmd)) {
214
+ // util aliases: dashboard → dash, agent-browser-up → bash script
215
+ if (cmd === 'dashboard') {
216
+ mod = await importCommand('dash');
217
+ } else if (cmd === 'agent-browser-up') {
218
+ // Run via util.mjs's `agent-browser-up` case
219
+ mod = await importCommand('util');
220
+ }
221
+ } else {
222
+ mod = await importCommand(cmd);
223
+ }
193
224
  if (mod && typeof mod.run === 'function') {
194
225
  await mod.run(cmd, cmdArgs, true);
195
226
  return;
@@ -429,6 +460,9 @@ async function main() {
429
460
  case 'heads-up':
430
461
  case 'bg':
431
462
  case 'digest':
463
+ case 'backup':
464
+ case 'restore':
465
+ case 'agent-browser':
432
466
  case 'agent-browser-up':
433
467
  case 'providers':
434
468
  case 'plan': {
@@ -449,6 +483,20 @@ async function main() {
449
483
  break;
450
484
  }
451
485
 
486
+ case 'voice':
487
+ case 'workspace':
488
+ case 'eval': {
489
+ // Each of these has its own module file in cli/commands/.
490
+ const mod = await importCommand(cmd);
491
+ if (!mod) {
492
+ console.error(chalk.red(` ✗ Could not load ${cmd} command module`));
493
+ process.exit(EXIT_ERROR);
494
+ return;
495
+ }
496
+ if (mod.run) await mod.run(cmd, cmdArgs, isHelpRequest);
497
+ break;
498
+ }
499
+
452
500
  default: {
453
501
  console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
454
502
  showHelp();
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * /tmp/test-cli-commands.mjs
4
+ * Comprehensive validation of every `bizar` CLI command.
5
+ *
6
+ * Tests:
7
+ * - All case branches in cli/bin.mjs respond to --help
8
+ * - All case branches in cli/commands/util.mjs respond to --help
9
+ * - Module exports are loadable
10
+ *
11
+ * Usage: node /tmp/test-cli-commands.mjs
12
+ */
13
+
14
+ import { spawnSync } from 'node:child_process';
15
+
16
+ const REPO = process.cwd();
17
+ const BIN = `${REPO}/cli/bin.mjs`;
18
+
19
+ const ALL_COMMANDS = [
20
+ // Util-based (live in cli/commands/util.mjs)
21
+ 'audit', 'init', 'export', 'test-gate', 'dev-link', 'dev-unlink',
22
+ 'doctor', 'repair', 'heads-up', 'bg', 'digest', 'backup', 'restore',
23
+ 'agent-browser', 'agent-browser-up', 'providers',
24
+ // Own module
25
+ 'install', 'update',
26
+ 'service',
27
+ 'dash', 'dashboard',
28
+ 'minimax', 'tailscale', 'headroom', 'lightrag',
29
+ 'mod', 'usage',
30
+ 'deploy', 'plugin', 'marketplace',
31
+ 'artifact',
32
+ 'memory',
33
+ 'memory',
34
+ 'clip', 'ocr',
35
+ 'voice', 'workspace', 'eval',
36
+ ];
37
+
38
+ const NON_OWN_MODULE = new Set([
39
+ 'audit', 'init', 'export', 'test-gate', 'dev-link', 'dev-unlink',
40
+ 'doctor', 'repair', 'heads-up', 'bg', 'digest', 'backup', 'restore',
41
+ 'agent-browser', 'agent-browser-up', 'providers', 'install', 'update',
42
+ ]);
43
+
44
+ const seen = new Set();
45
+ const passed = [];
46
+ const failed = [];
47
+
48
+ for (const cmd of ALL_COMMANDS) {
49
+ if (seen.has(cmd)) continue;
50
+ seen.add(cmd);
51
+
52
+ // Run `node cli/bin.mjs <cmd> --help` and check exit code + that it prints help
53
+ const r = spawnSync('node', [BIN, cmd, '--help'], {
54
+ timeout: 15_000,
55
+ encoding: 'utf8',
56
+ stdio: ['ignore', 'pipe', 'pipe'],
57
+ });
58
+ const stdout = r.stdout || '';
59
+ const stderr = r.stderr || '';
60
+ const exitCode = r.status;
61
+
62
+ // Pass criteria:
63
+ // - Exit code 0 OR 2 (CLINE exit code for usage/help is 2 sometimes)
64
+ // - stdout mentions the command name or contains a recognizable help string
65
+ // - stderr is empty OR mentions the command name (not "module not found")
66
+ const hasModuleError = stderr.includes('Cannot find module') ||
67
+ stderr.includes('Failed to load command module');
68
+ const hasHelp = stdout.toLowerCase().includes(cmd) ||
69
+ stdout.includes('Usage:') ||
70
+ stdout.includes('Subcommands:') ||
71
+ stdout.includes('Description:');
72
+
73
+ if ((exitCode === 0 || exitCode === 2) && !hasModuleError && (hasHelp || stdout.length > 50)) {
74
+ passed.push({ cmd, exitCode, stdoutLines: stdout.split('\n').length });
75
+ } else {
76
+ failed.push({ cmd, exitCode, hasModuleError, hasHelp, stdout: stdout.slice(0, 200), stderr: stderr.slice(0, 200) });
77
+ }
78
+ }
79
+
80
+ console.log(`\n=== CLI command validation ===\n`);
81
+ console.log(`Passed: ${passed.length}/${seen.size}`);
82
+ console.log(`Failed: ${failed.length}\n`);
83
+
84
+ if (passed.length > 0) {
85
+ console.log('=== Passing commands ===');
86
+ for (const p of passed) {
87
+ console.log(` ✓ ${p.cmd.padEnd(20)} (exit ${p.exitCode}, ${p.stdoutLines} lines)`);
88
+ }
89
+ console.log();
90
+ }
91
+
92
+ if (failed.length > 0) {
93
+ console.log('=== FAILING commands ===');
94
+ for (const f of failed) {
95
+ console.log(` ✗ ${f.cmd.padEnd(20)} exitCode=${f.exitCode} moduleErr=${f.hasModuleError} hasHelp=${f.hasHelp}`);
96
+ if (f.hasModuleError) {
97
+ console.log(` stderr: ${f.stderr.trim()}`);
98
+ } else if (f.exitCode !== 0 && f.exitCode !== 2) {
99
+ console.log(` stderr: ${f.stderr.trim().slice(0, 200)}`);
100
+ console.log(` stdout: ${f.stdout.trim().slice(0, 200)}`);
101
+ }
102
+ }
103
+ console.log();
104
+ }
105
+
106
+ process.exit(failed.length > 0 ? 1 : 0);
@@ -10,7 +10,24 @@
10
10
  */
11
11
 
12
12
  import chalk from 'chalk';
13
- import { readDashboardConn } from './headroom.mjs';
13
+ // Mirror `readDashboardConn` from clip.mjs / minimax.mjs / usage.mjs.
14
+ function readDashboardConn() {
15
+ const cfgDir = process.env.XDG_CONFIG_HOME
16
+ ? require_('node:path').join(process.env.XDG_CONFIG_HOME, 'bizar')
17
+ : require_('node:path').join(require_('node:os').homedir(), '.config', 'bizar');
18
+ const portPath = require_('node:path').join(cfgDir, 'dashboard.port');
19
+ const secretPath = require_('node:path').join(cfgDir, 'dashboard.secret');
20
+ const port = require_('node:fs').existsSync(portPath)
21
+ ? parseInt(require_('node:fs').readFileSync(portPath, 'utf8').trim(), 10)
22
+ : 4321;
23
+ const secret = require_('node:fs').existsSync(secretPath)
24
+ ? require_('node:fs').readFileSync(secretPath, 'utf8').trim()
25
+ : '';
26
+ return {
27
+ port: Number.isFinite(port) && port > 0 ? port : 4321,
28
+ secret,
29
+ };
30
+ }
14
31
 
15
32
  // ── API helpers ────────────────────────────────────────────────────────────────
16
33
 
@@ -15,7 +15,25 @@ import chalk from 'chalk';
15
15
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
16
16
  import { join, dirname } from 'node:path';
17
17
  import { homedir } from 'node:os';
18
- import { readDashboardConn } from './headroom.mjs';
18
+
19
+ // Mirror `readDashboardConn` from clip.mjs / minimax.mjs / usage.mjs.
20
+ function readDashboardConn() {
21
+ const cfgDir = process.env.XDG_CONFIG_HOME
22
+ ? join(process.env.XDG_CONFIG_HOME, 'bizar')
23
+ : join(homedir(), '.config', 'bizar');
24
+ const portPath = join(cfgDir, 'dashboard.port');
25
+ const secretPath = join(cfgDir, 'dashboard.secret');
26
+ const port = existsSync(portPath)
27
+ ? parseInt(readFileSync(portPath, 'utf8').trim(), 10)
28
+ : 4321;
29
+ const secret = existsSync(secretPath)
30
+ ? readFileSync(secretPath, 'utf8').trim()
31
+ : '';
32
+ return {
33
+ port: Number.isFinite(port) && port > 0 ? port : 4321,
34
+ secret,
35
+ };
36
+ }
19
37
 
20
38
  const HOME = homedir();
21
39
  const TEMPLATES_DIR = join(HOME, '.config', 'bizar', 'templates', 'eval-fixtures');
@@ -18,7 +18,6 @@
18
18
  import chalk from 'chalk';
19
19
  import { existsSync, readFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
- import { process } from 'node:process';
22
21
 
23
22
  export function showLightragHelp() {
24
23
  console.log(`
@@ -11,7 +11,24 @@
11
11
 
12
12
  import chalk from 'chalk';
13
13
  import { readFileSync, existsSync } from 'node:fs';
14
- import { readDashboardConn } from './headroom.mjs';
14
+ // Mirror `readDashboardConn` from clip.mjs / minimax.mjs / usage.mjs.
15
+ function readDashboardConn() {
16
+ const cfgDir = process.env.XDG_CONFIG_HOME
17
+ ? require_('node:path').join(process.env.XDG_CONFIG_HOME, 'bizar')
18
+ : require_('node:path').join(require_('node:os').homedir(), '.config', 'bizar');
19
+ const portPath = require_('node:path').join(cfgDir, 'dashboard.port');
20
+ const secretPath = require_('node:path').join(cfgDir, 'dashboard.secret');
21
+ const port = require_('node:fs').existsSync(portPath)
22
+ ? parseInt(require_('node:fs').readFileSync(portPath, 'utf8').trim(), 10)
23
+ : 4321;
24
+ const secret = require_('node:fs').existsSync(secretPath)
25
+ ? require_('node:fs').readFileSync(secretPath, 'utf8').trim()
26
+ : '';
27
+ return {
28
+ port: Number.isFinite(port) && port > 0 ? port : 4321,
29
+ secret,
30
+ };
31
+ }
15
32
 
16
33
  // ── API helpers ────────────────────────────────────────────────────────────────
17
34
 
@@ -105,5 +105,9 @@ async function runUsageCommand(args, wantJson = false) {
105
105
  }
106
106
 
107
107
  export async function run(name, args, isHelpRequest) {
108
+ if (isHelpRequest || args.length === 0) {
109
+ showUsageHelp();
110
+ return;
111
+ }
108
112
  await runUsageCommand(args, false);
109
113
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Miscellaneous utility commands:
5
5
  * audit, init, export, test-gate, dev-link, dev-unlink,
6
- * doctor, repair, heads-up, bg, agent-browser-up, providers detect,
6
+ * doctor, repair, heads-up, bg, agent-browser, agent-browser-up, providers detect,
7
7
  * backup, restore
8
8
  */
9
9
  import chalk from 'chalk';
@@ -154,6 +154,24 @@ export function showBackupHelp() {
154
154
  `);
155
155
  }
156
156
 
157
+ export function showProvidersHelp() {
158
+ console.log(`
159
+ bizar providers - Auto-detect provider API keys
160
+
161
+ Usage:
162
+ bizar providers detect Auto-detect provider API keys from env + cline.json
163
+ bizar providers --help Show this help
164
+
165
+ Description:
166
+ Scans process.env + ~/.config/cline/cline.json for known provider
167
+ API keys (OpenAI, Anthropic, OpenRouter, MiniMax, etc.) and reports
168
+ what's available. Useful before the first \`bizar install\` to verify
169
+ credentials are picked up.
170
+
171
+ No API keys are sent over the network - detection is local.
172
+ `);
173
+ }
174
+
157
175
  export function showRestoreHelp() {
158
176
  console.log(`
159
177
  bizar restore — Restore BizarHarness from a backup
@@ -209,6 +227,18 @@ export async function runTestGate() {
209
227
 
210
228
  export async function run(name, args, isHelpRequest) {
211
229
  switch (name) {
230
+ case 'update':
231
+ // `bizar update` lives in commands/install.mjs (the install/update
232
+ // pair share a code path). Proxy to it.
233
+ if (isHelpRequest) {
234
+ const { showUpdateHelp } = await import('./install.mjs');
235
+ showUpdateHelp();
236
+ } else {
237
+ const { runUpdate } = await import('./install.mjs');
238
+ await runUpdate(args, {});
239
+ }
240
+ break;
241
+
212
242
  case 'audit':
213
243
  if (isHelpRequest) showAuditHelp();
214
244
  else {
@@ -441,7 +471,75 @@ export async function run(name, args, isHelpRequest) {
441
471
  break;
442
472
  }
443
473
 
474
+ case 'agent-browser': {
475
+ // v6.0.0 — install / update / verify the agent-browser CLI.
476
+ // (The 'agent-browser-up' sibling is the bash wrapper that just
477
+ // manages the daemon process; this is the rich installer + updater.)
478
+ const { install, update, detectState, ensureRunning, printStatus } =
479
+ await import('../agent-browser-update.mjs');
480
+ const sub = args[0] || 'status';
481
+ switch (sub) {
482
+ case 'status':
483
+ printStatus();
484
+ break;
485
+ case 'install': {
486
+ const s = install({ silent: false });
487
+ console.log(chalk.green('\n agent-browser ready.'));
488
+ console.log(` version: ${s.version}`);
489
+ console.log(` daemon: ${s.daemonRunning ? 'running' : 'stopped'}`);
490
+ console.log(` profile: ${s.profileDir}`);
491
+ break;
492
+ }
493
+ case 'update': {
494
+ const s = update({ silent: false });
495
+ console.log(chalk.green('\n agent-browser up-to-date.'));
496
+ console.log(` version: ${s.version}`);
497
+ console.log(` daemon: ${s.daemonRunning ? 'running' : 'stopped'}`);
498
+ break;
499
+ }
500
+ case 'detect': {
501
+ const s = detectState();
502
+ console.log(JSON.stringify(s, null, 2));
503
+ break;
504
+ }
505
+ case 'start':
506
+ ensureRunning({ silent: false });
507
+ break;
508
+ case 'stop': {
509
+ const { spawnSync } = await import('node:child_process');
510
+ const killed = spawnSync('pkill', ['-f', 'agent-browser serve'], { stdio: 'ignore' });
511
+ if (killed.status === 0) {
512
+ console.log(chalk.green(' ✓ daemon stopped'));
513
+ } else {
514
+ console.log(chalk.dim(' daemon was not running'));
515
+ }
516
+ break;
517
+ }
518
+ default:
519
+ console.log(`bizar agent-browser <sub>
520
+
521
+ Subcommands:
522
+ status one-line status (default)
523
+ install install agent-browser + download Chrome
524
+ update upgrade to the latest version
525
+ detect JSON state (for scripts)
526
+ start start the daemon
527
+ stop stop the daemon
528
+
529
+ Examples:
530
+ bizar agent-browser status
531
+ bizar agent-browser install
532
+ bizar agent-browser update
533
+ bizar agent-browser start`);
534
+ }
535
+ break;
536
+ }
537
+
444
538
  case 'providers':
539
+ if (isHelpRequest || args.length === 0) {
540
+ showProvidersHelp();
541
+ break;
542
+ }
445
543
  if (args[0] === 'detect') {
446
544
  const { runProvidersDetect } = await import('../providers-detect.mjs');
447
545
  await runProvidersDetect(args.slice(1));
package/cli/digest.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  import chalk from 'chalk';
14
14
  import { readFileSync, existsSync } from 'node:fs';
15
15
 
16
- function showDigestHelp() {
16
+ export function showDigestHelp() {
17
17
  console.log(`
18
18
  bizar digest — Manage weekly digests
19
19
 
package/cli/provision.mjs CHANGED
@@ -835,6 +835,44 @@ export async function installLightragProvision({ dryRun = false } = {}) {
835
835
  }
836
836
  }
837
837
 
838
+ /**
839
+ * v6.0.0 — Ensure `agent-browser` (native Rust CLI from vercel-labs) is
840
+ * installed and the daemon is up. agent-browser replaces the v5.x
841
+ * `browser-harness` (Python CDP wrapper). The CLI is installed via
842
+ * `npm install -g agent-browser`; Chrome for Testing is downloaded by
843
+ * `agent-browser install`; the daemon is started by
844
+ * `agent-browser serve --headless --port <port> --profile <dir>`.
845
+ *
846
+ * Idempotent. Returns { ok, message, version?, daemonRunning? }.
847
+ */
848
+ export async function ensureAgentBrowser({ dryRun = false, mode = 'install' } = {}) {
849
+ const ab = await import('./agent-browser-update.mjs');
850
+ const before = ab.detectState();
851
+ if (dryRun) {
852
+ return {
853
+ ok: true,
854
+ message: `[dry-run] agent-browser: ${before.installed ? 'installed' : 'not installed'}` +
855
+ (before.installed ? ` (v${before.version})` : ''),
856
+ version: before.version,
857
+ daemonRunning: before.daemonRunning,
858
+ };
859
+ }
860
+ try {
861
+ const after = mode === 'update' && before.installed
862
+ ? ab.update({ silent: true, startDaemon: true })
863
+ : ab.install({ silent: true, startDaemon: true });
864
+ return {
865
+ ok: true,
866
+ message: `agent-browser: ${after.installed ? `v${after.version}` : 'install attempted'}` +
867
+ ` · daemon: ${after.daemonRunning ? 'running' : 'stopped'}`,
868
+ version: after.version,
869
+ daemonRunning: after.daemonRunning,
870
+ };
871
+ } catch (err) {
872
+ return { ok: false, message: `agent-browser install failed: ${err.message}` };
873
+ }
874
+ }
875
+
838
876
  /**
839
877
  * v4.4.11 — The mods step. By default, NEVER install or upgrade mods
840
878
  * during `bizar install` or `bizar update`. The step just reports the
package/install.sh CHANGED
@@ -14,8 +14,8 @@
14
14
  # package manager:
15
15
  #
16
16
  # - Linux: ensure node is installed (apt/dnf/pacman/zypper), install uv,
17
- # python3.12, jq, gh, agent-browser, Chrome runtime libs.
18
- # - macOS: ensure homebrew is installed; everything else is via brew.
17
+ # python3.12, jq, gh, agent-browser (npm), Chrome for Testing.
18
+ # - macOS: ensure homebrew is installed; everything else is via brew/npm.
19
19
  # - Windows: a stub that prints "use install.ps1".
20
20
  #
21
21
  # Agent files / plugin copy / cline.json patching / service registration /
@@ -189,6 +189,41 @@ check_deps() {
189
189
  fi
190
190
  }
191
191
 
192
+ install_agent_browser() {
193
+ # v6.0.0 — agent-browser (native Rust CLI from vercel-labs). Replaces
194
+ # the v5.x browser-harness (Python CDP wrapper). Installs via npm.
195
+ #
196
+ # If npm is not available, this is a soft-fail: agent-browser is
197
+ # optional, and `cli/provision.mjs:ensureAgentBrowser` will retry it
198
+ # once npm is available.
199
+ if have_cmd agent-browser; then
200
+ local ab_ver
201
+ ab_ver="$(agent-browser --version 2>/dev/null || echo 'unknown')"
202
+ note "agent-browser ${ab_ver} already installed"
203
+ return 0
204
+ fi
205
+ action "Installing agent-browser via npm..."
206
+ if ! have_cmd npm; then
207
+ warn "npm not available — skipping agent-browser install"
208
+ warn " Install later: npm install -g agent-browser"
209
+ return 0
210
+ fi
211
+ if dry npm install -g agent-browser 2>&1; then
212
+ local ab_ver
213
+ ab_ver="$(agent-browser --version 2>/dev/null || echo 'unknown')"
214
+ note "agent-browser ${ab_ver} installed"
215
+ # Download Chrome for Testing
216
+ if dry agent-browser install 2>&1; then
217
+ note "Chrome for Testing downloaded"
218
+ else
219
+ warn "Chrome download failed — retry later: agent-browser install"
220
+ fi
221
+ else
222
+ warn "agent-browser install failed — the browser tools will not be available"
223
+ warn " Install later: npm install -g agent-browser"
224
+ fi
225
+ }
226
+
192
227
  install_lightrag() {
193
228
  # LightRAG is optional but recommended. Install via uv tool.
194
229
  # uv tools install to ~/.local/bin/ — ensure that's on PATH.
@@ -359,6 +394,7 @@ install_linux() {
359
394
  ensure_node
360
395
  check_deps
361
396
  install_missing_deps_linux
397
+ install_agent_browser
362
398
  install_service
363
399
  }
364
400
 
@@ -366,6 +402,7 @@ install_macos() {
366
402
  note "Detected macOS ($(uname -m))"
367
403
  check_deps
368
404
  install_missing_deps_macos
405
+ install_agent_browser
369
406
  install_service
370
407
  }
371
408
 
@@ -380,7 +417,7 @@ main() {
380
417
  fi
381
418
 
382
419
  echo ""
383
- echo -e "${BOLD}${CYAN} ⚡ BizarHarness Installer v4.4.7${NC}"
420
+ echo -e "${BOLD}${CYAN} ⚡ BizarHarness Installer v6.0.0${NC}"
384
421
  if [ "$UPDATE_MODE" -eq 1 ]; then
385
422
  echo -e " ${DIM}Update mode${NC}"
386
423
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "5.6.0-beta.7",
3
+ "version": "5.6.0-beta.9",
4
4
  "description": "Norse-pantheon multi-agent system for cline — 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, cline plugin, and typed SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-sdk",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.9",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",