@achieveai/azuredevops-mcp 1.3.17 → 1.3.19

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 (119) hide show
  1. package/README.md +76 -0
  2. package/dist/Interfaces/Common.js +37 -1
  3. package/dist/Interfaces/Common.js.map +1 -1
  4. package/dist/Services/AzureDevOpsService.js +234 -32
  5. package/dist/Services/AzureDevOpsService.js.map +1 -1
  6. package/dist/Services/BoardsSprintsService.js +111 -13
  7. package/dist/Services/BoardsSprintsService.js.map +1 -1
  8. package/dist/Services/BuildService.js +157 -24
  9. package/dist/Services/BuildService.js.map +1 -1
  10. package/dist/Services/GitService.js +26 -3
  11. package/dist/Services/GitService.js.map +1 -1
  12. package/dist/Services/ProjectService.js +47 -6
  13. package/dist/Services/ProjectService.js.map +1 -1
  14. package/dist/Services/WorkItemService.js +183 -170
  15. package/dist/Services/WorkItemService.js.map +1 -1
  16. package/dist/Tools/BoardsSprintsTools.js +2 -8
  17. package/dist/Tools/BoardsSprintsTools.js.map +1 -1
  18. package/dist/Tools/BuildTools.js +5 -8
  19. package/dist/Tools/BuildTools.js.map +1 -1
  20. package/dist/Tools/GitTools.js +177 -62
  21. package/dist/Tools/GitTools.js.map +1 -1
  22. package/dist/Tools/WorkItemTools.js +110 -172
  23. package/dist/Tools/WorkItemTools.js.map +1 -1
  24. package/dist/index.js +31 -26
  25. package/dist/index.js.map +1 -1
  26. package/dist/utils/apiUsageGuidance.js +336 -0
  27. package/dist/utils/apiUsageGuidance.js.map +1 -0
  28. package/dist/utils/formatHelpers.js +15 -0
  29. package/dist/utils/formatHelpers.js.map +1 -1
  30. package/package.json +3 -3
  31. package/dist/Services/BuildService.project.test.js +0 -91
  32. package/dist/Services/BuildService.project.test.js.map +0 -1
  33. package/dist/Services/GitService.project.test.js +0 -407
  34. package/dist/Services/GitService.project.test.js.map +0 -1
  35. package/dist/package.json +0 -59
  36. package/dist/src/Interfaces/AIAssisted.js +0 -3
  37. package/dist/src/Interfaces/AIAssisted.js.map +0 -1
  38. package/dist/src/Interfaces/ArtifactManagement.js +0 -3
  39. package/dist/src/Interfaces/ArtifactManagement.js.map +0 -1
  40. package/dist/src/Interfaces/AzureDevOps.js +0 -3
  41. package/dist/src/Interfaces/AzureDevOps.js.map +0 -1
  42. package/dist/src/Interfaces/BoardsAndSprints.js +0 -3
  43. package/dist/src/Interfaces/BoardsAndSprints.js.map +0 -1
  44. package/dist/src/Interfaces/CodeAndRepositories.js +0 -3
  45. package/dist/src/Interfaces/CodeAndRepositories.js.map +0 -1
  46. package/dist/src/Interfaces/Common.js +0 -134
  47. package/dist/src/Interfaces/Common.js.map +0 -1
  48. package/dist/src/Interfaces/CostResourceManagement.js +0 -3
  49. package/dist/src/Interfaces/CostResourceManagement.js.map +0 -1
  50. package/dist/src/Interfaces/DevSecOps.js +0 -3
  51. package/dist/src/Interfaces/DevSecOps.js.map +0 -1
  52. package/dist/src/Interfaces/ExternalIntegrations.js +0 -3
  53. package/dist/src/Interfaces/ExternalIntegrations.js.map +0 -1
  54. package/dist/src/Interfaces/HybridCrossPlatform.js +0 -3
  55. package/dist/src/Interfaces/HybridCrossPlatform.js.map +0 -1
  56. package/dist/src/Interfaces/Pipelines.js +0 -3
  57. package/dist/src/Interfaces/Pipelines.js.map +0 -1
  58. package/dist/src/Interfaces/ProjectManagement.js +0 -3
  59. package/dist/src/Interfaces/ProjectManagement.js.map +0 -1
  60. package/dist/src/Interfaces/TestingCapabilities.js +0 -3
  61. package/dist/src/Interfaces/TestingCapabilities.js.map +0 -1
  62. package/dist/src/Interfaces/Wiki.js +0 -3
  63. package/dist/src/Interfaces/Wiki.js.map +0 -1
  64. package/dist/src/Interfaces/WorkItems.js +0 -3
  65. package/dist/src/Interfaces/WorkItems.js.map +0 -1
  66. package/dist/src/Services/AIAssistedDevelopmentService.js +0 -195
  67. package/dist/src/Services/AIAssistedDevelopmentService.js.map +0 -1
  68. package/dist/src/Services/ArtifactManagementService.js +0 -346
  69. package/dist/src/Services/ArtifactManagementService.js.map +0 -1
  70. package/dist/src/Services/AzureDevOpsService.js +0 -385
  71. package/dist/src/Services/AzureDevOpsService.js.map +0 -1
  72. package/dist/src/Services/BoardsSprintsService.js +0 -339
  73. package/dist/src/Services/BoardsSprintsService.js.map +0 -1
  74. package/dist/src/Services/BuildService.js +0 -405
  75. package/dist/src/Services/BuildService.js.map +0 -1
  76. package/dist/src/Services/DevSecOpsService.js +0 -307
  77. package/dist/src/Services/DevSecOpsService.js.map +0 -1
  78. package/dist/src/Services/EntraAuthHandler.js +0 -337
  79. package/dist/src/Services/EntraAuthHandler.js.map +0 -1
  80. package/dist/src/Services/GitService.js +0 -1595
  81. package/dist/src/Services/GitService.js.map +0 -1
  82. package/dist/src/Services/ProjectService.js +0 -257
  83. package/dist/src/Services/ProjectService.js.map +0 -1
  84. package/dist/src/Services/TestingCapabilitiesService.js +0 -149
  85. package/dist/src/Services/TestingCapabilitiesService.js.map +0 -1
  86. package/dist/src/Services/WikiService.js +0 -90
  87. package/dist/src/Services/WikiService.js.map +0 -1
  88. package/dist/src/Services/WorkItemService.js +0 -885
  89. package/dist/src/Services/WorkItemService.js.map +0 -1
  90. package/dist/src/Tools/AIAssistedDevelopmentTools.js +0 -137
  91. package/dist/src/Tools/AIAssistedDevelopmentTools.js.map +0 -1
  92. package/dist/src/Tools/ArtifactManagementTools.js +0 -140
  93. package/dist/src/Tools/ArtifactManagementTools.js.map +0 -1
  94. package/dist/src/Tools/BoardsSprintsTools.js +0 -338
  95. package/dist/src/Tools/BoardsSprintsTools.js.map +0 -1
  96. package/dist/src/Tools/BuildTools.js +0 -468
  97. package/dist/src/Tools/BuildTools.js.map +0 -1
  98. package/dist/src/Tools/DevSecOpsTools.js +0 -147
  99. package/dist/src/Tools/DevSecOpsTools.js.map +0 -1
  100. package/dist/src/Tools/GitTools.js +0 -1475
  101. package/dist/src/Tools/GitTools.js.map +0 -1
  102. package/dist/src/Tools/ProjectTools.js +0 -360
  103. package/dist/src/Tools/ProjectTools.js.map +0 -1
  104. package/dist/src/Tools/TestingCapabilitiesTools.js +0 -157
  105. package/dist/src/Tools/TestingCapabilitiesTools.js.map +0 -1
  106. package/dist/src/Tools/WikiTools.js +0 -137
  107. package/dist/src/Tools/WikiTools.js.map +0 -1
  108. package/dist/src/Tools/WorkItemTools.js +0 -862
  109. package/dist/src/Tools/WorkItemTools.js.map +0 -1
  110. package/dist/src/config.js +0 -176
  111. package/dist/src/config.js.map +0 -1
  112. package/dist/src/index.js +0 -1716
  113. package/dist/src/index.js.map +0 -1
  114. package/dist/src/utils/formatHelpers.js +0 -257
  115. package/dist/src/utils/formatHelpers.js.map +0 -1
  116. package/dist/src/utils/getClassMethods.js +0 -8
  117. package/dist/src/utils/getClassMethods.js.map +0 -1
  118. package/dist/src/utils/repositoryResolver.js +0 -40
  119. package/dist/src/utils/repositoryResolver.js.map +0 -1
@@ -1,885 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WorkItemService = void 0;
4
- const WorkItemTrackingInterfaces_1 = require("azure-devops-node-api/interfaces/WorkItemTrackingInterfaces");
5
- const VSSInterfaces_1 = require("azure-devops-node-api/interfaces/common/VSSInterfaces");
6
- const AzureDevOpsService_1 = require("./AzureDevOpsService");
7
- const formatHelpers_1 = require("../utils/formatHelpers");
8
- /** Rich-text fields that expect HTML — markdown is auto-converted for these */
9
- const RICH_TEXT_FIELDS = new Set([
10
- 'System.Description',
11
- 'System.History',
12
- 'System.ReproSteps',
13
- 'Microsoft.VSTS.TCM.Steps',
14
- 'Microsoft.VSTS.Common.AcceptanceCriteria',
15
- ]);
16
- class WorkItemService extends AzureDevOpsService_1.AzureDevOpsService {
17
- constructor(config) {
18
- super(config);
19
- this.wiqlCache = new Map();
20
- this.CACHE_TTL_MS = 60000; // 60 seconds
21
- this.MAX_CACHE_ENTRIES = 50;
22
- this.DEFAULT_RECENT_DAYS = 7;
23
- this.MAX_RECENT_DAYS = 30;
24
- this.DEFAULT_FIELDS = [
25
- ...AzureDevOpsService_1.AzureDevOpsService.BASE_SUMMARY_FIELDS,
26
- ...AzureDevOpsService_1.AzureDevOpsService.SCHEDULING_FIELDS,
27
- ];
28
- }
29
- getCachedWiql(cacheKey) {
30
- const entry = this.wiqlCache.get(cacheKey);
31
- if (entry && Date.now() - entry.timestamp < this.CACHE_TTL_MS) {
32
- return entry.result;
33
- }
34
- if (entry) {
35
- this.wiqlCache.delete(cacheKey);
36
- }
37
- return undefined;
38
- }
39
- setCachedWiql(cacheKey, result) {
40
- if (this.wiqlCache.size >= this.MAX_CACHE_ENTRIES) {
41
- const oldestKey = this.wiqlCache.keys().next().value;
42
- if (oldestKey !== undefined) {
43
- this.wiqlCache.delete(oldestKey);
44
- }
45
- }
46
- this.wiqlCache.set(cacheKey, { result, timestamp: Date.now() });
47
- }
48
- buildWiqlCacheKey(query, top) {
49
- return `${this.config.project}:${top ?? 'none'}:${query}`;
50
- }
51
- normalizeRecentDays(days) {
52
- if (!days || Number.isNaN(days)) {
53
- return this.DEFAULT_RECENT_DAYS;
54
- }
55
- return Math.min(Math.max(Math.floor(days), 1), this.MAX_RECENT_DAYS);
56
- }
57
- escapeWiqlLiteral(value) {
58
- return value.replace(/'/g, "''");
59
- }
60
- applyRecentChangesFilter(query, days) {
61
- const normalizedDays = this.normalizeRecentDays(days);
62
- if (/\bfrom\s+workitemlinks\b/i.test(query) || /\[System\.ChangedDate\]/i.test(query)) {
63
- return { query };
64
- }
65
- const orderByMatch = query.match(/\border\s+by\b/i);
66
- const whereMatch = query.match(/\bwhere\b/i);
67
- const fromWorkItemsMatch = query.match(/\bfrom\s+workitems\b/i);
68
- if (!whereMatch) {
69
- if (!fromWorkItemsMatch || fromWorkItemsMatch.index === undefined) {
70
- return { query };
71
- }
72
- const insertAt = fromWorkItemsMatch.index + fromWorkItemsMatch[0].length;
73
- const scopedQuery = `${query.slice(0, insertAt)}\n WHERE [System.ChangedDate] >= @today - ${normalizedDays}${query.slice(insertAt)}`;
74
- return { query: scopedQuery, recentDaysApplied: normalizedDays };
75
- }
76
- const filter = `\n AND [System.ChangedDate] >= @today - ${normalizedDays}`;
77
- if (orderByMatch && orderByMatch.index !== undefined) {
78
- return {
79
- query: `${query.slice(0, orderByMatch.index)}${filter}\n ${query.slice(orderByMatch.index)}`,
80
- recentDaysApplied: normalizedDays,
81
- };
82
- }
83
- return {
84
- query: `${query}${filter}`,
85
- recentDaysApplied: normalizedDays,
86
- };
87
- }
88
- /**
89
- * Query work items using WIQL
90
- */
91
- async listWorkItems(wiqlQuery, top, days, fields) {
92
- try {
93
- const serverTop = top ?? 100;
94
- const scopedQuery = this.applyRecentChangesFilter(wiqlQuery, days);
95
- const cacheKey = this.buildWiqlCacheKey(scopedQuery.query, serverTop);
96
- const cached = this.getCachedWiql(cacheKey);
97
- if (cached)
98
- return cached;
99
- const throttleNotices = [];
100
- const witApi = await this.getWorkItemTrackingApi();
101
- const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({ query: scopedQuery.query }, { project: this.config.project }, undefined, serverTop), {
102
- operationName: 'workItems.list.queryByWiql',
103
- details: { project: this.config.project, top: serverTop, recentDays: scopedQuery.recentDaysApplied },
104
- }, throttleNotices);
105
- const hydratedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems || [], {
106
- fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.list.batchHydrate',
107
- throttleAccumulator: throttleNotices,
108
- });
109
- const response = {
110
- ...queryResult,
111
- workItems: hydratedWorkItems,
112
- count: hydratedWorkItems.length,
113
- originalQuery: wiqlQuery,
114
- effectiveQuery: scopedQuery.query,
115
- recentDaysApplied: scopedQuery.recentDaysApplied,
116
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
117
- };
118
- this.setCachedWiql(cacheKey, response);
119
- return response;
120
- }
121
- catch (error) {
122
- console.error('Error listing work items:', error);
123
- throw error;
124
- }
125
- }
126
- /**
127
- * Get a work item by ID
128
- */
129
- async getWorkItemById(params) {
130
- try {
131
- const witApi = await this.getWorkItemTrackingApi();
132
- const workItem = await this.withAuthRetry(() => witApi.getWorkItem(params.id, undefined, undefined, WorkItemTrackingInterfaces_1.WorkItemExpand.Relations, this.config.project));
133
- // Transform to streamlined format for MCP tool consumption
134
- if (workItem && workItem.fields) {
135
- const streamlined = {
136
- id: workItem.id,
137
- rev: workItem.rev,
138
- title: workItem.fields['System.Title'],
139
- workItemType: workItem.fields['System.WorkItemType'],
140
- state: workItem.fields['System.State'],
141
- areaPath: workItem.fields['System.AreaPath'],
142
- iterationPath: workItem.fields['System.IterationPath'],
143
- assignedTo: workItem.fields['System.AssignedTo'] ? {
144
- displayName: workItem.fields['System.AssignedTo'].displayName,
145
- uniqueName: workItem.fields['System.AssignedTo'].uniqueName
146
- } : null,
147
- createdBy: workItem.fields['System.CreatedBy'] ? {
148
- displayName: workItem.fields['System.CreatedBy'].displayName,
149
- uniqueName: workItem.fields['System.CreatedBy'].uniqueName
150
- } : null,
151
- createdDate: workItem.fields['System.CreatedDate'],
152
- changedDate: workItem.fields['System.ChangedDate'],
153
- description: workItem.fields['System.Description'],
154
- priority: workItem.fields['Microsoft.VSTS.Common.Priority'],
155
- originalEstimate: workItem.fields['Microsoft.VSTS.Scheduling.OriginalEstimate'],
156
- completedWork: workItem.fields['Microsoft.VSTS.Scheduling.CompletedWork'],
157
- remainingWork: workItem.fields['Microsoft.VSTS.Scheduling.RemainingWork']
158
- };
159
- // Add work item relations/dependencies
160
- if (workItem.relations && workItem.relations.length > 0) {
161
- streamlined.relations = workItem.relations.map((relation) => {
162
- if (relation.rel === 'ArtifactLink') {
163
- // Artifact link (PR, Build, Branch, Commit, etc.)
164
- const artifactInfo = this.parseArtifactUri(relation.url);
165
- return {
166
- relationshipType: relation.rel,
167
- artifactType: artifactInfo.type,
168
- artifactId: artifactInfo.id,
169
- artifactDisplayName: relation.attributes?.name || artifactInfo.type,
170
- artifactUri: relation.url,
171
- comment: relation.attributes?.comment || null
172
- };
173
- }
174
- else {
175
- // Work item link
176
- const urlParts = relation.url.split('/');
177
- const relatedId = parseInt(urlParts[urlParts.length - 1]);
178
- return {
179
- relationshipType: relation.rel,
180
- relatedWorkItemId: relatedId,
181
- comment: relation.attributes?.comment || null
182
- };
183
- }
184
- });
185
- }
186
- // Remove undefined fields to keep response clean
187
- Object.keys(streamlined).forEach(key => {
188
- if (streamlined[key] === undefined) {
189
- delete streamlined[key];
190
- }
191
- });
192
- return streamlined;
193
- }
194
- return workItem;
195
- }
196
- catch (error) {
197
- console.error(`Error getting work item ${params.id}:`, error);
198
- throw error;
199
- }
200
- }
201
- /**
202
- * Get work item with child effort roll-up
203
- */
204
- async getWorkItemWithEffortRollup(params) {
205
- try {
206
- const workItem = await this.getWorkItemById(params);
207
- // If this work item has child relationships, get effort roll-up
208
- if (workItem.relations) {
209
- const childRelations = workItem.relations.filter((rel) => rel.relationshipType === 'System.LinkTypes.Hierarchy-Forward');
210
- if (childRelations.length > 0) {
211
- // Fetch child work items to calculate effort roll-up
212
- const childEffort = await this.calculateChildEffort(childRelations);
213
- // Add roll-up information to the work item
214
- workItem.childEffortRollup = {
215
- childCount: childRelations.length,
216
- totalOriginalEstimate: childEffort.totalOriginal,
217
- totalCompletedWork: childEffort.totalCompleted,
218
- totalRemainingWork: childEffort.totalRemaining,
219
- childWorkItems: childEffort.childDetails
220
- };
221
- }
222
- }
223
- return workItem;
224
- }
225
- catch (error) {
226
- console.error(`Error getting work item with effort rollup ${params.id}:`, error);
227
- throw error;
228
- }
229
- }
230
- /**
231
- * Parse a vstfs:/// artifact URI into type and ID.
232
- * Handles both %2F and / separators in composite IDs.
233
- */
234
- parseArtifactUri(uri) {
235
- // Normalize %2F to / for easier parsing
236
- const normalized = uri.replace(/%2[fF]/g, '/');
237
- // vstfs:///Git/PullRequestId/{projectId}/{repoId}/{prId}
238
- const prMatch = normalized.match(/vstfs:\/\/\/Git\/PullRequestId\/[^/]+\/[^/]+\/(\d+)/);
239
- if (prMatch)
240
- return { type: 'Pull Request', id: prMatch[1] };
241
- // vstfs:///Build/Build/{buildId}
242
- const buildMatch = normalized.match(/vstfs:\/\/\/Build\/Build\/(\d+)/);
243
- if (buildMatch)
244
- return { type: 'Build', id: buildMatch[1] };
245
- // vstfs:///Git/Ref/{projectId}/{repoId}/GB{branchName}
246
- const branchMatch = normalized.match(/vstfs:\/\/\/Git\/Ref\/[^/]+\/[^/]+\/GB(.+)/);
247
- if (branchMatch)
248
- return { type: 'Branch', id: branchMatch[1] };
249
- // vstfs:///Git/Commit/{projectId}/{repoId}/{commitSha}
250
- const commitMatch = normalized.match(/vstfs:\/\/\/Git\/Commit\/[^/]+\/[^/]+\/([a-f0-9]+)/i);
251
- if (commitMatch)
252
- return { type: 'Commit', id: commitMatch[1] };
253
- // Fallback: extract tool/type from URI pattern vstfs:///{tool}/{type}/...
254
- const genericMatch = normalized.match(/vstfs:\/\/\/([^/]+)\/([^/]+)\/(.*)/);
255
- if (genericMatch)
256
- return { type: `${genericMatch[1]}/${genericMatch[2]}`, id: genericMatch[3] };
257
- return { type: 'Unknown', id: uri };
258
- }
259
- /**
260
- * Calculate effort roll-up from child work items
261
- */
262
- async calculateChildEffort(childRelations) {
263
- const witApi = await this.getWorkItemTrackingApi();
264
- let totalOriginal = 0;
265
- let totalCompleted = 0;
266
- let totalRemaining = 0;
267
- const childDetails = [];
268
- // Fetch child work items in batch
269
- const childIds = childRelations.map((rel) => rel.relatedWorkItemId);
270
- try {
271
- const childWorkItems = await witApi.getWorkItems(childIds, ['System.Id', 'System.Title', 'System.WorkItemType', 'System.State',
272
- 'Microsoft.VSTS.Scheduling.OriginalEstimate',
273
- 'Microsoft.VSTS.Scheduling.CompletedWork',
274
- 'Microsoft.VSTS.Scheduling.RemainingWork'], undefined, undefined, undefined, this.config.project);
275
- childWorkItems.forEach((child) => {
276
- const original = child.fields['Microsoft.VSTS.Scheduling.OriginalEstimate'] || 0;
277
- const completed = child.fields['Microsoft.VSTS.Scheduling.CompletedWork'] || 0;
278
- const remaining = child.fields['Microsoft.VSTS.Scheduling.RemainingWork'] || 0;
279
- totalOriginal += original;
280
- totalCompleted += completed;
281
- totalRemaining += remaining;
282
- childDetails.push({
283
- id: child.id,
284
- title: child.fields['System.Title'],
285
- workItemType: child.fields['System.WorkItemType'],
286
- state: child.fields['System.State'],
287
- originalEstimate: original,
288
- completedWork: completed,
289
- remainingWork: remaining
290
- });
291
- });
292
- }
293
- catch (error) {
294
- console.error('Error fetching child work items for effort calculation:', error);
295
- }
296
- return {
297
- totalOriginal,
298
- totalCompleted,
299
- totalRemaining,
300
- childDetails
301
- };
302
- }
303
- /**
304
- * Search work items using text
305
- */
306
- async searchWorkItems(params) {
307
- try {
308
- const serverTop = params.top || 25;
309
- const recentDays = this.normalizeRecentDays(params.days);
310
- const searchText = this.escapeWiqlLiteral(params.searchText);
311
- const query = `SELECT [System.Id], [System.Title], [System.State], [System.ChangedDate]
312
- FROM WorkItems
313
- WHERE [System.TeamProject] = @project
314
- AND [System.ChangedDate] >= @today - ${recentDays}
315
- AND (
316
- [System.Title] CONTAINS '${searchText}'
317
- OR [System.Description] CONTAINS '${searchText}'
318
- )
319
- ORDER BY [System.ChangedDate] DESC`;
320
- const cacheKey = this.buildWiqlCacheKey(query, serverTop);
321
- const cached = this.getCachedWiql(cacheKey);
322
- if (cached)
323
- return cached;
324
- const throttleNotices = [];
325
- const witApi = await this.getWorkItemTrackingApi();
326
- const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({
327
- query
328
- }, {
329
- project: this.config.project
330
- }, undefined, serverTop), {
331
- operationName: 'workItems.search.queryByWiql',
332
- details: { project: this.config.project, top: serverTop, recentDays },
333
- }, throttleNotices);
334
- const advisory = 'Search uses CONTAINS clauses, which are expensive in Azure DevOps. Prefer listWorkItems with focused WIQL or saved queries when possible.';
335
- if (queryResult.workItems && queryResult.workItems.length > 0) {
336
- const transformedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems, {
337
- fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.search.batchHydrate',
338
- throttleAccumulator: throttleNotices,
339
- });
340
- const response = {
341
- searchQuery: params.searchText,
342
- totalResults: queryResult.workItems.length,
343
- returnedResults: transformedWorkItems.length,
344
- workItems: transformedWorkItems,
345
- wiql: query,
346
- recentDaysApplied: recentDays,
347
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
348
- advisory,
349
- };
350
- this.setCachedWiql(cacheKey, response);
351
- return response;
352
- }
353
- const emptyResponse = {
354
- searchQuery: params.searchText,
355
- totalResults: 0,
356
- returnedResults: 0,
357
- workItems: [],
358
- wiql: query,
359
- recentDaysApplied: recentDays,
360
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
361
- advisory,
362
- };
363
- this.setCachedWiql(cacheKey, emptyResponse);
364
- return emptyResponse;
365
- }
366
- catch (error) {
367
- console.error('Error searching work items:', error);
368
- throw error;
369
- }
370
- }
371
- /**
372
- * Get recently updated work items
373
- */
374
- async getRecentWorkItems(params) {
375
- try {
376
- const days = this.normalizeRecentDays(params.days);
377
- const top = params.top || 10;
378
- const skip = params.skip || 0;
379
- const serverTop = skip + top;
380
- const query = `SELECT [System.Id], [System.Title], [System.State], [System.ChangedDate]
381
- FROM WorkItems
382
- WHERE [System.TeamProject] = @project
383
- AND [System.ChangedDate] >= @today - ${days}
384
- ORDER BY [System.ChangedDate] DESC`;
385
- const cacheKey = this.buildWiqlCacheKey(query, serverTop);
386
- const cached = this.getCachedWiql(cacheKey);
387
- if (cached)
388
- return cached;
389
- const throttleNotices = [];
390
- const witApi = await this.getWorkItemTrackingApi();
391
- const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({
392
- query
393
- }, {
394
- project: this.config.project
395
- }, undefined, serverTop), {
396
- operationName: 'workItems.recent.queryByWiql',
397
- details: { project: this.config.project, top: serverTop, recentDays: days },
398
- }, throttleNotices);
399
- const pageRefs = queryResult.workItems ? queryResult.workItems.slice(skip, skip + top) : [];
400
- const hydratedWorkItems = await this.hydrateWorkItemRefs(pageRefs, {
401
- fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.recent.batchHydrate',
402
- throttleAccumulator: throttleNotices,
403
- });
404
- const response = {
405
- ...queryResult,
406
- workItems: hydratedWorkItems,
407
- count: hydratedWorkItems.length,
408
- recentDaysApplied: days,
409
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
410
- };
411
- this.setCachedWiql(cacheKey, response);
412
- return response;
413
- }
414
- catch (error) {
415
- console.error('Error getting recent work items:', error);
416
- throw error;
417
- }
418
- }
419
- /**
420
- * Get work items assigned to current user
421
- */
422
- async getMyWorkItems(params) {
423
- try {
424
- let stateCondition = '';
425
- if (params.state) {
426
- stateCondition = `AND [System.State] = '${this.escapeWiqlLiteral(params.state)}'`;
427
- }
428
- const serverTop = params.top || 50;
429
- const recentDays = this.normalizeRecentDays(params.days);
430
- const query = `SELECT [System.Id], [System.Title], [System.State], [System.CreatedDate]
431
- FROM WorkItems
432
- WHERE [System.TeamProject] = @project
433
- AND [System.AssignedTo] = @me
434
- AND [System.ChangedDate] >= @today - ${recentDays}
435
- ${stateCondition}
436
- ORDER BY [System.CreatedDate] DESC`;
437
- const cacheKey = this.buildWiqlCacheKey(query, serverTop);
438
- const cached = this.getCachedWiql(cacheKey);
439
- if (cached)
440
- return cached;
441
- const throttleNotices = [];
442
- const witApi = await this.getWorkItemTrackingApi();
443
- const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({
444
- query
445
- }, {
446
- project: this.config.project
447
- }, undefined, serverTop), {
448
- operationName: 'workItems.mine.queryByWiql',
449
- details: { project: this.config.project, top: serverTop, recentDays },
450
- }, throttleNotices);
451
- const hydratedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems || [], {
452
- fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.mine.batchHydrate',
453
- throttleAccumulator: throttleNotices,
454
- });
455
- const response = {
456
- ...queryResult,
457
- workItems: hydratedWorkItems,
458
- count: hydratedWorkItems.length,
459
- recentDaysApplied: recentDays,
460
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
461
- };
462
- this.setCachedWiql(cacheKey, response);
463
- return response;
464
- }
465
- catch (error) {
466
- console.error('Error getting my work items:', error);
467
- throw error;
468
- }
469
- }
470
- /**
471
- * Create a work item
472
- */
473
- async createWorkItem(params) {
474
- try {
475
- const witApi = await this.getWorkItemTrackingApi();
476
- const patchDocument = [];
477
- // Add title
478
- patchDocument.push({
479
- op: VSSInterfaces_1.Operation.Add,
480
- path: "/fields/System.Title",
481
- value: params.title
482
- });
483
- // Add description if provided (convert markdown to HTML for Azure DevOps rich-text field)
484
- if (params.description) {
485
- patchDocument.push({
486
- op: VSSInterfaces_1.Operation.Add,
487
- path: "/fields/System.Description",
488
- value: (0, formatHelpers_1.markdownToHtml)(params.description)
489
- });
490
- }
491
- // Add assigned to if provided
492
- if (params.assignedTo) {
493
- patchDocument.push({
494
- op: VSSInterfaces_1.Operation.Add,
495
- path: "/fields/System.AssignedTo",
496
- value: params.assignedTo
497
- });
498
- }
499
- // Add state if provided
500
- if (params.state) {
501
- patchDocument.push({
502
- op: VSSInterfaces_1.Operation.Add,
503
- path: "/fields/System.State",
504
- value: params.state
505
- });
506
- }
507
- // Add area path if provided
508
- if (params.areaPath) {
509
- patchDocument.push({
510
- op: VSSInterfaces_1.Operation.Add,
511
- path: "/fields/System.AreaPath",
512
- value: params.areaPath
513
- });
514
- }
515
- // Add iteration path if provided
516
- if (params.iterationPath) {
517
- patchDocument.push({
518
- op: VSSInterfaces_1.Operation.Add,
519
- path: "/fields/System.IterationPath",
520
- value: params.iterationPath
521
- });
522
- }
523
- // Add additional fields if provided
524
- if (params.additionalFields) {
525
- for (const [key, value] of Object.entries(params.additionalFields)) {
526
- patchDocument.push({
527
- op: VSSInterfaces_1.Operation.Add,
528
- path: `/fields/${key}`,
529
- value: value
530
- });
531
- }
532
- }
533
- const workItem = await witApi.createWorkItem(undefined, patchDocument, this.config.project, params.workItemType);
534
- return workItem;
535
- }
536
- catch (error) {
537
- console.error('Error creating work item:', error);
538
- throw error;
539
- }
540
- }
541
- /**
542
- * Update a work item
543
- */
544
- async updateWorkItem(params) {
545
- try {
546
- const witApi = await this.getWorkItemTrackingApi();
547
- const patchDocument = [];
548
- // Add fields from the params (rich-text fields auto-converted from markdown to HTML)
549
- for (const [key, value] of Object.entries(params.fields)) {
550
- patchDocument.push({
551
- op: VSSInterfaces_1.Operation.Add,
552
- path: `/fields/${key}`,
553
- value: RICH_TEXT_FIELDS.has(key) && typeof value === 'string' ? (0, formatHelpers_1.markdownToHtml)(value) : value
554
- });
555
- }
556
- const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.id, this.config.project);
557
- return workItem;
558
- }
559
- catch (error) {
560
- console.error(`Error updating work item ${params.id}:`, error);
561
- throw error;
562
- }
563
- }
564
- /**
565
- * Get comments on a work item
566
- */
567
- async getWorkItemComments(params) {
568
- try {
569
- const witApi = await this.getWorkItemTrackingApi();
570
- const sortOrder = params.order === 'asc' ? WorkItemTrackingInterfaces_1.CommentSortOrder.Asc : WorkItemTrackingInterfaces_1.CommentSortOrder.Desc;
571
- const commentList = await this.withAuthRetry(() => witApi.getComments(this.config.project, params.id, params.top, undefined, // continuationToken
572
- params.includeDeleted ?? false, WorkItemTrackingInterfaces_1.CommentExpandOptions.None, sortOrder));
573
- return commentList;
574
- }
575
- catch (error) {
576
- console.error(`Error getting comments for work item ${params.id}:`, error);
577
- throw error;
578
- }
579
- }
580
- /**
581
- * Add a comment to a work item.
582
- * Uses ADO's server-side markdown renderer (format=markdown) for comments.
583
- */
584
- async addWorkItemComment(params) {
585
- try {
586
- const format = params.format === 'html' ? 'html' : 'markdown';
587
- const text = format === 'markdown' ? (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.text)) : params.text;
588
- // Sanitise inputs before URL interpolation (SDK calls handle this internally, but this is a raw REST call)
589
- const id = Math.floor(Number(params.id));
590
- if (!Number.isSafeInteger(id) || id <= 0)
591
- throw new Error(`Invalid work item ID: ${params.id}`);
592
- const baseUrl = this.connection.serverUrl.replace(/\/+$/, '');
593
- const project = encodeURIComponent(this.config.project);
594
- const url = `${baseUrl}/${project}/_apis/wit/workItems/${id}/comments?format=${format}&api-version=7.2-preview.4`;
595
- const response = await this.withAuthRetry(() => this.connection.rest.create(url, { text }));
596
- if (!response.result) {
597
- throw new Error(`Azure DevOps API returned no data when creating comment on work item ${params.id}`);
598
- }
599
- return response.result;
600
- }
601
- catch (error) {
602
- console.error(`Error adding comment to work item ${params.id}:`, error);
603
- throw error;
604
- }
605
- }
606
- /**
607
- * Update an existing comment on a work item.
608
- * Uses ADO's server-side markdown renderer (format=markdown) for comments.
609
- */
610
- async updateWorkItemComment(params) {
611
- try {
612
- const format = params.format === 'html' ? 'html' : 'markdown';
613
- const text = format === 'markdown' ? (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.text)) : params.text;
614
- const id = Math.floor(Number(params.id));
615
- if (!Number.isSafeInteger(id) || id <= 0)
616
- throw new Error(`Invalid work item ID: ${params.id}`);
617
- const commentId = Math.floor(Number(params.commentId));
618
- if (!Number.isSafeInteger(commentId) || commentId <= 0)
619
- throw new Error(`Invalid comment ID: ${params.commentId}`);
620
- const baseUrl = this.connection.serverUrl.replace(/\/+$/, '');
621
- const project = encodeURIComponent(this.config.project);
622
- const url = `${baseUrl}/${project}/_apis/wit/workItems/${id}/comments/${commentId}?format=${format}&api-version=7.2-preview.4`;
623
- const response = await this.withAuthRetry(() => this.connection.rest.update(url, { text }));
624
- if (!response.result) {
625
- throw new Error(`Azure DevOps API returned no data when updating comment ${params.commentId} on work item ${params.id}`);
626
- }
627
- return response.result;
628
- }
629
- catch (error) {
630
- console.error(`Error updating comment ${params.commentId} on work item ${params.id}:`, error);
631
- throw error;
632
- }
633
- }
634
- /**
635
- * Update work item state
636
- */
637
- async updateWorkItemState(params) {
638
- try {
639
- const witApi = await this.getWorkItemTrackingApi();
640
- const patchDocument = [
641
- {
642
- op: VSSInterfaces_1.Operation.Add,
643
- path: "/fields/System.State",
644
- value: params.state
645
- }
646
- ];
647
- // Add comment if provided (convert markdown to HTML for rich-text field)
648
- if (params.comment) {
649
- patchDocument.push({
650
- op: VSSInterfaces_1.Operation.Add,
651
- path: "/fields/System.History",
652
- value: (0, formatHelpers_1.markdownToHtml)(params.comment)
653
- });
654
- }
655
- const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.id, this.config.project);
656
- return workItem;
657
- }
658
- catch (error) {
659
- console.error(`Error updating state for work item ${params.id}:`, error);
660
- throw error;
661
- }
662
- }
663
- /**
664
- * Assign work item to a user
665
- */
666
- async assignWorkItem(params) {
667
- try {
668
- const witApi = await this.getWorkItemTrackingApi();
669
- const patchDocument = [
670
- {
671
- op: VSSInterfaces_1.Operation.Add,
672
- path: "/fields/System.AssignedTo",
673
- value: params.assignedTo
674
- }
675
- ];
676
- const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.id, this.config.project);
677
- return workItem;
678
- }
679
- catch (error) {
680
- console.error(`Error assigning work item ${params.id}:`, error);
681
- throw error;
682
- }
683
- }
684
- /**
685
- * Create a link between a work item and another work item or artifact
686
- */
687
- async createLink(params) {
688
- try {
689
- const witApi = await this.getWorkItemTrackingApi();
690
- let relationValue;
691
- if (params.artifactUri) {
692
- // Artifact link (PR, Build, Branch, Commit)
693
- relationValue = {
694
- rel: "ArtifactLink",
695
- url: params.artifactUri,
696
- attributes: {
697
- comment: params.comment || "",
698
- name: params.artifactName || ""
699
- }
700
- };
701
- }
702
- else {
703
- // Work item link (existing behavior)
704
- relationValue = {
705
- rel: params.linkType,
706
- url: `${this.config.orgUrl}/_apis/wit/workItems/${params.targetWorkItemId}`,
707
- attributes: {
708
- comment: params.comment || ""
709
- }
710
- };
711
- }
712
- const patchDocument = [
713
- {
714
- op: VSSInterfaces_1.Operation.Add,
715
- path: "/relations/-",
716
- value: relationValue
717
- }
718
- ];
719
- const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.sourceId, this.config.project);
720
- return workItem;
721
- }
722
- catch (error) {
723
- console.error(`Error creating link:`, error);
724
- throw error;
725
- }
726
- }
727
- /**
728
- * Bulk create or update work items
729
- */
730
- async bulkUpdateWorkItems(params) {
731
- try {
732
- const results = [];
733
- for (const workItemParams of params.workItems) {
734
- if ('id' in workItemParams) {
735
- // It's an update
736
- const result = await this.updateWorkItem(workItemParams);
737
- results.push(result);
738
- }
739
- else {
740
- // It's a create
741
- const result = await this.createWorkItem(workItemParams);
742
- results.push(result);
743
- }
744
- }
745
- return {
746
- count: results.length,
747
- workItems: results
748
- };
749
- }
750
- catch (error) {
751
- console.error('Error in bulk work item operation:', error);
752
- throw error;
753
- }
754
- }
755
- // ── New Work Item Enhancement Methods ──────────────────────────
756
- /**
757
- * Get multiple work items by IDs in a single call.
758
- */
759
- async getWorkItemsBatch(params) {
760
- try {
761
- const witApi = await this.getWorkItemTrackingApi();
762
- const workItems = await witApi.getWorkItems(params.ids, params.fields, undefined, // asOf
763
- WorkItemTrackingInterfaces_1.WorkItemExpand.Relations);
764
- return workItems || [];
765
- }
766
- catch (error) {
767
- console.error('Error getting work items batch:', error);
768
- throw error;
769
- }
770
- }
771
- /**
772
- * Get revision history for a work item.
773
- */
774
- async getWorkItemRevisions(params) {
775
- try {
776
- const witApi = await this.getWorkItemTrackingApi();
777
- const revisions = await witApi.getRevisions(params.id, params.top, params.skip);
778
- return revisions || [];
779
- }
780
- catch (error) {
781
- console.error('Error getting work item revisions:', error);
782
- throw error;
783
- }
784
- }
785
- /**
786
- * Execute a saved WIQL query by query ID and return the work items.
787
- */
788
- async getQueryResults(params) {
789
- try {
790
- const throttleNotices = [];
791
- const witApi = await this.getWorkItemTrackingApi();
792
- const queryResult = await this.withAuthRetry(() => witApi.queryById(params.queryId, { project: this.config.project }), {
793
- operationName: 'workItems.savedQuery.queryById',
794
- details: { project: this.config.project, queryId: params.queryId },
795
- }, throttleNotices);
796
- if (!queryResult || !queryResult.workItems || queryResult.workItems.length === 0) {
797
- return {
798
- workItems: [],
799
- count: 0,
800
- queryType: queryResult?.queryType,
801
- columns: queryResult?.columns?.map((c) => c.referenceName),
802
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
803
- };
804
- }
805
- const hydratedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems, {
806
- fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.savedQuery.batchHydrate',
807
- throttleAccumulator: throttleNotices,
808
- });
809
- return {
810
- workItems: hydratedWorkItems,
811
- count: hydratedWorkItems.length,
812
- queryType: queryResult.queryType,
813
- columns: queryResult.columns?.map((c) => c.referenceName),
814
- throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
815
- };
816
- }
817
- catch (error) {
818
- console.error('Error executing saved query:', error);
819
- throw error;
820
- }
821
- }
822
- /**
823
- * Create a child work item linked to a parent.
824
- */
825
- async addChildWorkItem(params) {
826
- try {
827
- // First create the work item
828
- const createParams = {
829
- workItemType: params.workItemType,
830
- title: params.title,
831
- description: params.description,
832
- assignedTo: params.assignedTo,
833
- state: params.state,
834
- areaPath: params.areaPath,
835
- iterationPath: params.iterationPath,
836
- additionalFields: params.additionalFields,
837
- };
838
- const child = await this.createWorkItem(createParams);
839
- if (!child || !child.id) {
840
- throw new Error('Failed to create child work item.');
841
- }
842
- // Link child to parent
843
- const patchDocument = [
844
- {
845
- op: VSSInterfaces_1.Operation.Add,
846
- path: '/relations/-',
847
- value: {
848
- rel: 'System.LinkTypes.Hierarchy-Reverse',
849
- url: `${this.config.orgUrl}/_apis/wit/workItems/${params.parentId}`,
850
- attributes: { comment: 'Created as child work item' },
851
- },
852
- },
853
- ];
854
- const witApi = await this.getWorkItemTrackingApi();
855
- const updated = await witApi.updateWorkItem({}, // customHeaders
856
- patchDocument, child.id, this.config.project);
857
- return updated || child;
858
- }
859
- catch (error) {
860
- console.error('Error creating child work item:', error);
861
- throw error;
862
- }
863
- }
864
- /**
865
- * Remove a relation (link) from a work item by relation index.
866
- */
867
- async unlinkWorkItem(params) {
868
- try {
869
- const witApi = await this.getWorkItemTrackingApi();
870
- const patchDocument = [
871
- {
872
- op: VSSInterfaces_1.Operation.Remove,
873
- path: `/relations/${params.relationIndex}`,
874
- },
875
- ];
876
- return await witApi.updateWorkItem({}, patchDocument, params.id, this.config.project);
877
- }
878
- catch (error) {
879
- console.error('Error unlinking work item:', error);
880
- throw error;
881
- }
882
- }
883
- }
884
- exports.WorkItemService = WorkItemService;
885
- //# sourceMappingURL=WorkItemService.js.map