/** * Context Loader - Loads project context files for agent prompts * * Provides a shared utility to load context files from .automaker/context/ * and format them as system prompt content. Used by both auto-mode-service * and agent-service to ensure all agents are aware of project context. * * Context files contain project-specific rules, conventions, and guidelines * that agents must follow when working on the project. */ import path from 'path'; import fs from 'fs/promises'; /** * Metadata structure for context files * Stored in {projectPath}/.automaker/context/context-metadata.json */ export interface ContextMetadata { files: Record; } /** * Individual context file with metadata */ export interface ContextFileInfo { name: string; path: string; content: string; description?: string; } /** * Result of loading context files */ export interface ContextFilesResult { files: ContextFileInfo[]; formattedPrompt: string; } /** * Options for loading context files */ export interface LoadContextFilesOptions { /** Project path to load context from */ projectPath: string; /** Optional custom secure fs module (for dependency injection) */ fsModule?: { access: (path: string) => Promise; readdir: (path: string) => Promise; readFile: (path: string, encoding: string) => Promise; }; } /** * Get the context directory path for a project */ function getContextDir(projectPath: string): string { return path.join(projectPath, '.automaker', 'context'); } /** * Load context metadata from the metadata file */ async function loadContextMetadata( contextDir: string, fsModule: typeof fs ): Promise { const metadataPath = path.join(contextDir, 'context-metadata.json'); try { const content = await fsModule.readFile(metadataPath, 'utf-8'); return JSON.parse(content); } catch { // Metadata file doesn't exist yet - that's fine return { files: {} }; } } /** * Format a single context file entry for the prompt */ function formatContextFileEntry(file: ContextFileInfo): string { const header = `## ${file.name}`; const pathInfo = `**Path:** \`${file.path}\``; let descriptionInfo = ''; if (file.description) { descriptionInfo = `\n**Purpose:** ${file.description}`; } return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`; } /** * Build the formatted system prompt from context files */ function buildContextPrompt(files: ContextFileInfo[]): string { if (files.length === 0) { return ''; } const formattedFiles = files.map(formatContextFileEntry); return `# Project Context Files The following context files provide project-specific rules, conventions, and guidelines. Each file serves a specific purpose - use the description to understand when to reference it. If you need more details about a context file, you can read the full file at the path provided. **IMPORTANT**: You MUST follow the rules and conventions specified in these files. - Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`) - Follow ALL coding conventions, commit message formats, and architectural patterns specified - Reference these rules before running ANY shell commands or making commits --- ${formattedFiles.join('\n\n---\n\n')} --- **REMINDER**: Before taking any action, verify you are following the conventions specified above. `; } /** * Load context files from a project's .automaker/context/ directory * * This function loads all .md and .txt files from the context directory, * along with their metadata (descriptions), and formats them into a * system prompt that can be prepended to agent prompts. * * @param options - Configuration options * @returns Promise resolving to context files and formatted prompt * * @example * ```typescript * const { formattedPrompt, files } = await loadContextFiles({ * projectPath: '/path/to/project' * }); * * // Use as system prompt * const executeOptions = { * prompt: userPrompt, * systemPrompt: formattedPrompt, * }; * ``` */ export async function loadContextFiles( options: LoadContextFilesOptions ): Promise { const { projectPath, fsModule = fs } = options; const contextDir = path.resolve(getContextDir(projectPath)); try { // Check if directory exists await fsModule.access(contextDir); // Read directory contents const allFiles = await fsModule.readdir(contextDir); // Filter for text-based context files (case-insensitive for cross-platform) const textFiles = allFiles.filter((f) => { const lower = f.toLowerCase(); return (lower.endsWith('.md') || lower.endsWith('.txt')) && f !== 'context-metadata.json'; }); if (textFiles.length === 0) { return { files: [], formattedPrompt: '' }; } // Load metadata for descriptions const metadata = await loadContextMetadata(contextDir, fsModule as typeof fs); // Load each file with its content and metadata const files: ContextFileInfo[] = []; for (const fileName of textFiles) { const filePath = path.join(contextDir, fileName); try { const content = await fsModule.readFile(filePath, 'utf-8'); files.push({ name: fileName, path: filePath, content, description: metadata.files[fileName]?.description, }); } catch (error) { console.warn(`[ContextLoader] Failed to read context file ${fileName}:`, error); } } const formattedPrompt = buildContextPrompt(files); console.log( `[ContextLoader] Loaded ${files.length} context file(s): ${files.map((f) => f.name).join(', ')}` ); return { files, formattedPrompt }; } catch { // Context directory doesn't exist or is inaccessible - this is fine return { files: [], formattedPrompt: '' }; } } /** * Get a summary of available context files (names and descriptions only) * Useful for informing the agent about what context is available without * loading full content. */ export async function getContextFilesSummary( options: LoadContextFilesOptions ): Promise> { const { projectPath, fsModule = fs } = options; const contextDir = path.resolve(getContextDir(projectPath)); try { await fsModule.access(contextDir); const allFiles = await fsModule.readdir(contextDir); const textFiles = allFiles.filter((f) => { const lower = f.toLowerCase(); return (lower.endsWith('.md') || lower.endsWith('.txt')) && f !== 'context-metadata.json'; }); if (textFiles.length === 0) { return []; } const metadata = await loadContextMetadata(contextDir, fsModule as typeof fs); return textFiles.map((fileName) => ({ name: fileName, path: path.join(contextDir, fileName), description: metadata.files[fileName]?.description, })); } catch { return []; } }