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:
@@ -644,6 +644,7 @@ def sitemap():
|
||||
static_pages = [
|
||||
("/", "1.0", "daily"),
|
||||
("/tools", "0.9", "daily"),
|
||||
("/collections", "0.8", "weekly"),
|
||||
("/docs", "0.8", "weekly"),
|
||||
("/docs/getting-started", "0.8", "weekly"),
|
||||
("/docs/installation", "0.7", "weekly"),
|
||||
@@ -744,3 +745,35 @@ def dashboard_undeprecate_tool(owner: str, name: str):
|
||||
data = request.get_json() or {}
|
||||
status, payload = _api_post(f"/api/v1/tools/{owner}/{name}/undeprecate", data=data, token=token)
|
||||
return jsonify(payload), status
|
||||
|
||||
|
||||
# ============================================
|
||||
# Collections Routes
|
||||
# ============================================
|
||||
|
||||
@web_bp.route("/collections", endpoint="collections")
|
||||
def collections():
|
||||
"""List all available collections."""
|
||||
status, payload = _api_get("/api/v1/collections")
|
||||
if status != 200:
|
||||
return render_template("errors/500.html"), 500
|
||||
return render_template(
|
||||
"pages/collections.html",
|
||||
collections=payload.get("data", []),
|
||||
)
|
||||
|
||||
|
||||
@web_bp.route("/collections/<name>", endpoint="collection_detail")
|
||||
def collection_detail(name: str):
|
||||
"""Show a specific collection with its tools."""
|
||||
status, payload = _api_get(f"/api/v1/collections/{name}")
|
||||
if status == 404:
|
||||
return render_template("errors/404.html"), 404
|
||||
if status != 200:
|
||||
return render_template("errors/500.html"), 500
|
||||
|
||||
collection = payload.get("data", {})
|
||||
return render_template(
|
||||
"pages/collection_detail.html",
|
||||
collection=collection,
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
|
||||
{# Collection card macro #}
|
||||
{% macro collection_card(collection) %}
|
||||
<article class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow p-5">
|
||||
<!-- Collection header -->
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
{% if collection.icon %}
|
||||
<span class="text-2xl">{{ collection.icon }}</span>
|
||||
{% else %}
|
||||
<svg class="w-6 h-6 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 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<a href="{{ url_for('web.collection_detail', name=collection.name) }}"
|
||||
class="hover:text-purple-600 transition-colors">
|
||||
{{ collection.display_name }}
|
||||
</a>
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">by {{ collection.maintainer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="mt-3 text-sm text-gray-600 line-clamp-2">
|
||||
{{ collection.description or 'A curated collection of tools.' }}
|
||||
</p>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="mt-4 flex items-center justify-between text-sm">
|
||||
<span class="flex items-center text-purple-600">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
{{ collection.tool_count }} tools
|
||||
</span>
|
||||
{% if collection.tags %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for tag in collection.tags[:3] %}
|
||||
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Install command -->
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center bg-gray-100 rounded px-3 py-2 group">
|
||||
<code class="text-xs text-gray-700 flex-1 truncate font-mono">
|
||||
cmdforge install --collection {{ collection.name }}
|
||||
</code>
|
||||
<button type="button"
|
||||
onclick="copyToClipboard('cmdforge install --collection {{ collection.name }}')"
|
||||
class="ml-2 p-1 text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label="Copy install command">
|
||||
<svg class="w-4 h-4" 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>
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
@@ -22,6 +22,10 @@
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/tools') %}bg-slate-700{% endif %}">
|
||||
Registry
|
||||
</a>
|
||||
<a href="{{ url_for('web.collections') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/collections') %}bg-slate-700{% endif %}">
|
||||
Collections
|
||||
</a>
|
||||
<a href="{{ url_for('forum.index') }}"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/forum') %}bg-slate-700{% endif %}">
|
||||
Forum
|
||||
@@ -119,6 +123,10 @@
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/tools') %}bg-slate-700{% endif %}">
|
||||
Registry
|
||||
</a>
|
||||
<a href="{{ url_for('web.collections') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/collections') %}bg-slate-700{% endif %}">
|
||||
Collections
|
||||
</a>
|
||||
<a href="{{ url_for('forum.index') }}"
|
||||
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/forum') %}bg-slate-700{% endif %}">
|
||||
Forum
|
||||
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/collection_card.html" import collection_card %}
|
||||
|
||||
{% block title %}Collections - CmdForge Registry{% endblock %}
|
||||
|
||||
{% block meta_description %}Browse curated collections of AI tools. Install multiple tools at once with a single command.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Page 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">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Collections</h1>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Curated groups of tools that work great together. Install an entire collection with a single command.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{% if collections %}
|
||||
<!-- Results Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ collections|length }} collection{{ 's' if collections|length != 1 else '' }} available
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Collections Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{% for collection in collections %}
|
||||
{{ collection_card(collection) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No collections yet</h3>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Collections are coming soon! They'll let you install groups of related tools together.
|
||||
</p>
|
||||
<a href="{{ url_for('web.tools') }}"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Browse Individual Tools
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -131,6 +131,29 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Collections Callout -->
|
||||
<div class="mt-12 p-6 bg-purple-50 rounded-lg border border-purple-100">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Tool Collections</h3>
|
||||
<p class="text-sm text-gray-600">Install curated groups of tools with a single command</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('web.collections') }}" class="text-purple-600 hover:text-purple-800 font-medium flex items-center">
|
||||
Browse Collections
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -65,6 +65,25 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if tool.source %}
|
||||
<div class="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-amber-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="text-sm text-amber-800">
|
||||
Based on
|
||||
{% if tool.source_url %}
|
||||
<a href="{{ tool.source_url }}" target="_blank" rel="noopener" class="font-medium underline hover:text-amber-900">{{ tool.source }}</a>
|
||||
{% else %}
|
||||
<span class="font-medium">{{ tool.source }}</span>
|
||||
{% endif %}
|
||||
pattern
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Deprecation Warning -->
|
||||
|
||||
Reference in New Issue
Block a user