diff --git a/automation/completions.bash b/automation/completions.bash index d5d5fef..67c68d0 100644 --- a/automation/completions.bash +++ b/automation/completions.bash @@ -69,6 +69,18 @@ _claude_agent_info() { COMPREPLY=($(compgen -W "--tree --list" -- "${cur}")) } +_claude_diff() { + local cur="${COMP_WORDS[COMP_CWORD]}" + + COMPREPLY=($(compgen -W "--backup --list --json" -- "${cur}")) +} + +_claude_template() { + local cur="${COMP_WORDS[COMP_CWORD]}" + + COMPREPLY=($(compgen -W "--list --create --use --delete" -- "${cur}")) +} + _claude_memory_add() { local cur="${COMP_WORDS[COMP_CWORD]}" local prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -108,6 +120,8 @@ complete -F _claude_upgrade upgrade.sh complete -F _claude_workflow workflow-info.py complete -F _claude_skill_info skill-info.py complete -F _claude_agent_info agent-info.py +complete -F _claude_diff config-diff.py +complete -F _claude_template session-template.py # Alias completions for convenience alias claude-validate='~/.claude/automation/validate-setup.sh' @@ -130,8 +144,10 @@ alias claude-upgrade='~/.claude/automation/upgrade.sh' alias claude-workflow='python3 ~/.claude/automation/workflow-info.py' alias claude-skill='python3 ~/.claude/automation/skill-info.py' alias claude-agent='python3 ~/.claude/automation/agent-info.py' +alias claude-diff='python3 ~/.claude/automation/config-diff.py' +alias claude-template='python3 ~/.claude/automation/session-template.py' echo "Claude Code completions loaded. Available aliases:" echo " claude-{validate,status,backup,restore,clean,memory-add,memory-list,search}" echo " claude-{history,install,test,maintenance,log,debug,export,mcp,upgrade}" -echo " claude-{workflow,skill,agent}" +echo " claude-{workflow,skill,agent,diff,template}" diff --git a/automation/completions.zsh b/automation/completions.zsh index 5f1c9f2..c285b2e 100644 --- a/automation/completions.zsh +++ b/automation/completions.zsh @@ -131,6 +131,24 @@ _claude_agent_info() { '*:name:' } +# Config diff completion +_claude_diff() { + _arguments \ + '--backup[Backup file]:file:_files' \ + '--list[List backups]' \ + '--json[JSON output]' +} + +# Template completion +_claude_template() { + _arguments \ + '--list[List templates]' \ + '--create[Create template]:name:' \ + '--use[Use template]:name:' \ + '--delete[Delete template]:name:' \ + '--non-interactive[Non-interactive mode]' +} + # Register completions compdef _memory_add memory-add.py compdef _memory_list memory-list.py @@ -145,6 +163,8 @@ compdef _claude_upgrade upgrade.sh compdef _claude_workflow workflow-info.py compdef _claude_skill_info skill-info.py compdef _claude_agent_info agent-info.py +compdef _claude_diff config-diff.py +compdef _claude_template session-template.py # Aliases alias claude-validate='~/.claude/automation/validate-setup.sh' @@ -167,8 +187,10 @@ alias claude-upgrade='~/.claude/automation/upgrade.sh' alias claude-workflow='python3 ~/.claude/automation/workflow-info.py' alias claude-skill='python3 ~/.claude/automation/skill-info.py' alias claude-agent='python3 ~/.claude/automation/agent-info.py' +alias claude-diff='python3 ~/.claude/automation/config-diff.py' +alias claude-template='python3 ~/.claude/automation/session-template.py' echo "Claude Code completions loaded (zsh)" echo " Aliases: claude-{validate,status,backup,restore,clean,memory-add,memory-list,search}" echo " claude-{history,install,test,maintenance,log,debug,export,mcp,upgrade}" -echo " claude-{workflow,skill,agent}" +echo " claude-{workflow,skill,agent,diff,template}" diff --git a/automation/config-diff.py b/automation/config-diff.py new file mode 100755 index 0000000..490293d --- /dev/null +++ b/automation/config-diff.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +""" +Compare current configuration with backup or default. +Usage: python3 config-diff.py [--backup FILE] [--default] [--json] +""" + +import argparse +import json +import os +import sys +import tarfile +import tempfile +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +CLAUDE_DIR = Path.home() / ".claude" +BACKUP_DIR = CLAUDE_DIR / "backups" + + +def load_json_safe(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 get_config_files() -> List[Path]: + """Get list of configuration files to compare.""" + patterns = [ + "CLAUDE.md", + "VERSION", + ".claude-plugin/plugin.json", + "hooks/hooks.json", + "state/*.json", + "state/**/*.json", + ] + + files = [] + for pattern in patterns: + files.extend(CLAUDE_DIR.glob(pattern)) + + return sorted(files) + + +def extract_backup(backup_path: Path) -> Path: + """Extract backup to temporary directory.""" + temp_dir = Path(tempfile.mkdtemp()) + + with tarfile.open(backup_path, "r:gz") as tar: + tar.extractall(temp_dir) + + return temp_dir + + +def compare_files(current: Path, other: Path) -> Dict: + """Compare two files and return differences.""" + result = { + "current_exists": current.exists(), + "other_exists": other.exists(), + "same": False, + "diff": None + } + + if not current.exists() and not other.exists(): + result["same"] = True + return result + + if not current.exists() or not other.exists(): + return result + + # Compare content + try: + current_content = current.read_text() + other_content = other.read_text() + + if current_content == other_content: + result["same"] = True + return result + + # For JSON files, do structural comparison + if current.suffix == ".json": + try: + current_json = json.loads(current_content) + other_json = json.loads(other_content) + + # Find differences + result["diff"] = { + "type": "json", + "added": [], + "removed": [], + "changed": [] + } + + # Simple key comparison for top-level + current_keys = set(current_json.keys()) if isinstance(current_json, dict) else set() + other_keys = set(other_json.keys()) if isinstance(other_json, dict) else set() + + result["diff"]["added"] = list(current_keys - other_keys) + result["diff"]["removed"] = list(other_keys - current_keys) + + # Check for changed values + for key in current_keys & other_keys: + if current_json.get(key) != other_json.get(key): + result["diff"]["changed"].append(key) + + return result + + except json.JSONDecodeError: + pass + + # Text comparison + current_lines = current_content.split("\n") + other_lines = other_content.split("\n") + + result["diff"] = { + "type": "text", + "current_lines": len(current_lines), + "other_lines": len(other_lines), + "line_diff": len(current_lines) - len(other_lines) + } + + except Exception as e: + result["error"] = str(e) + + return result + + +def get_latest_backup() -> Optional[Path]: + """Get the most recent backup file.""" + if not BACKUP_DIR.exists(): + return None + + backups = list(BACKUP_DIR.glob("*.tar.gz")) + if not backups: + return None + + return max(backups, key=lambda p: p.stat().st_mtime) + + +def compare_with_backup(backup_path: Path) -> Dict[str, Dict]: + """Compare current config with a backup.""" + temp_dir = extract_backup(backup_path) + + try: + results = {} + config_files = get_config_files() + + for current_file in config_files: + rel_path = current_file.relative_to(CLAUDE_DIR) + backup_file = temp_dir / rel_path + + results[str(rel_path)] = compare_files(current_file, backup_file) + + # Check for files in backup that don't exist in current + for backup_file in temp_dir.rglob("*.json"): + rel_path = backup_file.relative_to(temp_dir) + current_file = CLAUDE_DIR / rel_path + + if str(rel_path) not in results: + results[str(rel_path)] = compare_files(current_file, backup_file) + + return results + + finally: + # Cleanup temp directory + import shutil + shutil.rmtree(temp_dir, ignore_errors=True) + + +def format_results(results: Dict[str, Dict], json_output: bool = False) -> str: + """Format comparison results.""" + if json_output: + return json.dumps(results, indent=2) + + lines = ["\nšŸ“Š Configuration Diff\n"] + + # Group by status + same = [] + added = [] + removed = [] + changed = [] + + for path, info in results.items(): + if info.get("same"): + same.append(path) + elif info.get("current_exists") and not info.get("other_exists"): + added.append(path) + elif not info.get("current_exists") and info.get("other_exists"): + removed.append(path) + else: + changed.append((path, info)) + + # Summary + lines.append(f"Unchanged: {len(same)}") + lines.append(f"Added: {len(added)}") + lines.append(f"Removed: {len(removed)}") + lines.append(f"Changed: {len(changed)}") + lines.append("") + + # Details + if added: + lines.append("=== Added Files ===") + for path in added: + lines.append(f" + {path}") + lines.append("") + + if removed: + lines.append("=== Removed Files ===") + for path in removed: + lines.append(f" - {path}") + lines.append("") + + if changed: + lines.append("=== Changed Files ===") + for path, info in changed: + lines.append(f" ~ {path}") + diff = info.get("diff", {}) + if diff: + if diff.get("type") == "json": + if diff.get("added"): + lines.append(f" Added keys: {', '.join(diff['added'][:5])}") + if diff.get("removed"): + lines.append(f" Removed keys: {', '.join(diff['removed'][:5])}") + if diff.get("changed"): + lines.append(f" Changed keys: {', '.join(diff['changed'][:5])}") + elif diff.get("type") == "text": + lines.append(f" Lines: {diff['other_lines']} -> {diff['current_lines']}") + lines.append("") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Compare configuration with backup") + parser.add_argument("--backup", "-b", type=str, + help="Backup file to compare with (default: latest)") + parser.add_argument("--list", "-l", action="store_true", + help="List available backups") + parser.add_argument("--json", "-j", action="store_true", + help="Output as JSON") + + args = parser.parse_args() + + if args.list: + if not BACKUP_DIR.exists(): + print("No backups directory found.") + return 0 + + backups = sorted(BACKUP_DIR.glob("*.tar.gz"), + key=lambda p: p.stat().st_mtime, reverse=True) + + if not backups: + print("No backups found.") + return 0 + + print("\nšŸ“¦ Available Backups\n") + for b in backups[:10]: + mtime = datetime.fromtimestamp(b.stat().st_mtime) + size = b.stat().st_size + if size > 1024 * 1024: + size_str = f"{size / 1024 / 1024:.1f}M" + else: + size_str = f"{size / 1024:.1f}K" + print(f" {b.name:<40} {size_str:<8} {mtime:%Y-%m-%d %H:%M}") + + return 0 + + # Get backup to compare + if args.backup: + backup_path = Path(args.backup) + if not backup_path.exists(): + backup_path = BACKUP_DIR / args.backup + if not backup_path.exists(): + print(f"Backup not found: {args.backup}") + return 1 + else: + backup_path = get_latest_backup() + if not backup_path: + print("No backups found. Create one with: claude-backup") + return 1 + + print(f"Comparing with: {backup_path.name}") + + results = compare_with_backup(backup_path) + print(format_results(results, args.json)) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/automation/session-template.py b/automation/session-template.py new file mode 100755 index 0000000..2c2d9f9 --- /dev/null +++ b/automation/session-template.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +""" +Manage session templates for common workflows. +Usage: python3 session-template.py [--list|--create NAME|--use NAME|--delete NAME] +""" + +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" +TEMPLATES_DIR = CLAUDE_DIR / "state" / "personal-assistant" / "templates" + + +def ensure_templates_dir(): + """Ensure templates directory exists.""" + TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) + + +def load_template(name: str) -> Optional[Dict]: + """Load a template by name.""" + template_file = TEMPLATES_DIR / f"{name}.json" + try: + with open(template_file) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return None + + +def save_template(name: str, template: Dict): + """Save a template.""" + ensure_templates_dir() + template_file = TEMPLATES_DIR / f"{name}.json" + with open(template_file, "w") as f: + json.dump(template, f, indent=2) + + +def list_templates(): + """List available templates.""" + ensure_templates_dir() + + templates = list(TEMPLATES_DIR.glob("*.json")) + + if not templates: + print("\nNo templates found. Create one with --create ") + print("\nBuilt-in templates you can create:") + print(" - daily-standup: Morning status check and planning") + print(" - code-review: Review code changes") + print(" - troubleshoot: Debug an issue") + print(" - deploy: Deployment workflow") + return + + print(f"\nšŸ“‹ Session Templates ({len(templates)})\n") + + for t in sorted(templates): + template = load_template(t.stem) + if template: + desc = template.get("description", "No description") + category = template.get("category", "general") + print(f" {t.stem}") + print(f" Category: {category}") + print(f" {desc}") + print("") + + +def create_template(name: str, interactive: bool = True): + """Create a new template.""" + ensure_templates_dir() + + # Check for built-in templates + builtin_templates = { + "daily-standup": { + "name": "daily-standup", + "description": "Morning status check and planning", + "category": "routine", + "context_level": "moderate", + "initial_commands": ["/status"], + "checklist": [ + "Check system health", + "Review pending items", + "Check calendar", + "Plan priorities" + ], + "prompt_template": "Good morning! Let's start with a quick status check and plan the day." + }, + "code-review": { + "name": "code-review", + "description": "Review code changes in a project", + "category": "development", + "context_level": "comprehensive", + "initial_commands": [], + "checklist": [ + "Understand the change scope", + "Check for issues", + "Verify tests", + "Suggest improvements" + ], + "prompt_template": "Please review the recent changes in {project}. Focus on {focus_areas}." + }, + "troubleshoot": { + "name": "troubleshoot", + "description": "Debug an issue systematically", + "category": "debugging", + "context_level": "comprehensive", + "initial_commands": ["/debug"], + "checklist": [ + "Gather symptoms", + "Check logs", + "Identify root cause", + "Implement fix", + "Verify resolution" + ], + "prompt_template": "I'm experiencing {issue}. Let's debug this systematically." + }, + "deploy": { + "name": "deploy", + "description": "Deploy application to environment", + "category": "operations", + "context_level": "moderate", + "initial_commands": ["/k8s:cluster-status"], + "checklist": [ + "Verify cluster health", + "Check application readiness", + "Perform deployment", + "Verify deployment", + "Monitor for issues" + ], + "prompt_template": "Deploy {app} to {environment}." + } + } + + if name in builtin_templates: + template = builtin_templates[name] + save_template(name, template) + print(f"āœ“ Created template from built-in: {name}") + print(f" {template['description']}") + return + + if interactive: + print(f"\nCreating new template: {name}\n") + + template = { + "name": name, + "created": datetime.now().isoformat(), + } + + # Get details interactively + template["description"] = input("Description: ").strip() or "No description" + template["category"] = input("Category [general]: ").strip() or "general" + template["context_level"] = input("Context level [moderate]: ").strip() or "moderate" + + print("Initial commands (comma-separated, e.g., /status,/debug): ") + commands = input("> ").strip() + template["initial_commands"] = [c.strip() for c in commands.split(",") if c.strip()] + + print("Checklist items (one per line, empty line to finish):") + checklist = [] + while True: + item = input(" - ").strip() + if not item: + break + checklist.append(item) + template["checklist"] = checklist + + template["prompt_template"] = input("Prompt template: ").strip() + + save_template(name, template) + print(f"\nāœ“ Template '{name}' created successfully") + else: + # Create minimal template + template = { + "name": name, + "description": "Custom template", + "category": "custom", + "context_level": "moderate", + "initial_commands": [], + "checklist": [], + "prompt_template": "", + "created": datetime.now().isoformat() + } + save_template(name, template) + print(f"āœ“ Created empty template: {name}") + print(f" Edit: {TEMPLATES_DIR / f'{name}.json'}") + + +def use_template(name: str): + """Display template for use.""" + template = load_template(name) + + if not template: + print(f"Template '{name}' not found.") + list_templates() + return + + print(f"\nšŸ“‹ Template: {name}\n") + print(f"Description: {template.get('description', 'No description')}") + print(f"Category: {template.get('category', 'general')}") + print(f"Context: {template.get('context_level', 'moderate')}") + + commands = template.get("initial_commands", []) + if commands: + print(f"\nInitial commands:") + for cmd in commands: + print(f" {cmd}") + + checklist = template.get("checklist", []) + if checklist: + print(f"\nChecklist:") + for i, item in enumerate(checklist, 1): + print(f" [ ] {item}") + + prompt = template.get("prompt_template", "") + if prompt: + print(f"\nPrompt template:") + print(f" {prompt}") + + print("") + + +def delete_template(name: str): + """Delete a template.""" + template_file = TEMPLATES_DIR / f"{name}.json" + + if not template_file.exists(): + print(f"Template '{name}' not found.") + return + + template_file.unlink() + print(f"āœ“ Deleted template: {name}") + + +def main(): + parser = argparse.ArgumentParser(description="Manage session templates") + parser.add_argument("--list", "-l", action="store_true", help="List templates") + parser.add_argument("--create", "-c", type=str, help="Create a template") + parser.add_argument("--use", "-u", type=str, help="Use a template") + parser.add_argument("--delete", "-d", type=str, help="Delete a template") + parser.add_argument("--non-interactive", "-n", action="store_true", + help="Non-interactive mode for --create") + + args = parser.parse_args() + + if args.create: + create_template(args.create, interactive=not args.non_interactive) + elif args.use: + use_template(args.use) + elif args.delete: + delete_template(args.delete) + else: + list_templates() + + +if __name__ == "__main__": + main() diff --git a/automation/test-scripts.sh b/automation/test-scripts.sh index 885a16f..002166a 100755 --- a/automation/test-scripts.sh +++ b/automation/test-scripts.sh @@ -106,6 +106,20 @@ else fail "agent-info.py syntax error" fi +# Test 13: config-diff.py +if python3 -m py_compile "${AUTOMATION_DIR}/config-diff.py" 2>/dev/null; then + pass "config-diff.py syntax valid" +else + fail "config-diff.py syntax error" +fi + +# Test 14: session-template.py +if python3 -m py_compile "${AUTOMATION_DIR}/session-template.py" 2>/dev/null; then + pass "session-template.py syntax valid" +else + fail "session-template.py syntax error" +fi + echo "" echo "=== Skill Scripts ===" diff --git a/commands/README.md b/commands/README.md index a4f1f00..96ceeb4 100644 --- a/commands/README.md +++ b/commands/README.md @@ -22,6 +22,8 @@ Slash commands for quick actions. User-invoked (type `/command` to trigger). | `/workflow` | `/workflows`, `/wf` | List and describe workflows | | `/skill-info` | `/skill`, `/skills-info` | Show skill information | | `/agent-info` | `/agent`, `/agents` | Show agent information | +| `/diff` | `/config-diff`, `/compare` | Compare config with backup | +| `/template` | `/templates`, `/session-template` | Manage session templates | | `/maintain` | `/maintenance`, `/admin` | Configuration maintenance | | `/programmer` | | Code development tasks | | `/gcal` | `/calendar`, `/cal` | Google Calendar access | diff --git a/commands/diff.md b/commands/diff.md new file mode 100644 index 0000000..d7e917d --- /dev/null +++ b/commands/diff.md @@ -0,0 +1,34 @@ +--- +name: diff +description: Compare configuration with backup +aliases: [config-diff, compare] +invokes: command:diff +--- + +# Diff Command + +Compare current configuration with a backup to see what changed. + +## Usage + +``` +/diff # Compare with latest backup +/diff --backup # Compare with specific backup +/diff --list # List available backups +/diff --json # Output as JSON +``` + +## Implementation + +```bash +python3 ~/.claude/automation/config-diff.py [options] +``` + +## Output + +Shows changes grouped by type: +- **Added** - New files in current config +- **Removed** - Files that existed in backup but not now +- **Changed** - Modified files with details + +For JSON files, shows which keys were added/removed/changed. diff --git a/commands/template.md b/commands/template.md new file mode 100644 index 0000000..3dc1762 --- /dev/null +++ b/commands/template.md @@ -0,0 +1,44 @@ +--- +name: template +description: Manage session templates for common workflows +aliases: [templates, session-template] +invokes: command:template +--- + +# Template Command + +Create and use session templates for repeatable workflows. + +## Usage + +``` +/template # List all templates +/template --use # Display template for use +/template --create # Create new template +/template --delete # Delete a template +``` + +## Implementation + +```bash +python3 ~/.claude/automation/session-template.py [options] +``` + +## Built-in Templates + +| Name | Category | Description | +|------|----------|-------------| +| `daily-standup` | routine | Morning status check and planning | +| `code-review` | development | Review code changes in a project | +| `troubleshoot` | debugging | Debug an issue systematically | +| `deploy` | operations | Deploy application to environment | + +## Template Contents + +Each template includes: +- **Description** - What the template is for +- **Category** - Grouping for organization +- **Context level** - How much context to gather +- **Initial commands** - Commands to run at start +- **Checklist** - Steps to follow +- **Prompt template** - Suggested starting prompt diff --git a/state/component-registry.json b/state/component-registry.json index 3b7be9c..ae08ecf 100644 --- a/state/component-registry.json +++ b/state/component-registry.json @@ -199,6 +199,16 @@ "description": "Show agent information", "aliases": ["/agent", "/agents"], "invokes": "command:agent-info" + }, + "/diff": { + "description": "Compare config with backup", + "aliases": ["/config-diff", "/compare"], + "invokes": "command:diff" + }, + "/template": { + "description": "Manage session templates", + "aliases": ["/templates", "/session-template"], + "invokes": "command:template" } }, "agents": { @@ -336,7 +346,9 @@ "upgrade": "~/.claude/automation/upgrade.sh", "workflow-info": "~/.claude/automation/workflow-info.py", "skill-info": "~/.claude/automation/skill-info.py", - "agent-info": "~/.claude/automation/agent-info.py" + "agent-info": "~/.claude/automation/agent-info.py", + "config-diff": "~/.claude/automation/config-diff.py", + "session-template": "~/.claude/automation/session-template.py" }, "completions": { "bash": "~/.claude/automation/completions.bash", diff --git a/state/personal-assistant/templates/daily-standup.json b/state/personal-assistant/templates/daily-standup.json new file mode 100644 index 0000000..3bec4b8 --- /dev/null +++ b/state/personal-assistant/templates/daily-standup.json @@ -0,0 +1,16 @@ +{ + "name": "daily-standup", + "description": "Morning status check and planning", + "category": "routine", + "context_level": "moderate", + "initial_commands": [ + "/status" + ], + "checklist": [ + "Check system health", + "Review pending items", + "Check calendar", + "Plan priorities" + ], + "prompt_template": "Good morning! Let's start with a quick status check and plan the day." +} \ No newline at end of file