From e4c7bc8e02ed73a3352a6c44813b59485ff10da6 Mon Sep 17 00:00:00 2001 From: PeninsulaInd Date: Fri, 3 Apr 2026 09:40:31 -0500 Subject: [PATCH] Add create_task_set CLI script + update company directory New script replaces inline task creation commands with a proper CLI. Supports all task types (NEW/OPT/LINKS/PR) with validation, dependencies, per-type tag/date overrides, and custom fields. Updated companies.md: renamed RPM Mechanical to RPM Industrial Rubber Parts, added Royal Purple Industrial entry. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/create_task_set.py | 255 +++++++++++++++++++++++++++++++++++++ skills/companies.md | 10 +- 2 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 scripts/create_task_set.py diff --git a/scripts/create_task_set.py b/scripts/create_task_set.py new file mode 100644 index 0000000..b9873d8 --- /dev/null +++ b/scripts/create_task_set.py @@ -0,0 +1,255 @@ +"""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() diff --git a/skills/companies.md b/skills/companies.md index 77308ab..34d3bbb 100644 --- a/skills/companies.md +++ b/skills/companies.md @@ -66,10 +66,10 @@ - **Website:** - **GBP:** -## RPM Mechanical Inc. +## RPM Industrial Rubber Parts - **Executive:** Mike McNeil, Vice President - **PA Org ID:** 19395 -- **Website:** +- **Website:** https://www.rpmrubberparts.com/ - **GBP:** ## Green Bay Plastics @@ -132,6 +132,12 @@ - **Website:** - **GBP:** +## Royal Purple Industrial +- **Executive:** Rusty Waples, Brand and Product Marketing Director +- **PA Org ID:** 23623 +- **Website:** https://www.royalpurpleind.com/ +- **GBP:** https://maps.app.goo.gl/wBgq49g3Xs4Y91zP9 + ## EVR Products - **Executive:** Gary Waldick, Vice President of EVR Products - **Website:**