clickup-agent-cli 0.2.1 → 0.3.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 +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/dist/clickup.js +570 -54
- package/package.json +3 -2
- package/skills/clickup/SKILL.md +38 -3
- package/skills/clickup-fields/SKILL.md +12 -1
- package/skills/clickup-rollout/SKILL.md +111 -0
- package/skills/clickup-spaces/SKILL.md +3 -0
- package/skills/clickup-tasks/SKILL.md +15 -2
- package/skills/clickup-templates/SKILL.md +76 -0
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
{
|
|
11
11
|
"name": "clickup",
|
|
12
12
|
"source": "./",
|
|
13
|
-
"description": "ClickUp CLI with
|
|
14
|
-
"version": "0.
|
|
13
|
+
"description": "ClickUp CLI with 25 agent skills covering the full API -- token-efficient alternative to MCP with chat, time tracking, docs, and project management workflows",
|
|
14
|
+
"version": "0.3.0",
|
|
15
15
|
"homepage": "https://github.com/henryreith/clickup-cli",
|
|
16
16
|
"keywords": ["clickup", "project-management", "tasks", "time-tracking"],
|
|
17
17
|
"category": "productivity"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clickup",
|
|
3
|
-
"description": "ClickUp CLI with
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "ClickUp CLI with 25 agent skills covering the full API -- token-efficient alternative to MCP with chat, time tracking, docs, and project management workflows",
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Henry Reith"
|
|
7
7
|
},
|
package/dist/clickup.js
CHANGED
|
@@ -47,7 +47,7 @@ function mapToExitCode(error) {
|
|
|
47
47
|
return 1;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES;
|
|
50
|
+
var ClickUpError, ECODE_MESSAGES, STATUS_MESSAGES, EXIT_CODES, DryRunComplete;
|
|
51
51
|
var init_errors = __esm({
|
|
52
52
|
"src/errors.ts"() {
|
|
53
53
|
"use strict";
|
|
@@ -85,6 +85,12 @@ var init_errors = __esm({
|
|
|
85
85
|
RATE_LIMITED: 6,
|
|
86
86
|
NETWORK_ERROR: 7
|
|
87
87
|
};
|
|
88
|
+
DryRunComplete = class extends Error {
|
|
89
|
+
constructor() {
|
|
90
|
+
super("Dry run complete");
|
|
91
|
+
this.name = "DryRunComplete";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
88
94
|
}
|
|
89
95
|
});
|
|
90
96
|
|
|
@@ -140,6 +146,31 @@ var init_client = __esm({
|
|
|
140
146
|
async delete(path, body) {
|
|
141
147
|
return this.request("DELETE", path, body);
|
|
142
148
|
}
|
|
149
|
+
async downloadUrl(url) {
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
headers: { Authorization: this.token },
|
|
155
|
+
signal: controller.signal
|
|
156
|
+
});
|
|
157
|
+
clearTimeout(timeoutId);
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const body = await this.safeJson(response);
|
|
160
|
+
throw parseApiError(body, response.status);
|
|
161
|
+
}
|
|
162
|
+
return response.arrayBuffer();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
if (error instanceof ClickUpError) throw error;
|
|
166
|
+
throw new ClickUpError(
|
|
167
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
168
|
+
0,
|
|
169
|
+
void 0,
|
|
170
|
+
void 0
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
143
174
|
async upload(path, filePath, filename) {
|
|
144
175
|
const fileBuffer = readFileSync(filePath);
|
|
145
176
|
const resolvedFilename = filename ?? basename(filePath);
|
|
@@ -148,7 +179,7 @@ var init_client = __esm({
|
|
|
148
179
|
const url = `${this.baseUrl}${path}`;
|
|
149
180
|
if (this.dryRun) {
|
|
150
181
|
this.logDryRun("POST", url, "[multipart form data]");
|
|
151
|
-
|
|
182
|
+
throw new DryRunComplete();
|
|
152
183
|
}
|
|
153
184
|
const controller = new AbortController();
|
|
154
185
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -193,7 +224,7 @@ var init_client = __esm({
|
|
|
193
224
|
}
|
|
194
225
|
if (this.dryRun) {
|
|
195
226
|
this.logDryRun(method, url, body);
|
|
196
|
-
|
|
227
|
+
throw new DryRunComplete();
|
|
197
228
|
}
|
|
198
229
|
let lastError;
|
|
199
230
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
@@ -328,35 +359,111 @@ Headers: ${JSON.stringify(redactedHeaders)}
|
|
|
328
359
|
});
|
|
329
360
|
|
|
330
361
|
// src/config.ts
|
|
362
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
331
363
|
import Conf from "conf";
|
|
332
364
|
function isValidConfigKey(key) {
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return
|
|
365
|
+
return ALL_CONFIG_KEYS.includes(key);
|
|
366
|
+
}
|
|
367
|
+
function setProfileOverride(nameOrKey) {
|
|
368
|
+
_profileOverride = nameOrKey ? findProfileKey(nameOrKey) : void 0;
|
|
369
|
+
}
|
|
370
|
+
function getActiveProfileKey() {
|
|
371
|
+
if (_profileOverride) return _profileOverride;
|
|
372
|
+
const active = config.get("active_profile");
|
|
373
|
+
return active ?? "default";
|
|
374
|
+
}
|
|
375
|
+
function getProfiles() {
|
|
376
|
+
const stored = config.get("profiles");
|
|
377
|
+
return stored ?? {};
|
|
378
|
+
}
|
|
379
|
+
function setProfile(key, profile) {
|
|
380
|
+
const profiles = getProfiles();
|
|
381
|
+
profiles[key] = profile;
|
|
382
|
+
config.set("profiles", profiles);
|
|
342
383
|
}
|
|
343
|
-
function
|
|
344
|
-
|
|
384
|
+
function deleteProfile(key) {
|
|
385
|
+
const profiles = getProfiles();
|
|
386
|
+
delete profiles[key];
|
|
387
|
+
config.set("profiles", profiles);
|
|
388
|
+
}
|
|
389
|
+
function slugifyWorkspaceName(name) {
|
|
390
|
+
return name.toLowerCase().replace(/'/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
391
|
+
}
|
|
392
|
+
function findProfileKey(nameOrKey) {
|
|
393
|
+
const profiles = getProfiles();
|
|
394
|
+
if (profiles[nameOrKey] !== void 0) return nameOrKey;
|
|
395
|
+
const lower = nameOrKey.toLowerCase();
|
|
396
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
397
|
+
if (profile.workspace_name?.toLowerCase() === lower) return key;
|
|
398
|
+
if (profile.nickname?.toLowerCase() === lower) return key;
|
|
399
|
+
}
|
|
400
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
401
|
+
if (profile.workspace_name?.toLowerCase().includes(lower)) return key;
|
|
402
|
+
if (profile.nickname?.toLowerCase().includes(lower)) return key;
|
|
403
|
+
}
|
|
404
|
+
return nameOrKey;
|
|
405
|
+
}
|
|
406
|
+
function migrateConfig() {
|
|
407
|
+
const legacyToken = config.get("token");
|
|
408
|
+
const legacyWorkspaceId = config.get("workspace_id");
|
|
409
|
+
if (!legacyToken && !legacyWorkspaceId) return;
|
|
410
|
+
const profiles = getProfiles();
|
|
411
|
+
if (!profiles["default"]) {
|
|
412
|
+
const defaultProfile = {};
|
|
413
|
+
if (legacyToken) defaultProfile.token = legacyToken;
|
|
414
|
+
if (legacyWorkspaceId) defaultProfile.workspace_id = legacyWorkspaceId;
|
|
415
|
+
setProfile("default", defaultProfile);
|
|
416
|
+
config.delete("token");
|
|
417
|
+
config.delete("workspace_id");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function resolveToken(flagValue, tokenFilePath) {
|
|
421
|
+
if (flagValue) return flagValue;
|
|
422
|
+
if (tokenFilePath) {
|
|
423
|
+
try {
|
|
424
|
+
const content = readFileSync2(tokenFilePath, "utf8").trim();
|
|
425
|
+
if (content) return content;
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const envVal = process.env["CLICKUP_API_TOKEN"];
|
|
430
|
+
if (envVal) return envVal;
|
|
431
|
+
const profileKey = getActiveProfileKey();
|
|
432
|
+
const profiles = getProfiles();
|
|
433
|
+
const activeProfile = profiles[profileKey];
|
|
434
|
+
if (activeProfile?.token) return activeProfile.token;
|
|
435
|
+
const legacyToken = config.get("token");
|
|
436
|
+
if (legacyToken) return legacyToken;
|
|
437
|
+
return void 0;
|
|
345
438
|
}
|
|
346
439
|
function resolveWorkspaceId(flagValue) {
|
|
347
|
-
|
|
440
|
+
if (flagValue) return flagValue;
|
|
441
|
+
const envVal = process.env["CLICKUP_WORKSPACE_ID"];
|
|
442
|
+
if (envVal) return envVal;
|
|
443
|
+
const profileKey = getActiveProfileKey();
|
|
444
|
+
const profiles = getProfiles();
|
|
445
|
+
const activeProfile = profiles[profileKey];
|
|
446
|
+
if (activeProfile?.workspace_id) return activeProfile.workspace_id;
|
|
447
|
+
const withWorkspace = Object.values(profiles).filter((p) => p.workspace_id);
|
|
448
|
+
if (withWorkspace.length === 1) return withWorkspace[0].workspace_id;
|
|
449
|
+
const legacyId = config.get("workspace_id");
|
|
450
|
+
if (legacyId) return legacyId;
|
|
451
|
+
return void 0;
|
|
348
452
|
}
|
|
349
453
|
function resolveOutputFormat(flagValue) {
|
|
350
|
-
|
|
351
|
-
|
|
454
|
+
if (flagValue) return flagValue;
|
|
455
|
+
const envVal = process.env["CLICKUP_OUTPUT_FORMAT"];
|
|
456
|
+
if (envVal) return envVal;
|
|
457
|
+
const stored = config.get("output_format");
|
|
458
|
+
if (stored) return stored;
|
|
352
459
|
return process.stdout.isTTY ? "table" : "json";
|
|
353
460
|
}
|
|
354
|
-
var
|
|
461
|
+
var ALL_CONFIG_KEYS, VALID_CONFIG_KEYS, config, _profileOverride;
|
|
355
462
|
var init_config = __esm({
|
|
356
463
|
"src/config.ts"() {
|
|
357
464
|
"use strict";
|
|
358
|
-
|
|
359
|
-
VALID_CONFIG_KEYS =
|
|
465
|
+
ALL_CONFIG_KEYS = ["token", "workspace_id", "output_format", "color", "page_size", "timezone", "active_profile"];
|
|
466
|
+
VALID_CONFIG_KEYS = ALL_CONFIG_KEYS;
|
|
360
467
|
config = new Conf({
|
|
361
468
|
projectName: "clickup-cli",
|
|
362
469
|
schema: {
|
|
@@ -365,17 +472,15 @@ var init_config = __esm({
|
|
|
365
472
|
output_format: { type: "string", enum: ["table", "json", "csv", "tsv", "quiet", "id"] },
|
|
366
473
|
color: { type: "boolean", default: true },
|
|
367
474
|
page_size: { type: "number", default: 100 },
|
|
368
|
-
timezone: { type: "string" }
|
|
475
|
+
timezone: { type: "string" },
|
|
476
|
+
active_profile: { type: "string" },
|
|
477
|
+
profiles: { type: "object" }
|
|
369
478
|
}
|
|
370
479
|
});
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
color: "CLICKUP_COLOR",
|
|
376
|
-
page_size: "CLICKUP_PAGE_SIZE",
|
|
377
|
-
timezone: "CLICKUP_TIMEZONE"
|
|
378
|
-
};
|
|
480
|
+
try {
|
|
481
|
+
migrateConfig();
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
379
484
|
}
|
|
380
485
|
});
|
|
381
486
|
|
|
@@ -532,9 +637,10 @@ import { Command } from "commander";
|
|
|
532
637
|
|
|
533
638
|
// src/commands/auth-cmd.ts
|
|
534
639
|
init_config();
|
|
640
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
535
641
|
function registerAuthCommands(program, getClient) {
|
|
536
642
|
const auth = program.command("auth").description("Manage authentication");
|
|
537
|
-
auth.command("login").description("Authenticate with ClickUp").option("--token <token>", "Personal API token").option("--oauth", "Use OAuth2 PKCE browser flow (requires CLICKUP_CLIENT_ID and CLICKUP_CLIENT_SECRET)").action(async (opts) => {
|
|
643
|
+
auth.command("login").description("Authenticate with ClickUp").option("--token <token>", "Personal API token").option("--token-file <path>", "Read token from this file path").option("--oauth", "Use OAuth2 PKCE browser flow (requires CLICKUP_CLIENT_ID and CLICKUP_CLIENT_SECRET)").action(async (opts) => {
|
|
538
644
|
if (opts.oauth) {
|
|
539
645
|
const { oauthLogin: oauthLogin2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
540
646
|
const token2 = await oauthLogin2();
|
|
@@ -550,9 +656,19 @@ function registerAuthCommands(program, getClient) {
|
|
|
550
656
|
return;
|
|
551
657
|
}
|
|
552
658
|
let token = opts.token;
|
|
659
|
+
if (!token && opts.tokenFile) {
|
|
660
|
+
try {
|
|
661
|
+
token = readFileSync3(opts.tokenFile, "utf8").trim();
|
|
662
|
+
} catch {
|
|
663
|
+
process.stderr.write(`Error: Could not read token file: ${opts.tokenFile}
|
|
664
|
+
`);
|
|
665
|
+
process.exit(2);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
553
669
|
if (!token) {
|
|
554
670
|
if (!process.stdin.isTTY) {
|
|
555
|
-
process.stderr.write("Error: No token provided. Use --token <token
|
|
671
|
+
process.stderr.write("Error: No token provided. Use --token <token>, --token-file <path>, or --oauth in non-interactive mode.\n");
|
|
556
672
|
process.exit(2);
|
|
557
673
|
return;
|
|
558
674
|
}
|
|
@@ -571,15 +687,29 @@ function registerAuthCommands(program, getClient) {
|
|
|
571
687
|
const client = new ClientClass({ token });
|
|
572
688
|
try {
|
|
573
689
|
const userData = await client.get("/user");
|
|
574
|
-
|
|
690
|
+
const profileKey = getActiveProfileKey();
|
|
691
|
+
const profiles = getProfiles();
|
|
692
|
+
const existing = profiles[profileKey] ?? {};
|
|
693
|
+
setProfile(profileKey, { ...existing, token });
|
|
575
694
|
process.stdout.write(`Authenticated as ${userData.user.username} (${userData.user.email})
|
|
576
695
|
`);
|
|
696
|
+
const profile = getProfiles()[profileKey];
|
|
697
|
+
if (!profile?.workspace_id) {
|
|
698
|
+
process.stdout.write("Run: clickup workspace setup -- to configure your workspace.\n");
|
|
699
|
+
}
|
|
577
700
|
} catch {
|
|
578
701
|
process.stderr.write("Error: Invalid token. Authentication failed.\n");
|
|
579
702
|
process.exit(3);
|
|
580
703
|
}
|
|
581
704
|
});
|
|
582
705
|
auth.command("logout").description("Remove stored credentials").action(() => {
|
|
706
|
+
const profileKey = getActiveProfileKey();
|
|
707
|
+
const profiles = getProfiles();
|
|
708
|
+
const existing = profiles[profileKey];
|
|
709
|
+
if (existing) {
|
|
710
|
+
const { token: _removed, ...profileWithoutToken } = existing;
|
|
711
|
+
setProfile(profileKey, profileWithoutToken);
|
|
712
|
+
}
|
|
583
713
|
config.delete("token");
|
|
584
714
|
process.stdout.write("Logged out. Token removed.\n");
|
|
585
715
|
});
|
|
@@ -592,9 +722,17 @@ function registerAuthCommands(program, getClient) {
|
|
|
592
722
|
const prefix = token.slice(0, 8);
|
|
593
723
|
process.stdout.write(`Token: ${prefix}...
|
|
594
724
|
`);
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
725
|
+
const profileKey = getActiveProfileKey();
|
|
726
|
+
process.stdout.write(`Profile: ${profileKey}
|
|
727
|
+
`);
|
|
728
|
+
const profiles = getProfiles();
|
|
729
|
+
const activeProfile = profiles[profileKey];
|
|
730
|
+
if (activeProfile?.workspace_id) {
|
|
731
|
+
process.stdout.write(`Workspace: ${activeProfile.workspace_name ?? activeProfile.workspace_id}
|
|
732
|
+
`);
|
|
733
|
+
} else {
|
|
734
|
+
const legacyId = config.get("workspace_id");
|
|
735
|
+
if (legacyId) process.stdout.write(`Workspace: ${legacyId}
|
|
598
736
|
`);
|
|
599
737
|
}
|
|
600
738
|
try {
|
|
@@ -620,7 +758,7 @@ function registerAuthCommands(program, getClient) {
|
|
|
620
758
|
|
|
621
759
|
// src/commands/config-cmd.ts
|
|
622
760
|
init_config();
|
|
623
|
-
function registerConfigCommands(program) {
|
|
761
|
+
function registerConfigCommands(program, getClient) {
|
|
624
762
|
const configCmd = program.command("config").description("Manage CLI configuration");
|
|
625
763
|
configCmd.command("set").description("Set a configuration value").argument("<key>", "Config key").argument("<value>", "Config value").action((key, value) => {
|
|
626
764
|
if (!isValidConfigKey(key)) {
|
|
@@ -694,6 +832,141 @@ function registerConfigCommands(program) {
|
|
|
694
832
|
configCmd.command("path").description("Print the config file path").action(() => {
|
|
695
833
|
process.stdout.write(config.path + "\n");
|
|
696
834
|
});
|
|
835
|
+
configCmd.command("validate").description("Verify the stored token works and show current identity").action(async () => {
|
|
836
|
+
const client = getClient();
|
|
837
|
+
let user;
|
|
838
|
+
try {
|
|
839
|
+
const userData = await client.get("/user");
|
|
840
|
+
user = userData.user;
|
|
841
|
+
} catch (err) {
|
|
842
|
+
process.stderr.write(`Error: Token validation failed. Run: clickup auth login
|
|
843
|
+
`);
|
|
844
|
+
process.exit(3);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
let workspaceName = "(none)";
|
|
848
|
+
let workspaceId = "(none)";
|
|
849
|
+
try {
|
|
850
|
+
const teamsData = await client.get("/team");
|
|
851
|
+
const activeKey = getActiveProfileKey();
|
|
852
|
+
const profiles = getProfiles();
|
|
853
|
+
const activeProfile = profiles[activeKey];
|
|
854
|
+
if (activeProfile?.workspace_id) {
|
|
855
|
+
const match = teamsData.teams.find((t) => t.id === activeProfile.workspace_id);
|
|
856
|
+
workspaceName = match?.name ?? activeProfile.workspace_name ?? activeProfile.workspace_id;
|
|
857
|
+
workspaceId = activeProfile.workspace_id;
|
|
858
|
+
} else if (teamsData.teams.length > 0) {
|
|
859
|
+
workspaceName = teamsData.teams.map((t) => t.name).join(", ");
|
|
860
|
+
workspaceId = teamsData.teams.map((t) => t.id).join(", ");
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
process.stdout.write(`User: ${user.username} (${user.email})
|
|
865
|
+
`);
|
|
866
|
+
process.stdout.write(`Active profile: ${getActiveProfileKey()}
|
|
867
|
+
`);
|
|
868
|
+
process.stdout.write(`Workspace: ${workspaceName}
|
|
869
|
+
`);
|
|
870
|
+
process.stdout.write(`Workspace ID: ${workspaceId}
|
|
871
|
+
`);
|
|
872
|
+
process.stdout.write(`Token status: Valid
|
|
873
|
+
`);
|
|
874
|
+
});
|
|
875
|
+
const profileCmd = configCmd.command("profile").description("Manage named profiles");
|
|
876
|
+
profileCmd.command("list").description("List all profiles").action(() => {
|
|
877
|
+
const profiles = getProfiles();
|
|
878
|
+
const activeKey = getActiveProfileKey();
|
|
879
|
+
const keys = Object.keys(profiles);
|
|
880
|
+
if (keys.length === 0) {
|
|
881
|
+
process.stdout.write("No profiles configured. Run: clickup auth login\n");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const col = (s, w) => s.padEnd(w).slice(0, w);
|
|
885
|
+
process.stdout.write(
|
|
886
|
+
`${col("Key", 20)} ${col("Workspace Name", 30)} ${col("Nickname", 15)} ${col("Workspace ID", 16)} Active
|
|
887
|
+
`
|
|
888
|
+
);
|
|
889
|
+
process.stdout.write(`${"-".repeat(20)} ${"-".repeat(30)} ${"-".repeat(15)} ${"-".repeat(16)} ------
|
|
890
|
+
`);
|
|
891
|
+
for (const key of keys) {
|
|
892
|
+
const p = profiles[key];
|
|
893
|
+
const active = key === activeKey ? "*" : "";
|
|
894
|
+
process.stdout.write(
|
|
895
|
+
`${col(key, 20)} ${col(p.workspace_name ?? "", 30)} ${col(p.nickname ?? "", 15)} ${col(p.workspace_id ?? "", 16)} ${active}
|
|
896
|
+
`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
profileCmd.command("create").description("Create an empty profile").argument("<key>", "Profile key").action((key) => {
|
|
901
|
+
const profiles = getProfiles();
|
|
902
|
+
if (profiles[key]) {
|
|
903
|
+
process.stderr.write(`Error: Profile "${key}" already exists.
|
|
904
|
+
`);
|
|
905
|
+
process.exit(2);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
setProfile(key, {});
|
|
909
|
+
process.stdout.write(`Profile "${key}" created.
|
|
910
|
+
`);
|
|
911
|
+
});
|
|
912
|
+
profileCmd.command("delete").description("Delete a profile").argument("<key>", "Profile key").option("--confirm", "Skip confirmation prompt").action(async (key, opts) => {
|
|
913
|
+
const profiles = getProfiles();
|
|
914
|
+
if (!profiles[key]) {
|
|
915
|
+
process.stderr.write(`Error: Profile "${key}" not found.
|
|
916
|
+
`);
|
|
917
|
+
process.exit(2);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const isActive = key === getActiveProfileKey();
|
|
921
|
+
if (!opts.confirm) {
|
|
922
|
+
if (!process.stdin.isTTY) {
|
|
923
|
+
process.stderr.write("Error: Add --confirm to delete a profile in non-interactive mode.\n");
|
|
924
|
+
process.exit(2);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
928
|
+
const answer = await confirm({
|
|
929
|
+
message: isActive ? `"${key}" is the active profile. Delete it?` : `Delete profile "${key}"?`,
|
|
930
|
+
default: false
|
|
931
|
+
});
|
|
932
|
+
if (!answer) {
|
|
933
|
+
process.stdout.write("Cancelled.\n");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
deleteProfile(key);
|
|
938
|
+
if (isActive) {
|
|
939
|
+
config.delete("active_profile");
|
|
940
|
+
}
|
|
941
|
+
process.stdout.write(`Profile "${key}" deleted.
|
|
942
|
+
`);
|
|
943
|
+
});
|
|
944
|
+
profileCmd.command("use").description("Switch the active profile").argument("<key>", "Profile key, workspace name, or nickname").action((nameOrKey) => {
|
|
945
|
+
const profiles = getProfiles();
|
|
946
|
+
const resolvedKey = findProfileKey(nameOrKey);
|
|
947
|
+
if (!profiles[resolvedKey]) {
|
|
948
|
+
process.stderr.write(`Error: Profile "${nameOrKey}" not found.
|
|
949
|
+
`);
|
|
950
|
+
process.exit(2);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
config.set("active_profile", resolvedKey);
|
|
954
|
+
process.stdout.write(`Active profile: ${resolvedKey}
|
|
955
|
+
`);
|
|
956
|
+
});
|
|
957
|
+
profileCmd.command("nickname").description("Set a short nickname for a profile").argument("<key>", "Profile key").argument("<nickname>", "Short nickname").action((key, nickname) => {
|
|
958
|
+
const profiles = getProfiles();
|
|
959
|
+
const profile = profiles[key];
|
|
960
|
+
if (!profile) {
|
|
961
|
+
process.stderr.write(`Error: Profile "${key}" not found.
|
|
962
|
+
`);
|
|
963
|
+
process.exit(2);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
setProfile(key, { ...profile, nickname });
|
|
967
|
+
process.stdout.write(`Nickname "${nickname}" set for profile "${key}".
|
|
968
|
+
`);
|
|
969
|
+
});
|
|
697
970
|
}
|
|
698
971
|
|
|
699
972
|
// src/commands/workspace.ts
|
|
@@ -869,6 +1142,7 @@ function getFields(resource, action) {
|
|
|
869
1142
|
}
|
|
870
1143
|
|
|
871
1144
|
// src/commands/workspace.ts
|
|
1145
|
+
registerSchema("workspace", "setup", "Configure workspace -- fetches workspaces and saves profile", []);
|
|
872
1146
|
registerSchema("workspace", "list", "List all workspaces accessible to the authenticated user", []);
|
|
873
1147
|
registerSchema("workspace", "get", "Get workspace details", [
|
|
874
1148
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" }
|
|
@@ -912,6 +1186,49 @@ function requireWorkspaceId(program) {
|
|
|
912
1186
|
}
|
|
913
1187
|
function registerWorkspaceCommands(program, getClient) {
|
|
914
1188
|
const workspace = program.command("workspace").description("Manage workspaces");
|
|
1189
|
+
workspace.command("setup").description("Configure workspace for the active profile").action(async () => {
|
|
1190
|
+
const client = getClient();
|
|
1191
|
+
const data = await client.get("/team");
|
|
1192
|
+
const teams = data.teams;
|
|
1193
|
+
if (teams.length === 0) {
|
|
1194
|
+
process.stderr.write("Error: No workspaces found for this token.\n");
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
let selected;
|
|
1199
|
+
if (teams.length === 1) {
|
|
1200
|
+
selected = teams[0];
|
|
1201
|
+
} else {
|
|
1202
|
+
if (!process.stdin.isTTY) {
|
|
1203
|
+
process.stderr.write("Error: Multiple workspaces found. Use --workspace-id or run in interactive mode.\n");
|
|
1204
|
+
process.exit(2);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const { select } = await import("@inquirer/prompts");
|
|
1208
|
+
const selectedId = await select({
|
|
1209
|
+
message: "Select workspace:",
|
|
1210
|
+
choices: teams.map((t) => ({ name: t.name, value: t.id }))
|
|
1211
|
+
});
|
|
1212
|
+
selected = teams.find((t) => t.id === selectedId);
|
|
1213
|
+
}
|
|
1214
|
+
const profileKey = getActiveProfileKey();
|
|
1215
|
+
const profiles = getProfiles();
|
|
1216
|
+
const existing = profiles[profileKey] ?? {};
|
|
1217
|
+
const slugKey = slugifyWorkspaceName(selected.name);
|
|
1218
|
+
setProfile(profileKey, {
|
|
1219
|
+
...existing,
|
|
1220
|
+
workspace_id: selected.id,
|
|
1221
|
+
workspace_name: selected.name
|
|
1222
|
+
});
|
|
1223
|
+
process.stdout.write(
|
|
1224
|
+
`Workspace "${selected.name}" saved as profile "${profileKey}".
|
|
1225
|
+
`
|
|
1226
|
+
);
|
|
1227
|
+
if (slugKey !== profileKey) {
|
|
1228
|
+
process.stdout.write(`Use --profile "${selected.name}" or --profile ${profileKey}
|
|
1229
|
+
`);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
915
1232
|
workspace.command("list").description("List all workspaces").action(async () => {
|
|
916
1233
|
const client = getClient();
|
|
917
1234
|
const data = await client.get("/team");
|
|
@@ -1351,6 +1668,17 @@ registerSchema("task", "delete", "Delete a task", [
|
|
|
1351
1668
|
registerSchema("task", "time-in-status", "Get time spent in each status for a task", [
|
|
1352
1669
|
{ flag: "<task-id>", type: "string", required: true, description: "Task ID" }
|
|
1353
1670
|
]);
|
|
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
|
+
]);
|
|
1354
1682
|
var TASK_COLUMNS = [
|
|
1355
1683
|
{ key: "id", header: "ID", width: 12 },
|
|
1356
1684
|
{ key: "name", header: "Name", width: 30 },
|
|
@@ -1377,6 +1705,35 @@ function requireWorkspaceId3(program) {
|
|
|
1377
1705
|
function collect(value, previous) {
|
|
1378
1706
|
return previous.concat([value]);
|
|
1379
1707
|
}
|
|
1708
|
+
var PRIORITY_MAP = {
|
|
1709
|
+
urgent: 1,
|
|
1710
|
+
high: 2,
|
|
1711
|
+
normal: 3,
|
|
1712
|
+
low: 4
|
|
1713
|
+
};
|
|
1714
|
+
function parsePriority(value) {
|
|
1715
|
+
const lower = value.toLowerCase();
|
|
1716
|
+
if (PRIORITY_MAP[lower] !== void 0) return PRIORITY_MAP[lower];
|
|
1717
|
+
const num = parseInt(value, 10);
|
|
1718
|
+
if (!isNaN(num) && num >= 1 && num <= 4) return num;
|
|
1719
|
+
throw new Error("--priority must be 1-4 or urgent/high/normal/low");
|
|
1720
|
+
}
|
|
1721
|
+
async function runConcurrent(tasks, limit) {
|
|
1722
|
+
const results = new Array(tasks.length);
|
|
1723
|
+
let idx = 0;
|
|
1724
|
+
async function worker() {
|
|
1725
|
+
while (idx < tasks.length) {
|
|
1726
|
+
const current = idx++;
|
|
1727
|
+
try {
|
|
1728
|
+
results[current] = await tasks[current]();
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
results[current] = e instanceof Error ? e : new Error(String(e));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
|
|
1735
|
+
return results;
|
|
1736
|
+
}
|
|
1380
1737
|
function buildTaskListParams(opts) {
|
|
1381
1738
|
const params = {};
|
|
1382
1739
|
if (opts.archived) params["archived"] = "true";
|
|
@@ -1455,13 +1812,13 @@ function registerTaskCommands(program, getClient) {
|
|
|
1455
1812
|
const data = await client.get(`/task/${taskId}`, params);
|
|
1456
1813
|
formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
|
|
1457
1814
|
});
|
|
1458
|
-
task.command("create").description("Create a new task").requiredOption("--list-id <id>", "List ID").requiredOption("--name <name>", "Task name").option("--description <desc>", "Plain text description").option("--markdown-description <md>", "Markdown description (overrides --description)").option("--status <s>", "Initial status").option("--priority <n>", "Priority (1
|
|
1815
|
+
task.command("create").description("Create a new task").requiredOption("--list-id <id>", "List ID").requiredOption("--name <name>", "Task name").option("--description <desc>", "Plain text description").option("--markdown-description <md>", "Markdown description (overrides --description)").option("--status <s>", "Initial status").option("--priority <n>", "Priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "Due date (Unix ms)").option("--start-date <date>", "Start date (Unix ms)").option("--assignee <id>", "Assignee user ID (repeatable)", collect, []).option("--tag <name>", "Tag name (repeatable)", collect, []).option("--time-estimate <ms>", "Time estimate in milliseconds", parseInt).option("--notify-all", "Notify all assignees and watchers").option("--parent <task-id>", "Parent task ID (creates subtask)").option("--links-to <task-id>", "Link to another task").option("--custom-field <id=value>", "Set custom field (repeatable)", collect, []).option("--check-required-custom-fields", "Reject if required custom fields are missing").action(async (opts) => {
|
|
1459
1816
|
const client = getClient();
|
|
1460
1817
|
const body = { name: opts.name };
|
|
1461
1818
|
if (opts.markdownDescription !== void 0) body["markdown_description"] = opts.markdownDescription;
|
|
1462
1819
|
else if (opts.description !== void 0) body["description"] = opts.description;
|
|
1463
1820
|
if (opts.status !== void 0) body["status"] = opts.status;
|
|
1464
|
-
if (opts.priority !== void 0) body["priority"] = opts.priority;
|
|
1821
|
+
if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
|
|
1465
1822
|
if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
|
|
1466
1823
|
if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
|
|
1467
1824
|
const assignees = opts.assignee;
|
|
@@ -1490,13 +1847,13 @@ function registerTaskCommands(program, getClient) {
|
|
|
1490
1847
|
const data = await client.post(`/list/${opts.listId}/task`, body);
|
|
1491
1848
|
formatOutput(data, TASK_COLUMNS, getOutputOptions(program));
|
|
1492
1849
|
});
|
|
1493
|
-
task.command("update").description("Update a task").argument("<task-id>", "Task ID").option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4)"
|
|
1850
|
+
task.command("update").description("Update a task").argument("<task-id>", "Task ID").option("--name <name>", "New task name").option("--description <desc>", "New description").option("--status <s>", "New status").option("--priority <n>", "New priority (1-4 or urgent/high/normal/low)").option("--due-date <date>", "New due date (Unix ms)").option("--start-date <date>", "New start date (Unix ms)").option("--time-estimate <ms>", "New time estimate in milliseconds", parseInt).option("--assignee-add <id>", "Add assignee (repeatable)", collect, []).option("--assignee-remove <id>", "Remove assignee (repeatable)", collect, []).option("--archived <bool>", "Archive or unarchive").action(async (taskId, opts) => {
|
|
1494
1851
|
const client = getClient();
|
|
1495
1852
|
const body = {};
|
|
1496
1853
|
if (opts.name !== void 0) body["name"] = opts.name;
|
|
1497
1854
|
if (opts.description !== void 0) body["description"] = opts.description;
|
|
1498
1855
|
if (opts.status !== void 0) body["status"] = opts.status;
|
|
1499
|
-
if (opts.priority !== void 0) body["priority"] = opts.priority;
|
|
1856
|
+
if (opts.priority !== void 0) body["priority"] = parsePriority(opts.priority);
|
|
1500
1857
|
if (opts.dueDate !== void 0) body["due_date"] = parseInt(opts.dueDate, 10);
|
|
1501
1858
|
if (opts.startDate !== void 0) body["start_date"] = parseInt(opts.startDate, 10);
|
|
1502
1859
|
if (opts.timeEstimate !== void 0) body["time_estimate"] = opts.timeEstimate;
|
|
@@ -1553,6 +1910,68 @@ function registerTaskCommands(program, getClient) {
|
|
|
1553
1910
|
{ key: "statuses", header: "Statuses", width: 50 }
|
|
1554
1911
|
], getOutputOptions(program));
|
|
1555
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
|
+
});
|
|
1556
1975
|
}
|
|
1557
1976
|
|
|
1558
1977
|
// src/commands/checklist.ts
|
|
@@ -1900,11 +2319,21 @@ function registerRelationCommands(program, getClient) {
|
|
|
1900
2319
|
}
|
|
1901
2320
|
|
|
1902
2321
|
// src/commands/attachment.ts
|
|
2322
|
+
import { writeFileSync } from "fs";
|
|
2323
|
+
import { join } from "path";
|
|
1903
2324
|
registerSchema("attachment", "upload", "Upload a file to a task", [
|
|
1904
2325
|
{ flag: "--task-id", type: "string", required: true, description: "Task ID" },
|
|
1905
2326
|
{ flag: "--file", type: "string", required: true, description: "Local file path" },
|
|
1906
2327
|
{ flag: "--filename", type: "string", required: false, description: "Override display filename" }
|
|
1907
2328
|
]);
|
|
2329
|
+
registerSchema("attachment", "list", "List attachments on a task", [
|
|
2330
|
+
{ flag: "--task-id", type: "string", required: true, description: "Task ID" }
|
|
2331
|
+
]);
|
|
2332
|
+
registerSchema("attachment", "download", "Download an attachment from a task", [
|
|
2333
|
+
{ flag: "--task-id", type: "string", required: true, description: "Task ID" },
|
|
2334
|
+
{ 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>)" }
|
|
2336
|
+
]);
|
|
1908
2337
|
var ATTACHMENT_COLUMNS = [
|
|
1909
2338
|
{ key: "id", header: "ID", width: 20 },
|
|
1910
2339
|
{ key: "title", header: "Title", width: 30 },
|
|
@@ -1919,6 +2348,35 @@ function registerAttachmentCommands(program, getClient) {
|
|
|
1919
2348
|
const data = await client.upload(`/task/${opts.taskId}/attachment`, opts.file, opts.filename);
|
|
1920
2349
|
formatOutput(data, ATTACHMENT_COLUMNS, getOutputOptions(program));
|
|
1921
2350
|
});
|
|
2351
|
+
attachment.command("list").description("List attachments on a task").requiredOption("--task-id <id>", "Task ID").action(async (opts) => {
|
|
2352
|
+
const client = getClient();
|
|
2353
|
+
const data = await client.get(`/task/${opts.taskId}`);
|
|
2354
|
+
const attachments = data["attachments"] ?? [];
|
|
2355
|
+
formatOutput(attachments, ATTACHMENT_COLUMNS, getOutputOptions(program));
|
|
2356
|
+
});
|
|
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) => {
|
|
2358
|
+
const { default: ora } = await import("ora");
|
|
2359
|
+
const client = getClient();
|
|
2360
|
+
const data = await client.get(`/task/${opts.taskId}`);
|
|
2361
|
+
const attachments = data["attachments"] ?? [];
|
|
2362
|
+
const attachment2 = attachments.find((a) => a.id === opts.attachmentId);
|
|
2363
|
+
if (!attachment2) {
|
|
2364
|
+
process.stderr.write(`Error: Attachment "${opts.attachmentId}" not found on task "${opts.taskId}".
|
|
2365
|
+
`);
|
|
2366
|
+
process.exit(4);
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
const outputPath = opts.output ?? join(process.cwd(), `attachment-${attachment2.id}-${attachment2.title}`);
|
|
2370
|
+
const spinner = ora(`Downloading ${attachment2.title}...`).start();
|
|
2371
|
+
try {
|
|
2372
|
+
const buffer = await client.downloadUrl(attachment2.url);
|
|
2373
|
+
writeFileSync(outputPath, Buffer.from(buffer));
|
|
2374
|
+
spinner.succeed(`Downloaded to ${outputPath}`);
|
|
2375
|
+
} catch (err) {
|
|
2376
|
+
spinner.fail("Download failed");
|
|
2377
|
+
throw err;
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
1922
2380
|
}
|
|
1923
2381
|
|
|
1924
2382
|
// src/commands/comment.ts
|
|
@@ -3373,6 +3831,22 @@ registerSchema("template", "list", "List task templates in a workspace", [
|
|
|
3373
3831
|
{ flag: "--workspace-id", type: "string", required: true, description: "Workspace ID" },
|
|
3374
3832
|
{ flag: "--page", type: "integer", required: false, description: "Page number (starts at 0)" }
|
|
3375
3833
|
]);
|
|
3834
|
+
registerSchema("template", "apply-task", "Create a task from a task template", [
|
|
3835
|
+
{ flag: "--list-id", type: "string", required: true, description: "List to create the task in" },
|
|
3836
|
+
{ flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
|
|
3837
|
+
{ flag: "--name", type: "string", required: false, description: "Override the template name" }
|
|
3838
|
+
]);
|
|
3839
|
+
registerSchema("template", "apply-list", "Create a list from a list template", [
|
|
3840
|
+
{ flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
|
|
3841
|
+
{ flag: "--folder-id", type: "string", required: false, description: "Folder to create the list in (use this or --space-id)" },
|
|
3842
|
+
{ flag: "--space-id", type: "string", required: false, description: "Space to create the list in (use this or --folder-id)" },
|
|
3843
|
+
{ flag: "--name", type: "string", required: false, description: "Override the template name" }
|
|
3844
|
+
]);
|
|
3845
|
+
registerSchema("template", "apply-folder", "Create a folder from a folder template", [
|
|
3846
|
+
{ flag: "--space-id", type: "string", required: true, description: "Space to create the folder in" },
|
|
3847
|
+
{ flag: "--template-id", type: "string", required: true, description: "Template ID to apply" },
|
|
3848
|
+
{ flag: "--name", type: "string", required: false, description: "Override the template name" }
|
|
3849
|
+
]);
|
|
3376
3850
|
function registerTemplateCommands(program, getClient) {
|
|
3377
3851
|
const template = program.command("template").description("Manage task templates");
|
|
3378
3852
|
template.command("list").description("List task templates").option("--page <n>", "Page number (starts at 0)", parseInt).action(async (opts) => {
|
|
@@ -3384,6 +3858,39 @@ function registerTemplateCommands(program, getClient) {
|
|
|
3384
3858
|
const data = await client.get(`/team/${workspaceId}/taskTemplate`, params);
|
|
3385
3859
|
formatOutput(data.templates, TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
3386
3860
|
});
|
|
3861
|
+
template.command("apply-task").description("Create a task from a task template").requiredOption("--list-id <id>", "List to create the task in").requiredOption("--template-id <id>", "Template ID to apply").option("--name <name>", "Override the template name").action(async (opts) => {
|
|
3862
|
+
const client = getClient();
|
|
3863
|
+
const body = {};
|
|
3864
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
3865
|
+
const data = await client.post(
|
|
3866
|
+
`/list/${opts.listId}/taskTemplate/${opts.templateId}`,
|
|
3867
|
+
body
|
|
3868
|
+
);
|
|
3869
|
+
formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
3870
|
+
});
|
|
3871
|
+
template.command("apply-list").description("Create a list from a list template").requiredOption("--template-id <id>", "Template ID to apply").option("--folder-id <id>", "Folder to create the list in").option("--space-id <id>", "Space to create the list in (folderless)").option("--name <name>", "Override the template name").action(async (opts) => {
|
|
3872
|
+
if (!opts.folderId && !opts.spaceId) {
|
|
3873
|
+
process.stderr.write("Error: Provide either --folder-id or --space-id\n");
|
|
3874
|
+
process.exit(2);
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
const client = getClient();
|
|
3878
|
+
const body = {};
|
|
3879
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
3880
|
+
const path = opts.folderId ? `/folder/${opts.folderId}/listTemplate/${opts.templateId}` : `/space/${opts.spaceId}/listTemplate/${opts.templateId}`;
|
|
3881
|
+
const data = await client.post(path, body);
|
|
3882
|
+
formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
3883
|
+
});
|
|
3884
|
+
template.command("apply-folder").description("Create a folder from a folder template").requiredOption("--space-id <id>", "Space to create the folder in").requiredOption("--template-id <id>", "Template ID to apply").option("--name <name>", "Override the template name").action(async (opts) => {
|
|
3885
|
+
const client = getClient();
|
|
3886
|
+
const body = {};
|
|
3887
|
+
if (opts.name !== void 0) body["name"] = opts.name;
|
|
3888
|
+
const data = await client.post(
|
|
3889
|
+
`/space/${opts.spaceId}/folderTemplate/${opts.templateId}`,
|
|
3890
|
+
body
|
|
3891
|
+
);
|
|
3892
|
+
formatOutput([data], TEMPLATE_COLUMNS, getOutputOptions(program));
|
|
3893
|
+
});
|
|
3387
3894
|
}
|
|
3388
3895
|
|
|
3389
3896
|
// src/commands/task-type.ts
|
|
@@ -3729,8 +4236,8 @@ function registerDocCommands(program, getClient) {
|
|
|
3729
4236
|
}
|
|
3730
4237
|
|
|
3731
4238
|
// src/commands/skill-cmd.ts
|
|
3732
|
-
import { readdirSync, readFileSync as
|
|
3733
|
-
import { join, dirname } from "path";
|
|
4239
|
+
import { readdirSync, readFileSync as readFileSync4, existsSync } from "fs";
|
|
4240
|
+
import { join as join2, dirname } from "path";
|
|
3734
4241
|
import { fileURLToPath } from "url";
|
|
3735
4242
|
var SKILL_COLUMNS = [
|
|
3736
4243
|
{ key: "name", header: "Name", width: 28 },
|
|
@@ -3746,9 +4253,9 @@ registerSchema("skill", "path", "Print the file system path to a skill directory
|
|
|
3746
4253
|
]);
|
|
3747
4254
|
function findSkillsDir() {
|
|
3748
4255
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
3749
|
-
const bundledDir =
|
|
4256
|
+
const bundledDir = join2(thisDir, "..", "skills");
|
|
3750
4257
|
if (existsSync(bundledDir)) return bundledDir;
|
|
3751
|
-
const projectDir =
|
|
4258
|
+
const projectDir = join2(thisDir, "..", "..", "skills");
|
|
3752
4259
|
if (existsSync(projectDir)) return projectDir;
|
|
3753
4260
|
return void 0;
|
|
3754
4261
|
}
|
|
@@ -3789,16 +4296,16 @@ function loadSkills(skillsDir) {
|
|
|
3789
4296
|
return skills;
|
|
3790
4297
|
}
|
|
3791
4298
|
for (const entry of entries) {
|
|
3792
|
-
const skillFile =
|
|
4299
|
+
const skillFile = join2(skillsDir, entry, "SKILL.md");
|
|
3793
4300
|
if (!existsSync(skillFile)) continue;
|
|
3794
4301
|
try {
|
|
3795
|
-
const content =
|
|
4302
|
+
const content = readFileSync4(skillFile, "utf-8");
|
|
3796
4303
|
const { frontmatter } = parseFrontmatter(content);
|
|
3797
4304
|
skills.push({
|
|
3798
4305
|
name: frontmatter["name"] || entry,
|
|
3799
4306
|
description: (frontmatter["description"] || "").slice(0, 100),
|
|
3800
4307
|
type: classifySkill(frontmatter),
|
|
3801
|
-
path:
|
|
4308
|
+
path: join2(skillsDir, entry),
|
|
3802
4309
|
frontmatter
|
|
3803
4310
|
});
|
|
3804
4311
|
} catch {
|
|
@@ -3813,11 +4320,11 @@ function loadSkills(skillsDir) {
|
|
|
3813
4320
|
});
|
|
3814
4321
|
}
|
|
3815
4322
|
function findSkill(skillsDir, name) {
|
|
3816
|
-
const skillDir =
|
|
3817
|
-
const skillFile =
|
|
4323
|
+
const skillDir = join2(skillsDir, name);
|
|
4324
|
+
const skillFile = join2(skillDir, "SKILL.md");
|
|
3818
4325
|
if (!existsSync(skillFile)) return void 0;
|
|
3819
4326
|
try {
|
|
3820
|
-
const content =
|
|
4327
|
+
const content = readFileSync4(skillFile, "utf-8");
|
|
3821
4328
|
return { skillDir, content };
|
|
3822
4329
|
} catch {
|
|
3823
4330
|
return void 0;
|
|
@@ -3945,15 +4452,18 @@ function registerChatCommands(program, getClient) {
|
|
|
3945
4452
|
}
|
|
3946
4453
|
|
|
3947
4454
|
// src/cli.ts
|
|
3948
|
-
var VERSION = "0.
|
|
4455
|
+
var VERSION = "0.3.0";
|
|
3949
4456
|
function createProgram() {
|
|
3950
4457
|
const program = new Command();
|
|
3951
|
-
program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").option("--workspace-id <id>", "Workspace ID").option("--format <format>", "Output format (table|json|csv|tsv|quiet|id|md)").option("--no-color", "Disable colors").option("--no-header", "Omit column headers").option("--fields <fields>", "Show only specified fields (comma-separated)").option("--filter <filter>", "Client-side filter (key=value)").option("--sort <sort>", "Sort by field (field[:asc|:desc])").option("--limit <n>", "Limit results", parseInt).option("--verbose", "Show request details").option("--debug", "Full debug output").option("--dry-run", "Print what would be sent without executing");
|
|
4458
|
+
program.name("clickup").description("ClickUp CLI - Manage ClickUp workspaces from the terminal").version(VERSION).option("--token <token>", "API token").option("--token-file <path>", "Read API token from this file path").option("--profile <name>", "Profile to use (key, workspace name, or nickname)").option("--workspace-id <id>", "Workspace ID").option("--format <format>", "Output format (table|json|csv|tsv|quiet|id|md)").option("--no-color", "Disable colors").option("--no-header", "Omit column headers").option("--fields <fields>", "Show only specified fields (comma-separated)").option("--filter <filter>", "Client-side filter (key=value)").option("--sort <sort>", "Sort by field (field[:asc|:desc])").option("--limit <n>", "Limit results", parseInt).option("--verbose", "Show request details").option("--debug", "Full debug output").option("--dry-run", "Print what would be sent without executing");
|
|
3952
4459
|
return program;
|
|
3953
4460
|
}
|
|
3954
4461
|
function createClient(program) {
|
|
3955
4462
|
const globalOpts = program.opts();
|
|
3956
|
-
const token = resolveToken(
|
|
4463
|
+
const token = resolveToken(
|
|
4464
|
+
globalOpts["token"],
|
|
4465
|
+
globalOpts["tokenFile"]
|
|
4466
|
+
);
|
|
3957
4467
|
if (!token) {
|
|
3958
4468
|
process.stderr.write("Error: No API token found. Run: clickup auth login\n");
|
|
3959
4469
|
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
@@ -3983,8 +4493,11 @@ function getOutputOptions(program) {
|
|
|
3983
4493
|
}
|
|
3984
4494
|
function run() {
|
|
3985
4495
|
const program = createProgram();
|
|
4496
|
+
program.hook("preAction", () => {
|
|
4497
|
+
setProfileOverride(program.opts()["profile"]);
|
|
4498
|
+
});
|
|
3986
4499
|
registerAuthCommands(program, () => createClient(program));
|
|
3987
|
-
registerConfigCommands(program);
|
|
4500
|
+
registerConfigCommands(program, () => createClient(program));
|
|
3988
4501
|
registerWorkspaceCommands(program, () => createClient(program));
|
|
3989
4502
|
registerSpaceCommands(program, () => createClient(program));
|
|
3990
4503
|
registerFolderCommands(program, () => createClient(program));
|
|
@@ -4014,6 +4527,9 @@ function run() {
|
|
|
4014
4527
|
registerSchemaCommands(program);
|
|
4015
4528
|
registerSkillCommands(program);
|
|
4016
4529
|
program.parseAsync(process.argv).catch((error) => {
|
|
4530
|
+
if (error instanceof DryRunComplete) {
|
|
4531
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
4532
|
+
}
|
|
4017
4533
|
if (error instanceof ClickUpError) {
|
|
4018
4534
|
process.stderr.write(`Error: ${error.message}
|
|
4019
4535
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clickup-agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI covering the entire ClickUp API v2 surface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"build": "tsup",
|
|
19
19
|
"dev": "tsx bin/clickup.ts",
|
|
20
20
|
"test": "vitest run",
|
|
21
|
-
"typecheck": "tsc --noEmit"
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"version": "node scripts/sync-version.js"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
24
25
|
"clickup",
|
package/skills/clickup/SKILL.md
CHANGED
|
@@ -41,6 +41,7 @@ clickup <resource> <action> --help # Full help text
|
|
|
41
41
|
| `clickup-chat` | Chat channels, sending messages and notifications |
|
|
42
42
|
| `clickup-webhooks` | Webhook registration and management |
|
|
43
43
|
| `clickup-fields` | Custom fields, tags, custom task types |
|
|
44
|
+
| `clickup-templates` | Templates: list available templates, apply task/list/folder templates |
|
|
44
45
|
|
|
45
46
|
## Recipe Skills (multi-step workflows)
|
|
46
47
|
|
|
@@ -57,6 +58,7 @@ Recipes accept natural language arguments. Scope them to any team, department, p
|
|
|
57
58
|
| `clickup-sprint-closeout` | Close a sprint, carry over incomplete work |
|
|
58
59
|
| `clickup-time-audit` | Audit time tracking and utilization |
|
|
59
60
|
| `clickup-project-setup` | Scaffold a new project structure |
|
|
61
|
+
| `clickup-rollout` | Roll out a saved process template (apply + configure) |
|
|
60
62
|
| `clickup-capacity-check` | Check team workload and availability |
|
|
61
63
|
| `clickup-blocker-report` | Find blocked tasks and dependency chains |
|
|
62
64
|
| `clickup-goal-progress` | Report on goal/OKR completion |
|
|
@@ -81,6 +83,10 @@ clickup task list --list-id <id> --format quiet | xargs -I{} clickup task get {}
|
|
|
81
83
|
|
|
82
84
|
All commands support: `--format json|table|csv|tsv|quiet|id|md`, `--dry-run`, `--debug`, `--no-color`
|
|
83
85
|
|
|
86
|
+
Additional global flags:
|
|
87
|
+
- `--profile <name>` -- select a named profile (accepts profile key, workspace name, or nickname)
|
|
88
|
+
- `--token-file <path>` -- read API token from a file (useful in CI/CD and secret managers)
|
|
89
|
+
|
|
84
90
|
Use `--format md` to render output as a markdown table -- ideal for displaying results in chat messages or documents.
|
|
85
91
|
|
|
86
92
|
## Auth
|
|
@@ -91,16 +97,45 @@ Requires a ClickUp API token.
|
|
|
91
97
|
# Set up authentication
|
|
92
98
|
clickup auth login --token pk_XXXXXXXX_YYYYYYYY
|
|
93
99
|
|
|
100
|
+
# Read token from file (CI/CD-friendly)
|
|
101
|
+
clickup auth login --token-file /run/secrets/clickup_token
|
|
102
|
+
|
|
94
103
|
# Or use an environment variable
|
|
95
|
-
export
|
|
104
|
+
export CLICKUP_API_TOKEN=pk_XXXXXXXX_YYYYYYYY
|
|
96
105
|
|
|
97
|
-
#
|
|
98
|
-
clickup
|
|
106
|
+
# Auto-configure workspace after login (single workspace: auto-selects)
|
|
107
|
+
clickup workspace setup
|
|
99
108
|
|
|
100
109
|
# Check current auth status
|
|
101
110
|
clickup auth status
|
|
111
|
+
|
|
112
|
+
# Validate token and show identity
|
|
113
|
+
clickup config validate
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Named Profiles (Multi-Account)
|
|
117
|
+
|
|
118
|
+
Profiles store credentials per workspace. A single-workspace setup is transparent -- no `--profile` flag needed.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# List profiles
|
|
122
|
+
clickup config profile list
|
|
123
|
+
|
|
124
|
+
# Switch active profile
|
|
125
|
+
clickup config profile use "Acme Corp"
|
|
126
|
+
|
|
127
|
+
# Set a short nickname
|
|
128
|
+
clickup config profile nickname acme-corp acme
|
|
129
|
+
|
|
130
|
+
# Use a specific profile for one command
|
|
131
|
+
clickup task list --list-id 123 --profile acme
|
|
102
132
|
```
|
|
103
133
|
|
|
134
|
+
Profile resolution for `--profile` accepts:
|
|
135
|
+
1. The profile key (e.g. `default`)
|
|
136
|
+
2. Workspace name, case-insensitive (e.g. `"Henry's Workspace"`)
|
|
137
|
+
3. Nickname if set (e.g. `acme`)
|
|
138
|
+
|
|
104
139
|
## Creating Custom Skills
|
|
105
140
|
|
|
106
141
|
Users can create their own skills alongside the built-in ones. Add a SKILL.md to `.claude/skills/<name>/` in any project:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clickup-fields
|
|
3
3
|
description: Manages ClickUp custom fields, tags, and custom task types. Use when the user asks about custom fields, wants to set field values on tasks, manage tags, or work with custom task types.
|
|
4
|
-
allowed-tools: Bash(clickup custom-field *), Bash(clickup tag *), Bash(clickup custom-task-type *), Bash(clickup schema field*), Bash(clickup schema tag*)
|
|
4
|
+
allowed-tools: Bash(clickup custom-field *), Bash(clickup tag *), Bash(clickup custom-task-type *), Bash(clickup attachment *), Bash(clickup schema field*), Bash(clickup schema tag*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# ClickUp Custom Fields, Tags, and Task Types
|
|
@@ -69,9 +69,20 @@ clickup field list --list-id 998877 --format json
|
|
|
69
69
|
clickup field list --list-id 998877 --format md
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Attachment Commands
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
clickup attachment upload --task-id <id> --file <path> [--filename <name>]
|
|
76
|
+
clickup attachment list --task-id <id>
|
|
77
|
+
clickup attachment download --task-id <id> --attachment-id <id> [--output <path>]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`attachment download` fetches the file with the auth header and writes to `--output` (default: `./attachment-<id>-<title>`). Shows a spinner during download.
|
|
81
|
+
|
|
72
82
|
## Discovery
|
|
73
83
|
|
|
74
84
|
```bash
|
|
75
85
|
clickup schema fields.set # Show field set options
|
|
76
86
|
clickup schema tags.create # Show tag create fields
|
|
87
|
+
clickup schema attachment.list # Show attachment list options
|
|
77
88
|
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clickup-rollout
|
|
3
|
+
description: Rolls out a saved process template in ClickUp by applying it to a target location, then configuring assignees, due dates, and custom fields. Use when the user says "start a new [project/process/sprint/onboarding]" or wants to kick off a repeatable workflow from a template.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
context: fork
|
|
6
|
+
agent: general-purpose
|
|
7
|
+
argument-hint: "[template name or ID] [target location] [optional: assignee, due date, custom fields]"
|
|
8
|
+
allowed-tools: Bash(clickup *)
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Template Rollout
|
|
12
|
+
|
|
13
|
+
Apply a saved ClickUp template and configure the resulting task, list, or folder for a new instance of the process.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
### Step 1: Find the template
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# List all templates to find the right one
|
|
21
|
+
clickup template list --format json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If the user gave a template name, find its ID by matching the `name` field. If they gave an ID directly, skip this step.
|
|
25
|
+
|
|
26
|
+
### Step 2: Determine the template type and target
|
|
27
|
+
|
|
28
|
+
Based on what the user wants to create:
|
|
29
|
+
- **Task template**: needs a `--list-id` target
|
|
30
|
+
- **List template**: needs a `--folder-id` or `--space-id` target
|
|
31
|
+
- **Folder template**: needs a `--space-id` target
|
|
32
|
+
|
|
33
|
+
If the user hasn't specified a target, find it:
|
|
34
|
+
```bash
|
|
35
|
+
clickup space list --format json
|
|
36
|
+
clickup folder list --space-id <id> --format json
|
|
37
|
+
clickup list list --folder-id <id> --format json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Step 3: Apply the template
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Task template
|
|
44
|
+
TASK_ID=$(clickup template apply-task \
|
|
45
|
+
--list-id <list-id> \
|
|
46
|
+
--template-id <tmpl-id> \
|
|
47
|
+
--name "<instance-name>" \
|
|
48
|
+
--format id)
|
|
49
|
+
|
|
50
|
+
# List template (into folder)
|
|
51
|
+
LIST_ID=$(clickup template apply-list \
|
|
52
|
+
--folder-id <folder-id> \
|
|
53
|
+
--template-id <tmpl-id> \
|
|
54
|
+
--name "<instance-name>" \
|
|
55
|
+
--format id)
|
|
56
|
+
|
|
57
|
+
# List template (into space, folderless)
|
|
58
|
+
LIST_ID=$(clickup template apply-list \
|
|
59
|
+
--space-id <space-id> \
|
|
60
|
+
--template-id <tmpl-id> \
|
|
61
|
+
--name "<instance-name>" \
|
|
62
|
+
--format id)
|
|
63
|
+
|
|
64
|
+
# Folder template
|
|
65
|
+
FOLDER_ID=$(clickup template apply-folder \
|
|
66
|
+
--space-id <space-id> \
|
|
67
|
+
--template-id <tmpl-id> \
|
|
68
|
+
--name "<instance-name>" \
|
|
69
|
+
--format id)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Step 4: Configure the created resource
|
|
73
|
+
|
|
74
|
+
For a task template result, set assignees, dates, and custom fields:
|
|
75
|
+
```bash
|
|
76
|
+
# Assign team members
|
|
77
|
+
clickup task update "$TASK_ID" --assignee-add <user-id>
|
|
78
|
+
|
|
79
|
+
# Set start and due dates (Unix milliseconds)
|
|
80
|
+
clickup task update "$TASK_ID" --start-date <ms> --due-date <ms>
|
|
81
|
+
|
|
82
|
+
# Fill in custom fields
|
|
83
|
+
clickup field set --task-id "$TASK_ID" --field-id <field-id> --value "<value>"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For a list template result, populate it with initial tasks if needed:
|
|
87
|
+
```bash
|
|
88
|
+
clickup task list --list-id "$LIST_ID" --format json # review what the template created
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For a folder template result, review the created structure:
|
|
92
|
+
```bash
|
|
93
|
+
clickup list list --folder-id "$FOLDER_ID" --format table
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Step 5: Confirm and summarize
|
|
97
|
+
|
|
98
|
+
Report what was created:
|
|
99
|
+
- Resource type and ID
|
|
100
|
+
- Name used
|
|
101
|
+
- Assignees set
|
|
102
|
+
- Dates configured
|
|
103
|
+
- Custom fields populated
|
|
104
|
+
- Link or path in the ClickUp hierarchy
|
|
105
|
+
|
|
106
|
+
## Tips
|
|
107
|
+
|
|
108
|
+
- Use `--name` to give each rollout a unique name (e.g. "Client Onboarding - Acme Corp" or "Sprint 14 - Apr 7")
|
|
109
|
+
- Templates carry over all custom field definitions -- just fill in the values after applying
|
|
110
|
+
- Task IDs from `--format id` can be piped directly into update and field commands
|
|
111
|
+
- If rolling out multiple instances at once, loop over a list of names or targets
|
|
@@ -11,12 +11,15 @@ Manage the organizational hierarchy: Workspace > Space > Folder > List. Lists co
|
|
|
11
11
|
## Workspace Commands
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
+
clickup workspace setup # Auto-configure workspace (run after auth login)
|
|
14
15
|
clickup workspace list # List all workspaces
|
|
15
16
|
clickup workspace get [--workspace-id <id>] # Get workspace details
|
|
16
17
|
clickup workspace seats [--workspace-id <id>] # Show seat usage
|
|
17
18
|
clickup workspace plan [--workspace-id <id>] # Show billing plan
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
`workspace setup` calls `GET /team`, auto-selects if only one workspace exists, prompts in TTY if multiple, and saves the workspace to the active profile. After running it, `--workspace-id` is not needed for subsequent commands.
|
|
22
|
+
|
|
20
23
|
## Space Commands
|
|
21
24
|
|
|
22
25
|
```bash
|
|
@@ -33,7 +33,7 @@ clickup task get <task-id> [--include-subtasks] [--include-markdown-description]
|
|
|
33
33
|
```bash
|
|
34
34
|
clickup task create --list-id <id> --name <name>
|
|
35
35
|
[--description <text>] [--markdown-description <md>]
|
|
36
|
-
[--status <s>] [--priority <1-4>] [--due-date <ts>] [--start-date <ts>]
|
|
36
|
+
[--status <s>] [--priority <1-4|urgent|high|normal|low>] [--due-date <ts>] [--start-date <ts>]
|
|
37
37
|
[--assignee <id>...] [--tag <name>...] [--time-estimate <ms>]
|
|
38
38
|
[--parent <task-id>] [--custom-field <id=value>...]
|
|
39
39
|
```
|
|
@@ -41,15 +41,28 @@ clickup task create --list-id <id> --name <name>
|
|
|
41
41
|
### Update a task
|
|
42
42
|
```bash
|
|
43
43
|
clickup task update <task-id> [--name <name>] [--description <text>]
|
|
44
|
-
[--status <s>] [--priority <1-4>] [--due-date <ts>] [--start-date <ts>]
|
|
44
|
+
[--status <s>] [--priority <1-4|urgent|high|normal|low>] [--due-date <ts>] [--start-date <ts>]
|
|
45
45
|
[--assignee-add <id>...] [--assignee-remove <id>...] [--archived <bool>]
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
Priority accepts both integers (`1`-`4`) and strings (`urgent`, `high`, `normal`, `low`). Mapping: urgent=1, high=2, normal=3, low=4.
|
|
49
|
+
|
|
48
50
|
### Delete a task
|
|
49
51
|
```bash
|
|
50
52
|
clickup task delete <task-id> --confirm
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
### Bulk operations (concurrency limit 3)
|
|
56
|
+
```bash
|
|
57
|
+
# Apply the same update to multiple tasks
|
|
58
|
+
clickup task bulk-update --task-id <id> --task-id <id> ... [same update flags as task update]
|
|
59
|
+
|
|
60
|
+
# Delete multiple tasks
|
|
61
|
+
clickup task bulk-delete --task-id <id> --task-id <id> ... --confirm
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both bulk commands output a results table with `task_id | name | result`.
|
|
65
|
+
|
|
53
66
|
### Time in status
|
|
54
67
|
```bash
|
|
55
68
|
clickup task time-in-status <task-id>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clickup-templates
|
|
3
|
+
description: Lists and applies ClickUp templates to create tasks, lists, and folders from saved process templates. Use when the user wants to roll out a repeatable process, create items from a template, or find what templates are available in the workspace.
|
|
4
|
+
allowed-tools: Bash(clickup template *), Bash(clickup schema template*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ClickUp Templates
|
|
8
|
+
|
|
9
|
+
List available templates and apply them to create tasks, lists, and folders. Templates are defined in the ClickUp UI and can be rolled out repeatedly via the CLI.
|
|
10
|
+
|
|
11
|
+
## List Templates
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
clickup template list [--workspace-id <id>] [--page <n>]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Returns all task templates in the workspace. Use this to find template IDs before applying them.
|
|
18
|
+
|
|
19
|
+
## Apply a Task Template
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
clickup template apply-task --list-id <id> --template-id <id> [--name <override-name>]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Creates a new task in the specified list using the template structure, including all custom fields defined on the template.
|
|
26
|
+
|
|
27
|
+
## Apply a List Template
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Into a folder
|
|
31
|
+
clickup template apply-list --folder-id <id> --template-id <id> [--name <override-name>]
|
|
32
|
+
|
|
33
|
+
# Into a space (folderless)
|
|
34
|
+
clickup template apply-list --space-id <id> --template-id <id> [--name <override-name>]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Provide exactly one of `--folder-id` or `--space-id`.
|
|
38
|
+
|
|
39
|
+
## Apply a Folder Template
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
clickup template apply-folder --space-id <id> --template-id <id> [--name <override-name>]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Creates an entire folder structure (with nested lists and tasks) from the template.
|
|
46
|
+
|
|
47
|
+
## Common Patterns
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Step 1: Find available templates
|
|
51
|
+
clickup template list --format json
|
|
52
|
+
|
|
53
|
+
# Step 2: Apply a task template and capture the new task ID
|
|
54
|
+
TASK_ID=$(clickup template apply-task --list-id <id> --template-id <tmpl_id> --format id)
|
|
55
|
+
|
|
56
|
+
# Step 3: Set assignee and due date on the created task
|
|
57
|
+
clickup task update "$TASK_ID" --assignee-add 112233 --due-date 1735689600000
|
|
58
|
+
|
|
59
|
+
# Step 4: Fill in custom fields
|
|
60
|
+
clickup field set --task-id "$TASK_ID" --field-id cf_001 --value "In Review"
|
|
61
|
+
|
|
62
|
+
# Roll out a named instance of a process template
|
|
63
|
+
clickup template apply-list --folder-id <id> --template-id <tmpl_id> --name "Q1 Launch - March 2026"
|
|
64
|
+
|
|
65
|
+
# Apply a folder template to scaffold a whole project
|
|
66
|
+
clickup template apply-folder --space-id <id> --template-id <tmpl_id> --name "Client Onboarding - Acme Corp"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Discovery
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
clickup schema template # List all template actions
|
|
73
|
+
clickup schema template.apply-task # Show apply-task fields
|
|
74
|
+
clickup schema template.apply-list # Show apply-list fields
|
|
75
|
+
clickup schema template.apply-folder # Show apply-folder fields
|
|
76
|
+
```
|