Imported from ryokun6/ryos (
AGENTS.md). Install upstream withnpx skills add ryokun6/ryos. Copyright stays with the author.
Cloud-specific instructions
ryOS Cloud Environment Guide
Development Environment
This project uses Bun as the package manager and runtime. Local API testing should use the standalone Bun server + Vite proxy; production runs the same standalone Bun server via Docker on Coolify (self-hosted cloud).
Package Manager
- Bun is required (version 1.3.5+)
- Use
bun installto install dependencies - Use
bun run <script>to run package.json scripts
Key Commands
# Development
bun run dev # Start full stack (API + Vite with proxy) — the default
bun run dev:vite # Start Vite dev server only (frontend-only, no API)
bun run dev:api # Start standalone Bun API server only (port 3000)
# Build & Production
bun run build # TypeScript compile + Vite build
# Testing
bun test # Run all tests via bun:test (API tests require server running)
bun run test:unit # Unit/wiring tests only (no server needed)
bun run test:api # API integration tests only
Running the Application
For full functionality (default):
bun run dev
For frontend-only development (no API):
bun run dev:vite
For API server only (e.g. to run tests against):
bun run dev:api
The frontend runs on port 5173 by default. The standalone API defaults to port 3000.
Environment Variables
The following environment variables are required for full functionality:
Required for Core Features
- Redis backend, either:
REDIS_KV_REST_API_URL+REDIS_KV_REST_API_TOKEN- Upstash Redis REST APIREDIS_URL- standard Redis / Valkey connection string (required for local WebSocket pub/sub)
Required for AI Features
- AI provider API keys (at least one):
- OpenAI, Anthropic, or Google AI keys are configured via Vercel AI SDK
Required for Real-time Features
- Pusher mode (
REALTIME_PROVIDER=pusher, default):PUSHER_APP_IDPUSHER_KEYPUSHER_SECRETPUSHER_CLUSTER
- Local WebSocket mode (
REALTIME_PROVIDER=local): requiresREDIS_URL; optionalREALTIME_WS_PATHdefaults to/ws.
Optional Features
RESEND_API_KEY- Resend API key enabling the account-recovery email channel (verify recovery email + email-delivered password-reset codes). Required together withRECOVERY_EMAIL_FROM— both must be set for the email channel to work. When either is unset, email recovery is unavailable and account recovery falls back to a linked Telegram account.RECOVERY_EMAIL_FROM-Fromaddress for recovery emails (e.g.ryOS <noreply@os.ryo.lu>). Required together withRESEND_API_KEYfor the email channel to activate.ELEVENLABS_API_KEY- ElevenLabs API (for text-to-speech)YOUTUBE_API_KEY- YouTube Data API (for video metadata)YOUTUBE_API_KEY_2- YouTube Data API fallback keyOPENAI_API_KEY- OpenAI API (for audio transcription)MAPKIT_TEAM_ID/MAPKIT_KEY_ID/MAPKIT_PRIVATE_KEY/MAPKIT_ORIGIN- Apple MapKit (powers the Maps app + the AI'smapsSearchPlacestool via the Apple Maps Server API)MUSICKIT_TEAM_ID/MUSICKIT_KEY_ID/MUSICKIT_PRIVATE_KEY/MUSICKIT_ORIGIN- MusicKit JS v3 (Apple Music) used by the iPod's "Apple Music" library mode. Reuse the same.p8key as MapKit if both services are enabled on the key — the signer falls back toMAPKIT_TEAM_ID/MAPKIT_KEY_ID/MAPKIT_PRIVATE_KEYwhen theMUSICKIT_*variants are unset.IP_GEOLOCATION_URL_TEMPLATE- Optional override for the IP-geolocation provider (defaults tohttps://ipwho.is/{ip}). Use to switch to a paid provider. Use{ip}as the placeholder.IP_GEOLOCATION_DISABLED- Set to1/trueto disable the IP-geolocation fallback entirely (no outbound calls).UPCITEMDB_USER_KEY- Optional UPCitemdb paid/user key for Stuff product lookup. When set, barcode + keyword search usehttps://api.upcitemdb.com/prod/v1with higher quotas; otherwise the public trial endpoint is used.STUFF_DISABLE_RETAIL_SCRAPE- Set to1/trueto disable Stuff's best-effort Amazon / DuckDuckGo-images / Apple.com HTML scrapes (catalog APIs + Wikipedia + books still run).
Deployment Hardening
These knobs apply to Coolify / Docker / plain-Bun deployments.
TRUSTED_PROXY_COUNT- Number of trusted reverse-proxy hops in front of the API. Defaults to0, meaning client-suppliedX-Forwarded-For/X-Real-IPare NOT trusted (the standalone Bun server's socket peer IP is used instead). Set to1if you have one trusted reverse proxy (nginx, Caddy, Render's edge, Fly.io's edge, etc.) injecting the client IP into the right-most XFF entry; set to2+ for chained proxies. Tests (bun run dev:api) exportTRUSTED_PROXY_COUNT=1because integration tests use spoofed XFF to exercise per-IP rate limits.bun run dev:apialso defaultsTELEGRAM_BOT_API_BASE_URLto the local Telegram mock used by the webhook integration suite.AUTH_COOKIE_SECURE- Force theSecureflag on/off for the auth cookie. Set to1/trueon any HTTPS-fronted self-hosted deployment if auto-detection is wrong. Auto-detection enablesSecurewheneverAPP_PUBLIC_ORIGINstarts withhttps://or the runtime env isproduction.
Localization / Scripts
GOOGLE_GENERATIVE_AI_API_KEY- Google Generative AI (for machine translation of locale files)
Useful i18n maintenance scripts:
bun run i18n:extract --pattern "src/apps/[app]/**/*.{ts,tsx}" # Find hardcoded strings
bun run i18n:sync:mark-todo # Add missing locale keys as [TODO]
bun run i18n:translate # Machine-translate [TODO] keys
bun run i18n:translate:dry-run # Preview machine translations
bun run i18n:sync:dry-run # Validate key coverage
bun run i18n:audit # Validate terminology, placeholders, and plurals
bun run i18n:audit:fix # Safely fix supported terminology drift
bun run i18n:apple-glossary # Refresh Apple terminology source data
bun run i18n:find-untranslated # Heuristic hardcoded-string scan
Note: The application will run with limited functionality without these environment variables. API endpoints requiring these services will fail gracefully.
Using .env.local
The project includes a .env.local file with all required keys pre-configured. Scripts that need API keys (e.g. scripts/machine-translate.ts) do not auto-load .env.local. Export the key before running:
export GOOGLE_GENERATIVE_AI_API_KEY="$(grep GOOGLE_GENERATIVE_AI_API_KEY .env.local | cut -d'"' -f2)"
bun run i18n:translate
Project Structure
api/- Node-style API route handlers served by the standalone Bun serversrc/apps/- Individual application modules (Finder, TextEdit, Chats, etc.)public/- Static assets (fonts, icons, wallpapers, sounds)scripts/- Build and maintenance scripts
Testing
Tests use Bun's native test runner (bun:test). Suites live under tests/unit/<domain>/ (no server) and tests/integration/{api,opt-in}/ (server / env-gated). Shared helpers are in tests/helpers/. See tests/README.md.
Running Tests
# Run all tests (unit + API integration — requires API server running)
bun test
# Run only unit/wiring tests (no server required)
bun run test:unit
bun test tests/unit/chat/test-chat-notification-logic.test.ts tests/unit/realtime/test-pusher-client-refcount.test.ts
# Run a single suite
bun test tests/integration/api/test-admin.test.ts
# Run API integration tests only (requires server)
bun run test:api
Targeted Suite Commands (package.json)
| Command | What it runs |
|---|---|
bun run test |
All tests (bun test) |
bun run test:registration |
Verify API/opt-in test registration |
bun run test:api |
All API integration suites |
bun run test:unit |
All unit/wiring suites |
bun run test:sync-v2:unit |
Sync v2 unit suites |
bun run test:sync-v2 |
Sync v2 unit + API suites |
bun run test:api-validation |
API validation boundary tests |
bun run test:new-api |
Auth, rooms, messages, presence |
bun run test:admin |
Admin endpoint |
bun run test:song |
Songs endpoints |
bun run test:ai |
AI endpoints (chat, applet-ai, ie-generate, ryo-reply) |
bun run test:media |
audio-transcribe, youtube-search |
bun run test:auth-extra |
Auth edge-case suites |
bun run test:auth-ban-lockout |
Auth ban/lockout suites |
bun run test:listen-security |
Listen Together security suites |
bun run test:realtime-auth |
Realtime auth/channel suites |
bun run test:realtime-ws-local |
Opt-in local WebSocket realtime suite |
bun run test:chat-wiring |
All chat wiring suites |
bun run test:pusher-regression |
All Pusher-related suites |
bun run test:chat-regression |
Chat + Pusher regression suites |
API Integration Tests
API integration tests require the standalone API server to be running:
# Terminal 1
bun run dev:api
# Terminal 2
bun test # all tests
bun run test:api # API integration only
bun test tests/integration/api/test-admin.test.ts # single suite
Writing Tests
Tests use describe/test/expect from bun:test. Shared HTTP helpers are in tests/helpers/test-utils.ts:
fetchWithOrigin(url, opts)— addsOrigin: http://localhost:3000fetchWithAuth(url, username, token, opts)— adds Origin + Authorization + X-UsernamemakeRateLimitBypassHeaders()— random IP to avoid rate limits in testsensureUserAuth(username, password)— register-or-login, returns token
Unit suites go in tests/unit/<domain>/ (auto-discovered). API suites go in tests/integration/api/ and must be listed in API_TEST_FILES in scripts/test-groups.ts.
Manual Testing Guidelines
- Skip computer use / GUI-driven testing unless the user explicitly requests it
- When demoing UI changes or visual verification, prefer screenshots over video walkthroughs
- Only create video walkthroughs when the user explicitly asks for a video
- Do not run
bun run buildfor self-verification in local Cursor sessions unless the user explicitly asks. Runningbun run buildis okay when operating on a cloud agent / cloud environment. - For API testing, use
bun run dev(full stack) orbun run dev:api+bun run dev:viteseparately - Only use the
computerUsesubagent for manual browser testing when the user specifically asks for visual verification or UI testing
Important Notes
- Linter warnings: The codebase has pre-existing linter warnings for unused variables. These are not blockers.
- Linting:
bun run lintmay still report pre-existing issues unrelated to your change. Check the current output before treating a lint failure as a regression. - API endpoints: API routes are Node-style handlers under
api/and require Redis for caching/storage. - Build process:
bun run buildwrites Vite output and generated service worker files (sw.js,workbox-*.js) todist/. - Port conflicts: If port 3000 is occupied, set
API_PORT=<port>forbun run dev:apiand adjust proxy target accordingly.
Cursor Cloud specific instructions
These notes are specific to the Cursor Cloud Agent VM (where dependencies are already installed by the startup update script). They complement the guide above; standard commands live in the ## Cloud-specific instructions / README / package.json sections.
- Secrets are pre-injected as environment variables — there is no
.env.localfile on the cloud VM. Redis (Upstash REST:REDIS_KV_REST_API_URL/REDIS_KV_REST_API_TOKEN), Pusher, and AI keys (OpenAI/Anthropic/Google) are already present. The README's.env.localinstructions are for local maintainer machines only. - Redis/realtime on the cloud VM must be Upstash REST + Pusher; do NOT use the production Coolify values. The cloud-agent secret set is configured with
REDIS_PROVIDER=upstash(and noREDIS_URL, noREALTIME_PROVIDER), so a barebun run dev/bun run test:api"just works": Redis resolves to Upstash REST and realtime defaults to Pusher (clusterus3), both reachable and validated. Do not addREDIS_URLor setREALTIME_PROVIDER=localhere — the productionREDIS_URLis an internal Docker host (e.g.redis://t0408...:6379) that does not resolve from this VM (getent hostsfails),getRedisBackend()(api/_utils/redis.ts) prefersREDIS_URLover Upstash so it would break core storage, andlocalrealtime needs that same unreachable host for pub/sub. If storage suddenly fails to connect, check thatREDIS_PROVIDERis stillupstashand that noREDIS_URLleaked back into the env (REDIS_PROVIDER=redis-url/redis/standarddemandsREDIS_URLand will crash the API on boot). - AI chat is rate-limited, which can look like a broken AI.
/api/chat(the Ryo assistant in the Chats app) allows only 3 messages/day for anonymous users (per IP) and 15 per 5h for authenticated users (per username) (seeapi/_utils/_rate-limit.ts). The shared anonymous per-IP budget is quickly exhausted by the API test suite, so if Ryo doesn't reply while logged out you're almost certainly rate-limited (HTTP 429), not missing keys. Sign in (register/login) before testing AI in the UI. bun run test:unitruns every suite in one Bun process, so cross-file pollution is the usual cause of aggregate-only failures. Bun's file execution order differs between machines (it is not the CLI/alphabetical order), so a suite can pass in CI yet fail locally, or vice versa. If an aggregate failure does not reproduce in isolation (bun test ./tests/unit/<domain>/<suite>.test.ts), bisect the actual execution order (thetests/<file>:headers in the log) to find the polluting suite instead of treating it as a regression. Known traps: unregistering a happy-domGlobalRegistratoranother suite registered; Radix UI's layout-effect shim freezing its DOM detection at first import; non-writableIS_REACT_ACT_ENVIRONMENTleft by earlier React suites; leaked store action mocks (restore real setters inafterAll); leakeduseFilesStore/ chats state and debounced IndexedDB writes from earlier suites (settle + reset beforedeleteDatabase, which silently no-ops when blocked). The full API integration suite (bun run dev:api+bun run test:api, with theREDIS_PROVIDER=upstash REALTIME_PROVIDER=pusherprefix noted above) passes ~357/358; the lone occasional failure isiframe-check > raw proxy forwards POST body to upstream, a 5s timeout against an external upstream that passes on rerun (network flake, not a regression).