Add meta-tools, collections, and attribution features

Meta-tools:
- Add ToolStep dataclass for tools calling other tools
- Implement execute_tool_step() with recursion depth limiting
- Add check_dependencies() for validating tool dependencies
- Add 'cmdforge check' command for dependency verification

Collections:
- Add collections API endpoints and database schema
- Create collections web UI (list and detail pages)
- Add collections to navigation header and homepage
- Document collections in REGISTRY.md

Attribution:
- Add source attribution fields to tool detail page
- Document source types (original, adapted, imported)
- Add license field documentation

Documentation updates across README, DESIGN.md, REGISTRY.md,
and new META_TOOLS.md design document.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-03 03:05:23 -04:00
co-authored by Claude Opus 4.5
parent 497fe87fc5
commit 2c139b3982
20 changed files with 1299 additions and 11 deletions
+160 -3
View File
@@ -5,11 +5,63 @@ import sys
from pathlib import Path
from typing import Optional
from .tool import Tool, PromptStep, CodeStep
from .tool import Tool, PromptStep, CodeStep, ToolStep
from .providers import call_provider, mock_provider
from .resolver import resolve_tool, ToolNotFoundError, ToolSpec
from .manifest import load_manifest
# Maximum recursion depth for nested tool calls
MAX_TOOL_DEPTH = 10
def check_dependencies(tool: Tool, checked: set = None) -> list[str]:
"""
Check if all dependencies for a tool are available.
Args:
tool: Tool to check dependencies for
checked: Set of already checked tools (prevents infinite loops)
Returns:
List of missing dependency tool references
"""
if checked is None:
checked = set()
missing = []
# Check explicit dependencies
for dep in tool.dependencies:
if dep in checked:
continue
checked.add(dep)
try:
resolved = resolve_tool(dep)
# Recursively check nested dependencies
nested_missing = check_dependencies(resolved.tool, checked)
missing.extend(nested_missing)
except ToolNotFoundError:
missing.append(dep)
# Also check tool steps for implicit dependencies
for step in tool.steps:
if isinstance(step, ToolStep):
tool_ref = step.tool
if tool_ref in checked:
continue
checked.add(tool_ref)
try:
resolved = resolve_tool(tool_ref)
# Recursively check nested dependencies
nested_missing = check_dependencies(resolved.tool, checked)
missing.extend(nested_missing)
except ToolNotFoundError:
missing.append(tool_ref)
return missing
def substitute_variables(template: str, variables: dict) -> str:
"""
@@ -111,6 +163,75 @@ def execute_code_step(step: CodeStep, variables: dict) -> tuple[dict, bool]:
return {}, False
def execute_tool_step(
step: ToolStep,
variables: dict,
depth: int = 0,
provider_override: Optional[str] = None,
dry_run: bool = False,
verbose: bool = False
) -> tuple[str, bool]:
"""
Execute a tool step by calling another tool.
Args:
step: The tool step to execute
variables: Current variable values
depth: Current recursion depth
provider_override: Override provider for nested calls
dry_run: Just show what would happen
verbose: Show debug info
Returns:
Tuple of (output_value, success)
"""
if depth >= MAX_TOOL_DEPTH:
print(f"Error: Maximum tool nesting depth ({MAX_TOOL_DEPTH}) exceeded", file=sys.stderr)
return "", False
# Resolve the tool reference
try:
resolved = resolve_tool(step.tool)
nested_tool = resolved.tool
except ToolNotFoundError as e:
print(f"Error: Tool '{step.tool}' not found", file=sys.stderr)
print(f"Hint: Install it with: cmdforge install {step.tool}", file=sys.stderr)
return "", False
# Prepare input by substituting variables
input_text = substitute_variables(step.input_template, variables)
# Prepare arguments by substituting variables in arg values
custom_args = {}
for key, value in step.args.items():
custom_args[key] = substitute_variables(str(value), variables)
# Determine effective provider (step override > parent override)
effective_provider = step.provider or provider_override
if verbose:
print(f"[verbose] Tool step: calling {step.tool}", file=sys.stderr)
print(f"[verbose] Input length: {len(input_text)} chars", file=sys.stderr)
print(f"[verbose] Args: {list(custom_args.keys())}", file=sys.stderr)
if dry_run:
return f"[DRY RUN - would call tool {step.tool}]", True
# Run the nested tool
output, exit_code = run_tool(
tool=nested_tool,
input_text=input_text,
custom_args=custom_args,
provider_override=effective_provider,
dry_run=dry_run,
show_prompt=False,
verbose=verbose,
_depth=depth + 1 # Track nesting depth
)
return output, exit_code == 0
def run_tool(
tool: Tool,
input_text: str,
@@ -118,7 +239,8 @@ def run_tool(
provider_override: Optional[str] = None,
dry_run: bool = False,
show_prompt: bool = False,
verbose: bool = False
verbose: bool = False,
_depth: int = 0 # Internal: tracks nesting depth for tool steps
) -> tuple[str, int]:
"""
Execute a tool.
@@ -131,10 +253,19 @@ def run_tool(
dry_run: Just show what would happen
show_prompt: Show prompts in addition to output
verbose: Show debug info
_depth: Internal recursion depth tracker
Returns:
Tuple of (output_text, exit_code)
"""
# Check dependencies on first level only (not for nested calls)
if _depth == 0 and (tool.dependencies or any(isinstance(s, ToolStep) for s in tool.steps)):
missing = check_dependencies(tool)
if missing:
print(f"Warning: Missing dependencies: {', '.join(missing)}", file=sys.stderr)
print(f"Install with: cmdforge install {' '.join(missing)}", file=sys.stderr)
# Continue anyway - the actual step execution will fail with a better error
# Initialize variables with input and arguments
variables = {"input": input_text}
@@ -156,7 +287,14 @@ def run_tool(
# Execute each step
for i, step in enumerate(tool.steps):
if verbose:
step_type = "PROMPT" if isinstance(step, PromptStep) else "CODE"
if isinstance(step, PromptStep):
step_type = "PROMPT"
elif isinstance(step, CodeStep):
step_type = "CODE"
elif isinstance(step, ToolStep):
step_type = f"TOOL({step.tool})"
else:
step_type = "UNKNOWN"
print(f"[verbose] Step {i+1}: {step_type} -> {{{step.output_var}}}", file=sys.stderr)
if isinstance(step, PromptStep):
@@ -192,6 +330,25 @@ def run_tool(
# Merge all output vars into variables
variables.update(outputs)
elif isinstance(step, ToolStep):
if verbose or dry_run:
print(f"=== TOOL (Step {i+1}) -> {{{step.output_var}}} ===", file=sys.stderr)
print(f" Calling: {step.tool}", file=sys.stderr)
print(f" Args: {step.args}", file=sys.stderr)
print("=== END TOOL ===", file=sys.stderr)
output, success = execute_tool_step(
step,
variables,
depth=_depth,
provider_override=provider_override,
dry_run=dry_run,
verbose=verbose
)
if not success:
return "", 3
variables[step.output_var] = output
# Generate final output
output = substitute_variables(tool.output, variables)