@fink-andreas/pi-linear-tools 0.4.2 → 0.4.3

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/src/linear.js CHANGED
@@ -90,6 +90,360 @@ const ISSUES_WITH_RELATIONS_QUERY = `
90
90
  }
91
91
  `;
92
92
 
93
+ const PROJECT_DETAILS_QUERY = `
94
+ query ProjectDetails($id: String!, $milestoneLimit: Int!) {
95
+ project(id: $id) {
96
+ id
97
+ name
98
+ description
99
+ content
100
+ color
101
+ icon
102
+ priority
103
+ progress
104
+ health
105
+ startDate
106
+ targetDate
107
+ slugId
108
+ url
109
+ archivedAt
110
+ completedAt
111
+ canceledAt
112
+ status {
113
+ id
114
+ name
115
+ type
116
+ color
117
+ }
118
+ lead {
119
+ id
120
+ name
121
+ displayName
122
+ }
123
+ teams {
124
+ nodes {
125
+ id
126
+ key
127
+ name
128
+ }
129
+ }
130
+ projectMilestones(first: $milestoneLimit) {
131
+ nodes {
132
+ id
133
+ name
134
+ status
135
+ progress
136
+ targetDate
137
+ }
138
+ }
139
+ }
140
+ }
141
+ `;
142
+
143
+ const PROJECTS_LOOKUP_QUERY = `
144
+ query ProjectsLookup($includeArchived: Boolean!) {
145
+ projects(first: 250, includeArchived: $includeArchived) {
146
+ nodes {
147
+ id
148
+ name
149
+ slugId
150
+ archivedAt
151
+ }
152
+ }
153
+ }
154
+ `;
155
+
156
+ const PROJECT_CREATE_MUTATION = `
157
+ mutation ProjectCreate($input: ProjectCreateInput!) {
158
+ projectCreate(input: $input) {
159
+ success
160
+ project {
161
+ id
162
+ name
163
+ }
164
+ }
165
+ }
166
+ `;
167
+
168
+ const PROJECT_UPDATE_MUTATION = `
169
+ mutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
170
+ projectUpdate(id: $id, input: $input) {
171
+ success
172
+ project {
173
+ id
174
+ name
175
+ }
176
+ }
177
+ }
178
+ `;
179
+
180
+ const PROJECT_DELETE_MUTATION = `
181
+ mutation ProjectDelete($id: String!) {
182
+ projectDelete(id: $id) {
183
+ success
184
+ entity {
185
+ id
186
+ name
187
+ }
188
+ }
189
+ }
190
+ `;
191
+
192
+ const PROJECT_ARCHIVE_MUTATION = `
193
+ mutation ProjectArchive($id: String!) {
194
+ projectArchiveResult: projectDelete(id: $id) {
195
+ success
196
+ entity {
197
+ id
198
+ name
199
+ }
200
+ }
201
+ }
202
+ `;
203
+
204
+ const PROJECT_UNARCHIVE_MUTATION = `
205
+ mutation ProjectUnarchive($id: String!) {
206
+ projectUnarchive(id: $id) {
207
+ success
208
+ entity {
209
+ id
210
+ name
211
+ }
212
+ }
213
+ }
214
+ `;
215
+
216
+ const PROJECT_UPDATES_BY_PROJECT_QUERY = `
217
+ query ProjectUpdatesByProject($id: String!, $first: Int!, $includeArchived: Boolean!) {
218
+ project(id: $id) {
219
+ id
220
+ name
221
+ projectUpdates(first: $first, includeArchived: $includeArchived) {
222
+ nodes {
223
+ id
224
+ body
225
+ health
226
+ createdAt
227
+ updatedAt
228
+ archivedAt
229
+ url
230
+ slugId
231
+ isDiffHidden
232
+ isStale
233
+ user {
234
+ id
235
+ name
236
+ displayName
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ `;
243
+
244
+ const PROJECT_UPDATE_DETAILS_QUERY = `
245
+ query ProjectUpdateDetails($id: String!) {
246
+ projectUpdate(id: $id) {
247
+ id
248
+ body
249
+ health
250
+ createdAt
251
+ updatedAt
252
+ archivedAt
253
+ editedAt
254
+ url
255
+ slugId
256
+ isDiffHidden
257
+ isStale
258
+ project {
259
+ id
260
+ name
261
+ }
262
+ user {
263
+ id
264
+ name
265
+ displayName
266
+ }
267
+ }
268
+ }
269
+ `;
270
+
271
+ const PROJECT_UPDATE_CREATE_MUTATION = `
272
+ mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {
273
+ projectUpdateCreate(input: $input) {
274
+ success
275
+ projectUpdate {
276
+ id
277
+ }
278
+ }
279
+ }
280
+ `;
281
+
282
+ const PROJECT_UPDATE_UPDATE_MUTATION = `
283
+ mutation ProjectUpdateUpdate($id: String!, $input: ProjectUpdateUpdateInput!) {
284
+ projectUpdateUpdate(id: $id, input: $input) {
285
+ success
286
+ projectUpdate {
287
+ id
288
+ }
289
+ }
290
+ }
291
+ `;
292
+
293
+ const PROJECT_UPDATE_ARCHIVE_MUTATION = `
294
+ mutation ProjectUpdateArchive($id: String!) {
295
+ projectUpdateArchive(id: $id) {
296
+ success
297
+ entity {
298
+ id
299
+ }
300
+ }
301
+ }
302
+ `;
303
+
304
+ const PROJECT_UPDATE_UNARCHIVE_MUTATION = `
305
+ mutation ProjectUpdateUnarchive($id: String!) {
306
+ projectUpdateUnarchive(id: $id) {
307
+ success
308
+ entity {
309
+ id
310
+ }
311
+ }
312
+ }
313
+ `;
314
+
315
+ const DOCUMENT_DETAILS_QUERY = `
316
+ query DocumentDetails($id: String!) {
317
+ document(id: $id) {
318
+ id
319
+ title
320
+ content
321
+ icon
322
+ color
323
+ slugId
324
+ url
325
+ archivedAt
326
+ createdAt
327
+ updatedAt
328
+ project {
329
+ id
330
+ name
331
+ }
332
+ issue {
333
+ id
334
+ identifier
335
+ title
336
+ }
337
+ }
338
+ }
339
+ `;
340
+
341
+ const DOCUMENT_CREATE_MUTATION = `
342
+ mutation DocumentCreate($input: DocumentCreateInput!) {
343
+ documentCreate(input: $input) {
344
+ success
345
+ document {
346
+ id
347
+ }
348
+ }
349
+ }
350
+ `;
351
+
352
+ const DOCUMENT_UPDATE_MUTATION = `
353
+ mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) {
354
+ documentUpdate(id: $id, input: $input) {
355
+ success
356
+ document {
357
+ id
358
+ }
359
+ }
360
+ }
361
+ `;
362
+
363
+ const ISSUE_ACTIVITY_QUERY = `
364
+ query IssueActivity($id: String!, $first: Int!, $includeArchived: Boolean!) {
365
+ issue(id: $id) {
366
+ id
367
+ identifier
368
+ title
369
+ url
370
+ history(first: $first, includeArchived: $includeArchived) {
371
+ nodes {
372
+ id
373
+ createdAt
374
+ updatedAt
375
+ archived
376
+ archivedAt
377
+ autoArchived
378
+ autoClosed
379
+ trashed
380
+ updatedDescription
381
+ fromTitle
382
+ toTitle
383
+ fromPriority
384
+ toPriority
385
+ fromState {
386
+ id
387
+ name
388
+ }
389
+ toState {
390
+ id
391
+ name
392
+ }
393
+ fromAssignee {
394
+ id
395
+ name
396
+ displayName
397
+ }
398
+ toAssignee {
399
+ id
400
+ name
401
+ displayName
402
+ }
403
+ fromProject {
404
+ id
405
+ name
406
+ }
407
+ toProject {
408
+ id
409
+ name
410
+ }
411
+ fromProjectMilestone {
412
+ id
413
+ name
414
+ }
415
+ toProjectMilestone {
416
+ id
417
+ name
418
+ }
419
+ addedLabels {
420
+ id
421
+ name
422
+ }
423
+ removedLabels {
424
+ id
425
+ name
426
+ }
427
+ relationChanges {
428
+ identifier
429
+ type
430
+ }
431
+ attachment {
432
+ id
433
+ title
434
+ url
435
+ }
436
+ actor {
437
+ id
438
+ name
439
+ displayName
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ `;
446
+
93
447
  /**
94
448
  * Execute an optimized GraphQL query using rawRequest
95
449
  * Falls back to SDK method if rawRequest is not available (e.g., in tests)
@@ -136,6 +490,21 @@ async function executeOptimizedQuery(client, query, variables) {
136
490
  };
137
491
  }
138
492
 
493
+ async function executeGraphQL(client, query, variables = {}) {
494
+ const rawRequest =
495
+ (typeof client.client?.rawRequest === 'function' ? client.client.rawRequest.bind(client.client) : null) ||
496
+ (typeof client.rawRequest === 'function' ? client.rawRequest.bind(client) : null);
497
+
498
+ if (!rawRequest) {
499
+ throw new Error('GraphQL rawRequest is unavailable on this Linear client');
500
+ }
501
+
502
+ const response = await rawRequest(query, variables);
503
+ updateRateLimitState(response);
504
+ checkRateLimitWarning();
505
+ return response.data;
506
+ }
507
+
139
508
  /**
140
509
  * Transform raw GraphQL issue data to plain object format
141
510
  * Used by optimized queries to avoid SDK lazy loading
@@ -379,9 +748,201 @@ function isLinearId(value) {
379
748
  function normalizeIssueLookupInput(issue) {
380
749
  const value = String(issue || '').trim();
381
750
  if (!value) throw new Error('Missing required issue identifier');
751
+
752
+ const issueUrlMatch = value.match(/\/issue\/([A-Za-z0-9]+-\d+)(?:[/?#]|$)/i);
753
+ if (issueUrlMatch?.[1]) {
754
+ return issueUrlMatch[1].toUpperCase();
755
+ }
756
+
382
757
  return value;
383
758
  }
384
759
 
760
+ function extractProjectLookupValue(projectRef) {
761
+ const ref = String(projectRef || '').trim();
762
+ if (!ref) {
763
+ return '';
764
+ }
765
+
766
+ const projectUrlMatch = ref.match(/\/project\/([^/?#]+)/i);
767
+ if (projectUrlMatch?.[1]) {
768
+ return projectUrlMatch[1];
769
+ }
770
+
771
+ return ref;
772
+ }
773
+
774
+ function getProjectLookupCandidates(projectRef) {
775
+ const lookupValue = extractProjectLookupValue(projectRef);
776
+ const candidates = new Set([lookupValue]);
777
+
778
+ if (lookupValue.includes('-')) {
779
+ const slugSuffix = lookupValue.split('-').pop();
780
+ if (slugSuffix) {
781
+ candidates.add(slugSuffix);
782
+ }
783
+ }
784
+
785
+ return Array.from(candidates).filter(Boolean);
786
+ }
787
+
788
+ const PROJECT_UPDATE_HEALTH_VALUES = ['onTrack', 'atRisk', 'offTrack'];
789
+
790
+ function normalizePositiveInteger(value, fieldName, defaultValue) {
791
+ if (value === undefined || value === null) {
792
+ return defaultValue;
793
+ }
794
+
795
+ const parsed = typeof value === 'number' ? value : Number(String(value).trim());
796
+ if (!Number.isInteger(parsed) || parsed < 1) {
797
+ throw new Error(`${fieldName} must be a positive integer`);
798
+ }
799
+
800
+ return parsed;
801
+ }
802
+
803
+ function normalizeProjectUpdateHealth(value) {
804
+ if (value === undefined) {
805
+ return undefined;
806
+ }
807
+
808
+ const normalized = String(value).trim();
809
+ if (!normalized) {
810
+ throw new Error(`health must be one of: ${PROJECT_UPDATE_HEALTH_VALUES.join(', ')}`);
811
+ }
812
+
813
+ if (!PROJECT_UPDATE_HEALTH_VALUES.includes(normalized)) {
814
+ throw new Error(`health must be one of: ${PROJECT_UPDATE_HEALTH_VALUES.join(', ')}`);
815
+ }
816
+
817
+ return normalized;
818
+ }
819
+
820
+ function formatPriorityLabel(value) {
821
+ if (value === undefined || value === null) {
822
+ return null;
823
+ }
824
+
825
+ const numeric = Number(value);
826
+ if (!Number.isFinite(numeric)) {
827
+ return String(value);
828
+ }
829
+
830
+ const priorityNames = ['No priority', 'Urgent', 'High', 'Medium', 'Low'];
831
+ return priorityNames[numeric] || `Priority ${numeric}`;
832
+ }
833
+
834
+ function getUserDisplayName(user) {
835
+ return user?.displayName || user?.name || 'Unknown';
836
+ }
837
+
838
+ function summarizeIssueHistoryEntry(entry) {
839
+ if (entry.fromState?.name || entry.toState?.name) {
840
+ if (entry.fromState?.name && entry.toState?.name) {
841
+ return `moved state from ${entry.fromState.name} to ${entry.toState.name}`;
842
+ }
843
+ if (entry.toState?.name) {
844
+ return `set state to ${entry.toState.name}`;
845
+ }
846
+ return `cleared state ${entry.fromState.name}`;
847
+ }
848
+
849
+ if (entry.fromAssignee || entry.toAssignee) {
850
+ const fromAssignee = entry.fromAssignee ? getUserDisplayName(entry.fromAssignee) : null;
851
+ const toAssignee = entry.toAssignee ? getUserDisplayName(entry.toAssignee) : null;
852
+ if (fromAssignee && toAssignee) {
853
+ return `reassigned from ${fromAssignee} to ${toAssignee}`;
854
+ }
855
+ if (toAssignee) {
856
+ return `assigned to ${toAssignee}`;
857
+ }
858
+ return `unassigned from ${fromAssignee}`;
859
+ }
860
+
861
+ if (entry.fromTitle || entry.toTitle) {
862
+ if (entry.fromTitle && entry.toTitle) {
863
+ return `renamed issue from "${entry.fromTitle}" to "${entry.toTitle}"`;
864
+ }
865
+ if (entry.toTitle) {
866
+ return `set title to "${entry.toTitle}"`;
867
+ }
868
+ }
869
+
870
+ if (entry.fromPriority !== undefined || entry.toPriority !== undefined) {
871
+ const fromPriority = formatPriorityLabel(entry.fromPriority);
872
+ const toPriority = formatPriorityLabel(entry.toPriority);
873
+ if (fromPriority && toPriority) {
874
+ return `changed priority from ${fromPriority} to ${toPriority}`;
875
+ }
876
+ if (toPriority) {
877
+ return `set priority to ${toPriority}`;
878
+ }
879
+ }
880
+
881
+ if (entry.fromProject?.name || entry.toProject?.name) {
882
+ if (entry.fromProject?.name && entry.toProject?.name) {
883
+ return `moved project from ${entry.fromProject.name} to ${entry.toProject.name}`;
884
+ }
885
+ if (entry.toProject?.name) {
886
+ return `added to project ${entry.toProject.name}`;
887
+ }
888
+ return `removed from project ${entry.fromProject.name}`;
889
+ }
890
+
891
+ if (entry.fromProjectMilestone?.name || entry.toProjectMilestone?.name) {
892
+ if (entry.fromProjectMilestone?.name && entry.toProjectMilestone?.name) {
893
+ return `moved milestone from ${entry.fromProjectMilestone.name} to ${entry.toProjectMilestone.name}`;
894
+ }
895
+ if (entry.toProjectMilestone?.name) {
896
+ return `set milestone to ${entry.toProjectMilestone.name}`;
897
+ }
898
+ return `cleared milestone ${entry.fromProjectMilestone.name}`;
899
+ }
900
+
901
+ if ((entry.addedLabels?.length || 0) > 0 || (entry.removedLabels?.length || 0) > 0) {
902
+ const labelChanges = [];
903
+ if ((entry.addedLabels?.length || 0) > 0) {
904
+ labelChanges.push(`added labels ${entry.addedLabels.map((label) => label.name).join(', ')}`);
905
+ }
906
+ if ((entry.removedLabels?.length || 0) > 0) {
907
+ labelChanges.push(`removed labels ${entry.removedLabels.map((label) => label.name).join(', ')}`);
908
+ }
909
+ return labelChanges.join('; ');
910
+ }
911
+
912
+ if ((entry.relationChanges?.length || 0) > 0) {
913
+ const relationSummary = entry.relationChanges
914
+ .map((relation) => `${relation.type} ${relation.identifier}`)
915
+ .join(', ');
916
+ return `updated relations: ${relationSummary}`;
917
+ }
918
+
919
+ if (entry.updatedDescription) {
920
+ return 'updated description';
921
+ }
922
+
923
+ if (entry.attachment?.title || entry.attachment?.url) {
924
+ return `linked attachment ${entry.attachment.title ? `"${entry.attachment.title}"` : entry.attachment.url}`;
925
+ }
926
+
927
+ if (entry.archivedAt || entry.archived === true) {
928
+ return entry.autoArchived ? 'auto-archived issue' : 'archived issue';
929
+ }
930
+
931
+ if (entry.autoClosed) {
932
+ return 'auto-closed issue';
933
+ }
934
+
935
+ if (entry.trashed === true) {
936
+ return 'trashed issue';
937
+ }
938
+
939
+ if (entry.trashed === false) {
940
+ return 'restored issue from trash';
941
+ }
942
+
943
+ return 'updated issue';
944
+ }
945
+
385
946
  /**
386
947
  * Safely resolve a lazy-loaded relation without triggering unnecessary API calls
387
948
  * Only fetches if the relation is already cached or if we have minimal data
@@ -686,28 +1247,65 @@ export async function fetchIssuesByProject(client, projectId, states, options =
686
1247
  /**
687
1248
  * Fetch all accessible projects from Linear API
688
1249
  * @param {LinearClient} client - Linear SDK client
1250
+ * @param {{ includeArchived?: boolean }} options - Fetch options
689
1251
  * @returns {Promise<Array<{id: string, name: string}>>}
690
1252
  */
691
- export async function fetchProjects(client) {
1253
+ export async function fetchProjects(client, options = {}) {
692
1254
  return withLinearErrorHandling(async () => {
1255
+ const { includeArchived = false, forceGraphql = false } = options;
693
1256
  const cacheKey = getClientCacheKey(client);
694
- const cached = getCache(projectsCache, cacheKey);
1257
+ const scopedCacheKey = `${cacheKey}::projects::${includeArchived ? 'all' : 'active'}::${forceGraphql ? 'graphql' : 'sdk'}`;
1258
+ const cached = getCache(projectsCache, scopedCacheKey);
695
1259
  if (cached) return cached;
696
1260
 
697
- const result = await client.projects();
698
- const nodes = result.nodes ?? [];
1261
+ let nodes = [];
1262
+ if (includeArchived || forceGraphql) {
1263
+ const data = await executeGraphQL(client, PROJECTS_LOOKUP_QUERY, {
1264
+ includeArchived,
1265
+ });
1266
+ nodes = data?.projects?.nodes ?? [];
1267
+ } else {
1268
+ const result = await client.projects();
1269
+ nodes = result.nodes ?? [];
1270
+ }
699
1271
 
700
1272
  debug('Fetched Linear projects', {
701
1273
  projectCount: nodes.length,
1274
+ includeArchived,
702
1275
  projects: nodes.map((p) => ({ id: p.id, name: p.name })),
703
1276
  });
704
1277
 
705
- const projects = nodes.map(p => ({ id: p.id, name: p.name }));
706
- setCache(projectsCache, cacheKey, projects, CACHE_TTL_MS.projects);
1278
+ const projects = nodes.map((p) => ({
1279
+ id: p.id,
1280
+ name: p.name,
1281
+ slugId: p.slugId ?? null,
1282
+ archivedAt: p.archivedAt ?? null,
1283
+ }));
1284
+ setCache(projectsCache, scopedCacheKey, projects, CACHE_TTL_MS.projects);
707
1285
  return projects;
708
1286
  }, 'fetchProjects');
709
1287
  }
710
1288
 
1289
+ export async function fetchProjectDetails(client, projectRef, options = {}) {
1290
+ return withLinearErrorHandling(async () => {
1291
+ const { milestoneLimit = 10 } = options;
1292
+ const ref = String(projectRef || '').trim();
1293
+ const projectId = isLinearId(ref)
1294
+ ? ref
1295
+ : (await resolveProjectRef(client, ref)).id;
1296
+ const data = await executeGraphQL(client, PROJECT_DETAILS_QUERY, {
1297
+ id: projectId,
1298
+ milestoneLimit,
1299
+ });
1300
+
1301
+ if (!data?.project) {
1302
+ throw new Error(`Project not found: ${projectRef}`);
1303
+ }
1304
+
1305
+ return transformProject(data.project);
1306
+ }, 'fetchProjectDetails');
1307
+ }
1308
+
711
1309
  /**
712
1310
  * Fetch available workspaces (organization context) from Linear API
713
1311
  * @param {LinearClient} client - Linear SDK client
@@ -871,10 +1469,12 @@ export async function getTeamWorkflowStates(client, teamRef) {
871
1469
  * Resolve a project reference (name or ID) to a project object
872
1470
  * @param {LinearClient} client - Linear SDK client
873
1471
  * @param {string} projectRef - Project name or ID
1472
+ * @param {{ includeArchived?: boolean }} options - Lookup options
874
1473
  * @returns {Promise<{id: string, name: string}>}
875
1474
  */
876
- export async function resolveProjectRef(client, projectRef) {
1475
+ export async function resolveProjectRef(client, projectRef, options = {}) {
877
1476
  const ref = String(projectRef || '').trim();
1477
+ const { includeArchived = false } = options;
878
1478
  if (!ref) {
879
1479
  throw new Error('Missing project reference');
880
1480
  }
@@ -890,7 +1490,7 @@ export async function resolveProjectRef(client, projectRef) {
890
1490
  // fall back to cached/full-project lookup below
891
1491
  }
892
1492
 
893
- const projectsById = await fetchProjects(client);
1493
+ const projectsById = await fetchProjects(client, { includeArchived });
894
1494
  const byId = projectsById.find((p) => p.id === ref);
895
1495
  if (byId) {
896
1496
  return byId;
@@ -898,7 +1498,13 @@ export async function resolveProjectRef(client, projectRef) {
898
1498
  throw new Error(`Project not found with ID: ${ref}`);
899
1499
  }
900
1500
 
901
- const projects = await fetchProjects(client);
1501
+ const lookupCandidates = getProjectLookupCandidates(ref);
1502
+ const lookupValue = lookupCandidates[0] || ref;
1503
+ const shouldUseGraphqlLookup = lookupValue !== ref;
1504
+ const projects = await fetchProjects(client, {
1505
+ includeArchived,
1506
+ forceGraphql: shouldUseGraphqlLookup,
1507
+ });
902
1508
 
903
1509
  // Try exact name match
904
1510
  const exactName = projects.find((p) => p.name === ref);
@@ -906,6 +1512,12 @@ export async function resolveProjectRef(client, projectRef) {
906
1512
  return exactName;
907
1513
  }
908
1514
 
1515
+ // Try exact slug match
1516
+ const exactSlug = projects.find((p) => lookupCandidates.includes(p.slugId));
1517
+ if (exactSlug) {
1518
+ return exactSlug;
1519
+ }
1520
+
909
1521
  // Try case-insensitive name match
910
1522
  const lowerRef = ref.toLowerCase();
911
1523
  const insensitiveName = projects.find((p) => p.name?.toLowerCase() === lowerRef);
@@ -913,9 +1525,398 @@ export async function resolveProjectRef(client, projectRef) {
913
1525
  return insensitiveName;
914
1526
  }
915
1527
 
1528
+ // Try case-insensitive slug match
1529
+ const lowerLookupValues = lookupCandidates.map((candidate) => candidate.toLowerCase());
1530
+ const insensitiveSlug = projects.find((p) => p.slugId && lowerLookupValues.includes(p.slugId.toLowerCase()));
1531
+ if (insensitiveSlug) {
1532
+ return insensitiveSlug;
1533
+ }
1534
+
916
1535
  throw new Error(`Project not found: ${ref}. Available projects: ${projects.map((p) => p.name).join(', ')}`);
917
1536
  }
918
1537
 
1538
+ export async function createProject(client, input) {
1539
+ return withLinearErrorHandling(async () => {
1540
+ const name = String(input.name || '').trim();
1541
+ if (!name) {
1542
+ throw new Error('Missing required field: name');
1543
+ }
1544
+
1545
+ const teamIds = Array.isArray(input.teamIds)
1546
+ ? input.teamIds.map((value) => String(value || '').trim()).filter(Boolean)
1547
+ : [];
1548
+
1549
+ if (teamIds.length === 0) {
1550
+ throw new Error('Missing required field: teamIds');
1551
+ }
1552
+
1553
+ const createInput = {
1554
+ name,
1555
+ teamIds,
1556
+ };
1557
+
1558
+ for (const field of ['description', 'color', 'icon', 'leadId', 'startDate', 'targetDate']) {
1559
+ if (input[field] !== undefined) {
1560
+ createInput[field] = input[field];
1561
+ }
1562
+ }
1563
+
1564
+ if (input.priority !== undefined) {
1565
+ createInput.priority = input.priority;
1566
+ }
1567
+
1568
+ const payload = await executeGraphQL(client, PROJECT_CREATE_MUTATION, {
1569
+ input: createInput,
1570
+ });
1571
+
1572
+ if (!payload?.projectCreate?.success || !payload?.projectCreate?.project?.id) {
1573
+ throw new Error('Failed to create project');
1574
+ }
1575
+
1576
+ invalidateProjectsCache(client);
1577
+ return fetchProjectDetails(client, payload.projectCreate.project.id);
1578
+ }, 'createProject');
1579
+ }
1580
+
1581
+ export async function updateProject(client, projectRef, patch = {}) {
1582
+ return withLinearErrorHandling(async () => {
1583
+ const resolved = await resolveProjectRef(client, projectRef);
1584
+ const updateInput = {};
1585
+
1586
+ for (const field of ['name', 'description', 'content', 'color', 'icon', 'startDate', 'targetDate']) {
1587
+ if (patch[field] !== undefined) {
1588
+ updateInput[field] = patch[field];
1589
+ }
1590
+ }
1591
+
1592
+ if (patch.priority !== undefined) {
1593
+ updateInput.priority = patch.priority;
1594
+ }
1595
+
1596
+ if (patch.leadId !== undefined) {
1597
+ updateInput.leadId = patch.leadId;
1598
+ }
1599
+
1600
+ if (patch.teamIds !== undefined) {
1601
+ updateInput.teamIds = patch.teamIds;
1602
+ }
1603
+
1604
+ if (Object.keys(updateInput).length === 0) {
1605
+ throw new Error('No update fields provided');
1606
+ }
1607
+
1608
+ const payload = await executeGraphQL(client, PROJECT_UPDATE_MUTATION, {
1609
+ id: resolved.id,
1610
+ input: updateInput,
1611
+ });
1612
+
1613
+ if (!payload?.projectUpdate?.success || !payload?.projectUpdate?.project?.id) {
1614
+ throw new Error('Failed to update project');
1615
+ }
1616
+
1617
+ invalidateProjectsCache(client);
1618
+ const project = await fetchProjectDetails(client, payload.projectUpdate.project.id);
1619
+
1620
+ return {
1621
+ project,
1622
+ changed: Object.keys(updateInput),
1623
+ };
1624
+ }, 'updateProject');
1625
+ }
1626
+
1627
+ export async function deleteProject(client, projectRef) {
1628
+ return withLinearErrorHandling(async () => {
1629
+ const resolved = await resolveProjectRef(client, projectRef);
1630
+ const payload = await executeGraphQL(client, PROJECT_DELETE_MUTATION, {
1631
+ id: resolved.id,
1632
+ });
1633
+
1634
+ if (!payload?.projectDelete?.success) {
1635
+ throw new Error('Failed to delete project');
1636
+ }
1637
+
1638
+ invalidateProjectsCache(client);
1639
+
1640
+ return {
1641
+ success: true,
1642
+ projectId: resolved.id,
1643
+ name: resolved.name,
1644
+ entity: transformProject(payload.projectDelete.entity),
1645
+ };
1646
+ }, 'deleteProject');
1647
+ }
1648
+
1649
+ export async function archiveProject(client, projectRef) {
1650
+ return withLinearErrorHandling(async () => {
1651
+ const resolved = await resolveProjectRef(client, projectRef);
1652
+ const payload = await executeGraphQL(client, PROJECT_ARCHIVE_MUTATION, {
1653
+ id: resolved.id,
1654
+ });
1655
+
1656
+ if (!payload?.projectArchiveResult?.success) {
1657
+ throw new Error('Failed to archive project');
1658
+ }
1659
+
1660
+ invalidateProjectsCache(client);
1661
+
1662
+ return {
1663
+ success: true,
1664
+ projectId: resolved.id,
1665
+ name: resolved.name,
1666
+ entity: transformProject(payload.projectArchiveResult.entity),
1667
+ };
1668
+ }, 'archiveProject');
1669
+ }
1670
+
1671
+ export async function unarchiveProject(client, projectRef) {
1672
+ return withLinearErrorHandling(async () => {
1673
+ const ref = String(projectRef || '').trim();
1674
+ const resolved = isLinearId(ref)
1675
+ ? { id: ref, name: null }
1676
+ : await resolveProjectRef(client, ref, { includeArchived: true });
1677
+
1678
+ const payload = await executeGraphQL(client, PROJECT_UNARCHIVE_MUTATION, {
1679
+ id: resolved.id,
1680
+ });
1681
+
1682
+ if (!payload?.projectUnarchive?.success) {
1683
+ throw new Error('Failed to unarchive project');
1684
+ }
1685
+
1686
+ invalidateProjectsCache(client);
1687
+ const project = await fetchProjectDetails(client, resolved.id);
1688
+
1689
+ return {
1690
+ success: true,
1691
+ project,
1692
+ };
1693
+ }, 'unarchiveProject');
1694
+ }
1695
+
1696
+ export async function fetchProjectUpdates(client, projectRef, options = {}) {
1697
+ return withLinearErrorHandling(async () => {
1698
+ const includeArchived = options.includeArchived === true;
1699
+ const limit = normalizePositiveInteger(options.limit, 'limit', 10);
1700
+ const resolved = await resolveProjectRef(client, projectRef, { includeArchived });
1701
+ const data = await executeGraphQL(client, PROJECT_UPDATES_BY_PROJECT_QUERY, {
1702
+ id: resolved.id,
1703
+ first: limit,
1704
+ includeArchived,
1705
+ });
1706
+
1707
+ const nodes = data?.project?.projectUpdates?.nodes || [];
1708
+ return {
1709
+ project: {
1710
+ id: data?.project?.id || resolved.id,
1711
+ name: data?.project?.name || resolved.name,
1712
+ },
1713
+ updates: nodes.map(transformProjectUpdate),
1714
+ };
1715
+ }, 'fetchProjectUpdates');
1716
+ }
1717
+
1718
+ export async function fetchProjectUpdateDetails(client, projectUpdateId) {
1719
+ return withLinearErrorHandling(async () => {
1720
+ const id = String(projectUpdateId || '').trim();
1721
+ if (!id) {
1722
+ throw new Error('Missing required field: projectUpdate');
1723
+ }
1724
+
1725
+ const data = await executeGraphQL(client, PROJECT_UPDATE_DETAILS_QUERY, { id });
1726
+
1727
+ if (!data?.projectUpdate) {
1728
+ throw new Error(`Project update not found: ${id}`);
1729
+ }
1730
+
1731
+ return transformProjectUpdate(data.projectUpdate);
1732
+ }, 'fetchProjectUpdateDetails');
1733
+ }
1734
+
1735
+ export async function createProjectUpdate(client, input) {
1736
+ return withLinearErrorHandling(async () => {
1737
+ const projectId = String(input.projectId || '').trim();
1738
+ if (!projectId) {
1739
+ throw new Error('Missing required field: projectId');
1740
+ }
1741
+
1742
+ const createInput = { projectId };
1743
+ if (input.body !== undefined) createInput.body = String(input.body);
1744
+ if (input.health !== undefined) createInput.health = normalizeProjectUpdateHealth(input.health);
1745
+ if (input.isDiffHidden !== undefined) createInput.isDiffHidden = input.isDiffHidden;
1746
+
1747
+ if (createInput.body === undefined && createInput.health === undefined) {
1748
+ throw new Error('At least one of body or health is required');
1749
+ }
1750
+
1751
+ const payload = await executeGraphQL(client, PROJECT_UPDATE_CREATE_MUTATION, {
1752
+ input: createInput,
1753
+ });
1754
+
1755
+ if (!payload?.projectUpdateCreate?.success || !payload?.projectUpdateCreate?.projectUpdate?.id) {
1756
+ throw new Error('Failed to create project update');
1757
+ }
1758
+
1759
+ return fetchProjectUpdateDetails(client, payload.projectUpdateCreate.projectUpdate.id);
1760
+ }, 'createProjectUpdate');
1761
+ }
1762
+
1763
+ export async function updateProjectUpdate(client, projectUpdateId, patch = {}) {
1764
+ return withLinearErrorHandling(async () => {
1765
+ const id = String(projectUpdateId || '').trim();
1766
+ if (!id) {
1767
+ throw new Error('Missing required field: projectUpdate');
1768
+ }
1769
+
1770
+ const updateInput = {};
1771
+ if (patch.body !== undefined) updateInput.body = String(patch.body);
1772
+ if (patch.health !== undefined) updateInput.health = normalizeProjectUpdateHealth(patch.health);
1773
+ if (patch.isDiffHidden !== undefined) updateInput.isDiffHidden = patch.isDiffHidden;
1774
+
1775
+ if (Object.keys(updateInput).length === 0) {
1776
+ throw new Error('No update fields provided');
1777
+ }
1778
+
1779
+ const payload = await executeGraphQL(client, PROJECT_UPDATE_UPDATE_MUTATION, {
1780
+ id,
1781
+ input: updateInput,
1782
+ });
1783
+
1784
+ if (!payload?.projectUpdateUpdate?.success || !payload?.projectUpdateUpdate?.projectUpdate?.id) {
1785
+ throw new Error('Failed to update project update');
1786
+ }
1787
+
1788
+ const projectUpdate = await fetchProjectUpdateDetails(client, payload.projectUpdateUpdate.projectUpdate.id);
1789
+ return {
1790
+ projectUpdate,
1791
+ changed: Object.keys(updateInput),
1792
+ };
1793
+ }, 'updateProjectUpdate');
1794
+ }
1795
+
1796
+ export async function archiveProjectUpdate(client, projectUpdateId) {
1797
+ return withLinearErrorHandling(async () => {
1798
+ const id = String(projectUpdateId || '').trim();
1799
+ if (!id) {
1800
+ throw new Error('Missing required field: projectUpdate');
1801
+ }
1802
+
1803
+ const payload = await executeGraphQL(client, PROJECT_UPDATE_ARCHIVE_MUTATION, { id });
1804
+ if (!payload?.projectUpdateArchive?.success) {
1805
+ throw new Error('Failed to archive project update');
1806
+ }
1807
+
1808
+ return {
1809
+ success: true,
1810
+ projectUpdateId: id,
1811
+ };
1812
+ }, 'archiveProjectUpdate');
1813
+ }
1814
+
1815
+ export async function unarchiveProjectUpdate(client, projectUpdateId) {
1816
+ return withLinearErrorHandling(async () => {
1817
+ const id = String(projectUpdateId || '').trim();
1818
+ if (!id) {
1819
+ throw new Error('Missing required field: projectUpdate');
1820
+ }
1821
+
1822
+ const payload = await executeGraphQL(client, PROJECT_UPDATE_UNARCHIVE_MUTATION, { id });
1823
+ if (!payload?.projectUpdateUnarchive?.success) {
1824
+ throw new Error('Failed to unarchive project update');
1825
+ }
1826
+
1827
+ const projectUpdate = await fetchProjectUpdateDetails(client, id);
1828
+ return {
1829
+ success: true,
1830
+ projectUpdate,
1831
+ };
1832
+ }, 'unarchiveProjectUpdate');
1833
+ }
1834
+
1835
+ export async function fetchDocumentDetails(client, documentRef) {
1836
+ return withLinearErrorHandling(async () => {
1837
+ const id = String(documentRef || '').trim();
1838
+ if (!id) {
1839
+ throw new Error('Missing required field: document');
1840
+ }
1841
+
1842
+ const data = await executeGraphQL(client, DOCUMENT_DETAILS_QUERY, { id });
1843
+
1844
+ if (!data?.document) {
1845
+ throw new Error(`Document not found: ${id}`);
1846
+ }
1847
+
1848
+ return transformDocument(data.document);
1849
+ }, 'fetchDocumentDetails');
1850
+ }
1851
+
1852
+ export async function createDocument(client, input = {}) {
1853
+ return withLinearErrorHandling(async () => {
1854
+ const title = String(input.title || '').trim();
1855
+ if (!title) {
1856
+ throw new Error('Missing required field: title');
1857
+ }
1858
+
1859
+ const createInput = { title };
1860
+ if (input.projectId !== undefined) createInput.projectId = input.projectId;
1861
+ if (input.issueId !== undefined) createInput.issueId = input.issueId;
1862
+
1863
+ if (!createInput.projectId && !createInput.issueId) {
1864
+ throw new Error('Document create requires either projectId or issueId');
1865
+ }
1866
+
1867
+ for (const field of ['content', 'icon', 'color']) {
1868
+ if (input[field] !== undefined) {
1869
+ createInput[field] = input[field];
1870
+ }
1871
+ }
1872
+
1873
+ const payload = await executeGraphQL(client, DOCUMENT_CREATE_MUTATION, {
1874
+ input: createInput,
1875
+ });
1876
+
1877
+ if (!payload?.documentCreate?.success || !payload?.documentCreate?.document?.id) {
1878
+ throw new Error('Failed to create document');
1879
+ }
1880
+
1881
+ return fetchDocumentDetails(client, payload.documentCreate.document.id);
1882
+ }, 'createDocument');
1883
+ }
1884
+
1885
+ export async function updateDocument(client, documentRef, patch = {}) {
1886
+ return withLinearErrorHandling(async () => {
1887
+ const id = String(documentRef || '').trim();
1888
+ if (!id) {
1889
+ throw new Error('Missing required field: document');
1890
+ }
1891
+
1892
+ const updateInput = {};
1893
+ for (const field of ['title', 'content', 'icon', 'color', 'projectId', 'issueId']) {
1894
+ if (patch[field] !== undefined) {
1895
+ updateInput[field] = patch[field];
1896
+ }
1897
+ }
1898
+
1899
+ if (Object.keys(updateInput).length === 0) {
1900
+ throw new Error('No update fields provided');
1901
+ }
1902
+
1903
+ const payload = await executeGraphQL(client, DOCUMENT_UPDATE_MUTATION, {
1904
+ id,
1905
+ input: updateInput,
1906
+ });
1907
+
1908
+ if (!payload?.documentUpdate?.success || !payload?.documentUpdate?.document?.id) {
1909
+ throw new Error('Failed to update document');
1910
+ }
1911
+
1912
+ const document = await fetchDocumentDetails(client, payload.documentUpdate.document.id);
1913
+ return {
1914
+ document,
1915
+ changed: Object.keys(updateInput),
1916
+ };
1917
+ }, 'updateDocument');
1918
+ }
1919
+
919
1920
  /**
920
1921
  * Fetch detailed issue information including comments, parent, children, and attachments
921
1922
  * @param {LinearClient} client - Linear SDK client
@@ -1030,6 +2031,79 @@ export async function fetchIssueDetails(client, issueRef, options = {}) {
1030
2031
  }, 'fetchIssueDetails');
1031
2032
  }
1032
2033
 
2034
+ export async function fetchIssueActivity(client, issueRef, options = {}) {
2035
+ return withLinearErrorHandling(async () => {
2036
+ const limit = normalizePositiveInteger(options.limit, 'limit', 20);
2037
+ const includeArchived = options.includeArchived === true;
2038
+ const resolved = await resolveIssue(client, issueRef);
2039
+ const data = await executeGraphQL(client, ISSUE_ACTIVITY_QUERY, {
2040
+ id: resolved.id,
2041
+ first: limit,
2042
+ includeArchived,
2043
+ });
2044
+
2045
+ if (!data?.issue) {
2046
+ throw new Error(`Issue not found: ${issueRef}`);
2047
+ }
2048
+
2049
+ return {
2050
+ issue: {
2051
+ id: data.issue.id,
2052
+ identifier: data.issue.identifier,
2053
+ title: data.issue.title,
2054
+ url: data.issue.url ?? null,
2055
+ },
2056
+ activity: (data.issue.history?.nodes || []).map((entry) => ({
2057
+ id: entry.id,
2058
+ createdAt: entry.createdAt ?? null,
2059
+ updatedAt: entry.updatedAt ?? null,
2060
+ actor: entry.actor ? {
2061
+ id: entry.actor.id,
2062
+ name: entry.actor.name,
2063
+ displayName: entry.actor.displayName,
2064
+ } : null,
2065
+ fromState: entry.fromState ? { id: entry.fromState.id, name: entry.fromState.name } : null,
2066
+ toState: entry.toState ? { id: entry.toState.id, name: entry.toState.name } : null,
2067
+ fromAssignee: entry.fromAssignee ? {
2068
+ id: entry.fromAssignee.id,
2069
+ name: entry.fromAssignee.name,
2070
+ displayName: entry.fromAssignee.displayName,
2071
+ } : null,
2072
+ toAssignee: entry.toAssignee ? {
2073
+ id: entry.toAssignee.id,
2074
+ name: entry.toAssignee.name,
2075
+ displayName: entry.toAssignee.displayName,
2076
+ } : null,
2077
+ fromTitle: entry.fromTitle ?? null,
2078
+ toTitle: entry.toTitle ?? null,
2079
+ fromPriority: entry.fromPriority ?? null,
2080
+ toPriority: entry.toPriority ?? null,
2081
+ fromProject: entry.fromProject ? { id: entry.fromProject.id, name: entry.fromProject.name } : null,
2082
+ toProject: entry.toProject ? { id: entry.toProject.id, name: entry.toProject.name } : null,
2083
+ fromProjectMilestone: entry.fromProjectMilestone ? { id: entry.fromProjectMilestone.id, name: entry.fromProjectMilestone.name } : null,
2084
+ toProjectMilestone: entry.toProjectMilestone ? { id: entry.toProjectMilestone.id, name: entry.toProjectMilestone.name } : null,
2085
+ addedLabels: (entry.addedLabels || []).map((label) => ({ id: label.id, name: label.name })),
2086
+ removedLabels: (entry.removedLabels || []).map((label) => ({ id: label.id, name: label.name })),
2087
+ relationChanges: (entry.relationChanges || []).map((relation) => ({
2088
+ identifier: relation.identifier,
2089
+ type: relation.type,
2090
+ })),
2091
+ attachment: entry.attachment ? {
2092
+ id: entry.attachment.id,
2093
+ title: entry.attachment.title,
2094
+ url: entry.attachment.url,
2095
+ } : null,
2096
+ archived: entry.archived ?? null,
2097
+ archivedAt: entry.archivedAt ?? null,
2098
+ autoArchived: entry.autoArchived ?? false,
2099
+ autoClosed: entry.autoClosed ?? false,
2100
+ trashed: entry.trashed ?? null,
2101
+ updatedDescription: entry.updatedDescription ?? false,
2102
+ })),
2103
+ };
2104
+ }, 'fetchIssueActivity');
2105
+ }
2106
+
1033
2107
  // ===== MUTATION FUNCTIONS =====
1034
2108
 
1035
2109
  /**
@@ -1514,6 +2588,113 @@ async function transformMilestone(sdkMilestone) {
1514
2588
  };
1515
2589
  }
1516
2590
 
2591
+ function transformProject(rawProject) {
2592
+ if (!rawProject) return null;
2593
+
2594
+ const milestoneNodes = rawProject.projectMilestones?.nodes || [];
2595
+ const teamNodes = rawProject.teams?.nodes || [];
2596
+
2597
+ return {
2598
+ id: rawProject.id,
2599
+ name: rawProject.name,
2600
+ description: rawProject.description ?? '',
2601
+ content: rawProject.content ?? null,
2602
+ color: rawProject.color ?? null,
2603
+ icon: rawProject.icon ?? null,
2604
+ priority: rawProject.priority ?? null,
2605
+ progress: rawProject.progress ?? null,
2606
+ health: rawProject.health ?? null,
2607
+ startDate: rawProject.startDate ?? null,
2608
+ targetDate: rawProject.targetDate ?? null,
2609
+ slugId: rawProject.slugId ?? null,
2610
+ url: rawProject.url ?? null,
2611
+ archivedAt: rawProject.archivedAt ?? null,
2612
+ completedAt: rawProject.completedAt ?? null,
2613
+ canceledAt: rawProject.canceledAt ?? null,
2614
+ status: rawProject.status ? {
2615
+ id: rawProject.status.id,
2616
+ name: rawProject.status.name,
2617
+ type: rawProject.status.type,
2618
+ color: rawProject.status.color,
2619
+ } : null,
2620
+ lead: rawProject.lead ? {
2621
+ id: rawProject.lead.id,
2622
+ name: rawProject.lead.name,
2623
+ displayName: rawProject.lead.displayName,
2624
+ } : null,
2625
+ teams: teamNodes.map((team) => ({
2626
+ id: team.id,
2627
+ key: team.key,
2628
+ name: team.name,
2629
+ })),
2630
+ projectMilestones: milestoneNodes.map((milestone) => ({
2631
+ id: milestone.id,
2632
+ name: milestone.name,
2633
+ status: milestone.status,
2634
+ progress: milestone.progress ?? null,
2635
+ targetDate: milestone.targetDate ?? null,
2636
+ })),
2637
+ };
2638
+ }
2639
+
2640
+ function transformProjectUpdate(rawUpdate) {
2641
+ if (!rawUpdate) return null;
2642
+
2643
+ return {
2644
+ id: rawUpdate.id,
2645
+ body: rawUpdate.body ?? '',
2646
+ health: rawUpdate.health ?? null,
2647
+ createdAt: rawUpdate.createdAt ?? null,
2648
+ updatedAt: rawUpdate.updatedAt ?? null,
2649
+ archivedAt: rawUpdate.archivedAt ?? null,
2650
+ editedAt: rawUpdate.editedAt ?? null,
2651
+ url: rawUpdate.url ?? null,
2652
+ slugId: rawUpdate.slugId ?? null,
2653
+ isDiffHidden: rawUpdate.isDiffHidden ?? false,
2654
+ isStale: rawUpdate.isStale ?? false,
2655
+ project: rawUpdate.project ? {
2656
+ id: rawUpdate.project.id,
2657
+ name: rawUpdate.project.name,
2658
+ } : null,
2659
+ user: rawUpdate.user ? {
2660
+ id: rawUpdate.user.id,
2661
+ name: rawUpdate.user.name,
2662
+ displayName: rawUpdate.user.displayName,
2663
+ } : null,
2664
+ };
2665
+ }
2666
+
2667
+ function transformDocument(rawDocument) {
2668
+ if (!rawDocument) return null;
2669
+
2670
+ return {
2671
+ id: rawDocument.id,
2672
+ title: rawDocument.title ?? '',
2673
+ content: rawDocument.content ?? '',
2674
+ icon: rawDocument.icon ?? null,
2675
+ color: rawDocument.color ?? null,
2676
+ slugId: rawDocument.slugId ?? null,
2677
+ url: rawDocument.url ?? null,
2678
+ archivedAt: rawDocument.archivedAt ?? null,
2679
+ createdAt: rawDocument.createdAt ?? null,
2680
+ updatedAt: rawDocument.updatedAt ?? null,
2681
+ project: rawDocument.project ? {
2682
+ id: rawDocument.project.id,
2683
+ name: rawDocument.project.name,
2684
+ } : null,
2685
+ issue: rawDocument.issue ? {
2686
+ id: rawDocument.issue.id,
2687
+ identifier: rawDocument.issue.identifier,
2688
+ title: rawDocument.issue.title,
2689
+ } : null,
2690
+ };
2691
+ }
2692
+
2693
+ function invalidateProjectsCache(client) {
2694
+ const cacheKey = getClientCacheKey(client);
2695
+ projectsCache.delete(cacheKey);
2696
+ }
2697
+
1517
2698
  /**
1518
2699
  * Fetch milestones for a project
1519
2700
  * @param {LinearClient} client - Linear SDK client
@@ -2009,3 +3190,31 @@ export function formatIssueAsMarkdown(issueData, options = {}) {
2009
3190
 
2010
3191
  return lines.join('\n');
2011
3192
  }
3193
+
3194
+ export function formatIssueActivityAsMarkdown(issueData, options = {}) {
3195
+ const { limit } = options;
3196
+ const lines = [`# Activity for ${issueData.issue.identifier}: ${issueData.issue.title}`];
3197
+
3198
+ if (issueData.issue.url) {
3199
+ lines.push('');
3200
+ lines.push(`**URL:** ${issueData.issue.url}`);
3201
+ }
3202
+
3203
+ if (!issueData.activity || issueData.activity.length === 0) {
3204
+ lines.push('');
3205
+ lines.push('No activity entries found.');
3206
+ return lines.join('\n');
3207
+ }
3208
+
3209
+ lines.push('');
3210
+ lines.push(`Showing ${issueData.activity.length}${limit ? ` of up to ${limit}` : ''} activity entries.`);
3211
+ lines.push('');
3212
+
3213
+ for (const entry of issueData.activity) {
3214
+ const actor = entry.actor ? getUserDisplayName(entry.actor) : 'System';
3215
+ const timestamp = entry.createdAt ? formatRelativeTime(entry.createdAt) : 'unknown time';
3216
+ lines.push(`- **${actor}** ${summarizeIssueHistoryEntry(entry)} _(${timestamp})_`);
3217
+ }
3218
+
3219
+ return lines.join('\n');
3220
+ }