From 0136f2c1528647188fe61a3fe9fbe13c481443cd Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Sat, 24 Jan 2026 20:44:54 +0000 Subject: [PATCH 1/6] chore: Rename from claude-session-analytics to agent-session-analytics - Rename GitHub repo via gh repo rename - Update package name in pyproject.toml - Update version() calls in __init__.py and server.py - Rename service files (LaunchAgent plist, systemd service) - Update all script references - Update Makefile service names - Update test comments This aligns with the agent-event-bus rename. Co-Authored-By: Claude Opus 4.5 --- Makefile | 8 ++++---- pyproject.toml | 2 +- ...alytics.service => agent-session-analytics.service} | 0 ...st => com.evansenter.agent-session-analytics.plist} | 2 +- scripts/dev.sh | 2 +- scripts/install-launchagent.sh | 6 +++--- scripts/install-systemd.sh | 6 +++--- scripts/uninstall-launchagent.sh | 4 ++-- scripts/uninstall-systemd.sh | 4 ++-- src/session_analytics/__init__.py | 4 ++-- src/session_analytics/server.py | 2 +- tests/__init__.py | 2 +- tests/test_ingest.py | 10 +++++----- 13 files changed, 26 insertions(+), 26 deletions(-) rename scripts/{claude-session-analytics.service => agent-session-analytics.service} (100%) rename scripts/{com.evansenter.claude-session-analytics.plist => com.evansenter.agent-session-analytics.plist} (94%) diff --git a/Makefile b/Makefile index f4739fe..428a6a9 100644 --- a/Makefile +++ b/Makefile @@ -64,13 +64,13 @@ install: venv # Restart the service (pick up code changes) restart: @if [ "$$(uname)" = "Darwin" ]; then \ - PLIST="$$HOME/Library/LaunchAgents/com.evansenter.claude-session-analytics.plist"; \ + PLIST="$$HOME/Library/LaunchAgents/com.evansenter.agent-session-analytics.plist"; \ if [ -f "$$PLIST" ]; then \ echo "Restarting session-analytics..."; \ launchctl unload "$$PLIST" 2>/dev/null || true; \ launchctl load "$$PLIST"; \ sleep 1; \ - if launchctl list | grep -q "com.evansenter.claude-session-analytics"; then \ + if launchctl list | grep -q "com.evansenter.agent-session-analytics"; then \ echo "Service restarted successfully"; \ else \ echo "Error: Service failed to start. Check ~/.claude/session-analytics.err"; \ @@ -82,9 +82,9 @@ restart: fi; \ else \ echo "Restarting session-analytics..."; \ - systemctl --user restart claude-session-analytics; \ + systemctl --user restart agent-session-analytics; \ sleep 1; \ - if systemctl --user is-active claude-session-analytics &>/dev/null; then \ + if systemctl --user is-active agent-session-analytics &>/dev/null; then \ echo "Service restarted successfully"; \ else \ echo "Error: Service failed to start. Check ~/.claude/session-analytics.err"; \ diff --git a/pyproject.toml b/pyproject.toml index cb3aabc..152d0c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "claude-session-analytics" +name = "agent-session-analytics" version = "0.1.0" description = "MCP server for queryable analytics on Claude Code session logs" readme = "README.md" diff --git a/scripts/claude-session-analytics.service b/scripts/agent-session-analytics.service similarity index 100% rename from scripts/claude-session-analytics.service rename to scripts/agent-session-analytics.service diff --git a/scripts/com.evansenter.claude-session-analytics.plist b/scripts/com.evansenter.agent-session-analytics.plist similarity index 94% rename from scripts/com.evansenter.claude-session-analytics.plist rename to scripts/com.evansenter.agent-session-analytics.plist index d8421b0..9889c64 100644 --- a/scripts/com.evansenter.claude-session-analytics.plist +++ b/scripts/com.evansenter.agent-session-analytics.plist @@ -3,7 +3,7 @@ Label - com.evansenter.claude-session-analytics + com.evansenter.agent-session-analytics ProgramArguments diff --git a/scripts/dev.sh b/scripts/dev.sh index c86e1f3..1b4b626 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -3,7 +3,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -LABEL="com.evansenter.claude-session-analytics" +LABEL="com.evansenter.agent-session-analytics" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" cd "$PROJECT_DIR" diff --git a/scripts/install-launchagent.sh b/scripts/install-launchagent.sh index 4a40f8f..b1feea5 100755 --- a/scripts/install-launchagent.sh +++ b/scripts/install-launchagent.sh @@ -6,9 +6,9 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" VENV_PYTHON="$PROJECT_DIR/.venv/bin/python" -PLIST_TEMPLATE="$SCRIPT_DIR/com.evansenter.claude-session-analytics.plist" -PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.claude-session-analytics.plist" -LABEL="com.evansenter.claude-session-analytics" +PLIST_TEMPLATE="$SCRIPT_DIR/com.evansenter.agent-session-analytics.plist" +PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.agent-session-analytics.plist" +LABEL="com.evansenter.agent-session-analytics" # Check venv exists if [[ ! -f "$VENV_PYTHON" ]]; then diff --git a/scripts/install-systemd.sh b/scripts/install-systemd.sh index 8578205..8e00f5f 100755 --- a/scripts/install-systemd.sh +++ b/scripts/install-systemd.sh @@ -6,10 +6,10 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" VENV_PYTHON="$PROJECT_DIR/.venv/bin/python" -SERVICE_TEMPLATE="$SCRIPT_DIR/claude-session-analytics.service" +SERVICE_TEMPLATE="$SCRIPT_DIR/agent-session-analytics.service" SERVICE_DIR="$HOME/.config/systemd/user" -SERVICE_DEST="$SERVICE_DIR/claude-session-analytics.service" -SERVICE_NAME="claude-session-analytics" +SERVICE_DEST="$SERVICE_DIR/agent-session-analytics.service" +SERVICE_NAME="agent-session-analytics" # Check venv exists if [[ ! -f "$VENV_PYTHON" ]]; then diff --git a/scripts/uninstall-launchagent.sh b/scripts/uninstall-launchagent.sh index e9693db..9316a5b 100755 --- a/scripts/uninstall-launchagent.sh +++ b/scripts/uninstall-launchagent.sh @@ -3,8 +3,8 @@ set -e -PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.claude-session-analytics.plist" -LABEL="com.evansenter.claude-session-analytics" +PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.agent-session-analytics.plist" +LABEL="com.evansenter.agent-session-analytics" if [[ ! -f "$PLIST_DEST" ]]; then echo "LaunchAgent not installed." diff --git a/scripts/uninstall-systemd.sh b/scripts/uninstall-systemd.sh index 9985d79..6092e94 100755 --- a/scripts/uninstall-systemd.sh +++ b/scripts/uninstall-systemd.sh @@ -4,8 +4,8 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SERVICE_DEST="$HOME/.config/systemd/user/claude-session-analytics.service" -SERVICE_NAME="claude-session-analytics" +SERVICE_DEST="$HOME/.config/systemd/user/agent-session-analytics.service" +SERVICE_NAME="agent-session-analytics" # Stop and disable service if running if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then diff --git a/src/session_analytics/__init__.py b/src/session_analytics/__init__.py index 7a29f7a..792e9bb 100644 --- a/src/session_analytics/__init__.py +++ b/src/session_analytics/__init__.py @@ -1,9 +1,9 @@ -"""Claude Session Analytics - MCP server for queryable session log analytics.""" +"""Agent Session Analytics - MCP server for queryable session log analytics.""" from importlib.metadata import version try: - __version__ = version("claude-session-analytics") + __version__ = version("agent-session-analytics") except Exception: __version__ = "0.1.0" # Fallback for development diff --git a/src/session_analytics/server.py b/src/session_analytics/server.py index e6e1626..2bf36d6 100644 --- a/src/session_analytics/server.py +++ b/src/session_analytics/server.py @@ -11,7 +11,7 @@ # Read version from package metadata try: - __version__ = version("claude-session-analytics") + __version__ = version("agent-session-analytics") except Exception: __version__ = "0.1.0" # Fallback for development diff --git a/tests/__init__.py b/tests/__init__.py index b76b24c..e54f3e6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for claude-session-analytics.""" +"""Tests for agent-session-analytics.""" diff --git a/tests/test_ingest.py b/tests/test_ingest.py index 68a9953..e336d3b 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -1523,12 +1523,12 @@ def test_ambiguous_path_finds_existing(self, tmp_path): assert result == tmp_path / "a-b" / "c" def test_real_world_pattern(self, tmp_path): - """Pattern like claude-session-analytics decodes when directory exists.""" - # Simulate: /projects/claude-session-analytics - (tmp_path / "projects" / "claude-session-analytics").mkdir(parents=True) - encoded = str(tmp_path / "projects" / "claude-session-analytics").replace("/", "-") + """Pattern like agent-session-analytics decodes when directory exists.""" + # Simulate: /projects/agent-session-analytics + (tmp_path / "projects" / "agent-session-analytics").mkdir(parents=True) + encoded = str(tmp_path / "projects" / "agent-session-analytics").replace("/", "-") result = decode_project_path(encoded) - assert result == tmp_path / "projects" / "claude-session-analytics" + assert result == tmp_path / "projects" / "agent-session-analytics" def test_file_not_directory_returns_none(self, tmp_path): """Path pointing to a file (not directory) returns None.""" From 9c4c9dfb3590ce49b0fac5e927d651f743a09c2e Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Sat, 24 Jan 2026 20:50:16 +0000 Subject: [PATCH 2/6] chore: Complete rename - update remaining Claude references Address review feedback: - Regenerate uv.lock with new package name - Update README title to "Agent Session Analytics" - Update CLI description - Update server startup message - Update systemd service description - Update event-bus link in README Co-Authored-By: Claude Opus 4.5 --- README.md | 4 +- scripts/agent-session-analytics.service | 2 +- src/session_analytics/cli.py | 2 +- src/session_analytics/server.py | 2 +- uv.lock | 52 ++++++++++++------------- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a57dac5..822596f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Claude Session Analytics +# Agent Session Analytics MCP server and CLI for queryable analytics on Claude Code session logs. -**Related**: [claude-event-bus](https://github.com/evansenter/claude-event-bus) shares design patterns with this project. +**Related**: [agent-event-bus](https://github.com/evansenter/agent-event-bus) shares design patterns with this project. ## What It Does diff --git a/scripts/agent-session-analytics.service b/scripts/agent-session-analytics.service index 429dcf0..061cc07 100644 --- a/scripts/agent-session-analytics.service +++ b/scripts/agent-session-analytics.service @@ -1,5 +1,5 @@ [Unit] -Description=Claude Session Analytics - MCP server for session log analysis +Description=Agent Session Analytics - MCP server for session log analysis After=network.target [Service] diff --git a/src/session_analytics/cli.py b/src/session_analytics/cli.py index 0bfa2d9..f630492 100644 --- a/src/session_analytics/cli.py +++ b/src/session_analytics/cli.py @@ -1512,7 +1512,7 @@ def main(): Data location: ~/.claude/contrib/analytics/data.db """ parser = argparse.ArgumentParser( - description="Claude Session Analytics CLI - Analyze your Claude Code usage patterns", + description="Agent Session Analytics CLI - Analyze your Claude Code usage patterns", prog="session-analytics-cli", epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/src/session_analytics/server.py b/src/session_analytics/server.py index 2bf36d6..b7a15cc 100644 --- a/src/session_analytics/server.py +++ b/src/session_analytics/server.py @@ -730,7 +730,7 @@ def main(): port = int(os.environ.get("PORT", 8081)) host = os.environ.get("HOST", "127.0.0.1") - print(f"Starting Claude Session Analytics on {host}:{port}") + print(f"Starting Agent Session Analytics on {host}:{port}") print( f"Add to Claude Code: claude mcp add --transport http --scope user session-analytics http://{host}:{port}/mcp" ) diff --git a/uv.lock b/uv.lock index 2ec66d4..de2acb4 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,32 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "agent-session-analytics" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "fastmcp" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastmcp", specifier = ">=0.1.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, + { name = "uvicorn", specifier = ">=0.30.0" }, +] +provides-extras = ["dev"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -271,32 +297,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "claude-session-analytics" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "fastmcp" }, - { name = "uvicorn" }, -] - -[package.optional-dependencies] -dev = [ - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "fastmcp", specifier = ">=0.1.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, - { name = "uvicorn", specifier = ">=0.30.0" }, -] -provides-extras = ["dev"] - [[package]] name = "click" version = "8.3.1" From 602ba476afe3da4cfae946a00ef4c0a276f47b0e Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Sat, 24 Jan 2026 21:21:51 +0000 Subject: [PATCH 3/6] chore: Complete rename to agent-session-analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename Python package: session_analytics → agent_session_analytics - Rename CLI: session-analytics-cli → agent-session-analytics-cli - Rename MCP server: session-analytics → agent-session-analytics - Update resource URI: agent-session-analytics://guide - Update data path: ~/.claude/contrib/agent-session-analytics/ - Rename env vars: AGENT_SESSION_ANALYTICS_* - Update event-bus path: ~/.claude/contrib/agent-event-bus/ All 384 tests passing. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 6 +- Makefile | 12 +-- README.md | 72 +++++++------- docs/SCHEMA.md | 4 +- pyproject.toml | 6 +- scripts/agent-session-analytics.service | 6 +- ...m.evansenter.agent-session-analytics.plist | 6 +- scripts/dev.sh | 10 +- scripts/install-cli.sh | 14 +-- scripts/install-launchagent.sh | 14 +-- scripts/install-systemd.sh | 10 +- scripts/uninstall-cli.sh | 8 +- scripts/uninstall-launchagent.sh | 6 +- scripts/uninstall-systemd.sh | 4 +- .../__init__.py | 4 +- .../bus_ingest.py | 10 +- .../cli.py | 70 +++++++------- .../guide.md | 10 +- .../ingest.py | 4 +- .../patterns.py | 6 +- .../queries.py | 6 +- .../server.py | 16 ++-- .../storage.py | 6 +- tests/conftest.py | 4 +- tests/test_cli.py | 94 +++++++++--------- tests/test_ingest.py | 58 +++++------ tests/test_patterns.py | 48 +++++----- tests/test_queries.py | 96 +++++++++---------- tests/test_server.py | 2 +- tests/test_smoke_real_data.py | 2 +- tests/test_storage.py | 2 +- 31 files changed, 308 insertions(+), 308 deletions(-) rename src/{session_analytics => agent_session_analytics}/__init__.py (81%) rename src/{session_analytics => agent_session_analytics}/bus_ingest.py (92%) rename src/{session_analytics => agent_session_analytics}/cli.py (97%) rename src/{session_analytics => agent_session_analytics}/guide.md (96%) rename src/{session_analytics => agent_session_analytics}/ingest.py (99%) rename src/{session_analytics => agent_session_analytics}/patterns.py (99%) rename src/{session_analytics => agent_session_analytics}/queries.py (99%) rename src/{session_analytics => agent_session_analytics}/server.py (97%) rename src/{session_analytics => agent_session_analytics}/storage.py (99%) diff --git a/CLAUDE.md b/CLAUDE.md index ff9bb3e..a3da538 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Queryable analytics for Claude Code session logs, exposed as an MCP server and CLI. -**API Reference**: `session-analytics-cli --help` or `src/session_analytics/guide.md` (MCP resource: `session-analytics://guide`). +**API Reference**: `agent-session-analytics-cli --help` or `src/agent_session_analytics/guide.md` (MCP resource: `agent-session-analytics://guide`). **Schema Design**: See [docs/SCHEMA.md](docs/SCHEMA.md) for database tables, indexes, and migration history. @@ -10,7 +10,7 @@ Queryable analytics for Claude Code session logs, exposed as an MCP server and C ## ⚠️ DATABASE PROTECTION -**The database at `~/.claude/contrib/analytics/data.db` contains irreplaceable historical data.** +**The database at `~/.claude/contrib/agent-session-analytics/data.db` contains irreplaceable historical data.** ### NEVER: - Delete the database file @@ -19,7 +19,7 @@ Queryable analytics for Claude Code session logs, exposed as an MCP server and C ### Before schema changes: ```bash -cp ~/.claude/contrib/analytics/data.db ~/.claude/contrib/analytics/data.db.backup-$(date +%Y%m%d-%H%M%S) +cp ~/.claude/contrib/agent-session-analytics/data.db ~/.claude/contrib/agent-session-analytics/data.db.backup-$(date +%Y%m%d-%H%M%S) ``` ### When adding new columns: diff --git a/Makefile b/Makefile index 428a6a9..68a4a92 100644 --- a/Makefile +++ b/Makefile @@ -48,12 +48,12 @@ install: venv @echo "Adding to Claude Code..." @CLAUDE_CMD=$$(command -v claude || echo "$$HOME/.local/bin/claude"); \ if [ -x "$$CLAUDE_CMD" ]; then \ - $$CLAUDE_CMD mcp add --transport http --scope user session-analytics http://localhost:8081/mcp 2>/dev/null && \ - echo "Added session-analytics to Claude Code" || \ - echo "session-analytics already configured in Claude Code"; \ + $$CLAUDE_CMD mcp add --transport http --scope user agent-session-analytics http://localhost:8081/mcp 2>/dev/null && \ + echo "Added agent-session-analytics to Claude Code" || \ + echo "agent-session-analytics already configured in Claude Code"; \ else \ echo "Note: claude not found. Run manually:"; \ - echo " claude mcp add --transport http --scope user session-analytics http://localhost:8081/mcp"; \ + echo " claude mcp add --transport http --scope user agent-session-analytics http://localhost:8081/mcp"; \ fi @echo "" @echo "Installation complete!" @@ -73,7 +73,7 @@ restart: if launchctl list | grep -q "com.evansenter.agent-session-analytics"; then \ echo "Service restarted successfully"; \ else \ - echo "Error: Service failed to start. Check ~/.claude/session-analytics.err"; \ + echo "Error: Service failed to start. Check ~/.claude/contrib/agent-session-analytics/agent-session-analytics.err"; \ exit 1; \ fi; \ else \ @@ -87,7 +87,7 @@ restart: if systemctl --user is-active agent-session-analytics &>/dev/null; then \ echo "Service restarted successfully"; \ else \ - echo "Error: Service failed to start. Check ~/.claude/session-analytics.err"; \ + echo "Error: Service failed to start. Check ~/.claude/contrib/agent-session-analytics/agent-session-analytics.err"; \ exit 1; \ fi; \ fi diff --git a/README.md b/README.md index 822596f..5ccb916 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Parses your Claude Code session logs (`~/.claude/projects/**/*.jsonl`) and provi - **Permission gaps** - Commands that should be added to settings.json - **Token usage** - Usage breakdown by day, session, or model - **Session timeline** - Events across conversations with filtering -- **Cross-session insights** - Gotchas, patterns, and learnings from [event-bus](https://github.com/evansenter/claude-event-bus) +- **Cross-session insights** - Gotchas, patterns, and learnings from [event-bus](https://github.com/evansenter/agent-event-bus) Data is stored persistently in SQLite and auto-refreshes when stale (>5 min old). @@ -26,7 +26,7 @@ make install This will: 1. Create a virtual environment and install dependencies -2. Set up a LaunchAgent for auto-start (macOS) +2. Set up a LaunchAgent for auto-start (macOS) or systemd service (Linux) 3. Add the MCP server to Claude Code 4. Install the CLI to your path @@ -34,58 +34,58 @@ This will: ```bash # Status & Ingestion -session-analytics-cli status # Database stats -session-analytics-cli ingest --days 7 # Refresh data from logs +agent-session-analytics-cli status # Database stats +agent-session-analytics-cli ingest --days 7 # Refresh data from logs # Core Analytics -session-analytics-cli frequency # Tool usage (--no-expand to hide breakdowns) -session-analytics-cli commands # Bash command breakdown (--prefix git) -session-analytics-cli sessions # Session metadata and tokens -session-analytics-cli tokens --by day # Token usage (day/session/model) +agent-session-analytics-cli frequency # Tool usage (--no-expand to hide breakdowns) +agent-session-analytics-cli commands # Bash command breakdown (--prefix git) +agent-session-analytics-cli sessions # Session metadata and tokens +agent-session-analytics-cli tokens --by day # Token usage (day/session/model) # Workflow Analysis -session-analytics-cli sequences # Tool chains (--expand for command-level) -session-analytics-cli permissions # Commands needing settings.json -session-analytics-cli insights # Pre-computed patterns for /improve-workflow +agent-session-analytics-cli sequences # Tool chains (--expand for command-level) +agent-session-analytics-cli permissions # Commands needing settings.json +agent-session-analytics-cli insights # Pre-computed patterns for /improve-workflow # File & Project Activity -session-analytics-cli file-activity # File reads/edits/writes -session-analytics-cli languages # Language distribution -session-analytics-cli projects # Activity by project -session-analytics-cli mcp-usage # MCP server/tool usage +agent-session-analytics-cli file-activity # File reads/edits/writes +agent-session-analytics-cli languages # Language distribution +agent-session-analytics-cli projects # Activity by project +agent-session-analytics-cli mcp-usage # MCP server/tool usage # Agent Activity -session-analytics-cli agents # Task subagent activity vs main session +agent-session-analytics-cli agents # Task subagent activity vs main session # Session Analysis -session-analytics-cli signals # Raw session metrics for LLM interpretation -session-analytics-cli classify # Categorize sessions (debug/dev/research) -session-analytics-cli failures # Error patterns and rework detection -session-analytics-cli error-details # Detailed errors with tool parameters -session-analytics-cli trends # Compare usage across time periods -session-analytics-cli handoff # Context summary for session handoff +agent-session-analytics-cli signals # Raw session metrics for LLM interpretation +agent-session-analytics-cli classify # Categorize sessions (debug/dev/research) +agent-session-analytics-cli failures # Error patterns and rework detection +agent-session-analytics-cli error-details # Detailed errors with tool parameters +agent-session-analytics-cli trends # Compare usage across time periods +agent-session-analytics-cli handoff # Context summary for session handoff # User Messages -session-analytics-cli journey # User messages across sessions -session-analytics-cli search # Full-text search on messages +agent-session-analytics-cli journey # User messages across sessions +agent-session-analytics-cli search # Full-text search on messages # Session Relationships -session-analytics-cli parallel # Find simultaneously active sessions -session-analytics-cli related # Find sessions with similar patterns +agent-session-analytics-cli parallel # Find simultaneously active sessions +agent-session-analytics-cli related # Find sessions with similar patterns # Git Integration -session-analytics-cli git-ingest # Import git commit history -session-analytics-cli git-correlate # Link commits to sessions -session-analytics-cli session-commits # Show commits per session +agent-session-analytics-cli git-ingest # Import git commit history +agent-session-analytics-cli git-correlate # Link commits to sessions +agent-session-analytics-cli session-commits # Show commits per session # Event-Bus Integration -session-analytics-cli bus-events # Query cross-session events (gotchas, patterns) +agent-session-analytics-cli bus-events # Query cross-session events (gotchas, patterns) # Pattern Inspection -session-analytics-cli sample-sequences # Sample instances of a pattern with context +agent-session-analytics-cli sample-sequences # Sample instances of a pattern with context # Performance -session-analytics-cli benchmark # Benchmark all MCP tool response times +agent-session-analytics-cli benchmark # Benchmark all MCP tool response times ``` All commands support: @@ -110,7 +110,7 @@ All commands support: | **Git** | `ingest_git_history`, `correlate_git_with_sessions`, `get_session_commits` | | **Event-Bus** | `ingest_bus_events`, `get_bus_events` | -For detailed usage, read the MCP resource `session-analytics://guide` or see [guide.md](src/session_analytics/guide.md). +For detailed usage, read the MCP resource `agent-session-analytics://guide` or see [guide.md](src/agent_session_analytics/guide.md). ## Development @@ -130,9 +130,9 @@ make check ## Data Location -- **Database**: `~/.claude/contrib/analytics/data.db` +- **Database**: `~/.claude/contrib/agent-session-analytics/data.db` - **Logs parsed from**: `~/.claude/projects/**/*.jsonl` -- **Event-bus source**: `~/.claude/contrib/event-bus/data.db` (if [claude-event-bus](https://github.com/evansenter/claude-event-bus) is installed) +- **Event-bus source**: `~/.claude/contrib/agent-event-bus/data.db` (if [agent-event-bus](https://github.com/evansenter/agent-event-bus) is installed) ## How It Works @@ -156,7 +156,7 @@ See `CLAUDE.md` for more details on contributing. ## Related -- [claude-event-bus](https://github.com/evansenter/claude-event-bus) - Cross-session communication for Claude Code +- [agent-event-bus](https://github.com/evansenter/agent-event-bus) - Cross-session communication for Claude Code ## Uninstall diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index 7513ee9..f55fab3 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -1,8 +1,8 @@ # Database Schema Design -This document describes the SQLite database schema for session-analytics. +This document describes the SQLite database schema for agent-session-analytics. -**Location**: `~/.claude/contrib/analytics/data.db` +**Location**: `~/.claude/contrib/agent-session-analytics/data.db` --- diff --git a/pyproject.toml b/pyproject.toml index 152d0c7..a7bdeef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,11 +35,11 @@ dev = [ ] [project.scripts] -session-analytics = "session_analytics.server:main" -session-analytics-cli = "session_analytics.cli:main" +agent-session-analytics = "agent_session_analytics.server:main" +agent-session-analytics-cli = "agent_session_analytics.cli:main" [tool.hatch.build.targets.wheel] -packages = ["src/session_analytics"] +packages = ["src/agent_session_analytics"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/agent-session-analytics.service b/scripts/agent-session-analytics.service index 061cc07..63c10ff 100644 --- a/scripts/agent-session-analytics.service +++ b/scripts/agent-session-analytics.service @@ -6,11 +6,11 @@ After=network.target Type=simple WorkingDirectory=__PROJECT_DIR__ Environment=PYTHONPATH=__PROJECT_DIR__/src -ExecStart=__VENV_PYTHON__ -m session_analytics.server +ExecStart=__VENV_PYTHON__ -m agent_session_analytics.server Restart=always RestartSec=5 -StandardOutput=append:__HOME__/.claude/session-analytics.log -StandardError=append:__HOME__/.claude/session-analytics.err +StandardOutput=append:__HOME__/.claude/contrib/agent-session-analytics/agent-session-analytics.log +StandardError=append:__HOME__/.claude/contrib/agent-session-analytics/agent-session-analytics.err [Install] WantedBy=default.target diff --git a/scripts/com.evansenter.agent-session-analytics.plist b/scripts/com.evansenter.agent-session-analytics.plist index 9889c64..6951b9d 100644 --- a/scripts/com.evansenter.agent-session-analytics.plist +++ b/scripts/com.evansenter.agent-session-analytics.plist @@ -9,7 +9,7 @@ __VENV_PYTHON__ -m - session_analytics.server + agent_session_analytics.server WorkingDirectory @@ -30,10 +30,10 @@ StandardOutPath - __HOME__/.claude/session-analytics.log + __HOME__/.claude/contrib/agent-session-analytics/agent-session-analytics.log StandardErrorPath - __HOME__/.claude/session-analytics.err + __HOME__/.claude/contrib/agent-session-analytics/agent-session-analytics.err ProcessType Background diff --git a/scripts/dev.sh b/scripts/dev.sh index 1b4b626..ee4378f 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -15,7 +15,7 @@ if launchctl list 2>/dev/null | grep -q "$LABEL"; then echo "Stopping LaunchAgent for dev mode..." launchctl unload "$PLIST" 2>/dev/null LAUNCHAGENT_WAS_RUNNING=true - osascript -e 'display notification "Stopped for dev mode" with title "Session Analytics"' 2>/dev/null + osascript -e 'display notification "Stopped for dev mode" with title "Agent Session Analytics"' 2>/dev/null fi # Restart LaunchAgent on exit @@ -24,14 +24,14 @@ cleanup() { echo "" echo "Restarting LaunchAgent..." launchctl load "$PLIST" - osascript -e 'display notification "LaunchAgent restarted" with title "Session Analytics"' 2>/dev/null + osascript -e 'display notification "LaunchAgent restarted" with title "Agent Session Analytics"' 2>/dev/null fi } trap cleanup EXIT -echo "Starting session analytics in dev mode (Ctrl+C to stop)..." -echo "Add to Claude Code: claude mcp add --transport http --scope user session-analytics http://127.0.0.1:8081/mcp" +echo "Starting agent-session-analytics in dev mode (Ctrl+C to stop)..." +echo "Add to Claude Code: claude mcp add --transport http --scope user agent-session-analytics http://127.0.0.1:8081/mcp" echo "" # DEV_MODE enables verbose logging -DEV_MODE=1 uvicorn session_analytics.server:create_app --host 127.0.0.1 --port 8081 --reload --factory +DEV_MODE=1 uvicorn agent_session_analytics.server:create_app --host 127.0.0.1 --port 8081 --reload --factory diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 016911b..bad6dea 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -1,13 +1,13 @@ #!/bin/bash -# Install session-analytics-cli to ~/.local/bin as a symlink +# Install agent-session-analytics-cli to ~/.local/bin as a symlink set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -VENV_CLI="$PROJECT_DIR/.venv/bin/session-analytics-cli" +VENV_CLI="$PROJECT_DIR/.venv/bin/agent-session-analytics-cli" INSTALL_DIR="$HOME/.local/bin" -CLI_PATH="$INSTALL_DIR/session-analytics-cli" +CLI_PATH="$INSTALL_DIR/agent-session-analytics-cli" # Check venv CLI exists if [[ ! -f "$VENV_CLI" ]]; then @@ -23,7 +23,7 @@ mkdir -p "$INSTALL_DIR" if [[ -e "$CLI_PATH" || -L "$CLI_PATH" ]]; then # Skip if already correctly symlinked if [[ -L "$CLI_PATH" && "$(readlink "$CLI_PATH")" == "$VENV_CLI" ]]; then - echo "session-analytics-cli already symlinked correctly" + echo "agent-session-analytics-cli already symlinked correctly" exit 0 fi rm -f "$CLI_PATH" @@ -31,7 +31,7 @@ fi # Create symlink ln -s "$VENV_CLI" "$CLI_PATH" -echo "Installed session-analytics-cli to $CLI_PATH (symlink)" +echo "Installed agent-session-analytics-cli to $CLI_PATH (symlink)" # Check if ~/.local/bin is in PATH if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then @@ -45,8 +45,8 @@ fi # Test it works if "$CLI_PATH" --help > /dev/null 2>&1; then - echo "Verified: session-analytics-cli is working" + echo "Verified: agent-session-analytics-cli is working" else - echo "Warning: session-analytics-cli installed but test failed" + echo "Warning: agent-session-analytics-cli installed but test failed" exit 1 fi diff --git a/scripts/install-launchagent.sh b/scripts/install-launchagent.sh index b1feea5..3fe1dc0 100755 --- a/scripts/install-launchagent.sh +++ b/scripts/install-launchagent.sh @@ -19,7 +19,7 @@ fi # Create LaunchAgents directory if needed mkdir -p "$HOME/Library/LaunchAgents" -mkdir -p "$HOME/.claude" +mkdir -p "$HOME/.claude/contrib/agent-session-analytics" # Stop existing service if running if launchctl list | grep -q "$LABEL"; then @@ -42,9 +42,9 @@ launchctl load "$PLIST_DEST" sleep 1 if launchctl list | grep -q "$LABEL"; then echo "" - echo "Session analytics installed and running!" - echo " Logs: ~/.claude/session-analytics.log" - echo " Errors: ~/.claude/session-analytics.err" + echo "Agent Session Analytics installed and running!" + echo " Logs: ~/.claude/contrib/agent-session-analytics/agent-session-analytics.log" + echo " Errors: ~/.claude/contrib/agent-session-analytics/agent-session-analytics.err" echo "" # Also install CLI for use in hooks/scripts @@ -52,9 +52,9 @@ if launchctl list | grep -q "$LABEL"; then "$SCRIPT_DIR/install-cli.sh" echo "" echo "To uninstall: $SCRIPT_DIR/uninstall-launchagent.sh" - osascript -e 'display notification "LaunchAgent installed and running" with title "Session Analytics"' 2>/dev/null + osascript -e 'display notification "LaunchAgent installed and running" with title "Agent Session Analytics"' 2>/dev/null else - echo "Error: Service failed to start. Check ~/.claude/session-analytics.err" - osascript -e 'display notification "Failed to start - check logs" with title "Session Analytics" sound name "Basso"' 2>/dev/null + echo "Error: Service failed to start. Check ~/.claude/contrib/agent-session-analytics/agent-session-analytics.err" + osascript -e 'display notification "Failed to start - check logs" with title "Agent Session Analytics" sound name "Basso"' 2>/dev/null exit 1 fi diff --git a/scripts/install-systemd.sh b/scripts/install-systemd.sh index 8e00f5f..d5bdf27 100755 --- a/scripts/install-systemd.sh +++ b/scripts/install-systemd.sh @@ -20,7 +20,7 @@ fi # Create directories if needed mkdir -p "$SERVICE_DIR" -mkdir -p "$HOME/.claude/contrib/analytics" +mkdir -p "$HOME/.claude/contrib/agent-session-analytics" # Stop existing service if running if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then @@ -44,9 +44,9 @@ systemctl --user enable --now "$SERVICE_NAME" sleep 1 if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then echo "" - echo "Session analytics installed and running!" - echo " Logs: ~/.claude/session-analytics.log" - echo " Errors: ~/.claude/session-analytics.err" + echo "Agent Session Analytics installed and running!" + echo " Logs: ~/.claude/contrib/agent-session-analytics/agent-session-analytics.log" + echo " Errors: ~/.claude/contrib/agent-session-analytics/agent-session-analytics.err" echo " Status: systemctl --user status $SERVICE_NAME" echo "" @@ -58,6 +58,6 @@ if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then else echo "Error: Service failed to start. Check logs:" echo " journalctl --user -u $SERVICE_NAME" - echo " ~/.claude/session-analytics.err" + echo " ~/.claude/contrib/agent-session-analytics/agent-session-analytics.err" exit 1 fi diff --git a/scripts/uninstall-cli.sh b/scripts/uninstall-cli.sh index 9373764..373ea47 100755 --- a/scripts/uninstall-cli.sh +++ b/scripts/uninstall-cli.sh @@ -1,14 +1,14 @@ #!/bin/bash -# Uninstall session-analytics-cli from ~/.local/bin +# Uninstall agent-session-analytics-cli from ~/.local/bin set -e -CLI_PATH="$HOME/.local/bin/session-analytics-cli" +CLI_PATH="$HOME/.local/bin/agent-session-analytics-cli" if [[ ! -e "$CLI_PATH" && ! -L "$CLI_PATH" ]]; then - echo "session-analytics-cli not installed." + echo "agent-session-analytics-cli not installed." exit 0 fi rm -f "$CLI_PATH" -echo "Removed session-analytics-cli from ~/.local/bin" +echo "Removed agent-session-analytics-cli from ~/.local/bin" diff --git a/scripts/uninstall-launchagent.sh b/scripts/uninstall-launchagent.sh index 9316a5b..352deae 100755 --- a/scripts/uninstall-launchagent.sh +++ b/scripts/uninstall-launchagent.sh @@ -17,12 +17,12 @@ launchctl unload "$PLIST_DEST" 2>/dev/null || true echo "Removing plist..." rm -f "$PLIST_DEST" -echo "Session analytics LaunchAgent uninstalled." +echo "Agent Session Analytics LaunchAgent uninstalled." # Also uninstall CLI SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" "$SCRIPT_DIR/uninstall-cli.sh" echo "" -echo "Note: Data remains at ~/.claude/contrib/analytics/" -osascript -e 'display notification "LaunchAgent uninstalled" with title "Session Analytics"' 2>/dev/null +echo "Note: Data remains at ~/.claude/contrib/agent-session-analytics/" +osascript -e 'display notification "LaunchAgent uninstalled" with title "Agent Session Analytics"' 2>/dev/null diff --git a/scripts/uninstall-systemd.sh b/scripts/uninstall-systemd.sh index 6092e94..ed7f91f 100755 --- a/scripts/uninstall-systemd.sh +++ b/scripts/uninstall-systemd.sh @@ -29,5 +29,5 @@ fi "$SCRIPT_DIR/uninstall-cli.sh" echo "" -echo "Session analytics uninstalled." -echo "Note: Database preserved at ~/.claude/contrib/analytics/data.db" +echo "Agent Session Analytics uninstalled." +echo "Note: Database preserved at ~/.claude/contrib/agent-session-analytics/data.db" diff --git a/src/session_analytics/__init__.py b/src/agent_session_analytics/__init__.py similarity index 81% rename from src/session_analytics/__init__.py rename to src/agent_session_analytics/__init__.py index 792e9bb..119b53d 100644 --- a/src/session_analytics/__init__.py +++ b/src/agent_session_analytics/__init__.py @@ -8,8 +8,8 @@ __version__ = "0.1.0" # Fallback for development # Re-export public API -from session_analytics.queries import build_where_clause, get_cutoff, normalize_datetime -from session_analytics.storage import ( +from agent_session_analytics.queries import build_where_clause, get_cutoff, normalize_datetime +from agent_session_analytics.storage import ( Event, GitCommit, IngestionState, diff --git a/src/session_analytics/bus_ingest.py b/src/agent_session_analytics/bus_ingest.py similarity index 92% rename from src/session_analytics/bus_ingest.py rename to src/agent_session_analytics/bus_ingest.py index e06d008..f6106c6 100644 --- a/src/session_analytics/bus_ingest.py +++ b/src/agent_session_analytics/bus_ingest.py @@ -1,7 +1,7 @@ """Event-bus ingestion for cross-session insights. -Reads events from ~/.claude/contrib/event-bus/data.db and stores them -in session-analytics for queryable cross-session insights. +Reads events from ~/.claude/contrib/agent-event-bus/data.db and stores them +in agent-session-analytics for queryable cross-session insights. """ import logging @@ -9,11 +9,11 @@ from datetime import datetime, timedelta from pathlib import Path -from session_analytics.storage import SQLiteStorage +from agent_session_analytics.storage import SQLiteStorage -logger = logging.getLogger("session-analytics") +logger = logging.getLogger("agent-session-analytics") -EVENT_BUS_DB = Path.home() / ".claude" / "contrib" / "event-bus" / "data.db" +EVENT_BUS_DB = Path.home() / ".claude" / "contrib" / "agent-event-bus" / "data.db" def _extract_repo(channel: str | None) -> str | None: diff --git a/src/session_analytics/cli.py b/src/agent_session_analytics/cli.py similarity index 97% rename from src/session_analytics/cli.py rename to src/agent_session_analytics/cli.py index f630492..af082dc 100644 --- a/src/session_analytics/cli.py +++ b/src/agent_session_analytics/cli.py @@ -6,7 +6,7 @@ import statistics import time -from session_analytics.ingest import ( +from agent_session_analytics.ingest import ( correlate_git_with_sessions, ingest_git_history, ingest_git_history_all_projects, @@ -15,7 +15,7 @@ # Note: correlate_git_with_sessions and ingest_git_history_all_projects are kept # for CLI backward compatibility, though MCP now consolidates them into ingest_git_history -from session_analytics.patterns import ( +from agent_session_analytics.patterns import ( analyze_failures, analyze_trends, compute_permission_gaps, @@ -24,7 +24,7 @@ get_session_signals, sample_sequences, ) -from session_analytics.queries import ( +from agent_session_analytics.queries import ( analyze_pre_compaction_patterns, classify_sessions, detect_parallel_sessions, @@ -47,7 +47,7 @@ query_tokens, query_tool_frequency, ) -from session_analytics.storage import SQLiteStorage +from agent_session_analytics.storage import SQLiteStorage # Formatter registry: list of (predicate, formatter) tuples # Each predicate checks if this formatter can handle the data @@ -957,7 +957,7 @@ def cmd_bus_events(args): RFC #54: Shows events from event-bus (gotchas, patterns, help, etc.). """ - from session_analytics.bus_ingest import ingest_bus_events + from agent_session_analytics.bus_ingest import ingest_bus_events storage = SQLiteStorage() # Ingest latest events before querying @@ -1346,73 +1346,73 @@ def cmd_benchmark(args): Note: When adding new MCP tools, add them to the tool_functions dict below. """ - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( analyze_failures as patterns_analyze_failures, ) - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( analyze_trends as patterns_analyze_trends, ) - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( compute_permission_gaps as patterns_compute_permission_gaps, ) - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( compute_sequence_patterns as patterns_compute_sequence_patterns, ) - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( get_insights as patterns_get_insights, ) - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( get_session_signals as patterns_get_session_signals, ) - from session_analytics.patterns import ( + from agent_session_analytics.patterns import ( sample_sequences as patterns_sample_sequences, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( classify_sessions as queries_classify_sessions, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( detect_parallel_sessions as queries_detect_parallel_sessions, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( get_compaction_events as queries_get_compaction_events, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( get_handoff_context as queries_get_handoff_context, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( get_large_tool_results as queries_get_large_tool_results, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( get_session_efficiency as queries_get_session_efficiency, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( get_user_journey as queries_get_user_journey, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_agent_activity as queries_query_agent_activity, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_error_details as queries_query_error_details, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_file_activity as queries_query_file_activity, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_mcp_usage as queries_query_mcp_usage, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_projects as queries_query_projects, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_sessions as queries_query_sessions, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_timeline as queries_query_timeline, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_tokens as queries_query_tokens, ) - from session_analytics.queries import ( + from agent_session_analytics.queries import ( query_tool_frequency as queries_query_tool_frequency, ) @@ -1502,18 +1502,18 @@ def main(): """CLI entry point.""" epilog = """ Examples: - session-analytics-cli status # Database stats - session-analytics-cli frequency --days 30 # Tool usage last 30 days - session-analytics-cli commands --prefix git # Git commands only - session-analytics-cli tokens --by model # Token usage by model - session-analytics-cli permissions # Commands needing settings.json + agent-session-analytics-cli status # Database stats + agent-session-analytics-cli frequency --days 30 # Tool usage last 30 days + agent-session-analytics-cli commands --prefix git # Git commands only + agent-session-analytics-cli tokens --by model # Token usage by model + agent-session-analytics-cli permissions # Commands needing settings.json All commands support --json for machine-readable output. -Data location: ~/.claude/contrib/analytics/data.db +Data location: ~/.claude/contrib/agent-session-analytics/data.db """ parser = argparse.ArgumentParser( description="Agent Session Analytics CLI - Analyze your Claude Code usage patterns", - prog="session-analytics-cli", + prog="agent-session-analytics-cli", epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter, ) diff --git a/src/session_analytics/guide.md b/src/agent_session_analytics/guide.md similarity index 96% rename from src/session_analytics/guide.md rename to src/agent_session_analytics/guide.md index 833a3a5..f9e3b60 100644 --- a/src/session_analytics/guide.md +++ b/src/agent_session_analytics/guide.md @@ -1,10 +1,10 @@ -# Session Analytics Usage Guide +# Agent Session Analytics Usage Guide -> **Tip:** Read this guide via the MCP resource `session-analytics://guide` for usage patterns and best practices. +> **Tip:** Read this guide via the MCP resource `agent-session-analytics://guide` for usage patterns and best practices. ## What is this? -Session Analytics provides queryable analytics on Claude Code session logs. It parses +Agent Session Analytics provides queryable analytics on Claude Code session logs. It parses the JSONL files from `~/.claude/projects/` and stores them in SQLite for fast querying. Use it to understand your Claude Code usage patterns, find workflow improvements, and identify permission gaps. @@ -276,13 +276,13 @@ get_session_commits(session_id="abc") - **Day filters**: `days=7` for recent trends, `days=30` for patterns. - **Permission gaps**: Compare against `~/.claude/settings.json`. Higher `min_count` = less noise. - **Sequences**: `length=3` finds complex patterns but needs more data. -- **CLI parity**: `session-analytics-cli` mirrors all MCP tools for terminal use. +- **CLI parity**: `agent-session-analytics-cli` mirrors all MCP tools for terminal use. ## Data | Item | Path | |------|------| -| Database | `~/.claude/contrib/analytics/data.db` | +| Database | `~/.claude/contrib/agent-session-analytics/data.db` | | Source logs | `~/.claude/projects/**/*.jsonl` | Ingestion is incremental - only changed files are re-parsed. diff --git a/src/session_analytics/ingest.py b/src/agent_session_analytics/ingest.py similarity index 99% rename from src/session_analytics/ingest.py rename to src/agent_session_analytics/ingest.py index 909dbe6..bfc691d 100644 --- a/src/session_analytics/ingest.py +++ b/src/agent_session_analytics/ingest.py @@ -8,8 +8,8 @@ from datetime import datetime, timedelta from pathlib import Path -from session_analytics.queries import get_cutoff, normalize_datetime -from session_analytics.storage import Event, GitCommit, IngestionState, Session, SQLiteStorage +from agent_session_analytics.queries import get_cutoff, normalize_datetime +from agent_session_analytics.storage import Event, GitCommit, IngestionState, Session, SQLiteStorage logger = logging.getLogger("session-analytics") diff --git a/src/session_analytics/patterns.py b/src/agent_session_analytics/patterns.py similarity index 99% rename from src/session_analytics/patterns.py rename to src/agent_session_analytics/patterns.py index 4f4fa58..2033d22 100644 --- a/src/session_analytics/patterns.py +++ b/src/agent_session_analytics/patterns.py @@ -8,8 +8,8 @@ from datetime import datetime, timedelta from pathlib import Path -from session_analytics.queries import get_cutoff -from session_analytics.storage import Pattern, SQLiteStorage +from agent_session_analytics.queries import get_cutoff +from agent_session_analytics.storage import Pattern, SQLiteStorage logger = logging.getLogger("session-analytics") @@ -987,7 +987,7 @@ def get_insights( insights["summary"]["has_failure_analysis"] = False # Session classification summary (Phase 5) - import here to avoid circular - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions try: classification = classify_sessions(storage, days=days) diff --git a/src/session_analytics/queries.py b/src/agent_session_analytics/queries.py similarity index 99% rename from src/session_analytics/queries.py rename to src/agent_session_analytics/queries.py index 3cde9ff..3f0374a 100644 --- a/src/session_analytics/queries.py +++ b/src/agent_session_analytics/queries.py @@ -5,7 +5,7 @@ import re from datetime import datetime, timedelta -from session_analytics.storage import SQLiteStorage +from agent_session_analytics.storage import SQLiteStorage def _format_timestamp(ts) -> str | None: @@ -110,14 +110,14 @@ def ensure_fresh_data( True if data was refreshed, False if data was fresh """ if force: - from session_analytics.ingest import ingest_logs + from agent_session_analytics.ingest import ingest_logs ingest_logs(storage, days=days, project=project) return True last_ingest = storage.get_last_ingestion_time() if last_ingest is None or (datetime.now() - last_ingest) > timedelta(minutes=max_age_minutes): - from session_analytics.ingest import ingest_logs + from agent_session_analytics.ingest import ingest_logs ingest_logs(storage, days=days, project=project) return True diff --git a/src/session_analytics/server.py b/src/agent_session_analytics/server.py similarity index 97% rename from src/session_analytics/server.py rename to src/agent_session_analytics/server.py index b7a15cc..7c63cc8 100644 --- a/src/session_analytics/server.py +++ b/src/agent_session_analytics/server.py @@ -17,8 +17,8 @@ from fastmcp import FastMCP -from session_analytics import ingest, patterns, queries -from session_analytics.storage import SQLiteStorage +from agent_session_analytics import ingest, patterns, queries +from agent_session_analytics.storage import SQLiteStorage # Configure logging logging.basicConfig( @@ -26,18 +26,18 @@ format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) -logger = logging.getLogger("session-analytics") +logger = logging.getLogger("agent-session-analytics") if os.environ.get("DEV_MODE"): logger.setLevel(logging.DEBUG) # Initialize MCP server -mcp = FastMCP("session-analytics") +mcp = FastMCP("agent-session-analytics") # Initialize storage storage = SQLiteStorage() -@mcp.resource("session-analytics://guide", description="Usage guide and best practices") +@mcp.resource("agent-session-analytics://guide", description="Usage guide and best practices") def usage_guide() -> str: """Return the session analytics usage guide from external markdown file.""" guide_path = Path(__file__).parent / "guide.md" @@ -653,7 +653,7 @@ class TailscaleAuthMiddleware: (Tailscale-User-Login) into requests. This middleware rejects requests that don't have these headers. - Set SESSION_ANALYTICS_AUTH_DISABLED=1 to disable (for testing/local dev). + Set AGENT_SESSION_ANALYTICS_AUTH_DISABLED=1 to disable (for testing/local dev). """ TAILSCALE_USER_HEADER = b"tailscale-user-login" @@ -706,11 +706,11 @@ async def _send_unauthorized(self, send): def create_app(): """Create the ASGI app for uvicorn. - Set SESSION_ANALYTICS_AUTH_DISABLED=1 to disable auth (for testing/local dev). + Set AGENT_SESSION_ANALYTICS_AUTH_DISABLED=1 to disable auth (for testing/local dev). """ app = mcp.http_app(stateless_http=True) - auth_disabled = os.environ.get("SESSION_ANALYTICS_AUTH_DISABLED", "").lower() in ( + auth_disabled = os.environ.get("AGENT_SESSION_ANALYTICS_AUTH_DISABLED", "").lower() in ( "1", "true", ) diff --git a/src/session_analytics/storage.py b/src/agent_session_analytics/storage.py similarity index 99% rename from src/session_analytics/storage.py rename to src/agent_session_analytics/storage.py index 384bfed..0d9222c 100644 --- a/src/session_analytics/storage.py +++ b/src/agent_session_analytics/storage.py @@ -11,7 +11,7 @@ from datetime import datetime from pathlib import Path -logger = logging.getLogger("session-analytics") +logger = logging.getLogger("agent-session-analytics") # Register datetime adapters/converters (required for Python 3.12+) @@ -171,7 +171,7 @@ class BusEvent: # Default database path -DEFAULT_DB_PATH = Path.home() / ".claude" / "contrib" / "analytics" / "data.db" +DEFAULT_DB_PATH = Path.home() / ".claude" / "contrib" / "agent-session-analytics" / "data.db" # Schema version for migrations SCHEMA_VERSION = 12 @@ -594,7 +594,7 @@ class SQLiteStorage: def __init__(self, db_path: str | Path | None = None): """Initialize storage with optional custom DB path.""" if db_path is None: - db_path = os.environ.get("SESSION_ANALYTICS_DB", str(DEFAULT_DB_PATH)) + db_path = os.environ.get("AGENT_SESSION_ANALYTICS_DB", str(DEFAULT_DB_PATH)) self.db_path = Path(db_path) self.db_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/conftest.py b/tests/conftest.py index 9dd18b1..f924734 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,13 +7,13 @@ import pytest -from session_analytics.storage import Event, Session, SQLiteStorage +from agent_session_analytics.storage import Event, Session, SQLiteStorage def pytest_configure(config): """Set up test environment before any imports happen.""" # Disable Tailscale auth for tests - os.environ["SESSION_ANALYTICS_AUTH_DISABLED"] = "1" + os.environ["AGENT_SESSION_ANALYTICS_AUTH_DISABLED"] = "1" @pytest.fixture diff --git a/tests/test_cli.py b/tests/test_cli.py index 46b19c9..b614614 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,7 +3,7 @@ from datetime import datetime from unittest.mock import patch -from session_analytics.cli import ( +from agent_session_analytics.cli import ( cmd_benchmark, cmd_classify, cmd_commands, @@ -33,7 +33,7 @@ cmd_trends, format_output, ) -from session_analytics.storage import GitCommit +from agent_session_analytics.storage import GitCommit # Uses fixtures from conftest.py: storage, populated_storage @@ -144,7 +144,7 @@ def test_cmd_status(self, populated_storage, capsys): class Args: json = False - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_status(Args()) captured = capsys.readouterr() @@ -158,7 +158,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_frequency(Args()) captured = capsys.readouterr() @@ -173,7 +173,7 @@ class Args: project = None prefix = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_commands(Args()) captured = capsys.readouterr() @@ -188,7 +188,7 @@ class Args: project = None limit = 20 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_sessions(Args()) captured = capsys.readouterr() @@ -203,7 +203,7 @@ class Args: project = None by = "day" - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_tokens(Args()) captured = capsys.readouterr() @@ -219,7 +219,7 @@ class Args: length = 2 expand = False - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_sequences(Args()) captured = capsys.readouterr() @@ -233,7 +233,7 @@ class Args: days = 7 min_count = 1 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_permissions(Args()) captured = capsys.readouterr() @@ -248,7 +248,7 @@ class Args: refresh = True basic = False - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_insights(Args()) captured = capsys.readouterr() @@ -262,7 +262,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_frequency(Args()) captured = capsys.readouterr() @@ -277,7 +277,7 @@ class Args: limit = 50 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_search(Args()) captured = capsys.readouterr() @@ -293,7 +293,7 @@ class Args: limit = 50 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_search(Args()) captured = capsys.readouterr() @@ -308,7 +308,7 @@ class Args: limit = 50 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_search(Args()) captured = capsys.readouterr() @@ -325,7 +325,7 @@ class Args: limit = 50 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_search(Args()) captured = capsys.readouterr() @@ -341,7 +341,7 @@ class Args: limit = 50 project = "-test" - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_search(Args()) captured = capsys.readouterr() @@ -357,7 +357,7 @@ class Args: min_count = 1 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_signals(Args()) captured = capsys.readouterr() @@ -373,7 +373,7 @@ class Args: min_count = 1 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_signals(Args()) captured = capsys.readouterr() @@ -395,7 +395,7 @@ class Args: session_id = None project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_session_commits(Args()) captured = capsys.readouterr() @@ -416,7 +416,7 @@ class Args: session_id = "s1" project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_session_commits(Args()) captured = capsys.readouterr() @@ -432,7 +432,7 @@ class Args: project = None force = False - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_ingest(Args()) captured = capsys.readouterr() @@ -449,7 +449,7 @@ class Args: limit = 20 collapse_worktrees = False - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_file_activity(Args()) captured = capsys.readouterr() @@ -463,7 +463,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_languages(Args()) captured = capsys.readouterr() @@ -476,7 +476,7 @@ class Args: json = False days = 7 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_projects(Args()) captured = capsys.readouterr() @@ -490,7 +490,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_mcp_usage(Args()) captured = capsys.readouterr() @@ -507,7 +507,7 @@ class Args: days = 7 expand = False - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_sample_sequences(Args()) captured = capsys.readouterr() @@ -523,7 +523,7 @@ class Args: session_id = None limit = 100 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_journey(Args()) captured = capsys.readouterr() @@ -538,7 +538,7 @@ class Args: days = 1 # days * 24 = hours min_overlap = 5 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_parallel(Args()) captured = capsys.readouterr() @@ -554,7 +554,7 @@ class Args: days = 7 limit = 10 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_related(Args()) captured = capsys.readouterr() @@ -568,7 +568,7 @@ class Args: days = 7 rework_window = 10 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_failures(Args()) captured = capsys.readouterr() @@ -583,7 +583,7 @@ class Args: project = None limit = 20 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_classify(Args()) captured = capsys.readouterr() @@ -598,7 +598,7 @@ class Args: days = 0.17 # days * 24 = ~4 hours limit = 10 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_handoff(Args()) captured = capsys.readouterr() @@ -613,7 +613,7 @@ class Args: days = 7 compare_to = "previous" - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_trends(Args()) captured = capsys.readouterr() @@ -628,7 +628,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_git_ingest(Args()) captured = capsys.readouterr() @@ -641,7 +641,7 @@ class Args: json = False days = 7 - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_git_correlate(Args()) captured = capsys.readouterr() @@ -654,7 +654,7 @@ class Args: json = False iterations = 1 # Minimal iterations for speed - with patch("session_analytics.cli.SQLiteStorage", return_value=populated_storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=populated_storage): cmd_benchmark(Args()) captured = capsys.readouterr() @@ -768,7 +768,7 @@ class Args: project = None no_expand = False - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_frequency(Args()) captured = capsys.readouterr() @@ -784,7 +784,7 @@ class Args: project = None limit = 20 - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_sessions(Args()) captured = capsys.readouterr() @@ -800,7 +800,7 @@ class Args: project = None prefix = None - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_commands(Args()) captured = capsys.readouterr() @@ -817,7 +817,7 @@ class Args: length = 2 expand = False - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_sequences(Args()) captured = capsys.readouterr() @@ -833,7 +833,7 @@ class Args: refresh = False basic = False - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_insights(Args()) captured = capsys.readouterr() @@ -850,7 +850,7 @@ class Args: session_id = None limit = 100 - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_journey(Args()) captured = capsys.readouterr() @@ -866,7 +866,7 @@ class Args: min_count = 1 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_signals(Args()) captured = capsys.readouterr() @@ -883,7 +883,7 @@ class Args: limit = 20 collapse_worktrees = False - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_file_activity(Args()) captured = capsys.readouterr() @@ -898,7 +898,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_languages(Args()) captured = capsys.readouterr() @@ -912,7 +912,7 @@ class Args: json = False days = 7 - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_projects(Args()) captured = capsys.readouterr() @@ -927,7 +927,7 @@ class Args: days = 7 project = None - with patch("session_analytics.cli.SQLiteStorage", return_value=storage): + with patch("agent_session_analytics.cli.SQLiteStorage", return_value=storage): cmd_mcp_usage(Args()) captured = capsys.readouterr() diff --git a/tests/test_ingest.py b/tests/test_ingest.py index e336d3b..dfcdc7c 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -6,7 +6,7 @@ import pytest -from session_analytics.ingest import ( +from agent_session_analytics.ingest import ( calculate_result_size, decode_project_path, detect_compaction, @@ -462,7 +462,7 @@ def test_parse_ismeta_without_command_heading(self): def test_user_message_text_truncation_at_boundary(self): """Test that user_message_text is truncated at USER_MESSAGE_MAX_LENGTH (2000 chars).""" - from session_analytics.ingest import USER_MESSAGE_MAX_LENGTH + from agent_session_analytics.ingest import USER_MESSAGE_MAX_LENGTH # Test content exactly at the limit - should not be truncated exact_limit_content = "x" * USER_MESSAGE_MAX_LENGTH @@ -493,7 +493,7 @@ def test_user_message_text_truncation_at_boundary(self): def test_user_message_text_truncation_with_list_content(self): """Test truncation when content is a list of text blocks.""" - from session_analytics.ingest import USER_MESSAGE_MAX_LENGTH + from agent_session_analytics.ingest import USER_MESSAGE_MAX_LENGTH # Create content with multiple text blocks that exceed limit when joined text_block = "z" * 1500 @@ -593,8 +593,8 @@ def test_ingest_logs(self, storage, sample_logs_dir): RFC #41: Assistant with tool_use creates 2 events, so 3 entries → 4 events. """ # Use find_log_files with explicit logs_dir - from session_analytics.ingest import ingest_file as do_ingest_file - from session_analytics.ingest import update_session_stats + from agent_session_analytics.ingest import ingest_file as do_ingest_file + from agent_session_analytics.ingest import update_session_stats files = find_log_files(logs_dir=sample_logs_dir, days=7) assert len(files) == 1 @@ -613,7 +613,7 @@ class TestIngestGitHistory: def test_not_a_git_repo(self, storage): """Test with non-git directory.""" - from session_analytics.ingest import ingest_git_history + from agent_session_analytics.ingest import ingest_git_history with tempfile.TemporaryDirectory() as tmpdir: result = ingest_git_history(storage, repo_path=tmpdir) @@ -624,7 +624,7 @@ def test_not_a_git_repo(self, storage): def test_git_ingest_returns_stats(self, storage): """Test that git ingest returns proper stats structure.""" - from session_analytics.ingest import ingest_git_history + from agent_session_analytics.ingest import ingest_git_history # Using current directory which is a git repo result = ingest_git_history(storage, days=1) @@ -640,7 +640,7 @@ def test_git_ingest_handles_malformed_output(self, storage): from pathlib import Path from unittest.mock import MagicMock, patch - from session_analytics.ingest import ingest_git_history + from agent_session_analytics.ingest import ingest_git_history # Create a fake git directory with tempfile.TemporaryDirectory() as tmpdir: @@ -683,7 +683,7 @@ class TestCorrelateGitWithSessions: def test_empty_database(self, storage): """Test with empty database.""" - from session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.ingest import correlate_git_with_sessions result = correlate_git_with_sessions(storage, days=7) @@ -696,8 +696,8 @@ def test_correlation_with_matching_session(self, storage): """Test that commits during sessions are correlated.""" from datetime import datetime, timedelta - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() @@ -750,8 +750,8 @@ def test_commit_at_session_boundary(self, storage): """Test commit exactly 5 minutes after session end is included.""" from datetime import datetime, timedelta - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() session_end = now - timedelta(minutes=30) @@ -798,8 +798,8 @@ def test_commit_just_outside_buffer(self, storage): """Test commit 6 minutes after session end is NOT included.""" from datetime import datetime, timedelta - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() session_end = now - timedelta(minutes=30) @@ -846,8 +846,8 @@ def test_overlapping_sessions_picks_first_match(self, storage): """Test behavior when commit falls within multiple session windows.""" from datetime import datetime, timedelta - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() @@ -915,8 +915,8 @@ def test_commit_before_session_start_within_buffer(self, storage): """Test commit 5 minutes BEFORE session start IS included (pre-session buffer).""" from datetime import datetime, timedelta - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() session_start = now - timedelta(hours=1) @@ -966,8 +966,8 @@ def test_commit_before_session_outside_pre_buffer(self, storage): """Test commit 6 minutes BEFORE session start is NOT included.""" from datetime import datetime, timedelta - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() session_start = now - timedelta(hours=1) @@ -1019,8 +1019,8 @@ def test_timezone_aware_commit_correlates_correctly(self, storage): """ from datetime import datetime, timedelta, timezone - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() session_start = now - timedelta(hours=1) @@ -1080,8 +1080,8 @@ def test_batch_correlation_error_logged(self, storage, caplog): from datetime import datetime, timedelta from unittest.mock import patch - from session_analytics.ingest import correlate_git_with_sessions - from session_analytics.storage import Event, GitCommit + from agent_session_analytics.ingest import correlate_git_with_sessions + from agent_session_analytics.storage import Event, GitCommit now = datetime.now() session_start = now - timedelta(hours=1) @@ -1309,7 +1309,7 @@ def test_token_deduplication_on_ingest(self, storage, tmp_path): """ import json - from session_analytics.ingest import ingest_file + from agent_session_analytics.ingest import ingest_file # Create JSONL with assistant having 3 tool_uses jsonl_content = json.dumps( @@ -1558,7 +1558,7 @@ def test_projects_without_git(self, storage, tmp_path): """Projects without .git are skipped.""" from datetime import datetime - from session_analytics.storage import Event + from agent_session_analytics.storage import Event # Create a directory without .git project_dir = tmp_path / "no-git-project" @@ -1589,7 +1589,7 @@ def test_project_with_git(self, storage, tmp_path): from datetime import datetime from unittest.mock import patch - from session_analytics.storage import Event + from agent_session_analytics.storage import Event # Create a directory with .git project_dir = tmp_path / "my-project" @@ -1627,7 +1627,7 @@ def test_decode_failure_skipped(self, storage): """Projects that can't be decoded are skipped.""" from datetime import datetime - from session_analytics.storage import Event + from agent_session_analytics.storage import Event # Add event with invalid project path that can't be decoded storage.add_event( diff --git a/tests/test_patterns.py b/tests/test_patterns.py index d0ee4a2..588b28e 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from pathlib import Path -from session_analytics.patterns import ( +from agent_session_analytics.patterns import ( compute_all_patterns, compute_command_patterns, compute_permission_gaps, @@ -14,7 +14,7 @@ load_allowed_commands, sample_sequences, ) -from session_analytics.storage import Event +from agent_session_analytics.storage import Event # Uses fixtures from conftest.py: storage, pattern_storage @@ -237,7 +237,7 @@ def test_permission_gaps_filters_non_actionable_commands(self, storage): Commands like pwd, cd, echo, and shell builtins should not appear in permission gap results because they are not actionable. """ - from session_analytics.patterns import NON_ACTIONABLE_COMMANDS + from agent_session_analytics.patterns import NON_ACTIONABLE_COMMANDS now = datetime.now() @@ -583,7 +583,7 @@ class TestAnalyzeFailures: def test_analyze_failures_basic(self, storage): """Test basic failure analysis with errors.""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -616,7 +616,7 @@ def test_analyze_failures_basic(self, storage): def test_analyze_failures_no_errors(self, storage): """Test when there are no errors.""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -638,7 +638,7 @@ def test_analyze_failures_no_errors(self, storage): def test_rework_detection(self, storage): """Test detection of rework patterns (multiple edits to same file).""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() # 4 edits to the same file within 10 minutes - should be detected as rework @@ -691,7 +691,7 @@ def test_rework_detection(self, storage): def test_rework_not_detected_different_files(self, storage): """Test that edits to different files aren't counted as rework.""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -736,7 +736,7 @@ def test_analyze_failures_error_examples(self, storage): RFC #49: When errors_by_tool shows 'Bash: 5 errors', error_examples should reveal WHICH commands failed, enabling actionable diagnosis. """ - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -804,7 +804,7 @@ class TestAnalyzeTrends: def test_empty_database(self, storage): """Test with empty database.""" - from session_analytics.patterns import analyze_trends + from agent_session_analytics.patterns import analyze_trends result = analyze_trends(storage, days=7) @@ -818,7 +818,7 @@ def test_empty_database(self, storage): def test_trend_metrics(self, storage): """Test that trends are calculated correctly.""" - from session_analytics.patterns import analyze_trends + from agent_session_analytics.patterns import analyze_trends now = datetime.now() @@ -860,7 +860,7 @@ def test_trend_metrics(self, storage): def test_tool_changes_included(self, storage): """Test that tool-specific changes are included.""" - from session_analytics.patterns import analyze_trends + from agent_session_analytics.patterns import analyze_trends now = datetime.now() @@ -891,7 +891,7 @@ def test_tool_changes_included(self, storage): def test_compare_to_previous(self, storage): """Test compare_to='previous' mode.""" - from session_analytics.patterns import analyze_trends + from agent_session_analytics.patterns import analyze_trends result = analyze_trends(storage, days=7, compare_to="previous") @@ -899,7 +899,7 @@ def test_compare_to_previous(self, storage): def test_compare_to_same_last_month(self, storage): """Test compare_to='same_last_month' mode compares to same week last month.""" - from session_analytics.patterns import analyze_trends + from agent_session_analytics.patterns import analyze_trends now = datetime.now() @@ -941,7 +941,7 @@ def test_compare_to_same_last_month(self, storage): def test_trend_unchanged_threshold(self, storage): """Test that changes within +/- 5% are marked as 'unchanged'.""" - from session_analytics.patterns import analyze_trends + from agent_session_analytics.patterns import analyze_trends now = datetime.now() @@ -1083,7 +1083,7 @@ class TestAnalyzeFailuresJoinLogic: def test_errors_by_tool_join(self, storage): """Test that errors are properly joined to their tool_use events.""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -1141,7 +1141,7 @@ def test_errors_by_tool_join(self, storage): def test_errors_without_tool_id_not_in_errors_by_tool(self, storage): """Test that errors without tool_id are in total but not in errors_by_tool.""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -1166,7 +1166,7 @@ def test_errors_without_tool_id_not_in_errors_by_tool(self, storage): def test_rework_not_detected_across_sessions(self, storage): """Test that same file edits in different sessions aren't counted as rework.""" - from session_analytics.patterns import analyze_failures + from agent_session_analytics.patterns import analyze_failures now = datetime.now() events = [ @@ -1298,7 +1298,7 @@ class TestGetSessionSignals: def test_get_signals_empty_database(self, storage): """Test with empty database.""" - from session_analytics.patterns import get_session_signals + from agent_session_analytics.patterns import get_session_signals result = get_session_signals(storage, days=7) @@ -1307,8 +1307,8 @@ def test_get_signals_empty_database(self, storage): def test_get_signals_with_commits(self, storage): """Test that commit counts are included in signals.""" - from session_analytics.patterns import get_session_signals - from session_analytics.storage import GitCommit, Session + from agent_session_analytics.patterns import get_session_signals + from agent_session_analytics.storage import GitCommit, Session now = datetime.now() @@ -1348,7 +1348,7 @@ def test_get_signals_with_commits(self, storage): def test_get_signals_with_errors(self, storage): """Test that error rates are included in signals.""" - from session_analytics.patterns import get_session_signals + from agent_session_analytics.patterns import get_session_signals now = datetime.now() @@ -1382,7 +1382,7 @@ def test_get_signals_with_errors(self, storage): def test_get_signals_min_count_filter(self, storage): """Test that sessions below min_count threshold are excluded.""" - from session_analytics.patterns import get_session_signals + from agent_session_analytics.patterns import get_session_signals now = datetime.now() @@ -1408,8 +1408,8 @@ def test_get_signals_min_count_filter(self, storage): def test_get_signals_includes_all_raw_fields(self, storage): """Test that all expected raw signal fields are present.""" - from session_analytics.patterns import get_session_signals - from session_analytics.storage import Session + from agent_session_analytics.patterns import get_session_signals + from agent_session_analytics.storage import Session now = datetime.now() diff --git a/tests/test_queries.py b/tests/test_queries.py index 30e5407..09c4d64 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -from session_analytics.queries import ( +from agent_session_analytics.queries import ( ensure_fresh_data, get_cutoff, query_agent_activity, @@ -17,7 +17,7 @@ query_tokens, query_tool_frequency, ) -from session_analytics.storage import Event, Session +from agent_session_analytics.storage import Event, Session # Uses fixtures from conftest.py: storage, populated_storage @@ -176,7 +176,7 @@ class TestEnsureFreshData: def test_fresh_data_not_refreshed(self, populated_storage): """Test that fresh data is not refreshed.""" # First, update ingestion state to make data appear fresh - from session_analytics.storage import IngestionState + from agent_session_analytics.storage import IngestionState populated_storage.update_ingestion_state( IngestionState( @@ -206,7 +206,7 @@ class TestGetUserJourney: def test_basic_journey(self, storage): """Test basic user journey extraction.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() events = [ @@ -239,7 +239,7 @@ def test_basic_journey(self, storage): def test_journey_excludes_tool_events(self, storage): """Test that journey only includes user messages.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() events = [ @@ -269,7 +269,7 @@ def test_journey_excludes_tool_events(self, storage): def test_journey_with_session_id_filter(self, storage): """Test get_user_journey with session_id filter.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() # Add user messages from two different sessions @@ -305,7 +305,7 @@ def test_journey_with_session_id_filter(self, storage): def test_journey_includes_assistant_messages(self, storage): """Test that journey includes assistant messages by default.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() events = [ @@ -339,7 +339,7 @@ def test_journey_includes_assistant_messages(self, storage): def test_journey_custom_entry_types(self, storage): """Test filtering by custom entry_types.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() events = [ @@ -388,7 +388,7 @@ def test_journey_custom_entry_types(self, storage): def test_journey_max_message_length_truncation(self, storage): """Test message truncation with max_message_length.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() long_message = "A" * 1000 @@ -422,7 +422,7 @@ class TestDetectParallelSessions: def test_detect_overlapping_sessions(self, storage): """Test detection of overlapping sessions.""" - from session_analytics.queries import detect_parallel_sessions + from agent_session_analytics.queries import detect_parallel_sessions now = datetime.now() # Two sessions that overlap @@ -475,7 +475,7 @@ def test_detect_overlapping_sessions(self, storage): def test_no_parallel_sessions(self, storage): """Test when sessions don't overlap.""" - from session_analytics.queries import detect_parallel_sessions + from agent_session_analytics.queries import detect_parallel_sessions now = datetime.now() # Two non-overlapping sessions @@ -525,7 +525,7 @@ class TestFindRelatedSessions: def test_find_by_files(self, storage): """Test finding related sessions by shared files.""" - from session_analytics.queries import find_related_sessions + from agent_session_analytics.queries import find_related_sessions now = datetime.now() events = [ @@ -561,7 +561,7 @@ def test_find_by_files(self, storage): def test_find_by_commands(self, storage): """Test finding related sessions by shared commands.""" - from session_analytics.queries import find_related_sessions + from agent_session_analytics.queries import find_related_sessions now = datetime.now() events = [ @@ -592,7 +592,7 @@ def test_find_by_commands(self, storage): def test_find_by_temporal(self, storage): """Test finding related sessions by temporal proximity.""" - from session_analytics.queries import find_related_sessions + from agent_session_analytics.queries import find_related_sessions now = datetime.now() events = [ @@ -621,7 +621,7 @@ def test_find_by_temporal(self, storage): def test_invalid_method(self, storage): """Test that invalid method returns error.""" - from session_analytics.queries import find_related_sessions + from agent_session_analytics.queries import find_related_sessions result = find_related_sessions(storage, session_id="s1", method="invalid", days=7) @@ -629,7 +629,7 @@ def test_invalid_method(self, storage): def test_find_by_files_no_files_in_target(self, storage): """Test when target session has no file_path values.""" - from session_analytics.queries import find_related_sessions + from agent_session_analytics.queries import find_related_sessions now = datetime.now() events = [ @@ -665,7 +665,7 @@ def test_find_by_files_no_files_in_target(self, storage): def test_find_by_commands_no_commands_in_target(self, storage): """Test when target session has no command values.""" - from session_analytics.queries import find_related_sessions + from agent_session_analytics.queries import find_related_sessions now = datetime.now() events = [ @@ -707,7 +707,7 @@ class TestGetHandoffContext: def test_no_recent_sessions(self, storage): """Test when no recent sessions exist.""" - from session_analytics.queries import get_handoff_context + from agent_session_analytics.queries import get_handoff_context result = get_handoff_context(storage, hours=1) @@ -716,7 +716,7 @@ def test_no_recent_sessions(self, storage): def test_specific_session_not_found(self, storage): """Test when specified session doesn't exist.""" - from session_analytics.queries import get_handoff_context + from agent_session_analytics.queries import get_handoff_context result = get_handoff_context(storage, session_id="nonexistent-session") @@ -725,7 +725,7 @@ def test_specific_session_not_found(self, storage): def test_returns_session_info(self, storage): """Test that session info is returned correctly.""" - from session_analytics.queries import get_handoff_context + from agent_session_analytics.queries import get_handoff_context now = datetime.now() events = [ @@ -770,7 +770,7 @@ def test_returns_session_info(self, storage): def test_returns_recent_messages(self, storage): """Test that recent user messages are returned.""" - from session_analytics.queries import get_handoff_context + from agent_session_analytics.queries import get_handoff_context now = datetime.now() events = [ @@ -801,7 +801,7 @@ def test_returns_recent_messages(self, storage): def test_returns_modified_files(self, storage): """Test that modified files are returned.""" - from session_analytics.queries import get_handoff_context + from agent_session_analytics.queries import get_handoff_context now = datetime.now() events = [ @@ -844,7 +844,7 @@ def test_returns_modified_files(self, storage): def test_auto_selects_most_recent_session(self, storage): """Test that most recent session is auto-selected.""" - from session_analytics.queries import get_handoff_context + from agent_session_analytics.queries import get_handoff_context now = datetime.now() events = [ @@ -877,7 +877,7 @@ class TestClassifySessions: def test_debugging_classification(self, storage): """Test sessions with high error rate are classified as debugging.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [] @@ -924,7 +924,7 @@ def test_debugging_classification(self, storage): def test_development_classification(self, storage): """Test sessions with high edit percentage are classified as development.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [] @@ -978,7 +978,7 @@ def test_development_classification(self, storage): def test_research_classification(self, storage): """Test sessions with Read/search heavy usage are classified as research.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [] @@ -1031,7 +1031,7 @@ def test_research_classification(self, storage): def test_maintenance_classification(self, storage): """Test sessions with git/build commands are classified as maintenance.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [] @@ -1073,7 +1073,7 @@ def test_maintenance_classification(self, storage): def test_mixed_classification(self, storage): """Test sessions without dominant patterns are classified as mixed.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [ @@ -1141,7 +1141,7 @@ def test_mixed_classification(self, storage): def test_project_filter(self, storage): """Test that project filter correctly limits results.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [] @@ -1179,7 +1179,7 @@ def test_project_filter(self, storage): def test_min_event_threshold(self, storage): """Test that sessions with <5 events are excluded.""" - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [ @@ -1226,7 +1226,7 @@ def test_classification_factors_included(self, storage): RFC #49: Without classification_factors, an LLM seeing 'category: debugging' cannot explain to the user why it was classified that way. """ - from session_analytics.queries import classify_sessions + from agent_session_analytics.queries import classify_sessions now = datetime.now() events = [] @@ -1287,7 +1287,7 @@ class TestGetUserJourneyIncludeProjects: def test_journey_without_projects(self, storage): """Test that include_projects=False excludes project info.""" - from session_analytics.queries import get_user_journey + from agent_session_analytics.queries import get_user_journey now = datetime.now() events = [ @@ -1636,7 +1636,7 @@ class TestNormalizeDatetime: def test_naive_datetime_unchanged(self): """Test that naive datetime is returned unchanged.""" - from session_analytics.queries import normalize_datetime + from agent_session_analytics.queries import normalize_datetime naive_dt = datetime(2024, 1, 15, 12, 30, 45) result = normalize_datetime(naive_dt) @@ -1647,7 +1647,7 @@ def test_utc_timezone_stripped(self): """Test that UTC timezone is stripped.""" from datetime import timezone - from session_analytics.queries import normalize_datetime + from agent_session_analytics.queries import normalize_datetime aware_dt = datetime(2024, 1, 15, 12, 30, 45, tzinfo=timezone.utc) result = normalize_datetime(aware_dt) @@ -1671,7 +1671,7 @@ def test_non_utc_timezone_stripped(self): """ from datetime import timezone - from session_analytics.queries import normalize_datetime + from agent_session_analytics.queries import normalize_datetime # Create a timezone offset (e.g., +05:30) tz_offset = timezone(timedelta(hours=5, minutes=30)) @@ -1687,7 +1687,7 @@ def test_comparison_after_normalization(self): """Test that normalized datetimes can be compared safely.""" from datetime import timezone - from session_analytics.queries import normalize_datetime + from agent_session_analytics.queries import normalize_datetime naive_dt = datetime(2024, 1, 15, 12, 30, 45) aware_dt = datetime(2024, 1, 15, 12, 30, 45, tzinfo=timezone.utc) @@ -2341,7 +2341,7 @@ class TestGetCompactionEvents: def test_returns_compaction_events(self, storage): """Test that compaction events are returned.""" - from session_analytics.queries import get_compaction_events + from agent_session_analytics.queries import get_compaction_events now = datetime.now() events = [ @@ -2376,7 +2376,7 @@ def test_returns_compaction_events(self, storage): def test_filters_by_session_id(self, storage): """Test session_id filter works.""" - from session_analytics.queries import get_compaction_events + from agent_session_analytics.queries import get_compaction_events now = datetime.now() events = [ @@ -2404,7 +2404,7 @@ def test_filters_by_session_id(self, storage): def test_aggregate_mode_groups_by_session(self, storage): """Test that aggregate=True groups compactions by session.""" - from session_analytics.queries import get_compaction_events + from agent_session_analytics.queries import get_compaction_events now = datetime.now() events = [ @@ -2464,7 +2464,7 @@ class TestGetPreCompactionEvents: def test_returns_events_before_compaction(self, storage): """Test that events before the compaction timestamp are returned.""" - from session_analytics.queries import get_pre_compaction_events + from agent_session_analytics.queries import get_pre_compaction_events now = datetime.now() compaction_time = now - timedelta(hours=1) @@ -2519,7 +2519,7 @@ def test_returns_events_before_compaction(self, storage): def test_filters_by_session_id(self, storage): """Test that only events from the specified session are returned.""" - from session_analytics.queries import get_pre_compaction_events + from agent_session_analytics.queries import get_pre_compaction_events now = datetime.now() compaction_time = now - timedelta(hours=1) @@ -2557,7 +2557,7 @@ def test_filters_by_session_id(self, storage): def test_respects_limit_parameter(self, storage): """Test that the limit parameter is respected.""" - from session_analytics.queries import get_pre_compaction_events + from agent_session_analytics.queries import get_pre_compaction_events now = datetime.now() compaction_time = now - timedelta(hours=1) @@ -2591,7 +2591,7 @@ class TestAnalyzePreCompactionPatterns: def test_returns_empty_when_no_compactions(self, storage): """Test that empty result is returned when no compactions exist.""" - from session_analytics.queries import analyze_pre_compaction_patterns + from agent_session_analytics.queries import analyze_pre_compaction_patterns result = analyze_pre_compaction_patterns(storage, days=7) @@ -2601,7 +2601,7 @@ def test_returns_empty_when_no_compactions(self, storage): def test_detects_consecutive_reads(self, storage): """Test that consecutive reads are detected as a pattern.""" - from session_analytics.queries import analyze_pre_compaction_patterns + from agent_session_analytics.queries import analyze_pre_compaction_patterns now = datetime.now() compaction_time = now - timedelta(hours=1) @@ -2638,7 +2638,7 @@ def test_detects_consecutive_reads(self, storage): def test_detects_files_read_multiple_times(self, storage): """Test that files read multiple times are detected.""" - from session_analytics.queries import analyze_pre_compaction_patterns + from agent_session_analytics.queries import analyze_pre_compaction_patterns now = datetime.now() compaction_time = now - timedelta(hours=1) @@ -2677,7 +2677,7 @@ def test_detects_files_read_multiple_times(self, storage): def test_generates_recommendations_for_antipatterns(self, storage): """Test that recommendations are generated when antipatterns detected.""" - from session_analytics.queries import analyze_pre_compaction_patterns + from agent_session_analytics.queries import analyze_pre_compaction_patterns now = datetime.now() compaction_time = now - timedelta(hours=1) @@ -2719,7 +2719,7 @@ class TestGetLargeToolResults: def test_returns_large_results(self, storage): """Test that large tool results are returned.""" - from session_analytics.queries import get_large_tool_results + from agent_session_analytics.queries import get_large_tool_results now = datetime.now() # Need both tool_use and tool_result with matching tool_id for the JOIN @@ -2780,7 +2780,7 @@ class TestGetSessionEfficiency: def test_returns_efficiency_metrics(self, storage): """Test that efficiency metrics are returned.""" - from session_analytics.queries import get_session_efficiency + from agent_session_analytics.queries import get_session_efficiency now = datetime.now() # Need 10+ events to pass HAVING clause in query diff --git a/tests/test_server.py b/tests/test_server.py index a3412e7..c9522fd 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,7 +2,7 @@ import pytest -from session_analytics.server import ( +from agent_session_analytics.server import ( TailscaleAuthMiddleware, analyze_failures, analyze_trends, diff --git a/tests/test_smoke_real_data.py b/tests/test_smoke_real_data.py index eabe7ee..44670d6 100644 --- a/tests/test_smoke_real_data.py +++ b/tests/test_smoke_real_data.py @@ -25,7 +25,7 @@ @pytest.fixture def real_storage(): """Get storage instance pointing to real database.""" - from session_analytics.storage import SQLiteStorage + from agent_session_analytics.storage import SQLiteStorage db_path = Path.home() / ".claude" / "contrib" / "analytics" / "data.db" if not db_path.exists(): diff --git a/tests/test_storage.py b/tests/test_storage.py index 489ca10..ac59b53 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -4,7 +4,7 @@ import pytest -from session_analytics.storage import ( +from agent_session_analytics.storage import ( Event, GitCommit, IngestionState, From e2e38cb3dd61f50000589c842689f7ea6aaae50b Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Sat, 24 Jan 2026 21:27:55 +0000 Subject: [PATCH 4/6] fix: Address remaining rename feedback - Fix MCP server message to use agent-session-analytics - Fix Makefile uninstall command to use agent-session-analytics - Update logger names in ingest.py and patterns.py - Update test database path in smoke tests - Update global-report.sh to use new CLI name and output path Co-Authored-By: Claude Opus 4.5 --- Makefile | 6 +++--- scripts/global-report.sh | 10 +++++----- src/agent_session_analytics/ingest.py | 2 +- src/agent_session_analytics/patterns.py | 2 +- src/agent_session_analytics/server.py | 2 +- tests/test_smoke_real_data.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 68a4a92..8febeb8 100644 --- a/Makefile +++ b/Makefile @@ -110,9 +110,9 @@ uninstall: @echo "Removing from Claude Code..." @CLAUDE_CMD=$$(command -v claude || echo "$$HOME/.local/bin/claude"); \ if [ -x "$$CLAUDE_CMD" ]; then \ - $$CLAUDE_CMD mcp remove --scope user session-analytics 2>/dev/null && \ - echo "Removed session-analytics from Claude Code" || \ - echo "session-analytics not found in Claude Code"; \ + $$CLAUDE_CMD mcp remove --scope user agent-session-analytics 2>/dev/null && \ + echo "Removed agent-session-analytics from Claude Code" || \ + echo "agent-session-analytics not found in Claude Code"; \ fi @echo "" @echo "Uninstall complete!" diff --git a/scripts/global-report.sh b/scripts/global-report.sh index 5d03678..d5e9a79 100755 --- a/scripts/global-report.sh +++ b/scripts/global-report.sh @@ -1,6 +1,6 @@ #!/bin/bash # Generate a 7-day global analytics report -# Outputs to /tmp/session-analytics-report.md +# Outputs to /tmp/agent-session-analytics-report.md # # Restructured for RFC #41 with focus on actionable insights: # - Removed: languages (curiosity only), sessions (too verbose), mcp-usage (secondary) @@ -8,16 +8,16 @@ set -e -OUTPUT="/tmp/session-analytics-report.md" +OUTPUT="/tmp/agent-session-analytics-report.md" DAYS=7 -CLI="session-analytics-cli" +CLI="agent-session-analytics-cli" # Check if CLI is available if ! command -v "$CLI" &> /dev/null; then SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - CLI="$SCRIPT_DIR/../.venv/bin/session-analytics-cli" + CLI="$SCRIPT_DIR/../.venv/bin/agent-session-analytics-cli" if [[ ! -x "$CLI" ]]; then - echo "Error: session-analytics-cli not found" >&2 + echo "Error: agent-session-analytics-cli not found" >&2 exit 1 fi fi diff --git a/src/agent_session_analytics/ingest.py b/src/agent_session_analytics/ingest.py index bfc691d..4eb3df5 100644 --- a/src/agent_session_analytics/ingest.py +++ b/src/agent_session_analytics/ingest.py @@ -11,7 +11,7 @@ from agent_session_analytics.queries import get_cutoff, normalize_datetime from agent_session_analytics.storage import Event, GitCommit, IngestionState, Session, SQLiteStorage -logger = logging.getLogger("session-analytics") +logger = logging.getLogger("agent-session-analytics") # Default location for Claude Code session logs DEFAULT_LOGS_DIR = Path.home() / ".claude" / "projects" diff --git a/src/agent_session_analytics/patterns.py b/src/agent_session_analytics/patterns.py index 2033d22..4a2d868 100644 --- a/src/agent_session_analytics/patterns.py +++ b/src/agent_session_analytics/patterns.py @@ -11,7 +11,7 @@ from agent_session_analytics.queries import get_cutoff from agent_session_analytics.storage import Pattern, SQLiteStorage -logger = logging.getLogger("session-analytics") +logger = logging.getLogger("agent-session-analytics") # Default settings.json location DEFAULT_SETTINGS_PATH = Path.home() / ".claude" / "settings.json" diff --git a/src/agent_session_analytics/server.py b/src/agent_session_analytics/server.py index 7c63cc8..d0ce76f 100644 --- a/src/agent_session_analytics/server.py +++ b/src/agent_session_analytics/server.py @@ -732,7 +732,7 @@ def main(): print(f"Starting Agent Session Analytics on {host}:{port}") print( - f"Add to Claude Code: claude mcp add --transport http --scope user session-analytics http://{host}:{port}/mcp" + f"Add to Claude Code: claude mcp add --transport http --scope user agent-session-analytics http://{host}:{port}/mcp" ) uvicorn.run(create_app(), host=host, port=port) diff --git a/tests/test_smoke_real_data.py b/tests/test_smoke_real_data.py index 44670d6..e8a5390 100644 --- a/tests/test_smoke_real_data.py +++ b/tests/test_smoke_real_data.py @@ -27,7 +27,7 @@ def real_storage(): """Get storage instance pointing to real database.""" from agent_session_analytics.storage import SQLiteStorage - db_path = Path.home() / ".claude" / "contrib" / "analytics" / "data.db" + db_path = Path.home() / ".claude" / "contrib" / "agent-session-analytics" / "data.db" if not db_path.exists(): pytest.skip("Real database not found") return SQLiteStorage(db_path) From 1075e0ba29cad6dec59966db149b8b892e70a347 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Sat, 24 Jan 2026 21:37:40 +0000 Subject: [PATCH 5/6] feat: Add automatic DB migration from old path Migrates database from ~/.claude/contrib/analytics/data.db to ~/.claude/contrib/agent-session-analytics/data.db Matches the migration pattern used in agent-event-bus. Co-Authored-By: Claude Opus 4.5 --- src/agent_session_analytics/storage.py | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/agent_session_analytics/storage.py b/src/agent_session_analytics/storage.py index 0d9222c..c43fdc4 100644 --- a/src/agent_session_analytics/storage.py +++ b/src/agent_session_analytics/storage.py @@ -170,8 +170,11 @@ class BusEvent: payload: str | None = None # Raw payload text -# Default database path +# Database paths +# New canonical path (aligned with agent-event-bus naming) DEFAULT_DB_PATH = Path.home() / ".claude" / "contrib" / "agent-session-analytics" / "data.db" +# Old path for automatic migration +OLD_DB_PATH = Path.home() / ".claude" / "contrib" / "analytics" / "data.db" # Schema version for migrations SCHEMA_VERSION = 12 @@ -597,10 +600,33 @@ def __init__(self, db_path: str | Path | None = None): db_path = os.environ.get("AGENT_SESSION_ANALYTICS_DB", str(DEFAULT_DB_PATH)) self.db_path = Path(db_path) + + # Migrate from old location if needed (before creating directory) + self._migrate_db_location() + self.db_path.parent.mkdir(parents=True, exist_ok=True) self._init_db() + def _migrate_db_location(self) -> None: + """Migrate database from old location to new location. + + Old: ~/.claude/contrib/analytics/data.db + New: ~/.claude/contrib/agent-session-analytics/data.db + """ + if self.db_path.exists(): + return # Already at new location, nothing to do + + if OLD_DB_PATH.exists(): + logger.info(f"Migrating database from {OLD_DB_PATH} to {self.db_path}") + # Create new directory + self.db_path.parent.mkdir(parents=True, exist_ok=True) + # Move the database file + import shutil + + shutil.move(str(OLD_DB_PATH), str(self.db_path)) + logger.info("Database migration complete") + @contextmanager def _connect(self): """Context manager for database connections.""" From ebe3241486590f9ee2e2483672fbf022e0132eec Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Sat, 24 Jan 2026 23:22:23 +0000 Subject: [PATCH 6/6] chore: Rename env var to AGENT_SESSION_ANALYTICS_TESTING Aligns with agent-event-bus naming convention (AGENT_EVENT_BUS_TESTING). Co-Authored-By: Claude Opus 4.5 --- tests/test_smoke_real_data.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_smoke_real_data.py b/tests/test_smoke_real_data.py index e8a5390..46f5c8e 100644 --- a/tests/test_smoke_real_data.py +++ b/tests/test_smoke_real_data.py @@ -1,13 +1,13 @@ """Smoke tests that validate assumptions against real database data. These tests are skipped by default (no real database) and only run when -SESSION_ANALYTICS_SMOKE_TEST=1 is set. They catch issues like: +AGENT_SESSION_ANALYTICS_TESTING=1 is set. They catch issues like: - Compaction detection not finding entries - result_size_bytes not being populated - Entry type distribution anomalies - Token count reasonableness -Run with: SESSION_ANALYTICS_SMOKE_TEST=1 pytest tests/test_smoke_real_data.py -v +Run with: AGENT_SESSION_ANALYTICS_TESTING=1 pytest tests/test_smoke_real_data.py -v """ import os @@ -17,8 +17,8 @@ # Skip all tests in this file unless smoke test env var is set pytestmark = pytest.mark.skipif( - os.environ.get("SESSION_ANALYTICS_SMOKE_TEST") != "1", - reason="Smoke tests require SESSION_ANALYTICS_SMOKE_TEST=1 and real database", + os.environ.get("AGENT_SESSION_ANALYTICS_TESTING") != "1", + reason="Smoke tests require AGENT_SESSION_ANALYTICS_TESTING=1 and real database", )