@krodak/clickup-cli 0.7.0 → 0.8.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 +11 -0
- package/README.md +74 -30
- package/dist/index.js +319 -214
- package/package.json +3 -1
- package/skills/clickup-cli/SKILL.md +149 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clickup-cli",
|
|
3
|
+
"description": "Claude Code skills for the cu (ClickUp CLI) tool - manage tasks, sprints, initiatives, and comments",
|
|
4
|
+
"version": "0.6.1",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Krzysztof Rodak"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/krodak/clickup-cli",
|
|
9
|
+
"repository": "https://github.com/krodak/clickup-cli",
|
|
10
|
+
"license": "MIT"
|
|
11
|
+
}
|
package/README.md
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# cu - ClickUp CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A ClickUp CLI built for AI agents that also works well for humans. Outputs JSON when piped, interactive tables when run in a terminal.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[](https://nodejs.org)
|
|
7
|
-
[](./LICENSE)
|
|
8
|
-
[](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml)
|
|
5
|
+
## Quick start
|
|
9
6
|
|
|
10
7
|
```bash
|
|
11
8
|
npm install -g @krodak/clickup-cli # or: brew tap krodak/tap && brew install clickup-cli
|
|
12
9
|
cu init # walks you through API token + workspace setup
|
|
13
10
|
```
|
|
14
11
|
|
|
15
|
-
You need a ClickUp personal API token (`pk_...` from https://app.clickup.com/settings/apps).
|
|
12
|
+
You need Node 22+ and a ClickUp personal API token (`pk_...` from https://app.clickup.com/settings/apps).
|
|
16
13
|
|
|
17
14
|
## Using with AI agents
|
|
18
15
|
|
|
@@ -20,13 +17,21 @@ This is the primary use case. Install the tool, install the skill file, and your
|
|
|
20
17
|
|
|
21
18
|
### 1. Install the skill
|
|
22
19
|
|
|
23
|
-
The repo includes a skill file at `
|
|
20
|
+
The repo includes a skill file at `skills/clickup-cli/SKILL.md` that teaches agents all available commands and when to use them. It's also packaged as a Claude Code plugin.
|
|
24
21
|
|
|
25
|
-
**Claude Code:**
|
|
22
|
+
**Claude Code (plugin - recommended):**
|
|
23
|
+
|
|
24
|
+
The repo ships as a Claude Code plugin. Point Claude Code at the repo or installed npm package:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
claude --plugin-dir ./node_modules/@krodak/clickup-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or copy the skill manually:
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
mkdir -p ~/.claude/skills/clickup
|
|
29
|
-
cp
|
|
34
|
+
cp skills/clickup-cli/SKILL.md ~/.claude/skills/clickup/SKILL.md
|
|
30
35
|
```
|
|
31
36
|
|
|
32
37
|
Then reference it in your `CLAUDE.md` or project instructions.
|
|
@@ -35,12 +40,12 @@ Then reference it in your `CLAUDE.md` or project instructions.
|
|
|
35
40
|
|
|
36
41
|
```bash
|
|
37
42
|
mkdir -p ~/.config/opencode/skills/clickup
|
|
38
|
-
cp
|
|
43
|
+
cp skills/clickup-cli/SKILL.md ~/.config/opencode/skills/clickup/SKILL.md
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
**Codex / other agents:**
|
|
42
47
|
|
|
43
|
-
Copy the contents of `
|
|
48
|
+
Copy the contents of `skills/clickup-cli/SKILL.md` into your system prompt or project instructions. It's a standalone markdown document.
|
|
44
49
|
|
|
45
50
|
### 2. Talk to your agent
|
|
46
51
|
|
|
@@ -84,13 +89,13 @@ cu update abc123 -s "done" # update status
|
|
|
84
89
|
cu assign abc123 --to me # assign yourself
|
|
85
90
|
```
|
|
86
91
|
|
|
87
|
-
Pass `--json` to any
|
|
92
|
+
Pass `--json` to any command to get raw JSON output instead of the interactive UI.
|
|
88
93
|
|
|
89
|
-
When output is piped (no TTY), all commands output
|
|
94
|
+
When output is piped (no TTY), all commands automatically output JSON. All commands support `--json` to force JSON output even in a terminal.
|
|
90
95
|
|
|
91
96
|
## Commands
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
24 commands total. All support `--help` for full flag details.
|
|
94
99
|
|
|
95
100
|
### `cu init`
|
|
96
101
|
|
|
@@ -136,6 +141,15 @@ cu sprint --status "in progress"
|
|
|
136
141
|
cu sprint --json
|
|
137
142
|
```
|
|
138
143
|
|
|
144
|
+
### `cu sprints`
|
|
145
|
+
|
|
146
|
+
List all sprints across sprint folders. Marks the currently active sprint.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cu sprints
|
|
150
|
+
cu sprints --json
|
|
151
|
+
```
|
|
152
|
+
|
|
139
153
|
### `cu assigned`
|
|
140
154
|
|
|
141
155
|
All tasks assigned to me, grouped by pipeline stage (code review, in progress, to do, etc.).
|
|
@@ -158,15 +172,13 @@ cu inbox --json
|
|
|
158
172
|
|
|
159
173
|
### `cu task <id>`
|
|
160
174
|
|
|
161
|
-
Get task details. Pretty summary in terminal, JSON when piped.
|
|
175
|
+
Get task details including custom fields. Pretty summary in terminal, JSON when piped.
|
|
162
176
|
|
|
163
177
|
```bash
|
|
164
178
|
cu task abc123
|
|
165
179
|
cu task abc123 --json
|
|
166
180
|
```
|
|
167
181
|
|
|
168
|
-
**Note:** When piped, `cu task` outputs a structured Markdown summary of the task. For the full raw API response with all fields (custom fields, checklists, etc.), use `--json`.
|
|
169
|
-
|
|
170
182
|
### `cu subtasks <id>`
|
|
171
183
|
|
|
172
184
|
List subtasks of a task or initiative.
|
|
@@ -188,16 +200,18 @@ cu update abc123 --priority high
|
|
|
188
200
|
cu update abc123 --due-date 2025-03-15
|
|
189
201
|
cu update abc123 --assignee 12345
|
|
190
202
|
cu update abc123 -n "New name" -s "done" --priority urgent
|
|
203
|
+
cu update abc123 -s "in progress" --json
|
|
191
204
|
```
|
|
192
205
|
|
|
193
|
-
| Flag | Description
|
|
194
|
-
| -------------------------- |
|
|
195
|
-
| `-n, --name <text>` | New task name
|
|
196
|
-
| `-d, --description <text>` | New description (markdown supported)
|
|
197
|
-
| `-s, --status <status>` | New status (e.g. `"
|
|
198
|
-
| `--priority <level>` | Priority: `urgent`, `high`, `normal`, `low` (or 1-4)
|
|
199
|
-
| `--due-date <date>` | Due date (`YYYY-MM-DD`)
|
|
200
|
-
| `--assignee <userId>` | Add assignee by numeric user ID
|
|
206
|
+
| Flag | Description |
|
|
207
|
+
| -------------------------- | --------------------------------------------------------------------------- |
|
|
208
|
+
| `-n, --name <text>` | New task name |
|
|
209
|
+
| `-d, --description <text>` | New description (markdown supported) |
|
|
210
|
+
| `-s, --status <status>` | New status, supports fuzzy matching (e.g. `"prog"` matches `"in progress"`) |
|
|
211
|
+
| `--priority <level>` | Priority: `urgent`, `high`, `normal`, `low` (or 1-4) |
|
|
212
|
+
| `--due-date <date>` | Due date (`YYYY-MM-DD`) |
|
|
213
|
+
| `--assignee <userId>` | Add assignee by numeric user ID |
|
|
214
|
+
| `--json` | Force JSON output even in terminal |
|
|
201
215
|
|
|
202
216
|
### `cu create`
|
|
203
217
|
|
|
@@ -209,6 +223,7 @@ cu create -n "Subtask name" -p <parentTaskId> # --list auto-detected
|
|
|
209
223
|
cu create -n "Task" -l <listId> -d "desc" -s "open"
|
|
210
224
|
cu create -n "Task" -l <listId> --priority high --due-date 2025-06-01
|
|
211
225
|
cu create -n "Task" -l <listId> --assignee 12345 --tags "bug,frontend"
|
|
226
|
+
cu create -n "Fix bug" -l <listId> --json
|
|
212
227
|
```
|
|
213
228
|
|
|
214
229
|
| Flag | Required | Description |
|
|
@@ -222,6 +237,7 @@ cu create -n "Task" -l <listId> --assignee 12345 --tags "bug,frontend"
|
|
|
222
237
|
| `--due-date <date>` | no | Due date (`YYYY-MM-DD`) |
|
|
223
238
|
| `--assignee <userId>` | no | Assignee by numeric user ID |
|
|
224
239
|
| `--tags <tags>` | no | Comma-separated tag names |
|
|
240
|
+
| `--json` | no | Force JSON output even in terminal |
|
|
225
241
|
|
|
226
242
|
### `cu comment <id>`
|
|
227
243
|
|
|
@@ -240,6 +256,15 @@ cu comments abc123
|
|
|
240
256
|
cu comments abc123 --json
|
|
241
257
|
```
|
|
242
258
|
|
|
259
|
+
### `cu activity <id>`
|
|
260
|
+
|
|
261
|
+
View task details and comment history together. Combines `cu task` and `cu comments` into a single view.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
cu activity abc123
|
|
265
|
+
cu activity abc123 --json
|
|
266
|
+
```
|
|
267
|
+
|
|
243
268
|
### `cu lists <spaceId>`
|
|
244
269
|
|
|
245
270
|
List all lists in a space, including lists inside folders. Useful for discovering list IDs needed by `--list` filter and `cu create -l`.
|
|
@@ -284,6 +309,17 @@ cu open abc123 --json
|
|
|
284
309
|
|
|
285
310
|
If the query matches multiple tasks by name, all matches are listed and the first is opened.
|
|
286
311
|
|
|
312
|
+
### `cu search <query>`
|
|
313
|
+
|
|
314
|
+
Search my tasks by name. Supports multi-word queries with case-insensitive matching. Status filter supports fuzzy matching.
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
cu search "login bug"
|
|
318
|
+
cu search auth
|
|
319
|
+
cu search "payment flow" --json
|
|
320
|
+
cu search auth --status "prog" # fuzzy matches "in progress"
|
|
321
|
+
```
|
|
322
|
+
|
|
287
323
|
### `cu summary`
|
|
288
324
|
|
|
289
325
|
Daily standup helper. Shows tasks grouped into: recently completed, in progress, and overdue.
|
|
@@ -326,6 +362,15 @@ cu assign abc123 --to me --json
|
|
|
326
362
|
| `--remove <userId>` | Remove assignee (user ID or `me`) |
|
|
327
363
|
| `--json` | Force JSON output |
|
|
328
364
|
|
|
365
|
+
### `cu auth`
|
|
366
|
+
|
|
367
|
+
Check authentication status. Validates your API token and shows your user info.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
cu auth
|
|
371
|
+
cu auth --json
|
|
372
|
+
```
|
|
373
|
+
|
|
329
374
|
### `cu config`
|
|
330
375
|
|
|
331
376
|
Manage CLI configuration.
|
|
@@ -365,11 +410,10 @@ cu completion fish > ~/.config/fish/completions/cu.fish # Fish
|
|
|
365
410
|
|
|
366
411
|
Environment variables override config file values:
|
|
367
412
|
|
|
368
|
-
| Variable | Description
|
|
369
|
-
| -------------- |
|
|
370
|
-
| `CU_API_TOKEN` | ClickUp personal API token (`pk_`)
|
|
371
|
-
| `CU_TEAM_ID` | Workspace (team) ID
|
|
372
|
-
| `CU_OUTPUT` | Set to `json` to force JSON output when piped (default: markdown) |
|
|
413
|
+
| Variable | Description |
|
|
414
|
+
| -------------- | ---------------------------------- |
|
|
415
|
+
| `CU_API_TOKEN` | ClickUp personal API token (`pk_`) |
|
|
416
|
+
| `CU_TEAM_ID` | Workspace (team) ID |
|
|
373
417
|
|
|
374
418
|
When both are set, the config file is not required. Useful for CI/CD and containerized agents.
|
|
375
419
|
|
package/dist/index.js
CHANGED
|
@@ -61,9 +61,13 @@ function getConfigPath() {
|
|
|
61
61
|
function writeConfig(config) {
|
|
62
62
|
const dir = configDir();
|
|
63
63
|
if (!fs.existsSync(dir)) {
|
|
64
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
const filePath = join(dir, "config.json");
|
|
67
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
mode: 384
|
|
70
|
+
});
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// src/api.ts
|
|
@@ -187,6 +191,9 @@ var ClickUpClient = class {
|
|
|
187
191
|
const data = await this.request("/team");
|
|
188
192
|
return data.teams ?? [];
|
|
189
193
|
}
|
|
194
|
+
async getSpaceWithStatuses(spaceId) {
|
|
195
|
+
return this.request(`/space/${spaceId}`);
|
|
196
|
+
}
|
|
190
197
|
async getSpaces(teamId) {
|
|
191
198
|
const data = await this.request(`/team/${teamId}/space?archived=false`);
|
|
192
199
|
return data.spaces ?? [];
|
|
@@ -220,11 +227,6 @@ import chalk from "chalk";
|
|
|
220
227
|
function isTTY() {
|
|
221
228
|
return Boolean(process.stdout.isTTY);
|
|
222
229
|
}
|
|
223
|
-
function shouldOutputJson(forceJson) {
|
|
224
|
-
if (forceJson) return true;
|
|
225
|
-
if (process.env["CU_OUTPUT"] === "json") return true;
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
230
|
function cell(value, width) {
|
|
229
231
|
if (value.length > width) return value.slice(0, width - 1) + "\u2026";
|
|
230
232
|
return value.padEnd(width);
|
|
@@ -257,128 +259,6 @@ var TASK_COLUMNS = [
|
|
|
257
259
|
{ key: "list", label: "LIST" }
|
|
258
260
|
];
|
|
259
261
|
|
|
260
|
-
// src/markdown.ts
|
|
261
|
-
function escapeCell(value) {
|
|
262
|
-
return value.replace(/\|/g, "\\|");
|
|
263
|
-
}
|
|
264
|
-
function formatMarkdownTable(rows, columns) {
|
|
265
|
-
const header = "| " + columns.map((c) => c.label).join(" | ") + " |";
|
|
266
|
-
const divider = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
267
|
-
const lines = [header, divider];
|
|
268
|
-
for (const row of rows) {
|
|
269
|
-
const cells = columns.map((c) => escapeCell(String(row[c.key] ?? "")));
|
|
270
|
-
lines.push("| " + cells.join(" | ") + " |");
|
|
271
|
-
}
|
|
272
|
-
return lines.join("\n");
|
|
273
|
-
}
|
|
274
|
-
var TASK_MD_COLUMNS = [
|
|
275
|
-
{ key: "id", label: "ID" },
|
|
276
|
-
{ key: "name", label: "Name" },
|
|
277
|
-
{ key: "status", label: "Status" },
|
|
278
|
-
{ key: "list", label: "List" }
|
|
279
|
-
];
|
|
280
|
-
function formatTasksMarkdown(tasks) {
|
|
281
|
-
if (tasks.length === 0) return "No tasks found.";
|
|
282
|
-
return formatMarkdownTable(tasks, TASK_MD_COLUMNS);
|
|
283
|
-
}
|
|
284
|
-
function formatCommentsMarkdown(comments) {
|
|
285
|
-
if (comments.length === 0) return "No comments found.";
|
|
286
|
-
return comments.map((c) => `**${c.user}** (${c.date})
|
|
287
|
-
|
|
288
|
-
${c.text}`).join("\n\n---\n\n");
|
|
289
|
-
}
|
|
290
|
-
var LIST_MD_COLUMNS = [
|
|
291
|
-
{ key: "id", label: "ID" },
|
|
292
|
-
{ key: "name", label: "Name" },
|
|
293
|
-
{ key: "folder", label: "Folder" }
|
|
294
|
-
];
|
|
295
|
-
function formatListsMarkdown(lists) {
|
|
296
|
-
if (lists.length === 0) return "No lists found.";
|
|
297
|
-
return formatMarkdownTable(lists, LIST_MD_COLUMNS);
|
|
298
|
-
}
|
|
299
|
-
var SPACE_MD_COLUMNS = [
|
|
300
|
-
{ key: "id", label: "ID" },
|
|
301
|
-
{ key: "name", label: "Name" }
|
|
302
|
-
];
|
|
303
|
-
function formatSpacesMarkdown(spaces) {
|
|
304
|
-
if (spaces.length === 0) return "No spaces found.";
|
|
305
|
-
return formatMarkdownTable(spaces, SPACE_MD_COLUMNS);
|
|
306
|
-
}
|
|
307
|
-
function formatGroupedTasksMarkdown(groups) {
|
|
308
|
-
const sections = groups.filter((g) => g.tasks.length > 0).map((g) => `## ${g.label}
|
|
309
|
-
|
|
310
|
-
${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
|
|
311
|
-
if (sections.length === 0) return "No tasks found.";
|
|
312
|
-
return sections.join("\n\n");
|
|
313
|
-
}
|
|
314
|
-
function formatDate(ms) {
|
|
315
|
-
const d = new Date(Number(ms));
|
|
316
|
-
const year = d.getUTCFullYear();
|
|
317
|
-
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
318
|
-
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
319
|
-
return `${year}-${month}-${day}`;
|
|
320
|
-
}
|
|
321
|
-
function formatDuration(ms) {
|
|
322
|
-
const totalMinutes = Math.floor(ms / 6e4);
|
|
323
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
324
|
-
const minutes = totalMinutes % 60;
|
|
325
|
-
return `${hours}h ${minutes}m`;
|
|
326
|
-
}
|
|
327
|
-
function formatTaskDetailMarkdown(task) {
|
|
328
|
-
const lines = [`# ${task.name}`, ""];
|
|
329
|
-
const isInitiative2 = (task.custom_item_id ?? 0) !== 0;
|
|
330
|
-
const fields = [
|
|
331
|
-
["ID", task.id],
|
|
332
|
-
["Status", task.status.status],
|
|
333
|
-
["Type", isInitiative2 ? "initiative" : "task"],
|
|
334
|
-
["List", task.list.name],
|
|
335
|
-
["URL", task.url],
|
|
336
|
-
[
|
|
337
|
-
"Assignees",
|
|
338
|
-
task.assignees.length > 0 ? task.assignees.map((a) => a.username).join(", ") : void 0
|
|
339
|
-
],
|
|
340
|
-
["Priority", task.priority?.priority],
|
|
341
|
-
["Parent", task.parent ?? void 0],
|
|
342
|
-
["Start Date", task.start_date ? formatDate(task.start_date) : void 0],
|
|
343
|
-
["Due Date", task.due_date ? formatDate(task.due_date) : void 0],
|
|
344
|
-
[
|
|
345
|
-
"Time Estimate",
|
|
346
|
-
task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
|
|
347
|
-
],
|
|
348
|
-
[
|
|
349
|
-
"Time Spent",
|
|
350
|
-
task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
|
|
351
|
-
],
|
|
352
|
-
["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
|
|
353
|
-
["Created", task.date_created ? formatDate(task.date_created) : void 0],
|
|
354
|
-
["Updated", task.date_updated ? formatDate(task.date_updated) : void 0]
|
|
355
|
-
];
|
|
356
|
-
for (const [label, value] of fields) {
|
|
357
|
-
if (value != null && value !== "") {
|
|
358
|
-
lines.push(`**${label}:** ${value}`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (task.description) {
|
|
362
|
-
lines.push("", "## Description", "", task.description);
|
|
363
|
-
}
|
|
364
|
-
return lines.join("\n");
|
|
365
|
-
}
|
|
366
|
-
function formatUpdateConfirmation(id, name) {
|
|
367
|
-
return `Updated task ${id}: "${name}"`;
|
|
368
|
-
}
|
|
369
|
-
function formatCreateConfirmation(id, name, url) {
|
|
370
|
-
return `Created task ${id}: "${name}" - ${url}`;
|
|
371
|
-
}
|
|
372
|
-
function formatCommentConfirmation(id) {
|
|
373
|
-
return `Comment posted (id: ${id})`;
|
|
374
|
-
}
|
|
375
|
-
function formatAssignConfirmation(taskId, opts) {
|
|
376
|
-
const parts = [];
|
|
377
|
-
if (opts.to) parts.push(`Assigned ${opts.to} to ${taskId}`);
|
|
378
|
-
if (opts.remove) parts.push(`Removed ${opts.remove} from ${taskId}`);
|
|
379
|
-
return parts.join("; ");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
262
|
// src/interactive.ts
|
|
383
263
|
import { execFileSync } from "child_process";
|
|
384
264
|
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
@@ -422,6 +302,40 @@ function descriptionPreview(text, maxLines = 3) {
|
|
|
422
302
|
${chalk2.dim(`... (${lines.length - maxLines} more lines)`)}`;
|
|
423
303
|
return result;
|
|
424
304
|
}
|
|
305
|
+
function stringifyFieldValue(value) {
|
|
306
|
+
if (typeof value === "string") return value;
|
|
307
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
308
|
+
return JSON.stringify(value);
|
|
309
|
+
}
|
|
310
|
+
function formatCustomFieldValue(field) {
|
|
311
|
+
if (field.value === null || field.value === void 0) return null;
|
|
312
|
+
const options = field.type_config?.options;
|
|
313
|
+
switch (field.type) {
|
|
314
|
+
case "drop_down": {
|
|
315
|
+
if (!options) return stringifyFieldValue(field.value);
|
|
316
|
+
const match = options.find((o) => o.id === Number(field.value));
|
|
317
|
+
return match?.name ?? stringifyFieldValue(field.value);
|
|
318
|
+
}
|
|
319
|
+
case "labels": {
|
|
320
|
+
if (!Array.isArray(field.value) || !options) return stringifyFieldValue(field.value);
|
|
321
|
+
const names = field.value.map((id) => options.find((o) => o.id === id)?.name).filter((n) => n !== void 0);
|
|
322
|
+
return names.length > 0 ? names.join(", ") : null;
|
|
323
|
+
}
|
|
324
|
+
case "date": {
|
|
325
|
+
const ts = Number(field.value);
|
|
326
|
+
if (!Number.isFinite(ts)) return stringifyFieldValue(field.value);
|
|
327
|
+
return new Date(ts).toLocaleDateString("en-US", {
|
|
328
|
+
month: "short",
|
|
329
|
+
day: "numeric",
|
|
330
|
+
year: "numeric"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
case "checkbox":
|
|
334
|
+
return field.value === true || field.value === "true" ? "Yes" : "No";
|
|
335
|
+
default:
|
|
336
|
+
return stringifyFieldValue(field.value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
425
339
|
function formatTaskDetail(task) {
|
|
426
340
|
const lines = [];
|
|
427
341
|
const isInitiative2 = (task.custom_item_id ?? 0) !== 0;
|
|
@@ -451,6 +365,16 @@ function formatTaskDetail(task) {
|
|
|
451
365
|
if (!value) continue;
|
|
452
366
|
lines.push(` ${chalk2.bold(label.padEnd(maxLabel + 1))} ${value}`);
|
|
453
367
|
}
|
|
368
|
+
if (task.custom_fields?.length) {
|
|
369
|
+
const formatted = task.custom_fields.map((f) => [f.name, formatCustomFieldValue(f)]).filter((pair) => pair[1] !== null);
|
|
370
|
+
if (formatted.length > 0) {
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push(chalk2.bold("Custom Fields"));
|
|
373
|
+
for (const [name, value] of formatted) {
|
|
374
|
+
lines.push(` ${chalk2.bold(name)} ${value}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
454
378
|
if (task.text_content?.trim()) {
|
|
455
379
|
lines.push("");
|
|
456
380
|
lines.push(descriptionPreview(task.text_content));
|
|
@@ -566,14 +490,10 @@ async function fetchMyTasks(config, opts = {}) {
|
|
|
566
490
|
return filtered.map(summarize);
|
|
567
491
|
}
|
|
568
492
|
async function printTasks(tasks, forceJson, config) {
|
|
569
|
-
if (
|
|
493
|
+
if (forceJson || !isTTY()) {
|
|
570
494
|
console.log(JSON.stringify(tasks, null, 2));
|
|
571
495
|
return;
|
|
572
496
|
}
|
|
573
|
-
if (!isTTY()) {
|
|
574
|
-
console.log(formatTasksMarkdown(tasks));
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
497
|
if (tasks.length === 0) {
|
|
578
498
|
console.log("No tasks found.");
|
|
579
499
|
return;
|
|
@@ -586,6 +506,19 @@ async function printTasks(tasks, forceJson, config) {
|
|
|
586
506
|
await showDetailsAndOpen(selected, fetchTask);
|
|
587
507
|
}
|
|
588
508
|
|
|
509
|
+
// src/status.ts
|
|
510
|
+
function matchStatus(input, statuses) {
|
|
511
|
+
if (!input) return null;
|
|
512
|
+
const lower = input.toLowerCase();
|
|
513
|
+
const exact = statuses.find((s) => s.toLowerCase() === lower);
|
|
514
|
+
if (exact) return exact;
|
|
515
|
+
const startsWith = statuses.find((s) => s.toLowerCase().startsWith(lower));
|
|
516
|
+
if (startsWith) return startsWith;
|
|
517
|
+
const contains = statuses.find((s) => s.toLowerCase().includes(lower));
|
|
518
|
+
if (contains) return contains;
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
589
522
|
// src/commands/update.ts
|
|
590
523
|
var PRIORITY_MAP = {
|
|
591
524
|
urgent: 1,
|
|
@@ -631,12 +564,30 @@ function buildUpdatePayload(opts) {
|
|
|
631
564
|
function hasUpdateFields(options) {
|
|
632
565
|
return options.name !== void 0 || options.description !== void 0 || options.status !== void 0 || options.priority !== void 0 || options.due_date !== void 0 || options.assignees !== void 0;
|
|
633
566
|
}
|
|
567
|
+
async function resolveStatus(client, taskId, statusInput) {
|
|
568
|
+
const task = await client.getTask(taskId);
|
|
569
|
+
if (!task.space) return statusInput;
|
|
570
|
+
const space = await client.getSpaceWithStatuses(task.space.id);
|
|
571
|
+
const available = space.statuses.map((s) => s.status);
|
|
572
|
+
const matched = matchStatus(statusInput, available);
|
|
573
|
+
if (!matched) {
|
|
574
|
+
throw new Error(`No matching status for "${statusInput}". Available: ${available.join(", ")}`);
|
|
575
|
+
}
|
|
576
|
+
if (matched.toLowerCase() !== statusInput.toLowerCase()) {
|
|
577
|
+
process.stderr.write(`Status matched: "${statusInput}" -> "${matched}"
|
|
578
|
+
`);
|
|
579
|
+
}
|
|
580
|
+
return matched;
|
|
581
|
+
}
|
|
634
582
|
async function updateTask(config, taskId, options) {
|
|
635
583
|
if (!hasUpdateFields(options))
|
|
636
584
|
throw new Error(
|
|
637
585
|
"Provide at least one of: --name, --description, --status, --priority, --due-date, --assignee"
|
|
638
586
|
);
|
|
639
587
|
const client = new ClickUpClient(config);
|
|
588
|
+
if (options.status !== void 0) {
|
|
589
|
+
options.status = await resolveStatus(client, taskId, options.status);
|
|
590
|
+
}
|
|
640
591
|
const task = await client.updateTask(taskId, options);
|
|
641
592
|
return { id: task.id, name: task.name };
|
|
642
593
|
}
|
|
@@ -814,6 +765,83 @@ async function runSprintCommand(config, opts) {
|
|
|
814
765
|
await printTasks(summaries, opts.json ?? false, config);
|
|
815
766
|
}
|
|
816
767
|
|
|
768
|
+
// src/commands/sprints.ts
|
|
769
|
+
var SPRINT_COLUMNS = [
|
|
770
|
+
{ key: "id", label: "ID" },
|
|
771
|
+
{ key: "sprint", label: "SPRINT", maxWidth: 60 },
|
|
772
|
+
{ key: "dates", label: "DATES" }
|
|
773
|
+
];
|
|
774
|
+
function formatDate(d) {
|
|
775
|
+
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
776
|
+
}
|
|
777
|
+
function buildSprintInfos(lists, folderName, today) {
|
|
778
|
+
return lists.map((list) => {
|
|
779
|
+
const dates = parseSprintDates(list.name);
|
|
780
|
+
const active = dates !== null && today >= dates.start && today <= dates.end;
|
|
781
|
+
return {
|
|
782
|
+
id: list.id,
|
|
783
|
+
name: list.name,
|
|
784
|
+
folder: folderName,
|
|
785
|
+
start: dates ? dates.start.toISOString() : null,
|
|
786
|
+
end: dates ? dates.end.toISOString() : null,
|
|
787
|
+
active
|
|
788
|
+
};
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async function listSprints(config, opts = {}) {
|
|
792
|
+
const client = new ClickUpClient(config);
|
|
793
|
+
const [myTasks, allSpaces] = await Promise.all([
|
|
794
|
+
client.getMyTasks(config.teamId),
|
|
795
|
+
client.getSpaces(config.teamId)
|
|
796
|
+
]);
|
|
797
|
+
let spaces;
|
|
798
|
+
if (opts.space) {
|
|
799
|
+
spaces = allSpaces.filter(
|
|
800
|
+
(s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
|
|
801
|
+
);
|
|
802
|
+
if (spaces.length === 0) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`No space matching "${opts.space}" found. Use \`cu spaces\` to list available spaces.`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
const mySpaceIds = new Set(
|
|
809
|
+
myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
|
|
810
|
+
);
|
|
811
|
+
spaces = findRelatedSpaces(mySpaceIds, allSpaces);
|
|
812
|
+
}
|
|
813
|
+
const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
|
|
814
|
+
const sprintFolders = foldersBySpace.flat().filter((f) => f.name.toLowerCase().includes("sprint"));
|
|
815
|
+
const today = /* @__PURE__ */ new Date();
|
|
816
|
+
const allSprints = [];
|
|
817
|
+
const listsByFolder = await Promise.all(
|
|
818
|
+
sprintFolders.map((folder) => client.getFolderLists(folder.id))
|
|
819
|
+
);
|
|
820
|
+
for (let i = 0; i < sprintFolders.length; i++) {
|
|
821
|
+
const folder = sprintFolders[i];
|
|
822
|
+
const lists = listsByFolder[i];
|
|
823
|
+
allSprints.push(...buildSprintInfos(lists, folder.name, today));
|
|
824
|
+
}
|
|
825
|
+
if (opts.json || !isTTY()) {
|
|
826
|
+
console.log(JSON.stringify(allSprints, null, 2));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (allSprints.length === 0) {
|
|
830
|
+
console.log("No sprints found.");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const rows = allSprints.map((s) => {
|
|
834
|
+
const dates = parseSprintDates(s.name);
|
|
835
|
+
const dateStr = dates ? `${formatDate(dates.start)} - ${formatDate(dates.end)}` : "";
|
|
836
|
+
return {
|
|
837
|
+
id: s.id,
|
|
838
|
+
sprint: s.active ? `* ${s.name}` : s.name,
|
|
839
|
+
dates: dateStr
|
|
840
|
+
};
|
|
841
|
+
});
|
|
842
|
+
console.log(formatTable(rows, SPRINT_COLUMNS));
|
|
843
|
+
}
|
|
844
|
+
|
|
817
845
|
// src/commands/subtasks.ts
|
|
818
846
|
async function fetchSubtasks(config, taskId) {
|
|
819
847
|
const client = new ClickUpClient(config);
|
|
@@ -851,14 +879,10 @@ async function fetchComments(config, taskId) {
|
|
|
851
879
|
}));
|
|
852
880
|
}
|
|
853
881
|
function printComments(comments, forceJson) {
|
|
854
|
-
if (
|
|
882
|
+
if (forceJson || !isTTY()) {
|
|
855
883
|
console.log(JSON.stringify(comments, null, 2));
|
|
856
884
|
return;
|
|
857
885
|
}
|
|
858
|
-
if (!isTTY()) {
|
|
859
|
-
console.log(formatCommentsMarkdown(comments));
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
886
|
if (comments.length === 0) {
|
|
863
887
|
console.log("No comments found.");
|
|
864
888
|
return;
|
|
@@ -896,14 +920,10 @@ async function fetchLists(config, spaceId, opts = {}) {
|
|
|
896
920
|
return results;
|
|
897
921
|
}
|
|
898
922
|
function printLists(lists, forceJson) {
|
|
899
|
-
if (
|
|
923
|
+
if (forceJson || !isTTY()) {
|
|
900
924
|
console.log(JSON.stringify(lists, null, 2));
|
|
901
925
|
return;
|
|
902
926
|
}
|
|
903
|
-
if (!isTTY()) {
|
|
904
|
-
console.log(formatListsMarkdown(lists));
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
927
|
if (lists.length === 0) {
|
|
908
928
|
console.log("No lists found.");
|
|
909
929
|
return;
|
|
@@ -971,7 +991,7 @@ async function fetchInbox(config, days = 30) {
|
|
|
971
991
|
async function printInbox(tasks, forceJson, config) {
|
|
972
992
|
const now = Date.now();
|
|
973
993
|
const groups = groupTasks(tasks, now);
|
|
974
|
-
if (
|
|
994
|
+
if (forceJson || !isTTY()) {
|
|
975
995
|
const jsonGroups = {};
|
|
976
996
|
for (const { key } of TIME_PERIODS) {
|
|
977
997
|
if (groups[key].length > 0) {
|
|
@@ -981,14 +1001,6 @@ async function printInbox(tasks, forceJson, config) {
|
|
|
981
1001
|
console.log(JSON.stringify(jsonGroups, null, 2));
|
|
982
1002
|
return;
|
|
983
1003
|
}
|
|
984
|
-
if (!isTTY()) {
|
|
985
|
-
const mdGroups = TIME_PERIODS.filter((p) => groups[p.key].length > 0).map((p) => ({
|
|
986
|
-
label: p.label,
|
|
987
|
-
tasks: groups[p.key]
|
|
988
|
-
}));
|
|
989
|
-
console.log(formatGroupedTasksMarkdown(mdGroups));
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
1004
|
if (tasks.length === 0) {
|
|
993
1005
|
console.log("No recently updated tasks.");
|
|
994
1006
|
return;
|
|
@@ -1020,11 +1032,7 @@ async function listSpaces(config, opts) {
|
|
|
1020
1032
|
);
|
|
1021
1033
|
spaces = spaces.filter((s) => mySpaceIds.has(s.id));
|
|
1022
1034
|
}
|
|
1023
|
-
if (
|
|
1024
|
-
console.log(JSON.stringify(spaces, null, 2));
|
|
1025
|
-
} else if (!isTTY()) {
|
|
1026
|
-
console.log(formatSpacesMarkdown(spaces.map((s) => ({ id: s.id, name: s.name }))));
|
|
1027
|
-
} else {
|
|
1035
|
+
if (!opts.json && isTTY()) {
|
|
1028
1036
|
const table = formatTable(
|
|
1029
1037
|
spaces.map((s) => ({ id: s.id, name: s.name })),
|
|
1030
1038
|
[
|
|
@@ -1033,6 +1041,8 @@ async function listSpaces(config, opts) {
|
|
|
1033
1041
|
]
|
|
1034
1042
|
);
|
|
1035
1043
|
console.log(table);
|
|
1044
|
+
} else {
|
|
1045
|
+
console.log(JSON.stringify(spaces, null, 2));
|
|
1036
1046
|
}
|
|
1037
1047
|
}
|
|
1038
1048
|
|
|
@@ -1087,7 +1097,7 @@ async function runAssignedCommand(config, opts) {
|
|
|
1087
1097
|
const client = new ClickUpClient(config);
|
|
1088
1098
|
const allTasks = await client.getMyTasks(config.teamId);
|
|
1089
1099
|
const groups = groupByStatus(allTasks, opts.includeClosed ?? false);
|
|
1090
|
-
if (
|
|
1100
|
+
if (opts.json || !isTTY()) {
|
|
1091
1101
|
const result = {};
|
|
1092
1102
|
for (const group of groups) {
|
|
1093
1103
|
result[group.status.toLowerCase()] = group.tasks.map((t) => toJsonTask(t, summarize(t)));
|
|
@@ -1095,14 +1105,6 @@ async function runAssignedCommand(config, opts) {
|
|
|
1095
1105
|
console.log(JSON.stringify(result, null, 2));
|
|
1096
1106
|
return;
|
|
1097
1107
|
}
|
|
1098
|
-
if (!isTTY()) {
|
|
1099
|
-
const mdGroups = groups.map((g) => ({
|
|
1100
|
-
label: g.status,
|
|
1101
|
-
tasks: g.tasks.map((t) => summarize(t))
|
|
1102
|
-
}));
|
|
1103
|
-
console.log(formatGroupedTasksMarkdown(mdGroups));
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
1108
|
if (groups.length === 0) {
|
|
1107
1109
|
console.log("No tasks found.");
|
|
1108
1110
|
return;
|
|
@@ -1128,13 +1130,13 @@ async function openTask(config, query, opts = {}) {
|
|
|
1128
1130
|
} catch {
|
|
1129
1131
|
}
|
|
1130
1132
|
if (task) {
|
|
1131
|
-
if (
|
|
1133
|
+
if (opts.json) {
|
|
1132
1134
|
console.log(JSON.stringify(task, null, 2));
|
|
1133
|
-
} else if (!isTTY()) {
|
|
1134
|
-
console.log(formatTaskDetailMarkdown(task));
|
|
1135
1135
|
} else {
|
|
1136
|
-
|
|
1137
|
-
|
|
1136
|
+
if (isTTY()) {
|
|
1137
|
+
console.log(task.name);
|
|
1138
|
+
console.log(task.url);
|
|
1139
|
+
}
|
|
1138
1140
|
openUrl(task.url);
|
|
1139
1141
|
}
|
|
1140
1142
|
return task;
|
|
@@ -1152,18 +1154,15 @@ async function openTask(config, query, opts = {}) {
|
|
|
1152
1154
|
}
|
|
1153
1155
|
console.log("Opening first match...");
|
|
1154
1156
|
}
|
|
1155
|
-
if (
|
|
1157
|
+
if (opts.json) {
|
|
1156
1158
|
const fullTask = await client.getTask(first.id);
|
|
1157
1159
|
console.log(JSON.stringify(fullTask, null, 2));
|
|
1158
1160
|
return fullTask;
|
|
1159
1161
|
}
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
console.log(
|
|
1163
|
-
return fullTask;
|
|
1162
|
+
if (isTTY()) {
|
|
1163
|
+
console.log(first.name);
|
|
1164
|
+
console.log(first.url);
|
|
1164
1165
|
}
|
|
1165
|
-
console.log(first.name);
|
|
1166
|
-
console.log(first.url);
|
|
1167
1166
|
openUrl(first.url);
|
|
1168
1167
|
return {
|
|
1169
1168
|
id: first.id,
|
|
@@ -1225,19 +1224,10 @@ async function runSummaryCommand(config, opts) {
|
|
|
1225
1224
|
const client = new ClickUpClient(config);
|
|
1226
1225
|
const allTasks = await client.getMyTasks(config.teamId);
|
|
1227
1226
|
const result = categorizeTasks(allTasks, opts.hours);
|
|
1228
|
-
if (
|
|
1227
|
+
if (opts.json || !isTTY()) {
|
|
1229
1228
|
console.log(JSON.stringify(result, null, 2));
|
|
1230
1229
|
return;
|
|
1231
1230
|
}
|
|
1232
|
-
if (!isTTY()) {
|
|
1233
|
-
const mdGroups = [
|
|
1234
|
-
{ label: "Completed Recently", tasks: result.completed },
|
|
1235
|
-
{ label: "In Progress", tasks: result.inProgress },
|
|
1236
|
-
{ label: "Overdue", tasks: result.overdue }
|
|
1237
|
-
];
|
|
1238
|
-
console.log(formatGroupedTasksMarkdown(mdGroups));
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
1231
|
printSection("Completed Recently", result.completed);
|
|
1242
1232
|
printSection("In Progress", result.inProgress);
|
|
1243
1233
|
printSection("Overdue", result.overdue);
|
|
@@ -1317,6 +1307,55 @@ async function assignTask(config, taskId, opts) {
|
|
|
1317
1307
|
});
|
|
1318
1308
|
}
|
|
1319
1309
|
|
|
1310
|
+
// src/commands/activity.ts
|
|
1311
|
+
import chalk4 from "chalk";
|
|
1312
|
+
function formatDate3(timestamp) {
|
|
1313
|
+
return new Date(Number(timestamp)).toLocaleString("en-US", {
|
|
1314
|
+
month: "short",
|
|
1315
|
+
day: "numeric",
|
|
1316
|
+
year: "numeric",
|
|
1317
|
+
hour: "numeric",
|
|
1318
|
+
minute: "2-digit"
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async function fetchActivity(config, taskId) {
|
|
1322
|
+
const client = new ClickUpClient(config);
|
|
1323
|
+
const [task, rawComments] = await Promise.all([
|
|
1324
|
+
client.getTask(taskId),
|
|
1325
|
+
client.getTaskComments(taskId)
|
|
1326
|
+
]);
|
|
1327
|
+
const comments = rawComments.map((c) => ({
|
|
1328
|
+
id: c.id,
|
|
1329
|
+
user: c.user.username,
|
|
1330
|
+
date: c.date,
|
|
1331
|
+
text: c.comment_text
|
|
1332
|
+
}));
|
|
1333
|
+
return { task, comments };
|
|
1334
|
+
}
|
|
1335
|
+
function printActivity(result, forceJson) {
|
|
1336
|
+
if (forceJson || !isTTY()) {
|
|
1337
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
console.log(formatTaskDetail(result.task));
|
|
1341
|
+
console.log("");
|
|
1342
|
+
console.log(chalk4.bold("Comments"));
|
|
1343
|
+
console.log(chalk4.dim("-".repeat(60)));
|
|
1344
|
+
if (result.comments.length === 0) {
|
|
1345
|
+
console.log("No comments.");
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
for (let i = 0; i < result.comments.length; i++) {
|
|
1349
|
+
const c = result.comments[i];
|
|
1350
|
+
if (i > 0) {
|
|
1351
|
+
console.log("");
|
|
1352
|
+
console.log(chalk4.dim("-".repeat(60)));
|
|
1353
|
+
}
|
|
1354
|
+
console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatDate3(c.date))}`);
|
|
1355
|
+
console.log(c.text);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1320
1359
|
// src/commands/completion.ts
|
|
1321
1360
|
function bashCompletion() {
|
|
1322
1361
|
return `_cu_completions() {
|
|
@@ -1721,6 +1760,47 @@ function generateCompletion(shell) {
|
|
|
1721
1760
|
}
|
|
1722
1761
|
}
|
|
1723
1762
|
|
|
1763
|
+
// src/commands/auth.ts
|
|
1764
|
+
async function checkAuth(config) {
|
|
1765
|
+
const client = new ClickUpClient(config);
|
|
1766
|
+
try {
|
|
1767
|
+
const user = await client.getMe();
|
|
1768
|
+
return { authenticated: true, user };
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1771
|
+
return { authenticated: false, error: message };
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/commands/search.ts
|
|
1776
|
+
async function searchTasks(config, query, opts = {}) {
|
|
1777
|
+
const trimmed = query.trim();
|
|
1778
|
+
if (!trimmed) {
|
|
1779
|
+
throw new Error("Search query cannot be empty");
|
|
1780
|
+
}
|
|
1781
|
+
const client = new ClickUpClient(config);
|
|
1782
|
+
const allTasks = await client.getMyTasks(config.teamId);
|
|
1783
|
+
const words = trimmed.toLowerCase().split(/\s+/);
|
|
1784
|
+
let matched = allTasks.filter((task) => {
|
|
1785
|
+
const name = task.name.toLowerCase();
|
|
1786
|
+
return words.every((word) => name.includes(word));
|
|
1787
|
+
});
|
|
1788
|
+
if (opts.status) {
|
|
1789
|
+
const availableStatuses = [...new Set(allTasks.map((t) => t.status.status))];
|
|
1790
|
+
const resolved = matchStatus(opts.status, availableStatuses);
|
|
1791
|
+
if (resolved) {
|
|
1792
|
+
if (resolved.toLowerCase() !== opts.status.toLowerCase()) {
|
|
1793
|
+
process.stderr.write(`Status matched: "${opts.status}" -> "${resolved}"
|
|
1794
|
+
`);
|
|
1795
|
+
}
|
|
1796
|
+
matched = matched.filter((t) => t.status.status.toLowerCase() === resolved.toLowerCase());
|
|
1797
|
+
} else {
|
|
1798
|
+
matched = matched.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase());
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return matched.map(summarize);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1724
1804
|
// src/index.ts
|
|
1725
1805
|
var require2 = createRequire(import.meta.url);
|
|
1726
1806
|
var { version } = require2("../package.json");
|
|
@@ -1739,6 +1819,20 @@ program.command("init").description("Set up cu for the first time").action(
|
|
|
1739
1819
|
await runInitCommand();
|
|
1740
1820
|
})
|
|
1741
1821
|
);
|
|
1822
|
+
program.command("auth").description("Validate API token and show current user").option("--json", "Force JSON output even in terminal").action(
|
|
1823
|
+
wrapAction(async (opts) => {
|
|
1824
|
+
const config = loadConfig();
|
|
1825
|
+
const result = await checkAuth(config);
|
|
1826
|
+
if (opts.json || !isTTY()) {
|
|
1827
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1828
|
+
} else if (result.authenticated && result.user) {
|
|
1829
|
+
console.log(`Authenticated as @${result.user.username} (id: ${result.user.id})`);
|
|
1830
|
+
} else {
|
|
1831
|
+
console.error(`Authentication failed: ${result.error ?? "unknown error"}`);
|
|
1832
|
+
process.exit(1);
|
|
1833
|
+
}
|
|
1834
|
+
})
|
|
1835
|
+
);
|
|
1742
1836
|
program.command("tasks").description("List tasks assigned to me").option("--status <status>", 'Filter by status (e.g. "in progress")').option("--list <listId>", "Filter by list ID").option("--space <spaceId>", "Filter by space ID").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
|
|
1743
1837
|
wrapAction(async (opts) => {
|
|
1744
1838
|
const config = loadConfig();
|
|
@@ -1769,45 +1863,33 @@ program.command("task <taskId>").description("Get task details").option("--json"
|
|
|
1769
1863
|
wrapAction(async (taskId, opts) => {
|
|
1770
1864
|
const config = loadConfig();
|
|
1771
1865
|
const result = await getTask(config, taskId);
|
|
1772
|
-
if (
|
|
1866
|
+
if (opts.json || !isTTY()) {
|
|
1773
1867
|
console.log(JSON.stringify(result, null, 2));
|
|
1774
|
-
} else if (!isTTY()) {
|
|
1775
|
-
console.log(formatTaskDetailMarkdown(result));
|
|
1776
1868
|
} else {
|
|
1777
|
-
|
|
1778
|
-
`ID: ${result.id}`,
|
|
1779
|
-
`Name: ${result.name}`,
|
|
1780
|
-
`Status: ${result.status?.status ?? "unknown"}`,
|
|
1781
|
-
`Type: ${(result.custom_item_id ?? 0) !== 0 ? "initiative" : "task"}`,
|
|
1782
|
-
`List: ${result.list?.name ?? "unknown"}`,
|
|
1783
|
-
`URL: ${result.url}`,
|
|
1784
|
-
...result.parent ? [`Parent: ${result.parent}`] : [],
|
|
1785
|
-
...result.description ? ["", result.description] : []
|
|
1786
|
-
];
|
|
1787
|
-
console.log(lines.join("\n"));
|
|
1869
|
+
console.log(formatTaskDetail(result));
|
|
1788
1870
|
}
|
|
1789
1871
|
})
|
|
1790
1872
|
);
|
|
1791
|
-
program.command("update <taskId>").description("Update a task").option("-n, --name <text>", "New task name").option("-d, --description <text>", "New description (markdown supported)").option("-s, --status <status>", 'New status (e.g. "in progress", "done")').option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Add assignee by user ID").action(
|
|
1873
|
+
program.command("update <taskId>").description("Update a task").option("-n, --name <text>", "New task name").option("-d, --description <text>", "New description (markdown supported)").option("-s, --status <status>", 'New status (e.g. "in progress", "done")').option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Add assignee by user ID").option("--json", "Force JSON output even in terminal").action(
|
|
1792
1874
|
wrapAction(async (taskId, opts) => {
|
|
1793
1875
|
const config = loadConfig();
|
|
1794
1876
|
const payload = buildUpdatePayload(opts);
|
|
1795
1877
|
const result = await updateTask(config, taskId, payload);
|
|
1796
|
-
if (
|
|
1878
|
+
if (opts.json || !isTTY()) {
|
|
1797
1879
|
console.log(JSON.stringify(result, null, 2));
|
|
1798
1880
|
} else {
|
|
1799
|
-
console.log(
|
|
1881
|
+
console.log(`Updated task ${result.id}: "${result.name}"`);
|
|
1800
1882
|
}
|
|
1801
1883
|
})
|
|
1802
1884
|
);
|
|
1803
|
-
program.command("create").description("Create a new task").option("-l, --list <listId>", "Target list ID (auto-detected from --parent if omitted)").requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Assignee user ID").option("--tags <tags>", "Comma-separated tag names").action(
|
|
1885
|
+
program.command("create").description("Create a new task").option("-l, --list <listId>", "Target list ID (auto-detected from --parent if omitted)").requiredOption("-n, --name <name>", "Task name").option("-d, --description <text>", "Task description").option("-p, --parent <taskId>", "Parent task ID (list auto-detected from parent)").option("-s, --status <status>", "Initial status").option("--priority <level>", "Priority: urgent, high, normal, low (or 1-4)").option("--due-date <date>", "Due date (YYYY-MM-DD)").option("--assignee <userId>", "Assignee user ID").option("--tags <tags>", "Comma-separated tag names").option("--json", "Force JSON output even in terminal").action(
|
|
1804
1886
|
wrapAction(async (opts) => {
|
|
1805
1887
|
const config = loadConfig();
|
|
1806
1888
|
const result = await createTask(config, opts);
|
|
1807
|
-
if (
|
|
1889
|
+
if (opts.json || !isTTY()) {
|
|
1808
1890
|
console.log(JSON.stringify(result, null, 2));
|
|
1809
1891
|
} else {
|
|
1810
|
-
console.log(
|
|
1892
|
+
console.log(`Created task ${result.id}: "${result.name}" - ${result.url}`);
|
|
1811
1893
|
}
|
|
1812
1894
|
})
|
|
1813
1895
|
);
|
|
@@ -1817,6 +1899,12 @@ program.command("sprint").description("List my tasks in the current active sprin
|
|
|
1817
1899
|
await runSprintCommand(config, opts);
|
|
1818
1900
|
})
|
|
1819
1901
|
);
|
|
1902
|
+
program.command("sprints").description("List all sprints in sprint folders").option("--space <nameOrId>", "Filter by space (partial name or ID)").option("--json", "Force JSON output even in terminal").action(
|
|
1903
|
+
wrapAction(async (opts) => {
|
|
1904
|
+
const config = loadConfig();
|
|
1905
|
+
await listSprints(config, opts);
|
|
1906
|
+
})
|
|
1907
|
+
);
|
|
1820
1908
|
program.command("subtasks <taskId>").description("List subtasks of a task or initiative").option("--json", "Force JSON output even in terminal").action(
|
|
1821
1909
|
wrapAction(async (taskId, opts) => {
|
|
1822
1910
|
const config = loadConfig();
|
|
@@ -1828,10 +1916,10 @@ program.command("comment <taskId>").description("Post a comment on a task").requ
|
|
|
1828
1916
|
wrapAction(async (taskId, opts) => {
|
|
1829
1917
|
const config = loadConfig();
|
|
1830
1918
|
const result = await postComment(config, taskId, opts.message);
|
|
1831
|
-
if (
|
|
1832
|
-
console.log(
|
|
1919
|
+
if (isTTY()) {
|
|
1920
|
+
console.log(`Comment posted (id: ${result.id})`);
|
|
1833
1921
|
} else {
|
|
1834
|
-
console.log(
|
|
1922
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1835
1923
|
}
|
|
1836
1924
|
})
|
|
1837
1925
|
);
|
|
@@ -1842,6 +1930,13 @@ program.command("comments <taskId>").description("List comments on a task").opti
|
|
|
1842
1930
|
printComments(comments, opts.json ?? false);
|
|
1843
1931
|
})
|
|
1844
1932
|
);
|
|
1933
|
+
program.command("activity <taskId>").description("Show task details and comments combined").option("--json", "Force JSON output even in terminal").action(
|
|
1934
|
+
wrapAction(async (taskId, opts) => {
|
|
1935
|
+
const config = loadConfig();
|
|
1936
|
+
const result = await fetchActivity(config, taskId);
|
|
1937
|
+
printActivity(result, opts.json ?? false);
|
|
1938
|
+
})
|
|
1939
|
+
);
|
|
1845
1940
|
program.command("lists <spaceId>").description("List all lists in a space (including lists inside folders)").option("--name <partial>", "Filter by name (case-insensitive contains)").option("--json", "Force JSON output even in terminal").action(
|
|
1846
1941
|
wrapAction(async (spaceId, opts) => {
|
|
1847
1942
|
const config = loadConfig();
|
|
@@ -1879,6 +1974,13 @@ program.command("open <query>").description("Open a task in the browser by ID or
|
|
|
1879
1974
|
await openTask(config, query, opts);
|
|
1880
1975
|
})
|
|
1881
1976
|
);
|
|
1977
|
+
program.command("search <query>").description("Search my tasks by name").option("--status <status>", "Filter by status").option("--json", "Force JSON output even in terminal").action(
|
|
1978
|
+
wrapAction(async (query, opts) => {
|
|
1979
|
+
const config = loadConfig();
|
|
1980
|
+
const tasks = await searchTasks(config, query, { status: opts.status });
|
|
1981
|
+
await printTasks(tasks, opts.json ?? false, config);
|
|
1982
|
+
})
|
|
1983
|
+
);
|
|
1882
1984
|
program.command("summary").description("Daily standup summary: completed, in-progress, overdue").option("--hours <n>", "Completed-tasks lookback in hours", "24").option("--json", "Force JSON output even in terminal").action(
|
|
1883
1985
|
wrapAction(async (opts) => {
|
|
1884
1986
|
const config = loadConfig();
|
|
@@ -1901,10 +2003,13 @@ program.command("assign <taskId>").description("Assign or unassign users from a
|
|
|
1901
2003
|
wrapAction(async (taskId, opts) => {
|
|
1902
2004
|
const config = loadConfig();
|
|
1903
2005
|
const result = await assignTask(config, taskId, opts);
|
|
1904
|
-
if (
|
|
2006
|
+
if (opts.json || !isTTY()) {
|
|
1905
2007
|
console.log(JSON.stringify(result, null, 2));
|
|
1906
2008
|
} else {
|
|
1907
|
-
|
|
2009
|
+
const parts = [];
|
|
2010
|
+
if (opts.to) parts.push(`Assigned ${opts.to} to task ${taskId}`);
|
|
2011
|
+
if (opts.remove) parts.push(`Removed ${opts.remove} from task ${taskId}`);
|
|
2012
|
+
console.log(parts.join("; "));
|
|
1908
2013
|
}
|
|
1909
2014
|
})
|
|
1910
2015
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krodak/clickup-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "ClickUp CLI for AI agents and humans",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
+
".claude-plugin",
|
|
28
|
+
"skills",
|
|
27
29
|
"README.md",
|
|
28
30
|
"LICENSE"
|
|
29
31
|
],
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clickup
|
|
3
|
+
description: 'Use when managing ClickUp tasks, initiatives, sprints, or comments via the `cu` CLI tool. Covers task queries, status updates, sprint tracking, and project management workflows.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ClickUp CLI (`cu`)
|
|
7
|
+
|
|
8
|
+
Reference for AI agents using the `cu` command-line tool to interact with ClickUp. Covers task management, sprint tracking, initiatives, and comments.
|
|
9
|
+
|
|
10
|
+
Keywords: ClickUp, task management, sprint, initiative, project management, agile, backlog, subtasks
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Config at `~/.config/cu/config.json` (or `$XDG_CONFIG_HOME/cu/config.json`) with `apiToken` and `teamId`. Run `cu init` to set up.
|
|
15
|
+
|
|
16
|
+
Alternatively, set `CU_API_TOKEN` and `CU_TEAM_ID` environment variables (overrides config file, no file needed when both are set).
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
All commands support `--help` for full flag details.
|
|
21
|
+
|
|
22
|
+
### Read
|
|
23
|
+
|
|
24
|
+
| Command | What it returns |
|
|
25
|
+
| -------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
26
|
+
| `cu tasks [--status s] [--name q] [--list id] [--space id] [--json]` | My tasks (workspace-wide) |
|
|
27
|
+
| `cu initiatives [--status s] [--name q] [--list id] [--space id] [--json]` | My initiatives |
|
|
28
|
+
| `cu assigned [--include-closed] [--json]` | All my tasks grouped by status |
|
|
29
|
+
| `cu sprint [--status s] [--space nameOrId] [--json]` | Tasks in active sprint (auto-detected) |
|
|
30
|
+
| `cu inbox [--days n] [--json]` | Tasks updated in last n days (default 30) |
|
|
31
|
+
| `cu task <id> [--json]` | Single task details |
|
|
32
|
+
| `cu subtasks <id> [--json]` | Subtasks of a task or initiative |
|
|
33
|
+
| `cu comments <id> [--json]` | List comments on a task |
|
|
34
|
+
| `cu spaces [--name partial] [--my] [--json]` | List/filter workspace spaces |
|
|
35
|
+
| `cu lists <spaceId> [--name partial] [--json]` | List all lists in a space (including folder lists) |
|
|
36
|
+
| `cu open <query> [--json]` | Open task in browser by ID or name |
|
|
37
|
+
| `cu summary [--hours n] [--json]` | Standup helper: completed, in-progress, overdue |
|
|
38
|
+
| `cu overdue [--json]` | Tasks past their due date |
|
|
39
|
+
| `cu search <query> [--status s] [--json]` | Search my tasks by name (multi-word, fuzzy status) |
|
|
40
|
+
| `cu activity <id> [--json]` | Task details + comment history combined |
|
|
41
|
+
| `cu auth [--json]` | Check authentication status |
|
|
42
|
+
| `cu sprints [--space nameOrId] [--json]` | List all sprints (marks active with \*) |
|
|
43
|
+
|
|
44
|
+
### Write
|
|
45
|
+
|
|
46
|
+
| Command | What it does |
|
|
47
|
+
| ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
48
|
+
| `cu update <id> [-n name] [-d desc] [-s status] [--priority p] [--due-date d] [--assignee id] [--json]` | Update task fields |
|
|
49
|
+
| `cu create -n name [-l listId] [-p parentId] [-d desc] [-s status] [--priority p] [--due-date d] [--assignee id] [--tags t] [--json]` | Create task (list auto-detected from parent) |
|
|
50
|
+
| `cu comment <id> -m text` | Post comment on task |
|
|
51
|
+
| `cu assign <id> [--to userId\|me] [--remove userId\|me] [--json]` | Assign/unassign users from a task |
|
|
52
|
+
| `cu config get <key>` / `cu config set <key> <value>` / `cu config path` | Manage CLI config |
|
|
53
|
+
| `cu completion <shell>` | Output shell completions (bash/zsh/fish) |
|
|
54
|
+
|
|
55
|
+
## Output modes
|
|
56
|
+
|
|
57
|
+
- **TTY (terminal)**: Interactive picker UI. Use `--json` to bypass.
|
|
58
|
+
- **Piped / non-TTY**: Always JSON. This is what agents get by default.
|
|
59
|
+
- **Agents should always pass `--json`** to guarantee machine-readable output.
|
|
60
|
+
- Set `NO_COLOR` to disable color output (tables still render, just without color).
|
|
61
|
+
|
|
62
|
+
## Key facts
|
|
63
|
+
|
|
64
|
+
- All task IDs are stable alphanumeric strings (e.g. `abc123def`)
|
|
65
|
+
- Initiatives are detected via `custom_item_id !== 0` (not `task_type`)
|
|
66
|
+
- `--list` is optional in `cu create` when `--parent` is given (list auto-detected from parent)
|
|
67
|
+
- `cu sprint` auto-detects active sprint from spaces where user has tasks, using view API and date range parsing from sprint names like "Acme Sprint 4 (3/1 - 3/14)"
|
|
68
|
+
- `--name` on tasks/initiatives filters by partial name match (case-insensitive)
|
|
69
|
+
- `--space` on sprint/tasks accepts partial name match (e.g. `--space Acm`)
|
|
70
|
+
- `--priority` accepts names (`urgent`, `high`, `normal`, `low`) or numbers (1-4)
|
|
71
|
+
- `--due-date` accepts `YYYY-MM-DD` format
|
|
72
|
+
- `--assignee` takes a numeric user ID (use `cu task <id> --json` to find assignee IDs)
|
|
73
|
+
- `cu assign` supports `me` as a shorthand for your own user ID
|
|
74
|
+
- `cu open` tries task ID lookup first, then falls back to name search
|
|
75
|
+
- `cu summary` categories: completed (done/complete/closed within N hours), in progress, overdue
|
|
76
|
+
- `cu overdue` excludes done/complete/closed tasks, sorted most overdue first
|
|
77
|
+
- `--tags` accepts comma-separated tag names (e.g. `--tags "bug,frontend"`)
|
|
78
|
+
- `cu lists <spaceId>` discovers list IDs needed for `--list` and `cu create -l`
|
|
79
|
+
- Strict argument parsing - excess/unknown arguments are rejected
|
|
80
|
+
- `cu update -s` supports fuzzy status matching (exact > starts-with > contains). Prints matched status to stderr when fuzzy-resolved.
|
|
81
|
+
- `cu task` shows custom fields in detail view (read-only)
|
|
82
|
+
- `cu search` matches all query words against task name (case-insensitive). `--status` supports fuzzy matching.
|
|
83
|
+
- Errors go to stderr with exit code 1
|
|
84
|
+
|
|
85
|
+
## Agent workflow example
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# List my in-progress tasks
|
|
89
|
+
cu tasks --status "in progress" --json
|
|
90
|
+
|
|
91
|
+
# Find tasks by partial name
|
|
92
|
+
cu tasks --name "login" --json
|
|
93
|
+
|
|
94
|
+
# Check current sprint
|
|
95
|
+
cu sprint --json | jq '.[].name'
|
|
96
|
+
|
|
97
|
+
# Get full details on a task
|
|
98
|
+
cu task abc123def --json
|
|
99
|
+
|
|
100
|
+
# List subtasks of an initiative
|
|
101
|
+
cu subtasks abc123def --json
|
|
102
|
+
|
|
103
|
+
# Read comments on a task
|
|
104
|
+
cu comments abc123def --json
|
|
105
|
+
|
|
106
|
+
# Update task status
|
|
107
|
+
cu update abc123def -s "done"
|
|
108
|
+
|
|
109
|
+
# Update priority and due date
|
|
110
|
+
cu update abc123def --priority high --due-date 2025-03-15
|
|
111
|
+
|
|
112
|
+
# Create subtask under initiative (no --list needed)
|
|
113
|
+
cu create -n "Fix the thing" -p abc123def
|
|
114
|
+
|
|
115
|
+
# Create task with priority and tags
|
|
116
|
+
cu create -n "Fix bug" -l <listId> --priority urgent --tags "bug,frontend"
|
|
117
|
+
|
|
118
|
+
# Post a comment
|
|
119
|
+
cu comment abc123def -m "Completed in PR #42"
|
|
120
|
+
|
|
121
|
+
# Discover lists in a space
|
|
122
|
+
cu lists <spaceId> --json
|
|
123
|
+
|
|
124
|
+
# Open a task in the browser
|
|
125
|
+
cu open abc123def
|
|
126
|
+
|
|
127
|
+
# Standup summary
|
|
128
|
+
cu summary --json
|
|
129
|
+
|
|
130
|
+
# Check overdue tasks
|
|
131
|
+
cu overdue --json
|
|
132
|
+
|
|
133
|
+
# Assign yourself to a task
|
|
134
|
+
cu assign abc123def --to me
|
|
135
|
+
|
|
136
|
+
# Check/set config
|
|
137
|
+
cu config get teamId
|
|
138
|
+
cu config set apiToken pk_example_token
|
|
139
|
+
|
|
140
|
+
cu search "login bug" --json
|
|
141
|
+
|
|
142
|
+
cu activity abc123def --json
|
|
143
|
+
|
|
144
|
+
cu auth --json
|
|
145
|
+
|
|
146
|
+
cu sprints --json
|
|
147
|
+
|
|
148
|
+
cu update abc123def -s "prog"
|
|
149
|
+
```
|