@krodak/clickup-cli 0.9.1 → 0.11.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.
package/README.md CHANGED
@@ -98,7 +98,7 @@ When output is piped (no TTY), all commands output **Markdown** by default - opt
98
98
 
99
99
  ## Commands
100
100
 
101
- 24 commands total. All support `--help` for full flag details.
101
+ 25 commands total. All support `--help` for full flag details.
102
102
 
103
103
  ### `cu init`
104
104
 
@@ -118,6 +118,7 @@ cu tasks --status "in progress"
118
118
  cu tasks --name "login"
119
119
  cu tasks --list <listId>
120
120
  cu tasks --space <spaceId>
121
+ cu tasks --include-closed
121
122
  cu tasks --json
122
123
  ```
123
124
 
@@ -131,6 +132,7 @@ cu initiatives --status "to do"
131
132
  cu initiatives --name "auth"
132
133
  cu initiatives --list <listId>
133
134
  cu initiatives --space <spaceId>
135
+ cu initiatives --include-closed
134
136
  cu initiatives --json
135
137
  ```
136
138
 
@@ -141,6 +143,7 @@ List my tasks in the currently active sprint (auto-detected from sprint folder d
141
143
  ```bash
142
144
  cu sprint
143
145
  cu sprint --status "in progress"
146
+ cu sprint --include-closed
144
147
  cu sprint --json
145
148
  ```
146
149
 
@@ -159,6 +162,7 @@ All tasks assigned to me, grouped by pipeline stage (code review, in progress, t
159
162
 
160
163
  ```bash
161
164
  cu assigned
165
+ cu assigned --status "in progress"
162
166
  cu assigned --include-closed
163
167
  cu assigned --json
164
168
  ```
@@ -190,6 +194,8 @@ List subtasks of a task or initiative.
190
194
 
191
195
  ```bash
192
196
  cu subtasks abc123
197
+ cu subtasks abc123 --status "in progress"
198
+ cu subtasks abc123 --name "auth"
193
199
  cu subtasks abc123 --include-closed
194
200
  cu subtasks abc123 --json
195
201
  ```
@@ -206,18 +212,20 @@ cu update abc123 --priority high
206
212
  cu update abc123 --due-date 2025-03-15
207
213
  cu update abc123 --assignee 12345
208
214
  cu update abc123 -n "New name" -s "done" --priority urgent
215
+ cu update abc123 --time-estimate 2h
209
216
  cu update abc123 -s "in progress" --json
210
217
  ```
211
218
 
212
- | Flag | Description |
213
- | -------------------------- | --------------------------------------------------------------------------- |
214
- | `-n, --name <text>` | New task name |
215
- | `-d, --description <text>` | New description (markdown supported) |
216
- | `-s, --status <status>` | New status, supports fuzzy matching (e.g. `"prog"` matches `"in progress"`) |
217
- | `--priority <level>` | Priority: `urgent`, `high`, `normal`, `low` (or 1-4) |
218
- | `--due-date <date>` | Due date (`YYYY-MM-DD`) |
219
- | `--assignee <userId>` | Add assignee by numeric user ID |
220
- | `--json` | Force JSON output even in terminal |
219
+ | Flag | Description |
220
+ | ---------------------------- | --------------------------------------------------------------------------- |
221
+ | `-n, --name <text>` | New task name |
222
+ | `-d, --description <text>` | New description (markdown supported) |
223
+ | `-s, --status <status>` | New status, supports fuzzy matching (e.g. `"prog"` matches `"in progress"`) |
224
+ | `--priority <level>` | Priority: `urgent`, `high`, `normal`, `low` (or 1-4) |
225
+ | `--due-date <date>` | Due date (`YYYY-MM-DD`) |
226
+ | `--time-estimate <duration>` | Time estimate (e.g. `"2h"`, `"30m"`, `"1h30m"`) |
227
+ | `--assignee <userId>` | Add assignee by numeric user ID |
228
+ | `--json` | Force JSON output even in terminal |
221
229
 
222
230
  ### `cu create`
223
231
 
@@ -229,21 +237,25 @@ cu create -n "Subtask name" -p <parentTaskId> # --list auto-detected
229
237
  cu create -n "Task" -l <listId> -d "desc" -s "open"
230
238
  cu create -n "Task" -l <listId> --priority high --due-date 2025-06-01
231
239
  cu create -n "Task" -l <listId> --assignee 12345 --tags "bug,frontend"
240
+ cu create -n "Initiative" -l <listId> --custom-item-id 1
241
+ cu create -n "Task" -l <listId> --time-estimate 2h
232
242
  cu create -n "Fix bug" -l <listId> --json
233
243
  ```
234
244
 
235
- | Flag | Required | Description |
236
- | -------------------------- | ---------------- | ---------------------------------------------------- |
237
- | `-n, --name <name>` | yes | Task name |
238
- | `-l, --list <listId>` | if no `--parent` | Target list ID |
239
- | `-p, --parent <taskId>` | no | Parent task (list auto-detected) |
240
- | `-d, --description <text>` | no | Description (markdown) |
241
- | `-s, --status <status>` | no | Initial status |
242
- | `--priority <level>` | no | Priority: `urgent`, `high`, `normal`, `low` (or 1-4) |
243
- | `--due-date <date>` | no | Due date (`YYYY-MM-DD`) |
244
- | `--assignee <userId>` | no | Assignee by numeric user ID |
245
- | `--tags <tags>` | no | Comma-separated tag names |
246
- | `--json` | no | Force JSON output even in terminal |
245
+ | Flag | Required | Description |
246
+ | ---------------------------- | ---------------- | ---------------------------------------------------- |
247
+ | `-n, --name <name>` | yes | Task name |
248
+ | `-l, --list <listId>` | if no `--parent` | Target list ID |
249
+ | `-p, --parent <taskId>` | no | Parent task (list auto-detected) |
250
+ | `-d, --description <text>` | no | Description (markdown) |
251
+ | `-s, --status <status>` | no | Initial status |
252
+ | `--priority <level>` | no | Priority: `urgent`, `high`, `normal`, `low` (or 1-4) |
253
+ | `--due-date <date>` | no | Due date (`YYYY-MM-DD`) |
254
+ | `--time-estimate <duration>` | no | Time estimate (e.g. `"2h"`, `"30m"`, `"1h30m"`) |
255
+ | `--assignee <userId>` | no | Assignee by numeric user ID |
256
+ | `--tags <tags>` | no | Comma-separated tag names |
257
+ | `--custom-item-id <id>` | no | Custom task type ID (for creating initiatives) |
258
+ | `--json` | no | Force JSON output even in terminal |
247
259
 
248
260
  ### `cu comment <id>`
249
261
 
@@ -324,6 +336,7 @@ cu search "login bug"
324
336
  cu search auth
325
337
  cu search "payment flow" --json
326
338
  cu search auth --status "prog" # fuzzy matches "in progress"
339
+ cu search "old task" --include-closed
327
340
  ```
328
341
 
329
342
  ### `cu summary`
@@ -368,6 +381,25 @@ cu assign abc123 --to me --json
368
381
  | `--remove <userId>` | Remove assignee (user ID or `me`) |
369
382
  | `--json` | Force JSON output |
370
383
 
384
+ ### `cu depend <id>`
385
+
386
+ Add or remove task dependencies. Set a task as waiting on or blocking another task.
387
+
388
+ ```bash
389
+ cu depend abc123 --on def456 # abc123 depends on (waits for) def456
390
+ cu depend abc123 --blocks def456 # abc123 blocks def456
391
+ cu depend abc123 --on def456 --remove # remove the dependency
392
+ cu depend abc123 --blocks def456 --remove
393
+ cu depend abc123 --on def456 --json
394
+ ```
395
+
396
+ | Flag | Description |
397
+ | ------------------- | ------------------------------------------- |
398
+ | `--on <taskId>` | Task that this task depends on (waiting on) |
399
+ | `--blocks <taskId>` | Task that this task blocks |
400
+ | `--remove` | Remove the dependency instead of adding it |
401
+ | `--json` | Force JSON output |
402
+
371
403
  ### `cu auth`
372
404
 
373
405
  Check authentication status. Validates your API token and shows your user info.
package/dist/index.js CHANGED
@@ -223,6 +223,23 @@ var ClickUpClient = class {
223
223
  async getViewTasks(viewId) {
224
224
  return this.paginate((page) => `/view/${viewId}/task?page=${page}`);
225
225
  }
226
+ async addDependency(taskId, opts) {
227
+ const body = {};
228
+ if (opts.dependsOn) body.depends_on = opts.dependsOn;
229
+ if (opts.dependencyOf) body.dependency_of = opts.dependencyOf;
230
+ await this.request(`/task/${taskId}/dependency`, {
231
+ method: "POST",
232
+ body: JSON.stringify(body)
233
+ });
234
+ }
235
+ async deleteDependency(taskId, opts) {
236
+ const params = new URLSearchParams();
237
+ if (opts.dependsOn) params.set("depends_on", opts.dependsOn);
238
+ if (opts.dependencyOf) params.set("dependency_of", opts.dependencyOf);
239
+ await this.request(`/task/${taskId}/dependency?${params.toString()}`, {
240
+ method: "DELETE"
241
+ });
242
+ }
226
243
  };
227
244
 
228
245
  // src/output.ts
@@ -680,6 +697,18 @@ function parseAssigneeId(value) {
680
697
  if (!Number.isInteger(id)) throw new Error("Assignee must be a numeric user ID");
681
698
  return id;
682
699
  }
700
+ function parseTimeEstimate(value) {
701
+ const pattern = /^(?:(\d+)h)?(?:(\d+)m)?$/i;
702
+ const match = value.match(pattern);
703
+ if (match && (match[1] || match[2])) {
704
+ const hours = Number(match[1] ?? 0);
705
+ const minutes = Number(match[2] ?? 0);
706
+ return (hours * 60 + minutes) * 60 * 1e3;
707
+ }
708
+ const ms = Number(value);
709
+ if (Number.isFinite(ms) && ms > 0) return ms;
710
+ throw new Error('Time estimate must be a duration (e.g. "2h", "30m", "1h30m") or milliseconds');
711
+ }
683
712
  function buildUpdatePayload(opts) {
684
713
  const payload = {};
685
714
  if (opts.name !== void 0) payload.name = opts.name;
@@ -693,10 +722,13 @@ function buildUpdatePayload(opts) {
693
722
  if (opts.assignee !== void 0) {
694
723
  payload.assignees = { add: [parseAssigneeId(opts.assignee)] };
695
724
  }
725
+ if (opts.timeEstimate !== void 0) {
726
+ payload.time_estimate = parseTimeEstimate(opts.timeEstimate);
727
+ }
696
728
  return payload;
697
729
  }
698
730
  function hasUpdateFields(options) {
699
- return options.name !== void 0 || options.description !== void 0 || options.status !== void 0 || options.priority !== void 0 || options.due_date !== void 0 || options.assignees !== void 0;
731
+ return options.name !== void 0 || options.description !== void 0 || options.status !== void 0 || options.priority !== void 0 || options.due_date !== void 0 || options.time_estimate !== void 0 || options.assignees !== void 0;
700
732
  }
701
733
  async function resolveStatus(client, taskId, statusInput) {
702
734
  const task = await client.getTask(taskId);
@@ -716,7 +748,7 @@ async function resolveStatus(client, taskId, statusInput) {
716
748
  async function updateTask(config, taskId, options) {
717
749
  if (!hasUpdateFields(options))
718
750
  throw new Error(
719
- "Provide at least one of: --name, --description, --status, --priority, --due-date, --assignee"
751
+ "Provide at least one of: --name, --description, --status, --priority, --due-date, --time-estimate, --assignee"
720
752
  );
721
753
  const client = new ClickUpClient(config);
722
754
  if (options.status !== void 0) {
@@ -756,6 +788,15 @@ async function createTask(config, options) {
756
788
  if (options.tags !== void 0) {
757
789
  payload.tags = options.tags.split(",").map((t) => t.trim());
758
790
  }
791
+ if (options.customItemId !== void 0) {
792
+ const id = Number(options.customItemId);
793
+ if (!Number.isInteger(id) || id < 0)
794
+ throw new Error("Custom item ID must be a non-negative integer");
795
+ payload.custom_item_id = id;
796
+ }
797
+ if (options.timeEstimate !== void 0) {
798
+ payload.time_estimate = parseTimeEstimate(options.timeEstimate);
799
+ }
759
800
  const task = await client.createTask(listId, payload);
760
801
  return { id: task.id, name: task.name, url: task.url };
761
802
  }
@@ -893,7 +934,10 @@ async function runSprintCommand(config, opts) {
893
934
  } else {
894
935
  allTasks = await client.getTasksFromList(activeList.id);
895
936
  }
896
- const sprintTasks = allTasks.filter((t) => t.assignees.some((a) => Number(a.id) === me.id));
937
+ let sprintTasks = allTasks.filter((t) => t.assignees.some((a) => Number(a.id) === me.id));
938
+ if (!opts.includeClosed) {
939
+ sprintTasks = sprintTasks.filter((t) => !isDoneStatus(t.status.status));
940
+ }
897
941
  const filtered = opts.status ? sprintTasks.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase()) : sprintTasks;
898
942
  const summaries = filtered.map(summarize);
899
943
  await printTasks(summaries, opts.json ?? false, config);
@@ -956,7 +1000,7 @@ async function listSprints(config, opts = {}) {
956
1000
  const lists = listsByFolder[i];
957
1001
  allSprints.push(...buildSprintInfos(lists, folder.name, today));
958
1002
  }
959
- if (opts.json || !isTTY()) {
1003
+ if (shouldOutputJson(opts.json ?? false)) {
960
1004
  console.log(JSON.stringify(allSprints, null, 2));
961
1005
  return;
962
1006
  }
@@ -973,6 +1017,15 @@ async function listSprints(config, opts = {}) {
973
1017
  dates: dateStr
974
1018
  };
975
1019
  });
1020
+ if (!isTTY()) {
1021
+ const mdColumns = [
1022
+ { key: "id", label: "ID" },
1023
+ { key: "sprint", label: "Sprint" },
1024
+ { key: "dates", label: "Dates" }
1025
+ ];
1026
+ console.log(formatMarkdownTable(rows, mdColumns));
1027
+ return;
1028
+ }
976
1029
  console.log(formatTable(rows, SPRINT_COLUMNS));
977
1030
  }
978
1031
 
@@ -1190,6 +1243,8 @@ async function listSpaces(config, opts) {
1190
1243
  console.log(JSON.stringify(spaces, null, 2));
1191
1244
  } else if (!isTTY()) {
1192
1245
  console.log(formatSpacesMarkdown(spaces.map((s) => ({ id: s.id, name: s.name }))));
1246
+ } else if (spaces.length === 0) {
1247
+ console.log("No spaces found.");
1193
1248
  } else {
1194
1249
  const table = formatTable(
1195
1250
  spaces.map((s) => ({ id: s.id, name: s.name })),
@@ -1254,7 +1309,11 @@ async function runAssignedCommand(config, opts) {
1254
1309
  const allTasks = await client.getMyTasks(config.teamId, {
1255
1310
  includeClosed: opts.includeClosed
1256
1311
  });
1257
- const groups = groupByStatus(allTasks, opts.includeClosed ?? false);
1312
+ let groups = groupByStatus(allTasks, opts.includeClosed ?? false);
1313
+ if (opts.status) {
1314
+ const lower = opts.status.toLowerCase();
1315
+ groups = groups.filter((g) => g.status.toLowerCase() === lower);
1316
+ }
1258
1317
  if (shouldOutputJson(opts.json ?? false)) {
1259
1318
  const result = {};
1260
1319
  for (const group of groups) {
@@ -1511,10 +1570,20 @@ async function fetchActivity(config, taskId) {
1511
1570
  return { task, comments };
1512
1571
  }
1513
1572
  function printActivity(result, forceJson) {
1514
- if (forceJson || !isTTY()) {
1573
+ if (shouldOutputJson(forceJson)) {
1515
1574
  console.log(JSON.stringify(result, null, 2));
1516
1575
  return;
1517
1576
  }
1577
+ if (!isTTY()) {
1578
+ const taskMd = formatTaskDetailMarkdown(result.task);
1579
+ const commentsMd = formatCommentsMarkdown(result.comments);
1580
+ console.log(`${taskMd}
1581
+
1582
+ ## Comments
1583
+
1584
+ ${commentsMd}`);
1585
+ return;
1586
+ }
1518
1587
  console.log(formatTaskDetail(result.task));
1519
1588
  console.log("");
1520
1589
  console.log(chalk4.bold("Comments"));
@@ -1957,7 +2026,9 @@ async function searchTasks(config, query, opts = {}) {
1957
2026
  throw new Error("Search query cannot be empty");
1958
2027
  }
1959
2028
  const client = new ClickUpClient(config);
1960
- const allTasks = await client.getMyTasks(config.teamId);
2029
+ const allTasks = await client.getMyTasks(config.teamId, {
2030
+ includeClosed: opts.includeClosed
2031
+ });
1961
2032
  const words = trimmed.toLowerCase().split(/\s+/);
1962
2033
  let matched = allTasks.filter((task) => {
1963
2034
  const name = task.name.toLowerCase();
@@ -1979,6 +2050,31 @@ async function searchTasks(config, query, opts = {}) {
1979
2050
  return matched.map(summarize);
1980
2051
  }
1981
2052
 
2053
+ // src/commands/depend.ts
2054
+ async function manageDependency(config, taskId, opts) {
2055
+ if (!opts.on && !opts.blocks) {
2056
+ throw new Error("Provide --on <taskId> or --blocks <taskId>");
2057
+ }
2058
+ if (opts.on && opts.blocks) {
2059
+ throw new Error("Provide only one of --on or --blocks per invocation");
2060
+ }
2061
+ const client = new ClickUpClient(config);
2062
+ if (opts.remove) {
2063
+ await client.deleteDependency(taskId, {
2064
+ dependsOn: opts.on,
2065
+ dependencyOf: opts.blocks
2066
+ });
2067
+ if (opts.on) return `Removed dependency: ${taskId} no longer depends on ${opts.on}`;
2068
+ return `Removed dependency: ${taskId} no longer blocks ${opts.blocks}`;
2069
+ }
2070
+ await client.addDependency(taskId, {
2071
+ dependsOn: opts.on,
2072
+ dependencyOf: opts.blocks
2073
+ });
2074
+ if (opts.on) return `Added dependency: ${taskId} depends on ${opts.on}`;
2075
+ return `Added dependency: ${taskId} blocks ${opts.blocks}`;
2076
+ }
2077
+
1982
2078
  // src/index.ts
1983
2079
  var require2 = createRequire(import.meta.url);
1984
2080
  var { version } = require2("../package.json");
@@ -2001,17 +2097,16 @@ program.command("auth").description("Validate API token and show current user").
2001
2097
  wrapAction(async (opts) => {
2002
2098
  const config = loadConfig();
2003
2099
  const result = await checkAuth(config);
2004
- if (opts.json || !isTTY()) {
2100
+ if (shouldOutputJson(opts.json ?? false)) {
2005
2101
  console.log(JSON.stringify(result, null, 2));
2006
2102
  } else if (result.authenticated && result.user) {
2007
2103
  console.log(`Authenticated as @${result.user.username} (id: ${result.user.id})`);
2008
2104
  } else {
2009
- console.error(`Authentication failed: ${result.error ?? "unknown error"}`);
2010
- process.exit(1);
2105
+ throw new Error(`Authentication failed: ${result.error ?? "unknown error"}`);
2011
2106
  }
2012
2107
  })
2013
2108
  );
2014
- program.command("tasks").description("List tasks assigned to me").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("--json", "Force JSON output even in terminal").action(
2109
+ program.command("tasks").description("List tasks assigned to me").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("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
2015
2110
  wrapAction(async (opts) => {
2016
2111
  const config = loadConfig();
2017
2112
  const tasks = await fetchMyTasks(config, {
@@ -2019,12 +2114,13 @@ program.command("tasks").description("List tasks assigned to me").option("--stat
2019
2114
  statuses: opts.status ? [opts.status] : void 0,
2020
2115
  listIds: opts.list ? [opts.list] : void 0,
2021
2116
  spaceIds: opts.space ? [opts.space] : void 0,
2022
- name: opts.name
2117
+ name: opts.name,
2118
+ includeClosed: opts.includeClosed
2023
2119
  });
2024
2120
  await printTasks(tasks, opts.json ?? false, config);
2025
2121
  })
2026
2122
  );
2027
- program.command("initiatives").description("List initiatives assigned to me").option("--status <status>", "Filter by status").option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
2123
+ program.command("initiatives").description("List initiatives assigned to me").option("--status <status>", "Filter by status").option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
2028
2124
  wrapAction(async (opts) => {
2029
2125
  const config = loadConfig();
2030
2126
  const tasks = await fetchMyTasks(config, {
@@ -2032,7 +2128,8 @@ program.command("initiatives").description("List initiatives assigned to me").op
2032
2128
  statuses: opts.status ? [opts.status] : void 0,
2033
2129
  listIds: opts.list ? [opts.list] : void 0,
2034
2130
  spaceIds: opts.space ? [opts.space] : void 0,
2035
- name: opts.name
2131
+ name: opts.name,
2132
+ includeClosed: opts.includeClosed
2036
2133
  });
2037
2134
  await printTasks(tasks, opts.json ?? false, config);
2038
2135
  })
@@ -2050,34 +2147,36 @@ program.command("task <taskId>").description("Get task details").option("--json"
2050
2147
  }
2051
2148
  })
2052
2149
  );
2053
- program.command("update <taskId>").description("Update a task").option("-n, --name <text>", "New task name").option("-d, --description <text>", "New description (markdown supported)").option("-s, --status <status>", 'New status (e.g. "in progress", "done")').option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Add assignee by user ID").option("--json", "Force JSON output even in terminal").action(
2150
+ program.command("update <taskId>").description("Update a task").option("-n, --name <text>", "New task name").option("-d, --description <text>", "New description (markdown supported)").option("-s, --status <status>", 'New status (e.g. "in progress", "done")').option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--time-estimate <duration>", 'Time estimate (e.g. "2h", "30m", "1h30m")').option("--assignee <userId>", "Add assignee by user ID").option("--json", "Force JSON output even in terminal").action(
2054
2151
  wrapAction(async (taskId, opts) => {
2055
2152
  const config = loadConfig();
2056
2153
  const payload = buildUpdatePayload(opts);
2057
2154
  const result = await updateTask(config, taskId, payload);
2058
- if (shouldOutputJson(false)) {
2155
+ if (shouldOutputJson(opts.json ?? false)) {
2059
2156
  console.log(JSON.stringify(result, null, 2));
2060
2157
  } else {
2061
2158
  console.log(formatUpdateConfirmation(result.id, result.name));
2062
2159
  }
2063
2160
  })
2064
2161
  );
2065
- program.command("create").description("Create a new task").option("-l, --list <listId>", "Target list ID (auto-detected from --parent if omitted)").requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Assignee user ID").option("--tags <tags>", "Comma-separated tag names").option("--json", "Force JSON output even in terminal").action(
2162
+ program.command("create").description("Create a new task").option("-l, --list <listId>", "Target list ID (auto-detected from --parent if omitted)").requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Assignee user ID").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("--json", "Force JSON output even in terminal").action(
2066
2163
  wrapAction(async (opts) => {
2067
2164
  const config = loadConfig();
2068
2165
  const result = await createTask(config, opts);
2069
- if (shouldOutputJson(false)) {
2166
+ if (shouldOutputJson(opts.json ?? false)) {
2070
2167
  console.log(JSON.stringify(result, null, 2));
2071
2168
  } else {
2072
2169
  console.log(formatCreateConfirmation(result.id, result.name, result.url));
2073
2170
  }
2074
2171
  })
2075
2172
  );
2076
- program.command("sprint").description("List my tasks in the current active sprint (auto-detected)").option("--status <status>", "Filter by status").option("--space <nameOrId>", "Narrow sprint search to a specific space (partial name or ID)").option("--json", "Force JSON output even in terminal").action(
2077
- wrapAction(async (opts) => {
2078
- const config = loadConfig();
2079
- await runSprintCommand(config, opts);
2080
- })
2173
+ program.command("sprint").description("List my tasks in the current active sprint (auto-detected)").option("--status <status>", "Filter by status").option("--space <nameOrId>", "Narrow sprint search to a specific space (partial name or ID)").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
2174
+ wrapAction(
2175
+ async (opts) => {
2176
+ const config = loadConfig();
2177
+ await runSprintCommand(config, opts);
2178
+ }
2179
+ )
2081
2180
  );
2082
2181
  program.command("sprints").description("List all sprints in sprint folders").option("--space <nameOrId>", "Filter by space (partial name or ID)").option("--json", "Force JSON output even in terminal").action(
2083
2182
  wrapAction(async (opts) => {
@@ -2085,18 +2184,28 @@ program.command("sprints").description("List all sprints in sprint folders").opt
2085
2184
  await listSprints(config, opts);
2086
2185
  })
2087
2186
  );
2088
- program.command("subtasks <taskId>").description("List subtasks of a task or initiative").option("--include-closed", "Include closed/done subtasks").option("--json", "Force JSON output even in terminal").action(
2089
- wrapAction(async (taskId, opts) => {
2090
- const config = loadConfig();
2091
- const tasks = await fetchSubtasks(config, taskId, { includeClosed: opts.includeClosed });
2092
- await printTasks(tasks, opts.json ?? false, config);
2093
- })
2187
+ program.command("subtasks <taskId>").description("List subtasks of a task or initiative").option("--status <status>", "Filter by status").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--include-closed", "Include closed/done subtasks").option("--json", "Force JSON output even in terminal").action(
2188
+ wrapAction(
2189
+ async (taskId, opts) => {
2190
+ const config = loadConfig();
2191
+ let tasks = await fetchSubtasks(config, taskId, { includeClosed: opts.includeClosed });
2192
+ if (opts.status) {
2193
+ const lower = opts.status.toLowerCase();
2194
+ tasks = tasks.filter((t) => t.status.toLowerCase() === lower);
2195
+ }
2196
+ if (opts.name) {
2197
+ const query = opts.name.toLowerCase();
2198
+ tasks = tasks.filter((t) => t.name.toLowerCase().includes(query));
2199
+ }
2200
+ await printTasks(tasks, opts.json ?? false, config);
2201
+ }
2202
+ )
2094
2203
  );
2095
- program.command("comment <taskId>").description("Post a comment on a task").requiredOption("-m, --message <text>", "Comment text").action(
2204
+ program.command("comment <taskId>").description("Post a comment on a task").requiredOption("-m, --message <text>", "Comment text").option("--json", "Force JSON output even in terminal").action(
2096
2205
  wrapAction(async (taskId, opts) => {
2097
2206
  const config = loadConfig();
2098
2207
  const result = await postComment(config, taskId, opts.message);
2099
- if (shouldOutputJson(false)) {
2208
+ if (shouldOutputJson(opts.json ?? false)) {
2100
2209
  console.log(JSON.stringify(result, null, 2));
2101
2210
  } else {
2102
2211
  console.log(formatCommentConfirmation(result.id));
@@ -2135,14 +2244,13 @@ program.command("inbox").description("Recently updated tasks grouped by time per
2135
2244
  const config = loadConfig();
2136
2245
  const days = Number(opts.days ?? 30);
2137
2246
  if (!Number.isFinite(days) || days <= 0) {
2138
- console.error("Error: --days must be a positive number");
2139
- process.exit(1);
2247
+ throw new Error("--days must be a positive number");
2140
2248
  }
2141
2249
  const tasks = await fetchInbox(config, days);
2142
2250
  await printInbox(tasks, opts.json ?? false, config);
2143
2251
  })
2144
2252
  );
2145
- program.command("assigned").description("Show all tasks assigned to me, grouped by status").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
2253
+ program.command("assigned").description("Show all tasks assigned to me, grouped by status").option("--status <status>", "Show only tasks with this status").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
2146
2254
  wrapAction(async (opts) => {
2147
2255
  const config = loadConfig();
2148
2256
  await runAssignedCommand(config, opts);
@@ -2154,20 +2262,24 @@ program.command("open <query>").description("Open a task in the browser by ID or
2154
2262
  await openTask(config, query, opts);
2155
2263
  })
2156
2264
  );
2157
- program.command("search <query>").description("Search my tasks by name").option("--status <status>", "Filter by status").option("--json", "Force JSON output even in terminal").action(
2158
- wrapAction(async (query, opts) => {
2159
- const config = loadConfig();
2160
- const tasks = await searchTasks(config, query, { status: opts.status });
2161
- await printTasks(tasks, opts.json ?? false, config);
2162
- })
2265
+ program.command("search <query>").description("Search my tasks by name").option("--status <status>", "Filter by status").option("--include-closed", "Include done/closed tasks in search").option("--json", "Force JSON output even in terminal").action(
2266
+ wrapAction(
2267
+ async (query, opts) => {
2268
+ const config = loadConfig();
2269
+ const tasks = await searchTasks(config, query, {
2270
+ status: opts.status,
2271
+ includeClosed: opts.includeClosed
2272
+ });
2273
+ await printTasks(tasks, opts.json ?? false, config);
2274
+ }
2275
+ )
2163
2276
  );
2164
2277
  program.command("summary").description("Daily standup summary: completed, in-progress, overdue").option("--hours <n>", "Completed-tasks lookback in hours", "24").option("--json", "Force JSON output even in terminal").action(
2165
2278
  wrapAction(async (opts) => {
2166
2279
  const config = loadConfig();
2167
2280
  const hours = Number(opts.hours ?? 24);
2168
2281
  if (!Number.isFinite(hours) || hours <= 0) {
2169
- console.error("Error: --hours must be a positive number");
2170
- process.exit(1);
2282
+ throw new Error("--hours must be a positive number");
2171
2283
  }
2172
2284
  await runSummaryCommand(config, { hours, json: opts.json ?? false });
2173
2285
  })
@@ -2190,6 +2302,17 @@ program.command("assign <taskId>").description("Assign or unassign users from a
2190
2302
  }
2191
2303
  })
2192
2304
  );
2305
+ program.command("depend <taskId>").description("Add or remove task dependencies").option("--on <taskId>", "Task that this task depends on (waiting on)").option("--blocks <taskId>", "Task that this task blocks").option("--remove", "Remove the dependency instead of adding it").option("--json", "Force JSON output even in terminal").action(
2306
+ wrapAction(async (taskId, opts) => {
2307
+ const config = loadConfig();
2308
+ const message = await manageDependency(config, taskId, opts);
2309
+ if (shouldOutputJson(opts.json ?? false)) {
2310
+ console.log(JSON.stringify({ taskId, ...opts, message }, null, 2));
2311
+ } else {
2312
+ console.log(message);
2313
+ }
2314
+ })
2315
+ );
2193
2316
  var configCmd = program.command("config").description("Manage CLI configuration");
2194
2317
  configCmd.command("get <key>").description("Print a config value").action(
2195
2318
  wrapAction(async (key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,62 +34,66 @@ All commands support `--help` for full flag details.
34
34
 
35
35
  ### Read
36
36
 
37
- | Command | What it returns |
38
- | -------------------------------------------------------------------------- | -------------------------------------------------- |
39
- | `cu tasks [--status s] [--name q] [--list id] [--space id] [--json]` | My tasks (workspace-wide) |
40
- | `cu initiatives [--status s] [--name q] [--list id] [--space id] [--json]` | My initiatives |
41
- | `cu assigned [--include-closed] [--json]` | All my tasks grouped by status |
42
- | `cu sprint [--status s] [--space nameOrId] [--json]` | Tasks in active sprint (auto-detected) |
43
- | `cu sprints [--space nameOrId] [--json]` | List all sprints (marks active with \*) |
44
- | `cu search <query> [--status s] [--json]` | Search my tasks by name (multi-word, fuzzy status) |
45
- | `cu task <id> [--json]` | Single task details |
46
- | `cu subtasks <id> [--include-closed] [--json]` | Subtasks of a task or initiative |
47
- | `cu comments <id> [--json]` | Comments on a task |
48
- | `cu activity <id> [--json]` | Task details + comment history combined |
49
- | `cu inbox [--days n] [--json]` | Tasks updated in last n days (default 30) |
50
- | `cu summary [--hours n] [--json]` | Standup helper: completed, in-progress, overdue |
51
- | `cu overdue [--json]` | Tasks past their due date |
52
- | `cu spaces [--name partial] [--my] [--json]` | List/filter workspace spaces |
53
- | `cu lists <spaceId> [--name partial] [--json]` | Lists in a space (including folder lists) |
54
- | `cu open <query> [--json]` | Open task in browser by ID or name |
55
- | `cu auth [--json]` | Check authentication status |
37
+ | Command | What it returns |
38
+ | --------------------------------------------------------------------------------------------- | -------------------------------------------------- |
39
+ | `cu tasks [--status s] [--name q] [--list id] [--space id] [--include-closed] [--json]` | My tasks (workspace-wide) |
40
+ | `cu initiatives [--status s] [--name q] [--list id] [--space id] [--include-closed] [--json]` | My initiatives |
41
+ | `cu assigned [--status s] [--include-closed] [--json]` | All my tasks grouped by status |
42
+ | `cu sprint [--status s] [--space nameOrId] [--include-closed] [--json]` | Tasks in active sprint (auto-detected) |
43
+ | `cu sprints [--space nameOrId] [--json]` | List all sprints (marks active with \*) |
44
+ | `cu search <query> [--status s] [--include-closed] [--json]` | Search my tasks by name (multi-word, fuzzy status) |
45
+ | `cu task <id> [--json]` | Single task details |
46
+ | `cu subtasks <id> [--status s] [--name q] [--include-closed] [--json]` | Subtasks of a task or initiative |
47
+ | `cu comments <id> [--json]` | Comments on a task |
48
+ | `cu activity <id> [--json]` | Task details + comment history combined |
49
+ | `cu inbox [--days n] [--json]` | Tasks updated in last n days (default 30) |
50
+ | `cu summary [--hours n] [--json]` | Standup helper: completed, in-progress, overdue |
51
+ | `cu overdue [--json]` | Tasks past their due date |
52
+ | `cu spaces [--name partial] [--my] [--json]` | List/filter workspace spaces |
53
+ | `cu lists <spaceId> [--name partial] [--json]` | Lists in a space (including folder lists) |
54
+ | `cu open <query> [--json]` | Open task in browser by ID or name |
55
+ | `cu auth [--json]` | Check authentication status |
56
56
 
57
57
  ### Write
58
58
 
59
- | Command | What it does |
60
- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
61
- | `cu update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--assignee id] [--json]` | Update task fields |
62
- | `cu create -n name [-l listId] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--assignee id] [--tags t] [--json]` | Create task (list auto-detected from parent) |
63
- | `cu comment <id> -m text` | Post comment on task |
64
- | `cu assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users |
65
- | `cu config get <key>` / `cu config set <key> <value>` / `cu config path` | Manage CLI config |
66
- | `cu completion <shell>` | Shell completions (bash/zsh/fish) |
59
+ | Command | What it does |
60
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
61
+ | `cu update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id] [--json]` | Update task fields |
62
+ | `cu create -n name [-l listId] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id] [--tags t] [--custom-item-id n] [--json]` | Create task (list auto-detected from parent) |
63
+ | `cu comment <id> -m text` | Post comment on task |
64
+ | `cu assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users |
65
+ | `cu depend <id> [--on taskId] [--blocks taskId] [--remove] [--json]` | Add/remove task dependencies |
66
+ | `cu config get <key>` / `cu config set <key> <value>` / `cu config path` | Manage CLI config |
67
+ | `cu completion <shell>` | Shell completions (bash/zsh/fish) |
67
68
 
68
69
  ## Quick Reference
69
70
 
70
- | Topic | Detail |
71
- | ------------------- | --------------------------------------------------------------------------------- |
72
- | Task IDs | Stable alphanumeric strings (e.g. `abc123def`) |
73
- | Initiatives | Detected via `custom_item_id !== 0` |
74
- | `--list` on create | Optional when `--parent` is given (auto-detected) |
75
- | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
76
- | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
77
- | `--due-date` | `YYYY-MM-DD` format |
78
- | `--assignee` | Numeric user ID (find via `cu task <id> --json`) |
79
- | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
80
- | `--space` | Partial name match or exact ID |
81
- | `--name` | Partial match, case-insensitive |
82
- | `--include-closed` | Include closed/done tasks (on `subtasks` and `assigned`) |
83
- | `cu assign --to me` | Shorthand for your own user ID |
84
- | `cu search` | Matches all query words against task name, case-insensitive |
85
- | `cu sprint` | Auto-detects active sprint via view API and date range parsing |
86
- | `cu summary` | Categories: completed (done/complete/closed within N hours), in progress, overdue |
87
- | `cu overdue` | Excludes closed tasks, sorted most overdue first |
88
- | `cu open` | Tries task ID first, falls back to name search |
89
- | `cu task` | Shows custom fields in detail view |
90
- | `cu lists` | Discovers list IDs needed for `--list` and `cu create -l` |
91
- | Errors | stderr with exit code 1 |
92
- | Parsing | Strict - excess/unknown arguments rejected |
71
+ | Topic | Detail |
72
+ | ------------------- | ------------------------------------------------------------------------------------------------- |
73
+ | Task IDs | Stable alphanumeric strings (e.g. `abc123def`) |
74
+ | Initiatives | Detected via `custom_item_id !== 0` |
75
+ | `--list` on create | Optional when `--parent` is given (auto-detected) |
76
+ | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
77
+ | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
78
+ | `--due-date` | `YYYY-MM-DD` format |
79
+ | `--assignee` | Numeric user ID (find via `cu task <id> --json`) |
80
+ | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
81
+ | `--time-estimate` | Duration format: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
82
+ | `--custom-item-id` | Custom task type ID (e.g. `1` for initiative) |
83
+ | `--on` / `--blocks` | Task dependency direction (used with `cu depend`) |
84
+ | `--space` | Partial name match or exact ID |
85
+ | `--name` | Partial match, case-insensitive |
86
+ | `--include-closed` | Include closed/done tasks (on `tasks`, `initiatives`, `assigned`, `subtasks`, `sprint`, `search`) |
87
+ | `cu assign --to me` | Shorthand for your own user ID |
88
+ | `cu search` | Matches all query words against task name, case-insensitive |
89
+ | `cu sprint` | Auto-detects active sprint via view API and date range parsing |
90
+ | `cu summary` | Categories: completed (done/complete/closed within N hours), in progress, overdue |
91
+ | `cu overdue` | Excludes closed tasks, sorted most overdue first |
92
+ | `cu open` | Tries task ID first, falls back to name search |
93
+ | `cu task` | Shows custom fields in detail view |
94
+ | `cu lists` | Discovers list IDs needed for `--list` and `cu create -l` |
95
+ | Errors | stderr with exit code 1 |
96
+ | Parsing | Strict - excess/unknown arguments rejected |
93
97
 
94
98
  ## Agent Workflow Examples
95
99
 
@@ -121,10 +125,14 @@ cu inbox --days 7 # recently updated
121
125
  ```bash
122
126
  cu update abc123def -s "done"
123
127
  cu update abc123def --priority high --due-date 2025-03-15
128
+ cu update abc123def --time-estimate 2h
124
129
  cu create -n "Fix the thing" -p abc123def
125
130
  cu create -n "Fix bug" -l <listId> --priority urgent --tags "bug,frontend"
131
+ cu create -n "Q3 Roadmap" -l <listId> --custom-item-id 1 # create initiative
126
132
  cu comment abc123def -m "Completed in PR #42"
127
133
  cu assign abc123def --to me
134
+ cu depend task3 --on task2 # task3 waits for task2
135
+ cu depend task1 --blocks task2 # task1 blocks task2
128
136
  ```
129
137
 
130
138
  ### Discover workspace structure