Skip to content

Commit bc0598f

Browse files
committed
feat(glob): add regex-based glob pattern matching
Implement more robust glob pattern matching by converting patterns to regular expressions. This supports *, ?, and [] syntax for more flexible file matching. refactor(chained): support loading prompts from individual files Extend chained prompt loading functionality to handle both directories and individual .md files, improving flexibility in prompt organization.
1 parent e20cd7f commit bc0598f

File tree

2 files changed

+107
-12
lines changed

2 files changed

+107
-12
lines changed

src/agents/runner/chained.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,34 @@ function filenameToName(filename: string): string {
8787
return filename.replace(/\.md$/i, '');
8888
}
8989

90+
/**
91+
* Load a single chained prompt from a file
92+
*/
93+
async function loadPromptFromFile(
94+
filePath: string,
95+
projectRoot: string
96+
): Promise<ChainedPrompt | null> {
97+
const absolutePath = path.isAbsolute(filePath)
98+
? filePath
99+
: path.resolve(projectRoot, filePath);
100+
101+
try {
102+
const rawContent = await fs.readFile(absolutePath, 'utf-8');
103+
const { frontmatter, body } = parseFrontmatter(rawContent);
104+
const filename = path.basename(absolutePath);
105+
106+
debug(`Loaded chained prompt from file: ${absolutePath}`);
107+
return {
108+
name: frontmatter.name || filenameToName(filename),
109+
label: frontmatter.description || filenameToLabel(filename),
110+
content: body.trim(),
111+
};
112+
} catch (err) {
113+
debug(`Failed to read chained prompt file ${absolutePath}: ${err}`);
114+
return null;
115+
}
116+
}
117+
90118
/**
91119
* Load chained prompts from a single folder
92120
*/
@@ -144,6 +172,37 @@ async function loadPromptsFromFolder(
144172
return prompts;
145173
}
146174

175+
/**
176+
* Load chained prompts from a path (file or folder)
177+
*/
178+
async function loadPromptsFromPath(
179+
inputPath: string,
180+
projectRoot: string
181+
): Promise<ChainedPrompt[]> {
182+
const absolutePath = path.isAbsolute(inputPath)
183+
? inputPath
184+
: path.resolve(projectRoot, inputPath);
185+
186+
try {
187+
const stat = await fs.stat(absolutePath);
188+
189+
if (stat.isFile() && absolutePath.endsWith('.md')) {
190+
// Single file
191+
const prompt = await loadPromptFromFile(absolutePath, projectRoot);
192+
return prompt ? [prompt] : [];
193+
} else if (stat.isDirectory()) {
194+
// Folder
195+
return loadPromptsFromFolder(absolutePath, projectRoot);
196+
} else {
197+
debug(`chainedPromptsPath is neither a .md file nor directory: ${absolutePath}`);
198+
return [];
199+
}
200+
} catch {
201+
debug(`chainedPromptsPath does not exist: ${absolutePath}`);
202+
return [];
203+
}
204+
}
205+
147206
/**
148207
* Type guard for conditional path entry
149208
*/
@@ -168,11 +227,11 @@ function getPath(entry: ChainedPathEntry): string {
168227
}
169228

170229
/**
171-
* Load chained prompts from one or more folders
230+
* Load chained prompts from one or more paths (files or folders)
172231
* Files are sorted by filename within each folder (01-first.md, 02-second.md, etc.)
173-
* When multiple folders are provided, prompts are loaded in folder order
232+
* When multiple paths are provided, prompts are loaded in path order
174233
*
175-
* @param chainedPromptsPath - Path or array of paths to folder(s) containing .md files
234+
* @param chainedPromptsPath - Path or array of paths to file(s) or folder(s) containing .md files
176235
* @param projectRoot - Project root for resolving relative paths
177236
* @param selectedConditions - User-selected conditions for filtering conditional paths
178237
* @returns Array of ChainedPrompt objects sorted by filename within each folder
@@ -193,8 +252,8 @@ export async function loadChainedPrompts(
193252
continue;
194253
}
195254

196-
const folderPath = getPath(entry);
197-
const prompts = await loadPromptsFromFolder(folderPath, projectRoot);
255+
const inputPath = getPath(entry);
256+
const prompts = await loadPromptsFromPath(inputPath, projectRoot);
198257
allPrompts.push(...prompts);
199258
}
200259

src/shared/prompts/content/glob.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,46 @@ export function isGlobPattern(filePath: string): boolean {
99
return filePath.includes('*') || filePath.includes('?') || filePath.includes('[');
1010
}
1111

12+
/**
13+
* Converts a glob pattern to a RegExp
14+
* Supports: * (any chars), ? (single char), [abc] (char class)
15+
* Examples:
16+
* *.md -> matches file.md
17+
* product-brief-*.md -> matches product-brief-2025-12-11.md
18+
* file?.txt -> matches file1.txt
19+
*/
20+
function globToRegex(pattern: string): RegExp {
21+
let regex = '';
22+
let i = 0;
23+
24+
while (i < pattern.length) {
25+
const char = pattern[i];
26+
27+
if (char === '*') {
28+
regex += '.*';
29+
} else if (char === '?') {
30+
regex += '.';
31+
} else if (char === '[') {
32+
// Find closing bracket
33+
const closeIdx = pattern.indexOf(']', i);
34+
if (closeIdx !== -1) {
35+
regex += pattern.slice(i, closeIdx + 1);
36+
i = closeIdx;
37+
} else {
38+
regex += '\\[';
39+
}
40+
} else if ('.^$+{}()|\\'.includes(char)) {
41+
// Escape regex special chars
42+
regex += '\\' + char;
43+
} else {
44+
regex += char;
45+
}
46+
i++;
47+
}
48+
49+
return new RegExp(`^${regex}$`);
50+
}
51+
1252
/**
1353
* Matches files against a glob pattern
1454
* Returns an array of absolute file paths sorted alphabetically
@@ -28,6 +68,7 @@ export async function matchGlobPattern(
2868
try {
2969
const files = await readdir(directory);
3070
const matchedFiles: string[] = [];
71+
const regex = globToRegex(filePattern);
3172

3273
for (const file of files) {
3374
const fullPath = path.join(directory, file);
@@ -40,13 +81,8 @@ export async function matchGlobPattern(
4081
continue;
4182
}
4283

43-
// Simple pattern matching for *.ext patterns
44-
if (filePattern.startsWith('*')) {
45-
const extension = filePattern.substring(1); // e.g., ".md"
46-
if (file.endsWith(extension)) {
47-
matchedFiles.push(fullPath);
48-
}
49-
} else if (filePattern === file) {
84+
// Match against glob pattern regex
85+
if (regex.test(file)) {
5086
matchedFiles.push(fullPath);
5187
}
5288
}

0 commit comments

Comments
 (0)