@krodak/clickup-cli 0.21.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clickup-cli",
3
3
  "description": "ClickUp CLI skills for managing tasks, sprints, comments, checklists, custom fields, tags, and time tracking via the cup command",
4
- "version": "0.21.0",
4
+ "version": "1.0.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/README.md CHANGED
@@ -162,14 +162,14 @@ Status: :white_check_mark: implemented | :construction: planned | :no_entry_sign
162
162
 
163
163
  ### Sprints & Planning
164
164
 
165
- | Feature | Command | Status |
166
- | ------------------------ | -------------- | ------------------ |
167
- | Active sprint tasks | `cup sprint` | :white_check_mark: |
168
- | List all sprints | `cup sprints` | :white_check_mark: |
169
- | Assigned tasks by status | `cup assigned` | :white_check_mark: |
170
- | Standup summary | `cup summary` | :white_check_mark: |
171
- | Overdue tasks | `cup overdue` | :white_check_mark: |
172
- | Recently updated | `cup inbox` | :white_check_mark: |
165
+ | Feature | Command | Status |
166
+ | ------------------------ | ----------------------- | ------------------ |
167
+ | Active sprint tasks | `cup sprint [--folder]` | :white_check_mark: |
168
+ | List all sprints | `cup sprints` | :white_check_mark: |
169
+ | Assigned tasks by status | `cup assigned` | :white_check_mark: |
170
+ | Standup summary | `cup summary` | :white_check_mark: |
171
+ | Overdue tasks | `cup overdue` | :white_check_mark: |
172
+ | Recently updated | `cup inbox` | :white_check_mark: |
173
173
 
174
174
  ### Comments
175
175
 
@@ -299,12 +299,13 @@ Most commands scope to your assigned tasks by default - keeping output small and
299
299
 
300
300
  ### Config file
301
301
 
302
- `~/.config/cu/config.json` (or `$XDG_CONFIG_HOME/cu/config.json`):
302
+ `~/.config/cup/config.json` (or `$XDG_CONFIG_HOME/cup/config.json`):
303
303
 
304
304
  ```json
305
305
  {
306
306
  "apiToken": "pk_...",
307
- "teamId": "12345678"
307
+ "teamId": "12345678",
308
+ "sprintFolderId": "optional - folder ID to skip auto-detection"
308
309
  }
309
310
  ```
310
311
 
package/dist/index.js CHANGED
@@ -390,18 +390,36 @@ import fs from "fs";
390
390
  import { homedir } from "os";
391
391
  import { join } from "path";
392
392
  function configDir() {
393
+ const xdg = process.env.XDG_CONFIG_HOME;
394
+ if (xdg) return join(xdg, "cup");
395
+ return join(homedir(), ".config", "cup");
396
+ }
397
+ function legacyConfigDir() {
393
398
  const xdg = process.env.XDG_CONFIG_HOME;
394
399
  if (xdg) return join(xdg, "cu");
395
400
  return join(homedir(), ".config", "cu");
396
401
  }
402
+ var migrationChecked = false;
403
+ function migrateFromLegacy() {
404
+ if (migrationChecked) return;
405
+ migrationChecked = true;
406
+ const legacy = legacyConfigDir();
407
+ const current = configDir();
408
+ if (fs.existsSync(join(legacy, "config.json")) && !fs.existsSync(join(current, "config.json"))) {
409
+ fs.mkdirSync(current, { recursive: true, mode: 448 });
410
+ fs.copyFileSync(join(legacy, "config.json"), join(current, "config.json"));
411
+ }
412
+ }
397
413
  function configPath() {
398
414
  return join(configDir(), "config.json");
399
415
  }
400
416
  function loadConfig() {
417
+ migrateFromLegacy();
401
418
  const envToken = process.env.CU_API_TOKEN?.trim();
402
419
  const envTeamId = process.env.CU_TEAM_ID?.trim();
403
420
  let fileToken;
404
421
  let fileTeamId;
422
+ let fileSprintFolderId;
405
423
  const path = configPath();
406
424
  if (fs.existsSync(path)) {
407
425
  const raw = fs.readFileSync(path, "utf-8");
@@ -413,6 +431,7 @@ function loadConfig() {
413
431
  }
414
432
  fileToken = parsed.apiToken?.trim();
415
433
  fileTeamId = parsed.teamId?.trim();
434
+ fileSprintFolderId = parsed.sprintFolderId?.trim() || void 0;
416
435
  }
417
436
  const apiToken = envToken || fileToken;
418
437
  if (!apiToken) {
@@ -425,9 +444,10 @@ function loadConfig() {
425
444
  if (!teamId) {
426
445
  throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cup init");
427
446
  }
428
- return { apiToken, teamId };
447
+ return { apiToken, teamId, ...fileSprintFolderId ? { sprintFolderId: fileSprintFolderId } : {} };
429
448
  }
430
449
  function loadRawConfig() {
450
+ migrateFromLegacy();
431
451
  const path = configPath();
432
452
  if (!fs.existsSync(path)) return {};
433
453
  try {
@@ -437,6 +457,7 @@ function loadRawConfig() {
437
457
  }
438
458
  }
439
459
  function getConfigPath() {
460
+ migrateFromLegacy();
440
461
  return configPath();
441
462
  }
442
463
  function writeConfig(config) {
@@ -453,10 +474,33 @@ function writeConfig(config) {
453
474
 
454
475
  // src/date.ts
455
476
  function formatDate(ms) {
477
+ return new Date(Number(ms)).toLocaleDateString("en-US", {
478
+ month: "short",
479
+ day: "numeric",
480
+ year: "numeric"
481
+ });
482
+ }
483
+ function formatTimestamp(ms) {
484
+ return new Date(Number(ms)).toLocaleString("en-US", {
485
+ month: "short",
486
+ day: "numeric",
487
+ hour: "numeric",
488
+ minute: "2-digit"
489
+ });
490
+ }
491
+ function formatDuration(ms) {
492
+ const totalMinutes = Math.round(Math.abs(ms) / 6e4);
493
+ const hours = Math.floor(totalMinutes / 60);
494
+ const minutes = totalMinutes % 60;
495
+ if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
496
+ if (hours > 0) return `${hours}h`;
497
+ return `${minutes}m`;
498
+ }
499
+ function formatDateISO(ms) {
456
500
  const d = new Date(Number(ms));
457
- const year = d.getUTCFullYear();
458
- const month = String(d.getUTCMonth() + 1).padStart(2, "0");
459
- const day = String(d.getUTCDate()).padStart(2, "0");
501
+ const year = d.getFullYear();
502
+ const month = String(d.getMonth() + 1).padStart(2, "0");
503
+ const day = String(d.getDate()).padStart(2, "0");
460
504
  return `${year}-${month}-${day}`;
461
505
  }
462
506
 
@@ -560,12 +604,6 @@ ${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
560
604
  if (sections.length === 0) return "No tasks found.";
561
605
  return sections.join("\n\n");
562
606
  }
563
- function formatDuration(ms) {
564
- const totalMinutes = Math.floor(ms / 6e4);
565
- const hours = Math.floor(totalMinutes / 60);
566
- const minutes = totalMinutes % 60;
567
- return `${hours}h ${minutes}m`;
568
- }
569
607
  function formatTaskDetailMarkdown(task) {
570
608
  const lines = [`# ${task.name}`, ""];
571
609
  const isInitiative = (task.custom_item_id ?? 0) !== 0;
@@ -581,8 +619,8 @@ function formatTaskDetailMarkdown(task) {
581
619
  ],
582
620
  ["Priority", task.priority?.priority],
583
621
  ["Parent", task.parent ?? void 0],
584
- ["Start Date", task.start_date ? formatDate(task.start_date) : void 0],
585
- ["Due Date", task.due_date ? formatDate(task.due_date) : void 0],
622
+ ["Start Date", task.start_date ? formatDateISO(task.start_date) : void 0],
623
+ ["Due Date", task.due_date ? formatDateISO(task.due_date) : void 0],
586
624
  [
587
625
  "Time Estimate",
588
626
  task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
@@ -592,8 +630,8 @@ function formatTaskDetailMarkdown(task) {
592
630
  task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
593
631
  ],
594
632
  ["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
595
- ["Created", task.date_created ? formatDate(task.date_created) : void 0],
596
- ["Updated", task.date_updated ? formatDate(task.date_updated) : void 0]
633
+ ["Created", task.date_created ? formatDateISO(task.date_created) : void 0],
634
+ ["Updated", task.date_updated ? formatDateISO(task.date_updated) : void 0]
597
635
  ];
598
636
  for (const [label, value] of fields) {
599
637
  if (value != null && value !== "") {
@@ -673,20 +711,6 @@ function openUrl(url) {
673
711
  `);
674
712
  }
675
713
  }
676
- function formatMs(ms) {
677
- const hours = Math.floor(ms / 36e5);
678
- const mins = Math.floor(ms % 36e5 / 6e4);
679
- if (hours > 0 && mins > 0) return `${hours}h ${mins}m`;
680
- if (hours > 0) return `${hours}h`;
681
- return `${mins}m`;
682
- }
683
- function formatTimestamp(ts) {
684
- return new Date(Number(ts)).toLocaleDateString("en-US", {
685
- month: "short",
686
- day: "numeric",
687
- year: "numeric"
688
- });
689
- }
690
714
  function descriptionPreview(text, maxLines = 3) {
691
715
  const lines = text.split("\n").filter((l) => l.trim().length > 0);
692
716
  const preview = lines.slice(0, maxLines);
@@ -746,10 +770,10 @@ function formatTaskDetail(task) {
746
770
  task.assignees?.length ? task.assignees.map((a) => a.username).join(", ") : void 0
747
771
  ],
748
772
  ["Priority", task.priority?.priority],
749
- ["Start", task.start_date ? formatTimestamp(task.start_date) : void 0],
750
- ["Due", task.due_date ? formatTimestamp(task.due_date) : void 0],
751
- ["Estimate", task.time_estimate ? formatMs(task.time_estimate) : void 0],
752
- ["Tracked", task.time_spent ? formatMs(task.time_spent) : void 0],
773
+ ["Start", task.start_date ? formatDate(task.start_date) : void 0],
774
+ ["Due", task.due_date ? formatDate(task.due_date) : void 0],
775
+ ["Estimate", task.time_estimate ? formatDuration(task.time_estimate) : void 0],
776
+ ["Tracked", task.time_spent ? formatDuration(task.time_spent) : void 0],
753
777
  ["Tags", task.tags?.length ? task.tags.map((t) => t.name).join(", ") : void 0],
754
778
  ["Parent", task.parent || void 0],
755
779
  ["URL", task.url]
@@ -1004,7 +1028,8 @@ function parseDueDate(value) {
1004
1028
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1005
1029
  throw new Error("Date must be in YYYY-MM-DD format");
1006
1030
  }
1007
- const date = new Date(value);
1031
+ const parts = value.split("-");
1032
+ const date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
1008
1033
  if (isNaN(date.getTime())) throw new Error(`Invalid date: ${value}`);
1009
1034
  return date.getTime();
1010
1035
  }
@@ -1180,23 +1205,75 @@ async function runInitCommand() {
1180
1205
  }
1181
1206
 
1182
1207
  // src/commands/sprint.ts
1183
- function parseSprintDates(name) {
1208
+ import { select as select2 } from "@inquirer/prompts";
1209
+ var SPRINT_KEYWORDS = ["sprint", "iteration", "cycle", "scrum"];
1210
+ function parseUSDateRange(name) {
1184
1211
  const m = name.match(/\((\d{1,2})\/(\d{1,2})\s*[-–]\s*(\d{1,2})\/(\d{1,2})\)/);
1185
1212
  if (!m) return null;
1186
1213
  const year = (/* @__PURE__ */ new Date()).getFullYear();
1187
1214
  const start = new Date(year, Number(m[1]) - 1, Number(m[2]));
1188
1215
  const end = new Date(year, Number(m[3]) - 1, Number(m[4]), 23, 59, 59);
1189
- if (end < start) {
1190
- end.setFullYear(end.getFullYear() + 1);
1191
- }
1216
+ if (end < start) end.setFullYear(end.getFullYear() + 1);
1192
1217
  return { start, end };
1193
1218
  }
1219
+ function parseISODateRange(name) {
1220
+ const m = name.match(/\((\d{4}-\d{2}-\d{2})\s*[-–]\s*(\d{4}-\d{2}-\d{2})\)/);
1221
+ if (!m) return null;
1222
+ const [sy, sm, sd] = m[1].split("-").map(Number);
1223
+ const [ey, em, ed] = m[2].split("-").map(Number);
1224
+ const start = new Date(sy, sm - 1, sd);
1225
+ const end = new Date(ey, em - 1, ed, 23, 59, 59);
1226
+ return { start, end };
1227
+ }
1228
+ function parseMonthDayRange(name) {
1229
+ const months = {
1230
+ jan: 0,
1231
+ feb: 1,
1232
+ mar: 2,
1233
+ apr: 3,
1234
+ may: 4,
1235
+ jun: 5,
1236
+ jul: 6,
1237
+ aug: 7,
1238
+ sep: 8,
1239
+ oct: 9,
1240
+ nov: 10,
1241
+ dec: 11
1242
+ };
1243
+ const m = name.match(/\(([A-Za-z]{3})\s+(\d{1,2})\s*[-–]\s*([A-Za-z]{3})\s+(\d{1,2})\)/);
1244
+ if (!m) return null;
1245
+ const sm = months[m[1].toLowerCase()];
1246
+ const em = months[m[3].toLowerCase()];
1247
+ if (sm === void 0 || em === void 0) return null;
1248
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
1249
+ const start = new Date(year, sm, Number(m[2]));
1250
+ const end = new Date(year, em, Number(m[4]), 23, 59, 59);
1251
+ if (end < start) end.setFullYear(end.getFullYear() + 1);
1252
+ return { start, end };
1253
+ }
1254
+ function parseEuropeanDateRange(name) {
1255
+ const m = name.match(/\((\d{1,2})\.(\d{1,2})\s*[-–]\s*(\d{1,2})\.(\d{1,2})\)/);
1256
+ if (!m) return null;
1257
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
1258
+ const start = new Date(year, Number(m[2]) - 1, Number(m[1]));
1259
+ const end = new Date(year, Number(m[4]) - 1, Number(m[3]), 23, 59, 59);
1260
+ if (end < start) end.setFullYear(end.getFullYear() + 1);
1261
+ return { start, end };
1262
+ }
1263
+ function parseSprintDates(name) {
1264
+ return parseUSDateRange(name) ?? parseISODateRange(name) ?? parseMonthDayRange(name) ?? parseEuropeanDateRange(name);
1265
+ }
1194
1266
  function findActiveSprintList(lists, today = /* @__PURE__ */ new Date()) {
1195
1267
  if (lists.length === 0) return null;
1196
1268
  for (const list of lists) {
1197
1269
  const dates = parseSprintDates(list.name);
1198
- if (dates && today >= dates.start && today <= dates.end) {
1199
- return list;
1270
+ if (dates && today >= dates.start && today <= dates.end) return list;
1271
+ }
1272
+ for (const list of lists) {
1273
+ if (list.start_date && list.due_date) {
1274
+ const start = new Date(Number(list.start_date));
1275
+ const end = new Date(Number(list.due_date));
1276
+ if (today >= start && today <= end) return list;
1200
1277
  }
1201
1278
  }
1202
1279
  return lists[lists.length - 1] ?? null;
@@ -1216,37 +1293,67 @@ function findRelatedSpaces(mySpaceIds, allSpaces) {
1216
1293
  async function runSprintCommand(config, opts) {
1217
1294
  const client = new ClickUpClient(config);
1218
1295
  process.stderr.write("Detecting active sprint...\n");
1296
+ const folderId = opts.folder ?? config.sprintFolderId;
1219
1297
  const [myTasks, allSpaces, customTypes] = await Promise.all([
1220
1298
  client.getMyTasks(config.teamId),
1221
- client.getSpaces(config.teamId),
1299
+ folderId ? Promise.resolve([]) : client.getSpaces(config.teamId),
1222
1300
  client.getCustomTaskTypes(config.teamId)
1223
1301
  ]);
1224
1302
  const typeMap = buildTypeMap(customTypes);
1225
- let spaces;
1226
- if (opts.space) {
1227
- spaces = allSpaces.filter(
1228
- (s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
1229
- );
1230
- if (spaces.length === 0) {
1231
- throw new Error(
1232
- `No space matching "${opts.space}" found. Use \`cu spaces\` to list available spaces.`
1303
+ let sprintLists;
1304
+ if (folderId) {
1305
+ sprintLists = await client.getFolderLists(folderId);
1306
+ } else {
1307
+ let spaces;
1308
+ if (opts.space) {
1309
+ spaces = allSpaces.filter(
1310
+ (s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
1311
+ );
1312
+ if (spaces.length === 0) {
1313
+ throw new Error(
1314
+ `No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
1315
+ );
1316
+ }
1317
+ } else {
1318
+ const mySpaceIds = new Set(
1319
+ myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
1233
1320
  );
1321
+ spaces = findRelatedSpaces(mySpaceIds, allSpaces);
1234
1322
  }
1235
- } else {
1236
- const mySpaceIds = new Set(
1237
- myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
1323
+ const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
1324
+ const sprintFolders = foldersBySpace.flat().filter((f) => {
1325
+ const lower = f.name.toLowerCase();
1326
+ return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
1327
+ });
1328
+ const listsByFolder = await Promise.all(
1329
+ sprintFolders.map((folder) => client.getFolderLists(folder.id))
1238
1330
  );
1239
- spaces = findRelatedSpaces(mySpaceIds, allSpaces);
1331
+ sprintLists = listsByFolder.flat();
1332
+ }
1333
+ let activeList = findActiveSprintList(sprintLists);
1334
+ if (!activeList && sprintLists.length > 1 && isTTY()) {
1335
+ const choice = await select2({
1336
+ message: "Multiple sprint lists found. Which one?",
1337
+ choices: sprintLists.map((l) => ({
1338
+ name: `${l.name} (${l.id})`,
1339
+ value: l
1340
+ }))
1341
+ });
1342
+ activeList = choice;
1343
+ }
1344
+ if (!activeList && sprintLists.length > 1) {
1345
+ process.stderr.write(
1346
+ `Multiple sprint lists found:
1347
+ ${sprintLists.map((l) => ` - ${l.name} (${l.id})`).join("\n")}
1348
+ Using: ${sprintLists[sprintLists.length - 1].name}
1349
+ `
1350
+ );
1351
+ activeList = sprintLists[sprintLists.length - 1] ?? null;
1240
1352
  }
1241
- const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
1242
- const sprintFolders = foldersBySpace.flat().filter((f) => f.name.toLowerCase().includes("sprint"));
1243
- const listsByFolder = await Promise.all(
1244
- sprintFolders.map((folder) => client.getFolderLists(folder.id))
1245
- );
1246
- const sprintLists = listsByFolder.flat();
1247
- const activeList = findActiveSprintList(sprintLists);
1248
1353
  if (!activeList) {
1249
- throw new Error('No sprint list found. Ensure sprint folders contain "sprint" in their name.');
1354
+ throw new Error(
1355
+ 'No sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
1356
+ );
1250
1357
  }
1251
1358
  process.stderr.write(`Active sprint: ${activeList.name}
1252
1359
  `);
@@ -1274,7 +1381,7 @@ var SPRINT_COLUMNS = [
1274
1381
  { key: "sprint", label: "SPRINT", maxWidth: 60 },
1275
1382
  { key: "dates", label: "DATES" }
1276
1383
  ];
1277
- function formatDate2(d) {
1384
+ function formatSprintDate(d) {
1278
1385
  return `${d.getMonth() + 1}/${d.getDate()}`;
1279
1386
  }
1280
1387
  function buildSprintInfos(lists, folderName, today) {
@@ -1304,7 +1411,7 @@ async function listSprints(config, opts = {}) {
1304
1411
  );
1305
1412
  if (spaces.length === 0) {
1306
1413
  throw new Error(
1307
- `No space matching "${opts.space}" found. Use \`cu spaces\` to list available spaces.`
1414
+ `No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
1308
1415
  );
1309
1416
  }
1310
1417
  } else {
@@ -1314,7 +1421,10 @@ async function listSprints(config, opts = {}) {
1314
1421
  spaces = findRelatedSpaces(mySpaceIds, allSpaces);
1315
1422
  }
1316
1423
  const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
1317
- const sprintFolders = foldersBySpace.flat().filter((f) => f.name.toLowerCase().includes("sprint"));
1424
+ const sprintFolders = foldersBySpace.flat().filter((f) => {
1425
+ const lower = f.name.toLowerCase();
1426
+ return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
1427
+ });
1318
1428
  const today = /* @__PURE__ */ new Date();
1319
1429
  const allSprints = [];
1320
1430
  const listsByFolder = await Promise.all(
@@ -1335,7 +1445,7 @@ async function listSprints(config, opts = {}) {
1335
1445
  }
1336
1446
  const rows = allSprints.map((s) => {
1337
1447
  const dates = parseSprintDates(s.name);
1338
- const dateStr = dates ? `${formatDate2(dates.start)} - ${formatDate2(dates.end)}` : "";
1448
+ const dateStr = dates ? `${formatSprintDate(dates.start)} - ${formatSprintDate(dates.end)}` : "";
1339
1449
  return {
1340
1450
  id: s.id,
1341
1451
  sprint: s.active ? `* ${s.name}` : s.name,
@@ -1379,15 +1489,6 @@ async function postComment(config, taskId, text, notifyAll) {
1379
1489
 
1380
1490
  // src/commands/comments.ts
1381
1491
  import chalk3 from "chalk";
1382
- function formatDate3(timestamp) {
1383
- return new Date(Number(timestamp)).toLocaleString("en-US", {
1384
- month: "short",
1385
- day: "numeric",
1386
- year: "numeric",
1387
- hour: "numeric",
1388
- minute: "2-digit"
1389
- });
1390
- }
1391
1492
  async function fetchComments(config, taskId) {
1392
1493
  const client = new ClickUpClient(config);
1393
1494
  const comments = await client.getTaskComments(taskId);
@@ -1415,7 +1516,7 @@ function printComments(comments, forceJson) {
1415
1516
  for (let i = 0; i < comments.length; i++) {
1416
1517
  const c = comments[i];
1417
1518
  if (i > 0) console.log(separator);
1418
- console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate3(c.date))}`);
1519
+ console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatTimestamp(c.date))}`);
1419
1520
  console.log(c.text);
1420
1521
  if (i < comments.length - 1) console.log("");
1421
1522
  }
@@ -1812,7 +1913,7 @@ async function fetchOverdueTasks(config, opts = {}) {
1812
1913
  }
1813
1914
 
1814
1915
  // src/commands/config.ts
1815
- var VALID_KEYS = /* @__PURE__ */ new Set(["apiToken", "teamId"]);
1916
+ var VALID_KEYS = /* @__PURE__ */ new Set(["apiToken", "teamId", "sprintFolderId"]);
1816
1917
  function assertValidKey(key) {
1817
1918
  if (!VALID_KEYS.has(key)) {
1818
1919
  throw new Error(`Unknown config key: ${key}. Valid keys: ${[...VALID_KEYS].join(", ")}`);
@@ -1868,15 +1969,6 @@ async function assignTask(config, taskId, opts) {
1868
1969
 
1869
1970
  // src/commands/activity.ts
1870
1971
  import chalk4 from "chalk";
1871
- function formatDate4(timestamp) {
1872
- return new Date(Number(timestamp)).toLocaleString("en-US", {
1873
- month: "short",
1874
- day: "numeric",
1875
- year: "numeric",
1876
- hour: "numeric",
1877
- minute: "2-digit"
1878
- });
1879
- }
1880
1972
  async function fetchActivity(config, taskId) {
1881
1973
  const client = new ClickUpClient(config);
1882
1974
  const [task, rawComments] = await Promise.all([
@@ -1920,7 +2012,7 @@ ${commentsMd}`);
1920
2012
  console.log("");
1921
2013
  console.log(chalk4.dim("-".repeat(60)));
1922
2014
  }
1923
- console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatDate4(c.date))}`);
2015
+ console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatTimestamp(c.date))}`);
1924
2016
  console.log(c.text);
1925
2017
  }
1926
2018
  }
@@ -1973,7 +2065,7 @@ function bashCompletion(name) {
1973
2065
  COMPREPLY=($(compgen -W "-l --list -n --name -d --description -p --parent -s --status --priority --due-date --assignee --tags --custom-item-id --time-estimate --json" -- "$cur"))
1974
2066
  ;;
1975
2067
  sprint)
1976
- COMPREPLY=($(compgen -W "--status --space --include-closed --json" -- "$cur"))
2068
+ COMPREPLY=($(compgen -W "--status --space --folder --include-closed --json" -- "$cur"))
1977
2069
  ;;
1978
2070
  sprints)
1979
2071
  COMPREPLY=($(compgen -W "--space --json" -- "$cur"))
@@ -2070,7 +2162,7 @@ function bashCompletion(name) {
2070
2162
  local subcmd="\${words[2]}"
2071
2163
  case "$subcmd" in
2072
2164
  get|set)
2073
- COMPREPLY=($(compgen -W "apiToken teamId" -- "$cur"))
2165
+ COMPREPLY=($(compgen -W "apiToken teamId sprintFolderId" -- "$cur"))
2074
2166
  ;;
2075
2167
  esac
2076
2168
  fi
@@ -2186,6 +2278,7 @@ _${name}() {
2186
2278
  _arguments \\
2187
2279
  '--status[Filter by status]:status:(open "in progress" "in review" done closed)' \\
2188
2280
  '--space[Narrow sprint search to a space]:space:' \\
2281
+ '--folder[Sprint folder ID]:folder_id:' \\
2189
2282
  '--include-closed[Include done/closed tasks]' \\
2190
2283
  '--json[Force JSON output]'
2191
2284
  ;;
@@ -2461,7 +2554,7 @@ _${name}() {
2461
2554
  config_args)
2462
2555
  case $words[1] in
2463
2556
  get|set)
2464
- _arguments '1:key:(apiToken teamId)'
2557
+ _arguments '1:key:(apiToken teamId sprintFolderId)'
2465
2558
  ;;
2466
2559
  esac
2467
2560
  ;;
@@ -2558,6 +2651,7 @@ complete -c ${name} -n '__fish_seen_subcommand_from create' -l json -d 'Force JS
2558
2651
 
2559
2652
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l status -d 'Filter by status'
2560
2653
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l space -d 'Narrow sprint search to a space'
2654
+ complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l folder -d 'Sprint folder ID'
2561
2655
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l include-closed -d 'Include done/closed tasks'
2562
2656
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l json -d 'Force JSON output'
2563
2657
 
@@ -2673,7 +2767,7 @@ complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l json -d 'Fo
2673
2767
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a get -d 'Print a config value'
2674
2768
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a set -d 'Set a config value'
2675
2769
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a path -d 'Print config file path'
2676
- complete -c ${name} -n '__fish_seen_subcommand_from get set' -a 'apiToken teamId' -d 'Config key'
2770
+ complete -c ${name} -n '__fish_seen_subcommand_from get set' -a 'apiToken teamId sprintFolderId' -d 'Config key'
2677
2771
 
2678
2772
  complete -c ${name} -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell type'
2679
2773
  `;
@@ -2969,6 +3063,17 @@ function formatChecklists(checklists) {
2969
3063
  }
2970
3064
  return lines.join("\n");
2971
3065
  }
3066
+ function formatChecklistsMarkdown(checklists) {
3067
+ if (checklists.length === 0) return "No checklists";
3068
+ return checklists.map((cl) => {
3069
+ const resolved = cl.items.filter((i) => i.resolved).length;
3070
+ const header = `### ${cl.name} (${resolved}/${cl.items.length})`;
3071
+ const items = cl.items.map(
3072
+ (item) => `- [${item.resolved ? "x" : " "}] ${item.name}`
3073
+ );
3074
+ return [header, "", ...items].join("\n");
3075
+ }).join("\n\n");
3076
+ }
2972
3077
 
2973
3078
  // src/commands/comment-edit.ts
2974
3079
  async function editComment(config, commentId, text, resolved) {
@@ -2998,11 +3103,21 @@ function formatReplies(replies) {
2998
3103
  if (replies.length === 0) return "No replies";
2999
3104
  return replies.map((r) => {
3000
3105
  const user = r.user?.username ?? "Unknown";
3001
- const date = new Date(Number(r.date)).toLocaleString();
3106
+ const date = formatTimestamp(Number(r.date));
3002
3107
  return `${chalk6.bold(user)} ${chalk6.dim(date)}
3003
3108
  ${r.comment_text}`;
3004
3109
  }).join("\n\n");
3005
3110
  }
3111
+ function formatRepliesMarkdown(replies) {
3112
+ if (replies.length === 0) return "No replies";
3113
+ return replies.map((r) => {
3114
+ const user = r.user?.username ?? "Unknown";
3115
+ const date = formatTimestamp(Number(r.date));
3116
+ return `**${user}** (${date})
3117
+
3118
+ ${r.comment_text}`;
3119
+ }).join("\n\n---\n\n");
3120
+ }
3006
3121
 
3007
3122
  // src/commands/link.ts
3008
3123
  async function manageTaskLink(config, taskId, linksTo, remove) {
@@ -3029,22 +3144,6 @@ async function attachFile(config, taskId, filePath) {
3029
3144
 
3030
3145
  // src/commands/time.ts
3031
3146
  import chalk7 from "chalk";
3032
- function formatDuration2(ms) {
3033
- const totalMinutes = Math.round(Math.abs(ms) / 6e4);
3034
- const hours = Math.floor(totalMinutes / 60);
3035
- const minutes = totalMinutes % 60;
3036
- if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
3037
- if (hours > 0) return `${hours}h`;
3038
- return `${minutes}m`;
3039
- }
3040
- function formatTimestamp2(ms) {
3041
- return new Date(Number(ms)).toLocaleString("en-US", {
3042
- month: "short",
3043
- day: "numeric",
3044
- hour: "numeric",
3045
- minute: "2-digit"
3046
- });
3047
- }
3048
3147
  async function startTimer(config, taskId, description) {
3049
3148
  const client = new ClickUpClient(config);
3050
3149
  return client.startTimeEntry(config.teamId, taskId, description);
@@ -3079,11 +3178,11 @@ function formatTimeEntry(entry) {
3079
3178
  const taskId = entry.task?.id ?? "";
3080
3179
  const isRunning = entry.duration < 0;
3081
3180
  const elapsed = isRunning ? Date.now() - Number(entry.start) : entry.duration;
3082
- const durationStr = formatDuration2(elapsed);
3181
+ const durationStr = formatDuration(elapsed);
3083
3182
  const status = isRunning ? chalk7.green("RUNNING") : "";
3084
3183
  lines.push(`${chalk7.bold(taskName)} ${chalk7.dim(taskId)} ${status}`);
3085
3184
  lines.push(
3086
- ` ${durationStr} - ${formatTimestamp2(entry.start)}${entry.description ? ` - ${entry.description}` : ""}`
3185
+ ` ${durationStr} - ${formatTimestamp(entry.start)}${entry.description ? ` - ${entry.description}` : ""}`
3087
3186
  );
3088
3187
  return lines.join("\n");
3089
3188
  }
@@ -3091,6 +3190,19 @@ function formatTimeEntries(entries) {
3091
3190
  if (entries.length === 0) return "No time entries";
3092
3191
  return entries.map(formatTimeEntry).join("\n");
3093
3192
  }
3193
+ function formatTimeEntryMarkdown(entry) {
3194
+ const taskName = entry.task?.name ?? "No task";
3195
+ const taskId = entry.task?.id ?? "";
3196
+ const isRunning = entry.duration < 0;
3197
+ const elapsed = isRunning ? Date.now() - Number(entry.start) : entry.duration;
3198
+ const durationStr = formatDuration(elapsed);
3199
+ const status = isRunning ? " (RUNNING)" : "";
3200
+ return `**${taskName}** ${taskId}${status} - ${durationStr}${entry.description ? ` - ${entry.description}` : ""}`;
3201
+ }
3202
+ function formatTimeEntriesMarkdown(entries) {
3203
+ if (entries.length === 0) return "No time entries";
3204
+ return entries.map(formatTimeEntryMarkdown).join("\n");
3205
+ }
3094
3206
 
3095
3207
  // src/index.ts
3096
3208
  var require2 = createRequire(import.meta.url);
@@ -3185,7 +3297,7 @@ program.command("create").description("Create a new task").option("-l, --list <l
3185
3297
  }
3186
3298
  })
3187
3299
  );
3188
- 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(
3300
+ 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("--folder <folderId>", "Sprint folder ID (overrides config and auto-detection)").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
3189
3301
  wrapAction(
3190
3302
  async (opts) => {
3191
3303
  const config = loadConfig();
@@ -3269,8 +3381,10 @@ program.command("replies <commentId>").description("List threaded replies on a c
3269
3381
  const replies = await getReplies(config, commentId);
3270
3382
  if (shouldOutputJson(opts.json ?? false)) {
3271
3383
  console.log(JSON.stringify(replies, null, 2));
3272
- } else {
3384
+ } else if (isTTY()) {
3273
3385
  console.log(formatReplies(replies));
3386
+ } else {
3387
+ console.log(formatRepliesMarkdown(replies));
3274
3388
  }
3275
3389
  })
3276
3390
  );
@@ -3490,8 +3604,10 @@ checklistCmd.command("view <taskId>").description("View checklists on a task").o
3490
3604
  const checklists = await viewChecklists(config, taskId);
3491
3605
  if (shouldOutputJson(opts.json ?? false)) {
3492
3606
  console.log(JSON.stringify(checklists, null, 2));
3493
- } else {
3607
+ } else if (isTTY()) {
3494
3608
  console.log(formatChecklists(checklists));
3609
+ } else {
3610
+ console.log(formatChecklistsMarkdown(checklists));
3495
3611
  }
3496
3612
  })
3497
3613
  );
@@ -3578,8 +3694,10 @@ timeCmd.command("stop").description("Stop the running timer").option("--json", "
3578
3694
  const result = await stopTimer(config);
3579
3695
  if (shouldOutputJson(opts.json ?? false)) {
3580
3696
  console.log(JSON.stringify(result, null, 2));
3581
- } else {
3697
+ } else if (isTTY()) {
3582
3698
  console.log(formatTimeEntry(result));
3699
+ } else {
3700
+ console.log(formatTimeEntryMarkdown(result));
3583
3701
  }
3584
3702
  })
3585
3703
  );
@@ -3589,10 +3707,12 @@ timeCmd.command("status").description("Show the currently running timer").option
3589
3707
  const result = await timerStatus(config);
3590
3708
  if (shouldOutputJson(opts.json ?? false)) {
3591
3709
  console.log(JSON.stringify(result, null, 2));
3592
- } else if (result) {
3710
+ } else if (!result) {
3711
+ console.log("No timer running");
3712
+ } else if (isTTY()) {
3593
3713
  console.log(formatTimeEntry(result));
3594
3714
  } else {
3595
- console.log("No timer running");
3715
+ console.log(formatTimeEntryMarkdown(result));
3596
3716
  }
3597
3717
  })
3598
3718
  );
@@ -3619,8 +3739,10 @@ timeCmd.command("list").description("List recent time entries (default: last 7 d
3619
3739
  const entries = await listTimeEntries(config, { days, taskId: opts.task });
3620
3740
  if (shouldOutputJson(opts.json ?? false)) {
3621
3741
  console.log(JSON.stringify(entries, null, 2));
3622
- } else {
3742
+ } else if (isTTY()) {
3623
3743
  console.log(formatTimeEntries(entries));
3744
+ } else {
3745
+ console.log(formatTimeEntriesMarkdown(entries));
3624
3746
  }
3625
3747
  })
3626
3748
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.21.0",
3
+ "version": "1.0.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,7 +52,6 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@eslint/js": "^10.0.1",
55
- "@inquirer/testing": "^3.3.0",
56
55
  "@types/node": "^25.3.0",
57
56
  "dotenv": "^17.3.1",
58
57
  "eslint": "^10.0.2",
@@ -11,7 +11,7 @@ Keywords: ClickUp, task management, sprint, project management, agile, backlog,
11
11
 
12
12
  ## Setup
13
13
 
14
- Config at `~/.config/cu/config.json` with `apiToken` and `teamId`. Run `cup init` to set up interactively.
14
+ Config at `~/.config/cup/config.json` with `apiToken` and `teamId`. Optional: `sprintFolderId` to pin sprint detection to a specific folder. Run `cup init` to set up interactively.
15
15
 
16
16
  Environment variables `CU_API_TOKEN` and `CU_TEAM_ID` override config file when both are set.
17
17
 
@@ -38,7 +38,7 @@ All commands support `--help` for full flag details.
38
38
  | --------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
39
39
  | `cup tasks [--status s] [--name q] [--type t] [--list id] [--space id] [--include-closed] [--json]` | My tasks (all types, or filter with --type) |
40
40
  | `cup assigned [--status s] [--include-closed] [--json]` | All my tasks grouped by status |
41
- | `cup sprint [--status s] [--space nameOrId] [--include-closed] [--json]` | Tasks in active sprint (auto-detected) |
41
+ | `cup sprint [--status s] [--space nameOrId] [--folder id] [--include-closed] [--json]` | Tasks in active sprint (auto-detected) |
42
42
  | `cup sprints [--space nameOrId] [--json]` | List all sprints (marks active with \*) |
43
43
  | `cup search <query> [--status s] [--include-closed] [--json]` | Search my tasks by name (multi-word, fuzzy status) |
44
44
  | `cup task <id> [--json]` | Single task details |
@@ -55,79 +55,79 @@ All commands support `--help` for full flag details.
55
55
 
56
56
  ### Write
57
57
 
58
- | Command | What it does |
59
- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
60
- | `cup update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id\|me] [--parent id] [--json]` | Update task fields (desc supports markdown) |
61
- | `cup create -n name [-l listId] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id\|me] [--tags t] [--custom-item-id n] [--json]` | Create task (desc supports markdown) |
62
- | `cup comment <id> -m text [--notify-all] [--json]` | Post comment on task |
63
- | `cup comment-edit <commentId> -m text [--resolved] [--unresolved] [--json]` | Edit an existing comment |
64
- | `cup assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users |
65
- | `cup depend <id> [--on taskId] [--blocks taskId] [--remove] [--json]` | Add/remove task dependencies |
66
- | `cup move <id> [--to listId] [--remove listId] [--json]` | Add/remove task from lists |
67
- | `cup field <id> [--set "Name" value] [--remove "Name"] [--json]` | Set/remove custom field values |
68
- | `cup delete <id> [--confirm] [--json]` | Delete a task (DESTRUCTIVE, irreversible) |
69
- | `cup tag <id> [--add tags] [--remove tags] [--json]` | Add/remove tags on a task |
70
- | `cup checklist view <id> [--json]` | View checklists on a task |
71
- | `cup checklist create <id> <name> [--json]` | Create a checklist |
72
- | `cup checklist delete <checklistId> [--json]` | Delete a checklist |
73
- | `cup checklist add-item <checklistId> <name> [--json]` | Add item to a checklist |
74
- | `cup checklist edit-item <checklistId> <itemId> [--name n] [--resolved] [--unresolved] [--assignee id] [--json]` | Edit a checklist item |
75
- | `cup checklist delete-item <checklistId> <itemId> [--json]` | Delete a checklist item |
76
- | `cup comment-delete <commentId> [--json]` | Delete a comment |
77
- | `cup replies <commentId> [--json]` | List threaded replies on a comment |
78
- | `cup reply <commentId> -m text [--notify-all] [--json]` | Reply to a comment |
79
- | `cup link <taskId> <linksTo> [--remove] [--json]` | Add or remove link between tasks |
80
- | `cup attach <taskId> <filePath> [--json]` | Upload file attachment to a task |
81
- | `cup time start <taskId> [-d desc] [--json]` | Start tracking time on a task |
82
- | `cup time stop [--json]` | Stop the running timer |
83
- | `cup time status [--json]` | Show currently running timer |
84
- | `cup time log <taskId> <duration> [-d desc] [--json]` | Log manual time entry (e.g. "2h", "30m") |
85
- | `cup time list [--days n] [--task id] [--json]` | List recent time entries |
86
- | `cup config get <key>` / `cup config set <key> <value>` / `cup config path` | Manage CLI config |
87
- | `cup completion <shell>` | Shell completions (bash/zsh/fish) |
58
+ | Command | What it does |
59
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
60
+ | `cup update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id\|me] [--parent id] [--json]` | Update task fields (desc supports markdown) |
61
+ | `cup create -n name [-l listId] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--time-estimate t] [--assignee id\|me] [--tags t] [--custom-item-id n] [--json]` | Create task (desc supports markdown) |
62
+ | `cup comment <id> -m text [--notify-all] [--json]` | Post comment on task |
63
+ | `cup comment-edit <commentId> -m text [--resolved] [--unresolved] [--json]` | Edit an existing comment |
64
+ | `cup assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users |
65
+ | `cup depend <id> [--on taskId] [--blocks taskId] [--remove] [--json]` | Add/remove task dependencies |
66
+ | `cup move <id> [--to listId] [--remove listId] [--json]` | Add/remove task from lists |
67
+ | `cup field <id> [--set "Name" value] [--remove "Name"] [--json]` | Set/remove custom field values |
68
+ | `cup delete <id> [--confirm] [--json]` | Delete a task (DESTRUCTIVE, irreversible) |
69
+ | `cup tag <id> [--add tags] [--remove tags] [--json]` | Add/remove tags on a task |
70
+ | `cup checklist view <id> [--json]` | View checklists on a task |
71
+ | `cup checklist create <id> <name> [--json]` | Create a checklist |
72
+ | `cup checklist delete <checklistId> [--json]` | Delete a checklist |
73
+ | `cup checklist add-item <checklistId> <name> [--json]` | Add item to a checklist |
74
+ | `cup checklist edit-item <checklistId> <itemId> [--name n] [--resolved] [--unresolved] [--assignee id] [--json]` | Edit a checklist item |
75
+ | `cup checklist delete-item <checklistId> <itemId> [--json]` | Delete a checklist item |
76
+ | `cup comment-delete <commentId> [--json]` | Delete a comment |
77
+ | `cup replies <commentId> [--json]` | List threaded replies on a comment |
78
+ | `cup reply <commentId> -m text [--notify-all] [--json]` | Reply to a comment |
79
+ | `cup link <taskId> <linksTo> [--remove] [--json]` | Add or remove link between tasks |
80
+ | `cup attach <taskId> <filePath> [--json]` | Upload file attachment to a task |
81
+ | `cup time start <taskId> [-d desc] [--json]` | Start tracking time on a task |
82
+ | `cup time stop [--json]` | Stop the running timer |
83
+ | `cup time status [--json]` | Show currently running timer |
84
+ | `cup time log <taskId> <duration> [-d desc] [--json]` | Log manual time entry (e.g. "2h", "30m") |
85
+ | `cup time list [--days n] [--task id] [--json]` | List recent time entries |
86
+ | `cup config get <key>` / `cup config set <key> <value>` / `cup config path` | Manage CLI config (keys: apiToken, teamId, sprintFolderId) |
87
+ | `cup completion <shell>` | Shell completions (bash/zsh/fish) |
88
88
 
89
89
  ## Quick Reference
90
90
 
91
- | Topic | Detail |
92
- | --------------------------- | ------------------------------------------------------------------------------------------------------ |
93
- | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
94
- | `--type` | Filter by task type: `task` (regular), or custom type name/ID (e.g. `initiative`, `Bug`) |
95
- | `--list` on create | Optional when `--parent` is given (auto-detected) |
96
- | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
97
- | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
98
- | `--due-date` | `YYYY-MM-DD` format |
99
- | `--assignee` | User ID or `me` (on `cup create`, `cup update`, `cup assign`) |
100
- | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
101
- | `--time-estimate` | Duration format: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
102
- | `--custom-item-id` | Custom task type ID for `cup create` (e.g. `1` for initiative) |
103
- | `--on` / `--blocks` | Task dependency direction (used with `cup depend`) |
104
- | `--to` / `--remove` | List ID to add/remove task (used with `cup move`) |
105
- | `cup field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), date (YYYY-MM-DD), url, email |
106
- | `cup field` | Field names resolved case-insensitively; errors list available fields/options |
107
- | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
108
- | `cup tag --add/--remove` | Comma-separated tag names (e.g. `--add "bug,frontend"`) |
109
- | `--space` | Partial name match or exact ID |
110
- | `--name` | Partial match, case-insensitive |
111
- | `--include-closed` | Include closed/done tasks (on `tasks`, `assigned`, `subtasks`, `sprint`, `search`, `inbox`, `overdue`) |
112
- | `cup assign --to me` | Shorthand for your own user ID |
113
- | `cup search` | Matches all query words against task name, case-insensitive |
114
- | `cup sprint` | Auto-detects active sprint via view API and date range parsing |
115
- | `cup summary` | Categories: completed (done/complete/closed within N hours), in progress, overdue |
116
- | `cup overdue` | Excludes closed tasks, sorted most overdue first |
117
- | `cup open` | Tries task ID first, falls back to name search |
118
- | `cup checklist` | Full CRUD for task checklists: view, create, delete, add-item, edit-item, delete-item |
119
- | `cup time` | Track time: start/stop timer, log entries, list history. Duration format: "2h", "30m", "1h30m" |
120
- | `cup comment-edit` | Edit comment text and resolution status |
121
- | `cup comment-delete` | Delete a comment |
122
- | `cup replies` / `cup reply` | View and post threaded comment replies |
123
- | Custom task IDs | Auto-detected by format (`PROJ-123`). Uses `teamId` from config. All commands support them |
124
- | `cup link` + custom IDs | Both IDs must be the same type (both custom or both native). Mixing may not work |
125
- | `cup link` | Link/unlink tasks (different from dependencies) |
126
- | `cup attach` | Upload files to tasks. Attachments shown in `cup task` detail view |
127
- | `cup task` | Shows custom fields, checklists, attachments, dependencies, and linked tasks in detail view |
128
- | `cup lists` | Discovers list IDs needed for `--list` and `cup create -l` |
129
- | Errors | stderr with exit code 1 |
130
- | Parsing | Strict - excess/unknown arguments rejected |
91
+ | Topic | Detail |
92
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | Task IDs | Native (`abc123def`) or custom (`PROJ-123`). Custom IDs auto-detected by `PREFIX-DIGITS` format |
94
+ | `--type` | Filter by task type: `task` (regular), or custom type name/ID (e.g. `initiative`, `Bug`) |
95
+ | `--list` on create | Optional when `--parent` is given (auto-detected) |
96
+ | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
97
+ | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
98
+ | `--due-date` | `YYYY-MM-DD` format |
99
+ | `--assignee` | User ID or `me` (on `cup create`, `cup update`, `cup assign`) |
100
+ | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
101
+ | `--time-estimate` | Duration format: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
102
+ | `--custom-item-id` | Custom task type ID for `cup create` (e.g. `1` for initiative) |
103
+ | `--on` / `--blocks` | Task dependency direction (used with `cup depend`) |
104
+ | `--to` / `--remove` | List ID to add/remove task (used with `cup move`) |
105
+ | `cup field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), date (YYYY-MM-DD), url, email |
106
+ | `cup field` | Field names resolved case-insensitively; errors list available fields/options |
107
+ | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
108
+ | `cup tag --add/--remove` | Comma-separated tag names (e.g. `--add "bug,frontend"`) |
109
+ | `--space` | Partial name match or exact ID |
110
+ | `--name` | Partial match, case-insensitive |
111
+ | `--include-closed` | Include closed/done tasks (on `tasks`, `assigned`, `subtasks`, `sprint`, `search`, `inbox`, `overdue`) |
112
+ | `cup assign --to me` | Shorthand for your own user ID |
113
+ | `cup search` | Matches all query words against task name, case-insensitive |
114
+ | `cup sprint` | Auto-detects active sprint by searching for folders named sprint/iteration/cycle/scrum, parses multiple date formats (US, ISO, month-day, European), prompts in TTY when ambiguous. Override with `--folder <id>` or `cup config set sprintFolderId <id>` |
115
+ | `cup summary` | Categories: completed (done/complete/closed within N hours), in progress, overdue |
116
+ | `cup overdue` | Excludes closed tasks, sorted most overdue first |
117
+ | `cup open` | Tries task ID first, falls back to name search |
118
+ | `cup checklist` | Full CRUD for task checklists: view, create, delete, add-item, edit-item, delete-item |
119
+ | `cup time` | Track time: start/stop timer, log entries, list history. Duration format: "2h", "30m", "1h30m" |
120
+ | `cup comment-edit` | Edit comment text and resolution status |
121
+ | `cup comment-delete` | Delete a comment |
122
+ | `cup replies` / `cup reply` | View and post threaded comment replies |
123
+ | Custom task IDs | Auto-detected by format (`PROJ-123`). Uses `teamId` from config. All commands support them |
124
+ | `cup link` + custom IDs | Both IDs must be the same type (both custom or both native). Mixing may not work |
125
+ | `cup link` | Link/unlink tasks (different from dependencies) |
126
+ | `cup attach` | Upload files to tasks. Attachments shown in `cup task` detail view |
127
+ | `cup task` | Shows custom fields, checklists, attachments, dependencies, and linked tasks in detail view |
128
+ | `cup lists` | Discovers list IDs needed for `--list` and `cup create -l` |
129
+ | Errors | stderr with exit code 1 |
130
+ | Parsing | Strict - excess/unknown arguments rejected |
131
131
 
132
132
  ## Agent Workflow Examples
133
133
 
@@ -203,6 +203,7 @@ cup spaces --name "Engineering" # find space ID by name
203
203
  cup lists <spaceId> # lists in a space (needs ID from cup spaces)
204
204
  cup sprints # all sprints across folders
205
205
  cup auth # verify token works
206
+ cup config set sprintFolderId <id> # pin sprint detection to a folder
206
207
  ```
207
208
 
208
209
  ### Standup