Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

AI Activity

Not affiliated. AI Activity is an independent, unofficial project. It is not affiliated with, endorsed or sponsored by Anthropic, OpenAI, Anysphere, Google or the OpenCode project. Claude, Claude Code, Codex, Cursor, Antigravity, OpenCode and the other product names used here are trademarks of their respective owners, named only to identify the tools whose usage the dashboard measures. It reads local usage files or receives metrics through the tools' hooks; it does not call their APIs on your behalf or work around their limits. Using those tools stays subject to their own terms.

Site analytics

The server counts visits to its own pages, for the admin panel only, and sends nothing to a third party. No cookie is set for it. Each browser keeps a random id in localStorage for 13 months, so a return visit is told apart from a new one; the server stores only keyed hashes of it, one per day that cannot be linked to the next, plus the first and last day seen. What is counted: the page category (never the address or the profile name), the referring site's host name, sign-ups, and reads of the public API by other sites and programs. IP addresses and user-agent strings are never stored: without the id, a hash of them under a key that changes every day and is never written to disk stands for the visitor. Daily counts are deleted after 90 days.

Friends

Sign in and open Friends from the avatar menu to see the people you follow on GitHub who also have an enabled AI Activity profile. Each entry links to that public profile and shows measured tokens, conversations and last activity for the past seven days. The server reads GitHub's public following list using the OAuth app's client ID and secret, without requesting an OAuth scope or keeping a user's GitHub token. If GitHub is unavailable, the page offers a retry.

API-equivalent value

Next to the token totals, profiles and the leaderboard show an estimated API-equivalent value: what the measured tokens would cost at the providers' published retail API rates, in USD. It is a valuation of usage, whatever paid for it (a subscription or an API key): not what you paid, not a saving, not what serving it costs the provider. Nothing new is asked of you; no plan, invoice or key is needed.

  • Where rates come from, first match wins:
    1. shared/pricing.json, the priority file: verified official rates, each with its source and the day it took effect, and aliases that price a model as another one (OpenCode's free opencode/muse-spark-1.3-contributor-free at Meta's paid meta/muse-spark-1.3-contributor rate). Edit this file to add a rate or an alias; the server refuses to start on an invalid one (npm test checks it too).
    2. LiteLLM's price list for every other model: the server downloads it at most once a day and keeps it next to the database (litellm-prices.json), so a restart or GitHub being down changes nothing. Only the provider's own rate is used, never a reseller's for the same model name. These are community rates: values using them say so. They have no effective dates: the copy in use prices all of a model's history, so a LiteLLM price change moves that model's past values too (verified rates in the priority file do not). LITELLM_PRICES_URL points elsewhere, or empty turns it off.
    3. Nothing: models without a known rate (Codex's codex-auto-review, a ChatGPT-only model; a local model) are counted as unpriced, never valued at $0: a figure that leaves some out shows the share of tokens priced ("93 % priced"), and one with nothing priced shows "—". Admin panel → Pricing lists them, with their tokens and why, and the LiteLLM copy in use.
  • What it is computed from: each message's model, input, output, cache read and cache write tokens (cached input and reasoning are never counted twice), Anthropic's 5-minute and 1-hour cache writes (2× input instead of 1.25×), Claude Code's fast mode, Anthropic's US-only inference (1.1×), Codex's service tier (Fast/priority, Flex) and the long-context rates where a model has them (Sonnet 4 and 4.5 above 200K prompt tokens; GPT-5.4, GPT-5.5, GPT-5.6 and GPT-6 above 272K).
  • Versions: the priority file has a version (shown with the values), changed with every edit. Usage is priced at the rate of its day; usage older than a model's oldest published rate is priced at that rate and flagged. The dashboard and the leaderboard use the same rates, so their figures for a period agree.
  • Assumptions it flags: messages recorded before the collectors sent the cache durations (Claude Code collector 4) are priced at the cheaper 5-minute rate, shown as "≥" (a lower bound). Updated collectors send their history once more, which fills that in. Batch discounts, Codex data residency surcharges, server tool fees (web search) and negotiated discounts are not measured, so not included.
  • Codex Fast mode is valued as requested. Under load, OpenAI can serve a Fast (priority) request at Standard speed and bill it at Standard rates, but the rollout only records the tier Codex asked for. Such responses are valued at Fast rates: the value can be slightly too high. Nothing in the local files tells them apart today.
  • Leaderboard: rank by tokens (the default) or by API value over the same period; accounts with nothing priced rank last.

Dashboard panels

On your own profile, use Edit layout at the top right of Dashboard panels. While you edit, every card shows sample data (the fictional /demo data), so you see how the dashboard will look. Arrange it by dragging the cards: drop a card next to another to share its row (up to three cards per row), above or below a row to start a new one, or onto a card of a full row to swap them. An empty box shows where the card will land, and the other cards move aside. Drag the bar between two cards to set their widths (half, ⅔ · ⅓ or ⅓ · ⅔). The drawer at the bottom holds the panels not shown (every tool and four usage cards: Today by tool, Today by hour, Best day and Leaderboard · 7 days): drag one onto the dashboard to add it, or drag a card onto the drawer to remove it. Claude Code, Codex and Antigravity each have a quotas and a details panel, which can be shown together. On a touch screen, press and hold a card to pick it up. With a keyboard, focus a card, press Space, move it with the arrow keys and press Space again (Escape cancels). Save layout publishes it on your public profile; visitors see it but cannot change it. Until you save one, your profile follows the default layout, new tools included. The public /demo page shows every panel with fictional, labeled data.

One-command install

Create a device key (Settings → Devices), then on that machine, as the user who runs the tools (no sudo, no administrator shell):

# Linux, macOS
curl -fsSL <server>/install.sh | AI_ACTIVITY_URL=<server> AI_ACTIVITY_KEY=<device key> sh
# Windows (PowerShell)
$env:AI_ACTIVITY_URL="<server>"; $env:AI_ACTIVITY_KEY="<device key>"; irm <server>/install.ps1 | iex

After you create a key, Settings → Devices offers buttons to copy the Linux/macOS command, Windows command, or an AI setup prompt. Each existing device has a Set up action for the same options and a way to copy its key again. The prompt gives an AI assistant both commands, asks it to use the one that matches the device, and tells it to explain the installer's output and any follow-up steps. The installer detects tools automatically; the prompt does not ask you to select them. You can review the full prompt before copying. It includes the device ingestion key: pasting it into an AI service shares that key with the service. Revoke the key in Settings → Devices if it is exposed.

The copy buttons include the key in each command (your profile links to Settings → Devices under the tools). The key is passed in the environment, never in a URL, and ends up only in the installed collectors (files readable by you alone on Linux/macOS). Both scripts need Python 3 (python3; on Windows python or the py launcher) and install the collectors of the tools they find (claude / codex / cursor / agy / opencode on the PATH, or their config folders), as the sections below describe:

  • Claude Code: ~/.claude/ai-activity-claude-code.py, its five hooks (tokens) and the statusLine (quotas) in ~/.claude/settings.json, next to your other hooks. Another status line already set is left alone: the tokens are still sent, the quotas are not (AI_ACTIVITY_FORCE=1 replaces it: Claude Code runs only one). Restart Claude Code.
  • Codex: ~/.codex/ai-activity-codex.py and its three hooks in ~/.codex/hooks.json, next to your other hooks. Review them once with /hooks.
  • Cursor: ~/.cursor/hooks/ai-activity-cursor.py and afterAgentResponse in ~/.cursor/hooks.json, next to your other hooks. Check Settings → Hooks. Only new Agent turns reporting tokens are collected.
  • Antigravity: ~/.gemini/ai-activity-antigravity.py and the named ai-activity hook in ~/.gemini/config/hooks.json. Restart it and check the hook is enabled. Quotas stay off (AI_ACTIVITY_ANTIGRAVITY_QUOTAS).
  • OpenCode: the collector and plugin in ~/.config/opencode. Restart it.

On Linux/macOS the commands are the ones below (python3 ~/…; on macOS, which has no setsid, Codex runs the script with --hook). On Windows they name the absolute path of the Python that ran the installer, so the tools find it whatever their PATH (OpenCode's plugin still runs python).

Other entries of those files are kept, and a file that is a symlink (a dotfiles repo) stays one: its target is updated. If one of them is not valid JSON, nothing is installed until it is fixed. AI_ACTIVITY_TOOLS=claude-code,codex ($env:AI_ACTIVITY_TOOLS=… on Windows) picks the tools instead. Running it again only updates what changed (a new key or server URL, a newer collector), never duplicates a hook. It checks that the server takes the key first, refuses a <server> that redirects (use the address it redirects to, e.g. https://: the collectors do not follow redirects), and refuses to run as root unless AI_ACTIVITY_ALLOW_ROOT=1.

Send Claude Code usage from a device

  1. Create a device key in the dashboard, Settings → Devices (one key per machine, it serves every tool on it; Copy key there gives it back any time).
  2. Copy collectors/claude-code.py to ~/.claude/ai-activity-claude-code.py on the device and replace <server> (e.g. http://localhost:3000 or your tunnel URL) and <device key> at its top (or set AI_ACTIVITY_URL / AI_ACTIVITY_KEY in the environment Claude Code runs in).
  3. Add this to ~/.claude/settings.json (merge the hooks into any you already have), then restart Claude Code:
"hooks": {
  "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
  "PostToolUse": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
  "Stop": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
  "StopFailure": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
  "SessionEnd": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}]
},
"statusLine": {
  "type": "command",
  "command": "python3 ~/.claude/ai-activity-claude-code.py"
}

On Windows, use this instead, replacing <user> with your Windows user directory name (backslashes and quotes are already JSON-escaped):

"hooks": {
  "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
  "PostToolUse": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
  "Stop": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
  "StopFailure": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
  "SessionEnd": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}]
},
"statusLine": {
  "type": "command",
  "command": "python \"C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py\""
}

It needs Python 3 (standard library only) and runs as your user: no root, nothing printed in the status line. Use the absolute path of the interpreter if it is not on Claude Code's PATH (python3 -c 'import sys; print(sys.executable)', or python -c "import sys; print(sys.executable)" on Windows, JSON-escaped like the script path).

Windows hooks use command plus args: Claude launches Python directly, so they work when Claude is started from PowerShell or cmd, with or without Git Bash. An absolute command path must have no extra shell quotes. The Windows installer also creates ai-activity-claude-code.ps1 for the status line, calling the installing Python interpreter with PowerShell's & operator. It invokes that wrapper explicitly with powershell.exe -NoProfile -File, so spaced interpreter paths work from either shell too.

The hooks send the tokens, the status line the quotas. Claude Code only gives its 5-hour and 7-day quotas and the context fill to the status line; the tokens are in the transcripts, which any hook can read. So the hooks alone count every token, headless runs (claude -p, the Agent SDK) included, and keep working next to a status line of your own: only the quotas are then missing ("Unavailable").

Replacing an earlier install (the statusLine alone, or the former setsid -f python3 -c "…" one-liner): run the install command again, or add the hooks and swap the statusLine command for the one above. The one-liner's first run sends every transcript again (its offsets did not say which server they were for); the server stores each message id once, so nothing is counted twice or missed.

What the hooks do (a prompt, every tool call, the end of a turn, a turn ended by an API error such as a rate limit, the end of a session). A turn you interrupt fires none of them: its tokens go with the next prompt, or at exit:

  • Read what was added to every transcript under ~/.claude/projects (sessions and subagents, all projects) since the last successful upload, and send one entry per Anthropic message id with its token counts. Prompts and replies never leave the device, only ids, model, time (with the device's UTC offset at that time) and counts. The hook's own input is never sent.
  • The first run therefore sends every transcript still on disk (Claude Code keeps about 30 days by default): that is the import of past sessions. It also replaces the rows the old snapshot collector sent for those sessions, which counted most API calls twice.
  • How far each file was sent is kept in ~/.cache/ai-activity/offsets.json, per server and device key, and only moves forward once the server accepted everything, so nothing is lost while the server is down: the next run sends the backlog. A new server or key starts from nothing, so its first run sends the whole history. Delete that file to send everything again (safe: the server stores each message id once).
  • The server stores each message id once. Claude Code sometimes writes a partial entry (a few output tokens) before the final one; the final counts replace it.
  • The script answers at once and starts the upload detached (its own session on Linux/macOS; on Windows out of the console, the process group and, when Windows permits it, the parent job), so it never slows Claude Code down and survives it being cancelled. Uploads wait for each other (a lock in ~/.cache/ai-activity); since tool calls come fast, at most one more waits behind the active one and the others exit at once. A run gives up after 15 minutes.

What the status line does on every refresh: sends the 5-hour and 7-day quotas and the context fill, detached the same way, unless the same values went to the same server in the last 5 minutes (status.json in ~/.cache/ai-activity): it refreshes several times a second while Claude Code works, and shares the device's request budget with the hooks. It never reads the transcripts, and prints nothing. While a POST is in flight, newer observations are saved per session and coalesced; the active uploader sends the latest without needing another refresh. Failed observations stay queued for the next refresh or hook. The last context for each of the 100 most recent sessions is retained and reapplied after its tokens arrive, even if the status was uploaded first.

Days are local, like GitHub's contribution calendar. Each entry carries the device's UTC offset when it happened (daylight saving included), and counts on that local day for every visitor: it never moves afterwards, even if you travel. "Today" and the streak end on the day it is at the offset of your latest entry. Entries sent before collectors had offsets count as UTC days; delete ~/.cache/ai-activity/offsets.json (and codex.json, opencode.json) once to resend the history with offsets: nothing is counted twice, the server only adds the missing offsets.

The tool is part of the URL (/api/ingest/claude-code, /api/ingest/codex, /api/ingest/opencode, /api/ingest/antigravity); a bare /api/ingest answers 404. See AGENTS.md §5 for the payload contract.

Send Codex usage from a device

  1. Create a device key as above (the same key can serve both tools).
  2. Copy collectors/codex.py to ~/.codex/ai-activity-codex.py on the device and replace <server> and <device key> at its top (or set AI_ACTIVITY_URL / AI_ACTIVITY_KEY in the environment Codex runs in).
  3. Add the hooks to ~/.codex/hooks.json (the same command four times):

~/.codex/hooks.json:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 10 }] }
    ],
    "PostToolUse": [
      { "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 10 }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 10 }] }
    ],
    "SessionEnd": [
      { "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 3 }] }
    ]
  }
}

On macOS, replace each command value above with python3 ~/.codex/ai-activity-codex.py --hook. The script answers the hook and starts its upload in a detached process without setsid.

On Windows, use this instead, replacing <user> with your Windows user directory name (--hook answers Codex and starts the upload detached). These commands target PowerShell: & is its call operator and is required before a quoted executable path. If Python is not on Codex's PATH, replace python with its quoted absolute path, JSON-escaped as shown below:

%USERPROFILE%\.codex\hooks.json:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 10 }] }
    ],
    "PostToolUse": [
      { "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 10 }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 10 }] }
    ],
    "SessionEnd": [
      { "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 3 }] }
    ]
  }
}

For example, a Python installation with spaces in its path uses this JSON command for each of the four hooks:

{ "command": "& \"C:\\Program Files\\Python312\\python.exe\" \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook" }

Use paths that exist on your device. The equivalent command entered directly in PowerShell is:

& "C:\Program Files\Python312\python.exe" "C:\Users\<user>\.codex\ai-activity-codex.py" --hook

If your hook runner uses cmd.exe, omit &; it is PowerShell syntax. Claude Code's Windows hooks above use direct executable/argument form, which needs neither PowerShell's & nor cmd-specific quoting.

Codex asks you to review a new hook once (/hooks) before running it.

Windows troubleshooting

Repeated hook exited with code 1 errors can mean PowerShell rejected a quoted executable path before Python ran. Check the call operator and JSON escaping above; a successful --hook invocation returns exit code 0 and {}. Re-run the one-command installer to update all three AI Activity Codex hooks; it preserves your other hooks.

If hooks succeed but console windows still flash, Codex CLI's shared background server can be responsible. The following workaround stopped the flashes in the Windows setup reported in AI Activity #173:

codex --no-daemon
# To resume a session:
codex --no-daemon resume

This runs the CLI session without the shared background server. Keep the collector hooks enabled: automatic uploads, historical imports and archived sessions continue to work. Check codex --help for --no-daemon in your installed version. This is a CLI workaround, not a guaranteed fix for every source of console flashes or for the desktop app. Switching to pythonw.exe alone did not eliminate the flashes in that setup. Upstream reports: openai/codex #48422 and openai/codex #48074.

When you update ~/.codex/hooks.json, re-copy collectors/codex.py to ~/.codex/ai-activity-codex.py at the same time: an older script blocks on codex.lock, so the frequent PostToolUse runs would pile up behind it.

What it does after every tool call and at the end of every turn:

  • Reads what was added to every rollout under ~/.codex/sessions and ~/.codex/archived_sessions since the last successful upload. Every Codex front end writes those files (CLI, codex exec, the IDE extension and the desktop app), so tasks started anywhere are counted, including ones already in progress when the hook was added.
  • Sends one entry per model response (token_usage_record, keyed by its resp_… id) with its token counts, model and the machine's UTC offset at that time. Rollouts from Codex versions without those records are read from their token_count lines, one per response, keyed by the thread's running total so a repeated line counts once. Prompts, replies and tool output never leave the device.
  • The first run sends every rollout still on disk: that is the import of past sessions.
  • Also sends the 5-hour and weekly rate limits and the context fill Codex recorded with each response, dated when Codex measured them. A limit snapshot written without token counts (e.g. when a turn failed) is sent too, so an exhausted quota still records its final value.
  • Codex has no StopFailure hook. Its Stop hook does not fire when Codex stops on a rate limit (upstream Codex bug), which would leave that final snapshot unsent: the UserPromptSubmit hook runs the same script on your next prompt and picks it up. If Codex is driven without prompts (codex exec), run the script by hand or from cron after hitting a limit instead.
  • How far each file was sent is kept in ~/.cache/ai-activity/codex.json, per server and device key (a new one gets the whole history), and only moves forward once the server accepted everything, so nothing is lost while the server is down (no separate spool needed): the next turn sends the backlog with its original times. Delete that file to send everything again (safe: the server stores each response once).
  • PostToolUse sends a long turn's usage while it runs (Codex writes each response to the rollout as it comes), so the dashboard follows the turn instead of catching up at its end. A reply without any tool call waits for Stop.
  • SessionEnd sends any remaining rollout lines when the main session ends. Codex may delay this event until the session has been idle for 30 minutes; it is not an immediate replacement for UserPromptSubmit after a failed turn.
  • On Linux, setsid -f detaches the upload so Codex goes on at once; echo '{}' is the (empty) JSON answer Codex expects from a hook. On macOS and Windows, --hook prints that answer and detaches the worker. One run at a time, with at most one waiting behind it (it reads the rollouts once its turn comes, so any other run can stop at once); a run gives up after 15 minutes. The script is idempotent: it can also run by hand or from cron.

Send Antigravity usage from a device

  1. Create a device key as above (the same key serves every tool).
  2. Copy collectors/antigravity.py to ~/.gemini/ai-activity-antigravity.py. Replace <server> and <device key> at its top, or set AI_ACTIVITY_URL and AI_ACTIVITY_KEY in the environment Antigravity runs in. Python 3 with the standard-library SQLite module is required.
  3. Merge this named hook into ~/.gemini/config/hooks.json (keep existing hooks). For Linux/macOS:
{
  "ai-activity": {
    "enabled": true,
    "PostInvocation": [
      {
        "type": "command",
        "command": "python3 ~/.gemini/ai-activity-antigravity.py --post-invocation",
        "timeout": 10
      }
    ],
    "Stop": [
      {
        "type": "command",
        "command": "python3 ~/.gemini/ai-activity-antigravity.py --hook",
        "timeout": 10
      }
    ]
  }
}

For Windows, use 8.3 short paths with no quotes instead, replacing <user> with your Windows user directory name. Backslashes below are already JSON-escaped. Quotes would seem natural here, but agy's hook runner splits the command naively on spaces and keeps the quotes in the pieces, so a quoted path never resolves; the one-command install writes these short paths for you (find one with cmd /c for %I in ("<path>") do @echo %~sI):

{
  "ai-activity": {
    "enabled": true,
    "PostInvocation": [
      {
        "type": "command",
        "command": "C:\\PROGRA~1\\Python312\\python.exe C:\\Users\\<user>\\.gemini\\ai-activity-antigravity.py --post-invocation",
        "timeout": 10
      }
    ],
    "Stop": [
      {
        "type": "command",
        "command": "C:\\PROGRA~1\\Python312\\python.exe C:\\Users\\<user>\\.gemini\\ai-activity-antigravity.py --hook",
        "timeout": 10
      }
    ]
  }
}

Use absolute paths to both Python and the collector if Python is not on Antigravity's PATH (desktop apps can inherit a different PATH from your terminal). Find the interpreter with python3 -c 'import sys; print(sys.executable)' on Linux/macOS, or python -c "import sys; print(sys.executable)" on Windows. Quote paths containing spaces; on Windows JSON-escape the interpreter path in the same way as the collector path. Configure the URL/key in the copied script if Antigravity does not inherit your terminal's environment; use a stable server URL for ongoing collection. Keep the device key out of the hook command and never commit the configured copy.

The Antigravity hook configuration is shared by Antigravity 2.0, CLI, and IDE. PostInvocation refreshes after each model invocation during a turn; Stop refreshes when the execution loop ends. Both return immediately and launch a detached collector, which waits two seconds for metadata to flush after acquiring the collection lock. At most one hook worker collects and one waits; additional hooks coalesce into that waiting pass, which reads a fresh snapshot. A hook during collection can queue the next pass, so the final Stop update is included. Only one uploads at a time. The quota probe runs after the collection lock is released, so a queued worker never waits for it. Updates need no manual command after setup, while Antigravity is running and these hooks are enabled.

  1. Restart Antigravity, then confirm ai-activity is enabled: /hooks in CLI, Settings → Customizations → Hooks in Antigravity 2.0, or … → Customizations → Hooks in the IDE agent side panel.
  2. Run the copied script without either hook flag once to import history and see diagnostics. Exit code 0 means supported entries were processed; warnings can still indicate skipped unsupported rows or databases, or an unavailable quota report (with its reason). Exit code 1 means a busy database or an upload failure; fix it and run again.
  3. Complete a new Antigravity turn and leave the dashboard open. Its existing 5-second refresh should show supported persisted usage after collection. If it does not, check the hook is loaded, Python and script paths resolve in Antigravity, the configured URL/key are correct, and a manual run works. Unsupported database formats may still produce no usage; see below.

For retries while Antigravity is idle, or a version that persists metadata later than its hooks run, you can additionally schedule the script without hook flags every minute (cron on Linux/macOS or Task Scheduler on Windows). Use the same user, configured script, and absolute interpreter/script paths; on Windows set the task not to start another instance if already running. Hooks and scheduled runs share checkpoints and safely deduplicate uploads. On Windows, workers detach from the console and create a new process group. They also break away from the parent job when Windows permits it; jobs that forbid breakaway fall back to console/group detachment. If the host kills its entire job, use the scheduled retry above to cover that restriction.

What it does:

  • Reads existing SQLite databases only under ~/.gemini/{antigravity,antigravity-cli,antigravity-ide}/conversations/. GEMINI_CLI_HOME can replace ~/.gemini. Support depends on a database containing the recognized gen_metadata table; encrypted/legacy conversation files and transcript-only versions are not supported.
  • Selects generation metadata and, when needed, step metadata from a read-only snapshot. Never selects conversation text, prompts, responses, tool output, workspace paths, or authentication data.
  • Sends ids, recorded model (unknown stays unknown), token counts, the original generation timestamp, and this machine's UTC offset at that time. Input is uncached input; cached input is separate; text and thinking output are added once. Antigravity CLI 1.2.12 writes a constant in 1.4.1 that is not an input counter and repeats total output in 1.4.3: both are ignored, so a first turn reports 12 input / 7 output, not 1048. Subagent databases count as separate conversations because parent attribution is unavailable.
  • Imports supported history, then skips every database whose stamp is unchanged since all of it was accepted. The stamp covers the database and its WAL: mtime, size, ctime, inode, the database header's change counter and the WAL header's salts. A changed database is read again in full (edits to older rows included), but only new responses, or ones with more output tokens, are sent. Checkpoints in ~/.cache/ai-activity/antigravity.json hold, per server and device key and per conversation (hashed path), that stamp and the output tokens accepted per response id; a batch is recorded only once accepted, and deleted conversations are forgotten. A malformed checkpoint file starts over (the server deduplicates the replay); removing it replays history too. Switching server or device key automatically starts a new import.
  • A busy (locked) database fails the run and is read again next time. Any other unreadable or unsupported database (not SQLite, another layout, a WAL database whose -shm file cannot be created) is skipped with a warning until its stamp changes.
  • Unconfigured URL/key placeholders exit before reading history. The first HTTP/network failure on a usage upload stops the pass, including quota probing. HTTP 429/503 honor Retry-After (seconds or HTTP date; bounded to one day, with a one-minute fallback). Device keys are never forwarded through redirects.
  • A generation without its own timestamp takes its step's, streamed from the same snapshot, only when its step/bot key belongs to one response; otherwise it is skipped with a warning, never dated by import time. Metadata blobs over 1 MiB are skipped.
  • Quotas are off by default. With AI_ACTIVITY_ANTIGRAVITY_QUOTAS=1 in the environment the hooks run in, it collects measured five-hour and weekly quota snapshots using the signed-in Antigravity CLI's /usage JSON report. That runs agy automatically, and each probe reaches Google's backend. Antigravity's terms forbid using third-party tools to access the service and allow suspending the account; the probe uses Google's own CLI and sign-in, but enable it at your own risk. Without it the card shows Unavailable quotas. Install agy 1.1.11 or later, sign in with the same Google account you use in Antigravity, and make agy available on the collector's PATH (including hooks and scheduled tasks). Verify agy --version (its output must contain the version) and agy -p /usage --output-format json --print-timeout 90s in a terminal. The CLI handles its own authentication; the collector never reads provider credential files. Desktop/IDE history still imports without the CLI; a missing CLI, an old or unrecognized version, a failed probe or an unsupported report leave quota windows Unavailable, with a diagnostic naming which. A window agy reports untouched (100% left, resetting a full window length from now, or no reset time) has not started yet: it stays Unavailable rather than showing 0%.
  • Quotas refresh on hooks/manual/scheduled runs, at most once per minute after a successful upload, including runs with no new token activity. Failed probes or uploads back off for five minutes (or the server's Retry-After), in ~/.cache/ai-activity/antigravity-quota.json, apart from usage uploads: a refused quota upload never delays token imports. Attempts are saved before probing, so interruption cannot reset the throttle. Token collection finishes first; the probe then runs outside the collection lock, under its own lock, and a run that finds another probe in progress skips its own. Use the optional one-minute schedule above for updates while idle. Gemini and Claude/GPT pools remain separate: the card uses the same percentage bars, elapsed-window marks, reset countdowns and expiry behavior as Codex and Claude Code. Percentages are used quota, never inferred from tokens. Missing/disabled buckets stay unavailable; after reset, the old value stays unavailable until a fresh snapshot arrives. Free plans may expose only a weekly quota. Context fill remains unavailable. To preview the card, open /demo on your server (no account needed). That Demonstration data page includes fictional Antigravity quotas, activity and conversations; it never writes them to the server.
  • The quota subprocess runs /usage in an empty temporary directory, without the Activity URL/key, and cannot recursively trigger this collector's hooks. Versions older than 1.1.11 or an unrecognized version never receive /usage, since older print modes may treat it as a prompt. Leave AI_ACTIVITY_ANTIGRAVITY_QUOTAS unset (or anything but 1) to keep quota probing off.

Quota command/schema evidence comes from CodexBar's Antigravity implementation. Google documents the quota command and plan windows. Quota tests run the collector against a fake agy with synthetic reports.

Format limitations: Antigravity's persisted protobuf layout is undocumented. The parser follows independently observed field evidence, plus the CLI 1.2.12 Gemini layout verified against a local stub (synthetic usage 12/7, 31/11, then 37 input / 13 output / 4 cached / 6 thinking): response id in 1.4.7 (or the request_id in chat.20 when the API returned none), model in 1.19, uncached input in 1.4.2, cached input in 1.4.5, thinking in 1.4.9, text output in 1.4.10. It accepts standard protobuf generation timestamps, or a unique matching step UUID (4) and response id with a standard step timestamp (steps.1). Unknown timestamp layouts, missing response ids, corrupt records, and ambiguous step matches are skipped with a diagnostic, and read again only when their database changes. Rows whose model is not Gemini are skipped too under the 1.2.12 rules (their field meanings are unverified), and rows without a per-response id share their turn's request id and count once (the largest response). They are never assigned the database modification time or import time, so totals may be incomplete on unsupported versions. Automated tests use synthetic SQLite/protobuf fixtures for both the legacy and the 1.2.12 layouts.

Send Cursor usage from a device

  1. Create a device key as above (the same key serves every tool).
  2. Copy collectors/cursor.py to ~/.cursor/hooks/ai-activity-cursor.py and replace <server> and <device key> at its top (or set AI_ACTIVITY_URL / AI_ACTIVITY_KEY in Cursor's environment).
  3. Merge this into the user ~/.cursor/hooks.json:
{
  "version": 1,
  "hooks": {
    "afterAgentResponse": [
      {"command": "python3 ~/.cursor/hooks/ai-activity-cursor.py --hook", "timeout": 10}
    ]
  }
}

On Windows, ~ is C:\Users\<user>. Use python and an absolute script path in the command, with JSON-escaped backslashes and quotes:

{
  "version": 1,
  "hooks": {
    "afterAgentResponse": [
      {"command": "python \"C:\\Users\\<user>\\.cursor\\hooks\\ai-activity-cursor.py\" --hook", "timeout": 10}
    ]
  }
}

The one-command installer uses the absolute installing interpreter and a PowerShell wrapper on Windows, so spaced interpreter and user paths work. Prefer it if Python is not on Cursor's PATH. Check the hook in Cursor's Settings → Hooks / Hooks output channel; Cursor reloads the configuration on save (restart if it has not loaded).

What is measured. afterAgentResponse is the installed source of truth. The collector also accepts a stop payload if wired manually; a repeated conversation + generation ID counts once, with final counts replacing a partial turn when output rises. Each dashboard call represents one measured Agent turn, which can contain several model requests; the UI labels these as "Agent turns". It requires all four nonnegative integer counters: absent counts stay unavailable, never estimated.

Raw hook fields are input_tokens, output_tokens, cache_read_tokens, cache_write_tokens (camelCase aliases are accepted). Hook input includes cache reads and writes: the server stores input minus both caches, stores cache separately, and retains output including reasoning. For input 1,000, output 80, cache read 600 and cache write 100, the total is 1,080, with 300 uncached input. Cache sums above input or unsafe totals are rejected as unavailable, rather than silently counting a changed counter convention. These are raw hook counts, not the disjoint SDK usage shape. The mapping was checked in the official Cursor agent package 2026.09.28-64d2043: the hook forwards turn usage and its Anthropic adapter subtracts both cache counters from input. Hook configuration is described in Cursor's hooks reference.

Privacy and retries. Before answering {}, the hook retains only conversation/generation IDs, model, counts, timestamp and the local UTC offset in ~/.cache/ai-activity/cursor.db (a private SQLite metrics journal, using Python's standard library). It never retains or uploads reply/prompt text, email, paths or provider keys. If a hook has no timestamp, live receipt time is saved once; numeric epoch seconds/milliseconds and ISO timestamps are accepted. Retries and partial/final updates preserve time and offset. A detached worker uploads batches below the API's body-size limit, with one uploader and at most one waiter. Indexed revisions select only new or changed turns; idle runs never scan the retained history. Each of the eight recent server/device targets keeps one checkpoint, which advances only after acceptance, without a growing set of per-turn accepted hashes.

An HTTP 429 respects Retry-After (seconds or an HTTP date); other failed uploads back off too. Retry on a later hook or run the copied script without --hook; schedule that command if retries are needed while Cursor is idle. Run it with --replay to resend retained history to the current target; changing the URL/key also replays it. The journal keeps one compact row per turn so history can be replayed; it grows with measured turns. Removing cursor.db while the collector is stopped erases that local history. Older cursor-events/*.json journals migrate once, preserving times and local days, with a safe server-deduplicated replay. Invalid legacy files are quarantined in cursor-invalid/; invalid rows cannot block other metrics or upload unvalidated fields. A damaged SQLite database is kept for recovery, never silently replaced with an empty one.

Scope and gaps. This installs user hooks for local IDE Agent Chat / Cmd+K only. It collects future hook observations, with no import of pre-install Cursor history. Tab completions are a different hook surface. Some CLI or non-interactive versions omit these hooks or token fields; cloud agents load project hooks and cannot read your user configuration. Neither is promised in this first version. Parent turn counts can omit subagent usage; no subagent tokens, quotas or context fill are inferred. Missing windows stay unavailable and the Cursor card shows activity, like OpenCode. See Cursor's explanation of the CLI hook gaps and parent-only token counts. Tests use synthetic hook fixtures; a real IDE turn remains to be verified.

Send OpenCode usage from a device

  1. Create a device key as above (the same key serves every tool).
  2. Copy collectors/opencode.py to ~/.config/opencode/ai-activity-opencode.py and replace <server> and <device key> at its top (or set AI_ACTIVITY_URL / AI_ACTIVITY_KEY in the environment OpenCode runs in).
  3. Copy collectors/opencode-plugin.js to ~/.config/opencode/plugins/ai-activity.js. OpenCode loads it at start.

On Windows, ~ is your user directory (C:\Users\<user>), for OpenCode's folders too, and the plugin runs python instead of python3: it must be on OpenCode's PATH.

What it does:

  • The plugin runs the collector, detached, when OpenCode starts and whenever a session goes idle (one run at a time).
  • The collector reads OpenCode's own database (~/.local/share/opencode/opencode.db, read-only) and sends one entry per assistant message (keyed by its msg_… id) with its token counts, provider and model (stored as provider/model) and the machine's UTC offset at that time. It selects numeric fields only: prompts, replies, tool output, titles and paths are never read.
  • Subagent sessions are sent as their root session, so a conversation with subagents counts once.
  • OpenCode counts reasoning apart from output; it is added to output, like OpenCode's own totals, never twice.
  • The first run sends the whole database: that is the import of past sessions.
  • No 5-hour or weekly limit: OpenCode has none of its own, so the card shows the active conversations and today's usage instead.
  • How far the database was sent is kept in ~/.cache/ai-activity/opencode.json, per server and device key (a new one gets the whole history), and only moves forward once the server accepted a batch, so nothing is lost while the server is down (the database is the queue). Delete that file to send everything again (safe: the server stores each message once). The script is idempotent: it can also run by hand or from cron.

How the collector scripts work

Each tool has one Python script in collectors/. They share the same design:

  • One file, standard library only. Copy it next to the tool, set <server> and <device key> at its top, or AI_ACTIVITY_URL / AI_ACTIVITY_KEY in the tool's environment (the environment wins). Linux, macOS and Windows alike (python3 or python).
  • Metrics only. They read local usage files read-only or receive live hook metrics, and send ids, model, time, the machine's UTC offset and token counts (plus quotas and context fill where the tool has them, and what a message's API-equivalent price depends on: Claude Code's cache write durations, speed, service_tier and inference_geo; Codex's service tier and model provider). Prompts, replies, tool output, titles, paths and provider keys never leave the device.
  • The key only goes to your server. Uploads never follow an HTTP redirect: a redirect fails the run (progress unchanged) instead of sending the device key somewhere else.
  • Progress only moves on success. How far each source was sent is kept under ~/.cache/ai-activity/ (JSON offsets, or Cursor's SQLite checkpoints), saved once the server accepted it. While the server is down nothing moves: the next run sends the backlog with its original times. Delete JSON offsets, or run Cursor with --replay, to resend history; the server stores each message once, and a message seen again with more output tokens replaces its partial counts.
  • Progress is per server and key. The progress store keeps one set of offsets per server URL and device key (JSON {"targets": {"<fingerprint>": …}}, or Cursor's targets table; the fingerprint is the first 16 hex digits of the SHA-256 of both, never the key itself), for the 8 most recently used. Point a device at a new server, or give it a new key, and its next run sends the whole local history there; switch back and it resumes where it was. With a new key on the same account, everything comes back as already stored. With another account on the same server, messages the first account already sent stay with it (the server never moves them between accounts). Claude Code's and Codex's progress also records which message fields it was sent with ("fields"): a collector that sends new fields sends its history once more, and the server fills them in on messages it stored without them (nothing is counted twice).
  • Never in the tool's way. Called from a hook or the status line, a script answers at once and uploads from a detached copy of itself (Linux/macOS: its own session; Windows: out of the console, the process group and, when permitted, the parent job). A failure prints one line on stderr and exits 1; the tool is never blocked, and the next run retries.
  • One upload at a time. A lock file in ~/.cache/ai-activity/ (flock; on Windows msvcrt on its first byte) serializes runs. Where hooks fire often (Claude Code, Codex, Antigravity), one more run may wait behind the active one and any other exits at once: the waiter reads the sources only once its turn comes, so it sends what they would have.
  • Idempotent. Any script can also run by hand or from cron (Task Scheduler on Windows): it sends only what is new.
  • Versioned. Each script has a VERSION at its top and sends it with every upload. When the server has a newer one, the script says so on stderr and in ~/.cache/ai-activity/update-available-<tool> (removed once up to date), and Settings → Devices flags that device's collector as outdated. To update, run the device's install command again (or copy the new script by hand). The server never sends code: updates are always yours to run. A collector too old for the server (426) keeps its backlog, which goes out once it is updated.
Script Copy to Run by Reads Progress file Locks
claude-code.py ~/.claude/ai-activity-claude-code.py (Windows status wrapper: .ps1 next to it) the UserPromptSubmit, PostToolUse, Stop, StopFailure and SessionEnd hooks (tokens); the statusLine, every refresh (quotas, context) ~/.claude/projects/**/*.jsonl (sessions and subagents) offsets.json (byte offset per transcript), status.json (posted/pending status and recent contexts per target) lock, waiter.lock, status-state.lock, status-<target>.lock
codex.py ~/.codex/ai-activity-codex.py the UserPromptSubmit, PostToolUse, Stop and SessionEnd hooks ~/.codex/sessions, ~/.codex/archived_sessions (CODEX_HOME) codex.json (byte offset per rollout) codex.lock, codex-waiter.lock
cursor.py ~/.cursor/hooks/ai-activity-cursor.py (Windows: .ps1 next to it) afterAgentResponse user hook stdin metrics, then cursor.db cursor.db (metrics and one revision checkpoint per target; --replay resets the current target) SQLite transactions, cursor.lock, cursor-waiter.lock
opencode.py ~/.config/opencode/ai-activity-opencode.py opencode-plugin.js, at start and on session.idle ~/.local/share/opencode/opencode.db (XDG_DATA_HOME, OPENCODE_DB), numeric fields only opencode.json (last time_updated sent) opencode.lock
antigravity.py ~/.gemini/ai-activity-antigravity.py the PostInvocation and Stop hooks ~/.gemini/{antigravity,antigravity-cli,antigravity-ide}/conversations/*.db (GEMINI_CLI_HOME); quotas from agy, opt-in antigravity.json (per database), antigravity-quota.json antigravity.lock, antigravity-waiter.lock, antigravity-quota.lock

How each one is started:

  • claude-code.py: --hook (the hooks) reads the hook's JSON on stdin (never sent), starts claude-code.py --worker detached, and prints nothing. Without arguments (the statusLine), it hands the status line's JSON to claude-code.py --worker --status, which posts the quotas and context fill. --worker alone collects the tokens in the foreground: run python3 ~/.claude/ai-activity-claude-code.py --worker (on Windows, python "<path>" --worker) to see errors while setting up.
  • codex.py: without arguments, collects in the foreground (by hand, cron, and the Linux hooks, which detach it with setsid -f). --hook (the macOS and Windows hooks) prints {} for Codex and starts the script again detached.
  • cursor.py: --hook allowlists stdin metrics into its local journal, prints {}, and starts itself detached. Without arguments it uploads unaccepted journal entries in the foreground (also suitable for cron).
  • opencode.py: without arguments, collects in the foreground; the plugin starts it detached, one run at a time.
  • antigravity.py: without arguments, collects in the foreground and prints diagnostics (skipped rows or databases, quota availability). --post-invocation and --hook answer the hook ({}, or {"decision":"stop"} for Stop) and start a detached worker, which waits 2 seconds for Antigravity to write its metadata.

Claude Code, Codex, Cursor and OpenCode runs give up after 15 minutes (the progress already accepted is kept). The payloads each script sends are described in AGENTS.md §5.

Collector CI

npm test runs the installed collector integration tests on Linux and ARM64; the Windows and macOS CI jobs run them explicitly. Each test starts a temporary app and local HTTP receiver, runs the served installer in an isolated home, invokes the installed status line, hook or plugin against synthetic tool data, then checks the upload format, retry, deduplication and dashboard totals. No model API or external account is needed.

The Collector CLI smoke workflow runs on relevant collector changes, on pull requests and pushes to main, every Monday against the latest CLI releases, and can be started manually from Actions (pinned or latest versions). Its Linux x64, Linux ARM64, Windows x64 and macOS jobs install the Claude Code, Codex and OpenCode CLIs, point each at a local fake model API, complete one chat, and check that the installed integration reaches the real app. Windows uses a temporary ConPTY for Claude Code's interactive status line; macOS uses a pseudo-terminal through node-pty for the same check and exercises Codex without setsid. The same jobs also install the latest Antigravity CLI, chat with a local Gemini stub, and verify that its installed hook uploads measured usage; see the Antigravity section above. agy stays in its own steps because it is a native binary rather than a Node CLI: it cannot be pinned with the rest and intentionally tracks the latest release, and its chat is headless (agy -p print mode, no TUI). No model API key or external account is needed. This path-filtered workflow is not a required check on unrelated PRs. Each run's summary lists, per platform, the CLI version tested and the result per tool; a failed weekly run opens or updates an issue naming the failing tool and platform.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages