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