mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-30 17:34:27 +08:00
launch cli-anything-hub pkg manager.
This commit is contained in:
@@ -32,11 +32,19 @@ def main():
|
||||
"## Quick Install",
|
||||
"",
|
||||
"```bash",
|
||||
"# Install any CLI",
|
||||
f"pip install git+{data['meta']['repo']}.git#subdirectory=<name>/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 <name>` (installs `cli-anything-<name>` pip package)",
|
||||
"3. Run: `cli-anything-<name>` for REPL, or `cli-anything-<name> <command>` 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']}",
|
||||
])
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
CLI-Anything: Bridging the Gap Between AI Agents and the World's Software</strong><br>
|
||||
</p>
|
||||
|
||||
**🌐 [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 <name>` — 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</stron
|
||||
|
||||
> 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.
|
||||
|
||||
|
||||
+42
-12
@@ -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 <keyword>` or `cli-hub list -c <category>`
|
||||
3. **Install**: `cli-hub install <name>` (installs the `cli-anything-<name>` pip package)
|
||||
4. **Run**: `cli-anything-<name>` for REPL, or `cli-anything-<name> <command>` 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=<software>/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-<software> --json <command> [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
|
||||
|
||||
@@ -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 <keyword>` or `cli-hub list --json` to discover tools
|
||||
3. `cli-hub install <name>` 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)
|
||||
@@ -0,0 +1,3 @@
|
||||
"""cli-hub — Download, manage, and browse CLI-Anything harnesses."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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-<name> with underscores
|
||||
# but we derive it from the install_cmd's subdirectory
|
||||
# The namespace package is cli_anything.<name>, entry point is cli-anything-<name>
|
||||
# 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}"
|
||||
@@ -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"]))
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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=<name>/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-<name>
|
||||
|
||||
# One-shot command
|
||||
cli-anything-<name> <group> <command> [options]
|
||||
|
||||
# JSON output for agents
|
||||
cli-anything-<name> --json <group> <command>
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -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=<name>/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-<name>
|
||||
|
||||
# One-shot command
|
||||
cli-anything-<name> <group> <command> [options]
|
||||
|
||||
# JSON output for agents
|
||||
cli-anything-<name> --json <group> <command>
|
||||
```
|
||||
|
||||
## 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
|
||||
+101
-15
@@ -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 @@
|
||||
<h1><span class="hero-accent">CLI-</span><span class="hero-dim">Anything</span> <span class="hero-accent">Hub</span></h1>
|
||||
<p class="hero-tagline"><strong>Any software. Any codebase. Any Web API.</strong> Generate an agent-native CLI and let AI agents operate it — install with a single pip command.</p>
|
||||
|
||||
<div class="empower-row">
|
||||
<div class="empower-card">
|
||||
<h3>Empower your agents — <span>install in one command</span></h3>
|
||||
<div class="install-row">
|
||||
@@ -793,6 +835,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empower-card empower-self">
|
||||
<h3>Empower yourself — <span>your CLI toolkit</span></h3>
|
||||
<div class="install-row">
|
||||
<code>pip install cli-anything-hub</code>
|
||||
<button class="copy-btn" onclick="copyCmd(this, 'pip install cli-anything-hub')">Copy</button>
|
||||
</div>
|
||||
<div class="cmd-grid">
|
||||
<div class="cmd-item"><span class="cmd-name">cli-hub list</span><span class="cmd-desc">Browse all CLIs</span></div>
|
||||
<div class="cmd-item"><span class="cmd-name">cli-hub search</span><span class="cmd-desc">Find by keyword</span></div>
|
||||
<div class="cmd-item"><span class="cmd-name">cli-hub install</span><span class="cmd-desc">Install a CLI</span></div>
|
||||
<div class="cmd-item"><span class="cmd-name">cli-hub info</span><span class="cmd-desc">CLI details</span></div>
|
||||
<div class="cmd-item"><span class="cmd-name">cli-hub update</span><span class="cmd-desc">Update a CLI</span></div>
|
||||
<div class="cmd-item"><span class="cmd-name">cli-hub uninstall</span><span class="cmd-desc">Remove a CLI</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a class="btn-primary" href="./SKILL.txt" target="_blank">Agent SKILL (SKILL.txt)</a>
|
||||
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
|
||||
@@ -1031,8 +1090,8 @@
|
||||
${dateHtml}
|
||||
${requiresHtml}
|
||||
<div class="card-install">
|
||||
<code>${esc(c.install_cmd)}</code>
|
||||
<button class="card-copy-btn" onclick="copyCmd(this, '${esc(c.install_cmd)}')">Copy</button>
|
||||
<code>pip install cli-anything-hub\ncli-hub install ${esc(c.name)}</code>
|
||||
<button class="card-copy-btn" onclick="copyCmd(this, 'pip install cli-anything-hub && cli-hub install ${esc(c.name)}')">Copy</button>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="card-links">${sourceLink}${skillLink ? ' · ' + skillLink : ''}</div>
|
||||
@@ -1083,18 +1142,47 @@
|
||||
const isWebdriver = navigator.webdriver === true;
|
||||
let humanConfirmed = false;
|
||||
|
||||
// ── Send event to BOTH Umami website IDs ──
|
||||
// The global `umami` object only binds to one script tag, so we also
|
||||
// POST directly to the send API for the second site.
|
||||
const UMAMI_SEND = 'https://cloud.umami.is/api/send';
|
||||
const DUAL_WEBSITE_IDS = [
|
||||
'07082d05-efd3-4f85-a7a1-b426b0e8bfaa',
|
||||
'a076c661-bed1-405c-a522-813794e688b4',
|
||||
];
|
||||
function trackBoth(eventName, eventData) {
|
||||
// POST directly to both sites — do NOT also call umami.track()
|
||||
// because that would double-count on whichever site umami is bound to.
|
||||
DUAL_WEBSITE_IDS.forEach(wid => {
|
||||
const payload = {
|
||||
type: 'event',
|
||||
payload: {
|
||||
website: wid,
|
||||
hostname: location.hostname,
|
||||
url: location.pathname,
|
||||
name: eventName,
|
||||
data: eventData || {},
|
||||
},
|
||||
};
|
||||
fetch(UMAMI_SEND, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
keepalive: true,
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// Tag the visit type via Umami custom events
|
||||
if (typeof umami !== 'undefined') {
|
||||
if (isKnownAgent || isWebdriver) {
|
||||
umami.track('visit-agent', { ua: ua.slice(0, 200) });
|
||||
}
|
||||
if (isKnownAgent || isWebdriver) {
|
||||
trackBoth('visit-agent', { ua: ua.slice(0, 200) });
|
||||
}
|
||||
|
||||
// Wait for Umami to load, then tag
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
if (typeof umami !== 'undefined' && (isKnownAgent || isWebdriver)) {
|
||||
umami.track('visit-agent', { ua: ua.slice(0, 200) });
|
||||
if (isKnownAgent || isWebdriver) {
|
||||
trackBoth('visit-agent', { ua: ua.slice(0, 200) });
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
@@ -1103,9 +1191,7 @@
|
||||
function onHumanInteraction() {
|
||||
if (humanConfirmed) return;
|
||||
humanConfirmed = true;
|
||||
if (typeof umami !== 'undefined') {
|
||||
umami.track('visit-human');
|
||||
}
|
||||
trackBoth('visit-human');
|
||||
// Clean up listeners
|
||||
['mousemove', 'touchstart', 'scroll', 'keydown', 'click'].forEach(evt => {
|
||||
document.removeEventListener(evt, onHumanInteraction);
|
||||
|
||||
Reference in New Issue
Block a user