keystream-cli
v0.0.1
Published
Real-time terminal chat with Vim-style navigation training.
Maintainers
Readme
Keystream
Keystream is a real-time terminal chat app built with Bun. It is designed to do two jobs at once:
- let multiple users chat live from the CLI
- turn normal chat usage into Vim-style keyboard practice
The app has a standalone Bun WebSocket server, a full-screen terminal client, persistent SQLite history, room-based chat, and a modal UI with NORMAL, INSERT, COMMAND, and SEARCH modes.
Key Features
- Real-time multi-user chat over WebSockets
- Persistent message history stored in SQLite
- Room-based messaging with live room switching
- Full-screen terminal UI built for keyboard-first use
- Vim-style modal interaction
- Motion counts like
5j - Feed jumps with
ggandG - Search mode with
/ - Command mode with
: - Strict Vim mode that blocks arrow keys and tracks violations
- Lightweight training stats for mode time, streaks, and arrow-key usage
- Auto-reconnect behavior in the CLI client
- Local multi-client practice launcher with
tmux
Current Scope
This version is intentionally narrow:
- authentication is username-only
- chat is room-first
- direct messages are not implemented
- file upload, reactions, and message editing are not implemented
- this is best suited for trusted users or local/LAN usage unless you add TLS and stronger access control around it
Requirements
- Bun 1.3+
- macOS, Linux, or another environment where Bun and terminal WebSocket clients work
tmuxonly if you want to usebun run dev:stack
Install
bun installQuick Start
Option 1: Run the full local practice stack
This is the fastest way to try Keystream as a Vim practice tool.
bun run dev:stackWhat it does:
- starts one server
- opens three CLI clients in a tiled
tmuxsession - connects them to the same
generalroom - enables
--strict-vimin each client
If tmux is not installed, use the manual setup below.
Option 2: Run server and clients manually
Start the server:
bun --cwd apps/server src/index.ts --host 0.0.0.0 --port 3088 --db ./data/keystream.sqliteStart a client:
bun --cwd apps/cli src/index.ts --url ws://127.0.0.1:3088 --username alice --room general --strict-vimStart another client in a second terminal:
bun --cwd apps/cli src/index.ts --url ws://127.0.0.1:3088 --username bob --room general --strict-vimYou can omit --strict-vim if you want arrow keys to translate to motions instead of being counted as violations.
Public Install And Distribution
Install the public npm CLI
Once published, the CLI can be installed globally with npm:
npm install -g keystream-cli
keystream --helpThe installed command is keystream, while the package name is keystream-cli.
Run the published CLI
keystream --url ws://127.0.0.1:3088 --username alice --room general --strict-vimPull the backend Docker image
Once a tagged GitHub release has published the backend image:
docker pull ghcr.io/lihaowang/keystream-server:v0.0.1Run the backend Docker image with persistent storage
mkdir -p ./keystream-data
docker run --rm \
-p 3088:3088 \
-e KEYSTREAM_HOST=0.0.0.0 \
-e KEYSTREAM_PORT=3088 \
-e KEYSTREAM_DB=/data/keystream.sqlite \
-v "$(pwd)/keystream-data:/data" \
ghcr.io/lihaowang/keystream-server:v0.0.1This mounted /data path is the persistent SQLite store. If you do not mount it, history will not survive container replacement.
CLI Flags And Env Vars
Server
Command:
bun --cwd apps/server src/index.ts --host 0.0.0.0 --port 3088 --db ./data/keystream.sqliteFlags:
--hostbind address, default0.0.0.0--portlistening port, default3088--dbSQLite database path, default./data/keystream.sqlite
Environment variables:
KEYSTREAM_HOSTKEYSTREAM_PORTKEYSTREAM_DB
Client
Command:
bun --cwd apps/cli src/index.ts --url ws://127.0.0.1:3088 --username alice --room general --strict-vimFlags:
--urlWebSocket server URL, defaultws://127.0.0.1:3088--usernameusername to claim on connect--roominitial room name, defaultgeneral--strict-vimblock arrow keys and record violations
Environment variables:
KEYSTREAM_URLKEYSTREAM_USERNAMEKEYSTREAM_ROOMKEYSTREAM_STRICT_VIM=1
Flag precedence is higher than environment variables.
Published npm usage:
keystream --url ws://127.0.0.1:3088 --username alice --room general --strict-vimHow To Use Keystream
Screen layout
The client screen has four regions:
- left pane: room list
- main pane: message feed for the current room
- bottom input pane: mode-specific input area
- status line: mode, username, room, training stats, and status messages
Modes
Keystream is modal. Most interaction starts in NORMAL mode.
NORMAL
Use this mode to navigate rooms and messages.
i,a,o: enterINSERTmode:: enterCOMMANDmode/: enterSEARCHmodehandl: move between roomsjandk: move through messages in the current feed5j,10k, etc.: counted motionsgg: jump to the top of the current feedG: jump to the bottom of the current feedCtrl-u: move up 10 messagesCtrl-d: move down 10 messagesCtrl-c: quit immediately
INSERT
Use this mode to compose and send a message.
- type to edit the current message buffer
Backspace: delete one characterEnter: send the message to the active roomEsc: return toNORMAL
COMMAND
Use this mode for room and app commands.
- starts with
: - type a command, then press
Enter Esccancels command mode
SEARCH
Use this mode to filter the current room feed.
- starts with
/ - type a search term, then press
Enter - search is case-insensitive
- only the current room feed is filtered
- empty search clears the filter
Esccancels search mode
Built-in commands
:join <room>: join or create a room and switch to it:leave: leave the current room on the server:rooms: show known rooms in the status line:users: show active occupants in the current room:history [limit]: load older messages before the earliest loaded message:stats: show mode timing, streak, and arrow-key stats:help: print the command list:commands: alias for:help:quit: exit the client:q: alias for:quit
Vim training behavior
Keystream is not just a terminal chat client with some Vim keys added. The app is intentionally structured to train habits through repetition.
- Navigation is concentrated in
NORMALmode. - Writing happens in
INSERTmode. - Commands happen in
COMMANDmode. - Search happens in
SEARCHmode. - The status line tracks clean-motion streaks and arrow-key violations.
- In strict mode, arrow keys are blocked and replaced with a hint to use
h,j,k,l.
Typical workflow
- Start the server.
- Open one or more clients.
- Land in
NORMALmode. - Press
ito enterINSERTmode and type a message. - Press
Enterto send it. - Press
Escto go back toNORMAL. - Use
j,k,gg,G, and search to move through the feed. - Use
:join devto switch into another room. - Use
:statsto check your mode time and arrow-key violations.
Development Guide
Repository layout
apps/
cli/ Terminal client
server/ Bun WebSocket server + SQLite persistence
packages/
shared/ Shared protocol, types, and validation
scripts/
dev-stack.ts Local tmux-based practice launcher
test/
integration/ End-to-end server integration testsMain modules
packages/shared/src/protocol.ts: typed wire protocol and runtime validationapps/server/src/chat-server.ts: WebSocket lifecycle and room/message coordinationapps/server/src/store.ts: SQLite schema and persistence logicapps/cli/src/app.ts: modal terminal UI and client-side state machineapps/cli/src/client.ts: reconnecting WebSocket transportapps/cli/src/modal-state.ts: Vim training state and motion logicapps/cli/src/commands.ts: command parsing
Install dependencies
bun installRun tests
Run all tests:
bun testRun the TypeScript checks:
bunx tsc -p packages/shared/tsconfig.json --noEmit
bunx tsc -p apps/server/tsconfig.json --noEmit
bunx tsc -p apps/cli/tsconfig.json --noEmitBuild release artifacts locally
Build the Node-compatible npm CLI bundle:
bun run build:cliDry-run the npm package tarball:
bun run pack:cli --dry-runBuild the backend Docker image:
bun run docker:build:serverRun the backend Docker smoke test:
bun run docker:smoke:serverRecommended development loop
- Start the server in one terminal.
- Start one or more clients in separate terminals.
- Use
bun testafter behavior changes. - Re-run the TypeScript checks before finishing a change.
- Use
bun run dev:stackwhen you want rapid multi-user keyboard practice.
Manual smoke test
Use this after touching the server-client interaction path:
- Start the server on a local port.
- Open two clients with different usernames.
- Confirm both receive
welcomeandroom_snapshotbehavior. - Send messages from one client and confirm the other updates live.
- Join a new room with
:join. - Use
/search and:history. - Run
:statsand confirm Vim training counters update.
Persistence notes
- The server uses SQLite and creates the database directory automatically.
- The default database path is
./data/keystream.sqlite. - The local dev stack uses
./data/dev-stack.sqlite. - Message history survives server restarts when you reuse the same database path.
Networking notes
- The server speaks WebSocket only.
- The client reconnects automatically if the connection drops.
- Duplicate active usernames are rejected.
- Room message ordering is assigned by the server per room using monotonically increasing sequence numbers.
Scripts
bun run build: workspace build/typecheck script entrypointbun run check: alias forbun testbun run build:cli: build the public Node-compatible CLI package intoapps/cli/distbun run pack:cli --dry-run: generate and inspect the npm tarball fromapps/cli/distbun run docker:build:server: build the local backend container imagebun run docker:smoke:server: build, run, restart, and verify the Dockerized backend with persisted SQLite databun run release:check: run the release validation path used before taggingbun run dev:server: run the server package scriptbun run dev:cli: run the CLI package scriptbun run dev:stack: launch the tmux multi-client practice environment
Deployment Guide
Backend container
The Docker image targets the backend only. It does not package the CLI.
- Registry:
ghcr.io/lihaowang/keystream-server - Default internal port:
3088 - Default container DB path:
/data/keystream.sqlite - Supported runtime configuration:
KEYSTREAM_HOST,KEYSTREAM_PORT,KEYSTREAM_DB
Recommended deployment pattern:
- Mount a persistent volume to
/data - Expose port
3088or reverse-proxy it - Front the service with TLS if it is reachable outside a trusted LAN
- Reuse the same mounted database volume across restarts
GitHub Container Registry release path
The backend image is published by GitHub Actions on version tags:
- workflow:
.github/workflows/release-server-image.yml - trigger:
v*tags - image tags:
ghcr.io/lihaowang/keystream-server:<tag>ghcr.io/lihaowang/keystream-server:latest
npm CLI release path
The CLI release path is manual for final publish, but validated in CI:
- Build the CLI package with
bun run build:cli - Validate the package with
bun run pack:cli --dry-run - Log in to npm with
npm login - Publish from
apps/cli/distwith:
npm publish ./apps/cli/dist --access publicSee docs/releasing.md for the maintainer flow.
Known Limitations
:leaveacknowledges on the server but the UI does not implement a separate “roomless” screen state.- Presence updates are modeled in the protocol but not yet rendered as live status changes in the UI.
- The README examples use direct Bun entrypoints because they are the most explicit and reliable during development.
- Public internet deployment should be fronted by TLS and stronger access control if you plan to expose it beyond a trusted environment.
Future Improvements
- Direct messages
- Better presence rendering
- Message editing and reactions
- Room privacy and invitations
- Richer training metrics and drills
- PTY-level end-to-end tests for the full terminal UI
