Skip to content
311 changes: 311 additions & 0 deletions docs/PRD-composable-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
# PRD: Composable & Agentic Brev CLI

## Vision

Make the Brev CLI idiomatic, programmable, and agent-friendly. Users and AI agents should be able to compose commands using standard Unix patterns (`|`, `grep`, `awk`, `jq`) while also having structured output options for programmatic access.

## Why Now

Coding agents (Claude Code, Cursor, Cline, Aider, OpenCode, Clawdbot) are becoming the primary interface between developers and their tools. These agents prefer CLIs over APIs:

- **Text-native** - LLMs think in text; pipes and grep are natural
- **Self-documenting** - `--help` and tab completion beat reading API docs
- **Composable** - Chain steps: `brev search | brev create | brev exec "setup.sh"`
- **Learned from training** - Agents already know Unix conventions from GitHub/Stack Overflow

Most GPU clouds have dashboards and APIs, but weak CLIs. A composable Brev CLI becomes the default for autonomous GPU workflows.

## Goals

1. **Unix Idiomatic** - Commands work naturally with pipes and standard tools
2. **Programmable** - JSON output mode for all commands that return data
3. **Agentic** - Claude Code skills can orchestrate complex workflows
4. **Composable** - Output of one command feeds into input of another

## Design Principles

### Pipe Detection
- Commands detect when stdout is piped (`os.Stdout.Stat()`)
- Piped output: clean table format (no colors, no help text)
- Interactive output: colored, with contextual help

### Input Handling
- Commands accept arguments directly OR from stdin
- Stdin is read line-by-line when piped
- First column of table input is parsed as the primary identifier

### Output Formats
| Mode | Trigger | Format |
|------|---------|--------|
| Interactive | TTY | Colored table + help text |
| Piped | `cmd \| ...` | Plain table (greppable) |
| JSON | `--json` | Structured JSON array |

### Data Passthrough
- Filter flags (e.g., `--min-disk`) should propagate through pipes
- Table output includes computed fields (e.g., `TARGET_DISK`)
- JSON output includes all relevant fields

## Implemented Features

### Pipeable Commands
| Command | Stdin | Stdout (piped) | Status |
|---------|-------|----------------|--------|
| `brev ls` | - | Plain table | ✅ |
| `brev ls orgs` | - | Plain table | ✅ |
| `brev search` | - | Plain table w/ TARGET_DISK | ✅ |
| `brev stop` | Instance names | Instance names | ✅ |
| `brev start` | Instance names | Instance names | ✅ |
| `brev delete` | Instance names | Instance names | ✅ |
| `brev create` | Instance types (table or JSON) | Instance names | ✅ |
| `brev shell` | - | - (interactive) | ✅ |
| `brev exec` | Instance names | Command stdout/stderr | ✅ |
| `brev open` | Instance names | Instance names | ✅ |

### Exec Command (`brev exec`)

Non-interactive command execution for scripted and agentic workflows.

**Run commands directly**:
```bash
brev exec my-gpu "nvidia-smi"
brev exec my-gpu "python train.py && echo done"
```

**Run local scripts remotely** (`@filepath` syntax):
```bash
brev exec my-gpu @setup.sh # Runs local setup.sh on remote
brev exec my-gpu @scripts/deploy.sh # Relative paths supported
```

**Multi-instance support**:
```bash
# Run on multiple instances
brev exec gpu-1 gpu-2 gpu-3 "nvidia-smi"

# Pipe from create
brev create my-cluster --count 3 | brev exec "nvidia-smi"

# Chain with other commands
brev ls | grep RUNNING | brev exec "df -h"
```

**Output for chaining**:
Outputs instance names after execution completes, enabling pipelines:
```bash
brev create my-gpu | brev exec "pip install torch" | brev exec "python train.py"
```

### Shell Command (`brev shell`)

Interactive SSH session to an instance. Use `brev exec` for non-interactive commands.

```bash
brev shell my-gpu # Interactive shell
brev shell $(brev create my-gpu) # Create and connect
brev shell my-gpu --host # SSH to host instead of container
```

### Open Enhancements (`brev open`)

Open instances in editors/terminals with multi-instance and cross-platform support.

**Editor options**:
```bash
brev open my-gpu vscode # VS Code (default)
brev open my-gpu cursor # Cursor
brev open my-gpu vim # Vim over SSH
brev open my-gpu terminal # Terminal/SSH session
brev open my-gpu tmux # Tmux session
```

**Multi-instance support**:
```bash
# Open multiple instances (each in separate window)
brev open gpu-1 gpu-2 gpu-3 cursor

# Pipe from create
brev create my-cluster --count 3 | brev open cursor
```

**Output for chaining**:
Outputs instance names when piped, enabling pipelines:
```bash
# Create, open in editor, then run setup
brev create my-gpu | brev open cursor | brev exec "pip install -r requirements.txt"
```

**Cross-platform support**:
- macOS: Terminal.app, iTerm2
- Linux: Default terminal emulator
- Windows/WSL: Fixed exec format errors

### Search Filters
```bash
brev search --gpu-name H100 # Filter by GPU
brev search --min-vram 40 # Min VRAM per GPU
brev search --min-total-vram 80 # Min total VRAM
brev search --min-disk 500 # Min disk size (GB)
brev search --max-boot-time 5 # Max boot time (minutes)
brev search --stoppable # Can stop/restart
brev search --rebootable # Can reboot
brev search --flex-ports # Configurable firewall
```

### JSON Mode
```bash
brev ls --json
brev ls orgs --json
brev search --json
```

## Example Workflows

### Filter and Create
```bash
# Find stoppable H100s with 500GB disk, create first match
brev search --min-disk 500 --stoppable | grep H100 | head -1 | brev create --name my-gpu
```

### Batch Operations
```bash
# Stop all running instances
brev ls | grep RUNNING | awk '{print $1}' | brev stop

# Delete all stopped instances
brev ls | grep STOPPED | awk '{print $1}' | brev delete
```

### Chained Lifecycle
```bash
# Create, use, cleanup
brev search --gpu-name A100 | head -1 | brev create --name job-1 | brev exec "python train.py" && brev delete job-1
```

### JSON Processing
```bash
# Get cheapest H100 with jq
brev search --json | jq '[.[] | select(.gpu_name == "H100")] | sort_by(.price_per_hour) | .[0]'
```

## Claude Code Integration

### Why Skills Matter

The composable CLI is necessary but not sufficient for agentic use. Skills bridge the gap between:

1. **Raw CLI** - Powerful but requires knowing exact flags and syntax
2. **Natural Language** - How users actually describe intent

Without skills, an agent must:
- Know that `--min-total-vram` exists (not `--vram`, `--gpu-memory`, etc.)
- Remember flag combinations for common tasks
- Handle error messages and retry logic
- Understand which commands can be piped together

Skills encode this domain knowledge, turning "spin up a cheap GPU for testing" into the correct `brev search --stoppable --sort price | head -1 | brev create` pipeline.

### Skill Capabilities

The `/brev-cli` skill provides:

**Natural Language → CLI Translation**
- "Create an A100 instance for ML training" → selects appropriate flags
- "Find GPUs with 40GB VRAM under $2/hr" → `--min-total-vram 40` + price filter
- "Stop all my running instances" → `brev ls | grep RUNNING | ... | brev stop`

**Context-Aware Defaults**
- Knows common GPU requirements for ML workloads
- Suggests `--stoppable` for dev instances (cost savings)
- Recommends disk sizes based on use case

**Error Recovery**
- Retries with fallback instance types on capacity errors
- Suggests alternatives when requested GPU unavailable
- Handles "instance already exists" gracefully

**Workflow Orchestration**
- Multi-step operations (create → wait → execute → cleanup)
- Monitors instance health during long-running jobs
- Streams logs and captures results

### Agentic Patterns

With composable CLI + skills, agents can autonomously:

1. **Provision** - Search, filter, and create instances matching workload requirements
2. **Deploy** - Stream code/data to instances via pipeable `cp`
3. **Execute** - Run workloads via `brev exec`, capture output
4. **Monitor** - Poll status via `brev ls --json`, stream logs
5. **Scale** - Spin up parallel instances, distribute work
6. **Cleanup** - Stop/delete instances, manage costs

### Example: Autonomous Training Job

```
User: "Train my model on an H100, save checkpoints every hour"

Agent:
1. brev search --gpu-name H100 --stoppable --min-disk 500 | head -1 | brev create --name training-job
2. brev wait training-job --state ready
3. tar czf - ./src | brev cp - training-job:/app/
4. brev exec training-job "cd /app && python train.py --checkpoint-interval 3600"
5. brev cp training-job:/app/checkpoints - | tar xzf - -C ./results/
6. brev delete training-job
```

The skill handles the translation, error recovery, and orchestration—the composable CLI makes each step possible.

## Future Considerations

### Planned

#### `brev logs` - Stream/tail instance logs
```bash
brev logs my-gpu # Follow logs
brev logs my-gpu --since 5m # Last 5 minutes
brev logs my-gpu | grep ERROR # Filter logs
```

#### `brev wait` - Block until instance reaches state
```bash
brev create --name my-gpu ... && brev wait my-gpu --state ready
brev stop my-gpu && brev wait my-gpu --state stopped
```

#### `brev cp` - Pipeable file copy (stdin/stdout)

Stream data directly through stdin/stdout without intermediate files. Uses `-` to indicate stdin/stdout (standard Unix convention).

**Current behavior** (requires temp files):
```bash
brev cp local.tar.gz my-gpu:/data/
brev cp my-gpu:/results/output.csv ./output.csv
```

**Proposed pipeable behavior**:
```bash
# Stream archive directly to instance
tar czf - ./data | brev cp - my-gpu:/data/archive.tar.gz

# Pipe file content to instance
cat model.pt | brev cp - my-gpu:/models/model.pt

# Stream from instance and process locally
brev cp my-gpu:/results/output.csv - | grep "success" > filtered.csv

# Transfer between instances without local storage
brev cp gpu-1:/checkpoint.pt - | brev cp - gpu-2:/checkpoint.pt
```

**Agentic use cases**:
```bash
# Agent streams training data, captures results
cat dataset.jsonl | brev exec my-gpu "python train.py" > results.log

# Agent deploys code without temp files
tar czf - ./src | brev cp - my-gpu:/app/src.tar.gz

# Agent extracts specific results
brev cp my-gpu:/logs/metrics.json - | jq '.accuracy'
```
2 changes: 2 additions & 0 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/brevdev/brev-cli/pkg/cmd/create"
"github.com/brevdev/brev-cli/pkg/cmd/delete"
"github.com/brevdev/brev-cli/pkg/cmd/envvars"
"github.com/brevdev/brev-cli/pkg/cmd/exec"
"github.com/brevdev/brev-cli/pkg/cmd/fu"
"github.com/brevdev/brev-cli/pkg/cmd/healthcheck"
"github.com/brevdev/brev-cli/pkg/cmd/hello"
Expand Down Expand Up @@ -273,6 +274,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
cmd.AddCommand(configureenvvars.NewCmdConfigureEnvVars(t, loginCmdStore))
cmd.AddCommand(importideconfig.NewCmdImportIDEConfig(t, noLoginCmdStore))
cmd.AddCommand(shell.NewCmdShell(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(exec.NewCmdExec(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(copy.NewCmdCopy(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(open.NewCmdOpen(t, loginCmdStore, noLoginCmdStore))
cmd.AddCommand(ollama.NewCmdOllama(t, loginCmdStore))
Expand Down
6 changes: 3 additions & 3 deletions pkg/cmd/cmderrors/cmderrors.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func DisplayAndHandleError(err error) {
case *breverrors.NvidiaMigrationError:
// Handle nvidia migration error
if nvErr, ok := errors.Cause(err).(*breverrors.NvidiaMigrationError); ok {
fmt.Println("\n This account has been migrated to NVIDIA Auth. Attempting to log in with NVIDIA account...")
fmt.Fprintln(os.Stderr, "\n This account has been migrated to NVIDIA Auth. Attempting to log in with NVIDIA account...")
brevBin, err1 := os.Executable()
if err1 == nil {
cmd := exec.Command(brevBin, "login", "--auth", "nvidia") // #nosec G204
Expand Down Expand Up @@ -68,9 +68,9 @@ func DisplayAndHandleError(err error) {
}
}
if featureflag.Debug() || featureflag.IsDev() {
fmt.Println(err)
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println(prettyErr)
fmt.Fprintln(os.Stderr, prettyErr)
}
}
}
Expand Down
Loading
Loading