524 lines
20 KiB
Markdown
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 |
|