@krodak/clickup-cli 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # cu - ClickUp CLI
2
2
 
3
- A ClickUp CLI built for AI agents that also works well for humans. Outputs JSON when piped, interactive tables when run in a terminal.
3
+ > A ClickUp CLI built for AI agents that also works well for humans. Outputs Markdown when piped (optimized for AI context windows), interactive tables when run in a terminal.
4
4
 
5
- ## Quick start
5
+ [![npm](https://img.shields.io/npm/v/@krodak/clickup-cli)](https://www.npmjs.com/package/@krodak/clickup-cli)
6
+ [![node](https://img.shields.io/node/v/@krodak/clickup-cli)](https://nodejs.org)
7
+ [![license](https://img.shields.io/npm/l/@krodak/clickup-cli)](./LICENSE)
8
+ [![CI](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml)
6
9
 
7
10
  ```bash
8
11
  npm install -g @krodak/clickup-cli # or: brew tap krodak/tap && brew install clickup-cli
9
12
  cu init # walks you through API token + workspace setup
10
13
  ```
11
14
 
12
- You need Node 22+ and a ClickUp personal API token (`pk_...` from https://app.clickup.com/settings/apps).
15
+ You need a ClickUp personal API token (`pk_...` from https://app.clickup.com/settings/apps).
13
16
 
14
17
  ## Using with AI agents
15
18
 
@@ -89,9 +92,9 @@ cu update abc123 -s "done" # update status
89
92
  cu assign abc123 --to me # assign yourself
90
93
  ```
91
94
 
92
- Pass `--json` to any command to get raw JSON output instead of the interactive UI.
95
+ Pass `--json` to any read command to force JSON output instead of the default format.
93
96
 
94
- When output is piped (no TTY), all commands automatically output JSON. All commands support `--json` to force JSON output even in a terminal.
97
+ When output is piped (no TTY), all commands output **Markdown** by default - optimized for AI agent context windows. Pass `--json` to any command for JSON output. Set `CU_OUTPUT=json` environment variable to always get JSON when piped.
95
98
 
96
99
  ## Commands
97
100
 
@@ -179,12 +182,15 @@ cu task abc123
179
182
  cu task abc123 --json
180
183
  ```
181
184
 
185
+ **Note:** When piped, `cu task` outputs a structured Markdown summary of the task. For the full raw API response with all fields (custom fields, checklists, etc.), use `--json`.
186
+
182
187
  ### `cu subtasks <id>`
183
188
 
184
189
  List subtasks of a task or initiative.
185
190
 
186
191
  ```bash
187
192
  cu subtasks abc123
193
+ cu subtasks abc123 --include-closed
188
194
  cu subtasks abc123 --json
189
195
  ```
190
196
 
@@ -410,10 +416,11 @@ cu completion fish > ~/.config/fish/completions/cu.fish # Fish
410
416
 
411
417
  Environment variables override config file values:
412
418
 
413
- | Variable | Description |
414
- | -------------- | ---------------------------------- |
415
- | `CU_API_TOKEN` | ClickUp personal API token (`pk_`) |
416
- | `CU_TEAM_ID` | Workspace (team) ID |
419
+ | Variable | Description |
420
+ | -------------- | ----------------------------------------------------------------- |
421
+ | `CU_API_TOKEN` | ClickUp personal API token (`pk_`) |
422
+ | `CU_TEAM_ID` | Workspace (team) ID |
423
+ | `CU_OUTPUT` | Set to `json` to force JSON output when piped (default: markdown) |
417
424
 
418
425
  When both are set, the config file is not required. Useful for CI/CD and containerized agents.
419
426
 
package/dist/index.js CHANGED
@@ -135,6 +135,7 @@ var ClickUpClient = class {
135
135
  const baseParams = new URLSearchParams({
136
136
  subtasks: String(filters.subtasks ?? true)
137
137
  });
138
+ if (filters.includeClosed) baseParams.set("include_closed", "true");
138
139
  baseParams.append("assignees[]", String(me.id));
139
140
  for (const s of filters.statuses ?? []) baseParams.append("statuses[]", s);
140
141
  for (const id of filters.listIds ?? []) baseParams.append("list_ids[]", id);
@@ -161,9 +162,11 @@ var ClickUpClient = class {
161
162
  const data = await this.request(`/task/${taskId}/comment`);
162
163
  return data.comments ?? [];
163
164
  }
164
- async getTasksFromList(listId, params = {}) {
165
+ async getTasksFromList(listId, params = {}, options = {}) {
165
166
  return this.paginate((page) => {
166
- const qs = new URLSearchParams({ subtasks: "true", page: String(page), ...params }).toString();
167
+ const base = { subtasks: "true", page: String(page), ...params };
168
+ if (options.includeClosed) base["include_closed"] = "true";
169
+ const qs = new URLSearchParams(base).toString();
167
170
  return `/list/${listId}/task?${qs}`;
168
171
  });
169
172
  }
@@ -227,6 +230,11 @@ import chalk from "chalk";
227
230
  function isTTY() {
228
231
  return Boolean(process.stdout.isTTY);
229
232
  }
233
+ function shouldOutputJson(forceJson) {
234
+ if (forceJson) return true;
235
+ if (process.env["CU_OUTPUT"] === "json") return true;
236
+ return false;
237
+ }
230
238
  function cell(value, width) {
231
239
  if (value.length > width) return value.slice(0, width - 1) + "\u2026";
232
240
  return value.padEnd(width);
@@ -259,6 +267,128 @@ var TASK_COLUMNS = [
259
267
  { key: "list", label: "LIST" }
260
268
  ];
261
269
 
270
+ // src/markdown.ts
271
+ function escapeCell(value) {
272
+ return value.replace(/\|/g, "\\|");
273
+ }
274
+ function formatMarkdownTable(rows, columns) {
275
+ const header = "| " + columns.map((c) => c.label).join(" | ") + " |";
276
+ const divider = "| " + columns.map(() => "---").join(" | ") + " |";
277
+ const lines = [header, divider];
278
+ for (const row of rows) {
279
+ const cells = columns.map((c) => escapeCell(String(row[c.key] ?? "")));
280
+ lines.push("| " + cells.join(" | ") + " |");
281
+ }
282
+ return lines.join("\n");
283
+ }
284
+ var TASK_MD_COLUMNS = [
285
+ { key: "id", label: "ID" },
286
+ { key: "name", label: "Name" },
287
+ { key: "status", label: "Status" },
288
+ { key: "list", label: "List" }
289
+ ];
290
+ function formatTasksMarkdown(tasks) {
291
+ if (tasks.length === 0) return "No tasks found.";
292
+ return formatMarkdownTable(tasks, TASK_MD_COLUMNS);
293
+ }
294
+ function formatCommentsMarkdown(comments) {
295
+ if (comments.length === 0) return "No comments found.";
296
+ return comments.map((c) => `**${c.user}** (${c.date})
297
+
298
+ ${c.text}`).join("\n\n---\n\n");
299
+ }
300
+ var LIST_MD_COLUMNS = [
301
+ { key: "id", label: "ID" },
302
+ { key: "name", label: "Name" },
303
+ { key: "folder", label: "Folder" }
304
+ ];
305
+ function formatListsMarkdown(lists) {
306
+ if (lists.length === 0) return "No lists found.";
307
+ return formatMarkdownTable(lists, LIST_MD_COLUMNS);
308
+ }
309
+ var SPACE_MD_COLUMNS = [
310
+ { key: "id", label: "ID" },
311
+ { key: "name", label: "Name" }
312
+ ];
313
+ function formatSpacesMarkdown(spaces) {
314
+ if (spaces.length === 0) return "No spaces found.";
315
+ return formatMarkdownTable(spaces, SPACE_MD_COLUMNS);
316
+ }
317
+ function formatGroupedTasksMarkdown(groups) {
318
+ const sections = groups.filter((g) => g.tasks.length > 0).map((g) => `## ${g.label}
319
+
320
+ ${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
321
+ if (sections.length === 0) return "No tasks found.";
322
+ return sections.join("\n\n");
323
+ }
324
+ function formatDate(ms) {
325
+ const d = new Date(Number(ms));
326
+ const year = d.getUTCFullYear();
327
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
328
+ const day = String(d.getUTCDate()).padStart(2, "0");
329
+ return `${year}-${month}-${day}`;
330
+ }
331
+ function formatDuration(ms) {
332
+ const totalMinutes = Math.floor(ms / 6e4);
333
+ const hours = Math.floor(totalMinutes / 60);
334
+ const minutes = totalMinutes % 60;
335
+ return `${hours}h ${minutes}m`;
336
+ }
337
+ function formatTaskDetailMarkdown(task) {
338
+ const lines = [`# ${task.name}`, ""];
339
+ const isInitiative2 = (task.custom_item_id ?? 0) !== 0;
340
+ const fields = [
341
+ ["ID", task.id],
342
+ ["Status", task.status.status],
343
+ ["Type", isInitiative2 ? "initiative" : "task"],
344
+ ["List", task.list.name],
345
+ ["URL", task.url],
346
+ [
347
+ "Assignees",
348
+ task.assignees.length > 0 ? task.assignees.map((a) => a.username).join(", ") : void 0
349
+ ],
350
+ ["Priority", task.priority?.priority],
351
+ ["Parent", task.parent ?? void 0],
352
+ ["Start Date", task.start_date ? formatDate(task.start_date) : void 0],
353
+ ["Due Date", task.due_date ? formatDate(task.due_date) : void 0],
354
+ [
355
+ "Time Estimate",
356
+ task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
357
+ ],
358
+ [
359
+ "Time Spent",
360
+ task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
361
+ ],
362
+ ["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
363
+ ["Created", task.date_created ? formatDate(task.date_created) : void 0],
364
+ ["Updated", task.date_updated ? formatDate(task.date_updated) : void 0]
365
+ ];
366
+ for (const [label, value] of fields) {
367
+ if (value != null && value !== "") {
368
+ lines.push(`**${label}:** ${value}`);
369
+ }
370
+ }
371
+ if (task.description) {
372
+ lines.push("", "## Description", "", task.description);
373
+ }
374
+ return lines.join("\n");
375
+ }
376
+ function formatUpdateConfirmation(id, name) {
377
+ return `Updated task ${id}: "${name}"`;
378
+ }
379
+ function formatCreateConfirmation(id, name, url) {
380
+ return `Created task ${id}: "${name}" - ${url}`;
381
+ }
382
+ function formatCommentConfirmation(id) {
383
+ return `Comment posted (id: ${id})`;
384
+ }
385
+ function formatAssignConfirmation(taskId, opts) {
386
+ const parts = [];
387
+ if (opts.to) parts.push(`Assigned ${opts.to} to ${taskId}`);
388
+ if (opts.remove) parts.push(`Removed ${opts.remove} from ${taskId}`);
389
+ return parts.join("; ");
390
+ }
391
+
262
392
  // src/interactive.ts
263
393
  import { execFileSync } from "child_process";
264
394
  import { checkbox, confirm, Separator } from "@inquirer/prompts";
@@ -490,10 +620,14 @@ async function fetchMyTasks(config, opts = {}) {
490
620
  return filtered.map(summarize);
491
621
  }
492
622
  async function printTasks(tasks, forceJson, config) {
493
- if (forceJson || !isTTY()) {
623
+ if (shouldOutputJson(forceJson)) {
494
624
  console.log(JSON.stringify(tasks, null, 2));
495
625
  return;
496
626
  }
627
+ if (!isTTY()) {
628
+ console.log(formatTasksMarkdown(tasks));
629
+ return;
630
+ }
497
631
  if (tasks.length === 0) {
498
632
  console.log("No tasks found.");
499
633
  return;
@@ -771,7 +905,7 @@ var SPRINT_COLUMNS = [
771
905
  { key: "sprint", label: "SPRINT", maxWidth: 60 },
772
906
  { key: "dates", label: "DATES" }
773
907
  ];
774
- function formatDate(d) {
908
+ function formatDate2(d) {
775
909
  return `${d.getMonth() + 1}/${d.getDate()}`;
776
910
  }
777
911
  function buildSprintInfos(lists, folderName, today) {
@@ -832,7 +966,7 @@ async function listSprints(config, opts = {}) {
832
966
  }
833
967
  const rows = allSprints.map((s) => {
834
968
  const dates = parseSprintDates(s.name);
835
- const dateStr = dates ? `${formatDate(dates.start)} - ${formatDate(dates.end)}` : "";
969
+ const dateStr = dates ? `${formatDate2(dates.start)} - ${formatDate2(dates.end)}` : "";
836
970
  return {
837
971
  id: s.id,
838
972
  sprint: s.active ? `* ${s.name}` : s.name,
@@ -843,10 +977,14 @@ async function listSprints(config, opts = {}) {
843
977
  }
844
978
 
845
979
  // src/commands/subtasks.ts
846
- async function fetchSubtasks(config, taskId) {
980
+ async function fetchSubtasks(config, taskId, options = {}) {
847
981
  const client = new ClickUpClient(config);
848
982
  const parent = await client.getTask(taskId);
849
- const tasks = await client.getTasksFromList(parent.list.id, { parent: taskId, subtasks: "false" });
983
+ const tasks = await client.getTasksFromList(
984
+ parent.list.id,
985
+ { parent: taskId, subtasks: "false" },
986
+ { includeClosed: options.includeClosed }
987
+ );
850
988
  return tasks.map(summarize);
851
989
  }
852
990
 
@@ -859,7 +997,7 @@ async function postComment(config, taskId, text) {
859
997
 
860
998
  // src/commands/comments.ts
861
999
  import chalk3 from "chalk";
862
- function formatDate2(timestamp) {
1000
+ function formatDate3(timestamp) {
863
1001
  return new Date(Number(timestamp)).toLocaleString("en-US", {
864
1002
  month: "short",
865
1003
  day: "numeric",
@@ -879,10 +1017,14 @@ async function fetchComments(config, taskId) {
879
1017
  }));
880
1018
  }
881
1019
  function printComments(comments, forceJson) {
882
- if (forceJson || !isTTY()) {
1020
+ if (shouldOutputJson(forceJson)) {
883
1021
  console.log(JSON.stringify(comments, null, 2));
884
1022
  return;
885
1023
  }
1024
+ if (!isTTY()) {
1025
+ console.log(formatCommentsMarkdown(comments));
1026
+ return;
1027
+ }
886
1028
  if (comments.length === 0) {
887
1029
  console.log("No comments found.");
888
1030
  return;
@@ -891,7 +1033,7 @@ function printComments(comments, forceJson) {
891
1033
  for (let i = 0; i < comments.length; i++) {
892
1034
  const c = comments[i];
893
1035
  if (i > 0) console.log(separator);
894
- console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate2(c.date))}`);
1036
+ console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate3(c.date))}`);
895
1037
  console.log(c.text);
896
1038
  if (i < comments.length - 1) console.log("");
897
1039
  }
@@ -920,10 +1062,14 @@ async function fetchLists(config, spaceId, opts = {}) {
920
1062
  return results;
921
1063
  }
922
1064
  function printLists(lists, forceJson) {
923
- if (forceJson || !isTTY()) {
1065
+ if (shouldOutputJson(forceJson)) {
924
1066
  console.log(JSON.stringify(lists, null, 2));
925
1067
  return;
926
1068
  }
1069
+ if (!isTTY()) {
1070
+ console.log(formatListsMarkdown(lists));
1071
+ return;
1072
+ }
927
1073
  if (lists.length === 0) {
928
1074
  console.log("No lists found.");
929
1075
  return;
@@ -991,7 +1137,7 @@ async function fetchInbox(config, days = 30) {
991
1137
  async function printInbox(tasks, forceJson, config) {
992
1138
  const now = Date.now();
993
1139
  const groups = groupTasks(tasks, now);
994
- if (forceJson || !isTTY()) {
1140
+ if (shouldOutputJson(forceJson)) {
995
1141
  const jsonGroups = {};
996
1142
  for (const { key } of TIME_PERIODS) {
997
1143
  if (groups[key].length > 0) {
@@ -1001,6 +1147,14 @@ async function printInbox(tasks, forceJson, config) {
1001
1147
  console.log(JSON.stringify(jsonGroups, null, 2));
1002
1148
  return;
1003
1149
  }
1150
+ if (!isTTY()) {
1151
+ const mdGroups = TIME_PERIODS.filter((p) => groups[p.key].length > 0).map((p) => ({
1152
+ label: p.label,
1153
+ tasks: groups[p.key]
1154
+ }));
1155
+ console.log(formatGroupedTasksMarkdown(mdGroups));
1156
+ return;
1157
+ }
1004
1158
  if (tasks.length === 0) {
1005
1159
  console.log("No recently updated tasks.");
1006
1160
  return;
@@ -1032,7 +1186,11 @@ async function listSpaces(config, opts) {
1032
1186
  );
1033
1187
  spaces = spaces.filter((s) => mySpaceIds.has(s.id));
1034
1188
  }
1035
- if (!opts.json && isTTY()) {
1189
+ if (shouldOutputJson(opts.json ?? false)) {
1190
+ console.log(JSON.stringify(spaces, null, 2));
1191
+ } else if (!isTTY()) {
1192
+ console.log(formatSpacesMarkdown(spaces.map((s) => ({ id: s.id, name: s.name }))));
1193
+ } else {
1036
1194
  const table = formatTable(
1037
1195
  spaces.map((s) => ({ id: s.id, name: s.name })),
1038
1196
  [
@@ -1041,8 +1199,6 @@ async function listSpaces(config, opts) {
1041
1199
  ]
1042
1200
  );
1043
1201
  console.log(table);
1044
- } else {
1045
- console.log(JSON.stringify(spaces, null, 2));
1046
1202
  }
1047
1203
  }
1048
1204
 
@@ -1095,9 +1251,11 @@ function groupByStatus(tasks, includeClosed) {
1095
1251
  }
1096
1252
  async function runAssignedCommand(config, opts) {
1097
1253
  const client = new ClickUpClient(config);
1098
- const allTasks = await client.getMyTasks(config.teamId);
1254
+ const allTasks = await client.getMyTasks(config.teamId, {
1255
+ includeClosed: opts.includeClosed
1256
+ });
1099
1257
  const groups = groupByStatus(allTasks, opts.includeClosed ?? false);
1100
- if (opts.json || !isTTY()) {
1258
+ if (shouldOutputJson(opts.json ?? false)) {
1101
1259
  const result = {};
1102
1260
  for (const group of groups) {
1103
1261
  result[group.status.toLowerCase()] = group.tasks.map((t) => toJsonTask(t, summarize(t)));
@@ -1105,6 +1263,14 @@ async function runAssignedCommand(config, opts) {
1105
1263
  console.log(JSON.stringify(result, null, 2));
1106
1264
  return;
1107
1265
  }
1266
+ if (!isTTY()) {
1267
+ const mdGroups = groups.map((g) => ({
1268
+ label: g.status,
1269
+ tasks: g.tasks.map((t) => summarize(t))
1270
+ }));
1271
+ console.log(formatGroupedTasksMarkdown(mdGroups));
1272
+ return;
1273
+ }
1108
1274
  if (groups.length === 0) {
1109
1275
  console.log("No tasks found.");
1110
1276
  return;
@@ -1130,13 +1296,13 @@ async function openTask(config, query, opts = {}) {
1130
1296
  } catch {
1131
1297
  }
1132
1298
  if (task) {
1133
- if (opts.json) {
1299
+ if (shouldOutputJson(opts.json ?? false)) {
1134
1300
  console.log(JSON.stringify(task, null, 2));
1301
+ } else if (!isTTY()) {
1302
+ console.log(formatTaskDetailMarkdown(task));
1135
1303
  } else {
1136
- if (isTTY()) {
1137
- console.log(task.name);
1138
- console.log(task.url);
1139
- }
1304
+ console.log(task.name);
1305
+ console.log(task.url);
1140
1306
  openUrl(task.url);
1141
1307
  }
1142
1308
  return task;
@@ -1154,15 +1320,18 @@ async function openTask(config, query, opts = {}) {
1154
1320
  }
1155
1321
  console.log("Opening first match...");
1156
1322
  }
1157
- if (opts.json) {
1323
+ if (shouldOutputJson(opts.json ?? false)) {
1158
1324
  const fullTask = await client.getTask(first.id);
1159
1325
  console.log(JSON.stringify(fullTask, null, 2));
1160
1326
  return fullTask;
1161
1327
  }
1162
- if (isTTY()) {
1163
- console.log(first.name);
1164
- console.log(first.url);
1328
+ if (!isTTY()) {
1329
+ const fullTask = await client.getTask(first.id);
1330
+ console.log(formatTaskDetailMarkdown(fullTask));
1331
+ return fullTask;
1165
1332
  }
1333
+ console.log(first.name);
1334
+ console.log(first.url);
1166
1335
  openUrl(first.url);
1167
1336
  return {
1168
1337
  id: first.id,
@@ -1224,10 +1393,19 @@ async function runSummaryCommand(config, opts) {
1224
1393
  const client = new ClickUpClient(config);
1225
1394
  const allTasks = await client.getMyTasks(config.teamId);
1226
1395
  const result = categorizeTasks(allTasks, opts.hours);
1227
- if (opts.json || !isTTY()) {
1396
+ if (shouldOutputJson(opts.json)) {
1228
1397
  console.log(JSON.stringify(result, null, 2));
1229
1398
  return;
1230
1399
  }
1400
+ if (!isTTY()) {
1401
+ const mdGroups = [
1402
+ { label: "Completed Recently", tasks: result.completed },
1403
+ { label: "In Progress", tasks: result.inProgress },
1404
+ { label: "Overdue", tasks: result.overdue }
1405
+ ];
1406
+ console.log(formatGroupedTasksMarkdown(mdGroups));
1407
+ return;
1408
+ }
1231
1409
  printSection("Completed Recently", result.completed);
1232
1410
  printSection("In Progress", result.inProgress);
1233
1411
  printSection("Overdue", result.overdue);
@@ -1309,7 +1487,7 @@ async function assignTask(config, taskId, opts) {
1309
1487
 
1310
1488
  // src/commands/activity.ts
1311
1489
  import chalk4 from "chalk";
1312
- function formatDate3(timestamp) {
1490
+ function formatDate4(timestamp) {
1313
1491
  return new Date(Number(timestamp)).toLocaleString("en-US", {
1314
1492
  month: "short",
1315
1493
  day: "numeric",
@@ -1351,7 +1529,7 @@ function printActivity(result, forceJson) {
1351
1529
  console.log("");
1352
1530
  console.log(chalk4.dim("-".repeat(60)));
1353
1531
  }
1354
- console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatDate3(c.date))}`);
1532
+ console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatDate4(c.date))}`);
1355
1533
  console.log(c.text);
1356
1534
  }
1357
1535
  }
@@ -1863,8 +2041,10 @@ program.command("task <taskId>").description("Get task details").option("--json"
1863
2041
  wrapAction(async (taskId, opts) => {
1864
2042
  const config = loadConfig();
1865
2043
  const result = await getTask(config, taskId);
1866
- if (opts.json || !isTTY()) {
2044
+ if (shouldOutputJson(opts.json ?? false)) {
1867
2045
  console.log(JSON.stringify(result, null, 2));
2046
+ } else if (!isTTY()) {
2047
+ console.log(formatTaskDetailMarkdown(result));
1868
2048
  } else {
1869
2049
  console.log(formatTaskDetail(result));
1870
2050
  }
@@ -1875,10 +2055,10 @@ program.command("update <taskId>").description("Update a task").option("-n, --na
1875
2055
  const config = loadConfig();
1876
2056
  const payload = buildUpdatePayload(opts);
1877
2057
  const result = await updateTask(config, taskId, payload);
1878
- if (opts.json || !isTTY()) {
2058
+ if (shouldOutputJson(false)) {
1879
2059
  console.log(JSON.stringify(result, null, 2));
1880
2060
  } else {
1881
- console.log(`Updated task ${result.id}: "${result.name}"`);
2061
+ console.log(formatUpdateConfirmation(result.id, result.name));
1882
2062
  }
1883
2063
  })
1884
2064
  );
@@ -1886,10 +2066,10 @@ program.command("create").description("Create a new task").option("-l, --list <l
1886
2066
  wrapAction(async (opts) => {
1887
2067
  const config = loadConfig();
1888
2068
  const result = await createTask(config, opts);
1889
- if (opts.json || !isTTY()) {
2069
+ if (shouldOutputJson(false)) {
1890
2070
  console.log(JSON.stringify(result, null, 2));
1891
2071
  } else {
1892
- console.log(`Created task ${result.id}: "${result.name}" - ${result.url}`);
2072
+ console.log(formatCreateConfirmation(result.id, result.name, result.url));
1893
2073
  }
1894
2074
  })
1895
2075
  );
@@ -1905,10 +2085,10 @@ program.command("sprints").description("List all sprints in sprint folders").opt
1905
2085
  await listSprints(config, opts);
1906
2086
  })
1907
2087
  );
1908
- program.command("subtasks <taskId>").description("List subtasks of a task or initiative").option("--json", "Force JSON output even in terminal").action(
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(
1909
2089
  wrapAction(async (taskId, opts) => {
1910
2090
  const config = loadConfig();
1911
- const tasks = await fetchSubtasks(config, taskId);
2091
+ const tasks = await fetchSubtasks(config, taskId, { includeClosed: opts.includeClosed });
1912
2092
  await printTasks(tasks, opts.json ?? false, config);
1913
2093
  })
1914
2094
  );
@@ -1916,10 +2096,10 @@ program.command("comment <taskId>").description("Post a comment on a task").requ
1916
2096
  wrapAction(async (taskId, opts) => {
1917
2097
  const config = loadConfig();
1918
2098
  const result = await postComment(config, taskId, opts.message);
1919
- if (isTTY()) {
1920
- console.log(`Comment posted (id: ${result.id})`);
1921
- } else {
2099
+ if (shouldOutputJson(false)) {
1922
2100
  console.log(JSON.stringify(result, null, 2));
2101
+ } else {
2102
+ console.log(formatCommentConfirmation(result.id));
1923
2103
  }
1924
2104
  })
1925
2105
  );
@@ -2003,13 +2183,10 @@ program.command("assign <taskId>").description("Assign or unassign users from a
2003
2183
  wrapAction(async (taskId, opts) => {
2004
2184
  const config = loadConfig();
2005
2185
  const result = await assignTask(config, taskId, opts);
2006
- if (opts.json || !isTTY()) {
2186
+ if (shouldOutputJson(opts.json ?? false)) {
2007
2187
  console.log(JSON.stringify(result, null, 2));
2008
2188
  } else {
2009
- const parts = [];
2010
- if (opts.to) parts.push(`Assigned ${opts.to} to task ${taskId}`);
2011
- if (opts.remove) parts.push(`Removed ${opts.remove} from task ${taskId}`);
2012
- console.log(parts.join("; "));
2189
+ console.log(formatAssignConfirmation(taskId, { to: opts.to, remove: opts.remove }));
2013
2190
  }
2014
2191
  })
2015
2192
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,19 +1,32 @@
1
1
  ---
2
2
  name: clickup
3
- description: 'Use when managing ClickUp tasks, initiatives, sprints, or comments via the `cu` CLI tool. Covers task queries, status updates, sprint tracking, and project management workflows.'
3
+ description: 'Use when managing ClickUp tasks, initiatives, sprints, or comments via the `cu` CLI tool. Triggers: task queries, status updates, sprint tracking, creating subtasks, posting comments, standup summaries, searching tasks, checking overdue items, assigning tasks, listing spaces and lists, opening tasks in browser, checking auth or config.'
4
4
  ---
5
5
 
6
6
  # ClickUp CLI (`cu`)
7
7
 
8
- Reference for AI agents using the `cu` command-line tool to interact with ClickUp. Covers task management, sprint tracking, initiatives, and comments.
8
+ Reference for AI agents using the `cu` CLI tool. Covers task management, sprint tracking, initiatives, comments, and project workflows.
9
9
 
10
- Keywords: ClickUp, task management, sprint, initiative, project management, agile, backlog, subtasks
10
+ Keywords: ClickUp, task management, sprint, initiative, project management, agile, backlog, subtasks, standup, overdue, search
11
11
 
12
12
  ## Setup
13
13
 
14
- Config at `~/.config/cu/config.json` (or `$XDG_CONFIG_HOME/cu/config.json`) with `apiToken` and `teamId`. Run `cu init` to set up.
14
+ Config at `~/.config/cu/config.json` with `apiToken` and `teamId`. Run `cu init` to set up interactively.
15
15
 
16
- Alternatively, set `CU_API_TOKEN` and `CU_TEAM_ID` environment variables (overrides config file, no file needed when both are set).
16
+ Environment variables `CU_API_TOKEN` and `CU_TEAM_ID` override config file when both are set.
17
+
18
+ ## Output Modes
19
+
20
+ | Context | Default output | Override |
21
+ | --------------- | --------------------- | ----------------- |
22
+ | Terminal (TTY) | Interactive picker UI | `--json` for JSON |
23
+ | Piped / non-TTY | Markdown tables | `--json` for JSON |
24
+
25
+ - Default piped output is **Markdown** - optimized for agent context windows
26
+ - `cu task <id>` outputs a Markdown summary when piped; use `--json` for the full raw API object (custom fields, checklists, etc.)
27
+ - Set `CU_OUTPUT=json` to always get JSON when piped
28
+ - Set `NO_COLOR` to disable color (tables still render, just uncolored)
29
+ - Agents typically don't need `--json` unless parsing structured data with `jq`
17
30
 
18
31
  ## Commands
19
32
 
@@ -27,19 +40,19 @@ All commands support `--help` for full flag details.
27
40
  | `cu initiatives [--status s] [--name q] [--list id] [--space id] [--json]` | My initiatives |
28
41
  | `cu assigned [--include-closed] [--json]` | All my tasks grouped by status |
29
42
  | `cu sprint [--status s] [--space nameOrId] [--json]` | Tasks in active sprint (auto-detected) |
30
- | `cu inbox [--days n] [--json]` | Tasks updated in last n days (default 30) |
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) |
31
45
  | `cu task <id> [--json]` | Single task details |
32
- | `cu subtasks <id> [--json]` | Subtasks of a task or initiative |
33
- | `cu comments <id> [--json]` | List comments on a task |
34
- | `cu spaces [--name partial] [--my] [--json]` | List/filter workspace spaces |
35
- | `cu lists <spaceId> [--name partial] [--json]` | List all lists in a space (including folder lists) |
36
- | `cu open <query> [--json]` | Open task in browser by ID or name |
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) |
37
50
  | `cu summary [--hours n] [--json]` | Standup helper: completed, in-progress, overdue |
38
51
  | `cu overdue [--json]` | Tasks past their due date |
39
- | `cu search <query> [--status s] [--json]` | Search my tasks by name (multi-word, fuzzy status) |
40
- | `cu activity <id> [--json]` | Task details + comment history combined |
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 |
41
55
  | `cu auth [--json]` | Check authentication status |
42
- | `cu sprints [--space nameOrId] [--json]` | List all sprints (marks active with \*) |
43
56
 
44
57
  ### Write
45
58
 
@@ -48,102 +61,85 @@ All commands support `--help` for full flag details.
48
61
  | `cu update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--assignee id] [--json]` | Update task fields |
49
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) |
50
63
  | `cu comment <id> -m text` | Post comment on task |
51
- | `cu assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users from a task |
64
+ | `cu assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users |
52
65
  | `cu config get <key>` / `cu config set <key> <value>` / `cu config path` | Manage CLI config |
53
- | `cu completion <shell>` | Output shell completions (bash/zsh/fish) |
54
-
55
- ## Output modes
56
-
57
- - **TTY (terminal)**: Interactive picker UI. Use `--json` to bypass.
58
- - **Piped / non-TTY**: Always JSON. This is what agents get by default.
59
- - **Agents should always pass `--json`** to guarantee machine-readable output.
60
- - Set `NO_COLOR` to disable color output (tables still render, just without color).
61
-
62
- ## Key facts
63
-
64
- - All task IDs are stable alphanumeric strings (e.g. `abc123def`)
65
- - Initiatives are detected via `custom_item_id !== 0` (not `task_type`)
66
- - `--list` is optional in `cu create` when `--parent` is given (list auto-detected from parent)
67
- - `cu sprint` auto-detects active sprint from spaces where user has tasks, using view API and date range parsing from sprint names like "Acme Sprint 4 (3/1 - 3/14)"
68
- - `--name` on tasks/initiatives filters by partial name match (case-insensitive)
69
- - `--space` on sprint/tasks accepts partial name match (e.g. `--space Acm`)
70
- - `--priority` accepts names (`urgent`, `high`, `normal`, `low`) or numbers (1-4)
71
- - `--due-date` accepts `YYYY-MM-DD` format
72
- - `--assignee` takes a numeric user ID (use `cu task <id> --json` to find assignee IDs)
73
- - `cu assign` supports `me` as a shorthand for your own user ID
74
- - `cu open` tries task ID lookup first, then falls back to name search
75
- - `cu summary` categories: completed (done/complete/closed within N hours), in progress, overdue
76
- - `cu overdue` excludes done/complete/closed tasks, sorted most overdue first
77
- - `--tags` accepts comma-separated tag names (e.g. `--tags "bug,frontend"`)
78
- - `cu lists <spaceId>` discovers list IDs needed for `--list` and `cu create -l`
79
- - Strict argument parsing - excess/unknown arguments are rejected
80
- - `cu update -s` supports fuzzy status matching (exact > starts-with > contains). Prints matched status to stderr when fuzzy-resolved.
81
- - `cu task` shows custom fields in detail view (read-only)
82
- - `cu search` matches all query words against task name (case-insensitive). `--status` supports fuzzy matching.
83
- - Errors go to stderr with exit code 1
84
-
85
- ## Agent workflow example
66
+ | `cu completion <shell>` | Shell completions (bash/zsh/fish) |
67
+
68
+ ## Quick Reference
69
+
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 |
93
+
94
+ ## Agent Workflow Examples
95
+
96
+ ### Investigate a task
86
97
 
87
98
  ```bash
88
- # List my in-progress tasks
89
- cu tasks --status "in progress" --json
90
-
91
- # Find tasks by partial name
92
- cu tasks --name "login" --json
93
-
94
- # Check current sprint
95
- cu sprint --json | jq '.[].name'
99
+ cu task abc123def # markdown summary
100
+ cu subtasks abc123def # child tasks (open only)
101
+ cu subtasks abc123def --include-closed # all child tasks
102
+ cu comments abc123def # discussion
103
+ cu activity abc123def # task + comments combined
104
+ ```
96
105
 
97
- # Get full details on a task
98
- cu task abc123def --json
106
+ ### Find tasks
99
107
 
100
- # List subtasks of an initiative
101
- cu subtasks abc123def --json
108
+ ```bash
109
+ cu tasks --status "in progress" # by status
110
+ cu tasks --name "login" # by partial name
111
+ cu search "payment flow" # multi-word search
112
+ cu search auth --status "prog" # fuzzy status match
113
+ cu sprint # current sprint
114
+ cu assigned # all my tasks by status
115
+ cu overdue # past due date
116
+ cu inbox --days 7 # recently updated
117
+ ```
102
118
 
103
- # Read comments on a task
104
- cu comments abc123def --json
119
+ ### Make changes
105
120
 
106
- # Update task status
121
+ ```bash
107
122
  cu update abc123def -s "done"
108
-
109
- # Update priority and due date
110
123
  cu update abc123def --priority high --due-date 2025-03-15
111
-
112
- # Create subtask under initiative (no --list needed)
113
124
  cu create -n "Fix the thing" -p abc123def
114
-
115
- # Create task with priority and tags
116
125
  cu create -n "Fix bug" -l <listId> --priority urgent --tags "bug,frontend"
117
-
118
- # Post a comment
119
126
  cu comment abc123def -m "Completed in PR #42"
120
-
121
- # Discover lists in a space
122
- cu lists <spaceId> --json
123
-
124
- # Open a task in the browser
125
- cu open abc123def
126
-
127
- # Standup summary
128
- cu summary --json
129
-
130
- # Check overdue tasks
131
- cu overdue --json
132
-
133
- # Assign yourself to a task
134
127
  cu assign abc123def --to me
128
+ ```
135
129
 
136
- # Check/set config
137
- cu config get teamId
138
- cu config set apiToken pk_example_token
139
-
140
- cu search "login bug" --json
141
-
142
- cu activity abc123def --json
130
+ ### Discover workspace structure
143
131
 
144
- cu auth --json
132
+ ```bash
133
+ cu spaces # all spaces
134
+ cu spaces --name "Engineering" # find space ID by name
135
+ cu lists <spaceId> # lists in a space (needs ID from cu spaces)
136
+ cu sprints # all sprints across folders
137
+ cu auth # verify token works
138
+ ```
145
139
 
146
- cu sprints --json
140
+ ### Standup
147
141
 
148
- cu update abc123def -s "prog"
142
+ ```bash
143
+ cu summary # completed / in progress / overdue
144
+ cu summary --hours 48 # wider window
149
145
  ```