diff --git a/client/index.html b/client/index.html index 98b53ec..7367af8 100644 --- a/client/index.html +++ b/client/index.html @@ -43,6 +43,16 @@

Agent.md Processing

+

Generate AGENTS.MD from GitHub Repository

+
+ + + +
+ +

Source code of the node selected

diff --git a/client/js/app.js b/client/js/app.js index ed34baa..b021893 100644 --- a/client/js/app.js +++ b/client/js/app.js @@ -167,6 +167,31 @@ document.getElementById('btnTestConnection').addEventListener('click', function vscode.postMessage({ command: 'testConnection', baseUrl: baseUrl, model: model }); }); +// Generate AGENTS.MD button handler +document.getElementById('btnGenerateAgentMd').addEventListener('click', function () { + var repoUrl = document.getElementById('githubRepoUrl').value || ''; + var statusEl = document.getElementById('generateAgentMdStatus'); + if (!repoUrl.trim()) { + if (statusEl) { + statusEl.textContent = 'Please enter a GitHub repository URL.'; + statusEl.className = 'error'; + } + return; + } + if (statusEl) { + statusEl.textContent = 'Generating...'; + statusEl.className = 'processing'; + } + var baseUrl = document.getElementById('baseUrlInput').value || ''; + var model = document.getElementById('modelInput').value || ''; + vscode.postMessage({ + command: 'generateAgentMd', + repoUrl: repoUrl, + baseUrl: baseUrl, + model: model + }); +}); + /* ============================================================= 3. General helpers ============================================================= */ @@ -351,6 +376,26 @@ window.addEventListener('message', function (event) { } } + if (message.command === 'generateAgentMdResult') { + var genStatusEl = document.getElementById('generateAgentMdStatus'); + var genTextarea = document.getElementById('generatedAgentMd'); + if (message.success) { + if (genTextarea && message.content) { + genTextarea.value = message.content; + genTextarea.style.display = 'block'; + } + if (genStatusEl) { + genStatusEl.textContent = '✓ AGENTS.MD generated successfully!'; + genStatusEl.className = 'success'; + } + } else { + if (genStatusEl) { + genStatusEl.textContent = '✗ ' + (message.message || 'Generation failed'); + genStatusEl.className = 'error'; + } + } + } + if (message.command === 'leafSimilarities') { renderSimilarities(message.node, message.similarities); renderLizardMetrics(message.node); diff --git a/media/webview.html b/media/webview.html index ee85afe..18ca2b0 100644 --- a/media/webview.html +++ b/media/webview.html @@ -168,6 +168,15 @@ #agentMdProcessStatus.processing { color: #856404; background: #fff3cd; } #agentMdProcessStatus.success { color: #155724; background: #d4edda; } #agentMdProcessStatus.error { color: #721c24; background: #f8d7da; } + + #generateAgentMdStatus { + font-size: 13px; + padding: 4px 8px; + border-radius: 4px; + } + #generateAgentMdStatus.processing { color: #856404; background: #fff3cd; } + #generateAgentMdStatus.success { color: #155724; background: #d4edda; } + #generateAgentMdStatus.error { color: #721c24; background: #f8d7da; } @@ -201,6 +210,16 @@

Agent.md Processing

+

Generate AGENTS.MD from GitHub Repository

+
+ + + +
+ +

Source code of the node selected


@@ -398,6 +417,31 @@

Prompts and outputs

vscode.postMessage({ command: 'testConnection', baseUrl, model }); }); + // Generate AGENTS.MD button handler + document.getElementById('btnGenerateAgentMd').addEventListener('click', () => { + const repoUrl = document.getElementById('githubRepoUrl').value || ''; + const statusEl = document.getElementById('generateAgentMdStatus'); + if (!repoUrl.trim()) { + if (statusEl) { + statusEl.textContent = 'Please enter a GitHub repository URL.'; + statusEl.className = 'error'; + } + return; + } + if (statusEl) { + statusEl.textContent = 'Generating...'; + statusEl.className = 'processing'; + } + const baseUrl = document.getElementById('baseUrlInput').value || ''; + const model = document.getElementById('modelInput').value || ''; + vscode.postMessage({ + command: 'generateAgentMd', + repoUrl, + baseUrl, + model + }); + }); + function sendPrompt() { const prompt = document.getElementById('prompt').value; const code = document.getElementById('code').value; @@ -556,6 +600,26 @@

Prompts and outputs

} } + if (message.command === 'generateAgentMdResult') { + const genStatusEl = document.getElementById('generateAgentMdStatus'); + const genTextarea = document.getElementById('generatedAgentMd'); + if (message.success) { + if (genTextarea && message.content) { + genTextarea.value = message.content; + genTextarea.style.display = 'block'; + } + if (genStatusEl) { + genStatusEl.textContent = '✓ AGENTS.MD generated successfully!'; + genStatusEl.className = 'success'; + } + } else { + if (genStatusEl) { + genStatusEl.textContent = '✗ ' + (message.message || 'Generation failed'); + genStatusEl.className = 'error'; + } + } + } + if (message.command === 'leafSimilarities') { // message.node: comlete node selected // message.similarities: [{ id, similarity }, ...] diff --git a/package-lock.json b/package-lock.json index 341b424..478a295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -419,7 +419,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -819,7 +818,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1037,7 +1035,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -1482,7 +1479,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3428,7 +3424,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3911,7 +3906,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4003,7 +3997,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz", "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -4052,7 +4045,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/src/extension.ts b/src/extension.ts index e63b603..5d5ae08 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -655,6 +655,61 @@ export function activate(context: vscode.ExtensionContext) { return; } + if (message.command === 'generateAgentMd') { + const repoUrl = message.repoUrl || ''; + baseUrl = message.baseUrl || baseUrl; + LLMmodel = message.model || LLMmodel; + + bonsaiLog('Generating AGENTS.MD for repo:', repoUrl); + panel.webview.postMessage({ command: 'loading', text: 'Parsing GitHub repository URL...' }); + + try { + const parsed = parseGitHubUrl(repoUrl); + if (!parsed) { + throw new Error('Invalid GitHub URL. Expected format: https://github.com/owner/repo'); + } + + // Verify LLM connection first + bonsaiLog('Verifying LLM connection before generating AGENTS.MD'); + if (!/^[\w.-]+(:\d+)?(\/[\w./]*)?$/.test(baseUrl)) { + throw new Error('Invalid LLM URL format. Expected format: host:port/path (e.g., localhost:1234/v1)'); + } + const modelsRes = await fetch(`http://${baseUrl}/models`, { + method: 'GET', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer lm-studio' }, + signal: AbortSignal.timeout(5000) + }); + if (!modelsRes.ok) { + throw new Error(`LLM server returned ${modelsRes.status}: ${modelsRes.statusText}`); + } + + // Fetch repo content from GitHub + panel.webview.postMessage({ command: 'loading', text: 'Fetching repository from GitHub...' }); + const repoContent = await fetchGitHubRepoContent(parsed.owner, parsed.repo); + bonsaiLog('Repository content fetched. Length:', repoContent.length); + + // Generate AGENTS.MD via LLM + panel.webview.postMessage({ command: 'loading', text: 'Generating AGENTS.MD with LLM...' }); + const { content: agentsMd, reasoning } = await generateAgentMdFromRepo(repoContent); + bonsaiLog('AGENTS.MD generated successfully. Length:', agentsMd.length); + + panel.webview.postMessage({ + command: 'generateAgentMdResult', + success: true, + content: agentsMd, + reasoning + }); + } catch (err: any) { + bonsaiLog('AGENTS.MD generation failed:', err?.message || err); + panel.webview.postMessage({ + command: 'generateAgentMdResult', + success: false, + message: err?.message || 'Generation failed' + }); + } + return; + } + if (message.command === 'generate') { const selectedNodeIdForPrompt = selectedNodeId; let code = message.code; @@ -1127,6 +1182,233 @@ Validate your output against the RULES before responding. throw new Error("Failed to generate code from Agent.md after multiple attempts"); } +// --------------------------------------------------------------------------- +// GitHub helpers – fetch repository structure and key files +// --------------------------------------------------------------------------- + +/** Parse owner and repo name from a GitHub URL */ +function parseGitHubUrl(url: string): { owner: string; repo: string } | null { + // Support formats: https://github.com/owner/repo, github.com/owner/repo, owner/repo + const match = url.trim().match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#?]+)|^([^/\s]+)\/([^/\s#?]+)$/); + if (match) { + const owner = match[1] || match[3]; + const repo = (match[2] || match[4]).replace(/\.git$/, ''); + return { owner, repo }; + } + return null; +} + +/** Fetch the repository tree and key file contents from GitHub API */ +async function fetchGitHubRepoContent(owner: string, repo: string): Promise { + const headers: Record = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'BonsAIDE' + }; + + // 1. Fetch repo metadata + const repoRes = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { headers, signal: AbortSignal.timeout(15000) }); + if (!repoRes.ok) { + throw new Error(`GitHub API error: ${repoRes.status} ${repoRes.statusText}`); + } + const repoData: any = await repoRes.json(); + const defaultBranch = repoData.default_branch || 'main'; + const description = repoData.description || ''; + const language = repoData.language || ''; + + // 2. Fetch directory tree (recursive) + const treeRes = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(defaultBranch)}?recursive=1`, { headers, signal: AbortSignal.timeout(15000) }); + if (!treeRes.ok) { + throw new Error(`GitHub tree API error: ${treeRes.status} ${treeRes.statusText}`); + } + const treeData: any = await treeRes.json(); + const allFiles: string[] = (treeData.tree || []) + .filter((item: any) => item.type === 'blob') + .map((item: any) => item.path); + + // 3. Identify key files to fetch content for + const keyPatterns = [ + /^readme\.md$/i, + /^package\.json$/i, + /^pyproject\.toml$/i, + /^cargo\.toml$/i, + /^go\.mod$/i, + /^pom\.xml$/i, + /^build\.gradle$/i, + /^makefile$/i, + /^dockerfile$/i, + /^requirements\.txt$/i, + /^setup\.py$/i, + /^tsconfig\.json$/i, + /^agents\.md$/i, + ]; + + // Also grab top-level source entry points (e.g. src/main.*, src/index.*, app.*, main.*) + const entryPatterns = [ + /^(?:src\/)?(?:main|index|app|server|extension)\.[a-z]+$/i, + ]; + + const keyFiles = allFiles.filter(f => { + const basename = f.split('/').pop() || ''; + return keyPatterns.some(p => p.test(basename)) || entryPatterns.some(p => p.test(f)); + }); + + // Limit to a reasonable number of files + const filesToFetch = keyFiles.slice(0, 15); + + // 4. Fetch contents of key files (in parallel, with size limits) + const fileContents: { path: string; content: string }[] = []; + const MAX_FILE_SIZE = 8000; // characters per file + + await Promise.all(filesToFetch.map(async (filePath) => { + try { + const fileRes = await fetch( + `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(filePath)}?ref=${encodeURIComponent(defaultBranch)}`, + { headers, signal: AbortSignal.timeout(10000) } + ); + if (!fileRes.ok) { return; } + const fileData: any = await fileRes.json(); + if (fileData.encoding === 'base64' && fileData.content) { + let decoded = Buffer.from(fileData.content, 'base64').toString('utf-8'); + if (decoded.length > MAX_FILE_SIZE) { + decoded = decoded.substring(0, MAX_FILE_SIZE) + '\n... (truncated)'; + } + fileContents.push({ path: filePath, content: decoded }); + } + } catch { /* skip files that fail */ } + })); + + // 5. Compose the summary for the LLM + let summary = `# Repository: ${owner}/${repo}\n`; + summary += `- Description: ${description}\n`; + summary += `- Primary language: ${language}\n`; + summary += `- Default branch: ${defaultBranch}\n`; + summary += `- Total files: ${allFiles.length}\n\n`; + + summary += `## Directory structure\n\`\`\`\n`; + // Show a compact tree (limit depth to avoid overwhelming the LLM) + const treeLines = allFiles.filter(f => { + const depth = f.split('/').length; + return depth <= 3; + }); + summary += treeLines.slice(0, 150).join('\n'); + if (treeLines.length > 150) { summary += '\n... (truncated)'; } + summary += '\n```\n\n'; + + summary += `## Key file contents\n\n`; + for (const fc of fileContents) { + summary += `### ${fc.path}\n\`\`\`\n${fc.content}\n\`\`\`\n\n`; + } + + return summary; +} + +// --------------------------------------------------------------------------- +// LLM – generateAgentMdFromRepo (generates AGENTS.MD from a GitHub repository) +// --------------------------------------------------------------------------- + +export async function generateAgentMdFromRepo( + repoContent: string +): Promise<{ content: string; reasoning: string }> { + const systemPrompt = ` +You are an expert technical writer and software architect. You will be given information about a GitHub repository including its structure, metadata, and key file contents. + +Your job is to generate a comprehensive AGENTS.MD file that summarizes the repository. The AGENTS.MD should help developers and AI agents quickly understand the codebase. + +You MUST return output using ONLY the two XML tags below, with nothing before or after them. Absolutely NO backticks wrapping the entire output, NO prose outside the tags. + +### REQUIRED SCHEMA (use exactly these tags and order): + +[ONLY the final AGENTS.MD content here — valid Markdown format] + + +[ONLY the explanation of how you analyzed the repository and what you included — plain text, no code fences] + + +### AGENTS.MD CONTENT GUIDELINES: +The generated AGENTS.MD should include these sections (adapt based on what's relevant): +1) **Repository Overview** – Brief description of what the project does +2) **Environment Setup** – How to install dependencies and set up the development environment +3) **Build & Run** – Commands to build, run, and test the project +4) **Architecture** – High-level description of the codebase structure and key modules +5) **Key Files & Directories** – Table mapping paths to their purposes +6) **Code Conventions** – Naming patterns, style guidelines, important patterns used +7) **Testing** – How tests are organized and run +8) **Dependencies** – Key runtime and development dependencies +9) **Configuration** – Environment variables, config files, and their purpose +10) **Security Notes** – Any security-relevant information + +### RULES (strict): +1) Output MUST start with "" on the first line and end with "" on the last line. +2) No additional tags, headers, or text outside the two blocks. +3) The content inside MUST be valid Markdown suitable for an AGENTS.MD file. +4) Put ALL explanation inside . Do NOT include code fences there. +5) Do NOT wrap anything in triple backticks outside the tags. +6) Be thorough but concise. Focus on actionable information that helps someone work with the codebase. + +Validate your output against the RULES before responding. + `.trim(); + + const BASE_URL = baseUrl || "localhost:1234/v1"; + const MODEL = LLMmodel || "deepseek/deepseek-r1-0528-qwen3-8b"; + const API_KEY = "lm-studio"; + + async function requestLLM(): Promise<{ response: string; usage?: any }> { + const res = await fetch(`http://${BASE_URL}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: MODEL, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: `Analyze this GitHub repository and generate a comprehensive AGENTS.MD file:\n\n${repoContent}` }, + ], + temperature: 0.7, + stream: false, + }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`HTTP ${res.status} ${res.statusText}${text ? ` - ${text}` : ""}`); + } + + const json: any = await res.json(); + const output = + json?.choices?.[0]?.message?.content?.trim?.() ?? + json?.choices?.[0]?.text?.trim?.() ?? + ""; + + return { response: output, usage: json?.usage }; + } + + // Try up to 3 times to get a valid response + for (let attempt = 0; attempt < 3; attempt++) { + try { + const data = await requestLLM(); + const output = data.response; + const codeMatch = output.match(/([\s\S]*?)<\/code>/i); + const reasoningMatch = output.match(/([\s\S]*?)<\/reasoning>/i); + const content = codeMatch?.[1]?.trim() ?? ""; + const reasoning = reasoningMatch?.[1]?.trim() ?? "(no reasoning provided)"; + + if (content) { + return { content, reasoning }; + } else { + console.warn("No block found in LLM response. Retrying..."); + } + } catch (err) { + console.warn("Error during AGENTS.MD generation:", (err as Error).message); + if (attempt === 2) throw err; + } + await new Promise((res) => setTimeout(res, 1000)); + } + + throw new Error("Failed to generate AGENTS.MD after multiple attempts"); +} + /** Map VSCode languageId to a file extension understood by Lizard */ function extFromLanguageId(lang: string): string { diff --git a/src/server.ts b/src/server.ts index 133fec9..6dffc35 100644 --- a/src/server.ts +++ b/src/server.ts @@ -355,6 +355,226 @@ Validate your output against the RULES before responding. throw new Error('Failed to generate code from Agent.md after multiple attempts'); } +// --------------------------------------------------------------------------- +// GitHub helpers – fetch repository structure and key files +// --------------------------------------------------------------------------- + +/** Parse owner and repo name from a GitHub URL */ +function parseGitHubUrl(url: string): { owner: string; repo: string } | null { + const match = url.trim().match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#?]+)|^([^/\s]+)\/([^/\s#?]+)$/); + if (match) { + const owner = match[1] || match[3]; + const repo = (match[2] || match[4]).replace(/\.git$/, ''); + return { owner, repo }; + } + return null; +} + +/** Fetch the repository tree and key file contents from GitHub API */ +async function fetchGitHubRepoContent(owner: string, repo: string): Promise { + const headers: Record = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'BonsAIDE' + }; + + // 1. Fetch repo metadata + const repoRes = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { headers, signal: AbortSignal.timeout(15000) }); + if (!repoRes.ok) { + throw new Error(`GitHub API error: ${repoRes.status} ${repoRes.statusText}`); + } + const repoData: any = await repoRes.json(); + const defaultBranch = repoData.default_branch || 'main'; + const description = repoData.description || ''; + const language = repoData.language || ''; + + // 2. Fetch directory tree (recursive) + const treeRes = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(defaultBranch)}?recursive=1`, { headers, signal: AbortSignal.timeout(15000) }); + if (!treeRes.ok) { + throw new Error(`GitHub tree API error: ${treeRes.status} ${treeRes.statusText}`); + } + const treeData: any = await treeRes.json(); + const allFiles: string[] = (treeData.tree || []) + .filter((item: any) => item.type === 'blob') + .map((item: any) => item.path); + + // 3. Identify key files to fetch content for + const keyPatterns = [ + /^readme\.md$/i, + /^package\.json$/i, + /^pyproject\.toml$/i, + /^cargo\.toml$/i, + /^go\.mod$/i, + /^pom\.xml$/i, + /^build\.gradle$/i, + /^makefile$/i, + /^dockerfile$/i, + /^requirements\.txt$/i, + /^setup\.py$/i, + /^tsconfig\.json$/i, + /^agents\.md$/i, + ]; + + const entryPatterns = [ + /^(?:src\/)?(?:main|index|app|server|extension)\.[a-z]+$/i, + ]; + + const keyFiles = allFiles.filter(f => { + const basename = f.split('/').pop() || ''; + return keyPatterns.some(p => p.test(basename)) || entryPatterns.some(p => p.test(f)); + }); + + const filesToFetch = keyFiles.slice(0, 15); + + // 4. Fetch contents of key files (in parallel, with size limits) + const fileContents: { path: string; content: string }[] = []; + const MAX_FILE_SIZE = 8000; + + await Promise.all(filesToFetch.map(async (filePath) => { + try { + const fileRes = await fetch( + `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(filePath)}?ref=${encodeURIComponent(defaultBranch)}`, + { headers, signal: AbortSignal.timeout(10000) } + ); + if (!fileRes.ok) { return; } + const fileData: any = await fileRes.json(); + if (fileData.encoding === 'base64' && fileData.content) { + let decoded = Buffer.from(fileData.content, 'base64').toString('utf-8'); + if (decoded.length > MAX_FILE_SIZE) { + decoded = decoded.substring(0, MAX_FILE_SIZE) + '\n... (truncated)'; + } + fileContents.push({ path: filePath, content: decoded }); + } + } catch { /* skip files that fail */ } + })); + + // 5. Compose the summary for the LLM + let summary = `# Repository: ${owner}/${repo}\n`; + summary += `- Description: ${description}\n`; + summary += `- Primary language: ${language}\n`; + summary += `- Default branch: ${defaultBranch}\n`; + summary += `- Total files: ${allFiles.length}\n\n`; + + summary += `## Directory structure\n\`\`\`\n`; + const treeLines = allFiles.filter(f => { + const depth = f.split('/').length; + return depth <= 3; + }); + summary += treeLines.slice(0, 150).join('\n'); + if (treeLines.length > 150) { summary += '\n... (truncated)'; } + summary += '\n```\n\n'; + + summary += `## Key file contents\n\n`; + for (const fc of fileContents) { + summary += `### ${fc.path}\n\`\`\`\n${fc.content}\n\`\`\`\n\n`; + } + + return summary; +} + +// --------------------------------------------------------------------------- +// LLM – generateAgentMdFromRepo (generates AGENTS.MD from a GitHub repository) +// --------------------------------------------------------------------------- + +async function generateAgentMdFromRepo( + repoContent: string +): Promise<{ content: string; reasoning: string }> { + const systemPrompt = ` +You are an expert technical writer and software architect. You will be given information about a GitHub repository including its structure, metadata, and key file contents. + +Your job is to generate a comprehensive AGENTS.MD file that summarizes the repository. The AGENTS.MD should help developers and AI agents quickly understand the codebase. + +You MUST return output using ONLY the two XML tags below, with nothing before or after them. Absolutely NO backticks wrapping the entire output, NO prose outside the tags. + +### REQUIRED SCHEMA (use exactly these tags and order): + +[ONLY the final AGENTS.MD content here — valid Markdown format] + + +[ONLY the explanation of how you analyzed the repository and what you included — plain text, no code fences] + + +### AGENTS.MD CONTENT GUIDELINES: +The generated AGENTS.MD should include these sections (adapt based on what's relevant): +1) **Repository Overview** – Brief description of what the project does +2) **Environment Setup** – How to install dependencies and set up the development environment +3) **Build & Run** – Commands to build, run, and test the project +4) **Architecture** – High-level description of the codebase structure and key modules +5) **Key Files & Directories** – Table mapping paths to their purposes +6) **Code Conventions** – Naming patterns, style guidelines, important patterns used +7) **Testing** – How tests are organized and run +8) **Dependencies** – Key runtime and development dependencies +9) **Configuration** – Environment variables, config files, and their purpose +10) **Security Notes** – Any security-relevant information + +### RULES (strict): +1) Output MUST start with "" on the first line and end with "" on the last line. +2) No additional tags, headers, or text outside the two blocks. +3) The content inside MUST be valid Markdown suitable for an AGENTS.MD file. +4) Put ALL explanation inside . Do NOT include code fences there. +5) Do NOT wrap anything in triple backticks outside the tags. +6) Be thorough but concise. Focus on actionable information that helps someone work with the codebase. + +Validate your output against the RULES before responding. + `.trim(); + + const BASE_URL = baseUrl || 'localhost:1234/v1'; + const MODEL = LLMmodel || 'deepseek/deepseek-r1-0528-qwen3-8b'; + const API_KEY = 'lm-studio'; + + async function requestLLM(): Promise<{ response: string; usage?: any }> { + const res = await fetch(`http://${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: `Analyze this GitHub repository and generate a comprehensive AGENTS.MD file:\n\n${repoContent}` }, + ], + temperature: 0.7, + stream: false, + }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`HTTP ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`); + } + + const json: any = await res.json(); + const output: string = + json?.choices?.[0]?.message?.content?.trim?.() ?? + json?.choices?.[0]?.text?.trim?.() ?? ''; + return { response: output, usage: json?.usage }; + } + + for (let attempt = 0; attempt < 3; attempt++) { + try { + const data = await requestLLM(); + const output = data.response; + const codeMatch = output.match(/([\s\S]*?)<\/code>/i); + const reasoningMatch = output.match(/([\s\S]*?)<\/reasoning>/i); + const content = codeMatch?.[1]?.trim() ?? ''; + const reasoning = reasoningMatch?.[1]?.trim() ?? '(no reasoning provided)'; + + if (content) { + return { content, reasoning }; + } else { + console.warn('No block found in LLM response. Retrying...'); + } + } catch (err) { + console.warn('Error during AGENTS.MD generation:', (err as Error).message); + if (attempt === 2) throw err; + } + await new Promise(res => setTimeout(res, 1000)); + } + + throw new Error('Failed to generate AGENTS.MD after multiple attempts'); +} + // --------------------------------------------------------------------------- // Initialise a fresh Bonsai with a root placeholder // --------------------------------------------------------------------------- @@ -683,6 +903,61 @@ async function handleMessage(message: any): Promise { } return; } + + if (message.command === 'generateAgentMd') { + const repoUrl = message.repoUrl || ''; + baseUrl = message.baseUrl || baseUrl; + LLMmodel = message.model || LLMmodel; + + bonsaiLog('Generating AGENTS.MD for repo:', repoUrl); + broadcast({ command: 'loading', text: 'Parsing GitHub repository URL...' }); + + try { + const parsed = parseGitHubUrl(repoUrl); + if (!parsed) { + throw new Error('Invalid GitHub URL. Expected format: https://github.com/owner/repo'); + } + + // Verify LLM connection first + bonsaiLog('Verifying LLM connection before generating AGENTS.MD'); + if (!/^[\w.-]+(:\d+)?(\/[\w./]*)?$/.test(baseUrl)) { + throw new Error('Invalid LLM URL format. Expected format: host:port/path (e.g., localhost:1234/v1)'); + } + const modelsRes = await fetch(`http://${baseUrl}/models`, { + method: 'GET', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer lm-studio' }, + signal: AbortSignal.timeout(5000) + }); + if (!modelsRes.ok) { + throw new Error(`LLM server returned ${modelsRes.status}: ${modelsRes.statusText}`); + } + + // Fetch repo content from GitHub + broadcast({ command: 'loading', text: 'Fetching repository from GitHub...' }); + const repoContent = await fetchGitHubRepoContent(parsed.owner, parsed.repo); + bonsaiLog('Repository content fetched. Length:', repoContent.length); + + // Generate AGENTS.MD via LLM + broadcast({ command: 'loading', text: 'Generating AGENTS.MD with LLM...' }); + const { content: agentsMd, reasoning } = await generateAgentMdFromRepo(repoContent); + bonsaiLog('AGENTS.MD generated successfully. Length:', agentsMd.length); + + broadcast({ + command: 'generateAgentMdResult', + success: true, + content: agentsMd, + reasoning + }); + } catch (err: any) { + bonsaiLog('AGENTS.MD generation failed:', err?.message || err); + broadcast({ + command: 'generateAgentMdResult', + success: false, + message: err?.message || 'Generation failed' + }); + } + return; + } } // ---------------------------------------------------------------------------