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:
14
data/decisions.json
Normal file
14
data/decisions.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -32,11 +32,12 @@
|
|||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"title": "Documentation complete",
|
"title": "Documentation complete",
|
||||||
"done": false,
|
"done": true,
|
||||||
"created": "2026-01-30T23:44:53.223477"
|
"created": "2026-01-30T23:44:53.223477",
|
||||||
|
"completed": "2026-01-30T23:47:41.400479"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"progress": 75
|
"progress": 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -18,7 +18,8 @@
|
|||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"2026-01-30": [
|
"2026-01-30": [
|
||||||
"journal"
|
"journal",
|
||||||
|
"reading"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
data/ideas.json
Normal file
32
data/ideas.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
35
data/metrics.json
Normal file
35
data/metrics.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
"task": "More building",
|
"task": "Late night building - wine edition",
|
||||||
"project": "sandbox",
|
"project": "sandbox",
|
||||||
"start": "2026-01-30T23:27:15.604246"
|
"start": "2026-01-30T23:53:10.102195"
|
||||||
},
|
},
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
@ -11,6 +11,13 @@
|
|||||||
"start": "2026-01-30T22:27:15.578348",
|
"start": "2026-01-30T22:27:15.578348",
|
||||||
"end": "2026-01-30T23:27:15.578348",
|
"end": "2026-01-30T23:27:15.578348",
|
||||||
"duration_min": 60
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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()
|
||||||
5
ws
5
ws
@ -38,6 +38,11 @@ COMMANDS = {
|
|||||||
'reading': ('tools/reading.py', 'Reading list'),
|
'reading': ('tools/reading.py', 'Reading list'),
|
||||||
'people': ('tools/people.py', 'Personal CRM'),
|
'people': ('tools/people.py', 'Personal CRM'),
|
||||||
'ref': ('tools/ref.py', 'Quick reference snippets'),
|
'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
|
# Projects
|
||||||
'news': ('projects/news-feed/main.py', 'RSS news reader'),
|
'news': ('projects/news-feed/main.py', 'RSS news reader'),
|
||||||
|
|||||||
Reference in New Issue
Block a user