@krodak/clickup-cli 1.33.0 → 1.35.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clickup-cli",
3
3
  "description": "ClickUp CLI skills for managing tasks, sprints, comments, checklists, custom fields, tags, and time tracking via the cup command",
4
- "version": "1.33.0",
4
+ "version": "1.35.0",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
package/dist/index.js CHANGED
@@ -97,10 +97,38 @@ var ClickUpClient = class {
97
97
  }
98
98
  return "";
99
99
  }
100
+ sleep(ms) {
101
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
102
+ }
103
+ retryDelayMs(res, attempt) {
104
+ const retryAfter = res.headers.get("retry-after");
105
+ if (retryAfter) {
106
+ const seconds = Number(retryAfter);
107
+ if (Number.isFinite(seconds) && seconds > 0) {
108
+ return Math.min(seconds, 60) * 1e3;
109
+ }
110
+ }
111
+ return Math.min(2 ** (attempt - 1) * 1e3, 6e4);
112
+ }
113
+ async fetchWithRetry(url, init) {
114
+ const maxRetries = 3;
115
+ let attempt = 0;
116
+ for (; ; ) {
117
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(3e4) });
118
+ const retryable = res.status === 429 || res.status === 502 || res.status === 503 || res.status === 504;
119
+ if (!retryable || attempt >= maxRetries) return res;
120
+ attempt++;
121
+ const delayMs = this.retryDelayMs(res, attempt);
122
+ process.stderr.write(
123
+ `Rate limited (${res.status}). Retrying in ${Math.round(delayMs / 1e3)}s... (attempt ${attempt}/${maxRetries})
124
+ `
125
+ );
126
+ await this.sleep(delayMs);
127
+ }
128
+ }
100
129
  async _fetch(baseUrl, path, options = {}) {
101
- const res = await fetch(`${baseUrl}${path}`, {
130
+ const res = await this.fetchWithRetry(`${baseUrl}${path}`, {
102
131
  ...options,
103
- signal: AbortSignal.timeout(3e4),
104
132
  headers: {
105
133
  Authorization: this.apiToken,
106
134
  ...options.body ? { "Content-Type": "application/json" } : {},
@@ -134,8 +162,7 @@ var ClickUpClient = class {
134
162
  return this._fetch(BASE_URL_V3, path, options);
135
163
  }
136
164
  async requestV3Array(path) {
137
- const res = await fetch(`${BASE_URL_V3}${path}`, {
138
- signal: AbortSignal.timeout(3e4),
165
+ const res = await this.fetchWithRetry(`${BASE_URL_V3}${path}`, {
139
166
  headers: { Authorization: this.apiToken }
140
167
  });
141
168
  if (res.status === 204 || res.headers.get("content-length") === "0") {
@@ -4145,6 +4172,18 @@ var commandMetadata = [
4145
4172
  }
4146
4173
  ]
4147
4174
  },
4175
+ {
4176
+ name: "attach-get",
4177
+ description: "Download task attachment(s) by ID or title",
4178
+ flags: ["-o", "--output", "--dir", "--all", "--force", "--json"],
4179
+ quickReference: [
4180
+ {
4181
+ section: "read",
4182
+ usage: "attach-get <taskId> [selector]",
4183
+ description: "Download task attachment(s)"
4184
+ }
4185
+ ]
4186
+ },
4148
4187
  {
4149
4188
  name: "task-members",
4150
4189
  description: "List members with access to a task",
@@ -6922,6 +6961,83 @@ async function attachFile(config, taskId, filePath) {
6922
6961
  return client.createTaskAttachment(taskId, filePath);
6923
6962
  }
6924
6963
 
6964
+ // src/commands/attach-get.ts
6965
+ function sanitizeFilename(name) {
6966
+ const base = name.replace(/[/\\]/g, "_").replace(/^\.+/, "");
6967
+ return base.length > 0 ? base : "attachment";
6968
+ }
6969
+ function selectAttachments(attachments, selector, all) {
6970
+ if (all) return attachments;
6971
+ if (attachments.length === 0) {
6972
+ throw new Error("No attachments found on this task");
6973
+ }
6974
+ if (!selector) {
6975
+ if (attachments.length === 1) return [attachments[0]];
6976
+ const list = attachments.map((a) => ` ${a.id} ${a.title}`).join("\n");
6977
+ throw new Error(
6978
+ `Task has ${attachments.length} attachments. Specify one by ID or title, or use --all:
6979
+ ${list}`
6980
+ );
6981
+ }
6982
+ const lower = selector.toLowerCase();
6983
+ const byId = attachments.find((a) => a.id === selector);
6984
+ if (byId) return [byId];
6985
+ const byExactTitle = attachments.find((a) => a.title.toLowerCase() === lower);
6986
+ if (byExactTitle) return [byExactTitle];
6987
+ const byPartial = attachments.filter((a) => a.title.toLowerCase().includes(lower));
6988
+ if (byPartial.length === 1) return [byPartial[0]];
6989
+ if (byPartial.length > 1) {
6990
+ const list = byPartial.map((a) => ` ${a.id} ${a.title}`).join("\n");
6991
+ throw new Error(`Multiple attachments match "${selector}":
6992
+ ${list}`);
6993
+ }
6994
+ const available = attachments.map((a) => ` ${a.id} ${a.title}`).join("\n");
6995
+ throw new Error(`No attachment matching "${selector}". Available:
6996
+ ${available}`);
6997
+ }
6998
+ async function fileExists(path) {
6999
+ const { access } = await import("fs/promises");
7000
+ try {
7001
+ await access(path);
7002
+ return true;
7003
+ } catch {
7004
+ return false;
7005
+ }
7006
+ }
7007
+ async function downloadAttachment(attachment, targetPath, force) {
7008
+ const { writeFile } = await import("fs/promises");
7009
+ if (!force && await fileExists(targetPath)) {
7010
+ throw new Error(`File already exists: ${targetPath} (use --force to overwrite)`);
7011
+ }
7012
+ const res = await fetch(attachment.url, { signal: AbortSignal.timeout(6e4) });
7013
+ if (!res.ok) {
7014
+ throw new Error(
7015
+ `Failed to download "${attachment.title}": HTTP ${res.status} ${res.statusText}`
7016
+ );
7017
+ }
7018
+ const buffer = Buffer.from(await res.arrayBuffer());
7019
+ await writeFile(targetPath, buffer);
7020
+ return { title: attachment.title, path: targetPath, size: buffer.length };
7021
+ }
7022
+ async function attachGet(config, taskId, selector, opts) {
7023
+ const { resolve: resolve2 } = await import("path");
7024
+ const client = new ClickUpClient(config);
7025
+ const attachments = await client.getTaskAttachments(taskId);
7026
+ const selected = selectAttachments(attachments, selector, opts.all ?? false);
7027
+ const results = [];
7028
+ for (const att of selected) {
7029
+ let targetPath;
7030
+ if (opts.output && !opts.all) {
7031
+ targetPath = resolve2(opts.output);
7032
+ } else {
7033
+ const dir = opts.dir ?? ".";
7034
+ targetPath = resolve2(dir, sanitizeFilename(att.title));
7035
+ }
7036
+ results.push(await downloadAttachment(att, targetPath, opts.force ?? false));
7037
+ }
7038
+ return results;
7039
+ }
7040
+
6925
7041
  // src/commands/docs.ts
6926
7042
  var DOC_COLUMNS = [
6927
7043
  { key: "id", label: "ID", maxWidth: 15 },
@@ -9101,6 +9217,21 @@ function buildProgram(programName = basename(process.argv[1] ?? "cup")) {
9101
9217
  }
9102
9218
  })
9103
9219
  );
9220
+ program.command("attach-get <taskId> [selector]").description("Download task attachment(s) by ID or title").option("-o, --output <path>", "Output file path (single attachment only)").option("--dir <dir>", "Directory to save into (default: current dir)").option("--all", "Download all attachments").option("--force", "Overwrite existing files").option("--json", "Force JSON output even in terminal").action(
9221
+ wrapAction(
9222
+ async (taskId, selector, opts) => {
9223
+ const config = loadConfig(getProfileName());
9224
+ const results = await attachGet(config, taskId, selector, opts);
9225
+ if (shouldOutputJson(opts.json ?? false)) {
9226
+ console.log(JSON.stringify(results, null, 2));
9227
+ } else {
9228
+ for (const r of results) {
9229
+ console.log(`Downloaded "${r.title}" -> ${r.path} (${r.size} bytes)`);
9230
+ }
9231
+ }
9232
+ }
9233
+ )
9234
+ );
9104
9235
  program.command("task-members <taskId>").description("List members with access to a task").option("--json", "Force JSON output even in terminal").action(
9105
9236
  wrapAction(async (taskId, opts) => {
9106
9237
  const config = loadConfig(getProfileName());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.33.0",
3
+ "version": "1.35.0",
4
4
  "description": "ClickUp CLI for AI agents and humans",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,11 +3,11 @@ name: clickup
3
3
  description: 'Use when managing ClickUp tasks, sprints, or comments via the `cup` 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, attachments, file uploads, listing members, listing fields, duplicating tasks, bulk operations, goals, key results, saved filters, favorites.'
4
4
  ---
5
5
 
6
- # ClickUp CLI (`cup`) - skill version 1.33.0
6
+ # ClickUp CLI (`cup`) - skill version 1.35.0
7
7
 
8
8
  Reference for AI agents using the `cup` CLI tool. Covers task management, sprint tracking, comments, time tracking, custom fields, goals, docs, and project workflows.
9
9
 
10
- > **Version check:** Run `cup --version`. If your installed version is older than 1.33.0, update with `npm install -g @krodak/clickup-cli` and refresh this skill with `cup skill`.
10
+ > **Version check:** Run `cup --version`. If your installed version is older than 1.35.0, update with `npm install -g @krodak/clickup-cli` and refresh this skill with `cup skill`.
11
11
 
12
12
  ## Install & Configure
13
13
 
@@ -107,55 +107,56 @@ All commands support `--help` for full flag details. All commands support `--jso
107
107
 
108
108
  ### Read
109
109
 
110
- | Command | What it returns |
111
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
112
- | `cup tasks [--status s] [--name q] [--type t] [--list id] [--space id] [--all] [--include-closed] [--assignee id\|me] [--tag t] [--due-before d] [--due-after d] [--created-after d] [--created-before d] [--field "Name" val]` | Tasks assigned to you (filter by status, name, type, list, space, assignee, tag, dates, custom fields). `--all` for all assignees |
113
- | `cup assigned [--status s] [--include-closed]` | All my tasks grouped by status |
114
- | `cup sprint [--status s] [--space nameOrId] [--folder id] [--include-closed]` | Tasks in active sprint (auto-detected) |
115
- | `cup sprints [--space nameOrId]` | List all sprints (marks active with \*) |
116
- | `cup search <query> [--status s] [--list id] [--space id] [--all] [--include-closed] [--assignee id\|me] [--tag t] [--due-before d] [--due-after d] [--created-after d] [--created-before d] [--field "Name" val]` | Search your tasks by name. `--all` for all assignees |
117
- | `cup task <id>` | Single task details (custom fields, checklists, attachments, deps, links) |
118
- | `cup subtasks <id> [--status s] [--name q] [--include-closed]` | Subtasks of a task |
119
- | `cup comments <id>` | Comments on a task |
120
- | `cup activity <id>` | Task details + comment history combined |
121
- | `cup inbox [--days n] [--include-closed]` | Tasks updated in last n days (default 30) |
122
- | `cup summary [--hours n]` | Standup: completed, in-progress, overdue |
123
- | `cup overdue [--all] [--include-closed]` | Tasks past due date (most overdue first) |
124
- | `cup spaces [--name partial] [--my] [--archived]` | List/filter workspace spaces |
125
- | `cup lists <spaceId> [--name partial] [--archived]` | Lists in a space (including folder lists) |
126
- | `cup folders <spaceId> [--name partial] [--archived]` | Folders in a space (with their lists) |
127
- | `cup time-in-status <id>` | Show how long a task has been in each status |
128
- | `cup members` | Workspace members (username, ID, email) |
129
- | `cup groups` | User groups/teams (handle, name, UUID, member count) for `--group-assignee` flags |
130
- | `cup fields <listId>` | Custom fields on a list (type, required, options) |
131
- | `cup attachments <taskId>` | List attachments on a task (name, size, URL) |
132
- | `cup task-members <taskId>` | List members with access to a task |
133
- | `cup plan` | Show workspace plan and usage |
134
- | `cup tags <spaceId>` | Tags available in a space |
135
- | `cup goals` | Workspace goals with progress |
136
- | `cup key-results <goalId>` | Key results for a goal |
137
- | `cup docs [query]` | Workspace docs (optionally filter by name) |
138
- | `cup doc <docId> [pageId]` | Doc metadata + page tree, or a specific page |
139
- | `cup doc-pages <docId>` | All pages in a doc with content |
140
- | `cup task-types` | Custom task types (for `--custom-item-id`) |
141
- | `cup templates` | Task templates (for `--template`) |
142
- | `cup list-templates` | List templates (for `list-from-template`) |
143
- | `cup folder-templates` | Folder templates |
144
- | `cup views <listId>` | List views on a list |
145
- | `cup view <viewId>` | Get view details |
146
- | `cup open <query>` | Open task in browser by ID or name |
147
- | `cup auth` | Check authentication status |
148
- | `cup list-comments <listId>` | Comments on a list |
149
- | `cup view-comments <viewId>` | Comments on a view |
150
- | `cup webhook list` | List webhooks in workspace |
151
- | `cup shared` | Shared spaces, folders, and lists |
152
- | `cup chat channels [--all] [--type type]` | List chat channels |
153
- | `cup chat channel <id>` | Show channel details |
154
- | `cup chat messages <channelId> [--limit n]` | List channel messages |
155
- | `cup chat members <channelId>` | List channel members |
156
- | `cup chat followers <channelId>` | List channel followers |
157
- | `cup chat replies <messageId>` | List message replies |
158
- | `cup chat reactions <messageId>` | List reactions on a message |
110
+ | Command | What it returns |
111
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
112
+ | `cup tasks [--status s] [--name q] [--type t] [--list id] [--space id] [--all] [--include-closed] [--assignee id\|me] [--tag t] [--due-before d] [--due-after d] [--created-after d] [--created-before d] [--field "Name" val]` | Tasks assigned to you (filter by status, name, type, list, space, assignee, tag, dates, custom fields). `--all` for all assignees |
113
+ | `cup assigned [--status s] [--include-closed]` | All my tasks grouped by status |
114
+ | `cup sprint [--status s] [--space nameOrId] [--folder id] [--include-closed]` | Tasks in active sprint (auto-detected) |
115
+ | `cup sprints [--space nameOrId]` | List all sprints (marks active with \*) |
116
+ | `cup search <query> [--status s] [--list id] [--space id] [--all] [--include-closed] [--assignee id\|me] [--tag t] [--due-before d] [--due-after d] [--created-after d] [--created-before d] [--field "Name" val]` | Search your tasks by name. `--all` for all assignees |
117
+ | `cup task <id>` | Single task details (custom fields, checklists, attachments, deps, links) |
118
+ | `cup subtasks <id> [--status s] [--name q] [--include-closed]` | Subtasks of a task |
119
+ | `cup comments <id>` | Comments on a task |
120
+ | `cup activity <id>` | Task details + comment history combined |
121
+ | `cup inbox [--days n] [--include-closed]` | Tasks updated in last n days (default 30) |
122
+ | `cup summary [--hours n]` | Standup: completed, in-progress, overdue |
123
+ | `cup overdue [--all] [--include-closed]` | Tasks past due date (most overdue first) |
124
+ | `cup spaces [--name partial] [--my] [--archived]` | List/filter workspace spaces |
125
+ | `cup lists <spaceId> [--name partial] [--archived]` | Lists in a space (including folder lists) |
126
+ | `cup folders <spaceId> [--name partial] [--archived]` | Folders in a space (with their lists) |
127
+ | `cup time-in-status <id>` | Show how long a task has been in each status |
128
+ | `cup members` | Workspace members (username, ID, email) |
129
+ | `cup groups` | User groups/teams (handle, name, UUID, member count) for `--group-assignee` flags |
130
+ | `cup fields <listId>` | Custom fields on a list (type, required, options) |
131
+ | `cup attachments <taskId>` | List attachments on a task (name, size, URL) |
132
+ | `cup attach-get <taskId> [idOrTitle] [-o path] [--all] [--dir d] [--force]` | Download task attachment(s). No selector + 1 attachment downloads it; multiple requires a selector or `--all`. Saves to the attachment's filename by default |
133
+ | `cup task-members <taskId>` | List members with access to a task |
134
+ | `cup plan` | Show workspace plan and usage |
135
+ | `cup tags <spaceId>` | Tags available in a space |
136
+ | `cup goals` | Workspace goals with progress |
137
+ | `cup key-results <goalId>` | Key results for a goal |
138
+ | `cup docs [query]` | Workspace docs (optionally filter by name) |
139
+ | `cup doc <docId> [pageId]` | Doc metadata + page tree, or a specific page |
140
+ | `cup doc-pages <docId>` | All pages in a doc with content |
141
+ | `cup task-types` | Custom task types (for `--custom-item-id`) |
142
+ | `cup templates` | Task templates (for `--template`) |
143
+ | `cup list-templates` | List templates (for `list-from-template`) |
144
+ | `cup folder-templates` | Folder templates |
145
+ | `cup views <listId>` | List views on a list |
146
+ | `cup view <viewId>` | Get view details |
147
+ | `cup open <query>` | Open task in browser by ID or name |
148
+ | `cup auth` | Check authentication status |
149
+ | `cup list-comments <listId>` | Comments on a list |
150
+ | `cup view-comments <viewId>` | Comments on a view |
151
+ | `cup webhook list` | List webhooks in workspace |
152
+ | `cup shared` | Shared spaces, folders, and lists |
153
+ | `cup chat channels [--all] [--type type]` | List chat channels |
154
+ | `cup chat channel <id>` | Show channel details |
155
+ | `cup chat messages <channelId> [--limit n]` | List channel messages |
156
+ | `cup chat members <channelId>` | List channel members |
157
+ | `cup chat followers <channelId>` | List channel followers |
158
+ | `cup chat replies <messageId>` | List message replies |
159
+ | `cup chat reactions <messageId>` | List reactions on a message |
159
160
 
160
161
  ### Write
161
162
 
@@ -288,6 +289,7 @@ All commands support `--help` for full flag details. All commands support `--jso
288
289
  | `cup link` | Both IDs must be the same type (both custom or both native) |
289
290
  | `cup delete` | DESTRUCTIVE. Requires `--confirm` in non-interactive mode. Cannot be undone |
290
291
  | Errors | stderr with exit code 1. Strict parsing - excess/unknown arguments rejected |
292
+ | Rate limiting | The client auto-retries on HTTP 429 and transient 5xx (502/503/504) with `Retry-After`-aware backoff (up to 3 retries). Retry warnings go to stderr. No action needed by callers |
291
293
  | Description quoting | Use `$'...'` quoting for `-d` / `-m` values containing backticks or newlines: `-d $'Use \`init()\` first.\n\n- Step 1'`. Heredocs and double quotes strip backticks |
292
294
  | `--mention <user>` | Real ClickUp @mention (notifies the user). Accepts ID, email, username, or `me`. Repeatable. Also: `<@userId>` inline token in `-m` for mid-sentence mentions. Bare `@Name` is NOT parsed (ambiguous). On `comment`, `reply`, `comment-edit`, `list-comment`, `view-comment` |
293
295
  | Comment links | In comment messages, bare URLs (`https://...`) and markdown links (`[text](url)`) both render as clickable links. Unfurled preview cards are not supported (web-UI only) |