exponential-cli 1.9.0 → 1.11.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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command19 } from "commander";
4
+ import { Command as Command22 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { Command } from "commander";
@@ -752,30 +752,59 @@ function outputStagesJson(stages) {
752
752
  total: stages.length
753
753
  }, null, 2));
754
754
  }
755
- function transformComment(comment) {
756
- const parentKey = "actionId" in comment ? "actionId" : "ticketId";
757
- const parentId = comment[parentKey];
755
+ var COMMENT_PARENT_KEYS = ["actionId", "ticketId", "goalId", "featureId", "pageId"];
756
+ function normalizeComment(comment) {
757
+ const raw = comment;
758
+ const parentKey = COMMENT_PARENT_KEYS.find((k) => k in raw) ?? "parentId";
759
+ const author = raw.author ?? raw.createdBy ?? null;
758
760
  return {
759
761
  id: comment.id,
760
- [parentKey]: parentId,
761
- authorId: comment.authorId,
762
- content: comment.content,
763
- author: comment.author,
764
- createdAt: new Date(comment.createdAt).toISOString(),
765
- updatedAt: new Date(comment.updatedAt).toISOString()
762
+ parentKey,
763
+ parentId: raw[parentKey],
764
+ authorId: raw.authorId ?? raw.createdById,
765
+ author,
766
+ content: raw.content ?? raw.body ?? "",
767
+ createdAt: new Date(comment.createdAt),
768
+ updatedAt: new Date(comment.updatedAt),
769
+ parentCommentId: raw.parentId ?? null,
770
+ threadId: raw.threadId ?? null,
771
+ resolvedAt: raw.resolvedAt ? new Date(raw.resolvedAt) : null
772
+ };
773
+ }
774
+ function transformComment(comment) {
775
+ const n = normalizeComment(comment);
776
+ const out = {
777
+ id: n.id,
778
+ [n.parentKey]: n.parentId,
779
+ authorId: n.authorId,
780
+ content: n.content,
781
+ author: n.author,
782
+ createdAt: n.createdAt.toISOString(),
783
+ updatedAt: n.updatedAt.toISOString()
766
784
  };
785
+ if (n.threadId !== null) out.threadId = n.threadId;
786
+ if (n.parentCommentId !== null && n.parentKey !== "parentId") {
787
+ out.parentCommentId = n.parentCommentId;
788
+ }
789
+ if (n.resolvedAt) out.resolvedAt = n.resolvedAt.toISOString();
790
+ return out;
767
791
  }
768
792
  function outputCommentJson(comment) {
769
793
  console.log(JSON.stringify(transformComment(comment), null, 2));
770
794
  }
771
795
  function outputCommentPretty(comment) {
772
- const author = comment.author?.name ?? comment.authorId;
773
- const when = formatDate(new Date(comment.createdAt));
796
+ const n = normalizeComment(comment);
797
+ const author = n.author?.name ?? n.authorId;
798
+ const when = formatDate(n.createdAt);
799
+ const badges = [
800
+ n.parentCommentId && n.parentKey !== "parentId" ? chalk3.gray("(reply)") : "",
801
+ n.resolvedAt ? chalk3.green("(resolved)") : ""
802
+ ].filter(Boolean).join(" ");
774
803
  console.log(chalk3.gray("\u2500".repeat(50)));
775
- console.log(`${chalk3.bold(author)} ${chalk3.gray(`\u2014 ${when}`)}`);
776
- console.log(chalk3.gray(` ID: ${comment.id}`));
804
+ console.log(`${chalk3.bold(author)} ${chalk3.gray(`\u2014 ${when}`)}${badges ? ` ${badges}` : ""}`);
805
+ console.log(chalk3.gray(` ID: ${n.id}`));
777
806
  console.log();
778
- console.log(comment.content);
807
+ console.log(n.content);
779
808
  console.log();
780
809
  }
781
810
  function outputCommentsJson(comments) {
@@ -1382,6 +1411,343 @@ Scopes (${scopes.length} total)`));
1382
1411
  }
1383
1412
  console.log();
1384
1413
  }
1414
+ function getGoalStatusColor(status) {
1415
+ switch (status) {
1416
+ case "planned":
1417
+ return "blue";
1418
+ case "active":
1419
+ return "yellow";
1420
+ case "completed":
1421
+ return "green";
1422
+ case "archived":
1423
+ return "gray";
1424
+ case "on-hold":
1425
+ return "red";
1426
+ default:
1427
+ return "gray";
1428
+ }
1429
+ }
1430
+ function getKeyResultStatusColor(status) {
1431
+ switch (status) {
1432
+ case "not-started":
1433
+ return "gray";
1434
+ case "on-track":
1435
+ return "green";
1436
+ case "at-risk":
1437
+ return "yellow";
1438
+ case "off-track":
1439
+ return "red";
1440
+ case "achieved":
1441
+ return "green";
1442
+ default:
1443
+ return "gray";
1444
+ }
1445
+ }
1446
+ function keyResultProgress(kr) {
1447
+ const range = kr.targetValue - kr.startValue;
1448
+ if (range <= 0) return 0;
1449
+ return Math.max(
1450
+ 0,
1451
+ Math.min(100, Math.round((kr.currentValue - kr.startValue) / range * 100))
1452
+ );
1453
+ }
1454
+ function transformGoal(goal) {
1455
+ return {
1456
+ id: goal.id,
1457
+ title: goal.title,
1458
+ description: goal.description,
1459
+ status: goal.status,
1460
+ period: goal.period,
1461
+ health: goal.healthOverride ?? goal.health,
1462
+ // Effective progress: manual override, else the key-result mean, else null
1463
+ // for "no signal". Undefined (rather than null) means the server predates
1464
+ // the field — report null rather than inventing a 0.
1465
+ progress: goal.resolvedProgress ?? null,
1466
+ isProgressManual: goal.isProgressManual ?? false,
1467
+ dueDate: goal.dueDate ? new Date(goal.dueDate).toISOString() : null,
1468
+ workspaceId: goal.workspaceId,
1469
+ workspace: goal.workspace ? { id: goal.workspace.id, slug: goal.workspace.slug, name: goal.workspace.name } : null,
1470
+ parentGoalId: goal.parentGoalId,
1471
+ driUserId: goal.driUserId,
1472
+ lifeDomain: goal.lifeDomain ? { id: goal.lifeDomain.id, title: goal.lifeDomain.title } : null,
1473
+ // THE JOIN: actions carry a projectId and goals carry projects, so inverting
1474
+ // this map is what ties a day's work back to an objective. Never elided.
1475
+ projects: goal.projects?.map((p) => ({ id: p.id, name: p.name, slug: p.slug ?? null })) ?? [],
1476
+ childGoals: goal.childGoals?.map((c) => ({ id: c.id, title: c.title, status: c.status })) ?? [],
1477
+ keyResultCount: goal._count?.keyResults ?? goal.keyResults?.length ?? null,
1478
+ createdAt: goal.createdAt ? new Date(goal.createdAt).toISOString() : null,
1479
+ updatedAt: goal.updatedAt ? new Date(goal.updatedAt).toISOString() : null
1480
+ };
1481
+ }
1482
+ function outputGoalJson(goal) {
1483
+ console.log(JSON.stringify(transformGoal(goal), null, 2));
1484
+ }
1485
+ function outputGoalPretty(goal) {
1486
+ console.log(chalk3.gray("\u2500".repeat(50)));
1487
+ const badge = chalk3[getGoalStatusColor(goal.status)](`[${goal.status}]`);
1488
+ console.log(`
1489
+ ${badge} ${chalk3.bold(goal.title)} ${chalk3.gray(`#${goal.id}`)}`);
1490
+ if (goal.period) console.log(` ${chalk3.gray("Period:")} ${goal.period}`);
1491
+ const health = goal.healthOverride ?? goal.health;
1492
+ if (health) console.log(` ${chalk3.gray("Health:")} ${health}`);
1493
+ if (goal.description) {
1494
+ console.log(` ${chalk3.gray("Description:")} ${goal.description.substring(0, 200)}${goal.description.length > 200 ? "..." : ""}`);
1495
+ }
1496
+ if (goal.parentGoal) {
1497
+ console.log(` ${chalk3.gray("Parent:")} ${goal.parentGoal.title} ${chalk3.gray(`#${goal.parentGoal.id}`)}`);
1498
+ }
1499
+ if (goal.projects?.length) {
1500
+ console.log(` ${chalk3.cyan("Projects:")} ${goal.projects.map((p) => p.name).join(", ")}`);
1501
+ }
1502
+ if (goal.childGoals?.length) {
1503
+ console.log(` ${chalk3.cyan("Sub-goals:")} ${goal.childGoals.length}`);
1504
+ }
1505
+ console.log();
1506
+ }
1507
+ function outputGoalsJson(goals) {
1508
+ console.log(JSON.stringify({
1509
+ goals: goals.map(transformGoal),
1510
+ total: goals.length
1511
+ }, null, 2));
1512
+ }
1513
+ function outputGoalsPretty(goals) {
1514
+ if (goals.length === 0) {
1515
+ console.log(chalk3.gray("No goals found."));
1516
+ return;
1517
+ }
1518
+ console.log(chalk3.bold(`
1519
+ Goals (${goals.length} total)`));
1520
+ console.log(chalk3.gray("\u2500".repeat(50)));
1521
+ for (const g of goals) {
1522
+ const badge = chalk3[getGoalStatusColor(g.status)](`[${g.status}]`);
1523
+ const period = g.period ? chalk3.gray(` ${g.period}`) : "";
1524
+ const krs = g._count?.keyResults ? chalk3.gray(` \u2014 ${g._count.keyResults} KRs`) : "";
1525
+ console.log(` ${badge}${period} ${chalk3.bold(g.title)}${krs}`);
1526
+ console.log(chalk3.gray(` ID: ${g.id}`));
1527
+ }
1528
+ console.log();
1529
+ }
1530
+ function transformGoalTree(node) {
1531
+ return {
1532
+ ...transformGoal(node),
1533
+ keyResults: node.keyResults?.map((kr) => ({
1534
+ id: kr.id,
1535
+ status: kr.status,
1536
+ currentValue: kr.currentValue,
1537
+ targetValue: kr.targetValue
1538
+ })) ?? [],
1539
+ children: node.childGoals?.map(transformGoalTree) ?? []
1540
+ };
1541
+ }
1542
+ function outputGoalTreeJson(nodes) {
1543
+ console.log(JSON.stringify({
1544
+ goals: nodes.map(transformGoalTree),
1545
+ total: nodes.length
1546
+ }, null, 2));
1547
+ }
1548
+ function outputGoalTreePretty(nodes) {
1549
+ if (nodes.length === 0) {
1550
+ console.log(chalk3.gray("No goals found."));
1551
+ return;
1552
+ }
1553
+ console.log(chalk3.bold("\nGoal tree"));
1554
+ console.log(chalk3.gray("\u2500".repeat(50)));
1555
+ const walk = (node, depth) => {
1556
+ const pad = " ".repeat(depth + 1);
1557
+ const badge = chalk3[getGoalStatusColor(node.status)](`[${node.status}]`);
1558
+ const period = node.period ? chalk3.gray(` ${node.period}`) : "";
1559
+ const krs = node.keyResults?.length ? chalk3.gray(` \u2014 ${node.keyResults.length} KRs`) : "";
1560
+ console.log(`${pad}${depth > 0 ? "\u2514 " : ""}${badge}${period} ${chalk3.bold(node.title)} ${chalk3.gray(`#${node.id}`)}${krs}`);
1561
+ for (const child of node.childGoals ?? []) {
1562
+ walk(child, depth + 1);
1563
+ }
1564
+ };
1565
+ for (const node of nodes) walk(node, 0);
1566
+ console.log();
1567
+ }
1568
+ function outputGoalPeriodsJson(periods) {
1569
+ console.log(JSON.stringify({ periods, total: periods.length }, null, 2));
1570
+ }
1571
+ function outputGoalPeriodsPretty(periods) {
1572
+ console.log(chalk3.bold(`
1573
+ Periods (${periods.length} total)`));
1574
+ console.log(chalk3.gray("\u2500".repeat(50)));
1575
+ for (const p of periods) {
1576
+ console.log(` ${chalk3.bold(p.value)} ${chalk3.gray(p.label)}`);
1577
+ }
1578
+ console.log();
1579
+ }
1580
+ function outputGoalStatsJson(stats) {
1581
+ console.log(JSON.stringify({
1582
+ ...stats,
1583
+ periodEndDate: stats.periodEndDate ? new Date(stats.periodEndDate).toISOString() : null
1584
+ }, null, 2));
1585
+ }
1586
+ function outputGoalStatsPretty(stats) {
1587
+ console.log(chalk3.bold("\nOKR stats"));
1588
+ console.log(chalk3.gray("\u2500".repeat(50)));
1589
+ console.log(` ${chalk3.gray("Objectives:")} ${stats.totalObjectives}`);
1590
+ console.log(` ${chalk3.gray("Key results:")} ${stats.totalKeyResults} (${stats.completedKeyResults} achieved)`);
1591
+ console.log(
1592
+ ` ${chalk3.gray("Breakdown:")} ${chalk3.green(`${stats.statusBreakdown.onTrack} on-track`)}, ${chalk3.yellow(`${stats.statusBreakdown.atRisk} at-risk`)}, ${chalk3.red(`${stats.statusBreakdown.offTrack} off-track`)}, ${chalk3.green(`${stats.statusBreakdown.achieved} achieved`)}`
1593
+ );
1594
+ console.log(` ${chalk3.gray("Avg progress:")} ${stats.averageProgress}%`);
1595
+ if (stats.averageConfidence != null) {
1596
+ console.log(` ${chalk3.gray("Avg confidence:")} ${stats.averageConfidence}%`);
1597
+ }
1598
+ if (stats.periodEndDate) {
1599
+ console.log(` ${chalk3.gray("Period ends:")} ${new Date(stats.periodEndDate).toISOString().slice(0, 10)}`);
1600
+ }
1601
+ console.log();
1602
+ }
1603
+ function lastCheckInAt(kr) {
1604
+ if (!kr.checkIns?.length) return null;
1605
+ const latest = kr.checkIns.reduce(
1606
+ (newest, c) => new Date(c.createdAt) > new Date(newest.createdAt) ? c : newest
1607
+ );
1608
+ return new Date(latest.createdAt).toISOString();
1609
+ }
1610
+ function transformKeyResult(kr) {
1611
+ return {
1612
+ id: kr.id,
1613
+ goalId: kr.goalId,
1614
+ goal: kr.goal ? { id: kr.goal.id, title: kr.goal.title } : null,
1615
+ title: kr.title,
1616
+ description: kr.description,
1617
+ status: kr.statusOverride ?? kr.status,
1618
+ statusOverride: kr.statusOverride ?? null,
1619
+ startValue: kr.startValue,
1620
+ currentValue: kr.currentValue,
1621
+ targetValue: kr.targetValue,
1622
+ progress: keyResultProgress(kr),
1623
+ unit: kr.unit,
1624
+ unitLabel: kr.unitLabel ?? null,
1625
+ period: kr.period,
1626
+ periodEnd: kr.periodEnd ? new Date(kr.periodEnd).toISOString() : null,
1627
+ lastCheckInAt: lastCheckInAt(kr),
1628
+ confidence: kr.confidence ?? null,
1629
+ driUserId: kr.driUserId,
1630
+ workspaceId: kr.workspaceId,
1631
+ projects: kr.projects?.map((l) => ({ id: l.projectId, name: l.project?.name ?? null })) ?? [],
1632
+ features: kr.features?.map((l) => ({ id: l.featureId, name: l.feature?.name ?? null })) ?? [],
1633
+ createdAt: kr.createdAt ? new Date(kr.createdAt).toISOString() : null,
1634
+ updatedAt: kr.updatedAt ? new Date(kr.updatedAt).toISOString() : null
1635
+ };
1636
+ }
1637
+ function outputKeyResultJson(kr) {
1638
+ console.log(JSON.stringify(transformKeyResult(kr), null, 2));
1639
+ }
1640
+ function outputKeyResultPretty(kr) {
1641
+ const status = kr.statusOverride ?? kr.status;
1642
+ const badge = chalk3[getKeyResultStatusColor(status)](`[${status}]`);
1643
+ console.log(` ${badge} ${chalk3.bold(kr.title)}`);
1644
+ console.log(
1645
+ chalk3.gray(
1646
+ ` ${kr.currentValue}/${kr.targetValue} ${kr.unit} (${keyResultProgress(kr)}%)${kr.period ? ` \xB7 ${kr.period}` : ""} \xB7 objective #${kr.goalId}`
1647
+ )
1648
+ );
1649
+ console.log(chalk3.gray(` ID: ${kr.id}`));
1650
+ if (kr.projects?.length) {
1651
+ console.log(chalk3.gray(` Projects: ${kr.projects.map((l) => l.project?.name ?? l.projectId).join(", ")}`));
1652
+ }
1653
+ if (kr.features?.length) {
1654
+ console.log(chalk3.gray(` Features: ${kr.features.map((l) => l.feature?.name ?? l.featureId).join(", ")}`));
1655
+ }
1656
+ }
1657
+ function outputKeyResultsJson(keyResults) {
1658
+ console.log(JSON.stringify({
1659
+ keyResults: keyResults.map(transformKeyResult),
1660
+ total: keyResults.length
1661
+ }, null, 2));
1662
+ }
1663
+ function outputKeyResultsPretty(keyResults) {
1664
+ if (keyResults.length === 0) {
1665
+ console.log(chalk3.gray("No key results found."));
1666
+ return;
1667
+ }
1668
+ console.log(chalk3.bold(`
1669
+ Key results (${keyResults.length} total)`));
1670
+ console.log(chalk3.gray("\u2500".repeat(50)));
1671
+ for (const kr of keyResults) {
1672
+ outputKeyResultPretty(kr);
1673
+ }
1674
+ console.log();
1675
+ }
1676
+ function outputCheckInJson(checkIn) {
1677
+ console.log(JSON.stringify({
1678
+ id: checkIn.id,
1679
+ keyResultId: checkIn.keyResultId,
1680
+ previousValue: checkIn.previousValue,
1681
+ newValue: checkIn.newValue,
1682
+ notes: checkIn.notes,
1683
+ createdAt: checkIn.createdAt ? new Date(checkIn.createdAt).toISOString() : null
1684
+ }, null, 2));
1685
+ }
1686
+ function outputCheckInPretty(checkIn) {
1687
+ console.log(` ${chalk3.gray("Value:")} ${checkIn.previousValue} \u2192 ${chalk3.bold(String(checkIn.newValue))}`);
1688
+ if (checkIn.notes) console.log(` ${chalk3.gray("Note:")} ${checkIn.notes}`);
1689
+ console.log(chalk3.gray(` Check-in ID: ${checkIn.id}`));
1690
+ console.log();
1691
+ }
1692
+ function transformObjective(objective) {
1693
+ return {
1694
+ id: objective.id,
1695
+ title: objective.title,
1696
+ description: objective.description,
1697
+ status: objective.status,
1698
+ period: objective.period,
1699
+ health: objective.health,
1700
+ progress: objective.progress,
1701
+ statusCounts: objective.statusCounts,
1702
+ workspaceId: objective.workspaceId,
1703
+ workspace: objective.workspace ? {
1704
+ id: objective.workspace.id,
1705
+ slug: objective.workspace.slug,
1706
+ name: objective.workspace.name
1707
+ } : null,
1708
+ parentGoalId: objective.parentGoalId,
1709
+ keyResults: objective.keyResults.map(transformKeyResult)
1710
+ };
1711
+ }
1712
+ function outputObjectivesJson(objectives) {
1713
+ const totalKeyResults = objectives.reduce((n, o) => n + o.keyResults.length, 0);
1714
+ console.log(JSON.stringify({
1715
+ objectives: objectives.map(transformObjective),
1716
+ total: objectives.length,
1717
+ totalKeyResults
1718
+ }, null, 2));
1719
+ }
1720
+ function outputObjectivesPretty(objectives) {
1721
+ if (objectives.length === 0) {
1722
+ console.log(chalk3.gray("No OKRs found."));
1723
+ return;
1724
+ }
1725
+ const totalKeyResults = objectives.reduce((n, o) => n + o.keyResults.length, 0);
1726
+ console.log(
1727
+ chalk3.bold(`
1728
+ OKRs (${objectives.length} objectives, ${totalKeyResults} key results)`)
1729
+ );
1730
+ console.log(chalk3.gray("\u2500".repeat(50)));
1731
+ for (const o of objectives) {
1732
+ const badge = chalk3[getGoalStatusColor(o.status)](`[${o.status}]`);
1733
+ const period = o.period ? chalk3.gray(` ${o.period}`) : "";
1734
+ console.log(
1735
+ `
1736
+ ${badge}${period} ${chalk3.bold(o.title)} ${chalk3.gray(`#${o.id}`)} ${chalk3.gray(`${o.progress}%`)}`
1737
+ );
1738
+ if (o.keyResults.length === 0) {
1739
+ console.log(chalk3.gray(" (no key results)"));
1740
+ continue;
1741
+ }
1742
+ for (const kr of o.keyResults) {
1743
+ const krStatus = kr.statusOverride ?? kr.status;
1744
+ const krBadge = chalk3[getKeyResultStatusColor(krStatus)](`[${krStatus}]`);
1745
+ console.log(` ${krBadge} ${kr.title} ${chalk3.gray(`${kr.currentValue}/${kr.targetValue} ${kr.unit}`)}`);
1746
+ console.log(chalk3.gray(` ID: ${kr.id}`));
1747
+ }
1748
+ }
1749
+ console.log();
1750
+ }
1385
1751
 
1386
1752
  // src/commands/actions.ts
1387
1753
  var VALID_PRIORITIES = [
@@ -2085,6 +2451,9 @@ import { Command as Command5 } from "commander";
2085
2451
 
2086
2452
  // src/utils/resolve.ts
2087
2453
  async function resolveWorkspaceId(client, slugOrId) {
2454
+ return (await resolveWorkspace(client, slugOrId)).id;
2455
+ }
2456
+ async function resolveWorkspace(client, slugOrId) {
2088
2457
  const config = getConfig();
2089
2458
  const candidate = slugOrId ?? config.defaultWorkspaceSlug ?? config.defaultWorkspaceId;
2090
2459
  if (!candidate) {
@@ -2094,9 +2463,9 @@ async function resolveWorkspaceId(client, slugOrId) {
2094
2463
  }
2095
2464
  const workspaces = await client.workspaces.list();
2096
2465
  const byId = workspaces.find((w) => w.id === candidate);
2097
- if (byId) return byId.id;
2466
+ if (byId) return byId;
2098
2467
  const bySlug = workspaces.find((w) => w.slug === candidate);
2099
- if (bySlug) return bySlug.id;
2468
+ if (bySlug) return bySlug;
2100
2469
  const known = workspaces.map((w) => w.slug).join(", ");
2101
2470
  throw new Error(
2102
2471
  `Workspace "${candidate}" not found. Available: ${known || "(none)"}`
@@ -2235,7 +2604,7 @@ function createEpicsCommand() {
2235
2604
  }
2236
2605
 
2237
2606
  // src/commands/features.ts
2238
- import { Command as Command10 } from "commander";
2607
+ import { Command as Command11 } from "commander";
2239
2608
 
2240
2609
  // src/commands/stories.ts
2241
2610
  import { Command as Command6 } from "commander";
@@ -2656,6 +3025,240 @@ function createAreasCommand() {
2656
3025
  return areas;
2657
3026
  }
2658
3027
 
3028
+ // src/commands/featureComments.ts
3029
+ import { Command as Command10 } from "commander";
3030
+
3031
+ // src/utils/mentions.ts
3032
+ function candidateLabels(member) {
3033
+ const labels = [member.id];
3034
+ if (member.name) {
3035
+ labels.push(member.name);
3036
+ const first = member.name.trim().split(/\s+/)[0];
3037
+ if (first) labels.push(first);
3038
+ }
3039
+ if (member.email) {
3040
+ labels.push(member.email);
3041
+ const local = member.email.split("@")[0];
3042
+ if (local) labels.push(local);
3043
+ }
3044
+ return labels;
3045
+ }
3046
+ function describe(member) {
3047
+ return member.name ?? member.email ?? member.id;
3048
+ }
3049
+ function resolveMember(members, token) {
3050
+ const needle = token.trim().toLowerCase().replace(/^@/, "");
3051
+ const exact = members.filter(
3052
+ (m) => m.id.toLowerCase() === needle || m.name?.toLowerCase() === needle || m.email?.toLowerCase() === needle
3053
+ );
3054
+ if (exact.length === 1) return exact[0];
3055
+ if (exact.length > 1) {
3056
+ throw new Error(
3057
+ `"${token}" matches ${exact.length} members. Use a user id instead: ${exact.map((m) => `${describe(m)} (${m.id})`).join(", ")}`
3058
+ );
3059
+ }
3060
+ const partial = members.filter(
3061
+ (m) => candidateLabels(m).some((l) => l.toLowerCase() === needle)
3062
+ );
3063
+ if (partial.length === 1) return partial[0];
3064
+ if (partial.length > 1) {
3065
+ throw new Error(
3066
+ `"${token}" is ambiguous \u2014 matches ${partial.map((m) => `${describe(m)} (${m.id})`).join(", ")}. Use a full name, email, or user id.`
3067
+ );
3068
+ }
3069
+ const known = members.map(describe).join(", ");
3070
+ throw new Error(
3071
+ `No workspace member matches "${token}". Members: ${known || "(none)"}. Run \`exponential workspaces members\` to see the roster.`
3072
+ );
3073
+ }
3074
+ function escapeRegExp(value) {
3075
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3076
+ }
3077
+ async function applyMentions(client, workspaceId, body, mentions) {
3078
+ if (!mentions || mentions.length === 0) return body;
3079
+ const members = await client.workspaces.listMembers(workspaceId);
3080
+ let result = body;
3081
+ const prepend = [];
3082
+ for (const token of mentions) {
3083
+ const member = resolveMember(members, token);
3084
+ const handle = token.trim().replace(/^@/, "");
3085
+ const inline = new RegExp(`@${escapeRegExp(handle)}(?![\\w-])`, "gi");
3086
+ const substituted = result.replace(inline, member.mentionSyntax);
3087
+ if (substituted !== result) {
3088
+ result = substituted;
3089
+ } else if (!result.includes(member.mentionSyntax)) {
3090
+ prepend.push(member.mentionSyntax);
3091
+ }
3092
+ }
3093
+ return prepend.length > 0 ? `${prepend.join(" ")} ${result}` : result;
3094
+ }
3095
+ var MENTION_OPTION_DESCRIPTION = "Mention a workspace member by name, email, or id (repeatable). Writing @name in the message substitutes in place; otherwise the mention is prepended.";
3096
+ function collectMention(value, previous) {
3097
+ return [...previous, value];
3098
+ }
3099
+
3100
+ // src/commands/featureComments.ts
3101
+ function createFeatureCommentsCommand() {
3102
+ const comment = new Command10("comment").description(
3103
+ "Read and post comments on a feature. Mention teammates with --mention."
3104
+ );
3105
+ comment.command("list").description("List comments on a feature").requiredOption("--feature <id>", "Feature ID").action(async (options, cmd) => {
3106
+ const globalOpts = cmd.optsWithGlobals();
3107
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3108
+ try {
3109
+ const client = getClient();
3110
+ const comments = await client.featureComments.list(options.feature);
3111
+ if (useJson) outputCommentsJson(comments);
3112
+ else outputCommentsPretty(comments);
3113
+ } catch (error) {
3114
+ handleError(error, useJson);
3115
+ }
3116
+ });
3117
+ comment.command("add").description("Add a comment to a feature").requiredOption("--feature <id>", "Feature ID").requiredOption("-m, --message <text>", "Comment body (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3118
+ "--workspace <slug|id>",
3119
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3120
+ ).option("--scope <id>", "Attach to a scope's activity feed instead of the feature").action(
3121
+ async (options, cmd) => {
3122
+ const globalOpts = cmd.optsWithGlobals();
3123
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3124
+ try {
3125
+ const client = getClient();
3126
+ const body = options.mention.length > 0 ? await applyMentions(
3127
+ client,
3128
+ await resolveWorkspaceId(client, options.workspace),
3129
+ options.message,
3130
+ options.mention
3131
+ ) : options.message;
3132
+ const created = await client.featureComments.create({
3133
+ featureId: options.feature,
3134
+ scopeId: options.scope,
3135
+ body
3136
+ });
3137
+ if (useJson) {
3138
+ outputCommentJson(created);
3139
+ } else {
3140
+ console.log("\n\u2713 Comment added");
3141
+ outputCommentPretty(created);
3142
+ }
3143
+ } catch (error) {
3144
+ handleError(error, useJson);
3145
+ }
3146
+ }
3147
+ );
3148
+ comment.command("reply").description("Reply to a comment on a feature").requiredOption("--comment-id <id>", "Comment ID to reply to").requiredOption("-m, --message <text>", "Reply body (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3149
+ "--workspace <slug|id>",
3150
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3151
+ ).action(
3152
+ async (options, cmd) => {
3153
+ const globalOpts = cmd.optsWithGlobals();
3154
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3155
+ try {
3156
+ const client = getClient();
3157
+ const body = options.mention.length > 0 ? await applyMentions(
3158
+ client,
3159
+ await resolveWorkspaceId(client, options.workspace),
3160
+ options.message,
3161
+ options.mention
3162
+ ) : options.message;
3163
+ const created = await client.featureComments.reply({
3164
+ parentId: options.commentId,
3165
+ body
3166
+ });
3167
+ if (useJson) {
3168
+ outputCommentJson(created);
3169
+ } else {
3170
+ console.log("\n\u2713 Reply added");
3171
+ outputCommentPretty(created);
3172
+ }
3173
+ } catch (error) {
3174
+ handleError(error, useJson);
3175
+ }
3176
+ }
3177
+ );
3178
+ comment.command("update").description("Update one of your own comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment body").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3179
+ "--workspace <slug|id>",
3180
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3181
+ ).action(
3182
+ async (options, cmd) => {
3183
+ const globalOpts = cmd.optsWithGlobals();
3184
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3185
+ try {
3186
+ const client = getClient();
3187
+ const body = options.mention.length > 0 ? await applyMentions(
3188
+ client,
3189
+ await resolveWorkspaceId(client, options.workspace),
3190
+ options.message,
3191
+ options.mention
3192
+ ) : options.message;
3193
+ const updated = await client.featureComments.update({
3194
+ commentId: options.commentId,
3195
+ body
3196
+ });
3197
+ if (useJson) {
3198
+ outputCommentJson(updated);
3199
+ } else {
3200
+ console.log("\n\u2713 Comment updated");
3201
+ outputCommentPretty(updated);
3202
+ }
3203
+ } catch (error) {
3204
+ handleError(error, useJson);
3205
+ }
3206
+ }
3207
+ );
3208
+ comment.command("rm").description("Delete one of your own comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
3209
+ const globalOpts = cmd.optsWithGlobals();
3210
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3211
+ try {
3212
+ const client = getClient();
3213
+ await client.featureComments.delete(options.commentId);
3214
+ if (useJson) {
3215
+ console.log(JSON.stringify({ deleted: true, id: options.commentId }, null, 2));
3216
+ } else {
3217
+ console.log("\u2713 Comment deleted");
3218
+ }
3219
+ } catch (error) {
3220
+ handleError(error, useJson);
3221
+ }
3222
+ });
3223
+ comment.command("resolve").description("Resolve an anchored comment thread").requiredOption("--feature <id>", "Feature ID").requiredOption("--thread <id>", "Thread ID (anchored comments only)").action(async (options, cmd) => {
3224
+ const globalOpts = cmd.optsWithGlobals();
3225
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3226
+ try {
3227
+ const client = getClient();
3228
+ await client.featureComments.resolve({
3229
+ featureId: options.feature,
3230
+ threadId: options.thread
3231
+ });
3232
+ if (useJson) {
3233
+ console.log(JSON.stringify({ resolved: true, threadId: options.thread }, null, 2));
3234
+ } else {
3235
+ console.log("\u2713 Thread resolved");
3236
+ }
3237
+ } catch (error) {
3238
+ handleError(error, useJson);
3239
+ }
3240
+ });
3241
+ comment.command("unresolve").description("Reopen a resolved comment thread").requiredOption("--feature <id>", "Feature ID").requiredOption("--thread <id>", "Thread ID (anchored comments only)").action(async (options, cmd) => {
3242
+ const globalOpts = cmd.optsWithGlobals();
3243
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3244
+ try {
3245
+ const client = getClient();
3246
+ await client.featureComments.unresolve({
3247
+ featureId: options.feature,
3248
+ threadId: options.thread
3249
+ });
3250
+ if (useJson) {
3251
+ console.log(JSON.stringify({ resolved: false, threadId: options.thread }, null, 2));
3252
+ } else {
3253
+ console.log("\u2713 Thread reopened");
3254
+ }
3255
+ } catch (error) {
3256
+ handleError(error, useJson);
3257
+ }
3258
+ });
3259
+ return comment;
3260
+ }
3261
+
2659
3262
  // src/commands/features.ts
2660
3263
  var FEATURE_STATUSES = [
2661
3264
  "IDEA",
@@ -2686,7 +3289,7 @@ function validateFeatureStatus(value) {
2686
3289
  return value;
2687
3290
  }
2688
3291
  function createFeaturesCommand() {
2689
- const features = new Command10("features").description(
3292
+ const features = new Command11("features").description(
2690
3293
  "Manage features. A feature lives under a product and groups user stories, scopes, and tickets."
2691
3294
  );
2692
3295
  features.command("list").description("List features in a product").requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--status <status>", `Filter by status: ${FEATURE_STATUSES.join(", ")}`).action(
@@ -2934,11 +3537,12 @@ Aborted: ${reason}. Feature "${feature.name}" was NOT deleted.`);
2934
3537
  features.addCommand(createScopesCommand());
2935
3538
  features.addCommand(createRequirementsCommand());
2936
3539
  features.addCommand(createAreasCommand());
3540
+ features.addCommand(createFeatureCommentsCommand());
2937
3541
  return features;
2938
3542
  }
2939
3543
 
2940
3544
  // src/commands/labels.ts
2941
- import { Command as Command11 } from "commander";
3545
+ import { Command as Command12 } from "commander";
2942
3546
  import chalk5 from "chalk";
2943
3547
  var TRIAGE_LABELS = [
2944
3548
  { slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
@@ -2992,7 +3596,7 @@ async function resolveLabelIds(client, workspaceId, values) {
2992
3596
  return [...byId, ...resolvedFromSlugs];
2993
3597
  }
2994
3598
  function createLabelsCommand() {
2995
- const labels = new Command11("labels").description(
3599
+ const labels = new Command12("labels").description(
2996
3600
  "Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
2997
3601
  );
2998
3602
  labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
@@ -3095,7 +3699,7 @@ function createLabelsCommand() {
3095
3699
  }
3096
3700
 
3097
3701
  // src/commands/organizations.ts
3098
- import { Command as Command12 } from "commander";
3702
+ import { Command as Command13 } from "commander";
3099
3703
  var VALID_SIZES = [
3100
3704
  "1-10",
3101
3705
  "11-50",
@@ -3105,7 +3709,7 @@ var VALID_SIZES = [
3105
3709
  "1000+"
3106
3710
  ];
3107
3711
  function createOrganizationsCommand() {
3108
- const organizations = new Command12("organizations").alias("orgs").description("Manage CRM organizations");
3712
+ const organizations = new Command13("organizations").alias("orgs").description("Manage CRM organizations");
3109
3713
  organizations.command("list").description("List organizations").requiredOption("--workspace <id>", "Workspace ID").option("--search <query>", "Search by name or description").option("--industry <industry>", "Filter by industry").option("--limit <n>", "Max results to return", parseInt).option("--cursor <cursor>", "Pagination cursor").action(async (options, cmd) => {
3110
3714
  const globalOpts = cmd.optsWithGlobals();
3111
3715
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3174,7 +3778,105 @@ function createOrganizationsCommand() {
3174
3778
 
3175
3779
  // src/commands/pages.ts
3176
3780
  import { readFileSync } from "fs";
3177
- import { Command as Command13 } from "commander";
3781
+ import { Command as Command15 } from "commander";
3782
+
3783
+ // src/commands/pageComments.ts
3784
+ import { Command as Command14 } from "commander";
3785
+ function createPageCommentsCommand() {
3786
+ const comment = new Command14("comment").description(
3787
+ "Read and post comments on a page. Mention teammates with --mention."
3788
+ );
3789
+ comment.command("list").description("List comments on a page").requiredOption("--page <id>", "Page ID").action(async (options, cmd) => {
3790
+ const globalOpts = cmd.optsWithGlobals();
3791
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3792
+ try {
3793
+ const client = getClient();
3794
+ const comments = await client.pageComments.list(options.page);
3795
+ if (useJson) outputCommentsJson(comments);
3796
+ else outputCommentsPretty(comments);
3797
+ } catch (error) {
3798
+ handleError(error, useJson);
3799
+ }
3800
+ });
3801
+ comment.command("add").description("Add a comment to a page").requiredOption("--page <id>", "Page ID").requiredOption("-m, --message <text>", "Comment body (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3802
+ "--workspace <slug|id>",
3803
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3804
+ ).action(
3805
+ async (options, cmd) => {
3806
+ const globalOpts = cmd.optsWithGlobals();
3807
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3808
+ try {
3809
+ const client = getClient();
3810
+ const body = options.mention.length > 0 ? await applyMentions(
3811
+ client,
3812
+ await resolveWorkspaceId(client, options.workspace),
3813
+ options.message,
3814
+ options.mention
3815
+ ) : options.message;
3816
+ const created = await client.pageComments.create({
3817
+ pageId: options.page,
3818
+ body
3819
+ });
3820
+ if (useJson) {
3821
+ outputCommentJson(created);
3822
+ } else {
3823
+ console.log("\n\u2713 Comment added");
3824
+ outputCommentPretty(created);
3825
+ }
3826
+ } catch (error) {
3827
+ handleError(error, useJson);
3828
+ }
3829
+ }
3830
+ );
3831
+ comment.command("update").description("Update one of your own comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment body").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3832
+ "--workspace <slug|id>",
3833
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3834
+ ).action(
3835
+ async (options, cmd) => {
3836
+ const globalOpts = cmd.optsWithGlobals();
3837
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3838
+ try {
3839
+ const client = getClient();
3840
+ const body = options.mention.length > 0 ? await applyMentions(
3841
+ client,
3842
+ await resolveWorkspaceId(client, options.workspace),
3843
+ options.message,
3844
+ options.mention
3845
+ ) : options.message;
3846
+ const updated = await client.pageComments.update({
3847
+ commentId: options.commentId,
3848
+ body
3849
+ });
3850
+ if (useJson) {
3851
+ outputCommentJson(updated);
3852
+ } else {
3853
+ console.log("\n\u2713 Comment updated");
3854
+ outputCommentPretty(updated);
3855
+ }
3856
+ } catch (error) {
3857
+ handleError(error, useJson);
3858
+ }
3859
+ }
3860
+ );
3861
+ comment.command("rm").description("Delete one of your own comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
3862
+ const globalOpts = cmd.optsWithGlobals();
3863
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3864
+ try {
3865
+ const client = getClient();
3866
+ await client.pageComments.delete(options.commentId);
3867
+ if (useJson) {
3868
+ console.log(JSON.stringify({ deleted: true, id: options.commentId }, null, 2));
3869
+ } else {
3870
+ console.log("\u2713 Comment deleted");
3871
+ }
3872
+ } catch (error) {
3873
+ handleError(error, useJson);
3874
+ }
3875
+ });
3876
+ return comment;
3877
+ }
3878
+
3879
+ // src/commands/pages.ts
3178
3880
  function readBody(body, bodyFile) {
3179
3881
  if (body !== void 0) return body;
3180
3882
  if (bodyFile === void 0) return void 0;
@@ -3182,7 +3884,7 @@ function readBody(body, bodyFile) {
3182
3884
  return readFileSync(bodyFile, "utf-8");
3183
3885
  }
3184
3886
  function createPagesCommand() {
3185
- const pages = new Command13("pages").description(
3887
+ const pages = new Command15("pages").description(
3186
3888
  'Manage Knowledge pages (PRDs, research, technical specs). Link one to a feature with "features link-page".'
3187
3889
  );
3188
3890
  pages.command("list").description("List pages in a workspace").option("--workspace <slug|id>", "Workspace (defaults to the configured default)").option("--search <text>", "Filter by title/content").action(
@@ -3260,13 +3962,14 @@ function createPagesCommand() {
3260
3962
  }
3261
3963
  }
3262
3964
  );
3965
+ pages.addCommand(createPageCommentsCommand());
3263
3966
  return pages;
3264
3967
  }
3265
3968
 
3266
3969
  // src/commands/products.ts
3267
- import { Command as Command14 } from "commander";
3970
+ import { Command as Command16 } from "commander";
3268
3971
  function createProductsCommand() {
3269
- const products = new Command14("products").description(
3972
+ const products = new Command16("products").description(
3270
3973
  "Manage products. A workspace contains products; a product holds features and tickets."
3271
3974
  );
3272
3975
  products.command("list").description("List products in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID (defaults to configured workspace)").action(async (options, cmd) => {
@@ -3329,9 +4032,9 @@ function createProductsCommand() {
3329
4032
  }
3330
4033
 
3331
4034
  // src/commands/projects.ts
3332
- import { Command as Command15 } from "commander";
4035
+ import { Command as Command17 } from "commander";
3333
4036
  function createProjectsCommand() {
3334
- const projects = new Command15("projects").description("Manage projects");
4037
+ const projects = new Command17("projects").description("Manage projects");
3335
4038
  projects.command("list").description("List all projects").option("--workspace <id>", "Filter by workspace ID").option("--include-actions", "Include actions in output").action(async (options, cmd) => {
3336
4039
  const globalOpts = cmd.optsWithGlobals();
3337
4040
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3354,7 +4057,7 @@ function createProjectsCommand() {
3354
4057
  }
3355
4058
 
3356
4059
  // src/commands/search.ts
3357
- import { Command as Command16 } from "commander";
4060
+ import { Command as Command18 } from "commander";
3358
4061
  import chalk6 from "chalk";
3359
4062
  var TYPE_LABELS = {
3360
4063
  workspace: "Workspaces",
@@ -3373,7 +4076,7 @@ var TYPE_LABELS = {
3373
4076
  product: "Products"
3374
4077
  };
3375
4078
  function createSearchCommand() {
3376
- const search = new Command16("search").description(
4079
+ const search = new Command18("search").description(
3377
4080
  "Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
3378
4081
  ).argument("<query>", "Text to search for").option("-w, --workspace <slug>", "Restrict results to one workspace (slug or ID)").option("-l, --limit <n>", "Max results per entity type (1-25, default 10)").action(async (query, options, cmd) => {
3379
4082
  const globalOpts = cmd.optsWithGlobals();
@@ -3432,7 +4135,7 @@ ${TYPE_LABELS[type] ?? type}`));
3432
4135
  }
3433
4136
 
3434
4137
  // src/commands/tickets.ts
3435
- import { Command as Command17 } from "commander";
4138
+ import { Command as Command19 } from "commander";
3436
4139
  function collectRepeatable(value, previous) {
3437
4140
  return [...previous, value];
3438
4141
  }
@@ -3475,7 +4178,7 @@ function validateTicketType(value) {
3475
4178
  return value;
3476
4179
  }
3477
4180
  function createTicketsCommand() {
3478
- const tickets = new Command17("tickets").description(
4181
+ const tickets = new Command19("tickets").description(
3479
4182
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
3480
4183
  );
3481
4184
  tickets.command("list").description(
@@ -3789,25 +4492,78 @@ function createTicketsCommand() {
3789
4492
  handleError(error, useJson);
3790
4493
  }
3791
4494
  });
3792
- const comment = new Command17("comment").description("Manage comments on a ticket");
3793
- comment.command("add").description("Add a comment to a ticket").requiredOption("--id <id>", "Ticket CUID").requiredOption("-m, --message <text>", "Comment content (markdown supported)").action(async (options, cmd) => {
4495
+ const comment = new Command19("comment").description("Manage comments on a ticket");
4496
+ comment.command("list").description("List comments on a ticket").requiredOption("--id <id>", "Ticket CUID").action(async (options, cmd) => {
3794
4497
  const globalOpts = cmd.optsWithGlobals();
3795
4498
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3796
4499
  try {
3797
4500
  const client = getClient();
3798
- const created = await client.tickets.addComment({
3799
- ticketId: options.id,
3800
- content: options.message
3801
- });
3802
- if (useJson) outputCommentJson(created);
3803
- else {
3804
- console.log("\n\u2713 Comment added");
3805
- outputCommentPretty(created);
3806
- }
4501
+ const ticket = await client.tickets.get(options.id);
4502
+ const comments = ticket.comments ?? [];
4503
+ if (useJson) outputCommentsJson(comments);
4504
+ else outputCommentsPretty(comments);
3807
4505
  } catch (error) {
3808
4506
  handleError(error, useJson);
3809
4507
  }
3810
4508
  });
4509
+ comment.command("add").description("Add a comment to a ticket").requiredOption("--id <id>", "Ticket CUID").requiredOption("-m, --message <text>", "Comment content (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
4510
+ "--workspace <slug|id>",
4511
+ "Workspace used to resolve --mention (defaults to your default workspace)"
4512
+ ).action(
4513
+ async (options, cmd) => {
4514
+ const globalOpts = cmd.optsWithGlobals();
4515
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4516
+ try {
4517
+ const client = getClient();
4518
+ const content = options.mention.length > 0 ? await applyMentions(
4519
+ client,
4520
+ await resolveWorkspaceId(client, options.workspace),
4521
+ options.message,
4522
+ options.mention
4523
+ ) : options.message;
4524
+ const created = await client.tickets.addComment({
4525
+ ticketId: options.id,
4526
+ content
4527
+ });
4528
+ if (useJson) outputCommentJson(created);
4529
+ else {
4530
+ console.log("\n\u2713 Comment added");
4531
+ outputCommentPretty(created);
4532
+ }
4533
+ } catch (error) {
4534
+ handleError(error, useJson);
4535
+ }
4536
+ }
4537
+ );
4538
+ comment.command("update").description("Update one of your own ticket comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment content").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
4539
+ "--workspace <slug|id>",
4540
+ "Workspace used to resolve --mention (defaults to your default workspace)"
4541
+ ).action(
4542
+ async (options, cmd) => {
4543
+ const globalOpts = cmd.optsWithGlobals();
4544
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4545
+ try {
4546
+ const client = getClient();
4547
+ const content = options.mention.length > 0 ? await applyMentions(
4548
+ client,
4549
+ await resolveWorkspaceId(client, options.workspace),
4550
+ options.message,
4551
+ options.mention
4552
+ ) : options.message;
4553
+ const updated = await client.tickets.updateComment({
4554
+ id: options.commentId,
4555
+ content
4556
+ });
4557
+ if (useJson) outputCommentJson(updated);
4558
+ else {
4559
+ console.log("\n\u2713 Comment updated");
4560
+ outputCommentPretty(updated);
4561
+ }
4562
+ } catch (error) {
4563
+ handleError(error, useJson);
4564
+ }
4565
+ }
4566
+ );
3811
4567
  comment.command("delete").description("Delete one of your own ticket comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
3812
4568
  const globalOpts = cmd.optsWithGlobals();
3813
4569
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3828,10 +4584,10 @@ function createTicketsCommand() {
3828
4584
  }
3829
4585
 
3830
4586
  // src/commands/workspaces.ts
3831
- import { Command as Command18 } from "commander";
4587
+ import { Command as Command20 } from "commander";
3832
4588
  import chalk7 from "chalk";
3833
4589
  function createWorkspacesCommand() {
3834
- const workspaces = new Command18("workspaces").description("Manage workspaces");
4590
+ const workspaces = new Command20("workspaces").description("Manage workspaces");
3835
4591
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
3836
4592
  const globalOpts = cmd.optsWithGlobals();
3837
4593
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3873,11 +4629,759 @@ function createWorkspacesCommand() {
3873
4629
  handleError(error);
3874
4630
  }
3875
4631
  });
4632
+ workspaces.command("members").description(
4633
+ "List workspace members. Each row carries the exact @[Name](id) markup to paste into a comment to mention that person."
4634
+ ).option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").option("--search <text>", "Filter by name or email").action(
4635
+ async (options, cmd) => {
4636
+ const globalOpts = cmd.optsWithGlobals();
4637
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4638
+ try {
4639
+ const client = getClient();
4640
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
4641
+ let members = await client.workspaces.listMembers(workspaceId);
4642
+ if (options.search) {
4643
+ const needle = options.search.toLowerCase();
4644
+ members = members.filter(
4645
+ (m) => m.name?.toLowerCase().includes(needle) || m.email?.toLowerCase().includes(needle)
4646
+ );
4647
+ }
4648
+ if (useJson) {
4649
+ console.log(
4650
+ JSON.stringify({ members, total: members.length }, null, 2)
4651
+ );
4652
+ return;
4653
+ }
4654
+ if (members.length === 0) {
4655
+ console.log(chalk7.gray("No members found."));
4656
+ return;
4657
+ }
4658
+ console.log(chalk7.bold(`
4659
+ Members (${members.length})
4660
+ `));
4661
+ for (const m of members) {
4662
+ const label = m.name ?? m.email ?? m.id;
4663
+ const via = m.source === "team" ? chalk7.gray(
4664
+ ` via team${m.teams.length ? `: ${m.teams.map((t) => t.name).join(", ")}` : ""}` + (m.teamRole ? ` [team ${m.teamRole}]` : "")
4665
+ ) : "";
4666
+ console.log(`${chalk7.bold(label)} ${chalk7.gray(`(${m.role})`)}${via}`);
4667
+ if (m.email) console.log(chalk7.gray(` ${m.email}`));
4668
+ console.log(chalk7.gray(` mention: ${m.mentionSyntax}`));
4669
+ console.log();
4670
+ }
4671
+ } catch (error) {
4672
+ handleError(error, useJson);
4673
+ }
4674
+ }
4675
+ );
3876
4676
  return workspaces;
3877
4677
  }
3878
4678
 
4679
+ // src/commands/goals.ts
4680
+ import { Command as Command21 } from "commander";
4681
+ var GOAL_STATUSES = [
4682
+ "planned",
4683
+ "active",
4684
+ "completed",
4685
+ "archived",
4686
+ "on-hold"
4687
+ ];
4688
+ var GOAL_WRITABLE_STATUSES = [
4689
+ "planned",
4690
+ "active",
4691
+ "completed",
4692
+ "archived"
4693
+ ];
4694
+ var KEY_RESULT_STATUSES = [
4695
+ "not-started",
4696
+ "on-track",
4697
+ "at-risk",
4698
+ "off-track",
4699
+ "achieved"
4700
+ ];
4701
+ var KEY_RESULT_UNITS = [
4702
+ "percent",
4703
+ "count",
4704
+ "currency",
4705
+ "hours",
4706
+ "custom"
4707
+ ];
4708
+ function currentPeriod() {
4709
+ const now = /* @__PURE__ */ new Date();
4710
+ return `Q${Math.floor(now.getMonth() / 3) + 1}-${now.getFullYear()}`;
4711
+ }
4712
+ async function targetWorkspaces(client, options) {
4713
+ if (options.allWorkspaces) {
4714
+ if (options.workspace) {
4715
+ throw new Error("--workspace and --all-workspaces are mutually exclusive.");
4716
+ }
4717
+ return await client.workspaces.list();
4718
+ }
4719
+ return [await resolveWorkspace(client, options.workspace)];
4720
+ }
4721
+ async function fanOutByWorkspace(workspaces, read) {
4722
+ const settled = await Promise.allSettled(workspaces.map(read));
4723
+ const items = [];
4724
+ const skipped = [];
4725
+ settled.forEach((result, i) => {
4726
+ if (result.status === "fulfilled") {
4727
+ items.push(...result.value);
4728
+ return;
4729
+ }
4730
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
4731
+ skipped.push(`${workspaces[i].slug}: ${reason}`);
4732
+ });
4733
+ if (skipped.length === workspaces.length) {
4734
+ const [first] = settled.filter((r) => r.status === "rejected");
4735
+ throw first.reason;
4736
+ }
4737
+ for (const line of skipped) {
4738
+ console.error(`Warning: skipped workspace ${line}`);
4739
+ }
4740
+ return items;
4741
+ }
4742
+ function parseGoalId(value) {
4743
+ const id = Number(value);
4744
+ if (!Number.isInteger(id)) {
4745
+ throw new Error(
4746
+ `Goal id must be a whole number, got "${value}". List them with \`exponential goals list\`.`
4747
+ );
4748
+ }
4749
+ return id;
4750
+ }
4751
+ function parseNumber(value, label) {
4752
+ const n = Number(value);
4753
+ if (!Number.isFinite(n)) {
4754
+ throw new Error(`${label} must be a number, got "${value}".`);
4755
+ }
4756
+ return n;
4757
+ }
4758
+ function validateGoalStatus(value) {
4759
+ if (!value) return void 0;
4760
+ if (!GOAL_STATUSES.includes(value)) {
4761
+ throw new Error(`Invalid status "${value}". Valid: ${GOAL_STATUSES.join(", ")}`);
4762
+ }
4763
+ return value;
4764
+ }
4765
+ function validateWritableGoalStatus(value) {
4766
+ if (!value) return void 0;
4767
+ if (!GOAL_WRITABLE_STATUSES.includes(value)) {
4768
+ const extra = value === "on-hold" ? " Use `goals set-status` for on-hold." : "";
4769
+ throw new Error(
4770
+ `Invalid status "${value}". Valid: ${GOAL_WRITABLE_STATUSES.join(", ")}.${extra}`
4771
+ );
4772
+ }
4773
+ return value;
4774
+ }
4775
+ function validateKeyResultStatus(value) {
4776
+ if (!value) return void 0;
4777
+ if (!KEY_RESULT_STATUSES.includes(value)) {
4778
+ throw new Error(
4779
+ `Invalid status "${value}". Valid: ${KEY_RESULT_STATUSES.join(", ")}`
4780
+ );
4781
+ }
4782
+ return value;
4783
+ }
4784
+ function validateKeyResultUnit(value) {
4785
+ if (!value) return void 0;
4786
+ if (!KEY_RESULT_UNITS.includes(value)) {
4787
+ throw new Error(`Invalid unit "${value}". Valid: ${KEY_RESULT_UNITS.join(", ")}`);
4788
+ }
4789
+ return value;
4790
+ }
4791
+ function createKeyResultsCommand() {
4792
+ const kr = new Command21("kr").description(
4793
+ "Key results on an objective. Ids are CUIDs; --goal takes the objective's integer id."
4794
+ );
4795
+ kr.command("list").description("List key results").option("--goal <id>", "Objective ID (a number)").option("-w, --workspace <slug|id>", "Workspace (workspace-wide: every member's KRs)").option("--period <period>", "Period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${KEY_RESULT_STATUSES.join(", ")}`).option("--mine", "Narrow a workspace list to key results you own").action(
4796
+ async (options, cmd) => {
4797
+ const globalOpts = cmd.optsWithGlobals();
4798
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4799
+ try {
4800
+ const status = validateKeyResultStatus(options.status);
4801
+ const client = getClient();
4802
+ const workspaceId = options.workspace ? await resolveWorkspaceId(client, options.workspace) : void 0;
4803
+ const list = await client.goals.keyResults.list({
4804
+ workspaceId,
4805
+ goalId: options.goal ? parseGoalId(options.goal) : void 0,
4806
+ period: options.period,
4807
+ status,
4808
+ onlyMine: options.mine
4809
+ });
4810
+ if (useJson) outputKeyResultsJson(list);
4811
+ else outputKeyResultsPretty(list);
4812
+ } catch (error) {
4813
+ handleError(error, useJson);
4814
+ }
4815
+ }
4816
+ );
4817
+ kr.command("get <id>").description("Get a key result by CUID").action(async (id, _options, cmd) => {
4818
+ const globalOpts = cmd.optsWithGlobals();
4819
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4820
+ try {
4821
+ const keyResult = await getClient().goals.keyResults.get(id);
4822
+ if (useJson) outputKeyResultJson(keyResult);
4823
+ else outputKeyResultPretty(keyResult);
4824
+ } catch (error) {
4825
+ handleError(error, useJson);
4826
+ }
4827
+ });
4828
+ kr.command("create").description("Create a key result on an objective").requiredOption("--goal <id>", "Objective ID (a number)").requiredOption("-t, --title <text>", "Key result title").requiredOption("--target <n>", "Target value").option("--start <n>", "Starting value (default 0)").option("--current <n>", "Current value (defaults to the start value)").option("--unit <unit>", `Unit: ${KEY_RESULT_UNITS.join(", ")}`).option("--period <period>", "Period, e.g. Q3-2026 (defaults to the objective's)").option("-d, --description <text>", "Description").option("--dri <userId>", "Directly responsible individual (user ID)").action(
4829
+ async (options, cmd) => {
4830
+ const globalOpts = cmd.optsWithGlobals();
4831
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4832
+ try {
4833
+ const unit = validateKeyResultUnit(options.unit);
4834
+ const client = getClient();
4835
+ const goalId = parseGoalId(options.goal);
4836
+ const startValue = options.start ? parseNumber(options.start, "--start") : void 0;
4837
+ let period = options.period;
4838
+ let workspaceId;
4839
+ if (!period) {
4840
+ const goal = await client.goals.get(goalId);
4841
+ if (!goal.period) {
4842
+ throw new Error(
4843
+ `Objective #${goalId} has no period, so --period is required. See \`exponential goals periods\`.`
4844
+ );
4845
+ }
4846
+ period = goal.period;
4847
+ workspaceId = goal.workspaceId ?? void 0;
4848
+ }
4849
+ const keyResult = await client.goals.keyResults.create({
4850
+ goalId,
4851
+ workspaceId,
4852
+ title: options.title,
4853
+ targetValue: parseNumber(options.target, "--target"),
4854
+ startValue,
4855
+ currentValue: options.current ? parseNumber(options.current, "--current") : startValue,
4856
+ unit,
4857
+ period,
4858
+ description: options.description,
4859
+ driUserId: options.dri
4860
+ });
4861
+ if (useJson) outputKeyResultJson(keyResult);
4862
+ else {
4863
+ console.log("\n\u2713 Key result created");
4864
+ outputKeyResultPretty(keyResult);
4865
+ }
4866
+ } catch (error) {
4867
+ handleError(error, useJson);
4868
+ }
4869
+ }
4870
+ );
4871
+ kr.command("update").description("Update a key result (only the flags you pass are written)").requiredOption("--id <cuid>", "Key result CUID").option("-t, --title <text>", "New title").option("-d, --description <text>", "New description").option("--current <n>", "Current value (prefer `kr checkin` \u2014 it records history)").option("--target <n>", "Target value").option("--start <n>", "Starting value").option("--unit <unit>", `Unit: ${KEY_RESULT_UNITS.join(", ")}`).option("--status <status>", `Status: ${KEY_RESULT_STATUSES.join(", ")}`).option("--confidence <n>", "Confidence 0-100").option("--dri <userId>", "Directly responsible individual (user ID)").option("--goal <id>", "Move the key result to a different objective").action(
4872
+ async (options, cmd) => {
4873
+ const globalOpts = cmd.optsWithGlobals();
4874
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4875
+ try {
4876
+ const status = validateKeyResultStatus(options.status);
4877
+ const unit = validateKeyResultUnit(options.unit);
4878
+ const keyResult = await getClient().goals.keyResults.update({
4879
+ id: options.id,
4880
+ title: options.title,
4881
+ description: options.description,
4882
+ currentValue: options.current ? parseNumber(options.current, "--current") : void 0,
4883
+ targetValue: options.target ? parseNumber(options.target, "--target") : void 0,
4884
+ startValue: options.start ? parseNumber(options.start, "--start") : void 0,
4885
+ unit,
4886
+ status,
4887
+ confidence: options.confidence ? parseNumber(options.confidence, "--confidence") : void 0,
4888
+ driUserId: options.dri,
4889
+ goalId: options.goal ? parseGoalId(options.goal) : void 0
4890
+ });
4891
+ if (useJson) outputKeyResultJson(keyResult);
4892
+ else {
4893
+ console.log("\n\u2713 Key result updated");
4894
+ outputKeyResultPretty(keyResult);
4895
+ }
4896
+ } catch (error) {
4897
+ handleError(error, useJson);
4898
+ }
4899
+ }
4900
+ );
4901
+ kr.command("checkin").description(
4902
+ "Record a progress check-in. Moves the value AND re-derives the status from where it lands between start and target."
4903
+ ).requiredOption("--id <cuid>", "Key result CUID").requiredOption("--value <n>", "New current value").option("--note <text>", "Check-in note").action(
4904
+ async (options, cmd) => {
4905
+ const globalOpts = cmd.optsWithGlobals();
4906
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4907
+ try {
4908
+ const checkIn = await getClient().goals.keyResults.checkIn({
4909
+ id: options.id,
4910
+ value: parseNumber(options.value, "--value"),
4911
+ note: options.note
4912
+ });
4913
+ if (useJson) outputCheckInJson(checkIn);
4914
+ else {
4915
+ console.log("\n\u2713 Check-in recorded");
4916
+ outputCheckInPretty(checkIn);
4917
+ }
4918
+ } catch (error) {
4919
+ handleError(error, useJson);
4920
+ }
4921
+ }
4922
+ );
4923
+ kr.command("delete").description("Delete a key result").requiredOption("--id <cuid>", "Key result CUID").action(async (options, cmd) => {
4924
+ const globalOpts = cmd.optsWithGlobals();
4925
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4926
+ try {
4927
+ await getClient().goals.keyResults.delete(options.id);
4928
+ if (useJson) {
4929
+ console.log(JSON.stringify({ deleted: true, id: options.id }, null, 2));
4930
+ } else {
4931
+ console.log("\u2713 Key result deleted");
4932
+ }
4933
+ } catch (error) {
4934
+ handleError(error, useJson);
4935
+ }
4936
+ });
4937
+ kr.command("link").description("Link executing work (a project or a feature) to a key result").requiredOption("--id <cuid>", "Key result CUID").option("--project <cuid>", "Project CUID").option("--feature <cuid>", "Feature CUID").action(
4938
+ async (options, cmd) => {
4939
+ const globalOpts = cmd.optsWithGlobals();
4940
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4941
+ try {
4942
+ if (!options.project === !options.feature) {
4943
+ throw new Error("Pass exactly one of --project or --feature.");
4944
+ }
4945
+ const client = getClient();
4946
+ const result = options.project ? await client.goals.keyResults.linkProject({
4947
+ keyResultId: options.id,
4948
+ projectId: options.project
4949
+ }) : await client.goals.keyResults.linkFeature({
4950
+ keyResultId: options.id,
4951
+ featureId: options.feature
4952
+ });
4953
+ if (useJson) console.log(JSON.stringify(result, null, 2));
4954
+ else console.log(`
4955
+ \u2713 ${options.project ? "Project" : "Feature"} linked`);
4956
+ } catch (error) {
4957
+ handleError(error, useJson);
4958
+ }
4959
+ }
4960
+ );
4961
+ kr.command("unlink").description("Unlink a project or feature from a key result").requiredOption("--id <cuid>", "Key result CUID").option("--project <cuid>", "Project CUID").option("--feature <cuid>", "Feature CUID").action(
4962
+ async (options, cmd) => {
4963
+ const globalOpts = cmd.optsWithGlobals();
4964
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4965
+ try {
4966
+ if (!options.project === !options.feature) {
4967
+ throw new Error("Pass exactly one of --project or --feature.");
4968
+ }
4969
+ const client = getClient();
4970
+ const result = options.project ? await client.goals.keyResults.unlinkProject({
4971
+ keyResultId: options.id,
4972
+ projectId: options.project
4973
+ }) : await client.goals.keyResults.unlinkFeature({
4974
+ keyResultId: options.id,
4975
+ featureId: options.feature
4976
+ });
4977
+ if (useJson) console.log(JSON.stringify(result, null, 2));
4978
+ else console.log(`
4979
+ \u2713 ${options.project ? "Project" : "Feature"} unlinked`);
4980
+ } catch (error) {
4981
+ handleError(error, useJson);
4982
+ }
4983
+ }
4984
+ );
4985
+ return kr;
4986
+ }
4987
+ function createGoalCommentsCommand() {
4988
+ const comment = new Command21("comment").description(
4989
+ "Read and post comments on a goal. Mention teammates with --mention."
4990
+ );
4991
+ comment.command("list").description("List comments on a goal").requiredOption("--goal <id>", "Goal ID (a number)").action(async (options, cmd) => {
4992
+ const globalOpts = cmd.optsWithGlobals();
4993
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4994
+ try {
4995
+ const client = getClient();
4996
+ const comments = await client.goalComments.list(parseGoalId(options.goal));
4997
+ if (useJson) outputCommentsJson(comments);
4998
+ else outputCommentsPretty(comments);
4999
+ } catch (error) {
5000
+ handleError(error, useJson);
5001
+ }
5002
+ });
5003
+ comment.command("add").description("Add a comment to a goal").requiredOption("--goal <id>", "Goal ID (a number)").requiredOption("-m, --message <text>", "Comment content (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
5004
+ "--workspace <slug|id>",
5005
+ "Workspace used to resolve --mention (defaults to your default workspace)"
5006
+ ).option("--parent-update <id>", "Thread under a specific goal update").action(
5007
+ async (options, cmd) => {
5008
+ const globalOpts = cmd.optsWithGlobals();
5009
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5010
+ try {
5011
+ const client = getClient();
5012
+ const goalId = parseGoalId(options.goal);
5013
+ const content = options.mention.length > 0 ? await applyMentions(
5014
+ client,
5015
+ await resolveWorkspaceId(client, options.workspace),
5016
+ options.message,
5017
+ options.mention
5018
+ ) : options.message;
5019
+ const created = await client.goalComments.add({
5020
+ goalId,
5021
+ content,
5022
+ parentUpdateId: options.parentUpdate
5023
+ });
5024
+ if (useJson) {
5025
+ outputCommentJson(created);
5026
+ } else {
5027
+ console.log("\n\u2713 Comment added");
5028
+ outputCommentPretty(created);
5029
+ }
5030
+ } catch (error) {
5031
+ handleError(error, useJson);
5032
+ }
5033
+ }
5034
+ );
5035
+ comment.command("update").description("Update one of your own comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment content").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
5036
+ "--workspace <slug|id>",
5037
+ "Workspace used to resolve --mention (defaults to your default workspace)"
5038
+ ).action(
5039
+ async (options, cmd) => {
5040
+ const globalOpts = cmd.optsWithGlobals();
5041
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5042
+ try {
5043
+ const client = getClient();
5044
+ const content = options.mention.length > 0 ? await applyMentions(
5045
+ client,
5046
+ await resolveWorkspaceId(client, options.workspace),
5047
+ options.message,
5048
+ options.mention
5049
+ ) : options.message;
5050
+ const updated = await client.goalComments.update({
5051
+ commentId: options.commentId,
5052
+ content
5053
+ });
5054
+ if (useJson) {
5055
+ outputCommentJson(updated);
5056
+ } else {
5057
+ console.log("\n\u2713 Comment updated");
5058
+ outputCommentPretty(updated);
5059
+ }
5060
+ } catch (error) {
5061
+ handleError(error, useJson);
5062
+ }
5063
+ }
5064
+ );
5065
+ comment.command("rm").description("Delete one of your own comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
5066
+ const globalOpts = cmd.optsWithGlobals();
5067
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5068
+ try {
5069
+ const client = getClient();
5070
+ await client.goalComments.delete(options.commentId);
5071
+ if (useJson) {
5072
+ console.log(JSON.stringify({ deleted: true, id: options.commentId }, null, 2));
5073
+ } else {
5074
+ console.log("\u2713 Comment deleted");
5075
+ }
5076
+ } catch (error) {
5077
+ handleError(error, useJson);
5078
+ }
5079
+ });
5080
+ return comment;
5081
+ }
5082
+ function createGoalsCommand() {
5083
+ const goals = new Command21("goals").description(
5084
+ "Manage goals (objectives) and their key results. Objectives have integer ids; key results are CUIDs."
5085
+ );
5086
+ goals.command("list").description(
5087
+ "List objectives, each with the projects linked to it \u2014 the join that ties an action (which carries a projectId) back to a goal."
5088
+ ).option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--all-workspaces", "List across every workspace you belong to").option("--period <period>", "Filter by period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${GOAL_STATUSES.join(", ")}`).option("--tree", "Render the parent/child cascade instead of a flat list").action(
5089
+ async (options, cmd) => {
5090
+ const globalOpts = cmd.optsWithGlobals();
5091
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5092
+ try {
5093
+ const status = validateGoalStatus(options.status);
5094
+ const client = getClient();
5095
+ const workspaces = await targetWorkspaces(client, options);
5096
+ if (options.tree) {
5097
+ const trees = await fanOutByWorkspace(
5098
+ workspaces,
5099
+ (w) => client.goals.tree({ workspaceId: w.id, status })
5100
+ );
5101
+ const filtered = options.period ? trees.filter((g) => g.period === options.period) : trees;
5102
+ if (useJson) outputGoalTreeJson(filtered);
5103
+ else outputGoalTreePretty(filtered);
5104
+ return;
5105
+ }
5106
+ const list = await fanOutByWorkspace(workspaces, async (workspace) => {
5107
+ const goals2 = await client.goals.list({
5108
+ workspaceId: workspace.id,
5109
+ period: options.period,
5110
+ status
5111
+ });
5112
+ return goals2.map((g) => ({ ...g, workspace: g.workspace ?? workspace }));
5113
+ });
5114
+ if (useJson) outputGoalsJson(list);
5115
+ else outputGoalsPretty(list);
5116
+ } catch (error) {
5117
+ handleError(error, useJson);
5118
+ }
5119
+ }
5120
+ );
5121
+ goals.command("get <id>").description("Get an objective by its integer ID").action(async (id, _options, cmd) => {
5122
+ const globalOpts = cmd.optsWithGlobals();
5123
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5124
+ try {
5125
+ const goal = await getClient().goals.get(parseGoalId(id));
5126
+ if (useJson) outputGoalJson(goal);
5127
+ else outputGoalPretty(goal);
5128
+ } catch (error) {
5129
+ handleError(error, useJson);
5130
+ }
5131
+ });
5132
+ goals.command("create").description("Create an objective").requiredOption("-t, --title <text>", "Objective title").option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--period <period>", "Period, e.g. Q3-2026 or Annual-2026").option("--status <status>", `Status: ${GOAL_WRITABLE_STATUSES.join(", ")}`).option("-d, --description <text>", "Description (markdown supported)").option("--why <text>", "Why this goal matters").option("--dri <userId>", "Directly responsible individual (user ID)").option("--parent <id>", "Parent objective ID to nest under (max depth 5)").option("--project <cuid>", "Link a project on creation").action(
5133
+ async (options, cmd) => {
5134
+ const globalOpts = cmd.optsWithGlobals();
5135
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5136
+ try {
5137
+ const status = validateWritableGoalStatus(options.status);
5138
+ const client = getClient();
5139
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
5140
+ const goal = await client.goals.create({
5141
+ title: options.title,
5142
+ workspaceId,
5143
+ period: options.period,
5144
+ status,
5145
+ description: options.description,
5146
+ whyThisGoal: options.why,
5147
+ driUserId: options.dri,
5148
+ parentGoalId: options.parent ? parseGoalId(options.parent) : void 0,
5149
+ projectId: options.project
5150
+ });
5151
+ if (useJson) outputGoalJson(goal);
5152
+ else {
5153
+ console.log("\n\u2713 Goal created");
5154
+ outputGoalPretty(goal);
5155
+ }
5156
+ } catch (error) {
5157
+ handleError(error, useJson);
5158
+ }
5159
+ }
5160
+ );
5161
+ goals.command("update").description(
5162
+ "Update an objective. Only the flags you pass are written \u2014 everything else is left exactly as it is. For a status change use `set-status`; to re-parent use `reparent`."
5163
+ ).requiredOption("--id <n>", "Objective ID (a number)").option("-t, --title <text>", "New title").option("-d, --description <text>", 'New description ("none" clears it)').option("--why <text>", 'Why this goal matters ("none" clears it)').option("--period <period>", 'New period ("none" clears it)').option("--dri <userId>", "Directly responsible individual (user ID)").option("--status <status>", `Status: ${GOAL_WRITABLE_STATUSES.join(", ")}`).option("--project <cuid>", 'Replace the project links with this project ("none" clears them)').action(
5164
+ async (options, cmd) => {
5165
+ const globalOpts = cmd.optsWithGlobals();
5166
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5167
+ try {
5168
+ const status = validateWritableGoalStatus(options.status);
5169
+ const clearable = (value) => value === void 0 ? void 0 : value === "none" ? null : value;
5170
+ const goal = await getClient().goals.update({
5171
+ id: parseGoalId(options.id),
5172
+ title: options.title,
5173
+ description: clearable(options.description),
5174
+ whyThisGoal: clearable(options.why),
5175
+ period: clearable(options.period),
5176
+ driUserId: options.dri,
5177
+ status,
5178
+ projectId: clearable(options.project)
5179
+ });
5180
+ if (useJson) outputGoalJson(goal);
5181
+ else {
5182
+ console.log("\n\u2713 Goal updated");
5183
+ outputGoalPretty(goal);
5184
+ }
5185
+ } catch (error) {
5186
+ handleError(error, useJson);
5187
+ }
5188
+ }
5189
+ );
5190
+ goals.command("set-status").description(
5191
+ "Set an objective's status and nothing else. Routes through the status-only procedure, so no other field can be touched."
5192
+ ).requiredOption("--id <n>", "Objective ID (a number)").requiredOption("--status <status>", `Status: ${GOAL_STATUSES.join(", ")}`).action(async (options, cmd) => {
5193
+ const globalOpts = cmd.optsWithGlobals();
5194
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5195
+ try {
5196
+ const status = validateGoalStatus(options.status);
5197
+ const goal = await getClient().goals.setStatus({
5198
+ id: parseGoalId(options.id),
5199
+ status
5200
+ });
5201
+ if (useJson) outputGoalJson(goal);
5202
+ else {
5203
+ console.log(`
5204
+ \u2713 Goal status set to ${status}`);
5205
+ outputGoalPretty(goal);
5206
+ }
5207
+ } catch (error) {
5208
+ handleError(error, useJson);
5209
+ }
5210
+ });
5211
+ goals.command("close").description("Close out an objective \u2014 one command per goal when a quarter ends").requiredOption("--id <n>", "Objective ID (a number)").option("--status <status>", "completed (default) or archived", "completed").action(async (options, cmd) => {
5212
+ const globalOpts = cmd.optsWithGlobals();
5213
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5214
+ try {
5215
+ const status = validateGoalStatus(options.status);
5216
+ const goal = await getClient().goals.setStatus({
5217
+ id: parseGoalId(options.id),
5218
+ status
5219
+ });
5220
+ if (useJson) outputGoalJson(goal);
5221
+ else {
5222
+ console.log(`
5223
+ \u2713 Goal closed as ${status}`);
5224
+ outputGoalPretty(goal);
5225
+ }
5226
+ } catch (error) {
5227
+ handleError(error, useJson);
5228
+ }
5229
+ });
5230
+ goals.command("reparent").description(
5231
+ "Move an objective under another (or detach it with --parent none). Writes the parent alone."
5232
+ ).requiredOption("--id <n>", "Objective ID (a number)").requiredOption("--parent <id|none>", 'New parent objective ID, or "none" to detach').action(async (options, cmd) => {
5233
+ const globalOpts = cmd.optsWithGlobals();
5234
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5235
+ try {
5236
+ const goal = await getClient().goals.setParent({
5237
+ id: parseGoalId(options.id),
5238
+ parentGoalId: options.parent === "none" ? null : parseGoalId(options.parent)
5239
+ });
5240
+ if (useJson) outputGoalJson(goal);
5241
+ else {
5242
+ console.log(
5243
+ options.parent === "none" ? "\n\u2713 Goal detached" : "\n\u2713 Goal re-parented"
5244
+ );
5245
+ outputGoalPretty(goal);
5246
+ }
5247
+ } catch (error) {
5248
+ handleError(error, useJson);
5249
+ }
5250
+ });
5251
+ goals.command("delete").description("Delete an objective").requiredOption("--id <n>", "Objective ID (a number)").action(async (options, cmd) => {
5252
+ const globalOpts = cmd.optsWithGlobals();
5253
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5254
+ try {
5255
+ const id = parseGoalId(options.id);
5256
+ await getClient().goals.delete(id);
5257
+ if (useJson) {
5258
+ console.log(JSON.stringify({ deleted: true, id }, null, 2));
5259
+ } else {
5260
+ console.log("\u2713 Goal deleted");
5261
+ }
5262
+ } catch (error) {
5263
+ handleError(error, useJson);
5264
+ }
5265
+ });
5266
+ goals.command("periods").description("List the conventional OKR period strings").action(async (_options, cmd) => {
5267
+ const globalOpts = cmd.optsWithGlobals();
5268
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5269
+ try {
5270
+ const periods = await getClient().goals.periods();
5271
+ if (useJson) outputGoalPeriodsJson(periods);
5272
+ else outputGoalPeriodsPretty(periods);
5273
+ } catch (error) {
5274
+ handleError(error, useJson);
5275
+ }
5276
+ });
5277
+ goals.command("stats").description("Objective and key-result counts, average progress and confidence").option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--period <period>", "Period, e.g. Q3-2026").action(
5278
+ async (options, cmd) => {
5279
+ const globalOpts = cmd.optsWithGlobals();
5280
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5281
+ try {
5282
+ const client = getClient();
5283
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
5284
+ const stats = await client.goals.stats({
5285
+ workspaceId,
5286
+ period: options.period
5287
+ });
5288
+ if (useJson) outputGoalStatsJson(stats);
5289
+ else outputGoalStatsPretty(stats);
5290
+ } catch (error) {
5291
+ handleError(error, useJson);
5292
+ }
5293
+ }
5294
+ );
5295
+ goals.addCommand(createKeyResultsCommand());
5296
+ goals.addCommand(createGoalCommentsCommand());
5297
+ return goals;
5298
+ }
5299
+ function createOkrsCommand() {
5300
+ const okrs = new Command21("okrs").description(
5301
+ "OKRs \u2014 objectives with their key results nested. Manage the parts with `goals` and `goals kr`."
5302
+ );
5303
+ okrs.command("list").description(
5304
+ "List objectives with their key results. Defaults to the current quarter \u2014 pass --period all for every period."
5305
+ ).option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--all-workspaces", "List across every workspace you belong to").option(
5306
+ "--period <period>",
5307
+ `Period, e.g. Q3-2026, or "all" for every period (default: ${currentPeriod()})`
5308
+ ).option("--paired-period", "Also include the period's parent annual period").option(
5309
+ "--status <status>",
5310
+ `Keep only key results with this status: ${KEY_RESULT_STATUSES.join(", ")}`
5311
+ ).option("--mine", "Only objectives and key results you are the DRI for").action(
5312
+ async (options, cmd) => {
5313
+ const globalOpts = cmd.optsWithGlobals();
5314
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5315
+ try {
5316
+ const status = validateKeyResultStatus(options.status);
5317
+ const client = getClient();
5318
+ const workspaces = await targetWorkspaces(client, options);
5319
+ const period = options.period === "all" ? void 0 : options.period ?? currentPeriod();
5320
+ let objectives = await fanOutByWorkspace(workspaces, async (workspace) => {
5321
+ const found = await client.goals.keyResults.byObjective({
5322
+ workspaceId: workspace.id,
5323
+ period,
5324
+ includePairedPeriod: options.pairedPeriod,
5325
+ onlyMine: options.mine
5326
+ });
5327
+ return found.map((o) => ({ ...o, workspace }));
5328
+ });
5329
+ if (status) {
5330
+ objectives = objectives.map((o) => ({
5331
+ ...o,
5332
+ keyResults: o.keyResults.filter(
5333
+ (kr) => (kr.statusOverride ?? kr.status) === status
5334
+ )
5335
+ })).filter((o) => o.keyResults.length > 0);
5336
+ }
5337
+ if (useJson) outputObjectivesJson(objectives);
5338
+ else outputObjectivesPretty(objectives);
5339
+ } catch (error) {
5340
+ handleError(error, useJson);
5341
+ }
5342
+ }
5343
+ );
5344
+ okrs.command("stats").description("Objective and key-result counts, average progress and confidence").option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--period <period>", "Period, e.g. Q3-2026").action(
5345
+ async (options, cmd) => {
5346
+ const globalOpts = cmd.optsWithGlobals();
5347
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5348
+ try {
5349
+ const client = getClient();
5350
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
5351
+ const stats = await client.goals.stats({
5352
+ workspaceId,
5353
+ period: options.period
5354
+ });
5355
+ if (useJson) outputGoalStatsJson(stats);
5356
+ else outputGoalStatsPretty(stats);
5357
+ } catch (error) {
5358
+ handleError(error, useJson);
5359
+ }
5360
+ }
5361
+ );
5362
+ okrs.command("periods").description("List the conventional OKR period strings").action(async (_options, cmd) => {
5363
+ const globalOpts = cmd.optsWithGlobals();
5364
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5365
+ try {
5366
+ const periods = await getClient().goals.periods();
5367
+ if (useJson) outputGoalPeriodsJson(periods);
5368
+ else outputGoalPeriodsPretty(periods);
5369
+ } catch (error) {
5370
+ handleError(error, useJson);
5371
+ }
5372
+ });
5373
+ okrs.addCommand(createKeyResultsCommand());
5374
+ return okrs;
5375
+ }
5376
+
3879
5377
  // src/index.ts
3880
- var program = new Command19();
5378
+ import { readFileSync as readFileSync2 } from "fs";
5379
+ import { dirname, join } from "path";
5380
+ import { fileURLToPath } from "url";
5381
+ var PKG_VERSION = JSON.parse(
5382
+ readFileSync2(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
5383
+ ).version;
5384
+ var program = new Command22();
3881
5385
  program.name("exponential").description(
3882
5386
  [
3883
5387
  "CLI to interact with Exponential productivity app.",
@@ -3886,7 +5390,7 @@ program.name("exponential").description(
3886
5390
  " workspace -> product -> feature -> ticket -> action",
3887
5391
  " epics are workspace-scoped and can group tickets and actions across products."
3888
5392
  ].join("\n")
3889
- ).version("1.0.0").option("--json", "Output as JSON (default when piped)").option("--pretty", "Force pretty-printed output");
5393
+ ).version(PKG_VERSION).option("--json", "Output as JSON (default when piped)").option("--pretty", "Force pretty-printed output");
3890
5394
  program.addCommand(createAuthCommand());
3891
5395
  program.addCommand(createActionsCommand());
3892
5396
  program.addCommand(createContactsCommand());
@@ -3901,5 +5405,7 @@ program.addCommand(createProjectsCommand());
3901
5405
  program.addCommand(createSearchCommand());
3902
5406
  program.addCommand(createTicketsCommand());
3903
5407
  program.addCommand(createWorkspacesCommand());
5408
+ program.addCommand(createGoalsCommand());
5409
+ program.addCommand(createOkrsCommand());
3904
5410
  program.parse();
3905
5411
  //# sourceMappingURL=index.js.map