Files
ajet-chat/docs/prd/cli.md
2026-02-17 17:30:45 -05:00

20 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
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:

  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=<uuid>
Cookie: ajet_session=<token>
Accept: text/event-stream
Last-Event-ID: <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).

# 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: <short description>

  <details or suggestion>

Hint: <actionable next step>

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