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.
Files changed (41) hide show
  1. package/dist/CyrusAgentSession.d.ts +14 -0
  2. package/dist/CyrusAgentSession.d.ts.map +1 -1
  3. package/dist/agent-runner-types.d.ts +97 -0
  4. package/dist/agent-runner-types.d.ts.map +1 -1
  5. package/dist/config-types.d.ts +22 -4
  6. package/dist/config-types.d.ts.map +1 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/issue-tracker/IIssueTrackerService.d.ts +18 -5
  12. package/dist/issue-tracker/IIssueTrackerService.d.ts.map +1 -1
  13. package/dist/issue-tracker/adapters/CLIEventTransport.d.ts +91 -0
  14. package/dist/issue-tracker/adapters/CLIEventTransport.d.ts.map +1 -0
  15. package/dist/issue-tracker/adapters/CLIEventTransport.js +104 -0
  16. package/dist/issue-tracker/adapters/CLIEventTransport.js.map +1 -0
  17. package/dist/issue-tracker/adapters/CLIIssueTrackerService.d.ts +239 -0
  18. package/dist/issue-tracker/adapters/CLIIssueTrackerService.d.ts.map +1 -0
  19. package/dist/issue-tracker/adapters/CLIIssueTrackerService.js +1112 -0
  20. package/dist/issue-tracker/adapters/CLIIssueTrackerService.js.map +1 -0
  21. package/dist/issue-tracker/adapters/CLIRPCServer.d.ts +318 -0
  22. package/dist/issue-tracker/adapters/CLIRPCServer.d.ts.map +1 -0
  23. package/dist/issue-tracker/adapters/CLIRPCServer.js +536 -0
  24. package/dist/issue-tracker/adapters/CLIRPCServer.js.map +1 -0
  25. package/dist/issue-tracker/adapters/CLITypes.d.ts +243 -0
  26. package/dist/issue-tracker/adapters/CLITypes.d.ts.map +1 -0
  27. package/dist/issue-tracker/adapters/CLITypes.js +378 -0
  28. package/dist/issue-tracker/adapters/CLITypes.js.map +1 -0
  29. package/dist/issue-tracker/adapters/index.d.ts +14 -0
  30. package/dist/issue-tracker/adapters/index.d.ts.map +1 -0
  31. package/dist/issue-tracker/adapters/index.js +11 -0
  32. package/dist/issue-tracker/adapters/index.js.map +1 -0
  33. package/dist/issue-tracker/index.d.ts +2 -1
  34. package/dist/issue-tracker/index.d.ts.map +1 -1
  35. package/dist/issue-tracker/index.js +3 -2
  36. package/dist/issue-tracker/index.js.map +1 -1
  37. package/dist/issue-tracker/types.d.ts +141 -29
  38. package/dist/issue-tracker/types.d.ts.map +1 -1
  39. package/dist/issue-tracker/types.js.map +1 -1
  40. package/package.json +30 -30
  41. 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 `![attachment](${url})`;
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