@krodak/clickup-cli 0.20.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.
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,21 +431,23 @@ 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) {
419
- throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cu init");
438
+ throw new Error("Config missing required field: apiToken.\nSet CU_API_TOKEN or run: cup init");
420
439
  }
421
440
  if (!apiToken.startsWith("pk_")) {
422
441
  throw new Error("Config apiToken must start with pk_. The configured token does not.");
423
442
  }
424
443
  const teamId = envTeamId || fileTeamId;
425
444
  if (!teamId) {
426
- throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cu init");
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,12 +2767,12 @@ 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
  `;
2680
2774
  }
2681
- function generateCompletion(shell, name = "cu") {
2775
+ function generateCompletion(shell, name = "cup") {
2682
2776
  switch (shell) {
2683
2777
  case "bash":
2684
2778
  return bashCompletion(name);
@@ -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,11 +3190,24 @@ 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);
3097
3209
  var { version } = require2("../package.json");
3098
- var programName = basename(process.argv[1] ?? "cu");
3210
+ var programName = basename(process.argv[1] ?? "cup");
3099
3211
  function wrapAction(fn) {
3100
3212
  return (...args) => {
3101
3213
  fn(...args).catch((err) => {
@@ -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
  );