diff --git a/.github/scripts/generate_meta_skill.py b/.github/scripts/generate_meta_skill.py
index c3bba90d5..11fcfdf23 100644
--- a/.github/scripts/generate_meta_skill.py
+++ b/.github/scripts/generate_meta_skill.py
@@ -32,11 +32,19 @@ def main():
"## Quick Install",
"",
"```bash",
- "# Install any CLI",
- f"pip install git+{data['meta']['repo']}.git#subdirectory=/agent-harness",
+ "# First, install the CLI Hub package manager",
+ "pip install cli-anything-hub",
"",
- "# Example: Install GIMP CLI",
- f"pip install git+{data['meta']['repo']}.git#subdirectory=gimp/agent-harness",
+ "# Browse available CLIs",
+ "cli-hub list",
+ "",
+ "# Install any CLI by name",
+ "cli-hub install gimp",
+ "cli-hub install blender",
+ "",
+ "# Search by category or keyword",
+ "cli-hub search image",
+ "cli-hub search 3d",
"```",
"",
"## Available CLIs",
@@ -53,12 +61,19 @@ def main():
for cli in sorted(clis, key=lambda x: x['name']):
name = cli['display_name']
desc = cli['description']
- install = f"`{cli['install_cmd']}`"
+ install = f"`cli-hub install {cli['name']}`"
lines.append(f"| **{name}** | {desc} | {install} |")
lines.append("")
lines.extend([
+ "## How It Works",
+ "",
+ "`cli-hub` is a lightweight wrapper around `pip`. When you run `cli-hub install gimp`,",
+ "it installs a separate Python package (`cli-anything-gimp`) with its own CLI entry point",
+ "(`cli-anything-gimp`). Each CLI is an independent pip package โ `cli-hub` simply resolves",
+ "names from the registry and tracks installs.",
+ "",
"## Usage Pattern",
"",
"All CLIs follow the same pattern:",
@@ -76,15 +91,17 @@ def main():
"",
"## For AI Agents",
"",
- "1. Install the CLI you need from the table above",
- "2. Read its full SKILL.md at the repo path shown in registry.json",
- "3. Always use `--json` flag for machine-readable output",
- "4. Check exit codes (0=success, non-zero=error)",
+ "1. Install the hub: `pip install cli-anything-hub`",
+ "2. Install the CLI you need: `cli-hub install ` (installs `cli-anything-` pip package)",
+ "3. Run: `cli-anything-` for REPL, or `cli-anything- ` for one-shot",
+ "4. Read its full SKILL.md at the repo path shown in registry.json",
+ "5. Always use `--json` flag for machine-readable output",
+ "6. Check exit codes (0=success, non-zero=error)",
"",
"## More Info",
"",
f"- Repository: {data['meta']['repo']}",
- "- Web Hub: https://hkuds.github.io/CLI-Anything/",
+ "- Web Hub: https://clianything.cc",
f"- Last Updated: {data['meta']['updated']}",
])
diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml
index b0e4c1984..10339ed07 100644
--- a/.github/workflows/deploy-pages.yml
+++ b/.github/workflows/deploy-pages.yml
@@ -7,6 +7,8 @@ on:
paths:
- '*/agent-harness/**'
- 'registry.json'
+ - 'cli-hub/**'
+ - 'docs/hub/**'
- '.github/workflows/deploy-pages.yml'
- '.github/scripts/update_registry_dates.py'
- '.github/scripts/generate_meta_skill.py'
@@ -44,6 +46,9 @@ jobs:
- name: Copy catalog as .txt to avoid Jekyll processing
run: cp cli-hub-skill/SKILL.md docs/hub/SKILL.txt
+ - name: Copy registry.json to hub for cli-hub access
+ run: cp registry.json docs/hub/registry.json
+
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
diff --git a/.gitignore b/.gitignore
index d74f9c3f2..562fcb472 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,7 @@
!/skill_generation/
!/openclaw-skill/
!/cli-hub-meta-skill/
+!/cli-hub/
# Ignore cli-hub-skill (auto-generated, not tracked)
/cli-hub-skill/
diff --git a/README.md b/README.md
index 57bfa2039..1b93679ff 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
CLI-Anything: Bridging the Gap Between AI Agents and the World's Software
-**๐ [CLI-Hub](https://hkuds.github.io/CLI-Anything/)**: Explore all community-built CLIs and install with one command at the **[CLI-Hub](https://hkuds.github.io/CLI-Anything/)**. Want to add your own? [Open a PR](https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md) โ the hub updates instantly.
+**๐ [CLI-Hub](https://hkuds.github.io/CLI-Anything/)**: `pip install cli-anything-hub` then `cli-hub install ` โ browse, install, and manage all community-built CLIs. Want to add your own? [Open a PR](https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md) โ the hub updates instantly.
**๐ฌ [See Demos](#-real-world-demos)**: Watch AI agents use generated CLIs to produce real artifacts โ diagrams, gameplay, subtitles, and more.
@@ -45,7 +45,13 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software Thanks to all invaluable efforts from the community! More updates continuously on the way everyday..
-- **2026-04-08** ๐ฌ **Openscreen CLI** merged (#183) โ screen recording editor harness with fused manual + auto-generated tests (101 tests).
+- **2026-04-10** ๐ฆ **CLI-Hub package manager** launched โ `pip install cli-anything-hub` to browse, install, and manage all CLIs. Download tracking via Umami analytics. Hub frontend updated with new install flow and "Empower yourself" toolkit card.
+
+- **2026-04-09** ๐งน Cleanup and docs pass (#200) โ fixed Openscreen test subtotals, added Openscreen to Chinese README and project structure.
+
+- **2026-04-08** ๐ฌ **Openscreen CLI** merged (#183) โ screen recording editor harness with 101 tests. โ๏ธ **CloudAnalyzer CLI** merged (#181) โ cloud cost analysis harness with 27 commands. ๐ **SeaClip / PM2 / ChromaDB** harnesses merged (#129).
+
+- **2026-04-07** ๐ **Dify Workflow CLI** merged (#191) โ workflow automation wrapper. ๐ง **Inkscape** auto-save fix (#193, fixes #182). ๐ก๏ธ **DomShell security hardening** (#156) โ URL validation and DOM sanitization for browser CLI. ๐ฅง **Pi Coding Agent extension** merged (#178).
- **2026-04-06** ๐ **Exa CLI** merged (#172) โ AI-powered web search and answers harness. ๐ฎ **Godot CLI** merged (#140) โ game engine harness with full demo-game E2E pipeline. CLI-Hub frontend improvements.
diff --git a/cli-hub-meta-skill/SKILL.md b/cli-hub-meta-skill/SKILL.md
index 802ac6c12..25fe7e427 100644
--- a/cli-hub-meta-skill/SKILL.md
+++ b/cli-hub-meta-skill/SKILL.md
@@ -9,13 +9,33 @@ description: >-
CLI-Hub is a marketplace of agent-native command-line interfaces that make professional software accessible to AI agents.
+## Quick Start
+
+```bash
+# Install the CLI Hub package manager
+pip install cli-anything-hub
+
+# Browse all available CLIs
+cli-hub list
+
+# Search by category or keyword
+cli-hub search image
+cli-hub search "3d modeling"
+
+# Install a CLI
+cli-hub install gimp
+
+# Show details for a CLI
+cli-hub info gimp
+```
+
## Live Catalog
-**URL**: [`https://hkuds.github.io/CLI-Anything/SKILL.txt`](https://hkuds.github.io/CLI-Anything/SKILL.txt)
+**URL**: [`https://clianything.cc/SKILL.txt`](https://clianything.cc/SKILL.txt)
The catalog is auto-updated and provides:
- Full list of available CLIs organized by category
-- One-line `pip install` commands for each tool
+- One-line `cli-hub install` commands for each tool
- Complete descriptions and usage patterns
**Note**: The file is served as `.txt` but contains markdown formatting for easy parsing.
@@ -33,26 +53,36 @@ CLI-Hub covers a broad range of software and codebases, empowering agents to con
Each CLI provides stateful operations, JSON output for agents, REPL mode, and integrates with real software backends.
+## How It Works
+
+`cli-hub` is a lightweight wrapper around `pip`. When you run `cli-hub install gimp`, it installs a separate Python package (`cli-anything-gimp`) with its own CLI entry point (`cli-anything-gimp`). Each CLI is an independent pip package โ `cli-hub` simply resolves names from the registry and tracks installs.
+
## How to Use
-1. **Read the catalog**: Fetch `https://hkuds.github.io/CLI-Anything/SKILL.txt` (markdown format)
-2. **Find your tool**: Browse by category to discover the CLI you need
-3. **Install**: Use the provided `pip install` command
-4. **Execute**: All CLIs support `--json` flag for machine-readable output
+1. **Install cli-hub**: `pip install cli-anything-hub`
+2. **Find your tool**: `cli-hub search ` or `cli-hub list -c `
+3. **Install**: `cli-hub install ` (installs the `cli-anything-` pip package)
+4. **Run**: `cli-anything-` for REPL, or `cli-anything- ` for one-shot
+5. **JSON output**: All CLIs support `--json` flag for machine-readable output
## Example Workflow
```bash
-# Fetch the catalog to find available tools
-# Install the CLI you need
-pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=/agent-harness
+# Install the hub
+pip install cli-anything-hub
+
+# Find what you need
+cli-hub search video
+
+# Install it
+cli-hub install kdenlive
# Use it with JSON output
-cli-anything- --json [options]
+cli-anything-kdenlive --json project create --name my-project
```
## More Info
-- Live Catalog: https://hkuds.github.io/CLI-Anything/SKILL.txt
-- Web Hub: https://hkuds.github.io/CLI-Anything/
+- Live Catalog: https://clianything.cc/SKILL.txt
+- Web Hub: https://clianything.cc
- Repository: https://github.com/HKUDS/CLI-Anything
diff --git a/cli-hub/README.md b/cli-hub/README.md
new file mode 100644
index 000000000..0830e6b9b
--- /dev/null
+++ b/cli-hub/README.md
@@ -0,0 +1,85 @@
+# cli-hub
+
+Package manager for [CLI-Anything](https://github.com/HKUDS/CLI-Anything) โ a framework that auto-generates stateful CLI interfaces for GUI applications, making them agent-native.
+
+Browse, install, and manage 40+ CLI harnesses for software like GIMP, Blender, Inkscape, LibreOffice, Audacity, OBS Studio, and more โ all from your terminal.
+
+**Web Hub**: [clianything.cc](https://clianything.cc)
+
+## Install
+
+```bash
+pip install cli-anything-hub
+```
+
+## Usage
+
+```bash
+# Browse all available CLIs, grouped by category
+cli-hub list
+
+# Filter by category (image, 3d, video, audio, office, ai, ...)
+cli-hub list -c image
+
+# Search by name, description, or category
+cli-hub search "3d modeling"
+
+# Show details for a CLI
+cli-hub info gimp
+
+# Install a CLI harness
+cli-hub install gimp
+
+# Update a CLI to the latest version
+cli-hub update gimp
+
+# Uninstall a CLI
+cli-hub uninstall gimp
+```
+
+## What gets installed
+
+Each CLI harness is a standalone Python package that wraps a real application (GIMP, Blender, etc.) with a stateful command-line interface. Every harness supports:
+
+- **REPL mode**: `cli-anything-gimp` launches an interactive session
+- **One-shot commands**: `cli-anything-gimp project create --name my-project`
+- **JSON output**: `cli-anything-gimp --json project list` for machine-readable output
+- **Undo/redo**: Stateful project management with full operation history
+
+## For AI agents
+
+cli-hub is designed to be agent-friendly. AI coding agents can:
+
+1. `pip install cli-anything-hub` to get the package manager
+2. `cli-hub search ` or `cli-hub list --json` to discover tools
+3. `cli-hub install ` to install what they need
+4. Use `--json` output for structured data parsing
+
+## Available categories
+
+3D, AI, Audio, Communication, Database, Design, DevOps, Diagrams, Game, GameDev, Generation, Graphics, Image, Music, Network, Office, OSINT, Project Management, Search, Streaming, Testing, Video, Web
+
+## JSON output
+
+All listing commands support `--json` for machine-readable output:
+
+```bash
+cli-hub list --json
+cli-hub search blender --json
+```
+
+## Analytics
+
+cli-hub sends anonymous install/uninstall events to help track adoption (via [Umami](https://umami.is)). No personal data is collected.
+
+Opt out:
+
+```bash
+export CLI_HUB_NO_ANALYTICS=1
+```
+
+## Links
+
+- **Web Hub**: [clianything.cc](https://clianything.cc)
+- **Repository**: [github.com/HKUDS/CLI-Anything](https://github.com/HKUDS/CLI-Anything)
+- **Live Catalog**: [clianything.cc/SKILL.txt](https://clianything.cc/SKILL.txt)
diff --git a/cli-hub/cli_hub/__init__.py b/cli-hub/cli_hub/__init__.py
new file mode 100644
index 000000000..7f360542e
--- /dev/null
+++ b/cli-hub/cli_hub/__init__.py
@@ -0,0 +1,3 @@
+"""cli-hub โ Download, manage, and browse CLI-Anything harnesses."""
+
+__version__ = "0.1.0"
diff --git a/cli-hub/cli_hub/analytics.py b/cli-hub/cli_hub/analytics.py
new file mode 100644
index 000000000..379f0bd1d
--- /dev/null
+++ b/cli-hub/cli_hub/analytics.py
@@ -0,0 +1,111 @@
+"""Lightweight, opt-out-able download event tracking via Umami."""
+
+import os
+import platform
+import threading
+
+import requests
+
+from cli_hub import __version__
+
+UMAMI_URL = "https://cloud.umami.is/api/send"
+WEBSITE_ID = "a076c661-bed1-405c-a522-813794e688b4"
+HOSTNAME = "clianything.cc"
+USER_AGENT = f"Mozilla/5.0 (compatible; cli-anything-hub/{__version__})"
+
+
+def _is_enabled():
+ return os.environ.get("CLI_HUB_NO_ANALYTICS", "").strip() not in ("1", "true", "yes")
+
+
+def _send_event(payload):
+ """Send a single event payload. Blocking โ callers should use threads."""
+ try:
+ return requests.post(
+ UMAMI_URL, json=payload, timeout=5,
+ headers={"User-Agent": USER_AGENT},
+ )
+ except Exception:
+ return None # analytics must never break the user's workflow
+
+
+def track_event(event_name, url="/cli-anything-hub", data=None):
+ """Fire-and-forget event to Umami. Non-blocking, never raises."""
+ if not _is_enabled():
+ return
+
+ payload = {
+ "type": "event",
+ "payload": {
+ "website": WEBSITE_ID,
+ "hostname": HOSTNAME,
+ "url": url,
+ "name": event_name,
+ "data": data or {},
+ },
+ }
+
+ threading.Thread(target=_send_event, args=(payload,), daemon=True).start()
+
+
+def track_install(cli_name, version):
+ """Track a CLI install event โ event name includes the CLI for dashboard visibility."""
+ track_event(f"cli-install:{cli_name}", url=f"/cli-anything-hub/install/{cli_name}", data={
+ "cli": cli_name,
+ "version": version,
+ "platform": platform.system().lower(),
+ })
+
+
+def track_uninstall(cli_name):
+ """Track a CLI uninstall event."""
+ track_event(f"cli-uninstall:{cli_name}", url=f"/cli-anything-hub/uninstall/{cli_name}", data={
+ "cli": cli_name,
+ })
+
+
+def track_visit(is_agent=False):
+ """Track a visit-human or visit-agent event, matching the hub website's convention."""
+ event_name = "visit-agent" if is_agent else "visit-human"
+ track_event(event_name, url="/cli-anything-hub", data={
+ "source": "cli-anything-hub",
+ "platform": platform.system().lower(),
+ })
+
+
+def track_first_run():
+ """Send a one-time 'cli-hub-installed' event on first invocation."""
+ from pathlib import Path
+ marker = Path.home() / ".cli-hub" / ".first_run_sent"
+ if marker.exists():
+ return
+ track_event("cli-anything-hub-installed", url="/cli-anything-hub/installed", data={
+ "version": __version__,
+ "platform": platform.system().lower(),
+ })
+ try:
+ marker.parent.mkdir(parents=True, exist_ok=True)
+ marker.write_text(__version__)
+ except Exception:
+ pass
+
+
+def _detect_is_agent():
+ """Detect if cli-hub is likely being invoked by an AI agent."""
+ indicators = [
+ "CLAUDE_CODE", # Claude Code
+ "CODEX", # OpenAI Codex
+ "CURSOR_SESSION", # Cursor
+ "CLINE_SESSION", # Cline
+ "COPILOT", # GitHub Copilot
+ "AIDER", # Aider
+ "CONTINUE_SESSION", # Continue.dev
+ ]
+ for var in indicators:
+ if os.environ.get(var):
+ return True
+ # Check if stdin is not a terminal (piped / scripted)
+ import sys
+ if not sys.stdin.isatty():
+ return True
+ return False
diff --git a/cli-hub/cli_hub/cli.py b/cli-hub/cli_hub/cli.py
new file mode 100644
index 000000000..65557cfa9
--- /dev/null
+++ b/cli-hub/cli_hub/cli.py
@@ -0,0 +1,168 @@
+"""cli-hub โ CLI entry point."""
+
+import click
+
+from cli_hub import __version__
+from cli_hub.registry import fetch_registry, get_cli, search_clis, list_categories
+from cli_hub.installer import install_cli, uninstall_cli, get_installed, update_cli
+from cli_hub.analytics import track_install, track_uninstall, track_visit, track_first_run, _detect_is_agent
+
+
+@click.group(invoke_without_command=True)
+@click.option("--version", is_flag=True, help="Show version.")
+@click.pass_context
+def main(ctx, version):
+ """cli-hub โ Download and manage CLI-Anything harnesses."""
+ track_first_run()
+ track_visit(is_agent=_detect_is_agent())
+ if version:
+ click.echo(f"cli-hub {__version__}")
+ return
+ if ctx.invoked_subcommand is None:
+ click.echo(ctx.get_help())
+
+
+@main.command()
+@click.argument("name")
+def install(name):
+ """Install a CLI harness by name."""
+ click.echo(f"Installing {name}...")
+ success, msg = install_cli(name)
+ if success:
+ cli = get_cli(name)
+ track_install(name, cli["version"] if cli else "unknown")
+ click.secho(f"โ {msg}", fg="green")
+ click.echo(f" Run it with: {cli['entry_point']}" if cli else "")
+ else:
+ click.secho(f"โ {msg}", fg="red", err=True)
+ raise SystemExit(1)
+
+
+@main.command()
+@click.argument("name")
+def uninstall(name):
+ """Uninstall a CLI harness by name."""
+ success, msg = uninstall_cli(name)
+ if success:
+ track_uninstall(name)
+ click.secho(f"โ {msg}", fg="green")
+ else:
+ click.secho(f"โ {msg}", fg="red", err=True)
+ raise SystemExit(1)
+
+
+@main.command()
+@click.argument("name")
+def update(name):
+ """Update a CLI harness to the latest version."""
+ click.echo(f"Updating {name}...")
+ success, msg = update_cli(name)
+ if success:
+ cli = get_cli(name)
+ track_install(name, cli["version"] if cli else "unknown")
+ click.secho(f"โ {msg}", fg="green")
+ else:
+ click.secho(f"โ {msg}", fg="red", err=True)
+ raise SystemExit(1)
+
+
+@main.command("list")
+@click.option("--category", "-c", default=None, help="Filter by category.")
+@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
+def list_clis(category, as_json):
+ """List all available CLI harnesses."""
+ try:
+ registry = fetch_registry()
+ except Exception as e:
+ click.secho(f"Failed to fetch registry: {e}", fg="red", err=True)
+ raise SystemExit(1)
+
+ clis = registry["clis"]
+ if category:
+ clis = [c for c in clis if c.get("category", "").lower() == category.lower()]
+
+ installed = get_installed()
+
+ if as_json:
+ import json
+ click.echo(json.dumps(clis, indent=2))
+ return
+
+ if not clis:
+ click.echo("No CLIs found." + (f" Category '{category}' may not exist." if category else ""))
+ return
+
+ # Group by category
+ by_cat = {}
+ for cli in clis:
+ cat = cli.get("category", "uncategorized")
+ by_cat.setdefault(cat, []).append(cli)
+
+ for cat in sorted(by_cat):
+ click.secho(f"\n {cat.upper()}", fg="blue", bold=True)
+ for cli in sorted(by_cat[cat], key=lambda c: c["name"]):
+ marker = click.style(" โ", fg="green") if cli["name"] in installed else " "
+ name = click.style(f"{cli['name']:20s}", bold=True)
+ desc = cli["description"][:60]
+ click.echo(f" {marker} {name} {desc}")
+
+ total = len(clis)
+ inst = sum(1 for c in clis if c["name"] in installed)
+ click.echo(f"\n {total} CLIs available, {inst} installed")
+ cats = list_categories(registry)
+ click.echo(f" Categories: {', '.join(cats)}")
+
+
+@main.command()
+@click.argument("query")
+@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
+def search(query, as_json):
+ """Search CLIs by name, description, or category."""
+ results = search_clis(query)
+
+ if as_json:
+ import json
+ click.echo(json.dumps(results, indent=2))
+ return
+
+ if not results:
+ click.echo(f"No CLIs matching '{query}'.")
+ return
+
+ installed = get_installed()
+ for cli in results:
+ marker = click.style("โ", fg="green") if cli["name"] in installed else " "
+ name = click.style(cli["name"], bold=True)
+ cat = click.style(f"[{cli.get('category', '')}]", fg="blue")
+ click.echo(f" {marker} {name} {cat} โ {cli['description'][:70]}")
+ click.echo(f" Install: cli-hub install {cli['name']}")
+
+
+@main.command()
+@click.argument("name")
+def info(name):
+ """Show details for a specific CLI."""
+ cli = get_cli(name)
+ if not cli:
+ click.secho(f"CLI '{name}' not found.", fg="red", err=True)
+ raise SystemExit(1)
+
+ installed = get_installed()
+ is_installed = cli["name"] in installed
+
+ click.secho(f"\n {cli['display_name']}", bold=True)
+ click.echo(f" {cli['description']}")
+ click.echo(f" Category: {cli.get('category', 'N/A')}")
+ click.echo(f" Version: {cli['version']}")
+ click.echo(f" Requires: {cli.get('requires') or 'nothing'}")
+ click.echo(f" Entry point: {cli['entry_point']}")
+ click.echo(f" Homepage: {cli.get('homepage', 'N/A')}")
+ click.echo(f" Contributor: {cli.get('contributor', 'N/A')}")
+ status = click.style("installed", fg="green") if is_installed else "not installed"
+ click.echo(f" Status: {status}")
+ click.echo(f"\n Install: cli-hub install {cli['name']}")
+ click.echo()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/cli-hub/cli_hub/installer.py b/cli-hub/cli_hub/installer.py
new file mode 100644
index 000000000..a8f761fcd
--- /dev/null
+++ b/cli-hub/cli_hub/installer.py
@@ -0,0 +1,107 @@
+"""Install, uninstall, and manage CLI-Anything harnesses via pip."""
+
+import json
+import subprocess
+import sys
+from pathlib import Path
+
+from cli_hub.registry import get_cli, fetch_registry
+
+INSTALLED_FILE = Path.home() / ".cli-hub" / "installed.json"
+
+
+def _load_installed():
+ if INSTALLED_FILE.exists():
+ try:
+ return json.loads(INSTALLED_FILE.read_text())
+ except json.JSONDecodeError:
+ pass
+ return {}
+
+
+def _save_installed(data):
+ INSTALLED_FILE.parent.mkdir(parents=True, exist_ok=True)
+ INSTALLED_FILE.write_text(json.dumps(data, indent=2))
+
+
+def install_cli(name):
+ """Install a CLI harness by name. Returns (success, message)."""
+ cli = get_cli(name)
+ if cli is None:
+ return False, f"CLI '{name}' not found in registry. Use 'cli-hub list' to see available CLIs."
+
+ install_cmd = cli["install_cmd"]
+ result = subprocess.run(
+ [sys.executable, "-m", "pip", "install"] + install_cmd.replace("pip install ", "").split(),
+ capture_output=True, text=True
+ )
+
+ if result.returncode == 0:
+ installed = _load_installed()
+ installed[cli["name"]] = {
+ "version": cli["version"],
+ "entry_point": cli["entry_point"],
+ "install_cmd": install_cmd,
+ }
+ _save_installed(installed)
+ return True, f"Installed {cli['display_name']} ({cli['entry_point']})"
+ else:
+ return False, f"pip install failed:\n{result.stderr}"
+
+
+def uninstall_cli(name):
+ """Uninstall a CLI harness by name. Returns (success, message)."""
+ cli = get_cli(name)
+ if cli is None:
+ return False, f"CLI '{name}' not found in registry."
+
+ # The pip package name follows the pattern: cli-anything- with underscores
+ # but we derive it from the install_cmd's subdirectory
+ # The namespace package is cli_anything., entry point is cli-anything-
+ # pip package name in subdirectory installs is the name from setup.py
+ # We'll uninstall by the entry_point pattern
+ pkg_name = f"cli-anything-{cli['name']}"
+
+ result = subprocess.run(
+ [sys.executable, "-m", "pip", "uninstall", "-y", pkg_name],
+ capture_output=True, text=True
+ )
+
+ if result.returncode == 0:
+ installed = _load_installed()
+ installed.pop(cli["name"], None)
+ _save_installed(installed)
+ return True, f"Uninstalled {cli['display_name']}"
+ else:
+ return False, f"pip uninstall failed:\n{result.stderr}"
+
+
+def get_installed():
+ """Return dict of installed CLIs."""
+ return _load_installed()
+
+
+def update_cli(name):
+ """Update a CLI by reinstalling from the latest source."""
+ cli = get_cli(name, fetch_registry(force_refresh=True))
+ if cli is None:
+ return False, f"CLI '{name}' not found in registry."
+
+ install_cmd = cli["install_cmd"]
+ result = subprocess.run(
+ [sys.executable, "-m", "pip", "install", "--upgrade", "--force-reinstall"]
+ + install_cmd.replace("pip install ", "").split(),
+ capture_output=True, text=True
+ )
+
+ if result.returncode == 0:
+ installed = _load_installed()
+ installed[cli["name"]] = {
+ "version": cli["version"],
+ "entry_point": cli["entry_point"],
+ "install_cmd": install_cmd,
+ }
+ _save_installed(installed)
+ return True, f"Updated {cli['display_name']} to {cli['version']}"
+ else:
+ return False, f"Update failed:\n{result.stderr}"
diff --git a/cli-hub/cli_hub/registry.py b/cli-hub/cli_hub/registry.py
new file mode 100644
index 000000000..8b6a3b99c
--- /dev/null
+++ b/cli-hub/cli_hub/registry.py
@@ -0,0 +1,72 @@
+"""Fetch and cache the CLI-Anything registry."""
+
+import json
+import os
+import time
+from pathlib import Path
+
+import requests
+
+REGISTRY_URL = "https://hkuds.github.io/CLI-Anything/registry.json"
+CACHE_DIR = Path.home() / ".cli-hub"
+CACHE_FILE = CACHE_DIR / "registry_cache.json"
+CACHE_TTL = 3600 # 1 hour
+
+
+def _ensure_cache_dir():
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
+
+
+def fetch_registry(force_refresh=False):
+ """Fetch registry.json, using a local cache with TTL."""
+ _ensure_cache_dir()
+
+ if not force_refresh and CACHE_FILE.exists():
+ try:
+ cached = json.loads(CACHE_FILE.read_text())
+ if time.time() - cached.get("_cached_at", 0) < CACHE_TTL:
+ return cached["data"]
+ except (json.JSONDecodeError, KeyError):
+ pass
+
+ resp = requests.get(REGISTRY_URL, timeout=15)
+ resp.raise_for_status()
+ data = resp.json()
+
+ cache_payload = {"_cached_at": time.time(), "data": data}
+ CACHE_FILE.write_text(json.dumps(cache_payload, indent=2))
+
+ return data
+
+
+def get_cli(name, registry=None):
+ """Look up a CLI entry by name (case-insensitive)."""
+ if registry is None:
+ registry = fetch_registry()
+ name_lower = name.lower()
+ for cli in registry["clis"]:
+ if cli["name"].lower() == name_lower:
+ return cli
+ return None
+
+
+def search_clis(query, registry=None):
+ """Search CLIs by name, description, or category."""
+ if registry is None:
+ registry = fetch_registry()
+ query_lower = query.lower()
+ results = []
+ for cli in registry["clis"]:
+ if (query_lower in cli["name"].lower()
+ or query_lower in cli["description"].lower()
+ or query_lower in cli.get("category", "").lower()
+ or query_lower in cli.get("display_name", "").lower()):
+ results.append(cli)
+ return results
+
+
+def list_categories(registry=None):
+ """Return sorted list of unique categories."""
+ if registry is None:
+ registry = fetch_registry()
+ return sorted(set(cli.get("category", "uncategorized") for cli in registry["clis"]))
diff --git a/cli-hub/setup.py b/cli-hub/setup.py
new file mode 100644
index 000000000..1c2f89532
--- /dev/null
+++ b/cli-hub/setup.py
@@ -0,0 +1,49 @@
+"""cli-hub โ package manager for CLI-Anything harnesses."""
+
+from setuptools import setup, find_packages
+
+setup(
+ name="cli-anything-hub",
+ version="0.1.0",
+ description="Package manager for CLI-Anything โ browse, install, and manage 40+ agent-native CLI interfaces for GUI applications",
+ long_description=open("README.md").read(),
+ long_description_content_type="text/markdown",
+ author="HKUDS",
+ author_email="hkuds@connect.hku.hk",
+ url="https://github.com/HKUDS/CLI-Anything",
+ project_urls={
+ "Homepage": "https://clianything.cc",
+ "Repository": "https://github.com/HKUDS/CLI-Anything",
+ "Bug Tracker": "https://github.com/HKUDS/CLI-Anything/issues",
+ "Catalog": "https://clianything.cc/SKILL.txt",
+ },
+ license="MIT",
+ packages=find_packages(exclude=["tests", "tests.*"]),
+ python_requires=">=3.10",
+ install_requires=[
+ "click>=8.0",
+ "requests>=2.28",
+ ],
+ entry_points={
+ "console_scripts": [
+ "cli-hub=cli_hub.cli:main",
+ ],
+ },
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "Intended Audience :: System Administrators",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
+ "Topic :: System :: Installation/Setup",
+ "Topic :: Utilities",
+ ],
+ keywords="cli, agent, gui, automation, package-manager, cli-anything",
+)
diff --git a/cli-hub/tests/__init__.py b/cli-hub/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/cli-hub/tests/test_cli_hub.py b/cli-hub/tests/test_cli_hub.py
new file mode 100644
index 000000000..37f85deaf
--- /dev/null
+++ b/cli-hub/tests/test_cli_hub.py
@@ -0,0 +1,420 @@
+"""Tests for cli-hub โ registry, installer, analytics, and CLI."""
+
+import json
+import os
+import tempfile
+from pathlib import Path
+from unittest.mock import patch, MagicMock
+
+import pytest
+import click.testing
+
+from cli_hub import __version__
+from cli_hub.registry import fetch_registry, get_cli, search_clis, list_categories
+from cli_hub.installer import install_cli, uninstall_cli, get_installed, _load_installed, _save_installed
+from cli_hub.analytics import _is_enabled, track_event, track_install, track_uninstall as analytics_track_uninstall, track_visit, track_first_run, _detect_is_agent
+from cli_hub.cli import main
+
+
+# โโโ Sample registry data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+SAMPLE_REGISTRY = {
+ "meta": {"repo": "https://github.com/HKUDS/CLI-Anything", "description": "test"},
+ "clis": [
+ {
+ "name": "gimp",
+ "display_name": "GIMP",
+ "version": "1.0.0",
+ "description": "Image editing via GIMP",
+ "requires": "gimp",
+ "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": None,
+ "category": "image",
+ "contributor": "test-user",
+ "contributor_url": "https://github.com/test-user",
+ },
+ {
+ "name": "blender",
+ "display_name": "Blender",
+ "version": "1.0.0",
+ "description": "3D modeling via Blender",
+ "requires": "blender",
+ "homepage": "https://blender.org",
+ "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness",
+ "entry_point": "cli-anything-blender",
+ "skill_md": None,
+ "category": "3d",
+ "contributor": "test-user",
+ "contributor_url": "https://github.com/test-user",
+ },
+ {
+ "name": "audacity",
+ "display_name": "Audacity",
+ "version": "1.0.0",
+ "description": "Audio editing and processing via sox",
+ "requires": "sox",
+ "homepage": "https://audacityteam.org",
+ "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness",
+ "entry_point": "cli-anything-audacity",
+ "skill_md": None,
+ "category": "audio",
+ "contributor": "test-user",
+ "contributor_url": "https://github.com/test-user",
+ },
+ ],
+}
+
+
+# โโโ Registry tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+class TestRegistry:
+ """Tests for registry.py โ fetch, cache, search, and lookup."""
+
+ @patch("cli_hub.registry.requests.get")
+ @patch("cli_hub.registry.CACHE_FILE", Path(tempfile.mktemp()))
+ def test_fetch_registry_from_remote(self, mock_get):
+ mock_resp = MagicMock()
+ mock_resp.json.return_value = SAMPLE_REGISTRY
+ mock_resp.raise_for_status = MagicMock()
+ mock_get.return_value = mock_resp
+
+ result = fetch_registry(force_refresh=True)
+ assert result["clis"][0]["name"] == "gimp"
+ mock_get.assert_called_once()
+
+ def test_get_cli_found(self):
+ cli = get_cli("gimp", SAMPLE_REGISTRY)
+ assert cli is not None
+ assert cli["display_name"] == "GIMP"
+
+ def test_get_cli_case_insensitive(self):
+ cli = get_cli("GIMP", SAMPLE_REGISTRY)
+ assert cli is not None
+ assert cli["name"] == "gimp"
+
+ def test_get_cli_not_found(self):
+ cli = get_cli("nonexistent", SAMPLE_REGISTRY)
+ assert cli is None
+
+ def test_search_by_name(self):
+ results = search_clis("gimp", SAMPLE_REGISTRY)
+ assert len(results) == 1
+ assert results[0]["name"] == "gimp"
+
+ def test_search_by_category(self):
+ results = search_clis("3d", SAMPLE_REGISTRY)
+ assert len(results) == 1
+ assert results[0]["name"] == "blender"
+
+ def test_search_by_description(self):
+ results = search_clis("audio", SAMPLE_REGISTRY)
+ assert len(results) == 1
+ assert results[0]["name"] == "audacity"
+
+ def test_search_no_results(self):
+ results = search_clis("nonexistent_xyz", SAMPLE_REGISTRY)
+ assert len(results) == 0
+
+ def test_list_categories(self):
+ cats = list_categories(SAMPLE_REGISTRY)
+ assert cats == ["3d", "audio", "image"]
+
+
+# โโโ Installer tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+class TestInstaller:
+ """Tests for installer.py โ install, uninstall, tracking."""
+
+ def test_load_installed_empty(self, tmp_path):
+ with patch("cli_hub.installer.INSTALLED_FILE", tmp_path / "installed.json"):
+ assert _load_installed() == {}
+
+ def test_save_and_load_installed(self, tmp_path):
+ installed_file = tmp_path / "installed.json"
+ with patch("cli_hub.installer.INSTALLED_FILE", installed_file):
+ _save_installed({"gimp": {"version": "1.0.0"}})
+ data = _load_installed()
+ assert data["gimp"]["version"] == "1.0.0"
+
+ @patch("cli_hub.installer.subprocess.run")
+ @patch("cli_hub.installer.get_cli")
+ @patch("cli_hub.installer.INSTALLED_FILE", Path(tempfile.mktemp()))
+ def test_install_success(self, mock_get_cli, mock_run):
+ mock_get_cli.return_value = SAMPLE_REGISTRY["clis"][0]
+ mock_run.return_value = MagicMock(returncode=0)
+
+ success, msg = install_cli("gimp")
+ assert success
+ assert "GIMP" in msg
+
+ @patch("cli_hub.installer.get_cli")
+ def test_install_not_found(self, mock_get_cli):
+ mock_get_cli.return_value = None
+ success, msg = install_cli("nonexistent")
+ assert not success
+ assert "not found" in msg
+
+ @patch("cli_hub.installer.subprocess.run")
+ @patch("cli_hub.installer.get_cli")
+ @patch("cli_hub.installer.INSTALLED_FILE", Path(tempfile.mktemp()))
+ def test_install_pip_failure(self, mock_get_cli, mock_run):
+ mock_get_cli.return_value = SAMPLE_REGISTRY["clis"][0]
+ mock_run.return_value = MagicMock(returncode=1, stderr="some error")
+
+ success, msg = install_cli("gimp")
+ assert not success
+ assert "failed" in msg
+
+ @patch("cli_hub.installer.subprocess.run")
+ @patch("cli_hub.installer.get_cli")
+ @patch("cli_hub.installer.INSTALLED_FILE", Path(tempfile.mktemp()))
+ def test_uninstall_success(self, mock_get_cli, mock_run):
+ mock_get_cli.return_value = SAMPLE_REGISTRY["clis"][0]
+ mock_run.return_value = MagicMock(returncode=0)
+
+ success, msg = uninstall_cli("gimp")
+ assert success
+ assert "GIMP" in msg
+
+
+# โโโ Analytics tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+class TestAnalytics:
+ """Tests for analytics.py โ opt-out, event firing, event names."""
+
+ def test_analytics_enabled_by_default(self):
+ with patch.dict(os.environ, {}, clear=True):
+ assert _is_enabled()
+
+ def test_analytics_disabled_by_env(self):
+ with patch.dict(os.environ, {"CLI_HUB_NO_ANALYTICS": "1"}):
+ assert not _is_enabled()
+
+ def test_analytics_disabled_by_true(self):
+ with patch.dict(os.environ, {"CLI_HUB_NO_ANALYTICS": "true"}):
+ assert not _is_enabled()
+
+ @patch("cli_hub.analytics._send_event")
+ def test_track_event_sends_request(self, mock_send):
+ with patch.dict(os.environ, {}, clear=True):
+ track_event("test-event", data={"key": "value"})
+ import time
+ time.sleep(0.2)
+ mock_send.assert_called_once()
+ payload = mock_send.call_args[0][0]
+ assert payload["payload"]["name"] == "test-event"
+ assert payload["payload"]["hostname"] == "clianything.cc"
+
+ @patch("cli_hub.analytics._send_event")
+ def test_track_event_noop_when_disabled(self, mock_send):
+ with patch.dict(os.environ, {"CLI_HUB_NO_ANALYTICS": "1"}):
+ track_event("test-event")
+ import time
+ time.sleep(0.2)
+ mock_send.assert_not_called()
+
+ @patch("cli_hub.analytics._send_event")
+ def test_track_install_event_name_includes_cli(self, mock_send):
+ """cli-install event name must include CLI name for dashboard visibility."""
+ with patch.dict(os.environ, {}, clear=True):
+ track_install("gimp", "1.0.0")
+ import time
+ time.sleep(0.2)
+ mock_send.assert_called_once()
+ payload = mock_send.call_args[0][0]
+ assert payload["payload"]["name"] == "cli-install:gimp"
+ assert payload["payload"]["url"] == "/cli-anything-hub/install/gimp"
+ assert payload["payload"]["data"]["cli"] == "gimp"
+ assert payload["payload"]["data"]["version"] == "1.0.0"
+ assert "platform" in payload["payload"]["data"]
+
+ @patch("cli_hub.analytics._send_event")
+ def test_track_uninstall_event_name_includes_cli(self, mock_send):
+ """cli-uninstall event name must include CLI name for dashboard visibility."""
+ with patch.dict(os.environ, {}, clear=True):
+ analytics_track_uninstall("blender")
+ import time
+ time.sleep(0.2)
+ mock_send.assert_called_once()
+ payload = mock_send.call_args[0][0]
+ assert payload["payload"]["name"] == "cli-uninstall:blender"
+ assert payload["payload"]["url"] == "/cli-anything-hub/uninstall/blender"
+ assert payload["payload"]["data"]["cli"] == "blender"
+
+ @patch("cli_hub.analytics._send_event")
+ def test_track_visit_human(self, mock_send):
+ """visit-human event sent when not detected as agent."""
+ with patch.dict(os.environ, {}, clear=True):
+ track_visit(is_agent=False)
+ import time
+ time.sleep(0.2)
+ mock_send.assert_called_once()
+ payload = mock_send.call_args[0][0]
+ assert payload["payload"]["name"] == "visit-human"
+ assert payload["payload"]["url"] == "/cli-anything-hub"
+ assert payload["payload"]["data"]["source"] == "cli-anything-hub"
+
+ @patch("cli_hub.analytics._send_event")
+ def test_track_visit_agent(self, mock_send):
+ """visit-agent event sent when agent environment detected."""
+ with patch.dict(os.environ, {}, clear=True):
+ track_visit(is_agent=True)
+ import time
+ time.sleep(0.2)
+ mock_send.assert_called_once()
+ payload = mock_send.call_args[0][0]
+ assert payload["payload"]["name"] == "visit-agent"
+
+ def test_detect_agent_claude_code(self):
+ with patch.dict(os.environ, {"CLAUDE_CODE": "1"}):
+ assert _detect_is_agent() is True
+
+ def test_detect_agent_codex(self):
+ with patch.dict(os.environ, {"CODEX": "1"}):
+ assert _detect_is_agent() is True
+
+ def test_detect_not_agent_clean_env(self):
+ """Clean env with a tty should not detect as agent."""
+ with patch.dict(os.environ, {}, clear=True):
+ with patch("sys.stdin") as mock_stdin:
+ mock_stdin.isatty.return_value = True
+ assert _detect_is_agent() is False
+
+ @patch("cli_hub.analytics._send_event")
+ def test_first_run_sends_event(self, mock_send, tmp_path):
+ """First invocation sends cli-hub-installed event."""
+ with patch.dict(os.environ, {"HOME": str(tmp_path)}, clear=False):
+ track_first_run()
+ import time
+ time.sleep(0.2)
+ mock_send.assert_called_once()
+ payload = mock_send.call_args[0][0]
+ assert payload["payload"]["name"] == "cli-anything-hub-installed"
+ assert payload["payload"]["url"] == "/cli-anything-hub/installed"
+ # Marker file should now exist
+ assert (tmp_path / ".cli-hub" / ".first_run_sent").exists()
+
+ @patch("cli_hub.analytics._send_event")
+ def test_first_run_skips_if_marker_exists(self, mock_send, tmp_path):
+ """Second invocation does NOT send cli-hub-installed event."""
+ cli_hub_dir = tmp_path / ".cli-hub"
+ cli_hub_dir.mkdir()
+ (cli_hub_dir / ".first_run_sent").write_text("0.1.0")
+ with patch.dict(os.environ, {"HOME": str(tmp_path)}, clear=False):
+ track_first_run()
+ import time
+ time.sleep(0.2)
+ mock_send.assert_not_called()
+
+
+# โโโ CLI tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+class TestCLI:
+ """Tests for the Click CLI interface."""
+
+ def setup_method(self):
+ self.runner = click.testing.CliRunner()
+
+ @patch("cli_hub.cli.track_first_run")
+ @patch("cli_hub.cli.track_visit")
+ @patch("cli_hub.cli._detect_is_agent", return_value=False)
+ def test_version(self, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["--version"])
+ assert __version__ in result.output
+ assert result.exit_code == 0
+ mock_visit.assert_called_once_with(is_agent=False)
+ mock_first_run.assert_called_once()
+
+ @patch("cli_hub.cli.track_first_run")
+ @patch("cli_hub.cli.track_visit")
+ @patch("cli_hub.cli._detect_is_agent", return_value=False)
+ def test_help(self, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["--help"])
+ 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_is_agent", return_value=False)
+ @patch("cli_hub.cli.fetch_registry", return_value=SAMPLE_REGISTRY)
+ @patch("cli_hub.cli.get_installed", return_value={})
+ def test_list_command(self, mock_installed, mock_fetch, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["list"])
+ assert "gimp" in result.output
+ assert "blender" 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_is_agent", return_value=False)
+ @patch("cli_hub.cli.fetch_registry", return_value=SAMPLE_REGISTRY)
+ @patch("cli_hub.cli.get_installed", return_value={})
+ def test_list_with_category(self, mock_installed, mock_fetch, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["list", "-c", "image"])
+ assert "gimp" in result.output
+ assert "blender" not in result.output
+
+ @patch("cli_hub.cli.track_first_run")
+ @patch("cli_hub.cli.track_visit")
+ @patch("cli_hub.cli._detect_is_agent", return_value=False)
+ @patch("cli_hub.cli.search_clis", return_value=[SAMPLE_REGISTRY["clis"][0]])
+ @patch("cli_hub.cli.get_installed", return_value={})
+ def test_search_command(self, mock_installed, mock_search, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["search", "gimp"])
+ assert "gimp" 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_is_agent", return_value=False)
+ @patch("cli_hub.cli.get_cli", return_value=SAMPLE_REGISTRY["clis"][0])
+ @patch("cli_hub.cli.get_installed", return_value={})
+ def test_info_command(self, mock_installed, mock_get, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["info", "gimp"])
+ assert "GIMP" in result.output
+ assert "image" 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_is_agent", return_value=False)
+ @patch("cli_hub.cli.get_cli", return_value=None)
+ def test_info_not_found(self, mock_get, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["info", "nonexistent"])
+ assert result.exit_code == 1
+
+ @patch("cli_hub.cli.track_first_run")
+ @patch("cli_hub.cli.track_visit")
+ @patch("cli_hub.cli._detect_is_agent", return_value=False)
+ @patch("cli_hub.cli.track_install")
+ @patch("cli_hub.cli.install_cli", return_value=(True, "Installed GIMP (cli-anything-gimp)"))
+ @patch("cli_hub.cli.get_cli", return_value=SAMPLE_REGISTRY["clis"][0])
+ def test_install_command(self, mock_get, mock_install, mock_track, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["install", "gimp"])
+ assert result.exit_code == 0
+ assert "Installed" in result.output
+ mock_track.assert_called_once()
+
+ @patch("cli_hub.cli.track_first_run")
+ @patch("cli_hub.cli.track_visit")
+ @patch("cli_hub.cli._detect_is_agent", return_value=False)
+ @patch("cli_hub.cli.track_uninstall")
+ @patch("cli_hub.cli.uninstall_cli", return_value=(True, "Uninstalled GIMP"))
+ def test_uninstall_command(self, mock_uninstall, mock_track, mock_detect, mock_visit, mock_first_run):
+ result = self.runner.invoke(main, ["uninstall", "gimp"])
+ assert result.exit_code == 0
+ mock_track.assert_called_once()
+
+ @patch("cli_hub.cli.track_first_run")
+ @patch("cli_hub.cli.track_visit")
+ @patch("cli_hub.cli._detect_is_agent", return_value=True)
+ def test_visit_agent_on_invocation(self, mock_detect, mock_visit, mock_first_run):
+ """When agent env detected, track_visit is called with is_agent=True."""
+ result = self.runner.invoke(main, ["--version"])
+ mock_visit.assert_called_once_with(is_agent=True)
diff --git a/docs/hub/SKILL.md b/docs/hub/SKILL.md
deleted file mode 100644
index 95e8caa70..000000000
--- a/docs/hub/SKILL.md
+++ /dev/null
@@ -1,141 +0,0 @@
----
-name: cli-anything-hub
-description: >-
- Browse and install 21+ agent-native CLI tools for GUI software.
- Covers image editing, 3D, video, audio, office, diagrams, AI, and more.
----
-
-# CLI-Anything Hub
-
-Agent-native stateful CLI interfaces for 21 applications. All CLIs support `--json` output, REPL mode, and undo/redo.
-
-## Quick Install
-
-```bash
-# Install any CLI
-pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=/agent-harness
-
-# Example: Install GIMP CLI
-pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness
-```
-
-## Available CLIs
-
-### 3D
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Blender** | 3D modeling, animation, and rendering via blender --background --python | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness` |
-
-### Ai
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **ComfyUI** | AI image generation workflow management via ComfyUI REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=comfyui/agent-harness` |
-| **NotebookLM** | Experimental NotebookLM harness scaffold wrapping the installed notebooklm CLI for notebook, source, chat, artifact, download, and sharing workflows | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=notebooklm/agent-harness` |
-| **Novita** | Access AI models via Novita's OpenAI-compatible API (DeepSeek, GLM, MiniMax) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=novita/agent-harness` |
-| **Ollama** | Local LLM inference and model management via Ollama REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=ollama/agent-harness` |
-
-### Audio
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Audacity** | Audio editing and processing via sox | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness` |
-
-### Communication
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Zoom** | Meeting management via Zoom REST API (OAuth2) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=zoom/agent-harness` |
-
-### Design
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Sketch** | Generate Sketch design files (.sketch) from JSON design specifications via sketch-constructor | `cd sketch/agent-harness && npm install && npm link` |
-
-### Diagrams
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Draw.io** | Diagram creation and export via draw.io CLI | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=drawio/agent-harness` |
-| **Mermaid** | Mermaid Live Editor state files and renderer URLs | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mermaid/agent-harness` |
-
-### Generation
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **AnyGen** | Generate docs, slides, websites and more via AnyGen cloud API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=anygen/agent-harness` |
-
-### Image
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **GIMP** | Raster image processing via gimp -i -b (batch mode) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness` |
-| **Inkscape** | SVG vector graphics with export via inkscape --export-filename | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=inkscape/agent-harness` |
-
-### Music
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **MuseScore** | CLI for music notation โ transpose, export PDF/audio/MIDI, extract parts, manage instruments | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=musescore/agent-harness` |
-
-### Network
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **AdGuardHome** | DNS ad-blocking and network infrastructure management via AdGuardHome REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=adguardhome/agent-harness` |
-
-### Office
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **LibreOffice** | Create and manipulate ODF documents, export to PDF/DOCX/XLSX/PPTX via headless mode | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=libreoffice/agent-harness` |
-| **Mubu** | Knowledge management and outlining via local Mubu desktop data | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mubu/agent-harness` |
-
-### Streaming
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **OBS Studio** | Create and manage streaming/recording scenes via command line | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=obs-studio/agent-harness` |
-
-### Video
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Kdenlive** | Video editing and rendering via melt | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=kdenlive/agent-harness` |
-| **Shotcut** | Video editing and rendering via melt/ffmpeg | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=shotcut/agent-harness` |
-
-### Web
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Browser** | Browser automation via DOMShell MCP server. Maps Chrome's Accessibility Tree to a virtual filesystem for agent-native navigation. | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=browser/agent-harness` |
-
-## Usage Pattern
-
-All CLIs follow the same pattern:
-
-```bash
-# Interactive REPL
-cli-anything-
-
-# One-shot command
-cli-anything- [options]
-
-# JSON output for agents
-cli-anything- --json
-```
-
-## For AI Agents
-
-1. Install the CLI you need from the table above
-2. Read its full SKILL.md at the repo path shown in registry.json
-3. Always use `--json` flag for machine-readable output
-4. Check exit codes (0=success, non-zero=error)
-
-## More Info
-
-- Repository: https://github.com/HKUDS/CLI-Anything
-- Web Hub: https://hkuds.github.io/CLI-Anything/
-- Last Updated: 2026-03-18
diff --git a/docs/hub/SKILL.txt b/docs/hub/SKILL.txt
deleted file mode 100644
index 95e8caa70..000000000
--- a/docs/hub/SKILL.txt
+++ /dev/null
@@ -1,141 +0,0 @@
----
-name: cli-anything-hub
-description: >-
- Browse and install 21+ agent-native CLI tools for GUI software.
- Covers image editing, 3D, video, audio, office, diagrams, AI, and more.
----
-
-# CLI-Anything Hub
-
-Agent-native stateful CLI interfaces for 21 applications. All CLIs support `--json` output, REPL mode, and undo/redo.
-
-## Quick Install
-
-```bash
-# Install any CLI
-pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=/agent-harness
-
-# Example: Install GIMP CLI
-pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness
-```
-
-## Available CLIs
-
-### 3D
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Blender** | 3D modeling, animation, and rendering via blender --background --python | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness` |
-
-### Ai
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **ComfyUI** | AI image generation workflow management via ComfyUI REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=comfyui/agent-harness` |
-| **NotebookLM** | Experimental NotebookLM harness scaffold wrapping the installed notebooklm CLI for notebook, source, chat, artifact, download, and sharing workflows | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=notebooklm/agent-harness` |
-| **Novita** | Access AI models via Novita's OpenAI-compatible API (DeepSeek, GLM, MiniMax) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=novita/agent-harness` |
-| **Ollama** | Local LLM inference and model management via Ollama REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=ollama/agent-harness` |
-
-### Audio
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Audacity** | Audio editing and processing via sox | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness` |
-
-### Communication
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Zoom** | Meeting management via Zoom REST API (OAuth2) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=zoom/agent-harness` |
-
-### Design
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Sketch** | Generate Sketch design files (.sketch) from JSON design specifications via sketch-constructor | `cd sketch/agent-harness && npm install && npm link` |
-
-### Diagrams
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Draw.io** | Diagram creation and export via draw.io CLI | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=drawio/agent-harness` |
-| **Mermaid** | Mermaid Live Editor state files and renderer URLs | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mermaid/agent-harness` |
-
-### Generation
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **AnyGen** | Generate docs, slides, websites and more via AnyGen cloud API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=anygen/agent-harness` |
-
-### Image
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **GIMP** | Raster image processing via gimp -i -b (batch mode) | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness` |
-| **Inkscape** | SVG vector graphics with export via inkscape --export-filename | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=inkscape/agent-harness` |
-
-### Music
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **MuseScore** | CLI for music notation โ transpose, export PDF/audio/MIDI, extract parts, manage instruments | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=musescore/agent-harness` |
-
-### Network
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **AdGuardHome** | DNS ad-blocking and network infrastructure management via AdGuardHome REST API | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=adguardhome/agent-harness` |
-
-### Office
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **LibreOffice** | Create and manipulate ODF documents, export to PDF/DOCX/XLSX/PPTX via headless mode | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=libreoffice/agent-harness` |
-| **Mubu** | Knowledge management and outlining via local Mubu desktop data | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mubu/agent-harness` |
-
-### Streaming
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **OBS Studio** | Create and manage streaming/recording scenes via command line | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=obs-studio/agent-harness` |
-
-### Video
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Kdenlive** | Video editing and rendering via melt | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=kdenlive/agent-harness` |
-| **Shotcut** | Video editing and rendering via melt/ffmpeg | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=shotcut/agent-harness` |
-
-### Web
-
-| Name | Description | Install |
-|------|-------------|---------|
-| **Browser** | Browser automation via DOMShell MCP server. Maps Chrome's Accessibility Tree to a virtual filesystem for agent-native navigation. | `pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=browser/agent-harness` |
-
-## Usage Pattern
-
-All CLIs follow the same pattern:
-
-```bash
-# Interactive REPL
-cli-anything-
-
-# One-shot command
-cli-anything- [options]
-
-# JSON output for agents
-cli-anything- --json
-```
-
-## For AI Agents
-
-1. Install the CLI you need from the table above
-2. Read its full SKILL.md at the repo path shown in registry.json
-3. Always use `--json` flag for machine-readable output
-4. Check exit codes (0=success, non-zero=error)
-
-## More Info
-
-- Repository: https://github.com/HKUDS/CLI-Anything
-- Web Hub: https://hkuds.github.io/CLI-Anything/
-- Last Updated: 2026-03-18
diff --git a/docs/hub/index.html b/docs/hub/index.html
index 1499b77e2..5bc4fa80e 100644
--- a/docs/hub/index.html
+++ b/docs/hub/index.html
@@ -161,9 +161,18 @@
.hero-tagline strong { color: var(--text); font-weight: 600; }
/* โโ Empower card โโ */
- .empower-card {
- max-width: 560px;
+ .empower-row {
+ display: flex;
+ gap: 1rem;
+ max-width: 1100px;
margin: 0 auto 1.5rem;
+ align-items: stretch;
+ }
+
+ .empower-card {
+ flex: 1;
+ min-width: 0;
+ margin: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
@@ -227,6 +236,35 @@
.empower-prompt strong { color: var(--text-secondary); font-weight: 600; }
.empower-prompt em { color: var(--accent); font-style: italic; }
+ .empower-card.empower-self::before { background: var(--green); }
+ .empower-card.empower-self h3 span { color: var(--green); }
+ .empower-self .cmd-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.4rem;
+ }
+ .empower-self .cmd-item {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 0.4rem 0.65rem;
+ font-size: 0.76rem;
+ }
+ .empower-self .cmd-item:hover { border-color: var(--text-tertiary); }
+ .empower-self .cmd-item .cmd-name {
+ color: var(--green);
+ font-family: 'JetBrains Mono', monospace;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+ .empower-self .cmd-item .cmd-desc {
+ color: var(--text-tertiary);
+ font-size: 0.72rem;
+ }
+
.hero-actions {
display: flex;
justify-content: center;
@@ -547,10 +585,11 @@
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: var(--text-secondary);
- white-space: nowrap;
+ white-space: pre-line;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
+ line-height: 1.6;
}
.card-copy-btn {
@@ -732,7 +771,9 @@
.filter-row { padding: 0.75rem 1.25rem 0; }
.nav { padding: 0.75rem 1.25rem; }
.footer { padding: 1.5rem 1.25rem; flex-direction: column; align-items: flex-start; }
- .empower-card { margin: 0 auto 1.25rem; padding: 1.25rem; }
+ .empower-row { flex-direction: column; }
+ .empower-card { margin: 0 0 1rem; padding: 1.25rem; }
+ .empower-self .cmd-grid { grid-template-columns: 1fr; }
}
@media (max-width: 400px) {
@@ -776,6 +817,7 @@
CLI-Anything Hub
Any software. Any codebase. Any Web API. Generate an agent-native CLI and let AI agents operate it — install with a single pip command.
+
Empower your agents — install in one command
@@ -793,6 +835,23 @@
+
+
Empower yourself — your CLI toolkit
+
+ pip install cli-anything-hub
+
+
+
+
cli-hub listBrowse all CLIs
+
cli-hub searchFind by keyword
+
cli-hub installInstall a CLI
+
cli-hub infoCLI details
+
cli-hub updateUpdate a CLI
+
cli-hub uninstallRemove a CLI
+
+
+
+