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 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.3.22')
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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,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