issues-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/chunk-Y53ZARWV.js +1671 -0
- package/dist/chunk-Y53ZARWV.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1671 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var ENV_SPECS = [
|
|
3
|
+
{
|
|
4
|
+
kind: "jira",
|
|
5
|
+
required: ["JIRA_BASE_URL", "JIRA_API_TOKEN", "JIRA_ACCOUNT_EMAIL"],
|
|
6
|
+
secretVars: ["JIRA_API_TOKEN"],
|
|
7
|
+
build: (env) => ({
|
|
8
|
+
baseUrl: env.JIRA_BASE_URL ?? "",
|
|
9
|
+
credentials: {
|
|
10
|
+
type: "apiKey",
|
|
11
|
+
apiKey: env.JIRA_API_TOKEN ?? "",
|
|
12
|
+
accountId: env.JIRA_ACCOUNT_EMAIL ?? ""
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
kind: "redmine",
|
|
18
|
+
required: ["REDMINE_BASE_URL", "REDMINE_API_KEY"],
|
|
19
|
+
secretVars: ["REDMINE_API_KEY"],
|
|
20
|
+
build: (env) => ({
|
|
21
|
+
baseUrl: env.REDMINE_BASE_URL ?? "",
|
|
22
|
+
credentials: { type: "apiKey", apiKey: env.REDMINE_API_KEY ?? "" }
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
function loadConfig(env = process.env) {
|
|
27
|
+
const trackers = [];
|
|
28
|
+
const secrets = [];
|
|
29
|
+
const warnings = [];
|
|
30
|
+
for (const spec of ENV_SPECS) {
|
|
31
|
+
const present = spec.required.filter((name) => hasValue(env[name]));
|
|
32
|
+
if (present.length === 0) continue;
|
|
33
|
+
if (present.length < spec.required.length) {
|
|
34
|
+
const missing = spec.required.filter((name) => !hasValue(env[name]));
|
|
35
|
+
warnings.push(`${spec.kind}: skipped \u2014 missing required env var(s): ${missing.join(", ")}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
trackers.push({ kind: spec.kind, connection: spec.build(env) });
|
|
39
|
+
for (const name of spec.secretVars) {
|
|
40
|
+
const value = env[name];
|
|
41
|
+
if (hasValue(value)) secrets.push(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { trackers, secrets, warnings };
|
|
45
|
+
}
|
|
46
|
+
function hasValue(value) {
|
|
47
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
48
|
+
}
|
|
49
|
+
function makeRedactor(secrets) {
|
|
50
|
+
const real = [...new Set(secrets)].filter((s) => s.length > 0);
|
|
51
|
+
real.sort((a, b) => b.length - a.length);
|
|
52
|
+
return (text) => real.reduce((acc, s) => acc.split(s).join("***"), text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/server.ts
|
|
56
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
57
|
+
|
|
58
|
+
// ../core/dist/errors.js
|
|
59
|
+
var IssueTrackerError = class extends Error {
|
|
60
|
+
constructor(message, options) {
|
|
61
|
+
super(message, options);
|
|
62
|
+
this.name = new.target.name;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var NotFoundError = class extends IssueTrackerError {
|
|
66
|
+
};
|
|
67
|
+
var AuthError = class extends IssueTrackerError {
|
|
68
|
+
};
|
|
69
|
+
var ValidationError = class extends IssueTrackerError {
|
|
70
|
+
fields;
|
|
71
|
+
constructor(message, options) {
|
|
72
|
+
super(message, options);
|
|
73
|
+
this.fields = options?.fields;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var RateLimitError = class extends IssueTrackerError {
|
|
77
|
+
retryAfterSeconds;
|
|
78
|
+
constructor(message, options) {
|
|
79
|
+
super(message, options);
|
|
80
|
+
this.retryAfterSeconds = options?.retryAfterSeconds;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var NotSupportedError = class extends IssueTrackerError {
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ../jira/dist/client.js
|
|
87
|
+
var JiraClient = class {
|
|
88
|
+
baseUrl;
|
|
89
|
+
authHeader;
|
|
90
|
+
constructor(config) {
|
|
91
|
+
const { credentials } = config;
|
|
92
|
+
if (credentials.accountId === void 0 || credentials.accountId === "") {
|
|
93
|
+
throw new AuthError("Jira requires credentials.accountId (the account email) for Basic auth.");
|
|
94
|
+
}
|
|
95
|
+
const token = `${credentials.accountId}:${credentials.apiKey}`;
|
|
96
|
+
this.authHeader = `Basic ${Buffer.from(token).toString("base64")}`;
|
|
97
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
98
|
+
}
|
|
99
|
+
// Perform a request against `/rest/api/3{path}` and return the parsed JSON
|
|
100
|
+
// body, or undefined for empty (204) responses.
|
|
101
|
+
async request(path, options = {}) {
|
|
102
|
+
const url = new URL(`${this.baseUrl}/rest/api/3${path}`);
|
|
103
|
+
if (options.query !== void 0) {
|
|
104
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
105
|
+
if (value !== void 0) {
|
|
106
|
+
url.searchParams.set(key, String(value));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const headers = {
|
|
111
|
+
Authorization: this.authHeader,
|
|
112
|
+
Accept: "application/json"
|
|
113
|
+
};
|
|
114
|
+
const init = {
|
|
115
|
+
method: options.method ?? "GET",
|
|
116
|
+
headers,
|
|
117
|
+
signal: options.signal
|
|
118
|
+
};
|
|
119
|
+
if (options.body !== void 0) {
|
|
120
|
+
headers["Content-Type"] = "application/json";
|
|
121
|
+
init.body = JSON.stringify(options.body);
|
|
122
|
+
}
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
response = await fetch(url, init);
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
throw new IssueTrackerError(`Network error calling Jira: ${String(cause)}`, { cause });
|
|
128
|
+
}
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
await this.throwForStatus(response);
|
|
131
|
+
}
|
|
132
|
+
if (response.status === 204) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
if (text === "") {
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
return JSON.parse(text);
|
|
140
|
+
}
|
|
141
|
+
// Map a non-2xx response onto the typed error hierarchy. Always throws.
|
|
142
|
+
async throwForStatus(response) {
|
|
143
|
+
const status = response.status;
|
|
144
|
+
const raw = await response.text().catch(() => "");
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = raw === "" ? void 0 : JSON.parse(raw);
|
|
148
|
+
} catch {
|
|
149
|
+
parsed = void 0;
|
|
150
|
+
}
|
|
151
|
+
const messages = parsed?.errorMessages ?? [];
|
|
152
|
+
const fields = parsed?.errors;
|
|
153
|
+
const summary = messages.length > 0 ? messages.join("; ") : fields !== void 0 ? Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join("; ") : raw.slice(0, 500);
|
|
154
|
+
switch (status) {
|
|
155
|
+
case 401:
|
|
156
|
+
throw new AuthError(`Jira authentication failed (401): ${summary}`);
|
|
157
|
+
case 403:
|
|
158
|
+
throw new AuthError(`Jira authorization failed (403): ${summary}`);
|
|
159
|
+
case 404:
|
|
160
|
+
throw new NotFoundError(`Jira resource not found (404): ${summary}`);
|
|
161
|
+
case 400:
|
|
162
|
+
case 422:
|
|
163
|
+
throw new ValidationError(`Jira rejected the request (${status}): ${summary}`, {
|
|
164
|
+
fields
|
|
165
|
+
});
|
|
166
|
+
case 429: {
|
|
167
|
+
const retryHeader = response.headers.get("Retry-After");
|
|
168
|
+
const retryAfterSeconds = retryHeader !== null && retryHeader !== "" ? Number(retryHeader) : void 0;
|
|
169
|
+
throw new RateLimitError(`Jira is throttling requests (429): ${summary}`, {
|
|
170
|
+
retryAfterSeconds: Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : void 0
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
throw new IssueTrackerError(`Jira request failed (${status}): ${summary}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ../jira/dist/adf.js
|
|
180
|
+
function adfToText(node) {
|
|
181
|
+
if (node === null || node === void 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return flatten(node).replace(/\n{3,}/g, "\n\n").trim();
|
|
185
|
+
}
|
|
186
|
+
var BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
187
|
+
"paragraph",
|
|
188
|
+
"heading",
|
|
189
|
+
"blockquote",
|
|
190
|
+
"listItem",
|
|
191
|
+
"bulletList",
|
|
192
|
+
"orderedList",
|
|
193
|
+
"codeBlock",
|
|
194
|
+
"rule"
|
|
195
|
+
]);
|
|
196
|
+
function flatten(node) {
|
|
197
|
+
if (node.type === "text") {
|
|
198
|
+
return node.text ?? "";
|
|
199
|
+
}
|
|
200
|
+
if (node.type === "hardBreak") {
|
|
201
|
+
return "\n";
|
|
202
|
+
}
|
|
203
|
+
const children = node.content ?? [];
|
|
204
|
+
const inner = children.map(flatten).join("");
|
|
205
|
+
if (node.type !== void 0 && BLOCK_TYPES.has(node.type)) {
|
|
206
|
+
return `${inner}
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
return inner;
|
|
210
|
+
}
|
|
211
|
+
function textToAdf(text) {
|
|
212
|
+
return {
|
|
213
|
+
type: "doc",
|
|
214
|
+
version: 1,
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "paragraph",
|
|
218
|
+
content: [{ type: "text", text }]
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ../jira/dist/mappers.js
|
|
225
|
+
function mapUser(raw) {
|
|
226
|
+
const user2 = {
|
|
227
|
+
id: raw.accountId,
|
|
228
|
+
name: raw.displayName ?? raw.accountId
|
|
229
|
+
};
|
|
230
|
+
if (raw.emailAddress !== void 0) {
|
|
231
|
+
user2.email = raw.emailAddress;
|
|
232
|
+
}
|
|
233
|
+
return user2;
|
|
234
|
+
}
|
|
235
|
+
function mapStatusCategory(key) {
|
|
236
|
+
switch (key) {
|
|
237
|
+
case "new":
|
|
238
|
+
return "open";
|
|
239
|
+
case "indeterminate":
|
|
240
|
+
return "in_progress";
|
|
241
|
+
case "done":
|
|
242
|
+
return "done";
|
|
243
|
+
default:
|
|
244
|
+
return "unknown";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function mapStatus(raw) {
|
|
248
|
+
return {
|
|
249
|
+
id: raw.id,
|
|
250
|
+
name: raw.name,
|
|
251
|
+
category: mapStatusCategory(raw.statusCategory?.key)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function mapComment(raw) {
|
|
255
|
+
const item = {
|
|
256
|
+
kind: "comment",
|
|
257
|
+
id: raw.id,
|
|
258
|
+
author: raw.author !== void 0 ? mapUser(raw.author) : { id: "unknown", name: "Unknown" },
|
|
259
|
+
body: adfToText(raw.body) ?? "",
|
|
260
|
+
createdAt: raw.created
|
|
261
|
+
};
|
|
262
|
+
if (raw.updated !== void 0) {
|
|
263
|
+
item.updatedAt = raw.updated;
|
|
264
|
+
}
|
|
265
|
+
return item;
|
|
266
|
+
}
|
|
267
|
+
function mapChangelogHistory(raw) {
|
|
268
|
+
return {
|
|
269
|
+
kind: "change",
|
|
270
|
+
id: raw.id,
|
|
271
|
+
author: raw.author !== void 0 ? mapUser(raw.author) : { id: "unknown", name: "Unknown" },
|
|
272
|
+
createdAt: raw.created,
|
|
273
|
+
changes: (raw.items ?? []).map((item) => ({
|
|
274
|
+
field: item.field ?? "",
|
|
275
|
+
from: item.fromString ?? null,
|
|
276
|
+
to: item.toString ?? null
|
|
277
|
+
}))
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function mapIssue(raw) {
|
|
281
|
+
const fields = raw.fields;
|
|
282
|
+
const items = [];
|
|
283
|
+
for (const history of raw.changelog?.histories ?? []) {
|
|
284
|
+
items.push(mapChangelogHistory(history));
|
|
285
|
+
}
|
|
286
|
+
for (const comment of fields.comment?.comments ?? []) {
|
|
287
|
+
items.push(mapComment(comment));
|
|
288
|
+
}
|
|
289
|
+
const status = fields.status !== void 0 ? mapStatus(fields.status) : { id: "unknown", name: "Unknown", category: "unknown" };
|
|
290
|
+
const issue = {
|
|
291
|
+
id: raw.id,
|
|
292
|
+
key: raw.key,
|
|
293
|
+
title: fields.summary ?? "",
|
|
294
|
+
description: adfToText(fields.description),
|
|
295
|
+
status,
|
|
296
|
+
author: fields.reporter !== void 0 && fields.reporter !== null ? mapUser(fields.reporter) : null,
|
|
297
|
+
assignee: fields.assignee !== void 0 && fields.assignee !== null ? mapUser(fields.assignee) : null,
|
|
298
|
+
attributes: {},
|
|
299
|
+
items,
|
|
300
|
+
createdAt: fields.created,
|
|
301
|
+
updatedAt: fields.updated
|
|
302
|
+
};
|
|
303
|
+
if (fields.project !== void 0) {
|
|
304
|
+
issue.projectId = fields.project.id;
|
|
305
|
+
}
|
|
306
|
+
return issue;
|
|
307
|
+
}
|
|
308
|
+
function mapTransition(raw) {
|
|
309
|
+
return {
|
|
310
|
+
id: raw.id,
|
|
311
|
+
name: raw.name,
|
|
312
|
+
to: raw.to !== void 0 ? mapStatus(raw.to) : { id: "unknown", name: "Unknown", category: "unknown" }
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function mapWorklog(raw, issueId) {
|
|
316
|
+
const entry = {
|
|
317
|
+
id: `${issueId}/${raw.id}`,
|
|
318
|
+
issueId,
|
|
319
|
+
user: raw.author !== void 0 ? mapUser(raw.author) : { id: "unknown", name: "Unknown" },
|
|
320
|
+
date: raw.started,
|
|
321
|
+
durationMinutes: Math.round(raw.timeSpentSeconds / 60),
|
|
322
|
+
createdAt: raw.created,
|
|
323
|
+
updatedAt: raw.updated
|
|
324
|
+
};
|
|
325
|
+
const description = adfToText(raw.comment);
|
|
326
|
+
if (description !== null) {
|
|
327
|
+
entry.description = description;
|
|
328
|
+
}
|
|
329
|
+
return entry;
|
|
330
|
+
}
|
|
331
|
+
function attributeToJiraValue(attr) {
|
|
332
|
+
switch (attr.type) {
|
|
333
|
+
case "string":
|
|
334
|
+
case "text":
|
|
335
|
+
case "number":
|
|
336
|
+
case "date":
|
|
337
|
+
case "boolean":
|
|
338
|
+
return attr.value;
|
|
339
|
+
case "enum":
|
|
340
|
+
return { id: attr.value.id };
|
|
341
|
+
case "multi_enum":
|
|
342
|
+
return attr.value.map((option) => ({ id: option.id }));
|
|
343
|
+
case "user":
|
|
344
|
+
return { accountId: attr.value.id };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ../jira/dist/jql.js
|
|
349
|
+
function jqlString(value) {
|
|
350
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
351
|
+
}
|
|
352
|
+
function statusCategoryToJql(category) {
|
|
353
|
+
switch (category) {
|
|
354
|
+
case "open":
|
|
355
|
+
return "To Do";
|
|
356
|
+
case "in_progress":
|
|
357
|
+
return "In Progress";
|
|
358
|
+
case "done":
|
|
359
|
+
return "Done";
|
|
360
|
+
default:
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function buildJql(filter) {
|
|
365
|
+
const clauses = [];
|
|
366
|
+
if (filter.text !== void 0 && filter.text !== "") {
|
|
367
|
+
clauses.push(`text ~ ${jqlString(filter.text)}`);
|
|
368
|
+
}
|
|
369
|
+
if (filter.projectId !== void 0) {
|
|
370
|
+
clauses.push(`project = ${jqlString(filter.projectId)}`);
|
|
371
|
+
}
|
|
372
|
+
if (filter.assigneeId !== void 0) {
|
|
373
|
+
clauses.push(`assignee = ${jqlString(filter.assigneeId)}`);
|
|
374
|
+
}
|
|
375
|
+
if (filter.authorId !== void 0) {
|
|
376
|
+
clauses.push(`reporter = ${jqlString(filter.authorId)}`);
|
|
377
|
+
}
|
|
378
|
+
if (filter.statusCategory !== void 0) {
|
|
379
|
+
const mapped = statusCategoryToJql(filter.statusCategory);
|
|
380
|
+
if (mapped !== void 0) {
|
|
381
|
+
clauses.push(`statusCategory = ${jqlString(mapped)}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (filter.createdAfter !== void 0) {
|
|
385
|
+
clauses.push(`created >= ${jqlString(filter.createdAfter)}`);
|
|
386
|
+
}
|
|
387
|
+
if (filter.createdBefore !== void 0) {
|
|
388
|
+
clauses.push(`created <= ${jqlString(filter.createdBefore)}`);
|
|
389
|
+
}
|
|
390
|
+
if (filter.updatedAfter !== void 0) {
|
|
391
|
+
clauses.push(`updated >= ${jqlString(filter.updatedAfter)}`);
|
|
392
|
+
}
|
|
393
|
+
if (filter.updatedBefore !== void 0) {
|
|
394
|
+
clauses.push(`updated <= ${jqlString(filter.updatedBefore)}`);
|
|
395
|
+
}
|
|
396
|
+
let jql = clauses.join(" AND ");
|
|
397
|
+
if (filter.sort !== void 0) {
|
|
398
|
+
const dir = filter.sort.dir === "desc" ? "DESC" : "ASC";
|
|
399
|
+
jql = `${jql}${jql === "" ? "" : " "}ORDER BY ${filter.sort.field} ${dir}`;
|
|
400
|
+
}
|
|
401
|
+
return jql;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ../jira/dist/issuesTracker.js
|
|
405
|
+
var ISSUE_FIELDS = [
|
|
406
|
+
"summary",
|
|
407
|
+
"description",
|
|
408
|
+
"status",
|
|
409
|
+
"reporter",
|
|
410
|
+
"assignee",
|
|
411
|
+
"project",
|
|
412
|
+
"created",
|
|
413
|
+
"updated"
|
|
414
|
+
];
|
|
415
|
+
function buildFieldsPayload(input) {
|
|
416
|
+
const fields = {};
|
|
417
|
+
if (input.title !== void 0) {
|
|
418
|
+
fields.summary = input.title;
|
|
419
|
+
}
|
|
420
|
+
if (input.description !== void 0) {
|
|
421
|
+
fields.description = input.description === null ? null : textToAdf(input.description);
|
|
422
|
+
}
|
|
423
|
+
if (input.projectId !== void 0) {
|
|
424
|
+
fields.project = { key: input.projectId };
|
|
425
|
+
}
|
|
426
|
+
if (input.assigneeId !== void 0) {
|
|
427
|
+
fields.assignee = input.assigneeId === null ? null : { accountId: input.assigneeId };
|
|
428
|
+
}
|
|
429
|
+
if (input.attributes !== void 0) {
|
|
430
|
+
for (const [key, attr] of Object.entries(input.attributes)) {
|
|
431
|
+
const value = attr;
|
|
432
|
+
if (key === "issuetype") {
|
|
433
|
+
fields.issuetype = value.type === "enum" ? { id: value.value.id } : attributeToJiraValue(value);
|
|
434
|
+
} else {
|
|
435
|
+
fields[key] = attributeToJiraValue(value);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return fields;
|
|
440
|
+
}
|
|
441
|
+
var JiraIssuesTracker = class {
|
|
442
|
+
client;
|
|
443
|
+
constructor(client) {
|
|
444
|
+
this.client = client;
|
|
445
|
+
}
|
|
446
|
+
async capabilities() {
|
|
447
|
+
const fields = [
|
|
448
|
+
{
|
|
449
|
+
id: "project",
|
|
450
|
+
name: "Project",
|
|
451
|
+
type: "enum",
|
|
452
|
+
required: true,
|
|
453
|
+
readOnly: false,
|
|
454
|
+
allowedValues: await this.fetchProjects()
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: "issuetype",
|
|
458
|
+
name: "Issue Type",
|
|
459
|
+
type: "enum",
|
|
460
|
+
required: true,
|
|
461
|
+
readOnly: false,
|
|
462
|
+
allowedValues: await this.fetchIssueTypes()
|
|
463
|
+
},
|
|
464
|
+
{ id: "summary", name: "Summary", type: "string", required: true, readOnly: false },
|
|
465
|
+
{ id: "description", name: "Description", type: "text", required: false, readOnly: false }
|
|
466
|
+
];
|
|
467
|
+
return {
|
|
468
|
+
canCreate: true,
|
|
469
|
+
canUpdate: true,
|
|
470
|
+
canDelete: true,
|
|
471
|
+
canComment: true,
|
|
472
|
+
canTransition: true,
|
|
473
|
+
fields
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
async fetchProjects() {
|
|
477
|
+
const res = await this.client.request("/project/search", {
|
|
478
|
+
query: { maxResults: 100 }
|
|
479
|
+
});
|
|
480
|
+
return (res.values ?? []).map((project) => ({
|
|
481
|
+
id: project.id,
|
|
482
|
+
name: project.name ?? project.key ?? project.id
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
async fetchIssueTypes() {
|
|
486
|
+
const res = await this.client.request("/issuetype");
|
|
487
|
+
return res.map((type) => ({ id: type.id, name: type.name }));
|
|
488
|
+
}
|
|
489
|
+
async search(filter) {
|
|
490
|
+
const body = {
|
|
491
|
+
jql: buildJql(filter),
|
|
492
|
+
fields: ISSUE_FIELDS
|
|
493
|
+
};
|
|
494
|
+
if (filter.limit !== void 0) {
|
|
495
|
+
body.maxResults = filter.limit;
|
|
496
|
+
}
|
|
497
|
+
if (filter.cursor !== void 0) {
|
|
498
|
+
body.nextPageToken = filter.cursor;
|
|
499
|
+
}
|
|
500
|
+
const res = await this.client.request("/search/jql", {
|
|
501
|
+
method: "POST",
|
|
502
|
+
body
|
|
503
|
+
});
|
|
504
|
+
const page = {
|
|
505
|
+
items: (res.issues ?? []).map(mapIssue)
|
|
506
|
+
};
|
|
507
|
+
if (res.nextPageToken !== void 0 && res.isLast !== true) {
|
|
508
|
+
page.nextCursor = res.nextPageToken;
|
|
509
|
+
}
|
|
510
|
+
return page;
|
|
511
|
+
}
|
|
512
|
+
async get(id) {
|
|
513
|
+
const raw = await this.client.request(`/issue/${encodeURIComponent(id)}`, {
|
|
514
|
+
query: { expand: "changelog" }
|
|
515
|
+
});
|
|
516
|
+
const issue = mapIssue(raw);
|
|
517
|
+
const comments = await this.client.request(`/issue/${encodeURIComponent(id)}/comment`);
|
|
518
|
+
const commentItems = (comments.comments ?? []).map(mapComment);
|
|
519
|
+
issue.items = [...issue.items, ...commentItems];
|
|
520
|
+
return issue;
|
|
521
|
+
}
|
|
522
|
+
async create(input) {
|
|
523
|
+
const fields = buildFieldsPayload(input);
|
|
524
|
+
const created = await this.client.request("/issue", {
|
|
525
|
+
method: "POST",
|
|
526
|
+
body: { fields }
|
|
527
|
+
});
|
|
528
|
+
const idOrKey = created.key ?? created.id;
|
|
529
|
+
if (idOrKey === void 0) {
|
|
530
|
+
throw new ValidationError("Jira did not return an id or key for the created issue.");
|
|
531
|
+
}
|
|
532
|
+
return this.get(idOrKey);
|
|
533
|
+
}
|
|
534
|
+
async update(id, input) {
|
|
535
|
+
const fields = buildFieldsPayload(input);
|
|
536
|
+
await this.client.request(`/issue/${encodeURIComponent(id)}`, {
|
|
537
|
+
method: "PUT",
|
|
538
|
+
body: { fields }
|
|
539
|
+
});
|
|
540
|
+
return this.get(id);
|
|
541
|
+
}
|
|
542
|
+
async delete(id) {
|
|
543
|
+
await this.client.request(`/issue/${encodeURIComponent(id)}`, {
|
|
544
|
+
method: "DELETE"
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
async addComment(issueId, body) {
|
|
548
|
+
const created = await this.client.request(`/issue/${encodeURIComponent(issueId)}/comment`, {
|
|
549
|
+
method: "POST",
|
|
550
|
+
body: { body: textToAdf(body) }
|
|
551
|
+
});
|
|
552
|
+
return mapComment(created);
|
|
553
|
+
}
|
|
554
|
+
async getTransitions(issueId) {
|
|
555
|
+
const res = await this.client.request(`/issue/${encodeURIComponent(issueId)}/transitions`);
|
|
556
|
+
return (res.transitions ?? []).map(mapTransition);
|
|
557
|
+
}
|
|
558
|
+
async transition(issueId, transitionId) {
|
|
559
|
+
await this.client.request(`/issue/${encodeURIComponent(issueId)}/transitions`, {
|
|
560
|
+
method: "POST",
|
|
561
|
+
body: { transition: { id: transitionId } }
|
|
562
|
+
});
|
|
563
|
+
return this.get(issueId);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// ../jira/dist/timeTracker.js
|
|
568
|
+
function parseWorklogId(id) {
|
|
569
|
+
const slash = id.indexOf("/");
|
|
570
|
+
if (slash === -1) {
|
|
571
|
+
throw new ValidationError(`Invalid worklog id "${id}". Expected the format "<issueId>/<worklogId>".`);
|
|
572
|
+
}
|
|
573
|
+
return { issueId: id.slice(0, slash), worklogId: id.slice(slash + 1) };
|
|
574
|
+
}
|
|
575
|
+
function toJiraStarted(isoDate) {
|
|
576
|
+
const date = new Date(isoDate);
|
|
577
|
+
if (Number.isNaN(date.getTime())) {
|
|
578
|
+
throw new ValidationError(`Invalid date "${isoDate}" for worklog.`);
|
|
579
|
+
}
|
|
580
|
+
const iso = date.toISOString();
|
|
581
|
+
return iso.replace("Z", "+0000");
|
|
582
|
+
}
|
|
583
|
+
var JiraTimeTracker = class {
|
|
584
|
+
client;
|
|
585
|
+
constructor(client) {
|
|
586
|
+
this.client = client;
|
|
587
|
+
}
|
|
588
|
+
capabilities() {
|
|
589
|
+
return Promise.resolve({
|
|
590
|
+
canCreate: true,
|
|
591
|
+
canUpdate: true,
|
|
592
|
+
canDelete: true,
|
|
593
|
+
requiresIssue: true,
|
|
594
|
+
activities: []
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async search(filter) {
|
|
598
|
+
if (filter.issueId === void 0) {
|
|
599
|
+
throw new NotSupportedError("Jira core worklogs are scoped per issue; set filter.issueId to search worklogs.");
|
|
600
|
+
}
|
|
601
|
+
const res = await this.client.request(`/issue/${encodeURIComponent(filter.issueId)}/worklog`);
|
|
602
|
+
let entries = (res.worklogs ?? []).map((worklog) => mapWorklog(worklog, filter.issueId));
|
|
603
|
+
if (filter.userId !== void 0) {
|
|
604
|
+
entries = entries.filter((entry) => entry.user.id === filter.userId);
|
|
605
|
+
}
|
|
606
|
+
if (filter.from !== void 0) {
|
|
607
|
+
const from = new Date(filter.from).getTime();
|
|
608
|
+
entries = entries.filter((entry) => new Date(entry.date).getTime() >= from);
|
|
609
|
+
}
|
|
610
|
+
if (filter.to !== void 0) {
|
|
611
|
+
const to = new Date(filter.to).getTime();
|
|
612
|
+
entries = entries.filter((entry) => new Date(entry.date).getTime() <= to);
|
|
613
|
+
}
|
|
614
|
+
return { items: entries };
|
|
615
|
+
}
|
|
616
|
+
async get(id) {
|
|
617
|
+
const { issueId, worklogId } = parseWorklogId(id);
|
|
618
|
+
const raw = await this.client.request(`/issue/${encodeURIComponent(issueId)}/worklog/${encodeURIComponent(worklogId)}`);
|
|
619
|
+
return mapWorklog(raw, issueId);
|
|
620
|
+
}
|
|
621
|
+
async create(input) {
|
|
622
|
+
const body = {
|
|
623
|
+
started: toJiraStarted(input.date),
|
|
624
|
+
timeSpentSeconds: input.durationMinutes * 60
|
|
625
|
+
};
|
|
626
|
+
if (input.description !== void 0) {
|
|
627
|
+
body.comment = textToAdf(input.description);
|
|
628
|
+
}
|
|
629
|
+
const raw = await this.client.request(`/issue/${encodeURIComponent(input.issueId)}/worklog`, { method: "POST", body });
|
|
630
|
+
return mapWorklog(raw, input.issueId);
|
|
631
|
+
}
|
|
632
|
+
async update(id, input) {
|
|
633
|
+
const { issueId, worklogId } = parseWorklogId(id);
|
|
634
|
+
const body = {};
|
|
635
|
+
if (input.date !== void 0) {
|
|
636
|
+
body.started = toJiraStarted(input.date);
|
|
637
|
+
}
|
|
638
|
+
if (input.durationMinutes !== void 0) {
|
|
639
|
+
body.timeSpentSeconds = input.durationMinutes * 60;
|
|
640
|
+
}
|
|
641
|
+
if (input.description !== void 0) {
|
|
642
|
+
body.comment = textToAdf(input.description);
|
|
643
|
+
}
|
|
644
|
+
const raw = await this.client.request(`/issue/${encodeURIComponent(issueId)}/worklog/${encodeURIComponent(worklogId)}`, { method: "PUT", body });
|
|
645
|
+
return mapWorklog(raw, issueId);
|
|
646
|
+
}
|
|
647
|
+
async delete(id) {
|
|
648
|
+
const { issueId, worklogId } = parseWorklogId(id);
|
|
649
|
+
await this.client.request(`/issue/${encodeURIComponent(issueId)}/worklog/${encodeURIComponent(worklogId)}`, { method: "DELETE" });
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// ../jira/dist/session.js
|
|
654
|
+
var JiraSession = class {
|
|
655
|
+
tracker;
|
|
656
|
+
user;
|
|
657
|
+
constructor(tracker, user2) {
|
|
658
|
+
this.tracker = tracker;
|
|
659
|
+
this.user = user2;
|
|
660
|
+
}
|
|
661
|
+
currentUser() {
|
|
662
|
+
return Promise.resolve(this.user);
|
|
663
|
+
}
|
|
664
|
+
close() {
|
|
665
|
+
return Promise.resolve();
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
var JiraAuthenticator = class {
|
|
669
|
+
async authenticate(config) {
|
|
670
|
+
const client = new JiraClient(config);
|
|
671
|
+
let me;
|
|
672
|
+
try {
|
|
673
|
+
me = await client.request("/myself");
|
|
674
|
+
} catch (cause) {
|
|
675
|
+
if (cause instanceof AuthError) {
|
|
676
|
+
throw cause;
|
|
677
|
+
}
|
|
678
|
+
throw new AuthError(`Failed to validate Jira credentials: ${String(cause)}`, { cause });
|
|
679
|
+
}
|
|
680
|
+
const tracker = {
|
|
681
|
+
issues: new JiraIssuesTracker(client),
|
|
682
|
+
time: new JiraTimeTracker(client)
|
|
683
|
+
};
|
|
684
|
+
return new JiraSession(tracker, mapUser(me));
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// ../redmine/dist/client.js
|
|
689
|
+
var RedmineClient = class {
|
|
690
|
+
baseUrl;
|
|
691
|
+
apiKey;
|
|
692
|
+
constructor(config) {
|
|
693
|
+
this.apiKey = config.credentials.apiKey;
|
|
694
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
695
|
+
}
|
|
696
|
+
// Perform a request against `{baseUrl}{path}` and return the parsed JSON body,
|
|
697
|
+
// or undefined for empty (204) responses.
|
|
698
|
+
async request(path, options = {}) {
|
|
699
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
700
|
+
if (options.query !== void 0) {
|
|
701
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
702
|
+
if (value === void 0) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (Array.isArray(value)) {
|
|
706
|
+
for (const entry of value) {
|
|
707
|
+
url.searchParams.append(key, entry);
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
url.searchParams.set(key, String(value));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const headers = {
|
|
715
|
+
"X-Redmine-API-Key": this.apiKey,
|
|
716
|
+
Accept: "application/json"
|
|
717
|
+
};
|
|
718
|
+
const init = {
|
|
719
|
+
method: options.method ?? "GET",
|
|
720
|
+
headers,
|
|
721
|
+
signal: options.signal
|
|
722
|
+
};
|
|
723
|
+
if (options.body !== void 0) {
|
|
724
|
+
headers["Content-Type"] = "application/json";
|
|
725
|
+
init.body = JSON.stringify(options.body);
|
|
726
|
+
}
|
|
727
|
+
let response;
|
|
728
|
+
try {
|
|
729
|
+
response = await fetch(url, init);
|
|
730
|
+
} catch (cause) {
|
|
731
|
+
throw new IssueTrackerError(`Network error calling Redmine: ${String(cause)}`, { cause });
|
|
732
|
+
}
|
|
733
|
+
if (!response.ok) {
|
|
734
|
+
await this.throwForStatus(response);
|
|
735
|
+
}
|
|
736
|
+
if (response.status === 204) {
|
|
737
|
+
return void 0;
|
|
738
|
+
}
|
|
739
|
+
const text = await response.text();
|
|
740
|
+
if (text === "") {
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
return JSON.parse(text);
|
|
744
|
+
}
|
|
745
|
+
// Map a non-2xx response onto the typed error hierarchy. Always throws.
|
|
746
|
+
async throwForStatus(response) {
|
|
747
|
+
const status = response.status;
|
|
748
|
+
const raw = await response.text().catch(() => "");
|
|
749
|
+
let parsed;
|
|
750
|
+
try {
|
|
751
|
+
parsed = raw === "" ? void 0 : JSON.parse(raw);
|
|
752
|
+
} catch {
|
|
753
|
+
parsed = void 0;
|
|
754
|
+
}
|
|
755
|
+
const messages = parsed?.errors ?? [];
|
|
756
|
+
const summary = messages.length > 0 ? messages.join("; ") : raw.slice(0, 500);
|
|
757
|
+
switch (status) {
|
|
758
|
+
case 401:
|
|
759
|
+
throw new AuthError(`Redmine authentication failed (401): ${summary}`);
|
|
760
|
+
case 403:
|
|
761
|
+
throw new AuthError(`Redmine authorization failed (403): ${summary}`);
|
|
762
|
+
case 404:
|
|
763
|
+
throw new NotFoundError(`Redmine resource not found (404): ${summary}`);
|
|
764
|
+
case 422:
|
|
765
|
+
throw new ValidationError(`Redmine rejected the request (422): ${summary}`);
|
|
766
|
+
case 429: {
|
|
767
|
+
const retryHeader = response.headers.get("Retry-After");
|
|
768
|
+
const retryAfterSeconds = retryHeader !== null && retryHeader !== "" ? Number(retryHeader) : void 0;
|
|
769
|
+
throw new RateLimitError(`Redmine is throttling requests (429): ${summary}`, {
|
|
770
|
+
retryAfterSeconds: retryAfterSeconds !== void 0 && Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : void 0
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
default:
|
|
774
|
+
throw new IssueTrackerError(`Redmine request failed (${status}): ${summary}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// ../redmine/dist/mappers.js
|
|
780
|
+
function mapCurrentUser(user2) {
|
|
781
|
+
const name = `${user2.firstname ?? ""} ${user2.lastname ?? ""}`.trim();
|
|
782
|
+
const mapped = {
|
|
783
|
+
id: String(user2.id),
|
|
784
|
+
name: name !== "" ? name : user2.name ?? String(user2.id)
|
|
785
|
+
};
|
|
786
|
+
if (user2.mail !== void 0 && user2.mail !== "") {
|
|
787
|
+
mapped.email = user2.mail;
|
|
788
|
+
}
|
|
789
|
+
return mapped;
|
|
790
|
+
}
|
|
791
|
+
function mapNamedUser(ref) {
|
|
792
|
+
return { id: String(ref.id), name: ref.name };
|
|
793
|
+
}
|
|
794
|
+
function categorizeStatus(status) {
|
|
795
|
+
if (status.is_closed === true) {
|
|
796
|
+
return "done";
|
|
797
|
+
}
|
|
798
|
+
if (/progress|doing|in work/i.test(status.name)) {
|
|
799
|
+
return "in_progress";
|
|
800
|
+
}
|
|
801
|
+
return "open";
|
|
802
|
+
}
|
|
803
|
+
function mapStatus2(ref, categories) {
|
|
804
|
+
if (ref === void 0) {
|
|
805
|
+
return { id: "", name: "", category: "unknown" };
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
id: String(ref.id),
|
|
809
|
+
name: ref.name,
|
|
810
|
+
category: categories.get(ref.id) ?? "open"
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function mapCustomField(field) {
|
|
814
|
+
if (field.multiple === true || Array.isArray(field.value)) {
|
|
815
|
+
const values = Array.isArray(field.value) ? field.value : field.value === null ? [] : [field.value];
|
|
816
|
+
return { type: "multi_enum", value: values.map((v) => ({ id: v, name: v })) };
|
|
817
|
+
}
|
|
818
|
+
if (field.value === null) {
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
return { type: "string", value: field.value };
|
|
822
|
+
}
|
|
823
|
+
function mapJournal(journal) {
|
|
824
|
+
const items = [];
|
|
825
|
+
const author = journal.user !== void 0 ? mapNamedUser(journal.user) : { id: "", name: "" };
|
|
826
|
+
if (journal.notes !== void 0 && journal.notes !== "") {
|
|
827
|
+
items.push({
|
|
828
|
+
kind: "comment",
|
|
829
|
+
id: `${journal.id}-note`,
|
|
830
|
+
author,
|
|
831
|
+
body: journal.notes,
|
|
832
|
+
createdAt: journal.created_on
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const details = journal.details ?? [];
|
|
836
|
+
if (details.length > 0) {
|
|
837
|
+
items.push({
|
|
838
|
+
kind: "change",
|
|
839
|
+
id: `${journal.id}-change`,
|
|
840
|
+
author,
|
|
841
|
+
createdAt: journal.created_on,
|
|
842
|
+
changes: details.map((d) => ({
|
|
843
|
+
field: d.name,
|
|
844
|
+
from: d.old_value,
|
|
845
|
+
to: d.new_value
|
|
846
|
+
}))
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return items;
|
|
850
|
+
}
|
|
851
|
+
function mapIssue2(issue, categories) {
|
|
852
|
+
const attributes2 = {};
|
|
853
|
+
for (const field of issue.custom_fields ?? []) {
|
|
854
|
+
const value = mapCustomField(field);
|
|
855
|
+
if (value !== void 0) {
|
|
856
|
+
attributes2[field.name] = value;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const items = [];
|
|
860
|
+
for (const journal of issue.journals ?? []) {
|
|
861
|
+
items.push(...mapJournal(journal));
|
|
862
|
+
}
|
|
863
|
+
const mapped = {
|
|
864
|
+
id: String(issue.id),
|
|
865
|
+
title: issue.subject,
|
|
866
|
+
description: issue.description ?? null,
|
|
867
|
+
status: mapStatus2(issue.status, categories),
|
|
868
|
+
author: issue.author !== void 0 ? mapNamedUser(issue.author) : null,
|
|
869
|
+
assignee: issue.assigned_to !== void 0 ? mapNamedUser(issue.assigned_to) : null,
|
|
870
|
+
attributes: attributes2,
|
|
871
|
+
items,
|
|
872
|
+
createdAt: issue.created_on,
|
|
873
|
+
updatedAt: issue.updated_on
|
|
874
|
+
};
|
|
875
|
+
if (issue.project !== void 0) {
|
|
876
|
+
mapped.projectId = String(issue.project.id);
|
|
877
|
+
}
|
|
878
|
+
return mapped;
|
|
879
|
+
}
|
|
880
|
+
function mapTimeEntry(entry) {
|
|
881
|
+
const mapped = {
|
|
882
|
+
id: String(entry.id),
|
|
883
|
+
issueId: entry.issue !== void 0 ? String(entry.issue.id) : "",
|
|
884
|
+
user: mapNamedUser(entry.user),
|
|
885
|
+
date: entry.spent_on,
|
|
886
|
+
durationMinutes: Math.round(entry.hours * 60),
|
|
887
|
+
createdAt: entry.created_on,
|
|
888
|
+
updatedAt: entry.updated_on
|
|
889
|
+
};
|
|
890
|
+
if (entry.comments !== void 0 && entry.comments !== "") {
|
|
891
|
+
mapped.description = entry.comments;
|
|
892
|
+
}
|
|
893
|
+
if (entry.activity !== void 0) {
|
|
894
|
+
mapped.activity = { id: String(entry.activity.id), name: entry.activity.name };
|
|
895
|
+
}
|
|
896
|
+
return mapped;
|
|
897
|
+
}
|
|
898
|
+
function attributeToRedmineValue(attr) {
|
|
899
|
+
switch (attr.type) {
|
|
900
|
+
case "string":
|
|
901
|
+
case "text":
|
|
902
|
+
return attr.value;
|
|
903
|
+
case "number":
|
|
904
|
+
return String(attr.value);
|
|
905
|
+
case "date":
|
|
906
|
+
return attr.value;
|
|
907
|
+
case "boolean":
|
|
908
|
+
return attr.value ? "1" : "0";
|
|
909
|
+
case "enum":
|
|
910
|
+
return attr.value.id;
|
|
911
|
+
case "multi_enum":
|
|
912
|
+
return attr.value.map((o) => o.id);
|
|
913
|
+
case "user":
|
|
914
|
+
return attr.value.id;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ../redmine/dist/issuesTracker.js
|
|
919
|
+
var MAX_LIMIT = 100;
|
|
920
|
+
var DEFAULT_LIMIT = 25;
|
|
921
|
+
var RedmineIssuesTracker = class {
|
|
922
|
+
client;
|
|
923
|
+
statusCache;
|
|
924
|
+
categoryCache;
|
|
925
|
+
constructor(client) {
|
|
926
|
+
this.client = client;
|
|
927
|
+
}
|
|
928
|
+
// Fetch and cache the issue-status definitions. Used both to derive status
|
|
929
|
+
// categories and to expose transitions.
|
|
930
|
+
async loadStatuses() {
|
|
931
|
+
if (this.statusCache === void 0) {
|
|
932
|
+
const res = await this.client.request("/issue_statuses.json");
|
|
933
|
+
this.statusCache = res.issue_statuses;
|
|
934
|
+
}
|
|
935
|
+
return this.statusCache;
|
|
936
|
+
}
|
|
937
|
+
// Build (and cache) the status-id to category lookup.
|
|
938
|
+
async categories() {
|
|
939
|
+
if (this.categoryCache === void 0) {
|
|
940
|
+
const statuses = await this.loadStatuses();
|
|
941
|
+
const map = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const status of statuses) {
|
|
943
|
+
map.set(status.id, categorizeStatus(status));
|
|
944
|
+
}
|
|
945
|
+
this.categoryCache = map;
|
|
946
|
+
}
|
|
947
|
+
return this.categoryCache;
|
|
948
|
+
}
|
|
949
|
+
async capabilities() {
|
|
950
|
+
const fields = [];
|
|
951
|
+
const projectOptions = await this.fetchOptions(() => this.client.request("/projects.json", {
|
|
952
|
+
query: { limit: MAX_LIMIT }
|
|
953
|
+
}), (res) => res.projects);
|
|
954
|
+
fields.push({
|
|
955
|
+
id: "project",
|
|
956
|
+
name: "Project",
|
|
957
|
+
type: "enum",
|
|
958
|
+
required: true,
|
|
959
|
+
readOnly: false,
|
|
960
|
+
allowedValues: projectOptions
|
|
961
|
+
});
|
|
962
|
+
const trackerOptions = await this.fetchOptions(() => this.client.request("/trackers.json"), (res) => res.trackers);
|
|
963
|
+
fields.push({
|
|
964
|
+
id: "tracker",
|
|
965
|
+
name: "Tracker",
|
|
966
|
+
type: "enum",
|
|
967
|
+
required: true,
|
|
968
|
+
readOnly: false,
|
|
969
|
+
allowedValues: trackerOptions
|
|
970
|
+
});
|
|
971
|
+
fields.push({
|
|
972
|
+
id: "subject",
|
|
973
|
+
name: "Subject",
|
|
974
|
+
type: "string",
|
|
975
|
+
required: true,
|
|
976
|
+
readOnly: false
|
|
977
|
+
});
|
|
978
|
+
fields.push({
|
|
979
|
+
id: "description",
|
|
980
|
+
name: "Description",
|
|
981
|
+
type: "text",
|
|
982
|
+
required: false,
|
|
983
|
+
readOnly: false
|
|
984
|
+
});
|
|
985
|
+
const statuses = await this.loadStatuses();
|
|
986
|
+
fields.push({
|
|
987
|
+
id: "status",
|
|
988
|
+
name: "Status",
|
|
989
|
+
type: "enum",
|
|
990
|
+
required: false,
|
|
991
|
+
readOnly: false,
|
|
992
|
+
allowedValues: statuses.map((s) => ({ id: String(s.id), name: s.name }))
|
|
993
|
+
});
|
|
994
|
+
try {
|
|
995
|
+
const res = await this.client.request("/custom_fields.json");
|
|
996
|
+
for (const cf of res.custom_fields) {
|
|
997
|
+
fields.push({
|
|
998
|
+
id: `cf_${cf.id}`,
|
|
999
|
+
name: cf.name,
|
|
1000
|
+
type: "string",
|
|
1001
|
+
required: cf.is_required ?? false,
|
|
1002
|
+
readOnly: false
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
canCreate: true,
|
|
1009
|
+
canUpdate: true,
|
|
1010
|
+
canDelete: true,
|
|
1011
|
+
canComment: true,
|
|
1012
|
+
canTransition: true,
|
|
1013
|
+
fields
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
// Helper to fetch a reference list and map it to FieldOption[], swallowing
|
|
1017
|
+
// failures (returns []) so capabilities() degrades gracefully.
|
|
1018
|
+
async fetchOptions(fetcher, extract) {
|
|
1019
|
+
try {
|
|
1020
|
+
const res = await fetcher();
|
|
1021
|
+
return extract(res).map((o) => ({ id: String(o.id), name: o.name }));
|
|
1022
|
+
} catch {
|
|
1023
|
+
return [];
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
async search(filter) {
|
|
1027
|
+
const categories = await this.categories();
|
|
1028
|
+
const limit = Math.min(filter.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
1029
|
+
const offset = filter.cursor !== void 0 ? Number(filter.cursor) : 0;
|
|
1030
|
+
const startOffset = Number.isFinite(offset) && offset > 0 ? offset : 0;
|
|
1031
|
+
const query = {
|
|
1032
|
+
offset: startOffset,
|
|
1033
|
+
limit
|
|
1034
|
+
};
|
|
1035
|
+
if (filter.projectId !== void 0) {
|
|
1036
|
+
query["project_id"] = filter.projectId;
|
|
1037
|
+
}
|
|
1038
|
+
if (filter.assigneeId !== void 0) {
|
|
1039
|
+
query["assigned_to_id"] = filter.assigneeId;
|
|
1040
|
+
}
|
|
1041
|
+
if (filter.authorId !== void 0) {
|
|
1042
|
+
query["author_id"] = filter.authorId;
|
|
1043
|
+
}
|
|
1044
|
+
let clientSideInProgress = false;
|
|
1045
|
+
if (filter.statusCategory === "open") {
|
|
1046
|
+
query["status_id"] = "open";
|
|
1047
|
+
} else if (filter.statusCategory === "done") {
|
|
1048
|
+
query["status_id"] = "closed";
|
|
1049
|
+
} else if (filter.statusCategory === "in_progress") {
|
|
1050
|
+
query["status_id"] = "open";
|
|
1051
|
+
clientSideInProgress = true;
|
|
1052
|
+
}
|
|
1053
|
+
const created = buildDateRange(filter.createdAfter, filter.createdBefore);
|
|
1054
|
+
if (created !== void 0) {
|
|
1055
|
+
query["created_on"] = created;
|
|
1056
|
+
}
|
|
1057
|
+
const updated = buildDateRange(filter.updatedAfter, filter.updatedBefore);
|
|
1058
|
+
if (updated !== void 0) {
|
|
1059
|
+
query["updated_on"] = updated;
|
|
1060
|
+
}
|
|
1061
|
+
if (filter.sort !== void 0) {
|
|
1062
|
+
query["sort"] = `${filter.sort.field}:${filter.sort.dir}`;
|
|
1063
|
+
}
|
|
1064
|
+
if (filter.text !== void 0 && filter.text !== "") {
|
|
1065
|
+
query["f[]"] = ["subject"];
|
|
1066
|
+
query["op[subject]"] = "~";
|
|
1067
|
+
query["v[subject][]"] = filter.text;
|
|
1068
|
+
}
|
|
1069
|
+
const res = await this.client.request("/issues.json", { query });
|
|
1070
|
+
let items = res.issues.map((i) => mapIssue2(i, categories));
|
|
1071
|
+
if (clientSideInProgress) {
|
|
1072
|
+
items = items.filter((i) => i.status.category === "in_progress");
|
|
1073
|
+
}
|
|
1074
|
+
const page = { items, total: res.total_count };
|
|
1075
|
+
const nextOffset = res.offset + res.limit;
|
|
1076
|
+
if (nextOffset < res.total_count) {
|
|
1077
|
+
page.nextCursor = String(nextOffset);
|
|
1078
|
+
}
|
|
1079
|
+
return page;
|
|
1080
|
+
}
|
|
1081
|
+
async get(id) {
|
|
1082
|
+
const categories = await this.categories();
|
|
1083
|
+
const res = await this.client.request(`/issues/${id}.json`, {
|
|
1084
|
+
query: { include: "journals" }
|
|
1085
|
+
});
|
|
1086
|
+
return mapIssue2(res.issue, categories);
|
|
1087
|
+
}
|
|
1088
|
+
async create(input) {
|
|
1089
|
+
const categories = await this.categories();
|
|
1090
|
+
const attributes2 = input.attributes ?? {};
|
|
1091
|
+
const trackerId = readEnumAttribute(attributes2, "tracker");
|
|
1092
|
+
if (trackerId === void 0) {
|
|
1093
|
+
throw new ValidationError("Redmine requires a `tracker` attribute (enum) to create an issue.");
|
|
1094
|
+
}
|
|
1095
|
+
const body = {
|
|
1096
|
+
subject: input.title,
|
|
1097
|
+
tracker_id: Number(trackerId)
|
|
1098
|
+
};
|
|
1099
|
+
if (input.projectId !== void 0) {
|
|
1100
|
+
body.project_id = Number(input.projectId);
|
|
1101
|
+
}
|
|
1102
|
+
if (input.description !== void 0) {
|
|
1103
|
+
body.description = input.description;
|
|
1104
|
+
}
|
|
1105
|
+
if (input.assigneeId !== void 0 && input.assigneeId !== null) {
|
|
1106
|
+
body.assigned_to_id = Number(input.assigneeId);
|
|
1107
|
+
}
|
|
1108
|
+
const statusId = readEnumAttribute(attributes2, "status");
|
|
1109
|
+
if (statusId !== void 0) {
|
|
1110
|
+
body.status_id = Number(statusId);
|
|
1111
|
+
}
|
|
1112
|
+
const customFields = buildCustomFields(attributes2);
|
|
1113
|
+
if (customFields.length > 0) {
|
|
1114
|
+
body.custom_fields = customFields;
|
|
1115
|
+
}
|
|
1116
|
+
const res = await this.client.request("/issues.json", {
|
|
1117
|
+
method: "POST",
|
|
1118
|
+
body: { issue: body }
|
|
1119
|
+
});
|
|
1120
|
+
return mapIssue2(res.issue, categories);
|
|
1121
|
+
}
|
|
1122
|
+
async update(id, input) {
|
|
1123
|
+
const body = {};
|
|
1124
|
+
if (input.title !== void 0) {
|
|
1125
|
+
body.subject = input.title;
|
|
1126
|
+
}
|
|
1127
|
+
if (input.description !== void 0) {
|
|
1128
|
+
body.description = input.description;
|
|
1129
|
+
}
|
|
1130
|
+
if (input.projectId !== void 0) {
|
|
1131
|
+
body.project_id = Number(input.projectId);
|
|
1132
|
+
}
|
|
1133
|
+
if (input.assigneeId !== void 0) {
|
|
1134
|
+
body.assigned_to_id = input.assigneeId === null ? null : Number(input.assigneeId);
|
|
1135
|
+
}
|
|
1136
|
+
if (input.attributes !== void 0) {
|
|
1137
|
+
const statusId = readEnumAttribute(input.attributes, "status");
|
|
1138
|
+
if (statusId !== void 0) {
|
|
1139
|
+
body.status_id = Number(statusId);
|
|
1140
|
+
}
|
|
1141
|
+
const customFields = buildCustomFields(input.attributes);
|
|
1142
|
+
if (customFields.length > 0) {
|
|
1143
|
+
body.custom_fields = customFields;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
await this.client.request(`/issues/${id}.json`, {
|
|
1147
|
+
method: "PUT",
|
|
1148
|
+
body: { issue: body }
|
|
1149
|
+
});
|
|
1150
|
+
return this.get(id);
|
|
1151
|
+
}
|
|
1152
|
+
async delete(id) {
|
|
1153
|
+
await this.client.request(`/issues/${id}.json`, { method: "DELETE" });
|
|
1154
|
+
}
|
|
1155
|
+
async addComment(issueId, body) {
|
|
1156
|
+
await this.client.request(`/issues/${issueId}.json`, {
|
|
1157
|
+
method: "PUT",
|
|
1158
|
+
body: { issue: { notes: body } }
|
|
1159
|
+
});
|
|
1160
|
+
const res = await this.client.request(`/issues/${issueId}.json`, {
|
|
1161
|
+
query: { include: "journals" }
|
|
1162
|
+
});
|
|
1163
|
+
const journals = res.issue.journals ?? [];
|
|
1164
|
+
for (let i = journals.length - 1; i >= 0; i--) {
|
|
1165
|
+
const journal = journals[i];
|
|
1166
|
+
if (journal !== void 0 && journal.notes !== void 0 && journal.notes !== "") {
|
|
1167
|
+
const comment = mapJournal(journal).find((item) => item.kind === "comment");
|
|
1168
|
+
if (comment !== void 0) {
|
|
1169
|
+
return comment;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
throw new NotFoundError(`Comment was added to issue ${issueId} but could not be retrieved.`);
|
|
1174
|
+
}
|
|
1175
|
+
// Redmine does not enforce workflow transitions through the REST API: status
|
|
1176
|
+
// is set directly. We therefore offer every issue status as a transition.
|
|
1177
|
+
async getTransitions() {
|
|
1178
|
+
const categories = await this.categories();
|
|
1179
|
+
const statuses = await this.loadStatuses();
|
|
1180
|
+
return statuses.map((s) => {
|
|
1181
|
+
const to = mapStatus2({ id: s.id, name: s.name }, categories);
|
|
1182
|
+
return { id: String(s.id), name: s.name, to };
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
async transition(issueId, transitionId) {
|
|
1186
|
+
await this.client.request(`/issues/${issueId}.json`, {
|
|
1187
|
+
method: "PUT",
|
|
1188
|
+
body: { issue: { status_id: Number(transitionId) } }
|
|
1189
|
+
});
|
|
1190
|
+
return this.get(issueId);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
function buildDateRange(after, before) {
|
|
1194
|
+
if (after !== void 0 && before !== void 0) {
|
|
1195
|
+
return `><${after}|${before}`;
|
|
1196
|
+
}
|
|
1197
|
+
if (after !== void 0) {
|
|
1198
|
+
return `>=${after}`;
|
|
1199
|
+
}
|
|
1200
|
+
if (before !== void 0) {
|
|
1201
|
+
return `<=${before}`;
|
|
1202
|
+
}
|
|
1203
|
+
return void 0;
|
|
1204
|
+
}
|
|
1205
|
+
function readEnumAttribute(attributes2, key) {
|
|
1206
|
+
const attr = attributes2[key];
|
|
1207
|
+
if (attr === void 0) {
|
|
1208
|
+
return void 0;
|
|
1209
|
+
}
|
|
1210
|
+
if (attr.type === "enum") {
|
|
1211
|
+
return attr.value.id;
|
|
1212
|
+
}
|
|
1213
|
+
if (attr.type === "string" || attr.type === "number") {
|
|
1214
|
+
return String(attr.value);
|
|
1215
|
+
}
|
|
1216
|
+
return void 0;
|
|
1217
|
+
}
|
|
1218
|
+
function buildCustomFields(attributes2) {
|
|
1219
|
+
const result = [];
|
|
1220
|
+
for (const [key, attr] of Object.entries(attributes2)) {
|
|
1221
|
+
const match = /^cf_(\d+)$/.exec(key);
|
|
1222
|
+
if (match === null || match[1] === void 0) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
result.push({ id: Number(match[1]), value: attributeToRedmineValue(attr) });
|
|
1226
|
+
}
|
|
1227
|
+
return result;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// ../redmine/dist/timeTracker.js
|
|
1231
|
+
var MAX_LIMIT2 = 100;
|
|
1232
|
+
var DEFAULT_LIMIT2 = 25;
|
|
1233
|
+
var RedmineTimeTracker = class {
|
|
1234
|
+
client;
|
|
1235
|
+
constructor(client) {
|
|
1236
|
+
this.client = client;
|
|
1237
|
+
}
|
|
1238
|
+
async capabilities() {
|
|
1239
|
+
let activities = [];
|
|
1240
|
+
try {
|
|
1241
|
+
const res = await this.client.request("/enumerations/time_entry_activities.json");
|
|
1242
|
+
activities = res.time_entry_activities.map((a) => ({ id: String(a.id), name: a.name }));
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
canCreate: true,
|
|
1247
|
+
canUpdate: true,
|
|
1248
|
+
canDelete: true,
|
|
1249
|
+
requiresIssue: false,
|
|
1250
|
+
activities
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
async search(filter) {
|
|
1254
|
+
const limit = Math.min(filter.limit ?? DEFAULT_LIMIT2, MAX_LIMIT2);
|
|
1255
|
+
const offset = filter.cursor !== void 0 ? Number(filter.cursor) : 0;
|
|
1256
|
+
const startOffset = Number.isFinite(offset) && offset > 0 ? offset : 0;
|
|
1257
|
+
const query = {
|
|
1258
|
+
offset: startOffset,
|
|
1259
|
+
limit
|
|
1260
|
+
};
|
|
1261
|
+
if (filter.issueId !== void 0) {
|
|
1262
|
+
query["issue_id"] = filter.issueId;
|
|
1263
|
+
}
|
|
1264
|
+
if (filter.userId !== void 0) {
|
|
1265
|
+
query["user_id"] = filter.userId;
|
|
1266
|
+
}
|
|
1267
|
+
if (filter.projectId !== void 0) {
|
|
1268
|
+
query["project_id"] = filter.projectId;
|
|
1269
|
+
}
|
|
1270
|
+
if (filter.from !== void 0) {
|
|
1271
|
+
query["from"] = filter.from;
|
|
1272
|
+
}
|
|
1273
|
+
if (filter.to !== void 0) {
|
|
1274
|
+
query["to"] = filter.to;
|
|
1275
|
+
}
|
|
1276
|
+
if (filter.sort !== void 0) {
|
|
1277
|
+
query["sort"] = `${filter.sort.field}:${filter.sort.dir}`;
|
|
1278
|
+
}
|
|
1279
|
+
const res = await this.client.request("/time_entries.json", {
|
|
1280
|
+
query
|
|
1281
|
+
});
|
|
1282
|
+
const page = {
|
|
1283
|
+
items: res.time_entries.map(mapTimeEntry),
|
|
1284
|
+
total: res.total_count
|
|
1285
|
+
};
|
|
1286
|
+
const nextOffset = res.offset + res.limit;
|
|
1287
|
+
if (nextOffset < res.total_count) {
|
|
1288
|
+
page.nextCursor = String(nextOffset);
|
|
1289
|
+
}
|
|
1290
|
+
return page;
|
|
1291
|
+
}
|
|
1292
|
+
async get(id) {
|
|
1293
|
+
const res = await this.client.request(`/time_entries/${id}.json`);
|
|
1294
|
+
return mapTimeEntry(res.time_entry);
|
|
1295
|
+
}
|
|
1296
|
+
async create(input) {
|
|
1297
|
+
const body = {
|
|
1298
|
+
issue_id: Number(input.issueId),
|
|
1299
|
+
hours: input.durationMinutes / 60,
|
|
1300
|
+
spent_on: input.date
|
|
1301
|
+
};
|
|
1302
|
+
if (input.activityId !== void 0) {
|
|
1303
|
+
body.activity_id = Number(input.activityId);
|
|
1304
|
+
}
|
|
1305
|
+
if (input.description !== void 0) {
|
|
1306
|
+
body.comments = input.description;
|
|
1307
|
+
}
|
|
1308
|
+
const res = await this.client.request("/time_entries.json", {
|
|
1309
|
+
method: "POST",
|
|
1310
|
+
body: { time_entry: body }
|
|
1311
|
+
});
|
|
1312
|
+
return mapTimeEntry(res.time_entry);
|
|
1313
|
+
}
|
|
1314
|
+
async update(id, input) {
|
|
1315
|
+
const body = {};
|
|
1316
|
+
if (input.issueId !== void 0) {
|
|
1317
|
+
body.issue_id = Number(input.issueId);
|
|
1318
|
+
}
|
|
1319
|
+
if (input.durationMinutes !== void 0) {
|
|
1320
|
+
body.hours = input.durationMinutes / 60;
|
|
1321
|
+
}
|
|
1322
|
+
if (input.date !== void 0) {
|
|
1323
|
+
body.spent_on = input.date;
|
|
1324
|
+
}
|
|
1325
|
+
if (input.activityId !== void 0) {
|
|
1326
|
+
body.activity_id = Number(input.activityId);
|
|
1327
|
+
}
|
|
1328
|
+
if (input.description !== void 0) {
|
|
1329
|
+
body.comments = input.description;
|
|
1330
|
+
}
|
|
1331
|
+
await this.client.request(`/time_entries/${id}.json`, {
|
|
1332
|
+
method: "PUT",
|
|
1333
|
+
body: { time_entry: body }
|
|
1334
|
+
});
|
|
1335
|
+
return this.get(id);
|
|
1336
|
+
}
|
|
1337
|
+
async delete(id) {
|
|
1338
|
+
await this.client.request(`/time_entries/${id}.json`, { method: "DELETE" });
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
// ../redmine/dist/session.js
|
|
1343
|
+
var RedmineSession = class {
|
|
1344
|
+
tracker;
|
|
1345
|
+
user;
|
|
1346
|
+
constructor(tracker, user2) {
|
|
1347
|
+
this.tracker = tracker;
|
|
1348
|
+
this.user = user2;
|
|
1349
|
+
}
|
|
1350
|
+
async currentUser() {
|
|
1351
|
+
return this.user;
|
|
1352
|
+
}
|
|
1353
|
+
async close() {
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
var RedmineAuthenticator = class {
|
|
1357
|
+
async authenticate(config) {
|
|
1358
|
+
const client = new RedmineClient(config);
|
|
1359
|
+
let user2;
|
|
1360
|
+
try {
|
|
1361
|
+
const res = await client.request("/users/current.json");
|
|
1362
|
+
user2 = mapCurrentUser(res.user);
|
|
1363
|
+
} catch (cause) {
|
|
1364
|
+
if (cause instanceof AuthError) {
|
|
1365
|
+
throw cause;
|
|
1366
|
+
}
|
|
1367
|
+
throw new AuthError(`Redmine credential validation failed: ${String(cause)}`, { cause });
|
|
1368
|
+
}
|
|
1369
|
+
const tracker = {
|
|
1370
|
+
issues: new RedmineIssuesTracker(client),
|
|
1371
|
+
time: new RedmineTimeTracker(client)
|
|
1372
|
+
};
|
|
1373
|
+
return new RedmineSession(tracker, user2);
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
// src/result.ts
|
|
1378
|
+
function ok(data) {
|
|
1379
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1380
|
+
}
|
|
1381
|
+
function fail(text) {
|
|
1382
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
1383
|
+
}
|
|
1384
|
+
async function runTool(redact, fn) {
|
|
1385
|
+
try {
|
|
1386
|
+
return ok(await fn());
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
let message;
|
|
1389
|
+
if (err instanceof IssueTrackerError) {
|
|
1390
|
+
message = `${err.name}: ${err.message}`;
|
|
1391
|
+
} else if (err instanceof Error) {
|
|
1392
|
+
message = err.message;
|
|
1393
|
+
} else {
|
|
1394
|
+
message = String(err);
|
|
1395
|
+
}
|
|
1396
|
+
return fail(redact(message));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/schemas.ts
|
|
1401
|
+
import { z } from "zod";
|
|
1402
|
+
var fieldOption = z.object({ id: z.string(), name: z.string() });
|
|
1403
|
+
var user = z.object({
|
|
1404
|
+
id: z.string(),
|
|
1405
|
+
name: z.string(),
|
|
1406
|
+
email: z.string().optional()
|
|
1407
|
+
});
|
|
1408
|
+
var attributeValue = z.discriminatedUnion("type", [
|
|
1409
|
+
z.object({ type: z.literal("string"), value: z.string() }),
|
|
1410
|
+
z.object({ type: z.literal("text"), value: z.string() }),
|
|
1411
|
+
z.object({ type: z.literal("number"), value: z.number() }),
|
|
1412
|
+
z.object({ type: z.literal("date"), value: z.string() }),
|
|
1413
|
+
z.object({ type: z.literal("boolean"), value: z.boolean() }),
|
|
1414
|
+
z.object({ type: z.literal("enum"), value: fieldOption }),
|
|
1415
|
+
z.object({ type: z.literal("multi_enum"), value: z.array(fieldOption) }),
|
|
1416
|
+
z.object({ type: z.literal("user"), value: user })
|
|
1417
|
+
]);
|
|
1418
|
+
var attributes = z.record(z.string(), attributeValue);
|
|
1419
|
+
var statusCategory = z.enum(["open", "in_progress", "done", "unknown"]);
|
|
1420
|
+
var sort = z.object({ field: z.string(), dir: z.enum(["asc", "desc"]) });
|
|
1421
|
+
var issueFilterShape = {
|
|
1422
|
+
text: z.string().optional(),
|
|
1423
|
+
projectId: z.string().optional(),
|
|
1424
|
+
statusCategory: statusCategory.optional(),
|
|
1425
|
+
assigneeId: z.string().optional(),
|
|
1426
|
+
authorId: z.string().optional(),
|
|
1427
|
+
updatedAfter: z.string().optional(),
|
|
1428
|
+
updatedBefore: z.string().optional(),
|
|
1429
|
+
createdAfter: z.string().optional(),
|
|
1430
|
+
createdBefore: z.string().optional(),
|
|
1431
|
+
sort: sort.optional(),
|
|
1432
|
+
limit: z.number().int().positive().optional(),
|
|
1433
|
+
cursor: z.string().optional()
|
|
1434
|
+
};
|
|
1435
|
+
var getIssueShape = { id: z.string() };
|
|
1436
|
+
var createIssueShape = {
|
|
1437
|
+
title: z.string(),
|
|
1438
|
+
description: z.string().nullable().optional(),
|
|
1439
|
+
projectId: z.string().optional(),
|
|
1440
|
+
assigneeId: z.string().nullable().optional(),
|
|
1441
|
+
attributes: attributes.optional()
|
|
1442
|
+
};
|
|
1443
|
+
var updateIssueShape = {
|
|
1444
|
+
id: z.string(),
|
|
1445
|
+
title: z.string().optional(),
|
|
1446
|
+
description: z.string().nullable().optional(),
|
|
1447
|
+
projectId: z.string().optional(),
|
|
1448
|
+
assigneeId: z.string().nullable().optional(),
|
|
1449
|
+
attributes: attributes.optional()
|
|
1450
|
+
};
|
|
1451
|
+
var deleteIssueShape = { id: z.string() };
|
|
1452
|
+
var addCommentShape = { issueId: z.string(), body: z.string() };
|
|
1453
|
+
var getTransitionsShape = { issueId: z.string() };
|
|
1454
|
+
var transitionShape = { issueId: z.string(), transitionId: z.string() };
|
|
1455
|
+
var timeEntryFilterShape = {
|
|
1456
|
+
issueId: z.string().optional(),
|
|
1457
|
+
userId: z.string().optional(),
|
|
1458
|
+
projectId: z.string().optional(),
|
|
1459
|
+
from: z.string().optional(),
|
|
1460
|
+
to: z.string().optional(),
|
|
1461
|
+
sort: sort.optional(),
|
|
1462
|
+
limit: z.number().int().positive().optional(),
|
|
1463
|
+
cursor: z.string().optional()
|
|
1464
|
+
};
|
|
1465
|
+
var getTimeEntryShape = { id: z.string() };
|
|
1466
|
+
var createTimeEntryShape = {
|
|
1467
|
+
issueId: z.string(),
|
|
1468
|
+
date: z.string(),
|
|
1469
|
+
durationMinutes: z.number().positive(),
|
|
1470
|
+
description: z.string().optional(),
|
|
1471
|
+
activityId: z.string().optional(),
|
|
1472
|
+
attributes: attributes.optional()
|
|
1473
|
+
};
|
|
1474
|
+
var updateTimeEntryShape = {
|
|
1475
|
+
id: z.string(),
|
|
1476
|
+
issueId: z.string().optional(),
|
|
1477
|
+
date: z.string().optional(),
|
|
1478
|
+
durationMinutes: z.number().positive().optional(),
|
|
1479
|
+
description: z.string().optional(),
|
|
1480
|
+
activityId: z.string().optional(),
|
|
1481
|
+
attributes: attributes.optional()
|
|
1482
|
+
};
|
|
1483
|
+
var deleteTimeEntryShape = { id: z.string() };
|
|
1484
|
+
|
|
1485
|
+
// src/tools.ts
|
|
1486
|
+
var READ_ONLY = { readOnlyHint: true };
|
|
1487
|
+
var DESTRUCTIVE = { destructiveHint: true };
|
|
1488
|
+
function registerTrackerTools(server, kind, session, redact) {
|
|
1489
|
+
const { issues, time } = session.tracker;
|
|
1490
|
+
const name = (op) => `${kind}_${op}`;
|
|
1491
|
+
const run = (fn) => runTool(redact, fn);
|
|
1492
|
+
server.registerTool(
|
|
1493
|
+
name("whoami"),
|
|
1494
|
+
{
|
|
1495
|
+
title: `${kind}: current user`,
|
|
1496
|
+
description: `Return the authenticated ${kind} account.`,
|
|
1497
|
+
annotations: READ_ONLY
|
|
1498
|
+
},
|
|
1499
|
+
() => run(() => session.currentUser())
|
|
1500
|
+
);
|
|
1501
|
+
server.registerTool(
|
|
1502
|
+
name("capabilities"),
|
|
1503
|
+
{
|
|
1504
|
+
title: `${kind}: capabilities`,
|
|
1505
|
+
description: `Describe what the ${kind} tracker supports, including the fields accepted on create/update. Consult this before constructing a create_issue payload.`,
|
|
1506
|
+
annotations: READ_ONLY
|
|
1507
|
+
},
|
|
1508
|
+
() => run(async () => ({
|
|
1509
|
+
issues: await issues.capabilities(),
|
|
1510
|
+
time: time ? await time.capabilities() : null
|
|
1511
|
+
}))
|
|
1512
|
+
);
|
|
1513
|
+
server.registerTool(
|
|
1514
|
+
name("search_issues"),
|
|
1515
|
+
{
|
|
1516
|
+
title: `${kind}: search issues`,
|
|
1517
|
+
description: `Search ${kind} issues with a normalized filter. Returns a page of issues with an optional nextCursor.`,
|
|
1518
|
+
inputSchema: issueFilterShape,
|
|
1519
|
+
annotations: READ_ONLY
|
|
1520
|
+
},
|
|
1521
|
+
(args) => run(() => issues.search(args))
|
|
1522
|
+
);
|
|
1523
|
+
server.registerTool(
|
|
1524
|
+
name("get_issue"),
|
|
1525
|
+
{
|
|
1526
|
+
title: `${kind}: get issue`,
|
|
1527
|
+
description: `Fetch a single ${kind} issue by id (or key), including its timeline items.`,
|
|
1528
|
+
inputSchema: getIssueShape,
|
|
1529
|
+
annotations: READ_ONLY
|
|
1530
|
+
},
|
|
1531
|
+
({ id }) => run(() => issues.get(id))
|
|
1532
|
+
);
|
|
1533
|
+
server.registerTool(
|
|
1534
|
+
name("create_issue"),
|
|
1535
|
+
{
|
|
1536
|
+
title: `${kind}: create issue`,
|
|
1537
|
+
description: `Create a ${kind} issue. Tracker-specific required fields go in \`attributes\`; check \`${kind}_capabilities\` first.`,
|
|
1538
|
+
inputSchema: createIssueShape
|
|
1539
|
+
},
|
|
1540
|
+
(args) => run(() => issues.create(args))
|
|
1541
|
+
);
|
|
1542
|
+
server.registerTool(
|
|
1543
|
+
name("update_issue"),
|
|
1544
|
+
{
|
|
1545
|
+
title: `${kind}: update issue`,
|
|
1546
|
+
description: `Update fields on a ${kind} issue. Status is changed via transitions, not here.`,
|
|
1547
|
+
inputSchema: updateIssueShape,
|
|
1548
|
+
annotations: { idempotentHint: true }
|
|
1549
|
+
},
|
|
1550
|
+
({ id, ...input }) => run(() => issues.update(id, input))
|
|
1551
|
+
);
|
|
1552
|
+
server.registerTool(
|
|
1553
|
+
name("delete_issue"),
|
|
1554
|
+
{
|
|
1555
|
+
title: `${kind}: delete issue`,
|
|
1556
|
+
description: `Permanently delete a ${kind} issue.`,
|
|
1557
|
+
inputSchema: deleteIssueShape,
|
|
1558
|
+
annotations: DESTRUCTIVE
|
|
1559
|
+
},
|
|
1560
|
+
({ id }) => run(() => issues.delete(id).then(() => ({ deleted: id })))
|
|
1561
|
+
);
|
|
1562
|
+
server.registerTool(
|
|
1563
|
+
name("add_comment"),
|
|
1564
|
+
{
|
|
1565
|
+
title: `${kind}: add comment`,
|
|
1566
|
+
description: `Add a comment to a ${kind} issue.`,
|
|
1567
|
+
inputSchema: addCommentShape
|
|
1568
|
+
},
|
|
1569
|
+
({ issueId, body }) => run(() => issues.addComment(issueId, body))
|
|
1570
|
+
);
|
|
1571
|
+
server.registerTool(
|
|
1572
|
+
name("get_transitions"),
|
|
1573
|
+
{
|
|
1574
|
+
title: `${kind}: get transitions`,
|
|
1575
|
+
description: `List the status transitions currently available for a ${kind} issue.`,
|
|
1576
|
+
inputSchema: getTransitionsShape,
|
|
1577
|
+
annotations: READ_ONLY
|
|
1578
|
+
},
|
|
1579
|
+
({ issueId }) => run(() => issues.getTransitions(issueId))
|
|
1580
|
+
);
|
|
1581
|
+
server.registerTool(
|
|
1582
|
+
name("transition_issue"),
|
|
1583
|
+
{
|
|
1584
|
+
title: `${kind}: transition issue`,
|
|
1585
|
+
description: `Move a ${kind} issue through a transition (use get_transitions for valid ids).`,
|
|
1586
|
+
inputSchema: transitionShape
|
|
1587
|
+
},
|
|
1588
|
+
({ issueId, transitionId }) => run(() => issues.transition(issueId, transitionId))
|
|
1589
|
+
);
|
|
1590
|
+
if (!time) return;
|
|
1591
|
+
server.registerTool(
|
|
1592
|
+
name("search_time_entries"),
|
|
1593
|
+
{
|
|
1594
|
+
title: `${kind}: search time entries`,
|
|
1595
|
+
description: `Search ${kind} time entries with a normalized filter.`,
|
|
1596
|
+
inputSchema: timeEntryFilterShape,
|
|
1597
|
+
annotations: READ_ONLY
|
|
1598
|
+
},
|
|
1599
|
+
(args) => run(() => time.search(args))
|
|
1600
|
+
);
|
|
1601
|
+
server.registerTool(
|
|
1602
|
+
name("get_time_entry"),
|
|
1603
|
+
{
|
|
1604
|
+
title: `${kind}: get time entry`,
|
|
1605
|
+
description: `Fetch a single ${kind} time entry by id.`,
|
|
1606
|
+
inputSchema: getTimeEntryShape,
|
|
1607
|
+
annotations: READ_ONLY
|
|
1608
|
+
},
|
|
1609
|
+
({ id }) => run(() => time.get(id))
|
|
1610
|
+
);
|
|
1611
|
+
server.registerTool(
|
|
1612
|
+
name("create_time_entry"),
|
|
1613
|
+
{
|
|
1614
|
+
title: `${kind}: log time`,
|
|
1615
|
+
description: `Log a ${kind} time entry against an issue. durationMinutes is in minutes.`,
|
|
1616
|
+
inputSchema: createTimeEntryShape
|
|
1617
|
+
},
|
|
1618
|
+
(args) => run(() => time.create(args))
|
|
1619
|
+
);
|
|
1620
|
+
server.registerTool(
|
|
1621
|
+
name("update_time_entry"),
|
|
1622
|
+
{
|
|
1623
|
+
title: `${kind}: update time entry`,
|
|
1624
|
+
description: `Update a ${kind} time entry.`,
|
|
1625
|
+
inputSchema: updateTimeEntryShape,
|
|
1626
|
+
annotations: { idempotentHint: true }
|
|
1627
|
+
},
|
|
1628
|
+
({ id, ...input }) => run(() => time.update(id, input))
|
|
1629
|
+
);
|
|
1630
|
+
server.registerTool(
|
|
1631
|
+
name("delete_time_entry"),
|
|
1632
|
+
{
|
|
1633
|
+
title: `${kind}: delete time entry`,
|
|
1634
|
+
description: `Permanently delete a ${kind} time entry.`,
|
|
1635
|
+
inputSchema: deleteTimeEntryShape,
|
|
1636
|
+
annotations: DESTRUCTIVE
|
|
1637
|
+
},
|
|
1638
|
+
({ id }) => run(() => time.delete(id).then(() => ({ deleted: id })))
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/server.ts
|
|
1643
|
+
var AUTHENTICATORS = {
|
|
1644
|
+
jira: () => new JiraAuthenticator(),
|
|
1645
|
+
redmine: () => new RedmineAuthenticator()
|
|
1646
|
+
};
|
|
1647
|
+
async function createServer(env = process.env) {
|
|
1648
|
+
const { trackers, secrets, warnings: configWarnings } = loadConfig(env);
|
|
1649
|
+
const redact = makeRedactor(secrets);
|
|
1650
|
+
const server = new McpServer({ name: "issues-mcp", version: "1.0.0" });
|
|
1651
|
+
const sessions = [];
|
|
1652
|
+
const warnings = [...configWarnings];
|
|
1653
|
+
for (const { kind, connection } of trackers) {
|
|
1654
|
+
try {
|
|
1655
|
+
const session = await AUTHENTICATORS[kind]().authenticate(connection);
|
|
1656
|
+
sessions.push(session);
|
|
1657
|
+
registerTrackerTools(server, kind, session, redact);
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1660
|
+
warnings.push(`${kind}: authentication failed \u2014 ${redact(message)}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return { server, sessions, warnings };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
export {
|
|
1667
|
+
loadConfig,
|
|
1668
|
+
makeRedactor,
|
|
1669
|
+
createServer
|
|
1670
|
+
};
|
|
1671
|
+
//# sourceMappingURL=chunk-Y53ZARWV.js.map
|