18 KiB
18 KiB
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
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
;; 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 <command> [options] [args]
3.2 Commands
Authentication
ajet login # Interactive OAuth login (opens browser)
ajet login --token <api-token> # Login with API token (for scripts)
ajet logout # Clear session
ajet whoami # Show current user info
Channels & Communities
ajet communities # List communities
ajet channels # List channels in default community
ajet channels --community <slug> # List channels in specific community
ajet channels --join <name> # Join a public channel
ajet channels --leave <name> # Leave a channel
Messages
ajet read <channel> # Read last 50 messages in channel
ajet read <channel> --limit 100 # Custom limit
ajet read <channel> --before <id> # Older messages (pagination)
ajet read <channel> --thread <id> # Read thread replies
ajet send <channel> <message> # Send message
ajet send <channel> --stdin # Read message from stdin (piping)
ajet send <channel> --image <path> # Send message with image
ajet edit <message-id> <new-text> # Edit a message
ajet delete <message-id> # Delete a message
DMs
ajet dms # List DM channels
ajet dm <username> <message> # Send DM (creates if needed)
ajet dm <username> --read # Read DM conversation
Notifications
ajet notifications # List unread notifications
ajet notifications --all # List all notifications
ajet notifications --mark-read # Mark all as read
Search
ajet search <query> # Global search
ajet search <query> --channel <ch> # Search in specific channel
ajet search <query> --from <user> # Search by author
ajet search <query> --type messages # Filter by type
Presence & Status
ajet status # Show current status
ajet status "Working on backend" # Set status
ajet who # Show online users in current community
Invites
ajet invite create # Generate invite link
ajet invite create --max-uses 10 # With use limit
ajet invite list # List active invites
ajet invite revoke <id> # Revoke an invite
Admin
ajet config # Show current config
ajet config set <key> <value> # Set config value
ajet config server <url> # 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):
ajet read general --json | jq '.messages[].body_md'
Pipe-friendly:
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
ajet tui # Open TUI (interactive mode)
ajet tui --community <slug> # Open to specific community
ajet tui --channel <name> # 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:
- Message with attachment arrives
- TUI SM provides image URL in event
- TUI client downloads image in background
- Renders inline using timg/sixel
- 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=<uuid>
Cookie: ajet_session=<token>
Accept: text/event-stream
Last-Event-ID: <id> (for reconnection)
- On connect: receive
initevent, 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. Test Cases
5.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 |
5.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 |
5.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 |
5.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 |
5.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) |
5.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 |
5.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 |
5.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 |