@pschroee/redmine-mcp 0.4.5 → 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/formatters/account.d.ts +2 -0
- package/dist/formatters/account.js +48 -0
- package/dist/formatters/agile.d.ts +6 -0
- package/dist/formatters/agile.js +53 -0
- package/dist/formatters/checklist.d.ts +5 -0
- package/dist/formatters/checklist.js +30 -0
- package/dist/formatters/file.d.ts +5 -0
- package/dist/formatters/file.js +61 -0
- package/dist/formatters/group.d.ts +11 -0
- package/dist/formatters/group.js +53 -0
- package/dist/formatters/index.d.ts +15 -1
- package/dist/formatters/index.js +15 -1
- package/dist/formatters/issue.d.ts +13 -3
- package/dist/formatters/issue.js +43 -10
- package/dist/formatters/journals.d.ts +7 -1
- package/dist/formatters/journals.js +13 -15
- package/dist/formatters/membership.d.ts +5 -0
- package/dist/formatters/membership.js +41 -0
- package/dist/formatters/metadata.d.ts +135 -0
- package/dist/formatters/metadata.js +213 -0
- package/dist/formatters/project.d.ts +11 -0
- package/dist/formatters/project.js +82 -0
- package/dist/formatters/relation.d.ts +5 -0
- package/dist/formatters/relation.js +55 -0
- package/dist/formatters/search.d.ts +5 -0
- package/dist/formatters/search.js +61 -0
- package/dist/formatters/time.d.ts +11 -0
- package/dist/formatters/time.js +47 -0
- package/dist/formatters/user.d.ts +11 -0
- package/dist/formatters/user.js +83 -0
- package/dist/formatters/utils.d.ts +12 -0
- package/dist/formatters/utils.js +31 -0
- package/dist/formatters/version.d.ts +11 -0
- package/dist/formatters/version.js +75 -0
- package/dist/formatters/wiki.d.ts +11 -0
- package/dist/formatters/wiki.js +86 -0
- package/dist/redmine/client.d.ts +4 -0
- package/dist/redmine/client.js +6 -0
- package/dist/server.js +1 -1
- package/dist/tools/account.js +7 -1
- package/dist/tools/admin.js +25 -4
- package/dist/tools/agile.js +19 -3
- package/dist/tools/checklists.js +13 -2
- package/dist/tools/core.js +23 -5
- package/dist/tools/enumerations.js +19 -3
- package/dist/tools/files.js +13 -2
- package/dist/tools/memberships.js +13 -2
- package/dist/tools/metadata.js +37 -6
- package/dist/tools/relations.js +25 -4
- package/dist/tools/roles.js +13 -2
- package/dist/tools/search.js +7 -1
- package/dist/tools/time.js +13 -2
- package/dist/tools/wiki.js +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { formatDate } from "./utils.js";
|
|
2
|
+
const USER_STATUS = {
|
|
3
|
+
1: "Active",
|
|
4
|
+
2: "Registered",
|
|
5
|
+
3: "Locked",
|
|
6
|
+
};
|
|
7
|
+
export function formatMyAccount(response) {
|
|
8
|
+
const user = response.user;
|
|
9
|
+
const lines = [];
|
|
10
|
+
lines.push(`# ${user.firstname} ${user.lastname}`);
|
|
11
|
+
lines.push("");
|
|
12
|
+
const statusParts = [];
|
|
13
|
+
statusParts.push(`**Login:** ${user.login}`);
|
|
14
|
+
if (user.status !== undefined) {
|
|
15
|
+
statusParts.push(`**Status:** ${USER_STATUS[user.status] || "Unknown"}`);
|
|
16
|
+
}
|
|
17
|
+
if (user.admin) {
|
|
18
|
+
statusParts.push(`**Role:** Admin`);
|
|
19
|
+
}
|
|
20
|
+
lines.push(statusParts.join(" | "));
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push("| Field | Value |");
|
|
23
|
+
lines.push("|-------|-------|");
|
|
24
|
+
lines.push(`| Email | ${user.mail} |`);
|
|
25
|
+
lines.push(`| Created | ${formatDate(user.created_on)} |`);
|
|
26
|
+
if (user.last_login_on) {
|
|
27
|
+
lines.push(`| Last Login | ${formatDate(user.last_login_on)} |`);
|
|
28
|
+
}
|
|
29
|
+
if (user.twofa_scheme) {
|
|
30
|
+
lines.push(`| 2FA | ${user.twofa_scheme} |`);
|
|
31
|
+
}
|
|
32
|
+
if (user.api_key) {
|
|
33
|
+
lines.push(`| API Key | ${user.api_key} |`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("");
|
|
36
|
+
if (user.custom_fields && user.custom_fields.length > 0) {
|
|
37
|
+
lines.push("## Custom Fields");
|
|
38
|
+
lines.push("");
|
|
39
|
+
lines.push("| Field | Value |");
|
|
40
|
+
lines.push("|-------|-------|");
|
|
41
|
+
for (const cf of user.custom_fields) {
|
|
42
|
+
const value = Array.isArray(cf.value) ? cf.value.join(", ") : cf.value;
|
|
43
|
+
lines.push(`| ${cf.name} | ${value} |`);
|
|
44
|
+
}
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n").trimEnd();
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RedmineAgileSprint, RedmineAgileSprintsResponse, RedmineAgileDataResponse } from "../redmine/types.js";
|
|
2
|
+
export declare function formatSprint(response: {
|
|
3
|
+
agile_sprint: RedmineAgileSprint;
|
|
4
|
+
}): string;
|
|
5
|
+
export declare function formatSprintList(response: RedmineAgileSprintsResponse): string;
|
|
6
|
+
export declare function formatAgileData(response: RedmineAgileDataResponse): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function formatSprint(response) {
|
|
2
|
+
const sprint = response.agile_sprint;
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push(`# ${sprint.name}`);
|
|
5
|
+
lines.push("");
|
|
6
|
+
lines.push("| Field | Value |");
|
|
7
|
+
lines.push("|-------|-------|");
|
|
8
|
+
lines.push(`| ID | ${sprint.id} |`);
|
|
9
|
+
lines.push(`| Status | ${sprint.status} |`);
|
|
10
|
+
if (sprint.start_date) {
|
|
11
|
+
lines.push(`| Start Date | ${sprint.start_date} |`);
|
|
12
|
+
}
|
|
13
|
+
if (sprint.end_date) {
|
|
14
|
+
lines.push(`| End Date | ${sprint.end_date} |`);
|
|
15
|
+
}
|
|
16
|
+
if (sprint.description) {
|
|
17
|
+
lines.push(`| Description | ${sprint.description} |`);
|
|
18
|
+
}
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
21
|
+
export function formatSprintList(response) {
|
|
22
|
+
const sprints = response.agile_sprints;
|
|
23
|
+
if (sprints.length === 0) {
|
|
24
|
+
return "No sprints found.";
|
|
25
|
+
}
|
|
26
|
+
const lines = [];
|
|
27
|
+
lines.push(`# Agile Sprints (${sprints.length})`);
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push("| ID | Name | Status | Start | End |");
|
|
30
|
+
lines.push("|----|------|--------|-------|-----|");
|
|
31
|
+
for (const sprint of sprints) {
|
|
32
|
+
const start = sprint.start_date || "-";
|
|
33
|
+
const end = sprint.end_date || "-";
|
|
34
|
+
lines.push(`| ${sprint.id} | ${sprint.name} | ${sprint.status} | ${start} | ${end} |`);
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function formatAgileData(response) {
|
|
39
|
+
const data = response.agile_data;
|
|
40
|
+
const lines = [];
|
|
41
|
+
lines.push(`# Agile Data for Issue #${data.issue_id}`);
|
|
42
|
+
lines.push("");
|
|
43
|
+
lines.push("| Field | Value |");
|
|
44
|
+
lines.push("|-------|-------|");
|
|
45
|
+
lines.push(`| Position | ${data.position} |`);
|
|
46
|
+
if (data.story_points !== undefined) {
|
|
47
|
+
lines.push(`| Story Points | ${data.story_points} |`);
|
|
48
|
+
}
|
|
49
|
+
if (data.agile_sprint_id !== undefined) {
|
|
50
|
+
lines.push(`| Sprint ID | ${data.agile_sprint_id} |`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RedmineChecklist, RedmineChecklistsResponse } from "../redmine/types.js";
|
|
2
|
+
export declare function formatChecklist(response: {
|
|
3
|
+
checklist: RedmineChecklist;
|
|
4
|
+
}): string;
|
|
5
|
+
export declare function formatChecklistList(response: RedmineChecklistsResponse): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function formatChecklist(response) {
|
|
2
|
+
const item = response.checklist;
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push(`# Checklist Item #${item.id}`);
|
|
5
|
+
lines.push("");
|
|
6
|
+
lines.push("| Field | Value |");
|
|
7
|
+
lines.push("|-------|-------|");
|
|
8
|
+
lines.push(`| Subject | ${item.subject} |`);
|
|
9
|
+
lines.push(`| Status | ${item.is_done ? "Done" : "Pending"} |`);
|
|
10
|
+
lines.push(`| Issue | #${item.issue_id} |`);
|
|
11
|
+
lines.push(`| Position | ${item.position} |`);
|
|
12
|
+
return lines.join("\n");
|
|
13
|
+
}
|
|
14
|
+
export function formatChecklistList(response) {
|
|
15
|
+
const items = response.checklists;
|
|
16
|
+
if (items.length === 0) {
|
|
17
|
+
return "No checklist items found.";
|
|
18
|
+
}
|
|
19
|
+
const sorted = [...items].sort((a, b) => a.position - b.position);
|
|
20
|
+
const doneCount = items.filter((i) => i.is_done).length;
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(`# Checklist (${items.length} items)`);
|
|
23
|
+
lines.push(`_${doneCount}/${items.length} completed_`);
|
|
24
|
+
lines.push("");
|
|
25
|
+
for (const item of sorted) {
|
|
26
|
+
const checkbox = item.is_done ? "[x]" : "[ ]";
|
|
27
|
+
lines.push(`- ${checkbox} ${item.subject}`);
|
|
28
|
+
}
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { formatDate, formatDateShort } from "./utils.js";
|
|
2
|
+
function formatFileSize(bytes) {
|
|
3
|
+
if (bytes < 1024)
|
|
4
|
+
return `${bytes} B`;
|
|
5
|
+
if (bytes < 1024 * 1024)
|
|
6
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
7
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
8
|
+
}
|
|
9
|
+
export function formatAttachment(response) {
|
|
10
|
+
const att = response.attachment;
|
|
11
|
+
const lines = [];
|
|
12
|
+
lines.push(`# ${att.filename}`);
|
|
13
|
+
lines.push("");
|
|
14
|
+
lines.push("| Field | Value |");
|
|
15
|
+
lines.push("|-------|-------|");
|
|
16
|
+
lines.push(`| ID | ${att.id} |`);
|
|
17
|
+
lines.push(`| Size | ${formatFileSize(att.filesize)} |`);
|
|
18
|
+
lines.push(`| Type | ${att.content_type} |`);
|
|
19
|
+
lines.push(`| Author | ${att.author.name} |`);
|
|
20
|
+
lines.push(`| Created | ${formatDate(att.created_on)} |`);
|
|
21
|
+
if (att.description) {
|
|
22
|
+
lines.push(`| Description | ${att.description} |`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(`**Download:** ${att.content_url}`);
|
|
26
|
+
if (att.thumbnail_url) {
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push(`**Thumbnail:** ${att.thumbnail_url}`);
|
|
29
|
+
}
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
export function formatFileList(response) {
|
|
33
|
+
const files = response.files;
|
|
34
|
+
if (files.length === 0) {
|
|
35
|
+
return "No files found.";
|
|
36
|
+
}
|
|
37
|
+
const lines = [];
|
|
38
|
+
const hasVersion = files.some((f) => f.version !== undefined);
|
|
39
|
+
lines.push(`# Project Files (${files.length})`);
|
|
40
|
+
lines.push("");
|
|
41
|
+
if (hasVersion) {
|
|
42
|
+
lines.push("| Filename | Size | Version | Downloads | Author | Date |");
|
|
43
|
+
lines.push("|----------|------|---------|-----------|--------|------|");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
lines.push("| Filename | Size | Downloads | Author | Date |");
|
|
47
|
+
lines.push("|----------|------|-----------|--------|------|");
|
|
48
|
+
}
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const size = formatFileSize(file.filesize);
|
|
51
|
+
const date = formatDateShort(file.created_on);
|
|
52
|
+
if (hasVersion) {
|
|
53
|
+
const version = file.version?.name || "";
|
|
54
|
+
lines.push(`| ${file.filename} | ${size} | ${version} | ${file.downloads} | ${file.author.name} | ${date} |`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push(`| ${file.filename} | ${size} | ${file.downloads} | ${file.author.name} | ${date} |`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineGroup, RedmineGroupsResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single group as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatGroup(response: {
|
|
6
|
+
group: RedmineGroup;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of groups as Markdown table
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatGroupList(response: RedmineGroupsResponse): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a single group as complete Markdown
|
|
3
|
+
*/
|
|
4
|
+
export function formatGroup(response) {
|
|
5
|
+
const group = response.group;
|
|
6
|
+
const lines = [];
|
|
7
|
+
// Header
|
|
8
|
+
lines.push(`# ${group.name}`);
|
|
9
|
+
lines.push("");
|
|
10
|
+
// Users
|
|
11
|
+
if (group.users && group.users.length > 0) {
|
|
12
|
+
lines.push("## Users");
|
|
13
|
+
lines.push("");
|
|
14
|
+
for (const user of group.users) {
|
|
15
|
+
lines.push(`- ${user.name}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push("");
|
|
18
|
+
}
|
|
19
|
+
// Project Memberships
|
|
20
|
+
if (group.memberships && group.memberships.length > 0) {
|
|
21
|
+
lines.push("## Project Memberships");
|
|
22
|
+
lines.push("");
|
|
23
|
+
for (const membership of group.memberships) {
|
|
24
|
+
const roles = membership.roles.map((r) => r.name).join(", ");
|
|
25
|
+
lines.push(`- **${membership.project.name}:** ${roles}`);
|
|
26
|
+
}
|
|
27
|
+
lines.push("");
|
|
28
|
+
}
|
|
29
|
+
return lines.join("\n").trimEnd();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Format a list of groups as Markdown table
|
|
33
|
+
*/
|
|
34
|
+
export function formatGroupList(response) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
// Header
|
|
37
|
+
lines.push(`# Groups (${response.total_count})`);
|
|
38
|
+
lines.push("");
|
|
39
|
+
// Empty case
|
|
40
|
+
if (response.groups.length === 0) {
|
|
41
|
+
lines.push("No groups found.");
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
// Table header
|
|
45
|
+
lines.push("| ID | Name | Members |");
|
|
46
|
+
lines.push("|----|------|---------|");
|
|
47
|
+
// Table rows
|
|
48
|
+
for (const group of response.groups) {
|
|
49
|
+
const members = group.users && group.users.length > 0 ? String(group.users.length) : "-";
|
|
50
|
+
lines.push(`| ${group.id} | ${group.name} | ${members} |`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
export { formatJournals, type NameLookup } from "./journals.js";
|
|
2
|
-
export { formatIssue, formatIssueResponse } from "./issue.js";
|
|
2
|
+
export { formatIssue, formatIssueResponse, formatIssueList } from "./issue.js";
|
|
3
|
+
export { formatProject, formatProjectList } from "./project.js";
|
|
4
|
+
export { formatUser, formatUserList } from "./user.js";
|
|
5
|
+
export { formatSearchResults } from "./search.js";
|
|
6
|
+
export { formatWikiPage, formatWikiPageList } from "./wiki.js";
|
|
7
|
+
export { formatVersion, formatVersionList } from "./version.js";
|
|
8
|
+
export { formatTimeEntry, formatTimeEntryList } from "./time.js";
|
|
9
|
+
export { formatGroup, formatGroupList } from "./group.js";
|
|
10
|
+
export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, formatCategory, formatCustomFieldList, formatQueryList, formatDocumentCategoryList, } from "./metadata.js";
|
|
11
|
+
export { formatRelation, formatRelationList } from "./relation.js";
|
|
12
|
+
export { formatAttachment, formatFileList } from "./file.js";
|
|
13
|
+
export { formatMembership, formatMembershipList } from "./membership.js";
|
|
14
|
+
export { formatChecklist, formatChecklistList } from "./checklist.js";
|
|
15
|
+
export { formatSprint, formatSprintList, formatAgileData } from "./agile.js";
|
|
16
|
+
export { formatMyAccount } from "./account.js";
|
package/dist/formatters/index.js
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
export { formatJournals } from "./journals.js";
|
|
2
|
-
export { formatIssue, formatIssueResponse } from "./issue.js";
|
|
2
|
+
export { formatIssue, formatIssueResponse, formatIssueList } from "./issue.js";
|
|
3
|
+
export { formatProject, formatProjectList } from "./project.js";
|
|
4
|
+
export { formatUser, formatUserList } from "./user.js";
|
|
5
|
+
export { formatSearchResults } from "./search.js";
|
|
6
|
+
export { formatWikiPage, formatWikiPageList } from "./wiki.js";
|
|
7
|
+
export { formatVersion, formatVersionList } from "./version.js";
|
|
8
|
+
export { formatTimeEntry, formatTimeEntryList } from "./time.js";
|
|
9
|
+
export { formatGroup, formatGroupList } from "./group.js";
|
|
10
|
+
export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, formatCategory, formatCustomFieldList, formatQueryList, formatDocumentCategoryList, } from "./metadata.js";
|
|
11
|
+
export { formatRelation, formatRelationList } from "./relation.js";
|
|
12
|
+
export { formatAttachment, formatFileList } from "./file.js";
|
|
13
|
+
export { formatMembership, formatMembershipList } from "./membership.js";
|
|
14
|
+
export { formatChecklist, formatChecklistList } from "./checklist.js";
|
|
15
|
+
export { formatSprint, formatSprintList, formatAgileData } from "./agile.js";
|
|
16
|
+
export { formatMyAccount } from "./account.js";
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import type { RedmineIssue } from "../redmine/types.js";
|
|
1
|
+
import type { RedmineIssue, RedmineIssuesResponse } from "../redmine/types.js";
|
|
2
2
|
import { type NameLookup } from "./journals.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for formatting issues
|
|
5
|
+
*/
|
|
6
|
+
export interface IssueFormatOptions {
|
|
7
|
+
includeDescriptionDiffs?: boolean;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* Format an issue as complete Markdown
|
|
5
11
|
*/
|
|
6
|
-
export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup): string;
|
|
12
|
+
export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup, options?: IssueFormatOptions): string;
|
|
7
13
|
/**
|
|
8
14
|
* Format an issue API response as Markdown
|
|
9
15
|
*/
|
|
10
16
|
export declare function formatIssueResponse(response: {
|
|
11
17
|
issue: RedmineIssue;
|
|
12
|
-
}, lookup?: NameLookup): string;
|
|
18
|
+
}, lookup?: NameLookup, options?: IssueFormatOptions): string;
|
|
19
|
+
/**
|
|
20
|
+
* Format a list of issues as a Markdown table
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatIssueList(response: RedmineIssuesResponse): string;
|
package/dist/formatters/issue.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { formatJournals } from "./journals.js";
|
|
2
|
-
|
|
3
|
-
* Format a date string to readable format
|
|
4
|
-
*/
|
|
5
|
-
function formatDate(isoDate) {
|
|
6
|
-
return new Date(isoDate).toISOString().slice(0, 16).replace("T", " ");
|
|
7
|
-
}
|
|
2
|
+
import { formatDate, formatDateShort } from "./utils.js";
|
|
8
3
|
/**
|
|
9
4
|
* Format an issue as complete Markdown
|
|
10
5
|
*/
|
|
11
|
-
export function formatIssue(issue, lookup = {}) {
|
|
6
|
+
export function formatIssue(issue, lookup = {}, options = {}) {
|
|
12
7
|
const lines = [];
|
|
13
8
|
// Title
|
|
14
9
|
lines.push(`# #${issue.id}: ${issue.subject}`);
|
|
@@ -116,13 +111,51 @@ export function formatIssue(issue, lookup = {}) {
|
|
|
116
111
|
}
|
|
117
112
|
// Journals (history)
|
|
118
113
|
if (issue.journals && issue.journals.length > 0) {
|
|
119
|
-
lines.push(formatJournals(issue.journals, lookup
|
|
114
|
+
lines.push(formatJournals(issue.journals, lookup, {
|
|
115
|
+
includeDescriptionDiffs: options.includeDescriptionDiffs,
|
|
116
|
+
}));
|
|
120
117
|
}
|
|
121
118
|
return lines.join("\n");
|
|
122
119
|
}
|
|
123
120
|
/**
|
|
124
121
|
* Format an issue API response as Markdown
|
|
125
122
|
*/
|
|
126
|
-
export function formatIssueResponse(response, lookup = {}) {
|
|
127
|
-
return formatIssue(response.issue, lookup);
|
|
123
|
+
export function formatIssueResponse(response, lookup = {}, options = {}) {
|
|
124
|
+
return formatIssue(response.issue, lookup, options);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Format a list of issues as a Markdown table
|
|
128
|
+
*/
|
|
129
|
+
export function formatIssueList(response) {
|
|
130
|
+
const { issues, total_count, offset, limit } = response;
|
|
131
|
+
const lines = [];
|
|
132
|
+
// Header with count
|
|
133
|
+
lines.push(`# Issues (${issues.length} of ${total_count})`);
|
|
134
|
+
lines.push("");
|
|
135
|
+
// Pagination info if needed
|
|
136
|
+
if (offset > 0 || total_count > limit) {
|
|
137
|
+
const start = offset + 1;
|
|
138
|
+
const end = offset + issues.length;
|
|
139
|
+
lines.push(`_Showing ${start}-${end} of ${total_count}_`);
|
|
140
|
+
lines.push("");
|
|
141
|
+
}
|
|
142
|
+
// Empty case
|
|
143
|
+
if (issues.length === 0) {
|
|
144
|
+
lines.push("No issues found.");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
// Table header
|
|
148
|
+
lines.push("| ID | Subject | Status | Priority | Assigned | Updated |");
|
|
149
|
+
lines.push("|---|---|---|---|---|---|");
|
|
150
|
+
// Table rows
|
|
151
|
+
for (const issue of issues) {
|
|
152
|
+
const id = `#${issue.id}`;
|
|
153
|
+
const subject = issue.subject;
|
|
154
|
+
const status = issue.status.name;
|
|
155
|
+
const priority = issue.priority.name;
|
|
156
|
+
const assigned = issue.assigned_to?.name ?? "_(unassigned)_";
|
|
157
|
+
const updated = formatDateShort(issue.updated_on);
|
|
158
|
+
lines.push(`| ${id} | ${subject} | ${status} | ${priority} | ${assigned} | ${updated} |`);
|
|
159
|
+
}
|
|
160
|
+
return lines.join("\n");
|
|
128
161
|
}
|
|
@@ -4,7 +4,13 @@ import type { RedmineJournal } from "../redmine/types.js";
|
|
|
4
4
|
* Key: field name (e.g., "status_id"), Value: map of ID -> name
|
|
5
5
|
*/
|
|
6
6
|
export type NameLookup = Record<string, Record<string, string>>;
|
|
7
|
+
/**
|
|
8
|
+
* Options for formatting journals
|
|
9
|
+
*/
|
|
10
|
+
export interface JournalFormatOptions {
|
|
11
|
+
includeDescriptionDiffs?: boolean;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* Format an array of journal entries as Markdown
|
|
9
15
|
*/
|
|
10
|
-
export declare function formatJournals(journals: RedmineJournal[], lookup?: NameLookup): string;
|
|
16
|
+
export declare function formatJournals(journals: RedmineJournal[], lookup?: NameLookup, options?: JournalFormatOptions): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { diffLines } from "diff";
|
|
2
|
+
import { formatDate } from "./utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* Fields that contain large text and should use diff formatting
|
|
4
5
|
*/
|
|
@@ -68,13 +69,6 @@ const FIELD_DISPLAY_NAMES = {
|
|
|
68
69
|
parent_id: "parent",
|
|
69
70
|
project_id: "project",
|
|
70
71
|
};
|
|
71
|
-
/**
|
|
72
|
-
* Format a date string to a readable format
|
|
73
|
-
*/
|
|
74
|
-
function formatDate(isoDate) {
|
|
75
|
-
const date = new Date(isoDate);
|
|
76
|
-
return date.toISOString().slice(0, 16).replace("T", " ");
|
|
77
|
-
}
|
|
78
72
|
/**
|
|
79
73
|
* Generate a unified diff for large text changes
|
|
80
74
|
*/
|
|
@@ -116,7 +110,7 @@ function resolveValue(fieldName, value, lookup) {
|
|
|
116
110
|
/**
|
|
117
111
|
* Format a single journal detail (field change)
|
|
118
112
|
*/
|
|
119
|
-
function formatDetail(detail, lookup) {
|
|
113
|
+
function formatDetail(detail, lookup, options = {}) {
|
|
120
114
|
const { property, name, old_value, new_value } = detail;
|
|
121
115
|
// Handle attachment additions/removals
|
|
122
116
|
if (property === "attachment") {
|
|
@@ -157,8 +151,11 @@ function formatDetail(detail, lookup) {
|
|
|
157
151
|
}
|
|
158
152
|
// Fall through to default formatting if parsing failed
|
|
159
153
|
}
|
|
160
|
-
// Large text fields get diff formatting
|
|
154
|
+
// Large text fields get diff formatting (only if option enabled)
|
|
161
155
|
if (LARGE_TEXT_FIELDS.includes(name) && old_value && new_value) {
|
|
156
|
+
if (!options.includeDescriptionDiffs) {
|
|
157
|
+
return `- ${name}: _(changed - use include_description_diffs to see diff)_`;
|
|
158
|
+
}
|
|
162
159
|
const diff = generateDiff(old_value, new_value);
|
|
163
160
|
return `- ${name}:\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
164
161
|
}
|
|
@@ -181,13 +178,14 @@ function formatDetail(detail, lookup) {
|
|
|
181
178
|
/**
|
|
182
179
|
* Format a single journal entry as Markdown
|
|
183
180
|
*/
|
|
184
|
-
function formatJournalEntry(journal, lookup) {
|
|
181
|
+
function formatJournalEntry(journal, index, lookup, options = {}) {
|
|
185
182
|
const lines = [];
|
|
186
|
-
// Header with date and user
|
|
183
|
+
// Header with note number, date and user
|
|
184
|
+
const noteNum = index + 1;
|
|
187
185
|
const date = formatDate(journal.created_on);
|
|
188
186
|
const user = journal.user.name;
|
|
189
187
|
const privateTag = journal.private_notes ? " 🔒" : "";
|
|
190
|
-
lines.push(`### ${date} - ${user}${privateTag}`);
|
|
188
|
+
lines.push(`### #${noteNum} - ${date} - ${user}${privateTag}`);
|
|
191
189
|
lines.push("");
|
|
192
190
|
// Notes (if any)
|
|
193
191
|
if (journal.notes && journal.notes.trim()) {
|
|
@@ -198,7 +196,7 @@ function formatJournalEntry(journal, lookup) {
|
|
|
198
196
|
if (journal.details && journal.details.length > 0) {
|
|
199
197
|
lines.push("**Changes:**");
|
|
200
198
|
for (const detail of journal.details) {
|
|
201
|
-
lines.push(formatDetail(detail, lookup));
|
|
199
|
+
lines.push(formatDetail(detail, lookup, options));
|
|
202
200
|
}
|
|
203
201
|
lines.push("");
|
|
204
202
|
}
|
|
@@ -207,11 +205,11 @@ function formatJournalEntry(journal, lookup) {
|
|
|
207
205
|
/**
|
|
208
206
|
* Format an array of journal entries as Markdown
|
|
209
207
|
*/
|
|
210
|
-
export function formatJournals(journals, lookup = {}) {
|
|
208
|
+
export function formatJournals(journals, lookup = {}, options = {}) {
|
|
211
209
|
if (!journals || journals.length === 0) {
|
|
212
210
|
return "";
|
|
213
211
|
}
|
|
214
212
|
const header = `## History (${journals.length} entries)\n\n`;
|
|
215
|
-
const entries = journals.map(j => formatJournalEntry(j, lookup)).join("\n---\n\n");
|
|
213
|
+
const entries = journals.map((j, i) => formatJournalEntry(j, i, lookup, options)).join("\n---\n\n");
|
|
216
214
|
return header + entries;
|
|
217
215
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RedmineMembership, RedmineMembershipsResponse } from "../redmine/types.js";
|
|
2
|
+
export declare function formatMembership(response: {
|
|
3
|
+
membership: RedmineMembership;
|
|
4
|
+
}): string;
|
|
5
|
+
export declare function formatMembershipList(response: RedmineMembershipsResponse): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function formatRoles(roles) {
|
|
2
|
+
return roles
|
|
3
|
+
.map((r) => (r.inherited ? `${r.name} (inherited)` : r.name))
|
|
4
|
+
.join(", ");
|
|
5
|
+
}
|
|
6
|
+
export function formatMembership(response) {
|
|
7
|
+
const mem = response.membership;
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(`# Membership #${mem.id}`);
|
|
10
|
+
lines.push("");
|
|
11
|
+
lines.push("| Field | Value |");
|
|
12
|
+
lines.push("|-------|-------|");
|
|
13
|
+
lines.push(`| Project | ${mem.project.name} |`);
|
|
14
|
+
if (mem.user) {
|
|
15
|
+
lines.push(`| User | ${mem.user.name} |`);
|
|
16
|
+
}
|
|
17
|
+
if (mem.group) {
|
|
18
|
+
lines.push(`| Group | ${mem.group.name} |`);
|
|
19
|
+
}
|
|
20
|
+
lines.push(`| Roles | ${formatRoles(mem.roles)} |`);
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
export function formatMembershipList(response) {
|
|
24
|
+
const memberships = response.memberships;
|
|
25
|
+
const totalCount = response.total_count;
|
|
26
|
+
if (memberships.length === 0) {
|
|
27
|
+
return "No memberships found.";
|
|
28
|
+
}
|
|
29
|
+
const lines = [];
|
|
30
|
+
lines.push(`# Project Memberships (${totalCount})`);
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push("| ID | User/Group | Type | Roles |");
|
|
33
|
+
lines.push("|----|------------|------|-------|");
|
|
34
|
+
for (const mem of memberships) {
|
|
35
|
+
const name = mem.user?.name || mem.group?.name || "Unknown";
|
|
36
|
+
const type = mem.user ? "User" : "Group";
|
|
37
|
+
const roles = formatRoles(mem.roles);
|
|
38
|
+
lines.push(`| ${mem.id} | ${name} | ${type} | ${roles} |`);
|
|
39
|
+
}
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|