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 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-01-15 17:58:36 -08:00
commit 63a37bae66
2 changed files with 137 additions and 0 deletions

View file

@ -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:

127
mise-tasks/blumeops-tasks Executable file
View file

@ -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())