diff --git a/evi/evi-typescript-control-plane/.env.example b/evi/evi-typescript-control-plane/.env.example
new file mode 100644
index 00000000..b4ea3f51
--- /dev/null
+++ b/evi/evi-typescript-control-plane/.env.example
@@ -0,0 +1 @@
+HUME_API_KEY=your_api_key_here
diff --git a/evi/evi-typescript-control-plane/README.md b/evi/evi-typescript-control-plane/README.md
new file mode 100644
index 00000000..bc064018
--- /dev/null
+++ b/evi/evi-typescript-control-plane/README.md
@@ -0,0 +1,150 @@
+
+

+
Empathic Voice Interface | Control Plane Example
+
+ Control active EVI chats from a trusted backend using the control plane API
+
+
+
+## Overview
+
+This project demonstrates how to use Hume's EVI Control Plane API to:
+
+- **Send control messages** to an active chat (`POST /chat/:chatId/send`)
+ - Update session settings (system prompt, voice ID, etc.)
+ - Set supplemental LLM API keys
+ - Send user messages
+- **Observe active chats** (`WSS /chat/:chatId/connect`)
+ - Receive full chat history on connect
+ - Stream live events in real-time
+ - Mirror chats for analytics, moderation, or logging
+
+The control plane works alongside your active Chat's data plane, allowing you to update session settings and attach mirrors from trusted servers without exposing secrets or disrupting the live stream.
+
+## Prerequisites
+
+- **Node.js**: Version `18.0.0` or higher
+- **npm** or **pnpm**: For package management
+- **Active EVI Chat**: You need an active chat and its `chatId` to use the control plane
+
+## Setup Instructions
+
+1. **Install dependencies:**
+
+ ```bash
+ npm install
+ # or
+ pnpm install
+ ```
+
+2. **Set your API key:**
+
+ Create a `.env` file in the project root:
+
+ ```sh
+ HUME_API_KEY="your_api_key_here"
+ ```
+
+ Or you can set it directly in the code (not recommended for production).
+
+## Usage
+
+### Getting a Chat ID
+
+Before you can use the control plane, you need an active chat ID. You can get one by:
+
+1. **Starting a chat** using the [EVI TypeScript Quickstart](https://github.com/HumeAI/hume-api-examples/tree/main/evi/evi-typescript-quickstart) example
+2. **Getting the chatId** from the `chat_metadata` event when the chat starts
+3. **Using the chat history API** to find an active chat
+
+### Running the Examples
+
+Run the control plane examples:
+
+```bash
+npm run dev
+```
+
+For example:
+
+```bash
+npm run dev abc123-def456-ghi789
+```
+
+### Example 1: Sending Control Messages
+
+The example demonstrates how to:
+
+- **Update system prompt**: Change the assistant's behavior mid-chat
+- **Update session settings**: Change voice ID, temperature, etc.
+- **Set supplemental LLM API key**: Rotate or set API keys server-side
+- **Send user messages**: Post messages to the chat from your backend
+
+### Example 2: Observing Active Chat
+
+The example demonstrates how to:
+
+- **Connect to an existing chat** using its `chatId`
+- **Receive full chat history** when you first connect
+- **Stream live events** as they happen in real-time
+- **Handle different event types**: user messages, assistant messages, audio output, errors, etc.
+
+## Code Examples
+
+### Send Control Message
+
+```typescript
+import { HumeClient } from "hume";
+
+const client = new HumeClient({ apiKey: "your-api-key" });
+
+// Update system prompt
+await client.empathicVoice.chat.send({
+ chatId: "your-chat-id",
+ message: {
+ type: "session_settings",
+ session_settings: {
+ system_prompt: "You are a helpful assistant.",
+ },
+ },
+});
+```
+
+### Observe Active Chat
+
+```typescript
+import { HumeClient } from "hume";
+
+const client = new HumeClient({ apiKey: "your-api-key" });
+
+const socket = client.empathicVoice.chat.connect({ chatId: "your-chat-id" });
+
+socket.on("open", () => {
+ console.log("Connected to chat");
+});
+
+socket.on("message", (event) => {
+ console.log("Received event:", event.type);
+ // Handle different event types
+});
+```
+
+## Key Features
+
+- **Server-side control**: Update chat settings without exposing secrets to the client
+- **Real-time observation**: Mirror active chats for analytics, moderation, or logging
+- **Full history replay**: Get complete chat history when connecting
+- **Bi-directional**: Send control messages (except audio_input) through the observation socket
+
+## Important Notes
+
+- The control plane only works with **currently active chats**
+- Use the [chat history APIs](https://dev.hume.ai/reference/speech-to-speech-evi/chats/list-chats) to fetch transcripts for past sessions
+- You cannot send `audio_input` through the control plane or observation socket
+- Authentication uses the same API key as your EVI chat connection
+
+## Learn More
+
+- [Control Plane Documentation](https://dev.hume.ai/docs/empathic-voice-interface-evi/control-plane)
+- [EVI Quickstart Guide](https://dev.hume.ai/docs/empathic-voice-interface-evi/quickstart/typescript)
+- [API Reference](https://dev.hume.ai/reference/speech-to-speech-evi)
diff --git a/evi/evi-typescript-control-plane/package-lock.json b/evi/evi-typescript-control-plane/package-lock.json
new file mode 100644
index 00000000..3d6f0442
--- /dev/null
+++ b/evi/evi-typescript-control-plane/package-lock.json
@@ -0,0 +1,665 @@
+{
+ "name": "hume-evi-typescript-control-plane",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hume-evi-typescript-control-plane",
+ "version": "0.0.0",
+ "dependencies": {
+ "dotenv": "^16.4.5",
+ "hume": "0.15.3-beta.2"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.18",
+ "tsx": "^4.7.0",
+ "typescript": "^5.2.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz",
+ "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+ "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/hume": {
+ "version": "0.15.3-beta.2",
+ "resolved": "https://registry.npmjs.org/hume/-/hume-0.15.3-beta.2.tgz",
+ "integrity": "sha512-+4F5SarRPvSLx+idel9/Dx0+2kvEnUxiQX51SO4U7fl1olRJPLuSNpivOUq6rgG7gsaV4KMZ55gXTAmx8plRAQ==",
+ "dependencies": {
+ "uuid": "9.0.1",
+ "ws": "^8.16.0",
+ "zod": "^3.23.8"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.20.6",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
+ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.25.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/evi/evi-typescript-control-plane/package.json b/evi/evi-typescript-control-plane/package.json
new file mode 100644
index 00000000..81f7873d
--- /dev/null
+++ b/evi/evi-typescript-control-plane/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "hume-evi-typescript-control-plane",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "tsx src/index.ts",
+ "build": "tsc",
+ "start": "node dist/index.js",
+ "test": "tsx src/test.ts"
+ },
+ "dependencies": {
+ "hume": "0.15.3-beta.2",
+ "dotenv": "^16.4.5"
+ },
+ "devDependencies": {
+ "@types/node": "^22.15.18",
+ "tsx": "^4.7.0",
+ "typescript": "^5.2.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/evi/evi-typescript-control-plane/src/index.ts b/evi/evi-typescript-control-plane/src/index.ts
new file mode 100644
index 00000000..352e7e95
--- /dev/null
+++ b/evi/evi-typescript-control-plane/src/index.ts
@@ -0,0 +1,216 @@
+import { HumeClient } from "hume";
+import type { Hume } from "hume";
+import * as dotenv from "dotenv";
+
+// Load environment variables
+dotenv.config();
+
+const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm";
+
+if (!API_KEY) {
+ throw new Error("HUME_API_KEY environment variable is required");
+}
+
+/**
+ * Send a control message to an active chat using the SDK's control plane API
+ */
+async function sendControlMessage(chatId: string, message: any): Promise {
+ const client = new HumeClient({ apiKey: API_KEY });
+ await client.empathicVoice.controlPlane.send(chatId, message);
+}
+
+/**
+ * Example 1: Send control messages to an active chat
+ *
+ * This demonstrates how to use POST /v0/evi/chat/:chatId/send
+ * to send control messages like updating session settings or system prompt.
+ */
+export async function sendControlMessages(chatId: string) {
+ console.log("\n=== Example 1: Sending Control Messages ===\n");
+ console.log(`Chat ID: ${chatId}`);
+
+ try {
+ // Example 1a: Update system prompt
+ console.log("\n1. Updating system prompt...");
+ await sendControlMessage(chatId, {
+ type: "session_settings",
+ session_settings: {
+ system_prompt: "You are a helpful assistant. Be concise and friendly.",
+ },
+ });
+ console.log("✓ System prompt updated successfully");
+
+ // Example 1b: Update session settings (e.g., voice ID)
+ console.log("\n2. Updating session settings (voice ID)...");
+ await sendControlMessage(chatId, {
+ type: "session_settings",
+ session_settings: {
+ voice_id: "5bb7de05-c8fe-426a-8fcc-ba4fc4ce9f9c",
+ },
+ });
+ console.log("✓ Voice ID updated successfully");
+
+ // Example 1c: Set supplemental LLM API key
+ console.log("\n3. Setting supplemental LLM API key...");
+ await sendControlMessage(chatId, {
+ type: "session_settings",
+ session_settings: {
+ supplemental_language_model_api_key: "your-supplemental-key-here",
+ },
+ });
+ console.log("✓ Supplemental LLM API key set successfully");
+
+ // Example 1d: Send a user message
+ console.log("\n4. Sending a user message...");
+ await sendControlMessage(chatId, {
+ type: "user_input",
+ text: "Hello from the control plane!",
+ });
+ console.log("✓ User message sent successfully");
+
+ } catch (error) {
+ console.error("Error sending control message:", error);
+ throw error;
+ }
+}
+
+/**
+ * Example 2: Connect to an existing active chat to observe it
+ *
+ * This demonstrates how to use WSS /chat/:chatId/connect
+ * to attach to a running chat and receive full history + live events.
+ */
+export async function observeActiveChat(chatId: string) {
+ const client = new HumeClient({ apiKey: API_KEY });
+
+ console.log("\n=== Example 2: Observing Active Chat ===\n");
+ console.log(`Connecting to chat: ${chatId}`);
+
+ return new Promise((resolve, reject) => {
+ // Use the SDK's control plane connect method
+ // Note: connect() returns a Promise that resolves to a ControlPlaneSocket
+ client.empathicVoice.controlPlane.connect({ chatId })
+ .then((socket) => {
+ console.log("✓ Control plane socket connected");
+ let eventCount = 0;
+
+ socket.on("open", () => {
+ console.log("✓ Connected to chat successfully");
+ console.log("Waiting for events (history replay + live events)...\n");
+ });
+
+ socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => {
+ eventCount++;
+ console.log(`[Event ${eventCount}] Type: ${event.type}`);
+
+ switch (event.type) {
+ case "chat_metadata":
+ const metadata = event as any;
+ console.log(" Chat metadata:", JSON.stringify(event, null, 2));
+ const metadataChatId = metadata.chat_id || metadata.chatId;
+ if (metadataChatId && metadataChatId !== chatId) {
+ console.log(` ⚠️ Warning: Metadata chatId (${metadataChatId}) differs from requested chatId (${chatId})`);
+ } else if (metadataChatId === chatId) {
+ console.log(` ✓ Confirmed: Metadata chatId matches requested chatId`);
+ }
+ break;
+ case "user_message":
+ const userMsg = event as any;
+ console.log(" User message event:", JSON.stringify(userMsg, null, 2));
+ const userContent = userMsg.user_message?.content || userMsg.message_text || userMsg.text || JSON.stringify(userMsg);
+ console.log(` User message content: ${userContent}`);
+ // Check if this matches our control plane message
+ if (userContent.includes("Hello from the control plane")) {
+ console.log(" ✓ This matches the message we sent via control plane!");
+ }
+ break;
+ case "assistant_message":
+ const assistantMsg = event as any;
+ const assistantContent = assistantMsg.assistant_message?.content || assistantMsg.message_text || JSON.stringify(assistantMsg);
+ console.log(` Assistant message: ${assistantContent}`);
+ break;
+ case "audio_output":
+ console.log(" Audio output received (base64 encoded)");
+ break;
+ case "user_interruption":
+ console.log(" User interruption detected");
+ break;
+ case "error":
+ const errorEvent = event as any;
+ console.error(` Error: ${errorEvent.error?.message || errorEvent.message || JSON.stringify(errorEvent)}`);
+ break;
+ default:
+ console.log(" Event data:", JSON.stringify(event, null, 2));
+ }
+ });
+
+ socket.on("error", (error: any) => {
+ console.error("Socket error:", error);
+ reject(error);
+ });
+
+ socket.on("close", (event: any) => {
+ console.log("\n✓ Socket closed:", event);
+ resolve();
+ });
+
+ // Auto-close after 30 seconds for demo purposes
+ // In production, you'd keep this open to observe the chat
+ setTimeout(() => {
+ console.log("\nClosing observation connection after 30 seconds...");
+ socket.close();
+ }, 30000);
+ })
+ .catch((error) => {
+ console.error("Failed to connect:", error);
+ reject(error);
+ });
+ });
+}
+
+/**
+ * Main function to demonstrate control plane usage
+ */
+async function main() {
+ console.log("Hume EVI Control Plane Example");
+ console.log("===============================\n");
+
+ // You need an active chat ID to use the control plane
+ // In a real scenario, you'd get this from:
+ // 1. Creating a chat and storing the chatId
+ // 2. Receiving it from a webhook event
+ // 3. Querying your chat history
+
+ // For this example, we'll prompt for a chatId
+ // If you have an active chat, replace this with your chatId
+ const chatId = process.argv[2];
+
+ if (!chatId) {
+ console.error("Error: Chat ID is required");
+ console.log("\nUsage:");
+ console.log(" npm run dev ");
+ console.log("\nTo get a chat ID:");
+ console.log(" 1. Start a chat using the EVI quickstart example");
+ console.log(" 2. Get the chatId from the chat_metadata event");
+ console.log(" 3. Or use the chat history API to find an active chat");
+ process.exit(1);
+ }
+
+ try {
+ // Example 1: Send control messages
+ await sendControlMessages(chatId);
+
+ // Example 2: Observe the active chat
+ await observeActiveChat(chatId);
+
+ console.log("\n✓ Control plane examples completed successfully!");
+ } catch (error) {
+ console.error("\n✗ Error running examples:", error);
+ process.exit(1);
+ }
+}
+
+// Run the examples only if this file is executed directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main().catch(console.error);
+}
diff --git a/evi/evi-typescript-control-plane/src/start-chat.ts b/evi/evi-typescript-control-plane/src/start-chat.ts
new file mode 100644
index 00000000..544b9dd7
--- /dev/null
+++ b/evi/evi-typescript-control-plane/src/start-chat.ts
@@ -0,0 +1,82 @@
+import { HumeClient } from "hume";
+import type { Hume } from "hume";
+import * as dotenv from "dotenv";
+
+// Load environment variables
+dotenv.config();
+
+const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm";
+
+if (!API_KEY) {
+ throw new Error("HUME_API_KEY environment variable is required");
+}
+
+/**
+ * Start a chat and return the chatId
+ * This creates a new chat connection and captures the chatId from chat_metadata
+ */
+export async function startChatAndGetId(): Promise {
+ return new Promise((resolve, reject) => {
+ const client = new HumeClient({ apiKey: API_KEY });
+ const socket = client.empathicVoice.chat.connect({});
+
+ let chatId: string | null = null;
+
+ socket.on("open", () => {
+ console.log("✓ Chat connection opened");
+ console.log("Waiting for chat_metadata to get chatId...\n");
+ });
+
+ socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => {
+ if (event.type === "chat_metadata") {
+ const metadata = event as any;
+ chatId = metadata.chat_id || metadata.chatId;
+
+ if (chatId) {
+ console.log(`✓ Chat started! Chat ID: ${chatId}\n`);
+ // Keep the socket open so the chat stays active
+ resolve(chatId);
+ } else {
+ console.error("Error: chatId not found in chat_metadata:", metadata);
+ reject(new Error("chatId not found in chat_metadata"));
+ }
+ } else {
+ console.log(`Received event: ${event.type}`);
+ }
+ });
+
+ socket.on("error", (error) => {
+ console.error("Socket error:", error);
+ reject(error);
+ });
+
+ socket.on("close", () => {
+ if (!chatId) {
+ reject(new Error("Socket closed before chatId was received"));
+ }
+ });
+
+ // Timeout after 10 seconds
+ setTimeout(() => {
+ if (!chatId) {
+ socket.close();
+ reject(new Error("Timeout waiting for chat_metadata"));
+ }
+ }, 10000);
+ });
+}
+
+// Run if called directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ startChatAndGetId()
+ .then((chatId) => {
+ console.log(`\nChat ID: ${chatId}`);
+ console.log("\nUse this chatId to test the control plane:");
+ console.log(`npm run dev ${chatId}`);
+ process.exit(0);
+ })
+ .catch((error) => {
+ console.error("Failed to start chat:", error);
+ process.exit(1);
+ });
+}
diff --git a/evi/evi-typescript-control-plane/src/test.ts b/evi/evi-typescript-control-plane/src/test.ts
new file mode 100644
index 00000000..a8fb28bb
--- /dev/null
+++ b/evi/evi-typescript-control-plane/src/test.ts
@@ -0,0 +1,148 @@
+import { HumeClient } from "hume";
+import type { Hume } from "hume";
+import * as dotenv from "dotenv";
+import { sendControlMessages, observeActiveChat } from "./index.js";
+
+// Load environment variables
+dotenv.config();
+
+const API_KEY = process.env.HUME_API_KEY || "cY7Yjq84riLl3HcGcRQos0h5HkAFF8cYLlBb1uOGKuYbCNpm";
+
+if (!API_KEY) {
+ throw new Error("HUME_API_KEY environment variable is required");
+}
+
+/**
+ * Start a chat and return the chatId
+ * This creates a new chat connection and captures the chatId from chat_metadata
+ * The connection is kept alive so the chat remains active for control plane testing
+ */
+async function startChatAndGetId(): Promise<{ chatId: string; socket: any }> {
+ return new Promise<{ chatId: string; socket: any }>((resolve, reject) => {
+ const client = new HumeClient({ apiKey: API_KEY });
+ const socket = client.empathicVoice.chat.connect({});
+
+ let chatId: string | null = null;
+
+ socket.on("open", () => {
+ console.log("✓ Chat connection opened");
+ console.log("Waiting for chat_metadata to get chatId...\n");
+ });
+
+ socket.on("message", (event: Hume.empathicVoice.chat.SubscribeEvent) => {
+ if (event.type === "chat_metadata") {
+ const metadata = event as any;
+ chatId = metadata.chat_id || metadata.chatId;
+
+ if (chatId) {
+ console.log(`✓ Chat started! Chat ID: ${chatId}\n`);
+ // Keep the socket open so the chat stays active
+ resolve({ chatId, socket });
+ } else {
+ console.error("Error: chatId not found in chat_metadata:", JSON.stringify(metadata, null, 2));
+ reject(new Error("chatId not found in chat_metadata"));
+ }
+ } else {
+ console.log(` Received event: ${event.type}`);
+ }
+ });
+
+ socket.on("error", (error) => {
+ console.error("Socket error:", error);
+ reject(error);
+ });
+
+ socket.on("close", () => {
+ if (!chatId) {
+ reject(new Error("Socket closed before chatId was received"));
+ }
+ });
+
+ // Timeout after 15 seconds
+ setTimeout(() => {
+ if (!chatId) {
+ socket.close();
+ reject(new Error("Timeout waiting for chat_metadata"));
+ }
+ }, 15000);
+ });
+}
+
+/**
+ * Main test function
+ */
+async function main() {
+ console.log("Hume EVI Control Plane Test");
+ console.log("============================\n");
+
+ let chatSocket: any = null;
+
+ try {
+ // Step 1: Start a chat and get the chatId
+ console.log("Step 1: Starting a new chat...");
+ const { chatId, socket } = await startChatAndGetId();
+ chatSocket = socket; // Keep reference to keep chat alive
+
+ console.log(`\nChat ID obtained: ${chatId}`);
+ console.log("Keeping chat connection alive for control plane testing...\n");
+
+ // Wait a moment for the chat to be fully initialized
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // Step 2: Test sending control messages
+ console.log("\n" + "=".repeat(50));
+ console.log("Step 2: Testing control plane - sending messages");
+ console.log("=".repeat(50));
+
+ try {
+ await sendControlMessages(chatId);
+ } catch (error) {
+ console.error("Error testing control messages:", error);
+ throw error;
+ }
+
+ // Step 3: Test observing the chat
+ console.log("\n" + "=".repeat(50));
+ console.log("Step 3: Testing control plane - observing chat");
+ console.log("=".repeat(50));
+
+ try {
+ // Run observation for 10 seconds
+ await Promise.race([
+ observeActiveChat(chatId),
+ new Promise(resolve => setTimeout(resolve, 10000)).then(() => {
+ console.log("\nObservation test completed (10 second timeout)");
+ })
+ ]);
+ } catch (error) {
+ console.error("Error testing observation:", error);
+ // Don't throw - observation might fail if SDK doesn't support chatId yet
+ }
+
+ console.log("\n" + "=".repeat(50));
+ console.log("✓ Control plane test completed!");
+ console.log("=".repeat(50));
+ console.log("\nChat is still active. You can test more by running:");
+ console.log(` npm run dev ${chatId}`);
+ console.log("\nClosing initial chat connection in 5 seconds...");
+
+ // Keep the chat alive for a bit more, then close
+ setTimeout(() => {
+ if (chatSocket) {
+ chatSocket.close();
+ console.log("Initial chat connection closed.");
+ }
+ process.exit(0);
+ }, 5000);
+
+ } catch (error) {
+ console.error("\n✗ Test failed:", error);
+ if (chatSocket) {
+ chatSocket.close();
+ }
+ process.exit(1);
+ }
+}
+
+// Run the test
+main().catch(console.error);
diff --git a/evi/evi-typescript-control-plane/tsconfig.json b/evi/evi-typescript-control-plane/tsconfig.json
new file mode 100644
index 00000000..7109bbed
--- /dev/null
+++ b/evi/evi-typescript-control-plane/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ES2022",
+ "lib": ["ES2022"],
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}