1ba0014d47
- 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
175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
import os
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
import asyncio
|
|
import aiosqlite
|
|
from anthropic import AsyncAnthropic
|
|
|
|
import db
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
|
|
ANTHROPIC_BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "https://www.bytecatcode.org")
|
|
SUMMARY_HOUR = int(os.environ.get("SUMMARY_HOUR", "23"))
|
|
TRIGGER_PATH = os.environ.get("TRIGGER_PATH", "/app/data/trigger")
|
|
|
|
|
|
async def generate_summary_for_date(date_str: str):
|
|
"""Generate summary for a specific date"""
|
|
try:
|
|
logger.info(f"Generating summary for {date_str}")
|
|
|
|
# Query conversations for this date
|
|
async with aiosqlite.connect(db.DB_PATH) as conn:
|
|
# Get conversations
|
|
cursor = await conn.execute("""
|
|
SELECT session_id, project_path, slug, message_count,
|
|
total_input_tokens, total_output_tokens
|
|
FROM conversations
|
|
WHERE DATE(started_at) = ? OR DATE(last_updated_at) = ?
|
|
ORDER BY started_at
|
|
""", (date_str, date_str))
|
|
conversations = await cursor.fetchall()
|
|
|
|
if not conversations:
|
|
logger.info(f"No conversations found for {date_str}")
|
|
return
|
|
|
|
# Get sample messages from each conversation
|
|
conversation_summaries = []
|
|
total_messages = 0
|
|
total_tokens = 0
|
|
projects = set()
|
|
|
|
for conv in conversations:
|
|
session_id, project_path, slug, msg_count, input_tokens, output_tokens = conv
|
|
projects.add(project_path)
|
|
total_messages += msg_count
|
|
total_tokens += (input_tokens + output_tokens)
|
|
|
|
# Get first few user messages to understand what was worked on
|
|
cursor = await conn.execute("""
|
|
SELECT content_text FROM messages
|
|
WHERE session_id = ? AND role = 'user' AND content_text IS NOT NULL
|
|
ORDER BY timestamp
|
|
LIMIT 3
|
|
""", (session_id,))
|
|
user_messages = await cursor.fetchall()
|
|
|
|
# Get tool usage
|
|
cursor = await conn.execute("""
|
|
SELECT tool_name, COUNT(*) as count
|
|
FROM tool_calls
|
|
WHERE message_uuid IN (
|
|
SELECT message_uuid FROM messages WHERE session_id = ?
|
|
)
|
|
GROUP BY tool_name
|
|
ORDER BY count DESC
|
|
LIMIT 5
|
|
""", (session_id,))
|
|
tools = await cursor.fetchall()
|
|
|
|
conversation_summaries.append({
|
|
'slug': slug or 'Untitled',
|
|
'project': project_path.split('/')[-1] if project_path else 'unknown',
|
|
'messages': msg_count,
|
|
'tokens': input_tokens + output_tokens,
|
|
'user_messages': [msg[0][:200] for msg in user_messages if msg[0]],
|
|
'tools': [f"{tool[0]} ({tool[1]}x)" for tool in tools]
|
|
})
|
|
|
|
# Format for Claude
|
|
formatted_convs = []
|
|
for i, conv in enumerate(conversation_summaries, 1):
|
|
formatted_convs.append(f"""
|
|
**Conversation {i}: {conv['slug']}**
|
|
- Project: {conv['project']}
|
|
- Messages: {conv['messages']} ({conv['tokens']} tokens)
|
|
- Initial requests: {', '.join(conv['user_messages'][:2]) if conv['user_messages'] else 'N/A'}
|
|
- Tools used: {', '.join(conv['tools']) if conv['tools'] else 'None'}
|
|
""")
|
|
|
|
prompt = f"""You are analyzing a developer's Claude Code conversations from {date_str}.
|
|
|
|
**Overview:**
|
|
- Conversations: {len(conversations)}
|
|
- Total messages: {total_messages}
|
|
- Total tokens: {total_tokens:,}
|
|
- Projects: {', '.join(sorted(projects))}
|
|
|
|
**Conversations:**
|
|
{''.join(formatted_convs)}
|
|
|
|
Generate a concise daily summary covering:
|
|
1. Main tasks and goals worked on
|
|
2. Key decisions and solutions implemented
|
|
3. Files and components modified (if evident from tool usage)
|
|
4. Challenges encountered and how they were resolved
|
|
5. Tools and patterns used
|
|
6. Overall progress and outcomes
|
|
|
|
Use markdown formatting. Be specific and technical. Focus on what was accomplished. Keep it under 500 words."""
|
|
|
|
# Call Claude API
|
|
client = AsyncAnthropic(api_key=ANTHROPIC_API_KEY, base_url=ANTHROPIC_BASE_URL)
|
|
response = await client.messages.create(
|
|
model="claude-haiku-4-5-20251001",
|
|
max_tokens=4096,
|
|
messages=[{"role": "user", "content": prompt}]
|
|
)
|
|
|
|
summary_content = response.content[0].text
|
|
|
|
# Store summary
|
|
await db.upsert_summary(date_str, {
|
|
'project_path': ', '.join(sorted(projects)),
|
|
'conversation_count': len(conversations),
|
|
'total_messages': total_messages,
|
|
'total_tokens': total_tokens,
|
|
'summary_content': summary_content
|
|
})
|
|
|
|
logger.info(f"Summary generated for {date_str}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating summary for {date_str}: {e}", exc_info=True)
|
|
|
|
|
|
async def check_trigger():
|
|
"""Check for manual trigger file"""
|
|
if os.path.exists(TRIGGER_PATH):
|
|
try:
|
|
os.remove(TRIGGER_PATH)
|
|
logger.info("Manual trigger detected")
|
|
# Generate summary for yesterday
|
|
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
await generate_summary_for_date(yesterday)
|
|
except Exception as e:
|
|
logger.error(f"Error processing trigger: {e}")
|
|
|
|
|
|
async def summarize_daily():
|
|
"""Daily summarization loop"""
|
|
while True:
|
|
try:
|
|
now = datetime.now()
|
|
|
|
# Check for manual trigger
|
|
await check_trigger()
|
|
|
|
# Check if it's time for daily summary
|
|
if now.hour == SUMMARY_HOUR and now.minute < 5:
|
|
yesterday = (now - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
await generate_summary_for_date(yesterday)
|
|
|
|
# Sleep until next hour to avoid duplicate runs
|
|
await asyncio.sleep(3600)
|
|
else:
|
|
# Check every 5 minutes
|
|
await asyncio.sleep(300)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in summarization loop: {e}", exc_info=True)
|
|
await asyncio.sleep(300)
|