"""CLI script to create a bundled set of ClickUp tasks (LINKS, PR, NEW, OPT). Usage: uv run python scripts/create_task_set.py \ --client "RPM Rubber" --keyword "rubber gaskets" \ --types OPT,LINKS,PR --url "https://example.com/page" \ --tag apr26 --due-date 2026-04-08 \ --pr-topic "discuss RPM rubber gasket capabilities" uv run python scripts/create_task_set.py \ --client "Metal Craft Spinning" --keyword "fan panels" \ --types NEW,LINKS --tag may26 --due-date 2026-05-11 uv run python scripts/create_task_set.py \ --client "Hogge Precision" --keyword "swiss machining" \ --types LINKS --url "https://example.com/page" \ --tag jun26 --due-date 2026-06-08 --articles 5 """ from __future__ import annotations import argparse import json import os import sys from datetime import UTC, datetime, timedelta from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from dotenv import load_dotenv from cheddahbot.clickup import ClickUpClient DEFAULT_ASSIGNEE = 10765627 # Bryan Bigari VALID_TYPES = {"NEW", "OPT", "LINKS", "PR"} # Time estimates in milliseconds TIME_ESTIMATES = { "NEW": 14400000, # 4h "OPT": 7200000, # 2h "LINKS": 9000000, # 2.5h "PR": 5400000, # 1.5h } WORK_CATEGORIES = { "NEW": "Content Creation", "OPT": "On Page Optimization", "LINKS": "Link Building", "PR": "Press Release", } def _date_to_unix_ms(date_str: str) -> int: """Convert YYYY-MM-DD to Unix milliseconds (noon UTC).""" dt = datetime.strptime(date_str, "%Y-%m-%d").replace(hour=12, tzinfo=UTC) return int(dt.timestamp() * 1000) def _add_days_ms(unix_ms: int, days: int) -> int: """Add days to a Unix ms timestamp.""" return unix_ms + days * 86400 * 1000 def main(): load_dotenv() parser = argparse.ArgumentParser( description="Create a bundled set of ClickUp tasks" ) parser.add_argument("--client", required=True, help="Client folder name") parser.add_argument("--keyword", required=True, help="SEO keyword") parser.add_argument( "--types", required=True, help="Comma-separated task types: NEW, OPT, LINKS, PR", ) parser.add_argument("--due-date", required=True, help="Content due date (YYYY-MM-DD)") parser.add_argument("--tag", required=True, help="Month tag (e.g. apr26)") parser.add_argument("--url", default="", help="IMSURL (required for OPT)") parser.add_argument("--pr-topic", default="", help="PR topic (required if PR in types)") parser.add_argument("--articles", type=int, default=0, help="Tier-1 article count for LINKS") parser.add_argument("--anchors", default="", help="Custom anchors for LINKS (comma-delimited)") parser.add_argument("--priority", type=int, default=2, help="Priority (default: 2/High)") parser.add_argument("--assignee", type=int, default=DEFAULT_ASSIGNEE, help="ClickUp user ID") parser.add_argument( "--links-tag", default="", help="Override tag for LINKS/PR (if different from content tag)", ) parser.add_argument( "--pr-tag", default="", help="Override tag for PR (if different from content tag)", ) parser.add_argument( "--links-date", default="", help="Override due date for LINKS (YYYY-MM-DD, default: content + 7 days)", ) parser.add_argument( "--pr-date", default="", help="Override due date for PR (YYYY-MM-DD, default: content + 7 days)", ) args = parser.parse_args() # Parse and validate types task_types = [t.strip().upper() for t in args.types.split(",")] for t in task_types: if t not in VALID_TYPES: print(f"Error: Invalid type '{t}'. Must be one of: {VALID_TYPES}", file=sys.stderr) sys.exit(1) has_content = "NEW" in task_types or "OPT" in task_types # Validate required fields if "OPT" in task_types and not args.url: print("Error: --url is required when OPT is in types", file=sys.stderr) sys.exit(1) if "PR" in task_types and not args.pr_topic: print("Error: --pr-topic is required when PR is in types", file=sys.stderr) sys.exit(1) if not args.url and "NEW" not in task_types: needs_url = [t for t in task_types if t in ("LINKS", "PR")] if needs_url: print( f"Error: --url is required for {needs_url} when NEW is not in types", file=sys.stderr, ) sys.exit(1) api_token = os.environ.get("CLICKUP_API_TOKEN", "") space_id = os.environ.get("CLICKUP_SPACE_ID", "") if not api_token: print("Error: CLICKUP_API_TOKEN not set", file=sys.stderr) sys.exit(1) if not space_id: print("Error: CLICKUP_SPACE_ID not set", file=sys.stderr) sys.exit(1) client = ClickUpClient(api_token=api_token) try: list_id = client.find_list_in_folder(space_id, args.client) if not list_id: print(f"Error: No folder '{args.client}' with 'Overall' list", file=sys.stderr) sys.exit(1) content_due_ms = _date_to_unix_ms(args.due_date) followup_due_ms = _add_days_ms(content_due_ms, 7) created = {} new_task_id = None # Determine task order: content tasks first (need ID for dependencies) ordered = [] for t in ["NEW", "OPT"]: if t in task_types: ordered.append(t) for t in ["LINKS", "PR"]: if t in task_types: ordered.append(t) for task_type in ordered: # Task name name = f"{task_type} - {args.keyword}" # Due date if task_type in ("NEW", "OPT"): due_ms = content_due_ms elif task_type == "LINKS" and args.links_date: due_ms = _date_to_unix_ms(args.links_date) elif task_type == "PR" and args.pr_date: due_ms = _date_to_unix_ms(args.pr_date) elif has_content: due_ms = followup_due_ms else: due_ms = content_due_ms # Tag if task_type == "LINKS" and args.links_tag: tag = args.links_tag elif task_type == "PR" and args.pr_tag: tag = args.pr_tag else: tag = args.tag result = client.create_task( list_id=list_id, name=name, status="to do", priority=args.priority, assignees=[args.assignee], due_date=due_ms, tags=[tag], time_estimate=TIME_ESTIMATES[task_type], ) task_id = result.get("id", "") task_url = result.get("url", "") # Common fields client.set_custom_field_smart(task_id, list_id, "Client", args.client) client.set_custom_field_smart( task_id, list_id, "Work Category", WORK_CATEGORIES[task_type] ) client.set_custom_field_smart(task_id, list_id, "Delegate to Claude", "true") # IMSURL if args.url and task_type != "NEW": client.set_custom_field_smart(task_id, list_id, "IMSURL", args.url) # Type-specific fields if task_type == "LINKS": client.set_custom_field_smart(task_id, list_id, "LB Method", "Cora Backlinks") client.set_custom_field_smart(task_id, list_id, "BrandedPlusRatio", "0.80") if args.articles: client.set_custom_field_smart( task_id, list_id, "CLIFlags", f"--tier-1 {args.articles}" ) if args.anchors: client.set_custom_field_smart( task_id, list_id, "CustomAnchors", args.anchors ) if task_type == "PR": client.set_custom_field_smart(task_id, list_id, "PR Topic", args.pr_topic) # Track NEW task ID for dependencies if task_type == "NEW": new_task_id = task_id # Set dependency if NEW exists and no URL if task_type in ("LINKS", "PR") and new_task_id and not args.url: ok = client.add_dependency(task_id, depends_on=new_task_id) dep_status = "blocked by NEW" if ok else "DEPENDENCY FAILED" else: dep_status = "" created[task_type] = { "id": task_id, "name": name, "url": task_url, "dep": dep_status, } status_parts = [f"{task_type} created: {task_id}"] if dep_status: status_parts.append(dep_status) print(" | ".join(status_parts)) # Summary print(json.dumps(created, indent=2)) finally: client.close() if __name__ == "__main__": main()