"""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.

What is CmdForge?

CmdForge is a lightweight personal tool builder that lets you:

Quick Start

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.

How It Works

Each tool is a YAML file that defines:

  1. Arguments - Custom flags your tool accepts
  2. Steps - Prompts to send to AI or Python code to run
  3. Output - How to format the final result

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}"

Next Steps

Get Help

Stuck? Have questions? We've got you covered:

""", "headings": [ ("what-is-cmdforge", "What is CmdForge?"), ("quick-start", "Quick Start"), ("how-it-works", "How It Works"), ("next-steps", "Next Steps"), ("get-help", "Get Help"), ], }, "installation": { "title": "Installation", "description": "How to install CmdForge on your system", "parent": "getting-started", "content": """

CmdForge requires Python 3.8+ and works on Linux, macOS, and Windows.

Install with pip

The simplest way to install CmdForge:

pip install cmdforge

Or with pipx for isolated installation:

pipx install cmdforge

Verify Installation

cmdforge --version
cmdforge --help

Configure a Provider

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

Wrapper Scripts Location

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"

Development Installation

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.

Create the Tool

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}"

Test Your Tool

# 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

Understanding the Config

Arguments

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

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}

Output

The output template formats the final result:

output: "{explanation}"  # Print the explanation variable

Next Steps

""", "headings": [ ("create-tool", "Create the Tool"), ("test-it", "Test Your Tool"), ("understanding-config", "Understanding the Config"), ("next", "Next Steps"), ], }, "publishing": { "title": "Publishing Tools", "description": "Share your tools with the CmdForge community", "content": """

Share your tools with the community by publishing to the CmdForge Registry.

Before Publishing

Make sure your tool has:

Create an Account

Register at the registry to get your publisher namespace.

Connect Your Account

Link your CLI or GUI to your registry account with a simple pairing flow:

Option 1: From the Command Line

# Start the connection flow
cmdforge config connect yourusername

This opens your browser to approve the connection. Once approved, you're ready to publish!

Option 2: From the Desktop GUI

  1. Launch cmdforge (the desktop app)
  2. Go to the Registry page
  3. Click the Connect button
  4. Enter your username and approve in the browser

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 Your Tool

# 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:

  1. Open the My Tools page
  2. Right-click on your tool and select Publish
  3. Confirm the version and publish

Moderation Process

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

Versioning

Published versions are immutable. To update a tool:

  1. Make your changes
  2. Bump the version in config.yaml
  3. Run cmdforge registry publish mytool

Best Practices

""", "headings": [ ("before-publishing", "Before Publishing"), ("create-account", "Create an Account"), ("connect-account", "Connect Your Account"), ("publish", "Publish Your Tool"), ("moderation", "Moderation Process"), ("versioning", "Versioning"), ("best-practices", "Best Practices"), ], }, "providers": { "title": "AI Providers", "description": "Configure different AI providers for your tools", "content": """

CmdForge works with any AI provider that has a CLI interface. Configure providers in ~/.cmdforge/providers.yaml.

Provider Configuration

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]'"

Using Providers in Tools

Specify the provider in your step:

steps:
  - type: prompt
    provider: claude  # Uses the "claude" provider from config
    prompt: "..."
    output_var: response

Claude (Anthropic)

# Install
pip install claude-cli

# Configure with your API key
export ANTHROPIC_API_KEY="sk-ant-..."
# providers.yaml
providers:
  - name: claude
    command: "claude -p"

OpenAI

# Install
pip install openai-cli

# Configure
export OPENAI_API_KEY="sk-..."

Ollama (Local)

# Install Ollama from ollama.ai
# Pull a model
ollama pull llama2
# providers.yaml
providers:
  - name: ollama
    command: "ollama run llama2"

Testing with Mock Provider

Use the mock provider to test tools without API calls:

providers:
  - name: mock
    command: "echo 'This is a mock response for testing'"

Choosing a Provider

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)
""", "headings": [ ("provider-config", "Provider Configuration"), ("using-providers", "Using Providers in Tools"), ("popular-providers", "Popular Providers"), ("testing", "Testing with Mock Provider"), ("provider-selection", "Choosing a Provider"), ], }, "parallel-orchestration": { "title": "Parallel Orchestration", "description": "Run multiple CmdForge concurrently for faster workflows", "content": """

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.

Why Parallel Execution?

Consider a code review workflow that needs input from multiple perspectives:

Basic Pattern

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'])

Real-World Example: Multi-Perspective Analysis

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()

Adding Progress Feedback

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

Error Handling

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
        }

Best Practices

Full Example: orchestrated-discussions

For a complete implementation of parallel CmdForge orchestration, see the orchestrated-discussions project. It implements:

""", "headings": [ ("why-parallel", "Why Parallel Execution?"), ("basic-pattern", "Basic Pattern"), ("real-world-example", "Real-World Example"), ("with-progress", "Adding Progress Feedback"), ("error-handling", "Error Handling"), ("best-practices", "Best Practices"), ("example-project", "Full Example Project"), ], }, "yaml-config": { "title": "The Anatomy of a SmartTool", "description": "Master the YAML configuration that powers every SmartTool", "content": """

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

The Simplest Tool That Actually Works

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.

The Five Parts of Every Tool

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}"

Variables Are Pipes

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.

The YAML Trap Everyone Falls Into

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.

Try It Yourself

Exercise: Build a Tone Shifter

Create a tool that rewrites text in different tones. It should:

  1. Accept a --tone flag (default: "professional")
  2. Rewrite the input in that tone
  3. Output just the rewritten text
Click to see the solution
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}"

Naming Your Tools

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."

Where To Next?

You now understand the structure. Time to add superpowers:

""", "headings": [ ("the-simplest-tool", "The Simplest Tool That Works"), ("the-five-parts", "The Five Parts of Every Tool"), ("variables-are-pipes", "Variables Are Pipes"), ("the-yaml-trap", "The YAML Trap"), ("try-it", "Try It Yourself"), ("naming-rules", "Naming Your Tools"), ("where-to-next", "Where To Next?"), ], }, "arguments": { "title": "Adding Knobs and Switches", "description": "Give your tools superpowers with custom flags", "content": """

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.

Your First Flag

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.

Anatomy of an Argument

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%.

Stacking Multiple Flags

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

The Art of Good Flags

Do This

Not This

The Numeric Gotcha

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.

Try It: Build a Code Reviewer

Exercise

Create a code review tool with these flags:

See the solution
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}"

Next Up

You've mastered single-step tools with arguments. Ready for the real power?

""", "headings": [ ("your-first-flag", "Your First Flag"), ("anatomy-of-an-argument", "Anatomy of an Argument"), ("stacking-flags", "Stacking Multiple Flags"), ("flag-design", "The Art of Good Flags"), ("numeric-gotcha", "The Numeric Gotcha"), ("try-it", "Try It: Build a Code Reviewer"), ("next-up", "Next Up"), ], }, "multi-step": { "title": "Chaining AI Like a Pro", "description": "Build powerful pipelines by connecting multiple AI calls", "content": """

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

Your First Pipeline

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.

The Variable Waterfall

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!)

Adding Python Glue

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.

The Three Patterns That Solve Everything

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

When Things Go Wrong

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

Try It: Build a Code Explainer

Exercise

Build a tool that:

  1. Identifies the programming language
  2. Explains what the code does (using the language info)
  3. Suggests improvements
See the solution
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}

Debugging Multi-Step Tools

# 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

Next Up

Ready to go deeper? Learn the full power of code steps:

""", "headings": [ ("your-first-pipeline", "Your First Pipeline"), ("the-variable-waterfall", "The Variable Waterfall"), ("adding-python-glue", "Adding Python Glue"), ("the-three-patterns", "The Three Patterns"), ("when-things-go-wrong", "When Things Go Wrong"), ("try-it", "Try It: Build a Code Explainer"), ("debugging-tips", "Debugging Tips"), ("next-up", "Next Up"), ], }, "code-steps": { "title": "Python Superpowers", "description": "Inject Python into your AI workflows for ultimate control", "content": """

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

Your First Code Step

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

A Real Example: Smart Word Counter

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.

The Code Step Cookbook

Copy-paste these recipes for common tasks:

Parse JSON from AI
- 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
Extract with Regex
- 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
Remove Empty Lines
- type: code
  code: |
    lines = [l for l in input.split('\\n') if l.strip()]
    cleaned = '\\n'.join(lines)
  output_var: cleaned
Add Timestamps
- type: code
  code: |
    from datetime import datetime
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    result = f"[{timestamp}] {input}"
  output_var: result
Limit/Truncate Text
- 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

Bulletproof Error Handling

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 AI + Code Power Combo

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}"

Try It: Build a CSV Analyzer

Exercise

Create a tool that:

  1. Takes CSV data as input
  2. Uses Python to count rows and extract headers
  3. Asks AI to describe what the data likely represents
See the solution
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}

What You Can Import

The Python standard library is fully available:

json
re
csv
datetime
pathlib
collections
itertools
math

Third-party packages work too—just make sure they're installed in your environment.

Security: The Rules

Never Do This

Code steps run with your user permissions. Treat input as untrusted data—parse it, don't execute it.

Level Up

Ready for the advanced stuff?

""", "headings": [ ("hello-code", "Your First Code Step"), ("real-example", "A Real Example"), ("the-cookbook", "The Code Step Cookbook"), ("error-handling", "Bulletproof Error Handling"), ("combining-with-ai", "The AI + Code Power Combo"), ("try-it", "Try It: Build a CSV Analyzer"), ("what-you-can-import", "What You Can Import"), ("security", "Security: The Rules"), ("next-up", "Level Up"), ], }, "advanced-workflows": { "title": "The Advanced Playbook", "description": "Patterns that separate the pros from the beginners", "content": """

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

Right Model, Right Job

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.

Tools That Adapt to Their Input

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}"

The Self-Improving Loop

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.

Dynamic Prompt Building

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}"

Calling External Tools

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}

Try It: Build a Research Assistant

Boss Level Exercise

Create a tool that:

  1. Detects whether input is a question or a topic
  2. Uses a fast model to generate 3 research angles
  3. Uses a powerful model to explore the best angle
  4. Adds a code step to format with headers and timestamps
See the solution
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}"

Performance Secrets

Speed Tricks

Cost Tricks

What's Next?

You've learned the advanced patterns. Now go parallel:

""", "headings": [ ("right-model-right-job", "Right Model, Right Job"), ("adaptive-tools", "Tools That Adapt"), ("self-improving", "The Self-Improving Loop"), ("dynamic-prompts", "Dynamic Prompt Building"), ("external-tools", "Calling External Tools"), ("try-it", "Try It: Research Assistant"), ("performance", "Performance Secrets"), ("whats-next", "What's Next?"), ], }, "visual-builder": { "title": "The Visual Builder", "description": "Build tools visually with the CmdForge desktop GUI", "content": """

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

Launching the Application

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

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

Creating Tools with the Visual Builder

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:

Step-by-Step: Creating a Summarizer

  1. Launch: Run cmdforge to open the application
  2. Navigate: Click My Tools in the sidebar
  3. Create: Click the New Tool button
  4. Basic Info: Enter name summarize and a description
  5. Add Argument: Click Add Argument, set flag to --length with default 100
  6. Add Step: Click Add Step and choose Prompt Step
  7. Configure Step: Select your AI provider and enter the prompt template
  8. Save: Click Save to create your tool

Browsing the Registry

The 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.

Managing Providers

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:

Testing Tools

Test your tools before deploying them using the step-by-step test feature:

  1. In the Tool Builder, click Test on any step
  2. Enter sample input text
  3. Set any argument values
  4. Run the test to see the output

This lets you verify prompts and variable substitution. For quick tests without API calls, configure a mock provider that echoes input back.

Keyboard Shortcuts

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

CLI vs Visual Builder

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

Next Up

Now that you know how to use the Visual Builder:

""", "headings": [ ("launching", "Launching the Application"), ("my-tools", "The My Tools Page"), ("creating-tools", "Creating Tools with the Visual Builder"), ("registry", "Browsing the Registry"), ("managing-providers", "Managing Providers"), ("testing", "Testing Tools"), ("keyboard-shortcuts", "Keyboard Shortcuts"), ("cli-vs-gui", "CLI vs Visual Builder"), ("next-up", "Next Up"), ], }, } def get_doc(path: str) -> dict: """Get documentation content by path.""" # Normalize path path = path.strip("/").replace("docs/", "") or "getting-started" return DOCS.get(path, None) def get_toc(): """Get table of contents structure.""" from types import SimpleNamespace return [ SimpleNamespace(slug="getting-started", title="Getting Started", children=[ SimpleNamespace(slug="installation", title="Installation"), SimpleNamespace(slug="first-tool", title="Your First Tool"), SimpleNamespace(slug="visual-builder", title="Visual Builder"), SimpleNamespace(slug="yaml-config", title="YAML Config"), ]), SimpleNamespace(slug="arguments", title="Custom Arguments", children=[]), SimpleNamespace(slug="multi-step", title="Multi-Step Workflows", children=[ SimpleNamespace(slug="code-steps", title="Code Steps"), ]), SimpleNamespace(slug="providers", title="Providers", children=[]), SimpleNamespace(slug="publishing", title="Publishing", children=[]), SimpleNamespace(slug="advanced-workflows", title="Advanced Workflows", children=[ SimpleNamespace(slug="parallel-orchestration", title="Parallel Orchestration"), ]), ]