diff --git a/journal/2026/01/2026-01-30.json b/journal/2026/01/2026-01-30.json new file mode 100644 index 0000000..7da5aa7 --- /dev/null +++ b/journal/2026/01/2026-01-30.json @@ -0,0 +1,10 @@ +{ + "date": "2026-01-30", + "entries": [ + { + "type": "note", + "time": "2026-01-30T23:40:08.015251", + "text": "Testing the journal - late night building session with D J" + } + ] +} \ No newline at end of file diff --git a/tools/backup.py b/tools/backup.py new file mode 100755 index 0000000..4977275 --- /dev/null +++ b/tools/backup.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +backup - Workspace backup and export + +Create snapshots of the workspace for safekeeping. +""" + +import os +import sys +import tarfile +import json +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +BACKUP_DIR = Path.home() / ".openclaw" / "backups" + +EXCLUDE_PATTERNS = { + '.git', + '__pycache__', + '*.pyc', + 'node_modules', + '.venv', + 'venv', +} + +def should_exclude(path: str) -> bool: + """Check if path should be excluded.""" + for pattern in EXCLUDE_PATTERNS: + if pattern.startswith('*'): + if path.endswith(pattern[1:]): + return True + elif pattern in path.split(os.sep): + return True + return False + +def create_backup(output_dir: Path = None): + """Create a backup of the workspace.""" + if output_dir is None: + output_dir = BACKUP_DIR + + output_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_name = f"workspace_{timestamp}" + backup_path = output_dir / f"{backup_name}.tar.gz" + + print(f"šŸ“¦ Creating backup...") + print(f" Source: {WORKSPACE}") + print(f" Output: {backup_path}") + + file_count = 0 + total_size = 0 + + with tarfile.open(backup_path, "w:gz") as tar: + for root, dirs, files in os.walk(WORKSPACE): + # Filter directories + dirs[:] = [d for d in dirs if not should_exclude(d)] + + for file in files: + if should_exclude(file): + continue + + file_path = Path(root) / file + arc_name = file_path.relative_to(WORKSPACE) + + if not should_exclude(str(arc_name)): + tar.add(file_path, arcname=arc_name) + file_count += 1 + total_size += file_path.stat().st_size + + backup_size = backup_path.stat().st_size / 1024 # KB + + print(f"\nāœ“ Backup complete!") + print(f" Files: {file_count}") + print(f" Original: {total_size / 1024:.1f} KB") + print(f" Compressed: {backup_size:.1f} KB") + print(f" Ratio: {backup_size / (total_size / 1024) * 100:.0f}%") + + return backup_path + +def list_backups(): + """List available backups.""" + if not BACKUP_DIR.exists(): + print("No backups found") + return + + backups = sorted(BACKUP_DIR.glob("workspace_*.tar.gz"), reverse=True) + + if not backups: + print("No backups found") + return + + print(f"\nšŸ“¦ Backups ({len(backups)} total)") + print("=" * 50) + + for backup in backups[:10]: + size = backup.stat().st_size / 1024 + mtime = datetime.fromtimestamp(backup.stat().st_mtime) + print(f" {backup.name}") + print(f" {mtime.strftime('%Y-%m-%d %H:%M')} | {size:.0f} KB") + +def restore_backup(backup_path: str, target_dir: Path = None): + """Restore from a backup.""" + backup_file = Path(backup_path) + + if not backup_file.exists(): + # Check in backup dir + backup_file = BACKUP_DIR / backup_path + if not backup_file.exists(): + print(f"Backup not found: {backup_path}") + return + + if target_dir is None: + target_dir = WORKSPACE.parent / "workspace_restored" + + target_dir.mkdir(parents=True, exist_ok=True) + + print(f"šŸ“¦ Restoring backup...") + print(f" From: {backup_file}") + print(f" To: {target_dir}") + + with tarfile.open(backup_file, "r:gz") as tar: + tar.extractall(target_dir) + + print(f"\nāœ“ Restore complete!") + print(f" Location: {target_dir}") + +def export_data(): + """Export key data files as JSON bundle.""" + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + export_path = BACKUP_DIR / f"data_export_{timestamp}.json" + + data = { + 'exported': datetime.now().isoformat(), + 'workspace': str(WORKSPACE), + 'files': {} + } + + # Collect JSON data files + data_files = [ + 'data/habits.json', + 'data/timetrack.json', + 'data/wisdom.json', + 'inbox/captures.json', + 'docs/clips.json', + ] + + for file_path in data_files: + full_path = WORKSPACE / file_path + if full_path.exists(): + try: + with open(full_path) as f: + data['files'][file_path] = json.load(f) + except: + pass + + # Also include memory files + memory_dir = WORKSPACE / "memory" + if memory_dir.exists(): + data['memory'] = {} + for f in sorted(memory_dir.glob("*.md"))[-30:]: # Last 30 days + data['memory'][f.name] = f.read_text() + + with open(export_path, 'w') as f: + json.dump(data, f, indent=2) + + print(f"āœ“ Data exported to: {export_path}") + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" backup create [dir] - Create workspace backup") + print(" backup list - List backups") + print(" backup restore - Restore from backup") + print(" backup export - Export data as JSON") + list_backups() + return + + cmd = sys.argv[1] + + if cmd == 'create': + output = Path(sys.argv[2]) if len(sys.argv) > 2 else None + create_backup(output) + elif cmd == 'list': + list_backups() + elif cmd == 'restore' and len(sys.argv) > 2: + restore_backup(sys.argv[2]) + elif cmd == 'export': + export_data() + else: + print("Unknown command. Run 'backup' for help.") + +if __name__ == "__main__": + main() diff --git a/tools/journal.py b/tools/journal.py new file mode 100755 index 0000000..735c5ff --- /dev/null +++ b/tools/journal.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +journal - Structured daily journaling + +Guided prompts for daily reflection and growth. +""" + +import json +import sys +from datetime import datetime, timedelta +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +JOURNAL_DIR = WORKSPACE / "journal" + +PROMPTS = { + 'morning': [ + "What am I grateful for today?", + "What would make today great?", + "What am I looking forward to?", + ], + 'evening': [ + "What went well today?", + "What could have gone better?", + "What did I learn?", + "What am I grateful for?", + ], + 'weekly': [ + "What were this week's wins?", + "What challenges did I face?", + "What do I want to focus on next week?", + "How am I progressing on my goals?", + ], +} + +def get_journal_path(date: datetime = None) -> Path: + """Get the journal file path for a date.""" + if date is None: + date = datetime.now() + year = date.strftime("%Y") + month = date.strftime("%m") + day = date.strftime("%d") + + path = JOURNAL_DIR / year / month + path.mkdir(parents=True, exist_ok=True) + return path / f"{date.strftime('%Y-%m-%d')}.json" + +def load_entry(date: datetime = None) -> dict: + """Load a journal entry.""" + path = get_journal_path(date) + if path.exists(): + with open(path) as f: + return json.load(f) + return { + 'date': (date or datetime.now()).strftime("%Y-%m-%d"), + 'entries': [], + } + +def save_entry(entry: dict, date: datetime = None): + """Save a journal entry.""" + path = get_journal_path(date) + with open(path, 'w') as f: + json.dump(entry, f, indent=2) + +def morning(): + """Morning journal routine.""" + print("\nšŸŒ… Morning Journal") + print("=" * 40) + print(f"Date: {datetime.now().strftime('%A, %B %d, %Y')}\n") + + entry = load_entry() + responses = [] + + for i, prompt in enumerate(PROMPTS['morning'], 1): + print(f"{i}. {prompt}") + response = input(" → ").strip() + if response: + responses.append({'prompt': prompt, 'response': response}) + print() + + if responses: + entry['entries'].append({ + 'type': 'morning', + 'time': datetime.now().isoformat(), + 'responses': responses, + }) + save_entry(entry) + print("āœ“ Morning journal saved") + +def evening(): + """Evening journal routine.""" + print("\nšŸŒ™ Evening Reflection") + print("=" * 40) + print(f"Date: {datetime.now().strftime('%A, %B %d, %Y')}\n") + + entry = load_entry() + responses = [] + + for i, prompt in enumerate(PROMPTS['evening'], 1): + print(f"{i}. {prompt}") + response = input(" → ").strip() + if response: + responses.append({'prompt': prompt, 'response': response}) + print() + + if responses: + entry['entries'].append({ + 'type': 'evening', + 'time': datetime.now().isoformat(), + 'responses': responses, + }) + save_entry(entry) + print("āœ“ Evening reflection saved") + +def quick(text: str): + """Quick journal entry.""" + entry = load_entry() + entry['entries'].append({ + 'type': 'note', + 'time': datetime.now().isoformat(), + 'text': text, + }) + save_entry(entry) + print(f"āœ“ Journal entry saved") + +def show(days_ago: int = 0): + """Show a journal entry.""" + date = datetime.now() - timedelta(days=days_ago) + entry = load_entry(date) + + print(f"\nšŸ“” Journal: {entry['date']}") + print("=" * 40) + + if not entry['entries']: + print("No entries for this day") + return + + for item in entry['entries']: + time = item['time'][11:16] + + if item['type'] == 'note': + print(f"\n[{time}] šŸ“ {item['text']}") + + elif item['type'] in ('morning', 'evening'): + emoji = 'šŸŒ…' if item['type'] == 'morning' else 'šŸŒ™' + print(f"\n[{time}] {emoji} {item['type'].title()}") + for resp in item.get('responses', []): + print(f" Q: {resp['prompt']}") + print(f" A: {resp['response']}") + + print() + +def stats(): + """Show journaling statistics.""" + total = 0 + streak = 0 + current_streak = True + + date = datetime.now() + for i in range(365): + check_date = date - timedelta(days=i) + entry = load_entry(check_date) + + if entry['entries']: + total += 1 + if current_streak: + streak += 1 + else: + current_streak = False + + print(f"\nšŸ“Š Journal Stats") + print("=" * 40) + print(f" Current streak: {streak} days") + print(f" Total entries: {total} (last 365 days)") + print() + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" journal morning - Morning prompts") + print(" journal evening - Evening reflection") + print(" journal - Quick entry") + print(" journal show [days] - Show entry (0=today)") + print(" journal stats - Statistics") + show(0) + return + + cmd = sys.argv[1] + + if cmd == 'morning': + morning() + elif cmd == 'evening': + evening() + elif cmd == 'show': + days = int(sys.argv[2]) if len(sys.argv) > 2 else 0 + show(days) + elif cmd == 'stats': + stats() + else: + # Quick entry + text = ' '.join(sys.argv[1:]) + quick(text) + +if __name__ == "__main__": + main() diff --git a/ws b/ws index dac8224..5d724e2 100755 --- a/ws +++ b/ws @@ -32,6 +32,8 @@ COMMANDS = { 'today': ('tools/today.py', 'Daily overview dashboard'), 'wisdom': ('tools/wisdom.py', 'Quotes and wisdom collection'), 'sys': ('tools/sysmon.py', 'System monitor'), + 'journal': ('tools/journal.py', 'Structured journaling'), + 'backup': ('tools/backup.py', 'Backup and export'), # Projects 'news': ('projects/news-feed/main.py', 'RSS news reader'),