@hasna/connectors 0.3.16 → 0.4.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.
Files changed (127) hide show
  1. package/bin/index.js +71 -1
  2. package/bin/mcp.js +71 -1
  3. package/bin/serve.js +70 -0
  4. package/connectors/connect-asana/.env.example +11 -0
  5. package/connectors/connect-asana/CLAUDE.md +128 -0
  6. package/connectors/connect-asana/README.md +193 -0
  7. package/connectors/connect-asana/package.json +52 -0
  8. package/connectors/connect-asana/src/api/client.ts +119 -0
  9. package/connectors/connect-asana/src/api/index.ts +319 -0
  10. package/connectors/connect-asana/src/cli/index.ts +731 -0
  11. package/connectors/connect-asana/src/index.ts +19 -0
  12. package/connectors/connect-asana/src/types/index.ts +270 -0
  13. package/connectors/connect-asana/src/utils/config.ts +171 -0
  14. package/connectors/connect-asana/src/utils/output.ts +119 -0
  15. package/connectors/connect-asana/tsconfig.json +16 -0
  16. package/connectors/connect-clickup/.env.example +11 -0
  17. package/connectors/connect-clickup/CLAUDE.md +128 -0
  18. package/connectors/connect-clickup/README.md +193 -0
  19. package/connectors/connect-clickup/package.json +52 -0
  20. package/connectors/connect-clickup/src/api/client.ts +116 -0
  21. package/connectors/connect-clickup/src/api/index.ts +400 -0
  22. package/connectors/connect-clickup/src/cli/index.ts +625 -0
  23. package/connectors/connect-clickup/src/index.ts +19 -0
  24. package/connectors/connect-clickup/src/types/index.ts +591 -0
  25. package/connectors/connect-clickup/src/utils/config.ts +157 -0
  26. package/connectors/connect-clickup/src/utils/output.ts +119 -0
  27. package/connectors/connect-clickup/tsconfig.json +16 -0
  28. package/connectors/connect-confluence/.env.example +11 -0
  29. package/connectors/connect-confluence/CLAUDE.md +272 -0
  30. package/connectors/connect-confluence/README.md +193 -0
  31. package/connectors/connect-confluence/package.json +53 -0
  32. package/connectors/connect-confluence/scripts/release.ts +179 -0
  33. package/connectors/connect-confluence/src/api/client.ts +213 -0
  34. package/connectors/connect-confluence/src/api/example.ts +48 -0
  35. package/connectors/connect-confluence/src/api/index.ts +51 -0
  36. package/connectors/connect-confluence/src/cli/index.ts +254 -0
  37. package/connectors/connect-confluence/src/index.ts +103 -0
  38. package/connectors/connect-confluence/src/types/index.ts +237 -0
  39. package/connectors/connect-confluence/src/utils/auth.ts +274 -0
  40. package/connectors/connect-confluence/src/utils/bulk.ts +212 -0
  41. package/connectors/connect-confluence/src/utils/config.ts +326 -0
  42. package/connectors/connect-confluence/src/utils/output.ts +175 -0
  43. package/connectors/connect-confluence/src/utils/settings.ts +114 -0
  44. package/connectors/connect-confluence/src/utils/storage.ts +198 -0
  45. package/connectors/connect-confluence/tsconfig.json +16 -0
  46. package/connectors/connect-jira/.env.example +11 -0
  47. package/connectors/connect-jira/CLAUDE.md +128 -0
  48. package/connectors/connect-jira/README.md +193 -0
  49. package/connectors/connect-jira/package.json +53 -0
  50. package/connectors/connect-jira/src/api/client.ts +131 -0
  51. package/connectors/connect-jira/src/api/index.ts +266 -0
  52. package/connectors/connect-jira/src/cli/index.ts +653 -0
  53. package/connectors/connect-jira/src/index.ts +23 -0
  54. package/connectors/connect-jira/src/types/index.ts +448 -0
  55. package/connectors/connect-jira/src/utils/config.ts +179 -0
  56. package/connectors/connect-jira/src/utils/output.ts +119 -0
  57. package/connectors/connect-jira/tsconfig.json +16 -0
  58. package/connectors/connect-linear/CLAUDE.md +88 -0
  59. package/connectors/connect-linear/README.md +201 -0
  60. package/connectors/connect-linear/package.json +45 -0
  61. package/connectors/connect-linear/src/api/client.ts +62 -0
  62. package/connectors/connect-linear/src/api/index.ts +46 -0
  63. package/connectors/connect-linear/src/api/issues.ts +247 -0
  64. package/connectors/connect-linear/src/api/projects.ts +179 -0
  65. package/connectors/connect-linear/src/api/teams.ts +125 -0
  66. package/connectors/connect-linear/src/api/users.ts +112 -0
  67. package/connectors/connect-linear/src/cli/index.ts +560 -0
  68. package/connectors/connect-linear/src/index.ts +27 -0
  69. package/connectors/connect-linear/src/types/index.ts +275 -0
  70. package/connectors/connect-linear/src/utils/config.ts +249 -0
  71. package/connectors/connect-linear/src/utils/output.ts +119 -0
  72. package/connectors/connect-linear/tsconfig.json +16 -0
  73. package/connectors/connect-slack/.env.example +7 -0
  74. package/connectors/connect-slack/CLAUDE.md +69 -0
  75. package/connectors/connect-slack/README.md +150 -0
  76. package/connectors/connect-slack/package.json +44 -0
  77. package/connectors/connect-slack/src/api/channels.ts +112 -0
  78. package/connectors/connect-slack/src/api/client.ts +97 -0
  79. package/connectors/connect-slack/src/api/index.ts +42 -0
  80. package/connectors/connect-slack/src/api/messages.ts +127 -0
  81. package/connectors/connect-slack/src/api/users.ts +110 -0
  82. package/connectors/connect-slack/src/cli/index.ts +494 -0
  83. package/connectors/connect-slack/src/index.ts +21 -0
  84. package/connectors/connect-slack/src/types/index.ts +263 -0
  85. package/connectors/connect-slack/src/utils/config.ts +297 -0
  86. package/connectors/connect-slack/src/utils/output.ts +119 -0
  87. package/connectors/connect-slack/tsconfig.json +16 -0
  88. package/connectors/connect-telegram/.env.example +2 -0
  89. package/connectors/connect-telegram/package.json +49 -0
  90. package/connectors/connect-todoist/.env.example +11 -0
  91. package/connectors/connect-todoist/CLAUDE.md +104 -0
  92. package/connectors/connect-todoist/README.md +193 -0
  93. package/connectors/connect-todoist/package.json +52 -0
  94. package/connectors/connect-todoist/src/api/client.ts +117 -0
  95. package/connectors/connect-todoist/src/api/index.ts +188 -0
  96. package/connectors/connect-todoist/src/cli/index.ts +990 -0
  97. package/connectors/connect-todoist/src/index.ts +21 -0
  98. package/connectors/connect-todoist/src/types/index.ts +240 -0
  99. package/connectors/connect-todoist/src/utils/config.ts +157 -0
  100. package/connectors/connect-todoist/src/utils/output.ts +119 -0
  101. package/connectors/connect-todoist/tsconfig.json +16 -0
  102. package/connectors/connect-trello/.env.example +11 -0
  103. package/connectors/connect-trello/CLAUDE.md +128 -0
  104. package/connectors/connect-trello/README.md +193 -0
  105. package/connectors/connect-trello/package.json +53 -0
  106. package/connectors/connect-trello/src/api/client.ts +128 -0
  107. package/connectors/connect-trello/src/api/index.ts +278 -0
  108. package/connectors/connect-trello/src/cli/index.ts +737 -0
  109. package/connectors/connect-trello/src/index.ts +21 -0
  110. package/connectors/connect-trello/src/types/index.ts +314 -0
  111. package/connectors/connect-trello/src/utils/config.ts +182 -0
  112. package/connectors/connect-trello/src/utils/output.ts +119 -0
  113. package/connectors/connect-trello/tsconfig.json +16 -0
  114. package/connectors/connect-whatsapp/.env.example +11 -0
  115. package/connectors/connect-whatsapp/CLAUDE.md +113 -0
  116. package/connectors/connect-whatsapp/README.md +193 -0
  117. package/connectors/connect-whatsapp/package.json +53 -0
  118. package/connectors/connect-whatsapp/src/api/client.ts +133 -0
  119. package/connectors/connect-whatsapp/src/api/index.ts +365 -0
  120. package/connectors/connect-whatsapp/src/cli/index.ts +686 -0
  121. package/connectors/connect-whatsapp/src/index.ts +25 -0
  122. package/connectors/connect-whatsapp/src/types/index.ts +502 -0
  123. package/connectors/connect-whatsapp/src/utils/config.ts +179 -0
  124. package/connectors/connect-whatsapp/src/utils/output.ts +119 -0
  125. package/connectors/connect-whatsapp/tsconfig.json +16 -0
  126. package/dist/index.js +70 -0
  127. package/package.json +1 -1
@@ -0,0 +1,247 @@
1
+ import type { LinearClient } from './client';
2
+ import type {
3
+ LinearIssue,
4
+ IssueListOptions,
5
+ CreateIssueInput,
6
+ UpdateIssueInput,
7
+ IssuesResponse,
8
+ IssueResponse,
9
+ CreateIssueResponse,
10
+ UpdateIssueResponse,
11
+ ArchiveIssueResponse,
12
+ IssueFilter,
13
+ } from '../types';
14
+
15
+ const ISSUE_FRAGMENT = `
16
+ fragment IssueFragment on Issue {
17
+ id
18
+ identifier
19
+ title
20
+ description
21
+ priority
22
+ priorityLabel
23
+ estimate
24
+ sortOrder
25
+ boardOrder
26
+ startedAt
27
+ completedAt
28
+ canceledAt
29
+ dueDate
30
+ createdAt
31
+ updatedAt
32
+ archivedAt
33
+ url
34
+ number
35
+ team {
36
+ id
37
+ name
38
+ key
39
+ }
40
+ state {
41
+ id
42
+ name
43
+ color
44
+ type
45
+ }
46
+ assignee {
47
+ id
48
+ name
49
+ displayName
50
+ email
51
+ }
52
+ creator {
53
+ id
54
+ name
55
+ displayName
56
+ }
57
+ labels {
58
+ nodes {
59
+ id
60
+ name
61
+ color
62
+ }
63
+ }
64
+ project {
65
+ id
66
+ name
67
+ }
68
+ }
69
+ `;
70
+
71
+ export class IssuesApi {
72
+ constructor(private readonly client: LinearClient) {}
73
+
74
+ /**
75
+ * List issues with optional filters
76
+ */
77
+ async list(options: IssueListOptions = {}): Promise<LinearIssue[]> {
78
+ const { first = 50, after, teamId, projectId, assigneeId, filter } = options;
79
+
80
+ // Build filter object
81
+ const filterObj: IssueFilter = filter || {};
82
+ if (teamId) {
83
+ filterObj.team = { id: { eq: teamId } };
84
+ }
85
+ if (projectId) {
86
+ filterObj.project = { id: { eq: projectId } };
87
+ }
88
+ if (assigneeId) {
89
+ filterObj.assignee = { id: { eq: assigneeId } };
90
+ }
91
+
92
+ const query = `
93
+ ${ISSUE_FRAGMENT}
94
+ query Issues($first: Int, $after: String, $filter: IssueFilter) {
95
+ issues(first: $first, after: $after, filter: $filter) {
96
+ nodes {
97
+ ...IssueFragment
98
+ }
99
+ pageInfo {
100
+ hasNextPage
101
+ endCursor
102
+ }
103
+ }
104
+ }
105
+ `;
106
+
107
+ const result = await this.client.query<IssuesResponse>(query, {
108
+ first,
109
+ after,
110
+ filter: Object.keys(filterObj).length > 0 ? filterObj : undefined,
111
+ });
112
+
113
+ return result.issues.nodes;
114
+ }
115
+
116
+ /**
117
+ * Get a single issue by ID
118
+ */
119
+ async get(id: string): Promise<LinearIssue> {
120
+ const query = `
121
+ ${ISSUE_FRAGMENT}
122
+ query Issue($id: String!) {
123
+ issue(id: $id) {
124
+ ...IssueFragment
125
+ }
126
+ }
127
+ `;
128
+
129
+ const result = await this.client.query<IssueResponse>(query, { id });
130
+ return result.issue;
131
+ }
132
+
133
+ /**
134
+ * Create a new issue
135
+ */
136
+ async create(input: CreateIssueInput): Promise<LinearIssue> {
137
+ const mutation = `
138
+ ${ISSUE_FRAGMENT}
139
+ mutation CreateIssue($input: IssueCreateInput!) {
140
+ issueCreate(input: $input) {
141
+ success
142
+ issue {
143
+ ...IssueFragment
144
+ }
145
+ }
146
+ }
147
+ `;
148
+
149
+ const result = await this.client.mutate<CreateIssueResponse>(mutation, {
150
+ input,
151
+ });
152
+
153
+ if (!result.issueCreate.success || !result.issueCreate.issue) {
154
+ throw new Error('Failed to create issue');
155
+ }
156
+
157
+ return result.issueCreate.issue;
158
+ }
159
+
160
+ /**
161
+ * Update an existing issue
162
+ */
163
+ async update(id: string, input: UpdateIssueInput): Promise<LinearIssue> {
164
+ const mutation = `
165
+ ${ISSUE_FRAGMENT}
166
+ mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
167
+ issueUpdate(id: $id, input: $input) {
168
+ success
169
+ issue {
170
+ ...IssueFragment
171
+ }
172
+ }
173
+ }
174
+ `;
175
+
176
+ const result = await this.client.mutate<UpdateIssueResponse>(mutation, {
177
+ id,
178
+ input,
179
+ });
180
+
181
+ if (!result.issueUpdate.success || !result.issueUpdate.issue) {
182
+ throw new Error('Failed to update issue');
183
+ }
184
+
185
+ return result.issueUpdate.issue;
186
+ }
187
+
188
+ /**
189
+ * Archive an issue (soft delete)
190
+ */
191
+ async archive(id: string): Promise<boolean> {
192
+ const mutation = `
193
+ mutation ArchiveIssue($id: String!) {
194
+ issueArchive(id: $id) {
195
+ success
196
+ }
197
+ }
198
+ `;
199
+
200
+ const result = await this.client.mutate<ArchiveIssueResponse>(mutation, { id });
201
+ return result.issueArchive.success;
202
+ }
203
+
204
+ /**
205
+ * Change issue state by state ID
206
+ */
207
+ async changeState(id: string, stateId: string): Promise<LinearIssue> {
208
+ return this.update(id, { stateId });
209
+ }
210
+
211
+ /**
212
+ * Assign issue to a user
213
+ */
214
+ async assign(id: string, assigneeId: string): Promise<LinearIssue> {
215
+ return this.update(id, { assigneeId });
216
+ }
217
+
218
+ /**
219
+ * Add issue to a project
220
+ */
221
+ async addToProject(id: string, projectId: string): Promise<LinearIssue> {
222
+ return this.update(id, { projectId });
223
+ }
224
+
225
+ /**
226
+ * Search issues by query text
227
+ */
228
+ async search(query: string, first = 20): Promise<LinearIssue[]> {
229
+ const gqlQuery = `
230
+ ${ISSUE_FRAGMENT}
231
+ query SearchIssues($query: String!, $first: Int) {
232
+ issueSearch(query: $query, first: $first) {
233
+ nodes {
234
+ ...IssueFragment
235
+ }
236
+ }
237
+ }
238
+ `;
239
+
240
+ const result = await this.client.query<{ issueSearch: { nodes: LinearIssue[] } }>(
241
+ gqlQuery,
242
+ { query, first }
243
+ );
244
+
245
+ return result.issueSearch.nodes;
246
+ }
247
+ }
@@ -0,0 +1,179 @@
1
+ import type { LinearClient } from './client';
2
+ import type { LinearProject, ProjectsResponse, ProjectResponse, ListOptions } from '../types';
3
+
4
+ const PROJECT_FRAGMENT = `
5
+ fragment ProjectFragment on Project {
6
+ id
7
+ name
8
+ description
9
+ icon
10
+ color
11
+ state
12
+ progress
13
+ startDate
14
+ targetDate
15
+ createdAt
16
+ updatedAt
17
+ lead {
18
+ id
19
+ name
20
+ displayName
21
+ email
22
+ }
23
+ teams {
24
+ nodes {
25
+ id
26
+ name
27
+ key
28
+ }
29
+ }
30
+ }
31
+ `;
32
+
33
+ export interface CreateProjectInput {
34
+ name: string;
35
+ description?: string;
36
+ teamIds: string[];
37
+ leadId?: string;
38
+ startDate?: string;
39
+ targetDate?: string;
40
+ color?: string;
41
+ icon?: string;
42
+ }
43
+
44
+ export interface UpdateProjectInput {
45
+ name?: string;
46
+ description?: string;
47
+ leadId?: string;
48
+ startDate?: string;
49
+ targetDate?: string;
50
+ color?: string;
51
+ icon?: string;
52
+ state?: string;
53
+ }
54
+
55
+ interface ProjectPayload {
56
+ success: boolean;
57
+ project?: LinearProject;
58
+ }
59
+
60
+ interface CreateProjectResponse {
61
+ projectCreate: ProjectPayload;
62
+ }
63
+
64
+ interface UpdateProjectResponse {
65
+ projectUpdate: ProjectPayload;
66
+ }
67
+
68
+ export class ProjectsApi {
69
+ constructor(private readonly client: LinearClient) {}
70
+
71
+ /**
72
+ * List all projects
73
+ */
74
+ async list(options: ListOptions = {}): Promise<LinearProject[]> {
75
+ const { first = 50, after } = options;
76
+
77
+ const query = `
78
+ ${PROJECT_FRAGMENT}
79
+ query Projects($first: Int, $after: String) {
80
+ projects(first: $first, after: $after) {
81
+ nodes {
82
+ ...ProjectFragment
83
+ }
84
+ pageInfo {
85
+ hasNextPage
86
+ endCursor
87
+ }
88
+ }
89
+ }
90
+ `;
91
+
92
+ const result = await this.client.query<ProjectsResponse>(query, {
93
+ first,
94
+ after,
95
+ });
96
+
97
+ return result.projects.nodes;
98
+ }
99
+
100
+ /**
101
+ * Get a single project by ID
102
+ */
103
+ async get(id: string): Promise<LinearProject> {
104
+ const query = `
105
+ ${PROJECT_FRAGMENT}
106
+ query Project($id: String!) {
107
+ project(id: $id) {
108
+ ...ProjectFragment
109
+ }
110
+ }
111
+ `;
112
+
113
+ const result = await this.client.query<ProjectResponse>(query, { id });
114
+ return result.project;
115
+ }
116
+
117
+ /**
118
+ * Create a new project
119
+ */
120
+ async create(input: CreateProjectInput): Promise<LinearProject> {
121
+ const mutation = `
122
+ ${PROJECT_FRAGMENT}
123
+ mutation CreateProject($input: ProjectCreateInput!) {
124
+ projectCreate(input: $input) {
125
+ success
126
+ project {
127
+ ...ProjectFragment
128
+ }
129
+ }
130
+ }
131
+ `;
132
+
133
+ const result = await this.client.mutate<CreateProjectResponse>(mutation, {
134
+ input,
135
+ });
136
+
137
+ if (!result.projectCreate.success || !result.projectCreate.project) {
138
+ throw new Error('Failed to create project');
139
+ }
140
+
141
+ return result.projectCreate.project;
142
+ }
143
+
144
+ /**
145
+ * Update a project
146
+ */
147
+ async update(id: string, input: UpdateProjectInput): Promise<LinearProject> {
148
+ const mutation = `
149
+ ${PROJECT_FRAGMENT}
150
+ mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {
151
+ projectUpdate(id: $id, input: $input) {
152
+ success
153
+ project {
154
+ ...ProjectFragment
155
+ }
156
+ }
157
+ }
158
+ `;
159
+
160
+ const result = await this.client.mutate<UpdateProjectResponse>(mutation, {
161
+ id,
162
+ input,
163
+ });
164
+
165
+ if (!result.projectUpdate.success || !result.projectUpdate.project) {
166
+ throw new Error('Failed to update project');
167
+ }
168
+
169
+ return result.projectUpdate.project;
170
+ }
171
+
172
+ /**
173
+ * Search projects by name
174
+ */
175
+ async findByName(name: string): Promise<LinearProject | undefined> {
176
+ const projects = await this.list();
177
+ return projects.find(p => p.name.toLowerCase() === name.toLowerCase());
178
+ }
179
+ }
@@ -0,0 +1,125 @@
1
+ import type { LinearClient } from './client';
2
+ import type { LinearTeam, LinearWorkflowState, TeamsResponse, TeamResponse, ListOptions } from '../types';
3
+
4
+ const TEAM_FRAGMENT = `
5
+ fragment TeamFragment on Team {
6
+ id
7
+ name
8
+ key
9
+ description
10
+ icon
11
+ color
12
+ private
13
+ createdAt
14
+ updatedAt
15
+ }
16
+ `;
17
+
18
+ const WORKFLOW_STATE_FRAGMENT = `
19
+ fragment WorkflowStateFragment on WorkflowState {
20
+ id
21
+ name
22
+ color
23
+ type
24
+ position
25
+ }
26
+ `;
27
+
28
+ interface WorkflowStatesResponse {
29
+ workflowStates: {
30
+ nodes: LinearWorkflowState[];
31
+ };
32
+ }
33
+
34
+ export class TeamsApi {
35
+ constructor(private readonly client: LinearClient) {}
36
+
37
+ /**
38
+ * List all teams
39
+ */
40
+ async list(options: ListOptions = {}): Promise<LinearTeam[]> {
41
+ const { first = 50, after } = options;
42
+
43
+ const query = `
44
+ ${TEAM_FRAGMENT}
45
+ query Teams($first: Int, $after: String) {
46
+ teams(first: $first, after: $after) {
47
+ nodes {
48
+ ...TeamFragment
49
+ }
50
+ pageInfo {
51
+ hasNextPage
52
+ endCursor
53
+ }
54
+ }
55
+ }
56
+ `;
57
+
58
+ const result = await this.client.query<TeamsResponse>(query, {
59
+ first,
60
+ after,
61
+ });
62
+
63
+ return result.teams.nodes;
64
+ }
65
+
66
+ /**
67
+ * Get a single team by ID
68
+ */
69
+ async get(id: string): Promise<LinearTeam> {
70
+ const query = `
71
+ ${TEAM_FRAGMENT}
72
+ query Team($id: String!) {
73
+ team(id: $id) {
74
+ ...TeamFragment
75
+ }
76
+ }
77
+ `;
78
+
79
+ const result = await this.client.query<TeamResponse>(query, { id });
80
+ return result.team;
81
+ }
82
+
83
+ /**
84
+ * Find team by key (e.g., "ENG", "DES")
85
+ */
86
+ async findByKey(key: string): Promise<LinearTeam | undefined> {
87
+ const teams = await this.list();
88
+ return teams.find(t => t.key.toLowerCase() === key.toLowerCase());
89
+ }
90
+
91
+ /**
92
+ * Find team by name
93
+ */
94
+ async findByName(name: string): Promise<LinearTeam | undefined> {
95
+ const teams = await this.list();
96
+ return teams.find(t => t.name.toLowerCase() === name.toLowerCase());
97
+ }
98
+
99
+ /**
100
+ * Get workflow states for a team
101
+ */
102
+ async getWorkflowStates(teamId: string): Promise<LinearWorkflowState[]> {
103
+ const query = `
104
+ ${WORKFLOW_STATE_FRAGMENT}
105
+ query WorkflowStates($teamId: String!) {
106
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
107
+ nodes {
108
+ ...WorkflowStateFragment
109
+ }
110
+ }
111
+ }
112
+ `;
113
+
114
+ const result = await this.client.query<WorkflowStatesResponse>(query, { teamId });
115
+ return result.workflowStates.nodes;
116
+ }
117
+
118
+ /**
119
+ * Get a specific workflow state by name for a team
120
+ */
121
+ async findWorkflowState(teamId: string, stateName: string): Promise<LinearWorkflowState | undefined> {
122
+ const states = await this.getWorkflowStates(teamId);
123
+ return states.find(s => s.name.toLowerCase() === stateName.toLowerCase());
124
+ }
125
+ }
@@ -0,0 +1,112 @@
1
+ import type { LinearClient } from './client';
2
+ import type { LinearUser, UsersResponse, UserResponse, ViewerResponse, ListOptions } from '../types';
3
+
4
+ const USER_FRAGMENT = `
5
+ fragment UserFragment on User {
6
+ id
7
+ name
8
+ displayName
9
+ email
10
+ avatarUrl
11
+ active
12
+ admin
13
+ createdAt
14
+ updatedAt
15
+ }
16
+ `;
17
+
18
+ export class UsersApi {
19
+ constructor(private readonly client: LinearClient) {}
20
+
21
+ /**
22
+ * List all users
23
+ */
24
+ async list(options: ListOptions = {}): Promise<LinearUser[]> {
25
+ const { first = 100, after } = options;
26
+
27
+ const query = `
28
+ ${USER_FRAGMENT}
29
+ query Users($first: Int, $after: String) {
30
+ users(first: $first, after: $after) {
31
+ nodes {
32
+ ...UserFragment
33
+ }
34
+ pageInfo {
35
+ hasNextPage
36
+ endCursor
37
+ }
38
+ }
39
+ }
40
+ `;
41
+
42
+ const result = await this.client.query<UsersResponse>(query, {
43
+ first,
44
+ after,
45
+ });
46
+
47
+ return result.users.nodes;
48
+ }
49
+
50
+ /**
51
+ * Get a single user by ID
52
+ */
53
+ async get(id: string): Promise<LinearUser> {
54
+ const query = `
55
+ ${USER_FRAGMENT}
56
+ query User($id: String!) {
57
+ user(id: $id) {
58
+ ...UserFragment
59
+ }
60
+ }
61
+ `;
62
+
63
+ const result = await this.client.query<UserResponse>(query, { id });
64
+ return result.user;
65
+ }
66
+
67
+ /**
68
+ * Get the currently authenticated user
69
+ */
70
+ async me(): Promise<LinearUser> {
71
+ const query = `
72
+ ${USER_FRAGMENT}
73
+ query Viewer {
74
+ viewer {
75
+ ...UserFragment
76
+ }
77
+ }
78
+ `;
79
+
80
+ const result = await this.client.query<ViewerResponse>(query);
81
+ return result.viewer;
82
+ }
83
+
84
+ /**
85
+ * Find user by email
86
+ */
87
+ async findByEmail(email: string): Promise<LinearUser | undefined> {
88
+ const users = await this.list();
89
+ return users.find(u => u.email.toLowerCase() === email.toLowerCase());
90
+ }
91
+
92
+ /**
93
+ * Find user by name or display name
94
+ */
95
+ async findByName(name: string): Promise<LinearUser | undefined> {
96
+ const users = await this.list();
97
+ const lowerName = name.toLowerCase();
98
+ return users.find(
99
+ u =>
100
+ u.name.toLowerCase() === lowerName ||
101
+ u.displayName.toLowerCase() === lowerName
102
+ );
103
+ }
104
+
105
+ /**
106
+ * List only active users
107
+ */
108
+ async listActive(options: ListOptions = {}): Promise<LinearUser[]> {
109
+ const users = await this.list(options);
110
+ return users.filter(u => u.active);
111
+ }
112
+ }