Files
2026-02-17 17:30:45 -05:00

524 lines
20 KiB
Markdown

# 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 |