@pschroee/redmine-mcp 0.4.4 → 0.5.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.
@@ -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,10 @@
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, } from "./metadata.js";
@@ -1,2 +1,10 @@
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, } from "./metadata.js";
@@ -1,4 +1,4 @@
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
3
  /**
4
4
  * Format an issue as complete Markdown
@@ -10,3 +10,7 @@ export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup): s
10
10
  export declare function formatIssueResponse(response: {
11
11
  issue: RedmineIssue;
12
12
  }, lookup?: NameLookup): string;
13
+ /**
14
+ * Format a list of issues as a Markdown table
15
+ */
16
+ export declare function formatIssueList(response: RedmineIssuesResponse): string;
@@ -126,3 +126,45 @@ export function formatIssue(issue, lookup = {}) {
126
126
  export function formatIssueResponse(response, lookup = {}) {
127
127
  return formatIssue(response.issue, lookup);
128
128
  }
129
+ /**
130
+ * Format a date string to YYYY-MM-DD format
131
+ */
132
+ function formatDateShort(isoDate) {
133
+ return new Date(isoDate).toISOString().slice(0, 10);
134
+ }
135
+ /**
136
+ * Format a list of issues as a Markdown table
137
+ */
138
+ export function formatIssueList(response) {
139
+ const { issues, total_count, offset, limit } = response;
140
+ const lines = [];
141
+ // Header with count
142
+ lines.push(`# Issues (${issues.length} of ${total_count})`);
143
+ lines.push("");
144
+ // Pagination info if needed
145
+ if (offset > 0 || total_count > limit) {
146
+ const start = offset + 1;
147
+ const end = offset + issues.length;
148
+ lines.push(`_Showing ${start}-${end} of ${total_count}_`);
149
+ lines.push("");
150
+ }
151
+ // Empty case
152
+ if (issues.length === 0) {
153
+ lines.push("No issues found.");
154
+ return lines.join("\n");
155
+ }
156
+ // Table header
157
+ lines.push("| ID | Subject | Status | Priority | Assigned | Updated |");
158
+ lines.push("|---|---|---|---|---|---|");
159
+ // Table rows
160
+ for (const issue of issues) {
161
+ const id = `#${issue.id}`;
162
+ const subject = issue.subject;
163
+ const status = issue.status.name;
164
+ const priority = issue.priority.name;
165
+ const assigned = issue.assigned_to?.name ?? "_(unassigned)_";
166
+ const updated = formatDateShort(issue.updated_on);
167
+ lines.push(`| ${id} | ${subject} | ${status} | ${priority} | ${assigned} | ${updated} |`);
168
+ }
169
+ return lines.join("\n");
170
+ }
@@ -3,6 +3,58 @@ import { diffLines } from "diff";
3
3
  * Fields that contain large text and should use diff formatting
4
4
  */
5
5
  const LARGE_TEXT_FIELDS = ["description"];
6
+ /**
7
+ * Format checklist changes as a readable diff
8
+ * Only shows items that changed between old and new state
9
+ */
10
+ function formatChecklistDiff(oldValue, newValue) {
11
+ let oldItems = [];
12
+ let newItems = [];
13
+ try {
14
+ oldItems = JSON.parse(oldValue || "[]");
15
+ }
16
+ catch {
17
+ // Invalid JSON, fall back to raw display
18
+ return null;
19
+ }
20
+ try {
21
+ newItems = JSON.parse(newValue || "[]");
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ // Create maps for easy lookup by subject (more reliable than ID for diffing)
27
+ const oldBySubject = new Map(oldItems.map(item => [item.subject, item]));
28
+ const newBySubject = new Map(newItems.map(item => [item.subject, item]));
29
+ const changes = [];
30
+ // Find changed and added items
31
+ for (const newItem of newItems) {
32
+ const oldItem = oldBySubject.get(newItem.subject);
33
+ if (!oldItem) {
34
+ // New item added
35
+ const status = newItem.is_done ? "✅" : "❌";
36
+ changes.push(` - ➕ ${newItem.subject}: ${status}`);
37
+ }
38
+ else if (oldItem.is_done !== newItem.is_done) {
39
+ // Status changed
40
+ const oldStatus = oldItem.is_done ? "✅" : "❌";
41
+ const newStatus = newItem.is_done ? "✅" : "❌";
42
+ changes.push(` - ${newItem.subject}: ${oldStatus} → ${newStatus}`);
43
+ }
44
+ // If subject and status are same, no change to report
45
+ }
46
+ // Find removed items
47
+ for (const oldItem of oldItems) {
48
+ if (!newBySubject.has(oldItem.subject)) {
49
+ const status = oldItem.is_done ? "✅" : "❌";
50
+ changes.push(` - ➖ ${oldItem.subject}: ${status}`);
51
+ }
52
+ }
53
+ if (changes.length === 0) {
54
+ return null; // No meaningful changes
55
+ }
56
+ return changes.join("\n");
57
+ }
6
58
  /**
7
59
  * Field name mappings for display (remove _id suffix)
8
60
  */
@@ -97,6 +149,14 @@ function formatDetail(detail, lookup) {
97
149
  }
98
150
  // Handle attribute changes
99
151
  if (property === "attr") {
152
+ // Checklist changes get special formatting
153
+ if (name === "checklist") {
154
+ const checklistDiff = formatChecklistDiff(old_value || "", new_value || "");
155
+ if (checklistDiff) {
156
+ return `- checklist:\n${checklistDiff}`;
157
+ }
158
+ // Fall through to default formatting if parsing failed
159
+ }
100
160
  // Large text fields get diff formatting
101
161
  if (LARGE_TEXT_FIELDS.includes(name) && old_value && new_value) {
102
162
  const diff = generateDiff(old_value, new_value);
@@ -0,0 +1,88 @@
1
+ interface RedmineTracker {
2
+ id: number;
3
+ name: string;
4
+ description?: string;
5
+ }
6
+ interface RedmineTrackersResponse {
7
+ trackers: RedmineTracker[];
8
+ }
9
+ interface RedmineIssueStatus {
10
+ id: number;
11
+ name: string;
12
+ is_closed: boolean;
13
+ }
14
+ interface RedmineIssueStatusesResponse {
15
+ issue_statuses: RedmineIssueStatus[];
16
+ }
17
+ interface RedmineCategory {
18
+ id: number;
19
+ name: string;
20
+ project?: {
21
+ id: number;
22
+ name: string;
23
+ };
24
+ assigned_to?: {
25
+ id: number;
26
+ name: string;
27
+ };
28
+ }
29
+ interface RedmineCategoriesResponse {
30
+ issue_categories: RedmineCategory[];
31
+ }
32
+ interface RedminePriority {
33
+ id: number;
34
+ name: string;
35
+ is_default: boolean;
36
+ }
37
+ interface RedminePrioritiesResponse {
38
+ issue_priorities: RedminePriority[];
39
+ }
40
+ interface RedmineActivity {
41
+ id: number;
42
+ name: string;
43
+ is_default: boolean;
44
+ active?: boolean;
45
+ }
46
+ interface RedmineActivitiesResponse {
47
+ time_entry_activities: RedmineActivity[];
48
+ }
49
+ interface RedmineRole {
50
+ id: number;
51
+ name: string;
52
+ assignable?: boolean;
53
+ permissions?: string[];
54
+ }
55
+ interface RedmineRolesResponse {
56
+ roles: RedmineRole[];
57
+ }
58
+ /**
59
+ * Format a list of trackers as a Markdown table
60
+ */
61
+ export declare function formatTrackerList(response: RedmineTrackersResponse): string;
62
+ /**
63
+ * Format a list of issue statuses as a Markdown table
64
+ */
65
+ export declare function formatStatusList(response: RedmineIssueStatusesResponse): string;
66
+ /**
67
+ * Format a list of issue categories as a Markdown table
68
+ */
69
+ export declare function formatCategoryList(response: RedmineCategoriesResponse): string;
70
+ /**
71
+ * Format a list of issue priorities as a Markdown table
72
+ */
73
+ export declare function formatPriorityList(response: RedminePrioritiesResponse): string;
74
+ /**
75
+ * Format a list of time entry activities as a Markdown table
76
+ */
77
+ export declare function formatActivityList(response: RedmineActivitiesResponse): string;
78
+ /**
79
+ * Format a list of roles as a Markdown table
80
+ */
81
+ export declare function formatRoleList(response: RedmineRolesResponse): string;
82
+ /**
83
+ * Format a single role with details as Markdown
84
+ */
85
+ export declare function formatRole(response: {
86
+ role: RedmineRole;
87
+ }): string;
88
+ export {};
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Format a list of trackers as a Markdown table
3
+ */
4
+ export function formatTrackerList(response) {
5
+ const lines = [];
6
+ lines.push("# Trackers");
7
+ lines.push("");
8
+ if (response.trackers.length === 0) {
9
+ lines.push("No trackers found.");
10
+ return lines.join("\n");
11
+ }
12
+ lines.push("| ID | Name |");
13
+ lines.push("|----|------|");
14
+ for (const tracker of response.trackers) {
15
+ lines.push(`| ${tracker.id} | ${tracker.name} |`);
16
+ }
17
+ return lines.join("\n");
18
+ }
19
+ /**
20
+ * Format a list of issue statuses as a Markdown table
21
+ */
22
+ export function formatStatusList(response) {
23
+ const lines = [];
24
+ lines.push("# Issue Statuses");
25
+ lines.push("");
26
+ if (response.issue_statuses.length === 0) {
27
+ lines.push("No statuses found.");
28
+ return lines.join("\n");
29
+ }
30
+ lines.push("| ID | Name | Closed |");
31
+ lines.push("|----|------|--------|");
32
+ for (const status of response.issue_statuses) {
33
+ const closed = status.is_closed ? "Yes" : "No";
34
+ lines.push(`| ${status.id} | ${status.name} | ${closed} |`);
35
+ }
36
+ return lines.join("\n");
37
+ }
38
+ /**
39
+ * Format a list of issue categories as a Markdown table
40
+ */
41
+ export function formatCategoryList(response) {
42
+ const lines = [];
43
+ lines.push("# Issue Categories");
44
+ lines.push("");
45
+ if (response.issue_categories.length === 0) {
46
+ lines.push("No categories found.");
47
+ return lines.join("\n");
48
+ }
49
+ lines.push("| ID | Name | Assigned To |");
50
+ lines.push("|----|------|-------------|");
51
+ for (const category of response.issue_categories) {
52
+ const assignedTo = category.assigned_to?.name || "";
53
+ lines.push(`| ${category.id} | ${category.name} | ${assignedTo} |`);
54
+ }
55
+ return lines.join("\n");
56
+ }
57
+ /**
58
+ * Format a list of issue priorities as a Markdown table
59
+ */
60
+ export function formatPriorityList(response) {
61
+ const lines = [];
62
+ lines.push("# Issue Priorities");
63
+ lines.push("");
64
+ if (response.issue_priorities.length === 0) {
65
+ lines.push("No priorities found.");
66
+ return lines.join("\n");
67
+ }
68
+ lines.push("| ID | Name | Default |");
69
+ lines.push("|----|------|---------|");
70
+ for (const priority of response.issue_priorities) {
71
+ const isDefault = priority.is_default ? "Yes" : "No";
72
+ lines.push(`| ${priority.id} | ${priority.name} | ${isDefault} |`);
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+ /**
77
+ * Format a list of time entry activities as a Markdown table
78
+ */
79
+ export function formatActivityList(response) {
80
+ const lines = [];
81
+ lines.push("# Time Entry Activities");
82
+ lines.push("");
83
+ if (response.time_entry_activities.length === 0) {
84
+ lines.push("No activities found.");
85
+ return lines.join("\n");
86
+ }
87
+ lines.push("| ID | Name | Default | Active |");
88
+ lines.push("|----|------|---------|--------|");
89
+ for (const activity of response.time_entry_activities) {
90
+ const isDefault = activity.is_default ? "Yes" : "No";
91
+ const isActive = activity.active === undefined ? "" : activity.active ? "Yes" : "No";
92
+ lines.push(`| ${activity.id} | ${activity.name} | ${isDefault} | ${isActive} |`);
93
+ }
94
+ return lines.join("\n");
95
+ }
96
+ /**
97
+ * Format a list of roles as a Markdown table
98
+ */
99
+ export function formatRoleList(response) {
100
+ const lines = [];
101
+ lines.push("# Roles");
102
+ lines.push("");
103
+ if (response.roles.length === 0) {
104
+ lines.push("No roles found.");
105
+ return lines.join("\n");
106
+ }
107
+ lines.push("| ID | Name | Assignable |");
108
+ lines.push("|----|------|------------|");
109
+ for (const role of response.roles) {
110
+ const assignable = role.assignable === undefined ? "" : role.assignable ? "Yes" : "No";
111
+ lines.push(`| ${role.id} | ${role.name} | ${assignable} |`);
112
+ }
113
+ return lines.join("\n");
114
+ }
115
+ /**
116
+ * Format a single role with details as Markdown
117
+ */
118
+ export function formatRole(response) {
119
+ const role = response.role;
120
+ const lines = [];
121
+ lines.push(`# ${role.name}`);
122
+ lines.push("");
123
+ lines.push(`**ID:** ${role.id}`);
124
+ if (role.assignable !== undefined) {
125
+ lines.push(`**Assignable:** ${role.assignable ? "Yes" : "No"}`);
126
+ }
127
+ lines.push("");
128
+ if (role.permissions && role.permissions.length > 0) {
129
+ lines.push("## Permissions");
130
+ lines.push("");
131
+ for (const permission of role.permissions) {
132
+ lines.push(`- ${permission}`);
133
+ }
134
+ lines.push("");
135
+ }
136
+ return lines.join("\n").trimEnd();
137
+ }
@@ -0,0 +1,11 @@
1
+ import type { RedmineProject, RedmineProjectsResponse } from "../redmine/types.js";
2
+ /**
3
+ * Format a single project as complete Markdown
4
+ */
5
+ export declare function formatProject(response: {
6
+ project: RedmineProject;
7
+ }): string;
8
+ /**
9
+ * Format a list of projects as Markdown
10
+ */
11
+ export declare function formatProjectList(response: RedmineProjectsResponse): string;
@@ -0,0 +1,87 @@
1
+ const PROJECT_STATUS = {
2
+ 1: "Active",
3
+ 5: "Closed",
4
+ 9: "Archived",
5
+ };
6
+ /**
7
+ * Format a date string to readable format
8
+ */
9
+ function formatDate(isoDate) {
10
+ return new Date(isoDate).toISOString().slice(0, 16).replace("T", " ");
11
+ }
12
+ /**
13
+ * Format a single project as complete Markdown
14
+ */
15
+ export function formatProject(response) {
16
+ const project = response.project;
17
+ const lines = [];
18
+ // Title
19
+ lines.push(`# ${project.name}`);
20
+ lines.push("");
21
+ // Status line
22
+ const statusParts = [];
23
+ statusParts.push(`**Identifier:** ${project.identifier}`);
24
+ statusParts.push(`**Status:** ${PROJECT_STATUS[project.status] || "Unknown"}`);
25
+ statusParts.push(`**Public:** ${project.is_public ? "Yes" : "No"}`);
26
+ lines.push(statusParts.join(" | "));
27
+ lines.push("");
28
+ // Description
29
+ if (project.description) {
30
+ lines.push("## Description");
31
+ lines.push("");
32
+ lines.push(project.description);
33
+ lines.push("");
34
+ }
35
+ // Metadata table
36
+ lines.push("| Field | Value |");
37
+ lines.push("|-------|-------|");
38
+ if (project.parent) {
39
+ lines.push(`| Parent | ${project.parent.name} |`);
40
+ }
41
+ if (project.homepage) {
42
+ lines.push(`| Homepage | ${project.homepage} |`);
43
+ }
44
+ lines.push(`| Created | ${formatDate(project.created_on)} |`);
45
+ lines.push(`| Updated | ${formatDate(project.updated_on)} |`);
46
+ lines.push("");
47
+ // Trackers
48
+ if (project.trackers && project.trackers.length > 0) {
49
+ lines.push("## Trackers");
50
+ lines.push("");
51
+ for (const tracker of project.trackers) {
52
+ lines.push(`- ${tracker.name}`);
53
+ }
54
+ lines.push("");
55
+ }
56
+ // Enabled modules
57
+ if (project.enabled_modules && project.enabled_modules.length > 0) {
58
+ lines.push("## Enabled Modules");
59
+ lines.push("");
60
+ for (const mod of project.enabled_modules) {
61
+ lines.push(`- ${mod.name}`);
62
+ }
63
+ lines.push("");
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+ /**
68
+ * Format a list of projects as Markdown
69
+ */
70
+ export function formatProjectList(response) {
71
+ const lines = [];
72
+ lines.push(`# Projects (${response.total_count})`);
73
+ lines.push("");
74
+ if (response.projects.length === 0) {
75
+ lines.push("No projects found.");
76
+ return lines.join("\n");
77
+ }
78
+ // Table header
79
+ lines.push("| Name | Identifier | Status | Public |");
80
+ lines.push("|------|------------|--------|--------|");
81
+ for (const project of response.projects) {
82
+ const status = PROJECT_STATUS[project.status] || "Unknown";
83
+ const isPublic = project.is_public ? "Yes" : "No";
84
+ lines.push(`| ${project.name} | ${project.identifier} | ${status} | ${isPublic} |`);
85
+ }
86
+ return lines.join("\n");
87
+ }
@@ -0,0 +1,5 @@
1
+ import type { RedmineSearchResponse } from "../redmine/types.js";
2
+ /**
3
+ * Format search results as Markdown
4
+ */
5
+ export declare function formatSearchResults(response: RedmineSearchResponse): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Get icon for search result type
3
+ */
4
+ function getTypeIcon(type) {
5
+ switch (type.toLowerCase()) {
6
+ case "issue":
7
+ return "\u{1F3AB}"; // ticket
8
+ case "wiki-page":
9
+ return "\u{1F4C4}"; // page
10
+ case "project":
11
+ return "\u{1F4C1}"; // folder
12
+ default:
13
+ return "\u{1F4CB}"; // clipboard
14
+ }
15
+ }
16
+ /**
17
+ * Format a date string to YYYY-MM-DD format
18
+ */
19
+ function formatDateShort(isoDate) {
20
+ return new Date(isoDate).toISOString().slice(0, 10);
21
+ }
22
+ /**
23
+ * Truncate text to a maximum length with ellipsis
24
+ */
25
+ function truncate(text, maxLength) {
26
+ if (text.length <= maxLength) {
27
+ return text;
28
+ }
29
+ return text.slice(0, maxLength) + "...";
30
+ }
31
+ /**
32
+ * Format a single search result as Markdown
33
+ */
34
+ function formatSearchResult(result) {
35
+ const lines = [];
36
+ const icon = getTypeIcon(result.type);
37
+ // Title with icon
38
+ lines.push(`### ${icon} ${result.title}`);
39
+ // Type and date metadata
40
+ lines.push(`**Type:** ${result.type} | **Date:** ${formatDateShort(result.datetime)}`);
41
+ // Description (truncated)
42
+ if (result.description) {
43
+ lines.push("");
44
+ lines.push(truncate(result.description, 200));
45
+ }
46
+ return lines.join("\n");
47
+ }
48
+ /**
49
+ * Format search results as Markdown
50
+ */
51
+ export function formatSearchResults(response) {
52
+ const { results, total_count, offset } = response;
53
+ const lines = [];
54
+ // Header with count
55
+ lines.push(`# Search Results (${results.length} of ${total_count})`);
56
+ lines.push("");
57
+ // Offset info if applicable
58
+ if (offset > 0) {
59
+ lines.push(`_Starting from result ${offset + 1}_`);
60
+ lines.push("");
61
+ }
62
+ // Empty case
63
+ if (results.length === 0) {
64
+ lines.push("No results found.");
65
+ return lines.join("\n");
66
+ }
67
+ // Format each result
68
+ for (let i = 0; i < results.length; i++) {
69
+ if (i > 0) {
70
+ lines.push("");
71
+ }
72
+ lines.push(formatSearchResult(results[i]));
73
+ }
74
+ return lines.join("\n");
75
+ }
@@ -0,0 +1,11 @@
1
+ import type { RedmineTimeEntry, RedmineTimeEntriesResponse } from "../redmine/types.js";
2
+ /**
3
+ * Format a single time entry as complete Markdown
4
+ */
5
+ export declare function formatTimeEntry(response: {
6
+ time_entry: RedmineTimeEntry;
7
+ }): string;
8
+ /**
9
+ * Format a list of time entries as Markdown
10
+ */
11
+ export declare function formatTimeEntryList(response: RedmineTimeEntriesResponse): string;