@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +11 -10
- package/dist/index.js +237 -115
- package/package.json +1 -2
- package/skills/clickup-cli/SKILL.md +73 -72
|
@@ -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.
|
|
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
|
|
166
|
-
| ------------------------ |
|
|
167
|
-
| Active sprint tasks | `cup sprint`
|
|
168
|
-
| List all sprints | `cup sprints`
|
|
169
|
-
| Assigned tasks by status | `cup assigned`
|
|
170
|
-
| Standup summary | `cup summary`
|
|
171
|
-
| Overdue tasks | `cup overdue`
|
|
172
|
-
| Recently updated | `cup inbox`
|
|
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/
|
|
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.
|
|
458
|
-
const month = String(d.
|
|
459
|
-
const day = String(d.
|
|
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 ?
|
|
585
|
-
["Due Date", task.due_date ?
|
|
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 ?
|
|
596
|
-
["Updated", task.date_updated ?
|
|
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 ?
|
|
750
|
-
["Due", task.due_date ?
|
|
751
|
-
["Estimate", task.time_estimate ?
|
|
752
|
-
["Tracked", task.time_spent ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1236
|
-
const
|
|
1237
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 \`
|
|
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) =>
|
|
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 ? `${
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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} - ${
|
|
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(
|
|
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.
|
|
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/
|
|
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]`
|
|
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
|
|
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
|