feat: add Claude Code conversation tracker
- Create claude-code-tracker service to monitor and parse Claude Code conversations - Parse JSONL format with messages, tool calls, and metadata - Store in SQLite with full-text search support - Generate daily summaries using Claude API - Add Conversations UI with search, stats, and conversation browsing - Integrate with dashboard backend and frontend - Add sync script for Mac to NAS file transfer
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
import db
|
||||
import parser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONVERSATIONS_PATH = os.environ.get("CONVERSATIONS_PATH", "/app/conversations")
|
||||
COLLECT_INTERVAL = int(os.environ.get("COLLECT_INTERVAL", "60"))
|
||||
|
||||
|
||||
async def discover_jsonl_files():
|
||||
"""Discover all .jsonl files in conversations directory"""
|
||||
conversations_dir = Path(CONVERSATIONS_PATH)
|
||||
if not conversations_dir.exists():
|
||||
logger.warning(f"Conversations directory does not exist: {CONVERSATIONS_PATH}")
|
||||
return []
|
||||
|
||||
jsonl_files = []
|
||||
for file_path in conversations_dir.rglob("*.jsonl"):
|
||||
if file_path.is_file():
|
||||
jsonl_files.append(str(file_path))
|
||||
|
||||
return jsonl_files
|
||||
|
||||
|
||||
async def process_file(file_path: str):
|
||||
"""Process a single conversation file incrementally"""
|
||||
try:
|
||||
# Get checkpoint
|
||||
checkpoint = await db.get_checkpoint(file_path)
|
||||
start_line = checkpoint['last_line_count'] if checkpoint else 0
|
||||
|
||||
# Get file modification time
|
||||
file_stat = os.stat(file_path)
|
||||
file_mtime = datetime.fromtimestamp(file_stat.st_mtime).isoformat()
|
||||
|
||||
# Check if file has been modified
|
||||
if checkpoint and checkpoint['last_modified'] == file_mtime:
|
||||
# File hasn't changed, skip
|
||||
return 0
|
||||
|
||||
# Count total lines
|
||||
with open(file_path, 'r') as f:
|
||||
total_lines = sum(1 for _ in f)
|
||||
|
||||
if total_lines <= start_line:
|
||||
# No new lines
|
||||
return 0
|
||||
|
||||
# Parse new content
|
||||
logger.info(f"Processing {file_path} from line {start_line} to {total_lines}")
|
||||
conversation_data = parser.parse_conversation_file(file_path, start_line)
|
||||
|
||||
if not conversation_data:
|
||||
logger.warning(f"No data extracted from {file_path}")
|
||||
return 0
|
||||
|
||||
# Store in database
|
||||
await db.upsert_conversation(
|
||||
conversation_data['session_id'],
|
||||
{
|
||||
'project_path': conversation_data['project_path'],
|
||||
'git_branch': conversation_data.get('git_branch'),
|
||||
'started_at': conversation_data['started_at'],
|
||||
'last_updated_at': conversation_data['last_updated_at'],
|
||||
'message_count': conversation_data['message_count'],
|
||||
'user_message_count': conversation_data['user_message_count'],
|
||||
'assistant_message_count': conversation_data['assistant_message_count'],
|
||||
'total_input_tokens': conversation_data['total_input_tokens'],
|
||||
'total_output_tokens': conversation_data['total_output_tokens'],
|
||||
'model': conversation_data.get('model'),
|
||||
'slug': conversation_data.get('slug'),
|
||||
'file_path': file_path
|
||||
}
|
||||
)
|
||||
|
||||
# Store messages
|
||||
for msg in conversation_data['messages']:
|
||||
await db.insert_message(msg)
|
||||
|
||||
# Store tool calls
|
||||
for tool_call in conversation_data['tool_calls']:
|
||||
await db.insert_tool_call(tool_call)
|
||||
|
||||
# Update checkpoint
|
||||
await db.update_checkpoint(file_path, total_lines, file_mtime)
|
||||
|
||||
logger.info(f"Processed {len(conversation_data['messages'])} messages from {file_path}")
|
||||
return len(conversation_data['messages'])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {file_path}: {e}", exc_info=True)
|
||||
return 0
|
||||
|
||||
|
||||
async def collect_conversations():
|
||||
"""Main collection loop"""
|
||||
while True:
|
||||
try:
|
||||
logger.info("Starting conversation collection cycle")
|
||||
|
||||
# Discover files
|
||||
files = await discover_jsonl_files()
|
||||
logger.info(f"Found {len(files)} conversation files")
|
||||
|
||||
# Process each file
|
||||
total_messages = 0
|
||||
for file_path in files:
|
||||
messages_processed = await process_file(file_path)
|
||||
total_messages += messages_processed
|
||||
|
||||
if total_messages > 0:
|
||||
logger.info(f"Collection cycle complete: processed {total_messages} new messages")
|
||||
else:
|
||||
logger.debug("Collection cycle complete: no new messages")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in collection cycle: {e}", exc_info=True)
|
||||
|
||||
# Wait before next cycle
|
||||
await asyncio.sleep(COLLECT_INTERVAL)
|
||||
Reference in New Issue
Block a user