ai-lens
v0.8.107
Published
Centralized session analytics for AI coding tools
Readme
AI Lens
Analytics for AI coding sessions. Captures hook events from Claude Code, Cursor, and Codex, normalizes them to a unified format, queues locally, and ships to a centralized server with a web dashboard and MCP integration.
Hook fires → capture.js → normalize → queue.jsonl → sender.js → POST /api/events → server → dashboardDeveloper Setup (client)
Run the init command on each developer machine:
npx -y ai-lens initThis will:
- Detect installed AI tools (Claude Code, Cursor, Codex)
- Copy client files to
~/.ai-lens/client/ - Configure hooks in
~/.claude/settings.jsonand/or~/.cursor/hooks.json - Configure Codex native hooks in project
.codex/hooks.jsonvia--project-hooks(Codex 0.130.x ignores user-layer~/.codex/hooks.json) - Register the MCP server for in-editor analytics (optional)
Re-running is safe — it updates outdated hooks and skips current ones.
Deploying hooks in a specific project (project-level hooks)
To write hooks into the project directory (.cursor/hooks.json and .claude/settings.json) instead of global ~/.cursor/ and ~/.claude/, run from the project root:
npx -y ai-lens init --server https://ai-lens.rantsports.com --no-mcp --project-hooksAdd --use-repo-path to run capture.js directly from the package (repo or npx cache) instead of copying to ~/.ai-lens/client/. Useful when the repo is next to the workspace.
CLI commands
npx ai-lens init # Setup wizard — detect tools, install hooks, configure MCP
npx ai-lens status # Run health checks and generate a diagnostic report
npx ai-lens remove # Remove hooks, client files, and MCP config
npx ai-lens version # Show installed versionCLI options
| Flag | Description |
|------|-------------|
| --server URL | Server URL (default: http://localhost:3000) |
| --yes, -y | Non-interactive mode, accept all defaults |
| --no-mcp | Skip MCP server registration |
| --mcp-scope SCOPE | MCP scope: user (default), local, or project |
| --projects LIST | Comma-separated project paths to monitor (default: all) |
| --project-hooks | Write hooks into project directory instead of global config |
| --use-repo-path | Run capture.js from package path instead of copying to ~/.ai-lens/client/ |
Environment variables (client)
# In your shell profile (~/.zshrc, ~/.bashrc)
export AI_LENS_SERVER_URL=https://ai-lens.rantsports.com
export AI_LENS_PROJECTS="~/meta/, ~/meta-cursor/" # optional, default: allClaude Code — ~/.claude/settings.json (hooks section):
{
"hooks": {
"SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }]
}
}Cursor — ~/.cursor/hooks.json:
{
"version": 1,
"hooks": {
"sessionStart": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"beforeSubmitPrompt": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"postToolUse": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"afterFileEdit": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"afterShellExecution": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"afterMCPExecution": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"stop": [{ "command": "node ~/.ai-lens/client/capture.js" }],
"sessionEnd": [{ "command": "node ~/.ai-lens/client/capture.js" }]
}
}Codex — <project>/.codex/hooks.json (use npx -y ai-lens init --project-hooks; user-layer ~/.codex/hooks.json is silently ignored by Codex 0.130.x):
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PermissionRequest": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PreCompact": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"PostCompact": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"SubagentStart": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"SubagentStop": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "node ~/.ai-lens/client/capture.js" }] }]
}
}All 10 native Codex events are installed: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, PreCompact, PostCompact, SubagentStart, SubagentStop, Stop (the last two are 0.135 additions). Pre-trust hashes match Codex's command_hook_hash bit-for-bit for all 10 — verified against Codex 0.135: with the full set pre-trusted, no "needs review" banner appears and codex leaves every [hooks.state.*] entry untouched.
The matcher field is intentionally omitted — Codex coerces matcher to None for UserPromptSubmit/Stop before hashing, so writing matcher: "" leaves those entries stuck in Modified trust status.
Init also pre-trusts each hook by writing [hooks.state."<hooks.json-path>:<event>:<group>:<handler>"] blocks into ~/.codex/config.toml with a trusted_hash matching what Codex computes. The state key is the bare hooks.json path (no file: scheme prefix) — that's the exact key codex-rs/hooks/src/lib.rs::hook_key looks up. (An earlier file: prefix never matched, so the pre-trust was silently ignored and hooks never fired.)
codex exec captures events end-to-end with no interactive step — the pre-trust is all it needs (verified on Codex 0.135).
Interactive codex (TUI) — when the full hook set is pre-trusted with matching hashes, Codex 0.135 shows no review banner and fires hooks immediately. If a banner does appear (e.g. partial/older trust state), pick "Trust all and continue" on Codex 0.131+/0.135 (one keystroke for all hooks); after that, every interactive session fires hooks automatically. Pre-trust still earns its keep: without it the hooks show as Modified and re-prompt on every command change.
Server Setup
Docker (production)
docker compose up -dStarts three containers:
| Container | Image | Purpose |
|-----------|-------|---------|
| app | ai-lens/app | API server + web dashboard (port 3000) |
| postgres | postgres:16-alpine | PostgreSQL 16 database |
| analyzer | ai-lens/analyzer | Background session analyzer (needs claude login) |
Dashboard: https://ai-lens.rantsports.com
Images are stored in ECR (267996409571.dkr.ecr.eu-north-1.amazonaws.com/ai-lens/) and mirrored to GHCR (ghcr.io/r-ms/ai-lens/) on every push to main.
Environment variables (server)
| Variable | Default | Description |
|----------|---------|-------------|
| PORT | 3000 | Server port |
| DATABASE_URL | (required) | PostgreSQL connection string |
| POSTGRES_PASSWORD | ailens | PostgreSQL password (docker compose) |
| ANALYSIS_INTERVAL | 60 | Seconds between analysis runs |
| AI_LENS_ADMIN_SECRET | (none) | Admin secret for auth token management |
| OPENAI_API_KEY | (none) | OpenAI API key for FAISS vector search; text search works without it |
| TEAMS_CONFIG | (none) | JSON config for team definitions |
| CORS_ALLOWED_ORIGINS | (none) | Allowed CORS origins |
Auth0 SSO
| Variable | Description |
|----------|-------------|
| AUTH0_DOMAIN | Auth0 tenant domain |
| AUTH0_CLIENT_ID | Auth0 SPA client ID |
| AUTH0_AUDIENCE | Auth0 API audience identifier |
| AUTH0_ALLOWED_DOMAIN | Restrict login to a specific email domain |
| AUTH0_CLI_CLIENT_ID | Auth0 Native app client ID for device code flow |
| MCP_SERVER_URL | Public server URL for MCP OAuth callbacks |
Without Auth0, the server uses git email headers for identity (personal mode).
Local development
docker compose up postgres -d
npm install
DATABASE_URL=postgresql://ailens:ailens@localhost:5432/ailens npm startDashboard
React + TypeScript SPA with:
- Organization-wide KPIs and adoption trends
- Team and developer breakdowns
- Session timelines with tool usage
- AI-generated session and team analyses
- Token usage by model
- MCP server and skill distribution
- Knowledge base and recurring problems
cd dashboard
npm install
npm run dev # Vite dev server with HMR (proxies API to localhost:3000)
npm run build # Production build (served by Express as static files)Tech: Vite, Tailwind CSS, Nivo charts, TanStack Query, react-router-dom.
MCP Tools
When MCP is enabled during npx ai-lens init, these tools become available inside Claude Code and Cursor:
| Tool | Description |
|------|-------------|
| who_am_i | Identify yourself by git email — returns your developer profile and team(s) |
| get_overview | Organization-wide KPIs: active developers, adoption rate, AI hours, MCP and skill distribution |
| list_teams | List all teams with member counts, adoption rate, and AI hours |
| get_team | Team detail: KPIs, members, tasks, activity trend, MCP and skill distribution |
| get_team_analysis | AI-generated team analysis: achievements, recurring problems, recommendations |
| get_developer | Developer profile: sessions, AI hours, tasks, MCP and skill usage, team comparison |
| get_mcp_distribution | MCP server usage across the organization |
| get_chain | Session chain with compact event timeline, plan mode segments, and timing |
| get_events | Full event data for specific event IDs |
| get_chain_analysis | AI-generated chain analysis: tasks, problems, tool errors, unanswered questions |
| request_analysis | Manually trigger analysis for a specific session chain |
| get_token_usage | Token usage statistics grouped by model (input/output/cache tokens) |
| knowhow_search | Search the team knowledge base built from session analyses |
| knowhow_update | Add or update a knowledge base entry |
| export_developer_tips | Export personalized tips as a Markdown document |
| search | Natural language search across sessions, tasks, and projects |
API
POST /api/events
Batch insert events. Deduplicates by event_id (ON CONFLICT DO NOTHING) — safe to re-send.
Headers: X-Developer-Git-Email, X-Developer-Name, X-Auth-Token
Body: [{ source, session_id, type, project_path, timestamp, data, raw, event_id }]
Response: { received, skipped, deduplicated }GET /api/sessions
List sessions. Query params: developer_id, source, days (default 30).
GET /api/sessions/:id
Session detail with all events, plan segments, and metadata.
GET /api/developers
List all developers.
GET /api/dashboard/*
Aggregate endpoints for dashboard charts: overview, teams, developers, tokens, MCP distribution, developer activity, knowledge base, problems, company/team/developer analyses.
Event Types
| Type | Source | Description |
|------|--------|-------------|
| SessionStart | All | Session opened |
| SessionEnd | All | Session closed |
| UserPromptSubmit | All | User sent a prompt |
| PostToolUse | All | Tool execution completed |
| PostToolUseFailure | All | Tool execution failed |
| Stop | All | Agent stopped |
| PreCompact | All | Context compaction triggered |
| PlanModeStart | Claude Code | Entered plan mode |
| PlanModeEnd | Claude Code | Exited plan mode (plan content in raw payload) |
| SubagentStart | All | Subagent spawned |
| SubagentStop | All | Subagent finished |
| FileEdit | Cursor | File edited |
| ShellExecution | Cursor | Shell command executed |
| MCPExecution | Cursor | MCP tool executed |
| AgentResponse | Cursor | Agent response (includes token usage in raw payload) |
| AgentThought | Cursor | Agent reasoning |
Supported Tools
| Tool | Hook mechanism |
|------|---------------|
| Claude Code | Hooks via ~/.claude/settings.json |
| Cursor | Hooks via ~/.cursor/hooks.json |
| Codex | Native hooks via project .codex/hooks.json (Codex 0.130.x ignores user-layer ~/.codex/hooks.json) |
Client Data
Stored in ~/.ai-lens/:
| File | Purpose |
|------|---------|
| client/ | Installed client files (capture.js, sender.js, config.js) |
| config.json | Server URL, auth token, project list |
| queue.jsonl | Pending events |
| queue.sending.jsonl | Events being sent (atomic rename as mutex) |
| sender.log | Sender activity log |
| capture.log | Capture drop log (normalization failures, write errors) |
| session-paths.json | Session-to-project path cache |
Development
npm test # Run all tests (vitest, 683 tests)
npm run test:watch # Watch mode
npm run dev:dashboard # Dashboard dev serverTests require PostgreSQL — set DATABASE_URL or use docker compose up postgres -d (test DB ailens_test is created automatically).
Architecture decisions are recorded under docs/adr/ — e.g. ADR 0001 — Canonical metric predicates.
Deployment
GitLab CI (.gitlab-ci.yml) on push to main:
- build-app — builds
ai-lens/appDocker image, pushes to ECR + GHCR - build-analyzer — builds
ai-lens/analyzerDocker image, pushes to ECR + GHCR - deploy — zero-downtime rolling deploy to production (scale up new container, health check, remove old)
Build jobs trigger only when relevant files change (Dockerfile, server/, dashboard/, etc.).
Requirements
- Node.js 20+
- Docker + Docker Compose (for production deployment)
- PostgreSQL 16 (for local development without Docker)
