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