vora-ai 0.1.36 → 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.
- package/dist/.buildstamp +1 -1
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/cli-startup-metadata.json +1 -1
- package/dist/{command-registry-C-dQzQLt.js → command-registry-DoRR_ykS.js} +4 -4
- package/dist/{command-registry-D5bgvc-g.js → command-registry-oO4_XKsL.js} +1 -1
- package/dist/completion-cli-B6W-7yKI.js +2 -0
- package/dist/{completion-cli-CuDyNItM.js → completion-cli-BSVP90BO.js} +2 -2
- package/dist/{doctor-completion-1du2om2X.js → doctor-completion-CLfiq4-c.js} +1 -1
- package/dist/entry.js +2 -2
- package/dist/extensions/telegram/.vora-runtime-deps-stamp.json +1 -1
- package/dist/{gateway-chat-JO0UI9m6.js → gateway-chat-BT9D_xTh.js} +1 -0
- package/dist/{gateway-cli-D3jVKXtU.js → gateway-cli-DNaH7pBF.js} +1 -1
- package/dist/{help-BX3qk2j9.js → help-BWGrT64x.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{onboard-DwchOLHk.js → onboard-VNwsDuUB.js} +1 -1
- package/dist/{program-Dah2D_cO.js → program-q6W4mv2i.js} +2 -2
- package/dist/{prompt-select-styled-fmGo-EJl.js → prompt-select-styled-2R-9jWL7.js} +1 -1
- package/dist/{register.maintenance-BHLVtWhx.js → register.maintenance-DMZegmso.js} +1 -1
- package/dist/{register.onboard-BQXwigRU.js → register.onboard-CxR6bI1n.js} +1 -1
- package/dist/{register.setup-BdrATrzp.js → register.setup-BT2-kAsK.js} +1 -1
- package/dist/{register.subclis-Dx_SLWv5.js → register.subclis-B-9NwtNV.js} +7 -7
- package/dist/{register.subclis-CrmcN8zA.js → register.subclis-BQu4VqOI.js} +2 -2
- package/dist/{root-help-DDj9BENB.js → root-help-Tv4Oc4kX.js} +2 -2
- package/dist/{run-main-C6NIk22l.js → run-main-BWhbuxQt.js} +5 -5
- package/dist/{setup-CEe7DH30.js → setup-B35nGXkQ.js} +3 -3
- package/dist/{setup.finalize-tiOrCMkC.js → setup.finalize-BueEFawv.js} +4 -4
- package/dist/{subcli-descriptors-BTTrqeCm.js → subcli-descriptors-Cc423xKz.js} +1 -1
- package/dist/{tui-Bay3TR6u.js → tui-DCtnC9C0.js} +1 -1
- package/dist/{tui-cli-Cii_Df7D.js → tui-cli-CU809_m8.js} +1 -1
- package/dist/{update-cli-Dmyo-EkW.js → update-cli-BbcBxt3_.js} +3 -3
- package/dist/{voice-cli-BD1lHjLd.js → voice-cli-CXiYCrQf.js} +476 -31
- package/package.json +1 -1
- package/dist/completion-cli-BcvhPH1f.js +0 -2
package/dist/.buildstamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"builtAt":
|
|
1
|
+
{"builtAt":1776477728789,"head":"7cb18f8b1e45487f1627768901dac2dc10dda292"}
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
61df678f55f820c36800453e93a0b24dc4caa6d8ac7e6ec54d71e4885e06c512
|
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
"signal",
|
|
11
11
|
"imessage"
|
|
12
12
|
],
|
|
13
|
-
"rootHelpText": "\n🌊 VORA 0.1.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
85
|
+
(await import("./register.maintenance-DMZegmso.js")).registerMaintenanceCommands(program);
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
{
|
|
@@ -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-
|
|
7
|
-
import { n as registerCoreCliByName, t as getCoreCliCommandNames } from "./command-registry-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
});
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
255
|
+
(await import("./completion-cli-B6W-7yKI.js")).registerCompletionCli(program);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
];
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./subcli-descriptors-
|
|
2
|
-
import { n as loadValidatedConfigForPluginRegistration, r as registerSubCliByName } from "./register.subclis-
|
|
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-
|
|
4
|
-
import { t as configureProgramHelp } from "./help-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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/
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
19
|
-
import { t as runTui } from "./tui-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
31
|
-
import { r as ensureCompletionCacheExists, t as checkShellCompletionStatus } from "./doctor-completion-
|
|
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-
|
|
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(
|
|
100
|
-
const value = raw?.trim().toLowerCase();
|
|
101
|
-
if (value === "none" || value === "elevenlabs") return value;
|
|
102
|
-
|
|
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(
|
|
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 === "
|
|
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(
|
|
907
|
-
}),
|
|
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: () =>
|
|
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: () =>
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1168
|
-
|
|
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
|
|
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