feat(cli-hub): migrate analytics to PostHog, add stats pipeline and refined footer

- Switch cli-hub analytics client from Umami to PostHog (us.i.posthog.com),
  with agent/human classification derived from env vars and parent-process
  names, and persistent analytics id at ~/.cli-hub/.analytics_id.
- Add daily deploy-pages step running .github/scripts/update_hub_analytics_stats.py
  to pull PostHog totals into docs/hub/analytics-stats.json.
- Redesign docs/hub footer analytics panel (index.html + index-modern.html)
  as a compact Apple/OpenAI-style card: live eyebrow, total, 2px ratio bar,
  and inline Human/Agent split with percentages computed client-side.
- Expand cli-hub tests to cover the new analytics context fields and
  provider switch.
This commit is contained in:
yuhao
2026-04-23 16:27:14 +00:00
parent 07b833cfd3
commit 8cae111a48
9 changed files with 1542 additions and 346 deletions

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Generate docs/hub/analytics-stats.json from a fixed Umami baseline plus PostHog increments."""
from __future__ import annotations
import json
import os
import sys
from datetime import UTC, datetime
from pathlib import Path
from urllib import error, request
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_PATH = REPO_ROOT / "docs" / "hub" / "analytics-stats.json"
SITE = "clianything.cc"
MIGRATION_STARTED_AT = "2026-04-23T09:06:38Z"
MIGRATION_STARTED_AT_MS = 1776935198505
DEFAULT_POSTHOG_APP_HOST = "https://us.posthog.com"
DEFAULT_POSTHOG_PROJECT_ID = "393992"
BASELINE = {
"provider": "umami",
"site": SITE,
"scope": "clianything.cc only",
"queried_at": "2026-04-23T09:06:38.505000+00:00",
"total": 21123,
"human": 7689,
"agent": 13434,
"cli_hub_total": 16361,
"cli_hub_human": 3237,
"cli_hub_agent": 13124,
}
def _posthog_increment():
api_key = os.environ.get("POSTHOG_PERSONAL_API_KEY", "").strip()
app_host = os.environ.get("POSTHOG_APP_HOST", DEFAULT_POSTHOG_APP_HOST).rstrip("/")
project_id = os.environ.get("POSTHOG_PROJECT_ID", DEFAULT_POSTHOG_PROJECT_ID).strip()
if not api_key:
return {
"status": "missing_api_key",
"total": 0,
"human": 0,
"agent": 0,
"cli_hub_total": 0,
"cli_hub_human": 0,
"cli_hub_agent": 0,
}
hogql = (
"select "
"countIf(event = 'visit-human' and properties.source = 'web' and properties.site = 'clianything.cc') as human, "
"countIf(event = 'visit-agent' and properties.source = 'web' and properties.site = 'clianything.cc') as agent, "
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = false) as cli_hub_human, "
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = true) as cli_hub_agent "
"from events "
f"where timestamp >= parseDateTimeBestEffort('{MIGRATION_STARTED_AT}') "
)
payload = {
"query": {
"kind": "HogQLQuery",
"query": hogql,
},
"name": "cli-anything hub public website stats",
}
req = request.Request(
f"{app_host}/api/projects/{project_id}/query/",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with request.urlopen(req, timeout=30) as response:
data = json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace").strip()
print(f"Warning: PostHog stats query failed ({exc.code}): {detail}", file=sys.stderr)
return {
"status": f"http_{exc.code}",
"total": 0,
"human": 0,
"agent": 0,
"cli_hub_total": 0,
"cli_hub_human": 0,
"cli_hub_agent": 0,
}
except Exception as exc: # pragma: no cover - exercised in CI failure mode
print(f"Warning: PostHog stats query failed: {exc}", file=sys.stderr)
return {
"status": "error",
"total": 0,
"human": 0,
"agent": 0,
"cli_hub_total": 0,
"cli_hub_human": 0,
"cli_hub_agent": 0,
}
results = data.get("results") or [[0, 0, 0, 0]]
row = results[0] if results else [0, 0, 0, 0]
human = int(row[0] or 0)
agent = int(row[1] or 0)
cli_hub_human = int(row[2] or 0)
cli_hub_agent = int(row[3] or 0)
return {
"status": "ok",
"app_host": app_host,
"project_id": project_id,
"human": human,
"agent": agent,
"total": human + agent,
"cli_hub_human": cli_hub_human,
"cli_hub_agent": cli_hub_agent,
"cli_hub_total": cli_hub_human + cli_hub_agent,
}
def main():
posthog_increment = _posthog_increment()
totals = {
"total": BASELINE["total"] + posthog_increment["total"],
"human": BASELINE["human"] + posthog_increment["human"],
"agent": BASELINE["agent"] + posthog_increment["agent"],
"cli_hub_total": BASELINE["cli_hub_total"] + posthog_increment["cli_hub_total"],
"cli_hub_human": BASELINE["cli_hub_human"] + posthog_increment["cli_hub_human"],
"cli_hub_agent": BASELINE["cli_hub_agent"] + posthog_increment["cli_hub_agent"],
}
payload = {
"site": SITE,
"provider": "posthog",
"generated_at": datetime.now(UTC).isoformat(),
"migration_started_at": MIGRATION_STARTED_AT,
"baseline": BASELINE,
"posthog_increment": posthog_increment,
"totals": totals,
}
OUTPUT_PATH.write_text(json.dumps(payload, indent=2) + "\n")
print(json.dumps(payload, indent=2))
if __name__ == "__main__":
main()

View File

@@ -12,6 +12,7 @@ on:
- 'docs/hub/**'
- '.github/workflows/deploy-pages.yml'
- '.github/scripts/update_registry_dates.py'
- '.github/scripts/update_hub_analytics_stats.py'
- '.github/scripts/generate_meta_skill.py'
workflow_dispatch:
@@ -44,6 +45,13 @@ jobs:
- name: Generate meta-skill
run: python3 .github/scripts/generate_meta_skill.py
- name: Generate hub analytics stats
env:
POSTHOG_APP_HOST: https://us.posthog.com
POSTHOG_PROJECT_ID: "393992"
POSTHOG_PERSONAL_API_KEY: ${{ secrets.POSTHOG_PERSONAL_API_KEY }}
run: python3 .github/scripts/update_hub_analytics_stats.py
- name: Install AWS CLI
run: pip install awscli

View File

@@ -70,7 +70,7 @@ 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.
cli-hub sends anonymous usage events to help track adoption. The default provider is [PostHog](https://posthog.com), while the legacy Umami path remains available via `CLI_HUB_ANALYTICS_PROVIDER=umami`. No personal data is collected.
Opt out:

View File

@@ -1,22 +1,74 @@
"""Lightweight, opt-out-able download event tracking via Umami."""
"""Lightweight, opt-out-able analytics with switchable providers."""
import atexit
import os
import platform
import re
import sys
import threading
import uuid
from pathlib import Path
import requests
from cli_hub import __version__
ANALYTICS_PROVIDER = "posthog"
UMAMI_URL = "https://cloud.umami.is/api/send"
WEBSITE_ID = "a076c661-bed1-405c-a522-813794e688b4"
UMAMI_WEBSITE_ID = "a076c661-bed1-405c-a522-813794e688b4"
POSTHOG_API_HOST = "https://us.i.posthog.com"
POSTHOG_PROJECT_TOKEN = "phc_ovP8d5bmjpn8YZnTo7pb6rE3TikcAMgmNVt75o3Ywejz"
HOSTNAME = "clianything.cc"
USER_AGENT = f"Mozilla/5.0 (compatible; cli-anything-hub/{__version__})"
ANALYTICS_ID_FILE = ".analytics_id"
_pending_threads = []
_lock = threading.Lock()
_AGENT_ENV_RULES = (
("CLAUDE_CODE", "agent_tool", "claude-code-env"),
("CLAUDECODE", "agent_tool", "claude-code-env-alt"),
("CODEX", "agent_tool", "codex-env"),
("OPENAI_CODEX", "agent_tool", "codex-env-alt"),
("CURSOR_SESSION", "agent_tool", "cursor-session-env"),
("CURSOR_TRACE_ID", "agent_tool", "cursor-trace-env"),
("CLINE_SESSION", "agent_tool", "cline-session-env"),
("AIDER", "agent_tool", "aider-env"),
("AIDER_SESSION_ID", "agent_tool", "aider-session-env"),
("CONTINUE_SESSION", "agent_tool", "continue-session-env"),
("OPENHANDS_AGENT", "agent_tool", "openhands-agent-env"),
("OPENHANDS_RUNTIME", "agent_tool", "openhands-runtime-env"),
("BROWSER_USE", "agent_tool", "browser-use-env"),
("STAGEHAND", "agent_tool", "stagehand-env"),
("GOOSE_AGENT", "agent_tool", "goose-agent-env"),
("ROO_CODE", "agent_tool", "roo-code-env"),
("WINDSURF_AGENT", "agent_tool", "windsurf-agent-env"),
)
_PARENT_PROCESS_RULES = (
("claude-code-process", "agent_tool", re.compile(r"\bclaude(?:[ -]?code)?\b", re.IGNORECASE)),
("codex-process", "agent_tool", re.compile(r"\bcodex(?:-cli)?\b", re.IGNORECASE)),
("copilot-process", "agent_tool", re.compile(r"\bcopilot(?:-cli)?\b", re.IGNORECASE)),
("cursor-process", "agent_tool", re.compile(r"\bcursor\b", re.IGNORECASE)),
("cline-process", "agent_tool", re.compile(r"\bcline\b", re.IGNORECASE)),
("aider-process", "agent_tool", re.compile(r"\baider\b", re.IGNORECASE)),
("continue-process", "agent_tool", re.compile(r"\bcontinue\b", re.IGNORECASE)),
("gemini-process", "agent_tool", re.compile(r"\bgemini(?:-cli)?\b", re.IGNORECASE)),
("auggie-process", "agent_tool", re.compile(r"\bauggie(?:-cli)?\b", re.IGNORECASE)),
("augment-process", "agent_tool", re.compile(r"\baugment(?:[ -]?agent)?\b", re.IGNORECASE)),
("amp-process", "agent_tool", re.compile(r"\bamp(?:code)?\b", re.IGNORECASE)),
("opencode-process", "agent_tool", re.compile(r"\bopencode\b", re.IGNORECASE)),
("kilo-process", "agent_tool", re.compile(r"\bkilo(?:code)?\b", re.IGNORECASE)),
("qodo-process", "agent_tool", re.compile(r"\bqodo\b", re.IGNORECASE)),
("kiro-process", "agent_tool", re.compile(r"\bkiro\b", re.IGNORECASE)),
("openhands-process", "agent_tool", re.compile(r"\bopenhands\b", re.IGNORECASE)),
("browser-use-process", "agent_tool", re.compile(r"\bbrowser[- ]use\b", re.IGNORECASE)),
("stagehand-process", "agent_tool", re.compile(r"\bstagehand\b", re.IGNORECASE)),
("roo-process", "agent_tool", re.compile(r"\broo(?:-code)?\b", re.IGNORECASE)),
("windsurf-process", "agent_tool", re.compile(r"\bwindsurf\b", re.IGNORECASE)),
("goose-process", "agent_tool", re.compile(r"\bgoose\b", re.IGNORECASE)),
)
def _flush_pending():
"""Wait for in-flight analytics requests before process exit."""
@@ -33,11 +85,179 @@ def _is_enabled():
return os.environ.get("CLI_HUB_NO_ANALYTICS", "").strip() not in ("1", "true", "yes")
def _provider():
provider = os.environ.get("CLI_HUB_ANALYTICS_PROVIDER", ANALYTICS_PROVIDER).strip().lower()
return provider if provider in {"posthog", "umami"} else ANALYTICS_PROVIDER
def _analytics_dir():
return Path.home() / ".cli-hub"
def _stdin_is_tty():
try:
return sys.stdin.isatty()
except Exception:
return False
def _read_parent_pid(pid):
status_path = Path("/proc") / str(pid) / "status"
try:
for line in status_path.read_text().splitlines():
if line.startswith("PPid:"):
parts = line.split()
return int(parts[1]) if len(parts) > 1 else None
except Exception:
return None
return None
def _read_process_cmdline(pid):
cmdline_path = Path("/proc") / str(pid) / "cmdline"
try:
raw = cmdline_path.read_bytes()
except Exception:
return ""
return raw.replace(b"\x00", b" ").decode("utf-8", errors="ignore").strip()
def _parent_process_commands(max_depth=4):
commands = []
pid = os.getpid()
for _ in range(max_depth):
pid = _read_parent_pid(pid)
if not pid or pid <= 1:
break
cmd = _read_process_cmdline(pid)
if cmd:
commands.append(cmd)
return commands
def detect_invocation_context():
"""Classify the current cli-hub invocation as human, agent, or scripted."""
signals = []
for env_name, category, signal_id in _AGENT_ENV_RULES:
if os.environ.get(env_name):
signals.append({"id": signal_id, "category": category})
for cmd in _parent_process_commands():
for signal_id, category, pattern in _PARENT_PROCESS_RULES:
if pattern.search(cmd):
signals.append({"id": signal_id, "category": category})
seen = set()
unique_signals = []
for signal in signals:
if signal["id"] in seen:
continue
seen.add(signal["id"])
unique_signals.append(signal)
stdin_tty = _stdin_is_tty()
if unique_signals:
primary = unique_signals[0]
return {
"is_agent": True,
"traffic_type": "agent",
"category": primary["category"],
"reason": primary["id"],
"signals": [signal["id"] for signal in unique_signals],
"stdin_tty": stdin_tty,
"is_interactive": stdin_tty,
}
if not stdin_tty:
return {
"is_agent": True,
"traffic_type": "agent",
"category": "scripted_client",
"reason": "stdin-not-tty",
"signals": ["stdin-not-tty"],
"stdin_tty": False,
"is_interactive": False,
}
return {
"is_agent": False,
"traffic_type": "human",
"category": "human",
"reason": "human",
"signals": [],
"stdin_tty": True,
"is_interactive": True,
}
def _get_distinct_id():
override = os.environ.get("CLI_HUB_ANALYTICS_DISTINCT_ID", "").strip()
if override:
return override
marker = _analytics_dir() / ANALYTICS_ID_FILE
try:
if marker.exists():
value = marker.read_text().strip()
if value:
return value
marker.parent.mkdir(parents=True, exist_ok=True)
value = str(uuid.uuid4())
marker.write_text(value)
return value
except Exception:
return f"cli-hub-anon-{uuid.uuid4()}"
def _posthog_capture_url():
host = os.environ.get("CLI_HUB_POSTHOG_API_HOST", POSTHOG_API_HOST).rstrip("/")
return f"{host}/capture/"
def _build_umami_payload(event_name, url, data):
return {
"type": "event",
"payload": {
"website": UMAMI_WEBSITE_ID,
"hostname": HOSTNAME,
"url": url,
"name": event_name,
"data": data,
},
}
def _build_posthog_payload(event_name, url, data):
return {
"api_key": os.environ.get("CLI_HUB_POSTHOG_PROJECT_TOKEN", POSTHOG_PROJECT_TOKEN),
"event": event_name,
"distinct_id": _get_distinct_id(),
"properties": {
"$current_url": f"https://{HOSTNAME}{url}",
"hostname": HOSTNAME,
"source": "cli",
"channel": "cli-hub",
"hub_version": __version__,
**(data or {}),
},
}
def _send_event(payload):
"""Send a single event payload. Blocking — callers should use threads."""
try:
if _provider() == "umami":
return requests.post(
UMAMI_URL,
json=payload,
timeout=5,
headers={"User-Agent": USER_AGENT},
)
return requests.post(
UMAMI_URL, json=payload, timeout=5,
_posthog_capture_url(),
json=payload,
timeout=5,
headers={"User-Agent": USER_AGENT},
)
except Exception:
@@ -45,20 +265,15 @@ def _send_event(payload):
def track_event(event_name, url="/cli-anything-hub", data=None):
"""Fire-and-forget event to Umami. Non-blocking, never raises."""
"""Fire-and-forget event to the active provider. 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 {},
},
}
event_data = data or {}
if _provider() == "umami":
payload = _build_umami_payload(event_name, url, event_data)
else:
payload = _build_posthog_payload(event_name, url, event_data)
t = threading.Thread(target=_send_event, args=(payload,), daemon=True)
with _lock:
@@ -82,19 +297,34 @@ def track_uninstall(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",
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()
context = detection or {
"is_agent": is_agent,
"traffic_type": "agent" if is_agent else "human",
"category": "legacy-agent" if is_agent else "human",
"reason": "legacy-flag" if is_agent else "human",
"signals": ["legacy-flag"] if is_agent else [],
"stdin_tty": stdin_tty,
"is_interactive": stdin_tty,
}
track_event("cli-hub call", url="/cli-anything-hub/call", data={
"command": command,
"is_agent": context["is_agent"],
"traffic_type": context["traffic_type"],
"agent_category": context["category"],
"agent_reason": context["reason"],
"agent_signals": context["signals"][:12],
"stdin_tty": context["stdin_tty"],
"is_interactive": context["is_interactive"],
"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"
marker = _analytics_dir() / ".first_run_sent"
if marker.exists():
return
track_event("cli-anything-hub-installed", url="/cli-anything-hub/installed", data={
@@ -110,20 +340,4 @@ def track_first_run():
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
return detect_invocation_context()["is_agent"]

View File

@@ -2,13 +2,28 @@
import os
import shutil
import sys
import click
from cli_hub import __version__
from cli_hub.registry import fetch_all_clis, get_cli, search_clis, list_categories
from cli_hub.installer import install_cli, uninstall_cli, get_installed, update_cli
from cli_hub.analytics import track_install, track_uninstall, track_visit, track_first_run, _detect_is_agent
from cli_hub.analytics import detect_invocation_context, track_install, track_uninstall, track_visit, track_first_run
def _invocation_command(ctx, version):
"""Return a compact label for the current invocation."""
argv = sys.argv[1:]
if version:
return "--version"
if ctx.invoked_subcommand:
return ctx.invoked_subcommand
if any(arg in ("--help", "-h") for arg in argv):
return "--help"
if argv:
return argv[0]
return "root"
@click.group(invoke_without_command=True)
@@ -17,7 +32,7 @@ from cli_hub.analytics import track_install, track_uninstall, track_visit, track
def main(ctx, version):
"""cli-hub — Download and manage CLI-Anything harnesses and public CLIs."""
track_first_run()
track_visit(is_agent=_detect_is_agent())
track_visit(command=_invocation_command(ctx, version), detection=detect_invocation_context())
if version:
click.echo(f"cli-hub {__version__}")
return

View File

@@ -22,7 +22,7 @@ from cli_hub.installer import (
_install_strategy,
_UV_INSTALL_HINT,
)
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.analytics import _is_enabled, track_event, track_install, track_uninstall as analytics_track_uninstall, track_visit, track_first_run, _detect_is_agent, detect_invocation_context
from cli_hub.cli import main
@@ -510,8 +510,9 @@ class TestAnalytics:
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"
assert payload["event"] == "test-event"
assert payload["properties"]["hostname"] == "clianything.cc"
assert payload["properties"]["source"] == "cli"
@patch("cli_hub.analytics._send_event")
def test_track_event_noop_when_disabled(self, mock_send):
@@ -521,6 +522,17 @@ class TestAnalytics:
time.sleep(0.2)
mock_send.assert_not_called()
@patch("cli_hub.analytics._send_event")
def test_track_event_supports_umami_provider(self, mock_send):
with patch.dict(os.environ, {"CLI_HUB_ANALYTICS_PROVIDER": "umami"}, clear=False):
track_event("test-event")
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_install_event_name_includes_cli(self, mock_send):
"""cli-install event name must include CLI name for dashboard visibility."""
@@ -530,11 +542,11 @@ class TestAnalytics:
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"]
assert payload["event"] == "cli-install:gimp"
assert payload["properties"]["$current_url"] == "https://clianything.cc/cli-anything-hub/install/gimp"
assert payload["properties"]["cli"] == "gimp"
assert payload["properties"]["version"] == "1.0.0"
assert "platform" in payload["properties"]
@patch("cli_hub.analytics._send_event")
def test_track_uninstall_event_name_includes_cli(self, mock_send):
@@ -545,33 +557,38 @@ class TestAnalytics:
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"
assert payload["event"] == "cli-uninstall:blender"
assert payload["properties"]["$current_url"] == "https://clianything.cc/cli-anything-hub/uninstall/blender"
assert payload["properties"]["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."""
"""cli-hub call 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"
assert payload["event"] == "cli-hub call"
assert payload["properties"]["$current_url"] == "https://clianything.cc/cli-anything-hub/call"
assert payload["properties"]["command"] == "root"
assert payload["properties"]["is_agent"] is False
assert payload["properties"]["traffic_type"] == "human"
@patch("cli_hub.analytics._send_event")
def test_track_visit_agent(self, mock_send):
"""visit-agent event sent when agent environment detected."""
"""cli-hub call event captures the agent flag."""
with patch.dict(os.environ, {}, clear=True):
track_visit(is_agent=True)
track_visit(is_agent=True, command="--version")
import time
time.sleep(0.2)
mock_send.assert_called_once()
payload = mock_send.call_args[0][0]
assert payload["payload"]["name"] == "visit-agent"
assert payload["event"] == "cli-hub call"
assert payload["properties"]["command"] == "--version"
assert payload["properties"]["is_agent"] is True
assert payload["properties"]["traffic_type"] == "agent"
def test_detect_agent_claude_code(self):
with patch.dict(os.environ, {"CLAUDE_CODE": "1"}):
@@ -581,13 +598,79 @@ class TestAnalytics:
with patch.dict(os.environ, {"CODEX": "1"}):
assert _detect_is_agent() is True
def test_detect_not_agent_clean_env(self):
@patch("cli_hub.analytics._parent_process_commands", return_value=["/usr/local/bin/codex --run"])
def test_detect_agent_from_parent_process(self, mock_cmds):
with patch.dict(os.environ, {}, clear=True):
context = detect_invocation_context()
assert context["is_agent"] is True
assert context["reason"] == "codex-process"
assert "codex-process" in context["signals"]
@pytest.mark.parametrize(
("command", "expected_reason"),
[
("/usr/local/bin/gemini --prompt fix tests", "gemini-process"),
("/usr/local/bin/copilot agent", "copilot-process"),
("/usr/local/bin/auggie --print review", "auggie-process"),
("/opt/augment/bin/augment", "augment-process"),
("/usr/local/bin/ampcode fix build", "amp-process"),
("/usr/local/bin/opencode agent create", "opencode-process"),
("/usr/local/bin/kilo auth", "kilo-process"),
("/usr/local/bin/qodo chat", "qodo-process"),
("/usr/local/bin/kiro /agent create", "kiro-process"),
],
)
@patch("cli_hub.analytics._parent_process_commands")
def test_detect_agent_from_expanded_parent_process_names(self, mock_cmds, command, expected_reason):
mock_cmds.return_value = [command]
with patch.dict(os.environ, {}, clear=True):
context = detect_invocation_context()
assert context["is_agent"] is True
assert context["reason"] == expected_reason
assert expected_reason in context["signals"]
@patch("cli_hub.analytics._parent_process_commands", return_value=[])
def test_detect_not_agent_clean_env(self, mock_cmds):
"""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._parent_process_commands", return_value=[])
def test_detect_non_tty_is_agent(self, mock_cmds):
with patch.dict(os.environ, {}, clear=True):
with patch("sys.stdin") as mock_stdin:
mock_stdin.isatty.return_value = False
context = detect_invocation_context()
assert context["is_agent"] is True
assert context["traffic_type"] == "agent"
assert context["category"] == "scripted_client"
assert context["reason"] == "stdin-not-tty"
@patch("cli_hub.analytics._send_event")
def test_track_visit_uses_detection_context(self, mock_send):
detection = {
"is_agent": True,
"traffic_type": "agent",
"category": "agent_tool",
"reason": "codex-process",
"signals": ["codex-process", "stdin-not-tty"],
"stdin_tty": False,
"is_interactive": False,
}
with patch.dict(os.environ, {}, clear=True):
track_visit(command="search", detection=detection)
import time
time.sleep(0.2)
payload = mock_send.call_args[0][0]
assert payload["properties"]["command"] == "search"
assert payload["properties"]["agent_reason"] == "codex-process"
assert payload["properties"]["agent_category"] == "agent_tool"
assert payload["properties"]["agent_signals"] == ["codex-process", "stdin-not-tty"]
assert payload["properties"]["stdin_tty"] is False
assert payload["properties"]["is_interactive"] 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."""
@@ -597,8 +680,8 @@ class TestAnalytics:
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"
assert payload["event"] == "cli-anything-hub-installed"
assert payload["properties"]["$current_url"] == "https://clianything.cc/cli-anything-hub/installed"
# Marker file should now exist
assert (tmp_path / ".cli-hub" / ".first_run_sent").exists()
@@ -623,32 +706,53 @@ class TestCLI:
def setup_method(self):
self.runner = click.testing.CliRunner()
self.human_detection = {
"is_agent": False,
"traffic_type": "human",
"category": "human",
"reason": "human",
"signals": [],
"stdin_tty": True,
"is_interactive": True,
}
self.agent_detection = {
"is_agent": True,
"traffic_type": "agent",
"category": "agent_tool",
"reason": "codex-env",
"signals": ["codex-env"],
"stdin_tty": False,
"is_interactive": False,
}
@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.detect_invocation_context")
def test_version(self, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
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_visit.assert_called_once_with(command="--version", detection=self.human_detection)
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)
@patch("cli_hub.cli.detect_invocation_context")
def test_help(self, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
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.detect_invocation_context")
@patch("cli_hub.cli.fetch_all_clis", return_value=SAMPLE_REGISTRY["clis"])
@patch("cli_hub.cli.list_categories", return_value=["3d", "audio", "image"])
@patch("cli_hub.cli.get_installed", return_value={})
def test_list_command(self, mock_installed, mock_categories, mock_fetch, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["list"])
assert "gimp" in result.output
assert "blender" in result.output
@@ -656,31 +760,34 @@ class TestCLI:
@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.detect_invocation_context")
@patch("cli_hub.cli.fetch_all_clis", return_value=SAMPLE_REGISTRY["clis"])
@patch("cli_hub.cli.list_categories", return_value=["3d", "audio", "image"])
@patch("cli_hub.cli.get_installed", return_value={})
def test_list_with_category(self, mock_installed, mock_categories, mock_fetch, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
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.detect_invocation_context")
@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):
mock_detect.return_value = self.human_detection
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.detect_invocation_context")
@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):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["info", "gimp"])
assert "GIMP" in result.output
assert "image" in result.output
@@ -689,19 +796,21 @@ class TestCLI:
@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.detect_invocation_context")
@patch("cli_hub.cli.get_cli", return_value=None)
def test_info_not_found(self, mock_get, mock_detect, mock_visit, mock_first_run):
mock_detect.return_value = self.human_detection
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.detect_invocation_context")
@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):
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["install", "gimp"])
assert result.exit_code == 0
assert "Installed" in result.output
@@ -709,30 +818,33 @@ class TestCLI:
@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.detect_invocation_context")
@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):
mock_detect.return_value = self.human_detection
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)
@patch("cli_hub.cli.detect_invocation_context")
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."""
"""When agent env detected, track_visit is called with the new cli-hub call metadata."""
mock_detect.return_value = self.agent_detection
result = self.runner.invoke(main, ["--version"])
mock_visit.assert_called_once_with(is_agent=True)
mock_visit.assert_called_once_with(command="--version", detection=self.agent_detection)
@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.detect_invocation_context")
@patch("cli_hub.cli.install_cli", return_value=(True, "Installed Jimeng / Dreamina CLI (dreamina)"))
@patch("cli_hub.cli.get_cli", return_value={**SAMPLE_REGISTRY["clis"][0], "entry_point": "dreamina", "name": "jimeng", "display_name": "Jimeng / Dreamina CLI", "version": "latest", "_source": "public"})
@patch("cli_hub.cli.track_install")
def test_install_shows_launch_hint(self, mock_track, mock_get, mock_install, mock_detect, mock_visit, mock_first_run):
"""Post-install output includes both entry point and cli-hub launch hint."""
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["install", "jimeng"])
assert result.exit_code == 0
assert "dreamina" in result.output
@@ -740,32 +852,35 @@ class TestCLI:
@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.detect_invocation_context")
@patch("cli_hub.cli.shutil.which", return_value="/usr/bin/dreamina")
@patch("cli_hub.cli.os.execvp")
@patch("cli_hub.cli.get_cli", return_value=JIMENG_CLI)
def test_launch_execs_entry_point(self, mock_get, mock_execvp, mock_which, mock_detect, mock_visit, mock_first_run):
"""launch execs the CLI entry point, passing through extra args."""
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["launch", "jimeng", "login"])
mock_execvp.assert_called_once_with("dreamina", ["dreamina", "login"])
@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.detect_invocation_context")
@patch("cli_hub.cli.shutil.which", return_value=None)
@patch("cli_hub.cli.get_cli", return_value=JIMENG_CLI)
def test_launch_not_on_path_shows_install_hint(self, mock_get, mock_which, mock_detect, mock_visit, mock_first_run):
"""launch fails gracefully when entry point not on PATH."""
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["launch", "jimeng"])
assert result.exit_code == 1
assert "cli-hub install jimeng" 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.detect_invocation_context")
@patch("cli_hub.cli.get_cli", return_value=None)
def test_launch_unknown_cli(self, mock_get, mock_detect, mock_visit, mock_first_run):
"""launch with an unknown CLI name exits with error."""
mock_detect.return_value = self.human_detection
result = self.runner.invoke(main, ["launch", "nonexistent"])
assert result.exit_code == 1
assert "not found" in result.output

View File

@@ -0,0 +1,37 @@
{
"site": "clianything.cc",
"provider": "posthog",
"generated_at": "2026-04-23T15:31:59.837617+00:00",
"migration_started_at": "2026-04-23T09:06:38Z",
"baseline": {
"provider": "umami",
"site": "clianything.cc",
"scope": "clianything.cc only",
"queried_at": "2026-04-23T09:06:38.505000+00:00",
"total": 21123,
"human": 7689,
"agent": 13434,
"cli_hub_total": 16361,
"cli_hub_human": 3237,
"cli_hub_agent": 13124
},
"posthog_increment": {
"status": "ok",
"app_host": "https://us.posthog.com",
"project_id": "393992",
"human": 3,
"agent": 1,
"total": 4,
"cli_hub_human": 0,
"cli_hub_agent": 2,
"cli_hub_total": 2
},
"totals": {
"total": 21127,
"human": 7692,
"agent": 13435,
"cli_hub_total": 16363,
"cli_hub_human": 3237,
"cli_hub_agent": 13126
}
}

View File

@@ -1487,8 +1487,8 @@
backdrop-filter: blur(18px);
box-shadow: var(--shadow-md);
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.8fr);
gap: 0.9rem;
grid-template-columns: 1fr;
gap: 0.8rem;
}
.footer-band,
@@ -1538,45 +1538,89 @@
flex-direction: column;
justify-content: center;
gap: 0.55rem;
width: min(760px, 100%);
margin: 0 auto;
}
.footer-analytics-link {
.footer-analytics-board {
border-radius: 20px;
border: 1px solid var(--border);
background: rgba(255,255,255,0.03);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
padding: 0.34rem 0.95rem;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
}
.analytics-row {
display: grid;
grid-template-columns: minmax(0, 168px) repeat(3, minmax(0, 1fr));
align-items: end;
gap: 0.7rem;
padding: 0.74rem 0;
}
.analytics-row + .analytics-row {
border-top: 1px solid var(--border);
}
.analytics-title {
font-size: 0.93rem;
font-weight: 600;
color: var(--text);
letter-spacing: -0.02em;
line-height: 1.2;
}
.metric-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.18rem;
min-width: 0;
}
.metric-label {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 0.55rem;
color: var(--text-secondary);
font-size: 0.9rem;
}
.footer-analytics-link svg {
width: 15px;
height: 15px;
gap: 0.34rem;
min-width: 0;
font-size: 0.69rem;
color: var(--text-tertiary);
letter-spacing: 0.01em;
white-space: nowrap;
}
.stat-dot {
width: 0.45rem;
height: 0.45rem;
.metric-dot {
width: 0.42rem;
height: 0.42rem;
border-radius: 999px;
display: inline-block;
flex-shrink: 0;
background: rgba(129, 144, 171, 0.55);
}
.dot-total {
background: var(--text);
.metric-value {
font-size: 1.05rem;
line-height: 1;
color: var(--text);
font-variant-numeric: tabular-nums;
letter-spacing: -0.03em;
white-space: nowrap;
}
.dot-human {
.metric-cell.total .metric-value {
font-weight: 700;
}
.metric-cell.human .metric-dot {
background: var(--green);
}
.dot-agent {
background: var(--amber);
.metric-cell.agent .metric-dot {
background: var(--accent);
}
.analytics-sep {
color: var(--text-tertiary);
.metric-cell.total .metric-label {
color: var(--text-secondary);
}
.sr-only {
@@ -1694,6 +1738,15 @@
.grid {
grid-template-columns: 1fr;
}
.analytics-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem 0.75rem;
}
.analytics-title {
grid-column: 1 / -1;
}
}
@media (max-width: 640px) {
@@ -1767,18 +1820,24 @@
padding-left: 0.9rem;
padding-right: 0.9rem;
}
.footer-analytics-board {
padding: 0.3rem 0.78rem;
}
.analytics-row {
padding: 0.58rem 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.analytics-title {
grid-column: 1 / -1;
}
}
</style>
<!-- Umami Analytics — hkuds.github.io -->
<script defer src="https://cloud.umami.is/script.js" data-website-id="07082d05-efd3-4f85-a7a1-b426b0e8bfaa"></script>
<!-- Umami Analytics — clianything.cc -->
<script defer src="https://cloud.umami.is/script.js" data-website-id="a076c661-bed1-405c-a522-813794e688b4"></script>
<!-- Analytics provider is configured in the footer script. -->
</head>
<body>
<noscript>
<img src="https://cloud.umami.is/api/send?type=event&amp;payload=%7B%22website%22%3A%2207082d05-efd3-4f85-a7a1-b426b0e8bfaa%22%2C%22name%22%3A%22noscript-visit%22%7D" style="position:absolute;left:-9999px" alt="" width="1" height="1">
</noscript>
<div class="page-shell">
<nav class="nav">
<a class="nav-brand" href="#">
@@ -2033,16 +2092,37 @@
</div>
<div class="footer-stats">
<div class="footer-label">Traffic Snapshot</div>
<div class="footer-analytics-link" aria-label="Traffic snapshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<span class="stat-dot dot-total"></span>
<span id="stat-total">&mdash;</span> visits
<span class="analytics-sep">&middot;</span>
<span class="stat-dot dot-human"></span>
<span id="stat-human">&mdash;</span> human
<span class="analytics-sep">&middot;</span>
<span class="stat-dot dot-agent"></span>
<span id="stat-agent">&mdash;</span> AI agent
<div class="footer-analytics-board" aria-label="Traffic snapshot">
<div class="analytics-row">
<div class="analytics-title">Website Visits</div>
<div class="metric-cell total">
<span class="metric-label">Total</span>
<strong id="stat-total" class="metric-value">21,123</strong>
</div>
<div class="metric-cell human">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Human</span>
<strong id="stat-human" class="metric-value">7,689</strong>
</div>
<div class="metric-cell agent">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Agent</span>
<strong id="stat-agent" class="metric-value">13,434</strong>
</div>
</div>
<div class="analytics-row">
<div class="analytics-title">CLI-Hub Calls</div>
<div class="metric-cell total">
<span class="metric-label">Total</span>
<strong id="stat-cli-total" class="metric-value">16,361</strong>
</div>
<div class="metric-cell human">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Human</span>
<strong id="stat-cli-human" class="metric-value">3,237</strong>
</div>
<div class="metric-cell agent">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Agent</span>
<strong id="stat-cli-agent" class="metric-value">13,124</strong>
</div>
</div>
</div>
</div>
</footer>
@@ -2394,7 +2474,7 @@
return '<div class="card-command-block">' +
'<div class="card-command-label"><span>Install Commands</span><span>' + esc(sourceLabel) + '</span></div>' +
'<div class="card-command">' + display + '</div>' +
'<div class="card-command-actions"><button class="card-copy-btn" onclick=\'copyCmd(this, ' + JSON.stringify(copyText) + ')'>Copy</button></div>' +
'<div class="card-command-actions"><button class="card-copy-btn" onclick=\'copyCmd(this, ' + JSON.stringify(copyText) + ')\'>Copy</button></div>' +
'</div>';
}
@@ -2550,67 +2630,276 @@
loadRegistries().then(updateDeckPosition);
(function() {
const AI_UA_PATTERNS = [
/claude/i, /anthropic/i, /openai/i, /gpt/i, /chatgpt/i,
/bingbot/i, /googlebot/i, /baiduspider/i, /yandexbot/i,
/slurp/i, /duckduckbot/i, /facebookexternalhit/i,
/twitterbot/i, /linkedinbot/i, /whatsapp/i, /telegrambot/i,
/applebot/i, /semrushbot/i, /ahrefsbot/i, /dotbot/i,
/petalbot/i, /mj12bot/i, /bytespider/i,
/headless/i, /phantom/i, /selenium/i, /puppeteer/i, /playwright/i,
/crawl/i, /spider/i, /bot\\b/i, /scraper/i,
/python-requests/i, /axios/i, /node-fetch/i, /go-http/i, /curl/i, /wget/i,
/ccbot/i, /gptbot/i, /perplexity/i, /cohere/i,
const ANALYTICS_PROVIDER = 'posthog';
const ANALYTICS_SITE = 'clianything.cc';
const COUNTED_HOSTS = new Set(['clianything.cc', 'www.clianything.cc']);
const BASELINE_STATS = {
total: 21123,
human: 7689,
agent: 13434,
cli_hub_total: 16361,
cli_hub_human: 3237,
cli_hub_agent: 13124,
};
const ANALYTICS_TEST_RUN = new URLSearchParams(location.search).get('analytics_test_run') || '';
const POSTHOG_CONFIG = {
projectToken: 'phc_ovP8d5bmjpn8YZnTo7pb6rE3TikcAMgmNVt75o3Ywejz',
apiHost: 'https://us.i.posthog.com',
defaults: '2026-01-30',
personProfiles: 'identified_only',
};
const UMAMI_CONFIG = {
sendUrl: 'https://cloud.umami.is/api/send',
websiteId: 'a076c661-bed1-405c-a522-813794e688b4',
};
const OFFICIAL_AGENT_UA_RULES = [
{ id: 'openai-oai-searchbot', category: 'official_ai_agent', regex: /\bOAI-SearchBot\b/i },
{ id: 'openai-gptbot', category: 'official_ai_agent', regex: /\bGPTBot\b/i },
{ id: 'openai-chatgpt-user', category: 'official_ai_agent', regex: /\bChatGPT-User\b/i },
{ id: 'anthropic-claudebot', category: 'official_ai_agent', regex: /\bClaudeBot\b/i },
{ id: 'anthropic-claude-user', category: 'official_ai_agent', regex: /\bClaude-User\b/i },
{ id: 'anthropic-claude-searchbot', category: 'official_ai_agent', regex: /\bClaude-SearchBot\b/i },
{ id: 'perplexity-bot', category: 'official_ai_agent', regex: /\bPerplexityBot\b/i },
{ id: 'perplexity-user', category: 'official_ai_agent', regex: /\bPerplexity-User\b/i },
{ id: 'google-vertex-bot', category: 'official_ai_agent', regex: /\bGoogle-CloudVertexBot\b/i },
];
const ua = navigator.userAgent || '';
const isKnownAgent = AI_UA_PATTERNS.some((p) => p.test(ua));
const isWebdriver = navigator.webdriver === true;
const AGENT_TOOL_UA_RULES = [
{ id: 'claude-code', category: 'agent_tool', regex: /\bclaude(?:[ -]?code)\b/i },
{ id: 'codex', category: 'agent_tool', regex: /\bcodex\b/i },
{ id: 'cursor', category: 'agent_tool', regex: /\bcursor\b/i },
{ id: 'cline', category: 'agent_tool', regex: /\bcline\b/i },
{ id: 'aider', category: 'agent_tool', regex: /\baider\b/i },
{ id: 'continue', category: 'agent_tool', regex: /\bcontinue\b/i },
{ id: 'openhands', category: 'agent_tool', regex: /\bopenhands\b/i },
{ id: 'openclaw', category: 'agent_tool', regex: /\bopenclaw\b/i },
{ id: 'nanobot', category: 'agent_tool', regex: /\bnanobot\b/i },
{ id: 'browser-use', category: 'agent_tool', regex: /\bbrowser[- ]use\b/i },
{ id: 'stagehand', category: 'agent_tool', regex: /\bstagehand\b/i },
];
const AUTOMATION_UA_RULES = [
{ id: 'headless-browser', category: 'automation_runtime', regex: /\bHeadlessChrome\b|\bHeadless\b/i },
{ id: 'playwright', category: 'automation_runtime', regex: /\bPlaywright\b/i },
{ id: 'puppeteer', category: 'automation_runtime', regex: /\bPuppeteer\b/i },
{ id: 'selenium', category: 'automation_runtime', regex: /\bSelenium\b/i },
{ id: 'phantomjs', category: 'automation_runtime', regex: /\bPhantomJS\b|\bPhantom\b/i },
];
const SCRIPTED_CLIENT_UA_RULES = [
{ id: 'python-requests', category: 'scripted_client', regex: /\bpython-requests\b/i },
{ id: 'python-httpx', category: 'scripted_client', regex: /\bpython-httpx\b/i },
{ id: 'axios', category: 'scripted_client', regex: /\baxios\b/i },
{ id: 'node-fetch', category: 'scripted_client', regex: /\bnode-fetch\b/i },
{ id: 'undici', category: 'scripted_client', regex: /\bundici\b/i },
{ id: 'go-http-client', category: 'scripted_client', regex: /\bGo-http-client\b/i },
{ id: 'curl', category: 'scripted_client', regex: /\bcurl\b/i },
{ id: 'wget', category: 'scripted_client', regex: /\bwget\b/i },
];
const CATEGORY_PRIORITY = ['official_ai_agent', 'agent_tool', 'automation_runtime', 'scripted_client'];
let humanConfirmed = false;
let posthogReady = false;
const pendingPosthogEvents = [];
const UMAMI_SEND = 'https://cloud.umami.is/api/send';
const DUAL_WEBSITE_IDS = [
'07082d05-efd3-4f85-a7a1-b426b0e8bfaa',
'a076c661-bed1-405c-a522-813794e688b4',
];
function isTrackedHost() {
return COUNTED_HOSTS.has(location.hostname);
}
function trackBoth(eventName, eventData) {
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(() => {});
function applyVisitorStats(stats) {
document.getElementById('stat-total').textContent = (stats.total || 0).toLocaleString();
document.getElementById('stat-human').textContent = (stats.human || 0).toLocaleString();
document.getElementById('stat-agent').textContent = (stats.agent || 0).toLocaleString();
document.getElementById('stat-cli-total').textContent = (stats.cli_hub_total || 0).toLocaleString();
document.getElementById('stat-cli-human').textContent = (stats.cli_hub_human || 0).toLocaleString();
document.getElementById('stat-cli-agent').textContent = (stats.cli_hub_agent || 0).toLocaleString();
}
function readUaBrands() {
const brands = navigator.userAgentData && Array.isArray(navigator.userAgentData.brands)
? navigator.userAgentData.brands
: [];
return brands.map((brand) => brand.brand).join(' ');
}
function collectRuleSignals(ua, rules, signals) {
rules.forEach((rule) => {
if (rule.regex.test(ua)) {
signals.push(rule);
}
});
}
if (isKnownAgent || isWebdriver) {
trackBoth('visit-agent', { ua: ua.slice(0, 200) });
function collectRuntimeSignals(brands, signals) {
if (navigator.webdriver === true) {
signals.push({ id: 'webdriver', category: 'automation_runtime' });
}
if (/HeadlessChrome/i.test(brands)) {
signals.push({ id: 'ua-brand-headlesschrome', category: 'automation_runtime' });
}
if ('__playwright__binding__' in window || '__pwManual' in window) {
signals.push({ id: 'playwright-runtime', category: 'automation_runtime' });
}
if ('Cypress' in window) {
signals.push({ id: 'cypress-runtime', category: 'automation_runtime' });
}
if ('_phantom' in window || 'callPhantom' in window) {
signals.push({ id: 'phantom-runtime', category: 'automation_runtime' });
}
if ('domAutomation' in window || 'domAutomationController' in window) {
signals.push({ id: 'dom-automation', category: 'automation_runtime' });
}
}
window.addEventListener('load', () => {
setTimeout(() => {
if (isKnownAgent || isWebdriver) {
trackBoth('visit-agent', { ua: ua.slice(0, 200) });
}
}, 500);
});
function classifyVisitor() {
const ua = navigator.userAgent || '';
const brands = readUaBrands();
const signals = [];
collectRuleSignals(ua, OFFICIAL_AGENT_UA_RULES, signals);
collectRuleSignals(ua, AGENT_TOOL_UA_RULES, signals);
collectRuleSignals(ua, AUTOMATION_UA_RULES, signals);
collectRuleSignals(ua, SCRIPTED_CLIENT_UA_RULES, signals);
collectRuntimeSignals(brands, signals);
function onHumanInteraction() {
if (humanConfirmed) return;
const seen = new Set();
const uniqueSignals = signals.filter((signal) => {
if (seen.has(signal.id)) return false;
seen.add(signal.id);
return true;
}).sort((left, right) => CATEGORY_PRIORITY.indexOf(left.category) - CATEGORY_PRIORITY.indexOf(right.category));
const primary = uniqueSignals[0] || null;
const isAgent = uniqueSignals.length > 0;
return {
ua,
brands,
webdriver: navigator.webdriver === true,
isAgent,
isAutomation: !!primary && (primary.category === 'automation_runtime' || primary.category === 'scripted_client'),
trafficType: isAgent ? 'agent' : 'human',
reason: primary ? primary.id : 'human',
category: primary ? primary.category : 'human',
signals: uniqueSignals.map((signal) => signal.id),
};
}
const visitor = classifyVisitor();
function analyticsContext(eventData) {
const base = {
source: 'web',
site: ANALYTICS_SITE,
host: location.hostname,
visitor_type: visitor.trafficType,
is_agent: visitor.isAgent,
is_automation: visitor.isAutomation,
agent_category: visitor.category,
agent_reason: visitor.reason,
agent_signals: visitor.signals.slice(0, 12),
ua: visitor.ua.slice(0, 200),
ua_brands: visitor.brands.slice(0, 200),
webdriver: visitor.webdriver,
};
if (ANALYTICS_TEST_RUN) {
base.test_run = ANALYTICS_TEST_RUN;
}
return { ...base, ...eventData };
}
function initPosthog() {
if (!isTrackedHost()) return;
!function(t,e){var o,n,p,r;e.__SV||(window.posthog&&window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split('.');2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement('script')).type='text/javascript',p.crossOrigin='anonymous',p.async=!0,p.src=s.api_host.replace('.i.posthog.com','-assets.i.posthog.com')+'/static/array.js',(r=t.getElementsByTagName('script')[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a='posthog',u.people=u.people||[],u.toString=function(t){var e='posthog';return'posthog'!==a&&(e+='.'+a),t||(e+=' (stub)'),e},u.people.toString=function(){return u.toString(1)+'.people (stub)'},o='init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug'.split(' '),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
window.posthog.init(POSTHOG_CONFIG.projectToken, {
api_host: POSTHOG_CONFIG.apiHost,
defaults: POSTHOG_CONFIG.defaults,
person_profiles: POSTHOG_CONFIG.personProfiles,
opt_out_useragent_filter: true,
loaded: function(posthog) {
posthogReady = true;
while (pendingPosthogEvents.length) {
const queued = pendingPosthogEvents.shift();
posthog.capture(queued.eventName, analyticsContext(queued.eventData));
}
},
});
}
function initUmami() {
if (!isTrackedHost()) return;
const script = document.createElement('script');
script.defer = true;
script.src = 'https://cloud.umami.is/script.js';
script.dataset.websiteId = UMAMI_CONFIG.websiteId;
document.head.appendChild(script);
}
function trackWithPosthog(eventName, eventData) {
if (!isTrackedHost() || !window.posthog) return;
if (!posthogReady || typeof window.posthog.capture !== 'function') {
pendingPosthogEvents.push({ eventName, eventData });
return;
}
window.posthog.capture(eventName, analyticsContext(eventData));
}
function trackWithUmami(eventName, eventData) {
if (!isTrackedHost()) return;
const payload = {
type: 'event',
payload: {
website: UMAMI_CONFIG.websiteId,
hostname: ANALYTICS_SITE,
url: location.pathname,
name: eventName,
data: analyticsContext(eventData),
},
};
fetch(UMAMI_CONFIG.sendUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true,
}).catch(() => {});
}
function trackAnalyticsEvent(eventName, eventData) {
if (ANALYTICS_PROVIDER === 'umami') {
trackWithUmami(eventName, eventData);
return;
}
trackWithPosthog(eventName, eventData);
}
if (ANALYTICS_PROVIDER === 'umami') {
initUmami();
} else {
initPosthog();
}
if (ANALYTICS_TEST_RUN) {
window.__CLI_ANYTHING_ANALYTICS__ = {
visitor: {
isAgent: visitor.isAgent,
isAutomation: visitor.isAutomation,
trafficType: visitor.trafficType,
category: visitor.category,
reason: visitor.reason,
signals: visitor.signals.slice(),
webdriver: visitor.webdriver,
ua: visitor.ua,
},
};
}
if (isTrackedHost() && visitor.isAgent) {
trackAnalyticsEvent('visit-agent', {
classification_source: 'initial-load',
});
}
function onHumanInteraction(event) {
if (humanConfirmed || !isTrackedHost() || visitor.trafficType !== 'human') return;
humanConfirmed = true;
trackBoth('visit-human');
trackAnalyticsEvent('visit-human', {
classification_source: 'user-interaction',
interaction: event.type,
});
['mousemove', 'touchstart', 'scroll', 'keydown', 'click'].forEach((evt) => {
document.removeEventListener(evt, onHumanInteraction);
});
@@ -2620,49 +2909,24 @@
document.addEventListener(evt, onHumanInteraction, { once: false, passive: true });
});
const UMAMI_API = 'https://api.umami.is/v1';
const UMAMI_KEY = 'api_idAebMhzn6z0hsUQT7BSxRuCK2GUZvRY';
const HKUDS_WEBSITE_ID = '07082d05-efd3-4f85-a7a1-b426b0e8bfaa';
const CC_WEBSITE_ID = 'a076c661-bed1-405c-a522-813794e688b4';
const headers = { Accept: 'application/json', 'x-umami-api-key': UMAMI_KEY };
async function loadVisitorStats() {
applyVisitorStats(BASELINE_STATS);
try {
const now = Date.now();
let hkudsHumanVisits = 0;
let ccHumanVisits = 0;
let ccAgentVisits = 0;
const [hkudsStatsResp, ccEventsResp] = await Promise.all([
fetch(UMAMI_API + '/websites/' + HKUDS_WEBSITE_ID + '/stats?startAt=0&endAt=' + now, { headers }),
fetch(UMAMI_API + '/websites/' + CC_WEBSITE_ID + '/events/series?startAt=0&endAt=' + now + '&unit=year&timezone=UTC', { headers })
]);
if (hkudsStatsResp.ok) {
const stats = await hkudsStatsResp.json();
hkudsHumanVisits = stats.visits ?? 0;
const response = await fetch('./analytics-stats.json', { cache: 'no-store' });
if (!response.ok) return;
const stats = await response.json();
if (stats && stats.totals) {
applyVisitorStats(stats.totals);
}
if (ccEventsResp.ok) {
const events = await ccEventsResp.json();
events.forEach((e) => {
if (e.x === 'visit-human') ccHumanVisits += e.y || 0;
if (e.x === 'visit-agent') ccAgentVisits += e.y || 0;
});
}
const humanCount = hkudsHumanVisits + ccHumanVisits;
const totalVisits = humanCount + ccAgentVisits;
document.getElementById('stat-total').textContent = totalVisits.toLocaleString();
document.getElementById('stat-human').textContent = humanCount.toLocaleString();
document.getElementById('stat-agent').textContent = ccAgentVisits.toLocaleString();
} catch (_) {
// Leave fallback dashes.
// Fall back to the fixed baseline values.
}
}
loadVisitorStats();
loadGitHubStars();
if (typeof loadGitHubStars === 'function') {
loadGitHubStars();
}
})();
</script>
</body>

View File

@@ -1578,47 +1578,199 @@
width: 100%;
display: flex;
justify-content: center;
gap: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border-subtle);
margin-top: 0.5rem;
padding-top: 1.1rem;
margin-top: 0.35rem;
}
.footer-analytics-link {
.analytics-report {
width: min(780px, 100%);
padding: 0.95rem 1.2rem 0.9rem;
border: 1px solid var(--border-subtle);
border-radius: 14px;
background: linear-gradient(180deg, rgba(255,255,255,0.018), rgba(255,255,255,0));
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, sans-serif;
transition: border-color 0.2s ease, background 0.2s ease;
}
:root[data-theme="light"] .analytics-report {
background: linear-gradient(180deg, rgba(9,9,11,0.012), rgba(9,9,11,0));
}
.analytics-report:hover {
border-color: var(--border);
}
.analytics-report-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.analytics-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.78rem;
gap: 0.5rem;
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-tertiary);
padding: 0.35rem 0.75rem;
border: 1px solid var(--border);
border-radius: 999px;
}
.footer-analytics-link svg { opacity: 0.6; flex-shrink: 0; }
.analytics-eyebrow::before {
content: "";
width: 0.4rem;
height: 0.4rem;
border-radius: 999px;
background: var(--green);
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
animation: analyticsPulse 2.4s ease-in-out infinite;
}
.analytics-badge {
@keyframes analyticsPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
.analytics-legend {
display: inline-flex;
align-items: center;
gap: 0.3rem;
gap: 1rem;
font-size: 0.7rem;
color: var(--text-tertiary);
margin-left: 0.15rem;
letter-spacing: 0.01em;
}
.analytics-sep { opacity: 0.4; }
.analytics-legend-item {
display: inline-flex;
align-items: center;
gap: 0.42rem;
}
.stat-dot {
width: 6px;
height: 6px;
border-radius: 50%;
.analytics-dot {
width: 0.42rem;
height: 0.42rem;
border-radius: 999px;
background: var(--text-tertiary);
flex-shrink: 0;
display: inline-block;
}
.dot-total { background: var(--text-tertiary); }
.dot-human { background: var(--green); }
.dot-agent { background: var(--accent); }
.analytics-dot.human { background: var(--green); }
.analytics-dot.agent { background: var(--accent); }
.analytics-report-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.5rem;
}
.analytics-stat {
display: flex;
flex-direction: column;
gap: 0.42rem;
min-width: 0;
}
.analytics-stat + .analytics-stat {
border-left: 1px solid var(--border-subtle);
padding-left: 1.5rem;
}
.analytics-stat-label {
font-size: 0.7rem;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0;
line-height: 1;
}
.analytics-stat-value {
display: block;
font-size: 1.55rem;
font-weight: 600;
line-height: 1;
color: var(--text);
font-variant-numeric: tabular-nums;
letter-spacing: -0.04em;
white-space: nowrap;
}
.analytics-bar {
position: relative;
display: flex;
width: 100%;
height: 2px;
border-radius: 999px;
background: var(--border-subtle);
overflow: hidden;
}
.analytics-bar > span {
display: block;
height: 100%;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.analytics-bar .bar-human {
background: var(--green);
}
.analytics-bar .bar-agent {
background: var(--accent);
}
.analytics-split {
display: flex;
flex-wrap: wrap;
gap: 0.3rem 1.1rem;
margin: 0;
}
.analytics-split-item {
display: inline-flex;
align-items: baseline;
gap: 0.42rem;
min-width: 0;
}
.analytics-split-item dt {
display: inline-flex;
align-items: center;
gap: 0.34rem;
font-size: 0.68rem;
color: var(--text-tertiary);
letter-spacing: 0.01em;
line-height: 1;
margin: 0;
}
.analytics-split-item dd {
display: inline-flex;
align-items: baseline;
gap: 0.34rem;
margin: 0;
min-width: 0;
}
.analytics-split-num {
font-size: 0.82rem;
font-weight: 500;
color: var(--text);
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
line-height: 1;
white-space: nowrap;
}
.analytics-split-pct {
font-size: 0.68rem;
color: var(--text-tertiary);
font-variant-numeric: tabular-nums;
letter-spacing: 0;
line-height: 1;
}
/* ── Responsive ── */
@media (max-width: 640px) {
@@ -1681,6 +1833,25 @@
.empower-self .cmd-grid { grid-template-columns: 1fr; }
.deck-nav { padding: 1.25rem 1.25rem 0; }
.deck-arrow-btn span { display: none; }
.analytics-report {
padding: 0.85rem 1rem 0.8rem;
border-radius: 12px;
}
.analytics-report-head {
gap: 0.75rem;
margin-bottom: 0.65rem;
}
.analytics-report-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.analytics-stat + .analytics-stat {
border-left: none;
border-top: 1px solid var(--border-subtle);
padding-left: 0;
padding-top: 0.75rem;
}
.analytics-stat-value { font-size: 1.4rem; }
}
@media (max-width: 1120px) {
@@ -1711,16 +1882,9 @@
}
</style>
<script>document.documentElement.classList.add('js');</script>
<!-- Umami Analytics — hkuds.github.io -->
<script defer src="https://cloud.umami.is/script.js" data-website-id="07082d05-efd3-4f85-a7a1-b426b0e8bfaa"></script>
<!-- Umami Analytics — clianything.cc -->
<script defer src="https://cloud.umami.is/script.js" data-website-id="a076c661-bed1-405c-a522-813794e688b4"></script>
<!-- Analytics provider is configured in the footer script. -->
</head>
<body>
<!-- noscript pixel: catches non-JS visitors (AI crawlers) -->
<noscript>
<img src="https://cloud.umami.is/api/send?type=event&payload=%7B%22website%22%3A%2207082d05-efd3-4f85-a7a1-b426b0e8bfaa%22%2C%22name%22%3A%22noscript-visit%22%7D" style="position:absolute;left:-9999px" alt="" width="1" height="1">
</noscript>
<!-- Nav -->
<nav class="nav">
<a class="nav-brand" href="#">CLI-Hub</a>
@@ -1912,17 +2076,53 @@
Powered by <a href="https://github.com/HKUDS/CLI-Anything" target="_blank">CLI-Anything</a>
</div>
<div class="footer-stats">
<div class="footer-analytics-link" aria-label="Traffic snapshot">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<span class="stat-dot dot-total"></span>
<span id="stat-total">&mdash;</span> visits
<span class="analytics-sep">&middot;</span>
<span class="stat-dot dot-human"></span>
<span id="stat-human">&mdash;</span> human
<span class="analytics-sep">&middot;</span>
<span class="stat-dot dot-agent"></span>
<span id="stat-agent">&mdash;</span> AI agent
</div>
<section class="analytics-report" aria-label="Traffic snapshot">
<header class="analytics-report-head">
<span class="analytics-eyebrow">Live traffic</span>
<span class="analytics-legend" aria-hidden="true">
<span class="analytics-legend-item"><span class="analytics-dot human"></span>Human</span>
<span class="analytics-legend-item"><span class="analytics-dot agent"></span>Agent</span>
</span>
</header>
<div class="analytics-report-grid">
<article class="analytics-stat">
<span class="analytics-stat-label">Website visits</span>
<strong id="stat-total" class="analytics-stat-value">21,123</strong>
<div class="analytics-bar" aria-hidden="true">
<span class="bar-human" id="bar-site-human" style="width: 36.4%"></span>
<span class="bar-agent" id="bar-site-agent" style="width: 63.6%"></span>
</div>
<dl class="analytics-split">
<div class="analytics-split-item">
<dt><span class="analytics-dot human"></span>Human</dt>
<dd><span id="stat-human" class="analytics-split-num">7,689</span><span id="pct-site-human" class="analytics-split-pct">36%</span></dd>
</div>
<div class="analytics-split-item">
<dt><span class="analytics-dot agent"></span>Agent</dt>
<dd><span id="stat-agent" class="analytics-split-num">13,434</span><span id="pct-site-agent" class="analytics-split-pct">64%</span></dd>
</div>
</dl>
</article>
<article class="analytics-stat">
<span class="analytics-stat-label">CLI-Hub calls</span>
<strong id="stat-cli-total" class="analytics-stat-value">16,361</strong>
<div class="analytics-bar" aria-hidden="true">
<span class="bar-human" id="bar-cli-human" style="width: 19.8%"></span>
<span class="bar-agent" id="bar-cli-agent" style="width: 80.2%"></span>
</div>
<dl class="analytics-split">
<div class="analytics-split-item">
<dt><span class="analytics-dot human"></span>Human</dt>
<dd><span id="stat-cli-human" class="analytics-split-num">3,237</span><span id="pct-cli-human" class="analytics-split-pct">20%</span></dd>
</div>
<div class="analytics-split-item">
<dt><span class="analytics-dot agent"></span>Agent</dt>
<dd><span id="stat-cli-agent" class="analytics-split-num">13,124</span><span id="pct-cli-agent" class="analytics-split-pct">80%</span></dd>
</div>
</dl>
</article>
</div>
</section>
</div>
</footer>
@@ -2624,129 +2824,323 @@
// ── Visitor classification: human vs AI agent ──
(function() {
const AI_UA_PATTERNS = [
/claude/i, /anthropic/i, /openai/i, /gpt/i, /chatgpt/i,
/bingbot/i, /googlebot/i, /baiduspider/i, /yandexbot/i,
/slurp/i, /duckduckbot/i, /facebookexternalhit/i,
/twitterbot/i, /linkedinbot/i, /whatsapp/i, /telegrambot/i,
/applebot/i, /semrushbot/i, /ahrefsbot/i, /dotbot/i,
/petalbot/i, /mj12bot/i, /bytespider/i,
/headless/i, /phantom/i, /selenium/i, /puppeteer/i, /playwright/i,
/crawl/i, /spider/i, /bot\b/i, /scraper/i,
/python-requests/i, /axios/i, /node-fetch/i, /go-http/i, /curl/i, /wget/i,
/ccbot/i, /gptbot/i, /perplexity/i, /cohere/i,
const ANALYTICS_PROVIDER = 'posthog';
const ANALYTICS_SITE = 'clianything.cc';
const COUNTED_HOSTS = new Set(['clianything.cc', 'www.clianything.cc']);
const BASELINE_STATS = {
total: 21123,
human: 7689,
agent: 13434,
cli_hub_total: 16361,
cli_hub_human: 3237,
cli_hub_agent: 13124,
};
const ANALYTICS_TEST_RUN = new URLSearchParams(location.search).get('analytics_test_run') || '';
const POSTHOG_CONFIG = {
projectToken: 'phc_ovP8d5bmjpn8YZnTo7pb6rE3TikcAMgmNVt75o3Ywejz',
apiHost: 'https://us.i.posthog.com',
defaults: '2026-01-30',
personProfiles: 'identified_only',
};
const UMAMI_CONFIG = {
sendUrl: 'https://cloud.umami.is/api/send',
websiteId: 'a076c661-bed1-405c-a522-813794e688b4',
};
const OFFICIAL_AGENT_UA_RULES = [
{ id: 'openai-oai-searchbot', category: 'official_ai_agent', regex: /\bOAI-SearchBot\b/i },
{ id: 'openai-gptbot', category: 'official_ai_agent', regex: /\bGPTBot\b/i },
{ id: 'openai-chatgpt-user', category: 'official_ai_agent', regex: /\bChatGPT-User\b/i },
{ id: 'anthropic-claudebot', category: 'official_ai_agent', regex: /\bClaudeBot\b/i },
{ id: 'anthropic-claude-user', category: 'official_ai_agent', regex: /\bClaude-User\b/i },
{ id: 'anthropic-claude-searchbot', category: 'official_ai_agent', regex: /\bClaude-SearchBot\b/i },
{ id: 'perplexity-bot', category: 'official_ai_agent', regex: /\bPerplexityBot\b/i },
{ id: 'perplexity-user', category: 'official_ai_agent', regex: /\bPerplexity-User\b/i },
{ id: 'google-vertex-bot', category: 'official_ai_agent', regex: /\bGoogle-CloudVertexBot\b/i },
];
const ua = navigator.userAgent || '';
const isKnownAgent = AI_UA_PATTERNS.some(p => p.test(ua));
const isWebdriver = navigator.webdriver === true;
const AGENT_TOOL_UA_RULES = [
{ id: 'claude-code', category: 'agent_tool', regex: /\bclaude(?:[ -]?code)\b/i },
{ id: 'codex', category: 'agent_tool', regex: /\bcodex\b/i },
{ id: 'cursor', category: 'agent_tool', regex: /\bcursor\b/i },
{ id: 'cline', category: 'agent_tool', regex: /\bcline\b/i },
{ id: 'aider', category: 'agent_tool', regex: /\baider\b/i },
{ id: 'continue', category: 'agent_tool', regex: /\bcontinue\b/i },
{ id: 'openhands', category: 'agent_tool', regex: /\bopenhands\b/i },
{ id: 'openclaw', category: 'agent_tool', regex: /\bopenclaw\b/i },
{ id: 'nanobot', category: 'agent_tool', regex: /\bnanobot\b/i },
{ id: 'browser-use', category: 'agent_tool', regex: /\bbrowser[- ]use\b/i },
{ id: 'stagehand', category: 'agent_tool', regex: /\bstagehand\b/i },
];
const AUTOMATION_UA_RULES = [
{ id: 'headless-browser', category: 'automation_runtime', regex: /\bHeadlessChrome\b|\bHeadless\b/i },
{ id: 'playwright', category: 'automation_runtime', regex: /\bPlaywright\b/i },
{ id: 'puppeteer', category: 'automation_runtime', regex: /\bPuppeteer\b/i },
{ id: 'selenium', category: 'automation_runtime', regex: /\bSelenium\b/i },
{ id: 'phantomjs', category: 'automation_runtime', regex: /\bPhantomJS\b|\bPhantom\b/i },
];
const SCRIPTED_CLIENT_UA_RULES = [
{ id: 'python-requests', category: 'scripted_client', regex: /\bpython-requests\b/i },
{ id: 'python-httpx', category: 'scripted_client', regex: /\bpython-httpx\b/i },
{ id: 'axios', category: 'scripted_client', regex: /\baxios\b/i },
{ id: 'node-fetch', category: 'scripted_client', regex: /\bnode-fetch\b/i },
{ id: 'undici', category: 'scripted_client', regex: /\bundici\b/i },
{ id: 'go-http-client', category: 'scripted_client', regex: /\bGo-http-client\b/i },
{ id: 'curl', category: 'scripted_client', regex: /\bcurl\b/i },
{ id: 'wget', category: 'scripted_client', regex: /\bwget\b/i },
];
const CATEGORY_PRIORITY = ['official_ai_agent', 'agent_tool', 'automation_runtime', 'scripted_client'];
let humanConfirmed = false;
let posthogReady = false;
const pendingPosthogEvents = [];
// ── 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(() => {});
function isTrackedHost() {
return COUNTED_HOSTS.has(location.hostname);
}
function applyVisitorStats(stats) {
const setText = (id, val) => {
const el = document.getElementById(id);
if (el) el.textContent = (val || 0).toLocaleString();
};
const setSplit = (prefix, human, agent) => {
const total = (human || 0) + (agent || 0);
const humanPct = total > 0 ? (human / total) * 100 : 0;
const agentPct = total > 0 ? 100 - humanPct : 0;
const fmtPct = (n) => (n >= 10 || n === 0 ? Math.round(n) : n.toFixed(1)) + '%';
const barH = document.getElementById('bar-' + prefix + '-human');
const barA = document.getElementById('bar-' + prefix + '-agent');
if (barH) barH.style.width = humanPct.toFixed(2) + '%';
if (barA) barA.style.width = agentPct.toFixed(2) + '%';
const pctH = document.getElementById('pct-' + prefix + '-human');
const pctA = document.getElementById('pct-' + prefix + '-agent');
if (pctH) pctH.textContent = fmtPct(humanPct);
if (pctA) pctA.textContent = fmtPct(agentPct);
};
setText('stat-total', stats.total);
setText('stat-human', stats.human);
setText('stat-agent', stats.agent);
setText('stat-cli-total', stats.cli_hub_total);
setText('stat-cli-human', stats.cli_hub_human);
setText('stat-cli-agent', stats.cli_hub_agent);
setSplit('site', stats.human || 0, stats.agent || 0);
setSplit('cli', stats.cli_hub_human || 0, stats.cli_hub_agent || 0);
}
function readUaBrands() {
const brands = navigator.userAgentData && Array.isArray(navigator.userAgentData.brands)
? navigator.userAgentData.brands
: [];
return brands.map((brand) => brand.brand).join(' ');
}
function collectRuleSignals(ua, rules, signals) {
rules.forEach((rule) => {
if (rule.regex.test(ua)) {
signals.push(rule);
}
});
}
// Tag the visit type via Umami custom events
if (isKnownAgent || isWebdriver) {
trackBoth('visit-agent', { ua: ua.slice(0, 200) });
function collectRuntimeSignals(brands, signals) {
if (navigator.webdriver === true) {
signals.push({ id: 'webdriver', category: 'automation_runtime' });
}
if (/HeadlessChrome/i.test(brands)) {
signals.push({ id: 'ua-brand-headlesschrome', category: 'automation_runtime' });
}
if ('__playwright__binding__' in window || '__pwManual' in window) {
signals.push({ id: 'playwright-runtime', category: 'automation_runtime' });
}
if ('Cypress' in window) {
signals.push({ id: 'cypress-runtime', category: 'automation_runtime' });
}
if ('_phantom' in window || 'callPhantom' in window) {
signals.push({ id: 'phantom-runtime', category: 'automation_runtime' });
}
if ('domAutomation' in window || 'domAutomationController' in window) {
signals.push({ id: 'dom-automation', category: 'automation_runtime' });
}
}
// Wait for Umami to load, then tag
window.addEventListener('load', () => {
setTimeout(() => {
if (isKnownAgent || isWebdriver) {
trackBoth('visit-agent', { ua: ua.slice(0, 200) });
}
}, 500);
});
function classifyVisitor() {
const ua = navigator.userAgent || '';
const brands = readUaBrands();
const signals = [];
collectRuleSignals(ua, OFFICIAL_AGENT_UA_RULES, signals);
collectRuleSignals(ua, AGENT_TOOL_UA_RULES, signals);
collectRuleSignals(ua, AUTOMATION_UA_RULES, signals);
collectRuleSignals(ua, SCRIPTED_CLIENT_UA_RULES, signals);
collectRuntimeSignals(brands, signals);
// Real human interaction detection
function onHumanInteraction() {
if (humanConfirmed) return;
const seen = new Set();
const uniqueSignals = signals.filter((signal) => {
if (seen.has(signal.id)) return false;
seen.add(signal.id);
return true;
}).sort((left, right) => CATEGORY_PRIORITY.indexOf(left.category) - CATEGORY_PRIORITY.indexOf(right.category));
const primary = uniqueSignals[0] || null;
const isAgent = uniqueSignals.length > 0;
return {
ua,
brands,
webdriver: navigator.webdriver === true,
isAgent,
isAutomation: !!primary && (primary.category === 'automation_runtime' || primary.category === 'scripted_client'),
trafficType: isAgent ? 'agent' : 'human',
reason: primary ? primary.id : 'human',
category: primary ? primary.category : 'human',
signals: uniqueSignals.map((signal) => signal.id),
};
}
const visitor = classifyVisitor();
function analyticsContext(eventData) {
const base = {
source: 'web',
site: ANALYTICS_SITE,
host: location.hostname,
visitor_type: visitor.trafficType,
is_agent: visitor.isAgent,
is_automation: visitor.isAutomation,
agent_category: visitor.category,
agent_reason: visitor.reason,
agent_signals: visitor.signals.slice(0, 12),
ua: visitor.ua.slice(0, 200),
ua_brands: visitor.brands.slice(0, 200),
webdriver: visitor.webdriver,
};
if (ANALYTICS_TEST_RUN) {
base.test_run = ANALYTICS_TEST_RUN;
}
return { ...base, ...eventData };
}
function initPosthog() {
if (!isTrackedHost()) return;
!function(t,e){var o,n,p,r;e.__SV||(window.posthog&&window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split('.');2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement('script')).type='text/javascript',p.crossOrigin='anonymous',p.async=!0,p.src=s.api_host.replace('.i.posthog.com','-assets.i.posthog.com')+'/static/array.js',(r=t.getElementsByTagName('script')[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a='posthog',u.people=u.people||[],u.toString=function(t){var e='posthog';return'posthog'!==a&&(e+='.'+a),t||(e+=' (stub)'),e},u.people.toString=function(){return u.toString(1)+'.people (stub)'},o='init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug'.split(' '),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
window.posthog.init(POSTHOG_CONFIG.projectToken, {
api_host: POSTHOG_CONFIG.apiHost,
defaults: POSTHOG_CONFIG.defaults,
person_profiles: POSTHOG_CONFIG.personProfiles,
opt_out_useragent_filter: true,
loaded: function(posthog) {
posthogReady = true;
while (pendingPosthogEvents.length) {
const queued = pendingPosthogEvents.shift();
posthog.capture(queued.eventName, analyticsContext(queued.eventData));
}
},
});
}
function initUmami() {
if (!isTrackedHost()) return;
const script = document.createElement('script');
script.defer = true;
script.src = 'https://cloud.umami.is/script.js';
script.dataset.websiteId = UMAMI_CONFIG.websiteId;
document.head.appendChild(script);
}
function trackWithPosthog(eventName, eventData) {
if (!isTrackedHost() || !window.posthog) return;
if (!posthogReady || typeof window.posthog.capture !== 'function') {
pendingPosthogEvents.push({ eventName, eventData });
return;
}
window.posthog.capture(eventName, analyticsContext(eventData));
}
function trackWithUmami(eventName, eventData) {
if (!isTrackedHost()) return;
const payload = {
type: 'event',
payload: {
website: UMAMI_CONFIG.websiteId,
hostname: ANALYTICS_SITE,
url: location.pathname,
name: eventName,
data: analyticsContext(eventData),
},
};
fetch(UMAMI_CONFIG.sendUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true,
}).catch(() => {});
}
function trackAnalyticsEvent(eventName, eventData) {
if (ANALYTICS_PROVIDER === 'umami') {
trackWithUmami(eventName, eventData);
return;
}
trackWithPosthog(eventName, eventData);
}
if (ANALYTICS_PROVIDER === 'umami') {
initUmami();
} else {
initPosthog();
}
if (ANALYTICS_TEST_RUN) {
window.__CLI_ANYTHING_ANALYTICS__ = {
visitor: {
isAgent: visitor.isAgent,
isAutomation: visitor.isAutomation,
trafficType: visitor.trafficType,
category: visitor.category,
reason: visitor.reason,
signals: visitor.signals.slice(),
webdriver: visitor.webdriver,
ua: visitor.ua,
},
};
}
if (isTrackedHost() && visitor.isAgent) {
trackAnalyticsEvent('visit-agent', {
classification_source: 'initial-load',
});
}
function onHumanInteraction(event) {
if (humanConfirmed || !isTrackedHost() || visitor.trafficType !== 'human') return;
humanConfirmed = true;
trackBoth('visit-human');
// Clean up listeners
['mousemove', 'touchstart', 'scroll', 'keydown', 'click'].forEach(evt => {
trackAnalyticsEvent('visit-human', {
classification_source: 'user-interaction',
interaction: event.type,
});
['mousemove', 'touchstart', 'scroll', 'keydown', 'click'].forEach((evt) => {
document.removeEventListener(evt, onHumanInteraction);
});
}
['mousemove', 'touchstart', 'scroll', 'keydown', 'click'].forEach(evt => {
['mousemove', 'touchstart', 'scroll', 'keydown', 'click'].forEach((evt) => {
document.addEventListener(evt, onHumanInteraction, { once: false, passive: true });
});
// ── Fetch all-time stats from Umami Cloud API (both sites) ──
const UMAMI_API = 'https://api.umami.is/v1';
const UMAMI_KEY = 'api_idAebMhzn6z0hsUQT7BSxRuCK2GUZvRY';
const HKUDS_WEBSITE_ID = '07082d05-efd3-4f85-a7a1-b426b0e8bfaa';
const CC_WEBSITE_ID = 'a076c661-bed1-405c-a522-813794e688b4';
const headers = { 'Accept': 'application/json', 'x-umami-api-key': UMAMI_KEY };
async function loadVisitorStats() {
applyVisitorStats(BASELINE_STATS);
try {
const now = Date.now();
let hkudsHumanVisits = 0;
let ccHumanVisits = 0;
let ccAgentVisits = 0;
const [hkudsStatsResp, ccEventsResp] = await Promise.all([
fetch(`${UMAMI_API}/websites/${HKUDS_WEBSITE_ID}/stats?startAt=0&endAt=${now}`, { headers }),
fetch(`${UMAMI_API}/websites/${CC_WEBSITE_ID}/events/series?startAt=0&endAt=${now}&unit=year&timezone=UTC`, { headers })
]);
if (hkudsStatsResp.ok) {
const stats = await hkudsStatsResp.json();
hkudsHumanVisits = stats.visits ?? 0;
const response = await fetch('./analytics-stats.json', { cache: 'no-store' });
if (!response.ok) return;
const stats = await response.json();
if (stats && stats.totals) {
applyVisitorStats(stats.totals);
}
if (ccEventsResp.ok) {
const events = await ccEventsResp.json();
events.forEach(e => {
if (e.x === 'visit-human') ccHumanVisits += e.y || 0;
if (e.x === 'visit-agent') ccAgentVisits += e.y || 0;
});
}
const humanCount = hkudsHumanVisits + ccHumanVisits;
const totalVisits = humanCount + ccAgentVisits;
document.getElementById('stat-total').textContent = totalVisits.toLocaleString();
document.getElementById('stat-human').textContent = humanCount.toLocaleString();
document.getElementById('stat-agent').textContent = ccAgentVisits.toLocaleString();
} catch (_) {
// Stats not available — leave dashes
// Fall back to the fixed baseline values.
}
}
loadVisitorStats();
loadGitHubStars();
if (typeof loadGitHubStars === 'function') {
loadGitHubStars();
}
})();
</script>
</body>