- setup.sh: idempotent one-shot droplet provisioner — agentbbs on :22 (admin OpenSSH moved to :2202), rootless podman, Caddy front end for https://bbs.profullstack.com with tilde-style /~user homepages - internal/sites + domain@ SSH route: self-service custom domains (ssh domain@host add example.com) backed by a symlink farm and an on-demand-TLS ask endpoint so Caddy only issues certs for mapped hosts - internal/mail + join@ email verification: optional email at signup, confirmation link served by a loopback /verify endpoint behind Caddy - internal/source + cmd/ascii-live: live video → terminal ASCII groundwork (docs/ascii-live.md) - store: additive sqlite migrations (email/verify columns, domains table) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
PRD: ASCII Live (revised)
Feature: ASCII Live — watch live video sources as terminal-native ASCII over SSH and browser-terminal. Primary host: AgentBBS (Go). Status: Revised draft (supersedes the standalone "@logicsrc/plugin-ascii-live" PRD). Owner: Profullstack / LogicSRC.
Why this revision exists. The original PRD specified a single Node.js/TypeScript package (
@logicsrc/plugin-ascii-live) hosted inside AgentBBS, with a fresh char-ramp ASCII renderer and PairUX support as a future phase. Grounding that against the real repos found two structural problems:
- AgentBBS is Go 1.26 (
go.mod) with a Go plugin interface (internal/plugin/plugin.go). It cannot load a TS package. The TSPluginDefinitionshape (logicsrc/packages/plugin-core) is a different plugin system — the logicsrc web/Hono/SDK world.- The core capability already exists in AgentBBS.
internal/ascii/ascii.gorenders RGB24 → truecolor half-block (▀) ANSI, andinternal/calls/already joins a PairUX LiveKit call and renders it as ASCII over SSH (shipped/verified 2026-06-11, "128k truecolor cells streamed"). The original PRD's char-ramp renderer is a visual downgrade from this, and its "PairUX = future phase" is largely already done.This PRD refits the product as two components and scopes the MVP to the work that is genuinely new.
1. Architecture: two components
ASCII Live is not one package. It is:
| Component | Repo / stack | Responsibility |
|---|---|---|
A. ascii-live core |
Go, in agentbbs |
Source adapters (YouTube/HLS), shared FFmpeg worker, viewer fan-out, terminal rendering, the tv@/tv-<slug>@ SSH route, and a cmd/ascii-live local CLI. Reuses internal/ascii and the internal/calls patterns. |
B. @logicsrc/plugin-ascii-live |
TS, in logicsrc |
Browser-terminal viewer (xterm.js + SSE), public stream directory, and the stream metadata/events API. Built as a logicsrc PluginDefinition (routes + events + tuiPanels) — the shape it is literally designed for. |
Component A is the MVP. Component B is V1 (the browser viewer) and is the only
place the @logicsrc/plugin-ascii-live TS package exists.
┌─────────────────────── AgentBBS (Go) ───────────────────────┐
YouTube URL ─▶ │ source adapter ─▶ shared FFmpeg worker ─▶ RGB24 frames │
HLS .m3u8 ─▶ │ (yt-dlp -g) (one per stream) │ │
PairUX call ─▶ │ LiveKit tracks ─▶ VP8→IVF→ffmpeg ───────────▶│ │
│ ▼ │
│ internal/ascii.FrameRGB (▀ truecolor)
│ │ │
│ fan-out multiplexer (1 worker → N) │
│ │ │ │
└────────────────────────────┼────────────┼────────────────────┘
▼ ▼ (frames via SSE)
SSH PTY viewers @logicsrc/plugin-ascii-live
(tv-<slug>@) browser xterm.js viewer
2. What already exists (reuse, do not rebuild)
internal/ascii/ascii.go—FrameRGB(buf, w, h)renders packed RGB24 to truecolor half-block ANSI (two pixels per cell via▀);FitEven(cols, rows)clamps geometry for the renderer leaving a status line. This is the default renderer.internal/calls/{calls,livekit}.go— joins a PairUX LiveKit call from an SSH session (video-<code>@,video@), pipeline VP8 → IVF → ffmpeg → RGB24 → ANSI. Codes are minted by PairUX only; SSH never creates calls.internal/store— pure-Go (modernc) sqlite store. ASCII Live metadata uses this, not a parallel DB.- Plugin contract —
internal/plugin/plugin.go(ID/Title/Description/RequiresAuth/New,ExitMsg). Hub menu integration. ~/src/intr0s— real branded intro/logo.mp4assets for intro/BRB cards.
Hard-won pipeline lessons (must carry forward)
- Subscriber must send PLI or the SFU never forwards video.
lksdkIVF replay mispaces — useReaderTrackWithFrameDuration.- Publish needs
VideoWidth/Heightor dynacast pauses the track. - ffmpeg pipe needs
-probesize 32. - Go pinned 1.26 via
mise.toml(lksdk requires it).
3. What is genuinely new (the MVP)
- URL source adapter —
yt-dlp -f 'best[height<=480]/best' -g <url>to resolve a YouTube Live URL to an HLS URL (or accept a direct.m3u8), then ffmpeg-vf "fps=N,scale=W:-2:flags=lanczos,format=rgb24" -pix_fmt rgb24 -f rawvideo→ RGB24 frames. (The existing path ingests LiveKit tracks; this adds a URL source.) Always convert torgb24—yuv420pwill fail. - Shared-worker fan-out — one ffmpeg worker per distinct stream, N
PTY viewers attached. The current
calls.Handleis standalone (one decode per session, leaving ends the session). The multiplexer — viewer join/leave, backpressure, reap-after-last-viewer-unless-pinned — is the headline value-add. - SSRF / source hardening — user-supplied URLs are a new attack surface the
video@route never had (PairUX mints all codes there). This is an MVP acceptance criterion, not a later nicety. Block: private/link-local/loopback IPs, cloud metadata ranges (169.254.169.254 etc.),file://, and any protocol other thanhttp(s)and the resolved HLS. Re-validate after DNS resolution and after any redirect.
4. Interaction model — username routing, not slash commands
AgentBBS routes by SSH username (bbs@, pod@, agent@, video-<code>@) +
Bubble Tea hub menus. There is no in-room slash-command chat layer; chat is the
separate agent@ surface. So the original /ascii-live open <url> + "room chat
alongside" model does not fit. ASCII Live adopts username routing:
ssh tv@host # browse the public stream directory (hub menu)
ssh tv-<slug>@host # attach directly to a running stream
- Browsing/attaching is open to guests for public streams.
- Opening a new stream from a URL is an authenticated action (hub menu for members/agents, or the CLI/web) — never an anonymous SSH command, because that is the SSRF + abuse surface. First viewer to open a URL becomes the worker owner.
- Chat, if shown, is a separate lipgloss text region beneath the frame (mirror the existing status-line layout). Never composite chat into pixels.
The local CLI mirrors the surfaces for dev/testing:
ascii-live watch "https://youtube.com/live/<id>" # local terminal preview
ascii-live watch "pairux:room_abc123" # reuse the calls path
ascii-live serve --ssh-port is a local dev convenience only — production
reuses the AgentBBS wish server. Do not stand up a second SSH server.
5. Renderer
- Default: truecolor half-block via
internal/ascii.FrameRGB(existing). This is the house style (matches doom-ascii) and looks far better than char-ramp. - Themes are color-degradation fallbacks, not the primary path:
truecolor(default),ansi(256/16-color approximation),mono/green/amber(char-ramp.:-=+*#%@for terminals without truecolor). The char ramp is the fallback, never the default.
- Adaptive geometry via
FitEven(cols, rows): clamp to even pixel height, leave one status line,width = min(config.width, terminal.columns).
6. Free vs paid
Reuse the existing CoinPay / pods plumbing — no new billing system.
- Free: public YouTube/HLS URLs, public
tv-<slug>@directory, low default FPS, header/watermark, session + concurrency limits. - Creator/Pro/Studio: private streams, PairUX-as-source under access tokens, no watermark, higher FPS, presets, archive/replay, API/webhooks. Gate via the same plan capability check used elsewhere in AgentBBS.
7. Data model
Integrate with internal/store (pure-Go sqlite already in agentbbs). Do not
create a parallel DB. Minimal tables:
CREATE TABLE ascii_live_streams (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
owner_user_id TEXT,
source_type TEXT NOT NULL, -- youtube | hls | pairux
source_url TEXT NOT NULL,
title TEXT,
status TEXT NOT NULL, -- starting | live | stopped | error
fps INTEGER NOT NULL DEFAULT 10,
width INTEGER NOT NULL DEFAULT 120,
theme TEXT NOT NULL DEFAULT 'truecolor',
pinned INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME, stopped_at DATETIME,
error_message TEXT
);
CREATE TABLE ascii_live_viewers (
id TEXT PRIMARY KEY,
stream_id TEXT NOT NULL REFERENCES ascii_live_streams(id),
user_id TEXT,
connection_type TEXT NOT NULL, -- ssh | web
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
left_at DATETIME
);
(Presets table is optional, deferred to when saved presets ship.)
8. Config (Go)
Mirror the existing AGENTBBS_* / LIVEKIT_* env convention. Equivalent of the
original TS config as a Go struct + env knobs:
ASCIILIVE_ENABLED (bool)
ASCIILIVE_DEFAULT_FPS (int, default 10)
ASCIILIVE_DEFAULT_WIDTH (int, default 120)
ASCIILIVE_DEFAULT_THEME (truecolor|ansi|mono|green|amber)
ASCIILIVE_MAX_DURATION_MIN (int)
ASCIILIVE_MAX_CONCURRENT (int) # streams
ASCIILIVE_MAX_VIEWERS (int) # per stream
ASCIILIVE_MAX_FPS / _MAX_WIDTH (int)
ASCIILIVE_SHOW_HEADER (bool)
ASCIILIVE_WATERMARK (string, optional)
ASCIILIVE_ALLOW_HLS (bool) # direct .m3u8 input
9. Events (component B only)
The frame event model (ascii_live.frame carrying the full frame string) is for
SSE/web delivery only (component B). SSH frames write straight to the PTY
as internal/calls already does — do not route SSH frames through an event bus.
Bus events worth emitting for the web/directory + agents: stream_started,
stream_stopped, error, viewer_joined, viewer_left (metadata only, no
per-frame payload on the control bus).
10. Phases (revised)
| Phase | Scope | Notes vs original PRD |
|---|---|---|
| 0 | Go cmd/ascii-live watch <url> — yt-dlp resolve + ffmpeg rgb24 + internal/ascii render, FPS/width flags, clean Ctrl+C. |
Was "local TS CLI"; now Go, reusing the renderer. |
| 1 (MVP) | tv@/tv-<slug>@ SSH route; authenticated open-from-URL; shared worker fan-out (1 ffmpeg → N viewers, reap after last); SSRF guard; truecolor render; theme/fps/width; status + clean errors; process cleanup. |
Pulls original "Phase 3 multiplexing" into the MVP; drops the chat-room + char-ramp + TS-package assumptions. |
| 2 | Browser viewer = @logicsrc/plugin-ascii-live (TS, in logicsrc): xterm.js + SSE, shared stream state with SSH, public directory. |
This is the only TS package. |
| 3 | PairUX-as-source under the tv directory with access tokens. |
Mostly already built (internal/calls, raw tracks). Reframe as "expose existing call render in the directory + tokens," not a composed-stream ingest. |
| 4 | Paid/private sources, plan limits, usage tracking via existing CoinPay/pods. | |
| 5 | intr0s intro/BRB/outro cards; archive/replay; captions/transcripts. | |
| 6 (optional) | ASCII-styled RTMP publishing — separate pipeline, not the viewer path. | Explicitly out of scope for everything above. |
11. MVP acceptance criteria
MVP (Phase 1) is complete when:
- A member can open a public YouTube Live or HLS URL (authenticated path).
- The stream renders as truecolor half-block ASCII over SSH via
tv-<slug>@. - ≥2 viewers attach to the same stream with exactly one ffmpeg worker.
- The worker is reaped after the last viewer leaves (unless
pinned). - Theme, FPS, and width are adjustable within configured limits.
- SSRF guard rejects private/loopback/metadata IPs,
file://, and non-http(s) protocols, re-checking after DNS resolution and redirects. - Errors are shown cleanly (no raw ffmpeg/yt-dlp dumps to normal users); admins/ debug mode can see detail.
- Process + temp cleanup is reliable on disconnect, close, and crash.
12. Decisions (resolving the original §29 open questions)
- SSH ownership — AgentBBS owns SSH; reuse
wish. No second server. - PairUX shape — raw LiveKit tracks (already shipped). No composed-stream ingest.
- Chat placement — separate lipgloss text region below the frame; never in pixels.
- Audio — ignored for MVP (already the case); captions are a later transcript feature.
- Free sources — public-only; directory is a V1 web feature, not MVP.
- Self-host — yes, day one (repo is OSS; Go CLI gives it for free).
- Standalone CLI — yes, as a Go
cmd/ascii-liveinagentbbs, sharing one frame package acrosscmd/and the SSH route. Not a separate TS tool.
13. Net
The "video → ASCII → SSH" core is already proven in this codebase. ASCII Live is
therefore a small feature: a Go source-adapter + shared-worker + tv@ route
reusing internal/ascii/internal/calls, plus a separate TS
@logicsrc/plugin-ascii-live strictly for the browser viewer. Build it in that
order; keep RTMP publishing out of the viewer path entirely.