diff --git a/CLAUDE.md b/CLAUDE.md index 9639c5b..44e655e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,16 @@ You are encouraged to explore the zk, follow links, and propose updates to it as 4. Use `brew services` or Launch Agents to control services on macos hosts. 5. Test all changes before applying them - ie with ansible, use a --check --diff run. +## Task Discovery + +To discover pending blumeops tasks, run: + +```bash +mise run blumeops-tasks +``` + +This fetches tasks from the "Blumeops" project in Todoist (via 1Password for API credentials) and displays them sorted by priority: p1 (urgent), p2 (high), p4 (normal/default), p3 (backlog). The typical workflow is to pick a task from this list at the start of a session, then dive in with planning. + ## Remote Hosts This repo is typically edited from a workstation (e.g., gilbert), but services run on remote hosts in the tailnet. Use SSH to explore or check state on remote machines: diff --git a/mise-tasks/blumeops-tasks b/mise-tasks/blumeops-tasks new file mode 100755 index 0000000..39f363f --- /dev/null +++ b/mise-tasks/blumeops-tasks @@ -0,0 +1,127 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["httpx>=0.27.0", "rich>=13.0.0"] +# /// +#MISE description="List Blumeops tasks from Todoist sorted by priority" +"""Fetch and display Blumeops tasks from Todoist, sorted by priority.""" + +import subprocess +import sys + +import httpx +from rich.console import Console +from rich.text import Text + +TODOIST_API_BASE = "https://api.todoist.com/rest/v2" +PROJECT_NAME = "Blumeops" + +# Priority mapping: Todoist API uses 1=normal(p4), 2=moderate(p3), 3=high(p2), 4=urgent(p1) +# User wants order: p1, p2, p4, p3 (p3 is backlog, goes last) +PRIORITY_LABELS = {4: "p1", 3: "p2", 1: "p4", 2: "p3"} +PRIORITY_SORT_ORDER = {4: 1, 3: 2, 1: 3, 2: 4} # Lower = earlier + + +def get_todoist_token() -> str: + """Retrieve Todoist API token from 1Password.""" + result = subprocess.run( + [ + "op", + "--vault", + "vg6xf6vvfmoh5hqjjhlhbeoaie", + "item", + "get", + "c53h3xnmswhvexa5mntoyvhgpm", + "--fields", + "credential", + "--reveal", + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to get Todoist token from 1Password: {result.stderr}") + return result.stdout.strip() + + +def get_project_id(client: httpx.Client, project_name: str) -> str: + """Find project ID by name.""" + response = client.get(f"{TODOIST_API_BASE}/projects") + response.raise_for_status() + projects = response.json() + + for project in projects: + if project["name"] == project_name: + return project["id"] + + raise RuntimeError(f"Project '{project_name}' not found in Todoist") + + +def get_tasks(client: httpx.Client, project_id: str) -> list[dict]: + """Get all tasks for a project.""" + response = client.get(f"{TODOIST_API_BASE}/tasks", params={"project_id": project_id}) + response.raise_for_status() + return response.json() + + +def sort_tasks(tasks: list[dict]) -> list[dict]: + """Sort tasks by custom priority order: p1, p2, p4, p3.""" + return sorted(tasks, key=lambda t: PRIORITY_SORT_ORDER.get(t["priority"], 5)) + + +def main() -> int: + console = Console() + + # Get API token + try: + token = get_todoist_token() + except RuntimeError as e: + console.print(f"[red]Error:[/red] {e}") + return 1 + + # Create HTTP client with auth header + with httpx.Client(headers={"Authorization": f"Bearer {token}"}) as client: + # Find project + try: + project_id = get_project_id(client, PROJECT_NAME) + except RuntimeError as e: + console.print(f"[red]Error:[/red] {e}") + return 1 + + # Get and sort tasks + tasks = get_tasks(client, project_id) + sorted_tasks = sort_tasks(tasks) + + if not sorted_tasks: + console.print("No tasks found in Blumeops project") + return 0 + + # Display tasks + console.print(f"[bold]Blumeops Tasks[/bold] ({len(sorted_tasks)} tasks)") + console.print("=" * 40) + console.print() + + for task in sorted_tasks: + priority = task["priority"] + label = PRIORITY_LABELS.get(priority, "p?") + content = task["content"] + description = task.get("description", "") + + # Header line with priority and content + header = Text() + header.append(f"[{label}]", style="bold") + header.append(f" {content}") + console.print(header) + + # Description indented + if description: + for line in description.split("\n"): + console.print(f" {line}", style="dim") + + console.print() + + return 0 + + +if __name__ == "__main__": + sys.exit(main())