clickup-cli 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clickup_cli/__init__.py +3 -0
- clickup_cli/__main__.py +5 -0
- clickup_cli/cli.py +176 -0
- clickup_cli/client.py +115 -0
- clickup_cli/commands/__init__.py +71 -0
- clickup_cli/commands/comments.py +278 -0
- clickup_cli/commands/docs.py +441 -0
- clickup_cli/commands/folders.py +202 -0
- clickup_cli/commands/init.py +153 -0
- clickup_cli/commands/lists.py +258 -0
- clickup_cli/commands/spaces.py +137 -0
- clickup_cli/commands/tags.py +132 -0
- clickup_cli/commands/tasks.py +733 -0
- clickup_cli/commands/team.py +114 -0
- clickup_cli/config.py +200 -0
- clickup_cli/helpers.py +163 -0
- clickup_cli-1.2.0.dist-info/METADATA +204 -0
- clickup_cli-1.2.0.dist-info/RECORD +21 -0
- clickup_cli-1.2.0.dist-info/WHEEL +4 -0
- clickup_cli-1.2.0.dist-info/entry_points.txt +2 -0
- clickup_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Team command handlers — whoami, members."""
|
|
2
|
+
|
|
3
|
+
from ..config import WORKSPACE_ID
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_parser(subparsers, F):
|
|
7
|
+
"""Register all team subcommands on the given subparsers object."""
|
|
8
|
+
team_parser = subparsers.add_parser(
|
|
9
|
+
"team",
|
|
10
|
+
formatter_class=F,
|
|
11
|
+
help="Workspace and team member information",
|
|
12
|
+
description="""\
|
|
13
|
+
Inspect workspace and team details — authenticated user info and member list.
|
|
14
|
+
|
|
15
|
+
Subcommands:
|
|
16
|
+
whoami — show workspace info and authenticated user context
|
|
17
|
+
members — list all members of the workspace
|
|
18
|
+
|
|
19
|
+
All commands are read-only.""",
|
|
20
|
+
epilog="""\
|
|
21
|
+
examples:
|
|
22
|
+
clickup team whoami
|
|
23
|
+
clickup team members
|
|
24
|
+
clickup --pretty team whoami""",
|
|
25
|
+
)
|
|
26
|
+
team_sub = team_parser.add_subparsers(dest="command", required=True)
|
|
27
|
+
|
|
28
|
+
# team whoami
|
|
29
|
+
team_sub.add_parser(
|
|
30
|
+
"whoami",
|
|
31
|
+
formatter_class=F,
|
|
32
|
+
help="Show workspace info and member context",
|
|
33
|
+
description="""\
|
|
34
|
+
Show the workspace name, ID, and all members visible to the authenticated
|
|
35
|
+
user. Use this as a quick sanity check to confirm which workspace and
|
|
36
|
+
identity the CLI is operating under.""",
|
|
37
|
+
epilog="""\
|
|
38
|
+
returns:
|
|
39
|
+
{"workspace": {"id": ..., "name": ...}, "members": [...], "member_count": N}
|
|
40
|
+
|
|
41
|
+
examples:
|
|
42
|
+
clickup team whoami
|
|
43
|
+
clickup --pretty team whoami""",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# team members
|
|
47
|
+
team_sub.add_parser(
|
|
48
|
+
"members",
|
|
49
|
+
formatter_class=F,
|
|
50
|
+
help="List all workspace members",
|
|
51
|
+
description="""\
|
|
52
|
+
List all members of the workspace with their IDs, usernames, emails,
|
|
53
|
+
and roles. Use this to discover user IDs for task assignment or to
|
|
54
|
+
verify team membership.""",
|
|
55
|
+
epilog="""\
|
|
56
|
+
returns:
|
|
57
|
+
{"members": [...], "count": N}
|
|
58
|
+
|
|
59
|
+
Each member has: id, username, email, role, initials.
|
|
60
|
+
|
|
61
|
+
examples:
|
|
62
|
+
clickup team members
|
|
63
|
+
clickup --pretty team members""",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_workspace(client):
|
|
68
|
+
"""Fetch teams and find the configured workspace."""
|
|
69
|
+
resp = client.get_v2("/team")
|
|
70
|
+
teams = resp.get("teams", [])
|
|
71
|
+
for team in teams:
|
|
72
|
+
if str(team.get("id")) == WORKSPACE_ID:
|
|
73
|
+
return team
|
|
74
|
+
# Shouldn't happen, but return first team if workspace ID doesn't match
|
|
75
|
+
if teams:
|
|
76
|
+
return teams[0]
|
|
77
|
+
return resp
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _format_member(m):
|
|
81
|
+
"""Extract user fields from a member object."""
|
|
82
|
+
u = m.get("user", {})
|
|
83
|
+
return {
|
|
84
|
+
"id": u.get("id"),
|
|
85
|
+
"username": u.get("username"),
|
|
86
|
+
"email": u.get("email"),
|
|
87
|
+
"role": u.get("role_key"),
|
|
88
|
+
"initials": u.get("initials"),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def cmd_team_whoami(client, args):
|
|
93
|
+
"""Show authenticated user and workspace info."""
|
|
94
|
+
team = _get_workspace(client)
|
|
95
|
+
members = team.get("members", [])
|
|
96
|
+
return {
|
|
97
|
+
"workspace": {
|
|
98
|
+
"id": team.get("id"),
|
|
99
|
+
"name": team.get("name"),
|
|
100
|
+
"color": team.get("color"),
|
|
101
|
+
},
|
|
102
|
+
"members": [_format_member(m) for m in members],
|
|
103
|
+
"member_count": len(members),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_team_members(client, args):
|
|
108
|
+
"""List workspace members."""
|
|
109
|
+
team = _get_workspace(client)
|
|
110
|
+
members = team.get("members", [])
|
|
111
|
+
return {
|
|
112
|
+
"members": [_format_member(m) for m in members],
|
|
113
|
+
"count": len(members),
|
|
114
|
+
}
|
clickup_cli/config.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""ClickUp CLI configuration — lazy-loaded from JSON file or environment variables.
|
|
2
|
+
|
|
3
|
+
Config resolution order:
|
|
4
|
+
1. CLICKUP_CONFIG_PATH env var → exact file path
|
|
5
|
+
2. ~/.config/clickup-cli/config.json → XDG-ish default
|
|
6
|
+
3. clickup-config.json in current working directory → project-local override
|
|
7
|
+
4. Environment variables only (CLICKUP_API_TOKEN + CLICKUP_WORKSPACE_ID)
|
|
8
|
+
|
|
9
|
+
workspace_id is auto-detected from the API when missing (single-workspace accounts).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from .helpers import error
|
|
17
|
+
|
|
18
|
+
_config_cache = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _auto_detect_workspace(token):
|
|
22
|
+
"""Auto-detect workspace ID when only a token is available.
|
|
23
|
+
|
|
24
|
+
Returns workspace_id string. Exits with error if detection fails.
|
|
25
|
+
"""
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
print("Auto-detecting workspace...", file=sys.stderr)
|
|
29
|
+
headers = {"Authorization": token, "Content-Type": "application/json"}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
resp = requests.get(
|
|
33
|
+
"https://api.clickup.com/api/v2/team", headers=headers, timeout=15
|
|
34
|
+
)
|
|
35
|
+
except requests.ConnectionError:
|
|
36
|
+
error("Could not reach ClickUp API to auto-detect workspace")
|
|
37
|
+
|
|
38
|
+
if resp.status_code == 401:
|
|
39
|
+
error("Authentication failed — check your API token")
|
|
40
|
+
|
|
41
|
+
if not resp.ok:
|
|
42
|
+
error(f"API error {resp.status_code} while detecting workspace: {resp.text}")
|
|
43
|
+
|
|
44
|
+
teams = resp.json().get("teams", [])
|
|
45
|
+
|
|
46
|
+
if not teams:
|
|
47
|
+
error("No workspaces found for this token")
|
|
48
|
+
|
|
49
|
+
if len(teams) == 1:
|
|
50
|
+
workspace_id = str(teams[0]["id"])
|
|
51
|
+
print(
|
|
52
|
+
f"Found workspace: {teams[0]['name']} (ID: {workspace_id})",
|
|
53
|
+
file=sys.stderr,
|
|
54
|
+
)
|
|
55
|
+
return workspace_id
|
|
56
|
+
|
|
57
|
+
# Multiple workspaces — can't auto-select
|
|
58
|
+
lines = ["Multiple workspaces found — set workspace_id manually:"]
|
|
59
|
+
for t in teams:
|
|
60
|
+
lines.append(f" {t['name']}: {t['id']}")
|
|
61
|
+
lines.append("\nSet it in your config file or via: export CLICKUP_WORKSPACE_ID=<id>")
|
|
62
|
+
error("\n".join(lines))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _save_field_to_config(path, field, value):
|
|
66
|
+
"""Update a single field in an existing config file."""
|
|
67
|
+
try:
|
|
68
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
69
|
+
config = json.load(f)
|
|
70
|
+
config[field] = value
|
|
71
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
72
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
73
|
+
f.write("\n")
|
|
74
|
+
print(f"Saved {field} to {path}", file=sys.stderr)
|
|
75
|
+
except (OSError, json.JSONDecodeError):
|
|
76
|
+
pass # Non-critical — config works in memory even if save fails
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_config_path():
|
|
80
|
+
"""Find config file using the fallback chain. Returns path or None."""
|
|
81
|
+
# 1. Explicit env var
|
|
82
|
+
env_path = os.environ.get("CLICKUP_CONFIG_PATH")
|
|
83
|
+
if env_path:
|
|
84
|
+
if os.path.exists(env_path):
|
|
85
|
+
return env_path
|
|
86
|
+
error(f"CLICKUP_CONFIG_PATH points to a missing file: {env_path}")
|
|
87
|
+
|
|
88
|
+
# 2. XDG-ish default
|
|
89
|
+
xdg_path = os.path.expanduser("~/.config/clickup-cli/config.json")
|
|
90
|
+
if os.path.exists(xdg_path):
|
|
91
|
+
return xdg_path
|
|
92
|
+
|
|
93
|
+
# 3. Current working directory
|
|
94
|
+
cwd_path = os.path.join(os.getcwd(), "clickup-config.json")
|
|
95
|
+
if os.path.exists(cwd_path):
|
|
96
|
+
return cwd_path
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _load_from_file(path):
|
|
102
|
+
"""Load and validate config from a JSON file."""
|
|
103
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
104
|
+
try:
|
|
105
|
+
config = json.load(f)
|
|
106
|
+
except json.JSONDecodeError as e:
|
|
107
|
+
error(f"Invalid JSON in {path}: {e}")
|
|
108
|
+
|
|
109
|
+
# Allow env var to override token from file
|
|
110
|
+
env_token = os.environ.get("CLICKUP_API_TOKEN")
|
|
111
|
+
if env_token:
|
|
112
|
+
config["api_token"] = env_token
|
|
113
|
+
|
|
114
|
+
if not config.get("api_token"):
|
|
115
|
+
error(
|
|
116
|
+
f"Missing required field in {path}: api_token\n"
|
|
117
|
+
"See clickup-config.example.json for the expected schema."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Auto-detect workspace_id if missing
|
|
121
|
+
if not config.get("workspace_id"):
|
|
122
|
+
config["workspace_id"] = _auto_detect_workspace(config["api_token"])
|
|
123
|
+
_save_field_to_config(path, "workspace_id", config["workspace_id"])
|
|
124
|
+
|
|
125
|
+
return config
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _load_from_env():
|
|
129
|
+
"""Build minimal config from environment variables only."""
|
|
130
|
+
token = os.environ.get("CLICKUP_API_TOKEN")
|
|
131
|
+
if not token:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
workspace_id = os.environ.get("CLICKUP_WORKSPACE_ID")
|
|
135
|
+
if not workspace_id:
|
|
136
|
+
workspace_id = _auto_detect_workspace(token)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"api_token": token,
|
|
140
|
+
"workspace_id": workspace_id,
|
|
141
|
+
"user_id": os.environ.get("CLICKUP_USER_ID", ""),
|
|
142
|
+
"spaces": {},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def load_config():
|
|
147
|
+
"""Load config from file or environment. Caches after first call."""
|
|
148
|
+
global _config_cache
|
|
149
|
+
if _config_cache is not None:
|
|
150
|
+
return _config_cache
|
|
151
|
+
|
|
152
|
+
path = _find_config_path()
|
|
153
|
+
|
|
154
|
+
if path:
|
|
155
|
+
_config_cache = _load_from_file(path)
|
|
156
|
+
return _config_cache
|
|
157
|
+
|
|
158
|
+
env_config = _load_from_env()
|
|
159
|
+
if env_config:
|
|
160
|
+
_config_cache = env_config
|
|
161
|
+
return _config_cache
|
|
162
|
+
|
|
163
|
+
error(
|
|
164
|
+
"No ClickUp configuration found.\n\n"
|
|
165
|
+
"Set up with one of:\n"
|
|
166
|
+
" clickup init # interactive setup\n"
|
|
167
|
+
" clickup init --token pk_YOUR_TOKEN # non-interactive setup\n\n"
|
|
168
|
+
"Or configure manually:\n"
|
|
169
|
+
" 1. Copy clickup-config.example.json to ~/.config/clickup-cli/config.json\n"
|
|
170
|
+
" 2. Fill in your API token and workspace ID\n\n"
|
|
171
|
+
"Or set environment variables:\n"
|
|
172
|
+
" export CLICKUP_API_TOKEN=pk_YOUR_TOKEN\n"
|
|
173
|
+
" export CLICKUP_WORKSPACE_ID=YOUR_WORKSPACE_ID"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _reset():
|
|
178
|
+
"""Reset cached config (for testing)."""
|
|
179
|
+
global _config_cache
|
|
180
|
+
_config_cache = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Lazy attribute access — config is loaded on first use, not at import time.
|
|
184
|
+
# This allows `clickup init` to run before any config file exists.
|
|
185
|
+
_ATTR_MAP = {
|
|
186
|
+
"WORKSPACE_ID": lambda c: c["workspace_id"],
|
|
187
|
+
"USER_ID": lambda c: c.get("user_id", ""),
|
|
188
|
+
"SPACES": lambda c: c.get("spaces", {}),
|
|
189
|
+
"DEFAULT_TAGS": lambda c: c.get("default_tags", []),
|
|
190
|
+
"DRAFT_TAG": lambda c: c.get("draft_tag", "draft"),
|
|
191
|
+
"GOOD_AS_IS_TAG": lambda c: c.get("good_as_is_tag", "good as is"),
|
|
192
|
+
"DEFAULT_PRIORITY": lambda c: c.get("default_priority", 4),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def __getattr__(name):
|
|
197
|
+
if name in _ATTR_MAP:
|
|
198
|
+
config = load_config()
|
|
199
|
+
return _ATTR_MAP[name](config)
|
|
200
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
clickup_cli/helpers.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Shared helper functions for output, errors, and content reading."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def add_id_argument(parser, name, help_text):
|
|
9
|
+
"""Add an argument that accepts both positional and --flag forms.
|
|
10
|
+
|
|
11
|
+
Agents naturally use flag forms (--task-id) while humans prefer positional.
|
|
12
|
+
This adds both so either style works.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
add_id_argument(parser, 'task_id', 'ClickUp task ID')
|
|
16
|
+
# Accepts: `command abc123` OR `command --task-id abc123`
|
|
17
|
+
"""
|
|
18
|
+
parser.add_argument(name, nargs="?", default=None, help=help_text)
|
|
19
|
+
flag = f"--{name.replace('_', '-')}"
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
flag, dest=f"_{name}_flag", default=None, help=argparse.SUPPRESS
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_id_args(args):
|
|
26
|
+
"""Resolve positional-or-flag ID arguments after parsing.
|
|
27
|
+
|
|
28
|
+
For each _<name>_flag attribute, merges with the positional <name>:
|
|
29
|
+
- If flag provided, use it
|
|
30
|
+
- If positional provided, use it
|
|
31
|
+
- If both provided, error
|
|
32
|
+
- If neither provided, error with helpful message
|
|
33
|
+
"""
|
|
34
|
+
flag_attrs = [a for a in vars(args) if a.endswith("_flag") and a.startswith("_")]
|
|
35
|
+
for flag_attr in flag_attrs:
|
|
36
|
+
base_attr = flag_attr[1:-5] # strip _ prefix and _flag suffix
|
|
37
|
+
flag_val = getattr(args, flag_attr)
|
|
38
|
+
pos_val = getattr(args, base_attr, None)
|
|
39
|
+
flag_name = f"--{base_attr.replace('_', '-')}"
|
|
40
|
+
|
|
41
|
+
if flag_val is not None and pos_val is not None:
|
|
42
|
+
error(f"Provide {base_attr} as positional or {flag_name}, not both")
|
|
43
|
+
|
|
44
|
+
if flag_val is not None:
|
|
45
|
+
setattr(args, base_attr, flag_val)
|
|
46
|
+
elif pos_val is None:
|
|
47
|
+
error(f"Missing required argument: {base_attr} (positional or {flag_name})")
|
|
48
|
+
|
|
49
|
+
delattr(args, flag_attr)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def output(data, pretty=False):
|
|
53
|
+
"""Print JSON to stdout."""
|
|
54
|
+
print(json.dumps(data, indent=2 if pretty else None, ensure_ascii=False))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def error(msg):
|
|
58
|
+
"""Print error to stderr and exit."""
|
|
59
|
+
print(f"Error: {msg}", file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def read_content(inline, file_path, flag_name="--content"):
|
|
64
|
+
"""Read content from inline string or file path. Returns string or None."""
|
|
65
|
+
if inline and file_path:
|
|
66
|
+
error(f"Cannot use both {flag_name} and {flag_name}-file")
|
|
67
|
+
if file_path:
|
|
68
|
+
try:
|
|
69
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
70
|
+
return f.read()
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
error(f"File not found: {file_path}")
|
|
73
|
+
return inline
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_space_id(space_arg):
|
|
77
|
+
"""Resolve a space name (from config) or raw space ID."""
|
|
78
|
+
from .config import SPACES
|
|
79
|
+
|
|
80
|
+
if space_arg in SPACES:
|
|
81
|
+
return SPACES[space_arg]["space_id"]
|
|
82
|
+
# If it's not numeric, it's likely a misspelled config name
|
|
83
|
+
if not space_arg.isdigit():
|
|
84
|
+
available = ", ".join(sorted(SPACES.keys())) if SPACES else "(none configured)"
|
|
85
|
+
error(f"Unknown space: {space_arg}. Available: {available}")
|
|
86
|
+
return space_arg
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fetch_all_comments(client, task_id):
|
|
90
|
+
"""Fetch all comments for a task, paginating through all pages."""
|
|
91
|
+
all_comments = []
|
|
92
|
+
params = None
|
|
93
|
+
while True:
|
|
94
|
+
resp = client.get_v2(f"/task/{task_id}/comment", params=params)
|
|
95
|
+
comments = resp.get("comments", [])
|
|
96
|
+
if not comments:
|
|
97
|
+
break
|
|
98
|
+
all_comments.extend(comments)
|
|
99
|
+
last = comments[-1]
|
|
100
|
+
params = {"start": str(last["date"]), "start_id": last["id"]}
|
|
101
|
+
return all_comments
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Default fields for compact task output
|
|
105
|
+
COMPACT_FIELDS = ["id", "name", "status", "priority", "url"]
|
|
106
|
+
|
|
107
|
+
# Reverse map for priority numbers to names
|
|
108
|
+
PRIORITY_NAMES = {1: "urgent", 2: "high", 3: "normal", 4: "low"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_status(task):
|
|
112
|
+
"""Extract the status string from a task's status field."""
|
|
113
|
+
status = task.get("status")
|
|
114
|
+
return status.get("status") if isinstance(status, dict) else status
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _extract_priority(task):
|
|
118
|
+
"""Extract the priority string from a task's priority field."""
|
|
119
|
+
priority = task.get("priority")
|
|
120
|
+
if isinstance(priority, dict) and priority:
|
|
121
|
+
return priority.get("priority") or PRIORITY_NAMES.get(
|
|
122
|
+
priority.get("orderindex"), "unknown"
|
|
123
|
+
)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def compact_task(task):
|
|
128
|
+
"""Return a compact view of a task with only essential fields."""
|
|
129
|
+
return {
|
|
130
|
+
"id": task.get("id"),
|
|
131
|
+
"name": task.get("name"),
|
|
132
|
+
"status": _extract_status(task),
|
|
133
|
+
"priority": _extract_priority(task),
|
|
134
|
+
"url": task.get("url"),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def filter_task_fields(task, fields):
|
|
139
|
+
"""Return only the requested fields from a task.
|
|
140
|
+
|
|
141
|
+
Supports nested status/priority extraction: if 'status' or 'priority'
|
|
142
|
+
is requested, returns the string name rather than the nested object.
|
|
143
|
+
"""
|
|
144
|
+
extractors = {"status": _extract_status, "priority": _extract_priority}
|
|
145
|
+
result = {}
|
|
146
|
+
for field in fields:
|
|
147
|
+
extractor = extractors.get(field)
|
|
148
|
+
result[field] = extractor(task) if extractor else task.get(field)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def format_tasks(tasks, full=False, fields=None):
|
|
153
|
+
"""Apply compact/fields/full formatting to a list of tasks.
|
|
154
|
+
|
|
155
|
+
- full=True: return raw API objects unchanged
|
|
156
|
+
- fields: return only those fields per task
|
|
157
|
+
- default: return compact view (id, name, status, priority, url)
|
|
158
|
+
"""
|
|
159
|
+
if full:
|
|
160
|
+
return tasks
|
|
161
|
+
if fields:
|
|
162
|
+
return [filter_task_fields(t, fields) for t in tasks]
|
|
163
|
+
return [compact_task(t) for t in tasks]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clickup-cli
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: The missing ClickUp CLI. Built for developers and AI agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/efetoker/clickup-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/efetoker/clickup-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/efetoker/clickup-cli/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/efetoker/clickup-cli/releases
|
|
9
|
+
Author-email: Efe Toker <efetoker@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agent,api,cli,clickup,task-management
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Scheduling
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: requests>=2.20
|
|
27
|
+
Requires-Dist: urllib3<2
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# clickup-cli
|
|
34
|
+
|
|
35
|
+
The missing ClickUp CLI. Built for developers and AI agents.
|
|
36
|
+
|
|
37
|
+
There's no official ClickUp CLI. If you're a developer who lives in the terminal, or an AI agent that needs structured data from ClickUp, this fills the gap. JSON stdout, errors to stderr, dry-run on every mutation.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install clickup-cli
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
clickup init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This prompts for your API token, discovers your workspaces and spaces, identifies your user, and writes a config file to `~/.config/clickup-cli/config.json`.
|
|
52
|
+
|
|
53
|
+
If you have a single workspace, it's selected automatically. Same for user detection in single-member workspaces.
|
|
54
|
+
|
|
55
|
+
Get your API token at [app.clickup.com/settings/apps](https://app.clickup.com/settings/apps).
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
clickup spaces list # see your spaces
|
|
61
|
+
clickup tasks list --space <name> # list tasks
|
|
62
|
+
clickup tasks search "login bug" # search across workspace
|
|
63
|
+
clickup tasks get abc123 # get task + comments
|
|
64
|
+
clickup --dry-run tasks create --space <name> --name "Fix auth" # preview
|
|
65
|
+
clickup tasks create --space <name> --name "Fix auth" # create for real
|
|
66
|
+
clickup comments add abc123 --text "Done" # add a comment
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## For AI Agents
|
|
70
|
+
|
|
71
|
+
This CLI is designed to be used by AI coding agents (Claude Code, Codex, etc.) as a tool for interacting with ClickUp.
|
|
72
|
+
|
|
73
|
+
**What makes it agent-friendly:**
|
|
74
|
+
- All output is JSON on stdout — easy to parse
|
|
75
|
+
- Errors go to stderr with non-zero exit code — easy to detect
|
|
76
|
+
- Every command has detailed `--help` — agents can self-discover usage
|
|
77
|
+
- `--dry-run` on all mutations — agents can preview before acting
|
|
78
|
+
- `--pretty` for readable output during debugging
|
|
79
|
+
- **Flag aliases for all positional args** — agents can use `--task-id`, `--query`, `--doc-id`, `--comment-id` etc. instead of positional arguments (both forms work)
|
|
80
|
+
- **Auto-infer `--space` from `--list`** — `tasks create --list 12345 --name "Fix bug"` works without `--space`
|
|
81
|
+
|
|
82
|
+
**Plug-and-play skill file:** Copy `.claude/skills/clickup-cli.md` from this repo into your project's `.claude/skills/` directory. It teaches Claude Code how to use the CLI: command discovery, safety patterns, common workflows.
|
|
83
|
+
|
|
84
|
+
**Minimal system prompt snippet:**
|
|
85
|
+
```
|
|
86
|
+
You have access to the `clickup` CLI for managing ClickUp tasks and docs.
|
|
87
|
+
Use `clickup <group> --help` to discover commands.
|
|
88
|
+
Always use `--dry-run` before mutating commands.
|
|
89
|
+
Output is JSON on stdout; errors go to stderr.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
| Group | Subcommands | Description |
|
|
95
|
+
|-------|-------------|-------------|
|
|
96
|
+
| `init` | — | Interactive workspace setup |
|
|
97
|
+
| `tasks` | list, get, create, update, search, delete, move, merge | Full task CRUD |
|
|
98
|
+
| `comments` | list, add, update, delete, thread, reply | Full comment CRUD with threading |
|
|
99
|
+
| `docs` | list, get, create, pages, get-page, edit-page, create-page | Docs and page management |
|
|
100
|
+
| `folders` | list, get, create, update, delete | Folder CRUD |
|
|
101
|
+
| `lists` | list, get, create, update, delete | List CRUD |
|
|
102
|
+
| `spaces` | list, get, statuses | Space inspection |
|
|
103
|
+
| `team` | whoami, members | Workspace and member info |
|
|
104
|
+
| `tags` | list, add, remove | Tag management |
|
|
105
|
+
|
|
106
|
+
Use `clickup <group> <command> --help` for detailed usage, examples, and return format.
|
|
107
|
+
|
|
108
|
+
## Global Flags
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
--pretty Pretty-print JSON output
|
|
112
|
+
--dry-run Preview mutations without executing
|
|
113
|
+
--debug Log API requests and responses to stderr
|
|
114
|
+
--version Show version
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Global flags can appear before or after the command group:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
clickup --pretty tasks list --space <name>
|
|
121
|
+
clickup tasks list --space <name> --pretty
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Key Behaviors
|
|
125
|
+
|
|
126
|
+
- **Flag aliases** — every positional argument also accepts a `--flag` form. `tasks get abc123` and `tasks get --task-id abc123` are equivalent. Same for `--query`, `--doc-id`, `--page-id`, `--folder-id`, `--list-id`, `--comment-id`, `--space`.
|
|
127
|
+
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided.
|
|
128
|
+
- **`tasks get`** auto-fetches comments and appends them to the output. Use `--no-comments` to skip.
|
|
129
|
+
- **`tasks search`** auto-detects task ID patterns like `PROJ-39` and applies prefix filtering.
|
|
130
|
+
- **`tasks create`** checks for duplicates before creating. Use `--skip-dedup` to bypass.
|
|
131
|
+
- **`docs edit-page --append`** reads the current page content, appends your new content, and sends one update.
|
|
132
|
+
- **Tag names** are auto-lowercased (ClickUp API stores them lowercase regardless of UI display).
|
|
133
|
+
- **Doc ID ≠ page ID.** Always use `docs pages <doc_id>` to discover page IDs before using `get-page` or `edit-page`.
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
### Config file
|
|
138
|
+
|
|
139
|
+
`clickup init` creates `~/.config/clickup-cli/config.json`:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"api_token": "pk_...",
|
|
144
|
+
"workspace_id": "12345",
|
|
145
|
+
"user_id": "67890",
|
|
146
|
+
"spaces": {
|
|
147
|
+
"myspace": {"space_id": "111", "list_id": "222"}
|
|
148
|
+
},
|
|
149
|
+
"default_tags": [],
|
|
150
|
+
"draft_tag": "draft",
|
|
151
|
+
"good_as_is_tag": "good as is",
|
|
152
|
+
"default_priority": 4
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Config resolution order
|
|
157
|
+
|
|
158
|
+
1. `CLICKUP_CONFIG_PATH` env var → exact file path
|
|
159
|
+
2. `~/.config/clickup-cli/config.json` → default location
|
|
160
|
+
3. `clickup-config.json` in current working directory → project-local override
|
|
161
|
+
|
|
162
|
+
### Environment variables
|
|
163
|
+
|
|
164
|
+
| Variable | Purpose |
|
|
165
|
+
|----------|---------|
|
|
166
|
+
| `CLICKUP_API_TOKEN` | API token (overrides config file token) |
|
|
167
|
+
| `CLICKUP_WORKSPACE_ID` | Workspace ID (auto-detected if you have one workspace) |
|
|
168
|
+
| `CLICKUP_USER_ID` | User ID for task assignment |
|
|
169
|
+
| `CLICKUP_CONFIG_PATH` | Custom config file path |
|
|
170
|
+
|
|
171
|
+
You can run without a config file by setting just `CLICKUP_API_TOKEN` — the workspace ID is auto-detected if your account has a single workspace. Set `CLICKUP_WORKSPACE_ID` explicitly for multi-workspace accounts.
|
|
172
|
+
|
|
173
|
+
## Coverage and Gaps
|
|
174
|
+
|
|
175
|
+
**Covered:** tasks, comments, docs/pages, folders, lists, spaces, tags, team/workspace info.
|
|
176
|
+
|
|
177
|
+
**Not yet covered:** checklists, time tracking, custom fields, task relationships, attachments, goals, webhooks, automations.
|
|
178
|
+
|
|
179
|
+
## Development Tools
|
|
180
|
+
|
|
181
|
+
This repo includes automations for contributors using [Claude Code](https://claude.com/claude-code):
|
|
182
|
+
|
|
183
|
+
- **Auto-lint hook** — ruff check + format runs on every Python file edit
|
|
184
|
+
- **Sensitive file guard** — blocks accidental edits to `.env`, `.key`, `.pem`, and credentials files
|
|
185
|
+
- **context7 MCP** — live ClickUp API docs available during development (via `.mcp.json`)
|
|
186
|
+
- **Skills** — `/release` workflow, `add-command` step-by-step guide, `clickup-cli` usage reference
|
|
187
|
+
- **Subagents** — `test-writer` generates pytest tests following project patterns
|
|
188
|
+
|
|
189
|
+
These are configured in `.claude/` and `.mcp.json`. Non-Claude-Code contributors can ignore them.
|
|
190
|
+
|
|
191
|
+
## Contributing
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git clone https://github.com/efetoker/clickup-cli.git
|
|
195
|
+
cd clickup-cli
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
pytest -v
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Issues and PRs welcome at [github.com/efetoker/clickup-cli](https://github.com/efetoker/clickup-cli).
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|