Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Python

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
check:
name: Format & Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Check formatting
run: ruff format --check src tests
- name: Lint
run: ruff check src tests

test:
name: Test Suite
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run tests
run: pytest -v
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Claude Session Analytics

Queryable analytics for Claude Code session logs, exposed as an MCP server.

## Overview

This MCP server replaces the bash script `~/.claude/contrib/parse-session-logs.sh` with a persistent, queryable analytics layer. It parses JSONL session logs from `~/.claude/projects/` and provides:

- **User-centric timeline**: Events across conversations, organized by timestamp
- **Rich querying**: Tool frequency, command breakdown, sequences, permission gaps
- **Persistent storage**: SQLite at `~/.claude/contrib/analytics/data.db`
- **Auto-refresh**: Queries automatically refresh stale data (>5 min old)

## Installation

```bash
make install # Install LaunchAgent + CLI
make uninstall # Remove LaunchAgent + CLI
```

## Development

```bash
make check # Run fmt, lint, test
make dev # Run in dev mode with auto-reload
```

## MCP Tools

| Tool | Purpose |
|------|---------|
| `ingest_logs` | Refresh data from JSONL files |
| `query_tool_frequency` | Tool usage counts |
| `get_status` | Ingestion status + DB stats |
54 changes: 54 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "session-analytics"
version = "0.1.0"
description = "Queryable analytics for Claude Code session logs, exposed as an MCP server"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{ name = "Evan Senter" }
]
keywords = ["mcp", "claude", "analytics", "session-logs"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"fastmcp>=0.1.0",
"uvicorn>=0.30.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.8.0",
]

[project.scripts]
session-analytics = "session_analytics.server:main"

[tool.hatch.build.targets.wheel]
packages = ["src/session_analytics"]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

[tool.ruff]
target-version = "py310"
line-length = 100
src = ["src", "tests"]

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
ignore = ["E501"]
28 changes: 28 additions & 0 deletions src/session_analytics/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""MCP server for Claude Code session analytics."""

from fastmcp import FastMCP

mcp = FastMCP("session-analytics")


def _get_status_impl() -> dict:
"""Get ingestion status and database stats."""
return {
"status": "ok",
"message": "Session analytics server is running",
}


@mcp.tool()
def get_status() -> dict:
"""Get ingestion status and database stats."""
return _get_status_impl()


def main():
"""Run the MCP server."""
mcp.run()


if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Tests for the session analytics server."""

from session_analytics.server import _get_status_impl


def test_get_status():
"""Test that get_status returns expected structure."""
result = _get_status_impl()
assert result["status"] == "ok"
assert "message" in result