cyrus-core 0.2.4 → 0.2.6
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/dist/CyrusAgentSession.d.ts +14 -0
- package/dist/CyrusAgentSession.d.ts.map +1 -1
- package/dist/agent-runner-types.d.ts +97 -0
- package/dist/agent-runner-types.d.ts.map +1 -1
- package/dist/config-types.d.ts +22 -4
- package/dist/config-types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/issue-tracker/IIssueTrackerService.d.ts +18 -5
- package/dist/issue-tracker/IIssueTrackerService.d.ts.map +1 -1
- package/dist/issue-tracker/adapters/CLIEventTransport.d.ts +91 -0
- package/dist/issue-tracker/adapters/CLIEventTransport.d.ts.map +1 -0
- package/dist/issue-tracker/adapters/CLIEventTransport.js +104 -0
- package/dist/issue-tracker/adapters/CLIEventTransport.js.map +1 -0
- package/dist/issue-tracker/adapters/CLIIssueTrackerService.d.ts +239 -0
- package/dist/issue-tracker/adapters/CLIIssueTrackerService.d.ts.map +1 -0
- package/dist/issue-tracker/adapters/CLIIssueTrackerService.js +1112 -0
- package/dist/issue-tracker/adapters/CLIIssueTrackerService.js.map +1 -0
- package/dist/issue-tracker/adapters/CLIRPCServer.d.ts +318 -0
- package/dist/issue-tracker/adapters/CLIRPCServer.d.ts.map +1 -0
- package/dist/issue-tracker/adapters/CLIRPCServer.js +536 -0
- package/dist/issue-tracker/adapters/CLIRPCServer.js.map +1 -0
- package/dist/issue-tracker/adapters/CLITypes.d.ts +243 -0
- package/dist/issue-tracker/adapters/CLITypes.d.ts.map +1 -0
- package/dist/issue-tracker/adapters/CLITypes.js +378 -0
- package/dist/issue-tracker/adapters/CLITypes.js.map +1 -0
- package/dist/issue-tracker/adapters/index.d.ts +14 -0
- package/dist/issue-tracker/adapters/index.d.ts.map +1 -0
- package/dist/issue-tracker/adapters/index.js +11 -0
- package/dist/issue-tracker/adapters/index.js.map +1 -0
- package/dist/issue-tracker/index.d.ts +2 -1
- package/dist/issue-tracker/index.d.ts.map +1 -1
- package/dist/issue-tracker/index.js +3 -2
- package/dist/issue-tracker/index.js.map +1 -1
- package/dist/issue-tracker/types.d.ts +141 -29
- package/dist/issue-tracker/types.d.ts.map +1 -1
- package/dist/issue-tracker/types.js.map +1 -1
- package/package.json +30 -30
- package/LICENSE +0 -674
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI/in-memory implementation of IIssueTrackerService.
|
|
3
|
+
*
|
|
4
|
+
* This adapter provides an in-memory mock of Linear's issue tracking platform
|
|
5
|
+
* for testing purposes. It implements all methods from IIssueTrackerService
|
|
6
|
+
* while storing data in memory using Maps for O(1) lookups.
|
|
7
|
+
*
|
|
8
|
+
* Unlike Linear's async properties, this implementation uses synchronous properties
|
|
9
|
+
* for immediate access to related entities.
|
|
10
|
+
*
|
|
11
|
+
* @module issue-tracker/adapters/CLIIssueTrackerService
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
14
|
+
import { AgentActivityType, AgentSessionStatus, AgentSessionType, } from "../types.js";
|
|
15
|
+
import { CLIEventTransport } from "./CLIEventTransport.js";
|
|
16
|
+
import { createCLIAgentSession, createCLIComment, createCLIIssue, createCLILabel, createCLITeam, createCLIUser, createCLIWorkflowState, } from "./CLITypes.js";
|
|
17
|
+
/**
|
|
18
|
+
* CLI implementation of IIssueTrackerService.
|
|
19
|
+
*
|
|
20
|
+
* This class provides an in-memory implementation of the issue tracker service
|
|
21
|
+
* for testing purposes. All data is stored in Maps with synchronous property access.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const service = new CLIIssueTrackerService();
|
|
26
|
+
*
|
|
27
|
+
* // Fetch an issue
|
|
28
|
+
* const issue = await service.fetchIssue('issue-1');
|
|
29
|
+
*
|
|
30
|
+
* // Create a comment
|
|
31
|
+
* const comment = await service.createComment(issue.id, {
|
|
32
|
+
* body: 'This is a comment'
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class CLIIssueTrackerService extends EventEmitter {
|
|
37
|
+
state;
|
|
38
|
+
eventTransport = null;
|
|
39
|
+
/**
|
|
40
|
+
* Create a new CLIIssueTrackerService.
|
|
41
|
+
*
|
|
42
|
+
* @param initialState - Optional initial state (useful for testing)
|
|
43
|
+
*/
|
|
44
|
+
constructor(initialState) {
|
|
45
|
+
super();
|
|
46
|
+
this.state = {
|
|
47
|
+
issues: initialState?.issues ?? new Map(),
|
|
48
|
+
comments: initialState?.comments ?? new Map(),
|
|
49
|
+
teams: initialState?.teams ?? new Map(),
|
|
50
|
+
labels: initialState?.labels ?? new Map(),
|
|
51
|
+
workflowStates: initialState?.workflowStates ?? new Map(),
|
|
52
|
+
users: initialState?.users ?? new Map(),
|
|
53
|
+
agentSessions: initialState?.agentSessions ?? new Map(),
|
|
54
|
+
agentActivities: initialState?.agentActivities ?? new Map(),
|
|
55
|
+
currentUserId: initialState?.currentUserId ?? "user-default",
|
|
56
|
+
issueCounter: initialState?.issueCounter ?? 1,
|
|
57
|
+
commentCounter: initialState?.commentCounter ?? 1,
|
|
58
|
+
sessionCounter: initialState?.sessionCounter ?? 1,
|
|
59
|
+
activityCounter: initialState?.activityCounter ?? 1,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// ========================================================================
|
|
63
|
+
// ISSUE OPERATIONS
|
|
64
|
+
// ========================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Fetch a single issue by ID or identifier.
|
|
67
|
+
*/
|
|
68
|
+
async fetchIssue(idOrIdentifier) {
|
|
69
|
+
// Try to find by ID first
|
|
70
|
+
let issueData = this.state.issues.get(idOrIdentifier);
|
|
71
|
+
// If not found, try to find by identifier
|
|
72
|
+
if (!issueData) {
|
|
73
|
+
for (const [, candidateIssue] of this.state.issues) {
|
|
74
|
+
if (candidateIssue.identifier === idOrIdentifier) {
|
|
75
|
+
issueData = candidateIssue;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!issueData) {
|
|
81
|
+
throw new Error(`Issue ${idOrIdentifier} not found`);
|
|
82
|
+
}
|
|
83
|
+
// Resolve label data
|
|
84
|
+
const resolvedLabels = issueData.labelIds
|
|
85
|
+
.map((id) => this.state.labels.get(id))
|
|
86
|
+
.filter((l) => l !== undefined);
|
|
87
|
+
return createCLIIssue(issueData, resolvedLabels);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create a new issue in a team.
|
|
91
|
+
*
|
|
92
|
+
* @param input - Issue creation parameters
|
|
93
|
+
* @returns Promise resolving to the created issue
|
|
94
|
+
*/
|
|
95
|
+
async createIssue(input) {
|
|
96
|
+
// Validate team exists
|
|
97
|
+
const team = await this.fetchTeam(input.teamId);
|
|
98
|
+
// Validate state if provided
|
|
99
|
+
if (input.stateId) {
|
|
100
|
+
const state = this.state.workflowStates.get(input.stateId);
|
|
101
|
+
if (!state) {
|
|
102
|
+
throw new Error(`Workflow state ${input.stateId} not found`);
|
|
103
|
+
}
|
|
104
|
+
if (state.teamId !== team.id) {
|
|
105
|
+
throw new Error(`Workflow state ${input.stateId} does not belong to team ${team.id}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Validate assignee if provided
|
|
109
|
+
if (input.assigneeId) {
|
|
110
|
+
const assignee = this.state.users.get(input.assigneeId);
|
|
111
|
+
if (!assignee) {
|
|
112
|
+
throw new Error(`User ${input.assigneeId} not found`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Validate parent if provided
|
|
116
|
+
if (input.parentId) {
|
|
117
|
+
await this.fetchIssue(input.parentId);
|
|
118
|
+
}
|
|
119
|
+
// Validate labels if provided
|
|
120
|
+
if (input.labelIds && input.labelIds.length > 0) {
|
|
121
|
+
for (const labelId of input.labelIds) {
|
|
122
|
+
const label = this.state.labels.get(labelId);
|
|
123
|
+
if (!label) {
|
|
124
|
+
throw new Error(`Label ${labelId} not found`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Generate issue number and ID
|
|
129
|
+
const issueNumber = this.state.issueCounter++;
|
|
130
|
+
const issueId = `issue-${issueNumber}`;
|
|
131
|
+
const identifier = `${team.key}-${issueNumber}`;
|
|
132
|
+
// Create the issue data
|
|
133
|
+
const issueData = {
|
|
134
|
+
id: issueId,
|
|
135
|
+
identifier,
|
|
136
|
+
title: input.title,
|
|
137
|
+
description: input.description,
|
|
138
|
+
number: issueNumber,
|
|
139
|
+
url: `https://linear.app/test/issue/${identifier}`,
|
|
140
|
+
branchName: `${team.key.toLowerCase()}-${issueNumber}-${input.title.toLowerCase().replace(/\s+/g, "-").slice(0, 30)}`,
|
|
141
|
+
priority: input.priority ?? 0,
|
|
142
|
+
priorityLabel: this.getPriorityLabel(input.priority ?? 0),
|
|
143
|
+
boardOrder: 0,
|
|
144
|
+
sortOrder: 0,
|
|
145
|
+
prioritySortOrder: 0,
|
|
146
|
+
labelIds: input.labelIds ?? [],
|
|
147
|
+
previousIdentifiers: [],
|
|
148
|
+
customerTicketCount: 0,
|
|
149
|
+
createdAt: new Date(),
|
|
150
|
+
updatedAt: new Date(),
|
|
151
|
+
teamId: team.id,
|
|
152
|
+
stateId: input.stateId,
|
|
153
|
+
assigneeId: input.assigneeId,
|
|
154
|
+
parentId: input.parentId,
|
|
155
|
+
};
|
|
156
|
+
// Save to state
|
|
157
|
+
this.state.issues.set(issueId, issueData);
|
|
158
|
+
// Resolve label data
|
|
159
|
+
const resolvedLabels = issueData.labelIds
|
|
160
|
+
.map((id) => this.state.labels.get(id))
|
|
161
|
+
.filter((l) => l !== undefined);
|
|
162
|
+
// Create and return the issue
|
|
163
|
+
const issue = createCLIIssue(issueData, resolvedLabels);
|
|
164
|
+
// Emit state change event
|
|
165
|
+
this.emit("issue:created", { issue });
|
|
166
|
+
return issue;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get priority label from priority number.
|
|
170
|
+
*/
|
|
171
|
+
getPriorityLabel(priority) {
|
|
172
|
+
switch (priority) {
|
|
173
|
+
case 1:
|
|
174
|
+
return "Urgent";
|
|
175
|
+
case 2:
|
|
176
|
+
return "High";
|
|
177
|
+
case 3:
|
|
178
|
+
return "Normal";
|
|
179
|
+
case 4:
|
|
180
|
+
return "Low";
|
|
181
|
+
default:
|
|
182
|
+
return "No priority";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Fetch child issues (sub-issues) for a parent issue.
|
|
187
|
+
*/
|
|
188
|
+
async fetchIssueChildren(issueId, options) {
|
|
189
|
+
const parentIssue = await this.fetchIssue(issueId);
|
|
190
|
+
// Find all child issues
|
|
191
|
+
const allChildren = [];
|
|
192
|
+
for (const [, issueData] of this.state.issues) {
|
|
193
|
+
if (issueData.parentId === parentIssue.id) {
|
|
194
|
+
const resolvedLabels = issueData.labelIds
|
|
195
|
+
.map((id) => this.state.labels.get(id))
|
|
196
|
+
.filter((l) => l !== undefined);
|
|
197
|
+
allChildren.push(createCLIIssue(issueData, resolvedLabels));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Apply filters
|
|
201
|
+
let filteredChildren = allChildren;
|
|
202
|
+
if (options?.includeCompleted === false) {
|
|
203
|
+
filteredChildren = filteredChildren.filter((child) => {
|
|
204
|
+
const childStateId = child.stateId;
|
|
205
|
+
if (!childStateId)
|
|
206
|
+
return true;
|
|
207
|
+
const state = this.state.workflowStates.get(childStateId);
|
|
208
|
+
return state?.type !== "completed";
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (options?.includeArchived === false) {
|
|
212
|
+
filteredChildren = filteredChildren.filter((child) => !child.archivedAt);
|
|
213
|
+
}
|
|
214
|
+
// Apply limit (must be positive)
|
|
215
|
+
if (options?.limit && options.limit > 0) {
|
|
216
|
+
filteredChildren = filteredChildren.slice(0, options.limit);
|
|
217
|
+
}
|
|
218
|
+
// Create IssueWithChildren by extending the parent issue
|
|
219
|
+
const issueWithChildren = Object.assign(Object.create(Object.getPrototypeOf(parentIssue)), parentIssue, {
|
|
220
|
+
children: filteredChildren,
|
|
221
|
+
childCount: filteredChildren.length,
|
|
222
|
+
});
|
|
223
|
+
return issueWithChildren;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Update an issue's properties.
|
|
227
|
+
*/
|
|
228
|
+
async updateIssue(issueId, updates) {
|
|
229
|
+
const issueData = this.state.issues.get(issueId);
|
|
230
|
+
if (!issueData) {
|
|
231
|
+
throw new Error(`Issue ${issueId} not found`);
|
|
232
|
+
}
|
|
233
|
+
// Update the issue data directly
|
|
234
|
+
if (updates.stateId !== undefined) {
|
|
235
|
+
const state = this.state.workflowStates.get(updates.stateId);
|
|
236
|
+
if (!state) {
|
|
237
|
+
throw new Error(`Workflow state ${updates.stateId} not found`);
|
|
238
|
+
}
|
|
239
|
+
// Validate state belongs to issue's team
|
|
240
|
+
if (state.teamId !== issueData.teamId) {
|
|
241
|
+
throw new Error(`Workflow state ${updates.stateId} does not belong to team ${issueData.teamId}`);
|
|
242
|
+
}
|
|
243
|
+
issueData.stateId = updates.stateId;
|
|
244
|
+
}
|
|
245
|
+
if (updates.assigneeId !== undefined) {
|
|
246
|
+
if (updates.assigneeId !== null && updates.assigneeId !== "") {
|
|
247
|
+
const assignee = this.state.users.get(updates.assigneeId);
|
|
248
|
+
if (!assignee) {
|
|
249
|
+
throw new Error(`User ${updates.assigneeId} not found`);
|
|
250
|
+
}
|
|
251
|
+
issueData.assigneeId = updates.assigneeId;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Clear assignee
|
|
255
|
+
issueData.assigneeId = undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (updates.title !== undefined) {
|
|
259
|
+
issueData.title = updates.title;
|
|
260
|
+
}
|
|
261
|
+
if (updates.description !== undefined) {
|
|
262
|
+
issueData.description = updates.description;
|
|
263
|
+
}
|
|
264
|
+
if (updates.priority !== undefined) {
|
|
265
|
+
issueData.priority = updates.priority;
|
|
266
|
+
}
|
|
267
|
+
if (updates.parentId !== undefined) {
|
|
268
|
+
if (updates.parentId !== null && updates.parentId !== "") {
|
|
269
|
+
await this.fetchIssue(updates.parentId); // Validate parent exists
|
|
270
|
+
// Check for circular reference
|
|
271
|
+
if (updates.parentId === issueId) {
|
|
272
|
+
throw new Error("Issue cannot be its own parent");
|
|
273
|
+
}
|
|
274
|
+
issueData.parentId = updates.parentId;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Clear parent
|
|
278
|
+
issueData.parentId = undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (updates.labelIds !== undefined) {
|
|
282
|
+
// Validate all labels exist (if any provided)
|
|
283
|
+
if (updates.labelIds.length > 0) {
|
|
284
|
+
for (const labelId of updates.labelIds) {
|
|
285
|
+
const label = this.state.labels.get(labelId);
|
|
286
|
+
if (!label) {
|
|
287
|
+
throw new Error(`Label ${labelId} not found`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
issueData.labelIds = updates.labelIds;
|
|
292
|
+
}
|
|
293
|
+
// Update timestamp
|
|
294
|
+
issueData.updatedAt = new Date();
|
|
295
|
+
// Emit state change event
|
|
296
|
+
const resolvedLabels = issueData.labelIds
|
|
297
|
+
.map((id) => this.state.labels.get(id))
|
|
298
|
+
.filter((l) => l !== undefined);
|
|
299
|
+
const issue = createCLIIssue(issueData, resolvedLabels);
|
|
300
|
+
this.emit("issue:updated", { issue });
|
|
301
|
+
return issue;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Fetch attachments for an issue.
|
|
305
|
+
*/
|
|
306
|
+
async fetchIssueAttachments(issueId) {
|
|
307
|
+
const issue = await this.fetchIssue(issueId);
|
|
308
|
+
// Get attachments from the issue
|
|
309
|
+
const attachmentsConnection = await issue.attachments();
|
|
310
|
+
return attachmentsConnection.nodes.map(() => ({
|
|
311
|
+
title: "Untitled attachment",
|
|
312
|
+
url: "",
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
// ========================================================================
|
|
316
|
+
// COMMENT OPERATIONS
|
|
317
|
+
// ========================================================================
|
|
318
|
+
/**
|
|
319
|
+
* Fetch comments for an issue with optional pagination.
|
|
320
|
+
*/
|
|
321
|
+
async fetchComments(issueId, options) {
|
|
322
|
+
const issue = await this.fetchIssue(issueId);
|
|
323
|
+
// Find all comments for this issue
|
|
324
|
+
const allComments = [];
|
|
325
|
+
for (const [, commentData] of this.state.comments) {
|
|
326
|
+
if (commentData.issueId === issue.id) {
|
|
327
|
+
allComments.push(createCLIComment(commentData));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Sort by creation date
|
|
331
|
+
allComments.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
332
|
+
// Apply pagination
|
|
333
|
+
const first = options?.first ?? 50;
|
|
334
|
+
const paginatedComments = allComments.slice(0, first);
|
|
335
|
+
return {
|
|
336
|
+
nodes: paginatedComments,
|
|
337
|
+
pageInfo: {
|
|
338
|
+
hasNextPage: allComments.length > first,
|
|
339
|
+
hasPreviousPage: false,
|
|
340
|
+
startCursor: paginatedComments[0]?.id,
|
|
341
|
+
endCursor: paginatedComments[paginatedComments.length - 1]?.id,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Fetch a single comment by ID.
|
|
347
|
+
*/
|
|
348
|
+
async fetchComment(commentId) {
|
|
349
|
+
const commentData = this.state.comments.get(commentId);
|
|
350
|
+
if (!commentData) {
|
|
351
|
+
throw new Error(`Comment ${commentId} not found`);
|
|
352
|
+
}
|
|
353
|
+
return createCLIComment(commentData);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Fetch a comment with attachments.
|
|
357
|
+
*/
|
|
358
|
+
async fetchCommentWithAttachments(commentId) {
|
|
359
|
+
const comment = await this.fetchComment(commentId);
|
|
360
|
+
// Create comment with attachments
|
|
361
|
+
const commentWithAttachments = Object.assign(Object.create(Object.getPrototypeOf(comment)), comment, {
|
|
362
|
+
attachments: [],
|
|
363
|
+
});
|
|
364
|
+
return commentWithAttachments;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Create a comment on an issue.
|
|
368
|
+
*/
|
|
369
|
+
async createComment(issueId, input) {
|
|
370
|
+
const issue = await this.fetchIssue(issueId);
|
|
371
|
+
const currentUser = await this.fetchCurrentUser();
|
|
372
|
+
// Build the comment body with attachments if provided
|
|
373
|
+
let finalBody = input.body;
|
|
374
|
+
if (input.attachmentUrls && input.attachmentUrls.length > 0) {
|
|
375
|
+
const attachmentMarkdown = input.attachmentUrls
|
|
376
|
+
.map((url) => {
|
|
377
|
+
const isImage = /\.(png|jpg|jpeg|gif|svg|webp|bmp)(\?|#|$)/i.test(url);
|
|
378
|
+
if (isImage) {
|
|
379
|
+
return ``;
|
|
380
|
+
}
|
|
381
|
+
return `[attachment](${url})`;
|
|
382
|
+
})
|
|
383
|
+
.join("\n");
|
|
384
|
+
finalBody = input.body
|
|
385
|
+
? `${input.body}\n\n${attachmentMarkdown}`
|
|
386
|
+
: attachmentMarkdown;
|
|
387
|
+
}
|
|
388
|
+
// Generate comment ID
|
|
389
|
+
const commentId = `comment-${this.state.commentCounter++}`;
|
|
390
|
+
// Create the comment data
|
|
391
|
+
const commentData = {
|
|
392
|
+
id: commentId,
|
|
393
|
+
body: finalBody,
|
|
394
|
+
url: `https://linear.app/test/issue/${issue.identifier}#comment-${commentId}`,
|
|
395
|
+
createdAt: new Date(),
|
|
396
|
+
updatedAt: new Date(),
|
|
397
|
+
userId: currentUser.id,
|
|
398
|
+
issueId: issue.id,
|
|
399
|
+
parentId: input.parentId,
|
|
400
|
+
};
|
|
401
|
+
// Save to state
|
|
402
|
+
this.state.comments.set(commentId, commentData);
|
|
403
|
+
// Create and return the comment
|
|
404
|
+
const comment = createCLIComment(commentData);
|
|
405
|
+
// Emit state change event
|
|
406
|
+
this.emit("comment:created", { comment });
|
|
407
|
+
return comment;
|
|
408
|
+
}
|
|
409
|
+
// ========================================================================
|
|
410
|
+
// TEAM OPERATIONS
|
|
411
|
+
// ========================================================================
|
|
412
|
+
/**
|
|
413
|
+
* Fetch all teams in the workspace/organization.
|
|
414
|
+
*/
|
|
415
|
+
async fetchTeams(options) {
|
|
416
|
+
const allTeams = Array.from(this.state.teams.values()).map((data) => createCLITeam(data));
|
|
417
|
+
// Apply pagination
|
|
418
|
+
const first = options?.first ?? 50;
|
|
419
|
+
const paginatedTeams = allTeams.slice(0, first);
|
|
420
|
+
return {
|
|
421
|
+
nodes: paginatedTeams,
|
|
422
|
+
pageInfo: {
|
|
423
|
+
hasNextPage: allTeams.length > first,
|
|
424
|
+
hasPreviousPage: false,
|
|
425
|
+
startCursor: paginatedTeams[0]?.id,
|
|
426
|
+
endCursor: paginatedTeams[paginatedTeams.length - 1]?.id,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Fetch a single team by ID or key.
|
|
432
|
+
*/
|
|
433
|
+
async fetchTeam(idOrKey) {
|
|
434
|
+
// Try to find by ID first
|
|
435
|
+
let teamData = this.state.teams.get(idOrKey);
|
|
436
|
+
// If not found, try to find by key
|
|
437
|
+
if (!teamData) {
|
|
438
|
+
for (const [, candidateTeam] of this.state.teams) {
|
|
439
|
+
if (candidateTeam.key === idOrKey) {
|
|
440
|
+
teamData = candidateTeam;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (!teamData) {
|
|
446
|
+
throw new Error(`Team ${idOrKey} not found`);
|
|
447
|
+
}
|
|
448
|
+
return createCLITeam(teamData);
|
|
449
|
+
}
|
|
450
|
+
// ========================================================================
|
|
451
|
+
// LABEL OPERATIONS
|
|
452
|
+
// ========================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Fetch all issue labels in the workspace/organization.
|
|
455
|
+
*/
|
|
456
|
+
async fetchLabels(options) {
|
|
457
|
+
const allLabels = Array.from(this.state.labels.values()).map((data) => createCLILabel(data));
|
|
458
|
+
// Apply pagination
|
|
459
|
+
const first = options?.first ?? 50;
|
|
460
|
+
const paginatedLabels = allLabels.slice(0, first);
|
|
461
|
+
return {
|
|
462
|
+
nodes: paginatedLabels,
|
|
463
|
+
pageInfo: {
|
|
464
|
+
hasNextPage: allLabels.length > first,
|
|
465
|
+
hasPreviousPage: false,
|
|
466
|
+
startCursor: paginatedLabels[0]?.id,
|
|
467
|
+
endCursor: paginatedLabels[paginatedLabels.length - 1]?.id,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Fetch a single label by ID or name.
|
|
473
|
+
*/
|
|
474
|
+
async fetchLabel(idOrName) {
|
|
475
|
+
// Try to find by ID first
|
|
476
|
+
let labelData = this.state.labels.get(idOrName);
|
|
477
|
+
// If not found, try to find by name
|
|
478
|
+
if (!labelData) {
|
|
479
|
+
for (const [, candidateLabel] of this.state.labels) {
|
|
480
|
+
if (candidateLabel.name === idOrName) {
|
|
481
|
+
labelData = candidateLabel;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!labelData) {
|
|
487
|
+
throw new Error(`Label ${idOrName} not found`);
|
|
488
|
+
}
|
|
489
|
+
return createCLILabel(labelData);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Fetch label names for a specific issue.
|
|
493
|
+
*/
|
|
494
|
+
async getIssueLabels(issueId) {
|
|
495
|
+
const issue = await this.fetchIssue(issueId);
|
|
496
|
+
// Get label names from the issue's labelIds
|
|
497
|
+
const labelNames = [];
|
|
498
|
+
for (const labelId of issue.labelIds) {
|
|
499
|
+
const labelData = this.state.labels.get(labelId);
|
|
500
|
+
if (labelData) {
|
|
501
|
+
labelNames.push(labelData.name);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return labelNames;
|
|
505
|
+
}
|
|
506
|
+
// ========================================================================
|
|
507
|
+
// WORKFLOW STATE OPERATIONS
|
|
508
|
+
// ========================================================================
|
|
509
|
+
/**
|
|
510
|
+
* Fetch workflow states for a team.
|
|
511
|
+
*/
|
|
512
|
+
async fetchWorkflowStates(teamId, options) {
|
|
513
|
+
const team = await this.fetchTeam(teamId);
|
|
514
|
+
// Find all workflow states for this team
|
|
515
|
+
const allStates = [];
|
|
516
|
+
for (const [, stateData] of this.state.workflowStates) {
|
|
517
|
+
if (stateData.teamId === team.id) {
|
|
518
|
+
allStates.push(createCLIWorkflowState(stateData));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Apply pagination
|
|
522
|
+
const first = options?.first ?? 50;
|
|
523
|
+
const paginatedStates = allStates.slice(0, first);
|
|
524
|
+
return {
|
|
525
|
+
nodes: paginatedStates,
|
|
526
|
+
pageInfo: {
|
|
527
|
+
hasNextPage: allStates.length > first,
|
|
528
|
+
hasPreviousPage: false,
|
|
529
|
+
startCursor: paginatedStates[0]?.id,
|
|
530
|
+
endCursor: paginatedStates[paginatedStates.length - 1]?.id,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Fetch a single workflow state by ID.
|
|
536
|
+
*/
|
|
537
|
+
async fetchWorkflowState(stateId) {
|
|
538
|
+
const stateData = this.state.workflowStates.get(stateId);
|
|
539
|
+
if (!stateData) {
|
|
540
|
+
throw new Error(`Workflow state ${stateId} not found`);
|
|
541
|
+
}
|
|
542
|
+
return createCLIWorkflowState(stateData);
|
|
543
|
+
}
|
|
544
|
+
// ========================================================================
|
|
545
|
+
// USER OPERATIONS
|
|
546
|
+
// ========================================================================
|
|
547
|
+
/**
|
|
548
|
+
* Fetch a user by ID.
|
|
549
|
+
*/
|
|
550
|
+
async fetchUser(userId) {
|
|
551
|
+
const userData = this.state.users.get(userId);
|
|
552
|
+
if (!userData) {
|
|
553
|
+
throw new Error(`User ${userId} not found`);
|
|
554
|
+
}
|
|
555
|
+
return createCLIUser(userData);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Fetch the current authenticated user.
|
|
559
|
+
*/
|
|
560
|
+
async fetchCurrentUser() {
|
|
561
|
+
return await this.fetchUser(this.state.currentUserId);
|
|
562
|
+
}
|
|
563
|
+
// ========================================================================
|
|
564
|
+
// AGENT SESSION OPERATIONS
|
|
565
|
+
// ========================================================================
|
|
566
|
+
/**
|
|
567
|
+
* Create an agent session on an issue.
|
|
568
|
+
*/
|
|
569
|
+
createAgentSessionOnIssue(input) {
|
|
570
|
+
return this.createAgentSessionInternal(input.issueId, undefined, input);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Create an agent session on a comment thread.
|
|
574
|
+
*/
|
|
575
|
+
createAgentSessionOnComment(input) {
|
|
576
|
+
return this.createAgentSessionInternal(undefined, input.commentId, input);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Internal helper to create agent sessions.
|
|
580
|
+
*/
|
|
581
|
+
async createAgentSessionInternal(issueId, commentId, input) {
|
|
582
|
+
// Validate input and fetch issue/comment
|
|
583
|
+
let issue;
|
|
584
|
+
let comment;
|
|
585
|
+
if (issueId) {
|
|
586
|
+
issue = await this.fetchIssue(issueId);
|
|
587
|
+
}
|
|
588
|
+
if (commentId) {
|
|
589
|
+
comment = await this.fetchComment(commentId);
|
|
590
|
+
// If comment provided but no issue, get issue from comment
|
|
591
|
+
if (!issue && comment) {
|
|
592
|
+
const commentData = this.state.comments.get(commentId);
|
|
593
|
+
if (commentData?.issueId) {
|
|
594
|
+
issue = await this.fetchIssue(commentData.issueId);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Generate session ID
|
|
599
|
+
const sessionId = `session-${this.state.sessionCounter++}`;
|
|
600
|
+
const lastSyncId = Date.now();
|
|
601
|
+
// Create agent session data
|
|
602
|
+
const sessionData = {
|
|
603
|
+
id: sessionId,
|
|
604
|
+
externalLink: input.externalLink,
|
|
605
|
+
status: AgentSessionStatus.Active,
|
|
606
|
+
type: AgentSessionType.CommentThread,
|
|
607
|
+
createdAt: new Date(),
|
|
608
|
+
updatedAt: new Date(),
|
|
609
|
+
issueId: issue?.id,
|
|
610
|
+
commentId,
|
|
611
|
+
};
|
|
612
|
+
// Save to state
|
|
613
|
+
this.state.agentSessions.set(sessionId, sessionData);
|
|
614
|
+
// Create the session object
|
|
615
|
+
const agentSession = createCLIAgentSession(sessionData);
|
|
616
|
+
// Emit state change event
|
|
617
|
+
this.emit("agentSession:created", { agentSession });
|
|
618
|
+
// Emit AgentSessionCreated webhook event if transport is available
|
|
619
|
+
if (this.eventTransport && issue) {
|
|
620
|
+
// Get team and state info for the issue
|
|
621
|
+
const issueData = this.state.issues.get(issue.id);
|
|
622
|
+
const team = issueData?.teamId
|
|
623
|
+
? await this.fetchTeam(issueData.teamId)
|
|
624
|
+
: undefined;
|
|
625
|
+
// Construct a webhook-like event that matches Linear's structure
|
|
626
|
+
const now = new Date();
|
|
627
|
+
const nowIso = now.toISOString();
|
|
628
|
+
const webhookEvent = {
|
|
629
|
+
type: "AgentSessionEvent",
|
|
630
|
+
action: "created",
|
|
631
|
+
organizationId: "cli-workspace",
|
|
632
|
+
oauthClientId: "cli-oauth-client",
|
|
633
|
+
appUserId: "cli-app-user",
|
|
634
|
+
createdAt: now,
|
|
635
|
+
agentSession: {
|
|
636
|
+
id: sessionId,
|
|
637
|
+
appUserId: "cli-app-user",
|
|
638
|
+
organizationId: "cli-workspace",
|
|
639
|
+
createdAt: nowIso,
|
|
640
|
+
updatedAt: nowIso,
|
|
641
|
+
status: "active",
|
|
642
|
+
type: "issue",
|
|
643
|
+
issue: {
|
|
644
|
+
id: issue.id,
|
|
645
|
+
identifier: issue.identifier,
|
|
646
|
+
title: issue.title,
|
|
647
|
+
url: `cli://issues/${issue.identifier}`,
|
|
648
|
+
teamId: team?.id ?? "default-team",
|
|
649
|
+
team: team
|
|
650
|
+
? {
|
|
651
|
+
id: team.id,
|
|
652
|
+
key: team.key,
|
|
653
|
+
name: team.name,
|
|
654
|
+
}
|
|
655
|
+
: {
|
|
656
|
+
id: "default-team",
|
|
657
|
+
key: "DEF",
|
|
658
|
+
name: "Default Team",
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
comment: comment
|
|
662
|
+
? {
|
|
663
|
+
id: comment.id,
|
|
664
|
+
body: comment.body,
|
|
665
|
+
}
|
|
666
|
+
: undefined,
|
|
667
|
+
},
|
|
668
|
+
guidance: [], // Empty array for CLI mode
|
|
669
|
+
};
|
|
670
|
+
// Emit the event through the transport
|
|
671
|
+
this.eventTransport.emitEvent(webhookEvent);
|
|
672
|
+
}
|
|
673
|
+
// Return payload with session wrapped in Promise
|
|
674
|
+
const payload = {
|
|
675
|
+
success: true,
|
|
676
|
+
lastSyncId,
|
|
677
|
+
agentSession: Promise.resolve(agentSession),
|
|
678
|
+
};
|
|
679
|
+
return payload;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Fetch an agent session by ID.
|
|
683
|
+
*/
|
|
684
|
+
fetchAgentSession(sessionId) {
|
|
685
|
+
return (async () => {
|
|
686
|
+
const sessionData = this.state.agentSessions.get(sessionId);
|
|
687
|
+
if (!sessionData) {
|
|
688
|
+
throw new Error(`Agent session ${sessionId} not found`);
|
|
689
|
+
}
|
|
690
|
+
return createCLIAgentSession(sessionData);
|
|
691
|
+
})();
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* List agent sessions with optional filtering.
|
|
695
|
+
*
|
|
696
|
+
* @param options - Filtering options (issueId, limit, offset)
|
|
697
|
+
* @returns Array of agent session data
|
|
698
|
+
*/
|
|
699
|
+
listAgentSessions(options) {
|
|
700
|
+
const { issueId, limit = 50, offset = 0 } = options ?? {};
|
|
701
|
+
let sessions = Array.from(this.state.agentSessions.values());
|
|
702
|
+
// Filter by issueId if provided
|
|
703
|
+
if (issueId) {
|
|
704
|
+
sessions = sessions.filter((s) => s.issueId === issueId);
|
|
705
|
+
}
|
|
706
|
+
// Sort by creation date descending (most recent first)
|
|
707
|
+
sessions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
708
|
+
// Apply pagination
|
|
709
|
+
return sessions.slice(offset, offset + limit);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Update an agent session's status.
|
|
713
|
+
*
|
|
714
|
+
* @param sessionId - The session ID to update
|
|
715
|
+
* @param status - The new status
|
|
716
|
+
* @returns The updated session
|
|
717
|
+
*/
|
|
718
|
+
async updateAgentSessionStatus(sessionId, status) {
|
|
719
|
+
const sessionData = this.state.agentSessions.get(sessionId);
|
|
720
|
+
if (!sessionData) {
|
|
721
|
+
throw new Error(`Agent session ${sessionId} not found`);
|
|
722
|
+
}
|
|
723
|
+
// Update the status
|
|
724
|
+
sessionData.status = status;
|
|
725
|
+
sessionData.updatedAt = new Date();
|
|
726
|
+
// Set endedAt if session is being stopped
|
|
727
|
+
const isStopping = status === AgentSessionStatus.Complete ||
|
|
728
|
+
status === AgentSessionStatus.Error;
|
|
729
|
+
if (isStopping) {
|
|
730
|
+
sessionData.endedAt = new Date();
|
|
731
|
+
}
|
|
732
|
+
// Emit state change event
|
|
733
|
+
const agentSession = createCLIAgentSession(sessionData);
|
|
734
|
+
this.emit("agentSession:updated", { agentSession });
|
|
735
|
+
return agentSession;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Emit a stop signal webhook event for the EdgeWorker to handle.
|
|
739
|
+
* Should be called by the caller after stopping a session (e.g., CLIRPCServer.handleStopSession).
|
|
740
|
+
*/
|
|
741
|
+
async emitStopSignalEvent(sessionId) {
|
|
742
|
+
const sessionData = this.state.agentSessions.get(sessionId);
|
|
743
|
+
if (!this.eventTransport || !sessionData?.issueId) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const issue = await this.fetchIssue(sessionData.issueId);
|
|
747
|
+
if (!issue) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const issueData = this.state.issues.get(issue.id);
|
|
751
|
+
const team = issueData?.teamId
|
|
752
|
+
? await this.fetchTeam(issueData.teamId)
|
|
753
|
+
: undefined;
|
|
754
|
+
const now = new Date();
|
|
755
|
+
const nowIso = now.toISOString();
|
|
756
|
+
const webhookEvent = {
|
|
757
|
+
type: "AgentSessionEvent",
|
|
758
|
+
action: "prompted",
|
|
759
|
+
organizationId: "cli-workspace",
|
|
760
|
+
oauthClientId: "cli-oauth-client",
|
|
761
|
+
appUserId: "cli-app-user",
|
|
762
|
+
createdAt: now,
|
|
763
|
+
agentSession: {
|
|
764
|
+
id: sessionId,
|
|
765
|
+
appUserId: "cli-app-user",
|
|
766
|
+
organizationId: "cli-workspace",
|
|
767
|
+
createdAt: sessionData.createdAt.toISOString(),
|
|
768
|
+
updatedAt: nowIso,
|
|
769
|
+
status: sessionData.status,
|
|
770
|
+
type: "issue",
|
|
771
|
+
issue: {
|
|
772
|
+
id: issue.id,
|
|
773
|
+
identifier: issue.identifier,
|
|
774
|
+
title: issue.title,
|
|
775
|
+
url: `cli://issues/${issue.identifier}`,
|
|
776
|
+
teamId: team?.id ?? "default-team",
|
|
777
|
+
team: team
|
|
778
|
+
? {
|
|
779
|
+
id: team.id,
|
|
780
|
+
key: team.key,
|
|
781
|
+
name: team.name,
|
|
782
|
+
}
|
|
783
|
+
: {
|
|
784
|
+
id: "default-team",
|
|
785
|
+
key: "DEF",
|
|
786
|
+
name: "Default Team",
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
agentActivity: {
|
|
791
|
+
id: `activity-stop-${Date.now()}`,
|
|
792
|
+
agentSessionId: sessionId,
|
|
793
|
+
content: { type: "prompt", body: "Stop session" },
|
|
794
|
+
createdAt: nowIso,
|
|
795
|
+
updatedAt: nowIso,
|
|
796
|
+
signal: "stop",
|
|
797
|
+
},
|
|
798
|
+
guidance: [],
|
|
799
|
+
};
|
|
800
|
+
this.eventTransport.emitEvent(webhookEvent);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Prompt an agent session with a user message.
|
|
804
|
+
* This creates a comment on the associated issue and emits a prompted event.
|
|
805
|
+
*
|
|
806
|
+
* @param sessionId - The session ID to prompt
|
|
807
|
+
* @param message - The user's prompt message
|
|
808
|
+
* @returns The created comment
|
|
809
|
+
*/
|
|
810
|
+
async promptAgentSession(sessionId, message) {
|
|
811
|
+
const sessionData = this.state.agentSessions.get(sessionId);
|
|
812
|
+
if (!sessionData) {
|
|
813
|
+
throw new Error(`Agent session ${sessionId} not found`);
|
|
814
|
+
}
|
|
815
|
+
if (!sessionData.issueId) {
|
|
816
|
+
throw new Error(`Agent session ${sessionId} is not associated with an issue`);
|
|
817
|
+
}
|
|
818
|
+
// Check if the session is stopped/completed
|
|
819
|
+
if (sessionData.status === AgentSessionStatus.Complete) {
|
|
820
|
+
throw new Error(`Cannot prompt completed session ${sessionId}`);
|
|
821
|
+
}
|
|
822
|
+
// Create a comment on the issue
|
|
823
|
+
const comment = await this.createComment(sessionData.issueId, {
|
|
824
|
+
body: message,
|
|
825
|
+
});
|
|
826
|
+
// Update session status to awaiting processing
|
|
827
|
+
sessionData.updatedAt = new Date();
|
|
828
|
+
// Create an activity record for the prompt
|
|
829
|
+
await this.createAgentActivity({
|
|
830
|
+
agentSessionId: sessionId,
|
|
831
|
+
content: {
|
|
832
|
+
type: AgentActivityType.Prompt,
|
|
833
|
+
body: message,
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
// Emit prompted event
|
|
837
|
+
this.emit("agentSession:prompted", {
|
|
838
|
+
sessionId,
|
|
839
|
+
message,
|
|
840
|
+
comment,
|
|
841
|
+
issueId: sessionData.issueId,
|
|
842
|
+
});
|
|
843
|
+
// Emit AgentSessionEvent webhook for prompted action if transport is available
|
|
844
|
+
if (this.eventTransport) {
|
|
845
|
+
const issue = await this.fetchIssue(sessionData.issueId);
|
|
846
|
+
if (issue) {
|
|
847
|
+
const issueData = this.state.issues.get(issue.id);
|
|
848
|
+
const team = issueData?.teamId
|
|
849
|
+
? await this.fetchTeam(issueData.teamId)
|
|
850
|
+
: undefined;
|
|
851
|
+
const now = new Date();
|
|
852
|
+
const nowIso = now.toISOString();
|
|
853
|
+
const webhookEvent = {
|
|
854
|
+
type: "AgentSessionEvent",
|
|
855
|
+
action: "prompted",
|
|
856
|
+
organizationId: "cli-workspace",
|
|
857
|
+
oauthClientId: "cli-oauth-client",
|
|
858
|
+
appUserId: "cli-app-user",
|
|
859
|
+
createdAt: now,
|
|
860
|
+
agentSession: {
|
|
861
|
+
id: sessionId,
|
|
862
|
+
appUserId: "cli-app-user",
|
|
863
|
+
organizationId: "cli-workspace",
|
|
864
|
+
createdAt: sessionData.createdAt.toISOString(),
|
|
865
|
+
updatedAt: nowIso,
|
|
866
|
+
status: sessionData.status,
|
|
867
|
+
type: "issue",
|
|
868
|
+
issue: {
|
|
869
|
+
id: issue.id,
|
|
870
|
+
identifier: issue.identifier,
|
|
871
|
+
title: issue.title,
|
|
872
|
+
url: `cli://issues/${issue.identifier}`,
|
|
873
|
+
teamId: team?.id ?? "default-team",
|
|
874
|
+
team: team
|
|
875
|
+
? {
|
|
876
|
+
id: team.id,
|
|
877
|
+
key: team.key,
|
|
878
|
+
name: team.name,
|
|
879
|
+
}
|
|
880
|
+
: {
|
|
881
|
+
id: "default-team",
|
|
882
|
+
key: "DEF",
|
|
883
|
+
name: "Default Team",
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
agentActivity: {
|
|
888
|
+
id: `activity-prompt-${Date.now()}`,
|
|
889
|
+
agentSessionId: sessionId,
|
|
890
|
+
content: { type: "prompt", body: message },
|
|
891
|
+
createdAt: nowIso,
|
|
892
|
+
updatedAt: nowIso,
|
|
893
|
+
sourceCommentId: comment.id,
|
|
894
|
+
},
|
|
895
|
+
guidance: [],
|
|
896
|
+
};
|
|
897
|
+
this.eventTransport.emitEvent(webhookEvent);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return comment;
|
|
901
|
+
}
|
|
902
|
+
// ========================================================================
|
|
903
|
+
// AGENT ACTIVITY OPERATIONS
|
|
904
|
+
// ========================================================================
|
|
905
|
+
/**
|
|
906
|
+
* Post an agent activity to an agent session.
|
|
907
|
+
*/
|
|
908
|
+
async createAgentActivity(input) {
|
|
909
|
+
// Validate session exists
|
|
910
|
+
await this.fetchAgentSession(input.agentSessionId);
|
|
911
|
+
// Generate activity ID
|
|
912
|
+
const activityId = `activity-${this.state.activityCounter++}`;
|
|
913
|
+
// Store the activity
|
|
914
|
+
const activityData = {
|
|
915
|
+
id: activityId,
|
|
916
|
+
agentSessionId: input.agentSessionId,
|
|
917
|
+
type: input.content.type,
|
|
918
|
+
content: "body" in input.content
|
|
919
|
+
? typeof input.content.body === "string"
|
|
920
|
+
? input.content.body
|
|
921
|
+
: JSON.stringify(input.content.body)
|
|
922
|
+
: JSON.stringify(input.content),
|
|
923
|
+
createdAt: new Date(),
|
|
924
|
+
ephemeral: input.ephemeral ?? undefined,
|
|
925
|
+
signal: input.signal ?? undefined,
|
|
926
|
+
};
|
|
927
|
+
this.state.agentActivities.set(activityId, activityData);
|
|
928
|
+
// Emit state change event
|
|
929
|
+
this.emit("agentActivity:created", { input, activityId });
|
|
930
|
+
// Return success payload with agentActivity that can be awaited
|
|
931
|
+
// AgentSessionManager expects result.agentActivity to be promise-like
|
|
932
|
+
return {
|
|
933
|
+
agentActivity: Promise.resolve({ id: activityId }),
|
|
934
|
+
success: true,
|
|
935
|
+
lastSyncId: Date.now(),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* List agent activities for a session.
|
|
940
|
+
*
|
|
941
|
+
* @param sessionId - The session ID to get activities for
|
|
942
|
+
* @param options - Pagination options
|
|
943
|
+
* @returns Array of agent activity data
|
|
944
|
+
*/
|
|
945
|
+
listAgentActivities(sessionId, options) {
|
|
946
|
+
const { limit = 50, offset = 0 } = options ?? {};
|
|
947
|
+
// Get all activities for this session
|
|
948
|
+
const activities = Array.from(this.state.agentActivities.values()).filter((a) => a.agentSessionId === sessionId);
|
|
949
|
+
// Sort by creation date ascending
|
|
950
|
+
activities.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
951
|
+
// Apply pagination
|
|
952
|
+
return activities.slice(offset, offset + limit);
|
|
953
|
+
}
|
|
954
|
+
// ========================================================================
|
|
955
|
+
// FILE OPERATIONS
|
|
956
|
+
// ========================================================================
|
|
957
|
+
/**
|
|
958
|
+
* Request a file upload URL from the platform.
|
|
959
|
+
*/
|
|
960
|
+
async requestFileUpload(request) {
|
|
961
|
+
// Generate mock upload URLs
|
|
962
|
+
const uploadUrl = `https://mock-upload.linear.app/${Date.now()}/${request.filename}`;
|
|
963
|
+
const assetUrl = `https://mock-assets.linear.app/${Date.now()}/${request.filename}`;
|
|
964
|
+
return {
|
|
965
|
+
uploadUrl,
|
|
966
|
+
headers: {
|
|
967
|
+
"Content-Type": request.contentType,
|
|
968
|
+
"x-amz-acl": request.makePublic ? "public-read" : "private",
|
|
969
|
+
},
|
|
970
|
+
assetUrl,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
// ========================================================================
|
|
974
|
+
// PLATFORM METADATA
|
|
975
|
+
// ========================================================================
|
|
976
|
+
/**
|
|
977
|
+
* Get the platform type identifier.
|
|
978
|
+
*/
|
|
979
|
+
getPlatformType() {
|
|
980
|
+
return "cli";
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Get the platform's API version or other metadata.
|
|
984
|
+
*/
|
|
985
|
+
getPlatformMetadata() {
|
|
986
|
+
return {
|
|
987
|
+
platform: "cli",
|
|
988
|
+
implementation: "in-memory",
|
|
989
|
+
version: "1.0.0",
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
// ========================================================================
|
|
993
|
+
// EVENT TRANSPORT
|
|
994
|
+
// ========================================================================
|
|
995
|
+
/**
|
|
996
|
+
* Create an event transport for receiving webhook events.
|
|
997
|
+
*
|
|
998
|
+
* @param config - Transport configuration
|
|
999
|
+
* @returns CLI event transport implementation
|
|
1000
|
+
*/
|
|
1001
|
+
createEventTransport(config) {
|
|
1002
|
+
// Type narrow to CLI config
|
|
1003
|
+
if (config.platform !== "cli") {
|
|
1004
|
+
throw new Error(`Invalid platform "${config.platform}" for CLIIssueTrackerService. Expected "cli".`);
|
|
1005
|
+
}
|
|
1006
|
+
// Store the event transport so we can emit events
|
|
1007
|
+
this.eventTransport = new CLIEventTransport(config);
|
|
1008
|
+
return this.eventTransport;
|
|
1009
|
+
}
|
|
1010
|
+
// ========================================================================
|
|
1011
|
+
// TESTING/DEBUGGING UTILITIES
|
|
1012
|
+
// ========================================================================
|
|
1013
|
+
/**
|
|
1014
|
+
* Seed default teams and workflow states for testing.
|
|
1015
|
+
* Creates a "default" team with standard workflow states.
|
|
1016
|
+
*/
|
|
1017
|
+
seedDefaultData() {
|
|
1018
|
+
// Create default user
|
|
1019
|
+
const defaultUser = {
|
|
1020
|
+
id: "user-default",
|
|
1021
|
+
name: "Test User",
|
|
1022
|
+
displayName: "Test User",
|
|
1023
|
+
email: "test@example.com",
|
|
1024
|
+
url: "https://linear.app/test/user/test-user",
|
|
1025
|
+
active: true,
|
|
1026
|
+
admin: false,
|
|
1027
|
+
app: false,
|
|
1028
|
+
guest: false,
|
|
1029
|
+
isMe: true,
|
|
1030
|
+
isAssignable: true,
|
|
1031
|
+
isMentionable: true,
|
|
1032
|
+
avatarBackgroundColor: "#3b82f6",
|
|
1033
|
+
initials: "TU",
|
|
1034
|
+
createdIssueCount: 0,
|
|
1035
|
+
createdAt: new Date(),
|
|
1036
|
+
updatedAt: new Date(),
|
|
1037
|
+
};
|
|
1038
|
+
this.state.users.set(defaultUser.id, defaultUser);
|
|
1039
|
+
// Create default team
|
|
1040
|
+
const defaultTeam = {
|
|
1041
|
+
id: "team-default",
|
|
1042
|
+
key: "DEF",
|
|
1043
|
+
name: "Default Team",
|
|
1044
|
+
displayName: "Default Team",
|
|
1045
|
+
description: "Default team for F1 CLI testing",
|
|
1046
|
+
private: false,
|
|
1047
|
+
issueCount: 0,
|
|
1048
|
+
inviteHash: "default-invite",
|
|
1049
|
+
cyclesEnabled: false,
|
|
1050
|
+
cycleDuration: 1,
|
|
1051
|
+
cycleCooldownTime: 0,
|
|
1052
|
+
cycleStartDay: 0,
|
|
1053
|
+
cycleLockToActive: false,
|
|
1054
|
+
cycleIssueAutoAssignStarted: false,
|
|
1055
|
+
cycleIssueAutoAssignCompleted: false,
|
|
1056
|
+
defaultIssueEstimate: 0,
|
|
1057
|
+
issueEstimationType: "notUsed",
|
|
1058
|
+
issueEstimationAllowZero: true,
|
|
1059
|
+
issueEstimationExtended: false,
|
|
1060
|
+
autoArchivePeriod: 0,
|
|
1061
|
+
createdAt: new Date(),
|
|
1062
|
+
updatedAt: new Date(),
|
|
1063
|
+
};
|
|
1064
|
+
this.state.teams.set(defaultTeam.id, defaultTeam);
|
|
1065
|
+
// Create workflow states for the default team
|
|
1066
|
+
const workflowStates = [
|
|
1067
|
+
{
|
|
1068
|
+
id: "state-todo",
|
|
1069
|
+
name: "Todo",
|
|
1070
|
+
description: "Work that has not been started",
|
|
1071
|
+
color: "#e2e2e2",
|
|
1072
|
+
type: "unstarted",
|
|
1073
|
+
position: 0,
|
|
1074
|
+
teamId: defaultTeam.id,
|
|
1075
|
+
createdAt: new Date(),
|
|
1076
|
+
updatedAt: new Date(),
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
id: "state-in-progress",
|
|
1080
|
+
name: "In Progress",
|
|
1081
|
+
description: "Work that is actively being worked on",
|
|
1082
|
+
color: "#f2c94c",
|
|
1083
|
+
type: "started",
|
|
1084
|
+
position: 1,
|
|
1085
|
+
teamId: defaultTeam.id,
|
|
1086
|
+
createdAt: new Date(),
|
|
1087
|
+
updatedAt: new Date(),
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
id: "state-done",
|
|
1091
|
+
name: "Done",
|
|
1092
|
+
description: "Work that has been completed",
|
|
1093
|
+
color: "#5e6ad2",
|
|
1094
|
+
type: "completed",
|
|
1095
|
+
position: 2,
|
|
1096
|
+
teamId: defaultTeam.id,
|
|
1097
|
+
createdAt: new Date(),
|
|
1098
|
+
updatedAt: new Date(),
|
|
1099
|
+
},
|
|
1100
|
+
];
|
|
1101
|
+
for (const state of workflowStates) {
|
|
1102
|
+
this.state.workflowStates.set(state.id, state);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Get the current in-memory state (for testing/debugging).
|
|
1107
|
+
*/
|
|
1108
|
+
getState() {
|
|
1109
|
+
return this.state;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
//# sourceMappingURL=CLIIssueTrackerService.js.map
|