From 7925efe4f15f06f80286f1454d70e7db91f033e6 Mon Sep 17 00:00:00 2001 From: Case Date: Fri, 30 Jan 2026 23:46:35 -0600 Subject: [PATCH] Add goals, reading list, and people CRM - goals.py: Goal tracking with milestones and progress - reading.py: Reading list tracker (articles, books, papers) - people.py: Personal CRM for keeping notes on people - 21 tools total now --- data/goals.json | 42 +++++++++++ data/people.json | 15 ++++ data/reading.json | 22 ++++++ tools/goals.py | 173 ++++++++++++++++++++++++++++++++++++++++++++++ tools/people.py | 151 ++++++++++++++++++++++++++++++++++++++++ tools/reading.py | 163 +++++++++++++++++++++++++++++++++++++++++++ ws | 3 + 7 files changed, 569 insertions(+) create mode 100644 data/goals.json create mode 100644 data/people.json create mode 100644 data/reading.json create mode 100755 tools/goals.py create mode 100755 tools/people.py create mode 100755 tools/reading.py diff --git a/data/goals.json b/data/goals.json new file mode 100644 index 0000000..1775da4 --- /dev/null +++ b/data/goals.json @@ -0,0 +1,42 @@ +{ + "goals": [ + { + "id": 1, + "title": "Build out the sandbox into a daily driver", + "description": "", + "deadline": null, + "created": "2026-01-30T23:44:53.121855", + "status": "active", + "milestones": [ + { + "id": 1, + "title": "Core tools built", + "done": true, + "created": "2026-01-30T23:44:53.147088", + "completed": "2026-01-30T23:44:53.248604" + }, + { + "id": 2, + "title": "Projects created", + "done": true, + "created": "2026-01-30T23:44:53.172153", + "completed": "2026-01-30T23:44:53.273691" + }, + { + "id": 3, + "title": "Git/Gitea integration", + "done": true, + "created": "2026-01-30T23:44:53.197877", + "completed": "2026-01-30T23:44:53.299278" + }, + { + "id": 4, + "title": "Documentation complete", + "done": false, + "created": "2026-01-30T23:44:53.223477" + } + ], + "progress": 75 + } + ] +} \ No newline at end of file diff --git a/data/people.json b/data/people.json new file mode 100644 index 0000000..34fed42 --- /dev/null +++ b/data/people.json @@ -0,0 +1,15 @@ +{ + "d_j": { + "name": "D J", + "context": "My human, built this workspace together", + "added": "2026-01-30T23:45:35.014343", + "notes": [ + { + "text": "Late night building session, very productive", + "date": "2026-01-30T23:46:25.644083" + } + ], + "tags": [], + "last_contact": "2026-01-30T23:46:25.644093" + } +} \ No newline at end of file diff --git a/data/reading.json b/data/reading.json new file mode 100644 index 0000000..e0f57c8 --- /dev/null +++ b/data/reading.json @@ -0,0 +1,22 @@ +[ + { + "id": 1, + "title": "The Art of Doing Science and Engineering", + "url": null, + "type": "book", + "tags": [], + "added": "2026-01-30T23:45:14.470706", + "status": "unread", + "notes": null + }, + { + "id": 2, + "title": "Attention Is All You Need", + "url": null, + "type": "paper", + "tags": [], + "added": "2026-01-30T23:45:14.495258", + "status": "unread", + "notes": null + } +] \ No newline at end of file diff --git a/tools/goals.py b/tools/goals.py new file mode 100755 index 0000000..17a929d --- /dev/null +++ b/tools/goals.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +goals - Goal and project tracking with milestones + +Track long-term goals and break them into actionable steps. +""" + +import json +import sys +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +GOALS_FILE = WORKSPACE / "data" / "goals.json" + +def load_goals() -> dict: + """Load goals data.""" + GOALS_FILE.parent.mkdir(parents=True, exist_ok=True) + if GOALS_FILE.exists(): + with open(GOALS_FILE) as f: + return json.load(f) + return {'goals': []} + +def save_goals(data: dict): + """Save goals data.""" + with open(GOALS_FILE, 'w') as f: + json.dump(data, f, indent=2) + +def add_goal(title: str, description: str = "", deadline: str = None): + """Add a new goal.""" + data = load_goals() + + goal = { + 'id': len(data['goals']) + 1, + 'title': title, + 'description': description, + 'deadline': deadline, + 'created': datetime.now().isoformat(), + 'status': 'active', + 'milestones': [], + 'progress': 0, + } + + data['goals'].append(goal) + save_goals(data) + print(f"šŸŽÆ Goal #{goal['id']}: {title}") + +def add_milestone(goal_id: int, title: str): + """Add a milestone to a goal.""" + data = load_goals() + + for goal in data['goals']: + if goal['id'] == goal_id: + milestone = { + 'id': len(goal['milestones']) + 1, + 'title': title, + 'done': False, + 'created': datetime.now().isoformat(), + } + goal['milestones'].append(milestone) + save_goals(data) + print(f" āœ“ Added milestone: {title}") + return + + print(f"Goal not found: #{goal_id}") + +def complete_milestone(goal_id: int, milestone_id: int): + """Mark a milestone as complete.""" + data = load_goals() + + for goal in data['goals']: + if goal['id'] == goal_id: + for m in goal['milestones']: + if m['id'] == milestone_id: + m['done'] = True + m['completed'] = datetime.now().isoformat() + + # Update progress + done = len([x for x in goal['milestones'] if x['done']]) + total = len(goal['milestones']) + goal['progress'] = int(done / total * 100) if total > 0 else 0 + + save_goals(data) + print(f" āœ“ Completed: {m['title']}") + print(f" Progress: {goal['progress']}%") + return + + print("Milestone not found") + +def show_goals(show_all: bool = False): + """Display all goals.""" + data = load_goals() + + goals = data['goals'] + if not show_all: + goals = [g for g in goals if g['status'] == 'active'] + + if not goals: + print("No active goals. Add one with: goals add ") + return + + print(f"\nšŸŽÆ Goals ({len(goals)})") + print("=" * 50) + + for goal in goals: + status = "āœ…" if goal['status'] == 'completed' else "šŸ”µ" + bar = "ā–ˆ" * (goal['progress'] // 10) + "ā–‘" * (10 - goal['progress'] // 10) + + print(f"\n{status} #{goal['id']} {goal['title']}") + print(f" [{bar}] {goal['progress']}%") + + if goal.get('deadline'): + print(f" šŸ“… Deadline: {goal['deadline']}") + + if goal['milestones']: + for m in goal['milestones']: + check = "āœ“" if m['done'] else "ā—‹" + print(f" {check} {m['title']}") + + print() + +def complete_goal(goal_id: int): + """Mark a goal as completed.""" + data = load_goals() + + for goal in data['goals']: + if goal['id'] == goal_id: + goal['status'] = 'completed' + goal['progress'] = 100 + goal['completed'] = datetime.now().isoformat() + save_goals(data) + print(f"šŸŽ‰ Goal completed: {goal['title']}") + return + + print(f"Goal not found: #{goal_id}") + +def main(): + if len(sys.argv) < 2: + show_goals() + return + + cmd = sys.argv[1] + + if cmd == 'add' and len(sys.argv) > 2: + title = ' '.join(sys.argv[2:]) + add_goal(title) + + elif cmd == 'milestone' and len(sys.argv) > 3: + goal_id = int(sys.argv[2]) + title = ' '.join(sys.argv[3:]) + add_milestone(goal_id, title) + + elif cmd == 'done' and len(sys.argv) > 3: + goal_id = int(sys.argv[2]) + milestone_id = int(sys.argv[3]) + complete_milestone(goal_id, milestone_id) + + elif cmd == 'complete' and len(sys.argv) > 2: + complete_goal(int(sys.argv[2])) + + elif cmd == 'list': + show_goals(show_all='--all' in sys.argv) + + else: + print("Usage:") + print(" goals - Show active goals") + print(" goals add <title> - Add new goal") + print(" goals milestone <id> <text> - Add milestone") + print(" goals done <goal> <milestone> - Complete milestone") + print(" goals complete <id> - Complete goal") + +if __name__ == "__main__": + main() diff --git a/tools/people.py b/tools/people.py new file mode 100755 index 0000000..3eb297b --- /dev/null +++ b/tools/people.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +people - Personal CRM / people notes + +Keep notes about people you know. +""" + +import json +import sys +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +PEOPLE_FILE = WORKSPACE / "data" / "people.json" + +def load_people() -> dict: + """Load people data.""" + PEOPLE_FILE.parent.mkdir(parents=True, exist_ok=True) + if PEOPLE_FILE.exists(): + with open(PEOPLE_FILE) as f: + return json.load(f) + return {} + +def save_people(data: dict): + """Save people data.""" + with open(PEOPLE_FILE, 'w') as f: + json.dump(data, f, indent=2) + +def add_person(name: str, context: str = None): + """Add a new person.""" + people = load_people() + key = name.lower().replace(' ', '_') + + if key in people: + print(f"Person already exists: {name}") + return + + people[key] = { + 'name': name, + 'context': context, + 'added': datetime.now().isoformat(), + 'notes': [], + 'tags': [], + 'last_contact': None, + } + + save_people(people) + print(f"šŸ‘¤ Added: {name}") + +def add_note(name: str, note: str): + """Add a note about someone.""" + people = load_people() + search = name.lower().replace(' ', '') + + # Find by partial match (normalize spaces) + matches = [k for k in people if search in k.replace('_', '') or search in people[k]['name'].lower().replace(' ', '')] + if not matches: + print(f"Person not found: {name}") + return + + key = matches[0] + people[key]['notes'].append({ + 'text': note, + 'date': datetime.now().isoformat(), + }) + people[key]['last_contact'] = datetime.now().isoformat() + + save_people(people) + print(f"šŸ“ Note added for {people[key]['name']}") + +def show_person(name: str): + """Show info about a person.""" + people = load_people() + search = name.lower().replace(' ', '') + + # Find by partial match (normalize spaces) + matches = [k for k in people if search in k.replace('_', '') or search in people[k]['name'].lower().replace(' ', '')] + if not matches: + print(f"Person not found: {name}") + return + + person = people[matches[0]] + + print(f"\nšŸ‘¤ {person['name']}") + print("=" * 40) + + if person.get('context'): + print(f"Context: {person['context']}") + + if person.get('last_contact'): + last = person['last_contact'][:10] + print(f"Last contact: {last}") + + if person['notes']: + print(f"\nšŸ“ Notes ({len(person['notes'])})") + for note in person['notes'][-5:]: + date = note['date'][:10] + print(f" [{date}] {note['text'][:50]}") + + print() + +def list_people(): + """List all people.""" + people = load_people() + + if not people: + print("No people yet. Add with: people add <name>") + return + + print(f"\nšŸ‘„ People ({len(people)})") + print("=" * 40) + + for key, person in sorted(people.items()): + notes = len(person['notes']) + context = person.get('context', '')[:20] + print(f" {person['name']:20} {notes} notes {context}") + + print() + +def main(): + if len(sys.argv) < 2: + list_people() + return + + cmd = sys.argv[1] + + if cmd == 'add' and len(sys.argv) > 2: + name = sys.argv[2] + context = ' '.join(sys.argv[3:]) if len(sys.argv) > 3 else None + add_person(name, context) + + elif cmd == 'note' and len(sys.argv) > 3: + name = sys.argv[2] + note = ' '.join(sys.argv[3:]) + add_note(name, note) + + elif cmd == 'show' and len(sys.argv) > 2: + show_person(sys.argv[2]) + + elif cmd == 'list': + list_people() + + else: + print("Usage:") + print(" people - List all") + print(" people add <name> [context] - Add person") + print(" people note <name> <text> - Add note") + print(" people show <name> - Show person") + +if __name__ == "__main__": + main() diff --git a/tools/reading.py b/tools/reading.py new file mode 100755 index 0000000..d16b8d6 --- /dev/null +++ b/tools/reading.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +reading - Reading list and article tracker + +Track articles, books, and things to read. +""" + +import json +import sys +from datetime import datetime +from pathlib import Path + +WORKSPACE = Path("/home/wdjones/.openclaw/workspace") +READING_FILE = WORKSPACE / "data" / "reading.json" + +def load_list() -> list: + """Load reading list.""" + READING_FILE.parent.mkdir(parents=True, exist_ok=True) + if READING_FILE.exists(): + with open(READING_FILE) as f: + return json.load(f) + return [] + +def save_list(items: list): + """Save reading list.""" + with open(READING_FILE, 'w') as f: + json.dump(items, f, indent=2) + +def add(title: str, url: str = None, item_type: str = 'article', tags: list = None): + """Add to reading list.""" + items = load_list() + + item = { + 'id': len(items) + 1, + 'title': title, + 'url': url, + 'type': item_type, + 'tags': tags or [], + 'added': datetime.now().isoformat(), + 'status': 'unread', + 'notes': None, + } + + items.append(item) + save_list(items) + + emoji = {'article': 'šŸ“„', 'book': 'šŸ“š', 'video': 'šŸŽ¬', 'paper': 'šŸ“‘'}.get(item_type, 'šŸ“„') + print(f"{emoji} Added: {title}") + +def show_list(status: str = None, limit: int = 20): + """Show reading list.""" + items = load_list() + + if status: + items = [i for i in items if i['status'] == status] + + if not items: + print("Reading list is empty") + return + + # Group by status + unread = [i for i in items if i['status'] == 'unread'] + reading = [i for i in items if i['status'] == 'reading'] + done = [i for i in items if i['status'] == 'done'] + + print(f"\nšŸ“š Reading List") + print("=" * 50) + + if reading: + print(f"\nšŸ“– Currently Reading ({len(reading)})") + for item in reading: + print(f" #{item['id']} {item['title'][:40]}") + + if unread: + print(f"\nšŸ“„ To Read ({len(unread)})") + for item in unread[:10]: + emoji = {'article': 'šŸ“„', 'book': 'šŸ“š', 'video': 'šŸŽ¬'}.get(item['type'], 'šŸ“„') + print(f" #{item['id']} {emoji} {item['title'][:40]}") + if len(unread) > 10: + print(f" ... and {len(unread) - 10} more") + + if done: + print(f"\nāœ… Completed ({len(done)})") + for item in done[-5:]: + print(f" #{item['id']} {item['title'][:40]}") + + print() + +def start_reading(item_id: int): + """Mark item as currently reading.""" + items = load_list() + for item in items: + if item['id'] == item_id: + item['status'] = 'reading' + item['started'] = datetime.now().isoformat() + save_list(items) + print(f"šŸ“– Started: {item['title']}") + return + print(f"Item not found: #{item_id}") + +def finish_reading(item_id: int, notes: str = None): + """Mark item as done.""" + items = load_list() + for item in items: + if item['id'] == item_id: + item['status'] = 'done' + item['finished'] = datetime.now().isoformat() + if notes: + item['notes'] = notes + save_list(items) + print(f"āœ… Finished: {item['title']}") + return + print(f"Item not found: #{item_id}") + +def main(): + if len(sys.argv) < 2: + show_list() + return + + cmd = sys.argv[1] + + if cmd == 'add' and len(sys.argv) > 2: + # Parse: add <title> [--url URL] [--type TYPE] + args = sys.argv[2:] + title_parts = [] + url = None + item_type = 'article' + + i = 0 + while i < len(args): + if args[i] == '--url' and i + 1 < len(args): + url = args[i + 1] + i += 2 + elif args[i] == '--type' and i + 1 < len(args): + item_type = args[i + 1] + i += 2 + else: + title_parts.append(args[i]) + i += 1 + + add(' '.join(title_parts), url, item_type) + + elif cmd == 'start' and len(sys.argv) > 2: + start_reading(int(sys.argv[2])) + + elif cmd == 'done' and len(sys.argv) > 2: + item_id = int(sys.argv[2]) + notes = ' '.join(sys.argv[3:]) if len(sys.argv) > 3 else None + finish_reading(item_id, notes) + + elif cmd == 'list': + show_list() + + else: + print("Usage:") + print(" reading - Show list") + print(" reading add <title> - Add article") + print(" reading add <title> --type book --url URL") + print(" reading start <id> - Start reading") + print(" reading done <id> [notes] - Finish reading") + +if __name__ == "__main__": + main() diff --git a/ws b/ws index 5d724e2..84aadc5 100755 --- a/ws +++ b/ws @@ -34,6 +34,9 @@ COMMANDS = { 'sys': ('tools/sysmon.py', 'System monitor'), 'journal': ('tools/journal.py', 'Structured journaling'), 'backup': ('tools/backup.py', 'Backup and export'), + 'goals': ('tools/goals.py', 'Goal tracking'), + 'reading': ('tools/reading.py', 'Reading list'), + 'people': ('tools/people.py', 'Personal CRM'), # Projects 'news': ('projects/news-feed/main.py', 'RSS news reader'),