@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.
Files changed (54) hide show
  1. package/dist/formatters/account.d.ts +2 -0
  2. package/dist/formatters/account.js +48 -0
  3. package/dist/formatters/agile.d.ts +6 -0
  4. package/dist/formatters/agile.js +53 -0
  5. package/dist/formatters/checklist.d.ts +5 -0
  6. package/dist/formatters/checklist.js +30 -0
  7. package/dist/formatters/file.d.ts +5 -0
  8. package/dist/formatters/file.js +61 -0
  9. package/dist/formatters/group.d.ts +11 -0
  10. package/dist/formatters/group.js +53 -0
  11. package/dist/formatters/index.d.ts +15 -1
  12. package/dist/formatters/index.js +15 -1
  13. package/dist/formatters/issue.d.ts +13 -3
  14. package/dist/formatters/issue.js +43 -10
  15. package/dist/formatters/journals.d.ts +7 -1
  16. package/dist/formatters/journals.js +13 -15
  17. package/dist/formatters/membership.d.ts +5 -0
  18. package/dist/formatters/membership.js +41 -0
  19. package/dist/formatters/metadata.d.ts +135 -0
  20. package/dist/formatters/metadata.js +213 -0
  21. package/dist/formatters/project.d.ts +11 -0
  22. package/dist/formatters/project.js +82 -0
  23. package/dist/formatters/relation.d.ts +5 -0
  24. package/dist/formatters/relation.js +55 -0
  25. package/dist/formatters/search.d.ts +5 -0
  26. package/dist/formatters/search.js +61 -0
  27. package/dist/formatters/time.d.ts +11 -0
  28. package/dist/formatters/time.js +47 -0
  29. package/dist/formatters/user.d.ts +11 -0
  30. package/dist/formatters/user.js +83 -0
  31. package/dist/formatters/utils.d.ts +12 -0
  32. package/dist/formatters/utils.js +31 -0
  33. package/dist/formatters/version.d.ts +11 -0
  34. package/dist/formatters/version.js +75 -0
  35. package/dist/formatters/wiki.d.ts +11 -0
  36. package/dist/formatters/wiki.js +86 -0
  37. package/dist/redmine/client.d.ts +4 -0
  38. package/dist/redmine/client.js +6 -0
  39. package/dist/server.js +1 -1
  40. package/dist/tools/account.js +7 -1
  41. package/dist/tools/admin.js +25 -4
  42. package/dist/tools/agile.js +19 -3
  43. package/dist/tools/checklists.js +13 -2
  44. package/dist/tools/core.js +23 -5
  45. package/dist/tools/enumerations.js +19 -3
  46. package/dist/tools/files.js +13 -2
  47. package/dist/tools/memberships.js +13 -2
  48. package/dist/tools/metadata.js +37 -6
  49. package/dist/tools/relations.js +25 -4
  50. package/dist/tools/roles.js +13 -2
  51. package/dist/tools/search.js +7 -1
  52. package/dist/tools/time.js +13 -2
  53. package/dist/tools/wiki.js +13 -2
  54. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ import type { RedmineMyAccountResponse } from "../redmine/types.js";
2
+ export declare function formatMyAccount(response: RedmineMyAccountResponse): string;
@@ -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,5 @@
1
+ import type { RedmineAttachment, RedmineFilesResponse } from "../redmine/types.js";
2
+ export declare function formatAttachment(response: {
3
+ attachment: RedmineAttachment;
4
+ }): string;
5
+ export declare function formatFileList(response: RedmineFilesResponse): string;
@@ -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";
@@ -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;
@@ -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
+ }