mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
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:
@@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user