@krodak/clickup-cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -120,6 +120,9 @@ var ClickUpClient = class {
120
120
  async getSpaceWithStatuses(spaceId) {
121
121
  return this.request(`/space/${spaceId}`);
122
122
  }
123
+ async getListWithStatuses(listId) {
124
+ return this.request(`/list/${listId}`);
125
+ }
123
126
  async getSpaces(teamId) {
124
127
  const data = await this.request(`/team/${teamId}/space?archived=false`);
125
128
  return data.spaces ?? [];
@@ -201,6 +204,25 @@ var ClickUpClient = class {
201
204
  body: JSON.stringify(body)
202
205
  });
203
206
  }
207
+ async deleteComment(commentId) {
208
+ await this.request(`/comment/${commentId}`, { method: "DELETE" });
209
+ }
210
+ async getThreadedComments(commentId) {
211
+ const data = await this.request(`/comment/${commentId}/reply`);
212
+ return data.comments ?? [];
213
+ }
214
+ async createThreadedComment(commentId, text) {
215
+ await this.request(`/comment/${commentId}/reply`, {
216
+ method: "POST",
217
+ body: JSON.stringify({ comment_text: text })
218
+ });
219
+ }
220
+ async addTaskLink(taskId, linksTo) {
221
+ await this.request(`/task/${taskId}/link/${linksTo}`, { method: "POST" });
222
+ }
223
+ async deleteTaskLink(taskId, linksTo) {
224
+ await this.request(`/task/${taskId}/link/${linksTo}`, { method: "DELETE" });
225
+ }
204
226
  async getListCustomFields(listId) {
205
227
  const data = await this.request(`/list/${listId}/field`);
206
228
  return data.fields ?? [];
@@ -235,6 +257,63 @@ var ClickUpClient = class {
235
257
  { method: "DELETE" }
236
258
  );
237
259
  }
260
+ async startTimeEntry(teamId, taskId, description) {
261
+ const body = {
262
+ tid: taskId,
263
+ start: Date.now(),
264
+ duration: -1
265
+ };
266
+ if (description) body.description = description;
267
+ const data = await this.request(`/team/${teamId}/time_entries/start`, {
268
+ method: "POST",
269
+ body: JSON.stringify(body)
270
+ });
271
+ return data.data;
272
+ }
273
+ async stopTimeEntry(teamId) {
274
+ const data = await this.request(`/team/${teamId}/time_entries/stop`, {
275
+ method: "POST"
276
+ });
277
+ return data.data;
278
+ }
279
+ async getRunningTimeEntry(teamId) {
280
+ const data = await this.request(
281
+ `/team/${teamId}/time_entries/current`
282
+ );
283
+ return data.data ?? null;
284
+ }
285
+ async createTimeEntry(teamId, taskId, duration, opts) {
286
+ const start = opts?.start ?? Date.now() - duration;
287
+ const body = {
288
+ tid: taskId,
289
+ start,
290
+ duration
291
+ };
292
+ if (opts?.description) body.description = opts.description;
293
+ const data = await this.request(`/team/${teamId}/time_entries`, {
294
+ method: "POST",
295
+ body: JSON.stringify(body)
296
+ });
297
+ return data.data;
298
+ }
299
+ async getTimeEntries(teamId, opts) {
300
+ const params = new URLSearchParams();
301
+ if (opts?.startDate != null) params.set("start_date", String(opts.startDate));
302
+ if (opts?.endDate != null) params.set("end_date", String(opts.endDate));
303
+ const query = params.toString();
304
+ const url = `/team/${teamId}/time_entries${query ? `?${query}` : ""}`;
305
+ const data = await this.request(url);
306
+ const entries = data.data ?? [];
307
+ if (opts?.taskId) {
308
+ return entries.filter((e) => e.task?.id === opts.taskId);
309
+ }
310
+ return entries;
311
+ }
312
+ async deleteTimeEntry(teamId, timeEntryId) {
313
+ await this.request(`/team/${teamId}/time_entries/${timeEntryId}`, {
314
+ method: "DELETE"
315
+ });
316
+ }
238
317
  };
239
318
 
240
319
  // src/config.ts
@@ -865,9 +944,8 @@ function hasUpdateFields(options) {
865
944
  }
866
945
  async function resolveStatus(client, taskId, statusInput) {
867
946
  const task = await client.getTask(taskId);
868
- if (!task.space) return statusInput;
869
- const space = await client.getSpaceWithStatuses(task.space.id);
870
- const available = space.statuses.map((s) => s.status);
947
+ const list = await client.getListWithStatuses(task.list.id);
948
+ const available = list.statuses.map((s) => s.status);
871
949
  const matched = matchStatus(statusInput, available);
872
950
  if (!matched) {
873
951
  throw new Error(`No matching status for "${statusInput}". Available: ${available.join(", ")}`);
@@ -1749,7 +1827,7 @@ function bashCompletion() {
1749
1827
  cword=$COMP_CWORD
1750
1828
  fi
1751
1829
 
1752
- local commands="init auth tasks task update create sprint sprints subtasks comment comment-edit comments activity lists spaces inbox assigned open search summary overdue assign depend move field delete tag checklist config completion"
1830
+ local commands="init auth tasks task update create sprint sprints subtasks comment comment-edit comment-delete comments replies reply activity lists spaces inbox assigned open search summary overdue assign depend link move field delete tag checklist time config completion"
1753
1831
 
1754
1832
  if [[ $cword -eq 1 ]]; then
1755
1833
  COMPREPLY=($(compgen -W "$commands --help --version" -- "$cur"))
@@ -1850,9 +1928,26 @@ function bashCompletion() {
1850
1928
  COMPREPLY=($(compgen -W "view create delete add-item edit-item delete-item" -- "$cur"))
1851
1929
  fi
1852
1930
  ;;
1931
+ time)
1932
+ if [[ $cword -eq 2 ]]; then
1933
+ COMPREPLY=($(compgen -W "start stop status log list" -- "$cur"))
1934
+ fi
1935
+ ;;
1853
1936
  comment-edit)
1854
1937
  COMPREPLY=($(compgen -W "-m --message --resolved --unresolved --json" -- "$cur"))
1855
1938
  ;;
1939
+ comment-delete)
1940
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
1941
+ ;;
1942
+ replies)
1943
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
1944
+ ;;
1945
+ reply)
1946
+ COMPREPLY=($(compgen -W "-m --message --json" -- "$cur"))
1947
+ ;;
1948
+ link)
1949
+ COMPREPLY=($(compgen -W "--remove --json" -- "$cur"))
1950
+ ;;
1856
1951
  config)
1857
1952
  if [[ $cword -eq 2 ]]; then
1858
1953
  COMPREPLY=($(compgen -W "get set path" -- "$cur"))
@@ -1906,7 +2001,12 @@ _cu() {
1906
2001
  'delete:Delete a task'
1907
2002
  'tag:Add or remove tags from a task'
1908
2003
  'checklist:Manage checklists on a task'
2004
+ 'time:Track time on tasks'
1909
2005
  'comment-edit:Edit an existing comment'
2006
+ 'comment-delete:Delete a comment'
2007
+ 'replies:List threaded replies on a comment'
2008
+ 'reply:Reply to a comment'
2009
+ 'link:Add or remove a link between two tasks'
1910
2010
  'config:Manage CLI configuration'
1911
2011
  'completion:Output shell completion script'
1912
2012
  )
@@ -2142,6 +2242,53 @@ _cu() {
2142
2242
  ;;
2143
2243
  esac
2144
2244
  ;;
2245
+ time)
2246
+ local -a time_cmds
2247
+ time_cmds=(
2248
+ 'start:Start tracking time on a task'
2249
+ 'stop:Stop the running timer'
2250
+ 'status:Show the currently running timer'
2251
+ 'log:Log a manual time entry'
2252
+ 'list:List recent time entries'
2253
+ )
2254
+ _arguments -C \\
2255
+ '1:time command:->time_cmd' \\
2256
+ '*::time_arg:->time_args'
2257
+ case $state in
2258
+ time_cmd)
2259
+ _describe 'time command' time_cmds
2260
+ ;;
2261
+ time_args)
2262
+ case $words[1] in
2263
+ start)
2264
+ _arguments \\
2265
+ '1:task_id:' \\
2266
+ '(-d --description)'{-d,--description}'[Description]:text:' \\
2267
+ '--json[Force JSON output]'
2268
+ ;;
2269
+ stop)
2270
+ _arguments '--json[Force JSON output]'
2271
+ ;;
2272
+ status)
2273
+ _arguments '--json[Force JSON output]'
2274
+ ;;
2275
+ log)
2276
+ _arguments \\
2277
+ '1:task_id:' \\
2278
+ '2:duration:' \\
2279
+ '(-d --description)'{-d,--description}'[Description]:text:' \\
2280
+ '--json[Force JSON output]'
2281
+ ;;
2282
+ list)
2283
+ _arguments \\
2284
+ '--days[Number of days to look back]:days:' \\
2285
+ '--task[Filter by task ID]:task_id:' \\
2286
+ '--json[Force JSON output]'
2287
+ ;;
2288
+ esac
2289
+ ;;
2290
+ esac
2291
+ ;;
2145
2292
  comment-edit)
2146
2293
  _arguments \\
2147
2294
  '1:comment_id:' \\
@@ -2150,6 +2297,29 @@ _cu() {
2150
2297
  '--unresolved[Mark comment as unresolved]' \\
2151
2298
  '--json[Force JSON output]'
2152
2299
  ;;
2300
+ comment-delete)
2301
+ _arguments \\
2302
+ '1:comment_id:' \\
2303
+ '--json[Force JSON output]'
2304
+ ;;
2305
+ replies)
2306
+ _arguments \\
2307
+ '1:comment_id:' \\
2308
+ '--json[Force JSON output]'
2309
+ ;;
2310
+ reply)
2311
+ _arguments \\
2312
+ '1:comment_id:' \\
2313
+ '(-m --message)'{-m,--message}'[Reply text]:text:' \\
2314
+ '--json[Force JSON output]'
2315
+ ;;
2316
+ link)
2317
+ _arguments \\
2318
+ '1:task_id:' \\
2319
+ '2:links_to:' \\
2320
+ '--remove[Remove the link instead of adding it]' \\
2321
+ '--json[Force JSON output]'
2322
+ ;;
2153
2323
  config)
2154
2324
  local -a config_cmds
2155
2325
  config_cmds=(
@@ -2217,7 +2387,12 @@ complete -c cu -n __fish_use_subcommand -a field -d 'Set or remove a custom fiel
2217
2387
  complete -c cu -n __fish_use_subcommand -a delete -d 'Delete a task'
2218
2388
  complete -c cu -n __fish_use_subcommand -a tag -d 'Add or remove tags from a task'
2219
2389
  complete -c cu -n __fish_use_subcommand -a checklist -d 'Manage checklists on a task'
2390
+ complete -c cu -n __fish_use_subcommand -a time -d 'Track time on tasks'
2220
2391
  complete -c cu -n __fish_use_subcommand -a comment-edit -d 'Edit an existing comment'
2392
+ complete -c cu -n __fish_use_subcommand -a comment-delete -d 'Delete a comment'
2393
+ complete -c cu -n __fish_use_subcommand -a replies -d 'List threaded replies on a comment'
2394
+ complete -c cu -n __fish_use_subcommand -a reply -d 'Reply to a comment'
2395
+ complete -c cu -n __fish_use_subcommand -a link -d 'Add or remove a link between two tasks'
2221
2396
  complete -c cu -n __fish_use_subcommand -a config -d 'Manage CLI configuration'
2222
2397
  complete -c cu -n __fish_use_subcommand -a completion -d 'Output shell completion script'
2223
2398
 
@@ -2339,6 +2514,27 @@ complete -c cu -n '__fish_seen_subcommand_from edit-item' -l resolved -d 'Mark i
2339
2514
  complete -c cu -n '__fish_seen_subcommand_from edit-item' -l unresolved -d 'Mark item as unresolved'
2340
2515
  complete -c cu -n '__fish_seen_subcommand_from edit-item' -l assignee -d 'Assign user by ID'
2341
2516
 
2517
+ complete -c cu -n '__fish_seen_subcommand_from time; and not __fish_seen_subcommand_from start stop status log list' -a start -d 'Start tracking time on a task'
2518
+ complete -c cu -n '__fish_seen_subcommand_from time; and not __fish_seen_subcommand_from start stop status log list' -a stop -d 'Stop the running timer'
2519
+ complete -c cu -n '__fish_seen_subcommand_from time; and not __fish_seen_subcommand_from start stop status log list' -a status -d 'Show the currently running timer'
2520
+ complete -c cu -n '__fish_seen_subcommand_from time; and not __fish_seen_subcommand_from start stop status log list' -a log -d 'Log a manual time entry'
2521
+ complete -c cu -n '__fish_seen_subcommand_from time; and not __fish_seen_subcommand_from start stop status log list' -a list -d 'List recent time entries'
2522
+ complete -c cu -n '__fish_seen_subcommand_from start stop status log list; and __fish_seen_subcommand_from time' -l json -d 'Force JSON output'
2523
+ complete -c cu -n '__fish_seen_subcommand_from start; and __fish_seen_subcommand_from time' -s d -l description -d 'Description'
2524
+ complete -c cu -n '__fish_seen_subcommand_from log; and __fish_seen_subcommand_from time' -s d -l description -d 'Description'
2525
+ complete -c cu -n '__fish_seen_subcommand_from list; and __fish_seen_subcommand_from time' -l days -d 'Number of days to look back'
2526
+ complete -c cu -n '__fish_seen_subcommand_from list; and __fish_seen_subcommand_from time' -l task -d 'Filter by task ID'
2527
+
2528
+ complete -c cu -n '__fish_seen_subcommand_from comment-delete' -l json -d 'Force JSON output'
2529
+
2530
+ complete -c cu -n '__fish_seen_subcommand_from replies' -l json -d 'Force JSON output'
2531
+
2532
+ complete -c cu -n '__fish_seen_subcommand_from reply' -s m -l message -d 'Reply text'
2533
+ complete -c cu -n '__fish_seen_subcommand_from reply' -l json -d 'Force JSON output'
2534
+
2535
+ complete -c cu -n '__fish_seen_subcommand_from link' -l remove -d 'Remove the link'
2536
+ complete -c cu -n '__fish_seen_subcommand_from link' -l json -d 'Force JSON output'
2537
+
2342
2538
  complete -c cu -n '__fish_seen_subcommand_from comment-edit' -s m -l message -d 'New comment text'
2343
2539
  complete -c cu -n '__fish_seen_subcommand_from comment-edit' -l resolved -d 'Mark comment as resolved'
2344
2540
  complete -c cu -n '__fish_seen_subcommand_from comment-edit' -l unresolved -d 'Mark comment as unresolved'
@@ -2651,6 +2847,109 @@ async function editComment(config, commentId, text, resolved) {
2651
2847
  await client.updateComment(commentId, text, resolved);
2652
2848
  }
2653
2849
 
2850
+ // src/commands/comment-delete.ts
2851
+ async function deleteComment(config, commentId) {
2852
+ const client = new ClickUpClient(config);
2853
+ await client.deleteComment(commentId);
2854
+ }
2855
+
2856
+ // src/commands/replies.ts
2857
+ import chalk6 from "chalk";
2858
+ async function getReplies(config, commentId) {
2859
+ const client = new ClickUpClient(config);
2860
+ return client.getThreadedComments(commentId);
2861
+ }
2862
+ async function createReply(config, commentId, text) {
2863
+ if (!text.trim()) throw new Error("Reply text cannot be empty");
2864
+ const client = new ClickUpClient(config);
2865
+ await client.createThreadedComment(commentId, text);
2866
+ }
2867
+ function formatReplies(replies) {
2868
+ if (replies.length === 0) return "No replies";
2869
+ return replies.map((r) => {
2870
+ const user = r.user?.username ?? "Unknown";
2871
+ const date = new Date(Number(r.date)).toLocaleString();
2872
+ return `${chalk6.bold(user)} ${chalk6.dim(date)}
2873
+ ${r.comment_text}`;
2874
+ }).join("\n\n");
2875
+ }
2876
+
2877
+ // src/commands/link.ts
2878
+ async function manageTaskLink(config, taskId, linksTo, remove) {
2879
+ const client = new ClickUpClient(config);
2880
+ if (remove) {
2881
+ await client.deleteTaskLink(taskId, linksTo);
2882
+ return `Removed link between ${taskId} and ${linksTo}`;
2883
+ }
2884
+ await client.addTaskLink(taskId, linksTo);
2885
+ return `Linked ${taskId} to ${linksTo}`;
2886
+ }
2887
+
2888
+ // src/commands/time.ts
2889
+ import chalk7 from "chalk";
2890
+ function formatDuration2(ms) {
2891
+ const totalMinutes = Math.round(Math.abs(ms) / 6e4);
2892
+ const hours = Math.floor(totalMinutes / 60);
2893
+ const minutes = totalMinutes % 60;
2894
+ if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
2895
+ if (hours > 0) return `${hours}h`;
2896
+ return `${minutes}m`;
2897
+ }
2898
+ function formatTimestamp2(ms) {
2899
+ return new Date(Number(ms)).toLocaleString("en-US", {
2900
+ month: "short",
2901
+ day: "numeric",
2902
+ hour: "numeric",
2903
+ minute: "2-digit"
2904
+ });
2905
+ }
2906
+ async function startTimer(config, taskId, description) {
2907
+ const client = new ClickUpClient(config);
2908
+ return client.startTimeEntry(config.teamId, taskId, description);
2909
+ }
2910
+ async function stopTimer(config) {
2911
+ const client = new ClickUpClient(config);
2912
+ return client.stopTimeEntry(config.teamId);
2913
+ }
2914
+ async function timerStatus(config) {
2915
+ const client = new ClickUpClient(config);
2916
+ return client.getRunningTimeEntry(config.teamId);
2917
+ }
2918
+ async function logTime(config, taskId, durationStr, description) {
2919
+ const client = new ClickUpClient(config);
2920
+ const duration = parseTimeEstimate(durationStr);
2921
+ return client.createTimeEntry(config.teamId, taskId, duration, { description });
2922
+ }
2923
+ async function listTimeEntries(config, opts) {
2924
+ const client = new ClickUpClient(config);
2925
+ const days = opts?.days ?? 7;
2926
+ const endDate = Date.now();
2927
+ const startDate = endDate - days * 24 * 60 * 60 * 1e3;
2928
+ return client.getTimeEntries(config.teamId, {
2929
+ startDate,
2930
+ endDate,
2931
+ taskId: opts?.taskId
2932
+ });
2933
+ }
2934
+ function formatTimeEntry(entry) {
2935
+ const lines = [];
2936
+ const taskName = entry.task?.name ?? "No task";
2937
+ const taskId = entry.task?.id ?? "";
2938
+ const isRunning = entry.duration < 0;
2939
+ const elapsed = isRunning ? Date.now() - Number(entry.start) : entry.duration;
2940
+ const durationStr = formatDuration2(elapsed);
2941
+ const status = isRunning ? chalk7.green("RUNNING") : "";
2942
+ lines.push(`${chalk7.bold(taskName)} ${chalk7.dim(taskId)} ${status}`);
2943
+ lines.push(
2944
+ ` ${durationStr} - ${formatTimestamp2(entry.start)}${entry.description ? ` - ${entry.description}` : ""}`
2945
+ );
2946
+ return lines.join("\n");
2947
+ }
2948
+ function formatTimeEntries(entries) {
2949
+ if (entries.length === 0) return "No time entries";
2950
+ return entries.map(formatTimeEntry).join("\n");
2951
+ }
2952
+
2654
2953
  // src/index.ts
2655
2954
  var require2 = createRequire(import.meta.url);
2656
2955
  var { version } = require2("../package.json");
@@ -2808,6 +3107,39 @@ program.command("comment-edit <commentId>").description("Edit an existing commen
2808
3107
  }
2809
3108
  )
2810
3109
  );
3110
+ program.command("comment-delete <commentId>").description("Delete a comment").option("--json", "Force JSON output even in terminal").action(
3111
+ wrapAction(async (commentId, opts) => {
3112
+ const config = loadConfig();
3113
+ await deleteComment(config, commentId);
3114
+ if (shouldOutputJson(opts.json ?? false)) {
3115
+ console.log(JSON.stringify({ success: true, commentId }, null, 2));
3116
+ } else {
3117
+ console.log(`Deleted comment ${commentId}`);
3118
+ }
3119
+ })
3120
+ );
3121
+ program.command("replies <commentId>").description("List threaded replies on a comment").option("--json", "Force JSON output even in terminal").action(
3122
+ wrapAction(async (commentId, opts) => {
3123
+ const config = loadConfig();
3124
+ const replies = await getReplies(config, commentId);
3125
+ if (shouldOutputJson(opts.json ?? false)) {
3126
+ console.log(JSON.stringify(replies, null, 2));
3127
+ } else {
3128
+ console.log(formatReplies(replies));
3129
+ }
3130
+ })
3131
+ );
3132
+ program.command("reply <commentId>").description("Reply to a comment").requiredOption("-m, --message <text>", "Reply text").option("--json", "Force JSON output even in terminal").action(
3133
+ wrapAction(async (commentId, opts) => {
3134
+ const config = loadConfig();
3135
+ await createReply(config, commentId, opts.message);
3136
+ if (shouldOutputJson(opts.json ?? false)) {
3137
+ console.log(JSON.stringify({ success: true, commentId }, null, 2));
3138
+ } else {
3139
+ console.log(`Replied to comment ${commentId}`);
3140
+ }
3141
+ })
3142
+ );
2811
3143
  program.command("activity <taskId>").description("Show task details and comments combined").option("--json", "Force JSON output even in terminal").action(
2812
3144
  wrapAction(async (taskId, opts) => {
2813
3145
  const config = loadConfig();
@@ -2908,6 +3240,25 @@ program.command("depend <taskId>").description("Add or remove task dependencies"
2908
3240
  }
2909
3241
  })
2910
3242
  );
3243
+ program.command("link <taskId> <linksTo>").description("Add or remove a link between two tasks").option("--remove", "Remove the link instead of adding it").option("--json", "Force JSON output even in terminal").action(
3244
+ wrapAction(
3245
+ async (taskId, linksTo, opts) => {
3246
+ const config = loadConfig();
3247
+ const result = await manageTaskLink(config, taskId, linksTo, opts.remove ?? false);
3248
+ if (shouldOutputJson(opts.json ?? false)) {
3249
+ console.log(
3250
+ JSON.stringify(
3251
+ { success: true, taskId, linksTo, action: opts.remove ? "removed" : "added" },
3252
+ null,
3253
+ 2
3254
+ )
3255
+ );
3256
+ } else {
3257
+ console.log(result);
3258
+ }
3259
+ }
3260
+ )
3261
+ );
2911
3262
  program.command("move <taskId>").description("Add or remove a task from a list").option("--to <listId>", "Add task to this list").option("--remove <listId>", "Remove task from this list").option("--json", "Force JSON output even in terminal").action(
2912
3263
  wrapAction(async (taskId, opts) => {
2913
3264
  const config = loadConfig();
@@ -3049,6 +3400,71 @@ checklistCmd.command("delete-item <checklistId> <checklistItemId>").description(
3049
3400
  }
3050
3401
  })
3051
3402
  );
3403
+ var timeCmd = program.command("time").description("Track time on tasks");
3404
+ timeCmd.command("start <taskId>").description("Start tracking time on a task").option("-d, --description <text>", "Description for the time entry").option("--json", "Force JSON output even in terminal").action(
3405
+ wrapAction(async (taskId, opts) => {
3406
+ const config = loadConfig();
3407
+ const result = await startTimer(config, taskId, opts.description);
3408
+ if (shouldOutputJson(opts.json ?? false)) {
3409
+ console.log(JSON.stringify(result, null, 2));
3410
+ } else {
3411
+ const taskName = result.task?.name ?? taskId;
3412
+ console.log(`Started timer on "${taskName}"`);
3413
+ }
3414
+ })
3415
+ );
3416
+ timeCmd.command("stop").description("Stop the running timer").option("--json", "Force JSON output even in terminal").action(
3417
+ wrapAction(async (opts) => {
3418
+ const config = loadConfig();
3419
+ const result = await stopTimer(config);
3420
+ if (shouldOutputJson(opts.json ?? false)) {
3421
+ console.log(JSON.stringify(result, null, 2));
3422
+ } else {
3423
+ console.log(formatTimeEntry(result));
3424
+ }
3425
+ })
3426
+ );
3427
+ timeCmd.command("status").description("Show the currently running timer").option("--json", "Force JSON output even in terminal").action(
3428
+ wrapAction(async (opts) => {
3429
+ const config = loadConfig();
3430
+ const result = await timerStatus(config);
3431
+ if (shouldOutputJson(opts.json ?? false)) {
3432
+ console.log(JSON.stringify(result, null, 2));
3433
+ } else if (result) {
3434
+ console.log(formatTimeEntry(result));
3435
+ } else {
3436
+ console.log("No timer running");
3437
+ }
3438
+ })
3439
+ );
3440
+ timeCmd.command("log <taskId> <duration>").description('Log a manual time entry (e.g. "2h", "30m", "1h30m")').option("-d, --description <text>", "Description for the time entry").option("--json", "Force JSON output even in terminal").action(
3441
+ wrapAction(
3442
+ async (taskId, duration, opts) => {
3443
+ const config = loadConfig();
3444
+ const result = await logTime(config, taskId, duration, opts.description);
3445
+ if (shouldOutputJson(opts.json ?? false)) {
3446
+ console.log(JSON.stringify(result, null, 2));
3447
+ } else {
3448
+ console.log(`Logged ${duration} on task ${taskId}`);
3449
+ }
3450
+ }
3451
+ )
3452
+ );
3453
+ timeCmd.command("list").description("List recent time entries (default: last 7 days)").option("--days <n>", "Number of days to look back", "7").option("--task <taskId>", "Filter by task ID").option("--json", "Force JSON output even in terminal").action(
3454
+ wrapAction(async (opts) => {
3455
+ const config = loadConfig();
3456
+ const days = opts.days ? Number(opts.days) : 7;
3457
+ if (!Number.isFinite(days) || days <= 0) {
3458
+ throw new Error("--days must be a positive number");
3459
+ }
3460
+ const entries = await listTimeEntries(config, { days, taskId: opts.task });
3461
+ if (shouldOutputJson(opts.json ?? false)) {
3462
+ console.log(JSON.stringify(entries, null, 2));
3463
+ } else {
3464
+ console.log(formatTimeEntries(entries));
3465
+ }
3466
+ })
3467
+ );
3052
3468
  var configCmd = program.command("config").description("Manage CLI configuration");
3053
3469
  configCmd.command("get <key>").description("Print a config value").action(
3054
3470
  wrapAction(async (key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clickup
3
- description: 'Use when managing ClickUp tasks, sprints, or comments via the `cu` CLI tool. Triggers: task queries, status updates, sprint tracking, creating subtasks, posting comments, standup summaries, searching tasks, checking overdue items, assigning tasks, listing spaces and lists, opening tasks in browser, checking auth or config, setting custom fields, deleting tasks, managing tags, managing checklists, editing comments.'
3
+ description: 'Use when managing ClickUp tasks, sprints, or comments via the `cu` CLI tool. Triggers: task queries, status updates, sprint tracking, creating subtasks, posting comments, threaded replies, standup summaries, searching tasks, checking overdue items, assigning tasks, listing spaces and lists, opening tasks in browser, checking auth or config, setting custom fields, deleting tasks, managing tags, managing checklists, editing comments, task links, time tracking.'
4
4
  ---
5
5
 
6
6
  # ClickUp CLI (`cu`)
@@ -73,44 +73,57 @@ All commands support `--help` for full flag details.
73
73
  | `cu checklist add-item <checklistId> <name> [--json]` | Add item to a checklist |
74
74
  | `cu checklist edit-item <checklistId> <itemId> [--name n] [--resolved] [--unresolved] [--assignee id] [--json]` | Edit a checklist item |
75
75
  | `cu checklist delete-item <checklistId> <itemId> [--json]` | Delete a checklist item |
76
+ | `cu comment-delete <commentId> [--json]` | Delete a comment |
77
+ | `cu replies <commentId> [--json]` | List threaded replies on a comment |
78
+ | `cu reply <commentId> -m text [--json]` | Reply to a comment |
79
+ | `cu link <taskId> <linksTo> [--remove] [--json]` | Add or remove link between tasks |
80
+ | `cu time start <taskId> [-d desc] [--json]` | Start tracking time on a task |
81
+ | `cu time stop [--json]` | Stop the running timer |
82
+ | `cu time status [--json]` | Show currently running timer |
83
+ | `cu time log <taskId> <duration> [-d desc] [--json]` | Log manual time entry (e.g. "2h", "30m") |
84
+ | `cu time list [--days n] [--task id] [--json]` | List recent time entries |
76
85
  | `cu config get <key>` / `cu config set <key> <value>` / `cu config path` | Manage CLI config |
77
86
  | `cu completion <shell>` | Shell completions (bash/zsh/fish) |
78
87
 
79
88
  ## Quick Reference
80
89
 
81
- | Topic | Detail |
82
- | ----------------------- | ------------------------------------------------------------------------------------------------------ |
83
- | Task IDs | Stable alphanumeric strings (e.g. `abc123def`) |
84
- | `--type` | Filter by task type: `task` (regular), or custom type name/ID (e.g. `initiative`, `Bug`) |
85
- | `--list` on create | Optional when `--parent` is given (auto-detected) |
86
- | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
87
- | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
88
- | `--due-date` | `YYYY-MM-DD` format |
89
- | `--assignee` | User ID or `me` (on `cu create`, `cu update`, `cu assign`) |
90
- | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
91
- | `--time-estimate` | Duration format: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
92
- | `--custom-item-id` | Custom task type ID for `cu create` (e.g. `1` for initiative) |
93
- | `--on` / `--blocks` | Task dependency direction (used with `cu depend`) |
94
- | `--to` / `--remove` | List ID to add/remove task (used with `cu move`) |
95
- | `cu field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), date (YYYY-MM-DD), url, email |
96
- | `cu field` | Field names resolved case-insensitively; errors list available fields/options |
97
- | `cu delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
98
- | `cu tag --add/--remove` | Comma-separated tag names (e.g. `--add "bug,frontend"`) |
99
- | `--space` | Partial name match or exact ID |
100
- | `--name` | Partial match, case-insensitive |
101
- | `--include-closed` | Include closed/done tasks (on `tasks`, `assigned`, `subtasks`, `sprint`, `search`, `inbox`, `overdue`) |
102
- | `cu assign --to me` | Shorthand for your own user ID |
103
- | `cu search` | Matches all query words against task name, case-insensitive |
104
- | `cu sprint` | Auto-detects active sprint via view API and date range parsing |
105
- | `cu summary` | Categories: completed (done/complete/closed within N hours), in progress, overdue |
106
- | `cu overdue` | Excludes closed tasks, sorted most overdue first |
107
- | `cu open` | Tries task ID first, falls back to name search |
108
- | `cu checklist` | Full CRUD for task checklists: view, create, delete, add-item, edit-item, delete-item |
109
- | `cu comment-edit` | Edit comment text and resolution status |
110
- | `cu task` | Shows custom fields and checklists in detail view |
111
- | `cu lists` | Discovers list IDs needed for `--list` and `cu create -l` |
112
- | Errors | stderr with exit code 1 |
113
- | Parsing | Strict - excess/unknown arguments rejected |
90
+ | Topic | Detail |
91
+ | ------------------------- | ------------------------------------------------------------------------------------------------------ |
92
+ | Task IDs | Stable alphanumeric strings (e.g. `abc123def`) |
93
+ | `--type` | Filter by task type: `task` (regular), or custom type name/ID (e.g. `initiative`, `Bug`) |
94
+ | `--list` on create | Optional when `--parent` is given (auto-detected) |
95
+ | `--status` | Fuzzy matching: exact > starts-with > contains. Prints match to stderr. |
96
+ | `--priority` | Names (`urgent`, `high`, `normal`, `low`) or numbers (1-4) |
97
+ | `--due-date` | `YYYY-MM-DD` format |
98
+ | `--assignee` | User ID or `me` (on `cu create`, `cu update`, `cu assign`) |
99
+ | `--tags` | Comma-separated (e.g. `--tags "bug,frontend"`) |
100
+ | `--time-estimate` | Duration format: `"2h"`, `"30m"`, `"1h30m"`, or raw milliseconds |
101
+ | `--custom-item-id` | Custom task type ID for `cu create` (e.g. `1` for initiative) |
102
+ | `--on` / `--blocks` | Task dependency direction (used with `cu depend`) |
103
+ | `--to` / `--remove` | List ID to add/remove task (used with `cu move`) |
104
+ | `cu field --set` | Supports: text, number, checkbox (true/false), dropdown (option name), date (YYYY-MM-DD), url, email |
105
+ | `cu field` | Field names resolved case-insensitively; errors list available fields/options |
106
+ | `cu delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
107
+ | `cu tag --add/--remove` | Comma-separated tag names (e.g. `--add "bug,frontend"`) |
108
+ | `--space` | Partial name match or exact ID |
109
+ | `--name` | Partial match, case-insensitive |
110
+ | `--include-closed` | Include closed/done tasks (on `tasks`, `assigned`, `subtasks`, `sprint`, `search`, `inbox`, `overdue`) |
111
+ | `cu assign --to me` | Shorthand for your own user ID |
112
+ | `cu search` | Matches all query words against task name, case-insensitive |
113
+ | `cu sprint` | Auto-detects active sprint via view API and date range parsing |
114
+ | `cu summary` | Categories: completed (done/complete/closed within N hours), in progress, overdue |
115
+ | `cu overdue` | Excludes closed tasks, sorted most overdue first |
116
+ | `cu open` | Tries task ID first, falls back to name search |
117
+ | `cu checklist` | Full CRUD for task checklists: view, create, delete, add-item, edit-item, delete-item |
118
+ | `cu time` | Track time: start/stop timer, log entries, list history. Duration format: "2h", "30m", "1h30m" |
119
+ | `cu comment-edit` | Edit comment text and resolution status |
120
+ | `cu comment-delete` | Delete a comment |
121
+ | `cu replies` / `cu reply` | View and post threaded comment replies |
122
+ | `cu link` | Link/unlink tasks (different from dependencies) |
123
+ | `cu task` | Shows custom fields and checklists in detail view |
124
+ | `cu lists` | Discovers list IDs needed for `--list` and `cu create -l` |
125
+ | Errors | stderr with exit code 1 |
126
+ | Parsing | Strict - excess/unknown arguments rejected |
114
127
 
115
128
  ## Agent Workflow Examples
116
129
 
@@ -164,6 +177,16 @@ cu checklist create abc123def "QA Steps" # add checklist
164
177
  cu checklist add-item <clId> "Run unit tests" # add item
165
178
  cu checklist edit-item <clId> <itemId> --resolved # check off item
166
179
  cu comment-edit <commentId> -m "Updated findings" # edit a comment
180
+ cu comment-delete <commentId> # delete a comment
181
+ cu replies <commentId> # view threaded replies
182
+ cu reply <commentId> -m "Agreed, fixing" # reply to a comment
183
+ cu link abc123 def456 # link two tasks
184
+ cu link abc123 def456 --remove # unlink two tasks
185
+ cu time start abc123def -d "Working on feature" # start timer
186
+ cu time status # check running timer
187
+ cu time stop # stop timer
188
+ cu time log abc123def 2h -d "Code review" # log manual entry
189
+ cu time list --days 7 # recent entries
167
190
  cu delete abc123def --confirm # irreversible!
168
191
  ```
169
192