# 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 <command> [options] [args]
```

### 3.2 Commands

#### Authentication
```bash
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
```bash
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
```bash
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
```bash
ajet dms                            # List DM channels
ajet dm <username> <message>        # Send DM (creates if needed)
ajet dm <username> --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 <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
```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 <id>             # Revoke an invite
```

#### Admin
```bash
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):**
```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 <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).

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