vora-ai 0.1.35 → 0.1.37

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 (34) 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-C-dQzQLt.js → command-registry-DoRR_ykS.js} +4 -4
  6. package/dist/{command-registry-D5bgvc-g.js → command-registry-oO4_XKsL.js} +1 -1
  7. package/dist/completion-cli-B6W-7yKI.js +2 -0
  8. package/dist/{completion-cli-CuDyNItM.js → completion-cli-BSVP90BO.js} +2 -2
  9. package/dist/{doctor-completion-1du2om2X.js → doctor-completion-CLfiq4-c.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-chat-JO0UI9m6.js → gateway-chat-BT9D_xTh.js} +1 -0
  13. package/dist/{gateway-cli-D3jVKXtU.js → gateway-cli-DNaH7pBF.js} +1 -1
  14. package/dist/{help-BX3qk2j9.js → help-BWGrT64x.js} +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/{onboard-DwchOLHk.js → onboard-VNwsDuUB.js} +1 -1
  17. package/dist/{program-Dah2D_cO.js → program-q6W4mv2i.js} +2 -2
  18. package/dist/{prompt-select-styled-fmGo-EJl.js → prompt-select-styled-2R-9jWL7.js} +1 -1
  19. package/dist/{register.maintenance-BHLVtWhx.js → register.maintenance-DMZegmso.js} +1 -1
  20. package/dist/{register.onboard-BQXwigRU.js → register.onboard-CxR6bI1n.js} +1 -1
  21. package/dist/{register.setup-BdrATrzp.js → register.setup-BT2-kAsK.js} +1 -1
  22. package/dist/{register.subclis-Dx_SLWv5.js → register.subclis-B-9NwtNV.js} +7 -7
  23. package/dist/{register.subclis-CrmcN8zA.js → register.subclis-BQu4VqOI.js} +2 -2
  24. package/dist/{root-help-DDj9BENB.js → root-help-Tv4Oc4kX.js} +2 -2
  25. package/dist/{run-main-C6NIk22l.js → run-main-BWhbuxQt.js} +5 -5
  26. package/dist/{setup-CEe7DH30.js → setup-B35nGXkQ.js} +3 -3
  27. package/dist/{setup.finalize-tiOrCMkC.js → setup.finalize-BueEFawv.js} +4 -4
  28. package/dist/{subcli-descriptors-BTTrqeCm.js → subcli-descriptors-Cc423xKz.js} +1 -1
  29. package/dist/{tui-Bay3TR6u.js → tui-DCtnC9C0.js} +1 -1
  30. package/dist/{tui-cli-Cii_Df7D.js → tui-cli-CU809_m8.js} +1 -1
  31. package/dist/{update-cli-Dmyo-EkW.js → update-cli-BbcBxt3_.js} +3 -3
  32. package/dist/{voice-cli-BD1lHjLd.js → voice-cli-CXiYCrQf.js} +476 -31
  33. package/package.json +1 -1
  34. package/dist/completion-cli-BcvhPH1f.js +0 -2
package/dist/.buildstamp CHANGED
@@ -1 +1 @@
1
- {"builtAt":1776448165924,"head":"592461e44a1c51949bcc88392e07313f5d8bb7b4"}
1
+ {"builtAt":1776477728789,"head":"7cb18f8b1e45487f1627768901dac2dc10dda292"}
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.35",
3
- "commit": "592461e44a1c51949bcc88392e07313f5d8bb7b4",
4
- "builtAt": "2026-04-17T17:49:26.654Z"
2
+ "version": "0.1.37",
3
+ "commit": "7cb18f8b1e45487f1627768901dac2dc10dda292",
4
+ "builtAt": "2026-04-18T02:02:09.438Z"
5
5
  }
@@ -1 +1 @@
1
- 9d2491f3df1dc0273883fcb4736d000c6f77e84a6f422fc691e1a1a773f79352
1
+ 61df678f55f820c36800453e93a0b24dc4caa6d8ac7e6ec54d71e4885e06c512
@@ -10,5 +10,5 @@
10
10
  "signal",
11
11
  "imessage"
12
12
  ],
13
- "rootHelpText": "\n🌊 VORA 0.1.35 (592461e) — Bringing Agora's voice to your terminal.\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"
13
+ "rootHelpText": "\n🌊 VORA 0.1.37 (7cb18f8) — Local processing, global conversational AI.\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"
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-Dx_SLWv5.js";
4
+ import { i as registerSubCliCommands } from "./register.subclis-B-9NwtNV.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-BdrATrzp.js")).registerSetupCommand(program);
18
+ (await import("./register.setup-BT2-kAsK.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-BQXwigRU.js")).registerOnboardCommand(program);
28
+ (await import("./register.onboard-CxR6bI1n.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-BHLVtWhx.js")).registerMaintenanceCommands(program);
85
+ (await import("./register.maintenance-DMZegmso.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-C-dQzQLt.js";
2
+ import { n as registerCoreCliByName } from "./command-registry-DoRR_ykS.js";
3
3
  export { registerCoreCliByName };
@@ -0,0 +1,2 @@
1
+ import { a as registerCompletionCli } from "./completion-cli-BSVP90BO.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-Dx_SLWv5.js";
7
- import { n as registerCoreCliByName, t as getCoreCliCommandNames } from "./command-registry-C-dQzQLt.js";
6
+ import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName, t as getSubCliEntries } from "./register.subclis-B-9NwtNV.js";
7
+ import { n as registerCoreCliByName, t as getCoreCliCommandNames } from "./command-registry-DoRR_ykS.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-CuDyNItM.js";
4
+ import { c as usesSlowDynamicCompletion, i as isCompletionInstalled, o as resolveCompletionCachePath, r as installCompletion, s as resolveShellFromEnv, t as completionCacheExists } from "./completion-cli-BSVP90BO.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-DDj9BENB.js").then(({ outputRootHelp }) => {
196
+ import("./root-help-Tv4Oc4kX.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-C6NIk22l.js").then(({ runCli }) => runCli(argv)).catch((error) => {
203
+ import("./run-main-BWhbuxQt.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:49:25.810Z"
3
+ "generatedAt": "2026-04-18T02:02:08.668Z"
4
4
  }
@@ -84,6 +84,7 @@ var GatewayChatClient = class GatewayChatClient {
84
84
  message: opts.message,
85
85
  thinking: opts.thinking,
86
86
  deliver: opts.deliver,
87
+ attachments: opts.attachments,
87
88
  timeoutMs: opts.timeoutMs,
88
89
  idempotencyKey: runId
89
90
  });
@@ -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-CEe7DH30.js";
199
+ import { t as runSetupWizard } from "./setup-B35nGXkQ.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-BTTrqeCm.js";
9
+ import { t as getSubCliCommandsWithSubcommands } from "./subcli-descriptors-Cc423xKz.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-C6NIk22l.js")]);
31
+ const [{ installGaxiosFetchCompat }, { runCli }] = await Promise.all([import("./gaxios-fetch-compat-BZRlVrBH.js"), import("./run-main-BWhbuxQt.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-CEe7DH30.js";
14
+ import { t as runSetupWizard } from "./setup-B35nGXkQ.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-C-dQzQLt.js";
10
+ import { i as registerProgramCommands } from "./command-registry-DoRR_ykS.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-BX3qk2j9.js";
15
+ import { t as configureProgramHelp } from "./help-BWGrT64x.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-1du2om2X.js";
111
+ import { n as doctorShellCompletion } from "./doctor-completion-CLfiq4-c.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-fmGo-EJl.js";
19
+ import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-2R-9jWL7.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-DwchOLHk.js";
7
+ import { n as CORE_ONBOARD_AUTH_FLAGS, t as setupWizardCommand } from "./onboard-VNwsDuUB.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-DwchOLHk.js";
14
+ import { t as setupWizardCommand } from "./onboard-VNwsDuUB.js";
15
15
  import fs from "node:fs/promises";
16
16
  import { z } from "zod";
17
17
  import JSON5 from "json5";
@@ -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-BTTrqeCm.js";
4
+ import { n as getSubCliEntries$1 } from "./subcli-descriptors-Cc423xKz.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-D3jVKXtU.js")).registerGatewayCli(program);
33
+ (await import("./gateway-cli-DNaH7pBF.js")).registerGatewayCli(program);
34
34
  }
35
35
  },
36
36
  {
@@ -110,15 +110,15 @@ const entries = [
110
110
  description: "Open a terminal UI connected to the Gateway",
111
111
  hasSubcommands: false,
112
112
  register: async (program) => {
113
- (await import("./tui-cli-Cii_Df7D.js")).registerTuiCli(program);
113
+ (await import("./tui-cli-CU809_m8.js")).registerTuiCli(program);
114
114
  }
115
115
  },
116
116
  {
117
117
  name: "voice",
118
- description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional ElevenLabs TTS)",
118
+ description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional Hume TTS)",
119
119
  hasSubcommands: true,
120
120
  register: async (program) => {
121
- (await import("./voice-cli-BD1lHjLd.js")).registerVoiceCli(program);
121
+ (await import("./voice-cli-CXiYCrQf.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-Dmyo-EkW.js")).registerUpdateCli(program);
247
+ (await import("./update-cli-BbcBxt3_.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-BcvhPH1f.js")).registerCompletionCli(program);
255
+ (await import("./completion-cli-B6W-7yKI.js")).registerCompletionCli(program);
256
256
  }
257
257
  }
258
258
  ];
@@ -1,3 +1,3 @@
1
- import "./subcli-descriptors-BTTrqeCm.js";
2
- import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName } from "./register.subclis-Dx_SLWv5.js";
1
+ import "./subcli-descriptors-Cc423xKz.js";
2
+ import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName } from "./register.subclis-B-9NwtNV.js";
3
3
  export { loadValidatedConfigForPluginRegistration, registerSubCliByName };
@@ -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-BTTrqeCm.js";
4
- import { t as configureProgramHelp } from "./help-BX3qk2j9.js";
3
+ import { n as getSubCliEntries } from "./subcli-descriptors-Cc423xKz.js";
4
+ import { t as configureProgramHelp } from "./help-BWGrT64x.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-DDj9BENB.js");
371
+ const { outputRootHelp } = await import("./root-help-Tv4Oc4kX.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-Dah2D_cO.js");
377
+ const { buildProgram } = await import("./program-q6W4mv2i.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-D5bgvc-g.js");
391
+ const { registerCoreCliByName } = await import("./command-registry-oO4_XKsL.js");
392
392
  await registerCoreCliByName(program, ctx, primary, parseArgv);
393
393
  }
394
- const { registerSubCliByName } = await import("./register.subclis-CrmcN8zA.js");
394
+ const { registerSubCliByName } = await import("./register.subclis-BQu4VqOI.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-CrmcN8zA.js");
403
+ const { loadValidatedConfigForPluginRegistration } = await import("./register.subclis-BQu4VqOI.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/ElevenLabs keys are not required."
106
+ "STT/TTS use the VORA backend by default, so local Agora/Hume keys are not required."
107
107
  ].join("\n"), "Voice runtime");
108
108
  try {
109
- const { runVoiceSetup } = await import("./voice-cli-BD1lHjLd.js");
109
+ const { runVoiceSetup } = await import("./voice-cli-CXiYCrQf.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-tiOrCMkC.js");
467
+ const { finalizeSetupWizard } = await import("./setup.finalize-BueEFawv.js");
468
468
  const { launchedTui } = await finalizeSetupWizard({
469
469
  flow,
470
470
  opts,
@@ -10,13 +10,13 @@ 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-CuDyNItM.js";
13
+ import { r as installCompletion } from "./completion-cli-BSVP90BO.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-1du2om2X.js";
19
- import { t as runTui } from "./tui-Bay3TR6u.js";
18
+ import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-CLfiq4-c.js";
19
+ import { t as runTui } from "./tui-DCtnC9C0.js";
20
20
  import path from "node:path";
21
21
  import os from "node:os";
22
22
  import fs from "node:fs/promises";
@@ -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-BD1lHjLd.js");
354
+ const { runVoiceLoop } = await import("./voice-cli-CXiYCrQf.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 ElevenLabs TTS)",
65
+ description: "Wake-word terminal voice loop (OpenWakeWord trigger + STT bridge + Gateway chat + optional Hume TTS)",
66
66
  hasSubcommands: true
67
67
  },
68
68
  {
@@ -11,7 +11,7 @@ import { t as normalizeGroupActivation } from "./group-activation-C-78lmfs.js";
11
11
  import { n as formatTimeAgo, t as formatRelativeTimestamp } from "./format-relative-DFtNOr25.js";
12
12
  import { a as formatTokenCount, r as stripLeadingInboundMetadata } from "./strip-inbound-meta-EUsXOL1l.js";
13
13
  import { a as listChatCommandsForConfig, i as listChatCommands } from "./commands-registry-e0jUxhko.js";
14
- import { t as GatewayChatClient } from "./gateway-chat-JO0UI9m6.js";
14
+ import { t as GatewayChatClient } from "./gateway-chat-BT9D_xTh.js";
15
15
  import { spawn } from "node:child_process";
16
16
  import chalk from "chalk";
17
17
  import { randomUUID } from "node:crypto";
@@ -2,7 +2,7 @@ import { n as defaultRuntime } from "./runtime-a6RdoDky.js";
2
2
  import { t as formatDocsLink } from "./links-D-QW4VCX.js";
3
3
  import { r as theme } from "./theme-DEBOahQW.js";
4
4
  import { t as parseTimeoutMs } from "./parse-timeout-CIdCwobj.js";
5
- import { t as runTui } from "./tui-Bay3TR6u.js";
5
+ import { t as runTui } from "./tui-DCtnC9C0.js";
6
6
  //#region src/cli/tui-cli.ts
7
7
  function registerTuiCli(program) {
8
8
  program.command("tui").description("Open a terminal UI connected to the Gateway").option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)").option("--token <token>", "Gateway token (if required)").option("--password <password>", "Gateway password (if required)").option("--session <key>", "Session key (default: \"main\", or \"global\" when scope is global)").option("--deliver", "Deliver assistant replies", false).option("--thinking <level>", "Thinking level override").option("--message <text>", "Send an initial message after connecting").option("--timeout-ms <ms>", "Agent timeout in ms (defaults to agents.defaults.timeoutSeconds)").option("--history-limit <n>", "History entries to load", "200").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/tui", "docs.vora.ai/cli/tui")}\n`).action(async (opts) => {
@@ -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-CuDyNItM.js";
24
+ import { r as installCompletion } from "./completion-cli-BSVP90BO.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-fmGo-EJl.js";
31
- import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-1du2om2X.js";
30
+ import { n as doctorCommand, t as selectStyled } from "./prompt-select-styled-2R-9jWL7.js";
31
+ import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-CLfiq4-c.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";
@@ -1,8 +1,9 @@
1
1
  import { n as defaultRuntime } from "./runtime-a6RdoDky.js";
2
2
  import { t as formatDocsLink } from "./links-D-QW4VCX.js";
3
3
  import { r as theme } from "./theme-DEBOahQW.js";
4
+ import { g as resolveDefaultAgentWorkspaceDir, i as DEFAULT_HEARTBEAT_FILENAME } from "./workspace-gu8OIy8V.js";
4
5
  import { t as parseTimeoutMs } from "./parse-timeout-CIdCwobj.js";
5
- import { t as GatewayChatClient } from "./gateway-chat-JO0UI9m6.js";
6
+ import { t as GatewayChatClient } from "./gateway-chat-BT9D_xTh.js";
6
7
  import { existsSync, promises } from "node:fs";
7
8
  import path from "node:path";
8
9
  import { spawn, spawnSync } from "node:child_process";
@@ -15,10 +16,17 @@ const DEFAULT_WAKE_THRESHOLD = .5;
15
16
  const DEFAULT_WAIT_MS = 4e4;
16
17
  const DEFAULT_STT_TIMEOUT_MS = 25e3;
17
18
  const DEFAULT_STT_LANGUAGE = "en-US,vi-VN";
19
+ const DEFAULT_HUME_VOICE_ID = "9e068547-5ba4-4c8e-8e03-69282a008f04";
20
+ const DEFAULT_HUME_SPEED = 1.2;
21
+ const DEFAULT_TTS_TIMEOUT_MS = 2e4;
22
+ const DEFAULT_TTS_MAX_CHARS = 900;
18
23
  const DEFAULT_ELEVENLABS_VOICE_ID = "JBFqnCBsd6RMkjVDRZzb";
19
24
  const DEFAULT_ELEVENLABS_MODEL_ID = "eleven_multilingual_v2";
20
25
  const DEFAULT_ELEVENLABS_OUTPUT_FORMAT = "mp3_44100_128";
21
26
  const DEFAULT_VOICE_BACKEND_URL = "https://vora-ai-backend-uemj.onrender.com";
27
+ const DEFAULT_FOLLOW_UP_MS = 45e3;
28
+ const DEFAULT_FOLLOW_UP_MAX_TURNS = 4;
29
+ const DEFAULT_FOLLOW_UP_STT_TIMEOUT_MS = 12e3;
22
30
  const DEFAULT_BACKEND_PROBE_TIMEOUT_MS = 6e4;
23
31
  const DEFAULT_BACKEND_STT_PROBE_TIMEOUT_MS = 6e4;
24
32
  const DEFAULT_STT_BRIDGE_COMMAND_GRACE_MS = 6e4;
@@ -91,15 +99,30 @@ function parseMs(raw, fallback) {
91
99
  const parsed = parseTimeoutMs(raw);
92
100
  return parsed && parsed > 0 ? parsed : fallback;
93
101
  }
102
+ function parseBoundedNumber(raw, fallback, min, max) {
103
+ const text = trimToUndefined(raw);
104
+ if (!text) return fallback;
105
+ const parsed = Number.parseFloat(text);
106
+ if (!Number.isFinite(parsed)) return fallback;
107
+ return Math.max(min, Math.min(max, parsed));
108
+ }
109
+ function parseBoundedInt(raw, fallback, min, max) {
110
+ const text = trimToUndefined(raw);
111
+ if (!text) return fallback;
112
+ const parsed = Number.parseInt(text, 10);
113
+ if (!Number.isFinite(parsed)) return fallback;
114
+ return Math.max(min, Math.min(max, parsed));
115
+ }
94
116
  function parseSttProvider(raw, agoraCommand, backendUrl) {
95
117
  const value = raw?.trim().toLowerCase();
96
118
  if (value === "manual" || value === "agora") return value;
97
119
  return agoraCommand || backendUrl ? "agora" : "manual";
98
120
  }
99
- function parseTtsProvider(raw, apiKey, backendUrl) {
100
- const value = raw?.trim().toLowerCase();
101
- if (value === "none" || value === "elevenlabs") return value;
102
- return apiKey || backendUrl ? "elevenlabs" : "none";
121
+ function parseTtsProvider(params) {
122
+ const value = params.raw?.trim().toLowerCase();
123
+ if (value === "none" || value === "hume" || value === "elevenlabs") return value;
124
+ if (params.humeApiKey || params.backendUrl) return "hume";
125
+ return params.elevenLabsApiKey ? "elevenlabs" : "none";
103
126
  }
104
127
  function detectPythonBinary(explicitPython) {
105
128
  const venvPython = resolveVenvPythonBin();
@@ -216,8 +239,14 @@ function resolveVoiceRuntimeOptions(opts) {
216
239
  const sttProvider = parseSttProvider(sttProviderRaw, explicitAgoraCommand, backendUrl);
217
240
  const bundledAgoraCommand = sttProvider === "agora" ? buildBundledAgoraBridgeCommand(backendUrl) : void 0;
218
241
  const agoraSttCommand = explicitAgoraCommand ?? bundledAgoraCommand;
242
+ const humeApiKey = trimToUndefined(opts.humeApiKey) ?? trimToUndefined(process.env.VORA_HUME_API_KEY) ?? trimToUndefined(process.env.HUME_API_KEY);
219
243
  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);
244
+ const ttsProvider = parseTtsProvider({
245
+ raw: trimToUndefined(opts.ttsProvider) ?? trimToUndefined(process.env.VORA_VOICE_TTS_PROVIDER),
246
+ humeApiKey,
247
+ elevenLabsApiKey,
248
+ backendUrl
249
+ });
221
250
  return {
222
251
  gateway: {
223
252
  url: trimToUndefined(opts.url),
@@ -248,11 +277,21 @@ function resolveVoiceRuntimeOptions(opts) {
248
277
  tts: {
249
278
  provider: ttsProvider,
250
279
  backendUrl,
280
+ timeoutMs: parseMs(opts.ttsTimeoutMs, DEFAULT_TTS_TIMEOUT_MS),
281
+ humeApiKey,
282
+ humeVoiceId: trimToUndefined(opts.humeVoiceId) ?? trimToUndefined(process.env.VORA_HUME_VOICE_ID) ?? trimToUndefined(process.env.HUME_VOICE_ID) ?? DEFAULT_HUME_VOICE_ID,
283
+ humeSpeed: parseBoundedNumber(trimToUndefined(opts.humeSpeed) ?? trimToUndefined(process.env.VORA_HUME_SPEED) ?? trimToUndefined(process.env.HUME_SPEED), DEFAULT_HUME_SPEED, .5, 2),
251
284
  elevenLabsApiKey,
252
285
  elevenLabsVoiceId: trimToUndefined(opts.elevenLabsVoiceId) ?? trimToUndefined(process.env.VORA_ELEVENLABS_VOICE_ID) ?? trimToUndefined(process.env.ELEVENLABS_VOICE_ID) ?? DEFAULT_ELEVENLABS_VOICE_ID,
253
286
  elevenLabsModelId: trimToUndefined(opts.elevenLabsModelId) ?? trimToUndefined(process.env.VORA_ELEVENLABS_MODEL_ID) ?? trimToUndefined(process.env.ELEVENLABS_MODEL_ID) ?? DEFAULT_ELEVENLABS_MODEL_ID,
254
287
  elevenLabsOutputFormat: trimToUndefined(opts.elevenLabsOutputFormat) ?? trimToUndefined(process.env.VORA_ELEVENLABS_OUTPUT_FORMAT) ?? trimToUndefined(process.env.ELEVENLABS_OUTPUT_FORMAT) ?? DEFAULT_ELEVENLABS_OUTPUT_FORMAT
255
288
  },
289
+ conversation: {
290
+ followUpEnabled: opts.followUp !== false,
291
+ followUpMs: parseMs(opts.followUpMs, DEFAULT_FOLLOW_UP_MS),
292
+ followUpMaxTurns: parseBoundedInt(opts.followUpMaxTurns, DEFAULT_FOLLOW_UP_MAX_TURNS, 0, 25),
293
+ followUpSttTimeoutMs: parseMs(opts.followUpSttTimeoutMs, DEFAULT_FOLLOW_UP_STT_TIMEOUT_MS)
294
+ },
256
295
  once: Boolean(opts.once)
257
296
  };
258
297
  }
@@ -474,7 +513,40 @@ async function runVoiceDoctor(opts) {
474
513
  ok: true,
475
514
  message: "manual (type transcript in terminal)"
476
515
  });
477
- if (resolved.tts.provider === "elevenlabs") {
516
+ if (resolved.tts.provider === "hume") {
517
+ if (resolved.tts.backendUrl) {
518
+ let humeReady = backendProviderStatus(backendHealth, "hume");
519
+ if (humeReady === void 0) try {
520
+ const humeStatus = await fetchJsonWithTimeout(`${resolved.tts.backendUrl}/api/tts/hume`, DEFAULT_BACKEND_PROBE_TIMEOUT_MS);
521
+ humeReady = humeStatus && typeof humeStatus === "object" && humeStatus.configured === true || humeStatus?.ok === true;
522
+ } catch {
523
+ humeReady = void 0;
524
+ }
525
+ checks.push({
526
+ key: "backend_hume",
527
+ label: "backend Hume",
528
+ ok: backendHealthOk && humeReady === true,
529
+ message: humeReady === true ? `configured on backend (speed=${resolved.tts.humeSpeed})` : humeReady === false ? "backend is missing Hume env" : "backend health does not expose Hume status"
530
+ });
531
+ } else checks.push({
532
+ key: "hume_api_key",
533
+ label: "Hume API key",
534
+ ok: Boolean(resolved.tts.humeApiKey),
535
+ message: resolved.tts.humeApiKey ? "configured" : "missing VORA_HUME_API_KEY/HUME_API_KEY"
536
+ });
537
+ checks.push({
538
+ key: "hume_voice_id",
539
+ label: "Hume voice id",
540
+ ok: Boolean(resolved.tts.humeVoiceId),
541
+ message: resolved.tts.humeVoiceId
542
+ });
543
+ checks.push({
544
+ key: "hume_speed",
545
+ label: "Hume speed",
546
+ ok: resolved.tts.humeSpeed >= .5 && resolved.tts.humeSpeed <= 2,
547
+ message: String(resolved.tts.humeSpeed)
548
+ });
549
+ } else if (resolved.tts.provider === "elevenlabs") {
478
550
  if (resolved.tts.backendUrl) {
479
551
  const elevenLabsReady = backendProviderStatus(backendHealth, "elevenlabs");
480
552
  checks.push({
@@ -897,14 +969,15 @@ function extractTranscriptFromCommandOutput(rawOutput) {
897
969
  } catch {}
898
970
  return trimmed;
899
971
  }
900
- async function transcribeSpeech(resolved) {
901
- if (resolved.stt.provider === "manual") return (await promptLine("Say command (type transcript): ")).trim();
972
+ async function transcribeSpeech(resolved, opts) {
973
+ if (resolved.stt.provider === "manual") return (await promptLine(opts?.prompt ?? "Say command (type transcript): ")).trim();
902
974
  const command = resolved.stt.agoraCommand;
903
975
  if (!command) throw new Error("Agora STT provider selected but no bridge command is available. Set --agora-stt-command or VORA_AGORA_STT_COMMAND.");
976
+ const timeoutMs = opts?.timeoutMs ?? resolved.stt.timeoutMs;
904
977
  const result = await runShellCommand(applyTemplate(command, {
905
978
  lang: resolved.stt.language,
906
- timeout_ms: String(resolved.stt.timeoutMs)
907
- }), resolved.stt.timeoutMs + DEFAULT_STT_BRIDGE_COMMAND_GRACE_MS);
979
+ timeout_ms: String(timeoutMs)
980
+ }), timeoutMs + DEFAULT_STT_BRIDGE_COMMAND_GRACE_MS);
908
981
  if (result.code !== 0) throw new Error(`Agora STT bridge failed (exit ${result.code}): ${result.stderr.trim() || "no stderr"}`);
909
982
  return extractTranscriptFromCommandOutput(result.stdout);
910
983
  }
@@ -958,11 +1031,76 @@ async function waitForAssistantReply(params) {
958
1031
  }
959
1032
  return null;
960
1033
  }
1034
+ async function fetchResponseWithTimeout(url, init, timeoutMs) {
1035
+ const controller = new AbortController();
1036
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1037
+ try {
1038
+ return await fetch(url, {
1039
+ ...init,
1040
+ signal: controller.signal
1041
+ });
1042
+ } finally {
1043
+ clearTimeout(timer);
1044
+ }
1045
+ }
1046
+ function prepareTtsText(text) {
1047
+ const normalized = text.replace(/\s+/g, " ").trim();
1048
+ if (normalized.length <= DEFAULT_TTS_MAX_CHARS) return normalized;
1049
+ return `${normalized.slice(0, DEFAULT_TTS_MAX_CHARS).trim()} ...`;
1050
+ }
1051
+ async function synthesizeHumeAudio(params) {
1052
+ const attempts = [];
1053
+ if (params.backendUrl) attempts.push({
1054
+ label: "backend",
1055
+ run: () => fetchResponseWithTimeout(`${params.backendUrl}/api/tts/hume`, {
1056
+ method: "POST",
1057
+ headers: { "Content-Type": "application/json" },
1058
+ body: JSON.stringify({
1059
+ text: params.text,
1060
+ voiceId: params.voiceId,
1061
+ speed: params.speed
1062
+ })
1063
+ }, params.timeoutMs)
1064
+ });
1065
+ if (params.apiKey) attempts.push({
1066
+ label: "direct",
1067
+ run: () => fetchResponseWithTimeout("https://api.hume.ai/v0/tts/file", {
1068
+ method: "POST",
1069
+ headers: {
1070
+ "Content-Type": "application/json",
1071
+ "X-Hume-Api-Key": params.apiKey ?? ""
1072
+ },
1073
+ body: JSON.stringify({
1074
+ utterances: [{
1075
+ text: params.text,
1076
+ voice: { id: params.voiceId },
1077
+ speed: params.speed
1078
+ }],
1079
+ format: { type: "mp3" },
1080
+ num_generations: 1
1081
+ })
1082
+ }, params.timeoutMs)
1083
+ });
1084
+ let lastError;
1085
+ for (const attempt of attempts) {
1086
+ const response = await attempt.run();
1087
+ if (!response.ok) {
1088
+ const detail = await response.text().catch(() => "");
1089
+ lastError = /* @__PURE__ */ new Error(`Hume TTS ${attempt.label} request failed (${response.status}): ${detail.slice(0, 280) || "empty response"}`);
1090
+ continue;
1091
+ }
1092
+ const audioBytes = Buffer.from(await response.arrayBuffer());
1093
+ const filePath = path.join(os.tmpdir(), `vora-hume-${Date.now()}-${randomUUID().slice(0, 8)}.mp3`);
1094
+ await promises.writeFile(filePath, audioBytes);
1095
+ return filePath;
1096
+ }
1097
+ throw lastError ?? /* @__PURE__ */ new Error("Hume TTS unavailable: missing backend URL or API key");
1098
+ }
961
1099
  async function synthesizeElevenLabsAudio(params) {
962
1100
  const attempts = [];
963
1101
  if (params.backendUrl) attempts.push({
964
1102
  label: "backend",
965
- run: () => fetch(`${params.backendUrl}/api/tts/elevenlabs`, {
1103
+ run: () => fetchResponseWithTimeout(`${params.backendUrl}/api/tts/elevenlabs`, {
966
1104
  method: "POST",
967
1105
  headers: { "Content-Type": "application/json" },
968
1106
  body: JSON.stringify({
@@ -971,11 +1109,11 @@ async function synthesizeElevenLabsAudio(params) {
971
1109
  modelId: params.modelId,
972
1110
  outputFormat: params.outputFormat
973
1111
  })
974
- })
1112
+ }, params.timeoutMs)
975
1113
  });
976
1114
  if (params.apiKey) attempts.push({
977
1115
  label: "direct",
978
- run: () => fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(params.voiceId)}?output_format=${encodeURIComponent(params.outputFormat)}`, {
1116
+ run: () => fetchResponseWithTimeout(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(params.voiceId)}?output_format=${encodeURIComponent(params.outputFormat)}`, {
979
1117
  method: "POST",
980
1118
  headers: {
981
1119
  Accept: "audio/mpeg",
@@ -986,7 +1124,7 @@ async function synthesizeElevenLabsAudio(params) {
986
1124
  text: params.text,
987
1125
  model_id: params.modelId
988
1126
  })
989
- })
1127
+ }, params.timeoutMs)
990
1128
  });
991
1129
  let lastError;
992
1130
  for (const attempt of attempts) {
@@ -1011,6 +1149,23 @@ function hasBinary(command) {
1011
1149
  }
1012
1150
  async function playAudioFile(filePath) {
1013
1151
  if (process.platform === "darwin" && hasBinary("afplay")) return (await runShellCommand(`afplay "${filePath.replaceAll("\"", "\\\"")}"`, 12e4)).code === 0;
1152
+ if (process.platform === "win32" && hasBinary("powershell")) return (await runShellCommand([
1153
+ "powershell",
1154
+ "-NoProfile",
1155
+ "-ExecutionPolicy",
1156
+ "Bypass",
1157
+ "-STA",
1158
+ "-EncodedCommand",
1159
+ encodePowerShellCommand([
1160
+ "Add-Type -AssemblyName PresentationCore;",
1161
+ "$player = New-Object System.Windows.Media.MediaPlayer;",
1162
+ `$player.Open([Uri]${quotePowerShellLiteral(filePath)});`,
1163
+ "$player.Play();",
1164
+ "for ($i = 0; $i -lt 100 -and -not $player.NaturalDuration.HasTimeSpan; $i++) { Start-Sleep -Milliseconds 50 };",
1165
+ "if ($player.NaturalDuration.HasTimeSpan) { Start-Sleep -Milliseconds ([int]$player.NaturalDuration.TimeSpan.TotalMilliseconds + 250) } else { Start-Sleep -Milliseconds 5000 };",
1166
+ "$player.Close();"
1167
+ ].join(" "))
1168
+ ].join(" "), 12e4)).code === 0;
1014
1169
  if (process.platform === "linux" && hasBinary("ffplay")) return (await runShellCommand(`ffplay -nodisp -autoexit -loglevel quiet "${filePath.replaceAll("\"", "\\\"")}"`, 12e4)).code === 0;
1015
1170
  return false;
1016
1171
  }
@@ -1044,24 +1199,54 @@ async function speakWithSystemVoice(text) {
1044
1199
  }
1045
1200
  }
1046
1201
  async function speakReply(resolved, text) {
1047
- if (resolved.tts.provider !== "elevenlabs") return;
1202
+ if (resolved.tts.provider === "none") return;
1203
+ const ttsText = prepareTtsText(text);
1204
+ if (!ttsText) return;
1205
+ if (resolved.tts.provider === "hume") {
1206
+ if (!resolved.tts.backendUrl && !resolved.tts.humeApiKey) {
1207
+ defaultRuntime.error("[voice] Hume TTS skipped: missing backend/API key");
1208
+ return;
1209
+ }
1210
+ let audioPath;
1211
+ try {
1212
+ audioPath = await synthesizeHumeAudio({
1213
+ text: ttsText,
1214
+ apiKey: resolved.tts.humeApiKey,
1215
+ backendUrl: resolved.tts.backendUrl,
1216
+ voiceId: resolved.tts.humeVoiceId,
1217
+ speed: resolved.tts.humeSpeed,
1218
+ timeoutMs: resolved.tts.timeoutMs
1219
+ });
1220
+ } catch (error) {
1221
+ defaultRuntime.error(`[voice] Hume TTS failed: ${String(error)}; using system fallback`);
1222
+ if (!await speakWithSystemVoice(ttsText)) defaultRuntime.error("[voice] system TTS fallback unavailable");
1223
+ return;
1224
+ }
1225
+ try {
1226
+ if (!await playAudioFile(audioPath)) defaultRuntime.log(`[voice] Hume audio generated: ${audioPath}`);
1227
+ } finally {
1228
+ await promises.rm(audioPath, { force: true }).catch(() => void 0);
1229
+ }
1230
+ return;
1231
+ }
1048
1232
  if (!resolved.tts.backendUrl && !resolved.tts.elevenLabsApiKey) {
1049
- if (!await speakWithSystemVoice(text)) defaultRuntime.error("[voice] ElevenLabs TTS skipped: missing API key");
1233
+ if (!await speakWithSystemVoice(ttsText)) defaultRuntime.error("[voice] ElevenLabs TTS skipped: missing API key");
1050
1234
  return;
1051
1235
  }
1052
1236
  let audioPath;
1053
1237
  try {
1054
1238
  audioPath = await synthesizeElevenLabsAudio({
1055
- text,
1239
+ text: ttsText,
1056
1240
  apiKey: resolved.tts.elevenLabsApiKey,
1057
1241
  backendUrl: resolved.tts.backendUrl,
1058
1242
  voiceId: resolved.tts.elevenLabsVoiceId,
1059
1243
  modelId: resolved.tts.elevenLabsModelId,
1060
- outputFormat: resolved.tts.elevenLabsOutputFormat
1244
+ outputFormat: resolved.tts.elevenLabsOutputFormat,
1245
+ timeoutMs: resolved.tts.timeoutMs
1061
1246
  });
1062
1247
  } catch (error) {
1063
1248
  defaultRuntime.error(`[voice] ElevenLabs TTS failed: ${String(error)}`);
1064
- if (!await speakWithSystemVoice(text)) defaultRuntime.error("[voice] system TTS fallback unavailable");
1249
+ if (!await speakWithSystemVoice(ttsText)) defaultRuntime.error("[voice] system TTS fallback unavailable");
1065
1250
  return;
1066
1251
  }
1067
1252
  try {
@@ -1070,19 +1255,211 @@ async function speakReply(resolved, text) {
1070
1255
  await promises.rm(audioPath, { force: true }).catch(() => void 0);
1071
1256
  }
1072
1257
  }
1258
+ function normalizeVoiceIntentText(text) {
1259
+ return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1260
+ }
1261
+ function isVoiceSleepIntent(text) {
1262
+ const normalized = normalizeVoiceIntentText(text);
1263
+ return [
1264
+ "sleep",
1265
+ "go to sleep",
1266
+ "stop listening",
1267
+ "stop listen",
1268
+ "that is all",
1269
+ "thats all",
1270
+ "thank you vora",
1271
+ "thanks vora",
1272
+ "nghi di",
1273
+ "dung nghe"
1274
+ ].includes(normalized);
1275
+ }
1276
+ function isRememberScreenIntent(text) {
1277
+ const normalized = normalizeVoiceIntentText(text);
1278
+ return [
1279
+ "remember what am i doing now",
1280
+ "remember what i am doing now",
1281
+ "remember what im doing now",
1282
+ "remember what i m doing now",
1283
+ "remember what i am doing",
1284
+ "remember what im doing",
1285
+ "remember this screen",
1286
+ "remember my screen",
1287
+ "save what i am doing",
1288
+ "save what im doing",
1289
+ "look at my screen and remember",
1290
+ "nho toi dang lam gi",
1291
+ "nho man hinh nay",
1292
+ "ghi nho man hinh",
1293
+ "ghi nho toi dang lam gi"
1294
+ ].some((phrase) => normalized.includes(phrase));
1295
+ }
1296
+ function truncateForVoiceContext(text, maxChars) {
1297
+ const normalized = text.replace(/\s+/g, " ").trim();
1298
+ if (normalized.length <= maxChars) return normalized;
1299
+ return `${normalized.slice(0, maxChars).trim()}...`;
1300
+ }
1301
+ function buildVoiceMessage(params) {
1302
+ const transcript = params.transcript.trim();
1303
+ if (!params.followUp || !params.state?.lastUser || !params.state.lastAssistant) return transcript;
1304
+ return [
1305
+ "[VORA voice follow-up: answer as part of the same spoken conversation. Use the previous turn only as lightweight context.]",
1306
+ `Previous user: ${truncateForVoiceContext(params.state.lastUser, 220)}`,
1307
+ `Previous VORA: ${truncateForVoiceContext(params.state.lastAssistant, 320)}`,
1308
+ "",
1309
+ `Latest user: ${transcript}`
1310
+ ].join("\n");
1311
+ }
1312
+ function buildRememberScreenPrompt(transcript) {
1313
+ return [
1314
+ "The user asked VORA to remember what they are doing now.",
1315
+ "Analyze the attached screenshot. Reply in 1-3 short sentences.",
1316
+ "State what the user appears to be doing and ask whether they want help continuing.",
1317
+ "Keep it natural and concise. Do not mention internal storage unless the user asks.",
1318
+ "",
1319
+ `User voice command: ${transcript}`
1320
+ ].join("\n");
1321
+ }
1322
+ async function captureScreenToPng() {
1323
+ const filePath = path.join(os.tmpdir(), `vora-screen-${Date.now()}-${randomUUID().slice(0, 8)}.png`);
1324
+ if (process.platform === "darwin") {
1325
+ const result = await runShellCommand(`screencapture -x ${quoteShell(filePath)}`, 2e4);
1326
+ if (result.code !== 0) throw new Error(result.stderr.trim() || "screencapture failed");
1327
+ return filePath;
1328
+ }
1329
+ if (process.platform === "win32" && hasBinary("powershell")) {
1330
+ const result = await runShellCommand([
1331
+ "powershell",
1332
+ "-NoProfile",
1333
+ "-ExecutionPolicy",
1334
+ "Bypass",
1335
+ "-STA",
1336
+ "-EncodedCommand",
1337
+ encodePowerShellCommand([
1338
+ "Add-Type -AssemblyName System.Windows.Forms;",
1339
+ "Add-Type -AssemblyName System.Drawing;",
1340
+ "$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;",
1341
+ "$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height;",
1342
+ "$graphics = [System.Drawing.Graphics]::FromImage($bitmap);",
1343
+ "$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size);",
1344
+ `$bitmap.Save(${quotePowerShellLiteral(filePath)}, [System.Drawing.Imaging.ImageFormat]::Png);`,
1345
+ "$graphics.Dispose();",
1346
+ "$bitmap.Dispose();"
1347
+ ].join(" "))
1348
+ ].join(" "), 2e4);
1349
+ if (result.code !== 0) throw new Error(result.stderr.trim() || "PowerShell screen capture failed");
1350
+ return filePath;
1351
+ }
1352
+ if (process.platform === "linux") {
1353
+ const commands = [
1354
+ hasBinary("gnome-screenshot") ? `gnome-screenshot -f ${quoteShell(filePath)}` : void 0,
1355
+ hasBinary("scrot") ? `scrot ${quoteShell(filePath)}` : void 0,
1356
+ hasBinary("import") ? `import -window root ${quoteShell(filePath)}` : void 0
1357
+ ].filter((entry) => Boolean(entry));
1358
+ for (const command of commands) if ((await runShellCommand(command, 2e4)).code === 0) return filePath;
1359
+ }
1360
+ throw new Error("screen capture is not available on this OS without an installed capture tool");
1361
+ }
1362
+ async function captureScreenAttachment() {
1363
+ const filePath = await captureScreenToPng();
1364
+ const bytes = await promises.readFile(filePath);
1365
+ return {
1366
+ filePath,
1367
+ attachment: {
1368
+ type: "image",
1369
+ mimeType: "image/png",
1370
+ fileName: path.basename(filePath),
1371
+ content: bytes.toString("base64")
1372
+ }
1373
+ };
1374
+ }
1375
+ async function persistScreenMemory(params) {
1376
+ const workspaceDir = resolveDefaultAgentWorkspaceDir();
1377
+ const memoryDir = path.join(workspaceDir, "memory", "voice-screen");
1378
+ await promises.mkdir(memoryDir, { recursive: true });
1379
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
1380
+ const safeStamp = stamp.replace(/[:.]/g, "-");
1381
+ let screenshotRel = "";
1382
+ if (params.screenshotPath) {
1383
+ const screenshotTarget = path.join(memoryDir, `${safeStamp}.png`);
1384
+ await promises.copyFile(params.screenshotPath, screenshotTarget).catch(() => void 0);
1385
+ screenshotRel = path.relative(workspaceDir, screenshotTarget).replace(/\\/g, "/");
1386
+ }
1387
+ const summary = truncateForVoiceContext(params.assistantReply, 900);
1388
+ const memoryPath = path.join(memoryDir, "screen-memory.md");
1389
+ await promises.appendFile(memoryPath, [
1390
+ `## ${stamp}`,
1391
+ "",
1392
+ `User asked: ${params.transcript}`,
1393
+ `VORA observed: ${summary}`,
1394
+ screenshotRel ? `Screenshot: ${screenshotRel}` : "",
1395
+ ""
1396
+ ].filter((line) => line !== "").join("\n") + "\n", "utf8");
1397
+ const heartbeatPath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
1398
+ let existing = "# HEARTBEAT.md\n";
1399
+ try {
1400
+ existing = await promises.readFile(heartbeatPath, "utf8");
1401
+ } catch {
1402
+ await promises.mkdir(path.dirname(heartbeatPath), { recursive: true });
1403
+ }
1404
+ const blockStart = "<!-- VORA_VOICE_LAST_SCREEN_START -->";
1405
+ const blockEnd = "<!-- VORA_VOICE_LAST_SCREEN_END -->";
1406
+ const block = [
1407
+ blockStart,
1408
+ "## Voice Screen Memory",
1409
+ "",
1410
+ `Last observed at: ${stamp}`,
1411
+ `Summary: ${summary}`,
1412
+ screenshotRel ? `Screenshot file: ${screenshotRel}` : "",
1413
+ "",
1414
+ "On next startup/heartbeat, briefly remind the user: \"Last time I saw you were working on this. Do you want help continuing?\"",
1415
+ "If you already reminded them in this active session, reply HEARTBEAT_OK unless there is a new task.",
1416
+ blockEnd
1417
+ ].filter((line) => line !== "").join("\n");
1418
+ const pattern = new RegExp(`${blockStart}[\\s\\S]*?${blockEnd}`);
1419
+ const next = pattern.test(existing) ? existing.replace(pattern, block) : `${existing.trimEnd()}\n\n${block}\n`;
1420
+ await promises.writeFile(heartbeatPath, next, "utf8");
1421
+ defaultRuntime.log(`[voice] screen memory saved: ${memoryPath}`);
1422
+ }
1423
+ async function prepareVoiceTurn(params) {
1424
+ if (!isRememberScreenIntent(params.transcript)) return { message: buildVoiceMessage(params) };
1425
+ try {
1426
+ const snapshot = await captureScreenAttachment();
1427
+ defaultRuntime.log("[voice] captured screen for memory");
1428
+ return {
1429
+ message: buildRememberScreenPrompt(params.transcript),
1430
+ attachments: [snapshot.attachment],
1431
+ rememberScreenshotPath: snapshot.filePath
1432
+ };
1433
+ } catch (error) {
1434
+ defaultRuntime.error(`[voice] screen capture failed: ${String(error)}`);
1435
+ return { message: [
1436
+ "The user asked VORA to remember what they are doing now, but screen capture failed.",
1437
+ `Capture error: ${String(error)}`,
1438
+ "Reply briefly with the failure and ask them to grant screen-recording permission or retry.",
1439
+ "",
1440
+ `User voice command: ${params.transcript}`
1441
+ ].join("\n") };
1442
+ }
1443
+ }
1073
1444
  async function runGatewayVoiceTurn(params) {
1074
1445
  const transcript = params.transcript.trim();
1075
- if (!transcript) return false;
1446
+ if (!transcript) return { replied: false };
1076
1447
  defaultRuntime.log(`You: ${transcript}`);
1448
+ const turn = await prepareVoiceTurn({
1449
+ transcript,
1450
+ state: params.state,
1451
+ followUp: params.followUp
1452
+ });
1077
1453
  const beforeText = latestAssistantText(normalizeHistoryMessages(await params.gatewayClient.loadHistory({
1078
1454
  sessionKey: params.resolved.gateway.sessionKey,
1079
1455
  limit: 100
1080
1456
  })));
1081
1457
  const run = await params.gatewayClient.sendChat({
1082
1458
  sessionKey: params.resolved.gateway.sessionKey,
1083
- message: transcript,
1459
+ message: turn.message,
1084
1460
  thinking: params.resolved.gateway.thinking,
1085
1461
  deliver: params.resolved.gateway.deliver,
1462
+ attachments: turn.attachments,
1086
1463
  timeoutMs: params.resolved.gateway.timeoutMs
1087
1464
  });
1088
1465
  defaultRuntime.log(`[voice] run started: ${run.runId}`);
@@ -1094,11 +1471,68 @@ async function runGatewayVoiceTurn(params) {
1094
1471
  });
1095
1472
  if (!reply) {
1096
1473
  defaultRuntime.error(`[voice] no assistant reply received within ${params.resolved.gateway.waitMs}ms`);
1097
- return false;
1474
+ return { replied: false };
1098
1475
  }
1099
1476
  defaultRuntime.log(`Vora: ${reply}`);
1100
1477
  await speakReply(params.resolved, reply);
1101
- return true;
1478
+ if (params.state) {
1479
+ params.state.lastUser = transcript;
1480
+ params.state.lastAssistant = reply;
1481
+ }
1482
+ if (turn.rememberScreenshotPath) await persistScreenMemory({
1483
+ transcript,
1484
+ assistantReply: reply,
1485
+ screenshotPath: turn.rememberScreenshotPath
1486
+ }).catch((error) => {
1487
+ defaultRuntime.error(`[voice] failed to save screen memory: ${String(error)}`);
1488
+ });
1489
+ return {
1490
+ replied: true,
1491
+ reply
1492
+ };
1493
+ }
1494
+ async function runVoiceConversation(params) {
1495
+ let transcript = params.firstTranscript.trim();
1496
+ let followUp = false;
1497
+ let followUpTurns = 0;
1498
+ while (transcript && !isVoiceSleepIntent(transcript)) {
1499
+ if (!(await runGatewayVoiceTurn({
1500
+ resolved: params.resolved,
1501
+ gatewayClient: params.gatewayClient,
1502
+ transcript,
1503
+ state: params.state,
1504
+ followUp
1505
+ })).replied) return;
1506
+ params.onSuccessfulTurn();
1507
+ if (params.resolved.once) return;
1508
+ if (!params.resolved.conversation.followUpEnabled || followUpTurns >= params.resolved.conversation.followUpMaxTurns) return;
1509
+ const followUpListenMs = Math.min(params.resolved.conversation.followUpMs, params.resolved.conversation.followUpSttTimeoutMs);
1510
+ defaultRuntime.log(`[voice] follow-up window open for ${Math.round(followUpListenMs / 1e3)}s; speak naturally, or stay quiet to sleep`);
1511
+ let nextTranscript = "";
1512
+ try {
1513
+ nextTranscript = (await transcribeSpeech(params.resolved, {
1514
+ timeoutMs: followUpListenMs,
1515
+ prompt: "Follow-up (blank to sleep): "
1516
+ })).trim();
1517
+ } catch (error) {
1518
+ if (String(error).toLowerCase().includes("timeout")) {
1519
+ defaultRuntime.log("[voice] follow-up silence; returning to wake word");
1520
+ return;
1521
+ }
1522
+ throw error;
1523
+ }
1524
+ if (!nextTranscript) {
1525
+ defaultRuntime.log("[voice] follow-up silence; returning to wake word");
1526
+ return;
1527
+ }
1528
+ if (isVoiceSleepIntent(nextTranscript)) {
1529
+ defaultRuntime.log("[voice] sleeping; wake word armed next");
1530
+ return;
1531
+ }
1532
+ followUp = true;
1533
+ followUpTurns += 1;
1534
+ transcript = nextTranscript;
1535
+ }
1102
1536
  }
1103
1537
  async function runVoiceLoop(opts) {
1104
1538
  const resolved = resolveVoiceRuntimeOptions(opts);
@@ -1108,6 +1542,7 @@ async function runVoiceLoop(opts) {
1108
1542
  const wakeDeps = checkWakePythonDependencies(resolved.wake.pythonBin);
1109
1543
  if (!wakeDeps.ok) throw new Error(formatWakePythonDependencyError(resolved.wake.pythonBin, wakeDeps.message));
1110
1544
  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.");
1545
+ 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, VORA_HUME_API_KEY, or HUME_API_KEY.");
1111
1546
  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.");
1112
1547
  const gatewayClient = await GatewayChatClient.connect({
1113
1548
  url: resolved.gateway.url,
@@ -1115,6 +1550,7 @@ async function runVoiceLoop(opts) {
1115
1550
  password: resolved.gateway.password
1116
1551
  });
1117
1552
  const wakeWord = new WakeWordEngine(resolved.wake);
1553
+ const conversationState = {};
1118
1554
  let busy = false;
1119
1555
  let turns = 0;
1120
1556
  let stopRequested = false;
@@ -1135,7 +1571,8 @@ async function runVoiceLoop(opts) {
1135
1571
  await runGatewayVoiceTurn({
1136
1572
  resolved,
1137
1573
  gatewayClient,
1138
- transcript: resolved.gateway.initialMessage
1574
+ transcript: resolved.gateway.initialMessage,
1575
+ state: conversationState
1139
1576
  });
1140
1577
  } catch (err) {
1141
1578
  defaultRuntime.error(`[voice] initial turn failed: ${String(err)}`);
@@ -1160,12 +1597,19 @@ async function runVoiceLoop(opts) {
1160
1597
  defaultRuntime.log("[voice] transcript empty; waiting for next wake trigger");
1161
1598
  return;
1162
1599
  }
1163
- if (!await runGatewayVoiceTurn({
1600
+ if (isVoiceSleepIntent(transcript)) {
1601
+ defaultRuntime.log("[voice] sleep command heard; wake word remains armed");
1602
+ return;
1603
+ }
1604
+ await runVoiceConversation({
1164
1605
  resolved,
1165
1606
  gatewayClient,
1166
- transcript
1167
- })) return;
1168
- turns += 1;
1607
+ firstTranscript: transcript,
1608
+ state: conversationState,
1609
+ onSuccessfulTurn: () => {
1610
+ turns += 1;
1611
+ }
1612
+ });
1169
1613
  if (resolved.once && turns >= 1) requestStop();
1170
1614
  } catch (err) {
1171
1615
  defaultRuntime.error(`[voice] turn failed: ${String(err)}`);
@@ -1184,6 +1628,7 @@ async function runVoiceLoop(opts) {
1184
1628
  await wakeWord.start();
1185
1629
  defaultRuntime.log(theme.heading("Voice Loop"));
1186
1630
  defaultRuntime.log(`wake=${resolved.wake.modelPath} threshold=${resolved.wake.threshold} stt=${resolved.stt.provider} tts=${resolved.tts.provider}`);
1631
+ if (resolved.conversation.followUpEnabled && !resolved.once) defaultRuntime.log(`follow-up=on max=${resolved.conversation.followUpMaxTurns} listen=${Math.round(Math.min(resolved.conversation.followUpMs, resolved.conversation.followUpSttTimeoutMs) / 1e3)}s`);
1187
1632
  defaultRuntime.log("Listening for wake word. Press Ctrl+C to stop. Run `vora voice doctor` if dependencies fail.");
1188
1633
  await new Promise((resolve) => {
1189
1634
  resolveStopLoop = resolve;
@@ -1198,10 +1643,10 @@ async function runVoiceLoop(opts) {
1198
1643
  }
1199
1644
  }
1200
1645
  function addVoiceOptions(cmd) {
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");
1646
+ 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|hume|elevenlabs>", "Voice reply provider").option("--tts-timeout-ms <ms>", "TTS synthesis timeout (ms)").option("--hume-api-key <key>", "Hume API key").option("--hume-voice-id <id>", "Hume voice ID").option("--hume-speed <speed>", "Hume speaking speed (0.5..2, default 1.2)").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").option("--no-follow-up", "Require wake word before every voice turn").option("--follow-up-ms <ms>", "Maximum follow-up listening window after each reply").option("--follow-up-max-turns <n>", "Maximum follow-up turns before re-arming wake word").option("--follow-up-stt-timeout-ms <ms>", "STT timeout for each follow-up turn");
1202
1647
  }
1203
1648
  function registerVoiceCli(program) {
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`));
1649
+ 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`));
1205
1650
  voice.action(async (opts) => {
1206
1651
  try {
1207
1652
  await runVoiceLoop(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vora-ai",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
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,2 +0,0 @@
1
- import { a as registerCompletionCli } from "./completion-cli-CuDyNItM.js";
2
- export { registerCompletionCli };