jira-pilot 1.0.1 → 1.0.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/README.md CHANGED
@@ -87,7 +87,7 @@ jira issue list --export json # Creates issues-TIMESTAMP.json
87
87
  jira issue list --export md # Creates issues-TIMESTAMP.md
88
88
 
89
89
  # Combine filters and export (Power User)
90
- jira issue list --project TRAIN --assignee <assignee_name> --status Done --export json
90
+ jira issue list --project "project-name" --assignee "assignee_name" --status Done --export json
91
91
  ```
92
92
 
93
93
  ### Projects & Sprints
package/bin/jira.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import { readFileSync } from 'fs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-pilot",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "AI-powered Jira CLI for humans and agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -42,8 +42,17 @@ export function registerAiCommand(program) {
42
42
  console.log(aiResponse);
43
43
 
44
44
  } catch (e) {
45
- spinner.fail('Failed to generate summary');
46
- console.error(chalk.red(e.message));
45
+ spinner.stop(); // Ensure spinner stops
46
+ if (e.response && e.response.config && e.response.config.url.includes('/issue/')) {
47
+ console.error(chalk.red(`\nError: Issue "${issueKey}" not found.`));
48
+ } else {
49
+ console.error(chalk.red('\nFailed to generate summary:'));
50
+ if (e.response) {
51
+ console.error(chalk.red(`API Error ${e.response.status}: `), e.response.data);
52
+ } else {
53
+ console.error(chalk.red(e.message));
54
+ }
55
+ }
47
56
  }
48
57
  });
49
58
 
@@ -36,6 +36,27 @@ export function registerConfigCommand(program) {
36
36
  name: 'apiToken',
37
37
  message: 'Jira API Token:',
38
38
  initial: current.apiToken ? '*****' : undefined
39
+ },
40
+ {
41
+ type: 'confirm',
42
+ name: 'aiEnabled',
43
+ message: 'Enable AI features?',
44
+ initial: current.aiEnabled || false
45
+ },
46
+ {
47
+ type: 'select',
48
+ name: 'aiProvider',
49
+ message: 'Select AI Provider:',
50
+ choices: ['openai'], // Add 'gemini', 'anthropic' when ready
51
+ initial: current.aiProvider || 'openai',
52
+ skip: (state) => !state.answers.aiEnabled
53
+ },
54
+ {
55
+ type: 'password',
56
+ name: 'aiKey',
57
+ message: 'AI API Key:',
58
+ initial: current.aiKey ? '*****' : undefined,
59
+ skip: (state) => !state.answers.aiEnabled
39
60
  }
40
61
  ]);
41
62
 
@@ -87,5 +108,47 @@ export function registerConfigCommand(program) {
87
108
  console.log(chalk.green('Credentials cleared.'));
88
109
  });
89
110
 
111
+ const aiConfigCmd = new Command('ai')
112
+ .description('Manage AI settings');
113
+
114
+ aiConfigCmd
115
+ .command('enable')
116
+ .description('Enable AI features')
117
+ .action(async () => {
118
+ const current = getCredentials();
119
+ let key = current.aiKey;
120
+
121
+ if (!key) {
122
+ const response = await enquirer.prompt({
123
+ type: 'password',
124
+ name: 'aiKey',
125
+ message: 'Enter AI API Key:'
126
+ });
127
+ key = response.aiKey;
128
+ }
129
+
130
+ setCredentials({ aiEnabled: true, aiKey: key });
131
+ console.log(chalk.green('AI features enabled!'));
132
+ });
133
+
134
+ aiConfigCmd
135
+ .command('disable')
136
+ .description('Disable AI features')
137
+ .action(() => {
138
+ setCredentials({ aiEnabled: false });
139
+ console.log(chalk.yellow('AI features disabled.'));
140
+ });
141
+
142
+ aiConfigCmd
143
+ .command('status')
144
+ .description('Check AI feature status')
145
+ .action(() => {
146
+ const { aiEnabled, aiProvider } = getCredentials();
147
+ console.log(`AI Enabled: ${aiEnabled ? chalk.green('Yes') : chalk.red('No')}`);
148
+ console.log(`Provider: ${aiProvider || 'None'}`);
149
+ });
150
+
151
+ configCmd.addCommand(aiConfigCmd);
152
+
90
153
  program.addCommand(configCmd);
91
154
  }
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import { table } from 'table';
4
4
  import { api } from '../services/api-service.js';
5
5
  import ora from 'ora';
6
+ import { parseADF } from '../utils/adf-parser.js';
6
7
 
7
8
  export function registerIssueCommand(program) {
8
9
  const issueCmd = new Command('issue')
@@ -38,9 +39,9 @@ export function registerIssueCommand(program) {
38
39
 
39
40
  const searchApi = '/search/jql';
40
41
  const body = {
41
- jql: jql || '',
42
+ jql: jql || 'created is not empty',
42
43
  maxResults: parseInt(options.limit),
43
- fields: ['summary', 'status', 'assignee', 'created', 'updated']
44
+ fields: ['summary', 'status', 'assignee', 'created', 'updated', 'description']
44
45
  };
45
46
 
46
47
  const data = await api.post(searchApi, body);
@@ -84,7 +85,7 @@ export function registerIssueCommand(program) {
84
85
  }
85
86
 
86
87
  const tableData = [
87
- [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
88
+ [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Created'), chalk.bold('Updated')]
88
89
  ];
89
90
 
90
91
  data.issues.forEach(i => {
@@ -92,7 +93,9 @@ export function registerIssueCommand(program) {
92
93
  chalk.cyan(i.key),
93
94
  i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
94
95
  i.fields.status ? i.fields.status.name : '',
95
- i.fields.assignee ? i.fields.assignee.displayName : 'Unassigned'
96
+ i.fields.assignee ? i.fields.assignee.displayName : 'Unassigned',
97
+ i.fields.created ? i.fields.created.split('T')[0] : '',
98
+ i.fields.updated ? i.fields.updated.split('T')[0] : ''
96
99
  ]);
97
100
  });
98
101
 
@@ -108,5 +111,45 @@ export function registerIssueCommand(program) {
108
111
  }
109
112
  });
110
113
 
114
+ issueCmd
115
+ .command('view')
116
+ .description('View issue details')
117
+ .argument('<issueKey>', 'Issue Key')
118
+ .action(async (issueKey) => {
119
+ const spinner = ora(`Fetching issue ${issueKey}...`).start();
120
+ try {
121
+ const issue = await api.get(`/issue/${issueKey}`);
122
+ spinner.stop();
123
+
124
+ console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
125
+ console.log(chalk.grey(`${issue.fields.issuetype.name} - ${issue.fields.status.name} - ${issue.fields.priority ? issue.fields.priority.name : 'No Priority'}`));
126
+ console.log(chalk.bold('\nDescription:'));
127
+ console.log(parseADF(issue.fields.description) || 'No description provided.');
128
+
129
+ if (issue.fields.assignee) {
130
+ console.log(chalk.bold('\nAssignee: ') + issue.fields.assignee.displayName);
131
+ }
132
+
133
+ if (issue.fields.comment && issue.fields.comment.comments.length > 0) {
134
+ console.log(chalk.bold('\nComments:'));
135
+ issue.fields.comment.comments.forEach(c => {
136
+ console.log(chalk.cyan(c.author.displayName) + ': ' + c.body);
137
+ });
138
+ }
139
+ console.log('');
140
+ } catch (e) {
141
+ spinner.fail('Failed to fetch issue');
142
+ if (e.response) {
143
+ if (e.response.status === 404) {
144
+ console.error(chalk.red(`Issue "${issueKey}" not found.`));
145
+ } else {
146
+ console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
147
+ }
148
+ } else {
149
+ console.error(chalk.red(e.message));
150
+ }
151
+ }
152
+ });
153
+
111
154
  program.addCommand(issueCmd);
112
155
  }
@@ -37,7 +37,41 @@ export function registerSprintCommand(program) {
37
37
  // Using full path:
38
38
  const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
39
39
  const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
40
- const fullUrl = `${domain}/rest/agile/1.0/board/${options.board}/sprint?state=${options.state}`;
40
+
41
+ let boardId = options.board;
42
+
43
+ // If board option is not a number, try to look it up using the Board Name/Key
44
+ if (isNaN(boardId)) {
45
+ spinner.text = `Looking up board "${options.board}"...`;
46
+ const boardSearchUrl = `${domain}/rest/agile/1.0/board?name=${encodeURIComponent(options.board)}`;
47
+ const boardData = await api.get(boardSearchUrl);
48
+
49
+ if (!boardData.values || boardData.values.length === 0) {
50
+ // Fallback: It might be a project key. Let's try searching for boards associated with this project.
51
+ // But the API doesn't support projectKey filter directly on /board easily without iterating.
52
+ // For now, fail if name match doesn't work.
53
+ throw new Error(`Board with name "${options.board}" not found. Please provide the numeric Board ID.`);
54
+ }
55
+
56
+ // Strict match or pick first? Let's pick the first one but warn if multiple
57
+ if (boardData.values.length > 1) {
58
+ // Try to find exact match
59
+ const exact = boardData.values.find(b => b.name.toLowerCase() === options.board.toLowerCase());
60
+ if (exact) {
61
+ boardId = exact.id;
62
+ } else {
63
+ // Just pick first? Or error?
64
+ // Let's pick first but log
65
+ console.log(chalk.yellow(`\nMultiple boards found for "${options.board}". Using "${boardData.values[0].name}" (ID: ${boardData.values[0].id}).`));
66
+ boardId = boardData.values[0].id;
67
+ }
68
+ } else {
69
+ boardId = boardData.values[0].id;
70
+ }
71
+ spinner.text = `Fetching sprints for board ${options.board} (ID: ${boardId})...`;
72
+ }
73
+
74
+ const fullUrl = `${domain}/rest/agile/1.0/board/${boardId}/sprint?state=${options.state}`;
41
75
 
42
76
  const data = await api.get(fullUrl);
43
77
  spinner.stop();
@@ -65,7 +99,12 @@ export function registerSprintCommand(program) {
65
99
  } catch (e) {
66
100
  spinner.fail('Failed to list sprints');
67
101
  if (e.response) {
68
- console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
102
+ if (e.response.status === 404) {
103
+ console.error(chalk.red(`\nError: Board with ID "${options.board}" not found or you do not have permission to view it.`));
104
+ console.error(chalk.grey('Tip: Verify the Board ID in your Jira URL: /jira/software/c/projects/KEY/boards/ID'));
105
+ } else {
106
+ console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
107
+ }
69
108
  } else {
70
109
  console.error(chalk.red(e.message));
71
110
  }
@@ -5,10 +5,14 @@ export class AiService {
5
5
  constructor() { }
6
6
 
7
7
  async generate(prompt) {
8
- const { aiKey, aiProvider } = getCredentials();
8
+ const { aiKey, aiProvider, aiEnabled } = getCredentials();
9
+
10
+ if (!aiEnabled) {
11
+ throw new Error('AI features are disabled. Run "jira config ai enable" to enable.');
12
+ }
9
13
 
10
14
  if (!aiKey) {
11
- throw new Error('AI API Key not configured. Run "jira config setup" or manually set it in config.');
15
+ throw new Error('AI API Key not configured. Run "jira config ai enable" or "jira config setup".');
12
16
  }
13
17
 
14
18
  // Basic implementation for OpenAI - extensible for others
@@ -0,0 +1,49 @@
1
+ export function parseADF(content) {
2
+ if (!content) return '';
3
+ if (typeof content === 'string') return content;
4
+
5
+ if (content.type === 'doc') {
6
+ return content.content.map(node => parseNode(node)).join('\n');
7
+ }
8
+
9
+ return JSON.stringify(content);
10
+ }
11
+
12
+ function parseNode(node) {
13
+ if (!node) return '';
14
+
15
+ switch (node.type) {
16
+ case 'paragraph':
17
+ return parseParagraph(node);
18
+ case 'text':
19
+ return node.text;
20
+ case 'bulletList':
21
+ return parseList(node, '•');
22
+ case 'orderedList':
23
+ return parseList(node, '1.');
24
+ case 'heading':
25
+ return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map(c => parseNode(c)).join('')}\n`;
26
+ case 'codeBlock':
27
+ return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map(c => c.text).join('')}\n\`\`\`\n`;
28
+ case 'blockquote':
29
+ return `> ${node.content.map(c => parseNode(c)).join('')}`;
30
+ default:
31
+ if (node.content) {
32
+ return node.content.map(c => parseNode(c)).join('');
33
+ }
34
+ return ''; // Unknown node, skip or fallback
35
+ }
36
+ }
37
+
38
+ function parseParagraph(node) {
39
+ if (!node.content) return '\n';
40
+ return node.content.map(c => parseNode(c)).join('') + '\n';
41
+ }
42
+
43
+ function parseList(node, marker) {
44
+ if (!node.content) return '';
45
+ return node.content.map((item, index) => {
46
+ const prefix = marker === '1.' ? `${index + 1}. ` : `${marker} `;
47
+ return `${prefix}${item.content.map(c => parseNode(c)).join('')}`;
48
+ }).join('\n') + '\n';
49
+ }
@@ -18,6 +18,10 @@ const schema = {
18
18
  aiProvider: {
19
19
  type: 'string',
20
20
  default: 'openai'
21
+ },
22
+ aiEnabled: {
23
+ type: 'boolean',
24
+ default: false
21
25
  }
22
26
  };
23
27
 
@@ -32,16 +36,18 @@ export const getCredentials = () => {
32
36
  email: config.get('email'),
33
37
  apiToken: config.get('apiToken'),
34
38
  aiKey: config.get('aiKey'),
35
- aiProvider: config.get('aiProvider')
39
+ aiProvider: config.get('aiProvider'),
40
+ aiEnabled: config.get('aiEnabled')
36
41
  };
37
42
  };
38
43
 
39
- export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider }) => {
44
+ export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider, aiEnabled }) => {
40
45
  if (jiraUrl) config.set('jiraUrl', jiraUrl);
41
46
  if (email) config.set('email', email);
42
47
  if (apiToken) config.set('apiToken', apiToken);
43
48
  if (aiKey) config.set('aiKey', aiKey);
44
49
  if (aiProvider) config.set('aiProvider', aiProvider);
50
+ if (typeof aiEnabled !== 'undefined') config.set('aiEnabled', aiEnabled);
45
51
  };
46
52
 
47
53
  export const clearCredentials = () => {