@krodak/clickup-cli 0.21.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { createRequire } from "module";
7
7
 
8
8
  // src/api.ts
9
9
  var BASE_URL = "https://api.clickup.com/api/v2";
10
+ var BASE_URL_V3 = "https://api.clickup.com/api/v3";
10
11
  var MAX_PAGES = 100;
11
12
  function isCustomTaskId(id) {
12
13
  return /^[A-Z]+-\d+$/i.test(id);
@@ -56,6 +57,29 @@ var ClickUpClient = class {
56
57
  }
57
58
  return data;
58
59
  }
60
+ async requestV3(path, options = {}) {
61
+ const res = await fetch(`${BASE_URL_V3}${path}`, {
62
+ ...options,
63
+ signal: AbortSignal.timeout(3e4),
64
+ headers: {
65
+ Authorization: this.apiToken,
66
+ ...options.body ? { "Content-Type": "application/json" } : {},
67
+ ...options.headers
68
+ }
69
+ });
70
+ let data;
71
+ try {
72
+ data = await res.json();
73
+ } catch {
74
+ throw new Error(`ClickUp API error ${res.status}: response was not valid JSON`);
75
+ }
76
+ if (!res.ok) {
77
+ const raw = data.err ?? data.error ?? data.ECODE ?? res.statusText;
78
+ const msg = typeof raw === "string" ? raw : JSON.stringify(raw);
79
+ throw new Error(`ClickUp API error ${res.status}: ${msg}`);
80
+ }
81
+ return data;
82
+ }
59
83
  async getMe() {
60
84
  if (this.meCache) return this.meCache;
61
85
  const data = await this.request("/user");
@@ -383,6 +407,42 @@ var ClickUpClient = class {
383
407
  }
384
408
  return data;
385
409
  }
410
+ async getDocs(workspaceId) {
411
+ const data = await this.requestV3(`/workspaces/${workspaceId}/docs`);
412
+ return data.docs ?? [];
413
+ }
414
+ async getDocPage(workspaceId, docId, pageId) {
415
+ return this.requestV3(
416
+ `/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}?content_format=text/md`
417
+ );
418
+ }
419
+ async createDoc(workspaceId, title, content, parentId) {
420
+ const body = { title };
421
+ if (content) body.content = content;
422
+ if (parentId) {
423
+ body.parent_id = parentId;
424
+ body.parent_type = "doc";
425
+ }
426
+ return this.requestV3(`/workspaces/${workspaceId}/docs`, {
427
+ method: "POST",
428
+ body: JSON.stringify(body)
429
+ });
430
+ }
431
+ async createDocPage(workspaceId, docId, name, content, parentPageId) {
432
+ const body = { name, content_format: "text/md" };
433
+ if (content) body.content = content;
434
+ if (parentPageId) body.parent_page_id = parentPageId;
435
+ return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages`, {
436
+ method: "POST",
437
+ body: JSON.stringify(body)
438
+ });
439
+ }
440
+ async editDocPage(workspaceId, docId, pageId, updates) {
441
+ return this.requestV3(`/workspaces/${workspaceId}/docs/${docId}/pages/${pageId}`, {
442
+ method: "PUT",
443
+ body: JSON.stringify(updates)
444
+ });
445
+ }
386
446
  };
387
447
 
388
448
  // src/config.ts
@@ -390,18 +450,36 @@ import fs from "fs";
390
450
  import { homedir } from "os";
391
451
  import { join } from "path";
392
452
  function configDir() {
453
+ const xdg = process.env.XDG_CONFIG_HOME;
454
+ if (xdg) return join(xdg, "cup");
455
+ return join(homedir(), ".config", "cup");
456
+ }
457
+ function legacyConfigDir() {
393
458
  const xdg = process.env.XDG_CONFIG_HOME;
394
459
  if (xdg) return join(xdg, "cu");
395
460
  return join(homedir(), ".config", "cu");
396
461
  }
462
+ var migrationChecked = false;
463
+ function migrateFromLegacy() {
464
+ if (migrationChecked) return;
465
+ migrationChecked = true;
466
+ const legacy = legacyConfigDir();
467
+ const current = configDir();
468
+ if (fs.existsSync(join(legacy, "config.json")) && !fs.existsSync(join(current, "config.json"))) {
469
+ fs.mkdirSync(current, { recursive: true, mode: 448 });
470
+ fs.copyFileSync(join(legacy, "config.json"), join(current, "config.json"));
471
+ }
472
+ }
397
473
  function configPath() {
398
474
  return join(configDir(), "config.json");
399
475
  }
400
476
  function loadConfig() {
477
+ migrateFromLegacy();
401
478
  const envToken = process.env.CU_API_TOKEN?.trim();
402
479
  const envTeamId = process.env.CU_TEAM_ID?.trim();
403
480
  let fileToken;
404
481
  let fileTeamId;
482
+ let fileSprintFolderId;
405
483
  const path = configPath();
406
484
  if (fs.existsSync(path)) {
407
485
  const raw = fs.readFileSync(path, "utf-8");
@@ -413,6 +491,7 @@ function loadConfig() {
413
491
  }
414
492
  fileToken = parsed.apiToken?.trim();
415
493
  fileTeamId = parsed.teamId?.trim();
494
+ fileSprintFolderId = parsed.sprintFolderId?.trim() || void 0;
416
495
  }
417
496
  const apiToken = envToken || fileToken;
418
497
  if (!apiToken) {
@@ -425,9 +504,10 @@ function loadConfig() {
425
504
  if (!teamId) {
426
505
  throw new Error("Config missing required field: teamId.\nSet CU_TEAM_ID or run: cup init");
427
506
  }
428
- return { apiToken, teamId };
507
+ return { apiToken, teamId, ...fileSprintFolderId ? { sprintFolderId: fileSprintFolderId } : {} };
429
508
  }
430
509
  function loadRawConfig() {
510
+ migrateFromLegacy();
431
511
  const path = configPath();
432
512
  if (!fs.existsSync(path)) return {};
433
513
  try {
@@ -437,6 +517,7 @@ function loadRawConfig() {
437
517
  }
438
518
  }
439
519
  function getConfigPath() {
520
+ migrateFromLegacy();
440
521
  return configPath();
441
522
  }
442
523
  function writeConfig(config) {
@@ -453,10 +534,33 @@ function writeConfig(config) {
453
534
 
454
535
  // src/date.ts
455
536
  function formatDate(ms) {
537
+ return new Date(Number(ms)).toLocaleDateString("en-US", {
538
+ month: "short",
539
+ day: "numeric",
540
+ year: "numeric"
541
+ });
542
+ }
543
+ function formatTimestamp(ms) {
544
+ return new Date(Number(ms)).toLocaleString("en-US", {
545
+ month: "short",
546
+ day: "numeric",
547
+ hour: "numeric",
548
+ minute: "2-digit"
549
+ });
550
+ }
551
+ function formatDuration(ms) {
552
+ const totalMinutes = Math.round(Math.abs(ms) / 6e4);
553
+ const hours = Math.floor(totalMinutes / 60);
554
+ const minutes = totalMinutes % 60;
555
+ if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
556
+ if (hours > 0) return `${hours}h`;
557
+ return `${minutes}m`;
558
+ }
559
+ function formatDateISO(ms) {
456
560
  const d = new Date(Number(ms));
457
- const year = d.getUTCFullYear();
458
- const month = String(d.getUTCMonth() + 1).padStart(2, "0");
459
- const day = String(d.getUTCDate()).padStart(2, "0");
561
+ const year = d.getFullYear();
562
+ const month = String(d.getMonth() + 1).padStart(2, "0");
563
+ const day = String(d.getDate()).padStart(2, "0");
460
564
  return `${year}-${month}-${day}`;
461
565
  }
462
566
 
@@ -560,12 +664,6 @@ ${formatMarkdownTable(g.tasks, TASK_MD_COLUMNS)}`);
560
664
  if (sections.length === 0) return "No tasks found.";
561
665
  return sections.join("\n\n");
562
666
  }
563
- function formatDuration(ms) {
564
- const totalMinutes = Math.floor(ms / 6e4);
565
- const hours = Math.floor(totalMinutes / 60);
566
- const minutes = totalMinutes % 60;
567
- return `${hours}h ${minutes}m`;
568
- }
569
667
  function formatTaskDetailMarkdown(task) {
570
668
  const lines = [`# ${task.name}`, ""];
571
669
  const isInitiative = (task.custom_item_id ?? 0) !== 0;
@@ -581,8 +679,8 @@ function formatTaskDetailMarkdown(task) {
581
679
  ],
582
680
  ["Priority", task.priority?.priority],
583
681
  ["Parent", task.parent ?? void 0],
584
- ["Start Date", task.start_date ? formatDate(task.start_date) : void 0],
585
- ["Due Date", task.due_date ? formatDate(task.due_date) : void 0],
682
+ ["Start Date", task.start_date ? formatDateISO(task.start_date) : void 0],
683
+ ["Due Date", task.due_date ? formatDateISO(task.due_date) : void 0],
586
684
  [
587
685
  "Time Estimate",
588
686
  task.time_estimate != null && task.time_estimate > 0 ? formatDuration(task.time_estimate) : void 0
@@ -592,8 +690,8 @@ function formatTaskDetailMarkdown(task) {
592
690
  task.time_spent != null && task.time_spent > 0 ? formatDuration(task.time_spent) : void 0
593
691
  ],
594
692
  ["Tags", task.tags && task.tags.length > 0 ? task.tags.map((t) => t.name).join(", ") : void 0],
595
- ["Created", task.date_created ? formatDate(task.date_created) : void 0],
596
- ["Updated", task.date_updated ? formatDate(task.date_updated) : void 0]
693
+ ["Created", task.date_created ? formatDateISO(task.date_created) : void 0],
694
+ ["Updated", task.date_updated ? formatDateISO(task.date_updated) : void 0]
597
695
  ];
598
696
  for (const [label, value] of fields) {
599
697
  if (value != null && value !== "") {
@@ -673,20 +771,6 @@ function openUrl(url) {
673
771
  `);
674
772
  }
675
773
  }
676
- function formatMs(ms) {
677
- const hours = Math.floor(ms / 36e5);
678
- const mins = Math.floor(ms % 36e5 / 6e4);
679
- if (hours > 0 && mins > 0) return `${hours}h ${mins}m`;
680
- if (hours > 0) return `${hours}h`;
681
- return `${mins}m`;
682
- }
683
- function formatTimestamp(ts) {
684
- return new Date(Number(ts)).toLocaleDateString("en-US", {
685
- month: "short",
686
- day: "numeric",
687
- year: "numeric"
688
- });
689
- }
690
774
  function descriptionPreview(text, maxLines = 3) {
691
775
  const lines = text.split("\n").filter((l) => l.trim().length > 0);
692
776
  const preview = lines.slice(0, maxLines);
@@ -746,10 +830,10 @@ function formatTaskDetail(task) {
746
830
  task.assignees?.length ? task.assignees.map((a) => a.username).join(", ") : void 0
747
831
  ],
748
832
  ["Priority", task.priority?.priority],
749
- ["Start", task.start_date ? formatTimestamp(task.start_date) : void 0],
750
- ["Due", task.due_date ? formatTimestamp(task.due_date) : void 0],
751
- ["Estimate", task.time_estimate ? formatMs(task.time_estimate) : void 0],
752
- ["Tracked", task.time_spent ? formatMs(task.time_spent) : void 0],
833
+ ["Start", task.start_date ? formatDate(task.start_date) : void 0],
834
+ ["Due", task.due_date ? formatDate(task.due_date) : void 0],
835
+ ["Estimate", task.time_estimate ? formatDuration(task.time_estimate) : void 0],
836
+ ["Tracked", task.time_spent ? formatDuration(task.time_spent) : void 0],
753
837
  ["Tags", task.tags?.length ? task.tags.map((t) => t.name).join(", ") : void 0],
754
838
  ["Parent", task.parent || void 0],
755
839
  ["URL", task.url]
@@ -1004,7 +1088,8 @@ function parseDueDate(value) {
1004
1088
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1005
1089
  throw new Error("Date must be in YYYY-MM-DD format");
1006
1090
  }
1007
- const date = new Date(value);
1091
+ const parts = value.split("-");
1092
+ const date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
1008
1093
  if (isNaN(date.getTime())) throw new Error(`Invalid date: ${value}`);
1009
1094
  return date.getTime();
1010
1095
  }
@@ -1180,23 +1265,75 @@ async function runInitCommand() {
1180
1265
  }
1181
1266
 
1182
1267
  // src/commands/sprint.ts
1183
- function parseSprintDates(name) {
1268
+ import { select as select2 } from "@inquirer/prompts";
1269
+ var SPRINT_KEYWORDS = ["sprint", "iteration", "cycle", "scrum"];
1270
+ function parseUSDateRange(name) {
1184
1271
  const m = name.match(/\((\d{1,2})\/(\d{1,2})\s*[-–]\s*(\d{1,2})\/(\d{1,2})\)/);
1185
1272
  if (!m) return null;
1186
1273
  const year = (/* @__PURE__ */ new Date()).getFullYear();
1187
1274
  const start = new Date(year, Number(m[1]) - 1, Number(m[2]));
1188
1275
  const end = new Date(year, Number(m[3]) - 1, Number(m[4]), 23, 59, 59);
1189
- if (end < start) {
1190
- end.setFullYear(end.getFullYear() + 1);
1191
- }
1276
+ if (end < start) end.setFullYear(end.getFullYear() + 1);
1277
+ return { start, end };
1278
+ }
1279
+ function parseISODateRange(name) {
1280
+ const m = name.match(/\((\d{4}-\d{2}-\d{2})\s*[-–]\s*(\d{4}-\d{2}-\d{2})\)/);
1281
+ if (!m) return null;
1282
+ const [sy, sm, sd] = m[1].split("-").map(Number);
1283
+ const [ey, em, ed] = m[2].split("-").map(Number);
1284
+ const start = new Date(sy, sm - 1, sd);
1285
+ const end = new Date(ey, em - 1, ed, 23, 59, 59);
1192
1286
  return { start, end };
1193
1287
  }
1288
+ function parseMonthDayRange(name) {
1289
+ const months = {
1290
+ jan: 0,
1291
+ feb: 1,
1292
+ mar: 2,
1293
+ apr: 3,
1294
+ may: 4,
1295
+ jun: 5,
1296
+ jul: 6,
1297
+ aug: 7,
1298
+ sep: 8,
1299
+ oct: 9,
1300
+ nov: 10,
1301
+ dec: 11
1302
+ };
1303
+ const m = name.match(/\(([A-Za-z]{3})\s+(\d{1,2})\s*[-–]\s*([A-Za-z]{3})\s+(\d{1,2})\)/);
1304
+ if (!m) return null;
1305
+ const sm = months[m[1].toLowerCase()];
1306
+ const em = months[m[3].toLowerCase()];
1307
+ if (sm === void 0 || em === void 0) return null;
1308
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
1309
+ const start = new Date(year, sm, Number(m[2]));
1310
+ const end = new Date(year, em, Number(m[4]), 23, 59, 59);
1311
+ if (end < start) end.setFullYear(end.getFullYear() + 1);
1312
+ return { start, end };
1313
+ }
1314
+ function parseEuropeanDateRange(name) {
1315
+ const m = name.match(/\((\d{1,2})\.(\d{1,2})\s*[-–]\s*(\d{1,2})\.(\d{1,2})\)/);
1316
+ if (!m) return null;
1317
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
1318
+ const start = new Date(year, Number(m[2]) - 1, Number(m[1]));
1319
+ const end = new Date(year, Number(m[4]) - 1, Number(m[3]), 23, 59, 59);
1320
+ if (end < start) end.setFullYear(end.getFullYear() + 1);
1321
+ return { start, end };
1322
+ }
1323
+ function parseSprintDates(name) {
1324
+ return parseUSDateRange(name) ?? parseISODateRange(name) ?? parseMonthDayRange(name) ?? parseEuropeanDateRange(name);
1325
+ }
1194
1326
  function findActiveSprintList(lists, today = /* @__PURE__ */ new Date()) {
1195
1327
  if (lists.length === 0) return null;
1196
1328
  for (const list of lists) {
1197
1329
  const dates = parseSprintDates(list.name);
1198
- if (dates && today >= dates.start && today <= dates.end) {
1199
- return list;
1330
+ if (dates && today >= dates.start && today <= dates.end) return list;
1331
+ }
1332
+ for (const list of lists) {
1333
+ if (list.start_date && list.due_date) {
1334
+ const start = new Date(Number(list.start_date));
1335
+ const end = new Date(Number(list.due_date));
1336
+ if (today >= start && today <= end) return list;
1200
1337
  }
1201
1338
  }
1202
1339
  return lists[lists.length - 1] ?? null;
@@ -1216,37 +1353,67 @@ function findRelatedSpaces(mySpaceIds, allSpaces) {
1216
1353
  async function runSprintCommand(config, opts) {
1217
1354
  const client = new ClickUpClient(config);
1218
1355
  process.stderr.write("Detecting active sprint...\n");
1356
+ const folderId = opts.folder ?? config.sprintFolderId;
1219
1357
  const [myTasks, allSpaces, customTypes] = await Promise.all([
1220
1358
  client.getMyTasks(config.teamId),
1221
- client.getSpaces(config.teamId),
1359
+ folderId ? Promise.resolve([]) : client.getSpaces(config.teamId),
1222
1360
  client.getCustomTaskTypes(config.teamId)
1223
1361
  ]);
1224
1362
  const typeMap = buildTypeMap(customTypes);
1225
- let spaces;
1226
- if (opts.space) {
1227
- spaces = allSpaces.filter(
1228
- (s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
1229
- );
1230
- if (spaces.length === 0) {
1231
- throw new Error(
1232
- `No space matching "${opts.space}" found. Use \`cu spaces\` to list available spaces.`
1363
+ let sprintLists;
1364
+ if (folderId) {
1365
+ sprintLists = await client.getFolderLists(folderId);
1366
+ } else {
1367
+ let spaces;
1368
+ if (opts.space) {
1369
+ spaces = allSpaces.filter(
1370
+ (s) => s.name.toLowerCase().includes(opts.space.toLowerCase()) || s.id === opts.space
1371
+ );
1372
+ if (spaces.length === 0) {
1373
+ throw new Error(
1374
+ `No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
1375
+ );
1376
+ }
1377
+ } else {
1378
+ const mySpaceIds = new Set(
1379
+ myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
1233
1380
  );
1381
+ spaces = findRelatedSpaces(mySpaceIds, allSpaces);
1234
1382
  }
1235
- } else {
1236
- const mySpaceIds = new Set(
1237
- myTasks.map((t) => t.space?.id).filter((id) => Boolean(id))
1383
+ const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
1384
+ const sprintFolders = foldersBySpace.flat().filter((f) => {
1385
+ const lower = f.name.toLowerCase();
1386
+ return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
1387
+ });
1388
+ const listsByFolder = await Promise.all(
1389
+ sprintFolders.map((folder) => client.getFolderLists(folder.id))
1238
1390
  );
1239
- spaces = findRelatedSpaces(mySpaceIds, allSpaces);
1391
+ sprintLists = listsByFolder.flat();
1392
+ }
1393
+ let activeList = findActiveSprintList(sprintLists);
1394
+ if (!activeList && sprintLists.length > 1 && isTTY()) {
1395
+ const choice = await select2({
1396
+ message: "Multiple sprint lists found. Which one?",
1397
+ choices: sprintLists.map((l) => ({
1398
+ name: `${l.name} (${l.id})`,
1399
+ value: l
1400
+ }))
1401
+ });
1402
+ activeList = choice;
1403
+ }
1404
+ if (!activeList && sprintLists.length > 1) {
1405
+ process.stderr.write(
1406
+ `Multiple sprint lists found:
1407
+ ${sprintLists.map((l) => ` - ${l.name} (${l.id})`).join("\n")}
1408
+ Using: ${sprintLists[sprintLists.length - 1].name}
1409
+ `
1410
+ );
1411
+ activeList = sprintLists[sprintLists.length - 1] ?? null;
1240
1412
  }
1241
- const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
1242
- const sprintFolders = foldersBySpace.flat().filter((f) => f.name.toLowerCase().includes("sprint"));
1243
- const listsByFolder = await Promise.all(
1244
- sprintFolders.map((folder) => client.getFolderLists(folder.id))
1245
- );
1246
- const sprintLists = listsByFolder.flat();
1247
- const activeList = findActiveSprintList(sprintLists);
1248
1413
  if (!activeList) {
1249
- throw new Error('No sprint list found. Ensure sprint folders contain "sprint" in their name.');
1414
+ throw new Error(
1415
+ 'No sprint list found. Ensure sprint folders contain "sprint", "iteration", "cycle", or "scrum" in their name.'
1416
+ );
1250
1417
  }
1251
1418
  process.stderr.write(`Active sprint: ${activeList.name}
1252
1419
  `);
@@ -1274,7 +1441,7 @@ var SPRINT_COLUMNS = [
1274
1441
  { key: "sprint", label: "SPRINT", maxWidth: 60 },
1275
1442
  { key: "dates", label: "DATES" }
1276
1443
  ];
1277
- function formatDate2(d) {
1444
+ function formatSprintDate(d) {
1278
1445
  return `${d.getMonth() + 1}/${d.getDate()}`;
1279
1446
  }
1280
1447
  function buildSprintInfos(lists, folderName, today) {
@@ -1304,7 +1471,7 @@ async function listSprints(config, opts = {}) {
1304
1471
  );
1305
1472
  if (spaces.length === 0) {
1306
1473
  throw new Error(
1307
- `No space matching "${opts.space}" found. Use \`cu spaces\` to list available spaces.`
1474
+ `No space matching "${opts.space}" found. Use \`cup spaces\` to list available spaces.`
1308
1475
  );
1309
1476
  }
1310
1477
  } else {
@@ -1314,7 +1481,10 @@ async function listSprints(config, opts = {}) {
1314
1481
  spaces = findRelatedSpaces(mySpaceIds, allSpaces);
1315
1482
  }
1316
1483
  const foldersBySpace = await Promise.all(spaces.map((space) => client.getFolders(space.id)));
1317
- const sprintFolders = foldersBySpace.flat().filter((f) => f.name.toLowerCase().includes("sprint"));
1484
+ const sprintFolders = foldersBySpace.flat().filter((f) => {
1485
+ const lower = f.name.toLowerCase();
1486
+ return SPRINT_KEYWORDS.some((kw) => lower.includes(kw));
1487
+ });
1318
1488
  const today = /* @__PURE__ */ new Date();
1319
1489
  const allSprints = [];
1320
1490
  const listsByFolder = await Promise.all(
@@ -1335,7 +1505,7 @@ async function listSprints(config, opts = {}) {
1335
1505
  }
1336
1506
  const rows = allSprints.map((s) => {
1337
1507
  const dates = parseSprintDates(s.name);
1338
- const dateStr = dates ? `${formatDate2(dates.start)} - ${formatDate2(dates.end)}` : "";
1508
+ const dateStr = dates ? `${formatSprintDate(dates.start)} - ${formatSprintDate(dates.end)}` : "";
1339
1509
  return {
1340
1510
  id: s.id,
1341
1511
  sprint: s.active ? `* ${s.name}` : s.name,
@@ -1379,15 +1549,6 @@ async function postComment(config, taskId, text, notifyAll) {
1379
1549
 
1380
1550
  // src/commands/comments.ts
1381
1551
  import chalk3 from "chalk";
1382
- function formatDate3(timestamp) {
1383
- return new Date(Number(timestamp)).toLocaleString("en-US", {
1384
- month: "short",
1385
- day: "numeric",
1386
- year: "numeric",
1387
- hour: "numeric",
1388
- minute: "2-digit"
1389
- });
1390
- }
1391
1552
  async function fetchComments(config, taskId) {
1392
1553
  const client = new ClickUpClient(config);
1393
1554
  const comments = await client.getTaskComments(taskId);
@@ -1415,7 +1576,7 @@ function printComments(comments, forceJson) {
1415
1576
  for (let i = 0; i < comments.length; i++) {
1416
1577
  const c = comments[i];
1417
1578
  if (i > 0) console.log(separator);
1418
- console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatDate3(c.date))}`);
1579
+ console.log(`${chalk3.bold(c.user)} ${chalk3.dim(formatTimestamp(c.date))}`);
1419
1580
  console.log(c.text);
1420
1581
  if (i < comments.length - 1) console.log("");
1421
1582
  }
@@ -1812,7 +1973,7 @@ async function fetchOverdueTasks(config, opts = {}) {
1812
1973
  }
1813
1974
 
1814
1975
  // src/commands/config.ts
1815
- var VALID_KEYS = /* @__PURE__ */ new Set(["apiToken", "teamId"]);
1976
+ var VALID_KEYS = /* @__PURE__ */ new Set(["apiToken", "teamId", "sprintFolderId"]);
1816
1977
  function assertValidKey(key) {
1817
1978
  if (!VALID_KEYS.has(key)) {
1818
1979
  throw new Error(`Unknown config key: ${key}. Valid keys: ${[...VALID_KEYS].join(", ")}`);
@@ -1868,15 +2029,6 @@ async function assignTask(config, taskId, opts) {
1868
2029
 
1869
2030
  // src/commands/activity.ts
1870
2031
  import chalk4 from "chalk";
1871
- function formatDate4(timestamp) {
1872
- return new Date(Number(timestamp)).toLocaleString("en-US", {
1873
- month: "short",
1874
- day: "numeric",
1875
- year: "numeric",
1876
- hour: "numeric",
1877
- minute: "2-digit"
1878
- });
1879
- }
1880
2032
  async function fetchActivity(config, taskId) {
1881
2033
  const client = new ClickUpClient(config);
1882
2034
  const [task, rawComments] = await Promise.all([
@@ -1920,7 +2072,7 @@ ${commentsMd}`);
1920
2072
  console.log("");
1921
2073
  console.log(chalk4.dim("-".repeat(60)));
1922
2074
  }
1923
- console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatDate4(c.date))}`);
2075
+ console.log(`${chalk4.bold(c.user)} ${chalk4.dim(formatTimestamp(c.date))}`);
1924
2076
  console.log(c.text);
1925
2077
  }
1926
2078
  }
@@ -1939,7 +2091,7 @@ function bashCompletion(name) {
1939
2091
  cword=$COMP_CWORD
1940
2092
  fi
1941
2093
 
1942
- local commands="init auth tasks task update create sprint sprints subtasks comment comment-edit comment-delete comments replies reply activity lists spaces inbox assigned open search summary overdue assign depend link attach move field delete tag checklist time config completion"
2094
+ local commands="init auth tasks task update create sprint sprints subtasks comment comment-edit comment-delete comments replies reply activity lists spaces inbox assigned open search summary overdue assign depend link attach move field delete tag checklist time docs doc doc-create doc-page-create doc-page-edit config completion"
1943
2095
 
1944
2096
  if [[ $cword -eq 1 ]]; then
1945
2097
  COMPREPLY=($(compgen -W "$commands --help --version" -- "$cur"))
@@ -1973,7 +2125,7 @@ function bashCompletion(name) {
1973
2125
  COMPREPLY=($(compgen -W "-l --list -n --name -d --description -p --parent -s --status --priority --due-date --assignee --tags --custom-item-id --time-estimate --json" -- "$cur"))
1974
2126
  ;;
1975
2127
  sprint)
1976
- COMPREPLY=($(compgen -W "--status --space --include-closed --json" -- "$cur"))
2128
+ COMPREPLY=($(compgen -W "--status --space --folder --include-closed --json" -- "$cur"))
1977
2129
  ;;
1978
2130
  sprints)
1979
2131
  COMPREPLY=($(compgen -W "--space --json" -- "$cur"))
@@ -2063,6 +2215,21 @@ function bashCompletion(name) {
2063
2215
  attach)
2064
2216
  COMPREPLY=($(compgen -f -- "$cur"))
2065
2217
  ;;
2218
+ docs)
2219
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2220
+ ;;
2221
+ doc)
2222
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2223
+ ;;
2224
+ doc-create)
2225
+ COMPREPLY=($(compgen -W "-c --content --json" -- "$cur"))
2226
+ ;;
2227
+ doc-page-create)
2228
+ COMPREPLY=($(compgen -W "-c --content --parent-page --json" -- "$cur"))
2229
+ ;;
2230
+ doc-page-edit)
2231
+ COMPREPLY=($(compgen -W "--name -c --content --json" -- "$cur"))
2232
+ ;;
2066
2233
  config)
2067
2234
  if [[ $cword -eq 2 ]]; then
2068
2235
  COMPREPLY=($(compgen -W "get set path" -- "$cur"))
@@ -2070,7 +2237,7 @@ function bashCompletion(name) {
2070
2237
  local subcmd="\${words[2]}"
2071
2238
  case "$subcmd" in
2072
2239
  get|set)
2073
- COMPREPLY=($(compgen -W "apiToken teamId" -- "$cur"))
2240
+ COMPREPLY=($(compgen -W "apiToken teamId sprintFolderId" -- "$cur"))
2074
2241
  ;;
2075
2242
  esac
2076
2243
  fi
@@ -2123,6 +2290,11 @@ _${name}() {
2123
2290
  'reply:Reply to a comment'
2124
2291
  'link:Add or remove a link between two tasks'
2125
2292
  'attach:Upload a file attachment to a task'
2293
+ 'docs:List workspace docs'
2294
+ 'doc:View a doc page'
2295
+ 'doc-create:Create a new doc'
2296
+ 'doc-page-create:Create a page in a doc'
2297
+ 'doc-page-edit:Edit a doc page'
2126
2298
  'config:Manage CLI configuration'
2127
2299
  'completion:Output shell completion script'
2128
2300
  )
@@ -2186,6 +2358,7 @@ _${name}() {
2186
2358
  _arguments \\
2187
2359
  '--status[Filter by status]:status:(open "in progress" "in review" done closed)' \\
2188
2360
  '--space[Narrow sprint search to a space]:space:' \\
2361
+ '--folder[Sprint folder ID]:folder_id:' \\
2189
2362
  '--include-closed[Include done/closed tasks]' \\
2190
2363
  '--json[Force JSON output]'
2191
2364
  ;;
@@ -2444,6 +2617,39 @@ _${name}() {
2444
2617
  '2:file_path:_files' \\
2445
2618
  '--json[Force JSON output]'
2446
2619
  ;;
2620
+ docs)
2621
+ _arguments \\
2622
+ '1:query:' \\
2623
+ '--json[Force JSON output]'
2624
+ ;;
2625
+ doc)
2626
+ _arguments \\
2627
+ '1:doc_id:' \\
2628
+ '2:page_id:' \\
2629
+ '--json[Force JSON output]'
2630
+ ;;
2631
+ doc-create)
2632
+ _arguments \\
2633
+ '1:title:' \\
2634
+ '(-c --content)'{-c,--content}'[Initial content]:text:' \\
2635
+ '--json[Force JSON output]'
2636
+ ;;
2637
+ doc-page-create)
2638
+ _arguments \\
2639
+ '1:doc_id:' \\
2640
+ '2:name:' \\
2641
+ '(-c --content)'{-c,--content}'[Page content]:text:' \\
2642
+ '--parent-page[Parent page ID]:page_id:' \\
2643
+ '--json[Force JSON output]'
2644
+ ;;
2645
+ doc-page-edit)
2646
+ _arguments \\
2647
+ '1:doc_id:' \\
2648
+ '2:page_id:' \\
2649
+ '--name[New page name]:text:' \\
2650
+ '(-c --content)'{-c,--content}'[New page content]:text:' \\
2651
+ '--json[Force JSON output]'
2652
+ ;;
2447
2653
  config)
2448
2654
  local -a config_cmds
2449
2655
  config_cmds=(
@@ -2461,7 +2667,7 @@ _${name}() {
2461
2667
  config_args)
2462
2668
  case $words[1] in
2463
2669
  get|set)
2464
- _arguments '1:key:(apiToken teamId)'
2670
+ _arguments '1:key:(apiToken teamId sprintFolderId)'
2465
2671
  ;;
2466
2672
  esac
2467
2673
  ;;
@@ -2518,6 +2724,11 @@ complete -c ${name} -n __fish_use_subcommand -a replies -d 'List threaded replie
2518
2724
  complete -c ${name} -n __fish_use_subcommand -a reply -d 'Reply to a comment'
2519
2725
  complete -c ${name} -n __fish_use_subcommand -a link -d 'Add or remove a link between two tasks'
2520
2726
  complete -c ${name} -n __fish_use_subcommand -a attach -d 'Upload a file attachment to a task'
2727
+ complete -c ${name} -n __fish_use_subcommand -a docs -d 'List workspace docs'
2728
+ complete -c ${name} -n __fish_use_subcommand -a doc -d 'View a doc page'
2729
+ complete -c ${name} -n __fish_use_subcommand -a doc-create -d 'Create a new doc'
2730
+ complete -c ${name} -n __fish_use_subcommand -a doc-page-create -d 'Create a page in a doc'
2731
+ complete -c ${name} -n __fish_use_subcommand -a doc-page-edit -d 'Edit a doc page'
2521
2732
  complete -c ${name} -n __fish_use_subcommand -a config -d 'Manage CLI configuration'
2522
2733
  complete -c ${name} -n __fish_use_subcommand -a completion -d 'Output shell completion script'
2523
2734
 
@@ -2558,6 +2769,7 @@ complete -c ${name} -n '__fish_seen_subcommand_from create' -l json -d 'Force JS
2558
2769
 
2559
2770
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l status -d 'Filter by status'
2560
2771
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l space -d 'Narrow sprint search to a space'
2772
+ complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l folder -d 'Sprint folder ID'
2561
2773
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l include-closed -d 'Include done/closed tasks'
2562
2774
  complete -c ${name} -n '__fish_seen_subcommand_from sprint' -l json -d 'Force JSON output'
2563
2775
 
@@ -2670,10 +2882,25 @@ complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l resolved -d
2670
2882
  complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l unresolved -d 'Mark comment as unresolved'
2671
2883
  complete -c ${name} -n '__fish_seen_subcommand_from comment-edit' -l json -d 'Force JSON output'
2672
2884
 
2885
+ complete -c ${name} -n '__fish_seen_subcommand_from docs' -l json -d 'Force JSON output'
2886
+
2887
+ complete -c ${name} -n '__fish_seen_subcommand_from doc' -l json -d 'Force JSON output'
2888
+
2889
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-create' -s c -l content -d 'Initial content'
2890
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-create' -l json -d 'Force JSON output'
2891
+
2892
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-create' -s c -l content -d 'Page content'
2893
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-create' -l parent-page -d 'Parent page ID'
2894
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-create' -l json -d 'Force JSON output'
2895
+
2896
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-edit' -l name -d 'New page name'
2897
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-edit' -s c -l content -d 'New page content'
2898
+ complete -c ${name} -n '__fish_seen_subcommand_from doc-page-edit' -l json -d 'Force JSON output'
2899
+
2673
2900
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a get -d 'Print a config value'
2674
2901
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a set -d 'Set a config value'
2675
2902
  complete -c ${name} -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set path' -a path -d 'Print config file path'
2676
- complete -c ${name} -n '__fish_seen_subcommand_from get set' -a 'apiToken teamId' -d 'Config key'
2903
+ complete -c ${name} -n '__fish_seen_subcommand_from get set' -a 'apiToken teamId sprintFolderId' -d 'Config key'
2677
2904
 
2678
2905
  complete -c ${name} -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell type'
2679
2906
  `;
@@ -2969,6 +3196,17 @@ function formatChecklists(checklists) {
2969
3196
  }
2970
3197
  return lines.join("\n");
2971
3198
  }
3199
+ function formatChecklistsMarkdown(checklists) {
3200
+ if (checklists.length === 0) return "No checklists";
3201
+ return checklists.map((cl) => {
3202
+ const resolved = cl.items.filter((i) => i.resolved).length;
3203
+ const header = `### ${cl.name} (${resolved}/${cl.items.length})`;
3204
+ const items = cl.items.map(
3205
+ (item) => `- [${item.resolved ? "x" : " "}] ${item.name}`
3206
+ );
3207
+ return [header, "", ...items].join("\n");
3208
+ }).join("\n\n");
3209
+ }
2972
3210
 
2973
3211
  // src/commands/comment-edit.ts
2974
3212
  async function editComment(config, commentId, text, resolved) {
@@ -2998,11 +3236,21 @@ function formatReplies(replies) {
2998
3236
  if (replies.length === 0) return "No replies";
2999
3237
  return replies.map((r) => {
3000
3238
  const user = r.user?.username ?? "Unknown";
3001
- const date = new Date(Number(r.date)).toLocaleString();
3239
+ const date = formatTimestamp(Number(r.date));
3002
3240
  return `${chalk6.bold(user)} ${chalk6.dim(date)}
3003
3241
  ${r.comment_text}`;
3004
3242
  }).join("\n\n");
3005
3243
  }
3244
+ function formatRepliesMarkdown(replies) {
3245
+ if (replies.length === 0) return "No replies";
3246
+ return replies.map((r) => {
3247
+ const user = r.user?.username ?? "Unknown";
3248
+ const date = formatTimestamp(Number(r.date));
3249
+ return `**${user}** (${date})
3250
+
3251
+ ${r.comment_text}`;
3252
+ }).join("\n\n---\n\n");
3253
+ }
3006
3254
 
3007
3255
  // src/commands/link.ts
3008
3256
  async function manageTaskLink(config, taskId, linksTo, remove) {
@@ -3027,24 +3275,52 @@ async function attachFile(config, taskId, filePath) {
3027
3275
  return client.createTaskAttachment(taskId, filePath);
3028
3276
  }
3029
3277
 
3030
- // src/commands/time.ts
3278
+ // src/commands/docs.ts
3031
3279
  import chalk7 from "chalk";
3032
- function formatDuration2(ms) {
3033
- const totalMinutes = Math.round(Math.abs(ms) / 6e4);
3034
- const hours = Math.floor(totalMinutes / 60);
3035
- const minutes = totalMinutes % 60;
3036
- if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
3037
- if (hours > 0) return `${hours}h`;
3038
- return `${minutes}m`;
3280
+ async function listDocs(config, query) {
3281
+ const client = new ClickUpClient(config);
3282
+ const docs = await client.getDocs(config.teamId);
3283
+ if (query) {
3284
+ const lower = query.toLowerCase();
3285
+ return docs.filter((d) => d.name.toLowerCase().includes(lower));
3286
+ }
3287
+ return docs;
3039
3288
  }
3040
- function formatTimestamp2(ms) {
3041
- return new Date(Number(ms)).toLocaleString("en-US", {
3042
- month: "short",
3043
- day: "numeric",
3044
- hour: "numeric",
3045
- minute: "2-digit"
3046
- });
3289
+ function formatDocs(docs) {
3290
+ if (docs.length === 0) return "No docs found";
3291
+ return docs.map((d) => `${chalk7.bold(d.name)} ${chalk7.dim(d.id)}`).join("\n");
3292
+ }
3293
+ function formatDocsMarkdown(docs) {
3294
+ if (docs.length === 0) return "No docs found";
3295
+ return docs.map((d) => `- **${d.name}** (${d.id})`).join("\n");
3296
+ }
3297
+
3298
+ // src/commands/doc.ts
3299
+ async function getDocPage(config, docId, pageId) {
3300
+ const client = new ClickUpClient(config);
3301
+ return client.getDocPage(config.teamId, docId, pageId);
3302
+ }
3303
+ async function createDoc(config, title, content) {
3304
+ if (!title.trim()) throw new Error("Doc title cannot be empty");
3305
+ const client = new ClickUpClient(config);
3306
+ const doc = await client.createDoc(config.teamId, title, content);
3307
+ return { id: doc.id, title: doc.name ?? title };
3047
3308
  }
3309
+ async function createDocPage(config, docId, name, content, parentPageId) {
3310
+ if (!name.trim()) throw new Error("Page name cannot be empty");
3311
+ const client = new ClickUpClient(config);
3312
+ return client.createDocPage(config.teamId, docId, name, content, parentPageId);
3313
+ }
3314
+ async function editDocPage(config, docId, pageId, updates) {
3315
+ if (!updates.name && !updates.content) {
3316
+ throw new Error("Provide --name or --content to update");
3317
+ }
3318
+ const client = new ClickUpClient(config);
3319
+ return client.editDocPage(config.teamId, docId, pageId, updates);
3320
+ }
3321
+
3322
+ // src/commands/time.ts
3323
+ import chalk8 from "chalk";
3048
3324
  async function startTimer(config, taskId, description) {
3049
3325
  const client = new ClickUpClient(config);
3050
3326
  return client.startTimeEntry(config.teamId, taskId, description);
@@ -3079,11 +3355,11 @@ function formatTimeEntry(entry) {
3079
3355
  const taskId = entry.task?.id ?? "";
3080
3356
  const isRunning = entry.duration < 0;
3081
3357
  const elapsed = isRunning ? Date.now() - Number(entry.start) : entry.duration;
3082
- const durationStr = formatDuration2(elapsed);
3083
- const status = isRunning ? chalk7.green("RUNNING") : "";
3084
- lines.push(`${chalk7.bold(taskName)} ${chalk7.dim(taskId)} ${status}`);
3358
+ const durationStr = formatDuration(elapsed);
3359
+ const status = isRunning ? chalk8.green("RUNNING") : "";
3360
+ lines.push(`${chalk8.bold(taskName)} ${chalk8.dim(taskId)} ${status}`);
3085
3361
  lines.push(
3086
- ` ${durationStr} - ${formatTimestamp2(entry.start)}${entry.description ? ` - ${entry.description}` : ""}`
3362
+ ` ${durationStr} - ${formatTimestamp(entry.start)}${entry.description ? ` - ${entry.description}` : ""}`
3087
3363
  );
3088
3364
  return lines.join("\n");
3089
3365
  }
@@ -3091,6 +3367,19 @@ function formatTimeEntries(entries) {
3091
3367
  if (entries.length === 0) return "No time entries";
3092
3368
  return entries.map(formatTimeEntry).join("\n");
3093
3369
  }
3370
+ function formatTimeEntryMarkdown(entry) {
3371
+ const taskName = entry.task?.name ?? "No task";
3372
+ const taskId = entry.task?.id ?? "";
3373
+ const isRunning = entry.duration < 0;
3374
+ const elapsed = isRunning ? Date.now() - Number(entry.start) : entry.duration;
3375
+ const durationStr = formatDuration(elapsed);
3376
+ const status = isRunning ? " (RUNNING)" : "";
3377
+ return `**${taskName}** ${taskId}${status} - ${durationStr}${entry.description ? ` - ${entry.description}` : ""}`;
3378
+ }
3379
+ function formatTimeEntriesMarkdown(entries) {
3380
+ if (entries.length === 0) return "No time entries";
3381
+ return entries.map(formatTimeEntryMarkdown).join("\n");
3382
+ }
3094
3383
 
3095
3384
  // src/index.ts
3096
3385
  var require2 = createRequire(import.meta.url);
@@ -3185,7 +3474,7 @@ program.command("create").description("Create a new task").option("-l, --list <l
3185
3474
  }
3186
3475
  })
3187
3476
  );
3188
- program.command("sprint").description("List my tasks in the current active sprint (auto-detected)").option("--status <status>", "Filter by status").option("--space <nameOrId>", "Narrow sprint search to a specific space (partial name or ID)").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
3477
+ program.command("sprint").description("List my tasks in the current active sprint (auto-detected)").option("--status <status>", "Filter by status").option("--space <nameOrId>", "Narrow sprint search to a specific space (partial name or ID)").option("--folder <folderId>", "Sprint folder ID (overrides config and auto-detection)").option("--include-closed", "Include done/closed tasks").option("--json", "Force JSON output even in terminal").action(
3189
3478
  wrapAction(
3190
3479
  async (opts) => {
3191
3480
  const config = loadConfig();
@@ -3269,8 +3558,10 @@ program.command("replies <commentId>").description("List threaded replies on a c
3269
3558
  const replies = await getReplies(config, commentId);
3270
3559
  if (shouldOutputJson(opts.json ?? false)) {
3271
3560
  console.log(JSON.stringify(replies, null, 2));
3272
- } else {
3561
+ } else if (isTTY()) {
3273
3562
  console.log(formatReplies(replies));
3563
+ } else {
3564
+ console.log(formatRepliesMarkdown(replies));
3274
3565
  }
3275
3566
  })
3276
3567
  );
@@ -3490,8 +3781,10 @@ checklistCmd.command("view <taskId>").description("View checklists on a task").o
3490
3781
  const checklists = await viewChecklists(config, taskId);
3491
3782
  if (shouldOutputJson(opts.json ?? false)) {
3492
3783
  console.log(JSON.stringify(checklists, null, 2));
3493
- } else {
3784
+ } else if (isTTY()) {
3494
3785
  console.log(formatChecklists(checklists));
3786
+ } else {
3787
+ console.log(formatChecklistsMarkdown(checklists));
3495
3788
  }
3496
3789
  })
3497
3790
  );
@@ -3578,8 +3871,10 @@ timeCmd.command("stop").description("Stop the running timer").option("--json", "
3578
3871
  const result = await stopTimer(config);
3579
3872
  if (shouldOutputJson(opts.json ?? false)) {
3580
3873
  console.log(JSON.stringify(result, null, 2));
3581
- } else {
3874
+ } else if (isTTY()) {
3582
3875
  console.log(formatTimeEntry(result));
3876
+ } else {
3877
+ console.log(formatTimeEntryMarkdown(result));
3583
3878
  }
3584
3879
  })
3585
3880
  );
@@ -3589,10 +3884,12 @@ timeCmd.command("status").description("Show the currently running timer").option
3589
3884
  const result = await timerStatus(config);
3590
3885
  if (shouldOutputJson(opts.json ?? false)) {
3591
3886
  console.log(JSON.stringify(result, null, 2));
3592
- } else if (result) {
3887
+ } else if (!result) {
3888
+ console.log("No timer running");
3889
+ } else if (isTTY()) {
3593
3890
  console.log(formatTimeEntry(result));
3594
3891
  } else {
3595
- console.log("No timer running");
3892
+ console.log(formatTimeEntryMarkdown(result));
3596
3893
  }
3597
3894
  })
3598
3895
  );
@@ -3619,11 +3916,79 @@ timeCmd.command("list").description("List recent time entries (default: last 7 d
3619
3916
  const entries = await listTimeEntries(config, { days, taskId: opts.task });
3620
3917
  if (shouldOutputJson(opts.json ?? false)) {
3621
3918
  console.log(JSON.stringify(entries, null, 2));
3622
- } else {
3919
+ } else if (isTTY()) {
3623
3920
  console.log(formatTimeEntries(entries));
3921
+ } else {
3922
+ console.log(formatTimeEntriesMarkdown(entries));
3923
+ }
3924
+ })
3925
+ );
3926
+ program.command("docs [query]").description("List workspace docs (optionally filter by name)").option("--json", "Force JSON output even in terminal").action(
3927
+ wrapAction(async (query, opts) => {
3928
+ const config = loadConfig();
3929
+ const docs = await listDocs(config, query);
3930
+ if (shouldOutputJson(opts.json ?? false)) {
3931
+ console.log(JSON.stringify(docs, null, 2));
3932
+ } else if (isTTY()) {
3933
+ console.log(formatDocs(docs));
3934
+ } else {
3935
+ console.log(formatDocsMarkdown(docs));
3624
3936
  }
3625
3937
  })
3626
3938
  );
3939
+ program.command("doc <docId> <pageId>").description("View a doc page").option("--json", "Force JSON output even in terminal").action(
3940
+ wrapAction(async (docId, pageId, opts) => {
3941
+ const config = loadConfig();
3942
+ const page = await getDocPage(config, docId, pageId);
3943
+ if (shouldOutputJson(opts.json ?? false)) {
3944
+ console.log(JSON.stringify(page, null, 2));
3945
+ } else {
3946
+ if (page.name) console.log(`# ${page.name}
3947
+ `);
3948
+ console.log(page.content ?? "");
3949
+ }
3950
+ })
3951
+ );
3952
+ program.command("doc-create <title>").description("Create a new doc").option("-c, --content <text>", "Initial content (markdown)").option("--json", "Force JSON output even in terminal").action(
3953
+ wrapAction(async (title, opts) => {
3954
+ const config = loadConfig();
3955
+ const result = await createDoc(config, title, opts.content);
3956
+ if (shouldOutputJson(opts.json ?? false)) {
3957
+ console.log(JSON.stringify(result, null, 2));
3958
+ } else {
3959
+ console.log(`Created doc "${result.title}" (${result.id})`);
3960
+ }
3961
+ })
3962
+ );
3963
+ program.command("doc-page-create <docId> <name>").description("Create a page in a doc").option("-c, --content <text>", "Page content (markdown)").option("--parent-page <pageId>", "Parent page ID for nesting").option("--json", "Force JSON output even in terminal").action(
3964
+ wrapAction(
3965
+ async (docId, name, opts) => {
3966
+ const config = loadConfig();
3967
+ const page = await createDocPage(config, docId, name, opts.content, opts.parentPage);
3968
+ if (shouldOutputJson(opts.json ?? false)) {
3969
+ console.log(JSON.stringify(page, null, 2));
3970
+ } else {
3971
+ console.log(`Created page "${page.name}" (${page.id}) in doc ${docId}`);
3972
+ }
3973
+ }
3974
+ )
3975
+ );
3976
+ program.command("doc-page-edit <docId> <pageId>").description("Edit a doc page").option("--name <text>", "New page name").option("-c, --content <text>", "New page content (markdown)").option("--json", "Force JSON output even in terminal").action(
3977
+ wrapAction(
3978
+ async (docId, pageId, opts) => {
3979
+ const config = loadConfig();
3980
+ const page = await editDocPage(config, docId, pageId, {
3981
+ name: opts.name,
3982
+ content: opts.content
3983
+ });
3984
+ if (shouldOutputJson(opts.json ?? false)) {
3985
+ console.log(JSON.stringify(page, null, 2));
3986
+ } else {
3987
+ console.log(`Updated page "${page.name}" (${page.id})`);
3988
+ }
3989
+ }
3990
+ )
3991
+ );
3627
3992
  var configCmd = program.command("config").description("Manage CLI configuration");
3628
3993
  configCmd.command("get <key>").description("Print a config value").action(
3629
3994
  wrapAction(async (key) => {