jira-ai 0.6.0 → 0.6.1
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 +9 -0
- package/dist/commands/confluence.js +22 -0
- package/dist/lib/confluence-client.js +125 -0
- package/dist/lib/formatters.js +30 -0
- package/dist/lib/settings.js +2 -1
- package/dist/lib/validation.js +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ 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
20
|
import { getPersonWorklogCommand } from './commands/get-person-worklog.js';
|
|
21
|
+
import { confluenceGetPageCommand } from './commands/confluence.js';
|
|
21
22
|
import { aboutCommand } from './commands/about.js';
|
|
22
23
|
import { authCommand } from './commands/auth.js';
|
|
23
24
|
import { settingsCommand } from './commands/settings.js';
|
|
@@ -246,6 +247,14 @@ program
|
|
|
246
247
|
validateOptions(TimeframeSchema, args[1]);
|
|
247
248
|
}
|
|
248
249
|
}));
|
|
250
|
+
// Confluence commands
|
|
251
|
+
const confl = program
|
|
252
|
+
.command('confl')
|
|
253
|
+
.description('Interact with Confluence pages and content');
|
|
254
|
+
confl
|
|
255
|
+
.command('get-page <url>')
|
|
256
|
+
.description('Download and display Confluence page content and comments from a given URL.')
|
|
257
|
+
.action(withPermission('confl', confluenceGetPageCommand, { skipValidation: false }));
|
|
249
258
|
// About command (always allowed)
|
|
250
259
|
program
|
|
251
260
|
.command('about')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getPage, getPageComments } from '../lib/confluence-client.js';
|
|
3
|
+
import { formatConfluencePage } from '../lib/formatters.js';
|
|
4
|
+
import { ui } from '../lib/ui.js';
|
|
5
|
+
export async function confluenceGetPageCommand(url) {
|
|
6
|
+
ui.startSpinner(`Fetching Confluence page details for: ${url}`);
|
|
7
|
+
try {
|
|
8
|
+
const [page, comments] = await Promise.all([
|
|
9
|
+
getPage(url),
|
|
10
|
+
getPageComments(url)
|
|
11
|
+
]);
|
|
12
|
+
ui.succeedSpinner(chalk.green('Confluence page details retrieved'));
|
|
13
|
+
console.log(formatConfluencePage(page, comments));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
ui.failSpinner();
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
throw new Error('An unknown error occurred while fetching Confluence page');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ConfluenceClient } from 'confluence.js';
|
|
2
|
+
import { loadCredentials } from './auth-storage.js';
|
|
3
|
+
import { convertADFToMarkdown } from './utils.js';
|
|
4
|
+
let confluenceClient = null;
|
|
5
|
+
let organizationOverride = undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Set a global organization override for the current execution
|
|
8
|
+
*/
|
|
9
|
+
export function setOrganizationOverride(alias) {
|
|
10
|
+
organizationOverride = alias;
|
|
11
|
+
confluenceClient = null; // Force client recreation
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get or create Confluence client instance
|
|
15
|
+
*/
|
|
16
|
+
export function getConfluenceClient() {
|
|
17
|
+
if (!confluenceClient) {
|
|
18
|
+
const host = process.env.JIRA_HOST;
|
|
19
|
+
const email = process.env.JIRA_USER_EMAIL;
|
|
20
|
+
const apiToken = process.env.JIRA_API_TOKEN;
|
|
21
|
+
if (host && email && apiToken) {
|
|
22
|
+
confluenceClient = new ConfluenceClient({
|
|
23
|
+
host: host.includes('/wiki') ? host : `${host.replace(/\/$/, '')}/wiki`,
|
|
24
|
+
authentication: {
|
|
25
|
+
basic: {
|
|
26
|
+
email,
|
|
27
|
+
apiToken,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const storedCreds = loadCredentials(organizationOverride);
|
|
34
|
+
if (storedCreds) {
|
|
35
|
+
confluenceClient = new ConfluenceClient({
|
|
36
|
+
host: storedCreds.host.includes('/wiki') ? storedCreds.host : `${storedCreds.host.replace(/\/$/, '')}/wiki`,
|
|
37
|
+
authentication: {
|
|
38
|
+
basic: {
|
|
39
|
+
email: storedCreds.email,
|
|
40
|
+
apiToken: storedCreds.apiToken,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const errorMsg = organizationOverride
|
|
47
|
+
? `Credentials for organization "${organizationOverride}" not found.`
|
|
48
|
+
: 'Credentials not found. Please set environment variables or run "jira-ai auth"';
|
|
49
|
+
throw new Error(errorMsg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return confluenceClient;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse Confluence URL to extract page ID
|
|
57
|
+
*/
|
|
58
|
+
export function parseConfluenceUrl(url) {
|
|
59
|
+
try {
|
|
60
|
+
const parsedUrl = new URL(url);
|
|
61
|
+
// Pattern: /wiki/spaces/SPACE/pages/PAGE_ID/TITLE
|
|
62
|
+
const pagesMatch = parsedUrl.pathname.match(/\/pages\/(\d+)/);
|
|
63
|
+
if (pagesMatch && pagesMatch[1]) {
|
|
64
|
+
return pagesMatch[1];
|
|
65
|
+
}
|
|
66
|
+
// Pattern: /wiki/pages/viewpage.action?pageId=PAGE_ID
|
|
67
|
+
const pageIdParam = parsedUrl.searchParams.get('pageId');
|
|
68
|
+
if (pageIdParam) {
|
|
69
|
+
return pageIdParam;
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Could not extract Page ID from URL');
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof Error) {
|
|
75
|
+
throw new Error(`Invalid Confluence URL: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
throw new Error('Invalid Confluence URL');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get Confluence page details
|
|
82
|
+
*/
|
|
83
|
+
export async function getPage(url) {
|
|
84
|
+
const client = getConfluenceClient();
|
|
85
|
+
const pageId = parseConfluenceUrl(url);
|
|
86
|
+
const page = await client.content.getContentById({
|
|
87
|
+
id: pageId,
|
|
88
|
+
expand: ['body.atlas_doc_format', 'version', 'space', 'history.lastUpdated'],
|
|
89
|
+
});
|
|
90
|
+
const adfBody = page.body?.atlas_doc_format?.value;
|
|
91
|
+
const content = adfBody ? convertADFToMarkdown(JSON.parse(adfBody)) : 'No content available.';
|
|
92
|
+
// @ts-ignore - accessing host to show it in UI
|
|
93
|
+
const host = client.config.host || '';
|
|
94
|
+
return {
|
|
95
|
+
id: page.id || '',
|
|
96
|
+
title: page.title || '',
|
|
97
|
+
content,
|
|
98
|
+
space: page.space?.name || page.space?.key || 'Unknown',
|
|
99
|
+
author: page.history?.createdBy?.displayName || 'Unknown',
|
|
100
|
+
lastUpdated: page.history?.lastUpdated?.when || page.version?.when || '',
|
|
101
|
+
url: `${host}/pages/${page.id}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get Confluence page comments
|
|
106
|
+
*/
|
|
107
|
+
export async function getPageComments(url) {
|
|
108
|
+
const client = getConfluenceClient();
|
|
109
|
+
const pageId = parseConfluenceUrl(url);
|
|
110
|
+
const response = await client.contentChildrenAndDescendants.getContentChildrenByType({
|
|
111
|
+
id: pageId,
|
|
112
|
+
type: 'comment',
|
|
113
|
+
expand: ['body.atlas_doc_format', 'history.lastUpdated', 'version'],
|
|
114
|
+
});
|
|
115
|
+
return (response.results || []).map((comment) => {
|
|
116
|
+
const adfBody = comment.body?.atlas_doc_format?.value;
|
|
117
|
+
const body = adfBody ? convertADFToMarkdown(JSON.parse(adfBody)) : 'No content available.';
|
|
118
|
+
return {
|
|
119
|
+
id: comment.id,
|
|
120
|
+
author: comment.history?.createdBy?.displayName || 'Unknown',
|
|
121
|
+
body,
|
|
122
|
+
created: comment.history?.createdDate || comment.version?.when || '',
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
package/dist/lib/formatters.js
CHANGED
|
@@ -514,3 +514,33 @@ export function formatSettings(settings) {
|
|
|
514
514
|
output += table.toString() + '\n';
|
|
515
515
|
return output;
|
|
516
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Format Confluence page details
|
|
519
|
+
*/
|
|
520
|
+
export function formatConfluencePage(page, comments) {
|
|
521
|
+
let output = '\n' + chalk.bold.cyan(`Confluence Page: ${decode(page.title)}`) + '\n\n';
|
|
522
|
+
// Basic info table
|
|
523
|
+
const infoTable = createTable(['Property', 'Value'], [15, 65]);
|
|
524
|
+
infoTable.push(['Space', page.space], ['Author', page.author], ['Last Updated', formatTimestamp(page.lastUpdated)], ['URL', page.url]);
|
|
525
|
+
output += infoTable.toString() + '\n\n';
|
|
526
|
+
// Content
|
|
527
|
+
output += chalk.bold('Content:') + '\n';
|
|
528
|
+
output += chalk.dim('─'.repeat(80)) + '\n';
|
|
529
|
+
output += decode(page.content) + '\n';
|
|
530
|
+
output += chalk.dim('─'.repeat(80)) + '\n\n';
|
|
531
|
+
// Comments
|
|
532
|
+
if (comments.length > 0) {
|
|
533
|
+
output += chalk.bold(`Comments (${comments.length}):`) + '\n\n';
|
|
534
|
+
comments.forEach((comment, index) => {
|
|
535
|
+
output += chalk.cyan(`${index + 1}. ${comment.author}`) +
|
|
536
|
+
chalk.gray(` - ${formatTimestamp(comment.created)}`) + '\n';
|
|
537
|
+
output += chalk.dim('─'.repeat(80)) + '\n';
|
|
538
|
+
output += decode(comment.body) + '\n';
|
|
539
|
+
output += chalk.dim('─'.repeat(80)) + '\n\n';
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
output += chalk.gray('No comments found.\n\n');
|
|
544
|
+
}
|
|
545
|
+
return output;
|
|
546
|
+
}
|
package/dist/lib/settings.js
CHANGED
package/dist/lib/validation.js
CHANGED
|
@@ -95,5 +95,5 @@ export const ProjectSettingSchema = z.union([
|
|
|
95
95
|
]);
|
|
96
96
|
export const SettingsSchema = z.object({
|
|
97
97
|
projects: z.array(ProjectSettingSchema).nullish().transform(val => val || ['all']),
|
|
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']),
|
|
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', 'confl']),
|
|
99
99
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jira-ai",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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
|
"chalk": "^5.6.2",
|
|
53
53
|
"cli-table3": "^0.6.5",
|
|
54
54
|
"commander": "^11.0.0",
|
|
55
|
+
"confluence.js": "^2.1.0",
|
|
55
56
|
"dotenv": "^17.2.3",
|
|
56
57
|
"html-entities": "^2.6.0",
|
|
57
58
|
"inquirer": "^9.3.8",
|