@renseiai/agentfactory-linear 0.8.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/src/agent-client-project-repo.test.d.ts +2 -0
  4. package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
  5. package/dist/src/agent-client-project-repo.test.js +153 -0
  6. package/dist/src/agent-client.d.ts +261 -0
  7. package/dist/src/agent-client.d.ts.map +1 -0
  8. package/dist/src/agent-client.js +902 -0
  9. package/dist/src/agent-session.d.ts +303 -0
  10. package/dist/src/agent-session.d.ts.map +1 -0
  11. package/dist/src/agent-session.js +969 -0
  12. package/dist/src/checkbox-utils.d.ts +88 -0
  13. package/dist/src/checkbox-utils.d.ts.map +1 -0
  14. package/dist/src/checkbox-utils.js +120 -0
  15. package/dist/src/circuit-breaker.d.ts +76 -0
  16. package/dist/src/circuit-breaker.d.ts.map +1 -0
  17. package/dist/src/circuit-breaker.js +229 -0
  18. package/dist/src/circuit-breaker.test.d.ts +2 -0
  19. package/dist/src/circuit-breaker.test.d.ts.map +1 -0
  20. package/dist/src/circuit-breaker.test.js +292 -0
  21. package/dist/src/constants.d.ts +87 -0
  22. package/dist/src/constants.d.ts.map +1 -0
  23. package/dist/src/constants.js +101 -0
  24. package/dist/src/defaults/auto-trigger.d.ts +35 -0
  25. package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
  26. package/dist/src/defaults/auto-trigger.js +36 -0
  27. package/dist/src/defaults/index.d.ts +12 -0
  28. package/dist/src/defaults/index.d.ts.map +1 -0
  29. package/dist/src/defaults/index.js +11 -0
  30. package/dist/src/defaults/priority.d.ts +20 -0
  31. package/dist/src/defaults/priority.d.ts.map +1 -0
  32. package/dist/src/defaults/priority.js +37 -0
  33. package/dist/src/defaults/prompts.d.ts +42 -0
  34. package/dist/src/defaults/prompts.d.ts.map +1 -0
  35. package/dist/src/defaults/prompts.js +310 -0
  36. package/dist/src/defaults/prompts.test.d.ts +2 -0
  37. package/dist/src/defaults/prompts.test.d.ts.map +1 -0
  38. package/dist/src/defaults/prompts.test.js +263 -0
  39. package/dist/src/defaults/work-type-detection.d.ts +19 -0
  40. package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
  41. package/dist/src/defaults/work-type-detection.js +93 -0
  42. package/dist/src/errors.d.ts +91 -0
  43. package/dist/src/errors.d.ts.map +1 -0
  44. package/dist/src/errors.js +173 -0
  45. package/dist/src/frontend-adapter.d.ts +168 -0
  46. package/dist/src/frontend-adapter.d.ts.map +1 -0
  47. package/dist/src/frontend-adapter.js +314 -0
  48. package/dist/src/frontend-adapter.test.d.ts +2 -0
  49. package/dist/src/frontend-adapter.test.d.ts.map +1 -0
  50. package/dist/src/frontend-adapter.test.js +545 -0
  51. package/dist/src/index.d.ts +28 -0
  52. package/dist/src/index.d.ts.map +1 -0
  53. package/dist/src/index.js +30 -0
  54. package/dist/src/issue-tracker-proxy.d.ts +140 -0
  55. package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
  56. package/dist/src/issue-tracker-proxy.js +10 -0
  57. package/dist/src/platform-adapter.d.ts +132 -0
  58. package/dist/src/platform-adapter.d.ts.map +1 -0
  59. package/dist/src/platform-adapter.js +260 -0
  60. package/dist/src/platform-adapter.test.d.ts +2 -0
  61. package/dist/src/platform-adapter.test.d.ts.map +1 -0
  62. package/dist/src/platform-adapter.test.js +468 -0
  63. package/dist/src/proxy-client.d.ts +103 -0
  64. package/dist/src/proxy-client.d.ts.map +1 -0
  65. package/dist/src/proxy-client.js +191 -0
  66. package/dist/src/rate-limiter.d.ts +64 -0
  67. package/dist/src/rate-limiter.d.ts.map +1 -0
  68. package/dist/src/rate-limiter.js +163 -0
  69. package/dist/src/rate-limiter.test.d.ts +2 -0
  70. package/dist/src/rate-limiter.test.d.ts.map +1 -0
  71. package/dist/src/rate-limiter.test.js +217 -0
  72. package/dist/src/retry.d.ts +59 -0
  73. package/dist/src/retry.d.ts.map +1 -0
  74. package/dist/src/retry.js +82 -0
  75. package/dist/src/types.d.ts +492 -0
  76. package/dist/src/types.d.ts.map +1 -0
  77. package/dist/src/types.js +143 -0
  78. package/dist/src/utils.d.ts +52 -0
  79. package/dist/src/utils.d.ts.map +1 -0
  80. package/dist/src/utils.js +277 -0
  81. package/dist/src/webhook-types.d.ts +308 -0
  82. package/dist/src/webhook-types.d.ts.map +1 -0
  83. package/dist/src/webhook-types.js +46 -0
  84. package/package.json +70 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * LinearFrontendAdapter
3
+ *
4
+ * Implements the WorkSchedulingFrontend interface (defined in @renseiai/agentfactory)
5
+ * by wrapping the LinearAgentClient. This adapter translates between abstract,
6
+ * frontend-agnostic types and Linear-specific concepts.
7
+ *
8
+ * Structural typing note: This class structurally satisfies the WorkSchedulingFrontend
9
+ * interface from @renseiai/agentfactory without an explicit `implements` clause, avoiding
10
+ * a circular package dependency (core depends on linear at runtime, linear depends on
11
+ * core only for types). Consumers who import both packages can assign this to
12
+ * WorkSchedulingFrontend.
13
+ */
14
+ // ---------------------------------------------------------------------------
15
+ // Status mapping
16
+ // ---------------------------------------------------------------------------
17
+ const ABSTRACT_TO_LINEAR = {
18
+ icebox: 'Icebox',
19
+ backlog: 'Backlog',
20
+ started: 'Started',
21
+ finished: 'Finished',
22
+ delivered: 'Delivered',
23
+ accepted: 'Accepted',
24
+ rejected: 'Rejected',
25
+ canceled: 'Canceled',
26
+ };
27
+ const LINEAR_TO_ABSTRACT = {
28
+ Icebox: 'icebox',
29
+ Backlog: 'backlog',
30
+ Started: 'started',
31
+ Finished: 'finished',
32
+ Delivered: 'delivered',
33
+ Accepted: 'accepted',
34
+ Rejected: 'rejected',
35
+ Canceled: 'canceled',
36
+ };
37
+ // ---------------------------------------------------------------------------
38
+ // Helpers
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Map a Linear SDK Issue to an AbstractIssue.
42
+ */
43
+ async function toAbstractIssue(issue) {
44
+ const state = await issue.state;
45
+ const labels = await issue.labels();
46
+ const parent = await issue.parent;
47
+ const project = await issue.project;
48
+ const nativeStatus = state?.name ?? 'Backlog';
49
+ const status = LINEAR_TO_ABSTRACT[nativeStatus] ?? 'backlog';
50
+ return {
51
+ id: issue.id,
52
+ identifier: issue.identifier,
53
+ title: issue.title,
54
+ description: issue.description ?? undefined,
55
+ url: issue.url,
56
+ status,
57
+ priority: issue.priority,
58
+ labels: labels.nodes.map((l) => l.name),
59
+ parentId: parent?.id,
60
+ project: project?.name,
61
+ createdAt: issue.createdAt,
62
+ };
63
+ }
64
+ /**
65
+ * Map a Linear SDK Comment to an AbstractComment.
66
+ */
67
+ async function toAbstractComment(comment) {
68
+ const user = await comment.user;
69
+ return {
70
+ id: comment.id,
71
+ body: comment.body,
72
+ userId: user?.id,
73
+ userName: user?.name,
74
+ createdAt: comment.createdAt,
75
+ };
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // Adapter
79
+ // ---------------------------------------------------------------------------
80
+ /**
81
+ * Linear frontend adapter that wraps LinearAgentClient.
82
+ *
83
+ * Structurally satisfies WorkSchedulingFrontend from @renseiai/agentfactory.
84
+ */
85
+ export class LinearFrontendAdapter {
86
+ client;
87
+ name = 'linear';
88
+ constructor(client) {
89
+ this.client = client;
90
+ }
91
+ // ---- Status mapping ----
92
+ /**
93
+ * Resolve an abstract status to its Linear-native name.
94
+ */
95
+ resolveStatus(abstract) {
96
+ return ABSTRACT_TO_LINEAR[abstract];
97
+ }
98
+ /**
99
+ * Map a Linear-native status name to its abstract equivalent.
100
+ * Defaults to 'backlog' for unknown statuses.
101
+ */
102
+ abstractStatus(nativeStatus) {
103
+ return LINEAR_TO_ABSTRACT[nativeStatus] ?? 'backlog';
104
+ }
105
+ // ---- Read operations ----
106
+ /**
107
+ * Fetch an issue by ID or identifier and map to AbstractIssue.
108
+ */
109
+ async getIssue(id) {
110
+ const issue = await this.client.getIssue(id);
111
+ return toAbstractIssue(issue);
112
+ }
113
+ /**
114
+ * List issues in a project filtered by abstract status.
115
+ *
116
+ * Uses the underlying LinearClient directly since LinearAgentClient
117
+ * does not expose a project-scoped issue listing method.
118
+ */
119
+ async listIssuesByStatus(project, status) {
120
+ const nativeStatus = this.resolveStatus(status);
121
+ const linearClient = this.client.linearClient;
122
+ const issueConnection = await linearClient.issues({
123
+ filter: {
124
+ project: { name: { eq: project } },
125
+ state: { name: { eq: nativeStatus } },
126
+ },
127
+ });
128
+ const results = [];
129
+ for (const issue of issueConnection.nodes) {
130
+ results.push(await toAbstractIssue(issue));
131
+ }
132
+ return results;
133
+ }
134
+ /**
135
+ * Get comments for an issue and map to AbstractComment[].
136
+ */
137
+ async getIssueComments(id) {
138
+ const comments = await this.client.getIssueComments(id);
139
+ const results = [];
140
+ for (const comment of comments) {
141
+ results.push(await toAbstractComment(comment));
142
+ }
143
+ return results;
144
+ }
145
+ /**
146
+ * List issues in a project filtered by abstract status, excluding
147
+ * those that are blocked by other issues (have incoming "blocks" relations).
148
+ */
149
+ async getUnblockedIssues(project, status) {
150
+ const allIssues = await this.listIssuesByStatus(project, status);
151
+ const results = [];
152
+ for (const issue of allIssues) {
153
+ const relations = await this.client.getIssueRelations(issue.id);
154
+ // An issue is blocked if another issue has an outgoing "blocks" relation to it
155
+ // (appears as an inverse relation with type "blocks")
156
+ const isBlocked = relations.inverseRelations.some((r) => r.type === 'blocks');
157
+ if (!isBlocked) {
158
+ results.push(issue);
159
+ }
160
+ }
161
+ return results;
162
+ }
163
+ /**
164
+ * Check if an issue has child issues (is a parent issue).
165
+ */
166
+ async isParentIssue(id) {
167
+ return this.client.isParentIssue(id);
168
+ }
169
+ /**
170
+ * Check if an issue has a parent (is a child/sub-issue).
171
+ */
172
+ async isChildIssue(id) {
173
+ return this.client.isChildIssue(id);
174
+ }
175
+ /**
176
+ * Get sub-issues of a parent issue, mapped to AbstractIssue[].
177
+ */
178
+ async getSubIssues(id) {
179
+ const subIssues = await this.client.getSubIssues(id);
180
+ const results = [];
181
+ for (const issue of subIssues) {
182
+ results.push(await toAbstractIssue(issue));
183
+ }
184
+ return results;
185
+ }
186
+ // ---- Write operations ----
187
+ /**
188
+ * Transition an issue to a new status.
189
+ */
190
+ async transitionIssue(id, status) {
191
+ const nativeStatus = this.resolveStatus(status);
192
+ // LinearAgentClient.updateIssueStatus expects LinearWorkflowStatus
193
+ // which does not include 'Icebox', so we handle that case via updateIssue
194
+ if (nativeStatus === 'Icebox') {
195
+ const issue = await this.client.getIssue(id);
196
+ const team = await issue.team;
197
+ if (team) {
198
+ const statuses = await this.client.getTeamStatuses(team.id);
199
+ const stateId = statuses['Icebox'];
200
+ if (stateId) {
201
+ await this.client.updateIssue(id, { stateId });
202
+ return;
203
+ }
204
+ }
205
+ throw new Error(`Cannot transition issue ${id} to Icebox: status not found`);
206
+ }
207
+ await this.client.updateIssueStatus(id, nativeStatus);
208
+ }
209
+ /**
210
+ * Create a comment on an issue.
211
+ */
212
+ async createComment(id, body) {
213
+ await this.client.createComment(id, body);
214
+ }
215
+ /**
216
+ * Create a blocker issue that blocks the source issue.
217
+ * Creates the issue in Icebox with a "Needs Human" label (if found),
218
+ * then creates a "blocks" relation from the blocker to the source.
219
+ */
220
+ async createBlockerIssue(sourceId, data) {
221
+ // Determine the team — use provided teamId or look it up from the source issue
222
+ let teamId = data.teamId;
223
+ if (!teamId) {
224
+ const sourceIssue = await this.client.getIssue(sourceId);
225
+ const team = await sourceIssue.team;
226
+ if (!team) {
227
+ throw new Error(`Cannot create blocker: source issue ${sourceId} has no team`);
228
+ }
229
+ teamId = team.id;
230
+ }
231
+ // Find the Icebox state
232
+ const statuses = await this.client.getTeamStatuses(teamId);
233
+ const iceboxStateId = statuses['Icebox'];
234
+ // Find the "Needs Human" label
235
+ const allLabels = await this.client.linearClient.issueLabels();
236
+ const needsHumanLabel = allLabels.nodes.find((l) => l.name.toLowerCase() === 'needs human');
237
+ const issue = await this.client.createIssue({
238
+ title: data.title,
239
+ description: data.description,
240
+ teamId,
241
+ projectId: data.projectId,
242
+ ...(iceboxStateId && { stateId: iceboxStateId }),
243
+ ...(needsHumanLabel && { labelIds: [needsHumanLabel.id] }),
244
+ });
245
+ // Create blocking relation: blocker blocks source issue
246
+ await this.client.createIssueRelation({
247
+ issueId: issue.id,
248
+ relatedIssueId: sourceId,
249
+ type: 'blocks',
250
+ });
251
+ return toAbstractIssue(issue);
252
+ }
253
+ /**
254
+ * Create a new issue and return it as AbstractIssue.
255
+ */
256
+ async createIssue(data) {
257
+ // Resolve status name to state ID if provided
258
+ let stateId;
259
+ if (data.status && data.teamId) {
260
+ const nativeStatus = this.resolveStatus(data.status);
261
+ const statuses = await this.client.getTeamStatuses(data.teamId);
262
+ stateId = statuses[nativeStatus];
263
+ }
264
+ const issue = await this.client.createIssue({
265
+ title: data.title,
266
+ teamId: data.teamId,
267
+ description: data.description,
268
+ projectId: data.projectId,
269
+ stateId,
270
+ parentId: data.parentId,
271
+ priority: data.priority,
272
+ });
273
+ return toAbstractIssue(issue);
274
+ }
275
+ // ---- Agent session operations ----
276
+ /**
277
+ * Create an agent session on an issue.
278
+ * Returns the session ID.
279
+ */
280
+ async createAgentSession(issueId, externalUrls) {
281
+ const result = await this.client.createAgentSessionOnIssue({
282
+ issueId,
283
+ externalUrls,
284
+ });
285
+ if (!result.sessionId) {
286
+ throw new Error(`Failed to create agent session on issue: ${issueId}`);
287
+ }
288
+ return result.sessionId;
289
+ }
290
+ /**
291
+ * Update an existing agent session.
292
+ */
293
+ async updateAgentSession(sessionId, data) {
294
+ await this.client.updateAgentSession({
295
+ sessionId,
296
+ externalUrls: data.externalUrls,
297
+ plan: data.plan,
298
+ });
299
+ }
300
+ /**
301
+ * Create an activity on an agent session.
302
+ * Wraps the content as a ThoughtActivityContent.
303
+ */
304
+ async createActivity(sessionId, type, content) {
305
+ const activityContent = {
306
+ type: 'thought',
307
+ body: content,
308
+ };
309
+ await this.client.createAgentActivity({
310
+ agentSessionId: sessionId,
311
+ content: activityContent,
312
+ });
313
+ }
314
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=frontend-adapter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontend-adapter.test.d.ts","sourceRoot":"","sources":["../../src/frontend-adapter.test.ts"],"names":[],"mappings":""}