Skip to content

Commit a0c3dff

Browse files
committed
feat: add support for nested automatic discovery
1 parent b12fd17 commit a0c3dff

File tree

8 files changed

+712
-217
lines changed

8 files changed

+712
-217
lines changed

examples/mcpinput-pattern.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

src/loaders/promptLoader.ts

Lines changed: 30 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,43 @@
1-
import { PromptProtocol } from "../prompts/BasePrompt.js";
2-
import { join, dirname } from "path";
3-
import { promises as fs } from "fs";
4-
import { logger } from "../core/Logger.js";
1+
import { PromptProtocol } from '../prompts/BasePrompt.js';
2+
import { join, dirname } from 'path';
3+
import { promises as fs } from 'fs';
4+
import { logger } from '../core/Logger.js';
5+
import { discoverFilesRecursively, hasValidFiles } from '../utils/fileDiscovery.js';
56

67
export class PromptLoader {
78
private readonly PROMPTS_DIR: string;
8-
private readonly EXCLUDED_FILES = ["BasePrompt.js", "*.test.js", "*.spec.js"];
9+
private readonly EXCLUDED_FILES = ['BasePrompt.js', '*.test.js', '*.spec.js'];
910

1011
constructor(basePath?: string) {
11-
const mainModulePath = basePath || process.argv[1];
12-
this.PROMPTS_DIR = join(dirname(mainModulePath), "prompts");
13-
logger.debug(
14-
`Initialized PromptLoader with directory: ${this.PROMPTS_DIR}`
15-
);
12+
if (basePath) {
13+
// If basePath is provided, it should be the directory containing the prompts folder
14+
this.PROMPTS_DIR = join(basePath, 'prompts');
15+
} else {
16+
// For backwards compatibility, use the old behavior with process.argv[1]
17+
const mainModulePath = process.argv[1];
18+
this.PROMPTS_DIR = join(dirname(mainModulePath), 'prompts');
19+
}
20+
logger.debug(`Initialized PromptLoader with directory: ${this.PROMPTS_DIR}`);
1621
}
1722

1823
async hasPrompts(): Promise<boolean> {
1924
try {
20-
const stats = await fs.stat(this.PROMPTS_DIR);
21-
if (!stats.isDirectory()) {
22-
logger.debug("Prompts path exists but is not a directory");
23-
return false;
24-
}
25-
26-
const files = await fs.readdir(this.PROMPTS_DIR);
27-
const hasValidFiles = files.some((file) => this.isPromptFile(file));
28-
logger.debug(`Prompts directory has valid files: ${hasValidFiles}`);
29-
return hasValidFiles;
25+
return await hasValidFiles(this.PROMPTS_DIR, {
26+
extensions: ['.js'],
27+
excludePatterns: this.EXCLUDED_FILES,
28+
});
3029
} catch (error) {
3130
logger.debug(`No prompts directory found: ${(error as Error).message}`);
3231
return false;
3332
}
3433
}
3534

36-
private isPromptFile(file: string): boolean {
37-
if (!file.endsWith(".js")) return false;
38-
const isExcluded = this.EXCLUDED_FILES.some((pattern) => {
39-
if (pattern.includes("*")) {
40-
const regex = new RegExp(pattern.replace("*", ".*"));
41-
return regex.test(file);
42-
}
43-
return file === pattern;
44-
});
45-
46-
logger.debug(
47-
`Checking file ${file}: ${isExcluded ? "excluded" : "included"}`
48-
);
49-
return !isExcluded;
50-
}
51-
5235
private validatePrompt(prompt: any): prompt is PromptProtocol {
5336
const isValid = Boolean(
5437
prompt &&
55-
typeof prompt.name === "string" &&
38+
typeof prompt.name === 'string' &&
5639
prompt.promptDefinition &&
57-
typeof prompt.getMessages === "function"
40+
typeof prompt.getMessages === 'function'
5841
);
5942

6043
if (isValid) {
@@ -70,29 +53,21 @@ export class PromptLoader {
7053
try {
7154
logger.debug(`Attempting to load prompts from: ${this.PROMPTS_DIR}`);
7255

73-
let stats;
74-
try {
75-
stats = await fs.stat(this.PROMPTS_DIR);
76-
} catch (error) {
77-
logger.debug(`No prompts directory found: ${(error as Error).message}`);
78-
return [];
79-
}
56+
const promptFiles = await discoverFilesRecursively(this.PROMPTS_DIR, {
57+
extensions: ['.js'],
58+
excludePatterns: this.EXCLUDED_FILES,
59+
});
8060

81-
if (!stats.isDirectory()) {
82-
logger.error(`Path is not a directory: ${this.PROMPTS_DIR}`);
61+
if (promptFiles.length === 0) {
62+
logger.debug('No prompt files found');
8363
return [];
8464
}
8565

86-
const files = await fs.readdir(this.PROMPTS_DIR);
87-
logger.debug(`Found files in directory: ${files.join(", ")}`);
66+
logger.debug(`Found prompt files: ${promptFiles.join(', ')}`);
8867

8968
const prompts: PromptProtocol[] = [];
9069

91-
for (const file of files) {
92-
if (!this.isPromptFile(file)) {
93-
continue;
94-
}
95-
70+
for (const file of promptFiles) {
9671
try {
9772
const fullPath = join(this.PROMPTS_DIR, file);
9873
logger.debug(`Attempting to load prompt from: ${fullPath}`);
@@ -115,9 +90,7 @@ export class PromptLoader {
11590
}
11691

11792
logger.debug(
118-
`Successfully loaded ${prompts.length} prompts: ${prompts
119-
.map((p) => p.name)
120-
.join(", ")}`
93+
`Successfully loaded ${prompts.length} prompts: ${prompts.map((p) => p.name).join(', ')}`
12194
);
12295
return prompts;
12396
} catch (error) {

src/loaders/resourceLoader.ts

Lines changed: 31 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,44 @@
1-
import { ResourceProtocol } from "../resources/BaseResource.js";
2-
import { join, dirname } from "path";
3-
import { promises as fs } from "fs";
4-
import { logger } from "../core/Logger.js";
1+
import { ResourceProtocol } from '../resources/BaseResource.js';
2+
import { join, dirname } from 'path';
3+
import { promises as fs } from 'fs';
4+
import { logger } from '../core/Logger.js';
5+
import { discoverFilesRecursively, hasValidFiles } from '../utils/fileDiscovery.js';
56

67
export class ResourceLoader {
78
private readonly RESOURCES_DIR: string;
8-
private readonly EXCLUDED_FILES = [
9-
"BaseResource.js",
10-
"*.test.js",
11-
"*.spec.js",
12-
];
9+
private readonly EXCLUDED_FILES = ['BaseResource.js', '*.test.js', '*.spec.js'];
1310

1411
constructor(basePath?: string) {
15-
const mainModulePath = basePath || process.argv[1];
16-
this.RESOURCES_DIR = join(dirname(mainModulePath), "resources");
17-
logger.debug(
18-
`Initialized ResourceLoader with directory: ${this.RESOURCES_DIR}`
19-
);
12+
if (basePath) {
13+
// If basePath is provided, it should be the directory containing the resources folder
14+
this.RESOURCES_DIR = join(basePath, 'resources');
15+
} else {
16+
// For backwards compatibility, use the old behavior with process.argv[1]
17+
const mainModulePath = process.argv[1];
18+
this.RESOURCES_DIR = join(dirname(mainModulePath), 'resources');
19+
}
20+
logger.debug(`Initialized ResourceLoader with directory: ${this.RESOURCES_DIR}`);
2021
}
2122

2223
async hasResources(): Promise<boolean> {
2324
try {
24-
const stats = await fs.stat(this.RESOURCES_DIR);
25-
if (!stats.isDirectory()) {
26-
logger.debug("Resources path exists but is not a directory");
27-
return false;
28-
}
29-
30-
const files = await fs.readdir(this.RESOURCES_DIR);
31-
const hasValidFiles = files.some((file) => this.isResourceFile(file));
32-
logger.debug(`Resources directory has valid files: ${hasValidFiles}`);
33-
return hasValidFiles;
25+
return await hasValidFiles(this.RESOURCES_DIR, {
26+
extensions: ['.js'],
27+
excludePatterns: this.EXCLUDED_FILES,
28+
});
3429
} catch (error) {
3530
logger.debug(`No resources directory found: ${(error as Error).message}`);
3631
return false;
3732
}
3833
}
3934

40-
private isResourceFile(file: string): boolean {
41-
if (!file.endsWith(".js")) return false;
42-
const isExcluded = this.EXCLUDED_FILES.some((pattern) => {
43-
if (pattern.includes("*")) {
44-
const regex = new RegExp(pattern.replace("*", ".*"));
45-
return regex.test(file);
46-
}
47-
return file === pattern;
48-
});
49-
50-
logger.debug(
51-
`Checking file ${file}: ${isExcluded ? "excluded" : "included"}`
52-
);
53-
return !isExcluded;
54-
}
55-
5635
private validateResource(resource: any): resource is ResourceProtocol {
5736
const isValid = Boolean(
5837
resource &&
59-
typeof resource.uri === "string" &&
60-
typeof resource.name === "string" &&
38+
typeof resource.uri === 'string' &&
39+
typeof resource.name === 'string' &&
6140
resource.resourceDefinition &&
62-
typeof resource.read === "function"
41+
typeof resource.read === 'function'
6342
);
6443

6544
if (isValid) {
@@ -75,29 +54,21 @@ export class ResourceLoader {
7554
try {
7655
logger.debug(`Attempting to load resources from: ${this.RESOURCES_DIR}`);
7756

78-
let stats;
79-
try {
80-
stats = await fs.stat(this.RESOURCES_DIR);
81-
} catch (error) {
82-
logger.debug(`No resources directory found: ${(error as Error).message}`);
83-
return [];
84-
}
57+
const resourceFiles = await discoverFilesRecursively(this.RESOURCES_DIR, {
58+
extensions: ['.js'],
59+
excludePatterns: this.EXCLUDED_FILES,
60+
});
8561

86-
if (!stats.isDirectory()) {
87-
logger.error(`Path is not a directory: ${this.RESOURCES_DIR}`);
62+
if (resourceFiles.length === 0) {
63+
logger.debug('No resource files found');
8864
return [];
8965
}
9066

91-
const files = await fs.readdir(this.RESOURCES_DIR);
92-
logger.debug(`Found files in directory: ${files.join(", ")}`);
67+
logger.debug(`Found resource files: ${resourceFiles.join(', ')}`);
9368

9469
const resources: ResourceProtocol[] = [];
9570

96-
for (const file of files) {
97-
if (!this.isResourceFile(file)) {
98-
continue;
99-
}
100-
71+
for (const file of resourceFiles) {
10172
try {
10273
const fullPath = join(this.RESOURCES_DIR, file);
10374
logger.debug(`Attempting to load resource from: ${fullPath}`);
@@ -120,9 +91,7 @@ export class ResourceLoader {
12091
}
12192

12293
logger.debug(
123-
`Successfully loaded ${resources.length} resources: ${resources
124-
.map((r) => r.name)
125-
.join(", ")}`
94+
`Successfully loaded ${resources.length} resources: ${resources.map((r) => r.name).join(', ')}`
12695
);
12796
return resources;
12897
} catch (error) {

0 commit comments

Comments
 (0)