feat(cli-hub): bump to 0.4.0 and add CLI-Matrix analytics

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.
This commit is contained in:
yuhao
2026-06-25 13:45:58 +00:00
parent 5b9ec80def
commit dc73924892
4 changed files with 115 additions and 2 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""cli-hub — Download, manage, and browse CLI-Anything harnesses."""
__version__ = "0.3.0"
__version__ = "0.4.0"
+51
View File
@@ -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()
+62
View File
@@ -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))
+1 -1
View File
@@ -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",