@krodak/clickup-cli 1.0.0 → 1.2.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clickup-cli",
3
3
  "description": "ClickUp CLI skills for managing tasks, sprints, comments, checklists, custom fields, tags, and time tracking via the cup command",
4
- "version": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/README.md CHANGED
@@ -29,6 +29,10 @@ Install the CLI, add the skill file to your agent, and it works with ClickUp. No
29
29
 
30
30
  The agent reads the skill file, picks the right `cup` commands, and handles everything. You don't need to learn the CLI - the agent does.
31
31
 
32
+ ## Why a CLI and not MCP?
33
+
34
+ A CLI + skill file has fewer moving parts. No server process, no protocol layer. The agent already knows how to run shell commands - the skill file teaches it which ones exist. For tool-use with coding agents, CLI + instructions tends to work better than MCP in practice.
35
+
32
36
  ## Install
33
37
 
34
38
  You need Node 22+ and a ClickUp personal API token (`pk_...` from [ClickUp Settings > Apps](https://app.clickup.com/settings/apps)).
@@ -228,7 +232,7 @@ Status: :white_check_mark: implemented | :construction: planned | :no_entry_sign
228
232
  | List spaces | `cup spaces` | :white_check_mark: |
229
233
  | List lists | `cup lists <spaceId>` | :white_check_mark: |
230
234
  | Check auth | `cup auth` | :white_check_mark: |
231
- | List folders | `cup folders <spaceId>` | :construction: |
235
+ | List folders | `cup folders <spaceId>` | :white_check_mark: |
232
236
  | List members | `cup members` | :construction: |
233
237
 
234
238
  ### Goals & Key Results
@@ -241,10 +245,14 @@ Status: :white_check_mark: implemented | :construction: planned | :no_entry_sign
241
245
 
242
246
  ### Docs
243
247
 
244
- | Feature | Command | Status |
245
- | ----------------- | ------------------ | -------------- |
246
- | Search docs | `cup docs <query>` | :construction: |
247
- | View page content | `cup doc <id>` | :construction: |
248
+ | Feature | Command | Status |
249
+ | ---------------- | ------------------------------------ | ------------------ |
250
+ | Search docs | `cup docs [query]` | :white_check_mark: |
251
+ | View doc / page | `cup doc <docId> [pageId]` | :white_check_mark: |
252
+ | All page content | `cup doc-pages <docId>` | :white_check_mark: |
253
+ | Create doc | `cup doc-create <title>` | :white_check_mark: |
254
+ | Create page | `cup doc-page-create <docId> <name>` | :white_check_mark: |
255
+ | Edit page | `cup doc-page-edit <docId> <pageId>` | :white_check_mark: |
248
256
 
249
257
  ### Attachments
250
258
 
@@ -338,10 +346,6 @@ Custom ID resolution uses the `teamId` from your config, which is required (`cup
338
346
 
339
347
  **Task links with custom IDs:** The `cup link` command passes both task IDs in a single API request. When both IDs are custom, this works correctly. However, mixing custom and native IDs in a single link command may not work as expected because the ClickUp API applies the `custom_task_ids` flag to all IDs in the request.
340
348
 
341
- ## Why a CLI and not MCP?
342
-
343
- A CLI + skill file has fewer moving parts. No server process, no protocol layer. The agent already knows how to run shell commands - the skill file teaches it which ones exist. For tool-use with coding agents, CLI + instructions tends to work better than MCP in practice.
344
-
345
349
  ## Development
346
350
 
347
351
  ```bash
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { createRequire } from "module";
7
7
 
8
8
  // src/api.ts
9
9
  var BASE_URL = "https://api.clickup.com/api/v2";
10
+ var BASE_URL_V3 = "https://api.clickup.com/api/v3";
10
11
  var MAX_PAGES = 100;
11
12
  function isCustomTaskId(id) {
12
13
  return /^[A-Z]+-\d+$/i.test(id);
@@ -56,6 +57,29 @@ var ClickUpClient = class {
56
57
  }
57
58
  return data;
58
59
  }
60
+ async requestV3(path, options = {}) {
61
+ const res = await fetch(`${BASE_URL_V3}${path}`, {
62
+ ...options,
63
+ signal: AbortSignal.timeout(3e4),
64
+ headers: {
65
+ Authorization: this.apiToken,
66
+ ...options.body ? { "Content-Type": "application/json" } : {},
67
+ ...options.headers
68
+ }
69
+ });
70
+ let data;
71
+ try {
72
+ data = await res.json();
73
+ } catch {
74
+ throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
75
+ }
76
+ if (!res.ok) {
77
+ const raw = data.err ?? data.error ?? data.ECODE ?? res.statusText;
78
+ const msg = typeof raw === "string" ? raw : JSON.stringify(raw);
79
+ throw new Error(`ClickUp API error ${res.status}: ${msg}`);
80
+ }
81
+ return data;
82
+ }
59
83
  async getMe() {
60
84
  if (this.meCache) return this.meCache;
61
85
  const data = await this.request("/user");
@@ -383,6 +407,57 @@ var ClickUpClient = class {
383
407
  }
384
408
  return data;
385
409
  }
410
+ async getDocs(workspaceId) {
411
+ const data = await this.requestV3(`/workspaces/${workspaceId}/docs`);
412
+ return data.docs ?? [];
413
+ }
414
+ async getDocPage(workspaceId, docId, pageId) {
415
+ return this.requestV3(
416
+ `/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}?content_format=text/md`
417
+ );
418
+ }
419
+ async createDoc(workspaceId, title, content, parentId) {
420
+ const body = { title };
421
+ if (content) body.content = content;
422
+ if (parentId) {
423
+ body.parent_id = parentId;
424
+ body.parent_type = "doc";
425
+ }
426
+ return this.requestV3(`/workspaces/${workspaceId}/docs`, {
427
+ method: "POST",
428
+ body: JSON.stringify(body)
429
+ });
430
+ }
431
+ async createDocPage(workspaceId, docId, name, content, parentPageId) {
432
+ const body = { name, content_format: "text/md" };
433
+ if (content) body.content = content;
434
+ if (parentPageId) body.parent_page_id = parentPageId;
435
+ return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages`, {
436
+ method: "POST",
437
+ body: JSON.stringify(body)
438
+ });
439
+ }
440
+ async editDocPage(workspaceId, docId, pageId, updates) {
441
+ return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`, {
442
+ method: "PUT",
443
+ body: JSON.stringify(updates)
444
+ });
445
+ }
446
+ async getDoc(workspaceId, docId) {
447
+ return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}`);
448
+ }
449
+ async getDocPageListing(workspaceId, docId) {
450
+ const data = await this.requestV3(
451
+ `/workspaces/${workspaceId}/docs/${docId}/pagelisting`
452
+ );
453
+ return data.pages ?? [];
454
+ }
455
+ async getDocPages(workspaceId, docId) {
456
+ const data = await this.requestV3(
457
+ `/workspaces/${workspaceId}/docs/${docId}/pages?content_format=text/md`
458
+ );
459
+ return data.pages ?? [];
460
+ }
386
461
  };
387
462
 
388
463
  // src/config.ts
@@ -2031,7 +2106,7 @@ function bashCompletion(name) {
2031
2106
  cword=$COMP_CWORD
2032
2107
  fi
2033
2108
 
2034
- local commands="init auth tasks task update create sprint sprints subtasks comment comment-edit comment-delete comments replies reply activity lists spaces inbox assigned open search summary overdue assign depend link attach move field delete tag checklist time config completion"
2109
+ local commands="init auth tasks task update create sprint sprints subtasks comment comment-edit comment-delete comments replies reply activity lists spaces inbox assigned open search summary overdue assign depend link attach move field delete tag checklist time docs doc doc-create doc-pages doc-page-create doc-page-edit folders config completion"
2035
2110
 
2036
2111
  if [[ $cword -eq 1 ]]; then
2037
2112
  COMPREPLY=($(compgen -W "$commands --help --version" -- "$cur"))
@@ -2155,6 +2230,27 @@ function bashCompletion(name) {
2155
2230
  attach)
2156
2231
  COMPREPLY=($(compgen -f -- "$cur"))
2157
2232
  ;;
2233
+ docs)
2234
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2235
+ ;;
2236
+ doc)
2237
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2238
+ ;;
2239
+ doc-pages)
2240
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2241
+ ;;
2242
+ folders)
2243
+ COMPREPLY=($(compgen -W "--name --json" -- "$cur"))
2244
+ ;;
2245
+ doc-create)
2246
+ COMPREPLY=($(compgen -W "-c --content --json" -- "$cur"))
2247
+ ;;
2248
+ doc-page-create)
2249
+ COMPREPLY=($(compgen -W "-c --content --parent-page --json" -- "$cur"))
2250
+ ;;
2251
+ doc-page-edit)
2252
+ COMPREPLY=($(compgen -W "--name -c --content --json" -- "$cur"))
2253
+ ;;
2158
2254
  config)
2159
2255
  if [[ $cword -eq 2 ]]; then
2160
2256
  COMPREPLY=($(compgen -W "get set path" -- "$cur"))
@@ -2215,6 +2311,13 @@ _${name}() {
2215
2311
  'reply:Reply to a comment'
2216
2312
  'link:Add or remove a link between two tasks'
2217
2313
  'attach:Upload a file attachment to a task'
2314
+ 'docs:List workspace docs'
2315
+ 'doc:View a doc or doc page'
2316
+ 'doc-create:Create a new doc'
2317
+ 'doc-pages:List all pages in a doc with content'
2318
+ 'doc-page-create:Create a page in a doc'
2319
+ 'doc-page-edit:Edit a doc page'
2320
+ 'folders:List folders in a space'
2218
2321
  'config:Manage CLI configuration'
2219
2322
  'completion:Output shell completion script'
2220
2323
  )
@@ -2537,6 +2640,50 @@ _${name}() {
2537
2640
  '2:file_path:_files' \\
2538
2641
  '--json[Force JSON output]'
2539
2642
  ;;
2643
+ docs)
2644
+ _arguments \\
2645
+ '1:query:' \\
2646
+ '--json[Force JSON output]'
2647
+ ;;
2648
+ doc)
2649
+ _arguments \\
2650
+ '1:doc_id:' \\
2651
+ '2:page_id:' \\
2652
+ '--json[Force JSON output]'
2653
+ ;;
2654
+ doc-pages)
2655
+ _arguments \\
2656
+ '1:doc_id:' \\
2657
+ '--json[Force JSON output]'
2658
+ ;;
2659
+ folders)
2660
+ _arguments \\
2661
+ '1:space_id:' \\
2662
+ '--name[Filter by folder name]:text:' \\
2663
+ '--json[Force JSON output]'
2664
+ ;;
2665
+ doc-create)
2666
+ _arguments \\
2667
+ '1:title:' \\
2668
+ '(-c --content)'{-c,--content}'[Initial content]:text:' \\
2669
+ '--json[Force JSON output]'
2670
+ ;;
2671
+ doc-page-create)
2672
+ _arguments \\
2673
+ '1:doc_id:' \\
2674
+ '2:name:' \\
2675
+ '(-c --content)'{-c,--content}'[Page content]:text:' \\
2676
+ '--parent-page[Parent page ID]:page_id:' \\
2677
+ '--json[Force JSON output]'
2678
+ ;;
2679
+ doc-page-edit)
2680
+ _arguments \\
2681
+ '1:doc_id:' \\
2682
+ '2:page_id:' \\
2683
+ '--name[New page name]:text:' \\
2684
+ '(-c --content)'{-c,--content}'[New page content]:text:' \\
2685
+ '--json[Force JSON output]'
2686
+ ;;
2540
2687
  config)
2541
2688
  local -a config_cmds
2542
2689
  config_cmds=(
@@ -2611,6 +2758,13 @@ complete -c ${name} -n __fish_use_subcommand -a replies -d 'List threaded replie
2611
2758
  complete -c ${name} -n __fish_use_subcommand -a reply -d 'Reply to a comment'
2612
2759
  complete -c ${name} -n __fish_use_subcommand -a link -d 'Add or remove a link between two tasks'
2613
2760
  complete -c ${name} -n __fish_use_subcommand -a attach -d 'Upload a file attachment to a task'
2761
+ complete -c ${name} -n __fish_use_subcommand -a docs -d 'List workspace docs'
2762
+ complete -c ${name} -n __fish_use_subcommand -a doc -d 'View a doc or doc page'
2763
+ complete -c ${name} -n __fish_use_subcommand -a doc-create -d 'Create a new doc'
2764
+ complete -c ${name} -n __fish_use_subcommand -a doc-pages -d 'List all pages in a doc with content'
2765
+ complete -c ${name} -n __fish_use_subcommand -a doc-page-create -d 'Create a page in a doc'
2766
+ complete -c ${name} -n __fish_use_subcommand -a doc-page-edit -d 'Edit a doc page'
2767
+ complete -c ${name} -n __fish_use_subcommand -a folders -d 'List folders in a space'
2614
2768
  complete -c ${name} -n __fish_use_subcommand -a config -d 'Manage CLI configuration'
2615
2769
  complete -c ${name} -n __fish_use_subcommand -a completion -d 'Output shell completion script'
2616
2770
 
@@ -2764,6 +2918,26 @@ complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l resolved -d
2764
2918
  complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l unresolved -d 'Mark comment as unresolved'
2765
2919
  complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l json -d 'Force JSON output'
2766
2920
 
2921
+ complete -c ${name} -n '__fish_seen_subcommand_from docs' -l json -d 'Force JSON output'
2922
+
2923
+ complete -c ${name} -n '__fish_seen_subcommand_from doc' -l json -d 'Force JSON output'
2924
+
2925
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-pages' -l json -d 'Force JSON output'
2926
+
2927
+ complete -c ${name} -n '__fish_seen_subcommand_from folders' -l name -d 'Filter by folder name'
2928
+ complete -c ${name} -n '__fish_seen_subcommand_from folders' -l json -d 'Force JSON output'
2929
+
2930
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-create' -s c -l content -d 'Initial content'
2931
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-create' -l json -d 'Force JSON output'
2932
+
2933
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-create' -s c -l content -d 'Page content'
2934
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-create' -l parent-page -d 'Parent page ID'
2935
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-create' -l json -d 'Force JSON output'
2936
+
2937
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-edit' -l name -d 'New page name'
2938
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-edit' -s c -l content -d 'New page content'
2939
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-edit' -l json -d 'Force JSON output'
2940
+
2767
2941
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a get -d 'Print a config value'
2768
2942
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a set -d 'Set a config value'
2769
2943
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a path -d 'Print config file path'
@@ -3142,8 +3316,154 @@ async function attachFile(config, taskId, filePath) {
3142
3316
  return client.createTaskAttachment(taskId, filePath);
3143
3317
  }
3144
3318
 
3145
- // src/commands/time.ts
3319
+ // src/commands/docs.ts
3146
3320
  import chalk7 from "chalk";
3321
+ async function listDocs(config, query) {
3322
+ const client = new ClickUpClient(config);
3323
+ const docs = await client.getDocs(config.teamId);
3324
+ if (query) {
3325
+ const lower = query.toLowerCase();
3326
+ return docs.filter((d) => d.name.toLowerCase().includes(lower));
3327
+ }
3328
+ return docs;
3329
+ }
3330
+ function formatDocs(docs) {
3331
+ if (docs.length === 0) return "No docs found";
3332
+ return docs.map((d) => `${chalk7.bold(d.name)} ${chalk7.dim(d.id)}`).join("\n");
3333
+ }
3334
+ function formatDocsMarkdown(docs) {
3335
+ if (docs.length === 0) return "No docs found";
3336
+ return docs.map((d) => `- **${d.name}** (${d.id})`).join("\n");
3337
+ }
3338
+
3339
+ // src/commands/doc.ts
3340
+ import chalk8 from "chalk";
3341
+ async function getDocInfo(config, docId) {
3342
+ const client = new ClickUpClient(config);
3343
+ const [doc, pages] = await Promise.all([
3344
+ client.getDoc(config.teamId, docId),
3345
+ client.getDocPageListing(config.teamId, docId)
3346
+ ]);
3347
+ return { doc, pages };
3348
+ }
3349
+ function formatDocInfo(doc, pages, indent = 0) {
3350
+ const lines = [];
3351
+ if (indent === 0) {
3352
+ lines.push(`${chalk8.bold(doc.name)} ${chalk8.dim(doc.id)}`);
3353
+ if (pages.length === 0) {
3354
+ lines.push(" (no pages)");
3355
+ }
3356
+ }
3357
+ for (const page of pages) {
3358
+ const prefix = " ".repeat(indent + 1);
3359
+ lines.push(`${prefix}${page.name} ${chalk8.dim(page.id)}`);
3360
+ if (page.pages && page.pages.length > 0) {
3361
+ lines.push(formatDocInfo(doc, page.pages, indent + 1));
3362
+ }
3363
+ }
3364
+ return lines.join("\n");
3365
+ }
3366
+ function formatDocInfoMarkdown(doc, pages, indent = 0) {
3367
+ const lines = [];
3368
+ if (indent === 0) {
3369
+ lines.push(`# ${doc.name}`);
3370
+ lines.push(`ID: ${doc.id}`);
3371
+ lines.push("");
3372
+ if (pages.length === 0) {
3373
+ lines.push("No pages.");
3374
+ return lines.join("\n");
3375
+ }
3376
+ lines.push("## Pages");
3377
+ }
3378
+ for (const page of pages) {
3379
+ const prefix = " ".repeat(indent);
3380
+ lines.push(`${prefix}- **${page.name}** (${page.id})`);
3381
+ if (page.pages && page.pages.length > 0) {
3382
+ lines.push(formatDocInfoMarkdown(doc, page.pages, indent + 1));
3383
+ }
3384
+ }
3385
+ return lines.join("\n");
3386
+ }
3387
+ async function getDocPage(config, docId, pageId) {
3388
+ const client = new ClickUpClient(config);
3389
+ return client.getDocPage(config.teamId, docId, pageId);
3390
+ }
3391
+ async function getAllDocPages(config, docId) {
3392
+ const client = new ClickUpClient(config);
3393
+ return client.getDocPages(config.teamId, docId);
3394
+ }
3395
+ function formatDocPages(pages) {
3396
+ if (pages.length === 0) return "No pages found";
3397
+ return pages.map((p) => {
3398
+ const header = `# ${p.name}
3399
+ `;
3400
+ return header + (p.content ?? "");
3401
+ }).join("\n\n---\n\n");
3402
+ }
3403
+ function formatDocPagesMarkdown(pages) {
3404
+ if (pages.length === 0) return "No pages found";
3405
+ return pages.map((p) => {
3406
+ const header = `# ${p.name}`;
3407
+ return header + "\n\n" + (p.content ?? "");
3408
+ }).join("\n\n---\n\n");
3409
+ }
3410
+ async function createDoc(config, title, content) {
3411
+ if (!title.trim()) throw new Error("Doc title cannot be empty");
3412
+ const client = new ClickUpClient(config);
3413
+ const doc = await client.createDoc(config.teamId, title, content);
3414
+ return { id: doc.id, title: doc.name ?? title };
3415
+ }
3416
+ async function createDocPage(config, docId, name, content, parentPageId) {
3417
+ if (!name.trim()) throw new Error("Page name cannot be empty");
3418
+ const client = new ClickUpClient(config);
3419
+ return client.createDocPage(config.teamId, docId, name, content, parentPageId);
3420
+ }
3421
+ async function editDocPage(config, docId, pageId, updates) {
3422
+ if (!updates.name && !updates.content) {
3423
+ throw new Error("Provide --name or --content to update");
3424
+ }
3425
+ const client = new ClickUpClient(config);
3426
+ return client.editDocPage(config.teamId, docId, pageId, updates);
3427
+ }
3428
+
3429
+ // src/commands/folders.ts
3430
+ import chalk9 from "chalk";
3431
+ async function listFolders(config, spaceId, nameFilter) {
3432
+ const client = new ClickUpClient(config);
3433
+ const folders = await client.getFolders(spaceId);
3434
+ let filtered = folders;
3435
+ if (nameFilter) {
3436
+ const lower = nameFilter.toLowerCase();
3437
+ filtered = folders.filter((f) => f.name.toLowerCase().includes(lower));
3438
+ }
3439
+ const results = [];
3440
+ for (const folder of filtered) {
3441
+ const lists = await client.getFolderLists(folder.id);
3442
+ results.push({ id: folder.id, name: folder.name, lists });
3443
+ }
3444
+ return results;
3445
+ }
3446
+ function formatFolders(folders) {
3447
+ if (folders.length === 0) return "No folders found";
3448
+ return folders.map((f) => {
3449
+ const header = `${chalk9.bold(f.name)} ${chalk9.dim(f.id)}`;
3450
+ if (f.lists.length === 0) return header;
3451
+ const listLines = f.lists.map((l) => ` ${l.name} ${chalk9.dim(l.id)}`);
3452
+ return [header, ...listLines].join("\n");
3453
+ }).join("\n\n");
3454
+ }
3455
+ function formatFoldersMarkdown(folders) {
3456
+ if (folders.length === 0) return "No folders found";
3457
+ return folders.map((f) => {
3458
+ const header = `- **${f.name}** (${f.id})`;
3459
+ if (f.lists.length === 0) return header;
3460
+ const listLines = f.lists.map((l) => ` - ${l.name} (${l.id})`);
3461
+ return [header, ...listLines].join("\n");
3462
+ }).join("\n");
3463
+ }
3464
+
3465
+ // src/commands/time.ts
3466
+ import chalk10 from "chalk";
3147
3467
  async function startTimer(config, taskId, description) {
3148
3468
  const client = new ClickUpClient(config);
3149
3469
  return client.startTimeEntry(config.teamId, taskId, description);
@@ -3179,8 +3499,8 @@ function formatTimeEntry(entry) {
3179
3499
  const isRunning = entry.duration < 0;
3180
3500
  const elapsed = isRunning ? Date.now() - Number(entry.start) : entry.duration;
3181
3501
  const durationStr = formatDuration(elapsed);
3182
- const status = isRunning ? chalk7.green("RUNNING") : "";
3183
- lines.push(`${chalk7.bold(taskName)} ${chalk7.dim(taskId)} ${status}`);
3502
+ const status = isRunning ? chalk10.green("RUNNING") : "";
3503
+ lines.push(`${chalk10.bold(taskName)} ${chalk10.dim(taskId)} ${status}`);
3184
3504
  lines.push(
3185
3505
  ` ${durationStr} - ${formatTimestamp(entry.start)}${entry.description ? ` - ${entry.description}` : ""}`
3186
3506
  );
@@ -3746,6 +4066,109 @@ timeCmd.command("list").description("List recent time entries (default: last 7 d
3746
4066
  }
3747
4067
  })
3748
4068
  );
4069
+ program.command("docs [query]").description("List workspace docs (optionally filter by name)").option("--json", "Force JSON output even in terminal").action(
4070
+ wrapAction(async (query, opts) => {
4071
+ const config = loadConfig();
4072
+ const docs = await listDocs(config, query);
4073
+ if (shouldOutputJson(opts.json ?? false)) {
4074
+ console.log(JSON.stringify(docs, null, 2));
4075
+ } else if (isTTY()) {
4076
+ console.log(formatDocs(docs));
4077
+ } else {
4078
+ console.log(formatDocsMarkdown(docs));
4079
+ }
4080
+ })
4081
+ );
4082
+ program.command("doc <docId> [pageId]").description("View a doc (metadata + page tree) or a specific page").option("--json", "Force JSON output even in terminal").action(
4083
+ wrapAction(async (docId, pageId, opts) => {
4084
+ const config = loadConfig();
4085
+ if (pageId) {
4086
+ const page = await getDocPage(config, docId, pageId);
4087
+ if (shouldOutputJson(opts.json ?? false)) {
4088
+ console.log(JSON.stringify(page, null, 2));
4089
+ } else {
4090
+ if (page.name) console.log(`# ${page.name}
4091
+ `);
4092
+ console.log(page.content ?? "");
4093
+ }
4094
+ } else {
4095
+ const { doc, pages } = await getDocInfo(config, docId);
4096
+ if (shouldOutputJson(opts.json ?? false)) {
4097
+ console.log(JSON.stringify({ ...doc, pages }, null, 2));
4098
+ } else if (isTTY()) {
4099
+ console.log(formatDocInfo(doc, pages));
4100
+ } else {
4101
+ console.log(formatDocInfoMarkdown(doc, pages));
4102
+ }
4103
+ }
4104
+ })
4105
+ );
4106
+ program.command("doc-pages <docId>").description("List all pages in a doc with content").option("--json", "Force JSON output even in terminal").action(
4107
+ wrapAction(async (docId, opts) => {
4108
+ const config = loadConfig();
4109
+ const pages = await getAllDocPages(config, docId);
4110
+ if (shouldOutputJson(opts.json ?? false)) {
4111
+ console.log(JSON.stringify(pages, null, 2));
4112
+ } else if (isTTY()) {
4113
+ console.log(formatDocPages(pages));
4114
+ } else {
4115
+ console.log(formatDocPagesMarkdown(pages));
4116
+ }
4117
+ })
4118
+ );
4119
+ program.command("folders <spaceId>").description("List folders in a space (with their lists)").option("--name <partial>", "Filter folders by partial name match").option("--json", "Force JSON output even in terminal").action(
4120
+ wrapAction(async (spaceId, opts) => {
4121
+ const config = loadConfig();
4122
+ const folders = await listFolders(config, spaceId, opts.name);
4123
+ if (shouldOutputJson(opts.json ?? false)) {
4124
+ console.log(JSON.stringify(folders, null, 2));
4125
+ } else if (isTTY()) {
4126
+ console.log(formatFolders(folders));
4127
+ } else {
4128
+ console.log(formatFoldersMarkdown(folders));
4129
+ }
4130
+ })
4131
+ );
4132
+ program.command("doc-create <title>").description("Create a new doc").option("-c, --content <text>", "Initial content (markdown)").option("--json", "Force JSON output even in terminal").action(
4133
+ wrapAction(async (title, opts) => {
4134
+ const config = loadConfig();
4135
+ const result = await createDoc(config, title, opts.content);
4136
+ if (shouldOutputJson(opts.json ?? false)) {
4137
+ console.log(JSON.stringify(result, null, 2));
4138
+ } else {
4139
+ console.log(`Created doc "${result.title}" (${result.id})`);
4140
+ }
4141
+ })
4142
+ );
4143
+ program.command("doc-page-create <docId> <name>").description("Create a page in a doc").option("-c, --content <text>", "Page content (markdown)").option("--parent-page <pageId>", "Parent page ID for nesting").option("--json", "Force JSON output even in terminal").action(
4144
+ wrapAction(
4145
+ async (docId, name, opts) => {
4146
+ const config = loadConfig();
4147
+ const page = await createDocPage(config, docId, name, opts.content, opts.parentPage);
4148
+ if (shouldOutputJson(opts.json ?? false)) {
4149
+ console.log(JSON.stringify(page, null, 2));
4150
+ } else {
4151
+ console.log(`Created page "${page.name}" (${page.id}) in doc ${docId}`);
4152
+ }
4153
+ }
4154
+ )
4155
+ );
4156
+ program.command("doc-page-edit <docId> <pageId>").description("Edit a doc page").option("--name <text>", "New page name").option("-c, --content <text>", "New page content (markdown)").option("--json", "Force JSON output even in terminal").action(
4157
+ wrapAction(
4158
+ async (docId, pageId, opts) => {
4159
+ const config = loadConfig();
4160
+ const page = await editDocPage(config, docId, pageId, {
4161
+ name: opts.name,
4162
+ content: opts.content
4163
+ });
4164
+ if (shouldOutputJson(opts.json ?? false)) {
4165
+ console.log(JSON.stringify(page, null, 2));
4166
+ } else {
4167
+ console.log(`Updated page "${page.name}" (${page.id})`);
4168
+ }
4169
+ }
4170
+ )
4171
+ );
3749
4172
  var configCmd = program.command("config").description("Manage CLI configuration");
3750
4173
  configCmd.command("get <key>").description("Print a config value").action(
3751
4174
  wrapAction(async (key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,6 +52,10 @@ All commands support `--help` for full flag details.
52
52
  | `cup lists <spaceId> [--name partial] [--json]` | Lists in a space (including folder lists) |
53
53
  | `cup open <query> [--json]` | Open task in browser by ID or name |
54
54
  | `cup auth [--json]` | Check authentication status |
55
+ | `cup folders <spaceId> [--name partial] [--json]` | Folders in a space (with their lists) |
56
+ | `cup docs [query] [--json]` | List workspace docs (optionally filter by name) |
57
+ | `cup doc <docId> [pageId] [--json]` | View doc metadata + page tree, or a specific page |
58
+ | `cup doc-pages <docId> [--json]` | All pages in a doc with content |
55
59
 
56
60
  ### Write
57
61
 
@@ -83,6 +87,9 @@ All commands support `--help` for full flag details.
83
87
  | `cup time status [--json]` | Show currently running timer |
84
88
  | `cup time log <taskId> <duration> [-d desc] [--json]` | Log manual time entry (e.g. "2h", "30m") |
85
89
  | `cup time list [--days n] [--task id] [--json]` | List recent time entries |
90
+ | `cup doc-create <title> [-c content] [--json]` | Create a new doc |
91
+ | `cup doc-page-create <docId> <name> [-c content] [--parent-page pageId] [--json]` | Create a page in a doc |
92
+ | `cup doc-page-edit <docId> <pageId> [--name text] [-c content] [--json]` | Edit a doc page |
86
93
  | `cup config get <key>` / `cup config set <key> <value>` / `cup config path` | Manage CLI config (keys: apiToken, teamId, sprintFolderId) |
87
94
  | `cup completion <shell>` | Shell completions (bash/zsh/fish) |
88
95
 
@@ -124,6 +131,13 @@ All commands support `--help` for full flag details.
124
131
  | `cup link` + custom IDs | Both IDs must be the same type (both custom or both native). Mixing may not work |
125
132
  | `cup link` | Link/unlink tasks (different from dependencies) |
126
133
  | `cup attach` | Upload files to tasks. Attachments shown in `cup task` detail view |
134
+ | `cup folders` | List folders in a space with their contained lists |
135
+ | `cup docs` | List and search workspace docs by name |
136
+ | `cup doc` | View doc metadata + page tree (no pageId), or a specific page (with pageId) |
137
+ | `cup doc-pages` | Dump all pages in a doc with full content |
138
+ | `cup doc-create` | Create a new doc with optional initial content |
139
+ | `cup doc-page-create` | Create a page in a doc, optionally nested under a parent page |
140
+ | `cup doc-page-edit` | Edit a doc page name or content |
127
141
  | `cup task` | Shows custom fields, checklists, attachments, dependencies, and linked tasks in detail view |
128
142
  | `cup lists` | Discovers list IDs needed for `--list` and `cup create -l` |
129
143
  | Errors | stderr with exit code 1 |
@@ -195,11 +209,29 @@ cup time list --days 7 # recent entries
195
209
  cup delete abc123def --confirm # irreversible!
196
210
  ```
197
211
 
212
+ ### Work with docs
213
+
214
+ ```bash
215
+ cup docs # list all docs
216
+ cup docs "design" # search docs by name
217
+ cup doc <docId> # doc metadata + page tree
218
+ cup doc <docId> <pageId> # view page content
219
+ cup doc-pages <docId> # all pages with content
220
+ cup doc-create "Architecture Notes" # create a doc
221
+ cup doc-create "Notes" -c "# Draft" # create with content
222
+ cup doc-page-create <docId> "New Section" # add page to doc
223
+ cup doc-page-create <docId> "Sub" --parent-page <pageId> # nested page
224
+ cup doc-page-edit <docId> <pageId> --name "Renamed" # rename page
225
+ cup doc-page-edit <docId> <pageId> -c "# Updated content" # edit content
226
+ ```
227
+
198
228
  ### Discover workspace structure
199
229
 
200
230
  ```bash
201
231
  cup spaces # all spaces
202
232
  cup spaces --name "Engineering" # find space ID by name
233
+ cup folders <spaceId> # folders with their lists
234
+ cup folders <spaceId> --name "sprint" # filter folders by name
203
235
  cup lists <spaceId> # lists in a space (needs ID from cup spaces)
204
236
  cup sprints # all sprints across folders
205
237
  cup auth # verify token works