Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,25 @@ coding-agent-context -p feature="User Login" -p language=Go add-feature

### Memory Files

Markdown files included in every generated context. Can include bootstrap scripts in frontmatter.
Markdown files included in every generated context. Bootstrap scripts can be provided in separate files.

**Example** (`.coding-agent-context/memories/setup.md`):
```markdown
---
bootstrap: |
#!/bin/bash
npm install
---
# Development Setup

This project requires Node.js dependencies.
```

**Bootstrap file** (`.coding-agent-context/memories/setup-bootstrap`):
```bash
#!/bin/bash
npm install
```

For each memory file `<name>.md`, you can optionally create a corresponding `<name>-bootstrap` file that will be executed during setup.


## Output Files

Expand Down Expand Up @@ -186,15 +191,18 @@ coding-agent-context -p featureName="Authentication" -p language=Go add-feature
```bash
cat > .coding-agent-context/memories/setup.md << 'EOF'
---
bootstrap: |
#!/bin/bash
go mod download
---
# Project Setup

This Go project uses modules.
EOF

cat > .coding-agent-context/memories/setup-bootstrap << 'EOF'
#!/bin/bash
go mod download
EOF
chmod +x .coding-agent-context/memories/setup-bootstrap

coding-agent-context -o ./output my-task
cd output && ./bootstrap
```
Expand Down
236 changes: 236 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package main

import (
"os"
"os/exec"
"path/filepath"
"testing"
)

func TestBootstrapFromFile(t *testing.T) {
// Build the binary
binaryPath := filepath.Join(t.TempDir(), "coding-agent-context")
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to build binary: %v\n%s", err, output)
}

// Create a temporary directory structure
tmpDir := t.TempDir()
contextDir := filepath.Join(tmpDir, ".coding-agent-context")
memoriesDir := filepath.Join(contextDir, "memories")
promptsDir := filepath.Join(contextDir, "prompts")
outputDir := filepath.Join(tmpDir, "output")

if err := os.MkdirAll(memoriesDir, 0755); err != nil {
t.Fatalf("failed to create memories dir: %v", err)
}
if err := os.MkdirAll(promptsDir, 0755); err != nil {
t.Fatalf("failed to create prompts dir: %v", err)
}

// Create a memory file
memoryFile := filepath.Join(memoriesDir, "setup.md")
memoryContent := `---
---
# Development Setup

This is a setup guide.
`
if err := os.WriteFile(memoryFile, []byte(memoryContent), 0644); err != nil {
t.Fatalf("failed to write memory file: %v", err)
}

// Create a bootstrap file for the memory (setup.md -> setup-bootstrap)
bootstrapFile := filepath.Join(memoriesDir, "setup-bootstrap")
bootstrapContent := `#!/bin/bash
echo "Running bootstrap"
npm install
`
if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0755); err != nil {
t.Fatalf("failed to write bootstrap file: %v", err)
}

// Create a prompt file
promptFile := filepath.Join(promptsDir, "test-task.md")
promptContent := `---
---
# Test Task

Please help with this task.
`
if err := os.WriteFile(promptFile, []byte(promptContent), 0644); err != nil {
t.Fatalf("failed to write prompt file: %v", err)
}

// Run the binary
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
cmd.Dir = tmpDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to run binary: %v\n%s", err, output)
}

// Check that the bootstrap.d directory was created
bootstrapDDir := filepath.Join(outputDir, "bootstrap.d")
if _, err := os.Stat(bootstrapDDir); os.IsNotExist(err) {
t.Errorf("bootstrap.d directory was not created")
}

// Check that a bootstrap file exists in bootstrap.d
files, err := os.ReadDir(bootstrapDDir)
if err != nil {
t.Fatalf("failed to read bootstrap.d dir: %v", err)
}
if len(files) != 1 {
t.Errorf("expected 1 bootstrap file, got %d", len(files))
}

// Check that the bootstrap file has the correct content
if len(files) > 0 {
bootstrapPath := filepath.Join(bootstrapDDir, files[0].Name())
content, err := os.ReadFile(bootstrapPath)
if err != nil {
t.Fatalf("failed to read bootstrap file: %v", err)
}
if string(content) != bootstrapContent {
t.Errorf("bootstrap content mismatch:\ngot: %q\nwant: %q", string(content), bootstrapContent)
}
}

// Check that the prompt.md file was created
promptOutput := filepath.Join(outputDir, "prompt.md")
if _, err := os.Stat(promptOutput); os.IsNotExist(err) {
t.Errorf("prompt.md file was not created")
}
}

func TestBootstrapFileNotRequired(t *testing.T) {
// Build the binary
binaryPath := filepath.Join(t.TempDir(), "coding-agent-context")
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to build binary: %v\n%s", err, output)
}

// Create a temporary directory structure
tmpDir := t.TempDir()
contextDir := filepath.Join(tmpDir, ".coding-agent-context")
memoriesDir := filepath.Join(contextDir, "memories")
promptsDir := filepath.Join(contextDir, "prompts")
outputDir := filepath.Join(tmpDir, "output")

if err := os.MkdirAll(memoriesDir, 0755); err != nil {
t.Fatalf("failed to create memories dir: %v", err)
}
if err := os.MkdirAll(promptsDir, 0755); err != nil {
t.Fatalf("failed to create prompts dir: %v", err)
}

// Create a memory file WITHOUT a bootstrap
memoryFile := filepath.Join(memoriesDir, "info.md")
memoryContent := `---
---
# Project Info

Just some information.
`
if err := os.WriteFile(memoryFile, []byte(memoryContent), 0644); err != nil {
t.Fatalf("failed to write memory file: %v", err)
}

// Create a prompt file
promptFile := filepath.Join(promptsDir, "test-task.md")
promptContent := `---
---
# Test Task

Please help with this task.
`
if err := os.WriteFile(promptFile, []byte(promptContent), 0644); err != nil {
t.Fatalf("failed to write prompt file: %v", err)
}

// Run the binary
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
cmd.Dir = tmpDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to run binary: %v\n%s", err, output)
}

// Check that the bootstrap.d directory was created but is empty
bootstrapDDir := filepath.Join(outputDir, "bootstrap.d")
files, err := os.ReadDir(bootstrapDDir)
if err != nil {
t.Fatalf("failed to read bootstrap.d dir: %v", err)
}
if len(files) != 0 {
t.Errorf("expected 0 bootstrap files, got %d", len(files))
}

// Check that the prompt.md file was still created
promptOutput := filepath.Join(outputDir, "prompt.md")
if _, err := os.Stat(promptOutput); os.IsNotExist(err) {
t.Errorf("prompt.md file was not created")
}
}

func TestMultipleBootstrapFiles(t *testing.T) {
// Build the binary
binaryPath := filepath.Join(t.TempDir(), "coding-agent-context")
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to build binary: %v\n%s", err, output)
}

// Create a temporary directory structure
tmpDir := t.TempDir()
contextDir := filepath.Join(tmpDir, ".coding-agent-context")
memoriesDir := filepath.Join(contextDir, "memories")
promptsDir := filepath.Join(contextDir, "prompts")
outputDir := filepath.Join(tmpDir, "output")

if err := os.MkdirAll(memoriesDir, 0755); err != nil {
t.Fatalf("failed to create memories dir: %v", err)
}
if err := os.MkdirAll(promptsDir, 0755); err != nil {
t.Fatalf("failed to create prompts dir: %v", err)
}

// Create first memory file with bootstrap
if err := os.WriteFile(filepath.Join(memoriesDir, "setup.md"), []byte("---\n---\n# Setup\n"), 0644); err != nil {
t.Fatalf("failed to write memory file: %v", err)
}
if err := os.WriteFile(filepath.Join(memoriesDir, "setup-bootstrap"), []byte("#!/bin/bash\necho setup\n"), 0755); err != nil {
t.Fatalf("failed to write bootstrap file: %v", err)
}

// Create second memory file with bootstrap
if err := os.WriteFile(filepath.Join(memoriesDir, "deps.md"), []byte("---\n---\n# Dependencies\n"), 0644); err != nil {
t.Fatalf("failed to write memory file: %v", err)
}
if err := os.WriteFile(filepath.Join(memoriesDir, "deps-bootstrap"), []byte("#!/bin/bash\necho deps\n"), 0755); err != nil {
t.Fatalf("failed to write bootstrap file: %v", err)
}

// Create a prompt file
if err := os.WriteFile(filepath.Join(promptsDir, "test-task.md"), []byte("---\n---\n# Test\n"), 0644); err != nil {
t.Fatalf("failed to write prompt file: %v", err)
}

// Run the binary
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
cmd.Dir = tmpDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to run binary: %v\n%s", err, output)
}

// Check that both bootstrap files exist in bootstrap.d
bootstrapDDir := filepath.Join(outputDir, "bootstrap.d")
files, err := os.ReadDir(bootstrapDDir)
if err != nil {
t.Fatalf("failed to read bootstrap.d dir: %v", err)
}
if len(files) != 2 {
t.Errorf("expected 2 bootstrap files, got %d", len(files))
}
}
29 changes: 21 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"
"text/template"
)

Expand Down Expand Up @@ -77,6 +78,12 @@ func run(args []string) error {

for _, dir := range dirs {
memoryDir := filepath.Join(dir, "memories")

// Skip if the directory doesn't exist
if _, err := os.Stat(memoryDir); os.IsNotExist(err) {
continue
}

err := filepath.Walk(memoryDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
Expand All @@ -85,21 +92,27 @@ func run(args []string) error {
return nil
}

slog.Info("Including memory file", "path", path)

var frontmatter struct {
Bootstrap string `yaml:"bootstrap"`
// Only process .md files as memory files
if filepath.Ext(path) != ".md" {
return nil
}

content, err := parseMarkdownFile(path, &frontmatter)
slog.Info("Including memory file", "path", path)

content, err := parseMarkdownFile(path, &struct{}{})
if err != nil {
return fmt.Errorf("failed to parse markdown file: %w", err)
}

if bootstrap := frontmatter.Bootstrap; bootstrap != "" {
hash := sha256.Sum256([]byte(bootstrap))
// Check for a bootstrap file named <markdown-file-without-md-suffix>-bootstrap
// For example, setup.md -> setup-bootstrap
baseNameWithoutExt := strings.TrimSuffix(path, ".md")
bootstrapFilePath := baseNameWithoutExt + "-bootstrap"

if bootstrapContent, err := os.ReadFile(bootstrapFilePath); err == nil {
hash := sha256.Sum256(bootstrapContent)
bootstrapPath := filepath.Join(bootstrapDir, fmt.Sprintf("%x", hash))
if err := os.WriteFile(bootstrapPath, []byte(bootstrap), 0700); err != nil {
if err := os.WriteFile(bootstrapPath, bootstrapContent, 0700); err != nil {
return fmt.Errorf("failed to write bootstrap file: %w", err)
}
}
Expand Down