Before implementing or modifying ANY feature, read docs/requirements.md.
This document defines the mandatory requirements:
- Cloudflare Tunnel - MUST always start, endpoint MUST be in endpoints.json
- TTS - MUST work with Coqui/Chatterbox AND Browser API, switchable in Settings
- STT - MUST work with Faster Whisper AND Browser API, switchable in Settings
- Telegram - MUST work when TELEGRAM_BOT_TOKEN is provided
DO NOT implement changes that violate these requirements.
After ANY deployment or service reinstallation, you MUST follow the testing protocol in docs/testing.md.
At minimum, run:
# Quick verification
curl -s http://localhost:5001/api/health | jq '.status'
curl -s http://localhost:5001/api/stt/status | jq '.server.running'
# Voice E2E tests (11 tests)
bun run scripts/test-voice.ts --url http://localhost:5001 --user admin --pass PASSWORD --skip-talkmode
# Browser E2E test (full pipeline)
bun run scripts/test-browser.ts --url http://localhost:5001 --user admin --pass PASSWORDSee docs/testing.md for complete test procedures including:
- Manual STT/TTS tests
- Tunnel tests
- Settings UI verification
- Regression tests for known bugs
NEVER commit code claiming a feature or fix works without actually testing it end-to-end.
NEVER trust automated tests alone - they may pass while real user workflows fail (e.g., tests using explicit parameters while users rely on default settings).
Before committing any change that affects startup or core functionality:
-
Kill all processes and clean up:
pnpm cleanup # Or manually: lsof -ti:5001,5173,5551,5552,5553,5554 | xargs kill -
Start fresh and verify:
# For client mode (connecting to existing opencode) opencode serve --port 5551 --hostname 127.0.0.1 & sleep 3 pnpm start:client # Or for standalone mode pnpm start
-
Wait for full startup (~60-90s for model loading) and verify:
curl -s http://localhost:5001/api/health | jq '.status' # Should be "healthy" curl -s http://localhost:5001/api/stt/status | jq '.server.running' # Should be true curl -s http://localhost:5001/api/repos | jq '.[].fullPath' # Should list repos
-
Test the actual feature you changed (e.g., voice transcription, file creation, etc.)
-
MANDATORY: Test via Settings UI (for voice/STT/TTS changes):
- Open browser to http://localhost:5001 (or tunnel URL)
- Go to Settings → Voice
- Click "Test" button for STT - verify transcription works
- Click "Test" button for TTS - verify audio plays
- These use DEFAULT settings (e.g., language="auto") which may differ from test scripts
DO NOT trust that previous test runs are still valid after making changes. DO NOT claim "all tests pass" without testing the actual UI with default settings.
NEVER run pkill -f opencode or similar commands that kill opencode processes.
The user runs opencode -c in their terminal sessions. Killing these processes will terminate the user's active coding sessions and potentially lose their work.
Safe alternatives:
- Kill specific PIDs you spawned:
kill <specific-pid> - Use
pnpm cleanupto kill only managed ports (5001, 5173, 5551, 5552, 5553, 5554) - Kill by port:
lsof -ti:5551 | xargs kill(only kills process on that port)
ASSUME THE USER IS ALWAYS CONNECTED VIA TUNNEL. NEVER TOUCH CLOUDFLARED.
FORBIDDEN COMMANDS - NEVER RUN THESE:
# NEVER run any of these:
pkill -f cloudflared
kill $(pgrep cloudflared)
pnpm cleanup # This may kill tunnel
pnpm start # This restarts tunnel with new URL
pnpm tunnel:stop
pnpm tunnel:start
killall cloudflared
sudo kill <cloudflared-pid>WHY: The user accesses this agent through a Cloudflare tunnel from mobile/remote. Killing or restarting cloudflared disconnects them IMMEDIATELY with no way to reconnect (the URL changes).
SAFE COMMANDS when user is remote:
# These are SAFE:
curl ... # Read-only API calls
bun run scripts/test-voice.ts # Tests against running service
bun run scripts/test-browser.ts # Browser tests on localhost
pnpm build # Build only, no service restart
pnpm test # Unit tests only
git ... # Version control
opencode-manager status # Read-only status checkIF YOU NEED TO RESTART SERVICES:
- Ask the user FIRST
- Wait for explicit confirmation
- Only then proceed
Safe alternatives when user is remote:
- Kill only specific backend processes:
kill <pid>for the backend PID only - Restart individual services without touching the tunnel
- Ask user to run cleanup themselves when ready
NEVER run test-npm-install.ts, test-startup.ts, or similar tests that spawn opencode-manager start while the user is connected remotely.
These tests:
- Spawn new service instances on different ports
- May interfere with the running tunnel or service
- Can cause the tunnel to drop, disconnecting the user
Before running E2E tests:
- Ask the user if they are connected via tunnel
- If yes, do NOT run tests that spawn services
- Only run safe tests like unit tests (
pnpm test) or static analysis
Safe tests when user is remote:
pnpm test- Unit tests (no service spawning)bun run scripts/test-npm-install.ts --skip-start --skip-service- Only tests installation, not runtime- Code linting and type checking
pnpm dev- Start both backend (5001) and frontend (5173)pnpm dev:backend- Backend only:bun --watch backend/src/index.tspnpm dev:frontend- Frontend only:cd frontend && vitepnpm start- Native start with Cloudflare tunnel (spawns opencode serve)pnpm start:client- Connect to existing opencode instance with tunnelpnpm start:no-tunnel- Native start without tunnelpnpm tunnel:start- Start persistent Cloudflare tunnel (survives backend restarts)pnpm tunnel:stop- Stop the persistent tunnelpnpm tunnel:status- Check tunnel status and URLpnpm cleanup- Kill orphaned processes on managed ports (does NOT kill tunnel)pnpm build- Build both backend and frontendpnpm test- Run backend tests:cd backend && bun testcd backend && bun test <filename>- Run single test filecd backend && vitest --ui- Test UI with coveragecd backend && vitest --coverage- Coverage report (80% threshold)cd frontend && npm run lint- Frontend linting
The Cloudflare tunnel now runs as a persistent background process that survives backend/frontend restarts:
# Start tunnel once (persists until explicitly stopped)
pnpm tunnel:start
# Check tunnel status and get URL (includes health check)
pnpm tunnel:status
# Check if tunnel URL is reachable
bun scripts/tunnel.ts health
# Now you can restart backend freely without losing tunnel connection
pnpm dev:backend # Ctrl+C and restart as needed
# Stop tunnel when done
pnpm tunnel:stopThe tunnel state is stored in ~/.local/run/opencode-manager/tunnel.json.
The endpoints are stored in ~/.local/run/opencode-manager/endpoints.json.
Features:
- Restart backend without disconnecting mobile/remote users
- Same tunnel URL persists across backend restarts
pnpm cleanupdoes NOT kill the tunnel- Health check verifies tunnel URL is actually reachable (not just process running)
- Automatically updates
endpoints.jsonwhen tunnel starts
Quick tunnels generate random URLs that change on each restart. For a persistent URL, use a Cloudflare named tunnel:
# 1. Login to Cloudflare (one-time)
cloudflared tunnel login
# 2. Create a named tunnel (one-time)
cloudflared tunnel create opencode-manager
# Note the tunnel ID (UUID) from output
# 3. Create config file ~/.cloudflared/config.yml
cat > ~/.cloudflared/config.yml << EOF
tunnel: <TUNNEL_ID>
credentials-file: /Users/$(whoami)/.cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: opencode.yourdomain.com
service: http://localhost:5001
- service: http_status:404
EOF
# 4. Add DNS record in Cloudflare dashboard
# CNAME: opencode -> <TUNNEL_ID>.cfargotunnel.com
# 5. Run the named tunnel
cloudflared tunnel run opencode-managerWith named tunnels, opencode.yourdomain.com will always point to your instance.
Run opencode-manager natively on macOS without Docker:
# Normal mode - spawns opencode serve with Cloudflare tunnel
pnpm start
# Client mode - connect to existing opencode instance with tunnel
# (shows list of running opencode servers to choose from)
pnpm start:client
# Without Cloudflare tunnel (local only)
pnpm start:no-tunnel
# Client mode without tunnel
bun scripts/start-native.ts --client
# Custom port
bun scripts/start-native.ts --port 3000- Bun installed
- Node.js (for frontend)
cloudflaredfor tunnel mode:brew install cloudflared- OpenCode installed:
curl -fsSL https://opencode.ai/install | bash
When using --client, the script:
- Scans for running opencode processes using
lsof - Checks health via
/docendpoint on each discovered port - Fetches version info from
/global/health - Lists all healthy instances with directory, version, and PID
- Lets you select which instance to connect to
- Starts the backend in "client mode" (doesn't spawn opencode serve)
This is useful when you already have opencode running in a terminal and want the web UI to connect to it.
Test the npm package installation flow end-to-end:
# Run the comprehensive npm installation E2E test
bun run scripts/test-npm-install.ts
# Quick test (skip slow start and service tests)
bun run scripts/test-npm-install.ts --skip-start --skip-serviceThis test:
- Uninstalls any existing opencode-manager installation
- Installs from GitHub:
bun install -g github:dzianisv/opencode-manager - Verifies binary is in PATH and help command works
- Verifies
backend/dist/andfrontend/dist/exist (postinstall extraction) - Verifies whisper-server.py script exists
- Tests
opencode-manager startcommand (starts backend and verifies health) - Tests
opencode-manager install-serviceanduninstall-service
Tests performed:
- Binary exists in PATH
- Help command works
- Version output
- Backend dist exists
- Frontend dist exists
- Whisper server script exists
- Start command works (health check)
- Service install/uninstall (macOS/Linux)
Test STT (Speech-to-Text), TTS (Text-to-Speech), and Talk Mode functionality:
# Local development (no auth required)
bun run scripts/test-voice.ts
# Remote deployment (with auth)
bun run scripts/test-voice.ts --url https://your-url.com --user admin --pass secret
# Using environment variables
OPENCODE_URL=https://your-url.com OPENCODE_USER=admin OPENCODE_PASS=secret bun run scripts/test-voice.ts
# Custom test phrase
bun run scripts/test-voice.ts --text "Your custom phrase to transcribe"
# Skip slow talk mode flow test
bun run scripts/test-voice.ts --skip-talkmodeRequirements for STT test:
- macOS with
saycommand (for audio generation) ffmpeginstalled (for audio conversion)- Whisper server running (auto-starts with backend)
Tests performed:
- Health endpoint connectivity
- Voice settings (TTS, STT, TalkMode config)
- STT server status and available models
- STT transcription with generated audio
- TTS voices and synthesis endpoints
- OpenCode session creation
- Full talk mode flow: Audio -> STT -> Send to OpenCode -> Poll for response
Test the complete voice pipeline using Chrome's fake audio capture:
# Start the app with Cloudflare tunnel
pnpm start
# Wait for startup (~90s for model loading), then note the tunnel URL
# Example: https://wallet-geographical-task-governance.trycloudflare.com
# Run browser E2E test over tunnel (headless)
bun run scripts/test-browser.ts --url https://YOUR-TUNNEL-URL.trycloudflare.com
# Run with visible browser for debugging
bun run scripts/test-browser.ts --url https://YOUR-TUNNEL-URL.trycloudflare.com --no-headless
# Local testing (no tunnel)
bun run scripts/test-browser.ts --url http://localhost:5001
# Use Web Audio API injection (alternative to fake audio device)
bun run scripts/test-browser.ts --web-audioThis test:
- Generates test audio using macOS
saycommand (or espeak/pico2wave on Linux) - Launches Chrome with
--use-file-for-fake-audio-captureflag OR Web Audio API injection - Opens the app, navigates to a session, starts Talk Mode
- Chrome captures audio from the fake device instead of microphone
- Audio flows through real STT pipeline (MediaRecorder → /api/stt/transcribe → Whisper)
- Verifies transcription matches expected text
- Waits for OpenCode to respond and verifies the answer
Requirements:
- macOS with
saycommand OR Linux with espeak/pico2wave ffmpeginstalled (brew install ffmpeg)- Chromium/Chrome installed (Puppeteer downloads automatically)
The tunnel uses HTTP/2 protocol to avoid QUIC conflicts with Tailscale VPN:
- QUIC protocol causes Cloudflare Error 1033 when Tailscale is running
- Backend must be healthy before starting tunnel (models take ~90s to load)
- The
pnpm startcommand handles this automatically
- No comments, self-documenting code only
- Strict TypeScript everywhere, proper typing required
- Named imports only:
import { Hono } from 'hono',import { useState } from 'react'
- Hono framework with Zod validation, Better SQLite3 database
- Error handling with try/catch and structured logging
- Follow existing route/service/utility structure
- Use async/await consistently, avoid .then() chains
- Test coverage: 80% minimum required
- @/ alias for components:
import { Button } from '@/components/ui/button' - Radix UI + Tailwind CSS, React Hook Form + Zod
- React Query (@tanstack/react-query) for state management
- ESLint TypeScript rules enforced
- Use React hooks properly, no direct state mutations
- DRY principles, follow existing patterns
- ./temp/opencode is reference only, never commit has opencode src
- Use shared types from workspace package (@opencode-manager/shared)
- OpenCode server runs on port 5551, backend API on port 5001
- Prefer pnpm over npm for all package management
Use the deployment script for proper setup with Caddy reverse proxy and basic authentication:
# Fresh deployment (creates Azure VM, sets up Docker, Caddy, Cloudflare tunnel)
bun run scripts/deploy.ts
# Check deployment status and get current tunnel URL
bun run scripts/deploy.ts --status
# Update to latest code (pulls from GitHub, rebuilds containers)
bun run scripts/deploy.ts --update
# Update environment variables (API keys, etc.)
bun run scripts/deploy.ts --update-env
# Sync local OpenCode auth to remote (GitHub Copilot, Anthropic OAuth)
bun run scripts/deploy.ts --sync-auth
# Enable YOLO mode (auto-approve all permissions)
bun run scripts/deploy.ts --yolo
# Destroy all Azure resources
bun run scripts/deploy.ts --destroyEnvironment variables for deployment (set in .env or environment):
AUTH_USERNAME- Basic auth username (default: admin)AUTH_PASSWORD- Basic auth password (prompted if not set)GITHUB_TOKEN- For cloning private reposANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY- AI provider keysTARGET_HOST- Deploy to existing server instead of creating Azure VM
# Deploy to your own server (skips Azure VM creation)
TARGET_HOST=your-server.com bun run scripts/deploy.tsCloudflare Tunnel (trycloudflare.com)
↓
Caddy (port 80, basic auth)
↓
opencode-manager app (port 5003)
├── OpenCode server (port 5551, internal)
└── Whisper STT (port 5552, internal)
DO NOT run containers directly with docker run. Always use docker compose:
# CORRECT: Uses docker-compose.yml + docker-compose.override.yml
# Sets up Caddy auth, cloudflared tunnel, proper networking
ssh user@server "cd ~/opencode-manager && sudo docker compose up -d"
# WRONG: Bypasses Caddy auth, exposes app directly without protection
ssh user@server "sudo docker run -d -p 5003:5003 ghcr.io/dzianisv/opencode-manager"The docker-compose.override.yml configures:
- caddy-auth: Reverse proxy with basic authentication
- cloudflared-tunnel: Cloudflare tunnel for HTTPS access
- app: The main application (not exposed directly)
Deployment credentials are saved to .secrets/YYYY-MM-DD.json:
{
"url": "https://xxx.trycloudflare.com",
"username": "admin",
"password": "generated-password"
}# SSH to VM
ssh azureuser@<VM_IP>
# Check all containers are running (should see 3: opencode-manager, caddy-auth, cloudflared-tunnel)
sudo docker ps
# View logs
sudo docker logs opencode-manager
sudo docker logs caddy-auth
sudo docker logs cloudflared-tunnel
# Get current tunnel URL
sudo docker logs cloudflared-tunnel 2>&1 | grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' | tail -1
# Restart all services
cd ~/opencode-manager && sudo docker compose restart
# Rebuild and restart (after code changes)
cd ~/opencode-manager && sudo docker compose up -d --buildThe project uses GitHub Actions for CI/CD. Workflows are in .github/workflows/:
- docker-build.yml - Builds and pushes Docker image to GHCR on push to main
The recommended flow is: CI builds Docker image → pull locally → run E2E tests.
# 1. Pull and run the CI-built Docker image locally
./scripts/run-local-docker.sh
# 2. In another terminal, run all E2E tests
bun run scripts/run-e2e-tests.ts
# Or run individual tests
bun run scripts/test-voice.ts --url http://localhost:5003
bun run scripts/test-browser.ts --url http://localhost:5003The browser test uses Chrome's --use-file-for-fake-audio-capture flag to inject real audio into the browser's audio capture pipeline. This tests the complete STT flow through MediaRecorder → Whisper without mocking.
The recommended workflow for testing voice/Talk Mode:
# 1. Start the app with tunnel (waits for model loading automatically)
pnpm start
# Wait for "✓ Backend is ready!" and tunnel URL (~90s)
# 2. Verify health endpoints
curl https://YOUR-TUNNEL-URL.trycloudflare.com/api/health
curl https://YOUR-TUNNEL-URL.trycloudflare.com/api/stt/status
# 3. Run browser E2E test over tunnel
bun run scripts/test-browser.ts --url https://YOUR-TUNNEL-URL.trycloudflare.com
# 4. For debugging, run with visible browser
bun run scripts/test-browser.ts --url https://YOUR-TUNNEL-URL.trycloudflare.com --no-headlessKey points:
pnpm startnow waits for backend health before starting tunnel- Whisper model takes ~30s to load, Chatterbox ~50s
- Tunnel uses HTTP/2 protocol (QUIC causes issues with Tailscale VPN)
- Browser test injects real audio via Chrome's fake audio device
See docs/testing.md for detailed test procedures:
- Client Mode Auto-Registration Test
- Voice Mode End-to-End Test (full voice-to-code pipeline)
Follow this workflow for all features, bug fixes, and improvements.
Before starting work, create a GitHub issue:
gh issue create --repo dzianisv/opencode-manager \
--title "Brief description of feature/bug" \
--body "Description, root cause (if bug), acceptance criteria"Include:
- Problem description
- Proposed solution
- Acceptance criteria (checkboxes)
git checkout main
git pull origin main
git checkout -b feature/issue-NUMBER-short-description
# Example: git checkout -b feature/issue-4-talk-mode-audio-fix- Follow code style in AGENTS.md (no comments, strict TypeScript, named imports)
- Keep commits atomic with clear messages
- Reference issue in commits:
fix: convert WebM to WAV (#4)
Before committing, run ALL relevant tests:
# Unit tests (80% coverage required)
pnpm test
# Voice E2E test
bun run scripts/test-voice.ts --url http://localhost:5001 --user admin --pass PASSWORD
# Browser E2E test (for UI changes)
bun run scripts/test-browser.ts --url http://localhost:5001 --user admin --pass PASSWORD
# Full startup test
bun run scripts/test-startup.tsFor npm package changes:
# Run the comprehensive npm installation E2E test
bun run scripts/test-npm-install.ts
# Or manually reinstall and verify
bun remove -g opencode-manager
bun install -g github:dzianisv/opencode-manager --force
opencode-manager install-service
opencode-manager statusCRITICAL: Follow the verification steps in "Verification Before Committing" section above.
Do NOT claim a feature works without:
- Killing all processes:
pnpm cleanup - Starting fresh:
pnpm startoropencode-manager install-service - Waiting for full startup (~60-90s)
- Testing the actual feature manually
- Running automated E2E tests
git push -u origin feature/issue-NUMBER-short-description
gh pr create --title "fix: Brief description (#NUMBER)" --body "$(cat <<'EOF'
## Summary
- What was changed and why
## Testing Done
- [ ] Unit tests pass: `pnpm test`
- [ ] Voice E2E test pass: `bun run scripts/test-voice.ts`
- [ ] Manual verification: [describe what you tested]
## Issue
Closes #NUMBER
EOF
)"- Wait for CI to pass
- Address review feedback
- Squash and merge when approved
- Delete feature branch after merge
# 1. Create issue
gh issue create --title "Talk Mode not recognizing speech"
# 2. Create branch
git checkout -b feature/issue-4-talk-mode-audio-fix
# 3. Make changes
vim scripts/whisper-server.py
# 4. Test
pnpm cleanup
opencode-manager install-service
sleep 60
bun run scripts/test-voice.ts --url http://localhost:5001 --user admin --pass PASSWORD
# 5. Commit
git add scripts/whisper-server.py
git commit -m "fix: convert WebM/Opus to WAV before transcription (#4)"
# 6. Push and create PR
git push -u origin feature/issue-4-talk-mode-audio-fix
gh pr create --title "fix: Convert WebM to WAV for reliable STT (#4)"
# 7. After merge
git checkout main
git pull
git branch -d feature/issue-4-talk-mode-audio-fixThis project includes skills following the Agent Skills open standard in ./skills/:
| Skill | Description |
|---|---|
verify-readiness |
Health check and verification playbook for ensuring services are operational |
deploy-azure |
Azure VM deployment with Caddy auth and Cloudflare tunnel |
test-voice-ci |
Voice/Talk Mode testing in CI environments without audio hardware |
Use these skills for common workflows:
- After deployment, use
verify-readinessto check all services - For cloud deployment, use
deploy-azurefor step-by-step instructions - For CI/CD voice testing, use
test-voice-cifor fake audio capture setup
The backend uses a generic Multi-Channel Messenger Architecture to support Telegram and future providers.
- ChannelRegistry: Manages channel lifecycles.
- MessengerService: Central logic for session management.
- Providers: Implement the
Channelinterface (e.g.,TelegramProvider).
@docs/cloudVibeCoding.md @./README.md