{"_id":"@aethexai/react","_rev":"2-4ab96a18cdabe9870b0abf33371981a9","name":"@aethexai/react","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@aethexai/react","version":"0.1.0","keywords":["aethex","voice","webrtc","react","sdk","ai-agent"],"license":"MIT","_id":"@aethexai/react@0.1.0","maintainers":[{"name":"ayooluwao","email":"ayooluwa@aethexai.com"}],"homepage":"https://github.com/aethexai/aethex-react#readme","bugs":{"url":"https://github.com/aethexai/aethex-react/issues"},"dist":{"shasum":"a0180d4fb0d505f81cbc8bd6782d35a80f4e464a","tarball":"https://registry.npmjs.org/@aethexai/react/-/react-0.1.0.tgz","fileCount":31,"integrity":"sha512-W4Oo/Q2mXaYV+rYp47sHPn1BafXbIAC6npeTDDbVoKHqUGEsGqSYqSUxp786YC+GYCwg4yx3bYo8zwovPHN/5w==","signatures":[{"sig":"MEUCIQCbsndSFRuHleYptTU21cz+dcfiAplgC8gZ7S2vmaSb1AIgHWQg+wT39p8vJcHKboLyQG5z/voabtQPcAJrfIPOQqs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4999962},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./widgets":{"import":{"types":"./dist/widgets/index.d.ts","default":"./dist/widgets/index.js"},"require":{"types":"./dist/widgets/index.d.cts","default":"./dist/widgets/index.cjs"}},"./assets/*":"./dist/assets/*","./package.json":"./package.json"},"gitHead":"01ad5cc6cedea0f2cb9fa2abd2c24a213a054d3a","scripts":{"ci":"npm run format:check && npm run typecheck && npm run lint && npm run test:coverage && npm run build && npm run size && npm run check:exports","dev":"tsup --watch","docs":"typedoc","lint":"eslint .","size":"size-limit","test":"vitest run","build":"tsup","format":"prettier --write .","release":"npm run build && changeset publish","version":"changeset version","typecheck":"tsc --noEmit","test:watch":"vitest","format:check":"prettier --check .","check:exports":"publint && attw --pack . --profile node16","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"ayooluwao","email":"ayooluwa@aethexai.com"},"repository":{"url":"git+https://github.com/aethexai/aethex-react.git","type":"git"},"_npmVersion":"11.7.0","description":"React SDK for Aethex voice agents — useAethexCall() hides WebRTC entirely.","directories":{},"sideEffects":false,"_nodeVersion":"25.3.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","jsdom":"^25.0.0","react":"^18.3.1","eslint":"^9.39.4","vitest":"^2.1.0","globals":"^17.6.0","publint":"^0.2.12","typedoc":"^0.27.9","axe-core":"^4.12.1","prettier":"^3.4.2","react-dom":"^18.3.1","@eslint/js":"^9.39.4","size-limit":"^11.1.6","typescript":"^5.6.0","@types/node":"^20.14.0","@types/react":"^18.3.0","@changesets/cli":"^2.31.0","@types/react-dom":"^18.3.7","typescript-eslint":"^8.61.1","@vitest/coverage-v8":"^2.1.0","@arethetypeswrong/cli":"^0.18.3","@testing-library/react":"^16.0.1","eslint-config-prettier":"^9.1.0","@testing-library/jest-dom":"^6.9.1","eslint-plugin-react-hooks":"^5.2.0","@size-limit/preset-small-lib":"^11.1.6"},"peerDependencies":{"react":">=18","react-dom":">=18"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/react_0.1.0_1785204579941_0.40651689421086723","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@aethexai/react","version":"1.0.0","description":"React SDK for Aethex voice agents. useAethexCall() hides WebRTC entirely.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/aethexai/aethex-react.git"},"homepage":"https://github.com/aethexai/aethex-react#readme","bugs":{"url":"https://github.com/aethexai/aethex-react/issues"},"type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","react-native":"./dist/native.js","types":"./dist/index.d.ts","exports":{".":{"react-native":{"types":"./dist/native.d.ts","default":"./dist/native.js"},"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./core":{"import":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"require":{"types":"./dist/core/index.d.cts","default":"./dist/core/index.cjs"}},"./react":{"import":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"require":{"types":"./dist/react/index.d.cts","default":"./dist/react/index.cjs"}},"./widgets":{"react-native":{"types":"./dist/widgets/native.d.ts","default":"./dist/widgets/native.js"},"import":{"types":"./dist/widgets/index.d.ts","default":"./dist/widgets/index.js"},"require":{"types":"./dist/widgets/index.d.cts","default":"./dist/widgets/index.cjs"}},"./native":{"import":{"types":"./dist/native.d.ts","default":"./dist/native.js"},"require":{"types":"./dist/native.d.cts","default":"./dist/native.cjs"}},"./assets/*":"./dist/assets/*","./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"eslint .","format":"prettier --write .","format:check":"prettier --check .","size":"size-limit","docs":"typedoc","check:exports":"publint && attw --pack . --profile node16","ci":"npm run format:check && npm run typecheck && npm run lint && npm run test:coverage && npm run build && npm run size && npm run check:exports","version":"changeset version","release":"npm run build && changeset publish","prepublishOnly":"npm run build"},"peerDependencies":{"react":">=18","react-dom":">=18","react-native":">=0.70","react-native-webrtc":">=118.0.0","react-native-incall-manager":">=4.0.0","@shopify/react-native-skia":">=1.0.0","react-native-reanimated":">=3.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"react-native":{"optional":true},"react-native-webrtc":{"optional":true},"react-native-incall-manager":{"optional":true},"@shopify/react-native-skia":{"optional":true},"react-native-reanimated":{"optional":true}},"devDependencies":{"@arethetypeswrong/cli":"^0.18.3","@changesets/cli":"^2.31.0","@eslint/js":"^9.39.4","@size-limit/preset-small-lib":"^11.1.6","@testing-library/jest-dom":"^6.9.1","@testing-library/react":"^16.0.1","@types/node":"^20.14.0","@types/react":"^18.3.0","@types/react-dom":"^18.3.7","@vitest/coverage-v8":"^2.1.0","axe-core":"^4.12.1","eslint":"^9.39.4","eslint-config-prettier":"^9.1.0","eslint-plugin-react-hooks":"^5.2.0","globals":"^17.6.0","jsdom":"^25.0.0","prettier":"^3.4.2","publint":"^0.2.12","react":"^18.3.1","react-dom":"^18.3.1","size-limit":"^11.1.6","tsup":"^8.3.0","typedoc":"^0.27.9","typescript":"^5.6.0","typescript-eslint":"^8.61.1","vitest":"^2.1.0"},"engines":{"node":">=18"},"keywords":["aethex","voice","webrtc","react","sdk","ai-agent"],"gitHead":"686f988d93540af8d68fe77a55d8f8a8f01c5f1c","_id":"@aethexai/react@1.0.0","_nodeVersion":"25.3.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-HD9nHeeB3NAhuB9KnhJuAfzzU9M5XvHt7MF55sSs0T4qVwoDTCWYt9xHyzwDNuGu7zzN4+O0EdeotSRnCdsP2w==","shasum":"34fd9b53252eb99cfacfb806c160807510fcc0da","tarball":"https://registry.npmjs.org/@aethexai/react/-/react-1.0.0.tgz","fileCount":45,"unpackedSize":5974573,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDlfgikEwYC1em65gEp89V6Ifxh8JLWUpxGW0DStRBrIgIgZj32Ue0EN6+3hjjVu6G9Rq/rDlYRzQCDHVvFrvuY2/c="}]},"_npmUser":{"name":"ayooluwao","email":"ayooluwa@aethexai.com"},"directories":{},"maintainers":[{"name":"ayooluwao","email":"ayooluwa@aethexai.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/react_1.0.0_1785331745366_0.7284003623039332"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T02:09:39.830Z","modified":"2026-07-29T13:29:05.816Z","0.1.0":"2026-07-28T02:09:40.168Z","1.0.0":"2026-07-29T13:29:05.618Z"},"bugs":{"url":"https://github.com/aethexai/aethex-react/issues"},"license":"MIT","homepage":"https://github.com/aethexai/aethex-react#readme","keywords":["aethex","voice","webrtc","react","sdk","ai-agent"],"repository":{"type":"git","url":"git+https://github.com/aethexai/aethex-react.git"},"description":"React SDK for Aethex voice agents. useAethexCall() hides WebRTC entirely.","maintainers":[{"name":"ayooluwao","email":"ayooluwa@aethexai.com"}],"readme":"<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/aethexai/aethex-react/main/.github/assets/aethex-react-banner.png\" alt=\"Aethex React SDK\" width=\"100%\">\n</p>\n\n<h1 align=\"center\">Aethex React SDK</h1>\n\n<p align=\"center\">\n  <b>The React SDK for Aethex voice agents.</b><br>\n  Start a live voice call with one hook, on the web or in React Native.<br>\n  Drop in ready-made UI, or build your own. There is no WebRTC to wire up.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@aethexai/react\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/@aethexai/react?style=flat-square&logo=npm&logoColor=white&label=npm&labelColor=0B0E14&color=38BDF8&cacheSeconds=300\"></a>\n  <a href=\"https://www.npmjs.com/package/@aethexai/react\"><img alt=\"TypeScript\" src=\"https://img.shields.io/badge/types-TypeScript-1E293B?style=flat-square&logo=typescript&logoColor=white&labelColor=0B0E14\"></a>\n  <img alt=\"Module: ESM + CJS\" src=\"https://img.shields.io/badge/module-ESM%20%2B%20CJS-22D3EE?style=flat-square&labelColor=0B0E14\">\n  <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/badge/license-MIT-64748B?style=flat-square&labelColor=0B0E14\"></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://developers.aethexai.com/docs\"><b>Documentation</b></a> &nbsp;·&nbsp;\n  <a href=\"https://developers.aethexai.com/dashboard\">Dashboard</a> &nbsp;·&nbsp;\n  <a href=\"https://developers.aethexai.com/docs/api-reference\">API Reference</a> &nbsp;·&nbsp;\n  <a href=\"https://discord.gg/ccyuJNZm7x\">Discord</a> &nbsp;·&nbsp;\n  <a href=\"mailto:developers@aethexai.com\">Support</a>\n</p>\n\n<br>\n\n| 🎙️ Live voice                  | 🪝 One hook                     | 🧩 Drop-in widgets                            | ✨ Voice orb                        |\n| :----------------------------- | :------------------------------ | :-------------------------------------------- | :---------------------------------- |\n| Talk to any agent in real time | `useAethexCall()` runs the call | Orb with mute, hang-up, and feedback controls | A unique orb per agent, five styles |\n\n## Install\n\n```bash\nnpm install @aethexai/react react react-dom\n```\n\n`react` and `react-dom` are peer dependencies for the hook. The `./core` entry\nneeds neither. Ships ESM, CJS, and types. It is safe under SSR and StrictMode,\nand accessibility is checked with axe. Versioned with Changesets.\n\n## Quickstart\n\n```tsx\n\"use client\"\nimport { useAethexCall, useAudioLevel } from \"@aethexai/react\"\n\nexport function CallButton() {\n  const { status, isConnected, start, stop, remoteStream, error } = useAethexCall({\n    agentId: \"11111111-1111-1111-1111-111111111111\",\n    apiBaseUrl: \"https://your-proxy.example.com\", // a proxy, never a key\n    onConnected: () => console.log(\"live\"),\n    onError: (e) => console.error(e.code, e.recoverable),\n  })\n  const { level } = useAudioLevel(remoteStream)\n\n  if (error) return <p>Call failed: {error.code}</p>\n  return (\n    <button onClick={isConnected ? stop : start}>\n      {status === \"connecting\" ? \"Connecting…\" : isConnected ? `Hang up (${level.toFixed(2)})` : \"Call\"}\n    </button>\n  )\n}\n```\n\n`start()` never throws, so watch `status` and `error` instead. The hook is\nSSR-safe (no browser APIs at import or render) and StrictMode-safe (teardown is\nidempotent). The React entry ships a `\"use client\"` banner for the Next.js App\nRouter.\n\nFetch the transcript after the call (live transcription is not available):\n\n```ts\nimport { getTranscript } from \"@aethexai/react\"\nconst turns = await getTranscript({ apiBaseUrl, sessionId })\n```\n\n## Ephemeral tokens (skip the proxy)\n\nInstead of proxying every request, mint a short-lived token on your server and\nhand it to the client with `getToken`. The SDK then talks to the Aethex API\ndirectly, so you host only a one-line mint route instead of a full proxy. Drop\n`apiBaseUrl` and pass `getToken`:\n\n```tsx\nuseAethexCall({\n  agentId,\n  getToken: async () => {\n    const res = await fetch(\"/api/aethex-token\", { method: \"POST\" })\n    return (await res.json()).token\n  },\n})\n```\n\nYour mint route calls `POST /api/v1/conversation/token` with your API key and\nreturns the token. The [Cloudflare proxy example](./examples/cloudflare-proxy)\nexposes it at `POST /token`. The token is scoped to one agent and expires with\nthe call, so it is safe to use from the client.\n\nBrowser callers need their origin allow-listed for CORS on the Aethex API.\nReact Native has no such restriction, so `getToken` is the recommended flow on\nmobile.\n\n## Widgets\n\nReady-made, accessible components built on the hook (import from `@aethexai/react/widgets`):\n\n```tsx\nimport { AethexCallButton, AethexVoiceWidget, AethexVoiceOrb } from \"@aethexai/react/widgets\"\n\n<AethexCallButton agentId={AGENT} apiBaseUrl={PROXY} />\n<AethexVoiceWidget agentId={AGENT} apiBaseUrl={PROXY} title=\"Talk to Kora\" />\n```\n\nAccessibility is built in: real buttons with full keyboard support, state shown\nin the visible label, `aria-busy` while connecting (the button stays enabled so\nyou can cancel without losing focus), one `aria-live` status region, and an\n`aria-hidden` audio visualizer that falls back to a static indicator under\n`prefers-reduced-motion`. Style it with `className` or `style`. Set the error\ncolor with the `--aethex-error-color` CSS variable.\n\n### Voice orb\n\n`AethexVoiceOrb` is the drop-in voice button: an orb, a label, and live status in\na themeable capsule (light or dark). One line, no styling required.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/aethexai/aethex-react/main/.github/assets/aethex-react-capsule.png\" alt=\"AethexVoiceOrb capsule in light and dark themes\" width=\"100%\">\n</p>\n\n```tsx\n<AethexVoiceOrb agentId={AGENT} getToken={getToken} title=\"Talk to Kora\" />\n```\n\nThe orb is generated from the agent's name, so every agent gets its own colour\nand texture. `orbType` picks one of five textures; `agentName` sets the seed:\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/aethexai/aethex-react/main/.github/assets/aethex-react-orbs.png\" alt=\"The five orb styles: aurora, pulse, liquid, fluid, pixel\" width=\"100%\">\n</p>\n\n| `orbType` | Texture                           |\n| :-------- | :-------------------------------- |\n| `aurora`  | fine grain (the default)          |\n| `pulse`   | medium blocks, punchy             |\n| `liquid`  | smooth, no scanlines              |\n| `fluid`   | fine and flowing                  |\n| `pixel`   | chunky blocks with bold scanlines |\n\n```tsx\n<AethexVoiceOrb agentId={AGENT} getToken={getToken} orbType=\"liquid\" agentName=\"Kora\" />\n```\n\nIt floats in the bottom-right corner by default (`position: fixed`), the usual\nspot for a voice or chat bubble. Pass `float={false}` to inline it, or use\n`float=\"bottom-left\"`, `floatOffset`, and `zIndex` to adjust. The orb is still at\nidle and animates only while connecting or in a call. It respects\n`prefers-reduced-motion`.\n\nPrefer a video? Pass `videoSrc` to show a looping muted `<video>` clipped to a\ncircle. Two ready-made clips ship in the package:\n\n```tsx\nimport { AethexVoiceOrb } from \"@aethexai/react/widgets\"\n// Bundlers (Vite, webpack) resolve the asset URL for you:\nimport orb from \"@aethexai/react/assets/orb-green.webm\" // or orb-magenta.webm\n\n;<AethexVoiceOrb agentId={AGENT} apiBaseUrl={PROXY} videoSrc={orb} />\n```\n\nThe clips are not in the JS bundle, so the widgets stay small. They ship as\nseparate files. If you serve static files from a folder (for example Next.js\n`public/`), copy the clip out of the package. See the\n[Next example](./examples/next-app) (`scripts/copy-orbs.mjs`).\n\n### Call controls and feedback\n\nPass `controls` to add a mute toggle and a stylized red hang-up button under the\norb during a call, `showVolume` for an output-volume slider (web), and `feedback`\nfor a one-tap 👍 / 👎 rating once the call ends. All are off by default, so the\nbare orb stays a single tap-to-call button.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/aethexai/aethex-react/main/.github/assets/aethex-react-controls.png\" alt=\"In-call mute and red hang-up controls, and a post-call rating prompt\" width=\"100%\">\n</p>\n\n```tsx\n<AethexVoiceOrb agentId={AGENT} getToken={getToken} controls showVolume feedback />\n```\n\nBuilding your own UI? The hook exposes the same controls directly:\n\n```tsx\nconst {\n  isSpeaking, // true while the agent is talking (web)\n  isMuted,\n  setMuted,\n  toggleMute,\n  volume,\n  setOutputVolume, // 0..1; device-level on native\n  submitFeedback, // (rating 1..5, comment?) for the just-ended call\n} = useAethexCall({ agentId, getToken })\n```\n\n`submitFeedback` posts the rating for the call the token opened. It works after\nthe call ends, so you can prompt for a rating on the summary screen.\n\n## React Native (Expo)\n\nThe same hook runs in React Native. `@aethexai/react` ships a native build that\nMetro resolves automatically, so the import is identical:\n\n```tsx\nimport { useAethexCall } from \"@aethexai/react\"\n\nfunction CallButton({ agentId, getToken }) {\n  const { isConnected, start, stop } = useAethexCall({ agentId, getToken })\n  return <Button title={isConnected ? \"Hang up\" : \"Talk\"} onPress={isConnected ? stop : start} />\n}\n```\n\nOn native the SDK runs on `react-native-webrtc` instead of browser WebRTC, and\nthe agent's audio plays through the device automatically. Use the\n[ephemeral-token flow](#ephemeral-tokens-skip-the-proxy) (`getToken`) here: React\nNative has no CORS, so the app connects to the Aethex API directly with no proxy\nat all. WebRTC is a native module, so this needs an Expo **development build** (it\ndoes not run in Expo Go).\n\nInstall the native peers and add the config plugin:\n\n```bash\nnpx expo install react-native-webrtc @config-plugins/react-native-webrtc expo-dev-client\n```\n\n```json\n// app.json\n{\n  \"expo\": {\n    \"plugins\": [\n      [\n        \"@config-plugins/react-native-webrtc\",\n        { \"microphonePermission\": \"Allow $(PRODUCT_NAME) to use your microphone.\" }\n      ]\n    ]\n  }\n}\n```\n\nThen build to a device:\n\n```bash\nnpx expo prebuild --clean\nnpx expo run:ios --device   # iOS needs a real device (the Simulator has no mic)\nnpx expo run:android        # an emulator is fine with host-mic input enabled\n```\n\n`AethexVoiceOrb` runs on native too, rendered with\n[`@shopify/react-native-skia`](https://shopify.github.io/react-native-skia/)\ninstead of a DOM canvas. Same props as the web orb, including `controls` and\n`feedback`, so the mute and hang-up buttons and the rating prompt work on device:\n\n```tsx\nimport { AethexVoiceOrb } from \"@aethexai/react/widgets\"\n\n;<AethexVoiceOrb agentId={AGENT} getToken={getToken} controls feedback />\n```\n\nAdd its peers alongside the WebRTC ones. `react-native-incall-manager` routes the\nagent to the loudspeaker, and Skia needs `react-native-reanimated` (add\n`react-native-reanimated/plugin` to `babel.config.js`, last in the list):\n\n```bash\nnpx expo install @shopify/react-native-skia react-native-reanimated react-native-incall-manager\n```\n\n`AethexVoiceWidget` and `AethexCallButton` are web-only; on native, use the orb\nor build on the hook. The audio-level hooks (`useAudioLevel`, `useAudioLevelRef`)\nand `isSpeaking` rely on Web Audio, so they read `0` on native; the orb\nself-animates during a call instead of reacting to the audio. A full runnable app\nis in [`examples/expo-app`](./examples/expo-app).\n\n## Core (framework-agnostic)\n\nThe core is a plain TypeScript WebRTC client. It talks to a proxy that keeps\nyour `ae_live_...` key on the server. The key must never reach the browser.\n\n```ts\nimport { VoiceCall } from \"@aethexai/react/core\"\n\nconst call = new VoiceCall({\n  agentId: \"11111111-1111-1111-1111-111111111111\",\n  apiBaseUrl: \"https://your-proxy.example.com\", // never the direct API or a key\n  callbacks: {\n    onStatusChange: (s) => console.log(s), // idle, connecting, connected, ended, error\n    onRemoteStream: (stream) => {\n      /* audio is played automatically through a managed <audio> */\n    },\n    onMetrics: (m) => console.log(m), // pipeline metrics from the `chat` channel\n    onError: (err) => console.error(err.code, err.recoverable),\n    onClose: () => console.log(\"call ended\"),\n  },\n})\n\nawait call.start()\n// …\ncall.stop() // idempotent teardown: stops the mic, closes the peer, tells the server\n```\n\n### Server-side status\n\n`getRemoteStatus()` returns the server's view of the session (duration, turn\ncount, lifecycle). This is different from `status`, which is the local WebRTC\nstate:\n\n```ts\nconst s = await getRemoteStatus() // { session_id, status, duration_s, turn_count, … }\n```\n\n### ICE restart\n\nIf a live call drops (network change, wifi to cellular), the SDK reconnects on\nits own by renegotiating (`restart_pc: true`) instead of failing. A brief\n`disconnected` is left to recover by itself. This is on by default.\n`maxIceRestarts` (default 1) caps how many failed attempts in a row it will try,\nand the budget resets after each clean reconnect. Set `iceRestart: false` to\nfail fast instead:\n\n```ts\nuseAethexCall({ agentId, apiBaseUrl, maxIceRestarts: 2 }) // recover harder\nuseAethexCall({ agentId, apiBaseUrl, iceRestart: false }) // fail fast\n```\n\nThis needs the proxy to forward the offer body as-is (the bundled example worker\ndoes).\n\n### Error handling\n\nEvery failure is an `AethexError` with a stable `code`:\n`unsupported_browser`, `mic_denied`, `mic_missing`, `connect_failed`,\n`offer_failed`, `quota_exceeded` (honors `Retry-After`), `payment_required`,\n`capacity`, `peer_failed`, `timeout`, `aborted`, `network`, `unknown`.\nUse `isAethexError(err)` to narrow the type, and `err.recoverable` to decide\nwhether to retry.\n\n## Security\n\n- Your API key must live in a proxy, never in client code. `apiBaseUrl` must\n  point to that proxy. The SDK throws if the URL looks like a key.\n- WebRTC and the microphone need HTTPS (or `localhost`).\n\nSee the [Cloudflare proxy example](./examples/cloudflare-proxy) for a small,\nproduction-shaped proxy that keeps your key on the server.\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck      # strict TS\nnpm run lint           # eslint (flat config, react-hooks)\nnpm test               # vitest (jsdom): core, hooks, widgets, SSR\nnpm run test:coverage  # with coverage thresholds\nnpm run build          # tsup: ESM, CJS, .d.ts, sourcemaps\nnpm run size           # size-limit budget\nnpm run check:exports  # publint + arethetypeswrong (node16 profile)\nnpm run ci             # the full gate (all of the above)\nnpm run docs           # typedoc to docs/api\n```\n\n## Release\n\nVersioned with [Changesets](https://github.com/changesets/changesets):\n\n```bash\nnpm run version   # apply pending changesets: bump version and CHANGELOG\nnpm run release   # build and publish (needs npm auth and access)\n```\n\n## License\n\nMIT © [AethexAI](https://aethexai.com)\n\n<p align=\"center\"><sub>Voice AI, built for the people it serves.</sub></p>\n","readmeFilename":"README.md"}