"""Documentation content for CmdForge web UI. This module contains the actual documentation text that gets rendered on the /docs pages. Content is stored as markdown-ish HTML for simplicity. """ DOCS = { "getting-started": { "title": "Getting Started", "description": "Learn how to install CmdForge and create your first AI-powered CLI tool", "content": """
CmdForge lets you build custom AI-powered CLI commands using simple YAML configuration. Create tools that work with any AI provider and compose them like Unix pipes.
CmdForge is a lightweight personal tool builder that lets you:
Get up and running in under a minute:
# Install CmdForge
pip install cmdforge
# Create your first tool (choose your style)
cmdforge # Desktop application with visual builder
cmdforge create # CLI wizard
# Or install a tool from the registry
cmdforge registry install official/summarize
# Use it!
cat article.txt | summarize
Two Ways to Build
cmdforge launches the desktop application with a visual builder.
cmdforge create uses a command-line wizard. Both create the same YAML config files.
Each tool is a YAML file that defines:
Here's a simple example:
name: summarize
version: "1.0.0"
description: Summarize text using AI
arguments:
- flag: --max-length
variable: max_length
default: "200"
description: Maximum summary length in words
steps:
- type: prompt
provider: claude
prompt: |
Summarize the following text in {max_length} words or less:
{input}
output_var: summary
output: "{summary}"
Stuck? Have questions? We've got you covered:
CmdForge requires Python 3.8+ and works on Linux, macOS, and Windows.
The simplest way to install CmdForge:
pip install cmdforge
Or with pipx for isolated installation:
pipx install cmdforge
cmdforge --version
cmdforge --help
CmdForge needs at least one AI provider configured. The easiest is Claude CLI:
# Install Claude CLI (if you have an Anthropic API key)
pip install claude-cli
# Or use OpenAI
pip install openai
# Configure your provider
cmdforge config
CmdForge installs wrapper scripts to ~/.local/bin/. Make sure this is in your PATH:
# Add to ~/.bashrc or ~/.zshrc
export PATH="$HOME/.local/bin:$PATH"
To contribute or modify CmdForge:
git clone https://gitea.brrd.tech/rob/CmdForge.git
cd CmdForge
pip install -e ".[dev]"
""",
"headings": [
("pip-install", "Install with pip"),
("verify", "Verify Installation"),
("configure-provider", "Configure a Provider"),
("wrapper-scripts", "Wrapper Scripts Location"),
("development-install", "Development Installation"),
],
},
"first-tool": {
"title": "Your First Tool",
"description": "Create your first CmdForge command step by step",
"parent": "getting-started",
"content": """
Let's create a simple tool that explains code. You'll learn the basics of tool configuration.
Run the interactive creator:
cmdforge create
Or create the file manually at ~/.cmdforge/explain/config.yaml:
name: explain
version: "1.0.0"
description: Explain code or concepts in simple terms
category: code
arguments:
- flag: --level
variable: level
default: "beginner"
description: "Explanation level: beginner, intermediate, or expert"
steps:
- type: prompt
provider: claude
prompt: |
Explain the following in simple terms suitable for a {level}:
{input}
Be concise but thorough. Use examples where helpful.
output_var: explanation
output: "{explanation}"
# Explain some code
echo "def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)" | explain
# Explain for an expert
cat complex_algorithm.py | explain --level expert
Each argument becomes a CLI flag. The variable name is used in templates:
arguments:
- flag: --level # CLI flag: --level beginner
variable: level # Use as {level} in prompts
default: "beginner" # Default if not specified
Steps run in order. Each step can be a prompt or Python code:
steps:
- type: prompt
provider: claude # Which AI to use
prompt: "..." # The prompt template
output_var: result # Store response in {result}
The output template formats the final result:
output: "{explanation}" # Print the explanation variable
Share your tools with the community by publishing to the CmdForge Registry.
Make sure your tool has:
name and descriptionversion (semver format: 1.0.0)README.md file with usage examplesRegister at the registry to get your publisher namespace.
Link your CLI or GUI to your registry account with a simple pairing flow:
# Start the connection flow
cmdforge config connect yourusername
This opens your browser to approve the connection. Once approved, you're ready to publish!
cmdforge (the desktop app)No More Tokens!
The connection flow replaces the old API token system. Your devices stay connected until you disconnect them from your Dashboard → Connections page.
# Publish from CLI
cmdforge registry publish mytool
# Dry run to validate without publishing
cmdforge registry publish mytool --dry-run
# Check your published tools
cmdforge registry my-tools
Or use the desktop GUI:
Submitted tools go through a brief moderation review to ensure quality:
Check your moderation status anytime:
# See status of your submissions
cmdforge registry status
# Sync moderator feedback to your local tool
cmdforge registry status --sync
Published versions are immutable. To update a tool:
config.yamlcmdforge registry publish mytoolCmdForge works with any AI provider that has a CLI interface. Configure providers in
~/.cmdforge/providers.yaml.
Create or edit ~/.cmdforge/providers.yaml:
providers:
- name: claude
command: "claude -p"
- name: openai
command: "openai-cli"
- name: ollama
command: "ollama run llama2"
- name: mock
command: "echo '[MOCK RESPONSE]'"
Specify the provider in your step:
steps:
- type: prompt
provider: claude # Uses the "claude" provider from config
prompt: "..."
output_var: response
# Install
pip install claude-cli
# Configure with your API key
export ANTHROPIC_API_KEY="sk-ant-..."
# providers.yaml
providers:
- name: claude
command: "claude -p"
# Install
pip install openai-cli
# Configure
export OPENAI_API_KEY="sk-..."
# Install Ollama from ollama.ai
# Pull a model
ollama pull llama2
# providers.yaml
providers:
- name: ollama
command: "ollama run llama2"
Use the mock provider to test tools without API calls:
providers:
- name: mock
command: "echo 'This is a mock response for testing'"
| Provider | Best For | Cost |
|---|---|---|
| Claude | Complex reasoning, long context | Pay per token |
| OpenAI | General purpose, fast | Pay per token |
| Ollama | Privacy, offline use | Free (local) |
CmdForge executes steps sequentially within a tool, but you can run multiple tools in parallel using Python's ThreadPoolExecutor. This pattern is ideal for multi-agent workflows, parallel analysis, or any task where you need responses from multiple AI providers simultaneously.
Consider a code review workflow that needs input from multiple perspectives:
Use Python's concurrent.futures to run multiple CmdForge in parallel:
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
def run_tool(tool_name: str, input_text: str) -> dict:
\"\"\"Run a SmartTool and return its output.\"\"\"
result = subprocess.run(
[tool_name],
input=input_text,
capture_output=True,
text=True
)
return {
"tool": tool_name,
"output": result.stdout,
"success": result.returncode == 0
}
def run_parallel(tools: list[str], input_text: str) -> list[dict]:
\"\"\"Run multiple tools in parallel on the same input.\"\"\"
results = []
with ThreadPoolExecutor(max_workers=len(tools)) as executor:
# Submit all tools
futures = {
executor.submit(run_tool, tool, input_text): tool
for tool in tools
}
# Collect results as they complete
for future in as_completed(futures):
results.append(future.result())
return results
# Example usage
tools = ["security-review", "performance-review", "style-review"]
code = open("main.py").read()
reviews = run_parallel(tools, code)
for review in reviews:
print(f"=== {review['tool']} ===")
print(review['output'])
Here's a complete script that gets multiple AI perspectives on a topic:
#!/usr/bin/env python3
\"\"\"Get multiple AI perspectives on a topic in parallel.\"\"\"
import subprocess
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
# Define your perspective tools (each is a SmartTool)
PERSPECTIVES = [
"perspective-optimist", # Focuses on opportunities
"perspective-critic", # Identifies problems
"perspective-pragmatist", # Focuses on actionability
]
def get_perspective(tool: str, topic: str) -> dict:
\"\"\"Get one perspective on a topic.\"\"\"
result = subprocess.run(
[tool],
input=topic,
capture_output=True,
text=True,
timeout=60 # Timeout after 60 seconds
)
return {
"perspective": tool.replace("perspective-", ""),
"response": result.stdout.strip(),
"success": result.returncode == 0
}
def analyze_topic(topic: str) -> list[dict]:
\"\"\"Get all perspectives in parallel.\"\"\"
with ThreadPoolExecutor(max_workers=len(PERSPECTIVES)) as executor:
futures = {
executor.submit(get_perspective, tool, topic): tool
for tool in PERSPECTIVES
}
results = []
for future in as_completed(futures):
try:
results.append(future.result())
except Exception as e:
tool = futures[future]
results.append({
"perspective": tool,
"response": f"Error: {e}",
"success": False
})
return results
if __name__ == "__main__":
import sys
topic = sys.stdin.read() if not sys.stdin.isatty() else input("Topic: ")
print("Gathering perspectives...\\n")
perspectives = analyze_topic(topic)
for p in perspectives:
status = "✓" if p["success"] else "✗"
print(f"[{status}] {p['perspective'].upper()}")
print("-" * 40)
print(p["response"])
print()
For long-running parallel tasks, show progress as tools complete:
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
def run_with_progress(tools: list[str], input_text: str):
\"\"\"Run tools in parallel with progress updates.\"\"\"
total = len(tools)
completed = 0
with ThreadPoolExecutor(max_workers=total) as executor:
futures = {
executor.submit(run_tool, tool, input_text): tool
for tool in tools
}
results = []
for future in as_completed(futures):
completed += 1
tool = futures[future]
result = future.result()
results.append(result)
# Progress update
status = "✓" if result["success"] else "✗"
print(f"[{completed}/{total}] {status} {tool}", file=sys.stderr)
return results
Handle failures gracefully so one tool doesn't break the entire workflow:
def run_tool_safe(tool_name: str, input_text: str, timeout: int = 120) -> dict:
\"\"\"Run a tool with timeout and error handling.\"\"\"
try:
result = subprocess.run(
[tool_name],
input=input_text,
capture_output=True,
text=True,
timeout=timeout
)
return {
"tool": tool_name,
"output": result.stdout,
"error": result.stderr if result.returncode != 0 else None,
"success": result.returncode == 0
}
except subprocess.TimeoutExpired:
return {
"tool": tool_name,
"output": "",
"error": f"Timeout after {timeout}s",
"success": False
}
except FileNotFoundError:
return {
"tool": tool_name,
"output": "",
"error": f"Tool '{tool_name}' not found",
"success": False
}
max_workers to your use caseFor a complete implementation of parallel CmdForge orchestration, see the orchestrated-discussions project. It implements:
Every SmartTool is just a YAML file with a secret superpower: it turns plain English instructions into Unix commands. In the next 10 minutes, you'll understand exactly how that magic works.
What You'll Learn
Let's start with something real. Here's a complete, working tool in just 8 lines:
name: shout
version: "1.0.0"
description: Makes text LOUD
steps:
- type: prompt
provider: claude
prompt: "Convert this to ALL CAPS with enthusiasm: {input}"
output_var: result
output: "{result}"
Save this to ~/.cmdforge/shout/config.yaml and you've got a working command:
$ echo "hello world" | shout
HELLO WORLD!!!
That's it. Everything else is just adding features to this basic pattern.
Think of a SmartTool config like a recipe card:
1. Identity
name, version, description
Who is this tool?
2. Arguments
Custom flags like --format
What options does it accept?
3. Steps
Prompts and code blocks
What does it do?
4. Output
The final template
What comes out?
Let's see all five in action:
# 1. IDENTITY - Who is this tool?
name: translate
version: "1.0.0"
description: Translate text to any language
# 2. ARGUMENTS - What knobs can users turn?
arguments:
- flag: --lang
variable: language
default: "Spanish"
description: Target language
# 3. STEPS - The actual work
steps:
- type: prompt
provider: claude
prompt: |
Translate this text to {language}:
{input}
output_var: translation
# 4. OUTPUT - What comes out the other end
output: "{translation}"
Here's the mental model that makes everything click: variables are pipes that carry data through your tool.
# The user runs:
echo "Hello" | translate --lang French
Inside your tool, three pipes are now flowing:
| Variable | Contains | Source |
|---|---|---|
{input} |
"Hello" | Piped in from stdin |
{language} |
"French" | From --lang flag |
{translation} |
"Bonjour" | Created by the AI step |
You can use any variable in any step that comes after it's created. They flow downstream, never up.
Warning: Unquoted Numbers
YAML is helpful in ways that will hurt you. Watch out for versions:
# WRONG - YAML sees this as the number 1.0
version: 1.0
# RIGHT - YAML sees this as the string "1.0.0"
version: "1.0.0"
Always quote your version numbers. Always. Even if they look fine. Future you will thank present you.
Exercise: Build a Tone Shifter
Create a tool that rewrites text in different tones. It should:
--tone flag (default: "professional")name: tone-shift
version: "1.0.0"
description: Rewrite text in a different tone
arguments:
- flag: --tone
variable: tone
default: "professional"
description: "Target tone (casual, professional, enthusiastic, formal)"
steps:
- type: prompt
provider: claude
prompt: |
Rewrite this text with a {tone} tone.
Keep the meaning but change the style.
Text: {input}
output_var: result
output: "{result}"
Tool names become Unix commands, so they follow Unix conventions:
# GOOD - lowercase, hyphens
name: code-review
name: fix-grammar
name: json2csv
# BAD - these won't work
name: CodeReview # No uppercase
name: fix_grammar # No underscores
name: fix grammar # No spaces
Pick names that complete the sentence: "I need to _____ this file."
You now understand the structure. Time to add superpowers:
The difference between a good tool and a great tool? Options. Let's add
--lang french and --format json to your toolkit.
What You'll Build
A flexible summarizer with adjustable length and style options.
Let's add a --style flag to control how our summary sounds:
name: summarize
version: "1.0.0"
description: Summarize text with style
arguments:
- flag: --style
variable: style
default: "concise"
description: "Style: concise, detailed, or bullet-points"
steps:
- type: prompt
provider: claude
prompt: |
Summarize this in a {style} style:
{input}
output_var: summary
output: "{summary}"
Now you can run:
# Default (concise)
cat article.txt | summarize
# Detailed analysis
cat article.txt | summarize --style detailed
# Quick bullet points
cat article.txt | summarize --style bullet-points
Notice how the flag value flows right into {style} in the prompt. That's the magic.
Every argument has four parts:
arguments:
- flag: --style # What users type
variable: style # Name in your templates
default: "concise" # Value when flag is omitted
description: "How to write the summary" # Help text
Pro Tip: The Golden Rule
Your tool should work perfectly with zero flags. Defaults are for the 80% case. Flags are for the other 20%.
Real tools need multiple options. Here's a translation tool with three knobs:
name: translate
version: "1.0.0"
description: Translate with control
arguments:
- flag: --to
variable: target_lang
default: "Spanish"
description: "Target language"
- flag: --tone
variable: tone
default: "neutral"
description: "Tone: casual, neutral, formal"
- flag: --preserve
variable: preserve
default: "meaning"
description: "Preserve: meaning, structure, or both"
steps:
- type: prompt
provider: claude
prompt: |
Translate to {target_lang}.
Use a {tone} tone.
Preserve the {preserve}.
Text:
{input}
output_var: result
output: "{result}"
Now you have fine-grained control:
# Simple translation
cat email.txt | translate --to French
# Formal business translation
cat email.txt | translate --to Japanese --tone formal
# Technical translation preserving structure
cat docs.md | translate --to German --preserve structure
Do This
--lang not --target-language--max not --maximum-length--style not --sNot This
-l (too cryptic)--target_language (underscores feel wrong)--TargetLanguage (this isn't Java)Warning: Quote Your Numbers!
YAML defaults are strings. Always quote numbers:
# WRONG - YAML might do weird things
default: 100
# RIGHT - Always a string
default: "100"
In your prompt, it'll work the same either way—but quoting prevents surprises.
Exercise
Create a code review tool with these flags:
--focus - What to focus on (bugs, style, performance)--severity - Minimum severity to report (low, medium, high)name: review
version: "1.0.0"
description: AI code review with focus
arguments:
- flag: --focus
variable: focus
default: "bugs"
description: "Focus: bugs, style, performance, security"
- flag: --severity
variable: severity
default: "medium"
description: "Minimum severity: low, medium, high"
steps:
- type: prompt
provider: claude
prompt: |
Review this code. Focus on {focus} issues.
Only report issues of {severity} severity or higher.
Code:
{input}
output_var: review
output: "{review}"
You've mastered single-step tools with arguments. Ready for the real power?
One AI call is nice. But the real magic happens when you chain them together—feeding the output of one model into the input of the next. Think of it as building an assembly line for intelligence.
What You'll Learn
Let's build something real: a tool that extracts key points from an article, then turns them into a tweet thread.
name: article-to-tweets
version: "1.0.0"
description: Turn articles into tweet threads
steps:
# Step 1: Extract the essence
- type: prompt
provider: claude
prompt: |
Extract 5 key insights from this article.
Be specific and quotable.
{input}
output_var: key_points
# Step 2: Transform into tweets
- type: prompt
provider: claude
prompt: |
Turn these insights into a compelling tweet thread.
Each tweet under 280 characters.
Make the first tweet a hook.
Insights:
{key_points}
output_var: thread
output: "{thread}"
The magic is in line 20: {key_points} contains the output from Step 1. Data flows downstream automatically.
Think of variables like water flowing downhill—they only go forward, never back:
Input
{input}
Step 1
{key_points}
Step 2
{thread}
Output
Step 2 can use: {input}, {key_points}
Step 2 cannot use: Variables from Step 3 (doesn't exist yet!)
AI is great at language. Python is great at data wrangling. Together? Unstoppable.
name: email-extractor
version: "1.0.0"
description: Extract and deduplicate emails from messy text
steps:
# AI finds the emails
- type: prompt
provider: claude
prompt: |
Extract all email addresses from this text.
Return them comma-separated, nothing else.
{input}
output_var: emails_raw
# Python cleans them up
- type: code
code: |
# Split, clean, deduplicate
emails = [e.strip().lower() for e in emails_raw.split(',')]
emails = [e for e in emails if '@' in e and '.' in e]
unique_emails = sorted(set(emails))
count = len(unique_emails)
clean_list = '\\n'.join(unique_emails)
output_var: clean_list, count
# AI formats the output nicely
- type: prompt
provider: claude
prompt: |
Format these {count} email addresses as a clean list
with any obvious categorization (personal, work, etc):
{clean_list}
output_var: result
output: "{result}"
Pro Tip: Code Steps Return Multiple Variables
Notice output_var: clean_list, count—you can export multiple variables
from a single code step. Just list them comma-separated.
Extract → Transform → Format
prompt # Pull out data
code # Clean/filter
prompt # Make it pretty
Use for: Data extraction, report generation
Analyze → Synthesize
prompt # Break it down
prompt # Build it up
Use for: Summaries, insights, rewriting
Validate → Process
code # Check input
prompt # Do the work
Use for: Safe processing, error handling
Warning: Steps Are All-Or-Nothing
If Step 2 fails, Step 3 never runs. Design defensively!
steps:
# Guard clause in code
- type: code
code: |
if not input.strip():
processed = "ERROR: Empty input"
is_valid = False
else:
processed = input
is_valid = True
output_var: processed, is_valid
# Only meaningful if valid
- type: prompt
provider: claude
prompt: |
Summarize this text: {processed}
output_var: summary
Exercise
Build a tool that:
name: code-explainer
version: "1.0.0"
description: Understand and improve any code
steps:
- type: prompt
provider: claude
prompt: |
What programming language is this?
Reply with just the language name.
{input}
output_var: language
- type: prompt
provider: claude
prompt: |
Explain this {language} code in plain English.
Describe what each part does.
{input}
output_var: explanation
- type: prompt
provider: claude
prompt: |
Suggest 3 improvements for this {language} code.
Consider readability, performance, and best practices.
Code:
{input}
Current understanding:
{explanation}
output_var: improvements
output: |
## Language: {language}
## Explanation
{explanation}
## Suggested Improvements
{improvements}
# See what prompts are being built
cat test.txt | my-tool --dry-run
# Watch each step execute
cat test.txt | my-tool --verbose
# Test with mock provider first
cat test.txt | my-tool --provider mock
Ready to go deeper? Learn the full power of code steps:
AI is great at understanding language. But sometimes you need Python's precision: parsing JSON, filtering data, doing math, or talking to APIs. Code steps let you mix Python into your AI workflows like a secret ingredient.
What You'll Learn
The simplest code step looks like this:
steps:
- type: code
code: |
result = input.upper()
output_var: result
That's it. Whatever you assign to result becomes available as {result} in subsequent steps.
What you have access to:
input
The original stdin
arg_name
Any --flag values
prev_output
Previous step outputs
Let's build something useful—a word counter that also finds the most common words:
name: wordstats
version: "1.0.0"
description: Analyze text statistics
steps:
- type: code
code: |
import re
from collections import Counter
# Clean and split
words = re.findall(r'\\b\\w+\\b', input.lower())
# Calculate stats
word_count = len(words)
unique_count = len(set(words))
char_count = len(input)
# Find top words
top_words = Counter(words).most_common(5)
top_formatted = '\\n'.join(f" {w}: {c}" for w, c in top_words)
output_var: word_count, unique_count, char_count, top_formatted
output: |
Words: {word_count}
Unique: {unique_count}
Characters: {char_count}
Top 5 words:
{top_formatted}
Pro Tip: Export Multiple Variables
Notice output_var: word_count, unique_count, char_count, top_formatted—
you can export as many variables as you need. Just list them comma-separated.
Copy-paste these recipes for common tasks:
- type: code
code: |
import json
try:
data = json.loads(ai_response)
result = json.dumps(data, indent=2)
except json.JSONDecodeError:
result = ai_response # Fallback to raw
output_var: result
- type: code
code: |
import re
# Find all emails
emails = re.findall(r'[\\w.-]+@[\\w.-]+\\.\\w+', input)
result = '\\n'.join(emails) or "No emails found"
output_var: result
- type: code
code: |
lines = [l for l in input.split('\\n') if l.strip()]
cleaned = '\\n'.join(lines)
output_var: cleaned
- type: code
code: |
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
result = f"[{timestamp}] {input}"
output_var: result
- type: code
code: |
max_len = int(max_length) # From --max-length flag
if len(input) > max_len:
truncated = input[:max_len] + "..."
else:
truncated = input
output_var: truncated
Warning: Crashes Kill Your Tool
An uncaught exception stops execution immediately. Always wrap risky code in try/except.
- type: code
code: |
import json
try:
# Risky operation
data = json.loads(ai_response)
items = data.get('items', [])
result = '\\n'.join(items)
error = None
except json.JSONDecodeError as e:
# Graceful fallback
result = ai_response
error = f"Parse warning: {e}"
except KeyError as e:
result = "Missing expected field"
error = str(e)
output_var: result, error
The real magic happens when you alternate between AI and code:
name: structured-extract
version: "1.0.0"
description: Extract and validate structured data
steps:
# AI extracts data
- type: prompt
provider: claude
prompt: |
Extract all dates from this text.
Return as JSON array: ["YYYY-MM-DD", ...]
{input}
output_var: dates_json
# Code validates and sorts
- type: code
code: |
import json
from datetime import datetime
try:
dates = json.loads(dates_json)
# Validate each date
valid = []
for d in dates:
try:
datetime.strptime(d, "%Y-%m-%d")
valid.append(d)
except ValueError:
pass
# Sort chronologically
sorted_dates = sorted(valid)
result = '\\n'.join(sorted_dates)
count = len(sorted_dates)
except json.JSONDecodeError:
result = "Could not parse dates"
count = 0
output_var: result, count
# AI formats nicely
- type: prompt
provider: claude
prompt: |
Format these {count} dates in a human-readable way:
{result}
output_var: formatted
output: "{formatted}"
Exercise
Create a tool that:
name: csv-describe
version: "1.0.0"
description: Understand CSV data at a glance
steps:
- type: code
code: |
import csv
from io import StringIO
reader = csv.reader(StringIO(input))
rows = list(reader)
if rows:
headers = rows[0]
row_count = len(rows) - 1
sample = rows[1:4] # First 3 data rows
headers_str = ', '.join(headers)
sample_str = '\\n'.join(', '.join(r) for r in sample)
else:
headers_str = "No headers"
row_count = 0
sample_str = "No data"
output_var: headers_str, row_count, sample_str
- type: prompt
provider: claude
prompt: |
This CSV has {row_count} rows with these columns:
{headers_str}
Sample data:
{sample_str}
What does this data likely represent?
What insights could we extract from it?
output_var: analysis
output: |
Columns: {headers_str}
Rows: {row_count}
{analysis}
The Python standard library is fully available:
jsonrecsvdatetimepathlibcollectionsitertoolsmathThird-party packages work too—just make sure they're installed in your environment.
Never Do This
eval(input) - Code injection waiting to happenexec(user_data) - Same problemos.system(input) - Shell injectionCode steps run with your user permissions. Treat input as untrusted data—parse it, don't execute it.
Ready for the advanced stuff?
You've mastered the basics. Now it's time for the patterns that make people ask, "Wait, you built that with YAML?" Multi-provider orchestration, conditional logic, self-improving loops, and more.
What You'll Master
Different AI models have different strengths. A smart tool uses them strategically:
Fast & Cheap
Haiku, Grok-mini
Extraction, formatting, simple tasks
Balanced
Sonnet, GPT-4o-mini
Analysis, writing, code review
Maximum Power
Opus, GPT-4, DeepSeek
Complex reasoning, synthesis
name: smart-analyzer
version: "1.0.0"
description: Uses the right model for each task
steps:
# Fast model extracts structure
- type: prompt
provider: opencode-grok
prompt: |
Extract all key facts, dates, and names from this text.
Return as a bullet list, nothing else.
{input}
output_var: facts
# Powerful model does the thinking
- type: prompt
provider: claude-sonnet
prompt: |
Based on these extracted facts, provide:
1. A summary of what happened
2. Key insights or patterns
3. Questions that remain unanswered
Facts:
{facts}
output_var: analysis
output: "{analysis}"
Pro Tip: Cost Optimization
Use fast models for extraction (structured data from messy text) and powerful models for synthesis (insights from structured data). You'll cut costs by 80% with no quality loss.
The best tools don't assume what they're getting. They figure it out:
name: universal-parser
version: "1.0.0"
description: Handles JSON, CSV, or plain text
steps:
# Detect format
- type: code
code: |
import json
text = input.strip()
if text.startswith('{') or text.startswith('['):
try:
json.loads(text)
format_type = "json"
except:
format_type = "text"
elif ',' in text and '\\n' in text:
format_type = "csv"
else:
format_type = "text"
format_instructions = {
"json": "Parse this JSON and describe its structure.",
"csv": "Analyze this CSV data and summarize the columns.",
"text": "Summarize the key points of this text."
}
instruction = format_instructions[format_type]
output_var: format_type, instruction
# Process accordingly
- type: prompt
provider: claude
prompt: |
This input is {format_type} format.
{instruction}
Input:
{input}
output_var: result
output: "[{format_type}] {result}"
Want better quality? Make your tool critique itself:
name: perfect-summary
version: "1.0.0"
description: Summarizes, then improves itself
steps:
# First attempt
- type: prompt
provider: claude-haiku
prompt: |
Write a 3-sentence summary of this text:
{input}
output_var: draft
# Self-critique
- type: prompt
provider: claude-sonnet
prompt: |
Rate this summary from 1-10 and list specific improvements:
Original text:
{input}
Summary:
{draft}
Be harsh but constructive.
output_var: critique
# Final polish
- type: prompt
provider: claude-sonnet
prompt: |
Rewrite this summary addressing the feedback.
Keep it to 3 sentences.
Original summary: {draft}
Feedback: {critique}
output_var: final
output: "{final}"
Warning: Know When to Stop
More iterations don't always mean better output. 2-3 passes is usually the sweet spot. Beyond that, you're paying for diminishing returns.
Let Python construct your prompts on the fly:
name: multi-task
version: "1.0.0"
description: One tool, many abilities
arguments:
- flag: --task
variable: task
default: "summarize"
description: "Task: summarize, explain, critique, expand, translate"
- flag: --style
variable: style
default: "professional"
steps:
- type: code
code: |
prompts = {
"summarize": f"Summarize in a {style} tone",
"explain": f"Explain for a beginner, {style} style",
"critique": f"Provide {style} constructive criticism",
"expand": f"Expand with more detail, keep {style}",
"translate": f"Translate, maintaining {style} register"
}
instruction = prompts.get(task, prompts["summarize"])
# Add context based on input length
length = len(input)
if length > 5000:
instruction += ". Focus on the most important parts."
elif length < 100:
instruction += ". Be thorough despite the short input."
output_var: instruction
- type: prompt
provider: claude
prompt: |
{instruction}
{input}
output_var: result
output: "{result}"
CmdForge can wrap any command-line tool:
name: lint-explain
version: "1.0.0"
description: Runs pylint and explains the results
steps:
# Run the linter
- type: code
code: |
import subprocess
import tempfile
import os
# Write code to temp file
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(input)
temp_path = f.name
try:
result = subprocess.run(
['pylint', '--output-format=text', temp_path],
capture_output=True,
text=True,
timeout=30
)
lint_output = result.stdout or result.stderr or "No issues found!"
except FileNotFoundError:
lint_output = "ERROR: pylint not installed"
except subprocess.TimeoutExpired:
lint_output = "ERROR: Linting timed out"
finally:
os.unlink(temp_path)
output_var: lint_output
# Explain in plain English
- type: prompt
provider: claude
prompt: |
Explain these linting results to a Python beginner.
For each issue, explain WHY it's a problem and HOW to fix it.
Results:
{lint_output}
output_var: explanation
output: |
## Lint Results
```
{lint_output}
```
## Explanation
{explanation}
Boss Level Exercise
Create a tool that:
name: research
version: "1.0.0"
description: Deep research on any topic
steps:
# Detect input type
- type: code
code: |
text = input.strip()
is_question = text.endswith('?') or text.lower().startswith(('what', 'how', 'why', 'when', 'who', 'where'))
input_type = "question" if is_question else "topic"
output_var: input_type
# Generate angles
- type: prompt
provider: opencode-grok
prompt: |
This is a {input_type}: {input}
Suggest 3 interesting angles to explore this.
Return as a numbered list.
output_var: angles
# Deep dive
- type: prompt
provider: claude-sonnet
prompt: |
Research request: {input}
Possible angles:
{angles}
Pick the most interesting angle and provide:
1. Background context
2. Key facts and insights
3. Different perspectives
4. Remaining questions
output_var: research
# Format nicely
- type: code
code: |
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
formatted = f'''
# Research Report
Generated: {timestamp}
Query: {input}
{research}
---
*Angles considered: {angles}*
'''
output_var: formatted
output: "{formatted}"
Speed Tricks
Cost Tricks
You've learned the advanced patterns. Now go parallel:
Not everyone wants to write YAML by hand. CmdForge includes a modern desktop application that lets you create, edit, and manage tools with a visual interface—no text editor required.
What You'll Learn
Start the desktop application with a single command:
cmdforge
The application opens with a clean, modern interface featuring a sidebar for navigation and a main content area:
Sidebar Navigation
Features
Pro Tip: Theme Switching
Use the View menu to switch between light and dark themes. Your preference is saved automatically.
The My Tools page shows all your installed tools organized by category in a tree view:
From this page you can:
Create
New tool
Edit
Existing tool
Publish
To registry
Delete
Remove tool
Click New Tool to open the tool builder. It has a form-based interface split into sections:
Basic Information
Arguments
Processing Steps
Build your tool's logic by adding steps:
cmdforge to open the applicationsummarize and a description--length with default 100The Registry page lets you discover and install tools created by the community:
Version Selection
When installing a tool, you can choose a specific version or get the latest. This is useful when you need a particular version for compatibility.
The Providers page shows all configured AI backends in a table view:
| Column | Description |
|---|---|
| Name | Provider identifier used in tool configs |
| Command | The CLI command that's executed |
| Description | What this provider does |
From this page you can:
Test your tools before deploying them using the step-by-step test feature:
This lets you verify prompts and variable substitution. For quick tests without API calls, configure a
mock provider that echoes input back.
Power users can navigate quickly with these shortcuts:
| Shortcut | Action |
|---|---|
Ctrl+1 to Ctrl+4 |
Switch to page (Tools, Registry, Providers, Profiles) |
Ctrl+N |
Create new tool |
Ctrl+S |
Save current tool |
Escape |
Cancel / go back |
Ctrl+Q |
Quit the application |
When should you use each?
| Use the Visual Builder when... | Use CLI/YAML when... |
|---|---|
| You're new to CmdForge | You're comfortable with YAML |
| Building your first few tools | Making quick edits to configs |
| Browsing the registry | Scripting tool installation |
| Managing providers visually | Copying tools between machines |
| Publishing tools to the registry | CI/CD integration |
Now that you know how to use the Visual Builder: