feat: Add logging
Add conversation logging with per-session files and visual separators Implements optional conversation logging where each session creates its own timestamped log file. Also adds dynamic terminal-width separators between messages for better visual organization.
This commit is contained in:
parent
85ee5f1adc
commit
63d4ad95d0
4 changed files with 134 additions and 19 deletions
|
|
@ -1,2 +1,4 @@
|
||||||
ANTHROPIC_API_KEY=""
|
ANTHROPIC_API_KEY=""
|
||||||
SYSTEM_PROMPT=""
|
SYSTEM_PROMPT=""
|
||||||
|
KLOD_LOGS=false
|
||||||
|
KLOD_LOG_FILE=""
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
.env
|
.env
|
||||||
|
klod
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -19,6 +19,7 @@ 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 https://github.com/leoalho/klod.git
|
||||||
cd anthropic-cli
|
cd anthropic-cli
|
||||||
|
|
@ -26,11 +27,13 @@ go build
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
@ -38,24 +41,45 @@ 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)
|
||||||
|
|
||||||
|
### Setup your config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create the config directory
|
||||||
|
mkdir -p ~/.config/klod
|
||||||
|
|
||||||
|
# Create config file
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
### Configuration options:
|
### Configuration options:
|
||||||
|
|
||||||
- `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
|
||||||
|
|
@ -65,6 +89,7 @@ Type `exit` or `quit` to end the conversation.
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Run without building:
|
Run without building:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run main.go your message here
|
go run main.go your message here
|
||||||
```
|
```
|
||||||
|
|
|
||||||
105
main.go
105
main.go
|
|
@ -10,6 +10,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
@ -57,6 +60,62 @@ 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
|
||||||
|
|
@ -104,6 +163,21 @@ func main() {
|
||||||
|
|
||||||
systemPrompt := os.Getenv("SYSTEM_PROMPT")
|
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
|
// Get initial prompt from command-line arguments
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: klod <your prompt>")
|
fmt.Fprintln(os.Stderr, "Usage: klod <your prompt>")
|
||||||
|
|
@ -113,28 +187,34 @@ func main() {
|
||||||
initialPrompt := strings.Join(os.Args[1:], " ")
|
initialPrompt := strings.Join(os.Args[1:], " ")
|
||||||
|
|
||||||
// Add initial user message to conversation history
|
// Add initial user message to conversation history
|
||||||
conversationHistory = append(conversationHistory, Message{
|
userMsg := Message{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: initialPrompt,
|
Content: initialPrompt,
|
||||||
})
|
}
|
||||||
|
conversationHistory = append(conversationHistory, userMsg)
|
||||||
|
logConversation(userMsg)
|
||||||
|
|
||||||
// Send initial message
|
// Send initial message
|
||||||
|
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)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
printSeparator()
|
||||||
|
|
||||||
// Add assistant's response to conversation history
|
// Add assistant's response to conversation history
|
||||||
conversationHistory = append(conversationHistory, Message{
|
assistantMsg := Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: response,
|
Content: response,
|
||||||
})
|
}
|
||||||
|
conversationHistory = append(conversationHistory, assistantMsg)
|
||||||
|
logConversation(assistantMsg)
|
||||||
|
|
||||||
// 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')
|
||||||
|
|
@ -151,23 +231,30 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user input to conversation history
|
// Add user input to conversation history
|
||||||
conversationHistory = append(conversationHistory, Message{
|
userMsg := 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
|
||||||
conversationHistory = append(conversationHistory, Message{
|
assistantMsg := Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: response,
|
Content: response,
|
||||||
})
|
}
|
||||||
|
conversationHistory = append(conversationHistory, assistantMsg)
|
||||||
|
logConversation(assistantMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue