bosun 0.37.0 → 0.37.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 (43) hide show
  1. package/.env.example +4 -1
  2. package/agent-tool-config.mjs +338 -0
  3. package/bosun-skills.mjs +59 -4
  4. package/bosun.schema.json +1 -1
  5. package/desktop/launch.mjs +18 -0
  6. package/desktop/main.mjs +52 -13
  7. package/fleet-coordinator.mjs +34 -1
  8. package/kanban-adapter.mjs +30 -3
  9. package/library-manager.mjs +66 -0
  10. package/maintenance.mjs +30 -5
  11. package/monitor.mjs +56 -0
  12. package/package.json +4 -1
  13. package/setup-web-server.mjs +73 -12
  14. package/setup.mjs +3 -3
  15. package/ui/app.js +40 -3
  16. package/ui/components/session-list.js +25 -7
  17. package/ui/components/workspace-switcher.js +48 -1
  18. package/ui/demo.html +176 -0
  19. package/ui/modules/mic-track-registry.js +83 -0
  20. package/ui/modules/settings-schema.js +4 -1
  21. package/ui/modules/state.js +25 -0
  22. package/ui/modules/streaming.js +1 -1
  23. package/ui/modules/voice-barge-in.js +27 -0
  24. package/ui/modules/voice-client-sdk.js +268 -42
  25. package/ui/modules/voice-client.js +665 -61
  26. package/ui/modules/voice-overlay.js +829 -47
  27. package/ui/setup.html +151 -9
  28. package/ui/styles.css +258 -0
  29. package/ui/tabs/chat.js +11 -0
  30. package/ui/tabs/library.js +890 -15
  31. package/ui/tabs/settings.js +51 -11
  32. package/ui/tabs/telemetry.js +327 -105
  33. package/ui/tabs/workflows.js +86 -0
  34. package/ui-server.mjs +1201 -107
  35. package/voice-action-dispatcher.mjs +81 -0
  36. package/voice-agents-sdk.mjs +2 -2
  37. package/voice-relay.mjs +131 -14
  38. package/voice-tools.mjs +475 -9
  39. package/workflow-engine.mjs +54 -0
  40. package/workflow-nodes.mjs +177 -28
  41. package/workflow-templates/github.mjs +205 -94
  42. package/workflow-templates/task-batch.mjs +247 -0
  43. package/workflow-templates.mjs +15 -0
@@ -5823,6 +5823,23 @@ export function getKanbanBackendName() {
5823
5823
  // Convenience exports: direct task operations via active adapter
5824
5824
  // ---------------------------------------------------------------------------
5825
5825
 
5826
+ // ── Workflow Event Bridge (lazy-loaded from monitor.mjs) ──────────────────
5827
+ let _kanbanQueueWorkflowEvent = null;
5828
+ function emitKanbanEvent(eventType, eventData = {}) {
5829
+ if (!_kanbanQueueWorkflowEvent) {
5830
+ import("./monitor.mjs")
5831
+ .then((mod) => {
5832
+ if (typeof mod.queueWorkflowEvent === "function") {
5833
+ _kanbanQueueWorkflowEvent = mod.queueWorkflowEvent;
5834
+ _kanbanQueueWorkflowEvent(eventType, eventData);
5835
+ }
5836
+ })
5837
+ .catch(() => {});
5838
+ return;
5839
+ }
5840
+ _kanbanQueueWorkflowEvent(eventType, eventData);
5841
+ }
5842
+
5826
5843
  export async function listProjects() {
5827
5844
  return getKanbanAdapter().listProjects();
5828
5845
  }
@@ -5836,7 +5853,9 @@ export async function getTask(taskId) {
5836
5853
  }
5837
5854
 
5838
5855
  export async function updateTaskStatus(taskId, status, options) {
5839
- return getKanbanAdapter().updateTaskStatus(taskId, status, options);
5856
+ const result = await getKanbanAdapter().updateTaskStatus(taskId, status, options);
5857
+ emitKanbanEvent("task.status_updated", { taskId, status, options });
5858
+ return result;
5840
5859
  }
5841
5860
 
5842
5861
  export async function updateTask(taskId, patch) {
@@ -5851,11 +5870,19 @@ export async function updateTask(taskId, patch) {
5851
5870
  }
5852
5871
 
5853
5872
  export async function createTask(projectId, taskData) {
5854
- return getKanbanAdapter().createTask(projectId, taskData);
5873
+ const result = await getKanbanAdapter().createTask(projectId, taskData);
5874
+ emitKanbanEvent("task.created", {
5875
+ projectId,
5876
+ taskId: result?.id || null,
5877
+ title: taskData?.title || null,
5878
+ });
5879
+ return result;
5855
5880
  }
5856
5881
 
5857
5882
  export async function deleteTask(taskId) {
5858
- return getKanbanAdapter().deleteTask(taskId);
5883
+ const result = await getKanbanAdapter().deleteTask(taskId);
5884
+ emitKanbanEvent("task.deleted", { taskId });
5885
+ return result;
5859
5886
  }
5860
5887
 
5861
5888
  export async function addComment(taskId, body) {
@@ -102,6 +102,8 @@ function nowISO() {
102
102
  * @property {string[]} [skills] - skill library refs to inject
103
103
  * @property {Object} [hookProfile] - hook profile overrides
104
104
  * @property {Object} [env] - extra env vars for the agent
105
+ * @property {string[]} [enabledTools] - list of tool IDs enabled for this agent (null = all)
106
+ * @property {string[]} [enabledMcpServers] - list of MCP server IDs enabled for this agent
105
107
  */
106
108
 
107
109
  /**
@@ -631,6 +633,7 @@ export const BUILTIN_AGENT_PROFILES = [
631
633
  hookProfile: null,
632
634
  env: {},
633
635
  tags: ["ui", "frontend", "portal", "css", "web"],
636
+ agentType: "task",
634
637
  },
635
638
  {
636
639
  id: "backend-agent",
@@ -645,6 +648,7 @@ export const BUILTIN_AGENT_PROFILES = [
645
648
  hookProfile: null,
646
649
  env: {},
647
650
  tags: ["api", "server", "backend", "database"],
651
+ agentType: "task",
648
652
  },
649
653
  {
650
654
  id: "devops-agent",
@@ -659,6 +663,7 @@ export const BUILTIN_AGENT_PROFILES = [
659
663
  hookProfile: null,
660
664
  env: {},
661
665
  tags: ["ci", "cd", "build", "deploy", "infra", "devops"],
666
+ agentType: "task",
662
667
  },
663
668
  {
664
669
  id: "docs-agent",
@@ -673,6 +678,7 @@ export const BUILTIN_AGENT_PROFILES = [
673
678
  hookProfile: null,
674
679
  env: {},
675
680
  tags: ["docs", "documentation", "readme", "markdown"],
681
+ agentType: "task",
676
682
  },
677
683
  {
678
684
  id: "test-agent",
@@ -687,6 +693,66 @@ export const BUILTIN_AGENT_PROFILES = [
687
693
  hookProfile: null,
688
694
  env: {},
689
695
  tags: ["test", "testing", "e2e", "unit", "coverage"],
696
+ agentType: "task",
697
+ },
698
+ {
699
+ id: "voice-agent-female",
700
+ name: "Voice Agent (Female)",
701
+ description: "Conversational voice specialist with concise guidance and call-friendly pacing.",
702
+ titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
703
+ scopes: ["voice", "assistant"],
704
+ sdk: null,
705
+ model: null,
706
+ promptOverride: null,
707
+ skills: ["concise-voice-guidance", "conversation-memory"],
708
+ hookProfile: null,
709
+ env: {},
710
+ tags: ["voice", "assistant", "realtime", "female", "default", "audio-agent"],
711
+ agentType: "voice",
712
+ voiceAgent: true,
713
+ voicePersona: "female",
714
+ voiceInstructions: "You are Nova, a female voice agent. Be concise, warm, and practical. Use tools for facts and execution. Keep spoken responses short and clear.",
715
+ enabledTools: null,
716
+ enabledMcpServers: [],
717
+ },
718
+ {
719
+ id: "voice-agent-male",
720
+ name: "Voice Agent (Male)",
721
+ description: "Operational voice specialist focused on diagnostics and execution.",
722
+ titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
723
+ scopes: ["voice", "assistant"],
724
+ sdk: null,
725
+ model: null,
726
+ promptOverride: null,
727
+ skills: ["ops-diagnostics", "task-execution"],
728
+ hookProfile: null,
729
+ env: {},
730
+ tags: ["voice", "assistant", "realtime", "male", "default", "audio-agent"],
731
+ agentType: "voice",
732
+ voiceAgent: true,
733
+ voicePersona: "male",
734
+ voiceInstructions: "You are Atlas, a male voice agent. Be direct and execution-oriented. Prefer actionable status updates. Use tools proactively for diagnostics.",
735
+ enabledTools: null,
736
+ enabledMcpServers: [],
737
+ },
738
+ {
739
+ id: "voice-agent",
740
+ name: "Voice Agent",
741
+ description: "Default voice assistant agent. Handles real-time voice sessions, tool calls, and delegate orchestration. Customize tools and MCP servers for voice interactions.",
742
+ titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
743
+ scopes: ["voice", "assistant"],
744
+ sdk: null,
745
+ model: null,
746
+ promptOverride: null,
747
+ skills: [],
748
+ hookProfile: null,
749
+ env: {},
750
+ tags: ["voice", "assistant", "realtime", "default"],
751
+ agentType: "voice",
752
+ voiceAgent: true,
753
+ voicePersona: "neutral",
754
+ enabledTools: null,
755
+ enabledMcpServers: [],
690
756
  },
691
757
  ];
692
758
 
package/maintenance.mjs CHANGED
@@ -28,6 +28,26 @@ import {
28
28
  } from "./worktree-manager.mjs";
29
29
 
30
30
  const isWindows = process.platform === "win32";
31
+
32
+ // ── Workflow event bridge ─────────────────────────────────────────────────
33
+ // Lazy-loaded to avoid circular imports (monitor → maintenance → monitor).
34
+ let _queueWorkflowEvent = null;
35
+ function emitMaintenanceEvent(eventType, data = {}) {
36
+ if (_queueWorkflowEvent) {
37
+ _queueWorkflowEvent(eventType, data);
38
+ return;
39
+ }
40
+ import("./monitor.mjs")
41
+ .then((mod) => {
42
+ if (typeof mod.queueWorkflowEvent === "function") {
43
+ _queueWorkflowEvent = mod.queueWorkflowEvent;
44
+ _queueWorkflowEvent(eventType, data);
45
+ }
46
+ })
47
+ .catch(() => {
48
+ /* best-effort — monitor not available during tests */
49
+ });
50
+ }
31
51
  const BRANCH_SYNC_LOG_THROTTLE_MS = Math.max(
32
52
  5_000,
33
53
  Number(process.env.BRANCH_SYNC_LOG_THROTTLE_MS || "300000") || 300000,
@@ -1318,11 +1338,7 @@ export async function runMaintenanceSweep(opts = {}) {
1318
1338
  /* best-effort */
1319
1339
  }
1320
1340
 
1321
- console.log(
1322
- `[maintenance] sweep complete: ${staleKilled} stale orchestrators, ${pushesReaped} stuck pushes, ${worktreesPruned} worktrees pruned, ${branchesSynced} branches synced, ${branchesDeleted} stale branches deleted`,
1323
- );
1324
-
1325
- return {
1341
+ const result = {
1326
1342
  staleKilled,
1327
1343
  pushesReaped,
1328
1344
  worktreesPruned,
@@ -1330,4 +1346,13 @@ export async function runMaintenanceSweep(opts = {}) {
1330
1346
  tasksArchived,
1331
1347
  branchesDeleted,
1332
1348
  };
1349
+
1350
+ console.log(
1351
+ `[maintenance] sweep complete: ${staleKilled} stale orchestrators, ${pushesReaped} stuck pushes, ${worktreesPruned} worktrees pruned, ${branchesSynced} branches synced, ${branchesDeleted} stale branches deleted`,
1352
+ );
1353
+
1354
+ // Emit workflow event so event-driven workflows can react to sweep results
1355
+ emitMaintenanceEvent("maintenance.sweep_complete", result);
1356
+
1357
+ return result;
1333
1358
  }
package/monitor.mjs CHANGED
@@ -12235,12 +12235,66 @@ runGuarded("startup-maintenance-sweep", () =>
12235
12235
  safeSetInterval("flush-error-queue", () => flushErrorQueue(), 60 * 1000);
12236
12236
 
12237
12237
  // ── Periodic maintenance: every 5 min, reap stuck pushes & prune worktrees ──
12238
+ // If a workflow replaces maintenance.mjs, skip the direct call and let
12239
+ // the workflow engine handle it via trigger.schedule evaluation.
12238
12240
  const maintenanceIntervalMs = 5 * 60 * 1000;
12239
12241
  safeSetInterval("maintenance-sweep", () => {
12242
+ if (isWorkflowReplacingModule("maintenance.mjs")) return;
12240
12243
  const childPid = currentChild ? currentChild.pid : undefined;
12241
12244
  return runMaintenanceSweep({ repoRoot, childPid });
12242
12245
  }, maintenanceIntervalMs);
12243
12246
 
12247
+ // ── Workflow schedule trigger polling ───────────────────────────────────────
12248
+ // Check all installed workflows that use trigger.schedule and fire any whose
12249
+ // interval has elapsed. This makes schedule-based templates (workspace hygiene,
12250
+ // nightly reports, etc.) actually execute without hardcoded safeSetInterval calls.
12251
+ const scheduleCheckIntervalMs = 60 * 1000; // check every 60s
12252
+ safeSetInterval("workflow-schedule-check", async () => {
12253
+ try {
12254
+ const engine = await ensureWorkflowAutomationEngine();
12255
+ if (!engine?.evaluateScheduleTriggers) return;
12256
+
12257
+ const triggered = engine.evaluateScheduleTriggers();
12258
+ if (!Array.isArray(triggered) || triggered.length === 0) return;
12259
+
12260
+ for (const match of triggered) {
12261
+ const workflowId = String(match?.workflowId || "").trim();
12262
+ if (!workflowId) continue;
12263
+ void engine
12264
+ .execute(workflowId, {
12265
+ _triggerSource: "schedule-poll",
12266
+ _triggeredBy: match?.triggeredBy || null,
12267
+ _lastRunAt: Date.now(),
12268
+ repoRoot,
12269
+ })
12270
+ .then((ctx) => {
12271
+ const runId = ctx?.id || "unknown";
12272
+ const runStatus =
12273
+ Array.isArray(ctx?.errors) && ctx.errors.length > 0
12274
+ ? "failed"
12275
+ : "completed";
12276
+ console.log(
12277
+ `[workflows] schedule-run ${runStatus} workflow=${workflowId} runId=${runId}`,
12278
+ );
12279
+ })
12280
+ .catch((err) => {
12281
+ console.warn(
12282
+ `[workflows] schedule-run failed workflow=${workflowId}: ${err?.message || err}`,
12283
+ );
12284
+ });
12285
+ }
12286
+
12287
+ if (triggered.length > 0) {
12288
+ console.log(
12289
+ `[workflows] schedule poll triggered ${triggered.length} workflow run(s)`,
12290
+ );
12291
+ }
12292
+ } catch (err) {
12293
+ // Schedule evaluation must not crash the monitor
12294
+ console.warn(`[workflows] schedule-check error: ${err?.message || err}`);
12295
+ }
12296
+ }, scheduleCheckIntervalMs);
12297
+
12244
12298
  // ── Periodic merged PR check: every 10 min, move merged PRs to done ─────────
12245
12299
  const mergedPRCheckIntervalMs = 10 * 60 * 1000;
12246
12300
  safeSetInterval("merged-pr-check", () => checkMergedPRsAndUpdateTasks(), mergedPRCheckIntervalMs);
@@ -13307,4 +13361,6 @@ export {
13307
13361
  // Container runner re-exports
13308
13362
  getContainerStatus,
13309
13363
  isContainerEnabled,
13364
+ // Workflow event bridge — for fleet/kanban modules to emit events
13365
+ queueWorkflowEvent,
13310
13366
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.37.0",
3
+ "version": "0.37.2",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -62,6 +62,7 @@
62
62
  "./agent-hooks": "./agent-hooks.mjs",
63
63
  "./hook-profiles": "./hook-profiles.mjs",
64
64
  "./agent-hook-bridge": "./agent-hook-bridge.mjs",
65
+ "./agent-tool-config": "./agent-tool-config.mjs",
65
66
  "./startup-service": "./startup-service.mjs",
66
67
  "./telegram-sentinel": "./telegram-sentinel.mjs",
67
68
  "./whatsapp-channel": "./whatsapp-channel.mjs",
@@ -240,6 +241,7 @@
240
241
  "agent-hooks.mjs",
241
242
  "hook-profiles.mjs",
242
243
  "agent-hook-bridge.mjs",
244
+ "agent-tool-config.mjs",
243
245
  "agent-supervisor.mjs",
244
246
  "agent-work-analyzer.mjs",
245
247
  "startup-service.mjs",
@@ -260,6 +262,7 @@
260
262
  "workflow-templates/planning.mjs",
261
263
  "workflow-templates/reliability.mjs",
262
264
  "workflow-templates/security.mjs",
265
+ "workflow-templates/task-batch.mjs",
263
266
  "workflow-templates/task-lifecycle.mjs",
264
267
  "ui/vendor/"
265
268
  ],
@@ -439,9 +439,12 @@ function buildStableSetupDefaults({
439
439
  voiceModel: "gpt-audio-1.5",
440
440
  voiceVisionModel: "gpt-4.1-nano",
441
441
  voiceId: "alloy",
442
- voiceTurnDetection: "server_vad",
442
+ voiceTurnDetection: "semantic_vad",
443
443
  voiceFallbackMode: "browser",
444
444
  voiceDelegateExecutor: "codex-sdk",
445
+ voiceTranscriptionEnabled: true,
446
+ voiceTranscriptionModel: "gpt-4o-transcribe",
447
+ voiceAzureTranscriptionEnabled: false,
445
448
  openaiRealtimeApiKey: "",
446
449
  azureOpenaiRealtimeEndpoint: "",
447
450
  azureOpenaiRealtimeApiKey: "",
@@ -892,7 +895,7 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
892
895
  sourceEnv.VOICE_TURN_DETECTION,
893
896
  ),
894
897
  ["server_vad", "semantic_vad", "none"],
895
- "server_vad",
898
+ "semantic_vad",
896
899
  );
897
900
  envMap.VOICE_FALLBACK_MODE = normalizeEnumValue(
898
901
  pickNonEmptyValue(
@@ -914,6 +917,32 @@ function applyNonBlockingSetupEnvDefaults(envMap, env = {}, sourceEnv = process.
914
917
  ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"],
915
918
  "codex-sdk",
916
919
  );
920
+ envMap.VOICE_TRANSCRIPTION_ENABLED = toBooleanEnvString(
921
+ pickNonEmptyValue(
922
+ env.voiceTranscriptionEnabled,
923
+ env.VOICE_TRANSCRIPTION_ENABLED,
924
+ envMap.VOICE_TRANSCRIPTION_ENABLED,
925
+ sourceEnv.VOICE_TRANSCRIPTION_ENABLED,
926
+ ),
927
+ true,
928
+ );
929
+ envMap.VOICE_TRANSCRIPTION_MODEL = String(
930
+ pickNonEmptyValue(
931
+ env.voiceTranscriptionModel,
932
+ env.VOICE_TRANSCRIPTION_MODEL,
933
+ envMap.VOICE_TRANSCRIPTION_MODEL,
934
+ sourceEnv.VOICE_TRANSCRIPTION_MODEL,
935
+ ) || "gpt-4o-transcribe",
936
+ ).trim() || "gpt-4o-transcribe";
937
+ envMap.VOICE_AZURE_TRANSCRIPTION_ENABLED = toBooleanEnvString(
938
+ pickNonEmptyValue(
939
+ env.voiceAzureTranscriptionEnabled,
940
+ env.VOICE_AZURE_TRANSCRIPTION_ENABLED,
941
+ envMap.VOICE_AZURE_TRANSCRIPTION_ENABLED,
942
+ sourceEnv.VOICE_AZURE_TRANSCRIPTION_ENABLED,
943
+ ),
944
+ false,
945
+ );
917
946
 
918
947
  const openaiRealtimeApiKey = pickNonEmptyValue(
919
948
  env.openaiRealtimeApiKey,
@@ -1481,14 +1510,12 @@ async function handleVoiceEndpointTest(body) {
1481
1510
  // Strip path suffix so users can paste full URLs without double-path 404s.
1482
1511
  let base = String(azureEndpoint).replace(/\/+$/, "");
1483
1512
  try { const u = new URL(base); base = `${u.protocol}//${u.host}`; } catch { /* keep as-is */ }
1484
- // Single-deployment GET only requires Cognitive Services User role.
1485
- // Use the GA api-version (2024-10-21) for broad compatibility across
1486
- // classic Azure OpenAI and Azure AI Foundry resources.
1487
- const dep = String(deployment || "").trim();
1488
- testUrl = dep
1489
- ? `${base}/openai/deployments/${encodeURIComponent(dep)}?api-version=2024-10-21`
1490
- : `${base}/openai/models?api-version=2024-10-21`;
1513
+ // Prefer /openai/models as a credential check it works on both classic
1514
+ // Azure OpenAI resources AND Azure AI Foundry "Global Standard" deployments.
1515
+ // If a deployment name is provided, we verify it separately after confirming
1516
+ // the endpoint + key are valid.
1491
1517
  headers["api-key"] = apiKey;
1518
+ testUrl = `${base}/openai/models?api-version=2024-10-21`;
1492
1519
  } else if (normalizedProvider === "claude") {
1493
1520
  testUrl = "https://api.anthropic.com/v1/models";
1494
1521
  headers["anthropic-version"] = "2023-06-01";
@@ -1525,6 +1552,36 @@ async function handleVoiceEndpointTest(body) {
1525
1552
  clearTimeout(timer);
1526
1553
  const latencyMs = Date.now() - start;
1527
1554
  if (resp.ok || resp.status === 200) {
1555
+ // For Azure with a deployment name, do a secondary check to verify the
1556
+ // deployment exists. We try chat/completions with max_tokens=1 — realtime
1557
+ // deployments return 400 (wrong endpoint type) which still confirms existence.
1558
+ if (normalizedProvider === "azure" && deployment) {
1559
+ const dep = String(deployment).trim();
1560
+ try {
1561
+ let base = String(azureEndpoint).replace(/\/+$/, "");
1562
+ try { const u = new URL(base); base = `${u.protocol}//${u.host}`; } catch { /* keep */ }
1563
+ const depUrl = `${base}/openai/deployments/${encodeURIComponent(dep)}/chat/completions?api-version=2024-10-21`;
1564
+ const depCtrl = new AbortController();
1565
+ const depTimer = setTimeout(() => depCtrl.abort(), 8_000);
1566
+ const depResp = await fetch(depUrl, {
1567
+ method: "POST",
1568
+ headers: { ...headers, "Content-Type": "application/json" },
1569
+ body: JSON.stringify({ messages: [{ role: "user", content: "test" }], max_tokens: 1 }),
1570
+ signal: depCtrl.signal,
1571
+ });
1572
+ clearTimeout(depTimer);
1573
+ // 200 = chat model works, 400 = realtime model (expected), both confirm deployment exists
1574
+ if (depResp.ok || depResp.status === 400) {
1575
+ return { ok: true, latencyMs, deployment: dep };
1576
+ }
1577
+ if (depResp.status === 404) {
1578
+ return { ok: false, error: `Credentials valid but deployment "${dep}" not found — check the deployment name in Azure AI Foundry`, latencyMs };
1579
+ }
1580
+ return { ok: true, latencyMs, warning: `Credentials valid. Could not verify deployment "${dep}" (HTTP ${depResp.status})` };
1581
+ } catch {
1582
+ return { ok: true, latencyMs, warning: `Credentials valid. Could not verify deployment "${dep}" (timeout)` };
1583
+ }
1584
+ }
1528
1585
  return { ok: true, latencyMs };
1529
1586
  }
1530
1587
  const text = await resp.text().catch(() => "");
@@ -1535,9 +1592,13 @@ async function handleVoiceEndpointTest(body) {
1535
1592
  } catch {
1536
1593
  // Keep generic HTTP status message.
1537
1594
  }
1538
- // Friendly message when the deployment name itself is not found (key is fine)
1539
- if (resp.status === 404 && deployment) {
1540
- error = `Deployment "${deployment}" not found check deployment name in Azure AI Foundry`;
1595
+ // Azure-specific: provide helpful messages for common errors
1596
+ if (normalizedProvider === "azure") {
1597
+ if (resp.status === 401 || resp.status === 403) {
1598
+ error = `Authentication failed (HTTP ${resp.status}) — check API key and endpoint URL`;
1599
+ } else if (resp.status === 404) {
1600
+ error = `Endpoint not found (HTTP 404) — check the Azure endpoint URL. Use https://<resource>.openai.azure.com`;
1601
+ }
1541
1602
  }
1542
1603
  return { ok: false, error, latencyMs };
1543
1604
  } catch (err) {
package/setup.mjs CHANGED
@@ -1955,7 +1955,7 @@ function normalizeSetupConfiguration({
1955
1955
  env.VOICE_TURN_DETECTION = normalizeEnum(
1956
1956
  env.VOICE_TURN_DETECTION,
1957
1957
  ["server_vad", "semantic_vad", "none"],
1958
- "server_vad",
1958
+ "semantic_vad",
1959
1959
  );
1960
1960
  env.VOICE_FALLBACK_MODE = normalizeEnum(
1961
1961
  env.VOICE_FALLBACK_MODE,
@@ -3305,7 +3305,7 @@ async function main() {
3305
3305
  );
3306
3306
  env.VOICE_TURN_DETECTION = await prompt.ask(
3307
3307
  "Turn detection (server_vad|semantic_vad|none)",
3308
- process.env.VOICE_TURN_DETECTION || "server_vad",
3308
+ process.env.VOICE_TURN_DETECTION || "semantic_vad",
3309
3309
  );
3310
3310
  env.VOICE_FALLBACK_MODE = await prompt.ask(
3311
3311
  "Fallback mode (browser|disabled)",
@@ -5659,7 +5659,7 @@ async function runNonInteractive({
5659
5659
  env.AZURE_OPENAI_REALTIME_DEPLOYMENT =
5660
5660
  process.env.AZURE_OPENAI_REALTIME_DEPLOYMENT || "gpt-realtime-1.5";
5661
5661
  env.VOICE_ID = process.env.VOICE_ID || "alloy";
5662
- env.VOICE_TURN_DETECTION = process.env.VOICE_TURN_DETECTION || "server_vad";
5662
+ env.VOICE_TURN_DETECTION = process.env.VOICE_TURN_DETECTION || "semantic_vad";
5663
5663
  env.VOICE_FALLBACK_MODE = process.env.VOICE_FALLBACK_MODE || "browser";
5664
5664
  env.VOICE_DELEGATE_EXECUTOR =
5665
5665
  process.env.VOICE_DELEGATE_EXECUTOR ||