linear-sdk-cli 0.1.3 → 0.2.3

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.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +96 -58
  4. package/COVERAGE.md +5 -5
  5. package/MIGRATING.md +71 -69
  6. package/README.md +115 -90
  7. package/package.json +6 -7
  8. package/skills/README.md +15 -0
  9. package/skills/linear-sdk-cli/SKILL.md +27 -3
  10. package/skills/linear-sdk-cli/references/api.md +9 -9
  11. package/skills/linear-sdk-cli/references/auth.md +18 -0
  12. package/skills/linear-sdk-cli/references/commands.md +2 -2
  13. package/skills/linear-sdk-cli/references/comment.md +9 -6
  14. package/skills/linear-sdk-cli/references/config.md +9 -11
  15. package/skills/linear-sdk-cli/references/issue.md +72 -18
  16. package/skills/linear-sdk-cli/references/open.md +29 -0
  17. package/skills/linear-sdk-cli/references/team.md +16 -0
  18. package/skills/linear-sdk-cli/scripts/generate-docs.ts +6 -2
  19. package/skills/linear-sdk-cli-maintenance/SKILL.md +97 -0
  20. package/src/bin/linear.ts +16 -6
  21. package/src/cli.ts +3 -1
  22. package/src/commands/agent-session.ts +13 -5
  23. package/src/commands/api.ts +15 -9
  24. package/src/commands/attach.ts +15 -4
  25. package/src/commands/comment.ts +50 -11
  26. package/src/commands/completion.ts +2 -6
  27. package/src/commands/cycle.ts +6 -1
  28. package/src/commands/discover.ts +6 -2
  29. package/src/commands/document.ts +4 -1
  30. package/src/commands/initiative.ts +5 -1
  31. package/src/commands/issue.ts +392 -91
  32. package/src/commands/meta.ts +139 -31
  33. package/src/commands/milestone.ts +4 -1
  34. package/src/commands/notification.ts +15 -6
  35. package/src/commands/open.ts +84 -0
  36. package/src/commands/project.ts +17 -4
  37. package/src/commands/team.ts +13 -0
  38. package/src/commands/user.ts +2 -2
  39. package/src/commands/webhook.ts +12 -2
  40. package/src/config.ts +501 -184
  41. package/src/context.ts +13 -4
  42. package/src/git.ts +28 -3
  43. package/src/lib/body.ts +26 -2
  44. package/src/lib/errors.ts +33 -8
  45. package/src/lib/introspect.ts +76 -7
  46. package/src/lib/keyring.ts +51 -9
  47. package/src/lib/mentions.ts +49 -0
  48. package/src/lib/open.ts +32 -0
  49. package/src/lib/options.ts +108 -12
  50. package/src/lib/output-shapes.ts +81 -9
  51. package/src/lib/pagination.ts +50 -17
  52. package/src/lib/resolve.ts +40 -7
  53. package/src/lib/shape.ts +16 -12
  54. package/src/lib/upload.ts +3 -1
  55. package/src/output/color.ts +52 -0
  56. package/src/output/format.ts +96 -18
  57. package/src/output/markdown.ts +133 -0
  58. package/src/output/pager.ts +131 -0
  59. package/src/output/sanitize.ts +1 -1
  60. package/src/output/table.ts +7 -7
  61. package/src/services/agent-session.ts +11 -3
  62. package/src/services/attachment.ts +14 -5
  63. package/src/services/comment.ts +37 -9
  64. package/src/services/cycle.ts +11 -4
  65. package/src/services/document.ts +29 -10
  66. package/src/services/favorite.ts +15 -9
  67. package/src/services/initiative-update.ts +16 -15
  68. package/src/services/initiative.ts +15 -10
  69. package/src/services/issue.ts +263 -19
  70. package/src/services/label.ts +12 -2
  71. package/src/services/milestone.ts +8 -5
  72. package/src/services/notification.ts +16 -2
  73. package/src/services/organization.ts +23 -18
  74. package/src/services/project.ts +55 -34
  75. package/src/services/roadmap.ts +7 -6
  76. package/src/services/state.ts +9 -3
  77. package/src/services/team.ts +67 -24
  78. package/src/services/user.ts +2 -2
  79. package/src/services/webhook.ts +28 -17
@@ -143,7 +143,7 @@ export interface ListFilters {
143
143
  export const MINE_STATE_TYPES = ["unstarted"];
144
144
 
145
145
  /** The sort orders `issue list` accepts, in `--sort`, env, and config alike. */
146
- export const ISSUE_SORTS = ["priority", "updated", "created"] as const;
146
+ export const ISSUE_SORTS = ["priority", "manual", "updated", "created"] as const;
147
147
  export type IssueSort = (typeof ISSUE_SORTS)[number];
148
148
 
149
149
  type SortConfig = Pick<
@@ -237,7 +237,9 @@ export async function buildFilter(
237
237
  // --all-teams wins over both the flag and the configured default.
238
238
  // Uppercase first, then dedupe: `--team tes --team TES` is one team.
239
239
  const teamKeys = [
240
- ...new Set(asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase())),
240
+ ...new Set(
241
+ asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase()),
242
+ ),
241
243
  ];
242
244
  // One team keeps the exact `eq` shape it has always sent; several use `in`
243
245
  // (the reference CLI spells the same thing as `or: [{ key: { eq } }, …]`).
@@ -358,7 +360,7 @@ export function sortSpec(sort: IssueSort = "priority"): Array<Record<string, unk
358
360
  // against the API, where Descending returns Backlog BEFORE In Progress.
359
361
  // The reference CLI hardcodes Descending, so a Low-priority backlog issue
360
362
  // sorts above an Urgent in-progress one there; we deliberately diverge
361
- // rather than copy that. See ALIGNMENT.md.
363
+ // because workflow state should take precedence over backlog urgency.
362
364
  //
363
365
  // Descending priority is urgency order (Urgent…Low); `nulls: "last"` keeps
364
366
  // "No priority" at the bottom, which is what `noPriorityFirst: false` did.
@@ -369,6 +371,11 @@ export function sortSpec(sort: IssueSort = "priority"): Array<Record<string, unk
369
371
  ];
370
372
  case "created":
371
373
  return [{ createdAt: { order: "Descending" } }];
374
+ case "manual":
375
+ // Linear's board order, without injecting workflow-state or priority
376
+ // grouping. This is the reference CLI's `issue_sort = "manual"` and is
377
+ // useful when a carefully curated backlog should read exactly as arranged.
378
+ return [{ manual: { nulls: "last", order: "Ascending" } }];
372
379
  case "updated":
373
380
  return [{ updatedAt: { order: "Descending" } }];
374
381
  }
@@ -486,6 +493,57 @@ function lifecycle(n: any) {
486
493
  * flattened to strings here: `team: "TES Test-workspace-bla"` is not even
487
494
  * parseable, since a team name can contain spaces.)
488
495
  */
496
+ export interface IssueDetailRef {
497
+ id: string;
498
+ identifier: string;
499
+ title: string;
500
+ state: { id: string; name: string; type: string } | null;
501
+ }
502
+
503
+ export interface IssueAttachmentDetail {
504
+ id: string;
505
+ title: string;
506
+ url: string;
507
+ subtitle: string | null;
508
+ sourceType: string | null;
509
+ createdAt: string;
510
+ }
511
+
512
+ export interface IssueDocumentDetail {
513
+ id: string;
514
+ title: string;
515
+ slugId: string;
516
+ url: string;
517
+ createdAt: string;
518
+ updatedAt: string;
519
+ }
520
+
521
+ export interface IssueRelationDetail {
522
+ id: string;
523
+ type: string;
524
+ issue: IssueDetailRef;
525
+ relatedIssue: IssueDetailRef;
526
+ }
527
+
528
+ /**
529
+ * A comment embedded in `IssueDetail`. It is a superset of the shared comment
530
+ * row: external authors and resolution metadata are retained so a caller can
531
+ * reconstruct threads without another request.
532
+ */
533
+ export interface IssueCommentDetail {
534
+ id: string;
535
+ body: string;
536
+ url: string;
537
+ createdAt: string;
538
+ editedAt: string | null;
539
+ resolvedAt: string | null;
540
+ parent: { id: string } | null;
541
+ user: { id: string; displayName: string } | null;
542
+ externalUser: { id: string; displayName: string } | null;
543
+ resolvingCommentId: string | null;
544
+ resolvingUser: { id: string; displayName: string } | null;
545
+ }
546
+
489
547
  export interface IssueDetail {
490
548
  id: string;
491
549
  identifier: string;
@@ -510,9 +568,15 @@ export interface IssueDetail {
510
568
  project: { id: string; name: string } | null;
511
569
  milestone: { id: string; name: string } | null;
512
570
  cycle: { id: string; number: number; name: string | null } | null;
513
- parent: { id: string; identifier: string } | null;
571
+ parent: IssueDetailRef | null;
572
+ children: IssueDetailRef[];
514
573
  labels: Array<{ id: string; name: string }>;
515
574
  subscribers: Array<{ id: string; displayName: string }>;
575
+ attachments: IssueAttachmentDetail[];
576
+ documents: IssueDocumentDetail[];
577
+ relations: IssueRelationDetail[];
578
+ inverseRelations: IssueRelationDetail[];
579
+ comments: IssueCommentDetail[];
516
580
  }
517
581
 
518
582
  /** The detail's shape as `linear commands` advertises it; checked against `IssueDetail`. */
@@ -540,9 +604,95 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
540
604
  project: { nullable: { id: "string", name: "string" } },
541
605
  milestone: { nullable: { id: "string", name: "string" } },
542
606
  cycle: { nullable: { id: "string", number: "number", name: "string|null" } },
543
- parent: { nullable: { id: "string", identifier: "string" } },
607
+ parent: {
608
+ nullable: {
609
+ id: "string",
610
+ identifier: "string",
611
+ title: "string",
612
+ state: { nullable: { id: "string", name: "string", type: "string" } },
613
+ },
614
+ },
615
+ children: [
616
+ {
617
+ id: "string",
618
+ identifier: "string",
619
+ title: "string",
620
+ state: { nullable: { id: "string", name: "string", type: "string" } },
621
+ },
622
+ ],
544
623
  labels: [{ id: "string", name: "string" }],
545
624
  subscribers: [{ id: "string", displayName: "string" }],
625
+ attachments: [
626
+ {
627
+ id: "string",
628
+ title: "string",
629
+ url: "string",
630
+ subtitle: "string|null",
631
+ sourceType: "string|null",
632
+ createdAt: "string",
633
+ },
634
+ ],
635
+ documents: [
636
+ {
637
+ id: "string",
638
+ title: "string",
639
+ slugId: "string",
640
+ url: "string",
641
+ createdAt: "string",
642
+ updatedAt: "string",
643
+ },
644
+ ],
645
+ relations: [
646
+ {
647
+ id: "string",
648
+ type: "string",
649
+ issue: {
650
+ id: "string",
651
+ identifier: "string",
652
+ title: "string",
653
+ state: { nullable: { id: "string", name: "string", type: "string" } },
654
+ },
655
+ relatedIssue: {
656
+ id: "string",
657
+ identifier: "string",
658
+ title: "string",
659
+ state: { nullable: { id: "string", name: "string", type: "string" } },
660
+ },
661
+ },
662
+ ],
663
+ inverseRelations: [
664
+ {
665
+ id: "string",
666
+ type: "string",
667
+ issue: {
668
+ id: "string",
669
+ identifier: "string",
670
+ title: "string",
671
+ state: { nullable: { id: "string", name: "string", type: "string" } },
672
+ },
673
+ relatedIssue: {
674
+ id: "string",
675
+ identifier: "string",
676
+ title: "string",
677
+ state: { nullable: { id: "string", name: "string", type: "string" } },
678
+ },
679
+ },
680
+ ],
681
+ comments: [
682
+ {
683
+ id: "string",
684
+ body: "string",
685
+ url: "string",
686
+ createdAt: "string",
687
+ editedAt: "string|null",
688
+ resolvedAt: "string|null",
689
+ parent: { nullable: { id: "string" } },
690
+ user: { nullable: { id: "string", displayName: "string" } },
691
+ externalUser: { nullable: { id: "string", displayName: "string" } },
692
+ resolvingCommentId: "string|null",
693
+ resolvingUser: { nullable: { id: "string", displayName: "string" } },
694
+ },
695
+ ],
546
696
  });
547
697
 
548
698
  /**
@@ -557,7 +707,7 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
557
707
  * depends on the API accepting human identifiers in `issue(id:)`.
558
708
  */
559
709
  const DETAIL_QUERY = `
560
- query CliIssueDetail($filter: IssueFilter!) {
710
+ query CliIssueDetail($filter: IssueFilter!, $includeComments: Boolean!) {
561
711
  issues(filter: $filter, first: 1, includeArchived: true) {
562
712
  nodes {
563
713
  id identifier title description priority priorityLabel estimate url branchName dueDate
@@ -568,9 +718,39 @@ query CliIssueDetail($filter: IssueFilter!) {
568
718
  project { id name }
569
719
  projectMilestone { id name }
570
720
  cycle { id number name }
571
- parent { id identifier }
721
+ parent { id identifier title state { id name type } }
722
+ children(first: 250) { nodes { id identifier title state { id name type } } }
572
723
  labels(first: 50) { nodes { id name } }
573
724
  subscribers(first: 50) { nodes { id displayName } }
725
+ attachments(first: 50) {
726
+ nodes { id title url subtitle sourceType createdAt }
727
+ }
728
+ documents(first: 50) {
729
+ nodes { id title slugId url createdAt updatedAt }
730
+ }
731
+ relations(first: 50) {
732
+ nodes {
733
+ id type
734
+ issue { id identifier title state { id name type } }
735
+ relatedIssue { id identifier title state { id name type } }
736
+ }
737
+ }
738
+ inverseRelations(first: 50) {
739
+ nodes {
740
+ id type
741
+ issue { id identifier title state { id name type } }
742
+ relatedIssue { id identifier title state { id name type } }
743
+ }
744
+ }
745
+ comments(first: 50, orderBy: createdAt) @include(if: $includeComments) {
746
+ nodes {
747
+ id body url createdAt editedAt resolvedAt resolvingCommentId
748
+ parent { id }
749
+ user { id displayName }
750
+ externalUser { id displayName }
751
+ resolvingUser { id displayName }
752
+ }
753
+ }
574
754
  }
575
755
  }
576
756
  }`;
@@ -581,15 +761,28 @@ const IDENTIFIER_RE = /^([a-zA-Z][a-zA-Z0-9]*)-(\d+)$/;
581
761
  function detailFilter(input: string): { filter: Record<string, unknown>; label: string } {
582
762
  if (isUuid(input)) return { filter: { id: { eq: input } }, label: input };
583
763
  const match = input.match(IDENTIFIER_RE);
584
- if (!match) throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
764
+ if (!match)
765
+ throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
585
766
  const key = match[1]!.toUpperCase();
586
767
  const number = Number.parseInt(match[2]!, 10);
587
- return { filter: { team: { key: { eq: key } }, number: { eq: number } }, label: `${key}-${number}` };
768
+ return {
769
+ filter: { team: { key: { eq: key } }, number: { eq: number } },
770
+ label: `${key}-${number}`,
771
+ };
588
772
  }
589
773
 
590
- export async function getIssueDetail(client: LinearClient, idArg: string): Promise<IssueDetail> {
774
+ export async function getIssueDetail(
775
+ client: LinearClient,
776
+ idArg: string,
777
+ opts: { includeComments?: boolean } = {},
778
+ ): Promise<IssueDetail> {
591
779
  const { filter, label } = detailFilter(idArg);
592
- const data: any = await withRetry(() => (client as any).client.rawRequest(DETAIL_QUERY, { filter }));
780
+ const data: any = await withRetry(() =>
781
+ (client as any).client.rawRequest(DETAIL_QUERY, {
782
+ filter,
783
+ includeComments: opts.includeComments !== false,
784
+ }),
785
+ );
593
786
  const n = data.data?.issues?.nodes?.[0];
594
787
  if (!n) throw notFound(`No issue ${label}.`);
595
788
  return {
@@ -613,8 +806,38 @@ export async function getIssueDetail(client: LinearClient, idArg: string): Promi
613
806
  milestone: n.projectMilestone ?? null,
614
807
  cycle: cycleRef(n.cycle),
615
808
  parent: n.parent ?? null,
809
+ children: n.children?.nodes ?? [],
616
810
  labels: n.labels?.nodes ?? [],
617
811
  subscribers: n.subscribers?.nodes ?? [],
812
+ attachments: (n.attachments?.nodes ?? []).map((a: any) => ({
813
+ id: a.id,
814
+ title: a.title,
815
+ url: a.url,
816
+ subtitle: a.subtitle ?? null,
817
+ sourceType: a.sourceType ?? null,
818
+ createdAt: a.createdAt,
819
+ })),
820
+ documents: n.documents?.nodes ?? [],
821
+ relations: n.relations?.nodes ?? [],
822
+ inverseRelations: n.inverseRelations?.nodes ?? [],
823
+ comments: (n.comments?.nodes ?? []).map(toIssueCommentDetail),
824
+ };
825
+ }
826
+
827
+ /** Normalize nullable comment relations while preserving thread metadata. */
828
+ function toIssueCommentDetail(c: any): IssueCommentDetail {
829
+ return {
830
+ id: c.id,
831
+ body: c.body,
832
+ url: c.url,
833
+ createdAt: c.createdAt,
834
+ editedAt: c.editedAt ?? null,
835
+ resolvedAt: c.resolvedAt ?? null,
836
+ parent: c.parent ?? null,
837
+ user: c.user ?? null,
838
+ externalUser: c.externalUser ?? null,
839
+ resolvingCommentId: c.resolvingCommentId ?? null,
840
+ resolvingUser: c.resolvingUser ?? null,
618
841
  };
619
842
  }
620
843
 
@@ -673,7 +896,8 @@ export async function createIssue(
673
896
  }
674
897
  if (opts.milestone) {
675
898
  // Resolved after the parent so a milestone can name one in the inherited project.
676
- if (!input.projectId) throw usageError("A milestone requires --project (or a --parent in a project).");
899
+ if (!input.projectId)
900
+ throw usageError("A milestone requires --project (or a --parent in a project).");
677
901
  input.projectMilestoneId = await resolveMilestoneId(client, input.projectId, opts.milestone);
678
902
  }
679
903
  if (opts.cycle) input.cycleId = await resolveCycleId(client, team.id, opts.cycle);
@@ -703,6 +927,8 @@ export interface UpdateOptions {
703
927
  estimate?: number;
704
928
  parent?: string;
705
929
  dueDate?: string;
930
+ /** Replace the complete label set (including `[]` to clear it). */
931
+ label?: string[];
706
932
  addLabel?: string[];
707
933
  removeLabel?: string[];
708
934
  /** Clear the assignee. Mutually exclusive with `assignee`. */
@@ -717,6 +943,8 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
717
943
  throw usageError("Pass either --assignee or --unassign, not both.");
718
944
  if (opts.clearCycle && opts.cycle)
719
945
  throw usageError("Pass either --cycle or --clear-cycle, not both.");
946
+ if (opts.label !== undefined && (opts.addLabel !== undefined || opts.removeLabel !== undefined))
947
+ throw usageError("Pass either --label (replace all) or --add-label/--remove-label, not both.");
720
948
 
721
949
  const issue = await resolveIssue(client, idArg);
722
950
  const currentTeamId = (await issue.team)?.id;
@@ -745,7 +973,9 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
745
973
  if (opts.project) input.projectId = await resolveProjectId(client, opts.project);
746
974
  if (opts.cycle && teamId) input.cycleId = await resolveCycleId(client, teamId, opts.cycle);
747
975
  if (opts.parent) input.parentId = (await resolveIssue(client, opts.parent)).id;
748
- if (opts.addLabel?.length) input.addedLabelIds = await resolveLabelIds(client, opts.addLabel, teamId);
976
+ if (opts.label !== undefined) input.labelIds = await resolveLabelIds(client, opts.label, teamId);
977
+ if (opts.addLabel?.length)
978
+ input.addedLabelIds = await resolveLabelIds(client, opts.addLabel, teamId);
749
979
  if (opts.removeLabel?.length)
750
980
  input.removedLabelIds = await resolveLabelIds(client, opts.removeLabel, teamId);
751
981
  if (opts.milestone) {
@@ -755,7 +985,8 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
755
985
  input.projectMilestoneId = await resolveMilestoneId(client, projectId, opts.milestone);
756
986
  }
757
987
 
758
- if (Object.keys(input).length === 0) throw usageError("Nothing to update; pass at least one field.");
988
+ if (Object.keys(input).length === 0)
989
+ throw usageError("Nothing to update; pass at least one field.");
759
990
  // The updated issue comes from the payload, never from `issue` — falling back
760
991
  // to the pre-mutation entity would print "Updated TES-1" for a write the API
761
992
  // refused.
@@ -777,14 +1008,19 @@ export async function archiveIssue(client: LinearClient, idArg: string, unarchiv
777
1008
 
778
1009
  export async function deleteIssue(client: LinearClient, idArg: string) {
779
1010
  const issue = await resolveIssue(client, idArg);
780
- await assertMutation(withRetry(() => client.deleteIssue(issue.id)), "Issue deletion");
1011
+ await assertMutation(
1012
+ withRetry(() => client.deleteIssue(issue.id)),
1013
+ "Issue deletion",
1014
+ );
781
1015
  return issue;
782
1016
  }
783
1017
 
784
1018
  export async function setSubscription(client: LinearClient, idArg: string, subscribe: boolean) {
785
1019
  const issue = await resolveIssue(client, idArg);
786
1020
  await assertMutation(
787
- withRetry(() => (subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id))),
1021
+ withRetry(() =>
1022
+ subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id),
1023
+ ),
788
1024
  subscribe ? "Issue subscribe" : "Issue unsubscribe",
789
1025
  );
790
1026
  return issue;
@@ -811,7 +1047,10 @@ export async function moveIssueState(client: LinearClient, issue: Issue, opts: S
811
1047
  const stateId = opts.stateInput
812
1048
  ? await resolveStateId(client, teamId, opts.stateInput)
813
1049
  : await firstStateOfType(client, teamId, "started");
814
- await assertMutation(withRetry(() => client.updateIssue(issue.id, { stateId })), "Issue update");
1050
+ await assertMutation(
1051
+ withRetry(() => client.updateIssue(issue.id, { stateId })),
1052
+ "Issue update",
1053
+ );
815
1054
  }
816
1055
 
817
1056
  export async function startIssue(client: LinearClient, idArg: string, opts: StartMove) {
@@ -835,7 +1074,11 @@ export async function addRemoveRelation(
835
1074
  if (op === "add") {
836
1075
  await assertMutation(
837
1076
  withRetry(() =>
838
- client.createIssueRelation({ issueId: from.id, relatedIssueId: to.id, type: apiType as any }),
1077
+ client.createIssueRelation({
1078
+ issueId: from.id,
1079
+ relatedIssueId: to.id,
1080
+ type: apiType as any,
1081
+ }),
839
1082
  ),
840
1083
  "Relation creation",
841
1084
  );
@@ -848,7 +1091,8 @@ export async function addRemoveRelation(
848
1091
  collect((await withRetry(() => from.inverseRelations())) as any, Infinity),
849
1092
  ]);
850
1093
  const match = await findRelation([...direct, ...inverse] as any[], from.id, to.id, apiType);
851
- if (!match) throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
1094
+ if (!match)
1095
+ throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
852
1096
  await assertMutation(
853
1097
  withRetry(() => client.deleteIssueRelation(match.id)),
854
1098
  "Relation removal",
@@ -78,7 +78,14 @@ export async function listLabels(
78
78
  filter = { or: [{ team: { key: { eq: team.key } } }, { team: { null: true } }] };
79
79
  }
80
80
 
81
- return collectRawQuery<LabelRow>(client as any, LIST_QUERY, { filter }, "issueLabels", limit, toLabelRow);
81
+ return collectRawQuery<LabelRow>(
82
+ client as any,
83
+ LIST_QUERY,
84
+ { filter },
85
+ "issueLabels",
86
+ limit,
87
+ toLabelRow,
88
+ );
82
89
  }
83
90
 
84
91
  /** Map a tailored-query label node to a display row. */
@@ -161,7 +168,10 @@ export async function deleteLabel(client: LinearClient, idArg: string) {
161
168
  const id = await resolveLabel(client, idArg);
162
169
  // Fetch the label first so we can report its name after deletion.
163
170
  const label = await withRetry(() => client.issueLabel(id));
164
- await assertMutation(withRetry(() => client.deleteIssueLabel(id)), "Label deletion");
171
+ await assertMutation(
172
+ withRetry(() => client.deleteIssueLabel(id)),
173
+ "Label deletion",
174
+ );
165
175
  return label;
166
176
  }
167
177
 
@@ -11,7 +11,12 @@
11
11
  import type { LinearClient } from "@linear/sdk";
12
12
  import { withRetry } from "../client.js";
13
13
  import { shape } from "../lib/shape.js";
14
- import { collect, pageSizeForMore } from "../lib/pagination.js";
14
+ import {
15
+ collect,
16
+ inheritPaginationMetadata,
17
+ pageSize,
18
+ pageSizeForMore,
19
+ } from "../lib/pagination.js";
15
20
  import { usageError, notFound } from "../lib/errors.js";
16
21
  import { assertMutation, unwrapMutation } from "../lib/mutation.js";
17
22
  import { resolveProjectId, resolveMilestoneId, isUuid } from "../lib/resolve.js";
@@ -54,11 +59,9 @@ export async function listMilestones(
54
59
  ): Promise<MilestoneRow[]> {
55
60
  const projectId = await resolveProjectId(client, projectInput);
56
61
  const project = await withRetry(() => client.project(projectId));
57
- const conn = await withRetry(() =>
58
- project.projectMilestones({ first: limit === Infinity ? 100 : Math.min(limit, 100) }),
59
- );
62
+ const conn = await withRetry(() => project.projectMilestones({ first: pageSize(limit) }));
60
63
  const nodes = await collect(conn as any, limit);
61
- return nodes.map(toRow);
64
+ return inheritPaginationMetadata(nodes.map(toRow), nodes);
62
65
  }
63
66
 
64
67
  export interface MilestoneDetail {
@@ -14,7 +14,7 @@ import { withRetry } from "../client.js";
14
14
  import { shape } from "../lib/shape.js";
15
15
  import { collectRawQuery } from "../lib/pagination.js";
16
16
  import { assertMutation } from "../lib/mutation.js";
17
- import { normalizeError } from "../lib/errors.js";
17
+ import { normalizeError, usageError } from "../lib/errors.js";
18
18
 
19
19
  export interface NotificationRow {
20
20
  id: string;
@@ -122,7 +122,11 @@ export interface MarkAllItem {
122
122
  error?: string;
123
123
  }
124
124
 
125
- export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({ id: "string", read: "boolean", "error?": "string" });
125
+ export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({
126
+ id: "string",
127
+ read: "boolean",
128
+ "error?": "string",
129
+ });
126
130
 
127
131
  /**
128
132
  * Mark all of the viewer's unread notifications as read. The SDK's
@@ -168,6 +172,16 @@ export async function archiveNotification(client: LinearClient, id: string): Pro
168
172
 
169
173
  /** Snooze a notification until the given ISO timestamp. */
170
174
  export async function snoozeNotification(client: LinearClient, id: string, untilISO: string) {
175
+ if (
176
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
177
+ untilISO,
178
+ ) ||
179
+ !Number.isFinite(Date.parse(untilISO))
180
+ ) {
181
+ throw usageError(
182
+ `Invalid snooze timestamp '${untilISO}'. Use ISO 8601 with a timezone, e.g. 2026-07-01T09:00:00Z.`,
183
+ );
184
+ }
171
185
  await assertMutation(
172
186
  withRetry(() => client.updateNotification(id, { snoozedUntilAt: untilISO } as any)),
173
187
  "Notification snooze",
@@ -11,8 +11,7 @@
11
11
  import type { LinearClient } from "@linear/sdk";
12
12
  import { withRetry } from "../client.js";
13
13
  import { shape } from "../lib/shape.js";
14
- import { collect } from "../lib/pagination.js";
15
- import { pageSize } from "../lib/pagination.js";
14
+ import { collect, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
16
15
 
17
16
  export interface OrganizationDetail {
18
17
  id: string;
@@ -83,14 +82,17 @@ export const ORGANIZATION_MEMBER_ROW_SHAPE = shape<MemberRow>({
83
82
  export async function listMembers(client: LinearClient, limit: number): Promise<MemberRow[]> {
84
83
  const conn = await withRetry(() => client.users({ first: pageSize(limit) }));
85
84
  const nodes = await collect(conn as any, limit);
86
- return nodes.map((u: any) => ({
87
- id: u.id,
88
- displayName: u.displayName,
89
- name: u.name,
90
- email: u.email,
91
- admin: !!u.admin,
92
- active: !!u.active,
93
- }));
85
+ return inheritPaginationMetadata(
86
+ nodes.map((u: any) => ({
87
+ id: u.id,
88
+ displayName: u.displayName,
89
+ name: u.name,
90
+ email: u.email,
91
+ admin: !!u.admin,
92
+ active: !!u.active,
93
+ })),
94
+ nodes,
95
+ );
94
96
  }
95
97
 
96
98
  export interface InviteRow {
@@ -128,12 +130,15 @@ export function inviteStatus(invite: { acceptedAt?: unknown; expiresAt?: unknown
128
130
  export async function listInvites(client: LinearClient, limit: number): Promise<InviteRow[]> {
129
131
  const conn = await withRetry(() => client.organizationInvites({ first: pageSize(limit) }));
130
132
  const nodes = await collect(conn as any, limit);
131
- return nodes.map((i: any) => ({
132
- id: i.id,
133
- email: i.email,
134
- status: inviteStatus(i),
135
- role: i.role ?? "member",
136
- external: !!i.external,
137
- createdAt: i.createdAt?.toISOString?.() ?? String(i.createdAt),
138
- }));
133
+ return inheritPaginationMetadata(
134
+ nodes.map((i: any) => ({
135
+ id: i.id,
136
+ email: i.email,
137
+ status: inviteStatus(i),
138
+ role: i.role ?? "member",
139
+ external: !!i.external,
140
+ createdAt: i.createdAt?.toISOString?.() ?? String(i.createdAt),
141
+ })),
142
+ nodes,
143
+ );
139
144
  }