@krodak/clickup-cli 0.6.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +97 -109
  2. package/dist/index.js +208 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,127 +1,97 @@
1
1
  # cu - ClickUp CLI
2
2
 
3
- A ClickUp CLI for AI agents and humans. Two operating modes: interactive tables with a task picker in terminals, raw JSON when piped.
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.
4
4
 
5
- ## Requirements
6
-
7
- - Node 22+
8
- - A ClickUp personal API token (`pk_...` from https://app.clickup.com/settings/apps)
9
-
10
- ## Install
11
-
12
- ```bash
13
- npm install -g @krodak/clickup-cli
14
- ```
15
-
16
- ## Getting started
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)
17
9
 
18
10
  ```bash
19
- cu init
11
+ npm install -g @krodak/clickup-cli # or: brew tap krodak/tap && brew install clickup-cli
12
+ cu init # walks you through API token + workspace setup
20
13
  ```
21
14
 
22
- Prompts for your API token, verifies it, auto-detects your workspace, and writes `~/.config/cu/config.json`.
23
-
24
- ## Operating Modes
15
+ You need a ClickUp personal API token (`pk_...` from https://app.clickup.com/settings/apps).
25
16
 
26
- ### Interactive TTY mode (for humans)
17
+ ## Using with AI agents
27
18
 
28
- When run in a terminal (TTY detected), list commands (`cu tasks`, `cu initiatives`, `cu sprint`, `cu inbox`, `cu subtasks`, `cu overdue`) display:
19
+ This is the primary use case. Install the tool, install the skill file, and your agent knows how to work with ClickUp.
29
20
 
30
- 1. A formatted table with auto-sized columns showing task ID, name, status, type, list, and URL.
31
- 2. An interactive checkbox picker (powered by @inquirer/prompts) - navigate with arrow keys, toggle selection with space, confirm with enter.
32
- 3. For selected tasks, a rich detail view showing: name (bold/underlined), ID, status, type, list, assignees, priority, dates, estimate, tracked time, tags, parent, URL, and a description preview (first 3 lines).
33
- 4. A prompt to open the selected tasks in the browser.
21
+ ### 1. Install the skill
34
22
 
35
- Pass `--json` to any list command to bypass interactive mode and get raw JSON output in a terminal.
23
+ The repo includes a skill file at `skill/SKILL.md` that teaches agents all available commands and when to use them.
36
24
 
37
- ### JSON piped mode (for AI agents and scripts)
38
-
39
- When output is piped (no TTY), all list commands output JSON arrays to stdout. No interactive UI is shown.
40
-
41
- - `cu task <id> --json` returns the full JSON response for a single task.
42
- - All list commands output JSON arrays.
43
- - Errors go to stderr with exit code 1.
44
- - Write commands (`update`, `create`, `comment`, `assign`) always output JSON regardless of mode.
45
- - Set `NO_COLOR` to disable color output.
46
-
47
- ## For AI agents
48
-
49
- Always use the `--json` flag or pipe output to ensure you get JSON. Parse with `jq` or programmatically.
25
+ **Claude Code:**
50
26
 
51
27
  ```bash
52
- # List my in-progress tasks
53
- cu tasks --status "in progress" --json | jq '.[] | {id, name}'
54
-
55
- # Get full task details as JSON
56
- cu task abc123 --json | jq '{id, name, status}'
28
+ mkdir -p ~/.claude/skills/clickup
29
+ cp skill/SKILL.md ~/.claude/skills/clickup/SKILL.md
30
+ ```
57
31
 
58
- # Check current sprint
59
- cu sprint --json | jq '.[] | select(.status != "done")'
32
+ Then reference it in your `CLAUDE.md` or project instructions.
60
33
 
61
- # Create subtask under initiative
62
- INITIATIVE_ID=$(cu initiatives --json | jq -r '.[0].id')
63
- cu create -n "New subtask" -p "$INITIATIVE_ID"
34
+ **OpenCode:**
64
35
 
65
- # Update status when done
66
- cu update abc123 -s "done"
36
+ ```bash
37
+ mkdir -p ~/.config/opencode/skills/clickup
38
+ cp skill/SKILL.md ~/.config/opencode/skills/clickup/SKILL.md
67
39
  ```
68
40
 
69
- Write commands (`update`, `create`, `comment`) always return JSON, no `--json` flag needed.
41
+ **Codex / other agents:**
70
42
 
71
- ## AI Agents Skill
43
+ Copy the contents of `skill/SKILL.md` into your system prompt or project instructions. It's a standalone markdown document.
72
44
 
73
- A skill file is included at `skill/SKILL.md` that teaches AI agents how to use `cu`. Install it for your agent of choice:
45
+ ### 2. Talk to your agent
74
46
 
75
- ### OpenCode
47
+ Once the skill is installed, you just tell the agent what you need in plain language. It figures out which `cu` commands to run.
76
48
 
77
- ```bash
78
- mkdir -p ~/.config/opencode/skills/clickup
79
- cp skill/SKILL.md ~/.config/opencode/skills/clickup/SKILL.md
80
49
  ```
50
+ "Read the description of task <id>, do the work, then mark it in review and leave a comment with the commit hash."
81
51
 
82
- ### Claude Code
52
+ "Check all subtasks under initiative <id> and improve their descriptions based on what's in the codebase."
83
53
 
84
- ```bash
85
- mkdir -p ~/.claude/skills/clickup
86
- cp skill/SKILL.md ~/.claude/skills/clickup/SKILL.md
87
- ```
54
+ "What's my standup summary? What did I finish yesterday, what's in progress, what's overdue?"
88
55
 
89
- Then reference it in your `CLAUDE.md` or project instructions.
56
+ "Do exploratory work for task <id>, update the description with your findings, and flag blockers in a comment."
90
57
 
91
- ### Codex
58
+ "Create a subtask under <id> for the edge case we just found."
92
59
 
93
- Copy the contents of `skill/SKILL.md` into your Codex system prompt or project instructions file.
60
+ "Check my sprint and tell me what's overdue."
61
+ ```
94
62
 
95
- ### Other agents
63
+ You don't need to learn the CLI commands yourself. The agent handles it.
96
64
 
97
- The skill file is a standalone markdown document. Feed it to any agent that supports custom instructions or tool documentation.
65
+ ### Why a CLI and not MCP?
98
66
 
99
- ## Config
67
+ A CLI + skill file has fewer moving parts. No extra server process, no protocol layer. The agent already knows how to run shell commands - the skill file just teaches it which ones exist. This matches what Peter Steinberger and the OpenClaw project have found: for tool-use with coding agents, CLI + instructions tends to work better than MCP in practice.
100
68
 
101
- ### Config file
69
+ ### Scoped output
102
70
 
103
- `~/.config/cu/config.json` (or `$XDG_CONFIG_HOME/cu/config.json`):
71
+ Most commands return only your tasks by default. `cu tasks`, `cu sprint`, `cu overdue`, `cu summary` all scope to what's assigned to you. `cu spaces --my` filters to spaces where you have tasks. This keeps output small and relevant - important when it's going into an agent's context window.
104
72
 
105
- ```json
106
- {
107
- "apiToken": "pk_...",
108
- "teamId": "12345678"
109
- }
110
- ```
73
+ ## Using from the terminal
111
74
 
112
- ### Environment variables
75
+ When you run `cu` in a terminal directly, you get an interactive mode with tables and a task picker.
113
76
 
114
- Environment variables override config file values:
77
+ ```bash
78
+ cu tasks # interactive table with checkbox picker
79
+ cu sprint # your sprint tasks, auto-detected
80
+ cu summary # standup helper: completed / in progress / overdue
81
+ cu overdue # tasks past their due date
82
+ cu open "login bug" # fuzzy search, opens in browser
83
+ cu update abc123 -s "done" # update status
84
+ cu assign abc123 --to me # assign yourself
85
+ ```
115
86
 
116
- | Variable | Description |
117
- | -------------- | ---------------------------------- |
118
- | `CU_API_TOKEN` | ClickUp personal API token (`pk_`) |
119
- | `CU_TEAM_ID` | Workspace (team) ID |
87
+ Pass `--json` to any read command to force JSON output instead of the default format.
120
88
 
121
- When both are set, the config file is not required. Useful for CI/CD and containerized agents.
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.
122
90
 
123
91
  ## Commands
124
92
 
93
+ 20 commands total. All support `--help` for full flag details.
94
+
125
95
  ### `cu init`
126
96
 
127
97
  First-time setup. Prompts for your API token, verifies it, auto-detects your workspace, and writes `~/.config/cu/config.json`.
@@ -140,7 +110,7 @@ cu tasks --status "in progress"
140
110
  cu tasks --name "login"
141
111
  cu tasks --list <listId>
142
112
  cu tasks --space <spaceId>
143
- cu tasks --json # force JSON output
113
+ cu tasks --json
144
114
  ```
145
115
 
146
116
  ### `cu initiatives`
@@ -192,9 +162,11 @@ Get task details. Pretty summary in terminal, JSON when piped.
192
162
 
193
163
  ```bash
194
164
  cu task abc123
195
- cu task abc123 --json # full JSON response
165
+ cu task abc123 --json
196
166
  ```
197
167
 
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
+
198
170
  ### `cu subtasks <id>`
199
171
 
200
172
  List subtasks of a task or initiative.
@@ -274,7 +246,7 @@ List all lists in a space, including lists inside folders. Useful for discoverin
274
246
 
275
247
  ```bash
276
248
  cu lists <spaceId>
277
- cu lists <spaceId> --name "sprint" # filter by partial name
249
+ cu lists <spaceId> --name "sprint"
278
250
  cu lists <spaceId> --json
279
251
  ```
280
252
 
@@ -289,8 +261,8 @@ List spaces in your workspace. Useful for getting space IDs for the `--space` fi
289
261
 
290
262
  ```bash
291
263
  cu spaces
292
- cu spaces --name "eng" # filter by partial name match (case-insensitive)
293
- cu spaces --my # show only spaces you are a member of
264
+ cu spaces --name "eng"
265
+ cu spaces --my
294
266
  cu spaces --json
295
267
  ```
296
268
 
@@ -305,9 +277,9 @@ cu spaces --json
305
277
  Open a task in the browser. Accepts a task ID or partial name.
306
278
 
307
279
  ```bash
308
- cu open abc123 # open by task ID
309
- cu open "login bug" # search by name, open first match
310
- cu open abc123 --json # output task JSON instead of opening
280
+ cu open abc123
281
+ cu open "login bug"
282
+ cu open abc123 --json
311
283
  ```
312
284
 
313
285
  If the query matches multiple tasks by name, all matches are listed and the first is opened.
@@ -318,7 +290,7 @@ Daily standup helper. Shows tasks grouped into: recently completed, in progress,
318
290
 
319
291
  ```bash
320
292
  cu summary
321
- cu summary --hours 48 # extend completed window to 48 hours
293
+ cu summary --hours 48
322
294
  cu summary --json
323
295
  ```
324
296
 
@@ -329,25 +301,23 @@ cu summary --json
329
301
 
330
302
  ### `cu overdue`
331
303
 
332
- List tasks that are past their due date (excludes done/closed tasks).
304
+ List tasks that are past their due date (excludes done/closed tasks). Sorted most overdue first.
333
305
 
334
306
  ```bash
335
307
  cu overdue
336
308
  cu overdue --json
337
309
  ```
338
310
 
339
- Tasks are sorted by due date ascending (most overdue first).
340
-
341
311
  ### `cu assign <id>`
342
312
 
343
313
  Assign or unassign users from a task. Supports `me` as shorthand for your user ID.
344
314
 
345
315
  ```bash
346
- cu assign abc123 --to 12345 # add assignee
347
- cu assign abc123 --to me # assign yourself
348
- cu assign abc123 --remove 12345 # remove assignee
349
- cu assign abc123 --to me --remove 67890 # both at once
350
- cu assign abc123 --to me --json # output updated task JSON
316
+ cu assign abc123 --to 12345
317
+ cu assign abc123 --to me
318
+ cu assign abc123 --remove 12345
319
+ cu assign abc123 --to me --remove 67890
320
+ cu assign abc123 --to me --json
351
321
  ```
352
322
 
353
323
  | Flag | Description |
@@ -361,9 +331,9 @@ cu assign abc123 --to me --json # output updated task JSON
361
331
  Manage CLI configuration.
362
332
 
363
333
  ```bash
364
- cu config get apiToken # print a config value
365
- cu config set teamId 12345 # set a config value
366
- cu config path # print config file path
334
+ cu config get apiToken
335
+ cu config set teamId 12345
336
+ cu config path
367
337
  ```
368
338
 
369
339
  Valid keys: `apiToken`, `teamId`. Setting `apiToken` validates the `pk_` prefix.
@@ -373,17 +343,35 @@ Valid keys: `apiToken`, `teamId`. Setting `apiToken` validates the `pk_` prefix.
373
343
  Output shell completion script. Supports `bash`, `zsh`, and `fish`.
374
344
 
375
345
  ```bash
376
- # Bash - add to ~/.bashrc
377
- eval "$(cu completion bash)"
346
+ eval "$(cu completion bash)" # Bash
347
+ eval "$(cu completion zsh)" # Zsh
348
+ cu completion fish > ~/.config/fish/completions/cu.fish # Fish
349
+ ```
378
350
 
379
- # Zsh - add to ~/.zshrc
380
- eval "$(cu completion zsh)"
351
+ ## Config
381
352
 
382
- # Fish - save to completions directory
383
- cu completion fish > ~/.config/fish/completions/cu.fish
353
+ ### Config file
354
+
355
+ `~/.config/cu/config.json` (or `$XDG_CONFIG_HOME/cu/config.json`):
356
+
357
+ ```json
358
+ {
359
+ "apiToken": "pk_...",
360
+ "teamId": "12345678"
361
+ }
384
362
  ```
385
363
 
386
- Completions cover all commands, flags, and known values (priority levels, status names, config keys).
364
+ ### Environment variables
365
+
366
+ Environment variables override config file values:
367
+
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) |
373
+
374
+ When both are set, the config file is not required. Useful for CI/CD and containerized agents.
387
375
 
388
376
  ## Development
389
377
 
package/dist/index.js CHANGED
@@ -220,6 +220,11 @@ import chalk from "chalk";
220
220
  function isTTY() {
221
221
  return Boolean(process.stdout.isTTY);
222
222
  }
223
+ function shouldOutputJson(forceJson) {
224
+ if (forceJson) return true;
225
+ if (process.env["CU_OUTPUT"] === "json") return true;
226
+ return false;
227
+ }
223
228
  function cell(value, width) {
224
229
  if (value.length > width) return value.slice(0, width - 1) + "\u2026";
225
230
  return value.padEnd(width);
@@ -252,6 +257,128 @@ var TASK_COLUMNS = [
252
257
  { key: "list", label: "LIST" }
253
258
  ];
254
259
 
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
+
255
382
  // src/interactive.ts
256
383
  import { execFileSync } from "child_process";
257
384
  import { checkbox, confirm, Separator } from "@inquirer/prompts";
@@ -439,10 +566,14 @@ async function fetchMyTasks(config, opts = {}) {
439
566
  return filtered.map(summarize);
440
567
  }
441
568
  async function printTasks(tasks, forceJson, config) {
442
- if (forceJson || !isTTY()) {
569
+ if (shouldOutputJson(forceJson)) {
443
570
  console.log(JSON.stringify(tasks, null, 2));
444
571
  return;
445
572
  }
573
+ if (!isTTY()) {
574
+ console.log(formatTasksMarkdown(tasks));
575
+ return;
576
+ }
446
577
  if (tasks.length === 0) {
447
578
  console.log("No tasks found.");
448
579
  return;
@@ -671,13 +802,13 @@ async function runSprintCommand(config, opts) {
671
802
  const me = await client.getMe();
672
803
  const viewData = await client.getListViews(activeList.id);
673
804
  const listView = viewData.required_views?.list;
674
- let sprintTasks;
805
+ let allTasks;
675
806
  if (listView) {
676
- const allViewTasks = await client.getViewTasks(listView.id);
677
- sprintTasks = allViewTasks.filter((t) => t.assignees.some((a) => a.id === me.id));
807
+ allTasks = await client.getViewTasks(listView.id);
678
808
  } else {
679
- sprintTasks = await client.getMyTasksFromList(activeList.id);
809
+ allTasks = await client.getTasksFromList(activeList.id);
680
810
  }
811
+ const sprintTasks = allTasks.filter((t) => t.assignees.some((a) => Number(a.id) === me.id));
681
812
  const filtered = opts.status ? sprintTasks.filter((t) => t.status.status.toLowerCase() === opts.status.toLowerCase()) : sprintTasks;
682
813
  const summaries = filtered.map(summarize);
683
814
  await printTasks(summaries, opts.json ?? false, config);
@@ -700,7 +831,7 @@ async function postComment(config, taskId, text) {
700
831
 
701
832
  // src/commands/comments.ts
702
833
  import chalk3 from "chalk";
703
- function formatDate(timestamp) {
834
+ function formatDate2(timestamp) {
704
835
  return new Date(Number(timestamp)).toLocaleString("en-US", {
705
836
  month: "short",
706
837
  day: "numeric",
@@ -720,10 +851,14 @@ async function fetchComments(config, taskId) {
720
851
  }));
721
852
  }
722
853
  function printComments(comments, forceJson) {
723
- if (forceJson || !isTTY()) {
854
+ if (shouldOutputJson(forceJson)) {
724
855
  console.log(JSON.stringify(comments, null, 2));
725
856
  return;
726
857
  }
858
+ if (!isTTY()) {
859
+ console.log(formatCommentsMarkdown(comments));
860
+ return;
861
+ }
727
862
  if (comments.length === 0) {
728
863
  console.log("No comments found.");
729
864
  return;
@@ -732,7 +867,7 @@ function printComments(comments, forceJson) {
732
867
  for (let i = 0; i < comments.length; i++) {
733
868
  const c = comments[i];
734
869
  if (i > 0) console.log(separator);
735
- console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate(c.date))}`);
870
+ console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate2(c.date))}`);
736
871
  console.log(c.text);
737
872
  if (i < comments.length - 1) console.log("");
738
873
  }
@@ -761,10 +896,14 @@ async function fetchLists(config, spaceId, opts = {}) {
761
896
  return results;
762
897
  }
763
898
  function printLists(lists, forceJson) {
764
- if (forceJson || !isTTY()) {
899
+ if (shouldOutputJson(forceJson)) {
765
900
  console.log(JSON.stringify(lists, null, 2));
766
901
  return;
767
902
  }
903
+ if (!isTTY()) {
904
+ console.log(formatListsMarkdown(lists));
905
+ return;
906
+ }
768
907
  if (lists.length === 0) {
769
908
  console.log("No lists found.");
770
909
  return;
@@ -832,7 +971,7 @@ async function fetchInbox(config, days = 30) {
832
971
  async function printInbox(tasks, forceJson, config) {
833
972
  const now = Date.now();
834
973
  const groups = groupTasks(tasks, now);
835
- if (forceJson || !isTTY()) {
974
+ if (shouldOutputJson(forceJson)) {
836
975
  const jsonGroups = {};
837
976
  for (const { key } of TIME_PERIODS) {
838
977
  if (groups[key].length > 0) {
@@ -842,6 +981,14 @@ async function printInbox(tasks, forceJson, config) {
842
981
  console.log(JSON.stringify(jsonGroups, null, 2));
843
982
  return;
844
983
  }
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
+ }
845
992
  if (tasks.length === 0) {
846
993
  console.log("No recently updated tasks.");
847
994
  return;
@@ -873,7 +1020,11 @@ async function listSpaces(config, opts) {
873
1020
  );
874
1021
  spaces = spaces.filter((s) => mySpaceIds.has(s.id));
875
1022
  }
876
- if (!opts.json && isTTY()) {
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 {
877
1028
  const table = formatTable(
878
1029
  spaces.map((s) => ({ id: s.id, name: s.name })),
879
1030
  [
@@ -882,8 +1033,6 @@ async function listSpaces(config, opts) {
882
1033
  ]
883
1034
  );
884
1035
  console.log(table);
885
- } else {
886
- console.log(JSON.stringify(spaces, null, 2));
887
1036
  }
888
1037
  }
889
1038
 
@@ -938,7 +1087,7 @@ async function runAssignedCommand(config, opts) {
938
1087
  const client = new ClickUpClient(config);
939
1088
  const allTasks = await client.getMyTasks(config.teamId);
940
1089
  const groups = groupByStatus(allTasks, opts.includeClosed ?? false);
941
- if (opts.json || !isTTY()) {
1090
+ if (shouldOutputJson(opts.json ?? false)) {
942
1091
  const result = {};
943
1092
  for (const group of groups) {
944
1093
  result[group.status.toLowerCase()] = group.tasks.map((t) => toJsonTask(t, summarize(t)));
@@ -946,6 +1095,14 @@ async function runAssignedCommand(config, opts) {
946
1095
  console.log(JSON.stringify(result, null, 2));
947
1096
  return;
948
1097
  }
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
+ }
949
1106
  if (groups.length === 0) {
950
1107
  console.log("No tasks found.");
951
1108
  return;
@@ -971,13 +1128,13 @@ async function openTask(config, query, opts = {}) {
971
1128
  } catch {
972
1129
  }
973
1130
  if (task) {
974
- if (opts.json) {
1131
+ if (shouldOutputJson(opts.json ?? false)) {
975
1132
  console.log(JSON.stringify(task, null, 2));
1133
+ } else if (!isTTY()) {
1134
+ console.log(formatTaskDetailMarkdown(task));
976
1135
  } else {
977
- if (isTTY()) {
978
- console.log(task.name);
979
- console.log(task.url);
980
- }
1136
+ console.log(task.name);
1137
+ console.log(task.url);
981
1138
  openUrl(task.url);
982
1139
  }
983
1140
  return task;
@@ -995,15 +1152,18 @@ async function openTask(config, query, opts = {}) {
995
1152
  }
996
1153
  console.log("Opening first match...");
997
1154
  }
998
- if (opts.json) {
1155
+ if (shouldOutputJson(opts.json ?? false)) {
999
1156
  const fullTask = await client.getTask(first.id);
1000
1157
  console.log(JSON.stringify(fullTask, null, 2));
1001
1158
  return fullTask;
1002
1159
  }
1003
- if (isTTY()) {
1004
- console.log(first.name);
1005
- console.log(first.url);
1160
+ if (!isTTY()) {
1161
+ const fullTask = await client.getTask(first.id);
1162
+ console.log(formatTaskDetailMarkdown(fullTask));
1163
+ return fullTask;
1006
1164
  }
1165
+ console.log(first.name);
1166
+ console.log(first.url);
1007
1167
  openUrl(first.url);
1008
1168
  return {
1009
1169
  id: first.id,
@@ -1017,7 +1177,7 @@ async function openTask(config, query, opts = {}) {
1017
1177
  }
1018
1178
 
1019
1179
  // src/commands/summary.ts
1020
- var IN_PROGRESS_PATTERNS = ["in progress", "in review", "doing"];
1180
+ var IN_PROGRESS_PATTERNS = ["in progress", "in review", "code review", "doing"];
1021
1181
  function isCompletedRecently(task, cutoff) {
1022
1182
  if (!isDoneStatus(task.status.status)) return false;
1023
1183
  const updated = Number(task.date_updated);
@@ -1065,10 +1225,19 @@ async function runSummaryCommand(config, opts) {
1065
1225
  const client = new ClickUpClient(config);
1066
1226
  const allTasks = await client.getMyTasks(config.teamId);
1067
1227
  const result = categorizeTasks(allTasks, opts.hours);
1068
- if (opts.json || !isTTY()) {
1228
+ if (shouldOutputJson(opts.json)) {
1069
1229
  console.log(JSON.stringify(result, null, 2));
1070
1230
  return;
1071
1231
  }
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
+ }
1072
1241
  printSection("Completed Recently", result.completed);
1073
1242
  printSection("In Progress", result.inProgress);
1074
1243
  printSection("Overdue", result.overdue);
@@ -1600,8 +1769,10 @@ program.command("task <taskId>").description("Get task details").option("--json"
1600
1769
  wrapAction(async (taskId, opts) => {
1601
1770
  const config = loadConfig();
1602
1771
  const result = await getTask(config, taskId);
1603
- if (opts.json || !isTTY()) {
1772
+ if (shouldOutputJson(opts.json ?? false)) {
1604
1773
  console.log(JSON.stringify(result, null, 2));
1774
+ } else if (!isTTY()) {
1775
+ console.log(formatTaskDetailMarkdown(result));
1605
1776
  } else {
1606
1777
  const lines = [
1607
1778
  `ID: ${result.id}`,
@@ -1622,10 +1793,10 @@ program.command("update <taskId>").description("Update a task").option("-n, --na
1622
1793
  const config = loadConfig();
1623
1794
  const payload = buildUpdatePayload(opts);
1624
1795
  const result = await updateTask(config, taskId, payload);
1625
- if (isTTY()) {
1626
- console.log(`Updated task ${result.id}: "${result.name}"`);
1627
- } else {
1796
+ if (shouldOutputJson(false)) {
1628
1797
  console.log(JSON.stringify(result, null, 2));
1798
+ } else {
1799
+ console.log(formatUpdateConfirmation(result.id, result.name));
1629
1800
  }
1630
1801
  })
1631
1802
  );
@@ -1633,10 +1804,10 @@ program.command("create").description("Create a new task").option("-l, --list <l
1633
1804
  wrapAction(async (opts) => {
1634
1805
  const config = loadConfig();
1635
1806
  const result = await createTask(config, opts);
1636
- if (isTTY()) {
1637
- console.log(`Created task ${result.id}: "${result.name}" - ${result.url}`);
1638
- } else {
1807
+ if (shouldOutputJson(false)) {
1639
1808
  console.log(JSON.stringify(result, null, 2));
1809
+ } else {
1810
+ console.log(formatCreateConfirmation(result.id, result.name, result.url));
1640
1811
  }
1641
1812
  })
1642
1813
  );
@@ -1657,10 +1828,10 @@ program.command("comment <taskId>").description("Post a comment on a task").requ
1657
1828
  wrapAction(async (taskId, opts) => {
1658
1829
  const config = loadConfig();
1659
1830
  const result = await postComment(config, taskId, opts.message);
1660
- if (isTTY()) {
1661
- console.log(`Comment posted (id: ${result.id})`);
1662
- } else {
1831
+ if (shouldOutputJson(false)) {
1663
1832
  console.log(JSON.stringify(result, null, 2));
1833
+ } else {
1834
+ console.log(formatCommentConfirmation(result.id));
1664
1835
  }
1665
1836
  })
1666
1837
  );
@@ -1730,13 +1901,10 @@ program.command("assign <taskId>").description("Assign or unassign users from a
1730
1901
  wrapAction(async (taskId, opts) => {
1731
1902
  const config = loadConfig();
1732
1903
  const result = await assignTask(config, taskId, opts);
1733
- if (opts.json || !isTTY()) {
1904
+ if (shouldOutputJson(opts.json ?? false)) {
1734
1905
  console.log(JSON.stringify(result, null, 2));
1735
1906
  } else {
1736
- const parts = [];
1737
- if (opts.to) parts.push(`Assigned ${opts.to} to task ${taskId}`);
1738
- if (opts.remove) parts.push(`Removed ${opts.remove} from task ${taskId}`);
1739
- console.log(parts.join("; "));
1907
+ console.log(formatAssignConfirmation(taskId, { to: opts.to, remove: opts.remove }));
1740
1908
  }
1741
1909
  })
1742
1910
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",