clickup-agent-cli 0.3.0 → 0.4.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/clickup.js CHANGED
@@ -31,20 +31,21 @@ function parseApiError(responseBody, status) {
31
31
  }
32
32
  function mapToExitCode(error) {
33
33
  if (!(error instanceof ClickUpError)) {
34
- return 1;
34
+ return EXIT_CODES.GENERAL_ERROR;
35
35
  }
36
36
  switch (error.status) {
37
+ case 0:
38
+ return EXIT_CODES.NETWORK_ERROR;
37
39
  case 401:
38
- return 3;
40
+ return EXIT_CODES.AUTH_FAILURE;
39
41
  case 403:
40
- return 5;
42
+ return EXIT_CODES.PERMISSION_DENIED;
41
43
  case 404:
42
- return 4;
44
+ return EXIT_CODES.NOT_FOUND;
43
45
  case 429:
44
- return 6;
46
+ return EXIT_CODES.RATE_LIMITED;
45
47
  default:
46
- if (error.status >= 500) return 1;
47
- return 1;
48
+ return EXIT_CODES.GENERAL_ERROR;
48
49
  }
49
50
  }
50
51
  var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES, DryRunComplete;
@@ -149,9 +150,19 @@ var init_client = __esm({
149
150
  async downloadUrl(url) {
150
151
  const controller = new AbortController();
151
152
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
153
+ const headers = {};
154
+ try {
155
+ const { hostname } = new URL(url);
156
+ if (hostname === "clickup.com" || hostname.endsWith(".clickup.com")) {
157
+ headers["Authorization"] = this.token;
158
+ }
159
+ } catch {
160
+ clearTimeout(timeoutId);
161
+ throw new ClickUpError(`Invalid download URL: ${url}`, 0, void 0, void 0);
162
+ }
152
163
  try {
153
164
  const response = await fetch(url, {
154
- headers: { Authorization: this.token },
165
+ headers,
155
166
  signal: controller.signal
156
167
  });
157
168
  clearTimeout(timeoutId);
@@ -271,17 +282,6 @@ Headers: ${JSON.stringify(redactedHeaders)}
271
282
  process.stderr.write(`[${method}] ${path} ${response.status} ${response.statusText} (${elapsed}ms)
272
283
  `);
273
284
  }
274
- const remaining = response.headers.get("X-RateLimit-Remaining");
275
- const reset = response.headers.get("X-RateLimit-Reset");
276
- if (remaining === "0" && reset) {
277
- const resetTime = parseInt(reset, 10) * 1e3;
278
- const waitMs = Math.max(0, resetTime - Date.now());
279
- if (waitMs > 0) {
280
- process.stderr.write(`Rate limited. Waiting ${Math.ceil(waitMs / 1e3)}s...
281
- `);
282
- await this.sleep(waitMs);
283
- }
284
- }
285
285
  if (!response.ok) {
286
286
  const responseBody = await this.safeJson(response);
287
287
  const error = parseApiError(responseBody, response.status);
@@ -293,6 +293,18 @@ Headers: ${JSON.stringify(redactedHeaders)}
293
293
  throw error;
294
294
  }
295
295
  if (RETRYABLE_STATUSES.has(response.status) && attempt < this.maxRetries) {
296
+ if (response.status === 429) {
297
+ const reset = response.headers.get("X-RateLimit-Reset");
298
+ if (reset) {
299
+ const resetTime = parseInt(reset, 10) * 1e3;
300
+ const waitMs = Math.min(Math.max(0, resetTime - Date.now()), 6e4);
301
+ if (waitMs > 0) {
302
+ process.stderr.write(`Rate limited. Waiting ${Math.ceil(waitMs / 1e3)}s...
303
+ `);
304
+ await this.sleep(waitMs);
305
+ }
306
+ }
307
+ }
296
308
  lastError = error;
297
309
  continue;
298
310
  }
@@ -352,18 +364,24 @@ Headers: ${JSON.stringify(redactedHeaders)}
352
364
  }
353
365
  }
354
366
  sleep(ms) {
355
- return new Promise((resolve) => setTimeout(resolve, ms));
367
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
356
368
  }
357
369
  };
358
370
  }
359
371
  });
360
372
 
361
373
  // src/config.ts
362
- import { readFileSync as readFileSync2 } from "fs";
374
+ import { readFileSync as readFileSync2, chmodSync, existsSync } from "fs";
363
375
  import Conf from "conf";
364
376
  function isValidConfigKey(key) {
365
377
  return ALL_CONFIG_KEYS.includes(key);
366
378
  }
379
+ function hardenConfigFile() {
380
+ try {
381
+ if (existsSync(config.path)) chmodSync(config.path, 384);
382
+ } catch {
383
+ }
384
+ }
367
385
  function setProfileOverride(nameOrKey) {
368
386
  _profileOverride = nameOrKey ? findProfileKey(nameOrKey) : void 0;
369
387
  }
@@ -376,10 +394,14 @@ function getProfiles() {
376
394
  const stored = config.get("profiles");
377
395
  return stored ?? {};
378
396
  }
397
+ function getProfile(key) {
398
+ return getProfiles()[key];
399
+ }
379
400
  function setProfile(key, profile) {
380
401
  const profiles = getProfiles();
381
402
  profiles[key] = profile;
382
403
  config.set("profiles", profiles);
404
+ hardenConfigFile();
383
405
  }
384
406
  function deleteProfile(key) {
385
407
  const profiles = getProfiles();
@@ -420,11 +442,17 @@ function migrateConfig() {
420
442
  function resolveToken(flagValue, tokenFilePath) {
421
443
  if (flagValue) return flagValue;
422
444
  if (tokenFilePath) {
445
+ let content;
423
446
  try {
424
- const content = readFileSync2(tokenFilePath, "utf8").trim();
425
- if (content) return content;
426
- } catch {
447
+ content = readFileSync2(tokenFilePath, "utf8").trim();
448
+ } catch (error) {
449
+ const reason = error instanceof Error ? error.message : String(error);
450
+ throw new Error(`Cannot read token file "${tokenFilePath}": ${reason}`);
427
451
  }
452
+ if (!content) {
453
+ throw new Error(`Token file "${tokenFilePath}" is empty`);
454
+ }
455
+ return content;
428
456
  }
429
457
  const envVal = process.env["CLICKUP_API_TOKEN"];
430
458
  if (envVal) return envVal;
@@ -469,7 +497,7 @@ var init_config = __esm({
469
497
  schema: {
470
498
  token: { type: "string" },
471
499
  workspace_id: { type: "string" },
472
- output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id"] },
500
+ output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id", "md"] },
473
501
  color: { type: "boolean", default: true },
474
502
  page_size: { type: "number", default: 100 },
475
503
  timezone: { type: "string" },
@@ -477,6 +505,7 @@ var init_config = __esm({
477
505
  profiles: { type: "object" }
478
506
  }
479
507
  });
508
+ hardenConfigFile();
480
509
  try {
481
510
  migrateConfig();
482
511
  } catch {
@@ -492,22 +521,29 @@ __export(auth_exports, {
492
521
  oauthLogin: () => oauthLogin
493
522
  });
494
523
  import { createServer } from "http";
495
- import { randomBytes, createHash } from "crypto";
524
+ import { randomBytes, createHash, timingSafeEqual } from "crypto";
496
525
  function generateCodeVerifier() {
497
526
  return randomBytes(48).toString("base64url");
498
527
  }
499
528
  function generateCodeChallenge(verifier) {
500
529
  return createHash("sha256").update(verifier).digest("base64url");
501
530
  }
502
- function buildAuthorizeUrl(clientId, codeChallenge) {
531
+ function buildAuthorizeUrl(clientId, codeChallenge, state) {
503
532
  const params = new URLSearchParams({
504
533
  client_id: clientId,
505
534
  redirect_uri: REDIRECT_URI,
506
535
  code_challenge: codeChallenge,
507
- code_challenge_method: "S256"
536
+ code_challenge_method: "S256",
537
+ state
508
538
  });
509
539
  return `${CLICKUP_AUTHORIZE_URL}?${params.toString()}`;
510
540
  }
541
+ function statesMatch(expected, received) {
542
+ const a = Buffer.from(expected);
543
+ const b = Buffer.from(received);
544
+ if (a.length !== b.length) return false;
545
+ return timingSafeEqual(a, b);
546
+ }
511
547
  async function exchangeCodeForToken(code, clientId, clientSecret, codeVerifier) {
512
548
  const response = await fetch(CLICKUP_TOKEN_URL, {
513
549
  method: "POST",
@@ -530,8 +566,8 @@ async function exchangeCodeForToken(code, clientId, clientSecret, codeVerifier)
530
566
  }
531
567
  return token;
532
568
  }
533
- function waitForCallback(server) {
534
- return new Promise((resolve, reject) => {
569
+ function waitForCallback(server, expectedState) {
570
+ return new Promise((resolve2, reject) => {
535
571
  const timeout = setTimeout(() => {
536
572
  server.close();
537
573
  reject(new Error("OAuth callback timed out after 120 seconds"));
@@ -545,6 +581,7 @@ function waitForCallback(server) {
545
581
  }
546
582
  const code = url.searchParams.get("code");
547
583
  const error = url.searchParams.get("error");
584
+ const state = url.searchParams.get("state");
548
585
  if (error) {
549
586
  res.writeHead(200, { "Content-Type": "text/html" });
550
587
  res.end("<html><body><h1>Authentication failed</h1><p>You can close this tab.</p></body></html>");
@@ -553,6 +590,11 @@ function waitForCallback(server) {
553
590
  reject(new Error(`OAuth error: ${error}`));
554
591
  return;
555
592
  }
593
+ if (!state || !statesMatch(expectedState, state)) {
594
+ res.writeHead(400, { "Content-Type": "text/html" });
595
+ res.end("<html><body><h1>Invalid request</h1><p>State mismatch. Please retry the login.</p></body></html>");
596
+ return;
597
+ }
556
598
  if (!code) {
557
599
  res.writeHead(400, { "Content-Type": "text/html" });
558
600
  res.end("<html><body><h1>Missing code</h1><p>No authorization code received.</p></body></html>");
@@ -562,24 +604,28 @@ function waitForCallback(server) {
562
604
  res.end("<html><body><h1>Authenticated</h1><p>You can close this tab and return to the terminal.</p></body></html>");
563
605
  clearTimeout(timeout);
564
606
  server.close();
565
- resolve(code);
607
+ resolve2(code);
566
608
  });
567
609
  });
568
610
  }
569
611
  async function openBrowser(url) {
570
612
  const { platform } = process;
613
+ const { execFile } = await import("child_process");
571
614
  let command;
615
+ let args;
572
616
  if (platform === "darwin") {
573
617
  command = "open";
618
+ args = [url];
574
619
  } else if (platform === "win32") {
575
- command = "start";
620
+ command = "cmd";
621
+ args = ["/c", "start", "", url];
576
622
  } else {
577
623
  command = "xdg-open";
624
+ args = [url];
578
625
  }
579
- const { exec } = await import("child_process");
580
- return new Promise((resolve) => {
581
- exec(`${command} "${url}"`, () => {
582
- resolve();
626
+ return new Promise((resolve2) => {
627
+ execFile(command, args, () => {
628
+ resolve2();
583
629
  });
584
630
  });
585
631
  }
@@ -598,22 +644,24 @@ async function oauthLogin() {
598
644
  }
599
645
  const codeVerifier = generateCodeVerifier();
600
646
  const codeChallenge = generateCodeChallenge(codeVerifier);
647
+ const state = randomBytes(24).toString("base64url");
601
648
  const server = createServer();
602
- await new Promise((resolve, reject) => {
603
- server.listen(CALLBACK_PORT, () => resolve());
649
+ await new Promise((resolve2, reject) => {
650
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => resolve2());
604
651
  server.on("error", reject);
605
652
  });
606
- const authorizeUrl = buildAuthorizeUrl(clientId, codeChallenge);
653
+ const authorizeUrl = buildAuthorizeUrl(clientId, codeChallenge, state);
607
654
  process.stderr.write(`Opening browser for authentication...
608
655
  `);
609
656
  process.stderr.write(`If the browser does not open, visit:
610
657
  ${authorizeUrl}
611
658
  `);
612
659
  await openBrowser(authorizeUrl);
613
- const code = await waitForCallback(server);
660
+ const code = await waitForCallback(server, state);
614
661
  process.stderr.write("Exchanging code for token...\n");
615
662
  const token = await exchangeCodeForToken(code, clientId, clientSecret, codeVerifier);
616
- config.set("token", token);
663
+ const profileKey = getActiveProfileKey();
664
+ setProfile(profileKey, { ...getProfile(profileKey), token });
617
665
  return token;
618
666
  }
619
667
  var CLICKUP_AUTHORIZE_URL, CLICKUP_TOKEN_URL, CALLBACK_PORT, CALLBACK_PATH, REDIRECT_URI;
@@ -633,7 +681,7 @@ var init_auth = __esm({
633
681
  init_client();
634
682
  init_errors();
635
683
  init_config();
636
- import { Command } from "commander";
684
+ import { Command, Option } from "commander";
637
685
 
638
686
  // src/commands/auth-cmd.ts
639
687
  init_config();
@@ -777,10 +825,20 @@ function registerConfigCommands(program, getClient) {
777
825
  return;
778
826
  }
779
827
  config.set(key, num);
828
+ } else if (key === "output_format") {
829
+ const valid = ["table", "json", "csv", "tsv", "quiet", "id", "md"];
830
+ if (!valid.includes(value)) {
831
+ process.stderr.write(`Error: output_format must be one of: ${valid.join(", ")}
832
+ `);
833
+ process.exit(2);
834
+ return;
835
+ }
836
+ config.set(key, value);
780
837
  } else {
781
838
  config.set(key, value);
782
839
  }
783
- process.stdout.write(`Set ${key} = ${value}
840
+ const shown = key === "token" ? `${value.slice(0, 8)}...` : value;
841
+ process.stdout.write(`Set ${key} = ${shown}
784
842
  `);
785
843
  });
786
844
  configCmd.command("get").description("Get a configuration value").argument("<key>", "Config key").action((key) => {
@@ -804,7 +862,8 @@ function registerConfigCommands(program, getClient) {
804
862
  for (const key of keys) {
805
863
  const value = store[key];
806
864
  if (value !== void 0) {
807
- process.stdout.write(`${key} = ${value}
865
+ const shown = key === "token" ? `${String(value).slice(0, 8)}...` : value;
866
+ process.stdout.write(`${key} = ${shown}
808
867
  `);
809
868
  }
810
869
  }
@@ -1045,18 +1104,23 @@ function applySort(rows, sort) {
1045
1104
  return desc ? -result : result;
1046
1105
  });
1047
1106
  }
1107
+ var CONTROL_CHARS_RE = new RegExp("[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f]", "g");
1108
+ function sanitize(str) {
1109
+ return str.replace(/\r?\n/g, " ").replace(CONTROL_CHARS_RE, "");
1110
+ }
1048
1111
  function getValue(row, key) {
1049
1112
  if (row && typeof row === "object") {
1050
1113
  const record = row;
1051
1114
  const val = record[key];
1052
1115
  if (val === null || val === void 0) return "";
1053
- if (typeof val === "object") return JSON.stringify(val);
1054
- return String(val);
1116
+ if (typeof val === "object") return sanitize(JSON.stringify(val));
1117
+ return sanitize(String(val));
1055
1118
  }
1056
1119
  return "";
1057
1120
  }
1058
1121
  function truncate(str, width) {
1059
1122
  if (str.length <= width) return str;
1123
+ if (width <= 3) return str.slice(0, Math.max(0, width));
1060
1124
  return str.slice(0, width - 3) + "...";
1061
1125
  }
1062
1126
  function printTable(rows, columns, opts) {
@@ -1086,7 +1150,10 @@ function printDelimited(rows, columns, delimiter, opts) {
1086
1150
  }
1087
1151
  for (const row of rows) {
1088
1152
  const values = columns.map((col) => {
1089
- const val = getValue(row, col.key);
1153
+ let val = getValue(row, col.key);
1154
+ if (delimiter === "," && /^[=+\-@\t\r]/.test(val)) {
1155
+ val = `'${val}`;
1156
+ }
1090
1157
  if (delimiter === "," && (val.includes(",") || val.includes('"') || val.includes("\n"))) {
1091
1158
  return `"${val.replace(/"/g, '""')}"`;
1092
1159
  }
@@ -1477,6 +1544,124 @@ function registerFolderCommands(program, getClient) {
1477
1544
  });
1478
1545
  }
1479
1546
 
1547
+ // src/dates.ts
1548
+ var RELATIVE_OFFSET_RE = /^([+-]\d+)([dwmh])$/;
1549
+ var UNIX_MS_RE = /^\d{13,}$/;
1550
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1551
+ var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
1552
+ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1553
+ function parseDate(input) {
1554
+ const raw = input.trim();
1555
+ const lower = raw.toLowerCase();
1556
+ if (UNIX_MS_RE.test(raw)) {
1557
+ return parseInt(raw, 10);
1558
+ }
1559
+ const now = /* @__PURE__ */ new Date();
1560
+ switch (lower) {
1561
+ case "today": {
1562
+ return startOfDay(now).getTime();
1563
+ }
1564
+ case "tomorrow": {
1565
+ const d = startOfDay(now);
1566
+ d.setDate(d.getDate() + 1);
1567
+ return d.getTime();
1568
+ }
1569
+ case "yesterday": {
1570
+ const d = startOfDay(now);
1571
+ d.setDate(d.getDate() - 1);
1572
+ return d.getTime();
1573
+ }
1574
+ }
1575
+ const offsetMatch = RELATIVE_OFFSET_RE.exec(lower);
1576
+ if (offsetMatch) {
1577
+ const amount = parseInt(offsetMatch[1], 10);
1578
+ const unit = offsetMatch[2];
1579
+ const ms = unitToMs(unit, amount);
1580
+ return now.getTime() + ms;
1581
+ }
1582
+ if (lower.startsWith("next ")) {
1583
+ const dayName = lower.slice(5);
1584
+ const dayIndex = DAY_NAMES.indexOf(dayName);
1585
+ if (dayIndex !== -1) {
1586
+ const today = now.getDay();
1587
+ let daysAhead = dayIndex - today;
1588
+ if (daysAhead <= 0) daysAhead += 7;
1589
+ const target = startOfDay(now);
1590
+ target.setDate(target.getDate() + daysAhead);
1591
+ return target.getTime();
1592
+ }
1593
+ }
1594
+ if (ISO_DATE_RE.test(raw)) {
1595
+ const d = /* @__PURE__ */ new Date(raw + "T00:00:00");
1596
+ if (!isNaN(d.getTime())) return d.getTime();
1597
+ }
1598
+ if (ISO_DATETIME_RE.test(raw)) {
1599
+ const d = new Date(raw);
1600
+ if (!isNaN(d.getTime())) return d.getTime();
1601
+ }
1602
+ throw new Error(`Unable to parse date: "${input}"`);
1603
+ }
1604
+ function startOfDay(date) {
1605
+ const d = new Date(date);
1606
+ d.setHours(0, 0, 0, 0);
1607
+ return d;
1608
+ }
1609
+ function unitToMs(unit, amount) {
1610
+ switch (unit) {
1611
+ case "m":
1612
+ return amount * 60 * 1e3;
1613
+ case "h":
1614
+ return amount * 60 * 60 * 1e3;
1615
+ case "d":
1616
+ return amount * 24 * 60 * 60 * 1e3;
1617
+ case "w":
1618
+ return amount * 7 * 24 * 60 * 60 * 1e3;
1619
+ default:
1620
+ return 0;
1621
+ }
1622
+ }
1623
+
1624
+ // src/parse.ts
1625
+ function fail(message) {
1626
+ process.stderr.write(`Error: ${message}
1627
+ `);
1628
+ process.exit(2);
1629
+ }
1630
+ function parseIntStrict(value, flag) {
1631
+ const num = parseInt(value, 10);
1632
+ if (isNaN(num)) fail(`${flag} must be a number, got "${value}"`);
1633
+ return num;
1634
+ }
1635
+ function parseFloatStrict(value, flag) {
1636
+ const num = parseFloat(value);
1637
+ if (isNaN(num)) fail(`${flag} must be a number, got "${value}"`);
1638
+ return num;
1639
+ }
1640
+ function parseDateStrict(value, flag) {
1641
+ try {
1642
+ return parseDate(value);
1643
+ } catch {
1644
+ fail(`${flag} must be a date (ISO 8601, relative like +3d, or Unix ms), got "${value}"`);
1645
+ }
1646
+ }
1647
+ function parseBoolStrict(value, flag) {
1648
+ if (value === "true") return true;
1649
+ if (value === "false") return false;
1650
+ fail(`${flag} must be "true" or "false", got "${value}"`);
1651
+ }
1652
+ function intArg(flag) {
1653
+ return (value) => parseIntStrict(value, flag);
1654
+ }
1655
+ function enumIntArg(flag, allowed) {
1656
+ return (value) => {
1657
+ const num = parseInt(value, 10);
1658
+ if (isNaN(num) || !allowed.includes(num)) {
1659
+ fail(`${flag} must be one of: ${allowed.join(", ")}`);
1660
+ }
1661
+ return num;
1662
+ };
1663
+ }
1664
+
1480
1665
  // src/commands/list.ts
1481
1666
  registerSchema("list", "list", "List lists in a folder", [
1482
1667
  { flag: "--folder-id", type: "string", required: true, description: "Folder ID" },
@@ -1549,11 +1734,11 @@ function registerListCommands(program, getClient) {
1549
1734
  const data = await client.get(`/list/${listId}`);
1550
1735
  formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
1551
1736
  });
1552
- list.command("create").description("Create a new list in a folder").requiredOption("--folder-id <id>", "Folder ID").requiredOption("--name <name>", "List name").option("--content <desc>", "List description").option("--due-date <ts>", "Due date (Unix ms)").option("--priority <n>", "Priority (1-4)", parseInt).option("--status <s>", "Default status").action(async (opts) => {
1737
+ list.command("create").description("Create a new list in a folder").requiredOption("--folder-id <id>", "Folder ID").requiredOption("--name <name>", "List name").option("--content <desc>", "List description").option("--due-date <ts>", "Due date (Unix ms)").option("--priority <n>", "Priority (1-4)", enumIntArg("--priority", [1, 2, 3, 4])).option("--status <s>", "Default status").action(async (opts) => {
1553
1738
  const client = getClient();
1554
1739
  const body = { name: opts.name };
1555
1740
  if (opts.content !== void 0) body["content"] = opts.content;
1556
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1741
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
1557
1742
  if (opts.priority !== void 0) body["priority"] = opts.priority;
1558
1743
  if (opts.status !== void 0) body["status"] = opts.status;
1559
1744
  const data = await client.post(`/folder/${opts.folderId}/list`, body);
@@ -1571,7 +1756,7 @@ function registerListCommands(program, getClient) {
1571
1756
  const body = {};
1572
1757
  if (opts.name !== void 0) body["name"] = opts.name;
1573
1758
  if (opts.content !== void 0) body["content"] = opts.content;
1574
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1759
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
1575
1760
  if (opts.unsetStatus) body["unset_status"] = true;
1576
1761
  const data = await client.put(`/list/${listId}`, body);
1577
1762
  formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
@@ -1611,6 +1796,116 @@ function registerListCommands(program, getClient) {
1611
1796
 
1612
1797
  // src/commands/task.ts
1613
1798
  init_config();
1799
+
1800
+ // src/commands/task-bulk.ts
1801
+ registerSchema("task", "bulk-update", "Apply the same update to multiple tasks", [
1802
+ { flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
1803
+ { flag: "--name", type: "string", required: false, description: "New task name" },
1804
+ { flag: "--description", type: "string", required: false, description: "New description" },
1805
+ { flag: "--status", type: "string", required: false, description: "New status" },
1806
+ { flag: "--priority", type: "string", required: false, description: "New priority (1-4 or urgent/high/normal/low)" }
1807
+ ]);
1808
+ registerSchema("task", "bulk-delete", "Delete multiple tasks", [
1809
+ { flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
1810
+ { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
1811
+ ]);
1812
+ async function runConcurrent(tasks, limit) {
1813
+ const results = new Array(tasks.length);
1814
+ let idx = 0;
1815
+ async function worker() {
1816
+ while (idx < tasks.length) {
1817
+ const current = idx++;
1818
+ try {
1819
+ results[current] = await tasks[current]();
1820
+ } catch (e) {
1821
+ results[current] = e instanceof Error ? e : new Error(String(e));
1822
+ }
1823
+ }
1824
+ }
1825
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
1826
+ return results;
1827
+ }
1828
+ function registerTaskBulkCommands(task, program, getClient) {
1829
+ task.command("bulk-time-in-status").description("Get time-in-status for multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).action(async (opts) => {
1830
+ const client = getClient();
1831
+ const taskIds = opts.taskId;
1832
+ const results = [];
1833
+ for (const id of taskIds) {
1834
+ const data = await client.get(`/task/${id}/time_in_status`);
1835
+ const entries = data.status_history ?? [];
1836
+ if (data.current_status) entries.push(data.current_status);
1837
+ results.push({ task_id: id, statuses: entries });
1838
+ }
1839
+ formatOutput(results, [
1840
+ { key: "task_id", header: "Task ID", width: 14 },
1841
+ { key: "statuses", header: "Statuses", width: 50 }
1842
+ ], getOutputOptions(program));
1843
+ });
1844
+ task.command("bulk-update").description("Apply the same update to multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", intArg("--time-estimate")).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).action(async (opts) => {
1845
+ const client = getClient();
1846
+ const taskIds = opts.taskId;
1847
+ const body = {};
1848
+ if (opts.name !== void 0) body["name"] = opts.name;
1849
+ if (opts.description !== void 0) body["description"] = opts.description;
1850
+ if (opts.status !== void 0) body["status"] = opts.status;
1851
+ if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
1852
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
1853
+ if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
1854
+ if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
1855
+ const addIds = opts.assigneeAdd;
1856
+ const remIds = opts.assigneeRemove;
1857
+ if (addIds.length || remIds.length) {
1858
+ body["assignees"] = {
1859
+ add: addIds.map((a) => parseIntStrict(a, "--assignee-add")),
1860
+ rem: remIds.map((a) => parseIntStrict(a, "--assignee-remove"))
1861
+ };
1862
+ }
1863
+ const tasks = taskIds.map((id) => async () => {
1864
+ const data = await client.put(`/task/${id}`, body);
1865
+ return { task_id: id, name: data["name"], result: "ok" };
1866
+ });
1867
+ const results = await runConcurrent(tasks, 3);
1868
+ const rows = results.map(
1869
+ (r, i) => r instanceof Error ? { task_id: taskIds[i], name: "", result: r.message } : r
1870
+ );
1871
+ formatOutput(rows, [
1872
+ { key: "task_id", header: "Task ID", width: 14 },
1873
+ { key: "name", header: "Name", width: 30 },
1874
+ { key: "result", header: "Result", width: 20 }
1875
+ ], getOutputOptions(program));
1876
+ });
1877
+ task.command("bulk-delete").description("Delete multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--confirm", "Skip confirmation prompt").action(async (opts) => {
1878
+ const client = getClient();
1879
+ const taskIds = opts.taskId;
1880
+ if (!opts.confirm) {
1881
+ if (!process.stdin.isTTY) {
1882
+ process.stderr.write("Error: Use --confirm to bulk delete in non-interactive mode.\n");
1883
+ process.exit(2);
1884
+ return;
1885
+ }
1886
+ const { confirm } = await import("@inquirer/prompts");
1887
+ const yes = await confirm({ message: `Delete ${taskIds.length} task(s)?` });
1888
+ if (!yes) {
1889
+ process.stdout.write("Cancelled.\n");
1890
+ return;
1891
+ }
1892
+ }
1893
+ const tasks = taskIds.map((id) => async () => {
1894
+ await client.delete(`/task/${id}`);
1895
+ return { task_id: id, result: "deleted" };
1896
+ });
1897
+ const results = await runConcurrent(tasks, 3);
1898
+ const rows = results.map(
1899
+ (r, i) => r instanceof Error ? { task_id: taskIds[i], result: r.message } : r
1900
+ );
1901
+ formatOutput(rows, [
1902
+ { key: "task_id", header: "Task ID", width: 14 },
1903
+ { key: "result", header: "Result", width: 20 }
1904
+ ], getOutputOptions(program));
1905
+ });
1906
+ }
1907
+
1908
+ // src/commands/task.ts
1614
1909
  registerSchema("task", "list", "List tasks in a list", [
1615
1910
  { flag: "--list-id", type: "string", required: true, description: "List ID" },
1616
1911
  { flag: "--archived", type: "boolean", required: false, description: "Include archived tasks" },
@@ -1668,17 +1963,6 @@ registerSchema("task", "delete", "Delete a task", [
1668
1963
  registerSchema("task", "time-in-status", "Get time spent in each status for a task", [
1669
1964
  { flag: "<task-id>", type: "string", required: true, description: "Task ID" }
1670
1965
  ]);
1671
- registerSchema("task", "bulk-update", "Apply the same update to multiple tasks", [
1672
- { flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
1673
- { flag: "--name", type: "string", required: false, description: "New task name" },
1674
- { flag: "--description", type: "string", required: false, description: "New description" },
1675
- { flag: "--status", type: "string", required: false, description: "New status" },
1676
- { flag: "--priority", type: "string", required: false, description: "New priority (1-4 or urgent/high/normal/low)" }
1677
- ]);
1678
- registerSchema("task", "bulk-delete", "Delete multiple tasks", [
1679
- { flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
1680
- { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
1681
- ]);
1682
1966
  var TASK_COLUMNS = [
1683
1967
  { key: "id", header: "ID", width: 12 },
1684
1968
  { key: "name", header: "Name", width: 30 },
@@ -1716,23 +2000,14 @@ function parsePriority(value) {
1716
2000
  if (PRIORITY_MAP[lower] !== void 0) return PRIORITY_MAP[lower];
1717
2001
  const num = parseInt(value, 10);
1718
2002
  if (!isNaN(num) && num >= 1 && num <= 4) return num;
1719
- throw new Error("--priority must be 1-4 or urgent/high/normal/low");
2003
+ fail("--priority must be 1-4 or urgent/high/normal/low");
1720
2004
  }
1721
- async function runConcurrent(tasks, limit) {
1722
- const results = new Array(tasks.length);
1723
- let idx = 0;
1724
- async function worker() {
1725
- while (idx < tasks.length) {
1726
- const current = idx++;
1727
- try {
1728
- results[current] = await tasks[current]();
1729
- } catch (e) {
1730
- results[current] = e instanceof Error ? e : new Error(String(e));
1731
- }
1732
- }
2005
+ function parseCustomFieldFilters(filters) {
2006
+ try {
2007
+ return JSON.stringify(filters.map((f) => JSON.parse(f)));
2008
+ } catch {
2009
+ fail("--custom-field filters must be valid JSON");
1733
2010
  }
1734
- await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
1735
- return results;
1736
2011
  }
1737
2012
  function buildTaskListParams(opts) {
1738
2013
  const params = {};
@@ -1755,20 +2030,18 @@ function buildTaskListParams(opts) {
1755
2030
  const tags = opts.tag;
1756
2031
  if (tags?.length) params["tags[]"] = tags;
1757
2032
  const customFields = opts.customField;
1758
- if (customFields?.length) {
1759
- for (const cf of customFields) params["custom_fields"] = JSON.stringify(customFields.map((f) => JSON.parse(f)));
1760
- }
2033
+ if (customFields?.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
1761
2034
  return params;
1762
2035
  }
1763
2036
  function registerTaskCommands(program, getClient) {
1764
2037
  const task = program.command("task").description("Manage tasks");
1765
- task.command("list").description("List tasks in a list").requiredOption("--list-id <id>", "List ID").option("--archived", "Include archived tasks").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks in results").option("--page <n>", "Page number (0-indexed)", parseInt).option("--status <s>", "Filter by status (repeatable)", collect, []).option("--assignee <id>", "Filter by assignee ID (repeatable)", collect, []).option("--tag <name>", "Filter by tag name (repeatable)", collect, []).option("--due-date-gt <ts>", "Tasks due after timestamp").option("--due-date-lt <ts>", "Tasks due before timestamp").option("--date-created-gt <ts>", "Tasks created after timestamp").option("--date-created-lt <ts>", "Tasks created before timestamp").option("--date-updated-gt <ts>", "Tasks updated after timestamp").option("--date-updated-lt <ts>", "Tasks updated before timestamp").option("--custom-field <json>", "Custom field filter as JSON (repeatable)", collect, []).option("--order-by <field>", "Sort by field (id|created|updated|due_date)").option("--reverse", "Reverse sort order").action(async (opts) => {
2038
+ task.command("list").description("List tasks in a list").requiredOption("--list-id <id>", "List ID").option("--archived", "Include archived tasks").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks in results").option("--page <n>", "Page number (0-indexed)", intArg("--page")).option("--status <s>", "Filter by status (repeatable)", collect, []).option("--assignee <id>", "Filter by assignee ID (repeatable)", collect, []).option("--tag <name>", "Filter by tag name (repeatable)", collect, []).option("--due-date-gt <ts>", "Tasks due after timestamp").option("--due-date-lt <ts>", "Tasks due before timestamp").option("--date-created-gt <ts>", "Tasks created after timestamp").option("--date-created-lt <ts>", "Tasks created before timestamp").option("--date-updated-gt <ts>", "Tasks updated after timestamp").option("--date-updated-lt <ts>", "Tasks updated before timestamp").option("--custom-field <json>", "Custom field filter as JSON (repeatable)", collect, []).option("--order-by <field>", "Sort by field (id|created|updated|due_date)").option("--reverse", "Reverse sort order").action(async (opts) => {
1766
2039
  const client = getClient();
1767
2040
  const params = buildTaskListParams(opts);
1768
2041
  const data = await client.get(`/list/${opts.listId}/task`, params);
1769
2042
  formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
1770
2043
  });
1771
- task.command("search").description("Search tasks across a workspace").option("--query <text>", "Full-text search query").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks").option("--page <n>", "Page number (0-indexed)", parseInt).option("--status <s>", "Filter by status (repeatable)", collect, []).option("--assignee <id>", "Filter by assignee ID (repeatable)", collect, []).option("--tag <name>", "Filter by tag name (repeatable)", collect, []).option("--priority <n>", "Filter by priority (repeatable)", collect, []).option("--list-id <id>", "Scope to list IDs (repeatable)", collect, []).option("--folder-id <id>", "Scope to folder IDs (repeatable)", collect, []).option("--space-id <id>", "Scope to space IDs (repeatable)", collect, []).option("--project-id <id>", "Scope to project IDs (repeatable)", collect, []).option("--due-date-gt <ts>", "Tasks due after timestamp").option("--due-date-lt <ts>", "Tasks due before timestamp").option("--date-created-gt <ts>", "Tasks created after timestamp").option("--date-created-lt <ts>", "Tasks created before timestamp").option("--custom-field <json>", "Custom field filter as JSON (repeatable)", collect, []).option("--order-by <field>", "Sort by field (id|created|updated|due_date)").option("--reverse", "Reverse sort order").action(async (opts) => {
2044
+ task.command("search").description("Search tasks across a workspace").option("--query <text>", "Full-text search query").option("--include-closed", "Include tasks in closed status").option("--subtasks", "Include subtasks").option("--page <n>", "Page number (0-indexed)", intArg("--page")).option("--status <s>", "Filter by status (repeatable)", collect, []).option("--assignee <id>", "Filter by assignee ID (repeatable)", collect, []).option("--tag <name>", "Filter by tag name (repeatable)", collect, []).option("--priority <n>", "Filter by priority (repeatable)", collect, []).option("--list-id <id>", "Scope to list IDs (repeatable)", collect, []).option("--folder-id <id>", "Scope to folder IDs (repeatable)", collect, []).option("--space-id <id>", "Scope to space IDs (repeatable)", collect, []).option("--project-id <id>", "Scope to project IDs (repeatable)", collect, []).option("--due-date-gt <ts>", "Tasks due after timestamp").option("--due-date-lt <ts>", "Tasks due before timestamp").option("--date-created-gt <ts>", "Tasks created after timestamp").option("--date-created-lt <ts>", "Tasks created before timestamp").option("--custom-field <json>", "Custom field filter as JSON (repeatable)", collect, []).option("--order-by <field>", "Sort by field (id|created|updated|due_date)").option("--reverse", "Reverse sort order").action(async (opts) => {
1772
2045
  const workspaceId = requireWorkspaceId3(program);
1773
2046
  if (!workspaceId) return;
1774
2047
  const client = getClient();
@@ -1800,7 +2073,7 @@ function registerTaskCommands(program, getClient) {
1800
2073
  const projectIds = opts.projectId;
1801
2074
  if (projectIds.length) params["project_ids[]"] = projectIds;
1802
2075
  const customFields = opts.customField;
1803
- if (customFields.length) params["custom_fields"] = JSON.stringify(customFields.map((f) => JSON.parse(f)));
2076
+ if (customFields.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
1804
2077
  const data = await client.get(`/team/${workspaceId}/task`, params);
1805
2078
  formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
1806
2079
  });
@@ -1812,17 +2085,17 @@ function registerTaskCommands(program, getClient) {
1812
2085
  const data = await client.get(`/task/${taskId}`, params);
1813
2086
  formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
1814
2087
  });
1815
- task.command("create").description("Create a new task").requiredOption("--list-id <id>", "List ID").requiredOption("--name <name>", "Task name").option("--description <desc>", "Plain text description").option("--markdown-description <md>", "Markdown description (overrides --description)").option("--status <s>", "Initial status").option("--priority <n>", "Priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "Due date (Unix ms)").option("--start-date <date>", "Start date (Unix ms)").option("--assignee <id>", "Assignee user ID (repeatable)", collect, []).option("--tag <name>", "Tag name (repeatable)", collect, []).option("--time-estimate <ms>", "Time estimate in milliseconds", parseInt).option("--notify-all", "Notify all assignees and watchers").option("--parent <task-id>", "Parent task ID (creates subtask)").option("--links-to <task-id>", "Link to another task").option("--custom-field <id=value>", "Set custom field (repeatable)", collect, []).option("--check-required-custom-fields", "Reject if required custom fields are missing").action(async (opts) => {
2088
+ task.command("create").description("Create a new task").requiredOption("--list-id <id>", "List ID").requiredOption("--name <name>", "Task name").option("--description <desc>", "Plain text description").option("--markdown-description <md>", "Markdown description (overrides --description)").option("--status <s>", "Initial status").option("--priority <n>", "Priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "Due date (Unix ms)").option("--start-date <date>", "Start date (Unix ms)").option("--assignee <id>", "Assignee user ID (repeatable)", collect, []).option("--tag <name>", "Tag name (repeatable)", collect, []).option("--time-estimate <ms>", "Time estimate in milliseconds", intArg("--time-estimate")).option("--notify-all", "Notify all assignees and watchers").option("--parent <task-id>", "Parent task ID (creates subtask)").option("--links-to <task-id>", "Link to another task").option("--custom-field <id=value>", "Set custom field (repeatable)", collect, []).option("--check-required-custom-fields", "Reject if required custom fields are missing").action(async (opts) => {
1816
2089
  const client = getClient();
1817
2090
  const body = { name: opts.name };
1818
2091
  if (opts.markdownDescription !== void 0) body["markdown_description"] = opts.markdownDescription;
1819
2092
  else if (opts.description !== void 0) body["description"] = opts.description;
1820
2093
  if (opts.status !== void 0) body["status"] = opts.status;
1821
2094
  if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
1822
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1823
- if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
2095
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
2096
+ if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
1824
2097
  const assignees = opts.assignee;
1825
- if (assignees.length) body["assignees"] = assignees.map((a) => parseInt(a, 10));
2098
+ if (assignees.length) body["assignees"] = assignees.map((a) => parseIntStrict(a, "--assignee"));
1826
2099
  const tags = opts.tag;
1827
2100
  if (tags.length) body["tags"] = tags;
1828
2101
  if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
@@ -1833,7 +2106,7 @@ function registerTaskCommands(program, getClient) {
1833
2106
  if (customFields.length) {
1834
2107
  body["custom_fields"] = customFields.map((cf) => {
1835
2108
  const eqIdx = cf.indexOf("=");
1836
- if (eqIdx === -1) throw new Error(`Invalid custom field format: ${cf}. Expected: <id>=<value>`);
2109
+ if (eqIdx === -1) fail(`Invalid custom field format: ${cf}. Expected: <id>=<value>`);
1837
2110
  const id = cf.slice(0, eqIdx);
1838
2111
  let value = cf.slice(eqIdx + 1);
1839
2112
  try {
@@ -1847,22 +2120,22 @@ function registerTaskCommands(program, getClient) {
1847
2120
  const data = await client.post(`/list/${opts.listId}/task`, body);
1848
2121
  formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
1849
2122
  });
1850
- task.command("update").description("Update a task").argument("<task-id>", "Task ID").option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", parseInt).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).option("--archived <bool>", "Archive or unarchive").action(async (taskId, opts) => {
2123
+ task.command("update").description("Update a task").argument("<task-id>", "Task ID").option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", intArg("--time-estimate")).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).option("--archived <bool>", "Archive or unarchive").action(async (taskId, opts) => {
1851
2124
  const client = getClient();
1852
2125
  const body = {};
1853
2126
  if (opts.name !== void 0) body["name"] = opts.name;
1854
2127
  if (opts.description !== void 0) body["description"] = opts.description;
1855
2128
  if (opts.status !== void 0) body["status"] = opts.status;
1856
2129
  if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
1857
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1858
- if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
2130
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
2131
+ if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
1859
2132
  if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
1860
2133
  const addIds = opts.assigneeAdd;
1861
2134
  const remIds = opts.assigneeRemove;
1862
2135
  if (addIds.length || remIds.length) {
1863
2136
  body["assignees"] = {
1864
- add: addIds.map((a) => parseInt(a, 10)),
1865
- rem: remIds.map((a) => parseInt(a, 10))
2137
+ add: addIds.map((a) => parseIntStrict(a, "--assignee-add")),
2138
+ rem: remIds.map((a) => parseIntStrict(a, "--assignee-remove"))
1866
2139
  };
1867
2140
  }
1868
2141
  if (opts.archived !== void 0) body["archived"] = opts.archived === "true";
@@ -1895,83 +2168,7 @@ function registerTaskCommands(program, getClient) {
1895
2168
  if (data.current_status) entries.push(data.current_status);
1896
2169
  formatOutput(entries, TIME_IN_STATUS_COLUMNS, getOutputOptions(program));
1897
2170
  });
1898
- task.command("bulk-time-in-status").description("Get time-in-status for multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).action(async (opts) => {
1899
- const client = getClient();
1900
- const taskIds = opts.taskId;
1901
- const results = [];
1902
- for (const id of taskIds) {
1903
- const data = await client.get(`/task/${id}/time_in_status`);
1904
- const entries = data.status_history ?? [];
1905
- if (data.current_status) entries.push(data.current_status);
1906
- results.push({ task_id: id, statuses: entries });
1907
- }
1908
- formatOutput(results, [
1909
- { key: "task_id", header: "Task ID", width: 14 },
1910
- { key: "statuses", header: "Statuses", width: 50 }
1911
- ], getOutputOptions(program));
1912
- });
1913
- task.command("bulk-update").description("Apply the same update to multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", parseInt).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).action(async (opts) => {
1914
- const client = getClient();
1915
- const taskIds = opts.taskId;
1916
- const body = {};
1917
- if (opts.name !== void 0) body["name"] = opts.name;
1918
- if (opts.description !== void 0) body["description"] = opts.description;
1919
- if (opts.status !== void 0) body["status"] = opts.status;
1920
- if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
1921
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1922
- if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
1923
- if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
1924
- const addIds = opts.assigneeAdd;
1925
- const remIds = opts.assigneeRemove;
1926
- if (addIds.length || remIds.length) {
1927
- body["assignees"] = {
1928
- add: addIds.map((a) => parseInt(a, 10)),
1929
- rem: remIds.map((a) => parseInt(a, 10))
1930
- };
1931
- }
1932
- const tasks = taskIds.map((id) => async () => {
1933
- const data = await client.put(`/task/${id}`, body);
1934
- return { task_id: id, name: data["name"], result: "ok" };
1935
- });
1936
- const results = await runConcurrent(tasks, 3);
1937
- const rows = results.map(
1938
- (r, i) => r instanceof Error ? { task_id: taskIds[i], name: "", result: r.message } : r
1939
- );
1940
- formatOutput(rows, [
1941
- { key: "task_id", header: "Task ID", width: 14 },
1942
- { key: "name", header: "Name", width: 30 },
1943
- { key: "result", header: "Result", width: 20 }
1944
- ], getOutputOptions(program));
1945
- });
1946
- task.command("bulk-delete").description("Delete multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--confirm", "Skip confirmation prompt").action(async (opts) => {
1947
- const client = getClient();
1948
- const taskIds = opts.taskId;
1949
- if (!opts.confirm) {
1950
- if (!process.stdin.isTTY) {
1951
- process.stderr.write("Error: Use --confirm to bulk delete in non-interactive mode.\n");
1952
- process.exit(2);
1953
- return;
1954
- }
1955
- const { confirm } = await import("@inquirer/prompts");
1956
- const yes = await confirm({ message: `Delete ${taskIds.length} task(s)?` });
1957
- if (!yes) {
1958
- process.stdout.write("Cancelled.\n");
1959
- return;
1960
- }
1961
- }
1962
- const tasks = taskIds.map((id) => async () => {
1963
- await client.delete(`/task/${id}`);
1964
- return { task_id: id, result: "deleted" };
1965
- });
1966
- const results = await runConcurrent(tasks, 3);
1967
- const rows = results.map(
1968
- (r, i) => r instanceof Error ? { task_id: taskIds[i], result: r.message } : r
1969
- );
1970
- formatOutput(rows, [
1971
- { key: "task_id", header: "Task ID", width: 14 },
1972
- { key: "result", header: "Result", width: 20 }
1973
- ], getOutputOptions(program));
1974
- });
2171
+ registerTaskBulkCommands(task, program, getClient);
1975
2172
  }
1976
2173
 
1977
2174
  // src/commands/checklist.ts
@@ -2019,7 +2216,7 @@ function registerChecklistCommands(program, getClient) {
2019
2216
  const data = await client.post(`/task/${opts.taskId}/checklist`, { name: opts.name });
2020
2217
  formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
2021
2218
  });
2022
- checklist.command("update").description("Update a checklist").argument("<checklist-id>", "Checklist ID").option("--name <name>", "New checklist name").option("--position <n>", "Position (0-indexed)", parseInt).action(async (checklistId, opts) => {
2219
+ checklist.command("update").description("Update a checklist").argument("<checklist-id>", "Checklist ID").option("--name <name>", "New checklist name").option("--position <n>", "Position (0-indexed)", intArg("--position")).action(async (checklistId, opts) => {
2023
2220
  const client = getClient();
2024
2221
  const body = {};
2025
2222
  if (opts.name !== void 0) body["name"] = opts.name;
@@ -2049,7 +2246,7 @@ function registerChecklistCommands(program, getClient) {
2049
2246
  checklist.command("add-item").description("Add an item to a checklist").argument("<checklist-id>", "Checklist ID").requiredOption("--name <name>", "Item name").option("--assignee <id>", "Assignee user ID").option("--resolved <bool>", "Mark as resolved").option("--parent <item-id>", "Parent item ID").action(async (checklistId, opts) => {
2050
2247
  const client = getClient();
2051
2248
  const body = { name: opts.name };
2052
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2249
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2053
2250
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2054
2251
  if (opts.parent !== void 0) body["parent"] = opts.parent;
2055
2252
  const data = await client.post(`/checklist/${checklistId}/checklist_item`, body);
@@ -2060,7 +2257,7 @@ function registerChecklistCommands(program, getClient) {
2060
2257
  const body = {};
2061
2258
  if (opts.name !== void 0) body["name"] = opts.name;
2062
2259
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2063
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2260
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2064
2261
  if (opts.parent !== void 0) body["parent"] = opts.parent;
2065
2262
  const data = await client.put(`/checklist/${checklistId}/checklist_item/${opts.itemId}`, body);
2066
2263
  formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
@@ -2087,6 +2284,7 @@ function registerChecklistCommands(program, getClient) {
2087
2284
  }
2088
2285
 
2089
2286
  // src/commands/custom-field.ts
2287
+ init_config();
2090
2288
  registerSchema("field", "list", "List custom fields for a list, folder, space, or workspace", [
2091
2289
  { flag: "--list-id", type: "string", required: false, description: "List ID" },
2092
2290
  { flag: "--folder-id", type: "string", required: false, description: "Folder ID" },
@@ -2100,7 +2298,8 @@ registerSchema("field", "set", "Set a custom field value on a task", [
2100
2298
  ]);
2101
2299
  registerSchema("field", "remove", "Remove a custom field value from a task", [
2102
2300
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2103
- { flag: "--field-id", type: "string", required: true, description: "Field ID" }
2301
+ { flag: "--field-id", type: "string", required: true, description: "Field ID" },
2302
+ { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
2104
2303
  ]);
2105
2304
  var FIELD_COLUMNS = [
2106
2305
  { key: "id", header: "ID", width: 20 },
@@ -2113,7 +2312,7 @@ function registerFieldCommands(program, getClient) {
2113
2312
  const field = program.command("field").description("Manage custom fields");
2114
2313
  field.command("list").description("List custom fields (provide one ID flag)").option("--list-id <id>", "List ID").option("--folder-id <id>", "Folder ID").option("--space-id <id>", "Space ID").action(async (opts) => {
2115
2314
  const client = getClient();
2116
- const globalWorkspaceId = program.opts()["workspaceId"];
2315
+ const globalWorkspaceId = resolveWorkspaceId(program.opts()["workspaceId"]);
2117
2316
  let endpoint;
2118
2317
  if (opts.listId) {
2119
2318
  endpoint = `/list/${opts.listId}/field`;
@@ -2141,8 +2340,21 @@ function registerFieldCommands(program, getClient) {
2141
2340
  const data = await client.post(`/task/${opts.taskId}/field/${opts.fieldId}`, { value: parsedValue });
2142
2341
  formatOutput(data, FIELD_COLUMNS, getOutputOptions(program));
2143
2342
  });
2144
- field.command("remove").description("Remove a custom field value from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--field-id <fid>", "Field ID").action(async (opts) => {
2343
+ field.command("remove").description("Remove a custom field value from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--field-id <fid>", "Field ID").option("--confirm", "Skip confirmation prompt").action(async (opts) => {
2145
2344
  const client = getClient();
2345
+ if (!opts.confirm) {
2346
+ if (!process.stdin.isTTY) {
2347
+ process.stderr.write("Error: Use --confirm to remove in non-interactive mode.\n");
2348
+ process.exit(2);
2349
+ return;
2350
+ }
2351
+ const { confirm } = await import("@inquirer/prompts");
2352
+ const yes = await confirm({ message: `Remove field ${opts.fieldId} value from task ${opts.taskId}?` });
2353
+ if (!yes) {
2354
+ process.stdout.write("Cancelled.\n");
2355
+ return;
2356
+ }
2357
+ }
2146
2358
  await client.delete(`/task/${opts.taskId}/field/${opts.fieldId}`);
2147
2359
  process.stdout.write(`Removed field ${opts.fieldId} from task ${opts.taskId}
2148
2360
  `);
@@ -2319,8 +2531,8 @@ function registerRelationCommands(program, getClient) {
2319
2531
  }
2320
2532
 
2321
2533
  // src/commands/attachment.ts
2322
- import { writeFileSync } from "fs";
2323
- import { join } from "path";
2534
+ import { writeFileSync, existsSync as existsSync2 } from "fs";
2535
+ import { join, basename as basename2, resolve } from "path";
2324
2536
  registerSchema("attachment", "upload", "Upload a file to a task", [
2325
2537
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2326
2538
  { flag: "--file", type: "string", required: true, description: "Local file path" },
@@ -2332,7 +2544,8 @@ registerSchema("attachment", "list", "List attachments on a task", [
2332
2544
  registerSchema("attachment", "download", "Download an attachment from a task", [
2333
2545
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2334
2546
  { flag: "--attachment-id", type: "string", required: true, description: "Attachment ID" },
2335
- { flag: "--output", type: "string", required: false, description: "Output file path (default: ./attachment-<id>-<title>)" }
2547
+ { flag: "--output", type: "string", required: false, description: "Output file path (default: ./attachment-<id>-<title>)" },
2548
+ { flag: "--force", type: "boolean", required: false, description: "Overwrite the output file if it exists" }
2336
2549
  ]);
2337
2550
  var ATTACHMENT_COLUMNS = [
2338
2551
  { key: "id", header: "ID", width: 20 },
@@ -2354,7 +2567,7 @@ function registerAttachmentCommands(program, getClient) {
2354
2567
  const attachments = data["attachments"] ?? [];
2355
2568
  formatOutput(attachments, ATTACHMENT_COLUMNS, getOutputOptions(program));
2356
2569
  });
2357
- attachment.command("download").description("Download an attachment from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--attachment-id <id>", "Attachment ID").option("--output <path>", "Output file path").action(async (opts) => {
2570
+ attachment.command("download").description("Download an attachment from a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--attachment-id <id>", "Attachment ID").option("--output <path>", "Output file path").option("--force", "Overwrite the output file if it exists").action(async (opts) => {
2358
2571
  const { default: ora } = await import("ora");
2359
2572
  const client = getClient();
2360
2573
  const data = await client.get(`/task/${opts.taskId}`);
@@ -2366,8 +2579,15 @@ function registerAttachmentCommands(program, getClient) {
2366
2579
  process.exit(4);
2367
2580
  return;
2368
2581
  }
2369
- const outputPath = opts.output ?? join(process.cwd(), `attachment-${attachment2.id}-${attachment2.title}`);
2370
- const spinner = ora(`Downloading ${attachment2.title}...`).start();
2582
+ const safeTitle = basename2(attachment2.title.replace(/[/\\]/g, "_")) || "file";
2583
+ const outputPath = opts.output ? resolve(opts.output) : join(process.cwd(), `attachment-${attachment2.id}-${safeTitle}`);
2584
+ if (existsSync2(outputPath) && !opts.force) {
2585
+ process.stderr.write(`Error: "${outputPath}" already exists. Use --force to overwrite or --output to pick another path.
2586
+ `);
2587
+ process.exit(2);
2588
+ return;
2589
+ }
2590
+ const spinner = ora(`Downloading ${safeTitle}...`).start();
2371
2591
  try {
2372
2592
  const buffer = await client.downloadUrl(attachment2.url);
2373
2593
  writeFileSync(outputPath, Buffer.from(buffer));
@@ -2463,7 +2683,7 @@ function registerCommentCommands(program, getClient) {
2463
2683
  if (!parent) return;
2464
2684
  const client = getClient();
2465
2685
  const body = { comment_text: opts.text };
2466
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2686
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2467
2687
  if (opts.notifyAll) body["notify_all"] = true;
2468
2688
  const data = await client.post(`/${parent.type}/${parent.id}/comment`, body);
2469
2689
  process.stdout.write(`Created comment ${data["id"] ?? ""}
@@ -2472,7 +2692,7 @@ function registerCommentCommands(program, getClient) {
2472
2692
  comment.command("update").description("Update a comment").argument("<comment-id>", "Comment ID").requiredOption("--text <text>", "New comment text").option("--assignee <id>", "Assignee user ID").option("--resolved <bool>", "Mark as resolved (true/false)").action(async (commentId, opts) => {
2473
2693
  const client = getClient();
2474
2694
  const body = { comment_text: opts.text };
2475
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2695
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2476
2696
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2477
2697
  await client.put(`/comment/${commentId}`, body);
2478
2698
  process.stdout.write(`Updated comment ${commentId}
@@ -2505,7 +2725,7 @@ function registerCommentCommands(program, getClient) {
2505
2725
  comment.command("reply").description("Reply to a comment (threaded)").argument("<comment-id>", "Comment ID").requiredOption("--text <text>", "Reply text").option("--assignee <id>", "Assignee user ID").option("--notify-all", "Notify all watchers").action(async (commentId, opts) => {
2506
2726
  const client = getClient();
2507
2727
  const body = { comment_text: opts.text };
2508
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2728
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2509
2729
  if (opts.notifyAll) body["notify_all"] = true;
2510
2730
  const data = await client.post(`/comment/${commentId}/thread`, body);
2511
2731
  process.stdout.write(`Created reply ${data["id"] ?? ""}
@@ -2515,85 +2735,6 @@ function registerCommentCommands(program, getClient) {
2515
2735
 
2516
2736
  // src/commands/time-tracking.ts
2517
2737
  init_config();
2518
-
2519
- // src/dates.ts
2520
- var RELATIVE_OFFSET_RE = /^([+-]\d+)([dwmh])$/;
2521
- var UNIX_MS_RE = /^\d{13,}$/;
2522
- var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
2523
- var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
2524
- var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
2525
- function parseDate(input) {
2526
- const raw = input.trim();
2527
- const lower = raw.toLowerCase();
2528
- if (UNIX_MS_RE.test(raw)) {
2529
- return parseInt(raw, 10);
2530
- }
2531
- const now = /* @__PURE__ */ new Date();
2532
- switch (lower) {
2533
- case "today": {
2534
- return startOfDay(now).getTime();
2535
- }
2536
- case "tomorrow": {
2537
- const d = startOfDay(now);
2538
- d.setDate(d.getDate() + 1);
2539
- return d.getTime();
2540
- }
2541
- case "yesterday": {
2542
- const d = startOfDay(now);
2543
- d.setDate(d.getDate() - 1);
2544
- return d.getTime();
2545
- }
2546
- }
2547
- const offsetMatch = RELATIVE_OFFSET_RE.exec(lower);
2548
- if (offsetMatch) {
2549
- const amount = parseInt(offsetMatch[1], 10);
2550
- const unit = offsetMatch[2];
2551
- const ms = unitToMs(unit, amount);
2552
- return now.getTime() + ms;
2553
- }
2554
- if (lower.startsWith("next ")) {
2555
- const dayName = lower.slice(5);
2556
- const dayIndex = DAY_NAMES.indexOf(dayName);
2557
- if (dayIndex !== -1) {
2558
- const today = now.getDay();
2559
- let daysAhead = dayIndex - today;
2560
- if (daysAhead <= 0) daysAhead += 7;
2561
- const target = startOfDay(now);
2562
- target.setDate(target.getDate() + daysAhead);
2563
- return target.getTime();
2564
- }
2565
- }
2566
- if (ISO_DATE_RE.test(raw)) {
2567
- const d = /* @__PURE__ */ new Date(raw + "T00:00:00");
2568
- if (!isNaN(d.getTime())) return d.getTime();
2569
- }
2570
- if (ISO_DATETIME_RE.test(raw)) {
2571
- const d = new Date(raw);
2572
- if (!isNaN(d.getTime())) return d.getTime();
2573
- }
2574
- throw new Error(`Unable to parse date: "${input}"`);
2575
- }
2576
- function startOfDay(date) {
2577
- const d = new Date(date);
2578
- d.setHours(0, 0, 0, 0);
2579
- return d;
2580
- }
2581
- function unitToMs(unit, amount) {
2582
- switch (unit) {
2583
- case "m":
2584
- return amount * 60 * 1e3;
2585
- case "h":
2586
- return amount * 60 * 60 * 1e3;
2587
- case "d":
2588
- return amount * 24 * 60 * 60 * 1e3;
2589
- case "w":
2590
- return amount * 7 * 24 * 60 * 60 * 1e3;
2591
- default:
2592
- return 0;
2593
- }
2594
- }
2595
-
2596
- // src/commands/time-tracking.ts
2597
2738
  registerSchema("time", "list", "List time entries for a task or workspace", [
2598
2739
  { flag: "--task-id", type: "string", required: false, description: "Task ID (or use --workspace-id for workspace-wide)" },
2599
2740
  { flag: "--workspace-id", type: "string", required: false, description: "Workspace ID" },
@@ -2624,7 +2765,8 @@ registerSchema("time", "update", "Update a time entry", [
2624
2765
  ]);
2625
2766
  registerSchema("time", "delete", "Delete a time entry", [
2626
2767
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
2627
- { flag: "<timer-id>", type: "string", required: true, description: "Time entry ID" }
2768
+ { flag: "<timer-id>", type: "string", required: true, description: "Time entry ID" },
2769
+ { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
2628
2770
  ]);
2629
2771
  registerSchema("time", "running", "Get current running timer", [
2630
2772
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
@@ -2712,12 +2854,12 @@ function registerTimeTrackingCommands(program, getClient) {
2712
2854
  time.command("create").description("Create a time entry on a task").requiredOption("--task-id <id>", "Task ID").requiredOption("--duration <ms>", "Duration in milliseconds").requiredOption("--start <ts>", "Start time (Unix ms or date string)").option("--description <desc>", "Description").option("--assignee <id>", "Assignee user ID").option("--billable <bool>", "Billable (true/false)").option("--tag <name>", "Tag name (repeatable)", collect2, []).action(async (opts) => {
2713
2855
  const client = getClient();
2714
2856
  const body = {
2715
- duration: parseInt(opts.duration, 10),
2857
+ duration: parseIntStrict(opts.duration, "--duration"),
2716
2858
  start: String(parseDate(opts.start))
2717
2859
  };
2718
2860
  if (opts.description !== void 0) body["description"] = opts.description;
2719
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2720
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2861
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2862
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2721
2863
  if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
2722
2864
  const data = await client.post(`/task/${opts.taskId}/time`, body);
2723
2865
  process.stdout.write(`Created time entry ${data.data?.id ?? ""}
@@ -2729,9 +2871,9 @@ function registerTimeTrackingCommands(program, getClient) {
2729
2871
  const client = getClient();
2730
2872
  const body = {};
2731
2873
  if (opts.description !== void 0) body["description"] = opts.description;
2732
- if (opts.duration !== void 0) body["duration"] = parseInt(opts.duration, 10);
2874
+ if (opts.duration !== void 0) body["duration"] = parseIntStrict(opts.duration, "--duration");
2733
2875
  if (opts.start !== void 0) body["start"] = String(parseDate(opts.start));
2734
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2876
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2735
2877
  if (opts.tag.length) {
2736
2878
  body["tags"] = opts.tag.map((t) => ({ name: t }));
2737
2879
  if (opts.tagAction) body["tag_action"] = opts.tagAction;
@@ -2740,10 +2882,23 @@ function registerTimeTrackingCommands(program, getClient) {
2740
2882
  process.stdout.write(`Updated time entry ${timerId}
2741
2883
  `);
2742
2884
  });
2743
- time.command("delete").description("Delete a time entry").argument("<timer-id>", "Time entry ID").action(async (timerId) => {
2885
+ time.command("delete").description("Delete a time entry").argument("<timer-id>", "Time entry ID").option("--confirm", "Skip confirmation prompt").action(async (timerId, opts) => {
2744
2886
  const workspaceId = requireWorkspaceId4(program);
2745
2887
  if (!workspaceId) return;
2746
2888
  const client = getClient();
2889
+ if (!opts.confirm) {
2890
+ if (!process.stdin.isTTY) {
2891
+ process.stderr.write("Error: Use --confirm to delete in non-interactive mode.\n");
2892
+ process.exit(2);
2893
+ return;
2894
+ }
2895
+ const { confirm } = await import("@inquirer/prompts");
2896
+ const yes = await confirm({ message: `Delete time entry ${timerId}?` });
2897
+ if (!yes) {
2898
+ process.stdout.write("Cancelled.\n");
2899
+ return;
2900
+ }
2901
+ }
2747
2902
  await client.delete(`/team/${workspaceId}/time_entries/${timerId}`);
2748
2903
  process.stdout.write(`Deleted time entry ${timerId}
2749
2904
  `);
@@ -2774,7 +2929,7 @@ function registerTimeTrackingCommands(program, getClient) {
2774
2929
  const client = getClient();
2775
2930
  const body = { tid: opts.taskId };
2776
2931
  if (opts.description !== void 0) body["description"] = opts.description;
2777
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2932
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2778
2933
  if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
2779
2934
  const data = await client.post(`/team/${workspaceId}/time_entries/start`, body);
2780
2935
  process.stdout.write(`Started timer ${data.data?.id ?? ""}
@@ -2939,7 +3094,7 @@ function registerGoalCommands(program, getClient) {
2939
3094
  if (opts.dueDate !== void 0) body["due_date"] = opts.dueDate;
2940
3095
  if (opts.description !== void 0) body["description"] = opts.description;
2941
3096
  if (opts.multipleOwners) body["multiple_owners"] = true;
2942
- if (opts.owner.length) body["owners"] = opts.owner.map((id) => parseInt(id, 10));
3097
+ if (opts.owner.length) body["owners"] = opts.owner.map((id) => parseIntStrict(id, "--owner"));
2943
3098
  if (opts.color !== void 0) body["color"] = opts.color;
2944
3099
  const data = await client.post(`/team/${workspaceId}/goal`, body);
2945
3100
  process.stdout.write(`Created goal ${data.goal?.id ?? ""}
@@ -2978,8 +3133,8 @@ function registerGoalCommands(program, getClient) {
2978
3133
  goal.command("add-key-result").description("Add a key result to a goal").argument("<goal-id>", "Goal ID").requiredOption("--name <name>", "Key result name").requiredOption("--type <type>", "Type (number, currency, boolean, percentage, automatic)").option("--steps-start <n>", "Starting value").option("--steps-end <n>", "Target value").option("--unit <unit>", "Unit label").option("--task-ids <id>", "Task ID (repeatable, for automatic type)", collect3, []).option("--list-ids <id>", "List ID (repeatable, for automatic type)", collect3, []).action(async (goalId, opts) => {
2979
3134
  const client = getClient();
2980
3135
  const body = { name: opts.name, type: opts.type };
2981
- if (opts.stepsStart !== void 0) body["steps_start"] = parseFloat(opts.stepsStart);
2982
- if (opts.stepsEnd !== void 0) body["steps_end"] = parseFloat(opts.stepsEnd);
3136
+ if (opts.stepsStart !== void 0) body["steps_start"] = parseFloatStrict(opts.stepsStart, "--steps-start");
3137
+ if (opts.stepsEnd !== void 0) body["steps_end"] = parseFloatStrict(opts.stepsEnd, "--steps-end");
2983
3138
  if (opts.unit !== void 0) body["unit"] = opts.unit;
2984
3139
  if (opts.taskIds.length) body["task_ids"] = opts.taskIds;
2985
3140
  if (opts.listIds.length) body["list_ids"] = opts.listIds;
@@ -2991,7 +3146,7 @@ function registerGoalCommands(program, getClient) {
2991
3146
  const client = getClient();
2992
3147
  const body = {};
2993
3148
  if (opts.name !== void 0) body["name"] = opts.name;
2994
- if (opts.stepsCurrent !== void 0) body["steps_current"] = parseFloat(opts.stepsCurrent);
3149
+ if (opts.stepsCurrent !== void 0) body["steps_current"] = parseFloatStrict(opts.stepsCurrent, "--steps-current");
2995
3150
  if (opts.note !== void 0) body["note"] = opts.note;
2996
3151
  await client.put(`/key_result/${keyResultId}`, body);
2997
3152
  process.stdout.write(`Updated key result ${keyResultId}
@@ -3019,6 +3174,7 @@ function registerGoalCommands(program, getClient) {
3019
3174
  }
3020
3175
 
3021
3176
  // src/commands/view.ts
3177
+ init_config();
3022
3178
  registerSchema("view", "list", "List views for a workspace, space, folder, or list", [
3023
3179
  { flag: "--workspace-id", type: "string", required: false, description: "Workspace ID (provide one parent)" },
3024
3180
  { flag: "--space-id", type: "string", required: false, description: "Space ID (provide one parent)" },
@@ -3071,6 +3227,10 @@ function resolveViewParent(opts, program) {
3071
3227
  if (opts.spaceId) parents.push({ segment: "space", id: opts.spaceId });
3072
3228
  if (opts.folderId) parents.push({ segment: "folder", id: opts.folderId });
3073
3229
  if (opts.listId) parents.push({ segment: "list", id: opts.listId });
3230
+ if (parents.length === 0) {
3231
+ const resolvedWsId = resolveWorkspaceId(void 0);
3232
+ if (resolvedWsId) parents.push({ segment: "team", id: resolvedWsId });
3233
+ }
3074
3234
  if (parents.length === 0) {
3075
3235
  process.stderr.write("Error: Provide one of --workspace-id, --space-id, --folder-id, or --list-id.\n");
3076
3236
  process.exit(2);
@@ -3274,6 +3434,11 @@ function registerWebhookCommands(program, getClient) {
3274
3434
  `);
3275
3435
  });
3276
3436
  webhook.command("update").description("Update a webhook").argument("<webhook-id>", "Webhook ID").option("--endpoint <url>", "New endpoint URL").option("--event <event>", "Event to subscribe to (repeatable, replaces existing)", collect4, []).option("--status <status>", "Status (active or inactive)").action(async (webhookId, opts) => {
3437
+ if (opts.status !== void 0 && opts.status !== "active" && opts.status !== "inactive") {
3438
+ process.stderr.write('Error: --status must be "active" or "inactive".\n');
3439
+ process.exit(2);
3440
+ return;
3441
+ }
3277
3442
  const client = getClient();
3278
3443
  const body = {};
3279
3444
  if (opts.endpoint !== void 0) body["endpoint"] = opts.endpoint;
@@ -3375,7 +3540,7 @@ function registerUserCommands(program, getClient) {
3375
3540
  const body = {};
3376
3541
  if (opts.username !== void 0) body["username"] = opts.username;
3377
3542
  if (opts.admin !== void 0) body["admin"] = opts.admin === "true";
3378
- if (opts.customRoleId !== void 0) body["custom_role_id"] = parseInt(opts.customRoleId, 10);
3543
+ if (opts.customRoleId !== void 0) body["custom_role_id"] = parseIntStrict(opts.customRoleId, "--custom-role-id");
3379
3544
  await client.put(`/team/${workspaceId}/user/${userId}`, body);
3380
3545
  process.stdout.write(`Updated user ${userId}
3381
3546
  `);
@@ -3464,7 +3629,7 @@ function registerGroupCommands(program, getClient) {
3464
3629
  const client = getClient();
3465
3630
  const body = { name: opts.name };
3466
3631
  if (opts.memberId.length) {
3467
- body["members"] = opts.memberId.map((id) => ({ id: parseInt(id, 10) }));
3632
+ body["members"] = opts.memberId.map((id) => ({ id: parseIntStrict(id, "--member-id") }));
3468
3633
  }
3469
3634
  const data = await client.post(`/team/${workspaceId}/group`, body);
3470
3635
  process.stdout.write(`Created group ${data["id"] ?? ""}
@@ -3475,8 +3640,8 @@ function registerGroupCommands(program, getClient) {
3475
3640
  const body = {};
3476
3641
  if (opts.name !== void 0) body["name"] = opts.name;
3477
3642
  const members = {};
3478
- if (opts.addMember.length) members["add"] = opts.addMember.map((id) => ({ id: parseInt(id, 10) }));
3479
- if (opts.removeMember.length) members["rem"] = opts.removeMember.map((id) => ({ id: parseInt(id, 10) }));
3643
+ if (opts.addMember.length) members["add"] = opts.addMember.map((id) => ({ id: parseIntStrict(id, "--add-member") }));
3644
+ if (opts.removeMember.length) members["rem"] = opts.removeMember.map((id) => ({ id: parseIntStrict(id, "--remove-member") }));
3480
3645
  if (Object.keys(members).length) body["members"] = members;
3481
3646
  await client.put(`/group/${groupId}`, body);
3482
3647
  process.stdout.write(`Updated group ${groupId}
@@ -3527,6 +3692,15 @@ function parseBool(val) {
3527
3692
  if (val === void 0) return void 0;
3528
3693
  return val === "true";
3529
3694
  }
3695
+ var PERMISSION_LEVELS = ["read", "comment", "edit", "create"];
3696
+ function requirePermission(level) {
3697
+ if (!PERMISSION_LEVELS.includes(level)) {
3698
+ process.stderr.write(`Error: --permission must be one of: ${PERMISSION_LEVELS.join(", ")}
3699
+ `);
3700
+ process.exit(2);
3701
+ }
3702
+ return level;
3703
+ }
3530
3704
  registerSchema("guest", "invite", "Invite a guest to a workspace", [
3531
3705
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
3532
3706
  { flag: "--email", type: "string", required: true, description: "Email address to invite" },
@@ -3641,7 +3815,7 @@ function registerGuestCommands(program, getClient) {
3641
3815
  });
3642
3816
  guest.command("add-to-task").description("Add a guest to a task").argument("<guest-id>", "Guest ID").requiredOption("--task-id <id>", "Task ID").requiredOption("--permission <level>", "Permission level (read, comment, edit, create)").action(async (guestId, opts) => {
3643
3817
  const client = getClient();
3644
- await client.post(`/task/${opts.taskId}/guest/${guestId}`, { permission_level: opts.permission });
3818
+ await client.post(`/task/${opts.taskId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
3645
3819
  process.stdout.write(`Added guest ${guestId} to task ${opts.taskId}
3646
3820
  `);
3647
3821
  });
@@ -3653,7 +3827,7 @@ function registerGuestCommands(program, getClient) {
3653
3827
  });
3654
3828
  guest.command("add-to-list").description("Add a guest to a list").argument("<guest-id>", "Guest ID").requiredOption("--list-id <id>", "List ID").requiredOption("--permission <level>", "Permission level (read, comment, edit, create)").action(async (guestId, opts) => {
3655
3829
  const client = getClient();
3656
- await client.post(`/list/${opts.listId}/guest/${guestId}`, { permission_level: opts.permission });
3830
+ await client.post(`/list/${opts.listId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
3657
3831
  process.stdout.write(`Added guest ${guestId} to list ${opts.listId}
3658
3832
  `);
3659
3833
  });
@@ -3665,7 +3839,7 @@ function registerGuestCommands(program, getClient) {
3665
3839
  });
3666
3840
  guest.command("add-to-folder").description("Add a guest to a folder").argument("<guest-id>", "Guest ID").requiredOption("--folder-id <id>", "Folder ID").requiredOption("--permission <level>", "Permission level (read, comment, edit, create)").action(async (guestId, opts) => {
3667
3841
  const client = getClient();
3668
- await client.post(`/folder/${opts.folderId}/guest/${guestId}`, { permission_level: opts.permission });
3842
+ await client.post(`/folder/${opts.folderId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
3669
3843
  process.stdout.write(`Added guest ${guestId} to folder ${opts.folderId}
3670
3844
  `);
3671
3845
  });
@@ -3849,7 +4023,7 @@ registerSchema("template", "apply-folder", "Create a folder from a folder templa
3849
4023
  ]);
3850
4024
  function registerTemplateCommands(program, getClient) {
3851
4025
  const template = program.command("template").description("Manage task templates");
3852
- template.command("list").description("List task templates").option("--page <n>", "Page number (starts at 0)", parseInt).action(async (opts) => {
4026
+ template.command("list").description("List task templates").option("--page <n>", "Page number (starts at 0)", intArg("--page")).action(async (opts) => {
3853
4027
  const workspaceId = requireWorkspaceId11(program);
3854
4028
  if (!workspaceId) return;
3855
4029
  const client = getClient();
@@ -4138,7 +4312,7 @@ function registerDocCommands(program, getClient) {
4138
4312
  const data = await client.get(`/v3/workspaces/${workspaceId}/docs/${opts.docId}`);
4139
4313
  formatOutput(data, DOC_COLUMNS, getOutputOptions(program));
4140
4314
  });
4141
- doc.command("create").description("Create a doc").requiredOption("--name <name>", "Doc name").option("--parent-id <id>", "Parent ID").option("--parent-type <type>", "Parent type (4=space, 5=folder, 6=list, 7=task)", parseInt).option("--visibility <visibility>", "Visibility (private, workspace)").action(async (opts) => {
4315
+ doc.command("create").description("Create a doc").requiredOption("--name <name>", "Doc name").option("--parent-id <id>", "Parent ID").option("--parent-type <type>", "Parent type (4=space, 5=folder, 6=list, 7=task)", enumIntArg("--parent-type", [4, 5, 6, 7])).option("--visibility <visibility>", "Visibility (private, workspace)").action(async (opts) => {
4142
4316
  const workspaceId = requireWorkspaceId14(program);
4143
4317
  if (!workspaceId) return;
4144
4318
  const client = getClient();
@@ -4236,7 +4410,7 @@ function registerDocCommands(program, getClient) {
4236
4410
  }
4237
4411
 
4238
4412
  // src/commands/skill-cmd.ts
4239
- import { readdirSync, readFileSync as readFileSync4, existsSync } from "fs";
4413
+ import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
4240
4414
  import { join as join2, dirname } from "path";
4241
4415
  import { fileURLToPath } from "url";
4242
4416
  var SKILL_COLUMNS = [
@@ -4254,9 +4428,9 @@ registerSchema("skill", "path", "Print the file system path to a skill directory
4254
4428
  function findSkillsDir() {
4255
4429
  const thisDir = dirname(fileURLToPath(import.meta.url));
4256
4430
  const bundledDir = join2(thisDir, "..", "skills");
4257
- if (existsSync(bundledDir)) return bundledDir;
4431
+ if (existsSync3(bundledDir)) return bundledDir;
4258
4432
  const projectDir = join2(thisDir, "..", "..", "skills");
4259
- if (existsSync(projectDir)) return projectDir;
4433
+ if (existsSync3(projectDir)) return projectDir;
4260
4434
  return void 0;
4261
4435
  }
4262
4436
  function parseFrontmatter(content) {
@@ -4297,7 +4471,7 @@ function loadSkills(skillsDir) {
4297
4471
  }
4298
4472
  for (const entry of entries) {
4299
4473
  const skillFile = join2(skillsDir, entry, "SKILL.md");
4300
- if (!existsSync(skillFile)) continue;
4474
+ if (!existsSync3(skillFile)) continue;
4301
4475
  try {
4302
4476
  const content = readFileSync4(skillFile, "utf-8");
4303
4477
  const { frontmatter } = parseFrontmatter(content);
@@ -4322,7 +4496,7 @@ function loadSkills(skillsDir) {
4322
4496
  function findSkill(skillsDir, name) {
4323
4497
  const skillDir = join2(skillsDir, name);
4324
4498
  const skillFile = join2(skillDir, "SKILL.md");
4325
- if (!existsSync(skillFile)) return void 0;
4499
+ if (!existsSync3(skillFile)) return void 0;
4326
4500
  try {
4327
4501
  const content = readFileSync4(skillFile, "utf-8");
4328
4502
  return { skillDir, content };
@@ -4452,10 +4626,12 @@ function registerChatCommands(program, getClient) {
4452
4626
  }
4453
4627
 
4454
4628
  // src/cli.ts
4455
- var VERSION = "0.3.0";
4629
+ var VERSION = "0.4.0";
4456
4630
  function createProgram() {
4457
4631
  const program = new Command();
4458
- program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").option("--token-file <path>", "Read API token from this file path").option("--profile <name>", "Profile to use (key, workspace name, or nickname)").option("--workspace-id <id>", "Workspace ID").option("--format <format>", "Output format (table|json|csv|tsv|quiet|id|md)").option("--no-color", "Disable colors").option("--no-header", "Omit column headers").option("--fields <fields>", "Show only specified fields (comma-separated)").option("--filter <filter>", "Client-side filter (key=value)").option("--sort <sort>", "Sort by field (field[:asc|:desc])").option("--limit <n>", "Limit results", parseInt).option("--verbose", "Show request details").option("--debug", "Full debug output").option("--dry-run", "Print what would be sent without executing");
4632
+ program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").option("--token-file <path>", "Read API token from this file path").option("--profile <name>", "Profile to use (key, workspace name, or nickname)").option("--workspace-id <id>", "Workspace ID").addOption(
4633
+ new Option("--format <format>", "Output format").choices(["table", "json", "csv", "tsv", "quiet", "id", "md"])
4634
+ ).option("--no-color", "Disable colors").option("--no-header", "Omit column headers").option("--fields <fields>", "Show only specified fields (comma-separated)").option("--filter <filter>", "Client-side filter (key=value)").option("--sort <sort>", "Sort by field (field[:asc|:desc])").option("--limit <n>", "Limit results", intArg("--limit")).option("--verbose", "Show request details").option("--debug", "Full debug output").option("--dry-run", "Print what would be sent without executing");
4459
4635
  return program;
4460
4636
  }
4461
4637
  function createClient(program) {
@@ -4492,6 +4668,9 @@ function getOutputOptions(program) {
4492
4668
  return result;
4493
4669
  }
4494
4670
  function run() {
4671
+ process.stdout.on("error", (err) => {
4672
+ if (err.code === "EPIPE") process.exit(EXIT_CODES.SUCCESS);
4673
+ });
4495
4674
  const program = createProgram();
4496
4675
  program.hook("preAction", () => {
4497
4676
  setProfileOverride(program.opts()["profile"]);