UNPKG

@robinson_ai_systems/free-agent-mcp

Version:

Free Agent MCP - Portable, workspace-agnostic code generation using FREE models (Ollama)

1,475 lines (1,442 loc) 655 kB
#!/usr/bin/env node import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __commonJS = (cb, mod) => function __require2() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/shared/shared-llm/ollama-client.ts async function pingOllama(timeoutMs = 1e3) { const urls = [ `${BASE}/api/tags`, `${BASE.replace("localhost", "127.0.0.1")}/api/tags` ]; for (const url of urls) { try { const r = await fetch(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs), headers: { "Accept": "application/json" } }); if (r.ok) { return true; } } catch (error) { console.error(`[pingOllama] Failed to ping ${url}: ${error?.message || error}`); } } return false; } async function ollamaGenerate(opts) { const { model, prompt, format, timeoutMs = 12e4, retries = 2 } = opts; console.error(`[sharedGenerate] Starting generation with model: ${model}, timeout: ${timeoutMs}ms`); let lastErr; for (let i = 0; i <= retries; i++) { try { console.error(`[sharedGenerate] Attempt ${i + 1}/${retries + 1}`); const body = { model, prompt, stream: false }; if (format === "json") { body.format = "json"; } console.error(`[sharedGenerate] Sending fetch to ${BASE}/api/generate`); const r = await fetch(`${BASE}/api/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs) }); console.error(`[sharedGenerate] Fetch completed with status: ${r.status}`); if (!r.ok) throw new Error(`HTTP ${r.status}`); console.error("[sharedGenerate] Parsing JSON response..."); const json = await r.json(); console.error("[sharedGenerate] JSON parsed successfully"); return json.response || ""; } catch (e) { console.error(`[sharedGenerate] Error on attempt ${i + 1}:`, e); lastErr = e; if (i < retries) { console.error(`[sharedGenerate] Retrying in ${500 * (i + 1)}ms...`); await sleep(500 * (i + 1)); } } } throw new Error(`Ollama generate failed after ${retries + 1} attempt(s): ${lastErr?.message || lastErr}`); } async function warmModels(models) { for (const model of models) { try { await ollamaGenerate({ model, prompt: "test", timeoutMs: 1e4, retries: 0 }); } catch { } } } var BASE, sleep; var init_ollama_client = __esm({ "src/shared/shared-llm/ollama-client.ts"() { "use strict"; BASE = (process.env.OLLAMA_BASE_URL || "http://localhost:11434").replace(/\/+$/, ""); sleep = (ms) => new Promise((r) => setTimeout(r, ms)); } }); // src/pipeline/types.ts function calculateWeightedScore(scores, weights = DEFAULT_PIPELINE_CONFIG.weights) { const w = weights; return scores.compilation * w.compilation + scores.tests_functional * w.tests_functional + scores.tests_edge * w.tests_edge + scores.types * w.types + scores.style * w.style + scores.security * w.security + (scores.conventions || 0) * (w.conventions || 0); } function meetsAcceptanceCriteria(verdict, config = DEFAULT_PIPELINE_CONFIG) { const score = calculateWeightedScore(verdict.scores, config.weights); if (score < (config.acceptThreshold ?? DEFAULT_PIPELINE_CONFIG.acceptThreshold)) { return false; } if (verdict.scores.compilation === 0) return false; if (verdict.scores.security === 0) return false; return true; } var DEFAULT_PIPELINE_CONFIG; var init_types = __esm({ "src/pipeline/types.ts"() { "use strict"; DEFAULT_PIPELINE_CONFIG = { maxAttempts: 5, acceptThreshold: 0.9, weights: { compilation: 0.15, tests_functional: 0.25, tests_edge: 0.15, types: 0.1, security: 0.1, style: 0.05, conventions: 0.2 }, allowedLibraries: [ // Node.js built-ins "fs", "path", "util", "crypto", "stream", "events", "buffer", // Common safe libraries "lodash", "axios", "express", "react", "vue", "next", // Testing "jest", "@jest/globals", "vitest", "mocha", "chai", // TypeScript "typescript", "@types/*" ], minCoverage: 80, testTimeout: 5e3, globalTimeout: 3e4, memoryLimit: 512, spec: "", provider: "ollama", model: "qwen2.5-coder:7b" }; } }); // src/utils/repo-tools.ts import * as fs from "fs"; import * as path from "path"; import { execSync } from "child_process"; async function runRepoTools(root, files) { const result = { passed: true, lintErrors: [], typeErrors: [], boundaryErrors: [], customRuleErrors: [], allErrors: [] }; const lintResult = await runESLint(root, files); result.lintErrors = lintResult.errors; if (lintResult.errors.length > 0) result.passed = false; const typeResult = await runTypeScript(root, files); result.typeErrors = typeResult.errors; if (typeResult.errors.length > 0) result.passed = false; const boundaryResult = await checkBoundaries(root, files); result.boundaryErrors = boundaryResult.errors; if (boundaryResult.errors.length > 0) result.passed = false; const customResult = await runCustomRules(root, files); result.customRuleErrors = customResult.errors; if (customResult.errors.length > 0) result.passed = false; result.allErrors = [ ...result.lintErrors, ...result.typeErrors, ...result.boundaryErrors, ...result.customRuleErrors ]; return result; } async function runESLint(root, files) { const errors = []; const eslintPath = path.join(root, "node_modules", ".bin", "eslint"); if (!fs.existsSync(eslintPath) && !fs.existsSync(eslintPath + ".cmd")) { return { errors: [] }; } try { const fileArgs = files.map((f) => `"${f}"`).join(" "); const cmd = process.platform === "win32" ? `"${eslintPath}.cmd" --format json ${fileArgs}` : `"${eslintPath}" --format json ${fileArgs}`; const output = execSync(cmd, { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); const results = JSON.parse(output); for (const result of results) { for (const message of result.messages) { errors.push(`${result.filePath}:${message.line}:${message.column} - ${message.message} (${message.ruleId})`); } } } catch (error) { if (error.stdout) { try { const results = JSON.parse(error.stdout); for (const result of results) { for (const message of result.messages) { errors.push(`${result.filePath}:${message.line}:${message.column} - ${message.message} (${message.ruleId})`); } } } catch (parseError) { errors.push(`ESLint error: ${error.message}`); } } } return { errors: errors.slice(0, 10) }; } async function runTypeScript(root, files) { const errors = []; const tscPath = path.join(root, "node_modules", ".bin", "tsc"); if (!fs.existsSync(tscPath) && !fs.existsSync(tscPath + ".cmd")) { return { errors: [] }; } try { const cmd = process.platform === "win32" ? `"${tscPath}.cmd" --noEmit --pretty false` : `"${tscPath}" --noEmit --pretty false`; execSync(cmd, { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); } catch (error) { if (error.stderr || error.stdout) { const output = error.stderr || error.stdout; const lines = output.split("\n"); for (const line of lines) { const match = line.match(/^(.+?)\((\d+),(\d+)\):\s*error\s+TS\d+:\s*(.+)$/); if (match) { const [, file, line2, col, message] = match; errors.push(`${file}:${line2}:${col} - ${message}`); } } } } return { errors: errors.slice(0, 10) }; } async function checkBoundaries(root, files) { const errors = []; const eslintConfigPath = findESLintConfig(root); if (!eslintConfigPath) { return { errors: [] }; } try { const configContent = fs.readFileSync(eslintConfigPath, "utf-8"); if (!configContent.includes("boundaries")) { return { errors: [] }; } const boundaryRules = parseBoundaryRules(configContent); for (const file of files) { const violations = checkFileBoundaries(root, file, boundaryRules); errors.push(...violations); } } catch (error) { } return { errors: errors.slice(0, 10) }; } function findESLintConfig(root) { const configFiles = [ ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc", "eslint.config.js" ]; for (const file of configFiles) { const fullPath = path.join(root, file); if (fs.existsSync(fullPath)) { return fullPath; } } return null; } function parseBoundaryRules(configContent) { const rules = []; const ruleMatches = configContent.matchAll(/"from":\s*\[([^\]]+)\][^}]*"allow":\s*\[([^\]]+)\]/g); for (const match of ruleMatches) { const from = match[1].split(",").map((s) => s.trim().replace(/['"]/g, "")); const allow = match[2].split(",").map((s) => s.trim().replace(/['"]/g, "")); rules.push({ from, allow }); } return rules; } function checkFileBoundaries(root, file, rules) { const errors = []; try { const content = fs.readFileSync(path.join(root, file), "utf-8"); const imports = extractImports(content); const fileLayer = determineLayer(file); if (!fileLayer) return errors; const rule = rules.find((r) => r.from.includes(fileLayer)); if (!rule) return errors; for (const imp of imports) { const importLayer = determineLayer(imp); if (!importLayer) continue; if (!rule.allow.includes(importLayer)) { errors.push(`${file}: Cannot import from ${importLayer} (${imp}). ${fileLayer} can only import from: ${rule.allow.join(", ")}`); } } } catch (error) { } return errors; } function extractImports(content) { const imports = []; const importMatches = content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g); for (const match of importMatches) { imports.push(match[1]); } const requireMatches = content.matchAll(/require\(['"]([^'"]+)['"]\)/g); for (const match of requireMatches) { imports.push(match[1]); } return imports; } function determineLayer(filePath) { if (filePath.includes("/features/") || filePath.includes("\\features\\")) return "feature"; if (filePath.includes("/domain/") || filePath.includes("\\domain\\")) return "domain"; if (filePath.includes("/infra/") || filePath.includes("\\infra\\")) return "infra"; if (filePath.includes("/utils/") || filePath.includes("\\utils\\")) return "utils"; if (filePath.includes("/lib/") || filePath.includes("\\lib\\")) return "lib"; if (filePath.includes("/core/") || filePath.includes("\\core\\")) return "core"; return null; } async function runCustomRules(root, files) { const errors = []; for (const file of files) { try { const content = fs.readFileSync(path.join(root, file), "utf-8"); const snakeCaseMatches = content.matchAll(/\b(const|let|var)\s+([a-z]+_[a-z_]+)\s*=/g); for (const match of snakeCaseMatches) { const varName = match[2]; if (!/^[A-Z_]+$/.test(varName)) { errors.push(`${file}: Use camelCase instead of snake_case for variable '${varName}'`); } } const anyTypeMatches = content.matchAll(/export\s+(function|const|class|interface|type)\s+[^:]+:\s*any/g); for (const match of anyTypeMatches) { errors.push(`${file}: Do not use 'any' type in public exports`); } if (!file.includes(".test.") && !file.includes(".spec.")) { const consoleMatches = content.matchAll(/console\.log\(/g); for (const match of consoleMatches) { errors.push(`${file}: Remove console.log() from production code`); } } } catch (error) { } } return { errors: errors.slice(0, 10) }; } var init_repo_tools = __esm({ "src/utils/repo-tools.ts"() { "use strict"; } }); // src/utils/edit-constraints.ts import * as path2 from "path"; async function checkEditConstraints(root, files, constraints) { const violations = []; for (const file of files) { const isAllowed = constraints.allowedPaths.some( (allowed) => file.path.startsWith(allowed) || matchesGlob(file.path, allowed) ); if (!isAllowed) { const isReadOnly = constraints.readOnlyPaths.some( (readonly) => file.path.startsWith(readonly) || matchesGlob(file.path, readonly) ); if (isReadOnly) { violations.push({ file: file.path, violation: "File is read-only and cannot be modified", severity: "error" }); continue; } } if (!constraints.allowPublicRenames) { const renameViolations = await checkPublicRenames(root, file); violations.push(...renameViolations); } } return violations; } function matchesGlob(filePath, pattern) { const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\./g, "\\."); const regex = new RegExp(`^${regexPattern}$`); return regex.test(filePath); } async function checkPublicRenames(root, file) { const violations = []; try { const newSymbols = extractSymbolsFromContent(file.content, file.path); const fs22 = await import("fs"); const fullPath = path2.join(root, file.path); if (!fs22.existsSync(fullPath)) { return violations; } const originalContent = fs22.readFileSync(fullPath, "utf-8"); const originalSymbols = extractSymbolsFromContent(originalContent, file.path); const originalPublic = originalSymbols.filter((s) => s.isPublic); const newPublic = newSymbols.filter((s) => s.isPublic); const originalNames = new Set(originalPublic.map((s) => s.name)); const newNames = new Set(newPublic.map((s) => s.name)); for (const name of originalNames) { if (!newNames.has(name)) { violations.push({ file: file.path, violation: `Public symbol '${name}' was removed or renamed. This is a breaking change.`, severity: "error" }); } } } catch (error) { } return violations; } function extractSymbolsFromContent(content, filePath) { const symbols = []; const lines = content.split("\n"); const patterns = [ // export function/const/class/interface/type { regex: /export\s+(async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "function", isPublic: true }, { regex: /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "const", isPublic: true }, { regex: /export\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "class", isPublic: true }, { regex: /export\s+interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "interface", isPublic: true }, { regex: /export\s+type\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "type", isPublic: true }, { regex: /export\s+enum\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "enum", isPublic: true } ]; for (const pattern of patterns) { let match; while ((match = pattern.regex.exec(content)) !== null) { const name = match[2] || match[1]; if (!name) continue; const lineNumber = content.substring(0, match.index).split("\n").length; symbols.push({ name, type: pattern.type, file: filePath, line: lineNumber, isPublic: pattern.isPublic }); } } return symbols; } function inferAllowedPaths(spec, root) { const allowedPaths = []; const pathMatches = spec.matchAll(/(?:^|\s)([a-zA-Z0-9_\-/]+\.[a-zA-Z]+)/g); for (const match of pathMatches) { allowedPaths.push(match[1]); } const dirMatches = spec.matchAll(/(?:^|\s)([a-zA-Z0-9_\-/]+\/)/g); for (const match of dirMatches) { allowedPaths.push(match[1] + "**/*"); } if (allowedPaths.length === 0) { allowedPaths.push("src/**/*"); } return allowedPaths; } function getDefaultReadOnlyPaths() { return [ "node_modules/**/*", "dist/**/*", "build/**/*", ".next/**/*", "coverage/**/*", "__generated__/**/*", "src/gen/**/*", "src/generated/**/*", "prisma/client/**/*" ]; } function calculateDiffSize(originalContent, newContent) { const originalLines = originalContent.split("\n"); const newLines = newContent.split("\n"); let changes = 0; const maxLines = Math.max(originalLines.length, newLines.length); for (let i = 0; i < maxLines; i++) { const originalLine = originalLines[i] || ""; const newLine = newLines[i] || ""; if (originalLine !== newLine) { changes++; } } return changes; } function isMinimalDiff(originalContent, newContent) { const diffSize = calculateDiffSize(originalContent, newContent); const totalLines = originalContent.split("\n").length; if (totalLines === 0) return true; const changePercentage = diffSize / totalLines * 100; return changePercentage < 50; } async function enforceMinimalDiffs(root, files) { const violations = []; const fs22 = await import("fs"); for (const file of files) { const fullPath = path2.join(root, file.path); if (!fs22.existsSync(fullPath)) { continue; } const originalContent = fs22.readFileSync(fullPath, "utf-8"); if (!isMinimalDiff(originalContent, file.content)) { const diffSize = calculateDiffSize(originalContent, file.content); const totalLines = originalContent.split("\n").length; const changePercentage = (diffSize / totalLines * 100).toFixed(1); violations.push({ file: file.path, violation: `Diff is too large (${changePercentage}% of file changed). Prefer minimal, targeted changes.`, severity: "warning" }); } } return violations; } var init_edit_constraints = __esm({ "src/utils/edit-constraints.ts"() { "use strict"; } }); // src/utils/convention-tests.ts function generateConventionTests(files, brief) { const tests = []; tests.push(generateImportPathTest(files, brief)); tests.push(generateNamingConventionTest(files, brief)); tests.push(generateNoAnyTest(files)); tests.push(generateLayeringTest(files, brief)); tests.push(generateFileNamingTest(files, brief)); return tests; } function generateImportPathTest(files, brief) { const code = ` describe('Import Path Conventions', () => { it('should use correct import paths', () => { const files = ${JSON.stringify(files.map((f) => ({ path: f.path, content: f.content })), null, 2)}; for (const file of files) { const imports = extractImports(file.content); for (const imp of imports) { // Check for relative imports going up too many levels const upLevels = (imp.match(/\\.\\./g) || []).length; expect(upLevels).toBeLessThanOrEqual(2); // Check for absolute imports from src if (imp.startsWith('src/')) { fail(\`File \${file.path} uses absolute import '\${imp}'. Use relative imports or path aliases.\`); } } } }); }); `; return { name: "import-paths", description: "Validates import path conventions", code }; } function generateNamingConventionTest(files, brief) { const code = ` describe('Naming Conventions', () => { it('should follow repo naming conventions', () => { const files = ${JSON.stringify(files.map((f) => ({ path: f.path, content: f.content })), null, 2)}; const namingRules = ${JSON.stringify(brief.naming, null, 2)}; for (const file of files) { // Check variable names (camelCase) const varMatches = file.content.matchAll(/(?:const|let|var)\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g); for (const match of varMatches) { const name = match[1]; // Skip UPPER_SNAKE_CASE constants if (name === name.toUpperCase()) continue; // Check camelCase if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) { fail(\`Variable '\${name}' in \${file.path} should be camelCase\`); } } // Check type names (PascalCase) const typeMatches = file.content.matchAll(/(?:interface|type|class|enum)\\s+([A-Z][a-zA-Z0-9]*)/g); for (const match of typeMatches) { const name = match[1]; // Check PascalCase if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) { fail(\`Type '\${name}' in \${file.path} should be PascalCase\`); } } } }); }); `; return { name: "naming-conventions", description: "Validates naming conventions (camelCase, PascalCase)", code }; } function generateNoAnyTest(files) { const code = ` describe('Type Safety', () => { it('should not use any in public exports', () => { const files = ${JSON.stringify(files.map((f) => ({ path: f.path, content: f.content })), null, 2)}; for (const file of files) { // Find export statements const exportMatches = file.content.matchAll(/export\\s+(?:function|const|class|interface|type)\\s+[^{;]+/g); for (const match of exportMatches) { const exportStatement = match[0]; // Check for 'any' type if (exportStatement.includes(': any') || exportStatement.includes('<any>')) { fail(\`Public export in \${file.path} uses 'any' type: \${exportStatement}\`); } } } }); }); `; return { name: "no-any-in-exports", description: "Ensures no any type in public exports", code }; } function generateLayeringTest(files, brief) { const code = ` describe('Layering Rules', () => { it('should respect layer boundaries', () => { const files = ${JSON.stringify(files.map((f) => ({ path: f.path, content: f.content })), null, 2)}; const layers = ${JSON.stringify(brief.layering?.layers || [], null, 2)}; // Skip if no layers defined if (!layers || layers.length === 0) { return; } for (const file of files) { // Determine file's layer const fileLayer = layers.find(layer => file.path.includes(layer.name)); if (!fileLayer) continue; // Extract imports const imports = extractImports(file.content); for (const imp of imports) { // Skip external imports if (!imp.startsWith('.') && !imp.startsWith('/')) continue; // Check if import violates layer rules const importedLayer = layers.find(layer => imp.includes(layer.name)); if (!importedLayer) continue; if (!fileLayer.canImport.includes(importedLayer.name)) { fail(\`File \${file.path} in layer '\${fileLayer.name}' cannot import from layer '\${importedLayer.name}'\`); } } } }); }); `; return { name: "layering-rules", description: "Validates layer boundary rules", code }; } function generateFileNamingTest(files, brief) { const code = ` describe('File Naming', () => { it('should follow file naming conventions', () => { const files = ${JSON.stringify(files.map((f) => f.path), null, 2)}; const fileNamingPattern = ${JSON.stringify(brief.naming.files)}; for (const filePath of files) { const fileName = filePath.split('/').pop() || ''; const baseName = fileName.replace(/\\.(ts|tsx|js|jsx)$/, ''); // Check kebab-case for files if (fileNamingPattern === 'kebab-case') { if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(baseName)) { fail(\`File '\${fileName}' should be kebab-case\`); } } // Check camelCase for files if (fileNamingPattern === 'camelCase') { if (!/^[a-z][a-zA-Z0-9]*$/.test(baseName)) { fail(\`File '\${fileName}' should be camelCase\`); } } // Check PascalCase for files if (fileNamingPattern === 'PascalCase') { if (!/^[A-Z][a-zA-Z0-9]*$/.test(baseName)) { fail(\`File '\${fileName}' should be PascalCase\`); } } } }); }); `; return { name: "file-naming", description: "Validates file naming conventions", code }; } function generateConventionTestFile(files, brief) { const tests = generateConventionTests(files, brief); const testFile = `/** * Convention Tests - Auto-generated * * These tests ensure generated code follows repository conventions. */ import { describe, it, expect } from '@jest/globals'; // Helper function used by multiple tests function extractImports(content: string): string[] { const imports: string[] = []; const importRegex = /import\\s+.*?from\\s+['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { imports.push(match[1]); } return imports; } ${tests.map((t) => t.code).join("\n\n")} `; return testFile; } var init_convention_tests = __esm({ "src/utils/convention-tests.ts"() { "use strict"; } }); // src/utils/symbol-indexer.ts import * as fs2 from "fs"; import * as path3 from "path"; async function buildSymbolIndex(root, options = {}) { const { exts = [".ts", ".tsx", ".js", ".jsx"], maxFiles = 2e3, maxPerFileIds = 200, exclude = ["node_modules", "dist", "build", ".next", "coverage"] } = options; const index = { symbols: [], byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map(), byType: /* @__PURE__ */ new Map() }; const files = await findFiles(root, exts, exclude, maxFiles); for (const file of files) { try { const content = fs2.readFileSync(file, "utf-8"); const symbols = extractSymbols(content, file, maxPerFileIds); index.symbols.push(...symbols); index.byFile.set(file, symbols); for (const symbol of symbols) { if (!index.byName.has(symbol.name)) { index.byName.set(symbol.name, []); } index.byName.get(symbol.name).push(symbol); if (!index.byType.has(symbol.type)) { index.byType.set(symbol.type, []); } index.byType.get(symbol.type).push(symbol); } } catch (error) { console.warn(`Failed to index ${file}:`, error); } } return index; } async function findFiles(dir, exts, exclude, maxFiles) { const files = []; function walk(currentDir) { if (files.length >= maxFiles) return; const entries = fs2.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { if (files.length >= maxFiles) break; const fullPath = path3.join(currentDir, entry.name); const relativePath = path3.relative(dir, fullPath); if (exclude.some((ex) => relativePath.startsWith(ex))) { continue; } if (entry.isDirectory()) { walk(fullPath); } else if (entry.isFile()) { const ext = path3.extname(entry.name); if (exts.includes(ext)) { files.push(fullPath); } } } } walk(dir); return files; } function extractSymbols(content, file, maxIds) { const symbols = []; const lines = content.split("\n"); const patterns = [ // export function/const/class/interface/type { regex: /export\s+(async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "function", isPublic: true }, { regex: /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "const", isPublic: true }, { regex: /export\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "class", isPublic: true }, { regex: /export\s+interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "interface", isPublic: true }, { regex: /export\s+type\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "type", isPublic: true }, { regex: /export\s+enum\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, type: "enum", isPublic: true }, // non-export function/const/class/interface/type { regex: /^(?!export)\s*(async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm, type: "function", isPublic: false }, { regex: /^(?!export)\s*const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm, type: "const", isPublic: false }, { regex: /^(?!export)\s*class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm, type: "class", isPublic: false }, { regex: /^(?!export)\s*interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm, type: "interface", isPublic: false }, { regex: /^(?!export)\s*type\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm, type: "type", isPublic: false }, { regex: /^(?!export)\s*enum\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/gm, type: "enum", isPublic: false } ]; for (const pattern of patterns) { let match; while ((match = pattern.regex.exec(content)) !== null) { if (symbols.length >= maxIds) break; const name = match[2] || match[1]; if (!name) continue; const lineNumber = content.substring(0, match.index).split("\n").length; symbols.push({ name, type: pattern.type, file, line: lineNumber, isPublic: pattern.isPublic }); } } return symbols; } function topFrequencyIdentifiers(index, options = {}) { const { minLen = 3, byDir = [], limit = 50 } = options; const frequency = /* @__PURE__ */ new Map(); for (const symbol of index.symbols) { if (byDir.length > 0 && !byDir.some((dir) => symbol.file.includes(dir))) { continue; } if (symbol.name.length < minLen) { continue; } frequency.set(symbol.name, (frequency.get(symbol.name) || 0) + 1); } const sorted = Array.from(frequency.entries()).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([name]) => name); return sorted; } function publicExports(index, patterns) { const exports = []; for (const symbol of index.symbols) { if (!symbol.isPublic) continue; const matches = patterns.some((pattern) => { const regex = new RegExp(pattern.replace(/\*/g, ".*")); return regex.test(symbol.file); }); if (matches) { exports.push(`${symbol.type} ${symbol.name} (${symbol.file}:${symbol.line})`); } } return exports; } function collectNamingExamples(index) { const examples = []; const types = ["function", "class", "interface", "type", "const", "enum"]; for (const type of types) { const symbols = index.byType.get(type) || []; const publicSymbols = symbols.filter((s) => s.isPublic).slice(0, 5); for (const symbol of publicSymbols) { examples.push(`${type}: ${symbol.name}`); } } return examples; } function inferNamingConvention(index) { const variables = index.byType.get("const") || []; const types = [...index.byType.get("class") || [], ...index.byType.get("interface") || []]; let camelCaseCount = 0; let snakeCaseCount = 0; for (const v of variables.slice(0, 100)) { if (/^[a-z][a-zA-Z0-9]*$/.test(v.name)) camelCaseCount++; if (/^[a-z][a-z0-9_]*$/.test(v.name)) snakeCaseCount++; } const variableStyle = camelCaseCount > snakeCaseCount ? "camelCase" : "snake_case"; let pascalCaseCount = 0; for (const t of types.slice(0, 100)) { if (/^[A-Z][a-zA-Z0-9]*$/.test(t.name)) pascalCaseCount++; } const typeStyle = pascalCaseCount > types.length / 2 ? "PascalCase" : "unknown"; const constants = variables.filter((v) => /^[A-Z_][A-Z0-9_]*$/.test(v.name)); const constantStyle = constants.length > 10 ? "UPPER_SNAKE_CASE" : "unknown"; const files = Array.from(new Set(index.symbols.map((s) => path3.basename(s.file)))); let kebabCaseCount = 0; let camelCaseFileCount = 0; for (const file of files.slice(0, 100)) { const name = file.replace(/\.(ts|tsx|js|jsx)$/, ""); if (/^[a-z][a-z0-9-]*$/.test(name)) kebabCaseCount++; if (/^[a-z][a-zA-Z0-9]*$/.test(name)) camelCaseFileCount++; } const fileStyle = kebabCaseCount > camelCaseFileCount ? "kebab-case" : "camelCase"; return { variables: variableStyle, types: typeStyle, constants: constantStyle, files: fileStyle }; } var init_symbol_indexer = __esm({ "src/utils/symbol-indexer.ts"() { "use strict"; } }); // src/utils/schema-codegen.ts import * as fs3 from "fs"; import * as path4 from "path"; async function detectSchemas(root) { const schemas = []; const openapiSchema = await detectOpenAPI(root); if (openapiSchema) schemas.push(openapiSchema); const graphqlSchema = await detectGraphQL(root); if (graphqlSchema) schemas.push(graphqlSchema); const prismaSchema = await detectPrisma(root); if (prismaSchema) schemas.push(prismaSchema); const migrationsSchema = await detectMigrations(root); if (migrationsSchema) schemas.push(migrationsSchema); return schemas; } async function detectOpenAPI(root) { const openapiPaths = [ "openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "api-spec.json", "docs/openapi.json" ]; for (const p of openapiPaths) { const fullPath = path4.join(root, p); if (fs3.existsSync(fullPath)) { try { const content = fs3.readFileSync(fullPath, "utf-8"); const spec = p.endsWith(".json") ? JSON.parse(content) : null; if (!spec) continue; const entities = []; const types = []; const enums = []; if (spec.components?.schemas) { for (const [name, schema] of Object.entries(spec.components.schemas)) { types.push(name); if (schema.enum) { enums.push(name); } else { entities.push(name); } } } return { source: "openapi", path: p, entities, types, enums }; } catch (error) { } } } return null; } async function detectGraphQL(root) { const graphqlPaths = [ "schema.graphql", "schema.gql", "src/schema.graphql", "graphql/schema.graphql" ]; for (const p of graphqlPaths) { const fullPath = path4.join(root, p); if (fs3.existsSync(fullPath)) { try { const content = fs3.readFileSync(fullPath, "utf-8"); const entities = []; const types = []; const enums = []; const typeMatches = content.matchAll(/type\s+([A-Z][a-zA-Z0-9]*)/g); for (const match of typeMatches) { const name = match[1]; if (name !== "Query" && name !== "Mutation" && name !== "Subscription") { entities.push(name); types.push(name); } } const enumMatches = content.matchAll(/enum\s+([A-Z][a-zA-Z0-9]*)/g); for (const match of enumMatches) { enums.push(match[1]); types.push(match[1]); } const interfaceMatches = content.matchAll(/interface\s+([A-Z][a-zA-Z0-9]*)/g); for (const match of interfaceMatches) { types.push(match[1]); } return { source: "graphql", path: p, entities, types, enums }; } catch (error) { } } } return null; } async function detectPrisma(root) { const prismaPath = path4.join(root, "prisma", "schema.prisma"); if (fs3.existsSync(prismaPath)) { try { const content = fs3.readFileSync(prismaPath, "utf-8"); const entities = []; const types = []; const enums = []; const modelMatches = content.matchAll(/model\s+([A-Z][a-zA-Z0-9]*)/g); for (const match of modelMatches) { entities.push(match[1]); types.push(match[1]); } const enumMatches = content.matchAll(/enum\s+([A-Z][a-zA-Z0-9]*)/g); for (const match of enumMatches) { enums.push(match[1]); types.push(match[1]); } return { source: "prisma", path: "prisma/schema.prisma", entities, types, enums }; } catch (error) { } } return null; } async function detectMigrations(root) { const migrationsDirs = [ "migrations", "db/migrations", "prisma/migrations", "database/migrations" ]; for (const dir of migrationsDirs) { const fullPath = path4.join(root, dir); if (fs3.existsSync(fullPath)) { try { const files = fs3.readdirSync(fullPath); const sqlFiles = files.filter((f) => f.endsWith(".sql")); if (sqlFiles.length === 0) continue; const entities = []; const types = []; for (const file of sqlFiles.slice(0, 10)) { const content = fs3.readFileSync(path4.join(fullPath, file), "utf-8"); const tableMatches = content.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z_][a-zA-Z0-9_]*)/gi); for (const match of tableMatches) { const tableName = match[1]; const entityName = tableName.split("_").map( (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() ).join(""); if (!entities.includes(entityName)) { entities.push(entityName); types.push(entityName); } } } return { source: "migrations", path: dir, entities, types, enums: [] }; } catch (error) { } } } return null; } var init_schema_codegen = __esm({ "src/utils/schema-codegen.ts"() { "use strict"; } }); // src/utils/project-brief.ts var project_brief_exports = {}; __export(project_brief_exports, { formatBriefForPrompt: () => formatBriefForPrompt, makeProjectBrief: () => makeProjectBrief }); import * as fs4 from "fs"; import * as path5 from "path"; async function makeProjectBrief(root) { const brief = { language: "unknown", versions: {}, style: {}, layering: { type: "unknown" }, testing: { framework: "unknown", testPattern: "**/*.test.*" }, schema: { sources: [] }, glossary: { entities: [], enums: [], constants: [], commonAbbreviations: {} }, naming: { variables: "camelCase", types: "PascalCase", constants: "UPPER_SNAKE_CASE", files: "kebab-case", examples: [] }, apis: { publicExports: [] }, doNotTouch: [] }; await detectLanguageAndVersions(root, brief); await detectStyleRules(root, brief); await detectLayering(root, brief); await detectTesting(root, brief); await detectSchemasForBrief(root, brief); await detectDoNotTouch(root, brief); await buildGlossaryAndNaming(root, brief); return brief; } async function detectLanguageAndVersions(root, brief) { const pkgPath = path5.join(root, "package.json"); if (fs4.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8")); brief.language = pkg.type === "module" ? "esm" : "commonjs"; brief.versions.node = pkg.engines?.node; if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) { brief.language = "typescript"; const tsconfigPath = path5.join(root, "tsconfig.json"); if (fs4.existsSync(tsconfigPath)) { const tsconfig = JSON.parse(fs4.readFileSync(tsconfigPath, "utf-8")); brief.versions.typescript = pkg.devDependencies?.typescript || pkg.dependencies?.typescript; brief.style.tsconfig = tsconfig.compilerOptions; } } } catch (error) { console.warn("Failed to parse package.json:", error); } } const pyprojectPath = path5.join(root, "pyproject.toml"); if (fs4.existsSync(pyprojectPath)) { brief.language = "python"; } const gomodPath = path5.join(root, "go.mod"); if (fs4.existsSync(gomodPath)) { brief.language = "go"; try { const gomod = fs4.readFileSync(gomodPath, "utf-8"); const match = gomod.match(/^go\s+(\d+\.\d+)/m); if (match) { brief.versions.go = match[1]; } } catch (error) { console.warn("Failed to parse go.mod:", error); } } } async function detectStyleRules(root, brief) { const editorconfigPath = path5.join(root, ".editorconfig"); if (fs4.existsSync(editorconfigPath)) { brief.style.editorconfig = fs4.readFileSync(editorconfigPath, "utf-8"); } const prettierPaths = [".prettierrc", ".prettierrc.json", ".prettierrc.js", "prettier.config.js"]; for (const p of prettierPaths) { const fullPath = path5.join(root, p); if (fs4.existsSync(fullPath)) { try { if (p.endsWith(".json") || p === ".prettierrc") { brief.style.prettier = JSON.parse(fs4.readFileSync(fullPath, "utf-8")); } } catch (error) { console.warn(`Failed to parse ${p}:`, error); } break; } } const eslintPaths = [".eslintrc", ".eslintrc.json", ".eslintrc.js", "eslint.config.js"]; for (const p of eslintPaths) { const fullPath = path5.join(root, p); if (fs4.existsSync(fullPath)) { try { if (p.endsWith(".json") || p === ".eslintrc") { brief.style.eslint = JSON.parse(fs4.readFileSync(fullPath, "utf-8")); } } catch (error) { console.warn(`Failed to parse ${p}:`, error); } break; } } } async function detectLayering(root, brief) { const packagesDir = path5.join(root, "packages"); const appsDir = path5.join(root, "apps"); if (fs4.existsSync(packagesDir) || fs4.existsSync(appsDir)) { brief.layering.type = "monorepo"; brief.layering.packages = []; if (fs4.existsSync(packagesDir)) { const packages = fs4.readdirSync(packagesDir).filter((p) => { const stat = fs4.statSync(path5.join(packagesDir, p)); return stat.isDirectory(); }); brief.layering.packages.push(...packages.map((p) => `packages/${p}`)); } if (fs4.existsSync(appsDir)) { const apps = fs4.readdirSync(appsDir).filter((p) => { const stat = fs4.statSync(path5.join(appsDir, p)); return stat.isDirectory(); }); brief.layering.packages.push(...apps.map((p) => `apps/${p}`)); } } else { brief.layering.type = "single"; } const srcDir = path5.join(root, "src"); if (fs4.existsSync(srcDir)) { const layers = []; const commonLayers = ["features", "domain", "infra", "utils", "lib", "core"]; for (const layer of commonLayers) { const layerPath = path5.join(srcDir, layer); if (fs4.existsSync(layerPath)) { layers.push({ name: layer, pattern: `src/${layer}/**/*`, canImport: inferCanImport(layer) }); } } if (layers.length > 0) { brief.layering.layers = layers; } } if (brief.style.eslint?.settings?.["boundaries/elements"]) { brief.layering.boundaries = "eslint-plugin-boundaries"; } } function inferCanImport(layer) { const rules = { features: ["domain", "infra", "utils", "lib", "core"], domain: ["domain", "infra", "utils", "lib", "core"], infra: ["infra", "utils", "lib", "core"], utils: ["utils", "lib", "core"], lib: ["lib", "core"], core: ["core"] }; return rules[layer] || []; } async function detectTesting(root, brief) { const pkgPath = path5.join(root, "package.json"); if (fs4.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8")); if (pkg.devDependencies?.jest || pkg.dependencies?.jest) { brief.testing.framework = "jest"; brief.testing.testPattern = "**/*.test.{ts,tsx,js,jsx}"; } else if (pkg.devDependencies?.vitest || pkg.dependencies?.vitest) { brief.testing.framework = "vitest"; brief.testing.testPattern = "**/*.test.{ts,tsx,js,jsx}"; } else if (pkg.devDependencies?.mocha || pkg.dependencies?.mocha) { brief.testing.framework = "mocha"; brief.testing.testPattern = "**/*.test.{ts,js}"; } } catch (error) { console.warn("Failed to detect testing framework:", error); } } } async function detectSchemasForBrief(root, brief) { const schemas = await detectSchemas(root); for (const schema of schemas) { brief.schema.sources.push({ type: schema.source, path: schema.path, entities: schema.entities }); if (!brief.schema.generatedTypes) { brief.schema.generatedTypes = []; } brief.schema.generatedTypes.push(...schema.types); } } async function detectDoNotTouch(root, brief) { const commonGenerated = [ "node_modules", "dist", "build", ".next", ".nuxt", "out", "coverage", ".turbo", "src/gen", "src/generated", "prisma/client", "__generated__" ]; for (const dir of commonGenerated) { const fullPath = path5.join(root, dir); if (fs4.existsSync(fullPath)) { brief.doNotTouch.push(dir); } } } function formatBriefForPrompt(brief) { const sections = []; sections.push(`# Project Brief `); sections.push(`## Language & Versions`); sections.push(`- Language: ${brief.language}`); if (brief.versions.node) sections.push(`- Node: ${brief.versions.node}`); if (brief.versions.typescript) sections.push(`- TypeScript: ${brief.versions.typescript}`); if (brief.versions.python) sections.push(`- Python: ${brief.versions.python}`); if (brief.versions.go) sections.push(`- Go: ${brief.versions.go}`); sections.push(""); sections.push(`## Style Rules`); if (brief.style.prettier) { sections.push(`- Prettier: ${JSON.stringify(brief.style.prettier, null, 2).slice(0, 200)}...`); } if (brief.style.eslint?.rules) { const topRules = Object.keys(brief.style.eslint.rules).slice(0, 10); sections.push(`- ESLint rules: ${topRules.join(", ")}`); } if (brief.style.tsconfig) { sections.push(`- TypeScript: strict=${brief.style.tsconfig.strict}, target=${brief.style.tsconfig.target}`); } sections.push(""); sections.push(`## Layering & Boundaries`); sections.push(`- Type: ${brief.layering.type}`); if (brief.layering.layers) { for (const layer of brief.layering.layers) { sections.push(`- ${layer.name}: can import ${layer.canImport.join(", ")}`); } } sections.push(""); sections.push(`## Testing`); sections.push(`- Framework: ${brief.testing.framework}`); sections.push(`- Pattern: ${brief.testing.testPattern}`); sections.push(""); sections.push(`## Naming Conventions`); sections.push(`- Variables: ${brief.naming.variables}`); sections.push(`- Types: ${brief.naming.types}`); sections.push(`- Constants: ${brief.naming.constants}`); sections.push(`- Files: ${brief.naming.files}`); sections.push(""); if (brief.schema.generatedTypes && brief.schema.generatedTypes.length > 0) { sections.push(`## Schema Types (Use These!)`); sections.push(`- ${brief.schema.generatedTypes.slice(0, 20).join(", ")}`); sections.push(""); } if (brief.doNotTouch.length > 0) { sections.push(`## Do Not Touch`); sections.push(`- ${brief.doNotTouch.join(", ")}`); sections.push(""); } return sections.join("\n"); } async function buildGlossaryAndNaming(root, brief) { try { const index = await buildSymbolIndex(root, { exts: [".ts", ".tsx", ".js", ".jsx"], maxFiles: 2e3, maxPerFileIds: 200, exclude: ["node_modules", "dist", "build", ".next", "coverage", "__generated__"] }); const topIds = topFrequencyIdentifiers(index, { minLen: 3, byDir: ["/domain", "/models", "/entities", "/types"], limit: 50 }); brief.glossary.entities = topIds.filter((id) => /^[A-Z]/.test(id) && !/^[A-Z_]+$/.test(id)); brief.glossary.enums = topIds.filter((id) => /^[A-Z][a-z]+[A-Z]/.test(id)); brief.glossary.constants = topIds.filter((id) => /^[A-Z_]+$/.test(id)); brief.apis.publicExports = publicExports(index, [ "src/**/index.ts", "src/**/api/**/*.ts", "src/lib/**/*.ts" ]).slice(0, 20); brief.naming.examples = collectNamingExamples(index); const conventions = inferNamingConvention(index); brief.naming.variables = conventions.variables; brief.naming.types = conventions.types; brief.naming.constants = conventions.constants; brief.naming.files = conventions.files; } catch (error) { console.warn("Failed to build glossary and naming:", error); } } var init_project_brief = __esm({ "src/utils/project-brief.ts"() { "use strict"; init_symbol_indexer(); init_schema_codegen(); } }); // src/utils/dependency-cache.ts import * as fs5 from "fs"; import * as path6 from "path"; import * as os from "os"; import { execSync as execSync2 } from "child_process"; import * as crypto from "crypto"; function ensureCacheDir() { if (!fs5.existsSync(CACHE_DIR)) { fs5.mkdirSync(CACHE_DIR, { recursive: true }); } } function generateCacheKey(packageJson) { const deps = { dependencies: packageJson.dependencies || {}, devDependencies: packageJson.devDependencies || {} }; const hash = crypto.createHash("sha256"); hash.update(JSON.stringify(deps)); return hash.digest("hex").substring(0, 16); } function getCachedNodeModulesPath(cacheKey) { return path6.join(CACHE_DIR, `node_modules-${cacheKey}`); } function installAndCacheDependencies(sandboxDir, packageJson) { ensureCacheDir(); const cacheKey = generateCacheKey(packageJson); const cachedPath = getCachedNodeModulesPath(cacheKey); if (fs5.existsSync(cachedPath)) { console.log(`Using cached dependencies (key: ${cacheKey})`); linkCachedDependencies(sandboxDir, cachedPath); return; } console.log(`Installing dependencies (will cache with key: ${cacheKey})...`); try { execSync2("npm install --silent --n