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.
- package/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/kanban-adapter.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/library-manager.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
],
|
package/setup-web-server.mjs
CHANGED
|
@@ -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: "
|
|
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
|
-
"
|
|
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
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
|
|
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
|
-
//
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
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
|
-
"
|
|
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 || "
|
|
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 || "
|
|
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 ||
|