✨ Features: - 🧠 Shadow learner that builds intelligence from command patterns - 🛡️ Smart command validation with safety checks - 💾 Automatic context monitoring and backup system - 🔄 Session continuity across Claude restarts 📚 Documentation: - Complete Diátaxis-organized documentation - Learning-oriented tutorial for getting started - Task-oriented how-to guides for specific problems - Information-oriented reference for quick lookup - Understanding-oriented explanations of architecture 🚀 Installation: - One-command installation script - Bootstrap prompt for installation via Claude - Cross-platform compatibility - Comprehensive testing suite 🎯 Ready for real-world use and community feedback! 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
348 lines
14 KiB
Python
348 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Session State Manager - Persistent session state and continuity"""
|
|
|
|
import json
|
|
import uuid
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Any, Set
|
|
|
|
try:
|
|
from .models import ToolExecution
|
|
except ImportError:
|
|
from models import ToolExecution
|
|
|
|
|
|
class SessionStateManager:
|
|
"""Manages persistent session state across Claude interactions"""
|
|
|
|
def __init__(self, state_dir: str = ".claude_hooks"):
|
|
self.state_dir = Path(state_dir)
|
|
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.state_file = self.state_dir / "session_state.json"
|
|
self.todos_file = Path("ACTIVE_TODOS.md")
|
|
self.last_session_file = Path("LAST_SESSION.md")
|
|
|
|
# Initialize session
|
|
self.session_id = str(uuid.uuid4())[:8]
|
|
self.current_state = self._load_or_create_state()
|
|
|
|
def _load_or_create_state(self) -> Dict[str, Any]:
|
|
"""Load existing state or create new session state"""
|
|
try:
|
|
if self.state_file.exists():
|
|
with open(self.state_file, 'r') as f:
|
|
state = json.load(f)
|
|
|
|
# Check if this is a continuation of recent session
|
|
last_activity = datetime.fromisoformat(state.get("last_activity", "1970-01-01"))
|
|
if (datetime.now() - last_activity).total_seconds() < 3600: # Within 1 hour
|
|
# Continue existing session
|
|
return state
|
|
|
|
# Create new session
|
|
return self._create_new_session()
|
|
|
|
except Exception:
|
|
# If loading fails, create new session
|
|
return self._create_new_session()
|
|
|
|
def _create_new_session(self) -> Dict[str, Any]:
|
|
"""Create new session state"""
|
|
return {
|
|
"session_id": self.session_id,
|
|
"start_time": datetime.now().isoformat(),
|
|
"last_activity": datetime.now().isoformat(),
|
|
"modified_files": [],
|
|
"commands_executed": [],
|
|
"tool_usage": {},
|
|
"backup_history": [],
|
|
"todos": [],
|
|
"context_snapshots": []
|
|
}
|
|
|
|
def update_from_tool_use(self, tool_data: Dict[str, Any]):
|
|
"""Update session state from tool usage"""
|
|
try:
|
|
tool = tool_data.get("tool", "")
|
|
params = tool_data.get("parameters", {})
|
|
timestamp = datetime.now().isoformat()
|
|
|
|
# Track file modifications
|
|
if tool in ["Edit", "Write", "MultiEdit"]:
|
|
file_path = params.get("file_path", "")
|
|
if file_path and file_path not in self.current_state["modified_files"]:
|
|
self.current_state["modified_files"].append(file_path)
|
|
|
|
# Track commands executed
|
|
if tool == "Bash":
|
|
command = params.get("command", "")
|
|
if command:
|
|
self.current_state["commands_executed"].append({
|
|
"command": command,
|
|
"timestamp": timestamp
|
|
})
|
|
|
|
# Keep only last 50 commands
|
|
if len(self.current_state["commands_executed"]) > 50:
|
|
self.current_state["commands_executed"] = self.current_state["commands_executed"][-50:]
|
|
|
|
# Track tool usage statistics
|
|
self.current_state["tool_usage"][tool] = self.current_state["tool_usage"].get(tool, 0) + 1
|
|
self.current_state["last_activity"] = timestamp
|
|
|
|
# Save state periodically
|
|
self._save_state()
|
|
|
|
except Exception:
|
|
pass # Don't let state tracking errors break the system
|
|
|
|
def add_backup(self, backup_id: str, backup_info: Dict[str, Any]):
|
|
"""Record backup in session history"""
|
|
try:
|
|
backup_record = {
|
|
"backup_id": backup_id,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"reason": backup_info.get("reason", "unknown"),
|
|
"success": backup_info.get("success", False)
|
|
}
|
|
|
|
self.current_state["backup_history"].append(backup_record)
|
|
|
|
# Keep only last 10 backups
|
|
if len(self.current_state["backup_history"]) > 10:
|
|
self.current_state["backup_history"] = self.current_state["backup_history"][-10:]
|
|
|
|
self._save_state()
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
def add_context_snapshot(self, context_data: Dict[str, Any]):
|
|
"""Add context snapshot for recovery"""
|
|
try:
|
|
snapshot = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"context_ratio": context_data.get("usage_ratio", 0.0),
|
|
"prompt_count": context_data.get("prompt_count", 0),
|
|
"tool_count": context_data.get("tool_executions", 0)
|
|
}
|
|
|
|
self.current_state["context_snapshots"].append(snapshot)
|
|
|
|
# Keep only last 20 snapshots
|
|
if len(self.current_state["context_snapshots"]) > 20:
|
|
self.current_state["context_snapshots"] = self.current_state["context_snapshots"][-20:]
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
def update_todos(self, todos: List[Dict[str, Any]]):
|
|
"""Update active todos list"""
|
|
try:
|
|
self.current_state["todos"] = todos
|
|
self._save_state()
|
|
self._update_todos_file()
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
def get_session_summary(self) -> Dict[str, Any]:
|
|
"""Generate comprehensive session summary"""
|
|
try:
|
|
return {
|
|
"session_id": self.current_state.get("session_id", "unknown"),
|
|
"start_time": self.current_state.get("start_time", "unknown"),
|
|
"last_activity": self.current_state.get("last_activity", "unknown"),
|
|
"modified_files": self.current_state.get("modified_files", []),
|
|
"tool_usage": self.current_state.get("tool_usage", {}),
|
|
"commands_executed": self.current_state.get("commands_executed", []),
|
|
"backup_history": self.current_state.get("backup_history", []),
|
|
"todos": self.current_state.get("todos", []),
|
|
"session_stats": self._calculate_session_stats()
|
|
}
|
|
except Exception:
|
|
return {"error": "Failed to generate session summary"}
|
|
|
|
def _calculate_session_stats(self) -> Dict[str, Any]:
|
|
"""Calculate session statistics"""
|
|
try:
|
|
total_tools = sum(self.current_state.get("tool_usage", {}).values())
|
|
total_commands = len(self.current_state.get("commands_executed", []))
|
|
total_files = len(self.current_state.get("modified_files", []))
|
|
|
|
start_time = datetime.fromisoformat(self.current_state.get("start_time", datetime.now().isoformat()))
|
|
duration = datetime.now() - start_time
|
|
|
|
return {
|
|
"duration_minutes": round(duration.total_seconds() / 60, 1),
|
|
"total_tool_calls": total_tools,
|
|
"total_commands": total_commands,
|
|
"total_files_modified": total_files,
|
|
"most_used_tools": self._get_top_tools(3)
|
|
}
|
|
except Exception:
|
|
return {}
|
|
|
|
def _get_top_tools(self, count: int) -> List[Dict[str, Any]]:
|
|
"""Get most frequently used tools"""
|
|
try:
|
|
tool_usage = self.current_state.get("tool_usage", {})
|
|
sorted_tools = sorted(tool_usage.items(), key=lambda x: x[1], reverse=True)
|
|
return [{"tool": tool, "count": usage} for tool, usage in sorted_tools[:count]]
|
|
except Exception:
|
|
return []
|
|
|
|
def create_continuation_docs(self):
|
|
"""Create LAST_SESSION.md and ACTIVE_TODOS.md"""
|
|
try:
|
|
self._create_last_session_doc()
|
|
self._update_todos_file()
|
|
except Exception:
|
|
pass # Don't let doc creation errors break the system
|
|
|
|
def _create_last_session_doc(self):
|
|
"""Create LAST_SESSION.md with session summary"""
|
|
try:
|
|
summary = self.get_session_summary()
|
|
|
|
content = f"""# Last Claude Session Summary
|
|
|
|
**Session ID**: {summary['session_id']}
|
|
**Duration**: {summary['start_time']} → {summary['last_activity']}
|
|
**Session Length**: {summary.get('session_stats', {}).get('duration_minutes', 0)} minutes
|
|
|
|
## Files Modified ({len(summary['modified_files'])})
|
|
"""
|
|
|
|
for file_path in summary['modified_files']:
|
|
content += f"- {file_path}\n"
|
|
|
|
content += f"\n## Tools Used ({summary.get('session_stats', {}).get('total_tool_calls', 0)} total)\n"
|
|
|
|
for tool, count in summary['tool_usage'].items():
|
|
content += f"- {tool}: {count} times\n"
|
|
|
|
content += f"\n## Recent Commands ({len(summary['commands_executed'])})\n"
|
|
|
|
# Show last 10 commands
|
|
recent_commands = summary['commands_executed'][-10:]
|
|
for cmd_info in recent_commands:
|
|
timestamp = cmd_info['timestamp'][:19] # Remove microseconds
|
|
content += f"- `{cmd_info['command']}` ({timestamp})\n"
|
|
|
|
content += f"\n## Backup History\n"
|
|
|
|
for backup in summary['backup_history']:
|
|
status = "✅" if backup['success'] else "❌"
|
|
content += f"- {status} {backup['backup_id']} - {backup['reason']} ({backup['timestamp'][:19]})\n"
|
|
|
|
content += f"""
|
|
## To Continue This Session
|
|
|
|
1. **Review Modified Files**: Check the files listed above for your recent changes
|
|
2. **Check Active Tasks**: Review `ACTIVE_TODOS.md` for pending work
|
|
3. **Restore Context**: Reference the commands and tools used above
|
|
4. **Use Backups**: If needed, restore from backup using `claude-hooks restore {summary['backup_history'][-1]['backup_id'] if summary['backup_history'] else 'latest'}`
|
|
|
|
## Quick Commands
|
|
|
|
```bash
|
|
# View current project status
|
|
git status
|
|
|
|
# Check for any uncommitted changes
|
|
git diff
|
|
|
|
# List available backups
|
|
claude-hooks list-backups
|
|
|
|
# Continue with active todos
|
|
cat ACTIVE_TODOS.md
|
|
```
|
|
"""
|
|
|
|
with open(self.last_session_file, 'w') as f:
|
|
f.write(content)
|
|
|
|
except Exception as e:
|
|
# Create minimal doc on error
|
|
try:
|
|
with open(self.last_session_file, 'w') as f:
|
|
f.write(f"# Last Session\n\nSession ended at {datetime.now().isoformat()}\n\nError creating summary: {e}\n")
|
|
except Exception:
|
|
pass
|
|
|
|
def _update_todos_file(self):
|
|
"""Update ACTIVE_TODOS.md file"""
|
|
try:
|
|
todos = self.current_state.get("todos", [])
|
|
|
|
if not todos:
|
|
content = """# Active TODOs
|
|
|
|
*No active todos. Add some to track your progress!*
|
|
|
|
## How to Add TODOs
|
|
|
|
Use Claude's TodoWrite tool to manage your task list:
|
|
- Track progress across sessions
|
|
- Break down complex tasks
|
|
- Never lose track of what you're working on
|
|
"""
|
|
else:
|
|
content = f"""# Active TODOs
|
|
|
|
*Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
|
|
|
"""
|
|
|
|
# Group by status
|
|
pending_todos = [t for t in todos if t.get('status') == 'pending']
|
|
in_progress_todos = [t for t in todos if t.get('status') == 'in_progress']
|
|
completed_todos = [t for t in todos if t.get('status') == 'completed']
|
|
|
|
if in_progress_todos:
|
|
content += "## 🚀 In Progress\n\n"
|
|
for todo in in_progress_todos:
|
|
priority = todo.get('priority', 'medium')
|
|
priority_emoji = {'high': '🔥', 'medium': '⭐', 'low': '📝'}.get(priority, '⭐')
|
|
content += f"- {priority_emoji} {todo.get('content', 'Unknown task')}\n"
|
|
content += "\n"
|
|
|
|
if pending_todos:
|
|
content += "## 📋 Pending\n\n"
|
|
for todo in pending_todos:
|
|
priority = todo.get('priority', 'medium')
|
|
priority_emoji = {'high': '🔥', 'medium': '⭐', 'low': '📝'}.get(priority, '⭐')
|
|
content += f"- {priority_emoji} {todo.get('content', 'Unknown task')}\n"
|
|
content += "\n"
|
|
|
|
if completed_todos:
|
|
content += "## ✅ Completed\n\n"
|
|
for todo in completed_todos[-5:]: # Show last 5 completed
|
|
content += f"- ✅ {todo.get('content', 'Unknown task')}\n"
|
|
content += "\n"
|
|
|
|
with open(self.todos_file, 'w') as f:
|
|
f.write(content)
|
|
|
|
except Exception:
|
|
pass # Don't let todo file creation break the system
|
|
|
|
def _save_state(self):
|
|
"""Save current state to disk"""
|
|
try:
|
|
with open(self.state_file, 'w') as f:
|
|
json.dump(self.current_state, f, indent=2)
|
|
except Exception:
|
|
pass # Don't let state saving errors break the system
|
|
|
|
def cleanup_session(self):
|
|
"""Clean up session and create final documentation"""
|
|
try:
|
|
self.create_continuation_docs()
|
|
self._save_state()
|
|
except Exception:
|
|
pass |