export interface GithubTemplate { name: string; path: string; content: string; download_url: string; variables?: string[]; variableMetadata?: Record; } export class TemplateService { private static REPO_NAME = 'agent-notes'; /** * Fetches template files from a GitHub repository. * @param profile The GitHub username/org (used for token fallback) * @param token The GitHub Personal Access Token (optional but recommended) * @param owner The owner of the repo to fetch from * @param repo The name of the repo to fetch from * @param path The subfolder path to fetch from */ async fetchTemplates( profile: string, token?: string, owner?: string, repo?: string, path?: string, branch?: string ): Promise { if (!profile) return []; const targetOwner = owner || profile; const targetRepo = repo || TemplateService.REPO_NAME; const targetPath = path || ''; const endpoint = `/api/github/contents?owner=${targetOwner}&repo=${targetRepo}&path=${targetPath}${token ? '&token=' + token : ''}&profile=${profile}${branch ? '&ref=' + branch : ''}`; try { const response = await fetch(endpoint); if (!response.ok) { if (response.status === 404) { console.warn(`Template repository not found for ${profile}`); return []; } throw new Error(`GitHub API error: ${response.status}`); } const files = await response.json(); console.log(`[TemplateService] Received ${Array.isArray(files) ? files.length : 'non-array'} entries from GitHub`); if (!Array.isArray(files)) { console.warn("[TemplateService] GitHub response is not an array:", files); return []; } // Filter for markdown and text files const mdFiles = files.filter((file: any) => file.type === 'file' && (file.name.endsWith('.md') || file.name.endsWith('.txt')) ); console.log(`[TemplateService] Found ${mdFiles.length} markdown/text files`); // Fetch content for each file const templates = await Promise.all(mdFiles.map(async (file: any) => { const rawUrl = `/api/github/raw?url=${encodeURIComponent(file.download_url)}${token ? '&token=' + token : ''}&profile=${profile}${branch ? '&ref=' + branch : ''}`; const contentResponse = await fetch(rawUrl); const content = await contentResponse.text(); // Parse for new JSON structure const { cleanContent, variables, variableMetadata } = this.parseTemplateJson(content); return { name: file.name.replace('.md', '').replace('.txt', ''), path: file.path, content: cleanContent, download_url: file.download_url, variables, variableMetadata }; })); return templates; } catch (error) { console.error("Failed to fetch GitHub templates:", error); return []; } } /** * Detects and parses a JSON block at the start of the template. * Format: * { * "variables": { "KEY": "Description" } * } */ private parseTemplateJson(content: string): { cleanContent: string, variables?: string[], variableMetadata?: Record } { // Look for JSON at the very beginning of the string const jsonMatch = content.match(/^\s*(\{[\s\S]*?\})(\s*[\r\n]+|$)/); if (jsonMatch) { try { const json = JSON.parse(jsonMatch[1]); if (json.variables) { const variables = Object.keys(json.variables); const variableMetadata = json.variables; // Content starts after the JSON block const cleanContent = content.substring(jsonMatch[0].length).trim(); return { cleanContent, variables, variableMetadata }; } } catch (e) { // Not valid JSON or doesn't follow structure, return original console.warn("Found potential JSON header but failed to parse it as template metadata.", e); } } return { cleanContent: content, variables: undefined }; } }