diff --git a/.grokkit/prompts/go.md b/.grokkit/prompts/go.md new file mode 100644 index 0000000..b1d17eb --- /dev/null +++ b/.grokkit/prompts/go.md @@ -0,0 +1,29 @@ +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 headings and bullet points): + +# Project Analysis: [Inferred Project Name] + +## Tech Stack & Layout +- Language/version, build system, key dependencies and why they were chosen +- High-level directory structure + +## Module & Package Relationships +- How packages depend on each other +- Main public APIs and their purpose + +## Function & Method Reference +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) + +## Object & Data Flow +- Important structs/types and their relationships +- Any database/ORM mappings or persistence patterns (if present) + +## Learning Path & Gotchas +- 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. Infer the project name from directory or go.mod if possible. 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 new file mode 100644 index 0000000..92c54d7 --- /dev/null +++ b/cmd/analyze.go @@ -0,0 +1,198 @@ +package cmd + +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" // note: we use package-level funcs + "gmgauthier.com/grokkit/internal/prompts" +) + +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" + } + // Fixed: config.GetModel takes (commandName, flagModel) + model := config.GetModel("analyze", viper.GetString("model")) + yes := viper.GetBool("yes") + + // 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.") + } + + // 1. Discover source files + files, err := discoverSourceFiles(dir) + if err != nil { + logger.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 + 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) + } + 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) + + // 5. Call Grok — use the exact working pattern you provided + messages := []map[string]string{ + {"role": "system", "content": promptContent}, + {"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) + // For a long report, StreamSilent is usually better (no live printing) + report := grok.NewClient().StreamSilent(messages, model) + + // 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 + _, err := fmt.Scanln(&confirm) + if err != nil { + return + } + 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 { + logger.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") + + rootCmd.AddCommand(analyzeCmd) +} + +// 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 + } + + // 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 (unchanged) +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 — 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 discovered: %d\n\n", len(files))) + + 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 { + sb.WriteString(" - " + rel + "\n") + } + if sb.Len() > 2500 { + break + } + } + + // Git remotes + if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" { + sb.WriteString("\nGit Remotes:\n" + out + "\n") + } + + 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/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 diff --git a/internal/linter/language.go b/internal/linter/language.go new file mode 100644 index 0000000..39396e5 --- /dev/null +++ b/internal/linter/language.go @@ -0,0 +1,51 @@ +package linter + +import "strings" + +// 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 { + 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 most common language + var bestLang string + maxCount := -1 + for lang, count := range counts { + if count > maxCount { + maxCount = count + bestLang = lang + } + } + + // Bias toward Go in mixed repos + if counts["go"] > 0 && counts["go"] >= maxCount/2 { + return "go" + } + + return bestLang +} + +// SupportedLanguages returns all known languages (for future use or error messages). +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 new file mode 100644 index 0000000..1bdb2f1 --- /dev/null +++ b/internal/prompts/analyze.go @@ -0,0 +1,40 @@ +package prompts + +import ( + "os" + "path/filepath" + "strings" +) + +// 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" { + 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 + } + + // 2. Global fallback + home := os.Getenv("HOME") + if home == "" { + home = os.Getenv("USERPROFILE") // Windows + } + 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 +} 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