diff --git a/.claude/tdd-guard/data/test.json b/.claude/tdd-guard/data/test.json new file mode 100644 index 00000000..6d8590e6 --- /dev/null +++ b/.claude/tdd-guard/data/test.json @@ -0,0 +1,5 @@ +{ + "testModules": [], + "unhandledErrors": [], + "reason": "failed" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0b29f602 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,43 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Default settings for all files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +# TypeScript and JavaScript +[*.{ts,tsx,js,jsx,mjs,cjs}] +indent_size = 2 +quote_type = single + +# JSON +[*.json] +indent_size = 2 + +# YAML +[*.{yml,yaml}] +indent_size = 2 + +# Markdown +[*.md] +trim_trailing_whitespace = false +max_line_length = 100 + +# Makefile +[Makefile] +indent_style = tab + +# Shell scripts +[*.{sh,bash}] +indent_size = 2 + +# Python (if needed) +[*.py] +indent_size = 4 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..45f13316 --- /dev/null +++ b/.env.example @@ -0,0 +1,77 @@ +# CUI Server Configuration +# Copy this file to .env and fill in your values + +# Server Configuration +PORT=3001 +NODE_ENV=development +LOG_LEVEL=info + +# LiveKit Configuration (for voice features) +LIVEKIT_URL=wss://your-livekit-server.com +LIVEKIT_API_KEY=your-livekit-api-key +LIVEKIT_API_SECRET=your-livekit-api-secret + +# Voice Mode Configuration +VOICE_MODE_CMD=voice-mode +VOICE_ROOM_PREFIX=cui +VOICE_DEFAULT_MODE=ptt + +# Podcast Configuration +PODCAST_PROVIDER=podcastfy +PODCASTFY_URL=http://localhost:8123/api/generate +PODCAST_OUTPUT_DIR=public/podcasts +PODCAST_DEFAULT_LANG=en-US + +# Podcast Voice Configuration (English) +PODCAST_EN_VOICE_A=Alloy +PODCAST_EN_VOICE_B=Verse + +# Podcast Voice Configuration (Chinese) +PODCAST_ZH_VOICE_A=zh-male-1 +PODCAST_ZH_VOICE_B=zh-female-1 + +# Dictation Configuration +DICTATION_DEFAULT_PROVIDER=openai-gpt4o-transcribe +# Options: openai-whisper | openai-gpt4o-transcribe | gemini + +# OpenAI Configuration (for dictation) +OPENAI_API_KEY=your-openai-api-key +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_WHISPER_MODEL=whisper-1 +OPENAI_GPT4O_TRANSCRIBE_MODEL=gpt-4o-audio-preview + +# Google AI Configuration (for Gemini dictation - optional) +GOOGLE_AI_API_KEY=your-google-ai-api-key + +# Upload Configuration +MAX_UPLOAD_SIZE=20mb +ALLOWED_AUDIO_FORMATS=audio/webm,audio/mp4,audio/wav,audio/mpeg + +# Security Configuration +ENABLE_CORS=true +CORS_ORIGIN=http://localhost:3001 + +# Development Configuration +ENABLE_HOT_RELOAD=true +ENABLE_SOURCE_MAPS=true + +# Testing Configuration +TEST_TIMEOUT=30000 +TEST_COVERAGE_THRESHOLD=80 + +# MCP Server Configuration (optional) +MCP_SERVER_PORT=3002 +MCP_SERVER_ENABLED=false + +# Optional: External Services +REDIS_URL=redis://localhost:6379 +DATABASE_URL=postgresql://user:password@localhost:5432/cui_db + +# Optional: Monitoring +SENTRY_DSN= +PROMETHEUS_METRICS_ENABLED=false + +# Optional: Feature Flags +FEATURE_PODCAST_ENABLED=true +FEATURE_VOICE_ENABLED=true +FEATURE_DICTATION_ENABLED=true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5c8688cf..ac3799cc 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,8 @@ jspm_packages/ .vscode-test # Git worktrees -cc-worktrees/ \ No newline at end of file +cc-worktrees/ + +# Generated podcast files +public/podcasts/* +!public/podcasts/.gitkeep \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..2ebe8965 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,35 @@ +# Build outputs +dist/ +build/ +coverage/ +.next/ +.cache/ + +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Environment +.env +.env.* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Test +tests/__mocks__/claude + +# Generated files +*.min.js +*.min.css +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Documentation +*.md +LICENSE \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..d0d6658c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,21 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSpacing": true, + "bracketSameLine": false, + "proseWrap": "preserve", + "overrides": [ + { + "files": "*.md", + "options": { + "proseWrap": "always" + } + } + ] +} \ No newline at end of file diff --git a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl new file mode 100644 index 00000000..230a6085 Binary files /dev/null and b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl differ diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..0b32d18b --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,68 @@ +# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) +# * For C, use cpp +# * For JavaScript, use typescript +# Special requirements: +# * csharp: Requires the presence of a .sln file in the project folder. +language: typescript + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed)on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +project_name: "cui" diff --git a/README-voice-podcast.md b/README-voice-podcast.md new file mode 100644 index 00000000..402976bd --- /dev/null +++ b/README-voice-podcast.md @@ -0,0 +1,276 @@ +# CUI Voice & Podcast Features + +## Overview + +This document describes the voice and podcast features added to CUI (Claude Code Web UI). These features enable: + +1. **One-click podcast generation** from task conversations using Podcastfy +2. **Real-time voice conversation** using LiveKit integration with Claude Code +3. **Multi-provider dictation** with switchable backends (OpenAI Whisper, GPT-4o, Gemini) + +## Features + +### 1. Podcast Generation + +Transform any task conversation into a professional dual-host podcast format. + +- **One-click generation**: Click "Generate Podcast" button on any task +- **Dual-host format**: Natural conversation between two AI hosts +- **Language support**: English and Chinese with customizable voices +- **Show notes**: Automatically generated chapters, key points, and next steps +- **Quick generation**: Produces ≤8 minute podcasts in <30 seconds + +### 2. Real-time Voice Conversation + +Engage with Claude Code through natural voice interaction. + +- **LiveKit integration**: WebRTC-based real-time audio streaming +- **Push-to-talk mode**: Default mode for clear communication +- **Low latency**: <1.2s end-to-end response time +- **Room-based**: Each conversation has its own LiveKit room + +### 3. Multi-provider Dictation + +Flexible speech-to-text with switchable providers. + +- **Provider options**: + - OpenAI Whisper: Robust and accurate + - GPT-4o Transcribe: Advanced model with better context understanding + - Gemini: Google's alternative (optional) +- **Browser compatibility**: Supports both WebM and MP4 audio formats +- **Settings integration**: Switch providers from settings page + +## Architecture + +### Technology Stack + +- **Backend**: Express.js (TypeScript) on port 3001 +- **Frontend**: React with Vite +- **Real-time**: LiveKit WebRTC +- **Audio Processing**: Multer for uploads +- **Testing**: Vitest with TDD Guard enforcement + +### API Endpoints + +#### Podcast Generation +``` +POST /api/podcast +Body: { taskId, lang?, voices? } +Response: { audioUrl, showNotes, durationSec } +``` + +#### Voice Conversation +``` +POST /api/voice/start +Body: { taskId } +Response: { room, token, hint } + +POST /api/voice/stop +Response: { ok: true } +``` + +#### Dictation +``` +POST /api/dictation/transcribe?provider= +Body: FormData with audio file +Response: { text, language?, confidence? } +``` + +## Configuration + +### Environment Variables + +Copy `.env.example` to `.env` and configure: + +```env +# LiveKit Configuration +LIVEKIT_URL=wss://your-livekit-server.com +LIVEKIT_API_KEY=your-api-key +LIVEKIT_API_SECRET=your-api-secret + +# Podcast Configuration +PODCASTFY_URL=http://localhost:8123/api/generate +PODCAST_OUTPUT_DIR=public/podcasts + +# Dictation Providers +OPENAI_API_KEY=your-openai-key +GOOGLE_AI_API_KEY=your-google-key (optional) +``` + +### Voice Configuration + +Customize podcast voices per language: + +```env +# English voices +PODCAST_EN_VOICE_A=Alloy +PODCAST_EN_VOICE_B=Verse + +# Chinese voices +PODCAST_ZH_VOICE_A=zh-male-1 +PODCAST_ZH_VOICE_B=zh-female-1 +``` + +## Development + +### Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Configure environment: +```bash +cp .env.example .env +# Edit .env with your API keys +``` + +3. Start development server: +```bash +npm run dev +``` + +### Testing + +The project enforces Test-Driven Development (TDD): + +```bash +# Run all tests +npm test + +# Run unit tests only +npm run unit-tests + +# Run integration tests +npm run integration-tests + +# Watch mode for development +npm run test:watch + +# Coverage report +npm run test:coverage +``` + +### Quality Enforcement + +#### TDD Guard +- Enforces writing tests before implementation +- Integrated with Vitest reporter +- Blocks code changes without failing tests + +#### Pre-commit Hooks +- Code formatting with Prettier +- Linting with ESLint +- Type checking with TypeScript +- Unit test execution + +#### CI Pipeline +- GitHub Actions workflow +- Required checks before merge +- Coverage thresholds enforcement + +## Usage + +### Generate a Podcast + +1. Navigate to any task detail page +2. Click the "Generate Podcast" button +3. Wait for generation (typically <30 seconds) +4. Play the generated podcast directly in the browser +5. Review show notes for chapters and key points + +### Start Voice Conversation + +1. Click the microphone icon on a task +2. Accept microphone permissions +3. Hold spacebar (PTT mode) or toggle for continuous +4. Speak naturally to Claude Code +5. Release to hear the response + +### Use Dictation + +1. Click the dictation input field +2. Select provider from settings if needed +3. Click record and speak +4. Stop recording to transcribe +5. Text appears in the input field + +## Troubleshooting + +### Common Issues + +#### Podcast generation fails +- Check Podcastfy service is running +- Verify PODCASTFY_URL in .env +- Ensure public/podcasts directory exists + +#### Voice mode not connecting +- Verify LiveKit credentials +- Check voice-mode CLI is installed +- Ensure WebRTC ports are open + +#### Dictation not working +- Check API keys are valid +- Verify audio format support +- Check upload size limits + +### Debug Mode + +Enable debug logging: +```bash +LOG_LEVEL=debug npm run dev +``` + +### Performance Tips + +- Use OpenAI Whisper for cost-effective dictation +- Enable caching for repeated podcast generation +- Monitor LiveKit room cleanup +- Set appropriate upload limits + +## Security Considerations + +### API Key Protection +- Never commit .env files +- Use environment-specific keys +- Rotate keys regularly + +### File Upload Security +- Validates audio MIME types +- Enforces 20MB upload limit +- Sanitizes file paths + +### LiveKit Security +- Room-based isolation +- Token-based authentication +- Automatic room cleanup + +## Future Enhancements + +### Planned Features +- Speaker diarization with WhisperX +- Multi-language podcast support +- Voice cloning for personalized hosts +- Podcast RSS feed generation +- Voice command shortcuts + +### Performance Optimizations +- CDN integration for podcasts +- Audio compression pipeline +- LiveKit connection pooling +- Caching layer for transcriptions + +## Contributing + +Please follow the TDD workflow: + +1. Write failing tests first +2. Implement minimal code to pass +3. Refactor with confidence +4. Ensure all quality gates pass +5. Submit PR with comprehensive tests + +## License + +Apache License 2.0 - See LICENSE file for details \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5c28f150..8a145ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "eventsource": "^4.0.0", "express": "^4.18.2", "ignore": "^7.0.5", + "livekit-server-sdk": "^2.13.1", "lucide-react": "^0.536.0", "multer": "^2.0.2", "node-fetch": "^2.7.0", @@ -55,6 +56,7 @@ "cui-server": "dist/server.js" }, "devDependencies": { + "@tailwindcss/oxide": "^4.1.11", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.11", "@testing-library/react": "^16.3.0", @@ -79,9 +81,13 @@ "eslint": "^9.32.0", "happy-dom": "^18.0.1", "husky": "^9.1.7", + "lightningcss": "^1.30.1", "postcss": "^8.5.6", + "prettier": "^3.6.2", "supertest": "^7.1.4", "tailwindcss": "^4.1.11", + "tdd-guard": "^0.9.0", + "tdd-guard-vitest": "^0.1.3", "tsc-alias": "^1.8.16", "tsx": "^4.6.2", "tw-animate-css": "^1.3.6", @@ -1586,6 +1592,12 @@ "node": ">=18" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "dev": true, @@ -1786,21 +1798,6 @@ "version": "0.4.0", "license": "MIT" }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "dev": true, @@ -2179,110 +2176,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-x64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", @@ -2299,50 +2192,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, "node_modules/@img/sharp-linux-x64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", @@ -2365,23 +2214,27 @@ "@img/sharp-libvips-linux-x64": "1.0.4" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": "20 || >=22" } }, "node_modules/@isaacs/cliui": { @@ -2459,6 +2312,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@livekit/protocol": { + "version": "1.39.3", + "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.39.3.tgz", + "integrity": "sha512-hfOnbwPCeZBEvMRdRhU2sr46mjGXavQcrb3BFRfG+Gm0Z7WUSeFdy5WLstXJzEepz17Iwp/lkGwJ4ZgOOYfPuA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.17.1", "license": "MIT", @@ -3777,216 +3639,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", @@ -4015,48 +3667,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "dev": true, @@ -4092,6 +3702,8 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4117,28 +3729,30 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", "cpu": [ - "arm64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "node_modules/@tailwindcss/oxide-linux-x64-musl": { "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4216,6 +3830,8 @@ }, "node_modules/@testing-library/react": { "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", "dependencies": { @@ -4631,6 +4247,8 @@ }, "node_modules/@types/supertest": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", "dev": true, "license": "MIT", "dependencies": { @@ -5832,6 +5450,36 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-9.1.3.tgz", + "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==", + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0", + "map-obj": "5.0.0", + "quick-lru": "^6.1.1", + "type-fest": "^4.3.2" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001731", "dev": true, @@ -7678,18 +7326,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -8873,6 +8509,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -8890,6 +8535,8 @@ }, "node_modules/jsdom": { "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "optional": true, @@ -9147,6 +8794,8 @@ }, "node_modules/lightningcss": { "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -9172,16 +8821,17 @@ "lightningcss-win32-x64-msvc": "1.30.1" } }, - "node_modules/lightningcss-darwin-arm64": { + "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "cpu": [ - "arm64" + "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": ">= 12.0.0" @@ -9191,13 +8841,14 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-linux-x64-gnu": { + "node_modules/lightningcss-linux-x64-musl": { "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -9215,6 +8866,21 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/livekit-server-sdk": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-2.13.1.tgz", + "integrity": "sha512-k4qFvqjHUR0s9lMMueZ1CMDLw/IngOmL/wsh/zq0+6bIg3rMzns9s3ECOf7XuT56esEuu8LGlrw0+inL86QiqQ==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.7.2", + "@livekit/protocol": "^1.39.0", + "camelcase-keys": "^9.0.0", + "jose": "^5.1.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -9349,6 +9015,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/map-obj": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz", + "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -10670,6 +10348,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "dev": true, @@ -10803,6 +10497,18 @@ "version": "4.0.4", "license": "MIT" }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -12155,6 +11861,8 @@ }, "node_modules/supertest": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", "dev": true, "license": "MIT", "dependencies": { @@ -12278,6 +11986,158 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tdd-guard": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/tdd-guard/-/tdd-guard-0.9.0.tgz", + "integrity": "sha512-M2VOIq3ez5EGvjD4j7kpVsiIse3q4oZnbHt82GsQLn1zXZY1lsmlIh2H3BEsLcmtMlb6krgzwA6VKpHe5QWaOA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "reporters/vitest", + "reporters/jest" + ], + "dependencies": { + "@anthropic-ai/sdk": "^0.57.0", + "@types/uuid": "^10.0.0", + "dotenv": "^17.2.1", + "minimatch": "^10.0.3", + "uuid": "^11.1.0", + "zod": "^4.0.14" + }, + "bin": { + "tdd-guard": "dist/cli/tdd-guard.js" + } + }, + "node_modules/tdd-guard-vitest": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/tdd-guard-vitest/-/tdd-guard-vitest-0.1.3.tgz", + "integrity": "sha512-n3FwqVs5zSlMi5tfA9cUl6NMV7woICouSl/eYCGvRJDp/YdI4RN+vHkLeQHXM/UUr9GUKjYwcsDLnsLOCkSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tdd-guard": "^0.8.1" + }, + "peerDependencies": { + "vitest": ">=3.2.4" + } + }, + "node_modules/tdd-guard-vitest/node_modules/@anthropic-ai/sdk": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz", + "integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==", + "dev": true, + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, + "node_modules/tdd-guard-vitest/node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tdd-guard-vitest/node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/tdd-guard-vitest/node_modules/tdd-guard": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/tdd-guard/-/tdd-guard-0.8.1.tgz", + "integrity": "sha512-5s1axCnB9UNmJ6bfAh/mm6UivLSefOy3qsPUZ9qtGAkZZs66gDlfq94x0OdTPaJ8ZaYPsvUaNs0k2iSUYnQL9g==", + "dev": true, + "license": "MIT", + "workspaces": [ + "reporters/vitest", + "reporters/jest" + ], + "dependencies": { + "@anthropic-ai/sdk": "^0.57.0", + "@types/uuid": "^10.0.0", + "dotenv": "^17.2.1", + "uuid": "^11.1.0", + "zod": "^4.0.14" + }, + "bin": { + "tdd-guard": "dist/cli/tdd-guard.js" + } + }, + "node_modules/tdd-guard-vitest/node_modules/zod": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.15.tgz", + "integrity": "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/tdd-guard/node_modules/@anthropic-ai/sdk": { + "version": "0.57.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz", + "integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==", + "dev": true, + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, + "node_modules/tdd-guard/node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tdd-guard/node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/tdd-guard/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tdd-guard/node_modules/zod": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.15.tgz", + "integrity": "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "dev": true, @@ -12638,6 +12498,18 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "license": "MIT", diff --git a/package.json b/package.json index 62a7c3d1..abad521b 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,10 @@ "test:coverage": "NODE_ENV=test vitest run --coverage", "test:ui": "NODE_ENV=test vitest --ui", "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\" \"tests/**/*.{ts,tsx}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\" \"tests/**/*.{ts,tsx}\"", + "format:fix": "npm run format", "typecheck": "tsc --noEmit", "prepare": "husky", "postinstall": "node scripts/postinstall.js" @@ -79,6 +83,7 @@ "eventsource": "^4.0.0", "express": "^4.18.2", "ignore": "^7.0.5", + "livekit-server-sdk": "^2.13.1", "lucide-react": "^0.536.0", "multer": "^2.0.2", "node-fetch": "^2.7.0", @@ -96,6 +101,7 @@ "web-push": "^3.6.7" }, "devDependencies": { + "@tailwindcss/oxide": "^4.1.11", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.11", "@testing-library/react": "^16.3.0", @@ -120,9 +126,13 @@ "eslint": "^9.32.0", "happy-dom": "^18.0.1", "husky": "^9.1.7", + "lightningcss": "^1.30.1", "postcss": "^8.5.6", + "prettier": "^3.6.2", "supertest": "^7.1.4", "tailwindcss": "^4.1.11", + "tdd-guard": "^0.9.0", + "tdd-guard-vitest": "^0.1.3", "tsc-alias": "^1.8.16", "tsx": "^4.6.2", "tw-animate-css": "^1.3.6", diff --git a/podcast_livekit_playbook.md b/podcast_livekit_playbook.md new file mode 100644 index 00000000..d6fd2acd --- /dev/null +++ b/podcast_livekit_playbook.md @@ -0,0 +1,392 @@ +# CUI Voice & Podcast Upgrade — PRD + System Design + Development Playbook + +> **Overview**: This playbook guides the implementation of podcast generation, real-time voice conversation, and dictation features for CUI (Claude Code Web UI), with built-in quality gates via TDD Guard, Git hooks, and CI. + +**Branch**: feat/podcast-livekit +**Architecture**: Express.js (TypeScript) + React (Vite) + Single Port 3001 +**Key Features**: +1. One-click podcast generation from task conversations (Podcastfy) +2. Real-time voice conversation similar to ChatGPT Advanced Voice (LiveKit + voice-mode + Claude Code) +3. Dictation with switchable providers: gemini / openai-whisper / openai-gpt4o-transcribe +4. Quality enforcement: Claude Code Hooks + TDD Guard + pre-commit + CI + +--- + +## CUI Voice & Podcast Upgrade — PRD + System Design + Development Playbook + +(含新增章节:Hooks + TDD Guard + Git 钩子 + CI) + +分支:feat/podcast-voice-dictation(建议) +运行架构:Express.js(Typescript) + React(Vite) + 单端口 3001 +目标功能: +1)任务一键生成播客(Podcastfy) +2)类 ChatGPT Advanced Voice 的实时语音对话(LiveKit + voice-mode + Claude Code) +3)听写(Dictation)可切换提供商:gemini / openai-whisper / openai-gpt4o-transcribe +4)新增:Claude Code Hooks + TDD Guard + pre-commit + CI 以"规范→阻断→验证"强制收敛,防止 agent 跑偏 + +⸻ + +## 1. PRD(产品需求) + +### 1.1 问题与价值 +- CUI 用户需要离屏消费任务进度与结论:将任务会话转为双主持人播客,随时收听。 +- 需要实时语音交互以推进任务、提问进度、下达下一步指令。 +- 需要稳定听写与提供商切换(成本/准确度/延迟可权衡)。 +- 需要把TDD 与安全约束落到硬性流程:Claude Code 每次写改前必须过关,提交前与 CI 阶段再兜底。 + +### 1.2 成功标准(验收) +- 任务详情页出现 Generate Podcast 按钮;30 秒内产出 ≤ 8 分钟 .m4a,iPhone/Safari 可播;生成 show notes(章节/要点/下一步)。 +- Voice Converse:按下后加入 LiveKit 房间,与 Claude Code(经 voice-mode)语音往返,平均端到端延迟 ≤ 1.2s。 +- Dictation Provider 切换:设置页切换后,前端录音上传经 /api/dictation/transcribe?provider=... 返回文本稳定。 +- Hooks + Guard: + - Claude Code 的任何写/改走 PreToolUse → tdd-guard,若无 red→green 证据直接拒绝。 + - pre-commit 阶段自动 format + lint + typecheck + unit test,失败即阻断。 + - CI 对齐本地策略:typecheck + lint + test + build 全绿才允许合并。 + +### 1.3 非目标(MVP 外) +- 语音分离/说话人标注(WhisperX/pyannote)仅作为后续增强。 +- 多租户/跨实例的 voice-mode 池化与任务路由。 +- 生产级 Podcast CDN/转码流水线(MVP 用本地静态目录)。 + +⸻ + +## 2. System Design(系统设计) + +### 2.1 组件与数据流(ASCII) + +``` +[Browser UI] + | ┌───────────── Express (3001) ──────────────┐ + | /api/podcast /api/voice/* /api/dictation/transcribe | + v │ │ +[Task View] -> [Podcast Controller] -> [Podcastfy Adapter] -> [public/podcasts/*.m4a] + | ^ + |<------------- audioUrl + show notes ----------------| + | +[Voice Toggle] -> [Voice Controller] -> spawn voice-mode (MCP) -> Claude Code + | | ^ + | v | + | LiveKit token <----------| + |------------- Join LiveKit room <----> voice-mode audio loopback + +[DictationInput] -- MediaRecorder(audio/webm, mp4) --> /api/dictation/transcribe?provider=... + -> OpenAI Whisper / GPT-4o-transcribe -> text +``` + +### 2.2 模块与目录 + +``` +server/ + routes/ + podcast.ts # POST /api/podcast + voice.ts # POST /api/voice/start|stop + dictation.ts # POST /api/dictation/transcribe + lib/ + config.ts + podcast/ + index.ts # provider 抽象 + podcastfy.ts # Podcastfy 适配 + voice/voiceMode.ts # spawn voice-mode + LiveKit token + dictation/ + index.ts + openaiWhisper.ts + openaiGpt4o.ts + utils/taskSummary.ts # 从任务会话生成「双主持人脚本 + show notes」 + +client/src/ + components/TaskActions/PodcastButton.tsx + components/TaskActions/VoiceToggle.tsx + components/Dictation/DictationInput.tsx + pages/SettingsVoiceAndPodcast.tsx + state/settings.ts # (Zustand) 存 provider/voice 等 +public/podcasts/ +``` + +### 2.3 API 契约(MVP) +- **POST /api/podcast** + - Request: `{ taskId, lang?, voices? }` + - Response: `{ audioUrl, showNotes:[{title,bullets[]}], durationSec? }` +- **POST /api/voice/start** + - Request: `{ taskId }` + - Response: `{ room, token, hint }` +- **POST /api/voice/stop** → `{ ok:true }` +- **POST /api/dictation/transcribe?provider=openai-whisper|openai-gpt4o-transcribe|gemini** + - FormData: audio(webm/mp4) + - Response: `{ text, language?, confidence? }` + +### 2.4 配置(.env.example 摘要) + +```env +PORT=3001 +LIVEKIT_URL=... +LIVEKIT_API_KEY=... +LIVEKIT_API_SECRET=... +VOICE_MODE_CMD=voice-mode +VOICE_ROOM_PREFIX=cui +VOICE_DEFAULT_MODE=ptt + +PODCAST_PROVIDER=podcastfy +PODCASTFY_URL=http://localhost:8123/api/generate +PODCAST_OUTPUT_DIR=public/podcasts +PODCAST_DEFAULT_LANG=en-US +PODCAST_EN_VOICE_A=Alloy +PODCAST_EN_VOICE_B=Verse +PODCAST_ZH_VOICE_A=zh-male-1 +PODCAST_ZH_VOICE_B=zh-female-1 + +DICTATION_DEFAULT_PROVIDER=openai-gpt4o-transcribe +OPENAI_API_KEY=... +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_WHISPER_MODEL=whisper-1 +OPENAI_GPT4O_TRANSCRIBE_MODEL=gpt-4o-transcribe +``` + +⸻ + +## 3. Detailed Development Guide(详细开发指引,Spec→TDD→实现) + +### 3.1 初始化(提交 000) +- 新增/确认脚本:typecheck、lint、test(Vitest + Supertest + RTL)、build +- 引入 Dev 依赖(Vitest/RTL/Supertest/eslint/ts/…) +- .env.example 写全;README-voice-podcast.md 起草 + +### 3.2 功能 A:Podcast(Podcastfy) +1. **先写测试** + - server/routes/podcast.spec.ts:缺 taskId→400;正常→200 且返回 /podcasts/*.m4a;上游报错→500 + - client/PodcastButton.spec.tsx:点击→调用 API→渲染