@krodak/clickup-cli 0.18.0 → 0.20.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.20.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,21 @@ 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
- async postComment(taskId, commentText) {
91
- return this.request(`/task/${taskId}/comment`, {
109
+ async postComment(taskId, commentText, notifyAll) {
110
+ const body = { comment_text: commentText };
111
+ if (notifyAll) body.notify_all = true;
112
+ return this.request(this.taskPath(taskId, "/comment"), {
92
113
  method: "POST",
93
- body: JSON.stringify({ comment_text: commentText })
114
+ body: JSON.stringify(body)
94
115
  });
95
116
  }
96
117
  async getTaskComments(taskId) {
97
- const data = await this.request(`/task/${taskId}/comment`);
118
+ const data = await this.request(this.taskPath(taskId, "/comment"));
98
119
  return data.comments ?? [];
99
120
  }
100
121
  async getTasksFromList(listId, params = {}, options = {}) {
@@ -106,7 +127,7 @@ var ClickUpClient = class {
106
127
  });
107
128
  }
108
129
  async getTask(taskId) {
109
- return this.request(`/task/${taskId}?include_markdown_description=true`);
130
+ return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
110
131
  }
111
132
  async createTask(listId, options) {
112
133
  return this.request(`/list/${listId}/task`, {
@@ -163,28 +184,32 @@ var ClickUpClient = class {
163
184
  await this.request(`/list/${listId}/task/${taskId}`, { method: "DELETE" });
164
185
  }
165
186
  async setCustomFieldValue(taskId, fieldId, value) {
166
- await this.request(`/task/${taskId}/field/${fieldId}`, {
187
+ await this.request(this.taskPath(taskId, `/field/${fieldId}`), {
167
188
  method: "POST",
168
189
  body: JSON.stringify({ value })
169
190
  });
170
191
  }
171
192
  async removeCustomFieldValue(taskId, fieldId) {
172
- await this.request(`/task/${taskId}/field/${fieldId}`, { method: "DELETE" });
193
+ await this.request(this.taskPath(taskId, `/field/${fieldId}`), { method: "DELETE" });
173
194
  }
174
195
  async deleteTask(taskId) {
175
- await this.request(`/task/${taskId}`, { method: "DELETE" });
196
+ await this.request(this.taskPath(taskId), { method: "DELETE" });
176
197
  }
177
198
  async addTagToTask(taskId, tagName) {
178
- await this.request(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, { method: "POST" });
199
+ await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
200
+ method: "POST"
201
+ });
179
202
  }
180
203
  async removeTagFromTask(taskId, tagName) {
181
- await this.request(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, { method: "DELETE" });
204
+ await this.request(this.taskPath(taskId, `/tag/${encodeURIComponent(tagName)}`), {
205
+ method: "DELETE"
206
+ });
182
207
  }
183
208
  async addDependency(taskId, opts) {
184
209
  const body = {};
185
210
  if (opts.dependsOn) body.depends_on = opts.dependsOn;
186
211
  if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
187
- await this.request(`/task/${taskId}/dependency`, {
212
+ await this.request(this.taskPath(taskId, "/dependency"), {
188
213
  method: "POST",
189
214
  body: JSON.stringify(body)
190
215
  });
@@ -193,7 +218,7 @@ var ClickUpClient = class {
193
218
  const params = new URLSearchParams();
194
219
  if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
195
220
  if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
196
- await this.request(`/task/${taskId}/dependency?${params.toString()}`, {
221
+ await this.request(this.taskPath(taskId, `/dependency?${params.toString()}`), {
197
222
  method: "DELETE"
198
223
  });
199
224
  }
@@ -212,24 +237,30 @@ var ClickUpClient = class {
212
237
  const data = await this.request(`/comment/${commentId}/reply`);
213
238
  return data.comments ?? [];
214
239
  }
215
- async createThreadedComment(commentId, text) {
240
+ async createThreadedComment(commentId, text, notifyAll) {
241
+ const body = { comment_text: text };
242
+ if (notifyAll) body.notify_all = true;
216
243
  await this.request(`/comment/${commentId}/reply`, {
217
244
  method: "POST",
218
- body: JSON.stringify({ comment_text: text })
245
+ body: JSON.stringify(body)
219
246
  });
220
247
  }
221
248
  async addTaskLink(taskId, linksTo) {
222
- await this.request(`/task/${taskId}/link/${linksTo}`, { method: "POST" });
249
+ await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
250
+ method: "POST"
251
+ });
223
252
  }
224
253
  async deleteTaskLink(taskId, linksTo) {
225
- await this.request(`/task/${taskId}/link/${linksTo}`, { method: "DELETE" });
254
+ await this.request(this.taskPath(taskId, `/link/${linksTo}`), {
255
+ method: "DELETE"
256
+ });
226
257
  }
227
258
  async getListCustomFields(listId) {
228
259
  const data = await this.request(`/list/${listId}/field`);
229
260
  return data.fields ?? [];
230
261
  }
231
262
  async createChecklist(taskId, name) {
232
- const data = await this.request(`/task/${taskId}/checklist`, {
263
+ const data = await this.request(this.taskPath(taskId, "/checklist"), {
233
264
  method: "POST",
234
265
  body: JSON.stringify({ name })
235
266
  });
@@ -265,10 +296,13 @@ var ClickUpClient = class {
265
296
  duration: -1
266
297
  };
267
298
  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
- });
299
+ const data = await this.request(
300
+ `/team/${teamId}/time_entries/start${this.customIdQueryParams(taskId)}`,
301
+ {
302
+ method: "POST",
303
+ body: JSON.stringify(body)
304
+ }
305
+ );
272
306
  return data.data;
273
307
  }
274
308
  async stopTimeEntry(teamId) {
@@ -291,10 +325,13 @@ var ClickUpClient = class {
291
325
  duration
292
326
  };
293
327
  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
- });
328
+ const data = await this.request(
329
+ `/team/${teamId}/time_entries${this.customIdQueryParams(taskId)}`,
330
+ {
331
+ method: "POST",
332
+ body: JSON.stringify(body)
333
+ }
334
+ );
298
335
  return data.data;
299
336
  }
300
337
  async getTimeEntries(teamId, opts) {
@@ -322,7 +359,7 @@ var ClickUpClient = class {
322
359
  const fileName = basename2(filePath);
323
360
  const formData = new FormData();
324
361
  formData.append("attachment", new Blob([fileBuffer]), fileName);
325
- const res = await fetch(`${BASE_URL}/task/${taskId}/attachment`, {
362
+ const res = await fetch(`${BASE_URL}${this.taskPath(taskId, "/attachment")}`, {
326
363
  method: "POST",
327
364
  headers: { Authorization: this.apiToken },
328
365
  body: formData,
@@ -584,6 +621,20 @@ function formatTaskDetailMarkdown(task) {
584
621
  lines.push(`- [${att.title}](${att.url})`);
585
622
  }
586
623
  }
624
+ if (task.dependencies?.length) {
625
+ lines.push("", "## Dependencies", "");
626
+ for (const dep of task.dependencies) {
627
+ const direction = dep.depends_on === task.id ? "blocks" : "depends on";
628
+ const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
629
+ lines.push(`- ${direction} ${otherId}`);
630
+ }
631
+ }
632
+ if (task.linked_tasks?.length) {
633
+ lines.push("", "## Linked Tasks", "");
634
+ for (const lt of task.linked_tasks) {
635
+ lines.push(`- ${lt.task_id}`);
636
+ }
637
+ }
587
638
  return lines.join("\n");
588
639
  }
589
640
  function formatUpdateConfirmation(id, name) {
@@ -737,6 +788,22 @@ function formatTaskDetail(task) {
737
788
  lines.push(` ${att.title} ${chalk2.dim(att.url)}`);
738
789
  }
739
790
  }
791
+ if (task.dependencies?.length) {
792
+ lines.push("");
793
+ lines.push(chalk2.bold("Dependencies"));
794
+ for (const dep of task.dependencies) {
795
+ const direction = dep.depends_on === task.id ? "blocks" : "depends on";
796
+ const otherId = dep.depends_on === task.id ? dep.task_id : dep.depends_on;
797
+ lines.push(` ${direction} ${chalk2.dim(otherId)}`);
798
+ }
799
+ }
800
+ if (task.linked_tasks?.length) {
801
+ lines.push("");
802
+ lines.push(chalk2.bold("Linked Tasks"));
803
+ for (const lt of task.linked_tasks) {
804
+ lines.push(` ${chalk2.dim(lt.task_id)}`);
805
+ }
806
+ }
740
807
  if (task.text_content?.trim()) {
741
808
  lines.push("");
742
809
  lines.push(descriptionPreview(task.text_content));
@@ -1304,10 +1371,10 @@ async function fetchSubtasks(config, taskId, options = {}) {
1304
1371
  }
1305
1372
 
1306
1373
  // src/commands/comment.ts
1307
- async function postComment(config, taskId, text) {
1374
+ async function postComment(config, taskId, text, notifyAll) {
1308
1375
  if (!text.trim()) throw new Error("Comment text cannot be empty");
1309
1376
  const client = new ClickUpClient(config);
1310
- return client.postComment(taskId, text);
1377
+ return client.postComment(taskId, text, notifyAll);
1311
1378
  }
1312
1379
 
1313
1380
  // src/commands/comments.ts
@@ -1598,7 +1665,7 @@ async function runAssignedCommand(config, opts) {
1598
1665
 
1599
1666
  // src/commands/open.ts
1600
1667
  function looksLikeTaskId(query) {
1601
- return /^[a-z0-9]+$/i.test(query) && query.length <= 12;
1668
+ return /^[a-z0-9]+$/i.test(query) && query.length <= 12 || isCustomTaskId(query);
1602
1669
  }
1603
1670
  async function openTask(config, query, opts = {}) {
1604
1671
  const client = new ClickUpClient(config);
@@ -1915,7 +1982,7 @@ function bashCompletion(name) {
1915
1982
  COMPREPLY=($(compgen -W "--status --name --include-closed --json" -- "$cur"))
1916
1983
  ;;
1917
1984
  comment)
1918
- COMPREPLY=($(compgen -W "-m --message --json" -- "$cur"))
1985
+ COMPREPLY=($(compgen -W "-m --message --notify-all --json" -- "$cur"))
1919
1986
  ;;
1920
1987
  comments)
1921
1988
  COMPREPLY=($(compgen -W "--json" -- "$cur"))
@@ -1988,7 +2055,7 @@ function bashCompletion(name) {
1988
2055
  COMPREPLY=($(compgen -W "--json" -- "$cur"))
1989
2056
  ;;
1990
2057
  reply)
1991
- COMPREPLY=($(compgen -W "-m --message --json" -- "$cur"))
2058
+ COMPREPLY=($(compgen -W "-m --message --notify-all --json" -- "$cur"))
1992
2059
  ;;
1993
2060
  link)
1994
2061
  COMPREPLY=($(compgen -W "--remove --json" -- "$cur"))
@@ -2139,6 +2206,7 @@ _${name}() {
2139
2206
  _arguments \\
2140
2207
  '1:task_id:' \\
2141
2208
  '(-m --message)'{-m,--message}'[Comment text]:text:' \\
2209
+ '--notify-all[Notify all assignees]' \\
2142
2210
  '--json[Force JSON output]'
2143
2211
  ;;
2144
2212
  comments)
@@ -2360,6 +2428,7 @@ _${name}() {
2360
2428
  _arguments \\
2361
2429
  '1:comment_id:' \\
2362
2430
  '(-m --message)'{-m,--message}'[Reply text]:text:' \\
2431
+ '--notify-all[Notify all assignees]' \\
2363
2432
  '--json[Force JSON output]'
2364
2433
  ;;
2365
2434
  link)
@@ -2501,6 +2570,7 @@ complete -c ${name} -n '__fish_seen_subcommand_from subtasks' -l include-closed
2501
2570
  complete -c ${name} -n '__fish_seen_subcommand_from subtasks' -l json -d 'Force JSON output'
2502
2571
 
2503
2572
  complete -c ${name} -n '__fish_seen_subcommand_from comment' -s m -l message -d 'Comment text'
2573
+ complete -c ${name} -n '__fish_seen_subcommand_from comment' -l notify-all -d 'Notify all assignees'
2504
2574
  complete -c ${name} -n '__fish_seen_subcommand_from comment' -l json -d 'Force JSON output'
2505
2575
 
2506
2576
  complete -c ${name} -n '__fish_seen_subcommand_from comments' -l json -d 'Force JSON output'
@@ -2586,6 +2656,7 @@ complete -c ${name} -n '__fish_seen_subcommand_from comment-delete' -l json -d '
2586
2656
  complete -c ${name} -n '__fish_seen_subcommand_from replies' -l json -d 'Force JSON output'
2587
2657
 
2588
2658
  complete -c ${name} -n '__fish_seen_subcommand_from reply' -s m -l message -d 'Reply text'
2659
+ complete -c ${name} -n '__fish_seen_subcommand_from reply' -l notify-all -d 'Notify all assignees'
2589
2660
  complete -c ${name} -n '__fish_seen_subcommand_from reply' -l json -d 'Force JSON output'
2590
2661
 
2591
2662
  complete -c ${name} -n '__fish_seen_subcommand_from link' -l remove -d 'Remove the link'
@@ -2918,10 +2989,10 @@ async function getReplies(config, commentId) {
2918
2989
  const client = new ClickUpClient(config);
2919
2990
  return client.getThreadedComments(commentId);
2920
2991
  }
2921
- async function createReply(config, commentId, text) {
2992
+ async function createReply(config, commentId, text, notifyAll) {
2922
2993
  if (!text.trim()) throw new Error("Reply text cannot be empty");
2923
2994
  const client = new ClickUpClient(config);
2924
- await client.createThreadedComment(commentId, text);
2995
+ await client.createThreadedComment(commentId, text, notifyAll);
2925
2996
  }
2926
2997
  function formatReplies(replies) {
2927
2998
  if (replies.length === 0) return "No replies";
@@ -3145,16 +3216,18 @@ program.command("subtasks <taskId>").description("List subtasks of a task or ini
3145
3216
  }
3146
3217
  )
3147
3218
  );
3148
- program.command("comment <taskId>").description("Post a comment on a task").requiredOption("-m, --message <text>", "Comment text").option("--json", "Force JSON output even in terminal").action(
3149
- wrapAction(async (taskId, opts) => {
3150
- const config = loadConfig();
3151
- const result = await postComment(config, taskId, opts.message);
3152
- if (shouldOutputJson(opts.json ?? false)) {
3153
- console.log(JSON.stringify(result, null, 2));
3154
- } else {
3155
- console.log(formatCommentConfirmation(result.id));
3219
+ program.command("comment <taskId>").description("Post a comment on a task").requiredOption("-m, --message <text>", "Comment text").option("--notify-all", "Notify all assignees").option("--json", "Force JSON output even in terminal").action(
3220
+ wrapAction(
3221
+ async (taskId, opts) => {
3222
+ const config = loadConfig();
3223
+ const result = await postComment(config, taskId, opts.message, opts.notifyAll);
3224
+ if (shouldOutputJson(opts.json ?? false)) {
3225
+ console.log(JSON.stringify(result, null, 2));
3226
+ } else {
3227
+ console.log(formatCommentConfirmation(result.id));
3228
+ }
3156
3229
  }
3157
- })
3230
+ )
3158
3231
  );
3159
3232
  program.command("comments <taskId>").description("List comments on a task").option("--json", "Force JSON output even in terminal").action(
3160
3233
  wrapAction(async (taskId, opts) => {
@@ -3201,16 +3274,18 @@ program.command("replies <commentId>").description("List threaded replies on a c
3201
3274
  }
3202
3275
  })
3203
3276
  );
3204
- program.command("reply <commentId>").description("Reply to a comment").requiredOption("-m, --message <text>", "Reply text").option("--json", "Force JSON output even in terminal").action(
3205
- wrapAction(async (commentId, opts) => {
3206
- const config = loadConfig();
3207
- await createReply(config, commentId, opts.message);
3208
- if (shouldOutputJson(opts.json ?? false)) {
3209
- console.log(JSON.stringify({ success: true, commentId }, null, 2));
3210
- } else {
3211
- console.log(`Replied to comment ${commentId}`);
3277
+ program.command("reply <commentId>").description("Reply to a comment").requiredOption("-m, --message <text>", "Reply text").option("--notify-all", "Notify all assignees").option("--json", "Force JSON output even in terminal").action(
3278
+ wrapAction(
3279
+ async (commentId, opts) => {
3280
+ const config = loadConfig();
3281
+ await createReply(config, commentId, opts.message, opts.notifyAll);
3282
+ if (shouldOutputJson(opts.json ?? false)) {
3283
+ console.log(JSON.stringify({ success: true, commentId }, null, 2));
3284
+ } else {
3285
+ console.log(`Replied to comment ${commentId}`);
3286
+ }
3212
3287
  }
3213
- })
3288
+ )
3214
3289
  );
3215
3290
  program.command("activity <taskId>").description("Show task details and comments combined").option("--json", "Force JSON output even in terminal").action(
3216
3291
  wrapAction(async (taskId, opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -63,7 +63,7 @@ All commands support `--help` for full flag details.
63
63
  | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
64
64
  | `cu update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id\|me] [--parent id] [--json]` | Update task fields (desc supports markdown) |
65
65
  | `cu create -n name [-l listId] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id\|me] [--tags t] [--custom-item-id n] [--json]` | Create task (desc supports markdown) |
66
- | `cu comment <id> -m text [--json]` | Post comment on task |
66
+ | `cu comment <id> -m text [--notify-all] [--json]` | Post comment on task |
67
67
  | `cu comment-edit <commentId> -m text [--resolved] [--unresolved] [--json]` | Edit an existing comment |
68
68
  | `cu assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users |
69
69
  | `cu depend <id> [--on taskId] [--blocks taskId] [--remove] [--json]` | Add/remove task dependencies |
@@ -79,7 +79,7 @@ All commands support `--help` for full flag details.
79
79
  | `cu checklist delete-item <checklistId> <itemId> [--json]` | Delete a checklist item |
80
80
  | `cu comment-delete <commentId> [--json]` | Delete a comment |
81
81
  | `cu replies <commentId> [--json]` | List threaded replies on a comment |
82
- | `cu reply <commentId> -m text [--json]` | Reply to a comment |
82
+ | `cu reply <commentId> -m text [--notify-all] [--json]` | Reply to a comment |
83
83
  | `cu link <taskId> <linksTo> [--remove] [--json]` | Add or remove link between tasks |
84
84
  | `cu attach <taskId> <filePath> [--json]` | Upload file attachment to a task |
85
85
  | `cu time start <taskId> [-d desc] [--json]` | Start tracking time on a task |
@@ -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,9 +124,11 @@ 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
- | `cu task` | Shows custom fields, checklists, and attachments in detail view |
131
+ | `cu task` | Shows custom fields, checklists, attachments, dependencies, and linked tasks in detail view |
130
132
  | `cu lists` | Discovers list IDs needed for `--list` and `cu create -l` |
131
133
  | Errors | stderr with exit code 1 |
132
134
  | Parsing | Strict - excess/unknown arguments rejected |