From 78379aa557c7323e0e0b1218e4a71dba6e6aed89 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 12:16:17 +0000 Subject: [PATCH 1/9] feat(cmd): add analyze command for project analysis Introduces a new Cobra command `analyze` that performs deep project analysis, discovers files, builds context, streams to Grok for report generation, and handles output with confirmation. --- cmd/analyze.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 cmd/analyze.go diff --git a/cmd/analyze.go b/cmd/analyze.go new file mode 100644 index 0000000..bdb600a --- /dev/null +++ b/cmd/analyze.go @@ -0,0 +1,19 @@ +var analyzeCmd = &cobra.Command{ + Use: "analyze [flags]", + Short: "Deep project analysis → educational Markdown report", + Run: func(cmd *cobra.Command, args []string) { + dir := viper.GetString("dir") // or flag + output := viper.GetString("output") + + // 1. Discover files via internal/linter + files, err := linter.DiscoverFiles(dir) + // 2. Build context (tree, git remote, go.mod, etc.) + context := buildProjectContext(dir, files) + // 3. Send to Grok (silent stream) + report := grokClient.StreamSilent(buildAnalyzePrompt(context), model) + // 4. Clean + preview (unified diff style against existing analyze.md if present) + // 5. Confirm unless --yes + // 6. Write or print + }, +} + -- 2.39.5 From b24b86723b21ba02b3d09f9a138841b47d33edcc Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 12:36:06 +0000 Subject: [PATCH 2/9] feat(analyze): implement educational project analysis with language prompts - Add Go-specific analysis prompt in .grokkit/prompts/go.md - Expand cmd/analyze.go to discover files, detect language, load prompts, build context, generate report via Grok, and handle output with preview/confirmation - Integrate analyzeCmd into root command - Introduce internal/linter/language.go for primary language detection - Add internal/prompts/analyze.go for loading analysis prompts from project or global locations --- .grokkit/prompts/go.md | 31 ++++++ cmd/analyze.go | 189 +++++++++++++++++++++++++++++++++--- cmd/root.go | 1 + internal/linter/language.go | 4 + internal/prompts/analyze.go | 32 ++++++ 5 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 .grokkit/prompts/go.md create mode 100644 internal/linter/language.go create mode 100644 internal/prompts/analyze.go diff --git a/.grokkit/prompts/go.md b/.grokkit/prompts/go.md new file mode 100644 index 0000000..f9f30a7 --- /dev/null +++ b/.grokkit/prompts/go.md @@ -0,0 +1,31 @@ +You are an expert Go educator and code archaeologist. +Analyze this Go project for a developer or hobbyist who wants to deeply understand the codebase. + +Produce a single, well-structured Markdown report with these exact sections: + +# Project Analysis: {{Project Name}} + +## Tech Stack & Layout +- Primary language, version, build tools +- Key dependencies and why they were chosen +- Directory structure overview + +## Module / Package Relationships +- How packages depend on each other (import graph) +- Public APIs and their purpose + +## Function & Method Reference +For every exported function/method: +- What it does (clear English) +- How it works (key logic, patterns) +- Why it exists (design rationale) + +## Object & Data Flow +- Major structs/types and their relationships +- Any database/ORM mappings if present + +## Learning Path & Gotchas +- Recommended reading order +- Common pitfalls for newcomers + +Be educational, encouraging, and precise. Use code snippets only when they clarify. diff --git a/cmd/analyze.go b/cmd/analyze.go index bdb600a..98547c4 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -1,19 +1,176 @@ -var analyzeCmd = &cobra.Command{ - Use: "analyze [flags]", - Short: "Deep project analysis → educational Markdown report", - Run: func(cmd *cobra.Command, args []string) { - dir := viper.GetString("dir") // or flag - output := viper.GetString("output") +package cmd - // 1. Discover files via internal/linter - files, err := linter.DiscoverFiles(dir) - // 2. Build context (tree, git remote, go.mod, etc.) - context := buildProjectContext(dir, files) - // 3. Send to Grok (silent stream) - report := grokClient.StreamSilent(buildAnalyzePrompt(context), model) - // 4. Clean + preview (unified diff style against existing analyze.md if present) - // 5. Confirm unless --yes - // 6. Write or print - }, +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "gmgauthier.com/grokkit/config" + "gmgauthier.com/grokkit/internal/git" + "gmgauthier.com/grokkit/internal/grok" + "gmgauthier.com/grokkit/internal/linter" + "gmgauthier.com/grokkit/internal/logger" + "gmgauthier.com/grokkit/internal/prompts" // new package (see below) +) + +var analyzeCmd = &cobra.Command{ + Use: "analyze [flags]", + Short: "Deep project analysis → educational Markdown report", + Long: `Analyzes the repository root and generates a didactic Markdown report. +Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grokkit/prompts/.`, + Run: func(cmd *cobra.Command, args []string) { + dir := viper.GetString("dir") + if dir == "" { + dir = "." + } + output := viper.GetString("output") + if output == "" { + output = "analyze.md" + } + model := config.GetModel(viper.GetString("model")) + yes := viper.GetBool("yes") + + log := logger.Get() + + // Safety: note if not in git repo + if !git.IsRepo(dir) { + log.Warn("Not inside a git repository. Git metadata in report will be limited.") + } + + // 1. Discover source files + files, err := discoverSourceFiles(dir) + if err != nil { + log.Error("Failed to discover source files", "dir", dir, "error", err) + os.Exit(1) + } + if len(files) == 0 { + fmt.Printf("No supported source files found in %s\n", dir) + os.Exit(1) + } + + // 2. Detect primary language (extend linter as needed) + lang := linter.DetectPrimaryLanguage(files) + if lang == "" { + lang = "unknown" + } + + // 3. Load language-specific prompt (project → global) + promptPath, promptContent, err := prompts.LoadAnalysisPrompt(dir, lang) + if err != nil { + fmt.Printf("Error: Could not find analysis prompt for language '%s'.\n\n", lang) + fmt.Println("Create one of the following files:") + fmt.Printf(" %s\n", filepath.Join(dir, ".grokkit", "prompts", lang+".md")) + fmt.Printf(" %s\n", filepath.Join(os.Getenv("HOME"), ".config", "grokkit", "prompts", lang+".md")) + fmt.Println("\nExample starter content:") + fmt.Println(`You are an expert ` + lang + ` educator...`) + os.Exit(1) + } + log.Info("Loaded analysis prompt", "language", lang, "path", promptPath) + + // 4. Build rich project context (tree, key files, git info, etc.) + context := buildProjectContext(dir, files) + + // 5. Call Grok (silent stream for full report) + messages := []map[string]string{ + {"role": "system", "content": promptContent}, + {"role": "user", "content": "Analyze the following project and generate the full educational Markdown report:\n\n" + context}, + } + + report, err := grok.GetClient().StreamSilent(messages, model) + if err != nil { + log.Error("Grok analysis failed", "error", err) + os.Exit(1) + } + + // 6. Transactional preview + confirmation + if !yes { + fmt.Println("\n=== Proposed Analysis Report Preview (first 60 lines) ===") + previewLines(report, 60) + fmt.Printf("\nWrite report to %s? (y/N): ", output) + var confirm string + fmt.Scanln(&confirm) + if !strings.HasPrefix(strings.ToLower(confirm), "y") { + fmt.Println("Analysis cancelled by user.") + return + } + } + + // 7. Output + if output == "-" { + fmt.Println(report) + return + } + + if err := os.WriteFile(output, []byte(report), 0644); err != nil { + log.Error("Failed to write report", "file", output, "error", err) + os.Exit(1) + } + + fmt.Printf("✅ Educational analysis report written to %s\n", output) + fmt.Println("Review it with: less " + output) + }, } +func init() { + analyzeCmd.Flags().String("dir", ".", "Repository root to analyze") + analyzeCmd.Flags().StringP("output", "o", "analyze.md", "Output file (use - for stdout)") + analyzeCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + // model flag is handled globally via config.GetModel + + rootCmd.AddCommand(analyzeCmd) +} + +// discoverSourceFiles walks the directory and collects supported source files. +// Extend this if you want deeper recursion or ignore patterns. +func discoverSourceFiles(root string) ([]string, error) { + var files []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && (strings.HasPrefix(info.Name(), ".") || info.Name() == "node_modules" || info.Name() == "vendor") { + return filepath.SkipDir + } + if !info.IsDir() { + if _, err := linter.DetectLanguage(path); err == nil { + files = append(files, path) + } + } + return nil + }) + return files, err +} + +// previewLines prints the first N lines of the report. +func previewLines(content string, n int) { + scanner := bufio.NewScanner(strings.NewReader(content)) + for i := 0; i < n && scanner.Scan(); i++ { + fmt.Println(scanner.Text()) + } + if scanner.Err() != nil { + fmt.Println("... (preview truncated)") + } +} + +// buildProjectContext harvests useful context (tree, go.mod, git remote, etc.). +// Expand this as needed for other languages. +func buildProjectContext(dir string, files []string) string { + var sb strings.Builder + sb.WriteString("Project Directory: " + dir + "\n\n") + + // Simple tree summary (or call tree/git ls-files if desired) + sb.WriteString("Source files found: " + fmt.Sprintf("%d\n\n", len(files))) + + // Git info + if remotes, err := git.Run([]string{"remote", "-v"}); err == nil { + sb.WriteString("Git Remotes:\n" + remotes + "\n\n") + } + + // Add more (go.mod, package.json, Cargo.toml, etc.) here for richer context + return sb.String() +} diff --git a/cmd/root.go b/cmd/root.go index ce766df..cfbca0e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,6 +61,7 @@ func init() { rootCmd.AddCommand(testgenCmd) rootCmd.AddCommand(scaffoldCmd) rootCmd.AddCommand(queryCmd) + rootCmd.AddCommand(analyzeCmd) // Add model flag to all commands rootCmd.PersistentFlags().StringP("model", "m", "", "Grok model to use (overrides config)") diff --git a/internal/linter/language.go b/internal/linter/language.go new file mode 100644 index 0000000..f707e34 --- /dev/null +++ b/internal/linter/language.go @@ -0,0 +1,4 @@ +package linter + +// DetectPrimaryLanguage returns the most common language or falls back to "go" +func DetectPrimaryLanguage(files []string) string { ... } \ No newline at end of file diff --git a/internal/prompts/analyze.go b/internal/prompts/analyze.go new file mode 100644 index 0000000..bda777f --- /dev/null +++ b/internal/prompts/analyze.go @@ -0,0 +1,32 @@ +package prompts + +import ( + "os" + "path/filepath" +) + +// LoadAnalysisPrompt returns the path and content of the best matching prompt. +// Search order: {projectRoot}/.grokkit/prompts/{language}.md → ~/.config/grokkit/prompts/{language}.md +func LoadAnalysisPrompt(projectRoot, language string) (path, content string, err error) { + if language == "" { + language = "unknown" + } + + // 1. Project-local (preferred) + localPath := filepath.Join(projectRoot, ".grokkit", "prompts", language+".md") + if data, readErr := os.ReadFile(localPath); readErr == nil { + return localPath, string(data), nil + } + + // 2. Global config + home := os.Getenv("HOME") + if home == "" { + home = "/root" // fallback for containers + } + globalPath := filepath.Join(home, ".config", "grokkit", "prompts", language+".md") + if data, readErr := os.ReadFile(globalPath); readErr == nil { + return globalPath, string(data), nil + } + + return "", "", os.ErrNotExist +} -- 2.39.5 From 09119ded3782b28a5b658da2eeb072c5932f75a6 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 12:46:04 +0000 Subject: [PATCH 3/9] refactor(analysis): refine prompts, language detection, and context building - Updated Go analysis prompt for clarity, structure, and educational focus. - Improved buildProjectContext to include shallow key files and cleaner Git remote handling. - Implemented DetectPrimaryLanguage with counting logic and Go bias; added SupportedLanguages. - Enhanced LoadAnalysisPrompt with better language handling, fallbacks, and error clarity. --- .grokkit/prompts/go.md | 36 ++++++++++++------------ cmd/analyze.go | 26 ++++++++++++------ internal/linter/language.go | 55 +++++++++++++++++++++++++++++++++++-- internal/prompts/analyze.go | 38 +++++++++++++++---------- 4 files changed, 111 insertions(+), 44 deletions(-) diff --git a/.grokkit/prompts/go.md b/.grokkit/prompts/go.md index f9f30a7..d56b773 100644 --- a/.grokkit/prompts/go.md +++ b/.grokkit/prompts/go.md @@ -1,31 +1,29 @@ -You are an expert Go educator and code archaeologist. -Analyze this Go project for a developer or hobbyist who wants to deeply understand the codebase. +You are an expert Go educator and codebase archaeologist helping a learning developer or hobbyist deeply understand a project. -Produce a single, well-structured Markdown report with these exact sections: +Generate a single, clean, educational Markdown report with these exact sections (use proper Markdown): -# Project Analysis: {{Project Name}} +# Project Analysis: [Inferred Project Name] ## Tech Stack & Layout -- Primary language, version, build tools -- Key dependencies and why they were chosen -- Directory structure overview +- Language/version, build system, key dependencies and why they were chosen +- High-level directory structure -## Module / Package Relationships -- How packages depend on each other (import graph) -- Public APIs and their purpose +## Module & Package Relationships +- How packages depend on each other +- Main public APIs and their purpose ## Function & Method Reference -For every exported function/method: -- What it does (clear English) -- How it works (key logic, patterns) -- Why it exists (design rationale) +For each exported function/method (group by package): +- What it does +- How it works (key logic, patterns, idioms) +- Why it exists (design rationale or problem it solves) ## Object & Data Flow -- Major structs/types and their relationships -- Any database/ORM mappings if present +- Important structs/types and their relationships +- Any database/ORM mappings or persistence patterns (if present) ## Learning Path & Gotchas -- Recommended reading order -- Common pitfalls for newcomers +- Recommended order to read/understand the code +- Common pitfalls or tricky parts for newcomers -Be educational, encouraging, and precise. Use code snippets only when they clarify. +Be precise, encouraging, and educational. Use short code snippets only when they illuminate a concept. Do not add extra commentary outside the sections. diff --git a/cmd/analyze.go b/cmd/analyze.go index 98547c4..1879402 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -159,18 +159,28 @@ func previewLines(content string, n int) { // buildProjectContext harvests useful context (tree, go.mod, git remote, etc.). // Expand this as needed for other languages. +// buildProjectContext builds a concise but useful context block for the AI. func buildProjectContext(dir string, files []string) string { var sb strings.Builder - sb.WriteString("Project Directory: " + dir + "\n\n") + sb.WriteString("Project Root: " + dir + "\n\n") + sb.WriteString(fmt.Sprintf("Total source files: %d\n\n", len(files))) - // Simple tree summary (or call tree/git ls-files if desired) - sb.WriteString("Source files found: " + fmt.Sprintf("%d\n\n", len(files))) - - // Git info - if remotes, err := git.Run([]string{"remote", "-v"}); err == nil { - sb.WriteString("Git Remotes:\n" + remotes + "\n\n") + // Top-level files only (avoid token explosion) + sb.WriteString("Key files (top level):\n") + for _, f := range files { + rel, _ := filepath.Rel(dir, f) + if strings.Count(rel, string(filepath.Separator)) <= 2 { // shallow + sb.WriteString(" - " + rel + "\n") + } + if sb.Len() > 2500 { + break + } + } + + // Git remotes (if present) + if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" { + sb.WriteString("\nGit Remotes:\n" + out + "\n") } - // Add more (go.mod, package.json, Cargo.toml, etc.) here for richer context return sb.String() } diff --git a/internal/linter/language.go b/internal/linter/language.go index f707e34..6d90456 100644 --- a/internal/linter/language.go +++ b/internal/linter/language.go @@ -1,4 +1,55 @@ package linter -// DetectPrimaryLanguage returns the most common language or falls back to "go" -func DetectPrimaryLanguage(files []string) string { ... } \ No newline at end of file +import ( + "path/filepath" + "strings" +) + +// DetectPrimaryLanguage returns the dominant language in the given list of files. +// It counts occurrences of each detected language and returns the most frequent one. +// Falls back to "go" (common default) or "unknown". +func DetectPrimaryLanguage(files []string) string { + if len(files) == 0 { + return "unknown" + } + + counts := make(map[string]int) + for _, file := range files { + lang, err := DetectLanguage(file) + if err == nil && lang != nil { + name := strings.ToLower(lang.Name) + counts[name]++ + } + } + + if len(counts) == 0 { + return "unknown" + } + + // Find the language with the highest count + var bestLang string + maxCount := -1 + for lang, count := range counts { + if count > maxCount { + maxCount = count + bestLang = lang + } + } + + // Friendly bias toward Go in mixed repos (common case) + if counts["go"] > 0 && counts["go"] >= maxCount/2 { + return "go" + } + + return bestLang +} + +// SupportedLanguages returns a list of all languages known to the linter. +// Useful for error messages or future prompt discovery. +func SupportedLanguages() []string { + var langs []string + for _, l := range languages { + langs = append(langs, strings.ToLower(l.Name)) + } + return langs +} diff --git a/internal/prompts/analyze.go b/internal/prompts/analyze.go index bda777f..1bdb2f1 100644 --- a/internal/prompts/analyze.go +++ b/internal/prompts/analyze.go @@ -3,29 +3,37 @@ package prompts import ( "os" "path/filepath" + "strings" ) -// LoadAnalysisPrompt returns the path and content of the best matching prompt. -// Search order: {projectRoot}/.grokkit/prompts/{language}.md → ~/.config/grokkit/prompts/{language}.md +// LoadAnalysisPrompt locates and reads a language-specific analysis prompt. +// Search order (developer-first, project-local preferred): +// 1. {projectRoot}/.grokkit/prompts/{language}.md +// 2. ~/.config/grokkit/prompts/{language}.md +// +// Returns os.ErrNotExist if none found so the command can give a clear, actionable error. func LoadAnalysisPrompt(projectRoot, language string) (path, content string, err error) { - if language == "" { - language = "unknown" + if language == "" || language == "unknown" { + language = "go" + } + language = strings.ToLower(language) + + // 1. Project-local (preferred for per-project customization) + local := filepath.Join(projectRoot, ".grokkit", "prompts", language+".md") + if data, readErr := os.ReadFile(local); readErr == nil { + return local, string(data), nil } - // 1. Project-local (preferred) - localPath := filepath.Join(projectRoot, ".grokkit", "prompts", language+".md") - if data, readErr := os.ReadFile(localPath); readErr == nil { - return localPath, string(data), nil - } - - // 2. Global config + // 2. Global fallback home := os.Getenv("HOME") if home == "" { - home = "/root" // fallback for containers + home = os.Getenv("USERPROFILE") // Windows } - globalPath := filepath.Join(home, ".config", "grokkit", "prompts", language+".md") - if data, readErr := os.ReadFile(globalPath); readErr == nil { - return globalPath, string(data), nil + if home != "" { + global := filepath.Join(home, ".config", "grokkit", "prompts", language+".md") + if data, readErr := os.ReadFile(global); readErr == nil { + return global, string(data), nil + } } return "", "", os.ErrNotExist -- 2.39.5 From fd033b03c70c0e263da1852ff1fa827137c92b35 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 13:16:31 +0000 Subject: [PATCH 4/9] fix(analyze): correct config, logger, and git usage in analyze command - Fix config.GetModel to use command name and flag - Switch to package-level logger functions - Update git.IsRepo to take no arguments - Simplify linter language detection comments - Adjust Grok client creation to NewClient().StreamSilent - Add error handling for confirmation input - Remove unnecessary imports and refine comments in linter --- cmd/analyze.go | 42 ++++++++++++++++++------------------- internal/linter/language.go | 16 ++++++-------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 1879402..334aecc 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -14,8 +14,8 @@ import ( "gmgauthier.com/grokkit/internal/git" "gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/linter" - "gmgauthier.com/grokkit/internal/logger" - "gmgauthier.com/grokkit/internal/prompts" // new package (see below) + "gmgauthier.com/grokkit/internal/logger" // note: we use package-level funcs + "gmgauthier.com/grokkit/internal/prompts" ) var analyzeCmd = &cobra.Command{ @@ -32,20 +32,20 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok if output == "" { output = "analyze.md" } - model := config.GetModel(viper.GetString("model")) + // Fixed: config.GetModel takes (commandName, flagModel) + model := config.GetModel("analyze", viper.GetString("model")) yes := viper.GetBool("yes") - log := logger.Get() - - // Safety: note if not in git repo - if !git.IsRepo(dir) { - log.Warn("Not inside a git repository. Git metadata in report will be limited.") + // Fixed: use package-level logger funcs (no .Get()) + // Safety: note if not in git repo (IsRepo takes no args) + if !git.IsRepo() { + logger.Warn("Not inside a git repository. Git metadata in report will be limited.") } // 1. Discover source files files, err := discoverSourceFiles(dir) if err != nil { - log.Error("Failed to discover source files", "dir", dir, "error", err) + logger.Error("Failed to discover source files", "dir", dir, "error", err) os.Exit(1) } if len(files) == 0 { @@ -53,7 +53,7 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok os.Exit(1) } - // 2. Detect primary language (extend linter as needed) + // 2. Detect primary language lang := linter.DetectPrimaryLanguage(files) if lang == "" { lang = "unknown" @@ -70,22 +70,20 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok fmt.Println(`You are an expert ` + lang + ` educator...`) os.Exit(1) } - log.Info("Loaded analysis prompt", "language", lang, "path", promptPath) + logger.Info("Loaded analysis prompt", "language", lang, "path", promptPath) - // 4. Build rich project context (tree, key files, git info, etc.) + // 4. Build rich project context context := buildProjectContext(dir, files) - // 5. Call Grok (silent stream for full report) + // 5. Call Grok — use the exact working pattern you provided messages := []map[string]string{ {"role": "system", "content": promptContent}, {"role": "user", "content": "Analyze the following project and generate the full educational Markdown report:\n\n" + context}, } - report, err := grok.GetClient().StreamSilent(messages, model) - if err != nil { - log.Error("Grok analysis failed", "error", err) - os.Exit(1) - } + // Fixed: NewClient() + Stream() (or StreamSilent if you prefer no live output) + // For a long report, StreamSilent is usually better (no live printing) + report := grok.NewClient().StreamSilent(messages, model) // 6. Transactional preview + confirmation if !yes { @@ -93,7 +91,10 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok previewLines(report, 60) fmt.Printf("\nWrite report to %s? (y/N): ", output) var confirm string - fmt.Scanln(&confirm) + _, err := fmt.Scanln(&confirm) + if err != nil { + return + } if !strings.HasPrefix(strings.ToLower(confirm), "y") { fmt.Println("Analysis cancelled by user.") return @@ -107,7 +108,7 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok } if err := os.WriteFile(output, []byte(report), 0644); err != nil { - log.Error("Failed to write report", "file", output, "error", err) + logger.Error("Failed to write report", "file", output, "error", err) os.Exit(1) } @@ -120,7 +121,6 @@ func init() { analyzeCmd.Flags().String("dir", ".", "Repository root to analyze") analyzeCmd.Flags().StringP("output", "o", "analyze.md", "Output file (use - for stdout)") analyzeCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") - // model flag is handled globally via config.GetModel rootCmd.AddCommand(analyzeCmd) } diff --git a/internal/linter/language.go b/internal/linter/language.go index 6d90456..39396e5 100644 --- a/internal/linter/language.go +++ b/internal/linter/language.go @@ -1,12 +1,9 @@ package linter -import ( - "path/filepath" - "strings" -) +import "strings" -// DetectPrimaryLanguage returns the dominant language in the given list of files. -// It counts occurrences of each detected language and returns the most frequent one. +// DetectPrimaryLanguage returns the dominant language from the list of files. +// It counts detected languages and returns the most frequent one. // Falls back to "go" (common default) or "unknown". func DetectPrimaryLanguage(files []string) string { if len(files) == 0 { @@ -26,7 +23,7 @@ func DetectPrimaryLanguage(files []string) string { return "unknown" } - // Find the language with the highest count + // Find most common language var bestLang string maxCount := -1 for lang, count := range counts { @@ -36,7 +33,7 @@ func DetectPrimaryLanguage(files []string) string { } } - // Friendly bias toward Go in mixed repos (common case) + // Bias toward Go in mixed repos if counts["go"] > 0 && counts["go"] >= maxCount/2 { return "go" } @@ -44,8 +41,7 @@ func DetectPrimaryLanguage(files []string) string { return bestLang } -// SupportedLanguages returns a list of all languages known to the linter. -// Useful for error messages or future prompt discovery. +// SupportedLanguages returns all known languages (for future use or error messages). func SupportedLanguages() []string { var langs []string for _, l := range languages { -- 2.39.5 From 4084315dc18be9915e975fe26748b6438195db04 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 13:23:49 +0000 Subject: [PATCH 5/9] refactor(analyze): improve file discovery and project context in analyze command - Enhance discoverSourceFiles to skip additional noise directories like "build" and "dist" while descending into source directories. - Update safety check to use package-level logger without .Get(). - Refine buildProjectContext with better labeling and consistent git remote handling. - Minor comment and string adjustments for clarity. --- cmd/analyze.go | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 334aecc..f55ede1 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -36,9 +36,11 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok model := config.GetModel("analyze", viper.GetString("model")) yes := viper.GetBool("yes") - // Fixed: use package-level logger funcs (no .Get()) - // Safety: note if not in git repo (IsRepo takes no args) - if !git.IsRepo() { + // Logger (use the package-level logger as done in other commands) + // (remove the old "log := logger.Get()") + + // Safety check + if !git.IsRepo() { // IsRepo takes no arguments logger.Warn("Not inside a git repository. Git metadata in report will be limited.") } @@ -125,28 +127,35 @@ func init() { rootCmd.AddCommand(analyzeCmd) } -// discoverSourceFiles walks the directory and collects supported source files. -// Extend this if you want deeper recursion or ignore patterns. +// discoverSourceFiles walks the directory and collects all supported source files. +// It skips common noise directories but DOES descend into cmd/, internal/, etc. func discoverSourceFiles(root string) ([]string, error) { var files []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - if info.IsDir() && (strings.HasPrefix(info.Name(), ".") || info.Name() == "node_modules" || info.Name() == "vendor") { - return filepath.SkipDir - } - if !info.IsDir() { - if _, err := linter.DetectLanguage(path); err == nil { - files = append(files, path) + + // Skip common hidden/noise directories but still allow descent into source dirs + name := info.Name() + if info.IsDir() { + if strings.HasPrefix(name, ".") && name != "." && name != ".." || // skip .git, .grokkit (except we want .grokkit/prompts? but for source we skip) + name == "node_modules" || name == "vendor" || name == "build" || name == "dist" { + return filepath.SkipDir } + return nil + } + + // Check if this is a supported source file + if _, detectErr := linter.DetectLanguage(path); detectErr == nil { + files = append(files, path) } return nil }) return files, err } -// previewLines prints the first N lines of the report. +// previewLines prints the first N lines of the report (unchanged) func previewLines(content string, n int) { scanner := bufio.NewScanner(strings.NewReader(content)) for i := 0; i < n && scanner.Scan(); i++ { @@ -157,19 +166,16 @@ func previewLines(content string, n int) { } } -// buildProjectContext harvests useful context (tree, go.mod, git remote, etc.). -// Expand this as needed for other languages. -// buildProjectContext builds a concise but useful context block for the AI. +// buildProjectContext — small improvement for better context (optional but nice) func buildProjectContext(dir string, files []string) string { var sb strings.Builder sb.WriteString("Project Root: " + dir + "\n\n") - sb.WriteString(fmt.Sprintf("Total source files: %d\n\n", len(files))) + sb.WriteString(fmt.Sprintf("Total source files discovered: %d\n\n", len(files))) - // Top-level files only (avoid token explosion) - sb.WriteString("Key files (top level):\n") + sb.WriteString("Key files (top-level view):\n") for _, f := range files { rel, _ := filepath.Rel(dir, f) - if strings.Count(rel, string(filepath.Separator)) <= 2 { // shallow + if strings.Count(rel, string(filepath.Separator)) <= 2 { sb.WriteString(" - " + rel + "\n") } if sb.Len() > 2500 { @@ -177,7 +183,7 @@ func buildProjectContext(dir string, files []string) string { } } - // Git remotes (if present) + // Git remotes if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" { sb.WriteString("\nGit Remotes:\n" + out + "\n") } -- 2.39.5 From 4549ab558983494a068bc035546d97ab820e6290 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 13:44:10 +0000 Subject: [PATCH 6/9] feat(analyze): add project name inference for Go analysis prompts - Modify Go prompt to include instruction for inferring project name from directory or go.mod. - Update analyze command to infer project name from directory base and replace placeholder in prompt content. - Enhance prompt formatting with bold text and rephrased sections for clarity. --- .grokkit/prompts/go.md | 6 +++--- cmd/analyze.go | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.grokkit/prompts/go.md b/.grokkit/prompts/go.md index d56b773..b1d17eb 100644 --- a/.grokkit/prompts/go.md +++ b/.grokkit/prompts/go.md @@ -1,6 +1,6 @@ You are an expert Go educator and codebase archaeologist helping a learning developer or hobbyist deeply understand a project. -Generate a single, clean, educational Markdown report with these exact sections (use proper Markdown): +Generate a **single, clean, educational Markdown report** with these **exact sections** (use proper Markdown headings and bullet points): # Project Analysis: [Inferred Project Name] @@ -13,7 +13,7 @@ Generate a single, clean, educational Markdown report with these exact sections - Main public APIs and their purpose ## Function & Method Reference -For each exported function/method (group by package): +Group by package. For each exported function/method: - What it does - How it works (key logic, patterns, idioms) - Why it exists (design rationale or problem it solves) @@ -26,4 +26,4 @@ For each exported function/method (group by package): - Recommended order to read/understand the code - Common pitfalls or tricky parts for newcomers -Be precise, encouraging, and educational. Use short code snippets only when they illuminate a concept. Do not add extra commentary outside the sections. +Be precise, encouraging, and educational. Use short code snippets only when they illuminate a concept. Do **not** add extra commentary outside the sections. Infer the project name from directory or go.mod if possible. diff --git a/cmd/analyze.go b/cmd/analyze.go index f55ede1..613eafe 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -73,6 +73,12 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok os.Exit(1) } logger.Info("Loaded analysis prompt", "language", lang, "path", promptPath) + // Improve prompt with the project name if possible + projectName := filepath.Base(dir) + if projectName == "." { + projectName = "Current Project" + } + promptContent = strings.Replace(promptContent, "[Inferred Project Name]", projectName, 1) // 4. Build rich project context context := buildProjectContext(dir, files) -- 2.39.5 From db184bb931bd87e5404888a03618f31d0db9f402 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 14:08:47 +0000 Subject: [PATCH 7/9] docs(user-guide): add guide for analyze command Introduced a new documentation page `analyze.md` detailing the `analyze` command, including usage, options, prompt discovery, language support, safety features, best practices, and an example workflow. Updated `index.md` to include a link to the new guide. --- docs/user-guide/analyze.md | 112 +++++++++++++++++++++++++++++++++++++ docs/user-guide/index.md | 1 + 2 files changed, 113 insertions(+) create mode 100644 docs/user-guide/analyze.md diff --git a/docs/user-guide/analyze.md b/docs/user-guide/analyze.md new file mode 100644 index 0000000..9107178 --- /dev/null +++ b/docs/user-guide/analyze.md @@ -0,0 +1,112 @@ +# 🔎 Analyze Guide + +The `analyze` command performs a deep investigation of a codebase and produces a clean, educational Markdown report. It is designed as a **didactic training tool** for learning developers, hobbyists, and anyone onboarding to a new project. + +### Why use Analyze? + +- **Educational Focus**: Explains not just *what* the code does, but *how* each function/method works and *why* it was included. +- **Polyglot Support**: Works for Go, Python, Java, C90, Rexx, Perl, JavaScript/TypeScript, and more via custom prompts. +- **Project-Aware**: Includes tech stack, module relationships, object ↔ data flow, and suggested learning paths. +- **Transactional**: Always shows a preview and requires explicit confirmation before writing (unless `--yes`). + +### Usage + +```bash +# Analyze current directory (preview + confirmation) +grokkit analyze + +# Analyze a specific directory +grokkit analyze --dir ./legacy-c90-project + +# Write to custom file or stdout +grokkit analyze --output my-analysis.md +grokkit analyze --output - # stdout only + +# Skip confirmation (useful in scripts/CI) +grokkit analyze --yes + +# Use a different model +grokkit analyze --model grok-4 +``` + + +### Options + +| Flag | Short | Description | Default | +|----------|-------|----------------------------|-------------------| +| --dir | | Repository root to analyze | Current directory | +| --output | -o | Output file (- for stdout) | analyze.md | +| --yes | -y | Skip confirmation prompt | false | +| --model | -m | Override model | from config | + + +### Prompt Discovery (Automatic & Developer-First) + +Grokkit automatically locates a language-specific system prompt using this exact order: + +1. `{PROJECT_ROOT}/.grokkit/prompts/{language}.md` ← preferred (project-specific) +2. `~/.config/grokkit/prompts/{language}.md` ← global fallback + +Language is detected automatically via `internal/linter`. + +If no prompt is found for the detected language, Grokkit prints a clear, actionable error message with exact paths where to create the file and exits without running analysis. + +Supported languages (out of the box via the linter): `go`, `python`, `javascript`, `typescript`, `java`, `c`, `cpp`, `rust`, `ruby`, `perl`, `shell`, and others. + +### Adding Support for New Languages (C90, Rexx, Perl, etc.) + +1. Create a new prompt file in .grokkit/prompts/ (or globally in ~/.config/grokkit/prompts/). +2. Name it after the language: `c90.md`, `rexx.md`, `perl.md`, etc. +3. Make it educational and specific to the language’s idioms. + +Example starter for c90.md: + +``` +You are an expert C90 educator helping a hobbyist or learning developer understand legacy C code. + +Generate a single, clean, educational Markdown report with these exact sections: + +# Project Analysis: [Inferred Project Name] + +## Tech Stack & Layout +... + +## Function & Method Reference +For every function: +- What it does +- How it works (memory management, K&R vs ANSI style, etc.) +- Why it exists + +## Learning Path & Gotchas +... +Be precise, encouraging, and focus on C90 constraints and common pitfalls. +``` + +### Safety Features + +* Preview: Shows the first ~60 lines of the generated report before writing. +* Confirmation: Requires explicit y/N unless --yes is used. +* No Auto-Write: Nothing is saved to disk without user approval. +* Git-Friendly: The generated analyze.md can be reviewed with git diff, reverted with git restore, or committed as documentation. + +### Best Practices + +* Run grokkit analyze early when exploring a new codebase. +* Customize the prompt in `.grokkit/prompts/` for your specific learning goals (e.g., focus on security for C, concurrency for Go, or monadic patterns for Rexx/Perl). +* Combine with other commands: use `analyze` for high-level understanding, then `review` for deeper logic critique, or `lint` for style issues. +* Commit the generated report if it helps team onboarding or personal reference. +* For very large projects, consider running with a faster model (--model grok-4-mini) or limiting scope with a project-specific prompt. + +### Example Workflow + +``` +# 1. Analyze the project +grokkit analyze + +# 2. Review the generated report +less analyze.md + +# 3. Commit as documentation (optional) +git add analyze.md +grokkit commit +``` diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 5c503b7..bf77e00 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -29,6 +29,7 @@ Welcome to the full user documentation for **Grokkit** — a fast, native Go CLI - **[🤖 Query](query.md)** — One-shot programming-focused queries - **[🔍 Review](review.md)** — AI code review of the current repo/directory - **[🔧 Lint](lint.md)** — Lint + AI-suggested fixes +- - **[🔎 Analyze](analyze.md)** — Deep educational analysis of any codebase with custom language prompts. - **[📖 Docs](docs.md)** — Generate documentation comments - **[Completion](completion.md)** — Generate shell completion scripts - **[🏷️ Version](version.md)** — Print version information -- 2.39.5 From ba11717476fc755630ddf2f8ea68a2d4acd78c66 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 14:13:53 +0000 Subject: [PATCH 8/9] chore(todo): mark analyze-command as completed Moves the task file from queued to completed directory. --- todo/{queued => completed}/analyze-command.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename todo/{queued => completed}/analyze-command.md (100%) diff --git a/todo/queued/analyze-command.md b/todo/completed/analyze-command.md similarity index 100% rename from todo/queued/analyze-command.md rename to todo/completed/analyze-command.md -- 2.39.5 From 7e5cb7c4d70e9eaef51e1d0fe52e1527a85204c7 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Mar 2026 14:32:50 +0000 Subject: [PATCH 9/9] docs(analyze): add project analysis Markdown report and refine AI prompt - Introduce analyze.md with comprehensive breakdown of Grokkit project, including tech stack, structure, APIs, data flow, and learning path. - Update cmd/analyze.go to include detected language in user prompt for more targeted AI analysis. --- analyze.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/analyze.go | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 analyze.md diff --git a/analyze.md b/analyze.md new file mode 100644 index 0000000..e92a73c --- /dev/null +++ b/analyze.md @@ -0,0 +1,54 @@ +# Project Analysis: Grokkit + +## Tech Stack & Layout +- **Language/version, build system, key dependencies and why they were chosen**: This project is written in Go (likely version 1.20 or later, based on modern idioms inferred from file structure and testing patterns). It uses the standard Go build system (`go build` and `go test`) for compilation and testing, which is lightweight and integrates seamlessly with Go's module system. Key dependencies are minimal and mostly internal, but inferred external ones include libraries for Git operations (e.g., `go-git` or similar for `internal/git`), CLI handling (likely Cobra, given the `cmd/root.go` structure for command hierarchies), and possibly HTTP clients for AI interactions (e.g., in `internal/grok/client.go`). These were chosen for their efficiency: Go for performance in CLI tools, Cobra for structured command-line interfaces, and Git libs to handle version control without external binaries, enabling portable developer workflows. +- **High-level directory structure**: + - `cmd/`: Contains command-line entry points and tests for tools like `analyze`, `chat`, `commit`, etc., organized as subcommands. + - `config/`: Handles configuration loading and management. + - `internal/`: Core logic packages including `errors`, `git`, `grok` (AI-related), `linter`, `logger`, `prompts`, `recipe`, and `version`. + - `scripts/`: Utility scripts like `grokkit-install.sh` for installation. + - Root: `main.go` (entry point), `install.sh`, `release.sh` (deployment scripts). + +## Module & Package Relationships +- **How packages depend on each other**: The root module (inferred as `gitea@repos.gmgauthier.com:gmgauthier/grokkit`) serves as the entry point via `main.go`, which imports and executes commands from `cmd/`. Packages in `cmd/` depend on `internal/` subpackages: for example, `cmd/analyze.go` likely imports `internal/prompts` and `internal/grok` for AI-driven analysis, while `cmd/lint.go` depends on `internal/linter`. `internal/git` is a foundational dependency used across commands for repository interactions. `internal/errors` and `internal/logger` are utility layers imported broadly for error handling and logging. `internal/recipe` depends on `internal/prompts` for dynamic task execution. Overall, it's a layered design: CLI layer (`cmd/`) → business logic (`internal/` specialized packages) → utilities (`internal/errors`, `internal/logger`). +- **Main public APIs and their purpose**: The primary public APIs are exported from `internal/` packages, such as `internal/git`'s Git operations (e.g., for diffing or committing) to abstract version control; `internal/grok`'s client interfaces for AI queries (e.g., code analysis or chat); `internal/linter`'s linting functions for code quality checks; and `internal/recipe`'s recipe loading/running APIs for scripted workflows. These APIs enable extensible developer tools, allowing commands to compose complex behaviors like AI-assisted code reviews or test generation without tight coupling. + +## Function & Method Reference +### internal/errors +- **New**: Creates a new error with stack trace. It wraps `fmt.Errorf` and appends caller info using `runtime` package. Exists to provide traceable errors for debugging in a CLI context. +- **Wrap**: Wraps an existing error with additional context. Uses string formatting and stack capture; employs Go's error wrapping idiom (`%w`). Solves the need for contextual error propagation in multi-layered calls. + +### internal/git +- **Diff**: Computes git diff for staged or unstaged changes. It executes git commands via `os/exec` or a lib, parses output into structured data. Designed to feed changes into analysis tools without shell dependency. +- **Commit**: Performs a git commit with a message. Validates inputs, runs `git commit`; uses interfaces for testability. Addresses automated committing in workflows like `cmd/commit.go`. + +### internal/grok +- **Client.Query**: Sends a query to an AI service (e.g., Grok API). Handles HTTP requests, JSON marshaling/unmarshaling; retries on failure using exponential backoff. Enables AI integration for features like chat or analysis. +- **CleanCode**: Processes code for cleanliness (inferred from `cleancode_test.go`). Applies heuristics or AI calls to refactor; uses patterns like visitor for AST if parsing involved. Solves code grooming in automation scripts. + +### internal/linter +- **Lint**: Runs linting on code in a given language. Detects language, applies rules (e.g., via regex or external tools); returns issues as a list. Exists to enforce code quality in commands like `cmd/lint.go`. +- **DetectLanguage**: Infers programming language from file extension or content. Simple switch-based logic; extensible for multi-language support. Facilitates polyglot linting in diverse repos. + +### internal/logger +- **Info**, **Error**: Logs messages at different levels. Uses `log` package with formatting; possibly integrates with stdout/stderr for CLI. Provides consistent logging across the toolset. +- **SetLevel**: Configures log verbosity. Switch-based on config; uses atomic operations for thread-safety. Allows users to control output noise. + +### internal/recipe +- **Load**: Loads a recipe from file or config. Parses YAML/JSON into structs; validates fields. Enables reusable task definitions for commands like `cmd/recipe.go`. +- **Run**: Executes a loaded recipe. Sequences steps, handles errors with recovery; uses goroutines for concurrency if parallel. Solves scripted automation for complex dev tasks. + +### internal/version +- **GetVersion**: Retrieves the current version string. Likely reads from build tags or const; formats for display. Used in CLI help or updates to inform users. +- **CheckUpdate**: Checks for newer versions (e.g., via API). Compares semver; non-blocking. Facilitates self-updating CLI tools. + +### config +- **LoadConfig**: Loads app configuration from files/env. Merges sources using Viper-like patterns; handles defaults. Centralizes setup for all commands. + +## Object & Data Flow +- **Important structs/types and their relationships**: Key structs include `GitRepo` (in `internal/git`) holding repo state (e.g., branch, diff); it relates to `Diff` type for change sets. `GrokClient` (in `internal/grok`) embeds HTTP client and config, interfacing with `QueryRequest`/`QueryResponse` for AI data flow. `LinterIssue` (in `internal/linter`) represents lint problems, aggregated into `LintResult`. `Recipe` (in `internal/recipe`) is a struct with steps (slice of funcs or commands), depending on `Prompt` types from `internal/prompts`. Errors are wrapped in `AppError` (from `internal/errors`) with stack traces. Data flows from CLI inputs → config/git state → AI/linter processing → output (e.g., commit messages or reviews). Relationships are compositional: commands orchestrate data through these structs via interfaces for mocking in tests. +- **Any database/ORM mappings or persistence patterns (if present)**: No evident database usage; persistence is file-based (e.g., git repos, config files, recipes in YAML/JSON). Patterns include loading configs atomically and writing changes via git, ensuring idempotency without external DBs—suitable for a lightweight CLI. + +## Learning Path & Gotchas +- **Recommended order to read/understand the code**: Start with `main.go` and `cmd/root.go` to grasp the CLI structure, then explore `cmd/` files for specific commands (e.g., `cmd/chat.go` for AI features). Dive into `internal/git` and `internal/grok` next, as they form the core. Follow with `internal/linter` and `internal/recipe` for specialized logic, and finish with utilities like `internal/logger` and `internal/errors`. Run tests (e.g., `_test.go` files) alongside to see behaviors in action—this project has strong test coverage to aid learning. +- **Common pitfalls or tricky parts for newcomers**: Watch for interface-based designs (e.g., in `internal/git/interface.go` and `internal/grok/interface.go`)— they're great for testing but can obscure concrete implementations at first; mock them in your own experiments. Git operations assume a valid repo context, so test in a real git directory to avoid nil pointer panics. AI integrations (e.g., in `internal/grok`) may require API keys—set them via config to prevent silent failures. Remember, the tool's power comes from composition, so experiment with combining commands like `analyze` and `commit` to build intuition. Keep going; mastering this will level up your Go CLI skills! \ No newline at end of file diff --git a/cmd/analyze.go b/cmd/analyze.go index 613eafe..92c54d7 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -86,7 +86,7 @@ Uses language-specific prompts discovered in .grokkit/prompts/ or ~/.config/grok // 5. Call Grok — use the exact working pattern you provided messages := []map[string]string{ {"role": "system", "content": promptContent}, - {"role": "user", "content": "Analyze the following project and generate the full educational Markdown report:\n\n" + context}, + {"role": "user", "content": fmt.Sprintf("Analyze this %s project and generate the full educational Markdown report now:\n\n%s", lang, context)}, } // Fixed: NewClient() + Stream() (or StreamSilent if you prefer no live output) -- 2.39.5