Compare commits
No commits in common. "981683a405db2870019732e4a6a653fa27256cd7" and "e4965f93686e63af6adcee15619c5bce5446944d" have entirely different histories.
981683a405
...
e4965f9368
4 changed files with 63 additions and 144 deletions
|
|
@ -1,4 +1,2 @@
|
|||
ANTHROPIC_API_KEY=""
|
||||
SYSTEM_PROMPT=""
|
||||
KLOD_LOGS=false
|
||||
KLOD_LOG_FILE=""
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1 @@
|
|||
.env
|
||||
klod
|
||||
|
|
|
|||
48
README.md
48
README.md
|
|
@ -1,15 +1,14 @@
|
|||
# Klod
|
||||
# klod
|
||||
|
||||
Klod (pronounced \klod\\), a simple Go-based CLI tool for interacting with the Anthropic API
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
|
|
@ -19,21 +18,18 @@ Sometimes you just need to quickly check/ask an LLM something. I basically live
|
|||
## Installation
|
||||
|
||||
1. Clone and build:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/leoalho/klod.git
|
||||
git clone <your-repo-url>
|
||||
cd anthropic-cli
|
||||
go build
|
||||
go build -o klod
|
||||
```
|
||||
|
||||
2. Create a symlink for global access:
|
||||
|
||||
```bash
|
||||
sudo ln -s $(pwd)/klod /usr/local/bin/klod
|
||||
```
|
||||
|
||||
Or install via Go:
|
||||
|
||||
```bash
|
||||
go install
|
||||
```
|
||||
|
|
@ -41,7 +37,6 @@ go install
|
|||
## Configuration
|
||||
|
||||
The tool looks for configuration files in the following order:
|
||||
|
||||
1. `~/.config/klod/config` (XDG standard location)
|
||||
2. `~/.klod.env` (home directory)
|
||||
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
|
||||
MODEL=claude-sonnet-4-5-20250929
|
||||
SYSTEM_PROMPT=
|
||||
KLOD_LOGS=false
|
||||
KLOD_LOG_FILE=
|
||||
EOF
|
||||
```
|
||||
|
||||
|
|
@ -67,29 +60,42 @@ EOF
|
|||
- `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
|
||||
- `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
|
||||
|
||||
Start a conversation:
|
||||
|
||||
```bash
|
||||
klod Hello, how are you?
|
||||
klod "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
|
||||
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
|
||||
|
||||
Run without building:
|
||||
|
||||
```bash
|
||||
go run main.go your message here
|
||||
go run main.go "your message here"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
110
main.go
110
main.go
|
|
@ -10,9 +10,6 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
|
@ -60,62 +57,6 @@ type StreamError struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
// Try loading config from multiple locations in order
|
||||
|
|
@ -163,55 +104,37 @@ func main() {
|
|||
|
||||
systemPrompt := os.Getenv("SYSTEM_PROMPT")
|
||||
|
||||
// Configure logging
|
||||
loggingEnabled = os.Getenv("KLOD_LOGS") == "true" || os.Getenv("KLOD_LOGS") == "1"
|
||||
logFilePath = os.Getenv("KLOD_LOG_FILE")
|
||||
if logFilePath == "" {
|
||||
// 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 len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "Usage: klod <your prompt>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get initial prompt from command-line arguments (if provided)
|
||||
if len(os.Args) >= 2 {
|
||||
initialPrompt := strings.Join(os.Args[1:], " ")
|
||||
|
||||
// Add initial user message to conversation history
|
||||
userMsg := Message{
|
||||
conversationHistory = append(conversationHistory, Message{
|
||||
Role: "user",
|
||||
Content: initialPrompt,
|
||||
}
|
||||
conversationHistory = append(conversationHistory, userMsg)
|
||||
logConversation(userMsg)
|
||||
})
|
||||
|
||||
// Send initial message
|
||||
printSeparator()
|
||||
fmt.Print("\033[34mAssistant: \033[0m")
|
||||
response, err := sendMessage(apiKey, model, systemPrompt)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
printSeparator()
|
||||
|
||||
// Add assistant's response to conversation history
|
||||
assistantMsg := Message{
|
||||
conversationHistory = append(conversationHistory, Message{
|
||||
Role: "assistant",
|
||||
Content: response,
|
||||
}
|
||||
conversationHistory = append(conversationHistory, assistantMsg)
|
||||
logConversation(assistantMsg)
|
||||
}
|
||||
})
|
||||
|
||||
// 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')
|
||||
|
|
@ -228,30 +151,23 @@ func main() {
|
|||
}
|
||||
|
||||
// Add user input to conversation history
|
||||
userMsg := Message{
|
||||
conversationHistory = append(conversationHistory, Message{
|
||||
Role: "user",
|
||||
Content: userInput,
|
||||
}
|
||||
conversationHistory = append(conversationHistory, userMsg)
|
||||
logConversation(userMsg)
|
||||
})
|
||||
|
||||
// Send message and get response
|
||||
printSeparator()
|
||||
fmt.Print("\033[34mAssistant: \033[0m")
|
||||
response, err := sendMessage(apiKey, model, systemPrompt)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
printSeparator()
|
||||
|
||||
// Add assistant's response to conversation history
|
||||
assistantMsg := Message{
|
||||
conversationHistory = append(conversationHistory, Message{
|
||||
Role: "assistant",
|
||||
Content: response,
|
||||
}
|
||||
conversationHistory = append(conversationHistory, assistantMsg)
|
||||
logConversation(assistantMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue