vora-ai 0.1.34 → 0.1.36

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 (32) hide show
  1. package/dist/.buildstamp +1 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  4. package/dist/cli-startup-metadata.json +1 -1
  5. package/dist/{command-registry-CJzgMEiu.js → command-registry-C-dQzQLt.js} +4 -4
  6. package/dist/{command-registry-BErLBmAn.js → command-registry-D5bgvc-g.js} +1 -1
  7. package/dist/completion-cli-BcvhPH1f.js +2 -0
  8. package/dist/{completion-cli-DOqvgg4j.js → completion-cli-CuDyNItM.js} +2 -2
  9. package/dist/{doctor-completion-CHyxjRnq.js → doctor-completion-1du2om2X.js} +1 -1
  10. package/dist/entry.js +2 -2
  11. package/dist/extensions/telegram/.vora-runtime-deps-stamp.json +1 -1
  12. package/dist/{gateway-cli-MWd326Fi.js → gateway-cli-D3jVKXtU.js} +1 -1
  13. package/dist/{help-BWGrT64x.js → help-BX3qk2j9.js} +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/{onboard-Mcuz9Qtv.js → onboard-DwchOLHk.js} +1 -1
  16. package/dist/{program-C1NP1wtT.js → program-Dah2D_cO.js} +2 -2
  17. package/dist/{prompt-select-styled-Cnschuoa.js → prompt-select-styled-fmGo-EJl.js} +1 -1
  18. package/dist/{register.maintenance-Crkq06lG.js → register.maintenance-BHLVtWhx.js} +1 -1
  19. package/dist/{register.onboard-BLyf9Ec0.js → register.onboard-BQXwigRU.js} +1 -1
  20. package/dist/{register.setup-DWHzXiXK.js → register.setup-BdrATrzp.js} +1 -1
  21. package/dist/{register.subclis-Cdt5rvRo.js → register.subclis-CrmcN8zA.js} +2 -2
  22. package/dist/{register.subclis-_jS15b16.js → register.subclis-Dx_SLWv5.js} +6 -6
  23. package/dist/{root-help-Tv4Oc4kX.js → root-help-DDj9BENB.js} +2 -2
  24. package/dist/{run-main-CaN20YYg.js → run-main-C6NIk22l.js} +5 -5
  25. package/dist/{setup-cXX6csIV.js → setup-CEe7DH30.js} +3 -3
  26. package/dist/{setup.finalize-DPRk6z4s.js → setup.finalize-tiOrCMkC.js} +4 -4
  27. package/dist/{subcli-descriptors-Cc423xKz.js → subcli-descriptors-BTTrqeCm.js} +1 -1
  28. package/dist/{update-cli-DXdoaDuF.js → update-cli-Dmyo-EkW.js} +3 -3
  29. package/dist/{voice-cli-BL4zwKEh.js → voice-cli-BD1lHjLd.js} +132 -62
  30. package/package.json +1 -1
  31. package/scripts/agora-stt-bridge.mjs +166 -10
  32. package/dist/completion-cli-D9cFhAqm.js +0 -2
package/dist/.buildstamp CHANGED
@@ -1 +1 @@
1
- {"builtAt":1776445600460,"head":"d10ac9af191c960d501998893b76031b50a877e1"}
1
+ {"builtAt":1776448382056,"head":"936286ec3e03bbfaa83f10717f1ccf01e29ff447"}
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.34",
3
- "commit": "d10ac9af191c960d501998893b76031b50a877e1",
4
- "builtAt": "2026-04-17T17:06:41.214Z"
2
+ "version": "0.1.36",
3
+ "commit": "936286ec3e03bbfaa83f10717f1ccf01e29ff447",
4
+ "builtAt": "2026-04-17T17:53:02.809Z"
5
5
  }
@@ -1 +1 @@
1
- 9b90cfd1a4773b5b89afdfa4a8c3553e1f52129d6604f2eed5074cb0678a4f3f
1
+ e41bbc2c2a8641d063e7675b117011f54b721f5d55b3b94967fee8da0da6a761
@@ -10,5 +10,5 @@
10
10
  "signal",
11
11
  "imessage"
12
12
  ],
13
- "rootHelpText": "\n🌊 VORA 0.1.34 (d10ac9a) — VORA runtime: Voice operations running flawlessly.\n\nUsage: vora [options] [command]\n\nOptions:\n --container <name> Run the CLI inside a running Podman/Docker container\n named <name> (default: env VORA_CONTAINER)\n --dev Dev profile: isolate state under ~/.vora-dev, default\n gateway port 19001, and shift derived ports\n (browser/canvas)\n -h, --help Display help for command\n --log-level <level> Global log level override for file + console\n (silent|fatal|error|warn|info|debug|trace)\n --no-color Disable ANSI colors\n --profile <name> Use a named profile (isolates\n VORA_STATE_DIR/VORA_CONFIG_PATH under ~/.vora-<name>)\n -V, --version output the version number\n\nCommands:\n Hint: commands suffixed with * have subcommands. Run <command> --help for details.\n acp * Agent Control Protocol tools\n agent Run one agent turn via the Gateway\n agents * Manage isolated agents (workspaces, auth, routing)\n approvals * Manage exec approvals (gateway or node host)\n backup * Create and verify local backup archives for Vora state\n channels * Manage connected chat channels (Telegram, Discord, etc.)\n clawbot * Legacy clawbot command aliases\n completion Generate shell completion script\n config * Non-interactive config helpers\n (get/set/unset/file/validate). Default: starts guided\n setup.\n configure Interactive configuration for credentials, channels,\n gateway, and agent defaults\n cron * Manage cron jobs via the Gateway scheduler\n daemon * Gateway service (legacy alias)\n dashboard Open the Control UI with your current token\n devices * Device pairing + token management\n directory * Lookup contact and group IDs (self, peers, groups) for\n supported chat channels\n dns * DNS helpers for wide-area discovery (Tailscale + CoreDNS)\n docs Search the live Vora docs\n doctor Health checks + quick fixes for the gateway and channels\n gateway * Run, inspect, and query the WebSocket Gateway\n health Fetch health from the running gateway\n help Display help for command\n hooks * Manage internal agent hooks\n logs Tail gateway file logs via RPC\n message * Send, read, and manage messages\n models * Discover, scan, and configure models\n node * Run and manage the headless node host service\n nodes * Manage gateway-owned node pairing and node commands\n onboard Interactive onboarding for gateway, workspace, and skills\n pairing * Secure DM pairing (approve inbound requests)\n plugins * Manage Vora plugins and extensions\n qr Generate iOS pairing QR/setup code\n reset Reset local config/state (keeps the CLI installed)\n sandbox * Manage sandbox containers for agent isolation\n secrets * Secrets runtime reload controls\n security * Security tools and local config audits\n sessions * List stored conversation sessions\n setup Initialize local config and agent workspace\n skills * List and inspect available skills\n status Show channel health and recent session recipients\n system * System events, heartbeat, and presence\n tasks * Inspect durable background task state\n tui Open a terminal UI connected to the Gateway\n uninstall Uninstall the gateway service + local data (CLI remains)\n update * Update Vora and inspect update channel status\n voice * Wake-word terminal voice loop (OpenWakeWord trigger + STT\n bridge + Gateway chat + optional Hume TTS)\n webhooks * Webhook helpers and integrations\n\nExamples:\n vora models --help\n Show detailed help for the models command.\n vora onboard\n Run interactive onboarding for the gateway, workspace, and skills.\n vora configure --section model --section gateway\n Re-open only the model and gateway configuration sections.\n vora gateway --port 27106\n Run the WebSocket Gateway locally.\n vora --dev gateway\n Run a dev Gateway (isolated state/config) on ws://127.0.0.1:19001.\n vora gateway --force\n Kill anything bound to the default gateway port, then start it.\n vora models status --plain\n Show the configured provider and default-model state.\n vora gateway ...\n Gateway control via WebSocket.\n vora message send --channel telegram --target @mychat --message \"Hi\"\n Send through a configured channel and print the result in the terminal.\n\nDocs: https://docs.vora.ai/cli\n\n"
13
+ "rootHelpText": "\n🌊 VORA 0.1.36 (936286e) — Listening out for 'Hey Vora'...\n\nUsage: vora [options] [command]\n\nOptions:\n --container <name> Run the CLI inside a running Podman/Docker container\n named <name> (default: env VORA_CONTAINER)\n --dev Dev profile: isolate state under ~/.vora-dev, default\n gateway port 19001, and shift derived ports\n (browser/canvas)\n -h, --help Display help for command\n --log-level <level> Global log level override for file + console\n (silent|fatal|error|warn|info|debug|trace)\n --no-color Disable ANSI colors\n --profile <name> Use a named profile (isolates\n VORA_STATE_DIR/VORA_CONFIG_PATH under ~/.vora-<name>)\n -V, --version output the version number\n\nCommands:\n Hint: commands suffixed with * have subcommands. Run <command> --help for details.\n acp * Agent Control Protocol tools\n agent Run one agent turn via the Gateway\n agents * Manage isolated agents (workspaces, auth, routing)\n approvals * Manage exec approvals (gateway or node host)\n backup * Create and verify local backup archives for Vora state\n channels * Manage connected chat channels (Telegram, Discord, etc.)\n clawbot * Legacy clawbot command aliases\n completion Generate shell completion script\n config * Non-interactive config helpers\n (get/set/unset/file/validate). Default: starts guided\n setup.\n configure Interactive configuration for credentials, channels,\n gateway, and agent defaults\n cron * Manage cron jobs via the Gateway scheduler\n daemon * Gateway service (legacy alias)\n dashboard Open the Control UI with your current token\n devices * Device pairing + token management\n directory * Lookup contact and group IDs (self, peers, groups) for\n supported chat channels\n dns * DNS helpers for wide-area discovery (Tailscale + CoreDNS)\n docs Search the live Vora docs\n doctor Health checks + quick fixes for the gateway and channels\n gateway * Run, inspect, and query the WebSocket Gateway\n health Fetch health from the running gateway\n help Display help for command\n hooks * Manage internal agent hooks\n logs Tail gateway file logs via RPC\n message * Send, read, and manage messages\n models * Discover, scan, and configure models\n node * Run and manage the headless node host service\n nodes * Manage gateway-owned node pairing and node commands\n onboard Interactive onboarding for gateway, workspace, and skills\n pairing * Secure DM pairing (approve inbound requests)\n plugins * Manage Vora plugins and extensions\n qr Generate iOS pairing QR/setup code\n reset Reset local config/state (keeps the CLI installed)\n sandbox * Manage sandbox containers for agent isolation\n secrets * Secrets runtime reload controls\n security * Security tools and local config audits\n sessions * List stored conversation sessions\n setup Initialize local config and agent workspace\n skills * List and inspect available skills\n status Show channel health and recent session recipients\n system * System events, heartbeat, and presence\n tasks * Inspect durable background task state\n tui Open a terminal UI connected to the Gateway\n uninstall Uninstall the gateway service + local data (CLI remains)\n update * Update Vora and inspect update channel status\n voice * Wake-word terminal voice loop (OpenWakeWord trigger + STT\n bridge + Gateway chat + optional ElevenLabs TTS)\n webhooks * Webhook helpers and integrations\n\nExamples:\n vora models --help\n Show detailed help for the models command.\n vora onboard\n Run interactive onboarding for the gateway, workspace, and skills.\n vora configure --section model --section gateway\n Re-open only the model and gateway configuration sections.\n vora gateway --port 27106\n Run the WebSocket Gateway locally.\n vora --dev gateway\n Run a dev Gateway (isolated state/config) on ws://127.0.0.1:19001.\n vora gateway --force\n Kill anything bound to the default gateway port, then start it.\n vora models status --plain\n Show the configured provider and default-model state.\n vora gateway ...\n Gateway control via WebSocket.\n vora message send --channel telegram --target @mychat --message \"Hi\"\n Send through a configured channel and print the result in the terminal.\n\nDocs: https://docs.vora.ai/cli\n\n"
14
14
  }
@@ -1,7 +1,7 @@
1
1
  import { O as hasHelpOrVersion, T as getPrimaryCommand } from "./logger-DtBbg3AQ.js";
2
2
  import { n as removeCommandByName, t as registerLazyCommand } from "./register-lazy-command-C4_WvYdL.js";
3
3
  import { t as getCoreCliCommandDescriptors } from "./core-command-descriptors-Dmh3xVCU.js";
4
- import { i as registerSubCliCommands } from "./register.subclis-_jS15b16.js";
4
+ import { i as registerSubCliCommands } from "./register.subclis-Dx_SLWv5.js";
5
5
  //#region src/cli/program/command-registry.ts
6
6
  const shouldRegisterCorePrimaryOnly = (argv) => {
7
7
  if (hasHelpOrVersion(argv)) return false;
@@ -15,7 +15,7 @@ const coreEntries = [
15
15
  hasSubcommands: false
16
16
  }],
17
17
  register: async ({ program }) => {
18
- (await import("./register.setup-DWHzXiXK.js")).registerSetupCommand(program);
18
+ (await import("./register.setup-BdrATrzp.js")).registerSetupCommand(program);
19
19
  }
20
20
  },
21
21
  {
@@ -25,7 +25,7 @@ const coreEntries = [
25
25
  hasSubcommands: false
26
26
  }],
27
27
  register: async ({ program }) => {
28
- (await import("./register.onboard-BLyf9Ec0.js")).registerOnboardCommand(program);
28
+ (await import("./register.onboard-BQXwigRU.js")).registerOnboardCommand(program);
29
29
  }
30
30
  },
31
31
  {
@@ -82,7 +82,7 @@ const coreEntries = [
82
82
  }
83
83
  ],
84
84
  register: async ({ program }) => {
85
- (await import("./register.maintenance-Crkq06lG.js")).registerMaintenanceCommands(program);
85
+ (await import("./register.maintenance-BHLVtWhx.js")).registerMaintenanceCommands(program);
86
86
  }
87
87
  },
88
88
  {
@@ -1,3 +1,3 @@
1
1
  import "./core-command-descriptors-Dmh3xVCU.js";
2
- import { n as registerCoreCliByName } from "./command-registry-CJzgMEiu.js";
2
+ import { n as registerCoreCliByName } from "./command-registry-C-dQzQLt.js";
3
3
  export { registerCoreCliByName };
@@ -0,0 +1,2 @@
1
+ import { a as registerCompletionCli } from "./completion-cli-CuDyNItM.js";
2
+ export { registerCompletionCli };
@@ -3,8 +3,8 @@ import { t as formatDocsLink } from "./links-D-QW4VCX.js";
3
3
  import { r as theme } from "./theme-DEBOahQW.js";
4
4
  import { _ as resolveStateDir } from "./paths-5dqsPi5h.js";
5
5
  import { m as pathExists } from "./utils-D25qzO4S.js";
6
- import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName, t as getSubCliEntries } from "./register.subclis-_jS15b16.js";
7
- import { n as registerCoreCliByName, t as getCoreCliCommandNames } from "./command-registry-CJzgMEiu.js";
6
+ import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName, t as getSubCliEntries } from "./register.subclis-Dx_SLWv5.js";
7
+ import { n as registerCoreCliByName, t as getCoreCliCommandNames } from "./command-registry-C-dQzQLt.js";
8
8
  import { t as getProgramContext } from "./program-context-C0Ru__k1.js";
9
9
  import path from "node:path";
10
10
  import os from "node:os";
@@ -1,7 +1,7 @@
1
1
  import { t as resolveVoraPackageRoot } from "./vora-root-fqgy2cBi.js";
2
2
  import { n as resolveCliName } from "./cli-name-C_IzpPbS.js";
3
3
  import { t as note } from "./note-DfcrAbT0.js";
4
- import { c as usesSlowDynamicCompletion, i as isCompletionInstalled, o as resolveCompletionCachePath, r as installCompletion, s as resolveShellFromEnv, t as completionCacheExists } from "./completion-cli-DOqvgg4j.js";
4
+ import { c as usesSlowDynamicCompletion, i as isCompletionInstalled, o as resolveCompletionCachePath, r as installCompletion, s as resolveShellFromEnv, t as completionCacheExists } from "./completion-cli-CuDyNItM.js";
5
5
  import path from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
7
  //#region src/commands/doctor-completion.ts
package/dist/entry.js CHANGED
@@ -193,14 +193,14 @@ function tryHandleRootHelpFastPath(argv, deps = {}) {
193
193
  Promise.resolve().then(() => deps.outputRootHelp?.()).catch(handleError);
194
194
  return true;
195
195
  }
196
- import("./root-help-Tv4Oc4kX.js").then(({ outputRootHelp }) => {
196
+ import("./root-help-DDj9BENB.js").then(({ outputRootHelp }) => {
197
197
  return outputRootHelp();
198
198
  }).catch(handleError);
199
199
  return true;
200
200
  }
201
201
  function runMainOrRootHelp(argv) {
202
202
  if (tryHandleRootHelpFastPath(argv)) return;
203
- import("./run-main-CaN20YYg.js").then(({ runCli }) => runCli(argv)).catch((error) => {
203
+ import("./run-main-C6NIk22l.js").then(({ runCli }) => runCli(argv)).catch((error) => {
204
204
  console.error("[vora] Failed to start CLI:", error instanceof Error ? error.stack ?? error.message : error);
205
205
  process$1.exitCode = 1;
206
206
  });
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "fingerprint": "f5cc4428157244c03371a02e98bc2b0556e1853613db4dfe31efcfd24cef35f1",
3
- "generatedAt": "2026-04-17T17:06:40.329Z"
3
+ "generatedAt": "2026-04-17T17:53:01.949Z"
4
4
  }
@@ -196,7 +196,7 @@ import { s as normalizeUpdateChannel } from "./update-channels-C8-n6_kE.js";
196
196
  import { n as compareSemverStrings, o as resolveNpmChannelTag, t as checkUpdateStatus } from "./update-check-Cw2Zo7Rn.js";
197
197
  import { i as resolveGatewayStartupPluginIds, r as resolveConfiguredDeferredChannelPluginIds } from "./channel-plugin-ids-BgeBHwp9.js";
198
198
  import { l as startTaskRegistryMaintenance, n as getInspectableTaskRegistrySummary } from "./task-registry.maintenance-CfvslEdg.js";
199
- import { t as runSetupWizard } from "./setup-cXX6csIV.js";
199
+ import { t as runSetupWizard } from "./setup-CEe7DH30.js";
200
200
  import { _ as buildGogWatchStartArgs, g as buildGogWatchServeArgs, i as ensureTailscaleEndpoint, w as resolveGmailHookRuntimeConfig } from "./gmail-setup-utils-CstpQWwK.js";
201
201
  import { i as loadAgentIdentity, o as pruneAgentConfig, r as findAgentEntryIndex, t as applyAgentConfig } from "./agents.config-BXIFVvWT.js";
202
202
  import { a as resolveApnsAuthConfigFromEnv, c as shouldClearStoredApnsRegistration, d as MediaOffloadError, f as parseMessageWithAttachments, l as resolveApnsRelayConfigFromEnv, n as loadApnsRegistration, o as sendApnsAlert, r as normalizeApnsEnvironment, s as sendApnsBackgroundWake, t as clearApnsRegistrationIfCurrent, u as normalizeRpcAttachmentsToChatAttachments } from "./push-apns-wrSw6X5_.js";
@@ -6,7 +6,7 @@ import { n as resolveCliName, t as replaceCliName } from "./cli-name-C_IzpPbS.js
6
6
  import { n as resolveCommitHash } from "./git-commit-DifBr62H.js";
7
7
  import { i as hasEmittedCliBanner, r as formatCliBannerLine } from "./banner-C5yJCudk.js";
8
8
  import { n as getCoreCliCommandsWithSubcommands } from "./core-command-descriptors-Dmh3xVCU.js";
9
- import { t as getSubCliCommandsWithSubcommands } from "./subcli-descriptors-Cc423xKz.js";
9
+ import { t as getSubCliCommandsWithSubcommands } from "./subcli-descriptors-BTTrqeCm.js";
10
10
  import { InvalidArgumentError } from "commander";
11
11
  //#region src/cli/log-level-option.ts
12
12
  const CLI_LOG_LEVEL_VALUES = ALLOWED_LOG_LEVELS.join("|");
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ let saveSessionStore;
28
28
  let toWhatsappJid;
29
29
  let waitForever;
30
30
  async function loadLegacyCliDeps() {
31
- const [{ installGaxiosFetchCompat }, { runCli }] = await Promise.all([import("./gaxios-fetch-compat-BZRlVrBH.js"), import("./run-main-CaN20YYg.js")]);
31
+ const [{ installGaxiosFetchCompat }, { runCli }] = await Promise.all([import("./gaxios-fetch-compat-BZRlVrBH.js"), import("./run-main-C6NIk22l.js")]);
32
32
  return {
33
33
  installGaxiosFetchCompat,
34
34
  runCli
@@ -11,7 +11,7 @@ import { c as normalizeGatewayTokenInput, d as randomToken, f as resolveControlU
11
11
  import { n as logConfigUpdated } from "./logging-BQ-k0nK2.js";
12
12
  import { t as WizardCancelledError } from "./prompts-BVJ3zQFl.js";
13
13
  import { t as createClackPrompter } from "./clack-prompter-Be8sX0Lg.js";
14
- import { t as runSetupWizard } from "./setup-cXX6csIV.js";
14
+ import { t as runSetupWizard } from "./setup-CEe7DH30.js";
15
15
  import { a as resolveManifestProviderOnboardAuthFlags } from "./provider-auth-choices-DcNzlbgs.js";
16
16
  import { i as resolveDeprecatedAuthChoiceReplacement, n as isDeprecatedAuthChoice, r as normalizeLegacyOnboardAuthChoice, t as formatDeprecatedNonInteractiveAuthChoiceError } from "./auth-choice-legacy-DGeZIT-G.js";
17
17
  import { r as applyLocalSetupWorkspaceConfig } from "./onboard-config-DkkHPbTO.js";
@@ -7,12 +7,12 @@ import { n as VERSION } from "./version-A4Ns-pm9.js";
7
7
  import { n as resolveCliName } from "./cli-name-C_IzpPbS.js";
8
8
  import { t as emitCliBanner } from "./banner-C5yJCudk.js";
9
9
  import { n as resolveCliChannelOptions } from "./channel-options-D_8z6c-h.js";
10
- import { i as registerProgramCommands } from "./command-registry-CJzgMEiu.js";
10
+ import { i as registerProgramCommands } from "./command-registry-C-dQzQLt.js";
11
11
  import { n as setProgramContext } from "./program-context-C0Ru__k1.js";
12
12
  import { t as isCommandJsonOutputMode } from "./json-mode-KmFHsA3R.js";
13
13
  import "./ports-CQo1U86K.js";
14
14
  import { n as resolvePluginInstallPreactionRequest, t as resolvePluginInstallInvalidConfigPolicy } from "./plugin-install-config-policy-BHANXE7k.js";
15
- import { t as configureProgramHelp } from "./help-BWGrT64x.js";
15
+ import { t as configureProgramHelp } from "./help-BX3qk2j9.js";
16
16
  import { Command } from "commander";
17
17
  //#region src/cli/program/context.ts
18
18
  function createProgramContext() {
@@ -108,7 +108,7 @@ import { t as ensureSystemdUserLingerInteractive } from "./systemd-linger-B_gzrf
108
108
  import { t as formatHealthCheckFailure } from "./health-format-TyQPx9cW.js";
109
109
  import { a as stripUnknownConfigKeys, i as resolveConfigPathTarget, n as formatConfigPath, r as noteOpencodeProviderOverrides, t as runDoctorConfigPreflight } from "./doctor-config-preflight-B2IpAFkH.js";
110
110
  import { a as isMattermostMutableAllowEntry, i as isMSTeamsMutableAllowEntry, n as isGoogleChatMutableAllowEntry, o as isSlackMutableAllowEntry, r as isIrcMutableAllowEntry, s as isZalouserMutableGroupEntry, t as isDiscordMutableAllowEntry } from "./mutable-allowlist-detectors-zVflHd8P.js";
111
- import { n as doctorShellCompletion } from "./doctor-completion-CHyxjRnq.js";
111
+ import { n as doctorShellCompletion } from "./doctor-completion-1du2om2X.js";
112
112
  import { t as collectChannelStatusIssues } from "./channels-status-issues-CCJ10zgF.js";
113
113
  import { t as resolveDefaultChannelAccountContext } from "./channel-account-context-ChxhkByq.js";
114
114
  import "./doctor-state-migrations-BlH3irT5.js";
@@ -16,7 +16,7 @@ import { r as resolveGatewayService } from "./service-BnUY5mkU.js";
16
16
  import { n as resolveConfiguredSecretInputWithFallback } from "./resolve-configured-secret-input-string-BGHutVyf.js";
17
17
  import { n as runCommandWithRuntime } from "./cli-utils-4n6KI2ho.js";
18
18
  import { a as removePath, i as listAgentSessionDirs, o as removeStateAndLinkedPaths, r as buildCleanupPlan, s as removeWorkspaceDirs } from "./backup-create-DZood8Ea.js";
19
- import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-Cnschuoa.js";
19
+ import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-fmGo-EJl.js";
20
20
  import path from "node:path";
21
21
  import { cancel, confirm, isCancel, multiselect } from "@clack/prompts";
22
22
  //#region src/infra/clipboard.ts
@@ -4,7 +4,7 @@ import { r as theme } from "./theme-DEBOahQW.js";
4
4
  import { n as runCommandWithRuntime } from "./cli-utils-4n6KI2ho.js";
5
5
  import { a as resolveManifestProviderOnboardAuthFlags } from "./provider-auth-choices-DcNzlbgs.js";
6
6
  import { n as formatAuthChoiceChoicesForCli } from "./auth-choice-options-Blz8a0tL.js";
7
- import { n as CORE_ONBOARD_AUTH_FLAGS, t as setupWizardCommand } from "./onboard-Mcuz9Qtv.js";
7
+ import { n as CORE_ONBOARD_AUTH_FLAGS, t as setupWizardCommand } from "./onboard-DwchOLHk.js";
8
8
  //#region src/cli/program/register.onboard.ts
9
9
  function resolveInstallDaemonFlag(command, opts) {
10
10
  if (!command || typeof command !== "object") return;
@@ -11,7 +11,7 @@ import { s as resolveSessionTranscriptsDir } from "./paths-UyCuvwtc.js";
11
11
  import { n as safeParseWithSchema } from "./zod-parse-DHX0rEx9.js";
12
12
  import { n as runCommandWithRuntime } from "./cli-utils-4n6KI2ho.js";
13
13
  import { n as logConfigUpdated, t as formatConfigPath } from "./logging-BQ-k0nK2.js";
14
- import { t as setupWizardCommand } from "./onboard-Mcuz9Qtv.js";
14
+ import { t as setupWizardCommand } from "./onboard-DwchOLHk.js";
15
15
  import fs from "node:fs/promises";
16
16
  import { z } from "zod";
17
17
  import JSON5 from "json5";
@@ -1,3 +1,3 @@
1
- import "./subcli-descriptors-Cc423xKz.js";
2
- import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName } from "./register.subclis-_jS15b16.js";
1
+ import "./subcli-descriptors-BTTrqeCm.js";
2
+ import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName } from "./register.subclis-Dx_SLWv5.js";
3
3
  export { loadValidatedConfigForPluginRegistration, registerSubCliByName };
@@ -1,7 +1,7 @@
1
1
  import { O as hasHelpOrVersion, T as getPrimaryCommand } from "./logger-DtBbg3AQ.js";
2
2
  import { t as isTruthyEnvValue } from "./env-CLTF3G6u.js";
3
3
  import { n as removeCommandByName, t as registerLazyCommand$1 } from "./register-lazy-command-C4_WvYdL.js";
4
- import { n as getSubCliEntries$1 } from "./subcli-descriptors-Cc423xKz.js";
4
+ import { n as getSubCliEntries$1 } from "./subcli-descriptors-BTTrqeCm.js";
5
5
  //#region src/cli/program/register.subclis.ts
6
6
  const shouldRegisterPrimaryOnly = (argv) => {
7
7
  if (isTruthyEnvValue(process.env.VORA_DISABLE_LAZY_SUBCOMMANDS)) return false;
@@ -30,7 +30,7 @@ const entries = [
30
30
  description: "Run, inspect, and query the WebSocket Gateway",
31
31
  hasSubcommands: true,
32
32
  register: async (program) => {
33
- (await import("./gateway-cli-MWd326Fi.js")).registerGatewayCli(program);
33
+ (await import("./gateway-cli-D3jVKXtU.js")).registerGatewayCli(program);
34
34
  }
35
35
  },
36
36
  {
@@ -115,10 +115,10 @@ const entries = [
115
115
  },
116
116
  {
117
117
  name: "voice",
118
- description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional Hume TTS)",
118
+ description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional ElevenLabs TTS)",
119
119
  hasSubcommands: true,
120
120
  register: async (program) => {
121
- (await import("./voice-cli-BL4zwKEh.js")).registerVoiceCli(program);
121
+ (await import("./voice-cli-BD1lHjLd.js")).registerVoiceCli(program);
122
122
  }
123
123
  },
124
124
  {
@@ -244,7 +244,7 @@ const entries = [
244
244
  description: "Update Vora and inspect update channel status",
245
245
  hasSubcommands: true,
246
246
  register: async (program) => {
247
- (await import("./update-cli-DXdoaDuF.js")).registerUpdateCli(program);
247
+ (await import("./update-cli-Dmyo-EkW.js")).registerUpdateCli(program);
248
248
  }
249
249
  },
250
250
  {
@@ -252,7 +252,7 @@ const entries = [
252
252
  description: "Generate shell completion script",
253
253
  hasSubcommands: false,
254
254
  register: async (program) => {
255
- (await import("./completion-cli-D9cFhAqm.js")).registerCompletionCli(program);
255
+ (await import("./completion-cli-BcvhPH1f.js")).registerCompletionCli(program);
256
256
  }
257
257
  }
258
258
  ];
@@ -1,7 +1,7 @@
1
1
  import { n as VERSION } from "./version-A4Ns-pm9.js";
2
2
  import { t as getCoreCliCommandDescriptors } from "./core-command-descriptors-Dmh3xVCU.js";
3
- import { n as getSubCliEntries } from "./subcli-descriptors-Cc423xKz.js";
4
- import { t as configureProgramHelp } from "./help-BWGrT64x.js";
3
+ import { n as getSubCliEntries } from "./subcli-descriptors-BTTrqeCm.js";
4
+ import { t as configureProgramHelp } from "./help-BX3qk2j9.js";
5
5
  import { t as getPluginCliCommandDescriptors } from "./cli-CQycN9EN.js";
6
6
  import { Command } from "commander";
7
7
  //#region src/cli/program/root-help.ts
@@ -368,13 +368,13 @@ async function runCli(argv = process$1.argv) {
368
368
  assertSupportedRuntime();
369
369
  try {
370
370
  if (shouldUseRootHelpFastPath(normalizedArgv)) {
371
- const { outputRootHelp } = await import("./root-help-Tv4Oc4kX.js");
371
+ const { outputRootHelp } = await import("./root-help-DDj9BENB.js");
372
372
  await outputRootHelp();
373
373
  return;
374
374
  }
375
375
  if (await tryRouteCli(normalizedArgv)) return;
376
376
  enableConsoleCapture();
377
- const { buildProgram } = await import("./program-C1NP1wtT.js");
377
+ const { buildProgram } = await import("./program-Dah2D_cO.js");
378
378
  const program = buildProgram();
379
379
  const { installUnhandledRejectionHandler } = await import("./unhandled-rejections-D0HD8aaS.js");
380
380
  installUnhandledRejectionHandler();
@@ -388,10 +388,10 @@ async function runCli(argv = process$1.argv) {
388
388
  const { getProgramContext } = await import("./program-context-Bntrf4rW.js");
389
389
  const ctx = getProgramContext(program);
390
390
  if (ctx) {
391
- const { registerCoreCliByName } = await import("./command-registry-BErLBmAn.js");
391
+ const { registerCoreCliByName } = await import("./command-registry-D5bgvc-g.js");
392
392
  await registerCoreCliByName(program, ctx, primary, parseArgv);
393
393
  }
394
- const { registerSubCliByName } = await import("./register.subclis-Cdt5rvRo.js");
394
+ const { registerSubCliByName } = await import("./register.subclis-CrmcN8zA.js");
395
395
  await registerSubCliByName(program, primary);
396
396
  }
397
397
  if (!shouldSkipPluginCommandRegistration({
@@ -400,7 +400,7 @@ async function runCli(argv = process$1.argv) {
400
400
  hasBuiltinPrimary: primary !== null && program.commands.some((command) => command.name() === primary)
401
401
  })) {
402
402
  const { registerPluginCliCommands } = await import("./cli-QwFXIS1_.js");
403
- const { loadValidatedConfigForPluginRegistration } = await import("./register.subclis-Cdt5rvRo.js");
403
+ const { loadValidatedConfigForPluginRegistration } = await import("./register.subclis-CrmcN8zA.js");
404
404
  const config = await loadValidatedConfigForPluginRegistration();
405
405
  if (config) {
406
406
  await registerPluginCliCommands(program, config, void 0, void 0, {
@@ -103,10 +103,10 @@ async function setupQuickstartVoiceRuntime(params) {
103
103
  await params.prompter.note([
104
104
  "QuickStart will prepare terminal voice runtime now.",
105
105
  "This installs wake-word Python dependencies into ~/.vora/voice-python.",
106
- "STT/TTS use the VORA backend by default, so local Agora/Hume keys are not required."
106
+ "STT/TTS use the VORA backend by default, so local Agora/ElevenLabs keys are not required."
107
107
  ].join("\n"), "Voice runtime");
108
108
  try {
109
- const { runVoiceSetup } = await import("./voice-cli-BL4zwKEh.js");
109
+ const { runVoiceSetup } = await import("./voice-cli-BD1lHjLd.js");
110
110
  await runVoiceSetup({}, params.runtime);
111
111
  await params.prompter.note([
112
112
  "Voice runtime is ready.",
@@ -464,7 +464,7 @@ async function runSetupWizard(opts, runtime = defaultRuntime, prompter) {
464
464
  mode
465
465
  });
466
466
  await writeConfigFile(nextConfig);
467
- const { finalizeSetupWizard } = await import("./setup.finalize-DPRk6z4s.js");
467
+ const { finalizeSetupWizard } = await import("./setup.finalize-tiOrCMkC.js");
468
468
  const { launchedTui } = await finalizeSetupWizard({
469
469
  flow,
470
470
  opts,
@@ -10,12 +10,12 @@ import { f as resolveControlUiLinks, g as waitForGatewayReachable, i as formatCo
10
10
  import { r as resolveGatewayService, t as describeGatewayServiceRestart } from "./service-BnUY5mkU.js";
11
11
  import { i as isSystemdUserServiceAvailable } from "./systemd-9Gclqlhk.js";
12
12
  import { t as listConfiguredWebSearchProviders } from "./runtime-B0GlgEXr.js";
13
- import { r as installCompletion } from "./completion-cli-DOqvgg4j.js";
13
+ import { r as installCompletion } from "./completion-cli-CuDyNItM.js";
14
14
  import { r as healthCommand } from "./health-DJqooRWK.js";
15
15
  import { t as ensureControlUiAssetsBuilt } from "./control-ui-assets-DVSZ-yd-.js";
16
16
  import { t as resolveSetupSecretInputString } from "./setup.secret-input-DeWdLao-.js";
17
17
  import { t as formatHealthCheckFailure } from "./health-format-TyQPx9cW.js";
18
- import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-CHyxjRnq.js";
18
+ import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-1du2om2X.js";
19
19
  import { t as runTui } from "./tui-Bay3TR6u.js";
20
20
  import path from "node:path";
21
21
  import os from "node:os";
@@ -335,7 +335,7 @@ async function finalizeSetupWizard(options) {
335
335
  }] : [],
336
336
  {
337
337
  value: "tui",
338
- label: "Hatch in TUI (OpenClaw fallback)"
338
+ label: "Hatch in TUI"
339
339
  },
340
340
  {
341
341
  value: "web",
@@ -351,7 +351,7 @@ async function finalizeSetupWizard(options) {
351
351
  if (hatchChoice === "voice") {
352
352
  restoreTerminalState("pre-setup voice", { resumeStdinIfPaused: true });
353
353
  try {
354
- const { runVoiceLoop } = await import("./voice-cli-BL4zwKEh.js");
354
+ const { runVoiceLoop } = await import("./voice-cli-BD1lHjLd.js");
355
355
  await runVoiceLoop({
356
356
  url: links.wsUrl,
357
357
  token: settings.authMode === "token" ? settings.gatewayToken : void 0,
@@ -62,7 +62,7 @@ const SUB_CLI_DESCRIPTORS = [
62
62
  },
63
63
  {
64
64
  name: "voice",
65
- description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional Hume TTS)",
65
+ description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional ElevenLabs TTS)",
66
66
  hasSubcommands: true
67
67
  },
68
68
  {
@@ -21,14 +21,14 @@ import { a as terminateStaleGatewayPids, i as renderRestartDiagnostics, s as wai
21
21
  import { r as formatDurationPrecise } from "./format-duration-C7KLrjgc.js";
22
22
  import { o as trimLogTail } from "./restart-sentinel-CjIY0IZ8.js";
23
23
  import { t as formatHelpExamples } from "./help-format-DC7U6A-Y.js";
24
- import { r as installCompletion } from "./completion-cli-DOqvgg4j.js";
24
+ import { r as installCompletion } from "./completion-cli-CuDyNItM.js";
25
25
  import { n as renderTable, t as getTerminalTableWidth } from "./table-CYPwb2Cg.js";
26
26
  import { c as resolveEffectiveUpdateChannel, i as formatUpdateChannelLabel, l as resolveUpdateChannelDisplay, r as channelToNpmTag, s as normalizeUpdateChannel } from "./update-channels-C8-n6_kE.js";
27
27
  import { i as fetchNpmTagVersion, n as compareSemverStrings, o as resolveNpmChannelTag, r as fetchNpmPackageTargetStatus, t as checkUpdateStatus } from "./update-check-Cw2Zo7Rn.js";
28
28
  import { a as collectInstalledGlobalPackageErrors, c as detectGlobalInstallManagerForRoot, d as resolveGlobalInstallSpec, f as resolveGlobalPackageRoot, h as readPackageVersion, i as cleanupGlobalRenameDirs, l as globalInstallArgs, m as readPackageName, n as runGatewayUpdate, o as createGlobalInstallEnv, p as normalizePackageTagInput, r as canResolveRegistryVersionForPackageTarget, s as detectGlobalInstallManagerByPresence, u as resolveExpectedInstalledVersionFromSpec } from "./server-startup-matrix-migration-DL743qrx.js";
29
29
  import { n as updateNpmInstalledPlugins, t as syncPluginsForUpdateChannel } from "./update-XayfF-ly.js";
30
- import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-Cnschuoa.js";
31
- import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-CHyxjRnq.js";
30
+ import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-fmGo-EJl.js";
31
+ import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-1du2om2X.js";
32
32
  import { i as resolveUpdateAvailability, n as formatUpdateOneLiner, t as formatUpdateAvailableHint } from "./status.update-209xudNW.js";
33
33
  import path from "node:path";
34
34
  import { spawn, spawnSync } from "node:child_process";
@@ -14,8 +14,10 @@ const DEFAULT_WAKE_MODEL_FILE = "hey_vora.onnx";
14
14
  const DEFAULT_WAKE_THRESHOLD = .5;
15
15
  const DEFAULT_WAIT_MS = 4e4;
16
16
  const DEFAULT_STT_TIMEOUT_MS = 25e3;
17
- const DEFAULT_STT_LANGUAGE = "vi-VN";
18
- const DEFAULT_HUME_VOICE_ID = "9e068547-5ba4-4c8e-8e03-69282a008f04";
17
+ const DEFAULT_STT_LANGUAGE = "en-US,vi-VN";
18
+ const DEFAULT_ELEVENLABS_VOICE_ID = "JBFqnCBsd6RMkjVDRZzb";
19
+ const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2";
20
+ const DEFAULT_ELEVENLABS_OUTPUT_FORMAT = "mp3_44100_128";
19
21
  const DEFAULT_VOICE_BACKEND_URL = "https://vora-ai-backend-uemj.onrender.com";
20
22
  const DEFAULT_BACKEND_PROBE_TIMEOUT_MS = 6e4;
21
23
  const DEFAULT_BACKEND_STT_PROBE_TIMEOUT_MS = 6e4;
@@ -40,7 +42,8 @@ function buildBundledAgoraBridgeCommand(backendUrl) {
40
42
  "node",
41
43
  quoteShell(scriptPath),
42
44
  "--lang {lang}",
43
- "--timeout-ms {timeout_ms}"
45
+ "--timeout-ms {timeout_ms}",
46
+ "--browser-mode managed"
44
47
  ];
45
48
  if (backendUrl) parts.push("--backend-url", quoteShell(backendUrl));
46
49
  return parts.join(" ");
@@ -93,10 +96,10 @@ function parseSttProvider(raw, agoraCommand, backendUrl) {
93
96
  if (value === "manual" || value === "agora") return value;
94
97
  return agoraCommand || backendUrl ? "agora" : "manual";
95
98
  }
96
- function parseTtsProvider(raw, humeApiKey, backendUrl) {
99
+ function parseTtsProvider(raw, apiKey, backendUrl) {
97
100
  const value = raw?.trim().toLowerCase();
98
- if (value === "none" || value === "hume") return value;
99
- return humeApiKey || backendUrl ? "hume" : "none";
101
+ if (value === "none" || value === "elevenlabs") return value;
102
+ return apiKey || backendUrl ? "elevenlabs" : "none";
100
103
  }
101
104
  function detectPythonBinary(explicitPython) {
102
105
  const venvPython = resolveVenvPythonBin();
@@ -213,8 +216,8 @@ function resolveVoiceRuntimeOptions(opts) {
213
216
  const sttProvider = parseSttProvider(sttProviderRaw, explicitAgoraCommand, backendUrl);
214
217
  const bundledAgoraCommand = sttProvider === "agora" ? buildBundledAgoraBridgeCommand(backendUrl) : void 0;
215
218
  const agoraSttCommand = explicitAgoraCommand ?? bundledAgoraCommand;
216
- const humeApiKey = trimToUndefined(opts.humeApiKey) ?? trimToUndefined(process.env.VORA_HUME_API_KEY);
217
- const ttsProvider = parseTtsProvider(trimToUndefined(opts.ttsProvider) ?? trimToUndefined(process.env.VORA_VOICE_TTS_PROVIDER), humeApiKey, backendUrl);
219
+ const elevenLabsApiKey = trimToUndefined(opts.elevenLabsApiKey) ?? trimToUndefined(process.env.VORA_ELEVENLABS_API_KEY) ?? trimToUndefined(process.env.ELEVENLABS_API_KEY);
220
+ const ttsProvider = parseTtsProvider(trimToUndefined(opts.ttsProvider) ?? trimToUndefined(process.env.VORA_VOICE_TTS_PROVIDER), elevenLabsApiKey, backendUrl);
218
221
  return {
219
222
  gateway: {
220
223
  url: trimToUndefined(opts.url),
@@ -245,8 +248,10 @@ function resolveVoiceRuntimeOptions(opts) {
245
248
  tts: {
246
249
  provider: ttsProvider,
247
250
  backendUrl,
248
- humeApiKey,
249
- humeVoiceId: trimToUndefined(opts.humeVoiceId) ?? trimToUndefined(process.env.VORA_HUME_VOICE_ID) ?? DEFAULT_HUME_VOICE_ID
251
+ elevenLabsApiKey,
252
+ elevenLabsVoiceId: trimToUndefined(opts.elevenLabsVoiceId) ?? trimToUndefined(process.env.VORA_ELEVENLABS_VOICE_ID) ?? trimToUndefined(process.env.ELEVENLABS_VOICE_ID) ?? DEFAULT_ELEVENLABS_VOICE_ID,
253
+ elevenLabsModelId: trimToUndefined(opts.elevenLabsModelId) ?? trimToUndefined(process.env.VORA_ELEVENLABS_MODEL_ID) ?? trimToUndefined(process.env.ELEVENLABS_MODEL_ID) ?? DEFAULT_ELEVENLABS_MODEL_ID,
254
+ elevenLabsOutputFormat: trimToUndefined(opts.elevenLabsOutputFormat) ?? trimToUndefined(process.env.VORA_ELEVENLABS_OUTPUT_FORMAT) ?? trimToUndefined(process.env.ELEVENLABS_OUTPUT_FORMAT) ?? DEFAULT_ELEVENLABS_OUTPUT_FORMAT
250
255
  },
251
256
  once: Boolean(opts.once)
252
257
  };
@@ -469,26 +474,32 @@ async function runVoiceDoctor(opts) {
469
474
  ok: true,
470
475
  message: "manual (type transcript in terminal)"
471
476
  });
472
- if (resolved.tts.provider === "hume") {
477
+ if (resolved.tts.provider === "elevenlabs") {
473
478
  if (resolved.tts.backendUrl) {
474
- const humeReady = backendProviderStatus(backendHealth, "hume");
479
+ const elevenLabsReady = backendProviderStatus(backendHealth, "elevenlabs");
475
480
  checks.push({
476
- key: "backend_hume",
477
- label: "backend Hume",
478
- ok: backendHealthOk && humeReady === true,
479
- message: humeReady === true ? "configured on backend" : humeReady === false ? "backend is missing Hume env" : "backend health does not expose Hume status"
481
+ key: "backend_elevenlabs",
482
+ label: "backend ElevenLabs",
483
+ ok: backendHealthOk && elevenLabsReady === true,
484
+ message: elevenLabsReady === true ? "configured on backend" : elevenLabsReady === false ? "backend is missing ElevenLabs env" : "backend health does not expose ElevenLabs status"
480
485
  });
481
486
  } else checks.push({
482
- key: "hume_api_key",
483
- label: "Hume API key",
484
- ok: Boolean(resolved.tts.humeApiKey),
485
- message: resolved.tts.humeApiKey ? "configured" : "missing VORA_HUME_API_KEY"
487
+ key: "elevenlabs_api_key",
488
+ label: "ElevenLabs API key",
489
+ ok: Boolean(resolved.tts.elevenLabsApiKey),
490
+ message: resolved.tts.elevenLabsApiKey ? "configured" : "missing VORA_ELEVENLABS_API_KEY/ELEVENLABS_API_KEY"
486
491
  });
487
492
  checks.push({
488
- key: "hume_voice_id",
489
- label: "Hume voice id",
490
- ok: Boolean(resolved.tts.humeVoiceId),
491
- message: resolved.tts.humeVoiceId
493
+ key: "elevenlabs_voice_id",
494
+ label: "ElevenLabs voice id",
495
+ ok: Boolean(resolved.tts.elevenLabsVoiceId),
496
+ message: resolved.tts.elevenLabsVoiceId
497
+ });
498
+ checks.push({
499
+ key: "elevenlabs_model_id",
500
+ label: "ElevenLabs model id",
501
+ ok: Boolean(resolved.tts.elevenLabsModelId),
502
+ message: resolved.tts.elevenLabsModelId
492
503
  });
493
504
  } else checks.push({
494
505
  key: "tts_provider",
@@ -947,37 +958,50 @@ async function waitForAssistantReply(params) {
947
958
  }
948
959
  return null;
949
960
  }
950
- async function synthesizeHumeAudio(params) {
951
- const response = params.backendUrl ? await fetch(`${params.backendUrl}/api/tts/hume`, {
952
- method: "POST",
953
- headers: { "Content-Type": "application/json" },
954
- body: JSON.stringify({
955
- text: params.text,
956
- voiceId: params.voiceId
961
+ async function synthesizeElevenLabsAudio(params) {
962
+ const attempts = [];
963
+ if (params.backendUrl) attempts.push({
964
+ label: "backend",
965
+ run: () => fetch(`${params.backendUrl}/api/tts/elevenlabs`, {
966
+ method: "POST",
967
+ headers: { "Content-Type": "application/json" },
968
+ body: JSON.stringify({
969
+ text: params.text,
970
+ voiceId: params.voiceId,
971
+ modelId: params.modelId,
972
+ outputFormat: params.outputFormat
973
+ })
957
974
  })
958
- }) : await fetch("https://api.hume.ai/v0/tts/file", {
959
- method: "POST",
960
- headers: {
961
- "Content-Type": "application/json",
962
- "X-Hume-Api-Key": params.apiKey ?? ""
963
- },
964
- body: JSON.stringify({
965
- utterances: [{
975
+ });
976
+ if (params.apiKey) attempts.push({
977
+ label: "direct",
978
+ run: () => fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(params.voiceId)}?output_format=${encodeURIComponent(params.outputFormat)}`, {
979
+ method: "POST",
980
+ headers: {
981
+ Accept: "audio/mpeg",
982
+ "Content-Type": "application/json",
983
+ "xi-api-key": params.apiKey ?? ""
984
+ },
985
+ body: JSON.stringify({
966
986
  text: params.text,
967
- voice: { id: params.voiceId },
968
- speed: 1
969
- }],
970
- format: { type: "mp3" }
987
+ model_id: params.modelId
988
+ })
971
989
  })
972
990
  });
973
- if (!response.ok) {
974
- const detail = await response.text().catch(() => "");
975
- throw new Error(`Hume TTS request failed (${response.status}): ${detail.slice(0, 280) || "empty response"}`);
991
+ let lastError;
992
+ for (const attempt of attempts) {
993
+ const response = await attempt.run();
994
+ if (!response.ok) {
995
+ const detail = await response.text().catch(() => "");
996
+ lastError = /* @__PURE__ */ new Error(`ElevenLabs TTS ${attempt.label} request failed (${response.status}): ${detail.slice(0, 280) || "empty response"}`);
997
+ continue;
998
+ }
999
+ const audioBytes = Buffer.from(await response.arrayBuffer());
1000
+ const filePath = path.join(os.tmpdir(), `vora-elevenlabs-${Date.now()}-${randomUUID().slice(0, 8)}.mp3`);
1001
+ await promises.writeFile(filePath, audioBytes);
1002
+ return filePath;
976
1003
  }
977
- const audioBytes = Buffer.from(await response.arrayBuffer());
978
- const filePath = path.join(os.tmpdir(), `vora-hume-${Date.now()}-${randomUUID().slice(0, 8)}.mp3`);
979
- await promises.writeFile(filePath, audioBytes);
980
- return filePath;
1004
+ throw lastError ?? /* @__PURE__ */ new Error("ElevenLabs TTS unavailable: missing backend URL or API key");
981
1005
  }
982
1006
  function hasBinary(command) {
983
1007
  return spawnSync(process.platform === "win32" ? "where" : "which", [command], {
@@ -990,18 +1014,56 @@ async function playAudioFile(filePath) {
990
1014
  if (process.platform === "linux" && hasBinary("ffplay")) return (await runShellCommand(`ffplay -nodisp -autoexit -loglevel quiet "${filePath.replaceAll("\"", "\\\"")}"`, 12e4)).code === 0;
991
1015
  return false;
992
1016
  }
1017
+ function quotePowerShellLiteral(value) {
1018
+ return `'${value.replaceAll("'", "''")}'`;
1019
+ }
1020
+ function encodePowerShellCommand(command) {
1021
+ return Buffer.from(command, "utf16le").toString("base64");
1022
+ }
1023
+ async function speakWithSystemVoice(text) {
1024
+ const textPath = path.join(os.tmpdir(), `vora-system-tts-${Date.now()}-${randomUUID().slice(0, 8)}.txt`);
1025
+ await promises.writeFile(textPath, text, "utf8");
1026
+ try {
1027
+ if (process.platform === "darwin" && hasBinary("say")) return (await runShellCommand(`say -f ${quoteShell(textPath)}`, 12e4)).code === 0;
1028
+ if (process.platform === "win32" && hasBinary("powershell")) return (await runShellCommand([
1029
+ "powershell",
1030
+ "-NoProfile",
1031
+ "-ExecutionPolicy",
1032
+ "Bypass",
1033
+ "-EncodedCommand",
1034
+ encodePowerShellCommand([
1035
+ "Add-Type -AssemblyName System.Speech;",
1036
+ "$speaker = New-Object System.Speech.Synthesis.SpeechSynthesizer;",
1037
+ `$speaker.Speak((Get-Content -Raw -LiteralPath ${quotePowerShellLiteral(textPath)}));`
1038
+ ].join(" "))
1039
+ ].join(" "), 12e4)).code === 0;
1040
+ if (process.platform === "linux" && hasBinary("espeak")) return (await runShellCommand(`espeak -f ${quoteShell(textPath)}`, 12e4)).code === 0;
1041
+ return false;
1042
+ } finally {
1043
+ await promises.rm(textPath, { force: true }).catch(() => void 0);
1044
+ }
1045
+ }
993
1046
  async function speakReply(resolved, text) {
994
- if (resolved.tts.provider !== "hume") return;
995
- if (!resolved.tts.backendUrl && !resolved.tts.humeApiKey) {
996
- defaultRuntime.error("[voice] Hume TTS skipped: missing API key");
1047
+ if (resolved.tts.provider !== "elevenlabs") return;
1048
+ if (!resolved.tts.backendUrl && !resolved.tts.elevenLabsApiKey) {
1049
+ if (!await speakWithSystemVoice(text)) defaultRuntime.error("[voice] ElevenLabs TTS skipped: missing API key");
1050
+ return;
1051
+ }
1052
+ let audioPath;
1053
+ try {
1054
+ audioPath = await synthesizeElevenLabsAudio({
1055
+ text,
1056
+ apiKey: resolved.tts.elevenLabsApiKey,
1057
+ backendUrl: resolved.tts.backendUrl,
1058
+ voiceId: resolved.tts.elevenLabsVoiceId,
1059
+ modelId: resolved.tts.elevenLabsModelId,
1060
+ outputFormat: resolved.tts.elevenLabsOutputFormat
1061
+ });
1062
+ } catch (error) {
1063
+ defaultRuntime.error(`[voice] ElevenLabs TTS failed: ${String(error)}`);
1064
+ if (!await speakWithSystemVoice(text)) defaultRuntime.error("[voice] system TTS fallback unavailable");
997
1065
  return;
998
1066
  }
999
- const audioPath = await synthesizeHumeAudio({
1000
- text,
1001
- apiKey: resolved.tts.humeApiKey,
1002
- backendUrl: resolved.tts.backendUrl,
1003
- voiceId: resolved.tts.humeVoiceId
1004
- });
1005
1067
  try {
1006
1068
  if (!await playAudioFile(audioPath)) defaultRuntime.log(`[voice] audio generated: ${audioPath}`);
1007
1069
  } finally {
@@ -1046,7 +1108,7 @@ async function runVoiceLoop(opts) {
1046
1108
  const wakeDeps = checkWakePythonDependencies(resolved.wake.pythonBin);
1047
1109
  if (!wakeDeps.ok) throw new Error(formatWakePythonDependencyError(resolved.wake.pythonBin, wakeDeps.message));
1048
1110
  if (resolved.stt.provider === "agora" && !resolved.stt.agoraCommand) throw new Error("Agora STT provider needs a bridge command. Use --agora-stt-command or VORA_AGORA_STT_COMMAND.");
1049
- if (resolved.tts.provider === "hume" && !resolved.tts.backendUrl && !resolved.tts.humeApiKey) throw new Error("Hume TTS enabled but no backend/API key is configured. Set --backend-url, --hume-api-key, or VORA_HUME_API_KEY.");
1111
+ if (resolved.tts.provider === "elevenlabs" && !resolved.tts.backendUrl && !resolved.tts.elevenLabsApiKey) throw new Error("ElevenLabs TTS enabled but no backend/API key is configured. Set --backend-url, --elevenlabs-api-key, VORA_ELEVENLABS_API_KEY, or ELEVENLABS_API_KEY.");
1050
1112
  const gatewayClient = await GatewayChatClient.connect({
1051
1113
  url: resolved.gateway.url,
1052
1114
  token: resolved.gateway.token,
@@ -1090,6 +1152,7 @@ async function runVoiceLoop(opts) {
1090
1152
  (async () => {
1091
1153
  if (busy || stopRequested) return;
1092
1154
  busy = true;
1155
+ wakeWord.stop();
1093
1156
  try {
1094
1157
  defaultRuntime.log(`[voice] wake trigger model=${event.model} score=${event.score.toFixed(2)} latency=${(Math.max(0, Date.now() / 1e3 - event.sourceTimestampSec) * 1e3).toFixed(0)}ms`);
1095
1158
  const transcript = (await transcribeSpeech(resolved)).trim();
@@ -1108,6 +1171,13 @@ async function runVoiceLoop(opts) {
1108
1171
  defaultRuntime.error(`[voice] turn failed: ${String(err)}`);
1109
1172
  } finally {
1110
1173
  busy = false;
1174
+ if (!stopRequested) try {
1175
+ await wakeWord.start();
1176
+ defaultRuntime.log("[voice] wake word armed for next turn");
1177
+ } catch (err) {
1178
+ defaultRuntime.error(`[voice] failed to restart wake word: ${String(err)}`);
1179
+ requestStop();
1180
+ }
1111
1181
  }
1112
1182
  })();
1113
1183
  });
@@ -1128,10 +1198,10 @@ async function runVoiceLoop(opts) {
1128
1198
  }
1129
1199
  }
1130
1200
  function addVoiceOptions(cmd) {
1131
- return cmd.option("--url <url>", "Gateway WebSocket URL").option("--token <token>", "Gateway token").option("--password <password>", "Gateway password").option("--session <key>", "Session key (default: \"main\")").option("--deliver", "Deliver assistant replies to linked channel routes", false).option("--message <text>", "Send an initial message before listening for wake word").option("--thinking <level>", "Thinking level override").option("--timeout-ms <ms>", "chat.send timeout override (ms)").option("--wait-ms <ms>", "Wait budget for assistant reply after send (ms)").option("--once", "Stop after one successful wake->reply turn", false).option("--wake-dir <path>", "Path to wake_word directory").option("--wake-model <path>", "Wake model path or file name").option("--wake-threshold <0..1>", "Wake word threshold").option("--python <bin>", "Python binary for wake word process").option("--stt-provider <manual|agora>", "STT mode (agora uses external bridge command)").option("--stt-lang <lang>", "STT language hint passed to bridge command").option("--stt-timeout-ms <ms>", "STT timeout (ms)").option("--agora-stt-command <cmd>", "External command for Agora STT bridge (supports {lang}, {timeout_ms}); default uses bundled browser bridge").option("--backend-url <url>", `Voice backend URL for provider secrets/tokens (default: ${DEFAULT_VOICE_BACKEND_URL}; use "off" for local env mode)`).option("--tts-provider <none|hume>", "Voice reply provider").option("--hume-api-key <key>", "Hume API key").option("--hume-voice-id <id>", "Hume voice ID");
1201
+ return cmd.option("--url <url>", "Gateway WebSocket URL").option("--token <token>", "Gateway token").option("--password <password>", "Gateway password").option("--session <key>", "Session key (default: \"main\")").option("--deliver", "Deliver assistant replies to linked channel routes", false).option("--message <text>", "Send an initial message before listening for wake word").option("--thinking <level>", "Thinking level override").option("--timeout-ms <ms>", "chat.send timeout override (ms)").option("--wait-ms <ms>", "Wait budget for assistant reply after send (ms)").option("--once", "Stop after one successful wake->reply turn", false).option("--wake-dir <path>", "Path to wake_word directory").option("--wake-model <path>", "Wake model path or file name").option("--wake-threshold <0..1>", "Wake word threshold").option("--python <bin>", "Python binary for wake word process").option("--stt-provider <manual|agora>", "STT mode (agora uses external bridge command)").option("--stt-lang <lang>", "STT language hint passed to bridge command").option("--stt-timeout-ms <ms>", "STT timeout (ms)").option("--agora-stt-command <cmd>", "External command for Agora STT bridge (supports {lang}, {timeout_ms}); default uses bundled Agora capture bridge").option("--backend-url <url>", `Voice backend URL for provider secrets/tokens (default: ${DEFAULT_VOICE_BACKEND_URL}; use "off" for local env mode)`).option("--tts-provider <none|elevenlabs>", "Voice reply provider").option("--eleven-labs-api-key <key>", "ElevenLabs API key").option("--eleven-labs-voice-id <id>", "ElevenLabs voice ID").option("--eleven-labs-model-id <id>", "ElevenLabs model ID").option("--eleven-labs-output-format <format>", "ElevenLabs output format");
1132
1202
  }
1133
1203
  function registerVoiceCli(program) {
1134
- const voice = addVoiceOptions(program.command("voice").description("Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional Hume TTS)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/voice", "docs.vora.ai/cli/voice")}\n`));
1204
+ const voice = addVoiceOptions(program.command("voice").description("Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional ElevenLabs TTS)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/voice", "docs.vora.ai/cli/voice")}\n`));
1135
1205
  voice.action(async (opts) => {
1136
1206
  try {
1137
1207
  await runVoiceLoop(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vora-ai",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Voice-first AI agent core engine — VORA",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/vora-ai/vora-core#readme",
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from "node:child_process";
3
+ import { spawn, spawnSync } from "node:child_process";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import http from "node:http";
6
6
 
7
7
  const DEFAULT_TIMEOUT_MS = 45_000;
8
8
  const DEFAULT_API_BASE = "https://api.agora.io";
9
9
  const DEFAULT_BACKEND_URL = "https://vora-ai-backend-uemj.onrender.com";
10
- const DEFAULT_LANGUAGE = "vi-VN";
10
+ const DEFAULT_LANGUAGE = "en-US,vi-VN";
11
11
  const DEFAULT_RTC_UID = "1002";
12
12
  const DEFAULT_STT_AUDIO_UID = "111";
13
13
  const DEFAULT_STT_TEXT_UID = "222";
@@ -15,16 +15,16 @@ const DEFAULT_STT_BOT_UID = DEFAULT_STT_AUDIO_UID;
15
15
 
16
16
  function printUsage() {
17
17
  const lines = [
18
- "Agora STT bridge (browser + RTC + Agora RTT) for `vora voice`",
18
+ "Agora STT bridge (RTC + Agora RTT capture) for `vora voice`",
19
19
  "",
20
20
  "Usage:",
21
21
  " node scripts/agora-stt-bridge.mjs [options]",
22
22
  "",
23
23
  "Options:",
24
- " --lang <code> STT language (default: vi-VN)",
24
+ ` --lang <code> STT language(s), comma-separated max 2 (default: ${DEFAULT_LANGUAGE})`,
25
25
  " --timeout-ms <ms> Timeout waiting for final transcript",
26
26
  " --channel <name> RTC channel name",
27
- " --uid <uid> Browser RTC UID (default: 1002)",
27
+ " --uid <uid> Capture RTC UID (default: 1002)",
28
28
  " --rtc-token <token> RTC token for browser + STT bot",
29
29
  " --stt-audio-uid <uid> STT audio bot UID for Agora RTT service",
30
30
  " --stt-text-uid <uid> STT text bot UID for Agora RTT data stream",
@@ -35,7 +35,9 @@ function printUsage() {
35
35
  " --customer-key <key> Agora customer key (or VORA_AGORA_CUSTOMER_KEY)",
36
36
  " --customer-secret <s> Agora customer secret (or VORA_AGORA_CUSTOMER_SECRET)",
37
37
  " --port <port> Fixed local bridge port",
38
- " --no-open Do not auto-open browser",
38
+ " --browser-mode <mode> managed|external|none (default: managed)",
39
+ " --show-browser Show the managed browser window for debugging",
40
+ " --no-open Do not start the managed/external capture page",
39
41
  " --help Show this help",
40
42
  "",
41
43
  "Environment fallbacks:",
@@ -44,6 +46,7 @@ function printUsage() {
44
46
  " VORA_AGORA_CHANNEL, VORA_AGORA_UID, VORA_AGORA_RTC_TOKEN",
45
47
  " VORA_AGORA_STT_AUDIO_UID, VORA_AGORA_STT_TEXT_UID, VORA_AGORA_STT_BOT_UID",
46
48
  " VORA_AGORA_API_BASE, VORA_AGORA_STT_TIMEOUT_MS, VORA_AGORA_STT_LANG",
49
+ " VORA_AGORA_STT_BROWSER_MODE, VORA_AGORA_STT_SHOW_BROWSER",
47
50
  ];
48
51
  process.stderr.write(`${lines.join("\n")}\n`);
49
52
  }
@@ -68,6 +71,10 @@ function parseArgs(argv) {
68
71
  out.open = true;
69
72
  continue;
70
73
  }
74
+ if (token === "--show-browser") {
75
+ out["show-browser"] = true;
76
+ continue;
77
+ }
71
78
  const eq = token.indexOf("=");
72
79
  if (eq >= 0) {
73
80
  const key = token.slice(2, eq);
@@ -150,6 +157,110 @@ function openUrl(url) {
150
157
  }
151
158
  }
152
159
 
160
+ function readBoolean(argValue, envName, fallback = false) {
161
+ if (typeof argValue === "boolean") {
162
+ return argValue;
163
+ }
164
+ const raw = typeof argValue === "string" ? argValue : process.env[envName];
165
+ if (!raw) {
166
+ return fallback;
167
+ }
168
+ const normalized = String(raw).trim().toLowerCase();
169
+ if (["1", "true", "yes", "on"].includes(normalized)) {
170
+ return true;
171
+ }
172
+ if (["0", "false", "no", "off"].includes(normalized)) {
173
+ return false;
174
+ }
175
+ return fallback;
176
+ }
177
+
178
+ function resolveBrowserMode(args) {
179
+ const raw = readText(args["browser-mode"], "VORA_AGORA_STT_BROWSER_MODE", "managed").toLowerCase();
180
+ if (raw === "managed" || raw === "external" || raw === "none") {
181
+ return raw;
182
+ }
183
+ return "managed";
184
+ }
185
+
186
+ function browserExecutableCandidates() {
187
+ if (process.platform === "darwin") {
188
+ return [
189
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
190
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
191
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
192
+ ];
193
+ }
194
+ if (process.platform === "win32") {
195
+ const roots = [
196
+ process.env.PROGRAMFILES,
197
+ process.env["PROGRAMFILES(X86)"],
198
+ process.env.LOCALAPPDATA,
199
+ ].filter(Boolean);
200
+ return roots.flatMap((root) => [
201
+ `${root}\\Microsoft\\Edge\\Application\\msedge.exe`,
202
+ `${root}\\Google\\Chrome\\Application\\chrome.exe`,
203
+ ]);
204
+ }
205
+ return [
206
+ "/usr/bin/google-chrome",
207
+ "/usr/bin/google-chrome-stable",
208
+ "/usr/bin/chromium",
209
+ "/usr/bin/chromium-browser",
210
+ "/snap/bin/chromium",
211
+ ];
212
+ }
213
+
214
+ async function openManagedBrowser(url, showBrowser) {
215
+ let chromium;
216
+ try {
217
+ ({ chromium } = await import("playwright-core"));
218
+ } catch (error) {
219
+ throw new Error(`playwright-core is unavailable: ${String(error)}`);
220
+ }
221
+
222
+ const executablePath = browserExecutableCandidates().find((candidate) => {
223
+ try {
224
+ return Boolean(candidate) && typeof candidate === "string" && spawnSync(candidate, ["--version"], {
225
+ stdio: "ignore",
226
+ shell: process.platform === "win32",
227
+ }).status === 0;
228
+ } catch {
229
+ return false;
230
+ }
231
+ });
232
+
233
+ const launchOptions = {
234
+ headless: !showBrowser,
235
+ args: [
236
+ "--use-fake-ui-for-media-stream",
237
+ "--autoplay-policy=no-user-gesture-required",
238
+ "--no-first-run",
239
+ "--disable-background-timer-throttling",
240
+ ],
241
+ };
242
+ if (executablePath) {
243
+ launchOptions.executablePath = executablePath;
244
+ } else if (process.platform === "win32") {
245
+ launchOptions.channel = "msedge";
246
+ } else {
247
+ launchOptions.channel = "chrome";
248
+ }
249
+
250
+ const browser = await chromium.launch(launchOptions);
251
+ const context = await browser.newContext({
252
+ permissions: ["microphone"],
253
+ viewport: { width: 720, height: 520 },
254
+ });
255
+ const page = await context.newPage();
256
+ await page.goto(url, { waitUntil: "domcontentloaded" });
257
+ return {
258
+ close: async () => {
259
+ await browser.close().catch(() => undefined);
260
+ },
261
+ };
262
+ }
263
+
153
264
  function jsonResponse(res, statusCode, payload) {
154
265
  const body = JSON.stringify(payload);
155
266
  res.writeHead(statusCode, {
@@ -437,7 +548,12 @@ function buildBrowserHtml(browserConfig) {
437
548
  if (words.length === 0) {
438
549
  return;
439
550
  }
440
- const text = words.map((word) => String(word?.text || "")).join("").trim();
551
+ const text = words
552
+ .map((word) => String(word?.text || "").trim())
553
+ .filter(Boolean)
554
+ .join(" ")
555
+ .replace(/\\s+([,.!?;:])/g, "$1")
556
+ .trim();
441
557
  if (!text) {
442
558
  return;
443
559
  }
@@ -553,6 +669,15 @@ function buildBrowserHtml(browserConfig) {
553
669
  </html>`;
554
670
  }
555
671
 
672
+ function parseLanguages(lang) {
673
+ const languages = String(lang || DEFAULT_LANGUAGE)
674
+ .split(",")
675
+ .map((entry) => entry.trim())
676
+ .filter(Boolean)
677
+ .slice(0, 2);
678
+ return languages.length > 0 ? languages : [DEFAULT_LANGUAGE.split(",")[0]];
679
+ }
680
+
556
681
  async function startAgoraStt(config, lang) {
557
682
  if (config.backendUrl) {
558
683
  const payload = await backendJsonRequest(config.backendUrl, "/api/agora/stt/start", {
@@ -573,7 +698,7 @@ async function startAgoraStt(config, lang) {
573
698
  const auth = Buffer.from(`${config.customerKey}:${config.customerSecret}`).toString("base64");
574
699
  const startPayload = {
575
700
  name: `vora-voice-${Date.now()}`,
576
- languages: [lang],
701
+ languages: parseLanguages(lang),
577
702
  maxIdleTime: Math.max(10, Math.min(300, Math.ceil(config.timeoutMs / 1000))),
578
703
  rtcConfig: {
579
704
  channelName: config.channel,
@@ -677,6 +802,8 @@ async function main() {
677
802
  lang: readText(args.lang, "VORA_AGORA_STT_LANG", DEFAULT_LANGUAGE),
678
803
  timeoutMs: readPositiveInt(args["timeout-ms"], "VORA_AGORA_STT_TIMEOUT_MS", DEFAULT_TIMEOUT_MS),
679
804
  open: args.open !== false,
805
+ browserMode: args.open === false ? "none" : resolveBrowserMode(args),
806
+ showBrowser: readBoolean(args["show-browser"], "VORA_AGORA_STT_SHOW_BROWSER", false),
680
807
  port: readPositiveInt(args.port, "VORA_AGORA_STT_PORT", 0),
681
808
  };
682
809
 
@@ -703,6 +830,16 @@ async function main() {
703
830
  let server;
704
831
  let done = false;
705
832
  let currentAgentId = "";
833
+ let browserController = null;
834
+
835
+ const closeBrowser = async () => {
836
+ if (!browserController) {
837
+ return;
838
+ }
839
+ const controller = browserController;
840
+ browserController = null;
841
+ await controller.close().catch(() => undefined);
842
+ };
706
843
 
707
844
  const fail = async (message) => {
708
845
  if (done) {
@@ -710,6 +847,7 @@ async function main() {
710
847
  }
711
848
  done = true;
712
849
  await stopAgoraStt(resolvedConfig, currentAgentId).catch(() => undefined);
850
+ await closeBrowser();
713
851
  if (server) {
714
852
  await closeServer(server);
715
853
  }
@@ -723,6 +861,7 @@ async function main() {
723
861
  }
724
862
  done = true;
725
863
  await stopAgoraStt(resolvedConfig, currentAgentId).catch(() => undefined);
864
+ await closeBrowser();
726
865
  if (server) {
727
866
  await closeServer(server);
728
867
  }
@@ -819,10 +958,27 @@ async function main() {
819
958
  const localUrl = `http://127.0.0.1:${address.port}/`;
820
959
  const source = resolvedConfig.backendUrl ? `backend=${safeBase(resolvedConfig.backendUrl)}` : "direct-agora";
821
960
  process.stderr.write(`[agora-stt-bridge] channel=${resolvedConfig.channel} uid=${resolvedConfig.uid} ${source}\n`);
822
- process.stderr.write(`[agora-stt-bridge] open this URL if browser does not auto-open:\n${localUrl}\n`);
961
+ process.stderr.write(`[agora-stt-bridge] local capture URL:\n${localUrl}\n`);
823
962
  process.stderr.write("[agora-stt-bridge] waiting for one final transcript...\n");
824
- if (config.open) {
963
+ if (config.browserMode === "managed") {
964
+ try {
965
+ browserController = await openManagedBrowser(localUrl, config.showBrowser);
966
+ process.stderr.write(
967
+ `[agora-stt-bridge] managed browser capture started${config.showBrowser ? "" : " (hidden)"}\n`,
968
+ );
969
+ } catch (error) {
970
+ await fail(
971
+ [
972
+ `managed browser unavailable: ${String(error)}`,
973
+ "Install Google Chrome/Microsoft Edge, or run with --browser-mode external for manual debugging.",
974
+ ].join("\n"),
975
+ );
976
+ return;
977
+ }
978
+ } else if (config.browserMode === "external") {
825
979
  openUrl(localUrl);
980
+ } else {
981
+ process.stderr.write("[agora-stt-bridge] browser auto-open disabled; open the local capture URL manually.\n");
826
982
  }
827
983
 
828
984
  const timeout = setTimeout(() => {
@@ -1,2 +0,0 @@
1
- import { a as registerCompletionCli } from "./completion-cli-DOqvgg4j.js";
2
- export { registerCompletionCli };