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
@@ -0,0 +1,127 @@
{% extends "base.html" %}
{% from "components/tool_card.html" import tool_card %}
{% block title %}{{ collection.display_name }} - CmdForge Collections{% endblock %}
{% block meta_description %}{{ collection.description or 'A curated collection of AI-powered CLI tools.' }}{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Collection Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<nav class="mb-4 text-sm text-gray-500">
<a href="{{ url_for('web.collections') }}" class="hover:text-indigo-600">Collections</a>
<span class="mx-2">/</span>
<span class="text-gray-900">{{ collection.display_name }}</span>
</nav>
<div class="flex items-start space-x-4">
<!-- Collection Icon -->
<div class="flex-shrink-0 w-16 h-16 bg-purple-100 rounded-lg flex items-center justify-center">
{% if collection.icon %}
<span class="text-3xl">{{ collection.icon }}</span>
{% else %}
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
{% endif %}
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold text-gray-900">{{ collection.display_name }}</h1>
<p class="mt-1 text-gray-500">
Curated by <span class="font-medium text-gray-700">{{ collection.maintainer }}</span>
</p>
{% if collection.description %}
<p class="mt-3 text-gray-600">{{ collection.description }}</p>
{% endif %}
</div>
</div>
<!-- Tags -->
{% if collection.tags %}
<div class="mt-4 flex flex-wrap gap-2">
{% for tag in collection.tags %}
<span class="px-3 py-1 bg-purple-100 text-purple-700 text-sm rounded-full">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
<!-- Install Command -->
<div class="mt-6 p-4 bg-gray-900 rounded-lg">
<div class="flex items-center justify-between">
<code class="text-green-400 font-mono">cmdforge install --collection {{ collection.name }}</code>
<button type="button"
onclick="copyToClipboard('cmdforge install --collection {{ collection.name }}')"
class="ml-4 p-2 text-gray-400 hover:text-white transition-colors"
aria-label="Copy install command">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
<p class="mt-2 text-sm text-gray-400">
Installs {{ collection.tools|length if collection.tools else 0 }} tool{{ 's' if collection.tools|length != 1 else '' }} with a single command
</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Tools Section -->
<div class="mb-6">
<h2 class="text-xl font-semibold text-gray-900">
Tools in this Collection
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ collection.tools|length if collection.tools else 0 }} tool{{ 's' if collection.tools|length != 1 else '' }} included
</p>
</div>
{% if collection.tools %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for tool in collection.tools %}
{% if tool.owner and tool.name %}
{{ tool_card(tool) }}
{% else %}
<!-- Tool not found in registry -->
<article class="bg-white rounded-lg border border-gray-200 border-dashed p-4 opacity-60">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-gray-500">{{ tool }}</h3>
<p class="text-sm text-gray-400">Tool not yet in registry</p>
</div>
</div>
</article>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="text-center py-12 bg-white rounded-lg border border-gray-200">
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No tools in this collection</h3>
<p class="mt-2 text-gray-600">
This collection doesn't have any tools yet.
</p>
</div>
{% endif %}
</div>
</div>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
// Could add a toast notification here
});
}
</script>
{% endblock %}