bosun 0.36.0 → 0.36.2

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 (98) hide show
  1. package/.env.example +98 -16
  2. package/README.md +27 -0
  3. package/agent-event-bus.mjs +5 -5
  4. package/agent-pool.mjs +129 -12
  5. package/agent-prompts.mjs +7 -1
  6. package/agent-sdk.mjs +13 -2
  7. package/agent-supervisor.mjs +2 -2
  8. package/agent-work-report.mjs +1 -1
  9. package/anomaly-detector.mjs +6 -6
  10. package/autofix.mjs +15 -15
  11. package/bosun-skills.mjs +4 -4
  12. package/bosun.schema.json +160 -4
  13. package/claude-shell.mjs +11 -11
  14. package/cli.mjs +21 -21
  15. package/codex-config.mjs +19 -19
  16. package/codex-shell.mjs +180 -29
  17. package/config-doctor.mjs +27 -2
  18. package/config.mjs +60 -7
  19. package/copilot-shell.mjs +4 -4
  20. package/error-detector.mjs +1 -1
  21. package/fleet-coordinator.mjs +2 -2
  22. package/gemini-shell.mjs +692 -0
  23. package/github-oauth-portal.mjs +1 -1
  24. package/github-reconciler.mjs +2 -2
  25. package/kanban-adapter.mjs +741 -168
  26. package/merge-strategy.mjs +25 -25
  27. package/monitor.mjs +123 -105
  28. package/opencode-shell.mjs +22 -22
  29. package/package.json +7 -1
  30. package/postinstall.mjs +22 -22
  31. package/pr-cleanup-daemon.mjs +6 -6
  32. package/prepublish-check.mjs +4 -4
  33. package/presence.mjs +2 -2
  34. package/primary-agent.mjs +85 -7
  35. package/publish.mjs +1 -1
  36. package/review-agent.mjs +1 -1
  37. package/session-tracker.mjs +11 -0
  38. package/setup-web-server.mjs +429 -21
  39. package/setup.mjs +367 -12
  40. package/shared-knowledge.mjs +1 -1
  41. package/startup-service.mjs +9 -9
  42. package/stream-resilience.mjs +58 -4
  43. package/sync-engine.mjs +2 -2
  44. package/task-assessment.mjs +9 -9
  45. package/task-cli.mjs +1 -1
  46. package/task-complexity.mjs +71 -2
  47. package/task-context.mjs +1 -2
  48. package/task-executor.mjs +104 -41
  49. package/telegram-bot.mjs +825 -494
  50. package/telegram-sentinel.mjs +28 -28
  51. package/ui/app.js +256 -23
  52. package/ui/app.monolith.js +1 -1
  53. package/ui/components/agent-selector.js +4 -3
  54. package/ui/components/chat-view.js +101 -28
  55. package/ui/components/diff-viewer.js +3 -3
  56. package/ui/components/kanban-board.js +3 -3
  57. package/ui/components/session-list.js +255 -35
  58. package/ui/components/workspace-switcher.js +3 -3
  59. package/ui/demo.html +209 -194
  60. package/ui/index.html +3 -3
  61. package/ui/modules/icon-utils.js +206 -142
  62. package/ui/modules/icons.js +2 -27
  63. package/ui/modules/settings-schema.js +29 -5
  64. package/ui/modules/streaming.js +30 -2
  65. package/ui/modules/vision-stream.js +275 -0
  66. package/ui/modules/voice-client.js +102 -9
  67. package/ui/modules/voice-fallback.js +62 -6
  68. package/ui/modules/voice-overlay.js +594 -59
  69. package/ui/modules/voice.js +31 -38
  70. package/ui/setup.html +284 -34
  71. package/ui/styles/components.css +47 -0
  72. package/ui/styles/sessions.css +75 -0
  73. package/ui/tabs/agents.js +73 -43
  74. package/ui/tabs/chat.js +37 -40
  75. package/ui/tabs/control.js +2 -2
  76. package/ui/tabs/dashboard.js +1 -1
  77. package/ui/tabs/infra.js +10 -10
  78. package/ui/tabs/library.js +8 -8
  79. package/ui/tabs/logs.js +10 -10
  80. package/ui/tabs/settings.js +20 -20
  81. package/ui/tabs/tasks.js +76 -47
  82. package/ui-server.mjs +1761 -124
  83. package/update-check.mjs +13 -13
  84. package/ve-kanban.mjs +1 -1
  85. package/whatsapp-channel.mjs +5 -5
  86. package/workflow-engine.mjs +20 -1
  87. package/workflow-nodes.mjs +904 -4
  88. package/workflow-templates/agents.mjs +321 -7
  89. package/workflow-templates/ci-cd.mjs +6 -6
  90. package/workflow-templates/github.mjs +156 -84
  91. package/workflow-templates/planning.mjs +8 -8
  92. package/workflow-templates/reliability.mjs +8 -8
  93. package/workflow-templates/security.mjs +3 -3
  94. package/workflow-templates.mjs +15 -9
  95. package/workspace-manager.mjs +85 -1
  96. package/workspace-monitor.mjs +2 -2
  97. package/workspace-registry.mjs +2 -2
  98. package/worktree-manager.mjs +1 -1
package/setup.mjs CHANGED
@@ -217,22 +217,22 @@ function headingStep(step, label, markProgress) {
217
217
  }
218
218
 
219
219
  function check(label, ok, hint) {
220
- const icon = ok ? "" : "";
220
+ const icon = ok ? ":check:" : ":close:";
221
221
  console.log(` ${icon} ${label}`);
222
222
  if (!ok && hint) console.log(` → ${hint}`);
223
223
  return ok;
224
224
  }
225
225
 
226
226
  function info(msg) {
227
- console.log(` ℹ️ ${msg}`);
227
+ console.log(` :help: ${msg}`);
228
228
  }
229
229
 
230
230
  function success(msg) {
231
- console.log(` ${msg}`);
231
+ console.log(` :check: ${msg}`);
232
232
  }
233
233
 
234
234
  function warn(msg) {
235
- console.log(` ⚠️ ${msg}`);
235
+ console.log(` :alert: ${msg}`);
236
236
  }
237
237
 
238
238
  function escapeTelegramHtml(value) {
@@ -917,6 +917,7 @@ function defaultVariantForExecutor(executor) {
917
917
  if (normalized === "COPILOT" || normalized === "CLAUDE") {
918
918
  return "CLAUDE_OPUS_4_6";
919
919
  }
920
+ if (normalized === "GEMINI") return "DEFAULT";
920
921
  return "DEFAULT";
921
922
  }
922
923
 
@@ -1592,6 +1593,31 @@ const EXECUTOR_PRESETS = {
1592
1593
  role: "primary",
1593
1594
  },
1594
1595
  ],
1596
+ "gemini-only": [
1597
+ {
1598
+ name: "gemini-default",
1599
+ executor: "GEMINI",
1600
+ variant: "DEFAULT",
1601
+ weight: 100,
1602
+ role: "primary",
1603
+ },
1604
+ ],
1605
+ "gemini-codex": [
1606
+ {
1607
+ name: "gemini-default",
1608
+ executor: "GEMINI",
1609
+ variant: "DEFAULT",
1610
+ weight: 60,
1611
+ role: "primary",
1612
+ },
1613
+ {
1614
+ name: "codex-backup",
1615
+ executor: "CODEX",
1616
+ variant: "DEFAULT",
1617
+ weight: 40,
1618
+ role: "backup",
1619
+ },
1620
+ ],
1595
1621
  "opencode-only": [
1596
1622
  {
1597
1623
  name: "opencode-default",
@@ -1740,11 +1766,29 @@ function applyTelegramMiniAppDefaults(env, sourceEnv = process.env) {
1740
1766
  );
1741
1767
 
1742
1768
  if (!env.TELEGRAM_UI_TUNNEL && !sourceEnv.TELEGRAM_UI_TUNNEL) {
1743
- env.TELEGRAM_UI_TUNNEL = "auto";
1769
+ env.TELEGRAM_UI_TUNNEL = "named";
1744
1770
  }
1745
1771
  if (!env.TELEGRAM_UI_ALLOW_UNSAFE && !sourceEnv.TELEGRAM_UI_ALLOW_UNSAFE) {
1746
1772
  env.TELEGRAM_UI_ALLOW_UNSAFE = "false";
1747
1773
  }
1774
+ if (
1775
+ !env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK
1776
+ && !sourceEnv.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK
1777
+ ) {
1778
+ env.TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK = "false";
1779
+ }
1780
+ if (
1781
+ !env.TELEGRAM_UI_FALLBACK_AUTH_ENABLED
1782
+ && !sourceEnv.TELEGRAM_UI_FALLBACK_AUTH_ENABLED
1783
+ ) {
1784
+ env.TELEGRAM_UI_FALLBACK_AUTH_ENABLED = "true";
1785
+ }
1786
+ if (
1787
+ !env.CLOUDFLARE_USERNAME_HOSTNAME_POLICY
1788
+ && !sourceEnv.CLOUDFLARE_USERNAME_HOSTNAME_POLICY
1789
+ ) {
1790
+ env.CLOUDFLARE_USERNAME_HOSTNAME_POLICY = "per-user-fixed";
1791
+ }
1748
1792
  return true;
1749
1793
  }
1750
1794
 
@@ -1872,6 +1916,57 @@ function normalizeSetupConfiguration({
1872
1916
  "internal",
1873
1917
  );
1874
1918
 
1919
+ env.VOICE_ENABLED = toBooleanEnvString(env.VOICE_ENABLED, true);
1920
+ env.VOICE_PROVIDER = normalizeEnum(
1921
+ env.VOICE_PROVIDER,
1922
+ ["auto", "openai", "azure", "claude", "gemini", "fallback"],
1923
+ "auto",
1924
+ );
1925
+ env.VOICE_MODEL =
1926
+ env.VOICE_MODEL || "gpt-4o-realtime-preview-2024-12-17";
1927
+ env.VOICE_VISION_MODEL =
1928
+ env.VOICE_VISION_MODEL || "gpt-4.1-mini";
1929
+ env.VOICE_ID = normalizeEnum(
1930
+ env.VOICE_ID,
1931
+ [
1932
+ "alloy",
1933
+ "ash",
1934
+ "ballad",
1935
+ "coral",
1936
+ "echo",
1937
+ "fable",
1938
+ "onyx",
1939
+ "nova",
1940
+ "sage",
1941
+ "shimmer",
1942
+ "verse",
1943
+ ],
1944
+ "alloy",
1945
+ );
1946
+ env.VOICE_TURN_DETECTION = normalizeEnum(
1947
+ env.VOICE_TURN_DETECTION,
1948
+ ["server_vad", "semantic_vad", "none"],
1949
+ "server_vad",
1950
+ );
1951
+ env.VOICE_FALLBACK_MODE = normalizeEnum(
1952
+ env.VOICE_FALLBACK_MODE,
1953
+ ["browser", "disabled"],
1954
+ "browser",
1955
+ );
1956
+ env.VOICE_DELEGATE_EXECUTOR = normalizeEnum(
1957
+ env.VOICE_DELEGATE_EXECUTOR,
1958
+ [
1959
+ "codex-sdk",
1960
+ "copilot-sdk",
1961
+ "claude-sdk",
1962
+ "gemini-sdk",
1963
+ "opencode-sdk",
1964
+ ],
1965
+ env.PRIMARY_AGENT || "codex-sdk",
1966
+ );
1967
+ env.AZURE_OPENAI_REALTIME_DEPLOYMENT =
1968
+ env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-4o-realtime-preview";
1969
+
1875
1970
  env.CODEX_MODEL_PROFILE = normalizeEnum(
1876
1971
  env.CODEX_MODEL_PROFILE,
1877
1972
  ["xl", "m"],
@@ -1940,6 +2035,11 @@ function normalizeSetupConfiguration({
1940
2035
  ["sdk", "auto", "cli"],
1941
2036
  "sdk",
1942
2037
  );
2038
+ env.GEMINI_TRANSPORT = normalizeEnum(
2039
+ env.GEMINI_TRANSPORT || process.env.GEMINI_TRANSPORT,
2040
+ ["sdk", "auto", "cli"],
2041
+ "auto",
2042
+ );
1943
2043
 
1944
2044
  env.WHATSAPP_ENABLED = toBooleanEnvString(env.WHATSAPP_ENABLED, false);
1945
2045
 
@@ -1983,7 +2083,13 @@ function normalizeSetupConfiguration({
1983
2083
  {
1984
2084
  const primaryExec = configJson.executors.find((e) => e.role === "primary");
1985
2085
  if (primaryExec) {
1986
- const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk", OPENCODE: "opencode-sdk" };
2086
+ const sdkMap = {
2087
+ CODEX: "codex-sdk",
2088
+ COPILOT: "copilot-sdk",
2089
+ CLAUDE: "claude-sdk",
2090
+ GEMINI: "gemini-sdk",
2091
+ OPENCODE: "opencode-sdk",
2092
+ };
1987
2093
  env.PRIMARY_AGENT = env.PRIMARY_AGENT ||
1988
2094
  sdkMap[String(primaryExec.executor).toUpperCase()] || "codex-sdk";
1989
2095
  }
@@ -2709,6 +2815,8 @@ async function main() {
2709
2815
  "Copilot + Codex (50/50 split)",
2710
2816
  "Copilot only (Claude Opus 4.6)",
2711
2817
  "Claude only (direct API)",
2818
+ "Gemini only (direct SDK + CLI fallback)",
2819
+ "Gemini + Codex (60/40 split)",
2712
2820
  "OpenCode only (local OpenCode server)",
2713
2821
  "OpenCode + Codex (60/40 split)",
2714
2822
  "Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
@@ -2719,6 +2827,8 @@ async function main() {
2719
2827
  "Copilot + Codex (50/50 split)",
2720
2828
  "Copilot only (Claude Opus 4.6)",
2721
2829
  "Claude only (direct API)",
2830
+ "Gemini only (direct SDK + CLI fallback)",
2831
+ "Gemini + Codex (60/40 split)",
2722
2832
  "OpenCode only (local OpenCode server)",
2723
2833
  "OpenCode + Codex (60/40 split)",
2724
2834
  "Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
@@ -2731,8 +2841,29 @@ async function main() {
2731
2841
  );
2732
2842
 
2733
2843
  const presetNames = isAdvancedSetup
2734
- ? ["codex-only", "copilot-codex", "copilot-only", "claude-only", "opencode-only", "opencode-codex", "triple", "custom"]
2735
- : ["codex-only", "copilot-codex", "copilot-only", "claude-only", "opencode-only", "opencode-codex", "triple"];
2844
+ ? [
2845
+ "codex-only",
2846
+ "copilot-codex",
2847
+ "copilot-only",
2848
+ "claude-only",
2849
+ "gemini-only",
2850
+ "gemini-codex",
2851
+ "opencode-only",
2852
+ "opencode-codex",
2853
+ "triple",
2854
+ "custom",
2855
+ ]
2856
+ : [
2857
+ "codex-only",
2858
+ "copilot-codex",
2859
+ "copilot-only",
2860
+ "claude-only",
2861
+ "gemini-only",
2862
+ "gemini-codex",
2863
+ "opencode-only",
2864
+ "opencode-codex",
2865
+ "triple",
2866
+ ];
2736
2867
  const presetKey = presetNames[presetIdx] || "codex-only";
2737
2868
 
2738
2869
  if (presetKey === "custom") {
@@ -2857,6 +2988,12 @@ async function main() {
2857
2988
  if (!usedSdks.has("CODEX")) { env.CODEX_SDK_DISABLED = "true"; } else { delete env.CODEX_SDK_DISABLED; }
2858
2989
  if (!usedSdks.has("COPILOT")) { env.COPILOT_SDK_DISABLED = "true"; } else { delete env.COPILOT_SDK_DISABLED; }
2859
2990
  if (!usedSdks.has("CLAUDE")) { env.CLAUDE_SDK_DISABLED = "true"; } else { delete env.CLAUDE_SDK_DISABLED; }
2991
+ if (!usedSdks.has("GEMINI")) { env.GEMINI_SDK_DISABLED = "true"; } else { delete env.GEMINI_SDK_DISABLED; }
2992
+ if (!usedSdks.has("OPENCODE")) {
2993
+ env.OPENCODE_SDK_DISABLED = "true";
2994
+ } else {
2995
+ delete env.OPENCODE_SDK_DISABLED;
2996
+ }
2860
2997
 
2861
2998
  if (isAdvancedSetup) {
2862
2999
  console.log();
@@ -2884,6 +3021,20 @@ async function main() {
2884
3021
  );
2885
3022
  if (!wantClaudeFallback) env.CLAUDE_SDK_DISABLED = "true";
2886
3023
  else delete env.CLAUDE_SDK_DISABLED;
3024
+
3025
+ const wantGeminiFallback = usedSdks.has("GEMINI") || await prompt.confirm(
3026
+ "Enable Gemini SDK fallback? (requires GEMINI_API_KEY or GOOGLE_API_KEY)",
3027
+ !!process.env.GEMINI_API_KEY || !!process.env.GOOGLE_API_KEY,
3028
+ );
3029
+ if (!wantGeminiFallback) env.GEMINI_SDK_DISABLED = "true";
3030
+ else delete env.GEMINI_SDK_DISABLED;
3031
+
3032
+ const wantOpenCodeFallback = usedSdks.has("OPENCODE") || await prompt.confirm(
3033
+ "Enable OpenCode SDK fallback? (requires opencode binary on PATH)",
3034
+ false,
3035
+ );
3036
+ if (!wantOpenCodeFallback) env.OPENCODE_SDK_DISABLED = "true";
3037
+ else delete env.OPENCODE_SDK_DISABLED;
2887
3038
  }
2888
3039
  saveSetupSnapshot(4, "Executor / Agent Configuration", env, configJson);
2889
3040
  } // end step 4
@@ -2906,6 +3057,8 @@ async function main() {
2906
3057
  const needsCodexSdk = usedSdks.has("CODEX") || env.CODEX_SDK_DISABLED !== "true";
2907
3058
  const needsCopilotSdk = usedSdks.has("COPILOT") || env.COPILOT_SDK_DISABLED !== "true";
2908
3059
  const needsClaudeSdk = usedSdks.has("CLAUDE") || env.CLAUDE_SDK_DISABLED !== "true";
3060
+ const needsGeminiSdk = usedSdks.has("GEMINI") || env.GEMINI_SDK_DISABLED !== "true";
3061
+ const needsOpencodeSdk = usedSdks.has("OPENCODE") || env.OPENCODE_SDK_DISABLED !== "true";
2909
3062
 
2910
3063
  // ── 5a. Copilot / GitHub Token ──────────────────────
2911
3064
  if (needsCopilotSdk) {
@@ -3031,6 +3184,168 @@ async function main() {
3031
3184
  // Codex not needed — skip OpenAI key prompts entirely
3032
3185
  info("Codex SDK not in executor preset — skipping OpenAI configuration.");
3033
3186
  }
3187
+
3188
+ // ── 5d. Gemini / Google AI Key ────────────────────────
3189
+ if (needsGeminiSdk) {
3190
+ console.log(chalk.bold("\n Gemini SDK") + chalk.dim(" (uses Google AI API key)\n"));
3191
+ const existingGeminiKey =
3192
+ process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || "";
3193
+ if (existingGeminiKey) {
3194
+ info(`Gemini API key detected (${existingGeminiKey.slice(0, 8)}…). Gemini SDK will use it.`);
3195
+ } else {
3196
+ const geminiKey = await prompt.ask(
3197
+ "Gemini API Key (GEMINI_API_KEY / GOOGLE_API_KEY, blank to skip)",
3198
+ "",
3199
+ );
3200
+ if (geminiKey) {
3201
+ env.GEMINI_API_KEY = geminiKey;
3202
+ }
3203
+ }
3204
+ env.GEMINI_MODEL =
3205
+ env.GEMINI_MODEL ||
3206
+ process.env.GEMINI_MODEL ||
3207
+ "gemini-2.5-pro";
3208
+ const geminiTransportIdx = await prompt.choose(
3209
+ "Gemini transport mode:",
3210
+ [
3211
+ "auto (recommended: SDK first, CLI fallback)",
3212
+ "sdk only",
3213
+ "cli only",
3214
+ ],
3215
+ 0,
3216
+ );
3217
+ env.GEMINI_TRANSPORT =
3218
+ geminiTransportIdx === 1
3219
+ ? "sdk"
3220
+ : geminiTransportIdx === 2
3221
+ ? "cli"
3222
+ : "auto";
3223
+ if (env.GEMINI_TRANSPORT !== "sdk") {
3224
+ const cliPath = await prompt.ask(
3225
+ "Gemini CLI binary path (GEMINI_CLI_PATH, leave blank for default 'gemini')",
3226
+ process.env.GEMINI_CLI_PATH || "",
3227
+ );
3228
+ if (cliPath) {
3229
+ env.GEMINI_CLI_PATH = cliPath;
3230
+ }
3231
+ }
3232
+ } else {
3233
+ info("Gemini SDK not in executor preset — skipping Gemini configuration.");
3234
+ }
3235
+
3236
+ // ── 5e. OpenCode local server ─────────────────────────
3237
+ if (needsOpencodeSdk) {
3238
+ console.log(chalk.bold("\n OpenCode SDK") + chalk.dim(" (uses local opencode server)\n"));
3239
+ env.OPENCODE_PORT = String(
3240
+ toPositiveInt(
3241
+ env.OPENCODE_PORT || process.env.OPENCODE_PORT || "4096",
3242
+ 4096,
3243
+ ),
3244
+ );
3245
+ env.OPENCODE_MODEL =
3246
+ env.OPENCODE_MODEL ||
3247
+ process.env.OPENCODE_MODEL ||
3248
+ "gpt-5.2-codex";
3249
+ env.OPENCODE_TIMEOUT_MS = String(
3250
+ toPositiveInt(
3251
+ env.OPENCODE_TIMEOUT_MS || process.env.OPENCODE_TIMEOUT_MS || "3600000",
3252
+ 3600000,
3253
+ ),
3254
+ );
3255
+ info(
3256
+ `OpenCode defaults: OPENCODE_PORT=${env.OPENCODE_PORT}, OPENCODE_MODEL=${env.OPENCODE_MODEL}, OPENCODE_TIMEOUT_MS=${env.OPENCODE_TIMEOUT_MS}`,
3257
+ );
3258
+ } else {
3259
+ info("OpenCode SDK not in executor preset — skipping OpenCode configuration.");
3260
+ }
3261
+
3262
+ // ── 5f. Voice Mode ─────────────────────────────────────
3263
+ console.log(chalk.bold("\n Voice Mode") + chalk.dim(" (Realtime voice assistant)\n"));
3264
+ const enableVoice = await prompt.confirm(
3265
+ "Enable voice mode in the UI?",
3266
+ parseBooleanEnvValue(process.env.VOICE_ENABLED, true),
3267
+ );
3268
+ env.VOICE_ENABLED = enableVoice ? "true" : "false";
3269
+ if (enableVoice) {
3270
+ const providerIdx = await prompt.choose(
3271
+ "Voice provider:",
3272
+ [
3273
+ "auto (recommended)",
3274
+ "openai",
3275
+ "azure",
3276
+ "claude (fallback voice + Claude vision)",
3277
+ "gemini (fallback voice + Gemini vision)",
3278
+ "fallback (browser speech only)",
3279
+ ],
3280
+ 0,
3281
+ );
3282
+ const providerOptions = ["auto", "openai", "azure", "claude", "gemini", "fallback"];
3283
+ env.VOICE_PROVIDER = providerOptions[providerIdx] || "auto";
3284
+
3285
+ env.VOICE_ID = await prompt.ask(
3286
+ "Voice ID",
3287
+ process.env.VOICE_ID || "alloy",
3288
+ );
3289
+ env.VOICE_MODEL = await prompt.ask(
3290
+ "Realtime voice model",
3291
+ process.env.VOICE_MODEL || "gpt-4o-realtime-preview-2024-12-17",
3292
+ );
3293
+ env.VOICE_VISION_MODEL = await prompt.ask(
3294
+ "Vision model for camera/screen analysis",
3295
+ process.env.VOICE_VISION_MODEL || "gpt-4.1-mini",
3296
+ );
3297
+ env.VOICE_TURN_DETECTION = await prompt.ask(
3298
+ "Turn detection (server_vad|semantic_vad|none)",
3299
+ process.env.VOICE_TURN_DETECTION || "server_vad",
3300
+ );
3301
+ env.VOICE_FALLBACK_MODE = await prompt.ask(
3302
+ "Fallback mode (browser|disabled)",
3303
+ process.env.VOICE_FALLBACK_MODE || "browser",
3304
+ );
3305
+ env.VOICE_DELEGATE_EXECUTOR = await prompt.ask(
3306
+ "Delegate executor (codex-sdk|copilot-sdk|claude-sdk|gemini-sdk|opencode-sdk)",
3307
+ process.env.VOICE_DELEGATE_EXECUTOR || env.PRIMARY_AGENT || "codex-sdk",
3308
+ );
3309
+ if (env.VOICE_PROVIDER === "openai" || env.VOICE_PROVIDER === "auto") {
3310
+ env.OPENAI_REALTIME_API_KEY = await prompt.ask(
3311
+ "OpenAI Realtime key (OPENAI_REALTIME_API_KEY, blank uses OPENAI_API_KEY)",
3312
+ process.env.OPENAI_REALTIME_API_KEY || env.OPENAI_API_KEY || "",
3313
+ );
3314
+ }
3315
+ if (env.VOICE_PROVIDER === "azure" || env.VOICE_PROVIDER === "auto") {
3316
+ env.AZURE_OPENAI_REALTIME_ENDPOINT = await prompt.ask(
3317
+ "Azure Realtime endpoint (AZURE_OPENAI_REALTIME_ENDPOINT)",
3318
+ process.env.AZURE_OPENAI_REALTIME_ENDPOINT || process.env.AZURE_OPENAI_ENDPOINT || "",
3319
+ );
3320
+ env.AZURE_OPENAI_REALTIME_API_KEY = await prompt.ask(
3321
+ "Azure Realtime key (AZURE_OPENAI_REALTIME_API_KEY, blank uses AZURE_OPENAI_API_KEY)",
3322
+ process.env.AZURE_OPENAI_REALTIME_API_KEY || process.env.AZURE_OPENAI_API_KEY || "",
3323
+ );
3324
+ env.AZURE_OPENAI_REALTIME_DEPLOYMENT = await prompt.ask(
3325
+ "Azure Realtime deployment (AZURE_OPENAI_REALTIME_DEPLOYMENT)",
3326
+ process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-4o-realtime-preview",
3327
+ );
3328
+ }
3329
+ if (env.VOICE_PROVIDER === "claude" && !env.ANTHROPIC_API_KEY) {
3330
+ env.ANTHROPIC_API_KEY = await prompt.ask(
3331
+ "Anthropic API key for Claude voice/vision fallback (ANTHROPIC_API_KEY)",
3332
+ process.env.ANTHROPIC_API_KEY || "",
3333
+ );
3334
+ }
3335
+ if (
3336
+ env.VOICE_PROVIDER === "gemini" &&
3337
+ !env.GEMINI_API_KEY &&
3338
+ !env.GOOGLE_API_KEY
3339
+ ) {
3340
+ const geminiVoiceKey = await prompt.ask(
3341
+ "Gemini API key for Gemini voice/vision fallback (GEMINI_API_KEY or GOOGLE_API_KEY)",
3342
+ process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || "",
3343
+ );
3344
+ if (geminiVoiceKey) {
3345
+ env.GEMINI_API_KEY = geminiVoiceKey;
3346
+ }
3347
+ }
3348
+ }
3034
3349
  saveSetupSnapshot(5, "AI Provider Keys", env, configJson);
3035
3350
  } // end step 5
3036
3351
 
@@ -3226,7 +3541,7 @@ async function main() {
3226
3541
  env.PROJECT_NAME || configJson.projectName || "Unknown",
3227
3542
  );
3228
3543
  const testMsg =
3229
- "🤖 <b>Telegram Bot Test</b>\n\n" +
3544
+ ":bot: <b>Telegram Bot Test</b>\n\n" +
3230
3545
  "Your bosun Telegram bot is configured correctly!\n\n" +
3231
3546
  `Project: ${projectLabel}\n` +
3232
3547
  "Try: /status, /tasks, /help";
@@ -5106,6 +5421,25 @@ async function runNonInteractive({
5106
5421
  env.GITHUB_REPO = env.GITHUB_REPOSITORY;
5107
5422
  }
5108
5423
  env.OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
5424
+ env.VOICE_ENABLED = process.env.VOICE_ENABLED || "true";
5425
+ env.VOICE_PROVIDER = process.env.VOICE_PROVIDER || "auto";
5426
+ env.VOICE_MODEL =
5427
+ process.env.VOICE_MODEL || "gpt-4o-realtime-preview-2024-12-17";
5428
+ env.VOICE_VISION_MODEL = process.env.VOICE_VISION_MODEL || "gpt-4.1-mini";
5429
+ env.OPENAI_REALTIME_API_KEY = process.env.OPENAI_REALTIME_API_KEY || "";
5430
+ env.AZURE_OPENAI_REALTIME_ENDPOINT =
5431
+ process.env.AZURE_OPENAI_REALTIME_ENDPOINT || "";
5432
+ env.AZURE_OPENAI_REALTIME_API_KEY =
5433
+ process.env.AZURE_OPENAI_REALTIME_API_KEY || "";
5434
+ env.AZURE_OPENAI_REALTIME_DEPLOYMENT =
5435
+ process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-4o-realtime-preview";
5436
+ env.VOICE_ID = process.env.VOICE_ID || "alloy";
5437
+ env.VOICE_TURN_DETECTION = process.env.VOICE_TURN_DETECTION || "server_vad";
5438
+ env.VOICE_FALLBACK_MODE = process.env.VOICE_FALLBACK_MODE || "browser";
5439
+ env.VOICE_DELEGATE_EXECUTOR =
5440
+ process.env.VOICE_DELEGATE_EXECUTOR ||
5441
+ process.env.PRIMARY_AGENT ||
5442
+ "codex-sdk";
5109
5443
  env.CODEX_MODEL_PROFILE = process.env.CODEX_MODEL_PROFILE || "xl";
5110
5444
  env.CODEX_MODEL_PROFILE_SUBAGENT =
5111
5445
  process.env.CODEX_MODEL_PROFILE_SUBAGENT ||
@@ -5191,6 +5525,8 @@ async function runNonInteractive({
5191
5525
  // Smart default: pick preset based on available API keys
5192
5526
  const hasOpenAI = !!process.env.OPENAI_API_KEY;
5193
5527
  const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
5528
+ const hasGemini =
5529
+ !!process.env.GEMINI_API_KEY || !!process.env.GOOGLE_API_KEY;
5194
5530
  const hasGitHub = !!process.env.GITHUB_TOKEN || !!process.env.COPILOT_CLI_TOKEN;
5195
5531
  const presetEnv = (process.env.EXECUTOR_PRESET || "").toLowerCase();
5196
5532
  if (presetEnv && EXECUTOR_PRESETS[presetEnv]) {
@@ -5201,6 +5537,10 @@ async function runNonInteractive({
5201
5537
  : EXECUTOR_PRESETS["copilot-only"];
5202
5538
  } else if (hasAnthropic) {
5203
5539
  configJson.executors = EXECUTOR_PRESETS["claude-only"];
5540
+ } else if (hasGemini) {
5541
+ configJson.executors = hasOpenAI
5542
+ ? EXECUTOR_PRESETS["gemini-codex"]
5543
+ : EXECUTOR_PRESETS["gemini-only"];
5204
5544
  } else {
5205
5545
  configJson.executors = EXECUTOR_PRESETS["codex-only"];
5206
5546
  }
@@ -5212,7 +5552,13 @@ async function runNonInteractive({
5212
5552
  (e) => e.role === "primary",
5213
5553
  );
5214
5554
  if (primaryExec) {
5215
- const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk", OPENCODE: "opencode-sdk" };
5555
+ const sdkMap = {
5556
+ CODEX: "codex-sdk",
5557
+ COPILOT: "copilot-sdk",
5558
+ CLAUDE: "claude-sdk",
5559
+ GEMINI: "gemini-sdk",
5560
+ OPENCODE: "opencode-sdk",
5561
+ };
5216
5562
  env.PRIMARY_AGENT = sdkMap[String(primaryExec.executor).toUpperCase()] || "codex-sdk";
5217
5563
  }
5218
5564
  }
@@ -5321,7 +5667,7 @@ async function runNonInteractive({
5321
5667
  if (r.success) {
5322
5668
  info(` ✓ Workspace repo ready: ${r.name}`);
5323
5669
  } else {
5324
- warn(` Workspace repo ${r.name}: ${r.error}`);
5670
+ warn(` :alert: Workspace repo ${r.name}: ${r.error}`);
5325
5671
  }
5326
5672
  }
5327
5673
  // Set BOSUN_AGENT_REPO_ROOT to workspace primary repo
@@ -5580,7 +5926,7 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
5580
5926
  " ╔═══════════════════════════════════════════════════════════════╗",
5581
5927
  );
5582
5928
  console.log(
5583
- " ║ Setup Complete! ║",
5929
+ " ║ :check: Setup Complete! ║",
5584
5930
  );
5585
5931
  console.log(
5586
5932
  " ╚═══════════════════════════════════════════════════════════════╝",
@@ -5621,6 +5967,15 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
5621
5967
  ) {
5622
5968
  info("No API key set — AI analysis & autofix will be disabled.");
5623
5969
  }
5970
+ if (
5971
+ !env.GEMINI_API_KEY &&
5972
+ !env.GOOGLE_API_KEY &&
5973
+ !process.env.GEMINI_API_KEY &&
5974
+ !process.env.GOOGLE_API_KEY &&
5975
+ !parseBooleanEnvValue(env.GEMINI_SDK_DISABLED, false)
5976
+ ) {
5977
+ info("No Gemini key set — Gemini SDK will be unavailable until configured.");
5978
+ }
5624
5979
 
5625
5980
  console.log("");
5626
5981
  console.log(chalk.bold(" Next steps:"));
@@ -400,7 +400,7 @@ export function getKnowledgeState() {
400
400
 
401
401
  export function formatKnowledgeSummary() {
402
402
  return [
403
- `📚 Shared Knowledge: ${knowledgeState.entriesWritten} entries written this session`,
403
+ `:u1f4da: Shared Knowledge: ${knowledgeState.entriesWritten} entries written this session`,
404
404
  `Target: ${knowledgeState.targetFile}`,
405
405
  `Dedup cache: ${knowledgeState.entryHashes.size} hashes`,
406
406
  knowledgeState.lastWriteAt
@@ -344,7 +344,7 @@ async function installWindows(options = {}) {
344
344
 
345
345
  // Strategy 2: Elevate via UAC prompt
346
346
  console.log(
347
- " ℹ️ Admin access required — requesting elevation (UAC prompt)...",
347
+ " :help: Admin access required — requesting elevation (UAC prompt)...",
348
348
  );
349
349
 
350
350
  // Delete + Create via elevated process
@@ -370,7 +370,7 @@ async function installWindows(options = {}) {
370
370
 
371
371
  // Strategy 3: Fall back to Startup folder (no admin needed)
372
372
  console.log(
373
- " ⚠️ Task Scheduler elevation failed — falling back to Startup folder.",
373
+ " :alert: Task Scheduler elevation failed — falling back to Startup folder.",
374
374
  );
375
375
  console.log(
376
376
  " (Startup folder works without admin, but has no auto-restart on failure)",
@@ -394,7 +394,7 @@ async function removeWindows() {
394
394
 
395
395
  if (isAccessDenied) {
396
396
  console.log(
397
- " ℹ️ Admin access required — requesting elevation (UAC prompt)...",
397
+ " :help: Admin access required — requesting elevation (UAC prompt)...",
398
398
  );
399
399
  const elevated = runElevated(`/Delete /TN "${TASK_NAME}" /F`);
400
400
  results.push({
@@ -543,7 +543,7 @@ async function installMacOS(options = {}) {
543
543
  }
544
544
 
545
545
  // Try with sudo — prompts for password in terminal via osascript or direct sudo
546
- console.log(" ℹ️ Permission required — requesting sudo access...");
546
+ console.log(" :help: Permission required — requesting sudo access...");
547
547
  try {
548
548
  // Write plist to temp location first
549
549
  const tmpPlist = resolve(__dirname, ".cache", `${SERVICE_LABEL}.plist`);
@@ -614,7 +614,7 @@ async function removeMacOS() {
614
614
  }
615
615
 
616
616
  // Elevate via osascript
617
- console.log(" ℹ️ Permission required — requesting sudo access...");
617
+ console.log(" :help: Permission required — requesting sudo access...");
618
618
  try {
619
619
  const escapedPlistPath = plistPath.replace(/'/g, "'\\''");
620
620
  const script = `do shell script "launchctl unload '${escapedPlistPath}' 2>/dev/null; rm -f '${escapedPlistPath}'" with administrator privileges`;
@@ -752,7 +752,7 @@ async function installLinux(options = {}) {
752
752
  }
753
753
 
754
754
  // Try with sudo for the systemctl commands (unit file is user-space)
755
- console.log(" ℹ️ Permission required — trying sudo...");
755
+ console.log(" :help: Permission required — trying sudo...");
756
756
  try {
757
757
  // The unit file write doesn't need sudo (it's in ~/.config)
758
758
  // but systemctl might if the session isn't fully initialized
@@ -772,13 +772,13 @@ async function installLinux(options = {}) {
772
772
  };
773
773
  } catch (sudoErr) {
774
774
  console.log(
775
- " ⚠️ systemd with sudo failed — falling back to crontab.",
775
+ " :alert: systemd with sudo failed — falling back to crontab.",
776
776
  );
777
777
  // Fall through to crontab
778
778
  }
779
779
  }
780
780
  } else {
781
- console.log(" ℹ️ systemd user session not available — using crontab.");
781
+ console.log(" :help: systemd user session not available — using crontab.");
782
782
  }
783
783
 
784
784
  // Strategy 2: crontab @reboot fallback (works everywhere, no root needed)
@@ -865,7 +865,7 @@ async function removeLinux() {
865
865
  err.message?.includes("EACCES");
866
866
 
867
867
  if (isPermission) {
868
- console.log(" ℹ️ Permission required — trying sudo...");
868
+ console.log(" :help: Permission required — trying sudo...");
869
869
  try {
870
870
  execSync(`sudo systemctl --user stop ${SYSTEMD_UNIT}`, {
871
871
  stdio: "inherit",