redmine-devrelay-client 0.1.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,18 @@
1
+ import type { RedmineConfig } from "./config.js";
2
+ import { RedmineHttp } from "./http.js";
3
+ import type { IssueInclude, ListProjectsResult, NormalizedIssueDetail, RedmineUser, SearchIssuesInput, SearchIssuesResult } from "./types.js";
4
+ export declare class RedmineClient {
5
+ readonly http: RedmineHttp;
6
+ readonly config: RedmineConfig;
7
+ constructor(http: RedmineHttp, config: RedmineConfig);
8
+ static fromEnv(env?: NodeJS.ProcessEnv): RedmineClient;
9
+ getCurrentUser(): Promise<RedmineUser>;
10
+ listProjects(opts?: {
11
+ search?: string;
12
+ limit?: number;
13
+ }): Promise<ListProjectsResult>;
14
+ searchIssues(input?: SearchIssuesInput): Promise<SearchIssuesResult>;
15
+ getIssue(issueId: number, opts?: {
16
+ include?: IssueInclude[];
17
+ }): Promise<NormalizedIssueDetail>;
18
+ }
package/dist/client.js ADDED
@@ -0,0 +1,73 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { RedmineHttp } from "./http.js";
3
+ import { getIssue, searchIssues } from "./issues.js";
4
+ function normalizeUser(raw) {
5
+ const name = [raw.lastname, raw.firstname].filter(Boolean).join(" ").trim();
6
+ return {
7
+ id: raw.id,
8
+ login: raw.login,
9
+ name: name || raw.login,
10
+ };
11
+ }
12
+ function normalizeProject(raw) {
13
+ return {
14
+ id: raw.id,
15
+ identifier: raw.identifier,
16
+ name: raw.name,
17
+ description: raw.description ?? "",
18
+ isPublic: Boolean(raw.is_public),
19
+ parent: raw.parent ?? null,
20
+ status: raw.status ?? 1,
21
+ };
22
+ }
23
+ export class RedmineClient {
24
+ http;
25
+ config;
26
+ constructor(http, config) {
27
+ this.http = http;
28
+ this.config = config;
29
+ }
30
+ static fromEnv(env = process.env) {
31
+ const config = loadConfig(env);
32
+ return new RedmineClient(new RedmineHttp(config), config);
33
+ }
34
+ async getCurrentUser() {
35
+ const data = await this.http.getJson("/users/current.json");
36
+ return normalizeUser(data.user);
37
+ }
38
+ async listProjects(opts = {}) {
39
+ const wanted = Math.min(opts.limit ?? this.config.maxResultCount, this.config.maxResultCount);
40
+ const pageSize = Math.min(100, wanted);
41
+ const collected = [];
42
+ let offset = 0;
43
+ let totalCount = 0;
44
+ while (collected.length < wanted) {
45
+ const page = await this.http.getJson("/projects.json", { limit: pageSize, offset });
46
+ totalCount = page.total_count;
47
+ collected.push(...page.projects.map(normalizeProject));
48
+ offset += page.projects.length;
49
+ if (page.projects.length === 0 || offset >= totalCount)
50
+ break;
51
+ }
52
+ let projects = collected;
53
+ if (opts.search) {
54
+ const q = opts.search.toLowerCase();
55
+ projects = projects.filter((p) => p.name.toLowerCase().includes(q) ||
56
+ p.identifier.toLowerCase().includes(q));
57
+ }
58
+ projects = projects.slice(0, wanted);
59
+ return {
60
+ projects,
61
+ totalCount,
62
+ returnedCount: projects.length,
63
+ hasMore: offset < totalCount && projects.length >= wanted,
64
+ };
65
+ }
66
+ searchIssues(input = {}) {
67
+ return searchIssues(this.http, this.config, input);
68
+ }
69
+ getIssue(issueId, opts = {}) {
70
+ return getIssue(this.http, issueId, opts);
71
+ }
72
+ }
73
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type RedmineConfig = {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ connectTimeoutMs: number;
5
+ requestTimeoutMs: number;
6
+ maxResultCount: number;
7
+ logLevel: string;
8
+ caCertPath?: string;
9
+ allowedHosts?: string[];
10
+ defaultProjectId?: number;
11
+ userAgent: string;
12
+ };
13
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): RedmineConfig;
package/dist/config.js ADDED
@@ -0,0 +1,109 @@
1
+ import { RedmineError } from "./errors.js";
2
+ const FORBIDDEN_HOSTS = new Set(["169.254.169.254", "metadata.google.internal"]);
3
+ /** Loopback / compose service names allowed for local Docker http. */
4
+ const LOCAL_HTTP_HOSTS = new Set(["localhost", "127.0.0.1", "redmine"]);
5
+ function isPrivateIpv4(host) {
6
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
7
+ if (!m)
8
+ return false;
9
+ const octets = m.slice(1).map(Number);
10
+ if (octets.some((n) => n > 255))
11
+ return false;
12
+ const [a, b] = octets;
13
+ if (a === 10)
14
+ return true;
15
+ if (a === 192 && b === 168)
16
+ return true;
17
+ if (a === 172 && b >= 16 && b <= 31)
18
+ return true;
19
+ return false;
20
+ }
21
+ function parseIntEnv(env, name, fallback) {
22
+ const raw = env[name];
23
+ if (raw === undefined || raw === "")
24
+ return fallback;
25
+ const n = Number(raw);
26
+ if (!Number.isFinite(n) || n <= 0) {
27
+ throw new RedmineError({
28
+ code: "REDMINE_VALIDATION_ERROR",
29
+ message: `Invalid ${name}: ${raw}`,
30
+ check: [`Set ${name} to a positive number`],
31
+ });
32
+ }
33
+ return n;
34
+ }
35
+ export function loadConfig(env = process.env) {
36
+ const urlRaw = env.REDMINE_URL?.trim();
37
+ const apiKey = env.REDMINE_API_KEY?.trim();
38
+ if (!urlRaw || !apiKey) {
39
+ throw new RedmineError({
40
+ code: "REDMINE_VALIDATION_ERROR",
41
+ message: "REDMINE_URL and REDMINE_API_KEY are required",
42
+ check: ["Export REDMINE_URL", "Export REDMINE_API_KEY"],
43
+ });
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = new URL(urlRaw);
48
+ }
49
+ catch {
50
+ throw new RedmineError({
51
+ code: "REDMINE_VALIDATION_ERROR",
52
+ message: `Invalid REDMINE_URL: ${urlRaw}`,
53
+ check: ["Use an absolute URL like https://redmine.example.com"],
54
+ });
55
+ }
56
+ if (parsed.protocol === "file:" || parsed.protocol === "ftp:") {
57
+ throw new RedmineError({
58
+ code: "REDMINE_VALIDATION_ERROR",
59
+ message: `Forbidden URL protocol: ${parsed.protocol}`,
60
+ });
61
+ }
62
+ const host = parsed.hostname.toLowerCase();
63
+ if (FORBIDDEN_HOSTS.has(host) || host.startsWith("169.254.")) {
64
+ throw new RedmineError({
65
+ code: "REDMINE_VALIDATION_ERROR",
66
+ message: "Forbidden link-local/metadata host",
67
+ });
68
+ }
69
+ const allowedHosts = env.REDMINE_ALLOWED_HOSTS
70
+ ?.split(",")
71
+ .map((h) => h.trim().toLowerCase())
72
+ .filter(Boolean);
73
+ if (allowedHosts?.length && !allowedHosts.includes(host)) {
74
+ throw new RedmineError({
75
+ code: "REDMINE_VALIDATION_ERROR",
76
+ message: `Host ${host} is not in REDMINE_ALLOWED_HOSTS`,
77
+ check: ["Add the Redmine hostname to REDMINE_ALLOWED_HOSTS"],
78
+ });
79
+ }
80
+ const isInsecureHttpAllowed = parsed.protocol === "http:" &&
81
+ !!allowedHosts?.includes(host) &&
82
+ (LOCAL_HTTP_HOSTS.has(host) || isPrivateIpv4(host));
83
+ if (parsed.protocol !== "https:" && !isInsecureHttpAllowed) {
84
+ throw new RedmineError({
85
+ code: "REDMINE_VALIDATION_ERROR",
86
+ message: "REDMINE_URL must use https (http only for allowlisted local/private hosts)",
87
+ check: [
88
+ "Use https://...",
89
+ "Or allowlist localhost/redmine/RFC1918 hosts for local Redmine",
90
+ ],
91
+ });
92
+ }
93
+ const baseUrl = urlRaw.replace(/\/$/, "");
94
+ return {
95
+ baseUrl,
96
+ apiKey,
97
+ connectTimeoutMs: parseIntEnv(env, "REDMINE_CONNECT_TIMEOUT_MS", 5000),
98
+ requestTimeoutMs: parseIntEnv(env, "REDMINE_REQUEST_TIMEOUT_MS", 15000),
99
+ maxResultCount: parseIntEnv(env, "REDMINE_MAX_RESULT_COUNT", 100),
100
+ logLevel: env.REDMINE_LOG_LEVEL ?? "info",
101
+ caCertPath: env.REDMINE_CA_CERT_PATH || undefined,
102
+ allowedHosts,
103
+ defaultProjectId: env.REDMINE_DEFAULT_PROJECT_ID
104
+ ? Number(env.REDMINE_DEFAULT_PROJECT_ID)
105
+ : undefined,
106
+ userAgent: "redmine-mcp/0.1.0",
107
+ };
108
+ }
109
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export type RedmineErrorCode = "REDMINE_CONNECTION_ERROR" | "REDMINE_AUTHENTICATION_ERROR" | "REDMINE_PERMISSION_DENIED" | "REDMINE_ISSUE_NOT_FOUND" | "REDMINE_PROJECT_NOT_FOUND" | "REDMINE_VALIDATION_ERROR" | "REDMINE_TIMEOUT" | "REDMINE_TLS_ERROR" | "REDMINE_UNKNOWN_ERROR";
2
+ export declare class RedmineError extends Error {
3
+ readonly code: RedmineErrorCode;
4
+ readonly httpStatus?: number;
5
+ readonly dataChanged: false;
6
+ readonly retrySafe: boolean;
7
+ readonly check: string[];
8
+ constructor(opts: {
9
+ code: RedmineErrorCode;
10
+ message: string;
11
+ httpStatus?: number;
12
+ retrySafe?: boolean;
13
+ check?: string[];
14
+ });
15
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,16 @@
1
+ export class RedmineError extends Error {
2
+ code;
3
+ httpStatus;
4
+ dataChanged = false;
5
+ retrySafe;
6
+ check;
7
+ constructor(opts) {
8
+ super(opts.message);
9
+ this.name = "RedmineError";
10
+ this.code = opts.code;
11
+ this.httpStatus = opts.httpStatus;
12
+ this.retrySafe = opts.retrySafe ?? false;
13
+ this.check = opts.check ?? [];
14
+ }
15
+ }
16
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,CAAmB;IACvB,UAAU,CAAU;IACpB,WAAW,GAAG,KAAc,CAAC;IAC7B,SAAS,CAAU;IACnB,KAAK,CAAW;IAEzB,YAAY,IAMX;QACC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAChC,CAAC;CACF"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { RedmineConfig } from "./config.js";
2
+ export type RedmineHttpOptions = {
3
+ retryBackoffMs?: number;
4
+ };
5
+ export declare class RedmineHttp {
6
+ private readonly config;
7
+ private readonly opts;
8
+ private readonly dispatcher?;
9
+ constructor(config: RedmineConfig, opts?: RedmineHttpOptions);
10
+ getJson<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
11
+ private buildUrl;
12
+ private request;
13
+ private mapStatus;
14
+ private mapNetworkError;
15
+ private isRetryableNetworkError;
16
+ private maskError;
17
+ private backoffMs;
18
+ private sleep;
19
+ }
package/dist/http.js ADDED
@@ -0,0 +1,168 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { Agent } from "undici";
3
+ import { RedmineError } from "./errors.js";
4
+ import { maskSecret } from "./mask.js";
5
+ const RETRYABLE = new Set([429, 502, 503, 504]);
6
+ const MAX_RETRIES = 2;
7
+ export class RedmineHttp {
8
+ config;
9
+ opts;
10
+ dispatcher;
11
+ constructor(config, opts = {}) {
12
+ this.config = config;
13
+ this.opts = opts;
14
+ if (config.caCertPath) {
15
+ const ca = readFileSync(config.caCertPath);
16
+ this.dispatcher = new Agent({ connect: { ca } });
17
+ }
18
+ }
19
+ async getJson(path, query) {
20
+ const url = this.buildUrl(path, query);
21
+ let attempt = 0;
22
+ let lastError;
23
+ while (attempt <= MAX_RETRIES) {
24
+ try {
25
+ const response = await this.request(url);
26
+ if (RETRYABLE.has(response.status) && attempt < MAX_RETRIES) {
27
+ await this.sleep(this.backoffMs(attempt));
28
+ attempt += 1;
29
+ continue;
30
+ }
31
+ if (!response.ok) {
32
+ const bodyText = await response.text();
33
+ this.mapStatus(response.status, bodyText);
34
+ }
35
+ return (await response.json());
36
+ }
37
+ catch (err) {
38
+ if (err instanceof RedmineError) {
39
+ throw this.maskError(err);
40
+ }
41
+ lastError = err;
42
+ if (this.isRetryableNetworkError(err) && attempt < MAX_RETRIES) {
43
+ await this.sleep(this.backoffMs(attempt));
44
+ attempt += 1;
45
+ continue;
46
+ }
47
+ throw this.mapNetworkError(err);
48
+ }
49
+ }
50
+ throw this.mapNetworkError(lastError);
51
+ }
52
+ buildUrl(path, query) {
53
+ const url = new URL(path, `${this.config.baseUrl}/`);
54
+ if (query) {
55
+ for (const [key, value] of Object.entries(query)) {
56
+ if (value === undefined)
57
+ continue;
58
+ url.searchParams.set(key, String(value));
59
+ }
60
+ }
61
+ return url.toString();
62
+ }
63
+ async request(url, body) {
64
+ const headers = {
65
+ Accept: "application/json",
66
+ "User-Agent": this.config.userAgent,
67
+ "X-Redmine-API-Key": this.config.apiKey,
68
+ };
69
+ if (body !== undefined) {
70
+ headers["Content-Type"] = "application/json";
71
+ }
72
+ const init = {
73
+ method: "GET",
74
+ headers,
75
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs),
76
+ };
77
+ if (this.dispatcher) {
78
+ init.dispatcher = this.dispatcher;
79
+ }
80
+ return fetch(url, init);
81
+ }
82
+ mapStatus(status, bodyText) {
83
+ const maskedBody = maskSecret(bodyText, this.config.apiKey);
84
+ if (status === 401) {
85
+ throw new RedmineError({
86
+ code: "REDMINE_AUTHENTICATION_ERROR",
87
+ message: "Redmine authentication failed",
88
+ httpStatus: status,
89
+ check: ["Verify REDMINE_API_KEY"],
90
+ retrySafe: false,
91
+ });
92
+ }
93
+ if (status === 403) {
94
+ throw new RedmineError({
95
+ code: "REDMINE_PERMISSION_DENIED",
96
+ message: "Redmine permission denied",
97
+ httpStatus: status,
98
+ check: ["Confirm the user can access this resource in Redmine"],
99
+ retrySafe: false,
100
+ });
101
+ }
102
+ if (status === 404) {
103
+ throw new RedmineError({
104
+ code: "REDMINE_UNKNOWN_ERROR",
105
+ message: `Redmine resource not found: ${maskedBody || status}`,
106
+ httpStatus: status,
107
+ retrySafe: false,
108
+ });
109
+ }
110
+ throw new RedmineError({
111
+ code: "REDMINE_UNKNOWN_ERROR",
112
+ message: `Redmine HTTP ${status}: ${maskedBody || "request failed"}`,
113
+ httpStatus: status,
114
+ retrySafe: RETRYABLE.has(status),
115
+ check: ["Retry later if Redmine is temporarily unavailable"],
116
+ });
117
+ }
118
+ mapNetworkError(err) {
119
+ const message = maskSecret(err instanceof Error ? err.message : String(err), this.config.apiKey);
120
+ if (err instanceof Error &&
121
+ (err.name === "TimeoutError" || err.name === "AbortError")) {
122
+ return new RedmineError({
123
+ code: "REDMINE_TIMEOUT",
124
+ message: `Redmine request timed out: ${message}`,
125
+ retrySafe: true,
126
+ check: ["Increase REDMINE_REQUEST_TIMEOUT_MS", "Check Redmine health"],
127
+ });
128
+ }
129
+ if (/certificate|SSL|TLS|CERT/i.test(message)) {
130
+ return new RedmineError({
131
+ code: "REDMINE_TLS_ERROR",
132
+ message: `Redmine TLS error: ${message}`,
133
+ retrySafe: false,
134
+ check: ["Set REDMINE_CA_CERT_PATH to your company CA file"],
135
+ });
136
+ }
137
+ return new RedmineError({
138
+ code: "REDMINE_CONNECTION_ERROR",
139
+ message: `Cannot reach Redmine: ${message}`,
140
+ retrySafe: true,
141
+ check: ["Verify REDMINE_URL", "Check VPN / network connectivity"],
142
+ });
143
+ }
144
+ isRetryableNetworkError(err) {
145
+ if (!(err instanceof Error))
146
+ return false;
147
+ if (err.name === "TimeoutError" || err.name === "AbortError")
148
+ return false;
149
+ return true;
150
+ }
151
+ maskError(err) {
152
+ return new RedmineError({
153
+ code: err.code,
154
+ message: maskSecret(err.message, this.config.apiKey),
155
+ httpStatus: err.httpStatus,
156
+ retrySafe: err.retrySafe,
157
+ check: err.check,
158
+ });
159
+ }
160
+ backoffMs(attempt) {
161
+ const base = this.opts.retryBackoffMs ?? 200;
162
+ return base * 2 ** attempt;
163
+ }
164
+ sleep(ms) {
165
+ return new Promise((resolve) => setTimeout(resolve, ms));
166
+ }
167
+ }
168
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { loadConfig, type RedmineConfig } from "./config.js";
2
+ export { RedmineError, type RedmineErrorCode } from "./errors.js";
3
+ export { maskSecret } from "./mask.js";
4
+ export { RedmineHttp, type RedmineHttpOptions } from "./http.js";
5
+ export { RedmineClient } from "./client.js";
6
+ export { buildIssueQuery } from "./issues.js";
7
+ export type * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { loadConfig } from "./config.js";
2
+ export { RedmineError } from "./errors.js";
3
+ export { maskSecret } from "./mask.js";
4
+ export { RedmineHttp } from "./http.js";
5
+ export { RedmineClient } from "./client.js";
6
+ export { buildIssueQuery } from "./issues.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAyB,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,WAAW,EAA2B,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { RedmineConfig } from "./config.js";
2
+ import type { RedmineHttp } from "./http.js";
3
+ import type { IssueInclude, NormalizedIssueDetail, SearchIssuesInput, SearchIssuesResult } from "./types.js";
4
+ export declare function buildIssueQuery(input: SearchIssuesInput): Record<string, string | number>;
5
+ export declare function searchIssues(http: RedmineHttp, config: RedmineConfig, input?: SearchIssuesInput): Promise<SearchIssuesResult>;
6
+ export declare function getIssue(http: RedmineHttp, issueId: number, opts?: {
7
+ include?: IssueInclude[];
8
+ }): Promise<NormalizedIssueDetail>;
package/dist/issues.js ADDED
@@ -0,0 +1,153 @@
1
+ import { RedmineError } from "./errors.js";
2
+ export function buildIssueQuery(input) {
3
+ const query = {};
4
+ if (input.projectId !== undefined)
5
+ query.project_id = input.projectId;
6
+ if (input.issueId !== undefined)
7
+ query.issue_id = input.issueId;
8
+ if (input.assignedTo !== undefined) {
9
+ query.assigned_to_id = String(input.assignedTo);
10
+ }
11
+ if (input.status === undefined || input.status === "open") {
12
+ query.status_id = "open";
13
+ }
14
+ else if (input.status === "closed") {
15
+ query.status_id = "closed";
16
+ }
17
+ else if (input.status === "all") {
18
+ query.status_id = "*";
19
+ }
20
+ else {
21
+ query.status_id = input.status;
22
+ }
23
+ if (input.trackerId !== undefined)
24
+ query.tracker_id = input.trackerId;
25
+ if (input.priorityId !== undefined)
26
+ query.priority_id = input.priorityId;
27
+ if (input.subjectContains)
28
+ query.subject = `~${input.subjectContains}`;
29
+ if (input.parentIssueId !== undefined) {
30
+ query.parent_id = input.parentIssueId;
31
+ }
32
+ if (input.createdAfter)
33
+ query.created_on = `>=${input.createdAfter}`;
34
+ if (input.updatedAfter)
35
+ query.updated_on = `>=${input.updatedAfter}`;
36
+ if (input.customFields) {
37
+ for (const field of input.customFields) {
38
+ query[`cf_${field.id}`] = field.value;
39
+ }
40
+ }
41
+ if (input.sort?.length) {
42
+ query.sort = input.sort
43
+ .map((s) => `${s.field}:${s.direction}`)
44
+ .join(",");
45
+ }
46
+ return query;
47
+ }
48
+ function normalizeSummary(raw) {
49
+ return {
50
+ id: raw.id,
51
+ subject: raw.subject,
52
+ project: raw.project ?? null,
53
+ tracker: raw.tracker ?? null,
54
+ status: raw.status ?? null,
55
+ priority: raw.priority ?? null,
56
+ assignedTo: raw.assigned_to ?? null,
57
+ updatedOn: raw.updated_on ?? null,
58
+ createdOn: raw.created_on ?? null,
59
+ };
60
+ }
61
+ function normalizeDetail(raw) {
62
+ return {
63
+ ...normalizeSummary(raw),
64
+ description: raw.description ?? "",
65
+ author: raw.author ?? null,
66
+ startDate: raw.start_date ?? null,
67
+ dueDate: raw.due_date ?? null,
68
+ doneRatio: raw.done_ratio ?? null,
69
+ estimatedHours: raw.estimated_hours ?? null,
70
+ parent: raw.parent ?? null,
71
+ customFields: (raw.custom_fields ?? []).map((f) => ({
72
+ id: f.id,
73
+ name: f.name,
74
+ value: f.value,
75
+ })),
76
+ journals: raw.journals?.map((j) => ({
77
+ id: j.id,
78
+ user: j.user ?? null,
79
+ notes: j.notes ?? "",
80
+ createdOn: j.created_on,
81
+ privateNotes: Boolean(j.private_notes),
82
+ })),
83
+ attachments: raw.attachments?.map((a) => ({
84
+ id: a.id,
85
+ filename: a.filename,
86
+ filesize: a.filesize,
87
+ contentType: a.content_type,
88
+ createdOn: a.created_on,
89
+ })),
90
+ relations: raw.relations,
91
+ children: raw.children,
92
+ allowedStatuses: raw.allowed_statuses,
93
+ };
94
+ }
95
+ export async function searchIssues(http, config, input = {}) {
96
+ const wanted = Math.min(input.limit ?? config.maxResultCount, config.maxResultCount);
97
+ const baseQuery = buildIssueQuery(input);
98
+ const startOffset = input.offset ?? 0;
99
+ const collected = [];
100
+ let offset = startOffset;
101
+ let totalCount = 0;
102
+ while (collected.length < wanted) {
103
+ const pageLimit = Math.min(100, wanted - collected.length);
104
+ const page = await http.getJson("/issues.json", {
105
+ ...baseQuery,
106
+ limit: pageLimit,
107
+ offset,
108
+ });
109
+ totalCount = page.total_count;
110
+ collected.push(...page.issues.map(normalizeSummary));
111
+ offset += page.issues.length;
112
+ if (page.issues.length === 0 || offset >= totalCount)
113
+ break;
114
+ }
115
+ const issues = collected.slice(0, wanted);
116
+ return {
117
+ issues,
118
+ totalCount,
119
+ returnedCount: issues.length,
120
+ hasMore: startOffset + issues.length < totalCount,
121
+ };
122
+ }
123
+ export async function getIssue(http, issueId, opts = {}) {
124
+ if (!Number.isInteger(issueId) || issueId <= 0) {
125
+ throw new RedmineError({
126
+ code: "REDMINE_VALIDATION_ERROR",
127
+ message: "issueId must be a positive integer",
128
+ });
129
+ }
130
+ const query = {};
131
+ if (opts.include?.length) {
132
+ query.include = opts.include.join(",");
133
+ }
134
+ try {
135
+ const data = await http.getJson(`/issues/${issueId}.json`, query);
136
+ return normalizeDetail(data.issue);
137
+ }
138
+ catch (err) {
139
+ if (err instanceof RedmineError &&
140
+ err.httpStatus === 404 &&
141
+ err.code === "REDMINE_UNKNOWN_ERROR") {
142
+ throw new RedmineError({
143
+ code: "REDMINE_ISSUE_NOT_FOUND",
144
+ message: `Issue #${issueId} was not found`,
145
+ httpStatus: 404,
146
+ check: ["Confirm the issue id", "Confirm project access"],
147
+ retrySafe: false,
148
+ });
149
+ }
150
+ throw err;
151
+ }
152
+ }
153
+ //# sourceMappingURL=issues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"issues.js","sourceRoot":"","sources":["../src/issues.ts"],"names":[],"mappings":"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"}
package/dist/mask.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function maskSecret(text: string, secret: string): string;
package/dist/mask.js ADDED
@@ -0,0 +1,6 @@
1
+ export function maskSecret(text, secret) {
2
+ if (!secret)
3
+ return text;
4
+ return text.split(secret).join("***");
5
+ }
6
+ //# sourceMappingURL=mask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAc;IACrD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,120 @@
1
+ export type RedmineUser = {
2
+ id: number;
3
+ login: string;
4
+ name: string;
5
+ };
6
+ export type RedmineProject = {
7
+ id: number;
8
+ identifier: string;
9
+ name: string;
10
+ description: string;
11
+ isPublic: boolean;
12
+ parent: {
13
+ id: number;
14
+ name: string;
15
+ } | null;
16
+ status: number;
17
+ };
18
+ export type ListProjectsResult = {
19
+ projects: RedmineProject[];
20
+ totalCount: number;
21
+ returnedCount: number;
22
+ hasMore: boolean;
23
+ };
24
+ export type IssueInclude = "journals" | "attachments" | "relations" | "children" | "allowed_statuses";
25
+ export type NormalizedIssueSummary = {
26
+ id: number;
27
+ subject: string;
28
+ project: {
29
+ id: number;
30
+ name: string;
31
+ } | null;
32
+ tracker: {
33
+ id: number;
34
+ name: string;
35
+ } | null;
36
+ status: {
37
+ id: number;
38
+ name: string;
39
+ } | null;
40
+ priority: {
41
+ id: number;
42
+ name: string;
43
+ } | null;
44
+ assignedTo: {
45
+ id: number;
46
+ name: string;
47
+ } | null;
48
+ updatedOn: string | null;
49
+ createdOn: string | null;
50
+ };
51
+ export type NormalizedIssueDetail = NormalizedIssueSummary & {
52
+ description: string;
53
+ author: {
54
+ id: number;
55
+ name: string;
56
+ } | null;
57
+ startDate: string | null;
58
+ dueDate: string | null;
59
+ doneRatio: number | null;
60
+ estimatedHours: number | null;
61
+ parent: {
62
+ id: number;
63
+ } | null;
64
+ customFields: Array<{
65
+ id: number;
66
+ name: string;
67
+ value: unknown;
68
+ }>;
69
+ journals?: Array<{
70
+ id: number;
71
+ user: {
72
+ id: number;
73
+ name: string;
74
+ } | null;
75
+ notes: string;
76
+ createdOn: string;
77
+ privateNotes: boolean;
78
+ }>;
79
+ attachments?: Array<{
80
+ id: number;
81
+ filename: string;
82
+ filesize: number;
83
+ contentType: string;
84
+ createdOn: string;
85
+ }>;
86
+ relations?: unknown[];
87
+ children?: unknown[];
88
+ allowedStatuses?: Array<{
89
+ id: number;
90
+ name: string;
91
+ }>;
92
+ };
93
+ export type SearchIssuesInput = {
94
+ projectId?: number;
95
+ issueId?: number;
96
+ assignedTo?: string | number;
97
+ status?: "open" | "closed" | "all" | number;
98
+ trackerId?: number;
99
+ priorityId?: number;
100
+ subjectContains?: string;
101
+ createdAfter?: string;
102
+ updatedAfter?: string;
103
+ parentIssueId?: number;
104
+ customFields?: Array<{
105
+ id: number;
106
+ value: string;
107
+ }>;
108
+ sort?: Array<{
109
+ field: string;
110
+ direction: "asc" | "desc";
111
+ }>;
112
+ limit?: number;
113
+ offset?: number;
114
+ };
115
+ export type SearchIssuesResult = {
116
+ issues: NormalizedIssueSummary[];
117
+ totalCount: number;
118
+ returnedCount: number;
119
+ hasMore: boolean;
120
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "redmine-devrelay-client",
3
+ "version": "0.1.0",
4
+ "description": "Redmine REST client used by redmine-devrelay MCP",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "22.13.10",
23
+ "typescript": "5.8.2",
24
+ "vitest": "3.0.9"
25
+ },
26
+ "dependencies": {
27
+ "undici": "7.8.0"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "test": "vitest run",
32
+ "lint": "tsc -p tsconfig.json --noEmit"
33
+ }
34
+ }