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