From b0ca3eca534f288cdf6db79f54adab75229263a1 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:01:21 +0000 Subject: [PATCH 1/8] ci: Add Python CI workflow (format, lint, test) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches claude-event-bus CI configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/python.yml | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/python.yml diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..f38bc07 --- /dev/null +++ b/.github/workflows/python.yml @@ -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 From d178d85a85936b4a6ce76a62605cf9fd7154e9d6 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:05:11 +0000 Subject: [PATCH 2/8] chore: Add pyproject.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- pyproject.toml | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a3e01a8 --- /dev/null +++ b/pyproject.toml @@ -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"] From f656591f8ce90403ee1a09c7eef053f8f2406d7f Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:05:26 +0000 Subject: [PATCH 3/8] feat: Add minimal server skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/session_analytics/server.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/session_analytics/server.py diff --git a/src/session_analytics/server.py b/src/session_analytics/server.py new file mode 100644 index 0000000..08bf3a7 --- /dev/null +++ b/src/session_analytics/server.py @@ -0,0 +1,23 @@ +"""MCP server for Claude Code session analytics.""" + +from fastmcp import FastMCP + +mcp = FastMCP("session-analytics") + + +@mcp.tool() +def get_status() -> dict: + """Get ingestion status and database stats.""" + return { + "status": "ok", + "message": "Session analytics server is running", + } + + +def main(): + """Run the MCP server.""" + mcp.run() + + +if __name__ == "__main__": + main() From f7b3ab91c30a2cb464ce08a40a559625f3518556 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:05:27 +0000 Subject: [PATCH 4/8] test: Add basic server test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- tests/test_server.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/test_server.py diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..017ce3e --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,10 @@ +"""Tests for the session analytics server.""" + +from session_analytics.server import get_status + + +def test_get_status(): + """Test that get_status returns expected structure.""" + result = get_status() + assert result["status"] == "ok" + assert "message" in result From 69e7c6479a6a4874b1305e4c6f37f1e8b654012a Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:05:27 +0000 Subject: [PATCH 5/8] chore: Add tests __init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) From 0bd9a15fe560cfa56c28dc5ad82f781db392b3d7 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:07:38 +0000 Subject: [PATCH 6/8] docs: Add README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5369db7 --- /dev/null +++ b/README.md @@ -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 | From 240dadaa6fd43125704fa886093cff178fd3052f Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:09:53 +0000 Subject: [PATCH 7/8] fix: Separate tool impl for testability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/session_analytics/server.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/session_analytics/server.py b/src/session_analytics/server.py index 08bf3a7..73390f0 100644 --- a/src/session_analytics/server.py +++ b/src/session_analytics/server.py @@ -5,8 +5,7 @@ mcp = FastMCP("session-analytics") -@mcp.tool() -def get_status() -> dict: +def _get_status_impl() -> dict: """Get ingestion status and database stats.""" return { "status": "ok", @@ -14,6 +13,12 @@ def get_status() -> dict: } +@mcp.tool() +def get_status() -> dict: + """Get ingestion status and database stats.""" + return _get_status_impl() + + def main(): """Run the MCP server.""" mcp.run() From 803e99f7227c82f56aca8dd5f0e0886982bc77d5 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Wed, 31 Dec 2025 05:09:53 +0000 Subject: [PATCH 8/8] fix: Update test to use impl function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- tests/test_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 017ce3e..43f5478 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,10 +1,10 @@ """Tests for the session analytics server.""" -from session_analytics.server import get_status +from session_analytics.server import _get_status_impl def test_get_status(): """Test that get_status returns expected structure.""" - result = get_status() + result = _get_status_impl() assert result["status"] == "ok" assert "message" in result