@krodak/clickup-cli 1.16.1 → 1.17.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.16.1",
4
+ "version": "1.17.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/dist/index.js CHANGED
@@ -128,14 +128,22 @@ var ClickUpClient = class {
128
128
  }
129
129
  async getMe() {
130
130
  if (this.meCache) return this.meCache;
131
- const data = await this.request("/user");
131
+ const data = await this.request(
132
+ "/user"
133
+ );
132
134
  const user = expectRecordField(data, "user", "user");
135
+ const timezone = typeof user.timezone === "string" && user.timezone ? user.timezone : void 0;
133
136
  this.meCache = {
134
137
  id: expectNumericField(user, "id", "user"),
135
- username: expectStringField(user, "username", "user")
138
+ username: expectStringField(user, "username", "user"),
139
+ ...timezone ? { timezone } : {}
136
140
  };
137
141
  return this.meCache;
138
142
  }
143
+ async getUserTimezone() {
144
+ const me = await this.getMe();
145
+ return me.timezone;
146
+ }
139
147
  async paginate(buildPath) {
140
148
  const allTasks = [];
141
149
  let page = 0;
@@ -168,9 +176,22 @@ var ClickUpClient = class {
168
176
  const me = await this.getMe();
169
177
  baseParams.append("assignees[]", String(me.id));
170
178
  }
179
+ if (filters.assignees) {
180
+ for (const id of filters.assignees) baseParams.append("assignees[]", String(id));
181
+ }
171
182
  for (const s of filters.statuses ?? []) baseParams.append("statuses[]", s);
172
183
  for (const id of filters.listIds ?? []) baseParams.append("list_ids[]", id);
173
184
  for (const id of filters.spaceIds ?? []) baseParams.append("space_ids[]", id);
185
+ for (const tag of filters.tags ?? []) baseParams.append("tags[]", tag);
186
+ if (filters.dueDateGt) baseParams.set("due_date_gt", String(filters.dueDateGt));
187
+ if (filters.dueDateLt) baseParams.set("due_date_lt", String(filters.dueDateLt));
188
+ if (filters.dateCreatedGt) baseParams.set("date_created_gt", String(filters.dateCreatedGt));
189
+ if (filters.dateCreatedLt) baseParams.set("date_created_lt", String(filters.dateCreatedLt));
190
+ if (filters.dateUpdatedGt) baseParams.set("date_updated_gt", String(filters.dateUpdatedGt));
191
+ if (filters.dateUpdatedLt) baseParams.set("date_updated_lt", String(filters.dateUpdatedLt));
192
+ if (filters.customFields?.length) {
193
+ baseParams.set("custom_fields", JSON.stringify(filters.customFields));
194
+ }
174
195
  return this.paginate((page) => {
175
196
  const params = new URLSearchParams(baseParams);
176
197
  params.set("page", String(page));
@@ -304,21 +325,21 @@ var ClickUpClient = class {
304
325
  }
305
326
  async getView(viewId) {
306
327
  const data = await this.request(`/view/${viewId}`);
307
- return data.view;
328
+ return expectRecordField(data, "view", "view");
308
329
  }
309
330
  async createListView(listId, payload) {
310
331
  const data = await this.request(`/list/${listId}/view`, {
311
332
  method: "POST",
312
333
  body: JSON.stringify(payload)
313
334
  });
314
- return data.view;
335
+ return expectRecordField(data, "view", "view");
315
336
  }
316
337
  async updateView(viewId, payload) {
317
338
  const data = await this.request(`/view/${viewId}`, {
318
339
  method: "PUT",
319
340
  body: JSON.stringify(payload)
320
341
  });
321
- return data.view;
342
+ return expectRecordField(data, "view", "view");
322
343
  }
323
344
  async deleteView(viewId) {
324
345
  await this.request(`/view/${viewId}`, { method: "DELETE" });
@@ -676,7 +697,7 @@ var ClickUpClient = class {
676
697
  async createGoal(teamId, name, opts) {
677
698
  const body = { name, multiple_owners: true };
678
699
  if (opts?.description) body.description = opts.description;
679
- if (opts?.dueDate) body.due_date = Number(opts.dueDate);
700
+ if (opts?.dueDate != null) body.due_date = opts.dueDate;
680
701
  if (opts?.color) body.color = opts.color;
681
702
  const data = await this.request(`/team/${teamId}/goal`, {
682
703
  method: "POST",
@@ -965,7 +986,7 @@ function loadConfig(profileName) {
965
986
  ...fileConfig.sprintFolderId ? { sprintFolderId: fileConfig.sprintFolderId } : {}
966
987
  };
967
988
  }
968
- const multi = migrateToMultiProfile(parsed, path);
989
+ const multi = loadMultiProfileConfig();
969
990
  const resolvedProfile = profileName ?? process.env.CU_PROFILE?.trim() ?? multi.defaultProfile;
970
991
  if (!resolvedProfile) {
971
992
  throw new Error("No default profile set. Run: cup profile use <name>");
@@ -1158,9 +1179,9 @@ function formatDuration(ms) {
1158
1179
  }
1159
1180
  function formatDateISO(ms) {
1160
1181
  const d = new Date(Number(ms));
1161
- const year = d.getFullYear();
1162
- const month = String(d.getMonth() + 1).padStart(2, "0");
1163
- const day = String(d.getDate()).padStart(2, "0");
1182
+ const year = d.getUTCFullYear();
1183
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
1184
+ const day = String(d.getUTCDate()).padStart(2, "0");
1164
1185
  return `${year}-${month}-${day}`;
1165
1186
  }
1166
1187
 
@@ -1725,14 +1746,40 @@ function parsePriority(value) {
1725
1746
  if (Number.isInteger(num) && num >= 1 && num <= 4) return num;
1726
1747
  throw new Error("Priority must be urgent, high, normal, low, or 1-4");
1727
1748
  }
1728
- function parseDueDate(value) {
1749
+ function parseDueDate(value, timezone) {
1729
1750
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1730
1751
  throw new Error("Date must be in YYYY-MM-DD format");
1731
1752
  }
1732
- const parts = value.split("-");
1733
- const date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
1734
- if (isNaN(date.getTime())) throw new Error(`Invalid date: ${value}`);
1735
- return date.getTime();
1753
+ const parts = value.split("-").map(Number);
1754
+ const y = parts[0];
1755
+ const m = parts[1];
1756
+ const d = parts[2];
1757
+ if (timezone) {
1758
+ try {
1759
+ const ms2 = dateToTimezoneMs(y, m, d, timezone);
1760
+ if (!isNaN(ms2)) return ms2;
1761
+ } catch {
1762
+ }
1763
+ }
1764
+ const ms = Date.UTC(y, m - 1, d);
1765
+ if (isNaN(ms)) throw new Error(`Invalid date: ${value}`);
1766
+ return ms;
1767
+ }
1768
+ function dateToTimezoneMs(year, month, day, timezone) {
1769
+ const approxUtc = new Date(Date.UTC(year, month - 1, day));
1770
+ const tzStr = approxUtc.toLocaleString("en-US", {
1771
+ timeZone: timezone,
1772
+ year: "numeric",
1773
+ month: "2-digit",
1774
+ day: "2-digit",
1775
+ hour: "2-digit",
1776
+ minute: "2-digit",
1777
+ second: "2-digit",
1778
+ hour12: false
1779
+ });
1780
+ const tzDate = /* @__PURE__ */ new Date(tzStr + " UTC");
1781
+ const offset = approxUtc.getTime() - tzDate.getTime();
1782
+ return approxUtc.getTime() + offset;
1736
1783
  }
1737
1784
  function parseAssigneeId(value) {
1738
1785
  const id = Number(value);
@@ -1761,7 +1808,7 @@ function parseTimeEstimate(value) {
1761
1808
  'Time estimate must be a duration (e.g. "2h", "30m", "1h30m"), milliseconds, or "0" to clear'
1762
1809
  );
1763
1810
  }
1764
- function buildUpdatePayload(opts) {
1811
+ function buildUpdatePayload(opts, timezone) {
1765
1812
  if (opts.archive && opts.unarchive) {
1766
1813
  throw new Error("Cannot use --archive and --unarchive together");
1767
1814
  }
@@ -1780,12 +1827,12 @@ function buildUpdatePayload(opts) {
1780
1827
  if (opts.dueDate === "none" || opts.dueDate === "clear") {
1781
1828
  payload.due_date = null;
1782
1829
  } else {
1783
- payload.due_date = parseDueDate(opts.dueDate);
1830
+ payload.due_date = parseDueDate(opts.dueDate, timezone);
1784
1831
  payload.due_date_time = false;
1785
1832
  }
1786
1833
  }
1787
1834
  if (opts.startDate !== void 0) {
1788
- payload.start_date = parseDueDate(opts.startDate);
1835
+ payload.start_date = parseDueDate(opts.startDate, timezone);
1789
1836
  payload.start_date_time = false;
1790
1837
  }
1791
1838
  if (opts.assignee !== void 0 || opts.removeAssignee !== void 0) {
@@ -1832,10 +1879,11 @@ async function updateTask(config, taskId, options) {
1832
1879
  "Provide at least one of: --name, --description, --status, --priority, --due-date, --start-date, --time-estimate, --assignee, --remove-assignee, --parent, --detach, --archive, --unarchive"
1833
1880
  );
1834
1881
  const client = new ClickUpClient(config);
1835
- if (options.status !== void 0) {
1836
- options.status = await resolveStatus(client, taskId, options.status);
1882
+ const resolved = { ...options };
1883
+ if (resolved.status !== void 0) {
1884
+ resolved.status = await resolveStatus(client, taskId, resolved.status);
1837
1885
  }
1838
- const task = await client.updateTask(taskId, options);
1886
+ const task = await client.updateTask(taskId, resolved);
1839
1887
  return { id: task.id, name: task.name };
1840
1888
  }
1841
1889
 
@@ -1855,6 +1903,7 @@ async function createTask(config, options) {
1855
1903
  const task2 = await client.createTaskFromTemplate(listId, options.template, options.name);
1856
1904
  return { id: task2.id, name: task2.name, url: task2.url };
1857
1905
  }
1906
+ const timezone = await client.getUserTimezone();
1858
1907
  const payload = {
1859
1908
  name: options.name,
1860
1909
  ...options.description !== void 0 ? { markdown_content: options.description } : {},
@@ -1865,11 +1914,11 @@ async function createTask(config, options) {
1865
1914
  payload.priority = parsePriority(options.priority);
1866
1915
  }
1867
1916
  if (options.dueDate !== void 0) {
1868
- payload.due_date = parseDueDate(options.dueDate);
1917
+ payload.due_date = parseDueDate(options.dueDate, timezone);
1869
1918
  payload.due_date_time = false;
1870
1919
  }
1871
1920
  if (options.startDate !== void 0) {
1872
- payload.start_date = parseDueDate(options.startDate);
1921
+ payload.start_date = parseDueDate(options.startDate, timezone);
1873
1922
  payload.start_date_time = false;
1874
1923
  }
1875
1924
  if (options.assignee !== void 0) {
@@ -2819,6 +2868,13 @@ var commandMetadata = [
2819
2868
  "--type",
2820
2869
  "--all",
2821
2870
  "--include-closed",
2871
+ "--assignee",
2872
+ "--tag",
2873
+ "--due-before",
2874
+ "--due-after",
2875
+ "--created-after",
2876
+ "--created-before",
2877
+ "--field",
2822
2878
  "--json"
2823
2879
  ],
2824
2880
  quickReference: [
@@ -3023,7 +3079,21 @@ var commandMetadata = [
3023
3079
  {
3024
3080
  name: "search",
3025
3081
  description: "Search my tasks by name (use --all for all tasks)",
3026
- flags: ["--status", "--all", "--include-closed", "--json"],
3082
+ flags: [
3083
+ "--status",
3084
+ "--list",
3085
+ "--space",
3086
+ "--all",
3087
+ "--include-closed",
3088
+ "--assignee",
3089
+ "--tag",
3090
+ "--due-before",
3091
+ "--due-after",
3092
+ "--created-after",
3093
+ "--created-before",
3094
+ "--field",
3095
+ "--json"
3096
+ ],
3027
3097
  quickReference: [
3028
3098
  { section: "read", usage: "search <query>", description: "Search my tasks by name" }
3029
3099
  ]
@@ -3783,7 +3853,15 @@ ${renderZshTopLevelCommands(name)}
3783
3853
  '--space[Filter by space ID]:space_id:' \\
3784
3854
  '--name[Filter by name]:query:' \\
3785
3855
  '--type[Filter by task type]:type:' \\
3856
+ '--all[Include all tasks, not just mine]' \\
3786
3857
  '--include-closed[Include done/closed tasks]' \\
3858
+ '--assignee[Filter by assignee]:user_id:' \\
3859
+ '--tag[Filter by tag name]:tag:' \\
3860
+ '--due-before[Tasks due before date]:date:' \\
3861
+ '--due-after[Tasks due after date]:date:' \\
3862
+ '--created-after[Tasks created after date]:date:' \\
3863
+ '--created-before[Tasks created before date]:date:' \\
3864
+ '--field[Filter by custom field]:field_name_and_value:' \\
3787
3865
  '--json[Force JSON output]'
3788
3866
  ;;
3789
3867
  task)
@@ -3897,8 +3975,17 @@ ${renderZshTopLevelCommands(name)}
3897
3975
  _arguments \\
3898
3976
  '1:query:' \\
3899
3977
  '--status[Filter by status]:status:(open "in progress" "in review" done closed)' \\
3900
- '--include-closed[Include done/closed tasks in search]' \\
3978
+ '--list[Filter by list ID]:list_id:' \\
3979
+ '--space[Filter by space ID]:space_id:' \\
3901
3980
  '--all[Search all workspace tasks, not just mine]' \\
3981
+ '--include-closed[Include done/closed tasks in search]' \\
3982
+ '--assignee[Filter by assignee]:user_id:' \\
3983
+ '--tag[Filter by tag name]:tag:' \\
3984
+ '--due-before[Tasks due before date]:date:' \\
3985
+ '--due-after[Tasks due after date]:date:' \\
3986
+ '--created-after[Tasks created after date]:date:' \\
3987
+ '--created-before[Tasks created before date]:date:' \\
3988
+ '--field[Filter by custom field]:field_name_and_value:' \\
3902
3989
  '--json[Force JSON output]'
3903
3990
  ;;
3904
3991
  summary)
@@ -4587,7 +4674,19 @@ async function searchTasks(config, query, opts = {}) {
4587
4674
  }
4588
4675
  const client = new ClickUpClient(config);
4589
4676
  const [allTasks, customTypes] = await Promise.all([
4590
- client.getMyTasks(config.teamId, { all: opts.all, includeClosed: opts.includeClosed }),
4677
+ client.getMyTasks(config.teamId, {
4678
+ all: opts.all,
4679
+ includeClosed: opts.includeClosed,
4680
+ listIds: opts.listIds,
4681
+ spaceIds: opts.spaceIds,
4682
+ assignees: opts.assignees,
4683
+ tags: opts.tags,
4684
+ dueDateGt: opts.dueDateGt,
4685
+ dueDateLt: opts.dueDateLt,
4686
+ dateCreatedGt: opts.dateCreatedGt,
4687
+ dateCreatedLt: opts.dateCreatedLt,
4688
+ customFields: opts.customFields
4689
+ }),
4591
4690
  client.getCustomTaskTypes(config.teamId)
4592
4691
  ]);
4593
4692
  const typeMap = buildTypeMap(customTypes);
@@ -5333,7 +5432,6 @@ function formatFieldsMarkdown(fields) {
5333
5432
  }
5334
5433
 
5335
5434
  // src/commands/duplicate.ts
5336
- var PRIORITY_MAP2 = { urgent: 1, high: 2, normal: 3, low: 4 };
5337
5435
  async function duplicateTask(config, taskId) {
5338
5436
  const client = new ClickUpClient(config);
5339
5437
  const task = await client.getTask(taskId);
@@ -5341,7 +5439,7 @@ async function duplicateTask(config, taskId) {
5341
5439
  name: `${task.name} (copy)`,
5342
5440
  description: task.description,
5343
5441
  markdown_content: task.markdown_content,
5344
- priority: task.priority ? PRIORITY_MAP2[task.priority.priority.toLowerCase()] : void 0,
5442
+ priority: task.priority ? parsePriority(task.priority.priority.toLowerCase()) : void 0,
5345
5443
  tags: task.tags?.map((t) => t.name),
5346
5444
  time_estimate: task.time_estimate ?? void 0
5347
5445
  });
@@ -5380,7 +5478,8 @@ async function bulkAssign(config, userIdOrMe, taskIds, action) {
5380
5478
  }
5381
5479
  async function bulkDueDate(config, date, taskIds) {
5382
5480
  const client = new ClickUpClient(config);
5383
- const payload = date === "none" || date === "clear" ? { due_date: null } : { due_date: parseDueDate(date), due_date_time: false };
5481
+ const timezone = await client.getUserTimezone();
5482
+ const payload = date === "none" || date === "clear" ? { due_date: null } : { due_date: parseDueDate(date, timezone), due_date_time: false };
5384
5483
  const failed = [];
5385
5484
  for (const id of taskIds) {
5386
5485
  try {
@@ -5438,7 +5537,12 @@ async function listGoals(config) {
5438
5537
  }
5439
5538
  async function createGoal(config, name, opts) {
5440
5539
  const client = new ClickUpClient(config);
5441
- return client.createGoal(config.teamId, name, opts);
5540
+ const timezone = await client.getUserTimezone();
5541
+ return client.createGoal(config.teamId, name, {
5542
+ description: opts?.description,
5543
+ color: opts?.color,
5544
+ ...opts?.dueDate ? { dueDate: parseDueDate(opts.dueDate, timezone) } : {}
5545
+ });
5442
5546
  }
5443
5547
  async function updateGoal(config, goalId, updates) {
5444
5548
  const client = new ClickUpClient(config);
@@ -5763,14 +5867,17 @@ function formatFiltersTable(filters) {
5763
5867
  }));
5764
5868
  return formatTable(rows, FILTER_COLUMNS);
5765
5869
  }
5870
+ function escapeMarkdownCell(value) {
5871
+ return value.replace(/\|/g, "\\|");
5872
+ }
5766
5873
  function formatFiltersMarkdown(filters) {
5767
5874
  const entries = Object.entries(filters);
5768
5875
  if (entries.length === 0) return "No filters saved";
5769
5876
  const lines = ["| Name | Command | Description |", "| --- | --- | --- |"];
5770
5877
  for (const [name, entry] of entries) {
5771
- const command = entry.command.join(" ");
5772
- const description = entry.description ?? "";
5773
- lines.push(`| ${name} | ${command} | ${description} |`);
5878
+ const command = escapeMarkdownCell(entry.command.join(" "));
5879
+ const description = escapeMarkdownCell(entry.description ?? "");
5880
+ lines.push(`| ${escapeMarkdownCell(name)} | ${command} | ${description} |`);
5774
5881
  }
5775
5882
  return lines.join("\n");
5776
5883
  }
@@ -5879,9 +5986,49 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
5879
5986
  program.command("tasks").description("List tasks assigned to me (use --all for all tasks)").option("--status <status>", 'Filter by status (e.g. "in progress")').option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--name <partial>", "Filter by name (case-insensitive contains)").option(
5880
5987
  "--type <type>",
5881
5988
  'Filter by task type (e.g. "task", "initiative", or custom type name/ID)'
5882
- ).option("--all", "Include all tasks, not just mine").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
5989
+ ).option("--all", "Include all tasks, not just mine").option("--include-closed", "Include done/closed tasks").option("--assignee <userId>", 'Filter by assignee (user ID or "me")').option("--tag <tag>", "Filter by tag name").option("--due-before <date>", "Tasks due before date (YYYY-MM-DD)").option("--due-after <date>", "Tasks due after date (YYYY-MM-DD)").option("--created-after <date>", "Tasks created after date (YYYY-MM-DD)").option("--created-before <date>", "Tasks created before date (YYYY-MM-DD)").option("--field <nameAndValue...>", 'Filter by custom field: --field "Name" value').option("--json", "Force JSON output even in terminal").action(
5883
5990
  wrapAction(async (opts) => {
5884
5991
  const config = loadConfig(getProfileName());
5992
+ let assigneeIds;
5993
+ if (opts.assignee) {
5994
+ if (opts.assignee === "me") {
5995
+ const client = new ClickUpClient(config);
5996
+ const me = await client.getMe();
5997
+ assigneeIds = [me.id];
5998
+ } else {
5999
+ assigneeIds = [Number(opts.assignee)];
6000
+ }
6001
+ }
6002
+ const parseDateFilter = (d) => {
6003
+ const parts = d.split("-");
6004
+ return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])).getTime();
6005
+ };
6006
+ let customFields;
6007
+ if (opts.field?.length) {
6008
+ if (opts.field.length % 2 !== 0) {
6009
+ throw new Error('--field requires pairs: --field "Name" value');
6010
+ }
6011
+ if (!opts.list) {
6012
+ throw new Error("--field filtering requires --list to resolve field names");
6013
+ }
6014
+ const client = new ClickUpClient(config);
6015
+ const fields = await client.getListCustomFields(opts.list);
6016
+ customFields = [];
6017
+ for (let i = 0; i < opts.field.length; i += 2) {
6018
+ const fieldName = opts.field[i];
6019
+ const fieldValue = opts.field[i + 1];
6020
+ const match = fields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase());
6021
+ if (!match) {
6022
+ const available = fields.map((f) => f.name).join(", ");
6023
+ throw new Error(`Field "${fieldName}" not found. Available: ${available}`);
6024
+ }
6025
+ customFields.push({
6026
+ field_id: match.id,
6027
+ operator: "=",
6028
+ value: fieldValue
6029
+ });
6030
+ }
6031
+ }
5885
6032
  const tasks = await fetchMyTasks(config, {
5886
6033
  typeFilter: opts.type,
5887
6034
  statuses: opts.status ? [opts.status] : void 0,
@@ -5889,6 +6036,13 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
5889
6036
  spaceIds: opts.space ? [opts.space] : void 0,
5890
6037
  name: opts.name,
5891
6038
  all: opts.all,
6039
+ assignees: assigneeIds,
6040
+ tags: opts.tag ? [opts.tag] : void 0,
6041
+ dueDateLt: opts.dueBefore ? parseDateFilter(opts.dueBefore) : void 0,
6042
+ dueDateGt: opts.dueAfter ? parseDateFilter(opts.dueAfter) : void 0,
6043
+ dateCreatedGt: opts.createdAfter ? parseDateFilter(opts.createdAfter) : void 0,
6044
+ dateCreatedLt: opts.createdBefore ? parseDateFilter(opts.createdBefore) : void 0,
6045
+ customFields,
5892
6046
  includeClosed: opts.includeClosed
5893
6047
  });
5894
6048
  await printTasks(tasks, opts.json ?? false, config);
@@ -5911,15 +6065,17 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
5911
6065
  wrapAction(
5912
6066
  async (taskId, opts) => {
5913
6067
  const config = loadConfig(getProfileName());
5914
- if (opts.assignee === "me") {
5915
- const client = new ClickUpClient(config);
5916
- opts.assignee = String(await resolveAssigneeId(client, "me"));
5917
- }
5918
- if (opts.removeAssignee === "me") {
5919
- const client = new ClickUpClient(config);
5920
- opts.removeAssignee = String(await resolveAssigneeId(client, "me"));
5921
- }
5922
- const payload = buildUpdatePayload(opts);
6068
+ const client = new ClickUpClient(config);
6069
+ const [timezone] = await Promise.all([
6070
+ client.getUserTimezone(),
6071
+ opts.assignee === "me" ? resolveAssigneeId(client, "me").then((id) => {
6072
+ opts.assignee = String(id);
6073
+ }) : Promise.resolve(),
6074
+ opts.removeAssignee === "me" ? resolveAssigneeId(client, "me").then((id) => {
6075
+ opts.removeAssignee = String(id);
6076
+ }) : Promise.resolve()
6077
+ ]);
6078
+ const payload = buildUpdatePayload(opts, timezone);
5923
6079
  const hasFields = (opts.field?.length ?? 0) > 0;
5924
6080
  if (!hasFields && Object.keys(payload).length === 0) {
5925
6081
  throw new Error(
@@ -5938,8 +6094,8 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
5938
6094
  await setCustomField(config, taskId, { set: [opts.field[i], opts.field[i + 1]] });
5939
6095
  }
5940
6096
  if (!result) {
5941
- const client = new ClickUpClient(config);
5942
- const task = await client.getTask(taskId);
6097
+ const client2 = new ClickUpClient(config);
6098
+ const task = await client2.getTask(taskId);
5943
6099
  result = { id: task.id, name: task.name };
5944
6100
  }
5945
6101
  }
@@ -6113,14 +6269,63 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
6113
6269
  await openTask(config, query, opts);
6114
6270
  })
6115
6271
  );
6116
- program.command("search <query>").description("Search my tasks by name (use --all for all tasks)").option("--status <status>", "Filter by status").option("--all", "Search all tasks, not just mine").option("--include-closed", "Include done/closed tasks in search").option("--json", "Force JSON output even in terminal").action(
6272
+ program.command("search <query>").description("Search my tasks by name (use --all for all tasks)").option("--status <status>", "Filter by status").option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--all", "Search all tasks, not just mine").option("--include-closed", "Include done/closed tasks in search").option("--assignee <userId>", 'Filter by assignee (user ID or "me")').option("--tag <tag>", "Filter by tag name").option("--due-before <date>", "Tasks due before date (YYYY-MM-DD)").option("--due-after <date>", "Tasks due after date (YYYY-MM-DD)").option("--created-after <date>", "Tasks created after date (YYYY-MM-DD)").option("--created-before <date>", "Tasks created before date (YYYY-MM-DD)").option("--field <nameAndValue...>", 'Filter by custom field: --field "Name" value').option("--json", "Force JSON output even in terminal").action(
6117
6273
  wrapAction(
6118
6274
  async (query, opts) => {
6119
6275
  const config = loadConfig(getProfileName());
6276
+ let assigneeIds;
6277
+ if (opts.assignee) {
6278
+ if (opts.assignee === "me") {
6279
+ const client = new ClickUpClient(config);
6280
+ const me = await client.getMe();
6281
+ assigneeIds = [me.id];
6282
+ } else {
6283
+ assigneeIds = [Number(opts.assignee)];
6284
+ }
6285
+ }
6286
+ const parseDateFilter = (d) => {
6287
+ const parts = d.split("-");
6288
+ return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])).getTime();
6289
+ };
6290
+ let customFields;
6291
+ if (opts.field?.length) {
6292
+ if (opts.field.length % 2 !== 0) {
6293
+ throw new Error('--field requires pairs: --field "Name" value');
6294
+ }
6295
+ if (!opts.list) {
6296
+ throw new Error("--field filtering requires --list to resolve field names");
6297
+ }
6298
+ const client = new ClickUpClient(config);
6299
+ const fields = await client.getListCustomFields(opts.list);
6300
+ customFields = [];
6301
+ for (let i = 0; i < opts.field.length; i += 2) {
6302
+ const fieldName = opts.field[i];
6303
+ const fieldValue = opts.field[i + 1];
6304
+ const match = fields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase());
6305
+ if (!match) {
6306
+ const available = fields.map((f) => f.name).join(", ");
6307
+ throw new Error(`Field "${fieldName}" not found. Available: ${available}`);
6308
+ }
6309
+ customFields.push({
6310
+ field_id: match.id,
6311
+ operator: "=",
6312
+ value: fieldValue
6313
+ });
6314
+ }
6315
+ }
6120
6316
  const tasks = await searchTasks(config, query, {
6121
6317
  status: opts.status,
6122
6318
  all: opts.all,
6123
- includeClosed: opts.includeClosed
6319
+ includeClosed: opts.includeClosed,
6320
+ listIds: opts.list ? [opts.list] : void 0,
6321
+ spaceIds: opts.space ? [opts.space] : void 0,
6322
+ assignees: assigneeIds,
6323
+ tags: opts.tag ? [opts.tag] : void 0,
6324
+ dueDateLt: opts.dueBefore ? parseDateFilter(opts.dueBefore) : void 0,
6325
+ dueDateGt: opts.dueAfter ? parseDateFilter(opts.dueAfter) : void 0,
6326
+ dateCreatedGt: opts.createdAfter ? parseDateFilter(opts.createdAfter) : void 0,
6327
+ dateCreatedLt: opts.createdBefore ? parseDateFilter(opts.createdBefore) : void 0,
6328
+ customFields
6124
6329
  });
6125
6330
  await printTasks(tasks, opts.json ?? false, config);
6126
6331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,39 +48,39 @@ All commands support `--help` for full flag details. All commands support `--jso
48
48
 
49
49
  ### Read
50
50
 
51
- | Command | What it returns |
52
- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
53
- | `cup tasks [--status s] [--name q] [--type t] [--list id] [--space id] [--all] [--include-closed]` | My tasks (filter by status, name, type, list, space). `--all` for all tasks in workspace |
54
- | `cup assigned [--status s] [--include-closed]` | All my tasks grouped by status |
55
- | `cup sprint [--status s] [--space nameOrId] [--folder id] [--include-closed]` | Tasks in active sprint (auto-detected) |
56
- | `cup sprints [--space nameOrId]` | List all sprints (marks active with \*) |
57
- | `cup search <query> [--status s] [--all] [--include-closed]` | Search my tasks by name. `--all` for all tasks |
58
- | `cup task <id>` | Single task details (custom fields, checklists, attachments, deps, links) |
59
- | `cup subtasks <id> [--status s] [--name q] [--include-closed]` | Subtasks of a task |
60
- | `cup comments <id>` | Comments on a task |
61
- | `cup activity <id>` | Task details + comment history combined |
62
- | `cup inbox [--days n] [--include-closed]` | Tasks updated in last n days (default 30) |
63
- | `cup summary [--hours n]` | Standup: completed, in-progress, overdue |
64
- | `cup overdue [--include-closed]` | Tasks past due date (most overdue first) |
65
- | `cup spaces [--name partial] [--my]` | List/filter workspace spaces |
66
- | `cup lists <spaceId> [--name partial]` | Lists in a space (including folder lists) |
67
- | `cup folders <spaceId> [--name partial]` | Folders in a space (with their lists) |
68
- | `cup members` | Workspace members (username, ID, email) |
69
- | `cup fields <listId>` | Custom fields on a list (type, required, options) |
70
- | `cup tags <spaceId>` | Tags available in a space |
71
- | `cup goals` | Workspace goals with progress |
72
- | `cup key-results <goalId>` | Key results for a goal |
73
- | `cup docs [query]` | Workspace docs (optionally filter by name) |
74
- | `cup doc <docId> [pageId]` | Doc metadata + page tree, or a specific page |
75
- | `cup doc-pages <docId>` | All pages in a doc with content |
76
- | `cup task-types` | Custom task types (for `--custom-item-id`) |
77
- | `cup templates` | Task templates (for `--template`) |
78
- | `cup list-templates` | List templates (for `list-from-template`) |
79
- | `cup folder-templates` | Folder templates |
80
- | `cup views <listId>` | List views on a list |
81
- | `cup view <viewId>` | Get view details |
82
- | `cup open <query>` | Open task in browser by ID or name |
83
- | `cup auth` | Check authentication status |
51
+ | Command | What it returns |
52
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
53
+ | `cup tasks [--status s] [--name q] [--type t] [--list id] [--space id] [--all] [--include-closed] [--assignee id\|me] [--tag t] [--due-before d] [--due-after d] [--created-after d] [--created-before d] [--field "Name" val]` | My tasks (filter by status, name, type, list, space, assignee, tag, dates, custom fields). `--all` for all tasks in workspace |
54
+ | `cup assigned [--status s] [--include-closed]` | All my tasks grouped by status |
55
+ | `cup sprint [--status s] [--space nameOrId] [--folder id] [--include-closed]` | Tasks in active sprint (auto-detected) |
56
+ | `cup sprints [--space nameOrId]` | List all sprints (marks active with \*) |
57
+ | `cup search <query> [--status s] [--list id] [--space id] [--all] [--include-closed] [--assignee id\|me] [--tag t] [--due-before d] [--due-after d] [--created-after d] [--created-before d] [--field "Name" val]` | Search my tasks by name. `--all` for all tasks |
58
+ | `cup task <id>` | Single task details (custom fields, checklists, attachments, deps, links) |
59
+ | `cup subtasks <id> [--status s] [--name q] [--include-closed]` | Subtasks of a task |
60
+ | `cup comments <id>` | Comments on a task |
61
+ | `cup activity <id>` | Task details + comment history combined |
62
+ | `cup inbox [--days n] [--include-closed]` | Tasks updated in last n days (default 30) |
63
+ | `cup summary [--hours n]` | Standup: completed, in-progress, overdue |
64
+ | `cup overdue [--include-closed]` | Tasks past due date (most overdue first) |
65
+ | `cup spaces [--name partial] [--my]` | List/filter workspace spaces |
66
+ | `cup lists <spaceId> [--name partial]` | Lists in a space (including folder lists) |
67
+ | `cup folders <spaceId> [--name partial]` | Folders in a space (with their lists) |
68
+ | `cup members` | Workspace members (username, ID, email) |
69
+ | `cup fields <listId>` | Custom fields on a list (type, required, options) |
70
+ | `cup tags <spaceId>` | Tags available in a space |
71
+ | `cup goals` | Workspace goals with progress |
72
+ | `cup key-results <goalId>` | Key results for a goal |
73
+ | `cup docs [query]` | Workspace docs (optionally filter by name) |
74
+ | `cup doc <docId> [pageId]` | Doc metadata + page tree, or a specific page |
75
+ | `cup doc-pages <docId>` | All pages in a doc with content |
76
+ | `cup task-types` | Custom task types (for `--custom-item-id`) |
77
+ | `cup templates` | Task templates (for `--template`) |
78
+ | `cup list-templates` | List templates (for `list-from-template`) |
79
+ | `cup folder-templates` | Folder templates |
80
+ | `cup views <listId>` | List views on a list |
81
+ | `cup view <viewId>` | Get view details |
82
+ | `cup open <query>` | Open task in browser by ID or name |
83
+ | `cup auth` | Check authentication status |
84
84
 
85
85
  ### Write
86
86
 
@@ -157,28 +157,31 @@ All commands support `--help` for full flag details. All commands support `--jso
157
157
 
158
158
  ## Flags & Conventions
159
159
 
160
- | Topic | Detail |
161
- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
- | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
163
- | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr |
164
- | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
165
- | `--due-date` | `YYYY-MM-DD` format |
166
- | `--assignee` | User ID or `me` |
167
- | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
168
- | `--time-estimate` | Duration: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
169
- | `--type` | `task` (regular) or custom type name/ID (e.g. `initiative`, `Bug`) |
170
- | `--custom-item-id` | Custom task type ID for `cup create` (find with `cup task-types`) |
171
- | `--space` | Partial name match or exact ID |
172
- | `--name` | Partial match, case-insensitive |
173
- | `--all` | Show all tasks in workspace, not just assigned to me. Available on `tasks`, `search`, `overdue`. Default: my tasks only (smaller output for agent context windows) |
174
- | `--include-closed` | Include closed/done tasks |
175
- | `--list` on create | Optional when `--parent` is given (auto-detected) |
176
- | `cup field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), date (YYYY-MM-DD), url, email. Names resolved case-insensitively; errors list available fields/options |
177
- | `cup field-create` | Use `--options "a,b,c"` for `drop_down` and `labels` types (required). Other types don't need `--options` |
178
- | `cup sprint` | Auto-detects active sprint by folder name (sprint/iteration/cycle/scrum), parses multiple date formats. Override with `--folder <id>` or `cup config set sprintFolderId <id>` |
179
- | `cup link` | Both IDs must be the same type (both custom or both native) |
180
- | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
181
- | Errors | stderr with exit code 1. Strict parsing - excess/unknown arguments rejected |
160
+ | Topic | Detail |
161
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
163
+ | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr |
164
+ | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
165
+ | `--due-date` | `YYYY-MM-DD` format |
166
+ | `--assignee` | User ID or `me` |
167
+ | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
168
+ | `--time-estimate` | Duration: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
169
+ | `--type` | `task` (regular) or custom type name/ID (e.g. `initiative`, `Bug`) |
170
+ | `--custom-item-id` | Custom task type ID for `cup create` (find with `cup task-types`) |
171
+ | `--space` | Partial name match or exact ID |
172
+ | `--name` | Partial match, case-insensitive |
173
+ | `--all` | Show all tasks in workspace, not just assigned to me. Available on `tasks`, `search`, `overdue`. Default: my tasks only (smaller output for agent context windows) |
174
+ | `--include-closed` | Include closed/done tasks |
175
+ | `--list` on create | Optional when `--parent` is given (auto-detected) |
176
+ | `cup field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), date (YYYY-MM-DD), url, email. Names resolved case-insensitively; errors list available fields/options |
177
+ | `cup field-create` | Use `--options "a,b,c"` for `drop_down` and `labels` types (required). Other types don't need `--options` |
178
+ | `--field` filter | `--field "Name" value` on `tasks` and `search` requires `--list` to resolve field names to IDs |
179
+ | `--due-before/after` | `YYYY-MM-DD` date filters for due date range |
180
+ | `--created-before/after` | `YYYY-MM-DD` date filters for creation date range |
181
+ | `cup sprint` | Auto-detects active sprint by folder name (sprint/iteration/cycle/scrum), parses multiple date formats. Override with `--folder <id>` or `cup config set sprintFolderId <id>` |
182
+ | `cup link` | Both IDs must be the same type (both custom or both native) |
183
+ | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
184
+ | Errors | stderr with exit code 1. Strict parsing - excess/unknown arguments rejected |
182
185
 
183
186
  ## Agent Workflow Examples
184
187
 
@@ -199,8 +202,13 @@ cup tasks --status "in progress" # by status
199
202
  cup tasks --name "login" # by partial name
200
203
  cup tasks --type initiative # initiatives only
201
204
  cup tasks --list 12345 --all # all tasks in list, not just mine
205
+ cup tasks --tag "bug" --status "to do" # by tag and status
206
+ cup tasks --list 123 --field "Sprint" "Week 1" # by custom field
207
+ cup tasks --due-before 2026-04-01 # due before a date
208
+ cup tasks --assignee me --created-after 2026-03-01 # my tasks created recently
202
209
  cup search "payment flow" # multi-word search
203
210
  cup search auth --status "prog" # fuzzy status match
211
+ cup search "auth" --list 123 --space 456 # search within list and space
204
212
  cup sprint # current sprint
205
213
  cup assigned # all my tasks by status
206
214
  cup overdue # past due date