jira-ai 1.4.0 → 1.6.0
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/README.md +130 -0
- package/dist/cli.js +120 -0
- package/dist/commands/issue-activity.js +38 -0
- package/dist/commands/issue-comments.js +29 -0
- package/dist/commands/issue-worklog.js +162 -0
- package/dist/lib/jira-client.js +315 -0
- package/dist/lib/utils.js +39 -0
- package/dist/lib/validation.js +54 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -49,6 +49,34 @@ Errors are returned as structured JSON to stdout:
|
|
|
49
49
|
{ "error": true, "message": "Issue not found", "hints": ["Check the issue key"], "exitCode": 1 }
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Dry-Run / Preview Mode
|
|
53
|
+
|
|
54
|
+
Preview write operations without executing them. The `--dry-run` flag is available on `issue create`, `issue update`, `issue transition`, and `issue worklog add/update/delete`. No Jira API write calls are made — output is purely a preview.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
jira-ai issue update PROJ-123 --priority High --dry-run
|
|
58
|
+
jira-ai issue transition PROJ-123 Done --resolution Fixed --dry-run
|
|
59
|
+
jira-ai issue create --project PROJ --type Bug --title "Fix crash" --dry-run
|
|
60
|
+
jira-ai issue worklog add PROJ-123 --time 2h --comment "Debugging" --dry-run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Dry-run output follows a consistent JSON structure:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"dryRun": true,
|
|
68
|
+
"command": "issue.update",
|
|
69
|
+
"target": "PROJ-123",
|
|
70
|
+
"changes": {
|
|
71
|
+
"priority": { "from": "Medium", "to": "High" }
|
|
72
|
+
},
|
|
73
|
+
"preview": { "...": "same output as the real command" },
|
|
74
|
+
"message": "No changes were made. Remove --dry-run to execute."
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `preview` field contains the same output the real command would produce, so AI agents can process it identically. Phase 1 supports `issue create`, `issue update`, and `issue transition`. Worklog dry-run: `issue worklog add`, `issue worklog update`, `issue worklog delete`.
|
|
79
|
+
|
|
52
80
|
### Issue Hierarchy Tree
|
|
53
81
|
|
|
54
82
|
Explore issue hierarchies with the `issue tree` command. It returns a directed graph (nodes + edges) representing the full parent-child hierarchy starting from a given issue:
|
|
@@ -147,6 +175,108 @@ jira-ai issue transitions PROJ-123
|
|
|
147
175
|
|
|
148
176
|
When a transition fails due to missing required fields, the error message lists what is needed and suggests running `issue transitions <key>` to discover them.
|
|
149
177
|
|
|
178
|
+
### Activity Feed & Comments
|
|
179
|
+
|
|
180
|
+
View a unified activity feed combining changelog entries and comments for an issue:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
jira-ai issue activity PROJ-123
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Filter by time, activity type, or author:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
jira-ai issue activity PROJ-123 --since 2026-01-01T00:00:00Z --types status_change,comment_added --author "Jane Smith"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `--compact` to strip comment bodies for maximum token efficiency:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
jira-ai issue activity PROJ-123 --compact
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
List comments on an issue:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
jira-ai issue comments PROJ-123
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Use `--reverse` for chronological (oldest-first) order, or `--since` to filter by time:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
jira-ai issue comments PROJ-123 --since 2026-01-01T00:00:00Z --reverse --limit 20
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Activity types:** `status_change`, `field_change`, `comment_added`, `comment_updated`, `attachment_added`, `attachment_removed`, `link_added`, `link_removed`
|
|
211
|
+
|
|
212
|
+
### Worklog Management
|
|
213
|
+
|
|
214
|
+
Log time against issues with full CRUD support:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
jira-ai issue worklog add PROJ-123 --time 2h
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Add a comment and specify when the work started:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
jira-ai issue worklog add PROJ-123 --time 1d2h30m --comment "Backend refactor" --started "2026-04-15T09:00:00+02:00"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The `--started` flag accepts any standard ISO 8601 timestamp. Timezone offsets are automatically normalized to Jira's required format (`yyyy-MM-dd'T'HH:mm:ss.SSS±HHMM`):
|
|
227
|
+
|
|
228
|
+
- `2026-04-15T07:00:00Z` → `2026-04-15T07:00:00.000+0000`
|
|
229
|
+
- `2026-04-15T10:00:00+03:00` → `2026-04-15T10:00:00.000+0300`
|
|
230
|
+
- `2026-04-15T07:00:00-05:30` → `2026-04-15T07:00:00.000-0530`
|
|
231
|
+
|
|
232
|
+
When omitted, `--started` defaults to the current time.
|
|
233
|
+
|
|
234
|
+
Log time with estimate adjustment (`--adjust-estimate` accepts `auto`, `new`, `leave`, or `manual`):
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
jira-ai issue worklog add PROJ-123 --time 4h --adjust-estimate new --new-estimate 2d
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Use `--reduce-by` with `--adjust-estimate manual` to decrease the remaining estimate:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
jira-ai issue worklog add PROJ-123 --time 3h --adjust-estimate manual --reduce-by 2h
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
List worklogs for an issue:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
jira-ai issue worklog list PROJ-123
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Filter by time range or author:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
jira-ai issue worklog list PROJ-123 --started-after 1713139200000 --started-before 1715731200000
|
|
256
|
+
jira-ai issue worklog list PROJ-123 --author-account-id 557058:abc123-def456-ghi789
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Update an existing worklog:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
jira-ai issue worklog update PROJ-123 --id 12345 --time 3h --comment "Updated after review"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Delete a worklog (use `--increase-by` with `--adjust-estimate manual` to restore estimate):
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
jira-ai issue worklog delete PROJ-123 --id 12345
|
|
269
|
+
jira-ai issue worklog delete PROJ-123 --id 12345 --adjust-estimate manual --increase-by 2h
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Preview any write operation with `--dry-run`:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
jira-ai issue worklog add PROJ-123 --time 2h --dry-run
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Duration format uses Jira-style notation: `1w` (5 working days), `1d` (8 hours), `1h`, `30m`, or combinations like `1d2h30m`.
|
|
279
|
+
|
|
150
280
|
## Service Account Authentication
|
|
151
281
|
|
|
152
282
|
Atlassian service accounts use scoped API tokens that must authenticate through the `api.atlassian.com` gateway rather than direct site URLs.
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,9 @@ import { epicListCommand, epicGetCommand, epicCreateCommand, epicUpdateCommand,
|
|
|
31
31
|
import { boardListCommand, boardGetCommand, boardConfigCommand, boardIssuesCommand, boardRankCommand, } from './commands/board.js';
|
|
32
32
|
import { sprintListCommand, sprintGetCommand, sprintCreateCommand, sprintStartCommand, sprintCompleteCommand, sprintUpdateCommand, sprintDeleteCommand, sprintIssuesCommand, sprintMoveCommand, } from './commands/sprint.js';
|
|
33
33
|
import { backlogMoveCommand } from './commands/backlog.js';
|
|
34
|
+
import { issueCommentsCommand } from './commands/issue-comments.js';
|
|
35
|
+
import { issueActivityCommand } from './commands/issue-activity.js';
|
|
36
|
+
import { issueWorklogListCommand, issueWorklogAddCommand, issueWorklogUpdateCommand, issueWorklogDeleteCommand, } from './commands/issue-worklog.js';
|
|
34
37
|
import { aboutCommand } from './commands/about.js';
|
|
35
38
|
import { authCommand } from './commands/auth.js';
|
|
36
39
|
import { settingsCommand } from './commands/settings.js';
|
|
@@ -208,6 +211,123 @@ issue
|
|
|
208
211
|
schema: UpdateDescriptionSchema,
|
|
209
212
|
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
210
213
|
}));
|
|
214
|
+
issue
|
|
215
|
+
.command('comments <issue-id>')
|
|
216
|
+
.description('List comments on a Jira issue.')
|
|
217
|
+
.option('--limit <n>', 'Maximum number of comments to return (default: 50)')
|
|
218
|
+
.option('--since <iso>', 'Only include comments created on or after this ISO timestamp')
|
|
219
|
+
.option('--reverse', 'Return comments in chronological order (oldest first)')
|
|
220
|
+
.action(withPermission('issue.comments', (issueKey, options) => {
|
|
221
|
+
return issueCommentsCommand({
|
|
222
|
+
issueKey,
|
|
223
|
+
limit: options.limit ? parseInt(options.limit, 10) : undefined,
|
|
224
|
+
since: options.since,
|
|
225
|
+
reverse: options.reverse,
|
|
226
|
+
});
|
|
227
|
+
}, {
|
|
228
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
229
|
+
}));
|
|
230
|
+
issue
|
|
231
|
+
.command('activity <issue-id>')
|
|
232
|
+
.description('Show a unified activity feed (changelog + comments) for a Jira issue.')
|
|
233
|
+
.option('--since <iso>', 'Only include activities on or after this ISO timestamp')
|
|
234
|
+
.option('--limit <n>', 'Maximum number of activities to return (default: 50)')
|
|
235
|
+
.option('--types <types>', 'Comma-separated activity types to include (e.g., status_change,comment_added)')
|
|
236
|
+
.option('--author <name-or-email>', 'Filter by author display name, email, or accountId')
|
|
237
|
+
.action(withPermission('issue.activity', (issueKey, options) => {
|
|
238
|
+
return issueActivityCommand({
|
|
239
|
+
issueKey,
|
|
240
|
+
since: options.since,
|
|
241
|
+
limit: options.limit ? parseInt(options.limit, 10) : undefined,
|
|
242
|
+
types: options.types,
|
|
243
|
+
author: options.author,
|
|
244
|
+
compact: program.opts().compact || options.compact,
|
|
245
|
+
});
|
|
246
|
+
}, {
|
|
247
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
248
|
+
}));
|
|
249
|
+
// Issue worklog subcommands
|
|
250
|
+
const worklog = issue
|
|
251
|
+
.command('worklog')
|
|
252
|
+
.description('Manage worklogs for a Jira issue');
|
|
253
|
+
worklog
|
|
254
|
+
.command('list <issue-id>')
|
|
255
|
+
.description('List all worklogs for a Jira issue.')
|
|
256
|
+
.option('--started-after <timestamp>', 'Only return worklogs started at or after this UNIX timestamp (ms)')
|
|
257
|
+
.option('--started-before <timestamp>', 'Only return worklogs started before this UNIX timestamp (ms)')
|
|
258
|
+
.option('--author-account-id <accountId>', 'Filter by author account ID')
|
|
259
|
+
.action(withPermission('issue.worklog.list', (issueKey, options) => {
|
|
260
|
+
return issueWorklogListCommand({
|
|
261
|
+
issueKey,
|
|
262
|
+
startedAfter: options.startedAfter ? parseInt(options.startedAfter, 10) : undefined,
|
|
263
|
+
startedBefore: options.startedBefore ? parseInt(options.startedBefore, 10) : undefined,
|
|
264
|
+
authorAccountId: options.authorAccountId,
|
|
265
|
+
});
|
|
266
|
+
}, {
|
|
267
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
268
|
+
}));
|
|
269
|
+
worklog
|
|
270
|
+
.command('add <issue-id>')
|
|
271
|
+
.description('Log time against a Jira issue.')
|
|
272
|
+
.requiredOption('--time <duration>', 'Time to log (e.g. 1h, 30m, 1d2h30m, 1w)')
|
|
273
|
+
.option('--comment <text>', 'Optional comment for this worklog entry')
|
|
274
|
+
.option('--started <datetime>', 'When the work started (ISO 8601, defaults to now). Timezone offsets are auto-normalized.')
|
|
275
|
+
.option('--adjust-estimate <method>', 'Estimate adjustment: auto, new, leave, manual')
|
|
276
|
+
.option('--new-estimate <duration>', 'New remaining estimate (use with --adjust-estimate new or manual)')
|
|
277
|
+
.option('--reduce-by <duration>', 'Reduce remaining estimate by this amount (use with --adjust-estimate manual)')
|
|
278
|
+
.action(withPermission('issue.worklog.add', (issueKey, options) => {
|
|
279
|
+
return issueWorklogAddCommand({
|
|
280
|
+
issueKey,
|
|
281
|
+
time: options.time,
|
|
282
|
+
comment: options.comment,
|
|
283
|
+
started: options.started,
|
|
284
|
+
adjustEstimate: options.adjustEstimate,
|
|
285
|
+
newEstimate: options.newEstimate,
|
|
286
|
+
reduceBy: options.reduceBy,
|
|
287
|
+
});
|
|
288
|
+
}, {
|
|
289
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
290
|
+
}));
|
|
291
|
+
worklog
|
|
292
|
+
.command('update <issue-id>')
|
|
293
|
+
.description('Update an existing worklog entry.')
|
|
294
|
+
.requiredOption('--id <worklog-id>', 'ID of the worklog to update')
|
|
295
|
+
.option('--time <duration>', 'New time spent (e.g. 1h, 30m, 1d)')
|
|
296
|
+
.option('--comment <text>', 'New comment for this worklog')
|
|
297
|
+
.option('--started <datetime>', 'New start time (ISO 8601). Timezone offsets are auto-normalized.')
|
|
298
|
+
.option('--adjust-estimate <method>', 'Estimate adjustment: auto, new, leave, manual')
|
|
299
|
+
.option('--new-estimate <duration>', 'New remaining estimate')
|
|
300
|
+
.action(withPermission('issue.worklog.update', (issueKey, options) => {
|
|
301
|
+
return issueWorklogUpdateCommand({
|
|
302
|
+
issueKey,
|
|
303
|
+
id: options.id,
|
|
304
|
+
time: options.time,
|
|
305
|
+
comment: options.comment,
|
|
306
|
+
started: options.started,
|
|
307
|
+
adjustEstimate: options.adjustEstimate,
|
|
308
|
+
newEstimate: options.newEstimate,
|
|
309
|
+
});
|
|
310
|
+
}, {
|
|
311
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
312
|
+
}));
|
|
313
|
+
worklog
|
|
314
|
+
.command('delete <issue-id>')
|
|
315
|
+
.description('Delete a worklog entry from a Jira issue.')
|
|
316
|
+
.requiredOption('--id <worklog-id>', 'ID of the worklog to delete')
|
|
317
|
+
.option('--adjust-estimate <method>', 'Estimate adjustment: auto, new, leave, manual')
|
|
318
|
+
.option('--new-estimate <duration>', 'New remaining estimate (use with --adjust-estimate new)')
|
|
319
|
+
.option('--increase-by <duration>', 'Increase remaining estimate by this amount')
|
|
320
|
+
.action(withPermission('issue.worklog.delete', (issueKey, options) => {
|
|
321
|
+
return issueWorklogDeleteCommand({
|
|
322
|
+
issueKey,
|
|
323
|
+
id: options.id,
|
|
324
|
+
adjustEstimate: options.adjustEstimate,
|
|
325
|
+
newEstimate: options.newEstimate,
|
|
326
|
+
increaseBy: options.increaseBy,
|
|
327
|
+
});
|
|
328
|
+
}, {
|
|
329
|
+
validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
|
|
330
|
+
}));
|
|
211
331
|
issue
|
|
212
332
|
.command('stats <issue-ids>')
|
|
213
333
|
.description('Calculate time-based metrics for one or more issues (comma-separated). Shows time logged, estimates, and status breakdown.')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getIssueActivityFeed } from '../lib/jira-client.js';
|
|
2
|
+
import { CommandError } from '../lib/errors.js';
|
|
3
|
+
import { outputResult } from '../lib/json-mode.js';
|
|
4
|
+
export async function issueActivityCommand(options) {
|
|
5
|
+
const { issueKey, since, limit, types, author, compact } = options;
|
|
6
|
+
if (since !== undefined && isNaN(new Date(since).getTime())) {
|
|
7
|
+
throw new CommandError('--since must be a valid ISO 8601 datetime (e.g. 2024-01-01T00:00:00Z)');
|
|
8
|
+
}
|
|
9
|
+
if (limit !== undefined && (limit < 1 || !Number.isInteger(limit))) {
|
|
10
|
+
throw new CommandError('--limit must be a positive integer (>= 1)');
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const result = await getIssueActivityFeed(issueKey, { since, limit, types, author });
|
|
14
|
+
if (compact) {
|
|
15
|
+
const compactResult = {
|
|
16
|
+
...result,
|
|
17
|
+
activities: result.activities.map(({ commentBody: _omit, ...rest }) => rest),
|
|
18
|
+
};
|
|
19
|
+
outputResult(compactResult);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
outputResult(result);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof CommandError)
|
|
27
|
+
throw error;
|
|
28
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
29
|
+
const hints = [];
|
|
30
|
+
if (errorMsg.includes('404')) {
|
|
31
|
+
hints.push('Check that the issue key is correct');
|
|
32
|
+
}
|
|
33
|
+
else if (errorMsg.includes('403')) {
|
|
34
|
+
hints.push('You may not have permission to view activity on this issue');
|
|
35
|
+
}
|
|
36
|
+
throw new CommandError(`Failed to get activity feed: ${error.message}`, { hints });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getIssueCommentsList } from '../lib/jira-client.js';
|
|
2
|
+
import { CommandError } from '../lib/errors.js';
|
|
3
|
+
import { outputResult } from '../lib/json-mode.js';
|
|
4
|
+
export async function issueCommentsCommand(options) {
|
|
5
|
+
const { issueKey, limit, since, reverse } = options;
|
|
6
|
+
if (since !== undefined && isNaN(new Date(since).getTime())) {
|
|
7
|
+
throw new CommandError('--since must be a valid ISO 8601 datetime (e.g. 2024-01-01T00:00:00Z)');
|
|
8
|
+
}
|
|
9
|
+
if (limit !== undefined && (limit < 1 || !Number.isInteger(limit))) {
|
|
10
|
+
throw new CommandError('--limit must be a positive integer (>= 1)');
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const result = await getIssueCommentsList(issueKey, { limit, since, reverse });
|
|
14
|
+
outputResult(result);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof CommandError)
|
|
18
|
+
throw error;
|
|
19
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
20
|
+
const hints = [];
|
|
21
|
+
if (errorMsg.includes('404')) {
|
|
22
|
+
hints.push('Check that the issue key is correct');
|
|
23
|
+
}
|
|
24
|
+
else if (errorMsg.includes('403')) {
|
|
25
|
+
hints.push('You may not have permission to view comments on this issue');
|
|
26
|
+
}
|
|
27
|
+
throw new CommandError(`Failed to get comments: ${error.message}`, { hints });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { getIssueWorklogsList, addWorklogEntry, updateWorklogEntry, deleteWorklogEntry, } from '../lib/jira-client.js';
|
|
2
|
+
import { CommandError } from '../lib/errors.js';
|
|
3
|
+
import { outputResult } from '../lib/json-mode.js';
|
|
4
|
+
import { isDryRun, formatDryRunResult } from '../lib/dry-run.js';
|
|
5
|
+
import { parseDuration, normalizeJiraTimestamp } from '../lib/utils.js';
|
|
6
|
+
export async function issueWorklogListCommand(options) {
|
|
7
|
+
const { issueKey, startedAfter, startedBefore, authorAccountId } = options;
|
|
8
|
+
const filterOptions = {};
|
|
9
|
+
if (startedAfter !== undefined)
|
|
10
|
+
filterOptions.startedAfter = startedAfter;
|
|
11
|
+
if (startedBefore !== undefined)
|
|
12
|
+
filterOptions.startedBefore = startedBefore;
|
|
13
|
+
if (authorAccountId !== undefined)
|
|
14
|
+
filterOptions.authorAccountId = authorAccountId;
|
|
15
|
+
try {
|
|
16
|
+
const result = await getIssueWorklogsList(issueKey, filterOptions);
|
|
17
|
+
outputResult(result);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof CommandError)
|
|
21
|
+
throw error;
|
|
22
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
23
|
+
const hints = [];
|
|
24
|
+
if (errorMsg.includes('404')) {
|
|
25
|
+
hints.push('Check that the issue key is correct');
|
|
26
|
+
}
|
|
27
|
+
else if (errorMsg.includes('403')) {
|
|
28
|
+
hints.push('You may not have permission to view worklogs on this issue');
|
|
29
|
+
}
|
|
30
|
+
throw new CommandError(`Failed to list worklogs: ${error.message}`, { hints });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function issueWorklogAddCommand(options) {
|
|
34
|
+
const { issueKey, time, comment, started, adjustEstimate, newEstimate, reduceBy } = options;
|
|
35
|
+
const timeSpentSeconds = parseDuration(time);
|
|
36
|
+
if (timeSpentSeconds === null) {
|
|
37
|
+
throw new CommandError(`Invalid duration: "${time}". Use Jira format e.g. 1h, 30m, 1d2h30m, 1w.`, { hints: ['Examples: 1h, 30m, 1d, 1w, 1d2h30m'] });
|
|
38
|
+
}
|
|
39
|
+
if (adjustEstimate === 'new' && !newEstimate) {
|
|
40
|
+
throw new CommandError('--new-estimate is required when --adjust-estimate is "new"', {
|
|
41
|
+
hints: ['Example: --adjust-estimate new --new-estimate 5h']
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (adjustEstimate === 'manual' && !newEstimate && !reduceBy) {
|
|
45
|
+
throw new CommandError('--new-estimate or --reduce-by is required when --adjust-estimate is "manual"', {
|
|
46
|
+
hints: ['Example: --adjust-estimate manual --reduce-by 1h']
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (isDryRun()) {
|
|
50
|
+
formatDryRunResult('issue worklog add', issueKey, { timeSpentSeconds, comment, started, adjustEstimate, newEstimate, reduceBy });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const result = await addWorklogEntry(issueKey, {
|
|
55
|
+
timeSpentSeconds,
|
|
56
|
+
comment,
|
|
57
|
+
started: started !== undefined ? normalizeJiraTimestamp(started) : undefined,
|
|
58
|
+
adjustEstimate,
|
|
59
|
+
newEstimate,
|
|
60
|
+
reduceBy,
|
|
61
|
+
});
|
|
62
|
+
outputResult(result);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof CommandError)
|
|
66
|
+
throw error;
|
|
67
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
68
|
+
const hints = [];
|
|
69
|
+
if (errorMsg.includes('404')) {
|
|
70
|
+
hints.push('Check that the issue key is correct');
|
|
71
|
+
}
|
|
72
|
+
else if (errorMsg.includes('403')) {
|
|
73
|
+
hints.push('You may not have permission to log work on this issue');
|
|
74
|
+
}
|
|
75
|
+
throw new CommandError(`Failed to add worklog: ${error.message}`, { hints });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function issueWorklogUpdateCommand(options) {
|
|
79
|
+
const { issueKey, id, time, comment, started, adjustEstimate, newEstimate } = options;
|
|
80
|
+
if (time === undefined && comment === undefined && started === undefined) {
|
|
81
|
+
throw new CommandError('At least one of --time, --comment, or --started must be provided.');
|
|
82
|
+
}
|
|
83
|
+
let timeSpentSeconds;
|
|
84
|
+
if (time !== undefined) {
|
|
85
|
+
const parsed = parseDuration(time);
|
|
86
|
+
if (parsed === null) {
|
|
87
|
+
throw new CommandError(`Invalid duration: "${time}". Use Jira format e.g. 1h, 30m, 1d2h30m, 1w.`, { hints: ['Examples: 1h, 30m, 1d, 1w, 1d2h30m'] });
|
|
88
|
+
}
|
|
89
|
+
timeSpentSeconds = parsed;
|
|
90
|
+
}
|
|
91
|
+
if (adjustEstimate === 'new' && !newEstimate) {
|
|
92
|
+
throw new CommandError('--new-estimate is required when --adjust-estimate is "new"', {
|
|
93
|
+
hints: ['Example: --adjust-estimate new --new-estimate 5h']
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (adjustEstimate === 'manual' && !newEstimate) {
|
|
97
|
+
throw new CommandError('--new-estimate is required when --adjust-estimate is "manual"', {
|
|
98
|
+
hints: ['Example: --adjust-estimate manual --new-estimate 5h']
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (isDryRun()) {
|
|
102
|
+
formatDryRunResult('issue worklog update', `${issueKey} / worklog ${id}`, { timeSpentSeconds, comment, started, adjustEstimate, newEstimate });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const result = await updateWorklogEntry(issueKey, id, {
|
|
107
|
+
timeSpentSeconds,
|
|
108
|
+
comment,
|
|
109
|
+
started: started !== undefined ? normalizeJiraTimestamp(started) : undefined,
|
|
110
|
+
adjustEstimate,
|
|
111
|
+
newEstimate,
|
|
112
|
+
});
|
|
113
|
+
outputResult(result);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof CommandError)
|
|
117
|
+
throw error;
|
|
118
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
119
|
+
const hints = [];
|
|
120
|
+
if (errorMsg.includes('404')) {
|
|
121
|
+
hints.push('Check that the issue key and worklog ID are correct');
|
|
122
|
+
}
|
|
123
|
+
else if (errorMsg.includes('403')) {
|
|
124
|
+
hints.push('You may not have permission to update this worklog');
|
|
125
|
+
}
|
|
126
|
+
throw new CommandError(`Failed to update worklog: ${error.message}`, { hints });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function issueWorklogDeleteCommand(options) {
|
|
130
|
+
const { issueKey, id, adjustEstimate, newEstimate, increaseBy } = options;
|
|
131
|
+
if (adjustEstimate === 'new' && !newEstimate) {
|
|
132
|
+
throw new CommandError('--new-estimate is required when --adjust-estimate is "new"', {
|
|
133
|
+
hints: ['Example: --adjust-estimate new --new-estimate 5h']
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (adjustEstimate === 'manual' && !newEstimate && !increaseBy) {
|
|
137
|
+
throw new CommandError('--new-estimate or --increase-by is required when --adjust-estimate is "manual"', {
|
|
138
|
+
hints: ['Example: --adjust-estimate manual --increase-by 1h']
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (isDryRun()) {
|
|
142
|
+
formatDryRunResult('issue worklog delete', `${issueKey} / worklog ${id}`, { id, adjustEstimate, newEstimate, increaseBy });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await deleteWorklogEntry(issueKey, id, { adjustEstimate, newEstimate, increaseBy });
|
|
147
|
+
outputResult({ deleted: true, issueKey, id });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof CommandError)
|
|
151
|
+
throw error;
|
|
152
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
153
|
+
const hints = [];
|
|
154
|
+
if (errorMsg.includes('404')) {
|
|
155
|
+
hints.push('Check that the issue key and worklog ID are correct');
|
|
156
|
+
}
|
|
157
|
+
else if (errorMsg.includes('403')) {
|
|
158
|
+
hints.push('You may not have permission to delete this worklog');
|
|
159
|
+
}
|
|
160
|
+
throw new CommandError(`Failed to delete worklog: ${error.message}`, { hints });
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/lib/jira-client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Version3Client } from 'jira.js';
|
|
2
|
+
import { markdownToAdf } from 'marklassian';
|
|
2
3
|
import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
|
|
3
4
|
import { loadCredentials } from './auth-storage.js';
|
|
4
5
|
import { applyGlobalFilters, isProjectAllowed, isCommandAllowed, validateIssueAgainstFilters, getAllowedProjects } from './settings.js';
|
|
@@ -1044,6 +1045,183 @@ export async function downloadAttachment(issueKey, attachmentId, outputPath) {
|
|
|
1044
1045
|
fs.writeFileSync(destPath, Buffer.from(content));
|
|
1045
1046
|
return destPath;
|
|
1046
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get comments for a Jira issue with pagination, since-filter, and ordering
|
|
1050
|
+
*/
|
|
1051
|
+
export async function getIssueCommentsList(issueKey, options = {}) {
|
|
1052
|
+
const client = getJiraClient();
|
|
1053
|
+
const { limit = 50, since, reverse = false } = options;
|
|
1054
|
+
const maxResults = 100;
|
|
1055
|
+
let startAt = 0;
|
|
1056
|
+
let allComments = [];
|
|
1057
|
+
let total = 0;
|
|
1058
|
+
// Paginate through all comments
|
|
1059
|
+
while (true) {
|
|
1060
|
+
const response = await client.issueComments.getComments({
|
|
1061
|
+
issueIdOrKey: issueKey,
|
|
1062
|
+
maxResults,
|
|
1063
|
+
startAt,
|
|
1064
|
+
orderBy: 'created',
|
|
1065
|
+
});
|
|
1066
|
+
total = response.total ?? 0;
|
|
1067
|
+
const pageComments = (response.comments || []).map((c) => ({
|
|
1068
|
+
id: c.id || '',
|
|
1069
|
+
author: {
|
|
1070
|
+
accountId: c.author?.accountId || '',
|
|
1071
|
+
displayName: c.author?.displayName || 'Unknown',
|
|
1072
|
+
emailAddress: c.author?.emailAddress,
|
|
1073
|
+
},
|
|
1074
|
+
body: convertADFToMarkdown(c.body) || '',
|
|
1075
|
+
created: c.created || '',
|
|
1076
|
+
updated: c.updated || '',
|
|
1077
|
+
}));
|
|
1078
|
+
allComments = allComments.concat(pageComments);
|
|
1079
|
+
if (allComments.length >= total || pageComments.length < maxResults) {
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
startAt += maxResults;
|
|
1083
|
+
}
|
|
1084
|
+
// Apply --since filter
|
|
1085
|
+
if (since) {
|
|
1086
|
+
const sinceDate = new Date(since).getTime();
|
|
1087
|
+
allComments = allComments.filter(c => new Date(c.created).getTime() >= sinceDate);
|
|
1088
|
+
}
|
|
1089
|
+
// Apply --reverse (ascending order; default is newest first)
|
|
1090
|
+
if (!reverse) {
|
|
1091
|
+
allComments = allComments.reverse();
|
|
1092
|
+
}
|
|
1093
|
+
// Apply --limit
|
|
1094
|
+
const hasMore = allComments.length > limit;
|
|
1095
|
+
const comments = allComments.slice(0, limit);
|
|
1096
|
+
return {
|
|
1097
|
+
issueKey,
|
|
1098
|
+
comments,
|
|
1099
|
+
total,
|
|
1100
|
+
hasMore,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Get unified activity feed (changelog + comments) for a Jira issue
|
|
1105
|
+
*/
|
|
1106
|
+
export async function getIssueActivityFeed(issueKey, options = {}) {
|
|
1107
|
+
const client = getJiraClient();
|
|
1108
|
+
const { since, limit = 50, types, author } = options;
|
|
1109
|
+
const sinceDate = since ? new Date(since).getTime() : null;
|
|
1110
|
+
const typeFilter = types ? new Set(types.split(',').map(t => t.trim())) : null;
|
|
1111
|
+
// --- Fetch all changelog entries with pagination ---
|
|
1112
|
+
let changelogStartAt = 0;
|
|
1113
|
+
const changelogMaxResults = 100;
|
|
1114
|
+
let allChangelog = [];
|
|
1115
|
+
while (true) {
|
|
1116
|
+
const response = await client.issues.getChangeLogs({
|
|
1117
|
+
issueIdOrKey: issueKey,
|
|
1118
|
+
maxResults: changelogMaxResults,
|
|
1119
|
+
startAt: changelogStartAt,
|
|
1120
|
+
});
|
|
1121
|
+
const values = response.values || [];
|
|
1122
|
+
allChangelog = allChangelog.concat(values);
|
|
1123
|
+
const responseTotal = response.total ?? 0;
|
|
1124
|
+
if (allChangelog.length >= responseTotal || values.length < changelogMaxResults) {
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
changelogStartAt += changelogMaxResults;
|
|
1128
|
+
}
|
|
1129
|
+
// --- Fetch all comments with pagination ---
|
|
1130
|
+
let commentStartAt = 0;
|
|
1131
|
+
const commentMaxResults = 100;
|
|
1132
|
+
let allApiComments = [];
|
|
1133
|
+
while (true) {
|
|
1134
|
+
const response = await client.issueComments.getComments({
|
|
1135
|
+
issueIdOrKey: issueKey,
|
|
1136
|
+
maxResults: commentMaxResults,
|
|
1137
|
+
startAt: commentStartAt,
|
|
1138
|
+
});
|
|
1139
|
+
const comments = response.comments || [];
|
|
1140
|
+
allApiComments = allApiComments.concat(comments);
|
|
1141
|
+
const responseTotal = response.total ?? 0;
|
|
1142
|
+
if (allApiComments.length >= responseTotal || comments.length < commentMaxResults) {
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
commentStartAt += commentMaxResults;
|
|
1146
|
+
}
|
|
1147
|
+
// --- Convert changelog to ActivityEntry list ---
|
|
1148
|
+
const changelogActivities = [];
|
|
1149
|
+
for (const history of allChangelog) {
|
|
1150
|
+
const historyAuthor = {
|
|
1151
|
+
accountId: history.author?.accountId || '',
|
|
1152
|
+
displayName: history.author?.displayName || 'Unknown',
|
|
1153
|
+
emailAddress: history.author?.emailAddress,
|
|
1154
|
+
};
|
|
1155
|
+
for (const item of history.items || []) {
|
|
1156
|
+
const field = item.field || '';
|
|
1157
|
+
const fieldLower = field.toLowerCase();
|
|
1158
|
+
let type;
|
|
1159
|
+
if (fieldLower === 'status') {
|
|
1160
|
+
type = 'status_change';
|
|
1161
|
+
}
|
|
1162
|
+
else if (fieldLower.startsWith('link') || fieldLower === 'issuelinks') {
|
|
1163
|
+
// Determine add vs remove from to/from strings
|
|
1164
|
+
type = item.to ? 'link_added' : 'link_removed';
|
|
1165
|
+
}
|
|
1166
|
+
else if (fieldLower === 'attachment') {
|
|
1167
|
+
type = item.to ? 'attachment_added' : 'attachment_removed';
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
type = 'field_change';
|
|
1171
|
+
}
|
|
1172
|
+
changelogActivities.push({
|
|
1173
|
+
id: `${history.id}-${field}`,
|
|
1174
|
+
type,
|
|
1175
|
+
timestamp: history.created || '',
|
|
1176
|
+
author: historyAuthor,
|
|
1177
|
+
field,
|
|
1178
|
+
from: item.fromString ?? undefined,
|
|
1179
|
+
to: item.toString ?? undefined,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// --- Convert comments to ActivityEntry list ---
|
|
1184
|
+
const commentActivities = allApiComments.map((c) => {
|
|
1185
|
+
const isUpdated = c.created !== c.updated;
|
|
1186
|
+
return {
|
|
1187
|
+
id: c.id || '',
|
|
1188
|
+
type: isUpdated ? 'comment_updated' : 'comment_added',
|
|
1189
|
+
timestamp: isUpdated ? c.updated : c.created,
|
|
1190
|
+
author: {
|
|
1191
|
+
accountId: c.author?.accountId || '',
|
|
1192
|
+
displayName: c.author?.displayName || 'Unknown',
|
|
1193
|
+
emailAddress: c.author?.emailAddress,
|
|
1194
|
+
},
|
|
1195
|
+
commentBody: convertADFToMarkdown(c.body) || '',
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
// --- Merge and sort by timestamp descending ---
|
|
1199
|
+
let activities = [...changelogActivities, ...commentActivities];
|
|
1200
|
+
activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1201
|
+
const totalChanges = activities.length;
|
|
1202
|
+
// --- Apply filters ---
|
|
1203
|
+
if (sinceDate) {
|
|
1204
|
+
activities = activities.filter(a => new Date(a.timestamp).getTime() >= sinceDate);
|
|
1205
|
+
}
|
|
1206
|
+
if (typeFilter) {
|
|
1207
|
+
activities = activities.filter(a => typeFilter.has(a.type));
|
|
1208
|
+
}
|
|
1209
|
+
if (author) {
|
|
1210
|
+
const authorLower = author.toLowerCase();
|
|
1211
|
+
activities = activities.filter(a => a.author.displayName.toLowerCase().includes(authorLower) ||
|
|
1212
|
+
(a.author.emailAddress && a.author.emailAddress.toLowerCase().includes(authorLower)) ||
|
|
1213
|
+
a.author.accountId.toLowerCase().includes(authorLower));
|
|
1214
|
+
}
|
|
1215
|
+
// --- Apply limit ---
|
|
1216
|
+
const hasMore = activities.length > limit;
|
|
1217
|
+
activities = activities.slice(0, limit);
|
|
1218
|
+
return {
|
|
1219
|
+
issueKey,
|
|
1220
|
+
activities,
|
|
1221
|
+
totalChanges,
|
|
1222
|
+
hasMore,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1047
1225
|
/**
|
|
1048
1226
|
* Delete an attachment by ID
|
|
1049
1227
|
*/
|
|
@@ -1051,3 +1229,140 @@ export async function deleteAttachment(issueKey, attachmentId) {
|
|
|
1051
1229
|
const client = getJiraClient();
|
|
1052
1230
|
await client.issueAttachments.removeAttachment(attachmentId);
|
|
1053
1231
|
}
|
|
1232
|
+
const WORKLOG_PAGE_SIZE = 5000;
|
|
1233
|
+
/**
|
|
1234
|
+
* List all worklogs for an issue (returns structured result).
|
|
1235
|
+
* Paginates automatically through all pages and supports optional filtering.
|
|
1236
|
+
*/
|
|
1237
|
+
export async function getIssueWorklogsList(issueIdOrKey, options = {}) {
|
|
1238
|
+
const { startedAfter, startedBefore, authorAccountId } = options;
|
|
1239
|
+
const client = getJiraClient();
|
|
1240
|
+
const allWorklogs = [];
|
|
1241
|
+
let startAt = 0;
|
|
1242
|
+
while (true) {
|
|
1243
|
+
const params = { issueIdOrKey, startAt, maxResults: WORKLOG_PAGE_SIZE };
|
|
1244
|
+
if (startedAfter !== undefined)
|
|
1245
|
+
params.startedAfter = startedAfter;
|
|
1246
|
+
if (startedBefore !== undefined)
|
|
1247
|
+
params.startedBefore = startedBefore;
|
|
1248
|
+
const response = await client.issueWorklogs.getIssueWorklog(params);
|
|
1249
|
+
const page = (response.worklogs || []).map((w) => ({
|
|
1250
|
+
id: w.id || '',
|
|
1251
|
+
author: {
|
|
1252
|
+
accountId: w.author?.accountId || '',
|
|
1253
|
+
displayName: w.author?.displayName || 'Unknown',
|
|
1254
|
+
emailAddress: w.author?.emailAddress,
|
|
1255
|
+
},
|
|
1256
|
+
comment: convertADFToMarkdown(w.comment),
|
|
1257
|
+
created: w.created || '',
|
|
1258
|
+
updated: w.updated || '',
|
|
1259
|
+
started: w.started || '',
|
|
1260
|
+
timeSpent: w.timeSpent || '',
|
|
1261
|
+
timeSpentSeconds: w.timeSpentSeconds || 0,
|
|
1262
|
+
issueKey: issueIdOrKey,
|
|
1263
|
+
}));
|
|
1264
|
+
allWorklogs.push(...page);
|
|
1265
|
+
const totalOnServer = typeof response.total === 'number' ? response.total : page.length;
|
|
1266
|
+
if (allWorklogs.length >= totalOnServer || page.length < WORKLOG_PAGE_SIZE)
|
|
1267
|
+
break;
|
|
1268
|
+
startAt += page.length;
|
|
1269
|
+
}
|
|
1270
|
+
const filtered = authorAccountId
|
|
1271
|
+
? allWorklogs.filter(w => w.author.accountId === authorAccountId)
|
|
1272
|
+
: allWorklogs;
|
|
1273
|
+
return {
|
|
1274
|
+
issueKey: issueIdOrKey,
|
|
1275
|
+
worklogs: filtered,
|
|
1276
|
+
total: filtered.length,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Add a worklog entry to an issue
|
|
1281
|
+
*/
|
|
1282
|
+
export async function addWorklogEntry(issueIdOrKey, options) {
|
|
1283
|
+
const client = getJiraClient();
|
|
1284
|
+
const { timeSpentSeconds, comment, started, adjustEstimate, newEstimate, reduceBy } = options;
|
|
1285
|
+
const params = {
|
|
1286
|
+
issueIdOrKey,
|
|
1287
|
+
timeSpentSeconds,
|
|
1288
|
+
comment: comment ? markdownToAdf(comment) : undefined,
|
|
1289
|
+
started: started || new Date().toISOString().replace('Z', '+0000'),
|
|
1290
|
+
};
|
|
1291
|
+
if (adjustEstimate)
|
|
1292
|
+
params.adjustEstimate = adjustEstimate;
|
|
1293
|
+
if (newEstimate)
|
|
1294
|
+
params.newEstimate = newEstimate;
|
|
1295
|
+
if (reduceBy)
|
|
1296
|
+
params.reduceBy = reduceBy;
|
|
1297
|
+
const w = await client.issueWorklogs.addWorklog(params);
|
|
1298
|
+
return {
|
|
1299
|
+
id: w.id || '',
|
|
1300
|
+
author: {
|
|
1301
|
+
accountId: w.author?.accountId || '',
|
|
1302
|
+
displayName: w.author?.displayName || 'Unknown',
|
|
1303
|
+
emailAddress: w.author?.emailAddress,
|
|
1304
|
+
},
|
|
1305
|
+
comment: convertADFToMarkdown(w.comment),
|
|
1306
|
+
created: w.created || '',
|
|
1307
|
+
updated: w.updated || '',
|
|
1308
|
+
started: w.started || '',
|
|
1309
|
+
timeSpent: w.timeSpent || '',
|
|
1310
|
+
timeSpentSeconds: w.timeSpentSeconds || 0,
|
|
1311
|
+
issueKey: issueIdOrKey,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Update an existing worklog entry
|
|
1316
|
+
*/
|
|
1317
|
+
export async function updateWorklogEntry(issueIdOrKey, worklogId, options) {
|
|
1318
|
+
const client = getJiraClient();
|
|
1319
|
+
const { timeSpentSeconds, comment, started, adjustEstimate, newEstimate } = options;
|
|
1320
|
+
const params = {
|
|
1321
|
+
issueIdOrKey,
|
|
1322
|
+
id: worklogId,
|
|
1323
|
+
};
|
|
1324
|
+
if (timeSpentSeconds !== undefined)
|
|
1325
|
+
params.timeSpentSeconds = timeSpentSeconds;
|
|
1326
|
+
if (comment !== undefined)
|
|
1327
|
+
params.comment = markdownToAdf(comment);
|
|
1328
|
+
if (started !== undefined)
|
|
1329
|
+
params.started = started;
|
|
1330
|
+
if (adjustEstimate)
|
|
1331
|
+
params.adjustEstimate = adjustEstimate;
|
|
1332
|
+
if (newEstimate)
|
|
1333
|
+
params.newEstimate = newEstimate;
|
|
1334
|
+
const w = await client.issueWorklogs.updateWorklog(params);
|
|
1335
|
+
return {
|
|
1336
|
+
id: w.id || worklogId,
|
|
1337
|
+
author: {
|
|
1338
|
+
accountId: w.author?.accountId || '',
|
|
1339
|
+
displayName: w.author?.displayName || 'Unknown',
|
|
1340
|
+
emailAddress: w.author?.emailAddress,
|
|
1341
|
+
},
|
|
1342
|
+
comment: convertADFToMarkdown(w.comment),
|
|
1343
|
+
created: w.created || '',
|
|
1344
|
+
updated: w.updated || '',
|
|
1345
|
+
started: w.started || '',
|
|
1346
|
+
timeSpent: w.timeSpent || '',
|
|
1347
|
+
timeSpentSeconds: w.timeSpentSeconds || 0,
|
|
1348
|
+
issueKey: issueIdOrKey,
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Delete a worklog entry
|
|
1353
|
+
*/
|
|
1354
|
+
export async function deleteWorklogEntry(issueIdOrKey, worklogId, options = {}) {
|
|
1355
|
+
const client = getJiraClient();
|
|
1356
|
+
const { adjustEstimate, newEstimate, increaseBy } = options;
|
|
1357
|
+
const params = {
|
|
1358
|
+
issueIdOrKey,
|
|
1359
|
+
id: worklogId,
|
|
1360
|
+
};
|
|
1361
|
+
if (adjustEstimate)
|
|
1362
|
+
params.adjustEstimate = adjustEstimate;
|
|
1363
|
+
if (newEstimate)
|
|
1364
|
+
params.newEstimate = newEstimate;
|
|
1365
|
+
if (increaseBy)
|
|
1366
|
+
params.increaseBy = increaseBy;
|
|
1367
|
+
await client.issueWorklogs.deleteWorklog(params);
|
|
1368
|
+
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -152,3 +152,42 @@ export function parseTimeframe(timeframe) {
|
|
|
152
152
|
export function formatDateForJql(date) {
|
|
153
153
|
return date.toISOString().split('T')[0];
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Normalize an ISO-8601 timestamp to the format Jira accepts: yyyy-MM-dd'T'HH:mm:ss.SSSZ
|
|
157
|
+
* - No colon in timezone offset, no Z suffix, milliseconds always present.
|
|
158
|
+
* - "2026-04-15T07:00:00.000Z" → "2026-04-15T07:00:00.000+0000"
|
|
159
|
+
* - "2026-04-15T10:00:00+03:00" → "2026-04-15T10:00:00.000+0300"
|
|
160
|
+
* - "2026-04-15T07:00:00Z" → "2026-04-15T07:00:00.000+0000"
|
|
161
|
+
* - "2026-04-15T07:00:00.000+0000" → unchanged
|
|
162
|
+
*/
|
|
163
|
+
export function normalizeJiraTimestamp(timestamp) {
|
|
164
|
+
// Replace Z suffix with +0000
|
|
165
|
+
let normalized = timestamp.replace(/Z$/, '+0000');
|
|
166
|
+
// Remove colon from timezone offset: +HH:MM → +HHMM or -HH:MM → -HHMM
|
|
167
|
+
normalized = normalized.replace(/([+-])(\d{2}):(\d{2})$/, '$1$2$3');
|
|
168
|
+
// Add .000 milliseconds if missing (before the timezone offset)
|
|
169
|
+
normalized = normalized.replace(/(\d{2}:\d{2}:\d{2})([+-]\d{4})$/, '$1.000$2');
|
|
170
|
+
return normalized;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse a Jira-style duration string into total seconds.
|
|
174
|
+
* Supports: 1w, 2d, 3h, 30m and combinations like 1d2h30m.
|
|
175
|
+
* Conversion: 1w = 5d, 1d = 8h.
|
|
176
|
+
* Returns null for invalid input.
|
|
177
|
+
*/
|
|
178
|
+
export function parseDuration(duration) {
|
|
179
|
+
const match = duration.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
|
|
180
|
+
if (!match || match[0] === '')
|
|
181
|
+
return null;
|
|
182
|
+
const weeks = parseInt(match[1] || '0', 10);
|
|
183
|
+
const days = parseInt(match[2] || '0', 10);
|
|
184
|
+
const hours = parseInt(match[3] || '0', 10);
|
|
185
|
+
const minutes = parseInt(match[4] || '0', 10);
|
|
186
|
+
const totalSeconds = weeks * 5 * 8 * 3600 +
|
|
187
|
+
days * 8 * 3600 +
|
|
188
|
+
hours * 3600 +
|
|
189
|
+
minutes * 60;
|
|
190
|
+
if (totalSeconds === 0)
|
|
191
|
+
return null;
|
|
192
|
+
return totalSeconds;
|
|
193
|
+
}
|
package/dist/lib/validation.js
CHANGED
|
@@ -254,3 +254,57 @@ export const AttachDownloadSchema = z.object({
|
|
|
254
254
|
id: z.string().trim().min(1, 'Attachment ID is required'),
|
|
255
255
|
output: z.string().trim().optional(),
|
|
256
256
|
});
|
|
257
|
+
// =============================================================================
|
|
258
|
+
// ISSUE COMMENTS / ACTIVITY SCHEMAS
|
|
259
|
+
// =============================================================================
|
|
260
|
+
export const CommentsListSchema = z.object({
|
|
261
|
+
issueKey: z.string().trim().min(1, 'Issue key is required').pipe(IssueKeySchema),
|
|
262
|
+
limit: z.number().int().positive().optional(),
|
|
263
|
+
since: z.string().datetime({ offset: true, message: 'since must be a valid ISO 8601 datetime (e.g. 2024-01-01T00:00:00Z)' }).optional(),
|
|
264
|
+
reverse: z.boolean().optional(),
|
|
265
|
+
});
|
|
266
|
+
export const ActivityFeedSchema = z.object({
|
|
267
|
+
issueKey: z.string().trim().min(1, 'Issue key is required').pipe(IssueKeySchema),
|
|
268
|
+
since: z.string().datetime({ offset: true, message: 'since must be a valid ISO 8601 datetime (e.g. 2024-01-01T00:00:00Z)' }).optional(),
|
|
269
|
+
limit: z.number().int().positive().optional(),
|
|
270
|
+
types: z.string().optional(),
|
|
271
|
+
author: z.string().optional(),
|
|
272
|
+
compact: z.boolean().optional(),
|
|
273
|
+
});
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// WORKLOG SCHEMAS
|
|
276
|
+
// =============================================================================
|
|
277
|
+
export const DurationSchema = z
|
|
278
|
+
.string()
|
|
279
|
+
.regex(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/, 'Duration must be in Jira format: e.g. 1h, 30m, 1d2h30m, 1w');
|
|
280
|
+
export const WorklogListSchema = z.object({
|
|
281
|
+
issueKey: z.string().trim().min(1, 'Issue key is required').pipe(IssueKeySchema),
|
|
282
|
+
startedAfter: z.number().int().positive().optional(),
|
|
283
|
+
startedBefore: z.number().int().positive().optional(),
|
|
284
|
+
authorAccountId: z.string().optional(),
|
|
285
|
+
});
|
|
286
|
+
export const WorklogAddSchema = z.object({
|
|
287
|
+
issueKey: z.string().trim().min(1, 'Issue key is required').pipe(IssueKeySchema),
|
|
288
|
+
time: DurationSchema,
|
|
289
|
+
comment: z.string().optional(),
|
|
290
|
+
started: z.string().optional(),
|
|
291
|
+
adjustEstimate: z.enum(['auto', 'new', 'leave', 'manual']).optional(),
|
|
292
|
+
newEstimate: z.string().optional(),
|
|
293
|
+
reduceBy: z.string().optional(),
|
|
294
|
+
});
|
|
295
|
+
export const WorklogUpdateSchema = z.object({
|
|
296
|
+
issueKey: z.string().trim().min(1, 'Issue key is required').pipe(IssueKeySchema),
|
|
297
|
+
id: z.string().min(1, 'Worklog ID is required'),
|
|
298
|
+
time: DurationSchema.optional(),
|
|
299
|
+
comment: z.string().optional(),
|
|
300
|
+
started: z.string().optional(),
|
|
301
|
+
adjustEstimate: z.enum(['auto', 'new', 'leave', 'manual']).optional(),
|
|
302
|
+
newEstimate: z.string().optional(),
|
|
303
|
+
});
|
|
304
|
+
export const WorklogDeleteSchema = z.object({
|
|
305
|
+
issueKey: z.string().trim().min(1, 'Issue key is required').pipe(IssueKeySchema),
|
|
306
|
+
id: z.string().min(1, 'Worklog ID is required'),
|
|
307
|
+
adjustEstimate: z.enum(['auto', 'new', 'leave', 'manual']).optional(),
|
|
308
|
+
newEstimate: z.string().optional(),
|
|
309
|
+
increaseBy: z.string().optional(),
|
|
310
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jira-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "AI friendly Jira CLI to save context",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -48,12 +48,13 @@
|
|
|
48
48
|
"node": ">=18.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
51
52
|
"adf-to-markdown": "^1.0.1",
|
|
52
53
|
"commander": "^11.0.0",
|
|
53
54
|
"confluence.js": "^2.1.0",
|
|
54
55
|
"dotenv": "^17.2.3",
|
|
55
56
|
"html-entities": "^2.6.0",
|
|
56
|
-
"jira.js": "^5.2.2",
|
|
57
|
+
"jira.js": "^5.2.2",
|
|
57
58
|
"js-yaml": "^4.1.1",
|
|
58
59
|
"marklassian": "^1.0.0",
|
|
59
60
|
"zod": "^4.3.5"
|