Compare commits

..

15 Commits

Author SHA1 Message Date
Gregory Gauthier
4ff65e8137 docs(chat): add documentation for interactive agent mode
Some checks failed
Auto-complete TODO / move-todo (pull_request) Failing after 2s
CI / Test (pull_request) Successful in 34s
CI / Lint (pull_request) Successful in 27s
CI / Build (pull_request) Successful in 21s
- Introduce `--agent` flag for `grokkit chat` as the new primary way to work with Grok on code
- Describe features: tool calling, safety previews, confirmation, persistent sessions
- Note deprecation of old `grokkit agent` command in v0.3.0
- Mention use of fast non-reasoning model for reliability
2026-03-04 14:31:05 +00:00
Gregory Gauthier
365f0c01ec refactor(cmd): clean up tool handling and Scanln usage
- Remove unnecessary comments and reset lines in chat.go for edit and scaffold tools.
- Ignore return values from fmt.Scanln in edit.go and scaffold.go to handle potential errors.
- Delete unused edit_helper_test.go file.
2026-03-04 14:18:23 +00:00
Gregory Gauthier
426288b356 refactor(cmd): replace global vars with flags for agent mode params
Use command-line flags for edit instructions and scaffold descriptions
instead of global variables to improve modularity and avoid globals.
2026-03-04 14:09:04 +00:00
Gregory Gauthier
2809b55912 refactor(cmd): integrate agent mode with edit and scaffold commands
Refactor edit and scaffold commands to support invocation from chat --agent mode by using temporary global variables for passing instructions/descriptions. Add interactive prompts for normal usage, preview diffs, and confirmation before writing files. Update chat handler to set these variables and reset after execution.
2026-03-04 14:04:29 +00:00
Gregory Gauthier
bec85a69e0 refactor(chat): remove temporary overrides for edit and scaffold tools
Simplify tool call handling by eliminating global overrides for instructions and descriptions, directly executing commands with provided arguments.
2026-03-04 12:24:23 +00:00
Gregory Gauthier
79c28da120 feat(chat): add overrides for direct instruction passing in tool calls
Introduce temporary global variables `editInstructionOverride` and `scaffoldDescriptionOverride` to pass instructions from the AI agent directly to the edit and scaffold commands. This avoids interactive prompts and enables seamless tool integration without major refactoring. Resets overrides after execution.
2026-03-04 12:20:20 +00:00
Gregory Gauthier
3e2b8ee7bf refactor(chat): simplify tool call handling by directly setting command args
Update the handleToolCall function to use cmd.SetArgs and Execute for edit, scaffold, testgen, and lint tools, removing the need for custom RunE overrides. This streamlines the execution flow while maintaining functionality.
2026-03-04 12:08:51 +00:00
Gregory Gauthier
d42b5278c1 refactor(chat): integrate tool calls with Cobra command execution
Refactor handleToolCall to set RunE on existing Cobra commands (edit, scaffold, testgen, lint) and execute them for better integration and reuse of CLI logic. Update commit tool to ignore output. Remove unused model parameter and adjust comments for consistency.
2026-03-04 12:05:01 +00:00
Gregory Gauthier
a5fda5bbfd refactor(chat): move tool handling to cmd/chat.go to resolve import cycle
Relocate ToolCall struct and HandleToolCall function from internal/agent/tools.go
to cmd/chat.go, renaming to handleToolCall for package-internal use. This eliminates
the import cycle between cmd and agent packages while preserving agent mode functionality.
2026-03-04 11:59:23 +00:00
Gregory Gauthier
69c5d776e2 feat(agent): implement tool calling in agent mode
Add support for Grok to call tools (edit, scaffold, testgen, lint, commit) via JSON in ```tool blocks.
Introduce HandleToolCall to parse and execute tool requests, integrating with existing commands.
Update system prompt and chat loop to handle tool calls and feed results back.
2026-03-04 11:50:21 +00:00
Gregory Gauthier
875e34669c fix(chat): remove unnecessary nil check in history loading
Simplify the condition for initializing chat history, assuming loadChatHistory always returns a non-nil slice.
2026-03-04 11:19:50 +00:00
Gregory Gauthier
b5e1733952 feat(chat): add chat history persistence
Implement loading and saving of chat history to a JSON file. The history file path is configurable via Viper or defaults to ~/.config/grokkit/chat_history.json. Add functions to load, save, and get the history file path. Remove outdated comment in model selection.
2026-03-04 11:15:54 +00:00
Gregory Gauthier
af7270967c fix(config): correct chat model config key
Update the Viper default key for the chat command from "commands.chat" to "commands.chat.model" to match expected structure.

Remove redundant comment in GetTimeout function for cleaner code.
2026-03-04 11:09:34 +00:00
Gregory Gauthier
6eeb919013 refactor(chat): improve chat command with better UI, prompts, and model handling
- Simplify model selection and defaults in config
- Enhance system prompt and history management
- Update CLI output with colors and exit commands
- Remove placeholder tool handling for cleaner agent mode
2026-03-04 11:04:08 +00:00
Gregory Gauthier
87851513f1 refactor(chat): deprecate agent command and integrate into chat with --agent flag
- Add deprecation warning to agent command, redirecting to `grokkit chat --agent`.
- Refactor chat command to support --agent mode with tool calling, custom models, and history management.
- Introduce basic tool call handling placeholder in agent mode.
- Update config defaults for chat and chat-agent models.
2026-03-04 10:49:53 +00:00
50 changed files with 438 additions and 628 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

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

View File

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

View File

@ -1,25 +1,3 @@
## [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.

View File

@ -38,7 +38,6 @@ grokkit version
- [Commands](#commands)
- [chat](#-grokkit-chat)
- [query](#-grokkit-query)
- [edit](#-grokkit-edit-file-instruction)
- [commit / commitmsg](#-grokkit-commitmsg)
- [review](#-grokkit-review)
@ -73,23 +72,21 @@ grokkit chat --debug # Enable debug logging
- History is saved automatically between sessions
- Use `--debug` to see API request timing
### 🤖 `grokkit query`
### 🧠 `grokkit chat --agent`
One-shot technical question answering. Perfect for quick programming or engineering questions.
**Interactive Agent Mode** — the new primary way to work with Grok on code.
Grok can now call tools directly (`edit`, `scaffold`, `testgen`, `lint`, `commit`) while maintaining full conversation history and safety previews.
```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"
grokkit chat --agent
```
Features:
- Uses the fast non-reasoning model for tool reliability
- Every tool action shows a preview and requires explicit confirmation (y/n)
- Persistent session — you can iterate, reject changes, or give follow-up instructions
- Normal grokkit chat (without --agent) remains unchanged (full reasoning model for thoughtful conversation)
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
**Note**: The old `grokkit agent` command is deprecated and will be removed in v0.3.0. Please migrate to chat --agent.
### ✏️ `grokkit edit FILE "instruction"`

View File

@ -18,6 +18,10 @@ var agentCmd = &cobra.Command{
Short: "Multi-file agent — Grok intelligently edits multiple files with preview",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
color.Red("⚠️ grokkit agent is deprecated!")
color.Red("Use `grokkit chat --agent` instead.")
color.Red("It provides the same (and better) multi-file editing with full conversation, safety previews, and tool control.")
color.Red("This command will be removed in v0.3.0.")
instruction := args[0]
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("agent", modelFlag)
@ -73,7 +77,6 @@ var agentCmd = &cobra.Command{
for i, file := range files {
color.Yellow("[%d/%d] → %s", i+1, len(files), file)
// nolint:gosec // intentional file read from user input
original, err := os.ReadFile(file)
if err != nil {
color.Red("Could not read %s", file)
@ -110,7 +113,7 @@ var agentCmd = &cobra.Command{
}
}
_ = os.WriteFile(file, []byte(newContent), 0600)
_ = os.WriteFile(file, []byte(newContent), 0644)
color.Green("✅ Applied %s", file)
}

View File

@ -27,7 +27,7 @@ func init() {
rootCmd.AddCommand(changelogCmd)
}
func runChangelog(cmd *cobra.Command, _ []string) {
func runChangelog(cmd *cobra.Command, args []string) {
stdout, _ := cmd.Flags().GetBool("stdout")
doCommit, _ := cmd.Flags().GetBool("commit")
version, _ := cmd.Flags().GetString("version")
@ -93,7 +93,7 @@ func runChangelog(cmd *cobra.Command, _ []string) {
return
}
if err := os.WriteFile("CHANGELOG.md", []byte(content), 0600); err != nil {
if err := os.WriteFile("CHANGELOG.md", []byte(content), 0644); err != nil {
color.Red("Failed to write CHANGELOG.md")
return
}

View File

@ -39,8 +39,6 @@ func TestBuildFullChangelog(t *testing.T) {
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()
@ -58,8 +56,6 @@ func TestBuildFullChangelog(t *testing.T) {
})
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()
@ -79,7 +75,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- old bug
`
require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0600))
require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0644))
result := buildFullChangelog(newSection)

View File

@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/git"
"gmgauthier.com/grokkit/internal/grok"
)
@ -19,9 +20,17 @@ type ChatHistory struct {
Messages []map[string]string `json:"messages"`
}
type ToolCall struct {
Tool string `json:"tool"`
File string `json:"file,omitempty"`
Path string `json:"path,omitempty"`
Instruction string `json:"instruction,omitempty"`
Description string `json:"description,omitempty"`
Message string `json:"message,omitempty"`
}
func loadChatHistory() []map[string]string {
histFile := getChatHistoryFile()
// nolint:gosec // intentional file read from config/home
data, err := os.ReadFile(histFile)
if err != nil {
return nil
@ -41,7 +50,7 @@ func saveChatHistory(messages []map[string]string) error {
if err != nil {
return err
}
return os.WriteFile(histFile, data, 0600)
return os.WriteFile(histFile, data, 0644)
}
func getChatHistoryFile() string {
@ -49,51 +58,78 @@ func getChatHistoryFile() string {
if configFile != "" {
return configFile
}
home, _ := os.UserHomeDir()
if home == "" {
home = "."
}
histDir := filepath.Join(home, ".config", "grokkit")
_ = os.MkdirAll(histDir, 0750) // Ignore error, WriteFile will catch it
_ = os.MkdirAll(histDir, 0755)
return filepath.Join(histDir, "chat_history.json")
}
var chatCmd = &cobra.Command{
Use: "chat",
Short: "Simple interactive CLI chat with Grok (full history + streaming)",
Run: func(cmd *cobra.Command, args []string) {
Short: "Interactive chat with Grok (use --agent for tool-enabled mode)",
Long: `Start a persistent conversation with Grok.
Normal mode: coherent, reasoning-focused chat (uses full model).
Agent mode (--agent): Grok can call tools with safety previews (uses fast model).`,
Run: runChat,
}
func init() {
chatCmd.Flags().Bool("agent", false, "Enable agent mode with tool calling (uses fast non-reasoning model)")
chatCmd.Flags().String("model", "", "Override model")
rootCmd.AddCommand(chatCmd)
}
func runChat(cmd *cobra.Command, args []string) {
agentMode, _ := cmd.Flags().GetBool("agent")
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("chat", modelFlag)
var model string
if modelFlag != "" {
model = modelFlag
} else if agentMode {
model = config.GetModel("chat-agent", "")
} else {
model = config.GetModel("chat", "")
}
client := grok.NewClient()
// Strong system prompt to lock in correct model identity
systemPrompt := map[string]string{
"role": "system",
"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.", model),
}
// Load history or start fresh
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...)
if agentMode {
systemPrompt["content"] = "You are Grok in Agent Mode.\n" +
"You can call tools using this exact JSON format inside ```tool blocks:\n\n" +
"```tool\n" +
"{\"tool\": \"edit\", \"file\": \"main.go\", \"instruction\": \"Add error handling\"}\n" +
"```\n\n" +
"Available tools: edit, scaffold, testgen, lint, commit.\n\n" +
"Always use tools when the user asks you to change code, generate tests, lint, or commit.\n" +
"After every tool call, wait for the result before continuing.\n" +
"Be concise and action-oriented."
}
history := loadChatHistory()
if len(history) == 0 {
history = []map[string]string{systemPrompt}
} else if history[0]["role"] != "system" {
history = append([]map[string]string{systemPrompt}, history...)
} else {
history[0] = systemPrompt
}
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
color.Cyan("│ Grokkit Chat — Model: %s │", model)
color.Cyan("│ Type /quit or Ctrl+C to exit │")
color.Cyan("│ Grokkit Chat %s — Model: %s │", map[bool]string{true: "(Agent Mode)", false: ""}[agentMode], model)
color.Cyan("│ Type /quit to exit │")
color.Cyan("└──────────────────────────────────────────────────────────────┘\n")
scanner := bufio.NewScanner(os.Stdin)
for {
color.Yellow("You > ")
if !scanner.Scan() {
@ -114,12 +150,73 @@ var chatCmd = &cobra.Command{
color.Green("Grok > ")
reply := client.Stream(history, model)
history = append(history, map[string]string{"role": "assistant", "content": reply})
if agentMode && strings.Contains(reply, "```tool") {
handleToolCall(reply, &history)
continue
}
// Save history after each exchange
history = append(history, map[string]string{"role": "assistant", "content": reply})
_ = saveChatHistory(history)
fmt.Println()
}
},
}
func handleToolCall(reply string, history *[]map[string]string) {
start := strings.Index(reply, "```tool")
if start == -1 {
return
}
end := strings.Index(reply[start+7:], "```")
if end == -1 {
return
}
block := strings.TrimSpace(reply[start+7 : start+7+end])
var tc ToolCall
if err := json.Unmarshal([]byte(block), &tc); err != nil {
color.Red("Failed to parse tool call: %v", err)
return
}
color.Yellow("\n[Agent] Grok wants to call tool: %s", tc.Tool)
switch tc.Tool {
case "edit":
if tc.File != "" && tc.Instruction != "" {
editInstruction = tc.Instruction
editCmd.SetArgs([]string{tc.File})
_ = editCmd.Execute()
}
case "scaffold":
if tc.Path != "" && tc.Description != "" {
scaffoldDescription = tc.Description
scaffoldCmd.SetArgs([]string{tc.Path})
_ = scaffoldCmd.Execute()
}
case "testgen":
if tc.File != "" {
testgenCmd.SetArgs([]string{tc.File})
_ = testgenCmd.Execute()
}
case "lint":
if tc.File != "" {
lintCmd.SetArgs([]string{tc.File})
_ = lintCmd.Execute()
}
case "commit":
if tc.Message != "" {
_, _ = git.Run([]string{"commit", "-m", tc.Message})
}
default:
color.Red("Unknown tool: %s", tc.Tool)
}
*history = append(*history, map[string]string{"role": "assistant", "content": reply})
}
// Temporary variables used by agent mode to pass instructions to existing commands
var (
editInstruction string
scaffoldDescription string
)

View File

@ -91,11 +91,11 @@ func TestLoadChatHistory_InvalidJSON(t *testing.T) {
// Create invalid JSON file
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)
}
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)
}

View File

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

View File

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

View File

@ -68,7 +68,6 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
return
}
// nolint:gosec // intentional file read from user input
originalContent, err := os.ReadFile(filePath)
if err != nil {
logger.Error("failed to read file", "file", filePath, "error", err)
@ -100,7 +99,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
if len(lines) < previewLines {
previewLines = len(lines)
}
for i := range previewLines {
for i := 0; i < previewLines; i++ {
fmt.Println(lines[i])
}
if len(lines) > previewLines {
@ -124,7 +123,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
}
}
if err := os.WriteFile(filePath, []byte(documented), 0600); err != nil {
if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil {
logger.Error("failed to write documented file", "file", filePath, "error", err)
color.Red("❌ Failed to write file: %v", err)
return

View File

@ -1,102 +1,91 @@
package cmd
import (
"bufio"
"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/logger"
)
var editCmd = &cobra.Command{
Use: "edit FILE INSTRUCTION",
Use: "edit [file]",
Short: "Edit a file in-place with Grok (safe preview)",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
filePath := args[0]
instruction := args[1]
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("edit", modelFlag)
logger.Info("edit command started",
"file", filePath,
"instruction", instruction,
"model", model)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
logger.Error("file not found", "file", filePath, "error", err)
color.Red("File not found: %s", filePath)
os.Exit(1)
Long: `Ask Grok to edit a file. Shows a preview diff and requires explicit confirmation before writing.`,
Run: runEdit,
}
// nolint:gosec // intentional file read from user input
original, err := 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)
func init() {
editCmd.Flags().String("instruction", "", "Edit instruction (used internally by agent mode)")
rootCmd.AddCommand(editCmd)
}
func runEdit(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("Usage: grokkit edit <file>")
return
}
file := args[0]
// If agent mode passed an instruction, use it directly (no prompt)
var instruction string
if editInstruction != "" {
instruction = editInstruction
color.Cyan("Agent instruction: %s", instruction)
} else {
// Normal interactive mode
color.Yellow("Enter edit instruction for %s: ", file)
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
instruction = strings.TrimSpace(scanner.Text())
}
if instruction == "" {
color.Yellow("No instruction provided.")
return
}
}
logger.Debug("file read successfully", "file", filePath, "size_bytes", len(original))
cleanedOriginal := removeLastModifiedComments(string(original))
client := grok.NewClient()
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": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(filePath), cleanedOriginal, instruction)},
}
messages := buildEditMessages(file, instruction)
color.Yellow("Asking Grok to %s...\n", instruction)
raw := client.StreamSilent(messages, model)
newContent := grok.CleanCodeResponse(raw)
color.Green("✓ Response received")
color.Yellow("Asking Grok to edit %s...", file)
edited := client.Stream(messages, config.GetModel("edit", ""))
color.Cyan("\nProposed changes:")
fmt.Println("--- a/" + filepath.Base(filePath))
fmt.Println("+++ b/" + filepath.Base(filePath))
fmt.Print(newContent)
// Show preview
color.Cyan("\n--- Proposed changes to %s ---\n%s\n--------------------------------", file, edited)
fmt.Print("\n\nApply these changes? (y/n): ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
color.Red("Failed to read input: %v", err)
color.Yellow("Changes discarded.")
return
}
color.Yellow("Apply these changes to %s? (y/n): ", file)
_, _ = fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
color.Yellow("Changes discarded.")
color.Yellow("Aborted.")
return
}
logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent))
if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err)
if err := os.WriteFile(file, []byte(edited), 0644); err != nil {
color.Red("Failed to write file: %v", err)
os.Exit(1)
return
}
logger.Info("changes applied successfully",
"file", filePath,
"original_size", len(original),
"new_size", len(newContent))
color.Green("✅ Applied successfully!")
color.Green("✅ Successfully edited %s", file)
}
// buildEditMessages is kept unchanged from original
func buildEditMessages(file, instruction string) []map[string]string {
content, _ := os.ReadFile(file)
return []map[string]string{
{
"role": "system",
"content": "You are an expert programmer. Edit the following file according to the user's instruction. Return ONLY the full new file content. Do not include any explanations.",
},
{
"role": "user",
"content": fmt.Sprintf("File: %s\n\nCurrent content:\n%s\n\nInstruction: %s", file, string(content), instruction),
},
}
func removeLastModifiedComments(content string) string {
lines := strings.Split(content, "\n")
cleanedLines := make([]string, 0, len(lines))
for _, line := range lines {
if strings.Contains(line, "Last modified") {
continue
}
cleanedLines = append(cleanedLines, line)
}
return strings.Join(cleanedLines, "\n")
}

View File

@ -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)
}
})
}
}

View File

@ -17,7 +17,7 @@ func TestEditCommand(t *testing.T) {
defer func() { _ = os.Remove(tmpfile.Name()) }()
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)
}
@ -34,7 +34,7 @@ func TestEditCommand(t *testing.T) {
newContent := grok.CleanCodeResponse(raw)
// 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)
}

View File

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

View File

@ -103,7 +103,6 @@ func runLint(cmd *cobra.Command, args []string) {
}
// Read original file content
// nolint:gosec // intentional file read from user input
originalContent, err := os.ReadFile(absPath)
if err != nil {
logger.Error("failed to read file", "file", absPath, "error", err)
@ -143,7 +142,7 @@ func runLint(cmd *cobra.Command, args []string) {
if len(lines) < previewLines {
previewLines = len(lines)
}
for i := range previewLines {
for i := 0; i < previewLines; i++ {
fmt.Println(lines[i])
}
if len(lines) > previewLines {
@ -170,7 +169,7 @@ func runLint(cmd *cobra.Command, args []string) {
}
// 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)
color.Red("❌ Failed to write file: %v", err)
return

View File

@ -14,23 +14,17 @@ var prDescribeCmd = &cobra.Command{
Run: runPRDescribe,
}
func init() {
prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against")
}
func runPRDescribe(cmd *cobra.Command, _ []string) {
base, _ := cmd.Flags().GetString("base")
diff, err := gitRun([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"})
func runPRDescribe(cmd *cobra.Command, args []string) {
diff, err := gitRun([]string{"diff", "main..HEAD", "--no-color"})
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 {
color.Red("Failed to get branch diff: %v", err)
return
}
}
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
}
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,
}
func runReview(cmd *cobra.Command, _ []string) {
func runReview(cmd *cobra.Command, args []string) {
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("review", modelFlag)

View File

@ -60,7 +60,6 @@ func init() {
rootCmd.AddCommand(docsCmd)
rootCmd.AddCommand(testgenCmd)
rootCmd.AddCommand(scaffoldCmd)
rootCmd.AddCommand(queryCmd)
// Add model flag to all commands
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 }
}
// 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 {
c := &cobra.Command{}
c.Flags().String("model", "", "")
c.Flags().String("base", "master", "")
return c
}
@ -309,62 +308,22 @@ func TestRunPRDescribe(t *testing.T) {
}
})
t.Run("uses custom base branch", func(t *testing.T) {
mock := &mockStreamer{response: "PR description"}
t.Run("second diff error — skips AI", func(t *testing.T) {
mock := &mockStreamer{}
defer withMockClient(mock)()
var capturedArgs []string
callCount := 0
defer withMockGit(func(args []string) (string, error) {
capturedArgs = args
return "diff content", nil
})()
cmd := testCmd()
if err := cmd.Flags().Set("base", "develop"); err != nil {
t.Fatal(err)
callCount++
if callCount == 2 {
return "", errors.New("no remote")
}
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
}
}
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
return "", nil
})()
runPRDescribe(testCmd(), nil)
if mock.calls != 1 {
t.Errorf("expected 1 AI call, 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)
if mock.calls != 0 {
t.Errorf("expected 0 AI calls, got %d", mock.calls)
}
})
}

View File

@ -1,187 +1,90 @@
package cmd
import (
"bufio"
"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/logger"
)
var scaffoldCmd = &cobra.Command{
Use: "scaffold FILE DESCRIPTION",
Use: "scaffold [path]",
Short: "Scaffold a new file with Grok (safe preview + confirmation)",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
filePath := args[0]
description := args[1]
withTests, _ := cmd.Flags().GetBool("with-tests")
dryRun, _ := cmd.Flags().GetBool("dry-run")
force, _ := cmd.Flags().GetBool("force")
yes, _ := cmd.Flags().GetBool("yes")
langOverride, _ := cmd.Flags().GetString("lang")
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("scaffold", modelFlag)
logger.Info("scaffold command started",
"file", filePath,
"description", description,
"with_tests", withTests,
"model", model)
// Safety: don't overwrite existing file unless --force
if _, err := os.Stat(filePath); err == nil && !force {
color.Red("File already exists: %s (use --force to overwrite)", filePath)
os.Exit(1)
}
dir := filepath.Dir(filePath)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0750); err != nil {
logger.Error("failed to create directory", "dir", dir, "error", err)
color.Red("Failed to create directory: %v", err)
os.Exit(1)
}
}
// Detect language and harvest context
lang := detectLanguage(filePath, langOverride)
context := harvestContext(filePath, lang)
// Build system prompt with style enforcement
systemPrompt := fmt.Sprintf(`You are an expert %s programmer.
Match the exact style, naming conventions, error handling, logging, and package structure of this project.
Return ONLY the complete code file. No explanations, no markdown, no backticks.`, lang)
if withTests {
systemPrompt += "\nAlso generate a basic _test.go file with at least one test when --with-tests is used."
}
messages := []map[string]string{
{"role": "system", "content": systemPrompt},
{"role": "user", "content": fmt.Sprintf(
"File path: %s\n\nProject context:\n%s\n\nCreate this new file:\n%s",
filePath, context, description)},
}
color.Yellow("Asking Grok to scaffold %s...\n", filepath.Base(filePath))
client := grok.NewClient()
raw := client.StreamSilent(messages, model)
newContent := grok.CleanCodeResponse(raw)
color.Green("Response received")
// Preview
color.Cyan("\nProposed new file:")
fmt.Printf("--- /dev/null\n")
fmt.Printf("+++ b/%s\n", filepath.Base(filePath))
fmt.Println(newContent)
if dryRun {
color.Yellow("\n--dry-run: file not written")
return
}
if !yes {
fmt.Print("\n\nCreate this file? (y/n): ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil || (confirm != "y" && confirm != "Y") {
color.Yellow("Scaffold cancelled.")
return
}
}
// Write main file
if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err)
os.Exit(1)
}
color.Green("✓ Created: %s", filePath)
// Optional test file
if withTests {
testPath := strings.TrimSuffix(filePath, filepath.Ext(filePath)) + "_test.go"
testMessages := []map[string]string{
{"role": "system", "content": systemPrompt},
{"role": "user", "content": fmt.Sprintf("Generate a basic test file for %s using the same style.", filepath.Base(filePath))},
}
testRaw := client.StreamSilent(testMessages, model)
testContent := grok.CleanCodeResponse(testRaw)
if err := os.WriteFile(testPath, []byte(testContent), 0600); err == nil {
color.Green("✓ Created test: %s", filepath.Base(testPath))
}
}
logger.Info("scaffold completed successfully", "file", filePath, "with_tests", withTests)
},
}
// Simple language detector (can be moved to internal/linter later)
func detectLanguage(path, override string) string {
if override != "" {
return override
}
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".go":
return "Go"
case ".py":
return "Python"
case ".js", ".ts":
return "TypeScript"
case ".c":
return "C"
case ".cpp":
return "C++"
case ".java":
return "Java"
// add more as needed
default:
return "code"
}
}
// Basic context harvester (~4000 token cap)
func harvestContext(filePath, _ string) string {
var sb strings.Builder
dir := filepath.Dir(filePath)
// Siblings
files, _ := os.ReadDir(dir)
for _, f := range files {
if f.IsDir() || strings.HasPrefix(f.Name(), ".") {
continue
}
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()))
if len(content) > 2000 {
content = content[:2000]
}
// Fixed: use Fprintf instead of WriteString + Sprintf
fmt.Fprintf(&sb, "=== %s ===\n%s\n\n", f.Name(), string(content))
}
}
// Rough token cap
if sb.Len() > 4000 {
return sb.String()[:4000] + "\n... (truncated)"
}
return sb.String()
Long: `Generate a new file from a description. Shows preview and requires explicit confirmation before writing.`,
Run: runScaffold,
}
func init() {
scaffoldCmd.Flags().Bool("with-tests", false, "Also scaffold a basic _test.go file")
scaffoldCmd.Flags().Bool("dry-run", false, "Preview only, do not write")
scaffoldCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
scaffoldCmd.Flags().Bool("force", false, "Overwrite existing file")
scaffoldCmd.Flags().String("lang", "", "Force language for prompt (Go, Python, etc.)")
scaffoldCmd.Flags().String("description", "", "Scaffold description (used internally by agent mode)")
rootCmd.AddCommand(scaffoldCmd)
}
func runScaffold(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("Usage: grokkit scaffold <path>")
return
}
path := args[0]
// If agent mode passed a description, use it directly
var description string
if scaffoldDescription != "" {
description = scaffoldDescription
color.Cyan("Agent description: %s", description)
} else {
// Normal interactive mode
color.Yellow("Enter description for %s: ", path)
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
description = strings.TrimSpace(scanner.Text())
}
if description == "" {
color.Yellow("No description provided.")
return
}
}
client := grok.NewClient()
messages := buildScaffoldMessages(path, description)
color.Yellow("Asking Grok to scaffold %s...", path)
scaffolded := client.Stream(messages, config.GetModel("scaffold", ""))
// Show preview
color.Cyan("\n--- Proposed content for %s ---\n%s\n--------------------------------", path, scaffolded)
var confirm string
color.Yellow("Create %s with this content? (y/n): ", path)
_, _ = fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
color.Yellow("Aborted.")
return
}
if err := os.WriteFile(path, []byte(scaffolded), 0644); err != nil {
color.Red("Failed to write file: %v", err)
return
}
color.Green("✅ Successfully scaffolded %s", path)
}
// buildScaffoldMessages is kept unchanged from original
func buildScaffoldMessages(path, description string) []map[string]string {
return []map[string]string{
{
"role": "system",
"content": "You are an expert programmer. Generate a complete, well-structured file based on the user's description. Return ONLY the full file content. Do not include any explanations or markdown fences.",
},
{
"role": "user",
"content": fmt.Sprintf("Path: %s\n\nDescription: %s", path, description),
},
}
}

View File

@ -1,7 +1,6 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -95,12 +94,11 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
// Handle existing test file
testExists := true
testInfo, err := os.Stat(testPath)
switch {
case errors.Is(err, os.ErrNotExist):
if os.IsNotExist(err) {
testExists = false
case err != nil:
} else if err != nil {
return fmt.Errorf("stat test file: %w", err)
case testInfo.IsDir():
} else if testInfo.IsDir() {
return fmt.Errorf("test path is dir: %s", testPath)
}
@ -160,7 +158,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
}
// 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)
}
@ -171,7 +169,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
func removeSourceComments(content, lang string) string {
lines := strings.Split(content, "\n")
cleanedLines := make([]string, 0, len(lines))
var cleanedLines []string
for _, line := range lines {
if strings.Contains(line, "Last modified") || strings.Contains(line, "Generated by") ||
strings.Contains(line, "Generated by testgen") {

View File

@ -93,7 +93,6 @@ int foo() {}`,
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := removeSourceComments(tt.input, tt.lang)
if 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 {
t.Run(tt.lang, func(t *testing.T) {
t.Parallel()
got := getTestPrompt(tt.lang)
if tt.wantPrefix != "" && !strings.HasPrefix(got, 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 {
t.Run(tt.filePath+"_"+tt.lang, func(t *testing.T) {
t.Parallel()
got := getTestFilePath(tt.filePath, tt.lang)
if 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 {
t.Run(tt.lang, func(t *testing.T) {
t.Parallel()
got := getCodeLang(tt.lang)
if 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"
)
// Load initializes the configuration from Viper
func Load() {
home, err := os.UserHomeDir()
if err != nil {
@ -37,13 +36,12 @@ func Load() {
viper.SetDefault("commands.prdescribe.model", "grok-4")
viper.SetDefault("commands.review.model", "grok-4")
viper.SetDefault("commands.docs.model", "grok-4")
viper.SetDefault("commands.query.model", "grok-4-1-fast-non-reasoning")
viper.SetDefault("commands.chat.model", "grok-4-1")
viper.SetDefault("commands.chat-agent.model", "grok-4-1-fast-non-reasoning")
// Config file is optional, so we ignore read errors
_ = viper.ReadInConfig()
}
// GetModel returns the model to use for a specific command, considering flags and aliases
func GetModel(commandName string, flagModel string) string {
if flagModel != "" {
if alias := viper.GetString("aliases." + flagModel); alias != "" {
@ -51,28 +49,24 @@ func GetModel(commandName string, flagModel string) string {
}
return flagModel
}
cmdModel := viper.GetString("commands." + commandName + ".model")
if cmdModel != "" {
if cmdModel := viper.GetString("commands." + commandName + ".model"); cmdModel != "" {
return cmdModel
}
return viper.GetString("default_model")
}
// GetTemperature returns the temperature from the configuration
func GetTemperature() float64 {
return viper.GetFloat64("temperature")
}
// GetTimeout returns the timeout from the configuration
func GetTimeout() int {
timeout := viper.GetInt("timeout")
if timeout <= 0 {
return 60 // Default 60 seconds
return 60
}
return timeout
}
// GetLogLevel returns the log level from the configuration
func GetLogLevel() string {
return viper.GetString("log_level")
}

View File

@ -9,7 +9,6 @@ type GitError struct {
}
func (e *GitError) Error() string {
_ = e.Err // keep field used for error message
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)
}
if !errors.Is(gitErr, baseErr) {
t.Errorf("GitError did not wrap base error")
if gitErr.Unwrap() != baseErr {
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)
}
if !errors.Is(fileErr, baseErr) {
t.Errorf("FileError did not wrap base error")
if fileErr.Unwrap() != baseErr {
t.Errorf("FileError.Unwrap() did not return base error")
}
}
@ -80,7 +80,7 @@ func TestAPIErrorUnwrap(t *testing.T) {
Message: "internal error",
Err: baseErr,
}
if !errors.Is(apiErr, baseErr) {
t.Errorf("APIError.Unwrap() = %v, want %v", apiErr.Unwrap(), baseErr)
if unwrap := apiErr.Unwrap(); 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, " ")
logger.Debug("executing git command", "command", cmdStr, "args", args)
// nolint:gosec // intentional subprocess for git operation
out, err := exec.Command("git", args...).Output()
if err != nil {
logger.Error("git command failed",
@ -31,7 +30,6 @@ func Run(args []string) (string, error) {
func IsRepo() bool {
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()
isRepo := err == nil
logger.Debug("git repository check completed", "is_repo", isRepo)

View File

@ -76,23 +76,20 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
"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)
if err != nil {
logger.Error("failed to marshal API request", "error", err)
color.Red("Failed to marshal request: %v", err)
cancel()
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 {
logger.Error("failed to create HTTP request", "error", err, "url", url)
color.Red("Failed to create request: %v", err)
cancel()
os.Exit(1)
}
req.Header.Set("Authorization", "Bearer "+c.APIKey)
@ -107,7 +104,6 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp
"model", model,
"duration_ms", time.Since(startTime).Milliseconds())
color.Red("Request failed: %v", err)
cancel()
os.Exit(1)
}
defer func() { _ = resp.Body.Close() }()

View File

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

View File

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

View File

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

View File

@ -5,22 +5,21 @@ This document provides a table of contents for all tasks and features currently
## 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
* [2] [non-interactive-query.md](./queued/non-interactive-query.md) : grokkit query (non-interactive one-shot questions)
* [3] [rg.md](./queued/rg.md) : grokkit agent ripgrep (rg) integration
* [4] [gotools.md](./queued/gotools.md) : grokkit agent Go tools integration
* [5] [make.md](./queued/make.md) : grokkit agent make integration
* [6] [tea.md](./queued/tea.md) : grokkit agent tea (Gitea CLI) integration
* [7] [cnotes.md](./queued/cnotes.md) : grokkit agent cnotes integration
* [8] [profile.md](./queued/profile.md) : grokkit profile
* [9] [pprof.md](./queued/pprof.md) : grokkit agent pprof integration
* [10] [audit.md](./queued/audit.md) : grokkit audit
* [11] [git-chglog.md](./queued/git-chglog.md) : grokkit agent git-chglog integration
* [12] [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+)*
* [changelog.md](./queued/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

View File

@ -1,4 +1,5 @@
# TODO ITEM 1
## Priority: 12 of 12
- [ ] 1 step one
- [ ] 2 step two
- [ ] 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`
## Priority: 9 of 12
**Description**: Comprehensive AI-powered code audit for security, performance, best practices, and potential bugs across single files or entire projects.
**Benefits**:

View File

@ -1,4 +1,5 @@
# `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").

View File

@ -1,4 +1,5 @@
# `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.

View File

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

View File

@ -1,5 +1,6 @@
# Grokkit Interactive Agent
**Priority:** 2 of 12 (right after testgen)
## Goal

View File

@ -1,4 +1,5 @@
# `grokkit agent` make integration
## Priority: 3 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").

View File

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

View File

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

View File

@ -1,4 +1,5 @@
# `grokkit agent` ripgrep (rg) integration
## Priority: 7 of 12
**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
## Priority: 4 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.