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,153 @@
|
|
|
1
|
+
"""Init command — interactive workspace setup."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cmd_init(args):
|
|
11
|
+
"""Set up ClickUp CLI configuration interactively or via --token."""
|
|
12
|
+
token = getattr(args, "token", None)
|
|
13
|
+
|
|
14
|
+
if not token:
|
|
15
|
+
print("Enter your ClickUp API token (starts with pk_):")
|
|
16
|
+
print(" Find it at: https://app.clickup.com/settings/apps")
|
|
17
|
+
try:
|
|
18
|
+
token = input("> ").strip()
|
|
19
|
+
except (EOFError, KeyboardInterrupt):
|
|
20
|
+
print("\nAborted.", file=sys.stderr)
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
if not token:
|
|
24
|
+
print("Error: API token is required.", file=sys.stderr)
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
# Fetch workspaces
|
|
28
|
+
print("Fetching workspaces...", 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
|
+
print("Error: Could not reach ClickUp API. Check your network.", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
if resp.status_code == 401:
|
|
40
|
+
print("Error: Invalid API token.", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
if not resp.ok:
|
|
44
|
+
print(f"Error: API returned {resp.status_code}: {resp.text}", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
teams = resp.json().get("teams", [])
|
|
48
|
+
if not teams:
|
|
49
|
+
print("Error: No workspaces found for this token.", file=sys.stderr)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
# Select workspace
|
|
53
|
+
if len(teams) == 1:
|
|
54
|
+
team = teams[0]
|
|
55
|
+
print(f"Found workspace: {team['name']} (ID: {team['id']})", file=sys.stderr)
|
|
56
|
+
else:
|
|
57
|
+
print(f"\nFound {len(teams)} workspaces:", file=sys.stderr)
|
|
58
|
+
for i, t in enumerate(teams, 1):
|
|
59
|
+
print(f" {i}. {t['name']} (ID: {t['id']})", file=sys.stderr)
|
|
60
|
+
try:
|
|
61
|
+
choice = input(f"Select workspace [1-{len(teams)}]: ").strip()
|
|
62
|
+
idx = int(choice) - 1
|
|
63
|
+
if idx < 0 or idx >= len(teams):
|
|
64
|
+
raise ValueError
|
|
65
|
+
team = teams[idx]
|
|
66
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
67
|
+
print("\nAborted.", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
workspace_id = str(team["id"])
|
|
71
|
+
|
|
72
|
+
# Find authenticated user
|
|
73
|
+
user_id = ""
|
|
74
|
+
members = team.get("members", [])
|
|
75
|
+
if len(members) == 1:
|
|
76
|
+
u = members[0].get("user", {})
|
|
77
|
+
user_id = str(u.get("id", ""))
|
|
78
|
+
print(f"Found user: {u.get('username', 'unknown')} (ID: {user_id})", file=sys.stderr)
|
|
79
|
+
elif members:
|
|
80
|
+
print(f"\nFound {len(members)} members:", file=sys.stderr)
|
|
81
|
+
for i, m in enumerate(members, 1):
|
|
82
|
+
u = m.get("user", {})
|
|
83
|
+
print(
|
|
84
|
+
f" {i}. {u.get('username', 'unknown')} — {u.get('email', '')} (ID: {u.get('id')})",
|
|
85
|
+
file=sys.stderr,
|
|
86
|
+
)
|
|
87
|
+
try:
|
|
88
|
+
choice = input(f"Which one is you? [1-{len(members)}] (Enter to skip): ").strip()
|
|
89
|
+
if choice:
|
|
90
|
+
idx = int(choice) - 1
|
|
91
|
+
if 0 <= idx < len(members):
|
|
92
|
+
u = members[idx].get("user", {})
|
|
93
|
+
user_id = str(u.get("id", ""))
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError
|
|
96
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
97
|
+
print("Skipped — set user_id in config later if needed.", file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
# Fetch spaces
|
|
100
|
+
print("Fetching spaces...", file=sys.stderr)
|
|
101
|
+
try:
|
|
102
|
+
resp = requests.get(
|
|
103
|
+
f"https://api.clickup.com/api/v2/team/{workspace_id}/space",
|
|
104
|
+
headers=headers,
|
|
105
|
+
timeout=15,
|
|
106
|
+
)
|
|
107
|
+
except requests.ConnectionError:
|
|
108
|
+
print("Error: Could not fetch spaces.", file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
spaces_raw = resp.json().get("spaces", [])
|
|
112
|
+
|
|
113
|
+
# Build spaces config
|
|
114
|
+
spaces = {}
|
|
115
|
+
for s in spaces_raw:
|
|
116
|
+
# Use lowercase name as key, sanitized for CLI use
|
|
117
|
+
key = s["name"].lower().replace(" ", "-")
|
|
118
|
+
lists_resp = requests.get(
|
|
119
|
+
f"https://api.clickup.com/api/v2/space/{s['id']}/list",
|
|
120
|
+
headers=headers,
|
|
121
|
+
timeout=15,
|
|
122
|
+
)
|
|
123
|
+
lists = lists_resp.json().get("lists", [])
|
|
124
|
+
default_list_id = lists[0]["id"] if lists else ""
|
|
125
|
+
spaces[key] = {"space_id": str(s["id"]), "list_id": default_list_id}
|
|
126
|
+
status = f" (default list: {default_list_id})" if default_list_id else " (no lists)"
|
|
127
|
+
print(f" {key}: {s['name']}{status}", file=sys.stderr)
|
|
128
|
+
|
|
129
|
+
config = {
|
|
130
|
+
"api_token": token,
|
|
131
|
+
"workspace_id": workspace_id,
|
|
132
|
+
"user_id": user_id,
|
|
133
|
+
"spaces": spaces,
|
|
134
|
+
"default_tags": [],
|
|
135
|
+
"draft_tag": "draft",
|
|
136
|
+
"good_as_is_tag": "good as is",
|
|
137
|
+
"default_priority": 4,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Write config
|
|
141
|
+
config_dir = os.path.expanduser("~/.config/clickup-cli")
|
|
142
|
+
config_path = os.path.join(config_dir, "config.json")
|
|
143
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
146
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
147
|
+
f.write("\n")
|
|
148
|
+
|
|
149
|
+
print(f"\nConfig saved to {config_path}", file=sys.stderr)
|
|
150
|
+
print(f"Workspace: {team['name']} ({len(spaces)} spaces)", file=sys.stderr)
|
|
151
|
+
print("\nTry it out:", file=sys.stderr)
|
|
152
|
+
print(" clickup spaces list", file=sys.stderr)
|
|
153
|
+
print(" clickup team whoami", file=sys.stderr)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""List command handlers — list, get, create, update, delete."""
|
|
2
|
+
|
|
3
|
+
from ..helpers import read_content, error, resolve_space_id, add_id_argument
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_parser(subparsers, F):
|
|
7
|
+
"""Register all lists subcommands on the given subparsers object."""
|
|
8
|
+
lists_parser = subparsers.add_parser(
|
|
9
|
+
"lists",
|
|
10
|
+
formatter_class=F,
|
|
11
|
+
help="Full list CRUD: list, get, create, update, delete",
|
|
12
|
+
description="""\
|
|
13
|
+
Manage ClickUp lists — the containers that hold tasks.
|
|
14
|
+
|
|
15
|
+
Lists can live directly in a space (folderless) or inside a folder.
|
|
16
|
+
Use --folder or --space to specify the parent depending on the context.
|
|
17
|
+
|
|
18
|
+
Subcommands:
|
|
19
|
+
list — list lists in a folder or folderless lists in a space
|
|
20
|
+
get — fetch full details of a list by ID
|
|
21
|
+
create — create a new list in a folder or space (mutating)
|
|
22
|
+
update — update a list's name, content, or status (mutating)
|
|
23
|
+
delete — delete a list (destructive)""",
|
|
24
|
+
epilog="""\
|
|
25
|
+
examples:
|
|
26
|
+
clickup lists list --folder 12345
|
|
27
|
+
clickup lists list --space <name>
|
|
28
|
+
clickup lists get 12345
|
|
29
|
+
clickup --dry-run lists create --folder 12345 --name "Tasks"
|
|
30
|
+
clickup lists create --space <name> --name "Backlog"
|
|
31
|
+
clickup lists update 12345 --name "Renamed list"
|
|
32
|
+
clickup --dry-run lists delete 12345""",
|
|
33
|
+
)
|
|
34
|
+
lists_sub = lists_parser.add_subparsers(dest="command", required=True)
|
|
35
|
+
|
|
36
|
+
# lists list
|
|
37
|
+
ll = lists_sub.add_parser(
|
|
38
|
+
"list",
|
|
39
|
+
formatter_class=F,
|
|
40
|
+
help="List lists in a folder or folderless lists in a space",
|
|
41
|
+
description="""\
|
|
42
|
+
List lists in a specific context. Exactly one of --folder or --space
|
|
43
|
+
is required:
|
|
44
|
+
|
|
45
|
+
--folder <id> — lists all lists inside a folder
|
|
46
|
+
--space <name> — lists only folderless lists in a space""",
|
|
47
|
+
epilog="""\
|
|
48
|
+
returns:
|
|
49
|
+
{"lists": [...], "count": N}
|
|
50
|
+
|
|
51
|
+
examples:
|
|
52
|
+
clickup lists list --folder 12345
|
|
53
|
+
clickup lists list --space <name>
|
|
54
|
+
clickup --pretty lists list --space <name>
|
|
55
|
+
|
|
56
|
+
notes:
|
|
57
|
+
--folder and --space are mutually exclusive.
|
|
58
|
+
--space only returns folderless lists. To see lists inside folders,
|
|
59
|
+
use 'folders list --space <name>' first to find folder IDs, then
|
|
60
|
+
'lists list --folder <id>' for each folder.""",
|
|
61
|
+
)
|
|
62
|
+
ll_target = ll.add_mutually_exclusive_group(required=True)
|
|
63
|
+
ll_target.add_argument(
|
|
64
|
+
"--folder", type=str, help="Folder ID — list all lists inside this folder"
|
|
65
|
+
)
|
|
66
|
+
ll_target.add_argument(
|
|
67
|
+
"--space",
|
|
68
|
+
type=str,
|
|
69
|
+
help="Space name or ID — list folderless lists in this space",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# lists get
|
|
73
|
+
lg = lists_sub.add_parser(
|
|
74
|
+
"get",
|
|
75
|
+
formatter_class=F,
|
|
76
|
+
help="Fetch full details of a list by ID",
|
|
77
|
+
description="""\
|
|
78
|
+
Fetch full details of a list by its ClickUp list ID.
|
|
79
|
+
|
|
80
|
+
Returns list metadata including name, content, task count, statuses,
|
|
81
|
+
folder, and space information.""",
|
|
82
|
+
epilog="""\
|
|
83
|
+
returns:
|
|
84
|
+
One list JSON object with all fields.
|
|
85
|
+
|
|
86
|
+
examples:
|
|
87
|
+
clickup lists get 901816700000
|
|
88
|
+
clickup --pretty lists get 12345""",
|
|
89
|
+
)
|
|
90
|
+
add_id_argument(lg, "list_id", "ClickUp list ID")
|
|
91
|
+
|
|
92
|
+
# lists create
|
|
93
|
+
lc = lists_sub.add_parser(
|
|
94
|
+
"create",
|
|
95
|
+
formatter_class=F,
|
|
96
|
+
help="Create a new list in a folder or space",
|
|
97
|
+
description="""\
|
|
98
|
+
Create a new list. This is a mutating command.
|
|
99
|
+
|
|
100
|
+
Exactly one of --folder or --space is required:
|
|
101
|
+
|
|
102
|
+
--folder <id> — creates the list inside a folder
|
|
103
|
+
--space <name> — creates a folderless list directly in a space
|
|
104
|
+
|
|
105
|
+
Use --dry-run to preview the request body without creating the list.
|
|
106
|
+
Global flags may appear before or after the command group:
|
|
107
|
+
clickup --dry-run lists create --folder 12345 --name "Tasks" """,
|
|
108
|
+
epilog="""\
|
|
109
|
+
returns:
|
|
110
|
+
The created list object from the API.
|
|
111
|
+
|
|
112
|
+
examples:
|
|
113
|
+
clickup lists create --folder 12345 --name "Tasks"
|
|
114
|
+
clickup lists create --space <name> --name "Backlog"
|
|
115
|
+
clickup --dry-run lists create --space <name> --name "Test list"
|
|
116
|
+
|
|
117
|
+
notes:
|
|
118
|
+
--folder and --space are mutually exclusive. Using both is an error.""",
|
|
119
|
+
)
|
|
120
|
+
lc_target = lc.add_mutually_exclusive_group(required=True)
|
|
121
|
+
lc_target.add_argument(
|
|
122
|
+
"--folder", type=str, help="Folder ID — create list inside this folder"
|
|
123
|
+
)
|
|
124
|
+
lc_target.add_argument(
|
|
125
|
+
"--space",
|
|
126
|
+
type=str,
|
|
127
|
+
help="Space name or ID — create a folderless list in this space",
|
|
128
|
+
)
|
|
129
|
+
lc.add_argument("--name", required=True, help="List name (required)")
|
|
130
|
+
lc.add_argument("--content", type=str, help="List description/content")
|
|
131
|
+
lc.add_argument("--status", type=str, help="Initial list status")
|
|
132
|
+
|
|
133
|
+
# lists update
|
|
134
|
+
lu = lists_sub.add_parser(
|
|
135
|
+
"update",
|
|
136
|
+
formatter_class=F,
|
|
137
|
+
help="Update a list (name, content, status)",
|
|
138
|
+
description="""\
|
|
139
|
+
Update a list's name, content, or status. This is a mutating command.
|
|
140
|
+
|
|
141
|
+
At least one mutable field is required: --name, --content, --content-file,
|
|
142
|
+
or --status. If none are provided, the command exits with an error.
|
|
143
|
+
|
|
144
|
+
Use --dry-run to preview the request body without applying changes.
|
|
145
|
+
Global flags may appear before or after the command group:
|
|
146
|
+
clickup --dry-run lists update 12345 --name "New name" """,
|
|
147
|
+
epilog="""\
|
|
148
|
+
returns:
|
|
149
|
+
The updated list object from the API.
|
|
150
|
+
|
|
151
|
+
examples:
|
|
152
|
+
clickup lists update 12345 --name "Renamed list"
|
|
153
|
+
clickup lists update 12345 --content "Updated description"
|
|
154
|
+
clickup --dry-run lists update 12345 --name "Test rename" """,
|
|
155
|
+
)
|
|
156
|
+
add_id_argument(lu, "list_id", "ClickUp list ID to update")
|
|
157
|
+
lu.add_argument("--name", type=str, help="New list name")
|
|
158
|
+
lu.add_argument(
|
|
159
|
+
"--content",
|
|
160
|
+
type=str,
|
|
161
|
+
help="Inline description (mutually exclusive with --content-file)",
|
|
162
|
+
)
|
|
163
|
+
lu.add_argument(
|
|
164
|
+
"--content-file", type=str, help="Path to a file containing list description"
|
|
165
|
+
)
|
|
166
|
+
lu.add_argument("--status", type=str, help="New list status")
|
|
167
|
+
|
|
168
|
+
# lists delete
|
|
169
|
+
ld = lists_sub.add_parser(
|
|
170
|
+
"delete",
|
|
171
|
+
formatter_class=F,
|
|
172
|
+
help="Delete a list (destructive)",
|
|
173
|
+
description="""\
|
|
174
|
+
Delete a list permanently. This is a destructive, irreversible command.
|
|
175
|
+
|
|
176
|
+
Deleting a list also deletes all tasks inside it. Use with extreme caution.
|
|
177
|
+
|
|
178
|
+
Use --dry-run to preview the operation without deleting anything.
|
|
179
|
+
Global flags may appear before or after the command group:
|
|
180
|
+
clickup --dry-run lists delete 12345""",
|
|
181
|
+
epilog="""\
|
|
182
|
+
returns:
|
|
183
|
+
{"status": "ok", "action": "deleted", "list_id": "..."}
|
|
184
|
+
|
|
185
|
+
examples:
|
|
186
|
+
clickup --dry-run lists delete 12345
|
|
187
|
+
clickup lists delete 12345""",
|
|
188
|
+
)
|
|
189
|
+
add_id_argument(ld, "list_id", "ClickUp list ID to delete")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _resolve_list_parent(args):
|
|
193
|
+
"""Resolve folder or space target for list operations."""
|
|
194
|
+
if args.folder:
|
|
195
|
+
return f"/folder/{args.folder}/list", {"folder_id": args.folder}
|
|
196
|
+
elif args.space:
|
|
197
|
+
space_id = resolve_space_id(args.space)
|
|
198
|
+
return f"/space/{space_id}/list", {"space_id": space_id}
|
|
199
|
+
else:
|
|
200
|
+
error("Provide either --folder <folder_id> or --space <name|id>")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_lists_list(client, args):
|
|
204
|
+
"""List lists in a folder or folderless lists in a space."""
|
|
205
|
+
path, _ = _resolve_list_parent(args)
|
|
206
|
+
resp = client.get_v2(path)
|
|
207
|
+
lists = resp.get("lists", [])
|
|
208
|
+
return {"lists": lists, "count": len(lists)}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_lists_get(client, args):
|
|
212
|
+
"""Get full details of a list by ID."""
|
|
213
|
+
return client.get_v2(f"/list/{args.list_id}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cmd_lists_create(client, args):
|
|
217
|
+
"""Create a list in a folder or directly in a space (folderless)."""
|
|
218
|
+
body = {"name": args.name}
|
|
219
|
+
if args.content:
|
|
220
|
+
body["content"] = args.content
|
|
221
|
+
if args.status:
|
|
222
|
+
body["status"] = args.status
|
|
223
|
+
|
|
224
|
+
endpoint, target = _resolve_list_parent(args)
|
|
225
|
+
|
|
226
|
+
if client.dry_run:
|
|
227
|
+
return {"dry_run": True, "action": "create_list", **target, "body": body}
|
|
228
|
+
|
|
229
|
+
return client.post_v2(endpoint, data=body)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def cmd_lists_update(client, args):
|
|
233
|
+
"""Update a list (name, content, status)."""
|
|
234
|
+
desc = read_content(args.content, args.content_file, "--content")
|
|
235
|
+
body = {}
|
|
236
|
+
if args.name:
|
|
237
|
+
body["name"] = args.name
|
|
238
|
+
if desc:
|
|
239
|
+
body["content"] = desc
|
|
240
|
+
if args.status:
|
|
241
|
+
body["status"] = args.status
|
|
242
|
+
|
|
243
|
+
if not body:
|
|
244
|
+
error("Nothing to update — provide at least one of: --name, --content, --content-file, --status")
|
|
245
|
+
|
|
246
|
+
if client.dry_run:
|
|
247
|
+
return {"dry_run": True, "action": "update_list", "list_id": args.list_id, "body": body}
|
|
248
|
+
|
|
249
|
+
return client.put_v2(f"/list/{args.list_id}", data=body)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_lists_delete(client, args):
|
|
253
|
+
"""Delete a list by ID."""
|
|
254
|
+
if client.dry_run:
|
|
255
|
+
return {"dry_run": True, "action": "delete_list", "list_id": args.list_id}
|
|
256
|
+
|
|
257
|
+
client.delete_v2(f"/list/{args.list_id}")
|
|
258
|
+
return {"status": "ok", "action": "deleted", "list_id": args.list_id}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Space command handlers — list, get, statuses."""
|
|
2
|
+
|
|
3
|
+
from ..config import WORKSPACE_ID
|
|
4
|
+
from ..helpers import resolve_space_id, add_id_argument
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register_parser(subparsers, F):
|
|
8
|
+
"""Register all spaces subcommands on the given subparsers object."""
|
|
9
|
+
spaces_parser = subparsers.add_parser(
|
|
10
|
+
"spaces",
|
|
11
|
+
formatter_class=F,
|
|
12
|
+
help="List spaces, view details, and discover statuses",
|
|
13
|
+
description="""\
|
|
14
|
+
Inspect workspace spaces — list all spaces, view space details, and
|
|
15
|
+
discover valid statuses per space.
|
|
16
|
+
|
|
17
|
+
Subcommands:
|
|
18
|
+
list — list all spaces in the workspace
|
|
19
|
+
get — fetch full details of a specific space
|
|
20
|
+
statuses — list valid statuses for a space
|
|
21
|
+
|
|
22
|
+
All commands are read-only. Configured space names and raw ClickUp
|
|
23
|
+
space IDs are both accepted.""",
|
|
24
|
+
epilog="""\
|
|
25
|
+
examples:
|
|
26
|
+
clickup spaces list
|
|
27
|
+
clickup spaces get <space>
|
|
28
|
+
clickup spaces statuses <space>
|
|
29
|
+
|
|
30
|
+
notes:
|
|
31
|
+
These commands hit the API directly — no caching.
|
|
32
|
+
Use 'spaces statuses' to find valid status names before setting
|
|
33
|
+
task statuses, avoiding "Status does not exist" errors.""",
|
|
34
|
+
)
|
|
35
|
+
spaces_sub = spaces_parser.add_subparsers(dest="command", required=True)
|
|
36
|
+
|
|
37
|
+
# spaces list
|
|
38
|
+
spaces_sub.add_parser(
|
|
39
|
+
"list",
|
|
40
|
+
formatter_class=F,
|
|
41
|
+
help="List all spaces in the workspace",
|
|
42
|
+
description="""\
|
|
43
|
+
List all spaces in the workspace. Returns space names, IDs, and basic
|
|
44
|
+
metadata for every space the authenticated user can access.
|
|
45
|
+
|
|
46
|
+
Use this for discovery — find available spaces and their IDs without
|
|
47
|
+
relying on hardcoded configuration.""",
|
|
48
|
+
epilog="""\
|
|
49
|
+
returns:
|
|
50
|
+
{"spaces": [...], "count": N}
|
|
51
|
+
|
|
52
|
+
examples:
|
|
53
|
+
clickup spaces list
|
|
54
|
+
clickup --pretty spaces list""",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# spaces get
|
|
58
|
+
sg = spaces_sub.add_parser(
|
|
59
|
+
"get",
|
|
60
|
+
formatter_class=F,
|
|
61
|
+
help="Fetch full details of a specific space",
|
|
62
|
+
description="""\
|
|
63
|
+
Fetch full details of a space including statuses, features, and members.
|
|
64
|
+
|
|
65
|
+
Accepts a configured space name or a raw ClickUp space ID.
|
|
66
|
+
Use 'spaces list' to discover available spaces.""",
|
|
67
|
+
epilog="""\
|
|
68
|
+
returns:
|
|
69
|
+
One space JSON object with all fields (statuses, features, members, etc.)
|
|
70
|
+
|
|
71
|
+
examples:
|
|
72
|
+
clickup spaces get <space>
|
|
73
|
+
clickup spaces get 901810200000
|
|
74
|
+
clickup --pretty spaces get <space>""",
|
|
75
|
+
)
|
|
76
|
+
add_id_argument(sg, "space", "Space name (from config) or raw space ID")
|
|
77
|
+
|
|
78
|
+
# spaces statuses
|
|
79
|
+
ss = spaces_sub.add_parser(
|
|
80
|
+
"statuses",
|
|
81
|
+
formatter_class=F,
|
|
82
|
+
help="List valid statuses for a space",
|
|
83
|
+
description="""\
|
|
84
|
+
List the valid workflow statuses for a space. Returns the status name,
|
|
85
|
+
type (open, closed, custom), color, and order.
|
|
86
|
+
|
|
87
|
+
Use this before setting a task status to avoid "Status does not exist"
|
|
88
|
+
errors. Status names are space-specific — each space has its own set.""",
|
|
89
|
+
epilog="""\
|
|
90
|
+
returns:
|
|
91
|
+
{"space": "<name>", "statuses": [...], "count": N}
|
|
92
|
+
|
|
93
|
+
Each status has: status (name), type, color, orderindex.
|
|
94
|
+
|
|
95
|
+
examples:
|
|
96
|
+
clickup spaces statuses <space>
|
|
97
|
+
clickup spaces statuses 901810200000
|
|
98
|
+
clickup --pretty spaces statuses <space>
|
|
99
|
+
|
|
100
|
+
notes:
|
|
101
|
+
Accepts a configured space name or raw space ID.
|
|
102
|
+
Statuses can only be modified via the ClickUp UI, not the API.""",
|
|
103
|
+
)
|
|
104
|
+
add_id_argument(ss, "space", "Space name (from config) or raw space ID")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_spaces_list(client, args):
|
|
108
|
+
"""List all spaces in the workspace."""
|
|
109
|
+
resp = client.get_v2(f"/team/{WORKSPACE_ID}/space")
|
|
110
|
+
spaces = resp.get("spaces", [])
|
|
111
|
+
return {"spaces": spaces, "count": len(spaces)}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_spaces_get(client, args):
|
|
115
|
+
"""Get full details of a specific space."""
|
|
116
|
+
space_id = resolve_space_id(args.space)
|
|
117
|
+
return client.get_v2(f"/space/{space_id}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_spaces_statuses(client, args):
|
|
121
|
+
"""List valid statuses for a space."""
|
|
122
|
+
space_id = resolve_space_id(args.space)
|
|
123
|
+
resp = client.get_v2(f"/space/{space_id}")
|
|
124
|
+
statuses = resp.get("statuses", [])
|
|
125
|
+
return {
|
|
126
|
+
"space": args.space,
|
|
127
|
+
"statuses": [
|
|
128
|
+
{
|
|
129
|
+
"status": s.get("status"),
|
|
130
|
+
"type": s.get("type"),
|
|
131
|
+
"color": s.get("color"),
|
|
132
|
+
"orderindex": s.get("orderindex"),
|
|
133
|
+
}
|
|
134
|
+
for s in statuses
|
|
135
|
+
],
|
|
136
|
+
"count": len(statuses),
|
|
137
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Tag command handlers — list, add, remove."""
|
|
2
|
+
|
|
3
|
+
from ..helpers import resolve_space_id, add_id_argument
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_parser(subparsers, F):
|
|
7
|
+
"""Register all tags subcommands on the given subparsers object."""
|
|
8
|
+
tags_parser = subparsers.add_parser(
|
|
9
|
+
"tags",
|
|
10
|
+
formatter_class=F,
|
|
11
|
+
help="List space tags, add/remove tags on tasks",
|
|
12
|
+
description="""\
|
|
13
|
+
Manage tags — list available tags in a space, add or remove tags on tasks.
|
|
14
|
+
|
|
15
|
+
Subcommands:
|
|
16
|
+
list — list all tags in a space
|
|
17
|
+
add — add a tag to a task (mutating)
|
|
18
|
+
remove — remove a tag from a task (mutating)
|
|
19
|
+
|
|
20
|
+
Tag names are lowercase in the API, even if they display with title case
|
|
21
|
+
in the ClickUp UI. The CLI lowercases tag names automatically.""",
|
|
22
|
+
epilog="""\
|
|
23
|
+
examples:
|
|
24
|
+
clickup tags list --space <name>
|
|
25
|
+
clickup tags add abc123 --tag "in review"
|
|
26
|
+
clickup tags remove abc123 --tag "draft" """,
|
|
27
|
+
)
|
|
28
|
+
tags_sub = tags_parser.add_subparsers(dest="command", required=True)
|
|
29
|
+
|
|
30
|
+
# tags list
|
|
31
|
+
tgl = tags_sub.add_parser(
|
|
32
|
+
"list",
|
|
33
|
+
formatter_class=F,
|
|
34
|
+
help="List all tags in a space",
|
|
35
|
+
description="""\
|
|
36
|
+
List all tags available in a space. Returns tag names, colors, and metadata.
|
|
37
|
+
|
|
38
|
+
Accepts a configured space name or a raw space ID.""",
|
|
39
|
+
epilog="""\
|
|
40
|
+
returns:
|
|
41
|
+
{"tags": [...], "count": N}
|
|
42
|
+
|
|
43
|
+
examples:
|
|
44
|
+
clickup tags list --space <name>
|
|
45
|
+
clickup --pretty tags list --space <name>""",
|
|
46
|
+
)
|
|
47
|
+
tgl.add_argument(
|
|
48
|
+
"--space",
|
|
49
|
+
required=True,
|
|
50
|
+
type=str,
|
|
51
|
+
help="Space name (from config) or raw space ID",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# tags add
|
|
55
|
+
tga = tags_sub.add_parser(
|
|
56
|
+
"add",
|
|
57
|
+
formatter_class=F,
|
|
58
|
+
help="Add a tag to a task",
|
|
59
|
+
description="""\
|
|
60
|
+
Add a tag to a task. This is a mutating command.
|
|
61
|
+
|
|
62
|
+
The tag name is automatically lowercased (ClickUp API requirement).
|
|
63
|
+
The tag must already exist in the space — this command does not create tags.
|
|
64
|
+
|
|
65
|
+
Use --dry-run to preview without applying.
|
|
66
|
+
Global flags may appear before or after the command group:
|
|
67
|
+
clickup --dry-run tags add abc123 --tag "in review" """,
|
|
68
|
+
epilog="""\
|
|
69
|
+
returns:
|
|
70
|
+
{"status": "ok", "action": "tag_added", "task_id": "...", "tag": "..."}
|
|
71
|
+
|
|
72
|
+
examples:
|
|
73
|
+
clickup tags add abc123 --tag "in review"
|
|
74
|
+
clickup --dry-run tags add abc123 --tag "draft" """,
|
|
75
|
+
)
|
|
76
|
+
add_id_argument(tga, "task_id", "ClickUp task ID")
|
|
77
|
+
tga.add_argument(
|
|
78
|
+
"--tag", required=True, type=str, help="Tag name to add (auto-lowercased)"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# tags remove
|
|
82
|
+
tgr = tags_sub.add_parser(
|
|
83
|
+
"remove",
|
|
84
|
+
formatter_class=F,
|
|
85
|
+
help="Remove a tag from a task",
|
|
86
|
+
description="""\
|
|
87
|
+
Remove a tag from a task. This is a mutating command.
|
|
88
|
+
|
|
89
|
+
The tag name is automatically lowercased (ClickUp API requirement).
|
|
90
|
+
|
|
91
|
+
Use --dry-run to preview without applying.
|
|
92
|
+
Global flags may appear before or after the command group:
|
|
93
|
+
clickup --dry-run tags remove abc123 --tag "draft" """,
|
|
94
|
+
epilog="""\
|
|
95
|
+
returns:
|
|
96
|
+
{"status": "ok", "action": "tag_removed", "task_id": "...", "tag": "..."}
|
|
97
|
+
|
|
98
|
+
examples:
|
|
99
|
+
clickup tags remove abc123 --tag "draft"
|
|
100
|
+
clickup --dry-run tags remove abc123 --tag "in review" """,
|
|
101
|
+
)
|
|
102
|
+
add_id_argument(tgr, "task_id", "ClickUp task ID")
|
|
103
|
+
tgr.add_argument(
|
|
104
|
+
"--tag", required=True, type=str, help="Tag name to remove (auto-lowercased)"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_tags_list(client, args):
|
|
109
|
+
"""List all tags in a space."""
|
|
110
|
+
space_id = resolve_space_id(args.space)
|
|
111
|
+
resp = client.get_v2(f"/space/{space_id}/tag")
|
|
112
|
+
tags = resp.get("tags", [])
|
|
113
|
+
return {"tags": tags, "count": len(tags)}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _tag_action(client, args, method, dry_action, done_action):
|
|
117
|
+
"""Shared logic for tag add/remove."""
|
|
118
|
+
tag_name = args.tag.lower()
|
|
119
|
+
if client.dry_run:
|
|
120
|
+
return {"dry_run": True, "action": dry_action, "task_id": args.task_id, "tag": tag_name}
|
|
121
|
+
method(f"/task/{args.task_id}/tag/{tag_name}", **({"data": {}} if method == client.post_v2 else {}))
|
|
122
|
+
return {"status": "ok", "action": done_action, "task_id": args.task_id, "tag": tag_name}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_tags_add(client, args):
|
|
126
|
+
"""Add a tag to a task."""
|
|
127
|
+
return _tag_action(client, args, client.post_v2, "add_tag", "tag_added")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_tags_remove(client, args):
|
|
131
|
+
"""Remove a tag from a task."""
|
|
132
|
+
return _tag_action(client, args, client.delete_v2, "remove_tag", "tag_removed")
|