From dc7392489222dbcc520817609290755d6dd8b0bb Mon Sep 17 00:00:00 2001 From: yuhao Date: Thu, 25 Jun 2026 13:45:58 +0000 Subject: [PATCH] feat(cli-hub): bump to 0.4.0 and add CLI-Matrix analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cli-matrix command family (#355) shipped to the repo but never reached PyPI: version stayed at 0.3.0, which was already published, so the publish workflow skipped it. Bump to 0.4.0 in both setup.py and __init__.py to trigger a fresh release carrying matrix.py / matrix_skill.py. Also instrument the matrix surface under the existing unified PostHog mechanism (flat event names, breakdown by properties — same convention as cli-install / cli-launch): - matrix-install: install / dry_run / skill_only / error, with scope + installed/skipped/failed counts (headline conversion signal) - matrix-preflight: capability-coverage check with covered/capabilities/gaps - matrix-discover: list / search / can / recipes, with query + result count - matrix-info: info / doctor inspects of a single matrix All events honor CLI_HUB_NO_ANALYTICS and the provider switch. 163 tests pass. --- cli-hub/cli_hub/__init__.py | 2 +- cli-hub/cli_hub/analytics.py | 51 +++++++++++++++++++++++++++++ cli-hub/cli_hub/cli.py | 62 ++++++++++++++++++++++++++++++++++++ cli-hub/setup.py | 2 +- 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/cli-hub/cli_hub/__init__.py b/cli-hub/cli_hub/__init__.py index a0fca10c3..b5cd191c5 100644 --- a/cli-hub/cli_hub/__init__.py +++ b/cli-hub/cli_hub/__init__.py @@ -1,3 +1,3 @@ """cli-hub — Download, manage, and browse CLI-Anything harnesses.""" -__version__ = "0.3.0" +__version__ = "0.4.0" diff --git a/cli-hub/cli_hub/analytics.py b/cli-hub/cli_hub/analytics.py index a5632d364..714304ccb 100644 --- a/cli-hub/cli_hub/analytics.py +++ b/cli-hub/cli_hub/analytics.py @@ -308,6 +308,57 @@ def track_launch(cli_name): }) +def track_matrix_install(matrix_name, scope="full", status="ok", + installed=0, skipped=0, failed=0, dry_run=False): + """Track a CLI-Matrix install. The headline conversion signal for matrices: + which matrix, the install scope (capability/recipe/only/full), and the outcome. + Matrix name and scope go in properties so the event catalog stays flat — dashboards + break down by properties.matrix / properties.scope, mirroring track_install.""" + track_event("matrix-install", url=f"/cli-anything-hub/matrix/install/{matrix_name}", data={ + "matrix": matrix_name, + "scope": scope, + "status": status, + "installed": installed, + "skipped": skipped, + "failed": failed, + "dry_run": dry_run, + "platform": platform.system().lower(), + }) + + +def track_matrix_preflight(matrix_name, scope="full", covered=0, capabilities=0, gaps=0): + """Track a CLI-Matrix preflight (capability-coverage check) — consideration signal + that precedes install. Captures how much of the matrix the environment already covers.""" + track_event("matrix-preflight", url=f"/cli-anything-hub/matrix/preflight/{matrix_name}", data={ + "matrix": matrix_name, + "scope": scope, + "covered": covered, + "capabilities": capabilities, + "gaps": gaps, + "platform": platform.system().lower(), + }) + + +def track_matrix_discover(kind, query="", results=0): + """Track CLI-Matrix discovery — `matrix list`, `matrix search`, `matrix recipes`, + and `cli-hub can`. `kind` distinguishes the surface; `results` is the hit count.""" + track_event("matrix-discover", url="/cli-anything-hub/matrix/discover", data={ + "kind": kind, + "query": (query or "")[:120], + "results": results, + "platform": platform.system().lower(), + }) + + +def track_matrix_info(matrix_name, kind="info"): + """Track inspecting a single CLI-Matrix — `matrix info` and `matrix doctor`.""" + track_event("matrix-info", url=f"/cli-anything-hub/matrix/info/{matrix_name}", data={ + "matrix": matrix_name, + "kind": kind, + "platform": platform.system().lower(), + }) + + def track_visit(is_agent=False, command="root", detection=None): """Track a cli-hub invocation using the new cli-hub call event.""" stdin_tty = _stdin_is_tty() diff --git a/cli-hub/cli_hub/cli.py b/cli-hub/cli_hub/cli.py index a2f3fba96..d037a6539 100644 --- a/cli-hub/cli_hub/cli.py +++ b/cli-hub/cli_hub/cli.py @@ -36,6 +36,10 @@ from cli_hub.analytics import ( track_first_run, track_install, track_launch, + track_matrix_discover, + track_matrix_info, + track_matrix_install, + track_matrix_preflight, track_uninstall, track_visit, ) @@ -409,6 +413,7 @@ def preview_open(preview_ref, output_path, poll_ms, port): def can(query, as_json): """Find a capability across all matrices for a task (e.g. cli-hub can "transcribe audio").""" hits = search_capabilities(query) + track_matrix_discover("can", query=query, results=len(hits)) if as_json: click.echo(json_mod.dumps({"query": query, "matched_capabilities": hits}, indent=2)) @@ -460,6 +465,7 @@ def list_matrices(as_json): click.secho(f"Failed to fetch matrix registry: {e}", fg="red", err=True) raise SystemExit(1) + track_matrix_discover("list", results=len(matrices)) installed = get_installed() if as_json: @@ -489,6 +495,7 @@ def list_matrices(as_json): def matrix_search(query, as_json): """Search matrices by name, capabilities, providers, recipes, or gaps.""" results = search_matrices(query) + track_matrix_discover("search", query=query, results=len(results)) query_lower = query.lower() if as_json: @@ -545,6 +552,7 @@ def matrix_info(name, as_json): click.secho(f"Matrix '{name}' not found.", fg="red", err=True) raise SystemExit(1) + track_matrix_info(matrix_item["name"], kind="info") installed = get_installed() cli_names = matrix_item.get("clis", []) installed_count = sum(1 for cli_name in cli_names if cli_name in installed) @@ -666,6 +674,15 @@ def matrix_preflight(name, capability, recipe, offline, fix_hints, summary_only, matrix_item, capability_id=capability, offline=offline, capability_ids=capability_ids ) + _preflight_summary = payload.get("summary", {}) + track_matrix_preflight( + matrix_item["name"], + scope=_matrix_scope_kind(capability, recipe, None), + covered=_preflight_summary.get("covered", 0), + capabilities=_preflight_summary.get("capabilities", 0), + gaps=_preflight_summary.get("gaps", 0), + ) + if as_json: click.echo(json_mod.dumps(payload, indent=2)) raise SystemExit(EXIT_PARTIAL if payload["summary"].get("gaps", 0) else EXIT_OK) @@ -780,6 +797,17 @@ def _render_dry_run(payload): + _scope_args(payload["scope"])) +def _matrix_scope_kind(capability, recipe, only): + """Compact scope label for analytics breakdowns (not the echoed flags).""" + if capability: + return "capability" + if recipe: + return "recipe" + if only: + return "only" + return "full" + + def _scope_args(scope): """Reconstruct the scope flags for an echoed command line.""" scope_type = scope.get("type") @@ -817,6 +845,11 @@ def matrix_install(name, capability, recipe, only, dry_run, resume, skill_only, click.secho(f"Matrix '{name}' not found.", fg="red", err=True) raise SystemExit(EXIT_FAIL) rendered_skill_path = render_matrix_skill_file(matrix_item, installed=get_installed()) + track_matrix_install( + matrix_item["name"], + scope=_matrix_scope_kind(capability, recipe, only), + status="skill_only", + ) click.echo(f" Local matrix skill: {rendered_skill_path}") click.echo(f" Install CLIs: cli-hub matrix install {matrix_item['name']}") return @@ -829,6 +862,12 @@ def matrix_install(name, capability, recipe, only, dry_run, resume, skill_only, else: click.secho(f"✗ {payload['error']}", fg="red", err=True) raise SystemExit(EXIT_USAGE if payload.get("arg_error") else EXIT_FAIL) + track_matrix_install( + payload["matrix"]["name"], + scope=_matrix_scope_kind(capability, recipe, only), + status="dry_run", + dry_run=True, + ) if as_json: data = {k: payload[k] for k in ("scope", "scope_label", "plan", "not_managed", "summary")} data["matrix"] = payload["matrix"]["name"] @@ -841,6 +880,11 @@ def matrix_install(name, capability, recipe, only, dry_run, resume, skill_only, name, capability=capability, recipe=recipe, only=only, resume=resume ) if payload.get("error"): + track_matrix_install( + name, + scope=_matrix_scope_kind(capability, recipe, only), + status="usage_error" if payload.get("arg_error") else "error", + ) if as_json: click.echo(json_mod.dumps({"error": payload["error"]}, indent=2)) else: @@ -850,6 +894,21 @@ def matrix_install(name, capability, recipe, only, dry_run, resume, skill_only, matrix_item = payload["matrix"] summary = payload["summary"] + if summary["failed"]: + _install_status = "partial" if (summary["installed"] or summary["skipped"]) else "failed" + elif payload.get("nothing_to_resume"): + _install_status = "noop" + else: + _install_status = "ok" + track_matrix_install( + matrix_item["name"], + scope=_matrix_scope_kind(capability, recipe, only), + status=_install_status, + installed=summary["installed"], + skipped=summary["skipped"], + failed=summary["failed"], + ) + if as_json: data = { "matrix": matrix_item["name"], @@ -908,6 +967,8 @@ def matrix_doctor(name, as_json): click.secho(f"✗ {payload['error']}", fg="red", err=True) raise SystemExit(EXIT_FAIL) + track_matrix_info(payload["matrix"]["name"], kind="doctor") + if as_json: data = {k: payload[k] for k in ("last_run", "checks", "summary")} data["matrix"] = payload["matrix"]["name"] @@ -941,6 +1002,7 @@ def matrix_doctor(name, as_json): def matrix_recipes(query, as_json): """List task-oriented recipes across all matrices (F1.4).""" recipes = all_recipes(query) + track_matrix_discover("recipes", query=query or "", results=len(recipes)) if as_json: click.echo(json_mod.dumps({"query": query, "recipes": recipes}, indent=2)) diff --git a/cli-hub/setup.py b/cli-hub/setup.py index 6274164ae..f942d9d75 100644 --- a/cli-hub/setup.py +++ b/cli-hub/setup.py @@ -50,7 +50,7 @@ class sdist(_sdist): setup( name="cli-anything-hub", - version="0.3.0", + version="0.4.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",