@krodak/clickup-cli 1.38.2 → 1.39.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.38.2",
4
+ "version": "1.39.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/dist/index.js CHANGED
@@ -291,6 +291,24 @@ var ClickUpClient = class {
291
291
  async getTask(taskId) {
292
292
  return this.request(this.taskPath(taskId, "?include_markdown_description=true"));
293
293
  }
294
+ /**
295
+ * Resolve any accepted task-id form to a native ClickUp task id.
296
+ * - Task URLs are reduced to their id segment.
297
+ * - Workspace custom ids (e.g. PROD-811) are resolved to the native id via GET.
298
+ * - Native ids pass through without an API call.
299
+ *
300
+ * Use this for task ids that appear in request bodies, query params, or
301
+ * secondary path segments, where ClickUp's custom_task_ids handling
302
+ * (applied by taskPath to the primary path id) does not reach.
303
+ */
304
+ async resolveTaskId(input) {
305
+ const normalized = normalizeTaskId(input);
306
+ if (isCustomTaskId(normalized) && this.teamId) {
307
+ const task = await this.getTask(normalized);
308
+ return task.id;
309
+ }
310
+ return normalized;
311
+ }
294
312
  async getTimeInStatus(taskId) {
295
313
  return this.request(this.taskPath(taskId, "/time_in_status"));
296
314
  }
@@ -522,19 +540,21 @@ var ClickUpClient = class {
522
540
  });
523
541
  }
524
542
  async addDependency(taskId, opts) {
543
+ const primary = await this.resolveTaskId(taskId);
525
544
  const body = {};
526
- if (opts.dependsOn) body.depends_on = opts.dependsOn;
527
- if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
528
- await this.request(this.taskPath(taskId, "/dependency"), {
545
+ if (opts.dependsOn) body.depends_on = await this.resolveTaskId(opts.dependsOn);
546
+ if (opts.dependencyOf) body.dependency_of = await this.resolveTaskId(opts.dependencyOf);
547
+ await this.request(`/task/${primary}/dependency`, {
529
548
  method: "POST",
530
549
  body: JSON.stringify(body)
531
550
  });
532
551
  }
533
552
  async deleteDependency(taskId, opts) {
553
+ const primary = await this.resolveTaskId(taskId);
534
554
  const params = new URLSearchParams();
535
- if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
536
- if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
537
- await this.request(this.taskPath(taskId, `/dependency?${params.toString()}`), {
555
+ if (opts.dependsOn) params.set("depends_on", await this.resolveTaskId(opts.dependsOn));
556
+ if (opts.dependencyOf) params.set("dependency_of", await this.resolveTaskId(opts.dependencyOf));
557
+ await this.request(`/task/${primary}/dependency?${params.toString()}`, {
538
558
  method: "DELETE"
539
559
  });
540
560
  }
@@ -562,16 +582,12 @@ var ClickUpClient = class {
562
582
  });
563
583
  }
564
584
  async addTaskLink(taskId, linksTo) {
565
- await this.request(
566
- this.taskPath(taskId, `/link/${normalizeTaskId(linksTo)}`),
567
- { method: "POST" }
568
- );
585
+ const [a, b] = await Promise.all([this.resolveTaskId(taskId), this.resolveTaskId(linksTo)]);
586
+ await this.request(`/task/${a}/link/${b}`, { method: "POST" });
569
587
  }
570
588
  async deleteTaskLink(taskId, linksTo) {
571
- await this.request(
572
- this.taskPath(taskId, `/link/${normalizeTaskId(linksTo)}`),
573
- { method: "DELETE" }
574
- );
589
+ const [a, b] = await Promise.all([this.resolveTaskId(taskId), this.resolveTaskId(linksTo)]);
590
+ await this.request(`/task/${a}/link/${b}`, { method: "DELETE" });
575
591
  }
576
592
  async getListCustomFields(listId) {
577
593
  const data = await this.request(`/list/${listId}/field`);
@@ -1062,9 +1078,11 @@ var ClickUpClient = class {
1062
1078
  );
1063
1079
  }
1064
1080
  async mergeTasks(taskId, mergeWithTaskIds) {
1065
- await this.request(this.taskPath(taskId, "/merge"), {
1081
+ const primary = await this.resolveTaskId(taskId);
1082
+ const mergeWith = await Promise.all(mergeWithTaskIds.map((id) => this.resolveTaskId(id)));
1083
+ await this.request(`/task/${primary}/merge`, {
1066
1084
  method: "POST",
1067
- body: JSON.stringify({ merge_with: mergeWithTaskIds.map(normalizeTaskId) })
1085
+ body: JSON.stringify({ merge_with: mergeWith })
1068
1086
  });
1069
1087
  }
1070
1088
  async updateTimeEstimatesByUser(taskId, estimates) {
@@ -2355,6 +2373,9 @@ async function updateTask(config, taskId, options, typeInput) {
2355
2373
  if (resolved.status !== void 0) {
2356
2374
  resolved.status = await resolveStatus(client, taskId, resolved.status);
2357
2375
  }
2376
+ if (typeof resolved.parent === "string") {
2377
+ resolved.parent = await client.resolveTaskId(resolved.parent);
2378
+ }
2358
2379
  if (resolved.custom_item_id === void 0 && typeInput !== void 0) {
2359
2380
  resolved.custom_item_id = await resolveTaskType2(client, config.teamId, typeInput);
2360
2381
  }
@@ -2367,9 +2388,15 @@ async function createTask(config, options) {
2367
2388
  if (!options.name.trim()) throw new Error("Task name cannot be empty");
2368
2389
  const client = new ClickUpClient(config);
2369
2390
  let listId = options.list;
2370
- if (!listId && options.parent) {
2371
- const parentTask = await client.getTask(options.parent);
2372
- listId = parentTask.list.id;
2391
+ let parentId = options.parent;
2392
+ if (options.parent) {
2393
+ if (!listId) {
2394
+ const parentTask = await client.getTask(options.parent);
2395
+ parentId = parentTask.id;
2396
+ listId = parentTask.list.id;
2397
+ } else {
2398
+ parentId = await client.resolveTaskId(options.parent);
2399
+ }
2373
2400
  }
2374
2401
  if (!listId) {
2375
2402
  throw new Error("Provide --list or --parent (list is auto-detected from parent task)");
@@ -2382,7 +2409,7 @@ async function createTask(config, options) {
2382
2409
  const payload = {
2383
2410
  name: options.name,
2384
2411
  ...options.description !== void 0 ? { markdown_content: options.description } : {},
2385
- ...options.parent !== void 0 ? { parent: options.parent } : {},
2412
+ ...parentId !== void 0 ? { parent: parentId } : {},
2386
2413
  ...options.status !== void 0 ? { status: options.status } : {}
2387
2414
  };
2388
2415
  if (options.priority !== void 0) {
@@ -2423,6 +2450,34 @@ async function createTask(config, options) {
2423
2450
  return { id: task.id, name: task.name, url: task.url };
2424
2451
  }
2425
2452
 
2453
+ // src/text-input.ts
2454
+ import { readFileSync } from "fs";
2455
+ function resolveTextInput(args) {
2456
+ const { inline, file, inlineFlag, fileFlag } = args;
2457
+ if (inline !== void 0 && file !== void 0) {
2458
+ throw new Error(`Cannot use ${inlineFlag} and ${fileFlag} together`);
2459
+ }
2460
+ if (file === void 0) return inline;
2461
+ const raw = file === "-" ? readStdin(fileFlag) : readTextFile(file, fileFlag);
2462
+ return raw.replace(/\r?\n$/, "");
2463
+ }
2464
+ function readTextFile(path, fileFlag) {
2465
+ try {
2466
+ return readFileSync(path, "utf8");
2467
+ } catch (err) {
2468
+ throw new Error(`Cannot read ${fileFlag} "${path}": ${err.message}`, { cause: err });
2469
+ }
2470
+ }
2471
+ function readStdin(fileFlag) {
2472
+ try {
2473
+ return readFileSync(0, "utf8");
2474
+ } catch (err) {
2475
+ throw new Error(`Failed to read ${fileFlag} from stdin: ${err.message}`, {
2476
+ cause: err
2477
+ });
2478
+ }
2479
+ }
2480
+
2426
2481
  // src/commands/get.ts
2427
2482
  async function getTask(config, taskId) {
2428
2483
  const client = new ClickUpClient(config);
@@ -3843,6 +3898,7 @@ var commandMetadata = [
3843
3898
  "--name",
3844
3899
  "-d",
3845
3900
  "--description",
3901
+ "--description-file",
3846
3902
  "-s",
3847
3903
  "--status",
3848
3904
  "--priority",
@@ -3872,6 +3928,7 @@ var commandMetadata = [
3872
3928
  "--name",
3873
3929
  "-d",
3874
3930
  "--description",
3931
+ "--description-file",
3875
3932
  "-p",
3876
3933
  "--parent",
3877
3934
  "-s",
@@ -3917,7 +3974,7 @@ var commandMetadata = [
3917
3974
  {
3918
3975
  name: "comment",
3919
3976
  description: "Post a comment on a task",
3920
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
3977
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
3921
3978
  quickReference: [
3922
3979
  { section: "write", usage: "comment <taskId>", description: "Post a comment on a task" }
3923
3980
  ]
@@ -3925,7 +3982,7 @@ var commandMetadata = [
3925
3982
  {
3926
3983
  name: "comment-edit",
3927
3984
  description: "Edit an existing comment",
3928
- flags: ["-m", "--message", "--resolved", "--unresolved", "--mention", "--json"],
3985
+ flags: ["-m", "--message", "--message-file", "--resolved", "--unresolved", "--mention", "--json"],
3929
3986
  quickReference: [
3930
3987
  {
3931
3988
  section: "write",
@@ -3969,7 +4026,7 @@ var commandMetadata = [
3969
4026
  {
3970
4027
  name: "reply",
3971
4028
  description: "Reply to a comment",
3972
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
4029
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
3973
4030
  quickReference: [
3974
4031
  { section: "write", usage: "reply <commentId>", description: "Reply to a comment" }
3975
4032
  ]
@@ -4804,7 +4861,7 @@ var commandMetadata = [
4804
4861
  {
4805
4862
  name: "list-comment",
4806
4863
  description: "Post a comment on a list",
4807
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
4864
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
4808
4865
  quickReference: [
4809
4866
  {
4810
4867
  section: "write",
@@ -4828,7 +4885,7 @@ var commandMetadata = [
4828
4885
  {
4829
4886
  name: "view-comment",
4830
4887
  description: "Post a comment on a view",
4831
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
4888
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
4832
4889
  quickReference: [
4833
4890
  {
4834
4891
  section: "write",
@@ -5215,6 +5272,7 @@ ${renderZshTopLevelCommands(name)}
5215
5272
  '1:task_id:' \\
5216
5273
  '(-n --name)'{-n,--name}'[New task name]:text:' \\
5217
5274
  '(-d --description)'{-d,--description}'[New description]:text:' \\
5275
+ '--description-file[Read description from a file (- for stdin)]:path:_files' \\
5218
5276
  '(-s --status)'{-s,--status}'[New status]:status:(open "in progress" "in review" done closed)' \\
5219
5277
  '--priority[Priority level]:priority:(urgent high normal low)' \\
5220
5278
  '--due-date[Due date (YYYY-MM-DD or "none" to clear)]:date:' \\
@@ -5233,6 +5291,7 @@ ${renderZshTopLevelCommands(name)}
5233
5291
  '(-l --list)'{-l,--list}'[Target list ID]:list_id:' \\
5234
5292
  '(-n --name)'{-n,--name}'[Task name]:name:' \\
5235
5293
  '(-d --description)'{-d,--description}'[Task description]:text:' \\
5294
+ '--description-file[Read description from a file (- for stdin)]:path:_files' \\
5236
5295
  '(-p --parent)'{-p,--parent}'[Parent task ID]:task_id:' \\
5237
5296
  '(-s --status)'{-s,--status}'[Initial status]:status:(open "in progress" "in review" done closed)' \\
5238
5297
  '--priority[Priority level]:priority:(urgent high normal low)' \\
@@ -5269,6 +5328,7 @@ ${renderZshTopLevelCommands(name)}
5269
5328
  _arguments \\
5270
5329
  '1:task_id:' \\
5271
5330
  '(-m --message)'{-m,--message}'[Comment text]:text:' \\
5331
+ '--message-file[Read comment from a file (- for stdin)]:path:_files' \\
5272
5332
  '--notify-all[Notify all assignees]' \\
5273
5333
  '--mention[Mention a user (ID, email, username, or me)]:user:' \\
5274
5334
  '--json[Force JSON output]'
@@ -5503,6 +5563,7 @@ ${renderZshTopLevelCommands(name)}
5503
5563
  _arguments \\
5504
5564
  '1:comment_id:' \\
5505
5565
  '(-m --message)'{-m,--message}'[New comment text]:text:' \\
5566
+ '--message-file[Read comment from a file (- for stdin)]:path:_files' \\
5506
5567
  '--resolved[Mark comment as resolved]' \\
5507
5568
  '--unresolved[Mark comment as unresolved]' \\
5508
5569
  '--mention[Mention a user (ID, email, username, or me)]:user:' \\
@@ -5525,6 +5586,7 @@ ${renderZshTopLevelCommands(name)}
5525
5586
  _arguments \\
5526
5587
  '1:comment_id:' \\
5527
5588
  '(-m --message)'{-m,--message}'[Reply text]:text:' \\
5589
+ '--message-file[Read reply from a file (- for stdin)]:path:_files' \\
5528
5590
  '--notify-all[Notify all assignees]' \\
5529
5591
  '--mention[Mention a user (ID, email, username, or me)]:user:' \\
5530
5592
  '--json[Force JSON output]'
@@ -5992,6 +6054,7 @@ ${renderZshTopLevelCommands(name)}
5992
6054
  _arguments \\
5993
6055
  '1:channel_id:' \\
5994
6056
  '(-m --message)'{-m,--message}'[Message content]:text:' \\
6057
+ '--message-file[Read message from a file (- for stdin)]:path:_files' \\
5995
6058
  '--post[Send as a post]' \\
5996
6059
  '--title[Post title]:text:' \\
5997
6060
  '--json[Force JSON output]'
@@ -6006,6 +6069,7 @@ ${renderZshTopLevelCommands(name)}
6006
6069
  _arguments \\
6007
6070
  '1:message_id:' \\
6008
6071
  '(-m --message)'{-m,--message}'[Reply content]:text:' \\
6072
+ '--message-file[Read reply from a file (- for stdin)]:path:_files' \\
6009
6073
  '--json[Force JSON output]'
6010
6074
  ;;
6011
6075
  replies)
@@ -6064,6 +6128,7 @@ ${renderZshTopLevelCommands(name)}
6064
6128
  _arguments \\
6065
6129
  '1:message_id:' \\
6066
6130
  '(-m --message)'{-m,--message}'[New message content]:text:' \\
6131
+ '--message-file[Read message from a file (- for stdin)]:path:_files' \\
6067
6132
  '--json[Force JSON output]'
6068
6133
  ;;
6069
6134
  message-delete)
@@ -6229,7 +6294,7 @@ function generateCompletion(shell, name = "cup") {
6229
6294
  }
6230
6295
 
6231
6296
  // src/commands/skill.ts
6232
- import { readFileSync, realpathSync, mkdirSync, copyFileSync, existsSync } from "fs";
6297
+ import { readFileSync as readFileSync2, realpathSync, mkdirSync, copyFileSync, existsSync } from "fs";
6233
6298
  import { join as join2, dirname } from "path";
6234
6299
  import { homedir as homedir2 } from "os";
6235
6300
  import chalk8 from "chalk";
@@ -6246,7 +6311,7 @@ function skillPath() {
6246
6311
  throw new Error("SKILL.md not found. Reinstall with: npm install -g @krodak/clickup-cli");
6247
6312
  }
6248
6313
  function printSkill() {
6249
- return readFileSync(skillPath(), "utf-8");
6314
+ return readFileSync2(skillPath(), "utf-8");
6250
6315
  }
6251
6316
  function getAgentTargets() {
6252
6317
  const home = homedir2();
@@ -8578,6 +8643,18 @@ function splitCommaList(value) {
8578
8643
  function collect(value, previous) {
8579
8644
  return [...previous, value];
8580
8645
  }
8646
+ function resolveRequiredMessage(opts) {
8647
+ const message = resolveTextInput({
8648
+ inline: opts.message,
8649
+ file: opts.messageFile,
8650
+ inlineFlag: "-m",
8651
+ fileFlag: "--message-file"
8652
+ });
8653
+ if (message === void 0) {
8654
+ throw new Error("Provide a message with -m/--message or --message-file");
8655
+ }
8656
+ return message;
8657
+ }
8581
8658
  async function resolveMentions(client, teamId, mentions) {
8582
8659
  if (mentions.length === 0) return [];
8583
8660
  const resolve2 = createCachedMemberResolver(client, teamId);
@@ -8706,6 +8783,9 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8706
8783
  })
8707
8784
  );
8708
8785
  program.command("update <taskId>").description("Update a task").option("-n, --name <text>", "New task name").option("-d, --description <text>", "New description (markdown supported)").option(
8786
+ "--description-file <path>",
8787
+ 'Read description from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -d'
8788
+ ).option(
8709
8789
  "-s, --status <status>",
8710
8790
  'New status (fuzzy matched, e.g. "prog" matches "in progress")'
8711
8791
  ).option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option(
@@ -8723,9 +8803,18 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8723
8803
  ).option(
8724
8804
  "--remove-group-assignee <groupIds...>",
8725
8805
  "Remove group assignee (UUID or @handle, can repeat or comma-separated)"
8726
- ).option("--parent <taskId>", "Set parent task (makes this a subtask)").option("--archive", "Archive the task").option("--unarchive", "Unarchive the task").option("--type <type>", "Change task type (name or custom_item_id)").option("--field <nameAndValue...>", 'Set custom field: --field "Name" value').option("--json", "Force JSON output even in terminal").action(
8806
+ ).option(
8807
+ "--parent <taskId>",
8808
+ "Set parent task (makes this a subtask): native id, custom id, or task URL"
8809
+ ).option("--archive", "Archive the task").option("--unarchive", "Unarchive the task").option("--type <type>", "Change task type (name or custom_item_id)").option("--field <nameAndValue...>", 'Set custom field: --field "Name" value').option("--json", "Force JSON output even in terminal").action(
8727
8810
  wrapAction(
8728
8811
  async (taskId, opts) => {
8812
+ opts.description = resolveTextInput({
8813
+ inline: opts.description,
8814
+ file: opts.descriptionFile,
8815
+ inlineFlag: "-d",
8816
+ fileFlag: "--description-file"
8817
+ });
8729
8818
  const config = loadConfig(getProfileName());
8730
8819
  const client = new ClickUpClient(config);
8731
8820
  const [timezone] = await Promise.all([
@@ -8783,7 +8872,13 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8783
8872
  }
8784
8873
  )
8785
8874
  );
8786
- program.command("create").description("Create a new task").option("-l, --list <listId>", 'Target list ID or "sprint:current" for active sprint').requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description (markdown supported)").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option(
8875
+ program.command("create").description("Create a new task").option("-l, --list <listId>", 'Target list ID or "sprint:current" for active sprint').requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description (markdown supported)").option(
8876
+ "--description-file <path>",
8877
+ 'Read description from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -d'
8878
+ ).option(
8879
+ "-p, --parent <taskId>",
8880
+ "Parent task: native id, custom id (e.g. PROD-811), or task URL (list auto-detected)"
8881
+ ).option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option(
8787
8882
  "--due-date <date>",
8788
8883
  "Due date (YYYY-MM-DD, YYYY-MM-DDTHH:MM[:SS], or full ISO 8601 with offset)"
8789
8884
  ).option(
@@ -8792,6 +8887,12 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8792
8887
  ).option("--assignee <userId>", 'Assignee user ID or "me"').option("--group-assignee <groupIds>", "Group assignees (UUID or @handle, comma-separated)").option("--tags <tags>", "Comma-separated tag names").option("--custom-item-id <id>", "Custom task type ID (use to create initiatives)").option("--time-estimate <duration>", 'Time estimate (e.g. "2h", "30m", "1h30m")').option("--template <id>", "Create from a task template (find IDs with cup templates)").option("--field <nameAndValue...>", 'Set custom field: --field "Name" value (can repeat)').option("--json", "Force JSON output even in terminal").action(
8793
8888
  wrapAction(
8794
8889
  async (opts) => {
8890
+ opts.description = resolveTextInput({
8891
+ inline: opts.description,
8892
+ file: opts.descriptionFile,
8893
+ inlineFlag: "-d",
8894
+ fileFlag: "--description-file"
8895
+ });
8795
8896
  const config = loadConfig(getProfileName());
8796
8897
  if (opts.list === "sprint:current") {
8797
8898
  opts.list = await resolveActiveSprintListId(config);
@@ -8877,7 +8978,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8877
8978
  }
8878
8979
  )
8879
8980
  );
8880
- program.command("comment <taskId>").description("Post a comment on a task").requiredOption("-m, --message <text>", "Comment text").option("--notify-all", "Notify all assignees").option(
8981
+ program.command("comment <taskId>").description("Post a comment on a task").option("-m, --message <text>", "Comment text").option(
8982
+ "--message-file <path>",
8983
+ 'Read comment from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
8984
+ ).option("--notify-all", "Notify all assignees").option(
8881
8985
  "--mention <user>",
8882
8986
  'Prepend an @mention (ID, email, username, or "me"), repeatable. For a mid-sentence mention, put a <@userId> token in -m instead; bare @Name is not parsed.',
8883
8987
  collect,
@@ -8885,10 +8989,11 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8885
8989
  ).option("--json", "Force JSON output even in terminal").action(
8886
8990
  wrapAction(
8887
8991
  async (taskId, opts) => {
8992
+ const message = resolveRequiredMessage(opts);
8888
8993
  const config = loadConfig(getProfileName());
8889
8994
  const client = new ClickUpClient(config);
8890
8995
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
8891
- const result = await postComment(config, taskId, opts.message, opts.notifyAll, mentionIds);
8996
+ const result = await postComment(config, taskId, message, opts.notifyAll, mentionIds);
8892
8997
  if (shouldOutputJson(opts.json ?? false)) {
8893
8998
  console.log(JSON.stringify(result, null, 2));
8894
8999
  } else {
@@ -8904,7 +9009,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8904
9009
  printComments(comments, opts.json ?? false);
8905
9010
  })
8906
9011
  );
8907
- program.command("comment-edit <commentId>").description("Edit an existing comment").option("-m, --message <text>", "New comment text").option("--resolved", "Mark comment as resolved").option("--unresolved", "Mark comment as unresolved").option(
9012
+ program.command("comment-edit <commentId>").description("Edit an existing comment").option("-m, --message <text>", "New comment text").option(
9013
+ "--message-file <path>",
9014
+ 'Read comment from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
9015
+ ).option("--resolved", "Mark comment as resolved").option("--unresolved", "Mark comment as unresolved").option(
8908
9016
  "--mention <user>",
8909
9017
  'Prepend an @mention (ID, email, username, or "me"), repeatable. For a mid-sentence mention, put a <@userId> token in -m instead; bare @Name is not parsed.',
8910
9018
  collect,
@@ -8912,13 +9020,19 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8912
9020
  ).option("--json", "Force JSON output even in terminal").action(
8913
9021
  wrapAction(
8914
9022
  async (commentId, opts) => {
9023
+ const message = resolveTextInput({
9024
+ inline: opts.message,
9025
+ file: opts.messageFile,
9026
+ inlineFlag: "-m",
9027
+ fileFlag: "--message-file"
9028
+ });
8915
9029
  const config = loadConfig(getProfileName());
8916
9030
  let resolved;
8917
9031
  if (opts.resolved) resolved = true;
8918
9032
  if (opts.unresolved) resolved = false;
8919
9033
  const client = new ClickUpClient(config);
8920
9034
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
8921
- await editComment(config, commentId, opts.message, resolved, mentionIds);
9035
+ await editComment(config, commentId, message, resolved, mentionIds);
8922
9036
  if (shouldOutputJson(opts.json ?? false)) {
8923
9037
  console.log(JSON.stringify({ success: true, commentId }, null, 2));
8924
9038
  } else {
@@ -8977,7 +9091,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8977
9091
  }
8978
9092
  })
8979
9093
  );
8980
- program.command("reply <commentId>").description("Reply to a comment").requiredOption("-m, --message <text>", "Reply text").option("--notify-all", "Notify all assignees").option(
9094
+ program.command("reply <commentId>").description("Reply to a comment").option("-m, --message <text>", "Reply text").option(
9095
+ "--message-file <path>",
9096
+ 'Read reply from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
9097
+ ).option("--notify-all", "Notify all assignees").option(
8981
9098
  "--mention <user>",
8982
9099
  'Prepend an @mention (ID, email, username, or "me"), repeatable. For a mid-sentence mention, put a <@userId> token in -m instead; bare @Name is not parsed.',
8983
9100
  collect,
@@ -8985,10 +9102,11 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8985
9102
  ).option("--json", "Force JSON output even in terminal").action(
8986
9103
  wrapAction(
8987
9104
  async (commentId, opts) => {
9105
+ const message = resolveRequiredMessage(opts);
8988
9106
  const config = loadConfig(getProfileName());
8989
9107
  const client = new ClickUpClient(config);
8990
9108
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
8991
- await createReply(config, commentId, opts.message, opts.notifyAll, mentionIds);
9109
+ await createReply(config, commentId, message, opts.notifyAll, mentionIds);
8992
9110
  if (shouldOutputJson(opts.json ?? false)) {
8993
9111
  console.log(JSON.stringify({ success: true, commentId }, null, 2));
8994
9112
  } else {
@@ -10516,15 +10634,19 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10516
10634
  }
10517
10635
  })
10518
10636
  );
10519
- chatCmd.command("send <channelId>").description("Send a message to a channel").requiredOption("-m, --message <text>", "Message content (markdown supported)").option("--post", "Send as a post instead of a message").option("--title <title>", "Post title (requires --post)").option("--json", "Force JSON output even in terminal").action(
10637
+ chatCmd.command("send <channelId>").description("Send a message to a channel").option("-m, --message <text>", "Message content (markdown supported)").option(
10638
+ "--message-file <path>",
10639
+ 'Read message from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
10640
+ ).option("--post", "Send as a post instead of a message").option("--title <title>", "Post title (requires --post)").option("--json", "Force JSON output even in terminal").action(
10520
10641
  wrapAction(
10521
10642
  async (channelId, opts) => {
10643
+ const message = resolveRequiredMessage(opts);
10522
10644
  if (opts.title && !opts.post) {
10523
10645
  throw new Error("--title requires --post");
10524
10646
  }
10525
10647
  const config = loadConfig(getProfileName());
10526
10648
  const client = new ClickUpClient(config);
10527
- const result = await client.sendChatMessage(channelId, opts.message, {
10649
+ const result = await client.sendChatMessage(channelId, message, {
10528
10650
  type: opts.post ? "post" : "message",
10529
10651
  postTitle: opts.title
10530
10652
  });
@@ -10664,17 +10786,23 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10664
10786
  }
10665
10787
  })
10666
10788
  );
10667
- chatCmd.command("reply <messageId>").description("Reply to a message").requiredOption("-m, --message <text>", "Reply content (markdown supported)").option("--json", "Force JSON output even in terminal").action(
10668
- wrapAction(async (messageId, opts) => {
10669
- const config = loadConfig(getProfileName());
10670
- const client = new ClickUpClient(config);
10671
- const result = await client.createChatMessageReply(messageId, opts.message);
10672
- if (shouldOutputJson(opts.json ?? false)) {
10673
- console.log(JSON.stringify(result, null, 2));
10674
- } else {
10675
- console.log(`Reply sent (id: ${result.id})`);
10789
+ chatCmd.command("reply <messageId>").description("Reply to a message").option("-m, --message <text>", "Reply content (markdown supported)").option(
10790
+ "--message-file <path>",
10791
+ 'Read reply from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
10792
+ ).option("--json", "Force JSON output even in terminal").action(
10793
+ wrapAction(
10794
+ async (messageId, opts) => {
10795
+ const message = resolveRequiredMessage(opts);
10796
+ const config = loadConfig(getProfileName());
10797
+ const client = new ClickUpClient(config);
10798
+ const result = await client.createChatMessageReply(messageId, message);
10799
+ if (shouldOutputJson(opts.json ?? false)) {
10800
+ console.log(JSON.stringify(result, null, 2));
10801
+ } else {
10802
+ console.log(`Reply sent (id: ${result.id})`);
10803
+ }
10676
10804
  }
10677
- })
10805
+ )
10678
10806
  );
10679
10807
  chatCmd.command("replies <messageId>").description("List replies to a message").option("--limit <n>", "Max replies (default: 50)").option("--json", "Force JSON output even in terminal").action(
10680
10808
  wrapAction(async (messageId, opts) => {
@@ -10732,17 +10860,23 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10732
10860
  }
10733
10861
  })
10734
10862
  );
10735
- chatCmd.command("message-update <messageId>").description("Edit a message").requiredOption("-m, --message <text>", "New message content").option("--json", "Force JSON output even in terminal").action(
10736
- wrapAction(async (messageId, opts) => {
10737
- const config = loadConfig(getProfileName());
10738
- const client = new ClickUpClient(config);
10739
- const result = await client.updateChatMessage(messageId, opts.message);
10740
- if (shouldOutputJson(opts.json ?? false)) {
10741
- console.log(JSON.stringify(result, null, 2));
10742
- } else {
10743
- console.log(`Message ${messageId} updated`);
10863
+ chatCmd.command("message-update <messageId>").description("Edit a message").option("-m, --message <text>", "New message content").option(
10864
+ "--message-file <path>",
10865
+ 'Read message from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
10866
+ ).option("--json", "Force JSON output even in terminal").action(
10867
+ wrapAction(
10868
+ async (messageId, opts) => {
10869
+ const message = resolveRequiredMessage(opts);
10870
+ const config = loadConfig(getProfileName());
10871
+ const client = new ClickUpClient(config);
10872
+ const result = await client.updateChatMessage(messageId, message);
10873
+ if (shouldOutputJson(opts.json ?? false)) {
10874
+ console.log(JSON.stringify(result, null, 2));
10875
+ } else {
10876
+ console.log(`Message ${messageId} updated`);
10877
+ }
10744
10878
  }
10745
- })
10879
+ )
10746
10880
  );
10747
10881
  chatCmd.command("message-delete <messageId>").description("Delete a message").option("--confirm", "Skip confirmation prompt").option("--json", "Force JSON output even in terminal").action(
10748
10882
  wrapAction(async (messageId, opts) => {
@@ -10817,7 +10951,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10817
10951
  printComments(comments, opts.json ?? false);
10818
10952
  })
10819
10953
  );
10820
- program.command("list-comment <listId>").description("Post a comment on a list").requiredOption("-m, --message <text>", "Comment text").option("--notify-all", "Notify all assignees").option(
10954
+ program.command("list-comment <listId>").description("Post a comment on a list").option("-m, --message <text>", "Comment text").option(
10955
+ "--message-file <path>",
10956
+ 'Read comment from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
10957
+ ).option("--notify-all", "Notify all assignees").option(
10821
10958
  "--mention <user>",
10822
10959
  'Prepend an @mention (ID, email, username, or "me"), repeatable. For a mid-sentence mention, put a <@userId> token in -m instead; bare @Name is not parsed.',
10823
10960
  collect,
@@ -10825,13 +10962,14 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10825
10962
  ).option("--json", "Force JSON output even in terminal").action(
10826
10963
  wrapAction(
10827
10964
  async (listId, opts) => {
10965
+ const message = resolveRequiredMessage(opts);
10828
10966
  const config = loadConfig(getProfileName());
10829
10967
  const client = new ClickUpClient(config);
10830
10968
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
10831
10969
  const result = await postListCommentCommand(
10832
10970
  config,
10833
10971
  listId,
10834
- opts.message,
10972
+ message,
10835
10973
  opts.notifyAll,
10836
10974
  mentionIds
10837
10975
  );
@@ -10850,7 +10988,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10850
10988
  printComments(comments, opts.json ?? false);
10851
10989
  })
10852
10990
  );
10853
- program.command("view-comment <viewId>").description("Post a comment on a view").requiredOption("-m, --message <text>", "Comment text").option("--notify-all", "Notify all assignees").option(
10991
+ program.command("view-comment <viewId>").description("Post a comment on a view").option("-m, --message <text>", "Comment text").option(
10992
+ "--message-file <path>",
10993
+ 'Read comment from a file ("-" for stdin); avoids shell quoting. Mutually exclusive with -m'
10994
+ ).option("--notify-all", "Notify all assignees").option(
10854
10995
  "--mention <user>",
10855
10996
  'Prepend an @mention (ID, email, username, or "me"), repeatable. For a mid-sentence mention, put a <@userId> token in -m instead; bare @Name is not parsed.',
10856
10997
  collect,
@@ -10858,13 +10999,14 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10858
10999
  ).option("--json", "Force JSON output even in terminal").action(
10859
11000
  wrapAction(
10860
11001
  async (viewId, opts) => {
11002
+ const message = resolveRequiredMessage(opts);
10861
11003
  const config = loadConfig(getProfileName());
10862
11004
  const client = new ClickUpClient(config);
10863
11005
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
10864
11006
  const result = await postViewCommentCommand(
10865
11007
  config,
10866
11008
  viewId,
10867
- opts.message,
11009
+ message,
10868
11010
  opts.notifyAll,
10869
11011
  mentionIds
10870
11012
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.38.2",
3
+ "version": "1.39.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,11 +3,11 @@ name: clickup
3
3
  description: 'Use when managing ClickUp tasks, sprints, or comments via the `cup` CLI tool. Triggers: task queries, status updates, sprint tracking, creating subtasks, posting comments, threaded replies, standup summaries, searching tasks, checking overdue items, assigning tasks, listing spaces and lists, opening tasks in browser, checking auth or config, setting custom fields, deleting tasks, managing tags, managing checklists, editing comments, task links, time tracking, attachments, file uploads, listing members, listing fields, duplicating tasks, bulk operations, goals, key results, saved filters, favorites.'
4
4
  ---
5
5
 
6
- # ClickUp CLI (`cup`) - skill version 1.38.2
6
+ # ClickUp CLI (`cup`) - skill version 1.39.0
7
7
 
8
8
  Reference for AI agents using the `cup` CLI tool. Covers task management, sprint tracking, comments, time tracking, custom fields, goals, docs, and project workflows.
9
9
 
10
- > **Version check:** Run `cup --version`. If your installed version is older than 1.38.2, update with `npm install -g @krodak/clickup-cli` and refresh this skill with `cup skill`.
10
+ > **Version check:** Run `cup --version`. If your installed version is older than 1.39.0, update with `npm install -g @krodak/clickup-cli` and refresh this skill with `cup skill`.
11
11
 
12
12
  ## Install & Configure
13
13
 
@@ -163,13 +163,13 @@ All commands support `--help` for full flag details. All commands support `--jso
163
163
 
164
164
  | Command | What it does |
165
165
  | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
166
- | `cup create -n name [-l listId\|sprint:current] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--start-date d] [--time-estimate t] [--assignee id\|me] [--group-assignee uuid\|@handle,...] [--tags t] [--custom-item-id n] [--template id] [--field "Name" val]` | Create task (`--list` accepts `sprint:current`, `--field` sets custom fields inline) |
167
- | `cup update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d\|none] [--start-date d] [--time-estimate t] [--assignee id\|me] [--remove-assignee id\|me] [--group-assignee uuid\|@handle] [--remove-group-assignee uuid\|@handle] [--parent id] [--archive] [--unarchive] [--type type] [--field "Name" val]` | Update task fields (including custom fields and group assignees) |
168
- | `cup comment <id> -m text [--notify-all] [--mention user]` | Post comment (markdown auto-converted to rich text; `--mention` for real @mentions, repeatable) |
169
- | `cup comment-edit <commentId> -m text [--resolved] [--unresolved] [--mention user]` | Edit a comment (markdown auto-converted to rich text; `--mention` for real @mentions) |
166
+ | `cup create -n name [-l listId\|sprint:current] [-p parentId] [-d desc\|--description-file path] [-s status] [--priority p] [--due-date d] [--start-date d] [--time-estimate t] [--assignee id\|me] [--group-assignee uuid\|@handle,...] [--tags t] [--custom-item-id n] [--template id] [--field "Name" val]` | Create task (`--list` accepts `sprint:current`, `--field` sets custom fields inline; `--description-file` reads markdown from a file or `-` for stdin) |
167
+ | `cup update <id> [-n name] [-d desc\|--description-file path] [-s status] [--priority p] [--due-date d\|none] [--start-date d] [--time-estimate t] [--assignee id\|me] [--remove-assignee id\|me] [--group-assignee uuid\|@handle] [--remove-group-assignee uuid\|@handle] [--parent id] [--archive] [--unarchive] [--type type] [--field "Name" val]` | Update task fields (`--description-file` reads markdown from a file or `-` for stdin) |
168
+ | `cup comment <id> -m text\|--message-file path [--notify-all] [--mention user]` | Post comment (markdown auto-converted to rich text; `--message-file` reads from a file or `-` for stdin; `--mention` for real @mentions, repeatable) |
169
+ | `cup comment-edit <commentId> -m text\|--message-file path [--resolved] [--unresolved] [--mention user]` | Edit a comment (markdown auto-converted to rich text; `--mention` for real @mentions) |
170
170
  | `cup comment-delete <commentId>` or `cup comment-delete --task <taskId> --mine [--match text]` | Delete a comment by ID or delete one of your task comments |
171
171
  | `cup replies <commentId>` | List threaded replies |
172
- | `cup reply <commentId> -m text [--notify-all] [--mention user]` | Reply to a comment (markdown auto-converted to rich text; `--mention` for real @mentions) |
172
+ | `cup reply <commentId> -m text\|--message-file path [--notify-all] [--mention user]` | Reply to a comment (markdown auto-converted to rich text; `--message-file` reads from a file or `-` for stdin; `--mention` for real @mentions) |
173
173
  | `cup assign <id> [--to ids\|me] [--remove ids\|me] [--group uuid\|@handle,...] [--remove-group uuid\|@handle,...]` | Assign/unassign users and groups (all flags accept comma-separated values) |
174
174
  | `cup depend <id> [--on taskId] [--blocks taskId] [--remove]` | Add/remove dependencies |
175
175
  | `cup move <id> [--to listId\|sprint:current] [--remove listId]` | Add/remove task from lists. **`--to` + `--remove` together changes the task's _home_ list** (uses v3 `home_list` endpoint with auto status mapping). `--to` alone adds multi-list membership. `--to` accepts `sprint:current`. |
@@ -292,7 +292,7 @@ All commands support `--help` for full flag details. All commands support `--jso
292
292
  | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
293
293
  | Errors | stderr with exit code 1. Strict parsing - excess/unknown arguments rejected |
294
294
  | Rate limiting | The client auto-retries on HTTP 429 and transient 5xx (502/503/504) with `Retry-After`-aware backoff (up to 3 retries). Retry warnings go to stderr. No action needed by callers |
295
- | Description quoting | Use `$'...'` quoting for `-d` / `-m` values containing backticks or newlines: `-d $'Use \`init()\` first.\n\n- Step 1'`. Heredocs and double quotes strip backticks |
295
+ | Multiline markdown (`-d` / `-m`) | **Best for agents:** use `--description-file <path>` / `--message-file <path>` (or `-` for stdin) to bypass shell quoting entirely — see the multiline example below. For inline multiline, use a **quoted heredoc** `"$(cat <<'EOF' … EOF)"` — it preserves backticks, newlines, and apostrophes. **Footgun:** never split a `$'...'` string with `'\''` for an apostrophe — the tail drops back to normal single quotes and `\n` becomes literal text (e.g. `$'…team'\''s…\n## Goals'` breaks). Plain double quotes execute backticks as commands. |
296
296
  | `--mention <user>` | Real ClickUp @mention (notifies the user). Accepts ID, email, username, or `me`. Repeatable. Also: `<@userId>` inline token in `-m` for mid-sentence mentions. Bare `@Name` is NOT parsed (ambiguous). On `comment`, `reply`, `comment-edit`, `list-comment`, `view-comment` |
297
297
  | Comment links | In comment messages, bare URLs (`https://...`) and markdown links (`[text](url)`) both render as clickable links. Unfurled preview cards are not supported (web-UI only) |
298
298
 
@@ -382,6 +382,34 @@ cup time log abc123def 2h -d "Code review"
382
382
  cup delete abc123def --confirm # irreversible!
383
383
  ```
384
384
 
385
+ ### Multiline markdown descriptions and comments
386
+
387
+ Agents produce structured markdown (headings, bullets, code, apostrophes). Shell quoting is the main failure point. Two robust patterns:
388
+
389
+ ```bash
390
+ # 1. Best for agents: write markdown to a file, then reference it (no shell quoting at all)
391
+ cup create -n "Improve onboarding" -l <listId> --description-file /tmp/desc.md
392
+ cup update <taskId> --description-file /tmp/desc.md
393
+ cup comment <taskId> --message-file /tmp/comment.md
394
+ # "-" reads stdin:
395
+ printf '## Notes\n\nTeam'\''s update with `code`.' | cup update <taskId> --description-file -
396
+
397
+ # 2. Inline multiline: quoted heredoc preserves backticks, newlines, AND apostrophes
398
+ cup create -n "Improve onboarding" -l <listId> -d "$(cat <<'EOF'
399
+ ## Overview
400
+
401
+ Refresh the team's onboarding docs. Use `init()` first.
402
+
403
+ ## Goals
404
+
405
+ - Goal one
406
+ - Goal two
407
+ EOF
408
+ )"
409
+ ```
410
+
411
+ Do **not** use `$'…team'\''s…\n\n## Goals'` — after the `'\''` apostrophe break the string is in normal single quotes, so `\n` is passed literally and headings/bullets render as `\n\n` text in ClickUp.
412
+
385
413
  ### Docs
386
414
 
387
415
  ```bash