Add search command, history browser, install script, and systemd timers
- /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>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
65
automation/daily-maintenance.sh
Executable file
65
automation/daily-maintenance.sh
Executable file
@@ -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 ==="
|
||||
184
automation/history-browser.py
Executable file
184
automation/history-browser.py
Executable file
@@ -0,0 +1,184 @@
|
||||
#!/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()
|
||||
225
automation/install.sh
Executable file
225
automation/install.sh
Executable file
@@ -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 <question> # Ask personal assistant"
|
||||
echo ""
|
||||
286
automation/search.py
Executable file
286
automation/search.py
Executable file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Search across PA memory, history, and configuration.
|
||||
Usage: python3 search.py [--memory|--history|--config|--recent [days]] <query>
|
||||
"""
|
||||
|
||||
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())
|
||||
12
automation/systemd/claude-maintenance.service
Normal file
12
automation/systemd/claude-maintenance.service
Normal file
@@ -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
|
||||
11
automation/systemd/claude-maintenance.timer
Normal file
11
automation/systemd/claude-maintenance.timer
Normal file
@@ -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
|
||||
31
automation/systemd/install-timers.sh
Executable file
31
automation/systemd/install-timers.sh
Executable file
@@ -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 ""
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user