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
clickup_cli/__init__.py
ADDED
clickup_cli/__main__.py
ADDED
clickup_cli/cli.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""CLI parser, dispatch, and main entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .client import ClickUpClient
|
|
9
|
+
from .helpers import output, error, resolve_id_args
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
GLOBAL_FLAGS = {"--pretty", "--dry-run", "--debug"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_cli_argv(argv):
|
|
16
|
+
"""Allow global flags before or after the command group."""
|
|
17
|
+
front = []
|
|
18
|
+
rest = []
|
|
19
|
+
|
|
20
|
+
for arg in argv:
|
|
21
|
+
if arg in GLOBAL_FLAGS:
|
|
22
|
+
if arg not in front:
|
|
23
|
+
front.append(arg)
|
|
24
|
+
else:
|
|
25
|
+
rest.append(arg)
|
|
26
|
+
|
|
27
|
+
return front + rest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_parser():
|
|
31
|
+
F = argparse.RawDescriptionHelpFormatter
|
|
32
|
+
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="clickup",
|
|
35
|
+
formatter_class=F,
|
|
36
|
+
description="""\
|
|
37
|
+
ClickUp CLI — manage tasks, comments, docs, folders, lists, spaces, and teams from the command line.
|
|
38
|
+
|
|
39
|
+
Covers eight command groups: tasks, comments, docs, folders, lists, spaces, team, and tags.
|
|
40
|
+
All successful output is JSON printed to stdout.
|
|
41
|
+
Errors are printed to stderr with a non-zero exit code.
|
|
42
|
+
|
|
43
|
+
Global flags can appear before or after the command group:
|
|
44
|
+
clickup --pretty tasks list --space <name>
|
|
45
|
+
clickup tasks list --space <name> --pretty
|
|
46
|
+
clickup --dry-run tasks create --space <name> --name "My task"
|
|
47
|
+
clickup --debug tasks list --space <name>""",
|
|
48
|
+
epilog="""\
|
|
49
|
+
examples:
|
|
50
|
+
clickup init # set up config
|
|
51
|
+
clickup tasks list --space <name>
|
|
52
|
+
clickup tasks list --list 12345
|
|
53
|
+
clickup --dry-run tasks create --space <name> --name "Fix login"
|
|
54
|
+
clickup folders list --space <name>
|
|
55
|
+
clickup lists list --folder 12345
|
|
56
|
+
clickup comments list abc123
|
|
57
|
+
clickup docs pages doc_abc123
|
|
58
|
+
|
|
59
|
+
current coverage:
|
|
60
|
+
tasks — list, get, create, update, search, delete, move, merge
|
|
61
|
+
comments — list, add, update, delete, thread, reply
|
|
62
|
+
docs — list, get, create, pages, get-page, edit-page, create-page
|
|
63
|
+
folders — list, get, create, update, delete
|
|
64
|
+
lists — list, get, create, update, delete
|
|
65
|
+
spaces — list, get, statuses
|
|
66
|
+
team — whoami, members
|
|
67
|
+
tags — list, add, remove
|
|
68
|
+
|
|
69
|
+
Use <group> --help for details on each group.""",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--pretty",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Pretty-print JSON output with indentation",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--dry-run",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Preview the API request without executing it (safe for mutations)",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--debug",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Log API requests and responses to stderr for troubleshooting",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--version",
|
|
88
|
+
action="version",
|
|
89
|
+
version=f"%(prog)s {__version__}",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
subparsers = parser.add_subparsers(dest="group", required=True)
|
|
93
|
+
|
|
94
|
+
# init (standalone — no register_parser, handled specially in main())
|
|
95
|
+
init_parser = subparsers.add_parser(
|
|
96
|
+
"init",
|
|
97
|
+
formatter_class=F,
|
|
98
|
+
help="Set up ClickUp CLI configuration",
|
|
99
|
+
description="""\
|
|
100
|
+
Set up the ClickUp CLI by connecting to your workspace.
|
|
101
|
+
|
|
102
|
+
Fetches your workspaces and spaces, then writes a config file to
|
|
103
|
+
~/.config/clickup-cli/config.json.
|
|
104
|
+
|
|
105
|
+
Run interactively (prompts for token) or pass --token for automation.""",
|
|
106
|
+
epilog="""\
|
|
107
|
+
examples:
|
|
108
|
+
clickup init
|
|
109
|
+
clickup init --token pk_YOUR_API_TOKEN""",
|
|
110
|
+
)
|
|
111
|
+
init_parser.add_argument(
|
|
112
|
+
"--token",
|
|
113
|
+
type=str,
|
|
114
|
+
help="ClickUp API token (skips interactive prompt)",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Register all command group parsers from their modules
|
|
118
|
+
from .commands.tasks import register_parser as tasks_register
|
|
119
|
+
from .commands.comments import register_parser as comments_register
|
|
120
|
+
from .commands.docs import register_parser as docs_register
|
|
121
|
+
from .commands.spaces import register_parser as spaces_register
|
|
122
|
+
from .commands.folders import register_parser as folders_register
|
|
123
|
+
from .commands.lists import register_parser as lists_register
|
|
124
|
+
from .commands.team import register_parser as team_register
|
|
125
|
+
from .commands.tags import register_parser as tags_register
|
|
126
|
+
|
|
127
|
+
tasks_register(subparsers, F)
|
|
128
|
+
comments_register(subparsers, F)
|
|
129
|
+
docs_register(subparsers, F)
|
|
130
|
+
spaces_register(subparsers, F)
|
|
131
|
+
folders_register(subparsers, F)
|
|
132
|
+
lists_register(subparsers, F)
|
|
133
|
+
team_register(subparsers, F)
|
|
134
|
+
tags_register(subparsers, F)
|
|
135
|
+
|
|
136
|
+
return parser
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def dispatch(client, args):
|
|
140
|
+
"""Route parsed args to the correct command handler."""
|
|
141
|
+
from .commands import HANDLERS
|
|
142
|
+
|
|
143
|
+
key = f"{args.group}_{args.command}"
|
|
144
|
+
handler = HANDLERS.get(key)
|
|
145
|
+
if not handler:
|
|
146
|
+
error(f"Unknown command: {args.group} {args.command}")
|
|
147
|
+
assert handler is not None
|
|
148
|
+
return handler(client, args)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main():
|
|
152
|
+
parser = build_parser()
|
|
153
|
+
args = parser.parse_args(normalize_cli_argv(sys.argv[1:]))
|
|
154
|
+
resolve_id_args(args)
|
|
155
|
+
|
|
156
|
+
# Handle init before loading config (config may not exist yet)
|
|
157
|
+
if args.group == "init":
|
|
158
|
+
from .commands.init import cmd_init
|
|
159
|
+
cmd_init(args)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Load config and resolve token
|
|
163
|
+
from .config import load_config
|
|
164
|
+
config = load_config()
|
|
165
|
+
|
|
166
|
+
token = os.environ.get("CLICKUP_API_TOKEN") or config.get("api_token")
|
|
167
|
+
if not token:
|
|
168
|
+
error(
|
|
169
|
+
"No API token found. Set CLICKUP_API_TOKEN or run: clickup init"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
client = ClickUpClient(token, dry_run=args.dry_run, debug=args.debug)
|
|
173
|
+
result = dispatch(client, args)
|
|
174
|
+
|
|
175
|
+
if result is not None:
|
|
176
|
+
output(result, pretty=args.pretty)
|
clickup_cli/client.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""ClickUp API client with rate limiting, dry-run, and debug support."""
|
|
2
|
+
|
|
3
|
+
import json as _json
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .helpers import error
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClickUpClient:
|
|
13
|
+
BASE_V2 = "https://api.clickup.com/api/v2"
|
|
14
|
+
BASE_V3 = "https://api.clickup.com/api/v3"
|
|
15
|
+
|
|
16
|
+
def __init__(self, token, dry_run=False, debug=False):
|
|
17
|
+
self.token = token
|
|
18
|
+
self.dry_run = dry_run
|
|
19
|
+
self.debug = debug
|
|
20
|
+
self.session = requests.Session()
|
|
21
|
+
self.session.headers.update(
|
|
22
|
+
{
|
|
23
|
+
"Authorization": token,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def _log(self, msg):
|
|
29
|
+
"""Print debug message to stderr."""
|
|
30
|
+
if self.debug:
|
|
31
|
+
print(f"[debug] {msg}", file=sys.stderr)
|
|
32
|
+
|
|
33
|
+
def _check_rate_limit(self, response):
|
|
34
|
+
"""Sleep proactively if rate limit is running low."""
|
|
35
|
+
remaining = response.headers.get("X-RateLimit-Remaining")
|
|
36
|
+
reset = response.headers.get("X-RateLimit-Reset")
|
|
37
|
+
if remaining is not None and int(remaining) < 10 and reset:
|
|
38
|
+
wait = max(0, int(reset) - int(time.time())) + 1
|
|
39
|
+
print(
|
|
40
|
+
f"Rate limit low ({remaining} remaining), waiting {wait}s...",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
time.sleep(wait)
|
|
44
|
+
|
|
45
|
+
def _request(self, method, url, allow_dry_run=False, **kwargs):
|
|
46
|
+
"""Make an HTTP request with rate limit handling and error reporting."""
|
|
47
|
+
if self.dry_run and not allow_dry_run:
|
|
48
|
+
return {"dry_run": True, "method": method, "url": url, "kwargs": kwargs}
|
|
49
|
+
|
|
50
|
+
self._log(f"{method} {url}")
|
|
51
|
+
if kwargs.get("params"):
|
|
52
|
+
self._log(f" params: {kwargs['params']}")
|
|
53
|
+
if kwargs.get("json"):
|
|
54
|
+
self._log(f" body: {_json.dumps(kwargs['json'], ensure_ascii=False)}")
|
|
55
|
+
|
|
56
|
+
def _do_request():
|
|
57
|
+
try:
|
|
58
|
+
return self.session.request(method, url, **kwargs)
|
|
59
|
+
except requests.ConnectionError:
|
|
60
|
+
error("Couldn't reach ClickUp API — check your network connection")
|
|
61
|
+
|
|
62
|
+
response = _do_request()
|
|
63
|
+
self._log(f" → {response.status_code} ({len(response.text)} bytes)")
|
|
64
|
+
|
|
65
|
+
if response.status_code == 429:
|
|
66
|
+
reset = response.headers.get("X-RateLimit-Reset")
|
|
67
|
+
if reset:
|
|
68
|
+
wait = max(0, int(reset) - int(time.time())) + 1
|
|
69
|
+
print(f"Rate limited (429), waiting {wait}s...", file=sys.stderr)
|
|
70
|
+
time.sleep(wait)
|
|
71
|
+
response = _do_request()
|
|
72
|
+
|
|
73
|
+
if response.status_code == 401:
|
|
74
|
+
error(
|
|
75
|
+
"Authentication failed — check your API token"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not response.ok:
|
|
79
|
+
body = response.text
|
|
80
|
+
try:
|
|
81
|
+
body = response.json()
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
error(f"API error {response.status_code} on {method} {url}: {body}")
|
|
85
|
+
|
|
86
|
+
self._check_rate_limit(response)
|
|
87
|
+
|
|
88
|
+
if response.status_code == 204 or not response.text:
|
|
89
|
+
return {}
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
def get_v2(self, path, params=None, allow_dry_run=False):
|
|
93
|
+
return self._request(
|
|
94
|
+
"GET", f"{self.BASE_V2}{path}", params=params, allow_dry_run=allow_dry_run
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def post_v2(self, path, data=None):
|
|
98
|
+
return self._request("POST", f"{self.BASE_V2}{path}", json=data)
|
|
99
|
+
|
|
100
|
+
def put_v2(self, path, data=None):
|
|
101
|
+
return self._request("PUT", f"{self.BASE_V2}{path}", json=data)
|
|
102
|
+
|
|
103
|
+
def delete_v2(self, path):
|
|
104
|
+
return self._request("DELETE", f"{self.BASE_V2}{path}")
|
|
105
|
+
|
|
106
|
+
def get_v3(self, path, params=None, allow_dry_run=False):
|
|
107
|
+
return self._request(
|
|
108
|
+
"GET", f"{self.BASE_V3}{path}", params=params, allow_dry_run=allow_dry_run
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def post_v3(self, path, data=None):
|
|
112
|
+
return self._request("POST", f"{self.BASE_V3}{path}", json=data)
|
|
113
|
+
|
|
114
|
+
def put_v3(self, path, data=None):
|
|
115
|
+
return self._request("PUT", f"{self.BASE_V3}{path}", json=data)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Command handler registry — maps dispatch keys to handler functions."""
|
|
2
|
+
|
|
3
|
+
from .tasks import (
|
|
4
|
+
cmd_tasks_list, cmd_tasks_get, cmd_tasks_create,
|
|
5
|
+
cmd_tasks_update, cmd_tasks_search,
|
|
6
|
+
cmd_tasks_delete, cmd_tasks_move, cmd_tasks_merge,
|
|
7
|
+
)
|
|
8
|
+
from .comments import (
|
|
9
|
+
cmd_comments_list, cmd_comments_add,
|
|
10
|
+
cmd_comments_update, cmd_comments_delete,
|
|
11
|
+
cmd_comments_thread, cmd_comments_reply,
|
|
12
|
+
)
|
|
13
|
+
from .docs import (
|
|
14
|
+
cmd_docs_list, cmd_docs_get, cmd_docs_create, cmd_docs_pages,
|
|
15
|
+
cmd_docs_get_page, cmd_docs_edit_page, cmd_docs_create_page,
|
|
16
|
+
)
|
|
17
|
+
from .spaces import cmd_spaces_list, cmd_spaces_get, cmd_spaces_statuses
|
|
18
|
+
from .team import cmd_team_whoami, cmd_team_members
|
|
19
|
+
from .tags import cmd_tags_list, cmd_tags_add, cmd_tags_remove
|
|
20
|
+
from .folders import (
|
|
21
|
+
cmd_folders_list, cmd_folders_get, cmd_folders_create,
|
|
22
|
+
cmd_folders_update, cmd_folders_delete,
|
|
23
|
+
)
|
|
24
|
+
from .lists import (
|
|
25
|
+
cmd_lists_list, cmd_lists_get, cmd_lists_create,
|
|
26
|
+
cmd_lists_update, cmd_lists_delete,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Keys use f"{args.group}_{args.command}" format from dispatch().
|
|
30
|
+
# Some keys retain hyphens matching argparse subparser names.
|
|
31
|
+
HANDLERS = {
|
|
32
|
+
"tasks_list": cmd_tasks_list,
|
|
33
|
+
"tasks_get": cmd_tasks_get,
|
|
34
|
+
"tasks_create": cmd_tasks_create,
|
|
35
|
+
"tasks_update": cmd_tasks_update,
|
|
36
|
+
"tasks_search": cmd_tasks_search,
|
|
37
|
+
"tasks_delete": cmd_tasks_delete,
|
|
38
|
+
"tasks_move": cmd_tasks_move,
|
|
39
|
+
"tasks_merge": cmd_tasks_merge,
|
|
40
|
+
"comments_list": cmd_comments_list,
|
|
41
|
+
"comments_add": cmd_comments_add,
|
|
42
|
+
"comments_update": cmd_comments_update,
|
|
43
|
+
"comments_delete": cmd_comments_delete,
|
|
44
|
+
"comments_thread": cmd_comments_thread,
|
|
45
|
+
"comments_reply": cmd_comments_reply,
|
|
46
|
+
"docs_list": cmd_docs_list,
|
|
47
|
+
"docs_get": cmd_docs_get,
|
|
48
|
+
"docs_create": cmd_docs_create,
|
|
49
|
+
"docs_pages": cmd_docs_pages,
|
|
50
|
+
"docs_get-page": cmd_docs_get_page,
|
|
51
|
+
"docs_edit-page": cmd_docs_edit_page,
|
|
52
|
+
"docs_create-page": cmd_docs_create_page,
|
|
53
|
+
"spaces_list": cmd_spaces_list,
|
|
54
|
+
"spaces_get": cmd_spaces_get,
|
|
55
|
+
"spaces_statuses": cmd_spaces_statuses,
|
|
56
|
+
"team_whoami": cmd_team_whoami,
|
|
57
|
+
"team_members": cmd_team_members,
|
|
58
|
+
"tags_list": cmd_tags_list,
|
|
59
|
+
"tags_add": cmd_tags_add,
|
|
60
|
+
"tags_remove": cmd_tags_remove,
|
|
61
|
+
"folders_list": cmd_folders_list,
|
|
62
|
+
"folders_get": cmd_folders_get,
|
|
63
|
+
"folders_create": cmd_folders_create,
|
|
64
|
+
"folders_update": cmd_folders_update,
|
|
65
|
+
"folders_delete": cmd_folders_delete,
|
|
66
|
+
"lists_list": cmd_lists_list,
|
|
67
|
+
"lists_get": cmd_lists_get,
|
|
68
|
+
"lists_create": cmd_lists_create,
|
|
69
|
+
"lists_update": cmd_lists_update,
|
|
70
|
+
"lists_delete": cmd_lists_delete,
|
|
71
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Comment command handlers — list, add, update, delete, thread, reply."""
|
|
2
|
+
|
|
3
|
+
from ..helpers import read_content, error, fetch_all_comments, add_id_argument
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_parser(subparsers, F):
|
|
7
|
+
"""Register all comments subcommands on the given subparsers object."""
|
|
8
|
+
comments_parser = subparsers.add_parser(
|
|
9
|
+
"comments",
|
|
10
|
+
formatter_class=F,
|
|
11
|
+
help="Full comment CRUD: list, add, update, delete, thread, reply",
|
|
12
|
+
description="""\
|
|
13
|
+
Manage task comments — full CRUD plus threaded replies.
|
|
14
|
+
|
|
15
|
+
Subcommands:
|
|
16
|
+
list — list comments on a task
|
|
17
|
+
add — add a comment to a task (mutating)
|
|
18
|
+
update — edit an existing comment (mutating)
|
|
19
|
+
delete — delete a comment (destructive)
|
|
20
|
+
thread — get threaded replies to a comment
|
|
21
|
+
reply — reply to a comment in a thread (mutating)""",
|
|
22
|
+
epilog="""\
|
|
23
|
+
examples:
|
|
24
|
+
clickup comments list abc123
|
|
25
|
+
clickup comments add abc123 --text "Work complete"
|
|
26
|
+
clickup --dry-run comments add abc123 --file report.md""",
|
|
27
|
+
)
|
|
28
|
+
comments_sub = comments_parser.add_subparsers(dest="command", required=True)
|
|
29
|
+
|
|
30
|
+
# comments list
|
|
31
|
+
cl = comments_sub.add_parser(
|
|
32
|
+
"list",
|
|
33
|
+
formatter_class=F,
|
|
34
|
+
help="List comments on a task",
|
|
35
|
+
description="""\
|
|
36
|
+
List comments on a task. By default returns the first page (up to 25).
|
|
37
|
+
Use --all to paginate through every comment on the task.
|
|
38
|
+
|
|
39
|
+
Use this when you need to read discussion or work logs on a task.""",
|
|
40
|
+
epilog="""\
|
|
41
|
+
returns:
|
|
42
|
+
{"comments": [...], "count": N}
|
|
43
|
+
|
|
44
|
+
examples:
|
|
45
|
+
clickup comments list abc123
|
|
46
|
+
clickup comments list abc123 --all
|
|
47
|
+
clickup --pretty comments list abc123""",
|
|
48
|
+
)
|
|
49
|
+
add_id_argument(cl, "task_id", "ClickUp task ID")
|
|
50
|
+
cl.add_argument(
|
|
51
|
+
"--all",
|
|
52
|
+
action="store_true",
|
|
53
|
+
dest="fetch_all",
|
|
54
|
+
help="Fetch all comment pages (default: first page only)",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# comments add
|
|
58
|
+
ca = comments_sub.add_parser(
|
|
59
|
+
"add",
|
|
60
|
+
formatter_class=F,
|
|
61
|
+
help="Add a comment to a task",
|
|
62
|
+
description="""\
|
|
63
|
+
Add a comment to a task. This is a mutating command.
|
|
64
|
+
|
|
65
|
+
Exactly one of --text or --file is required.
|
|
66
|
+
Use --text for short inline comments. Use --file to post content from
|
|
67
|
+
a file (useful for longer markdown or structured logs).
|
|
68
|
+
|
|
69
|
+
Use --dry-run to preview the request body without posting the comment.
|
|
70
|
+
Global flags may appear before or after the command group:
|
|
71
|
+
clickup --dry-run comments add abc123 --text "Preview this" """,
|
|
72
|
+
epilog="""\
|
|
73
|
+
returns:
|
|
74
|
+
The created comment object from the API.
|
|
75
|
+
|
|
76
|
+
examples:
|
|
77
|
+
clickup comments add abc123 --text "Task complete"
|
|
78
|
+
clickup comments add abc123 --file session_log.md
|
|
79
|
+
clickup --dry-run comments add abc123 --text "Test comment"
|
|
80
|
+
|
|
81
|
+
notes:
|
|
82
|
+
--text and --file are mutually exclusive. Using both is an error.""",
|
|
83
|
+
)
|
|
84
|
+
add_id_argument(ca, "task_id", "ClickUp task ID")
|
|
85
|
+
ca.add_argument(
|
|
86
|
+
"--text", type=str, help="Inline comment text (mutually exclusive with --file)"
|
|
87
|
+
)
|
|
88
|
+
ca.add_argument(
|
|
89
|
+
"--file", type=str, help="Path to a file containing comment content"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# comments update
|
|
93
|
+
cu = comments_sub.add_parser(
|
|
94
|
+
"update",
|
|
95
|
+
formatter_class=F,
|
|
96
|
+
help="Edit an existing comment",
|
|
97
|
+
description="""\
|
|
98
|
+
Edit an existing comment's text or resolved status. This is a mutating command.
|
|
99
|
+
|
|
100
|
+
At least one change is required: --text/--file for new text, or
|
|
101
|
+
--resolve/--unresolve to change the resolved state.
|
|
102
|
+
|
|
103
|
+
Use --dry-run to preview without applying changes.
|
|
104
|
+
Global flags may appear before or after the command group:
|
|
105
|
+
clickup --dry-run comments update 456 --text "Fixed text" """,
|
|
106
|
+
epilog="""\
|
|
107
|
+
returns:
|
|
108
|
+
{"status": "ok", "action": "updated", "comment_id": "..."}
|
|
109
|
+
|
|
110
|
+
examples:
|
|
111
|
+
clickup comments update 456 --text "Corrected info"
|
|
112
|
+
clickup comments update 456 --file updated_notes.md
|
|
113
|
+
clickup comments update 456 --resolve
|
|
114
|
+
clickup --dry-run comments update 456 --text "Test"
|
|
115
|
+
|
|
116
|
+
notes:
|
|
117
|
+
--text and --file are mutually exclusive. Using both is an error.
|
|
118
|
+
--resolve and --unresolve are mutually exclusive.""",
|
|
119
|
+
)
|
|
120
|
+
add_id_argument(cu, "comment_id", "ClickUp comment ID")
|
|
121
|
+
cu.add_argument(
|
|
122
|
+
"--text", type=str, help="New comment text (mutually exclusive with --file)"
|
|
123
|
+
)
|
|
124
|
+
cu.add_argument(
|
|
125
|
+
"--file", type=str, help="Path to a file containing new comment text"
|
|
126
|
+
)
|
|
127
|
+
resolve_group = cu.add_mutually_exclusive_group()
|
|
128
|
+
resolve_group.add_argument(
|
|
129
|
+
"--resolve",
|
|
130
|
+
action="store_const",
|
|
131
|
+
const=True,
|
|
132
|
+
dest="resolved",
|
|
133
|
+
help="Mark the comment as resolved",
|
|
134
|
+
)
|
|
135
|
+
resolve_group.add_argument(
|
|
136
|
+
"--unresolve",
|
|
137
|
+
action="store_const",
|
|
138
|
+
const=False,
|
|
139
|
+
dest="resolved",
|
|
140
|
+
help="Mark the comment as unresolved",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# comments delete
|
|
144
|
+
cd = comments_sub.add_parser(
|
|
145
|
+
"delete",
|
|
146
|
+
formatter_class=F,
|
|
147
|
+
help="Delete a comment (destructive)",
|
|
148
|
+
description="""\
|
|
149
|
+
Delete a comment permanently. This is a destructive, irreversible command.
|
|
150
|
+
|
|
151
|
+
Use --dry-run to preview the operation without deleting anything.
|
|
152
|
+
Global flags may appear before or after the command group:
|
|
153
|
+
clickup --dry-run comments delete 456""",
|
|
154
|
+
epilog="""\
|
|
155
|
+
returns:
|
|
156
|
+
{"status": "ok", "action": "deleted", "comment_id": "..."}
|
|
157
|
+
|
|
158
|
+
examples:
|
|
159
|
+
clickup --dry-run comments delete 456
|
|
160
|
+
clickup comments delete 456""",
|
|
161
|
+
)
|
|
162
|
+
add_id_argument(cd, "comment_id", "ClickUp comment ID to delete")
|
|
163
|
+
|
|
164
|
+
# comments thread
|
|
165
|
+
ct = comments_sub.add_parser(
|
|
166
|
+
"thread",
|
|
167
|
+
formatter_class=F,
|
|
168
|
+
help="Get threaded replies to a comment",
|
|
169
|
+
description="""\
|
|
170
|
+
Get all threaded replies to a specific comment. The parent comment is NOT
|
|
171
|
+
included in the response — only the replies.
|
|
172
|
+
|
|
173
|
+
Use this to read conversation threads on tasks.""",
|
|
174
|
+
epilog="""\
|
|
175
|
+
returns:
|
|
176
|
+
{"replies": [...], "count": N}
|
|
177
|
+
|
|
178
|
+
examples:
|
|
179
|
+
clickup comments thread 456
|
|
180
|
+
clickup --pretty comments thread 456""",
|
|
181
|
+
)
|
|
182
|
+
add_id_argument(ct, "comment_id", "Parent comment ID")
|
|
183
|
+
|
|
184
|
+
# comments reply
|
|
185
|
+
cr = comments_sub.add_parser(
|
|
186
|
+
"reply",
|
|
187
|
+
formatter_class=F,
|
|
188
|
+
help="Reply to a comment in a thread",
|
|
189
|
+
description="""\
|
|
190
|
+
Post a threaded reply to a specific comment. This is a mutating command.
|
|
191
|
+
|
|
192
|
+
Exactly one of --text or --file is required.
|
|
193
|
+
|
|
194
|
+
Use --dry-run to preview without posting.
|
|
195
|
+
Global flags may appear before or after the command group:
|
|
196
|
+
clickup --dry-run comments reply 456 --text "Preview" """,
|
|
197
|
+
epilog="""\
|
|
198
|
+
returns:
|
|
199
|
+
The created reply object from the API.
|
|
200
|
+
|
|
201
|
+
examples:
|
|
202
|
+
clickup comments reply 456 --text "Agreed, let's proceed"
|
|
203
|
+
clickup comments reply 456 --file detailed_response.md
|
|
204
|
+
clickup --dry-run comments reply 456 --text "Test reply"
|
|
205
|
+
|
|
206
|
+
notes:
|
|
207
|
+
--text and --file are mutually exclusive. Using both is an error.""",
|
|
208
|
+
)
|
|
209
|
+
add_id_argument(cr, "comment_id", "Parent comment ID to reply to")
|
|
210
|
+
cr.add_argument(
|
|
211
|
+
"--text", type=str, help="Inline reply text (mutually exclusive with --file)"
|
|
212
|
+
)
|
|
213
|
+
cr.add_argument("--file", type=str, help="Path to a file containing reply content")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cmd_comments_list(client, args):
|
|
217
|
+
if client.dry_run:
|
|
218
|
+
return {"dry_run": True, "action": "list_comments", "task_id": args.task_id}
|
|
219
|
+
|
|
220
|
+
resp = client.get_v2(f"/task/{args.task_id}/comment")
|
|
221
|
+
comments = resp.get("comments", [])
|
|
222
|
+
|
|
223
|
+
if not args.fetch_all or len(comments) < 25:
|
|
224
|
+
return {"comments": comments, "count": len(comments)}
|
|
225
|
+
|
|
226
|
+
# Paginate through all comments
|
|
227
|
+
all_comments = fetch_all_comments(client, args.task_id)
|
|
228
|
+
return {"comments": all_comments, "count": len(all_comments)}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_comments_add(client, args):
|
|
232
|
+
text = read_content(args.text, args.file, "--text")
|
|
233
|
+
if not text:
|
|
234
|
+
error("Provide comment text via --text or --file")
|
|
235
|
+
body = {"comment_text": text, "notify_all": False}
|
|
236
|
+
return client.post_v2(f"/task/{args.task_id}/comment", data=body)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_comments_update(client, args):
|
|
240
|
+
"""Update an existing comment's text or resolved status."""
|
|
241
|
+
text = read_content(args.text, args.file, "--text")
|
|
242
|
+
body = {}
|
|
243
|
+
if text:
|
|
244
|
+
body["comment_text"] = text
|
|
245
|
+
if args.resolved is not None:
|
|
246
|
+
body["resolved"] = args.resolved
|
|
247
|
+
if not body:
|
|
248
|
+
error("Nothing to update — provide at least one of: --text, --file, --resolve, --unresolve")
|
|
249
|
+
if client.dry_run:
|
|
250
|
+
return {"dry_run": True, "action": "update", "comment_id": args.comment_id, "body": body}
|
|
251
|
+
client.put_v2(f"/comment/{args.comment_id}", data=body)
|
|
252
|
+
return {"status": "ok", "action": "updated", "comment_id": args.comment_id}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def cmd_comments_delete(client, args):
|
|
256
|
+
"""Delete a comment by ID."""
|
|
257
|
+
if client.dry_run:
|
|
258
|
+
return {"dry_run": True, "action": "delete", "comment_id": args.comment_id}
|
|
259
|
+
client.delete_v2(f"/comment/{args.comment_id}")
|
|
260
|
+
return {"status": "ok", "action": "deleted", "comment_id": args.comment_id}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_comments_thread(client, args):
|
|
264
|
+
"""Get threaded replies to a comment."""
|
|
265
|
+
resp = client.get_v2(f"/comment/{args.comment_id}/reply")
|
|
266
|
+
replies = resp.get("comments", resp if isinstance(resp, list) else [])
|
|
267
|
+
return {"replies": replies, "count": len(replies)}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_comments_reply(client, args):
|
|
271
|
+
"""Reply to a comment (threaded)."""
|
|
272
|
+
text = read_content(args.text, args.file, "--text")
|
|
273
|
+
if not text:
|
|
274
|
+
error("Provide reply text via --text or --file")
|
|
275
|
+
body = {"comment_text": text, "notify_all": False}
|
|
276
|
+
if client.dry_run:
|
|
277
|
+
return {"dry_run": True, "action": "reply", "comment_id": args.comment_id, "body": body}
|
|
278
|
+
return client.post_v2(f"/comment/{args.comment_id}/reply", data=body)
|