diff --git a/README.md b/README.md index 4e95c250..10f25402 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ - [Helper Apps](#helper-apps) - [`to_pdf`](#to_pdf) - [`to_pdf` Installation](#to_pdf-installation) + - [`code_helper`](#code_helper) - [pbpaste](#pbpaste) - [Web Interface](#web-interface) - [Installing](#installing) @@ -599,6 +600,20 @@ go install github.com/danielmiessler/fabric/plugins/tools/to_pdf@latest Make sure you have a LaTeX distribution (like TeX Live or MiKTeX) installed on your system, as `to_pdf` requires `pdflatex` to be available in your system's PATH. +### `code_helper` + +`code_helper` is used in conjunction with the `create_coding_feature` pattern. +It generates a `json` representation of a directory of code that can be fed into an AI model +with instructions to create a new feature or edit the code in a specified way. + +See [the Create Coding Feature Pattern README](./patterns/create_coding_feature/README.md) for details. + +Install it first using: + +```bash +go install github.com/danielmiessler/fabric/plugins/tools/code_helper@latest +``` + ## pbpaste The [examples](#examples) use the macOS program `pbpaste` to paste content from the clipboard to pipe into `fabric` as the input. `pbpaste` is not available on Windows or Linux, but there are alternatives. diff --git a/common/attachment.go b/common/attachment.go index f493f700..253e5f95 100644 --- a/common/attachment.go +++ b/common/attachment.go @@ -6,7 +6,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "os" "path/filepath" @@ -29,7 +29,7 @@ func (a *Attachment) GetId() (ret string, err error) { hash = fmt.Sprintf("%x", sha256.Sum256(a.Content)) } else if a.Path != nil { var content []byte - if content, err = ioutil.ReadFile(*a.Path); err != nil { + if content, err = os.ReadFile(*a.Path); err != nil { return } hash = fmt.Sprintf("%x", sha256.Sum256(content)) @@ -83,7 +83,7 @@ func (a *Attachment) ContentBytes() (ret []byte, err error) { return } if a.Path != nil { - if ret, err = ioutil.ReadFile(*a.Path); err != nil { + if ret, err = os.ReadFile(*a.Path); err != nil { return } return @@ -94,7 +94,7 @@ func (a *Attachment) ContentBytes() (ret []byte, err error) { return } defer resp.Body.Close() - if ret, err = ioutil.ReadAll(resp.Body); err != nil { + if ret, err = io.ReadAll(resp.Body); err != nil { return } return diff --git a/common/file_manager.go b/common/file_manager.go new file mode 100644 index 00000000..406d746b --- /dev/null +++ b/common/file_manager.go @@ -0,0 +1,195 @@ +package common + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +// FileChangesMarker identifies the start of a file changes section in output +const FileChangesMarker = "__CREATE_CODING_FEATURE_FILE_CHANGES__" + +const ( + // MaxFileSize is the maximum size of a file that can be created (10MB) + MaxFileSize = 10 * 1024 * 1024 +) + +// FileChange represents a single file change operation to be performed +type FileChange struct { + Operation string `json:"operation"` // "create" or "update" + Path string `json:"path"` // Relative path from project root + Content string `json:"content"` // New file content +} + +// ParseFileChanges extracts and parses the file change marker section from LLM output +func ParseFileChanges(output string) (changeSummary string, changes []FileChange, err error) { + fileChangesStart := strings.Index(output, FileChangesMarker) + if fileChangesStart == -1 { + return output, nil, nil // No file changes section found + } + changeSummary = output[:fileChangesStart] // Everything before the marker + + // Extract the JSON part + jsonStart := fileChangesStart + len(FileChangesMarker) + // Find the first [ after the file changes marker + jsonArrayStart := strings.Index(output[jsonStart:], "[") + if jsonArrayStart == -1 { + return output, nil, fmt.Errorf("invalid %s format: no JSON array found", FileChangesMarker) + } + jsonStart += jsonArrayStart + + // Find the matching closing bracket for the array with proper bracket counting + bracketCount := 0 + jsonEnd := jsonStart + for i := jsonStart; i < len(output); i++ { + if output[i] == '[' { + bracketCount++ + } else if output[i] == ']' { + bracketCount-- + if bracketCount == 0 { + jsonEnd = i + 1 + break + } + } + } + + if bracketCount != 0 { + return output, nil, fmt.Errorf("invalid %s format: unbalanced brackets", FileChangesMarker) + } + + // Extract the JSON string and fix escape sequences + jsonStr := output[jsonStart:jsonEnd] + + // Fix specific invalid escape sequences + // First try with the common \C issue + jsonStr = strings.Replace(jsonStr, `\C`, `\\C`, -1) + + // Parse the JSON + var fileChanges []FileChange + err = json.Unmarshal([]byte(jsonStr), &fileChanges) + if err != nil { + // If still failing, try a more comprehensive fix + jsonStr = fixInvalidEscapes(jsonStr) + err = json.Unmarshal([]byte(jsonStr), &fileChanges) + if err != nil { + return changeSummary, nil, fmt.Errorf("failed to parse %s JSON: %w", FileChangesMarker, err) + } + } + + // Validate file changes + for i, change := range fileChanges { + // Validate operation + if change.Operation != "create" && change.Operation != "update" { + return changeSummary, nil, fmt.Errorf("invalid operation for file change %d: %s", i, change.Operation) + } + + // Validate path + if change.Path == "" { + return changeSummary, nil, fmt.Errorf("empty path for file change %d", i) + } + + // Check for suspicious paths (directory traversal) + if strings.Contains(change.Path, "..") { + return changeSummary, nil, fmt.Errorf("suspicious path for file change %d: %s", i, change.Path) + } + + // Check file size + if len(change.Content) > MaxFileSize { + return changeSummary, nil, fmt.Errorf("file content too large for file change %d: %d bytes", i, len(change.Content)) + } + } + + return changeSummary, fileChanges, nil +} + +// fixInvalidEscapes replaces invalid escape sequences in JSON strings +func fixInvalidEscapes(jsonStr string) string { + validEscapes := []byte{'b', 'f', 'n', 'r', 't', '\\', '/', '"', 'u'} + + var result strings.Builder + inQuotes := false + i := 0 + + for i < len(jsonStr) { + ch := jsonStr[i] + + // Track whether we're inside a JSON string + if ch == '"' && (i == 0 || jsonStr[i-1] != '\\') { + inQuotes = !inQuotes + } + + // Handle actual control characters inside string literals + if inQuotes { + // Convert literal control characters to proper JSON escape sequences + if ch == '\n' { + result.WriteString("\\n") + i++ + continue + } else if ch == '\r' { + result.WriteString("\\r") + i++ + continue + } else if ch == '\t' { + result.WriteString("\\t") + i++ + continue + } else if ch < 32 { + // Handle other control characters + fmt.Fprintf(&result, "\\u%04x", ch) + i++ + continue + } + } + + // Check for escape sequences only inside strings + if inQuotes && ch == '\\' && i+1 < len(jsonStr) { + nextChar := jsonStr[i+1] + isValid := false + + for _, validEscape := range validEscapes { + if nextChar == validEscape { + isValid = true + break + } + } + + if !isValid { + // Invalid escape sequence - add an extra backslash + result.WriteByte('\\') + result.WriteByte('\\') + i++ + continue + } + } + + result.WriteByte(ch) + i++ + } + + return result.String() +} + +// ApplyFileChanges applies the parsed file changes to the file system +func ApplyFileChanges(projectRoot string, changes []FileChange) error { + for i, change := range changes { + // Get the absolute path + absPath := filepath.Join(projectRoot, change.Path) + + // Create directories if necessary + dir := filepath.Dir(absPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s for file change %d: %w", dir, i, err) + } + + // Write the file + if err := os.WriteFile(absPath, []byte(change.Content), 0644); err != nil { + return fmt.Errorf("failed to write file %s for file change %d: %w", absPath, i, err) + } + + fmt.Printf("Applied %s operation to %s\n", change.Operation, change.Path) + } + + return nil +} diff --git a/common/file_manager_test.go b/common/file_manager_test.go new file mode 100644 index 00000000..43c3b349 --- /dev/null +++ b/common/file_manager_test.go @@ -0,0 +1,185 @@ +package common + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseFileChanges(t *testing.T) { + tests := []struct { + name string + input string + want int // number of expected file changes + wantErr bool + }{ + { + name: "No " + FileChangesMarker + " section", + input: "This is a normal response with no file changes.", + want: 0, + wantErr: false, + }, + { + name: "Valid " + FileChangesMarker + " section", + input: `Some text before. +` + FileChangesMarker + ` +[ + { + "operation": "create", + "path": "test.txt", + "content": "Hello, World!" + }, + { + "operation": "update", + "path": "other.txt", + "content": "Updated content" + } +] +Some text after.`, + want: 2, + wantErr: false, + }, + { + name: "Invalid JSON in " + FileChangesMarker + " section", + input: `Some text before. +` + FileChangesMarker + ` +[ + { + "operation": "create", + "path": "test.txt", + "content": "Hello, World!" + }, + { + "operation": "invalid", + "path": "other.txt" + "content": "Updated content" + } +]`, + want: 0, + wantErr: true, + }, + { + name: "Invalid operation", + input: `Some text before. +` + FileChangesMarker + ` +[ + { + "operation": "delete", + "path": "test.txt", + "content": "" + } +]`, + want: 0, + wantErr: true, + }, + { + name: "Empty path", + input: `Some text before. +` + FileChangesMarker + ` +[ + { + "operation": "create", + "path": "", + "content": "Hello, World!" + } +]`, + want: 0, + wantErr: true, + }, + { + name: "Suspicious path with directory traversal", + input: `Some text before. +` + FileChangesMarker + ` +[ + { + "operation": "create", + "path": "../etc/passwd", + "content": "Hello, World!" + } +]`, + want: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, got, err := ParseFileChanges(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseFileChanges() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && len(got) != tt.want { + t.Errorf("ParseFileChanges() got %d file changes, want %d", len(got), tt.want) + } + }) + } +} + +func TestApplyFileChanges(t *testing.T) { + // Create a temporary directory for testing + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "file-manager-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + // Test file changes + changes := []FileChange{ + { + Operation: "create", + Path: "test.txt", + Content: "Hello, World!", + }, + { + Operation: "create", + Path: "subdir/nested.txt", + Content: "Nested content", + }, + } + + // Apply the changes + if err := ApplyFileChanges(tempDir, changes); err != nil { + t.Fatalf("ApplyFileChanges() error = %v", err) + } + + // Verify the first file was created correctly + content, err := os.ReadFile(filepath.Join(tempDir, "test.txt")) + if err != nil { + t.Fatalf("Failed to read created file: %v", err) + } + if string(content) != "Hello, World!" { + t.Errorf("File content = %q, want %q", string(content), "Hello, World!") + } + + // Verify the nested file was created correctly + content, err = os.ReadFile(filepath.Join(tempDir, "subdir/nested.txt")) + if err != nil { + t.Fatalf("Failed to read created nested file: %v", err) + } + if string(content) != "Nested content" { + t.Errorf("Nested file content = %q, want %q", string(content), "Nested content") + } + + // Test updating a file + updateChanges := []FileChange{ + { + Operation: "update", + Path: "test.txt", + Content: "Updated content", + }, + } + + // Apply the update + if err := ApplyFileChanges(tempDir, updateChanges); err != nil { + t.Fatalf("ApplyFileChanges() error = %v", err) + } + // Verify the file was updated correctly + content, err = os.ReadFile(filepath.Join(tempDir, "test.txt")) + if err != nil { + t.Fatalf("Failed to read updated file: %v", err) + } + if string(content) != "Updated content" { + t.Errorf("Updated file content = %q, want %q", string(content), "Updated content") + } +} diff --git a/core/chatter.go b/core/chatter.go index bf23bbcd..2defd8f5 100644 --- a/core/chatter.go +++ b/core/chatter.go @@ -2,7 +2,9 @@ package core import ( "context" + "errors" "fmt" + "os" "strings" goopenai "github.com/sashabaranov/go-openai" @@ -28,6 +30,7 @@ type Chatter struct { strategy string } +// Send processes a chat request and applies any file changes if using the create_coding_feature pattern func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (session *fsdb.Session, err error) { if session, err = o.BuildSession(request, opts.Raw); err != nil { return @@ -79,6 +82,30 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s return } + // Process file changes if using the create_coding_feature pattern + if request.PatternName == "create_coding_feature" { + // Look for file changes in the response + summary, fileChanges, parseErr := common.ParseFileChanges(message) + if parseErr != nil { + fmt.Printf("Warning: Failed to parse file changes: %v\n", parseErr) + } else if len(fileChanges) > 0 { + // Get the project root - use the current directory + projectRoot, err := os.Getwd() + if err != nil { + fmt.Printf("Warning: Failed to get current directory: %v\n", err) + // Continue without applying changes + } else { + if applyErr := common.ApplyFileChanges(projectRoot, fileChanges); applyErr != nil { + fmt.Printf("Warning: Failed to apply file changes: %v\n", applyErr) + } else { + fmt.Println("Successfully applied file changes.") + fmt.Printf("You can review the changes with 'git diff' if you're using git.\n\n") + } + } + } + message = summary + } + session.Append(&goopenai.ChatCompletionMessage{Role: goopenai.ChatMessageRoleAssistant, Content: message}) if session.Name != "" { @@ -185,7 +212,7 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session * if session.IsEmpty() { session = nil - err = fmt.Errorf(NoSessionPatternUserMessages) + err = errors.New(NoSessionPatternUserMessages) } return } diff --git a/patterns/create_coding_feature/README.md b/patterns/create_coding_feature/README.md new file mode 100644 index 00000000..8126f642 --- /dev/null +++ b/patterns/create_coding_feature/README.md @@ -0,0 +1,85 @@ +# Create Coding Feature + +Generate code changes to an existing coding project using AI. + +## Installation + +After installing the `code_helper` binary: + +```bash +go install github.com/danielmiessler/fabric/plugins/tools/code_helper@latest +``` + +## Usage + +The create_coding_feature allows you to apply AI-suggested code changes directly to your project files. Use it like this: + +```bash +code_helper [project_directory] "[instructions for code changes]" | fabric --pattern create_coding_feature +``` + +For example: + +```bash +code_helper . "Create a simple Hello World C program in file main.c" | fabric --pattern create_coding_feature +``` + +## How It Works + +1. `code_helper` scans your project directory and creates a JSON representation +2. The AI model analyzes your project structure and instructions +3. AI generates file changes in a standard format +4. Fabric parses these changes and prompts you to confirm +5. If confirmed, changes are applied to your project files + +## Example Workflow + +```bash +# Request AI to create a Hello World program +code_helper . "Create a simple Hello World C program in file main.c" | fabric --pattern create_coding_feature + +# Review the changes made to your project +git diff + +# Run/test the code +make check + +# If satisfied, commit the changes +git add +git commit -s -m "Add Hello World program" +``` + +### Security Enhancement Example + +```bash +code_helper . "Ensure that all user input is validated and sanitized before being used in the program." | fabric --pattern create_coding_feature +git diff +make check +git add +git commit -s -m "Security fixes: Input validation" +``` + +## Important Notes + +- **Always run from project root**: File changes are applied relative to your current directory +- **Use with version control**: It's highly recommended to use this feature in a clean git repository so you can review and revert + changes. You will *not* be asked to approve each change. + +## Security Features + +- Path validation to prevent directory traversal attempts +- File size limits to prevent excessive file generation +- Operation validation (only create/update operations allowed) +- User confirmation required before applying changes + +## Suggestions for Future Improvements + +- Add a dry-run mode to show changes without applying them +- Enhance reporting with detailed change summaries +- Support for file deletions with safety checks +- Add configuration options for project-specific rules +- Provide rollback capability for applied changes +- Add support for project-specific validation rules +- Enhance script generation with conditional logic +- Include detailed logging for API responses +- Consider adding a GUI for ease of use diff --git a/patterns/create_coding_feature/system.md b/patterns/create_coding_feature/system.md new file mode 100644 index 00000000..a5685043 --- /dev/null +++ b/patterns/create_coding_feature/system.md @@ -0,0 +1,117 @@ +# IDENTITY and PURPOSE + +You are an elite programmer. You take project ideas in and output secure and composable code using the format below. You always use the latest technology and best practices. + +Take a deep breath and think step by step about how to best accomplish this goal using the following steps. + +Input is a JSON file with the following format: + +Example input: + +```json +[ + { + "type": "directory", + "name": ".", + "contents": [ + { + "type": "file", + "name": "README.md", + "content": "This is the README.md file content" + }, + { + "type": "file", + "name": "system.md", + "content": "This is the system.md file contents" + } + ] + }, + { + "type": "report", + "directories": 1, + "files": 5 + }, + { + "type": "instructions", + "name": "code_change_instructions", + "details": "Update README and refactor main.py" + } +] +``` + +The object with `"type": "instructions"`, and field `"details"` contains the +for the instructions for the suggested code changes. The `"name"` field is always +`"code_change_instructions"` + +The `"details"` field above, with type `"instructions"` contains the instructions for the suggested code changes. + +## File Management Interface Instructions + +You have access to a powerful file management system with the following capabilities: + +### File Creation and Modification + +- Use the **EXACT** JSON format below to define files that you want to be changed +- If the file listed does not exist, it will be created +- If a directory listed does not exist, it will be created +- If the file already exists, it will be overwritten +- It is **not possible** to delete files + +```plaintext +__CREATE_CODING_FEATURE_FILE_CHANGES__ +[ + { + "operation": "create", + "path": "README.md", + "content": "This is the new README.md file content" + }, + { + "operation": "update", + "path": "src/main.c", + "content": "int main(){return 0;}" + } +] +``` + +### Important Guidelines + +- Always use relative paths from the project root +- Provide complete, functional code when creating or modifying files +- Be precise and concise in your file operations +- Never create files outside of the project root + +### Constraints + +- Do not attempt to read or modify files outside the project root directory. +- Ensure code follows best practices and is production-ready. +- Handle potential errors gracefully in your code suggestions. +- Do not trust external input to applications, assume users are malicious. + +### Workflow + +1. Analyze the user's request +2. Determine necessary file operations +3. Provide clear, executable file creation/modification instructions +4. Explain the purpose and functionality of proposed changes + +## Output Sections + +- Output a summary of the file changes +- Output directory and file changes according to File Management Interface Instructions, in a json array marked by `__CREATE_CODING_FEATURE_FILE_CHANGES__` +- Be exact in the `__CREATE_CODING_FEATURE_FILE_CHANGES__` section, and do not deviate from the proposed JSON format. +- **never** omit the `__CREATE_CODING_FEATURE_FILE_CHANGES__` section. +- If the proposed changes change how the project is built and installed, document these changes in the projects README.md +- Implement build configurations changes if needed, prefer ninja if nothing already exists in the project, or is otherwise specified. +- Document new dependencies according to best practices for the language used in the project. +- Do not output sections that were not explicitly requested. + +## Output Instructions + +- Create the output using the formatting above +- Do not output warnings or notes—just the requested sections. +- Do not repeat items in the output sections +- Be open to suggestions and output file system changes according to the JSON API described above +- Output code that has comments for every step +- Do not use deprecated features + +## INPUT diff --git a/plugins/tools/code_helper/code.go b/plugins/tools/code_helper/code.go new file mode 100644 index 00000000..4794feda --- /dev/null +++ b/plugins/tools/code_helper/code.go @@ -0,0 +1,181 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +// FileItem represents a file in the project +type FileItem struct { + Type string `json:"type"` + Name string `json:"name"` + Content string `json:"content,omitempty"` + Contents []FileItem `json:"contents,omitempty"` +} + +// ProjectData represents the entire project structure with instructions +type ProjectData struct { + Files []FileItem `json:"files"` + Instructions struct { + Type string `json:"type"` + Name string `json:"name"` + Details string `json:"details"` + } `json:"instructions"` + Report struct { + Type string `json:"type"` + Directories int `json:"directories"` + Files int `json:"files"` + } `json:"report"` +} + +// ScanDirectory scans a directory and returns a JSON representation of its structure +func ScanDirectory(rootDir string, maxDepth int, instructions string, ignoreList []string) ([]byte, error) { + // Count totals for report + dirCount := 1 + fileCount := 0 + + // Create root directory item + rootItem := FileItem{ + Type: "directory", + Name: rootDir, + Contents: []FileItem{}, + } + + // Walk through the directory + err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip .git directory + if strings.Contains(path, ".git") { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + // Check if path matches any ignore pattern + relPath, err := filepath.Rel(rootDir, path) + if err != nil { + return err + } + + for _, pattern := range ignoreList { + if strings.Contains(relPath, pattern) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + } + + if relPath == "." { + return nil + } + + depth := len(strings.Split(relPath, string(filepath.Separator))) + if depth > maxDepth { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + // Create directory structure + if info.IsDir() { + dirCount++ + } else { + fileCount++ + + // Read file content + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("error reading file %s: %v", path, err) + } + + // Add file to appropriate parent directory + addFileToDirectory(&rootItem, relPath, string(content), rootDir) + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Create final data structure + var data []interface{} + data = append(data, rootItem) + + // Add report + reportItem := map[string]interface{}{ + "type": "report", + "directories": dirCount, + "files": fileCount, + } + data = append(data, reportItem) + + // Add instructions + instructionsItem := map[string]interface{}{ + "type": "instructions", + "name": "code_change_instructions", + "details": instructions, + } + data = append(data, instructionsItem) + + return json.MarshalIndent(data, "", " ") +} + +// addFileToDirectory adds a file to the correct directory in the structure +func addFileToDirectory(root *FileItem, path, content, rootDir string) { + parts := strings.Split(path, string(filepath.Separator)) + + // If this is a file at the root level + if len(parts) == 1 { + root.Contents = append(root.Contents, FileItem{ + Type: "file", + Name: parts[0], + Content: content, + }) + return + } + + // Otherwise, find or create the directory path + current := root + for i := 0; i < len(parts)-1; i++ { + dirName := parts[i] + found := false + + // Look for existing directory + for j, item := range current.Contents { + if item.Type == "directory" && item.Name == dirName { + current = ¤t.Contents[j] + found = true + break + } + } + + // Create directory if not found + if !found { + newDir := FileItem{ + Type: "directory", + Name: dirName, + Contents: []FileItem{}, + } + current.Contents = append(current.Contents, newDir) + current = ¤t.Contents[len(current.Contents)-1] + } + } + + // Add the file to the current directory + current.Contents = append(current.Contents, FileItem{ + Type: "file", + Name: parts[len(parts)-1], + Content: content, + }) +} diff --git a/plugins/tools/code_helper/main.go b/plugins/tools/code_helper/main.go new file mode 100644 index 00000000..bcb39f89 --- /dev/null +++ b/plugins/tools/code_helper/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" +) + +func main() { + // Define command line flags + maxDepth := flag.Int("depth", 3, "Maximum directory depth to scan") + ignorePatterns := flag.String("ignore", ".git,node_modules,vendor", "Comma-separated patterns to ignore") + outputFile := flag.String("out", "", "Output file (default: stdout)") + showHelp := flag.Bool("help", false, "Show help message") + + // Parse command line flags + flag.Parse() + + // Show help if requested or no arguments provided + if *showHelp || flag.NArg() < 1 { + printUsage() + os.Exit(0) + } + + // Get directory and instructions from positional arguments + directory := flag.Arg(0) + instructions := "" + if flag.NArg() > 1 { + instructions = flag.Arg(1) + } + + // Validate directory + if _, err := os.Stat(directory); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Error: Directory '%s' does not exist\n", directory) + os.Exit(1) + } + // Parse ignore patterns + ignoreList := ParseIgnorePatterns(*ignorePatterns) + + // Scan directory and generate JSON + var jsonData []byte + var err error + jsonData, err = ScanDirectory(directory, *maxDepth, instructions, ignoreList) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + // Write output + if *outputFile != "" { + if err := os.WriteFile(*outputFile, jsonData, 0644); err != nil { + fmt.Fprintf(os.Stderr, "Error writing to file: %v\n", err) + os.Exit(1) + } + } else { + fmt.Print(string(jsonData)) + } +} + +// ParseIgnorePatterns converts a comma-separated string of patterns to a slice +func ParseIgnorePatterns(patterns string) []string { + if patterns == "" { + return nil + } + return strings.Split(patterns, ",") +} + +func printUsage() { + fmt.Println(`code_helper - Code project scanner for use with Fabric AI + +Usage: + code_helper [options] [instructions] + +Examples: + # Scan current directory with instructions + code_helper . "Add input validation to all user inputs" + + # Scan specific directory with depth limit + code_helper -depth 4 ./my-project "Implement error handling" + + # Output to file instead of stdout + code_helper -out project.json ./src "Fix security issues" + +Options:`) + flag.PrintDefaults() +}