clickup-agent-cli 0.3.0 → 0.4.1

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,126 @@ 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 UNIX_SECONDS_RE = /^\d{10}$/;
1551
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1552
+ var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
1553
+ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1554
+ function parseDate(input) {
1555
+ const raw = input.trim();
1556
+ const lower = raw.toLowerCase();
1557
+ if (UNIX_MS_RE.test(raw)) {
1558
+ return parseInt(raw, 10);
1559
+ }
1560
+ if (UNIX_SECONDS_RE.test(raw)) {
1561
+ return parseInt(raw, 10) * 1e3;
1562
+ }
1563
+ const now = /* @__PURE__ */ new Date();
1564
+ switch (lower) {
1565
+ case "today": {
1566
+ return startOfDay(now).getTime();
1567
+ }
1568
+ case "tomorrow": {
1569
+ const d = startOfDay(now);
1570
+ d.setDate(d.getDate() + 1);
1571
+ return d.getTime();
1572
+ }
1573
+ case "yesterday": {
1574
+ const d = startOfDay(now);
1575
+ d.setDate(d.getDate() - 1);
1576
+ return d.getTime();
1577
+ }
1578
+ }
1579
+ const offsetMatch = RELATIVE_OFFSET_RE.exec(lower);
1580
+ if (offsetMatch) {
1581
+ const amount = parseInt(offsetMatch[1], 10);
1582
+ const unit = offsetMatch[2];
1583
+ const ms = unitToMs(unit, amount);
1584
+ return now.getTime() + ms;
1585
+ }
1586
+ const dayName = lower.startsWith("next ") ? lower.slice(5) : lower;
1587
+ const dayIndex = DAY_NAMES.indexOf(dayName);
1588
+ if (dayIndex !== -1) {
1589
+ const today = now.getDay();
1590
+ let daysAhead = dayIndex - today;
1591
+ if (daysAhead <= 0) daysAhead += 7;
1592
+ const target = startOfDay(now);
1593
+ target.setDate(target.getDate() + daysAhead);
1594
+ return target.getTime();
1595
+ }
1596
+ if (ISO_DATE_RE.test(raw)) {
1597
+ const d = /* @__PURE__ */ new Date(raw + "T00:00:00");
1598
+ if (!isNaN(d.getTime())) return d.getTime();
1599
+ }
1600
+ if (ISO_DATETIME_RE.test(raw)) {
1601
+ const d = new Date(raw);
1602
+ if (!isNaN(d.getTime())) return d.getTime();
1603
+ }
1604
+ throw new Error(`Unable to parse date: "${input}"`);
1605
+ }
1606
+ function startOfDay(date) {
1607
+ const d = new Date(date);
1608
+ d.setHours(0, 0, 0, 0);
1609
+ return d;
1610
+ }
1611
+ function unitToMs(unit, amount) {
1612
+ switch (unit) {
1613
+ case "m":
1614
+ return amount * 60 * 1e3;
1615
+ case "h":
1616
+ return amount * 60 * 60 * 1e3;
1617
+ case "d":
1618
+ return amount * 24 * 60 * 60 * 1e3;
1619
+ case "w":
1620
+ return amount * 7 * 24 * 60 * 60 * 1e3;
1621
+ default:
1622
+ return 0;
1623
+ }
1624
+ }
1625
+
1626
+ // src/parse.ts
1627
+ function fail(message) {
1628
+ process.stderr.write(`Error: ${message}
1629
+ `);
1630
+ process.exit(2);
1631
+ }
1632
+ function parseIntStrict(value, flag) {
1633
+ const num = parseInt(value, 10);
1634
+ if (isNaN(num)) fail(`${flag} must be a number, got "${value}"`);
1635
+ return num;
1636
+ }
1637
+ function parseFloatStrict(value, flag) {
1638
+ const num = parseFloat(value);
1639
+ if (isNaN(num)) fail(`${flag} must be a number, got "${value}"`);
1640
+ return num;
1641
+ }
1642
+ function parseDateStrict(value, flag) {
1643
+ try {
1644
+ return parseDate(value);
1645
+ } catch {
1646
+ fail(`${flag} must be a date (ISO 8601, relative like 3d or friday, or a Unix timestamp), got "${value}"`);
1647
+ }
1648
+ }
1649
+ function parseBoolStrict(value, flag) {
1650
+ if (value === "true") return true;
1651
+ if (value === "false") return false;
1652
+ fail(`${flag} must be "true" or "false", got "${value}"`);
1653
+ }
1654
+ function intArg(flag) {
1655
+ return (value) => parseIntStrict(value, flag);
1656
+ }
1657
+ function enumIntArg(flag, allowed) {
1658
+ return (value) => {
1659
+ const num = parseInt(value, 10);
1660
+ if (isNaN(num) || !allowed.includes(num)) {
1661
+ fail(`${flag} must be one of: ${allowed.join(", ")}`);
1662
+ }
1663
+ return num;
1664
+ };
1665
+ }
1666
+
1480
1667
  // src/commands/list.ts
1481
1668
  registerSchema("list", "list", "List lists in a folder", [
1482
1669
  { flag: "--folder-id", type: "string", required: true, description: "Folder ID" },
@@ -1549,11 +1736,11 @@ function registerListCommands(program, getClient) {
1549
1736
  const data = await client.get(`/list/${listId}`);
1550
1737
  formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
1551
1738
  });
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) => {
1739
+ 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
1740
  const client = getClient();
1554
1741
  const body = { name: opts.name };
1555
1742
  if (opts.content !== void 0) body["content"] = opts.content;
1556
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1743
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
1557
1744
  if (opts.priority !== void 0) body["priority"] = opts.priority;
1558
1745
  if (opts.status !== void 0) body["status"] = opts.status;
1559
1746
  const data = await client.post(`/folder/${opts.folderId}/list`, body);
@@ -1571,7 +1758,7 @@ function registerListCommands(program, getClient) {
1571
1758
  const body = {};
1572
1759
  if (opts.name !== void 0) body["name"] = opts.name;
1573
1760
  if (opts.content !== void 0) body["content"] = opts.content;
1574
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1761
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
1575
1762
  if (opts.unsetStatus) body["unset_status"] = true;
1576
1763
  const data = await client.put(`/list/${listId}`, body);
1577
1764
  formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
@@ -1611,6 +1798,116 @@ function registerListCommands(program, getClient) {
1611
1798
 
1612
1799
  // src/commands/task.ts
1613
1800
  init_config();
1801
+
1802
+ // src/commands/task-bulk.ts
1803
+ registerSchema("task", "bulk-update", "Apply the same update to multiple tasks", [
1804
+ { flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
1805
+ { flag: "--name", type: "string", required: false, description: "New task name" },
1806
+ { flag: "--description", type: "string", required: false, description: "New description" },
1807
+ { flag: "--status", type: "string", required: false, description: "New status" },
1808
+ { flag: "--priority", type: "string", required: false, description: "New priority (1-4 or urgent/high/normal/low)" }
1809
+ ]);
1810
+ registerSchema("task", "bulk-delete", "Delete multiple tasks", [
1811
+ { flag: "--task-id", type: "string[]", required: true, description: "Task ID (repeatable)" },
1812
+ { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
1813
+ ]);
1814
+ async function runConcurrent(tasks, limit) {
1815
+ const results = new Array(tasks.length);
1816
+ let idx = 0;
1817
+ async function worker() {
1818
+ while (idx < tasks.length) {
1819
+ const current = idx++;
1820
+ try {
1821
+ results[current] = await tasks[current]();
1822
+ } catch (e) {
1823
+ results[current] = e instanceof Error ? e : new Error(String(e));
1824
+ }
1825
+ }
1826
+ }
1827
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
1828
+ return results;
1829
+ }
1830
+ function registerTaskBulkCommands(task, program, getClient) {
1831
+ 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) => {
1832
+ const client = getClient();
1833
+ const taskIds = opts.taskId;
1834
+ const results = [];
1835
+ for (const id of taskIds) {
1836
+ const data = await client.get(`/task/${id}/time_in_status`);
1837
+ const entries = data.status_history ?? [];
1838
+ if (data.current_status) entries.push(data.current_status);
1839
+ results.push({ task_id: id, statuses: entries });
1840
+ }
1841
+ formatOutput(results, [
1842
+ { key: "task_id", header: "Task ID", width: 14 },
1843
+ { key: "statuses", header: "Statuses", width: 50 }
1844
+ ], getOutputOptions(program));
1845
+ });
1846
+ 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) => {
1847
+ const client = getClient();
1848
+ const taskIds = opts.taskId;
1849
+ const body = {};
1850
+ if (opts.name !== void 0) body["name"] = opts.name;
1851
+ if (opts.description !== void 0) body["description"] = opts.description;
1852
+ if (opts.status !== void 0) body["status"] = opts.status;
1853
+ if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
1854
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
1855
+ if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
1856
+ if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
1857
+ const addIds = opts.assigneeAdd;
1858
+ const remIds = opts.assigneeRemove;
1859
+ if (addIds.length || remIds.length) {
1860
+ body["assignees"] = {
1861
+ add: addIds.map((a) => parseIntStrict(a, "--assignee-add")),
1862
+ rem: remIds.map((a) => parseIntStrict(a, "--assignee-remove"))
1863
+ };
1864
+ }
1865
+ const tasks = taskIds.map((id) => async () => {
1866
+ const data = await client.put(`/task/${id}`, body);
1867
+ return { task_id: id, name: data["name"], result: "ok" };
1868
+ });
1869
+ const results = await runConcurrent(tasks, 3);
1870
+ const rows = results.map(
1871
+ (r, i) => r instanceof Error ? { task_id: taskIds[i], name: "", result: r.message } : r
1872
+ );
1873
+ formatOutput(rows, [
1874
+ { key: "task_id", header: "Task ID", width: 14 },
1875
+ { key: "name", header: "Name", width: 30 },
1876
+ { key: "result", header: "Result", width: 20 }
1877
+ ], getOutputOptions(program));
1878
+ });
1879
+ task.command("bulk-delete").description("Delete multiple tasks").requiredOption("--task-id <id>", "Task ID (repeatable)", collect, []).option("--confirm", "Skip confirmation prompt").action(async (opts) => {
1880
+ const client = getClient();
1881
+ const taskIds = opts.taskId;
1882
+ if (!opts.confirm) {
1883
+ if (!process.stdin.isTTY) {
1884
+ process.stderr.write("Error: Use --confirm to bulk delete in non-interactive mode.\n");
1885
+ process.exit(2);
1886
+ return;
1887
+ }
1888
+ const { confirm } = await import("@inquirer/prompts");
1889
+ const yes = await confirm({ message: `Delete ${taskIds.length} task(s)?` });
1890
+ if (!yes) {
1891
+ process.stdout.write("Cancelled.\n");
1892
+ return;
1893
+ }
1894
+ }
1895
+ const tasks = taskIds.map((id) => async () => {
1896
+ await client.delete(`/task/${id}`);
1897
+ return { task_id: id, result: "deleted" };
1898
+ });
1899
+ const results = await runConcurrent(tasks, 3);
1900
+ const rows = results.map(
1901
+ (r, i) => r instanceof Error ? { task_id: taskIds[i], result: r.message } : r
1902
+ );
1903
+ formatOutput(rows, [
1904
+ { key: "task_id", header: "Task ID", width: 14 },
1905
+ { key: "result", header: "Result", width: 20 }
1906
+ ], getOutputOptions(program));
1907
+ });
1908
+ }
1909
+
1910
+ // src/commands/task.ts
1614
1911
  registerSchema("task", "list", "List tasks in a list", [
1615
1912
  { flag: "--list-id", type: "string", required: true, description: "List ID" },
1616
1913
  { flag: "--archived", type: "boolean", required: false, description: "Include archived tasks" },
@@ -1668,17 +1965,6 @@ registerSchema("task", "delete", "Delete a task", [
1668
1965
  registerSchema("task", "time-in-status", "Get time spent in each status for a task", [
1669
1966
  { flag: "<task-id>", type: "string", required: true, description: "Task ID" }
1670
1967
  ]);
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
1968
  var TASK_COLUMNS = [
1683
1969
  { key: "id", header: "ID", width: 12 },
1684
1970
  { key: "name", header: "Name", width: 30 },
@@ -1716,23 +2002,14 @@ function parsePriority(value) {
1716
2002
  if (PRIORITY_MAP[lower] !== void 0) return PRIORITY_MAP[lower];
1717
2003
  const num = parseInt(value, 10);
1718
2004
  if (!isNaN(num) && num >= 1 && num <= 4) return num;
1719
- throw new Error("--priority must be 1-4 or urgent/high/normal/low");
2005
+ fail("--priority must be 1-4 or urgent/high/normal/low");
1720
2006
  }
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
- }
2007
+ function parseCustomFieldFilters(filters) {
2008
+ try {
2009
+ return JSON.stringify(filters.map((f) => JSON.parse(f)));
2010
+ } catch {
2011
+ fail("--custom-field filters must be valid JSON");
1733
2012
  }
1734
- await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
1735
- return results;
1736
2013
  }
1737
2014
  function buildTaskListParams(opts) {
1738
2015
  const params = {};
@@ -1755,20 +2032,18 @@ function buildTaskListParams(opts) {
1755
2032
  const tags = opts.tag;
1756
2033
  if (tags?.length) params["tags[]"] = tags;
1757
2034
  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
- }
2035
+ if (customFields?.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
1761
2036
  return params;
1762
2037
  }
1763
2038
  function registerTaskCommands(program, getClient) {
1764
2039
  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) => {
2040
+ 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
2041
  const client = getClient();
1767
2042
  const params = buildTaskListParams(opts);
1768
2043
  const data = await client.get(`/list/${opts.listId}/task`, params);
1769
2044
  formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
1770
2045
  });
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) => {
2046
+ 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
2047
  const workspaceId = requireWorkspaceId3(program);
1773
2048
  if (!workspaceId) return;
1774
2049
  const client = getClient();
@@ -1800,7 +2075,7 @@ function registerTaskCommands(program, getClient) {
1800
2075
  const projectIds = opts.projectId;
1801
2076
  if (projectIds.length) params["project_ids[]"] = projectIds;
1802
2077
  const customFields = opts.customField;
1803
- if (customFields.length) params["custom_fields"] = JSON.stringify(customFields.map((f) => JSON.parse(f)));
2078
+ if (customFields.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
1804
2079
  const data = await client.get(`/team/${workspaceId}/task`, params);
1805
2080
  formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
1806
2081
  });
@@ -1812,17 +2087,17 @@ function registerTaskCommands(program, getClient) {
1812
2087
  const data = await client.get(`/task/${taskId}`, params);
1813
2088
  formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
1814
2089
  });
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) => {
2090
+ 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
2091
  const client = getClient();
1817
2092
  const body = { name: opts.name };
1818
2093
  if (opts.markdownDescription !== void 0) body["markdown_description"] = opts.markdownDescription;
1819
2094
  else if (opts.description !== void 0) body["description"] = opts.description;
1820
2095
  if (opts.status !== void 0) body["status"] = opts.status;
1821
2096
  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);
2097
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
2098
+ if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
1824
2099
  const assignees = opts.assignee;
1825
- if (assignees.length) body["assignees"] = assignees.map((a) => parseInt(a, 10));
2100
+ if (assignees.length) body["assignees"] = assignees.map((a) => parseIntStrict(a, "--assignee"));
1826
2101
  const tags = opts.tag;
1827
2102
  if (tags.length) body["tags"] = tags;
1828
2103
  if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
@@ -1833,7 +2108,7 @@ function registerTaskCommands(program, getClient) {
1833
2108
  if (customFields.length) {
1834
2109
  body["custom_fields"] = customFields.map((cf) => {
1835
2110
  const eqIdx = cf.indexOf("=");
1836
- if (eqIdx === -1) throw new Error(`Invalid custom field format: ${cf}. Expected: <id>=<value>`);
2111
+ if (eqIdx === -1) fail(`Invalid custom field format: ${cf}. Expected: <id>=<value>`);
1837
2112
  const id = cf.slice(0, eqIdx);
1838
2113
  let value = cf.slice(eqIdx + 1);
1839
2114
  try {
@@ -1847,22 +2122,22 @@ function registerTaskCommands(program, getClient) {
1847
2122
  const data = await client.post(`/list/${opts.listId}/task`, body);
1848
2123
  formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
1849
2124
  });
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) => {
2125
+ 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
2126
  const client = getClient();
1852
2127
  const body = {};
1853
2128
  if (opts.name !== void 0) body["name"] = opts.name;
1854
2129
  if (opts.description !== void 0) body["description"] = opts.description;
1855
2130
  if (opts.status !== void 0) body["status"] = opts.status;
1856
2131
  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);
2132
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
2133
+ if (opts.startDate !== void 0) body["start_date"] = parseDateStrict(opts.startDate, "--start-date");
1859
2134
  if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
1860
2135
  const addIds = opts.assigneeAdd;
1861
2136
  const remIds = opts.assigneeRemove;
1862
2137
  if (addIds.length || remIds.length) {
1863
2138
  body["assignees"] = {
1864
- add: addIds.map((a) => parseInt(a, 10)),
1865
- rem: remIds.map((a) => parseInt(a, 10))
2139
+ add: addIds.map((a) => parseIntStrict(a, "--assignee-add")),
2140
+ rem: remIds.map((a) => parseIntStrict(a, "--assignee-remove"))
1866
2141
  };
1867
2142
  }
1868
2143
  if (opts.archived !== void 0) body["archived"] = opts.archived === "true";
@@ -1895,83 +2170,7 @@ function registerTaskCommands(program, getClient) {
1895
2170
  if (data.current_status) entries.push(data.current_status);
1896
2171
  formatOutput(entries, TIME_IN_STATUS_COLUMNS, getOutputOptions(program));
1897
2172
  });
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
- });
2173
+ registerTaskBulkCommands(task, program, getClient);
1975
2174
  }
1976
2175
 
1977
2176
  // src/commands/checklist.ts
@@ -2019,7 +2218,7 @@ function registerChecklistCommands(program, getClient) {
2019
2218
  const data = await client.post(`/task/${opts.taskId}/checklist`, { name: opts.name });
2020
2219
  formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
2021
2220
  });
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) => {
2221
+ 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
2222
  const client = getClient();
2024
2223
  const body = {};
2025
2224
  if (opts.name !== void 0) body["name"] = opts.name;
@@ -2049,7 +2248,7 @@ function registerChecklistCommands(program, getClient) {
2049
2248
  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
2249
  const client = getClient();
2051
2250
  const body = { name: opts.name };
2052
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2251
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2053
2252
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2054
2253
  if (opts.parent !== void 0) body["parent"] = opts.parent;
2055
2254
  const data = await client.post(`/checklist/${checklistId}/checklist_item`, body);
@@ -2060,7 +2259,7 @@ function registerChecklistCommands(program, getClient) {
2060
2259
  const body = {};
2061
2260
  if (opts.name !== void 0) body["name"] = opts.name;
2062
2261
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2063
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2262
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2064
2263
  if (opts.parent !== void 0) body["parent"] = opts.parent;
2065
2264
  const data = await client.put(`/checklist/${checklistId}/checklist_item/${opts.itemId}`, body);
2066
2265
  formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
@@ -2087,6 +2286,7 @@ function registerChecklistCommands(program, getClient) {
2087
2286
  }
2088
2287
 
2089
2288
  // src/commands/custom-field.ts
2289
+ init_config();
2090
2290
  registerSchema("field", "list", "List custom fields for a list, folder, space, or workspace", [
2091
2291
  { flag: "--list-id", type: "string", required: false, description: "List ID" },
2092
2292
  { flag: "--folder-id", type: "string", required: false, description: "Folder ID" },
@@ -2100,7 +2300,8 @@ registerSchema("field", "set", "Set a custom field value on a task", [
2100
2300
  ]);
2101
2301
  registerSchema("field", "remove", "Remove a custom field value from a task", [
2102
2302
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2103
- { flag: "--field-id", type: "string", required: true, description: "Field ID" }
2303
+ { flag: "--field-id", type: "string", required: true, description: "Field ID" },
2304
+ { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
2104
2305
  ]);
2105
2306
  var FIELD_COLUMNS = [
2106
2307
  { key: "id", header: "ID", width: 20 },
@@ -2113,7 +2314,7 @@ function registerFieldCommands(program, getClient) {
2113
2314
  const field = program.command("field").description("Manage custom fields");
2114
2315
  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
2316
  const client = getClient();
2116
- const globalWorkspaceId = program.opts()["workspaceId"];
2317
+ const globalWorkspaceId = resolveWorkspaceId(program.opts()["workspaceId"]);
2117
2318
  let endpoint;
2118
2319
  if (opts.listId) {
2119
2320
  endpoint = `/list/${opts.listId}/field`;
@@ -2141,8 +2342,21 @@ function registerFieldCommands(program, getClient) {
2141
2342
  const data = await client.post(`/task/${opts.taskId}/field/${opts.fieldId}`, { value: parsedValue });
2142
2343
  formatOutput(data, FIELD_COLUMNS, getOutputOptions(program));
2143
2344
  });
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) => {
2345
+ 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
2346
  const client = getClient();
2347
+ if (!opts.confirm) {
2348
+ if (!process.stdin.isTTY) {
2349
+ process.stderr.write("Error: Use --confirm to remove in non-interactive mode.\n");
2350
+ process.exit(2);
2351
+ return;
2352
+ }
2353
+ const { confirm } = await import("@inquirer/prompts");
2354
+ const yes = await confirm({ message: `Remove field ${opts.fieldId} value from task ${opts.taskId}?` });
2355
+ if (!yes) {
2356
+ process.stdout.write("Cancelled.\n");
2357
+ return;
2358
+ }
2359
+ }
2146
2360
  await client.delete(`/task/${opts.taskId}/field/${opts.fieldId}`);
2147
2361
  process.stdout.write(`Removed field ${opts.fieldId} from task ${opts.taskId}
2148
2362
  `);
@@ -2319,8 +2533,8 @@ function registerRelationCommands(program, getClient) {
2319
2533
  }
2320
2534
 
2321
2535
  // src/commands/attachment.ts
2322
- import { writeFileSync } from "fs";
2323
- import { join } from "path";
2536
+ import { writeFileSync, existsSync as existsSync2 } from "fs";
2537
+ import { join, basename as basename2, resolve } from "path";
2324
2538
  registerSchema("attachment", "upload", "Upload a file to a task", [
2325
2539
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2326
2540
  { flag: "--file", type: "string", required: true, description: "Local file path" },
@@ -2332,7 +2546,8 @@ registerSchema("attachment", "list", "List attachments on a task", [
2332
2546
  registerSchema("attachment", "download", "Download an attachment from a task", [
2333
2547
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2334
2548
  { 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>)" }
2549
+ { flag: "--output", type: "string", required: false, description: "Output file path (default: ./attachment-<id>-<title>)" },
2550
+ { flag: "--force", type: "boolean", required: false, description: "Overwrite the output file if it exists" }
2336
2551
  ]);
2337
2552
  var ATTACHMENT_COLUMNS = [
2338
2553
  { key: "id", header: "ID", width: 20 },
@@ -2354,7 +2569,7 @@ function registerAttachmentCommands(program, getClient) {
2354
2569
  const attachments = data["attachments"] ?? [];
2355
2570
  formatOutput(attachments, ATTACHMENT_COLUMNS, getOutputOptions(program));
2356
2571
  });
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) => {
2572
+ 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
2573
  const { default: ora } = await import("ora");
2359
2574
  const client = getClient();
2360
2575
  const data = await client.get(`/task/${opts.taskId}`);
@@ -2366,8 +2581,15 @@ function registerAttachmentCommands(program, getClient) {
2366
2581
  process.exit(4);
2367
2582
  return;
2368
2583
  }
2369
- const outputPath = opts.output ?? join(process.cwd(), `attachment-${attachment2.id}-${attachment2.title}`);
2370
- const spinner = ora(`Downloading ${attachment2.title}...`).start();
2584
+ const safeTitle = basename2(attachment2.title.replace(/[/\\]/g, "_")) || "file";
2585
+ const outputPath = opts.output ? resolve(opts.output) : join(process.cwd(), `attachment-${attachment2.id}-${safeTitle}`);
2586
+ if (existsSync2(outputPath) && !opts.force) {
2587
+ process.stderr.write(`Error: "${outputPath}" already exists. Use --force to overwrite or --output to pick another path.
2588
+ `);
2589
+ process.exit(2);
2590
+ return;
2591
+ }
2592
+ const spinner = ora(`Downloading ${safeTitle}...`).start();
2371
2593
  try {
2372
2594
  const buffer = await client.downloadUrl(attachment2.url);
2373
2595
  writeFileSync(outputPath, Buffer.from(buffer));
@@ -2463,7 +2685,7 @@ function registerCommentCommands(program, getClient) {
2463
2685
  if (!parent) return;
2464
2686
  const client = getClient();
2465
2687
  const body = { comment_text: opts.text };
2466
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2688
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2467
2689
  if (opts.notifyAll) body["notify_all"] = true;
2468
2690
  const data = await client.post(`/${parent.type}/${parent.id}/comment`, body);
2469
2691
  process.stdout.write(`Created comment ${data["id"] ?? ""}
@@ -2472,7 +2694,7 @@ function registerCommentCommands(program, getClient) {
2472
2694
  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
2695
  const client = getClient();
2474
2696
  const body = { comment_text: opts.text };
2475
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2697
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2476
2698
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2477
2699
  await client.put(`/comment/${commentId}`, body);
2478
2700
  process.stdout.write(`Updated comment ${commentId}
@@ -2505,7 +2727,7 @@ function registerCommentCommands(program, getClient) {
2505
2727
  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
2728
  const client = getClient();
2507
2729
  const body = { comment_text: opts.text };
2508
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2730
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2509
2731
  if (opts.notifyAll) body["notify_all"] = true;
2510
2732
  const data = await client.post(`/comment/${commentId}/thread`, body);
2511
2733
  process.stdout.write(`Created reply ${data["id"] ?? ""}
@@ -2515,85 +2737,6 @@ function registerCommentCommands(program, getClient) {
2515
2737
 
2516
2738
  // src/commands/time-tracking.ts
2517
2739
  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
2740
  registerSchema("time", "list", "List time entries for a task or workspace", [
2598
2741
  { flag: "--task-id", type: "string", required: false, description: "Task ID (or use --workspace-id for workspace-wide)" },
2599
2742
  { flag: "--workspace-id", type: "string", required: false, description: "Workspace ID" },
@@ -2624,7 +2767,8 @@ registerSchema("time", "update", "Update a time entry", [
2624
2767
  ]);
2625
2768
  registerSchema("time", "delete", "Delete a time entry", [
2626
2769
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
2627
- { flag: "<timer-id>", type: "string", required: true, description: "Time entry ID" }
2770
+ { flag: "<timer-id>", type: "string", required: true, description: "Time entry ID" },
2771
+ { flag: "--confirm", type: "boolean", required: false, description: "Skip confirmation prompt" }
2628
2772
  ]);
2629
2773
  registerSchema("time", "running", "Get current running timer", [
2630
2774
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
@@ -2712,12 +2856,12 @@ function registerTimeTrackingCommands(program, getClient) {
2712
2856
  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
2857
  const client = getClient();
2714
2858
  const body = {
2715
- duration: parseInt(opts.duration, 10),
2859
+ duration: parseIntStrict(opts.duration, "--duration"),
2716
2860
  start: String(parseDate(opts.start))
2717
2861
  };
2718
2862
  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";
2863
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2864
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2721
2865
  if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
2722
2866
  const data = await client.post(`/task/${opts.taskId}/time`, body);
2723
2867
  process.stdout.write(`Created time entry ${data.data?.id ?? ""}
@@ -2729,9 +2873,9 @@ function registerTimeTrackingCommands(program, getClient) {
2729
2873
  const client = getClient();
2730
2874
  const body = {};
2731
2875
  if (opts.description !== void 0) body["description"] = opts.description;
2732
- if (opts.duration !== void 0) body["duration"] = parseInt(opts.duration, 10);
2876
+ if (opts.duration !== void 0) body["duration"] = parseIntStrict(opts.duration, "--duration");
2733
2877
  if (opts.start !== void 0) body["start"] = String(parseDate(opts.start));
2734
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2878
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2735
2879
  if (opts.tag.length) {
2736
2880
  body["tags"] = opts.tag.map((t) => ({ name: t }));
2737
2881
  if (opts.tagAction) body["tag_action"] = opts.tagAction;
@@ -2740,10 +2884,23 @@ function registerTimeTrackingCommands(program, getClient) {
2740
2884
  process.stdout.write(`Updated time entry ${timerId}
2741
2885
  `);
2742
2886
  });
2743
- time.command("delete").description("Delete a time entry").argument("<timer-id>", "Time entry ID").action(async (timerId) => {
2887
+ time.command("delete").description("Delete a time entry").argument("<timer-id>", "Time entry ID").option("--confirm", "Skip confirmation prompt").action(async (timerId, opts) => {
2744
2888
  const workspaceId = requireWorkspaceId4(program);
2745
2889
  if (!workspaceId) return;
2746
2890
  const client = getClient();
2891
+ if (!opts.confirm) {
2892
+ if (!process.stdin.isTTY) {
2893
+ process.stderr.write("Error: Use --confirm to delete in non-interactive mode.\n");
2894
+ process.exit(2);
2895
+ return;
2896
+ }
2897
+ const { confirm } = await import("@inquirer/prompts");
2898
+ const yes = await confirm({ message: `Delete time entry ${timerId}?` });
2899
+ if (!yes) {
2900
+ process.stdout.write("Cancelled.\n");
2901
+ return;
2902
+ }
2903
+ }
2747
2904
  await client.delete(`/team/${workspaceId}/time_entries/${timerId}`);
2748
2905
  process.stdout.write(`Deleted time entry ${timerId}
2749
2906
  `);
@@ -2774,7 +2931,7 @@ function registerTimeTrackingCommands(program, getClient) {
2774
2931
  const client = getClient();
2775
2932
  const body = { tid: opts.taskId };
2776
2933
  if (opts.description !== void 0) body["description"] = opts.description;
2777
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2934
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2778
2935
  if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
2779
2936
  const data = await client.post(`/team/${workspaceId}/time_entries/start`, body);
2780
2937
  process.stdout.write(`Started timer ${data.data?.id ?? ""}
@@ -2936,10 +3093,10 @@ function registerGoalCommands(program, getClient) {
2936
3093
  if (!workspaceId) return;
2937
3094
  const client = getClient();
2938
3095
  const body = { name: opts.name };
2939
- if (opts.dueDate !== void 0) body["due_date"] = opts.dueDate;
3096
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
2940
3097
  if (opts.description !== void 0) body["description"] = opts.description;
2941
3098
  if (opts.multipleOwners) body["multiple_owners"] = true;
2942
- if (opts.owner.length) body["owners"] = opts.owner.map((id) => parseInt(id, 10));
3099
+ if (opts.owner.length) body["owners"] = opts.owner.map((id) => parseIntStrict(id, "--owner"));
2943
3100
  if (opts.color !== void 0) body["color"] = opts.color;
2944
3101
  const data = await client.post(`/team/${workspaceId}/goal`, body);
2945
3102
  process.stdout.write(`Created goal ${data.goal?.id ?? ""}
@@ -2949,7 +3106,7 @@ function registerGoalCommands(program, getClient) {
2949
3106
  const client = getClient();
2950
3107
  const body = {};
2951
3108
  if (opts.name !== void 0) body["name"] = opts.name;
2952
- if (opts.dueDate !== void 0) body["due_date"] = opts.dueDate;
3109
+ if (opts.dueDate !== void 0) body["due_date"] = parseDateStrict(opts.dueDate, "--due-date");
2953
3110
  if (opts.description !== void 0) body["description"] = opts.description;
2954
3111
  if (opts.color !== void 0) body["color"] = opts.color;
2955
3112
  await client.put(`/goal/${goalId}`, body);
@@ -2978,8 +3135,8 @@ function registerGoalCommands(program, getClient) {
2978
3135
  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
3136
  const client = getClient();
2980
3137
  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);
3138
+ if (opts.stepsStart !== void 0) body["steps_start"] = parseFloatStrict(opts.stepsStart, "--steps-start");
3139
+ if (opts.stepsEnd !== void 0) body["steps_end"] = parseFloatStrict(opts.stepsEnd, "--steps-end");
2983
3140
  if (opts.unit !== void 0) body["unit"] = opts.unit;
2984
3141
  if (opts.taskIds.length) body["task_ids"] = opts.taskIds;
2985
3142
  if (opts.listIds.length) body["list_ids"] = opts.listIds;
@@ -2991,7 +3148,7 @@ function registerGoalCommands(program, getClient) {
2991
3148
  const client = getClient();
2992
3149
  const body = {};
2993
3150
  if (opts.name !== void 0) body["name"] = opts.name;
2994
- if (opts.stepsCurrent !== void 0) body["steps_current"] = parseFloat(opts.stepsCurrent);
3151
+ if (opts.stepsCurrent !== void 0) body["steps_current"] = parseFloatStrict(opts.stepsCurrent, "--steps-current");
2995
3152
  if (opts.note !== void 0) body["note"] = opts.note;
2996
3153
  await client.put(`/key_result/${keyResultId}`, body);
2997
3154
  process.stdout.write(`Updated key result ${keyResultId}
@@ -3019,6 +3176,7 @@ function registerGoalCommands(program, getClient) {
3019
3176
  }
3020
3177
 
3021
3178
  // src/commands/view.ts
3179
+ init_config();
3022
3180
  registerSchema("view", "list", "List views for a workspace, space, folder, or list", [
3023
3181
  { flag: "--workspace-id", type: "string", required: false, description: "Workspace ID (provide one parent)" },
3024
3182
  { flag: "--space-id", type: "string", required: false, description: "Space ID (provide one parent)" },
@@ -3071,6 +3229,10 @@ function resolveViewParent(opts, program) {
3071
3229
  if (opts.spaceId) parents.push({ segment: "space", id: opts.spaceId });
3072
3230
  if (opts.folderId) parents.push({ segment: "folder", id: opts.folderId });
3073
3231
  if (opts.listId) parents.push({ segment: "list", id: opts.listId });
3232
+ if (parents.length === 0) {
3233
+ const resolvedWsId = resolveWorkspaceId(void 0);
3234
+ if (resolvedWsId) parents.push({ segment: "team", id: resolvedWsId });
3235
+ }
3074
3236
  if (parents.length === 0) {
3075
3237
  process.stderr.write("Error: Provide one of --workspace-id, --space-id, --folder-id, or --list-id.\n");
3076
3238
  process.exit(2);
@@ -3274,6 +3436,11 @@ function registerWebhookCommands(program, getClient) {
3274
3436
  `);
3275
3437
  });
3276
3438
  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) => {
3439
+ if (opts.status !== void 0 && opts.status !== "active" && opts.status !== "inactive") {
3440
+ process.stderr.write('Error: --status must be "active" or "inactive".\n');
3441
+ process.exit(2);
3442
+ return;
3443
+ }
3277
3444
  const client = getClient();
3278
3445
  const body = {};
3279
3446
  if (opts.endpoint !== void 0) body["endpoint"] = opts.endpoint;
@@ -3375,7 +3542,7 @@ function registerUserCommands(program, getClient) {
3375
3542
  const body = {};
3376
3543
  if (opts.username !== void 0) body["username"] = opts.username;
3377
3544
  if (opts.admin !== void 0) body["admin"] = opts.admin === "true";
3378
- if (opts.customRoleId !== void 0) body["custom_role_id"] = parseInt(opts.customRoleId, 10);
3545
+ if (opts.customRoleId !== void 0) body["custom_role_id"] = parseIntStrict(opts.customRoleId, "--custom-role-id");
3379
3546
  await client.put(`/team/${workspaceId}/user/${userId}`, body);
3380
3547
  process.stdout.write(`Updated user ${userId}
3381
3548
  `);
@@ -3464,7 +3631,7 @@ function registerGroupCommands(program, getClient) {
3464
3631
  const client = getClient();
3465
3632
  const body = { name: opts.name };
3466
3633
  if (opts.memberId.length) {
3467
- body["members"] = opts.memberId.map((id) => ({ id: parseInt(id, 10) }));
3634
+ body["members"] = opts.memberId.map((id) => ({ id: parseIntStrict(id, "--member-id") }));
3468
3635
  }
3469
3636
  const data = await client.post(`/team/${workspaceId}/group`, body);
3470
3637
  process.stdout.write(`Created group ${data["id"] ?? ""}
@@ -3475,8 +3642,8 @@ function registerGroupCommands(program, getClient) {
3475
3642
  const body = {};
3476
3643
  if (opts.name !== void 0) body["name"] = opts.name;
3477
3644
  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) }));
3645
+ if (opts.addMember.length) members["add"] = opts.addMember.map((id) => ({ id: parseIntStrict(id, "--add-member") }));
3646
+ if (opts.removeMember.length) members["rem"] = opts.removeMember.map((id) => ({ id: parseIntStrict(id, "--remove-member") }));
3480
3647
  if (Object.keys(members).length) body["members"] = members;
3481
3648
  await client.put(`/group/${groupId}`, body);
3482
3649
  process.stdout.write(`Updated group ${groupId}
@@ -3527,6 +3694,15 @@ function parseBool(val) {
3527
3694
  if (val === void 0) return void 0;
3528
3695
  return val === "true";
3529
3696
  }
3697
+ var PERMISSION_LEVELS = ["read", "comment", "edit", "create"];
3698
+ function requirePermission(level) {
3699
+ if (!PERMISSION_LEVELS.includes(level)) {
3700
+ process.stderr.write(`Error: --permission must be one of: ${PERMISSION_LEVELS.join(", ")}
3701
+ `);
3702
+ process.exit(2);
3703
+ }
3704
+ return level;
3705
+ }
3530
3706
  registerSchema("guest", "invite", "Invite a guest to a workspace", [
3531
3707
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
3532
3708
  { flag: "--email", type: "string", required: true, description: "Email address to invite" },
@@ -3641,7 +3817,7 @@ function registerGuestCommands(program, getClient) {
3641
3817
  });
3642
3818
  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
3819
  const client = getClient();
3644
- await client.post(`/task/${opts.taskId}/guest/${guestId}`, { permission_level: opts.permission });
3820
+ await client.post(`/task/${opts.taskId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
3645
3821
  process.stdout.write(`Added guest ${guestId} to task ${opts.taskId}
3646
3822
  `);
3647
3823
  });
@@ -3653,7 +3829,7 @@ function registerGuestCommands(program, getClient) {
3653
3829
  });
3654
3830
  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
3831
  const client = getClient();
3656
- await client.post(`/list/${opts.listId}/guest/${guestId}`, { permission_level: opts.permission });
3832
+ await client.post(`/list/${opts.listId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
3657
3833
  process.stdout.write(`Added guest ${guestId} to list ${opts.listId}
3658
3834
  `);
3659
3835
  });
@@ -3665,7 +3841,7 @@ function registerGuestCommands(program, getClient) {
3665
3841
  });
3666
3842
  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
3843
  const client = getClient();
3668
- await client.post(`/folder/${opts.folderId}/guest/${guestId}`, { permission_level: opts.permission });
3844
+ await client.post(`/folder/${opts.folderId}/guest/${guestId}`, { permission_level: requirePermission(opts.permission) });
3669
3845
  process.stdout.write(`Added guest ${guestId} to folder ${opts.folderId}
3670
3846
  `);
3671
3847
  });
@@ -3849,7 +4025,7 @@ registerSchema("template", "apply-folder", "Create a folder from a folder templa
3849
4025
  ]);
3850
4026
  function registerTemplateCommands(program, getClient) {
3851
4027
  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) => {
4028
+ template.command("list").description("List task templates").option("--page <n>", "Page number (starts at 0)", intArg("--page")).action(async (opts) => {
3853
4029
  const workspaceId = requireWorkspaceId11(program);
3854
4030
  if (!workspaceId) return;
3855
4031
  const client = getClient();
@@ -4138,7 +4314,7 @@ function registerDocCommands(program, getClient) {
4138
4314
  const data = await client.get(`/v3/workspaces/${workspaceId}/docs/${opts.docId}`);
4139
4315
  formatOutput(data, DOC_COLUMNS, getOutputOptions(program));
4140
4316
  });
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) => {
4317
+ 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
4318
  const workspaceId = requireWorkspaceId14(program);
4143
4319
  if (!workspaceId) return;
4144
4320
  const client = getClient();
@@ -4236,7 +4412,7 @@ function registerDocCommands(program, getClient) {
4236
4412
  }
4237
4413
 
4238
4414
  // src/commands/skill-cmd.ts
4239
- import { readdirSync, readFileSync as readFileSync4, existsSync } from "fs";
4415
+ import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
4240
4416
  import { join as join2, dirname } from "path";
4241
4417
  import { fileURLToPath } from "url";
4242
4418
  var SKILL_COLUMNS = [
@@ -4254,9 +4430,9 @@ registerSchema("skill", "path", "Print the file system path to a skill directory
4254
4430
  function findSkillsDir() {
4255
4431
  const thisDir = dirname(fileURLToPath(import.meta.url));
4256
4432
  const bundledDir = join2(thisDir, "..", "skills");
4257
- if (existsSync(bundledDir)) return bundledDir;
4433
+ if (existsSync3(bundledDir)) return bundledDir;
4258
4434
  const projectDir = join2(thisDir, "..", "..", "skills");
4259
- if (existsSync(projectDir)) return projectDir;
4435
+ if (existsSync3(projectDir)) return projectDir;
4260
4436
  return void 0;
4261
4437
  }
4262
4438
  function parseFrontmatter(content) {
@@ -4297,7 +4473,7 @@ function loadSkills(skillsDir) {
4297
4473
  }
4298
4474
  for (const entry of entries) {
4299
4475
  const skillFile = join2(skillsDir, entry, "SKILL.md");
4300
- if (!existsSync(skillFile)) continue;
4476
+ if (!existsSync3(skillFile)) continue;
4301
4477
  try {
4302
4478
  const content = readFileSync4(skillFile, "utf-8");
4303
4479
  const { frontmatter } = parseFrontmatter(content);
@@ -4322,7 +4498,7 @@ function loadSkills(skillsDir) {
4322
4498
  function findSkill(skillsDir, name) {
4323
4499
  const skillDir = join2(skillsDir, name);
4324
4500
  const skillFile = join2(skillDir, "SKILL.md");
4325
- if (!existsSync(skillFile)) return void 0;
4501
+ if (!existsSync3(skillFile)) return void 0;
4326
4502
  try {
4327
4503
  const content = readFileSync4(skillFile, "utf-8");
4328
4504
  return { skillDir, content };
@@ -4452,10 +4628,12 @@ function registerChatCommands(program, getClient) {
4452
4628
  }
4453
4629
 
4454
4630
  // src/cli.ts
4455
- var VERSION = "0.3.0";
4631
+ var VERSION = "0.4.1";
4456
4632
  function createProgram() {
4457
4633
  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");
4634
+ 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(
4635
+ new Option("--format <format>", "Output format").choices(["table", "json", "csv", "tsv", "quiet", "id", "md"])
4636
+ ).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
4637
  return program;
4460
4638
  }
4461
4639
  function createClient(program) {
@@ -4492,6 +4670,9 @@ function getOutputOptions(program) {
4492
4670
  return result;
4493
4671
  }
4494
4672
  function run() {
4673
+ process.stdout.on("error", (err) => {
4674
+ if (err.code === "EPIPE") process.exit(EXIT_CODES.SUCCESS);
4675
+ });
4495
4676
  const program = createProgram();
4496
4677
  program.hook("preAction", () => {
4497
4678
  setProfileOverride(program.opts()["profile"]);