garc-gws-agent-runtime/scripts/garc-tasks-helper.py
林 駿甫 (Shunsuke Hayashi) a69b9d9160 feat: initial release — GARC v0.1.0
Permission-first AI agent runtime for Google Workspace.
Ports the LARC/OpenClaw governance model (disclosure chain,
execution gates, queue/ingress) to Gmail, Calendar, Drive,
Sheets, Tasks, and People APIs with Claude Code as the
execution engine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 08:59:12 +09:00

317 lines
11 KiB
Python
Executable file

#!/usr/bin/env python3
"""
GARC Tasks Helper — Google Tasks operations
Supports multiple task lists, create/read/update/complete/delete
"""
import argparse
import json
import sys
from pathlib import Path
from datetime import datetime, timezone
sys.path.insert(0, str(Path(__file__).parent))
from garc_core import build_service, utc_now, with_retry
def get_service():
scopes = ["https://www.googleapis.com/auth/tasks"]
return build_service("tasks", "v1", scopes=scopes)
# ─────────────────────────────────────────────
# Task Lists
# ─────────────────────────────────────────────
@with_retry()
def list_tasklists():
"""List all task lists."""
service = get_service()
result = service.tasklists().list(maxResults=100).execute()
lists = result.get("items", [])
if not lists:
print("No task lists found.")
return
print(f"Task Lists ({len(lists)}):")
for tl in lists:
print(f" [{tl['id']}] {tl['title']}")
def _resolve_tasklist(service, tasklist_ref: str) -> str:
"""Resolve a task list name or partial ID to a full ID."""
if tasklist_ref == "@default":
return "@default"
result = service.tasklists().list(maxResults=100).execute()
for tl in result.get("items", []):
if tl["id"] == tasklist_ref or tl["title"].lower() == tasklist_ref.lower():
return tl["id"]
if tl["id"].startswith(tasklist_ref):
return tl["id"]
return tasklist_ref # fallback: use as-is
# ─────────────────────────────────────────────
# Task CRUD
# ─────────────────────────────────────────────
@with_retry()
def list_tasks(tasklist: str = "@default", show_completed: bool = False, format_: str = "table"):
"""List tasks in a task list."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
result = service.tasks().list(
tasklist=tasklist,
showCompleted=show_completed,
showHidden=show_completed,
maxResults=100,
).execute()
tasks = result.get("items", [])
if not tasks:
print("No tasks found.")
return
if format_ == "json":
output = []
for t in tasks:
output.append({
"id": t["id"],
"title": t.get("title", ""),
"status": t.get("status", ""),
"due": t.get("due", "")[:10] if t.get("due") else "",
"notes": t.get("notes", ""),
"updated": t.get("updated", "")[:10] if t.get("updated") else "",
})
print(json.dumps(output, ensure_ascii=False, indent=2))
return
print(f"Tasks ({len(tasks)}):")
for t in tasks:
status_icon = "" if t.get("status") == "completed" else ""
due = t.get("due", "")[:10] if t.get("due") else ""
due_str = f" [due: {due}]" if due else ""
short_id = t["id"][:12]
print(f" {status_icon} [{short_id}] {t.get('title', '')}{due_str}")
if t.get("notes"):
for line in t["notes"].splitlines():
print(f" {line}")
@with_retry()
def show_task(task_id: str, tasklist: str = "@default"):
"""Show full details of a single task."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
# Find full ID via list if partial
full_id = _find_task_id(service, task_id, tasklist)
if not full_id:
print(f"Task not found: {task_id}", file=sys.stderr)
sys.exit(1)
task = service.tasks().get(tasklist=tasklist, task=full_id).execute()
print(f"ID: {task['id']}")
print(f"Title: {task.get('title', '')}")
print(f"Status: {task.get('status', '')}")
if task.get("due"):
print(f"Due: {task['due'][:10]}")
if task.get("notes"):
print(f"Notes: {task['notes']}")
print(f"Updated: {task.get('updated', '')[:19]}")
@with_retry()
def create_task(
title: str,
tasklist: str = "@default",
due: str = None,
notes: str = None,
parent: str = None,
):
"""Create a new Google Task."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
body: dict = {"title": title, "status": "needsAction"}
if due:
# Normalize due date to RFC3339
if "T" not in due:
due = f"{due}T00:00:00.000Z"
body["due"] = due
if notes:
body["notes"] = notes
kwargs: dict = {"tasklist": tasklist, "body": body}
if parent:
kwargs["parent"] = parent
result = service.tasks().insert(**kwargs).execute()
print(f"✅ Task created: [{result['id'][:12]}] {title}")
if due:
print(f" Due: {due[:10]}")
@with_retry()
def update_task(
task_id: str,
tasklist: str = "@default",
title: str = None,
due: str = None,
notes: str = None,
):
"""Update an existing task."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
full_id = _find_task_id(service, task_id, tasklist)
if not full_id:
print(f"Task not found: {task_id}", file=sys.stderr)
sys.exit(1)
# Fetch current
task = service.tasks().get(tasklist=tasklist, task=full_id).execute()
if title:
task["title"] = title
if due:
if "T" not in due:
due = f"{due}T00:00:00.000Z"
task["due"] = due
if notes is not None:
task["notes"] = notes
result = service.tasks().update(tasklist=tasklist, task=full_id, body=task).execute()
print(f"✅ Task updated: [{result['id'][:12]}] {result.get('title', '')}")
@with_retry()
def complete_task(task_id: str, tasklist: str = "@default"):
"""Mark a task as completed."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
full_id = _find_task_id(service, task_id, tasklist)
if not full_id:
print(f"Task not found: {task_id}", file=sys.stderr)
sys.exit(1)
service.tasks().patch(
tasklist=tasklist,
task=full_id,
body={"status": "completed"},
).execute()
print(f"✅ Task {task_id[:12]} marked as completed")
@with_retry()
def delete_task(task_id: str, tasklist: str = "@default"):
"""Delete a task."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
full_id = _find_task_id(service, task_id, tasklist)
if not full_id:
print(f"Task not found: {task_id}", file=sys.stderr)
sys.exit(1)
service.tasks().delete(tasklist=tasklist, task=full_id).execute()
print(f"🗑️ Task {task_id[:12]} deleted")
@with_retry()
def clear_completed(tasklist: str = "@default"):
"""Clear all completed tasks from a task list."""
service = get_service()
tasklist = _resolve_tasklist(service, tasklist)
service.tasks().clear(tasklist=tasklist).execute()
print("✅ Cleared all completed tasks")
# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────
def _find_task_id(service, task_id_or_partial: str, tasklist: str) -> str | None:
"""Find full task ID from a partial ID or exact match."""
result = service.tasks().list(
tasklist=tasklist, showCompleted=True, showHidden=True, maxResults=200
).execute()
for t in result.get("items", []):
if t["id"] == task_id_or_partial:
return t["id"]
if t["id"].startswith(task_id_or_partial):
return t["id"]
return None
# ─────────────────────────────────────────────
# CLI
# ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="GARC Tasks Helper — Google Tasks operations")
parser.add_argument("--tasklist", "-l", default="@default", help="Task list ID or name (default: @default)")
parser.add_argument("--format", "-f", dest="format_", default="table", choices=["table", "json"])
subparsers = parser.add_subparsers(dest="command", required=True)
# list-tasklists
subparsers.add_parser("list-tasklists", help="Show all task lists")
# list
lp = subparsers.add_parser("list", help="List tasks")
lp.add_argument("--completed", action="store_true", help="Include completed tasks")
# show
sp = subparsers.add_parser("show", help="Show a single task")
sp.add_argument("--task-id", required=True)
# create
cp = subparsers.add_parser("create", help="Create a task")
cp.add_argument("--title", required=True)
cp.add_argument("--due", help="Due date (YYYY-MM-DD)")
cp.add_argument("--notes", help="Task notes")
cp.add_argument("--parent", help="Parent task ID (for subtasks)")
# update
up = subparsers.add_parser("update", help="Update a task")
up.add_argument("--task-id", required=True)
up.add_argument("--title")
up.add_argument("--due")
up.add_argument("--notes")
# complete
comp = subparsers.add_parser("complete", help="Mark task as completed")
comp.add_argument("--task-id", required=True)
# delete
dp = subparsers.add_parser("delete", help="Delete a task")
dp.add_argument("--task-id", required=True)
# clear-completed
subparsers.add_parser("clear-completed", help="Remove all completed tasks")
args = parser.parse_args()
try:
if args.command == "list-tasklists":
list_tasklists()
elif args.command == "list":
list_tasks(args.tasklist, args.completed, args.format_)
elif args.command == "show":
show_task(args.task_id, args.tasklist)
elif args.command == "create":
create_task(args.title, args.tasklist, args.due, args.notes, args.parent)
elif args.command == "update":
update_task(args.task_id, args.tasklist, args.title, args.due, args.notes)
elif args.command == "complete":
complete_task(args.task_id, args.tasklist)
elif args.command == "delete":
delete_task(args.task_id, args.tasklist)
elif args.command == "clear-completed":
clear_completed(args.tasklist)
except KeyboardInterrupt:
sys.exit(0)
if __name__ == "__main__":
main()