smarttools/examples/install.py

343 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Install all SmartTools example tools.
Usage:
python3 install.py # Install all tools
python3 install.py --list # List available tools
python3 install.py summarize # Install specific tool
python3 install.py --category dev # Install category
Run from anywhere:
curl -sSL https://raw.githubusercontent.com/yourusername/smarttools/main/examples/install.py | python3
"""
import argparse
import sys
from pathlib import Path
try:
import yaml
except ImportError:
print("PyYAML required. Install with: pip install pyyaml")
sys.exit(1)
TOOLS_DIR = Path.home() / ".smarttools"
# All example tool configurations
TOOLS = {
# TEXT PROCESSING
"summarize": {
"category": "text",
"description": "Condense long documents to key points",
"arguments": [{"flag": "--length", "variable": "length", "default": "3-5 bullet points"}],
"steps": [{"type": "prompt", "prompt": "Summarize the following text into {length}. Be concise and capture the key points:\n\n{input}", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
"translate": {
"category": "text",
"description": "Translate text to any language",
"arguments": [{"flag": "--lang", "variable": "lang", "default": "Spanish"}],
"steps": [{"type": "prompt", "prompt": "Translate the following text to {lang}. Only output the translation, nothing else:\n\n{input}", "provider": "claude-haiku", "output_var": "response"}],
"output": "{response}"
},
"fix-grammar": {
"category": "text",
"description": "Fix grammar and spelling errors",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Fix all grammar, spelling, and punctuation errors in the following text. Only output the corrected text, no explanations:\n\n{input}", "provider": "opencode-deepseek", "output_var": "response"}],
"output": "{response}"
},
"simplify": {
"category": "text",
"description": "Rewrite text for easier understanding",
"arguments": [{"flag": "--level", "variable": "level", "default": "5th grade reading level"}],
"steps": [{"type": "prompt", "prompt": "Rewrite the following text for a {level}. Keep the meaning but use simpler words and shorter sentences:\n\n{input}", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
"tone-shift": {
"category": "text",
"description": "Change the tone of text",
"arguments": [{"flag": "--tone", "variable": "tone", "default": "professional"}],
"steps": [{"type": "prompt", "prompt": "Rewrite the following text in a {tone} tone. Keep the core message but adjust the style:\n\n{input}", "provider": "opencode-deepseek", "output_var": "response"}],
"output": "{response}"
},
"eli5": {
"category": "text",
"description": "Explain like I'm 5",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Explain this like I'm 5 years old. Use simple words and fun analogies:\n\n{input}", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
"tldr": {
"category": "text",
"description": "One-line summary",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Give a one-line TL;DR summary of this text:\n\n{input}", "provider": "opencode-grok", "output_var": "response"}],
"output": "{response}"
},
"expand": {
"category": "text",
"description": "Expand bullet points to paragraphs",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Expand these bullet points into well-written paragraphs:\n\n{input}", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
# DEVELOPER TOOLS
"explain-error": {
"category": "dev",
"description": "Explain error messages and stack traces",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Explain this error/stack trace in plain English. What went wrong and how to fix it:\n\n{input}", "provider": "claude-haiku", "output_var": "response"}],
"output": "{response}"
},
"explain-code": {
"category": "dev",
"description": "Explain what code does",
"arguments": [{"flag": "--detail", "variable": "detail", "default": "moderate"}],
"steps": [{"type": "prompt", "prompt": "Explain what this code does at a {detail} level of detail:\n\n```\n{input}\n```", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
"review-code": {
"category": "dev",
"description": "Quick code review with suggestions",
"arguments": [{"flag": "--focus", "variable": "focus", "default": "bugs, security, and improvements"}],
"steps": [{"type": "prompt", "prompt": "Review this code focusing on {focus}. Be concise and actionable:\n\n```\n{input}\n```", "provider": "claude-sonnet", "output_var": "response"}],
"output": "{response}"
},
"gen-tests": {
"category": "dev",
"description": "Generate unit tests for code",
"arguments": [{"flag": "--framework", "variable": "framework", "default": "pytest"}],
"steps": [{"type": "prompt", "prompt": "Generate comprehensive unit tests for this code using {framework}. Include edge cases:\n\n```\n{input}\n```", "provider": "claude-haiku", "output_var": "response"}],
"output": "{response}"
},
"docstring": {
"category": "dev",
"description": "Add docstrings to functions/classes",
"arguments": [{"flag": "--style", "variable": "style", "default": "Google style"}],
"steps": [{"type": "prompt", "prompt": "Add {style} docstrings to all functions and classes in this code. Output the complete code with docstrings:\n\n```\n{input}\n```", "provider": "opencode-deepseek", "output_var": "response"}],
"output": "{response}"
},
"commit-msg": {
"category": "dev",
"description": "Generate commit message from diff",
"arguments": [{"flag": "--style", "variable": "style", "default": "conventional commits"}],
"steps": [{"type": "prompt", "prompt": "Generate a concise {style} commit message for this diff. Just the message, no explanation:\n\n{input}", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
# DATA TOOLS
"json-extract": {
"category": "data",
"description": "Extract structured data as validated JSON",
"arguments": [{"flag": "--fields", "variable": "fields", "default": "any relevant fields"}],
"steps": [
{"type": "prompt", "prompt": "Extract {fields} from this text as a JSON object. Output ONLY valid JSON, no markdown, no explanation:\n\n{input}", "provider": "opencode-deepseek", "output_var": "raw_json"},
{"type": "code", "code": "import json\nimport re\ntext = raw_json.strip()\ntext = re.sub(r'^```json?\\s*', '', text)\ntext = re.sub(r'\\s*```$', '', text)\ntry:\n parsed = json.loads(text)\n validated = json.dumps(parsed, indent=2)\nexcept json.JSONDecodeError as e:\n validated = f\"ERROR: Invalid JSON - {e}\\nRaw output: {text[:500]}\"", "output_var": "validated"}
],
"output": "{validated}"
},
"json2csv": {
"category": "data",
"description": "Convert JSON to CSV format",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Convert this JSON to CSV format. Output only the CSV, no explanation:\n\n{input}", "provider": "opencode-deepseek", "output_var": "response"}],
"output": "{response}"
},
"extract-emails": {
"category": "data",
"description": "Extract email addresses from text",
"arguments": [],
"steps": [{"type": "prompt", "prompt": "Extract all email addresses from this text. Output one email per line, nothing else:\n\n{input}", "provider": "opencode-pickle", "output_var": "response"}],
"output": "{response}"
},
"extract-contacts": {
"category": "data",
"description": "Extract contact info as structured CSV",
"arguments": [],
"steps": [
{"type": "prompt", "prompt": "Extract all contact information (name, email, phone, company) from this text. Output as JSON array with objects having keys: name, email, phone, company. Use null for missing fields. Output ONLY the JSON array:\n\n{input}", "provider": "opencode-pickle", "output_var": "contacts_json"},
{"type": "code", "code": "import json\nimport re\ntext = contacts_json.strip()\ntext = re.sub(r'^```json?\\s*', '', text)\ntext = re.sub(r'\\s*```$', '', text)\ntry:\n contacts = json.loads(text)\n lines = ['name,email,phone,company']\n for c in contacts:\n row = [str(c.get('name') or ''), str(c.get('email') or ''), str(c.get('phone') or ''), str(c.get('company') or '')]\n lines.append(','.join(f'\"{f}\"' for f in row))\n csv_output = '\\n'.join(lines)\nexcept:\n csv_output = f\"Error parsing contacts: {text[:200]}\"", "output_var": "csv_output"}
],
"output": "{csv_output}"
},
"sql-from-text": {
"category": "data",
"description": "Generate SQL from natural language",
"arguments": [{"flag": "--dialect", "variable": "dialect", "default": "PostgreSQL"}],
"steps": [{"type": "prompt", "prompt": "Generate a {dialect} SQL query for this request. Output only the SQL, no explanation:\n\n{input}", "provider": "claude-haiku", "output_var": "response"}],
"output": "{response}"
},
"safe-sql": {
"category": "data",
"description": "Generate SQL with safety checks",
"arguments": [{"flag": "--dialect", "variable": "dialect", "default": "PostgreSQL"}],
"steps": [
{"type": "prompt", "prompt": "Generate a {dialect} SELECT query for: {input}\n\nOutput ONLY the SQL query, no explanation.", "provider": "claude-haiku", "output_var": "raw_sql"},
{"type": "code", "code": "import re\nsql = raw_sql.strip()\nsql = re.sub(r'^```sql?\\s*', '', sql)\nsql = re.sub(r'\\s*```$', '', sql)\ndangerous = ['DROP', 'DELETE', 'TRUNCATE', 'UPDATE', 'INSERT', 'ALTER', 'CREATE', 'GRANT']\nwarnings = []\nfor keyword in dangerous:\n if re.search(r'\\b' + keyword + r'\\b', sql, re.I):\n warnings.append(f\"WARNING: Contains {keyword}\")\nif warnings:\n validated = '\\n'.join(warnings) + '\\n\\n' + sql\nelse:\n validated = sql", "output_var": "validated"}
],
"output": "{validated}"
},
"parse-log": {
"category": "data",
"description": "Summarize and analyze log files",
"arguments": [{"flag": "--focus", "variable": "focus", "default": "errors and warnings"}],
"steps": [{"type": "prompt", "prompt": "Analyze these logs focusing on {focus}. Summarize key issues and patterns:\n\n{input}", "provider": "claude-haiku", "output_var": "response"}],
"output": "{response}"
},
"csv-insights": {
"category": "data",
"description": "Analyze CSV with sampled data for large files",
"arguments": [{"flag": "--question", "variable": "question", "default": "What patterns and insights can you find?"}],
"steps": [
{"type": "code", "code": "import random\nlines = input.strip().split('\\n')\nheader = lines[0] if lines else ''\ndata_lines = lines[1:] if len(lines) > 1 else []\nif len(data_lines) > 50:\n sampled = random.sample(data_lines, 50)\n sample_note = f\"(Sampled 50 of {len(data_lines)} rows)\"\nelse:\n sampled = data_lines\n sample_note = f\"({len(data_lines)} rows)\"\nsampled_csv = header + '\\n' + '\\n'.join(sampled)\nstats = f\"Columns: {len(header.split(','))}, Rows: {len(data_lines)} {sample_note}\"", "output_var": "sampled_csv,stats"},
{"type": "prompt", "prompt": "Analyze this CSV data. {stats}\n\nQuestion: {question}\n\nData:\n{sampled_csv}", "provider": "claude-haiku", "output_var": "response"}
],
"output": "{response}"
},
# ADVANCED TOOLS
"log-errors": {
"category": "advanced",
"description": "Extract and explain errors from large log files",
"arguments": [],
"steps": [
{"type": "code", "code": "import re\nlines = input.split('\\n')\nerrors = [l for l in lines if re.search(r'\\b(ERROR|CRITICAL|FATAL|Exception|Traceback)\\b', l, re.I)]\nresult = []\nfor i, line in enumerate(lines):\n if re.search(r'\\b(ERROR|CRITICAL|FATAL|Exception|Traceback)\\b', line, re.I):\n result.extend(lines[i:i+5])\nextracted = '\\n'.join(result[:200])", "output_var": "extracted"},
{"type": "prompt", "prompt": "Analyze these error log entries. Group by error type, explain likely causes, and suggest fixes:\n\n{extracted}", "provider": "claude-haiku", "output_var": "response"}
],
"output": "{response}"
},
"diff-focus": {
"category": "advanced",
"description": "Review only the added/changed code in a diff",
"arguments": [],
"steps": [
{"type": "code", "code": "lines = input.split('\\n')\nresult = []\nfor i, line in enumerate(lines):\n if line.startswith('@@') or line.startswith('+++') or line.startswith('---'):\n result.append(line)\n elif line.startswith('+') and not line.startswith('+++'):\n result.append(line)\nextracted = '\\n'.join(result)", "output_var": "extracted"},
{"type": "prompt", "prompt": "Review these added lines of code. Focus on bugs, security issues, and improvements:\n\n{extracted}", "provider": "claude-haiku", "output_var": "response"}
],
"output": "{response}"
},
"changelog": {
"category": "advanced",
"description": "Generate changelog from git commits",
"arguments": [{"flag": "--since", "variable": "since", "default": "last week"}],
"steps": [
{"type": "code", "code": "lines = input.strip().split('\\n')\ncommits = []\nfor line in lines:\n if line.strip() and not line.startswith('commit '):\n commits.append(line.strip())\ngit_log = '\\n'.join(commits[:100])", "output_var": "git_log"},
{"type": "prompt", "prompt": "Generate a user-friendly changelog from these git commits. Group by category (Features, Fixes, Improvements, etc). Use markdown with bullet points:\n\n{git_log}", "provider": "opencode-pickle", "output_var": "changelog_raw"},
{"type": "code", "code": "from datetime import datetime\nheader = f\"\"\"# Changelog\\n\\nGenerated: {datetime.now().strftime('%Y-%m-%d')}\\n\\n\"\"\"\nformatted = header + changelog_raw", "output_var": "formatted"}
],
"output": "{formatted}"
},
"code-validate": {
"category": "advanced",
"description": "Generate and validate Python code",
"arguments": [{"flag": "--task", "variable": "task", "default": ""}],
"steps": [
{"type": "prompt", "prompt": "Write Python code to: {task}\n\nContext/input data:\n{input}\n\nOutput ONLY the Python code, no markdown, no explanation.", "provider": "claude-haiku", "output_var": "raw_code"},
{"type": "code", "code": "import ast\nimport re\ncode = raw_code.strip()\ncode = re.sub(r'^```python?\\s*', '', code)\ncode = re.sub(r'\\s*```$', '', code)\ntry:\n ast.parse(code)\n validated = code\nexcept SyntaxError as e:\n validated = f\"# SYNTAX ERROR at line {e.lineno}: {e.msg}\\n# Fix needed:\\n\\n{code}\"", "output_var": "validated"}
],
"output": "{validated}"
},
"reply-email": {
"category": "text",
"description": "Draft email replies",
"arguments": [{"flag": "--tone", "variable": "tone", "default": "professional and friendly"}],
"steps": [{"type": "prompt", "prompt": "Draft a {tone} reply to this email:\n\n{input}", "provider": "opencode-deepseek", "output_var": "response"}],
"output": "{response}"
},
}
CATEGORIES = {
"text": "Text Processing",
"dev": "Developer Tools",
"data": "Data Tools",
"advanced": "Advanced Multi-Step Tools",
}
def install_tool(name: str, config: dict, force: bool = False) -> bool:
"""Install a single tool."""
tool_dir = TOOLS_DIR / name
config_file = tool_dir / "config.yaml"
if config_file.exists() and not force:
print(f" {name}: already exists (use --force to overwrite)")
return False
tool_dir.mkdir(parents=True, exist_ok=True)
# Build config without category
tool_config = {
"name": name,
"description": config["description"],
"arguments": config.get("arguments", []),
"steps": config["steps"],
"output": config["output"]
}
with open(config_file, 'w') as f:
yaml.dump(tool_config, f, default_flow_style=False, sort_keys=False)
print(f" {name}: installed")
return True
def list_tools():
"""List all available tools."""
print("Available example tools:\n")
for cat_id, cat_name in CATEGORIES.items():
tools_in_cat = [(n, c) for n, c in TOOLS.items() if c.get("category") == cat_id]
if tools_in_cat:
print(f"{cat_name}:")
for name, config in tools_in_cat:
print(f" {name:<20} {config['description']}")
print()
def main():
parser = argparse.ArgumentParser(description="Install SmartTools example tools")
parser.add_argument("tools", nargs="*", help="Specific tools to install (default: all)")
parser.add_argument("--list", action="store_true", help="List available tools")
parser.add_argument("--category", "-c", choices=list(CATEGORIES.keys()), help="Install tools from category")
parser.add_argument("--force", "-f", action="store_true", help="Overwrite existing tools")
args = parser.parse_args()
if args.list:
list_tools()
return
# Determine which tools to install
if args.tools:
to_install = {n: c for n, c in TOOLS.items() if n in args.tools}
unknown = set(args.tools) - set(TOOLS.keys())
if unknown:
print(f"Unknown tools: {', '.join(unknown)}")
print("Use --list to see available tools")
return
elif args.category:
to_install = {n: c for n, c in TOOLS.items() if c.get("category") == args.category}
else:
to_install = TOOLS
if not to_install:
print("No tools to install")
return
print(f"Installing {len(to_install)} tools to {TOOLS_DIR}...\n")
installed = 0
for name, config in to_install.items():
if install_tool(name, config, args.force):
installed += 1
print(f"\nInstalled {installed} tools.")
print("\nRun 'smarttools refresh' to create executable wrappers.")
if __name__ == "__main__":
main()