Initial sandbox buildout - Structure: projects, docs, inbox, archive, templates, scripts, tools - Tools: search.py, inbox-processor.py, daily-digest.py - Shell aliases and bashrc integration - Templates for projects and notes - MEMORY.md, TASKS.md, STRUCTURE.md - tmux config

This commit is contained in:
2026-01-30 23:14:35 -06:00
commit f18a1944d6
19 changed files with 828 additions and 0 deletions

29
tools/README.md Normal file
View File

@ -0,0 +1,29 @@
# Tools
Local tools and utilities we build.
## Built
### search.py
Search workspace files for keywords.
```bash
python3 tools/search.py "query" [max_results]
```
### inbox-processor.py
Show inbox contents and status.
```bash
python3 tools/inbox-processor.py
```
### daily-digest.py
Generate daily activity summary.
```bash
python3 tools/daily-digest.py
```
## Planned
- [ ] Web clipper (save URLs with notes)
- [ ] File indexer with tags
- [ ] Reminder system

105
tools/daily-digest.py Executable file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Generate a daily digest of workspace activity.
"""
import os
from pathlib import Path
from datetime import datetime, timedelta
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
def get_recent_files(hours: int = 24) -> list:
"""Get files modified in the last N hours."""
cutoff = datetime.now() - timedelta(hours=hours)
recent = []
for root, dirs, files in os.walk(WORKSPACE):
dirs[:] = [d for d in dirs if not d.startswith('.')]
for fname in files:
fpath = Path(root) / fname
try:
mtime = datetime.fromtimestamp(fpath.stat().st_mtime)
if mtime > cutoff:
rel_path = fpath.relative_to(WORKSPACE)
recent.append({
'path': str(rel_path),
'modified': mtime,
'size': fpath.stat().st_size
})
except Exception:
continue
return sorted(recent, key=lambda x: x['modified'], reverse=True)
def get_tasks_summary() -> dict:
"""Parse TASKS.md and return summary."""
tasks_file = WORKSPACE / "TASKS.md"
if not tasks_file.exists():
return {}
summary = {
'inbox': [],
'in_progress': [],
'waiting': [],
'done_today': []
}
current_section = None
today = datetime.now().strftime("%Y-%m-%d")
with open(tasks_file) as f:
for line in f:
line = line.strip()
if line.startswith("## Inbox"):
current_section = 'inbox'
elif line.startswith("## In Progress"):
current_section = 'in_progress'
elif line.startswith("## Waiting"):
current_section = 'waiting'
elif line.startswith("## Done"):
current_section = 'done'
elif line.startswith("- ["):
if current_section == 'done' and today in line:
summary['done_today'].append(line)
elif current_section in summary:
summary[current_section].append(line)
return summary
def generate_digest():
"""Generate the daily digest."""
print("=" * 50)
print(f"📊 Daily Digest - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 50)
# Recent files
recent = get_recent_files(24)
print(f"\n📁 Files modified (last 24h): {len(recent)}")
for f in recent[:10]:
print(f" {f['path']}")
if len(recent) > 10:
print(f" ... and {len(recent) - 10} more")
# Tasks
tasks = get_tasks_summary()
print(f"\n✅ Tasks")
print(f" In Progress: {len(tasks.get('in_progress', []))}")
print(f" Waiting: {len(tasks.get('waiting', []))}")
print(f" Done Today: {len(tasks.get('done_today', []))}")
# Today's notes
today_file = WORKSPACE / "memory" / f"{datetime.now().strftime('%Y-%m-%d')}.md"
if today_file.exists():
with open(today_file) as f:
lines = len(f.readlines())
print(f"\n📝 Today's notes: {lines} lines")
print("\n" + "=" * 50)
if __name__ == "__main__":
generate_digest()

64
tools/inbox-processor.py Executable file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""
Inbox processor - shows pending items and helps sort them.
"""
import os
from pathlib import Path
from datetime import datetime
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
INBOX = WORKSPACE / "inbox"
def list_inbox():
"""List all items in inbox."""
items = []
for fpath in INBOX.iterdir():
if fpath.is_file() and not fpath.name.startswith('.'):
stat = fpath.stat()
items.append({
'name': fpath.name,
'path': fpath,
'size': stat.st_size,
'modified': datetime.fromtimestamp(stat.st_mtime)
})
return sorted(items, key=lambda x: x['modified'], reverse=True)
def show_inbox():
"""Display inbox contents."""
items = list_inbox()
if not items:
print("📭 Inbox is empty")
return
print(f"📬 Inbox ({len(items)} items)\n")
for item in items:
age = datetime.now() - item['modified']
if age.days > 0:
age_str = f"{age.days}d ago"
elif age.seconds > 3600:
age_str = f"{age.seconds // 3600}h ago"
else:
age_str = f"{age.seconds // 60}m ago"
print(f" [{age_str}] {item['name']} ({item['size']} bytes)")
# Show quick-notes if exists
quick_notes = INBOX / "quick-notes.md"
if quick_notes.exists():
print("\n--- Quick Notes ---")
with open(quick_notes) as f:
content = f.read().strip()
if content:
print(content)
else:
print("(empty)")
if __name__ == "__main__":
show_inbox()

81
tools/search.py Executable file
View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Simple workspace search tool.
Searches markdown files and returns ranked results.
"""
import os
import sys
import re
from pathlib import Path
from collections import defaultdict
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
EXTENSIONS = {'.md', '.txt', '.sh', '.py', '.json'}
def search(query: str, max_results: int = 10) -> list:
"""Search workspace for query, return list of (file, line_num, line, score)."""
results = []
query_lower = query.lower()
query_words = query_lower.split()
for root, dirs, files in os.walk(WORKSPACE):
# Skip hidden directories
dirs[:] = [d for d in dirs if not d.startswith('.')]
for fname in files:
fpath = Path(root) / fname
if fpath.suffix not in EXTENSIONS:
continue
try:
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
for i, line in enumerate(f, 1):
line_lower = line.lower()
# Calculate score
score = 0
for word in query_words:
if word in line_lower:
score += 1
# Bonus for exact phrase
if query_lower in line_lower:
score += 2
# Bonus for word boundaries
if re.search(rf'\b{re.escape(word)}\b', line_lower):
score += 1
if score > 0:
rel_path = fpath.relative_to(WORKSPACE)
results.append((str(rel_path), i, line.strip(), score))
except Exception:
continue
# Sort by score descending
results.sort(key=lambda x: -x[3])
return results[:max_results]
def main():
if len(sys.argv) < 2:
print("Usage: search.py <query> [max_results]")
sys.exit(1)
query = sys.argv[1]
max_results = int(sys.argv[2]) if len(sys.argv) > 2 else 10
results = search(query, max_results)
if not results:
print(f"No results for: {query}")
return
print(f"Results for: {query}\n")
for fpath, line_num, line, score in results:
# Truncate long lines
display = line[:80] + "..." if len(line) > 80 else line
print(f"[{fpath}:{line_num}] {display}")
if __name__ == "__main__":
main()