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
|
||||||
|
}
|
||||||
82
main.sh
82
main.sh
|
|
@ -4,19 +4,58 @@ 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:-""}
|
||||||
|
|
||||||
|
# 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 \
|
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
|
||||||
|
|
@ -31,5 +70,38 @@ curl -s https://api.anthropic.com/v1/messages \
|
||||||
# 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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue