- /search command to search across memory, history, and configuration - history-browser.py for browsing and analyzing session history - install.sh for first-time setup with directory creation and validation - daily-maintenance.sh for scheduled backup, cleanup, and validation - systemd timer units for automated daily maintenance at 6 AM - Updated shell completions with 11 aliases - Test suite now covers 19 tests - Bump version to 1.1.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
185 lines
5.8 KiB
Python
Executable File
185 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Browse and analyze session history.
|
|
Usage: python3 history-browser.py [--list|--show <id>|--stats|--unsummarized]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
CLAUDE_DIR = Path.home() / ".claude"
|
|
HISTORY_DIR = CLAUDE_DIR / "state" / "personal-assistant" / "history"
|
|
|
|
|
|
def load_index() -> Optional[Dict]:
|
|
"""Load history index."""
|
|
index_path = HISTORY_DIR / "index.json"
|
|
try:
|
|
with open(index_path) as f:
|
|
return json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return None
|
|
|
|
|
|
def list_sessions(limit: int = 20, show_all: bool = False):
|
|
"""List recent sessions."""
|
|
index = load_index()
|
|
if not index or "sessions" not in index:
|
|
print("No sessions found.")
|
|
return
|
|
|
|
sessions = index["sessions"]
|
|
if not show_all:
|
|
sessions = sessions[-limit:]
|
|
|
|
print(f"\n📜 Session History ({len(sessions)} of {len(index['sessions'])} total)\n")
|
|
print(f"{'ID':<12} {'Date':<12} {'Summarized':<12} {'Topics'}")
|
|
print("-" * 70)
|
|
|
|
for session in reversed(sessions):
|
|
sid = session.get("id", "unknown")[:10]
|
|
date = session.get("date", "unknown")[:10]
|
|
summarized = "✓" if session.get("summarized", False) else "○"
|
|
topics = ", ".join(session.get("topics", [])[:3])
|
|
if len(session.get("topics", [])) > 3:
|
|
topics += "..."
|
|
|
|
print(f"{sid:<12} {date:<12} {summarized:<12} {topics}")
|
|
|
|
print("")
|
|
|
|
|
|
def show_session(session_id: str):
|
|
"""Show details for a specific session."""
|
|
index = load_index()
|
|
if not index or "sessions" not in index:
|
|
print("No sessions found.")
|
|
return
|
|
|
|
# Find session by ID prefix
|
|
session = None
|
|
for s in index["sessions"]:
|
|
if s.get("id", "").startswith(session_id):
|
|
session = s
|
|
break
|
|
|
|
if not session:
|
|
print(f"Session '{session_id}' not found.")
|
|
return
|
|
|
|
print(f"\n📜 Session: {session.get('id', 'unknown')}")
|
|
print(f" Date: {session.get('date', 'unknown')}")
|
|
print(f" Summarized: {'Yes' if session.get('summarized') else 'No'}")
|
|
print(f"\n Topics:")
|
|
for topic in session.get("topics", []):
|
|
print(f" - {topic}")
|
|
print(f"\n Summary:")
|
|
print(f" {session.get('summary', 'No summary available')}")
|
|
|
|
# Check for associated JSONL file
|
|
jsonl_path = HISTORY_DIR / f"{session.get('id', '')}.jsonl"
|
|
if jsonl_path.exists():
|
|
print(f"\n Log file: {jsonl_path}")
|
|
try:
|
|
with open(jsonl_path) as f:
|
|
lines = f.readlines()
|
|
print(f" Log entries: {len(lines)}")
|
|
except Exception:
|
|
pass
|
|
print("")
|
|
|
|
|
|
def show_stats():
|
|
"""Show session statistics."""
|
|
index = load_index()
|
|
if not index or "sessions" not in index:
|
|
print("No sessions found.")
|
|
return
|
|
|
|
sessions = index["sessions"]
|
|
total = len(sessions)
|
|
summarized = sum(1 for s in sessions if s.get("summarized", False))
|
|
unsummarized = total - summarized
|
|
|
|
# Topic frequency
|
|
topic_counts: Dict[str, int] = {}
|
|
for s in sessions:
|
|
for topic in s.get("topics", []):
|
|
topic_counts[topic] = topic_counts.get(topic, 0) + 1
|
|
|
|
top_topics = sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
|
|
# Date range
|
|
dates = [s.get("date", "") for s in sessions if s.get("date")]
|
|
oldest = min(dates)[:10] if dates else "N/A"
|
|
newest = max(dates)[:10] if dates else "N/A"
|
|
|
|
print(f"\n📊 Session Statistics\n")
|
|
print(f" Total sessions: {total}")
|
|
print(f" Summarized: {summarized}")
|
|
print(f" Pending summary: {unsummarized}")
|
|
print(f" Date range: {oldest} to {newest}")
|
|
print(f"\n Top Topics:")
|
|
for topic, count in top_topics:
|
|
bar = "█" * min(count, 20)
|
|
print(f" {topic:<30} {bar} ({count})")
|
|
print("")
|
|
|
|
|
|
def list_unsummarized():
|
|
"""List sessions that need summarization."""
|
|
index = load_index()
|
|
if not index or "sessions" not in index:
|
|
print("No sessions found.")
|
|
return
|
|
|
|
unsummarized = [s for s in index["sessions"] if not s.get("summarized", False)]
|
|
|
|
if not unsummarized:
|
|
print("\n✓ All sessions are summarized!\n")
|
|
return
|
|
|
|
print(f"\n⚠ Unsummarized Sessions ({len(unsummarized)})\n")
|
|
print(f"{'ID':<12} {'Date':<12} {'Topics'}")
|
|
print("-" * 60)
|
|
|
|
for session in unsummarized:
|
|
sid = session.get("id", "unknown")[:10]
|
|
date = session.get("date", "unknown")[:10]
|
|
topics = ", ".join(session.get("topics", [])[:3]) or "No topics"
|
|
|
|
print(f"{sid:<12} {date:<12} {topics}")
|
|
|
|
print(f"\nRun /summarize to process these sessions.\n")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Browse session history")
|
|
parser.add_argument("--list", "-l", action="store_true", help="List recent sessions")
|
|
parser.add_argument("--all", "-a", action="store_true", help="Show all sessions (with --list)")
|
|
parser.add_argument("--show", "-s", type=str, help="Show session details by ID")
|
|
parser.add_argument("--stats", action="store_true", help="Show statistics")
|
|
parser.add_argument("--unsummarized", "-u", action="store_true",
|
|
help="List unsummarized sessions")
|
|
parser.add_argument("--limit", "-n", type=int, default=20,
|
|
help="Number of sessions to show (default: 20)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.show:
|
|
show_session(args.show)
|
|
elif args.stats:
|
|
show_stats()
|
|
elif args.unsummarized:
|
|
list_unsummarized()
|
|
elif args.list or not any([args.show, args.stats, args.unsummarized]):
|
|
list_sessions(limit=args.limit, show_all=args.all)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|