@krodak/clickup-cli 1.21.1 → 1.22.1

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.21.1",
4
+ "version": "1.22.1",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/README.md CHANGED
@@ -16,13 +16,24 @@ npm install -g @krodak/clickup-cli && cup init
16
16
 
17
17
  ## For AI Agents
18
18
 
19
- Paste this into any AI agent to get started immediately:
19
+ Paste this into any AI agent (Claude Code, Codex, Cursor, OpenCode, etc.):
20
20
 
21
21
  ```
22
- Fetch and follow instructions from https://raw.githubusercontent.com/krodak/clickup-cli/main/skills/clickup-cli/SKILL.md
22
+ Install and configure the ClickUp CLI for me. Fetch the setup guide from:
23
+ https://raw.githubusercontent.com/krodak/clickup-cli/main/skills/clickup-cli/SKILL.md
24
+
25
+ Then walk me through installing the CLI, getting a ClickUp API token,
26
+ and running cup init. Finally, install the skill with `cup skill` so
27
+ you have persistent access to the full command reference.
23
28
  ```
24
29
 
25
- Or install the skill permanently with `cup skill` (see [Set up your agent](#set-up-your-agent) below).
30
+ The fetched SKILL.md contains everything the agent needs: install commands,
31
+ where to get a ClickUp API token, non-interactive setup with `cup init --token --team`,
32
+ and the complete command reference. After setup, the agent can run any `cup` command
33
+ to manage your tasks, sprints, comments, time tracking, and more.
34
+
35
+ **Already have the CLI installed?** Just run `cup skill` to install the agent skill to
36
+ all detected locations (see [Set up your agent](#set-up-your-agent) below).
26
37
 
27
38
  ## Talk to your agent
28
39
 
package/dist/index.js CHANGED
@@ -286,8 +286,10 @@ var ClickUpClient = class {
286
286
  body: JSON.stringify({ name, multiple_assignees: true })
287
287
  });
288
288
  }
289
- async getSpaces(teamId) {
290
- const data = await this.request(`/team/${teamId}/space?archived=false`);
289
+ async getSpaces(teamId, archived = false) {
290
+ const data = await this.request(
291
+ `/team/${teamId}/space?archived=${archived ? "true" : "false"}`
292
+ );
291
293
  return readCollectionField(data, "spaces", "spaces");
292
294
  }
293
295
  async getCustomTaskTypes(teamId) {
@@ -324,18 +326,22 @@ var ClickUpClient = class {
324
326
  body: JSON.stringify({ name })
325
327
  });
326
328
  }
327
- async getLists(spaceId) {
328
- const data = await this.request(`/space/${spaceId}/list?archived=false`);
329
+ async getLists(spaceId, archived = false) {
330
+ const data = await this.request(
331
+ `/space/${spaceId}/list?archived=${archived ? "true" : "false"}`
332
+ );
329
333
  return readCollectionField(data, "lists", "space lists");
330
334
  }
331
- async getFolders(spaceId) {
335
+ async getFolders(spaceId, archived = false) {
332
336
  const data = await this.request(
333
- `/space/${spaceId}/folder?archived=false`
337
+ `/space/${spaceId}/folder?archived=${archived ? "true" : "false"}`
334
338
  );
335
339
  return readCollectionField(data, "folders", "space folders");
336
340
  }
337
- async getFolderLists(folderId) {
338
- const data = await this.request(`/folder/${folderId}/list?archived=false`);
341
+ async getFolderLists(folderId, archived = false) {
342
+ const data = await this.request(
343
+ `/folder/${folderId}/list?archived=${archived ? "true" : "false"}`
344
+ );
339
345
  return readCollectionField(data, "lists", "folder lists");
340
346
  }
341
347
  async getListViews(listId) {
@@ -1015,7 +1021,9 @@ function loadConfig(profileName) {
1015
1021
  if (envToken || envTeamId) {
1016
1022
  throw new Error("Both CU_API_TOKEN and CU_TEAM_ID must be set, or run: cup init");
1017
1023
  }
1018
- throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
1024
+ throw new Error(
1025
+ "No ClickUp CLI configuration found.\n\nTo get started:\n cup init\n\nFor scripts or AI agents:\n cup init --token pk_YOUR_TOKEN --team YOUR_TEAM_ID\n\nGet your API token: https://app.clickup.com/settings/apps"
1026
+ );
1019
1027
  }
1020
1028
  const { parsed } = parseRawConfig(path);
1021
1029
  if (isOldFormat(parsed)) {
@@ -1945,10 +1953,16 @@ function buildUpdatePayload(opts, timezone) {
1945
1953
  }
1946
1954
  if (opts.archive) payload.archived = true;
1947
1955
  if (opts.unarchive) payload.archived = false;
1956
+ if (opts.type !== void 0) {
1957
+ const num = Number(opts.type);
1958
+ if (Number.isInteger(num) && num >= 0) {
1959
+ payload.custom_item_id = num;
1960
+ }
1961
+ }
1948
1962
  return payload;
1949
1963
  }
1950
1964
  function hasUpdateFields(options) {
1951
- return options.name !== void 0 || options.description !== void 0 || options.markdown_content !== void 0 || options.status !== void 0 || options.priority !== void 0 || options.due_date !== void 0 || options.start_date !== void 0 || options.time_estimate !== void 0 || options.assignees !== void 0 || options.parent !== void 0 || options.archived !== void 0;
1965
+ return options.name !== void 0 || options.description !== void 0 || options.markdown_content !== void 0 || options.status !== void 0 || options.priority !== void 0 || options.due_date !== void 0 || options.start_date !== void 0 || options.time_estimate !== void 0 || options.assignees !== void 0 || options.parent !== void 0 || options.archived !== void 0 || options.custom_item_id !== void 0;
1952
1966
  }
1953
1967
  async function resolveStatus(client, taskId, statusInput) {
1954
1968
  const task = await client.getTask(taskId);
@@ -1964,16 +1978,29 @@ async function resolveStatus(client, taskId, statusInput) {
1964
1978
  }
1965
1979
  return matched;
1966
1980
  }
1967
- async function updateTask(config, taskId, options) {
1968
- if (!hasUpdateFields(options))
1981
+ async function resolveTaskType2(client, teamId, typeInput) {
1982
+ const types = await client.getCustomTaskTypes(teamId);
1983
+ const lower = typeInput.toLowerCase();
1984
+ const match = types.find((t) => t.name.toLowerCase() === lower);
1985
+ if (!match) {
1986
+ const available = types.map((t) => `${t.name} (${String(t.id)})`).join(", ");
1987
+ throw new Error(`No matching task type for "${typeInput}". Available: ${available}`);
1988
+ }
1989
+ return match.id;
1990
+ }
1991
+ async function updateTask(config, taskId, options, typeInput) {
1992
+ if (!hasUpdateFields(options) && typeInput === void 0)
1969
1993
  throw new Error(
1970
- "Provide at least one of: --name, --description, --status, --priority, --due-date, --start-date, --time-estimate, --assignee, --remove-assignee, --parent, --detach, --archive, --unarchive"
1994
+ "Provide at least one of: --name, --description, --status, --priority, --due-date, --start-date, --time-estimate, --assignee, --remove-assignee, --parent, --detach, --archive, --unarchive, --type"
1971
1995
  );
1972
1996
  const client = new ClickUpClient(config);
1973
1997
  const resolved = { ...options };
1974
1998
  if (resolved.status !== void 0) {
1975
1999
  resolved.status = await resolveStatus(client, taskId, resolved.status);
1976
2000
  }
2001
+ if (resolved.custom_item_id === void 0 && typeInput !== void 0) {
2002
+ resolved.custom_item_id = await resolveTaskType2(client, config.teamId, typeInput);
2003
+ }
1977
2004
  const task = await client.updateTask(taskId, resolved);
1978
2005
  return { id: task.id, name: task.name };
1979
2006
  }
@@ -2040,31 +2067,75 @@ async function getTask(config, taskId) {
2040
2067
  // src/commands/init.ts
2041
2068
  import { password, select, confirm as confirm2 } from "@inquirer/prompts";
2042
2069
  import fs2 from "fs";
2070
+ var TOKEN_URL = "https://app.clickup.com/settings/apps";
2071
+ var TOKEN_HELP = `
2072
+ How to get a ClickUp API token:
2073
+ 1. Open ${TOKEN_URL}
2074
+ 2. Find the "API Token" section, click "Generate" (or copy the existing one)
2075
+ 3. The token starts with "pk_"
2076
+ `;
2077
+ var NON_TTY_HELP = `cup init requires an interactive terminal.
2078
+
2079
+ For scripts, CI, or AI agents, use flags:
2080
+ cup init --token pk_YOUR_TOKEN --team YOUR_TEAM_ID
2081
+
2082
+ Or set environment variables:
2083
+ export CU_API_TOKEN=pk_YOUR_TOKEN
2084
+ export CU_TEAM_ID=YOUR_TEAM_ID
2085
+ ${TOKEN_HELP}`;
2086
+ var NEXT_STEPS = `
2087
+ Next steps:
2088
+ cup auth # verify setup
2089
+ cup tasks # list your assigned tasks
2090
+ cup sprint # show current sprint
2091
+ cup --help # see all commands
2092
+ `;
2093
+ function validateTokenFormat(token) {
2094
+ if (!token.startsWith("pk_")) {
2095
+ throw new Error(
2096
+ `Invalid token format. Personal API tokens start with "pk_".
2097
+ ${TOKEN_HELP}`
2098
+ );
2099
+ }
2100
+ }
2101
+ async function verifyToken(apiToken) {
2102
+ const client = new ClickUpClient({ apiToken });
2103
+ try {
2104
+ const me = await client.getMe();
2105
+ return me.username;
2106
+ } catch (err) {
2107
+ const msg = err instanceof Error ? err.message : String(err);
2108
+ throw new Error(
2109
+ `Token verification failed: ${msg}
2110
+
2111
+ Check the token is correct and not expired.
2112
+ ${TOKEN_HELP}`,
2113
+ { cause: err }
2114
+ );
2115
+ }
2116
+ }
2043
2117
  async function runInitCommand(opts) {
2044
2118
  if (opts?.token && opts?.team) {
2045
2119
  const apiToken2 = opts.token.trim();
2046
- if (!apiToken2.startsWith("pk_")) throw new Error("Token must start with pk_");
2047
- const client2 = new ClickUpClient({ apiToken: apiToken2 });
2048
- let username2;
2049
- try {
2050
- const me = await client2.getMe();
2051
- username2 = me.username;
2052
- } catch (err) {
2053
- throw new Error(`Invalid token: ${err instanceof Error ? err.message : String(err)}`, {
2054
- cause: err
2055
- });
2056
- }
2120
+ validateTokenFormat(apiToken2);
2121
+ const username2 = await verifyToken(apiToken2);
2057
2122
  process.stdout.write(`Authenticated as @${username2}
2058
2123
  `);
2059
2124
  writeConfig({ apiToken: apiToken2, teamId: opts.team });
2060
2125
  process.stdout.write(`Config written to ${getConfigPath()}
2061
- `);
2126
+ ${NEXT_STEPS}`);
2062
2127
  return;
2063
2128
  }
2064
2129
  if (opts?.token || opts?.team) {
2065
2130
  throw new Error("Both --token and --team are required for non-interactive setup");
2066
2131
  }
2132
+ if (!process.stdin.isTTY) {
2133
+ throw new Error(NON_TTY_HELP);
2134
+ }
2067
2135
  const configPath3 = getConfigPath();
2136
+ process.stdout.write("\nWelcome to ClickUp CLI!\n");
2137
+ process.stdout.write(TOKEN_HELP);
2138
+ process.stdout.write("\n");
2068
2139
  if (fs2.existsSync(configPath3)) {
2069
2140
  const overwrite = await confirm2({
2070
2141
  message: `Config already exists at ${configPath3}. Overwrite?`,
@@ -2075,20 +2146,14 @@ async function runInitCommand(opts) {
2075
2146
  return;
2076
2147
  }
2077
2148
  }
2078
- const apiToken = (await password({ message: "ClickUp API token (pk_...):" })).trim();
2079
- if (!apiToken.startsWith("pk_")) throw new Error("Token must start with pk_");
2080
- const client = new ClickUpClient({ apiToken });
2081
- let username;
2082
- try {
2083
- const me = await client.getMe();
2084
- username = me.username;
2085
- } catch (err) {
2086
- throw new Error(`Invalid token: ${err instanceof Error ? err.message : String(err)}`, {
2087
- cause: err
2088
- });
2089
- }
2149
+ const apiToken = (await password({
2150
+ message: "Paste your ClickUp API token (starts with pk_):"
2151
+ })).trim();
2152
+ validateTokenFormat(apiToken);
2153
+ const username = await verifyToken(apiToken);
2090
2154
  process.stdout.write(`Authenticated as @${username}
2091
2155
  `);
2156
+ const client = new ClickUpClient({ apiToken });
2092
2157
  const teams = await client.getTeams();
2093
2158
  if (teams.length === 0) throw new Error("No workspaces found for this token.");
2094
2159
  let teamId;
@@ -2105,8 +2170,9 @@ async function runInitCommand(opts) {
2105
2170
  });
2106
2171
  }
2107
2172
  writeConfig({ apiToken, teamId });
2108
- process.stdout.write(`Config written to ${configPath3}
2109
- `);
2173
+ process.stdout.write(`
2174
+ Config written to ${configPath3}
2175
+ ${NEXT_STEPS}`);
2110
2176
  }
2111
2177
 
2112
2178
  // src/commands/sprint.ts
@@ -2502,12 +2568,14 @@ function printComments(comments, forceJson) {
2502
2568
  async function fetchLists(config, spaceId, opts = {}) {
2503
2569
  const client = new ClickUpClient(config);
2504
2570
  const results = [];
2505
- const folderlessLists = await client.getLists(spaceId);
2571
+ const folderlessLists = await client.getLists(spaceId, opts.archived);
2506
2572
  for (const list of folderlessLists) {
2507
2573
  results.push({ id: list.id, name: list.name, folder: "(none)" });
2508
2574
  }
2509
- const folders = await client.getFolders(spaceId);
2510
- const folderListArrays = await Promise.all(folders.map((f) => client.getFolderLists(f.id)));
2575
+ const folders = await client.getFolders(spaceId, opts.archived);
2576
+ const folderListArrays = await Promise.all(
2577
+ folders.map((f) => client.getFolderLists(f.id, opts.archived))
2578
+ );
2511
2579
  for (let i = 0; i < folders.length; i++) {
2512
2580
  const folder = folders[i];
2513
2581
  for (const list of folderListArrays[i]) {
@@ -2637,7 +2705,7 @@ async function printInbox(tasks, forceJson, config) {
2637
2705
  // src/commands/spaces.ts
2638
2706
  async function listSpaces(config, opts) {
2639
2707
  const client = new ClickUpClient(config);
2640
- let spaces = await client.getSpaces(config.teamId);
2708
+ let spaces = await client.getSpaces(config.teamId, opts.archived);
2641
2709
  if (opts.name) {
2642
2710
  const lower = opts.name.toLowerCase();
2643
2711
  spaces = spaces.filter((s) => s.name.toLowerCase().includes(lower));
@@ -2949,6 +3017,9 @@ function configPath2() {
2949
3017
  }
2950
3018
 
2951
3019
  // src/commands/assign.ts
3020
+ function parseIdList(value) {
3021
+ return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
3022
+ }
2952
3023
  async function assignTask(config, taskId, opts) {
2953
3024
  if (!opts.to && !opts.remove) {
2954
3025
  throw new Error("Provide at least one of: --to, --remove");
@@ -2957,10 +3028,14 @@ async function assignTask(config, taskId, opts) {
2957
3028
  const add = [];
2958
3029
  const rem = [];
2959
3030
  if (opts.to) {
2960
- add.push(await resolveAssigneeId(client, opts.to));
3031
+ for (const value of parseIdList(opts.to)) {
3032
+ add.push(await resolveAssigneeId(client, value));
3033
+ }
2961
3034
  }
2962
3035
  if (opts.remove) {
2963
- rem.push(await resolveAssigneeId(client, opts.remove));
3036
+ for (const value of parseIdList(opts.remove)) {
3037
+ rem.push(await resolveAssigneeId(client, value));
3038
+ }
2964
3039
  }
2965
3040
  return client.updateTask(taskId, {
2966
3041
  assignees: {
@@ -3102,9 +3177,15 @@ function printTimeInStatus(result, forceJson) {
3102
3177
  var commandMetadata = [
3103
3178
  {
3104
3179
  name: "init",
3105
- description: "Set up cup for the first time",
3180
+ description: "Set up cup (interactive). Use --token and --team for non-interactive/agent setup",
3106
3181
  flags: ["--token", "--team"],
3107
- quickReference: [{ section: "setup", usage: "init", description: "First-time setup wizard" }]
3182
+ quickReference: [
3183
+ {
3184
+ section: "setup",
3185
+ usage: "init",
3186
+ description: "First-time setup (interactive, or use --token --team for agents)"
3187
+ }
3188
+ ]
3108
3189
  },
3109
3190
  {
3110
3191
  name: "auth",
@@ -3164,6 +3245,7 @@ var commandMetadata = [
3164
3245
  "--detach",
3165
3246
  "--archive",
3166
3247
  "--unarchive",
3248
+ "--type",
3167
3249
  "--field",
3168
3250
  "--json"
3169
3251
  ],
@@ -3294,7 +3376,7 @@ var commandMetadata = [
3294
3376
  {
3295
3377
  name: "lists",
3296
3378
  description: "List all lists in a space (including lists inside folders)",
3297
- flags: ["--name", "--json"],
3379
+ flags: ["--name", "--archived", "--json"],
3298
3380
  quickReference: [
3299
3381
  { section: "read", usage: "lists <spaceId>", description: "List all lists in a space" }
3300
3382
  ]
@@ -3302,7 +3384,7 @@ var commandMetadata = [
3302
3384
  {
3303
3385
  name: "spaces",
3304
3386
  description: "List spaces in your workspace",
3305
- flags: ["--name", "--my", "--json"],
3387
+ flags: ["--name", "--my", "--archived", "--json"],
3306
3388
  quickReference: [{ section: "read", usage: "spaces", description: "List spaces in workspace" }]
3307
3389
  },
3308
3390
  {
@@ -3627,7 +3709,7 @@ var commandMetadata = [
3627
3709
  {
3628
3710
  name: "folders",
3629
3711
  description: "List folders in a space (with their lists)",
3630
- flags: ["--name", "--json"],
3712
+ flags: ["--name", "--archived", "--json"],
3631
3713
  quickReference: [
3632
3714
  { section: "read", usage: "folders <spaceId>", description: "List folders in a space" }
3633
3715
  ]
@@ -3733,6 +3815,16 @@ var commandMetadata = [
3733
3815
  section: "write",
3734
3816
  usage: "bulk tag <tagName> <taskIds...>",
3735
3817
  description: "Bulk add/remove tag"
3818
+ },
3819
+ {
3820
+ section: "write",
3821
+ usage: "bulk priority <taskIds...>",
3822
+ description: "Bulk set priority on tasks"
3823
+ },
3824
+ {
3825
+ section: "write",
3826
+ usage: "bulk field <taskIds...>",
3827
+ description: "Bulk set a custom field value on tasks"
3736
3828
  }
3737
3829
  ]
3738
3830
  },
@@ -3981,7 +4073,7 @@ function parseCommandFlags(flags = []) {
3981
4073
  }
3982
4074
  function commandDescription(command, programName = "cup") {
3983
4075
  if (command.name === "init") {
3984
- return `Set up ${programName} for the first time`;
4076
+ return `Set up ${programName} (interactive). Use --token and --team for non-interactive/agent setup`;
3985
4077
  }
3986
4078
  return command.description;
3987
4079
  }
@@ -5219,7 +5311,11 @@ var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
5219
5311
  "checkbox",
5220
5312
  "date",
5221
5313
  "url",
5222
- "email"
5314
+ "email",
5315
+ "emoji",
5316
+ "manual_progress",
5317
+ "tasks",
5318
+ "users"
5223
5319
  ]);
5224
5320
  function findFieldByName(fields, name) {
5225
5321
  const lower = name.toLowerCase();
@@ -5284,6 +5380,30 @@ function parseFieldValue(field, rawValue) {
5284
5380
  throw new Error(`Value "${rawValue}" is not a valid date (use YYYY-MM-DD)`);
5285
5381
  return ms;
5286
5382
  }
5383
+ case "emoji": {
5384
+ const n = Number(rawValue);
5385
+ if (!Number.isFinite(n) || n < 0 || n > 5) {
5386
+ throw new Error("Rating value must be a number between 0 and 5");
5387
+ }
5388
+ return n;
5389
+ }
5390
+ case "manual_progress": {
5391
+ const n = Number(rawValue);
5392
+ if (!Number.isFinite(n) || n < 0 || n > 100) {
5393
+ throw new Error("Progress value must be a number between 0 and 100");
5394
+ }
5395
+ return { current: n };
5396
+ }
5397
+ case "tasks": {
5398
+ const ids = rawValue.split(",").map((s) => s.trim()).filter(Boolean);
5399
+ if (ids.length === 0) throw new Error("Provide at least one task ID (comma-separated)");
5400
+ return { add: ids };
5401
+ }
5402
+ case "users": {
5403
+ const ids = rawValue.split(",").map((s) => s.trim()).filter(Boolean);
5404
+ if (ids.length === 0) throw new Error("Provide at least one user ID (comma-separated)");
5405
+ return { add: ids.map(Number) };
5406
+ }
5287
5407
  default:
5288
5408
  return rawValue;
5289
5409
  }
@@ -5668,9 +5788,9 @@ async function deleteDocPage(config, docId, pageId) {
5668
5788
 
5669
5789
  // src/commands/folders.ts
5670
5790
  import chalk12 from "chalk";
5671
- async function listFolders(config, spaceId, nameFilter) {
5791
+ async function listFolders(config, spaceId, nameFilter, archived) {
5672
5792
  const client = new ClickUpClient(config);
5673
- const folders = await client.getFolders(spaceId);
5793
+ const folders = await client.getFolders(spaceId, archived);
5674
5794
  let filtered = folders;
5675
5795
  if (nameFilter) {
5676
5796
  const lower = nameFilter.toLowerCase();
@@ -5932,66 +6052,98 @@ async function duplicateTask(config, taskId) {
5932
6052
  }
5933
6053
 
5934
6054
  // src/commands/bulk.ts
5935
- async function bulkUpdateStatus(config, taskIds, status) {
5936
- const client = new ClickUpClient(config);
6055
+ var BULK_CONCURRENCY = 5;
6056
+ async function runInBatches(items, concurrency, fn) {
6057
+ const results = [];
6058
+ for (let i = 0; i < items.length; i += concurrency) {
6059
+ const batch = items.slice(i, i + concurrency);
6060
+ const batchResults = await Promise.allSettled(batch.map(fn));
6061
+ batchResults.forEach((res, idx) => {
6062
+ const item = batch[idx];
6063
+ if (res.status === "fulfilled") {
6064
+ results.push({ item, result: res.value });
6065
+ } else {
6066
+ results.push({
6067
+ item,
6068
+ error: res.reason instanceof Error ? res.reason : new Error(String(res.reason))
6069
+ });
6070
+ }
6071
+ });
6072
+ }
6073
+ return results;
6074
+ }
6075
+ function toBulkResult(outcomes) {
5937
6076
  const failed = [];
5938
- for (const id of taskIds) {
5939
- try {
5940
- await client.updateTask(id, { status });
5941
- } catch (err) {
5942
- const reason = err instanceof Error ? err.message : String(err);
5943
- failed.push({ id, reason });
6077
+ for (const outcome of outcomes) {
6078
+ if (outcome.error) {
6079
+ failed.push({ id: outcome.item, reason: outcome.error.message });
5944
6080
  }
5945
6081
  }
5946
- return { updated: taskIds.length - failed.length, failed };
6082
+ return { updated: outcomes.length - failed.length, failed };
6083
+ }
6084
+ async function bulkUpdateStatus(config, taskIds, status) {
6085
+ const client = new ClickUpClient(config);
6086
+ const outcomes = await runInBatches(
6087
+ taskIds,
6088
+ BULK_CONCURRENCY,
6089
+ (id) => client.updateTask(id, { status })
6090
+ );
6091
+ return toBulkResult(outcomes);
5947
6092
  }
5948
6093
  async function bulkAssign(config, userIdOrMe, taskIds, action) {
5949
6094
  const client = new ClickUpClient(config);
5950
6095
  const numericId = await resolveAssigneeId(client, userIdOrMe);
5951
- const failed = [];
5952
- for (const id of taskIds) {
5953
- try {
5954
- await client.updateTask(id, {
5955
- assignees: action === "add" ? { add: [numericId] } : { rem: [numericId] }
5956
- });
5957
- } catch (err) {
5958
- const reason = err instanceof Error ? err.message : String(err);
5959
- failed.push({ id, reason });
5960
- }
5961
- }
5962
- return { updated: taskIds.length - failed.length, failed };
6096
+ const payload = action === "add" ? { assignees: { add: [numericId] } } : { assignees: { rem: [numericId] } };
6097
+ const outcomes = await runInBatches(
6098
+ taskIds,
6099
+ BULK_CONCURRENCY,
6100
+ (id) => client.updateTask(id, payload)
6101
+ );
6102
+ return toBulkResult(outcomes);
5963
6103
  }
5964
6104
  async function bulkDueDate(config, date, taskIds) {
5965
6105
  const client = new ClickUpClient(config);
5966
6106
  const timezone = await client.getUserTimezone();
5967
6107
  const payload = date === "none" || date === "clear" ? { due_date: null } : { due_date: parseDueDate(date, timezone), due_date_time: false };
5968
- const failed = [];
5969
- for (const id of taskIds) {
5970
- try {
5971
- await client.updateTask(id, payload);
5972
- } catch (err) {
5973
- const reason = err instanceof Error ? err.message : String(err);
5974
- failed.push({ id, reason });
5975
- }
5976
- }
5977
- return { updated: taskIds.length - failed.length, failed };
6108
+ const outcomes = await runInBatches(
6109
+ taskIds,
6110
+ BULK_CONCURRENCY,
6111
+ (id) => client.updateTask(id, payload)
6112
+ );
6113
+ return toBulkResult(outcomes);
5978
6114
  }
5979
6115
  async function bulkTag(config, tagName, taskIds, action) {
5980
6116
  const client = new ClickUpClient(config);
5981
- const failed = [];
5982
- for (const id of taskIds) {
5983
- try {
5984
- if (action === "add") {
5985
- await client.addTagToTask(id, tagName);
5986
- } else {
5987
- await client.removeTagFromTask(id, tagName);
5988
- }
5989
- } catch (err) {
5990
- const reason = err instanceof Error ? err.message : String(err);
5991
- failed.push({ id, reason });
5992
- }
5993
- }
5994
- return { updated: taskIds.length - failed.length, failed };
6117
+ const outcomes = await runInBatches(
6118
+ taskIds,
6119
+ BULK_CONCURRENCY,
6120
+ (id) => action === "add" ? client.addTagToTask(id, tagName) : client.removeTagFromTask(id, tagName)
6121
+ );
6122
+ return toBulkResult(outcomes);
6123
+ }
6124
+ async function bulkPriority(config, priorityValue, taskIds) {
6125
+ const priority = parsePriority(priorityValue);
6126
+ const client = new ClickUpClient(config);
6127
+ const outcomes = await runInBatches(
6128
+ taskIds,
6129
+ BULK_CONCURRENCY,
6130
+ (id) => client.updateTask(id, { priority })
6131
+ );
6132
+ return toBulkResult(outcomes);
6133
+ }
6134
+ async function bulkField(config, fieldName, rawValue, taskIds) {
6135
+ if (taskIds.length === 0) return { updated: 0, failed: [] };
6136
+ const client = new ClickUpClient(config);
6137
+ const firstTask = await client.getTask(taskIds[0]);
6138
+ const fields = firstTask.custom_fields ?? [];
6139
+ const field = findFieldByName(fields, fieldName);
6140
+ const parsed = parseFieldValue(field, rawValue);
6141
+ const outcomes = await runInBatches(
6142
+ taskIds,
6143
+ BULK_CONCURRENCY,
6144
+ (id) => client.setCustomFieldValue(id, field.id, parsed)
6145
+ );
6146
+ return toBulkResult(outcomes);
5995
6147
  }
5996
6148
 
5997
6149
  // src/commands/goals.ts
@@ -6494,7 +6646,9 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
6494
6646
  function getProfileName() {
6495
6647
  return program.opts().profile;
6496
6648
  }
6497
- program.command("init").description(`Set up ${programName} for the first time`).option("--token <token>", "API token (pk_...) for non-interactive setup").option("--team <teamId>", "Workspace/team ID for non-interactive setup").action(
6649
+ program.command("init").description(
6650
+ `Set up ${programName} (interactive). Use --token and --team for non-interactive/agent setup`
6651
+ ).option("--token <token>", "API token (pk_...) for non-interactive setup").option("--team <teamId>", "Workspace/team ID for non-interactive setup").action(
6498
6652
  wrapAction(async (opts) => {
6499
6653
  await runInitCommand(opts);
6500
6654
  })
@@ -6596,7 +6750,7 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
6596
6750
  ).option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", 'Due date (YYYY-MM-DD, or "none"/"clear" to remove)').option("--start-date <date>", "Start date (YYYY-MM-DD)").option(
6597
6751
  "--time-estimate <duration>",
6598
6752
  'Time estimate (e.g. "2h", "30m", "1h30m", "0" or "none" to clear)'
6599
- ).option("--assignee <userId>", 'Add assignee by user ID or "me"').option("--remove-assignee <userId>", 'Remove assignee by user ID or "me"').option("--parent <taskId>", "Set parent task (makes this a subtask)").option("--detach", "Remove parent task (promote subtask to top-level)").option("--archive", "Archive the task").option("--unarchive", "Unarchive the task").option("--field <nameAndValue...>", 'Set custom field: --field "Name" value').option("--json", "Force JSON output even in terminal").action(
6753
+ ).option("--assignee <userId>", 'Add assignee by user ID or "me"').option("--remove-assignee <userId>", 'Remove assignee by user ID or "me"').option("--parent <taskId>", "Set parent task (makes this a subtask)").option("--detach", "Remove parent task (promote subtask to top-level)").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(
6600
6754
  wrapAction(
6601
6755
  async (taskId, opts) => {
6602
6756
  const config = loadConfig(getProfileName());
@@ -6612,14 +6766,15 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
6612
6766
  ]);
6613
6767
  const payload = buildUpdatePayload(opts, timezone);
6614
6768
  const hasFields = (opts.field?.length ?? 0) > 0;
6615
- if (!hasFields && Object.keys(payload).length === 0) {
6769
+ const hasTypeName = opts.type !== void 0;
6770
+ if (!hasFields && !hasTypeName && Object.keys(payload).length === 0) {
6616
6771
  throw new Error(
6617
- "Provide at least one of: --name, --description, --status, --priority, --due-date, --time-estimate, --assignee, --remove-assignee, --parent, --archive, --unarchive, --field"
6772
+ "Provide at least one of: --name, --description, --status, --priority, --due-date, --time-estimate, --assignee, --remove-assignee, --parent, --archive, --unarchive, --type, --field"
6618
6773
  );
6619
6774
  }
6620
6775
  let result;
6621
- if (Object.keys(payload).length > 0) {
6622
- result = await updateTask(config, taskId, payload);
6776
+ if (Object.keys(payload).length > 0 || hasTypeName) {
6777
+ result = await updateTask(config, taskId, payload, opts.type);
6623
6778
  }
6624
6779
  if (hasFields) {
6625
6780
  if ((opts.field?.length ?? 0) % 2 !== 0) {
@@ -6804,18 +6959,25 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
6804
6959
  printTimeInStatus(result, opts.json ?? false);
6805
6960
  })
6806
6961
  );
6807
- program.command("lists <spaceId>").description("List all lists in a space (including lists inside folders)").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
6808
- wrapAction(async (spaceId, opts) => {
6809
- const config = loadConfig(getProfileName());
6810
- const lists = await fetchLists(config, spaceId, { name: opts.name });
6811
- printLists(lists, opts.json ?? false);
6812
- })
6962
+ program.command("lists <spaceId>").description("List all lists in a space (including lists inside folders)").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--archived", "Include only archived items (default: active items)").option("--json", "Force JSON output even in terminal").action(
6963
+ wrapAction(
6964
+ async (spaceId, opts) => {
6965
+ const config = loadConfig(getProfileName());
6966
+ const lists = await fetchLists(config, spaceId, {
6967
+ name: opts.name,
6968
+ archived: opts.archived
6969
+ });
6970
+ printLists(lists, opts.json ?? false);
6971
+ }
6972
+ )
6813
6973
  );
6814
- program.command("spaces").description("List spaces in your workspace").option("--name <partial>", "Filter spaces by name (case-insensitive contains)").option("--my", "Show only spaces where I have assigned tasks").option("--json", "Force JSON output even in terminal").action(
6815
- wrapAction(async (opts) => {
6816
- const config = loadConfig(getProfileName());
6817
- await listSpaces(config, opts);
6818
- })
6974
+ program.command("spaces").description("List spaces in your workspace").option("--name <partial>", "Filter spaces by name (case-insensitive contains)").option("--my", "Show only spaces where I have assigned tasks").option("--archived", "Include only archived items (default: active items)").option("--json", "Force JSON output even in terminal").action(
6975
+ wrapAction(
6976
+ async (opts) => {
6977
+ const config = loadConfig(getProfileName());
6978
+ await listSpaces(config, opts);
6979
+ }
6980
+ )
6819
6981
  );
6820
6982
  program.command("inbox").description("Recently updated tasks grouped by time period").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").option("--days <n>", "Lookback period in days", "30").action(
6821
6983
  wrapAction(async (opts) => {
@@ -7433,6 +7595,23 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
7433
7595
  }
7434
7596
  )
7435
7597
  );
7598
+ bulkCmd.command("priority <taskIds...>").description("Bulk set priority on tasks (urgent/high/normal/low or 1-4)").requiredOption("--to <priority>", "Priority to set (urgent, high, normal, low, or 1-4)").option("--json", "Force JSON output even in terminal").action(
7599
+ wrapAction(async (taskIds, opts) => {
7600
+ const config = loadConfig(getProfileName());
7601
+ const result = await bulkPriority(config, opts.to, taskIds);
7602
+ outputBulkResult(result, opts.json ?? false, `priority ${opts.to}`);
7603
+ })
7604
+ );
7605
+ bulkCmd.command("field <taskIds...>").description("Bulk set the same custom field value on tasks").requiredOption("--set <nameAndValue...>", 'Set field: --set "Field Name" value').option("--json", "Force JSON output even in terminal").action(
7606
+ wrapAction(async (taskIds, opts) => {
7607
+ if (opts.set.length !== 2) {
7608
+ throw new Error("--set requires exactly two arguments: field name and value");
7609
+ }
7610
+ const config = loadConfig(getProfileName());
7611
+ const result = await bulkField(config, opts.set[0], opts.set[1], taskIds);
7612
+ outputBulkResult(result, opts.json ?? false, `field "${opts.set[0]}"`);
7613
+ })
7614
+ );
7436
7615
  program.command("goals").description("List goals in your workspace").option("--json", "Force JSON output even in terminal").action(
7437
7616
  wrapAction(async (opts) => {
7438
7617
  const config = loadConfig(getProfileName());
@@ -7602,18 +7781,20 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
7602
7781
  }
7603
7782
  })
7604
7783
  );
7605
- program.command("folders <spaceId>").description("List folders in a space (with their lists)").option("--name <partial>", "Filter folders by partial name match").option("--json", "Force JSON output even in terminal").action(
7606
- wrapAction(async (spaceId, opts) => {
7607
- const config = loadConfig(getProfileName());
7608
- const folders = await listFolders(config, spaceId, opts.name);
7609
- if (shouldOutputJson(opts.json ?? false)) {
7610
- console.log(JSON.stringify(folders, null, 2));
7611
- } else if (isTTY()) {
7612
- console.log(formatFolders(folders));
7613
- } else {
7614
- console.log(formatFoldersMarkdown(folders));
7784
+ program.command("folders <spaceId>").description("List folders in a space (with their lists)").option("--name <partial>", "Filter folders by partial name match").option("--archived", "Include only archived items (default: active items)").option("--json", "Force JSON output even in terminal").action(
7785
+ wrapAction(
7786
+ async (spaceId, opts) => {
7787
+ const config = loadConfig(getProfileName());
7788
+ const folders = await listFolders(config, spaceId, opts.name, opts.archived);
7789
+ if (shouldOutputJson(opts.json ?? false)) {
7790
+ console.log(JSON.stringify(folders, null, 2));
7791
+ } else if (isTTY()) {
7792
+ console.log(formatFolders(folders));
7793
+ } else {
7794
+ console.log(formatFoldersMarkdown(folders));
7795
+ }
7615
7796
  }
7616
- })
7797
+ )
7617
7798
  );
7618
7799
  program.command("space-create <name>").description("Create a new space in your workspace").option("--json", "Force JSON output even in terminal").action(
7619
7800
  wrapAction(async (name, opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.21.1",
3
+ "version": "1.22.1",
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.17.0
6
+ # ClickUp CLI (`cup`) - skill version 1.22.0
7
7
 
8
- Reference for AI agents using the `cup` CLI tool. Covers task management, sprint tracking, comments, and project workflows.
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.18.0, 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.22.0, update with `npm install -g @krodak/clickup-cli` and refresh this skill with `cup skill`.
11
11
 
12
12
  ## Install & Configure
13
13
 
@@ -152,7 +152,7 @@ All commands support `--help` for full flag details. All commands support `--jso
152
152
  | `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 |
153
153
  | `cup replies <commentId>` | List threaded replies |
154
154
  | `cup reply <commentId> -m text [--notify-all]` | Reply to a comment |
155
- | `cup assign <id> [--to userId\|me] [--remove userId\|me]` | Assign/unassign users |
155
+ | `cup assign <id> [--to ids\|me] [--remove ids\|me]` | Assign/unassign users (`--to`/`--remove` accept comma-separated IDs) |
156
156
  | `cup depend <id> [--on taskId] [--blocks taskId] [--remove]` | Add/remove dependencies |
157
157
  | `cup move <id> [--to listId\|sprint:current] [--remove listId]` | Add/remove task from lists (`--to` accepts `sprint:current`) |
158
158
  | `cup field <id> [--set "Name" value] [--remove "Name"]` | Set/remove custom field values |
@@ -219,32 +219,32 @@ All commands support `--help` for full flag details. All commands support `--jso
219
219
 
220
220
  ## Flags & Conventions
221
221
 
222
- | Topic | Detail |
223
- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224
- | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
225
- | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr |
226
- | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
227
- | `--due-date` | `YYYY-MM-DD` format |
228
- | `--assignee` | User ID or `me` |
229
- | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
230
- | `--time-estimate` | Duration: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
231
- | `--type` | `task` (regular) or custom type name/ID (e.g. `initiative`, `Bug`) |
232
- | `--custom-item-id` | Custom task type ID for `cup create` (find with `cup task-types`) |
233
- | `--space` | Partial name match or exact ID |
234
- | `--name` | Partial match, case-insensitive |
235
- | `--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) |
236
- | `--include-closed` | Include closed/done tasks |
237
- | `--list` on create | Optional when `--parent` is given (auto-detected). Accepts `sprint:current` pseudo-ID |
238
- | `cup field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), labels (comma-separated names), date (YYYY-MM-DD), url, email. Names resolved case-insensitively; errors list available fields/options |
239
- | `cup field-create` | Use `--options "a,b,c"` for `drop_down` and `labels` types (required). Other types don't need `--options` |
240
- | `--field` filter | `--field "Name" value` on `tasks` and `search` requires `--list` to resolve field names to IDs |
241
- | `--due-before/after` | `YYYY-MM-DD` date filters for due date range |
242
- | `--created-before/after` | `YYYY-MM-DD` date filters for creation date range |
243
- | `cup sprint` | Auto-detects active sprint by folder name (sprint/iteration/cycle/scrum), parses multiple date formats. Override with `--folder <id>`, `cup config set sprintFolderId <id>`, or favorite a sprint folder |
244
- | `cup favorite` | Local-only favorites (not synced to ClickUp). Types: sprint-folder, space, list, folder, view, task. Favorited sprint-folders auto-used by sprint commands |
245
- | `cup link` | Both IDs must be the same type (both custom or both native) |
246
- | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
247
- | Errors | stderr with exit code 1. Strict parsing - excess/unknown arguments rejected |
222
+ | Topic | Detail |
223
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224
+ | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
225
+ | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr |
226
+ | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
227
+ | `--due-date` | `YYYY-MM-DD` format |
228
+ | `--assignee` | User ID or `me` |
229
+ | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
230
+ | `--time-estimate` | Duration: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
231
+ | `--type` | `task` (regular) or custom type name/ID (e.g. `initiative`, `Bug`) |
232
+ | `--custom-item-id` | Custom task type ID for `cup create` (find with `cup task-types`) |
233
+ | `--space` | Partial name match or exact ID |
234
+ | `--name` | Partial match, case-insensitive |
235
+ | `--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) |
236
+ | `--include-closed` | Include closed/done tasks |
237
+ | `--list` on create | Optional when `--parent` is given (auto-detected). Accepts `sprint:current` pseudo-ID |
238
+ | `cup field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), labels (comma-separated names), date (YYYY-MM-DD), url, email, emoji/rating (0-5), manual_progress (0-100), tasks/relationship (comma-separated task IDs), users/people (comma-separated user IDs). Names resolved case-insensitively; errors list available fields/options |
239
+ | `cup field-create` | Use `--options "a,b,c"` for `drop_down` and `labels` types (required). Other types don't need `--options` |
240
+ | `--field` filter | `--field "Name" value` on `tasks` and `search` requires `--list` to resolve field names to IDs |
241
+ | `--due-before/after` | `YYYY-MM-DD` date filters for due date range |
242
+ | `--created-before/after` | `YYYY-MM-DD` date filters for creation date range |
243
+ | `cup sprint` | Auto-detects active sprint by folder name (sprint/iteration/cycle/scrum), parses multiple date formats. Override with `--folder <id>`, `cup config set sprintFolderId <id>`, or favorite a sprint folder |
244
+ | `cup favorite` | Local-only favorites (not synced to ClickUp). Types: sprint-folder, space, list, folder, view, task. Favorited sprint-folders auto-used by sprint commands |
245
+ | `cup link` | Both IDs must be the same type (both custom or both native) |
246
+ | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
247
+ | Errors | stderr with exit code 1. Strict parsing - excess/unknown arguments rejected |
248
248
 
249
249
  ## Agent Workflow Examples
250
250
 
@@ -289,6 +289,7 @@ cup create -n "Bug fix" -l sprint:current # create in active sprint
289
289
  cup create -n "Q3 Roadmap" -l <listId> --custom-item-id 1
290
290
  cup comment abc123def -m "Completed in PR #42"
291
291
  cup assign abc123def --to me
292
+ cup assign abc123def --to 12345,67890 # assign multiple users at once
292
293
  cup depend task3 --on task2 # task3 waits for task2
293
294
  cup move task1 --to list2 --remove list1
294
295
  cup move task1 --to sprint:current # move to active sprint