digital-tools 2.0.2 → 2.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.
- package/CHANGELOG.md +17 -0
- package/package.json +3 -4
- package/src/define.js +267 -0
- package/src/entities/advertising.js +999 -0
- package/src/entities/ai.js +756 -0
- package/src/entities/analytics.js +1588 -0
- package/src/entities/automation.js +601 -0
- package/src/entities/communication.js +1150 -0
- package/src/entities/crm.js +1386 -0
- package/src/entities/design.js +546 -0
- package/src/entities/development.js +2212 -0
- package/src/entities/document.js +874 -0
- package/src/entities/ecommerce.js +1429 -0
- package/src/entities/experiment.js +1039 -0
- package/src/entities/finance.js +3478 -0
- package/src/entities/forms.js +1892 -0
- package/src/entities/hr.js +661 -0
- package/src/entities/identity.js +997 -0
- package/src/entities/index.js +282 -0
- package/src/entities/infrastructure.js +1153 -0
- package/src/entities/knowledge.js +1438 -0
- package/src/entities/marketing.js +1610 -0
- package/src/entities/media.js +1634 -0
- package/src/entities/notification.js +1199 -0
- package/src/entities/presentation.js +1274 -0
- package/src/entities/productivity.js +1317 -0
- package/src/entities/project-management.js +1136 -0
- package/src/entities/recruiting.js +736 -0
- package/src/entities/shipping.js +509 -0
- package/src/entities/signature.js +1102 -0
- package/src/entities/site.js +222 -0
- package/src/entities/spreadsheet.js +1341 -0
- package/src/entities/storage.js +1198 -0
- package/src/entities/support.js +1166 -0
- package/src/entities/video-conferencing.js +1750 -0
- package/src/entities/video.js +950 -0
- package/src/entities.js +1663 -0
- package/src/index.js +74 -0
- package/src/providers/analytics/index.js +17 -0
- package/src/providers/analytics/mixpanel.js +255 -0
- package/src/providers/calendar/cal-com.js +303 -0
- package/src/providers/calendar/google-calendar.js +335 -0
- package/src/providers/calendar/index.js +20 -0
- package/src/providers/crm/hubspot.js +566 -0
- package/src/providers/crm/index.js +17 -0
- package/src/providers/development/github.js +472 -0
- package/src/providers/development/index.js +17 -0
- package/src/providers/ecommerce/index.js +17 -0
- package/src/providers/ecommerce/shopify.js +378 -0
- package/src/providers/email/index.js +20 -0
- package/src/providers/email/resend.js +258 -0
- package/src/providers/email/sendgrid.js +161 -0
- package/src/providers/finance/index.js +17 -0
- package/src/providers/finance/stripe.js +549 -0
- package/src/providers/forms/index.js +17 -0
- package/src/providers/forms/typeform.js +500 -0
- package/src/providers/index.js +123 -0
- package/src/providers/knowledge/index.js +17 -0
- package/src/providers/knowledge/notion.js +389 -0
- package/src/providers/marketing/index.js +17 -0
- package/src/providers/marketing/mailchimp.js +443 -0
- package/src/providers/media/cloudinary.js +318 -0
- package/src/providers/media/index.js +17 -0
- package/src/providers/messaging/index.js +20 -0
- package/src/providers/messaging/slack.js +393 -0
- package/src/providers/messaging/twilio-sms.js +249 -0
- package/src/providers/project-management/index.js +17 -0
- package/src/providers/project-management/linear.js +575 -0
- package/src/providers/registry.js +86 -0
- package/src/providers/spreadsheet/google-sheets.js +375 -0
- package/src/providers/spreadsheet/index.js +20 -0
- package/src/providers/spreadsheet/xlsx.js +423 -0
- package/src/providers/storage/index.js +24 -0
- package/src/providers/storage/s3.js +419 -0
- package/src/providers/support/index.js +17 -0
- package/src/providers/support/zendesk.js +373 -0
- package/src/providers/tasks/index.js +17 -0
- package/src/providers/tasks/todoist.js +286 -0
- package/src/providers/types.js +9 -0
- package/src/providers/video-conferencing/google-meet.js +286 -0
- package/src/providers/video-conferencing/index.js +31 -0
- package/src/providers/video-conferencing/jitsi.js +254 -0
- package/src/providers/video-conferencing/teams.js +270 -0
- package/src/providers/video-conferencing/zoom.js +332 -0
- package/src/registry.js +128 -0
- package/src/tools/communication.js +184 -0
- package/src/tools/data.js +205 -0
- package/src/tools/index.js +11 -0
- package/src/tools/web.js +137 -0
- package/src/types.js +10 -0
- package/test/define.test.js +306 -0
- package/test/registry.test.js +357 -0
- package/test/tools.test.js +363 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Project Management Provider
|
|
3
|
+
*
|
|
4
|
+
* Concrete implementation of ProjectManagementProvider using Linear GraphQL API.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { defineProvider } from '../registry.js';
|
|
9
|
+
const LINEAR_API_URL = 'https://api.linear.app/graphql';
|
|
10
|
+
/**
|
|
11
|
+
* Linear provider info
|
|
12
|
+
*/
|
|
13
|
+
export const linearInfo = {
|
|
14
|
+
id: 'project-management.linear',
|
|
15
|
+
name: 'Linear',
|
|
16
|
+
description: 'Linear issue tracking and project management',
|
|
17
|
+
category: 'project-management',
|
|
18
|
+
website: 'https://linear.app',
|
|
19
|
+
docsUrl: 'https://developers.linear.app',
|
|
20
|
+
requiredConfig: ['apiKey'],
|
|
21
|
+
optionalConfig: [],
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* GraphQL query helper
|
|
25
|
+
*/
|
|
26
|
+
async function graphqlRequest(apiKey, query, variables) {
|
|
27
|
+
const response = await fetch(LINEAR_API_URL, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Authorization: apiKey,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ query, variables }),
|
|
34
|
+
});
|
|
35
|
+
const result = (await response.json());
|
|
36
|
+
if (result.errors) {
|
|
37
|
+
throw new Error(result.errors[0]?.message || 'GraphQL query failed');
|
|
38
|
+
}
|
|
39
|
+
return result.data;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create Linear project management provider
|
|
43
|
+
*/
|
|
44
|
+
export function createLinearProvider(config) {
|
|
45
|
+
let apiKey;
|
|
46
|
+
return {
|
|
47
|
+
info: linearInfo,
|
|
48
|
+
async initialize(cfg) {
|
|
49
|
+
apiKey = cfg.apiKey;
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
throw new Error('Linear API key is required');
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async healthCheck() {
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
await graphqlRequest(apiKey, `query { viewer { id name } }`);
|
|
58
|
+
return {
|
|
59
|
+
healthy: true,
|
|
60
|
+
latencyMs: Date.now() - start,
|
|
61
|
+
message: 'Connected',
|
|
62
|
+
checkedAt: new Date(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
healthy: false,
|
|
68
|
+
latencyMs: Date.now() - start,
|
|
69
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
70
|
+
checkedAt: new Date(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async dispose() {
|
|
75
|
+
// No cleanup needed
|
|
76
|
+
},
|
|
77
|
+
async listProjects(options) {
|
|
78
|
+
const first = options?.limit || 50;
|
|
79
|
+
const after = options?.cursor;
|
|
80
|
+
const query = `
|
|
81
|
+
query($first: Int!, $after: String) {
|
|
82
|
+
projects(first: $first, after: $after) {
|
|
83
|
+
nodes {
|
|
84
|
+
id
|
|
85
|
+
key
|
|
86
|
+
name
|
|
87
|
+
description
|
|
88
|
+
lead {
|
|
89
|
+
id
|
|
90
|
+
}
|
|
91
|
+
url
|
|
92
|
+
}
|
|
93
|
+
pageInfo {
|
|
94
|
+
hasNextPage
|
|
95
|
+
endCursor
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
const data = await graphqlRequest(apiKey, query, { first, after });
|
|
101
|
+
return {
|
|
102
|
+
items: data.projects.nodes.map((p) => ({
|
|
103
|
+
id: p.id,
|
|
104
|
+
key: p.key,
|
|
105
|
+
name: p.name,
|
|
106
|
+
description: p.description,
|
|
107
|
+
lead: p.lead?.id,
|
|
108
|
+
url: p.url,
|
|
109
|
+
})),
|
|
110
|
+
hasMore: data.projects.pageInfo.hasNextPage,
|
|
111
|
+
nextCursor: data.projects.pageInfo.endCursor,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
async getProject(projectId) {
|
|
115
|
+
const query = `
|
|
116
|
+
query($id: String!) {
|
|
117
|
+
project(id: $id) {
|
|
118
|
+
id
|
|
119
|
+
key
|
|
120
|
+
name
|
|
121
|
+
description
|
|
122
|
+
lead {
|
|
123
|
+
id
|
|
124
|
+
}
|
|
125
|
+
url
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
try {
|
|
130
|
+
const data = await graphqlRequest(apiKey, query, { id: projectId });
|
|
131
|
+
if (!data.project) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id: data.project.id,
|
|
136
|
+
key: data.project.key,
|
|
137
|
+
name: data.project.name,
|
|
138
|
+
description: data.project.description,
|
|
139
|
+
lead: data.project.lead?.id,
|
|
140
|
+
url: data.project.url,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
async createIssue(projectId, issue) {
|
|
148
|
+
const query = `
|
|
149
|
+
mutation($input: IssueCreateInput!) {
|
|
150
|
+
issueCreate(input: $input) {
|
|
151
|
+
success
|
|
152
|
+
issue {
|
|
153
|
+
id
|
|
154
|
+
identifier
|
|
155
|
+
title
|
|
156
|
+
description
|
|
157
|
+
state {
|
|
158
|
+
name
|
|
159
|
+
}
|
|
160
|
+
priority
|
|
161
|
+
priorityLabel
|
|
162
|
+
labels {
|
|
163
|
+
nodes {
|
|
164
|
+
name
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
assignee {
|
|
168
|
+
id
|
|
169
|
+
}
|
|
170
|
+
creator {
|
|
171
|
+
id
|
|
172
|
+
}
|
|
173
|
+
createdAt
|
|
174
|
+
updatedAt
|
|
175
|
+
url
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
const input = {
|
|
181
|
+
projectId,
|
|
182
|
+
title: issue.title,
|
|
183
|
+
description: issue.description,
|
|
184
|
+
...(issue.type && { type: issue.type }),
|
|
185
|
+
...(issue.priority && { priority: parsePriority(issue.priority) }),
|
|
186
|
+
...(issue.assigneeId && { assigneeId: issue.assigneeId }),
|
|
187
|
+
...(issue.parentId && { parentId: issue.parentId }),
|
|
188
|
+
...(issue.estimate && { estimate: issue.estimate }),
|
|
189
|
+
};
|
|
190
|
+
if (issue.labels?.length) {
|
|
191
|
+
input.labelIds = issue.labels;
|
|
192
|
+
}
|
|
193
|
+
const data = await graphqlRequest(apiKey, query, { input });
|
|
194
|
+
if (!data.issueCreate.success) {
|
|
195
|
+
throw new Error('Failed to create issue');
|
|
196
|
+
}
|
|
197
|
+
const created = data.issueCreate.issue;
|
|
198
|
+
return {
|
|
199
|
+
id: created.id,
|
|
200
|
+
key: created.identifier,
|
|
201
|
+
title: created.title,
|
|
202
|
+
description: created.description,
|
|
203
|
+
type: issue.type || 'Issue',
|
|
204
|
+
status: created.state.name,
|
|
205
|
+
priority: created.priorityLabel,
|
|
206
|
+
labels: created.labels.nodes.map((l) => l.name),
|
|
207
|
+
assigneeId: created.assignee?.id,
|
|
208
|
+
reporterId: created.creator?.id,
|
|
209
|
+
createdAt: new Date(created.createdAt),
|
|
210
|
+
updatedAt: new Date(created.updatedAt),
|
|
211
|
+
url: created.url,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
async getIssue(issueId) {
|
|
215
|
+
const query = `
|
|
216
|
+
query($id: String!) {
|
|
217
|
+
issue(id: $id) {
|
|
218
|
+
id
|
|
219
|
+
identifier
|
|
220
|
+
title
|
|
221
|
+
description
|
|
222
|
+
state {
|
|
223
|
+
name
|
|
224
|
+
}
|
|
225
|
+
priority
|
|
226
|
+
priorityLabel
|
|
227
|
+
labels {
|
|
228
|
+
nodes {
|
|
229
|
+
name
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
assignee {
|
|
233
|
+
id
|
|
234
|
+
}
|
|
235
|
+
creator {
|
|
236
|
+
id
|
|
237
|
+
}
|
|
238
|
+
createdAt
|
|
239
|
+
updatedAt
|
|
240
|
+
url
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
try {
|
|
245
|
+
const data = await graphqlRequest(apiKey, query, { id: issueId });
|
|
246
|
+
if (!data.issue) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const issue = data.issue;
|
|
250
|
+
return {
|
|
251
|
+
id: issue.id,
|
|
252
|
+
key: issue.identifier,
|
|
253
|
+
title: issue.title,
|
|
254
|
+
description: issue.description,
|
|
255
|
+
type: 'Issue',
|
|
256
|
+
status: issue.state.name,
|
|
257
|
+
priority: issue.priorityLabel,
|
|
258
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
259
|
+
assigneeId: issue.assignee?.id,
|
|
260
|
+
reporterId: issue.creator?.id,
|
|
261
|
+
createdAt: new Date(issue.createdAt),
|
|
262
|
+
updatedAt: new Date(issue.updatedAt),
|
|
263
|
+
url: issue.url,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
async updateIssue(issueId, updates) {
|
|
271
|
+
const query = `
|
|
272
|
+
mutation($id: String!, $input: IssueUpdateInput!) {
|
|
273
|
+
issueUpdate(id: $id, input: $input) {
|
|
274
|
+
success
|
|
275
|
+
issue {
|
|
276
|
+
id
|
|
277
|
+
identifier
|
|
278
|
+
title
|
|
279
|
+
description
|
|
280
|
+
state {
|
|
281
|
+
name
|
|
282
|
+
}
|
|
283
|
+
priority
|
|
284
|
+
priorityLabel
|
|
285
|
+
labels {
|
|
286
|
+
nodes {
|
|
287
|
+
name
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
assignee {
|
|
291
|
+
id
|
|
292
|
+
}
|
|
293
|
+
creator {
|
|
294
|
+
id
|
|
295
|
+
}
|
|
296
|
+
createdAt
|
|
297
|
+
updatedAt
|
|
298
|
+
url
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
const input = {};
|
|
304
|
+
if (updates.title)
|
|
305
|
+
input.title = updates.title;
|
|
306
|
+
if (updates.description !== undefined)
|
|
307
|
+
input.description = updates.description;
|
|
308
|
+
if (updates.priority)
|
|
309
|
+
input.priority = parsePriority(updates.priority);
|
|
310
|
+
if (updates.assigneeId)
|
|
311
|
+
input.assigneeId = updates.assigneeId;
|
|
312
|
+
if (updates.estimate !== undefined)
|
|
313
|
+
input.estimate = updates.estimate;
|
|
314
|
+
const data = await graphqlRequest(apiKey, query, { id: issueId, input });
|
|
315
|
+
if (!data.issueUpdate.success) {
|
|
316
|
+
throw new Error('Failed to update issue');
|
|
317
|
+
}
|
|
318
|
+
const updated = data.issueUpdate.issue;
|
|
319
|
+
return {
|
|
320
|
+
id: updated.id,
|
|
321
|
+
key: updated.identifier,
|
|
322
|
+
title: updated.title,
|
|
323
|
+
description: updated.description,
|
|
324
|
+
type: 'Issue',
|
|
325
|
+
status: updated.state.name,
|
|
326
|
+
priority: updated.priorityLabel,
|
|
327
|
+
labels: updated.labels.nodes.map((l) => l.name),
|
|
328
|
+
assigneeId: updated.assignee?.id,
|
|
329
|
+
reporterId: updated.creator?.id,
|
|
330
|
+
createdAt: new Date(updated.createdAt),
|
|
331
|
+
updatedAt: new Date(updated.updatedAt),
|
|
332
|
+
url: updated.url,
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
async deleteIssue(issueId) {
|
|
336
|
+
const query = `
|
|
337
|
+
mutation($id: String!) {
|
|
338
|
+
issueDelete(id: $id) {
|
|
339
|
+
success
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
`;
|
|
343
|
+
try {
|
|
344
|
+
const data = await graphqlRequest(apiKey, query, { id: issueId });
|
|
345
|
+
return data.issueDelete.success;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
async listIssues(projectId, options) {
|
|
352
|
+
const first = options?.limit || 50;
|
|
353
|
+
const after = options?.cursor;
|
|
354
|
+
// Build filter
|
|
355
|
+
const filter = { project: { id: { eq: projectId } } };
|
|
356
|
+
if (options?.status?.length) {
|
|
357
|
+
filter.state = { name: { in: options.status } };
|
|
358
|
+
}
|
|
359
|
+
if (options?.assignee) {
|
|
360
|
+
filter.assignee = { id: { eq: options.assignee } };
|
|
361
|
+
}
|
|
362
|
+
if (options?.labels?.length) {
|
|
363
|
+
filter.labels = { name: { in: options.labels } };
|
|
364
|
+
}
|
|
365
|
+
const query = `
|
|
366
|
+
query($first: Int!, $after: String, $filter: IssueFilter) {
|
|
367
|
+
issues(first: $first, after: $after, filter: $filter) {
|
|
368
|
+
nodes {
|
|
369
|
+
id
|
|
370
|
+
identifier
|
|
371
|
+
title
|
|
372
|
+
description
|
|
373
|
+
state {
|
|
374
|
+
name
|
|
375
|
+
}
|
|
376
|
+
priority
|
|
377
|
+
priorityLabel
|
|
378
|
+
labels {
|
|
379
|
+
nodes {
|
|
380
|
+
name
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
assignee {
|
|
384
|
+
id
|
|
385
|
+
}
|
|
386
|
+
creator {
|
|
387
|
+
id
|
|
388
|
+
}
|
|
389
|
+
createdAt
|
|
390
|
+
updatedAt
|
|
391
|
+
url
|
|
392
|
+
}
|
|
393
|
+
pageInfo {
|
|
394
|
+
hasNextPage
|
|
395
|
+
endCursor
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
`;
|
|
400
|
+
const data = await graphqlRequest(apiKey, query, { first, after, filter });
|
|
401
|
+
return {
|
|
402
|
+
items: data.issues.nodes.map((issue) => ({
|
|
403
|
+
id: issue.id,
|
|
404
|
+
key: issue.identifier,
|
|
405
|
+
title: issue.title,
|
|
406
|
+
description: issue.description,
|
|
407
|
+
type: 'Issue',
|
|
408
|
+
status: issue.state.name,
|
|
409
|
+
priority: issue.priorityLabel,
|
|
410
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
411
|
+
assigneeId: issue.assignee?.id,
|
|
412
|
+
reporterId: issue.creator?.id,
|
|
413
|
+
createdAt: new Date(issue.createdAt),
|
|
414
|
+
updatedAt: new Date(issue.updatedAt),
|
|
415
|
+
url: issue.url,
|
|
416
|
+
})),
|
|
417
|
+
hasMore: data.issues.pageInfo.hasNextPage,
|
|
418
|
+
nextCursor: data.issues.pageInfo.endCursor,
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
async searchIssues(query, options) {
|
|
422
|
+
const first = options?.limit || 50;
|
|
423
|
+
const after = options?.cursor;
|
|
424
|
+
const graphqlQuery = `
|
|
425
|
+
query($first: Int!, $after: String, $filter: IssueFilter) {
|
|
426
|
+
issueSearch(first: $first, after: $after, query: $query, filter: $filter) {
|
|
427
|
+
nodes {
|
|
428
|
+
id
|
|
429
|
+
identifier
|
|
430
|
+
title
|
|
431
|
+
description
|
|
432
|
+
state {
|
|
433
|
+
name
|
|
434
|
+
}
|
|
435
|
+
priority
|
|
436
|
+
priorityLabel
|
|
437
|
+
labels {
|
|
438
|
+
nodes {
|
|
439
|
+
name
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
assignee {
|
|
443
|
+
id
|
|
444
|
+
}
|
|
445
|
+
creator {
|
|
446
|
+
id
|
|
447
|
+
}
|
|
448
|
+
createdAt
|
|
449
|
+
updatedAt
|
|
450
|
+
url
|
|
451
|
+
}
|
|
452
|
+
pageInfo {
|
|
453
|
+
hasNextPage
|
|
454
|
+
endCursor
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
// Build filter
|
|
460
|
+
const filter = {};
|
|
461
|
+
if (options?.status?.length) {
|
|
462
|
+
filter.state = { name: { in: options.status } };
|
|
463
|
+
}
|
|
464
|
+
if (options?.assignee) {
|
|
465
|
+
filter.assignee = { id: { eq: options.assignee } };
|
|
466
|
+
}
|
|
467
|
+
if (options?.labels?.length) {
|
|
468
|
+
filter.labels = { name: { in: options.labels } };
|
|
469
|
+
}
|
|
470
|
+
const data = await graphqlRequest(apiKey, graphqlQuery, { first, after, query, filter });
|
|
471
|
+
return {
|
|
472
|
+
items: data.issueSearch.nodes.map((issue) => ({
|
|
473
|
+
id: issue.id,
|
|
474
|
+
key: issue.identifier,
|
|
475
|
+
title: issue.title,
|
|
476
|
+
description: issue.description,
|
|
477
|
+
type: 'Issue',
|
|
478
|
+
status: issue.state.name,
|
|
479
|
+
priority: issue.priorityLabel,
|
|
480
|
+
labels: issue.labels.nodes.map((l) => l.name),
|
|
481
|
+
assigneeId: issue.assignee?.id,
|
|
482
|
+
reporterId: issue.creator?.id,
|
|
483
|
+
createdAt: new Date(issue.createdAt),
|
|
484
|
+
updatedAt: new Date(issue.updatedAt),
|
|
485
|
+
url: issue.url,
|
|
486
|
+
})),
|
|
487
|
+
hasMore: data.issueSearch.pageInfo.hasNextPage,
|
|
488
|
+
nextCursor: data.issueSearch.pageInfo.endCursor,
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
async addComment(issueId, body) {
|
|
492
|
+
const query = `
|
|
493
|
+
mutation($input: CommentCreateInput!) {
|
|
494
|
+
commentCreate(input: $input) {
|
|
495
|
+
success
|
|
496
|
+
comment {
|
|
497
|
+
id
|
|
498
|
+
body
|
|
499
|
+
user {
|
|
500
|
+
id
|
|
501
|
+
}
|
|
502
|
+
createdAt
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
`;
|
|
507
|
+
const data = await graphqlRequest(apiKey, query, {
|
|
508
|
+
input: {
|
|
509
|
+
issueId,
|
|
510
|
+
body,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
if (!data.commentCreate.success) {
|
|
514
|
+
throw new Error('Failed to create comment');
|
|
515
|
+
}
|
|
516
|
+
const comment = data.commentCreate.comment;
|
|
517
|
+
return {
|
|
518
|
+
id: comment.id,
|
|
519
|
+
issueId,
|
|
520
|
+
body: comment.body,
|
|
521
|
+
authorId: comment.user.id,
|
|
522
|
+
createdAt: new Date(comment.createdAt),
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
async transition(issueId, statusId) {
|
|
526
|
+
const query = `
|
|
527
|
+
mutation($id: String!, $stateId: String!) {
|
|
528
|
+
issueUpdate(id: $id, input: { stateId: $stateId }) {
|
|
529
|
+
success
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
`;
|
|
533
|
+
try {
|
|
534
|
+
const data = await graphqlRequest(apiKey, query, { id: issueId, stateId: statusId });
|
|
535
|
+
return data.issueUpdate.success;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
async assign(issueId, userId) {
|
|
542
|
+
const query = `
|
|
543
|
+
mutation($id: String!, $assigneeId: String!) {
|
|
544
|
+
issueUpdate(id: $id, input: { assigneeId: $assigneeId }) {
|
|
545
|
+
success
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
`;
|
|
549
|
+
try {
|
|
550
|
+
const data = await graphqlRequest(apiKey, query, { id: issueId, assigneeId: userId });
|
|
551
|
+
return data.issueUpdate.success;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Parse priority string to Linear priority number
|
|
561
|
+
*/
|
|
562
|
+
function parsePriority(priority) {
|
|
563
|
+
const map = {
|
|
564
|
+
urgent: 1,
|
|
565
|
+
high: 2,
|
|
566
|
+
medium: 3,
|
|
567
|
+
low: 4,
|
|
568
|
+
none: 0,
|
|
569
|
+
};
|
|
570
|
+
return map[priority.toLowerCase()] ?? 0;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Linear provider definition
|
|
574
|
+
*/
|
|
575
|
+
export const linearProvider = defineProvider(linearInfo, async (config) => createLinearProvider(config));
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Registry Implementation
|
|
3
|
+
*
|
|
4
|
+
* Central registry for discovering and instantiating providers.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Create a new provider registry
|
|
10
|
+
*/
|
|
11
|
+
export function createProviderRegistry() {
|
|
12
|
+
const providers = new Map();
|
|
13
|
+
return {
|
|
14
|
+
register(info, factory) {
|
|
15
|
+
if (providers.has(info.id)) {
|
|
16
|
+
throw new Error(`Provider '${info.id}' is already registered`);
|
|
17
|
+
}
|
|
18
|
+
providers.set(info.id, { info, factory });
|
|
19
|
+
},
|
|
20
|
+
get(providerId) {
|
|
21
|
+
return providers.get(providerId);
|
|
22
|
+
},
|
|
23
|
+
list(category) {
|
|
24
|
+
const all = Array.from(providers.values());
|
|
25
|
+
if (category) {
|
|
26
|
+
return all.filter((p) => p.info.category === category);
|
|
27
|
+
}
|
|
28
|
+
return all;
|
|
29
|
+
},
|
|
30
|
+
async create(providerId, config) {
|
|
31
|
+
const registered = providers.get(providerId);
|
|
32
|
+
if (!registered) {
|
|
33
|
+
throw new Error(`Provider '${providerId}' not found. Available: ${Array.from(providers.keys()).join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
// Validate required config
|
|
36
|
+
const missing = registered.info.requiredConfig.filter((key) => !(key in config));
|
|
37
|
+
if (missing.length > 0) {
|
|
38
|
+
throw new Error(`Provider '${providerId}' missing required config: ${missing.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const provider = await registered.factory(config);
|
|
41
|
+
await provider.initialize(config);
|
|
42
|
+
return provider;
|
|
43
|
+
},
|
|
44
|
+
has(providerId) {
|
|
45
|
+
return providers.has(providerId);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Global provider registry instance
|
|
51
|
+
*/
|
|
52
|
+
export const providerRegistry = createProviderRegistry();
|
|
53
|
+
/**
|
|
54
|
+
* Register a provider in the global registry
|
|
55
|
+
*/
|
|
56
|
+
export function registerProvider(info, factory) {
|
|
57
|
+
providerRegistry.register(info, factory);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get a provider from the global registry
|
|
61
|
+
*/
|
|
62
|
+
export function getProvider(providerId) {
|
|
63
|
+
return providerRegistry.get(providerId);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create a provider instance from the global registry
|
|
67
|
+
*/
|
|
68
|
+
export async function createProvider(providerId, config) {
|
|
69
|
+
return providerRegistry.create(providerId, config);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* List providers by category
|
|
73
|
+
*/
|
|
74
|
+
export function listProviders(category) {
|
|
75
|
+
return providerRegistry.list(category);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Helper to define a provider with type safety
|
|
79
|
+
*/
|
|
80
|
+
export function defineProvider(info, factory) {
|
|
81
|
+
return {
|
|
82
|
+
info,
|
|
83
|
+
factory,
|
|
84
|
+
register: () => registerProvider(info, factory),
|
|
85
|
+
};
|
|
86
|
+
}
|