Skip to content
Open
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
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ strands-agents-sops skills --output-dir ./skills
strands-agents-sops skills --sop-paths ~/my-sops --output-dir ./skills
```

### Cursor IDE Integration
```bash
# Generate Cursor commands from built-in SOPs (default: .cursor/commands)
strands-agents-sops cursor

# Specify custom output directory
strands-agents-sops cursor --output-dir .cursor/commands

# Include custom SOPs in commands generation
strands-agents-sops cursor --sop-paths ~/my-sops --output-dir .cursor/commands
```

**Usage in Cursor:**
1. Generate commands: Run `strands-agents-sops cursor` in your project root
2. Execute workflows: In Cursor chat, type `/` followed by the command name (e.g., `/code-assist`)
3. Provide parameters: When prompted, provide the required parameters for the workflow

**Note:** Cursor commands don't support explicit parameters, so the AI will prompt you for required inputs when you execute a command. The generated commands include parameter documentation to guide this interaction.

### Python Integration
```python
from strands import Agent
Expand Down
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,114 @@ Then connect your MCP-compatible AI assistant to access SOPs as tools. Here is a

---

## Cursor IDE Integration

Agent SOPs can be converted to Cursor commands, allowing you to execute workflows directly within Cursor IDE using the `/` command prefix.

### Understanding Cursor Rules vs Commands

- **Rules** (`.cursor/rules`): Provide persistent, system-level guidance that's automatically applied. Rules are static and don't support parameters.
- **Commands** (`.cursor/commands`): Reusable workflows triggered with `/` prefix. Commands can prompt users for input, making them ideal for parameterized SOPs.

Since Agent SOPs are parameterized workflows that need user input, they're best implemented as **Commands** rather than Rules.

### Converting SOPs to Cursor Commands

Each Agent SOP can be automatically converted to Cursor command format:

```bash
# Generate Cursor commands from built-in SOPs (default output: .cursor/commands)
strands-agents-sops cursor

# Or specify custom output directory
strands-agents-sops cursor --output-dir .cursor/commands

# Load external SOPs from custom directories
strands-agents-sops cursor --sop-paths ~/my-sops:/path/to/other-sops

# External SOPs override built-in SOPs with same name
strands-agents-sops cursor --sop-paths ~/custom-sops --output-dir .cursor/commands
```

#### External SOP Loading

The `--sop-paths` argument allows you to extend commands generation with your own SOPs:

- **File format**: Only files with `.sop.md` postfix are recognized as SOPs
- **Colon-separated paths**: `~/sops1:/absolute/path:relative/path`
- **Path expansion**: Supports `~` (home directory) and relative paths
- **First-wins precedence**: External SOPs override built-in SOPs with same name
- **Graceful error handling**: Invalid paths or malformed SOPs are skipped with warnings

**Example workflow:**
```bash
# Create your custom SOP
mkdir ~/my-sops
cat > ~/my-sops/custom-workflow.sop.md << 'EOF'
# Custom Workflow
## Overview
My custom workflow for specific tasks.
## Parameters
- **task** (required): Description of task
## Steps
### 1. Custom Step
Do something custom.
EOF

# Generate Cursor commands with your custom SOPs
strands-agents-sops cursor --sop-paths ~/my-sops
```

This creates command files in `.cursor/commands/`:
```
.cursor/commands/
├── code-assist.md
├── codebase-summary.md
├── code-task-generator.md
├── pdd.md
└── custom-workflow.md
```

### Using Commands in Cursor

1. **Generate commands**: Run `strands-agents-sops cursor` in your project root
2. **Execute workflows**: In Cursor chat, type `/` followed by the command name (e.g., `/code-assist`)
3. **Provide parameters**: When prompted, provide the required parameters for the workflow

**Example:**
```
You: /code-assist

AI: I'll help you implement code using the code-assist workflow.
Please provide the following required parameters:
- task_description: [description of the task]
- mode (optional, default: "auto"): "interactive" or "auto"

You: task_description: "Create a user authentication system"
mode: "interactive"
```

### Command Format

Each generated command includes:
- Clear usage instructions
- Parameter documentation (required and optional)
- Full SOP content for execution

The commands handle parameters by prompting users when executed, since Cursor doesn't support explicit parameter passing. The SOP's "Constraints for parameter acquisition" section guides this interaction.

### Parameter Handling

Since Cursor commands don't support explicit parameters, the generated commands include instructions to prompt users for required inputs. The AI assistant will:
1. Read the command file when you type `/command-name`
2. Identify required and optional parameters from the SOP
3. Prompt you for all required parameters upfront
4. Execute the workflow with the provided parameters

This approach maintains the parameterized nature of SOPs while working within Cursor's command system.

---

## Anthropic Skills Integration

Agent SOPs are fully compatible with Claude's [Skills system](https://support.claude.com/en/articles/12512176-what-are-skills), allowing you to teach Claude specialized workflows that can be reused across conversations and projects.
Expand Down
19 changes: 19 additions & 0 deletions python/strands_agents_sops/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse

from .cursor import generate_cursor_commands
from .mcp import run_mcp_server
from .rules import output_rules
from .skills import generate_anthropic_skills
Expand Down Expand Up @@ -37,13 +38,31 @@ def main():
# Rules output command
subparsers.add_parser("rule", help="Output agent SOP authoring rule")

# Cursor commands generation command
cursor_parser = subparsers.add_parser(
"cursor", help="Generate Cursor IDE commands from SOPs"
)
cursor_parser.add_argument(
"--output-dir",
default=".cursor/commands",
help="Output directory for Cursor commands (default: .cursor/commands)",
)
cursor_parser.add_argument(
"--sop-paths",
help="Colon-separated list of directory paths to load external SOPs from. "
"Supports absolute paths, relative paths, and tilde (~) expansion.",
)

args = parser.parse_args()

if args.command == "skills":
sop_paths = getattr(args, "sop_paths", None)
generate_anthropic_skills(args.output_dir, sop_paths=sop_paths)
elif args.command == "rule":
output_rules()
elif args.command == "cursor":
sop_paths = getattr(args, "sop_paths", None)
generate_cursor_commands(args.output_dir, sop_paths=sop_paths)
else:
# Default to MCP server
sop_paths = getattr(args, "sop_paths", None)
Expand Down
155 changes: 155 additions & 0 deletions python/strands_agents_sops/cursor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import logging
import re
from pathlib import Path

from .utils import expand_sop_paths, load_external_sops

logger = logging.getLogger(__name__)


def generate_cursor_commands(output_dir: str, sop_paths: str | None = None):
"""Generate Cursor commands from SOPs

Args:
output_dir: Output directory for Cursor commands (typically .cursor/commands)
sop_paths: Optional colon-separated string of external SOP directory paths
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)

processed_sops = set() # Track processed SOP names for first-wins behavior

# Process external SOPs first (higher precedence)
if sop_paths:
external_directories = expand_sop_paths(sop_paths)
external_sops = load_external_sops(external_directories)

for sop in external_sops:
if sop["name"] not in processed_sops:
processed_sops.add(sop["name"])
_create_command_file(
output_path, sop["name"], sop["content"], sop["description"]
)

# Process built-in SOPs last (lower precedence)
sops_dir = Path(__file__).parent / "sops"
for sop_file in sops_dir.glob("*.sop.md"):
command_name = sop_file.stem.removesuffix(".sop")

if command_name not in processed_sops:
processed_sops.add(command_name)
content = sop_file.read_text()

# Extract overview/description
overview_match = re.search(
r"## Overview\s*\n(.*?)(?=\n##|\n#|\Z)", content, re.DOTALL
)
if not overview_match:
raise ValueError(f"No Overview section found in {sop_file.name}")

description = overview_match.group(1).strip().replace("\n", " ")
_create_command_file(output_path, command_name, content, description)

print(f"\nCursor commands generated in: {output_path.absolute()}")


def _create_command_file(
output_path: Path, command_name: str, sop_content: str, description: str
):
"""Create a Cursor command file from SOP content

Args:
output_path: Directory where command file will be created
command_name: Name of the command (used as filename)
sop_content: Full SOP markdown content
description: Brief description of the SOP
"""
# Extract parameters section to add parameter handling instructions
parameters_section = _extract_parameters_section(sop_content)
parameter_instructions = _generate_parameter_instructions(parameters_section)

# Create command content
# Cursor commands are plain markdown, so we can include the full SOP
# but we'll add a header to make it clear this is a command
command_content = f"""# {command_name.replace('-', ' ').title()}

{description}

## Usage

Type `/` followed by `{command_name}` in the Cursor chat to execute this workflow.

{parameter_instructions}

---

{sop_content}
"""

command_file = output_path / f"{command_name}.md"
command_file.write_text(command_content, encoding="utf-8")
print(f"Created Cursor command: {command_file}")


def _extract_parameters_section(sop_content: str) -> str | None:
"""Extract the Parameters section from SOP content"""
# Match Parameters section until next top-level section
params_match = re.search(
r"## Parameters\s*\n(.*?)(?=\n##|\n#|\Z)", sop_content, re.DOTALL
)
if params_match:
return params_match.group(1).strip()
return None


def _generate_parameter_instructions(parameters_section: str | None) -> str:
"""Generate instructions for handling parameters in Cursor commands"""
if not parameters_section:
return """## Parameters

This workflow does not require any parameters. Simply execute the command to begin."""

# Parse parameters
required_params = []
optional_params = []

# Match parameter definitions: - **param_name** (required|optional[, default: value]): description
# Description can span multiple lines until next parameter or section
param_pattern = r"- \*\*(\w+)\*\* \((required|optional)(?:, default: ([^)]+))?\): (.+?)(?=\n- \*\*|\n\*\*Constraints|\n##|\Z)"

for match in re.finditer(param_pattern, parameters_section, re.DOTALL):
param_name, param_type, default_value, description = match.groups()
# Clean up description - remove extra whitespace and normalize newlines
description = re.sub(r'\s+', ' ', description.strip())
param_info = {
"name": param_name,
"description": description,
"default": default_value.strip() if default_value else None
}

if param_type == "required":
required_params.append(param_info)
else:
optional_params.append(param_info)

instructions = ["## Parameters\n"]

if required_params or optional_params:
instructions.append("When you execute this command, I will prompt you for the following parameters:\n")

if required_params:
instructions.append("### Required Parameters\n")
for param in required_params:
instructions.append(f"- **{param['name']}**: {param['description']}\n")

if optional_params:
instructions.append("### Optional Parameters\n")
for param in optional_params:
default_text = f" (default: {param['default']})" if param['default'] else ""
instructions.append(f"- **{param['name']}**: {param['description']}{default_text}\n")

instructions.append("\n**Note**: Please provide all required parameters when prompted. Optional parameters can be skipped to use their default values.\n")
else:
instructions.append("This workflow does not require any parameters. Simply execute the command to begin.\n")

return "".join(instructions)