linear-sdk-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +46 -46
  2. package/COVERAGE.md +5 -5
  3. package/MIGRATING.md +69 -71
  4. package/README.md +88 -96
  5. package/package.json +3 -4
  6. package/skills/linear-sdk-cli/SKILL.md +3 -13
  7. package/skills/linear-sdk-cli/references/api.md +9 -9
  8. package/skills/linear-sdk-cli/references/auth.md +0 -18
  9. package/skills/linear-sdk-cli/references/commands.md +2 -2
  10. package/skills/linear-sdk-cli/references/config.md +11 -9
  11. package/skills/linear-sdk-cli/references/issue.md +12 -63
  12. package/skills/linear-sdk-cli/references/team.md +0 -16
  13. package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
  14. package/src/bin/linear.ts +6 -16
  15. package/src/cli.ts +1 -3
  16. package/src/commands/agent-session.ts +5 -13
  17. package/src/commands/api.ts +9 -15
  18. package/src/commands/attach.ts +4 -15
  19. package/src/commands/comment.ts +2 -6
  20. package/src/commands/completion.ts +6 -2
  21. package/src/commands/cycle.ts +1 -6
  22. package/src/commands/discover.ts +2 -6
  23. package/src/commands/document.ts +1 -4
  24. package/src/commands/initiative.ts +1 -5
  25. package/src/commands/issue.ts +88 -376
  26. package/src/commands/meta.ts +31 -139
  27. package/src/commands/milestone.ts +1 -4
  28. package/src/commands/notification.ts +6 -15
  29. package/src/commands/project.ts +4 -17
  30. package/src/commands/team.ts +0 -13
  31. package/src/commands/webhook.ts +2 -12
  32. package/src/config.ts +183 -500
  33. package/src/context.ts +4 -13
  34. package/src/git.ts +3 -28
  35. package/src/lib/body.ts +2 -26
  36. package/src/lib/errors.ts +7 -32
  37. package/src/lib/introspect.ts +7 -76
  38. package/src/lib/keyring.ts +9 -51
  39. package/src/lib/options.ts +12 -108
  40. package/src/lib/output-shapes.ts +9 -81
  41. package/src/lib/pagination.ts +17 -50
  42. package/src/lib/resolve.ts +7 -40
  43. package/src/lib/shape.ts +12 -16
  44. package/src/lib/upload.ts +1 -3
  45. package/src/output/format.ts +18 -96
  46. package/src/output/table.ts +7 -7
  47. package/src/services/agent-session.ts +3 -11
  48. package/src/services/attachment.ts +5 -14
  49. package/src/services/comment.ts +4 -12
  50. package/src/services/cycle.ts +4 -11
  51. package/src/services/document.ts +10 -29
  52. package/src/services/favorite.ts +9 -15
  53. package/src/services/initiative-update.ts +15 -16
  54. package/src/services/initiative.ts +10 -15
  55. package/src/services/issue.ts +18 -262
  56. package/src/services/label.ts +2 -12
  57. package/src/services/milestone.ts +5 -8
  58. package/src/services/notification.ts +2 -16
  59. package/src/services/organization.ts +18 -23
  60. package/src/services/project.ts +34 -55
  61. package/src/services/roadmap.ts +6 -7
  62. package/src/services/state.ts +3 -9
  63. package/src/services/team.ts +24 -67
  64. package/src/services/user.ts +2 -2
  65. package/src/services/webhook.ts +17 -28
  66. package/skills/linear-sdk-cli/references/open.md +0 -29
  67. package/src/commands/open.ts +0 -84
  68. package/src/lib/open.ts +0 -32
  69. package/src/output/color.ts +0 -52
  70. package/src/output/markdown.ts +0 -133
  71. package/src/output/pager.ts +0 -131
@@ -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", "manual", "updated", "created"] as const;
146
+ export const ISSUE_SORTS = ["priority", "updated", "created"] as const;
147
147
  export type IssueSort = (typeof ISSUE_SORTS)[number];
148
148
 
149
149
  type SortConfig = Pick<
@@ -237,9 +237,7 @@ 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(
241
- asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase()),
242
- ),
240
+ ...new Set(asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase())),
243
241
  ];
244
242
  // One team keeps the exact `eq` shape it has always sent; several use `in`
245
243
  // (the reference CLI spells the same thing as `or: [{ key: { eq } }, …]`).
@@ -371,11 +369,6 @@ export function sortSpec(sort: IssueSort = "priority"): Array<Record<string, unk
371
369
  ];
372
370
  case "created":
373
371
  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" } }];
379
372
  case "updated":
380
373
  return [{ updatedAt: { order: "Descending" } }];
381
374
  }
@@ -493,57 +486,6 @@ function lifecycle(n: any) {
493
486
  * flattened to strings here: `team: "TES Test-workspace-bla"` is not even
494
487
  * parseable, since a team name can contain spaces.)
495
488
  */
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
-
547
489
  export interface IssueDetail {
548
490
  id: string;
549
491
  identifier: string;
@@ -568,15 +510,9 @@ export interface IssueDetail {
568
510
  project: { id: string; name: string } | null;
569
511
  milestone: { id: string; name: string } | null;
570
512
  cycle: { id: string; number: number; name: string | null } | null;
571
- parent: IssueDetailRef | null;
572
- children: IssueDetailRef[];
513
+ parent: { id: string; identifier: string } | null;
573
514
  labels: Array<{ id: string; name: string }>;
574
515
  subscribers: Array<{ id: string; displayName: string }>;
575
- attachments: IssueAttachmentDetail[];
576
- documents: IssueDocumentDetail[];
577
- relations: IssueRelationDetail[];
578
- inverseRelations: IssueRelationDetail[];
579
- comments: IssueCommentDetail[];
580
516
  }
581
517
 
582
518
  /** The detail's shape as `linear commands` advertises it; checked against `IssueDetail`. */
@@ -604,95 +540,9 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
604
540
  project: { nullable: { id: "string", name: "string" } },
605
541
  milestone: { nullable: { id: "string", name: "string" } },
606
542
  cycle: { nullable: { id: "string", number: "number", name: "string|null" } },
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
- ],
543
+ parent: { nullable: { id: "string", identifier: "string" } },
623
544
  labels: [{ id: "string", name: "string" }],
624
545
  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
- ],
696
546
  });
697
547
 
698
548
  /**
@@ -707,7 +557,7 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
707
557
  * depends on the API accepting human identifiers in `issue(id:)`.
708
558
  */
709
559
  const DETAIL_QUERY = `
710
- query CliIssueDetail($filter: IssueFilter!, $includeComments: Boolean!) {
560
+ query CliIssueDetail($filter: IssueFilter!) {
711
561
  issues(filter: $filter, first: 1, includeArchived: true) {
712
562
  nodes {
713
563
  id identifier title description priority priorityLabel estimate url branchName dueDate
@@ -718,39 +568,9 @@ query CliIssueDetail($filter: IssueFilter!, $includeComments: Boolean!) {
718
568
  project { id name }
719
569
  projectMilestone { id name }
720
570
  cycle { id number name }
721
- parent { id identifier title state { id name type } }
722
- children(first: 250) { nodes { id identifier title state { id name type } } }
571
+ parent { id identifier }
723
572
  labels(first: 50) { nodes { id name } }
724
573
  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
- }
754
574
  }
755
575
  }
756
576
  }`;
@@ -761,28 +581,15 @@ const IDENTIFIER_RE = /^([a-zA-Z][a-zA-Z0-9]*)-(\d+)$/;
761
581
  function detailFilter(input: string): { filter: Record<string, unknown>; label: string } {
762
582
  if (isUuid(input)) return { filter: { id: { eq: input } }, label: input };
763
583
  const match = input.match(IDENTIFIER_RE);
764
- if (!match)
765
- throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
584
+ if (!match) throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
766
585
  const key = match[1]!.toUpperCase();
767
586
  const number = Number.parseInt(match[2]!, 10);
768
- return {
769
- filter: { team: { key: { eq: key } }, number: { eq: number } },
770
- label: `${key}-${number}`,
771
- };
587
+ return { filter: { team: { key: { eq: key } }, number: { eq: number } }, label: `${key}-${number}` };
772
588
  }
773
589
 
774
- export async function getIssueDetail(
775
- client: LinearClient,
776
- idArg: string,
777
- opts: { includeComments?: boolean } = {},
778
- ): Promise<IssueDetail> {
590
+ export async function getIssueDetail(client: LinearClient, idArg: string): Promise<IssueDetail> {
779
591
  const { filter, label } = detailFilter(idArg);
780
- const data: any = await withRetry(() =>
781
- (client as any).client.rawRequest(DETAIL_QUERY, {
782
- filter,
783
- includeComments: opts.includeComments !== false,
784
- }),
785
- );
592
+ const data: any = await withRetry(() => (client as any).client.rawRequest(DETAIL_QUERY, { filter }));
786
593
  const n = data.data?.issues?.nodes?.[0];
787
594
  if (!n) throw notFound(`No issue ${label}.`);
788
595
  return {
@@ -806,38 +613,8 @@ export async function getIssueDetail(
806
613
  milestone: n.projectMilestone ?? null,
807
614
  cycle: cycleRef(n.cycle),
808
615
  parent: n.parent ?? null,
809
- children: n.children?.nodes ?? [],
810
616
  labels: n.labels?.nodes ?? [],
811
617
  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,
841
618
  };
842
619
  }
843
620
 
@@ -896,8 +673,7 @@ export async function createIssue(
896
673
  }
897
674
  if (opts.milestone) {
898
675
  // Resolved after the parent so a milestone can name one in the inherited project.
899
- if (!input.projectId)
900
- throw usageError("A milestone requires --project (or a --parent in a project).");
676
+ if (!input.projectId) throw usageError("A milestone requires --project (or a --parent in a project).");
901
677
  input.projectMilestoneId = await resolveMilestoneId(client, input.projectId, opts.milestone);
902
678
  }
903
679
  if (opts.cycle) input.cycleId = await resolveCycleId(client, team.id, opts.cycle);
@@ -927,8 +703,6 @@ export interface UpdateOptions {
927
703
  estimate?: number;
928
704
  parent?: string;
929
705
  dueDate?: string;
930
- /** Replace the complete label set (including `[]` to clear it). */
931
- label?: string[];
932
706
  addLabel?: string[];
933
707
  removeLabel?: string[];
934
708
  /** Clear the assignee. Mutually exclusive with `assignee`. */
@@ -943,8 +717,6 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
943
717
  throw usageError("Pass either --assignee or --unassign, not both.");
944
718
  if (opts.clearCycle && opts.cycle)
945
719
  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.");
948
720
 
949
721
  const issue = await resolveIssue(client, idArg);
950
722
  const currentTeamId = (await issue.team)?.id;
@@ -973,9 +745,7 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
973
745
  if (opts.project) input.projectId = await resolveProjectId(client, opts.project);
974
746
  if (opts.cycle && teamId) input.cycleId = await resolveCycleId(client, teamId, opts.cycle);
975
747
  if (opts.parent) input.parentId = (await resolveIssue(client, opts.parent)).id;
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);
748
+ if (opts.addLabel?.length) input.addedLabelIds = await resolveLabelIds(client, opts.addLabel, teamId);
979
749
  if (opts.removeLabel?.length)
980
750
  input.removedLabelIds = await resolveLabelIds(client, opts.removeLabel, teamId);
981
751
  if (opts.milestone) {
@@ -985,8 +755,7 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
985
755
  input.projectMilestoneId = await resolveMilestoneId(client, projectId, opts.milestone);
986
756
  }
987
757
 
988
- if (Object.keys(input).length === 0)
989
- throw usageError("Nothing to update; pass at least one field.");
758
+ if (Object.keys(input).length === 0) throw usageError("Nothing to update; pass at least one field.");
990
759
  // The updated issue comes from the payload, never from `issue` — falling back
991
760
  // to the pre-mutation entity would print "Updated TES-1" for a write the API
992
761
  // refused.
@@ -1008,19 +777,14 @@ export async function archiveIssue(client: LinearClient, idArg: string, unarchiv
1008
777
 
1009
778
  export async function deleteIssue(client: LinearClient, idArg: string) {
1010
779
  const issue = await resolveIssue(client, idArg);
1011
- await assertMutation(
1012
- withRetry(() => client.deleteIssue(issue.id)),
1013
- "Issue deletion",
1014
- );
780
+ await assertMutation(withRetry(() => client.deleteIssue(issue.id)), "Issue deletion");
1015
781
  return issue;
1016
782
  }
1017
783
 
1018
784
  export async function setSubscription(client: LinearClient, idArg: string, subscribe: boolean) {
1019
785
  const issue = await resolveIssue(client, idArg);
1020
786
  await assertMutation(
1021
- withRetry(() =>
1022
- subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id),
1023
- ),
787
+ withRetry(() => (subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id))),
1024
788
  subscribe ? "Issue subscribe" : "Issue unsubscribe",
1025
789
  );
1026
790
  return issue;
@@ -1047,10 +811,7 @@ export async function moveIssueState(client: LinearClient, issue: Issue, opts: S
1047
811
  const stateId = opts.stateInput
1048
812
  ? await resolveStateId(client, teamId, opts.stateInput)
1049
813
  : await firstStateOfType(client, teamId, "started");
1050
- await assertMutation(
1051
- withRetry(() => client.updateIssue(issue.id, { stateId })),
1052
- "Issue update",
1053
- );
814
+ await assertMutation(withRetry(() => client.updateIssue(issue.id, { stateId })), "Issue update");
1054
815
  }
1055
816
 
1056
817
  export async function startIssue(client: LinearClient, idArg: string, opts: StartMove) {
@@ -1074,11 +835,7 @@ export async function addRemoveRelation(
1074
835
  if (op === "add") {
1075
836
  await assertMutation(
1076
837
  withRetry(() =>
1077
- client.createIssueRelation({
1078
- issueId: from.id,
1079
- relatedIssueId: to.id,
1080
- type: apiType as any,
1081
- }),
838
+ client.createIssueRelation({ issueId: from.id, relatedIssueId: to.id, type: apiType as any }),
1082
839
  ),
1083
840
  "Relation creation",
1084
841
  );
@@ -1091,8 +848,7 @@ export async function addRemoveRelation(
1091
848
  collect((await withRetry(() => from.inverseRelations())) as any, Infinity),
1092
849
  ]);
1093
850
  const match = await findRelation([...direct, ...inverse] as any[], from.id, to.id, apiType);
1094
- if (!match)
1095
- throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
851
+ if (!match) throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
1096
852
  await assertMutation(
1097
853
  withRetry(() => client.deleteIssueRelation(match.id)),
1098
854
  "Relation removal",
@@ -78,14 +78,7 @@ export async function listLabels(
78
78
  filter = { or: [{ team: { key: { eq: team.key } } }, { team: { null: true } }] };
79
79
  }
80
80
 
81
- return collectRawQuery<LabelRow>(
82
- client as any,
83
- LIST_QUERY,
84
- { filter },
85
- "issueLabels",
86
- limit,
87
- toLabelRow,
88
- );
81
+ return collectRawQuery<LabelRow>(client as any, LIST_QUERY, { filter }, "issueLabels", limit, toLabelRow);
89
82
  }
90
83
 
91
84
  /** Map a tailored-query label node to a display row. */
@@ -168,10 +161,7 @@ export async function deleteLabel(client: LinearClient, idArg: string) {
168
161
  const id = await resolveLabel(client, idArg);
169
162
  // Fetch the label first so we can report its name after deletion.
170
163
  const label = await withRetry(() => client.issueLabel(id));
171
- await assertMutation(
172
- withRetry(() => client.deleteIssueLabel(id)),
173
- "Label deletion",
174
- );
164
+ await assertMutation(withRetry(() => client.deleteIssueLabel(id)), "Label deletion");
175
165
  return label;
176
166
  }
177
167
 
@@ -11,12 +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 {
15
- collect,
16
- inheritPaginationMetadata,
17
- pageSize,
18
- pageSizeForMore,
19
- } from "../lib/pagination.js";
14
+ import { collect, pageSizeForMore } from "../lib/pagination.js";
20
15
  import { usageError, notFound } from "../lib/errors.js";
21
16
  import { assertMutation, unwrapMutation } from "../lib/mutation.js";
22
17
  import { resolveProjectId, resolveMilestoneId, isUuid } from "../lib/resolve.js";
@@ -59,9 +54,11 @@ export async function listMilestones(
59
54
  ): Promise<MilestoneRow[]> {
60
55
  const projectId = await resolveProjectId(client, projectInput);
61
56
  const project = await withRetry(() => client.project(projectId));
62
- const conn = await withRetry(() => project.projectMilestones({ first: pageSize(limit) }));
57
+ const conn = await withRetry(() =>
58
+ project.projectMilestones({ first: limit === Infinity ? 100 : Math.min(limit, 100) }),
59
+ );
63
60
  const nodes = await collect(conn as any, limit);
64
- return inheritPaginationMetadata(nodes.map(toRow), nodes);
61
+ return nodes.map(toRow);
65
62
  }
66
63
 
67
64
  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, usageError } from "../lib/errors.js";
17
+ import { normalizeError } from "../lib/errors.js";
18
18
 
19
19
  export interface NotificationRow {
20
20
  id: string;
@@ -122,11 +122,7 @@ export interface MarkAllItem {
122
122
  error?: string;
123
123
  }
124
124
 
125
- export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({
126
- id: "string",
127
- read: "boolean",
128
- "error?": "string",
129
- });
125
+ export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({ id: "string", read: "boolean", "error?": "string" });
130
126
 
131
127
  /**
132
128
  * Mark all of the viewer's unread notifications as read. The SDK's
@@ -172,16 +168,6 @@ export async function archiveNotification(client: LinearClient, id: string): Pro
172
168
 
173
169
  /** Snooze a notification until the given ISO timestamp. */
174
170
  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
- }
185
171
  await assertMutation(
186
172
  withRetry(() => client.updateNotification(id, { snoozedUntilAt: untilISO } as any)),
187
173
  "Notification snooze",
@@ -11,7 +11,8 @@
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, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
14
+ import { collect } from "../lib/pagination.js";
15
+ import { pageSize } from "../lib/pagination.js";
15
16
 
16
17
  export interface OrganizationDetail {
17
18
  id: string;
@@ -82,17 +83,14 @@ export const ORGANIZATION_MEMBER_ROW_SHAPE = shape<MemberRow>({
82
83
  export async function listMembers(client: LinearClient, limit: number): Promise<MemberRow[]> {
83
84
  const conn = await withRetry(() => client.users({ first: pageSize(limit) }));
84
85
  const nodes = await collect(conn as any, limit);
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
- );
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
+ }));
96
94
  }
97
95
 
98
96
  export interface InviteRow {
@@ -130,15 +128,12 @@ export function inviteStatus(invite: { acceptedAt?: unknown; expiresAt?: unknown
130
128
  export async function listInvites(client: LinearClient, limit: number): Promise<InviteRow[]> {
131
129
  const conn = await withRetry(() => client.organizationInvites({ first: pageSize(limit) }));
132
130
  const nodes = await collect(conn as any, limit);
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
- );
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
+ }));
144
139
  }