import { promises as fs } from 'fs';
import * as path from 'path';
import Handlebars from 'handlebars';
import { LoggingService } from './LoggingService.js';
type CompiledTemplate = ReturnType<typeof Handlebars.compile>;
interface TemplateMap {
context: string;
files: string;
highlights: string;
prompt: string;
}
const DEFAULT_TEMPLATES: TemplateMap = {
context: `{{#if prompt}}
{{{prompt}}}
{{/if}}
{{#if project_notes}}
{{{project_notes}}}
{{/if}}
{{#if user_notes}}
{{{user_notes}}}
{{/if}}
# Repository Content: **{{project_name}}**
> π Generation timestamp: {{timestamp}}
> π Use \`lc-list-modified-files\` to track changes since generation
{{#if sample_requested_files}}
## π File Access Guide
Files in the repository structure are marked as:
- β Full content available
- β Outline available
- β Excluded/not loaded
To retrieve missing files, use the \`lc-get-files\` tool:
\`\`\`json
{
"path": "{{project_root}}",
"files": ["path/to/file"]
}
\`\`\`
{{/if}}
## π Repository Structure
\`\`\`
{{{folder_structure_diagram}}}
\`\`\`
{{#if files}}
## π Current Files
{{> files}}
{{/if}}
{{#if highlights}}
## π Code Outlines
{{> highlights}}
{{/if}}
## π Next Steps
1. Use \`lc-list-modified-files\` to check for changes
2. Request specific files with \`lc-get-files\`
3. Search code with \`search_context\``,
files: `{{#each files}}
### π {{path}}
{{#if metadata.analysis}}
> π Complexity: {{metadata.analysis.complexity}} | π Dependencies: {{metadata.analysis.imports.length}}
{{/if}}
\`\`\`{{language}}
{{{content}}}
\`\`\`
{{/each}}`,
highlights: `{{#each highlights}}
### π {{path}}
{{#if metadata.analysis}}
> π Complexity: {{metadata.analysis.complexity}}
{{/if}}
\`\`\`
{{{outline}}}
\`\`\`
{{/each}}`,
prompt: `# LLM Analysis Guide
## π― Role
Expert code analyst and developer focusing on understanding and improving the codebase.
## π Guidelines
1. π Analyze context before suggesting changes
2. π Consider dependencies and side effects
3. π Follow project's code style
4. β οΈ Preserve existing functionality
5. π Document significant changes
6. π‘οΈ Handle errors gracefully
## π‘ Response Structure
1. Acknowledge files/code being analyzed
2. Explain current implementation
3. Present suggestions clearly
4. Highlight potential impacts
5. Provide rationale for decisions
## π¨ Code Style
- Match existing conventions
- Use consistent formatting
- Choose clear names
- Add helpful comments
## π Security
- Protect sensitive data
- Validate inputs
- Handle errors securely
- Follow best practices
## β‘ Performance
- Consider efficiency
- Note performance impacts
- Suggest optimizations
Remember to balance ideal solutions with practical constraints.`
};
export class TemplateService {
private templates: Map<string, CompiledTemplate>;
private projectRoot: string;
private templatesDir: string;
private logger?: LoggingService;
constructor(projectRoot: string, logger?: LoggingService) {
this.logger = logger;
this.projectRoot = projectRoot;
this.templatesDir = path.join(projectRoot, '.llm-context', 'templates');
this.templates = new Map();
}
public async initialize(): Promise<void> {
// Create templates directory if it doesn't exist
await fs.mkdir(this.templatesDir, { recursive: true });
// Initialize default templates if they don't exist
for (const [name, content] of Object.entries(DEFAULT_TEMPLATES)) {
const templatePath = path.join(this.templatesDir, `${name}.hbs`);
if (!await this.fileExists(templatePath)) {
await fs.writeFile(templatePath, content);
}
}
// Load all templates
await this.loadTemplates();
}
private async fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
private async loadTemplates(): Promise<void> {
// Register partials first
const filesContent = await this.readTemplate('files');
const highlightsContent = await this.readTemplate('highlights');
Handlebars.registerPartial('files', filesContent);
Handlebars.registerPartial('highlights', highlightsContent);
// Compile and cache templates
for (const name of Object.keys(DEFAULT_TEMPLATES)) {
const content = await this.readTemplate(name as keyof TemplateMap);
this.templates.set(name, Handlebars.compile(content));
}
}
private async readTemplate(name: keyof TemplateMap): Promise<string> {
const templatePath = path.join(this.templatesDir, `${name}.hbs`);
try {
return await fs.readFile(templatePath, 'utf8');
} catch (error) {
await this.logger?.warning('Error reading template, using default', {
templateName: name,
templatePath,
error: error instanceof Error ? error.message : String(error),
operation: 'read_template'
});
return DEFAULT_TEMPLATES[name];
}
}
public async render(templateName: string, context: any): Promise<string> {
const template = this.templates.get(templateName);
if (!template) {
throw new Error(`Template '${templateName}' not found`);
}
try {
return template(context);
} catch (error) {
await this.logger?.error('Error rendering template', error as Error, {
templateName,
operation: 'render_template'
});
throw error;
}
}
public async getPrompt(): Promise<string> {
return this.render('prompt', {});
}
}