jira-pilot 2.0.1 → 2.0.3

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/bin/jira.js CHANGED
@@ -1,56 +1,63 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import chalk from 'chalk';
4
- import { readFileSync } from 'fs';
5
- import { join, dirname } from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- // Load package.json for version
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name('jira')
16
- .description('AI-powered Jira CLI for humans and agents')
17
- .version(pkg.version)
18
- .addHelpText('after', `
19
- Examples:
20
- $ jira issue list
21
- $ jira issue view PROJ-123
22
- $ jira issue create
23
- $ jira board list
24
- $ jira sprint list --board 123
25
- $ jira ai summarize PROJ-123
26
- `);
27
-
28
- import { registerConfigCommand } from '../src/commands/config.js';
29
- import { registerIssueCommand } from '../src/commands/issue.js';
30
- import { registerProjectCommand } from '../src/commands/project.js';
31
- import { registerSprintCommand } from '../src/commands/sprint.js';
32
- import { registerBoardCommand } from '../src/commands/board.js';
33
- import { registerGitCommand } from '../src/commands/git.js';
34
- import { registerAiCommand } from '../src/commands/ai.js';
35
- import { registerMcpCommand } from '../src/commands/mcp.js';
36
-
37
- // Register Commands
38
- registerConfigCommand(program);
39
- registerIssueCommand(program);
40
- registerProjectCommand(program);
41
- registerSprintCommand(program);
42
- registerBoardCommand(program);
43
- registerGitCommand(program);
44
- registerAiCommand(program);
45
- registerMcpCommand(program);
46
-
47
- program.on('command:*', () => {
48
- console.error(chalk.red('Invalid command: %s\nSee --help for a list of available commands.'), program.args.join(' '));
49
- process.exit(1);
50
- });
51
-
52
- if (!process.argv.slice(2).length) {
53
- program.outputHelp();
54
- }
55
-
56
- program.parse(process.argv);
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { readFileSync } from 'fs';
5
+ import { join, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ // Load package.json for version
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('jira')
16
+ .description('AI-powered Jira CLI for humans and agents')
17
+ .version(pkg.version)
18
+ .addHelpText('after', `
19
+ Examples:
20
+ $ jira dashboard
21
+ $ jira issue list
22
+ $ jira issue view PROJ-123
23
+ $ jira issue create
24
+ $ jira issue search "login bug"
25
+ $ jira board list
26
+ $ jira sprint list --board 123
27
+ $ jira bulk transition -j "project = PROJ" -s Done
28
+ $ jira ai summarize PROJ-123
29
+ `);
30
+
31
+ import { registerConfigCommand } from '../src/commands/config.js';
32
+ import { registerIssueCommand } from '../src/commands/issue.js';
33
+ import { registerProjectCommand } from '../src/commands/project.js';
34
+ import { registerSprintCommand } from '../src/commands/sprint.js';
35
+ import { registerBoardCommand } from '../src/commands/board.js';
36
+ import { registerGitCommand } from '../src/commands/git.js';
37
+ import { registerAiCommand } from '../src/commands/ai.js';
38
+ import { registerMcpCommand } from '../src/commands/mcp.js';
39
+ import { registerBulkCommand } from '../src/commands/bulk.js';
40
+ import { registerDashboardCommand } from '../src/commands/dashboard.js';
41
+
42
+ // Register Commands
43
+ registerConfigCommand(program);
44
+ registerIssueCommand(program);
45
+ registerProjectCommand(program);
46
+ registerSprintCommand(program);
47
+ registerBoardCommand(program);
48
+ registerGitCommand(program);
49
+ registerAiCommand(program);
50
+ registerMcpCommand(program);
51
+ registerBulkCommand(program);
52
+ registerDashboardCommand(program);
53
+
54
+ program.on('command:*', () => {
55
+ console.error(chalk.red('Invalid command: %s\nSee --help for a list of available commands.'), program.args.join(' '));
56
+ process.exit(1);
57
+ });
58
+
59
+ if (!process.argv.slice(2).length) {
60
+ program.outputHelp();
61
+ }
62
+
63
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,91 +1,91 @@
1
- {
2
- "name": "jira-pilot",
3
- "version": "2.0.1",
4
- "description": "AI-powered Jira CLI for humans and agents — manage issues, sprints, boards with interactive wizards, multi-provider AI (OpenAI/Gemini/Anthropic), and an 8-tool MCP server for AI assistants",
5
- "main": "src/index.js",
6
- "type": "module",
7
- "bin": {
8
- "jira": "./bin/jira.js",
9
- "jira-pilot": "./bin/jira.js"
10
- },
11
- "scripts": {
12
- "start": "node bin/jira.js",
13
- "mcp": "node bin/jira.js mcp",
14
- "test": "vitest run",
15
- "test:watch": "vitest",
16
- "test:coverage": "vitest run --coverage",
17
- "link": "npm link"
18
- },
19
- "engines": {
20
- "node": ">=18.0.0"
21
- },
22
- "files": [
23
- "bin/",
24
- "src/",
25
- "README.md",
26
- "LICENSE"
27
- ],
28
- "keywords": [
29
- "jira",
30
- "cli",
31
- "jira-cli",
32
- "jira-client",
33
- "jira-api",
34
- "atlassian",
35
- "atlassian-jira",
36
- "ai",
37
- "ai-cli",
38
- "openai",
39
- "gemini",
40
- "anthropic",
41
- "claude",
42
- "gpt",
43
- "mcp",
44
- "model-context-protocol",
45
- "ai-agent",
46
- "agent",
47
- "issue-tracker",
48
- "project-management",
49
- "task-management",
50
- "sprint",
51
- "kanban",
52
- "scrum",
53
- "agile",
54
- "command-line",
55
- "interactive",
56
- "terminal",
57
- "developer-tools",
58
- "devtools",
59
- "productivity",
60
- "workflow",
61
- "git-integration"
62
- ],
63
- "author": {
64
- "name": "Arul",
65
- "url": "https://github.com/Aarul5"
66
- },
67
- "repository": {
68
- "type": "git",
69
- "url": "git+https://github.com/Aarul5/jira-pilot.git"
70
- },
71
- "bugs": {
72
- "url": "https://github.com/Aarul5/jira-pilot/issues"
73
- },
74
- "homepage": "https://github.com/Aarul5/jira-pilot#readme",
75
- "license": "ISC",
76
- "dependencies": {
77
- "@modelcontextprotocol/sdk": "^0.6.0",
78
- "axios": "^1.6.0",
79
- "chalk": "^5.3.0",
80
- "commander": "^11.1.0",
81
- "conf": "^12.0.0",
82
- "enquirer": "^2.4.1",
83
- "open": "^10.0.0",
84
- "ora": "^8.0.0",
85
- "table": "^6.8.0"
86
- },
87
- "devDependencies": {
88
- "@vitest/coverage-v8": "^4.0.18",
89
- "vitest": "^4.0.18"
90
- }
1
+ {
2
+ "name": "jira-pilot",
3
+ "version": "2.0.3",
4
+ "description": "AI-powered Jira CLI and MCP server for humans and agents — manage issues, sprints, boards with interactive wizards, multi-provider AI (OpenAI/Gemini/Anthropic), and an 8-tool MCP server for AI assistants",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "jira": "./bin/jira.js",
9
+ "jira-pilot": "./bin/jira.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node bin/jira.js",
13
+ "mcp": "node bin/jira.js mcp",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage",
17
+ "link": "npm link"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "files": [
23
+ "bin/",
24
+ "src/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "jira",
30
+ "cli",
31
+ "jira-cli",
32
+ "jira-client",
33
+ "jira-api",
34
+ "atlassian",
35
+ "atlassian-jira",
36
+ "ai",
37
+ "ai-cli",
38
+ "openai",
39
+ "gemini",
40
+ "anthropic",
41
+ "claude",
42
+ "gpt",
43
+ "mcp",
44
+ "model-context-protocol",
45
+ "ai-agent",
46
+ "agent",
47
+ "issue-tracker",
48
+ "project-management",
49
+ "task-management",
50
+ "sprint",
51
+ "kanban",
52
+ "scrum",
53
+ "agile",
54
+ "command-line",
55
+ "interactive",
56
+ "terminal",
57
+ "developer-tools",
58
+ "devtools",
59
+ "productivity",
60
+ "workflow",
61
+ "git-integration"
62
+ ],
63
+ "author": {
64
+ "name": "Arul",
65
+ "url": "https://github.com/Aarul5"
66
+ },
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/Aarul5/jira-pilot.git"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/Aarul5/jira-pilot/issues"
73
+ },
74
+ "homepage": "https://github.com/Aarul5/jira-pilot#readme",
75
+ "license": "ISC",
76
+ "dependencies": {
77
+ "@modelcontextprotocol/sdk": "^0.6.0",
78
+ "axios": "^1.6.0",
79
+ "chalk": "^5.3.0",
80
+ "commander": "^11.1.0",
81
+ "conf": "^12.0.0",
82
+ "enquirer": "^2.4.1",
83
+ "open": "^10.0.0",
84
+ "ora": "^8.0.0",
85
+ "table": "^6.8.0"
86
+ },
87
+ "devDependencies": {
88
+ "@vitest/coverage-v8": "^4.0.18",
89
+ "vitest": "^4.0.18"
90
+ }
91
91
  }
@@ -0,0 +1,119 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import enquirer from 'enquirer';
4
+ import { api } from '../../services/api-service.js';
5
+ import { aiService } from '../../services/ai-service.js';
6
+ import { validateIssueKey } from '../../utils/validators.js';
7
+ import { parseADF } from '../../utils/adf-parser.js';
8
+ import { handleCommandError } from '../../utils/error-handler.js';
9
+
10
+ export async function planAction(epicKey, options) {
11
+ const check = validateIssueKey(epicKey);
12
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
13
+
14
+ const spinner = ora(`Fetching Epic ${epicKey}...`).start();
15
+
16
+ try {
17
+ const issue = await api.get(`/issue/${epicKey}`);
18
+ const summary = issue.fields.summary;
19
+ const description = issue.fields.description ? parseADF(issue.fields.description) : 'No description';
20
+ const projectKey = issue.fields.project.key;
21
+
22
+ spinner.text = 'AI is breaking down the Epic...';
23
+
24
+ const plan = await aiService.breakdownEpic(summary, description);
25
+
26
+ spinner.stop();
27
+
28
+ if (!plan || plan.length === 0) {
29
+ console.log(chalk.yellow('AI could not generate a plan.'));
30
+ return;
31
+ }
32
+
33
+ console.log(chalk.cyan(`\nProposed Breakdown for ${epicKey} (${summary}):\n`));
34
+
35
+ // Let user select items to create
36
+ const choices = plan.map((item, index) => ({
37
+ name: `${item.type}: ${item.summary}`,
38
+ value: index, // store index to retrieve item
39
+ checked: true
40
+ }));
41
+
42
+ const { selectedIndices } = await enquirer.prompt({
43
+ type: 'multiselect',
44
+ name: 'selectedIndices',
45
+ message: 'Select issues to create:',
46
+ choices: choices.map((c, i) => ({ ...c, value: i })), // ensure value is index
47
+ result(names) {
48
+ // map names back to indices
49
+ return names.map(name => this.map(name)); // 'this.map' returns value (index)
50
+ }
51
+ });
52
+
53
+ // Loop through selected and create
54
+ // Convert map result (object/array) to array of indices
55
+ const indicesToCreate = Object.values(selectedIndices);
56
+
57
+ if (indicesToCreate.length === 0) {
58
+ console.log(chalk.yellow('No items selected.'));
59
+ return;
60
+ }
61
+
62
+ console.log(chalk.dim('\nCreating issues...'));
63
+
64
+ const results = [];
65
+ for (const idx of indicesToCreate) {
66
+ const item = plan[idx];
67
+ const itemSpinner = ora(`Creating ${item.type}: ${item.summary}`).start();
68
+
69
+ try {
70
+ const payload = {
71
+ fields: {
72
+ project: { key: projectKey },
73
+ summary: item.summary,
74
+ description: item.description, // Simple string, Jira converts to ADF or accepts text depending on config.
75
+ // Note: v3 API often needs ADF. If description is simple text, it might fail.
76
+ // We should construct a basic paragraph ADF document.
77
+ description: {
78
+ version: 1,
79
+ type: 'doc',
80
+ content: [{
81
+ type: 'paragraph',
82
+ content: [{ type: 'text', text: item.description || '' }]
83
+ }]
84
+ },
85
+ issuetype: { name: item.type },
86
+ parent: { key: epicKey } // Try to link to Epic
87
+ }
88
+ };
89
+
90
+ const res = await api.post('/issue', payload);
91
+ itemSpinner.succeed(`${res.key} created.`);
92
+ results.push(res.key);
93
+ } catch (e) {
94
+ itemSpinner.fail(`Failed to create ${item.summary}.`);
95
+ // Fallback: try without description if ADF error
96
+ try {
97
+ const payloadNoDesc = {
98
+ fields: {
99
+ project: { key: projectKey },
100
+ summary: item.summary,
101
+ issuetype: { name: item.type },
102
+ parent: { key: epicKey }
103
+ }
104
+ };
105
+ const res = await api.post('/issue', payloadNoDesc);
106
+ itemSpinner.succeed(`${res.key} created (without description).`);
107
+ results.push(res.key);
108
+ } catch (e2) {
109
+ // ignore
110
+ }
111
+ }
112
+ }
113
+
114
+ console.log(chalk.green(`\nDone! Created ${results.length} issues linked to ${epicKey}.`));
115
+
116
+ } catch (e) {
117
+ handleCommandError(spinner, e, `Failed to plan ${epicKey}`);
118
+ }
119
+ }
@@ -0,0 +1,102 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import axios from 'axios';
4
+ import { execSync } from 'child_process';
5
+ import { api } from '../../services/api-service.js';
6
+ import { aiService } from '../../services/ai-service.js';
7
+ import { validateIssueKey } from '../../utils/validators.js';
8
+ import { getCredentials } from '../../utils/config.js';
9
+ import { parseADF } from '../../utils/adf-parser.js';
10
+ import { handleCommandError } from '../../utils/error-handler.js';
11
+
12
+ export async function reviewAction(issueKey, options) {
13
+ const check = validateIssueKey(issueKey);
14
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
15
+
16
+ const { githubToken } = getCredentials();
17
+ if (!githubToken) {
18
+ console.error(chalk.red('GitHub Token not found. Run "jira config setup" or manually add githubToken to config.'));
19
+ return;
20
+ }
21
+
22
+ const spinner = ora(`Fetching issue ${issueKey} and searching for PRs...`).start();
23
+
24
+ try {
25
+ // 1. Fetch Issue Context
26
+ const issue = await api.get(`/issue/${issueKey}?fields=summary,description,acceptanceCriteria`); // basic fields
27
+ const summary = issue.fields.summary;
28
+ const description = issue.fields.description ? parseADF(issue.fields.description) : 'No description';
29
+ const context = `Title: ${summary}\nDescription: ${description}`;
30
+
31
+ // 2. Determine GitHub Repo from local git
32
+ let repoOwner, repoName;
33
+ try {
34
+ const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
35
+ // Parse: https://github.com/owner/repo.git or git@github.com:owner/repo.git
36
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)/);
37
+ if (match) {
38
+ repoOwner = match[1];
39
+ repoName = match[2];
40
+ }
41
+ } catch (e) {
42
+ // Ignore, maybe not in a repo
43
+ }
44
+
45
+ if (!repoOwner || !repoName) {
46
+ spinner.fail('Could not detect GitHub repository from "git remote get-url origin".');
47
+ console.log(chalk.yellow('Ensure you are in the git repository folder.'));
48
+ return;
49
+ }
50
+
51
+ spinner.text = `Searching PRs in ${repoOwner}/${repoName} for ${issueKey}...`;
52
+
53
+ // 3. Search GitHub PRs
54
+ // We search for PRs that mention the issue key in title or branch name
55
+ const searchRes = await axios.get(`https://api.github.com/search/issues`, {
56
+ params: {
57
+ q: `repo:${repoOwner}/${repoName} is:pr ${issueKey}`
58
+ },
59
+ headers: {
60
+ 'Authorization': `token ${githubToken}`,
61
+ 'Accept': 'application/vnd.github.v3+json'
62
+ }
63
+ });
64
+
65
+ const prs = searchRes.data.items;
66
+ if (prs.length === 0) {
67
+ spinner.fail(`No open PRs found for ${issueKey} in ${repoOwner}/${repoName}.`);
68
+ return;
69
+ }
70
+
71
+ // Use the most recent PR
72
+ const pr = prs[0];
73
+ spinner.text = `Analyzing PR #${pr.number}: ${pr.title}...`;
74
+
75
+ // 4. Fetch Diff
76
+ const diffRes = await axios.get(pr.pull_request.url, {
77
+ headers: {
78
+ 'Authorization': `token ${githubToken}`,
79
+ 'Accept': 'application/vnd.github.v3.diff'
80
+ }
81
+ });
82
+ const diff = diffRes.data;
83
+
84
+ if (!diff) {
85
+ spinner.fail('Empty diff or failed to fetch diff.');
86
+ return;
87
+ }
88
+
89
+ // 5. AI Review
90
+ spinner.text = 'AI is reviewing the code changes...';
91
+ const review = await aiService.reviewCode(diff, context);
92
+
93
+ spinner.stop();
94
+
95
+ console.log(chalk.green(`\nšŸ¤– AI Code Review for PR #${pr.number} (${issueKey}):\n`));
96
+ console.log(review);
97
+ console.log(chalk.dim(`\nPR Link: ${pr.html_url}`));
98
+
99
+ } catch (e) {
100
+ handleCommandError(spinner, e, `Failed to review ${issueKey}`);
101
+ }
102
+ }
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { api } from '../../services/api-service.js';
4
+ import { aiService } from '../../services/ai-service.js';
5
+ import { parseADF } from '../../utils/adf-parser.js';
6
+ import { handleCommandError } from '../../utils/error-handler.js';
7
+
8
+ export async function standupAction(options) {
9
+ const spinner = ora('Analyzing your recent activity...').start();
10
+
11
+ try {
12
+ // 1. Fetch User Info
13
+ const myself = await api.get('/myself');
14
+ const accountId = myself.accountId;
15
+ const displayName = myself.displayName;
16
+
17
+ // 2. Fetch Issues Updated/Commented by User in last 24h
18
+ // JQL: updated >= -24h AND (assignee = currentUser() OR watcher = currentUser() OR comment ~ currentUser())
19
+ // Simplification: JQL: assignee = currentUser() AND updated >= -1d
20
+ const yesterdayJql = `assignee = currentUser() AND updated >= -1d ORDER BY updated DESC`;
21
+
22
+ const yesterdayIssuesRes = await api.get(`/search?jql=${encodeURIComponent(yesterdayJql)}&fields=summary,status,comment,worklog`);
23
+ const yesterdayIssues = yesterdayIssuesRes.issues.map(i => `- ${i.key} ${i.fields.summary} (${i.fields.status.name})`).join('\n');
24
+
25
+ // 3. Fetch Issues Assigned for Today (In Progress or To Do)
26
+ const todayJql = `assignee = currentUser() AND statusCategory IN ("To Do", "In Progress") ORDER BY priority DESC`;
27
+ const todayIssuesRes = await api.get(`/search?jql=${encodeURIComponent(todayJql)}&fields=summary,status,priority`);
28
+ const todayIssues = todayIssuesRes.issues.map(i => `- ${i.key} ${i.fields.summary} [${i.fields.priority.name}]`).join('\n');
29
+
30
+ spinner.text = 'Generating standup report...';
31
+
32
+ const report = await aiService.generateStandup(yesterdayIssues, todayIssues);
33
+
34
+ spinner.stop();
35
+
36
+ console.log(chalk.green(`\nšŸ“¢ Standup Report for ${displayName}:\n`));
37
+ console.log(report);
38
+
39
+ } catch (e) {
40
+ handleCommandError(spinner, e, 'Failed to generate standup');
41
+ }
42
+ }