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.
@@ -0,0 +1,441 @@
1
+ """Doc command handlers — list, get, create, pages, get-page, edit-page, create-page."""
2
+
3
+ from ..config import WORKSPACE_ID, SPACES
4
+ from ..helpers import read_content, error, add_id_argument
5
+
6
+
7
+ def register_parser(subparsers, F):
8
+ """Register all docs subcommands on the given subparsers object."""
9
+ docs_parser = subparsers.add_parser(
10
+ "docs",
11
+ formatter_class=F,
12
+ help="Create docs, list, read, and edit docs and pages",
13
+ description="""\
14
+ Manage ClickUp docs — create docs, list docs, inspect pages, edit and create pages.
15
+
16
+ Subcommands:
17
+ list — list docs in the workspace or a specific space
18
+ get — fetch one doc by ID
19
+ create — create a new doc in a space (mutating, v3)
20
+ pages — list pages in a doc (use this to discover page IDs)
21
+ get-page — fetch one page by doc ID and page ID
22
+ edit-page — edit an existing page (mutating)
23
+ create-page — create a new page in a doc (mutating)
24
+
25
+ Important: doc ID is not the same as page ID. After finding a doc,
26
+ use 'docs pages' to discover page IDs before using get-page or edit-page.
27
+
28
+ Does not cover: renaming docs, or deleting docs/pages
29
+ (these are not supported by the ClickUp API).""",
30
+ epilog="""\
31
+ typical workflow:
32
+ 1. clickup docs list — find the doc ID
33
+ 2. clickup docs pages <doc_id> — find the page ID
34
+ 3. clickup docs get-page <doc_id> <page_id> — read the page
35
+ 4. clickup docs edit-page <doc_id> <page_id> --content-file new.md
36
+
37
+ examples:
38
+ clickup docs list --space <name>
39
+ clickup docs pages doc_abc123
40
+ clickup --dry-run docs edit-page doc_abc page_xyz --content "Updated" """,
41
+ )
42
+ docs_sub = docs_parser.add_subparsers(dest="command", required=True)
43
+
44
+ # docs list
45
+ dl = docs_sub.add_parser(
46
+ "list",
47
+ formatter_class=F,
48
+ help="List docs in the workspace or a space",
49
+ description="""\
50
+ List docs in the workspace. Optionally filter by space.
51
+ Results are paginated internally and returned as a single JSON object.""",
52
+ epilog="""\
53
+ returns:
54
+ {"docs": [...], "count": N}
55
+
56
+ examples:
57
+ clickup docs list
58
+ clickup docs list --space <name>
59
+ clickup --pretty docs list""",
60
+ )
61
+ dl.add_argument(
62
+ "--space", type=str, help="Filter docs to a specific space"
63
+ )
64
+
65
+ # docs get
66
+ dg = docs_sub.add_parser(
67
+ "get",
68
+ formatter_class=F,
69
+ help="Fetch one doc by ID",
70
+ description="""\
71
+ Fetch a single doc by its ClickUp doc ID.
72
+
73
+ Use this when you need metadata about a doc. To read page content,
74
+ use 'docs pages' followed by 'docs get-page'.""",
75
+ epilog="""\
76
+ returns:
77
+ One doc JSON object.
78
+
79
+ examples:
80
+ clickup docs get doc_abc123
81
+ clickup --pretty docs get doc_abc123""",
82
+ )
83
+ add_id_argument(dg, "doc_id", "ClickUp doc ID")
84
+
85
+ # docs create
86
+ dc = docs_sub.add_parser(
87
+ "create",
88
+ formatter_class=F,
89
+ help="Create a new doc in a space",
90
+ description="""\
91
+ Create a new doc in a space. This is a mutating command (v3 API).
92
+
93
+ Optionally provide initial content via --content or --content-file.
94
+ If content is provided, it is written to the auto-created default page
95
+ automatically — no need for a separate edit-page call.
96
+
97
+ Use --dry-run to preview the request body without creating the doc.
98
+ Global flags may appear before or after the command group:
99
+ clickup --dry-run docs create --space <name> --name "My doc" """,
100
+ epilog="""\
101
+ returns:
102
+ The created doc object from the API. If content was written, includes
103
+ _initial_content_written: true and _page_id fields.
104
+
105
+ examples:
106
+ clickup docs create --space <name> --name "Sprint notes"
107
+ clickup docs create --space <name> --name "API spec" --content-file spec.md
108
+ clickup docs create --space <name> --name "Journal" --content "# Entry"
109
+ clickup --dry-run docs create --space <name> --name "Test doc"
110
+
111
+ notes:
112
+ --content and --content-file are mutually exclusive. Using both is an error.
113
+ Does not support: renaming or deleting docs (ClickUp API limitation).""",
114
+ )
115
+ dc.add_argument(
116
+ "--space",
117
+ required=True,
118
+ type=str,
119
+ help="Space to create the doc in (required)",
120
+ )
121
+ dc.add_argument("--name", required=True, help="Document name (required)")
122
+ dc.add_argument(
123
+ "--content",
124
+ type=str,
125
+ help="Inline initial content as markdown (mutually exclusive with --content-file)",
126
+ )
127
+ dc.add_argument(
128
+ "--content-file", type=str, help="Path to a file containing initial content"
129
+ )
130
+ dc.add_argument(
131
+ "--visibility", type=str, help="Doc visibility (e.g. 'PRIVATE', 'PUBLIC')"
132
+ )
133
+
134
+ # docs pages
135
+ dp = docs_sub.add_parser(
136
+ "pages",
137
+ formatter_class=F,
138
+ help="List pages in a doc (use this to discover page IDs)",
139
+ description="""\
140
+ List all pages in a doc. Returns page metadata including page IDs.
141
+
142
+ Use this to discover valid page IDs before calling get-page or edit-page.
143
+ Doc ID is not the same as page ID — this command bridges the gap.""",
144
+ epilog="""\
145
+ returns:
146
+ A JSON array of page objects with id, name, and content fields.
147
+
148
+ examples:
149
+ clickup docs pages doc_abc123
150
+ clickup --pretty docs pages doc_abc123
151
+
152
+ notes:
153
+ Always run this before get-page or edit-page if you do not already
154
+ have the page ID. Using the doc ID as a page ID will fail.""",
155
+ )
156
+ add_id_argument(dp, "doc_id", "ClickUp doc ID")
157
+
158
+ # docs get-page
159
+ dgp = docs_sub.add_parser(
160
+ "get-page",
161
+ formatter_class=F,
162
+ help="Fetch one page by doc ID and page ID",
163
+ description="""\
164
+ Fetch a single page from a doc by its doc ID and page ID.
165
+
166
+ Use docs pages first to discover valid page IDs. The doc ID is not
167
+ a valid page ID — using it as one will fail.
168
+
169
+ Returns one page object with content in the requested format.""",
170
+ epilog="""\
171
+ returns:
172
+ One page JSON object with id, name, and content.
173
+
174
+ examples:
175
+ clickup docs get-page doc_abc page_xyz
176
+ clickup docs get-page doc_abc page_xyz --format plain
177
+ clickup --pretty docs get-page doc_abc page_xyz
178
+
179
+ notes:
180
+ Default format is markdown (md). Use --format plain for plain text.
181
+ Page ID must come from 'docs pages', not from the doc ID itself.""",
182
+ )
183
+ add_id_argument(dgp, "doc_id", "ClickUp doc ID")
184
+ add_id_argument(dgp, "page_id", "Page ID (from 'docs pages')")
185
+ dgp.add_argument(
186
+ "--format",
187
+ choices=["md", "plain"],
188
+ default="md",
189
+ help="Content format: md (default) or plain",
190
+ )
191
+
192
+ # docs edit-page
193
+ dep = docs_sub.add_parser(
194
+ "edit-page",
195
+ formatter_class=F,
196
+ help="Edit an existing page in a doc",
197
+ description="""\
198
+ Edit an existing page in a doc. This is a mutating command.
199
+
200
+ At least one editable field is required: --content, --content-file, or --name.
201
+ If none are provided, the command exits with an error.
202
+
203
+ Use --content for inline text or --content-file for file-based content.
204
+ Do not use both at the same time. Content is sent as markdown.
205
+
206
+ Use --append to keep the existing page content and add the new content at
207
+ the end. In append mode, the CLI reads the current page first, combines
208
+ the content locally, then sends one update request.
209
+
210
+ Use --dry-run to preview the request body without applying changes.
211
+ Global flags may appear before or after the command group:
212
+ clickup --dry-run docs edit-page doc_abc page_xyz --content "New text"
213
+ clickup docs edit-page doc_abc page_xyz --content "New text" --dry-run""",
214
+ epilog="""\
215
+ returns:
216
+ The updated page object from the API.
217
+
218
+ examples:
219
+ clickup docs edit-page doc_abc page_xyz --content "# Updated heading"
220
+ clickup docs edit-page doc_abc page_xyz --content-file revised.md
221
+ clickup docs edit-page doc_abc page_xyz --content-file session.md --append
222
+ clickup docs edit-page doc_abc page_xyz --name "Renamed page"
223
+ clickup --dry-run docs edit-page doc_abc page_xyz --content "Test"
224
+
225
+ notes:
226
+ --content and --content-file are mutually exclusive. Using both is an error.
227
+ --append requires --content or --content-file.
228
+ Does not support: deleting pages (not available in the ClickUp API).
229
+ Use 'docs pages' to find the correct page ID before editing.""",
230
+ )
231
+ add_id_argument(dep, "doc_id", "ClickUp doc ID")
232
+ add_id_argument(dep, "page_id", "Page ID (from 'docs pages')")
233
+ dep.add_argument(
234
+ "--content",
235
+ type=str,
236
+ help="Inline page content as markdown (mutually exclusive with --content-file)",
237
+ )
238
+ dep.add_argument(
239
+ "--content-file", type=str, help="Path to a file containing page content"
240
+ )
241
+ dep.add_argument("--name", type=str, help="New page name (rename)")
242
+ dep.add_argument(
243
+ "--append",
244
+ action="store_true",
245
+ help="Append new content to the existing page instead of replacing it",
246
+ )
247
+
248
+ # docs create-page
249
+ dcp = docs_sub.add_parser(
250
+ "create-page",
251
+ formatter_class=F,
252
+ help="Create a new page in a doc",
253
+ description="""\
254
+ Create a new page inside an existing doc. This is a mutating command.
255
+
256
+ Use --content for inline text or --content-file for file-based content.
257
+ Do not use both at the same time.
258
+
259
+ Note: when a doc is first created, it already has a blank default page.
260
+ If you want to write to that existing page, use 'docs edit-page' instead
261
+ of creating an additional page.
262
+
263
+ Use --dry-run to preview the request body without creating the page.
264
+ Global flags may appear before or after the command group:
265
+ clickup --dry-run docs create-page doc_abc --name "New page" """,
266
+ epilog="""\
267
+ returns:
268
+ The created page object from the API.
269
+
270
+ examples:
271
+ clickup docs create-page doc_abc --name "Meeting notes"
272
+ clickup docs create-page doc_abc --name "Spec" --content-file spec.md
273
+ clickup --dry-run docs create-page doc_abc --name "Draft"
274
+
275
+ notes:
276
+ --content and --content-file are mutually exclusive. Using both is an error.
277
+ A newly created doc already has a default blank page. Use edit-page to
278
+ write to that page instead of creating a duplicate.
279
+ Does not support: deleting pages (not available in the ClickUp API).""",
280
+ )
281
+ add_id_argument(dcp, "doc_id", "ClickUp doc ID")
282
+ dcp.add_argument("--name", required=True, help="Page name (required)")
283
+ dcp.add_argument(
284
+ "--content",
285
+ type=str,
286
+ help="Inline page content as markdown (mutually exclusive with --content-file)",
287
+ )
288
+ dcp.add_argument(
289
+ "--content-file", type=str, help="Path to a file containing page content"
290
+ )
291
+
292
+
293
+ def _append_markdown(existing, new_content):
294
+ """Append markdown content with a readable separator when both sides exist."""
295
+ existing = existing or ""
296
+ new_content = new_content or ""
297
+
298
+ if not existing:
299
+ return new_content
300
+ if not new_content:
301
+ return existing
302
+
303
+ return f"{existing.rstrip()}\n\n{new_content.lstrip()}"
304
+
305
+
306
+ def cmd_docs_list(client, args):
307
+ if client.dry_run:
308
+ return {"dry_run": True, "action": "list_docs", "space": getattr(args, "space", None)}
309
+
310
+ params = {"limit": "100"}
311
+ if args.space:
312
+ space = SPACES.get(args.space)
313
+ if space:
314
+ params["parent_id"] = space["space_id"]
315
+ params["parent_type"] = "SPACE"
316
+
317
+ all_docs = []
318
+ cursor = None
319
+ while True:
320
+ if cursor:
321
+ params["cursor"] = cursor
322
+ resp = client.get_v3(f"/workspaces/{WORKSPACE_ID}/docs", params=params)
323
+ docs = resp.get("docs", [])
324
+ all_docs.extend(docs)
325
+ cursor = resp.get("next_cursor")
326
+ if not cursor:
327
+ break
328
+
329
+ return {"docs": all_docs, "count": len(all_docs)}
330
+
331
+
332
+ def cmd_docs_get(client, args):
333
+ return client.get_v3(f"/workspaces/{WORKSPACE_ID}/docs/{args.doc_id}")
334
+
335
+
336
+ def cmd_docs_create(client, args):
337
+ """Create a new doc in a space."""
338
+ content = read_content(args.content, args.content_file, "--content")
339
+ space = SPACES.get(args.space)
340
+ if not space:
341
+ error(f"Unknown space: {args.space}. Check your config file.")
342
+
343
+ body = {"name": args.name}
344
+ body["parent"] = {"id": space["space_id"], "type": 4}
345
+ if args.visibility:
346
+ body["visibility"] = args.visibility
347
+
348
+ if client.dry_run:
349
+ return {"dry_run": True, "action": "create_doc", "body": body}
350
+
351
+ doc = client.post_v3(f"/workspaces/{WORKSPACE_ID}/docs", data=body)
352
+
353
+ # If content provided, write it to the auto-created default page
354
+ if content and not client.dry_run:
355
+ doc_id = doc.get("id")
356
+ if doc_id:
357
+ pages = client.get_v3(
358
+ f"/workspaces/{WORKSPACE_ID}/docs/{doc_id}/pages",
359
+ params={"content_format": "text/md"},
360
+ )
361
+ page_list = pages if isinstance(pages, list) else pages.get("pages", [])
362
+ if page_list:
363
+ page_id = page_list[0].get("id")
364
+ if page_id:
365
+ client.put_v3(
366
+ f"/workspaces/{WORKSPACE_ID}/docs/{doc_id}/pages/{page_id}",
367
+ data={"content": content, "content_format": "text/md"},
368
+ )
369
+ doc["_initial_content_written"] = True
370
+ doc["_page_id"] = page_id
371
+
372
+ return doc
373
+
374
+
375
+ def cmd_docs_pages(client, args):
376
+ params = {"content_format": "text/md"}
377
+ return client.get_v3(
378
+ f"/workspaces/{WORKSPACE_ID}/docs/{args.doc_id}/pages", params=params
379
+ )
380
+
381
+
382
+ def cmd_docs_get_page(client, args):
383
+ fmt = "text/md" if args.format == "md" else "text/plain"
384
+ params = {"content_format": fmt}
385
+ return client.get_v3(
386
+ f"/workspaces/{WORKSPACE_ID}/docs/{args.doc_id}/pages/{args.page_id}",
387
+ params=params,
388
+ )
389
+
390
+
391
+ def cmd_docs_edit_page(client, args):
392
+ content = read_content(args.content, args.content_file, "--content")
393
+ body = {}
394
+
395
+ if getattr(args, "append", False):
396
+ if not content:
397
+ error("--append requires --content or --content-file")
398
+ page = client.get_v3(
399
+ f"/workspaces/{WORKSPACE_ID}/docs/{args.doc_id}/pages/{args.page_id}",
400
+ params={"content_format": "text/md"},
401
+ allow_dry_run=True,
402
+ )
403
+ content = _append_markdown(page.get("content", ""), content)
404
+
405
+ if content:
406
+ body["content"] = content
407
+ body["content_format"] = "text/md"
408
+ if args.name:
409
+ body["name"] = args.name
410
+
411
+ if not body:
412
+ error(
413
+ "Nothing to update — provide at least one of: --content, --content-file, --name"
414
+ )
415
+
416
+ if client.dry_run:
417
+ return {
418
+ "dry_run": True,
419
+ "action": "edit_page",
420
+ "doc_id": args.doc_id,
421
+ "page_id": args.page_id,
422
+ "body": body,
423
+ }
424
+
425
+ return client.put_v3(
426
+ f"/workspaces/{WORKSPACE_ID}/docs/{args.doc_id}/pages/{args.page_id}",
427
+ data=body,
428
+ )
429
+
430
+
431
+ def cmd_docs_create_page(client, args):
432
+ content = read_content(args.content, args.content_file, "--content")
433
+ body = {"name": args.name}
434
+ if content:
435
+ body["content"] = content
436
+ body["content_format"] = "text/md"
437
+
438
+ return client.post_v3(
439
+ f"/workspaces/{WORKSPACE_ID}/docs/{args.doc_id}/pages",
440
+ data=body,
441
+ )
@@ -0,0 +1,202 @@
1
+ """Folder command handlers — list, get, create, update, delete."""
2
+
3
+ from ..helpers import error, resolve_space_id, add_id_argument
4
+
5
+
6
+ def register_parser(subparsers, F):
7
+ """Register all folders subcommands on the given subparsers object."""
8
+ folders_parser = subparsers.add_parser(
9
+ "folders",
10
+ formatter_class=F,
11
+ help="Full folder CRUD: list, get, create, update, delete",
12
+ description="""\
13
+ Manage ClickUp folders — organize lists within spaces.
14
+
15
+ Folders are containers that sit between spaces and lists. Use them to
16
+ group related lists together (e.g. a "Sprint 1" folder under a space).
17
+
18
+ Subcommands:
19
+ list — list all folders in a space
20
+ get — fetch full details of a folder by ID
21
+ create — create a new folder in a space (mutating)
22
+ update — update a folder's name (mutating)
23
+ delete — delete a folder (destructive)
24
+
25
+ Does not cover: reordering folders or setting folder-level statuses
26
+ (use the ClickUp UI for these).""",
27
+ epilog="""\
28
+ examples:
29
+ clickup folders list --space <name>
30
+ clickup folders get 12345
31
+ clickup --dry-run folders create --space <name> --name "My folder"
32
+ clickup folders update 12345 --name "Renamed folder"
33
+ clickup --dry-run folders delete 12345""",
34
+ )
35
+ folders_sub = folders_parser.add_subparsers(dest="command", required=True)
36
+
37
+ # folders list
38
+ fl = folders_sub.add_parser(
39
+ "list",
40
+ formatter_class=F,
41
+ help="List all folders in a space",
42
+ description="""\
43
+ List all folders in a space. Returns folder names, IDs, and metadata.
44
+
45
+ Use this to discover folder IDs before creating lists inside them
46
+ or to see the organizational structure of a space.""",
47
+ epilog="""\
48
+ returns:
49
+ {"folders": [...], "count": N}
50
+
51
+ examples:
52
+ clickup folders list --space <name>
53
+ clickup folders list --space 901810200000
54
+ clickup --pretty folders list --space <name>""",
55
+ )
56
+ fl.add_argument(
57
+ "--space",
58
+ required=True,
59
+ type=str,
60
+ help="Space name (from config) or raw space ID",
61
+ )
62
+
63
+ # folders get
64
+ fg = folders_sub.add_parser(
65
+ "get",
66
+ formatter_class=F,
67
+ help="Fetch full details of a folder by ID",
68
+ description="""\
69
+ Fetch full details of a folder including its lists, statuses, and metadata.
70
+
71
+ Use this when you need to inspect a specific folder or discover the
72
+ lists inside it.""",
73
+ epilog="""\
74
+ returns:
75
+ One folder JSON object with all fields (id, name, lists, statuses, etc.)
76
+
77
+ examples:
78
+ clickup folders get 12345
79
+ clickup --pretty folders get 12345""",
80
+ )
81
+ add_id_argument(fg, "folder_id", "ClickUp folder ID")
82
+
83
+ # folders create
84
+ fc = folders_sub.add_parser(
85
+ "create",
86
+ formatter_class=F,
87
+ help="Create a new folder in a space",
88
+ description="""\
89
+ Create a new folder in a space. This is a mutating command.
90
+
91
+ Use --dry-run to preview the request body without creating the folder.
92
+ Global flags may appear before or after the command group:
93
+ clickup --dry-run folders create --space <name> --name "My folder" """,
94
+ epilog="""\
95
+ returns:
96
+ The created folder object from the API.
97
+
98
+ examples:
99
+ clickup folders create --space <name> --name "My folder"
100
+ clickup --dry-run folders create --space <name> --name "Test folder" """,
101
+ )
102
+ fc.add_argument(
103
+ "--space",
104
+ required=True,
105
+ type=str,
106
+ help="Space name (from config) or raw space ID",
107
+ )
108
+ fc.add_argument("--name", required=True, help="Folder name (required)")
109
+
110
+ # folders update
111
+ fu = folders_sub.add_parser(
112
+ "update",
113
+ formatter_class=F,
114
+ help="Update a folder (name)",
115
+ description="""\
116
+ Update a folder's name. This is a mutating command.
117
+
118
+ Use --dry-run to preview without applying changes.
119
+ Global flags may appear before or after the command group:
120
+ clickup --dry-run folders update 12345 --name "New name" """,
121
+ epilog="""\
122
+ returns:
123
+ The updated folder object from the API.
124
+
125
+ examples:
126
+ clickup folders update 12345 --name "Renamed folder"
127
+ clickup --dry-run folders update 12345 --name "Test rename" """,
128
+ )
129
+ add_id_argument(fu, "folder_id", "ClickUp folder ID to update")
130
+ fu.add_argument("--name", type=str, help="New folder name")
131
+
132
+ # folders delete
133
+ fd = folders_sub.add_parser(
134
+ "delete",
135
+ formatter_class=F,
136
+ help="Delete a folder (destructive)",
137
+ description="""\
138
+ Delete a folder permanently. This is a destructive, irreversible command.
139
+
140
+ Deleting a folder also deletes all lists and tasks inside it.
141
+ Use with extreme caution.
142
+
143
+ Use --dry-run to preview the operation without deleting anything.
144
+ Global flags may appear before or after the command group:
145
+ clickup --dry-run folders delete 12345""",
146
+ epilog="""\
147
+ returns:
148
+ {"status": "ok", "action": "deleted", "folder_id": "..."}
149
+
150
+ examples:
151
+ clickup --dry-run folders delete 12345
152
+ clickup folders delete 12345""",
153
+ )
154
+ add_id_argument(fd, "folder_id", "ClickUp folder ID to delete")
155
+
156
+
157
+ def cmd_folders_list(client, args):
158
+ """List all folders in a space."""
159
+ space_id = resolve_space_id(args.space)
160
+ resp = client.get_v2(f"/space/{space_id}/folder")
161
+ folders = resp.get("folders", [])
162
+ return {"folders": folders, "count": len(folders)}
163
+
164
+
165
+ def cmd_folders_get(client, args):
166
+ """Get full details of a folder by ID."""
167
+ return client.get_v2(f"/folder/{args.folder_id}")
168
+
169
+
170
+ def cmd_folders_create(client, args):
171
+ """Create a folder in a space."""
172
+ space_id = resolve_space_id(args.space)
173
+ body = {"name": args.name}
174
+
175
+ if client.dry_run:
176
+ return {"dry_run": True, "action": "create_folder", "space_id": space_id, "body": body}
177
+
178
+ return client.post_v2(f"/space/{space_id}/folder", data=body)
179
+
180
+
181
+ def cmd_folders_update(client, args):
182
+ """Update a folder (name)."""
183
+ body = {}
184
+ if args.name:
185
+ body["name"] = args.name
186
+
187
+ if not body:
188
+ error("Nothing to update — provide at least --name")
189
+
190
+ if client.dry_run:
191
+ return {"dry_run": True, "action": "update_folder", "folder_id": args.folder_id, "body": body}
192
+
193
+ return client.put_v2(f"/folder/{args.folder_id}", data=body)
194
+
195
+
196
+ def cmd_folders_delete(client, args):
197
+ """Delete a folder by ID."""
198
+ if client.dry_run:
199
+ return {"dry_run": True, "action": "delete_folder", "folder_id": args.folder_id}
200
+
201
+ client.delete_v2(f"/folder/{args.folder_id}")
202
+ return {"status": "ok", "action": "deleted", "folder_id": args.folder_id}