"""CLI script to create a ClickUp task in a client's Overall list. Usage: uv run python scripts/create_clickup_task.py --name "Task" --client "Client" uv run python scripts/create_clickup_task.py --name "LB" --client "Acme" \\ --category "Link Building" --due-date 2026-03-11 --tag feb26 \\ --field "Keyword=some keyword" --field "CLIFlags=--tier1-count 5" """ from __future__ import annotations import argparse import json import os import sys from datetime import UTC, datetime from pathlib import Path # Add project root to path so we can import cheddahbot 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 def _date_to_unix_ms(date_str: str) -> int: """Convert YYYY-MM-DD to Unix milliseconds (noon UTC). Noon UTC ensures the date displays correctly in US timezones. """ dt = datetime.strptime(date_str, "%Y-%m-%d").replace( hour=12, tzinfo=UTC ) return int(dt.timestamp() * 1000) def _parse_time_estimate(s: str) -> int: """Parse a human time string like '2h', '30m', '1h30m' to ms.""" import re total_min = 0 match = re.match(r"(?:(\d+)h)?(?:(\d+)m)?$", s.strip()) if not match or not any(match.groups()): raise ValueError(f"Invalid time estimate: '{s}' (use e.g. '2h', '30m', '1h30m')") if match.group(1): total_min += int(match.group(1)) * 60 if match.group(2): total_min += int(match.group(2)) return total_min * 60 * 1000 def main(): load_dotenv() parser = argparse.ArgumentParser(description="Create a ClickUp task") parser.add_argument("--name", required=True, help="Task name") parser.add_argument( "--client", required=True, help="Client folder name" ) parser.add_argument( "--category", default="", help="Work Category dropdown value" ) parser.add_argument("--description", default="", help="Task description") parser.add_argument( "--status", default="to do", help="Initial status (default: 'to do')" ) parser.add_argument( "--due-date", default="", help="Due date as YYYY-MM-DD" ) parser.add_argument( "--tag", action="append", default=[], help="Tag (mmmYY, repeatable)" ) parser.add_argument( "--field", action="append", default=[], help="Custom field as Name=Value (repeatable)", ) parser.add_argument( "--priority", type=int, default=2, help="Priority: 1=Urgent, 2=High, 3=Normal, 4=Low (default: 2)", ) parser.add_argument( "--assignee", type=int, action="append", default=[], help="ClickUp user ID (default: Bryan 10765627)", ) parser.add_argument( "--time-estimate", default="", help="Time estimate (e.g. '2h', '30m', '1h30m')", ) args = parser.parse_args() 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) # Parse custom fields custom_fields: dict[str, str] = {} for f in args.field: if "=" not in f: print(f"Error: --field must be Name=Value, got: {f}", file=sys.stderr) sys.exit(1) name, value = f.split("=", 1) custom_fields[name] = value client = ClickUpClient(api_token=api_token) try: # Find the client's Overall list list_id = client.find_list_in_folder(space_id, args.client) if not list_id: msg = f"Error: No folder '{args.client}' with 'Overall' list" print(msg, file=sys.stderr) sys.exit(1) # Build create_task kwargs create_kwargs: dict = { "list_id": list_id, "name": args.name, "description": args.description, "status": args.status, } if args.due_date: create_kwargs["due_date"] = _date_to_unix_ms(args.due_date) if args.tag: create_kwargs["tags"] = args.tag create_kwargs["priority"] = args.priority create_kwargs["assignees"] = args.assignee or [DEFAULT_ASSIGNEE] if args.time_estimate: create_kwargs["time_estimate"] = _parse_time_estimate( args.time_estimate ) # Create the task result = client.create_task(**create_kwargs) task_id = result.get("id", "") # Set Client dropdown field client.set_custom_field_smart(task_id, list_id, "Client", args.client) # Set Work Category if provided if args.category: client.set_custom_field_smart( task_id, list_id, "Work Category", args.category ) # Set any additional custom fields for field_name, field_value in custom_fields.items(): ok = client.set_custom_field_smart( task_id, list_id, field_name, field_value ) if not ok: print( f"Warning: Failed to set '{field_name}'", file=sys.stderr, ) print(json.dumps({ "id": task_id, "name": args.name, "client": args.client, "url": result.get("url", ""), "status": args.status, }, indent=2)) finally: client.close() if __name__ == "__main__": main()