jira-pilot 1.0.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/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2026
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Jira Pilot ✈️
2
+
3
+ **The AI-Powered Jira CLI for Humans and Agents.**
4
+
5
+ `jira-pilot` is a next-generation Command Line Interface for Jira. It bridges the gap between traditional developer tools and modern AI Agents.
6
+
7
+ - **For Humans:** A beautiful, interactive CLI to manage issues, sprints, and code without leaving your terminal.
8
+ - **For Agents:** A fully compliant **Model Context Protocol (MCP)** server that lets AI assistants (like Claude Desktop (Claude Desktop) or Gemini) interact with your Jira instance safely.
9
+
10
+ ## ✨ Features
11
+
12
+ ### 👤 Human-Centric Features
13
+ - **Interactive Wizards**: Create and transition issues with `enquirer` prompts. No more remembering complex flags.
14
+ - **Git Integration**: Create feature branches directly from issues with smart naming.
15
+ - `jira git branch PROJ-123` -> `feature/PROJ-123-fix-login-bug`
16
+ - **Rich Visualization**: Beautiful tables and formatted output.
17
+ - **AI Copilot**:
18
+ - `jira ai summarize PROJ-123`: Get a TL;DR of long issue threads.
19
+ - `jira ai draft`: Draft descriptions from bullet points (Coming Soon).
20
+
21
+ ### 🤖 Agentic Features (MCP)
22
+ - **Agent Skill**: Run `jira mcp` to start a stdio server.
23
+ - **Standardized Tools**: Exposes `list_issues`, `get_issue`, `create_issue` to any MCP client.
24
+ - **Low-Context Mode**: Optimized JSON outputs for LLM consumption.
25
+
26
+ ---
27
+
28
+ ## 🚀 Installation
29
+
30
+ ### Global Install (Recommended)
31
+ ```bash
32
+ npm install -g jira-pilot
33
+ ```
34
+
35
+ ### Local Development
36
+ ```bash
37
+ git clone https://github.com/yourusername/jira-pilot.git
38
+ cd jira-pilot
39
+ npm install
40
+ npm link
41
+ ```
42
+
43
+ ---
44
+
45
+ ## ⚙️ Configuration
46
+
47
+ Before using the tool, set up your credentials. You can get an API Token from [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens).
48
+
49
+ ```bash
50
+ jira config setup
51
+ ```
52
+
53
+ You will be prompted for:
54
+ 1. **Jira Site URL**: e.g., `https://your-company.atlassian.net`
55
+ 2. **Email**: Your Atlassian account email.
56
+ 3. **API Token**: The token you generated.
57
+ 4. **AI API Key (Optional)**: Your OpenAI API Key (for `jira ai` commands).
58
+
59
+ To view current config:
60
+ ```bash
61
+ jira config view
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 📖 Usage
67
+
68
+ ### Issues
69
+ ```bash
70
+ # List issues (default: assigned to you, active sprints)
71
+ jira issue list
72
+
73
+ # List with custom JQL
74
+ jira issue list --jql "project = PROJ AND priority = High"
75
+
76
+ # Create a new issue (interactive)
77
+ jira issue create
78
+
79
+ # View details
80
+ jira issue view PROJ-123
81
+
82
+ # Transition status (interactive)
83
+ jira issue transition PROJ-123
84
+
85
+ # Export issues to file
86
+ jira issue list --export json # Creates issues-TIMESTAMP.json
87
+ jira issue list --export md # Creates issues-TIMESTAMP.md
88
+
89
+ # Combine filters and export (Power User)
90
+ jira issue list --project TRAIN --assignee <assignee_name> --status Done --export json
91
+ ```
92
+
93
+ ### Projects & Sprints
94
+ ```bash
95
+ # List projects
96
+ jira project list
97
+
98
+ # List sprints for a board
99
+ jira sprint list --board 5
100
+ ```
101
+
102
+ ### Git Integration
103
+ Create a branch automatically named from the issue summary:
104
+ ```bash
105
+ jira git branch PROJ-123
106
+ # Output: Switched to a new branch 'feature/PROJ-123-fix-login-modal'
107
+ ```
108
+
109
+ ### AI Features
110
+ Summarize a complex issue thread:
111
+ ```bash
112
+ jira ai summarize PROJ-123
113
+ ```
114
+ *(Requires OpenAI Key in config)*
115
+
116
+ ---
117
+
118
+ ## 🧠 Using with AI Agents (Claude/Gemini)
119
+
120
+ `jira-pilot` implements the **Model Context Protocol (MCP)**, making it plug-and-play for AI assistants.
121
+
122
+ ### Claude Desktop Configuration
123
+ Add the following to your `claude_desktop_config.json`:
124
+
125
+ ```json
126
+ {
127
+ "mcpServers": {
128
+ "jira": {
129
+ "command": "node",
130
+ "args": ["/absolute/path/to/jira-pilot/bin/jira.js", "mcp"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Once connected, you can ask Claude things like:
137
+ > "Check my assigned Jira issues and create a feature branch for the highest priority one."
138
+
139
+ ---
140
+
141
+ ## 🛠️ Project Structure
142
+ - `bin/`: Entry point.
143
+ - `src/commands/`: CLI command definitions (Human UI).
144
+ - `src/server/`: MCP Server implementation (Agent UI).
145
+ - `src/services/`: Core logic (API, AI).
146
+
147
+ ## 📄 License
148
+ ISC
package/bin/jira.js ADDED
@@ -0,0 +1,45 @@
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
+
19
+ import { registerConfigCommand } from '../src/commands/config.js';
20
+ import { registerIssueCommand } from '../src/commands/issue.js';
21
+ import { registerProjectCommand } from '../src/commands/project.js';
22
+ import { registerSprintCommand } from '../src/commands/sprint.js';
23
+ import { registerGitCommand } from '../src/commands/git.js';
24
+ import { registerAiCommand } from '../src/commands/ai.js';
25
+ import { registerMcpCommand } from '../src/commands/mcp.js';
26
+
27
+ // Register Commands
28
+ registerConfigCommand(program);
29
+ registerIssueCommand(program);
30
+ registerProjectCommand(program);
31
+ registerSprintCommand(program);
32
+ registerGitCommand(program);
33
+ registerAiCommand(program);
34
+ registerMcpCommand(program);
35
+
36
+ program.on('command:*', () => {
37
+ console.error(chalk.red('Invalid command: %s\nSee --help for a list of available commands.'), program.args.join(' '));
38
+ process.exit(1);
39
+ });
40
+
41
+ if (!process.argv.slice(2).length) {
42
+ program.outputHelp();
43
+ }
44
+
45
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "jira-pilot",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered Jira CLI for humans and agents",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "jira": "./bin/jira.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1",
12
+ "link": "npm link"
13
+ },
14
+ "keywords": [
15
+ "jira",
16
+ "cli",
17
+ "ai",
18
+ "agent",
19
+ "mcp"
20
+ ],
21
+ "author": "Arul",
22
+ "license": "ISC",
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^0.6.0",
25
+ "axios": "^1.6.0",
26
+ "chalk": "^5.3.0",
27
+ "commander": "^11.1.0",
28
+ "conf": "^12.0.0",
29
+ "enquirer": "^2.4.1",
30
+ "open": "^10.0.0",
31
+ "ora": "^8.0.0",
32
+ "table": "^6.8.0"
33
+ }
34
+ }
@@ -0,0 +1,51 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { api } from '../services/api-service.js';
5
+ import { aiService } from '../services/ai-service.js';
6
+
7
+ export function registerAiCommand(program) {
8
+ const aiCmd = new Command('ai')
9
+ .description('AI Helper commands');
10
+
11
+ aiCmd
12
+ .command('summarize')
13
+ .description('Summarize an issue using AI')
14
+ .argument('<issueKey>', 'Jira Issue Key')
15
+ .action(async (issueKey) => {
16
+ const spinner = ora(`Fetching issue ${issueKey}...`).start();
17
+ try {
18
+ // Fetch issue details and comments
19
+ const issue = await api.get(`/issue/${issueKey}?fields=summary,description,comment`);
20
+ spinner.text = 'Generating summary...';
21
+
22
+ const summary = issue.fields.summary;
23
+ const description = issue.fields.description || 'No description';
24
+ const comments = issue.fields.comment.comments.map(c => `${c.author.displayName}: ${c.body}`).join('\n');
25
+
26
+ const prompt = `
27
+ You are a helpful Jira assistant. Please summarize the following Jira issue.
28
+
29
+ Title: ${summary}
30
+ Description: ${description}
31
+
32
+ Comments:
33
+ ${comments}
34
+
35
+ Provide a concise summary of the current status, key discussion points, and next steps if clear.
36
+ `;
37
+
38
+ const aiResponse = await aiService.generate(prompt);
39
+ spinner.stop();
40
+
41
+ console.log(chalk.green(`\n🤖 AI Summary for ${issueKey}:\n`));
42
+ console.log(aiResponse);
43
+
44
+ } catch (e) {
45
+ spinner.fail('Failed to generate summary');
46
+ console.error(chalk.red(e.message));
47
+ }
48
+ });
49
+
50
+ program.addCommand(aiCmd);
51
+ }
@@ -0,0 +1,91 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import enquirer from 'enquirer';
4
+ import { setCredentials, getCredentials, clearCredentials } from '../utils/config.js';
5
+ import ora from 'ora';
6
+ import { api } from '../services/api-service.js';
7
+
8
+ export function registerConfigCommand(program) {
9
+ const configCmd = new Command('config')
10
+ .description('Configure Jira credentials');
11
+
12
+ configCmd
13
+ .command('setup')
14
+ .description('Interactive setup of Jira credentials')
15
+ .action(async () => {
16
+ console.log(chalk.blue('Configuring jira-pilot...'));
17
+
18
+ const current = getCredentials();
19
+
20
+ try {
21
+ const answers = await enquirer.prompt([
22
+ {
23
+ type: 'input',
24
+ name: 'jiraUrl',
25
+ message: 'Jira Site URL (e.g., https://your-domain.atlassian.net):',
26
+ initial: current.jiraUrl
27
+ },
28
+ {
29
+ type: 'input',
30
+ name: 'email',
31
+ message: 'Jira Email Address:',
32
+ initial: current.email
33
+ },
34
+ {
35
+ type: 'password',
36
+ name: 'apiToken',
37
+ message: 'Jira API Token:',
38
+ initial: current.apiToken ? '*****' : undefined
39
+ }
40
+ ]);
41
+
42
+ // Keep existing token if user didn't change it (and entered ***** which is not real)
43
+ // Actually prompt returns text. If they leave it blank?
44
+ // Let's assume if they type nothing, we keep old? Enquirer behavior depends.
45
+ // Better to just save what we get.
46
+
47
+ // Validation check
48
+ const spinner = ora('Verifying credentials...').start();
49
+
50
+ // Temporarily set config to test
51
+ setCredentials(answers);
52
+ api.init(); // Refresh api client with new creds
53
+
54
+ try {
55
+ await api.get('/myself');
56
+ spinner.succeed(chalk.green('Credentials verified and saved!'));
57
+ } catch (e) {
58
+ spinner.fail(chalk.red('Verification failed! Credentials saved but might be incorrect.'));
59
+ console.error(e.message);
60
+ }
61
+
62
+ } catch (e) {
63
+ console.error(chalk.red('Setup cancelled or failed'), e);
64
+ }
65
+ });
66
+
67
+ configCmd
68
+ .command('view')
69
+ .description('View current configuration')
70
+ .action(() => {
71
+ const { jiraUrl, email } = getCredentials();
72
+ if (jiraUrl) {
73
+ console.log(chalk.green('Current Configuration:'));
74
+ console.log(`URL: ${jiraUrl}`);
75
+ console.log(`Email: ${email}`);
76
+ console.log(`Token: ************`);
77
+ } else {
78
+ console.log(chalk.yellow('No configuration found. Run "jira config setup"'));
79
+ }
80
+ });
81
+
82
+ configCmd
83
+ .command('clear')
84
+ .description('Clear saved credentials')
85
+ .action(() => {
86
+ clearCredentials();
87
+ console.log(chalk.green('Credentials cleared.'));
88
+ });
89
+
90
+ program.addCommand(configCmd);
91
+ }
@@ -0,0 +1,60 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { execSync } from 'child_process';
4
+ import { api } from '../services/api-service.js';
5
+ import ora from 'ora';
6
+ import enquirer from 'enquirer';
7
+
8
+ export function registerGitCommand(program) {
9
+ const gitCmd = new Command('git')
10
+ .description('Git integration for Jira');
11
+
12
+ gitCmd
13
+ .command('branch')
14
+ .description('Create a git branch from a Jira issue')
15
+ .argument('<issueKey>', 'Jira Issue Key (e.g., PROJ-123)')
16
+ .option('-t, --type <type>', 'Branch type (feature, bugfix, hotfix)', 'feature')
17
+ .action(async (issueKey, options) => {
18
+ const spinner = ora(`Fetching issue ${issueKey}...`).start();
19
+ try {
20
+ const issue = await api.get(`/issue/${issueKey}`);
21
+ spinner.stop();
22
+
23
+ const summary = issue.fields.summary;
24
+ const sanitizedSummary = summary
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphen
27
+ .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
28
+
29
+ const branchName = `${options.type}/${issueKey}-${sanitizedSummary}`;
30
+
31
+ console.log(chalk.blue(`Proposed Branch Name: ${chalk.bold(branchName)}`));
32
+
33
+ const { confirm } = await enquirer.prompt({
34
+ type: 'confirm',
35
+ name: 'confirm',
36
+ message: 'Create and switch to this branch?',
37
+ initial: true
38
+ });
39
+
40
+ if (confirm) {
41
+ try {
42
+ execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
43
+ console.log(chalk.green('\nBranch created and checked out!'));
44
+ } catch (gitError) {
45
+ console.error(chalk.red('\nFailed to create branch. Are you in a git repository?'));
46
+ }
47
+ }
48
+
49
+ } catch (e) {
50
+ spinner.fail('Failed to fetch issue');
51
+ if (e.response) {
52
+ console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
53
+ } else {
54
+ console.error(chalk.red(e.message));
55
+ }
56
+ }
57
+ });
58
+
59
+ program.addCommand(gitCmd);
60
+ }
@@ -0,0 +1,112 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { table } from 'table';
4
+ import { api } from '../services/api-service.js';
5
+ import ora from 'ora';
6
+
7
+ export function registerIssueCommand(program) {
8
+ const issueCmd = new Command('issue')
9
+ .description('Manage Jira issues');
10
+
11
+ issueCmd
12
+ .command('list')
13
+ .description('List issues')
14
+ .option('-j, --jql <query>', 'JQL query to filter issues')
15
+ .option('-l, --limit <number>', 'Limit results', '20')
16
+ .option('-p, --project <key>', 'Filter by project')
17
+ .option('-a, --assignee <id>', 'Filter by assignee (use "currentUser" for self)')
18
+ .option('-s, --status <status>', 'Filter by status')
19
+ .option('-e, --export <format>', 'Export output (json, md)')
20
+ .action(async (options) => {
21
+ const spinner = ora('Fetching issues...').start();
22
+ try {
23
+ const jqlParts = [];
24
+ if (options.project) jqlParts.push(`project = "${options.project}"`);
25
+ if (options.assignee) jqlParts.push(`assignee = ${options.assignee === 'currentUser' ? 'currentUser()' : `"${options.assignee}"`}`);
26
+ if (options.status) jqlParts.push(`status = "${options.status}"`);
27
+ if (options.jql) jqlParts.push(options.jql);
28
+
29
+ // Order by updated desc by default if no JQL
30
+ if (!options.jql && jqlParts.length === 0) {
31
+ jqlParts.push('order by updated DESC');
32
+ } else if (jqlParts.length > 0 && !options.jql) {
33
+ // Add order if not custom jql
34
+ // jqlParts.push('order by updated DESC');
35
+ }
36
+
37
+ const jql = jqlParts.join(' AND ');
38
+
39
+ const searchApi = '/search';
40
+ const body = {
41
+ jql: jql || '',
42
+ maxResults: parseInt(options.limit),
43
+ fields: ['summary', 'status', 'assignee', 'created', 'updated']
44
+ };
45
+
46
+ const data = await api.post(searchApi, body);
47
+ spinner.stop();
48
+
49
+ if (!data.issues || data.issues.length === 0) {
50
+ console.log(chalk.yellow('No issues found.'));
51
+ return;
52
+ }
53
+
54
+ // Handling Export
55
+ if (options.export) {
56
+ const fs = await import('fs');
57
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
58
+
59
+ if (options.export === 'json') {
60
+ const filename = `issues-${timestamp}.json`;
61
+ fs.writeFileSync(filename, JSON.stringify(data.issues, null, 2));
62
+ console.log(chalk.green(`\nExported ${data.issues.length} issues to ${chalk.bold(filename)}`));
63
+ return;
64
+ }
65
+
66
+ if (options.export === 'md') {
67
+ const filename = `issues-${timestamp}.md`;
68
+ let mdContent = `# Jira Issues Export\nGenerated: ${new Date().toLocaleString()}\n\n`;
69
+ mdContent += `| Key | Summary | Status | Assignee |\n`;
70
+ mdContent += `|---|---|---|---|\n`;
71
+
72
+ data.issues.forEach(i => {
73
+ const key = i.key;
74
+ const summary = i.fields.summary || '';
75
+ const status = i.fields.status?.name || '';
76
+ const assignee = i.fields.assignee?.displayName || 'Unassigned';
77
+ mdContent += `| ${key} | ${summary} | ${status} | ${assignee} |\n`;
78
+ });
79
+
80
+ fs.writeFileSync(filename, mdContent);
81
+ console.log(chalk.green(`\nExported ${data.issues.length} issues to ${chalk.bold(filename)}`));
82
+ return;
83
+ }
84
+ }
85
+
86
+ const tableData = [
87
+ [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
88
+ ];
89
+
90
+ data.issues.forEach(i => {
91
+ tableData.push([
92
+ chalk.cyan(i.key),
93
+ i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
94
+ i.fields.status ? i.fields.status.name : '',
95
+ i.fields.assignee ? i.fields.assignee.displayName : 'Unassigned'
96
+ ]);
97
+ });
98
+
99
+ console.log(table(tableData));
100
+
101
+ } catch (e) {
102
+ spinner.fail('Failed to list issues');
103
+ if (e.response) {
104
+ console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
105
+ } else {
106
+ console.error(chalk.red(e.message));
107
+ }
108
+ }
109
+ });
110
+
111
+ program.addCommand(issueCmd);
112
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { startServer } from '../server/mcp-server.js';
4
+
5
+ export function registerMcpCommand(program) {
6
+ const mcpCmd = new Command('mcp')
7
+ .description('Start MCP Agent Server (Stdio)')
8
+ .action(async () => {
9
+ // MCP server uses stdio, so we shouldn't log anything else to stdout.
10
+ // We can log to stderr if needed.
11
+ try {
12
+ await startServer();
13
+ } catch (e) {
14
+ console.error('MCP Server Error:', e);
15
+ process.exit(1);
16
+ }
17
+ });
18
+
19
+ program.addCommand(mcpCmd);
20
+ }
@@ -0,0 +1,46 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { table } from 'table';
4
+ import { api } from '../services/api-service.js';
5
+ import ora from 'ora';
6
+
7
+ export function registerProjectCommand(program) {
8
+ const projectCmd = new Command('project')
9
+ .description('Manage Jira projects');
10
+
11
+ projectCmd
12
+ .command('list')
13
+ .description('List accessible projects')
14
+ .action(async () => {
15
+ const spinner = ora('Fetching projects...').start();
16
+ try {
17
+ const data = await api.get('/project/search');
18
+ spinner.stop();
19
+
20
+ if (!data.values || data.values.length === 0) {
21
+ console.log(chalk.yellow('No projects found.'));
22
+ return;
23
+ }
24
+
25
+ const tableData = [
26
+ [chalk.bold('Key'), chalk.bold('Name'), chalk.bold('Leader'), chalk.bold('Style')]
27
+ ];
28
+
29
+ data.values.forEach(p => {
30
+ tableData.push([
31
+ chalk.cyan(p.key),
32
+ p.name,
33
+ p.lead ? p.lead.displayName : 'N/A',
34
+ p.style
35
+ ]);
36
+ });
37
+
38
+ console.log(table(tableData));
39
+ } catch (e) {
40
+ spinner.fail('Failed to list projects');
41
+ console.error(e.message);
42
+ }
43
+ });
44
+
45
+ program.addCommand(projectCmd);
46
+ }
@@ -0,0 +1,76 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { table } from 'table';
4
+ import { api } from '../services/api-service.js';
5
+ import ora from 'ora';
6
+
7
+ export function registerSprintCommand(program) {
8
+ const sprintCmd = new Command('sprint')
9
+ .description('Manage Sprints');
10
+
11
+ sprintCmd
12
+ .command('list')
13
+ .description('List sprints for a board')
14
+ .requiredOption('-b, --board <id>', 'Board ID')
15
+ .option('-s, --state <state>', 'State (active, future, closed)', 'active,future')
16
+ .action(async (options) => {
17
+ const spinner = ora(`Fetching sprints for board ${options.board}...`).start();
18
+ try {
19
+ // Agile API usually involves /rest/agile/1.0
20
+ // My default ApiService is /rest/api/3. I might need to override or allow full path?
21
+ // ApiService handles baseURL. I should make it flexible or add Agile support.
22
+
23
+ // HACK: ApiService constructor sets base to /rest/api/3.
24
+ // I need to use a different client or hack the URL.
25
+ // Axios allows absolute URLs to override baseURL.
26
+ // So if I pass full URL it works.
27
+
28
+ const { jiraUrl } = (await import('../utils/config.js')).getCredentials();
29
+ // Assuming api-service exposes client or get method.
30
+ // But get method prepend baseURL? No, axios usually supports absolute URL.
31
+
32
+ // Let's modify ApiService later to support 'type' or just use full path if needed.
33
+ // Or simpler: /rest/agile/1.0/board/${id}/sprint
34
+ // But api service baseURL is fixed.
35
+
36
+ // To fix this proper: I'll modify ApiService to allow changing API version/path or just use full path.
37
+ // Using full path:
38
+ const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
39
+ const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
40
+ const fullUrl = `${domain}/rest/agile/1.0/board/${options.board}/sprint?state=${options.state}`;
41
+
42
+ const data = await api.get(fullUrl);
43
+ spinner.stop();
44
+
45
+ if (!data.values || data.values.length === 0) {
46
+ console.log(chalk.yellow('No sprints found.'));
47
+ return;
48
+ }
49
+
50
+ const tableData = [
51
+ [chalk.bold('ID'), chalk.bold('Name'), chalk.bold('State'), chalk.bold('Dates')]
52
+ ];
53
+
54
+ data.values.forEach(s => {
55
+ tableData.push([
56
+ s.id,
57
+ s.name,
58
+ s.state === 'active' ? chalk.green(s.state) : s.state,
59
+ `${s.startDate ? s.startDate.split('T')[0] : ''} -> ${s.endDate ? s.endDate.split('T')[0] : ''}`
60
+ ]);
61
+ });
62
+
63
+ console.log(table(tableData));
64
+
65
+ } catch (e) {
66
+ spinner.fail('Failed to list sprints');
67
+ if (e.response) {
68
+ console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
69
+ } else {
70
+ console.error(chalk.red(e.message));
71
+ }
72
+ }
73
+ });
74
+
75
+ program.addCommand(sprintCmd);
76
+ }
@@ -0,0 +1,119 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { api } from "../services/api-service.js";
5
+
6
+ // Initialize MCP Server
7
+ const server = new Server(
8
+ {
9
+ name: "jira-pilot",
10
+ version: "1.0.0",
11
+ },
12
+ {
13
+ capabilities: {
14
+ tools: {},
15
+ },
16
+ }
17
+ );
18
+
19
+ // Define Tools
20
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
21
+ return {
22
+ tools: [
23
+ {
24
+ name: "jira_list_issues",
25
+ description: "List Jira issues using JQL",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ jql: { type: "string", description: "JQL query string" },
30
+ limit: { type: "number", description: "Max results", default: 10 }
31
+ }
32
+ }
33
+ },
34
+ {
35
+ name: "jira_get_issue",
36
+ description: "Get details of a specific Jira issue",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ issueKey: { type: "string", description: "Issue Key (e.g. PROJ-123)" }
41
+ },
42
+ required: ["issueKey"]
43
+ }
44
+ },
45
+ {
46
+ name: "jira_create_issue",
47
+ description: "Create a new Jira issue",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ projectKey: { type: "string", description: "Project Key" },
52
+ summary: { type: "string", description: "Issue Summary" },
53
+ description: { type: "string", description: "Issue Description" },
54
+ issueType: { type: "string", description: "Issue Type (Bug, Story, etc)", default: "Task" }
55
+ },
56
+ required: ["projectKey", "summary"]
57
+ }
58
+ }
59
+ ]
60
+ };
61
+ });
62
+
63
+ // Handle Tool Calls
64
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
65
+ const { name, arguments: args } = request.params;
66
+
67
+ try {
68
+ if (name === "jira_list_issues") {
69
+ const jql = args.jql || "";
70
+ const limit = args.limit || 10;
71
+ const data = await api.post('/search', {
72
+ jql,
73
+ maxResults: limit,
74
+ fields: ['summary', 'status', 'assignee', 'description']
75
+ });
76
+
77
+ return {
78
+ content: [{ type: "text", text: JSON.stringify(data.issues, null, 2) }]
79
+ };
80
+ }
81
+
82
+ if (name === "jira_get_issue") {
83
+ const data = await api.get(`/issue/${args.issueKey}`);
84
+ return {
85
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
86
+ };
87
+ }
88
+
89
+ if (name === "jira_create_issue") {
90
+ const body = {
91
+ fields: {
92
+ project: { key: args.projectKey },
93
+ summary: args.summary,
94
+ description: args.description,
95
+ issuetype: { name: args.issueType || 'Task' }
96
+ }
97
+ };
98
+ const data = await api.post('/issue', body);
99
+ return {
100
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
101
+ };
102
+ }
103
+
104
+ throw new Error(`Unknown tool: ${name}`);
105
+
106
+ } catch (e) {
107
+ const errorMessage = e.response?.data ? JSON.stringify(e.response.data) : e.message;
108
+ return {
109
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
110
+ isError: true
111
+ };
112
+ }
113
+ });
114
+
115
+ // Start Server
116
+ export async function startServer() {
117
+ const transport = new StdioServerTransport();
118
+ await server.connect(transport);
119
+ }
@@ -0,0 +1,52 @@
1
+ import axios from 'axios';
2
+ import { getCredentials } from '../utils/config.js';
3
+
4
+ export class AiService {
5
+ constructor() { }
6
+
7
+ async generate(prompt) {
8
+ const { aiKey, aiProvider } = getCredentials();
9
+
10
+ if (!aiKey) {
11
+ throw new Error('AI API Key not configured. Run "jira config setup" or manually set it in config.');
12
+ }
13
+
14
+ // Basic implementation for OpenAI - extensible for others
15
+ if (aiProvider === 'openai' || !aiProvider) {
16
+ return this.callOpenAI(aiKey, prompt);
17
+ } else if (aiProvider === 'gemini') {
18
+ return this.callGemini(aiKey, prompt);
19
+ } else if (aiProvider === 'anthropic') {
20
+ return this.callAnthropic(aiKey, prompt);
21
+ }
22
+
23
+ throw new Error(`Unsupported AI Provider: ${aiProvider}`);
24
+ }
25
+
26
+ async callOpenAI(key, prompt) {
27
+ try {
28
+ const response = await axios.post('https://api.openai.com/v1/chat/completions', {
29
+ model: 'gpt-4o', // or gpt-3.5-turbo
30
+ messages: [{ role: 'user', content: prompt }],
31
+ temperature: 0.7
32
+ }, {
33
+ headers: { 'Authorization': `Bearer ${key}` }
34
+ });
35
+ return response.data.choices[0].message.content;
36
+ } catch (e) {
37
+ throw new Error(`OpenAI API Error: ${e.response?.data?.error?.message || e.message}`);
38
+ }
39
+ }
40
+
41
+ async callGemini(key, prompt) {
42
+ // Placeholder for Gemini implementation
43
+ throw new Error("Gemini implementation pending.");
44
+ }
45
+
46
+ async callAnthropic(key, prompt) {
47
+ // Placeholder for Anthropic
48
+ throw new Error("Anthropic implementation pending.");
49
+ }
50
+ }
51
+
52
+ export const aiService = new AiService();
@@ -0,0 +1,94 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { getCredentials } from '../utils/config.js';
4
+
5
+ export class ApiService {
6
+ constructor() {
7
+ this.init();
8
+ }
9
+
10
+ init() {
11
+ const { jiraUrl, email, apiToken } = getCredentials();
12
+
13
+ if (!jiraUrl || !email || !apiToken) {
14
+ // Don't throw here, allow initialization for 'config' command usage
15
+ this.client = null;
16
+ return;
17
+ }
18
+
19
+ const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
20
+ const domain = match ? match[0] : jiraUrl;
21
+
22
+ this.client = axios.create({
23
+ baseURL: `${domain.replace(/\/$/, '')}/rest/api/3`,
24
+ headers: {
25
+ 'Authorization': `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`,
26
+ 'Accept': 'application/json',
27
+ 'Content-Type': 'application/json'
28
+ }
29
+ });
30
+
31
+ // Response interceptor for error handling
32
+ this.client.interceptors.response.use(
33
+ response => response,
34
+ error => {
35
+ if (error.response) {
36
+ if (error.response.status === 401) {
37
+ console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
38
+ } else if (error.response.status === 403) {
39
+ console.error(chalk.red('Access denied. You may not have permission for this resource.'));
40
+ } else if (error.response.status === 404) {
41
+ // Sometime 404 is valid (issues not found), let caller handle?
42
+ // Or log generic error? For now rethrow with clean message property if possible.
43
+ }
44
+ }
45
+ return Promise.reject(error);
46
+ }
47
+ );
48
+ }
49
+
50
+ ensureClient() {
51
+ if (!this.client) {
52
+ // Try to re-init in case config was just set
53
+ this.init();
54
+ if (!this.client) {
55
+ throw new Error('Jira credentials not configured. Run "jira config" first.');
56
+ }
57
+ }
58
+ }
59
+
60
+ async get(url, config = {}) {
61
+ this.ensureClient();
62
+ try {
63
+ const response = await this.client.get(url, config);
64
+ return response.data;
65
+ } catch (e) {
66
+ // Optional: Wrap error
67
+ throw e;
68
+ }
69
+ }
70
+
71
+ async post(url, data, config = {}) {
72
+ this.ensureClient();
73
+ try {
74
+ const response = await this.client.post(url, data, config);
75
+ return response.data;
76
+ } catch (e) {
77
+ throw e;
78
+ }
79
+ }
80
+
81
+ async put(url, data, config = {}) {
82
+ this.ensureClient();
83
+ const response = await this.client.put(url, data, config);
84
+ return response.data;
85
+ }
86
+
87
+ async delete(url, config = {}) {
88
+ this.ensureClient();
89
+ const response = await this.client.delete(url, config);
90
+ return response.data;
91
+ }
92
+ }
93
+
94
+ export const api = new ApiService();
@@ -0,0 +1,54 @@
1
+ import Conf from 'conf';
2
+
3
+ const schema = {
4
+ jiraUrl: {
5
+ type: 'string',
6
+ format: 'url'
7
+ },
8
+ email: {
9
+ type: 'string',
10
+ format: 'email'
11
+ },
12
+ apiToken: {
13
+ type: 'string'
14
+ },
15
+ aiKey: {
16
+ type: 'string'
17
+ },
18
+ aiProvider: {
19
+ type: 'string',
20
+ default: 'openai'
21
+ }
22
+ };
23
+
24
+ const config = new Conf({
25
+ projectName: 'jira-pilot',
26
+ schema
27
+ });
28
+
29
+ export const getCredentials = () => {
30
+ return {
31
+ jiraUrl: config.get('jiraUrl'),
32
+ email: config.get('email'),
33
+ apiToken: config.get('apiToken'),
34
+ aiKey: config.get('aiKey'),
35
+ aiProvider: config.get('aiProvider')
36
+ };
37
+ };
38
+
39
+ export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider }) => {
40
+ if (jiraUrl) config.set('jiraUrl', jiraUrl);
41
+ if (email) config.set('email', email);
42
+ if (apiToken) config.set('apiToken', apiToken);
43
+ if (aiKey) config.set('aiKey', aiKey);
44
+ if (aiProvider) config.set('aiProvider', aiProvider);
45
+ };
46
+
47
+ export const clearCredentials = () => {
48
+ config.clear();
49
+ };
50
+
51
+ export const hasCredentials = () => {
52
+ const creds = getCredentials();
53
+ return !!(creds.jiraUrl && creds.email && creds.apiToken);
54
+ };