@assistant-ui/mcp-docs-server 0.1.16 → 0.1.18
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/.docs/organized/code-examples/with-ag-ui.md +149 -155
- package/.docs/organized/code-examples/with-ai-sdk-v5.md +98 -103
- package/.docs/organized/code-examples/with-assistant-transport.md +134 -222
- package/.docs/organized/code-examples/with-cloud.md +127 -134
- package/.docs/organized/code-examples/with-custom-thread-list.md +28 -48
- package/.docs/organized/code-examples/with-external-store.md +149 -154
- package/.docs/organized/code-examples/with-ffmpeg.md +132 -142
- package/.docs/organized/code-examples/with-langgraph.md +234 -228
- package/.docs/organized/code-examples/with-parent-id-grouping.md +149 -154
- package/.docs/organized/code-examples/with-react-hook-form.md +149 -155
- package/.docs/organized/code-examples/{store-example.md → with-store.md} +181 -157
- package/.docs/organized/code-examples/with-tanstack.md +31 -45
- package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +36 -0
- package/.docs/raw/docs/runtimes/custom/local.mdx +31 -8
- package/.docs/raw/docs/ui/Scrollbar.mdx +0 -6
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -0
- package/dist/prepare-docs/code-examples.d.ts +2 -0
- package/dist/prepare-docs/code-examples.d.ts.map +1 -0
- package/dist/prepare-docs/code-examples.js +129 -0
- package/dist/prepare-docs/code-examples.js.map +1 -0
- package/dist/prepare-docs/copy-raw.d.ts +2 -0
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -0
- package/dist/prepare-docs/copy-raw.js +50 -0
- package/dist/prepare-docs/copy-raw.js.map +1 -0
- package/dist/prepare-docs/prepare.d.ts +2 -0
- package/dist/prepare-docs/prepare.d.ts.map +1 -0
- package/dist/prepare-docs/prepare.js +18 -195
- package/dist/prepare-docs/prepare.js.map +1 -0
- package/dist/stdio.d.ts +3 -0
- package/dist/stdio.d.ts.map +1 -0
- package/dist/stdio.js +4 -5
- package/dist/stdio.js.map +1 -0
- package/dist/tools/docs.d.ts +23 -0
- package/dist/tools/docs.d.ts.map +1 -0
- package/dist/tools/docs.js +168 -0
- package/dist/tools/docs.js.map +1 -0
- package/dist/tools/examples.d.ts +23 -0
- package/dist/tools/examples.d.ts.map +1 -0
- package/dist/tools/examples.js +95 -0
- package/dist/tools/examples.js.map +1 -0
- package/dist/tools/tests/test-setup.d.ts +4 -0
- package/dist/tools/tests/test-setup.d.ts.map +1 -0
- package/dist/tools/tests/test-setup.js +36 -0
- package/dist/tools/tests/test-setup.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +20 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mcp-format.d.ts +7 -0
- package/dist/utils/mcp-format.d.ts.map +1 -0
- package/dist/utils/mcp-format.js +11 -0
- package/dist/utils/mcp-format.js.map +1 -0
- package/dist/utils/mdx.d.ts +9 -0
- package/dist/utils/mdx.d.ts.map +1 -0
- package/dist/utils/mdx.js +27 -0
- package/dist/utils/mdx.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +84 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/security.d.ts +2 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +43 -0
- package/dist/utils/security.js.map +1 -0
- package/package.json +37 -19
- package/src/constants.ts +22 -0
- package/src/index.ts +51 -0
- package/src/prepare-docs/code-examples.ts +158 -0
- package/src/prepare-docs/copy-raw.ts +55 -0
- package/src/prepare-docs/prepare.ts +24 -0
- package/src/stdio.ts +7 -0
- package/src/tools/docs.ts +207 -0
- package/src/tools/examples.ts +107 -0
- package/src/tools/tests/docs.test.ts +122 -0
- package/src/tools/tests/examples.test.ts +94 -0
- package/src/tools/tests/integration.test.ts +46 -0
- package/src/tools/tests/json-parsing.test.ts +23 -0
- package/src/tools/tests/mcp-protocol.test.ts +133 -0
- package/src/tools/tests/path-traversal.test.ts +81 -0
- package/src/tools/tests/test-setup.ts +40 -0
- package/src/utils/logger.ts +20 -0
- package/src/utils/mcp-format.ts +12 -0
- package/src/utils/mdx.ts +39 -0
- package/src/utils/paths.ts +114 -0
- package/src/utils/security.ts +52 -0
- package/src/utils/tests/security.test.ts +119 -0
- package/dist/chunk-M2RKUM66.js +0 -38
- package/dist/chunk-NVNFQ5ZO.js +0 -423
package/src/utils/mdx.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
interface MDXContent {
|
|
6
|
+
content: string;
|
|
7
|
+
frontmatter: Record<string, any>;
|
|
8
|
+
excerpt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function readMDXFile(
|
|
12
|
+
filePath: string,
|
|
13
|
+
): Promise<MDXContent | null> {
|
|
14
|
+
try {
|
|
15
|
+
const fileContent = await readFile(filePath, "utf-8");
|
|
16
|
+
const { content, data } = matter(fileContent);
|
|
17
|
+
|
|
18
|
+
const excerptMatch = content.match(/^(.+?)(?:\n\n|$)/);
|
|
19
|
+
const excerpt =
|
|
20
|
+
excerptMatch?.[1] !== undefined
|
|
21
|
+
? excerptMatch[1].replace(/^#+ /, "")
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
content,
|
|
26
|
+
frontmatter: data,
|
|
27
|
+
...(excerpt !== undefined && { excerpt }),
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.error(`Failed to read MDX file: ${filePath}`, error);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatMDXContent(mdxContent: MDXContent): string {
|
|
36
|
+
const { content, frontmatter } = mdxContent;
|
|
37
|
+
|
|
38
|
+
return matter.stringify(content, frontmatter);
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, extname } from "node:path";
|
|
3
|
+
import { DOCS_PATH, MDX_EXTENSION, MD_EXTENSION } from "../constants.js";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
const SIMILARITY_THRESHOLDS = {
|
|
7
|
+
EXACT_MATCH: 1,
|
|
8
|
+
CONTAINS_MATCH: 0.8,
|
|
9
|
+
PARTIAL_MATCH: 0.5,
|
|
10
|
+
MIN_SUGGESTION: 0.3,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export async function listDirContents(dirPath: string): Promise<{
|
|
14
|
+
directories: string[];
|
|
15
|
+
files: string[];
|
|
16
|
+
}> {
|
|
17
|
+
try {
|
|
18
|
+
const items = await readdir(dirPath, { withFileTypes: true });
|
|
19
|
+
|
|
20
|
+
const directories = items
|
|
21
|
+
.filter((item) => item.isDirectory())
|
|
22
|
+
.map((item) => item.name)
|
|
23
|
+
.filter((name) => !name.startsWith("."))
|
|
24
|
+
.sort();
|
|
25
|
+
|
|
26
|
+
const files = items
|
|
27
|
+
.filter(
|
|
28
|
+
(item) =>
|
|
29
|
+
item.isFile() &&
|
|
30
|
+
(extname(item.name) === MDX_EXTENSION ||
|
|
31
|
+
extname(item.name) === MD_EXTENSION),
|
|
32
|
+
)
|
|
33
|
+
.map((item) => item.name)
|
|
34
|
+
.sort();
|
|
35
|
+
|
|
36
|
+
return { directories, files };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error(`Failed to list directory contents: ${dirPath}`, error);
|
|
39
|
+
return { directories: [], files: [] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getAvailablePaths(): Promise<string[]> {
|
|
44
|
+
const paths: string[] = [];
|
|
45
|
+
|
|
46
|
+
async function scanDirectory(
|
|
47
|
+
dir: string,
|
|
48
|
+
prefix: string = "",
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const { directories, files } = await listDirContents(dir);
|
|
51
|
+
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const name = file.replace(MDX_EXTENSION, "").replace(MD_EXTENSION, "");
|
|
54
|
+
paths.push(prefix ? `${prefix}/${name}` : name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const subdir of directories) {
|
|
58
|
+
const subdirPath = join(dir, subdir);
|
|
59
|
+
const newPrefix = prefix ? `${prefix}/${subdir}` : subdir;
|
|
60
|
+
paths.push(newPrefix);
|
|
61
|
+
await scanDirectory(subdirPath, newPrefix);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await scanDirectory(DOCS_PATH);
|
|
66
|
+
return paths.sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function findNearestPaths(
|
|
70
|
+
requestedPath: string,
|
|
71
|
+
availablePaths: string[],
|
|
72
|
+
): string[] {
|
|
73
|
+
const normalizedRequest = requestedPath
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9]/g, "");
|
|
76
|
+
|
|
77
|
+
const scored = availablePaths.map((path) => {
|
|
78
|
+
const normalizedPath = path.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
79
|
+
|
|
80
|
+
if (normalizedPath.includes(normalizedRequest)) {
|
|
81
|
+
return { path, score: SIMILARITY_THRESHOLDS.EXACT_MATCH };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (normalizedRequest.includes(normalizedPath)) {
|
|
85
|
+
return { path, score: SIMILARITY_THRESHOLDS.CONTAINS_MATCH };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const overlap = [...normalizedRequest].filter((char) =>
|
|
89
|
+
normalizedPath.includes(char),
|
|
90
|
+
).length;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
path,
|
|
94
|
+
score:
|
|
95
|
+
(overlap / normalizedRequest.length) *
|
|
96
|
+
SIMILARITY_THRESHOLDS.PARTIAL_MATCH,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return scored
|
|
101
|
+
.filter((item) => item.score > SIMILARITY_THRESHOLDS.MIN_SUGGESTION)
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, 3)
|
|
104
|
+
.map((item) => item.path);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function pathExists(path: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
await stat(path);
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sanitizePath(userPath: string): string {
|
|
4
|
+
if (!userPath || typeof userPath !== "string") {
|
|
5
|
+
throw new Error("Invalid path: Path must be a non-empty string");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Check for traversal patterns before normalization - defense in depth
|
|
9
|
+
if (userPath.includes("../") || userPath.includes("..\\")) {
|
|
10
|
+
throw new Error("Invalid path: Directory traversal attempt detected");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const normalized = path.normalize(userPath);
|
|
14
|
+
|
|
15
|
+
if (path.isAbsolute(normalized)) {
|
|
16
|
+
throw new Error("Invalid path: Absolute paths are not allowed");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const relativePath = path.relative("", normalized);
|
|
20
|
+
|
|
21
|
+
if (relativePath.startsWith("..")) {
|
|
22
|
+
throw new Error("Invalid path: Directory traversal attempt detected");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (relativePath.includes("..")) {
|
|
26
|
+
throw new Error("Invalid path: Path contains invalid traversal sequences");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (relativePath.includes("\0")) {
|
|
30
|
+
throw new Error("Invalid path: Path contains null bytes");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.platform !== "win32") {
|
|
34
|
+
if (normalized.includes("\\")) {
|
|
35
|
+
throw new Error("Invalid path: Backslashes not allowed");
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
if (normalized.includes(":") || normalized.startsWith("\\\\")) {
|
|
39
|
+
throw new Error("Invalid path: Path contains invalid Windows characters");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const segments = relativePath.split(path.sep);
|
|
44
|
+
for (const segment of segments) {
|
|
45
|
+
if (segment.startsWith(".") && segment !== ".") {
|
|
46
|
+
throw new Error("Invalid path: Hidden files are not allowed");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Always return Unix-style paths for consistency across platforms
|
|
51
|
+
return relativePath.replace(/\\/g, "/");
|
|
52
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { sanitizePath } from "../security.js";
|
|
3
|
+
|
|
4
|
+
describe("sanitizePath", () => {
|
|
5
|
+
describe("should reject directory traversal attempts", () => {
|
|
6
|
+
const maliciousPaths = [
|
|
7
|
+
"../../../etc/passwd",
|
|
8
|
+
"..\\..\\windows\\system32",
|
|
9
|
+
"/etc/passwd",
|
|
10
|
+
"/absolute/path",
|
|
11
|
+
"docs/../../../etc/passwd",
|
|
12
|
+
"valid/../../../../../../etc/passwd",
|
|
13
|
+
"../",
|
|
14
|
+
"..",
|
|
15
|
+
".../.../",
|
|
16
|
+
"foo/../../bar",
|
|
17
|
+
"./../../etc/passwd",
|
|
18
|
+
"path/with/../../../traversal",
|
|
19
|
+
"path/to/\0/null",
|
|
20
|
+
"path\\to\\..\\..\\file",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
maliciousPaths.push("C:\\Windows\\System32", "\\\\server\\share");
|
|
25
|
+
} else {
|
|
26
|
+
maliciousPaths.push("C:\\Windows\\System32", "\\\\server\\share");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
maliciousPaths.forEach((path) => {
|
|
30
|
+
it(`should reject: ${path}`, () => {
|
|
31
|
+
expect(() => sanitizePath(path)).toThrow();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("should reject invalid inputs", () => {
|
|
37
|
+
it("should reject empty string", () => {
|
|
38
|
+
expect(() => sanitizePath("")).toThrow(
|
|
39
|
+
"Invalid path: Path must be a non-empty string",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should reject null", () => {
|
|
44
|
+
expect(() => sanitizePath(null as any)).toThrow(
|
|
45
|
+
"Invalid path: Path must be a non-empty string",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should reject undefined", () => {
|
|
50
|
+
expect(() => sanitizePath(undefined as any)).toThrow(
|
|
51
|
+
"Invalid path: Path must be a non-empty string",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should reject numbers", () => {
|
|
56
|
+
expect(() => sanitizePath(123 as any)).toThrow(
|
|
57
|
+
"Invalid path: Path must be a non-empty string",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("should reject hidden files", () => {
|
|
63
|
+
const hiddenPaths = [
|
|
64
|
+
".hidden",
|
|
65
|
+
".git/config",
|
|
66
|
+
"docs/.secret",
|
|
67
|
+
"path/to/.env",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
hiddenPaths.forEach((path) => {
|
|
71
|
+
it(`should reject: ${path}`, () => {
|
|
72
|
+
expect(() => sanitizePath(path)).toThrow(
|
|
73
|
+
"Invalid path: Hidden files are not allowed",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("should allow valid paths", () => {
|
|
80
|
+
const validPaths = [
|
|
81
|
+
{ input: "getting-started", expected: "getting-started" },
|
|
82
|
+
{
|
|
83
|
+
input: "api-reference/primitives/Thread",
|
|
84
|
+
expected: "api-reference/primitives/Thread",
|
|
85
|
+
},
|
|
86
|
+
{ input: "guides/tools", expected: "guides/tools" },
|
|
87
|
+
{ input: "docs/index", expected: "docs/index" },
|
|
88
|
+
{ input: "examples/with-ai-sdk", expected: "examples/with-ai-sdk" },
|
|
89
|
+
{ input: "./current-dir-file", expected: "current-dir-file" },
|
|
90
|
+
{
|
|
91
|
+
input: "deeply/nested/path/to/file",
|
|
92
|
+
expected: "deeply/nested/path/to/file",
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
validPaths.forEach(({ input, expected }) => {
|
|
97
|
+
it(`should allow: ${input} => ${expected}`, () => {
|
|
98
|
+
expect(sanitizePath(input)).toBe(expected);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (process.platform === "win32") {
|
|
104
|
+
describe("Windows-specific tests", () => {
|
|
105
|
+
it("should reject Windows drive letters", () => {
|
|
106
|
+
expect(() => sanitizePath("C:")).toThrow(
|
|
107
|
+
"Invalid path: Path contains invalid Windows characters",
|
|
108
|
+
);
|
|
109
|
+
expect(() => sanitizePath("D:\\file")).toThrow();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should reject UNC paths", () => {
|
|
113
|
+
expect(() => sanitizePath("\\\\server\\share")).toThrow(
|
|
114
|
+
"Invalid path: Absolute paths are not allowed",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
package/dist/chunk-M2RKUM66.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'url';
|
|
2
|
-
import { dirname, join } from 'path';
|
|
3
|
-
|
|
4
|
-
// src/constants.ts
|
|
5
|
-
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
var ROOT_DIR = join(__dirname$1, "../../../");
|
|
7
|
-
var PACKAGE_DIR = join(__dirname$1, "../");
|
|
8
|
-
var EXAMPLES_PATH = join(ROOT_DIR, "examples");
|
|
9
|
-
var DOCS_BASE = join(PACKAGE_DIR, ".docs");
|
|
10
|
-
var DOCS_PATH = join(DOCS_BASE, "raw/docs");
|
|
11
|
-
join(DOCS_BASE, "raw/blog");
|
|
12
|
-
var CODE_EXAMPLES_PATH = join(DOCS_BASE, "organized/code-examples");
|
|
13
|
-
var MDX_EXTENSION = ".mdx";
|
|
14
|
-
var MD_EXTENSION = ".md";
|
|
15
|
-
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
16
|
-
var IS_PREPARE_MODE = process.env.PREPARE === "true";
|
|
17
|
-
|
|
18
|
-
// src/utils/logger.ts
|
|
19
|
-
var logger = {
|
|
20
|
-
debug: (message, ...args) => {
|
|
21
|
-
if (process.env.DEBUG) {
|
|
22
|
-
console.debug(`[DEBUG] ${message}`, ...args);
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
info: (message, ...args) => {
|
|
26
|
-
if (process.env.PREPARE === "true") {
|
|
27
|
-
console.log(`[INFO] ${message}`, ...args);
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
error: (message, ...args) => {
|
|
31
|
-
console.error(`[ERROR] ${message}`, ...args);
|
|
32
|
-
},
|
|
33
|
-
warn: (message, ...args) => {
|
|
34
|
-
console.warn(`[WARN] ${message}`, ...args);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export { CODE_EXAMPLES_PATH, DOCS_PATH, EXAMPLES_PATH, IS_PREPARE_MODE, MAX_FILE_SIZE, MDX_EXTENSION, MD_EXTENSION, ROOT_DIR, logger };
|