@brightmotion/agenthog
v0.11.0
Published
AgentHog analytics CLI — `ah login`, then query traffic, sessions, funnels and more from the terminal
Readme
ah — the AgentHog CLI
The agent-friendly read surface for AgentHog analytics:
dense, aligned, LLM-friendly terminal reports — traffic, sessions, funnels, campaigns,
retention, and more. Every command also takes --json.
Install & authenticate
npm i -g @brightmotion/agenthog
ah login # opens your browser — sign in, authorize, done
ah digest # you're in businessah login stores a scoped API token in ~/.config/agenthog/config.json (mode 0600).
Prefer env-only auth (CI, unattended agents)? Mint a token in the dashboard (or via
ah login on your machine and copy it) and set AH_TOKEN — it takes precedence over
the config file and never needs a browser:
AH_TOKEN=ah_tok_… ah digest --jsonSelf-hosted AgentHog? ah login --host https://your-agenthog.example (or set
AH_API_URL alongside AH_TOKEN).
ah whoami # which account/token/scope am I using?
ah version # which ah is installed (--check asks npm for the latest)
ah logout # revoke the login token and forget itStaying up to date
When it's talking to a real terminal, ah asks the npm registry once a day whether a
newer release exists, and prints this after the command until you update:
ah: update available — 0.2.0 → 0.3.0 (update with: npm i -g @brightmotion/agenthog)
silence this with AH_NO_UPDATE_CHECK=1The answer is cached in ~/.config/agenthog/update-check.json, so 23 of every 24 hours
the notice costs nothing at all. On the one run that does probe, the request overlaps your
command and is abandoned 750ms after the command finishes — a registry that is slow,
offline, or silently dropping packets can delay ah by that much and no more, and the
attempt is recorded either way so it won't retry until tomorrow.
It stays off for anything that isn't a human: AH_NO_UPDATE_CHECK=1, CI, non-tty stderr
(so agents and pipelines never see it), and repo checkouts. ah version --check asks on
demand.
Global flags
| flag | meaning |
|---|---|
| --project <key\|name> | which site (optional if only one project is in scope) |
| --since 24h\|7d\|30d\|Nd\|YYYY-MM-DD | time window (default 7d) |
| --all | include crawler + suspected-bot + test sessions (default: humans only) |
| --server | the default PLUS server sessions (token-authed relays) — for a funnel or campaign whose steps are relayed from a backend into one session, without admitting crawlers |
| --bots | ONLY crawler + suspected-bot sessions |
| --test | ONLY sessions from your listed test IPs (your own dev/QA traffic) |
| --json | raw JSON instead of tables |
Default filtering is humans + unknown — bots, crawlers and test traffic are excluded from aggregates unless you pass --all.
Verbs
| verb | what it answers |
|---|---|
| traffic | per-day sessions by classification + by-source breakdown |
| active [--weekly\|--monthly] | DAU/WAU/MAU (--by <dim> for breakdowns) |
| retention [--cohort day\|week\|month] | cohort retention triangle |
| campaigns [--source X] | per-campaign sessions, conversion %, junk % (bot share) |
| referrers | sources ranked by engaged-visitor rate |
| events [--name N] / events top | recent events / event names ranked by count |
| sessions list [filters] | session rows (--converted --returning --bounced --utm-source --utm-campaign --variant) |
| sessions show <id> | one session as a readable timeline (the narrative input) |
| funnel <name \| step1 step2 …> | ordered-subsequence funnel; --per-session, --by <dim> |
| funnels save <name> <steps…> / funnels list | persist a funnel definition |
| paths --to <event> | common event paths ending at a target (funnel discovery) |
| user <email\|anonId> | one person across all their sessions |
| users [--tag T] [--returning] | identity rows |
| clicks <path> | click targets on a page + dead clicks + rage clicks |
| crawlers | request-log crawler hits incl. AI-crawler subtotal |
| digest | one dense report: totals, deltas, top campaigns/sources/pages/events, friction |
| changes list/add/show/… | a changelog timeline to line up against traffic |
| track <event> [--at …] [--person …] [--prop k=v] | write one event, now or backdated (write token) |
| track --file <csv\|ndjson> … | bulk-import history from a file — --dry-run first (write token) |
| goals set <name> <event> / goals list | define a conversion goal |
| reports create <slug> --file report.md / reports read <slug> | publish a shared report (markdown + live ```widget fences) the whole org can open; read prints it resolved, preview validates a file first (create/update/delete need a write token) |
| projects list / projects create <name> --domains a,b | manage sites (create needs a write token; in early access new sites are approval-gated — request one from the dashboard) |
| sql "<query>" | SQL escape hatch (token mode: read-only, scoped to your projects) |
| version [--check] | which ah is installed; --check asks npm whether it's current |
Examples
ah digest --since 30d # the daily standup view
ah campaigns --since 30d # which ad is actually working
ah sessions list --converted # who converted
ah sessions show 831ac66d # one person's full visit
ah paths --to "form_submit: waitlist" # discover the funnel
ah track deploy_completed --prop version=1.9.2 # record something no SDK saw (write token)
ah track --file hours.csv --event time_logged --ts-col Date --dry-run # import history
ah clicks / # what people click (incl. dead clicks)
ah reports preview --file weekly.md # validate a report + run its live widgets
ah reports create weekly-growth --file weekly.md # publish it to the org (write token)
ah reports read weekly-growth # the resolved report, as a human sees itHow agents use it
The CLI is the contract: event names are deterministic and human-readable (click: Join the waitlist, form_submit: waitlist, pageview: /pricing), so an agent can compose questions in plain English → CLI flags → SQL without re-deriving the schema. The typical loop:
ah digestto get the lay of the landah paths --to <goal event>to discover the real funnel,ah funnels saveto keep itah funnel <name> --by device_typeto see where it breaks downah sessions show <id>to read individual stories behind the numbers
Operator mode (developing AgentHog itself)
With a repo checkout, DATABASE_URL connects the CLI straight to Postgres — no token —
and unlocks the operator-only verbs (sweep, tokens, raw sql, --account):
bun packages/cli/src/index.ts projects listDATABASE_URL beats the ah login config file, so a login never changes operator
behavior; AH_TOKEN beats both.
Building & publishing to npm
The shipped package is a single HTTP-only bundle — no schema, no query engine, no
drizzle. Everything runs from packages/cli/.
bun run build # bundle src/index.ts → dist/index.js (HTTP-only, the shipped shape)
bun run typecheck # tsc --noEmitbuild externalizes @agenthog/db, @agenthog/queries, drizzle-orm, and postgres
and defines __HTTP_ONLY__=true, so the operator engine is compiled out. build:operator
is the local-dev counterpart (__HTTP_ONLY__=false, no externals) — it must never be
published.
Publishing:
npm version patch # bump 0.1.1 → 0.1.2 (or minor/major); commits + tags
npm publish # runs prepublishOnly → build + assert-no-engine, then shipsprepack and prepublishOnly rebuild automatically, so a stale or operator dist/ can't
be published by accident. assert-no-engine scans the actual dist/index.js and exits
non-zero if it finds any schema, engine, or drizzle markers — run it standalone anytime
with bun run assert-no-engine. publishConfig.access is public, so no --access flag
is needed for the scoped @brightmotion/agenthog name.
