Ryan Malloy 44ed9936b7 Initial commit: Claude Code Project Tracker
Add comprehensive development intelligence system that tracks:
- Development sessions with automatic start/stop
- Full conversation history with semantic search
- Tool usage and file operation analytics
- Think time and engagement analysis
- Git activity correlation
- Learning pattern recognition
- Productivity insights and metrics

Features:
- FastAPI backend with SQLite database
- Modern web dashboard with interactive charts
- Claude Code hook integration for automatic tracking
- Comprehensive test suite with 100+ tests
- Complete API documentation (OpenAPI/Swagger)
- Privacy-first design with local data storage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 02:59:21 -06:00

162 lines
5.4 KiB
Python

"""
Activity model for tracking tool usage and file operations.
"""
from datetime import datetime
from typing import Optional, Dict, Any
from sqlalchemy import String, Text, Integer, DateTime, JSON, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from .base import Base, TimestampMixin
class Activity(Base, TimestampMixin):
"""
Represents a single tool usage or file operation during development.
Activities are generated by Claude Code tool usage and provide
detailed insight into the development workflow.
"""
__tablename__ = "activities"
# Primary key
id: Mapped[int] = mapped_column(primary_key=True)
# Foreign keys
session_id: Mapped[int] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True)
conversation_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("conversations.id"),
nullable=True,
index=True
)
# Activity timing
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=func.now(),
index=True
)
# Activity details
tool_name: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
action: Mapped[str] = mapped_column(String(100), nullable=False)
file_path: Mapped[Optional[str]] = mapped_column(Text, nullable=True, index=True)
# Metadata and results
metadata: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
success: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# Code change metrics (for Edit/Write operations)
lines_added: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
lines_removed: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
# Relationships
session: Mapped["Session"] = relationship("Session", back_populates="activities")
conversation: Mapped[Optional["Conversation"]] = relationship(
"Conversation",
foreign_keys=[conversation_id]
)
def __repr__(self) -> str:
file_info = f", file='{self.file_path}'" if self.file_path else ""
return f"<Activity(id={self.id}, tool='{self.tool_name}', action='{self.action}'{file_info})>"
@property
def is_file_operation(self) -> bool:
"""Check if this activity involves file operations."""
return self.tool_name in {"Edit", "Write", "Read"}
@property
def is_code_execution(self) -> bool:
"""Check if this activity involves code/command execution."""
return self.tool_name in {"Bash", "Task"}
@property
def is_search_operation(self) -> bool:
"""Check if this activity involves searching."""
return self.tool_name in {"Grep", "Glob"}
@property
def total_lines_changed(self) -> int:
"""Get total lines changed (added + removed)."""
added = self.lines_added or 0
removed = self.lines_removed or 0
return added + removed
@property
def net_lines_changed(self) -> int:
"""Get net lines changed (added - removed)."""
added = self.lines_added or 0
removed = self.lines_removed or 0
return added - removed
def get_file_extension(self) -> Optional[str]:
"""Extract file extension from file path."""
if not self.file_path:
return None
if "." in self.file_path:
return self.file_path.split(".")[-1].lower()
return None
def get_programming_language(self) -> Optional[str]:
"""Infer programming language from file extension."""
ext = self.get_file_extension()
if not ext:
return None
language_map = {
"py": "python",
"js": "javascript",
"ts": "typescript",
"jsx": "javascript",
"tsx": "typescript",
"go": "go",
"rs": "rust",
"java": "java",
"cpp": "cpp",
"c": "c",
"h": "c",
"hpp": "cpp",
"rb": "ruby",
"php": "php",
"html": "html",
"css": "css",
"scss": "scss",
"sql": "sql",
"md": "markdown",
"yml": "yaml",
"yaml": "yaml",
"json": "json",
"xml": "xml",
"sh": "shell",
"bash": "shell",
}
return language_map.get(ext)
def is_successful(self) -> bool:
"""Check if the activity completed successfully."""
return self.success and not self.error_message
def get_command_executed(self) -> Optional[str]:
"""Get the command that was executed (for Bash activities)."""
if self.tool_name == "Bash" and self.metadata:
return self.metadata.get("command")
return None
def get_search_pattern(self) -> Optional[str]:
"""Get the search pattern (for Grep activities)."""
if self.tool_name == "Grep" and self.metadata:
return self.metadata.get("pattern")
return None
def get_task_type(self) -> Optional[str]:
"""Get the task type (for Task activities)."""
if self.tool_name == "Task" and self.metadata:
return self.metadata.get("task_type")
return None