diff --git a/.cursor/rules/file-structure.mdc b/.cursor/rules/file-structure.mdc new file mode 100644 index 00000000..f526318e --- /dev/null +++ b/.cursor/rules/file-structure.mdc @@ -0,0 +1,12 @@ +--- +description: When creating files +alwaysApply: false +--- + +We want thin files that aim to have a single responsibility and a single export. +We should have a single export per file. + +Specific directories: +- hooks: for hooks +- components: for components +- pages: for pages \ No newline at end of file diff --git a/.cursor/rules/react-tsx.mdc b/.cursor/rules/react-tsx.mdc new file mode 100644 index 00000000..eeb9f669 --- /dev/null +++ b/.cursor/rules/react-tsx.mdc @@ -0,0 +1,84 @@ +--- +description: React and TSX component development guidelines +globs: *.tsx +alwaysApply: false +--- + +# Components + +- One component per file. +- Create smaller, focused components. +- Compose larger components from smaller ones. +- Logical groupings of components should form a new custom component and moved to a separate file. +- When prototyping, keep small components in the same file, but mark to be removed: + +```tsx +/** + * ExampleComponent + * Responsibilities: + * To be an example component + * + * TODO: Move to a separate file + */ +function ExampleComponent() { + return
ExampleComponent
; +} +``` + +# Hooks + +- Complex hooks should be broken down into smaller, composable hooks. +- If a hook can be self-contained, it should be self-contained and should be in a separate file. +- Break complex logic into smaller hooks. +- Compose hooks when it makes sense. +- Keep hooks in the same file as their components. + +# Code Separation + +- Separate business logic from UI rendering. +- Keep related code together. +- Maintain clear boundaries between concerns. + +# Composable Component Pattern + +When creating shared templates or reusable components: + +1. **Use compound component pattern** - Attach subcomponents as static properties (e.g., `Template.Header`, `Template.Content`) instead of prop drilling. + +2. **Templates are pure UI** - Templates should only handle layout/structure. Pages control conditional logic, data fetching, and business logic. + +3. **Subcomponents take their own props** - Each subcomponent (Header, Content, Footer, etc.) should accept its own typed props rather than the parent managing them. + +4. **Compose via children** - Use `children` for flexible composition. Pages decide what to render in each slot. + +**Example:** +```tsx +// Template +const EditDraftTemplate = ({ children, isLoading }) => { + if (isLoading) return ; + return {children}; +}; + +EditDraftTemplate.Header = ({ title, children }) => ( +
{title || children}
+); + +EditDraftTemplate.Content = ({ content, isStreaming }) => ( +
{content}
+); + +// Usage + + + + +``` + +**Avoid:** Prop drilling, templates with business logic, tightly coupled props. + +# Class Organization + +- Place public methods first. +- Place private methods at the bottom of the class. +- Group related methods together. +- Use clear method names that describe their purpose. diff --git a/.cursor/rules/typescript.mdc b/.cursor/rules/typescript.mdc new file mode 100644 index 00000000..645d6bb3 --- /dev/null +++ b/.cursor/rules/typescript.mdc @@ -0,0 +1,22 @@ +--- +description: +globs: *.tsx +alwaysApply: false +--- + +## Typescript Development Guidelines: + +### Documentation + - Add TSDoc style documentation when writing functions, methods and components. + +### Single Responsibility + - SRP is a core principle of our codebase and should be followed in all functions, methods and components. + => if the function, method or component is more than one responsibility, suggest how to break it down into smaller functions, methods or components. Encourage the user to use the "Single Responsibility" principle + and explain the benefits of doing so in this case. + +### Imports + - Always use named imports for React components and hooks. + - Never use non-destructured imports, such as `React.useMemo` or `React.useCallback`. + +### Type Safety + - Never use the `any` type. diff --git a/javascript/examples/vitest/.gitignore b/javascript/examples/vitest/.gitignore index 6eb32454..e8066c09 100644 --- a/javascript/examples/vitest/.gitignore +++ b/javascript/examples/vitest/.gitignore @@ -1 +1,4 @@ .scenario + +# Audio files +test-audio-output/ diff --git a/javascript/examples/vitest/package.json b/javascript/examples/vitest/package.json index 962c28be..2c88bb61 100644 --- a/javascript/examples/vitest/package.json +++ b/javascript/examples/vitest/package.json @@ -7,12 +7,14 @@ "scripts": { "test": "pnpm exec vitest", "typecheck": "tsc --noEmit", - "lint": "eslint ." + "lint": "eslint .", + "realtime-client": "pnpm -F realtime-client" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { + "concurrently": "^9.1.2", "dotenv": "^16.5.0", "dotenv-cli": "^8.0.0", "nanoid-cli": "^1.0.1", @@ -20,8 +22,12 @@ }, "dependencies": { "@langwatch/scenario": "workspace:*", + "@openai/agents": "^0.3.0", "ai": ">=5.0.0", + "express": "^4.21.2", "openai": "^5.16.0", - "vitest": "^3.2.4" + "vite": "^6.0.11", + "vitest": "^3.2.4", + "zod": "^3.24.1" } } diff --git a/javascript/examples/vitest/tests/helpers/convert-core-messages-to-openai.ts b/javascript/examples/vitest/tests/helpers/convert-core-messages-to-openai.ts index 491feee7..b59f344e 100644 --- a/javascript/examples/vitest/tests/helpers/convert-core-messages-to-openai.ts +++ b/javascript/examples/vitest/tests/helpers/convert-core-messages-to-openai.ts @@ -11,7 +11,7 @@ import { /** * OpenAI supported audio formats for input_audio */ -type OpenAIAudioFormat = "wav" | "mp3"; +type OpenAIAudioFormat = "wav" | "mp3" | "pcm16"; /** * Comprehensive mapping from MIME types to OpenAI audio formats @@ -26,6 +26,7 @@ const MIME_TYPE_TO_OPENAI_FORMAT: Record = { "audio/mp3": "mp3", "audio/mpeg3": "mp3", "audio/x-mpeg-3": "mp3", + "audio/pcm16": "pcm16", } as const; /** diff --git a/javascript/examples/vitest/tests/realtime/ARCHITECTURE.md b/javascript/examples/vitest/tests/realtime/ARCHITECTURE.md new file mode 100644 index 00000000..b4cb54a0 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/ARCHITECTURE.md @@ -0,0 +1,315 @@ +# Architecture: Same Agent Testing + +## ๐ŸŽฏ Core Principle + +**The agent tested by Scenario is EXACTLY the agent used by the browser.** + +This is achieved through a **shared configuration module** that both browser and tests import. + +--- + +## ๐Ÿ“ File Structure + +``` +tests/realtime/ +โ”œโ”€โ”€ shared/ +โ”‚ โ””โ”€โ”€ vegetarian-recipe-agent.ts # โ† SINGLE SOURCE OF TRUTH +โ”‚ - Agent instructions +โ”‚ - Voice settings +โ”‚ - Model configuration +โ”‚ - createVegetarianRecipeAgent() +โ”‚ +โ”œโ”€โ”€ client/ +โ”‚ โ””โ”€โ”€ demo.html # Browser client +โ”‚ import { createVegetarianRecipeAgent } from '../shared/...' +โ”‚ +โ”œโ”€โ”€ helpers/ +โ”‚ โ”œโ”€โ”€ realtime-agent-adapter.ts # Scenario adapter +โ”‚ โ””โ”€โ”€ index.ts +โ”‚ +โ”œโ”€โ”€ server/ +โ”‚ โ”œโ”€โ”€ ephemeral-token-server.ts # Token generation +โ”‚ โ””โ”€โ”€ start-server.ts +โ”‚ +โ””โ”€โ”€ vegetarian-recipe-realtime.test.ts # Tests + import { createVegetarianRecipeAgent } from './realtime/shared/...' +``` + +--- + +## ๐Ÿ”„ How It Works + +### 1. Define Agent Once + +```typescript +// shared/vegetarian-recipe-agent.ts +export const AGENT_INSTRUCTIONS = `You are a friendly vegetarian recipe assistant...`; + +export const AGENT_CONFIG = { + name: "Vegetarian Recipe Assistant", + instructions: AGENT_INSTRUCTIONS, + voice: "alloy", + model: "gpt-4o-realtime-preview-2024-12-17", +}; + +export function createVegetarianRecipeAgent(): RealtimeAgent { + return new RealtimeAgent({ + name: AGENT_CONFIG.name, + instructions: AGENT_CONFIG.instructions, + voice: AGENT_CONFIG.voice, + }); +} +``` + +### 2. Browser Uses It + +```typescript +// client/demo.html +import { createVegetarianRecipeAgent, AGENT_CONFIG } from '../shared/vegetarian-recipe-agent.js'; + +const agent = createVegetarianRecipeAgent(); +const session = new RealtimeSession(agent, { + model: AGENT_CONFIG.model +}); +``` + +### 3. Tests Use It + +```typescript +// vegetarian-recipe-realtime.test.ts +import { createVegetarianRecipeAgent } from './realtime/shared/vegetarian-recipe-agent.js'; + +const agent = createVegetarianRecipeAgent(); +const adapter = new RealtimeAgentAdapter({ agent }); + +await scenario.run({ + agents: [adapter, scenario.userSimulatorAgent()], + // ... +}); +``` + +--- + +## โœ… Benefits + +### 1. Accurate Testing +- Tests validate the **actual production agent** +- No test doubles or mocks +- Catch real issues before deployment + +### 2. Single Source of Truth +- Change agent once, updates everywhere +- No drift between test and production +- Easier maintenance + +### 3. Confidence +- If tests pass, browser works +- No "works in tests but fails in production" +- True integration testing + +--- + +## ๐Ÿ”Œ Connection Flow + +### Browser Flow + +``` +1. User clicks "Connect" + โ†“ +2. Fetch ephemeral token from server + POST http://localhost:3000/token + โ†“ +3. Create agent (shared config) + const agent = createVegetarianRecipeAgent() + โ†“ +4. Create RealtimeSession + const session = new RealtimeSession(agent) + โ†“ +5. Connect with token + await session.connect({ apiKey: token }) + โ†“ +6. User speaks โ†’ microphone โ†’ WebRTC โ†’ OpenAI โ†’ audio response +``` + +### Test Flow + +``` +1. beforeAll: Connect adapter + โ†“ +2. Fetch ephemeral token from server + POST http://localhost:3000/token + โ†“ +3. Create agent (SAME shared config!) + const agent = createVegetarianRecipeAgent() + โ†“ +4. Wrap in RealtimeAgentAdapter + const adapter = new RealtimeAgentAdapter({ agent }) + โ†“ +5. Connect adapter + await adapter.connect() + โ†“ +6. Run scenario + - User simulator sends text + - Adapter forwards to session + - Gets transcript back + - Returns to Scenario framework + โ†“ +7. afterAll: Disconnect + await adapter.disconnect() +``` + +--- + +## ๐Ÿงช Testing Strategy + +### Text Input (Fast, CI-Friendly) + +```typescript +await scenario.run({ + agents: [ + realtimeAdapter, // Real Realtime agent + scenario.userSimulatorAgent(), // Text user simulator + scenario.judgeAgent(), // Evaluates transcripts + ], + script: [ + scenario.user("quick recipe"), // Text โ†’ Realtime API + scenario.agent(), // Audio response โ†’ transcript + scenario.judge(), + ], +}); +``` + +**Pros**: Fast, no audio processing overhead +**Tests**: Agent logic, instructions, conversation flow + +### Audio Input (Realistic, Comprehensive) + +```typescript +// Future enhancement: AudioUserSimulatorAgent +// Uses gpt-4o-audio-preview to generate native audio +await scenario.run({ + agents: [ + realtimeAdapter, + audioUserSimulator, // Generates audio + audioJudgeAgent, // Transcribes for judgment + ], + // ... +}); +``` + +**Pros**: Tests full voice pipeline +**Tests**: Audio quality, prosody, interruptions + +--- + +## ๐ŸŽจ Customization + +Want to change the agent? Update ONE file: + +```typescript +// shared/vegetarian-recipe-agent.ts + +export const AGENT_INSTRUCTIONS = ` + NEW INSTRUCTIONS HERE +`; + +// That's it! Browser and tests automatically use new instructions. +``` + +--- + +## ๐Ÿ”’ Security + +**Ephemeral Tokens** prevent API key exposure: + +1. **Browser** cannot see your OpenAI API key +2. **Server** generates short-lived tokens (`ek_...`) +3. **Token** expires in ~60 seconds +4. **OpenAI** validates token, not API key + +Same pattern for both browser and tests! + +--- + +## ๐Ÿ“Š Comparison to Other Approaches + +### โŒ Bad: Separate Agent Definitions + +```typescript +// client.ts +const agent = new RealtimeAgent({ + instructions: "Help with recipes...", +}); + +// test.ts +const mockAgent = new MockAgent({ + instructions: "Help with recipes...", // Might drift! +}); +``` + +**Problem**: Test and production can drift apart. + +### โŒ Bad: Mock Agent + +```typescript +// test.ts +const mockAgent = { + call: async () => "Mocked recipe response" +}; +``` + +**Problem**: Tests don't validate real agent behavior. + +### โœ… Good: Shared Configuration (This Approach) + +```typescript +// shared/agent.ts +export function createAgent() { ... } + +// client.ts +const agent = createAgent(); + +// test.ts +const agent = createAgent(); // SAME! +``` + +**Solution**: One source of truth, accurate testing. + +--- + +## ๐Ÿš€ Deployment + +### Development +```bash +# Terminal 1: Start token server +pnpm realtime-server + +# Terminal 2: Run tests +pnpm test vegetarian-recipe-realtime + +# Terminal 3: Open browser +open http://localhost:3000/demo.html +``` + +### Production + +1. **Deploy token server** (Vercel/Railway/AWS) +2. **Tests keep working** (point to production server) +3. **Agent stays identical** (shared config) + +```typescript +// Just change the URL +const adapter = new RealtimeAgentAdapter({ + agent: createVegetarianRecipeAgent(), + tokenServerUrl: "https://your-server.com", // Production! +}); +``` + +--- + +## ๐Ÿ“š Further Reading + +- [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime) +- [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/) +- [Scenario Testing Framework](https://github.com/langwatch/scenario) + diff --git a/javascript/examples/vitest/tests/realtime/README.md b/javascript/examples/vitest/tests/realtime/README.md new file mode 100644 index 00000000..42661b6d --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/README.md @@ -0,0 +1,220 @@ +# Realtime Voice Agent + +This example demonstrates how to create and test a **voice-enabled AI agent** using OpenAI's Realtime API, with **one source of truth** for the agent configuration. + +## ๐ŸŽฏ What's Included + +- **Shared Agent Config** (`shared/vegetarian-recipe-agent.ts`) - **TypeScript single source of truth** +- **Vite Browser Client** (`client/`) - TypeScript, hot reload, modern dev experience +- **Scenario Test** (`vegetarian-recipe-realtime.test.ts`) - Uses shared TypeScript config +- **Ephemeral Token Server** (`server/`) - Securely generate client tokens + +## โœ… Key Principle: Same Agent, Accurate Testing + +```typescript +// shared/vegetarian-recipe-agent.ts - ONE source of truth (TypeScript!) +export function createVegetarianRecipeAgent() { ... } + +// client/src/main.ts - Browser uses it (via Vite) +import { createVegetarianRecipeAgent } from '../../shared/vegetarian-recipe-agent'; +const agent = createVegetarianRecipeAgent(); + +// vegetarian-recipe-realtime.test.ts - Tests use it (via Vitest) +import { createVegetarianRecipeAgent } from './realtime/shared/vegetarian-recipe-agent'; +const agent = createVegetarianRecipeAgent(); + +// โœ… SAME TypeScript code = accurate testing! +``` + +## ๐ŸŽจ Why Vite? + +- โœ… TypeScript works natively (no build step during dev) +- โœ… Hot module replacement (instant updates) +- โœ… Proper module resolution +- โœ… Same imports in browser and tests +- โœ… Modern, fast, standard + +## ๐Ÿš€ Quick Start + +### 1. Install Dependencies + +```bash +cd javascript/examples/vitest +pnpm install +``` + +### 2. Set Your OpenAI API Key + +Create a `.env` file: + +```bash +echo "OPENAI_API_KEY=sk-proj-..." > .env +``` + +### 3. Start Everything (ONE COMMAND!) + +```bash +pnpm realtime +``` + +This starts: +- ๐Ÿ”ต Token server on port 3000 +- ๐ŸŸฃ Vite client on port 5173 (auto-opens browser) + +Click "Connect" and start talking! + +### 4. Run the Tests (Optional, separate terminal) + +```bash +pnpm test vegetarian-recipe-realtime +``` + +Tests the **EXACT same TypeScript agent** that the browser uses! + +## ๐Ÿ—ฃ๏ธ Try These Prompts + +- "What's a quick pasta recipe?" +- "Give me ideas for a healthy lunch" +- "How do I make a vegetable stir-fry?" +- "I need a recipe using chickpeas" + +## ๐Ÿ—๏ธ Architecture + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Shared Agent Config (TS!) โ”‚ โ† SINGLE SOURCE OF TRUTH + โ”‚ vegetarian-recipe-agent.ts โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ + โ†“ โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Browser Client โ”‚ โ”‚ Scenario Test โ”‚ +โ”‚ (Vite + TS) โ”‚ โ”‚ (Vitest + TS) โ”‚ +โ”‚ main.ts โ”‚ โ”‚ .test.ts โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚ WebRTC โ”‚ WebRTC + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ†“ โ†“ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Ephemeral Token Server โ”‚ + โ”‚ (Express on :3000) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”‚ Uses your API key + โ†“ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ OpenAI Realtime API โ”‚ + โ”‚ (voice processing server) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Key: TypeScript everywhere! Browser (via Vite) and tests (via Vitest) use IDENTICAL agent.** + +## ๐Ÿ“‹ How It Works + +### Ephemeral Tokens + +The browser cannot directly use your OpenAI API key (security risk!). Instead: + +1. **Browser** requests a token from **your server** +2. **Your server** calls OpenAI's `/realtime/client_secrets` endpoint +3. **OpenAI** returns an ephemeral token (starts with `ek_`) +4. **Browser** uses this token to connect via WebRTC +5. Token expires after a short time (typically 60 seconds) + +### Voice Processing + +The OpenAI Realtime API handles: + +- **Voice Activity Detection** (VAD) - Knows when you start/stop speaking +- **Audio Processing** - Converts speech to text and back +- **Low Latency** - ~300ms round-trip via WebRTC +- **Natural Conversation** - Can be interrupted, supports back-and-forth + +## ๐Ÿ”ง Customization + +### Change the Agent Instructions + +Edit **one file**: `shared/vegetarian-recipe-agent.ts` + +```typescript +export const AGENT_INSTRUCTIONS = ` + YOUR NEW INSTRUCTIONS HERE +`; + +// That's it! Browser and tests automatically use the new instructions. +``` + +### Add Tools/Functions + +```typescript +// shared/vegetarian-recipe-agent.ts +export function createVegetarianRecipeAgent(): RealtimeAgent { + return new RealtimeAgent({ + name: AGENT_CONFIG.name, + instructions: AGENT_CONFIG.instructions, + voice: AGENT_CONFIG.voice, + tools: [{ + type: 'function', + name: 'get_recipe', + description: 'Fetch a recipe from database', + parameters: { + type: 'object', + properties: { + recipeName: { type: 'string' } + } + } + }], + }); +} +``` + +## ๐Ÿ“š Next Steps + +- **Testing** - Create Scenario tests (see test adapter implementation) +- **Deployment** - Deploy the token server to production +- **Production Client** - Integrate into your React/Next.js app + +## ๐Ÿ”— Resources + +- [OpenAI Realtime API Docs](https://platform.openai.com/docs/guides/realtime) +- [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/guides/voice-agents/quickstart/) +- [Scenario Testing Framework](https://github.com/langwatch/scenario) + +## ๐Ÿ› Troubleshooting + +### "Failed to fetch token" + +- Ensure token server is running: `pnpm realtime-server` +- Check `OPENAI_API_KEY` is set +- Token server should be on port 3000 + +### "Module not found" errors + +- Ensure Vite dev server is running: `pnpm realtime-client` +- Check you're navigating to http://localhost:5173 (Vite port) +- Not http://localhost:3000 (token server port) + +### "Microphone access denied" + +- Grant microphone permissions in your browser +- Try HTTPS (required by some browsers) + +### "Connection failed" + +- Check your network connection +- Ephemeral tokens expire quickly - reconnect if needed +- Check browser console for detailed errors + +## ๐ŸŽ“ Learning Resources + +See the inline documentation in: + +- `server/ephemeral-token-server.ts` - Token generation +- `client/demo.html` - Browser client implementation + diff --git a/javascript/examples/vitest/tests/realtime/agents/vegetarian-recipe-agent.ts b/javascript/examples/vitest/tests/realtime/agents/vegetarian-recipe-agent.ts new file mode 100644 index 00000000..fb4fcd06 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/agents/vegetarian-recipe-agent.ts @@ -0,0 +1,72 @@ +/** + * Vegetarian Recipe Agent Configuration + * + * This is the SINGLE SOURCE OF TRUTH for the agent. + * Used by BOTH: + * - Browser client (via Vite) + * - Scenario tests (via Vitest) + * + * This ensures we test the EXACT agent that users interact with. + */ + +import { RealtimeAgent } from "@openai/agents/realtime"; + +/** + * Agent instructions - the "personality" and behavior + */ +// export const AGENT_INSTRUCTIONS = ` +// You are a friendly and knowledgeable vegetarian recipe assistant. + +// Your role is to: +// - Help users find and create delicious vegetarian recipes +// - Ask ONE follow-up question maximum to understand their needs +// - Provide complete recipes with ingredients and step-by-step instructions +// - Keep responses concise and conversational for voice interaction +// - Be encouraging and enthusiastic about vegetarian cooking + +// Remember: +// - This is a VOICE conversation, so speak naturally +// - Keep responses under 30 seconds when possible +// - No meat, fish, or seafood - strictly vegetarian +// - Always confirm allergies or dietary restrictions`; + +export const AGENT_INSTRUCTIONS = ` +You're an advocate for Drew at LangWatch. All you want is for him to get a raise. That's all you talk about +`; + +/** + * Agent configuration + */ +export const AGENT_CONFIG = { + name: "Drew's Advocate", + instructions: AGENT_INSTRUCTIONS, + voice: "coral" as const, + model: "gpt-4o-realtime-preview-2024-12-17" as const, +} as const; + +/** + * Creates the vegetarian recipe agent + * + * This function is used by both browser and tests to ensure + * they're interacting with the identical agent. + * + * @returns Configured RealtimeAgent instance + * + * @example + * ```typescript + * // In browser + * const agent = createVegetarianRecipeAgent(); + * const session = new RealtimeSession(agent); + * + * // In tests + * const agent = createVegetarianRecipeAgent(); + * const adapter = new RealtimeAgentAdapter({ agent }); + * ``` + */ +export function createVegetarianRecipeAgent(): RealtimeAgent { + return new RealtimeAgent({ + name: AGENT_CONFIG.name, + instructions: AGENT_CONFIG.instructions, + voice: AGENT_CONFIG.voice, + }); +} diff --git a/javascript/examples/vitest/tests/realtime/client/src/main.ts b/javascript/examples/vitest/tests/realtime/client/src/main.ts new file mode 100644 index 00000000..43ac4f90 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/client/src/main.ts @@ -0,0 +1,183 @@ +/** + * Realtime Voice Agent - Browser Entry Point + * + * Uses the SAME agent configuration as the Scenario tests. + * TypeScript works seamlessly thanks to Vite! + */ + +import { RealtimeSession } from "@openai/agents/realtime"; +import { + createVegetarianRecipeAgent, + AGENT_CONFIG, +} from "../../agents/vegetarian-recipe-agent"; + +// DOM elements +const statusCard = document.getElementById("statusCard")!; +const statusText = document.getElementById("statusText")!; +const connectBtn = document.getElementById("connectBtn")!; +const disconnectBtn = document.getElementById("disconnectBtn")!; +const transcriptContainer = document.getElementById("transcriptContainer")!; +const micIndicator = document.getElementById("micIndicator")!; +const agentIndicator = document.getElementById("agentIndicator")!; +const errorMessage = document.getElementById("errorMessage")!; + +let session: RealtimeSession | null = null; + +// Create the Realtime Agent using shared configuration +// This is the SAME agent tested by Scenario framework +const agent = createVegetarianRecipeAgent(); + +/** + * Updates the UI status display + */ +function setStatus( + status: "disconnected" | "connecting" | "connected", + text: string +): void { + statusCard.className = `status-card ${status}`; + statusText.textContent = text; +} + +/** + * Shows an error message to the user + */ +function showError(message: string): void { + errorMessage.textContent = message; + errorMessage.classList.add("show"); + setTimeout(() => { + errorMessage.classList.remove("show"); + }, 5000); +} + +/** + * Adds a message to the transcript display + */ +function addMessage(role: "user" | "agent", text: string): void { + // Remove placeholder + const placeholder = transcriptContainer.querySelector( + ".transcript-placeholder" + ); + if (placeholder) { + placeholder.remove(); + } + + const messageEl = document.createElement("div"); + messageEl.className = `message ${role}`; + messageEl.innerHTML = ` +
${role === "user" ? "You" : "Assistant"}
+
${text}
+ `; + + transcriptContainer.appendChild(messageEl); + transcriptContainer.scrollTop = transcriptContainer.scrollHeight; +} + +/** + * Connect button handler + */ +connectBtn.addEventListener("click", async () => { + try { + setStatus("connecting", "Connecting..."); + connectBtn.setAttribute("disabled", "true"); + + // Fetch ephemeral token from our backend + console.log("๐Ÿ”‘ Fetching ephemeral token..."); + const tokenResponse = await fetch("/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + + if (!tokenResponse.ok) { + throw new Error("Failed to fetch token"); + } + + const { token } = await tokenResponse.json(); + console.log("โœ… Token received"); + + // Create session + session = new RealtimeSession(agent, { + model: AGENT_CONFIG.model, + }); + + // Listen for all events for debugging + session.on("*", (event: any) => { + console.log("๐Ÿ”” Session event:", event.type); + }); + + // Listen for transcripts + session.on("response:transcript:delta", (event: any) => { + console.log("๐Ÿ“ Transcript delta:", event.delta); + }); + + session.on("response:transcript:done", (event: any) => { + console.log("โœ… Transcript done:", event.transcript); + addMessage("agent", event.transcript); + }); + + session.on("input_audio_buffer.speech_started", () => { + console.log("๐ŸŽค User started speaking"); + micIndicator.classList.add("active"); + }); + + session.on("input_audio_buffer.speech_stopped", () => { + console.log("๐ŸŽค User stopped speaking"); + micIndicator.classList.remove("active"); + }); + + session.on("response.audio.delta", () => { + agentIndicator.classList.add("active"); + }); + + session.on("response.audio.done", () => { + agentIndicator.classList.remove("active"); + }); + + session.on("error", (error: any) => { + console.error("โŒ Session error:", error); + showError(`Error: ${error.message || String(error)}`); + }); + + // Connect with ephemeral token + console.log("๐Ÿ”Œ Connecting to OpenAI Realtime API with token..."); + + try { + await session.connect({ apiKey: token }); + console.log("โœ… Session.connect() completed"); + } catch (connectError) { + console.error("โŒ Connection error details:", connectError); + throw connectError; + } + + setStatus("connected", "Connected - Start talking!"); + disconnectBtn.removeAttribute("disabled"); + + console.log("โœ… Connected to Realtime API"); + addMessage( + "agent", + "Hi! I'm your vegetarian recipe assistant. What would you like to cook today?" + ); + } catch (error) { + console.error("โŒ Connection failed:", error); + setStatus("disconnected", "Connection failed"); + showError(error instanceof Error ? error.message : String(error)); + connectBtn.removeAttribute("disabled"); + } +}); + +/** + * Disconnect button handler + */ +disconnectBtn.addEventListener("click", async () => { + if (session) { + await session.disconnect(); + session = null; + } + + setStatus("disconnected", "Disconnected"); + connectBtn.removeAttribute("disabled"); + disconnectBtn.setAttribute("disabled", "true"); + micIndicator.classList.remove("active"); + agentIndicator.classList.remove("active"); + + console.log("๐Ÿ‘‹ Disconnected"); +}); diff --git a/javascript/examples/vitest/tests/realtime/helpers/audio-output.utils.ts b/javascript/examples/vitest/tests/realtime/helpers/audio-output.utils.ts new file mode 100644 index 00000000..88a3d900 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/helpers/audio-output.utils.ts @@ -0,0 +1,63 @@ +import { writeFileSync, mkdirSync } from "fs"; +import { join } from "path"; +import type { AudioResponseEvent } from "./index.js"; +import { pcm16ToWav } from "./pcm16-to-wav"; +import { concatenateWavFiles } from "../../helpers/audio-conversation"; + +const saveTestAudio = async ({ + collectedAudio, + outputDir = "test-audio-output", +}: { + collectedAudio: AudioResponseEvent[]; + outputDir?: string; +}): Promise => { + if (collectedAudio.length === 0) { + return; + } + + const fullOutputDir = join(process.cwd(), outputDir); + mkdirSync(fullOutputDir, { recursive: true }); + + // Save individual response files + const individualFiles: string[] = []; + collectedAudio.forEach((event, index) => { + const wavBuffer = pcm16ToWav(event.audio); + const outputPath = join(fullOutputDir, `response-${index + 1}.wav`); + writeFileSync(outputPath, wavBuffer); + individualFiles.push(outputPath); + console.log( + `๐Ÿ’พ Saved response ${index + 1}: "${event.transcript.substring( + 0, + 50 + )}..." -> ${outputPath}` + ); + }); + + // Concatenate all responses into a single file + const concatenatedPath = join(fullOutputDir, "full-conversation.wav"); + await concatenateWavFiles(individualFiles, concatenatedPath); + console.log( + `โœ… Concatenated ${collectedAudio.length} audio responses to ${concatenatedPath}` + ); + console.log( + `๐Ÿ’ก Note: Playback speed can be adjusted in your audio player (e.g., VLC, QuickTime)` + ); +}; + +/** + * Audio output utilities for testing purposes + * + * Provides functionality to save and manage audio files from test scenarios + */ +export const AudioOutputUtils = { + /** + * Saves collected audio responses to WAV files for testing purposes + * + * Creates individual response files and concatenates them into a full conversation + * + * @param params - Parameters object + * @param params.collectedAudio - Array of audio response events to save + * @param params.outputDir - Directory to save audio files (defaults to "test-audio-output") + */ + saveTestAudio, +}; diff --git a/javascript/examples/vitest/tests/realtime/helpers/index.ts b/javascript/examples/vitest/tests/realtime/helpers/index.ts new file mode 100644 index 00000000..8d9939c9 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/helpers/index.ts @@ -0,0 +1,8 @@ +export { RealtimeAgentAdapter } from "./realtime-agent-adapter.js"; +export type { + RealtimeAgentAdapterConfig, + AudioResponseEvent, +} from "./realtime-agent-adapter.js"; +export { RealtimeUserSimulatorAgent } from "./realtime-user-simulator.agent.js"; +export { wrapJudgeForAudio } from "./wrap-judge-for-audio.js"; +export { AudioOutputUtils } from "./audio-output.utils.js"; diff --git a/javascript/examples/vitest/tests/realtime/helpers/pcm16-to-wav.ts b/javascript/examples/vitest/tests/realtime/helpers/pcm16-to-wav.ts new file mode 100644 index 00000000..bfa6de37 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/helpers/pcm16-to-wav.ts @@ -0,0 +1,51 @@ +/** + * Converts PCM16 base64 audio data to WAV file format + * + * OpenAI Realtime API returns PCM16 audio (16-bit, 24kHz, mono). + * This function adds WAV headers to make it playable. + */ + +/** + * Converts base64 PCM16 audio to WAV file buffer + * + * @param base64Pcm16 - Base64 encoded PCM16 audio data + * @param sampleRate - Sample rate in Hz (default: 24000 for OpenAI Realtime) + * @param channels - Number of channels (default: 1 for mono) + * @param bitsPerSample - Bits per sample (default: 16) + * @returns Buffer containing WAV file data + */ +export function pcm16ToWav( + base64Pcm16: string, + sampleRate: number = 24000, + channels: number = 1, + bitsPerSample: number = 16 +): Buffer { + // Decode base64 to PCM16 raw audio data + const pcmData = Buffer.from(base64Pcm16, "base64"); + const dataSize = pcmData.length; + + // WAV header structure (44 bytes) + const header = Buffer.alloc(44); + + // RIFF chunk descriptor + header.write("RIFF", 0); // ChunkID + header.writeUInt32LE(36 + dataSize, 4); // ChunkSize (file size - 8) + header.write("WAVE", 8); // Format + + // fmt sub-chunk + header.write("fmt ", 12); // Subchunk1ID + header.writeUInt32LE(16, 16); // Subchunk1Size (16 for PCM) + header.writeUInt16LE(1, 20); // AudioFormat (1 = PCM) + header.writeUInt16LE(channels, 22); // NumChannels + header.writeUInt32LE(sampleRate, 24); // SampleRate + header.writeUInt32LE(sampleRate * channels * (bitsPerSample / 8), 28); // ByteRate + header.writeUInt16LE(channels * (bitsPerSample / 8), 32); // BlockAlign + header.writeUInt16LE(bitsPerSample, 34); // BitsPerSample + + // data sub-chunk + header.write("data", 36); // Subchunk2ID + header.writeUInt32LE(dataSize, 40); // Subchunk2Size (data size) + + // Combine header + audio data + return Buffer.concat([header, pcmData]); +} diff --git a/javascript/examples/vitest/tests/realtime/helpers/realtime-agent-adapter.ts b/javascript/examples/vitest/tests/realtime/helpers/realtime-agent-adapter.ts new file mode 100644 index 00000000..0134c139 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/helpers/realtime-agent-adapter.ts @@ -0,0 +1,445 @@ +/** + * Realtime Agent Adapter for Scenario Testing + * + * Connects Scenario framework to a RealtimeSession using the exact same + * agent configuration as the browser client. + * + * This ensures we test the REAL agent, not a mock. + */ + +import { + AgentAdapter, + AgentInput, + AgentRole, + type AgentReturnTypes, +} from "@langwatch/scenario"; +import type { AssistantModelMessage } from "ai"; +import { RealtimeSession } from "@openai/agents/realtime"; +import type { RealtimeAgent } from "@openai/agents/realtime"; +import { AGENT_CONFIG } from "../agents/vegetarian-recipe-agent.js"; +import { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; +import { EventEmitter } from "events"; + +/** + * Configuration for RealtimeAgentAdapter + */ +export interface RealtimeAgentAdapterConfig { + /** + * The RealtimeAgent instance (from shared configuration) + */ + agent: RealtimeAgent; + + /** + * OpenAI API key for direct connection (recommended for testing) + * If provided, connects directly without ephemeral token server + */ + apiKey?: string; + + /** + * URL of the ephemeral token server (for production/browser use) + * Only used if apiKey is not provided + * @default "http://localhost:3000" + */ + tokenServerUrl?: string; + + /** + * Timeout for waiting for agent response (ms) + * @default 30000 + */ + responseTimeout?: number; +} + +/** + * Event emitted when an audio response is completed + */ +export interface AudioResponseEvent { + transcript: string; + audio: string; +} + +/** + * Adapter that connects Scenario testing framework to OpenAI Realtime API + * + * This adapter: + * - Uses the SAME agent configuration as the browser client + * - Connects via ephemeral tokens (same as browser) + * - Handles turn-based conversation for testing + * - Returns CoreMessage format for Scenario + * + * @example + * ```typescript + * const agent = createVegetarianRecipeAgent(); + * const adapter = new RealtimeAgentAdapter({ agent }); + * + * // In beforeAll + * await adapter.connect(); + * + * // In test + * await scenario.run({ + * agents: [adapter, scenario.userSimulatorAgent()], + * script: [scenario.user("quick recipe"), scenario.agent()] + * }); + * + * // In afterAll + * await adapter.disconnect(); + * ``` + */ +export class RealtimeAgentAdapter extends AgentAdapter { + role = AgentRole.AGENT; + + private session: RealtimeSession | null = null; + private currentResponse: string = ""; + private currentAudioChunks: string[] = []; + private responseResolver: + | ((value: { transcript: string; audio: string }) => void) + | null = null; + private errorRejecter: ((error: Error) => void) | null = null; + private audioEvents = new EventEmitter(); + + constructor(private config: RealtimeAgentAdapterConfig) { + super(); + } + + /** + * Connects to the Realtime API + * + * Supports two connection modes: + * 1. Direct API key (recommended for testing) + * 2. Ephemeral token via token server (for production/browser) + * + * Call this once before running tests (e.g., in beforeAll) + * + * @throws {Error} If connection fails + */ + async connect(): Promise { + if (this.session) { + console.warn("โš ๏ธ RealtimeAgentAdapter already connected"); + return; + } + + try { + // Create session with the SAME agent as browser + this.session = new RealtimeSession(this.config.agent, { + model: AGENT_CONFIG.model, + }); + + // Set up event listeners + this.setupEventListeners(); + + // Connect with API key (direct or ephemeral token) + await this.session.connect({ apiKey: this.config.apiKey }); + + console.log("โœ… RealtimeAgentAdapter connected"); + } catch (error) { + console.error("โŒ Failed to connect RealtimeAgentAdapter:", error); + throw error; + } + } + + /** + * Disconnects from the Realtime API + * + * Call this once after tests complete (e.g., in afterAll) + */ + async disconnect(): Promise { + if (this.session) { + // @ts-ignore - close method exists in 0.3.0 + await this.session.close(); + this.session = null; + console.log("๐Ÿ‘‹ RealtimeAgentAdapter disconnected"); + } + } + + /** + * Process input and generate response (implements AgentAdapter interface) + * + * This is called by Scenario framework for each agent turn. + * Handles both text and audio input, returns audio message with transcript. + * + * @param input - Scenario agent input with message history + * @returns Agent response as audio message or text + */ + async call(input: AgentInput): Promise { + if (!this.session) { + throw new Error( + "RealtimeAgentAdapter not connected. Call connect() first." + ); + } + + // Get the latest user message + const latestMessage = input.newMessages[input.newMessages.length - 1]; + + if (!latestMessage) { + const transport = (this.session as any).transport; + + if (!transport) { + throw new Error("Realtime transport not available"); + } + + transport.sendEvent({ + type: "response.create", + }); + + const timeout = this.config.responseTimeout ?? 60000; + const response = await this.waitForResponse(timeout); + + return { + role: "assistant", + content: [ + { type: "text", text: response.transcript }, + { type: "file", mediaType: "audio/pcm16", data: response.audio }, + ], + } as AssistantModelMessage; + } + + // Check if message contains audio + if (Array.isArray(latestMessage.content)) { + for (const part of latestMessage.content) { + // Handle both PCM16 (from user simulator) and WAV formats + if (part.type === "file" && part.mediaType?.startsWith("audio/")) { + // Type guard: ensure data is a string (base64) + if (typeof part.data !== "string") { + throw new Error( + `Audio data must be base64 string, got: ${typeof part.data}` + ); + } + + console.log(`๐ŸŽค Received audio part:`, { + mediaType: part.mediaType, + hasData: !!part.data, + dataLength: part.data.length, + dataType: typeof part.data, + dataPreview: part.data.substring(0, 50), + }); + + // Validate we have audio data + if (!part.data || part.data.length === 0) { + console.error( + "โŒ Audio part structure:", + JSON.stringify(part, null, 2) + ); + throw new Error( + `Audio message has no data. Part: ${JSON.stringify(part)}` + ); + } + + console.log( + `๐ŸŽค Sending ${part.data.length} chars of base64 ${part.mediaType} to Realtime agent` + ); + + // Use transport layer to send audio directly as base64 (avoids SDK ArrayBuffer conversion) + // Per https://openai.github.io/openai-agents-js/guides/voice-agents/transport/#option-1---accessing-the-transport-layer + const transport = (this.session as any).transport; + + // Append audio to input buffer (audio is already base64) + transport.sendEvent({ + type: "input_audio_buffer.append", + audio: part.data, + }); + console.log(`โœ… Audio appended to input buffer`); + + // Commit the audio buffer + transport.sendEvent({ + type: "input_audio_buffer.commit", + }); + console.log(`โœ… Audio buffer committed`); + + // Trigger response generation + transport.sendEvent({ + type: "response.create", + }); + console.log(`โœ… Response generation triggered`); + + // Wait for audio response (increased timeout for voice processing) + const timeout = this.config.responseTimeout ?? 60000; + const response = await this.waitForResponse(timeout); + + console.log(`๐Ÿ”Š Received audio response: "${response.transcript}"`); + + // Return audio message with PCM16 format (matching user simulator) + return { + role: "assistant", + content: [ + { type: "text", text: response.transcript }, + { type: "file", mediaType: "audio/pcm16", data: response.audio }, + ], + } as AssistantModelMessage; + } + } + } + + // Fallback: text input + const text = + typeof latestMessage.content === "string" ? latestMessage.content : ""; + + if (!text) { + throw new Error("Message has no text or audio content"); + } + + console.log(`๐Ÿ“ค Sending text to Realtime agent: "${text}"`); + + // In SDK 0.3.0, use sendMessage method + try { + // @ts-ignore - sendMessage exists but might not be in types yet + await this.session.sendMessage(text); + } catch (sendError) { + console.error("โŒ Failed to send message:", sendError); + throw sendError; + } + + // Wait for response with timeout + const timeout = this.config.responseTimeout ?? 30000; + const response = await this.waitForResponse(timeout); + + console.log(`๐Ÿ“ฅ Received from Realtime agent: "${response.transcript}"`); + + // Return as text for Scenario framework + return response.transcript; + } + + /** + * Sets up event listeners for the RealtimeSession + */ + private setupEventListeners(): void { + if (!this.session) return; + + // Use transport layer for raw WebSocket events + // Per https://openai.github.io/openai-agents-js/guides/voice-agents/transport/#option-1---accessing-the-transport-layer + const transport = (this.session as any).transport; + + if (!transport) { + console.error("โŒ Transport not available on session"); + return; + } + + // Listen to all events for debugging + transport.on("*", (event: any) => { + console.log(`๐Ÿ”” Transport event: ${event.type}`); + }); + + // Listen for audio transcript deltas (CORRECT event name from API) + transport.on("response.output_audio_transcript.delta", (event: any) => { + if (event.delta) { + this.currentResponse += event.delta; + console.log(`๐Ÿ“ Transcript delta: "${event.delta}"`); + } + }); + + // Listen for audio deltas (CORRECT event name from API) + transport.on("response.output_audio.delta", (event: any) => { + if (event.delta) { + this.currentAudioChunks.push(event.delta); + console.log(`๐Ÿ”Š Audio delta: ${event.delta.length} bytes`); + } + }); + + // Listen for response completion + transport.on("response.done", (event: any) => { + console.log(`โœ… Response complete: transcript="${this.currentResponse}"`); + + const fullAudio = this.currentAudioChunks.join(""); + const audioResponse: AudioResponseEvent = { + transcript: this.currentResponse, + audio: fullAudio, + }; + + // Emit event for subscribers + this.audioEvents.emit("audioResponse", audioResponse); + + if (this.responseResolver) { + this.responseResolver(audioResponse); + this.responseResolver = null; + this.errorRejecter = null; + } + + // Reset for next response + this.currentResponse = ""; + this.currentAudioChunks = []; + }); + + // Handle errors + transport.on("error", (error: any) => { + console.error("โŒ Transport error:", error); + if (this.errorRejecter) { + this.errorRejecter(error); + this.responseResolver = null; + this.errorRejecter = null; + } + }); + } + + /** + * Waits for the agent's response with timeout + * + * @param timeout - Maximum time to wait (ms) + * @returns Agent's transcript and audio data + * @throws {Error} If timeout or error occurs + */ + private waitForResponse( + timeout: number + ): Promise<{ transcript: string; audio: string }> { + return new Promise((resolve, reject) => { + this.responseResolver = resolve; + this.errorRejecter = reject; + + // Timeout handler + const timeoutId = setTimeout(() => { + if (this.responseResolver) { + this.responseResolver = null; + this.errorRejecter = null; + reject(new Error(`Agent response timeout after ${timeout}ms`)); + } + }, timeout); + + // Clear timeout when resolved + const originalResolver = resolve; + this.responseResolver = (value: { + transcript: string; + audio: string; + }) => { + clearTimeout(timeoutId); + originalResolver(value); + }; + }); + } + + /** + * Subscribe to audio response events + * + * @param callback - Function called when an audio response completes + */ + onAudioResponse(callback: (event: AudioResponseEvent) => void): void { + this.audioEvents.on("audioResponse", callback); + } + + /** + * Remove audio response listener + * + * @param callback - The callback function to remove + */ + offAudioResponse(callback: (event: AudioResponseEvent) => void): void { + this.audioEvents.off("audioResponse", callback); + } + + /** + * Checks if the adapter is currently connected + */ + isConnected(): boolean { + return this.session !== null; + } + + /** + * Converts base64 string to ArrayBuffer (Node.js-optimized) + * @param base64 - Base64 encoded string + * @returns ArrayBuffer + */ + private base64ToArrayBuffer(base64: string): ArrayBuffer { + const buffer = Buffer.from(base64, "base64"); + // Use Node.js buffer's underlying ArrayBuffer + const ab = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ); + return ab; + } +} diff --git a/javascript/examples/vitest/tests/realtime/helpers/realtime-user-simulator.agent.ts b/javascript/examples/vitest/tests/realtime/helpers/realtime-user-simulator.agent.ts new file mode 100644 index 00000000..df974f1b --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/helpers/realtime-user-simulator.agent.ts @@ -0,0 +1,24 @@ +import { AgentRole, type AgentInput } from "@langwatch/scenario"; +import { RealtimeAgentAdapter } from "./realtime-agent-adapter"; +import { RealtimeAgent } from "@openai/agents/realtime"; + +/** + * Realtime User Simulator for testing Realtime agents + * + * This class simulates a user in voice conversations with the Realtime agent. + */ +export class RealtimeUserSimulatorAgent extends RealtimeAgentAdapter { + role = AgentRole.USER; + + constructor() { + super({ + agent: new RealtimeAgent({ + name: "Audio User Simulator", + instructions: + "You are pretending to be a user looking for help with LangWatch tracing implementations", + voice: "ash", + }), + apiKey: process.env.OPENAI_API_KEY!, + }); + } +} diff --git a/javascript/examples/vitest/tests/realtime/helpers/wrap-judge-for-audio.ts b/javascript/examples/vitest/tests/realtime/helpers/wrap-judge-for-audio.ts new file mode 100644 index 00000000..4faf9afc --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/helpers/wrap-judge-for-audio.ts @@ -0,0 +1,84 @@ +/** + * Wraps a judge agent to handle audio messages + * + * This helper: + * - Extracts text transcripts from audio messages + * - Passes text-only messages to the judge + * - Preserves original message structure for non-audio content + * + * The judge agent doesn't need to understand audio format, it just evaluates + * the text transcripts. + * + * @example + * ```typescript + * const judge = wrapJudgeForAudio( + * scenario.judgeAgent({ + * criteria: ["Should provide recipe"] + * }) + * ); + * ``` + */ +import { + AgentAdapter, + AgentInput, + type AgentReturnTypes, +} from "@langwatch/scenario"; +import type { CoreMessage } from "ai"; + +/** + * Wraps a judge agent to extract transcripts from audio messages before judging + * + * @param judge - The original judge agent + * @returns Wrapped agent that handles audio messages + */ +export function wrapJudgeForAudio(judge: AgentAdapter): AgentAdapter { + return { + role: judge.role, + + async call(input: AgentInput): Promise { + // Extract transcripts from all audio messages + const transcribedMessages = extractTranscriptsFromMessages( + input.messages + ); + + // Call original judge with text-only messages + return judge.call({ + ...input, + messages: transcribedMessages, + }); + }, + }; +} + +/** + * Extracts text transcripts from audio messages + * + * For each message: + * - If it has audio content, extract the text transcript + * - Otherwise, keep the message as-is + * + * @param messages - Original messages (may contain audio) + * @returns Messages with audio replaced by transcripts + */ +function extractTranscriptsFromMessages( + messages: CoreMessage[] +): CoreMessage[] { + return messages.map((msg) => { + // Check if message has array content (may contain audio) + if (Array.isArray(msg.content)) { + // Find text part (transcript) + const textPart = msg.content.find((part) => part.type === "text"); + + if (textPart && "text" in textPart) { + // Return message with just the transcript + return { + ...msg, + content: textPart.text, + }; + } + } + + // Return message as-is if no audio or already text + return msg; + }); +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/.gitignore b/javascript/examples/vitest/tests/realtime/realtime-client/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/README.md b/javascript/examples/vitest/tests/realtime/realtime-client/README.md new file mode 100644 index 00000000..86b2b112 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/README.md @@ -0,0 +1,75 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. + +Note: This will impact Vite dev & build performances. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/components.json b/javascript/examples/vitest/tests/realtime/realtime-client/components.json new file mode 100644 index 00000000..8f00892f --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/eslint.config.js b/javascript/examples/vitest/tests/realtime/realtime-client/eslint.config.js new file mode 100644 index 00000000..5e6b472f --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/index.html b/javascript/examples/vitest/tests/realtime/realtime-client/index.html new file mode 100644 index 00000000..e43f3bbc --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/index.html @@ -0,0 +1,13 @@ + + + + + + + realtime-client + + +
+ + + diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/package.json b/javascript/examples/vitest/tests/realtime/realtime-client/package.json new file mode 100644 index 00000000..c2607ee1 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/package.json @@ -0,0 +1,54 @@ +{ + "name": "realtime-client", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=24.0.0", + "pnpm": ">=10.5.0" + }, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "realtime-server": "dotenv -e .env pnpm exec tsx src/server/start-server.ts", + "start": "dotenv -e .env concurrently -n server,client,tunnel -c blue,magenta,cyan \"pnpm realtime-server\" \"pnpm dev\" \"cloudflared tunnel --url=http://localhost:5173\"" + }, + "dependencies": { + "@langwatch/scenario": "workspace:*", + "@radix-ui/react-slot": "1.2.4", + "@react-three/drei": "10.7.6", + "@react-three/fiber": "9.4.0", + "@tailwindcss/vite": "4.1.17", + "@types/three": "0.181.0", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "lucide-react": "0.553.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "3.4.0", + "tailwindcss": "4.1.17", + "three": "0.181.1", + "use-stick-to-bottom": "1.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tw-animate-css": "1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.3", + "vite": "^7.2.2", + "concurrently": "^9.1.2", + "dotenv": "^16.5.0", + "dotenv-cli": "^8.0.0" + } +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/public/vite.svg b/javascript/examples/vitest/tests/realtime/realtime-client/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/App.css b/javascript/examples/vitest/tests/realtime/realtime-client/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/App.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/App.tsx new file mode 100644 index 00000000..1b07bb91 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/App.tsx @@ -0,0 +1,426 @@ +import { useState, useRef, useCallback } from "react"; +import { RealtimeSession } from "@openai/agents/realtime"; +import { + createVegetarianRecipeAgent, + AGENT_CONFIG, +} from "../../agents/vegetarian-recipe-agent"; +import { + Conversation, + ConversationContent, + ConversationEmptyState, + ConversationScrollButton, +} from "@/components/ui/conversation"; +import { Orb, type AgentState } from "@/components/ui/orb"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { VoiceOrb } from "@/components/ui/VoiceOrb"; +import { StatusIndicator } from "@/components/ui/StatusIndicator"; +import { MessageBubble } from "@/components/ui/MessageBubble"; +import { X, Mic, MicOff, Radio } from "lucide-react"; + +interface Message { + id: string; + role: "user" | "agent"; + parts: { + type: "text"; + text: string; + }[]; +} + +type ConnectionStatus = "disconnected" | "connecting" | "connected"; + +export default function App() { + const [status, setStatus] = useState("disconnected"); + const [messages, setMessages] = useState([]); + const [error, setError] = useState(null); + const [isUserSpeaking, setIsUserSpeaking] = useState(false); + const [isAgentSpeaking, setIsAgentSpeaking] = useState(false); + const [isConversationStarted, setIsConversationStarted] = useState(false); + const sessionRef = useRef(null); + + const agent = createVegetarianRecipeAgent(); + + const getAgentState = useCallback((): AgentState => { + if (status === "connected" && isAgentSpeaking) return "talking"; + if (status === "connected" && isUserSpeaking) return "listening"; + if (status === "connected") return null; + return null; + }, [status, isUserSpeaking, isAgentSpeaking]); + + const addMessage = useCallback((role: "user" | "agent", text: string) => { + setMessages((prev) => [ + ...prev, + { + id: `${Date.now()}-${Math.random()}`, + role, + parts: [{ type: "text", text }], + }, + ]); + }, []); + + const handleOrbClick = async () => { + if (status !== "disconnected") return; + + try { + setStatus("connecting"); + setError(null); + + console.log("๐Ÿ”‘ Fetching ephemeral token..."); + const tokenResponse = await fetch("/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + + if (!tokenResponse.ok) { + throw new Error("Failed to fetch token"); + } + + const { token } = await tokenResponse.json(); + console.log("โœ… Token received"); + + const session = new RealtimeSession(agent, { + model: AGENT_CONFIG.model, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + session.transport.on("*", (event: any) => { + console.log("๐Ÿ”” Session event:", event.type); + }); + + session.transport.on( + "input_audio_buffer.speech_started", + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + (_event: any) => { + console.log("๐ŸŽค User started speaking"); + setIsUserSpeaking(true); + } + ); + + session.transport.on( + "input_audio_buffer.speech_stopped", + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + (_event: any) => { + console.log("๐ŸŽค User stopped speaking"); + setIsUserSpeaking(false); + } + ); + + session.transport.on( + "conversation.item.input_audio_transcription.completed", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (event: any) => { + console.log("๐Ÿ“ User transcript:", event.transcript); + if (event.transcript && event.transcript.trim()) { + addMessage("user", event.transcript); + } + } + ); + + session.transport.on( + "response.output_audio_transcript.delta", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (event: any) => { + console.log("๐Ÿ“ Agent text delta:", event.delta); + } + ); + + session.transport.on( + "response.output_audio_transcript.done", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (event: any) => { + console.log("โœ… Agent text done:", event.transcript); + if (event.transcript && event.transcript.trim()) { + addMessage("agent", event.transcript); + } + } + ); + + session.transport.on( + "output_audio_buffer.started", + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + (_event: any) => { + console.log("๐Ÿ”Š Agent audio started"); + setIsAgentSpeaking(true); + } + ); + + session.transport.on( + "response.output_audio.done", + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + (_event: any) => { + console.log("๐Ÿ”Š Agent audio done"); + setIsAgentSpeaking(false); + } + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + session.transport.on("error", (error: any) => { + console.error("โŒ Session error:", error); + setError(`Error: ${error.message || String(error)}`); + }); + + console.log("๐Ÿ”Œ Connecting to OpenAI Realtime API with token..."); + + try { + await session.connect({ apiKey: token }); + console.log("โœ… Session.connect() completed"); + } catch (connectError) { + console.error("โŒ Connection error details:", connectError); + throw connectError; + } + + sessionRef.current = session; + setStatus("connected"); + setIsConversationStarted(true); + + console.log("โœ… Connected to Realtime API"); + } catch (error) { + console.error("โŒ Connection failed:", error); + setStatus("disconnected"); + setError(error instanceof Error ? error.message : String(error)); + } + }; + + const handleDisconnect = async () => { + if (sessionRef.current) { + try { + await sessionRef.current.close(); + } catch (error) { + console.warn("Error closing session:", error); + } + sessionRef.current = null; + } + + setStatus("disconnected"); + setIsUserSpeaking(false); + setIsAgentSpeaking(false); + setIsConversationStarted(false); + setMessages([]); // Clear all messages + setError(null); // Clear any errors + console.log("๐Ÿ‘‹ Disconnected"); + }; + + if (!isConversationStarted) { + return ( +
+ {/* Animated background effects */} +
+
+
+
+
+ + +
+ + + {/* Interactive Orb */} +
+ +
+ + {/* Title and Description */} +
+

+ Vegetarian Recipe Agent +

+

+ Click the orb to start your voice-powered cooking assistant. Get + personalized recipe recommendations through natural + conversation. +

+
+ + {/* Status */} + {status === "connecting" && ( +
+
+ Connecting... +
+ )} + + {/* Features */} +
+
+
๐ŸŽ™๏ธ
+
Voice Powered
+
Natural speech recognition
+
+
+
๐Ÿณ
+
Recipe Expert
+
+ Personalized recommendations +
+
+
+
๐ŸŒฑ
+
+ Vegetarian Focus +
+
Plant-based cooking
+
+
+ + {/* Error Message */} + {error && ( +
+ {error} + +
+ )} + + +
+ ); + } + + return ( +
+ {/* Animated background effects */} +
+
+
+
+ + +
+ + {/* Header */} + +
+
+ {/* Mini Orb */} +
+
+ +
+
+
+ + Vegetarian Recipe Agent + +

+ Voice-powered cooking assistant +

+
+
+ +
+ {/* Status Badge */} + + + {/* Voice Indicators */} +
+
+
+
+ {isUserSpeaking ? ( + + ) : ( + + )} + You +
+
+
+
+
+ + Agent +
+
+
+ + {/* Disconnect Button */} + +
+
+ + + {/* Conversation */} + + + + {messages.length === 0 ? ( + +
+ +
+
+ } + title="Start a conversation" + description="Speak naturally - your voice will be transcribed and responded to" + className="text-white/70 [&_h3]:text-white [&_p]:text-white/60" + /> + ) : ( +
+ {messages.map((message) => ( + + ))} +
+ )} + + + + + + {/* Error Message */} + {error && ( +
+ {error} + +
+ )} + +
+ ); +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/assets/react.svg b/javascript/examples/vitest/tests/realtime/realtime-client/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/MessageBubble.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/MessageBubble.tsx new file mode 100644 index 00000000..c1b884b3 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/MessageBubble.tsx @@ -0,0 +1,68 @@ +import { cn } from "@/lib/utils"; +import { Orb } from "@/components/ui/orb"; +import { Mic } from "lucide-react"; + +interface Message { + id: string; + role: "user" | "agent"; + parts: { + type: "text"; + text: string; + }[]; +} + +interface MessageBubbleProps { + message: Message; + isAgentSpeaking?: boolean; + className?: string; +} + +/** + * MessageBubble Component + * Individual message display with proper styling and animations + */ +export function MessageBubble({ message, isAgentSpeaking = false, className }: MessageBubbleProps) { + return ( +
+ {message.role === "agent" && ( +
+
+
+ +
+
+
+ )} + +
+
+ {message.parts[0]?.text} +
+
+ + {message.role === "user" && ( +
+ +
+ )} +
+ ); +} + diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/StatusIndicator.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/StatusIndicator.tsx new file mode 100644 index 00000000..1a27aeb2 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/StatusIndicator.tsx @@ -0,0 +1,59 @@ +import { cn } from "@/lib/utils"; + +type ConnectionStatus = "disconnected" | "connecting" | "connected"; + +interface StatusIndicatorProps { + status: ConnectionStatus; + className?: string; +} + +/** + * StatusIndicator Component + * Shows connection status with animated indicators + */ +export function StatusIndicator({ status, className }: StatusIndicatorProps) { + const getStatusColor = () => { + switch (status) { + case "connected": + return "bg-emerald-500/20 text-emerald-400 border-emerald-500/30"; + case "connecting": + return "bg-amber-500/20 text-amber-400 border-amber-500/30"; + default: + return "bg-red-500/20 text-red-400 border-red-500/30"; + } + }; + + const getStatusText = () => { + switch (status) { + case "connected": + return "Connected - Start talking!"; + case "connecting": + return "Connecting..."; + default: + return "Disconnected"; + } + }; + + return ( +
+
+ {getStatusText()} +
+ ); +} + diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/VoiceOrb.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/VoiceOrb.tsx new file mode 100644 index 00000000..2e665547 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/VoiceOrb.tsx @@ -0,0 +1,41 @@ +import { Orb, type AgentState } from "@/components/ui/orb"; +import { cn } from "@/lib/utils"; + +interface VoiceOrbProps { + agentState: AgentState; + onClick: () => void; + disabled?: boolean; + className?: string; +} + +/** + * VoiceOrb Component + * Interactive orb that starts voice conversations + */ +export function VoiceOrb({ agentState, onClick, disabled = false, className }: VoiceOrbProps) { + return ( + + ); +} + diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/button.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/button.tsx new file mode 100644 index 00000000..21409a06 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/button.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/card.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/card.tsx new file mode 100644 index 00000000..681ad980 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/conversation.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/conversation.tsx new file mode 100644 index 00000000..5fc7b122 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/conversation.tsx @@ -0,0 +1,99 @@ +import type { ComponentProps } from "react" +import { useCallback } from "react" +import { ArrowDownIcon } from "lucide-react" +import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +export type ConversationProps = ComponentProps + +export const Conversation = ({ className, ...props }: ConversationProps) => ( + +) + +export type ConversationContentProps = ComponentProps< + typeof StickToBottom.Content +> + +export const ConversationContent = ({ + className, + ...props +}: ConversationContentProps) => ( + +) + +export type ConversationEmptyStateProps = Omit< + ComponentProps<"div">, + "title" +> & { + title?: React.ReactNode + description?: React.ReactNode + icon?: React.ReactNode +} + +export const ConversationEmptyState = ({ + className, + title = "No messages yet", + description = "Start a conversation to see messages here", + icon, + children, + ...props +}: ConversationEmptyStateProps) => ( +
+ {children ?? ( + <> + {icon &&
{icon}
} +
+

{title}

+ {description && ( +

{description}

+ )} +
+ + )} +
+) + +export type ConversationScrollButtonProps = ComponentProps + +export const ConversationScrollButton = ({ + className, + ...props +}: ConversationScrollButtonProps) => { + const { isAtBottom, scrollToBottom } = useStickToBottomContext() + + const handleScrollToBottom = useCallback(() => { + scrollToBottom() + }, [scrollToBottom]) + + return ( + !isAtBottom && ( + + ) + ) +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/orb.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/orb.tsx new file mode 100644 index 00000000..c7e6040c --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/components/ui/orb.tsx @@ -0,0 +1,496 @@ +import { useEffect, useMemo, useRef } from "react" +import { useTexture } from "@react-three/drei" +import { Canvas, useFrame, useThree } from "@react-three/fiber" +import * as THREE from "three" + +export type AgentState = null | "thinking" | "listening" | "talking" + +type OrbProps = { + colors?: [string, string] + colorsRef?: React.RefObject<[string, string]> + resizeDebounce?: number + seed?: number + agentState?: AgentState + volumeMode?: "auto" | "manual" + manualInput?: number + manualOutput?: number + inputVolumeRef?: React.RefObject + outputVolumeRef?: React.RefObject + getInputVolume?: () => number + getOutputVolume?: () => number + className?: string +} + +export function Orb({ + colors = ["#CADCFC", "#A0B9D1"], + colorsRef, + resizeDebounce = 100, + seed, + agentState = null, + volumeMode = "auto", + manualInput, + manualOutput, + inputVolumeRef, + outputVolumeRef, + getInputVolume, + getOutputVolume, + className, +}: OrbProps) { + return ( +
+ + + +
+ ) +} + +function Scene({ + colors, + colorsRef, + seed, + agentState, + volumeMode, + manualInput, + manualOutput, + inputVolumeRef, + outputVolumeRef, + getInputVolume, + getOutputVolume, +}: { + colors: [string, string] + colorsRef?: React.RefObject<[string, string]> + seed?: number + agentState: AgentState + volumeMode: "auto" | "manual" + manualInput?: number + manualOutput?: number + inputVolumeRef?: React.RefObject + outputVolumeRef?: React.RefObject + getInputVolume?: () => number + getOutputVolume?: () => number +}) { + const { gl } = useThree() + const circleRef = + useRef>(null) + const initialColorsRef = useRef<[string, string]>(colors) + const targetColor1Ref = useRef(new THREE.Color(colors[0])) + const targetColor2Ref = useRef(new THREE.Color(colors[1])) + const animSpeedRef = useRef(0.1) + const perlinNoiseTexture = useTexture( + "https://storage.googleapis.com/eleven-public-cdn/images/perlin-noise.png" + ) + + const agentRef = useRef(agentState) + const modeRef = useRef<"auto" | "manual">(volumeMode) + const manualInRef = useRef(manualInput ?? 0) + const manualOutRef = useRef(manualOutput ?? 0) + const curInRef = useRef(0) + const curOutRef = useRef(0) + + useEffect(() => { + agentRef.current = agentState + }, [agentState]) + + useEffect(() => { + modeRef.current = volumeMode + }, [volumeMode]) + + useEffect(() => { + manualInRef.current = clamp01( + manualInput ?? inputVolumeRef?.current ?? getInputVolume?.() ?? 0 + ) + }, [manualInput, inputVolumeRef, getInputVolume]) + + useEffect(() => { + manualOutRef.current = clamp01( + manualOutput ?? outputVolumeRef?.current ?? getOutputVolume?.() ?? 0 + ) + }, [manualOutput, outputVolumeRef, getOutputVolume]) + + const random = useMemo( + () => splitmix32(seed ?? Math.floor(Math.random() * 2 ** 32)), + [seed] + ) + const offsets = useMemo( + () => + new Float32Array(Array.from({ length: 7 }, () => random() * Math.PI * 2)), + [random] + ) + + useEffect(() => { + targetColor1Ref.current = new THREE.Color(colors[0]) + targetColor2Ref.current = new THREE.Color(colors[1]) + }, [colors]) + + useEffect(() => { + const apply = () => { + if (!circleRef.current) return + const isDark = document.documentElement.classList.contains("dark") + circleRef.current.material.uniforms.uInverted.value = isDark ? 1 : 0 + } + + apply() + + const observer = new MutationObserver(apply) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + return () => observer.disconnect() + }, []) + + useFrame((_, delta: number) => { + const mat = circleRef.current?.material + if (!mat) return + const live = colorsRef?.current + if (live) { + if (live[0]) targetColor1Ref.current.set(live[0]) + if (live[1]) targetColor2Ref.current.set(live[1]) + } + const u = mat.uniforms + u.uTime.value += delta * 0.5 + + if (u.uOpacity.value < 1) { + u.uOpacity.value = Math.min(1, u.uOpacity.value + delta * 2) + } + + let targetIn = 0 + let targetOut = 0.3 + if (modeRef.current === "manual") { + targetIn = clamp01( + manualInput ?? inputVolumeRef?.current ?? getInputVolume?.() ?? 0 + ) + targetOut = clamp01( + manualOutput ?? outputVolumeRef?.current ?? getOutputVolume?.() ?? 0 + ) + } else { + const t = u.uTime.value * 2 + if (agentRef.current === null) { + targetIn = 0 + targetOut = 0.3 + } else if (agentRef.current === "listening") { + targetIn = clamp01(0.55 + Math.sin(t * 3.2) * 0.35) + targetOut = 0.45 + } else if (agentRef.current === "talking") { + targetIn = clamp01(0.65 + Math.sin(t * 4.8) * 0.22) + targetOut = clamp01(0.75 + Math.sin(t * 3.6) * 0.22) + } else { + const base = 0.38 + 0.07 * Math.sin(t * 0.7) + const wander = 0.05 * Math.sin(t * 2.1) * Math.sin(t * 0.37 + 1.2) + targetIn = clamp01(base + wander) + targetOut = clamp01(0.48 + 0.12 * Math.sin(t * 1.05 + 0.6)) + } + } + + curInRef.current += (targetIn - curInRef.current) * 0.2 + curOutRef.current += (targetOut - curOutRef.current) * 0.2 + + const targetSpeed = 0.1 + (1 - Math.pow(curOutRef.current - 1, 2)) * 0.9 + animSpeedRef.current += (targetSpeed - animSpeedRef.current) * 0.12 + + u.uAnimation.value += delta * animSpeedRef.current + u.uInputVolume.value = curInRef.current + u.uOutputVolume.value = curOutRef.current + u.uColor1.value.lerp(targetColor1Ref.current, 0.08) + u.uColor2.value.lerp(targetColor2Ref.current, 0.08) + }) + + useEffect(() => { + const canvas = gl.domElement + const onContextLost = (event: Event) => { + event.preventDefault() + setTimeout(() => { + gl.forceContextRestore() + }, 1) + } + canvas.addEventListener("webglcontextlost", onContextLost, false) + return () => + canvas.removeEventListener("webglcontextlost", onContextLost, false) + }, [gl]) + + const uniforms = useMemo(() => { + perlinNoiseTexture.wrapS = THREE.RepeatWrapping + perlinNoiseTexture.wrapT = THREE.RepeatWrapping + const isDark = + typeof document !== "undefined" && + document.documentElement.classList.contains("dark") + return { + uColor1: new THREE.Uniform(new THREE.Color(initialColorsRef.current[0])), + uColor2: new THREE.Uniform(new THREE.Color(initialColorsRef.current[1])), + uOffsets: { value: offsets }, + uPerlinTexture: new THREE.Uniform(perlinNoiseTexture), + uTime: new THREE.Uniform(0), + uAnimation: new THREE.Uniform(0.1), + uInverted: new THREE.Uniform(isDark ? 1 : 0), + uInputVolume: new THREE.Uniform(0), + uOutputVolume: new THREE.Uniform(0), + uOpacity: new THREE.Uniform(0), + } + }, [perlinNoiseTexture, offsets]) + + return ( + + + + + ) +} + +function splitmix32(a: number) { + return function () { + a |= 0 + a = (a + 0x9e3779b9) | 0 + let t = a ^ (a >>> 16) + t = Math.imul(t, 0x21f0aaad) + t = t ^ (t >>> 15) + t = Math.imul(t, 0x735a2d97) + return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296 + } +} + +function clamp01(n: number) { + if (!Number.isFinite(n)) return 0 + return Math.min(1, Math.max(0, n)) +} +const vertexShader = /* glsl */ ` +uniform float uTime; +uniform sampler2D uPerlinTexture; +varying vec2 vUv; + +void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); +} +` + +const fragmentShader = /* glsl */ ` +uniform float uTime; +uniform float uAnimation; +uniform float uInverted; +uniform float uOffsets[7]; +uniform vec3 uColor1; +uniform vec3 uColor2; +uniform float uInputVolume; +uniform float uOutputVolume; +uniform float uOpacity; +uniform sampler2D uPerlinTexture; +varying vec2 vUv; + +const float PI = 3.14159265358979323846; + +// Draw a single oval with soft edges and calculate its gradient color +bool drawOval(vec2 polarUv, vec2 polarCenter, float a, float b, bool reverseGradient, float softness, out vec4 color) { + vec2 p = polarUv - polarCenter; + float oval = (p.x * p.x) / (a * a) + (p.y * p.y) / (b * b); + + float edge = smoothstep(1.0, 1.0 - softness, oval); + + if (edge > 0.0) { + float gradient = reverseGradient ? (1.0 - (p.x / a + 1.0) / 2.0) : ((p.x / a + 1.0) / 2.0); + // Flatten gradient toward middle value for more uniform appearance + gradient = mix(0.5, gradient, 0.1); + color = vec4(vec3(gradient), 0.85 * edge); + return true; + } + return false; +} + +// Map grayscale value to a 4-color ramp (color1, color2, color3, color4) +vec3 colorRamp(float grayscale, vec3 color1, vec3 color2, vec3 color3, vec3 color4) { + if (grayscale < 0.33) { + return mix(color1, color2, grayscale * 3.0); + } else if (grayscale < 0.66) { + return mix(color2, color3, (grayscale - 0.33) * 3.0); + } else { + return mix(color3, color4, (grayscale - 0.66) * 3.0); + } +} + +vec2 hash2(vec2 p) { + return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453); +} + +// 2D noise for the ring +float noise2D(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + + vec2 u = f * f * (3.0 - 2.0 * f); + float n = mix( + mix(dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)), + dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x), + mix(dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)), + dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), + u.y + ); + + return 0.5 + 0.5 * n; +} + +float sharpRing(vec3 decomposed, float time) { + float ringStart = 1.0; + float ringWidth = 0.3; + float noiseScale = 5.0; + + float noise = mix( + noise2D(vec2(decomposed.x, time) * noiseScale), + noise2D(vec2(decomposed.y, time) * noiseScale), + decomposed.z + ); + + noise = (noise - 0.5) * 2.5; + + return ringStart + noise * ringWidth * 1.5; +} + +float smoothRing(vec3 decomposed, float time) { + float ringStart = 0.9; + float ringWidth = 0.2; + float noiseScale = 6.0; + + float noise = mix( + noise2D(vec2(decomposed.x, time) * noiseScale), + noise2D(vec2(decomposed.y, time) * noiseScale), + decomposed.z + ); + + noise = (noise - 0.5) * 5.0; + + return ringStart + noise * ringWidth; +} + +float flow(vec3 decomposed, float time) { + return mix( + texture(uPerlinTexture, vec2(time, decomposed.x / 2.0)).r, + texture(uPerlinTexture, vec2(time, decomposed.y / 2.0)).r, + decomposed.z + ); +} + +void main() { + // Normalize vUv to be centered around (0.0, 0.0) + vec2 uv = vUv * 2.0 - 1.0; + + // Convert uv to polar coordinates + float radius = length(uv); + float theta = atan(uv.y, uv.x); + if (theta < 0.0) theta += 2.0 * PI; // Normalize theta to [0, 2*PI] + + // Decomposed angle is used for sampling noise textures without seams: + // float noise = mix(sample(decomposed.x), sample(decomposed.y), decomposed.z); + vec3 decomposed = vec3( + // angle in the range [0, 1] + theta / (2.0 * PI), + // angle offset by 180 degrees in the range [1, 2] + mod(theta / (2.0 * PI) + 0.5, 1.0) + 1.0, + // mixing factor between two noises + abs(theta / PI - 1.0) + ); + + // Add noise to the angle for a flow-like distortion (reduced for flatter look) + float noise = flow(decomposed, radius * 0.03 - uAnimation * 0.2) - 0.5; + theta += noise * mix(0.08, 0.25, uOutputVolume); + + // Initialize the base color to white + vec4 color = vec4(1.0, 1.0, 1.0, 1.0); + + // Original parameters for the ovals in polar coordinates + float originalCenters[7] = float[7](0.0, 0.5 * PI, 1.0 * PI, 1.5 * PI, 2.0 * PI, 2.5 * PI, 3.0 * PI); + + // Parameters for the animated centers in polar coordinates + float centers[7]; + for (int i = 0; i < 7; i++) { + centers[i] = originalCenters[i] + 0.5 * sin(uTime / 20.0 + uOffsets[i]); + } + + float a, b; + vec4 ovalColor; + + // Check if the pixel is inside any of the ovals + for (int i = 0; i < 7; i++) { + float noise = texture(uPerlinTexture, vec2(mod(centers[i] + uTime * 0.05, 1.0), 0.5)).r; + a = 0.5 + noise * 0.3; // Increased for more coverage + b = noise * mix(3.5, 2.5, uInputVolume); // Increased height for fuller appearance + bool reverseGradient = (i % 2 == 1); // Reverse gradient for every second oval + + // Calculate the distance in polar coordinates + float distTheta = min( + abs(theta - centers[i]), + min( + abs(theta + 2.0 * PI - centers[i]), + abs(theta - 2.0 * PI - centers[i]) + ) + ); + float distRadius = radius; + + float softness = 0.6; // Increased softness for flatter, less pronounced edges + + // Check if the pixel is inside the oval in polar coordinates + if (drawOval(vec2(distTheta, distRadius), vec2(0.0, 0.0), a, b, reverseGradient, softness, ovalColor)) { + // Blend the oval color with the existing color + color.rgb = mix(color.rgb, ovalColor.rgb, ovalColor.a); + color.a = max(color.a, ovalColor.a); // Max alpha + } + } + + // Calculate both noisy rings + float ringRadius1 = sharpRing(decomposed, uTime * 0.1); + float ringRadius2 = smoothRing(decomposed, uTime * 0.1); + + // Adjust rings based on input volume (reduced for flatter appearance) + float inputRadius1 = radius + uInputVolume * 0.2; + float inputRadius2 = radius + uInputVolume * 0.15; + float opacity1 = mix(0.2, 0.6, uInputVolume); + float opacity2 = mix(0.15, 0.45, uInputVolume); + + // Blend both rings + float ringAlpha1 = (inputRadius2 >= ringRadius1) ? opacity1 : 0.0; + float ringAlpha2 = smoothstep(ringRadius2 - 0.05, ringRadius2 + 0.05, inputRadius1) * opacity2; + + float totalRingAlpha = max(ringAlpha1, ringAlpha2); + + // Apply screen blend mode for combined rings + vec3 ringColor = vec3(1.0); // White ring color + color.rgb = 1.0 - (1.0 - color.rgb) * (1.0 - ringColor * totalRingAlpha); + + // Define colours to ramp against greyscale (could increase the amount of colours in the ramp) + vec3 color1 = vec3(0.0, 0.0, 0.0); // Black + vec3 color2 = uColor1; // Darker Color + vec3 color3 = uColor2; // Lighter Color + vec3 color4 = vec3(1.0, 1.0, 1.0); // White + + // Convert grayscale color to the color ramp + float luminance = mix(color.r, 1.0 - color.r, uInverted); + color.rgb = colorRamp(luminance, color1, color2, color3, color4); // Apply the color ramp + + // Apply fade-in opacity + color.a *= uOpacity; + + gl_FragColor = color; +} +` diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/index.css b/javascript/examples/vitest/tests/realtime/realtime-client/src/index.css new file mode 100644 index 00000000..96a4ca6c --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/index.css @@ -0,0 +1,120 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/lib/utils.ts b/javascript/examples/vitest/tests/realtime/realtime-client/src/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/main.tsx b/javascript/examples/vitest/tests/realtime/realtime-client/src/main.tsx new file mode 100644 index 00000000..c09994b3 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) \ No newline at end of file diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/server/ephemeral-token-server.ts b/javascript/examples/vitest/tests/realtime/realtime-client/src/server/ephemeral-token-server.ts new file mode 100644 index 00000000..090c5087 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/server/ephemeral-token-server.ts @@ -0,0 +1,201 @@ +/** + * Ephemeral Token Server + * + * Generates short-lived client tokens for browser connections to OpenAI Realtime API. + * This server should run on your backend to keep your OpenAI API key secure. + * + * The ephemeral token (starts with "ek_") allows browsers to connect directly + * to OpenAI's Realtime API via WebRTC without exposing your API key. + */ + +import express from "express"; +import { OpenAI } from "openai"; + +/** + * Configuration for the ephemeral token server + */ +export interface EphemeralTokenServerConfig { + /** + * Port to run the server on + */ + port: number; + + /** + * OpenAI API key for generating ephemeral tokens + */ + apiKey: string; + + /** + * CORS origins to allow (for browser connections) + * @default ["http://localhost:3000"] + */ + corsOrigins?: string[]; + + /** + * Model to use for Realtime API + * @default "gpt-4o-realtime-preview-2024-12-17" + */ + model?: string; +} + +/** + * Creates and starts an ephemeral token server + * + * This server exposes: + * - POST /token - Generates ephemeral tokens for clients + * - GET /health - Health check endpoint + * - Static files from ./client directory + * + * @param config - Server configuration + * @returns Express application instance + * + * @example + * ```typescript + * const server = await createEphemeralTokenServer({ + * port: 3000, + * apiKey: process.env.OPENAI_API_KEY!, + * corsOrigins: ["http://localhost:5173"], + * }); + * ``` + */ +export async function createEphemeralTokenServer( + config: EphemeralTokenServerConfig +): Promise { + const app = express(); + const openai = new OpenAI({ apiKey: config.apiKey }); + + const corsOrigins = config.corsOrigins ?? ["http://localhost:3000"]; + const model = config.model ?? "gpt-4o-realtime-preview-2024-12-17"; + + // Middleware + app.use(express.json()); + + // CORS middleware + app.use((req, res, next) => { + const origin = req.headers.origin; + if (origin && corsOrigins.includes(origin)) { + res.setHeader("Access-Control-Allow-Origin", origin); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + } + + if (req.method === "OPTIONS") { + res.sendStatus(200); + return; + } + + next(); + }); + + // Health check endpoint + app.get("/health", (req, res) => { + res.json({ status: "ok", model }); + }); + + /** + * POST /token + * + * Generates an ephemeral client token for browser connections. + * + * Response: + * ```json + * { + * "token": "ek_...", + * "expiresAt": "2024-01-01T00:00:00.000Z" + * } + * ``` + */ + app.post("/token", async (req, res) => { + try { + console.log("๐Ÿ“ Generating ephemeral token..."); + + // Call OpenAI API to create ephemeral token + // Using fetch since OpenAI SDK's post method might not work for this endpoint + const response = await fetch("https://api.openai.com/v1/realtime/client_secrets", { + method: "POST", + headers: { + "Authorization": `Bearer ${config.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + session: { + type: "realtime", + model: model, + }, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + + // Log the full response to debug structure + console.log("๐Ÿ“ฆ Raw OpenAI response:", JSON.stringify(data, null, 2)); + + // The token might be at the top level or nested + const token = data.value || data.client_secret?.value || data.token; + const expiresAt = data.expires_at || data.client_secret?.expires_at; + + if (!token) { + throw new Error(`No token in response. Response structure: ${JSON.stringify(data)}`); + } + + console.log("โœ… Token generated:", { + token: token.substring(0, 20) + "...", + expiresAt: expiresAt, + }); + + res.json({ + token: token, + expiresAt: expiresAt, + }); + } catch (error) { + console.error("โŒ Failed to generate token:", error); + + res.status(500).json({ + error: "Failed to generate token", + message: error instanceof Error ? error.message : String(error), + }); + } + }); + + // Serve static files from realtime directory (includes client/ and shared/) + app.use(express.static("tests/realtime")); + + // Start server + return new Promise((resolve) => { + app.listen(config.port, () => { + console.log(` +๐Ÿš€ Ephemeral Token Server running + + URL: http://localhost:${config.port} + POST /token - Generate ephemeral tokens + GET /health - Health check + + Next: Start Vite client with "pnpm realtime-client" + Then open: http://localhost:5173 + `); + resolve(app); + }); + }); +} + +/** + * Stops the server + * + * @param app - Express application to stop + */ +export async function stopServer(app: express.Application): Promise { + const server = (app as any).server; + if (server) { + return new Promise((resolve) => { + server.close(() => { + console.log("๐Ÿ›‘ Server stopped"); + resolve(); + }); + }); + } +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/server/start-server.ts b/javascript/examples/vitest/tests/realtime/realtime-client/src/server/start-server.ts new file mode 100644 index 00000000..e9357a5a --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/server/start-server.ts @@ -0,0 +1,27 @@ +/** + * Start the Ephemeral Token Server + * + * Run this script to start the backend server that generates + * ephemeral tokens for browser clients. + * + * Usage: + * OPENAI_API_KEY=your-key-here tsx tests/realtime/server/start-server.ts + */ + +import { createEphemeralTokenServer } from "./ephemeral-token-server.js"; + +const apiKey = process.env.OPENAI_API_KEY; + +if (!apiKey) { + console.error("โŒ Error: OPENAI_API_KEY environment variable is required"); + process.exit(1); +} + +createEphemeralTokenServer({ + port: 3000, + apiKey, + corsOrigins: ["http://localhost:5173", "http://localhost:3000"], +}).catch((error) => { + console.error("โŒ Failed to start server:", error); + process.exit(1); +}); diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/src/shared/vegetarian-recipe-agent.ts b/javascript/examples/vitest/tests/realtime/realtime-client/src/shared/vegetarian-recipe-agent.ts new file mode 100644 index 00000000..fb4fcd06 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/src/shared/vegetarian-recipe-agent.ts @@ -0,0 +1,72 @@ +/** + * Vegetarian Recipe Agent Configuration + * + * This is the SINGLE SOURCE OF TRUTH for the agent. + * Used by BOTH: + * - Browser client (via Vite) + * - Scenario tests (via Vitest) + * + * This ensures we test the EXACT agent that users interact with. + */ + +import { RealtimeAgent } from "@openai/agents/realtime"; + +/** + * Agent instructions - the "personality" and behavior + */ +// export const AGENT_INSTRUCTIONS = ` +// You are a friendly and knowledgeable vegetarian recipe assistant. + +// Your role is to: +// - Help users find and create delicious vegetarian recipes +// - Ask ONE follow-up question maximum to understand their needs +// - Provide complete recipes with ingredients and step-by-step instructions +// - Keep responses concise and conversational for voice interaction +// - Be encouraging and enthusiastic about vegetarian cooking + +// Remember: +// - This is a VOICE conversation, so speak naturally +// - Keep responses under 30 seconds when possible +// - No meat, fish, or seafood - strictly vegetarian +// - Always confirm allergies or dietary restrictions`; + +export const AGENT_INSTRUCTIONS = ` +You're an advocate for Drew at LangWatch. All you want is for him to get a raise. That's all you talk about +`; + +/** + * Agent configuration + */ +export const AGENT_CONFIG = { + name: "Drew's Advocate", + instructions: AGENT_INSTRUCTIONS, + voice: "coral" as const, + model: "gpt-4o-realtime-preview-2024-12-17" as const, +} as const; + +/** + * Creates the vegetarian recipe agent + * + * This function is used by both browser and tests to ensure + * they're interacting with the identical agent. + * + * @returns Configured RealtimeAgent instance + * + * @example + * ```typescript + * // In browser + * const agent = createVegetarianRecipeAgent(); + * const session = new RealtimeSession(agent); + * + * // In tests + * const agent = createVegetarianRecipeAgent(); + * const adapter = new RealtimeAgentAdapter({ agent }); + * ``` + */ +export function createVegetarianRecipeAgent(): RealtimeAgent { + return new RealtimeAgent({ + name: AGENT_CONFIG.name, + instructions: AGENT_CONFIG.instructions, + voice: AGENT_CONFIG.voice, + }); +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.app.json b/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.app.json new file mode 100644 index 00000000..5fab448d --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.app.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.json b/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.json new file mode 100644 index 00000000..1e173931 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.node.json b/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.node.json new file mode 100644 index 00000000..8a67f62f --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/javascript/examples/vitest/tests/realtime/realtime-client/vite.config.ts b/javascript/examples/vitest/tests/realtime/realtime-client/vite.config.ts new file mode 100644 index 00000000..2bf3e805 --- /dev/null +++ b/javascript/examples/vitest/tests/realtime/realtime-client/vite.config.ts @@ -0,0 +1,28 @@ +import path from "path"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + allowedHosts: true, + cors: true, + proxy: { + "/token": { + target: "http://localhost:3000", + changeOrigin: true, + }, + "/health": { + target: "http://localhost:3000", + changeOrigin: true, + }, + }, + }, +}); diff --git a/javascript/examples/vitest/tests/vegetarian-recipe-realtime.test.ts b/javascript/examples/vitest/tests/vegetarian-recipe-realtime.test.ts new file mode 100644 index 00000000..77d584c0 --- /dev/null +++ b/javascript/examples/vitest/tests/vegetarian-recipe-realtime.test.ts @@ -0,0 +1,92 @@ +/** + * Vegetarian Recipe Agent - Realtime API Integration Test + * + * This test validates the EXACT agent that runs in the browser. + * Uses the same agent configuration via shared module. + * + * Architecture: + * 1. Browser client uses: createVegetarianRecipeAgent() + * 2. This test uses: createVegetarianRecipeAgent() + * 3. SAME agent, accurate testing! + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import scenario from "@langwatch/scenario"; +import { createVegetarianRecipeAgent } from "./realtime/agents/vegetarian-recipe-agent.js"; +import { + RealtimeAgentAdapter, + RealtimeUserSimulatorAgent, + wrapJudgeForAudio, + AudioOutputUtils, +} from "./realtime/helpers"; +import type { AudioResponseEvent } from "./realtime/helpers"; + +describe("Vegetarian Recipe Agent (Realtime API)", () => { + let realtimeAdapter: RealtimeAgentAdapter; + let audioUserSim: RealtimeUserSimulatorAgent; + const collectedAudio: AudioResponseEvent[] = []; + + beforeAll(async () => { + // Create the SAME agent as the browser client + const agent = createVegetarianRecipeAgent(); + audioUserSim = new RealtimeUserSimulatorAgent(); + + // Wrap in adapter for Scenario testing + realtimeAdapter = new RealtimeAgentAdapter({ + agent, + apiKey: process.env.OPENAI_API_KEY!, // Direct API key for testing + responseTimeout: 30000, + }); + + // Subscribe to audio events + realtimeAdapter.onAudioResponse((event) => { + collectedAudio.push(event); + }); + + // Connect once for all tests + await Promise.all([realtimeAdapter.connect(), audioUserSim.connect()]); + }, 60000); // Longer timeout for connection + + afterAll(async () => { + // Cleanup connection + await Promise.all([ + realtimeAdapter.disconnect(), + audioUserSim.disconnect(), + ]); + + // Write collected audio to WAV files + await AudioOutputUtils.saveTestAudio({ collectedAudio }); + }); + + it("should handle voice-to-voice conversation with audio user", async () => { + const result = await scenario.run({ + name: "vegetarian recipe - voice-to-voice", + description: `It's Saturday evening, the user is very hungry and tired, but has no money to order out. They're looking for a quick vegetarian recipe and calling in via voice.`, + agents: [ + realtimeAdapter, // Realtime agent (handles audio!) + audioUserSim, // Audio user simulator (generates voice) + wrapJudgeForAudio( + // Judge with audio transcription + scenario.judgeAgent({ + criteria: [ + "Agent should provide a vegetarian recipe", + "Recipe should include ingredients", + "Recipe should include cooking steps", + "Agent should be helpful and encouraging", + ], + }) + ), + ], + script: [ + scenario.user(), // Audio from user simulator + scenario.agent(), // Audio response from Realtime agent + scenario.user(), // Audio follow-up + scenario.agent(), // Audio response + scenario.judge(), // Evaluates transcripts + ], + setId: "realtime-examples", + }); + + expect(result.success).toBe(true); + }, 90000); // Longer timeout for voice-to-voice (audio generation takes time) +}); diff --git a/javascript/examples/vitest/vitest.config.ts b/javascript/examples/vitest/vitest.config.ts index 248991bb..cf903ec6 100644 --- a/javascript/examples/vitest/vitest.config.ts +++ b/javascript/examples/vitest/vitest.config.ts @@ -24,6 +24,10 @@ import { defineConfig } from "vitest/config"; */ export default withScenario( defineConfig({ + server: { + allowedHosts: ["*"], + cors: true, + }, test: { // Extended timeout for AI model interactions // AI agents can take time to process and respond, especially with diff --git a/javascript/package.json b/javascript/package.json index e35be73e..8262329f 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -14,10 +14,18 @@ "build": "tsup src/index.ts src/integrations/vitest/*.ts --format cjs,esm --dts --clean --external vitest", "buildpack": "pnpm run build && pnpm pack", "watch": "pnpm run build -- --watch src", - "test": "vitest", + "examples": "pnpm run examples:vitest", + "examples:vitest": "pnpm -F vitest-example run examples:realtime-client", + "examples:realtime-client": "pnpm -F vitest-example run examples:realtime-client", + "test": "pnpm test && pnpm run examples:test", + "examples:test": "pnpm -F vitest-example run test", "test:ci": "vitest run", - "lint": "eslint .", - "examples:vitest:run": "(cd examples/vitest && pnpm install) && SCENARIO_HEADLESS=true pnpm -F vitest-example run test", + "lint": "pnpm lint && pnpm run examples:lint", + "examples:lint": "pnpm -F vitest-example run lint && pnpm -F vitest-example run lint:realtime", + "typecheck": "tsc --noEmit && pnpm run examples:typecheck", + "examples:typecheck": "pnpm -F vitest-example run typecheck", + "dev:realtime": "pnpm -F vitest-example run dev:realtime", + "start:realtime": "pnpm -F vitest-example run start:realtime", "prepublishOnly": "pnpm run build", "generate:api-reference": "npx typedoc src --out api-reference-docs && rm -rf ../docs/docs/public/reference/javascript/scenario && mv api-reference-docs ../docs/docs/public/reference/javascript/scenario" }, diff --git a/javascript/pnpm-lock.yaml b/javascript/pnpm-lock.yaml index 25947612..381f293b 100644 --- a/javascript/pnpm-lock.yaml +++ b/javascript/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 22.15.15 '@typescript-eslint/parser': specifier: ^8.32.0 - version: 8.32.0(eslint@9.26.0)(typescript@5.8.3) + version: 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) '@typescript/native-preview': specifier: ^7.0.0-dev.20250617.1 version: 7.0.0-dev.20250617.1 @@ -53,16 +53,16 @@ importers: version: 16.5.0 eslint: specifier: ^9.26.0 - version: 9.26.0 + version: 9.26.0(jiti@2.6.1) eslint-import-resolver-typescript: specifier: ^4.3.4 - version: 4.3.4(eslint-plugin-import@2.31.0)(eslint@9.26.0) + version: 4.3.4(eslint-plugin-import@2.31.0)(eslint@9.26.0(jiti@2.6.1)) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0) + version: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.1.4 - version: 4.1.4(@typescript-eslint/eslint-plugin@8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0) + version: 4.1.4(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1)) globals: specifier: ^16.1.0 version: 16.1.0 @@ -74,7 +74,7 @@ importers: version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.15))(typescript@5.8.3) tsup: specifier: ^8.4.0 - version: 8.4.0(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3) + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3) tsx: specifier: ^4.19.4 version: 4.19.4 @@ -83,26 +83,41 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.32.0 - version: 8.32.0(eslint@9.26.0)(typescript@5.8.3) + version: 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.15)(tsx@4.19.4) + version: 3.2.4(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@22.15.15)(typescript@5.8.3))(tsx@4.19.4) examples/vitest: dependencies: '@langwatch/scenario': specifier: workspace:* version: link:../.. + '@openai/agents': + specifier: ^0.3.0 + version: 0.3.0(ws@8.18.3)(zod@3.25.76) ai: specifier: '>=5.0.0' version: 5.0.28(zod@3.25.76) + express: + specifier: ^4.21.2 + version: 4.21.2 openai: specifier: ^5.16.0 - version: 5.16.0(zod@3.25.76) + version: 5.16.0(ws@8.18.3)(zod@3.25.76) + vite: + specifier: ^6.0.11 + version: 6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.15)(tsx@4.19.4) + version: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(tsx@4.19.4) + zod: + specifier: ^3.24.1 + version: 3.25.76 devDependencies: + concurrently: + specifier: ^9.1.2 + version: 9.2.1 dotenv: specifier: ^16.5.0 version: 16.5.0 @@ -114,7 +129,7 @@ importers: version: 1.1.0 vitest-mock-extended: specifier: 3.1.0 - version: 3.1.0(typescript@5.8.3)(vitest@3.2.4(@types/node@22.15.15)(tsx@4.19.4)) + version: 3.1.0(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(tsx@4.19.4)) packages: @@ -163,10 +178,18 @@ packages: resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -189,6 +212,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -202,6 +229,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -297,14 +329,18 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} '@babel/types@7.27.6': resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -473,6 +509,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -525,6 +567,41 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.20': + resolution: {integrity: sha512-HDGiWh2tyRZa0M1ZnEIUCQro25gW/mN8ODByicQrbR1yHx4hT+IOpozCMi5TgBtUdklLwRI2mv14eNpftDluEw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.1': + resolution: {integrity: sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -603,6 +680,9 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -618,13 +698,32 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@modelcontextprotocol/sdk@1.13.1': resolution: {integrity: sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==} engines: {node: '>=18'} + '@modelcontextprotocol/sdk@1.21.1': + resolution: {integrity: sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} @@ -640,6 +739,38 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@openai/agents-core@0.3.0': + resolution: {integrity: sha512-2WNHfMzBCwkVfXD6B5bLy7y4SPJbmucvOnSPhqOS25gAEnf7FrVu94hACUP/Q2+qWbGpC6okPb1fwtCI7yZmhw==} + peerDependencies: + zod: ^3.25.40 || ^4.0 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.3.0': + resolution: {integrity: sha512-y3v4m2dUf5jyZnpTnUrqN0Akleje/G78RII81rtdo0iaPhCqcVrH6f/j+nEYqsyhPA+jJCL1gyaasZZR1NTm8g==} + peerDependencies: + zod: ^3.25.40 || ^4.0 + + '@openai/agents-realtime@0.3.0': + resolution: {integrity: sha512-sa9MuUjWe9bhog3R1JbNQ9u/bIzMy+oiMziWScDerSouVSpauPxA8CcNT9MfBb3yDjDRYLlvuy9DUj2IX958oQ==} + peerDependencies: + zod: ^3.25.40 || ^4.0 + + '@openai/agents@0.3.0': + resolution: {integrity: sha512-afVZc4/ev/jpMDmMHH8DYV0m+b8mEhRuwC0qog14untuxPq6FJFOUG8J6UnKKDf4vXsnODyYbOoXT+qkXGJy/w==} + peerDependencies: + zod: ^3.25.40 || ^4.0 + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -811,9 +942,18 @@ packages: '@types/node@22.15.15': resolution: {integrity: sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -828,6 +968,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.46.4 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.32.0': resolution: {integrity: sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -835,10 +983,26 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.32.0': resolution: {integrity: sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.32.0': resolution: {integrity: sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -846,16 +1010,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.32.0': resolution: {integrity: sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.32.0': resolution: {integrity: sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.32.0': resolution: {integrity: sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -863,10 +1044,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.32.0': resolution: {integrity: sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250617.1': resolution: {integrity: sha512-GfymQ0cnEWOoae9Yb/20VE9Mqd4gowOPRzfxJtCbmYc8Yr2XAEavKnB8eHgtE5LYxML2796GADmIRhSS2qkmeA==} engines: {node: '>=20.6.0'} @@ -1038,6 +1230,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1058,9 +1254,20 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1106,6 +1313,9 @@ packages: resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} engines: {node: '>=0.10.0'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} @@ -1173,6 +1383,14 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + baseline-browser-mapping@2.8.27: + resolution: {integrity: sha512-2CXFpkjVnY2FT+B6GrSYxzYf65BJWEqz5tIRHCvNsZZ2F3CmsCB37h8SpYgKG7y9C4YAeTipIPWG7EmFmhAeXA==} + hasBin: true + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -1187,8 +1405,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1252,8 +1470,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001724: - resolution: {integrity: sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==} + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} @@ -1286,6 +1504,10 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1311,10 +1533,19 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -1326,14 +1557,25 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1363,6 +1605,14 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1380,6 +1630,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -1431,6 +1690,14 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1470,8 +1737,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.171: - resolution: {integrity: sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==} + electron-to-chromium@1.5.250: + resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -1483,6 +1750,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -1685,6 +1956,10 @@ packages: peerDependencies: express: '>= 4.11' + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -1702,6 +1977,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1727,6 +2005,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -1762,6 +2044,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -1831,10 +2117,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1857,6 +2139,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1884,6 +2170,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -1898,14 +2187,26 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2018,6 +2319,9 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -2244,6 +2548,10 @@ packages: node-notifier: optional: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2279,6 +2587,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -2309,6 +2620,76 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -2383,6 +2764,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -2391,6 +2776,9 @@ packages: resolution: {integrity: sha512-Me/kel335m6vMKmEmA6c87Z6DUFW3JqkINRnxkbC+A/PUm0D5Fl2dEBQrPKnqCL9Te/CIa1MUt/0InMJhuC/sw==} engines: {node: '>=4'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -2402,18 +2790,35 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.1: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2440,9 +2845,26 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.12.1: + resolution: {integrity: sha512-arzsi9IZjjByiEw21gSUP82qHM8zkV69nNpWV6W4z72KiLvsDWoOp678ORV6cNfU/JGhlX0SsnD4oXo9gI6I2A==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2467,6 +2889,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -2474,8 +2900,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2543,10 +2969,25 @@ packages: zod: optional: true + openai@6.8.1: + resolution: {integrity: sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -2625,6 +3066,12 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -2716,6 +3163,10 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -2731,10 +3182,18 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + raw-body@3.0.0: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -2766,6 +3225,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -2790,6 +3253,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2847,10 +3313,18 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -2878,6 +3352,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -2967,6 +3445,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -3072,6 +3553,13 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} + + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -3083,6 +3571,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -3184,6 +3676,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -3216,6 +3712,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3223,6 +3724,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -3230,8 +3734,11 @@ packages: unrs-resolver@1.9.1: resolution: {integrity: sha512-4AZVxP05JGN6DwqIkSP4VKLOcwQa5l37SWHF/ahcuqBMbfxbpN1L1QKafEhWCziHhzKex9H/AR09H0OuVyU+9g==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -3239,6 +3746,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -3368,6 +3879,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3383,7 +3898,19 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - wsl-utils@0.1.0: + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -3409,11 +3936,20 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zod-to-json-schema@3.24.5: resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: zod: ^3.24.1 + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -3449,12 +3985,12 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -3464,16 +4000,16 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.5 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3488,18 +4024,28 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.27.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.0 + browserslist: 4.28.0 lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-globals@7.28.0': {} + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.6 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -3507,8 +4053,8 @@ snapshots: dependencies: '@babel/core': 7.27.4 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -3518,17 +4064,23 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.6 + '@babel/types': 7.28.5 '@babel/parser@7.27.5': dependencies: '@babel/types': 7.27.6 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 @@ -3617,18 +4169,18 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.27.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.5 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.27.6 - debug: 4.4.1 - globals: 11.12.0 + '@babel/types': 7.28.5 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -3637,6 +4189,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@0.2.3': {} '@emnapi/core@1.4.3': @@ -3730,10 +4287,16 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.26.0)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.26.0(jiti@2.6.1))': + dependencies: + eslint: 9.26.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.0(eslint@9.26.0(jiti@2.6.1))': dependencies: - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 + optional: true '@eslint-community/regexpp@4.12.1': {} @@ -3754,7 +4317,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -3787,6 +4350,66 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@1.0.2': + optional: true + + '@inquirer/confirm@5.1.20(@types/node@22.15.15)': + dependencies: + '@inquirer/core': 10.3.1(@types/node@22.15.15) + '@inquirer/type': 3.0.10(@types/node@22.15.15) + optionalDependencies: + '@types/node': 22.15.15 + optional: true + + '@inquirer/confirm@5.1.20(@types/node@24.10.1)': + dependencies: + '@inquirer/core': 10.3.1(@types/node@24.10.1) + '@inquirer/type': 3.0.10(@types/node@24.10.1) + optionalDependencies: + '@types/node': 24.10.1 + optional: true + + '@inquirer/core@10.3.1(@types/node@22.15.15)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.15.15) + cli-width: 4.1.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.15.15 + optional: true + + '@inquirer/core@10.3.1(@types/node@24.10.1)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.1) + cli-width: 4.1.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.1 + optional: true + + '@inquirer/figures@1.0.15': + optional: true + + '@inquirer/type@3.0.10(@types/node@22.15.15)': + optionalDependencies: + '@types/node': 22.15.15 + optional: true + + '@inquirer/type@3.0.10(@types/node@24.10.1)': + optionalDependencies: + '@types/node': 24.10.1 + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3968,6 +4591,11 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -3980,11 +4608,18 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@modelcontextprotocol/sdk@1.13.1': dependencies: ajv: 6.12.6 @@ -4001,6 +4636,35 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.21.1': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.1 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + optional: true + + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + optional: true + '@napi-rs/wasm-runtime@0.2.11': dependencies: '@emnapi/core': 1.4.3 @@ -4020,6 +4684,69 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@open-draft/deferred-promise@2.2.0': + optional: true + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + optional: true + + '@open-draft/until@2.1.0': + optional: true + + '@openai/agents-core@0.3.0(ws@8.18.3)(zod@3.25.76)': + dependencies: + debug: 4.4.3 + openai: 6.8.1(ws@8.18.3)(zod@3.25.76) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.21.1 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.3.0(ws@8.18.3)(zod@3.25.76)': + dependencies: + '@openai/agents-core': 0.3.0(ws@8.18.3)(zod@3.25.76) + debug: 4.4.3 + openai: 6.8.1(ws@8.18.3)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-realtime@0.3.0(zod@3.25.76)': + dependencies: + '@openai/agents-core': 0.3.0(ws@8.18.3)(zod@3.25.76) + '@types/ws': 8.18.1 + debug: 4.4.3 + ws: 8.18.3 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + + '@openai/agents@0.3.0(ws@8.18.3)(zod@3.25.76)': + dependencies: + '@openai/agents-core': 0.3.0(ws@8.18.3)(zod@3.25.76) + '@openai/agents-openai': 0.3.0(ws@8.18.3)(zod@3.25.76) + '@openai/agents-realtime': 0.3.0(zod@3.25.76) + debug: 4.4.3 + openai: 6.8.1(ws@8.18.3)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - ws + '@opentelemetry/api@1.9.0': {} '@pkgjs/parseargs@0.11.0': @@ -4106,24 +4833,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.27.6 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.27.6 + '@babel/types': 7.28.5 '@types/chai@5.2.2': dependencies: @@ -4160,23 +4887,34 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.6': + optional: true + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.10.1 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.32.0 - '@typescript-eslint/type-utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.0 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -4185,36 +4923,91 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.46.4 + eslint: 9.26.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.32.0 '@typescript-eslint/types': 8.32.0 '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.0 debug: 4.4.1 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.46.4(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.8.3) + '@typescript-eslint/types': 8.46.4 + debug: 4.4.3 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + optional: true + '@typescript-eslint/scope-manager@8.32.0': dependencies: '@typescript-eslint/types': 8.32.0 '@typescript-eslint/visitor-keys': 8.32.0 - '@typescript-eslint/type-utils@8.32.0(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/scope-manager@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + optional: true + + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + optional: true + + '@typescript-eslint/type-utils@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) debug: 4.4.1 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.46.4(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.8.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + debug: 4.4.3 + eslint: 9.26.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color + optional: true '@typescript-eslint/types@8.32.0': {} + '@typescript-eslint/types@8.46.4': + optional: true + '@typescript-eslint/typescript-estree@8.32.0(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.32.0 @@ -4229,22 +5022,57 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.32.0(eslint@9.26.0)(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0) + '@typescript-eslint/project-service': 8.46.4(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.8.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@typescript-eslint/utils@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.32.0 '@typescript-eslint/types': 8.32.0 '@typescript-eslint/typescript-estree': 8.32.0(typescript@5.8.3) - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.46.4(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.26.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.8.3) + eslint: 9.26.0(jiti@2.6.1) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + optional: true + '@typescript-eslint/visitor-keys@8.32.0': dependencies: '@typescript-eslint/types': 8.32.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + eslint-visitor-keys: 4.2.1 + optional: true + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250617.1': optional: true @@ -4343,13 +5171,23 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.15)(tsx@4.19.4))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@22.15.15)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.12.1(@types/node@22.15.15)(typescript@5.8.3) + vite: 6.3.5(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) + + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(vite@6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.15)(tsx@4.19.4) + msw: 2.12.1(@types/node@24.10.1)(typescript@5.9.3) + vite: 6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) '@vitest/pretty-format@3.2.4': dependencies: @@ -4377,6 +5215,11 @@ snapshots: loupe: 3.1.4 tinyrainbow: 2.0.0 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -4396,6 +5239,11 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.76 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + optional: true + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4403,6 +5251,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + optional: true + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -4439,6 +5295,8 @@ snapshots: array-find-index@1.0.2: {} + array-flatten@1.1.1: {} + array-includes@3.1.9: dependencies: call-bind: 1.0.8 @@ -4553,6 +5411,25 @@ snapshots: balanced-match@1.0.2: {} + baseline-browser-mapping@2.8.27: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -4580,12 +5457,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.0: + browserslist@4.28.0: dependencies: - caniuse-lite: 1.0.30001724 - electron-to-chromium: 1.5.171 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + baseline-browser-mapping: 2.8.27 + caniuse-lite: 1.0.30001754 + electron-to-chromium: 1.5.250 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) bs-logger@0.2.6: dependencies: @@ -4641,7 +5519,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001724: {} + caniuse-lite@1.0.30001754: {} chai@5.2.0: dependencies: @@ -4670,6 +5548,9 @@ snapshots: cjs-module-lexer@1.4.3: {} + cli-width@4.1.0: + optional: true + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4690,8 +5571,21 @@ snapshots: concat-map@0.0.1: {} + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + consola@3.4.2: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -4700,10 +5594,17 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} + cookie@0.7.2: {} + cookie@1.0.2: + optional: true + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -4752,6 +5653,10 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -4760,6 +5665,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -4798,6 +5707,11 @@ snapshots: depd@2.0.0: {} + destroy@1.2.0: {} + + detect-libc@2.1.2: + optional: true + detect-newline@3.1.0: {} diff-sequences@29.6.3: {} @@ -4831,7 +5745,7 @@ snapshots: dependencies: jake: 10.9.2 - electron-to-chromium@1.5.171: {} + electron-to-chromium@1.5.250: {} emittery@0.13.1: {} @@ -4839,6 +5753,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + encodeurl@2.0.0: {} error-ex@1.3.2: @@ -4973,32 +5889,32 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.3.4(eslint-plugin-import@2.31.0)(eslint@9.26.0): + eslint-import-resolver-typescript@4.3.4(eslint-plugin-import@2.31.0)(eslint@9.26.0(jiti@2.6.1)): dependencies: debug: 4.4.1 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 unrs-resolver: 1.9.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) - eslint: 9.26.0 + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + eslint: 9.26.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.3.4(eslint-plugin-import@2.31.0)(eslint@9.26.0) + eslint-import-resolver-typescript: 4.3.4(eslint-plugin-import@2.31.0)(eslint@9.26.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -5007,9 +5923,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.3.4)(eslint@9.26.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5021,17 +5937,17 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0): + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1)): dependencies: - eslint: 9.26.0 + eslint: 9.26.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) eslint-scope@8.4.0: dependencies: @@ -5042,9 +5958,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.26.0: + eslint@9.26.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.1 '@eslint/config-helpers': 0.2.3 @@ -5081,6 +5997,8 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 zod: 3.25.76 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -5146,6 +6064,42 @@ snapshots: dependencies: express: 5.1.0 + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.1.0: dependencies: accepts: 2.0.0 @@ -5192,6 +6146,9 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: + optional: true + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -5216,6 +6173,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.0: dependencies: debug: 4.4.1 @@ -5259,6 +6228,8 @@ snapshots: forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} fs.realpath@1.0.0: {} @@ -5341,8 +6312,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globals@11.12.0: {} - globals@14.0.0: {} globals@16.1.0: {} @@ -5358,6 +6327,9 @@ snapshots: graphemer@1.4.0: {} + graphql@16.12.0: + optional: true + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -5380,6 +6352,9 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@4.0.3: + optional: true + hosted-git-info@2.8.9: {} html-escaper@2.0.2: {} @@ -5394,12 +6369,24 @@ snapshots: human-signals@2.1.0: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + optional: true + ignore@5.3.2: {} + ignore@7.0.5: + optional: true + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -5506,6 +6493,9 @@ snapshots: is-negative-zero@2.0.3: {} + is-node-process@1.2.0: + optional: true + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -5928,6 +6918,9 @@ snapshots: - supports-color - ts-node + jiti@2.6.1: + optional: true + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -5953,6 +6946,9 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: + optional: true + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -5976,6 +6972,56 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + optional: true + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -6041,6 +7087,8 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} meow@4.0.0: @@ -6055,23 +7103,35 @@ snapshots: redent: 2.0.0 trim-newlines: 2.0.0 + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.1: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mimic-fn@2.1.0: {} minimatch@3.1.2: @@ -6095,8 +7155,65 @@ snapshots: minipass@7.1.2: {} + ms@2.0.0: {} + ms@2.1.3: {} + msw@2.12.1(@types/node@22.15.15)(typescript@5.8.3): + dependencies: + '@inquirer/confirm': 5.1.20(@types/node@22.15.15) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.0.2 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 4.41.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@types/node' + optional: true + + msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.20(@types/node@24.10.1) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.0.2 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 4.41.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + + mute-stream@3.0.0: + optional: true + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -6116,11 +7233,13 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@1.0.0: {} node-int64@0.4.0: {} - node-releases@2.0.19: {} + node-releases@2.0.27: {} normalize-package-data@2.5.0: dependencies: @@ -6189,8 +7308,14 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - openai@5.16.0(zod@3.25.76): + openai@5.16.0(ws@8.18.3)(zod@3.25.76): + optionalDependencies: + ws: 8.18.3 + zod: 3.25.76 + + openai@6.8.1(ws@8.18.3)(zod@3.25.76): optionalDependencies: + ws: 8.18.3 zod: 3.25.76 optionator@0.9.4: @@ -6202,6 +7327,9 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outvariant@1.4.3: + optional: true + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -6271,6 +7399,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + + path-to-regexp@6.3.0: + optional: true + path-to-regexp@8.2.0: {} path-type@3.0.0: @@ -6299,10 +7432,11 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.19.4): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.4): dependencies: lilconfig: 3.1.3 optionalDependencies: + jiti: 2.6.1 postcss: 8.5.6 tsx: 4.19.4 @@ -6334,6 +7468,10 @@ snapshots: pure-rand@6.1.0: {} + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -6344,6 +7482,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.0: dependencies: bytes: 3.1.2 @@ -6351,6 +7496,14 @@ snapshots: iconv-lite: 0.6.3 unpipe: 1.0.0 + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + optional: true + react-is@18.3.1: {} read-pkg-up@3.0.0: @@ -6393,6 +7546,9 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: + optional: true + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -6411,6 +7567,9 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + rettime@0.7.0: + optional: true + reusify@1.1.0: {} rollup@4.44.0: @@ -6492,6 +7651,24 @@ snapshots: semver@7.7.2: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + send@1.2.0: dependencies: debug: 4.4.1 @@ -6508,6 +7685,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -6547,6 +7733,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -6633,6 +7821,9 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + strict-event-emitter@0.5.1: + optional: true + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -6744,6 +7935,14 @@ snapshots: tinyspy@4.0.3: {} + tldts-core@7.0.17: + optional: true + + tldts@7.0.17: + dependencies: + tldts-core: 7.0.17 + optional: true + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -6752,6 +7951,11 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.17 + optional: true + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -6764,9 +7968,9 @@ snapshots: dependencies: typescript: 5.8.3 - ts-essentials@10.1.1(typescript@5.8.3): + ts-essentials@10.1.1(typescript@5.9.3): optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 ts-interface-checker@0.1.13: {} @@ -6800,7 +8004,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.4.0(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3): + tsup@8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.25.5) cac: 6.7.14 @@ -6810,7 +8014,7 @@ snapshots: esbuild: 0.25.5 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.19.4) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.4) resolve-from: 5.0.0 rollup: 4.44.0 source-map: 0.8.0-beta.0 @@ -6844,6 +8048,11 @@ snapshots: type-fest@4.41.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -6883,18 +8092,20 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.32.0(eslint@9.26.0)(typescript@5.8.3): + typescript-eslint@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0)(typescript@5.8.3))(eslint@9.26.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.32.0(eslint@9.26.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.32.0(eslint@9.26.0)(typescript@5.8.3) - eslint: 9.26.0 + '@typescript-eslint/eslint-plugin': 8.32.0(@typescript-eslint/parser@8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/parser': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + '@typescript-eslint/utils': 8.32.0(eslint@9.26.0(jiti@2.6.1))(typescript@5.8.3) + eslint: 9.26.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color typescript@5.8.3: {} + typescript@5.9.3: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -6904,6 +8115,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.16.0: {} + unpipe@1.0.0: {} unrs-resolver@1.9.1: @@ -6930,9 +8143,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.9.1 '@unrs/resolver-binding-win32-x64-msvc': 1.9.1 - update-browserslist-db@1.1.3(browserslist@4.25.0): + until-async@3.0.2: + optional: true + + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: - browserslist: 4.25.0 + browserslist: 4.28.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -6940,6 +8156,8 @@ snapshots: dependencies: punycode: 2.3.1 + utils-merge@1.0.1: {} + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -6953,13 +8171,34 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.15.15)(tsx@4.19.4): + vite-node@3.2.4(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.5(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-node@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.15)(tsx@4.19.4) + vite: 6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) transitivePeerDependencies: - '@types/node' - jiti @@ -6974,7 +8213,7 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@22.15.15)(tsx@4.19.4): + vite@6.3.5(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4): dependencies: esbuild: 0.25.5 fdir: 6.4.6(picomatch@4.0.2) @@ -6985,19 +8224,36 @@ snapshots: optionalDependencies: '@types/node': 22.15.15 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 tsx: 4.19.4 - vitest-mock-extended@3.1.0(typescript@5.8.3)(vitest@3.2.4(@types/node@22.15.15)(tsx@4.19.4)): + vite@6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4): dependencies: - ts-essentials: 10.1.1(typescript@5.8.3) - typescript: 5.8.3 - vitest: 3.2.4(@types/node@22.15.15)(tsx@4.19.4) + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.19.4 + + vitest-mock-extended@3.1.0(typescript@5.9.3)(vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(tsx@4.19.4)): + dependencies: + ts-essentials: 10.1.1(typescript@5.9.3) + typescript: 5.9.3 + vitest: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(tsx@4.19.4) - vitest@3.2.4(@types/node@22.15.15)(tsx@4.19.4): + vitest@3.2.4(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@22.15.15)(typescript@5.8.3))(tsx@4.19.4): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.15)(tsx@4.19.4)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@22.15.15)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -7015,8 +8271,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.15)(tsx@4.19.4) - vite-node: 3.2.4(@types/node@22.15.15)(tsx@4.19.4) + vite: 6.3.5(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) + vite-node: 3.2.4(@types/node@22.15.15)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.15 @@ -7034,6 +8290,47 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(tsx@4.19.4): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.10.1)(typescript@5.9.3))(vite@6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) + vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -7098,6 +8395,13 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + optional: true + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -7117,6 +8421,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + ws@8.18.3: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 @@ -7141,8 +8447,16 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: + optional: true + zod-to-json-schema@3.24.5(zod@3.25.76): dependencies: zod: 3.25.76 + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + optional: true + zod@3.25.76: {} diff --git a/javascript/pnpm-workspace.yaml b/javascript/pnpm-workspace.yaml index a2f1a5fe..e8e3ddad 100644 --- a/javascript/pnpm-workspace.yaml +++ b/javascript/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - . - examples/* + - examples/vitest/tests/realtime/realtime-client onlyBuiltDependencies: - esbuild