bosun 0.36.2 → 0.36.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.
Files changed (57) hide show
  1. package/agent-prompts.mjs +95 -0
  2. package/analyze-agent-work-helpers.mjs +308 -0
  3. package/analyze-agent-work.mjs +926 -0
  4. package/autofix.mjs +2 -0
  5. package/bosun.schema.json +101 -3
  6. package/codex-shell.mjs +85 -10
  7. package/desktop/main.mjs +871 -48
  8. package/desktop/preload.mjs +54 -1
  9. package/desktop-shortcut.mjs +90 -11
  10. package/git-editor-fix.mjs +273 -0
  11. package/mcp-registry.mjs +579 -0
  12. package/meeting-workflow-service.mjs +631 -0
  13. package/monitor.mjs +18 -103
  14. package/package.json +21 -2
  15. package/primary-agent.mjs +32 -12
  16. package/session-tracker.mjs +68 -0
  17. package/setup-web-server.mjs +20 -10
  18. package/setup.mjs +376 -83
  19. package/startup-service.mjs +51 -6
  20. package/stream-resilience.mjs +17 -7
  21. package/ui/app.js +164 -4
  22. package/ui/components/agent-selector.js +145 -1
  23. package/ui/components/chat-view.js +161 -15
  24. package/ui/components/session-list.js +2 -2
  25. package/ui/components/shared.js +188 -15
  26. package/ui/modules/icons.js +13 -0
  27. package/ui/modules/utils.js +44 -0
  28. package/ui/modules/voice-client-sdk.js +733 -0
  29. package/ui/modules/voice-overlay.js +128 -15
  30. package/ui/modules/voice.js +15 -6
  31. package/ui/setup.html +281 -81
  32. package/ui/styles/components.css +99 -3
  33. package/ui/styles/sessions.css +122 -14
  34. package/ui/styles.css +14 -0
  35. package/ui/tabs/agents.js +1 -1
  36. package/ui/tabs/chat.js +123 -14
  37. package/ui/tabs/control.js +16 -22
  38. package/ui/tabs/dashboard.js +85 -8
  39. package/ui/tabs/library.js +113 -17
  40. package/ui/tabs/settings.js +116 -2
  41. package/ui/tabs/tasks.js +388 -39
  42. package/ui/tabs/telemetry.js +0 -1
  43. package/ui/tabs/workflows.js +4 -0
  44. package/ui-server.mjs +400 -22
  45. package/update-check.mjs +41 -13
  46. package/voice-action-dispatcher.mjs +844 -0
  47. package/voice-agents-sdk.mjs +664 -0
  48. package/voice-auth-manager.mjs +164 -0
  49. package/voice-relay.mjs +1194 -0
  50. package/voice-tools.mjs +914 -0
  51. package/workflow-templates/agents.mjs +6 -2
  52. package/workflow-templates/github.mjs +154 -12
  53. package/workflow-templates.mjs +3 -0
  54. package/github-reconciler.mjs +0 -506
  55. package/merge-strategy.mjs +0 -1210
  56. package/pr-cleanup-daemon.mjs +0 -992
  57. package/workspace-reaper.mjs +0 -405
package/setup.mjs CHANGED
@@ -53,7 +53,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
53
53
 
54
54
  const isNonInteractive =
55
55
  process.argv.includes("--non-interactive") || process.argv.includes("-y");
56
- const SETUP_TOTAL_STEPS = 10;
56
+ const SETUP_TOTAL_STEPS = 9;
57
57
 
58
58
  // ── Zero-dependency terminal styling (replaces chalk) ────────────────────────
59
59
  const isTTY = process.stdout.isTTY;
@@ -1765,8 +1765,14 @@ function applyTelegramMiniAppDefaults(env, sourceEnv = process.env) {
1765
1765
  env.TELEGRAM_UI_PORT || sourceEnv.TELEGRAM_UI_PORT,
1766
1766
  );
1767
1767
 
1768
+ // Default tunnel mode: if named tunnel credentials are present, use "named";
1769
+ // otherwise fall back to "quick" so the UI works out-of-the-box without setup.
1768
1770
  if (!env.TELEGRAM_UI_TUNNEL && !sourceEnv.TELEGRAM_UI_TUNNEL) {
1769
- env.TELEGRAM_UI_TUNNEL = "named";
1771
+ const hasNamedCreds = !!(
1772
+ (env.CLOUDFLARE_TUNNEL_NAME || sourceEnv.CLOUDFLARE_TUNNEL_NAME) &&
1773
+ (env.CLOUDFLARE_TUNNEL_CREDENTIALS || sourceEnv.CLOUDFLARE_TUNNEL_CREDENTIALS)
1774
+ );
1775
+ env.TELEGRAM_UI_TUNNEL = hasNamedCreds ? "named" : "quick";
1770
1776
  }
1771
1777
  if (!env.TELEGRAM_UI_ALLOW_UNSAFE && !sourceEnv.TELEGRAM_UI_ALLOW_UNSAFE) {
1772
1778
  env.TELEGRAM_UI_ALLOW_UNSAFE = "false";
@@ -1775,7 +1781,10 @@ function applyTelegramMiniAppDefaults(env, sourceEnv = process.env) {
1775
1781
  !env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK
1776
1782
  && !sourceEnv.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK
1777
1783
  ) {
1778
- env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "false";
1784
+ // Allow quick tunnel as fallback by default so named-tunnel failures don't
1785
+ // silently kill the UI. Users who explicitly set "named" during --setup will
1786
+ // have this set to "false" by the wizard if credentials were provided.
1787
+ env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "true";
1779
1788
  }
1780
1789
  if (
1781
1790
  !env.TELEGRAM_UI_FALLBACK_AUTH_ENABLED
@@ -1923,7 +1932,7 @@ function normalizeSetupConfiguration({
1923
1932
  "auto",
1924
1933
  );
1925
1934
  env.VOICE_MODEL =
1926
- env.VOICE_MODEL || "gpt-4o-realtime-preview-2024-12-17";
1935
+ env.VOICE_MODEL || "gpt-realtime-1.5";
1927
1936
  env.VOICE_VISION_MODEL =
1928
1937
  env.VOICE_VISION_MODEL || "gpt-4.1-mini";
1929
1938
  env.VOICE_ID = normalizeEnum(
@@ -1965,7 +1974,7 @@ function normalizeSetupConfiguration({
1965
1974
  env.PRIMARY_AGENT || "codex-sdk",
1966
1975
  );
1967
1976
  env.AZURE_OPENAI_REALTIME_DEPLOYMENT =
1968
- env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-4o-realtime-preview";
1977
+ env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-realtime-1.5";
1969
1978
 
1970
1979
  env.CODEX_MODEL_PROFILE = normalizeEnum(
1971
1980
  env.CODEX_MODEL_PROFILE,
@@ -3288,7 +3297,7 @@ async function main() {
3288
3297
  );
3289
3298
  env.VOICE_MODEL = await prompt.ask(
3290
3299
  "Realtime voice model",
3291
- process.env.VOICE_MODEL || "gpt-4o-realtime-preview-2024-12-17",
3300
+ process.env.VOICE_MODEL || "gpt-realtime-1.5",
3292
3301
  );
3293
3302
  env.VOICE_VISION_MODEL = await prompt.ask(
3294
3303
  "Vision model for camera/screen analysis",
@@ -3323,7 +3332,7 @@ async function main() {
3323
3332
  );
3324
3333
  env.AZURE_OPENAI_REALTIME_DEPLOYMENT = await prompt.ask(
3325
3334
  "Azure Realtime deployment (AZURE_OPENAI_REALTIME_DEPLOYMENT)",
3326
- process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-4o-realtime-preview",
3335
+ process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-realtime-1.5",
3327
3336
  );
3328
3337
  }
3329
3338
  if (env.VOICE_PROVIDER === "claude" && !env.ANTHROPIC_API_KEY) {
@@ -3579,6 +3588,154 @@ async function main() {
3579
3588
  }
3580
3589
  }
3581
3590
  }
3591
+
3592
+ // ── Sub-step 6b: Web UI / Telegram Mini App / Cloudflare Tunnel ──────────
3593
+ const hasTelegramToken = !!(env.TELEGRAM_BOT_TOKEN || process.env.TELEGRAM_BOT_TOKEN);
3594
+ if (hasTelegramToken) {
3595
+ console.log();
3596
+ console.log(chalk.bold(" Web UI & Telegram Mini App"));
3597
+ console.log(
3598
+ chalk.dim(" Bosun includes a browser-based dashboard that can open inside Telegram\n") +
3599
+ chalk.dim(" as a Mini App. External HTTPS access is provided via a Cloudflare tunnel\n") +
3600
+ chalk.dim(" (cloudflared). Without the tunnel, the UI is LAN-only.\n"),
3601
+ );
3602
+
3603
+ const wantWebUi = await prompt.confirm(
3604
+ "Enable browser Web UI / Telegram Mini App?",
3605
+ true,
3606
+ );
3607
+
3608
+ if (!wantWebUi) {
3609
+ env.TELEGRAM_MINIAPP_ENABLED = "false";
3610
+ env.TELEGRAM_UI_TUNNEL = "disabled";
3611
+ info("Web UI disabled. Re-run setup or add TELEGRAM_MINIAPP_ENABLED=true to .env to enable later.");
3612
+ } else {
3613
+ env.TELEGRAM_MINIAPP_ENABLED = "true";
3614
+ env.TELEGRAM_UI_PORT = normalizeTelegramUiPort(
3615
+ env.TELEGRAM_UI_PORT || process.env.TELEGRAM_UI_PORT,
3616
+ );
3617
+ env.TELEGRAM_UI_ALLOW_UNSAFE = "false";
3618
+
3619
+ console.log();
3620
+ const tunnelModeIdx = await prompt.choose(
3621
+ "How should the Web UI be exposed externally?",
3622
+ [
3623
+ "Quick tunnel — ephemeral trycloudflare.com URL (simplest, no account needed)",
3624
+ "Named tunnel — permanent custom URL (requires Cloudflare account + credentials)",
3625
+ "LAN only — local network access only, no public tunnel",
3626
+ ],
3627
+ 0,
3628
+ );
3629
+
3630
+ if (tunnelModeIdx === 0) {
3631
+ // ── Quick tunnel ──────────────────────────────────────────────────
3632
+ env.TELEGRAM_UI_TUNNEL = "quick";
3633
+ env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "true";
3634
+ info("✓ Quick tunnel selected — a trycloudflare.com URL will appear on startup.");
3635
+ console.log(chalk.dim(" The URL changes on restart. Install cloudflared if not already present:"));
3636
+ console.log(chalk.dim(" https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"));
3637
+
3638
+ } else if (tunnelModeIdx === 1) {
3639
+ // ── Named tunnel ──────────────────────────────────────────────────
3640
+ env.TELEGRAM_UI_TUNNEL = "named";
3641
+
3642
+ console.log();
3643
+ console.log(chalk.bold(" Named Tunnel Setup"));
3644
+ console.log(
3645
+ chalk.dim(" Prerequisites:\n") +
3646
+ chalk.dim(" 1. Cloudflare account with a registered domain\n") +
3647
+ chalk.dim(" 2. cloudflared CLI installed:\n") +
3648
+ chalk.dim(" https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n") +
3649
+ chalk.dim(" 3. Run: cloudflared tunnel login\n") +
3650
+ chalk.dim(" 4. Run: cloudflared tunnel create <your-tunnel-name>\n") +
3651
+ chalk.dim(" 5. Copy the credentials JSON path shown (e.g. ~/.cloudflared/<uuid>.json)\n"),
3652
+ );
3653
+
3654
+ const hasCredsReady = await prompt.confirm(
3655
+ "Do you have your tunnel credentials file ready?",
3656
+ false,
3657
+ );
3658
+
3659
+ if (hasCredsReady) {
3660
+ env.CLOUDFLARE_TUNNEL_NAME = await prompt.ask(
3661
+ "Tunnel name (from: cloudflared tunnel create <name>)",
3662
+ process.env.CLOUDFLARE_TUNNEL_NAME || "",
3663
+ );
3664
+ env.CLOUDFLARE_TUNNEL_CREDENTIALS = await prompt.ask(
3665
+ "Credentials file path (e.g. ~/.cloudflared/<uuid>.json)",
3666
+ process.env.CLOUDFLARE_TUNNEL_CREDENTIALS || "",
3667
+ );
3668
+ env.CLOUDFLARE_BASE_DOMAIN = await prompt.ask(
3669
+ "Your Cloudflare domain (e.g. example.com — used for auto hostname)",
3670
+ process.env.CLOUDFLARE_BASE_DOMAIN || "",
3671
+ );
3672
+
3673
+ if (isAdvancedSetup) {
3674
+ const explicitHostname = await prompt.ask(
3675
+ "Custom hostname (leave blank for auto: bosun-<username>.<domain>)",
3676
+ process.env.CLOUDFLARE_TUNNEL_HOSTNAME || "",
3677
+ );
3678
+ if (explicitHostname) {
3679
+ env.CLOUDFLARE_TUNNEL_HOSTNAME = explicitHostname;
3680
+ }
3681
+
3682
+ const cfApiToken = await prompt.ask(
3683
+ "Cloudflare API token for DNS auto-sync (optional — leave blank to skip)",
3684
+ process.env.CLOUDFLARE_API_TOKEN || "",
3685
+ );
3686
+ if (cfApiToken) {
3687
+ env.CLOUDFLARE_API_TOKEN = cfApiToken;
3688
+ env.CLOUDFLARE_DNS_SYNC_ENABLED = "true";
3689
+ const cfZoneId = await prompt.ask(
3690
+ "Cloudflare Zone ID (from your domain's Overview page)",
3691
+ process.env.CLOUDFLARE_ZONE_ID || "",
3692
+ );
3693
+ if (cfZoneId) env.CLOUDFLARE_ZONE_ID = cfZoneId;
3694
+ }
3695
+ }
3696
+
3697
+ const hasNameAndCreds = env.CLOUDFLARE_TUNNEL_NAME && env.CLOUDFLARE_TUNNEL_CREDENTIALS;
3698
+ if (hasNameAndCreds) {
3699
+ env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "false";
3700
+ info("✓ Named tunnel configured.");
3701
+ if (!env.CLOUDFLARE_BASE_DOMAIN && !process.env.CLOUDFLARE_BASE_DOMAIN) {
3702
+ warn("No base domain set — hostname auto-resolution will fail. Set CLOUDFLARE_BASE_DOMAIN in .env.");
3703
+ }
3704
+ } else {
3705
+ warn("Tunnel name or credentials path missing — enabling quick tunnel as fallback.");
3706
+ warn("Add CLOUDFLARE_TUNNEL_NAME + CLOUDFLARE_TUNNEL_CREDENTIALS to .env to activate named tunnel.");
3707
+ env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "true";
3708
+ }
3709
+ } else {
3710
+ warn("No problem! Add these to .env when ready:");
3711
+ console.log(chalk.cyan(" CLOUDFLARE_TUNNEL_NAME=<tunnel-name>"));
3712
+ console.log(chalk.cyan(" CLOUDFLARE_TUNNEL_CREDENTIALS=~/.cloudflared/<uuid>.json"));
3713
+ console.log(chalk.cyan(" CLOUDFLARE_BASE_DOMAIN=example.com"));
3714
+ console.log();
3715
+ warn("Enabling quick tunnel as fallback until named tunnel credentials are in .env.");
3716
+ env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "true";
3717
+ }
3718
+
3719
+ } else {
3720
+ // ── LAN only ──────────────────────────────────────────────────────
3721
+ env.TELEGRAM_UI_TUNNEL = "disabled";
3722
+ const lanPort = env.TELEGRAM_UI_PORT || "3080";
3723
+ info(`LAN-only mode — Web UI will be accessible at https://localhost:${lanPort}`);
3724
+ info("Note: Telegram Mini App requires an external HTTPS URL and will not work in LAN-only mode.");
3725
+ env.TELEGRAM_MINIAPP_ENABLED = "false";
3726
+ }
3727
+
3728
+ if (isAdvancedSetup && tunnelModeIdx < 2) {
3729
+ console.log();
3730
+ const customPort = await prompt.ask(
3731
+ "Web UI port",
3732
+ String(env.TELEGRAM_UI_PORT || process.env.TELEGRAM_UI_PORT || "3080"),
3733
+ );
3734
+ env.TELEGRAM_UI_PORT = customPort || env.TELEGRAM_UI_PORT || "3080";
3735
+ }
3736
+ }
3737
+ } // end sub-step 6b
3738
+
3582
3739
  saveSetupSnapshot(6, "Telegram Notifications", env, configJson);
3583
3740
  } // end step 6
3584
3741
 
@@ -5277,74 +5434,142 @@ async function main() {
5277
5434
  saveSetupSnapshot(8, "Optional Channels", env, configJson);
5278
5435
  } // end step 8
5279
5436
 
5280
- // ── Step 9: Desktop Shortcut ──────────────────────────
5437
+ // ── Step 9: Running Mode ──────────────────────────────
5281
5438
  if (resumeFromStep > 9) {
5282
5439
  info(`Skipping step 9 (restored from previous run).`);
5283
5440
  } else {
5284
- headingStepWithSnapshot(9, "Desktop Shortcut");
5441
+ headingStepWithSnapshot(9, "Running Mode");
5285
5442
 
5443
+ const {
5444
+ getStartupStatus,
5445
+ getStartupMethodName,
5446
+ } = await import("./startup-service.mjs");
5286
5447
  const {
5287
5448
  getDesktopShortcutStatus,
5288
5449
  getDesktopShortcutMethodName,
5450
+ resolveElectronLauncher,
5289
5451
  } = await import("./desktop-shortcut.mjs");
5290
- const currentDesktopShortcut = getDesktopShortcutStatus();
5452
+
5453
+ const currentStartup = getStartupStatus();
5454
+ const methodName = getStartupMethodName();
5291
5455
  const desktopMethod = getDesktopShortcutMethodName();
5456
+ const shortcutSupported = desktopMethod !== "unsupported";
5457
+ const currentShortcut = getDesktopShortcutStatus();
5458
+ const electronLauncher = resolveElectronLauncher();
5459
+ const hasElectron = Boolean(electronLauncher?.executable);
5460
+
5461
+ let skipRunningModeConfig = false;
5462
+ const alreadyConfigured = currentStartup.installed || currentShortcut.installed;
5463
+ if (alreadyConfigured) {
5464
+ if (currentStartup.installed) {
5465
+ info(`Background service already registered (${currentStartup.method}).`);
5466
+ }
5467
+ if (currentShortcut.installed) {
5468
+ info(`Desktop shortcut already installed (${currentShortcut.method}).`);
5469
+ }
5470
+ const change = await prompt.confirm("Change the running mode?", false);
5471
+ if (!change) {
5472
+ env._DESKTOP_SHORTCUT = "skip";
5473
+ env._STARTUP_SERVICE = "skip";
5474
+ env.BOSUN_SENTINEL_AUTO_START = env.BOSUN_SENTINEL_AUTO_START || "false";
5475
+ env._RUNNING_MODE = "skip";
5476
+ env._START_AFTER_SETUP = "0";
5477
+ env._OPEN_PORTAL_AFTER_SETUP = "0";
5478
+ skipRunningModeConfig = true;
5479
+ }
5480
+ }
5292
5481
 
5293
- if (desktopMethod === "unsupported") {
5294
- info("Desktop shortcut not supported on this OS.");
5295
- env._DESKTOP_SHORTCUT = "0";
5296
- } else if (currentDesktopShortcut.installed) {
5297
- info(`Desktop shortcut already installed (${currentDesktopShortcut.method}).`);
5298
- const reinstall = await prompt.confirm(
5299
- "Re-install desktop shortcut?",
5300
- false,
5301
- );
5302
- env._DESKTOP_SHORTCUT = reinstall ? "1" : "skip";
5303
- } else {
5482
+ if (!skipRunningModeConfig) {
5483
+ console.log();
5484
+ console.log(chalk.bold(" How should Bosun run on this machine?"));
5485
+ console.log();
5486
+ console.log(chalk.cyan(" 0 · Manual"));
5487
+ console.log(chalk.dim(" Start bosun yourself from a terminal whenever you need it."));
5488
+ console.log(chalk.dim(" Best for: CI servers, development, occasional use, SSH hosts."));
5489
+ console.log();
5304
5490
  console.log(
5305
- chalk.dim(` Add a desktop shortcut using ${desktopMethod}.`),
5491
+ chalk.cyan(" 1 · Quick Launch") +
5492
+ (shortcutSupported
5493
+ ? chalk.dim(` (${desktopMethod})`)
5494
+ : chalk.dim(" — shortcut unavailable on this OS")),
5306
5495
  );
5307
- const enableDesktopShortcut = await prompt.confirm(
5308
- "Create desktop shortcut for Bosun Portal?",
5309
- true,
5496
+ console.log(chalk.dim(" Adds a desktop shortcut for one-click launch of the Bosun portal."));
5497
+ console.log(chalk.dim(" Bosun does not start automatically — you open it when you need it."));
5498
+ console.log();
5499
+ console.log(chalk.cyan(" 2 · Background Service") + chalk.dim(" ← recommended"));
5500
+ console.log(chalk.dim(` Registers with ${methodName} to auto-start at login.`));
5501
+ console.log(chalk.dim(" Starts silently in the background. If it crashes, the OS restarts it."));
5502
+ console.log(chalk.dim(" Creates a desktop shortcut to open the portal UI anytime."));
5503
+ console.log();
5504
+ console.log(chalk.cyan(" 3 · Sentinel Mode") + chalk.dim(" (maximum uptime)"));
5505
+ console.log(chalk.dim(" Like Background Service, plus a Node-level watchdog that"));
5506
+ console.log(chalk.dim(" continuously monitors Bosun and revives it if it becomes unresponsive."));
5507
+ console.log(chalk.dim(" Ideal for always-on teams who depend on zero downtime."));
5508
+ console.log();
5509
+
5510
+ const defaultModeIdx = 2;
5511
+ const modeIdx = await prompt.choose(
5512
+ "Select running mode:",
5513
+ [
5514
+ "Manual — run from terminal when needed",
5515
+ "Quick Launch — desktop shortcut, no auto-start",
5516
+ "Background — auto-start at login via OS service (auto-restart on crash)",
5517
+ "Sentinel — background + Node watchdog for maximum uptime",
5518
+ ],
5519
+ defaultModeIdx,
5310
5520
  );
5311
- env._DESKTOP_SHORTCUT = enableDesktopShortcut ? "1" : "0";
5312
- }
5313
- saveSetupSnapshot(9, "Desktop Shortcut", env, configJson);
5314
- } // end step 9
5315
5521
 
5316
- // ── Step 10: Startup Service ───────────────────────────
5317
- headingStepWithSnapshot(10, "Startup Service");
5522
+ if (modeIdx === 0) {
5523
+ // Manual: no shortcut, no service
5524
+ env._DESKTOP_SHORTCUT = "0";
5525
+ env._STARTUP_SERVICE = "0";
5526
+ env.BOSUN_SENTINEL_AUTO_START = "false";
5527
+ } else if (modeIdx === 1) {
5528
+ // Quick Launch: shortcut only
5529
+ env._DESKTOP_SHORTCUT = shortcutSupported ? "1" : "0";
5530
+ env._STARTUP_SERVICE = "0";
5531
+ env.BOSUN_SENTINEL_AUTO_START = "false";
5532
+ } else if (modeIdx === 2) {
5533
+ // Background Service: OS-managed auto-start + shortcut
5534
+ env._DESKTOP_SHORTCUT = shortcutSupported ? "1" : "0";
5535
+ env._STARTUP_SERVICE = "1";
5536
+ env.BOSUN_SENTINEL_AUTO_START = "false";
5537
+ } else {
5538
+ // Sentinel Mode: background + sentinel watchdog + shortcut
5539
+ env._DESKTOP_SHORTCUT = shortcutSupported ? "1" : "0";
5540
+ env._STARTUP_SERVICE = "1";
5541
+ env.BOSUN_SENTINEL_AUTO_START = "true";
5542
+ }
5318
5543
 
5319
- const { getStartupStatus, getStartupMethodName } =
5320
- await import("./startup-service.mjs");
5321
- const currentStartup = getStartupStatus();
5322
- const methodName = getStartupMethodName();
5544
+ env._RUNNING_MODE = String(modeIdx);
5323
5545
 
5324
- if (currentStartup.installed) {
5325
- info(`Startup service already installed via ${currentStartup.method}.`);
5326
- const reinstall = await prompt.confirm(
5327
- "Re-install startup service?",
5328
- false,
5329
- );
5330
- env._STARTUP_SERVICE = reinstall ? "1" : "skip";
5331
- } else {
5332
- console.log(
5333
- chalk.dim(
5334
- ` Auto-start bosun when you log in using ${methodName}.`,
5335
- ),
5336
- );
5337
- console.log(
5338
- chalk.dim(
5339
- " It will run in daemon mode (background) with auto-restart on failure.",
5340
- ),
5341
- );
5342
- const enableStartup = await prompt.confirm(
5343
- "Enable auto-start on login?",
5344
- true,
5345
- );
5346
- env._STARTUP_SERVICE = enableStartup ? "1" : "0";
5546
+ // ── Post-setup launch options (background/sentinel modes only) ────
5547
+ if (modeIdx >= 2) {
5548
+ console.log();
5549
+ console.log(chalk.dim(" Bosun will auto-start on your next login."));
5550
+ const startNow = await prompt.confirm(
5551
+ "Start Bosun in the background right after setup completes?",
5552
+ true,
5553
+ );
5554
+ env._START_AFTER_SETUP = startNow ? "1" : "0";
5555
+ if (startNow && hasElectron) {
5556
+ const openPortal = await prompt.confirm(
5557
+ "Also open the Bosun desktop portal once Bosun has started?",
5558
+ false,
5559
+ );
5560
+ env._OPEN_PORTAL_AFTER_SETUP = openPortal ? "1" : "0";
5561
+ } else {
5562
+ env._OPEN_PORTAL_AFTER_SETUP = "0";
5563
+ }
5564
+ } else {
5565
+ env._START_AFTER_SETUP = "0";
5566
+ env._OPEN_PORTAL_AFTER_SETUP = "0";
5567
+ }
5347
5568
  }
5569
+
5570
+ saveSetupSnapshot(9, "Running Mode", env, configJson);
5571
+ } // end step 9
5572
+
5348
5573
  } finally {
5349
5574
  prompt.close();
5350
5575
  }
@@ -5424,7 +5649,7 @@ async function runNonInteractive({
5424
5649
  env.VOICE_ENABLED = process.env.VOICE_ENABLED || "true";
5425
5650
  env.VOICE_PROVIDER = process.env.VOICE_PROVIDER || "auto";
5426
5651
  env.VOICE_MODEL =
5427
- process.env.VOICE_MODEL || "gpt-4o-realtime-preview-2024-12-17";
5652
+ process.env.VOICE_MODEL || "gpt-realtime-1.5";
5428
5653
  env.VOICE_VISION_MODEL = process.env.VOICE_VISION_MODEL || "gpt-4.1-mini";
5429
5654
  env.OPENAI_REALTIME_API_KEY = process.env.OPENAI_REALTIME_API_KEY || "";
5430
5655
  env.AZURE_OPENAI_REALTIME_ENDPOINT =
@@ -5432,7 +5657,7 @@ async function runNonInteractive({
5432
5657
  env.AZURE_OPENAI_REALTIME_API_KEY =
5433
5658
  process.env.AZURE_OPENAI_REALTIME_API_KEY || "";
5434
5659
  env.AZURE_OPENAI_REALTIME_DEPLOYMENT =
5435
- process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-4o-realtime-preview";
5660
+ process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-realtime-1.5";
5436
5661
  env.VOICE_ID = process.env.VOICE_ID || "alloy";
5437
5662
  env.VOICE_TURN_DETECTION = process.env.VOICE_TURN_DETECTION || "server_vad";
5438
5663
  env.VOICE_FALLBACK_MODE = process.env.VOICE_FALLBACK_MODE || "browser";
@@ -5624,27 +5849,52 @@ async function runNonInteractive({
5624
5849
  };
5625
5850
  printHookScaffoldSummary(hookResult);
5626
5851
 
5627
- // Startup service: respect STARTUP_SERVICE env in non-interactive mode
5628
- if (parseBooleanEnvValue(process.env.STARTUP_SERVICE, false)) {
5629
- env._STARTUP_SERVICE = "1";
5630
- } else if (
5631
- process.env.STARTUP_SERVICE !== undefined &&
5632
- !parseBooleanEnvValue(process.env.STARTUP_SERVICE, true)
5633
- ) {
5634
- env._STARTUP_SERVICE = "0";
5635
- }
5636
- // else: don't set — writeConfigFiles will skip silently
5637
-
5638
- // Desktop shortcut: respect DESKTOP_SHORTCUT env in non-interactive mode
5639
- const desktopShortcutEnv =
5640
- process.env.DESKTOP_SHORTCUT ?? process.env.BOSUN_DESKTOP_SHORTCUT;
5641
- if (parseBooleanEnvValue(desktopShortcutEnv, false)) {
5642
- env._DESKTOP_SHORTCUT = "1";
5643
- } else if (
5644
- desktopShortcutEnv !== undefined &&
5645
- !parseBooleanEnvValue(desktopShortcutEnv, true)
5646
- ) {
5647
- env._DESKTOP_SHORTCUT = "0";
5852
+ // Running mode: RUNNING_MODE=0|1|2|3 sets startup+shortcut+sentinel together.
5853
+ // 0=Manual 1=Quick Launch (shortcut only) 2=Background Service 3=Sentinel
5854
+ // Individual STARTUP_SERVICE / DESKTOP_SHORTCUT env vars still work as legacy overrides.
5855
+ const runningModeRaw = process.env.RUNNING_MODE ?? process.env.BOSUN_RUNNING_MODE;
5856
+ if (runningModeRaw !== undefined) {
5857
+ const modeIdx = parseInt(runningModeRaw, 10);
5858
+ if (modeIdx === 0) {
5859
+ env._DESKTOP_SHORTCUT = "0";
5860
+ env._STARTUP_SERVICE = "0";
5861
+ env.BOSUN_SENTINEL_AUTO_START = env.BOSUN_SENTINEL_AUTO_START || "false";
5862
+ } else if (modeIdx === 1) {
5863
+ env._DESKTOP_SHORTCUT = "1";
5864
+ env._STARTUP_SERVICE = "0";
5865
+ env.BOSUN_SENTINEL_AUTO_START = env.BOSUN_SENTINEL_AUTO_START || "false";
5866
+ } else if (modeIdx === 2) {
5867
+ env._DESKTOP_SHORTCUT = "1";
5868
+ env._STARTUP_SERVICE = "1";
5869
+ env.BOSUN_SENTINEL_AUTO_START = env.BOSUN_SENTINEL_AUTO_START || "false";
5870
+ } else if (modeIdx === 3) {
5871
+ env._DESKTOP_SHORTCUT = "1";
5872
+ env._STARTUP_SERVICE = "1";
5873
+ env.BOSUN_SENTINEL_AUTO_START = "true";
5874
+ }
5875
+ } else {
5876
+ // Legacy: individual env vars still supported
5877
+ if (parseBooleanEnvValue(process.env.STARTUP_SERVICE, false)) {
5878
+ env._STARTUP_SERVICE = "1";
5879
+ } else if (
5880
+ process.env.STARTUP_SERVICE !== undefined &&
5881
+ !parseBooleanEnvValue(process.env.STARTUP_SERVICE, true)
5882
+ ) {
5883
+ env._STARTUP_SERVICE = "0";
5884
+ }
5885
+ // else: don't set — writeConfigFiles will skip silently
5886
+
5887
+ // Desktop shortcut: respect DESKTOP_SHORTCUT env in non-interactive mode
5888
+ const desktopShortcutEnv =
5889
+ process.env.DESKTOP_SHORTCUT ?? process.env.BOSUN_DESKTOP_SHORTCUT;
5890
+ if (parseBooleanEnvValue(desktopShortcutEnv, false)) {
5891
+ env._DESKTOP_SHORTCUT = "1";
5892
+ } else if (
5893
+ desktopShortcutEnv !== undefined &&
5894
+ !parseBooleanEnvValue(desktopShortcutEnv, true)
5895
+ ) {
5896
+ env._DESKTOP_SHORTCUT = "0";
5897
+ }
5648
5898
  }
5649
5899
 
5650
5900
  if (
@@ -5916,10 +6166,53 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
5916
6166
  }
5917
6167
  } else if (env._STARTUP_SERVICE === "0") {
5918
6168
  info(
5919
- "Startup service skipped — enable anytime: bosun --enable-startup",
6169
+ "Startup service skipped — enable anytime: bosun --setup",
5920
6170
  );
5921
6171
  }
5922
6172
 
6173
+ // ── Start Bosun Now ────────────────────────────────────
6174
+ if (env._START_AFTER_SETUP === "1") {
6175
+ heading("Starting Bosun");
6176
+ try {
6177
+ const { spawn: _spawn } = await import("child_process");
6178
+ const cliPath = resolve(__dirname, "cli.mjs");
6179
+ const daemonChild = _spawn(
6180
+ process.execPath,
6181
+ [cliPath, "--daemon"],
6182
+ {
6183
+ detached: true,
6184
+ stdio: "ignore",
6185
+ cwd: __dirname,
6186
+ windowsHide: true,
6187
+ },
6188
+ );
6189
+ daemonChild.unref();
6190
+ success("Bosun started in background (daemon mode)");
6191
+ info("Check status: bosun --daemon-status");
6192
+ info("View logs: bosun --logs");
6193
+ if (env._OPEN_PORTAL_AFTER_SETUP === "1") {
6194
+ const { resolveElectronLauncher: _getLauncher } =
6195
+ await import("./desktop-shortcut.mjs");
6196
+ const launcher = _getLauncher();
6197
+ if (launcher?.executable) {
6198
+ const portalChild = _spawn(
6199
+ launcher.executable,
6200
+ launcher.args || [],
6201
+ { detached: true, stdio: "ignore", windowsHide: false },
6202
+ );
6203
+ portalChild.unref();
6204
+ success("Bosun portal is opening...");
6205
+ } else {
6206
+ warn("Electron not found — portal launch skipped.");
6207
+ info("Open the portal manually: bosun --desktop");
6208
+ }
6209
+ }
6210
+ } catch (err) {
6211
+ warn(`Could not start Bosun: ${err.message}`);
6212
+ info("Start manually: bosun --daemon");
6213
+ }
6214
+ }
6215
+
5923
6216
  // ── Summary ────────────────────────────────────────────
5924
6217
  console.log("");
5925
6218
  console.log(
@@ -5985,7 +6278,7 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
5985
6278
  console.log(chalk.green(" bosun --setup"));
5986
6279
  console.log(chalk.dim(" Re-run this wizard anytime\n"));
5987
6280
  console.log(chalk.green(" bosun --enable-startup"));
5988
- console.log(chalk.dim(" Register auto-start on login\n"));
6281
+ console.log(chalk.dim(" Register auto-start on login (or use bosun --setup for the full wizard)\n"));
5989
6282
  console.log(chalk.green(" bosun --help"));
5990
6283
  console.log(chalk.dim(" See all options & env vars\n"));
5991
6284
  }
@@ -474,6 +474,18 @@ function generateLaunchdPlist({ daemon = true } = {}) {
474
474
 
475
475
  const argsXml = args.map((a) => ` <string>${a}</string>`).join("\n");
476
476
 
477
+ // Include Homebrew paths for both Apple Silicon (/opt/homebrew) and Intel (/usr/local)
478
+ const pathValue = [
479
+ `${home}/.local/bin`,
480
+ "/opt/homebrew/bin",
481
+ "/opt/homebrew/sbin",
482
+ "/usr/local/bin",
483
+ "/usr/bin",
484
+ "/bin",
485
+ "/usr/sbin",
486
+ "/sbin",
487
+ ].join(":");
488
+
477
489
  return `<?xml version="1.0" encoding="UTF-8"?>
478
490
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
479
491
  <plist version="1.0">
@@ -495,12 +507,16 @@ ${argsXml}
495
507
  </dict>
496
508
  <key>ThrottleInterval</key>
497
509
  <integer>30</integer>
510
+ <key>AbandonProcessGroup</key>
511
+ <true/>
498
512
  <key>EnvironmentVariables</key>
499
513
  <dict>
500
514
  <key>PATH</key>
501
- <string>${home}/.local/bin:/usr/local/bin:/usr/bin:/bin</string>
515
+ <string>${pathValue}</string>
502
516
  <key>HOME</key>
503
517
  <string>${home}</string>
518
+ <key>NODE_ENV</key>
519
+ <string>production</string>
504
520
  </dict>
505
521
  <key>StandardOutPath</key>
506
522
  <string>${logDir}/startup.log</string>
@@ -515,16 +531,36 @@ async function installMacOS(options = {}) {
515
531
  const plistContent = generateLaunchdPlist(options);
516
532
 
517
533
  try {
518
- // Unload existing agent if present
534
+ // Unload/bootout existing agent if present
519
535
  try {
520
- execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
536
+ // Try modern bootout first (macOS 10.11+), fall back to legacy unload
537
+ const uid = process.getuid?.() ?? "";
538
+ if (uid !== "") {
539
+ execSync(`launchctl bootout gui/${uid} "${plistPath}" 2>/dev/null || launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore", shell: true });
540
+ } else {
541
+ execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
542
+ }
521
543
  } catch {
522
- /* ok */
544
+ /* ok — agent may not be loaded */
523
545
  }
524
546
 
525
547
  mkdirSync(dirname(plistPath), { recursive: true });
526
548
  writeFileSync(plistPath, plistContent, "utf8");
527
- execSync(`launchctl load "${plistPath}"`, { stdio: "pipe" });
549
+
550
+ // Prefer modern bootstrap domain (macOS 10.11+); fall back to legacy load
551
+ let loaded = false;
552
+ try {
553
+ const uid = process.getuid?.() ?? "";
554
+ if (uid !== "") {
555
+ execSync(`launchctl bootstrap gui/${uid} "${plistPath}"`, { stdio: "pipe" });
556
+ loaded = true;
557
+ }
558
+ } catch {
559
+ /* fall through to legacy load */
560
+ }
561
+ if (!loaded) {
562
+ execSync(`launchctl load "${plistPath}"`, { stdio: "pipe" });
563
+ }
528
564
 
529
565
  return {
530
566
  success: true,
@@ -595,7 +631,16 @@ async function removeMacOS() {
595
631
  const plistPath = getLaunchdPlistPath();
596
632
  try {
597
633
  try {
598
- execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
634
+ // Prefer modern bootout; fall back to legacy unload
635
+ const uid = process.getuid?.() ?? "";
636
+ if (uid !== "") {
637
+ execSync(
638
+ `launchctl bootout gui/${uid} "${plistPath}" 2>/dev/null || launchctl unload "${plistPath}" 2>/dev/null`,
639
+ { stdio: "ignore", shell: true },
640
+ );
641
+ } else {
642
+ execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
643
+ }
599
644
  } catch {
600
645
  /* ok */
601
646
  }