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 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 | 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"] diff --git a/src/session_analytics/server.py b/src/session_analytics/server.py new file mode 100644 index 0000000..73390f0 --- /dev/null +++ b/src/session_analytics/server.py @@ -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() diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..43f5478 --- /dev/null +++ b/tests/test_server.py @@ -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