@krodak/clickup-cli 1.6.0 → 1.7.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.6.0",
4
+ "version": "1.7.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
- # cup - ClickUp CLI
2
-
3
- > A ClickUp CLI built for AI agents that also works well for humans. Outputs Markdown when piped (optimized for AI context windows), interactive tables when run in a terminal.
4
-
5
- [![npm](https://img.shields.io/npm/v/@krodak/clickup-cli)](https://www.npmjs.com/package/@krodak/clickup-cli)
6
- [![node](https://img.shields.io/node/v/@krodak/clickup-cli)](https://nodejs.org)
7
- [![license](https://img.shields.io/npm/l/@krodak/clickup-cli)](./LICENSE)
8
- [![CI](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml)
1
+ <p align="center">
2
+ <img src="banner.png" alt="cup - ClickUp CLI for AI agents (and humans)" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@krodak/clickup-cli"><img src="https://img.shields.io/npm/v/@krodak/clickup-cli" alt="npm"></a>
7
+ <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@krodak/clickup-cli" alt="node"></a>
8
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/@krodak/clickup-cli" alt="license"></a>
9
+ <a href="https://github.com/krodak/clickup-cli/actions/workflows/ci.yml"><img src="https://github.com/krodak/clickup-cli/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://github.com/krodak/homebrew-tap"><img src="https://img.shields.io/badge/homebrew-tap-FBB040?logo=homebrew&logoColor=black" alt="homebrew"></a>
11
+ </p>
9
12
 
10
13
  ```bash
11
14
  npm install -g @krodak/clickup-cli && cup init
12
15
  ```
13
16
 
14
- `cup` is the binary name. The previous `cu` name was retired in v0.21.0 to avoid conflict with the Unix [cu(1)](<https://en.wikipedia.org/wiki/Cu_(Unix_utility)>) utility.
15
-
16
17
  ## Talk to your agent
17
18
 
18
19
  Install the CLI, add the skill file to your agent, and it works with ClickUp. No API knowledge needed.
@@ -29,6 +30,18 @@ Install the CLI, add the skill file to your agent, and it works with ClickUp. No
29
30
 
30
31
  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
32
 
33
+ ### Agent mode
34
+
35
+ When piped (no TTY), output is Markdown optimized for AI context windows. Pass `--json` for structured data.
36
+
37
+ ![Agent Mode - markdown and JSON output](demos/agent-mode.gif)
38
+
39
+ ### Terminal mode
40
+
41
+ In a terminal, you get interactive tables with colors. Most commands scope to your assigned tasks by default.
42
+
43
+ ![TTY Mode - interactive tables and detail views](demos/tty-mode.gif)
44
+
32
45
  ## Why a CLI and not MCP?
33
46
 
34
47
  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.
@@ -130,39 +143,22 @@ The skill file follows the [Agent Skills](https://agentskills.io) open standard.
130
143
 
131
144
  Full CRUD for the core ClickUp workflow:
132
145
 
133
- **Tasks** - create, read, update, delete, duplicate, search, subtasks, assign, dependencies, links, multi-list, bulk status updates
134
-
135
- **Comments** - post, edit, delete, threaded replies, notify all
136
-
137
- **Docs** - list, read, create, edit, delete (v3 API)
138
-
139
- **Time Tracking** - start/stop timer, log entries, list/update/delete history
140
-
141
- **Checklists** - view, create, delete, add/edit/delete items
142
-
143
- **Custom Fields** - list, set, remove values (dropdown, date, checkbox, text, etc.)
144
-
145
- **Tags** - add/remove on tasks, space-level create/update/delete
146
-
147
- **Goals & OKRs** - goals CRUD, key results CRUD
148
-
149
- **Sprints** - auto-detect active sprint, flexible date parsing, config override
150
-
151
- **Workspace** - spaces, folders, lists, members, task types, templates
152
-
153
- **Attachments** - upload files to tasks, shown in detail views
146
+ | Area | Capabilities |
147
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
148
+ | ✅ **Tasks** | Create, read, update, delete, duplicate, search, subtasks, assign, dependencies, links, multi-list, bulk status updates |
149
+ | 💬 **Comments** | Post, edit, delete, threaded replies, notify all |
150
+ | 📄 **Docs** | List, read, create, edit, delete (v3 API) |
151
+ | ⏱️ **Time Tracking** | Start/stop timer, log entries, list/update/delete history |
152
+ | ☑️ **Checklists** | View, create, delete, add/edit/delete items |
153
+ | 🔧 **Custom Fields** | List, set, remove values (dropdown, date, checkbox, text, etc.) |
154
+ | 🏷️ **Tags** | Add/remove on tasks, space-level create/update/delete |
155
+ | 🎯 **Goals & OKRs** | Goals CRUD, key results CRUD |
156
+ | 🏃 **Sprints** | Auto-detect active sprint, flexible date parsing, config override |
157
+ | 🏢 **Workspace** | Spaces, folders, lists (read + create), members, task types, templates |
158
+ | 📎 **Attachments** | Upload files to tasks, shown in detail views |
154
159
 
155
160
  [Full API coverage details](docs/api-coverage.md) | [Command reference](docs/commands.md)
156
161
 
157
- ## Output Modes
158
-
159
- | Context | Default | Override |
160
- | -------------- | --------------------------- | ---------------------------- |
161
- | Terminal (TTY) | Interactive tables + picker | `--json` |
162
- | Piped (no TTY) | Markdown (optimized for AI) | `--json` or `CU_OUTPUT=json` |
163
-
164
- Most commands scope to your assigned tasks by default - keeping output small and relevant for agent context windows.
165
-
166
162
  ## Configuration
167
163
 
168
164
  ### Profiles
@@ -213,23 +209,6 @@ Environment variables override config file values:
213
209
 
214
210
  When both `CU_API_TOKEN` and `CU_TEAM_ID` are set, the config file is not required. Useful for CI/CD and containerized agents.
215
211
 
216
- ## Custom Task IDs
217
-
218
- ClickUp workspaces can configure custom task IDs with a prefix per space (e.g., `PROJ-123`, `DEV-42`). The CLI detects these automatically - any ID matching the `PREFIX-DIGITS` format (uppercase letters, hyphen, digits) is treated as a custom task ID.
219
-
220
- All commands that accept task IDs work with both native IDs and custom IDs:
221
-
222
- ```bash
223
- cup task PROJ-123
224
- cup update DEV-42 --status done
225
- cup comment PROJ-456 -m "Fixed in latest commit"
226
- cup subtasks DEV-100
227
- ```
228
-
229
- Custom ID resolution uses the `teamId` from your config, which is required (`cup init` sets it up).
230
-
231
- **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.
232
-
233
212
  ## Development
234
213
 
235
214
  ```bash
package/dist/index.js CHANGED
@@ -100,6 +100,12 @@ var ClickUpClient = class {
100
100
  ...options.headers
101
101
  }
102
102
  });
103
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
104
+ if (!res.ok) {
105
+ throw new Error(`ClickUp API error ${res.status}: ${res.statusText}`);
106
+ }
107
+ return {};
108
+ }
103
109
  let parsed;
104
110
  try {
105
111
  parsed = await res.json();
@@ -218,6 +224,12 @@ var ClickUpClient = class {
218
224
  async getListWithStatuses(listId) {
219
225
  return this.request(`/list/${listId}`);
220
226
  }
227
+ async createSpace(teamId, name) {
228
+ return this.request(`/team/${teamId}/space`, {
229
+ method: "POST",
230
+ body: JSON.stringify({ name, multiple_assignees: true })
231
+ });
232
+ }
221
233
  async getSpaces(teamId) {
222
234
  const data = await this.request(`/team/${teamId}/space?archived=false`);
223
235
  return readCollectionField(data, "spaces", "spaces");
@@ -232,6 +244,24 @@ var ClickUpClient = class {
232
244
  "custom task types"
233
245
  );
234
246
  }
247
+ async createList(spaceId, name) {
248
+ return this.request(`/space/${spaceId}/list`, {
249
+ method: "POST",
250
+ body: JSON.stringify({ name })
251
+ });
252
+ }
253
+ async createFolderList(folderId, name) {
254
+ return this.request(`/folder/${folderId}/list`, {
255
+ method: "POST",
256
+ body: JSON.stringify({ name })
257
+ });
258
+ }
259
+ async createFolder(spaceId, name) {
260
+ return this.request(`/space/${spaceId}/folder`, {
261
+ method: "POST",
262
+ body: JSON.stringify({ name })
263
+ });
264
+ }
235
265
  async getLists(spaceId) {
236
266
  const data = await this.request(`/space/${spaceId}/list?archived=false`);
237
267
  return readCollectionField(data, "lists", "space lists");
@@ -1567,7 +1597,10 @@ function parseTimeEstimate(value) {
1567
1597
  }
1568
1598
  function buildUpdatePayload(opts) {
1569
1599
  const payload = {};
1570
- if (opts.name !== void 0) payload.name = opts.name;
1600
+ if (opts.name !== void 0) {
1601
+ if (!opts.name.trim()) throw new Error("Task name cannot be empty");
1602
+ payload.name = opts.name;
1603
+ }
1571
1604
  if (opts.description !== void 0) payload.markdown_content = opts.description;
1572
1605
  if (opts.status !== void 0) payload.status = opts.status;
1573
1606
  if (opts.priority !== void 0) payload.priority = parsePriority(opts.priority);
@@ -1616,6 +1649,7 @@ async function updateTask(config, taskId, options) {
1616
1649
 
1617
1650
  // src/commands/create.ts
1618
1651
  async function createTask(config, options) {
1652
+ if (!options.name.trim()) throw new Error("Task name cannot be empty");
1619
1653
  const client = new ClickUpClient(config);
1620
1654
  let listId = options.list;
1621
1655
  if (!listId && options.parent) {
@@ -3014,6 +3048,38 @@ var commandMetadata = [
3014
3048
  { section: "read", usage: "folders <spaceId>", description: "List folders in a space" }
3015
3049
  ]
3016
3050
  },
3051
+ {
3052
+ name: "space-create",
3053
+ description: "Create a new space in your workspace",
3054
+ flags: ["--json"],
3055
+ quickReference: [
3056
+ { section: "write", usage: "space-create <name>", description: "Create a space" }
3057
+ ]
3058
+ },
3059
+ {
3060
+ name: "list-create",
3061
+ description: "Create a new list in a space",
3062
+ flags: ["--folder", "--json"],
3063
+ quickReference: [
3064
+ {
3065
+ section: "write",
3066
+ usage: "list-create <spaceId> <name>",
3067
+ description: "Create a list in a space"
3068
+ }
3069
+ ]
3070
+ },
3071
+ {
3072
+ name: "folder-create",
3073
+ description: "Create a new folder in a space",
3074
+ flags: ["--json"],
3075
+ quickReference: [
3076
+ {
3077
+ section: "write",
3078
+ usage: "folder-create <spaceId> <name>",
3079
+ description: "Create a folder in a space"
3080
+ }
3081
+ ]
3082
+ },
3017
3083
  {
3018
3084
  name: "members",
3019
3085
  description: "List workspace members",
@@ -3824,6 +3890,24 @@ ${renderZshTopLevelCommands(name)}
3824
3890
  '--name[Filter by folder name]:text:' \\
3825
3891
  '--json[Force JSON output]'
3826
3892
  ;;
3893
+ space-create)
3894
+ _arguments \\
3895
+ '1:name:' \\
3896
+ '--json[Force JSON output]'
3897
+ ;;
3898
+ list-create)
3899
+ _arguments \\
3900
+ '1:space_id:' \\
3901
+ '2:name:' \\
3902
+ '--folder[Create inside a folder]:folder_id:' \\
3903
+ '--json[Force JSON output]'
3904
+ ;;
3905
+ folder-create)
3906
+ _arguments \\
3907
+ '1:space_id:' \\
3908
+ '2:name:' \\
3909
+ '--json[Force JSON output]'
3910
+ ;;
3827
3911
  doc-create)
3828
3912
  _arguments \\
3829
3913
  '1:title:' \\
@@ -4772,7 +4856,7 @@ function wrapAction(fn) {
4772
4856
  return async (...args) => {
4773
4857
  await fn(...args).catch((err) => {
4774
4858
  console.error(err instanceof Error ? err.message : String(err));
4775
- process.exit(1);
4859
+ process.exitCode = 1;
4776
4860
  });
4777
4861
  };
4778
4862
  }
@@ -5622,6 +5706,47 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
5622
5706
  }
5623
5707
  })
5624
5708
  );
5709
+ program.command("space-create <name>").description("Create a new space in your workspace").option("--json", "Force JSON output even in terminal").action(
5710
+ wrapAction(async (name, opts) => {
5711
+ if (!name.trim()) throw new Error("Space name cannot be empty");
5712
+ const config = loadConfig(getProfileName());
5713
+ const client = new ClickUpClient(config);
5714
+ const space = await client.createSpace(config.teamId, name);
5715
+ if (shouldOutputJson(opts.json ?? false)) {
5716
+ console.log(JSON.stringify(space, null, 2));
5717
+ } else {
5718
+ console.log(`Created space "${space.name}" (${space.id})`);
5719
+ }
5720
+ })
5721
+ );
5722
+ program.command("list-create <spaceId> <name>").description("Create a new list in a space").option("--folder <folderId>", "Create the list inside a folder").option("--json", "Force JSON output even in terminal").action(
5723
+ wrapAction(
5724
+ async (spaceId, name, opts) => {
5725
+ if (!name.trim()) throw new Error("List name cannot be empty");
5726
+ const config = loadConfig(getProfileName());
5727
+ const client = new ClickUpClient(config);
5728
+ const list = opts.folder ? await client.createFolderList(opts.folder, name) : await client.createList(spaceId, name);
5729
+ if (shouldOutputJson(opts.json ?? false)) {
5730
+ console.log(JSON.stringify(list, null, 2));
5731
+ } else {
5732
+ console.log(`Created list "${list.name}" (${list.id})`);
5733
+ }
5734
+ }
5735
+ )
5736
+ );
5737
+ program.command("folder-create <spaceId> <name>").description("Create a new folder in a space").option("--json", "Force JSON output even in terminal").action(
5738
+ wrapAction(async (spaceId, name, opts) => {
5739
+ if (!name.trim()) throw new Error("Folder name cannot be empty");
5740
+ const config = loadConfig(getProfileName());
5741
+ const client = new ClickUpClient(config);
5742
+ const folder = await client.createFolder(spaceId, name);
5743
+ if (shouldOutputJson(opts.json ?? false)) {
5744
+ console.log(JSON.stringify(folder, null, 2));
5745
+ } else {
5746
+ console.log(`Created folder "${folder.name}" (${folder.id})`);
5747
+ }
5748
+ })
5749
+ );
5625
5750
  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(
5626
5751
  wrapAction(async (title, opts) => {
5627
5752
  const config = loadConfig(getProfileName());
@@ -5822,14 +5947,16 @@ async function run(argv = process.argv) {
5822
5947
  }
5823
5948
  process.on("SIGINT", () => {
5824
5949
  process.stderr.write("\nInterrupted\n");
5825
- process.exit(130);
5950
+ process.exitCode = 130;
5826
5951
  });
5827
- var isDirectExecution = false;
5828
- try {
5829
- isDirectExecution = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync(resolve(process.argv[1]));
5830
- } catch {
5831
- isDirectExecution = false;
5952
+ function checkDirectExecution() {
5953
+ try {
5954
+ return process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === realpathSync(resolve(process.argv[1]));
5955
+ } catch {
5956
+ return false;
5957
+ }
5832
5958
  }
5959
+ var isDirectExecution = checkDirectExecution();
5833
5960
  if (isDirectExecution) {
5834
5961
  await run();
5835
5962
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -108,6 +108,9 @@ All commands support `--help` for full flag details. All commands support `--jso
108
108
  | `cup doc-page-edit <docId> <pageId> [--name text] [-c content]` | Edit doc page |
109
109
  | `cup doc-delete <docId>` | Delete a doc |
110
110
  | `cup doc-page-delete <docId> <pageId>` | Delete doc page |
111
+ | `cup space-create <name>` | Create a space |
112
+ | `cup list-create <spaceId> <name> [--folder folderId]` | Create a list in a space or folder |
113
+ | `cup folder-create <spaceId> <name>` | Create a folder in a space |
111
114
  | `cup tag-create <spaceId> <name> [--fg color] [--bg color]` | Create space tag |
112
115
  | `cup tag-update <spaceId> <tagName> --name <newName> [--fg c] [--bg c]` | Update space tag |
113
116
  | `cup tag-delete <spaceId> <name>` | Delete space tag |
@@ -215,6 +218,10 @@ cup doc-page-edit <docId> <pageId> -c "# Updated"
215
218
  cup spaces --name "Engineering" # find space ID
216
219
  cup folders <spaceId> # folders with their lists
217
220
  cup lists <spaceId> # lists in a space
221
+ cup space-create "New Space" # create a space
222
+ cup folder-create <spaceId> "Q2" # create a folder
223
+ cup list-create <spaceId> "Backlog" # create a list
224
+ cup list-create <spaceId> "Sprint" --folder <folderId>
218
225
  cup members # workspace members
219
226
  cup fields <listId> # custom fields on a list
220
227
  cup task-types # custom task types