jira-ai 0.4.1 → 0.4.2
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 +14 -2
- package/dist/commands/get-person-worklog.js +61 -0
- package/dist/lib/formatters.js +30 -0
- package/dist/lib/jira-client.js +24 -0
- package/dist/lib/utils.js +23 -0
- package/dist/lib/validation.js +4 -0
- package/package.json +1 -1
- package/settings.yaml +2 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { deleteLabelCommand } from './commands/delete-label.js';
|
|
|
17
17
|
import { createTaskCommand } from './commands/create-task.js';
|
|
18
18
|
import { transitionCommand } from './commands/transition.js';
|
|
19
19
|
import { getIssueStatisticsCommand } from './commands/get-issue-statistics.js';
|
|
20
|
+
import { getPersonWorklogCommand } from './commands/get-person-worklog.js';
|
|
20
21
|
import { aboutCommand } from './commands/about.js';
|
|
21
22
|
import { authCommand } from './commands/auth.js';
|
|
22
23
|
import { listOrganizations, useOrganizationCommand, removeOrganizationCommand } from './commands/organization.js';
|
|
@@ -26,7 +27,7 @@ import { hasCredentials } from './lib/auth-storage.js';
|
|
|
26
27
|
import { CliError } from './types/errors.js';
|
|
27
28
|
import { CommandError } from './lib/errors.js';
|
|
28
29
|
import { ui } from './lib/ui.js';
|
|
29
|
-
import { validateOptions, CreateTaskSchema, AddCommentSchema, UpdateDescriptionSchema, RunJqlSchema, IssueKeySchema, ProjectKeySchema } from './lib/validation.js';
|
|
30
|
+
import { validateOptions, CreateTaskSchema, AddCommentSchema, UpdateDescriptionSchema, RunJqlSchema, GetPersonWorklogSchema, TimeframeSchema, IssueKeySchema, ProjectKeySchema } from './lib/validation.js';
|
|
30
31
|
import { realpathSync } from 'fs';
|
|
31
32
|
// Load environment variables
|
|
32
33
|
// @ts-ignore - quiet option exists but is not in types
|
|
@@ -36,7 +37,7 @@ const program = new Command();
|
|
|
36
37
|
program
|
|
37
38
|
.name('jira-ai')
|
|
38
39
|
.description('CLI tool for interacting with Atlassian Jira')
|
|
39
|
-
.version('0.
|
|
40
|
+
.version('0.4.2')
|
|
40
41
|
.option('-o, --organization <alias>', 'Override the active Jira organization');
|
|
41
42
|
// Hook to handle the global option before any command runs
|
|
42
43
|
program.on('option:organization', (alias) => {
|
|
@@ -222,6 +223,17 @@ program
|
|
|
222
223
|
.command('get-issue-statistics <task-ids>')
|
|
223
224
|
.description('Show time metrics and lifecycle of issues (comma-separated keys)')
|
|
224
225
|
.action(withPermission('get-issue-statistics', getIssueStatisticsCommand));
|
|
226
|
+
// Get person worklog command
|
|
227
|
+
program
|
|
228
|
+
.command('get-person-worklog <person> <timeframe>')
|
|
229
|
+
.description('Retrieve and display worklogs for a specific person within a given timeframe')
|
|
230
|
+
.option('--group-by-issue', 'Group the output by issue')
|
|
231
|
+
.action(withPermission('get-person-worklog', getPersonWorklogCommand, {
|
|
232
|
+
schema: GetPersonWorklogSchema,
|
|
233
|
+
validateArgs: (args) => {
|
|
234
|
+
validateOptions(TimeframeSchema, args[1]);
|
|
235
|
+
}
|
|
236
|
+
}));
|
|
225
237
|
// About command (always allowed)
|
|
226
238
|
program
|
|
227
239
|
.command('about')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ui } from '../lib/ui.js';
|
|
3
|
+
import { getIssueWorklogs, getJiraClient } from '../lib/jira-client.js';
|
|
4
|
+
import { parseTimeframe, formatDateForJql } from '../lib/utils.js';
|
|
5
|
+
import { formatWorklogs } from '../lib/formatters.js';
|
|
6
|
+
import { CommandError } from '../lib/errors.js';
|
|
7
|
+
export async function getPersonWorklogCommand(person, timeframe, options) {
|
|
8
|
+
ui.startSpinner(`Fetching worklogs for ${person}...`);
|
|
9
|
+
try {
|
|
10
|
+
const { startDate, endDate } = parseTimeframe(timeframe);
|
|
11
|
+
const startJql = formatDateForJql(startDate);
|
|
12
|
+
const endJql = formatDateForJql(endDate);
|
|
13
|
+
// 1. Search for issues where the person has tracked time in the timeframe
|
|
14
|
+
// We use a broader search first to find relevant issues
|
|
15
|
+
const jql = `worklogAuthor = "${person}" AND worklogDate >= "${startJql}" AND worklogDate <= "${endJql}"`;
|
|
16
|
+
// We need to fetch issues with their summaries
|
|
17
|
+
const client = getJiraClient();
|
|
18
|
+
const issueResponse = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
|
|
19
|
+
jql,
|
|
20
|
+
fields: ['summary'],
|
|
21
|
+
maxResults: 100,
|
|
22
|
+
});
|
|
23
|
+
const issues = issueResponse.issues || [];
|
|
24
|
+
if (issues.length === 0) {
|
|
25
|
+
ui.stopSpinner();
|
|
26
|
+
console.log(chalk.yellow(`
|
|
27
|
+
No worklogs found for ${person} between ${startJql} and ${endJql}.
|
|
28
|
+
`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const allWorklogs = [];
|
|
32
|
+
// 2. For each issue, fetch all worklogs and filter in-memory
|
|
33
|
+
for (const issue of issues) {
|
|
34
|
+
const worklogs = await getIssueWorklogs(issue.key);
|
|
35
|
+
const filteredWorklogs = worklogs.filter(w => {
|
|
36
|
+
const matchesPerson = w.author.accountId === person || w.author.emailAddress === person;
|
|
37
|
+
const worklogDate = new Date(w.started);
|
|
38
|
+
const matchesDate = worklogDate >= startDate && worklogDate <= endDate;
|
|
39
|
+
return matchesPerson && matchesDate;
|
|
40
|
+
});
|
|
41
|
+
filteredWorklogs.forEach(w => {
|
|
42
|
+
allWorklogs.push({
|
|
43
|
+
...w,
|
|
44
|
+
summary: issue.fields?.summary || '',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
ui.stopSpinner();
|
|
49
|
+
if (allWorklogs.length === 0) {
|
|
50
|
+
console.log(chalk.yellow(`
|
|
51
|
+
No worklogs found for ${person} after detailed filtering.
|
|
52
|
+
`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(formatWorklogs(allWorklogs, options.groupByIssue));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
ui.failSpinner(`Failed to fetch worklogs: ${error.message}`);
|
|
59
|
+
throw new CommandError(error.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/lib/formatters.js
CHANGED
|
@@ -330,3 +330,33 @@ export function formatUsers(users) {
|
|
|
330
330
|
output += table.toString() + '\n';
|
|
331
331
|
return output;
|
|
332
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Format worklogs list
|
|
335
|
+
*/
|
|
336
|
+
export function formatWorklogs(worklogs, groupByIssue = false) {
|
|
337
|
+
if (worklogs.length === 0) {
|
|
338
|
+
return chalk.yellow('\nNo worklogs found for the given person and timeframe.\n');
|
|
339
|
+
}
|
|
340
|
+
// Sort worklogs by date (started)
|
|
341
|
+
let sortedWorklogs = [...worklogs].sort((a, b) => new Date(a.started).getTime() - new Date(b.started).getTime());
|
|
342
|
+
if (groupByIssue) {
|
|
343
|
+
sortedWorklogs.sort((a, b) => a.issueKey.localeCompare(b.issueKey));
|
|
344
|
+
}
|
|
345
|
+
const table = createTable(['Date', 'Issue Key', 'Summary', 'Time Spent', 'Worklog Comment'], [12, 12, 30, 12, 45]);
|
|
346
|
+
let totalSeconds = 0;
|
|
347
|
+
sortedWorklogs.forEach((w) => {
|
|
348
|
+
totalSeconds += w.timeSpentSeconds;
|
|
349
|
+
table.push([
|
|
350
|
+
w.started.split('T')[0],
|
|
351
|
+
chalk.cyan(w.issueKey),
|
|
352
|
+
truncate(w.summary, 30),
|
|
353
|
+
w.timeSpent,
|
|
354
|
+
truncate(w.comment || '', 45),
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
357
|
+
const totalHours = (totalSeconds / 3600).toFixed(2);
|
|
358
|
+
let output = '\n' + chalk.bold('Worklogs') + '\n\n';
|
|
359
|
+
output += table.toString() + '\n';
|
|
360
|
+
output += chalk.bold(`Total time tracked: ${totalHours}h`) + '\n';
|
|
361
|
+
return output;
|
|
362
|
+
}
|
package/dist/lib/jira-client.js
CHANGED
|
@@ -454,3 +454,27 @@ export async function getUsers(projectKey) {
|
|
|
454
454
|
host: client.config.host || 'N/A',
|
|
455
455
|
}));
|
|
456
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Get all worklogs for an issue
|
|
459
|
+
*/
|
|
460
|
+
export async function getIssueWorklogs(issueIdOrKey) {
|
|
461
|
+
const client = getJiraClient();
|
|
462
|
+
const response = await client.issueWorklogs.getIssueWorklog({
|
|
463
|
+
issueIdOrKey,
|
|
464
|
+
});
|
|
465
|
+
return (response.worklogs || []).map((w) => ({
|
|
466
|
+
id: w.id || '',
|
|
467
|
+
author: {
|
|
468
|
+
accountId: w.author?.accountId || '',
|
|
469
|
+
displayName: w.author?.displayName || 'Unknown',
|
|
470
|
+
emailAddress: w.author?.emailAddress,
|
|
471
|
+
},
|
|
472
|
+
comment: convertADFToMarkdown(w.comment),
|
|
473
|
+
created: w.created || '',
|
|
474
|
+
updated: w.updated || '',
|
|
475
|
+
started: w.started || '',
|
|
476
|
+
timeSpent: w.timeSpent || '',
|
|
477
|
+
timeSpentSeconds: w.timeSpentSeconds || 0,
|
|
478
|
+
issueKey: issueIdOrKey,
|
|
479
|
+
}));
|
|
480
|
+
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -128,3 +128,26 @@ export function formatDuration(seconds, hoursPerDay = 24) {
|
|
|
128
128
|
parts.push(`${m}m`);
|
|
129
129
|
return parts.length > 0 ? parts.join(' ') : '0m';
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse relative timeframe (e.g., '7d', '30d') into start and end dates
|
|
133
|
+
*/
|
|
134
|
+
export function parseTimeframe(timeframe) {
|
|
135
|
+
const match = timeframe.match(/^(\d+)d$/);
|
|
136
|
+
if (!match) {
|
|
137
|
+
throw new CommandError('Invalid timeframe format. Use e.g., "7d" or "30d".');
|
|
138
|
+
}
|
|
139
|
+
const days = parseInt(match[1], 10);
|
|
140
|
+
const endDate = new Date();
|
|
141
|
+
const startDate = new Date();
|
|
142
|
+
startDate.setDate(endDate.getDate() - days);
|
|
143
|
+
// Set to start of day for startDate and end of day for endDate to be inclusive
|
|
144
|
+
startDate.setHours(0, 0, 0, 0);
|
|
145
|
+
endDate.setHours(23, 59, 59, 999);
|
|
146
|
+
return { startDate, endDate };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Format Date object to JQL compatible string (YYYY-MM-DD)
|
|
150
|
+
*/
|
|
151
|
+
export function formatDateForJql(date) {
|
|
152
|
+
return date.toISOString().split('T')[0];
|
|
153
|
+
}
|
package/dist/lib/validation.js
CHANGED
|
@@ -68,3 +68,7 @@ export const UpdateDescriptionSchema = z.object({
|
|
|
68
68
|
export const RunJqlSchema = z.object({
|
|
69
69
|
limit: NumericStringSchema.optional(),
|
|
70
70
|
});
|
|
71
|
+
export const TimeframeSchema = z.string().regex(/^\d+d$/, 'Timeframe must be in format like "7d" or "30d"');
|
|
72
|
+
export const GetPersonWorklogSchema = z.object({
|
|
73
|
+
groupByIssue: z.boolean().optional(),
|
|
74
|
+
});
|
package/package.json
CHANGED
package/settings.yaml
CHANGED
|
@@ -11,10 +11,11 @@ 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, get-issue-statistics, about, transition, add-label-to-issue, delete-label-from-issue
|
|
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, transition, add-label-to-issue, delete-label-from-issue, list-colleagues, get-person-worklog
|
|
15
15
|
commands:
|
|
16
16
|
- me
|
|
17
17
|
- get-issue-statistics
|
|
18
|
+
- get-person-worklog
|
|
18
19
|
# Uncomment below to allow all commands
|
|
19
20
|
# - all
|
|
20
21
|
# - task-with-details
|