Add Go implementation with streaming responses and rename to prompt

- Implement streaming API responses for real-time text output
- Add multi-location config file support (.env, ~/.config/prompt/config, ~/.prompt.env)
- Rename project from anthropic-cli to prompt
- Add interactive conversation loop to bash script
- Create README with installation and usage instructions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Leo 2026-01-13 10:22:27 +02:00
parent 69ad5c3ff3
commit b7ecba9385
6 changed files with 469 additions and 22 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
ANTHROPIC_API_KEY=""
SYSTEM_PROMPT=""

101
README.md Normal file
View file

@ -0,0 +1,101 @@
# prompt
A simple, fast CLI tool for chatting with Claude using the Anthropic API with real-time streaming responses.
## Features
- Interactive chat with Claude in your terminal
- Real-time streaming responses (see text as Claude types)
- Maintains conversation history within a session
- Configurable model and system prompts
- Multi-location config file support
## Requirements
- Go 1.21 or higher
- Anthropic API key ([get one here](https://console.anthropic.com/))
## Installation
1. Clone and build:
```bash
git clone <your-repo-url>
cd anthropic-cli
go build -o prompt
```
2. Create a symlink for global access:
```bash
sudo ln -s $(pwd)/prompt /usr/local/bin/prompt
```
Or install via Go:
```bash
go install
```
## Configuration
The tool looks for configuration files in the following order:
1. `.env` in the current directory (for project-specific configs)
2. `~/.config/prompt/config` (XDG standard location)
3. `~/.prompt.env` (home directory)
### Setup your config:
```bash
# Create the config directory
mkdir -p ~/.config/prompt
# Create config file
cat > ~/.config/prompt/config << EOF
ANTHROPIC_API_KEY=your-api-key-here
MODEL=claude-sonnet-4-5-20250929
SYSTEM_PROMPT=
EOF
```
### Configuration options:
- `ANTHROPIC_API_KEY` (required): Your Anthropic API key
- `MODEL` (optional): Model to use (default: claude-sonnet-4-5-20250929)
- `SYSTEM_PROMPT` (optional): Custom system prompt for Claude
## Usage
Start a conversation:
```bash
prompt "Hello, how are you?"
```
This will:
1. Send your initial message to Claude
2. Stream the response in real-time
3. Enter an interactive chat mode where you can continue the conversation
Type `exit` or `quit` to end the conversation.
## Examples
```bash
# Ask a quick question
prompt "What is the capital of France?"
# Start a coding session
prompt "Help me write a Python function to calculate fibonacci numbers"
# Use a different model (set in config)
# Edit your config file and change MODEL=claude-opus-4-5-20251101
prompt "Explain quantum computing"
```
## Development
Run without building:
```bash
go run main.go "your message here"
```
## License
MIT

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module prompt
go 1.21
require github.com/joho/godotenv v1.5.1

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

265
main.go Normal file
View file

@ -0,0 +1,265 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
)
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type APIRequest struct {
Model string `json:"model"`
System string `json:"system,omitempty"`
MaxTokens int `json:"max_tokens"`
Messages []Message `json:"messages"`
Stream bool `json:"stream"`
}
type ContentBlock struct {
Text string `json:"text"`
Type string `json:"type"`
}
type APIResponse struct {
Content []ContentBlock `json:"content"`
Error *struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
type StreamEvent struct {
Type string `json:"type"`
Delta *StreamDelta `json:"delta,omitempty"`
Error *StreamError `json:"error,omitempty"`
}
type StreamDelta struct {
Type string `json:"type"`
Text string `json:"text"`
}
type StreamError struct {
Type string `json:"type"`
Message string `json:"message"`
}
var conversationHistory []Message
func loadConfig() error {
// Try loading config from multiple locations in order
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
configPaths := []string{
".env", // Current directory
filepath.Join(homeDir, ".config", "prompt", "config"), // XDG standard
filepath.Join(homeDir, ".prompt.env"), // Home directory
}
var lastErr error
for _, path := range configPaths {
err := godotenv.Load(path)
if err == nil {
return nil // Successfully loaded
}
lastErr = err
}
return lastErr
}
func main() {
// Load config file
err := loadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Could not load config file: %v\n", err)
}
// Get configuration from environment
apiKey := os.Getenv("ANTHROPIC_API_KEY")
if apiKey == "" {
fmt.Fprintln(os.Stderr, "Error: ANTHROPIC_API_KEY not set in .env file")
os.Exit(1)
}
model := os.Getenv("MODEL")
if model == "" {
model = "claude-sonnet-4-5-20250929"
}
systemPrompt := os.Getenv("SYSTEM_PROMPT")
// Get initial prompt from command-line arguments
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: prompt <your prompt>")
os.Exit(1)
}
initialPrompt := strings.Join(os.Args[1:], " ")
// Add initial user message to conversation history
conversationHistory = append(conversationHistory, Message{
Role: "user",
Content: initialPrompt,
})
// Send initial message
response, err := sendMessage(apiKey, model, systemPrompt)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(response)
// Add assistant's response to conversation history
conversationHistory = append(conversationHistory, Message{
Role: "assistant",
Content: response,
})
// Interactive conversation loop
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println()
fmt.Print("\033[32mYou (or 'exit' to quit): \033[0m")
userInput, err := reader.ReadString('\n')
if err != nil {
break
}
userInput = strings.TrimSpace(userInput)
// Check if user wants to exit
if userInput == "exit" || userInput == "quit" || userInput == "" {
fmt.Println("Goodbye!")
break
}
// Add user input to conversation history
conversationHistory = append(conversationHistory, Message{
Role: "user",
Content: userInput,
})
// Send message and get response
response, err := sendMessage(apiKey, model, systemPrompt)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
continue
}
fmt.Println(response)
// Add assistant's response to conversation history
conversationHistory = append(conversationHistory, Message{
Role: "assistant",
Content: response,
})
}
}
func sendMessage(apiKey, model, systemPrompt string) (string, error) {
// Build API request with streaming enabled
request := APIRequest{
Model: model,
System: systemPrompt,
MaxTokens: 2048,
Messages: conversationHistory,
Stream: true,
}
jsonData, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Make API call
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", "2023-06-01")
req.Header.Set("content-type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check for HTTP errors
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
// Read and parse streaming response
var fullResponse strings.Builder
reader := bufio.NewReader(resp.Body)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return "", fmt.Errorf("failed to read stream: %w", err)
}
line = strings.TrimSpace(line)
// Skip empty lines and non-data lines
if line == "" || !strings.HasPrefix(line, "data: ") {
continue
}
// Extract JSON data
data := strings.TrimPrefix(line, "data: ")
// Skip the [DONE] message
if data == "[DONE]" {
break
}
// Parse the event
var event StreamEvent
if err := json.Unmarshal([]byte(data), &event); err != nil {
continue // Skip malformed events
}
// Check for errors
if event.Error != nil {
return "", fmt.Errorf("API error: %s - %s", event.Error.Type, event.Error.Message)
}
// Print text deltas as they arrive
if event.Type == "content_block_delta" && event.Delta != nil && event.Delta.Type == "text_delta" {
fmt.Print(event.Delta.Text)
fullResponse.WriteString(event.Delta.Text)
}
}
fmt.Println() // Add newline after streaming response
return fullResponse.String(), nil
}

94
main.sh
View file

@ -4,32 +4,104 @@ source .env
PROMPT="$*" PROMPT="$*"
TEMP_FILE=$(mktemp) TEMP_FILE=$(mktemp)
prompt=$(echo $PROMPT)
ANTHROPIC_MODEL=${MODEL:-"claude-sonnet-4-5-20250929"} ANTHROPIC_MODEL=${MODEL:-"claude-sonnet-4-5-20250929"}
SYSTEM_PROMPT=${SYSTEM_PROMPT:-""} SYSTEM_PROMPT=${SYSTEM_PROMPT:-""}
JSON_PAYLOAD=$(jq -n \ # Initialize conversation history array
declare -a CONVERSATION_HISTORY=()
# Function to build messages JSON array from conversation history
build_messages_json() {
local messages_json="["
local first=true
for ((i=0; i<${#CONVERSATION_HISTORY[@]}; i+=2)); do
if [ "$first" = true ]; then
first=false
else
messages_json+=","
fi
# Add user message
local user_msg="${CONVERSATION_HISTORY[$i]}"
user_msg="${user_msg//\\/\\\\}" # Escape backslashes
user_msg="${user_msg//\"/\\\"}" # Escape quotes
user_msg="${user_msg//$'\n'/\\n}" # Escape newlines
messages_json+="{\"role\":\"user\",\"content\":\"$user_msg\"}"
# Add assistant message if it exists
if [ $((i+1)) -lt ${#CONVERSATION_HISTORY[@]} ]; then
local assistant_msg="${CONVERSATION_HISTORY[$((i+1))]}"
assistant_msg="${assistant_msg//\\/\\\\}"
assistant_msg="${assistant_msg//\"/\\\"}"
assistant_msg="${assistant_msg//$'\n'/\\n}"
messages_json+=",{\"role\":\"assistant\",\"content\":\"$assistant_msg\"}"
fi
done
messages_json+="]"
echo "$messages_json"
}
# Function to send message and get response
send_message() {
local messages_json=$(build_messages_json)
JSON_PAYLOAD=$(jq -n \
--arg model "$ANTHROPIC_MODEL" \ --arg model "$ANTHROPIC_MODEL" \
--arg prompt "$PROMPT" \
--arg system_prompt "$SYSTEM_PROMPT" \ --arg system_prompt "$SYSTEM_PROMPT" \
--argjson messages "$messages_json" \
'{ '{
model: $model, model: $model,
system: $system_prompt, system: $system_prompt,
max_tokens: 2048, max_tokens: 2048,
messages: [{role: "user", content: $prompt}] messages: $messages
}') }')
# Show loading message # Show loading message
echo -ne "\033[36mFetching response from Claude...\033[0m" echo -ne "\033[36mFetching response from Claude...\033[0m"
curl -s https://api.anthropic.com/v1/messages \ curl -s https://api.anthropic.com/v1/messages \
--header "x-api-key: $ANTHROPIC_API_KEY" \ --header "x-api-key: $ANTHROPIC_API_KEY" \
--header "anthropic-version: 2023-06-01" \ --header "anthropic-version: 2023-06-01" \
--header "content-type: application/json" \ --header "content-type: application/json" \
--data "$JSON_PAYLOAD" > "$TEMP_FILE" --data "$JSON_PAYLOAD" > "$TEMP_FILE"
# Clear loading message # Clear loading message
echo -ne "\r\033[K" echo -ne "\r\033[K"
# Display response # Extract and display response
jq -r '.content[0].text' "$TEMP_FILE" local response=$(jq -r '.content[0].text' "$TEMP_FILE")
echo "$response"
# Add assistant's response to conversation history
CONVERSATION_HISTORY+=("$response")
}
# Add initial user prompt to conversation history
CONVERSATION_HISTORY+=("$PROMPT")
# Send initial message
send_message
# Conversation loop
while true; do
echo ""
echo -ne "\033[32mYou (or 'exit' to quit): \033[0m"
read -e user_input
# Check if user wants to exit
if [[ "$user_input" == "exit" ]] || [[ "$user_input" == "quit" ]] || [[ -z "$user_input" ]]; then
echo "Goodbye!"
break
fi
# Add user input to conversation history
CONVERSATION_HISTORY+=("$user_input")
# Send message and get response
send_message
done
# Cleanup
rm -f "$TEMP_FILE"