@krodak/clickup-cli 0.18.0 → 0.19.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 cu/cup command",
4
- "version": "0.18.0",
4
+ "version": "0.19.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/README.md CHANGED
@@ -307,6 +307,23 @@ Environment variables override config file values:
307
307
 
308
308
  When both are set, the config file is not required. Useful for CI/CD and containerized agents.
309
309
 
310
+ ## Custom Task IDs
311
+
312
+ 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.
313
+
314
+ All commands that accept task IDs work with both native IDs and custom IDs:
315
+
316
+ ```bash
317
+ cu task PROJ-123
318
+ cu update DEV-42 --status done
319
+ cu comment PROJ-456 -m "Fixed in latest commit"
320
+ cu subtasks DEV-100
321
+ ```
322
+
323
+ Custom ID resolution uses the `teamId` from your config, which is required (`cu init` sets it up).
324
+
325
+ **Task links with custom IDs:** The `cu 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.
326
+
310
327
  ## Why a CLI and not MCP?
311
328
 
312
329
  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.
package/dist/index.js CHANGED
@@ -8,11 +8,30 @@ import { createRequire } from "module";
8
8
  // src/api.ts
9
9
  var BASE_URL = "https://api.clickup.com/api/v2";
10
10
  var MAX_PAGES = 100;
11
+ function isCustomTaskId(id) {
12
+ return /^[A-Z]+-\d+$/i.test(id);
13
+ }
11
14
  var ClickUpClient = class {
12
15
  apiToken;
16
+ teamId;
13
17
  meCache = null;
14
18
  constructor(config) {
15
19
  this.apiToken = config.apiToken;
20
+ this.teamId = config.teamId;
21
+ }
22
+ taskPath(taskId, suffix = "") {
23
+ const base = `/task/${taskId}${suffix}`;
24
+ if (isCustomTaskId(taskId) && this.teamId) {
25
+ const sep = base.includes("?") ? "&" : "?";
26
+ return `${base}${sep}custom_task_ids=true&team_id=${this.teamId}`;
27
+ }
28
+ return base;
29
+ }
30
+ customIdQueryParams(taskId) {
31
+ if (isCustomTaskId(taskId) && this.teamId) {
32
+ return `?custom_task_ids=true&team_id=${this.teamId}`;
33
+ }
34
+ return "";
16
35
  }
17
36
  async request(path, options = {}) {
18
37
  const res = await fetch(`${BASE_URL}${path}`, {
@@ -82,19 +101,19 @@ var ClickUpClient = class {
82
101
  });
83
102
  }
84
103
  async updateTask(taskId, options) {
85
- return this.request(`/task/${taskId}`, {
104
+ return this.request(this.taskPath(taskId), {
86
105
  method: "PUT",
87
106
  body: JSON.stringify(options)
88
107
  });
89
108
  }
90
109
  async postComment(taskId, commentText) {
91
- return this.request(`/task/${taskId}/comment`, {
110
+ return this.request(this.taskPath(taskId, "/comment"), {
92
111
  method: "POST",
93
112
  body: JSON.stringify({ comment_text: commentText })
94
113
  });
95
114
  }
96
115
  async getTaskComments(taskId) {
97
- const data = await this.request(`/task/${taskId}/comment`);
116
+ const data = await this.request(this.taskPath(taskId, "/comment"));
98
117
  return data.comments ?? [];
99
118
  }
100
119
  async getTasksFromList(listId, params = {}, options = {}) {
@@ -106,7 +125,7 @@ var ClickUpClient = class {
106
125
  });
107
126
  }
108
127
  async getTask(taskId) {
109
- return this.request(`/task/${taskId}?include_markdown_description=true`);
128
+ return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
110
129
  }
111
130
  async createTask(listId, options) {
112
131
  return this.request(`/list/${listId}/task`, {
@@ -163,28 +182,32 @@ var ClickUpClient = class {
163
182
  await this.request(`/list/${listId}/task/${taskId}`, { method: "DELETE" });
164
183
  }
165
184
  async setCustomFieldValue(taskId, fieldId, value) {
166
- await this.request(`/task/${taskId}/field/${fieldId}`, {
185
+ await this.request(this.taskPath(taskId, `/field/${fieldId}`), {
167
186
  method: "POST",
168
187
  body: JSON.stringify({ value })
169
188
  });
170
189
  }
171
190
  async removeCustomFieldValue(taskId, fieldId) {
172
- await this.request(`/task/${taskId}/field/${fieldId}`, { method: "DELETE" });
191
+ await this.request(this.taskPath(taskId, `/field/${fieldId}`), { method: "DELETE" });
173
192
  }
174
193
  async deleteTask(taskId) {
175
- await this.request(`/task/${taskId}`, { method: "DELETE" });
194
+ await this.request(this.taskPath(taskId), { method: "DELETE" });
176
195
  }
177
196
  async addTagToTask(taskId, tagName) {
178
- await this.request(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, { method: "POST" });
197
+ await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
198
+ method: "POST"
199
+ });
179
200
  }
180
201
  async removeTagFromTask(taskId, tagName) {
181
- await this.request(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, { method: "DELETE" });
202
+ await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
203
+ method: "DELETE"
204
+ });
182
205
  }
183
206
  async addDependency(taskId, opts) {
184
207
  const body = {};
185
208
  if (opts.dependsOn) body.depends_on = opts.dependsOn;
186
209
  if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
187
- await this.request(`/task/${taskId}/dependency`, {
210
+ await this.request(this.taskPath(taskId, "/dependency"), {
188
211
  method: "POST",
189
212
  body: JSON.stringify(body)
190
213
  });
@@ -193,7 +216,7 @@ var ClickUpClient = class {
193
216
  const params = new URLSearchParams();
194
217
  if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
195
218
  if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
196
- await this.request(`/task/${taskId}/dependency?${params.toString()}`, {
219
+ await this.request(this.taskPath(taskId, `/dependency?${params.toString()}`), {
197
220
  method: "DELETE"
198
221
  });
199
222
  }
@@ -219,17 +242,21 @@ var ClickUpClient = class {
219
242
  });
220
243
  }
221
244
  async addTaskLink(taskId, linksTo) {
222
- await this.request(`/task/${taskId}/link/${linksTo}`, { method: "POST" });
245
+ await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
246
+ method: "POST"
247
+ });
223
248
  }
224
249
  async deleteTaskLink(taskId, linksTo) {
225
- await this.request(`/task/${taskId}/link/${linksTo}`, { method: "DELETE" });
250
+ await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
251
+ method: "DELETE"
252
+ });
226
253
  }
227
254
  async getListCustomFields(listId) {
228
255
  const data = await this.request(`/list/${listId}/field`);
229
256
  return data.fields ?? [];
230
257
  }
231
258
  async createChecklist(taskId, name) {
232
- const data = await this.request(`/task/${taskId}/checklist`, {
259
+ const data = await this.request(this.taskPath(taskId, "/checklist"), {
233
260
  method: "POST",
234
261
  body: JSON.stringify({ name })
235
262
  });
@@ -265,10 +292,13 @@ var ClickUpClient = class {
265
292
  duration: -1
266
293
  };
267
294
  if (description) body.description = description;
268
- const data = await this.request(`/team/${teamId}/time_entries/start`, {
269
- method: "POST",
270
- body: JSON.stringify(body)
271
- });
295
+ const data = await this.request(
296
+ `/team/${teamId}/time_entries/start${this.customIdQueryParams(taskId)}`,
297
+ {
298
+ method: "POST",
299
+ body: JSON.stringify(body)
300
+ }
301
+ );
272
302
  return data.data;
273
303
  }
274
304
  async stopTimeEntry(teamId) {
@@ -291,10 +321,13 @@ var ClickUpClient = class {
291
321
  duration
292
322
  };
293
323
  if (opts?.description) body.description = opts.description;
294
- const data = await this.request(`/team/${teamId}/time_entries`, {
295
- method: "POST",
296
- body: JSON.stringify(body)
297
- });
324
+ const data = await this.request(
325
+ `/team/${teamId}/time_entries${this.customIdQueryParams(taskId)}`,
326
+ {
327
+ method: "POST",
328
+ body: JSON.stringify(body)
329
+ }
330
+ );
298
331
  return data.data;
299
332
  }
300
333
  async getTimeEntries(teamId, opts) {
@@ -322,7 +355,7 @@ var ClickUpClient = class {
322
355
  const fileName = basename2(filePath);
323
356
  const formData = new FormData();
324
357
  formData.append("attachment", new Blob([fileBuffer]), fileName);
325
- const res = await fetch(`${BASE_URL}/task/${taskId}/attachment`, {
358
+ const res = await fetch(`${BASE_URL}${this.taskPath(taskId, "/attachment")}`, {
326
359
  method: "POST",
327
360
  headers: { Authorization: this.apiToken },
328
361
  body: formData,
@@ -1598,7 +1631,7 @@ async function runAssignedCommand(config, opts) {
1598
1631
 
1599
1632
  // src/commands/open.ts
1600
1633
  function looksLikeTaskId(query) {
1601
- return /^[a-z0-9]+$/i.test(query) && query.length <= 12;
1634
+ return /^[a-z0-9]+$/i.test(query) && query.length <= 12 || isCustomTaskId(query);
1602
1635
  }
1603
1636
  async function openTask(config, query, opts = {}) {
1604
1637
  const client = new ClickUpClient(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -94,7 +94,7 @@ All commands support `--help` for full flag details.
94
94
 
95
95
  | Topic | Detail |
96
96
  | ------------------------- | ------------------------------------------------------------------------------------------------------ |
97
- | Task IDs | Stable alphanumeric strings (e.g. `abc123def`) |
97
+ | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
98
98
  | `--type` | Filter by task type: `task` (regular), or custom type name/ID (e.g. `initiative`, `Bug`) |
99
99
  | `--list` on create | Optional when `--parent` is given (auto-detected) |
100
100
  | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
@@ -124,6 +124,8 @@ All commands support `--help` for full flag details.
124
124
  | `cu comment-edit` | Edit comment text and resolution status |
125
125
  | `cu comment-delete` | Delete a comment |
126
126
  | `cu replies` / `cu reply` | View and post threaded comment replies |
127
+ | Custom task IDs | Auto-detected by format (`PROJ-123`). Uses `teamId` from config. All commands support them |
128
+ | `cu link` + custom IDs | Both IDs must be the same type (both custom or both native). Mixing may not work |
127
129
  | `cu link` | Link/unlink tasks (different from dependencies) |
128
130
  | `cu attach` | Upload files to tasks. Attachments shown in `cu task` detail view |
129
131
  | `cu task` | Shows custom fields, checklists, and attachments in detail view |