diff --git a/data/wisdom.json b/data/wisdom.json new file mode 100644 index 0000000..d435110 --- /dev/null +++ b/data/wisdom.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "text": "The future is already here - it's just not evenly distributed.", + "category": "quote", + "source": "William Gibson", + "tags": [], + "added": "2026-01-30T23:38:38.352315", + "views": 0 + }, + { + "id": 2, + "text": "Simple is better than complex. Complex is better than complicated.", + "category": "principle", + "source": null, + "tags": [], + "added": "2026-01-30T23:38:38.378936", + "views": 0 + }, + { + "id": 3, + "text": "You can't optimize what you don't measure.", + "category": "lesson", + "source": null, + "tags": [], + "added": "2026-01-30T23:38:38.405313", + "views": 0 + }, + { + "id": 4, + "text": "The best time to plant a tree was 20 years ago. The second best time is now.", + "category": "reminder", + "source": null, + "tags": [], + "added": "2026-01-30T23:38:38.431626", + "views": 0 + } +] \ No newline at end of file diff --git a/tools/sysmon.py b/tools/sysmon.py new file mode 100755 index 0000000..e1b83fb --- /dev/null +++ b/tools/sysmon.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +sysmon - System monitor + +Quick system status and resource tracking. +""" + +import os +import json +import subprocess +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +HISTORY_FILE = WORKSPACE / "data" / "sysmon_history.json" + +def get_cpu_usage(): + """Get CPU usage percentage.""" + try: + # Read /proc/stat for CPU usage + with open('/proc/stat') as f: + line = f.readline() + parts = line.split() + idle = int(parts[4]) + total = sum(int(p) for p in parts[1:]) + + # Read again after a moment + import time + time.sleep(0.1) + + with open('/proc/stat') as f: + line = f.readline() + parts = line.split() + idle2 = int(parts[4]) + total2 = sum(int(p) for p in parts[1:]) + + idle_delta = idle2 - idle + total_delta = total2 - total + + if total_delta == 0: + return 0 + + usage = 100 * (1 - idle_delta / total_delta) + return round(usage, 1) + except: + return None + +def get_memory(): + """Get memory usage.""" + try: + with open('/proc/meminfo') as f: + lines = f.readlines() + + mem = {} + for line in lines: + parts = line.split() + key = parts[0].rstrip(':') + value = int(parts[1]) # in KB + mem[key] = value + + total = mem.get('MemTotal', 0) / 1024 / 1024 # GB + available = mem.get('MemAvailable', 0) / 1024 / 1024 # GB + used = total - available + pct = (used / total * 100) if total > 0 else 0 + + return { + 'total_gb': round(total, 1), + 'used_gb': round(used, 1), + 'available_gb': round(available, 1), + 'percent': round(pct, 1), + } + except: + return None + +def get_disk(): + """Get disk usage.""" + try: + stat = os.statvfs('/') + total = stat.f_blocks * stat.f_frsize / 1024 / 1024 / 1024 # GB + free = stat.f_bavail * stat.f_frsize / 1024 / 1024 / 1024 # GB + used = total - free + pct = (used / total * 100) if total > 0 else 0 + + return { + 'total_gb': round(total, 1), + 'used_gb': round(used, 1), + 'free_gb': round(free, 1), + 'percent': round(pct, 1), + } + except: + return None + +def get_load(): + """Get system load average.""" + try: + with open('/proc/loadavg') as f: + parts = f.read().split() + return { + 'load1': float(parts[0]), + 'load5': float(parts[1]), + 'load15': float(parts[2]), + } + except: + return None + +def get_uptime(): + """Get system uptime.""" + try: + with open('/proc/uptime') as f: + uptime_seconds = float(f.read().split()[0]) + + days = int(uptime_seconds // 86400) + hours = int((uptime_seconds % 86400) // 3600) + mins = int((uptime_seconds % 3600) // 60) + + if days > 0: + return f"{days}d {hours}h {mins}m" + elif hours > 0: + return f"{hours}h {mins}m" + else: + return f"{mins}m" + except: + return None + +def get_processes(): + """Get top processes by memory/CPU.""" + try: + result = subprocess.run( + ['ps', 'aux', '--sort=-rss'], + capture_output=True, text=True + ) + lines = result.stdout.strip().split('\n')[1:6] # Skip header, top 5 + + processes = [] + for line in lines: + parts = line.split(None, 10) + if len(parts) >= 11: + processes.append({ + 'user': parts[0], + 'cpu': float(parts[2]), + 'mem': float(parts[3]), + 'cmd': parts[10][:30], + }) + return processes + except: + return [] + +def bar(pct: float, width: int = 20) -> str: + """Create a progress bar.""" + filled = int(pct / 100 * width) + return "█" * filled + "░" * (width - filled) + +def show_status(): + """Show current system status.""" + print() + print("╔" + "═" * 40 + "╗") + print(f"║ 🖥️ System Monitor {' ' * 20}║") + print("╚" + "═" * 40 + "╝") + + # Uptime + uptime = get_uptime() + if uptime: + print(f"\n⏰ Uptime: {uptime}") + + # CPU + cpu = get_cpu_usage() + if cpu is not None: + print(f"\n🔲 CPU: {bar(cpu)} {cpu}%") + + # Memory + mem = get_memory() + if mem: + print(f"🔲 Memory: {bar(mem['percent'])} {mem['percent']}%") + print(f" {mem['used_gb']:.1f}GB / {mem['total_gb']:.1f}GB") + + # Disk + disk = get_disk() + if disk: + print(f"🔲 Disk: {bar(disk['percent'])} {disk['percent']}%") + print(f" {disk['used_gb']:.0f}GB / {disk['total_gb']:.0f}GB") + + # Load + load = get_load() + if load: + print(f"\n📊 Load: {load['load1']:.2f} / {load['load5']:.2f} / {load['load15']:.2f}") + + # Top processes + procs = get_processes() + if procs: + print(f"\n🔝 Top Processes:") + for p in procs[:3]: + print(f" {p['mem']:.1f}% {p['cmd']}") + + print() + +def record(): + """Record current stats to history.""" + HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True) + + if HISTORY_FILE.exists(): + with open(HISTORY_FILE) as f: + history = json.load(f) + else: + history = [] + + entry = { + 'timestamp': datetime.now().isoformat(), + 'cpu': get_cpu_usage(), + 'memory': get_memory(), + 'disk': get_disk(), + 'load': get_load(), + } + + history.append(entry) + + # Keep last 1000 entries + history = history[-1000:] + + with open(HISTORY_FILE, 'w') as f: + json.dump(history, f) + + print(f"✓ Recorded at {entry['timestamp'][:19]}") + +def main(): + import sys + + if len(sys.argv) > 1: + cmd = sys.argv[1] + if cmd == 'record': + record() + else: + print("Usage: sysmon [record]") + else: + show_status() + +if __name__ == "__main__": + main() diff --git a/tools/today.py b/tools/today.py new file mode 100755 index 0000000..43f3fb4 --- /dev/null +++ b/tools/today.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +today - Comprehensive daily overview + +Combines tasks, habits, time tracking, notes, and captures into one view. +""" + +import json +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") + +def load_json(path: Path) -> dict | list: + """Load JSON file safely.""" + if path.exists(): + try: + with open(path) as f: + return json.load(f) + except: + pass + return {} if str(path).endswith('json') else [] + +def get_time_tracking(): + """Get today's time tracking.""" + data = load_json(WORKSPACE / "data" / "timetrack.json") + today = datetime.now().strftime("%Y-%m-%d") + + entries = [e for e in data.get('entries', []) if e['start'].startswith(today)] + current = data.get('current') + + total_mins = sum(e['duration_min'] for e in entries) + + return { + 'current': current, + 'entries': entries, + 'total_mins': total_mins, + } + +def get_habits(): + """Get today's habit status.""" + data = load_json(WORKSPACE / "data" / "habits.json") + today = datetime.now().strftime("%Y-%m-%d") + + habits = data.get('habits', {}) + done = data.get('log', {}).get(today, []) + + active = [(name, name in done) for name, info in habits.items() + if info.get('active', True)] + + return { + 'habits': active, + 'done': len([h for h in active if h[1]]), + 'total': len(active), + } + +def get_captures(): + """Get unprocessed captures.""" + data = load_json(WORKSPACE / "inbox" / "captures.json") + if isinstance(data, list): + return [c for c in data if not c.get('processed')] + return [] + +def get_tasks(): + """Get tasks from TASKS.md.""" + tasks_file = WORKSPACE / "TASKS.md" + if not tasks_file.exists(): + return {'in_progress': [], 'waiting': [], 'inbox': []} + + result = {'in_progress': [], 'waiting': [], 'inbox': []} + section = None + + with open(tasks_file) as f: + for line in f: + if "## Inbox" in line: section = 'inbox' + elif "## In Progress" in line: section = 'in_progress' + elif "## Waiting" in line: section = 'waiting' + elif "## Done" in line: section = None + elif section and line.strip().startswith("- [ ]"): + task = line.strip()[6:].strip() + result[section].append(task) + + return result + +def get_notes(): + """Get today's notes.""" + today = datetime.now().strftime("%Y-%m-%d") + notes_file = WORKSPACE / "memory" / f"{today}.md" + + if not notes_file.exists(): + return None + + content = notes_file.read_text() + lines = [l for l in content.split('\n') if l.strip() and not l.startswith('#')] + return lines + +def format_duration(mins: float) -> str: + """Format minutes as human readable.""" + if mins < 60: + return f"{mins:.0f}m" + hours = int(mins // 60) + m = int(mins % 60) + return f"{hours}h {m}m" + +def main(): + now = datetime.now() + + print() + print("╔" + "═" * 50 + "╗") + print(f"║ 🖤 Today: {now.strftime('%A, %B %d, %Y'):^38} ║") + print(f"║ {now.strftime('%H:%M'):^48} ║") + print("╚" + "═" * 50 + "╝") + + # Time tracking + time_data = get_time_tracking() + print(f"\n⏱️ Time Tracked: {format_duration(time_data['total_mins'])}") + if time_data['current']: + c = time_data['current'] + print(f" └─ Currently: {c['task'][:35]}") + + # Habits + habits = get_habits() + if habits['total'] > 0: + print(f"\n✅ Habits: {habits['done']}/{habits['total']}") + for name, done in habits['habits']: + status = "✓" if done else "○" + print(f" {status} {name}") + + # Tasks + tasks = get_tasks() + in_progress = len(tasks['in_progress']) + waiting = len(tasks['waiting']) + inbox = len(tasks['inbox']) + + if in_progress + waiting + inbox > 0: + print(f"\n📋 Tasks: {in_progress} active, {waiting} waiting, {inbox} inbox") + for task in tasks['in_progress'][:3]: + print(f" → {task[:40]}") + + # Captures + captures = get_captures() + if captures: + print(f"\n📥 Inbox: {len(captures)} items") + for cap in captures[:3]: + emoji = {'idea': '💡', 'task': '✅', 'link': '🔗'}.get(cap.get('type'), '📝') + print(f" {emoji} {cap['content'][:35]}...") + + # Notes + notes = get_notes() + if notes: + print(f"\n📝 Notes: {len(notes)} entries today") + + # Quick actions + print("\n" + "─" * 52) + print("Quick: ws habits check | ws track start ") + print(" ws cap | ws note ") + print() + +if __name__ == "__main__": + main() diff --git a/tools/wisdom.py b/tools/wisdom.py new file mode 100755 index 0000000..1e5d402 --- /dev/null +++ b/tools/wisdom.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +""" +wisdom - Collect and recall quotes, ideas, and wisdom + +Store things worth remembering and get random inspiration. +""" + +import json +import random +import sys +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +WISDOM_FILE = WORKSPACE / "data" / "wisdom.json" + +CATEGORIES = { + 'quote': '💬', + 'idea': '💡', + 'lesson': '📚', + 'principle': '⚖️', + 'reminder': '🔔', + 'goal': '🎯', +} + +def load_wisdom() -> list: + """Load wisdom entries.""" + WISDOM_FILE.parent.mkdir(parents=True, exist_ok=True) + if WISDOM_FILE.exists(): + with open(WISDOM_FILE) as f: + return json.load(f) + return [] + +def save_wisdom(entries: list): + """Save wisdom entries.""" + with open(WISDOM_FILE, 'w') as f: + json.dump(entries, f, indent=2) + +def add(text: str, category: str = 'quote', source: str = None, tags: list = None): + """Add a new wisdom entry.""" + entries = load_wisdom() + + entry = { + 'id': len(entries) + 1, + 'text': text, + 'category': category if category in CATEGORIES else 'quote', + 'source': source, + 'tags': tags or [], + 'added': datetime.now().isoformat(), + 'views': 0, + } + + entries.append(entry) + save_wisdom(entries) + + emoji = CATEGORIES.get(category, '💬') + print(f"{emoji} Added #{entry['id']}: {text[:50]}...") + +def random_wisdom(category: str = None): + """Get a random piece of wisdom.""" + entries = load_wisdom() + + if not entries: + print("No wisdom yet. Add some with: wisdom add ") + return + + if category: + entries = [e for e in entries if e['category'] == category] + if not entries: + print(f"No entries in category: {category}") + return + + entry = random.choice(entries) + entry['views'] += 1 + save_wisdom(load_wisdom()) # Update view count + + show_entry(entry) + +def show_entry(entry: dict): + """Display a wisdom entry nicely.""" + emoji = CATEGORIES.get(entry['category'], '💬') + + print() + print("─" * 50) + print(f"{emoji} {entry['text']}") + if entry.get('source'): + print(f" — {entry['source']}") + print("─" * 50) + + tags = ' '.join(f"#{t}" for t in entry.get('tags', [])) + if tags: + print(f" {tags}") + print() + +def list_wisdom(category: str = None, limit: int = 10): + """List wisdom entries.""" + entries = load_wisdom() + + if category: + entries = [e for e in entries if e['category'] == category] + + if not entries: + print("No wisdom found") + return + + print(f"\n📚 Wisdom Collection ({len(entries)} entries)\n") + + for entry in entries[-limit:]: + emoji = CATEGORIES.get(entry['category'], '💬') + text = entry['text'][:50] + if len(entry['text']) > 50: + text += "..." + print(f" #{entry['id']:3} {emoji} {text}") + + print() + +def search_wisdom(query: str): + """Search wisdom entries.""" + entries = load_wisdom() + query_lower = query.lower() + + matches = [] + for entry in entries: + searchable = f"{entry['text']} {entry.get('source', '')} {' '.join(entry.get('tags', []))}".lower() + if query_lower in searchable: + matches.append(entry) + + if not matches: + print(f"No matches for: {query}") + return + + print(f"\n🔍 Found {len(matches)} matches:\n") + for entry in matches: + emoji = CATEGORIES.get(entry['category'], '💬') + print(f" #{entry['id']} {emoji} {entry['text'][:50]}...") + print() + +def daily(): + """Show the daily wisdom (same one each day, changes daily).""" + entries = load_wisdom() + + if not entries: + print("No wisdom yet. Add some with: wisdom add ") + return + + # Use the date as seed for consistent daily selection + day_seed = int(datetime.now().strftime("%Y%m%d")) + random.seed(day_seed) + entry = random.choice(entries) + random.seed() # Reset seed + + print("\n🌅 Today's Wisdom:") + show_entry(entry) + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" wisdom - Random wisdom") + print(" wisdom daily - Today's wisdom") + print(" wisdom add - Add wisdom") + print(" wisdom add -c quote - Add with category") + print(" wisdom add -s 'Author' - Add with source") + print(" wisdom list [category] - List entries") + print(" wisdom search - Search") + print("") + print(f"Categories: {', '.join(CATEGORIES.keys())}") + + # Show random by default + random_wisdom() + return + + cmd = sys.argv[1] + + if cmd == 'daily': + daily() + elif cmd == 'add' and len(sys.argv) > 2: + # Parse arguments + args = sys.argv[2:] + category = 'quote' + source = None + + # Check for flags + while args and args[0].startswith('-'): + if args[0] == '-c' and len(args) > 1: + category = args[1] + args = args[2:] + elif args[0] == '-s' and len(args) > 1: + source = args[1] + args = args[2:] + else: + args = args[1:] + + text = ' '.join(args) + add(text, category, source) + + elif cmd == 'list': + category = sys.argv[2] if len(sys.argv) > 2 else None + list_wisdom(category) + + elif cmd == 'search' and len(sys.argv) > 2: + search_wisdom(' '.join(sys.argv[2:])) + + else: + random_wisdom() + +if __name__ == "__main__": + main() diff --git a/ws b/ws index 7cf24e6..dac8224 100755 --- a/ws +++ b/ws @@ -29,6 +29,9 @@ COMMANDS = { 'habits': ('tools/habits.py', 'Habit tracker with streaks'), 'track': ('tools/track.py', 'Time tracking'), 'cap': ('tools/capture.py', 'Quick thought capture'), + 'today': ('tools/today.py', 'Daily overview dashboard'), + 'wisdom': ('tools/wisdom.py', 'Quotes and wisdom collection'), + 'sys': ('tools/sysmon.py', 'System monitor'), # Projects 'news': ('projects/news-feed/main.py', 'RSS news reader'),