feat: CLI-Matrix with multi-approach stages, skill discovery, and matrix search

Introduce CLI-Matrix — curated multi-CLI workflow matrices that agents can
install in one command. The video-creation matrix bundles 11 CLIs across 8
production stages (AI video gen, capture, audio, voice/TTS, music, NLE
editing, captions, thumbnails).

Each stage now exposes a goal, alternative approaches (Python libs, cloud
APIs, native commands), and skill_search_hints that encourage agents to
dynamically discover relevant skills via `npx skills search` rather than
relying on hard-coded tool lists.

Key changes:
- matrix_registry.json: extended stage schema with goal, alternatives,
  skill_search_hints fields
- cli-hub matrix list/search/info/install commands
- matrix_skill.py: renders dynamic SKILL.md with stage tooling overview,
  install status, and aggregated discovery commands
- Fixed brittle parents[2] repo root detection with git-based lookup
- 85 tests passing (10 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yuhao
2026-04-24 14:40:57 +00:00
co-authored by Claude Opus 4.6
parent ab2556360f
commit 51b55aaa52
9 changed files with 1159 additions and 11 deletions
+52 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generate cli-hub-skill/SKILL.md from registry.json and public_registry.json."""
"""Generate cli-hub-skill/SKILL.md from registry.json, public_registry.json, and matrix_registry.json."""
import json
from pathlib import Path
from collections import defaultdict
@@ -8,6 +8,7 @@ def main():
repo_root = Path(__file__).parent.parent.parent
registry_path = repo_root / 'registry.json'
public_registry_path = repo_root / 'public_registry.json'
matrix_registry_path = repo_root / 'matrix_registry.json'
output_path = repo_root / 'cli-hub-skill' / 'SKILL.md'
with open(registry_path) as f:
@@ -19,6 +20,12 @@ def main():
public_data = json.load(f)
public_clis = public_data.get('clis', [])
matrices = []
if matrix_registry_path.exists():
with open(matrix_registry_path) as f:
matrix_data = json.load(f)
matrices = matrix_data.get('matrices', [])
total_count = len(data['clis']) + len(public_clis)
# Group harness CLIs by category
@@ -65,6 +72,21 @@ def main():
"cli-hub launch <name> [args...]",
"```",
"",
"## CLI Matrices",
"",
f"`cli-hub` also ships {len(matrices)} curated cross-tool matrices: install one name to pull in a whole workflow kit and read its dedicated SKILL.md.",
"",
"```bash",
"# Browse curated matrices",
"cli-hub matrix list",
"",
"# Inspect one matrix",
"cli-hub matrix info video-creation",
"",
"# Install the whole matrix",
"cli-hub matrix install video-creation",
"```",
"",
"## CLI-Anything Harness CLIs",
"",
f"Stateful, agent-native wrappers for {len(data['clis'])} GUI applications. All support `--json` output, REPL mode, and undo/redo.",
@@ -97,15 +119,36 @@ def main():
clis = public_by_category[category]
lines.append(f"### {category.title()}")
lines.append("")
lines.append("| Name | Description | Entry Point | Install |")
lines.append("|------|-------------|-------------|---------|")
lines.append("| Name | Description | Entry Point | Install | Skill |")
lines.append("|------|-------------|-------------|---------|-------|")
for cli in sorted(clis, key=lambda x: x['name']):
name = cli['display_name']
desc = cli['description']
entry = f"`{cli['entry_point']}`"
install = f"`cli-hub install {cli['name']}`"
lines.append(f"| **{name}** | {desc} | {entry} | {install} |")
skill = cli.get('skill_md') or '—'
skill_cell = f"`{skill}`" if not str(skill).startswith("http") else skill
lines.append(f"| **{name}** | {desc} | {entry} | {install} | {skill_cell} |")
lines.append("")
if matrices:
lines.extend([
"## Curated Matrices",
"",
"Each matrix is a curated multi-CLI workflow pulled from the CLI Matrix. Installing a matrix installs all member CLIs and points you at a matrix-specific SKILL.md.",
"",
"| Matrix | Description | CLIs | Install | Skill |",
"|--------|-------------|------|---------|-------|",
])
for matrix in sorted(matrices, key=lambda x: x['name']):
skill = matrix.get('skill_md') or '—'
install = f"`cli-hub matrix install {matrix['name']}`"
lines.append(
f"| **{matrix['display_name']}** | {matrix['description']} | {len(matrix.get('clis', []))} | {install} | `{skill}` |"
)
lines.append("")
@@ -119,6 +162,7 @@ def main():
"- **uv CLIs**: installed via `uv tool install`",
"- **brew/script CLIs**: installed via the tool's native installer",
"- **bundled CLIs**: detected from PATH (pre-installed with the host app)",
"- **Matrices**: install a curated set of harness and public CLIs in one command",
"",
"## Harness CLI Usage Pattern",
"",
@@ -152,7 +196,10 @@ def main():
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text('\n'.join(lines) + '\n')
print(f"Generated meta-skill with {len(data['clis'])} harness CLIs + {len(public_clis)} public CLIs ({total_count} total) at {output_path}")
print(
f"Generated meta-skill with {len(data['clis'])} harness CLIs + "
f"{len(public_clis)} public CLIs + {len(matrices)} matrices at {output_path}"
)
if __name__ == '__main__':
main()
+5
View File
@@ -8,7 +8,9 @@ on:
- '*/agent-harness/**'
- 'registry.json'
- 'public_registry.json'
- 'matrix_registry.json'
- 'cli-hub/**'
- 'cli-hub-matrix/**'
- 'docs/hub/**'
- '.github/workflows/deploy-pages.yml'
- '.github/scripts/update_registry_dates.py'
@@ -61,6 +63,9 @@ jobs:
- name: Copy registry.json to hub for cli-hub access
run: cp registry.json docs/hub/registry.json
- name: Copy matrix_registry.json to hub for cli-hub access
run: cp matrix_registry.json docs/hub/matrix_registry.json
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
+169
View File
@@ -0,0 +1,169 @@
---
name: cli-hub-matrix-video-creation
description: >-
Curated multi-CLI matrix for generating, editing, captioning, and packaging
videos across CLI-Anything harnesses, public CLIs, Python libraries, and cloud APIs.
---
# Video Creation Matrix
This matrix covers end-to-end video production. Each stage lists a **primary goal**, **CLI tools** from the matrix, and **alternative approaches** (Python libraries, cloud APIs, native commands). Pick the approach that fits your current environment and constraints.
## Install
```bash
# Install the whole matrix
cli-hub matrix install video-creation
# Inspect what it includes
cli-hub matrix info video-creation
```
## Stages
### 1. AI Video Generation
**Goal:** Generate video clips from text prompts or reference images
| Approach | Options |
|----------|---------|
| CLI tools | `generate-veo-video` (Google Veo 3.1), `jimeng` (ByteDance Dreamina) |
| Python libs | `replicate` (Runway, Kling), `diffusers` |
| Cloud APIs | Runway API, Kling API, Pika API |
### 2. Screen/Cam Capture
**Goal:** Record screen activity, webcam, or application windows to video files
| Approach | Options |
|----------|---------|
| CLI tools | `cli-anything-openscreen` |
| Python libs | `pyautogui` + `cv2`, `mss` |
| Native cmds | `ffmpeg -f x11grab` (Linux), `screencapture` (macOS) |
### 3. Audio Capture/Edit
**Goal:** Record, clean, trim, normalize, and process audio tracks
| Approach | Options |
|----------|---------|
| CLI tools | `cli-anything-audacity` |
| Python libs | `pydub`, `soundfile`, `librosa`, `noisereduce` |
| Native cmds | `sox`, `ffmpeg` |
### 4. Voice/TTS
**Goal:** Generate speech audio from text for narration or voiceover
| Approach | Options |
|----------|---------|
| CLI tools | `minimax-cli`, `elevenlabs` |
| Python libs | `TTS` (coqui-ai), `pyttsx3`, `edge-tts` |
| Cloud APIs | Google Cloud TTS, Amazon Polly, Azure Speech |
### 5. Music/BGM Generation
**Goal:** Generate background music or sound effects from prompts
| Approach | Options |
|----------|---------|
| CLI tools | `minimax-cli`, `suno` |
| Python libs | `audiocraft` (Meta MusicGen), `stable-audio-tools` |
| Cloud APIs | Suno API, Udio API |
### 6. NLE Editing
**Goal:** Assemble video/audio clips on a timeline, add transitions, and export the final cut
| Approach | Options |
|----------|---------|
| CLI tools | `cli-anything-kdenlive`, `cli-anything-shotcut` |
| Python libs | `moviepy`, `ffmpeg-python` |
| Native cmds | `ffmpeg concat/filter_complex` |
### 7. Captions/Subtitles
**Goal:** Transcribe speech, generate subtitles (SRT/ASS), optionally translate, and burn into video
| Approach | Options |
|----------|---------|
| CLI tools | `cli-anything-videocaptioner` |
| Python libs | `whisper` (openai-whisper), `pysrt`, `stable-ts` |
| Cloud APIs | Google Speech-to-Text, AssemblyAI, Deepgram |
| Native cmds | `ffmpeg -vf subtitles=subs.srt` |
### 8. Thumbnail
**Goal:** Create a thumbnail image or social card for the video
| Approach | Options |
|----------|---------|
| CLI tools | `cli-anything-gimp` |
| Python libs | `Pillow`, `cairosvg`, `html2image` |
| Native cmds | `ffmpeg -ss 00:01:00 -frames:v 1 thumb.png`, ImageMagick `convert` |
## Discovering Additional Tools
Before starting a stage, search for skills and tools that may already be available:
```bash
# Search for Claude Code skills by keyword
npx skills search "subtitle"
npx skills search "video editing"
npx skills search "text-to-speech"
# Check what CLI-Hub has available
cli-hub search caption
cli-hub search audio
cli-hub search video
# Check for Python packages already installed
pip list 2>/dev/null | grep -iE "whisper|moviepy|pydub|Pillow"
# Check for native tools on PATH
which ffmpeg sox convert magick 2>/dev/null
```
When a stage's CLI tool is not installed and installing it is impractical, fall back to:
1. A Python library you can `pip install` in seconds
2. A native command already on the system (`ffmpeg`, `sox`, `ImageMagick`)
3. A cloud API if credentials are available in the environment
## Operating Pattern
1. The controller agent drafts the script, shot list, and prompt variants.
2. For each stage, check what tools are available (installed CLIs, Python libs on PATH, API keys in env).
3. Pick the simplest adequate tool for each stage -- a full CLI harness is not always needed.
4. Generate/capture media, assemble the edit, add captions, and create the thumbnail.
5. Keep all intermediate assets under a single workspace directory so sources stay linkable across tools.
## Example Flow
```bash
# 1) Discover what is available
cli-hub matrix info video-creation
npx skills search "video"
which ffmpeg && echo "ffmpeg available"
# 2) Generate hero clips
cli-hub launch generate-veo-video --help
# or: python -c "import replicate; ..."
# 3) Generate narration
cli-hub launch elevenlabs --help
# or: pip install edge-tts && edge-tts --text "Hello" --write-media narration.mp3
# 4) Assemble edit
cli-anything-kdenlive --json project new -o edit.json
cli-anything-kdenlive --json --project edit.json timeline add-track --type video
# or: python -c "from moviepy.editor import *; ..."
# 5) Add subtitles
cli-anything-videocaptioner process cut.mp4 --asr bijian --subtitle-mode hard -o captioned.mp4
# or: pip install openai-whisper && python -c "import whisper; model = whisper.load_model('base'); ..."
# 6) Create thumbnail
cli-anything-gimp --json project new -o thumb.json
# or: python -c "from PIL import Image, ImageDraw, ImageFont; ..."
```
## Agent Guidance
- **Check before installing**: If `whisper` is already pip-installed, use it directly instead of installing `cli-anything-videocaptioner` for a simple transcription task.
- **Match complexity to task**: A one-line `ffmpeg` command may beat a full CLI harness for simple operations like extracting a thumbnail or concatenating two clips.
- **Search for skills first**: Before each stage, run `npx skills search "<keyword>"` with the stage's topic to discover relevant skills you may not know about.
- **Prefer `--json` for harness CLIs**: When chaining harness CLI outputs to the next tool, use `--json` for structured data.
- **Use env vars for API routing**: Check for `GOOGLE_CLOUD_PROJECT`, `ELEVENLABS_API_KEY`, `MINIMAX_API_KEY`, etc. before choosing between CLI and direct API approaches.
- The matrix assumes a controller agent (Claude Code, Codex, OpenClaw, etc.) is doing the planning and orchestration.
+152 -4
View File
@@ -3,12 +3,15 @@
import os
import shutil
import sys
import json as json_mod
import click
from cli_hub import __version__
from cli_hub.registry import fetch_all_clis, get_cli, search_clis, list_categories
from cli_hub.installer import install_cli, uninstall_cli, get_installed, update_cli
from cli_hub.matrix import fetch_all_matrices, get_matrix, search_matrices
from cli_hub.matrix_skill import get_rendered_matrix_skill_path
from cli_hub.installer import install_cli, uninstall_cli, get_installed, update_cli, install_matrix
from cli_hub.analytics import (
detect_invocation_context,
track_first_run,
@@ -37,7 +40,7 @@ def _invocation_command(ctx, version):
@click.option("--version", is_flag=True, help="Show version.")
@click.pass_context
def main(ctx, version):
"""cli-hub — Download and manage CLI-Anything harnesses and public CLIs."""
"""cli-hub — Download and manage CLI-Anything CLIs and curated matrices."""
track_first_run()
track_visit(command=_invocation_command(ctx, version), detection=detect_invocation_context())
if version:
@@ -128,7 +131,6 @@ def list_clis(category, source, as_json):
installed = get_installed()
if as_json:
import json as json_mod
click.echo(json_mod.dumps(clis, indent=2))
return
@@ -168,7 +170,6 @@ def search(query, as_json):
results = search_clis(query)
if as_json:
import json as json_mod
click.echo(json_mod.dumps(results, indent=2))
return
@@ -216,6 +217,8 @@ def info(name):
click.echo(f" Version: {cli['version']}")
click.echo(f" Requires: {cli.get('requires') or 'nothing'}")
click.echo(f" Entry point: {cli['entry_point']}")
if cli.get("skill_md"):
click.echo(f" Skill: {cli['skill_md']}")
click.echo(f" Homepage: {cli.get('homepage', 'N/A')}")
contributors = cli.get("contributors", [])
if contributors:
@@ -250,5 +253,150 @@ def launch(name, args):
os.execvp(entry, [entry] + list(args))
@main.group(name="matrix", invoke_without_command=True)
@click.pass_context
def matrix(ctx):
"""Browse and install curated multi-CLI workflow matrices."""
if ctx.invoked_subcommand is None:
click.echo(ctx.get_help())
@matrix.command("list")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
def list_matrices(as_json):
"""List all available matrices."""
try:
matrices = fetch_all_matrices()
except Exception as e:
click.secho(f"Failed to fetch matrix registry: {e}", fg="red", err=True)
raise SystemExit(1)
installed = get_installed()
if as_json:
click.echo(json_mod.dumps(matrices, indent=2))
return
if not matrices:
click.echo("No matrices found.")
return
click.secho("\n MATRICES", fg="blue", bold=True)
for matrix_item in sorted(matrices, key=lambda s: s["name"]):
installed_count = sum(1 for cli_name in matrix_item.get("clis", []) if cli_name in installed)
total = len(matrix_item.get("clis", []))
marker = click.style(" ●", fg="green") if total and installed_count == total else " "
name = click.style(f"{matrix_item['name']:20s}", bold=True)
matrix_label = click.style(f"[{matrix_item.get('matrix_id', 'matrix')}]", fg="cyan")
click.echo(f" {marker} {name} {matrix_label} {matrix_item['description'][:65]}")
click.echo(f" Includes: {installed_count}/{total} CLIs installed")
click.echo(f"\n {len(matrices)} matrices available")
@matrix.command("search")
@click.argument("query")
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
def matrix_search(query, as_json):
"""Search matrices by name, description, or category."""
results = search_matrices(query)
if as_json:
click.echo(json_mod.dumps(results, indent=2))
return
if not results:
click.echo(f"No matrices matching '{query}'.")
return
installed = get_installed()
for matrix_item in results:
cli_names = matrix_item.get("clis", [])
installed_count = sum(1 for c in cli_names if c in installed)
total = len(cli_names)
name_str = click.style(matrix_item["name"], bold=True)
matrix_label = click.style(f"[{matrix_item.get('matrix_id', 'matrix')}]", fg="cyan")
click.echo(f" {name_str} {matrix_label} — {matrix_item['description'][:65]}")
click.echo(f" CLIs: {installed_count}/{total} installed")
click.echo(f" Install: cli-hub matrix install {matrix_item['name']}")
@matrix.command("info")
@click.argument("name")
def matrix_info(name):
"""Show details for a specific matrix."""
matrix_item = get_matrix(name)
if not matrix_item:
click.secho(f"Matrix '{name}' not found.", fg="red", err=True)
raise SystemExit(1)
installed = get_installed()
cli_names = matrix_item.get("clis", [])
installed_count = sum(1 for cli_name in cli_names if cli_name in installed)
click.secho(f"\n {matrix_item['display_name']}", bold=True)
click.echo(f" {matrix_item['description']}")
click.echo(f" Matrix: {matrix_item.get('matrix', 'N/A')} {matrix_item.get('matrix_id', '')}".rstrip())
click.echo(f" Category: {matrix_item.get('category', 'N/A')}")
click.echo(f" CLIs: {len(cli_names)}")
click.echo(f" Installed: {installed_count}/{len(cli_names)}")
if matrix_item.get("skill_md"):
click.echo(f" Skill: {matrix_item['skill_md']}")
rendered_skill_path = get_rendered_matrix_skill_path(matrix_item["name"])
if rendered_skill_path.exists():
click.echo(f" Local skill: {rendered_skill_path}")
if matrix_item.get("homepage"):
click.echo(f" Homepage: {matrix_item['homepage']}")
click.echo("\n Members:")
for cli_name in cli_names:
status = click.style("installed", fg="green") if cli_name in installed else "not installed"
click.echo(f" - {cli_name} ({status})")
stages = matrix_item.get("stages", [])
if stages:
click.echo("\n Stage Coverage:")
for stage in stages:
members = ", ".join(stage.get("clis", []))
goal = stage.get("goal", "")
goal_suffix = f" -- {goal}" if goal else ""
click.echo(f" - {stage['name']}: {members}{goal_suffix}")
click.echo(f"\n Install: cli-hub matrix install {matrix_item['name']}")
click.echo()
@matrix.command("install")
@click.argument("name")
def matrix_install(name):
"""Install every CLI in a matrix."""
click.echo(f"Installing matrix {name}...")
success, payload = install_matrix(name)
if payload.get("error"):
click.secho(f"✗ {payload['error']}", fg="red", err=True)
raise SystemExit(1)
matrix_item = payload["matrix"]
for result in payload["results"]:
status = result["status"]
prefix = "✓" if status in {"installed", "skipped"} else "✗"
color = "green" if status in {"installed", "skipped"} else "red"
click.secho(f" {prefix} {result['name']}: {result['message']}", fg=color, err=status == "failed")
summary = payload["summary"]
click.echo(
f"\n Summary: {summary['installed']} installed, "
f"{summary['skipped']} skipped, {summary['failed']} failed"
)
if matrix_item.get("skill_md"):
click.echo(f" Matrix skill: {matrix_item['skill_md']}")
if payload.get("rendered_skill_path"):
click.echo(f" Local matrix skill: {payload['rendered_skill_path']}")
click.echo(f" Inspect: cli-hub matrix info {matrix_item['name']}")
if not success:
raise SystemExit(1)
if __name__ == "__main__":
main()
+66 -1
View File
@@ -1,4 +1,4 @@
"""Install, uninstall, and manage CLIs — dispatches to pip or npm based on source."""
"""Install, uninstall, and manage CLIs and matrices."""
import json
import shlex
@@ -8,6 +8,8 @@ import sys
from pathlib import Path
from cli_hub.registry import get_cli
from cli_hub.matrix import get_matrix
from cli_hub.matrix_skill import render_matrix_skill_file
INSTALLED_FILE = Path.home() / ".cli-hub" / "installed.json"
@@ -371,3 +373,66 @@ def update_cli(name):
def get_installed():
"""Return dict of installed CLIs."""
return _load_installed()
def install_matrix(name):
"""Install every CLI in a named matrix.
Returns ``(success, payload)`` where payload contains the matrix metadata,
per-CLI results, and an aggregate summary. Success is false when any member
install fails or the matrix cannot be found.
"""
matrix_item = get_matrix(name)
if matrix_item is None:
return False, {"error": f"Matrix '{name}' not found. Use 'cli-hub matrix list' to see available matrices."}
installed = set(get_installed())
results = []
for cli_name in matrix_item.get("clis", []):
cli = get_cli(cli_name)
display_name = cli["display_name"] if cli else cli_name
if cli_name in installed:
results.append({
"name": cli_name,
"display_name": display_name,
"status": "skipped",
"message": "Already installed",
})
continue
if cli is None:
results.append({
"name": cli_name,
"display_name": display_name,
"status": "failed",
"message": "CLI not found in registry",
})
continue
success, msg = install_cli(cli_name)
results.append({
"name": cli_name,
"display_name": display_name,
"status": "installed" if success else "failed",
"message": msg,
})
if success:
installed.add(cli_name)
summary = {
"total": len(results),
"installed": sum(1 for result in results if result["status"] == "installed"),
"skipped": sum(1 for result in results if result["status"] == "skipped"),
"failed": sum(1 for result in results if result["status"] == "failed"),
}
installed_state = get_installed()
rendered_skill_path = render_matrix_skill_file(matrix_item, installed=installed_state)
payload = {
"matrix": matrix_item,
"results": results,
"summary": summary,
"rendered_skill_path": str(rendered_skill_path),
}
return summary["failed"] == 0, payload
+82
View File
@@ -0,0 +1,82 @@
"""Fetch, cache, and query curated CLI workflow matrices."""
import json
import time
from pathlib import Path
import requests
MATRIX_REGISTRY_URL = "https://hkuds.github.io/CLI-Anything/matrix_registry.json"
MATRIX_CACHE_FILE = Path.home() / ".cli-hub" / "matrix_registry_cache.json"
CACHE_TTL = 3600 # 1 hour
def _ensure_cache_dir():
MATRIX_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
def _load_cached_data():
if not MATRIX_CACHE_FILE.exists():
return None
try:
cached = json.loads(MATRIX_CACHE_FILE.read_text())
return cached["data"]
except (json.JSONDecodeError, KeyError):
return None
def fetch_matrix_registry(force_refresh=False):
"""Fetch the matrix registry with local file caching."""
_ensure_cache_dir()
if not force_refresh and MATRIX_CACHE_FILE.exists():
try:
cached = json.loads(MATRIX_CACHE_FILE.read_text())
if time.time() - cached.get("_cached_at", 0) < CACHE_TTL:
return cached["data"]
except (json.JSONDecodeError, KeyError):
pass
try:
resp = requests.get(MATRIX_REGISTRY_URL, timeout=15)
resp.raise_for_status()
data = resp.json()
except (requests.RequestException, ValueError):
cached_data = _load_cached_data()
if cached_data is not None:
return cached_data
raise
MATRIX_CACHE_FILE.write_text(json.dumps({"_cached_at": time.time(), "data": data}, indent=2))
return data
def fetch_all_matrices(force_refresh=False):
"""Return all matrix entries."""
return fetch_matrix_registry(force_refresh).get("matrices", [])
def get_matrix(name, force_refresh=False):
"""Look up a matrix entry by name (case-insensitive)."""
name_lower = name.lower()
for matrix_item in fetch_all_matrices(force_refresh):
if matrix_item["name"].lower() == name_lower:
return matrix_item
return None
def search_matrices(query, force_refresh=False):
"""Search matrices by name, display name, description, or category."""
query_lower = query.lower()
results = []
for matrix_item in fetch_all_matrices(force_refresh):
haystack_values = [
matrix_item["name"],
matrix_item.get("display_name", ""),
matrix_item.get("description", ""),
matrix_item.get("category", ""),
matrix_item.get("matrix_id", ""),
]
if any(query_lower in value.lower() for value in haystack_values):
results.append(matrix_item)
return results
+225
View File
@@ -0,0 +1,225 @@
"""Render local matrix skill files with resolved CLI skill paths."""
import subprocess
from importlib import metadata
from pathlib import Path
from cli_hub.registry import get_cli
MATRIX_SKILL_DIR = Path.home() / ".cli-hub" / "matrix"
def _find_repo_root():
"""Find the repository root via git, falling back to parent traversal."""
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
root = Path(result.stdout.strip())
if root.is_dir():
return root
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
# Fallback: walk up from this file looking for .git
current = Path(__file__).resolve().parent
for parent in [current] + list(current.parents):
if (parent / ".git").exists():
return parent
return None
def get_rendered_matrix_skill_path(name):
"""Return the local rendered SKILL.md path for a matrix."""
return MATRIX_SKILL_DIR / f"{name}.SKILL.md"
def resolve_local_skill_path(cli):
"""Resolve an installed harness CLI's local SKILL.md path if possible."""
if cli.get("_source", "harness") != "harness":
return None
dist_name = cli.get("dist_name") or f"cli-anything-{cli['name']}"
try:
dist = metadata.distribution(dist_name)
except metadata.PackageNotFoundError:
return _fallback_repo_skill_path(cli)
for file in dist.files or []:
file_str = str(file).replace("\\", "/")
if file_str.endswith("/skills/SKILL.md") or file_str.endswith("skills/SKILL.md"):
return str(dist.locate_file(file).resolve())
return _fallback_repo_skill_path(cli)
def _fallback_repo_skill_path(cli):
"""Use the repo-relative skill path when available in the current checkout."""
skill_ref = cli.get("skill_md")
if not skill_ref or "://" in skill_ref or skill_ref.startswith("npx "):
return None
repo_root = _find_repo_root()
if repo_root is None:
return None
candidate = repo_root / skill_ref
if candidate.exists():
return str(candidate.resolve())
return None
def render_matrix_skill_file(matrix_item, installed=None):
"""Write a local matrix SKILL.md with resolved member skill paths."""
output_path = get_rendered_matrix_skill_path(matrix_item["name"])
output_path.parent.mkdir(parents=True, exist_ok=True)
base_content = _load_matrix_skill_template(matrix_item).rstrip()
injected_section = _render_injected_section(matrix_item, installed or {})
output_path.write_text(
f"{base_content}\n\n<!-- MATRIX_SKILL_PATHS:START -->\n\n{injected_section}\n\n<!-- MATRIX_SKILL_PATHS:END -->\n",
encoding="utf-8",
)
return output_path
def _load_matrix_skill_template(matrix_item):
"""Load the tracked matrix skill template when available."""
skill_ref = matrix_item.get("skill_md")
if skill_ref and "://" not in skill_ref and not skill_ref.startswith("npx "):
repo_root = _find_repo_root()
if repo_root is not None:
candidate = repo_root / skill_ref
if candidate.exists():
return candidate.read_text(encoding="utf-8")
title = matrix_item.get("display_name", matrix_item["name"])
description = matrix_item.get("description", "")
return (
f"# {title}\n\n"
f"{description}\n\n"
f"Install with `cli-hub matrix install {matrix_item['name']}`."
)
def _render_injected_section(matrix_item, installed):
"""Render the injected skill reference section."""
lines = [
"## Installed CLI Skills",
"",
"Generated by `cli-hub matrix install` from the current local environment.",
"",
"| CLI | Entry Point | Canonical Skill | Local Skill | Status |",
"|---|---|---|---|---|",
]
for cli_name in matrix_item.get("clis", []):
cli = get_cli(cli_name) or {"name": cli_name, "entry_point": cli_name}
canonical_skill = cli.get("skill_md") or "—"
local_skill = resolve_local_skill_path(cli) or "—"
status = "installed" if cli_name in installed else "not installed"
lines.append(
f"| `{cli_name}` | `{cli.get('entry_point', '—')}` | "
f"{canonical_skill} | {local_skill} | {status} |"
)
stage_tooling = _render_stage_tooling(matrix_item, installed)
if stage_tooling:
lines.append("")
lines.append(stage_tooling)
discovery = _render_discovery_section(matrix_item)
if discovery:
lines.append("")
lines.append(discovery)
return "\n".join(lines)
def _render_stage_tooling(matrix_item, installed):
"""Render per-stage tooling overview with goals and alternatives."""
stages = matrix_item.get("stages", [])
has_goals = any(s.get("goal") for s in stages)
if not has_goals:
return ""
lines = [
"## Stage Tooling Overview",
"",
"What is available for each stage on this system.",
"",
]
for stage in stages:
goal = stage.get("goal")
if not goal:
continue
lines.append(f"### {stage['name']}")
lines.append(f"**Goal:** {goal}")
lines.append("")
for cli_name in stage.get("clis", []):
marker = "installed" if cli_name in installed else "not installed"
lines.append(f"- CLI: `{cli_name}` ({marker})")
alts = stage.get("alternatives", {})
if alts.get("python"):
lines.append(f"- Python: {', '.join(alts['python'])}")
if alts.get("api"):
lines.append(f"- APIs: {', '.join(alts['api'])}")
if alts.get("native"):
lines.append(f"- Native: {', '.join(alts['native'])}")
hints = stage.get("skill_search_hints", [])
if hints:
cmds = ", ".join(f'`npx skills search "{h}"`' for h in hints[:2])
lines.append(f"- Search for skills: {cmds}")
lines.append("")
return "\n".join(lines)
def _render_discovery_section(matrix_item):
"""Render aggregated skill discovery commands from all stages."""
all_hints = []
for stage in matrix_item.get("stages", []):
for hint in stage.get("skill_search_hints", []):
if hint not in all_hints:
all_hints.append(hint)
if not all_hints:
return ""
lines = [
"## Skill Discovery Commands",
"",
"Search for relevant skills and tools across all stages:",
"",
"```bash",
]
for hint in all_hints:
lines.append(f'npx skills search "{hint}"')
lines.extend([
"```",
"",
"Also check CLI-Hub:",
"",
"```bash",
])
seen = set()
for hint in all_hints:
keyword = hint.split()[0].lower()
if keyword not in seen:
seen.add(keyword)
lines.append(f"cli-hub search {keyword}")
lines.append("```")
return "\n".join(lines)
+272 -1
View File
@@ -12,8 +12,11 @@ import requests
from cli_hub import __version__
from cli_hub.registry import fetch_registry, fetch_all_clis, get_cli, search_clis, list_categories
from cli_hub.matrix import fetch_matrix_registry, fetch_all_matrices, get_matrix, search_matrices
from cli_hub.matrix_skill import resolve_local_skill_path, render_matrix_skill_file, _render_stage_tooling, _render_discovery_section
from cli_hub.installer import (
install_cli,
install_matrix,
uninstall_cli,
get_installed,
_load_installed,
@@ -40,7 +43,7 @@ SAMPLE_REGISTRY = {
"homepage": "https://gimp.org",
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness",
"entry_point": "cli-anything-gimp",
"skill_md": "skills/cli-anything-gimp/SKILL.md",
"skill_md": "gimp/agent-harness/cli_anything/gimp/skills/SKILL.md",
"category": "image",
"contributor": "test-user",
"contributor_url": "https://github.com/test-user",
@@ -76,6 +79,39 @@ SAMPLE_REGISTRY = {
],
}
SAMPLE_MATRIX_REGISTRY = {
"meta": {"repo": "https://github.com/HKUDS/CLI-Anything", "description": "test matrices"},
"matrices": [
{
"name": "video-creation",
"display_name": "Video Creation & Editing",
"description": "Curated video workflow matrix",
"category": "video",
"matrix": "cli-matrix",
"matrix_id": "V1",
"skill_md": "cli-hub-matrix/video-creation/SKILL.md",
"clis": ["gimp", "blender", "audacity"],
"stages": [
{
"name": "Thumbnail",
"clis": ["gimp"],
"goal": "Create a thumbnail image",
"alternatives": {"python": ["Pillow"], "native": ["ImageMagick convert"]},
"skill_search_hints": ["thumbnail", "image editing"],
},
{"name": "3D", "clis": ["blender"]},
{
"name": "Audio",
"clis": ["audacity"],
"goal": "Edit and process audio",
"alternatives": {"python": ["pydub"], "native": ["sox"]},
"skill_search_hints": ["audio editing"],
},
],
}
],
}
# ─── Registry tests ───────────────────────────────────────────────────
@@ -152,6 +188,138 @@ class TestRegistry:
cats = list_categories()
assert cats == ["3d", "audio", "image"]
class TestMatrixRegistry:
"""Tests for matrix.py — fetch, cache, search, and lookup."""
@patch("cli_hub.matrix.requests.get")
@patch("cli_hub.matrix.MATRIX_CACHE_FILE", Path(tempfile.mktemp()))
def test_fetch_matrix_registry_from_remote(self, mock_get):
mock_resp = MagicMock()
mock_resp.json.return_value = SAMPLE_MATRIX_REGISTRY
mock_resp.raise_for_status = MagicMock()
mock_get.return_value = mock_resp
result = fetch_matrix_registry(force_refresh=True)
assert result["matrices"][0]["name"] == "video-creation"
mock_get.assert_called_once()
@patch("cli_hub.matrix.fetch_all_matrices", return_value=SAMPLE_MATRIX_REGISTRY["matrices"])
def test_get_matrix_found(self, mock_fetch):
matrix_item = get_matrix("video-creation")
assert matrix_item is not None
assert matrix_item["display_name"] == "Video Creation & Editing"
@patch("cli_hub.matrix.fetch_all_matrices", return_value=SAMPLE_MATRIX_REGISTRY["matrices"])
def test_search_matrices_matches_description(self, mock_fetch):
results = search_matrices("video")
assert len(results) == 1
assert results[0]["name"] == "video-creation"
class TestMatrixSkill:
"""Tests for matrix_skill.py — local skill resolution and rendering."""
@patch("cli_hub.matrix_skill.metadata.distribution")
def test_resolve_local_skill_path_from_distribution(self, mock_distribution, tmp_path):
class FakeDist:
files = [Path("cli_anything/audacity/skills/SKILL.md")]
def locate_file(self, file):
return tmp_path / file
mock_distribution.return_value = FakeDist()
cli = {"name": "audacity", "_source": "harness"}
resolved = resolve_local_skill_path(cli)
assert resolved == str((tmp_path / "cli_anything/audacity/skills/SKILL.md").resolve())
@patch("cli_hub.matrix_skill.MATRIX_SKILL_DIR", Path(tempfile.mkdtemp()))
@patch("cli_hub.matrix_skill.resolve_local_skill_path")
@patch("cli_hub.matrix_skill.get_cli")
def test_render_matrix_skill_file_injects_paths(self, mock_get_cli, mock_resolve, tmp_dir=None):
mock_get_cli.side_effect = lambda name: next((c for c in SAMPLE_REGISTRY["clis"] if c["name"] == name), None)
mock_resolve.side_effect = lambda cli: f"/tmp/{cli['name']}/skills/SKILL.md" if cli["name"] != "blender" else None
rendered = render_matrix_skill_file(SAMPLE_MATRIX_REGISTRY["matrices"][0], installed={"gimp": {}, "audacity": {}})
content = Path(rendered).read_text()
assert "## Installed CLI Skills" in content
assert "/tmp/gimp/skills/SKILL.md" in content
assert "gimp/agent-harness/cli_anything/gimp/skills/SKILL.md" in content
assert "not installed" in content
class TestMultiApproachRendering:
"""Tests for multi-approach stage rendering in matrix_skill.py."""
def test_render_stage_tooling_includes_goals(self):
matrix_item = SAMPLE_MATRIX_REGISTRY["matrices"][0]
result = _render_stage_tooling(matrix_item, installed={"gimp": {}})
assert "## Stage Tooling Overview" in result
assert "Create a thumbnail image" in result
assert "Edit and process audio" in result
def test_render_stage_tooling_includes_alternatives(self):
matrix_item = SAMPLE_MATRIX_REGISTRY["matrices"][0]
result = _render_stage_tooling(matrix_item, installed={})
assert "Pillow" in result
assert "pydub" in result
assert "sox" in result
assert "ImageMagick convert" in result
def test_render_stage_tooling_shows_install_status(self):
matrix_item = SAMPLE_MATRIX_REGISTRY["matrices"][0]
result = _render_stage_tooling(matrix_item, installed={"gimp": {}})
assert "`gimp` (installed)" in result
assert "`audacity` (not installed)" in result
def test_render_stage_tooling_includes_skill_search_hints(self):
matrix_item = SAMPLE_MATRIX_REGISTRY["matrices"][0]
result = _render_stage_tooling(matrix_item, installed={})
assert 'npx skills search "thumbnail"' in result
assert 'npx skills search "audio editing"' in result
def test_render_stage_tooling_backward_compat_no_goal(self):
"""Stages without 'goal' field are skipped gracefully."""
matrix_no_goals = {
"name": "test",
"stages": [
{"name": "Stage1", "clis": ["foo"]},
],
}
result = _render_stage_tooling(matrix_no_goals, installed={})
assert result == ""
def test_render_discovery_section(self):
matrix_item = SAMPLE_MATRIX_REGISTRY["matrices"][0]
result = _render_discovery_section(matrix_item)
assert "## Skill Discovery Commands" in result
assert 'npx skills search "thumbnail"' in result
assert 'npx skills search "audio editing"' in result
assert "cli-hub search thumbnail" in result
assert "cli-hub search audio" in result
def test_render_discovery_section_empty_when_no_hints(self):
matrix_no_hints = {
"name": "test",
"stages": [{"name": "S1", "clis": ["foo"]}],
}
result = _render_discovery_section(matrix_no_hints)
assert result == ""
@patch("cli_hub.matrix_skill.MATRIX_SKILL_DIR", Path(tempfile.mkdtemp()))
@patch("cli_hub.matrix_skill.resolve_local_skill_path")
@patch("cli_hub.matrix_skill.get_cli")
def test_render_matrix_skill_file_includes_stage_tooling(self, mock_get_cli, mock_resolve):
mock_get_cli.side_effect = lambda name: next((c for c in SAMPLE_REGISTRY["clis"] if c["name"] == name), None)
mock_resolve.return_value = None
rendered = render_matrix_skill_file(SAMPLE_MATRIX_REGISTRY["matrices"][0], installed={"gimp": {}})
content = Path(rendered).read_text()
assert "## Stage Tooling Overview" in content
assert "## Skill Discovery Commands" in content
assert "Create a thumbnail image" in content
# ─── Installer tests ──────────────────────────────────────────────────
@@ -484,6 +652,27 @@ class TestScriptStrategy:
assert data["jimeng"]["strategy"] == "command"
assert data["jimeng"]["package_manager"] == "script"
@patch("cli_hub.installer.get_matrix", return_value=SAMPLE_MATRIX_REGISTRY["matrices"][0])
@patch("cli_hub.installer.get_cli", side_effect=lambda name: next((c for c in SAMPLE_REGISTRY["clis"] if c["name"] == name), None))
@patch("cli_hub.installer.install_cli", side_effect=[(True, "Installed Blender"), (False, "Install failed")])
@patch("cli_hub.installer.get_installed", return_value={"gimp": {"version": "1.0.0"}})
@patch("cli_hub.installer.render_matrix_skill_file", return_value=Path("/tmp/video-creation.SKILL.md"))
def test_install_matrix_reports_installed_skipped_and_failed(self, mock_render, mock_installed, mock_install_cli, mock_get_cli, mock_get_matrix):
success, payload = install_matrix("video-creation")
assert not success
assert payload["summary"] == {"total": 3, "installed": 1, "skipped": 1, "failed": 1}
assert payload["results"][0]["status"] == "skipped"
assert payload["results"][1]["status"] == "installed"
assert payload["results"][2]["status"] == "failed"
assert payload["rendered_skill_path"] == "/tmp/video-creation.SKILL.md"
@patch("cli_hub.installer.get_matrix", return_value=None)
def test_install_matrix_not_found(self, mock_get_matrix):
success, payload = install_matrix("missing-matrix")
assert not success
assert "not found" in payload["error"]
# ─── Analytics tests ──────────────────────────────────────────────────
@@ -760,6 +949,87 @@ class TestCLI:
assert "cli-hub" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.fetch_all_matrices", return_value=SAMPLE_MATRIX_REGISTRY["matrices"])
@patch("cli_hub.cli.get_installed", return_value={"gimp": {"version": "1.0.0"}})
def test_matrix_list_command(self, mock_installed, mock_fetch_matrices, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "list"])
assert "video-creation" in result.output
assert "1/3" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.search_matrices", return_value=SAMPLE_MATRIX_REGISTRY["matrices"])
@patch("cli_hub.cli.get_installed", return_value={"gimp": {"version": "1.0.0"}})
def test_matrix_search_command(self, mock_installed, mock_search, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "search", "video"])
assert "video-creation" in result.output
assert "1/3" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.search_matrices", return_value=[])
def test_matrix_search_no_results(self, mock_search, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "search", "nonexistent"])
assert "No matrices matching" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.get_matrix", return_value=SAMPLE_MATRIX_REGISTRY["matrices"][0])
@patch("cli_hub.cli.get_installed", return_value={"gimp": {"version": "1.0.0"}})
@patch("cli_hub.cli.get_rendered_matrix_skill_path", return_value=Path("/tmp/video-creation.SKILL.md"))
@patch("pathlib.Path.exists", return_value=True)
def test_matrix_info_command(self, mock_exists, mock_rendered, mock_installed, mock_get_matrix, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "info", "video-creation"])
assert "Video Creation & Editing" in result.output
assert "cli-hub matrix install video-creation" in result.output
assert "cli-hub-matrix/video-creation/SKILL.md" in result.output
assert "Local skill: /tmp/video-creation.SKILL.md" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.install_matrix", return_value=(False, {
"matrix": SAMPLE_MATRIX_REGISTRY["matrices"][0],
"results": [
{"name": "gimp", "status": "skipped", "message": "Already installed"},
{"name": "blender", "status": "installed", "message": "Installed Blender"},
{"name": "audacity", "status": "failed", "message": "Install failed"},
],
"summary": {"installed": 1, "skipped": 1, "failed": 1},
"rendered_skill_path": "/tmp/video-creation.SKILL.md",
}))
def test_matrix_install_command_partial_failure(self, mock_install_matrix, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "install", "video-creation"])
assert result.exit_code == 1
assert "Summary: 1 installed, 1 skipped, 1 failed" in result.output
assert "Matrix skill: cli-hub-matrix/video-creation/SKILL.md" in result.output
assert "Local matrix skill: /tmp/video-creation.SKILL.md" in result.output
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@patch("cli_hub.cli.install_matrix", return_value=(False, {"error": "Matrix 'missing' not found."}))
def test_matrix_install_command_not_found(self, mock_install_matrix, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["matrix", "install", "missing"])
assert result.exit_code == 1
assert "not found" in result.output
@patch("cli_hub.cli.track_first_run")
@patch("cli_hub.cli.track_visit")
@patch("cli_hub.cli.detect_invocation_context")
@@ -807,6 +1077,7 @@ class TestCLI:
assert "GIMP" in result.output
assert "image" in result.output
assert "Install: cli-hub install gimp" in result.output
assert "Skill:" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
+136
View File
@@ -0,0 +1,136 @@
{
"meta": {
"repo": "https://github.com/HKUDS/CLI-Anything",
"description": "Curated CLI Matrix registry for CLI-Hub multi-CLI workflows",
"updated": "2026-04-17"
},
"matrices": [
{
"name": "video-creation",
"display_name": "Video Creation & Editing",
"version": "2",
"description": "Curated end-to-end video production matrix spanning generation, capture, voice/music, editing, captioning, and thumbnailing for an agent-driven workflow.",
"category": "video",
"matrix": "cli-matrix",
"matrix_id": "V1",
"homepage": "https://github.com/HKUDS/CLI-Anything/blob/main/docs/cli-matrix.md",
"skill_md": "cli-hub-matrix/video-creation/SKILL.md",
"clis": [
"generate-veo-video",
"jimeng",
"openscreen",
"audacity",
"minimax-cli",
"elevenlabs",
"suno",
"kdenlive",
"shotcut",
"videocaptioner",
"gimp"
],
"stages": [
{
"name": "AI video gen",
"goal": "Generate video clips from text prompts or reference images",
"clis": [
"generate-veo-video",
"jimeng"
],
"alternatives": {
"python": ["replicate (Runway, Kling)", "diffusers"],
"api": ["Runway API", "Kling API", "Pika API"]
},
"skill_search_hints": ["video generation", "text-to-video", "AI video"]
},
{
"name": "Screen/cam capture",
"goal": "Record screen activity, webcam, or application windows to video files",
"clis": [
"openscreen"
],
"alternatives": {
"python": ["pyautogui + cv2", "mss"],
"native": ["ffmpeg -f x11grab (Linux)", "screencapture (macOS)"]
},
"skill_search_hints": ["screen recording", "capture", "screencast"]
},
{
"name": "Audio capture/edit",
"goal": "Record, clean, trim, normalize, and process audio tracks",
"clis": [
"audacity"
],
"alternatives": {
"python": ["pydub", "soundfile", "librosa", "noisereduce"],
"native": ["sox", "ffmpeg"]
},
"skill_search_hints": ["audio editing", "noise reduction", "audio processing"]
},
{
"name": "Voice/TTS",
"goal": "Generate speech audio from text for narration or voiceover",
"clis": [
"minimax-cli",
"elevenlabs"
],
"alternatives": {
"python": ["TTS (coqui-ai)", "pyttsx3", "edge-tts"],
"api": ["Google Cloud TTS", "Amazon Polly", "Azure Speech"]
},
"skill_search_hints": ["text-to-speech", "TTS", "voice synthesis", "narration"]
},
{
"name": "Music/BGM gen",
"goal": "Generate background music or sound effects from prompts",
"clis": [
"minimax-cli",
"suno"
],
"alternatives": {
"python": ["audiocraft (Meta MusicGen)", "stable-audio-tools"],
"api": ["Suno API", "Udio API"]
},
"skill_search_hints": ["music generation", "background music", "audio synthesis"]
},
{
"name": "NLE editing",
"goal": "Assemble video/audio clips on a timeline, add transitions, and export the final cut",
"clis": [
"kdenlive",
"shotcut"
],
"alternatives": {
"python": ["moviepy", "ffmpeg-python"],
"native": ["ffmpeg concat/filter_complex"]
},
"skill_search_hints": ["video editing", "timeline", "NLE", "video assembly"]
},
{
"name": "Captions/subs",
"goal": "Transcribe speech, generate subtitles (SRT/ASS), optionally translate, and burn into video",
"clis": [
"videocaptioner"
],
"alternatives": {
"python": ["whisper (openai-whisper)", "pysrt", "stable-ts"],
"api": ["Google Speech-to-Text", "AssemblyAI", "Deepgram"],
"native": ["ffmpeg -vf subtitles=subs.srt"]
},
"skill_search_hints": ["subtitle", "transcribe", "caption", "speech-to-text"]
},
{
"name": "Thumbnail",
"goal": "Create a thumbnail image or social card for the video",
"clis": [
"gimp"
],
"alternatives": {
"python": ["Pillow", "cairosvg", "html2image"],
"native": ["ffmpeg -ss 00:01:00 -frames:v 1 thumb.png", "ImageMagick convert"]
},
"skill_search_hints": ["image editing", "thumbnail", "graphic design"]
}
]
}
]
}