Compare commits
11 Commits
feature/in
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f6596c13f | |||
| f0322a84bd | |||
| 0cf0351826 | |||
| c35dbffe9b | |||
| d92b88e90b | |||
| b1d3a445ec | |||
|
|
11faf95573 | ||
|
|
d9b13739b5 | ||
| a6d52d890b | |||
|
|
587be3a046 | ||
|
|
cc6a2f642f |
@ -1,37 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,18 +1,14 @@
|
|||||||
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
|
||||||
|
|||||||
3
.grok/settings.json
Normal file
3
.grok/settings.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"model": "grok-code-fast-1"
|
||||||
|
}
|
||||||
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,3 +1,25 @@
|
|||||||
|
## [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
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|||||||
25
README.md
25
README.md
@ -38,6 +38,7 @@ 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)
|
||||||
@ -72,21 +73,23 @@ 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 chat --agent`
|
### 🤖 `grokkit query`
|
||||||
|
|
||||||
**Interactive Agent Mode** — the new primary way to work with Grok on code.
|
One-shot technical question answering. Perfect for quick programming or engineering questions.
|
||||||
|
|
||||||
Grok can now call tools directly (`edit`, `scaffold`, `testgen`, `lint`, `commit`) while maintaining full conversation history and safety previews.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grokkit chat --agent
|
# Basic usage
|
||||||
```
|
grokkit query "How do I sort a slice of structs by a field in Go?"
|
||||||
- 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)
|
|
||||||
|
|
||||||
**Note**: The old `grokkit agent` command is deprecated and will be removed in v0.3.0. Please migrate to chat --agent.
|
# 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"`
|
||||||
|
|||||||
@ -18,10 +18,6 @@ var agentCmd = &cobra.Command{
|
|||||||
Short: "Multi-file agent — Grok intelligently edits multiple files with preview",
|
Short: "Multi-file agent — Grok intelligently edits multiple files with preview",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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]
|
instruction := args[0]
|
||||||
modelFlag, _ := cmd.Flags().GetString("model")
|
modelFlag, _ := cmd.Flags().GetString("model")
|
||||||
model := config.GetModel("agent", modelFlag)
|
model := config.GetModel("agent", modelFlag)
|
||||||
@ -77,6 +73,7 @@ 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)
|
||||||
@ -113,7 +110,7 @@ var agentCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.WriteFile(file, []byte(newContent), 0644)
|
_ = os.WriteFile(file, []byte(newContent), 0600)
|
||||||
color.Green("✅ Applied %s", file)
|
color.Green("✅ Applied %s", file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(changelogCmd)
|
rootCmd.AddCommand(changelogCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runChangelog(cmd *cobra.Command, args []string) {
|
func runChangelog(cmd *cobra.Command, _ []string) {
|
||||||
stdout, _ := cmd.Flags().GetBool("stdout")
|
stdout, _ := cmd.Flags().GetBool("stdout")
|
||||||
doCommit, _ := cmd.Flags().GetBool("commit")
|
doCommit, _ := cmd.Flags().GetBool("commit")
|
||||||
version, _ := cmd.Flags().GetString("version")
|
version, _ := cmd.Flags().GetString("version")
|
||||||
@ -93,7 +93,7 @@ func runChangelog(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile("CHANGELOG.md", []byte(content), 0644); err != nil {
|
if err := os.WriteFile("CHANGELOG.md", []byte(content), 0600); err != nil {
|
||||||
color.Red("Failed to write CHANGELOG.md")
|
color.Red("Failed to write CHANGELOG.md")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,8 @@ func TestBuildFullChangelog(t *testing.T) {
|
|||||||
newSection := "## [v0.2.0] - 2026-03-03\n\n### Added\n- changelog command\n"
|
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.Run("creates new file with header", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// nolint:tparallel // os.Chdir affects the entire process
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
originalWd, err := os.Getwd()
|
originalWd, err := os.Getwd()
|
||||||
@ -56,6 +58,8 @@ func TestBuildFullChangelog(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("prepends to existing file", func(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()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
originalWd, err := os.Getwd()
|
originalWd, err := os.Getwd()
|
||||||
@ -75,7 +79,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### Fixed
|
### Fixed
|
||||||
- old bug
|
- old bug
|
||||||
`
|
`
|
||||||
require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0644))
|
require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0600))
|
||||||
|
|
||||||
result := buildFullChangelog(newSection)
|
result := buildFullChangelog(newSection)
|
||||||
|
|
||||||
|
|||||||
141
cmd/chat.go
141
cmd/chat.go
@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/git"
|
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
"gmgauthier.com/grokkit/internal/grok"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,17 +19,9 @@ type ChatHistory struct {
|
|||||||
Messages []map[string]string `json:"messages"`
|
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 {
|
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
|
||||||
@ -50,7 +41,7 @@ func saveChatHistory(messages []map[string]string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(histFile, data, 0644)
|
return os.WriteFile(histFile, data, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChatHistoryFile() string {
|
func getChatHistoryFile() string {
|
||||||
@ -58,78 +49,51 @@ func getChatHistoryFile() string {
|
|||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
return configFile
|
return configFile
|
||||||
}
|
}
|
||||||
|
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
if home == "" {
|
if home == "" {
|
||||||
home = "."
|
home = "."
|
||||||
}
|
}
|
||||||
histDir := filepath.Join(home, ".config", "grokkit")
|
histDir := filepath.Join(home, ".config", "grokkit")
|
||||||
_ = os.MkdirAll(histDir, 0755)
|
_ = os.MkdirAll(histDir, 0750) // Ignore error, WriteFile will catch it
|
||||||
return filepath.Join(histDir, "chat_history.json")
|
return filepath.Join(histDir, "chat_history.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
var chatCmd = &cobra.Command{
|
var chatCmd = &cobra.Command{
|
||||||
Use: "chat",
|
Use: "chat",
|
||||||
Short: "Interactive chat with Grok (use --agent for tool-enabled mode)",
|
Short: "Simple interactive CLI chat with Grok (full history + streaming)",
|
||||||
Long: `Start a persistent conversation with Grok.
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
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")
|
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()
|
client := grok.NewClient()
|
||||||
|
|
||||||
|
// Strong system prompt to lock in correct model identity
|
||||||
systemPrompt := map[string]string{
|
systemPrompt := map[string]string{
|
||||||
"role": "system",
|
"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.", model),
|
"content": fmt.Sprintf("You are Grok 4, the latest and most powerful model from xAI (2026). You are currently running as `%s`. Be helpful, truthful, and a little irreverent. Never claim to be an older model.", model),
|
||||||
}
|
|
||||||
|
|
||||||
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."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load history or start fresh
|
||||||
history := loadChatHistory()
|
history := loadChatHistory()
|
||||||
if len(history) == 0 {
|
if history == nil {
|
||||||
history = []map[string]string{systemPrompt}
|
history = []map[string]string{systemPrompt}
|
||||||
} else if history[0]["role"] != "system" {
|
|
||||||
history = append([]map[string]string{systemPrompt}, history...)
|
|
||||||
} else {
|
} else {
|
||||||
|
// Update system prompt in loaded history
|
||||||
|
if len(history) > 0 && history[0]["role"] == "system" {
|
||||||
history[0] = systemPrompt
|
history[0] = systemPrompt
|
||||||
|
} else {
|
||||||
|
history = append([]map[string]string{systemPrompt}, history...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
|
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
|
||||||
color.Cyan("│ Grokkit Chat %s — Model: %s │", map[bool]string{true: "(Agent Mode)", false: ""}[agentMode], model)
|
color.Cyan("│ Grokkit Chat — Model: %s │", model)
|
||||||
color.Cyan("│ Type /quit to exit │")
|
color.Cyan("│ Type /quit or Ctrl+C to exit │")
|
||||||
color.Cyan("└──────────────────────────────────────────────────────────────┘\n")
|
color.Cyan("└──────────────────────────────────────────────────────────────┘\n")
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
color.Yellow("You > ")
|
color.Yellow("You > ")
|
||||||
if !scanner.Scan() {
|
if !scanner.Scan() {
|
||||||
@ -150,73 +114,12 @@ func runChat(cmd *cobra.Command, args []string) {
|
|||||||
color.Green("Grok > ")
|
color.Green("Grok > ")
|
||||||
reply := client.Stream(history, model)
|
reply := client.Stream(history, model)
|
||||||
|
|
||||||
if agentMode && strings.Contains(reply, "```tool") {
|
|
||||||
handleToolCall(reply, &history)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
history = append(history, map[string]string{"role": "assistant", "content": reply})
|
history = append(history, map[string]string{"role": "assistant", "content": reply})
|
||||||
|
|
||||||
|
// Save history after each exchange
|
||||||
_ = saveChatHistory(history)
|
_ = saveChatHistory(history)
|
||||||
|
|
||||||
fmt.Println()
|
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
|
|
||||||
)
|
|
||||||
|
|||||||
@ -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, 0755); err != nil {
|
if err := os.MkdirAll(histDir, 0750); 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{{{"), 0644); err != nil {
|
if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0600); err != nil {
|
||||||
t.Fatalf("WriteFile() error: %v", err)
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ var commitCmd = &cobra.Command{
|
|||||||
Run: runCommit,
|
Run: runCommit,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCommit(cmd *cobra.Command, args []string) {
|
func runCommit(cmd *cobra.Command, _ []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,6 +45,7 @@ func runCommit(cmd *cobra.Command, args []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 {
|
||||||
|
|||||||
@ -49,6 +49,7 @@ 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":
|
||||||
|
|||||||
@ -68,6 +68,7 @@ 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)
|
||||||
@ -99,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 := 0; i < previewLines; i++ {
|
for i := range previewLines {
|
||||||
fmt.Println(lines[i])
|
fmt.Println(lines[i])
|
||||||
}
|
}
|
||||||
if len(lines) > previewLines {
|
if len(lines) > previewLines {
|
||||||
@ -123,7 +124,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil {
|
if err := os.WriteFile(filePath, []byte(documented), 0600); 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
|
||||||
|
|||||||
127
cmd/edit.go
127
cmd/edit.go
@ -1,91 +1,102 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
"gmgauthier.com/grokkit/internal/grok"
|
||||||
|
"gmgauthier.com/grokkit/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var editCmd = &cobra.Command{
|
var editCmd = &cobra.Command{
|
||||||
Use: "edit [file]",
|
Use: "edit FILE INSTRUCTION",
|
||||||
Short: "Edit a file in-place with Grok (safe preview)",
|
Short: "Edit a file in-place with Grok (safe preview)",
|
||||||
Long: `Ask Grok to edit a file. Shows a preview diff and requires explicit confirmation before writing.`,
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runEdit,
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
// nolint:gosec // intentional file read from user input
|
||||||
editCmd.Flags().String("instruction", "", "Edit instruction (used internally by agent mode)")
|
original, err := os.ReadFile(filePath)
|
||||||
rootCmd.AddCommand(editCmd)
|
if err != nil {
|
||||||
}
|
logger.Error("failed to read file", "file", filePath, "error", err)
|
||||||
|
color.Red("Failed to read file: %v", err)
|
||||||
func runEdit(cmd *cobra.Command, args []string) {
|
os.Exit(1)
|
||||||
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()
|
client := grok.NewClient()
|
||||||
messages := buildEditMessages(file, instruction)
|
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)},
|
||||||
|
}
|
||||||
|
|
||||||
color.Yellow("Asking Grok to edit %s...", file)
|
color.Yellow("Asking Grok to %s...\n", instruction)
|
||||||
edited := client.Stream(messages, config.GetModel("edit", ""))
|
raw := client.StreamSilent(messages, model)
|
||||||
|
newContent := grok.CleanCodeResponse(raw)
|
||||||
|
color.Green("✓ Response received")
|
||||||
|
|
||||||
// Show preview
|
color.Cyan("\nProposed changes:")
|
||||||
color.Cyan("\n--- Proposed changes to %s ---\n%s\n--------------------------------", file, edited)
|
fmt.Println("--- a/" + filepath.Base(filePath))
|
||||||
|
fmt.Println("+++ b/" + filepath.Base(filePath))
|
||||||
|
fmt.Print(newContent)
|
||||||
|
|
||||||
|
fmt.Print("\n\nApply these changes? (y/n): ")
|
||||||
var confirm string
|
var confirm string
|
||||||
color.Yellow("Apply these changes to %s? (y/n): ", file)
|
if _, err := fmt.Scanln(&confirm); err != nil {
|
||||||
_, _ = fmt.Scanln(&confirm)
|
color.Red("Failed to read input: %v", err)
|
||||||
|
color.Yellow("Changes discarded.")
|
||||||
|
return
|
||||||
|
}
|
||||||
if confirm != "y" && confirm != "Y" {
|
if confirm != "y" && confirm != "Y" {
|
||||||
color.Yellow("Aborted.")
|
color.Yellow("Changes discarded.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(file, []byte(edited), 0644); err != nil {
|
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)
|
||||||
color.Red("Failed to write file: %v", err)
|
color.Red("Failed to write file: %v", err)
|
||||||
return
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
logger.Info("changes applied successfully",
|
||||||
color.Green("✅ Successfully edited %s", file)
|
"file", filePath,
|
||||||
}
|
"original_size", len(original),
|
||||||
|
"new_size", len(newContent))
|
||||||
// buildEditMessages is kept unchanged from original
|
color.Green("✅ Applied successfully!")
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|||||||
53
cmd/edit_helper_test.go
Normal file
53
cmd/edit_helper_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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, 0644); err != nil {
|
if err := os.WriteFile(tmpfile.Name(), original, 0600); 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), 0644); err != nil {
|
if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ var historyCmd = &cobra.Command{
|
|||||||
Run: runHistory,
|
Run: runHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHistory(cmd *cobra.Command, args []string) {
|
func runHistory(cmd *cobra.Command, _ []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)
|
||||||
|
|||||||
@ -103,6 +103,7 @@ 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)
|
||||||
@ -142,7 +143,7 @@ func runLint(cmd *cobra.Command, args []string) {
|
|||||||
if len(lines) < previewLines {
|
if len(lines) < previewLines {
|
||||||
previewLines = len(lines)
|
previewLines = len(lines)
|
||||||
}
|
}
|
||||||
for i := 0; i < previewLines; i++ {
|
for i := range previewLines {
|
||||||
fmt.Println(lines[i])
|
fmt.Println(lines[i])
|
||||||
}
|
}
|
||||||
if len(lines) > previewLines {
|
if len(lines) > previewLines {
|
||||||
@ -169,7 +170,7 @@ func runLint(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply fixes
|
// Apply fixes
|
||||||
if err := os.WriteFile(absPath, []byte(fixedCode), 0644); err != nil {
|
if err := os.WriteFile(absPath, []byte(fixedCode), 0600); 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
|
||||||
|
|||||||
@ -14,17 +14,23 @@ var prDescribeCmd = &cobra.Command{
|
|||||||
Run: runPRDescribe,
|
Run: runPRDescribe,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPRDescribe(cmd *cobra.Command, args []string) {
|
func init() {
|
||||||
diff, err := gitRun([]string{"diff", "main..HEAD", "--no-color"})
|
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"})
|
||||||
if err != nil || diff == "" {
|
if err != nil || diff == "" {
|
||||||
diff, err = gitRun([]string{"diff", "origin/main..HEAD", "--no-color"})
|
diff, err = gitRun([]string{"diff", fmt.Sprintf("origin/%s..HEAD", base), "--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 main/origin/main.")
|
color.Yellow("No changes on this branch compared to %s/origin/%s.", base, base)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
modelFlag, _ := cmd.Flags().GetString("model")
|
modelFlag, _ := cmd.Flags().GetString("model")
|
||||||
|
|||||||
53
cmd/query.go
Normal file
53
cmd/query.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ var reviewCmd = &cobra.Command{
|
|||||||
Run: runReview,
|
Run: runReview,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runReview(cmd *cobra.Command, args []string) {
|
func runReview(cmd *cobra.Command, _ []string) {
|
||||||
modelFlag, _ := cmd.Flags().GetString("model")
|
modelFlag, _ := cmd.Flags().GetString("model")
|
||||||
model := config.GetModel("review", modelFlag)
|
model := config.GetModel("review", modelFlag)
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,7 @@ 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)")
|
||||||
|
|||||||
@ -47,10 +47,11 @@ func withMockGit(fn func([]string) (string, error)) func() {
|
|||||||
return func() { gitRun = orig }
|
return func() { gitRun = orig }
|
||||||
}
|
}
|
||||||
|
|
||||||
// testCmd returns a minimal cobra command with the model flag registered.
|
// testCmd returns a minimal cobra command with common flags 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,22 +309,62 @@ func TestRunPRDescribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("second diff error — skips AI", func(t *testing.T) {
|
t.Run("uses custom base branch", func(t *testing.T) {
|
||||||
mock := &mockStreamer{}
|
mock := &mockStreamer{response: "PR description"}
|
||||||
defer withMockClient(mock)()
|
defer withMockClient(mock)()
|
||||||
callCount := 0
|
var capturedArgs []string
|
||||||
defer withMockGit(func(args []string) (string, error) {
|
defer withMockGit(func(args []string) (string, error) {
|
||||||
callCount++
|
capturedArgs = args
|
||||||
if callCount == 2 {
|
return "diff content", nil
|
||||||
return "", errors.New("no remote")
|
})()
|
||||||
|
|
||||||
|
cmd := testCmd()
|
||||||
|
if err := cmd.Flags().Set("base", "develop"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return "", nil
|
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
|
||||||
})()
|
})()
|
||||||
|
|
||||||
runPRDescribe(testCmd(), nil)
|
runPRDescribe(testCmd(), nil)
|
||||||
|
|
||||||
if mock.calls != 0 {
|
if mock.calls != 1 {
|
||||||
t.Errorf("expected 0 AI calls, got %d", mock.calls)
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
239
cmd/scaffold.go
239
cmd/scaffold.go
@ -1,90 +1,187 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gmgauthier.com/grokkit/config"
|
"gmgauthier.com/grokkit/config"
|
||||||
"gmgauthier.com/grokkit/internal/grok"
|
"gmgauthier.com/grokkit/internal/grok"
|
||||||
|
"gmgauthier.com/grokkit/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var scaffoldCmd = &cobra.Command{
|
var scaffoldCmd = &cobra.Command{
|
||||||
Use: "scaffold [path]",
|
Use: "scaffold FILE DESCRIPTION",
|
||||||
Short: "Scaffold a new file with Grok (safe preview + confirmation)",
|
Short: "Scaffold a new file with Grok (safe preview + confirmation)",
|
||||||
Long: `Generate a new file from a description. Shows preview and requires explicit confirmation before writing.`,
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runScaffold,
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
scaffoldCmd.Flags().String("description", "", "Scaffold description (used internally by agent mode)")
|
scaffoldCmd.Flags().Bool("with-tests", false, "Also scaffold a basic _test.go file")
|
||||||
rootCmd.AddCommand(scaffoldCmd)
|
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")
|
||||||
func runScaffold(cmd *cobra.Command, args []string) {
|
scaffoldCmd.Flags().String("lang", "", "Force language for prompt (Go, Python, etc.)")
|
||||||
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),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -94,11 +95,12 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
|
|||||||
// Handle existing test file
|
// Handle existing test file
|
||||||
testExists := true
|
testExists := true
|
||||||
testInfo, err := os.Stat(testPath)
|
testInfo, err := os.Stat(testPath)
|
||||||
if os.IsNotExist(err) {
|
switch {
|
||||||
|
case errors.Is(err, os.ErrNotExist):
|
||||||
testExists = false
|
testExists = false
|
||||||
} else if err != nil {
|
case err != nil:
|
||||||
return fmt.Errorf("stat test file: %w", err)
|
return fmt.Errorf("stat test file: %w", err)
|
||||||
} else if testInfo.IsDir() {
|
case testInfo.IsDir():
|
||||||
return fmt.Errorf("test path is dir: %s", testPath)
|
return fmt.Errorf("test path is dir: %s", testPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +160,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply
|
// Apply
|
||||||
if err := os.WriteFile(testPath, []byte(newTestCode), 0644); err != nil {
|
if err := os.WriteFile(testPath, []byte(newTestCode), 0600); err != nil {
|
||||||
return fmt.Errorf("write test file: %w", err)
|
return fmt.Errorf("write test file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +171,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")
|
||||||
var cleanedLines []string
|
cleanedLines := make([]string, 0, len(lines))
|
||||||
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") {
|
||||||
|
|||||||
@ -93,6 +93,7 @@ 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)
|
||||||
@ -117,6 +118,7 @@ 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)
|
||||||
@ -144,6 +146,7 @@ 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)
|
||||||
@ -167,6 +170,7 @@ 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)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ 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 {
|
||||||
@ -36,12 +37,13 @@ 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.chat.model", "grok-4-1")
|
viper.SetDefault("commands.query.model", "grok-4-1-fast-non-reasoning")
|
||||||
viper.SetDefault("commands.chat-agent.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 != "" {
|
||||||
@ -49,24 +51,28 @@ func GetModel(commandName string, flagModel string) string {
|
|||||||
}
|
}
|
||||||
return flagModel
|
return flagModel
|
||||||
}
|
}
|
||||||
if cmdModel := viper.GetString("commands." + commandName + ".model"); cmdModel != "" {
|
cmdModel := viper.GetString("commands." + commandName + ".model")
|
||||||
|
if cmdModel != "" {
|
||||||
return cmdModel
|
return cmdModel
|
||||||
}
|
}
|
||||||
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 {
|
||||||
return 60
|
return 60 // Default 60 seconds
|
||||||
}
|
}
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 gitErr.Unwrap() != baseErr {
|
if !errors.Is(gitErr, baseErr) {
|
||||||
t.Errorf("GitError.Unwrap() did not return base error")
|
t.Errorf("GitError did not wrap 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 fileErr.Unwrap() != baseErr {
|
if !errors.Is(fileErr, baseErr) {
|
||||||
t.Errorf("FileError.Unwrap() did not return base error")
|
t.Errorf("FileError did not wrap base error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ func TestAPIErrorUnwrap(t *testing.T) {
|
|||||||
Message: "internal error",
|
Message: "internal error",
|
||||||
Err: baseErr,
|
Err: baseErr,
|
||||||
}
|
}
|
||||||
if unwrap := apiErr.Unwrap(); unwrap != baseErr {
|
if !errors.Is(apiErr, baseErr) {
|
||||||
t.Errorf("APIError.Unwrap() = %v, want %v", unwrap, baseErr)
|
t.Errorf("APIError.Unwrap() = %v, want %v", apiErr.Unwrap(), baseErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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",
|
||||||
@ -30,6 +31,7 @@ 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)
|
||||||
|
|||||||
@ -76,20 +76,23 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||||
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)
|
||||||
@ -104,6 +107,7 @@ 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() }()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package linter
|
package linter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -214,7 +215,7 @@ func FindAvailableLinter(lang *Language) (*Linter, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build install instructions
|
// Build install instructions
|
||||||
var installOptions []string
|
installOptions := make([]string, 0, len(lang.Linters))
|
||||||
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))
|
||||||
@ -232,10 +233,12 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) {
|
|||||||
"command", linter.Command)
|
"command", linter.Command)
|
||||||
|
|
||||||
// Build command arguments
|
// Build command arguments
|
||||||
args := append(linter.Args, filePath)
|
linterArgs := append([]string{}, linter.Args...)
|
||||||
|
linterArgs = append(linterArgs, filePath)
|
||||||
|
|
||||||
// Execute linter
|
// Execute linter
|
||||||
cmd := exec.Command(linter.Command, args...)
|
// nolint:gosec // intentional subprocess for linter
|
||||||
|
cmd := exec.Command(linter.Command, linterArgs...)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
result := &LintResult{
|
result := &LintResult{
|
||||||
@ -245,7 +248,8 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
var exitErr *exec.ExitError
|
||||||
|
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",
|
||||||
@ -302,7 +306,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 {
|
||||||
var langs []string
|
langs := make([]string, 0, len(languages))
|
||||||
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, ", ")))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -151,7 +151,7 @@ func main() {
|
|||||||
fmt.Println("Hello, World!")
|
fmt.Println("Hello, World!")
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte(validCode), 0600); 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), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte(validCode), 0600); 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"), 0644); err != nil {
|
if err := os.WriteFile(testFile, []byte("hello"), 0600); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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, 0755); err != nil {
|
if err := os.MkdirAll(logDir, 0750); 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, 0644)
|
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ 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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,21 +5,22 @@ This document provides a table of contents for all tasks and features currently
|
|||||||
## Queued
|
## Queued
|
||||||
|
|
||||||
* [1] [interactive-agent.md](./queued/interactive-agent.md) : Grokkit Interactive Agent
|
* [1] [interactive-agent.md](./queued/interactive-agent.md) : Grokkit Interactive Agent
|
||||||
* [2] [non-interactive-query.md](./queued/non-interactive-query.md) : grokkit query (non-interactive one-shot questions)
|
* [2] [rg.md](./queued/rg.md) : grokkit agent ripgrep (rg) integration
|
||||||
* [3] [rg.md](./queued/rg.md) : grokkit agent ripgrep (rg) integration
|
* [3] [gotools.md](./queued/gotools.md) : grokkit agent Go tools integration
|
||||||
* [4] [gotools.md](./queued/gotools.md) : grokkit agent Go tools integration
|
* [4] [make.md](./queued/make.md) : grokkit agent make integration
|
||||||
* [5] [make.md](./queued/make.md) : grokkit agent make integration
|
* [5] [tea.md](./queued/tea.md) : grokkit agent tea (Gitea CLI) integration
|
||||||
* [6] [tea.md](./queued/tea.md) : grokkit agent tea (Gitea CLI) integration
|
* [6] [cnotes.md](./queued/cnotes.md) : grokkit agent cnotes integration
|
||||||
* [7] [cnotes.md](./queued/cnotes.md) : grokkit agent cnotes integration
|
* [7] [profile.md](./queued/profile.md) : grokkit profile
|
||||||
* [8] [profile.md](./queued/profile.md) : grokkit profile
|
* [8] [pprof.md](./queued/pprof.md) : grokkit agent pprof integration
|
||||||
* [9] [pprof.md](./queued/pprof.md) : grokkit agent pprof integration
|
* [9] [audit.md](./queued/audit.md) : grokkit audit
|
||||||
* [10] [audit.md](./queued/audit.md) : grokkit audit
|
* [10] [git-chglog.md](./queued/git-chglog.md) : grokkit agent git-chglog integration
|
||||||
* [11] [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)
|
||||||
* [12] [TODO_ITEM.md](./queued/TODO_ITEM.md) : TODO ITEM template
|
* [99] [TODO_ITEM.md](./queued/TODO_ITEM.md) : TODO ITEM template
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
* [changelog.md](./queued/changelog.md) : grokkit changelog *(done — v0.1.8+)*
|
* [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
|
* [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
|
* [MODEL_ENFORCEMENT.md](./completed/MODEL_ENFORCEMENT.md) : Model Enforcement
|
||||||
* [scaffold.md](./completed/scaffold.md) : grokkit scaffold
|
* [scaffold.md](./completed/scaffold.md) : grokkit scaffold
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# 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
|
||||||
|
|||||||
19
todo/queued/admin.md
Normal file
19
todo/queued/admin.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# `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.
|
||||||
@ -1,5 +1,4 @@
|
|||||||
# `grokkit audit`
|
# `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.
|
**Description**: Comprehensive AI-powered code audit for security, performance, best practices, and potential bugs across single files or entire projects.
|
||||||
|
|
||||||
**Benefits**:
|
**Benefits**:
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `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").
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `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.
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `grokkit agent` Go tools integration
|
# `grokkit agent` Go tools integration
|
||||||
## Priority: 5 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.
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# Grokkit Interactive Agent
|
# Grokkit Interactive Agent
|
||||||
|
|
||||||
**Priority:** 2 of 12 (right after testgen)
|
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `grokkit agent` make integration
|
# `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").
|
**Description**: Wrappers for Makefile targets (test/lint/build/cover). Enables Grok to run/verify builds mid-agent workflow (e.g., "edit, test, fix loops").
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `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.
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `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**:
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `grokkit agent` ripgrep (rg) integration
|
# `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.
|
**Description**: Fast search wrapper for ripgrep (`rg`). Enables agent to grep project for symbols/patterns/context before edits.
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# `grokkit agent` tea integration
|
# `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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user