jira-pilot 2.1.2 → 2.2.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/README.md +54 -0
- package/bin/jira.ts +2 -0
- package/dist/bin/jira.js +2 -0
- package/dist/bin/jira.js.map +1 -1
- package/dist/src/commands/ai-actions/plan.js +1 -1
- package/dist/src/commands/ai-actions/plan.js.map +1 -1
- package/dist/src/commands/ai-actions/review.js +5 -4
- package/dist/src/commands/ai-actions/review.js.map +1 -1
- package/dist/src/commands/ai-actions/standup.js +1 -1
- package/dist/src/commands/ai-actions/standup.js.map +1 -1
- package/dist/src/commands/ai.js +1 -1
- package/dist/src/commands/ai.js.map +1 -1
- package/dist/src/commands/board.js +10 -5
- package/dist/src/commands/board.js.map +1 -1
- package/dist/src/commands/bulk.js +11 -10
- package/dist/src/commands/bulk.js.map +1 -1
- package/dist/src/commands/config.js +1 -1
- package/dist/src/commands/config.js.map +1 -1
- package/dist/src/commands/dashboard.js +19 -12
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/filter.js +7 -4
- package/dist/src/commands/filter.js.map +1 -1
- package/dist/src/commands/git.js +1 -1
- package/dist/src/commands/git.js.map +1 -1
- package/dist/src/commands/issue-attach.js +1 -1
- package/dist/src/commands/issue-attach.js.map +1 -1
- package/dist/src/commands/issue-pr.js +1 -1
- package/dist/src/commands/issue-pr.js.map +1 -1
- package/dist/src/commands/issue-worklog.js +10 -5
- package/dist/src/commands/issue-worklog.js.map +1 -1
- package/dist/src/commands/issue.js +173 -122
- package/dist/src/commands/issue.js.map +1 -1
- package/dist/src/commands/project.js +10 -5
- package/dist/src/commands/project.js.map +1 -1
- package/dist/src/commands/sprint.js +19 -8
- package/dist/src/commands/sprint.js.map +1 -1
- package/dist/src/commands/tui.d.ts +2 -0
- package/dist/src/commands/tui.js +10 -0
- package/dist/src/commands/tui.js.map +1 -0
- package/dist/src/server/mcp-server.js +209 -27
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/services/ai-service.js +7 -4
- package/dist/src/services/ai-service.js.map +1 -1
- package/dist/src/services/api-service.d.ts +2 -0
- package/dist/src/services/api-service.js +32 -20
- package/dist/src/services/api-service.js.map +1 -1
- package/dist/src/tui/App.d.ts +1 -0
- package/dist/src/tui/App.js +26 -0
- package/dist/src/tui/App.js.map +1 -0
- package/dist/src/tui/index.d.ts +1 -0
- package/dist/src/tui/index.js +8 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/screens/BoardList.d.ts +1 -0
- package/dist/src/tui/screens/BoardList.js +71 -0
- package/dist/src/tui/screens/BoardList.js.map +1 -0
- package/dist/src/tui/screens/Dashboard.d.ts +1 -0
- package/dist/src/tui/screens/Dashboard.js +41 -0
- package/dist/src/tui/screens/Dashboard.js.map +1 -0
- package/dist/src/tui/screens/IssueDetail.d.ts +6 -0
- package/dist/src/tui/screens/IssueDetail.js +40 -0
- package/dist/src/tui/screens/IssueDetail.js.map +1 -0
- package/dist/src/tui/screens/IssueList.d.ts +1 -0
- package/dist/src/tui/screens/IssueList.js +72 -0
- package/dist/src/tui/screens/IssueList.js.map +1 -0
- package/dist/src/tui/screens/KanbanBoard.d.ts +6 -0
- package/dist/src/tui/screens/KanbanBoard.js +86 -0
- package/dist/src/tui/screens/KanbanBoard.js.map +1 -0
- package/dist/src/tui/utils/adf-render.d.ts +1 -0
- package/dist/src/tui/utils/adf-render.js +29 -0
- package/dist/src/tui/utils/adf-render.js.map +1 -0
- package/dist/src/utils/api-paths.d.ts +31 -0
- package/dist/src/utils/api-paths.js +32 -0
- package/dist/src/utils/api-paths.js.map +1 -0
- package/dist/src/utils/error-handler.d.ts +2 -2
- package/dist/src/utils/error-handler.js.map +1 -1
- package/dist/src/utils/http.d.ts +27 -0
- package/dist/src/utils/http.js +95 -0
- package/dist/src/utils/http.js.map +1 -0
- package/dist/src/utils/spinner.d.ts +21 -0
- package/dist/src/utils/spinner.js +79 -0
- package/dist/src/utils/spinner.js.map +1 -0
- package/package.json +10 -5
- package/src/commands/ai-actions/plan.ts +1 -1
- package/src/commands/ai-actions/review.ts +5 -4
- package/src/commands/ai-actions/standup.ts +1 -1
- package/src/commands/ai.ts +1 -1
- package/src/commands/board.ts +10 -5
- package/src/commands/bulk.ts +11 -10
- package/src/commands/config.ts +1 -1
- package/src/commands/dashboard.ts +20 -12
- package/src/commands/filter.ts +8 -5
- package/src/commands/git.ts +1 -1
- package/src/commands/issue-attach.ts +1 -1
- package/src/commands/issue-pr.ts +1 -1
- package/src/commands/issue-worklog.ts +10 -5
- package/src/commands/issue.ts +181 -124
- package/src/commands/project.ts +10 -5
- package/src/commands/sprint.ts +19 -8
- package/src/commands/tui.ts +11 -0
- package/src/server/mcp-server.ts +234 -27
- package/src/services/ai-service.ts +7 -4
- package/src/services/api-service.ts +34 -21
- package/src/tui/App.tsx +61 -0
- package/src/tui/index.tsx +8 -0
- package/src/tui/screens/BoardList.tsx +102 -0
- package/src/tui/screens/Dashboard.tsx +75 -0
- package/src/tui/screens/IssueDetail.tsx +93 -0
- package/src/tui/screens/IssueList.tsx +116 -0
- package/src/tui/screens/KanbanBoard.tsx +133 -0
- package/src/tui/utils/adf-render.ts +30 -0
- package/src/utils/api-paths.ts +32 -0
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/http.ts +128 -0
- package/src/utils/spinner.ts +87 -0
package/src/commands/sprint.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import Table from '
|
|
3
|
+
import { Table } from 'cmd-table';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
|
-
import ora from '
|
|
5
|
+
import ora from '../utils/spinner.js';
|
|
6
6
|
import enquirer from 'enquirer';
|
|
7
7
|
import { handleCommandError } from '../utils/error-handler.js';
|
|
8
8
|
|
|
@@ -56,11 +56,16 @@ Common Actions:
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const table = new Table({
|
|
59
|
-
|
|
59
|
+
columns: [
|
|
60
|
+
{ name: chalk.bold('ID') },
|
|
61
|
+
{ name: chalk.bold('Name') },
|
|
62
|
+
{ name: chalk.bold('State') },
|
|
63
|
+
{ name: chalk.bold('Dates') }
|
|
64
|
+
]
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
data.values.forEach((s: any) => {
|
|
63
|
-
table.
|
|
68
|
+
table.addRow([
|
|
64
69
|
s.id,
|
|
65
70
|
s.name,
|
|
66
71
|
s.state === 'active' ? chalk.green(s.state) : s.state,
|
|
@@ -68,7 +73,7 @@ Common Actions:
|
|
|
68
73
|
]);
|
|
69
74
|
});
|
|
70
75
|
|
|
71
|
-
console.log(table.
|
|
76
|
+
console.log(table.render());
|
|
72
77
|
|
|
73
78
|
} catch (e: any) {
|
|
74
79
|
handleCommandError(spinner, e, 'Failed to list sprints');
|
|
@@ -131,10 +136,16 @@ Examples:
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
const table = new Table({
|
|
134
|
-
|
|
139
|
+
columns: [
|
|
140
|
+
{ name: chalk.bold('Key') },
|
|
141
|
+
{ name: chalk.bold('Summary') },
|
|
142
|
+
{ name: chalk.bold('Status') },
|
|
143
|
+
{ name: chalk.bold('Assignee') },
|
|
144
|
+
{ name: chalk.bold('Priority') }
|
|
145
|
+
]
|
|
135
146
|
});
|
|
136
147
|
issues.issues.forEach((i: any) => {
|
|
137
|
-
table.
|
|
148
|
+
table.addRow([
|
|
138
149
|
chalk.cyan(i.key),
|
|
139
150
|
i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
|
|
140
151
|
i.fields.status?.name || '',
|
|
@@ -142,7 +153,7 @@ Examples:
|
|
|
142
153
|
i.fields.priority?.name || ''
|
|
143
154
|
]);
|
|
144
155
|
});
|
|
145
|
-
console.log(table.
|
|
156
|
+
console.log(table.render());
|
|
146
157
|
console.log(chalk.grey(`${issues.issues.length} issue(s) in sprint`));
|
|
147
158
|
|
|
148
159
|
} catch (e: any) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { startTui } from '../tui/index.js';
|
|
3
|
+
|
|
4
|
+
export function registerTuiCommand(program: Command) {
|
|
5
|
+
program
|
|
6
|
+
.command('tui')
|
|
7
|
+
.description('Start the interactive TUI mode')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
startTui();
|
|
10
|
+
});
|
|
11
|
+
}
|
package/src/server/mcp-server.ts
CHANGED
|
@@ -1,18 +1,54 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
ListPromptsRequestSchema,
|
|
7
|
+
GetPromptRequestSchema,
|
|
8
|
+
ListResourceTemplatesRequestSchema,
|
|
9
|
+
ListResourcesRequestSchema,
|
|
10
|
+
ReadResourceRequestSchema,
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
4
12
|
import { api } from "../services/api-service.js";
|
|
5
13
|
import { textToADF } from "../utils/text-to-adf.js";
|
|
14
|
+
import { readFileSync, existsSync } from "fs";
|
|
15
|
+
import { join, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { API } from "../utils/api-paths.js";
|
|
18
|
+
|
|
19
|
+
// Load package.json for version
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
function getPackageVersion() {
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(__dirname, "../../package.json"),
|
|
25
|
+
join(__dirname, "../../../package.json"),
|
|
26
|
+
join(process.cwd(), "package.json"),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const p of candidates) {
|
|
30
|
+
if (!existsSync(p)) continue;
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
33
|
+
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
34
|
+
} catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
return "0.0.0";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const version = getPackageVersion();
|
|
6
40
|
|
|
7
41
|
// Initialize MCP Server
|
|
8
42
|
const server = new Server(
|
|
9
43
|
{
|
|
10
44
|
name: "jira-pilot",
|
|
11
|
-
version:
|
|
45
|
+
version: version,
|
|
12
46
|
},
|
|
13
47
|
{
|
|
14
48
|
capabilities: {
|
|
15
49
|
tools: {},
|
|
50
|
+
prompts: { listChanged: true },
|
|
51
|
+
resources: { subscribe: false, listChanged: true },
|
|
16
52
|
},
|
|
17
53
|
}
|
|
18
54
|
);
|
|
@@ -200,6 +236,175 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
200
236
|
};
|
|
201
237
|
});
|
|
202
238
|
|
|
239
|
+
// ── Prompt Definitions ────────────────────────────────────────────────
|
|
240
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
241
|
+
return {
|
|
242
|
+
prompts: [
|
|
243
|
+
{
|
|
244
|
+
name: "jira-assist",
|
|
245
|
+
description: "A system prompt to help the LLM understand how to assist with Jira tasks.",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "jira-summarize-issue",
|
|
249
|
+
description: "Summarize a specific Jira issue.",
|
|
250
|
+
arguments: [
|
|
251
|
+
{
|
|
252
|
+
name: "issueKey",
|
|
253
|
+
description: "The key of the issue to summarize (e.g., PROJ-123)",
|
|
254
|
+
required: true
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
263
|
+
const { name, arguments: args } = request.params;
|
|
264
|
+
|
|
265
|
+
if (name === "jira-assist") {
|
|
266
|
+
return {
|
|
267
|
+
messages: [
|
|
268
|
+
{
|
|
269
|
+
role: "user",
|
|
270
|
+
content: {
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `You are Jira Pilot, an intelligent assistant for Jira.
|
|
273
|
+
Your goal is to help users manage their projects, issues, and workflows efficiently.
|
|
274
|
+
|
|
275
|
+
Available Tools:
|
|
276
|
+
- Use 'jira_list_issues' to find issues.
|
|
277
|
+
- Use 'jira_get_issue' to see details.
|
|
278
|
+
- Use 'jira_create_issue', 'jira_update_issue', 'jira_transition_issue' to modify.
|
|
279
|
+
|
|
280
|
+
Guidelines:
|
|
281
|
+
1. Always be concise and helpful.
|
|
282
|
+
2. If the user asks to "fix" something, look for relevant issues first.
|
|
283
|
+
3. When creating issues, ask for clarification if fields are missing (Project, Type).
|
|
284
|
+
4. Use JQL for powerful searching.`
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (name === "jira-summarize-issue") {
|
|
292
|
+
const issueKey = args?.issueKey;
|
|
293
|
+
if (!issueKey) {
|
|
294
|
+
throw new Error("Missing required argument: issueKey");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
messages: [
|
|
299
|
+
{
|
|
300
|
+
role: "user",
|
|
301
|
+
content: {
|
|
302
|
+
type: "text",
|
|
303
|
+
text: `Please fetch details for Jira issue ${issueKey} using 'jira_get_issue', and then provide a concise summary of its status, priority, and recent activity.`
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw new Error(`Prompt not found: ${name}. Available: jira-assist, jira-summarize-issue`);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── Resource Templates ──────────────────────────────────────────────
|
|
314
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
315
|
+
return {
|
|
316
|
+
resourceTemplates: []
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ── Resource Definitions ──────────────────────────────────────────────
|
|
321
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
322
|
+
return {
|
|
323
|
+
resources: [
|
|
324
|
+
{
|
|
325
|
+
uri: "jira://myself",
|
|
326
|
+
name: "My Profile",
|
|
327
|
+
description: "Details of the currently authenticated user.",
|
|
328
|
+
mimeType: "application/json"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
uri: "jira://projects",
|
|
332
|
+
name: "All Projects",
|
|
333
|
+
description: "List of all accessible Jira projects.",
|
|
334
|
+
mimeType: "application/json"
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
341
|
+
const { uri } = request.params;
|
|
342
|
+
|
|
343
|
+
const createEnvelope = (type: string, data: any) => ({
|
|
344
|
+
source: "jira-pilot",
|
|
345
|
+
type,
|
|
346
|
+
data,
|
|
347
|
+
fetchedAt: new Date().toISOString()
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
if (uri === "jira://myself") {
|
|
352
|
+
const myself = await api.get(API.USER.MYSELF);
|
|
353
|
+
// Mask sensitive data if needed, though 'myself' usually implies permission to see own data.
|
|
354
|
+
// keeping it simple for now, but ensuring consistent shape.
|
|
355
|
+
const safeData = {
|
|
356
|
+
accountId: myself.accountId,
|
|
357
|
+
displayName: myself.displayName,
|
|
358
|
+
active: myself.active,
|
|
359
|
+
timeZone: myself.timeZone,
|
|
360
|
+
// Only include email if present, or maybe mask it? User asked to be careful.
|
|
361
|
+
// We'll exclude email to be safe as per user request "do not include email".
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
contents: [{
|
|
366
|
+
uri,
|
|
367
|
+
mimeType: "application/json",
|
|
368
|
+
text: JSON.stringify(createEnvelope("myself", safeData), null, 2)
|
|
369
|
+
}]
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (uri === "jira://projects") {
|
|
374
|
+
const data = await api.get(`${API.PROJECT.SEARCH}?maxResults=50`);
|
|
375
|
+
const projects = (data.values || []).map((p: any) => ({
|
|
376
|
+
key: p.key,
|
|
377
|
+
name: p.name,
|
|
378
|
+
id: p.id,
|
|
379
|
+
style: p.style
|
|
380
|
+
}));
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
contents: [{
|
|
384
|
+
uri,
|
|
385
|
+
mimeType: "application/json",
|
|
386
|
+
text: JSON.stringify(createEnvelope("projects", projects), null, 2)
|
|
387
|
+
}]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
throw new Error(`Resource not found: ${uri}. Available: jira://myself, jira://projects`);
|
|
392
|
+
|
|
393
|
+
} catch (e: any) {
|
|
394
|
+
// Handle Auth/Network errors specifically
|
|
395
|
+
if (e.response?.status === 401 || e.response?.status === 403) {
|
|
396
|
+
throw new Error(`Jira auth is missing or expired. Run 'jira config setup' to authenticate.`);
|
|
397
|
+
}
|
|
398
|
+
if (e.message.includes("Resource not found")) {
|
|
399
|
+
throw e; // Re-throw 404s we generated
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Upstream errors
|
|
403
|
+
const status = e.response?.status || "Unknown";
|
|
404
|
+
throw new Error(`Upstream Jira error (${status}): ${e.message}`);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
203
408
|
// ── Tool Handlers ────────────────────────────────────────────────────
|
|
204
409
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
205
410
|
const { name, arguments: args } = request.params as { name: string; arguments: any };
|
|
@@ -209,7 +414,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
209
414
|
if (name === "jira_list_issues") {
|
|
210
415
|
const jql = args.jql || "";
|
|
211
416
|
const limit = args.limit || 10;
|
|
212
|
-
const data = await api.post(
|
|
417
|
+
const data = await api.post(API.SEARCH.JQL, {
|
|
213
418
|
jql,
|
|
214
419
|
maxResults: limit,
|
|
215
420
|
fields: ['summary', 'status', 'assignee', 'priority', 'created', 'updated']
|
|
@@ -233,7 +438,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
233
438
|
|
|
234
439
|
// ── jira_get_issue ──────────────────────────────────
|
|
235
440
|
if (name === "jira_get_issue") {
|
|
236
|
-
const data = await api.get(
|
|
441
|
+
const data = await api.get(API.ISSUE.GET(args.issueKey));
|
|
237
442
|
|
|
238
443
|
// Return a cleaner summary for agents
|
|
239
444
|
const result = {
|
|
@@ -284,7 +489,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
284
489
|
body.fields.assignee = { accountId: args.assigneeId };
|
|
285
490
|
}
|
|
286
491
|
|
|
287
|
-
const data = await api.post(
|
|
492
|
+
const data = await api.post(API.ISSUE.BASE, body);
|
|
288
493
|
return {
|
|
289
494
|
content: [{ type: "text", text: JSON.stringify({ key: data.key, self: data.self }, null, 2) }]
|
|
290
495
|
};
|
|
@@ -294,8 +499,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
294
499
|
if (name === "jira_transition_issue") {
|
|
295
500
|
if (!args.transitionId) {
|
|
296
501
|
// List available transitions
|
|
297
|
-
const transData = await api.get(
|
|
298
|
-
const issue = await api.get(
|
|
502
|
+
const transData = await api.get(API.ISSUE.TRANSITIONS(args.issueKey));
|
|
503
|
+
const issue = await api.get(`${API.ISSUE.GET(args.issueKey)}?fields=summary,status`);
|
|
299
504
|
|
|
300
505
|
const result = {
|
|
301
506
|
issueKey: args.issueKey,
|
|
@@ -314,7 +519,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
314
519
|
}
|
|
315
520
|
|
|
316
521
|
// Execute transition
|
|
317
|
-
await api.post(
|
|
522
|
+
await api.post(API.ISSUE.TRANSITIONS(args.issueKey), {
|
|
318
523
|
transition: { id: args.transitionId }
|
|
319
524
|
});
|
|
320
525
|
|
|
@@ -329,11 +534,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
329
534
|
|
|
330
535
|
// Resolve "me" to actual account ID
|
|
331
536
|
if (accountId === 'me') {
|
|
332
|
-
const myself = await api.get(
|
|
537
|
+
const myself = await api.get(API.USER.MYSELF);
|
|
333
538
|
accountId = myself.accountId;
|
|
334
539
|
}
|
|
335
540
|
|
|
336
|
-
await api.put(
|
|
541
|
+
await api.put(API.ISSUE.ASSIGNEE(args.issueKey), {
|
|
337
542
|
accountId: accountId || null
|
|
338
543
|
});
|
|
339
544
|
|
|
@@ -344,7 +549,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
344
549
|
|
|
345
550
|
// ── jira_add_comment ────────────────────────────────
|
|
346
551
|
if (name === "jira_add_comment") {
|
|
347
|
-
const data = await api.post(
|
|
552
|
+
const data = await api.post(API.ISSUE.COMMENT(args.issueKey), {
|
|
348
553
|
body: textToADF(args.body)
|
|
349
554
|
});
|
|
350
555
|
|
|
@@ -364,7 +569,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
364
569
|
if (args.assigneeId) {
|
|
365
570
|
let accId = args.assigneeId;
|
|
366
571
|
if (accId === 'me') {
|
|
367
|
-
const myself = await api.get(
|
|
572
|
+
const myself = await api.get(API.USER.MYSELF);
|
|
368
573
|
accId = myself.accountId;
|
|
369
574
|
} else if (accId === 'none') {
|
|
370
575
|
accId = null;
|
|
@@ -379,7 +584,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
379
584
|
};
|
|
380
585
|
}
|
|
381
586
|
|
|
382
|
-
await api.put(
|
|
587
|
+
await api.put(API.ISSUE.GET(args.issueKey), updateBody);
|
|
383
588
|
|
|
384
589
|
return {
|
|
385
590
|
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey }) }]
|
|
@@ -388,12 +593,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
388
593
|
|
|
389
594
|
// ── jira_search_users ───────────────────────────────
|
|
390
595
|
if (name === "jira_search_users") {
|
|
391
|
-
const users = await api.get(
|
|
596
|
+
const users = await api.get(`${API.USER.SEARCH}?query=${encodeURIComponent(args.query)}`);
|
|
392
597
|
|
|
393
598
|
const results = (users || []).map((u: any) => ({
|
|
394
599
|
accountId: u.accountId,
|
|
395
600
|
displayName: u.displayName,
|
|
396
|
-
|
|
601
|
+
// Email excluded for safety
|
|
602
|
+
// email: u.emailAddress,
|
|
397
603
|
active: u.active
|
|
398
604
|
}));
|
|
399
605
|
|
|
@@ -404,11 +610,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
404
610
|
|
|
405
611
|
// ── jira_myself ─────────────────────────────────────
|
|
406
612
|
if (name === "jira_myself") {
|
|
407
|
-
const myself = await api.get(
|
|
613
|
+
const myself = await api.get(API.USER.MYSELF);
|
|
408
614
|
const result = {
|
|
409
615
|
accountId: myself.accountId,
|
|
410
616
|
displayName: myself.displayName,
|
|
411
|
-
|
|
617
|
+
// Email excluded for safety
|
|
618
|
+
// email: myself.emailAddress,
|
|
412
619
|
active: myself.active,
|
|
413
620
|
timeZone: myself.timeZone
|
|
414
621
|
};
|
|
@@ -421,7 +628,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
421
628
|
// ── jira_list_projects ──────────────────────────────
|
|
422
629
|
if (name === "jira_list_projects") {
|
|
423
630
|
const limit = args.limit || 50;
|
|
424
|
-
const data = await api.get(
|
|
631
|
+
const data = await api.get(`${API.PROJECT.SEARCH}?maxResults=${limit}`);
|
|
425
632
|
|
|
426
633
|
const projects = (data.values || []).map((p: any) => ({
|
|
427
634
|
key: p.key,
|
|
@@ -464,7 +671,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
464
671
|
body.comment = textToADF(args.comment);
|
|
465
672
|
}
|
|
466
673
|
|
|
467
|
-
await api.post(
|
|
674
|
+
await api.post(API.ISSUE.WORKLOG(args.issueKey), body);
|
|
468
675
|
|
|
469
676
|
return {
|
|
470
677
|
content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, timeSpent: args.timeSpent }) }]
|
|
@@ -474,7 +681,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
474
681
|
// ── jira_create_subtask ─────────────────────────────
|
|
475
682
|
if (name === "jira_create_subtask") {
|
|
476
683
|
// 1. Fetch parent to get project
|
|
477
|
-
const parent = await api.get(
|
|
684
|
+
const parent = await api.get(`${API.ISSUE.GET(args.parentKey)}?fields=project`);
|
|
478
685
|
const projectKey = parent.fields.project.key;
|
|
479
686
|
|
|
480
687
|
// 2. Find subtask issue type
|
|
@@ -504,13 +711,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
504
711
|
if (args.assigneeId) {
|
|
505
712
|
let accId = args.assigneeId;
|
|
506
713
|
if (accId === 'me') {
|
|
507
|
-
const myself = await api.get(
|
|
714
|
+
const myself = await api.get(API.USER.MYSELF);
|
|
508
715
|
accId = myself.accountId;
|
|
509
716
|
}
|
|
510
717
|
body.fields.assignee = { accountId: accId };
|
|
511
718
|
}
|
|
512
719
|
|
|
513
|
-
const data = await api.post(
|
|
720
|
+
const data = await api.post(API.ISSUE.BASE, body);
|
|
514
721
|
return {
|
|
515
722
|
content: [{ type: "text", text: JSON.stringify({ key: data.key, self: data.self }, null, 2) }]
|
|
516
723
|
};
|
|
@@ -520,15 +727,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
520
727
|
if (name === "jira_add_attachment") {
|
|
521
728
|
try {
|
|
522
729
|
// Dynamically import fs/path to avoid top-level node dependencies if this runs in browser-like env (unlikely but safe)
|
|
523
|
-
const
|
|
524
|
-
const path = await import(
|
|
730
|
+
const fs = await import("node:fs");
|
|
731
|
+
const path = await import("node:path");
|
|
525
732
|
|
|
526
733
|
const filePath = args.filePath;
|
|
527
|
-
const file = await openAsBlob(filePath);
|
|
734
|
+
const file = await fs.openAsBlob(filePath);
|
|
528
735
|
const formData = new FormData();
|
|
529
|
-
formData.append(
|
|
736
|
+
formData.append("file", file, path.basename(filePath));
|
|
530
737
|
|
|
531
|
-
const result = await api.upload(
|
|
738
|
+
const result = await api.upload(API.ISSUE.ATTACHMENTS(args.issueKey), formData);
|
|
532
739
|
|
|
533
740
|
return {
|
|
534
741
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HttpClient } from '../utils/http.js';
|
|
2
2
|
import { getCredentials } from '../utils/config.js';
|
|
3
3
|
|
|
4
4
|
export class AiService {
|
|
@@ -89,7 +89,8 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
89
89
|
|
|
90
90
|
async callOpenAI(key: string, prompt: string): Promise<string> {
|
|
91
91
|
try {
|
|
92
|
-
const
|
|
92
|
+
const client = new HttpClient();
|
|
93
|
+
const response = await client.post('https://api.openai.com/v1/chat/completions', {
|
|
93
94
|
model: 'gpt-4o',
|
|
94
95
|
messages: [{ role: 'user', content: prompt }],
|
|
95
96
|
temperature: 0.7
|
|
@@ -107,8 +108,9 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
107
108
|
// Gemini REST API — uses generativelanguage.googleapis.com
|
|
108
109
|
const model = 'gemini-2.0-flash';
|
|
109
110
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
|
|
111
|
+
const client = new HttpClient();
|
|
110
112
|
|
|
111
|
-
const response = await
|
|
113
|
+
const response = await client.post(url, {
|
|
112
114
|
contents: [{
|
|
113
115
|
parts: [{ text: prompt }]
|
|
114
116
|
}],
|
|
@@ -137,7 +139,8 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
137
139
|
async callAnthropic(key: string, prompt: string): Promise<string> {
|
|
138
140
|
try {
|
|
139
141
|
// Anthropic Messages API
|
|
140
|
-
const
|
|
142
|
+
const client = new HttpClient();
|
|
143
|
+
const response = await client.post('https://api.anthropic.com/v1/messages', {
|
|
141
144
|
model: 'claude-sonnet-4-20250514',
|
|
142
145
|
max_tokens: 2048,
|
|
143
146
|
messages: [{ role: 'user', content: prompt }]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HttpClient } from '../utils/http.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { getCredentials } from '../utils/config.js';
|
|
4
|
+
import { API } from '../utils/api-paths.js';
|
|
4
5
|
|
|
5
6
|
export class ApiService {
|
|
6
7
|
private client: any;
|
|
@@ -28,27 +29,28 @@ export class ApiService {
|
|
|
28
29
|
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`;
|
|
29
30
|
|
|
30
31
|
// Standard REST API v3 client
|
|
31
|
-
this.client =
|
|
32
|
+
this.client = new HttpClient({
|
|
32
33
|
baseURL: `${this._domain}/rest/api/3`,
|
|
33
34
|
headers: {
|
|
34
35
|
'Authorization': authHeader,
|
|
35
|
-
'Accept': 'application/json'
|
|
36
|
-
'Content-Type': 'application/json'
|
|
36
|
+
'Accept': 'application/json'
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Agile REST API v1 client (for boards, sprints, etc.)
|
|
41
|
-
this.agileClient =
|
|
41
|
+
this.agileClient = new HttpClient({
|
|
42
42
|
baseURL: `${this._domain}/rest/agile/1.0`,
|
|
43
43
|
headers: {
|
|
44
44
|
'Authorization': authHeader,
|
|
45
|
-
'Accept': 'application/json'
|
|
46
|
-
'Content-Type': 'application/json'
|
|
45
|
+
'Accept': 'application/json'
|
|
47
46
|
}
|
|
48
47
|
});
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
private async handleRequest(request: Promise<any>) {
|
|
51
|
+
try {
|
|
52
|
+
return await request;
|
|
53
|
+
} catch (error: any) {
|
|
52
54
|
if (error.response) {
|
|
53
55
|
if (error.response.status === 401) {
|
|
54
56
|
console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
|
|
@@ -56,11 +58,8 @@ export class ApiService {
|
|
|
56
58
|
console.error(chalk.red('Access denied. You may not have permission for this resource.'));
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
this.client.interceptors.response.use((r: any) => r, errorInterceptor);
|
|
63
|
-
this.agileClient.interceptors.response.use((r: any) => r, errorInterceptor);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/** @returns {string} The Jira domain URL */
|
|
@@ -81,28 +80,42 @@ export class ApiService {
|
|
|
81
80
|
|
|
82
81
|
async get(url: string, config: any = {}) {
|
|
83
82
|
this.ensureClient();
|
|
84
|
-
const response = await this.client.get(url, config);
|
|
83
|
+
const response = await this.handleRequest(this.client.get(url, config));
|
|
85
84
|
return response.data;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
async post(url: string, data: any, config: any = {}) {
|
|
89
88
|
this.ensureClient();
|
|
90
|
-
const response = await this.client.post(url, data, config);
|
|
89
|
+
const response = await this.handleRequest(this.client.post(url, data, config));
|
|
91
90
|
return response.data;
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
async put(url: string, data: any, config: any = {}) {
|
|
95
94
|
this.ensureClient();
|
|
96
|
-
const response = await this.client.put(url, data, config);
|
|
95
|
+
const response = await this.handleRequest(this.client.put(url, data, config));
|
|
97
96
|
return response.data;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
async delete(url: string, config: any = {}) {
|
|
101
100
|
this.ensureClient();
|
|
102
|
-
const response = await this.client.delete(url, config);
|
|
101
|
+
const response = await this.handleRequest(this.client.delete(url, config));
|
|
103
102
|
return response.data;
|
|
104
103
|
}
|
|
105
104
|
|
|
105
|
+
async search(jql: string, startAt: number = 0, maxResults: number = 50, nextPageToken?: string) {
|
|
106
|
+
const payload: any = {
|
|
107
|
+
jql,
|
|
108
|
+
maxResults,
|
|
109
|
+
fields: ['summary', 'status', 'assignee', 'priority', 'issuetype', 'created', 'updated', 'project']
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (nextPageToken) {
|
|
113
|
+
payload.nextPageToken = nextPageToken;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.post(API.SEARCH.JQL, payload);
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
async upload(url: string, formData: any) {
|
|
107
120
|
this.ensureClient();
|
|
108
121
|
// Jira requires this header for attachments
|
|
@@ -117,7 +130,7 @@ export class ApiService {
|
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
const config = { headers };
|
|
120
|
-
const response = await this.client.post(url, formData, config);
|
|
133
|
+
const response = await this.handleRequest(this.client.post(url, formData, config));
|
|
121
134
|
return response.data;
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -125,13 +138,13 @@ export class ApiService {
|
|
|
125
138
|
|
|
126
139
|
async agileGet(url: string, config: any = {}) {
|
|
127
140
|
this.ensureClient();
|
|
128
|
-
const response = await this.agileClient.get(url, config);
|
|
141
|
+
const response = await this.handleRequest(this.agileClient.get(url, config));
|
|
129
142
|
return response.data;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
async agilePost(url: string, data: any, config: any = {}) {
|
|
133
146
|
this.ensureClient();
|
|
134
|
-
const response = await this.agileClient.post(url, data, config);
|
|
147
|
+
const response = await this.handleRequest(this.agileClient.post(url, data, config));
|
|
135
148
|
return response.data;
|
|
136
149
|
}
|
|
137
150
|
}
|