From 8fe7568bbe895f62f4abd97c545b813f3f016277 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 15 Jan 2026 17:58:36 -0800 Subject: [PATCH] Add blumeops-tasks mise task for Todoist integration Adds a uv run script that fetches tasks from the Blumeops project in Todoist and displays them sorted by priority (p1, p2, p4, p3 with backlog last). Uses 1Password CLI for API credential retrieval. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 10 +++ mise-tasks/blumeops-tasks | 141 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100755 mise-tasks/blumeops-tasks 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..603bbe7 --- /dev/null +++ b/mise-tasks/blumeops-tasks @@ -0,0 +1,141 @@ +#!/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. + +This script is specific to Erich Blume's personal development workflow and +is not intended for general use. It requires: + + - A 1Password CLI (`op`) configured with access to the author's vault + - A Todoist account with a project named "Blumeops" + +The script fetches tasks and displays them sorted by a custom priority order: +p1 (urgent), p2 (high), p4 (normal/default), p3 (backlog). The p3-last ordering +reflects a deliberate choice to treat p3 as "backlog" rather than moderate +priority. + +Usage: mise run blumeops-tasks +""" + +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())