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:
parent
69ad5c3ff3
commit
b7ecba9385
6 changed files with 469 additions and 22 deletions
2
.env.example
Normal file
2
.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ANTHROPIC_API_KEY=""
|
||||
SYSTEM_PROMPT=""
|
||||
101
README.md
Normal file
101
README.md
Normal 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
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module prompt
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
||||
2
go.sum
Normal file
2
go.sum
Normal 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
265
main.go
Normal 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
|
||||
}
|
||||
116
main.sh
116
main.sh
|
|
@ -4,32 +4,104 @@ source .env
|
|||
|
||||
PROMPT="$*"
|
||||
TEMP_FILE=$(mktemp)
|
||||
prompt=$(echo $PROMPT)
|
||||
ANTHROPIC_MODEL=${MODEL:-"claude-sonnet-4-5-20250929"}
|
||||
SYSTEM_PROMPT=${SYSTEM_PROMPT:-""}
|
||||
|
||||
JSON_PAYLOAD=$(jq -n \
|
||||
--arg model "$ANTHROPIC_MODEL" \
|
||||
--arg prompt "$PROMPT" \
|
||||
--arg system_prompt "$SYSTEM_PROMPT" \
|
||||
'{
|
||||
model: $model,
|
||||
system: $system_prompt,
|
||||
max_tokens: 2048,
|
||||
messages: [{role: "user", content: $prompt}]
|
||||
}')
|
||||
# Initialize conversation history array
|
||||
declare -a CONVERSATION_HISTORY=()
|
||||
|
||||
# Show loading message
|
||||
echo -ne "\033[36mFetching response from Claude...\033[0m"
|
||||
# Function to build messages JSON array from conversation history
|
||||
build_messages_json() {
|
||||
local messages_json="["
|
||||
local first=true
|
||||
|
||||
curl -s https://api.anthropic.com/v1/messages \
|
||||
--header "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
--header "anthropic-version: 2023-06-01" \
|
||||
--header "content-type: application/json" \
|
||||
--data "$JSON_PAYLOAD" > "$TEMP_FILE"
|
||||
for ((i=0; i<${#CONVERSATION_HISTORY[@]}; i+=2)); do
|
||||
if [ "$first" = true ]; then
|
||||
first=false
|
||||
else
|
||||
messages_json+=","
|
||||
fi
|
||||
|
||||
# Clear loading message
|
||||
echo -ne "\r\033[K"
|
||||
# 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\"}"
|
||||
|
||||
# Display response
|
||||
jq -r '.content[0].text' "$TEMP_FILE"
|
||||
# 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 system_prompt "$SYSTEM_PROMPT" \
|
||||
--argjson messages "$messages_json" \
|
||||
'{
|
||||
model: $model,
|
||||
system: $system_prompt,
|
||||
max_tokens: 2048,
|
||||
messages: $messages
|
||||
}')
|
||||
|
||||
# Show loading message
|
||||
echo -ne "\033[36mFetching response from Claude...\033[0m"
|
||||
|
||||
curl -s https://api.anthropic.com/v1/messages \
|
||||
--header "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
--header "anthropic-version: 2023-06-01" \
|
||||
--header "content-type: application/json" \
|
||||
--data "$JSON_PAYLOAD" > "$TEMP_FILE"
|
||||
|
||||
# Clear loading message
|
||||
echo -ne "\r\033[K"
|
||||
|
||||
# Extract and display response
|
||||
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue