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) <noreply@anthropic.com>
clickup-runner
PeninsulaInd 2026-04-03 09:40:31 -05:00
parent 935ab2d772
commit e4c7bc8e02
2 changed files with 263 additions and 2 deletions

View File

@ -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()

View File

@ -66,10 +66,10 @@
- **Website:** - **Website:**
- **GBP:** - **GBP:**
## RPM Mechanical Inc. ## RPM Industrial Rubber Parts
- **Executive:** Mike McNeil, Vice President - **Executive:** Mike McNeil, Vice President
- **PA Org ID:** 19395 - **PA Org ID:** 19395
- **Website:** - **Website:** https://www.rpmrubberparts.com/
- **GBP:** - **GBP:**
## Green Bay Plastics ## Green Bay Plastics
@ -132,6 +132,12 @@
- **Website:** - **Website:**
- **GBP:** - **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 ## EVR Products
- **Executive:** Gary Waldick, Vice President of EVR Products - **Executive:** Gary Waldick, Vice President of EVR Products
- **Website:** - **Website:**