@krodak/clickup-cli 1.38.3 → 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.3",
4
+ "version": "1.39.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/dist/index.js CHANGED
@@ -2450,6 +2450,34 @@ async function createTask(config, options) {
2450
2450
  return { id: task.id, name: task.name, url: task.url };
2451
2451
  }
2452
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
+
2453
2481
  // src/commands/get.ts
2454
2482
  async function getTask(config, taskId) {
2455
2483
  const client = new ClickUpClient(config);
@@ -3870,6 +3898,7 @@ var commandMetadata = [
3870
3898
  "--name",
3871
3899
  "-d",
3872
3900
  "--description",
3901
+ "--description-file",
3873
3902
  "-s",
3874
3903
  "--status",
3875
3904
  "--priority",
@@ -3899,6 +3928,7 @@ var commandMetadata = [
3899
3928
  "--name",
3900
3929
  "-d",
3901
3930
  "--description",
3931
+ "--description-file",
3902
3932
  "-p",
3903
3933
  "--parent",
3904
3934
  "-s",
@@ -3944,7 +3974,7 @@ var commandMetadata = [
3944
3974
  {
3945
3975
  name: "comment",
3946
3976
  description: "Post a comment on a task",
3947
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
3977
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
3948
3978
  quickReference: [
3949
3979
  { section: "write", usage: "comment <taskId>", description: "Post a comment on a task" }
3950
3980
  ]
@@ -3952,7 +3982,7 @@ var commandMetadata = [
3952
3982
  {
3953
3983
  name: "comment-edit",
3954
3984
  description: "Edit an existing comment",
3955
- flags: ["-m", "--message", "--resolved", "--unresolved", "--mention", "--json"],
3985
+ flags: ["-m", "--message", "--message-file", "--resolved", "--unresolved", "--mention", "--json"],
3956
3986
  quickReference: [
3957
3987
  {
3958
3988
  section: "write",
@@ -3996,7 +4026,7 @@ var commandMetadata = [
3996
4026
  {
3997
4027
  name: "reply",
3998
4028
  description: "Reply to a comment",
3999
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
4029
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
4000
4030
  quickReference: [
4001
4031
  { section: "write", usage: "reply <commentId>", description: "Reply to a comment" }
4002
4032
  ]
@@ -4831,7 +4861,7 @@ var commandMetadata = [
4831
4861
  {
4832
4862
  name: "list-comment",
4833
4863
  description: "Post a comment on a list",
4834
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
4864
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
4835
4865
  quickReference: [
4836
4866
  {
4837
4867
  section: "write",
@@ -4855,7 +4885,7 @@ var commandMetadata = [
4855
4885
  {
4856
4886
  name: "view-comment",
4857
4887
  description: "Post a comment on a view",
4858
- flags: ["-m", "--message", "--notify-all", "--mention", "--json"],
4888
+ flags: ["-m", "--message", "--message-file", "--notify-all", "--mention", "--json"],
4859
4889
  quickReference: [
4860
4890
  {
4861
4891
  section: "write",
@@ -5242,6 +5272,7 @@ ${renderZshTopLevelCommands(name)}
5242
5272
  '1:task_id:' \\
5243
5273
  '(-n --name)'{-n,--name}'[New task name]:text:' \\
5244
5274
  '(-d --description)'{-d,--description}'[New description]:text:' \\
5275
+ '--description-file[Read description from a file (- for stdin)]:path:_files' \\
5245
5276
  '(-s --status)'{-s,--status}'[New status]:status:(open "in progress" "in review" done closed)' \\
5246
5277
  '--priority[Priority level]:priority:(urgent high normal low)' \\
5247
5278
  '--due-date[Due date (YYYY-MM-DD or "none" to clear)]:date:' \\
@@ -5260,6 +5291,7 @@ ${renderZshTopLevelCommands(name)}
5260
5291
  '(-l --list)'{-l,--list}'[Target list ID]:list_id:' \\
5261
5292
  '(-n --name)'{-n,--name}'[Task name]:name:' \\
5262
5293
  '(-d --description)'{-d,--description}'[Task description]:text:' \\
5294
+ '--description-file[Read description from a file (- for stdin)]:path:_files' \\
5263
5295
  '(-p --parent)'{-p,--parent}'[Parent task ID]:task_id:' \\
5264
5296
  '(-s --status)'{-s,--status}'[Initial status]:status:(open "in progress" "in review" done closed)' \\
5265
5297
  '--priority[Priority level]:priority:(urgent high normal low)' \\
@@ -5296,6 +5328,7 @@ ${renderZshTopLevelCommands(name)}
5296
5328
  _arguments \\
5297
5329
  '1:task_id:' \\
5298
5330
  '(-m --message)'{-m,--message}'[Comment text]:text:' \\
5331
+ '--message-file[Read comment from a file (- for stdin)]:path:_files' \\
5299
5332
  '--notify-all[Notify all assignees]' \\
5300
5333
  '--mention[Mention a user (ID, email, username, or me)]:user:' \\
5301
5334
  '--json[Force JSON output]'
@@ -5530,6 +5563,7 @@ ${renderZshTopLevelCommands(name)}
5530
5563
  _arguments \\
5531
5564
  '1:comment_id:' \\
5532
5565
  '(-m --message)'{-m,--message}'[New comment text]:text:' \\
5566
+ '--message-file[Read comment from a file (- for stdin)]:path:_files' \\
5533
5567
  '--resolved[Mark comment as resolved]' \\
5534
5568
  '--unresolved[Mark comment as unresolved]' \\
5535
5569
  '--mention[Mention a user (ID, email, username, or me)]:user:' \\
@@ -5552,6 +5586,7 @@ ${renderZshTopLevelCommands(name)}
5552
5586
  _arguments \\
5553
5587
  '1:comment_id:' \\
5554
5588
  '(-m --message)'{-m,--message}'[Reply text]:text:' \\
5589
+ '--message-file[Read reply from a file (- for stdin)]:path:_files' \\
5555
5590
  '--notify-all[Notify all assignees]' \\
5556
5591
  '--mention[Mention a user (ID, email, username, or me)]:user:' \\
5557
5592
  '--json[Force JSON output]'
@@ -6019,6 +6054,7 @@ ${renderZshTopLevelCommands(name)}
6019
6054
  _arguments \\
6020
6055
  '1:channel_id:' \\
6021
6056
  '(-m --message)'{-m,--message}'[Message content]:text:' \\
6057
+ '--message-file[Read message from a file (- for stdin)]:path:_files' \\
6022
6058
  '--post[Send as a post]' \\
6023
6059
  '--title[Post title]:text:' \\
6024
6060
  '--json[Force JSON output]'
@@ -6033,6 +6069,7 @@ ${renderZshTopLevelCommands(name)}
6033
6069
  _arguments \\
6034
6070
  '1:message_id:' \\
6035
6071
  '(-m --message)'{-m,--message}'[Reply content]:text:' \\
6072
+ '--message-file[Read reply from a file (- for stdin)]:path:_files' \\
6036
6073
  '--json[Force JSON output]'
6037
6074
  ;;
6038
6075
  replies)
@@ -6091,6 +6128,7 @@ ${renderZshTopLevelCommands(name)}
6091
6128
  _arguments \\
6092
6129
  '1:message_id:' \\
6093
6130
  '(-m --message)'{-m,--message}'[New message content]:text:' \\
6131
+ '--message-file[Read message from a file (- for stdin)]:path:_files' \\
6094
6132
  '--json[Force JSON output]'
6095
6133
  ;;
6096
6134
  message-delete)
@@ -6256,7 +6294,7 @@ function generateCompletion(shell, name = "cup") {
6256
6294
  }
6257
6295
 
6258
6296
  // src/commands/skill.ts
6259
- import { readFileSync, realpathSync, mkdirSync, copyFileSync, existsSync } from "fs";
6297
+ import { readFileSync as readFileSync2, realpathSync, mkdirSync, copyFileSync, existsSync } from "fs";
6260
6298
  import { join as join2, dirname } from "path";
6261
6299
  import { homedir as homedir2 } from "os";
6262
6300
  import chalk8 from "chalk";
@@ -6273,7 +6311,7 @@ function skillPath() {
6273
6311
  throw new Error("SKILL.md not found. Reinstall with: npm install -g @krodak/clickup-cli");
6274
6312
  }
6275
6313
  function printSkill() {
6276
- return readFileSync(skillPath(), "utf-8");
6314
+ return readFileSync2(skillPath(), "utf-8");
6277
6315
  }
6278
6316
  function getAgentTargets() {
6279
6317
  const home = homedir2();
@@ -8605,6 +8643,18 @@ function splitCommaList(value) {
8605
8643
  function collect(value, previous) {
8606
8644
  return [...previous, value];
8607
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
+ }
8608
8658
  async function resolveMentions(client, teamId, mentions) {
8609
8659
  if (mentions.length === 0) return [];
8610
8660
  const resolve2 = createCachedMemberResolver(client, teamId);
@@ -8733,6 +8783,9 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8733
8783
  })
8734
8784
  );
8735
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(
8736
8789
  "-s, --status <status>",
8737
8790
  'New status (fuzzy matched, e.g. "prog" matches "in progress")'
8738
8791
  ).option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option(
@@ -8756,6 +8809,12 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8756
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(
8757
8810
  wrapAction(
8758
8811
  async (taskId, opts) => {
8812
+ opts.description = resolveTextInput({
8813
+ inline: opts.description,
8814
+ file: opts.descriptionFile,
8815
+ inlineFlag: "-d",
8816
+ fileFlag: "--description-file"
8817
+ });
8759
8818
  const config = loadConfig(getProfileName());
8760
8819
  const client = new ClickUpClient(config);
8761
8820
  const [timezone] = await Promise.all([
@@ -8814,6 +8873,9 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8814
8873
  )
8815
8874
  );
8816
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(
8817
8879
  "-p, --parent <taskId>",
8818
8880
  "Parent task: native id, custom id (e.g. PROD-811), or task URL (list auto-detected)"
8819
8881
  ).option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option(
@@ -8825,6 +8887,12 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8825
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(
8826
8888
  wrapAction(
8827
8889
  async (opts) => {
8890
+ opts.description = resolveTextInput({
8891
+ inline: opts.description,
8892
+ file: opts.descriptionFile,
8893
+ inlineFlag: "-d",
8894
+ fileFlag: "--description-file"
8895
+ });
8828
8896
  const config = loadConfig(getProfileName());
8829
8897
  if (opts.list === "sprint:current") {
8830
8898
  opts.list = await resolveActiveSprintListId(config);
@@ -8910,7 +8978,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8910
8978
  }
8911
8979
  )
8912
8980
  );
8913
- 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(
8914
8985
  "--mention <user>",
8915
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.',
8916
8987
  collect,
@@ -8918,10 +8989,11 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8918
8989
  ).option("--json", "Force JSON output even in terminal").action(
8919
8990
  wrapAction(
8920
8991
  async (taskId, opts) => {
8992
+ const message = resolveRequiredMessage(opts);
8921
8993
  const config = loadConfig(getProfileName());
8922
8994
  const client = new ClickUpClient(config);
8923
8995
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
8924
- const result = await postComment(config, taskId, opts.message, opts.notifyAll, mentionIds);
8996
+ const result = await postComment(config, taskId, message, opts.notifyAll, mentionIds);
8925
8997
  if (shouldOutputJson(opts.json ?? false)) {
8926
8998
  console.log(JSON.stringify(result, null, 2));
8927
8999
  } else {
@@ -8937,7 +9009,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8937
9009
  printComments(comments, opts.json ?? false);
8938
9010
  })
8939
9011
  );
8940
- 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(
8941
9016
  "--mention <user>",
8942
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.',
8943
9018
  collect,
@@ -8945,13 +9020,19 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
8945
9020
  ).option("--json", "Force JSON output even in terminal").action(
8946
9021
  wrapAction(
8947
9022
  async (commentId, opts) => {
9023
+ const message = resolveTextInput({
9024
+ inline: opts.message,
9025
+ file: opts.messageFile,
9026
+ inlineFlag: "-m",
9027
+ fileFlag: "--message-file"
9028
+ });
8948
9029
  const config = loadConfig(getProfileName());
8949
9030
  let resolved;
8950
9031
  if (opts.resolved) resolved = true;
8951
9032
  if (opts.unresolved) resolved = false;
8952
9033
  const client = new ClickUpClient(config);
8953
9034
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
8954
- await editComment(config, commentId, opts.message, resolved, mentionIds);
9035
+ await editComment(config, commentId, message, resolved, mentionIds);
8955
9036
  if (shouldOutputJson(opts.json ?? false)) {
8956
9037
  console.log(JSON.stringify({ success: true, commentId }, null, 2));
8957
9038
  } else {
@@ -9010,7 +9091,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
9010
9091
  }
9011
9092
  })
9012
9093
  );
9013
- 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(
9014
9098
  "--mention <user>",
9015
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.',
9016
9100
  collect,
@@ -9018,10 +9102,11 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
9018
9102
  ).option("--json", "Force JSON output even in terminal").action(
9019
9103
  wrapAction(
9020
9104
  async (commentId, opts) => {
9105
+ const message = resolveRequiredMessage(opts);
9021
9106
  const config = loadConfig(getProfileName());
9022
9107
  const client = new ClickUpClient(config);
9023
9108
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
9024
- await createReply(config, commentId, opts.message, opts.notifyAll, mentionIds);
9109
+ await createReply(config, commentId, message, opts.notifyAll, mentionIds);
9025
9110
  if (shouldOutputJson(opts.json ?? false)) {
9026
9111
  console.log(JSON.stringify({ success: true, commentId }, null, 2));
9027
9112
  } else {
@@ -10549,15 +10634,19 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10549
10634
  }
10550
10635
  })
10551
10636
  );
10552
- 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(
10553
10641
  wrapAction(
10554
10642
  async (channelId, opts) => {
10643
+ const message = resolveRequiredMessage(opts);
10555
10644
  if (opts.title && !opts.post) {
10556
10645
  throw new Error("--title requires --post");
10557
10646
  }
10558
10647
  const config = loadConfig(getProfileName());
10559
10648
  const client = new ClickUpClient(config);
10560
- const result = await client.sendChatMessage(channelId, opts.message, {
10649
+ const result = await client.sendChatMessage(channelId, message, {
10561
10650
  type: opts.post ? "post" : "message",
10562
10651
  postTitle: opts.title
10563
10652
  });
@@ -10697,17 +10786,23 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10697
10786
  }
10698
10787
  })
10699
10788
  );
10700
- 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(
10701
- wrapAction(async (messageId, opts) => {
10702
- const config = loadConfig(getProfileName());
10703
- const client = new ClickUpClient(config);
10704
- const result = await client.createChatMessageReply(messageId, opts.message);
10705
- if (shouldOutputJson(opts.json ?? false)) {
10706
- console.log(JSON.stringify(result, null, 2));
10707
- } else {
10708
- 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
+ }
10709
10804
  }
10710
- })
10805
+ )
10711
10806
  );
10712
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(
10713
10808
  wrapAction(async (messageId, opts) => {
@@ -10765,17 +10860,23 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10765
10860
  }
10766
10861
  })
10767
10862
  );
10768
- 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(
10769
- wrapAction(async (messageId, opts) => {
10770
- const config = loadConfig(getProfileName());
10771
- const client = new ClickUpClient(config);
10772
- const result = await client.updateChatMessage(messageId, opts.message);
10773
- if (shouldOutputJson(opts.json ?? false)) {
10774
- console.log(JSON.stringify(result, null, 2));
10775
- } else {
10776
- 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
+ }
10777
10878
  }
10778
- })
10879
+ )
10779
10880
  );
10780
10881
  chatCmd.command("message-delete <messageId>").description("Delete a message").option("--confirm", "Skip confirmation prompt").option("--json", "Force JSON output even in terminal").action(
10781
10882
  wrapAction(async (messageId, opts) => {
@@ -10850,7 +10951,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10850
10951
  printComments(comments, opts.json ?? false);
10851
10952
  })
10852
10953
  );
10853
- 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(
10854
10958
  "--mention <user>",
10855
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.',
10856
10960
  collect,
@@ -10858,13 +10962,14 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10858
10962
  ).option("--json", "Force JSON output even in terminal").action(
10859
10963
  wrapAction(
10860
10964
  async (listId, opts) => {
10965
+ const message = resolveRequiredMessage(opts);
10861
10966
  const config = loadConfig(getProfileName());
10862
10967
  const client = new ClickUpClient(config);
10863
10968
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
10864
10969
  const result = await postListCommentCommand(
10865
10970
  config,
10866
10971
  listId,
10867
- opts.message,
10972
+ message,
10868
10973
  opts.notifyAll,
10869
10974
  mentionIds
10870
10975
  );
@@ -10883,7 +10988,10 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10883
10988
  printComments(comments, opts.json ?? false);
10884
10989
  })
10885
10990
  );
10886
- 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(
10887
10995
  "--mention <user>",
10888
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.',
10889
10997
  collect,
@@ -10891,13 +10999,14 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
10891
10999
  ).option("--json", "Force JSON output even in terminal").action(
10892
11000
  wrapAction(
10893
11001
  async (viewId, opts) => {
11002
+ const message = resolveRequiredMessage(opts);
10894
11003
  const config = loadConfig(getProfileName());
10895
11004
  const client = new ClickUpClient(config);
10896
11005
  const mentionIds = await resolveMentions(client, config.teamId, opts.mention);
10897
11006
  const result = await postViewCommentCommand(
10898
11007
  config,
10899
11008
  viewId,
10900
- opts.message,
11009
+ message,
10901
11010
  opts.notifyAll,
10902
11011
  mentionIds
10903
11012
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.38.3",
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.3
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.3, 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