Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Binary output
coding-context
coding-agent-context-cli
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,19 @@ Usage:
Options:
-C string
Change to directory before doing anything. (default ".")
-S value
Exclude rules with matching frontmatter. Can be specified multiple times as key=value.
-o string
Output file. If unspecified, output is written to stdout.
-p value
Parameter to substitute in the prompt. Can be specified multiple times as key=value.
-s value
Include rules with matching frontmatter. Can be specified multiple times as key=value.
-S value
Exclude rules with matching frontmatter. Can be specified multiple times as key=value.
```

### Example
### Examples

**Example 1: Pipe to another program**

```bash
coding-agent-context-cli -p jira_issue_key=PROJ-1234 fix-bug | llm -m gemini-pro
Expand All @@ -56,6 +60,17 @@ This command will:
6. Print the combined context (rules + task) to `stdout`.
7. Pipe the output to another program (in this case, `llm`).

**Example 2: Save to a file**

```bash
coding-agent-context-cli -p jira_issue_key=PROJ-1234 -o context.txt fix-bug
```

This command will:
1. Generate the combined context as in Example 1.
2. Write the output to `context.txt` instead of stdout.
3. Print diagnostic messages (file inclusions, token counts) to stderr.

### Example Tasks

The `<task-name>` is the name of the task you want the agent to perform. Here are some common examples:
Expand Down
173 changes: 172 additions & 1 deletion integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Please help with this task.
outputStr := string(output)
bootstrapIdx := strings.Index(outputStr, "Running bootstrap")
setupIdx := strings.Index(outputStr, "# Development Setup")

if bootstrapIdx == -1 {
t.Errorf("bootstrap output not found in stdout")
}
Expand Down Expand Up @@ -535,3 +535,174 @@ Please help with this task.
t.Errorf(".mdc file content not found in stdout")
}
}

func TestOutputFileFlag(t *testing.T) {
// Build the binary
binaryPath := filepath.Join(t.TempDir(), "coding-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()
rulesDir := filepath.Join(tmpDir, ".agents", "rules")
tasksDir := filepath.Join(tmpDir, ".agents", "tasks")

if err := os.MkdirAll(rulesDir, 0755); err != nil {
t.Fatalf("failed to create rules dir: %v", err)
}
if err := os.MkdirAll(tasksDir, 0755); err != nil {
t.Fatalf("failed to create tasks dir: %v", err)
}

// Create a rule file
ruleFile := filepath.Join(rulesDir, "info.md")
ruleContent := `---
---
# Project Info

General information about the project.
`
if err := os.WriteFile(ruleFile, []byte(ruleContent), 0644); err != nil {
t.Fatalf("failed to write rule file: %v", err)
}

// Create a task file
taskFile := filepath.Join(tasksDir, "test-task.md")
taskContent := `---
---
# Test Task

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

// Create output file path
outputFile := filepath.Join(tmpDir, "output.txt")

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

// Check that stdout is empty (since output went to file)
stdoutStr := string(output)
if strings.Contains(stdoutStr, "# Project Info") {
t.Errorf("rule content should not be in stdout when using -o flag")
}
if strings.Contains(stdoutStr, "# Test Task") {
t.Errorf("task content should not be in stdout when using -o flag")
}

// Check that diagnostic messages are still in stderr (CombinedOutput combines stdout and stderr)
if !strings.Contains(stdoutStr, "Including rule file") {
t.Errorf("diagnostic messages should still be in stderr")
}

// Read the output file and verify content
fileContent, err := os.ReadFile(outputFile)
if err != nil {
t.Fatalf("failed to read output file: %v", err)
}

fileStr := string(fileContent)
if !strings.Contains(fileStr, "# Project Info") {
t.Errorf("rule content not found in output file")
}
if !strings.Contains(fileStr, "# Test Task") {
t.Errorf("task content not found in output file")
}

// Verify diagnostic messages are NOT in the output file
if strings.Contains(fileStr, "Including rule file") {
t.Errorf("diagnostic messages should not be in output file")
}
}

func TestOutputFileWithBootstrap(t *testing.T) {
// Build the binary
binaryPath := filepath.Join(t.TempDir(), "coding-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()
rulesDir := filepath.Join(tmpDir, ".agents", "rules")
tasksDir := filepath.Join(tmpDir, ".agents", "tasks")

if err := os.MkdirAll(rulesDir, 0755); err != nil {
t.Fatalf("failed to create rules dir: %v", err)
}
if err := os.MkdirAll(tasksDir, 0755); err != nil {
t.Fatalf("failed to create tasks dir: %v", err)
}

// Create a rule file with bootstrap
ruleFile := filepath.Join(rulesDir, "setup.md")
ruleContent := `---
---
# Setup

Setup instructions.
`
if err := os.WriteFile(ruleFile, []byte(ruleContent), 0644); err != nil {
t.Fatalf("failed to write rule file: %v", err)
}

bootstrapFile := filepath.Join(rulesDir, "setup-bootstrap")
bootstrapContent := `#!/bin/bash
echo "Running setup bootstrap"
`
if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0755); err != nil {
t.Fatalf("failed to write bootstrap file: %v", err)
}

// Create a task file
taskFile := filepath.Join(tasksDir, "test-task.md")
taskContent := `---
---
# Test Task

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

// Create output file path
outputFile := filepath.Join(tmpDir, "output.txt")

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

// Check that bootstrap output is in stderr (part of CombinedOutput)
stdoutStr := string(output)
if !strings.Contains(stdoutStr, "Running setup bootstrap") {
t.Errorf("bootstrap output should still be in stderr")
}

// Read the output file
fileContent, err := os.ReadFile(outputFile)
if err != nil {
t.Fatalf("failed to read output file: %v", err)
}

fileStr := string(fileContent)
if !strings.Contains(fileStr, "# Setup") {
t.Errorf("rule content not found in output file")
}
if !strings.Contains(fileStr, "# Test Task") {
t.Errorf("task content not found in output file")
}
}
27 changes: 21 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
_ "embed"
"flag"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
Expand All @@ -14,17 +15,19 @@ import (
)

var (
workDir string
params = make(paramMap)
includes = make(selectorMap)
excludes = make(selectorMap)
workDir string
outputFile string
params = make(paramMap)
includes = make(selectorMap)
excludes = make(selectorMap)
)

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

flag.StringVar(&workDir, "C", ".", "Change to directory before doing anything.")
flag.StringVar(&outputFile, "o", "", "Output file. If unspecified, output is written to stdout.")
flag.Var(&params, "p", "Parameter to substitute in the prompt. Can be specified multiple times as key=value.")
flag.Var(&includes, "s", "Include rules with matching frontmatter. Can be specified multiple times as key=value.")
flag.Var(&excludes, "S", "Exclude rules with matching frontmatter. Can be specified multiple times as key=value.")
Expand All @@ -48,6 +51,18 @@ func main() {
}

func run(ctx context.Context, args []string) error {
// Setup output writer
var output io.Writer
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
output = f
} else {
output = os.Stdout
}
if len(args) != 1 {
return fmt.Errorf("invalid usage")
}
Expand Down Expand Up @@ -188,7 +203,7 @@ func run(ctx context.Context, args []string) error {
tokens := estimateTokens(content)
totalTokens += tokens
fmt.Fprintf(os.Stderr, "⪢ Including rule file: %s (~%d tokens)\n", path, tokens)
fmt.Println(content)
fmt.Fprintln(output, content)

return nil

Expand Down Expand Up @@ -216,7 +231,7 @@ func run(ctx context.Context, args []string) error {
totalTokens += tokens
fmt.Fprintf(os.Stderr, "⪢ Including task file: %s (~%d tokens)\n", taskPromptPath, tokens)

fmt.Println(expanded)
fmt.Fprintln(output, expanded)

// Print total token count
fmt.Fprintf(os.Stderr, "⪢ Total estimated tokens: %d\n", totalTokens)
Expand Down
6 changes: 3 additions & 3 deletions token_counter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import "testing"

func TestEstimateTokens(t *testing.T) {
tests := []struct {
name string
text string
want int
name string
text string
want int
}{
{
name: "empty string",
Expand Down