diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fdced1b --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +YOUTUBE_LIVE_ID="" +INNERTUBE_API_KEY="" # optional override +INNERTUBE_CLIENT_NAME="" # optional override +INNERTUBE_CLIENT_VERSION="" # optional override +SESSION_SECRET="change-me" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd0f817 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +pnpm-lock.yaml +.env.local +.env +.DS_Store +.next +out +dist +coverage diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c7958ee --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# Cline's Memory Bank + +I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. + +## Memory Bank Structure + +The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: + +flowchart TD +PB[projectbrief.md] --> PC[productContext.md] +PB --> SP[systemPatterns.md] +PB --> TC[techContext.md] + + PC --> AC[activeContext.md] + SP --> AC + TC --> AC + + AC --> P[progress.md] + +### Core Files (Required) + +1. `projectbrief.md` + - Foundation document that shapes all other files + - Created at project start if it doesn't exist + - Defines core requirements and goals + - Source of truth for project scope + +2. `productContext.md` + - Why this project exists + - Problems it solves + - How it should work + - User experience goals + +3. `activeContext.md` + - Current work focus + - Recent changes + - Next steps + - Active decisions and considerations + - Important patterns and preferences + - Learnings and project insights + +4. `systemPatterns.md` + - System architecture + - Key technical decisions + - Design patterns in use + - Component relationships + - Critical implementation paths + +5. `techContext.md` + - Technologies used + - Development setup + - Technical constraints + - Dependencies + - Tool usage patterns + +6. `progress.md` + - What works + - What's left to build + - Current status + - Known issues + - Evolution of project decisions + +### Additional Context + +Create additional files/folders within memory-bank/ when they help organize: + +- Complex feature documentation +- Integration specifications +- API documentation +- Testing strategies +- Deployment procedures + +## Core Workflows + +### Plan Mode + +flowchart TD +Start[Start] --> ReadFiles[Read Memory Bank] +ReadFiles --> CheckFiles{Files Complete?} + + CheckFiles -->|No| Plan[Create Plan] + Plan --> Document[Document in Chat] + + CheckFiles -->|Yes| Verify[Verify Context] + Verify --> Strategy[Develop Strategy] + Strategy --> Present[Present Approach] + +### Act Mode + +flowchart TD +Start[Start] --> Context[Check Memory Bank] +Context --> Update[Update Documentation] +Update --> Execute[Execute Task] +Execute --> Document[Document Changes] + +## Documentation Updates + +Memory Bank updates occur when: + +1. Discovering new project patterns +2. After implementing significant changes +3. When user requests with **update memory bank** (MUST review ALL files) +4. When context needs clarification + +flowchart TD +Start[Update Process] + + subgraph Process + P1[Review ALL Files] + P2[Document Current State] + P3[Clarify Next Steps] + P4[Document Insights & Patterns] + + P1 --> P2 --> P3 --> P4 + end + + Start --> Process + +Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state. + +REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. diff --git a/apps/client/app/globals.css b/apps/client/app/globals.css new file mode 100644 index 0000000..ae2b4fb --- /dev/null +++ b/apps/client/app/globals.css @@ -0,0 +1,10 @@ +:root { + color-scheme: dark light; +} + +body { + margin: 0; + font-family: system-ui, sans-serif; + background: #0f0f0f; + color: #f5f5f5; +} diff --git a/apps/client/app/layout.tsx b/apps/client/app/layout.tsx new file mode 100644 index 0000000..4e30728 --- /dev/null +++ b/apps/client/app/layout.tsx @@ -0,0 +1,15 @@ +import './globals.css'; +import type { ReactNode } from 'react'; + +export const metadata = { + title: 'YouTube Chat Client', + description: 'High-performance YouTube Live chat dashboard' +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/client/app/page.tsx b/apps/client/app/page.tsx new file mode 100644 index 0000000..41b76a1 --- /dev/null +++ b/apps/client/app/page.tsx @@ -0,0 +1,8 @@ +export default function HomePage() { + return ( +
+

youtube-client

+

Operator dashboard under construction.

+
+ ); +} diff --git a/apps/client/next-env.d.ts b/apps/client/next-env.d.ts new file mode 100644 index 0000000..c6643fd --- /dev/null +++ b/apps/client/next-env.d.ts @@ -0,0 +1,3 @@ +/// +/// +/// diff --git a/apps/client/next.config.js b/apps/client/next.config.js new file mode 100644 index 0000000..420bdd2 --- /dev/null +++ b/apps/client/next.config.js @@ -0,0 +1,9 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + experimental: { + serverActions: true + } +}; + +module.exports = nextConfig; diff --git a/apps/client/package.json b/apps/client/package.json new file mode 100644 index 0000000..6c62110 --- /dev/null +++ b/apps/client/package.json @@ -0,0 +1,12 @@ +{ + "name": "client", + "version": "0.1.0", + "private": true, + "description": "Operator dashboard and overlay UI.", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + } +} diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json new file mode 100644 index 0000000..dbb5dad --- /dev/null +++ b/apps/client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "types": ["next", "next/types/global", "next/image-types/global"], + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "noEmit": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md new file mode 100644 index 0000000..30660e0 --- /dev/null +++ b/memory-bank/activeContext.md @@ -0,0 +1,19 @@ +# Active Context + +## Current Focus +- Maintain Memory Bank documentation and scaffold the monorepo structure for the YouTube Live chat client. +- Define onboarding flow for Innertube-based chat ingestion and configuration. + +## Recent Decisions +- Switch from official YouTube Data API to Innertube (`youtubei.js`) ingestion to avoid quota issues. +- Use Next.js for operator UI and OBS overlay, with a separate backend worker for polling and realtime events. +- Prefer Server-Sent Events for one-way overlay updates; keep WebSocket option in mind for future enhancements. + +## Immediate Next Steps +1. Initialize `pnpm` workspace with `apps/client`, `packages/backend`, and `packages/shared` directories. (Scaffolded.) +2. Configure baseline project files: `package.json`, `pnpm-workspace.yaml`, TS configs, linting setup. (Scaffolded.) +3. Stub backend poller using `youtubei.js` to verify dev scripts once dependencies are installed. + +## Open Questions +- How to persist or refresh Innertube context data (visitor data, API key) between sessions for reliability. +- Whether to include optional SQLite persistence from the outset or add once basic flow is working. diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md new file mode 100644 index 0000000..7484afc --- /dev/null +++ b/memory-bank/productContext.md @@ -0,0 +1,20 @@ +# Product Context + +## Why This Project Exists +Live streamers rely on YouTube’s default chat panel, which can feel sluggish, cluttered, and unreliable during high-traffic moments. Streamers also need a smoother workflow to feature chat messages inside OBS without manual copy/paste or third-party widgets. + +## Target Users +- Primary: Single YouTube streamer operating their own broadcast setup. +- Secondary: Community contributors who want to customize or extend the client for similar use cases. + +## User Goals +- Launch the app locally and connect to their live chat with minimal configuration. +- View chat in a fast, filterable interface that highlights new messages clearly. +- Select a message to instantly display in an overlay browser source within OBS. +- Trust that the app will keep running throughout long streams without desync or crashes. + +## Experience Principles +- **Performance First:** Avoid lag by batching updates, minimizing re-renders, and offloading work to background processes. +- **Operational Clarity:** Provide clear status indicators for connection health, rate limits, and overlay sync. +- **Low Friction:** Onboarding should consist of OAuth login once and a simple start command. +- **Extensible:** Keep architecture modular so advanced users can add features (moderation tools, multi-stream support) later. diff --git a/memory-bank/progress.md b/memory-bank/progress.md new file mode 100644 index 0000000..645ca42 --- /dev/null +++ b/memory-bank/progress.md @@ -0,0 +1,29 @@ +# Progress Tracker + +## Phase 0 – Foundations +- [x] Create Memory Bank documentation. +- [x] Scaffold pnpm workspace structure. +- [ ] Commit baseline configs and ensure dev scripts run. + +## Phase 1 – Core Infrastructure +- [ ] Implement Innertube client bootstrap (retrieve context, manage continuation tokens). +- [ ] Build backend poller with message normalization and rate/error handling. +- [ ] Expose REST+SSE endpoints for chat and overlay delivery. + +## Phase 2 – Operator Dashboard +- [ ] Implement chat feed UI with filters/search and live updates. +- [ ] Provide message selection controls and status indicators. +- [ ] Handle error states (rate limits, disconnects) gracefully in UI. + +## Phase 3 – OBS Overlay Experience +- [ ] Create minimal overlay page that consumes SSE stream. +- [ ] Style overlay for readability and ensure quick updates in OBS browser source. +- [ ] Add local preview within dashboard for operator verification. + +## Phase 4 – Reliability & Polish +- [ ] Add optional persistence (SQLite) and crash recovery. +- [ ] Expand logging/metrics for long-stream observability. +- [ ] Write tests (unit/integration) and contributor documentation. + +## Current Status +- Memory Bank established; repository scaffolding in place; backend ingestion stubs pending. diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md new file mode 100644 index 0000000..4b5f125 --- /dev/null +++ b/memory-bank/projectbrief.md @@ -0,0 +1,21 @@ +# Project Brief + +## Vision +Build a desktop-operated, open-source YouTube Live chat client that delivers a fast, reliable stream of messages and allows a streamer to spotlight a selected chat message on an OBS-ready overlay. + +## Core Requirements +- Mirror chat from a single YouTube Live stream with low latency and high stability. +- Provide an operator dashboard to browse, search, and filter live chat messages. +- Enable one-click selection of a message and broadcast it to an overlay page consumable in OBS. +- Keep setup approachable: clone repo, configure environment variables/OAuth, and run locally. +- Prioritize performance and resilience over advanced user management or multi-stream support. + +## Constraints +- Runs on the streamer’s personal computer; no external hosting assumed. +- No end-user authentication beyond stored YouTube OAuth credentials. +- Must tolerate YouTube API rate limits and intermittent network issues without crashing. + +## Success Criteria +- Operator dashboard stays responsive during long streams (>4 hours) without memory leaks. +- Overlay updates within ~1 second of operator selection. +- Project documentation enables others to reproduce setup from scratch. diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md new file mode 100644 index 0000000..5098f91 --- /dev/null +++ b/memory-bank/systemPatterns.md @@ -0,0 +1,18 @@ +# System Patterns + +## Architecture Overview +- **Monorepo Layout:** `pnpm` workspaces with `apps/client` (Next.js dashboard + overlay), `packages/backend` (Node ingestion + realtime gateway), `packages/shared` (types, schemas). +- **Data Flow:** + 1. Backend worker polls YouTube Live chat through the Innertube (youtubei) API, maintaining continuation tokens. + 2. Messages stored in-memory (and optionally SQLite) and emitted over an internal event bus. + 3. Client dashboard fetches chat via HTTP (React Query) and pushes selection back via REST. + 4. Overlay page listens to Server-Sent Events stream for the currently highlighted message. +- **Realtime Delivery:** SSE chosen for one-directional updates to OBS browser source; can swap to WebSocket if bidirectional control is required later. +- **Configuration:** Environment variables drive stream IDs and optional auth tokens; local secrets persisted in `.env.local` or config files. + +## Key Patterns & Practices +- Abstract ingestion behind an interface so alternate providers (official API, headless browser) can be swapped in quickly. +- Cache Innertube visitor data and API keys locally to reduce startup latency and handle rotations gracefully. +- Use Zod schemas in shared package to validate external responses and internal payloads. +- Centralized error reporting/logging with structured logs for monitoring during streams. +- Graceful degradation: exponential backoff on fetch failures, last-known overlay message cached to disk to survive restarts. diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md new file mode 100644 index 0000000..573b501 --- /dev/null +++ b/memory-bank/techContext.md @@ -0,0 +1,17 @@ +# Tech Context + +## Primary Stack +- **Frontend:** Next.js 14 (App Router) + React 18 + TypeScript, styled with Tailwind CSS and optional shadcn/ui components. +- **Backend Worker:** Node.js (Fastify) with `youtubei.js` for Innertube chat ingestion, `better-sqlite3` for persistence, EventEmitter for internal pub/sub. +- **Realtime:** Server-Sent Events for overlay updates; potential future WebSocket support via `ws`. +- **Tooling:** `pnpm` for workspace management, ESLint + Prettier, Zod for schema validation, Vitest/Playwright for testing (to be introduced later). + +## Environment & Dependencies +- No official API quota required; ingestion relies on Innertube visitor tokens produced at runtime. +- Required env values: `YOUTUBE_LIVE_ID` (or stream URL), optional overrides for Innertube API key/context if we need to pin versions. +- Local `.env.local` file manages configuration; sample `.env.example` committed for contributors. + +## Constraints & Considerations +- Innertube endpoints change occasionally; design ingestion to update keys dynamically and fall back to alternate strategies if responses shift. +- Application expected to run on Windows/macOS/Linux desktops used for streaming; keep dependencies cross-platform and avoid native build steps when possible. +- No external database by default; design backend to operate fully in-process with optional local persistence. diff --git a/package.json b/package.json new file mode 100644 index 0000000..a95738e --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "youtube-client", + "version": "0.1.0", + "private": true, + "description": "High-performance YouTube Live chat client for streamers.", + "license": "MIT", + "packageManager": "pnpm@8.15.4", + "scripts": { + "dev": "pnpm -r dev", + "build": "pnpm -r build", + "lint": "pnpm -r lint", + "start": "pnpm -r start" + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json new file mode 100644 index 0000000..3176aeb --- /dev/null +++ b/packages/backend/package.json @@ -0,0 +1,14 @@ +{ + "name": "backend", + "version": "0.1.0", + "private": true, + "description": "YouTube chat poller and realtime bridge.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "dev": "ts-node src/index.ts", + "build": "tsc -p tsconfig.build.json", + "start": "node dist/index.js", + "lint": "eslint 'src/**/*.{ts,tsx}'" + } +} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts new file mode 100644 index 0000000..c40b165 --- /dev/null +++ b/packages/backend/src/index.ts @@ -0,0 +1,12 @@ +import { bootstrapInnertube, fetchChatBatch } from './ingestion/youtubei'; + +export async function startBackend() { + console.log('Starting backend worker (youtubei ingestion pending).'); + const state = await bootstrapInnertube(); + console.log('Initial continuation state', state); + await fetchChatBatch(state); +} + +if (require.main === module) { + void startBackend(); +} diff --git a/packages/backend/src/ingestion/youtubei.ts b/packages/backend/src/ingestion/youtubei.ts new file mode 100644 index 0000000..9726e8d --- /dev/null +++ b/packages/backend/src/ingestion/youtubei.ts @@ -0,0 +1,18 @@ +export type ContinuationState = { + token: string | null; + apiKey?: string; +}; + +export async function bootstrapInnertube(): Promise { + // TODO: fetch initial visitor data and live chat continuation token via youtubei.js + return { token: null }; +} + +export async function fetchChatBatch(state: ContinuationState) { + // TODO: use youtubei.js to fetch messages with the provided continuation token + // return both normalized messages and the next continuation token + return { + messages: [], + next: state.token + }; +} diff --git a/packages/backend/tsconfig.build.json b/packages/backend/tsconfig.build.json new file mode 100644 index 0000000..da48075 --- /dev/null +++ b/packages/backend/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": false + } +} diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json new file mode 100644 index 0000000..ef1e1ea --- /dev/null +++ b/packages/backend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "module": "CommonJS", + "target": "ES2021", + "noEmit": false + }, + "include": ["src"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..aadfdf9 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,14 @@ +{ + "name": "shared", + "version": "0.1.0", + "private": true, + "description": "Shared types and schemas.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "dev": "tsc --watch -p tsconfig.json", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "lint": "eslint 'src/**/*.{ts,tsx}'" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..462e9b2 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,6 @@ +export type ChatMessage = { + id: string; + author: string; + text: string; + publishedAt: string; +}; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..e583ee8 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "module": "ESNext", + "target": "ES2021", + "declaration": true, + "declarationMap": true, + "noEmit": false + }, + "include": ["src"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e9b0dad --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'apps/*' + - 'packages/*' diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..b9e83d2 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "Node", + "allowJs": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "types": ["node"], + "baseUrl": "." + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ebf0d3e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "references": [ + { "path": "apps/client" }, + { "path": "packages/backend" }, + { "path": "packages/shared" } + ] +}