jira-ai 0.3.12 → 0.3.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
@@ -14,6 +14,7 @@ import { addCommentCommand } from './commands/add-comment.js';
14
14
  import { addLabelCommand } from './commands/add-label.js';
15
15
  import { deleteLabelCommand } from './commands/delete-label.js';
16
16
  import { createTaskCommand } from './commands/create-task.js';
17
+ import { getIssueStatisticsCommand } from './commands/get-issue-statistics.js';
17
18
  import { aboutCommand } from './commands/about.js';
18
19
  import { authCommand } from './commands/auth.js';
19
20
  import { isCommandAllowed, getAllowedCommands } from './lib/settings.js';
@@ -161,6 +162,11 @@ program
161
162
  .requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
162
163
  .option('--parent <key>', 'Parent issue key (required for subtasks)')
163
164
  .action(withPermission('create-task', createTaskCommand, { schema: CreateTaskSchema }));
165
+ // Get issue statistics command
166
+ program
167
+ .command('get-issue-statistics <task-ids>')
168
+ .description('Show time metrics and lifecycle of issues (comma-separated keys)')
169
+ .action(withPermission('get-issue-statistics', getIssueStatisticsCommand));
164
170
  // About command (always allowed)
165
171
  program
166
172
  .command('about')
@@ -0,0 +1,29 @@
1
+ import chalk from 'chalk';
2
+ import { getIssueStatistics } from '../lib/jira-client.js';
3
+ import { formatIssueStatistics } from '../lib/formatters.js';
4
+ import { ui } from '../lib/ui.js';
5
+ export async function getIssueStatisticsCommand(taskIds) {
6
+ const ids = taskIds.split(',').map(id => id.trim()).filter(id => id !== '');
7
+ if (ids.length === 0) {
8
+ console.error(chalk.red('Please provide at least one issue ID.'));
9
+ return;
10
+ }
11
+ ui.startSpinner(`Fetching statistics for ${ids.length} issue(s)...`);
12
+ const results = [];
13
+ for (const id of ids) {
14
+ try {
15
+ const stats = await getIssueStatistics(id);
16
+ results.push(stats);
17
+ }
18
+ catch (error) {
19
+ console.error(chalk.red(`\nFailed to fetch statistics for ${id}: ${error.message}`));
20
+ }
21
+ }
22
+ if (results.length > 0) {
23
+ ui.succeedSpinner(chalk.green('Statistics retrieved'));
24
+ console.log(formatIssueStatistics(results));
25
+ }
26
+ else {
27
+ ui.failSpinner('Failed to retrieve statistics');
28
+ }
29
+ }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
- import { formatTimestamp, truncate } from './utils.js';
3
+ import { formatTimestamp, truncate, formatDuration } from './utils.js';
4
4
  /**
5
5
  * Create a styled table
6
6
  */
@@ -231,3 +231,40 @@ export function formatProjectIssueTypes(projectKey, issueTypes) {
231
231
  }
232
232
  return output;
233
233
  }
234
+ /**
235
+ * Format issue statistics
236
+ */
237
+ export function formatIssueStatistics(statsList) {
238
+ if (statsList.length === 0) {
239
+ return chalk.yellow('No statistics to display.');
240
+ }
241
+ const table = createTable([
242
+ 'Key',
243
+ 'Summary',
244
+ 'Time Logged',
245
+ 'Estimate',
246
+ 'Status Breakdown'
247
+ ], [12, 30, 15, 15, 40]);
248
+ statsList.forEach((stats) => {
249
+ // Breakdown of time spent in each unique status
250
+ const breakdown = Object.entries(stats.statusDurations)
251
+ .map(([status, seconds]) => `${status}: ${formatDuration(seconds, 24)}`)
252
+ .join('\n');
253
+ const timeSpentStr = formatDuration(stats.timeSpentSeconds, 8);
254
+ const estimateStr = formatDuration(stats.originalEstimateSeconds, 8);
255
+ // Highlight if time spent exceeds estimate
256
+ const timeSpentFormatted = stats.timeSpentSeconds > stats.originalEstimateSeconds && stats.originalEstimateSeconds > 0
257
+ ? chalk.red(timeSpentStr)
258
+ : chalk.green(timeSpentStr);
259
+ table.push([
260
+ chalk.cyan(stats.key),
261
+ truncate(stats.summary, 30),
262
+ timeSpentFormatted,
263
+ estimateStr,
264
+ breakdown
265
+ ]);
266
+ });
267
+ let output = '\n' + chalk.bold('Issue Statistics') + '\n\n';
268
+ output += table.toString() + '\n';
269
+ return output;
270
+ }
@@ -1,5 +1,5 @@
1
1
  import { Version3Client } from 'jira.js';
2
- import { convertADFToMarkdown } from './utils.js';
2
+ import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
3
3
  import { loadCredentials } from './auth-storage.js';
4
4
  let jiraClient = null;
5
5
  /**
@@ -324,3 +324,25 @@ export async function removeIssueLabels(taskId, labels) {
324
324
  },
325
325
  });
326
326
  }
327
+ /**
328
+ * Get issue statistics including status transitions and time tracking
329
+ */
330
+ export async function getIssueStatistics(issueIdOrKey) {
331
+ const client = getJiraClient();
332
+ const issue = await client.issues.getIssue({
333
+ issueIdOrKey,
334
+ expand: 'changelog',
335
+ fields: ['summary', 'status', 'timetracking', 'created'],
336
+ });
337
+ const histories = issue.changelog?.histories || [];
338
+ const statusName = issue.fields.status?.name || 'Unknown';
339
+ const statusDurations = calculateStatusStatistics(issue.fields.created, histories, statusName);
340
+ return {
341
+ key: issue.key || '',
342
+ summary: issue.fields.summary || '',
343
+ timeSpentSeconds: issue.fields.timetracking?.timeSpentSeconds || 0,
344
+ originalEstimateSeconds: issue.fields.timetracking?.originalEstimateSeconds || 0,
345
+ statusDurations,
346
+ currentStatus: statusName,
347
+ };
348
+ }
package/dist/lib/utils.js CHANGED
@@ -67,3 +67,64 @@ export function convertADFToMarkdown(content) {
67
67
  return JSON.stringify(content, null, 2);
68
68
  }
69
69
  }
70
+ /**
71
+ * Calculate time spent in each status
72
+ */
73
+ export function calculateStatusStatistics(created, histories, currentStatus, now = Date.now()) {
74
+ const stats = {};
75
+ // Sort histories by date
76
+ const sortedHistories = [...histories].sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
77
+ let lastTransitionTime = new Date(created).getTime();
78
+ let lastStatus = '';
79
+ const statusHistories = sortedHistories.filter(h => h.items.some((item) => item.field === 'status'));
80
+ if (statusHistories.length > 0) {
81
+ const firstStatusItem = statusHistories[0].items.find((item) => item.field === 'status');
82
+ lastStatus = firstStatusItem.fromString;
83
+ }
84
+ else {
85
+ lastStatus = currentStatus;
86
+ }
87
+ for (const history of statusHistories) {
88
+ const statusItem = history.items.find((item) => item.field === 'status');
89
+ if (!statusItem)
90
+ continue;
91
+ const transitionTime = new Date(history.created).getTime();
92
+ const durationSeconds = Math.max(0, Math.floor((transitionTime - lastTransitionTime) / 1000));
93
+ stats[lastStatus] = (stats[lastStatus] || 0) + durationSeconds;
94
+ lastStatus = statusItem.toString;
95
+ lastTransitionTime = transitionTime;
96
+ }
97
+ // Add time for the current status
98
+ const finalDurationSeconds = Math.max(0, Math.floor((now - lastTransitionTime) / 1000));
99
+ stats[lastStatus] = (stats[lastStatus] || 0) + finalDurationSeconds;
100
+ return stats;
101
+ }
102
+ /**
103
+ * Format duration in seconds to Jira human-readable format
104
+ * @param seconds - Duration in seconds
105
+ * @param hoursPerDay - Hours in a working day (default 24 for brutto time)
106
+ */
107
+ export function formatDuration(seconds, hoursPerDay = 24) {
108
+ if (seconds <= 0)
109
+ return '0m';
110
+ const daysPerWeek = hoursPerDay === 24 ? 7 : 5;
111
+ const secondsInDay = hoursPerDay * 3600;
112
+ const secondsInWeek = daysPerWeek * secondsInDay;
113
+ const w = Math.floor(seconds / secondsInWeek);
114
+ seconds %= secondsInWeek;
115
+ const d = Math.floor(seconds / secondsInDay);
116
+ seconds %= secondsInDay;
117
+ const h = Math.floor(seconds / 3600);
118
+ seconds %= 3600;
119
+ const m = Math.floor(seconds / 60);
120
+ const parts = [];
121
+ if (w > 0)
122
+ parts.push(`${w}w`);
123
+ if (d > 0)
124
+ parts.push(`${d}d`);
125
+ if (h > 0)
126
+ parts.push(`${h}h`);
127
+ if (m > 0)
128
+ parts.push(`${m}m`);
129
+ return parts.length > 0 ? parts.join(' ') : '0m';
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
package/settings.yaml CHANGED
@@ -11,9 +11,10 @@ projects:
11
11
  # - all
12
12
 
13
13
  # Commands: List of allowed commands (use "all" to allow all commands)
14
- # Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, create-task, about
14
+ # Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, create-task, get-issue-statistics, about
15
15
  commands:
16
16
  - me
17
+ - get-issue-statistics
17
18
  # Uncomment below to allow all commands
18
19
  # - all
19
20
  # - task-with-details