389 lines
10 KiB
Python
Executable File
389 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Import Fabric patterns as CmdForge tools.
|
|
|
|
Usage:
|
|
# Import all patterns
|
|
python scripts/import_fabric.py --all
|
|
|
|
# Import specific pattern(s)
|
|
python scripts/import_fabric.py summarize extract_wisdom
|
|
|
|
# List available patterns
|
|
python scripts/import_fabric.py --list
|
|
|
|
# Dry run (show what would be created)
|
|
python scripts/import_fabric.py --all --dry-run
|
|
|
|
# Specify output directory
|
|
python scripts/import_fabric.py --all --output /tmp/fabric-tools
|
|
|
|
# Specify default provider
|
|
python scripts/import_fabric.py --all --provider opencode-deepseek
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import yaml
|
|
|
|
|
|
# Fabric repo URL
|
|
FABRIC_REPO = "https://github.com/danielmiessler/fabric.git"
|
|
|
|
# Default provider for imported tools
|
|
DEFAULT_PROVIDER = "opencode-pickle"
|
|
|
|
# Category mapping based on pattern name prefixes/keywords
|
|
CATEGORY_RULES = [
|
|
(r"^analyze_", "Data"),
|
|
(r"^extract_", "Data"),
|
|
(r"^create_", "Developer"),
|
|
(r"^write_", "Text"),
|
|
(r"^improve_", "Text"),
|
|
(r"^summarize", "Text"),
|
|
(r"^explain_", "Text"),
|
|
(r"^review_", "Developer"),
|
|
(r"^code", "Developer"),
|
|
(r"^debug", "Developer"),
|
|
(r"^test", "Developer"),
|
|
(r"rate_", "Data"),
|
|
(r"json", "Data"),
|
|
(r"csv", "Data"),
|
|
(r"sql", "Data"),
|
|
(r"translate", "Text"),
|
|
(r"transcri", "Text"),
|
|
]
|
|
|
|
|
|
def get_category(pattern_name: str) -> str:
|
|
"""Determine category based on pattern name."""
|
|
name_lower = pattern_name.lower()
|
|
for regex, category in CATEGORY_RULES:
|
|
if re.search(regex, name_lower):
|
|
return category
|
|
return "Other"
|
|
|
|
|
|
def pattern_to_display_name(pattern_name: str) -> str:
|
|
"""Convert pattern name to display name."""
|
|
# Replace underscores with spaces and title case
|
|
return pattern_name.replace("_", " ").title()
|
|
|
|
|
|
def clean_prompt(prompt: str) -> str:
|
|
"""Clean up the Fabric prompt for use in CmdForge."""
|
|
# Remove trailing INPUT: placeholder (we'll add {input} ourselves)
|
|
prompt = prompt.strip()
|
|
|
|
# Remove various forms of input placeholder at the end
|
|
for suffix in ["INPUT:", "# INPUT", "# INPUT:", "INPUT"]:
|
|
if prompt.endswith(suffix):
|
|
prompt = prompt[:-len(suffix)].strip()
|
|
|
|
return prompt
|
|
|
|
|
|
def create_tool_config(
|
|
pattern_name: str,
|
|
system_prompt: str,
|
|
provider: str = DEFAULT_PROVIDER,
|
|
) -> dict:
|
|
"""Create a CmdForge tool config from a Fabric pattern."""
|
|
|
|
cleaned_prompt = clean_prompt(system_prompt)
|
|
display_name = pattern_to_display_name(pattern_name)
|
|
category = get_category(pattern_name)
|
|
|
|
# Build the full prompt with input placeholder
|
|
full_prompt = f"{cleaned_prompt}\n\n{{input}}"
|
|
|
|
config = {
|
|
"name": pattern_name,
|
|
"description": f"{display_name} - imported from Fabric patterns",
|
|
"version": "1.0.0",
|
|
"category": category,
|
|
|
|
# Attribution - marks this as an imported tool
|
|
"source": {
|
|
"type": "imported",
|
|
"license": "MIT",
|
|
"url": "https://github.com/danielmiessler/fabric",
|
|
"author": "Daniel Miessler",
|
|
"original_tool": f"fabric/patterns/{pattern_name}",
|
|
},
|
|
|
|
"arguments": [],
|
|
|
|
"steps": [
|
|
{
|
|
"type": "prompt",
|
|
"prompt": full_prompt,
|
|
"provider": provider,
|
|
"output_var": "response",
|
|
}
|
|
],
|
|
|
|
"output": "{response}",
|
|
}
|
|
|
|
return config
|
|
|
|
|
|
def clone_fabric(target_dir: Path) -> Path:
|
|
"""Clone or update Fabric repo, return patterns directory."""
|
|
fabric_dir = target_dir / "fabric"
|
|
patterns_dir = fabric_dir / "data" / "patterns"
|
|
|
|
if fabric_dir.exists():
|
|
print(f"Using existing Fabric clone at {fabric_dir}")
|
|
# Pull latest
|
|
subprocess.run(
|
|
["git", "-C", str(fabric_dir), "pull", "--quiet"],
|
|
check=False
|
|
)
|
|
else:
|
|
print(f"Cloning Fabric to {fabric_dir}...")
|
|
subprocess.run(
|
|
["git", "clone", "--depth", "1", FABRIC_REPO, str(fabric_dir)],
|
|
check=True,
|
|
capture_output=True
|
|
)
|
|
|
|
if not patterns_dir.exists():
|
|
raise RuntimeError(f"Patterns directory not found at {patterns_dir}")
|
|
|
|
return patterns_dir
|
|
|
|
|
|
def list_patterns(patterns_dir: Path) -> list[str]:
|
|
"""List all available pattern names."""
|
|
patterns = []
|
|
for entry in sorted(patterns_dir.iterdir()):
|
|
if entry.is_dir() and (entry / "system.md").exists():
|
|
patterns.append(entry.name)
|
|
return patterns
|
|
|
|
|
|
def import_pattern(
|
|
pattern_name: str,
|
|
patterns_dir: Path,
|
|
output_dir: Path,
|
|
provider: str,
|
|
dry_run: bool = False,
|
|
registry_format: bool = False,
|
|
namespace: str = "official",
|
|
) -> bool:
|
|
"""Import a single pattern. Returns True on success."""
|
|
pattern_path = patterns_dir / pattern_name
|
|
system_md = pattern_path / "system.md"
|
|
|
|
if not system_md.exists():
|
|
print(f" ✗ Pattern '{pattern_name}' not found", file=sys.stderr)
|
|
return False
|
|
|
|
# Read the system prompt
|
|
system_prompt = system_md.read_text()
|
|
|
|
# Create tool config
|
|
config = create_tool_config(pattern_name, system_prompt, provider)
|
|
|
|
# Output directory for this tool
|
|
if registry_format:
|
|
# Registry format: tools/<namespace>/<name>/config.yaml
|
|
tool_dir = output_dir / "tools" / namespace / pattern_name
|
|
else:
|
|
# Local format: <output>/<name>/config.yaml
|
|
tool_dir = output_dir / pattern_name
|
|
config_file = tool_dir / "config.yaml"
|
|
|
|
if dry_run:
|
|
print(f" [DRY RUN] Would create: {config_file}")
|
|
print(f" Category: {config['category']}")
|
|
print(f" Provider: {provider}")
|
|
return True
|
|
|
|
# Create tool directory
|
|
tool_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Write config.yaml with proper multi-line string handling
|
|
class LiteralStr(str):
|
|
pass
|
|
|
|
def literal_representer(dumper, data):
|
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
|
|
|
yaml.add_representer(LiteralStr, literal_representer)
|
|
|
|
# Convert the prompt to literal style
|
|
config["steps"][0]["prompt"] = LiteralStr(config["steps"][0]["prompt"])
|
|
|
|
with open(config_file, "w") as f:
|
|
yaml.dump(config, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
|
|
# Create a basic README.md
|
|
readme_content = f"""# {pattern_to_display_name(pattern_name)}
|
|
|
|
{config['description']}
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
cat input.txt | {pattern_name}
|
|
```
|
|
|
|
## Source
|
|
|
|
This tool was imported from [Fabric](https://github.com/danielmiessler/fabric) patterns.
|
|
|
|
- **Original pattern**: `{pattern_name}`
|
|
- **Author**: Daniel Miessler
|
|
- **License**: MIT
|
|
"""
|
|
(tool_dir / "README.md").write_text(readme_content)
|
|
|
|
print(f" ✓ {pattern_name} -> {tool_dir}")
|
|
return True
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Import Fabric patterns as CmdForge tools",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__
|
|
)
|
|
|
|
parser.add_argument(
|
|
"patterns",
|
|
nargs="*",
|
|
help="Pattern name(s) to import"
|
|
)
|
|
parser.add_argument(
|
|
"--all",
|
|
action="store_true",
|
|
help="Import all patterns"
|
|
)
|
|
parser.add_argument(
|
|
"--list",
|
|
action="store_true",
|
|
help="List available patterns"
|
|
)
|
|
parser.add_argument(
|
|
"--output", "-o",
|
|
type=Path,
|
|
default=Path.home() / ".cmdforge",
|
|
help="Output directory (default: ~/.cmdforge)"
|
|
)
|
|
parser.add_argument(
|
|
"--provider", "-p",
|
|
default=DEFAULT_PROVIDER,
|
|
help=f"Default provider for tools (default: {DEFAULT_PROVIDER})"
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would be created without writing files"
|
|
)
|
|
parser.add_argument(
|
|
"--fabric-dir",
|
|
type=Path,
|
|
default=Path("/tmp/fabric-import"),
|
|
help="Directory to clone Fabric repo (default: /tmp/fabric-import)"
|
|
)
|
|
parser.add_argument(
|
|
"--registry",
|
|
action="store_true",
|
|
help="Output in registry format (tools/official/<name>/config.yaml)"
|
|
)
|
|
parser.add_argument(
|
|
"--namespace",
|
|
default="official",
|
|
help="Namespace for registry format (default: official)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate arguments
|
|
if not args.list and not args.all and not args.patterns:
|
|
parser.error("Specify pattern names, --all, or --list")
|
|
|
|
# Clone/update Fabric
|
|
args.fabric_dir.mkdir(parents=True, exist_ok=True)
|
|
patterns_dir = clone_fabric(args.fabric_dir)
|
|
|
|
# List mode
|
|
if args.list:
|
|
patterns = list_patterns(patterns_dir)
|
|
print(f"\nAvailable Fabric patterns ({len(patterns)}):\n")
|
|
|
|
# Group by category
|
|
by_category = {}
|
|
for p in patterns:
|
|
cat = get_category(p)
|
|
by_category.setdefault(cat, []).append(p)
|
|
|
|
for cat in sorted(by_category.keys()):
|
|
print(f" {cat}:")
|
|
for p in sorted(by_category[cat]):
|
|
print(f" - {p}")
|
|
print()
|
|
|
|
return 0
|
|
|
|
# Determine which patterns to import
|
|
if args.all:
|
|
to_import = list_patterns(patterns_dir)
|
|
else:
|
|
to_import = args.patterns
|
|
|
|
if not to_import:
|
|
print("No patterns to import.", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"\nImporting {len(to_import)} pattern(s) to {args.output}")
|
|
print(f"Provider: {args.provider}")
|
|
if args.dry_run:
|
|
print("(DRY RUN - no files will be written)\n")
|
|
else:
|
|
print()
|
|
|
|
# Import each pattern
|
|
success = 0
|
|
failed = 0
|
|
|
|
for pattern in to_import:
|
|
if import_pattern(
|
|
pattern,
|
|
patterns_dir,
|
|
args.output,
|
|
args.provider,
|
|
args.dry_run,
|
|
args.registry,
|
|
args.namespace
|
|
):
|
|
success += 1
|
|
else:
|
|
failed += 1
|
|
|
|
print(f"\nDone: {success} imported, {failed} failed")
|
|
|
|
if not args.dry_run and success > 0:
|
|
print(f"\nNext steps:")
|
|
print(f" 1. Review generated tools in {args.output}")
|
|
print(f" 2. Run 'cmdforge refresh' to create wrapper scripts")
|
|
print(f" 3. Test with: cmdforge test <pattern_name>")
|
|
print(f" 4. Publish with: cmdforge registry publish {args.output}/<pattern_name>")
|
|
|
|
return 0 if failed == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|