cmte
Version:
Design by Committee™ except it's just you and LLMs
336 lines (296 loc) • 10.5 kB
JavaScript
import path from 'path';
import fs from 'fs';
import yaml from 'js-yaml';
import { loadTaskFromFile } from './frontmatter.js';
import { logger } from '../../utils/logger.js';
import { readFile, fileExists } from '../../utils/fs.js';
/**
* Component types supported by the registry
*/
export let ComponentType = /*#__PURE__*/function (ComponentType) {
ComponentType["TASK"] = "task";
ComponentType["SET"] = "set";
ComponentType["PHASE"] = "phase";
ComponentType["WORKFLOW"] = "workflow";
return ComponentType;
}({});
/**
* File context type for resolving file paths
*/
/**
* Registry for managing and loading components
*/
export class ComponentRegistry {
tasks = new Map();
sets = new Map();
phases = new Map();
workflows = new Map();
workflowPath = null;
constructor(basePath) {
this.basePath = basePath;
}
/**
* Set the current workflow path for workflow-specific templates
*/
setWorkflowPath(workflowPath) {
this.workflowPath = workflowPath;
}
/**
* Clear the current workflow path
*/
clearWorkflowPath() {
this.workflowPath = null;
}
/**
* Get the default file extension for a component type
*/
getDefaultExtension(componentType) {
switch (componentType) {
case ComponentType.TASK:
return '.task.md';
case ComponentType.SET:
return '.set.yaml';
case ComponentType.PHASE:
return '.phase.yaml';
case ComponentType.WORKFLOW:
return '.yaml';
default:
throw new Error(`Unknown component type: ${componentType}`);
}
}
/**
* Get all possible extensions for a component type
*/
getExtensions(type) {
switch (type) {
case ComponentType.WORKFLOW:
return ['.yaml', '.yml'];
case ComponentType.SET:
return ['.set.yaml', '.set.yml'];
case ComponentType.TASK:
return ['.md'];
default:
return ['.yaml', '.yml'];
}
}
/**
* Resolve a component path following the specificity cascade:
* 1. Look in the current workflow directory
*/
async resolveComponentPath(componentType, componentName, extension) {
if (extension) {
const filename = `${componentName}${extension}`;
const componentDir = `${componentType}s`;
// Look in the workflow directory if specified
if (this.workflowPath) {
const templatePath = path.isAbsolute(this.workflowPath)
? path.join(this.workflowPath, componentDir, filename)
: path.join(this.basePath, this.workflowPath, componentDir, filename);
// Use async file check
if (await fileExists(templatePath)) {
return templatePath;
}
}
// TODO: Add fallback to global components directory?
} else {
// Try all possible extensions
for (const ext of this.getExtensions(componentType)) {
// Use await for the recursive async call
const resolvedPath = await this.resolveComponentPath(componentType, componentName, ext);
if (resolvedPath) {
return resolvedPath;
}
}
}
return null;
}
/**
* Load a task from the registry, loading from disk if not found.
* Assumes task files are .md files containing only the prompt content.
*/
async loadTask(taskName) {
// Check cache first
const cachedTask = this.tasks.get(taskName);
if (cachedTask) {
return cachedTask;
}
// Validate task name
if (!taskName || typeof taskName !== 'string' || !taskName.trim()) {
throw new Error('Task name must be a non-empty string');
}
// Find task file (expects .md)
const taskPath = await this.resolveComponentPath(ComponentType.TASK, taskName);
if (!taskPath) {
// If not found, check if it's a built-in task (future enhancement?)
throw new Error(`Task file not found: ${taskName}. Expected location relative to workflow path based on registry configuration.`);
}
// Load task content directly from .md file
let content;
try {
content = await readFile(taskPath);
} catch (error) {
logger.error(`Error reading task file: ${taskPath}`, { error: error.message });
throw new Error(`Failed to read task file: ${taskPath}`);
}
// Validate content
if (!content || typeof content !== 'string' || !content.trim()) {
logger.error(`Invalid or empty task content in file: ${taskPath}`);
throw new Error(`Invalid or empty task content in file: ${taskPath}`);
}
// Create default task metadata since .md files don't contain it
const task = {
name: taskName,
description: `Task loaded from markdown file: ${path.basename(taskPath)}`,
processType: 'prompt-execution', // Assuming standard prompt execution
role: 'service', // Assuming standard service role
requiredInput: [], // Default to no required inputs
requiredOutput: [], // Default to no required outputs
// Add any other essential default fields?
};
// No separate taskMetadata to validate here, we just created it.
// Basic validation of created object
if (!task.name) { throw new Error('Internal Error: Default task metadata missing name.'); }
if (!task.processType) { throw new Error('Internal Error: Default task metadata missing processType.'); }
if (!task.role) { throw new Error('Internal Error: Default task metadata missing role.'); }
if (!Array.isArray(task.requiredInput)) { throw new Error('Internal Error: Default task metadata requiredInput not an array.'); }
if (!Array.isArray(task.requiredOutput)) { throw new Error('Internal Error: Default task metadata requiredOutput not an array.'); }
// Cache task data
const taskData = { task, content };
this.tasks.set(taskName, taskData);
logger.debug(`Loaded and cached task from markdown: ${taskName}`, { path: taskPath });
return taskData;
}
/**
* Load a set from a file and register it
*/
async loadSet(setName) {
// Check if already loaded
// if (this.sets.has(setName)) { // <-- Temporarily disable cache check
// return this.sets.get(setName);
// }
// Resolve path (now async)
const setPath = await this.resolveComponentPath(ComponentType.SET, setName);
if (!setPath) {
throw new Error(`Set template not found: ${setName}`);
}
// Load and parse set
const content = await readFile(setPath);
const set = yaml.load(content);
// Resolve file paths in context if present
if (set.context) {
set.context = this.resolveFilePaths(set.context);
}
// Resolve context in task references
if (set.tasks) {
for (const task of set.tasks) {
if (task.context) {
task.context = this.resolveFilePaths(task.context);
}
}
}
// Resolve context in nested set references
if (set.set) {
for (const nestedSet of set.set) {
if ('context' in nestedSet) {
nestedSet.context = this.resolveFilePaths(nestedSet.context);
}
}
}
this.sets.set(setName, set);
return set;
}
/**
* Load a phase from a file and register it
*/
async loadPhase(phaseName) {
// Check if already loaded
if (this.phases.has(phaseName)) {
return this.phases.get(phaseName);
}
// Resolve path (now async)
const phasePath = await this.resolveComponentPath(ComponentType.PHASE, phaseName);
if (!phasePath) {
throw new Error(`Phase template not found: ${phaseName}`);
}
// Load and parse phase
const content = await readFile(phasePath);
logger.debug('Loading phase YAML', {
content
});
const phase = yaml.load(content);
logger.debug('Loaded phase raw', {
phase
});
// Ensure phase.set is an array
if (!Array.isArray(phase.set)) {
phase.set = phase.set ? [phase.set] : [];
}
logger.debug('Loaded phase after array check', {
phase,
set: phase.set,
isArray: Array.isArray(phase.set)
});
// Resolve file paths in context if present
if (phase.context) {
phase.context = this.resolveFilePaths(phase.context);
}
// Resolve context in set references
for (const setRef of phase.set) {
if ('context' in setRef) {
setRef.context = this.resolveFilePaths(setRef.context);
}
}
this.phases.set(phaseName, phase);
return phase;
}
/**
* Load a workflow from a file and register it
*/
async loadWorkflow(workflowPath) {
// The workflowPath received is now the exact file path.
const workflowFilePath = path.resolve(workflowPath); // Ensure it's absolute
const workflowDir = path.dirname(workflowFilePath);
const workflowName = path.basename(workflowDir); // Use directory name for workflow name/cache key
// Check cache first using the directory-based name
if (this.workflows.has(workflowName)) {
logger.debug('Workflow already loaded from cache', {
workflowName
});
return this.workflows.get(workflowName);
}
// Set internal workflow path to the DIRECTORY for resolving sets/tasks
this.setWorkflowPath(workflowDir);
// Load the exact workflow file path
logger.debug('Loading workflow file', { workflowFilePath });
if (!(await fileExists(workflowFilePath))) {
// Clear the workflow path context if file doesn't exist to avoid issues
this.clearWorkflowPath();
throw new Error(`Workflow file not found: ${workflowFilePath}`);
}
// Load and parse workflow
const content = await readFile(workflowFilePath);
const workflow = yaml.load(content);
// Add name if not present
workflow.name = workflow.name || workflowName;
// Resolve file paths in context if present
if (workflow.context) {
workflow.context = this.resolveFilePaths(workflow.context, path.dirname(workflowFilePath));
}
// Removed loop iterating over workflow.phases as phases are deprecated
/*
// Resolve context in phase references
for (const phaseRef of workflow.phases) {
if ('context' in phaseRef) {
phaseRef.context = this.resolveFilePaths(phaseRef.context, path.dirname(workflowFilePath));
}
}
*/
logger.debug('Loaded workflow', {
name: workflow.name,
// phases: workflow.phases.length, // Removed reference to phases
context: workflow.context ? Object.keys(workflow.context) : []
});
this.workflows.set(workflowName, workflow);
return workflow;
}
}