Add blumeops-tasks mise task for Todoist integration (#14)
## Summary - Add `mise run blumeops-tasks` to fetch and display tasks from Todoist - Uses uv run script with inline dependencies (httpx, rich) - Fetches API credential securely via 1Password CLI - Sorts tasks by custom priority order: p1, p2, p4, p3 (backlog last) - Documents the task discovery workflow in CLAUDE.md ## Test plan - [x] Verified `mise run blumeops-tasks` fetches and displays tasks correctly - [x] Confirmed priority sorting works as expected 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/14
This commit is contained in:
parent
ae1513e7e9
commit
72c2dd7096
2 changed files with 151 additions and 0 deletions
10
CLAUDE.md
10
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:
|
||||
|
|
|
|||
141
mise-tasks/blumeops-tasks
Executable file
141
mise-tasks/blumeops-tasks
Executable file
|
|
@ -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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue