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:
29
tools/README.md
Normal file
29
tools/README.md
Normal 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
105
tools/daily-digest.py
Executable 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
64
tools/inbox-processor.py
Executable 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
81
tools/search.py
Executable 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()
|
||||
Reference in New Issue
Block a user