| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import path from 'path'; |
| import { secureFs } from '@automaker/platform'; |
| import { |
| getMemoryDir, |
| parseFrontmatter, |
| initializeMemoryFolder, |
| extractTerms, |
| calculateUsageScore, |
| countMatches, |
| incrementUsageStat, |
| type MemoryFsModule, |
| type MemoryMetadata, |
| } from './memory-loader.js'; |
|
|
| |
| |
| |
| |
| export interface ContextMetadata { |
| files: Record<string, { description: string }>; |
| } |
|
|
| |
| |
| |
| export interface ContextFileInfo { |
| name: string; |
| path: string; |
| content: string; |
| description?: string; |
| } |
|
|
| |
| |
| |
| export interface MemoryFileInfo { |
| name: string; |
| path: string; |
| content: string; |
| category: string; |
| } |
|
|
| |
| |
| |
| export interface ContextFilesResult { |
| files: ContextFileInfo[]; |
| memoryFiles: MemoryFileInfo[]; |
| formattedPrompt: string; |
| } |
|
|
| |
| |
| |
| |
| |
| export interface ContextFsModule { |
| access: (path: string) => Promise<void>; |
| readdir: (path: string) => Promise<string[]>; |
| readFile: (path: string, encoding?: BufferEncoding) => Promise<string | Buffer>; |
| |
| writeFile: (path: string, content: string) => Promise<void>; |
| mkdir: (path: string, options?: { recursive?: boolean }) => Promise<string | undefined>; |
| appendFile: (path: string, content: string) => Promise<void>; |
| } |
|
|
| |
| |
| |
| export interface TaskContext { |
| |
| title: string; |
| |
| description?: string; |
| } |
|
|
| |
| |
| |
| export interface LoadContextFilesOptions { |
| |
| projectPath: string; |
| |
| fsModule?: ContextFsModule; |
| |
| includeContextFiles?: boolean; |
| |
| includeMemory?: boolean; |
| |
| initializeMemory?: boolean; |
| |
| taskContext?: TaskContext; |
| |
| maxMemoryFiles?: number; |
| } |
|
|
| |
| |
| |
| function getContextDir(projectPath: string): string { |
| return path.join(projectPath, '.automaker', 'context'); |
| } |
|
|
| |
| |
| |
| async function loadContextMetadata( |
| contextDir: string, |
| fsModule: ContextFsModule |
| ): Promise<ContextMetadata> { |
| const metadataPath = path.join(contextDir, 'context-metadata.json'); |
| try { |
| const content = await fsModule.readFile(metadataPath, 'utf-8'); |
| return JSON.parse(content as string); |
| } catch { |
| |
| return { files: {} }; |
| } |
| } |
|
|
| |
| |
| |
| 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}`; |
| } |
|
|
| |
| |
| |
| 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. |
| `; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function loadContextFiles( |
| options: LoadContextFilesOptions |
| ): Promise<ContextFilesResult> { |
| const { |
| projectPath, |
| fsModule = secureFs, |
| includeContextFiles = true, |
| includeMemory = true, |
| initializeMemory = true, |
| taskContext, |
| maxMemoryFiles = 5, |
| } = options; |
| const contextDir = path.resolve(getContextDir(projectPath)); |
|
|
| const files: ContextFileInfo[] = []; |
| const memoryFiles: MemoryFileInfo[] = []; |
|
|
| |
| if (includeContextFiles) { |
| 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) { |
| |
| const metadata = await loadContextMetadata(contextDir, fsModule); |
|
|
| |
| 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: content as string, |
| description: metadata.files[fileName]?.description, |
| }); |
| } catch (error) { |
| console.warn(`[ContextLoader] Failed to read context file ${fileName}:`, error); |
| } |
| } |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| if (includeMemory) { |
| const memoryDir = getMemoryDir(projectPath); |
|
|
| |
| if (initializeMemory) { |
| try { |
| await initializeMemoryFolder(projectPath, fsModule as MemoryFsModule); |
| } catch { |
| |
| } |
| } |
|
|
| try { |
| await fsModule.access(memoryDir); |
| const allMemoryFiles = await fsModule.readdir(memoryDir); |
|
|
| |
| const memoryMdFiles = allMemoryFiles.filter((f) => { |
| const lower = f.toLowerCase(); |
| return lower.endsWith('.md') && lower !== '_index.md'; |
| }); |
|
|
| |
| const taskTerms = taskContext |
| ? extractTerms(taskContext.title + ' ' + (taskContext.description || '')) |
| : []; |
|
|
| |
| const scoredFiles: Array<{ |
| fileName: string; |
| filePath: string; |
| body: string; |
| metadata: MemoryMetadata; |
| score: number; |
| }> = []; |
|
|
| for (const fileName of memoryMdFiles) { |
| const filePath = path.join(memoryDir, fileName); |
| try { |
| const rawContent = await fsModule.readFile(filePath, 'utf-8'); |
| const { metadata, body } = parseFrontmatter(rawContent as string); |
|
|
| |
| if (!body.trim()) continue; |
|
|
| |
| let score = 0; |
|
|
| if (taskTerms.length > 0) { |
| |
| const tagScore = countMatches(metadata.tags, taskTerms) * 3; |
| const relevantToScore = countMatches(metadata.relevantTo, taskTerms) * 2; |
| const summaryTerms = extractTerms(metadata.summary); |
| const summaryScore = countMatches(summaryTerms, taskTerms); |
| |
| |
| const categoryTerms = fileName |
| .replace('.md', '') |
| .split(/[-_]/) |
| .filter((t) => t.length > 2); |
| const categoryScore = countMatches(categoryTerms, taskTerms) * 4; |
|
|
| |
| const usageScore = calculateUsageScore(metadata.usageStats); |
|
|
| score = |
| (tagScore + relevantToScore + summaryScore + categoryScore) * |
| metadata.importance * |
| usageScore; |
| } else { |
| |
| score = metadata.importance; |
| } |
|
|
| scoredFiles.push({ fileName, filePath, body, metadata, score }); |
| } catch (error) { |
| console.warn(`[ContextLoader] Failed to read memory file ${fileName}:`, error); |
| } |
| } |
|
|
| |
| scoredFiles.sort((a, b) => b.score - a.score); |
|
|
| |
| |
| |
| |
| const selectedFiles = new Set<string>(); |
|
|
| |
| if (maxMemoryFiles > 0) { |
| |
| const gotchasFile = scoredFiles.find((f) => f.fileName === 'gotchas.md'); |
| if (gotchasFile) { |
| selectedFiles.add('gotchas.md'); |
| } |
|
|
| |
| for (const file of scoredFiles) { |
| if (file.metadata.importance >= 0.9 && selectedFiles.size < maxMemoryFiles) { |
| selectedFiles.add(file.fileName); |
| } |
| } |
|
|
| |
| if (taskTerms.length > 0) { |
| for (const file of scoredFiles) { |
| if (file.score > 0 && selectedFiles.size < maxMemoryFiles) { |
| selectedFiles.add(file.fileName); |
| } |
| } |
| } |
| } |
|
|
| |
| for (const file of scoredFiles) { |
| if (selectedFiles.has(file.fileName)) { |
| memoryFiles.push({ |
| name: file.fileName, |
| path: file.filePath, |
| content: file.body, |
| category: file.fileName.replace('.md', ''), |
| }); |
|
|
| |
| |
| try { |
| await incrementUsageStat(file.filePath, 'loaded', fsModule as MemoryFsModule); |
| } catch { |
| |
| } |
| } |
| } |
|
|
| if (memoryFiles.length > 0) { |
| const selectedNames = memoryFiles.map((f) => f.category).join(', '); |
| console.log(`[ContextLoader] Selected memory files: ${selectedNames}`); |
| } |
| } catch { |
| |
| } |
| } |
|
|
| |
| const contextPrompt = buildContextPrompt(files); |
| const memoryPrompt = buildMemoryPrompt(memoryFiles); |
| const formattedPrompt = [contextPrompt, memoryPrompt].filter(Boolean).join('\n\n'); |
|
|
| const loadedItems = []; |
| if (files.length > 0) { |
| loadedItems.push(`${files.length} context file(s)`); |
| } |
| if (memoryFiles.length > 0) { |
| loadedItems.push(`${memoryFiles.length} memory file(s)`); |
| } |
| if (loadedItems.length > 0) { |
| console.log(`[ContextLoader] Loaded ${loadedItems.join(' and ')}`); |
| } |
|
|
| return { files, memoryFiles, formattedPrompt }; |
| } |
|
|
| |
| |
| |
| function buildMemoryPrompt(memoryFiles: MemoryFileInfo[]): string { |
| if (memoryFiles.length === 0) { |
| return ''; |
| } |
|
|
| const sections = memoryFiles.map((file) => { |
| return `## ${file.category.toUpperCase()} |
| |
| ${file.content}`; |
| }); |
|
|
| return `# Project Memory |
| |
| The following learnings and decisions from previous work are available. |
| **IMPORTANT**: Review these carefully before making changes that could conflict with past decisions. |
| |
| --- |
| |
| ${sections.join('\n\n---\n\n')} |
| |
| --- |
| `; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getContextFilesSummary( |
| options: LoadContextFilesOptions |
| ): Promise<Array<{ name: string; path: string; description?: string }>> { |
| const { projectPath, fsModule = secureFs } = 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); |
|
|
| return textFiles.map((fileName) => ({ |
| name: fileName, |
| path: path.join(contextDir, fileName), |
| description: metadata.files[fileName]?.description, |
| })); |
| } catch { |
| return []; |
| } |
| } |
|
|