refactor: reduce code duplication in agent-discovery.ts

Addresses PR feedback to reduce duplicated code in scanAgentsDirectory
by introducing an FsAdapter interface that abstracts the differences
between systemPaths (user directory) and secureFs (project directory).

Changes:
- Extract parseAgentContent helper for parsing agent file content
- Add FsAdapter interface with exists, readdir, and readFile methods
- Create createSystemPathAdapter for user-level paths
- Create createSecureFsAdapter for project-level paths
- Refactor scanAgentsDirectory to use a single loop with the adapter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2026-01-08 23:02:41 +01:00
parent 50da1b401c
commit e649c4ced5

View File

@@ -24,7 +24,7 @@ export interface FilesystemAgent {
} }
/** /**
* Parse agent .md file frontmatter and content * Parse agent content string into AgentDefinition
* Format: * Format:
* --- * ---
* name: agent-name # Optional * name: agent-name # Optional
@@ -34,61 +34,128 @@ export interface FilesystemAgent {
* --- * ---
* System prompt content here... * System prompt content here...
*/ */
async function parseAgentFile( function parseAgentContent(content: string, filePath: string): AgentDefinition | null {
filePath: string, // Extract frontmatter
isSystemPath: boolean const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
): Promise<AgentDefinition | null> { if (!frontmatterMatch) {
try { logger.warn(`Invalid agent file format (missing frontmatter): ${filePath}`);
const content = isSystemPath return null;
? ((await systemPaths.systemPathReadFile(filePath, 'utf-8')) as string) }
: ((await secureFs.readFile(filePath, 'utf-8')) as string);
// Extract frontmatter const [, frontmatter, prompt] = frontmatterMatch;
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
logger.warn(`Invalid agent file format (missing frontmatter): ${filePath}`);
return null;
}
const [, frontmatter, prompt] = frontmatterMatch; // Parse description (required)
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
if (!description) {
logger.warn(`Missing description in agent file: ${filePath}`);
return null;
}
// Parse description (required) // Parse tools (optional) - supports both comma-separated and space-separated
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim(); const toolsMatch = frontmatter.match(/tools:\s*(.+)/);
if (!description) { const tools = toolsMatch
logger.warn(`Missing description in agent file: ${filePath}`); ? toolsMatch[1]
return null; .split(/[,\s]+/) // Split by comma or whitespace
} .map((t) => t.trim())
.filter((t) => t && t !== '')
: undefined;
// Parse tools (optional) - supports both comma-separated and space-separated // Parse model (optional) - validate against allowed values
const toolsMatch = frontmatter.match(/tools:\s*(.+)/); const modelMatch = frontmatter.match(/model:\s*(\w+)/);
const tools = toolsMatch const modelValue = modelMatch?.[1]?.trim();
? toolsMatch[1] const validModels = ['sonnet', 'opus', 'haiku', 'inherit'] as const;
.split(/[,\s]+/) // Split by comma or whitespace const model =
.map((t) => t.trim()) modelValue && validModels.includes(modelValue as (typeof validModels)[number])
.filter((t) => t && t !== '') ? (modelValue as 'sonnet' | 'opus' | 'haiku' | 'inherit')
: undefined; : undefined;
// Parse model (optional) - validate against allowed values if (modelValue && !model) {
const modelMatch = frontmatter.match(/model:\s*(\w+)/); logger.warn(
const modelValue = modelMatch?.[1]?.trim(); `Invalid model "${modelValue}" in agent file: ${filePath}. Expected one of: ${validModels.join(', ')}`
const validModels = ['sonnet', 'opus', 'haiku', 'inherit'] as const; );
const model = }
modelValue && validModels.includes(modelValue as (typeof validModels)[number])
? (modelValue as 'sonnet' | 'opus' | 'haiku' | 'inherit')
: undefined;
if (modelValue && !model) { return {
logger.warn( description,
`Invalid model "${modelValue}" in agent file: ${filePath}. Expected one of: ${validModels.join(', ')}` prompt: prompt.trim(),
); tools,
} model,
};
}
return { /**
description, * Directory entry with type information
prompt: prompt.trim(), */
tools, interface DirEntry {
model, name: string;
}; isFile: boolean;
isDirectory: boolean;
}
/**
* Filesystem adapter interface for abstracting systemPaths vs secureFs
*/
interface FsAdapter {
exists: (filePath: string) => Promise<boolean>;
readdir: (dirPath: string) => Promise<DirEntry[]>;
readFile: (filePath: string) => Promise<string>;
}
/**
* Create a filesystem adapter for system paths (user directory)
*/
function createSystemPathAdapter(): FsAdapter {
return {
exists: (filePath) => Promise.resolve(systemPaths.systemPathExists(filePath)),
readdir: async (dirPath) => {
const entryNames = await systemPaths.systemPathReaddir(dirPath);
const entries: DirEntry[] = [];
for (const name of entryNames) {
const stat = await systemPaths.systemPathStat(path.join(dirPath, name));
entries.push({
name,
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
});
}
return entries;
},
readFile: (filePath) => systemPaths.systemPathReadFile(filePath, 'utf-8') as Promise<string>,
};
}
/**
* Create a filesystem adapter for project paths (secureFs)
*/
function createSecureFsAdapter(): FsAdapter {
return {
exists: (filePath) =>
secureFs
.access(filePath)
.then(() => true)
.catch(() => false),
readdir: async (dirPath) => {
const entries = await secureFs.readdir(dirPath, { withFileTypes: true });
return entries.map((entry) => ({
name: entry.name,
isFile: entry.isFile(),
isDirectory: entry.isDirectory(),
}));
},
readFile: (filePath) => secureFs.readFile(filePath, 'utf-8') as Promise<string>,
};
}
/**
* Parse agent file using the provided filesystem adapter
*/
async function parseAgentFileWithAdapter(
filePath: string,
fsAdapter: FsAdapter
): Promise<AgentDefinition | null> {
try {
const content = await fsAdapter.readFile(filePath);
return parseAgentContent(content, filePath);
} catch (error) { } catch (error) {
logger.error(`Failed to parse agent file: ${filePath}`, error); logger.error(`Failed to parse agent file: ${filePath}`, error);
return null; return null;
@@ -106,103 +173,50 @@ async function scanAgentsDirectory(
source: 'user' | 'project' source: 'user' | 'project'
): Promise<FilesystemAgent[]> { ): Promise<FilesystemAgent[]> {
const agents: FilesystemAgent[] = []; const agents: FilesystemAgent[] = [];
const isSystemPath = source === 'user'; // User directories use systemPaths const fsAdapter = source === 'user' ? createSystemPathAdapter() : createSecureFsAdapter();
try { try {
// Check if directory exists // Check if directory exists
const exists = isSystemPath const exists = await fsAdapter.exists(baseDir);
? await systemPaths.systemPathExists(baseDir)
: await secureFs
.access(baseDir)
.then(() => true)
.catch(() => false);
if (!exists) { if (!exists) {
logger.debug(`Directory does not exist: ${baseDir}`); logger.debug(`Directory does not exist: ${baseDir}`);
return agents; return agents;
} }
// Read all entries in the directory // Read all entries in the directory
if (isSystemPath) { const entries = await fsAdapter.readdir(baseDir);
// For system paths (user directory)
const entryNames = await systemPaths.systemPathReaddir(baseDir);
for (const entryName of entryNames) {
const entryPath = path.join(baseDir, entryName);
const stat = await systemPaths.systemPathStat(entryPath);
// Check for flat .md file format (agent-name.md) for (const entry of entries) {
if (stat.isFile() && entryName.endsWith('.md')) { // Check for flat .md file format (agent-name.md)
const agentName = entryName.slice(0, -3); // Remove .md extension if (entry.isFile && entry.name.endsWith('.md')) {
const definition = await parseAgentFile(entryPath, true); const agentName = entry.name.slice(0, -3); // Remove .md extension
if (definition) { const agentFilePath = path.join(baseDir, entry.name);
agents.push({ const definition = await parseAgentFileWithAdapter(agentFilePath, fsAdapter);
name: agentName, if (definition) {
definition, agents.push({
source, name: agentName,
filePath: entryPath, definition,
}); source,
logger.debug(`Discovered ${source} agent (flat): ${agentName}`); filePath: agentFilePath,
} });
} logger.debug(`Discovered ${source} agent (flat): ${agentName}`);
// Check for subdirectory format (agent-name/AGENT.md)
else if (stat.isDirectory()) {
const agentFilePath = path.join(entryPath, 'AGENT.md');
const agentFileExists = await systemPaths.systemPathExists(agentFilePath);
if (agentFileExists) {
const definition = await parseAgentFile(agentFilePath, true);
if (definition) {
agents.push({
name: entryName,
definition,
source,
filePath: agentFilePath,
});
logger.debug(`Discovered ${source} agent (subdirectory): ${entryName}`);
}
}
} }
} }
} else { // Check for subdirectory format (agent-name/AGENT.md)
// For project paths (use secureFs) else if (entry.isDirectory) {
const entries = await secureFs.readdir(baseDir, { withFileTypes: true }); const agentFilePath = path.join(baseDir, entry.name, 'AGENT.md');
for (const entry of entries) { const agentFileExists = await fsAdapter.exists(agentFilePath);
// Check for flat .md file format (agent-name.md)
if (entry.isFile() && entry.name.endsWith('.md')) { if (agentFileExists) {
const agentName = entry.name.slice(0, -3); // Remove .md extension const definition = await parseAgentFileWithAdapter(agentFilePath, fsAdapter);
const agentFilePath = path.join(baseDir, entry.name);
const definition = await parseAgentFile(agentFilePath, false);
if (definition) { if (definition) {
agents.push({ agents.push({
name: agentName, name: entry.name,
definition, definition,
source, source,
filePath: agentFilePath, filePath: agentFilePath,
}); });
logger.debug(`Discovered ${source} agent (flat): ${agentName}`); logger.debug(`Discovered ${source} agent (subdirectory): ${entry.name}`);
}
}
// Check for subdirectory format (agent-name/AGENT.md)
else if (entry.isDirectory()) {
const agentDir = path.join(baseDir, entry.name);
const agentFilePath = path.join(agentDir, 'AGENT.md');
const agentFileExists = await secureFs
.access(agentFilePath)
.then(() => true)
.catch(() => false);
if (agentFileExists) {
const definition = await parseAgentFile(agentFilePath, false);
if (definition) {
agents.push({
name: entry.name,
definition,
source,
filePath: agentFilePath,
});
logger.debug(`Discovered ${source} agent (subdirectory): ${entry.name}`);
}
} }
} }
} }