diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 210190e..ac061d2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,7 +10,7 @@ import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './servi const route = useRoute() const router = useRouter() -type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' +type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' onMounted(async () => { // Initialize WebMCP connection diff --git a/frontend/src/components/Toolbar.vue b/frontend/src/components/Toolbar.vue index 7f214ef..5000ae9 100644 --- a/frontend/src/components/Toolbar.vue +++ b/frontend/src/components/Toolbar.vue @@ -103,6 +103,13 @@ onMounted(() => { + + + + + + +
diff --git a/frontend/src/pages/SourceCodePage.vue b/frontend/src/pages/SourceCodePage.vue new file mode 100644 index 0000000..eb1e689 --- /dev/null +++ b/frontend/src/pages/SourceCodePage.vue @@ -0,0 +1,951 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index aa0b08c..b29c0a3 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -38,6 +38,11 @@ const router = createRouter({ path: '/database', name: 'database', component: () => import('../pages/DatabasePage.vue') + }, + { + path: '/source', + name: 'source', + component: () => import('../pages/SourceCodePage.vue') } ] }) diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 592309f..40089f0 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -29,8 +29,13 @@ import { unregisterDatabaseTools, DATABASE_TOOLS } from './tools/databaseTools' +import { + registerSourceCodeTools, + unregisterSourceCodeTools, + SOURCE_CODE_TOOLS +} from './tools/sourceCodeTools' -type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' +type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' interface PageToolSet { register: () => void @@ -95,6 +100,11 @@ const pageTools: Record = { register: registerDatabaseTools, unregister: unregisterDatabaseTools, toolNames: DATABASE_TOOLS + }, + source: { + register: registerSourceCodeTools, + unregister: unregisterSourceCodeTools, + toolNames: SOURCE_CODE_TOOLS } } diff --git a/frontend/src/services/tools/sourceCodeTools.ts b/frontend/src/services/tools/sourceCodeTools.ts new file mode 100644 index 0000000..a8813d2 --- /dev/null +++ b/frontend/src/services/tools/sourceCodeTools.ts @@ -0,0 +1,290 @@ +import { registerTool, unregisterTools } from '../webmcp' + +export const SOURCE_CODE_TOOLS = [ + 'get_repo_info', + 'list_repo_files', + 'read_repo_file', + 'search_repo_code' +] + +const API_BASE = 'http://localhost:4101' + +// Store credentials in memory (not persisted) +let giteaCredentials: { + giteaUrl: string + username: string + password: string + owner: string + repo: string + branch: string +} | null = null + +export function setGiteaCredentials(creds: typeof giteaCredentials) { + giteaCredentials = creds +} + +export function registerSourceCodeTools() { + // get_repo_info + registerTool( + 'get_repo_info', + 'Obtiene informacion del repositorio conectado en Gitea', + { + type: 'object', + properties: {} + }, + async () => { + if (!giteaCredentials) { + return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.' + } + + try { + const res = await fetch(`${API_BASE}/api/gitea/repo`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(giteaCredentials) + }) + + if (!res.ok) { + const err = await res.json() + return `Error: ${err.error}` + } + + const data = await res.json() + const repo = data.repo + + return `Repositorio: ${repo.owner.login}/${repo.name}\n` + + `Descripcion: ${repo.description || 'Sin descripcion'}\n` + + `Rama default: ${repo.default_branch}\n` + + `Stars: ${repo.stars_count}\n` + + `Forks: ${repo.forks_count}\n` + + `Ramas disponibles: ${data.branches.join(', ')}` + } catch (e: any) { + return `Error: ${e.message}` + } + } + ) + + // list_repo_files + registerTool( + 'list_repo_files', + 'Lista archivos y carpetas en una ruta del repositorio', + { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Ruta dentro del repositorio (vacio para raiz)' + } + } + }, + async (args: { path?: string }) => { + if (!giteaCredentials) { + return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.' + } + + try { + const res = await fetch(`${API_BASE}/api/gitea/tree`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...giteaCredentials, + path: args.path || '' + }) + }) + + if (!res.ok) { + const err = await res.json() + return `Error: ${err.error}` + } + + const data = await res.json() + const path = args.path || '/' + + if (data.tree.length === 0) { + return `No hay archivos en ${path}` + } + + const folders = data.tree.filter((f: any) => f.type === 'dir') + const files = data.tree.filter((f: any) => f.type === 'file') + + let result = `Contenido de ${path}:\n\n` + + if (folders.length > 0) { + result += `Carpetas (${folders.length}):\n` + result += folders.map((f: any) => ` [DIR] ${f.name}/`).join('\n') + result += '\n\n' + } + + if (files.length > 0) { + result += `Archivos (${files.length}):\n` + result += files.map((f: any) => ` ${f.name}`).join('\n') + } + + return result + } catch (e: any) { + return `Error: ${e.message}` + } + } + ) + + // read_repo_file + registerTool( + 'read_repo_file', + 'Lee el contenido de un archivo del repositorio', + { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Ruta del archivo dentro del repositorio' + }, + lines: { + type: 'number', + description: 'Numero maximo de lineas a retornar (default: 100)' + } + }, + required: ['path'] + }, + async (args: { path: string; lines?: number }) => { + if (!giteaCredentials) { + return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.' + } + + try { + const res = await fetch(`${API_BASE}/api/gitea/file`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...giteaCredentials, + path: args.path + }) + }) + + if (!res.ok) { + const err = await res.json() + return `Error: ${err.error}` + } + + const data = await res.json() + let content = data.content + + // Limit lines if specified + const maxLines = args.lines || 100 + const lines = content.split('\n') + if (lines.length > maxLines) { + content = lines.slice(0, maxLines).join('\n') + content += `\n\n... (${lines.length - maxLines} lineas mas)` + } + + return `Archivo: ${args.path}\nTamano: ${data.size} bytes\n\n${content}` + } catch (e: any) { + return `Error: ${e.message}` + } + } + ) + + // search_repo_code + registerTool( + 'search_repo_code', + 'Busca codigo en el repositorio (busqueda simple en archivos)', + { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Texto a buscar en los archivos' + }, + path: { + type: 'string', + description: 'Ruta donde buscar (default: raiz)' + }, + extension: { + type: 'string', + description: 'Extension de archivos a buscar (ej: ts, vue, js)' + } + }, + required: ['query'] + }, + async (args: { query: string; path?: string; extension?: string }) => { + if (!giteaCredentials) { + return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.' + } + + try { + // First get the file tree + const treeRes = await fetch(`${API_BASE}/api/gitea/tree`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...giteaCredentials, + path: args.path || '' + }) + }) + + if (!treeRes.ok) { + return 'Error al obtener lista de archivos' + } + + const treeData = await treeRes.json() + const files = treeData.tree.filter((f: any) => { + if (f.type !== 'file') return false + if (args.extension) { + return f.name.endsWith(`.${args.extension}`) + } + return true + }) + + const results: string[] = [] + const maxFiles = 10 + let filesSearched = 0 + + for (const file of files.slice(0, maxFiles)) { + try { + const fileRes = await fetch(`${API_BASE}/api/gitea/file`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...giteaCredentials, + path: file.path + }) + }) + + if (fileRes.ok) { + const fileData = await fileRes.json() + const lines = fileData.content.split('\n') + const matches: string[] = [] + + lines.forEach((line: string, idx: number) => { + if (line.toLowerCase().includes(args.query.toLowerCase())) { + matches.push(` L${idx + 1}: ${line.trim().substring(0, 80)}`) + } + }) + + if (matches.length > 0) { + results.push(`${file.path}:\n${matches.slice(0, 5).join('\n')}`) + } + } + filesSearched++ + } catch (e) { + // Skip file errors + } + } + + if (results.length === 0) { + return `No se encontro "${args.query}" en los primeros ${filesSearched} archivos` + } + + return `Busqueda: "${args.query}"\n` + + `Archivos buscados: ${filesSearched}\n` + + `Coincidencias:\n\n${results.join('\n\n')}` + } catch (e: any) { + return `Error: ${e.message}` + } + } + ) +} + +export function unregisterSourceCodeTools() { + unregisterTools(SOURCE_CODE_TOOLS) + giteaCredentials = null +} diff --git a/server/index.ts b/server/index.ts index b6e1a21..b2198ce 100644 --- a/server/index.ts +++ b/server/index.ts @@ -861,6 +861,142 @@ Bun.serve({ }, { headers: corsHeaders }) } + // ===================== + // API de Gitea (Source Code Viewer) + // ===================== + + // POST /api/gitea/repo - Connect and get repo info + if (url.pathname === '/api/gitea/repo' && req.method === 'POST') { + const body = await req.json() + const { giteaUrl, username, password, owner, repo } = body + + if (!giteaUrl || !username || !password || !owner || !repo) { + return Response.json({ error: 'Missing required fields' }, { status: 400, headers: corsHeaders }) + } + + try { + const auth = Buffer.from(`${username}:${password}`).toString('base64') + + // Get repo info + const repoRes = await fetch(`${giteaUrl}/api/v1/repos/${owner}/${repo}`, { + headers: { 'Authorization': `Basic ${auth}` } + }) + + if (!repoRes.ok) { + if (repoRes.status === 401) { + return Response.json({ error: 'Invalid credentials' }, { status: 401, headers: corsHeaders }) + } + if (repoRes.status === 404) { + return Response.json({ error: 'Repository not found' }, { status: 404, headers: corsHeaders }) + } + throw new Error('Failed to connect to Gitea') + } + + const repoData = await repoRes.json() + + // Get branches + const branchesRes = await fetch(`${giteaUrl}/api/v1/repos/${owner}/${repo}/branches`, { + headers: { 'Authorization': `Basic ${auth}` } + }) + + let branches = ['main'] + if (branchesRes.ok) { + const branchesData = await branchesRes.json() + branches = branchesData.map((b: any) => b.name) + } + + return Response.json({ + repo: { + name: repoData.name, + description: repoData.description, + default_branch: repoData.default_branch, + stars_count: repoData.stars_count, + forks_count: repoData.forks_count, + owner: { login: repoData.owner?.login || owner } + }, + branches + }, { headers: corsHeaders }) + } catch (e: any) { + return Response.json({ error: e.message }, { status: 500, headers: corsHeaders }) + } + } + + // POST /api/gitea/tree - Get file tree + if (url.pathname === '/api/gitea/tree' && req.method === 'POST') { + const body = await req.json() + const { giteaUrl, username, password, owner, repo, branch, path } = body + + try { + const auth = Buffer.from(`${username}:${password}`).toString('base64') + const apiPath = path ? `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branch}` + : `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents?ref=${branch}` + + const res = await fetch(apiPath, { + headers: { 'Authorization': `Basic ${auth}` } + }) + + if (!res.ok) { + throw new Error('Failed to load tree') + } + + const data = await res.json() + const items = Array.isArray(data) ? data : [data] + + const tree = items + .map((item: any) => ({ + name: item.name, + path: item.path, + type: item.type === 'dir' ? 'dir' : 'file', + children: item.type === 'dir' ? [] : undefined + })) + .sort((a: any, b: any) => { + // Folders first, then files + if (a.type !== b.type) return a.type === 'dir' ? -1 : 1 + return a.name.localeCompare(b.name) + }) + + return Response.json({ tree }, { headers: corsHeaders }) + } catch (e: any) { + return Response.json({ error: e.message }, { status: 500, headers: corsHeaders }) + } + } + + // POST /api/gitea/file - Get file content + if (url.pathname === '/api/gitea/file' && req.method === 'POST') { + const body = await req.json() + const { giteaUrl, username, password, owner, repo, branch, path } = body + + try { + const auth = Buffer.from(`${username}:${password}`).toString('base64') + + const res = await fetch( + `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branch}`, + { headers: { 'Authorization': `Basic ${auth}` } } + ) + + if (!res.ok) { + throw new Error('Failed to load file') + } + + const data = await res.json() + + // Decode base64 content + let content = '' + if (data.content) { + content = Buffer.from(data.content, 'base64').toString('utf-8') + } + + return Response.json({ + content, + encoding: data.encoding, + size: data.size, + sha: data.sha + }, { headers: corsHeaders }) + } catch (e: any) { + return Response.json({ error: e.message }, { status: 500, headers: corsHeaders }) + } + } + // ===================== // API de Database Explorer // =====================