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 +5 -0
- package/README.md +148 -0
- package/bin/jira.js +45 -0
- package/package.json +34 -0
- package/src/commands/ai.js +51 -0
- package/src/commands/config.js +91 -0
- package/src/commands/git.js +60 -0
- package/src/commands/issue.js +112 -0
- package/src/commands/mcp.js +20 -0
- package/src/commands/project.js +46 -0
- package/src/commands/sprint.js +76 -0
- package/src/server/mcp-server.js +119 -0
- package/src/services/ai-service.js +52 -0
- package/src/services/api-service.js +94 -0
- package/src/utils/config.js +54 -0
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
|
+
};
|