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.
- package/dist/client.d.ts +18 -0
- package/dist/client.js +73 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +109 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.js +168 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/issues.d.ts +8 -0
- package/dist/issues.js +153 -0
- package/dist/issues.js.map +1 -0
- package/dist/mask.d.ts +1 -0
- package/dist/mask.js +6 -0
- package/dist/mask.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +34 -0
package/dist/client.d.ts
ADDED
|
@@ -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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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAe3C,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,0BAA0B,CAAC,CAAC,CAAC;AAEjF,sEAAsE;AACtE,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;AAExE,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,CAAC,GAAG,8CAA8C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpE,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC;IACtB,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAClB,GAAsB,EACtB,IAAY,EACZ,QAAgB;IAEhB,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACrD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EAAE,WAAW,IAAI,KAAK,GAAG,EAAE;YAClC,KAAK,EAAE,CAAC,OAAO,IAAI,uBAAuB,CAAC;SAC5C,CAAC,CAAC;IACL,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EAAE,8CAA8C;YACvD,KAAK,EAAE,CAAC,oBAAoB,EAAE,wBAAwB,CAAC;SACxD,CAAC,CAAC;IACL,CAAC;IAED,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EAAE,wBAAwB,MAAM,EAAE;YACzC,KAAK,EAAE,CAAC,sDAAsD,CAAC;SAChE,CAAC,CAAC;IACL,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC9D,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EAAE,2BAA2B,MAAM,CAAC,QAAQ,EAAE;SACtD,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EAAE,oCAAoC;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,MAAM,YAAY,GAAG,GAAG,CAAC,qBAAqB;QAC5C,EAAE,KAAK,CAAC,GAAG,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAClC,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,IAAI,YAAY,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EAAE,QAAQ,IAAI,kCAAkC;YACvD,KAAK,EAAE,CAAC,mDAAmD,CAAC;SAC7D,CAAC,CAAC;IACL,CAAC;IAED,MAAM,qBAAqB,GACzB,MAAM,CAAC,QAAQ,KAAK,OAAO;QAC3B,CAAC,CAAC,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC;QAC9B,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC3D,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,0BAA0B;YAChC,OAAO,EACL,4EAA4E;YAC9E,KAAK,EAAE;gBACL,iBAAiB;gBACjB,gEAAgE;aACjE;SACF,CAAC,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE1C,OAAO;QACL,OAAO;QACP,MAAM;QACN,gBAAgB,EAAE,WAAW,CAAC,GAAG,EAAE,4BAA4B,EAAE,IAAI,CAAC;QACtE,gBAAgB,EAAE,WAAW,CAAC,GAAG,EAAE,4BAA4B,EAAE,KAAK,CAAC;QACvE,cAAc,EAAE,WAAW,CAAC,GAAG,EAAE,0BAA0B,EAAE,GAAG,CAAC;QACjE,QAAQ,EAAE,GAAG,CAAC,iBAAiB,IAAI,MAAM;QACzC,UAAU,EAAE,GAAG,CAAC,oBAAoB,IAAI,SAAS;QACjD,YAAY;QACZ,gBAAgB,EAAE,GAAG,CAAC,0BAA0B;YAC9C,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC;YACxC,CAAC,CAAC,SAAS;QACb,SAAS,EAAE,mBAAmB;KAC/B,CAAC;AACJ,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/issues.d.ts
ADDED
|
@@ -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
package/dist/mask.js.map
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|