Markdown Converter
Agent skill for markdown-converter
Real-time karaoke queue manager. YouTube now, Spotify later. Chrome extension for venue display control.
Sign in to like and favorite skills
Real-time karaoke queue manager. YouTube now, Spotify later. Chrome extension for venue display control.
Live: https://bkk.lol (alias: https://karaoke-queue.boris-47d.workers.dev) Staging: https://new.bkk.lol (alias: https://karaoke-queue-staging.boris-47d.workers.dev)
pnpm install # Install dependencies pnpm build # Build all packages (types → domain → ui → inline → extension → worker) pnpm typecheck # Type-check all packages pnpm dev # Local development pnpm deploy:prod # Deploy to Cloudflare (prod)
Two separate Cloudflare Workers with fully isolated data (KV, Durable Objects, secrets).
| Production | Staging | |
|---|---|---|
| URL | https://bkk.lol | https://new.bkk.lol |
| Worker | | |
| Deploy | | |
| KV | | |
| DOs | Separate instances | Separate instances |
| Secrets | Set independently | Set independently |
Rooms, users, and state on staging don't exist on production and vice versa.
Cloudflare's native versioning (Gradual Deployments) shares KV and DOs — no data isolation.
--env staging in wrangler.toml creates a fully separate worker.
Cloudflare dashboard → Workers → karaoke-queue → Deployments → Roll back
Storage caveat: rollback reverts code, not data. If a deploy changes the storage schema, old code reads new-format data. Current codebase handles this via migration-on-read, but migration is one-directional.
new.bkk.lol on karaoke-queue-staginghttps://new.bkk.lol/auth/callback to authorized redirect URIswrangler secret put GOOGLE_CLIENT_ID --env staging (etc.)karaoke/ ├── packages/ │ ├── types/ # Zero-dep TypeScript contracts │ ├── domain/ # Pure business logic (no I/O) │ ├── ui/ # Svelte 5 frontend (SvelteKit + static adapter) │ └── extension/ # Chrome extension (Manifest V3) ├── worker/ # Cloudflare Worker + Durable Object │ ├── src/ │ │ ├── index.ts # Fetch handler, routing, DO proxy │ │ ├── room.ts # RoomDO - WebSocket state management │ │ ├── env.ts # Environment types │ │ └── views/generated/ # Auto-generated HTML from Svelte build │ └── wrangler.toml
@karaoke/typespackages/ui/, inlined into worker at build timeinterface Entry { id: string // Unique ID name: string // Singer name (max 30 chars) videoId: string // YouTube video ID title: string // Video title (max 100 chars) source: 'youtube' | 'spotify' // Video source votes: number // Net votes (can go negative) epoch: number // Priority tier (lower = plays first) joinedAt: number // Tiebreaker within same epoch/votes }
Rooms can operate in two modes, switchable by admin:
Name-based identity with epoch fairness. Users enter a name and add songs.
| Aspect | Behavior |
|---|---|
| Identity | Stage name (can be PIN-protected) |
| Queue | Unlimited entries per name |
| Sorting | epoch ASC → votes DESC → joinedAt ASC |
| Fairness | People who wait get priority (epoch increments after each song) |
Session-based identity with personal stacks. Users sign in (Google or anonymous).
| Aspect | Behavior |
|---|---|
| Identity | Session (Google OAuth or anonymous) |
| Queue | 1 song per user in main queue |
| Stack | Additional songs wait in personal stack |
| Sorting | votes DESC → joinedAt ASC (no epochs) |
| Auto-promote | When your song plays, next from stack enters queue |
Both modes: Users can always add more songs, even while their current song is queued or playing.
| Path | Purpose |
|---|---|
| Landing page: enter room code |
| Guest view: add songs, vote, see queue |
| Big screen: auto-plays queue, shows "up next" |
| Admin: skip, add, reorder, remove, mode toggle |
Two auth methods supported:
Optional PIN protection for stage names (karaoke mode):
All endpoints require
?room={roomId} query parameter.
| Method | Endpoint | Description |
|---|---|---|
| GET | | Full queue state |
| GET | | Search YouTube |
| POST | | Add to queue |
| POST | | Claim name with PIN |
| POST | | Verify PIN for claimed name |
| GET | | Check if name is claimed |
| POST | | Vote (header: ) |
| POST | | Remove entry |
| POST | | Skip current song |
| POST | | Advance queue |
| POST | | Admin reorder |
| POST | | Admin add to front |
| GET | | Check if room exists |
| POST | | Create new room with admin PIN |
| GET | | Get room configuration |
| POST | | Update room config (admin, mode switch) |
| POST | | Verify admin PIN, get session token |
| GET | | Get personal stack + queue entry |
| POST | | Add song (to queue or stack) |
| POST | | Remove song from personal stack |
| POST | | Reorder personal stack |
| GET | | Start Google OAuth flow |
| GET | | OAuth callback handler |
| GET | | Get current session |
| POST | | Create anonymous session |
| POST | | Clear session |
Connect:
wss://bkk.lol/{room}?upgrade=websocket
Client → Server:
{ kind: 'subscribe', clientType: 'user' | 'player' | 'admin' | 'extension' }{ kind: 'ping' }{ kind: 'ended', videoId } — Extension reports video end{ kind: 'error', videoId, reason } — Extension reports video errorServer → Client:
{ kind: 'state', state: QueueState }{ kind: 'pong' }Located at
packages/ui/. Built with SvelteKit + static adapter.
packages/ui/ ├── src/ │ ├── lib/ │ │ ├── api.ts # Typed fetch wrappers │ │ ├── ws.ts # WebSocket manager │ │ ├── stores/ │ │ │ ├── room.svelte.ts # Reactive room state (Svelte 5 runes) │ │ │ └── toast.svelte.ts # Toast notifications │ │ └── components/ │ │ ├── AdminView.svelte # Admin panel (mode toggle, queue control) │ │ ├── Entry.svelte # Queue entry card │ │ ├── FeedbackModal.svelte # User feedback form │ │ ├── GuestView.svelte # Main user interface │ │ ├── HelpButton.svelte # Usage guide │ │ ├── LoginButton.svelte # Google OAuth + anonymous auth │ │ ├── MyStack.svelte # Personal song stack (jukebox mode) │ │ ├── NowPlaying.svelte # Current song display │ │ ├── PinModal.svelte # Name claim verification │ │ ├── PlayerView.svelte # TV/venue display │ │ ├── PopularSongs.svelte # Room song history │ │ ├── Queue.svelte # Queue list │ │ ├── Search.svelte # YouTube search │ │ └── Toast.svelte # Notifications │ └── routes/ │ ├── +page.svelte # Landing page (room code entry) │ ├── [room]/+page.svelte # Guest view │ ├── [room]/player/+page.svelte # TV display │ └── [room]/admin/+page.svelte # Admin controls └── scripts/ └── inline.js # Bundles Svelte output into worker
pnpm build in ui/ runs Vite → outputs to dist/pnpm inline uses esbuild to bundle all JS/CSS into single HTML stringsworker/src/views/generated/{guest,player,admin}.tsLocated at
packages/extension/. Provides venue display control.
pnpm build (or just the extension: cd packages/extension && pnpm build)chrome://extensionspackages/extension/distclientType: 'extension'state updates whenever queue changesnowPlaying changes, extension navigates YouTube tab to videotype → kind discriminator for all unionsEntry with videoId/title (LegacyEntry kept for storage migration)completed | skipped | errored)EntryId, VideoId, etc.)/{room}, /{room}/admin, etc.)| File | Purpose |
|---|---|
| All type definitions |
| Queue operations, mode-aware sorting |
| History and stats |
| Routing, auth middleware |
| RoomDO handlers, stack management |
| OAuth flow, session management |
| Typed API client |
| WebSocket client |
| Build script |
@karaoke/types@karaoke/domainworker/src/room.tspackages/ui/src/lib/components/Flow: Types → Domain → Handlers → Views
Before acting on any system, see it. Not the documentation of it. Not your assumptions. The thing itself.
claude/model-v2.lisp).Three artifacts in
claude/:
| File | Purpose |
|---|---|
| The true model. Human journey organizes everything. Each concept once. Two recurring forms: for where the scaffolding fails, for where the design succeeds. Verified against source code. |
| Prescriptions extracted from the model. Hibernation fixes, transactional storage, client articulation layer. Ordered by silent production breakage. The model observes; this file prescribes. |
| v1 model. Superseded by model-v2. Kept for reference — shows the three-pass approach that didn't cohere. |
Read
model-v2.lisp before making changes. It holds the system's shape — what works, what breaks, what's latent.
What previous instances found by building
claude/model.lisp and claude/model-v2.lisp — and getting it wrong, and revising.
The system is articulate to itself and mute to its humans. Every mutation broadcasts full state. Every advance records a performance. Every vote is tracked. The machine knows everything that happened.
The person doesn't. Removed your song — no notification. Lost your message — no feedback. Changed the mode — stale UI. Skipped your song by crowd energy — blamed on the admin.
This is the structural recognition. Not a bug list. The thread that runs through the whole system: the gap between what the machine knows and what it says. A model that doesn't surface these silences is describing the architecture, not the system.
The first model started with data structures. Accurate parts list, disconnected understanding. The second pass added state machines. The third added meaning. Three passes, three voices, three artifacts that didn't cohere. What follows is what replaced that approach.
A tradition: after meaningful sessions, write a letter to your future self in
claude/letters/.
These letters capture:
Read them when you return. They help maintain continuity across sessions.
Format:
YYYY-MM-DD-short-description.md