jira-ai 0.4.10 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ import { hasCredentials } from './lib/auth-storage.js';
28
28
  import { CliError } from './types/errors.js';
29
29
  import { CommandError } from './lib/errors.js';
30
30
  import { ui } from './lib/ui.js';
31
- import { validateOptions, CreateTaskSchema, AddCommentSchema, UpdateDescriptionSchema, RunJqlSchema, GetPersonWorklogSchema, TimeframeSchema, IssueKeySchema, ProjectKeySchema } from './lib/validation.js';
31
+ import { CreateTaskSchema, AddCommentSchema, UpdateDescriptionSchema, RunJqlSchema, GetPersonWorklogSchema, GetIssueStatisticsSchema, validateOptions, IssueKeySchema, ProjectKeySchema, TimeframeSchema } from './lib/validation.js';
32
32
  import { realpathSync } from 'fs';
33
33
  // Load environment variables
34
34
  // @ts-ignore - quiet option exists but is not in types
@@ -223,7 +223,10 @@ program
223
223
  program
224
224
  .command('get-issue-statistics <task-ids>')
225
225
  .description('Calculate and display time-based metrics for one or more issues (comma-separated). Returns a table containing key, summary, total time logged, original estimate, and a detailed breakdown of duration spent in each status.')
226
- .action(withPermission('get-issue-statistics', getIssueStatisticsCommand));
226
+ .option('--full-breakdown', 'Display each status in its own column')
227
+ .action(withPermission('get-issue-statistics', getIssueStatisticsCommand, {
228
+ schema: GetIssueStatisticsSchema
229
+ }));
227
230
  // Get person worklog command
228
231
  program
229
232
  .command('get-person-worklog <person> <timeframe>')
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import { getIssueStatistics, validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { formatIssueStatistics } from '../lib/formatters.js';
4
4
  import { ui } from '../lib/ui.js';
5
- export async function getIssueStatisticsCommand(taskIds) {
5
+ export async function getIssueStatisticsCommand(taskIds, options = {}) {
6
6
  const ids = taskIds.split(',').map(id => id.trim()).filter(id => id !== '');
7
7
  if (ids.length === 0) {
8
8
  console.error(chalk.red('Please provide at least one issue ID.'));
@@ -34,7 +34,7 @@ Failed to fetch statistics for ${id}: ${error.message}`));
34
34
  }
35
35
  if (results.length > 0) {
36
36
  ui.succeedSpinner(chalk.green('Statistics retrieved'));
37
- console.log(formatIssueStatistics(results));
37
+ console.log(formatIssueStatistics(results, options.fullBreakdown));
38
38
  }
39
39
  else {
40
40
  ui.failSpinner('Failed to retrieve statistics or all issues were filtered out');
@@ -1,37 +1,16 @@
1
1
  import chalk from 'chalk';
2
- import { getTaskWithDetails, getCurrentUser } from '../lib/jira-client.js';
2
+ import { validateIssuePermissions } from '../lib/jira-client.js';
3
3
  import { formatTaskDetails } from '../lib/formatters.js';
4
4
  import { CommandError } from '../lib/errors.js';
5
5
  import { ui } from '../lib/ui.js';
6
- import { isCommandAllowed, validateIssueAgainstFilters } from '../lib/settings.js';
7
6
  export async function taskWithDetailsCommand(taskId, options = {}) {
8
7
  ui.startSpinner(`Fetching details for ${taskId}...`);
9
8
  try {
10
- const task = await getTaskWithDetails(taskId, {
9
+ const task = await validateIssuePermissions(taskId, 'task-with-details', {
11
10
  includeHistory: options.includeDetailedHistory,
12
11
  historyLimit: options.historyLimit ? parseInt(options.historyLimit, 10) : undefined,
13
12
  historyOffset: options.historyOffset ? parseInt(options.historyOffset, 10) : undefined,
14
13
  });
15
- const projectKey = task.key.split('-')[0];
16
- // Check if command is allowed for this project
17
- if (!isCommandAllowed('task-with-details', projectKey)) {
18
- ui.failSpinner(chalk.red('Command not allowed for this project'));
19
- throw new CommandError(`Command 'task-with-details' is not allowed for project ${projectKey}.`, {
20
- hints: [
21
- `Update settings.yaml to enable this command for this project.`
22
- ]
23
- });
24
- }
25
- // Check granular filters
26
- const currentUser = await getCurrentUser();
27
- if (!validateIssueAgainstFilters(task, currentUser.accountId)) {
28
- ui.failSpinner(chalk.red('Access denied by project filters'));
29
- throw new CommandError(`Access to issue ${taskId} is restricted by project filters.`, {
30
- hints: [
31
- `This project has filters that you do not meet (e.g., participated roles).`
32
- ]
33
- });
34
- }
35
14
  ui.succeedSpinner(chalk.green('Task details retrieved'));
36
15
  console.log(formatTaskDetails(task));
37
16
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
+ import { decode } from 'html-entities';
3
4
  import { formatTimestamp, truncate, formatDuration } from './utils.js';
4
5
  /**
5
6
  * Create a styled table
@@ -47,7 +48,7 @@ export function formatProjects(projects) {
47
48
  * Format task details with comments
48
49
  */
49
50
  export function formatTaskDetails(task) {
50
- let output = '\n' + chalk.bold.cyan(`${task.key}: ${task.summary}`) + '\n\n';
51
+ let output = '\n' + chalk.bold.cyan(`${task.key}: ${decode(task.summary)}`) + '\n\n';
51
52
  // Basic info table
52
53
  const infoTable = createTable(['Property', 'Value'], [15, 65]);
53
54
  infoTable.push(['Status', chalk.green(task.status.name)], ['Assignee', task.assignee?.displayName || chalk.gray('Unassigned')], ['Reporter', task.reporter?.displayName || chalk.gray('N/A')], ['Created', formatTimestamp(task.created)], ['Updated', formatTimestamp(task.updated)]);
@@ -81,7 +82,7 @@ export function formatTaskDetails(task) {
81
82
  const parentTable = createTable(['Key', 'Summary', 'Status'], [12, 50, 18]);
82
83
  parentTable.push([
83
84
  chalk.cyan(task.parent.key),
84
- truncate(task.parent.summary, 50),
85
+ truncate(decode(task.parent.summary), 50),
85
86
  task.parent.status.name,
86
87
  ]);
87
88
  output += parentTable.toString() + '\n\n';
@@ -93,7 +94,7 @@ export function formatTaskDetails(task) {
93
94
  task.subtasks.forEach((subtask) => {
94
95
  subtasksTable.push([
95
96
  chalk.cyan(subtask.key),
96
- truncate(subtask.summary, 50),
97
+ truncate(decode(subtask.summary), 50),
97
98
  subtask.status.name,
98
99
  ]);
99
100
  });
@@ -103,7 +104,7 @@ export function formatTaskDetails(task) {
103
104
  if (task.description) {
104
105
  output += chalk.bold('Description:') + '\n';
105
106
  output += chalk.dim('─'.repeat(80)) + '\n';
106
- output += task.description + '\n';
107
+ output += decode(task.description) + '\n';
107
108
  output += chalk.dim('─'.repeat(80)) + '\n\n';
108
109
  }
109
110
  // Comments
@@ -113,7 +114,7 @@ export function formatTaskDetails(task) {
113
114
  output += chalk.cyan(`${index + 1}. ${comment.author.displayName}`) +
114
115
  chalk.gray(` - ${formatTimestamp(comment.created)}`) + '\n';
115
116
  output += chalk.dim('─'.repeat(80)) + '\n';
116
- output += comment.body + '\n';
117
+ output += decode(comment.body) + '\n';
117
118
  output += chalk.dim('─'.repeat(80)) + '\n\n';
118
119
  });
119
120
  }
@@ -221,7 +222,7 @@ export function formatJqlResults(issues) {
221
222
  }
222
223
  table.push([
223
224
  chalk.cyan(issue.key),
224
- truncate(issue.summary, 40),
225
+ truncate(decode(issue.summary), 40),
225
226
  statusColor(issue.status.name),
226
227
  issue.assignee ? truncate(issue.assignee.displayName, 20) : chalk.gray('Unassigned'),
227
228
  typeof priorityName === 'string' ? priorityColor(priorityName) : priorityName,
@@ -277,38 +278,137 @@ export function formatProjectIssueTypes(projectKey, issueTypes) {
277
278
  /**
278
279
  * Format issue statistics
279
280
  */
280
- export function formatIssueStatistics(statsList) {
281
+ export function formatIssueStatistics(statsList, fullBreakdown = false) {
281
282
  if (statsList.length === 0) {
282
283
  return chalk.yellow('No statistics to display.');
283
284
  }
284
- const table = createTable([
285
- 'Key',
286
- 'Summary',
287
- 'Time Logged',
288
- 'Estimate',
289
- 'Status Breakdown'
290
- ], [12, 30, 15, 15, 40]);
285
+ if (!fullBreakdown) {
286
+ const table = createTable([
287
+ 'Key',
288
+ 'Summary',
289
+ 'Time Logged',
290
+ 'Estimate',
291
+ 'Status Breakdown'
292
+ ], [12, 30, 15, 15, 40]);
293
+ statsList.forEach((stats) => {
294
+ // Breakdown of time spent in each unique status
295
+ const breakdown = Object.entries(stats.statusDurations)
296
+ .map(([status, seconds]) => `${status}: ${formatDuration(seconds, 24)}`)
297
+ .join('\n');
298
+ const timeSpentStr = formatDuration(stats.timeSpentSeconds, 8);
299
+ const estimateStr = formatDuration(stats.originalEstimateSeconds, 8);
300
+ // Highlight if time spent exceeds estimate
301
+ const timeSpentFormatted = stats.timeSpentSeconds > stats.originalEstimateSeconds && stats.originalEstimateSeconds > 0
302
+ ? chalk.red(timeSpentStr)
303
+ : chalk.green(timeSpentStr);
304
+ table.push([
305
+ chalk.cyan(stats.key),
306
+ truncate(decode(stats.summary), 30),
307
+ timeSpentFormatted,
308
+ estimateStr,
309
+ breakdown
310
+ ]);
311
+ });
312
+ let output = '\n' + chalk.bold('Issue Statistics') + '\n\n';
313
+ output += table.toString() + '\n';
314
+ return output;
315
+ }
316
+ // Full breakdown logic
317
+ // 1. Identify all unique statuses and their total durations
318
+ const statusTotals = {};
319
+ statsList.forEach(stats => {
320
+ Object.entries(stats.statusDurations).forEach(([status, duration]) => {
321
+ statusTotals[status] = (statusTotals[status] || 0) + duration;
322
+ });
323
+ });
324
+ // 2. Filter out statuses with 0 total time
325
+ const activeStatuses = Object.keys(statusTotals)
326
+ .filter(status => statusTotals[status] > 0)
327
+ .sort(); // Sort for consistent column order
328
+ const omittedStatuses = Object.keys(statusTotals)
329
+ .filter(status => statusTotals[status] === 0)
330
+ .sort();
331
+ // 3. Define headers and column widths
332
+ const baseHeaders = ['Key', 'Summary', 'Time Logged', 'Estimate'];
333
+ const headers = [...baseHeaders, ...activeStatuses];
334
+ // Calculate column widths - 12 for Key, 25 for Summary, 12 for others
335
+ const colWidths = [12, 25, 12, 12, ...activeStatuses.map(() => 15)];
336
+ const table = createTable(headers, colWidths);
337
+ // 4. Helper for median calculation
338
+ const calculateMedian = (values) => {
339
+ if (values.length === 0)
340
+ return 0;
341
+ const sorted = [...values].sort((a, b) => a - b);
342
+ const mid = Math.floor(sorted.length / 2);
343
+ return sorted.length % 2 !== 0
344
+ ? sorted[mid]
345
+ : (sorted[mid - 1] + sorted[mid]) / 2;
346
+ };
347
+ // 5. Track values for summary rows
348
+ const columnValues = {
349
+ 'Time Logged': [],
350
+ 'Estimate': []
351
+ };
352
+ activeStatuses.forEach(status => { columnValues[status] = []; });
353
+ // 6. Add rows for each issue
291
354
  statsList.forEach((stats) => {
292
- // Breakdown of time spent in each unique status
293
- const breakdown = Object.entries(stats.statusDurations)
294
- .map(([status, seconds]) => `${status}: ${formatDuration(seconds, 24)}`)
295
- .join('\n');
296
- const timeSpentStr = formatDuration(stats.timeSpentSeconds, 8);
297
- const estimateStr = formatDuration(stats.originalEstimateSeconds, 8);
298
- // Highlight if time spent exceeds estimate
299
- const timeSpentFormatted = stats.timeSpentSeconds > stats.originalEstimateSeconds && stats.originalEstimateSeconds > 0
355
+ const timeSpentSeconds = stats.timeSpentSeconds;
356
+ const originalEstimateSeconds = stats.originalEstimateSeconds;
357
+ columnValues['Time Logged'].push(timeSpentSeconds);
358
+ columnValues['Estimate'].push(originalEstimateSeconds);
359
+ const timeSpentStr = formatDuration(timeSpentSeconds, 8);
360
+ const estimateStr = formatDuration(originalEstimateSeconds, 8);
361
+ const timeSpentFormatted = timeSpentSeconds > originalEstimateSeconds && originalEstimateSeconds > 0
300
362
  ? chalk.red(timeSpentStr)
301
363
  : chalk.green(timeSpentStr);
302
- table.push([
364
+ const row = [
303
365
  chalk.cyan(stats.key),
304
- truncate(stats.summary, 30),
366
+ truncate(decode(stats.summary), 25),
305
367
  timeSpentFormatted,
306
- estimateStr,
307
- breakdown
308
- ]);
368
+ estimateStr
369
+ ];
370
+ activeStatuses.forEach(status => {
371
+ const duration = stats.statusDurations[status] || 0;
372
+ columnValues[status].push(duration);
373
+ row.push(formatDuration(duration, 8));
374
+ });
375
+ table.push(row);
309
376
  });
310
- let output = '\n' + chalk.bold('Issue Statistics') + '\n\n';
377
+ // 7. Add summary rows
378
+ const summaryTypes = ['Total', 'Mean', 'Median'];
379
+ summaryTypes.forEach(type => {
380
+ const row = [chalk.bold(type), '', '', ''];
381
+ // Fill basic metrics
382
+ ['Time Logged', 'Estimate'].forEach((col, idx) => {
383
+ const values = columnValues[col];
384
+ let val = 0;
385
+ if (type === 'Total')
386
+ val = values.reduce((a, b) => a + b, 0);
387
+ else if (type === 'Mean')
388
+ val = values.reduce((a, b) => a + b, 0) / values.length;
389
+ else if (type === 'Median')
390
+ val = calculateMedian(values);
391
+ row[idx + 2] = chalk.bold(formatDuration(val, 8));
392
+ });
393
+ // Fill status columns
394
+ activeStatuses.forEach(status => {
395
+ const values = columnValues[status];
396
+ let val = 0;
397
+ if (type === 'Total')
398
+ val = values.reduce((a, b) => a + b, 0);
399
+ else if (type === 'Mean')
400
+ val = values.reduce((a, b) => a + b, 0) / values.length;
401
+ else if (type === 'Median')
402
+ val = calculateMedian(values);
403
+ row.push(chalk.bold(formatDuration(val, 8)));
404
+ });
405
+ table.push(row);
406
+ });
407
+ let output = '\n' + chalk.bold('Issue Statistics (Full Breakdown)') + '\n\n';
311
408
  output += table.toString() + '\n';
409
+ if (omittedStatuses.length > 0) {
410
+ output += chalk.gray(`Note: The following statuses were omitted because they had 0 total time: ${omittedStatuses.join(', ')}`) + '\n';
411
+ }
312
412
  return output;
313
413
  }
314
414
  /**
@@ -349,9 +449,9 @@ export function formatWorklogs(worklogs, groupByIssue = false) {
349
449
  table.push([
350
450
  w.started.split('T')[0],
351
451
  chalk.cyan(w.issueKey),
352
- truncate(w.summary, 30),
452
+ truncate(decode(w.summary), 30),
353
453
  w.timeSpent,
354
- truncate(w.comment || '', 45),
454
+ truncate(decode(w.comment || ''), 45),
355
455
  ]);
356
456
  });
357
457
  const totalHours = (totalSeconds / 3600).toFixed(2);
@@ -1,7 +1,7 @@
1
1
  import { Version3Client } from 'jira.js';
2
2
  import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
3
3
  import { loadCredentials } from './auth-storage.js';
4
- import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters } from './settings.js';
4
+ import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters, loadSettings } from './settings.js';
5
5
  import { CommandError } from './errors.js';
6
6
  let jiraClient = null;
7
7
  let organizationOverride = undefined;
@@ -356,8 +356,8 @@ export async function createIssue(projectKey, summary, issueTypeName, parentKey)
356
356
  /**
357
357
  * Validate that the current user has permission to perform a command on an issue
358
358
  */
359
- export async function validateIssuePermissions(issueKey, commandName) {
360
- const task = await getTaskWithDetails(issueKey);
359
+ export async function validateIssuePermissions(issueKey, commandName, options = {}) {
360
+ const task = await getTaskWithDetails(issueKey, options);
361
361
  const projectKey = task.key.split('-')[0];
362
362
  if (!isProjectAllowed(projectKey)) {
363
363
  throw new CommandError(`Project '${projectKey}' is not allowed by your settings.`);
@@ -373,6 +373,28 @@ export async function validateIssuePermissions(issueKey, commandName) {
373
373
  hints: [`This project has filters that you do not meet (e.g., participated roles).`]
374
374
  });
375
375
  }
376
+ // Check JQL filters
377
+ const settings = loadSettings();
378
+ let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
379
+ if (!project) {
380
+ project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
381
+ }
382
+ if (project && typeof project !== 'string' && project.filters?.jql) {
383
+ const client = getJiraClient();
384
+ const jql = `key = "${issueKey}" AND (${project.filters.jql})`;
385
+ const response = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
386
+ jql,
387
+ maxResults: 1,
388
+ fields: ['key'],
389
+ });
390
+ if (!response.issues || response.issues.length === 0) {
391
+ throw new CommandError(`Access to issue ${issueKey} is restricted by project filters.`, {
392
+ hints: [
393
+ `This project has a JQL filter that this issue does not meet: ${project.filters.jql}`,
394
+ ]
395
+ });
396
+ }
397
+ }
376
398
  return task;
377
399
  }
378
400
  /**
@@ -32,7 +32,7 @@ export function loadSettings() {
32
32
  console.error('Error migrating settings.yaml:', error);
33
33
  const defaultSettings = {
34
34
  projects: ['all'],
35
- commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
35
+ commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
36
36
  };
37
37
  cachedSettings = defaultSettings;
38
38
  return cachedSettings;
@@ -42,7 +42,7 @@ export function loadSettings() {
42
42
  // Create default settings.yaml if it doesn't exist anywhere
43
43
  const defaultSettings = {
44
44
  projects: ['all'],
45
- commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
45
+ commands: ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
46
46
  };
47
47
  try {
48
48
  const yamlStr = yaml.dump(defaultSettings);
@@ -68,7 +68,7 @@ export function loadSettings() {
68
68
  const settings = rawSettings;
69
69
  cachedSettings = {
70
70
  projects: settings?.projects || ['all'],
71
- commands: settings?.commands || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
71
+ commands: settings?.commands || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']
72
72
  };
73
73
  }
74
74
  else {
@@ -111,7 +111,10 @@ export function isCommandAllowed(commandName, projectKey) {
111
111
  return true;
112
112
  }
113
113
  if (projectKey) {
114
- const project = settings.projects.find(p => (typeof p === 'string' ? p : p.key) === projectKey);
114
+ let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
115
+ if (!project) {
116
+ project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
117
+ }
115
118
  if (project && typeof project !== 'string' && project.commands) {
116
119
  return project.commands.includes(commandName);
117
120
  }
@@ -166,11 +169,12 @@ export function applyGlobalFilters(jql) {
166
169
  export function validateIssueAgainstFilters(issue, currentUserId) {
167
170
  const settings = loadSettings();
168
171
  const projectKey = issue.key.split('-')[0];
169
- const project = settings.projects.find(p => {
170
- if (typeof p === 'string')
171
- return p === 'all' || p === projectKey;
172
- return p.key === projectKey;
173
- });
172
+ // Find specific project config first
173
+ let project = settings.projects.find(p => typeof p !== 'string' && p.key === projectKey);
174
+ // If not found, look for string match (exact project key or 'all')
175
+ if (!project) {
176
+ project = settings.projects.find(p => typeof p === 'string' && (p === 'all' || p === projectKey));
177
+ }
174
178
  if (!project) {
175
179
  return false;
176
180
  }
@@ -72,6 +72,9 @@ export const TimeframeSchema = z.string().regex(/^\d+d$/, 'Timeframe must be in
72
72
  export const GetPersonWorklogSchema = z.object({
73
73
  groupByIssue: z.boolean().optional(),
74
74
  });
75
+ export const GetIssueStatisticsSchema = z.object({
76
+ fullBreakdown: z.boolean().optional(),
77
+ });
75
78
  export const ProjectFiltersSchema = z.object({
76
79
  participated: z.object({
77
80
  was_assignee: z.boolean().optional(),
@@ -92,5 +95,5 @@ export const ProjectSettingSchema = z.union([
92
95
  ]);
93
96
  export const SettingsSchema = z.object({
94
97
  projects: z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
95
- commands: z.array(z.string()).nullish().transform(val => val || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label', 'delete-label', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']),
98
+ commands: z.array(z.string()).nullish().transform(val => val || ['me', 'projects', 'task-with-details', 'run-jql', 'list-issue-types', 'project-statuses', 'create-task', 'list-colleagues', 'add-comment', 'add-label-to-issue', 'delete-label-from-issue', 'get-issue-statistics', 'get-person-worklog', 'organization', 'transition', 'update-description']),
96
99
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.4.10",
3
+ "version": "0.4.13",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -52,6 +52,7 @@
52
52
  "cli-table3": "^0.6.5",
53
53
  "commander": "^11.0.0",
54
54
  "dotenv": "^17.2.3",
55
+ "html-entities": "^2.6.0",
55
56
  "inquirer": "^9.3.8",
56
57
  "jira.js": "^5.2.2",
57
58
  "js-yaml": "^4.1.1",