devto-mcp 0.5.0 → 0.5.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/dist/cli.js +86 -44
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -1
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +577 -22
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +37 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +599 -15
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -17,10 +50,57 @@ exports.listEpics = listEpics;
|
|
|
17
50
|
exports.updateEpic = updateEpic;
|
|
18
51
|
exports.getEpic = getEpic;
|
|
19
52
|
exports.deleteComment = deleteComment;
|
|
53
|
+
exports.addCommentTool = addCommentTool;
|
|
54
|
+
exports.deleteIssueTool = deleteIssueTool;
|
|
55
|
+
exports.listSprints = listSprints;
|
|
56
|
+
exports.createSprintTool = createSprintTool;
|
|
57
|
+
exports.moveToSprintTool = moveToSprintTool;
|
|
58
|
+
exports.manageSprintTool = manageSprintTool;
|
|
20
59
|
exports.getStatus = getStatus;
|
|
60
|
+
exports.searchIssues = searchIssues;
|
|
61
|
+
exports.getCustomFields = getCustomFields;
|
|
62
|
+
exports.linkIssues = linkIssues;
|
|
63
|
+
exports.getIssueLinks = getIssueLinks;
|
|
64
|
+
exports.getLinkTypes = getLinkTypes;
|
|
65
|
+
exports.logWork = logWork;
|
|
66
|
+
exports.getWorklog = getWorklog;
|
|
67
|
+
exports.setEstimate = setEstimate;
|
|
68
|
+
exports.listVersions = listVersions;
|
|
69
|
+
exports.createVersion = createVersion;
|
|
70
|
+
exports.releaseVersion = releaseVersion;
|
|
71
|
+
exports.getBacklog = getBacklog;
|
|
72
|
+
exports.moveToBacklog = moveToBacklog;
|
|
73
|
+
exports.bulkCreateTasks = bulkCreateTasks;
|
|
74
|
+
exports.bulkUpdateTasks = bulkUpdateTasks;
|
|
75
|
+
exports.attachFile = attachFile;
|
|
76
|
+
exports.listAttachments = listAttachments;
|
|
77
|
+
exports.manageWatchers = manageWatchers;
|
|
21
78
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
79
|
+
const fs = __importStar(require("fs"));
|
|
80
|
+
const path = __importStar(require("path"));
|
|
22
81
|
const client_1 = require("./client");
|
|
23
82
|
const config_1 = require("./config");
|
|
83
|
+
// ─── Content sanitization (prompt injection defense) ─────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Wrap user-authored content from Jira in boundary markers so the LLM
|
|
86
|
+
* knows it's untrusted external content, not system instructions.
|
|
87
|
+
*/
|
|
88
|
+
function sanitizeUserContent(text) {
|
|
89
|
+
if (!text)
|
|
90
|
+
return text;
|
|
91
|
+
// Strip common prompt injection patterns
|
|
92
|
+
const cleaned = text
|
|
93
|
+
.replace(/ignore (?:all )?previous instructions/gi, "[filtered]")
|
|
94
|
+
.replace(/you are now/gi, "[filtered]")
|
|
95
|
+
.replace(/system:\s/gi, "[filtered] ");
|
|
96
|
+
return cleaned;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sanitize a task title from Jira (lightweight — just strips injection patterns).
|
|
100
|
+
*/
|
|
101
|
+
function sanitizeTitle(title) {
|
|
102
|
+
return sanitizeUserContent(title);
|
|
103
|
+
}
|
|
24
104
|
// ─── Anthropic system prompt ────────────────────────────────────────────────
|
|
25
105
|
const PLAN_SYSTEM_PROMPT = `You are DevTo, an AI work manager for software developers. Your job is to take a feature description and break it into a structured development plan. Always return a JSON object with this exact structure and nothing else — no preamble, no markdown, just the JSON:
|
|
26
106
|
{
|
|
@@ -91,12 +171,15 @@ function scoreMatch(query, task) {
|
|
|
91
171
|
* { resolved: false, prompt: string } — disambiguation needed
|
|
92
172
|
*/
|
|
93
173
|
async function resolveIssueKey(input) {
|
|
174
|
+
if (!input || typeof input !== "string") {
|
|
175
|
+
return { resolved: false, prompt: "No issue key provided. Please specify a ticket key (e.g. PROJ-5) or describe the task." };
|
|
176
|
+
}
|
|
94
177
|
// If it already looks like a valid issue key, pass through
|
|
95
178
|
if (ISSUE_KEY_PATTERN.test(input.trim().toUpperCase())) {
|
|
96
179
|
return { resolved: true, key: input.trim().toUpperCase() };
|
|
97
180
|
}
|
|
98
|
-
// Fetch all
|
|
99
|
-
const res = await (0, client_1.callApi)("GET", "/api/v1/tasks");
|
|
181
|
+
// Fetch all tasks (including done/in-review) for matching
|
|
182
|
+
const res = await (0, client_1.callApi)("GET", "/api/v1/tasks?all=true");
|
|
100
183
|
if (res.total === 0) {
|
|
101
184
|
return {
|
|
102
185
|
resolved: false,
|
|
@@ -251,7 +334,7 @@ async function createEpic(title, description) {
|
|
|
251
334
|
const res = await (0, client_1.callApi)("POST", "/api/v1/epic", { title, description });
|
|
252
335
|
return `Done. I've tracked that as **${res.key}**.\n${res.url}`;
|
|
253
336
|
}
|
|
254
|
-
async function createTask(title, description, epicKey) {
|
|
337
|
+
async function createTask(title, description, epicKey, issueType) {
|
|
255
338
|
let resolvedEpicKey = epicKey;
|
|
256
339
|
// Resolve ambiguous epic references
|
|
257
340
|
if (epicKey) {
|
|
@@ -261,12 +344,11 @@ async function createTask(title, description, epicKey) {
|
|
|
261
344
|
}
|
|
262
345
|
resolvedEpicKey = resolved.key;
|
|
263
346
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
})
|
|
269
|
-
return `Done. I've tracked that as **${res.key}**.\n${res.url}`;
|
|
347
|
+
const body = { title, description, epic_key: resolvedEpicKey };
|
|
348
|
+
if (issueType)
|
|
349
|
+
body.type = issueType;
|
|
350
|
+
const res = await (0, client_1.callApi)("POST", "/api/v1/task", body);
|
|
351
|
+
return `Done. I've tracked that as **${res.key}** (${issueType ?? "Story"}).\n${res.url}`;
|
|
270
352
|
}
|
|
271
353
|
async function createSubtask(title, description, parentKey) {
|
|
272
354
|
// Resolve ambiguous parent references
|
|
@@ -303,32 +385,65 @@ async function getTasks(epicKey) {
|
|
|
303
385
|
``,
|
|
304
386
|
];
|
|
305
387
|
res.tasks.forEach((task) => {
|
|
306
|
-
lines.push(`**${task.key}** — ${task.title}`);
|
|
388
|
+
lines.push(`**${task.key}** — ${sanitizeTitle(task.title)}`);
|
|
307
389
|
lines.push(` Status: ${task.status} | Type: ${task.type}${task.assignee ? ` | Assignee: ${task.assignee}` : ""}`);
|
|
308
390
|
if (task.parent)
|
|
309
|
-
lines.push(` Parent: ${task.parent.key} — ${task.parent.title}`);
|
|
391
|
+
lines.push(` Parent: ${task.parent.key} — ${sanitizeTitle(task.parent.title)}`);
|
|
310
392
|
lines.push(` ${task.url}`);
|
|
311
393
|
lines.push(``);
|
|
312
394
|
});
|
|
313
395
|
return lines.join("\n");
|
|
314
396
|
}
|
|
315
|
-
async function updateTask(issueKey, status, comment) {
|
|
397
|
+
async function updateTask(issueKey, status, comment, assignee, priority, labels, epicKey, title, description) {
|
|
316
398
|
// Resolve ambiguous references
|
|
317
399
|
const resolved = await resolveIssueKey(issueKey);
|
|
318
400
|
if (!resolved.resolved) {
|
|
319
401
|
return resolved.prompt;
|
|
320
402
|
}
|
|
403
|
+
// Resolve epic key if provided
|
|
404
|
+
let resolvedEpicKey;
|
|
405
|
+
if (epicKey) {
|
|
406
|
+
const epicResolved = await resolveIssueKey(epicKey);
|
|
407
|
+
if (!epicResolved.resolved) {
|
|
408
|
+
return epicResolved.prompt;
|
|
409
|
+
}
|
|
410
|
+
resolvedEpicKey = epicResolved.key;
|
|
411
|
+
}
|
|
321
412
|
const body = {};
|
|
322
413
|
if (status)
|
|
323
414
|
body.status = status;
|
|
324
415
|
if (comment)
|
|
325
416
|
body.comment = comment;
|
|
417
|
+
if (assignee)
|
|
418
|
+
body.assignee = assignee;
|
|
419
|
+
if (priority)
|
|
420
|
+
body.priority = priority;
|
|
421
|
+
if (labels)
|
|
422
|
+
body.labels = labels;
|
|
423
|
+
if (resolvedEpicKey)
|
|
424
|
+
body.epic_key = resolvedEpicKey;
|
|
425
|
+
if (title)
|
|
426
|
+
body.title = title;
|
|
427
|
+
if (description)
|
|
428
|
+
body.description = description;
|
|
326
429
|
const res = await (0, client_1.callApi)("PUT", `/api/v1/task/${resolved.key}`, body);
|
|
327
430
|
const parts = [];
|
|
431
|
+
if (title)
|
|
432
|
+
parts.push(`Renamed **${resolved.key}** to "${title}".`);
|
|
433
|
+
if (description)
|
|
434
|
+
parts.push(`Updated description for **${resolved.key}**.`);
|
|
328
435
|
if (status)
|
|
329
436
|
parts.push(`Moved **${resolved.key}** to \`${status}\`.`);
|
|
330
437
|
if (res.comment)
|
|
331
438
|
parts.push(`Comment added to **${resolved.key}**.`);
|
|
439
|
+
if (assignee)
|
|
440
|
+
parts.push(`Assigned **${resolved.key}** to ${assignee}.`);
|
|
441
|
+
if (priority)
|
|
442
|
+
parts.push(`Set priority to ${priority}.`);
|
|
443
|
+
if (labels)
|
|
444
|
+
parts.push(`Labels set: ${labels.join(", ")}.`);
|
|
445
|
+
if (resolvedEpicKey)
|
|
446
|
+
parts.push(`Linked **${resolved.key}** to epic **${resolvedEpicKey}**.`);
|
|
332
447
|
if (parts.length === 0)
|
|
333
448
|
parts.push(`Updated **${resolved.key}**.`);
|
|
334
449
|
return parts.join(" ");
|
|
@@ -349,7 +464,7 @@ async function getComments(issueKey) {
|
|
|
349
464
|
res.comments.forEach((c) => {
|
|
350
465
|
const date = new Date(c.created).toLocaleDateString();
|
|
351
466
|
lines.push(`**${c.author}** (${date}):`);
|
|
352
|
-
lines.push(c.body);
|
|
467
|
+
lines.push(sanitizeUserContent(c.body));
|
|
353
468
|
lines.push(``);
|
|
354
469
|
});
|
|
355
470
|
return lines.join("\n");
|
|
@@ -375,11 +490,11 @@ async function getProjectSummary() {
|
|
|
375
490
|
}
|
|
376
491
|
lines.push(`### Active Tasks`);
|
|
377
492
|
res.issues.forEach((issue) => {
|
|
378
|
-
const parts = [`**${issue.key}** — ${issue.title} (${issue.status})`];
|
|
493
|
+
const parts = [`**${issue.key}** — ${sanitizeTitle(issue.title)} (${issue.status})`];
|
|
379
494
|
if (issue.assignee)
|
|
380
495
|
parts.push(` Assignee: ${issue.assignee}`);
|
|
381
496
|
if (issue.parent_key)
|
|
382
|
-
parts.push(` Parent: ${issue.parent_key}${issue.epic_name ? ` — ${issue.epic_name}` : ""}`);
|
|
497
|
+
parts.push(` Parent: ${issue.parent_key}${issue.epic_name ? ` — ${sanitizeTitle(issue.epic_name)}` : ""}`);
|
|
383
498
|
lines.push(parts.join("\n"));
|
|
384
499
|
lines.push(``);
|
|
385
500
|
});
|
|
@@ -487,6 +602,94 @@ async function deleteComment(issueKey, commentId) {
|
|
|
487
602
|
await (0, client_1.callApi)("DELETE", `/api/v1/issues/${resolved.key}/comments/${commentId}`);
|
|
488
603
|
return `Comment \`${commentId}\` deleted from **${resolved.key}**.`;
|
|
489
604
|
}
|
|
605
|
+
// ─── Comment, delete, and sprint tools ────────────────────────────────────────
|
|
606
|
+
async function addCommentTool(issueKey, comment) {
|
|
607
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
608
|
+
if (!resolved.resolved) {
|
|
609
|
+
return resolved.prompt;
|
|
610
|
+
}
|
|
611
|
+
const res = await (0, client_1.callApi)("PUT", `/api/v1/task/${resolved.key}`, { comment });
|
|
612
|
+
return res.comment
|
|
613
|
+
? `Comment added to **${resolved.key}**.`
|
|
614
|
+
: `Updated **${resolved.key}** (comment may have been added).`;
|
|
615
|
+
}
|
|
616
|
+
async function deleteIssueTool(issueKey) {
|
|
617
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
618
|
+
if (!resolved.resolved) {
|
|
619
|
+
return resolved.prompt;
|
|
620
|
+
}
|
|
621
|
+
await (0, client_1.callApi)("DELETE", `/api/v1/issue/${resolved.key}`);
|
|
622
|
+
return `**${resolved.key}** has been permanently deleted.`;
|
|
623
|
+
}
|
|
624
|
+
async function listSprints() {
|
|
625
|
+
const res = await (0, client_1.callApi)("GET", "/api/v1/sprints");
|
|
626
|
+
if (res.total === 0) {
|
|
627
|
+
return "No active or upcoming sprints found. Use `create_sprint` to create one.";
|
|
628
|
+
}
|
|
629
|
+
const lines = [`## Sprints (${res.total})`, ``];
|
|
630
|
+
res.sprints.forEach((s) => {
|
|
631
|
+
const dates = s.start_date && s.end_date
|
|
632
|
+
? ` (${new Date(s.start_date).toLocaleDateString()} — ${new Date(s.end_date).toLocaleDateString()})`
|
|
633
|
+
: "";
|
|
634
|
+
lines.push(`**${s.name}** — ${s.state}${dates}`);
|
|
635
|
+
lines.push(` ID: ${s.id}`);
|
|
636
|
+
lines.push(``);
|
|
637
|
+
});
|
|
638
|
+
return lines.join("\n");
|
|
639
|
+
}
|
|
640
|
+
async function createSprintTool(name, startDate, endDate) {
|
|
641
|
+
const body = { name };
|
|
642
|
+
if (startDate)
|
|
643
|
+
body.start_date = startDate;
|
|
644
|
+
if (endDate)
|
|
645
|
+
body.end_date = endDate;
|
|
646
|
+
const res = await (0, client_1.callApi)("POST", "/api/v1/sprints", body);
|
|
647
|
+
return `Sprint **${res.sprint.name}** created (ID: ${res.sprint.id}, state: ${res.sprint.state}).`;
|
|
648
|
+
}
|
|
649
|
+
async function moveToSprintTool(sprintId, issueKeys) {
|
|
650
|
+
// Resolve issue keys semantically
|
|
651
|
+
const resolvedKeys = [];
|
|
652
|
+
for (const key of issueKeys) {
|
|
653
|
+
const resolved = await resolveIssueKey(key);
|
|
654
|
+
if (!resolved.resolved) {
|
|
655
|
+
return resolved.prompt;
|
|
656
|
+
}
|
|
657
|
+
resolvedKeys.push(resolved.key);
|
|
658
|
+
}
|
|
659
|
+
// Resolve sprint by name if not numeric
|
|
660
|
+
let numericSprintId = parseInt(sprintId, 10);
|
|
661
|
+
if (isNaN(numericSprintId)) {
|
|
662
|
+
// Fetch sprints and match by name
|
|
663
|
+
const sprints = await (0, client_1.callApi)("GET", "/api/v1/sprints");
|
|
664
|
+
const match = sprints.sprints.find((s) => s.name.toLowerCase().includes(sprintId.toLowerCase()));
|
|
665
|
+
if (!match) {
|
|
666
|
+
return `No sprint found matching "${sprintId}". Use \`list_sprints\` to see available sprints.`;
|
|
667
|
+
}
|
|
668
|
+
numericSprintId = match.id;
|
|
669
|
+
}
|
|
670
|
+
await (0, client_1.callApi)("POST", `/api/v1/sprints/${numericSprintId}/issues`, {
|
|
671
|
+
issue_keys: resolvedKeys,
|
|
672
|
+
});
|
|
673
|
+
return `Moved **${resolvedKeys.join(", ")}** to sprint ${numericSprintId}.`;
|
|
674
|
+
}
|
|
675
|
+
async function manageSprintTool(sprintId, action) {
|
|
676
|
+
// Resolve sprint by name if not numeric
|
|
677
|
+
let numericSprintId = parseInt(sprintId, 10);
|
|
678
|
+
if (isNaN(numericSprintId)) {
|
|
679
|
+
const sprints = await (0, client_1.callApi)("GET", "/api/v1/sprints");
|
|
680
|
+
const match = sprints.sprints.find((s) => s.name.toLowerCase().includes(sprintId.toLowerCase()));
|
|
681
|
+
if (!match) {
|
|
682
|
+
return `No sprint found matching "${sprintId}". Use \`list_sprints\` to see available sprints.`;
|
|
683
|
+
}
|
|
684
|
+
numericSprintId = match.id;
|
|
685
|
+
}
|
|
686
|
+
const state = action === "start" ? "active" : action === "close" ? "closed" : null;
|
|
687
|
+
if (!state) {
|
|
688
|
+
return `Invalid action "${action}". Use "start" or "close".`;
|
|
689
|
+
}
|
|
690
|
+
await (0, client_1.callApi)("PUT", `/api/v1/sprints/${numericSprintId}`, { state });
|
|
691
|
+
return `Sprint ${numericSprintId} is now **${state}**.`;
|
|
692
|
+
}
|
|
490
693
|
async function getStatus() {
|
|
491
694
|
const res = await (0, client_1.callApi)("GET", "/api/v1/status");
|
|
492
695
|
const lines = [
|
|
@@ -502,6 +705,387 @@ async function getStatus() {
|
|
|
502
705
|
if (res.current_sprint) {
|
|
503
706
|
lines.push(``, `**Current Sprint:** ${res.current_sprint}`);
|
|
504
707
|
}
|
|
708
|
+
if (res.available_issue_types && res.available_issue_types.length > 0) {
|
|
709
|
+
lines.push(``, `**Available Issue Types:** ${res.available_issue_types.join(", ")}`);
|
|
710
|
+
}
|
|
505
711
|
return lines.join("\n");
|
|
506
712
|
}
|
|
713
|
+
async function searchIssues(jql, maxResults) {
|
|
714
|
+
const res = await (0, client_1.callApi)("POST", "/api/v1/search", {
|
|
715
|
+
jql,
|
|
716
|
+
max_results: maxResults ?? 50,
|
|
717
|
+
});
|
|
718
|
+
if (res.total === 0) {
|
|
719
|
+
return `No issues found matching JQL: \`${jql}\``;
|
|
720
|
+
}
|
|
721
|
+
const lines = [
|
|
722
|
+
`## Search Results (${res.total} total)`,
|
|
723
|
+
``,
|
|
724
|
+
`| Key | Title | Status | Type | Assignee |`,
|
|
725
|
+
`|-----|-------|--------|------|----------|`,
|
|
726
|
+
];
|
|
727
|
+
res.issues.forEach((issue) => {
|
|
728
|
+
lines.push(`| ${issue.key} | ${issue.title} | ${issue.status} | ${issue.type} | ${issue.assignee ?? "—"} |`);
|
|
729
|
+
});
|
|
730
|
+
if (res.total > res.issues.length) {
|
|
731
|
+
lines.push(``, `_Showing ${res.issues.length} of ${res.total}. Refine your JQL for more specific results._`);
|
|
732
|
+
}
|
|
733
|
+
return lines.join("\n");
|
|
734
|
+
}
|
|
735
|
+
async function getCustomFields() {
|
|
736
|
+
const res = await (0, client_1.callApi)("GET", "/api/v1/fields");
|
|
737
|
+
if (res.total === 0) {
|
|
738
|
+
return "No custom fields found in the project.";
|
|
739
|
+
}
|
|
740
|
+
const lines = [
|
|
741
|
+
`## Custom Fields (${res.total})`,
|
|
742
|
+
``,
|
|
743
|
+
`| ID | Name | Type | Required |`,
|
|
744
|
+
`|----|------|------|----------|`,
|
|
745
|
+
];
|
|
746
|
+
res.fields.forEach((field) => {
|
|
747
|
+
lines.push(`| ${field.id} | ${field.name} | ${field.type} | ${field.required ? "Yes" : "No"} |`);
|
|
748
|
+
});
|
|
749
|
+
return lines.join("\n");
|
|
750
|
+
}
|
|
751
|
+
// ─── Issue Linking ────────────────────────────────────────────────────────────
|
|
752
|
+
async function linkIssues(issueKey1, issueKey2, linkType) {
|
|
753
|
+
const resolved1 = await resolveIssueKey(issueKey1);
|
|
754
|
+
if (!resolved1.resolved) {
|
|
755
|
+
return resolved1.prompt;
|
|
756
|
+
}
|
|
757
|
+
const resolved2 = await resolveIssueKey(issueKey2);
|
|
758
|
+
if (!resolved2.resolved) {
|
|
759
|
+
return resolved2.prompt;
|
|
760
|
+
}
|
|
761
|
+
await (0, client_1.callApi)("POST", "/api/v1/issues/link", {
|
|
762
|
+
inward_key: resolved1.key,
|
|
763
|
+
outward_key: resolved2.key,
|
|
764
|
+
link_type: linkType,
|
|
765
|
+
});
|
|
766
|
+
return `Linked **${resolved1.key}** → **${resolved2.key}** (${linkType}).`;
|
|
767
|
+
}
|
|
768
|
+
async function getIssueLinks(issueKey) {
|
|
769
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
770
|
+
if (!resolved.resolved) {
|
|
771
|
+
return resolved.prompt;
|
|
772
|
+
}
|
|
773
|
+
const res = await (0, client_1.callApi)("GET", `/api/v1/issues/${resolved.key}/links`);
|
|
774
|
+
if (res.total === 0) {
|
|
775
|
+
return `No links found on **${resolved.key}**.`;
|
|
776
|
+
}
|
|
777
|
+
const lines = [
|
|
778
|
+
`## Links on ${resolved.key} (${res.total})`,
|
|
779
|
+
``,
|
|
780
|
+
];
|
|
781
|
+
res.links.forEach((link) => {
|
|
782
|
+
if (link.inwardIssue) {
|
|
783
|
+
lines.push(`- **${link.type}** ← ${link.inwardIssue.key} — ${link.inwardIssue.title} (${link.inwardIssue.status})`);
|
|
784
|
+
}
|
|
785
|
+
if (link.outwardIssue) {
|
|
786
|
+
lines.push(`- **${link.type}** → ${link.outwardIssue.key} — ${link.outwardIssue.title} (${link.outwardIssue.status})`);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
return lines.join("\n");
|
|
790
|
+
}
|
|
791
|
+
async function getLinkTypes() {
|
|
792
|
+
const res = await (0, client_1.callApi)("GET", "/api/v1/issues/link-types");
|
|
793
|
+
if (res.total === 0) {
|
|
794
|
+
return "No link types available.";
|
|
795
|
+
}
|
|
796
|
+
const lines = [
|
|
797
|
+
`## Link Types (${res.total})`,
|
|
798
|
+
``,
|
|
799
|
+
`| Name | Inward | Outward |`,
|
|
800
|
+
`|------|--------|---------|`,
|
|
801
|
+
];
|
|
802
|
+
res.link_types.forEach((lt) => {
|
|
803
|
+
lines.push(`| ${lt.name} | ${lt.inward} | ${lt.outward} |`);
|
|
804
|
+
});
|
|
805
|
+
return lines.join("\n");
|
|
806
|
+
}
|
|
807
|
+
async function logWork(issueKey, timeSpent, comment) {
|
|
808
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
809
|
+
if (!resolved.resolved) {
|
|
810
|
+
return resolved.prompt;
|
|
811
|
+
}
|
|
812
|
+
const body = { time_spent: timeSpent };
|
|
813
|
+
if (comment)
|
|
814
|
+
body.comment = comment;
|
|
815
|
+
await (0, client_1.callApi)("POST", `/api/v1/issues/${resolved.key}/worklog`, body);
|
|
816
|
+
return `Logged **${timeSpent}** on **${resolved.key}**.${comment ? ` Comment: "${comment}"` : ""}`;
|
|
817
|
+
}
|
|
818
|
+
async function getWorklog(issueKey) {
|
|
819
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
820
|
+
if (!resolved.resolved) {
|
|
821
|
+
return resolved.prompt;
|
|
822
|
+
}
|
|
823
|
+
const res = await (0, client_1.callApi)("GET", `/api/v1/issues/${resolved.key}/worklog`);
|
|
824
|
+
if (res.total === 0) {
|
|
825
|
+
return `No work logged on **${resolved.key}**.`;
|
|
826
|
+
}
|
|
827
|
+
const lines = [
|
|
828
|
+
`## Work Log — ${resolved.key} (${res.total} entries)`,
|
|
829
|
+
``,
|
|
830
|
+
];
|
|
831
|
+
res.worklogs.forEach((entry) => {
|
|
832
|
+
const date = new Date(entry.started).toLocaleDateString();
|
|
833
|
+
lines.push(`**${entry.author}** — ${entry.time_spent} (${date})`);
|
|
834
|
+
if (entry.comment)
|
|
835
|
+
lines.push(` ${entry.comment}`);
|
|
836
|
+
lines.push(``);
|
|
837
|
+
});
|
|
838
|
+
return lines.join("\n");
|
|
839
|
+
}
|
|
840
|
+
async function setEstimate(issueKey, estimate) {
|
|
841
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
842
|
+
if (!resolved.resolved) {
|
|
843
|
+
return resolved.prompt;
|
|
844
|
+
}
|
|
845
|
+
await (0, client_1.callApi)("PUT", `/api/v1/issues/${resolved.key}/time`, {
|
|
846
|
+
original_estimate: estimate,
|
|
847
|
+
});
|
|
848
|
+
return `Set estimate for **${resolved.key}** to **${estimate}**.`;
|
|
849
|
+
}
|
|
850
|
+
async function listVersions() {
|
|
851
|
+
const res = await (0, client_1.callApi)("GET", "/api/v1/versions");
|
|
852
|
+
if (res.total === 0) {
|
|
853
|
+
return "No versions found in the project. Use `create_version` to create one.";
|
|
854
|
+
}
|
|
855
|
+
const lines = [
|
|
856
|
+
`## Versions (${res.total})`,
|
|
857
|
+
``,
|
|
858
|
+
];
|
|
859
|
+
res.versions.forEach((v) => {
|
|
860
|
+
const status = v.released ? "Released" : "Unreleased";
|
|
861
|
+
const date = v.release_date ? ` (${v.release_date})` : "";
|
|
862
|
+
lines.push(`**${v.name}** — ${status}${date}`);
|
|
863
|
+
if (v.description)
|
|
864
|
+
lines.push(` ${v.description}`);
|
|
865
|
+
lines.push(``);
|
|
866
|
+
});
|
|
867
|
+
return lines.join("\n");
|
|
868
|
+
}
|
|
869
|
+
async function createVersion(name, description, releaseDate) {
|
|
870
|
+
const body = { name };
|
|
871
|
+
if (description)
|
|
872
|
+
body.description = description;
|
|
873
|
+
if (releaseDate)
|
|
874
|
+
body.release_date = releaseDate;
|
|
875
|
+
const res = await (0, client_1.callApi)("POST", "/api/v1/versions", body);
|
|
876
|
+
return `Version **${res.name}** created (ID: ${res.id}).`;
|
|
877
|
+
}
|
|
878
|
+
async function releaseVersion(versionName) {
|
|
879
|
+
// Find version by name
|
|
880
|
+
const versions = await (0, client_1.callApi)("GET", "/api/v1/versions");
|
|
881
|
+
const match = versions.versions.find((v) => v.name.toLowerCase() === versionName.toLowerCase() || v.id === versionName);
|
|
882
|
+
if (!match) {
|
|
883
|
+
return `No version found matching "${versionName}". Use \`list_versions\` to see available versions.`;
|
|
884
|
+
}
|
|
885
|
+
if (match.released) {
|
|
886
|
+
return `Version **${match.name}** is already released.`;
|
|
887
|
+
}
|
|
888
|
+
await (0, client_1.callApi)("PUT", `/api/v1/versions/${match.id}`, {
|
|
889
|
+
released: true,
|
|
890
|
+
release_date: new Date().toISOString().split("T")[0],
|
|
891
|
+
});
|
|
892
|
+
return `Version **${match.name}** marked as released.`;
|
|
893
|
+
}
|
|
894
|
+
async function getBacklog() {
|
|
895
|
+
const res = await (0, client_1.callApi)("GET", "/api/v1/backlog");
|
|
896
|
+
if (res.total === 0) {
|
|
897
|
+
return "Backlog is empty — all issues are assigned to sprints.";
|
|
898
|
+
}
|
|
899
|
+
const lines = [
|
|
900
|
+
`## Backlog (${res.total} issues)`,
|
|
901
|
+
``,
|
|
902
|
+
];
|
|
903
|
+
res.issues.forEach((issue) => {
|
|
904
|
+
lines.push(`**${issue.key}** — ${issue.title}`);
|
|
905
|
+
lines.push(` Status: ${issue.status} | Type: ${issue.type}${issue.assignee ? ` | Assignee: ${issue.assignee}` : ""}`);
|
|
906
|
+
if (issue.parent)
|
|
907
|
+
lines.push(` Parent: ${issue.parent.key} — ${issue.parent.title}`);
|
|
908
|
+
lines.push(``);
|
|
909
|
+
});
|
|
910
|
+
return lines.join("\n");
|
|
911
|
+
}
|
|
912
|
+
async function moveToBacklog(issueKeys) {
|
|
913
|
+
const resolvedKeys = [];
|
|
914
|
+
for (const key of issueKeys) {
|
|
915
|
+
const resolved = await resolveIssueKey(key);
|
|
916
|
+
if (!resolved.resolved) {
|
|
917
|
+
return resolved.prompt;
|
|
918
|
+
}
|
|
919
|
+
resolvedKeys.push(resolved.key);
|
|
920
|
+
}
|
|
921
|
+
await (0, client_1.callApi)("POST", "/api/v1/backlog", {
|
|
922
|
+
issue_keys: resolvedKeys,
|
|
923
|
+
});
|
|
924
|
+
return `Moved **${resolvedKeys.join(", ")}** to backlog.`;
|
|
925
|
+
}
|
|
926
|
+
// ─── Bulk Operations ──────────────────────────────────────────────────────────
|
|
927
|
+
async function bulkCreateTasks(tasks) {
|
|
928
|
+
const results = [];
|
|
929
|
+
for (const task of tasks) {
|
|
930
|
+
try {
|
|
931
|
+
const body = { title: task.title, description: task.description };
|
|
932
|
+
if (task.type)
|
|
933
|
+
body.type = task.type;
|
|
934
|
+
if (task.epic_key)
|
|
935
|
+
body.epic_key = task.epic_key;
|
|
936
|
+
const res = await (0, client_1.callApi)("POST", "/api/v1/task", body);
|
|
937
|
+
results.push({ key: res.key, title: task.title });
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
results.push({
|
|
941
|
+
key: "FAILED",
|
|
942
|
+
title: task.title,
|
|
943
|
+
error: err instanceof Error ? err.message : String(err),
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const succeeded = results.filter((r) => r.key !== "FAILED");
|
|
948
|
+
const failed = results.filter((r) => r.key === "FAILED");
|
|
949
|
+
const lines = [
|
|
950
|
+
`## Bulk Create — ${succeeded.length}/${tasks.length} succeeded`,
|
|
951
|
+
``,
|
|
952
|
+
];
|
|
953
|
+
succeeded.forEach((r) => {
|
|
954
|
+
lines.push(`- **${r.key}** — ${r.title}`);
|
|
955
|
+
});
|
|
956
|
+
if (failed.length > 0) {
|
|
957
|
+
lines.push(``, `### Failed (${failed.length})`);
|
|
958
|
+
failed.forEach((r) => {
|
|
959
|
+
lines.push(`- ${r.title}: ${r.error}`);
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return lines.join("\n");
|
|
963
|
+
}
|
|
964
|
+
async function bulkUpdateTasks(updates) {
|
|
965
|
+
const results = [];
|
|
966
|
+
for (const update of updates) {
|
|
967
|
+
const resolved = await resolveIssueKey(update.issue_key);
|
|
968
|
+
if (!resolved.resolved) {
|
|
969
|
+
results.push({ key: update.issue_key, success: false, error: "Could not resolve issue key" });
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const body = {};
|
|
974
|
+
if (update.status)
|
|
975
|
+
body.status = update.status;
|
|
976
|
+
if (update.comment)
|
|
977
|
+
body.comment = update.comment;
|
|
978
|
+
if (update.assignee)
|
|
979
|
+
body.assignee = update.assignee;
|
|
980
|
+
if (update.priority)
|
|
981
|
+
body.priority = update.priority;
|
|
982
|
+
await (0, client_1.callApi)("PUT", `/api/v1/task/${resolved.key}`, body);
|
|
983
|
+
results.push({ key: resolved.key, success: true });
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
results.push({
|
|
987
|
+
key: resolved.key,
|
|
988
|
+
success: false,
|
|
989
|
+
error: err instanceof Error ? err.message : String(err),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const succeeded = results.filter((r) => r.success);
|
|
994
|
+
const failed = results.filter((r) => !r.success);
|
|
995
|
+
const lines = [
|
|
996
|
+
`## Bulk Update — ${succeeded.length}/${updates.length} succeeded`,
|
|
997
|
+
``,
|
|
998
|
+
];
|
|
999
|
+
succeeded.forEach((r) => {
|
|
1000
|
+
lines.push(`- **${r.key}** updated`);
|
|
1001
|
+
});
|
|
1002
|
+
if (failed.length > 0) {
|
|
1003
|
+
lines.push(``, `### Failed (${failed.length})`);
|
|
1004
|
+
failed.forEach((r) => {
|
|
1005
|
+
lines.push(`- **${r.key}**: ${r.error}`);
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
return lines.join("\n");
|
|
1009
|
+
}
|
|
1010
|
+
async function attachFile(issueKey, filePath) {
|
|
1011
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
1012
|
+
if (!resolved.resolved) {
|
|
1013
|
+
return resolved.prompt;
|
|
1014
|
+
}
|
|
1015
|
+
// Read the file from the local filesystem
|
|
1016
|
+
let fileBuffer;
|
|
1017
|
+
try {
|
|
1018
|
+
fileBuffer = fs.readFileSync(filePath);
|
|
1019
|
+
}
|
|
1020
|
+
catch (err) {
|
|
1021
|
+
return `Could not read file at \`${filePath}\`: ${err instanceof Error ? err.message : String(err)}`;
|
|
1022
|
+
}
|
|
1023
|
+
const filename = path.basename(filePath);
|
|
1024
|
+
const contentBase64 = fileBuffer.toString("base64");
|
|
1025
|
+
await (0, client_1.callApi)("POST", `/api/v1/issues/${resolved.key}/attachments`, {
|
|
1026
|
+
filename,
|
|
1027
|
+
content_base64: contentBase64,
|
|
1028
|
+
});
|
|
1029
|
+
return `Attached **${filename}** to **${resolved.key}**.`;
|
|
1030
|
+
}
|
|
1031
|
+
async function listAttachments(issueKey) {
|
|
1032
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
1033
|
+
if (!resolved.resolved) {
|
|
1034
|
+
return resolved.prompt;
|
|
1035
|
+
}
|
|
1036
|
+
const res = await (0, client_1.callApi)("GET", `/api/v1/issues/${resolved.key}/attachments`);
|
|
1037
|
+
if (res.total === 0) {
|
|
1038
|
+
return `No attachments on **${resolved.key}**.`;
|
|
1039
|
+
}
|
|
1040
|
+
const lines = [
|
|
1041
|
+
`## Attachments on ${resolved.key} (${res.total})`,
|
|
1042
|
+
``,
|
|
1043
|
+
];
|
|
1044
|
+
res.attachments.forEach((a) => {
|
|
1045
|
+
const size = a.size < 1024 ? `${a.size}B` : a.size < 1048576 ? `${Math.round(a.size / 1024)}KB` : `${(a.size / 1048576).toFixed(1)}MB`;
|
|
1046
|
+
const date = new Date(a.created).toLocaleDateString();
|
|
1047
|
+
lines.push(`- **${a.filename}** (${size}) — ${a.author}, ${date}`);
|
|
1048
|
+
});
|
|
1049
|
+
return lines.join("\n");
|
|
1050
|
+
}
|
|
1051
|
+
async function manageWatchers(issueKey, action, userName) {
|
|
1052
|
+
const resolved = await resolveIssueKey(issueKey);
|
|
1053
|
+
if (!resolved.resolved) {
|
|
1054
|
+
return resolved.prompt;
|
|
1055
|
+
}
|
|
1056
|
+
switch (action) {
|
|
1057
|
+
case "list": {
|
|
1058
|
+
const res = await (0, client_1.callApi)("GET", `/api/v1/issues/${resolved.key}/watchers`);
|
|
1059
|
+
if (res.total === 0) {
|
|
1060
|
+
return `No watchers on **${resolved.key}**.`;
|
|
1061
|
+
}
|
|
1062
|
+
const lines = [
|
|
1063
|
+
`## Watchers on ${resolved.key} (${res.total})`,
|
|
1064
|
+
``,
|
|
1065
|
+
];
|
|
1066
|
+
res.watchers.forEach((w) => {
|
|
1067
|
+
lines.push(`- ${w.display_name}`);
|
|
1068
|
+
});
|
|
1069
|
+
return lines.join("\n");
|
|
1070
|
+
}
|
|
1071
|
+
case "add": {
|
|
1072
|
+
if (!userName) {
|
|
1073
|
+
return `User name is required for the "add" action. Provide the display name of the person to add as a watcher.`;
|
|
1074
|
+
}
|
|
1075
|
+
await (0, client_1.callApi)("POST", `/api/v1/issues/${resolved.key}/watchers`, {
|
|
1076
|
+
user_name: userName,
|
|
1077
|
+
});
|
|
1078
|
+
return `Added **${userName}** as watcher on **${resolved.key}**.`;
|
|
1079
|
+
}
|
|
1080
|
+
case "remove": {
|
|
1081
|
+
if (!userName) {
|
|
1082
|
+
return `User name is required for the "remove" action. Provide the display name of the person to remove as a watcher.`;
|
|
1083
|
+
}
|
|
1084
|
+
await (0, client_1.callApi)("DELETE", `/api/v1/issues/${resolved.key}/watchers?user_name=${encodeURIComponent(userName)}`);
|
|
1085
|
+
return `Removed **${userName}** as watcher from **${resolved.key}**.`;
|
|
1086
|
+
}
|
|
1087
|
+
default:
|
|
1088
|
+
return `Invalid action "${action}". Use "list", "add", or "remove".`;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
507
1091
|
//# sourceMappingURL=tools.js.map
|