import packageJson from '../../package.json'; const APP_ENV = (import.meta.env.VITE_OPENHUMAN_APP_ENV as string | undefined) ?.trim() .toLowerCase(); const DEFAULT_BACKEND_URL = APP_ENV === 'staging' ? 'https://staging-api.tinyhumans.ai' : 'https://api.tinyhumans.ai'; /** * Build-time fallback for the Core JSON-RPC endpoint URL. * * **Not runtime-authoritative.** At runtime `getCoreRpcUrl()` (in * `services/coreRpcClient.ts`) is the source of truth: it first checks for a * URL stored by the user via the Welcome screen (`configPersistence`), then * falls back to this constant. Never read this constant directly from product * code that needs the live endpoint — call `getCoreRpcUrl()` instead. * * Override at build time via `VITE_OPENHUMAN_CORE_RPC_URL`. */ export const CORE_RPC_URL = import.meta.env.VITE_OPENHUMAN_CORE_RPC_URL || 'http://127.0.0.1:7788/rpc'; /** Matches core `OPENHUMAN_TOOL_TIMEOUT_SECS` (default 120s, max 3600s). */ const DEFAULT_TOOL_TIMEOUT_SECS = 120; const MAX_TOOL_TIMEOUT_SECS = 3600; function parseToolTimeoutSecs(): number { const raw = import.meta.env.VITE_TOOL_TIMEOUT_SECS as string | undefined; if (raw === undefined || raw === '') return DEFAULT_TOOL_TIMEOUT_SECS; const n = Number(raw); if (!Number.isFinite(n) || n <= 0 || n > MAX_TOOL_TIMEOUT_SECS) { return DEFAULT_TOOL_TIMEOUT_SECS; } return Math.round(n); } export const TOOL_TIMEOUT_SECS = parseToolTimeoutSecs(); /** * Per-request timeout for Core JSON-RPC `fetch()` calls, in milliseconds. * Without this the UI can hang indefinitely if the core sidecar stops * responding mid-flight. Bounded to [1s, 10min]; default 30s. Override with * `VITE_CORE_RPC_TIMEOUT_MS`. */ const DEFAULT_CORE_RPC_TIMEOUT_MS = 30_000; const MIN_CORE_RPC_TIMEOUT_MS = 1_000; const MAX_CORE_RPC_TIMEOUT_MS = 10 * 60 * 1_000; function parseCoreRpcTimeoutMs(): number { const raw = import.meta.env.VITE_CORE_RPC_TIMEOUT_MS as string | undefined; if (raw === undefined || raw === '') return DEFAULT_CORE_RPC_TIMEOUT_MS; const n = Number(raw); if (!Number.isFinite(n) || n < MIN_CORE_RPC_TIMEOUT_MS || n > MAX_CORE_RPC_TIMEOUT_MS) { return DEFAULT_CORE_RPC_TIMEOUT_MS; } return Math.round(n); } export const CORE_RPC_TIMEOUT_MS = parseCoreRpcTimeoutMs(); export const IS_DEV = import.meta.env.DEV; export const IS_PROD = import.meta.env.PROD; export const E2E_RESTART_APP_AS_RELOAD = import.meta.env.VITE_OPENHUMAN_E2E_RESTART_APP_AS_RELOAD === 'true'; export const E2E_DEFAULT_CORE_MODE = (import.meta.env.VITE_OPENHUMAN_E2E_DEFAULT_CORE_MODE as string | undefined) || ''; /** * True when the build behaves like a dev build for runtime purposes — either * a real `vite dev` (DEV=true) or a `vite build --mode development` (the E2E * harness — DEV=false but MODE='development'). `IS_DEV` alone is insufficient * for the E2E case because `vite build` always sets PROD=true / DEV=false * regardless of `--mode`. Consumers gating behavior that should NOT happen in * shipped binaries (e.g. the `restartApp` reload-instead-of-restart path) * should read this flag rather than touch `import.meta.env` directly. */ export const IS_DEV_LIKE = IS_DEV || import.meta.env.MODE === 'development'; /** Dev only: skip `.skip_onboarding` workspace check and ignore onboarded state so `/onboarding` always shows. Set `VITE_DEV_FORCE_ONBOARDING=true` in `.env.local`. */ export const DEV_FORCE_ONBOARDING = import.meta.env.DEV && import.meta.env.VITE_DEV_FORCE_ONBOARDING === 'true'; /** * Consumer-first-session UX (intent picker, home IA, trust affordances). * **Default off** so `main` stays unchanged until slices ship behind this flag. * Opt in locally or in staging: `VITE_CONSUMER_FIRST_SESSION=true` in `app/.env.local`. * Spec: `docs/plans/consumer-first-session-spec.md`. */ export const CONSUMER_FIRST_SESSION_ENABLED = import.meta.env.VITE_CONSUMER_FIRST_SESSION === 'true'; /** * Master kill-switch for chat multimodal attachments (image / video / document). * Enabled by default; the actual affordance is gated on the resolved model's * capability tier at the call site (`Conversations.tsx`) — images and video need * a vision-capable tier, documents flow on any model. Set * `VITE_CHAT_ATTACHMENTS=false` to hard-disable the whole feature for a build. */ export const CHAT_ATTACHMENTS_ENABLED = import.meta.env.VITE_CHAT_ATTACHMENTS !== 'false'; export const SKILLS_GITHUB_REPO = import.meta.env.VITE_SKILLS_GITHUB_REPO || 'tinyhumansai/openhuman-skills'; /** Google Analytics 4 Measurement ID. Leave blank to disable GA. */ export const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID as string | undefined; /** When true, allow GA in dev builds (for local debugging). Set `VITE_GA_FORCE_DEV=true` in `.env.local`. */ export const GA_FORCE_DEV = import.meta.env.VITE_GA_FORCE_DEV === 'true'; /** OpenPanel project client id. Leave blank to disable OpenPanel analytics. */ export const OPENPANEL_CLIENT_ID = ( (import.meta.env.VITE_OPENPANEL_CLIENT_ID as string | undefined) ?? 'e9c996d5-497f-4eec-9bde-630019ad525b' ).trim(); /** OpenPanel API base URL. */ export const OPENPANEL_API_URL = ( (import.meta.env.VITE_OPENPANEL_API_URL as string | undefined) ?? 'https://panel.tinyhumans.ai/api' ).trim(); /** Sentry DSN for error reporting. Leave blank to disable. */ export const SENTRY_DSN = import.meta.env.VITE_SENTRY_DSN as string | undefined; /** * Build-time fallback for the backend API base URL. * * **Not runtime-authoritative in Tauri.** In the desktop app, `getBackendUrl()` * (in `services/backendUrl.ts`) asks the core sidecar for the live API URL via * `openhuman.config_resolve_api_url`. If that call fails or returns an empty * URL, `getBackendUrl()` **throws** — it does not fall back to this constant. * This constant is only used in web/non-Tauri mode (where the sidecar is not * present). * * Override at build time via `VITE_BACKEND_URL`. */ export const BACKEND_URL = (import.meta.env.VITE_BACKEND_URL as string | undefined)?.trim() || DEFAULT_BACKEND_URL; /** Telegram bot username used for managed DM linking when backend does not return a launch URL. */ export const TELEGRAM_BOT_USERNAME = (import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined) || 'openhuman_bot'; /** Dev only: auto-inject JWT token to skip login flow. */ export const DEV_JWT_TOKEN = import.meta.env.DEV ? (import.meta.env.VITE_DEV_JWT_TOKEN as string | undefined) : undefined; export const APP_VERSION = packageJson.version; /** Desktop binary/package version reported with analytics events. */ export const APP_BINARY_VERSION = (import.meta.env.VITE_OPENHUMAN_BINARY_VERSION as string | undefined)?.trim() || APP_VERSION; /** Root Rust core crate version reported with analytics events. */ export const CORE_CARGO_VERSION = (import.meta.env.VITE_OPENHUMAN_CORE_CARGO_VERSION as string | undefined)?.trim() || APP_VERSION; /** Tauri shell Cargo crate version reported with analytics events. */ export const TAURI_CARGO_VERSION = (import.meta.env.VITE_OPENHUMAN_TAURI_CARGO_VERSION as string | undefined)?.trim() || APP_BINARY_VERSION; /** * Deployment environment reported to Sentry and other observability surfaces. * * Derived from `VITE_OPENHUMAN_APP_ENV` (set by CI for production / staging * bundles). Falls back to `development` in non-production builds so local * debugging never mingles with real user events. */ export const APP_ENVIRONMENT: 'production' | 'staging' | 'development' = IS_DEV ? 'development' : APP_ENV === 'staging' ? 'staging' : 'production'; /** Short git SHA baked in at build time (`VITE_BUILD_SHA`). Empty locally. */ export const BUILD_SHA = ((import.meta.env.VITE_BUILD_SHA as string | undefined) ?? '') .trim() .slice(0, 12); /** * Canonical Sentry release identifier: `openhuman@[+]`. * * Matches the tag the Rust core sidecar reports (see `src/main.rs`) so events * from the frontend, the core, and source-map uploads all group under the * same release in the Sentry dashboard. */ export const SENTRY_RELEASE = BUILD_SHA ? `openhuman@${APP_VERSION}+${BUILD_SHA}` : `openhuman@${APP_VERSION}`; /** * Minimum **desktop app** semver required for OAuth deep-link completion (`openhuman://oauth/success`). * * **Build-time embedding:** This value is baked into each shipped installer. Raising the floor for * users already on an older build requires them to install a **new** release (or use in-app update * when available)—changing CI vars alone does not retrofit existing binaries. For a fleet-wide * minimum that can move without a new app build, add a runtime policy endpoint later and consult it * here with this constant as fallback only. * * Set in production builds (e.g. GitHub Actions `vars`). Empty = no gate (default for local dev). */ export const MINIMUM_SUPPORTED_APP_VERSION = (import.meta.env.VITE_MINIMUM_SUPPORTED_APP_VERSION as string | undefined)?.trim() ?? ''; /** URL for the latest app release download page. Used for OAuth version-gate recovery and crash-recovery prompts. Override via VITE_LATEST_APP_DOWNLOAD_URL for deployment-specific download pages. */ export const LATEST_APP_DOWNLOAD_URL = (import.meta.env.VITE_LATEST_APP_DOWNLOAD_URL as string | undefined)?.trim() || 'https://github.com/tinyhumansai/openhuman/releases/latest'; /** Support page base URL. The crash screen appends `?ref=` so support can correlate a user's pasted Error ID to the exact Sentry event. Override via VITE_SUPPORT_URL for deployment-specific support endpoints. */ export const SUPPORT_URL = (import.meta.env.VITE_SUPPORT_URL as string | undefined)?.trim() || 'https://tinyhumans.ai/support'; /** * Set `VITE_SENTRY_SMOKE_TEST=true` in one build (or in `.env.local`) to * fire a one-shot diagnostic event at `initSentry()` time and verify the * Sentry pipeline end-to-end. Has no effect in normal builds. */ export const SENTRY_SMOKE_TEST = import.meta.env.VITE_SENTRY_SMOKE_TEST === 'true'; /** * ElevenLabs voice ID used for the mascot's reply speech. `JBFqnCBsd6RMkjVDRZzb` * is "George" — a warm multilingual voice that pairs cleanly with the * `eleven_multilingual_v2` model (`MASCOT_VOICE_MODEL_ID` below) so the * mascot can speak any locale we ship without a voice swap. Override with * `VITE_MASCOT_VOICE_ID` to A/B alternatives without a code change. */ export const MASCOT_VOICE_ID = (import.meta.env.VITE_MASCOT_VOICE_ID as string | undefined)?.trim() || 'JBFqnCBsd6RMkjVDRZzb'; /** * ElevenLabs model used for mascot reply speech. `eleven_multilingual_v2` * speaks every locale we ship; the older `eleven_monolingual_v1` would * choke on non-Latin scripts. Override with `VITE_MASCOT_VOICE_MODEL_ID` * to pin a different model (e.g. `eleven_turbo_v2_5` for lower latency * at the cost of accent fidelity). */ export const MASCOT_VOICE_MODEL_ID = (import.meta.env.VITE_MASCOT_VOICE_MODEL_ID as string | undefined)?.trim() || 'eleven_multilingual_v2'; /** * URL of the published mascot manifest (`dist/mascots.json` from the * `tinyhumansai/mascots` repo). This is the authoritative source for the * in-app mascot library — each entry names a Rive `.riv` runtime file plus its * `stateEngine` (poses, viseme codes, channels). Fetched directly over HTTPS * (the `raw.githubusercontent.com` host is CORS-open and allowed by the * webview CSP's `connect-src https:`). Override with `VITE_MASCOT_MANIFEST_URL` * to point at a fork or a locally-served manifest during development. */ export const MASCOT_MANIFEST_URL = (import.meta.env.VITE_MASCOT_MANIFEST_URL as string | undefined)?.trim() || 'https://raw.githubusercontent.com/tinyhumansai/mascots/main/dist/mascots.json';