Compare commits

..

No commits in common. "master" and "v0.1.7" have entirely different histories.

53 changed files with 217 additions and 884 deletions

View File

@ -0,0 +1,37 @@
name: 'Auto-complete TODO'
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
move-todo:
runs-on: ubuntu-gitea
steps:
- name: 'Clone PR branch, move TODO, push update'
env:
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
SERVER_URL: ${{ github.server_url }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.pull_request.head.repo.name }}
PR_BRANCH: ${{ github.head_ref }}
run: |
git clone https://${PAT_TOKEN}@${SERVER_URL}/${REPO_OWNER}/${REPO_NAME}.git pr-temp || exit 1
cd pr-temp
git checkout ${PR_BRANCH}
BRANCH=$(git rev-parse --abbrev-ref HEAD)
TODO_NAME="${BRANCH#feature/}.md"
if [[ "${BRANCH}" == feature/* && -f todo/queued/${TODO_NAME} ]]; then
mkdir -p todo/completed
mv todo/queued/${TODO_NAME} todo/completed/
git config user.name 'Gitea Actions Bot'
git config user.email 'actions@noreply.local'
git add todo/
git commit -m "chore: auto-complete ${TODO_NAME} via Gitea Actions"
git push https://${PAT_TOKEN}@${SERVER_URL}/${REPO_OWNER}/${REPO_NAME}.git ${PR_BRANCH}
echo "✅ Moved todo/queued/${TODO_NAME} → completed/"
else
echo " No action: branch='${BRANCH}', expected 'feature/*' with todo/queued/${TODO_NAME}"
fi
cd ..
rm -rf pr-temp

View File

@ -40,10 +40,10 @@ jobs:
run: | run: |
go tool cover -func=coverage.out | tail -1 go tool cover -func=coverage.out | tail -1
# - name: Enforce coverage threshold - name: Enforce coverage threshold
# run: | run: |
# PCT=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%') PCT=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%')
# awk "BEGIN { exit ($PCT < 65) }" || (echo "Coverage ${PCT}% is below 65%" && exit 1) awk "BEGIN { exit ($PCT < 65) }" || (echo "Coverage ${PCT}% is below 65%" && exit 1)
lint: lint:
name: Lint name: Lint

View File

@ -1,14 +1,18 @@
version: "2" version: "2"
linters: linters:
default: standard default: standard
enable: enable:
- misspell - misspell
settings: settings:
errcheck: errcheck:
check-type-assertions: true check-type-assertions: true
check-blank: false check-blank: false
formatters: formatters:
enable: enable:
- gofmt - gofmt
run: run:
timeout: 5m timeout: 5m

View File

@ -1,3 +0,0 @@
{
"model": "grok-code-fast-1"
}

View File

@ -1,53 +0,0 @@
## [v0.1.9] - 2026-03-04
Grokkit gets a quick-query upgrade—because who has time for chit-chat?
### Added
- Implement `query` command in cmd/query.go for non-interactive Grok queries focused on programming.
- Add wordy flag for detailed responses in query command.
- Add .grok/settings.json with fast model configuration.
- Set default model for query in config.go.
- Add entry for query command to commands list in README.
- Create new section in README with query command usage examples and features.
- Add spec for grokkit non-interactive query tool in TODO.
- Add detailed description, examples, and ROI for `query` feature in TODO.
- Introduce initial CHANGELOG.md with v0.1.8 entries.
### Changed
- Update root.go to include queryCmd.
- Reorder and update queued task list in TODO with new entries like non-interactive-query.md.
- Move changelog.md to completed tasks in TODO with version note.
- Standardize link formats and list markers in README.md.
- Rename TODO entry from "grokkit query Go tools integration" to "grokkit query Simple Query Tool".
- Revise TODO description to focus on one-shot prompt/answer tool for concise queries.
# Changelog
All notable changes to this project will be documented in this file.
## [v0.1.8] - 2026-03-03
Another step towards automated bliss, with AI changelogs and Git taking the safety wheel.
### Added
- Add release.sh script for automating releases with version validation, tagging, changelog generation, committing, and pushing.
- Add grokkit changelog command with flags for version, since, stdout, and commit reminder.
- Add git helpers for retrieving latest/previous tags and formatted logs since a reference.
- Add tests for changelog message building and full construction.
- Add todo/README.md as a table of contents for queued and completed tasks.
- Add section in README.md documenting grokkit changelog command with usage examples.
- Add detailed Git workflow for managing changes in README.md.
- Add detail in changelog.md about using generated CHANGELOG.md for Gitea release notes.
### Changed
- Change version flag shorthand from "v" to "V" in changelog command and update test assertion.
- Disable coverage threshold enforcement in CI workflow by commenting out the 65% minimum step.
- Update README.md to include grokkit changelog in the command features list.
- Reprioritize queued tasks in README.md to place changelog first, followed by interactive-agent, make, tea, and gotools.
- Update priorities in individual queued/*.md files to match new order.
- Adjust priorities for features including audit, changelog, gotools, interactive-agent, and rg in todo docs.
- Replace file backups with Git-based change management in safety features.
- Remove mentions of backups from README.md, cmd/lint.go, ARCHITECTURE.md, and TROUBLESHOOTING.md.
- Update troubleshooting guide with Git rollback instructions.
- Modify feature lists and safety descriptions to emphasize Git integration.
- Remove automatic .bak backup creation from edit, docs, lint, testgen, and agent commands.
- Update README.md, architecture docs, troubleshooting, TODOs, and tests to reflect removal of backups.

View File

@ -38,13 +38,11 @@ grokkit version
- [Commands](#commands) - [Commands](#commands)
- [chat](#-grokkit-chat) - [chat](#-grokkit-chat)
- [query](#-grokkit-query)
- [edit](#-grokkit-edit-file-instruction) - [edit](#-grokkit-edit-file-instruction)
- [commit / commitmsg](#-grokkit-commitmsg) - [commit / commitmsg](#-grokkit-commitmsg)
- [review](#-grokkit-review) - [review](#-grokkit-review)
- [pr-describe](#-grokkit-pr-describe) - [pr-describe](#-grokkit-pr-describe)
- [history](#-grokkit-history) - [history](#-grokkit-history)
- [changelog](#-grokkit-changelog)
- [lint](#-grokkit-lint-file) - [lint](#-grokkit-lint-file)
- [docs](#-grokkit-docs-file) - [docs](#-grokkit-docs-file)
- [testgen](#-grokkit-testgen) - [testgen](#-grokkit-testgen)
@ -73,27 +71,8 @@ grokkit chat --debug # Enable debug logging
- History is saved automatically between sessions - History is saved automatically between sessions
- Use `--debug` to see API request timing - Use `--debug` to see API request timing
### 🤖 `grokkit query`
One-shot technical question answering. Perfect for quick programming or engineering questions.
```bash
# Basic usage
grokkit query "How do I sort a slice of structs by a field in Go?"
# Longer, more detailed answer
grokkit query --wordy "Explain how Go's context package works with cancellation"
```
Features:
Default mode is concise, factual, and actionable
--wordy flag gives longer, more explanatory answers
Uses the fast non-reasoning model by default for speed
No persistent history or interactive chat UI
### ✏️ `grokkit edit FILE "instruction"` ### ✏️ `grokkit edit FILE "instruction"`
AI-powered file editing with preview. AI-powered file editing with preview and automatic backups.
```bash ```bash
# Basic usage # Basic usage
@ -110,6 +89,7 @@ grokkit edit utils.go "add detailed docstrings to all exported functions"
``` ```
**Safety features:** **Safety features:**
- Creates `.bak` backup before any changes
- Shows preview with diff-style output - Shows preview with diff-style output
- Requires confirmation before applying - Requires confirmation before applying
- Uses silent streaming (no console spam) - Uses silent streaming (no console spam)
@ -178,24 +158,6 @@ Summarize recent git commits.
grokkit history # Last 10 commits grokkit history # Last 10 commits
``` ```
### 🗒️ `grokkit changelog`
Generate a clean `CHANGELOG.md` section from git history, designed specifically so the output can be pasted directly into Gitea release notes.
```bash
# 1. Create your version tag first
git tag v0.2.0
# 2. Generate preview + write (with safety confirmation)
grokkit changelog
# 3. Output ONLY the new section (perfect for Gitea "Release notes")
grokkit changelog --stdout
# 4. Write file + get commit reminder
grokkit changelog --commit
```
### 📖 `grokkit docs <file> [file...]` ### 📖 `grokkit docs <file> [file...]`
Generate language-appropriate documentation comments using Grok AI. Generate language-appropriate documentation comments using Grok AI.
@ -227,6 +189,7 @@ grokkit docs app.py -m grok-4
| Shell | Shell comments (`# function: desc, # Args: ...`) | | Shell | Shell comments (`# function: desc, # Args: ...`) |
**Safety features:** **Safety features:**
- Creates `.bak` backup before any changes
- Shows first 50 lines of documented code as preview - Shows first 50 lines of documented code as preview
- Requires confirmation (unless `--auto-apply`) - Requires confirmation (unless `--auto-apply`)
@ -239,7 +202,7 @@ grokkit docs app.py -m grok-4
- Python: Pytest with `@parametrize`. - Python: Pytest with `@parametrize`.
- C: Check framework suites. - C: Check framework suites.
- C++: Google Test `EXPECT_*`. - C++: Google Test `EXPECT_*`.
- Boosts coverage; safe preview. - Boosts coverage; safe preview/backup.
**CLI examples**: **CLI examples**:
```bash ```bash
@ -250,6 +213,7 @@ grokkit testgen foo.c bar.cpp
**Safety features**: **Safety features**:
- Lang detection via `internal/linter`. - Lang detection via `internal/linter`.
- Creates `test_*.bak` backups.
- Unified diff preview. - Unified diff preview.
- Y/N (--yes auto). - Y/N (--yes auto).
@ -289,42 +253,11 @@ grokkit lint script.rb -m grok-4
- **Shell** (shellcheck) - **Shell** (shellcheck)
**Safety features:** **Safety features:**
- Creates `.bak` backup before changes
- Shows preview of fixes - Shows preview of fixes
- Verifies fixes by re-running linter - Verifies fixes by re-running linter
- Requires confirmation (unless `--auto-fix`) - Requires confirmation (unless `--auto-fix`)
## Safety & Change Management
Grokkit is designed to work seamlessly with Git. Rather than creating redundant `.bak` files, we lean on Git's powerful version control to manage changes and rollbacks.
### The Safety Workflow
1. **Preview**: Every command that modifies files (like `edit`, `lint`, `docs`) shows a diff-style preview first.
2. **Confirm**: You must explicitly confirm (`y/N`) before any changes are written to disk.
3. **Git Integration**: Use Git to manage the "pre-staged," "staged," and "committed" degrees of change.
### Managing Undesired Changes
If you've applied a change that you don't like, Git makes it easy to roll back:
- **Unstaged changes**: If you haven't `git add`-ed the changes yet:
```bash
git restore <file>
```
- **Staged changes**: If you've already staged the changes:
```bash
git restore --staged <file>
git restore <file>
```
- **Committed changes**: If you've already committed the changes:
```bash
git revert HEAD
# or to reset to a previous state:
git reset --hard HEAD~1
```
By using Git, you have a complete audit trail and multiple levels of undo, ensuring your codebase remains stable even when experimenting with AI-driven refactors.
## Configuration ## Configuration
### Environment Variables ### Environment Variables
@ -519,7 +452,7 @@ grokkit review -v
- ✅ **Persistent chat history** - Never lose your conversations - ✅ **Persistent chat history** - Never lose your conversations
- ✅ **Configurable parameters** - Temperature, timeout, model selection - ✅ **Configurable parameters** - Temperature, timeout, model selection
- ✅ **Shell completions** - Bash, Zsh, Fish, PowerShell - ✅ **Shell completions** - Bash, Zsh, Fish, PowerShell
- ✅ **Safe file editing** - Preview and confirmation, leverage git for rollbacks - ✅ **Safe file editing** - Automatic backups, preview, confirmation
- ✅ **Git workflow integration** - Commit messages, reviews, PR descriptions - ✅ **Git workflow integration** - Commit messages, reviews, PR descriptions
- ✅ **Multi-language linting** - 9 languages supported with AI-powered fixes - ✅ **Multi-language linting** - 9 languages supported with AI-powered fixes
- ✅ **AI documentation generation** - 8 doc styles (godoc, PEP 257, Doxygen, JSDoc, rustdoc, YARD, Javadoc, shell) - ✅ **AI documentation generation** - 8 doc styles (godoc, PEP 257, Doxygen, JSDoc, rustdoc, YARD, Javadoc, shell)

View File

@ -73,13 +73,15 @@ var agentCmd = &cobra.Command{
for i, file := range files { for i, file := range files {
color.Yellow("[%d/%d] → %s", i+1, len(files), file) color.Yellow("[%d/%d] → %s", i+1, len(files), file)
// nolint:gosec // intentional file read from user input
original, err := os.ReadFile(file) original, err := os.ReadFile(file)
if err != nil { if err != nil {
color.Red("Could not read %s", file) color.Red("Could not read %s", file)
continue continue
} }
backupPath := file + ".bak"
_ = os.WriteFile(backupPath, original, 0644)
messages := []map[string]string{ messages := []map[string]string{
{"role": "system", "content": "You are an expert programmer. Remove all unnecessary comments including last modified timestamps and ownership comments. Return clean, complete file content with no explanations, markdown, diffs, or extra text."}, {"role": "system", "content": "You are an expert programmer. Remove all unnecessary comments including last modified timestamps and ownership comments. Return clean, complete file content with no explanations, markdown, diffs, or extra text."},
{"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(file), original, instruction)}, {"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(file), original, instruction)},
@ -105,12 +107,12 @@ var agentCmd = &cobra.Command{
if answer == "a" { if answer == "a" {
applyAll = true applyAll = true
} else if answer != "y" { } else if answer != "y" {
color.Yellow("Skipped %s", file) color.Yellow("Skipped %s (backup kept)", file)
continue continue
} }
} }
_ = os.WriteFile(file, []byte(newContent), 0600) _ = os.WriteFile(file, []byte(newContent), 0644)
color.Green("✅ Applied %s", file) color.Green("✅ Applied %s", file)
} }

View File

@ -1,143 +0,0 @@
package cmd
import (
"fmt"
"os"
"strings"
"time"
"github.com/fatih/color"
"github.com/spf13/cobra"
"gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/git"
)
var changelogCmd = &cobra.Command{
Use: "changelog",
Short: "Generate CHANGELOG.md section from git history for Gitea releases",
Long: `AI-generated changelog using only Added/Changed/Fixed. Designed so the output can be pasted directly into Gitea release notes.`,
Run: runChangelog,
}
func init() {
changelogCmd.Flags().String("since", "", "Start from this tag/ref (default: previous tag)")
changelogCmd.Flags().StringP("version", "V", "", "Override version for header (default: latest git tag)")
changelogCmd.Flags().Bool("stdout", false, "Print ONLY the new section (ideal for Gitea release notes)")
changelogCmd.Flags().Bool("commit", false, "After writing, remind to run grokkit commit")
rootCmd.AddCommand(changelogCmd)
}
func runChangelog(cmd *cobra.Command, _ []string) {
stdout, _ := cmd.Flags().GetBool("stdout")
doCommit, _ := cmd.Flags().GetBool("commit")
version, _ := cmd.Flags().GetString("version")
since, _ := cmd.Flags().GetString("since")
if !git.IsRepo() {
color.Red("Not inside a git repository")
return
}
// Version from tag you set (or override)
if version == "" {
var err error
version, err = git.LatestTag()
if err != nil {
color.Red("No git tags found. Create one first: git tag vX.Y.Z")
return
}
}
// Since ref (defaults to previous tag)
if since == "" {
if prev, err := git.PreviousTag(version); err == nil {
since = prev
} else {
since = "HEAD~100" // safe first-release fallback
}
}
logOutput, err := git.LogSince(since)
if err != nil || strings.TrimSpace(logOutput) == "" {
color.Yellow("No new commits since last tag.")
return
}
// Grok generation (strong prompt for your exact format)
client := newGrokClient()
messages := buildChangelogMessages(logOutput, version)
model := config.GetModel("changelog", "")
color.Yellow("Asking Grok to categorize changes...")
section := client.Stream(messages, model)
date := time.Now().Format("2006-01-02")
newSection := fmt.Sprintf("## [%s] - %s\n\n%s\n", version, date, section)
if stdout {
fmt.Print(newSection)
return
}
// Build full file (prepend or create)
content := buildFullChangelog(newSection)
// Preview + safety confirm (exactly like commit/review)
color.Cyan("\n--- Proposed CHANGELOG.md update ---\n%s\n--------------------------------", content)
var confirm string
color.Yellow("Write this to CHANGELOG.md? (y/n): ")
_, _ = fmt.Scanln(&confirm) // we don't care about scan errors here
if confirm != "y" && confirm != "Y" {
color.Yellow("Aborted.")
return
}
if err := os.WriteFile("CHANGELOG.md", []byte(content), 0600); err != nil {
color.Red("Failed to write CHANGELOG.md")
return
}
color.Green("✅ CHANGELOG.md updated with version %s", version)
if doCommit {
color.Yellow("Run `grokkit commit` (or `git add CHANGELOG.md && git commit`) to stage it.")
}
}
func buildChangelogMessages(log, version string) []map[string]string {
return []map[string]string{
{
"role": "system",
"content": `You are an expert technical writer.
Generate a changelog section using **only** these headings (include only if content exists):
### Added
### Changed
### Fixed
Rules:
- Start directly with the section headings (no extra header we add it).
- One optional short summary sentence at the very top (light humour OK here only).
- Every bullet must be factual, imperative, one clear action per line.
- Be concise. No marketing language or explanations.
Output ONLY clean markdown.`,
},
{
"role": "user",
"content": fmt.Sprintf("Version: %s\n\nCommit history:\n%s", version, log),
},
}
}
func buildFullChangelog(newSection string) string {
existing, err := os.ReadFile("CHANGELOG.md")
if err != nil {
// File doesn't exist (or unreadable) → create a new changelog with a header
return `# Changelog
All notable changes to this project will be documented in this file.
` + newSection
}
return newSection + string(existing)
}

View File

@ -1,110 +0,0 @@
package cmd
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBuildChangelogMessages(t *testing.T) {
t.Parallel()
log := `feat: add changelog command
Implements #1
---
fix: typo in docs
---`
version := "v0.2.0"
messages := buildChangelogMessages(log, version)
require.Len(t, messages, 2)
assert.Equal(t, "system", messages[0]["role"])
assert.Contains(t, messages[0]["content"], "Generate a changelog section")
assert.Contains(t, messages[0]["content"], "### Added")
assert.Contains(t, messages[0]["content"], "### Changed")
assert.Contains(t, messages[0]["content"], "### Fixed")
assert.Equal(t, "user", messages[1]["role"])
assert.Contains(t, messages[1]["content"], "Version: v0.2.0")
assert.Contains(t, messages[1]["content"], log)
}
func TestBuildFullChangelog(t *testing.T) {
t.Parallel()
newSection := "## [v0.2.0] - 2026-03-03\n\n### Added\n- changelog command\n"
t.Run("creates new file with header", func(t *testing.T) {
t.Parallel()
// nolint:tparallel // os.Chdir affects the entire process
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.Chdir(originalWd))
})
require.NoError(t, os.Chdir(tmpDir))
result := buildFullChangelog(newSection)
assert.Contains(t, result, "# Changelog")
assert.Contains(t, result, "All notable changes to this project will be documented in this file.")
assert.Contains(t, result, newSection)
})
t.Run("prepends to existing file", func(t *testing.T) {
t.Parallel()
// nolint:tparallel // os.Chdir affects the entire process
tmpDir := t.TempDir()
originalWd, err := os.Getwd()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, os.Chdir(originalWd))
})
require.NoError(t, os.Chdir(tmpDir))
// Simulate existing changelog
existing := `# Changelog
All notable changes to this project will be documented in this file.
## [v0.1.0] - 2025-01-01
### Fixed
- old bug
`
require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0600))
result := buildFullChangelog(newSection)
assert.True(t, strings.HasPrefix(result, newSection), "new version section must be prepended at the top")
assert.Contains(t, result, "old bug")
})
}
func TestChangelogCmd_Flags(t *testing.T) {
t.Parallel()
require.NotNil(t, changelogCmd)
stdoutFlag := changelogCmd.Flags().Lookup("stdout")
require.NotNil(t, stdoutFlag)
assert.Equal(t, "Print ONLY the new section (ideal for Gitea release notes)", stdoutFlag.Usage)
versionFlag := changelogCmd.Flags().Lookup("version")
require.NotNil(t, versionFlag)
assert.Equal(t, "V", versionFlag.Shorthand)
commitFlag := changelogCmd.Flags().Lookup("commit")
require.NotNil(t, commitFlag)
assert.Equal(t, "After writing, remind to run grokkit commit", commitFlag.Usage)
sinceFlag := changelogCmd.Flags().Lookup("since")
require.NotNil(t, sinceFlag)
}

View File

@ -21,7 +21,6 @@ type ChatHistory struct {
func loadChatHistory() []map[string]string { func loadChatHistory() []map[string]string {
histFile := getChatHistoryFile() histFile := getChatHistoryFile()
// nolint:gosec // intentional file read from config/home
data, err := os.ReadFile(histFile) data, err := os.ReadFile(histFile)
if err != nil { if err != nil {
return nil return nil
@ -41,7 +40,7 @@ func saveChatHistory(messages []map[string]string) error {
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(histFile, data, 0600) return os.WriteFile(histFile, data, 0644)
} }
func getChatHistoryFile() string { func getChatHistoryFile() string {
@ -55,7 +54,7 @@ func getChatHistoryFile() string {
home = "." home = "."
} }
histDir := filepath.Join(home, ".config", "grokkit") histDir := filepath.Join(home, ".config", "grokkit")
_ = os.MkdirAll(histDir, 0750) // Ignore error, WriteFile will catch it _ = os.MkdirAll(histDir, 0755) // Ignore error, WriteFile will catch it
return filepath.Join(histDir, "chat_history.json") return filepath.Join(histDir, "chat_history.json")
} }

View File

@ -91,11 +91,11 @@ func TestLoadChatHistory_InvalidJSON(t *testing.T) {
// Create invalid JSON file // Create invalid JSON file
histDir := filepath.Join(tmpDir, ".config", "grokkit") histDir := filepath.Join(tmpDir, ".config", "grokkit")
if err := os.MkdirAll(histDir, 0750); err != nil { if err := os.MkdirAll(histDir, 0755); err != nil {
t.Fatalf("MkdirAll() error: %v", err) t.Fatalf("MkdirAll() error: %v", err)
} }
histFile := filepath.Join(histDir, "chat_history.json") histFile := filepath.Join(histDir, "chat_history.json")
if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0600); err != nil { if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0644); err != nil {
t.Fatalf("WriteFile() error: %v", err) t.Fatalf("WriteFile() error: %v", err)
} }

View File

@ -15,7 +15,7 @@ var commitCmd = &cobra.Command{
Run: runCommit, Run: runCommit,
} }
func runCommit(cmd *cobra.Command, _ []string) { func runCommit(cmd *cobra.Command, args []string) {
diff, err := gitRun([]string{"diff", "--cached", "--no-color"}) diff, err := gitRun([]string{"diff", "--cached", "--no-color"})
if err != nil { if err != nil {
color.Red("Failed to get staged changes: %v", err) color.Red("Failed to get staged changes: %v", err)
@ -45,7 +45,6 @@ func runCommit(cmd *cobra.Command, _ []string) {
return return
} }
// nolint:gosec // intentional subprocess for git operation
if err := exec.Command("git", "commit", "-m", msg).Run(); err != nil { if err := exec.Command("git", "commit", "-m", msg).Run(); err != nil {
color.Red("Git commit failed") color.Red("Git commit failed")
} else { } else {

View File

@ -49,7 +49,6 @@ PowerShell:
ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// nolint:gosec // intentional subprocess for shell completion
var err error var err error
switch args[0] { switch args[0] {
case "bash": case "bash":

View File

@ -32,6 +32,7 @@ Supported doc styles:
Shell Shell comments (# function: ...) Shell Shell comments (# function: ...)
Safety features: Safety features:
- Creates .bak backup before modifying files
- Shows preview of changes before applying - Shows preview of changes before applying
- Requires confirmation unless --auto-apply is used`, - Requires confirmation unless --auto-apply is used`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
@ -68,7 +69,6 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
return return
} }
// nolint:gosec // intentional file read from user input
originalContent, err := os.ReadFile(filePath) originalContent, err := os.ReadFile(filePath)
if err != nil { if err != nil {
logger.Error("failed to read file", "file", filePath, "error", err) logger.Error("failed to read file", "file", filePath, "error", err)
@ -100,7 +100,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
if len(lines) < previewLines { if len(lines) < previewLines {
previewLines = len(lines) previewLines = len(lines)
} }
for i := range previewLines { for i := 0; i < previewLines; i++ {
fmt.Println(lines[i]) fmt.Println(lines[i])
} }
if len(lines) > previewLines { if len(lines) > previewLines {
@ -124,7 +124,16 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
} }
} }
if err := os.WriteFile(filePath, []byte(documented), 0600); err != nil { // Create backup
backupPath := filePath + ".bak"
if err := os.WriteFile(backupPath, originalContent, 0644); err != nil {
logger.Error("failed to create backup", "file", filePath, "error", err)
color.Red("❌ Failed to create backup: %v", err)
return
}
logger.Info("backup created", "backup", backupPath)
if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil {
logger.Error("failed to write documented file", "file", filePath, "error", err) logger.Error("failed to write documented file", "file", filePath, "error", err)
color.Red("❌ Failed to write file: %v", err) color.Red("❌ Failed to write file: %v", err)
return return
@ -132,6 +141,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
logger.Info("documentation applied successfully", "file", filePath) logger.Info("documentation applied successfully", "file", filePath)
color.Green("✅ Documentation applied: %s", filePath) color.Green("✅ Documentation applied: %s", filePath)
color.Cyan("💾 Original saved to: %s", backupPath)
} }
func buildDocsMessages(language, code string) []map[string]string { func buildDocsMessages(language, code string) []map[string]string {

View File

@ -15,7 +15,7 @@ import (
var editCmd = &cobra.Command{ var editCmd = &cobra.Command{
Use: "edit FILE INSTRUCTION", Use: "edit FILE INSTRUCTION",
Short: "Edit a file in-place with Grok (safe preview)", Short: "Edit a file in-place with Grok (safe preview + backup)",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
filePath := args[0] filePath := args[0]
@ -35,7 +35,6 @@ var editCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
// nolint:gosec // intentional file read from user input
original, err := os.ReadFile(filePath) original, err := os.ReadFile(filePath)
if err != nil { if err != nil {
logger.Error("failed to read file", "file", filePath, "error", err) logger.Error("failed to read file", "file", filePath, "error", err)
@ -45,6 +44,15 @@ var editCmd = &cobra.Command{
logger.Debug("file read successfully", "file", filePath, "size_bytes", len(original)) logger.Debug("file read successfully", "file", filePath, "size_bytes", len(original))
cleanedOriginal := removeLastModifiedComments(string(original)) cleanedOriginal := removeLastModifiedComments(string(original))
backupPath := filePath + ".bak"
logger.Debug("creating backup", "backup_path", backupPath)
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{
{"role": "system", "content": "You are an expert programmer. Remove all unnecessary comments including last modified timestamps and ownership comments. Return only the cleaned code with no explanations, no markdown, no extra text."}, {"role": "system", "content": "You are an expert programmer. Remove all unnecessary comments including last modified timestamps and ownership comments. Return only the cleaned code with no explanations, no markdown, no extra text."},
@ -65,31 +73,32 @@ var editCmd = &cobra.Command{
var confirm string var confirm string
if _, err := fmt.Scanln(&confirm); err != nil { if _, err := fmt.Scanln(&confirm); err != nil {
color.Red("Failed to read input: %v", err) color.Red("Failed to read input: %v", err)
color.Yellow("Changes discarded.") color.Yellow("Changes discarded. Backup saved as %s", backupPath)
return return
} }
if confirm != "y" && confirm != "Y" { if confirm != "y" && confirm != "Y" {
color.Yellow("Changes discarded.") color.Yellow("Changes discarded. Backup saved as %s", backupPath)
return return
} }
logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent)) logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent))
if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil { if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err) logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err) color.Red("Failed to write file: %v", err)
os.Exit(1) os.Exit(1)
} }
logger.Info("changes applied successfully", logger.Info("changes applied successfully",
"file", filePath, "file", filePath,
"backup", backupPath,
"original_size", len(original), "original_size", len(original),
"new_size", len(newContent)) "new_size", len(newContent))
color.Green("✅ Applied successfully!") color.Green("✅ Applied successfully! Backup: %s", backupPath)
}, },
} }
func removeLastModifiedComments(content string) string { func removeLastModifiedComments(content string) string {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
cleanedLines := make([]string, 0, len(lines)) var cleanedLines []string
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "Last modified") { if strings.Contains(line, "Last modified") {

View File

@ -17,7 +17,7 @@ func TestEditCommand(t *testing.T) {
defer func() { _ = os.Remove(tmpfile.Name()) }() defer func() { _ = os.Remove(tmpfile.Name()) }()
original := []byte("package main\n\nfunc hello() {}\n") original := []byte("package main\n\nfunc hello() {}\n")
if err := os.WriteFile(tmpfile.Name(), original, 0600); err != nil { if err := os.WriteFile(tmpfile.Name(), original, 0644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -34,7 +34,7 @@ func TestEditCommand(t *testing.T) {
newContent := grok.CleanCodeResponse(raw) newContent := grok.CleanCodeResponse(raw)
// Apply the result (this is what the real command does after confirmation) // Apply the result (this is what the real command does after confirmation)
if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0600); err != nil { if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0644); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -12,7 +12,7 @@ var historyCmd = &cobra.Command{
Run: runHistory, Run: runHistory,
} }
func runHistory(cmd *cobra.Command, _ []string) { func runHistory(cmd *cobra.Command, args []string) {
log, err := gitRun([]string{"log", "--oneline", "-10"}) log, err := gitRun([]string{"log", "--oneline", "-10"})
if err != nil { if err != nil {
color.Red("Failed to get git log: %v", err) color.Red("Failed to get git log: %v", err)

View File

@ -32,6 +32,7 @@ The command will:
4. (Optional) Use Grok AI to generate and apply fixes 4. (Optional) Use Grok AI to generate and apply fixes
Safety features: Safety features:
- Creates backup before modifying files
- Shows preview of changes before applying - Shows preview of changes before applying
- Requires confirmation unless --auto-fix is used`, - Requires confirmation unless --auto-fix is used`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
@ -103,7 +104,6 @@ func runLint(cmd *cobra.Command, args []string) {
} }
// Read original file content // Read original file content
// nolint:gosec // intentional file read from user input
originalContent, err := os.ReadFile(absPath) originalContent, err := os.ReadFile(absPath)
if err != nil { if err != nil {
logger.Error("failed to read file", "file", absPath, "error", err) logger.Error("failed to read file", "file", absPath, "error", err)
@ -132,6 +132,16 @@ func runLint(cmd *cobra.Command, args []string) {
fixedCode := grok.CleanCodeResponse(response) fixedCode := grok.CleanCodeResponse(response)
logger.Info("received AI fixes", "file", absPath, "fixed_size", len(fixedCode)) 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 // Show preview if not auto-fix
if !autoFix { if !autoFix {
fmt.Println() fmt.Println()
@ -143,7 +153,7 @@ func runLint(cmd *cobra.Command, args []string) {
if len(lines) < previewLines { if len(lines) < previewLines {
previewLines = len(lines) previewLines = len(lines)
} }
for i := range previewLines { for i := 0; i < previewLines; i++ {
fmt.Println(lines[i]) fmt.Println(lines[i])
} }
if len(lines) > previewLines { if len(lines) > previewLines {
@ -157,20 +167,20 @@ func runLint(cmd *cobra.Command, args []string) {
var response string var response string
if _, err := fmt.Scanln(&response); err != nil { if _, err := fmt.Scanln(&response); err != nil {
logger.Info("failed to read user input", "file", absPath, "error", err) logger.Info("failed to read user input", "file", absPath, "error", err)
color.Yellow("❌ Cancelled. No changes made.") color.Yellow("❌ Cancelled. Backup preserved at: %s", backupPath)
return return
} }
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
if response != "y" && response != "yes" { if response != "y" && response != "yes" {
logger.Info("user cancelled fix application", "file", absPath) logger.Info("user cancelled fix application", "file", absPath)
color.Yellow("❌ Cancelled. No changes made.") color.Yellow("❌ Cancelled. Backup preserved at: %s", backupPath)
return return
} }
} }
// Apply fixes // Apply fixes
if err := os.WriteFile(absPath, []byte(fixedCode), 0600); err != nil { if err := os.WriteFile(absPath, []byte(fixedCode), 0644); err != nil {
logger.Error("failed to write fixed file", "file", absPath, "error", err) logger.Error("failed to write fixed file", "file", absPath, "error", err)
color.Red("❌ Failed to write file: %v", err) color.Red("❌ Failed to write file: %v", err)
return return
@ -178,6 +188,7 @@ func runLint(cmd *cobra.Command, args []string) {
logger.Info("fixes applied successfully", "file", absPath) logger.Info("fixes applied successfully", "file", absPath)
color.Green("✅ Fixes applied successfully!") color.Green("✅ Fixes applied successfully!")
color.Cyan("💾 Original saved to: %s", backupPath)
// Optionally run linter again to verify // Optionally run linter again to verify
color.Cyan("\n🔍 Re-running linter to verify fixes...") color.Cyan("\n🔍 Re-running linter to verify fixes...")

View File

@ -14,23 +14,17 @@ var prDescribeCmd = &cobra.Command{
Run: runPRDescribe, Run: runPRDescribe,
} }
func init() { func runPRDescribe(cmd *cobra.Command, args []string) {
prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against") diff, err := gitRun([]string{"diff", "main..HEAD", "--no-color"})
}
func runPRDescribe(cmd *cobra.Command, _ []string) {
base, _ := cmd.Flags().GetString("base")
diff, err := gitRun([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"})
if err != nil || diff == "" { if err != nil || diff == "" {
diff, err = gitRun([]string{"diff", fmt.Sprintf("origin/%s..HEAD", base), "--no-color"}) diff, err = gitRun([]string{"diff", "origin/main..HEAD", "--no-color"})
if err != nil { if err != nil {
color.Red("Failed to get branch diff: %v", err) color.Red("Failed to get branch diff: %v", err)
return return
} }
} }
if diff == "" { if diff == "" {
color.Yellow("No changes on this branch compared to %s/origin/%s.", base, base) color.Yellow("No changes on this branch compared to main/origin/main.")
return return
} }
modelFlag, _ := cmd.Flags().GetString("model") modelFlag, _ := cmd.Flags().GetString("model")

View File

@ -1,53 +0,0 @@
package cmd
import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/grok"
)
var queryCmd = &cobra.Command{
Use: "query [question]",
Short: "One-shot non-interactive query to Grok (programming focused)",
Long: `Ask Grok a single technical question and get a concise, actionable answer.
Default mode is factual and brief. Use --wordy for longer, more explanatory answers.`,
Args: cobra.MinimumNArgs(1),
Run: runQuery,
}
func init() {
queryCmd.Flags().Bool("wordy", false, "Give a longer, more detailed answer")
rootCmd.AddCommand(queryCmd)
}
func runQuery(cmd *cobra.Command, args []string) {
wordy, _ := cmd.Flags().GetBool("wordy")
question := args[0]
// Use fast model by default for quick queries
model := config.GetModel("query", "")
client := grok.NewClient()
systemPrompt := `You are Grok, a helpful and truthful AI built by xAI.
Focus on programming, software engineering, and technical questions.
Be concise, factual, and actionable. Include code snippets when helpful.
Do not add unnecessary fluff.`
if wordy {
systemPrompt = `You are Grok, a helpful and truthful AI built by xAI.
Give thorough, detailed, textbook-style answers to technical questions.
Explain concepts clearly, include examples, and allow light humour where appropriate.
Be comprehensive but still clear and well-structured.`
}
messages := []map[string]string{
{"role": "system", "content": systemPrompt},
{"role": "user", "content": question},
}
color.Yellow("Asking Grok...")
client.Stream(messages, model)
}

View File

@ -14,7 +14,7 @@ var reviewCmd = &cobra.Command{
Run: runReview, Run: runReview,
} }
func runReview(cmd *cobra.Command, _ []string) { func runReview(cmd *cobra.Command, args []string) {
modelFlag, _ := cmd.Flags().GetString("model") modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("review", modelFlag) model := config.GetModel("review", modelFlag)

View File

@ -60,7 +60,6 @@ func init() {
rootCmd.AddCommand(docsCmd) rootCmd.AddCommand(docsCmd)
rootCmd.AddCommand(testgenCmd) rootCmd.AddCommand(testgenCmd)
rootCmd.AddCommand(scaffoldCmd) rootCmd.AddCommand(scaffoldCmd)
rootCmd.AddCommand(queryCmd)
// Add model flag to all commands // Add model flag to all commands
rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)") rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)")

View File

@ -47,11 +47,10 @@ func withMockGit(fn func([]string) (string, error)) func() {
return func() { gitRun = orig } return func() { gitRun = orig }
} }
// testCmd returns a minimal cobra command with common flags registered. // testCmd returns a minimal cobra command with the model flag registered.
func testCmd() *cobra.Command { func testCmd() *cobra.Command {
c := &cobra.Command{} c := &cobra.Command{}
c.Flags().String("model", "", "") c.Flags().String("model", "", "")
c.Flags().String("base", "master", "")
return c return c
} }
@ -309,62 +308,22 @@ func TestRunPRDescribe(t *testing.T) {
} }
}) })
t.Run("uses custom base branch", func(t *testing.T) { t.Run("second diff error — skips AI", func(t *testing.T) {
mock := &mockStreamer{response: "PR description"} mock := &mockStreamer{}
defer withMockClient(mock)() defer withMockClient(mock)()
var capturedArgs []string callCount := 0
defer withMockGit(func(args []string) (string, error) { defer withMockGit(func(args []string) (string, error) {
capturedArgs = args callCount++
return "diff content", nil if callCount == 2 {
})() return "", errors.New("no remote")
cmd := testCmd()
if err := cmd.Flags().Set("base", "develop"); err != nil {
t.Fatal(err)
}
runPRDescribe(cmd, nil)
if mock.calls != 1 {
t.Errorf("expected 1 AI call, got %d", mock.calls)
}
// Expect "diff", "develop..HEAD", "--no-color"
expectedArg := "develop..HEAD"
found := false
for _, arg := range capturedArgs {
if arg == expectedArg {
found = true
break
} }
} return "", nil
if !found {
t.Errorf("expected arg %q not found in %v", expectedArg, capturedArgs)
}
})
t.Run("defaults to master", func(t *testing.T) {
mock := &mockStreamer{response: "PR description"}
defer withMockClient(mock)()
var capturedArgs []string
defer withMockGit(func(args []string) (string, error) {
capturedArgs = args
return "diff content", nil
})() })()
runPRDescribe(testCmd(), nil) runPRDescribe(testCmd(), nil)
if mock.calls != 1 { if mock.calls != 0 {
t.Errorf("expected 1 AI call, got %d", mock.calls) t.Errorf("expected 0 AI calls, got %d", mock.calls)
}
expectedArg := "master..HEAD"
found := false
for _, arg := range capturedArgs {
if arg == expectedArg {
found = true
break
}
}
if !found {
t.Errorf("expected arg %q not found in %v", expectedArg, capturedArgs)
} }
}) })
} }
@ -427,6 +386,7 @@ func TestProcessDocsFilePreviewAndCancel(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer func() { _ = os.Remove(f.Name()) }() defer func() { _ = os.Remove(f.Name()) }()
defer func() { _ = os.Remove(f.Name() + ".bak") }()
mock := &mockStreamer{response: "package main\n\n// Foo does nothing.\nfunc Foo() {}\n"} mock := &mockStreamer{response: "package main\n\n// Foo does nothing.\nfunc Foo() {}\n"}
@ -466,6 +426,7 @@ func TestProcessDocsFileAutoApply(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer func() { _ = os.Remove(f.Name()) }() defer func() { _ = os.Remove(f.Name()) }()
defer func() { _ = os.Remove(f.Name() + ".bak") }()
// CleanCodeResponse will trim the trailing newline from the AI response. // CleanCodeResponse will trim the trailing newline from the AI response.
aiResponse := "package main\n\n// Bar does nothing.\nfunc Bar() {}\n" aiResponse := "package main\n\n// Bar does nothing.\nfunc Bar() {}\n"
@ -486,6 +447,11 @@ func TestProcessDocsFileAutoApply(t *testing.T) {
if string(content) != documented { if string(content) != documented {
t.Errorf("file content = %q, want %q", string(content), documented) t.Errorf("file content = %q, want %q", string(content), documented)
} }
// Verify backup was created with original content.
backup, _ := os.ReadFile(f.Name() + ".bak")
if string(backup) != original {
t.Errorf("backup content = %q, want %q", string(backup), original)
}
} }
func TestRunDocs(t *testing.T) { func TestRunDocs(t *testing.T) {

View File

@ -44,7 +44,7 @@ var scaffoldCmd = &cobra.Command{
dir := filepath.Dir(filePath) dir := filepath.Dir(filePath)
if dir != "." && dir != "" { if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0750); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
logger.Error("failed to create directory", "dir", dir, "error", err) logger.Error("failed to create directory", "dir", dir, "error", err)
color.Red("Failed to create directory: %v", err) color.Red("Failed to create directory: %v", err)
os.Exit(1) os.Exit(1)
@ -98,7 +98,7 @@ Return ONLY the complete code file. No explanations, no markdown, no backticks.`
} }
// Write main file // Write main file
if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil { if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err) logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err) color.Red("Failed to write file: %v", err)
os.Exit(1) os.Exit(1)
@ -115,7 +115,7 @@ Return ONLY the complete code file. No explanations, no markdown, no backticks.`
testRaw := client.StreamSilent(testMessages, model) testRaw := client.StreamSilent(testMessages, model)
testContent := grok.CleanCodeResponse(testRaw) testContent := grok.CleanCodeResponse(testRaw)
if err := os.WriteFile(testPath, []byte(testContent), 0600); err == nil { if err := os.WriteFile(testPath, []byte(testContent), 0644); err == nil {
color.Green("✓ Created test: %s", filepath.Base(testPath)) color.Green("✓ Created test: %s", filepath.Base(testPath))
} }
} }
@ -150,7 +150,7 @@ func detectLanguage(path, override string) string {
} }
// Basic context harvester (~4000 token cap) // Basic context harvester (~4000 token cap)
func harvestContext(filePath, _ string) string { func harvestContext(filePath, lang string) string {
var sb strings.Builder var sb strings.Builder
dir := filepath.Dir(filePath) dir := filepath.Dir(filePath)
@ -161,7 +161,6 @@ func harvestContext(filePath, _ string) string {
continue continue
} }
if filepath.Ext(f.Name()) == filepath.Ext(filePath) && f.Name() != filepath.Base(filePath) { if filepath.Ext(f.Name()) == filepath.Ext(filePath) && f.Name() != filepath.Base(filePath) {
// nolint:gosec // intentional file read from project directory
content, _ := os.ReadFile(filepath.Join(dir, f.Name())) content, _ := os.ReadFile(filepath.Join(dir, f.Name()))
if len(content) > 2000 { if len(content) > 2000 {
content = content[:2000] content = content[:2000]

View File

@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -93,15 +92,29 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
testPath := getTestFilePath(filePath, lang) testPath := getTestFilePath(filePath, lang)
// Handle existing test file // Handle existing test file
var origTest []byte
testExists := true testExists := true
testInfo, err := os.Stat(testPath) testInfo, err := os.Stat(testPath)
switch { if os.IsNotExist(err) {
case errors.Is(err, os.ErrNotExist):
testExists = false testExists = false
case err != nil: } else if err != nil {
return fmt.Errorf("stat test file: %w", err) return fmt.Errorf("stat test file: %w", err)
case testInfo.IsDir(): } else if testInfo.IsDir() {
return fmt.Errorf("test path is dir: %s", testPath) return fmt.Errorf("test path is dir: %s", testPath)
} else {
origTest, err = os.ReadFile(testPath)
if err != nil {
return fmt.Errorf("read existing test: %w", err)
}
}
// Backup existing test
backupPath := testPath + ".bak"
if testExists {
if err := os.WriteFile(backupPath, origTest, 0644); err != nil {
return fmt.Errorf("backup test file: %w", err)
}
color.Yellow("💾 Backup: %s", backupPath)
} }
// Generate tests // Generate tests
@ -154,13 +167,13 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
} }
confirm = strings.TrimSpace(strings.ToLower(confirm)) confirm = strings.TrimSpace(strings.ToLower(confirm))
if confirm != "y" && confirm != "yes" { if confirm != "y" && confirm != "yes" {
color.Yellow("⏭️ Skipped %s", testPath) color.Yellow("⏭️ Skipped %s (backup: %s)", testPath, backupPath)
return nil return nil
} }
} }
// Apply // Apply
if err := os.WriteFile(testPath, []byte(newTestCode), 0600); err != nil { if err := os.WriteFile(testPath, []byte(newTestCode), 0644); err != nil {
return fmt.Errorf("write test file: %w", err) return fmt.Errorf("write test file: %w", err)
} }
@ -171,7 +184,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
func removeSourceComments(content, lang string) string { func removeSourceComments(content, lang string) string {
lines := strings.Split(content, "\n") lines := strings.Split(content, "\n")
cleanedLines := make([]string, 0, len(lines)) var cleanedLines []string
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "Last modified") || strings.Contains(line, "Generated by") || if strings.Contains(line, "Last modified") || strings.Contains(line, "Generated by") ||
strings.Contains(line, "Generated by testgen") { strings.Contains(line, "Generated by testgen") {

View File

@ -93,7 +93,6 @@ int foo() {}`,
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := removeSourceComments(tt.input, tt.lang) got := removeSourceComments(tt.input, tt.lang)
if got != tt.want { if got != tt.want {
t.Errorf("removeSourceComments() =\n%q\nwant\n%q", got, tt.want) t.Errorf("removeSourceComments() =\n%q\nwant\n%q", got, tt.want)
@ -118,7 +117,6 @@ func TestGetTestPrompt(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.lang, func(t *testing.T) { t.Run(tt.lang, func(t *testing.T) {
t.Parallel()
got := getTestPrompt(tt.lang) got := getTestPrompt(tt.lang)
if tt.wantPrefix != "" && !strings.HasPrefix(got, tt.wantPrefix) { if tt.wantPrefix != "" && !strings.HasPrefix(got, tt.wantPrefix) {
t.Errorf("getTestPrompt(%q) prefix =\n%q\nwant %q", tt.lang, got[:100], tt.wantPrefix) t.Errorf("getTestPrompt(%q) prefix =\n%q\nwant %q", tt.lang, got[:100], tt.wantPrefix)
@ -146,7 +144,6 @@ func TestGetTestFilePath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.filePath+"_"+tt.lang, func(t *testing.T) { t.Run(tt.filePath+"_"+tt.lang, func(t *testing.T) {
t.Parallel()
got := getTestFilePath(tt.filePath, tt.lang) got := getTestFilePath(tt.filePath, tt.lang)
if got != tt.want { if got != tt.want {
t.Errorf("getTestFilePath(%q, %q) = %q, want %q", tt.filePath, tt.lang, got, tt.want) t.Errorf("getTestFilePath(%q, %q) = %q, want %q", tt.filePath, tt.lang, got, tt.want)
@ -170,7 +167,6 @@ func TestGetCodeLang(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.lang, func(t *testing.T) { t.Run(tt.lang, func(t *testing.T) {
t.Parallel()
got := getCodeLang(tt.lang) got := getCodeLang(tt.lang)
if got != tt.want { if got != tt.want {
t.Errorf("getCodeLang(%q) = %q, want %q", tt.lang, got, tt.want) t.Errorf("getCodeLang(%q) = %q, want %q", tt.lang, got, tt.want)

View File

@ -7,7 +7,6 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// Load initializes the configuration from Viper
func Load() { func Load() {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
@ -37,13 +36,11 @@ func Load() {
viper.SetDefault("commands.prdescribe.model", "grok-4") viper.SetDefault("commands.prdescribe.model", "grok-4")
viper.SetDefault("commands.review.model", "grok-4") viper.SetDefault("commands.review.model", "grok-4")
viper.SetDefault("commands.docs.model", "grok-4") viper.SetDefault("commands.docs.model", "grok-4")
viper.SetDefault("commands.query.model", "grok-4-1-fast-non-reasoning")
// Config file is optional, so we ignore read errors // Config file is optional, so we ignore read errors
_ = viper.ReadInConfig() _ = viper.ReadInConfig()
} }
// GetModel returns the model to use for a specific command, considering flags and aliases
func GetModel(commandName string, flagModel string) string { func GetModel(commandName string, flagModel string) string {
if flagModel != "" { if flagModel != "" {
if alias := viper.GetString("aliases." + flagModel); alias != "" { if alias := viper.GetString("aliases." + flagModel); alias != "" {
@ -58,12 +55,10 @@ func GetModel(commandName string, flagModel string) string {
return viper.GetString("default_model") return viper.GetString("default_model")
} }
// GetTemperature returns the temperature from the configuration
func GetTemperature() float64 { func GetTemperature() float64 {
return viper.GetFloat64("temperature") return viper.GetFloat64("temperature")
} }
// GetTimeout returns the timeout from the configuration
func GetTimeout() int { func GetTimeout() int {
timeout := viper.GetInt("timeout") timeout := viper.GetInt("timeout")
if timeout <= 0 { if timeout <= 0 {
@ -72,7 +67,6 @@ func GetTimeout() int {
return timeout return timeout
} }
// GetLogLevel returns the log level from the configuration
func GetLogLevel() string { func GetLogLevel() string {
return viper.GetString("log_level") return viper.GetString("log_level")
} }

View File

@ -23,7 +23,7 @@ Grokkit follows these core principles:
- Single responsibility per package - Single responsibility per package
### 2. **Safety by Default** ### 2. **Safety by Default**
- Git-based version control for change management - File backups before any modification
- Confirmation prompts for destructive actions - Confirmation prompts for destructive actions
- Comprehensive error handling - Comprehensive error handling
@ -334,6 +334,7 @@ User: grokkit edit file.go "instruction"
editCmd.Run() editCmd.Run()
├─→ Validate file exists ├─→ Validate file exists
├─→ Read original content ├─→ Read original content
├─→ Create backup (.bak)
├─→ Clean "Last modified" comments ├─→ Clean "Last modified" comments
grok.Client.StreamSilent() # Get AI response grok.Client.StreamSilent() # Get AI response
@ -346,7 +347,8 @@ Display preview # Show diff-style output
Prompt user (y/n) Prompt user (y/n)
Write file (if confirmed) Write file (if confirmed)
└─→ logger.Info() # Log changes ├─→ logger.Info() # Log changes
└─→ Keep backup
``` ```
### Commit Command Flow ### Commit Command Flow
@ -392,6 +394,8 @@ grok.Client.StreamSilent() # Get fixed code
grok.CleanCodeResponse() # Remove markdown grok.CleanCodeResponse() # Remove markdown
Create backup (.bak)
(if not --auto-fix) → Show preview + prompt (if not --auto-fix) → Show preview + prompt
Write fixed file Write fixed file
@ -567,7 +571,7 @@ func Run(args []string) (string, error) {
**Current measures:** **Current measures:**
- API key via environment variable - API key via environment variable
- No credential storage - No credential storage
- Git-based rollbacks for safety - Backup files for safety
**Future considerations:** **Future considerations:**
- Encrypted config storage - Encrypted config storage

View File

@ -322,27 +322,27 @@ pwd
ls ls
``` ```
### Rolling back AI changes ### Permission denied on backup creation
**Symptom:** **Symptom:**
AI suggested changes are undesired or introduced bugs. ```
Failed to create backup: permission denied
**Solution:**
Use Git to roll back:
```bash
# If not yet added/staged:
git restore <file>
# If staged:
git restore --staged <file>
git restore <file>
# If committed:
git revert HEAD
``` ```
Always review changes with `git diff` before and after applying AI suggestions. **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 ### Failed to write file
@ -581,8 +581,8 @@ After applying AI fixes, re-running the linter still reports issues.
**Solution:** **Solution:**
```bash ```bash
# Some issues may require manual intervention # Some issues may require manual intervention
# Use git diff to compare changes # Check the backup file to compare
git diff file.py diff file.py file.py.bak
# Apply fixes iteratively # Apply fixes iteratively
grokkit lint file.py # Apply fixes grokkit lint file.py # Apply fixes

View File

@ -9,7 +9,6 @@ type GitError struct {
} }
func (e *GitError) Error() string { func (e *GitError) Error() string {
_ = e.Err // keep field used for error message
return fmt.Sprintf("git %s failed: %v", e.Command, e.Err) return fmt.Sprintf("git %s failed: %v", e.Command, e.Err)
} }

View File

@ -17,8 +17,8 @@ func TestGitError(t *testing.T) {
t.Errorf("GitError.Error() = %q, want %q", gitErr.Error(), expected) t.Errorf("GitError.Error() = %q, want %q", gitErr.Error(), expected)
} }
if !errors.Is(gitErr, baseErr) { if gitErr.Unwrap() != baseErr {
t.Errorf("GitError did not wrap base error") t.Errorf("GitError.Unwrap() did not return base error")
} }
} }
@ -68,8 +68,8 @@ func TestFileError(t *testing.T) {
t.Errorf("FileError.Error() = %q, want %q", fileErr.Error(), expected) t.Errorf("FileError.Error() = %q, want %q", fileErr.Error(), expected)
} }
if !errors.Is(fileErr, baseErr) { if fileErr.Unwrap() != baseErr {
t.Errorf("FileError did not wrap base error") t.Errorf("FileError.Unwrap() did not return base error")
} }
} }
@ -80,7 +80,7 @@ func TestAPIErrorUnwrap(t *testing.T) {
Message: "internal error", Message: "internal error",
Err: baseErr, Err: baseErr,
} }
if !errors.Is(apiErr, baseErr) { if unwrap := apiErr.Unwrap(); unwrap != baseErr {
t.Errorf("APIError.Unwrap() = %v, want %v", apiErr.Unwrap(), baseErr) t.Errorf("APIError.Unwrap() = %v, want %v", unwrap, baseErr)
} }
} }

View File

@ -12,7 +12,6 @@ func Run(args []string) (string, error) {
cmdStr := "git " + strings.Join(args, " ") cmdStr := "git " + strings.Join(args, " ")
logger.Debug("executing git command", "command", cmdStr, "args", args) logger.Debug("executing git command", "command", cmdStr, "args", args)
// nolint:gosec // intentional subprocess for git operation
out, err := exec.Command("git", args...).Output() out, err := exec.Command("git", args...).Output()
if err != nil { if err != nil {
logger.Error("git command failed", logger.Error("git command failed",
@ -31,32 +30,8 @@ func Run(args []string) (string, error) {
func IsRepo() bool { func IsRepo() bool {
logger.Debug("checking if directory is a git repository") logger.Debug("checking if directory is a git repository")
// nolint:gosec // intentional subprocess for git repository check
_, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output() _, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output()
isRepo := err == nil isRepo := err == nil
logger.Debug("git repository check completed", "is_repo", isRepo) logger.Debug("git repository check completed", "is_repo", isRepo)
return isRepo return isRepo
} }
func LatestTag() (string, error) {
out, err := Run([]string{"describe", "--tags", "--abbrev=0"})
if err != nil {
return "", err
}
return strings.TrimSpace(out), nil
}
// PreviousTag returns the tag immediately before the given one.
func PreviousTag(current string) (string, error) {
out, err := Run([]string{"describe", "--tags", "--abbrev=0", current + "^"})
if err != nil {
return "", err
}
return strings.TrimSpace(out), nil
}
// LogSince returns formatted commit log since the given ref (exactly matches the todo spec).
func LogSince(since string) (string, error) {
args := []string{"log", "--pretty=format:%s%n%b%n---", since + "..HEAD"}
return Run(args)
}

View File

@ -76,23 +76,20 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
"stream": true, "stream": true,
} }
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
// Manual cancel before os.Exit; otherwise defer is fine for the main path.
defer cancel()
body, err := json.Marshal(payload) body, err := json.Marshal(payload)
if err != nil { if err != nil {
logger.Error("failed to marshal API request", "error", err) logger.Error("failed to marshal API request", "error", err)
color.Red("Failed to marshal request: %v", err) color.Red("Failed to marshal request: %v", err)
cancel()
os.Exit(1) os.Exit(1)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil { if err != nil {
logger.Error("failed to create HTTP request", "error", err, "url", url) logger.Error("failed to create HTTP request", "error", err, "url", url)
color.Red("Failed to create request: %v", err) color.Red("Failed to create request: %v", err)
cancel()
os.Exit(1) os.Exit(1)
} }
req.Header.Set("Authorization", "Bearer "+c.APIKey) req.Header.Set("Authorization", "Bearer "+c.APIKey)
@ -107,7 +104,6 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
"model", model, "model", model,
"duration_ms", time.Since(startTime).Milliseconds()) "duration_ms", time.Since(startTime).Milliseconds())
color.Red("Request failed: %v", err) color.Red("Request failed: %v", err)
cancel()
os.Exit(1) os.Exit(1)
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()

View File

@ -1,7 +1,6 @@
package linter package linter
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -215,7 +214,7 @@ func FindAvailableLinter(lang *Language) (*Linter, error) {
} }
// Build install instructions // Build install instructions
installOptions := make([]string, 0, len(lang.Linters)) var installOptions []string
for _, linter := range lang.Linters { for _, linter := range lang.Linters {
installOptions = append(installOptions, installOptions = append(installOptions,
fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo)) fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo))
@ -233,12 +232,10 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) {
"command", linter.Command) "command", linter.Command)
// Build command arguments // Build command arguments
linterArgs := append([]string{}, linter.Args...) args := append(linter.Args, filePath)
linterArgs = append(linterArgs, filePath)
// Execute linter // Execute linter
// nolint:gosec // intentional subprocess for linter cmd := exec.Command(linter.Command, args...)
cmd := exec.Command(linter.Command, linterArgs...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
result := &LintResult{ result := &LintResult{
@ -248,8 +245,7 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) {
} }
if err != nil { if err != nil {
var exitErr *exec.ExitError if exitErr, ok := err.(*exec.ExitError); ok {
if errors.As(err, &exitErr) {
result.ExitCode = exitErr.ExitCode() result.ExitCode = exitErr.ExitCode()
result.HasIssues = true result.HasIssues = true
logger.Info("linter found issues", logger.Info("linter found issues",
@ -306,7 +302,7 @@ func LintFile(filePath string) (*LintResult, error) {
// GetSupportedLanguages returns a list of all supported languages // GetSupportedLanguages returns a list of all supported languages
func GetSupportedLanguages() []string { func GetSupportedLanguages() []string {
langs := make([]string, 0, len(languages)) var langs []string
for _, lang := range languages { for _, lang := range languages {
langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", "))) langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", ")))
} }

View File

@ -151,7 +151,7 @@ func main() {
fmt.Println("Hello, World!") fmt.Println("Hello, World!")
} }
` `
if err := os.WriteFile(testFile, []byte(validCode), 0600); err != nil { if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err) t.Fatalf("Failed to create test file: %v", err)
} }
@ -207,7 +207,7 @@ func main() {
fmt.Println("Hello, World!") fmt.Println("Hello, World!")
} }
` `
if err := os.WriteFile(testFile, []byte(validCode), 0600); err != nil { if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err) t.Fatalf("Failed to create test file: %v", err)
} }
@ -237,7 +237,7 @@ func main() {
t.Run("Lint unsupported file type", func(t *testing.T) { t.Run("Lint unsupported file type", func(t *testing.T) {
testFile := filepath.Join(tmpDir, "test.txt") testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("hello"), 0600); err != nil { if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err) t.Fatalf("Failed to create test file: %v", err)
} }

View File

@ -21,12 +21,12 @@ func Init(logLevel string) error {
} }
logDir := filepath.Join(home, ".config", "grokkit") logDir := filepath.Join(home, ".config", "grokkit")
if err := os.MkdirAll(logDir, 0750); err != nil { if err := os.MkdirAll(logDir, 0755); err != nil {
return err return err
} }
logFile := filepath.Join(logDir, "grokkit.log") logFile := filepath.Join(logDir, "grokkit.log")
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil { if err != nil {
return err return err
} }

View File

@ -33,7 +33,6 @@ func TestVersionInfo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tt.check(t) tt.check(t)
}) })
} }

View File

@ -1,65 +0,0 @@
#!/bin/bash
# release.sh — One-command release driver for Grokkit
# Usage: ./release.sh v0.2.3
set -euo pipefail
VERSION="${1:-}"
if [[ -z "$VERSION" || ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "❌ Usage: $0 vX.Y.Z"
echo " Example: $0 v0.2.3"
exit 1
fi
echo "🚀 Starting release process for $VERSION..."
# Safety check: clean working tree
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Working tree is dirty. Commit or stash changes first."
exit 1
fi
# Final human confirmation before anything touches git
echo ""
echo "This will:"
echo " 1. Create git tag $VERSION"
echo " 2. Run grokkit changelog (with preview + your confirmation)"
echo " 3. Stage CHANGELOG.md and run grokkit commit (AI message + confirmation)"
echo " 4. Push commit + tag"
echo ""
read -p "Proceed with release $VERSION? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# 1. Tag first (so changelog can see it as LatestTag)
echo "🏷️ Creating tag $VERSION..."
git tag "$VERSION"
# 2. Generate changelog (interactive preview + confirm inside the command)
echo "📝 Generating CHANGELOG.md section..."
grokkit changelog --version "$VERSION"
# 3. Commit with Grok (uses the exact same safety flow you already love)
echo "📦 Staging and committing changelog..."
git add CHANGELOG.md
grokkit commit
# 4. Push everything
echo "📤 Pushing commit + tag..."
git push
git push --tags
# 5. Show the exact notes ready for Gitea release page
echo ""
echo "✅ Release $VERSION complete!"
echo ""
echo "📋 Copy-paste this into the Gitea release notes:"
echo "------------------------------------------------"
grokkit changelog --stdout --version "$VERSION"
echo "------------------------------------------------"
echo ""
echo "🎉 Done! Go create the release on Gitea now."

View File

@ -1,27 +0,0 @@
# Grokkit TODO List
This document provides a table of contents for all tasks and features currently tracked in the `todo/` directory.
## Queued
* [1] [interactive-agent.md](./queued/interactive-agent.md) : Grokkit Interactive Agent
* [2] [rg.md](./queued/rg.md) : grokkit agent ripgrep (rg) integration
* [3] [gotools.md](./queued/gotools.md) : grokkit agent Go tools integration
* [4] [make.md](./queued/make.md) : grokkit agent make integration
* [5] [tea.md](./queued/tea.md) : grokkit agent tea (Gitea CLI) integration
* [6] [cnotes.md](./queued/cnotes.md) : grokkit agent cnotes integration
* [7] [profile.md](./queued/profile.md) : grokkit profile
* [8] [pprof.md](./queued/pprof.md) : grokkit agent pprof integration
* [9] [audit.md](./queued/audit.md) : grokkit audit
* [10] [git-chglog.md](./queued/git-chglog.md) : grokkit agent git-chglog integration
* [11] [admin.md](./queued/admin.md) : grokkit admin tool (to show token usage and other admin-only features)
* [99] [TODO_ITEM.md](./queued/TODO_ITEM.md) : TODO ITEM template
## Completed
* [non-interactive-query](./completed/non-interactive-query.md) : grokkit query *(done - v0.1.9)*
* [changelog.md](./completed/changelog.md) : grokkit changelog *(done — v0.1.8+)*
* [3-new-feature-suggestions.md](./completed/3-new-feature-suggestions.md) : 3 New AI-Enhanced Feature Suggestions for Grokkit
* [MODEL_ENFORCEMENT.md](./completed/MODEL_ENFORCEMENT.md) : Model Enforcement
* [scaffold.md](./completed/scaffold.md) : grokkit scaffold
* [testgen.md](./completed/testgen.md) : grokkit testgen [path...]

View File

@ -1,138 +0,0 @@
# `grokkit query` Simple Query Tool
## Priority: x of 13
**Description**: A "one shot" prompt/answer query tool. Use it to ask StackOverflow style questions, like "how do I do x? Or What library do I use for y?". The sweet spot is concise, actionable answers with just enough context, a code snippet if relevant, and no unnecessary fluff. Think: problem solved in 100-300 words, tops. However, a "wordy" flag will be included to allow for full "textbook" style answers, as well.
## Problem It Solves
Reduces the amount of context switching needed to answer basic programming questions.
### Example Short Query 1:
```
grokkit query "What is the difference between a slice and a map in Go?"
Slices are dynamic arrays for ordered sequences of elements (e.g., `[]int`), with length/capacity and fast index access. Use for lists or queues; append with `append(s, val)`.
Maps are unordered key-value stores (e.g., `map[string]int`), for fast lookups/deletes via hashing. Use for dictionaries or caches; access with `m[key]`.
Key diffs: Slices are indexed by integers and ordered; maps use arbitrary keys and are unordered. Both are reference types, initialized with `make`.
Example:
```go
s := []int{1, 2, 3} // Slice
m := map[string]int{"a": 1} // Map
```
```
### Example Short Query 2:
```
"$ grokkit query "Are there any other languages with a concept like Python list comprehensions?"
Yes, several languages have similar concise syntax for creating collections via expressions:
- **Haskell**: List comprehensions, e.g., `[x*2 | x <- [1..5]]` (outputs `[2,4,6,8,10]`).
- **Scala**: For-comprehensions, e.g., `for (x <- 1 to 5) yield x*2`.
- **JavaScript (ES6+)**: Array methods like `map`/`filter`, e.g., `[1,2,3,4,5].map(x => x*2)`. Not identical, but functional equivalent.
- **C#**: LINQ queries, e.g., `from x in Enumerable.Range(1,5) select x*2`.
- **Rust**: Iterators with `map`/`collect`, e.g., `(1..=5).map(|x| x*2).collect::<Vec<i32>>()`.
Python's are inspired by Haskell. For exact matches, Haskell is closest.
```
### Example Long Query:
```
$ grokkit query "What is the difference between a slice and a map in Go?" --wordy
Ah, slices and maps in Go—two of the language's built-in data structures that make life easier (or at least less array-tastic) for developers. They're both reference types, meaning they're lightweight and point to underlying data, but they serve very different purposes. I'll break it down step by step, with some examples, and keep it straightforward. If Go had a sense of humor, it'd say slices are for cutting things up neatly, while maps are for when you lose your keys... again.
### What is a Slice?
A slice is essentially a dynamic, resizable view of an array. Think of it as a flexible array that can grow or shrink without you having to manage the underlying memory manually. Slices are backed by an array under the hood, but they're more convenient because:
- They have a length (how many elements are currently in it) and a capacity (how much room is allocated before it needs to resize).
- You can append to them, slice them further (hence the name), and they're zero-initialized by default.
- They're ordered: Elements are accessed by integer indices, starting from 0.
**Key traits:**
- Type: `[]T` (e.g., `[]int` for a slice of integers).
- Ordered? Yes.
- Use case: Lists, stacks, queues, or any sequence of items where order matters and you need fast random access.
**Example:**
```go
package main
import "fmt"
func main() {
// Create a slice with make (initial length 3, capacity 5)
s := make([]string, 3, 5)
s[0] = "apple"
s[1] = "banana"
s[2] = "cherry"
// Append to it (grows if needed)
s = append(s, "date")
fmt.Println(s) // Output: [apple banana cherry date]
fmt.Println(s[1:3]) // Slicing: [banana cherry]
}
```
Slices are great for when you want array-like behavior without the fixed-size hassle of actual arrays in Go.
### What is a Map?
A map is Go's version of a hash table or dictionary: an unordered collection of key-value pairs. You use it to associate unique keys with values, and it's super efficient for lookups, inserts, and deletes (average O(1) time complexity, thanks to hashing).
- Keys must be comparable (e.g., strings, ints, structs without pointers), but values can be anything.
- Maps are unordered, so don't rely on iteration order—it's not guaranteed (though as of Go 1.12, it's pseudo-random but consistent per run for debugging).
- They're also nil-safe: An uninitialized map is nil, and you can't add to it until you `make` it.
**Key traits:**
- Type: `map[K]V` (e.g., `map[string]int` for string keys and int values).
- Ordered? No (unordered by design).
- Use case: Lookup tables, caches, configurations, or anywhere you need fast key-based access without caring about order.
**Example:**
```go
package main
import "fmt"
func main() {
// Create a map with make
m := make(map[string]int)
m["apple"] = 1
m["banana"] = 2
m["cherry"] = 3
// Access by key
fmt.Println(m["banana"]) // Output: 2
// Check if a key exists
value, exists := m["date"]
if !exists {
fmt.Println("Date not found!") // This will print
}
// Delete a key
delete(m, "cherry")
}
```
Maps are your go-to when you need to map one thing to another without the linear search nonsense.
### Key Differences
- **Structure and Purpose**: Slices are sequences of elements (like a list or array), ideal for ordered collections. Maps are associative arrays for key-value storage, perfect for quick lookups by key.
- **Access Method**: Slices use integer indices (e.g., `s[42]`). Maps use keys of any hashable type (e.g., `m["foo"]`).
- **Ordering**: Slices preserve insertion order and allow indexing. Maps are unordered—iteration might give you keys in a seemingly random order.
- **Resizing and Capacity**: Slices have explicit length and capacity; they can grow via `append`. Maps grow automatically as you add elements, but you don't control capacity directly.
- **Initialization**: Both often use `make` for creation, but slices can be literals like `[]int{1, 2, 3}`, while maps use `map[string]int{"a": 1}`.
- **Performance**: Slices are contiguous in memory (fast iteration and access), maps use hashing (fast lookups but more overhead for small sizes).
- **Nil Behavior**: A nil slice can still be appended to (it acts like an empty slice). A nil map will panic if you try to read/write to it.
- **When to Use Which**: Use a slice if you need a list (e.g., processing a queue of tasks). Use a map if you need to associate data (e.g., user IDs to profiles).
In short, if slices are like a neatly organized toolbox drawer, maps are like a junk drawer where you can find stuff quickly by yelling its name—but don't expect it to be in any particular spot.
```
## ROI
**Medium**

View File

@ -1,4 +1,5 @@
# TODO ITEM 1 # TODO ITEM 1
## Priority: 12 of 12
- [ ] 1 step one - [ ] 1 step one
- [ ] 2 step two - [ ] 2 step two
- [ ] 3 step three - [ ] 3 step three

View File

@ -1,19 +0,0 @@
# `grokkit admin` administration tools
### Category: Nice to Have
Add an `admin` command that, if an `XAI_MGMT_KEY` is set in the environment, displays up-to-date token usage on the api account/team.
### Details
- **Command**: `grokkit admin`
- **Subcommands**:
- `grokkit admin token-usage` <-- default (shows token usage for current team)
- `grokkit admin credit-balance` <-- shows credit balance for current team
- `grokkit admin models` <-- shows available models
- `grokkit admin api-keys` <-- shows available API keys
- **Condition**: Must check for `XAI_MGMT_KEY` env var.
- **Functionality**: Fetch and display token usage stats from the [XAI MANAGEMENT API](https://docs.x.ai/developers/rest-api-reference/management.
- **Goal**: Help me monitor my API costs and limits directly from the CLI.
NOTE: If possible, it would be cool if this command was "hidden" if the XAI_MGMT_KEY is not set.

View File

@ -1,4 +1,5 @@
# `grokkit audit` # `grokkit audit`
## Priority: 3 of 12
**Description**: Comprehensive AI-powered code audit for security, performance, best practices, and potential bugs across single files or entire projects. **Description**: Comprehensive AI-powered code audit for security, performance, best practices, and potential bugs across single files or entire projects.
**Benefits**: **Benefits**:

View File

@ -1,6 +1,6 @@
# `grokkit changelog` # `grokkit changelog`
## Priority: 1 of 12 ## Priority: 9 of 12
**Description**: AI-generated CHANGELOG.md updates from git history (commits/tags). The generated CHANGELOG.md should be used as the basis for the release notes for the release page of each release on gitea. **Description**: AI-generated CHANGELOG.md updates from git history (commits/tags).
**Benefits**: **Benefits**:
- Automates semantic release notes (feat/fix/docs/etc.). - Automates semantic release notes (feat/fix/docs/etc.).

View File

@ -1,4 +1,5 @@
# `grokkit agent` cnotes integration # `grokkit agent` cnotes integration
## Priority: 6 of 12
**Description**: Wrappers for your `cnotes` CLI logging suite. Allows Grok to automatically log coding sessions, notes, progress during agent workflows (e.g., "start work on feature", "log bug found"). **Description**: Wrappers for your `cnotes` CLI logging suite. Allows Grok to automatically log coding sessions, notes, progress during agent workflows (e.g., "start work on feature", "log bug found").

View File

@ -1,4 +1,5 @@
# `grokkit agent` git-chglog integration # `grokkit agent` git-chglog integration
## Priority: 11 of 12
**Description**: Wrapper for git-chglog CLI: generates CHANGELOG.md from conventional commits/tags. AI-enhanced parsing/validation. **Description**: Wrapper for git-chglog CLI: generates CHANGELOG.md from conventional commits/tags. AI-enhanced parsing/validation.

View File

@ -1,4 +1,5 @@
# `grokkit agent` Go tools integration # `grokkit agent` Go tools integration
## Priority: 7 of 12
**Description**: Wrappers for `go` subcommands: mod tidy, generate, vet, fmt. Ensures hygiene post-agent edits. **Description**: Wrappers for `go` subcommands: mod tidy, generate, vet, fmt. Ensures hygiene post-agent edits.

View File

@ -1,5 +1,6 @@
# Grokkit Interactive Agent # Grokkit Interactive Agent
**Priority:** 2 of 12 (right after testgen)
## Goal ## Goal
@ -30,7 +31,7 @@ grokkit chat --agent # or new subcommand: grokkit agent-chat
- Safe-by-default workflow: - Safe-by-default workflow:
- Always preview changes (diff style, same as current `edit`/`scaffold`) - Always preview changes (diff style, same as current `edit`/`scaffold`)
- Require explicit confirmation (`y/n` or `--yes`) - Require explicit confirmation (`y/n` or `--yes`)
- Preview and confirm all changes before application - Create `.bak` backups automatically
- Ability to chain actions naturally in conversation: - Ability to chain actions naturally in conversation:
- “The scaffold tests are flaky on Windows paths — fix it and add a test case.” - “The scaffold tests are flaky on Windows paths — fix it and add a test case.”
- “Refactor the context harvester to use rg, then run make test.” - “Refactor the context harvester to use rg, then run make test.”

View File

@ -1,4 +1,5 @@
# `grokkit agent` make integration # `grokkit agent` make integration
## Priority: 2 of 12
**Description**: Wrappers for Makefile targets (test/lint/build/cover). Enables Grok to run/verify builds mid-agent workflow (e.g., "edit, test, fix loops"). **Description**: Wrappers for Makefile targets (test/lint/build/cover). Enables Grok to run/verify builds mid-agent workflow (e.g., "edit, test, fix loops").

View File

@ -1,4 +1,5 @@
# `grokkit agent` pprof integration # `grokkit agent` pprof integration
## Priority: 10 of 12
**Description**: Go pprof profiling wrappers (CPU/memory/allocs). Captures profiles during agent runs, AI-analyzes hotspots. **Description**: Go pprof profiling wrappers (CPU/memory/allocs). Captures profiles during agent runs, AI-analyzes hotspots.

View File

@ -1,4 +1,5 @@
# `grokkit profile` # `grokkit profile`
## Priority: 8 of 12
**Description**: Run Go benchmarks/pprof, get AI-suggested performance optimizations. **Description**: Run Go benchmarks/pprof, get AI-suggested performance optimizations.
**Benefits**: **Benefits**:

View File

@ -1,4 +1,5 @@
# `grokkit agent` ripgrep (rg) integration # `grokkit agent` ripgrep (rg) integration
## Priority: 4 of 12
**Description**: Fast search wrapper for ripgrep (`rg`). Enables agent to grep project for symbols/patterns/context before edits. **Description**: Fast search wrapper for ripgrep (`rg`). Enables agent to grep project for symbols/patterns/context before edits.

View File

@ -1,4 +1,5 @@
# `grokkit agent` tea integration # `grokkit agent` tea integration
## Priority: 5 of 12
**Description**: Safe, AI-orchestrated wrappers for Gitea `tea` CLI commands. Enables Grok to manage repos (list/create PRs/issues, comments) as part of agent workflows, with previews and confirmations. **Description**: Safe, AI-orchestrated wrappers for Gitea `tea` CLI commands. Enables Grok to manage repos (list/create PRs/issues, comments) as part of agent workflows, with previews and confirmations.