# PRD: CLI & TUI Client **Module:** `cli/` | **Namespace:** `ajet.chat.cli.*` **Status:** v1 | **Last updated:** 2026-02-17 --- ## 1. Overview The CLI module is a single executable (babashka via bbin) that provides two modes: - **CLI Mode:** Stateless one-shot commands for scripting and quick interactions - **TUI Mode:** Full interactive terminal application with split panes, inline images, markdown rendering, and mouse support (built on clojure-tui) Both modes connect through the Auth Gateway. ## 2. Installation & Config ### 2.1 Installation ```bash bbin install ajet-chat # installs as `ajet` command ``` ### 2.2 Config Location ``` ~/.config/ajet-chat/ ├── config.edn # server URL, default community, preferences ├── session.edn # session token (encrypted at rest) └── state.edn # TUI state: last community, last channel per community ``` ### 2.3 Config Shape ```clojure ;; config.edn {:server-url "https://chat.example.com" :default-community "my-team" ;; slug :tui {:theme :dark ;; :dark | :light :image-viewer :timg ;; :timg | :sixel | :none :mouse true :timestamps :relative ;; :relative | :absolute | :none :notifications :bell}} ;; :bell | :none ;; session.edn (encrypted) {:token "base64url-session-token" :user-id "uuid" :username "alice" :expires-at "2026-03-19T..."} ``` ## 3. CLI Mode ### 3.1 Command Syntax ``` ajet [options] [args] ``` ### 3.2 Commands #### Authentication ```bash ajet login # Interactive OAuth login (opens browser) ajet login --token # Login with API token (for scripts) ajet logout # Clear session ajet whoami # Show current user info ``` #### Channels & Communities ```bash ajet communities # List communities ajet channels # List channels in default community ajet channels --community # List channels in specific community ajet channels --join # Join a public channel ajet channels --leave # Leave a channel ``` #### Messages ```bash ajet read # Read last 50 messages in channel ajet read --limit 100 # Custom limit ajet read --before # Older messages (pagination) ajet read --thread # Read thread replies ajet send # Send message ajet send --stdin # Read message from stdin (piping) ajet send --image # Send message with image ajet edit # Edit a message ajet delete # Delete a message ``` #### DMs ```bash ajet dms # List DM channels ajet dm # Send DM (creates if needed) ajet dm --read # Read DM conversation ``` #### Notifications ```bash ajet notifications # List unread notifications ajet notifications --all # List all notifications ajet notifications --mark-read # Mark all as read ``` #### Search ```bash ajet search # Global search ajet search --channel # Search in specific channel ajet search --from # Search by author ajet search --type messages # Filter by type ``` #### Presence & Status ```bash ajet status # Show current status ajet status "Working on backend" # Set status ajet who # Show online users in current community ``` #### Invites ```bash ajet invite create # Generate invite link ajet invite create --max-uses 10 # With use limit ajet invite list # List active invites ajet invite revoke # Revoke an invite ``` #### Admin ```bash ajet config # Show current config ajet config set # Set config value ajet config server # Set server URL ``` ### 3.3 Output Formats **Default (human-readable):** ``` $ ajet read general --limit 3 #general — My Team alice 10:30 AM hello everyone! bob 10:31 AM hey! check this out [image: screenshot.png] carol 10:45 AM @bob nice work! 👍 ``` **JSON output (for scripting):** ```bash ajet read general --json | jq '.messages[].body_md' ``` **Pipe-friendly:** ```bash echo "Build #42 passed!" | ajet send devops --stdin git log --oneline -5 | ajet send backend --stdin ``` ### 3.4 OAuth Login Flow (CLI) ``` 1. ajet login 2. CLI starts a temporary local HTTP server (localhost:random-port) 3. Opens browser to: https://chat.example.com/auth/login?redirect=http://localhost:{port}/callback 4. User completes OAuth in browser 5. Auth GW redirects to localhost callback with session token 6. CLI captures token, saves to ~/.config/ajet-chat/session.edn 7. Prints "Logged in as alice" ``` **Fallback (no browser):** ``` $ ajet login Opening browser... If your browser didn't open, visit: https://chat.example.com/auth/login?cli=true Then paste the token here: _ ``` ## 4. TUI Mode ### 4.1 Launch ```bash ajet tui # Open TUI (interactive mode) ajet tui --community # Open to specific community ajet tui --channel # Open to specific channel ``` ### 4.2 Layout (Rich TUI with clojure-tui) ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ ajet chat │ My Team alice ● online │ ├──────────────────┬──────────────────────────────────────────────────────────┤ │ Communities │ #general — Welcome to My Team! │ │ ● My Team │──────────────────────────────────────────────────────────│ │ Open Source │ │ │ │ alice 10:30 │ │ ──────────────── │ hello everyone! │ │ ▼ GENERAL │ │ │ #general 3 │ bob 10:31 │ │ #random │ hey! check out this screenshot │ │ │ ┌─────────────────────────────┐ │ │ ▼ DEV │ │ [inline image via timg] │ │ │ #backend ● │ │ │ │ │ #frontend │ └──────────────────────────────┘ │ │ │ │ │ ──────────────── │ carol 10:45 │ │ DMs │ @bob nice work! 👍 │ │ alice ● │ 💬 2 replies │ │ bob │ │ │ group (3) 2 │ ── Load older ── │ │ │ │ │ ──────────────── │──────────────────────────────────────────────────────────│ │ 🔍 /search │ bob is typing... │ │ │ > hello world_ │ │ │ │ ├──────────────────┴──────────────────────────────────────────────────────────┤ │ [/help] Ctrl+K: search Ctrl+N: next channel Ctrl+P: prev Ctrl+Q: quit │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### 4.3 TUI Panes | Pane | Description | |------|-------------| | Header | App name, community name, user status | | Sidebar | Communities, categories, channels, DMs with unread indicators | | Channel Header | Channel name + topic | | Message List | Scrollable message history with rendered markdown | | Input Area | Message composition with autocomplete | | Status Bar | Keybindings, connection status | | Thread Panel | Overlays right side when viewing thread (like web) | ### 4.4 Keyboard Navigation | Key | Action | |-----|--------| | `Enter` | Send message | | `Shift+Enter` / `Alt+Enter` | Newline in input | | `Ctrl+K` | Open search | | `Ctrl+N` | Next channel (down in sidebar) | | `Ctrl+P` | Previous channel (up in sidebar) | | `Ctrl+Q` | Quit | | `Ctrl+T` | Open thread for selected message | | `Ctrl+R` | React to selected message | | `Ctrl+E` | Edit selected message | | `Ctrl+D` | Delete selected message (with confirmation) | | `Tab` | Switch focus: input ↔ message list ↔ sidebar | | `↑/↓` | Navigate messages (when message list focused) | | `j/k` | Vim-style navigate messages | | `PgUp/PgDn` | Scroll message history (loads older on PgUp at top) | | `Esc` | Close thread panel / cancel edit / blur search | | `/` | Start slash command (when input focused) | | `@` | Start mention autocomplete | | `#` | Start channel autocomplete | | `Mouse click` | Select channel, message, or button | | `Mouse scroll` | Scroll message list | ### 4.5 Inline Image Rendering **Via timg (preferred):** - Detect terminal capabilities (sixel, kitty graphics, iterm2) - Use timg to render images inline in the message list - Fallback: show `[image: filename.png]` placeholder with URL **Image display flow:** 1. Message with attachment arrives 2. TUI SM provides image URL in event 3. TUI client downloads image in background 4. Renders inline using timg/sixel 5. If terminal doesn't support graphics: show filename + dimensions ### 4.6 Markdown Rendering (ANSI) | Markdown | ANSI Rendering | |----------|---------------| | `**bold**` | ANSI bold | | `*italic*` | ANSI italic (or dim on unsupported terminals) | | `~~strike~~` | ANSI strikethrough | | `__underline__` | ANSI underline | | `` `code` `` | Dim background or different color | | Code block | Box-drawing border + syntax highlighting (ANSI colors) | | `> quote` | Vertical bar prefix + dim text | | `\|\|spoiler\|\|` | Hidden text (press Enter on selected to reveal) | | Links | Underlined + hyperlink escape (OSC 8) | | Emoji | Unicode emoji (terminal must support) | | `@mention` | Highlighted (bold + color) | | `#channel` | Highlighted (different color), clickable | ### 4.7 SSE Connection TUI client connects to TUI SM via SSE: ``` GET https://chat.example.com/tui/sse/events?community_id= Cookie: ajet_session= Accept: text/event-stream Last-Event-ID: (for reconnection) ``` - On connect: receive `init` event, populate sidebar + channel list - On events: update message list, sidebar badges, typing indicators - On disconnect: show "Reconnecting..." in status bar, auto-retry with backoff ### 4.8 Notifications in TUI - Terminal bell (`\a`) on new @mention or DM (configurable) - Unread counts in sidebar (same as web) - Notification list accessible via slash command `/notifications` ## 5. Distribution & Packaging ### 5.1 bbin Packaging **Build:** Compile to an uberjar, then distribute via bbin (Babashka binary installer). ```bash # Build uberjar clj -T:build uber # produces target/ajet-chat-cli.jar # Install locally via bbin bbin install . --as ajet # installs 'ajet' command from local project # Install from remote (for users) bbin install io.github.ajet/ajet-chat-cli --as ajet ``` **Binary:** The `ajet` command is a shell wrapper that invokes `java -jar` (or `bb` if Babashka-compatible). First run may be slow due to JVM startup; subsequent runs benefit from Drip or CDS caching. ### 5.2 Babashka Compatibility **Goal:** CLI mode should be Babashka-compatible for fast startup. TUI mode requires JVM (clojure-tui dependency). **Constraints for Babashka compatibility:** - No `deftype` / `defrecord` (use maps + protocols sparingly) - No `gen-class` - Use `babashka.http-client` (not `clj-http`) - Use `clojure.data.json` (bb-compatible) - Avoid Java interop beyond what bb supports - All CLI commands (non-TUI) target < 100ms startup via bb **Fallback:** If Babashka compatibility proves too restrictive, ship as JVM uberjar with CDS (Class Data Sharing) for faster startup. ### 5.3 Exit Codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | General error (API error, unexpected failure) | | 2 | Usage error (bad arguments, unknown command) | | 3 | Authentication error (not logged in, token expired) | | 4 | Permission error (403 from API) | | 5 | Not found (404 from API — channel, message, user doesn't exist) | | 130 | Interrupted (Ctrl+C / SIGINT) | ### 5.4 Error Message UX All errors follow this format: ``` Error:
Hint: ``` **Examples:** ``` Error: Not logged in No session token found. You need to authenticate first. Hint: Run 'ajet login' to sign in ``` ``` Error: Channel not found: #nonexistent The channel doesn't exist or you don't have access. Hint: Run 'ajet channels' to see available channels ``` ``` Error: Edit window expired Messages can only be edited within 1 hour of creation. This message was sent 3 hours ago. ``` ### 5.5 Offline Behavior | Scenario | Behavior | |----------|----------| | Server unreachable | `Error: Cannot connect to server at chat.example.com` + hint to check config | | Timeout (> 10s) | `Error: Request timed out` + hint to retry | | TUI SSE disconnects | Status bar shows "Reconnecting..." + auto-retry with backoff | | TUI SSE reconnects | Catches up on missed events, no user action needed | --- ## 6. Test Cases ### 6.1 CLI Authentication | ID | Test | Description | |----|------|-------------| | CLI-T1 | Login via OAuth | Opens browser, captures callback, saves token | | CLI-T2 | Login via API token | `--token` flag saves token directly | | CLI-T3 | Logout clears session | Token removed from config | | CLI-T4 | Whoami shows user | Prints username, display name, communities | | CLI-T5 | Expired token | Commands return clear "session expired, run ajet login" message | | CLI-T6 | No config exists | First run creates config dir and prompts for server URL | ### 6.2 CLI Commands | ID | Test | Description | |----|------|-------------| | CLI-T7 | List communities | Shows user's communities | | CLI-T8 | List channels | Shows channels for default community | | CLI-T9 | Read messages | Displays formatted messages with avatars, timestamps | | CLI-T10 | Read with pagination | `--before` flag returns older messages | | CLI-T11 | Read thread | `--thread` flag shows thread replies | | CLI-T12 | Send message | Message appears in channel | | CLI-T13 | Send via stdin | `echo "msg" \| ajet send general --stdin` works | | CLI-T14 | Send with image | Image uploaded, message sent with attachment | | CLI-T15 | Edit message | Message updated within 1-hour window | | CLI-T16 | Edit after window | Returns error "edit window expired" | | CLI-T17 | Delete message | Message deleted with confirmation prompt | | CLI-T18 | List DMs | Shows DM conversations | | CLI-T19 | Send DM | Creates DM if needed, sends message | | CLI-T20 | List notifications | Shows unread notifications | | CLI-T21 | Mark notifications read | All notifications marked read | | CLI-T22 | Search | Returns matching results with context | | CLI-T23 | Search with filters | --channel, --from, --type filters work | | CLI-T24 | Set status | Status updated | | CLI-T25 | Who online | Shows online users list | | CLI-T26 | Create invite | Returns invite link | | CLI-T27 | JSON output | `--json` flag outputs raw JSON | | CLI-T28 | Unknown command | Prints help with suggestion | | CLI-T29 | No arguments | Prints usage/help | ### 6.3 TUI Launch & Layout | ID | Test | Description | |----|------|-------------| | TUI-T1 | TUI launches | Full layout renders with all panes | | TUI-T2 | Sidebar populated | Communities, channels, DMs shown from init event | | TUI-T3 | Messages loaded | Active channel messages displayed | | TUI-T4 | Unread badges | Channels with unread messages show count | | TUI-T5 | Online indicators | Online users have green dot in DM list | | TUI-T6 | Status bar | Shows keybindings and connection status | ### 6.4 TUI Navigation | ID | Test | Description | |----|------|-------------| | TUI-T7 | Ctrl+N next channel | Moves to next channel in sidebar | | TUI-T8 | Ctrl+P prev channel | Moves to previous channel | | TUI-T9 | Tab focus cycle | Focus cycles: input → messages → sidebar | | TUI-T10 | Click channel | Mouse click switches to channel | | TUI-T11 | Arrow keys in messages | Navigate between messages | | TUI-T12 | j/k vim navigation | Vim-style navigation in message list | | TUI-T13 | PgUp loads older | Scrolling up loads older messages | | TUI-T14 | Mouse scroll | Mouse scroll in message list | | TUI-T15 | Esc closes panels | Thread panel or search closes on Esc | | TUI-T16 | Community switch | Click community in sidebar → channels update | ### 6.5 TUI Messaging | ID | Test | Description | |----|------|-------------| | TUI-T17 | Send message | Enter sends, message appears in list | | TUI-T18 | Multiline input | Alt+Enter adds newline | | TUI-T19 | @mention autocomplete | Typing @ shows dropdown, Tab selects | | TUI-T20 | #channel autocomplete | Typing # shows dropdown | | TUI-T21 | /slash command | Typing / shows command list | | TUI-T22 | Edit message | Ctrl+E on selected → inline edit → Enter to save | | TUI-T23 | Delete message | Ctrl+D on selected → confirmation → deleted | | TUI-T24 | React to message | Ctrl+R → emoji input → reaction added | | TUI-T25 | Open thread | Ctrl+T on message with replies → thread panel | | TUI-T26 | Reply in thread | Type in thread input → reply sent | | TUI-T27 | Image paste | Not supported in TUI (CLI `--image` flag instead) | ### 6.6 TUI Real-Time | ID | Test | Description | |----|------|-------------| | TUI-T28 | New message arrives | SSE event → message appears at bottom | | TUI-T29 | Typing indicator | "bob is typing..." shows below message list | | TUI-T30 | Presence updates | Online dot changes when user goes offline | | TUI-T31 | Unread updates | New message in other channel updates sidebar badge | | TUI-T32 | SSE reconnect | Connection lost → "Reconnecting..." → auto-reconnects | | TUI-T33 | Bell notification | Terminal bell on @mention or DM | ### 6.7 TUI Rendering | ID | Test | Description | |----|------|-------------| | TUI-T34 | Markdown bold | `**text**` renders as ANSI bold | | TUI-T35 | Markdown italic | `*text*` renders as ANSI italic | | TUI-T36 | Code block | Fenced code block renders with border + syntax colors | | TUI-T37 | Inline code | Backtick code renders with different color | | TUI-T38 | Block quote | `> text` renders with vertical bar prefix | | TUI-T39 | Spoiler text | `\|\|text\|\|` renders hidden until Enter pressed | | TUI-T40 | Inline image (timg) | Image renders inline via timg when supported | | TUI-T41 | Image fallback | `[image: file.png 800x600]` when graphics not supported | | TUI-T42 | Mention highlight | @alice renders bold + colored | | TUI-T43 | Channel link | #general renders colored and navigable | | TUI-T44 | Hyperlinks | URLs render as OSC 8 hyperlinks when terminal supports | | TUI-T45 | Long message wrapping | Long messages wrap correctly within pane width | | TUI-T46 | Terminal resize | Layout reflows on terminal resize event | ### 6.8 TUI Error Handling | ID | Test | Description | |----|------|-------------| | TUI-T47 | Send fails | Error shown inline below input | | TUI-T48 | API timeout | Status bar shows warning, retries | | TUI-T49 | Ctrl+Q quit | Clean shutdown: close SSE, save state | | TUI-T50 | SIGINT handling | Ctrl+C during TUI gracefully exits |