Compare commits

..

No commits in common. "981683a405db2870019732e4a6a653fa27256cd7" and "e4965f93686e63af6adcee15619c5bce5446944d" have entirely different histories.

4 changed files with 63 additions and 144 deletions

View file

@ -1,4 +1,2 @@
ANTHROPIC_API_KEY="" ANTHROPIC_API_KEY=""
SYSTEM_PROMPT="" SYSTEM_PROMPT=""
KLOD_LOGS=false
KLOD_LOG_FILE=""

1
.gitignore vendored
View file

@ -1,2 +1 @@
.env .env
klod

View file

@ -1,15 +1,14 @@
# Klod # klod
Klod (pronounced \klod\\), a simple Go-based CLI tool for interacting with the Anthropic API A simple, fast CLI tool for chatting with Claude using the Anthropic API with real-time streaming responses.
## Motivation
Sometimes you just need to quickly check/ask an LLM something. I basically live in the terminal so I want to avoid the context switch to a browser. Claude code on the other hand is a bit too eager to help (which has its benefits but no necessarily for your quick short messages). Therefore I quickly stiched together klod, originally in bash, still living as [./main.sh](main.sh) at the root of the project.
## Features ## Features
- Interactive chat with Claude in your terminal
- Real-time streaming responses (see text as Claude types)
- Maintains conversation history within a session - Maintains conversation history within a session
- Configurable model and system prompts - Configurable model and system prompts
- Multi-location config file support
## Requirements ## Requirements
@ -19,21 +18,18 @@ Sometimes you just need to quickly check/ask an LLM something. I basically live
## Installation ## Installation
1. Clone and build: 1. Clone and build:
```bash ```bash
git clone https://github.com/leoalho/klod.git git clone <your-repo-url>
cd anthropic-cli cd anthropic-cli
go build go build -o klod
``` ```
2. Create a symlink for global access: 2. Create a symlink for global access:
```bash ```bash
sudo ln -s $(pwd)/klod /usr/local/bin/klod sudo ln -s $(pwd)/klod /usr/local/bin/klod
``` ```
Or install via Go: Or install via Go:
```bash ```bash
go install go install
``` ```
@ -41,7 +37,6 @@ go install
## Configuration ## Configuration
The tool looks for configuration files in the following order: The tool looks for configuration files in the following order:
1. `~/.config/klod/config` (XDG standard location) 1. `~/.config/klod/config` (XDG standard location)
2. `~/.klod.env` (home directory) 2. `~/.klod.env` (home directory)
3. `.env` in the current directory (for project-specific overrides) 3. `.env` in the current directory (for project-specific overrides)
@ -57,8 +52,6 @@ cat > ~/.config/klod/config << EOF
ANTHROPIC_API_KEY=your-api-key-here ANTHROPIC_API_KEY=your-api-key-here
MODEL=claude-sonnet-4-5-20250929 MODEL=claude-sonnet-4-5-20250929
SYSTEM_PROMPT= SYSTEM_PROMPT=
KLOD_LOGS=false
KLOD_LOG_FILE=
EOF EOF
``` ```
@ -67,29 +60,42 @@ EOF
- `ANTHROPIC_API_KEY` (required): Your Anthropic API key - `ANTHROPIC_API_KEY` (required): Your Anthropic API key
- `MODEL` (optional): Model to use (default: claude-sonnet-4-5-20250929) - `MODEL` (optional): Model to use (default: claude-sonnet-4-5-20250929)
- `SYSTEM_PROMPT` (optional): Custom system prompt for Claude - `SYSTEM_PROMPT` (optional): Custom system prompt for Claude
- `KLOD_LOGS` (optional): Enable conversation logging (default: false, set to "true" or "1" to enable)
- `KLOD_LOG_FILE` (optional): Custom log file path (default: `$XDG_STATE_HOME/klod/conversations.log` or `~/.local/state/klod/conversations.log`)
## Usage ## Usage
Start a conversation: Start a conversation:
```bash ```bash
klod Hello, how are you? klod "Hello, how are you?"
``` ```
This will: This will:
1. Send your initial message to Claude 1. Send your initial message to Claude
2. Stream the response in real-time 2. Stream the response in real-time
3. Enter an interactive chat mode where you can continue the conversation 3. Enter an interactive chat mode where you can continue the conversation
Type `exit` or `quit` to end the conversation. Type `exit` or `quit` to end the conversation.
## Examples
```bash
# Ask a quick question
klod "What is the capital of France?"
# Start a coding session
klod "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
klod "Explain quantum computing"
```
## Development ## Development
Run without building: Run without building:
```bash ```bash
go run main.go your message here go run main.go "your message here"
``` ```
## License
MIT

156
main.go
View file

@ -10,9 +10,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
"time"
"unsafe"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
@ -23,11 +20,11 @@ type Message struct {
} }
type APIRequest struct { type APIRequest struct {
Model string `json:"model"` Model string `json:"model"`
System string `json:"system,omitempty"` System string `json:"system,omitempty"`
MaxTokens int `json:"max_tokens"` MaxTokens int `json:"max_tokens"`
Messages []Message `json:"messages"` Messages []Message `json:"messages"`
Stream bool `json:"stream"` Stream bool `json:"stream"`
} }
type ContentBlock struct { type ContentBlock struct {
@ -44,9 +41,9 @@ type APIResponse struct {
} }
type StreamEvent struct { type StreamEvent struct {
Type string `json:"type"` Type string `json:"type"`
Delta *StreamDelta `json:"delta,omitempty"` Delta *StreamDelta `json:"delta,omitempty"`
Error *StreamError `json:"error,omitempty"` Error *StreamError `json:"error,omitempty"`
} }
type StreamDelta struct { type StreamDelta struct {
@ -60,62 +57,6 @@ type StreamError struct {
} }
var conversationHistory []Message var conversationHistory []Message
var loggingEnabled bool
var logFilePath string
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
func getTerminalWidth() int {
ws := &winsize{}
retCode, _, _ := syscall.Syscall(syscall.SYS_IOCTL,
uintptr(syscall.Stdin),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(ws)))
if int(retCode) == -1 {
return 80 // Default fallback
}
return int(ws.Col)
}
func printSeparator() {
width := getTerminalWidth()
fmt.Println(strings.Repeat("─", width))
}
func logConversation(message Message) error {
if !loggingEnabled {
return nil
}
// Ensure log directory exists
logDir := filepath.Dir(logFilePath)
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
// Open log file in append mode
file, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
}
defer file.Close()
// Write timestamp and message
timestamp := time.Now().Format("2006-01-02 15:04:05")
logEntry := fmt.Sprintf("[%s] %s: %s\n", timestamp, strings.ToUpper(message.Role), message.Content)
if _, err := file.WriteString(logEntry); err != nil {
return fmt.Errorf("failed to write to log file: %w", err)
}
return nil
}
func loadConfig() error { func loadConfig() error {
// Try loading config from multiple locations in order // Try loading config from multiple locations in order
@ -126,8 +67,8 @@ func loadConfig() error {
configPaths := []string{ configPaths := []string{
filepath.Join(homeDir, ".config", "klod", "config"), // XDG standard filepath.Join(homeDir, ".config", "klod", "config"), // XDG standard
filepath.Join(homeDir, ".klod.env"), // Home directory filepath.Join(homeDir, ".klod.env"), // Home directory
".env", // Current directory (for project-specific overrides) ".env", // Current directory (for project-specific overrides)
} }
var lastErr error var lastErr error
@ -163,55 +104,37 @@ func main() {
systemPrompt := os.Getenv("SYSTEM_PROMPT") systemPrompt := os.Getenv("SYSTEM_PROMPT")
// Configure logging // Get initial prompt from command-line arguments
loggingEnabled = os.Getenv("KLOD_LOGS") == "true" || os.Getenv("KLOD_LOGS") == "1" if len(os.Args) < 2 {
logFilePath = os.Getenv("KLOD_LOG_FILE") fmt.Fprintln(os.Stderr, "Usage: klod <your prompt>")
if logFilePath == "" { os.Exit(1)
// Use XDG_STATE_HOME or default to ~/.local/state per XDG spec
stateDir := os.Getenv("XDG_STATE_HOME")
if stateDir == "" {
homeDir, _ := os.UserHomeDir()
stateDir = filepath.Join(homeDir, ".local", "state")
}
// Create a unique log file for this session based on timestamp
sessionTime := time.Now().Format("2006-01-02_15-04-05")
logFilePath = filepath.Join(stateDir, "klod", "sessions", sessionTime+".log")
} }
// Get initial prompt from command-line arguments (if provided) initialPrompt := strings.Join(os.Args[1:], " ")
if len(os.Args) >= 2 {
initialPrompt := strings.Join(os.Args[1:], " ")
// Add initial user message to conversation history // Add initial user message to conversation history
userMsg := Message{ conversationHistory = append(conversationHistory, Message{
Role: "user", Role: "user",
Content: initialPrompt, Content: initialPrompt,
} })
conversationHistory = append(conversationHistory, userMsg)
logConversation(userMsg)
// Send initial message // Send initial message
printSeparator() response, err := sendMessage(apiKey, model, systemPrompt)
fmt.Print("\033[34mAssistant: \033[0m") if err != nil {
response, err := sendMessage(apiKey, model, systemPrompt) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
if err != nil { os.Exit(1)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
printSeparator()
// Add assistant's response to conversation history
assistantMsg := Message{
Role: "assistant",
Content: response,
}
conversationHistory = append(conversationHistory, assistantMsg)
logConversation(assistantMsg)
} }
// Add assistant's response to conversation history
conversationHistory = append(conversationHistory, Message{
Role: "assistant",
Content: response,
})
// Interactive conversation loop // Interactive conversation loop
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
fmt.Println()
fmt.Print("\033[32mYou (or 'exit' to quit): \033[0m") fmt.Print("\033[32mYou (or 'exit' to quit): \033[0m")
userInput, err := reader.ReadString('\n') userInput, err := reader.ReadString('\n')
@ -228,30 +151,23 @@ func main() {
} }
// Add user input to conversation history // Add user input to conversation history
userMsg := Message{ conversationHistory = append(conversationHistory, Message{
Role: "user", Role: "user",
Content: userInput, Content: userInput,
} })
conversationHistory = append(conversationHistory, userMsg)
logConversation(userMsg)
// Send message and get response // Send message and get response
printSeparator()
fmt.Print("\033[34mAssistant: \033[0m")
response, err := sendMessage(apiKey, model, systemPrompt) response, err := sendMessage(apiKey, model, systemPrompt)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
continue continue
} }
printSeparator()
// Add assistant's response to conversation history // Add assistant's response to conversation history
assistantMsg := Message{ conversationHistory = append(conversationHistory, Message{
Role: "assistant", Role: "assistant",
Content: response, Content: response,
} })
conversationHistory = append(conversationHistory, assistantMsg)
logConversation(assistantMsg)
} }
} }