UNPKG

sead-method-core

Version:

Specification Enforced Agentic Agile Development - A hybrid methodology preventing AI agent drift through catalog-based constraints with comprehensive external asset integration

313 lines (261 loc) 8.67 kB
/** * Resource Locator - Centralized file path resolution and caching * Reduces duplicate file system operations and memory usage */ const path = require('node:path'); const fs = require('fs-extra'); const moduleManager = require('./module-manager'); class ResourceLocator { constructor() { this._pathCache = new Map(); this._globCache = new Map(); this._seadCorePath = null; this._expansionPacksPath = null; } /** * Get the base path for sead-core */ getSeadCorePath() { if (!this._seadCorePath) { this._seadCorePath = path.join(__dirname, '../../../sead-core'); } return this._seadCorePath; } /** * Get the base path for expansion packs */ getExpansionPacksPath() { if (!this._expansionPacksPath) { this._expansionPacksPath = path.join(__dirname, '../../../expansion-packs'); } return this._expansionPacksPath; } /** * Find all files matching a pattern, with caching * @param {string} pattern - Glob pattern * @param {Object} options - Glob options * @returns {Promise<string[]>} Array of matched file paths */ async findFiles(pattern, options = {}) { const cacheKey = `${pattern}:${JSON.stringify(options)}`; if (this._globCache.has(cacheKey)) { return this._globCache.get(cacheKey); } const { glob } = await moduleManager.getModules(['glob']); const files = glob.sync(pattern, options); // Ensure we always return an array const result = Array.isArray(files) ? files : []; // Cache for 5 minutes this._globCache.set(cacheKey, result); setTimeout(() => this._globCache.delete(cacheKey), 5 * 60 * 1000); return result; } /** * Get agent path with caching * @param {string} agentId - Agent identifier * @returns {Promise<string|null>} Path to agent file or null if not found */ async getAgentPath(agentId) { const cacheKey = `agent:${agentId}`; if (this._pathCache.has(cacheKey)) { return this._pathCache.get(cacheKey); } // Check in sead-core let agentPath = path.join(this.getSeadCorePath(), 'agents', `${agentId}.md`); if (await fs.pathExists(agentPath)) { this._pathCache.set(cacheKey, agentPath); return agentPath; } // Check in expansion packs const expansionPacks = await this.getExpansionPacks(); for (const pack of expansionPacks) { agentPath = path.join(pack.path, 'agents', `${agentId}.md`); if (await fs.pathExists(agentPath)) { this._pathCache.set(cacheKey, agentPath); return agentPath; } } return null; } /** * Get available agents with metadata * @returns {Promise<Array>} Array of agent objects */ async getAvailableAgents() { const cacheKey = 'all-agents'; if (this._pathCache.has(cacheKey)) { return this._pathCache.get(cacheKey); } const agents = []; const yaml = require('js-yaml'); const { extractYamlFromAgent } = require('../../lib/yaml-utils'); // Get agents from sead-core const coreDir = this.getSeadCorePath(); const coreAgents = await this.findFiles('agents/*.md', { cwd: coreDir, }); for (const agentFile of coreAgents) { const content = await fs.readFile(path.join(this.getSeadCorePath(), agentFile), 'utf8'); const yamlContent = extractYamlFromAgent(content); if (yamlContent) { try { const metadata = yaml.load(yamlContent); agents.push({ id: path.basename(agentFile, '.md'), name: metadata.agent_name || path.basename(agentFile, '.md'), description: metadata.description || 'No description available', source: 'core', }); } catch { // Skip invalid agents } } } // Cache for 10 minutes this._pathCache.set(cacheKey, agents); setTimeout(() => this._pathCache.delete(cacheKey), 10 * 60 * 1000); return agents; } /** * Get available expansion packs * @returns {Promise<Array>} Array of expansion pack objects */ async getExpansionPacks() { const cacheKey = 'expansion-packs'; if (this._pathCache.has(cacheKey)) { return this._pathCache.get(cacheKey); } const packs = []; const expansionPacksPath = this.getExpansionPacksPath(); if (await fs.pathExists(expansionPacksPath)) { const entries = await fs.readdir(expansionPacksPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const configPath = path.join(expansionPacksPath, entry.name, 'config.yaml'); if (await fs.pathExists(configPath)) { try { const yaml = require('js-yaml'); const config = yaml.load(await fs.readFile(configPath, 'utf8')); packs.push({ id: entry.name, name: config.name || entry.name, version: config.version || '1.0.0', description: config.description || 'No description available', shortTitle: config['short-title'] || config.description || 'No description available', author: config.author || 'Unknown', path: path.join(expansionPacksPath, entry.name), }); } catch { // Skip invalid packs } } } } } // Cache for 10 minutes this._pathCache.set(cacheKey, packs); setTimeout(() => this._pathCache.delete(cacheKey), 10 * 60 * 1000); return packs; } /** * Get team configuration * @param {string} teamId - Team identifier * @returns {Promise<Object|null>} Team configuration or null */ async getTeamConfig(teamId) { const cacheKey = `team:${teamId}`; if (this._pathCache.has(cacheKey)) { return this._pathCache.get(cacheKey); } const teamPath = path.join(this.getSeadCorePath(), 'agent-teams', `${teamId}.yaml`); if (await fs.pathExists(teamPath)) { try { const yaml = require('js-yaml'); const content = await fs.readFile(teamPath, 'utf8'); const config = yaml.load(content); this._pathCache.set(cacheKey, config); return config; } catch { return null; } } return null; } /** * Get resource dependencies for an agent * @param {string} agentId - Agent identifier * @returns {Promise<Object>} Dependencies object */ async getAgentDependencies(agentId) { const cacheKey = `deps:${agentId}`; if (this._pathCache.has(cacheKey)) { return this._pathCache.get(cacheKey); } const agentPath = await this.getAgentPath(agentId); if (!agentPath) { return { all: [], byType: {} }; } const content = await fs.readFile(agentPath, 'utf8'); const { extractYamlFromAgent } = require('../../lib/yaml-utils'); const yamlContent = extractYamlFromAgent(content); if (!yamlContent) { return { all: [], byType: {} }; } try { const yaml = require('js-yaml'); const metadata = yaml.load(yamlContent); const dependencies = metadata.dependencies || {}; // Flatten dependencies const allDeps = []; const byType = {}; for (const [type, deps] of Object.entries(dependencies)) { if (Array.isArray(deps)) { byType[type] = deps; for (const dep of deps) { allDeps.push(`.sead-core/${type}/${dep}`); } } } const result = { all: allDeps, byType }; this._pathCache.set(cacheKey, result); return result; } catch { return { all: [], byType: {} }; } } /** * Clear all caches to free memory */ clearCache() { this._pathCache.clear(); this._globCache.clear(); } /** * Get IDE configuration * @param {string} ideId - IDE identifier * @returns {Promise<Object|null>} IDE configuration or null */ async getIdeConfig(ideId) { const cacheKey = `ide:${ideId}`; if (this._pathCache.has(cacheKey)) { return this._pathCache.get(cacheKey); } const idePath = path.join(this.getSeadCorePath(), 'ide-rules', `${ideId}.yaml`); if (await fs.pathExists(idePath)) { try { const yaml = require('js-yaml'); const content = await fs.readFile(idePath, 'utf8'); const config = yaml.load(content); this._pathCache.set(cacheKey, config); return config; } catch { return null; } } return null; } } // Singleton instance const resourceLocator = new ResourceLocator(); module.exports = resourceLocator;