Add capture tool - Quick thought/task/idea/link capture - Goes to inbox for later processing - Multiple types with emoji indicators - Export to markdown
This commit is contained in:
26
inbox/captures.json
Normal file
26
inbox/captures.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"content": "Build a voice memo transcription tool",
|
||||||
|
"type": "idea",
|
||||||
|
"tags": [],
|
||||||
|
"captured": "2026-01-30T23:28:05.330819",
|
||||||
|
"processed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"content": "Set up automatic backups",
|
||||||
|
"type": "task",
|
||||||
|
"tags": [],
|
||||||
|
"captured": "2026-01-30T23:28:05.356440",
|
||||||
|
"processed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"content": "This is a random thought",
|
||||||
|
"type": "note",
|
||||||
|
"tags": [],
|
||||||
|
"captured": "2026-01-30T23:28:05.381974",
|
||||||
|
"processed": false
|
||||||
|
}
|
||||||
|
]
|
||||||
177
tools/capture.py
Executable file
177
tools/capture.py
Executable file
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
capture - Quick thought capture
|
||||||
|
|
||||||
|
Quickly capture thoughts, tasks, ideas, and links.
|
||||||
|
They go to inbox for later processing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
|
||||||
|
CAPTURE_FILE = WORKSPACE / "inbox" / "captures.json"
|
||||||
|
|
||||||
|
TYPES = {
|
||||||
|
'thought': '💭',
|
||||||
|
'task': '✅',
|
||||||
|
'idea': '💡',
|
||||||
|
'link': '🔗',
|
||||||
|
'note': '📝',
|
||||||
|
'quote': '💬',
|
||||||
|
'question': '❓',
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_captures() -> list:
|
||||||
|
"""Load captured items."""
|
||||||
|
CAPTURE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if CAPTURE_FILE.exists():
|
||||||
|
with open(CAPTURE_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_captures(captures: list):
|
||||||
|
"""Save captures."""
|
||||||
|
with open(CAPTURE_FILE, 'w') as f:
|
||||||
|
json.dump(captures, f, indent=2)
|
||||||
|
|
||||||
|
def capture(content: str, capture_type: str = 'note', tags: list = None):
|
||||||
|
"""Capture something quickly."""
|
||||||
|
captures = load_captures()
|
||||||
|
|
||||||
|
item = {
|
||||||
|
'id': len(captures) + 1,
|
||||||
|
'content': content,
|
||||||
|
'type': capture_type if capture_type in TYPES else 'note',
|
||||||
|
'tags': tags or [],
|
||||||
|
'captured': datetime.now().isoformat(),
|
||||||
|
'processed': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
captures.append(item)
|
||||||
|
save_captures(captures)
|
||||||
|
|
||||||
|
emoji = TYPES.get(capture_type, '📝')
|
||||||
|
print(f"{emoji} Captured #{item['id']}: {content[:50]}...")
|
||||||
|
|
||||||
|
def list_captures(show_all: bool = False, type_filter: str = None):
|
||||||
|
"""List captured items."""
|
||||||
|
captures = load_captures()
|
||||||
|
|
||||||
|
if not show_all:
|
||||||
|
captures = [c for c in captures if not c.get('processed')]
|
||||||
|
|
||||||
|
if type_filter:
|
||||||
|
captures = [c for c in captures if c['type'] == type_filter]
|
||||||
|
|
||||||
|
if not captures:
|
||||||
|
print("📭 Inbox empty")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n📥 Inbox ({len(captures)} items)")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
for item in captures:
|
||||||
|
emoji = TYPES.get(item['type'], '📝')
|
||||||
|
status = "✓" if item.get('processed') else " "
|
||||||
|
content = item['content'][:60]
|
||||||
|
if len(item['content']) > 60:
|
||||||
|
content += "..."
|
||||||
|
|
||||||
|
print(f"[{status}] #{item['id']:3} {emoji} {content}")
|
||||||
|
|
||||||
|
if item.get('tags'):
|
||||||
|
tags = ' '.join(f"#{t}" for t in item['tags'])
|
||||||
|
print(f" {tags}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
def process_item(item_id: int):
|
||||||
|
"""Mark an item as processed."""
|
||||||
|
captures = load_captures()
|
||||||
|
|
||||||
|
for item in captures:
|
||||||
|
if item['id'] == item_id:
|
||||||
|
item['processed'] = True
|
||||||
|
save_captures(captures)
|
||||||
|
print(f"✓ Processed #{item_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Item not found: #{item_id}")
|
||||||
|
|
||||||
|
def clear_processed():
|
||||||
|
"""Remove processed items."""
|
||||||
|
captures = load_captures()
|
||||||
|
unprocessed = [c for c in captures if not c.get('processed')]
|
||||||
|
removed = len(captures) - len(unprocessed)
|
||||||
|
|
||||||
|
save_captures(unprocessed)
|
||||||
|
print(f"✓ Removed {removed} processed items")
|
||||||
|
|
||||||
|
def export_markdown():
|
||||||
|
"""Export unprocessed items as markdown."""
|
||||||
|
captures = load_captures()
|
||||||
|
unprocessed = [c for c in captures if not c.get('processed')]
|
||||||
|
|
||||||
|
if not unprocessed:
|
||||||
|
print("Nothing to export")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Group by type
|
||||||
|
by_type = {}
|
||||||
|
for item in unprocessed:
|
||||||
|
t = item['type']
|
||||||
|
if t not in by_type:
|
||||||
|
by_type[t] = []
|
||||||
|
by_type[t].append(item)
|
||||||
|
|
||||||
|
print("# Inbox Export")
|
||||||
|
print(f"*Exported: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
|
||||||
|
|
||||||
|
for type_name, items in sorted(by_type.items()):
|
||||||
|
emoji = TYPES.get(type_name, '📝')
|
||||||
|
print(f"## {emoji} {type_name.title()}s\n")
|
||||||
|
for item in items:
|
||||||
|
tags = ' '.join(f"`#{t}`" for t in item.get('tags', []))
|
||||||
|
print(f"- {item['content']} {tags}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage:")
|
||||||
|
print(" capture <text> - Quick capture")
|
||||||
|
print(" capture -t task <text> - Capture as task")
|
||||||
|
print(" capture -t idea <text> - Capture as idea")
|
||||||
|
print(" capture -t link <url> - Capture a link")
|
||||||
|
print(" capture list - Show inbox")
|
||||||
|
print(" capture list --all - Show all (incl. processed)")
|
||||||
|
print(" capture done <id> - Mark as processed")
|
||||||
|
print(" capture clear - Clear processed items")
|
||||||
|
print(" capture export - Export as markdown")
|
||||||
|
print("")
|
||||||
|
print(f"Types: {', '.join(TYPES.keys())}")
|
||||||
|
list_captures()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
if sys.argv[1] == 'list':
|
||||||
|
show_all = '--all' in sys.argv
|
||||||
|
list_captures(show_all=show_all)
|
||||||
|
elif sys.argv[1] == 'done' and len(sys.argv) > 2:
|
||||||
|
process_item(int(sys.argv[2]))
|
||||||
|
elif sys.argv[1] == 'clear':
|
||||||
|
clear_processed()
|
||||||
|
elif sys.argv[1] == 'export':
|
||||||
|
export_markdown()
|
||||||
|
elif sys.argv[1] == '-t' and len(sys.argv) > 3:
|
||||||
|
capture_type = sys.argv[2]
|
||||||
|
content = ' '.join(sys.argv[3:])
|
||||||
|
capture(content, capture_type)
|
||||||
|
else:
|
||||||
|
content = ' '.join(sys.argv[1:])
|
||||||
|
capture(content)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
ws
1
ws
@ -28,6 +28,7 @@ COMMANDS = {
|
|||||||
'focus': ('tools/focus.py', 'Pomodoro focus timer'),
|
'focus': ('tools/focus.py', 'Pomodoro focus timer'),
|
||||||
'habits': ('tools/habits.py', 'Habit tracker with streaks'),
|
'habits': ('tools/habits.py', 'Habit tracker with streaks'),
|
||||||
'track': ('tools/track.py', 'Time tracking'),
|
'track': ('tools/track.py', 'Time tracking'),
|
||||||
|
'cap': ('tools/capture.py', 'Quick thought capture'),
|
||||||
|
|
||||||
# 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