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.
- package/agent-prompts.mjs +95 -0
- package/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/bosun.schema.json +101 -3
- package/codex-shell.mjs +85 -10
- package/desktop/main.mjs +871 -48
- package/desktop/preload.mjs +54 -1
- package/desktop-shortcut.mjs +90 -11
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +21 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/setup-web-server.mjs +20 -10
- package/setup.mjs +376 -83
- package/startup-service.mjs +51 -6
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +164 -4
- package/ui/components/agent-selector.js +145 -1
- package/ui/components/chat-view.js +161 -15
- package/ui/components/session-list.js +2 -2
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice-client-sdk.js +733 -0
- package/ui/modules/voice-overlay.js +128 -15
- package/ui/modules/voice.js +15 -6
- package/ui/setup.html +281 -81
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +122 -14
- package/ui/styles.css +14 -0
- package/ui/tabs/agents.js +1 -1
- package/ui/tabs/chat.js +123 -14
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +400 -22
- package/update-check.mjs +41 -13
- package/voice-action-dispatcher.mjs +844 -0
- package/voice-agents-sdk.mjs +664 -0
- package/voice-auth-manager.mjs +164 -0
- package/voice-relay.mjs +1194 -0
- package/voice-tools.mjs +914 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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, "
|
|
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
|
-
|
|
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 (
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
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.
|
|
5491
|
+
chalk.cyan(" 1 · Quick Launch") +
|
|
5492
|
+
(shortcutSupported
|
|
5493
|
+
? chalk.dim(` (${desktopMethod})`)
|
|
5494
|
+
: chalk.dim(" — shortcut unavailable on this OS")),
|
|
5306
5495
|
);
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
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
|
-
|
|
5317
|
-
|
|
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
|
-
|
|
5320
|
-
await import("./startup-service.mjs");
|
|
5321
|
-
const currentStartup = getStartupStatus();
|
|
5322
|
-
const methodName = getStartupMethodName();
|
|
5544
|
+
env._RUNNING_MODE = String(modeIdx);
|
|
5323
5545
|
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
"
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
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-
|
|
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-
|
|
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
|
-
//
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
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 --
|
|
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
|
}
|
package/startup-service.mjs
CHANGED
|
@@ -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>${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|