#!/usr/bin/env python3 """ Godspeed Task Sweeper - Consolidate incomplete tasks from markdown files. This script recursively searches through directories (like 2024/, 2025/, etc.) and moves all incomplete tasks from markdown files into the Godspeed Inbox.md file. """ import argparse import re import shutil from pathlib import Path from typing import List, Tuple, Set from datetime import datetime class TaskSweeper: """Sweeps incomplete tasks from markdown files into Godspeed Inbox.""" def __init__(self, notes_dir: Path, godspeed_dir: Path, dry_run: bool = False): self.notes_dir = Path(notes_dir) self.godspeed_dir = Path(godspeed_dir) self.dry_run = dry_run self.inbox_file = self.godspeed_dir / "Inbox.md" # Import the sync engine for consistent parsing try: from src.services.godspeed.sync import GodspeedSync self.sync_engine = GodspeedSync(None, godspeed_dir) except ImportError: # Fallback parsing if import fails self.sync_engine = None def _parse_task_line_fallback(self, line: str) -> Tuple[str, str, str, str]: """Fallback task parsing if sync engine not available.""" # Match patterns like: - [ ] Task title task_pattern = ( r"^\s*-\s*\[([xX\s\-])\]\s*(.+?)(?:\s*)?\s*$" ) match = re.match(task_pattern, line.strip()) if not match: return None checkbox, title_and_notes, local_id = match.groups() # Determine status if checkbox.lower() == "x": status = "complete" elif checkbox == "-": status = "cleared" else: status = "incomplete" # Extract title (remove any inline notes after " if notes: formatted += f"\n {notes}" new_task_lines.append(formatted) # Combine with existing content if existing_content.strip(): new_content = ( existing_content.rstrip() + "\n\n" + "\n".join(new_task_lines) + "\n" ) else: new_content = "\n".join(new_task_lines) + "\n" with open(file_path, "w", encoding="utf-8") as f: f.write(new_content) def _clean_file(self, file_path: Path, non_task_lines: List[str]): """Remove tasks from original file, keeping only non-task content.""" if not non_task_lines or all(not line.strip() for line in non_task_lines): # File would be empty, delete it if not self.dry_run: file_path.unlink() print(f" ๐Ÿ—‘๏ธ Would delete empty file: {file_path}") else: # Write back non-task content cleaned_content = "\n".join(non_task_lines).strip() if cleaned_content: cleaned_content += "\n" if not self.dry_run: with open(file_path, "w", encoding="utf-8") as f: f.write(cleaned_content) print(f" โœ‚๏ธ Cleaned file (removed tasks): {file_path}") def find_markdown_files(self) -> List[Path]: """Find all markdown files in the notes directory, excluding Godspeed directory.""" markdown_files = [] for md_file in self.notes_dir.rglob("*.md"): # Skip files in the Godspeed directory if ( self.godspeed_dir in md_file.parents or md_file.parent == self.godspeed_dir ): continue # Skip hidden files and directories if any(part.startswith(".") for part in md_file.parts): continue markdown_files.append(md_file) return sorted(markdown_files) def sweep_tasks(self) -> dict: """Sweep incomplete tasks from all markdown files into Inbox.""" print(f"๐Ÿงน Sweeping incomplete tasks from: {self.notes_dir}") print(f"๐Ÿ“ฅ Target Inbox: {self.inbox_file}") print(f"๐Ÿ” Dry run: {self.dry_run}") print("=" * 60) markdown_files = self.find_markdown_files() print(f"\n๐Ÿ“ Found {len(markdown_files)} markdown files to process") swept_tasks = [] processed_files = [] empty_files_deleted = [] for file_path in markdown_files: rel_path = file_path.relative_to(self.notes_dir) print(f"\n๐Ÿ“„ Processing: {rel_path}") tasks, non_task_lines = self._parse_markdown_file(file_path) if not tasks: print(f" โ„น๏ธ No tasks found") continue # Separate incomplete tasks from completed/cleared ones incomplete_tasks = [] complete_tasks = [] for task in tasks: local_id, status, title, notes = task if status == "incomplete": incomplete_tasks.append(task) else: complete_tasks.append(task) if incomplete_tasks: print(f" ๐Ÿ”„ Found {len(incomplete_tasks)} incomplete tasks:") for _, status, title, notes in incomplete_tasks: print(f" โ€ข {title}") if notes: print(f" Notes: {notes}") # Add source file annotation source_annotation = f"" annotated_tasks = [] for local_id, status, title, notes in incomplete_tasks: # Add source info to notes source_notes = f"From: {rel_path}" if notes: combined_notes = f"{notes}\n{source_notes}" else: combined_notes = source_notes annotated_tasks.append((local_id, status, title, combined_notes)) swept_tasks.extend(annotated_tasks) processed_files.append(str(rel_path)) if complete_tasks: print( f" โœ… Keeping {len(complete_tasks)} completed/cleared tasks in place" ) # Reconstruct remaining content (non-tasks + completed tasks) remaining_content = non_task_lines.copy() # Add completed/cleared tasks back to remaining content if complete_tasks: remaining_content.append("") # Empty line before tasks for task in complete_tasks: if self.sync_engine: formatted = self.sync_engine._format_task_line(*task) else: local_id, status, title, notes = task checkbox = { "incomplete": "[ ]", "complete": "[x]", "cleared": "[-]", }[status] formatted = f"- {checkbox} {title} " if notes: formatted += f"\n {notes}" remaining_content.append(formatted) # Clean the original file if incomplete_tasks: self._clean_file(file_path, remaining_content) # Write swept tasks to Inbox if swept_tasks: print(f"\n๐Ÿ“ฅ Writing {len(swept_tasks)} tasks to Inbox...") if not self.dry_run: self._write_tasks_to_file(self.inbox_file, swept_tasks) print(f" โœ… Inbox updated: {self.inbox_file}") # Summary print(f"\n" + "=" * 60) print(f"๐Ÿ“Š SWEEP SUMMARY:") print(f" โ€ข Files processed: {len(processed_files)}") print(f" โ€ข Tasks swept: {len(swept_tasks)}") print(f" โ€ข Target: {self.inbox_file}") if self.dry_run: print(f"\nโš ๏ธ DRY RUN - No files were actually modified") print(f" Run without --dry-run to perform the sweep") return { "swept_tasks": len(swept_tasks), "processed_files": processed_files, "inbox_file": str(self.inbox_file), } def main(): parser = argparse.ArgumentParser( description="Sweep incomplete tasks from markdown files into Godspeed Inbox", epilog=""" Examples: python sweep_tasks.py ~/Documents/Notes ~/Documents/Godspeed python sweep_tasks.py . ./godspeed --dry-run python sweep_tasks.py ~/Notes ~/Notes/godspeed --dry-run """, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "notes_dir", type=Path, help="Root directory containing markdown files with tasks (e.g., ~/Documents/Notes)", ) parser.add_argument( "godspeed_dir", type=Path, help="Godspeed sync directory where Inbox.md will be created (e.g., ~/Documents/Godspeed)", ) parser.add_argument( "--dry-run", action="store_true", help="Show what would be done without making changes", ) args = parser.parse_args() # Validate directories if not args.notes_dir.exists(): print(f"โŒ Notes directory does not exist: {args.notes_dir}") return 1 if not args.notes_dir.is_dir(): print(f"โŒ Notes path is not a directory: {args.notes_dir}") return 1 # Godspeed directory will be created if it doesn't exist try: sweeper = TaskSweeper(args.notes_dir, args.godspeed_dir, args.dry_run) result = sweeper.sweep_tasks() if result["swept_tasks"] > 0: print(f"\n๐ŸŽ‰ Successfully swept {result['swept_tasks']} tasks!") if not args.dry_run: print(f"๐Ÿ’ก Next steps:") print(f" 1. Review tasks in: {result['inbox_file']}") print(f" 2. Run 'godspeed upload' to sync to API") print(f" 3. Organize tasks into appropriate lists in Godspeed app") else: print(f"\nโœจ No incomplete tasks found to sweep.") return 0 except Exception as e: print(f"โŒ Error during sweep: {e}") import traceback traceback.print_exc() return 1 if __name__ == "__main__": exit(main())