Add advanced tools - dashboard.py: Web dashboard on localhost:8080 - briefing.py: Morning briefing generator - scaffold.py: Project scaffolding (python, node, script, docs, experiment) - watcher.py: File change monitor - focus.py: Pomodoro-style focus timer

This commit is contained in:
2026-01-30 23:19:19 -06:00
parent 7123c4ccd2
commit b579251e90
5 changed files with 757 additions and 0 deletions

132
tools/briefing.py Executable file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Morning briefing generator - creates a daily summary for the human.
"""
import os
import json
from datetime import datetime, timedelta
from pathlib import Path
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
def get_weather_stub():
"""Placeholder for weather - would need API."""
return "☀️ Weather check not configured"
def get_tasks_summary():
"""Get task summary."""
tasks_file = WORKSPACE / "TASKS.md"
if not tasks_file.exists():
return None
summary = {'in_progress': [], 'waiting': [], 'inbox': []}
section = None
with open(tasks_file) as f:
for line in f:
if "## Inbox" in line: section = 'inbox'
elif "## In Progress" in line: section = 'in_progress'
elif "## Waiting" in line: section = 'waiting'
elif "## Done" in line: section = None
elif section and line.strip().startswith("- ["):
task = line.strip()[6:].strip() # Remove "- [ ] "
if task:
summary[section].append(task)
return summary
def get_yesterday_notes():
"""Get yesterday's notes summary."""
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
yesterday_file = WORKSPACE / "memory" / f"{yesterday}.md"
if not yesterday_file.exists():
return None
content = yesterday_file.read_text()
lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
return lines[:5] # First 5 non-header lines
def get_inbox_items():
"""Check inbox for pending items."""
inbox_dir = WORKSPACE / "inbox"
if not inbox_dir.exists():
return []
items = []
for f in inbox_dir.iterdir():
if f.is_file() and not f.name.startswith('.'):
items.append(f.name)
return items
def get_clips_recent():
"""Get recent clips."""
clips_file = WORKSPACE / "docs" / "clips.json"
if not clips_file.exists():
return []
try:
with open(clips_file) as f:
clips = json.load(f)
return clips[:3]
except:
return []
def generate_briefing():
"""Generate the morning briefing."""
now = datetime.now()
lines = []
lines.append(f"# 📋 Briefing - {now.strftime('%A, %B %d, %Y')}")
lines.append(f"Generated at {now.strftime('%H:%M')}")
lines.append("")
# Tasks
tasks = get_tasks_summary()
if tasks:
lines.append("## 🎯 Tasks")
if tasks['in_progress']:
lines.append(f"**In Progress ({len(tasks['in_progress'])}):**")
for t in tasks['in_progress'][:3]:
lines.append(f"{t[:60]}...")
if tasks['waiting']:
lines.append(f"**Waiting ({len(tasks['waiting'])}):** {len(tasks['waiting'])} items")
if tasks['inbox']:
lines.append(f"**Inbox ({len(tasks['inbox'])}):** {len(tasks['inbox'])} items to triage")
lines.append("")
# Yesterday
yesterday = get_yesterday_notes()
if yesterday:
lines.append("## 📝 Yesterday")
for note in yesterday[:3]:
lines.append(f"{note[:60]}")
lines.append("")
# Inbox
inbox = get_inbox_items()
if inbox:
lines.append("## 📬 Inbox")
lines.append(f"{len(inbox)} items waiting: {', '.join(inbox[:3])}")
lines.append("")
# Clips
clips = get_clips_recent()
if clips:
lines.append("## 📎 Recent Clips")
for clip in clips:
lines.append(f"{clip.get('title', 'Untitled')}")
lines.append("")
lines.append("---")
lines.append("*What's the focus today?*")
return "\n".join(lines)
def main():
briefing = generate_briefing()
print(briefing)
if __name__ == "__main__":
main()

187
tools/dashboard.py Executable file
View File

@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
Simple workspace dashboard - serves a status page on localhost.
"""
import http.server
import socketserver
import json
import os
from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import parse_qs, urlparse
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
PORT = 8080
def get_workspace_stats():
"""Gather workspace statistics."""
stats = {
'generated': datetime.now().isoformat(),
'files': {'total': 0, 'by_type': {}},
'recent': [],
'tasks': {'inbox': 0, 'in_progress': 0, 'waiting': 0, 'done_today': 0},
'memory': {'today': None, 'days_logged': 0}
}
# Count files
for root, dirs, files in os.walk(WORKSPACE):
dirs[:] = [d for d in dirs if not d.startswith('.')]
for fname in files:
if fname.startswith('.'):
continue
stats['files']['total'] += 1
ext = Path(fname).suffix or 'no-ext'
stats['files']['by_type'][ext] = stats['files']['by_type'].get(ext, 0) + 1
# Track recent files
fpath = Path(root) / fname
try:
mtime = datetime.fromtimestamp(fpath.stat().st_mtime)
if datetime.now() - mtime < timedelta(hours=24):
rel = str(fpath.relative_to(WORKSPACE))
stats['recent'].append({'path': rel, 'modified': mtime.isoformat()})
except:
pass
stats['recent'] = sorted(stats['recent'], key=lambda x: x['modified'], reverse=True)[:10]
# Parse tasks
tasks_file = WORKSPACE / "TASKS.md"
if tasks_file.exists():
section = None
today = datetime.now().strftime("%Y-%m-%d")
with open(tasks_file) as f:
for line in f:
if "## Inbox" in line: section = 'inbox'
elif "## In Progress" in line: section = 'in_progress'
elif "## Waiting" in line: section = 'waiting'
elif "## Done" in line: section = 'done'
elif line.strip().startswith("- ["):
if section == 'done' and today in line:
stats['tasks']['done_today'] += 1
elif section and section != 'done':
stats['tasks'][section] += 1
# Memory stats
memory_dir = WORKSPACE / "memory"
if memory_dir.exists():
days = [f for f in memory_dir.iterdir() if f.suffix == '.md']
stats['memory']['days_logged'] = len(days)
today_file = memory_dir / f"{datetime.now().strftime('%Y-%m-%d')}.md"
if today_file.exists():
stats['memory']['today'] = len(today_file.read_text().split('\n'))
return stats
def generate_html(stats):
"""Generate dashboard HTML."""
return f"""<!DOCTYPE html>
<html>
<head>
<title>Workspace Dashboard</title>
<meta charset="utf-8">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a; color: #e0e0e0; padding: 2rem;
}}
h1 {{ color: #fff; margin-bottom: 0.5rem; }}
.subtitle {{ color: #666; margin-bottom: 2rem; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }}
.card {{
background: #1a1a1a; border-radius: 12px; padding: 1.5rem;
border: 1px solid #333;
}}
.card h2 {{ color: #888; font-size: 0.875rem; text-transform: uppercase; margin-bottom: 1rem; }}
.stat {{ font-size: 2.5rem; font-weight: bold; color: #fff; }}
.stat-label {{ color: #666; font-size: 0.875rem; }}
.list {{ list-style: none; }}
.list li {{
padding: 0.5rem 0; border-bottom: 1px solid #222;
font-family: monospace; font-size: 0.875rem;
}}
.list li:last-child {{ border-bottom: none; }}
.tag {{
display: inline-block; background: #333; padding: 0.25rem 0.5rem;
border-radius: 4px; font-size: 0.75rem; margin-right: 0.5rem;
}}
.heart {{ color: #ff6b6b; }}
</style>
</head>
<body>
<h1>🖤 Case Dashboard</h1>
<p class="subtitle">Generated {stats['generated'][:19]}</p>
<div class="grid">
<div class="card">
<h2>📁 Files</h2>
<div class="stat">{stats['files']['total']}</div>
<div class="stat-label">total files in workspace</div>
<div style="margin-top: 1rem;">
{''.join(f'<span class="tag">{ext}: {count}</span>' for ext, count in sorted(stats['files']['by_type'].items(), key=lambda x: -x[1])[:5])}
</div>
</div>
<div class="card">
<h2>✅ Tasks</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div><div class="stat">{stats['tasks']['in_progress']}</div><div class="stat-label">in progress</div></div>
<div><div class="stat">{stats['tasks']['waiting']}</div><div class="stat-label">waiting</div></div>
<div><div class="stat">{stats['tasks']['inbox']}</div><div class="stat-label">inbox</div></div>
<div><div class="stat">{stats['tasks']['done_today']}</div><div class="stat-label">done today</div></div>
</div>
</div>
<div class="card">
<h2>📝 Memory</h2>
<div class="stat">{stats['memory']['days_logged']}</div>
<div class="stat-label">days logged</div>
<div style="margin-top: 1rem; color: #666;">
Today: {stats['memory']['today'] or 0} lines
</div>
</div>
<div class="card" style="grid-column: span 2;">
<h2>🕐 Recent Activity</h2>
<ul class="list">
{''.join(f"<li>{f['path']}</li>" for f in stats['recent'][:8]) or '<li>No recent activity</li>'}
</ul>
</div>
</div>
</body>
</html>"""
class DashboardHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/' or self.path == '/dashboard':
stats = get_workspace_stats()
html = generate_html(stats)
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.encode())
elif self.path == '/api/stats':
stats = get_workspace_stats()
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(stats, indent=2).encode())
else:
self.send_error(404)
def log_message(self, format, *args):
pass # Suppress logs
def main():
with socketserver.TCPServer(("", PORT), DashboardHandler) as httpd:
print(f"Dashboard running at http://localhost:{PORT}")
print("Press Ctrl+C to stop")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nShutting down...")
if __name__ == "__main__":
main()

137
tools/focus.py Executable file
View File

@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
Focus timer - simple pomodoro-style timer with logging.
"""
import sys
import time
import json
from datetime import datetime
from pathlib import Path
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
LOG_FILE = WORKSPACE / "memory" / "focus-log.json"
def load_log() -> list:
"""Load focus session log."""
if LOG_FILE.exists():
try:
with open(LOG_FILE) as f:
return json.load(f)
except:
return []
return []
def save_log(log: list):
"""Save focus session log."""
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, 'w') as f:
json.dump(log, f, indent=2)
def format_time(seconds: int) -> str:
"""Format seconds as MM:SS."""
mins, secs = divmod(seconds, 60)
return f"{mins:02d}:{secs:02d}"
def timer(minutes: int, task: str = None):
"""Run a focus timer."""
total_seconds = minutes * 60
remaining = total_seconds
start_time = datetime.now()
print(f"\n🎯 Focus session started: {minutes} minutes")
if task:
print(f" Task: {task}")
print()
try:
while remaining > 0:
print(f"\r ⏱️ {format_time(remaining)} remaining ", end='', flush=True)
time.sleep(1)
remaining -= 1
print(f"\r ✅ Session complete! ")
print("\n🔔 Time's up! Take a break.\n")
# Log the session
log = load_log()
log.append({
'start': start_time.isoformat(),
'end': datetime.now().isoformat(),
'duration_minutes': minutes,
'task': task,
'completed': True
})
save_log(log)
except KeyboardInterrupt:
elapsed = total_seconds - remaining
print(f"\r ⏸️ Stopped after {format_time(elapsed)} ")
if elapsed > 60: # Log if > 1 minute
log = load_log()
log.append({
'start': start_time.isoformat(),
'end': datetime.now().isoformat(),
'duration_minutes': round(elapsed / 60, 1),
'task': task,
'completed': False
})
save_log(log)
print(" Session logged (partial)")
def stats():
"""Show focus statistics."""
log = load_log()
if not log:
print("No focus sessions logged yet.")
return
today = datetime.now().strftime("%Y-%m-%d")
today_sessions = [s for s in log if s['start'].startswith(today)]
total_minutes = sum(s['duration_minutes'] for s in log)
today_minutes = sum(s['duration_minutes'] for s in today_sessions)
completed = len([s for s in log if s.get('completed')])
print("\n📊 Focus Stats")
print(f" Total sessions: {len(log)}")
print(f" Completed: {completed}")
print(f" Total time: {total_minutes:.0f} minutes ({total_minutes/60:.1f} hours)")
print(f" Today: {len(today_sessions)} sessions, {today_minutes:.0f} minutes")
if today_sessions:
print("\n Today's sessions:")
for s in today_sessions[-5:]:
start = s['start'][11:16]
status = "" if s.get('completed') else "⏸️"
task = s.get('task', '')[:30] or 'No task'
print(f" {status} {start} - {s['duration_minutes']}min - {task}")
print()
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" focus.py <minutes> [task] - Start a focus session")
print(" focus.py stats - Show statistics")
print("\nExamples:")
print(" focus.py 25 'Write documentation'")
print(" focus.py 15")
sys.exit(0)
if sys.argv[1] == 'stats':
stats()
return
try:
minutes = int(sys.argv[1])
except ValueError:
print("Minutes must be a number")
sys.exit(1)
task = ' '.join(sys.argv[2:]) if len(sys.argv) > 2 else None
timer(minutes, task)
if __name__ == "__main__":
main()

127
tools/scaffold.py Executable file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
Project scaffolding tool - creates project structures for different types.
"""
import os
import sys
from pathlib import Path
from datetime import datetime
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
PROJECTS = WORKSPACE / "projects"
TEMPLATES = {
'python': {
'files': {
'README.md': '# {name}\n\n{description}\n\n## Setup\n\n```bash\npython -m venv venv\nsource venv/bin/activate\npip install -r requirements.txt\n```\n\n## Usage\n\n```bash\npython main.py\n```\n',
'main.py': '#!/usr/bin/env python3\n"""\n{name} - {description}\n"""\n\ndef main():\n print("Hello from {name}")\n\nif __name__ == "__main__":\n main()\n',
'requirements.txt': '# Add dependencies here\n',
'.gitignore': 'venv/\n__pycache__/\n*.pyc\n.env\n*.egg-info/\ndist/\nbuild/\n',
},
'dirs': ['src', 'tests'],
},
'node': {
'files': {
'README.md': '# {name}\n\n{description}\n\n## Setup\n\n```bash\npnpm install\n```\n\n## Usage\n\n```bash\npnpm start\n```\n',
'package.json': '{{\n "name": "{name}",\n "version": "0.1.0",\n "description": "{description}",\n "main": "index.js",\n "scripts": {{\n "start": "node index.js"\n }}\n}}\n',
'index.js': '// {name}\n// {description}\n\nconsole.log("Hello from {name}");\n',
'.gitignore': 'node_modules/\n.env\ndist/\n',
},
'dirs': ['src', 'lib'],
},
'script': {
'files': {
'README.md': '# {name}\n\n{description}\n\n## Usage\n\n```bash\n./script.sh\n```\n',
'script.sh': '#!/bin/bash\n# {name} - {description}\n\nset -euo pipefail\n\necho "Running {name}"\n',
},
'dirs': [],
},
'docs': {
'files': {
'README.md': '# {name}\n\n{description}\n\n## Contents\n\n- [Overview](overview.md)\n',
'overview.md': '# Overview\n\nAdd documentation here.\n',
},
'dirs': ['images', 'guides'],
},
'experiment': {
'files': {
'README.md': '# {name}\n\n**Experiment started:** {date}\n\n## Hypothesis\n\n{description}\n\n## Method\n\n\n## Results\n\n\n## Conclusion\n\n',
'notes.md': '# Experiment Notes\n\n## {date}\n\n- Started experiment\n',
},
'dirs': ['data', 'output'],
},
}
def scaffold(name: str, template: str = 'python', description: str = ''):
"""Create a new project from template."""
if template not in TEMPLATES:
print(f"Unknown template: {template}")
print(f"Available: {', '.join(TEMPLATES.keys())}")
return False
project_dir = PROJECTS / name
if project_dir.exists():
print(f"Project already exists: {name}")
return False
tmpl = TEMPLATES[template]
date = datetime.now().strftime("%Y-%m-%d")
# Create directories
project_dir.mkdir(parents=True)
for d in tmpl['dirs']:
(project_dir / d).mkdir()
# Create files
for fname, content in tmpl['files'].items():
fpath = project_dir / fname
formatted = content.format(name=name, description=description or 'No description', date=date)
fpath.write_text(formatted)
# Make scripts executable
for f in project_dir.glob('*.sh'):
os.chmod(f, 0o755)
for f in project_dir.glob('*.py'):
if f.read_text().startswith('#!/'):
os.chmod(f, 0o755)
print(f"✓ Created {template} project: {name}")
print(f" Location: {project_dir}")
print(f" Files: {len(tmpl['files'])}")
if tmpl['dirs']:
print(f" Dirs: {', '.join(tmpl['dirs'])}")
return True
def list_templates():
"""List available templates."""
print("Available templates:\n")
for name, tmpl in TEMPLATES.items():
files = ', '.join(tmpl['files'].keys())
print(f" {name}:")
print(f" Files: {files}")
if tmpl['dirs']:
print(f" Dirs: {', '.join(tmpl['dirs'])}")
print()
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" scaffold.py <name> [template] [description]")
print(" scaffold.py --list")
print("\nTemplates: python, node, script, docs, experiment")
sys.exit(1)
if sys.argv[1] == '--list':
list_templates()
return
name = sys.argv[1]
template = sys.argv[2] if len(sys.argv) > 2 else 'python'
description = ' '.join(sys.argv[3:]) if len(sys.argv) > 3 else ''
scaffold(name, template, description)
if __name__ == "__main__":
main()

174
tools/watcher.py Executable file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Simple file watcher - monitors workspace for changes and logs them.
Uses polling (no external dependencies).
"""
import os
import sys
import time
import json
from datetime import datetime
from pathlib import Path
from collections import defaultdict
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
STATE_FILE = WORKSPACE / ".watcher-state.json"
LOG_FILE = WORKSPACE / "memory" / "file-changes.log"
IGNORE_PATTERNS = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.watcher-state.json'}
IGNORE_EXTENSIONS = {'.pyc', '.pyo', '.log'}
def should_ignore(path: Path) -> bool:
"""Check if path should be ignored."""
parts = path.parts
for pattern in IGNORE_PATTERNS:
if pattern in parts:
return True
if path.suffix in IGNORE_EXTENSIONS:
return True
return False
def get_file_state() -> dict:
"""Get current state of all files."""
state = {}
for root, dirs, files in os.walk(WORKSPACE):
# Filter directories
dirs[:] = [d for d in dirs if d not in IGNORE_PATTERNS and not d.startswith('.')]
for fname in files:
fpath = Path(root) / fname
if should_ignore(fpath):
continue
try:
stat = fpath.stat()
rel_path = str(fpath.relative_to(WORKSPACE))
state[rel_path] = {
'mtime': stat.st_mtime,
'size': stat.st_size,
}
except:
pass
return state
def load_state() -> dict:
"""Load previous state from file."""
if STATE_FILE.exists():
try:
with open(STATE_FILE) as f:
return json.load(f)
except:
pass
return {}
def save_state(state: dict):
"""Save current state to file."""
with open(STATE_FILE, 'w') as f:
json.dump(state, f)
def log_change(change_type: str, path: str):
"""Log a change to the log file."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{timestamp}] {change_type}: {path}\n"
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, 'a') as f:
f.write(line)
print(line.strip())
def diff_states(old: dict, new: dict) -> dict:
"""Compare states and return changes."""
changes = {'added': [], 'modified': [], 'deleted': []}
old_keys = set(old.keys())
new_keys = set(new.keys())
# Added files
for path in new_keys - old_keys:
changes['added'].append(path)
# Deleted files
for path in old_keys - new_keys:
changes['deleted'].append(path)
# Modified files
for path in old_keys & new_keys:
if old[path]['mtime'] != new[path]['mtime'] or old[path]['size'] != new[path]['size']:
changes['modified'].append(path)
return changes
def watch(interval: int = 5):
"""Watch for changes continuously."""
print(f"Watching {WORKSPACE}")
print(f"Interval: {interval}s")
print(f"Log: {LOG_FILE}")
print("Press Ctrl+C to stop\n")
state = get_file_state()
save_state(state)
try:
while True:
time.sleep(interval)
new_state = get_file_state()
changes = diff_states(state, new_state)
for path in changes['added']:
log_change("ADDED", path)
for path in changes['modified']:
log_change("MODIFIED", path)
for path in changes['deleted']:
log_change("DELETED", path)
if any(changes.values()):
save_state(new_state)
state = new_state
except KeyboardInterrupt:
print("\nStopped watching")
def check_once():
"""Check for changes once and exit."""
old_state = load_state()
new_state = get_file_state()
if not old_state:
print(f"First run - indexed {len(new_state)} files")
save_state(new_state)
return
changes = diff_states(old_state, new_state)
total = sum(len(v) for v in changes.values())
if total == 0:
print("No changes detected")
else:
print(f"Changes since last check:")
for path in changes['added']:
print(f" + {path}")
for path in changes['modified']:
print(f" ~ {path}")
for path in changes['deleted']:
print(f" - {path}")
save_state(new_state)
def main():
if len(sys.argv) > 1:
if sys.argv[1] == 'watch':
interval = int(sys.argv[2]) if len(sys.argv) > 2 else 5
watch(interval)
elif sys.argv[1] == 'check':
check_once()
else:
print("Usage:")
print(" watcher.py check - Check for changes once")
print(" watcher.py watch [interval] - Watch continuously")
else:
check_once()
if __name__ == "__main__":
main()