diff --git a/data/decisions.json b/data/decisions.json new file mode 100644 index 0000000..80ad8dd --- /dev/null +++ b/data/decisions.json @@ -0,0 +1,14 @@ +[ + { + "id": 1, + "title": "Build the sandbox as a daily driver", + "context": null, + "options_considered": [], + "reasoning": "Fresh environment, can customize fully, D J wants autonomous assistant", + "decided": "2026-01-30T23:53:41.544665", + "outcome": "Built 22 tools, 2 projects, full workspace in one night", + "outcome_date": "2026-01-30T23:53:41.595958", + "lessons": null, + "status": "good" + } +] \ No newline at end of file diff --git a/data/goals.json b/data/goals.json index 1775da4..8a6eb2b 100644 --- a/data/goals.json +++ b/data/goals.json @@ -32,11 +32,12 @@ { "id": 4, "title": "Documentation complete", - "done": false, - "created": "2026-01-30T23:44:53.223477" + "done": true, + "created": "2026-01-30T23:44:53.223477", + "completed": "2026-01-30T23:47:41.400479" } ], - "progress": 75 + "progress": 100 } ] } \ No newline at end of file diff --git a/data/habits.json b/data/habits.json index d5b64d4..c2e03cf 100644 --- a/data/habits.json +++ b/data/habits.json @@ -18,7 +18,8 @@ }, "log": { "2026-01-30": [ - "journal" + "journal", + "reading" ] } } \ No newline at end of file diff --git a/data/ideas.json b/data/ideas.json new file mode 100644 index 0000000..a410622 --- /dev/null +++ b/data/ideas.json @@ -0,0 +1,32 @@ +[ + { + "id": 1, + "title": "Voice memo transcription tool", + "description": null, + "tags": [], + "stage": "seed", + "notes": [], + "created": "2026-01-30T23:54:38.665336", + "updated": "2026-01-30T23:54:38.665342" + }, + { + "id": 2, + "title": "Automatic workspace sync between machines", + "description": null, + "tags": [], + "stage": "seed", + "notes": [], + "created": "2026-01-30T23:54:38.691565", + "updated": "2026-01-30T23:54:38.691571" + }, + { + "id": 3, + "title": "AI-powered daily briefing with news summarization", + "description": null, + "tags": [], + "stage": "seed", + "notes": [], + "created": "2026-01-30T23:54:38.718059", + "updated": "2026-01-30T23:54:38.718065" + } +] \ No newline at end of file diff --git a/data/metrics.json b/data/metrics.json new file mode 100644 index 0000000..1fcd42e --- /dev/null +++ b/data/metrics.json @@ -0,0 +1,35 @@ +{ + "metrics": { + "tools_built": { + "name": "tools_built", + "unit": "", + "description": "Number of tools built", + "entries": [ + { + "value": 22.0, + "timestamp": "2026-01-30T23:54:10.295568", + "note": "First day" + }, + { + "value": 27.0, + "timestamp": "2026-01-30T23:55:32.463773", + "note": "After midnight push" + } + ], + "created": "2026-01-30T23:54:10.266015" + }, + "commits": { + "name": "commits", + "unit": "", + "description": "Git commits", + "entries": [ + { + "value": 14.0, + "timestamp": "2026-01-30T23:54:10.354713", + "note": "First day" + } + ], + "created": "2026-01-30T23:54:10.325021" + } + } +} \ No newline at end of file diff --git a/data/timetrack.json b/data/timetrack.json index 768295f..6c12e15 100644 --- a/data/timetrack.json +++ b/data/timetrack.json @@ -1,8 +1,8 @@ { "current": { - "task": "More building", + "task": "Late night building - wine edition", "project": "sandbox", - "start": "2026-01-30T23:27:15.604246" + "start": "2026-01-30T23:53:10.102195" }, "entries": [ { @@ -11,6 +11,13 @@ "start": "2026-01-30T22:27:15.578348", "end": "2026-01-30T23:27:15.578348", "duration_min": 60 + }, + { + "task": "More building", + "project": "sandbox", + "start": "2026-01-30T23:27:15.604246", + "end": "2026-01-30T23:47:41.373585", + "duration_min": 20.4 } ] } \ No newline at end of file diff --git a/tools/decide.py b/tools/decide.py new file mode 100755 index 0000000..22c0336 --- /dev/null +++ b/tools/decide.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +decide - Decision journal + +Track decisions, their reasoning, and outcomes. +Learn from past choices. +""" + +import json +import sys +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +DECISIONS_FILE = WORKSPACE / "data" / "decisions.json" + +def load_decisions() -> list: + """Load decisions.""" + DECISIONS_FILE.parent.mkdir(parents=True, exist_ok=True) + if DECISIONS_FILE.exists(): + with open(DECISIONS_FILE) as f: + return json.load(f) + return [] + +def save_decisions(data: list): + """Save decisions.""" + with open(DECISIONS_FILE, 'w') as f: + json.dump(data, f, indent=2) + +def new_decision(title: str, context: str = None): + """Record a new decision.""" + decisions = load_decisions() + + decision = { + 'id': len(decisions) + 1, + 'title': title, + 'context': context, + 'options_considered': [], + 'reasoning': None, + 'decided': datetime.now().isoformat(), + 'outcome': None, + 'outcome_date': None, + 'lessons': None, + 'status': 'pending', # pending, good, bad, mixed + } + + decisions.append(decision) + save_decisions(decisions) + + print(f"⚖️ Decision #{decision['id']}: {title}") + print(" Add reasoning with: decide reason ") + +def add_reasoning(decision_id: int, reasoning: str): + """Add reasoning to a decision.""" + decisions = load_decisions() + + for d in decisions: + if d['id'] == decision_id: + d['reasoning'] = reasoning + save_decisions(decisions) + print(f"✓ Reasoning added to #{decision_id}") + return + + print(f"Decision not found: #{decision_id}") + +def record_outcome(decision_id: int, outcome: str, status: str = 'mixed'): + """Record the outcome of a decision.""" + decisions = load_decisions() + + for d in decisions: + if d['id'] == decision_id: + d['outcome'] = outcome + d['outcome_date'] = datetime.now().isoformat() + d['status'] = status if status in ['good', 'bad', 'mixed'] else 'mixed' + save_decisions(decisions) + + emoji = {'good': '✅', 'bad': '❌', 'mixed': '🔄'}[d['status']] + print(f"{emoji} Outcome recorded for #{decision_id}") + return + + print(f"Decision not found: #{decision_id}") + +def add_lesson(decision_id: int, lesson: str): + """Add a lesson learned from a decision.""" + decisions = load_decisions() + + for d in decisions: + if d['id'] == decision_id: + d['lessons'] = lesson + save_decisions(decisions) + print(f"📚 Lesson added to #{decision_id}") + return + + print(f"Decision not found: #{decision_id}") + +def show_decision(decision_id: int): + """Show details of a decision.""" + decisions = load_decisions() + + for d in decisions: + if d['id'] == decision_id: + status_emoji = {'pending': '⏳', 'good': '✅', 'bad': '❌', 'mixed': '🔄'} + + print(f"\n⚖️ Decision #{d['id']}: {d['title']}") + print("=" * 50) + print(f"Status: {status_emoji.get(d['status'], '⏳')} {d['status']}") + print(f"Date: {d['decided'][:10]}") + + if d.get('context'): + print(f"\n📋 Context:\n {d['context']}") + + if d.get('reasoning'): + print(f"\n🤔 Reasoning:\n {d['reasoning']}") + + if d.get('outcome'): + print(f"\n📊 Outcome:\n {d['outcome']}") + + if d.get('lessons'): + print(f"\n📚 Lessons:\n {d['lessons']}") + + print() + return + + print(f"Decision not found: #{decision_id}") + +def list_decisions(show_all: bool = False): + """List all decisions.""" + decisions = load_decisions() + + if not show_all: + decisions = [d for d in decisions if d['status'] == 'pending'] + + if not decisions: + print("No decisions recorded" if show_all else "No pending decisions") + return + + print(f"\n⚖️ Decisions ({len(decisions)})") + print("=" * 50) + + status_emoji = {'pending': '⏳', 'good': '✅', 'bad': '❌', 'mixed': '🔄'} + + for d in decisions: + emoji = status_emoji.get(d['status'], '⏳') + print(f" {emoji} #{d['id']} {d['title'][:40]}") + print(f" {d['decided'][:10]}") + + print() + +def review(): + """Review decisions that need outcomes.""" + decisions = load_decisions() + pending = [d for d in decisions if d['status'] == 'pending'] + + if not pending: + print("No decisions pending review") + return + + print(f"\n📋 Decisions Pending Review ({len(pending)})") + print("=" * 50) + + for d in pending: + days = (datetime.now() - datetime.fromisoformat(d['decided'])).days + print(f" #{d['id']} {d['title'][:40]}") + print(f" {days} days ago — needs outcome") + + print("\nRecord outcomes with: decide outcome --good/--bad/--mixed") + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" decide new - Record a decision") + print(" decide reason <id> <text> - Add reasoning") + print(" decide outcome <id> <text> [--good/--bad/--mixed]") + print(" decide lesson <id> <text> - Add lesson learned") + print(" decide show <id> - Show decision details") + print(" decide list [--all] - List decisions") + print(" decide review - Review pending decisions") + list_decisions() + return + + cmd = sys.argv[1] + + if cmd == 'new' and len(sys.argv) > 2: + title = ' '.join(sys.argv[2:]) + new_decision(title) + + elif cmd == 'reason' and len(sys.argv) > 3: + decision_id = int(sys.argv[2]) + reasoning = ' '.join(sys.argv[3:]) + add_reasoning(decision_id, reasoning) + + elif cmd == 'outcome' and len(sys.argv) > 3: + decision_id = int(sys.argv[2]) + status = 'mixed' + args = sys.argv[3:] + + if '--good' in args: + status = 'good' + args.remove('--good') + elif '--bad' in args: + status = 'bad' + args.remove('--bad') + elif '--mixed' in args: + args.remove('--mixed') + + outcome = ' '.join(args) + record_outcome(decision_id, outcome, status) + + elif cmd == 'lesson' and len(sys.argv) > 3: + decision_id = int(sys.argv[2]) + lesson = ' '.join(sys.argv[3:]) + add_lesson(decision_id, lesson) + + elif cmd == 'show' and len(sys.argv) > 2: + show_decision(int(sys.argv[2])) + + elif cmd == 'list': + list_decisions(show_all='--all' in sys.argv) + + elif cmd == 'review': + review() + + else: + print("Unknown command. Run 'decide' for help.") + +if __name__ == "__main__": + main() diff --git a/tools/ideas.py b/tools/ideas.py new file mode 100755 index 0000000..b1b680a --- /dev/null +++ b/tools/ideas.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +ideas - Idea incubator + +Capture, develop, and track ideas over time. +""" + +import json +import sys +import random +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +IDEAS_FILE = WORKSPACE / "data" / "ideas.json" + +STAGES = ['seed', 'growing', 'ready', 'planted', 'archived'] +STAGE_EMOJI = {'seed': '🌱', 'growing': '🌿', 'ready': '🌻', 'planted': '🌳', 'archived': '📦'} + +def load_ideas() -> list: + """Load ideas.""" + IDEAS_FILE.parent.mkdir(parents=True, exist_ok=True) + if IDEAS_FILE.exists(): + with open(IDEAS_FILE) as f: + return json.load(f) + return [] + +def save_ideas(ideas: list): + """Save ideas.""" + with open(IDEAS_FILE, 'w') as f: + json.dump(ideas, f, indent=2) + +def add_idea(title: str, description: str = None, tags: list = None): + """Add a new idea.""" + ideas = load_ideas() + + idea = { + 'id': len(ideas) + 1, + 'title': title, + 'description': description, + 'tags': tags or [], + 'stage': 'seed', + 'notes': [], + 'created': datetime.now().isoformat(), + 'updated': datetime.now().isoformat(), + } + + ideas.append(idea) + save_ideas(ideas) + print(f"🌱 Idea #{idea['id']}: {title}") + +def add_note(idea_id: int, note: str): + """Add a development note to an idea.""" + ideas = load_ideas() + + for idea in ideas: + if idea['id'] == idea_id: + idea['notes'].append({ + 'text': note, + 'date': datetime.now().isoformat(), + }) + idea['updated'] = datetime.now().isoformat() + save_ideas(ideas) + print(f"📝 Note added to idea #{idea_id}") + return + + print(f"Idea not found: #{idea_id}") + +def advance(idea_id: int): + """Advance an idea to the next stage.""" + ideas = load_ideas() + + for idea in ideas: + if idea['id'] == idea_id: + current = idea['stage'] + idx = STAGES.index(current) + if idx < len(STAGES) - 2: # Don't auto-advance to archived + idea['stage'] = STAGES[idx + 1] + idea['updated'] = datetime.now().isoformat() + save_ideas(ideas) + emoji = STAGE_EMOJI[idea['stage']] + print(f"{emoji} Idea #{idea_id} advanced to: {idea['stage']}") + else: + print("Idea already at final stage") + return + + print(f"Idea not found: #{idea_id}") + +def archive(idea_id: int): + """Archive an idea.""" + ideas = load_ideas() + + for idea in ideas: + if idea['id'] == idea_id: + idea['stage'] = 'archived' + idea['updated'] = datetime.now().isoformat() + save_ideas(ideas) + print(f"📦 Idea #{idea_id} archived") + return + + print(f"Idea not found: #{idea_id}") + +def show_idea(idea_id: int): + """Show idea details.""" + ideas = load_ideas() + + for idea in ideas: + if idea['id'] == idea_id: + emoji = STAGE_EMOJI[idea['stage']] + + print(f"\n{emoji} Idea #{idea['id']}: {idea['title']}") + print("=" * 50) + print(f"Stage: {idea['stage']}") + print(f"Created: {idea['created'][:10]}") + + if idea.get('description'): + print(f"\n{idea['description']}") + + if idea.get('tags'): + print(f"\nTags: {' '.join('#' + t for t in idea['tags'])}") + + if idea['notes']: + print(f"\n📝 Development Notes ({len(idea['notes'])})") + for note in idea['notes'][-5:]: + print(f" [{note['date'][:10]}] {note['text']}") + + print() + return + + print(f"Idea not found: #{idea_id}") + +def list_ideas(stage: str = None): + """List ideas.""" + ideas = load_ideas() + + if stage: + ideas = [i for i in ideas if i['stage'] == stage] + else: + ideas = [i for i in ideas if i['stage'] != 'archived'] + + if not ideas: + print("No ideas yet. Add with: ideas add <title>") + return + + print(f"\n💡 Ideas ({len(ideas)})") + print("=" * 50) + + for stage in STAGES[:-1]: # Exclude archived + stage_ideas = [i for i in ideas if i['stage'] == stage] + if stage_ideas: + emoji = STAGE_EMOJI[stage] + print(f"\n{emoji} {stage.title()} ({len(stage_ideas)})") + for idea in stage_ideas: + notes = len(idea['notes']) + print(f" #{idea['id']} {idea['title'][:35]} ({notes} notes)") + + print() + +def random_idea(): + """Get a random idea to work on.""" + ideas = load_ideas() + active = [i for i in ideas if i['stage'] in ['seed', 'growing']] + + if not active: + print("No active ideas") + return + + idea = random.choice(active) + emoji = STAGE_EMOJI[idea['stage']] + + print(f"\n🎲 Random idea to develop:\n") + print(f" {emoji} #{idea['id']} {idea['title']}") + if idea.get('description'): + print(f" {idea['description'][:60]}...") + print() + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" ideas add <title> - Add new idea") + print(" ideas note <id> <text> - Add development note") + print(" ideas advance <id> - Move to next stage") + print(" ideas show <id> - Show idea details") + print(" ideas list [stage] - List ideas") + print(" ideas random - Random idea to work on") + print(" ideas archive <id> - Archive idea") + print("") + print(f"Stages: {' → '.join(STAGES[:-1])}") + list_ideas() + return + + cmd = sys.argv[1] + + if cmd == 'add' and len(sys.argv) > 2: + title = ' '.join(sys.argv[2:]) + add_idea(title) + + elif cmd == 'note' and len(sys.argv) > 3: + idea_id = int(sys.argv[2]) + note = ' '.join(sys.argv[3:]) + add_note(idea_id, note) + + elif cmd == 'advance' and len(sys.argv) > 2: + advance(int(sys.argv[2])) + + elif cmd == 'show' and len(sys.argv) > 2: + show_idea(int(sys.argv[2])) + + elif cmd == 'list': + stage = sys.argv[2] if len(sys.argv) > 2 else None + list_ideas(stage) + + elif cmd == 'random': + random_idea() + + elif cmd == 'archive' and len(sys.argv) > 2: + archive(int(sys.argv[2])) + + else: + print("Unknown command. Run 'ideas' for help.") + +if __name__ == "__main__": + main() diff --git a/tools/metrics.py b/tools/metrics.py new file mode 100755 index 0000000..dc09e49 --- /dev/null +++ b/tools/metrics.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +metrics - Track arbitrary metrics over time + +Log numeric values and see trends. +""" + +import json +import sys +from datetime import datetime, timedelta +from pathlib import Path +from statistics import mean, stdev + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +METRICS_FILE = WORKSPACE / "data" / "metrics.json" + +def load_metrics() -> dict: + """Load metrics data.""" + METRICS_FILE.parent.mkdir(parents=True, exist_ok=True) + if METRICS_FILE.exists(): + with open(METRICS_FILE) as f: + return json.load(f) + return {'metrics': {}} + +def save_metrics(data: dict): + """Save metrics data.""" + with open(METRICS_FILE, 'w') as f: + json.dump(data, f, indent=2) + +def define_metric(name: str, unit: str = '', description: str = ''): + """Define a new metric to track.""" + data = load_metrics() + + key = name.lower().replace(' ', '_') + if key in data['metrics']: + print(f"Metric already exists: {name}") + return + + data['metrics'][key] = { + 'name': name, + 'unit': unit, + 'description': description, + 'entries': [], + 'created': datetime.now().isoformat(), + } + + save_metrics(data) + print(f"📊 Created metric: {name}") + +def log_value(name: str, value: float, note: str = None): + """Log a value for a metric.""" + data = load_metrics() + key = name.lower().replace(' ', '_') + + # Find metric by partial match + matches = [k for k in data['metrics'] if key in k] + if not matches: + print(f"Metric not found: {name}") + print("Create with: metrics define <name>") + return + + key = matches[0] + metric = data['metrics'][key] + + entry = { + 'value': value, + 'timestamp': datetime.now().isoformat(), + 'note': note, + } + + metric['entries'].append(entry) + save_metrics(data) + + unit = metric.get('unit', '') + print(f"📈 {metric['name']}: {value}{unit}") + +def show_metric(name: str, days: int = 7): + """Show metric details and trend.""" + data = load_metrics() + key = name.lower().replace(' ', '_') + + matches = [k for k in data['metrics'] if key in k] + if not matches: + print(f"Metric not found: {name}") + return + + key = matches[0] + metric = data['metrics'][key] + entries = metric['entries'] + + print(f"\n📊 {metric['name']}") + if metric.get('description'): + print(f" {metric['description']}") + print("=" * 40) + + if not entries: + print("No data yet") + return + + # Filter to date range + cutoff = datetime.now() - timedelta(days=days) + recent = [e for e in entries if datetime.fromisoformat(e['timestamp']) > cutoff] + + if not recent: + print(f"No data in last {days} days") + return + + values = [e['value'] for e in recent] + unit = metric.get('unit', '') + + # Stats + avg = mean(values) + latest = values[-1] + high = max(values) + low = min(values) + + print(f"\n Last {days} days ({len(recent)} entries):") + print(f" Latest: {latest}{unit}") + print(f" Average: {avg:.1f}{unit}") + print(f" High: {high}{unit}") + print(f" Low: {low}{unit}") + + # Trend + if len(values) >= 2: + first_half = mean(values[:len(values)//2]) + second_half = mean(values[len(values)//2:]) + if second_half > first_half * 1.05: + print(f" Trend: 📈 Up") + elif second_half < first_half * 0.95: + print(f" Trend: 📉 Down") + else: + print(f" Trend: ➡️ Stable") + + # Recent entries + print(f"\n Recent:") + for e in recent[-5:]: + date = e['timestamp'][:10] + note = f" ({e['note']})" if e.get('note') else "" + print(f" {date}: {e['value']}{unit}{note}") + + print() + +def list_metrics(): + """List all metrics.""" + data = load_metrics() + + if not data['metrics']: + print("No metrics defined yet") + print("Create with: metrics define <name> [unit] [description]") + return + + print(f"\n📊 Metrics ({len(data['metrics'])})") + print("=" * 40) + + for key, metric in data['metrics'].items(): + entries = len(metric['entries']) + unit = metric.get('unit', '') + + if entries > 0: + latest = metric['entries'][-1]['value'] + print(f" {metric['name']}: {latest}{unit} ({entries} entries)") + else: + print(f" {metric['name']}: (no data)") + + print() + +def main(): + if len(sys.argv) < 2: + print("Usage:") + print(" metrics define <name> [unit] [description]") + print(" metrics log <name> <value> [note]") + print(" metrics show <name> [days]") + print(" metrics list") + print("") + print("Examples:") + print(" metrics define weight kg") + print(" metrics log weight 75.5") + print(" metrics show weight 30") + list_metrics() + return + + cmd = sys.argv[1] + + if cmd == 'define' and len(sys.argv) > 2: + name = sys.argv[2] + unit = sys.argv[3] if len(sys.argv) > 3 else '' + desc = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else '' + define_metric(name, unit, desc) + + elif cmd == 'log' and len(sys.argv) > 3: + name = sys.argv[2] + value = float(sys.argv[3]) + note = ' '.join(sys.argv[4:]) if len(sys.argv) > 4 else None + log_value(name, value, note) + + elif cmd == 'show' and len(sys.argv) > 2: + name = sys.argv[2] + days = int(sys.argv[3]) if len(sys.argv) > 3 else 7 + show_metric(name, days) + + elif cmd == 'list': + list_metrics() + + else: + print("Unknown command. Run 'metrics' for help.") + +if __name__ == "__main__": + main() diff --git a/tools/standup.py b/tools/standup.py new file mode 100755 index 0000000..1d09bda --- /dev/null +++ b/tools/standup.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +standup - Generate daily standup notes + +Pulls from yesterday's notes, today's tasks, and active work. +""" + +import json +from datetime import datetime, timedelta +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") + +def load_json(path: Path): + """Load JSON file safely.""" + if path.exists(): + try: + with open(path) as f: + return json.load(f) + except: + pass + return {} if 'json' in str(path) else [] + +def get_yesterday_notes(): + """Get yesterday's activity from memory.""" + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + notes_file = WORKSPACE / "memory" / f"{yesterday}.md" + + if not notes_file.exists(): + return None + + return notes_file.read_text() + +def get_tasks(): + """Get current tasks.""" + tasks_file = WORKSPACE / "TASKS.md" + if not tasks_file.exists(): + return {'in_progress': [], 'waiting': []} + + result = {'in_progress': [], 'waiting': []} + section = None + + with open(tasks_file) as f: + for line in f: + if "## In Progress" in line: section = 'in_progress' + elif "## Waiting" in line: section = 'waiting' + elif "## Done" in line or "## Inbox" in line: section = None + elif section and line.strip().startswith("- [ ]"): + task = line.strip()[6:].strip() + result[section].append(task) + + return result + +def get_time_tracked(): + """Get yesterday's time tracking.""" + data = load_json(WORKSPACE / "data" / "timetrack.json") + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + + entries = [e for e in data.get('entries', []) if e['start'].startswith(yesterday)] + + if not entries: + return None + + total_mins = sum(e['duration_min'] for e in entries) + by_project = {} + for e in entries: + proj = e.get('project') or 'general' + by_project[proj] = by_project.get(proj, 0) + e['duration_min'] + + return {'total_mins': total_mins, 'by_project': by_project} + +def get_blockers(): + """Get waiting/blocked items as potential blockers.""" + tasks = get_tasks() + return tasks.get('waiting', []) + +def format_duration(mins: float) -> str: + if mins < 60: + return f"{mins:.0f}m" + hours = int(mins // 60) + m = int(mins % 60) + return f"{hours}h {m}m" + +def generate_standup(): + """Generate standup notes.""" + now = datetime.now() + + print(f"\n📋 Daily Standup - {now.strftime('%A, %B %d, %Y')}") + print("=" * 50) + + # Yesterday + print("\n**Yesterday:**") + time_data = get_time_tracked() + if time_data: + print(f" • Tracked {format_duration(time_data['total_mins'])} of work") + for proj, mins in time_data['by_project'].items(): + print(f" - {proj}: {format_duration(mins)}") + else: + print(" • (no time tracked)") + + # Today + print("\n**Today:**") + tasks = get_tasks() + if tasks['in_progress']: + for task in tasks['in_progress'][:5]: + print(f" • {task[:50]}") + else: + print(" • (no tasks in progress)") + + # Blockers + print("\n**Blockers:**") + blockers = get_blockers() + if blockers: + for blocker in blockers[:3]: + print(f" ⚠️ {blocker[:50]}") + else: + print(" • None") + + print("\n" + "=" * 50) + print() + +def main(): + generate_standup() + +if __name__ == "__main__": + main() diff --git a/tools/weather.py b/tools/weather.py new file mode 100755 index 0000000..5605c00 --- /dev/null +++ b/tools/weather.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +weather - Quick weather check + +Get current weather and forecasts using wttr.in. +""" + +import sys +import subprocess +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +DEFAULT_LOCATION = "auto" # wttr.in auto-detects + +def get_weather(location: str = None, detailed: bool = False): + """Get weather for a location.""" + loc = location or DEFAULT_LOCATION + loc = loc.replace(' ', '+') + + if detailed: + # Full forecast + cmd = f'curl -s "wttr.in/{loc}?T"' + else: + # Compact format + cmd = f'curl -s "wttr.in/{loc}?format=%l:+%c+%t+%h+%w"' + + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + output = result.stdout.strip() + + if 'Unknown location' in output: + print(f"Location not found: {location}") + return + + print(output) + +def get_moon(): + """Get moon phase.""" + cmd = 'curl -s "wttr.in/moon"' + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + print(result.stdout) + +def main(): + if len(sys.argv) < 2: + # Default: compact weather for auto location + get_weather() + return + + cmd = sys.argv[1] + + if cmd == 'moon': + get_moon() + elif cmd == 'full': + location = ' '.join(sys.argv[2:]) if len(sys.argv) > 2 else None + get_weather(location, detailed=True) + elif cmd in ['-h', '--help']: + print("Usage:") + print(" weather - Current weather (auto location)") + print(" weather <location> - Weather for location") + print(" weather full [loc] - Detailed forecast") + print(" weather moon - Moon phase") + print("") + print("Examples:") + print(" weather London") + print(" weather 'New York'") + print(" weather full Chicago") + else: + # Treat as location + location = ' '.join(sys.argv[1:]) + get_weather(location) + +if __name__ == "__main__": + main() diff --git a/ws b/ws index 14cfa63..edf74bf 100755 --- a/ws +++ b/ws @@ -38,6 +38,11 @@ COMMANDS = { 'reading': ('tools/reading.py', 'Reading list'), 'people': ('tools/people.py', 'Personal CRM'), 'ref': ('tools/ref.py', 'Quick reference snippets'), + 'decide': ('tools/decide.py', 'Decision journal'), + 'metrics': ('tools/metrics.py', 'Track metrics over time'), + 'ideas': ('tools/ideas.py', 'Idea incubator'), + 'weather': ('tools/weather.py', 'Weather check'), + 'standup': ('tools/standup.py', 'Daily standup generator'), # Projects 'news': ('projects/news-feed/main.py', 'RSS news reader'),