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 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
+ }
@@ -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
+ }
@@ -23,7 +23,8 @@ export const DEFAULT_SETTINGS = {
23
23
  'get-person-worklog',
24
24
  'organization',
25
25
  'transition',
26
- 'update-description'
26
+ 'update-description',
27
+ 'confl'
27
28
  ]
28
29
  };
29
30
  const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
@@ -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.0",
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",