@krodak/clickup-cli 1.45.0 → 1.45.2

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.45.0",
4
+ "version": "1.45.2",
5
5
  "author": {
6
6
  "name": "Krzysztof Rodak"
7
7
  },
@@ -269,6 +269,30 @@ async function writeBundleData(root, bundle, opts) {
269
269
  ]);
270
270
  return { dir, contentHash, attachmentsDownloaded: downloaded, attachmentsFailed: failed };
271
271
  }
272
+ async function backfillAttachments(root, taskId, download = defaultDownload) {
273
+ const dir = taskDir(root, taskId);
274
+ const file = join(dir, "attachments.json");
275
+ const entries = JSON.parse(await readFile(file, "utf8"));
276
+ const pending = entries.filter((a) => !a.local);
277
+ if (pending.length === 0) return { downloaded: 0, failed: [] };
278
+ const attDir = join(dir, "attachments");
279
+ await mkdir(attDir, { recursive: true });
280
+ const failed = [];
281
+ let downloaded = 0;
282
+ for (const att of pending) {
283
+ const name = safeAttachmentFilename(att.id, att.title);
284
+ const target = join(attDir, name);
285
+ try {
286
+ if (!existsSync(target)) await writeFile(target, await download(att.url));
287
+ att.local = `attachments/${name}`;
288
+ downloaded++;
289
+ } catch (err) {
290
+ failed.push({ id: att.id, title: att.title, error: err.message });
291
+ }
292
+ }
293
+ await writeFile(file, JSON.stringify(entries, null, 2) + "\n");
294
+ return { downloaded, failed };
295
+ }
272
296
  async function readBundleData(root, taskId) {
273
297
  const dir = taskDir(root, taskId);
274
298
  const [task, comments, attachments, meta] = await Promise.all([
@@ -318,6 +342,7 @@ export {
318
342
  safeAttachmentFilename,
319
343
  taskDir,
320
344
  writeBundleData,
345
+ backfillAttachments,
321
346
  readBundleData,
322
347
  renderBundleMarkdown,
323
348
  writeTaskBundle
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ backfillAttachments,
3
4
  formatDate,
4
5
  formatDateISO,
5
6
  formatDuration,
@@ -8,7 +9,7 @@ import {
8
9
  readBundleData,
9
10
  renderBundleMarkdown,
10
11
  writeBundleData
11
- } from "./chunk-ISEGUZ6H.js";
12
+ } from "./chunk-OPPKVJ6P.js";
12
13
 
13
14
  // src/index.ts
14
15
  import { realpathSync as realpathSync2 } from "fs";
@@ -7463,6 +7464,11 @@ function saveManifest(root, manifest) {
7463
7464
  renameSync(tmp, path);
7464
7465
  }
7465
7466
 
7467
+ // src/util/plural.ts
7468
+ function plural(n, singular, pluralForm = `${singular}s`) {
7469
+ return `${n} ${n === 1 ? singular : pluralForm}`;
7470
+ }
7471
+
7466
7472
  // src/export/engine.ts
7467
7473
  function addSlice(manifest, taskId, slice) {
7468
7474
  const entry = manifest.tasks[taskId];
@@ -7502,6 +7508,11 @@ async function runExport(client, plan, opts) {
7502
7508
  try {
7503
7509
  const cached = await readBundleData(opts.root, id);
7504
7510
  for (const s of cached.subtaskIds) enqueue(s);
7511
+ if (opts.downloadAttachments && cached.attachments.length > 0) {
7512
+ const bf = await backfillAttachments(opts.root, id, opts.download);
7513
+ summary.attachmentsDownloaded += bf.downloaded;
7514
+ summary.attachmentsFailed += bf.failed.length;
7515
+ }
7505
7516
  } catch {
7506
7517
  delete manifest.tasks[id];
7507
7518
  toFetch.push(id);
@@ -7540,7 +7551,7 @@ async function runExport(client, plan, opts) {
7540
7551
  const total = touched.size + queue.length;
7541
7552
  const done = summary.fetched + summary.skipped + summary.failed.length;
7542
7553
  opts.log(
7543
- `[${sliceName}] ${done}/${total} tasks (${summary.fetched} fetched, ${summary.skipped} cached, ${summary.failed.length} failed)`
7554
+ `[${sliceName}] ${done}/${plural(total, "task")} (${summary.fetched} fetched, ${summary.skipped} cached, ${summary.failed.length} failed)`
7544
7555
  );
7545
7556
  if (sinceCheckpoint >= checkpointEvery) {
7546
7557
  saveManifest(opts.root, manifest);
@@ -7628,7 +7639,7 @@ function renderDocReadme(doc, written, location) {
7628
7639
  const lines = [
7629
7640
  `# ${docTitle(doc)}`,
7630
7641
  "",
7631
- `Doc id: ${doc.id} \xB7 Location: ${location} \xB7 ${written.length} pages`,
7642
+ `Doc id: ${doc.id} \xB7 Location: ${location} \xB7 ${plural(written.length, "page")}`,
7632
7643
  ...doc.date_updated ? [`Last updated: ${new Date(Number(doc.date_updated)).toISOString()}`] : [],
7633
7644
  "",
7634
7645
  "## Pages",
@@ -7646,7 +7657,7 @@ function renderDocsIndex(entries) {
7646
7657
  const lines = [
7647
7658
  "# Docs",
7648
7659
  "",
7649
- `${entries.length} docs`,
7660
+ plural(entries.length, "doc"),
7650
7661
  "",
7651
7662
  "| Doc | Location | Pages |",
7652
7663
  "| --- | --- | --- |"
@@ -7680,7 +7691,7 @@ async function exportDocs(client, teamId, opts) {
7680
7691
  };
7681
7692
  const summary = { docs: 0, pages: 0, skipped: 0, failed: [] };
7682
7693
  const entries = [];
7683
- opts.log(`Plan [docs]: ${docs.length} docs in workspace "${teamName}"`);
7694
+ opts.log(`Plan [docs]: ${plural(docs.length, "doc")} in workspace "${teamName}"`);
7684
7695
  for (const doc of docs) {
7685
7696
  const dir = docDirName(doc);
7686
7697
  const location = locationOf(doc);
@@ -7707,7 +7718,7 @@ async function exportDocs(client, teamId, opts) {
7707
7718
  summary.pages += pageCount;
7708
7719
  entries.push({ doc, dir, pages: pageCount, location });
7709
7720
  opts.log(
7710
- `[docs] ${summary.docs + summary.skipped + summary.failed.length}/${docs.length} ${docTitle(doc)} (${pageCount} pages)`
7721
+ `[docs] ${summary.docs + summary.skipped + summary.failed.length}/${docs.length} ${docTitle(doc)} (${plural(pageCount, "page")})`
7711
7722
  );
7712
7723
  } catch (err) {
7713
7724
  summary.failed.push({ id: doc.id, error: err.message });
@@ -7732,17 +7743,16 @@ function link(task) {
7732
7743
  function assignees(task) {
7733
7744
  return task.assignees.map((a) => a.username).join(", ");
7734
7745
  }
7735
- function typeOf(task, initiativeItemId) {
7746
+ function typeOf(task, opts) {
7736
7747
  const id = task.custom_item_id ?? 0;
7737
7748
  if (id === 0) return "task";
7738
- return initiativeItemId !== void 0 && id === initiativeItemId ? "initiative" : `type ${id}`;
7749
+ if (opts.initiativeItemId !== void 0 && id === opts.initiativeItemId) return "initiative";
7750
+ return opts.typeNames?.[id] ?? `type ${id}`;
7739
7751
  }
7740
- function taskTable(tasks, initiativeItemId) {
7752
+ function taskTable(tasks, opts) {
7741
7753
  const lines = ["| Task | Status | Assignees | Type |", "| --- | --- | --- | --- |"];
7742
7754
  for (const t of tasks) {
7743
- lines.push(
7744
- `| ${link(t)} | ${t.status.status} | ${assignees(t)} | ${typeOf(t, initiativeItemId)} |`
7745
- );
7755
+ lines.push(`| ${link(t)} | ${t.status.status} | ${assignees(t)} | ${typeOf(t, opts)} |`);
7746
7756
  }
7747
7757
  return lines;
7748
7758
  }
@@ -7751,22 +7761,21 @@ function renderTeamIndex(hierarchy, tasks, opts) {
7751
7761
  const byList = /* @__PURE__ */ new Map();
7752
7762
  for (const t of topLevel) byList.set(t.list.id, [...byList.get(t.list.id) ?? [], t]);
7753
7763
  const listCount = hierarchy.lists.length + hierarchy.folders.reduce((n, f) => n + f.lists.length, 0);
7754
- const folderWord = hierarchy.folders.length === 1 ? "folder" : "folders";
7755
7764
  const lines = [
7756
7765
  `# ${hierarchy.space.name} (space)`,
7757
7766
  "",
7758
- `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${listCount} lists \xB7 ${hierarchy.folders.length} ${folderWord} \xB7 ${tasks.length} tasks`,
7767
+ `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${plural(listCount, "list")} \xB7 ${plural(hierarchy.folders.length, "folder")} \xB7 ${plural(tasks.length, "task")}`,
7759
7768
  ""
7760
7769
  ];
7761
7770
  const renderList = (list, level) => {
7762
7771
  const group = byList.get(list.id) ?? [];
7763
7772
  const h = "#".repeat(level);
7764
- lines.push(`${h} ${list.name} \u2014 ${group.length} tasks`, "");
7773
+ lines.push(`${h} ${list.name} \u2014 ${plural(group.length, "task")}`, "");
7765
7774
  const related = opts.relatedSlices?.filter((s) => s.listId === list.id) ?? [];
7766
7775
  for (const r of related) {
7767
7776
  lines.push(`\u2192 see also [${r.name}](../${r.name}/README.md)`, "");
7768
7777
  }
7769
- if (group.length > 0) lines.push(...taskTable(group, opts.initiativeItemId), "");
7778
+ if (group.length > 0) lines.push(...taskTable(group, opts), "");
7770
7779
  };
7771
7780
  for (const list of hierarchy.lists) renderList(list, 2);
7772
7781
  for (const folder of hierarchy.folders) {
@@ -7789,7 +7798,7 @@ function renderRoadmapIndex(list, tasks, opts) {
7789
7798
  const lines = [
7790
7799
  `# ${list.name}`,
7791
7800
  "",
7792
- `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${initiatives.length} initiatives \xB7 ${tasks.length} tasks`,
7801
+ `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${plural(initiatives.length, "initiative")} \xB7 ${plural(tasks.length, "task")}`,
7793
7802
  ""
7794
7803
  ];
7795
7804
  const renderTree = (parentId, depth) => {
@@ -7844,7 +7853,7 @@ function renderUserIndex(user, tasks, opts) {
7844
7853
  const lines = [
7845
7854
  `# ${user.username} \u2014 tasks`,
7846
7855
  "",
7847
- `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${tasks.length} tasks assigned \xB7 user id ${user.id}`,
7856
+ `Exported ${opts.exportedAt.slice(0, 10)} \xB7 ${plural(tasks.length, "task")} assigned \xB7 user id ${user.id}`,
7848
7857
  ""
7849
7858
  ];
7850
7859
  const open = tasks.filter((t) => !isDoneStatus(t.status.status));
@@ -7910,7 +7919,7 @@ function renderRootReadme(manifest) {
7910
7919
  const lines = [
7911
7920
  `# ClickUp export${ws ? `: ${ws.name}` : ""}`,
7912
7921
  "",
7913
- `${taskCount} tasks \xB7 ${docCount} docs \xB7 generated by \`cup export\``,
7922
+ `${plural(taskCount, "task")} \xB7 ${plural(docCount, "doc")} \xB7 generated by \`cup export\``,
7914
7923
  "",
7915
7924
  "## Slices",
7916
7925
  "",
@@ -8009,9 +8018,17 @@ function emptySummary(plan, opts) {
8009
8018
  attachmentsFailed: 0
8010
8019
  };
8011
8020
  }
8021
+ async function fetchTypeNames(client, teamId) {
8022
+ try {
8023
+ const types = await client.getCustomTaskTypes(teamId);
8024
+ return Object.fromEntries(types.map((t) => [t.id, t.name]));
8025
+ } catch {
8026
+ return {};
8027
+ }
8028
+ }
8012
8029
  function describePlan(plan) {
8013
8030
  const initiatives = plan.tasks.filter((t) => t.initiative).length;
8014
- return `Plan [${plan.slice.name}]: ${plan.tasks.length} tasks (${initiatives} initiatives) in workspace "${plan.workspace.name}"`;
8031
+ return `Plan [${plan.slice.name}]: ${plural(plan.tasks.length, "task")} (${plural(initiatives, "initiative")}) in workspace "${plan.workspace.name}"`;
8015
8032
  }
8016
8033
  async function execute(client, teamId, plan, opts, writeIndex) {
8017
8034
  opts.log(describePlan(plan));
@@ -8052,7 +8069,7 @@ async function exportUser(config, userRef, opts) {
8052
8069
  });
8053
8070
  }
8054
8071
  function formatExportSummary(s) {
8055
- if (s.dryRun) return `Dry run: ${s.planned} tasks would be exported to ${s.out}`;
8072
+ if (s.dryRun) return `Dry run: ${plural(s.planned, "task")} would be exported to ${s.out}`;
8056
8073
  const lines = [
8057
8074
  `Exported slice "${s.slice}" to ${s.out}`,
8058
8075
  ` tasks: ${s.fetched} fetched, ${s.skipped} already present, ${s.failed.length} failed`,
@@ -8076,13 +8093,15 @@ async function exportTeam(config, spaceRef, opts) {
8076
8093
  const manifest = loadManifest(root);
8077
8094
  const relatedSlices = Object.entries(manifest.slices).filter(([, s]) => s.kind === "roadmap" || s.kind === "initiatives").map(([name, s]) => ({ name, listId: s.scope }));
8078
8095
  const tasks = Object.values(plan.tasksById ?? {});
8096
+ const typeNames = await fetchTypeNames(client, teamId);
8079
8097
  writeSliceFiles(
8080
8098
  root,
8081
8099
  plan.slice.name,
8082
8100
  renderTeamIndex(plan.hierarchy, tasks, {
8083
8101
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8084
8102
  relatedSlices,
8085
- initiativeItemId: opts.initiativeItemId
8103
+ initiativeItemId: opts.initiativeItemId,
8104
+ typeNames
8086
8105
  }),
8087
8106
  { hierarchy: plan.hierarchy, taskIds: plan.tasks.map((t) => t.id) }
8088
8107
  );
@@ -8122,7 +8141,7 @@ async function exportInitiatives(config, listId, opts) {
8122
8141
  initiativeItemId: opts.initiativeItemId
8123
8142
  });
8124
8143
  return execute(client, teamId, plan, opts, async (root) => {
8125
- const { readBundleData: readBundleData2 } = await import("./writer-EYKGCPNB.js");
8144
+ const { readBundleData: readBundleData2 } = await import("./writer-UZHLSQMW.js");
8126
8145
  const manifest = loadManifest(root);
8127
8146
  const wanted = /* @__PURE__ */ new Set();
8128
8147
  const queue = plan.tasks.map((t) => t.id);
@@ -8157,7 +8176,7 @@ async function exportDocs2(config, opts) {
8157
8176
  const root = resolve(opts.out);
8158
8177
  if (opts.dryRun) {
8159
8178
  const docs = await client.getAllDocs(teamId);
8160
- opts.log(`Plan [docs]: ${docs.length} docs`);
8179
+ opts.log(`Plan [docs]: ${plural(docs.length, "doc")}`);
8161
8180
  return {
8162
8181
  slice: "docs",
8163
8182
  out: root,
@@ -8177,7 +8196,7 @@ async function exportDocs2(config, opts) {
8177
8196
  return { slice: "docs", out: root, dryRun: false, ...summary };
8178
8197
  }
8179
8198
  function formatDocsSummary(s) {
8180
- if (s.dryRun) return `Dry run: ${s.docs} docs would be exported to ${s.out}`;
8199
+ if (s.dryRun) return `Dry run: ${plural(s.docs, "doc")} would be exported to ${s.out}`;
8181
8200
  const lines = [
8182
8201
  `Exported docs to ${s.out}/docs`,
8183
8202
  ` docs: ${s.docs} fetched (${s.pages} pages), ${s.skipped} already present, ${s.failed.length} failed`
@@ -8210,9 +8229,9 @@ async function exportAll(config, opts) {
8210
8229
  const estMinutes = Math.ceil(estRequests / opts.rpm);
8211
8230
  opts.log(`Workspace export plan for "${workspaceName}":`);
8212
8231
  opts.log(
8213
- ` ${spaces.length} spaces, ${listCount} lists, ${allTaskIds.size} tasks, ${docs.length} docs`
8232
+ ` ${plural(spaces.length, "space")}, ${plural(listCount, "list")}, ${plural(allTaskIds.size, "task")}, ${plural(docs.length, "doc")}`
8214
8233
  );
8215
- opts.log(` Already exported: ${alreadyExported} tasks (will be skipped)`);
8234
+ opts.log(` Already exported: ${plural(alreadyExported, "task")} (will be skipped)`);
8216
8235
  opts.log(` Estimated requests: ~${estRequests}`);
8217
8236
  opts.log(
8218
8237
  ` Estimated time at ${opts.rpm} req/min: ~${estMinutes < 60 ? `${estMinutes}m` : `${Math.floor(estMinutes / 60)}h ${estMinutes % 60}m`}`
@@ -8246,6 +8265,7 @@ async function exportAll(config, opts) {
8246
8265
  if (!ok) throw new Error("Cancelled");
8247
8266
  }
8248
8267
  const spaceNames = Object.fromEntries(spaces.map((s) => [s.id, s.name]));
8268
+ const typeNames = await fetchTypeNames(client, teamId);
8249
8269
  const summary = { ...empty };
8250
8270
  for (const plan of plans) {
8251
8271
  const run2 = await runExport(client, plan, {
@@ -8267,7 +8287,8 @@ async function exportAll(config, opts) {
8267
8287
  plan.slice.name,
8268
8288
  renderTeamIndex(plan.hierarchy, tasks, {
8269
8289
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
8270
- initiativeItemId: opts.initiativeItemId
8290
+ initiativeItemId: opts.initiativeItemId,
8291
+ typeNames
8271
8292
  }),
8272
8293
  { hierarchy: plan.hierarchy, taskIds: plan.tasks.map((t) => t.id) }
8273
8294
  );
@@ -8278,7 +8299,7 @@ async function exportAll(config, opts) {
8278
8299
  }
8279
8300
  function formatAllSummary(s) {
8280
8301
  if (s.dryRun)
8281
- return `Dry run: ${s.planned} tasks across ${s.spaces.length} spaces would be exported to ${s.out}`;
8302
+ return `Dry run: ${plural(s.planned, "task")} across ${plural(s.spaces.length, "space")} would be exported to ${s.out}`;
8282
8303
  const lines = [
8283
8304
  `Exported workspace to ${s.out}`,
8284
8305
  ` spaces: ${s.spaces.length} (${s.spaces.join(", ")})`,
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ backfillAttachments,
3
4
  readBundleData,
4
5
  renderBundleMarkdown,
5
6
  safeAttachmentFilename,
6
7
  taskDir,
7
8
  writeBundleData,
8
9
  writeTaskBundle
9
- } from "./chunk-ISEGUZ6H.js";
10
+ } from "./chunk-OPPKVJ6P.js";
10
11
  export {
12
+ backfillAttachments,
11
13
  readBundleData,
12
14
  renderBundleMarkdown,
13
15
  safeAttachmentFilename,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krodak/clickup-cli",
3
- "version": "1.45.0",
3
+ "version": "1.45.2",
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.45.0
6
+ # ClickUp CLI (`cup`) - skill version 1.45.2
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.45.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.45.2, update with `npm install -g @krodak/clickup-cli` and refresh this skill with `cup skill`.
11
11
 
12
12
  ## Install & Configure
13
13
 
@@ -434,7 +434,7 @@ Every task stored once (`tasks/<id>/task.json` + `task.md` + comments + attachme
434
434
 
435
435
  ```bash
436
436
  cup export user me --out ./archive # my tasks, incl. closed + archived
437
- cup export team "Kayenta" --out ./archive # every list in a space
437
+ cup export team "Atlas" --out ./archive # every list in a space
438
438
  cup export roadmap <listId> --item-id 1004 --out ./archive # initiatives grouped with subtask trees
439
439
  cup export docs --out ./archive # every doc as markdown page trees
440
440
  cup export all --dry-run # plan: counts, estimated time