jira-pilot 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,24 +4,37 @@
4
4
 
5
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
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.
7
+ - **For Humans:** A beautiful, interactive CLI to manage issues, sprints, boards, and code without leaving your terminal.
8
+ - **For Agents:** A fully compliant **Model Context Protocol (MCP)** server with 8 tools that lets AI assistants (like Claude Desktop or Gemini) interact with your Jira instance safely.
9
9
 
10
- ## ✨ Features
10
+ ---
11
+
12
+ ## Features at a Glance
11
13
 
12
14
  ### 👤 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).
15
+ | Feature | Description |
16
+ |---------|-------------|
17
+ | **Issue Management** | Create, view, list, transition, assign, and comment on issues |
18
+ | **Interactive Wizards** | Step-by-step prompts with `enquirer` — no flags required |
19
+ | **Board & Sprint Management** | List boards, view sprints by state |
20
+ | **Git Integration** | Create feature branches from issues with smart naming |
21
+ | **AI Copilot** | Summarize issues, draft descriptions, get next-action suggestions |
22
+ | **Rich Visualization** | Beautiful tables, spinners, and formatted output |
23
+ | **Export** | Export issues list to JSON or Markdown files |
20
24
 
21
25
  ### 🤖 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.
26
+ | Feature | Description |
27
+ |---------|-------------|
28
+ | **8 MCP Tools** | list_issues, get_issue, create_issue, transition_issue, assign_issue, add_comment, list_projects, list_sprints |
29
+ | **LLM-Optimized** | Clean, structured JSON responses for efficient token usage |
30
+ | **Stdio Transport** | Standard MCP stdio server — works with any MCP client |
31
+
32
+ ### 🧠 Multi-Provider AI
33
+ | Provider | Model |
34
+ |----------|-------|
35
+ | **OpenAI** | GPT-4o |
36
+ | **Google Gemini** | gemini-2.0-flash |
37
+ | **Anthropic** | claude-sonnet (claude-sonnet-4-20250514) |
25
38
 
26
39
  ---
27
40
 
@@ -32,12 +45,14 @@
32
45
  npm install -g jira-pilot
33
46
  ```
34
47
 
48
+ After installing, the `jira` command is available globally.
49
+
35
50
  ### Local Development
36
51
  ```bash
37
- git clone https://github.com/yourusername/jira-pilot.git
52
+ git clone https://github.com/Aarul5/jira-pilot.git
38
53
  cd jira-pilot
39
54
  npm install
40
- npm link
55
+ npm link # Makes 'jira' command available globally
41
56
  ```
42
57
 
43
58
  ---
@@ -46,80 +61,211 @@ npm link
46
61
 
47
62
  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
63
 
64
+ ### Initial Setup
49
65
  ```bash
50
66
  jira config setup
51
67
  ```
52
68
 
53
69
  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:
70
+ 1. **Jira Site URL** e.g., `https://your-company.atlassian.net`
71
+ 2. **Email** Your Atlassian account email
72
+ 3. **API Token** The token you generated from Atlassian
73
+ 4. **Enable AI** Toggle AI features on/off
74
+ 5. **AI Provider** — Choose between `openai`, `gemini`, or `anthropic`
75
+ 6. **AI API Key** — Your API key for the selected provider
76
+
77
+ ### View / Clear Configuration
60
78
  ```bash
61
- jira config view
79
+ jira config view # Show current configuration (keys are masked)
80
+ jira config clear # Remove all stored credentials
62
81
  ```
63
82
 
83
+ > **Note:** Credentials are stored securely using the `conf` library in your system's config directory.
84
+
64
85
  ---
65
86
 
66
87
  ## 📖 Usage
67
88
 
68
- ### Issues
89
+ ### 📋 Issue Management
90
+
91
+ #### List Issues
69
92
  ```bash
70
- # List issues (default: assigned to you, active sprints)
93
+ # List issues assigned to you in active sprints
71
94
  jira issue list
72
95
 
73
96
  # List with custom JQL
74
97
  jira issue list --jql "project = PROJ AND priority = High"
75
98
 
76
- # Create a new issue (interactive)
77
- jira issue create
99
+ # Filter by project, assignee, or status
100
+ jira issue list --project PROJ --assignee "john.doe" --status "In Progress"
101
+
102
+ # Limit results
103
+ jira issue list --limit 20
78
104
 
79
- # View details
105
+ # Export results to file
106
+ jira issue list --export json # Creates issues-TIMESTAMP.json
107
+ jira issue list --export md # Creates issues-TIMESTAMP.md
108
+
109
+ # Combine filters and export
110
+ jira issue list --project PROJ --status Done --export json
111
+ ```
112
+
113
+ #### View Issue Details
114
+ ```bash
80
115
  jira issue view PROJ-123
116
+ ```
117
+ Displays: summary, status, priority, assignee, description, and recent comments.
118
+
119
+ #### Create Issue
120
+ ```bash
121
+ # Interactive wizard (recommended)
122
+ jira issue create
123
+
124
+ # Non-interactive with flags
125
+ jira issue create -p PROJ -s "Fix login bug"
126
+ jira issue create -p PROJ -t Bug -s "Crash on save" --priority High
127
+ jira issue create -p PROJ -t Story -s "Add dark mode" -d "Users want a dark theme" -a me
128
+ ```
81
129
 
82
- # Transition status (interactive)
130
+ **Interactive Wizard Steps:**
131
+ 1. **Select Project** — Choose from your accessible projects
132
+ 2. **Select Issue Type** — Bug, Story, Task, Epic, etc.
133
+ 3. **Enter Summary** — Required issue title
134
+ 4. **Enter Description** — Optional, converted to Jira ADF format
135
+ 5. **Select Priority** — High, Medium, Low, etc.
136
+ 6. **Assign** — Myself, Unassigned, or search by name/email
137
+
138
+ #### Transition Issue Status
139
+ ```bash
140
+ # Interactive — shows available transitions
83
141
  jira issue transition PROJ-123
84
142
 
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
143
+ # Direct specify target status
144
+ jira issue transition PROJ-123 --status "In Progress"
145
+ jira issue transition PROJ-123 -s Done
146
+ ```
147
+
148
+ #### Assign / Reassign Issue
149
+ ```bash
150
+ # Interactive — choose Myself, Unassign, or Search
151
+ jira issue assign PROJ-123
88
152
 
89
- # Combine filters and export (Power User)
90
- jira issue list --project "project-name" --assignee "assignee_name" --status Done --export json
153
+ # Quick assign
154
+ jira issue assign PROJ-123 -a me # Assign to yourself
155
+ jira issue assign PROJ-123 -a none # Unassign
91
156
  ```
92
157
 
93
- ### Projects & Sprints
158
+ #### Add Comment
159
+ ```bash
160
+ # Interactive — prompts for comment text
161
+ jira issue comment PROJ-123
162
+
163
+ # Inline comment
164
+ jira issue comment PROJ-123 -m "Fixed in latest build"
165
+ ```
166
+
167
+ ---
168
+
169
+ ### 📂 Projects & Boards
170
+
171
+ #### List Projects
94
172
  ```bash
95
- # List projects
96
173
  jira project list
174
+ ```
175
+ Displays: project key, name, lead, and style in a formatted table.
97
176
 
98
- # List sprints for a board
177
+ #### List Boards
178
+ ```bash
179
+ # List all boards
180
+ jira board list
181
+
182
+ # Filter by project
183
+ jira board list -p PROJ
184
+
185
+ # Filter by type
186
+ jira board list -t scrum
187
+ jira board list -t kanban
188
+ ```
189
+
190
+ #### List Sprints
191
+ ```bash
192
+ # List active and future sprints
99
193
  jira sprint list --board 5
194
+
195
+ # List by board name
196
+ jira sprint list --board "My Team Board"
197
+
198
+ # Filter by state
199
+ jira sprint list --board 5 --state active
200
+ jira sprint list --board 5 --state closed
100
201
  ```
101
202
 
102
- ### Git Integration
103
- Create a branch automatically named from the issue summary:
203
+ ---
204
+
205
+ ### 🌿 Git Integration
206
+
207
+ Create feature branches automatically named from the issue summary:
104
208
  ```bash
105
209
  jira git branch PROJ-123
106
210
  # Output: Switched to a new branch 'feature/PROJ-123-fix-login-modal'
107
211
  ```
108
212
 
109
- ### AI Features
110
- Summarize a complex issue thread:
213
+ ---
214
+
215
+ ### 🤖 AI Features
216
+
217
+ > **Requires:** AI features must be enabled via `jira config setup` with a valid API key for your chosen provider (OpenAI, Gemini, or Anthropic).
218
+
219
+ #### Summarize an Issue
220
+ Get an AI-generated TL;DR of long issue threads with comments:
111
221
  ```bash
112
222
  jira ai summarize PROJ-123
113
223
  ```
114
- *(Requires OpenAI Key in config)*
224
+
225
+ #### Draft an Issue Description
226
+ Generate a structured issue description from rough notes or bullet points:
227
+ ```bash
228
+ # Interactive — prompts for your notes
229
+ jira ai draft
230
+
231
+ # Inline with issue type context
232
+ jira ai draft -i "login fails, returns 500, only on mobile" -t bug
233
+ jira ai draft -i "add dark mode toggle to settings" -t story
234
+ ```
235
+
236
+ #### Suggest Next Actions
237
+ Analyze an issue and get AI-powered suggestions for what to do next:
238
+ ```bash
239
+ jira ai suggest PROJ-123
240
+ ```
241
+ Returns: **Immediate Next Action**, **Potential Blockers**, **Suggested Status Transition**, and **Recommendations**.
115
242
 
116
243
  ---
117
244
 
118
- ## 🧠 Using with AI Agents (Claude/Gemini)
245
+ ## 🧠 Using with AI Agents (MCP)
119
246
 
120
247
  `jira-pilot` implements the **Model Context Protocol (MCP)**, making it plug-and-play for AI assistants.
121
248
 
249
+ ### Starting the MCP Server
250
+ ```bash
251
+ jira mcp
252
+ ```
253
+
254
+ ### Available MCP Tools
255
+
256
+ | Tool | Description | Required Args |
257
+ |------|-------------|---------------|
258
+ | `jira_list_issues` | Search issues via JQL | `jql` |
259
+ | `jira_get_issue` | Get full issue details | `issueKey` |
260
+ | `jira_create_issue` | Create a new issue (ADF) | `projectKey`, `summary` |
261
+ | `jira_transition_issue` | List or execute transitions | `issueKey` |
262
+ | `jira_assign_issue` | Assign/unassign an issue | `issueKey` |
263
+ | `jira_add_comment` | Add a comment (ADF) | `issueKey`, `body` |
264
+ | `jira_list_projects` | List accessible projects | — |
265
+ | `jira_list_sprints` | List sprints for a board | `boardId` |
266
+
122
267
  ### Claude Desktop Configuration
268
+
123
269
  Add the following to your `claude_desktop_config.json`:
124
270
 
125
271
  ```json
@@ -133,12 +279,32 @@ Add the following to your `claude_desktop_config.json`:
133
279
  }
134
280
  ```
135
281
 
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."
282
+ ### VS Code / Cursor Configuration
283
+
284
+ Add to your `.vscode/mcp.json` or equivalent:
285
+
286
+ ```json
287
+ {
288
+ "servers": {
289
+ "jira-pilot": {
290
+ "command": "jira",
291
+ "args": ["mcp"]
292
+ }
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### Example Agent Prompts
298
+ Once connected, you can ask your AI assistant things like:
299
+ - *"Show me my high-priority Jira issues"*
300
+ - *"Create a bug for the login crash on mobile in project PROJ"*
301
+ - *"Transition PROJ-123 to In Progress and assign it to me"*
302
+ - *"Add a comment to PROJ-456 saying the fix is deployed"*
303
+ - *"What sprints are active on board 5?"*
138
304
 
139
305
  ---
140
306
 
141
- ## 🛠️ Testing & Verification
307
+ ## 🧪 Testing & Verification
142
308
 
143
309
  ### Testing the MCP Server
144
310
  You can test the MCP server functionality using the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
@@ -148,17 +314,60 @@ npx @modelcontextprotocol/inspector node ./bin/jira.js mcp
148
314
  ```
149
315
 
150
316
  This will launch a web interface where you can:
151
- 1. View available tools (`jira_list_issues`, `jira_get_issue`, `jira_create_issue`).
152
- 2. Execute tools and view output.
153
- 3. Inspect request/response logs.
317
+ 1. View all 8 available tools and their schemas
318
+ 2. Execute tools with custom arguments
319
+ 3. Inspect request/response logs
320
+
321
+ ---
322
+
323
+ ## 📦 CLI Command Reference
324
+
325
+ ```
326
+ jira [command]
327
+
328
+ Commands:
329
+ config Configure Jira credentials
330
+ issue Manage Jira issues
331
+ project Manage Jira projects
332
+ board Manage Jira boards
333
+ sprint Manage Sprints
334
+ git Git integration for Jira
335
+ ai AI Helper commands
336
+ mcp Start MCP Agent Server (Stdio)
337
+
338
+ Issue Subcommands:
339
+ issue list List issues (JQL, filters, export)
340
+ issue view View issue details
341
+ issue create Create a new issue (wizard or flags)
342
+ issue transition Transition issue status
343
+ issue assign Assign or reassign an issue
344
+ issue comment Add a comment to an issue
345
+
346
+ AI Subcommands:
347
+ ai summarize Summarize an issue using AI
348
+ ai draft Draft issue description from notes
349
+ ai suggest Suggest next actions for an issue
350
+
351
+ Board Subcommands:
352
+ board list List Jira boards
353
+
354
+ Sprint Subcommands:
355
+ sprint list List sprints for a board
356
+ ```
154
357
 
155
358
  ---
156
359
 
157
- ## 🛠️ Project Structure
158
- - `bin/`: Entry point.
159
- - `src/commands/`: CLI command definitions (Human UI).
160
- - `src/server/`: MCP Server implementation (Agent UI).
161
- - `src/services/`: Core logic (API, AI).
360
+ ## 🤝 Contributing
361
+
362
+ Contributions are welcome! Please open an issue or submit a pull request.
363
+
364
+ ```bash
365
+ git clone https://github.com/Aarul5/jira-pilot.git
366
+ cd jira-pilot
367
+ npm install
368
+ npm link
369
+ ```
162
370
 
163
371
  ## 📄 License
372
+
164
373
  ISC
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';
@@ -19,6 +19,8 @@ program
19
19
  Examples:
20
20
  $ jira issue list
21
21
  $ jira issue view PROJ-123
22
+ $ jira issue create
23
+ $ jira board list
22
24
  $ jira sprint list --board 123
23
25
  $ jira ai summarize PROJ-123
24
26
  `);
@@ -27,6 +29,7 @@ import { registerConfigCommand } from '../src/commands/config.js';
27
29
  import { registerIssueCommand } from '../src/commands/issue.js';
28
30
  import { registerProjectCommand } from '../src/commands/project.js';
29
31
  import { registerSprintCommand } from '../src/commands/sprint.js';
32
+ import { registerBoardCommand } from '../src/commands/board.js';
30
33
  import { registerGitCommand } from '../src/commands/git.js';
31
34
  import { registerAiCommand } from '../src/commands/ai.js';
32
35
  import { registerMcpCommand } from '../src/commands/mcp.js';
@@ -36,6 +39,7 @@ registerConfigCommand(program);
36
39
  registerIssueCommand(program);
37
40
  registerProjectCommand(program);
38
41
  registerSprintCommand(program);
42
+ registerBoardCommand(program);
39
43
  registerGitCommand(program);
40
44
  registerAiCommand(program);
41
45
  registerMcpCommand(program);
package/package.json CHANGED
@@ -1,32 +1,69 @@
1
1
  {
2
2
  "name": "jira-pilot",
3
- "version": "1.1.0",
4
- "description": "AI-powered Jira CLI for humans and agents",
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
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "jira": "./bin/jira.js"
8
+ "jira": "./bin/jira.js",
9
+ "jira-pilot": "./bin/jira.js"
9
10
  },
10
11
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1",
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",
12
17
  "link": "npm link"
13
18
  },
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "files": [
23
+ "bin/",
24
+ "src/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
14
28
  "keywords": [
15
29
  "jira",
16
30
  "cli",
17
- "ai",
18
- "agent",
19
- "mcp",
31
+ "jira-cli",
20
32
  "jira-client",
21
- "jiracli",
22
- "atlassian",
23
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",
24
47
  "issue-tracker",
48
+ "project-management",
25
49
  "task-management",
50
+ "sprint",
51
+ "kanban",
52
+ "scrum",
53
+ "agile",
26
54
  "command-line",
27
- "typescript"
55
+ "interactive",
56
+ "terminal",
57
+ "developer-tools",
58
+ "devtools",
59
+ "productivity",
60
+ "workflow",
61
+ "git-integration"
28
62
  ],
29
- "author": "Arul",
63
+ "author": {
64
+ "name": "Arul",
65
+ "url": "https://github.com/Aarul5"
66
+ },
30
67
  "repository": {
31
68
  "type": "git",
32
69
  "url": "git+https://github.com/Aarul5/jira-pilot.git"
@@ -46,5 +83,9 @@
46
83
  "open": "^10.0.0",
47
84
  "ora": "^8.0.0",
48
85
  "table": "^6.8.0"
86
+ },
87
+ "devDependencies": {
88
+ "@vitest/coverage-v8": "^4.0.18",
89
+ "vitest": "^4.0.18"
49
90
  }
50
91
  }
@@ -5,6 +5,7 @@ import enquirer from 'enquirer';
5
5
  import { api } from '../services/api-service.js';
6
6
  import { aiService } from '../services/ai-service.js';
7
7
  import { parseADF } from '../utils/adf-parser.js';
8
+ import { validateIssueKey } from '../utils/validators.js';
8
9
 
9
10
  export function registerAiCommand(program) {
10
11
  const aiCmd = new Command('ai')
@@ -22,6 +23,8 @@ Common Actions:
22
23
  .description('Summarize an issue using AI')
23
24
  .argument('<issueKey>', 'Jira Issue Key')
24
25
  .action(async (issueKey) => {
26
+ const check = validateIssueKey(issueKey);
27
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
25
28
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
26
29
  try {
27
30
  const issue = await api.get(`/issue/${issueKey}?fields=summary,description,comment`);
@@ -141,6 +144,8 @@ Keep it professional and concise. Output in plain text (not markdown headers, us
141
144
  .description('Suggest next actions for an issue based on its context')
142
145
  .argument('<issueKey>', 'Jira Issue Key')
143
146
  .action(async (issueKey) => {
147
+ const check = validateIssueKey(issueKey);
148
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
144
149
  const spinner = ora(`Analyzing issue ${issueKey}...`).start();
145
150
  try {
146
151
  const issue = await api.get(`/issue/${issueKey}?fields=summary,description,status,assignee,priority,comment,issuetype`);
@@ -0,0 +1,66 @@
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
+ import { handleCommandError } from '../utils/error-handler.js';
7
+
8
+ export function registerBoardCommand(program) {
9
+ const boardCmd = new Command('board')
10
+ .description('Manage Jira boards')
11
+ .addHelpText('after', `
12
+ Common Actions:
13
+ $ jira board list # List all boards
14
+ $ jira board list -p PROJ # List boards for a project
15
+ `);
16
+
17
+ boardCmd
18
+ .command('list')
19
+ .description('List Jira boards')
20
+ .option('-p, --project <key>', 'Filter by project key')
21
+ .option('-t, --type <type>', 'Filter by board type (scrum, kanban, simple)')
22
+ .option('-l, --limit <n>', 'Max results', '50')
23
+ .action(async (options) => {
24
+ const spinner = ora('Fetching boards...').start();
25
+ try {
26
+ const params = new URLSearchParams();
27
+ params.set('maxResults', options.limit);
28
+
29
+ if (options.project) {
30
+ params.set('projectKeyOrId', options.project);
31
+ }
32
+ if (options.type) {
33
+ params.set('type', options.type);
34
+ }
35
+
36
+ const data = await api.agileGet(`/board?${params.toString()}`);
37
+ spinner.stop();
38
+
39
+ if (!data.values || data.values.length === 0) {
40
+ console.log(chalk.yellow('No boards found.'));
41
+ return;
42
+ }
43
+
44
+ const tableData = [
45
+ [chalk.bold('ID'), chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Project')]
46
+ ];
47
+
48
+ data.values.forEach(b => {
49
+ tableData.push([
50
+ b.id,
51
+ b.name,
52
+ b.type,
53
+ b.location?.projectKey || '-'
54
+ ]);
55
+ });
56
+
57
+ console.log(table(tableData));
58
+ console.log(chalk.grey(`Showing ${data.values.length} board(s)`));
59
+
60
+ } catch (e) {
61
+ handleCommandError(spinner, e, 'Failed to list boards');
62
+ }
63
+ });
64
+
65
+ program.addCommand(boardCmd);
66
+ }
@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
4
4
  import { api } from '../services/api-service.js';
5
5
  import ora from 'ora';
6
6
  import enquirer from 'enquirer';
7
+ import { validateIssueKey } from '../utils/validators.js';
7
8
 
8
9
  export function registerGitCommand(program) {
9
10
  const gitCmd = new Command('git')
@@ -15,6 +16,8 @@ export function registerGitCommand(program) {
15
16
  .argument('<issueKey>', 'Jira Issue Key (e.g., PROJ-123)')
16
17
  .option('-t, --type <type>', 'Branch type (feature, bugfix, hotfix)', 'feature')
17
18
  .action(async (issueKey, options) => {
19
+ const check = validateIssueKey(issueKey);
20
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
18
21
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
19
22
  try {
20
23
  const issue = await api.get(`/issue/${issueKey}`);
@@ -6,6 +6,7 @@ import ora from 'ora';
6
6
  import enquirer from 'enquirer';
7
7
  import { parseADF } from '../utils/adf-parser.js';
8
8
  import { textToADF } from '../utils/text-to-adf.js';
9
+ import { validateIssueKey } from '../utils/validators.js';
9
10
 
10
11
  export function registerIssueCommand(program) {
11
12
  const issueCmd = new Command('issue')
@@ -136,6 +137,8 @@ Examples:
136
137
  $ jira issue view PROJ-123
137
138
  `)
138
139
  .action(async (issueKey) => {
140
+ const check = validateIssueKey(issueKey);
141
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
139
142
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
140
143
  try {
141
144
  const issue = await api.get(`/issue/${issueKey}`);
@@ -464,6 +467,8 @@ Examples:
464
467
  $ jira issue transition PROJ-123 -s Done
465
468
  `)
466
469
  .action(async (issueKey, options) => {
470
+ const check = validateIssueKey(issueKey);
471
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
467
472
  const spinner = ora(`Fetching transitions for ${issueKey}...`).start();
468
473
  try {
469
474
  // Fetch current issue to show context
@@ -554,6 +559,8 @@ Examples:
554
559
  $ jira issue assign PROJ-123 -a none # Unassign
555
560
  `)
556
561
  .action(async (issueKey, options) => {
562
+ const check = validateIssueKey(issueKey);
563
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
557
564
  try {
558
565
  let assigneeId = options.assignee;
559
566
 
@@ -649,6 +656,8 @@ Examples:
649
656
  $ jira issue comment PROJ-123 -m "Fixed in latest build"
650
657
  `)
651
658
  .action(async (issueKey, options) => {
659
+ const check = validateIssueKey(issueKey);
660
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
652
661
  try {
653
662
  let commentText = options.message;
654
663
 
@@ -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 { handleCommandError } from '../utils/error-handler.js';
6
7
 
7
8
  export function registerSprintCommand(program) {
8
9
  const sprintCmd = new Command('sprint')
@@ -15,57 +16,27 @@ Common Actions:
15
16
  sprintCmd
16
17
  .command('list')
17
18
  .description('List sprints for a board')
18
- .requiredOption('-b, --board <id>', 'Board ID')
19
+ .requiredOption('-b, --board <id>', 'Board ID or name')
19
20
  .option('-s, --state <state>', 'State (active, future, closed)', 'active,future')
20
21
  .action(async (options) => {
21
22
  const spinner = ora(`Fetching sprints for board ${options.board}...`).start();
22
23
  try {
23
- // Agile API usually involves /rest/agile/1.0
24
- // My default ApiService is /rest/api/3. I might need to override or allow full path?
25
- // ApiService handles baseURL. I should make it flexible or add Agile support.
26
-
27
- // HACK: ApiService constructor sets base to /rest/api/3.
28
- // I need to use a different client or hack the URL.
29
- // Axios allows absolute URLs to override baseURL.
30
- // So if I pass full URL it works.
31
-
32
- const { jiraUrl } = (await import('../utils/config.js')).getCredentials();
33
- // Assuming api-service exposes client or get method.
34
- // But get method prepend baseURL? No, axios usually supports absolute URL.
35
-
36
- // Let's modify ApiService later to support 'type' or just use full path if needed.
37
- // Or simpler: /rest/agile/1.0/board/${id}/sprint
38
- // But api service baseURL is fixed.
39
-
40
- // To fix this proper: I'll modify ApiService to allow changing API version/path or just use full path.
41
- // Using full path:
42
- const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
43
- const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
44
-
45
24
  let boardId = options.board;
46
25
 
47
- // If board option is not a number, try to look it up using the Board Name/Key
26
+ // If board option is not a number, look it up by name
48
27
  if (isNaN(boardId)) {
49
28
  spinner.text = `Looking up board "${options.board}"...`;
50
- const boardSearchUrl = `${domain}/rest/agile/1.0/board?name=${encodeURIComponent(options.board)}`;
51
- const boardData = await api.get(boardSearchUrl);
29
+ const boardData = await api.agileGet(`/board?name=${encodeURIComponent(options.board)}`);
52
30
 
53
31
  if (!boardData.values || boardData.values.length === 0) {
54
- // Fallback: It might be a project key. Let's try searching for boards associated with this project.
55
- // But the API doesn't support projectKey filter directly on /board easily without iterating.
56
- // For now, fail if name match doesn't work.
57
32
  throw new Error(`Board with name "${options.board}" not found. Please provide the numeric Board ID.`);
58
33
  }
59
34
 
60
- // Strict match or pick first? Let's pick the first one but warn if multiple
61
35
  if (boardData.values.length > 1) {
62
- // Try to find exact match
63
36
  const exact = boardData.values.find(b => b.name.toLowerCase() === options.board.toLowerCase());
64
37
  if (exact) {
65
38
  boardId = exact.id;
66
39
  } else {
67
- // Just pick first? Or error?
68
- // Let's pick first but log
69
40
  console.log(chalk.yellow(`\nMultiple boards found for "${options.board}". Using "${boardData.values[0].name}" (ID: ${boardData.values[0].id}).`));
70
41
  boardId = boardData.values[0].id;
71
42
  }
@@ -75,9 +46,7 @@ Common Actions:
75
46
  spinner.text = `Fetching sprints for board ${options.board} (ID: ${boardId})...`;
76
47
  }
77
48
 
78
- const fullUrl = `${domain}/rest/agile/1.0/board/${boardId}/sprint?state=${options.state}`;
79
-
80
- const data = await api.get(fullUrl);
49
+ const data = await api.agileGet(`/board/${boardId}/sprint?state=${options.state}`);
81
50
  spinner.stop();
82
51
 
83
52
  if (!data.values || data.values.length === 0) {
@@ -101,17 +70,7 @@ Common Actions:
101
70
  console.log(table(tableData));
102
71
 
103
72
  } catch (e) {
104
- spinner.fail('Failed to list sprints');
105
- if (e.response) {
106
- if (e.response.status === 404) {
107
- console.error(chalk.red(`\nError: Board with ID "${options.board}" not found or you do not have permission to view it.`));
108
- console.error(chalk.grey('Tip: Verify the Board ID in your Jira URL: /jira/software/c/projects/KEY/boards/ID'));
109
- } else {
110
- console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
111
- }
112
- } else {
113
- console.error(chalk.red(e.message));
114
- }
73
+ handleCommandError(spinner, e, 'Failed to list sprints');
115
74
  }
116
75
  });
117
76
 
@@ -3,7 +3,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { api } from "../services/api-service.js";
5
5
  import { textToADF } from "../utils/text-to-adf.js";
6
- import { getCredentials } from "../utils/config.js";
7
6
 
8
7
  // Initialize MCP Server
9
8
  const server = new Server(
@@ -298,14 +297,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
298
297
 
299
298
  // ── jira_list_sprints ───────────────────────────────
300
299
  if (name === "jira_list_sprints") {
301
- const { jiraUrl } = getCredentials();
302
- const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
303
- const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
304
-
305
300
  const state = args.state || 'active,future';
306
- const fullUrl = `${domain}/rest/agile/1.0/board/${args.boardId}/sprint?state=${state}`;
307
-
308
- const data = await api.get(fullUrl);
301
+ const data = await api.agileGet(`/board/${args.boardId}/sprint?state=${state}`);
309
302
 
310
303
  const sprints = (data.values || []).map(s => ({
311
304
  id: s.id,
@@ -11,45 +11,59 @@ export class ApiService {
11
11
  const { jiraUrl, email, apiToken } = getCredentials();
12
12
 
13
13
  if (!jiraUrl || !email || !apiToken) {
14
- // Don't throw here, allow initialization for 'config' command usage
15
14
  this.client = null;
15
+ this._domain = null;
16
16
  return;
17
17
  }
18
18
 
19
19
  const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
20
- const domain = match ? match[0] : jiraUrl;
20
+ this._domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
21
21
 
22
+ const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`;
23
+
24
+ // Standard REST API v3 client
22
25
  this.client = axios.create({
23
- baseURL: `${domain.replace(/\/$/, '')}/rest/api/3`,
26
+ baseURL: `${this._domain}/rest/api/3`,
27
+ headers: {
28
+ 'Authorization': authHeader,
29
+ 'Accept': 'application/json',
30
+ 'Content-Type': 'application/json'
31
+ }
32
+ });
33
+
34
+ // Agile REST API v1 client (for boards, sprints, etc.)
35
+ this.agileClient = axios.create({
36
+ baseURL: `${this._domain}/rest/agile/1.0`,
24
37
  headers: {
25
- 'Authorization': `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`,
38
+ 'Authorization': authHeader,
26
39
  'Accept': 'application/json',
27
40
  'Content-Type': 'application/json'
28
41
  }
29
42
  });
30
43
 
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
+ // Shared response interceptor
45
+ const errorInterceptor = (error) => {
46
+ if (error.response) {
47
+ if (error.response.status === 401) {
48
+ console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
49
+ } else if (error.response.status === 403) {
50
+ console.error(chalk.red('Access denied. You may not have permission for this resource.'));
44
51
  }
45
- return Promise.reject(error);
46
52
  }
47
- );
53
+ return Promise.reject(error);
54
+ };
55
+
56
+ this.client.interceptors.response.use(r => r, errorInterceptor);
57
+ this.agileClient.interceptors.response.use(r => r, errorInterceptor);
58
+ }
59
+
60
+ /** @returns {string} The Jira domain URL */
61
+ get domain() {
62
+ return this._domain;
48
63
  }
49
64
 
50
65
  ensureClient() {
51
66
  if (!this.client) {
52
- // Try to re-init in case config was just set
53
67
  this.init();
54
68
  if (!this.client) {
55
69
  throw new Error('Jira credentials not configured. Run "jira config" first.');
@@ -57,25 +71,18 @@ export class ApiService {
57
71
  }
58
72
  }
59
73
 
74
+ // ── Standard REST API v3 Methods ────────────────────────────────
75
+
60
76
  async get(url, config = {}) {
61
77
  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
- }
78
+ const response = await this.client.get(url, config);
79
+ return response.data;
69
80
  }
70
81
 
71
82
  async post(url, data, config = {}) {
72
83
  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
- }
84
+ const response = await this.client.post(url, data, config);
85
+ return response.data;
79
86
  }
80
87
 
81
88
  async put(url, data, config = {}) {
@@ -89,6 +96,20 @@ export class ApiService {
89
96
  const response = await this.client.delete(url, config);
90
97
  return response.data;
91
98
  }
99
+
100
+ // ── Agile REST API v1 Methods ───────────────────────────────────
101
+
102
+ async agileGet(url, config = {}) {
103
+ this.ensureClient();
104
+ const response = await this.agileClient.get(url, config);
105
+ return response.data;
106
+ }
107
+
108
+ async agilePost(url, data, config = {}) {
109
+ this.ensureClient();
110
+ const response = await this.agileClient.post(url, data, config);
111
+ return response.data;
112
+ }
92
113
  }
93
114
 
94
115
  export const api = new ApiService();
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Standardized error handler for CLI commands.
5
+ * Stops the spinner and prints a formatted error message.
6
+ *
7
+ * @param {object|null} spinner - Ora spinner instance (will be stopped/failed)
8
+ * @param {Error} error - The error object
9
+ * @param {string} [context] - Optional context (e.g., "Failed to list issues")
10
+ */
11
+ export function handleCommandError(spinner, error, context = 'Operation failed') {
12
+ // Handle user cancellation (Ctrl+C in enquirer)
13
+ if (error === '' || (error && error.message === '')) {
14
+ if (spinner) spinner.stop();
15
+ console.log(chalk.yellow('\nCancelled.'));
16
+ return;
17
+ }
18
+
19
+ if (spinner) {
20
+ spinner.fail(context);
21
+ } else {
22
+ console.error(chalk.red(`\n${context}:`));
23
+ }
24
+
25
+ if (error.response) {
26
+ const status = error.response.status;
27
+ if (status === 404) {
28
+ console.error(chalk.red('Resource not found. Check the ID or key.'));
29
+ } else if (status === 400) {
30
+ const data = error.response.data;
31
+ const messages = data?.errorMessages?.join(', ') || (data?.errors
32
+ ? Object.entries(data.errors).map(([k, v]) => `${k}: ${v}`).join(', ')
33
+ : JSON.stringify(data));
34
+ console.error(chalk.red(`Bad Request: ${messages}`));
35
+ } else {
36
+ console.error(chalk.red(`Error ${status}: `), error.response.data);
37
+ }
38
+ } else {
39
+ console.error(chalk.red(error.message));
40
+ }
41
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Input Validators for CLI commands.
3
+ * Validates user input before API calls to catch errors early.
4
+ */
5
+
6
+ /**
7
+ * Validates a Jira issue key format (e.g., PROJ-123).
8
+ * @param {string} key - The issue key to validate.
9
+ * @returns {{ valid: boolean, message?: string }}
10
+ */
11
+ export function validateIssueKey(key) {
12
+ if (!key || typeof key !== 'string') {
13
+ return { valid: false, message: 'Issue key is required.' };
14
+ }
15
+
16
+ const trimmed = key.trim().toUpperCase();
17
+ const pattern = /^[A-Z][A-Z0-9_]+-\d+$/;
18
+
19
+ if (!pattern.test(trimmed)) {
20
+ return {
21
+ valid: false,
22
+ message: `Invalid issue key "${key}". Expected format: PROJ-123 (letters/numbers, dash, digits).`
23
+ };
24
+ }
25
+
26
+ return { valid: true };
27
+ }
28
+
29
+ /**
30
+ * Validates a Jira project key (e.g., PROJ).
31
+ * @param {string} key - The project key to validate.
32
+ * @returns {{ valid: boolean, message?: string }}
33
+ */
34
+ export function validateProjectKey(key) {
35
+ if (!key || typeof key !== 'string') {
36
+ return { valid: false, message: 'Project key is required.' };
37
+ }
38
+
39
+ const trimmed = key.trim().toUpperCase();
40
+ const pattern = /^[A-Z][A-Z0-9_]+$/;
41
+
42
+ if (!pattern.test(trimmed)) {
43
+ return {
44
+ valid: false,
45
+ message: `Invalid project key "${key}". Must start with a letter and contain only uppercase letters, digits, or underscores.`
46
+ };
47
+ }
48
+
49
+ return { valid: true };
50
+ }
51
+
52
+ /**
53
+ * Validates a Jira site URL.
54
+ * @param {string} url - The URL to validate.
55
+ * @returns {{ valid: boolean, message?: string }}
56
+ */
57
+ export function validateUrl(url) {
58
+ if (!url || typeof url !== 'string') {
59
+ return { valid: false, message: 'URL is required.' };
60
+ }
61
+
62
+ const trimmed = url.trim();
63
+
64
+ try {
65
+ const parsed = new URL(trimmed);
66
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
67
+ return { valid: false, message: 'URL must use http or https protocol.' };
68
+ }
69
+ return { valid: true };
70
+ } catch {
71
+ return { valid: false, message: `Invalid URL: "${url}". Example: https://your-company.atlassian.net` };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Sanitizes a JQL string by escaping potentially dangerous characters.
77
+ * This is a basic sanitization — Jira's API does its own validation too.
78
+ * @param {string} jql - The JQL query string.
79
+ * @returns {string} The sanitized JQL string.
80
+ */
81
+ export function sanitizeJql(jql) {
82
+ if (!jql || typeof jql !== 'string') {
83
+ return '';
84
+ }
85
+
86
+ // Remove null bytes and control characters
87
+ return jql.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim();
88
+ }
@@ -1,22 +0,0 @@
1
- name: Publish Package
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- jobs:
8
- publish:
9
- runs-on: ubuntu-latest
10
- permissions:
11
- contents: read
12
- id-token: write
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: '20.x'
18
- registry-url: 'https://registry.npmjs.org'
19
- - run: npm ci
20
- - run: npm publish --provenance --access public
21
- env:
22
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}