What I Learned Building a CLI That People Actually Use
My first version of llmswap CLI was terrible.
llmswap --ask "What is Docker?" --provider anthropic --model claude-3-opus-20240229 --temperature 0.7 --max-tokens 4000
# Nobody wants to type this.
After building, testing, and iterating for months, the CLI now has 6000+ downloads on PyPI. Here’s what I learned about making command-line tools people actually want to use.
Lesson 1: Start With the User Experience, Not the Code
Wrong approach: Build based on your code structure
# My first attempt - mirrored my Python classes
llmswap --client-type llm --operation query --provider openai --input "hello"
Right approach: Think about what users type
# What users actually want
llmswap ask "hello"
llmswap chat
llmswap review app.py
I rewrote the entire argument parsing three times before getting this right. The final version has clean subcommands:
import argparse
parser = argparse.ArgumentParser(description='llmswap - Universal AI CLI')
subparsers = parser.add_subparsers(dest='command')
# Clean subcommands
ask_parser = subparsers.add_parser('ask', help='Ask a question')
chat_parser = subparsers.add_parser('chat', help='Start conversation')
review_parser = subparsers.add_parser('review', help='Review code')
Think git-style: git commit, git push, git log. Users know this pattern.
Lesson 2: Smart Defaults Beat Configuration
Wrong: Make users specify everything
llmswap ask "question" --provider anthropic --model claude-3-opus-20240229 --max-tokens 4000 --temperature 0.7
Right: Sane defaults, easy overrides
# Just works
llmswap ask "question"
# Override only what you need
llmswap ask "question" --provider openai
Implementation:
class LLMClient:
def __init__(
self,
provider=None, # Auto-detects from env vars
model=None, # Uses provider's default
temperature=None, # Sensible default per provider
max_tokens=None # Provider default
):
self.provider = provider or self._detect_provider()
self.model = model or self._get_default_model(self.provider)
# ...
90% of users never change defaults. Make the defaults excellent.
Lesson 3: Progressive Disclosure
Show complexity only when needed.
Basic usage - hide all complexity:
llmswap ask "What is Docker?"
# Clean, simple, works
Power users - full control available:
llmswap ask "What is Docker?" \
--provider anthropic \
--model claude-3-opus-20240229 \
--temperature 0.9 \
--max-tokens 2000 \
--mentor guru \
--age 25 \
--format markdown
Everyone gets what they need at their level.
Lesson 4: Error Messages Should Help, Not Scold
Bad error message:
Error: Invalid provider
What do I do with this?
Good error message:
Error: Provider 'opeanai' not found
Did you mean 'openai'?
Available providers:
- anthropic (Claude)
- openai (GPT-4)
- gemini (Google)
- groq (Fast inference)
Set provider with: --provider openai
Or set env var: export OPENAI_API_KEY="sk-..."
Implementation:
def validate_provider(provider):
valid_providers = ['anthropic', 'openai', 'gemini', 'groq', ...]
if provider not in valid_providers:
# Fuzzy match for typos
from difflib import get_close_matches
suggestions = get_close_matches(provider, valid_providers, n=1)
error = f"Error: Provider '{provider}' not found\n"
if suggestions:
error += f"\nDid you mean '{suggestions[0]}'?\n"
error += "\nAvailable providers:\n"
for p in valid_providers:
error += f" - {p}\n"
error += f"\nSet provider with: --provider {suggestions[0] if suggestions else 'anthropic'}"
raise ConfigurationError(error)
Help users fix the problem immediately.
Lesson 5: Interactive Mode is Powerful
Some tasks don’t fit one-off commands. Add interactive modes:
llmswap chat
Starting chat with Claude (anthropic)...
Type /help for commands, /quit to exit
You: What is Docker?
Claude: Docker is a containerization platform...
You: How do I install it?
Claude: Here are the installation steps...
You: /switch openai
Switched to OpenAI (gpt-4)
You: Same question
GPT-4: Docker installation varies by OS...
You: /quit
Goodbye!
The interactive loop:
def interactive_chat():
print("Starting chat... Type /help for commands, /quit to exit\n")
client = LLMClient()
history = []
while True:
try:
user_input = input("You: ").strip()
if not user_input:
continue
# Handle commands
if user_input.startswith('/'):
handle_command(user_input, client)
continue
# Regular chat
history.append({"role": "user", "content": user_input})
response = client.chat(history)
print(f"{client.provider.title()}: {response.content}\n")
history.append({"role": "assistant", "content": response.content})
except KeyboardInterrupt:
print("\nGoodbye!")
break
except Exception as e:
print(f"Error: {e}\n")
Interactive mode increased usage by 40%. People love it for exploratory tasks.
Lesson 6: Color and Formatting Matter
Without formatting:
Response from claude: Docker is a platform for developing shipping and running applications Docker provides the ability to package and run an application in a loosely isolated environment called a container
Hard to read. Walls of text.
With formatting:
from rich.console import Console
from rich.markdown import Markdown
console = Console()
# Pretty markdown rendering
console.print(Markdown(response.content))
# Colored status messages
console.print("[green]✓[/green] Response cached (saved $0.03)")
console.print("[yellow]![/yellow] Using fallback provider")
console.print("[red]✗[/red] API key not found")
Users noticed and appreciated the polish.
Lesson 7: Help Should Be Discoverable
Bad help:
llmswap --help # 200 lines of text, overwhelming
Good help:
# Top-level help - concise
llmswap --help
Usage: llmswap <command> [options]
Commands:
ask Ask a quick question
chat Start interactive chat
generate Generate code/commands
review Review code with AI
debug Debug errors
Run 'llmswap <command> --help' for command-specific help.
# Command-specific help
llmswap ask --help
Usage: llmswap ask <question> [options]
Ask a quick question and get an immediate answer.
Options:
--provider AI provider (anthropic, openai, gemini, groq)
--mentor Teaching style (guru, coach, friend)
--age Explanation level (10, 15, 25, expert)
Examples:
llmswap ask "What is Docker?"
llmswap ask "Explain decorators" --mentor guru
llmswap ask "How does TCP work?" --age 15
Organized help by what users need, when they need it.
Lesson 8: Examples Are the Best Documentation
Every command has real examples:
llmswap review --help
Examples:
# Basic code review
llmswap review app.py
# Focus on specific concerns
llmswap review app.py --focus security
llmswap review app.py --focus performance
llmswap review app.py --focus bugs
# Review from stdin
cat app.py | llmswap review
# Different language
llmswap review script.sh --language bash
Examples got copied more than documentation was read.
Lesson 9: Fail Fast with Clear Guidance
Bad startup:
llmswap ask "hello"
# Hangs for 30 seconds
Error: API request failed
Good startup:
llmswap ask "hello"
Checking configuration...
✗ No API keys found
To use llmswap, set at least one provider's API key:
export ANTHROPIC_API_KEY="sk-..."
export OPENAI_API_KEY="sk-..."
export GEMINI_API_KEY="..."
Or create ~/.llmswap/config.yaml:
provider:
default: anthropic
api_keys:
anthropic: "sk-..."
Get API keys:
- Anthropic: https://console.anthropic.com
- OpenAI: https://platform.openai.com
- Gemini: https://makersuite.google.com
Run 'llmswap providers' to see all supported providers.
Check requirements at startup. Guide users to success.
Lesson 10: Version and Cache Management
Users need visibility and control:
# Show version clearly
llmswap --version
llmswap version 5.0.2
# Cache management
llmswap cache stats
Hit rate: 82.3%
Entries: 1,247
Memory used: 47.2 MB / 100 MB
llmswap cache clear
Cache cleared. Freed 47.2 MB.
# Provider status
llmswap providers
✓ anthropic (Claude) - configured
✓ openai (GPT-4) - configured
✗ gemini (Gemini) - missing API key
✓ groq (Fast LLaMA) - configured
Give users insight into what’s happening.
The Numbers
After implementing these lessons:
Before:
- Daily active users: ~50
- Average session length: 2 minutes
- Commands per session: 1.3
- Error rate: 23%
- GitHub issues: “Too confusing”, “How do I…?”
After:
- Daily active users: ~400
- Average session length: 8 minutes
- Commands per session: 6.7
- Error rate: 4%
- GitHub issues: Feature requests, not confusion
Key Principles
1. Think like a user, not a programmer
- What would I type if I didn’t know how it worked?
- What’s the minimal information needed?
2. Optimize for the common case
- 90% of users do simple things - make those trivial
- Power features available but not required
3. Guide, don’t block
- Error messages should teach
- Examples should inspire
- Help should clarify
4. Polish matters
- Color improves comprehension
- Formatting aids readability
- Responsiveness feels professional
5. Test with real users
- Watch someone use it (don’t help!)
- Fix what confuses them
- Simplify what takes too long
The CLI Checklist
For any new CLI tool, I now ensure:
- ✓ Subcommands for different operations
- ✓ Smart defaults for 90% use case
- ✓ Help text with real examples
- ✓ Descriptive error messages
- ✓ Fuzzy matching for typos
- ✓ Interactive mode for complex workflows
- ✓ Version and status commands
- ✓ Colored output with rich/colorama
- ✓ Fail fast with clear guidance
- ✓ Progressive disclosure of complexity
The best CLIs feel invisible. Users get their work done and forget the tool was even there. That’s success.
Code: llmswap CLI is open source - github.com/sreenathmmenon/llmswap
Try it:
pip install llmswap
llmswap ask "What is the best way to design a CLI?"
The CLI will tell you itself. And it won’t be overwhelming.