Add decision journal, metrics, ideas, weather, standup - decide.py: Decision journal with outcomes and lessons - metrics.py: Track arbitrary metrics over time - ideas.py: Idea incubator with stages - weather.py: Quick weather via wttr.in - standup.py: Daily standup generator - 27 tools total now
This commit is contained in:
227
tools/decide.py
Executable file
227
tools/decide.py
Executable file
@ -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 <id> <text>")
|
||||
|
||||
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 <id> <text> --good/--bad/--mixed")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" decide new <title> - 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()
|
||||
223
tools/ideas.py
Executable file
223
tools/ideas.py
Executable file
@ -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()
|
||||
208
tools/metrics.py
Executable file
208
tools/metrics.py
Executable file
@ -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()
|
||||
126
tools/standup.py
Executable file
126
tools/standup.py
Executable file
@ -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()
|
||||
72
tools/weather.py
Executable file
72
tools/weather.py
Executable file
@ -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()
|
||||
Reference in New Issue
Block a user