@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.
@@ -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
- > A ClickUp CLI built for AI agents that also works well for humans. Outputs Markdown when piped (optimized for AI context windows), interactive tables when run in a terminal.
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
- [![npm](https://img.shields.io/npm/v/@krodak/clickup-cli)](https://www.npmjs.com/package/@krodak/clickup-cli)
6
- [![node](https://img.shields.io/node/v/@krodak/clickup-cli)](https://nodejs.org)
7
- [![license](https://img.shields.io/npm/l/@krodak/clickup-cli)](./LICENSE)
8
- [![CI](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/krodak/clickup-cli/actions/workflows/ci.yml)
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 `skill/SKILL.md` that teaches agents all available commands and when to use them.
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 skill/SKILL.md ~/.claude/skills/clickup/SKILL.md
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 skill/SKILL.md ~/.config/opencode/skills/clickup/SKILL.md
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 `skill/SKILL.md` into your system prompt or project instructions. It's a standalone markdown document.
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 read command to force JSON output instead of the default format.
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 **Markdown** by default - optimized for AI agent context windows. Pass `--json` to any command for JSON output. Set `CU_OUTPUT=json` environment variable to always get JSON when piped.
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
- 20 commands total. All support `--help` for full flag details.
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. `"in progress"`, `"done"`) |
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
- fs.writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
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 (shouldOutputJson(forceJson)) {
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 (shouldOutputJson(forceJson)) {
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 (shouldOutputJson(forceJson)) {
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 (shouldOutputJson(forceJson)) {
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 (shouldOutputJson(opts.json ?? false)) {
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 (shouldOutputJson(opts.json ?? false)) {
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 (shouldOutputJson(opts.json ?? false)) {
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
- console.log(task.name);
1137
- console.log(task.url);
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 (shouldOutputJson(opts.json ?? false)) {
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 (!isTTY()) {
1161
- const fullTask = await client.getTask(first.id);
1162
- console.log(formatTaskDetailMarkdown(fullTask));
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 (shouldOutputJson(opts.json)) {
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 (shouldOutputJson(opts.json ?? false)) {
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
- const lines = [
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 (shouldOutputJson(false)) {
1878
+ if (opts.json || !isTTY()) {
1797
1879
  console.log(JSON.stringify(result, null, 2));
1798
1880
  } else {
1799
- console.log(formatUpdateConfirmation(result.id, result.name));
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 (shouldOutputJson(false)) {
1889
+ if (opts.json || !isTTY()) {
1808
1890
  console.log(JSON.stringify(result, null, 2));
1809
1891
  } else {
1810
- console.log(formatCreateConfirmation(result.id, result.name, result.url));
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 (shouldOutputJson(false)) {
1832
- console.log(JSON.stringify(result, null, 2));
1919
+ if (isTTY()) {
1920
+ console.log(`Comment posted (id: ${result.id})`);
1833
1921
  } else {
1834
- console.log(formatCommentConfirmation(result.id));
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 (shouldOutputJson(opts.json ?? false)) {
2006
+ if (opts.json || !isTTY()) {
1905
2007
  console.log(JSON.stringify(result, null, 2));
1906
2008
  } else {
1907
- console.log(formatAssignConfirmation(taskId, { to: opts.to, remove: opts.remove }));
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.7.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
+ ```