Compare commits
No commits in common. "4f6e0df69838ab877f236fc8553e8c0605afe7c4" and "dc5665cdf8feec5c408a8eef9fcb392f3ff56870" have entirely different histories.
4f6e0df698
...
dc5665cdf8
@ -1,78 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master, main]
|
|
||||||
pull_request:
|
|
||||||
branches: [master, main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24'
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Download dependencies
|
|
||||||
run: go mod download
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: go test -v -race -coverprofile=coverage.out ./...
|
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
file: ./coverage.out
|
|
||||||
fail_ci_if_error: false
|
|
||||||
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24'
|
|
||||||
|
|
||||||
- name: golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@v4
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: make build
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: grokkit-linux-amd64
|
|
||||||
path: build/grokkit
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.24'
|
|
||||||
|
|
||||||
- name: Build for multiple platforms
|
|
||||||
run: |
|
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o build/grokkit-linux-amd64 .
|
|
||||||
GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -trimpath -o build/grokkit-linux-arm64 .
|
|
||||||
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o build/grokkit-darwin-amd64 .
|
|
||||||
GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -trimpath -o build/grokkit-darwin-arm64 .
|
|
||||||
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o build/grokkit-windows-amd64.exe .
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
build/grokkit-*
|
|
||||||
generate_release_notes: true
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,5 +4,3 @@ grokkit
|
|||||||
*.bak
|
*.bak
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
.env
|
|
||||||
chat_history.json
|
|
||||||
35
Makefile
35
Makefile
@ -1,35 +0,0 @@
|
|||||||
.PHONY: test test-cover test-agent build install clean
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test ./... -v
|
|
||||||
|
|
||||||
test-cover:
|
|
||||||
@mkdir -p build
|
|
||||||
go test ./... -coverprofile=build/coverage.out
|
|
||||||
go tool cover -html=build/coverage.out -o build/coverage.html
|
|
||||||
@echo "✅ Coverage report: open build/coverage.html in your browser"
|
|
||||||
|
|
||||||
test-agent:
|
|
||||||
go test -run TestAgent ./... -v
|
|
||||||
|
|
||||||
build:
|
|
||||||
@mkdir -p build
|
|
||||||
go build -ldflags "-s -w" -trimpath -o build/grokkit .
|
|
||||||
|
|
||||||
install: build
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
cp build/grokkit ~/.local/bin/grokkit
|
|
||||||
chmod +x ~/.local/bin/grokkit
|
|
||||||
@echo "✅ grokkit installed to ~/.local/bin"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf build/
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo " test Run all tests"
|
|
||||||
@echo " test-cover Run tests + generate HTML coverage report"
|
|
||||||
@echo " test-agent Run only agent tests"
|
|
||||||
@echo " build Build optimized binary"
|
|
||||||
@echo " install Build and install to ~/.local/bin"
|
|
||||||
@echo " clean Remove build artifacts"
|
|
||||||
475
README.md
475
README.md
@ -1,475 +0,0 @@
|
|||||||
# Grokkit
|
|
||||||
|
|
||||||
Grokkit is a fast Go CLI integrating Grok AI with git workflows and general chat/edit functionality.
|
|
||||||
|
|
||||||
[]()
|
|
||||||
[]()
|
|
||||||
[]()
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set your API key
|
|
||||||
export XAI_API_KEY=sk-...
|
|
||||||
|
|
||||||
# Install from source
|
|
||||||
git clone https://github.com/yourusername/grokkit.git
|
|
||||||
cd grokkit
|
|
||||||
make install
|
|
||||||
|
|
||||||
# Or build locally
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Verify installation
|
|
||||||
grokkit --help
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Table of Contents
|
|
||||||
|
|
||||||
- [Commands](#commands)
|
|
||||||
- [Configuration](#configuration)
|
|
||||||
- [Workflows](#workflows)
|
|
||||||
- [Shell Completions](#shell-completions)
|
|
||||||
- [Features](#features)
|
|
||||||
- [Development](#development)
|
|
||||||
- [Documentation](#documentation)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### 💬 `grokkit chat`
|
|
||||||
Interactive CLI chat with Grok. Features persistent history across sessions.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grokkit chat # Start chat session
|
|
||||||
grokkit chat -m grok-beta # Use specific model
|
|
||||||
grokkit chat --debug # Enable debug logging
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tips:**
|
|
||||||
- Type `/quit`, `/q`, or `exit` to exit
|
|
||||||
- History is saved automatically between sessions
|
|
||||||
- Use `--debug` to see API request timing
|
|
||||||
|
|
||||||
### ✏️ `grokkit edit FILE "instruction"`
|
|
||||||
AI-powered file editing with preview and automatic backups.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic usage
|
|
||||||
grokkit edit main.go "add error handling to all functions"
|
|
||||||
|
|
||||||
# Complex refactoring
|
|
||||||
grokkit edit server.go "convert this to use context for cancellation"
|
|
||||||
|
|
||||||
# Add features
|
|
||||||
grokkit edit api.go "add rate limiting middleware"
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
grokkit edit utils.go "add detailed docstrings to all exported functions"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Safety features:**
|
|
||||||
- Creates `.bak` backup before any changes
|
|
||||||
- Shows preview with diff-style output
|
|
||||||
- Requires confirmation before applying
|
|
||||||
- Uses silent streaming (no console spam)
|
|
||||||
|
|
||||||
### 📝 `grokkit commitmsg`
|
|
||||||
Generate conventional commit messages from staged changes.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
grokkit commitmsg # Generate message only
|
|
||||||
grokkit commitmsg -m grok-4 # Use specific model
|
|
||||||
```
|
|
||||||
|
|
||||||
Output format: `type(scope): subject\n\nbody`
|
|
||||||
|
|
||||||
### ✅ `grokkit commit`
|
|
||||||
Generate commit message and commit in one step.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
grokkit commit # Generate + confirm + commit
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔍 `grokkit review`
|
|
||||||
AI code review of staged or unstaged changes.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Review staged changes
|
|
||||||
git add feature.go
|
|
||||||
grokkit review
|
|
||||||
|
|
||||||
# Review all changes
|
|
||||||
grokkit review
|
|
||||||
|
|
||||||
# Get detailed review
|
|
||||||
grokkit review --debug # See API timing info
|
|
||||||
```
|
|
||||||
|
|
||||||
Output includes:
|
|
||||||
- Summary of changes
|
|
||||||
- 3-5 actionable improvements
|
|
||||||
- Potential bugs or issues
|
|
||||||
- Best practice suggestions
|
|
||||||
|
|
||||||
### 📋 `grokkit pr-describe`
|
|
||||||
Generate comprehensive PR descriptions.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From current branch vs main
|
|
||||||
grokkit pr-describe
|
|
||||||
|
|
||||||
# With specific model
|
|
||||||
grokkit pr-describe -m grok-4
|
|
||||||
```
|
|
||||||
|
|
||||||
Output includes:
|
|
||||||
- Title suggestion
|
|
||||||
- Summary of changes
|
|
||||||
- Motivation/context
|
|
||||||
- Testing notes
|
|
||||||
|
|
||||||
### 📜 `grokkit history`
|
|
||||||
Summarize recent git commits.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grokkit history # Last 10 commits
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🤖 `grokkit agent`
|
|
||||||
Multi-file agent for complex refactoring (experimental).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grokkit agent "refactor authentication to use JWT"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔧 `grokkit lint FILE`
|
|
||||||
Automatically detect language, run linter, and apply AI-suggested fixes.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Just check for issues (no fixes)
|
|
||||||
grokkit lint main.go --dry-run
|
|
||||||
|
|
||||||
# Interactive fix (preview + confirmation)
|
|
||||||
grokkit lint app.py
|
|
||||||
|
|
||||||
# Auto-fix without confirmation
|
|
||||||
grokkit lint server.js --auto-fix
|
|
||||||
|
|
||||||
# Use specific model
|
|
||||||
grokkit lint script.rb -m grok-4
|
|
||||||
```
|
|
||||||
|
|
||||||
**Supported languages:**
|
|
||||||
- **Go** (golangci-lint, go vet)
|
|
||||||
- **Python** (pylint, flake8, ruff)
|
|
||||||
- **JavaScript/JSX** (eslint)
|
|
||||||
- **TypeScript/TSX** (eslint, tsc)
|
|
||||||
- **Rust** (clippy)
|
|
||||||
- **Ruby** (rubocop)
|
|
||||||
- **Java** (checkstyle)
|
|
||||||
- **C/C++** (clang-tidy)
|
|
||||||
- **Shell** (shellcheck)
|
|
||||||
|
|
||||||
**Safety features:**
|
|
||||||
- Creates `.bak` backup before changes
|
|
||||||
- Shows preview of fixes
|
|
||||||
- Verifies fixes by re-running linter
|
|
||||||
- Requires confirmation (unless `--auto-fix`)
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export XAI_API_KEY=sk-... # Required: Your xAI API key
|
|
||||||
```
|
|
||||||
|
|
||||||
### Config File (Optional)
|
|
||||||
|
|
||||||
Create `~/.config/grokkit/config.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
timeout = 60 # API timeout in seconds
|
|
||||||
log_level = "info" # debug, info, warn, error
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
beta = "grok-beta-2"
|
|
||||||
fast = "grok-4-mini"
|
|
||||||
|
|
||||||
[chat]
|
|
||||||
history_file = "~/.config/grokkit/chat_history.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
**See also:** [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for advanced configuration options.
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
Logs are written to `~/.config/grokkit/grokkit.log` in JSON format.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View logs in real-time
|
|
||||||
tail -f ~/.config/grokkit/grokkit.log
|
|
||||||
|
|
||||||
# Find errors
|
|
||||||
cat ~/.config/grokkit/grokkit.log | jq 'select(.level=="ERROR")'
|
|
||||||
|
|
||||||
# Track API performance
|
|
||||||
cat ~/.config/grokkit/grokkit.log | jq 'select(.msg=="API request completed") | {model, duration_ms, response_length}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflows
|
|
||||||
|
|
||||||
### Git Workflow Integration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Make changes
|
|
||||||
vim src/api.go
|
|
||||||
|
|
||||||
# 2. Review with AI
|
|
||||||
git add src/api.go
|
|
||||||
grokkit review
|
|
||||||
|
|
||||||
# 3. Fix issues, then commit
|
|
||||||
git add src/api.go
|
|
||||||
grokkit commit
|
|
||||||
|
|
||||||
# 4. Generate PR description
|
|
||||||
grokkit pr-describe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Refactoring Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Chat to plan approach
|
|
||||||
grokkit chat
|
|
||||||
> "How should I refactor this authentication code to use middleware?"
|
|
||||||
|
|
||||||
# 2. Apply changes with edit
|
|
||||||
grokkit edit auth.go "implement middleware pattern as discussed"
|
|
||||||
|
|
||||||
# 3. Review changes
|
|
||||||
grokkit review
|
|
||||||
|
|
||||||
# 4. Commit
|
|
||||||
grokkit commit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Describe issue in chat
|
|
||||||
grokkit chat --debug
|
|
||||||
> "I'm getting a nil pointer error in handler.go:42"
|
|
||||||
|
|
||||||
# 2. Apply suggested fixes
|
|
||||||
grokkit edit handler.go "add nil checks before dereferencing user object"
|
|
||||||
|
|
||||||
# 3. Verify and commit
|
|
||||||
grokkit review
|
|
||||||
grokkit commit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Batch File Editing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Edit multiple files with consistent changes
|
|
||||||
for file in src/*.go; do
|
|
||||||
grokkit edit "$file" "add context parameter to all exported functions"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Review all changes together
|
|
||||||
grokkit review
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Check a file for linting issues
|
|
||||||
grokkit lint app.py --dry-run
|
|
||||||
|
|
||||||
# 2. Apply AI-suggested fixes with preview
|
|
||||||
grokkit lint app.py
|
|
||||||
|
|
||||||
# 3. Auto-fix multiple files
|
|
||||||
for file in src/*.js; do
|
|
||||||
grokkit lint "$file" --auto-fix
|
|
||||||
done
|
|
||||||
|
|
||||||
# 4. Review and commit
|
|
||||||
grokkit review
|
|
||||||
grokkit commit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Shell Completions
|
|
||||||
|
|
||||||
Generate shell completions for faster command entry:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Bash
|
|
||||||
grokkit completion bash | sudo tee /etc/bash_completion.d/grokkit
|
|
||||||
|
|
||||||
# Zsh (oh-my-zsh)
|
|
||||||
grokkit completion zsh > ~/.oh-my-zsh/completions/_grokkit
|
|
||||||
|
|
||||||
# Fish
|
|
||||||
grokkit completion fish > ~/.config/fish/completions/grokkit.fish
|
|
||||||
|
|
||||||
# PowerShell
|
|
||||||
grokkit completion powershell | Out-String | Invoke-Expression
|
|
||||||
```
|
|
||||||
|
|
||||||
## Flags
|
|
||||||
|
|
||||||
### Global Flags (work with all commands)
|
|
||||||
|
|
||||||
| Flag | Short | Description |
|
|
||||||
|------|-------|-------------|
|
|
||||||
| `--model` | `-m` | Override model (e.g., grok-4, grok-beta) |
|
|
||||||
| `--debug` | | Enable debug logging (stderr + file) |
|
|
||||||
| `--verbose` | `-v` | Enable verbose logging |
|
|
||||||
| `--help` | `-h` | Show help |
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use different model
|
|
||||||
grokkit chat -m grok-beta
|
|
||||||
|
|
||||||
# Debug API issues
|
|
||||||
grokkit edit main.go "refactor" --debug
|
|
||||||
|
|
||||||
# Verbose logging
|
|
||||||
grokkit review -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Core Features
|
|
||||||
- ✅ **Structured logging with slog** - JSON logs with request tracing, timing, and context
|
|
||||||
- ✅ **Context-aware HTTP requests** - 60s timeout, proper cancellation
|
|
||||||
- ✅ **Comprehensive error handling** - Custom error types with context
|
|
||||||
- ✅ **Persistent chat history** - Never lose your conversations
|
|
||||||
- ✅ **Configurable parameters** - Temperature, timeout, model selection
|
|
||||||
- ✅ **Shell completions** - Bash, Zsh, Fish, PowerShell
|
|
||||||
- ✅ **Safe file editing** - Automatic backups, preview, confirmation
|
|
||||||
- ✅ **Git workflow integration** - Commit messages, reviews, PR descriptions
|
|
||||||
- ✅ **Multi-language linting** - 9 languages supported with AI-powered fixes
|
|
||||||
|
|
||||||
### Quality & Testing
|
|
||||||
- ✅ **Test coverage 72%** - Comprehensive unit tests
|
|
||||||
- ✅ **CI/CD with Gitea Actions** - Automated testing and builds
|
|
||||||
- ✅ **Interface-based design** - Testable and maintainable
|
|
||||||
- ✅ **Zero external dependencies** - Only stdlib + well-known libs
|
|
||||||
|
|
||||||
### Observability
|
|
||||||
|
|
||||||
**Logged metrics:**
|
|
||||||
- API request/response timing and sizes
|
|
||||||
- Git command execution and output
|
|
||||||
- File operations with size tracking
|
|
||||||
- Error context with full details
|
|
||||||
|
|
||||||
**Example log entry:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"time": "2026-03-01T10:30:47.890Z",
|
|
||||||
"level": "INFO",
|
|
||||||
"msg": "API request completed",
|
|
||||||
"model": "grok-4",
|
|
||||||
"response_length": 1024,
|
|
||||||
"chunks_received": 42,
|
|
||||||
"duration_ms": 2434,
|
|
||||||
"duration": "2.434s"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run tests
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Run tests with coverage report
|
|
||||||
make test-cover
|
|
||||||
open build/coverage.html
|
|
||||||
|
|
||||||
# Run specific tests
|
|
||||||
go test -run TestEditCommand ./cmd -v
|
|
||||||
|
|
||||||
# Build binary
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Install locally
|
|
||||||
make install
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
make clean
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
grokkit/
|
|
||||||
├── cmd/ # CLI commands (cobra)
|
|
||||||
├── config/ # Configuration management (viper)
|
|
||||||
├── internal/
|
|
||||||
│ ├── errors/ # Custom error types
|
|
||||||
│ ├── git/ # Git operations wrapper
|
|
||||||
│ ├── grok/ # Grok API client
|
|
||||||
│ └── logger/ # Structured logging (slog)
|
|
||||||
├── docs/ # Documentation
|
|
||||||
├── .gitea/ # CI/CD workflows
|
|
||||||
└── Makefile # Build automation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- 📖 [Troubleshooting Guide](docs/TROUBLESHOOTING.md) - Common issues and solutions
|
|
||||||
- 🏗️ [Architecture Overview](docs/ARCHITECTURE.md) - System design and patterns
|
|
||||||
- ⚙️ [Configuration Guide](docs/CONFIGURATION.md) - Advanced configuration options
|
|
||||||
|
|
||||||
## API Usage & Costs
|
|
||||||
|
|
||||||
Grokkit uses the xAI Grok API. Be aware:
|
|
||||||
- API calls consume credits/tokens
|
|
||||||
- Default timeout: 60 seconds
|
|
||||||
- Streaming reduces perceived latency
|
|
||||||
- Consider using model aliases for different use cases (fast/expensive)
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Go 1.24+ (for building)
|
|
||||||
- Git (for git-related commands)
|
|
||||||
- XAI API key
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Common issues:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API key not set
|
|
||||||
Error: XAI_API_KEY environment variable not set
|
|
||||||
→ Solution: export XAI_API_KEY=sk-your-key
|
|
||||||
|
|
||||||
# Request timeout
|
|
||||||
Error: Request failed: context deadline exceeded
|
|
||||||
→ Solution: Increase timeout in config.toml or check network
|
|
||||||
|
|
||||||
# Permission denied on log file
|
|
||||||
→ Solution: chmod 644 ~/.config/grokkit/grokkit.log
|
|
||||||
```
|
|
||||||
|
|
||||||
**See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for more details.**
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[Unlicense](https://unlicense.org/) - Free for any use, no attribution required.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Made with ❤️ using Grok AI**
|
|
||||||
82
REAMDE.md
Normal file
82
REAMDE.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Grokkit
|
||||||
|
|
||||||
|
Grokkit is a fast Go CLI integrating Grok AI with git workflows and general chat/edit.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export XAI_API_KEY=sk-...
|
||||||
|
|
||||||
|
go install gmgauthier.com/grokkit@grokkit # or build locally
|
||||||
|
|
||||||
|
grokkit --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Config (optional)
|
||||||
|
|
||||||
|
`~/.config/grokkit/grokkit.yaml`:
|
||||||
|
```yaml
|
||||||
|
model: grok-4
|
||||||
|
chat:
|
||||||
|
history_file: ~/.config/grokkit/chat_history.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### 💬 `grokkit chat`
|
||||||
|
Interactive TUI chat with Grok (bubbletea: styled scrolling viewport, live streaming, mouse support).
|
||||||
|
|
||||||
|
```
|
||||||
|
grokkit chat
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 `grokkit commitmsg`
|
||||||
|
Generate conventional commit from staged changes.
|
||||||
|
|
||||||
|
```
|
||||||
|
git add .
|
||||||
|
grokkit commitmsg
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ `grokkit commit`
|
||||||
|
Generate & commit.
|
||||||
|
|
||||||
|
```
|
||||||
|
grokkit commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔍 `grokkit review`
|
||||||
|
AI code review of staged changes.
|
||||||
|
|
||||||
|
```
|
||||||
|
grokkit review
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 `grokkit pr-describe`
|
||||||
|
PR description from branch vs main.
|
||||||
|
|
||||||
|
```
|
||||||
|
grokkit pr-describe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📜 `grokkit history`
|
||||||
|
Summarize recent commits.
|
||||||
|
|
||||||
|
```
|
||||||
|
grokkit history
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✏️ `grokkit edit FILE "instruction"`
|
||||||
|
AI edit file.
|
||||||
|
|
||||||
|
```
|
||||||
|
grokkit edit main.go "add error handling"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
`--model, -m`: grok-4 (default), etc.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Unlicense](https://unlicense.org/)
|
||||||
24
UNLICENSE
24
UNLICENSE
@ -1,24 +0,0 @@
|
|||||||
This is free and unencumbered software released into the public domain.
|
|
||||||
|
|
||||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
||||||
distribute this software, either in source code form or as a compiled
|
|
||||||
binary, for any purpose, commercial or non-commercial, and by any
|
|
||||||
means.
|
|
||||||
|
|
||||||
In jurisdictions that recognize copyright laws, the author or authors
|
|
||||||
of this software dedicate any and all copyright interest in the
|
|
||||||
software to the public domain. We make this dedication for the benefit
|
|
||||||
of the public at large and to the detriment of our heirs and
|
|
||||||
successors. We intend this dedication to be an overt act of
|
|
||||||
relinquishment in perpetuity of all present and future rights to this
|
|
||||||
software under copyright law.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
||||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
For more information, please refer to <https://unlicense.org>
|
|
||||||
18
cmd/agent.go
18
cmd/agent.go
@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
@ -109,20 +108,3 @@ var agentCmd = &cobra.Command{
|
|||||||
color.Green("\n🎉 Agent mode complete! All changes applied.")
|
color.Green("\n🎉 Agent mode complete! All changes applied.")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanCodeResponse removes markdown fences and returns pure code content
|
|
||||||
func CleanCodeResponse(text string) string {
|
|
||||||
fence := "```"
|
|
||||||
|
|
||||||
// Remove any line that starts with the fence (opening fence, possibly with language tag)
|
|
||||||
text = regexp.MustCompile(`(?m)^`+regexp.QuoteMeta(fence)+`.*$`).ReplaceAllString(text, "")
|
|
||||||
|
|
||||||
// Remove any line that is just the fence (closing fence)
|
|
||||||
text = regexp.MustCompile(`(?m)^`+regexp.QuoteMeta(fence)+`\s*$`).ReplaceAllString(text, "")
|
|
||||||
|
|
||||||
// Trim only leading and trailing whitespace.
|
|
||||||
// Do NOT collapse internal blank lines — they are intentional in code.
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAgentCommand_PlanGeneration(t *testing.T) {
|
|
||||||
t.Log("Agent plan generation test placeholder — ready for expansion")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAgentCommand_CleanCodeResponseIntegration(t *testing.T) {
|
|
||||||
input := "```go\npackage main\nfunc main() {}\n```"
|
|
||||||
expected := "package main\nfunc main() {}"
|
|
||||||
|
|
||||||
got := CleanCodeResponse(input)
|
|
||||||
if got != expected {
|
|
||||||
t.Errorf("CleanCodeResponse() = %q, want %q", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
cmd/chat.go
62
cmd/chat.go
@ -2,62 +2,16 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
"gmgauthier.com/grokkit/internal/grok"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChatHistory struct {
|
|
||||||
Messages []map[string]string `json:"messages"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadChatHistory() []map[string]string {
|
|
||||||
histFile := getChatHistoryFile()
|
|
||||||
data, err := os.ReadFile(histFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var hist ChatHistory
|
|
||||||
if err := json.Unmarshal(data, &hist); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return hist.Messages
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveChatHistory(messages []map[string]string) error {
|
|
||||||
histFile := getChatHistoryFile()
|
|
||||||
hist := ChatHistory{Messages: messages}
|
|
||||||
data, err := json.MarshalIndent(hist, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(histFile, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getChatHistoryFile() string {
|
|
||||||
configFile := viper.GetString("chat.history_file")
|
|
||||||
if configFile != "" {
|
|
||||||
return configFile
|
|
||||||
}
|
|
||||||
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
if home == "" {
|
|
||||||
home = "."
|
|
||||||
}
|
|
||||||
histDir := filepath.Join(home, ".config", "grokkit")
|
|
||||||
os.MkdirAll(histDir, 0755)
|
|
||||||
return filepath.Join(histDir, "chat_history.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatCmd = &cobra.Command{
|
var chatCmd = &cobra.Command{
|
||||||
Use: "chat",
|
Use: "chat",
|
||||||
Short: "Simple interactive CLI chat with Grok (full history + streaming)",
|
Short: "Simple interactive CLI chat with Grok (full history + streaming)",
|
||||||
@ -73,18 +27,7 @@ var chatCmd = &cobra.Command{
|
|||||||
"content": fmt.Sprintf("You are Grok 4, the latest and most powerful model from xAI (2026). You are currently running as `%s`. Be helpful, truthful, and a little irreverent. Never claim to be an older model.", model),
|
"content": fmt.Sprintf("You are Grok 4, the latest and most powerful model from xAI (2026). You are currently running as `%s`. Be helpful, truthful, and a little irreverent. Never claim to be an older model.", model),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load history or start fresh
|
history := []map[string]string{systemPrompt}
|
||||||
history := loadChatHistory()
|
|
||||||
if history == nil {
|
|
||||||
history = []map[string]string{systemPrompt}
|
|
||||||
} else {
|
|
||||||
// Update system prompt in loaded history
|
|
||||||
if len(history) > 0 && history[0]["role"] == "system" {
|
|
||||||
history[0] = systemPrompt
|
|
||||||
} else {
|
|
||||||
history = append([]map[string]string{systemPrompt}, history...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
|
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
|
||||||
color.Cyan("│ Grokkit Chat — Model: %s │", model)
|
color.Cyan("│ Grokkit Chat — Model: %s │", model)
|
||||||
@ -115,9 +58,6 @@ var chatCmd = &cobra.Command{
|
|||||||
|
|
||||||
history = append(history, map[string]string{"role": "assistant", "content": reply})
|
history = append(history, map[string]string{"role": "assistant", "content": reply})
|
||||||
|
|
||||||
// Save history after each exchange
|
|
||||||
_ = saveChatHistory(history)
|
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,93 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetChatHistoryFile(t *testing.T) {
|
|
||||||
// Save original HOME
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
|
|
||||||
histFile := getChatHistoryFile()
|
|
||||||
expected := filepath.Join(tmpDir, ".config", "grokkit", "chat_history.json")
|
|
||||||
|
|
||||||
if histFile != expected {
|
|
||||||
t.Errorf("getChatHistoryFile() = %q, want %q", histFile, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadChatHistory_NoFile(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
history := loadChatHistory()
|
|
||||||
if history != nil {
|
|
||||||
t.Errorf("loadChatHistory() expected nil for non-existent file, got %v", history)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveAndLoadChatHistory(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
// Create test messages
|
|
||||||
messages := []map[string]string{
|
|
||||||
{"role": "system", "content": "test system message"},
|
|
||||||
{"role": "user", "content": "hello"},
|
|
||||||
{"role": "assistant", "content": "hi there"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
err := saveChatHistory(messages)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("saveChatHistory() error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load
|
|
||||||
loaded := loadChatHistory()
|
|
||||||
if loaded == nil {
|
|
||||||
t.Fatal("loadChatHistory() returned nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(loaded) != len(messages) {
|
|
||||||
t.Errorf("loadChatHistory() length = %d, want %d", len(loaded), len(messages))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify content
|
|
||||||
for i, msg := range loaded {
|
|
||||||
if msg["role"] != messages[i]["role"] {
|
|
||||||
t.Errorf("Message[%d] role = %q, want %q", i, msg["role"], messages[i]["role"])
|
|
||||||
}
|
|
||||||
if msg["content"] != messages[i]["content"] {
|
|
||||||
t.Errorf("Message[%d] content = %q, want %q", i, msg["content"], messages[i]["content"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadChatHistory_InvalidJSON(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
// Create invalid JSON file
|
|
||||||
histDir := filepath.Join(tmpDir, ".config", "grokkit")
|
|
||||||
os.MkdirAll(histDir, 0755)
|
|
||||||
histFile := filepath.Join(histDir, "chat_history.json")
|
|
||||||
os.WriteFile(histFile, []byte("invalid json{{{"), 0644)
|
|
||||||
|
|
||||||
history := loadChatHistory()
|
|
||||||
if history != nil {
|
|
||||||
t.Errorf("loadChatHistory() expected nil for invalid JSON, got %v", history)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,11 +15,7 @@ var commitCmd = &cobra.Command{
|
|||||||
Use: "commit",
|
Use: "commit",
|
||||||
Short: "Generate message and commit staged changes",
|
Short: "Generate message and commit staged changes",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
diff, err := git.Run([]string{"diff", "--cached", "--no-color"})
|
diff := git.Run([]string{"diff", "--cached", "--no-color"})
|
||||||
if err != nil {
|
|
||||||
color.Red("Failed to get staged changes: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if diff == "" {
|
if diff == "" {
|
||||||
color.Yellow("No staged changes!")
|
color.Yellow("No staged changes!")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -14,11 +14,7 @@ var commitMsgCmd = &cobra.Command{
|
|||||||
Use: "commit-msg",
|
Use: "commit-msg",
|
||||||
Short: "Generate conventional commit message from staged changes",
|
Short: "Generate conventional commit message from staged changes",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
diff, err := git.Run([]string{"diff", "--cached", "--no-color"})
|
diff := git.Run([]string{"diff", "--cached", "--no-color"})
|
||||||
if err != nil {
|
|
||||||
color.Red("Failed to get staged changes: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if diff == "" {
|
if diff == "" {
|
||||||
color.Yellow("No staged changes!")
|
color.Yellow("No staged changes!")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var completionCmd = &cobra.Command{
|
|
||||||
Use: "completion [bash|zsh|fish|powershell]",
|
|
||||||
Short: "Generate shell completion script",
|
|
||||||
Long: `Generate shell completion script for grokkit.
|
|
||||||
|
|
||||||
To load completions:
|
|
||||||
|
|
||||||
Bash:
|
|
||||||
$ source <(grokkit completion bash)
|
|
||||||
|
|
||||||
# To load completions for each session, execute once:
|
|
||||||
# Linux:
|
|
||||||
$ grokkit completion bash > /etc/bash_completion.d/grokkit
|
|
||||||
# macOS:
|
|
||||||
$ grokkit completion bash > $(brew --prefix)/etc/bash_completion.d/grokkit
|
|
||||||
|
|
||||||
Zsh:
|
|
||||||
# If shell completion is not already enabled in your environment,
|
|
||||||
# you will need to enable it. You can execute the following once:
|
|
||||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
|
||||||
|
|
||||||
# To load completions for each session, execute once:
|
|
||||||
$ grokkit completion zsh > "${fpath[1]}/_grokkit"
|
|
||||||
|
|
||||||
# You will need to start a new shell for this setup to take effect.
|
|
||||||
|
|
||||||
Fish:
|
|
||||||
$ grokkit completion fish | source
|
|
||||||
|
|
||||||
# To load completions for each session, execute once:
|
|
||||||
$ grokkit completion fish > ~/.config/fish/completions/grokkit.fish
|
|
||||||
|
|
||||||
PowerShell:
|
|
||||||
PS> grokkit completion powershell | Out-String | Invoke-Expression
|
|
||||||
|
|
||||||
# To load completions for every new session, run:
|
|
||||||
PS> grokkit completion powershell > grokkit.ps1
|
|
||||||
# and source this file from your PowerShell profile.
|
|
||||||
`,
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
switch args[0] {
|
|
||||||
case "bash":
|
|
||||||
cmd.Root().GenBashCompletion(os.Stdout)
|
|
||||||
case "zsh":
|
|
||||||
cmd.Root().GenZshCompletion(os.Stdout)
|
|
||||||
case "fish":
|
|
||||||
cmd.Root().GenFishCompletion(os.Stdout, true)
|
|
||||||
case "powershell":
|
|
||||||
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
40
cmd/edit.go
40
cmd/edit.go
@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
"gmgauthier.com/grokkit/internal/grok"
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var editCmd = &cobra.Command{
|
var editCmd = &cobra.Command{
|
||||||
@ -24,34 +23,16 @@ var editCmd = &cobra.Command{
|
|||||||
modelFlag, _ := cmd.Flags().GetString("model")
|
modelFlag, _ := cmd.Flags().GetString("model")
|
||||||
model := config.GetModel(modelFlag)
|
model := config.GetModel(modelFlag)
|
||||||
|
|
||||||
logger.Info("edit command started",
|
|
||||||
"file", filePath,
|
|
||||||
"instruction", instruction,
|
|
||||||
"model", model)
|
|
||||||
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
logger.Error("file not found", "file", filePath, "error", err)
|
|
||||||
color.Red("File not found: %s", filePath)
|
color.Red("File not found: %s", filePath)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
original, err := os.ReadFile(filePath)
|
original, _ := os.ReadFile(filePath)
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to read file", "file", filePath, "error", err)
|
|
||||||
color.Red("Failed to read file: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.Debug("file read successfully", "file", filePath, "size_bytes", len(original))
|
|
||||||
cleanedOriginal := removeLastModifiedComments(string(original))
|
cleanedOriginal := removeLastModifiedComments(string(original))
|
||||||
|
|
||||||
backupPath := filePath + ".bak"
|
backupPath := filePath + ".bak"
|
||||||
logger.Debug("creating backup", "backup_path", backupPath)
|
_ = os.WriteFile(backupPath, original, 0644)
|
||||||
if err := os.WriteFile(backupPath, original, 0644); err != nil {
|
|
||||||
logger.Error("failed to create backup", "backup_path", backupPath, "error", err)
|
|
||||||
color.Red("Failed to create backup: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.Info("backup created", "backup_path", backupPath)
|
|
||||||
|
|
||||||
client := grok.NewClient()
|
client := grok.NewClient()
|
||||||
messages := []map[string]string{
|
messages := []map[string]string{
|
||||||
@ -59,10 +40,9 @@ var editCmd = &cobra.Command{
|
|||||||
{"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(filePath), cleanedOriginal, instruction)},
|
{"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(filePath), cleanedOriginal, instruction)},
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Yellow("Asking Grok to %s...\n", instruction)
|
color.Yellow("Asking Grok to %s...", instruction)
|
||||||
raw := client.StreamSilent(messages, model)
|
raw := client.Stream(messages, model)
|
||||||
newContent := grok.CleanCodeResponse(raw)
|
newContent := grok.CleanCodeResponse(raw)
|
||||||
color.Green("✓ Response received")
|
|
||||||
|
|
||||||
color.Cyan("\nProposed changes:")
|
color.Cyan("\nProposed changes:")
|
||||||
fmt.Println("--- a/" + filepath.Base(filePath))
|
fmt.Println("--- a/" + filepath.Base(filePath))
|
||||||
@ -77,17 +57,7 @@ var editCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent))
|
_ = os.WriteFile(filePath, []byte(newContent), 0644)
|
||||||
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
|
|
||||||
logger.Error("failed to write file", "file", filePath, "error", err)
|
|
||||||
color.Red("Failed to write file: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.Info("changes applied successfully",
|
|
||||||
"file", filePath,
|
|
||||||
"backup", backupPath,
|
|
||||||
"original_size", len(original),
|
|
||||||
"new_size", len(newContent))
|
|
||||||
color.Green("✅ Applied successfully! Backup: %s", backupPath)
|
color.Green("✅ Applied successfully! Backup: %s", backupPath)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRemoveLastModifiedComments(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "removes last modified comment",
|
|
||||||
input: "// Last modified: 2024-01-01\npackage main\n\nfunc main() {}",
|
|
||||||
expected: "package main\n\nfunc main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "removes multiple last modified comments",
|
|
||||||
input: "// Last modified: 2024-01-01\npackage main\n// Last modified by: user\nfunc main() {}",
|
|
||||||
expected: "package main\nfunc main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preserves code without last modified",
|
|
||||||
input: "package main\n\nfunc main() {}",
|
|
||||||
expected: "package main\n\nfunc main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles empty string",
|
|
||||||
input: "",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preserves other comments",
|
|
||||||
input: "// This is a regular comment\npackage main\n// Last modified: 2024\n// Another comment\nfunc main() {}",
|
|
||||||
expected: "// This is a regular comment\npackage main\n// Another comment\nfunc main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles line with only last modified",
|
|
||||||
input: "Last modified: 2024-01-01",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := removeLastModifiedComments(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("removeLastModifiedComments() = %q, want %q", result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEditCommand(t *testing.T) {
|
|
||||||
// Create temporary test file
|
|
||||||
tmpfile, err := os.CreateTemp("", "test_edit_*.go")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpfile.Name())
|
|
||||||
|
|
||||||
original := []byte("package main\n\nfunc hello() {}\n")
|
|
||||||
os.WriteFile(tmpfile.Name(), original, 0644)
|
|
||||||
|
|
||||||
// Test the core editing logic directly (non-interactive)
|
|
||||||
instruction := "add a comment at the top"
|
|
||||||
client := grok.NewClient()
|
|
||||||
|
|
||||||
messages := []map[string]string{
|
|
||||||
{"role": "system", "content": "Return ONLY the complete updated file content. The VERY FIRST LINE must be a comment starting with //."},
|
|
||||||
{"role": "user", "content": "File: test.go\n\nOriginal content:\n" + string(original) + "\n\nTask: " + instruction},
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := client.StreamSilent(messages, "grok-4-1-fast-non-reasoning")
|
|
||||||
newContent := grok.CleanCodeResponse(raw)
|
|
||||||
|
|
||||||
// Apply the result (this is what the real command does after confirmation)
|
|
||||||
os.WriteFile(tmpfile.Name(), []byte(newContent), 0644)
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
content, _ := os.ReadFile(tmpfile.Name())
|
|
||||||
if !strings.HasPrefix(strings.TrimSpace(string(content)), "//") {
|
|
||||||
t.Errorf("Expected a comment at the very top. Got:\n%s", content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -12,11 +12,7 @@ var historyCmd = &cobra.Command{
|
|||||||
Use: "history",
|
Use: "history",
|
||||||
Short: "Summarize recent git history",
|
Short: "Summarize recent git history",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
log, err := git.Run([]string{"log", "--oneline", "-10"})
|
log := git.Run([]string{"log", "--oneline", "-10"})
|
||||||
if err != nil {
|
|
||||||
color.Red("Failed to get git log: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if log == "" {
|
if log == "" {
|
||||||
color.Yellow("No commits found.")
|
color.Yellow("No commits found.")
|
||||||
return
|
return
|
||||||
|
|||||||
231
cmd/lint.go
231
cmd/lint.go
@ -1,231 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gmgauthier.com/grokkit/config"
|
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
|
||||||
"gmgauthier.com/grokkit/internal/linter"
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dryRun bool
|
|
||||||
autoFix bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var lintCmd = &cobra.Command{
|
|
||||||
Use: "lint <file>",
|
|
||||||
Short: "Lint a file and optionally apply AI-suggested fixes",
|
|
||||||
Long: `Automatically detects the programming language of a file, runs the appropriate
|
|
||||||
linter, and optionally uses Grok AI to apply suggested fixes.
|
|
||||||
|
|
||||||
The command will:
|
|
||||||
1. Detect the file's programming language
|
|
||||||
2. Check for available linters
|
|
||||||
3. Run the linter and report issues
|
|
||||||
4. (Optional) Use Grok AI to generate and apply fixes
|
|
||||||
|
|
||||||
Safety features:
|
|
||||||
- Creates backup before modifying files
|
|
||||||
- Shows preview of changes before applying
|
|
||||||
- Requires confirmation unless --auto-fix is used`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: runLint,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(lintCmd)
|
|
||||||
lintCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show linting issues without fixing")
|
|
||||||
lintCmd.Flags().BoolVar(&autoFix, "auto-fix", false, "Apply fixes without confirmation")
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLint(cmd *cobra.Command, args []string) {
|
|
||||||
filePath := args[0]
|
|
||||||
|
|
||||||
logger.Info("starting lint operation", "file", filePath, "dry_run", dryRun, "auto_fix", autoFix)
|
|
||||||
|
|
||||||
// Check file exists
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
||||||
logger.Error("file not found", "file", filePath, "error", err)
|
|
||||||
color.Red("❌ File not found: %s", filePath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get absolute path for better logging
|
|
||||||
absPath, err := filepath.Abs(filePath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("could not get absolute path", "file", filePath, "error", err)
|
|
||||||
absPath = filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run linter
|
|
||||||
color.Cyan("🔍 Detecting language and running linter...")
|
|
||||||
result, err := linter.LintFile(absPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// Check if it's because no linter is available
|
|
||||||
if result != nil && !result.LinterExists {
|
|
||||||
logger.Warn("no linter available", "file", absPath, "language", result.Language)
|
|
||||||
color.Yellow("⚠️ %s", result.Output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Error("linting failed", "file", absPath, "error", err)
|
|
||||||
color.Red("❌ Failed to lint file: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
color.Cyan("📋 Language: %s", result.Language)
|
|
||||||
color.Cyan("🔧 Linter: %s", result.LinterUsed)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
if !result.HasIssues {
|
|
||||||
logger.Info("no linting issues found", "file", absPath, "linter", result.LinterUsed)
|
|
||||||
color.Green("✅ No issues found!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show linter output
|
|
||||||
logger.Info("linting issues found", "file", absPath, "exit_code", result.ExitCode)
|
|
||||||
color.Yellow("⚠️ Issues found:")
|
|
||||||
fmt.Println(strings.TrimSpace(result.Output))
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Stop here if dry-run
|
|
||||||
if dryRun {
|
|
||||||
logger.Info("dry-run mode, skipping fixes", "file", absPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read original file content
|
|
||||||
originalContent, err := os.ReadFile(absPath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to read file", "file", absPath, "error", err)
|
|
||||||
color.Red("❌ Failed to read file: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Grok AI to generate fixes
|
|
||||||
color.Cyan("🤖 Asking Grok AI for fixes...")
|
|
||||||
logger.Info("requesting AI fixes", "file", absPath, "original_size", len(originalContent))
|
|
||||||
|
|
||||||
modelFlag, _ := cmd.Flags().GetString("model")
|
|
||||||
model := config.GetModel(modelFlag)
|
|
||||||
|
|
||||||
client := grok.NewClient()
|
|
||||||
messages := buildLintFixMessages(result, string(originalContent))
|
|
||||||
response := client.StreamSilent(messages, model)
|
|
||||||
|
|
||||||
if response == "" {
|
|
||||||
logger.Error("AI returned empty response", "file", absPath)
|
|
||||||
color.Red("❌ Failed to get AI response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean the response
|
|
||||||
fixedCode := grok.CleanCodeResponse(response)
|
|
||||||
logger.Info("received AI fixes", "file", absPath, "fixed_size", len(fixedCode))
|
|
||||||
|
|
||||||
// Create backup
|
|
||||||
backupPath := absPath + ".bak"
|
|
||||||
if err := os.WriteFile(backupPath, originalContent, 0644); err != nil {
|
|
||||||
logger.Error("failed to create backup", "file", absPath, "error", err)
|
|
||||||
color.Red("❌ Failed to create backup: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("backup created", "backup", backupPath)
|
|
||||||
color.Green("💾 Backup created: %s", backupPath)
|
|
||||||
|
|
||||||
// Show preview if not auto-fix
|
|
||||||
if !autoFix {
|
|
||||||
fmt.Println()
|
|
||||||
color.Cyan("📝 Preview of fixes:")
|
|
||||||
fmt.Println(strings.Repeat("-", 80))
|
|
||||||
// Show first 50 lines of fixed code
|
|
||||||
lines := strings.Split(fixedCode, "\n")
|
|
||||||
previewLines := 50
|
|
||||||
if len(lines) < previewLines {
|
|
||||||
previewLines = len(lines)
|
|
||||||
}
|
|
||||||
for i := 0; i < previewLines; i++ {
|
|
||||||
fmt.Println(lines[i])
|
|
||||||
}
|
|
||||||
if len(lines) > previewLines {
|
|
||||||
fmt.Printf("... (%d more lines)\n", len(lines)-previewLines)
|
|
||||||
}
|
|
||||||
fmt.Println(strings.Repeat("-", 80))
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Ask for confirmation
|
|
||||||
color.Yellow("Apply these fixes? (y/N): ")
|
|
||||||
var response string
|
|
||||||
fmt.Scanln(&response)
|
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
|
||||||
|
|
||||||
if response != "y" && response != "yes" {
|
|
||||||
logger.Info("user cancelled fix application", "file", absPath)
|
|
||||||
color.Yellow("❌ Cancelled. Backup preserved at: %s", backupPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply fixes
|
|
||||||
if err := os.WriteFile(absPath, []byte(fixedCode), 0644); err != nil {
|
|
||||||
logger.Error("failed to write fixed file", "file", absPath, "error", err)
|
|
||||||
color.Red("❌ Failed to write file: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("fixes applied successfully", "file", absPath)
|
|
||||||
color.Green("✅ Fixes applied successfully!")
|
|
||||||
color.Cyan("💾 Original saved to: %s", backupPath)
|
|
||||||
|
|
||||||
// Optionally run linter again to verify
|
|
||||||
color.Cyan("\n🔍 Re-running linter to verify fixes...")
|
|
||||||
verifyResult, err := linter.LintFile(absPath)
|
|
||||||
if err == nil && !verifyResult.HasIssues {
|
|
||||||
logger.Info("verification successful, no issues remain", "file", absPath)
|
|
||||||
color.Green("✅ Verification passed! No issues remaining.")
|
|
||||||
} else if err == nil && verifyResult.HasIssues {
|
|
||||||
logger.Warn("verification found remaining issues", "file", absPath, "exit_code", verifyResult.ExitCode)
|
|
||||||
color.Yellow("⚠️ Some issues may remain:")
|
|
||||||
fmt.Println(strings.TrimSpace(verifyResult.Output))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLintFixMessages(result *linter.LintResult, originalCode string) []map[string]string {
|
|
||||||
systemPrompt := "You are a code quality expert. Fix linter issues and return only the corrected code with no explanations, markdown, or extra text."
|
|
||||||
|
|
||||||
userPrompt := fmt.Sprintf(`A linter has found issues in the following %s code.
|
|
||||||
|
|
||||||
Linter: %s
|
|
||||||
Issues found:
|
|
||||||
%s
|
|
||||||
|
|
||||||
Original code:
|
|
||||||
%s
|
|
||||||
|
|
||||||
Please fix all the issues identified by the linter. Return ONLY the corrected code without any explanations, markdown formatting, or code fences. The output should be ready to write directly to the file.
|
|
||||||
|
|
||||||
Important:
|
|
||||||
- Fix ALL issues reported by the linter
|
|
||||||
- Preserve all functionality
|
|
||||||
- Maintain the original code style as much as possible
|
|
||||||
- Do NOT include markdown code fences
|
|
||||||
- Do NOT include explanations or comments about the changes`,
|
|
||||||
result.Language,
|
|
||||||
result.LinterUsed,
|
|
||||||
strings.TrimSpace(result.Output),
|
|
||||||
originalCode)
|
|
||||||
|
|
||||||
return []map[string]string{
|
|
||||||
{"role": "system", "content": systemPrompt},
|
|
||||||
{"role": "user", "content": userPrompt},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,13 +14,9 @@ var prDescribeCmd = &cobra.Command{
|
|||||||
Use: "pr-describe",
|
Use: "pr-describe",
|
||||||
Short: "Generate full PR description from current branch",
|
Short: "Generate full PR description from current branch",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
diff, err := git.Run([]string{"diff", "main..HEAD", "--no-color"})
|
diff := git.Run([]string{"diff", "main..HEAD", "--no-color"})
|
||||||
if err != nil || diff == "" {
|
if diff == "" {
|
||||||
diff, err = git.Run([]string{"diff", "origin/main..HEAD", "--no-color"})
|
diff = git.Run([]string{"diff", "origin/main..HEAD", "--no-color"})
|
||||||
if err != nil {
|
|
||||||
color.Red("Failed to get branch diff: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if diff == "" {
|
if diff == "" {
|
||||||
color.Yellow("No changes on this branch compared to main/origin/main.")
|
color.Yellow("No changes on this branch compared to main/origin/main.")
|
||||||
|
|||||||
@ -18,16 +18,8 @@ var reviewCmd = &cobra.Command{
|
|||||||
model := config.GetModel(modelFlag)
|
model := config.GetModel(modelFlag)
|
||||||
|
|
||||||
client := grok.NewClient()
|
client := grok.NewClient()
|
||||||
diff, err := git.Run([]string{"diff", "--no-color"})
|
diff := git.Run([]string{"diff", "--no-color"})
|
||||||
if err != nil {
|
status := git.Run([]string{"status", "--short"})
|
||||||
color.Red("Failed to get git diff: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
status, err := git.Run([]string{"status", "--short"})
|
|
||||||
if err != nil {
|
|
||||||
color.Red("Failed to get git status: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
messages := []map[string]string{
|
messages := []map[string]string{
|
||||||
{"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."},
|
{"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."},
|
||||||
|
|||||||
22
cmd/root.go
22
cmd/root.go
@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@ -14,18 +13,6 @@ var rootCmd = &cobra.Command{
|
|||||||
Long: `A fast, native Go CLI for Grok. Chat, edit files, and supercharge your git workflow.`,
|
Long: `A fast, native Go CLI for Grok. Chat, edit files, and supercharge your git workflow.`,
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
config.Load()
|
config.Load()
|
||||||
|
|
||||||
// Determine log level: flag > config > default
|
|
||||||
logLevel := config.GetLogLevel()
|
|
||||||
if debug, _ := cmd.Flags().GetBool("debug"); debug {
|
|
||||||
logLevel = "debug"
|
|
||||||
}
|
|
||||||
if verbose, _ := cmd.Flags().GetBool("verbose"); verbose {
|
|
||||||
logLevel = "info"
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = logger.Init(logLevel) // Logging is optional, don't fail if it errors
|
|
||||||
logger.Info("grokkit starting", "command", cmd.Name(), "log_level", logLevel)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,12 +31,5 @@ func init() {
|
|||||||
rootCmd.AddCommand(prDescribeCmd)
|
rootCmd.AddCommand(prDescribeCmd)
|
||||||
rootCmd.AddCommand(historyCmd)
|
rootCmd.AddCommand(historyCmd)
|
||||||
rootCmd.AddCommand(agentCmd)
|
rootCmd.AddCommand(agentCmd)
|
||||||
rootCmd.AddCommand(completionCmd)
|
chatCmd.Flags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
||||||
|
|
||||||
// Add model flag to all commands
|
|
||||||
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")
|
|
||||||
|
|
||||||
// Add logging flags
|
|
||||||
rootCmd.PersistentFlags().Bool("debug", false, "Enable debug logging (logs to stderr and file)")
|
|
||||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Load() {
|
func Load() {
|
||||||
home, err := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
if err != nil {
|
|
||||||
// Fall back to current directory if home not found
|
|
||||||
home = "."
|
|
||||||
}
|
|
||||||
configPath := filepath.Join(home, ".config", "grokkit")
|
configPath := filepath.Join(home, ".config", "grokkit")
|
||||||
|
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
@ -23,10 +19,7 @@ func Load() {
|
|||||||
|
|
||||||
viper.SetDefault("default_model", "grok-4")
|
viper.SetDefault("default_model", "grok-4")
|
||||||
viper.SetDefault("temperature", 0.7)
|
viper.SetDefault("temperature", 0.7)
|
||||||
viper.SetDefault("log_level", "info")
|
|
||||||
viper.SetDefault("timeout", 60)
|
|
||||||
|
|
||||||
// Config file is optional, so we ignore read errors
|
|
||||||
_ = viper.ReadInConfig()
|
_ = viper.ReadInConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,19 +32,3 @@ func GetModel(flagModel string) string {
|
|||||||
}
|
}
|
||||||
return viper.GetString("default_model")
|
return viper.GetString("default_model")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTemperature() float64 {
|
|
||||||
return viper.GetFloat64("temperature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTimeout() int {
|
|
||||||
timeout := viper.GetInt("timeout")
|
|
||||||
if timeout <= 0 {
|
|
||||||
return 60 // Default 60 seconds
|
|
||||||
}
|
|
||||||
return timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogLevel() string {
|
|
||||||
return viper.GetString("log_level")
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetModel(t *testing.T) {
|
|
||||||
// Reset viper for testing
|
|
||||||
viper.Reset()
|
|
||||||
viper.SetDefault("default_model", "grok-4")
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
flagModel string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "returns flag model when provided",
|
|
||||||
flagModel: "grok-beta",
|
|
||||||
expected: "grok-beta",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "returns default when flag empty",
|
|
||||||
flagModel: "",
|
|
||||||
expected: "grok-4",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := GetModel(tt.flagModel)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("GetModel(%q) = %q, want %q", tt.flagModel, result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetModelWithAlias(t *testing.T) {
|
|
||||||
viper.Reset()
|
|
||||||
viper.Set("aliases.beta", "grok-beta-2")
|
|
||||||
viper.SetDefault("default_model", "grok-4")
|
|
||||||
|
|
||||||
result := GetModel("beta")
|
|
||||||
expected := "grok-beta-2"
|
|
||||||
if result != expected {
|
|
||||||
t.Errorf("GetModel('beta') = %q, want %q", result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoad(t *testing.T) {
|
|
||||||
// Just ensure Load doesn't panic
|
|
||||||
Load()
|
|
||||||
}
|
|
||||||
@ -1,597 +0,0 @@
|
|||||||
# Architecture Overview
|
|
||||||
|
|
||||||
This document describes the design principles, architecture patterns, and implementation details of Grokkit.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Design Philosophy](#design-philosophy)
|
|
||||||
- [Project Structure](#project-structure)
|
|
||||||
- [Core Components](#core-components)
|
|
||||||
- [Data Flow](#data-flow)
|
|
||||||
- [Testing Strategy](#testing-strategy)
|
|
||||||
- [Design Patterns](#design-patterns)
|
|
||||||
- [Future Considerations](#future-considerations)
|
|
||||||
|
|
||||||
## Design Philosophy
|
|
||||||
|
|
||||||
Grokkit follows these core principles:
|
|
||||||
|
|
||||||
### 1. **Simplicity First**
|
|
||||||
- Minimal dependencies (stdlib + well-known libs)
|
|
||||||
- Minimal interface surface (CLI commands over TUI interactions)
|
|
||||||
- Clear, readable code over clever solutions
|
|
||||||
- Single responsibility per package
|
|
||||||
|
|
||||||
### 2. **Safety by Default**
|
|
||||||
- File backups before any modification
|
|
||||||
- Confirmation prompts for destructive actions
|
|
||||||
- Comprehensive error handling
|
|
||||||
|
|
||||||
### 3. **Observability**
|
|
||||||
- Structured logging for all operations
|
|
||||||
- Request/response timing
|
|
||||||
- Contextual error messages
|
|
||||||
|
|
||||||
### 4. **Testability**
|
|
||||||
- Interface-based design
|
|
||||||
- Dependency injection where needed
|
|
||||||
- Unit tests for pure functions
|
|
||||||
|
|
||||||
### 5. **User Experience**
|
|
||||||
- Streaming responses for immediate feedback
|
|
||||||
- Persistent history
|
|
||||||
- Shell completions
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
grokkit/
|
|
||||||
├── main.go # Entry point
|
|
||||||
├── cmd/ # CLI commands (Cobra)
|
|
||||||
│ ├── root.go # Root command + flags
|
|
||||||
│ ├── chat.go # Chat command
|
|
||||||
│ ├── edit.go # Edit command
|
|
||||||
│ ├── review.go # Review command
|
|
||||||
│ ├── commit.go # Commit commands
|
|
||||||
│ ├── completion.go # Shell completions
|
|
||||||
│ └── *_test.go # Command tests
|
|
||||||
├── config/ # Configuration (Viper)
|
|
||||||
│ ├── config.go # Config loading + getters
|
|
||||||
│ └── config_test.go # Config tests
|
|
||||||
├── internal/ # Private packages
|
|
||||||
│ ├── errors/ # Custom error types
|
|
||||||
│ │ ├── errors.go
|
|
||||||
│ │ └── errors_test.go
|
|
||||||
│ ├── git/ # Git operations wrapper
|
|
||||||
│ │ ├── git.go
|
|
||||||
│ │ ├── interface.go # GitRunner interface
|
|
||||||
│ │ └── git_test.go
|
|
||||||
│ ├── grok/ # Grok API client
|
|
||||||
│ │ ├── client.go # HTTP client + streaming
|
|
||||||
│ │ ├── interface.go # AIClient interface
|
|
||||||
│ │ └── client_test.go
|
|
||||||
│ ├── linter/ # Multi-language linting
|
|
||||||
│ │ ├── linter.go # Language detection + linter execution
|
|
||||||
│ │ └── linter_test.go
|
|
||||||
│ └── logger/ # Structured logging (slog)
|
|
||||||
│ ├── logger.go # Logger setup + helpers
|
|
||||||
│ └── logger_test.go
|
|
||||||
├── docs/ # Documentation
|
|
||||||
│ ├── TROUBLESHOOTING.md
|
|
||||||
│ ├── ARCHITECTURE.md # This file
|
|
||||||
│ └── CONFIGURATION.md
|
|
||||||
├── .gitea/workflows/ # CI/CD
|
|
||||||
│ ├── ci.yml # Test + lint + build
|
|
||||||
│ └── release.yml # Multi-platform releases
|
|
||||||
└── Makefile # Build automation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Package Organization
|
|
||||||
|
|
||||||
- **`cmd/`**: Cobra commands, minimal logic (orchestration only)
|
|
||||||
- **`config/`**: Viper integration, configuration management
|
|
||||||
- **`internal/`**: Core business logic (not importable outside project)
|
|
||||||
- **`internal/errors/`**: Domain-specific error types
|
|
||||||
- **`internal/git/`**: Git command abstraction
|
|
||||||
- **`internal/grok/`**: API client with streaming support
|
|
||||||
- **`internal/linter/`**: Language detection and linter execution
|
|
||||||
- **`internal/logger/`**: Structured logging facade
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### 1. CLI Layer (`cmd/`)
|
|
||||||
|
|
||||||
**Responsibility:** Command parsing, user interaction, orchestration
|
|
||||||
|
|
||||||
**Pattern:** Command pattern (Cobra)
|
|
||||||
|
|
||||||
```go
|
|
||||||
var chatCmd = &cobra.Command{
|
|
||||||
Use: "chat",
|
|
||||||
Short: "Interactive chat",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
// 1. Parse flags
|
|
||||||
// 2. Load config
|
|
||||||
// 3. Call business logic
|
|
||||||
// 4. Handle errors
|
|
||||||
// 5. Display output
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key characteristics:**
|
|
||||||
- Thin orchestration layer
|
|
||||||
- No business logic
|
|
||||||
- Delegates to internal packages
|
|
||||||
- Handles user I/O only
|
|
||||||
|
|
||||||
### 2. Configuration Layer (`config/`)
|
|
||||||
|
|
||||||
**Responsibility:** Load and manage configuration from multiple sources
|
|
||||||
|
|
||||||
**Pattern:** Singleton (via Viper)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Priority: CLI flag > env var > config file > default
|
|
||||||
func GetModel(flagModel string) string {
|
|
||||||
if flagModel != "" {
|
|
||||||
return resolveAlias(flagModel)
|
|
||||||
}
|
|
||||||
return viper.GetString("default_model")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration sources (in order):**
|
|
||||||
1. CLI flags (`--model grok-4`)
|
|
||||||
2. Environment variables (`XAI_API_KEY`)
|
|
||||||
3. Config file (`~/.config/grokkit/config.toml`)
|
|
||||||
4. Defaults (hardcoded in `config.Load()`)
|
|
||||||
|
|
||||||
### 3. API Client (`internal/grok/`)
|
|
||||||
|
|
||||||
**Responsibility:** HTTP communication with Grok API
|
|
||||||
|
|
||||||
**Pattern:** Client pattern with streaming support
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Client struct {
|
|
||||||
APIKey string
|
|
||||||
BaseURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public API
|
|
||||||
func (c *Client) Stream(messages, model) string
|
|
||||||
func (c *Client) StreamSilent(messages, model) string
|
|
||||||
func (c *Client) StreamWithTemp(messages, model, temp) string
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key features:**
|
|
||||||
- Context-aware requests (60s timeout)
|
|
||||||
- Server-Sent Events (SSE) streaming
|
|
||||||
- Silent vs live output modes
|
|
||||||
- Comprehensive logging
|
|
||||||
|
|
||||||
**Request flow:**
|
|
||||||
1. Marshal request → JSON
|
|
||||||
2. Create HTTP request with context
|
|
||||||
3. Set headers (Authorization, Content-Type)
|
|
||||||
4. Send request
|
|
||||||
5. Stream response line-by-line
|
|
||||||
6. Parse SSE chunks
|
|
||||||
7. Build full response
|
|
||||||
8. Log metrics
|
|
||||||
|
|
||||||
### 4. Git Integration (`internal/git/`)
|
|
||||||
|
|
||||||
**Responsibility:** Abstract git command execution
|
|
||||||
|
|
||||||
**Pattern:** Command wrapper + Interface
|
|
||||||
|
|
||||||
```go
|
|
||||||
type GitRunner interface {
|
|
||||||
Run(args []string) (string, error)
|
|
||||||
IsRepo() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation
|
|
||||||
func Run(args []string) (string, error) {
|
|
||||||
out, err := exec.Command("git", args...).Output()
|
|
||||||
// Log execution
|
|
||||||
// Wrap errors
|
|
||||||
return string(out), err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why wrapped:**
|
|
||||||
- Logging of all git operations
|
|
||||||
- Consistent error handling
|
|
||||||
- Testability via interface
|
|
||||||
- Future: git library instead of exec
|
|
||||||
|
|
||||||
### 5. Linter (`internal/linter/`)
|
|
||||||
|
|
||||||
**Responsibility:** Language detection and linter orchestration
|
|
||||||
|
|
||||||
**Pattern:** Strategy pattern (multiple linters per language)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Language struct {
|
|
||||||
Name string
|
|
||||||
Extensions []string
|
|
||||||
Linters []Linter // Multiple options per language
|
|
||||||
}
|
|
||||||
|
|
||||||
type Linter struct {
|
|
||||||
Name string
|
|
||||||
Command string
|
|
||||||
Args []string
|
|
||||||
InstallInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-detect language and find available linter
|
|
||||||
func LintFile(filePath string) (*LintResult, error) {
|
|
||||||
lang := DetectLanguage(filePath) // By extension
|
|
||||||
linter := FindAvailableLinter(lang) // Check PATH
|
|
||||||
return RunLinter(filePath, linter) // Execute
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Supported languages:**
|
|
||||||
- Go (golangci-lint, go vet)
|
|
||||||
- Python (pylint, flake8, ruff)
|
|
||||||
- JavaScript/TypeScript (eslint, tsc)
|
|
||||||
- Rust (clippy)
|
|
||||||
- Ruby (rubocop)
|
|
||||||
- Java (checkstyle)
|
|
||||||
- C/C++ (clang-tidy)
|
|
||||||
- Shell (shellcheck)
|
|
||||||
|
|
||||||
**Design features:**
|
|
||||||
- Fallback linters (tries multiple options)
|
|
||||||
- Tool availability checking
|
|
||||||
- Helpful install instructions
|
|
||||||
- Exit code interpretation
|
|
||||||
|
|
||||||
### 6. Logging (`internal/logger/`)
|
|
||||||
|
|
||||||
**Responsibility:** Structured logging with context
|
|
||||||
|
|
||||||
**Pattern:** Facade over stdlib `log/slog`
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Structured logging with key-value pairs
|
|
||||||
logger.Info("API request completed",
|
|
||||||
"model", model,
|
|
||||||
"duration_ms", duration,
|
|
||||||
"response_length", length)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- JSON output format
|
|
||||||
- Dynamic log levels
|
|
||||||
- Multi-writer (file + stderr in debug)
|
|
||||||
- Context enrichment
|
|
||||||
|
|
||||||
**Log levels:**
|
|
||||||
- `DEBUG`: Detailed info for troubleshooting
|
|
||||||
- `INFO`: Normal operations
|
|
||||||
- `WARN`: Potential issues
|
|
||||||
- `ERROR`: Failures requiring attention
|
|
||||||
|
|
||||||
### 6. Error Handling (`internal/errors/`)
|
|
||||||
|
|
||||||
**Responsibility:** Domain-specific error types
|
|
||||||
|
|
||||||
**Pattern:** Custom error types with context
|
|
||||||
|
|
||||||
```go
|
|
||||||
type GitError struct {
|
|
||||||
Command string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIError struct {
|
|
||||||
StatusCode int
|
|
||||||
Message string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Type-safe error handling
|
|
||||||
- Rich error context
|
|
||||||
- Error wrapping (`errors.Is`, `errors.As`)
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
### Chat Command Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User Input
|
|
||||||
↓
|
|
||||||
chatCmd.Run()
|
|
||||||
↓
|
|
||||||
config.GetModel() # Resolve model
|
|
||||||
↓
|
|
||||||
loadChatHistory() # Load previous messages
|
|
||||||
↓
|
|
||||||
grok.Client.Stream() # API call
|
|
||||||
├─→ logger.Info() # Log request
|
|
||||||
├─→ HTTP POST # Send to API
|
|
||||||
├─→ Stream chunks # Receive SSE
|
|
||||||
└─→ logger.Info() # Log completion
|
|
||||||
↓
|
|
||||||
saveChatHistory() # Persist conversation
|
|
||||||
↓
|
|
||||||
Display to user
|
|
||||||
```
|
|
||||||
|
|
||||||
### Edit Command Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User: grokkit edit file.go "instruction"
|
|
||||||
↓
|
|
||||||
editCmd.Run()
|
|
||||||
├─→ Validate file exists
|
|
||||||
├─→ Read original content
|
|
||||||
├─→ Create backup (.bak)
|
|
||||||
├─→ Clean "Last modified" comments
|
|
||||||
↓
|
|
||||||
grok.Client.StreamSilent() # Get AI response
|
|
||||||
├─→ [Same as chat, but silent]
|
|
||||||
↓
|
|
||||||
grok.CleanCodeResponse() # Remove markdown fences
|
|
||||||
↓
|
|
||||||
Display preview # Show diff-style output
|
|
||||||
↓
|
|
||||||
Prompt user (y/n)
|
|
||||||
↓
|
|
||||||
Write file (if confirmed)
|
|
||||||
├─→ logger.Info() # Log changes
|
|
||||||
└─→ Keep backup
|
|
||||||
```
|
|
||||||
|
|
||||||
### Commit Command Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User: grokkit commit
|
|
||||||
↓
|
|
||||||
commitCmd.Run()
|
|
||||||
↓
|
|
||||||
git.Run(["diff", "--cached"]) # Get staged changes
|
|
||||||
├─→ logger.Debug() # Log git command
|
|
||||||
↓
|
|
||||||
grok.Client.Stream() # Generate commit message
|
|
||||||
↓
|
|
||||||
Display message + prompt
|
|
||||||
↓
|
|
||||||
exec.Command("git", "commit") # Execute commit
|
|
||||||
├─→ logger.Info() # Log result
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint Command Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User: grokkit lint file.py [--dry-run] [--auto-fix]
|
|
||||||
↓
|
|
||||||
lintCmd.Run()
|
|
||||||
├─→ Validate file exists
|
|
||||||
↓
|
|
||||||
linter.LintFile()
|
|
||||||
├─→ DetectLanguage() # By file extension
|
|
||||||
├─→ FindAvailableLinter() # Check PATH
|
|
||||||
├─→ RunLinter() # Execute linter
|
|
||||||
↓
|
|
||||||
Display linter output
|
|
||||||
↓
|
|
||||||
(if --dry-run) → Exit
|
|
||||||
↓
|
|
||||||
(if issues found) → Request AI fixes
|
|
||||||
↓
|
|
||||||
grok.Client.StreamSilent() # Get fixed code
|
|
||||||
├─→ Build prompt with linter output + original code
|
|
||||||
├─→ Stream API response
|
|
||||||
↓
|
|
||||||
grok.CleanCodeResponse() # Remove markdown
|
|
||||||
↓
|
|
||||||
Create backup (.bak)
|
|
||||||
↓
|
|
||||||
(if not --auto-fix) → Show preview + prompt
|
|
||||||
↓
|
|
||||||
Write fixed file
|
|
||||||
↓
|
|
||||||
linter.LintFile() # Verify fixes
|
|
||||||
├─→ Display verification result
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
**Coverage goals:**
|
|
||||||
- Internal packages: >70%
|
|
||||||
- Helper functions: 100%
|
|
||||||
- Commands: Test helpers, not Cobra execution
|
|
||||||
|
|
||||||
**Approach:**
|
|
||||||
- Table-driven tests
|
|
||||||
- Isolated environments (temp directories)
|
|
||||||
- No external dependencies in unit tests
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```go
|
|
||||||
func TestRemoveLastModifiedComments(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"removes comment", "// Last modified\ncode", "code"},
|
|
||||||
{"preserves other", "// Comment\ncode", "// Comment\ncode"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := removeLastModifiedComments(tt.input)
|
|
||||||
assert.Equal(t, tt.expected, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### What's NOT tested (and why)
|
|
||||||
|
|
||||||
❌ **Cobra command execution** - Requires CLI integration tests
|
|
||||||
❌ **User input (Scanln)** - Requires terminal mocking
|
|
||||||
❌ **API streaming** - Would need mock HTTP server
|
|
||||||
❌ **os.Exit() paths** - Can't test process termination
|
|
||||||
|
|
||||||
These require **integration/E2E tests**, which are a different testing layer.
|
|
||||||
|
|
||||||
### Test Organization
|
|
||||||
|
|
||||||
```
|
|
||||||
package/
|
|
||||||
├── implementation.go # Production code
|
|
||||||
└── implementation_test.go # Tests (same package)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Tests can access private functions
|
|
||||||
- Clear mapping between code and tests
|
|
||||||
- Go convention
|
|
||||||
|
|
||||||
## Design Patterns
|
|
||||||
|
|
||||||
### 1. Interface Segregation
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Small, focused interfaces
|
|
||||||
type AIClient interface {
|
|
||||||
Stream(messages []map[string]string, model string) string
|
|
||||||
StreamSilent(messages []map[string]string, model string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GitRunner interface {
|
|
||||||
Run(args []string) (string, error)
|
|
||||||
IsRepo() bool
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- Easy to mock for testing
|
|
||||||
- Flexibility to swap implementations
|
|
||||||
- Clear contracts
|
|
||||||
|
|
||||||
### 2. Facade Pattern (Logger)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Simple API hides slog complexity
|
|
||||||
logger.Info("message", "key", "value")
|
|
||||||
logger.Error("error", "context", ctx)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Command Pattern (CLI)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Each command is independent
|
|
||||||
var chatCmd = &cobra.Command{...}
|
|
||||||
var editCmd = &cobra.Command{...}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Strategy Pattern (Streaming modes)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Different behaviors via parameters
|
|
||||||
func streamInternal(messages, model, temp, printLive bool) {
|
|
||||||
if printLive {
|
|
||||||
fmt.Print(content) // Live streaming
|
|
||||||
}
|
|
||||||
// Silent streaming builds full response
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Template Method (Git operations)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Base wrapper with logging
|
|
||||||
func Run(args []string) (string, error) {
|
|
||||||
logger.Debug("executing", "command", args)
|
|
||||||
out, err := exec.Command("git", args...).Output()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed", "command", args, "error", err)
|
|
||||||
}
|
|
||||||
return string(out), err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Considerations
|
|
||||||
|
|
||||||
### Scalability
|
|
||||||
|
|
||||||
**Current limitations:**
|
|
||||||
- Single API key (no multi-user support)
|
|
||||||
- No request queuing
|
|
||||||
- No rate limiting client-side
|
|
||||||
- Chat history grows unbounded
|
|
||||||
|
|
||||||
**Future improvements:**
|
|
||||||
- Rate limit handling with backoff
|
|
||||||
- History trimming/rotation
|
|
||||||
- Concurrent request support
|
|
||||||
- Request caching
|
|
||||||
|
|
||||||
### Extensibility
|
|
||||||
|
|
||||||
**Easy to add:**
|
|
||||||
- New commands (add to `cmd/`)
|
|
||||||
- New models (config aliases)
|
|
||||||
- New output formats (change display logic)
|
|
||||||
- New logging destinations (slog handlers)
|
|
||||||
|
|
||||||
**Harder to add:**
|
|
||||||
- Multiple AI providers (requires abstraction)
|
|
||||||
- Non-streaming APIs (requires client refactor)
|
|
||||||
- GUI (CLI-focused architecture)
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
**Current bottlenecks:**
|
|
||||||
- Network latency (streaming helps with perception)
|
|
||||||
- API response time (model-dependent)
|
|
||||||
- File I/O for large history (rare)
|
|
||||||
|
|
||||||
**Optimization opportunities:**
|
|
||||||
- Request caching (for repeated queries)
|
|
||||||
- Parallel git operations (if multiple files)
|
|
||||||
- Lazy loading of history (if very large)
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
**Current measures:**
|
|
||||||
- API key via environment variable
|
|
||||||
- No credential storage
|
|
||||||
- Backup files for safety
|
|
||||||
|
|
||||||
**Future considerations:**
|
|
||||||
- Encrypted config storage
|
|
||||||
- API key rotation support
|
|
||||||
- Audit logging for file changes
|
|
||||||
- Sandboxed file operations
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When contributing, maintain these principles:
|
|
||||||
|
|
||||||
1. **Keep packages small and focused**
|
|
||||||
2. **Write tests for pure functions**
|
|
||||||
3. **Log all external operations**
|
|
||||||
4. **Use interfaces for testability**
|
|
||||||
5. **Handle errors explicitly**
|
|
||||||
6. **Document non-obvious behavior**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**See also:**
|
|
||||||
- [Configuration Guide](CONFIGURATION.md)
|
|
||||||
- [Troubleshooting](TROUBLESHOOTING.md)
|
|
||||||
@ -1,485 +0,0 @@
|
|||||||
# Configuration Guide
|
|
||||||
|
|
||||||
Advanced configuration options and use cases for Grokkit.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Configuration Priority](#configuration-priority)
|
|
||||||
- [Environment Variables](#environment-variables)
|
|
||||||
- [Config File](#config-file)
|
|
||||||
- [Configuration Examples](#configuration-examples)
|
|
||||||
- [Advanced Use Cases](#advanced-use-cases)
|
|
||||||
- [Best Practices](#best-practices)
|
|
||||||
|
|
||||||
## Configuration Priority
|
|
||||||
|
|
||||||
Grokkit resolves configuration in this order (highest to lowest):
|
|
||||||
|
|
||||||
1. **CLI Flags** - `grokkit chat --model grok-4`
|
|
||||||
2. **Environment Variables** - `export XAI_API_KEY=...`
|
|
||||||
3. **Config File** - `~/.config/grokkit/config.toml`
|
|
||||||
4. **Defaults** - Hardcoded in application
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```bash
|
|
||||||
# Config file says: default_model = "grok-4"
|
|
||||||
# Command uses:
|
|
||||||
grokkit chat -m grok-beta
|
|
||||||
|
|
||||||
# Result: Uses grok-beta (CLI flag wins)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### XAI_API_KEY (Required)
|
|
||||||
|
|
||||||
Your xAI API key.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set for current session
|
|
||||||
export XAI_API_KEY=sk-your-key-here
|
|
||||||
|
|
||||||
# Set permanently (bash)
|
|
||||||
echo 'export XAI_API_KEY=sk-your-key-here' >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Set permanently (zsh)
|
|
||||||
echo 'export XAI_API_KEY=sk-your-key-here' >> ~/.zshrc
|
|
||||||
source ~/.zshrc
|
|
||||||
|
|
||||||
# Verify (safely)
|
|
||||||
echo $XAI_API_KEY | sed 's/sk-.*$/sk-***/'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment-based Config
|
|
||||||
|
|
||||||
You can also set config values via environment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export GROKKIT_MODEL=grok-4
|
|
||||||
export GROKKIT_TEMPERATURE=0.8
|
|
||||||
export GROKKIT_LOG_LEVEL=debug
|
|
||||||
```
|
|
||||||
|
|
||||||
Viper (the config library) automatically reads these with `AutomaticEnv()`.
|
|
||||||
|
|
||||||
## Config File
|
|
||||||
|
|
||||||
### Location
|
|
||||||
|
|
||||||
Default location: `~/.config/grokkit/config.toml`
|
|
||||||
|
|
||||||
Custom location via Viper (not currently exposed, but easy to add).
|
|
||||||
|
|
||||||
### Full Configuration
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
|
|
||||||
# ===== Model Settings =====
|
|
||||||
default_model = "grok-4" # Model to use by default
|
|
||||||
temperature = 0.7 # Creativity (0.0 = deterministic, 1.0 = creative)
|
|
||||||
timeout = 60 # API timeout in seconds
|
|
||||||
|
|
||||||
# ===== Logging Settings =====
|
|
||||||
log_level = "info" # debug | info | warn | error
|
|
||||||
|
|
||||||
# ===== Model Aliases =====
|
|
||||||
# Create shortcuts for frequently used models
|
|
||||||
[aliases]
|
|
||||||
beta = "grok-beta-2" # grokkit chat -m beta
|
|
||||||
fast = "grok-4-mini" # grokkit chat -m fast
|
|
||||||
creative = "grok-4" # grokkit chat -m creative
|
|
||||||
|
|
||||||
# ===== Chat Settings =====
|
|
||||||
[chat]
|
|
||||||
history_file = "~/.config/grokkit/chat_history.json" # Custom history location
|
|
||||||
# max_history = 100 # Future: Limit history size
|
|
||||||
```
|
|
||||||
|
|
||||||
### Minimal Configuration
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# Just the essentials
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Config File
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create directory
|
|
||||||
mkdir -p ~/.config/grokkit
|
|
||||||
|
|
||||||
# Create config with defaults
|
|
||||||
cat > ~/.config/grokkit/config.toml <<'EOF'
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
timeout = 60
|
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
beta = "grok-beta-2"
|
|
||||||
fast = "grok-4-mini"
|
|
||||||
|
|
||||||
[chat]
|
|
||||||
history_file = "~/.config/grokkit/chat_history.json"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
cat ~/.config/grokkit/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Examples
|
|
||||||
|
|
||||||
### Example 1: Development Environment
|
|
||||||
|
|
||||||
Fast iteration, detailed logs, shorter timeouts.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
default_model = "grok-4-mini" # Fast model for quick iterations
|
|
||||||
temperature = 0.5 # More predictable responses
|
|
||||||
timeout = 30 # Fail fast
|
|
||||||
log_level = "debug" # Detailed logging
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
dev = "grok-4-mini"
|
|
||||||
prod = "grok-4"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Usage
|
|
||||||
grokkit chat -m dev # Fast model
|
|
||||||
grokkit review --debug # With detailed logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Production Environment
|
|
||||||
|
|
||||||
Reliable, balanced settings.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
timeout = 120 # Longer timeout for reliability
|
|
||||||
log_level = "warn" # Only warnings and errors
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
prod = "grok-4"
|
|
||||||
fast = "grok-4-mini"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Creative Writing
|
|
||||||
|
|
||||||
Higher temperature for more creative responses.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config-creative.toml
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.9 # More creative
|
|
||||||
timeout = 90
|
|
||||||
|
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
creative = "grok-4"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use with environment variable pointing to alt config
|
|
||||||
# (requires code modification to support)
|
|
||||||
grokkit chat # Uses high creativity settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 4: Team Settings
|
|
||||||
|
|
||||||
Standardized settings for team consistency.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# team-shared-config.toml
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
timeout = 60
|
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
review = "grok-4" # For code reviews
|
|
||||||
commit = "grok-4-mini" # For commit messages
|
|
||||||
docs = "grok-4" # For documentation
|
|
||||||
|
|
||||||
[chat]
|
|
||||||
# Don't save history in team environment
|
|
||||||
# (requires code modification)
|
|
||||||
save_history = false
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Usage
|
|
||||||
grokkit review -m review # Explicit model for reviews
|
|
||||||
grokkit commit -m commit # Fast model for commits
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 5: Cost Optimization
|
|
||||||
|
|
||||||
Minimize API costs while maintaining functionality.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
default_model = "grok-4-mini" # Cheaper model
|
|
||||||
temperature = 0.5 # More deterministic = fewer tokens
|
|
||||||
timeout = 45
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
cheap = "grok-4-mini"
|
|
||||||
expensive = "grok-4"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use cheap model by default
|
|
||||||
grokkit chat
|
|
||||||
|
|
||||||
# Use expensive model only when needed
|
|
||||||
grokkit edit complex.go "major refactor" -m expensive
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Use Cases
|
|
||||||
|
|
||||||
### Multiple Profiles
|
|
||||||
|
|
||||||
Create different config files for different use cases.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Directory structure
|
|
||||||
~/.config/grokkit/
|
|
||||||
├── config.toml # Default
|
|
||||||
├── config-dev.toml # Development
|
|
||||||
├── config-creative.toml # Creative work
|
|
||||||
└── config-production.toml # Production use
|
|
||||||
|
|
||||||
# Symlink to switch profiles
|
|
||||||
ln -sf ~/.config/grokkit/config-dev.toml ~/.config/grokkit/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project-specific Settings
|
|
||||||
|
|
||||||
Place `.env` file in project root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# /path/to/project/.env
|
|
||||||
export XAI_API_KEY=sk-project-specific-key
|
|
||||||
export GROKKIT_MODEL=grok-4-mini
|
|
||||||
export GROKKIT_LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# Load in shell
|
|
||||||
source .env
|
|
||||||
grokkit chat # Uses project settings
|
|
||||||
```
|
|
||||||
|
|
||||||
### Per-command Defaults
|
|
||||||
|
|
||||||
Use aliases for command-specific models:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[aliases]
|
|
||||||
chat = "grok-4" # Best quality for chat
|
|
||||||
edit = "grok-4" # Precision for edits
|
|
||||||
review = "grok-4" # Thorough reviews
|
|
||||||
commit = "grok-4-mini" # Fast for simple commits
|
|
||||||
history = "grok-4-mini" # Fast summaries
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Manually specify when calling
|
|
||||||
grokkit chat -m chat
|
|
||||||
grokkit edit file.go "task" -m edit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom History Location
|
|
||||||
|
|
||||||
Useful for separating concerns or compliance:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[chat]
|
|
||||||
# Separate histories per project
|
|
||||||
history_file = "~/projects/current/.grokkit-history.json"
|
|
||||||
|
|
||||||
# Or don't save history at all
|
|
||||||
history_file = "/dev/null"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Network-restricted Environments
|
|
||||||
|
|
||||||
Adjust timeouts for slow networks:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
timeout = 300 # 5 minutes for slow connections
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use proxy settings:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export HTTP_PROXY=http://proxy.corp.com:8080
|
|
||||||
export HTTPS_PROXY=http://proxy.corp.com:8080
|
|
||||||
grokkit chat
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Keep API Keys Secure
|
|
||||||
|
|
||||||
**❌ Don't:**
|
|
||||||
```toml
|
|
||||||
# config.toml
|
|
||||||
api_key = "sk-123..." # ❌ DON'T put in config file
|
|
||||||
```
|
|
||||||
|
|
||||||
**✅ Do:**
|
|
||||||
```bash
|
|
||||||
# Use environment variables
|
|
||||||
export XAI_API_KEY=sk-...
|
|
||||||
|
|
||||||
# Or use a secrets manager
|
|
||||||
export XAI_API_KEY=$(vault kv get -field=key secret/grokkit)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Use Aliases for Workflows
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[aliases]
|
|
||||||
# Descriptive names
|
|
||||||
review-critical = "grok-4" # Thorough for critical code
|
|
||||||
review-routine = "grok-4-mini" # Fast for routine changes
|
|
||||||
refactor = "grok-4" # Best model for complex refactoring
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Adjust Temperature by Use Case
|
|
||||||
|
|
||||||
| Use Case | Temperature | Reason |
|
|
||||||
|----------|-------------|--------|
|
|
||||||
| Code generation | 0.3 - 0.5 | Predictable, correct code |
|
|
||||||
| Code review | 0.5 - 0.7 | Balanced analysis |
|
|
||||||
| Commit messages | 0.5 | Consistent format |
|
|
||||||
| Documentation | 0.7 | Clear but varied language |
|
|
||||||
| Creative writing | 0.8 - 1.0 | More variety |
|
|
||||||
| Debugging | 0.3 | Precise analysis |
|
|
||||||
|
|
||||||
### 4. Manage Log Levels
|
|
||||||
|
|
||||||
| Environment | Log Level | Reason |
|
|
||||||
|-------------|-----------|--------|
|
|
||||||
| Development | `debug` | See everything |
|
|
||||||
| Staging | `info` | Track operations |
|
|
||||||
| Production | `warn` | Errors and warnings only |
|
|
||||||
| CI/CD | `error` | Failures only |
|
|
||||||
|
|
||||||
### 5. Set Appropriate Timeouts
|
|
||||||
|
|
||||||
| Network | Timeout | Reason |
|
|
||||||
|---------|---------|--------|
|
|
||||||
| Fast (LAN) | 30s | Fail fast |
|
|
||||||
| Normal (broadband) | 60s | Balanced |
|
|
||||||
| Slow (mobile/VPN) | 120s | Allow completion |
|
|
||||||
| Restricted (corporate) | 180s | Account for proxies |
|
|
||||||
|
|
||||||
### 6. Version Control Config
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add template to repo (without secrets)
|
|
||||||
cat > .grokkit-config.template.toml <<'EOF'
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
timeout = 60
|
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
dev = "grok-4-mini"
|
|
||||||
prod = "grok-4"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# .gitignore
|
|
||||||
echo "config.toml" >> .gitignore # Don't commit actual config
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Document Team Conventions
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# team-config.toml - Team standard configuration
|
|
||||||
# Copy this file to ~/.config/grokkit/config.toml
|
|
||||||
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7 # Don't change without team discussion
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
# Use these for consistency
|
|
||||||
code-review = "grok-4"
|
|
||||||
quick-commit = "grok-4-mini"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Schema
|
|
||||||
|
|
||||||
Reference for all available options:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# String: Model identifier
|
|
||||||
default_model = "grok-4"
|
|
||||||
|
|
||||||
# Float: 0.0-1.0 (creativity)
|
|
||||||
temperature = 0.7
|
|
||||||
|
|
||||||
# Integer: Seconds (API timeout)
|
|
||||||
timeout = 60
|
|
||||||
|
|
||||||
# String: "debug" | "info" | "warn" | "error"
|
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
# Map of string to string
|
|
||||||
[aliases]
|
|
||||||
alias_name = "actual-model-name"
|
|
||||||
|
|
||||||
# Chat-specific settings
|
|
||||||
[chat]
|
|
||||||
history_file = "/path/to/history.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting Config
|
|
||||||
|
|
||||||
### Check Current Config
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View config file
|
|
||||||
cat ~/.config/grokkit/config.toml
|
|
||||||
|
|
||||||
# Test with debug mode (shows resolved config)
|
|
||||||
grokkit chat --debug 2>&1 | head -20
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validate TOML Syntax
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Online validator
|
|
||||||
# https://www.toml-lint.com/
|
|
||||||
|
|
||||||
# Or install tomll
|
|
||||||
go install github.com/pelletier/go-toml/v2/cmd/tomll@latest
|
|
||||||
tomll ~/.config/grokkit/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset to Defaults
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup current config
|
|
||||||
mv ~/.config/grokkit/config.toml ~/.config/grokkit/config.toml.backup
|
|
||||||
|
|
||||||
# App will use defaults
|
|
||||||
grokkit chat
|
|
||||||
|
|
||||||
# Restore if needed
|
|
||||||
mv ~/.config/grokkit/config.toml.backup ~/.config/grokkit/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**See also:**
|
|
||||||
- [Troubleshooting Guide](TROUBLESHOOTING.md)
|
|
||||||
- [Architecture Overview](ARCHITECTURE.md)
|
|
||||||
@ -1,668 +0,0 @@
|
|||||||
# Troubleshooting Guide
|
|
||||||
|
|
||||||
Common issues and solutions for Grokkit.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Installation Issues](#installation-issues)
|
|
||||||
- [API & Network Issues](#api--network-issues)
|
|
||||||
- [Configuration Issues](#configuration-issues)
|
|
||||||
- [Git Integration Issues](#git-integration-issues)
|
|
||||||
- [File Operation Issues](#file-operation-issues)
|
|
||||||
- [Logging Issues](#logging-issues)
|
|
||||||
- [Performance Issues](#performance-issues)
|
|
||||||
|
|
||||||
## Installation Issues
|
|
||||||
|
|
||||||
### Go version mismatch
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
go: module gmgauthier.com/grokkit requires Go 1.24 or later
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check your Go version
|
|
||||||
go version
|
|
||||||
|
|
||||||
# Update Go to 1.24+
|
|
||||||
# Download from https://go.dev/dl/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Make command not found
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
bash: make: command not found
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# macOS
|
|
||||||
brew install make
|
|
||||||
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt-get install build-essential
|
|
||||||
|
|
||||||
# Fedora
|
|
||||||
sudo dnf install make
|
|
||||||
|
|
||||||
# Or build manually
|
|
||||||
go build -o grokkit .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Installation to ~/.local/bin fails
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
mkdir: cannot create directory '/home/user/.local/bin': Permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Create the directory manually
|
|
||||||
mkdir -p ~/.local/bin
|
|
||||||
|
|
||||||
# Add to PATH if not already
|
|
||||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Then reinstall
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
## API & Network Issues
|
|
||||||
|
|
||||||
### API key not set
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: XAI_API_KEY environment variable not set
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Set for current session
|
|
||||||
export XAI_API_KEY=sk-your-key-here
|
|
||||||
|
|
||||||
# Set permanently (bash)
|
|
||||||
echo 'export XAI_API_KEY=sk-your-key-here' >> ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Set permanently (zsh)
|
|
||||||
echo 'export XAI_API_KEY=sk-your-key-here' >> ~/.zshrc
|
|
||||||
source ~/.zshrc
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
echo $XAI_API_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request timeout
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: Request failed: context deadline exceeded
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
**1. Increase timeout in config:**
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
timeout = 120 # Increase to 120 seconds
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Check network connectivity:**
|
|
||||||
```bash
|
|
||||||
# Test xAI API access
|
|
||||||
curl -I https://api.x.ai/v1/models
|
|
||||||
|
|
||||||
# Check DNS resolution
|
|
||||||
nslookup api.x.ai
|
|
||||||
|
|
||||||
# Test with debug mode
|
|
||||||
grokkit chat --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Check for proxy issues:**
|
|
||||||
```bash
|
|
||||||
# If behind corporate proxy
|
|
||||||
export HTTP_PROXY=http://proxy.example.com:8080
|
|
||||||
export HTTPS_PROXY=http://proxy.example.com:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### API rate limiting
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: API error (status 429): Rate limit exceeded
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Wait before retrying
|
|
||||||
sleep 60
|
|
||||||
|
|
||||||
# Use different model
|
|
||||||
grokkit chat -m grok-4-mini # Faster, cheaper model
|
|
||||||
|
|
||||||
# Check your xAI dashboard for limits
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL/TLS certificate errors
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: x509: certificate signed by unknown authority
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Update CA certificates (Linux)
|
|
||||||
sudo update-ca-certificates
|
|
||||||
|
|
||||||
# Update CA certificates (macOS)
|
|
||||||
sudo security authorizationdb read com.apple.trust-settings > /tmp/trust.plist
|
|
||||||
sudo security authorizationdb write com.apple.trust-settings < /tmp/trust.plist
|
|
||||||
|
|
||||||
# Or check system time (certificate validation depends on time)
|
|
||||||
date
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Issues
|
|
||||||
|
|
||||||
### Config file not found
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Config settings don't take effect.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Create config directory
|
|
||||||
mkdir -p ~/.config/grokkit
|
|
||||||
|
|
||||||
# Create config file
|
|
||||||
cat > ~/.config/grokkit/config.toml <<EOF
|
|
||||||
default_model = "grok-4"
|
|
||||||
temperature = 0.7
|
|
||||||
log_level = "info"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Verify location
|
|
||||||
ls -la ~/.config/grokkit/config.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invalid TOML syntax
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: failed to read config
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Validate TOML syntax online: https://www.toml-lint.com/
|
|
||||||
|
|
||||||
# Or check with Go
|
|
||||||
go run -mod=mod github.com/pelletier/go-toml/v2/cmd/tomll@latest ~/.config/grokkit/config.toml
|
|
||||||
|
|
||||||
# Common errors:
|
|
||||||
# - Missing quotes around strings
|
|
||||||
# - Wrong indentation in sections
|
|
||||||
# - Duplicate keys
|
|
||||||
```
|
|
||||||
|
|
||||||
### Model alias not working
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Using alias returns error or uses wrong model.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
# Ensure aliases section is properly formatted
|
|
||||||
|
|
||||||
[aliases]
|
|
||||||
beta = "grok-beta-2" # ✓ Correct
|
|
||||||
fast = "grok-4-mini" # ✓ Correct
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test alias
|
|
||||||
grokkit chat -m beta --debug # Check which model is actually used
|
|
||||||
```
|
|
||||||
|
|
||||||
## Git Integration Issues
|
|
||||||
|
|
||||||
### Not in a git repository
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: fatal: not a git repository
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Initialize git repo if needed
|
|
||||||
git init
|
|
||||||
|
|
||||||
# Or cd into a git repository
|
|
||||||
cd /path/to/your/repo
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
git status
|
|
||||||
```
|
|
||||||
|
|
||||||
### No staged changes
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
No staged changes!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Stage your changes first
|
|
||||||
git add <files>
|
|
||||||
|
|
||||||
# Or stage all changes
|
|
||||||
git add .
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
git status
|
|
||||||
|
|
||||||
# Then run grokkit
|
|
||||||
grokkit commit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git command not found
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: git command failed: exec: "git": executable file not found
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Install git
|
|
||||||
# macOS
|
|
||||||
brew install git
|
|
||||||
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt-get install git
|
|
||||||
|
|
||||||
# Fedora
|
|
||||||
sudo dnf install git
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
git --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Operation Issues
|
|
||||||
|
|
||||||
### File not found
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
File not found: main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check file exists
|
|
||||||
ls -la main.go
|
|
||||||
|
|
||||||
# Use absolute or relative path
|
|
||||||
grokkit edit ./main.go "instruction"
|
|
||||||
grokkit edit /full/path/to/main.go "instruction"
|
|
||||||
|
|
||||||
# Check current directory
|
|
||||||
pwd
|
|
||||||
ls
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission denied on backup creation
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Failed to create backup: permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check file permissions
|
|
||||||
ls -la main.go
|
|
||||||
|
|
||||||
# Check directory permissions
|
|
||||||
ls -ld .
|
|
||||||
|
|
||||||
# Fix permissions
|
|
||||||
chmod 644 main.go # File
|
|
||||||
chmod 755 . # Directory
|
|
||||||
|
|
||||||
# Or run from writable directory
|
|
||||||
```
|
|
||||||
|
|
||||||
### Failed to write file
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Failed to write file: permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check if file is read-only
|
|
||||||
ls -la main.go
|
|
||||||
|
|
||||||
# Make writable
|
|
||||||
chmod 644 main.go
|
|
||||||
|
|
||||||
# Check disk space
|
|
||||||
df -h .
|
|
||||||
|
|
||||||
# Check if file is open in another program
|
|
||||||
lsof | grep main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging Issues
|
|
||||||
|
|
||||||
### Log file permission denied
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
Error: failed to open log file: permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Fix log directory permissions
|
|
||||||
chmod 755 ~/.config/grokkit
|
|
||||||
chmod 644 ~/.config/grokkit/grokkit.log
|
|
||||||
|
|
||||||
# Or recreate log directory
|
|
||||||
rm -rf ~/.config/grokkit
|
|
||||||
mkdir -p ~/.config/grokkit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Log file too large
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Log file consuming excessive disk space.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check log size
|
|
||||||
du -h ~/.config/grokkit/grokkit.log
|
|
||||||
|
|
||||||
# Truncate log file
|
|
||||||
: > ~/.config/grokkit/grokkit.log
|
|
||||||
|
|
||||||
# Or rotate logs manually
|
|
||||||
mv ~/.config/grokkit/grokkit.log ~/.config/grokkit/grokkit.log.old
|
|
||||||
gzip ~/.config/grokkit/grokkit.log.old
|
|
||||||
|
|
||||||
# Set up log rotation (Linux)
|
|
||||||
sudo tee /etc/logrotate.d/grokkit <<EOF
|
|
||||||
/home/*/.config/grokkit/grokkit.log {
|
|
||||||
weekly
|
|
||||||
rotate 4
|
|
||||||
compress
|
|
||||||
missingok
|
|
||||||
notifempty
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug output not showing
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
`--debug` flag doesn't show output.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Ensure you're using the flag correctly
|
|
||||||
grokkit chat --debug # ✓ Correct
|
|
||||||
grokkit --debug chat # ✗ Wrong order
|
|
||||||
|
|
||||||
# Check log level in config
|
|
||||||
cat ~/.config/grokkit/config.toml | grep log_level
|
|
||||||
|
|
||||||
# Force debug via config
|
|
||||||
echo 'log_level = "debug"' >> ~/.config/grokkit/config.toml
|
|
||||||
|
|
||||||
# Or set via environment
|
|
||||||
export LOG_LEVEL=debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Issues
|
|
||||||
|
|
||||||
### Slow API responses
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Requests take very long to complete.
|
|
||||||
|
|
||||||
**Solutions:**
|
|
||||||
|
|
||||||
**1. Use faster model:**
|
|
||||||
```bash
|
|
||||||
grokkit chat -m grok-4-mini
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Reduce temperature (makes responses more deterministic/faster):**
|
|
||||||
```toml
|
|
||||||
# ~/.config/grokkit/config.toml
|
|
||||||
temperature = 0.3
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Check network latency:**
|
|
||||||
```bash
|
|
||||||
# Test API latency
|
|
||||||
time curl -s https://api.x.ai/v1/models > /dev/null
|
|
||||||
|
|
||||||
# Check DNS resolution time
|
|
||||||
time nslookup api.x.ai
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Review logs for timing:**
|
|
||||||
```bash
|
|
||||||
# Check actual API duration
|
|
||||||
cat ~/.config/grokkit/grokkit.log | \
|
|
||||||
jq 'select(.msg=="API request completed") | {model, duration_ms}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### High memory usage
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Grokkit consuming excessive memory.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check memory usage
|
|
||||||
top -p $(pgrep grokkit)
|
|
||||||
|
|
||||||
# Clear chat history if very large
|
|
||||||
rm ~/.config/grokkit/chat_history.json
|
|
||||||
|
|
||||||
# Limit history size (future feature)
|
|
||||||
# For now, manually trim history file
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chat history growing too large
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Chat becomes slow or fails to save.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Check history size
|
|
||||||
du -h ~/.config/grokkit/chat_history.json
|
|
||||||
|
|
||||||
# Back up and clear
|
|
||||||
mv ~/.config/grokkit/chat_history.json \
|
|
||||||
~/.config/grokkit/chat_history.backup.json
|
|
||||||
|
|
||||||
# Or manually trim to last N exchanges
|
|
||||||
cat ~/.config/grokkit/chat_history.json | \
|
|
||||||
jq '.messages[-20:]' > ~/.config/grokkit/chat_history.new.json
|
|
||||||
mv ~/.config/grokkit/chat_history.new.json \
|
|
||||||
~/.config/grokkit/chat_history.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Linter Issues
|
|
||||||
|
|
||||||
### No linter found for language
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
⚠️ no linter found for Go
|
|
||||||
|
|
||||||
Install one of:
|
|
||||||
- golangci-lint: https://golangci-lint.run/usage/install/
|
|
||||||
- go vet: Built-in with Go
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Install appropriate linter for your language
|
|
||||||
|
|
||||||
# Go
|
|
||||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|
||||||
# Or use go vet (built-in)
|
|
||||||
|
|
||||||
# Python
|
|
||||||
pip install pylint flake8 ruff
|
|
||||||
|
|
||||||
# JavaScript/TypeScript
|
|
||||||
npm install -g eslint
|
|
||||||
|
|
||||||
# Rust
|
|
||||||
rustup component add clippy
|
|
||||||
|
|
||||||
# Ruby
|
|
||||||
gem install rubocop
|
|
||||||
|
|
||||||
# Shell
|
|
||||||
# Ubuntu/Debian
|
|
||||||
apt install shellcheck
|
|
||||||
# macOS
|
|
||||||
brew install shellcheck
|
|
||||||
|
|
||||||
# Verify installation
|
|
||||||
which golangci-lint # or your linter command
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linter found issues but AI fixes failed
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Linter reports issues but AI returns empty response or invalid code.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# 1. Try with a different model
|
|
||||||
grokkit lint file.py -m grok-4
|
|
||||||
|
|
||||||
# 2. Check the linter output is clear
|
|
||||||
grokkit lint file.py --dry-run
|
|
||||||
|
|
||||||
# 3. Try manual fix first, then use edit command
|
|
||||||
grokkit edit file.py "fix the issues reported by linter"
|
|
||||||
|
|
||||||
# 4. Enable debug logging to see API interaction
|
|
||||||
grokkit lint file.py --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linter verification still shows issues
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
After applying AI fixes, re-running the linter still reports issues.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
# Some issues may require manual intervention
|
|
||||||
# Check the backup file to compare
|
|
||||||
diff file.py file.py.bak
|
|
||||||
|
|
||||||
# Apply fixes iteratively
|
|
||||||
grokkit lint file.py # Apply fixes
|
|
||||||
grokkit lint file.py # Run again on remaining issues
|
|
||||||
|
|
||||||
# For complex issues, use edit with specific instructions
|
|
||||||
grokkit edit file.py "fix the remaining pylint warnings about docstrings"
|
|
||||||
```
|
|
||||||
|
|
||||||
### File extension not recognized
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
```
|
|
||||||
unsupported file type: .xyz
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
Currently supported extensions:
|
|
||||||
- **Go:** `.go`
|
|
||||||
- **Python:** `.py`
|
|
||||||
- **JavaScript:** `.js`, `.jsx`
|
|
||||||
- **TypeScript:** `.ts`, `.tsx`
|
|
||||||
- **Rust:** `.rs`
|
|
||||||
- **Ruby:** `.rb`
|
|
||||||
- **Java:** `.java`
|
|
||||||
- **C/C++:** `.c`, `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp`
|
|
||||||
- **Shell:** `.sh`, `.bash`
|
|
||||||
|
|
||||||
For unsupported languages, use the `edit` command instead:
|
|
||||||
```bash
|
|
||||||
grokkit edit myfile.xyz "review and fix code quality issues"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wrong linter used
|
|
||||||
|
|
||||||
**Symptom:**
|
|
||||||
Tool uses go vet but you want golangci-lint, or uses flake8 but you want ruff.
|
|
||||||
|
|
||||||
**Explanation:**
|
|
||||||
Grokkit tries linters in the order defined and uses the first available one. To use a specific linter, ensure it's installed and others are not in PATH, or contribute to the project to add linter preference configuration.
|
|
||||||
|
|
||||||
**Current linter priority:**
|
|
||||||
- **Go:** golangci-lint → go vet
|
|
||||||
- **Python:** pylint → flake8 → ruff
|
|
||||||
- **JavaScript:** eslint
|
|
||||||
- **TypeScript:** eslint → tsc
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
If none of these solutions work:
|
|
||||||
|
|
||||||
1. **Enable debug mode** to see detailed logging:
|
|
||||||
```bash
|
|
||||||
grokkit <command> --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Check logs** for specific errors:
|
|
||||||
```bash
|
|
||||||
tail -100 ~/.config/grokkit/grokkit.log | jq 'select(.level=="ERROR")'
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Verify environment**:
|
|
||||||
```bash
|
|
||||||
# Check versions
|
|
||||||
go version
|
|
||||||
git --version
|
|
||||||
|
|
||||||
# Check config
|
|
||||||
cat ~/.config/grokkit/config.toml
|
|
||||||
|
|
||||||
# Check API key is set
|
|
||||||
echo $XAI_API_KEY | sed 's/sk-.*$/sk-***/'
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Report issue** with:
|
|
||||||
- Error message
|
|
||||||
- Command used
|
|
||||||
- Relevant logs (sanitize API keys!)
|
|
||||||
- OS and Go version
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Still having issues?** Check the [Configuration Guide](CONFIGURATION.md) or [Architecture](ARCHITECTURE.md) docs.
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
package errors
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// GitError represents errors from git operations
|
|
||||||
type GitError struct {
|
|
||||||
Command string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *GitError) Error() string {
|
|
||||||
return fmt.Sprintf("git %s failed: %v", e.Command, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *GitError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIError represents errors from Grok API calls
|
|
||||||
type APIError struct {
|
|
||||||
StatusCode int
|
|
||||||
Message string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *APIError) Error() string {
|
|
||||||
if e.StatusCode > 0 {
|
|
||||||
return fmt.Sprintf("API error (status %d): %s", e.StatusCode, e.Message)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("API error: %s", e.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *APIError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileError represents file operation errors
|
|
||||||
type FileError struct {
|
|
||||||
Path string
|
|
||||||
Op string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *FileError) Error() string {
|
|
||||||
return fmt.Sprintf("file %s failed for %s: %v", e.Op, e.Path, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *FileError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
package errors
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGitError(t *testing.T) {
|
|
||||||
baseErr := errors.New("command failed")
|
|
||||||
gitErr := &GitError{
|
|
||||||
Command: "status",
|
|
||||||
Err: baseErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "git status failed: command failed"
|
|
||||||
if gitErr.Error() != expected {
|
|
||||||
t.Errorf("GitError.Error() = %q, want %q", gitErr.Error(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
if gitErr.Unwrap() != baseErr {
|
|
||||||
t.Errorf("GitError.Unwrap() did not return base error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIError(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
apiErr *APIError
|
|
||||||
expected string
|
|
||||||
shouldWrap bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with status code",
|
|
||||||
apiErr: &APIError{
|
|
||||||
StatusCode: 404,
|
|
||||||
Message: "not found",
|
|
||||||
},
|
|
||||||
expected: "API error (status 404): not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without status code",
|
|
||||||
apiErr: &APIError{
|
|
||||||
Message: "connection failed",
|
|
||||||
},
|
|
||||||
expected: "API error: connection failed",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.apiErr.Error() != tt.expected {
|
|
||||||
t.Errorf("APIError.Error() = %q, want %q", tt.apiErr.Error(), tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileError(t *testing.T) {
|
|
||||||
baseErr := errors.New("permission denied")
|
|
||||||
fileErr := &FileError{
|
|
||||||
Path: "/tmp/test.txt",
|
|
||||||
Op: "read",
|
|
||||||
Err: baseErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "file read failed for /tmp/test.txt: permission denied"
|
|
||||||
if fileErr.Error() != expected {
|
|
||||||
t.Errorf("FileError.Error() = %q, want %q", fileErr.Error(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileErr.Unwrap() != baseErr {
|
|
||||||
t.Errorf("FileError.Unwrap() did not return base error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Run(args []string) (string, error) {
|
|
||||||
cmdStr := "git " + strings.Join(args, " ")
|
|
||||||
logger.Debug("executing git command", "command", cmdStr, "args", args)
|
|
||||||
|
|
||||||
out, err := exec.Command("git", args...).Output()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("git command failed",
|
|
||||||
"command", cmdStr,
|
|
||||||
"error", err)
|
|
||||||
return "", fmt.Errorf("git command failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputLen := len(out)
|
|
||||||
logger.Debug("git command completed",
|
|
||||||
"command", cmdStr,
|
|
||||||
"output_length", outputLen)
|
|
||||||
|
|
||||||
return string(out), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsRepo() bool {
|
|
||||||
logger.Debug("checking if directory is a git repository")
|
|
||||||
_, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output()
|
|
||||||
isRepo := err == nil
|
|
||||||
logger.Debug("git repository check completed", "is_repo", isRepo)
|
|
||||||
return isRepo
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIsRepo(t *testing.T) {
|
|
||||||
// This test assumes we're running in a git repo
|
|
||||||
result := IsRepo()
|
|
||||||
if !result {
|
|
||||||
t.Log("Warning: Not in a git repository, test may be running in unexpected environment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRun(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
shouldErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "version command succeeds",
|
|
||||||
args: []string{"--version"},
|
|
||||||
shouldErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid command fails",
|
|
||||||
args: []string{"invalid-command-xyz"},
|
|
||||||
shouldErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
output, err := Run(tt.args)
|
|
||||||
if tt.shouldErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Run() expected error, got nil")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Run() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if output == "" {
|
|
||||||
t.Errorf("Run() expected output, got empty string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGitRunner(t *testing.T) {
|
|
||||||
runner := NewRunner()
|
|
||||||
|
|
||||||
// Test Run
|
|
||||||
output, err := runner.Run([]string{"--version"})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GitRunner.Run() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if output == "" {
|
|
||||||
t.Errorf("GitRunner.Run() expected output, got empty string")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test IsRepo
|
|
||||||
_ = runner.IsRepo() // Just ensure it doesn't panic
|
|
||||||
}
|
|
||||||
13
internal/git/helper.go
Normal file
13
internal/git/helper.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
func Run(args []string) string {
|
||||||
|
out, _ := exec.Command("git", args...).Output()
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsRepo() bool {
|
||||||
|
_, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package git
|
|
||||||
|
|
||||||
// GitRunner defines the interface for git operations
|
|
||||||
type GitRunner interface {
|
|
||||||
Run(args []string) (string, error)
|
|
||||||
IsRepo() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultRunner implements GitRunner
|
|
||||||
type DefaultRunner struct{}
|
|
||||||
|
|
||||||
func (r *DefaultRunner) Run(args []string) (string, error) {
|
|
||||||
return Run(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DefaultRunner) IsRepo() bool {
|
|
||||||
return IsRepo()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRunner creates a new git runner
|
|
||||||
func NewRunner() GitRunner {
|
|
||||||
return &DefaultRunner{}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
package grok
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestCleanCodeResponse_Comprehensive(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "removes go markdown fences",
|
|
||||||
input: "```go\npackage main\n\nfunc main() {}\n```",
|
|
||||||
expected: "package main\n\nfunc main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "removes python markdown fences",
|
|
||||||
input: "```python\ndef hello():\n print('hello')\n```",
|
|
||||||
expected: "def hello():\n print('hello')",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "removes plain markdown fences",
|
|
||||||
input: "```\nsome code\n```",
|
|
||||||
expected: "some code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles no fences",
|
|
||||||
input: "func main() {}",
|
|
||||||
expected: "func main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preserves internal blank lines",
|
|
||||||
input: "```\nline1\n\nline2\n\nline3\n```",
|
|
||||||
expected: "line1\n\nline2\n\nline3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "trims leading whitespace",
|
|
||||||
input: " \n\n```\ncode\n```",
|
|
||||||
expected: "code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "trims trailing whitespace",
|
|
||||||
input: "```\ncode\n```\n\n ",
|
|
||||||
expected: "code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles multiple languages",
|
|
||||||
input: "```javascript\nconst x = 1;\n```",
|
|
||||||
expected: "const x = 1;",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles fences with extra spaces",
|
|
||||||
input: "``` \ncode\n``` ",
|
|
||||||
expected: "code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "removes only fence lines",
|
|
||||||
input: "text before\n```go\ncode\n```\ntext after",
|
|
||||||
expected: "text before\n\ncode\n\ntext after",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles empty input",
|
|
||||||
input: "",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles only fences",
|
|
||||||
input: "```\n```",
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preserves code indentation",
|
|
||||||
input: "```\nfunc main() {\n fmt.Println(\"hello\")\n}\n```",
|
|
||||||
expected: "func main() {\n fmt.Println(\"hello\")\n}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := CleanCodeResponse(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("CleanCodeResponse():\ngot: %q\nwant: %q", result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,17 +3,13 @@ package grok
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@ -33,78 +29,37 @@ func NewClient() *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream prints live to terminal (used by non-TUI commands)
|
|
||||||
func (c *Client) Stream(messages []map[string]string, model string) string {
|
func (c *Client) Stream(messages []map[string]string, model string) string {
|
||||||
return c.StreamWithTemp(messages, model, 0.7)
|
return c.streamInternal(messages, model, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamWithTemp allows specifying temperature
|
|
||||||
func (c *Client) StreamWithTemp(messages []map[string]string, model string, temperature float64) string {
|
|
||||||
return c.streamInternal(messages, model, temperature, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamSilent returns the full text without printing (used by TUI and agent)
|
|
||||||
func (c *Client) StreamSilent(messages []map[string]string, model string) string {
|
func (c *Client) StreamSilent(messages []map[string]string, model string) string {
|
||||||
return c.streamInternal(messages, model, 0.7, false)
|
return c.streamInternal(messages, model, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) streamInternal(messages []map[string]string, model string, temperature float64, printLive bool) string {
|
func (c *Client) streamInternal(messages []map[string]string, model string, printLive bool) string {
|
||||||
startTime := time.Now()
|
|
||||||
url := c.BaseURL + "/chat/completions"
|
url := c.BaseURL + "/chat/completions"
|
||||||
|
|
||||||
logger.Debug("preparing API request",
|
|
||||||
"model", model,
|
|
||||||
"temperature", temperature,
|
|
||||||
"message_count", len(messages),
|
|
||||||
"stream", true)
|
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"temperature": temperature,
|
"temperature": 0.7,
|
||||||
"stream": true,
|
"stream": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
if err != nil {
|
req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||||
logger.Error("failed to marshal API request", "error", err)
|
|
||||||
color.Red("Failed to marshal request: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to create HTTP request", "error", err, "url", url)
|
|
||||||
color.Red("Failed to create request: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
logger.Info("sending API request", "model", model, "url", url)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("API request failed",
|
|
||||||
"error", err,
|
|
||||||
"model", model,
|
|
||||||
"duration_ms", time.Since(startTime).Milliseconds())
|
|
||||||
color.Red("Request failed: %v", err)
|
color.Red("Request failed: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
logger.Debug("API response received",
|
|
||||||
"status", resp.Status,
|
|
||||||
"status_code", resp.StatusCode,
|
|
||||||
"duration_ms", time.Since(startTime).Milliseconds())
|
|
||||||
|
|
||||||
var fullReply strings.Builder
|
var fullReply strings.Builder
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
chunkCount := 0
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, "data: ") {
|
if strings.HasPrefix(line, "data: ") {
|
||||||
@ -117,7 +72,6 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
|
|||||||
if choices, ok := chunk["choices"].([]any); ok && len(choices) > 0 {
|
if choices, ok := chunk["choices"].([]any); ok && len(choices) > 0 {
|
||||||
if delta, ok := choices[0].(map[string]any)["delta"].(map[string]any); ok {
|
if delta, ok := choices[0].(map[string]any)["delta"].(map[string]any); ok {
|
||||||
if content, ok := delta["content"].(string); ok && content != "" {
|
if content, ok := delta["content"].(string); ok && content != "" {
|
||||||
chunkCount++
|
|
||||||
fullReply.WriteString(content)
|
fullReply.WriteString(content)
|
||||||
if printLive {
|
if printLive {
|
||||||
fmt.Print(content)
|
fmt.Print(content)
|
||||||
@ -131,33 +85,12 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
|
|||||||
if printLive {
|
if printLive {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
responseLength := fullReply.Len()
|
|
||||||
duration := time.Since(startTime)
|
|
||||||
|
|
||||||
logger.Info("API request completed",
|
|
||||||
"model", model,
|
|
||||||
"response_length", responseLength,
|
|
||||||
"chunks_received", chunkCount,
|
|
||||||
"duration_ms", duration.Milliseconds(),
|
|
||||||
"duration", duration.String())
|
|
||||||
|
|
||||||
return fullReply.String()
|
return fullReply.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanCodeResponse removes markdown fences and returns pure code content
|
|
||||||
func CleanCodeResponse(text string) string {
|
func CleanCodeResponse(text string) string {
|
||||||
fence := "```"
|
text = strings.ReplaceAll(text, "", "")
|
||||||
|
text = strings.ReplaceAll(text, "", "")
|
||||||
// Remove any line that starts with the fence (opening fence, possibly with language tag)
|
|
||||||
text = regexp.MustCompile(`(?m)^`+regexp.QuoteMeta(fence)+`.*$`).ReplaceAllString(text, "")
|
|
||||||
|
|
||||||
// Remove any line that is just the fence (closing fence)
|
|
||||||
text = regexp.MustCompile(`(?m)^`+regexp.QuoteMeta(fence)+`\s*$`).ReplaceAllString(text, "")
|
|
||||||
|
|
||||||
// Trim only leading and trailing whitespace.
|
|
||||||
// Do NOT collapse internal blank lines — they are intentional in code.
|
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
|
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
package grok
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCleanCodeResponse(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "removes markdown fences",
|
|
||||||
input: "```go\nfunc main() {}\n```",
|
|
||||||
expected: "func main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "removes language tag",
|
|
||||||
input: "```python\nprint('hello')\n```",
|
|
||||||
expected: "print('hello')",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "handles no fences",
|
|
||||||
input: "func main() {}",
|
|
||||||
expected: "func main() {}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "preserves internal blank lines",
|
|
||||||
input: "```\nline1\n\nline2\n```",
|
|
||||||
expected: "line1\n\nline2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "trims whitespace",
|
|
||||||
input: " \n```\ncode\n```\n ",
|
|
||||||
expected: "code",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := CleanCodeResponse(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("CleanCodeResponse() = %q, want %q", result, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
|
||||||
// Save and restore env
|
|
||||||
oldKey := os.Getenv("XAI_API_KEY")
|
|
||||||
defer func() {
|
|
||||||
if oldKey != "" {
|
|
||||||
os.Setenv("XAI_API_KEY", oldKey)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("XAI_API_KEY")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
os.Setenv("XAI_API_KEY", "test-key")
|
|
||||||
client := NewClient()
|
|
||||||
|
|
||||||
if client.APIKey != "test-key" {
|
|
||||||
t.Errorf("NewClient() APIKey = %q, want %q", client.APIKey, "test-key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.BaseURL != "https://api.x.ai/v1" {
|
|
||||||
t.Errorf("NewClient() BaseURL = %q, want %q", client.BaseURL, "https://api.x.ai/v1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package grok
|
|
||||||
|
|
||||||
// AIClient defines the interface for AI interactions
|
|
||||||
type AIClient interface {
|
|
||||||
Stream(messages []map[string]string, model string) string
|
|
||||||
StreamSilent(messages []map[string]string, model string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Client implements AIClient
|
|
||||||
var _ AIClient = (*Client)(nil)
|
|
||||||
@ -1,310 +0,0 @@
|
|||||||
package linter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gmgauthier.com/grokkit/internal/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Language represents a programming language
|
|
||||||
type Language struct {
|
|
||||||
Name string
|
|
||||||
Extensions []string
|
|
||||||
Linters []Linter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linter represents a linting tool
|
|
||||||
type Linter struct {
|
|
||||||
Name string
|
|
||||||
Command string
|
|
||||||
Args []string
|
|
||||||
InstallInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
// LintResult contains linting results
|
|
||||||
type LintResult struct {
|
|
||||||
Language string
|
|
||||||
LinterUsed string
|
|
||||||
Output string
|
|
||||||
ExitCode int
|
|
||||||
HasIssues bool
|
|
||||||
LinterExists bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supported languages and their linters
|
|
||||||
var languages = []Language{
|
|
||||||
{
|
|
||||||
Name: "Go",
|
|
||||||
Extensions: []string{".go"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "golangci-lint",
|
|
||||||
Command: "golangci-lint",
|
|
||||||
Args: []string{"run"},
|
|
||||||
InstallInfo: "https://golangci-lint.run/usage/install/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "go vet",
|
|
||||||
Command: "go",
|
|
||||||
Args: []string{"vet"},
|
|
||||||
InstallInfo: "Built-in with Go",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Python",
|
|
||||||
Extensions: []string{".py"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "pylint",
|
|
||||||
Command: "pylint",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "pip install pylint",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "flake8",
|
|
||||||
Command: "flake8",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "pip install flake8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "ruff",
|
|
||||||
Command: "ruff",
|
|
||||||
Args: []string{"check"},
|
|
||||||
InstallInfo: "pip install ruff",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "JavaScript",
|
|
||||||
Extensions: []string{".js", ".jsx"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "eslint",
|
|
||||||
Command: "eslint",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "npm install -g eslint",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "TypeScript",
|
|
||||||
Extensions: []string{".ts", ".tsx"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "eslint",
|
|
||||||
Command: "eslint",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "npm install -g eslint @typescript-eslint/parser",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tsc",
|
|
||||||
Command: "tsc",
|
|
||||||
Args: []string{"--noEmit"},
|
|
||||||
InstallInfo: "npm install -g typescript",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Rust",
|
|
||||||
Extensions: []string{".rs"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "clippy",
|
|
||||||
Command: "cargo",
|
|
||||||
Args: []string{"clippy", "--"},
|
|
||||||
InstallInfo: "rustup component add clippy",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Ruby",
|
|
||||||
Extensions: []string{".rb"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "rubocop",
|
|
||||||
Command: "rubocop",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "gem install rubocop",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Java",
|
|
||||||
Extensions: []string{".java"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "checkstyle",
|
|
||||||
Command: "checkstyle",
|
|
||||||
Args: []string{"-c", "/google_checks.xml"},
|
|
||||||
InstallInfo: "https://checkstyle.org/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "C/C++",
|
|
||||||
Extensions: []string{".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "clang-tidy",
|
|
||||||
Command: "clang-tidy",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "apt install clang-tidy or brew install llvm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Shell",
|
|
||||||
Extensions: []string{".sh", ".bash"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "shellcheck",
|
|
||||||
Command: "shellcheck",
|
|
||||||
Args: []string{},
|
|
||||||
InstallInfo: "apt install shellcheck or brew install shellcheck",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectLanguage detects the programming language based on file extension
|
|
||||||
func DetectLanguage(filePath string) (*Language, error) {
|
|
||||||
ext := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
|
|
||||||
logger.Debug("detecting language", "file", filePath, "extension", ext)
|
|
||||||
|
|
||||||
for i := range languages {
|
|
||||||
for _, langExt := range languages[i].Extensions {
|
|
||||||
if ext == langExt {
|
|
||||||
logger.Info("language detected", "file", filePath, "language", languages[i].Name)
|
|
||||||
return &languages[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unsupported file type: %s", ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckLinterAvailable checks if a linter command is available
|
|
||||||
func CheckLinterAvailable(linter Linter) bool {
|
|
||||||
_, err := exec.LookPath(linter.Command)
|
|
||||||
available := err == nil
|
|
||||||
logger.Debug("checking linter availability",
|
|
||||||
"linter", linter.Name,
|
|
||||||
"command", linter.Command,
|
|
||||||
"available", available)
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAvailableLinter finds the first available linter for a language
|
|
||||||
func FindAvailableLinter(lang *Language) (*Linter, error) {
|
|
||||||
logger.Info("searching for available linter", "language", lang.Name, "options", len(lang.Linters))
|
|
||||||
|
|
||||||
for i := range lang.Linters {
|
|
||||||
if CheckLinterAvailable(lang.Linters[i]) {
|
|
||||||
logger.Info("found available linter",
|
|
||||||
"language", lang.Name,
|
|
||||||
"linter", lang.Linters[i].Name)
|
|
||||||
return &lang.Linters[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build install instructions
|
|
||||||
var installOptions []string
|
|
||||||
for _, linter := range lang.Linters {
|
|
||||||
installOptions = append(installOptions,
|
|
||||||
fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no linter found for %s\n\nInstall one of:\n%s",
|
|
||||||
lang.Name, strings.Join(installOptions, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunLinter executes the linter on the specified file
|
|
||||||
func RunLinter(filePath string, linter Linter) (*LintResult, error) {
|
|
||||||
logger.Info("running linter",
|
|
||||||
"file", filePath,
|
|
||||||
"linter", linter.Name,
|
|
||||||
"command", linter.Command)
|
|
||||||
|
|
||||||
// Build command arguments
|
|
||||||
args := append(linter.Args, filePath)
|
|
||||||
|
|
||||||
// Execute linter
|
|
||||||
cmd := exec.Command(linter.Command, args...)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
|
|
||||||
result := &LintResult{
|
|
||||||
LinterUsed: linter.Name,
|
|
||||||
Output: string(output),
|
|
||||||
LinterExists: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
||||||
result.ExitCode = exitErr.ExitCode()
|
|
||||||
result.HasIssues = true
|
|
||||||
logger.Info("linter found issues",
|
|
||||||
"linter", linter.Name,
|
|
||||||
"exit_code", result.ExitCode,
|
|
||||||
"output_length", len(output))
|
|
||||||
} else {
|
|
||||||
logger.Error("linter execution failed",
|
|
||||||
"linter", linter.Name,
|
|
||||||
"error", err)
|
|
||||||
return nil, fmt.Errorf("failed to execute %s: %w", linter.Name, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.ExitCode = 0
|
|
||||||
result.HasIssues = false
|
|
||||||
logger.Info("linter passed with no issues", "linter", linter.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LintFile detects language and runs appropriate linter
|
|
||||||
func LintFile(filePath string) (*LintResult, error) {
|
|
||||||
// Check file exists
|
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("file not found: %s", filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect language
|
|
||||||
lang, err := DetectLanguage(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find available linter
|
|
||||||
linter, err := FindAvailableLinter(lang)
|
|
||||||
if err != nil {
|
|
||||||
return &LintResult{
|
|
||||||
Language: lang.Name,
|
|
||||||
LinterExists: false,
|
|
||||||
Output: err.Error(),
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run linter
|
|
||||||
result, err := RunLinter(filePath, *linter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Language = lang.Name
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSupportedLanguages returns a list of all supported languages
|
|
||||||
func GetSupportedLanguages() []string {
|
|
||||||
var langs []string
|
|
||||||
for _, lang := range languages {
|
|
||||||
langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", ")))
|
|
||||||
}
|
|
||||||
return langs
|
|
||||||
}
|
|
||||||
@ -1,334 +0,0 @@
|
|||||||
package linter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDetectLanguage(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
filePath string
|
|
||||||
wantLang string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"Go file", "main.go", "Go", false},
|
|
||||||
{"Python file", "script.py", "Python", false},
|
|
||||||
{"JavaScript file", "app.js", "JavaScript", false},
|
|
||||||
{"JSX file", "component.jsx", "JavaScript", false},
|
|
||||||
{"TypeScript file", "app.ts", "TypeScript", false},
|
|
||||||
{"TSX file", "component.tsx", "TypeScript", false},
|
|
||||||
{"Rust file", "main.rs", "Rust", false},
|
|
||||||
{"Ruby file", "script.rb", "Ruby", false},
|
|
||||||
{"Java file", "Main.java", "Java", false},
|
|
||||||
{"C file", "program.c", "C/C++", false},
|
|
||||||
{"C++ file", "program.cpp", "C/C++", false},
|
|
||||||
{"Header file", "header.h", "C/C++", false},
|
|
||||||
{"Shell script", "script.sh", "Shell", false},
|
|
||||||
{"Bash script", "script.bash", "Shell", false},
|
|
||||||
{"Unsupported file", "file.txt", "", true},
|
|
||||||
{"No extension", "Makefile", "", true},
|
|
||||||
{"Case insensitive", "MAIN.GO", "Go", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
lang, err := DetectLanguage(tt.filePath)
|
|
||||||
if tt.wantErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("DetectLanguage(%q) expected error, got nil", tt.filePath)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("DetectLanguage(%q) unexpected error: %v", tt.filePath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if lang.Name != tt.wantLang {
|
|
||||||
t.Errorf("DetectLanguage(%q) = %q, want %q", tt.filePath, lang.Name, tt.wantLang)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckLinterAvailable(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
linter Linter
|
|
||||||
wantMsg string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "go command should be available",
|
|
||||||
linter: Linter{
|
|
||||||
Name: "go",
|
|
||||||
Command: "go",
|
|
||||||
},
|
|
||||||
wantMsg: "go should be available on system with Go installed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nonexistent command",
|
|
||||||
linter: Linter{
|
|
||||||
Name: "nonexistent",
|
|
||||||
Command: "this-command-does-not-exist-12345",
|
|
||||||
},
|
|
||||||
wantMsg: "nonexistent command should not be available",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
available := CheckLinterAvailable(tt.linter)
|
|
||||||
// We can't assert exact true/false since it depends on the system
|
|
||||||
// Just verify the function runs without panic
|
|
||||||
t.Logf("%s: available=%v", tt.wantMsg, available)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindAvailableLinter(t *testing.T) {
|
|
||||||
// Test with Go language since 'go' command should be available
|
|
||||||
// (we're running tests with Go installed)
|
|
||||||
t.Run("Go language should find a linter", func(t *testing.T) {
|
|
||||||
goLang := &Language{
|
|
||||||
Name: "Go",
|
|
||||||
Extensions: []string{".go"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "go vet",
|
|
||||||
Command: "go",
|
|
||||||
Args: []string{"vet"},
|
|
||||||
InstallInfo: "Built-in with Go",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
linter, err := FindAvailableLinter(goLang)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("FindAvailableLinter(Go) expected to find 'go', got error: %v", err)
|
|
||||||
}
|
|
||||||
if linter == nil {
|
|
||||||
t.Errorf("FindAvailableLinter(Go) returned nil linter")
|
|
||||||
} else if linter.Command != "go" {
|
|
||||||
t.Errorf("FindAvailableLinter(Go) = %q, want 'go'", linter.Command)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Language with no available linters", func(t *testing.T) {
|
|
||||||
fakeLang := &Language{
|
|
||||||
Name: "FakeLang",
|
|
||||||
Extensions: []string{".fake"},
|
|
||||||
Linters: []Linter{
|
|
||||||
{
|
|
||||||
Name: "fakelint",
|
|
||||||
Command: "this-does-not-exist-12345",
|
|
||||||
InstallInfo: "not installable",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
linter, err := FindAvailableLinter(fakeLang)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("FindAvailableLinter(FakeLang) expected error, got linter: %v", linter)
|
|
||||||
}
|
|
||||||
if linter != nil {
|
|
||||||
t.Errorf("FindAvailableLinter(FakeLang) expected nil linter, got: %v", linter)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunLinter(t *testing.T) {
|
|
||||||
// Create a temporary Go file with a simple error
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
|
||||||
|
|
||||||
// Valid Go code
|
|
||||||
validCode := `package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello, World!")
|
|
||||||
}
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Run go vet on valid file", func(t *testing.T) {
|
|
||||||
linter := Linter{
|
|
||||||
Name: "go vet",
|
|
||||||
Command: "go",
|
|
||||||
Args: []string{"vet"},
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := RunLinter(testFile, linter)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("RunLinter() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if result == nil {
|
|
||||||
t.Fatal("RunLinter() returned nil result")
|
|
||||||
}
|
|
||||||
if !result.LinterExists {
|
|
||||||
t.Errorf("RunLinter() LinterExists = false, want true")
|
|
||||||
}
|
|
||||||
if result.LinterUsed != "go vet" {
|
|
||||||
t.Errorf("RunLinter() LinterUsed = %q, want 'go vet'", result.LinterUsed)
|
|
||||||
}
|
|
||||||
// Note: go vet might find issues or not, so we don't assert HasIssues
|
|
||||||
t.Logf("go vet result: ExitCode=%d, HasIssues=%v, Output=%q",
|
|
||||||
result.ExitCode, result.HasIssues, result.Output)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Run nonexistent linter", func(t *testing.T) {
|
|
||||||
linter := Linter{
|
|
||||||
Name: "fake",
|
|
||||||
Command: "this-does-not-exist-12345",
|
|
||||||
Args: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := RunLinter(testFile, linter)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("RunLinter() expected error for nonexistent command, got result: %v", result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLintFile(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
t.Run("Lint valid Go file", func(t *testing.T) {
|
|
||||||
testFile := filepath.Join(tmpDir, "valid.go")
|
|
||||||
validCode := `package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello, World!")
|
|
||||||
}
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := LintFile(testFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("LintFile() unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if result == nil {
|
|
||||||
t.Fatal("LintFile() returned nil result")
|
|
||||||
}
|
|
||||||
if result.Language != "Go" {
|
|
||||||
t.Errorf("LintFile() Language = %q, want 'Go'", result.Language)
|
|
||||||
}
|
|
||||||
if !result.LinterExists {
|
|
||||||
t.Errorf("LintFile() LinterExists = false, want true")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Lint nonexistent file", func(t *testing.T) {
|
|
||||||
testFile := filepath.Join(tmpDir, "nonexistent.go")
|
|
||||||
|
|
||||||
result, err := LintFile(testFile)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("LintFile() expected error for nonexistent file, got result: %v", result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Lint unsupported file type", func(t *testing.T) {
|
|
||||||
testFile := filepath.Join(tmpDir, "test.txt")
|
|
||||||
if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := LintFile(testFile)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("LintFile() expected error for unsupported file type, got result: %v", result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSupportedLanguages(t *testing.T) {
|
|
||||||
langs := GetSupportedLanguages()
|
|
||||||
|
|
||||||
if len(langs) == 0 {
|
|
||||||
t.Errorf("GetSupportedLanguages() returned empty list")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that we have some expected languages
|
|
||||||
expectedLangs := []string{"Go", "Python", "JavaScript", "TypeScript"}
|
|
||||||
for _, expected := range expectedLangs {
|
|
||||||
found := false
|
|
||||||
for _, lang := range langs {
|
|
||||||
if contains(lang, expected) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Errorf("GetSupportedLanguages() missing expected language: %s", expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify format includes extensions
|
|
||||||
for _, lang := range langs {
|
|
||||||
if !contains(lang, "(") || !contains(lang, ")") {
|
|
||||||
t.Errorf("GetSupportedLanguages() entry missing format 'Name (extensions)': %s", lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLanguageStructure(t *testing.T) {
|
|
||||||
// Verify the languages slice is properly structured
|
|
||||||
if len(languages) == 0 {
|
|
||||||
t.Fatal("languages slice is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, lang := range languages {
|
|
||||||
if lang.Name == "" {
|
|
||||||
t.Errorf("Language with empty Name found")
|
|
||||||
}
|
|
||||||
if len(lang.Extensions) == 0 {
|
|
||||||
t.Errorf("Language %q has no extensions", lang.Name)
|
|
||||||
}
|
|
||||||
if len(lang.Linters) == 0 {
|
|
||||||
t.Errorf("Language %q has no linters", lang.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check extensions start with dot
|
|
||||||
for _, ext := range lang.Extensions {
|
|
||||||
if ext == "" || ext[0] != '.' {
|
|
||||||
t.Errorf("Language %q has invalid extension: %q", lang.Name, ext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check linters have required fields
|
|
||||||
for _, linter := range lang.Linters {
|
|
||||||
if linter.Name == "" {
|
|
||||||
t.Errorf("Language %q has linter with empty Name", lang.Name)
|
|
||||||
}
|
|
||||||
if linter.Command == "" {
|
|
||||||
t.Errorf("Language %q has linter %q with empty Command", lang.Name, linter.Name)
|
|
||||||
}
|
|
||||||
if linter.InstallInfo == "" {
|
|
||||||
t.Errorf("Language %q has linter %q with empty InstallInfo", lang.Name, linter.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
|
|
||||||
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
|
||||||
containsMiddle(s, substr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsMiddle(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logger *slog.Logger
|
|
||||||
level = new(slog.LevelVar) // Allows dynamic level changes
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init initializes the logger with the specified log level
|
|
||||||
func Init(logLevel string) error {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
home = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
logDir := filepath.Join(home, ".config", "grokkit")
|
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logFile := filepath.Join(logDir, "grokkit.log")
|
|
||||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse log level
|
|
||||||
switch logLevel {
|
|
||||||
case "debug":
|
|
||||||
level.Set(slog.LevelDebug)
|
|
||||||
case "info":
|
|
||||||
level.Set(slog.LevelInfo)
|
|
||||||
case "warn":
|
|
||||||
level.Set(slog.LevelWarn)
|
|
||||||
case "error":
|
|
||||||
level.Set(slog.LevelError)
|
|
||||||
default:
|
|
||||||
level.Set(slog.LevelInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create multi-writer for both file and stderr (if debug)
|
|
||||||
var w io.Writer = file
|
|
||||||
if logLevel == "debug" {
|
|
||||||
w = io.MultiWriter(file, os.Stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use JSON handler for structured logging
|
|
||||||
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{
|
|
||||||
Level: level,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger = slog.New(handler)
|
|
||||||
slog.SetDefault(logger)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLevel changes the log level dynamically
|
|
||||||
func SetLevel(logLevel string) {
|
|
||||||
switch logLevel {
|
|
||||||
case "debug":
|
|
||||||
level.Set(slog.LevelDebug)
|
|
||||||
case "info":
|
|
||||||
level.Set(slog.LevelInfo)
|
|
||||||
case "warn":
|
|
||||||
level.Set(slog.LevelWarn)
|
|
||||||
case "error":
|
|
||||||
level.Set(slog.LevelError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs at debug level with structured fields
|
|
||||||
func Debug(msg string, args ...any) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Debug(msg, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info logs at info level with structured fields
|
|
||||||
func Info(msg string, args ...any) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Info(msg, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn logs at warn level with structured fields
|
|
||||||
func Warn(msg string, args ...any) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn(msg, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs at error level with structured fields
|
|
||||||
func Error(msg string, args ...any) {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Error(msg, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With returns a logger with additional context fields
|
|
||||||
func With(args ...any) *slog.Logger {
|
|
||||||
if logger != nil {
|
|
||||||
return logger.With(args...)
|
|
||||||
}
|
|
||||||
return slog.Default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContext returns a logger with values from context
|
|
||||||
func WithContext(ctx context.Context) *slog.Logger {
|
|
||||||
if logger != nil {
|
|
||||||
return logger.With(slog.Any("context", ctx))
|
|
||||||
}
|
|
||||||
return slog.Default()
|
|
||||||
}
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInit(t *testing.T) {
|
|
||||||
// Create temp directory
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
logLevel string
|
|
||||||
}{
|
|
||||||
{"default level", "info"},
|
|
||||||
{"debug level", "debug"},
|
|
||||||
{"warn level", "warn"},
|
|
||||||
{"error level", "error"},
|
|
||||||
{"invalid level defaults to info", "invalid"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := Init(tt.logLevel)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Init(%q) unexpected error: %v", tt.logLevel, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check log file was created
|
|
||||||
logFile := filepath.Join(tmpDir, ".config", "grokkit", "grokkit.log")
|
|
||||||
if _, err := os.Stat(logFile); os.IsNotExist(err) {
|
|
||||||
t.Errorf("Log file not created at %s", logFile)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogging(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
if err := Init("debug"); err != nil {
|
|
||||||
t.Fatalf("Init() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test all log levels with structured fields
|
|
||||||
Debug("test debug message", "key", "value")
|
|
||||||
Info("test info message", "count", 42)
|
|
||||||
Warn("test warn message", "enabled", true)
|
|
||||||
Error("test error message", "error", "something went wrong")
|
|
||||||
|
|
||||||
// Verify log file has content
|
|
||||||
logFile := filepath.Join(tmpDir, ".config", "grokkit", "grokkit.log")
|
|
||||||
content, err := os.ReadFile(logFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to read log file: %v", err)
|
|
||||||
}
|
|
||||||
if len(content) == 0 {
|
|
||||||
t.Errorf("Log file is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for JSON structure (slog uses JSON handler)
|
|
||||||
contentStr := string(content)
|
|
||||||
if !strings.Contains(contentStr, `"level"`) {
|
|
||||||
t.Errorf("Log content doesn't contain JSON level field")
|
|
||||||
}
|
|
||||||
if !strings.Contains(contentStr, `"msg"`) {
|
|
||||||
t.Errorf("Log content doesn't contain JSON msg field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetLevel(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
if err := Init("info"); err != nil {
|
|
||||||
t.Fatalf("Init() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change level to debug
|
|
||||||
SetLevel("debug")
|
|
||||||
|
|
||||||
// Log at debug level
|
|
||||||
Debug("debug after level change", "test", true)
|
|
||||||
|
|
||||||
// Verify log file has the debug message
|
|
||||||
logFile := filepath.Join(tmpDir, ".config", "grokkit", "grokkit.log")
|
|
||||||
content, err := os.ReadFile(logFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to read log file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(string(content), "debug after level change") {
|
|
||||||
t.Errorf("Debug message not found after level change")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWith(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
oldHome := os.Getenv("HOME")
|
|
||||||
os.Setenv("HOME", tmpDir)
|
|
||||||
defer os.Setenv("HOME", oldHome)
|
|
||||||
|
|
||||||
if err := Init("info"); err != nil {
|
|
||||||
t.Fatalf("Init() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create logger with context
|
|
||||||
contextLogger := With("request_id", "123", "user", "testuser")
|
|
||||||
if contextLogger == nil {
|
|
||||||
t.Errorf("With() returned nil logger")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user