clickup-agent-cli 0.2.1 → 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,23 +31,24 @@ 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
- var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES;
51
+ var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES, DryRunComplete;
51
52
  var init_errors = __esm({
52
53
  "src/errors.ts"() {
53
54
  "use strict";
@@ -85,6 +86,12 @@ var init_errors = __esm({
85
86
  RATE_LIMITED: 6,
86
87
  NETWORK_ERROR: 7
87
88
  };
89
+ DryRunComplete = class extends Error {
90
+ constructor() {
91
+ super("Dry run complete");
92
+ this.name = "DryRunComplete";
93
+ }
94
+ };
88
95
  }
89
96
  });
90
97
 
@@ -140,6 +147,41 @@ var init_client = __esm({
140
147
  async delete(path, body) {
141
148
  return this.request("DELETE", path, body);
142
149
  }
150
+ async downloadUrl(url) {
151
+ const controller = new AbortController();
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
+ }
163
+ try {
164
+ const response = await fetch(url, {
165
+ headers,
166
+ signal: controller.signal
167
+ });
168
+ clearTimeout(timeoutId);
169
+ if (!response.ok) {
170
+ const body = await this.safeJson(response);
171
+ throw parseApiError(body, response.status);
172
+ }
173
+ return response.arrayBuffer();
174
+ } catch (error) {
175
+ clearTimeout(timeoutId);
176
+ if (error instanceof ClickUpError) throw error;
177
+ throw new ClickUpError(
178
+ `Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
179
+ 0,
180
+ void 0,
181
+ void 0
182
+ );
183
+ }
184
+ }
143
185
  async upload(path, filePath, filename) {
144
186
  const fileBuffer = readFileSync(filePath);
145
187
  const resolvedFilename = filename ?? basename(filePath);
@@ -148,7 +190,7 @@ var init_client = __esm({
148
190
  const url = `${this.baseUrl}${path}`;
149
191
  if (this.dryRun) {
150
192
  this.logDryRun("POST", url, "[multipart form data]");
151
- return {};
193
+ throw new DryRunComplete();
152
194
  }
153
195
  const controller = new AbortController();
154
196
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@@ -193,7 +235,7 @@ var init_client = __esm({
193
235
  }
194
236
  if (this.dryRun) {
195
237
  this.logDryRun(method, url, body);
196
- return {};
238
+ throw new DryRunComplete();
197
239
  }
198
240
  let lastError;
199
241
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
@@ -240,17 +282,6 @@ Headers: ${JSON.stringify(redactedHeaders)}
240
282
  process.stderr.write(`[${method}] ${path} ${response.status} ${response.statusText} (${elapsed}ms)
241
283
  `);
242
284
  }
243
- const remaining = response.headers.get("X-RateLimit-Remaining");
244
- const reset = response.headers.get("X-RateLimit-Reset");
245
- if (remaining === "0" && reset) {
246
- const resetTime = parseInt(reset, 10) * 1e3;
247
- const waitMs = Math.max(0, resetTime - Date.now());
248
- if (waitMs > 0) {
249
- process.stderr.write(`Rate limited. Waiting ${Math.ceil(waitMs / 1e3)}s...
250
- `);
251
- await this.sleep(waitMs);
252
- }
253
- }
254
285
  if (!response.ok) {
255
286
  const responseBody = await this.safeJson(response);
256
287
  const error = parseApiError(responseBody, response.status);
@@ -262,6 +293,18 @@ Headers: ${JSON.stringify(redactedHeaders)}
262
293
  throw error;
263
294
  }
264
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
+ }
265
308
  lastError = error;
266
309
  continue;
267
310
  }
@@ -321,61 +364,152 @@ Headers: ${JSON.stringify(redactedHeaders)}
321
364
  }
322
365
  }
323
366
  sleep(ms) {
324
- return new Promise((resolve) => setTimeout(resolve, ms));
367
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
325
368
  }
326
369
  };
327
370
  }
328
371
  });
329
372
 
330
373
  // src/config.ts
374
+ import { readFileSync as readFileSync2, chmodSync, existsSync } from "fs";
331
375
  import Conf from "conf";
332
376
  function isValidConfigKey(key) {
333
- return CONFIG_KEYS.includes(key);
334
- }
335
- function resolveString(key, flagValue) {
336
- if (flagValue !== void 0) return flagValue;
337
- const envVal = process.env[ENV_MAP[key]];
338
- if (envVal !== void 0) return envVal;
339
- const stored = config.get(key);
340
- if (typeof stored === "string") return stored;
341
- return void 0;
377
+ return ALL_CONFIG_KEYS.includes(key);
378
+ }
379
+ function hardenConfigFile() {
380
+ try {
381
+ if (existsSync(config.path)) chmodSync(config.path, 384);
382
+ } catch {
383
+ }
384
+ }
385
+ function setProfileOverride(nameOrKey) {
386
+ _profileOverride = nameOrKey ? findProfileKey(nameOrKey) : void 0;
387
+ }
388
+ function getActiveProfileKey() {
389
+ if (_profileOverride) return _profileOverride;
390
+ const active = config.get("active_profile");
391
+ return active ?? "default";
392
+ }
393
+ function getProfiles() {
394
+ const stored = config.get("profiles");
395
+ return stored ?? {};
396
+ }
397
+ function getProfile(key) {
398
+ return getProfiles()[key];
399
+ }
400
+ function setProfile(key, profile) {
401
+ const profiles = getProfiles();
402
+ profiles[key] = profile;
403
+ config.set("profiles", profiles);
404
+ hardenConfigFile();
405
+ }
406
+ function deleteProfile(key) {
407
+ const profiles = getProfiles();
408
+ delete profiles[key];
409
+ config.set("profiles", profiles);
410
+ }
411
+ function slugifyWorkspaceName(name) {
412
+ return name.toLowerCase().replace(/'/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
413
+ }
414
+ function findProfileKey(nameOrKey) {
415
+ const profiles = getProfiles();
416
+ if (profiles[nameOrKey] !== void 0) return nameOrKey;
417
+ const lower = nameOrKey.toLowerCase();
418
+ for (const [key, profile] of Object.entries(profiles)) {
419
+ if (profile.workspace_name?.toLowerCase() === lower) return key;
420
+ if (profile.nickname?.toLowerCase() === lower) return key;
421
+ }
422
+ for (const [key, profile] of Object.entries(profiles)) {
423
+ if (profile.workspace_name?.toLowerCase().includes(lower)) return key;
424
+ if (profile.nickname?.toLowerCase().includes(lower)) return key;
425
+ }
426
+ return nameOrKey;
427
+ }
428
+ function migrateConfig() {
429
+ const legacyToken = config.get("token");
430
+ const legacyWorkspaceId = config.get("workspace_id");
431
+ if (!legacyToken && !legacyWorkspaceId) return;
432
+ const profiles = getProfiles();
433
+ if (!profiles["default"]) {
434
+ const defaultProfile = {};
435
+ if (legacyToken) defaultProfile.token = legacyToken;
436
+ if (legacyWorkspaceId) defaultProfile.workspace_id = legacyWorkspaceId;
437
+ setProfile("default", defaultProfile);
438
+ config.delete("token");
439
+ config.delete("workspace_id");
440
+ }
342
441
  }
343
- function resolveToken(flagValue) {
344
- return resolveString("token", flagValue);
442
+ function resolveToken(flagValue, tokenFilePath) {
443
+ if (flagValue) return flagValue;
444
+ if (tokenFilePath) {
445
+ let content;
446
+ try {
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}`);
451
+ }
452
+ if (!content) {
453
+ throw new Error(`Token file "${tokenFilePath}" is empty`);
454
+ }
455
+ return content;
456
+ }
457
+ const envVal = process.env["CLICKUP_API_TOKEN"];
458
+ if (envVal) return envVal;
459
+ const profileKey = getActiveProfileKey();
460
+ const profiles = getProfiles();
461
+ const activeProfile = profiles[profileKey];
462
+ if (activeProfile?.token) return activeProfile.token;
463
+ const legacyToken = config.get("token");
464
+ if (legacyToken) return legacyToken;
465
+ return void 0;
345
466
  }
346
467
  function resolveWorkspaceId(flagValue) {
347
- return resolveString("workspace_id", flagValue);
468
+ if (flagValue) return flagValue;
469
+ const envVal = process.env["CLICKUP_WORKSPACE_ID"];
470
+ if (envVal) return envVal;
471
+ const profileKey = getActiveProfileKey();
472
+ const profiles = getProfiles();
473
+ const activeProfile = profiles[profileKey];
474
+ if (activeProfile?.workspace_id) return activeProfile.workspace_id;
475
+ const withWorkspace = Object.values(profiles).filter((p) => p.workspace_id);
476
+ if (withWorkspace.length === 1) return withWorkspace[0].workspace_id;
477
+ const legacyId = config.get("workspace_id");
478
+ if (legacyId) return legacyId;
479
+ return void 0;
348
480
  }
349
481
  function resolveOutputFormat(flagValue) {
350
- const val = resolveString("output_format", flagValue);
351
- if (val) return val;
482
+ if (flagValue) return flagValue;
483
+ const envVal = process.env["CLICKUP_OUTPUT_FORMAT"];
484
+ if (envVal) return envVal;
485
+ const stored = config.get("output_format");
486
+ if (stored) return stored;
352
487
  return process.stdout.isTTY ? "table" : "json";
353
488
  }
354
- var CONFIG_KEYS, VALID_CONFIG_KEYS, config, ENV_MAP;
489
+ var ALL_CONFIG_KEYS, VALID_CONFIG_KEYS, config, _profileOverride;
355
490
  var init_config = __esm({
356
491
  "src/config.ts"() {
357
492
  "use strict";
358
- CONFIG_KEYS = ["token", "workspace_id", "output_format", "color", "page_size", "timezone"];
359
- VALID_CONFIG_KEYS = CONFIG_KEYS;
493
+ ALL_CONFIG_KEYS = ["token", "workspace_id", "output_format", "color", "page_size", "timezone", "active_profile"];
494
+ VALID_CONFIG_KEYS = ALL_CONFIG_KEYS;
360
495
  config = new Conf({
361
496
  projectName: "clickup-cli",
362
497
  schema: {
363
498
  token: { type: "string" },
364
499
  workspace_id: { type: "string" },
365
- output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id"] },
500
+ output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id", "md"] },
366
501
  color: { type: "boolean", default: true },
367
502
  page_size: { type: "number", default: 100 },
368
- timezone: { type: "string" }
503
+ timezone: { type: "string" },
504
+ active_profile: { type: "string" },
505
+ profiles: { type: "object" }
369
506
  }
370
507
  });
371
- ENV_MAP = {
372
- token: "CLICKUP_API_TOKEN",
373
- workspace_id: "CLICKUP_WORKSPACE_ID",
374
- output_format: "CLICKUP_OUTPUT_FORMAT",
375
- color: "CLICKUP_COLOR",
376
- page_size: "CLICKUP_PAGE_SIZE",
377
- timezone: "CLICKUP_TIMEZONE"
378
- };
508
+ hardenConfigFile();
509
+ try {
510
+ migrateConfig();
511
+ } catch {
512
+ }
379
513
  }
380
514
  });
381
515
 
@@ -387,22 +521,29 @@ __export(auth_exports, {
387
521
  oauthLogin: () => oauthLogin
388
522
  });
389
523
  import { createServer } from "http";
390
- import { randomBytes, createHash } from "crypto";
524
+ import { randomBytes, createHash, timingSafeEqual } from "crypto";
391
525
  function generateCodeVerifier() {
392
526
  return randomBytes(48).toString("base64url");
393
527
  }
394
528
  function generateCodeChallenge(verifier) {
395
529
  return createHash("sha256").update(verifier).digest("base64url");
396
530
  }
397
- function buildAuthorizeUrl(clientId, codeChallenge) {
531
+ function buildAuthorizeUrl(clientId, codeChallenge, state) {
398
532
  const params = new URLSearchParams({
399
533
  client_id: clientId,
400
534
  redirect_uri: REDIRECT_URI,
401
535
  code_challenge: codeChallenge,
402
- code_challenge_method: "S256"
536
+ code_challenge_method: "S256",
537
+ state
403
538
  });
404
539
  return `${CLICKUP_AUTHORIZE_URL}?${params.toString()}`;
405
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
+ }
406
547
  async function exchangeCodeForToken(code, clientId, clientSecret, codeVerifier) {
407
548
  const response = await fetch(CLICKUP_TOKEN_URL, {
408
549
  method: "POST",
@@ -425,8 +566,8 @@ async function exchangeCodeForToken(code, clientId, clientSecret, codeVerifier)
425
566
  }
426
567
  return token;
427
568
  }
428
- function waitForCallback(server) {
429
- return new Promise((resolve, reject) => {
569
+ function waitForCallback(server, expectedState) {
570
+ return new Promise((resolve2, reject) => {
430
571
  const timeout = setTimeout(() => {
431
572
  server.close();
432
573
  reject(new Error("OAuth callback timed out after 120 seconds"));
@@ -440,6 +581,7 @@ function waitForCallback(server) {
440
581
  }
441
582
  const code = url.searchParams.get("code");
442
583
  const error = url.searchParams.get("error");
584
+ const state = url.searchParams.get("state");
443
585
  if (error) {
444
586
  res.writeHead(200, { "Content-Type": "text/html" });
445
587
  res.end("<html><body><h1>Authentication failed</h1><p>You can close this tab.</p></body></html>");
@@ -448,6 +590,11 @@ function waitForCallback(server) {
448
590
  reject(new Error(`OAuth error: ${error}`));
449
591
  return;
450
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
+ }
451
598
  if (!code) {
452
599
  res.writeHead(400, { "Content-Type": "text/html" });
453
600
  res.end("<html><body><h1>Missing code</h1><p>No authorization code received.</p></body></html>");
@@ -457,24 +604,28 @@ function waitForCallback(server) {
457
604
  res.end("<html><body><h1>Authenticated</h1><p>You can close this tab and return to the terminal.</p></body></html>");
458
605
  clearTimeout(timeout);
459
606
  server.close();
460
- resolve(code);
607
+ resolve2(code);
461
608
  });
462
609
  });
463
610
  }
464
611
  async function openBrowser(url) {
465
612
  const { platform } = process;
613
+ const { execFile } = await import("child_process");
466
614
  let command;
615
+ let args;
467
616
  if (platform === "darwin") {
468
617
  command = "open";
618
+ args = [url];
469
619
  } else if (platform === "win32") {
470
- command = "start";
620
+ command = "cmd";
621
+ args = ["/c", "start", "", url];
471
622
  } else {
472
623
  command = "xdg-open";
624
+ args = [url];
473
625
  }
474
- const { exec } = await import("child_process");
475
- return new Promise((resolve) => {
476
- exec(`${command} "${url}"`, () => {
477
- resolve();
626
+ return new Promise((resolve2) => {
627
+ execFile(command, args, () => {
628
+ resolve2();
478
629
  });
479
630
  });
480
631
  }
@@ -493,22 +644,24 @@ async function oauthLogin() {
493
644
  }
494
645
  const codeVerifier = generateCodeVerifier();
495
646
  const codeChallenge = generateCodeChallenge(codeVerifier);
647
+ const state = randomBytes(24).toString("base64url");
496
648
  const server = createServer();
497
- await new Promise((resolve, reject) => {
498
- server.listen(CALLBACK_PORT, () => resolve());
649
+ await new Promise((resolve2, reject) => {
650
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => resolve2());
499
651
  server.on("error", reject);
500
652
  });
501
- const authorizeUrl = buildAuthorizeUrl(clientId, codeChallenge);
653
+ const authorizeUrl = buildAuthorizeUrl(clientId, codeChallenge, state);
502
654
  process.stderr.write(`Opening browser for authentication...
503
655
  `);
504
656
  process.stderr.write(`If the browser does not open, visit:
505
657
  ${authorizeUrl}
506
658
  `);
507
659
  await openBrowser(authorizeUrl);
508
- const code = await waitForCallback(server);
660
+ const code = await waitForCallback(server, state);
509
661
  process.stderr.write("Exchanging code for token...\n");
510
662
  const token = await exchangeCodeForToken(code, clientId, clientSecret, codeVerifier);
511
- config.set("token", token);
663
+ const profileKey = getActiveProfileKey();
664
+ setProfile(profileKey, { ...getProfile(profileKey), token });
512
665
  return token;
513
666
  }
514
667
  var CLICKUP_AUTHORIZE_URL, CLICKUP_TOKEN_URL, CALLBACK_PORT, CALLBACK_PATH, REDIRECT_URI;
@@ -528,13 +681,14 @@ var init_auth = __esm({
528
681
  init_client();
529
682
  init_errors();
530
683
  init_config();
531
- import { Command } from "commander";
684
+ import { Command, Option } from "commander";
532
685
 
533
686
  // src/commands/auth-cmd.ts
534
687
  init_config();
688
+ import { readFileSync as readFileSync3 } from "fs";
535
689
  function registerAuthCommands(program, getClient) {
536
690
  const auth = program.command("auth").description("Manage authentication");
537
- auth.command("login").description("Authenticate with ClickUp").option("--token <token>", "Personal API token").option("--oauth", "Use OAuth2 PKCE browser flow (requires CLICKUP_CLIENT_ID and CLICKUP_CLIENT_SECRET)").action(async (opts) => {
691
+ auth.command("login").description("Authenticate with ClickUp").option("--token <token>", "Personal API token").option("--token-file <path>", "Read token from this file path").option("--oauth", "Use OAuth2 PKCE browser flow (requires CLICKUP_CLIENT_ID and CLICKUP_CLIENT_SECRET)").action(async (opts) => {
538
692
  if (opts.oauth) {
539
693
  const { oauthLogin: oauthLogin2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
540
694
  const token2 = await oauthLogin2();
@@ -550,9 +704,19 @@ function registerAuthCommands(program, getClient) {
550
704
  return;
551
705
  }
552
706
  let token = opts.token;
707
+ if (!token && opts.tokenFile) {
708
+ try {
709
+ token = readFileSync3(opts.tokenFile, "utf8").trim();
710
+ } catch {
711
+ process.stderr.write(`Error: Could not read token file: ${opts.tokenFile}
712
+ `);
713
+ process.exit(2);
714
+ return;
715
+ }
716
+ }
553
717
  if (!token) {
554
718
  if (!process.stdin.isTTY) {
555
- process.stderr.write("Error: No token provided. Use --token <token> or --oauth in non-interactive mode.\n");
719
+ process.stderr.write("Error: No token provided. Use --token <token>, --token-file <path>, or --oauth in non-interactive mode.\n");
556
720
  process.exit(2);
557
721
  return;
558
722
  }
@@ -571,15 +735,29 @@ function registerAuthCommands(program, getClient) {
571
735
  const client = new ClientClass({ token });
572
736
  try {
573
737
  const userData = await client.get("/user");
574
- config.set("token", token);
738
+ const profileKey = getActiveProfileKey();
739
+ const profiles = getProfiles();
740
+ const existing = profiles[profileKey] ?? {};
741
+ setProfile(profileKey, { ...existing, token });
575
742
  process.stdout.write(`Authenticated as ${userData.user.username} (${userData.user.email})
576
743
  `);
744
+ const profile = getProfiles()[profileKey];
745
+ if (!profile?.workspace_id) {
746
+ process.stdout.write("Run: clickup workspace setup -- to configure your workspace.\n");
747
+ }
577
748
  } catch {
578
749
  process.stderr.write("Error: Invalid token. Authentication failed.\n");
579
750
  process.exit(3);
580
751
  }
581
752
  });
582
753
  auth.command("logout").description("Remove stored credentials").action(() => {
754
+ const profileKey = getActiveProfileKey();
755
+ const profiles = getProfiles();
756
+ const existing = profiles[profileKey];
757
+ if (existing) {
758
+ const { token: _removed, ...profileWithoutToken } = existing;
759
+ setProfile(profileKey, profileWithoutToken);
760
+ }
583
761
  config.delete("token");
584
762
  process.stdout.write("Logged out. Token removed.\n");
585
763
  });
@@ -592,9 +770,17 @@ function registerAuthCommands(program, getClient) {
592
770
  const prefix = token.slice(0, 8);
593
771
  process.stdout.write(`Token: ${prefix}...
594
772
  `);
595
- const workspaceId = config.get("workspace_id");
596
- if (workspaceId) {
597
- process.stdout.write(`Workspace: ${workspaceId}
773
+ const profileKey = getActiveProfileKey();
774
+ process.stdout.write(`Profile: ${profileKey}
775
+ `);
776
+ const profiles = getProfiles();
777
+ const activeProfile = profiles[profileKey];
778
+ if (activeProfile?.workspace_id) {
779
+ process.stdout.write(`Workspace: ${activeProfile.workspace_name ?? activeProfile.workspace_id}
780
+ `);
781
+ } else {
782
+ const legacyId = config.get("workspace_id");
783
+ if (legacyId) process.stdout.write(`Workspace: ${legacyId}
598
784
  `);
599
785
  }
600
786
  try {
@@ -620,7 +806,7 @@ function registerAuthCommands(program, getClient) {
620
806
 
621
807
  // src/commands/config-cmd.ts
622
808
  init_config();
623
- function registerConfigCommands(program) {
809
+ function registerConfigCommands(program, getClient) {
624
810
  const configCmd = program.command("config").description("Manage CLI configuration");
625
811
  configCmd.command("set").description("Set a configuration value").argument("<key>", "Config key").argument("<value>", "Config value").action((key, value) => {
626
812
  if (!isValidConfigKey(key)) {
@@ -639,10 +825,20 @@ function registerConfigCommands(program) {
639
825
  return;
640
826
  }
641
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);
642
837
  } else {
643
838
  config.set(key, value);
644
839
  }
645
- process.stdout.write(`Set ${key} = ${value}
840
+ const shown = key === "token" ? `${value.slice(0, 8)}...` : value;
841
+ process.stdout.write(`Set ${key} = ${shown}
646
842
  `);
647
843
  });
648
844
  configCmd.command("get").description("Get a configuration value").argument("<key>", "Config key").action((key) => {
@@ -666,7 +862,8 @@ function registerConfigCommands(program) {
666
862
  for (const key of keys) {
667
863
  const value = store[key];
668
864
  if (value !== void 0) {
669
- process.stdout.write(`${key} = ${value}
865
+ const shown = key === "token" ? `${String(value).slice(0, 8)}...` : value;
866
+ process.stdout.write(`${key} = ${shown}
670
867
  `);
671
868
  }
672
869
  }
@@ -694,6 +891,141 @@ function registerConfigCommands(program) {
694
891
  configCmd.command("path").description("Print the config file path").action(() => {
695
892
  process.stdout.write(config.path + "\n");
696
893
  });
894
+ configCmd.command("validate").description("Verify the stored token works and show current identity").action(async () => {
895
+ const client = getClient();
896
+ let user;
897
+ try {
898
+ const userData = await client.get("/user");
899
+ user = userData.user;
900
+ } catch (err) {
901
+ process.stderr.write(`Error: Token validation failed. Run: clickup auth login
902
+ `);
903
+ process.exit(3);
904
+ return;
905
+ }
906
+ let workspaceName = "(none)";
907
+ let workspaceId = "(none)";
908
+ try {
909
+ const teamsData = await client.get("/team");
910
+ const activeKey = getActiveProfileKey();
911
+ const profiles = getProfiles();
912
+ const activeProfile = profiles[activeKey];
913
+ if (activeProfile?.workspace_id) {
914
+ const match = teamsData.teams.find((t) => t.id === activeProfile.workspace_id);
915
+ workspaceName = match?.name ?? activeProfile.workspace_name ?? activeProfile.workspace_id;
916
+ workspaceId = activeProfile.workspace_id;
917
+ } else if (teamsData.teams.length > 0) {
918
+ workspaceName = teamsData.teams.map((t) => t.name).join(", ");
919
+ workspaceId = teamsData.teams.map((t) => t.id).join(", ");
920
+ }
921
+ } catch {
922
+ }
923
+ process.stdout.write(`User: ${user.username} (${user.email})
924
+ `);
925
+ process.stdout.write(`Active profile: ${getActiveProfileKey()}
926
+ `);
927
+ process.stdout.write(`Workspace: ${workspaceName}
928
+ `);
929
+ process.stdout.write(`Workspace ID: ${workspaceId}
930
+ `);
931
+ process.stdout.write(`Token status: Valid
932
+ `);
933
+ });
934
+ const profileCmd = configCmd.command("profile").description("Manage named profiles");
935
+ profileCmd.command("list").description("List all profiles").action(() => {
936
+ const profiles = getProfiles();
937
+ const activeKey = getActiveProfileKey();
938
+ const keys = Object.keys(profiles);
939
+ if (keys.length === 0) {
940
+ process.stdout.write("No profiles configured. Run: clickup auth login\n");
941
+ return;
942
+ }
943
+ const col = (s, w) => s.padEnd(w).slice(0, w);
944
+ process.stdout.write(
945
+ `${col("Key", 20)} ${col("Workspace Name", 30)} ${col("Nickname", 15)} ${col("Workspace ID", 16)} Active
946
+ `
947
+ );
948
+ process.stdout.write(`${"-".repeat(20)} ${"-".repeat(30)} ${"-".repeat(15)} ${"-".repeat(16)} ------
949
+ `);
950
+ for (const key of keys) {
951
+ const p = profiles[key];
952
+ const active = key === activeKey ? "*" : "";
953
+ process.stdout.write(
954
+ `${col(key, 20)} ${col(p.workspace_name ?? "", 30)} ${col(p.nickname ?? "", 15)} ${col(p.workspace_id ?? "", 16)} ${active}
955
+ `
956
+ );
957
+ }
958
+ });
959
+ profileCmd.command("create").description("Create an empty profile").argument("<key>", "Profile key").action((key) => {
960
+ const profiles = getProfiles();
961
+ if (profiles[key]) {
962
+ process.stderr.write(`Error: Profile "${key}" already exists.
963
+ `);
964
+ process.exit(2);
965
+ return;
966
+ }
967
+ setProfile(key, {});
968
+ process.stdout.write(`Profile "${key}" created.
969
+ `);
970
+ });
971
+ profileCmd.command("delete").description("Delete a profile").argument("<key>", "Profile key").option("--confirm", "Skip confirmation prompt").action(async (key, opts) => {
972
+ const profiles = getProfiles();
973
+ if (!profiles[key]) {
974
+ process.stderr.write(`Error: Profile "${key}" not found.
975
+ `);
976
+ process.exit(2);
977
+ return;
978
+ }
979
+ const isActive = key === getActiveProfileKey();
980
+ if (!opts.confirm) {
981
+ if (!process.stdin.isTTY) {
982
+ process.stderr.write("Error: Add --confirm to delete a profile in non-interactive mode.\n");
983
+ process.exit(2);
984
+ return;
985
+ }
986
+ const { confirm } = await import("@inquirer/prompts");
987
+ const answer = await confirm({
988
+ message: isActive ? `"${key}" is the active profile. Delete it?` : `Delete profile "${key}"?`,
989
+ default: false
990
+ });
991
+ if (!answer) {
992
+ process.stdout.write("Cancelled.\n");
993
+ return;
994
+ }
995
+ }
996
+ deleteProfile(key);
997
+ if (isActive) {
998
+ config.delete("active_profile");
999
+ }
1000
+ process.stdout.write(`Profile "${key}" deleted.
1001
+ `);
1002
+ });
1003
+ profileCmd.command("use").description("Switch the active profile").argument("<key>", "Profile key, workspace name, or nickname").action((nameOrKey) => {
1004
+ const profiles = getProfiles();
1005
+ const resolvedKey = findProfileKey(nameOrKey);
1006
+ if (!profiles[resolvedKey]) {
1007
+ process.stderr.write(`Error: Profile "${nameOrKey}" not found.
1008
+ `);
1009
+ process.exit(2);
1010
+ return;
1011
+ }
1012
+ config.set("active_profile", resolvedKey);
1013
+ process.stdout.write(`Active profile: ${resolvedKey}
1014
+ `);
1015
+ });
1016
+ profileCmd.command("nickname").description("Set a short nickname for a profile").argument("<key>", "Profile key").argument("<nickname>", "Short nickname").action((key, nickname) => {
1017
+ const profiles = getProfiles();
1018
+ const profile = profiles[key];
1019
+ if (!profile) {
1020
+ process.stderr.write(`Error: Profile "${key}" not found.
1021
+ `);
1022
+ process.exit(2);
1023
+ return;
1024
+ }
1025
+ setProfile(key, { ...profile, nickname });
1026
+ process.stdout.write(`Nickname "${nickname}" set for profile "${key}".
1027
+ `);
1028
+ });
697
1029
  }
698
1030
 
699
1031
  // src/commands/workspace.ts
@@ -772,18 +1104,23 @@ function applySort(rows, sort) {
772
1104
  return desc ? -result : result;
773
1105
  });
774
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
+ }
775
1111
  function getValue(row, key) {
776
1112
  if (row && typeof row === "object") {
777
1113
  const record = row;
778
1114
  const val = record[key];
779
1115
  if (val === null || val === void 0) return "";
780
- if (typeof val === "object") return JSON.stringify(val);
781
- return String(val);
1116
+ if (typeof val === "object") return sanitize(JSON.stringify(val));
1117
+ return sanitize(String(val));
782
1118
  }
783
1119
  return "";
784
1120
  }
785
1121
  function truncate(str, width) {
786
1122
  if (str.length <= width) return str;
1123
+ if (width <= 3) return str.slice(0, Math.max(0, width));
787
1124
  return str.slice(0, width - 3) + "...";
788
1125
  }
789
1126
  function printTable(rows, columns, opts) {
@@ -813,7 +1150,10 @@ function printDelimited(rows, columns, delimiter, opts) {
813
1150
  }
814
1151
  for (const row of rows) {
815
1152
  const values = columns.map((col) => {
816
- const val = getValue(row, col.key);
1153
+ let val = getValue(row, col.key);
1154
+ if (delimiter === "," && /^[=+\-@\t\r]/.test(val)) {
1155
+ val = `'${val}`;
1156
+ }
817
1157
  if (delimiter === "," && (val.includes(",") || val.includes('"') || val.includes("\n"))) {
818
1158
  return `"${val.replace(/"/g, '""')}"`;
819
1159
  }
@@ -869,6 +1209,7 @@ function getFields(resource, action) {
869
1209
  }
870
1210
 
871
1211
  // src/commands/workspace.ts
1212
+ registerSchema("workspace", "setup", "Configure workspace -- fetches workspaces and saves profile", []);
872
1213
  registerSchema("workspace", "list", "List all workspaces accessible to the authenticated user", []);
873
1214
  registerSchema("workspace", "get", "Get workspace details", [
874
1215
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" }
@@ -912,6 +1253,49 @@ function requireWorkspaceId(program) {
912
1253
  }
913
1254
  function registerWorkspaceCommands(program, getClient) {
914
1255
  const workspace = program.command("workspace").description("Manage workspaces");
1256
+ workspace.command("setup").description("Configure workspace for the active profile").action(async () => {
1257
+ const client = getClient();
1258
+ const data = await client.get("/team");
1259
+ const teams = data.teams;
1260
+ if (teams.length === 0) {
1261
+ process.stderr.write("Error: No workspaces found for this token.\n");
1262
+ process.exit(1);
1263
+ return;
1264
+ }
1265
+ let selected;
1266
+ if (teams.length === 1) {
1267
+ selected = teams[0];
1268
+ } else {
1269
+ if (!process.stdin.isTTY) {
1270
+ process.stderr.write("Error: Multiple workspaces found. Use --workspace-id or run in interactive mode.\n");
1271
+ process.exit(2);
1272
+ return;
1273
+ }
1274
+ const { select } = await import("@inquirer/prompts");
1275
+ const selectedId = await select({
1276
+ message: "Select workspace:",
1277
+ choices: teams.map((t) => ({ name: t.name, value: t.id }))
1278
+ });
1279
+ selected = teams.find((t) => t.id === selectedId);
1280
+ }
1281
+ const profileKey = getActiveProfileKey();
1282
+ const profiles = getProfiles();
1283
+ const existing = profiles[profileKey] ?? {};
1284
+ const slugKey = slugifyWorkspaceName(selected.name);
1285
+ setProfile(profileKey, {
1286
+ ...existing,
1287
+ workspace_id: selected.id,
1288
+ workspace_name: selected.name
1289
+ });
1290
+ process.stdout.write(
1291
+ `Workspace "${selected.name}" saved as profile "${profileKey}".
1292
+ `
1293
+ );
1294
+ if (slugKey !== profileKey) {
1295
+ process.stdout.write(`Use --profile "${selected.name}" or --profile ${profileKey}
1296
+ `);
1297
+ }
1298
+ });
915
1299
  workspace.command("list").description("List all workspaces").action(async () => {
916
1300
  const client = getClient();
917
1301
  const data = await client.get("/team");
@@ -1160,6 +1544,124 @@ function registerFolderCommands(program, getClient) {
1160
1544
  });
1161
1545
  }
1162
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
+
1163
1665
  // src/commands/list.ts
1164
1666
  registerSchema("list", "list", "List lists in a folder", [
1165
1667
  { flag: "--folder-id", type: "string", required: true, description: "Folder ID" },
@@ -1232,11 +1734,11 @@ function registerListCommands(program, getClient) {
1232
1734
  const data = await client.get(`/list/${listId}`);
1233
1735
  formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
1234
1736
  });
1235
- 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) => {
1236
1738
  const client = getClient();
1237
1739
  const body = { name: opts.name };
1238
1740
  if (opts.content !== void 0) body["content"] = opts.content;
1239
- 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");
1240
1742
  if (opts.priority !== void 0) body["priority"] = opts.priority;
1241
1743
  if (opts.status !== void 0) body["status"] = opts.status;
1242
1744
  const data = await client.post(`/folder/${opts.folderId}/list`, body);
@@ -1254,7 +1756,7 @@ function registerListCommands(program, getClient) {
1254
1756
  const body = {};
1255
1757
  if (opts.name !== void 0) body["name"] = opts.name;
1256
1758
  if (opts.content !== void 0) body["content"] = opts.content;
1257
- 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");
1258
1760
  if (opts.unsetStatus) body["unset_status"] = true;
1259
1761
  const data = await client.put(`/list/${listId}`, body);
1260
1762
  formatOutput(data, LIST_COLUMNS, getOutputOptions(program));
@@ -1294,6 +1796,116 @@ function registerListCommands(program, getClient) {
1294
1796
 
1295
1797
  // src/commands/task.ts
1296
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
1297
1909
  registerSchema("task", "list", "List tasks in a list", [
1298
1910
  { flag: "--list-id", type: "string", required: true, description: "List ID" },
1299
1911
  { flag: "--archived", type: "boolean", required: false, description: "Include archived tasks" },
@@ -1377,6 +1989,26 @@ function requireWorkspaceId3(program) {
1377
1989
  function collect(value, previous) {
1378
1990
  return previous.concat([value]);
1379
1991
  }
1992
+ var PRIORITY_MAP = {
1993
+ urgent: 1,
1994
+ high: 2,
1995
+ normal: 3,
1996
+ low: 4
1997
+ };
1998
+ function parsePriority(value) {
1999
+ const lower = value.toLowerCase();
2000
+ if (PRIORITY_MAP[lower] !== void 0) return PRIORITY_MAP[lower];
2001
+ const num = parseInt(value, 10);
2002
+ if (!isNaN(num) && num >= 1 && num <= 4) return num;
2003
+ fail("--priority must be 1-4 or urgent/high/normal/low");
2004
+ }
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");
2010
+ }
2011
+ }
1380
2012
  function buildTaskListParams(opts) {
1381
2013
  const params = {};
1382
2014
  if (opts.archived) params["archived"] = "true";
@@ -1398,20 +2030,18 @@ function buildTaskListParams(opts) {
1398
2030
  const tags = opts.tag;
1399
2031
  if (tags?.length) params["tags[]"] = tags;
1400
2032
  const customFields = opts.customField;
1401
- if (customFields?.length) {
1402
- for (const cf of customFields) params["custom_fields"] = JSON.stringify(customFields.map((f) => JSON.parse(f)));
1403
- }
2033
+ if (customFields?.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
1404
2034
  return params;
1405
2035
  }
1406
2036
  function registerTaskCommands(program, getClient) {
1407
2037
  const task = program.command("task").description("Manage tasks");
1408
- 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) => {
1409
2039
  const client = getClient();
1410
2040
  const params = buildTaskListParams(opts);
1411
2041
  const data = await client.get(`/list/${opts.listId}/task`, params);
1412
2042
  formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
1413
2043
  });
1414
- 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) => {
1415
2045
  const workspaceId = requireWorkspaceId3(program);
1416
2046
  if (!workspaceId) return;
1417
2047
  const client = getClient();
@@ -1443,7 +2073,7 @@ function registerTaskCommands(program, getClient) {
1443
2073
  const projectIds = opts.projectId;
1444
2074
  if (projectIds.length) params["project_ids[]"] = projectIds;
1445
2075
  const customFields = opts.customField;
1446
- if (customFields.length) params["custom_fields"] = JSON.stringify(customFields.map((f) => JSON.parse(f)));
2076
+ if (customFields.length) params["custom_fields"] = parseCustomFieldFilters(customFields);
1447
2077
  const data = await client.get(`/team/${workspaceId}/task`, params);
1448
2078
  formatOutput(data.tasks, TASK_COLUMNS, getOutputOptions(program));
1449
2079
  });
@@ -1455,17 +2085,17 @@ function registerTaskCommands(program, getClient) {
1455
2085
  const data = await client.get(`/task/${taskId}`, params);
1456
2086
  formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
1457
2087
  });
1458
- 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=urgent, 2=high, 3=normal, 4=low)", parseInt).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) => {
1459
2089
  const client = getClient();
1460
2090
  const body = { name: opts.name };
1461
2091
  if (opts.markdownDescription !== void 0) body["markdown_description"] = opts.markdownDescription;
1462
2092
  else if (opts.description !== void 0) body["description"] = opts.description;
1463
2093
  if (opts.status !== void 0) body["status"] = opts.status;
1464
- if (opts.priority !== void 0) body["priority"] = opts.priority;
1465
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1466
- if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
2094
+ if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
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");
1467
2097
  const assignees = opts.assignee;
1468
- if (assignees.length) body["assignees"] = assignees.map((a) => parseInt(a, 10));
2098
+ if (assignees.length) body["assignees"] = assignees.map((a) => parseIntStrict(a, "--assignee"));
1469
2099
  const tags = opts.tag;
1470
2100
  if (tags.length) body["tags"] = tags;
1471
2101
  if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
@@ -1476,7 +2106,7 @@ function registerTaskCommands(program, getClient) {
1476
2106
  if (customFields.length) {
1477
2107
  body["custom_fields"] = customFields.map((cf) => {
1478
2108
  const eqIdx = cf.indexOf("=");
1479
- 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>`);
1480
2110
  const id = cf.slice(0, eqIdx);
1481
2111
  let value = cf.slice(eqIdx + 1);
1482
2112
  try {
@@ -1490,22 +2120,22 @@ function registerTaskCommands(program, getClient) {
1490
2120
  const data = await client.post(`/list/${opts.listId}/task`, body);
1491
2121
  formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
1492
2122
  });
1493
- 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)", parseInt).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) => {
1494
2124
  const client = getClient();
1495
2125
  const body = {};
1496
2126
  if (opts.name !== void 0) body["name"] = opts.name;
1497
2127
  if (opts.description !== void 0) body["description"] = opts.description;
1498
2128
  if (opts.status !== void 0) body["status"] = opts.status;
1499
- if (opts.priority !== void 0) body["priority"] = opts.priority;
1500
- if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
1501
- if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
2129
+ if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
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");
1502
2132
  if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
1503
2133
  const addIds = opts.assigneeAdd;
1504
2134
  const remIds = opts.assigneeRemove;
1505
2135
  if (addIds.length || remIds.length) {
1506
2136
  body["assignees"] = {
1507
- add: addIds.map((a) => parseInt(a, 10)),
1508
- 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"))
1509
2139
  };
1510
2140
  }
1511
2141
  if (opts.archived !== void 0) body["archived"] = opts.archived === "true";
@@ -1538,21 +2168,7 @@ function registerTaskCommands(program, getClient) {
1538
2168
  if (data.current_status) entries.push(data.current_status);
1539
2169
  formatOutput(entries, TIME_IN_STATUS_COLUMNS, getOutputOptions(program));
1540
2170
  });
1541
- 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) => {
1542
- const client = getClient();
1543
- const taskIds = opts.taskId;
1544
- const results = [];
1545
- for (const id of taskIds) {
1546
- const data = await client.get(`/task/${id}/time_in_status`);
1547
- const entries = data.status_history ?? [];
1548
- if (data.current_status) entries.push(data.current_status);
1549
- results.push({ task_id: id, statuses: entries });
1550
- }
1551
- formatOutput(results, [
1552
- { key: "task_id", header: "Task ID", width: 14 },
1553
- { key: "statuses", header: "Statuses", width: 50 }
1554
- ], getOutputOptions(program));
1555
- });
2171
+ registerTaskBulkCommands(task, program, getClient);
1556
2172
  }
1557
2173
 
1558
2174
  // src/commands/checklist.ts
@@ -1600,7 +2216,7 @@ function registerChecklistCommands(program, getClient) {
1600
2216
  const data = await client.post(`/task/${opts.taskId}/checklist`, { name: opts.name });
1601
2217
  formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
1602
2218
  });
1603
- 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) => {
1604
2220
  const client = getClient();
1605
2221
  const body = {};
1606
2222
  if (opts.name !== void 0) body["name"] = opts.name;
@@ -1630,7 +2246,7 @@ function registerChecklistCommands(program, getClient) {
1630
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) => {
1631
2247
  const client = getClient();
1632
2248
  const body = { name: opts.name };
1633
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2249
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
1634
2250
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
1635
2251
  if (opts.parent !== void 0) body["parent"] = opts.parent;
1636
2252
  const data = await client.post(`/checklist/${checklistId}/checklist_item`, body);
@@ -1641,7 +2257,7 @@ function registerChecklistCommands(program, getClient) {
1641
2257
  const body = {};
1642
2258
  if (opts.name !== void 0) body["name"] = opts.name;
1643
2259
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
1644
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2260
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
1645
2261
  if (opts.parent !== void 0) body["parent"] = opts.parent;
1646
2262
  const data = await client.put(`/checklist/${checklistId}/checklist_item/${opts.itemId}`, body);
1647
2263
  formatOutput(data.checklist, CHECKLIST_COLUMNS, getOutputOptions(program));
@@ -1668,6 +2284,7 @@ function registerChecklistCommands(program, getClient) {
1668
2284
  }
1669
2285
 
1670
2286
  // src/commands/custom-field.ts
2287
+ init_config();
1671
2288
  registerSchema("field", "list", "List custom fields for a list, folder, space, or workspace", [
1672
2289
  { flag: "--list-id", type: "string", required: false, description: "List ID" },
1673
2290
  { flag: "--folder-id", type: "string", required: false, description: "Folder ID" },
@@ -1681,7 +2298,8 @@ registerSchema("field", "set", "Set a custom field value on a task", [
1681
2298
  ]);
1682
2299
  registerSchema("field", "remove", "Remove a custom field value from a task", [
1683
2300
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
1684
- { 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" }
1685
2303
  ]);
1686
2304
  var FIELD_COLUMNS = [
1687
2305
  { key: "id", header: "ID", width: 20 },
@@ -1694,7 +2312,7 @@ function registerFieldCommands(program, getClient) {
1694
2312
  const field = program.command("field").description("Manage custom fields");
1695
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) => {
1696
2314
  const client = getClient();
1697
- const globalWorkspaceId = program.opts()["workspaceId"];
2315
+ const globalWorkspaceId = resolveWorkspaceId(program.opts()["workspaceId"]);
1698
2316
  let endpoint;
1699
2317
  if (opts.listId) {
1700
2318
  endpoint = `/list/${opts.listId}/field`;
@@ -1722,8 +2340,21 @@ function registerFieldCommands(program, getClient) {
1722
2340
  const data = await client.post(`/task/${opts.taskId}/field/${opts.fieldId}`, { value: parsedValue });
1723
2341
  formatOutput(data, FIELD_COLUMNS, getOutputOptions(program));
1724
2342
  });
1725
- 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) => {
1726
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
+ }
1727
2358
  await client.delete(`/task/${opts.taskId}/field/${opts.fieldId}`);
1728
2359
  process.stdout.write(`Removed field ${opts.fieldId} from task ${opts.taskId}
1729
2360
  `);
@@ -1900,11 +2531,22 @@ function registerRelationCommands(program, getClient) {
1900
2531
  }
1901
2532
 
1902
2533
  // src/commands/attachment.ts
2534
+ import { writeFileSync, existsSync as existsSync2 } from "fs";
2535
+ import { join, basename as basename2, resolve } from "path";
1903
2536
  registerSchema("attachment", "upload", "Upload a file to a task", [
1904
2537
  { flag: "--task-id", type: "string", required: true, description: "Task ID" },
1905
2538
  { flag: "--file", type: "string", required: true, description: "Local file path" },
1906
2539
  { flag: "--filename", type: "string", required: false, description: "Override display filename" }
1907
2540
  ]);
2541
+ registerSchema("attachment", "list", "List attachments on a task", [
2542
+ { flag: "--task-id", type: "string", required: true, description: "Task ID" }
2543
+ ]);
2544
+ registerSchema("attachment", "download", "Download an attachment from a task", [
2545
+ { flag: "--task-id", type: "string", required: true, description: "Task ID" },
2546
+ { flag: "--attachment-id", type: "string", required: true, description: "Attachment ID" },
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" }
2549
+ ]);
1908
2550
  var ATTACHMENT_COLUMNS = [
1909
2551
  { key: "id", header: "ID", width: 20 },
1910
2552
  { key: "title", header: "Title", width: 30 },
@@ -1919,6 +2561,42 @@ function registerAttachmentCommands(program, getClient) {
1919
2561
  const data = await client.upload(`/task/${opts.taskId}/attachment`, opts.file, opts.filename);
1920
2562
  formatOutput(data, ATTACHMENT_COLUMNS, getOutputOptions(program));
1921
2563
  });
2564
+ attachment.command("list").description("List attachments on a task").requiredOption("--task-id <id>", "Task ID").action(async (opts) => {
2565
+ const client = getClient();
2566
+ const data = await client.get(`/task/${opts.taskId}`);
2567
+ const attachments = data["attachments"] ?? [];
2568
+ formatOutput(attachments, ATTACHMENT_COLUMNS, getOutputOptions(program));
2569
+ });
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) => {
2571
+ const { default: ora } = await import("ora");
2572
+ const client = getClient();
2573
+ const data = await client.get(`/task/${opts.taskId}`);
2574
+ const attachments = data["attachments"] ?? [];
2575
+ const attachment2 = attachments.find((a) => a.id === opts.attachmentId);
2576
+ if (!attachment2) {
2577
+ process.stderr.write(`Error: Attachment "${opts.attachmentId}" not found on task "${opts.taskId}".
2578
+ `);
2579
+ process.exit(4);
2580
+ return;
2581
+ }
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();
2591
+ try {
2592
+ const buffer = await client.downloadUrl(attachment2.url);
2593
+ writeFileSync(outputPath, Buffer.from(buffer));
2594
+ spinner.succeed(`Downloaded to ${outputPath}`);
2595
+ } catch (err) {
2596
+ spinner.fail("Download failed");
2597
+ throw err;
2598
+ }
2599
+ });
1922
2600
  }
1923
2601
 
1924
2602
  // src/commands/comment.ts
@@ -2005,7 +2683,7 @@ function registerCommentCommands(program, getClient) {
2005
2683
  if (!parent) return;
2006
2684
  const client = getClient();
2007
2685
  const body = { comment_text: opts.text };
2008
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2686
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2009
2687
  if (opts.notifyAll) body["notify_all"] = true;
2010
2688
  const data = await client.post(`/${parent.type}/${parent.id}/comment`, body);
2011
2689
  process.stdout.write(`Created comment ${data["id"] ?? ""}
@@ -2014,7 +2692,7 @@ function registerCommentCommands(program, getClient) {
2014
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) => {
2015
2693
  const client = getClient();
2016
2694
  const body = { comment_text: opts.text };
2017
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2695
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2018
2696
  if (opts.resolved !== void 0) body["resolved"] = opts.resolved === "true";
2019
2697
  await client.put(`/comment/${commentId}`, body);
2020
2698
  process.stdout.write(`Updated comment ${commentId}
@@ -2047,7 +2725,7 @@ function registerCommentCommands(program, getClient) {
2047
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) => {
2048
2726
  const client = getClient();
2049
2727
  const body = { comment_text: opts.text };
2050
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2728
+ if (opts.assignee !== void 0) body["assignee"] = parseIntStrict(opts.assignee, "--assignee");
2051
2729
  if (opts.notifyAll) body["notify_all"] = true;
2052
2730
  const data = await client.post(`/comment/${commentId}/thread`, body);
2053
2731
  process.stdout.write(`Created reply ${data["id"] ?? ""}
@@ -2057,85 +2735,6 @@ function registerCommentCommands(program, getClient) {
2057
2735
 
2058
2736
  // src/commands/time-tracking.ts
2059
2737
  init_config();
2060
-
2061
- // src/dates.ts
2062
- var RELATIVE_OFFSET_RE = /^([+-]\d+)([dwmh])$/;
2063
- var UNIX_MS_RE = /^\d{13,}$/;
2064
- var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
2065
- var ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
2066
- var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
2067
- function parseDate(input) {
2068
- const raw = input.trim();
2069
- const lower = raw.toLowerCase();
2070
- if (UNIX_MS_RE.test(raw)) {
2071
- return parseInt(raw, 10);
2072
- }
2073
- const now = /* @__PURE__ */ new Date();
2074
- switch (lower) {
2075
- case "today": {
2076
- return startOfDay(now).getTime();
2077
- }
2078
- case "tomorrow": {
2079
- const d = startOfDay(now);
2080
- d.setDate(d.getDate() + 1);
2081
- return d.getTime();
2082
- }
2083
- case "yesterday": {
2084
- const d = startOfDay(now);
2085
- d.setDate(d.getDate() - 1);
2086
- return d.getTime();
2087
- }
2088
- }
2089
- const offsetMatch = RELATIVE_OFFSET_RE.exec(lower);
2090
- if (offsetMatch) {
2091
- const amount = parseInt(offsetMatch[1], 10);
2092
- const unit = offsetMatch[2];
2093
- const ms = unitToMs(unit, amount);
2094
- return now.getTime() + ms;
2095
- }
2096
- if (lower.startsWith("next ")) {
2097
- const dayName = lower.slice(5);
2098
- const dayIndex = DAY_NAMES.indexOf(dayName);
2099
- if (dayIndex !== -1) {
2100
- const today = now.getDay();
2101
- let daysAhead = dayIndex - today;
2102
- if (daysAhead <= 0) daysAhead += 7;
2103
- const target = startOfDay(now);
2104
- target.setDate(target.getDate() + daysAhead);
2105
- return target.getTime();
2106
- }
2107
- }
2108
- if (ISO_DATE_RE.test(raw)) {
2109
- const d = /* @__PURE__ */ new Date(raw + "T00:00:00");
2110
- if (!isNaN(d.getTime())) return d.getTime();
2111
- }
2112
- if (ISO_DATETIME_RE.test(raw)) {
2113
- const d = new Date(raw);
2114
- if (!isNaN(d.getTime())) return d.getTime();
2115
- }
2116
- throw new Error(`Unable to parse date: "${input}"`);
2117
- }
2118
- function startOfDay(date) {
2119
- const d = new Date(date);
2120
- d.setHours(0, 0, 0, 0);
2121
- return d;
2122
- }
2123
- function unitToMs(unit, amount) {
2124
- switch (unit) {
2125
- case "m":
2126
- return amount * 60 * 1e3;
2127
- case "h":
2128
- return amount * 60 * 60 * 1e3;
2129
- case "d":
2130
- return amount * 24 * 60 * 60 * 1e3;
2131
- case "w":
2132
- return amount * 7 * 24 * 60 * 60 * 1e3;
2133
- default:
2134
- return 0;
2135
- }
2136
- }
2137
-
2138
- // src/commands/time-tracking.ts
2139
2738
  registerSchema("time", "list", "List time entries for a task or workspace", [
2140
2739
  { flag: "--task-id", type: "string", required: false, description: "Task ID (or use --workspace-id for workspace-wide)" },
2141
2740
  { flag: "--workspace-id", type: "string", required: false, description: "Workspace ID" },
@@ -2166,7 +2765,8 @@ registerSchema("time", "update", "Update a time entry", [
2166
2765
  ]);
2167
2766
  registerSchema("time", "delete", "Delete a time entry", [
2168
2767
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
2169
- { 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" }
2170
2770
  ]);
2171
2771
  registerSchema("time", "running", "Get current running timer", [
2172
2772
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
@@ -2254,12 +2854,12 @@ function registerTimeTrackingCommands(program, getClient) {
2254
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) => {
2255
2855
  const client = getClient();
2256
2856
  const body = {
2257
- duration: parseInt(opts.duration, 10),
2857
+ duration: parseIntStrict(opts.duration, "--duration"),
2258
2858
  start: String(parseDate(opts.start))
2259
2859
  };
2260
2860
  if (opts.description !== void 0) body["description"] = opts.description;
2261
- if (opts.assignee !== void 0) body["assignee"] = parseInt(opts.assignee, 10);
2262
- 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");
2263
2863
  if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
2264
2864
  const data = await client.post(`/task/${opts.taskId}/time`, body);
2265
2865
  process.stdout.write(`Created time entry ${data.data?.id ?? ""}
@@ -2271,9 +2871,9 @@ function registerTimeTrackingCommands(program, getClient) {
2271
2871
  const client = getClient();
2272
2872
  const body = {};
2273
2873
  if (opts.description !== void 0) body["description"] = opts.description;
2274
- if (opts.duration !== void 0) body["duration"] = parseInt(opts.duration, 10);
2874
+ if (opts.duration !== void 0) body["duration"] = parseIntStrict(opts.duration, "--duration");
2275
2875
  if (opts.start !== void 0) body["start"] = String(parseDate(opts.start));
2276
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2876
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2277
2877
  if (opts.tag.length) {
2278
2878
  body["tags"] = opts.tag.map((t) => ({ name: t }));
2279
2879
  if (opts.tagAction) body["tag_action"] = opts.tagAction;
@@ -2282,10 +2882,23 @@ function registerTimeTrackingCommands(program, getClient) {
2282
2882
  process.stdout.write(`Updated time entry ${timerId}
2283
2883
  `);
2284
2884
  });
2285
- 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) => {
2286
2886
  const workspaceId = requireWorkspaceId4(program);
2287
2887
  if (!workspaceId) return;
2288
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
+ }
2289
2902
  await client.delete(`/team/${workspaceId}/time_entries/${timerId}`);
2290
2903
  process.stdout.write(`Deleted time entry ${timerId}
2291
2904
  `);
@@ -2316,7 +2929,7 @@ function registerTimeTrackingCommands(program, getClient) {
2316
2929
  const client = getClient();
2317
2930
  const body = { tid: opts.taskId };
2318
2931
  if (opts.description !== void 0) body["description"] = opts.description;
2319
- if (opts.billable !== void 0) body["billable"] = opts.billable === "true";
2932
+ if (opts.billable !== void 0) body["billable"] = parseBoolStrict(opts.billable, "--billable");
2320
2933
  if (opts.tag.length) body["tags"] = opts.tag.map((t) => ({ name: t }));
2321
2934
  const data = await client.post(`/team/${workspaceId}/time_entries/start`, body);
2322
2935
  process.stdout.write(`Started timer ${data.data?.id ?? ""}
@@ -2481,7 +3094,7 @@ function registerGoalCommands(program, getClient) {
2481
3094
  if (opts.dueDate !== void 0) body["due_date"] = opts.dueDate;
2482
3095
  if (opts.description !== void 0) body["description"] = opts.description;
2483
3096
  if (opts.multipleOwners) body["multiple_owners"] = true;
2484
- 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"));
2485
3098
  if (opts.color !== void 0) body["color"] = opts.color;
2486
3099
  const data = await client.post(`/team/${workspaceId}/goal`, body);
2487
3100
  process.stdout.write(`Created goal ${data.goal?.id ?? ""}
@@ -2520,8 +3133,8 @@ function registerGoalCommands(program, getClient) {
2520
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) => {
2521
3134
  const client = getClient();
2522
3135
  const body = { name: opts.name, type: opts.type };
2523
- if (opts.stepsStart !== void 0) body["steps_start"] = parseFloat(opts.stepsStart);
2524
- 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");
2525
3138
  if (opts.unit !== void 0) body["unit"] = opts.unit;
2526
3139
  if (opts.taskIds.length) body["task_ids"] = opts.taskIds;
2527
3140
  if (opts.listIds.length) body["list_ids"] = opts.listIds;
@@ -2533,7 +3146,7 @@ function registerGoalCommands(program, getClient) {
2533
3146
  const client = getClient();
2534
3147
  const body = {};
2535
3148
  if (opts.name !== void 0) body["name"] = opts.name;
2536
- 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");
2537
3150
  if (opts.note !== void 0) body["note"] = opts.note;
2538
3151
  await client.put(`/key_result/${keyResultId}`, body);
2539
3152
  process.stdout.write(`Updated key result ${keyResultId}
@@ -2561,6 +3174,7 @@ function registerGoalCommands(program, getClient) {
2561
3174
  }
2562
3175
 
2563
3176
  // src/commands/view.ts
3177
+ init_config();
2564
3178
  registerSchema("view", "list", "List views for a workspace, space, folder, or list", [
2565
3179
  { flag: "--workspace-id", type: "string", required: false, description: "Workspace ID (provide one parent)" },
2566
3180
  { flag: "--space-id", type: "string", required: false, description: "Space ID (provide one parent)" },
@@ -2613,6 +3227,10 @@ function resolveViewParent(opts, program) {
2613
3227
  if (opts.spaceId) parents.push({ segment: "space", id: opts.spaceId });
2614
3228
  if (opts.folderId) parents.push({ segment: "folder", id: opts.folderId });
2615
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
+ }
2616
3234
  if (parents.length === 0) {
2617
3235
  process.stderr.write("Error: Provide one of --workspace-id, --space-id, --folder-id, or --list-id.\n");
2618
3236
  process.exit(2);
@@ -2816,6 +3434,11 @@ function registerWebhookCommands(program, getClient) {
2816
3434
  `);
2817
3435
  });
2818
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
+ }
2819
3442
  const client = getClient();
2820
3443
  const body = {};
2821
3444
  if (opts.endpoint !== void 0) body["endpoint"] = opts.endpoint;
@@ -2917,7 +3540,7 @@ function registerUserCommands(program, getClient) {
2917
3540
  const body = {};
2918
3541
  if (opts.username !== void 0) body["username"] = opts.username;
2919
3542
  if (opts.admin !== void 0) body["admin"] = opts.admin === "true";
2920
- 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");
2921
3544
  await client.put(`/team/${workspaceId}/user/${userId}`, body);
2922
3545
  process.stdout.write(`Updated user ${userId}
2923
3546
  `);
@@ -3006,7 +3629,7 @@ function registerGroupCommands(program, getClient) {
3006
3629
  const client = getClient();
3007
3630
  const body = { name: opts.name };
3008
3631
  if (opts.memberId.length) {
3009
- body["members"] = opts.memberId.map((id) => ({ id: parseInt(id, 10) }));
3632
+ body["members"] = opts.memberId.map((id) => ({ id: parseIntStrict(id, "--member-id") }));
3010
3633
  }
3011
3634
  const data = await client.post(`/team/${workspaceId}/group`, body);
3012
3635
  process.stdout.write(`Created group ${data["id"] ?? ""}
@@ -3017,8 +3640,8 @@ function registerGroupCommands(program, getClient) {
3017
3640
  const body = {};
3018
3641
  if (opts.name !== void 0) body["name"] = opts.name;
3019
3642
  const members = {};
3020
- if (opts.addMember.length) members["add"] = opts.addMember.map((id) => ({ id: parseInt(id, 10) }));
3021
- 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") }));
3022
3645
  if (Object.keys(members).length) body["members"] = members;
3023
3646
  await client.put(`/group/${groupId}`, body);
3024
3647
  process.stdout.write(`Updated group ${groupId}
@@ -3069,6 +3692,15 @@ function parseBool(val) {
3069
3692
  if (val === void 0) return void 0;
3070
3693
  return val === "true";
3071
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
+ }
3072
3704
  registerSchema("guest", "invite", "Invite a guest to a workspace", [
3073
3705
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
3074
3706
  { flag: "--email", type: "string", required: true, description: "Email address to invite" },
@@ -3183,7 +3815,7 @@ function registerGuestCommands(program, getClient) {
3183
3815
  });
3184
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) => {
3185
3817
  const client = getClient();
3186
- 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) });
3187
3819
  process.stdout.write(`Added guest ${guestId} to task ${opts.taskId}
3188
3820
  `);
3189
3821
  });
@@ -3195,7 +3827,7 @@ function registerGuestCommands(program, getClient) {
3195
3827
  });
3196
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) => {
3197
3829
  const client = getClient();
3198
- 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) });
3199
3831
  process.stdout.write(`Added guest ${guestId} to list ${opts.listId}
3200
3832
  `);
3201
3833
  });
@@ -3207,7 +3839,7 @@ function registerGuestCommands(program, getClient) {
3207
3839
  });
3208
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) => {
3209
3841
  const client = getClient();
3210
- 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) });
3211
3843
  process.stdout.write(`Added guest ${guestId} to folder ${opts.folderId}
3212
3844
  `);
3213
3845
  });
@@ -3373,9 +4005,25 @@ registerSchema("template", "list", "List task templates in a workspace", [
3373
4005
  { flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
3374
4006
  { flag: "--page", type: "integer", required: false, description: "Page number (starts at 0)" }
3375
4007
  ]);
4008
+ registerSchema("template", "apply-task", "Create a task from a task template", [
4009
+ { flag: "--list-id", type: "string", required: true, description: "List to create the task in" },
4010
+ { flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
4011
+ { flag: "--name", type: "string", required: false, description: "Override the template name" }
4012
+ ]);
4013
+ registerSchema("template", "apply-list", "Create a list from a list template", [
4014
+ { flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
4015
+ { flag: "--folder-id", type: "string", required: false, description: "Folder to create the list in (use this or --space-id)" },
4016
+ { flag: "--space-id", type: "string", required: false, description: "Space to create the list in (use this or --folder-id)" },
4017
+ { flag: "--name", type: "string", required: false, description: "Override the template name" }
4018
+ ]);
4019
+ registerSchema("template", "apply-folder", "Create a folder from a folder template", [
4020
+ { flag: "--space-id", type: "string", required: true, description: "Space to create the folder in" },
4021
+ { flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
4022
+ { flag: "--name", type: "string", required: false, description: "Override the template name" }
4023
+ ]);
3376
4024
  function registerTemplateCommands(program, getClient) {
3377
4025
  const template = program.command("template").description("Manage task templates");
3378
- 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) => {
3379
4027
  const workspaceId = requireWorkspaceId11(program);
3380
4028
  if (!workspaceId) return;
3381
4029
  const client = getClient();
@@ -3384,6 +4032,39 @@ function registerTemplateCommands(program, getClient) {
3384
4032
  const data = await client.get(`/team/${workspaceId}/taskTemplate`, params);
3385
4033
  formatOutput(data.templates, TEMPLATE_COLUMNS, getOutputOptions(program));
3386
4034
  });
4035
+ template.command("apply-task").description("Create a task from a task template").requiredOption("--list-id <id>", "List to create the task in").requiredOption("--template-id <id>", "Template ID to apply").option("--name <name>", "Override the template name").action(async (opts) => {
4036
+ const client = getClient();
4037
+ const body = {};
4038
+ if (opts.name !== void 0) body["name"] = opts.name;
4039
+ const data = await client.post(
4040
+ `/list/${opts.listId}/taskTemplate/${opts.templateId}`,
4041
+ body
4042
+ );
4043
+ formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
4044
+ });
4045
+ template.command("apply-list").description("Create a list from a list template").requiredOption("--template-id <id>", "Template ID to apply").option("--folder-id <id>", "Folder to create the list in").option("--space-id <id>", "Space to create the list in (folderless)").option("--name <name>", "Override the template name").action(async (opts) => {
4046
+ if (!opts.folderId && !opts.spaceId) {
4047
+ process.stderr.write("Error: Provide either --folder-id or --space-id\n");
4048
+ process.exit(2);
4049
+ return;
4050
+ }
4051
+ const client = getClient();
4052
+ const body = {};
4053
+ if (opts.name !== void 0) body["name"] = opts.name;
4054
+ const path = opts.folderId ? `/folder/${opts.folderId}/listTemplate/${opts.templateId}` : `/space/${opts.spaceId}/listTemplate/${opts.templateId}`;
4055
+ const data = await client.post(path, body);
4056
+ formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
4057
+ });
4058
+ template.command("apply-folder").description("Create a folder from a folder template").requiredOption("--space-id <id>", "Space to create the folder in").requiredOption("--template-id <id>", "Template ID to apply").option("--name <name>", "Override the template name").action(async (opts) => {
4059
+ const client = getClient();
4060
+ const body = {};
4061
+ if (opts.name !== void 0) body["name"] = opts.name;
4062
+ const data = await client.post(
4063
+ `/space/${opts.spaceId}/folderTemplate/${opts.templateId}`,
4064
+ body
4065
+ );
4066
+ formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
4067
+ });
3387
4068
  }
3388
4069
 
3389
4070
  // src/commands/task-type.ts
@@ -3631,7 +4312,7 @@ function registerDocCommands(program, getClient) {
3631
4312
  const data = await client.get(`/v3/workspaces/${workspaceId}/docs/${opts.docId}`);
3632
4313
  formatOutput(data, DOC_COLUMNS, getOutputOptions(program));
3633
4314
  });
3634
- 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) => {
3635
4316
  const workspaceId = requireWorkspaceId14(program);
3636
4317
  if (!workspaceId) return;
3637
4318
  const client = getClient();
@@ -3729,8 +4410,8 @@ function registerDocCommands(program, getClient) {
3729
4410
  }
3730
4411
 
3731
4412
  // src/commands/skill-cmd.ts
3732
- import { readdirSync, readFileSync as readFileSync2, existsSync } from "fs";
3733
- import { join, dirname } from "path";
4413
+ import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
4414
+ import { join as join2, dirname } from "path";
3734
4415
  import { fileURLToPath } from "url";
3735
4416
  var SKILL_COLUMNS = [
3736
4417
  { key: "name", header: "Name", width: 28 },
@@ -3746,10 +4427,10 @@ registerSchema("skill", "path", "Print the file system path to a skill directory
3746
4427
  ]);
3747
4428
  function findSkillsDir() {
3748
4429
  const thisDir = dirname(fileURLToPath(import.meta.url));
3749
- const bundledDir = join(thisDir, "..", "skills");
3750
- if (existsSync(bundledDir)) return bundledDir;
3751
- const projectDir = join(thisDir, "..", "..", "skills");
3752
- if (existsSync(projectDir)) return projectDir;
4430
+ const bundledDir = join2(thisDir, "..", "skills");
4431
+ if (existsSync3(bundledDir)) return bundledDir;
4432
+ const projectDir = join2(thisDir, "..", "..", "skills");
4433
+ if (existsSync3(projectDir)) return projectDir;
3753
4434
  return void 0;
3754
4435
  }
3755
4436
  function parseFrontmatter(content) {
@@ -3789,16 +4470,16 @@ function loadSkills(skillsDir) {
3789
4470
  return skills;
3790
4471
  }
3791
4472
  for (const entry of entries) {
3792
- const skillFile = join(skillsDir, entry, "SKILL.md");
3793
- if (!existsSync(skillFile)) continue;
4473
+ const skillFile = join2(skillsDir, entry, "SKILL.md");
4474
+ if (!existsSync3(skillFile)) continue;
3794
4475
  try {
3795
- const content = readFileSync2(skillFile, "utf-8");
4476
+ const content = readFileSync4(skillFile, "utf-8");
3796
4477
  const { frontmatter } = parseFrontmatter(content);
3797
4478
  skills.push({
3798
4479
  name: frontmatter["name"] || entry,
3799
4480
  description: (frontmatter["description"] || "").slice(0, 100),
3800
4481
  type: classifySkill(frontmatter),
3801
- path: join(skillsDir, entry),
4482
+ path: join2(skillsDir, entry),
3802
4483
  frontmatter
3803
4484
  });
3804
4485
  } catch {
@@ -3813,11 +4494,11 @@ function loadSkills(skillsDir) {
3813
4494
  });
3814
4495
  }
3815
4496
  function findSkill(skillsDir, name) {
3816
- const skillDir = join(skillsDir, name);
3817
- const skillFile = join(skillDir, "SKILL.md");
3818
- if (!existsSync(skillFile)) return void 0;
4497
+ const skillDir = join2(skillsDir, name);
4498
+ const skillFile = join2(skillDir, "SKILL.md");
4499
+ if (!existsSync3(skillFile)) return void 0;
3819
4500
  try {
3820
- const content = readFileSync2(skillFile, "utf-8");
4501
+ const content = readFileSync4(skillFile, "utf-8");
3821
4502
  return { skillDir, content };
3822
4503
  } catch {
3823
4504
  return void 0;
@@ -3945,15 +4626,20 @@ function registerChatCommands(program, getClient) {
3945
4626
  }
3946
4627
 
3947
4628
  // src/cli.ts
3948
- var VERSION = "0.2.0";
4629
+ var VERSION = "0.4.0";
3949
4630
  function createProgram() {
3950
4631
  const program = new Command();
3951
- program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").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");
3952
4635
  return program;
3953
4636
  }
3954
4637
  function createClient(program) {
3955
4638
  const globalOpts = program.opts();
3956
- const token = resolveToken(globalOpts["token"]);
4639
+ const token = resolveToken(
4640
+ globalOpts["token"],
4641
+ globalOpts["tokenFile"]
4642
+ );
3957
4643
  if (!token) {
3958
4644
  process.stderr.write("Error: No API token found. Run: clickup auth login\n");
3959
4645
  process.exit(EXIT_CODES.AUTH_FAILURE);
@@ -3982,9 +4668,15 @@ function getOutputOptions(program) {
3982
4668
  return result;
3983
4669
  }
3984
4670
  function run() {
4671
+ process.stdout.on("error", (err) => {
4672
+ if (err.code === "EPIPE") process.exit(EXIT_CODES.SUCCESS);
4673
+ });
3985
4674
  const program = createProgram();
4675
+ program.hook("preAction", () => {
4676
+ setProfileOverride(program.opts()["profile"]);
4677
+ });
3986
4678
  registerAuthCommands(program, () => createClient(program));
3987
- registerConfigCommands(program);
4679
+ registerConfigCommands(program, () => createClient(program));
3988
4680
  registerWorkspaceCommands(program, () => createClient(program));
3989
4681
  registerSpaceCommands(program, () => createClient(program));
3990
4682
  registerFolderCommands(program, () => createClient(program));
@@ -4014,6 +4706,9 @@ function run() {
4014
4706
  registerSchemaCommands(program);
4015
4707
  registerSkillCommands(program);
4016
4708
  program.parseAsync(process.argv).catch((error) => {
4709
+ if (error instanceof DryRunComplete) {
4710
+ process.exit(EXIT_CODES.SUCCESS);
4711
+ }
4017
4712
  if (error instanceof ClickUpError) {
4018
4713
  process.stderr.write(`Error: ${error.message}
4019
4714
  `);