diff --git a/.gitignore b/.gitignore index 7d55d65..1ce277d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,15 @@ projects/ *.tmp *.swp +# Python cache +__pycache__/ +*.pyc +*.pyo + +# Go test output +*_test_output.txt +tmp_unused + # Todos (managed by Claude Code) todos/ repos/homelab diff --git a/CHANGELOG.md b/CHANGELOG.md index 8145340..2b55f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this Claude Code configuration. +## [1.1.0] - 2026-01-01 + +### Added + +#### Commands +- `/search` - Search across memory, history, and configuration +- `/remember` - Quick shortcut to save to memory +- `/config` - View and manage configuration settings + +#### Automation Scripts +- `search.py` - Search memory, history, config +- `history-browser.py` - Browse and analyze session history +- `install.sh` - First-time setup script +- `daily-maintenance.sh` - Scheduled maintenance tasks +- `memory-add.py` - Add items to PA memory +- `memory-list.py` - List memory items by category +- `test-scripts.sh` - Test suite for all scripts + +#### Systemd Integration +- `systemd/claude-maintenance.service` - Maintenance service unit +- `systemd/claude-maintenance.timer` - Daily timer (6 AM) +- `systemd/install-timers.sh` - Timer installation script + +#### Shell Completions +- `completions.bash` - Bash completions and aliases +- `completions.zsh` - Zsh completions and aliases + +### Changed +- Component registry now tracks automation scripts +- Shell completions expanded with 11 aliases +- Test suite now covers 19 tests + +--- + ## [1.0.0] - 2026-01-01 ### Added diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/automation/completions.bash b/automation/completions.bash index 3efb5ec..93a024c 100644 --- a/automation/completions.bash +++ b/automation/completions.bash @@ -4,11 +4,23 @@ _claude_automation() { local cur="${COMP_WORDS[COMP_CWORD]}" - local scripts="validate-setup quick-status backup restore clean memory-add memory-list" + local scripts="validate-setup quick-status backup restore clean memory-add memory-list search history install test maintenance" COMPREPLY=($(compgen -W "${scripts}" -- "${cur}")) } +_claude_search() { + local cur="${COMP_WORDS[COMP_CWORD]}" + + COMPREPLY=($(compgen -W "--memory --history --config --recent" -- "${cur}")) +} + +_claude_history() { + local cur="${COMP_WORDS[COMP_CWORD]}" + + COMPREPLY=($(compgen -W "--list --show --stats --unsummarized --all --limit" -- "${cur}")) +} + _claude_memory_add() { local cur="${COMP_WORDS[COMP_CWORD]}" local prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -38,6 +50,8 @@ _claude_restore() { complete -F _claude_memory_add memory-add.py complete -F _claude_memory_list memory-list.py complete -F _claude_restore restore.sh +complete -F _claude_search search.py +complete -F _claude_history history-browser.py # Alias completions for convenience alias claude-validate='~/.claude/automation/validate-setup.sh' @@ -47,7 +61,13 @@ alias claude-restore='~/.claude/automation/restore.sh' alias claude-clean='~/.claude/automation/clean.sh' alias claude-memory-add='python3 ~/.claude/automation/memory-add.py' alias claude-memory-list='python3 ~/.claude/automation/memory-list.py' +alias claude-search='python3 ~/.claude/automation/search.py' +alias claude-history='python3 ~/.claude/automation/history-browser.py' +alias claude-install='~/.claude/automation/install.sh' +alias claude-test='~/.claude/automation/test-scripts.sh' +alias claude-maintenance='~/.claude/automation/daily-maintenance.sh' echo "Claude Code completions loaded. Available aliases:" -echo " claude-validate, claude-status, claude-backup, claude-restore" -echo " claude-clean, claude-memory-add, claude-memory-list" +echo " claude-validate, claude-status, claude-backup, claude-restore, claude-clean" +echo " claude-memory-add, claude-memory-list, claude-search, claude-history" +echo " claude-install, claude-test, claude-maintenance" diff --git a/automation/completions.zsh b/automation/completions.zsh index da41c58..221dd91 100644 --- a/automation/completions.zsh +++ b/automation/completions.zsh @@ -43,10 +43,33 @@ _claude_restore() { fi } +# Search completion +_claude_search() { + _arguments \ + '--memory[Search only memory]' \ + '--history[Search only history]' \ + '--config[Search only config]' \ + '--recent[Show recent items]:days:' \ + '*:query:' +} + +# History browser completion +_claude_history() { + _arguments \ + '--list[List recent sessions]' \ + '--all[Show all sessions]' \ + '--show[Show session details]:session_id:' \ + '--stats[Show statistics]' \ + '--unsummarized[List unsummarized sessions]' \ + '--limit[Limit results]:count:' +} + # Register completions compdef _memory_add memory-add.py compdef _memory_list memory-list.py compdef _claude_restore restore.sh +compdef _claude_search search.py +compdef _claude_history history-browser.py # Aliases alias claude-validate='~/.claude/automation/validate-setup.sh' @@ -56,5 +79,12 @@ alias claude-restore='~/.claude/automation/restore.sh' alias claude-clean='~/.claude/automation/clean.sh' alias claude-memory-add='python3 ~/.claude/automation/memory-add.py' alias claude-memory-list='python3 ~/.claude/automation/memory-list.py' +alias claude-search='python3 ~/.claude/automation/search.py' +alias claude-history='python3 ~/.claude/automation/history-browser.py' +alias claude-install='~/.claude/automation/install.sh' +alias claude-test='~/.claude/automation/test-scripts.sh' +alias claude-maintenance='~/.claude/automation/daily-maintenance.sh' echo "Claude Code completions loaded (zsh)" +echo " Aliases: claude-{validate,status,backup,restore,clean,memory-add,memory-list}" +echo " claude-{search,history,install,test,maintenance}" diff --git a/automation/daily-maintenance.sh b/automation/daily-maintenance.sh new file mode 100755 index 0000000..54641bf --- /dev/null +++ b/automation/daily-maintenance.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Daily maintenance script for Claude Code configuration +# Runs via systemd timer or manually + +set -euo pipefail + +CLAUDE_DIR="${HOME}/.claude" +LOG_DIR="${CLAUDE_DIR}/logs" +LOG_FILE="${LOG_DIR}/maintenance-$(date +%Y-%m-%d).log" + +mkdir -p "${LOG_DIR}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}" +} + +log "=== Daily Maintenance Started ===" + +# 1. Validate configuration +log "Running validation..." +if "${CLAUDE_DIR}/automation/validate-setup.sh" >> "${LOG_FILE}" 2>&1; then + log "✓ Validation passed" +else + log "⚠ Validation had warnings" +fi + +# 2. Clean old files +log "Running cleanup..." +"${CLAUDE_DIR}/automation/clean.sh" >> "${LOG_FILE}" 2>&1 || true +log "✓ Cleanup complete" + +# 3. Create daily backup +log "Creating backup..." +if "${CLAUDE_DIR}/automation/backup.sh" >> "${LOG_FILE}" 2>&1; then + log "✓ Backup created" +else + log "⚠ Backup failed" +fi + +# 4. Check for unsummarized sessions +log "Checking session history..." +unsummarized=$(python3 -c " +import json +from pathlib import Path +idx = Path.home() / '.claude/state/personal-assistant/history/index.json' +if idx.exists(): + data = json.load(open(idx)) + count = sum(1 for s in data.get('sessions', []) if not s.get('summarized', False)) + print(count) +else: + print(0) +" 2>/dev/null || echo "0") + +if [[ "${unsummarized}" -gt 0 ]]; then + log "⚠ ${unsummarized} unsummarized session(s) pending" +else + log "✓ All sessions summarized" +fi + +# 5. Rotate old maintenance logs +log "Rotating old logs..." +find "${LOG_DIR}" -name "maintenance-*.log" -mtime +30 -delete 2>/dev/null || true +log "✓ Log rotation complete" + +log "=== Daily Maintenance Complete ===" diff --git a/automation/history-browser.py b/automation/history-browser.py new file mode 100755 index 0000000..69874b6 --- /dev/null +++ b/automation/history-browser.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +Browse and analyze session history. +Usage: python3 history-browser.py [--list|--show |--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() diff --git a/automation/install.sh b/automation/install.sh new file mode 100755 index 0000000..42b1f77 --- /dev/null +++ b/automation/install.sh @@ -0,0 +1,225 @@ +#!/bin/bash +# Install/setup script for Claude Code configuration +# Run: ./install.sh + +set -euo pipefail + +CLAUDE_DIR="${HOME}/.claude" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}ℹ${NC} $1"; } +success() { echo -e "${GREEN}✓${NC} $1"; } +warn() { echo -e "${YELLOW}⚠${NC} $1"; } +error() { echo -e "${RED}✗${NC} $1"; } + +echo "🚀 Claude Code Configuration Setup" +echo "" + +# Check if this is the right directory +if [[ ! -f "${CLAUDE_DIR}/CLAUDE.md" ]]; then + error "CLAUDE.md not found. Are you in the right directory?" + exit 1 +fi + +# Step 1: Create required directories +info "Creating directories..." +dirs=( + "${CLAUDE_DIR}/state/sysadmin" + "${CLAUDE_DIR}/state/programmer" + "${CLAUDE_DIR}/state/personal-assistant/memory" + "${CLAUDE_DIR}/state/personal-assistant/history" + "${CLAUDE_DIR}/backups" + "${CLAUDE_DIR}/logs" + "${CLAUDE_DIR}/mcp/gmail" + "${CLAUDE_DIR}/mcp/delegation" +) + +for dir in "${dirs[@]}"; do + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" + success "Created $dir" + fi +done + +# Step 2: Initialize state files if missing +info "Initializing state files..." + +# PA preferences +pa_prefs="${CLAUDE_DIR}/state/personal-assistant-preferences.json" +if [[ ! -f "$pa_prefs" ]]; then + cat > "$pa_prefs" << 'EOF' +{ + "version": "1.0", + "context_level": "moderate", + "created": "$(date -Iseconds)" +} +EOF + success "Created personal-assistant-preferences.json" +fi + +# Session context +session_ctx="${CLAUDE_DIR}/state/personal-assistant/session-context.json" +if [[ ! -f "$session_ctx" ]]; then + echo '{"context_level": null}' > "$session_ctx" + success "Created session-context.json" +fi + +# General instructions +gi_file="${CLAUDE_DIR}/state/personal-assistant/general-instructions.json" +if [[ ! -f "$gi_file" ]]; then + cat > "$gi_file" << 'EOF' +{ + "version": "1.0", + "instructions": [] +} +EOF + success "Created general-instructions.json" +fi + +# Memory files +memory_files=("preferences" "decisions" "projects" "facts") +for mem in "${memory_files[@]}"; do + mem_file="${CLAUDE_DIR}/state/personal-assistant/memory/${mem}.json" + if [[ ! -f "$mem_file" ]]; then + cat > "$mem_file" << EOF +{ + "version": "1.0", + "description": "${mem^} learned from sessions", + "items": [] +} +EOF + success "Created memory/${mem}.json" + fi +done + +# Memory meta +meta_file="${CLAUDE_DIR}/state/personal-assistant/memory/meta.json" +if [[ ! -f "$meta_file" ]]; then + cat > "$meta_file" << 'EOF' +{ + "version": "1.0", + "last_updated": null, + "last_summarized_session": null +} +EOF + success "Created memory/meta.json" +fi + +# History index +history_idx="${CLAUDE_DIR}/state/personal-assistant/history/index.json" +if [[ ! -f "$history_idx" ]]; then + cat > "$history_idx" << 'EOF' +{ + "version": "1.0", + "sessions": [] +} +EOF + success "Created history/index.json" +fi + +# KB files +for kb in "${CLAUDE_DIR}/state/kb.json" "${CLAUDE_DIR}/state/personal-assistant/kb.json"; do + if [[ ! -f "$kb" ]]; then + echo '{}' > "$kb" + success "Created $(basename "$kb")" + fi +done + +# Sysadmin autonomy +sysadmin_auto="${CLAUDE_DIR}/state/sysadmin/session-autonomy.json" +if [[ ! -f "$sysadmin_auto" ]]; then + cat > "$sysadmin_auto" << 'EOF' +{ + "level": "conservative", + "set_at": null, + "expires": null +} +EOF + success "Created sysadmin/session-autonomy.json" +fi + +# Step 3: Make scripts executable +info "Setting script permissions..." +find "${CLAUDE_DIR}" -name "*.sh" -type f -exec chmod +x {} \; +find "${CLAUDE_DIR}" -name "*.py" -type f -exec chmod +x {} \; +success "Made scripts executable" + +# Step 4: Validate JSON files +info "Validating JSON files..." +json_errors=0 +while IFS= read -r -d '' file; do + if ! python3 -c "import json; json.load(open('$file'))" 2>/dev/null; then + error "Invalid JSON: $file" + json_errors=$((json_errors + 1)) + fi +done < <(find "${CLAUDE_DIR}/state" -name "*.json" -print0) + +if [[ $json_errors -eq 0 ]]; then + success "All JSON files valid" +else + warn "$json_errors JSON file(s) have errors" +fi + +# Step 5: Check Python dependencies +info "Checking Python dependencies..." +missing_deps=() + +python3 -c "import google.oauth2.credentials" 2>/dev/null || missing_deps+=("google-auth") +python3 -c "import googleapiclient.discovery" 2>/dev/null || missing_deps+=("google-api-python-client") + +if [[ ${#missing_deps[@]} -gt 0 ]]; then + warn "Missing Python packages for Gmail/Calendar:" + for dep in "${missing_deps[@]}"; do + echo " - $dep" + done + echo "" + echo "Install with: pip install ${missing_deps[*]}" + echo "Or set up venv: cd ~/.claude/mcp/gmail && python3 -m venv venv && source venv/bin/activate && pip install gmail-mcp" +else + success "Python dependencies available" +fi + +# Step 6: Check kubectl +info "Checking kubectl..." +if command -v kubectl &> /dev/null; then + success "kubectl found: $(kubectl version --client --short 2>/dev/null || echo 'installed')" +else + warn "kubectl not found (k8s features will be limited)" +fi + +# Step 7: Shell completions hint +echo "" +info "Shell completions available. Add to your shell config:" +echo "" +echo " # For bash" +echo " source ~/.claude/automation/completions.bash" +echo "" +echo " # For zsh" +echo " source ~/.claude/automation/completions.zsh" +echo "" + +# Step 8: Run validation +echo "" +info "Running full validation..." +if "${CLAUDE_DIR}/automation/validate-setup.sh"; then + echo "" + success "Setup complete!" +else + echo "" + warn "Setup complete with warnings. Review above." +fi + +echo "" +echo "📚 Quick start:" +echo " claude # Start Claude Code" +echo " /help # Show available commands" +echo " /status # Quick status dashboard" +echo " /pa # Ask personal assistant" +echo "" diff --git a/automation/search.py b/automation/search.py new file mode 100755 index 0000000..505de3d --- /dev/null +++ b/automation/search.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +Search across PA memory, history, and configuration. +Usage: python3 search.py [--memory|--history|--config|--recent [days]] +""" + +import argparse +import json +import os +import re +import sys +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional + +CLAUDE_DIR = Path.home() / ".claude" +STATE_DIR = CLAUDE_DIR / "state" +PA_DIR = STATE_DIR / "personal-assistant" +MEMORY_DIR = PA_DIR / "memory" +HISTORY_DIR = PA_DIR / "history" + + +def load_json(path: Path) -> Optional[Dict]: + """Load JSON file safely.""" + try: + with open(path) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return None + + +def search_memory(query: str, case_insensitive: bool = True) -> List[Dict]: + """Search through memory files.""" + results = [] + pattern = re.compile(query, re.IGNORECASE if case_insensitive else 0) + + memory_files = ["preferences.json", "decisions.json", "projects.json", "facts.json"] + + for filename in memory_files: + data = load_json(MEMORY_DIR / filename) + if not data or "items" not in data: + continue + + category = filename.replace(".json", "") + for item in data["items"]: + content = item.get("content", "") + context = item.get("context", "") + + if pattern.search(content) or pattern.search(context): + results.append({ + "source": f"memory/{category}", + "date": item.get("date", "unknown"), + "content": content, + "context": context, + "id": item.get("id", "") + }) + + # Also search general-instructions.json + gi_data = load_json(PA_DIR / "general-instructions.json") + if gi_data and "instructions" in gi_data: + for item in gi_data["instructions"]: + if item.get("status") != "active": + continue + content = item.get("instruction", "") + if pattern.search(content): + results.append({ + "source": "memory/general-instructions", + "date": item.get("created", "unknown"), + "content": content, + "context": "", + "id": item.get("id", "") + }) + + return results + + +def search_history(query: str, case_insensitive: bool = True) -> List[Dict]: + """Search through session history.""" + results = [] + pattern = re.compile(query, re.IGNORECASE if case_insensitive else 0) + + index_path = HISTORY_DIR / "index.json" + index_data = load_json(index_path) + + if not index_data or "sessions" not in index_data: + return results + + for session in index_data["sessions"]: + session_id = session.get("id", "") + summary = session.get("summary", "") + topics = session.get("topics", []) + date = session.get("date", "unknown") + + # Search in summary and topics + topics_str = " ".join(topics) if topics else "" + if pattern.search(summary) or pattern.search(topics_str): + results.append({ + "source": "history", + "date": date, + "session_id": session_id, + "content": summary, + "topics": topics + }) + + return results + + +def search_config(query: str, case_insensitive: bool = True) -> List[Dict]: + """Search through configuration files.""" + results = [] + pattern = re.compile(query, re.IGNORECASE if case_insensitive else 0) + + config_files = [ + STATE_DIR / "component-registry.json", + STATE_DIR / "system-instructions.json", + STATE_DIR / "autonomy-levels.json", + STATE_DIR / "model-policy.json", + STATE_DIR / "kb.json", + PA_DIR / "kb.json", + ] + + for config_path in config_files: + if not config_path.exists(): + continue + + try: + content = config_path.read_text() + if pattern.search(content): + # Find matching lines + matches = [] + for i, line in enumerate(content.split('\n'), 1): + if pattern.search(line): + matches.append(f"L{i}: {line.strip()[:100]}") + + results.append({ + "source": f"config/{config_path.name}", + "matches": matches[:5], # Limit to 5 matches per file + "total_matches": len(matches) + }) + except Exception: + continue + + return results + + +def get_recent_items(days: int = 7) -> List[Dict]: + """Get items from the last N days.""" + results = [] + cutoff = datetime.now() - timedelta(days=days) + + # Check memory files + memory_files = ["preferences.json", "decisions.json", "projects.json", "facts.json"] + + for filename in memory_files: + data = load_json(MEMORY_DIR / filename) + if not data or "items" not in data: + continue + + category = filename.replace(".json", "") + for item in data["items"]: + date_str = item.get("date", "") + try: + item_date = datetime.strptime(date_str, "%Y-%m-%d") + if item_date >= cutoff: + results.append({ + "source": f"memory/{category}", + "date": date_str, + "content": item.get("content", ""), + "type": "memory" + }) + except ValueError: + continue + + # Check history + index_data = load_json(HISTORY_DIR / "index.json") + if index_data and "sessions" in index_data: + for session in index_data["sessions"]: + date_str = session.get("date", "") + try: + session_date = datetime.strptime(date_str[:10], "%Y-%m-%d") + if session_date >= cutoff: + results.append({ + "source": "history", + "date": date_str, + "content": session.get("summary", "No summary"), + "type": "session", + "session_id": session.get("id", "") + }) + except ValueError: + continue + + # Sort by date, newest first + results.sort(key=lambda x: x.get("date", ""), reverse=True) + return results + + +def format_results(results: List[Dict], search_type: str) -> str: + """Format search results for display.""" + if not results: + return f"No results found in {search_type}.\n" + + lines = [f"\n=== {search_type.title()} Results ({len(results)}) ===\n"] + + for r in results: + source = r.get("source", "unknown") + date = r.get("date", "") + + if "matches" in r: + # Config result + lines.append(f"📄 {source}") + lines.append(f" {r.get('total_matches', 0)} matches found:") + for match in r.get("matches", []): + lines.append(f" {match}") + elif "session_id" in r: + # History result + lines.append(f"📜 {date[:10]} - Session: {r.get('session_id', '')[:8]}...") + lines.append(f" {r.get('content', '')[:200]}") + if r.get("topics"): + lines.append(f" Topics: {', '.join(r['topics'][:5])}") + else: + # Memory result + lines.append(f"💾 [{source}] {date}") + lines.append(f" {r.get('content', '')[:200]}") + if r.get("context"): + lines.append(f" Context: {r.get('context', '')[:100]}") + + lines.append("") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Search PA memory, history, and configuration" + ) + parser.add_argument("query", nargs="*", help="Search query") + parser.add_argument("--memory", action="store_true", help="Search only memory") + parser.add_argument("--history", action="store_true", help="Search only history") + parser.add_argument("--config", action="store_true", help="Search only config") + parser.add_argument("--recent", type=int, nargs="?", const=7, + help="Show recent items (default: 7 days)") + parser.add_argument("-i", "--case-insensitive", action="store_true", default=True, + help="Case insensitive search (default)") + + args = parser.parse_args() + + # Handle --recent + if args.recent is not None: + results = get_recent_items(args.recent) + print(f"\n=== Items from last {args.recent} days ({len(results)}) ===\n") + for r in results: + print(f"[{r['date'][:10]}] {r['source']}: {r['content'][:100]}...") + return 0 + + # Need query for search + if not args.query: + parser.print_help() + return 1 + + query = " ".join(args.query) + + # Determine what to search + search_all = not (args.memory or args.history or args.config) + + output = [] + + if search_all or args.memory: + results = search_memory(query) + output.append(format_results(results, "memory")) + + if search_all or args.history: + results = search_history(query) + output.append(format_results(results, "history")) + + if search_all or args.config: + results = search_config(query) + output.append(format_results(results, "config")) + + print(f"\n🔍 Search: '{query}'") + print("".join(output)) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/automation/systemd/claude-maintenance.service b/automation/systemd/claude-maintenance.service new file mode 100644 index 0000000..a2289d0 --- /dev/null +++ b/automation/systemd/claude-maintenance.service @@ -0,0 +1,12 @@ +[Unit] +Description=Claude Code Configuration Maintenance +After=network.target + +[Service] +Type=oneshot +ExecStart=/home/will/.claude/automation/daily-maintenance.sh +User=will +Environment=HOME=/home/will + +[Install] +WantedBy=multi-user.target diff --git a/automation/systemd/claude-maintenance.timer b/automation/systemd/claude-maintenance.timer new file mode 100644 index 0000000..2174c94 --- /dev/null +++ b/automation/systemd/claude-maintenance.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Daily Claude Code Maintenance Timer +Requires=claude-maintenance.service + +[Timer] +OnCalendar=*-*-* 06:00:00 +Persistent=true +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target diff --git a/automation/systemd/install-timers.sh b/automation/systemd/install-timers.sh new file mode 100755 index 0000000..dea3239 --- /dev/null +++ b/automation/systemd/install-timers.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Install systemd timers for Claude Code maintenance +# Run as root or with sudo + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SYSTEMD_DIR="/etc/systemd/system" + +echo "Installing Claude Code systemd timers..." + +# Copy service and timer files +sudo cp "${SCRIPT_DIR}/claude-maintenance.service" "${SYSTEMD_DIR}/" +sudo cp "${SCRIPT_DIR}/claude-maintenance.timer" "${SYSTEMD_DIR}/" + +# Reload systemd +sudo systemctl daemon-reload + +# Enable and start timer +sudo systemctl enable claude-maintenance.timer +sudo systemctl start claude-maintenance.timer + +echo "" +echo "Timer installed successfully!" +echo "" +echo "Commands:" +echo " systemctl status claude-maintenance.timer # Check timer status" +echo " systemctl list-timers # List all timers" +echo " journalctl -u claude-maintenance.service # View logs" +echo " sudo systemctl start claude-maintenance # Run maintenance now" +echo "" diff --git a/automation/test-scripts.sh b/automation/test-scripts.sh index da043c3..88c845f 100755 --- a/automation/test-scripts.sh +++ b/automation/test-scripts.sh @@ -57,6 +57,20 @@ else fail "usage_report.py syntax error" fi +# Test 6: search.py +if python3 -m py_compile "${AUTOMATION_DIR}/search.py" 2>/dev/null; then + pass "search.py syntax valid" +else + fail "search.py syntax error" +fi + +# Test 7: history-browser.py +if python3 -m py_compile "${AUTOMATION_DIR}/history-browser.py" 2>/dev/null; then + pass "history-browser.py syntax valid" +else + fail "history-browser.py syntax error" +fi + echo "" echo "=== Skill Scripts ===" @@ -100,6 +114,17 @@ else fail "k8s/quick-status.sh syntax error" fi +# Test automation bash scripts +for script in install.sh daily-maintenance.sh backup.sh restore.sh clean.sh; do + if [[ -f "${AUTOMATION_DIR}/${script}" ]]; then + if bash -n "${AUTOMATION_DIR}/${script}" 2>/dev/null; then + pass "${script} syntax valid" + else + fail "${script} syntax error" + fi + fi +done + echo "" echo "=== Summary ===" echo -e "Passed: ${GREEN}${PASS}${NC}" diff --git a/commands/README.md b/commands/README.md index 3344b03..c4f0993 100644 --- a/commands/README.md +++ b/commands/README.md @@ -14,6 +14,7 @@ Slash commands for quick actions. User-invoked (type `/command` to trigger). | `/summarize` | `/save-session` | Summarize and save session to memory | | `/remember` | `/save`, `/note` | Quick save to memory | | `/config` | `/settings`, `/prefs` | View/manage configuration | +| `/search` | `/find`, `/lookup` | Search memory, history, config | | `/maintain` | `/maintenance`, `/admin` | Configuration maintenance | | `/programmer` | | Code development tasks | | `/gcal` | `/calendar`, `/cal` | Google Calendar access | diff --git a/commands/search.md b/commands/search.md new file mode 100644 index 0000000..f27770b --- /dev/null +++ b/commands/search.md @@ -0,0 +1,43 @@ +--- +name: search +description: Search through memory, history, and configuration +aliases: [find, lookup] +invokes: skill:search +--- + +# Search Command + +Search across PA memory, session history, and configuration files. + +## Usage + +``` +/search # Search everywhere +/search --memory # Search only memory +/search --history # Search only session history +/search --config # Search configuration files +/search --recent [days] # Show recent items (default 7 days) +``` + +## Implementation + +Run the search script: + +```bash +python3 ~/.claude/automation/search.py [options] +``` + +## Search Locations + +| Location | Contents | +|----------|----------| +| Memory | preferences, decisions, projects, facts | +| History | Past session summaries and topics | +| Config | State files, component registry | + +## Output + +Returns matching items with: +- Source location +- Match context +- Relevance score (when applicable) diff --git a/state/component-registry.json b/state/component-registry.json index 8ae95b3..b0e62e7 100644 --- a/state/component-registry.json +++ b/state/component-registry.json @@ -159,6 +159,11 @@ "description": "View and manage configuration settings", "aliases": ["/settings", "/prefs"], "invokes": "command:config" + }, + "/search": { + "description": "Search memory, history, and configuration", + "aliases": ["/find", "/lookup"], + "invokes": "command:search" } }, "agents": { @@ -274,5 +279,24 @@ "description": "Calendar API with tiered delegation", "location": "~/.claude/mcp/delegation/gcal_delegate.py" } + }, + "automation": { + "scripts": { + "validate-setup": "~/.claude/automation/validate-setup.sh", + "quick-status": "~/.claude/automation/quick-status.sh", + "backup": "~/.claude/automation/backup.sh", + "restore": "~/.claude/automation/restore.sh", + "clean": "~/.claude/automation/clean.sh", + "install": "~/.claude/automation/install.sh", + "test": "~/.claude/automation/test-scripts.sh", + "memory-add": "~/.claude/automation/memory-add.py", + "memory-list": "~/.claude/automation/memory-list.py", + "search": "~/.claude/automation/search.py", + "history-browser": "~/.claude/automation/history-browser.py" + }, + "completions": { + "bash": "~/.claude/automation/completions.bash", + "zsh": "~/.claude/automation/completions.zsh" + } } }