221 lines
6.9 KiB
Python
221 lines
6.9 KiB
Python
"""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
|
|
from clickup_runner.skill_map import SKILL_MAP
|
|
|
|
DEFAULT_ASSIGNEE = 10765627 # Bryan Bigari
|
|
|
|
|
|
def _build_stage_comment(category: str) -> str:
|
|
"""Build a pipeline stages comment from SKILL_MAP for a task type."""
|
|
stages = SKILL_MAP.get(category)
|
|
if not stages:
|
|
return ""
|
|
lines = ["Pipeline stages for %s:" % category]
|
|
for i, (stage, route) in enumerate(stages.items(), 1):
|
|
handler = route.handler if route.handler != "claude" else "Claude"
|
|
lines.append(
|
|
" %d. %s (%s) -> %s" % (i, stage, handler, route.next_status)
|
|
)
|
|
lines.append('Set Stage to "%s" to start.' % list(stages.keys())[0])
|
|
return "\n".join(lines)
|
|
|
|
|
|
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')",
|
|
)
|
|
parser.add_argument(
|
|
"--dependency",
|
|
action="append",
|
|
default=[],
|
|
help="Task ID this task is blocked by (repeatable)",
|
|
)
|
|
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
|
|
)
|
|
|
|
# Add dependencies (blocked by)
|
|
for dep_id in args.dependency:
|
|
if not client.add_dependency(task_id, dep_id):
|
|
print(
|
|
f"Warning: Failed to add dependency on {dep_id}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
# 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,
|
|
)
|
|
|
|
# Post pipeline stages comment if task type has stages
|
|
if args.category:
|
|
stage_comment = _build_stage_comment(args.category)
|
|
if stage_comment:
|
|
client.add_comment(task_id, stage_comment)
|
|
|
|
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()
|