mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-10 06:48:04 -05:00
Merge pull request #1389 from ksylvan/0323-nysan-conding-feature
Create Coding Feature
This commit is contained in:
15
README.md
15
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
195
common/file_manager.go
Normal file
195
common/file_manager.go
Normal file
@@ -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
|
||||
}
|
||||
185
common/file_manager_test.go
Normal file
185
common/file_manager_test.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
85
patterns/create_coding_feature/README.md
Normal file
85
patterns/create_coding_feature/README.md
Normal file
@@ -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 <changed files>
|
||||
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 <changed files>
|
||||
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
|
||||
117
patterns/create_coding_feature/system.md
Normal file
117
patterns/create_coding_feature/system.md
Normal file
@@ -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
|
||||
181
plugins/tools/code_helper/code.go
Normal file
181
plugins/tools/code_helper/code.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
87
plugins/tools/code_helper/main.go
Normal file
87
plugins/tools/code_helper/main.go
Normal file
@@ -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] <directory> [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()
|
||||
}
|
||||
Reference in New Issue
Block a user