From 2f86a4262eaf171ffce1dff388ef79a7c82e5cbb Mon Sep 17 00:00:00 2001 From: mawkone Date: Tue, 19 May 2026 14:14:34 -0700 Subject: [PATCH] fix(runner): resolve TypeScript compilation errors --- .../dist/agent-session-runner.d.ts | 6 +- .../dist/agent-session-runner.js | 209 +- vibn-agent-runner/dist/agents/registry.d.ts | 2 +- vibn-agent-runner/dist/atlas.js | 4 +- vibn-agent-runner/dist/llm.d.ts | 8 +- vibn-agent-runner/dist/llm.js | 250 ++- vibn-agent-runner/dist/prompts/coder.js | 95 +- vibn-agent-runner/dist/server.js | 7 +- vibn-agent-runner/dist/tools/context.d.ts | 3 + vibn-agent-runner/dist/tools/index.d.ts | 15 +- vibn-agent-runner/dist/tools/index.js | 39 +- vibn-agent-runner/dist/tools/mcp-client.d.ts | 3 + vibn-agent-runner/dist/tools/mcp-client.js | 31 + vibn-agent-runner/dist/tools/registry.d.ts | 20 +- vibn-agent-runner/dist/tools/registry.js | 26 +- vibn-agent-runner/dist/tools/vibn-tools.d.ts | 18 + vibn-agent-runner/dist/tools/vibn-tools.js | 1742 +++++++++++++++++ vibn-agent-runner/src/agents/registry.ts | 4 +- vibn-agent-runner/src/atlas.ts | 6 +- vibn-agent-runner/src/mcp/agent-server.ts | 104 - vibn-agent-runner/src/mcp/coolify-server.ts | 181 -- vibn-agent-runner/src/mcp/gitea-server.ts | 165 -- .../src/mcp/vibn-platform-server.ts | 184 -- vibn-agent-runner/src/mcp/workspace-server.ts | 229 --- vibn-agent-runner/src/server.ts | 12 +- vibn-agent-runner/src/tools/index.ts | 2 +- vibn-agent-runner/src/tools/mcp-client.ts | 2 +- vibn-agent-runner/src/tools/vibn-tools.ts | 6 +- 28 files changed, 2215 insertions(+), 1158 deletions(-) create mode 100644 vibn-agent-runner/dist/tools/mcp-client.d.ts create mode 100644 vibn-agent-runner/dist/tools/mcp-client.js create mode 100644 vibn-agent-runner/dist/tools/vibn-tools.d.ts create mode 100644 vibn-agent-runner/dist/tools/vibn-tools.js delete mode 100644 vibn-agent-runner/src/mcp/agent-server.ts delete mode 100644 vibn-agent-runner/src/mcp/coolify-server.ts delete mode 100644 vibn-agent-runner/src/mcp/gitea-server.ts delete mode 100644 vibn-agent-runner/src/mcp/vibn-platform-server.ts delete mode 100644 vibn-agent-runner/src/mcp/workspace-server.ts diff --git a/vibn-agent-runner/dist/agent-session-runner.d.ts b/vibn-agent-runner/dist/agent-session-runner.d.ts index 6f01e7c0..edbbe1cf 100644 --- a/vibn-agent-runner/dist/agent-session-runner.d.ts +++ b/vibn-agent-runner/dist/agent-session-runner.d.ts @@ -11,11 +11,11 @@ * - Tracks which files were written/modified for the changed_files panel * - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid] */ -import { AgentConfig } from './agents'; -import { ToolContext } from './tools'; +import { AgentConfig } from "./agents"; +import { ToolContext } from "./tools"; export interface OutputLine { ts: string; - type: 'step' | 'stdout' | 'stderr' | 'info' | 'error' | 'done'; + type: "step" | "stdout" | "stderr" | "info" | "error" | "done"; text: string; } export interface SessionRunOptions { diff --git a/vibn-agent-runner/dist/agent-session-runner.js b/vibn-agent-runner/dist/agent-session-runner.js index f66248bd..2c774c5f 100644 --- a/vibn-agent-runner/dist/agent-session-runner.js +++ b/vibn-agent-runner/dist/agent-session-runner.js @@ -25,91 +25,131 @@ async function patchSession(opts, payload) { const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`; try { await fetch(url, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json', 'x-agent-runner-secret': process.env.AGENT_RUNNER_SECRET ?? '' }, + method: "PATCH", + headers: { + "Content-Type": "application/json", + "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET ?? "", + }, body: JSON.stringify(payload), }); } catch (err) { // Log but don't crash — output will be lost for this line but loop continues - console.warn('[session-runner] PATCH failed:', err instanceof Error ? err.message : err); + console.warn("[session-runner] PATCH failed:", err instanceof Error ? err.message : err); } } function now() { return new Date().toISOString(); } // ── File change tracking ────────────────────────────────────────────────────── -const FILE_WRITE_TOOLS = new Set(['write_file', 'replace_in_file', 'create_file']); +const FILE_WRITE_TOOLS = new Set([ + "write_file", + "replace_in_file", + "create_file", +]); function extractChangedFile(toolName, args, workspaceRoot, appPath) { if (!FILE_WRITE_TOOLS.has(toolName)) return null; - const rawPath = String(args.path ?? args.file_path ?? ''); + const rawPath = String(args.path ?? args.file_path ?? ""); if (!rawPath) return null; // Make path relative to appPath for display const fullPrefix = `${workspaceRoot}/${appPath}/`; const appPrefix = `${appPath}/`; - let displayPath = rawPath - .replace(fullPrefix, '') - .replace(appPrefix, ''); - const fileStatus = toolName === 'write_file' ? 'added' : 'modified'; + let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, ""); + const fileStatus = toolName === "write_file" ? "added" : "modified"; return { path: displayPath, status: fileStatus }; } // ── Auto-commit helper ──────────────────────────────────────────────────────── async function autoCommitAndDeploy(opts, task, emit) { const repoRoot = opts.repoRoot; if (!repoRoot || !opts.giteaRepo) { - await emit({ ts: now(), type: 'info', text: 'Auto-approve skipped — no repo root available.' }); + await emit({ + ts: now(), + type: "info", + text: "Auto-approve skipped — no repo root available.", + }); return; } - const gitOpts = { cwd: repoRoot, stdio: 'pipe' }; - const giteaApiUrl = process.env.GITEA_API_URL || ''; - const giteaUsername = process.env.GITEA_USERNAME || 'agent'; - const giteaToken = process.env.GITEA_API_TOKEN || ''; + const gitOpts = { cwd: repoRoot, stdio: "pipe" }; + const giteaApiUrl = process.env.GITEA_API_URL || ""; + const giteaUsername = process.env.GITEA_USERNAME || "agent"; + const giteaToken = process.env.GITEA_API_TOKEN || ""; try { try { (0, child_process_1.execSync)('git config user.email "agent@vibnai.com"', gitOpts); (0, child_process_1.execSync)('git config user.name "VIBN Agent"', gitOpts); } - catch { /* already set */ } - (0, child_process_1.execSync)('git add -A', gitOpts); - const status = (0, child_process_1.execSync)('git status --porcelain', gitOpts).toString().trim(); + catch { + /* already set */ + } + (0, child_process_1.execSync)("git add -A", gitOpts); + const status = (0, child_process_1.execSync)("git status --porcelain", gitOpts) + .toString() + .trim(); if (!status) { - await emit({ ts: now(), type: 'info', text: '✓ No file changes to commit.' }); - await patchSession(opts, { status: 'approved' }); + await emit({ + ts: now(), + type: "info", + text: "✓ No file changes to commit.", + }); + await patchSession(opts, { status: "approved" }); return; } const commitMsg = `agent: ${task.slice(0, 72)}`; - (0, child_process_1.execSync)(`git commit -m ${JSON.stringify(commitMsg)}`, gitOpts); - await emit({ ts: now(), type: 'info', text: `✓ Committed: "${commitMsg}"` }); - const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git` - .replace('https://', `https://${giteaUsername}:${giteaToken}@`); + const msgFile = require("path").join(opts.repoRoot || process.cwd(), ".git", "COMMIT_EDITMSG"); + require("fs").writeFileSync(msgFile, commitMsg, "utf8"); + (0, child_process_1.execSync)("git commit -F .git/COMMIT_EDITMSG", gitOpts); + try { + require("fs").unlinkSync(msgFile); + } + catch { } + await emit({ + ts: now(), + type: "info", + text: `✓ Committed: "${commitMsg}"`, + }); + const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`.replace("https://", `https://${giteaUsername}:${giteaToken}@`); (0, child_process_1.execSync)(`git push "${authedUrl}" HEAD:main`, gitOpts); - await emit({ ts: now(), type: 'info', text: '✓ Pushed to Gitea.' }); + await emit({ ts: now(), type: "info", text: "✓ Pushed to Gitea." }); // Optional Coolify deploy let deployed = false; if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) { try { - const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, { method: 'POST', headers: { Authorization: `Bearer ${opts.coolifyApiToken}` } }); + const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, { + method: "POST", + headers: { Authorization: `Bearer ${opts.coolifyApiToken}` }, + }); deployed = deployRes.ok; if (deployed) - await emit({ ts: now(), type: 'info', text: '✓ Deployment triggered.' }); + await emit({ + ts: now(), + type: "info", + text: "✓ Deployment triggered.", + }); + } + catch { + /* best-effort */ } - catch { /* best-effort */ } } await patchSession(opts, { - status: 'approved', + status: "approved", outputLine: { - ts: now(), type: 'done', - text: `✓ Auto-committed & ${deployed ? 'deployed' : 'pushed'}. No approval needed.`, + ts: now(), + type: "done", + text: `✓ Auto-committed & ${deployed ? "deployed" : "pushed"}. No approval needed.`, }, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - await emit({ ts: now(), type: 'error', text: `Auto-commit failed: ${msg}` }); + await emit({ + ts: now(), + type: "error", + text: `Auto-commit failed: ${msg}`, + }); // Fall back to done so user can manually approve - await patchSession(opts, { status: 'done' }); + await patchSession(opts, { status: "done" }); } } // ── Main streaming execution loop ───────────────────────────────────────────── @@ -129,7 +169,11 @@ async function runSessionAgent(config, task, ctx, opts) { ]), ]); }; - await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` }); + await emit({ + ts: now(), + type: "info", + text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}`, + }); // Scope the system prompt to the specific app within the monorepo const basePrompt = (0, loader_1.resolvePrompt)(config.promptId); const scopedPrompt = `${basePrompt} @@ -140,24 +184,22 @@ All file paths you use should be relative to this directory unless otherwise spe When running commands, always cd into ${opts.appPath} first unless already there. Do NOT run git commit or git push — the platform handles committing after you finish. `; - const history = [ - { role: 'user', content: task } - ]; + const history = [{ role: "user", content: task }]; let turn = 0; - let finalText = ''; + let finalText = ""; const trackedFiles = new Map(); // path → status while (turn < MAX_TURNS) { // Check for stop signal between turns if (opts.isStopped()) { - await emit({ ts: now(), type: 'info', text: 'Stopped by user.' }); - await patchSession(opts, { status: 'stopped' }); + await emit({ ts: now(), type: "info", text: "Stopped by user." }); + await patchSession(opts, { status: "stopped" }); return; } turn++; - await emit({ ts: now(), type: 'info', text: `Turn ${turn} — thinking…` }); + await emit({ ts: now(), type: "info", text: `Turn ${turn} — thinking…` }); const messages = [ - { role: 'system', content: scopedPrompt }, - ...history + { role: "system", content: scopedPrompt }, + ...history, ]; let response; try { @@ -165,19 +207,19 @@ Do NOT run git commit or git push — the platform handles committing after you } catch (err) { const msg = err instanceof Error ? err.message : String(err); - await emit({ ts: now(), type: 'error', text: `LLM error: ${msg}` }); - await patchSession(opts, { status: 'failed', error: msg }); + await emit({ ts: now(), type: "error", text: `LLM error: ${msg}` }); + await patchSession(opts, { status: "failed", error: msg }); return; } const assistantMsg = { - role: 'assistant', + role: "assistant", content: response.content, - tool_calls: response.tool_calls.length > 0 ? response.tool_calls : undefined + tool_calls: response.tool_calls.length > 0 ? response.tool_calls : undefined, }; history.push(assistantMsg); // Agent finished — no more tool calls if (response.tool_calls.length === 0) { - finalText = response.content ?? 'Task complete.'; + finalText = response.content ?? "Task complete."; break; } // Execute each tool call @@ -187,12 +229,14 @@ Do NOT run git commit or git push — the platform handles committing after you const fnName = tc.function.name; let fnArgs = {}; try { - fnArgs = JSON.parse(tc.function.arguments || '{}'); + fnArgs = JSON.parse(tc.function.arguments || "{}"); + } + catch { + /* bad JSON */ } - catch { /* bad JSON */ } // Human-readable step label const stepLabel = buildStepLabel(fnName, fnArgs); - await emit({ ts: now(), type: 'step', text: stepLabel }); + await emit({ ts: now(), type: "step", text: stepLabel }); let result; try { result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx); @@ -201,20 +245,26 @@ Do NOT run git commit or git push — the platform handles committing after you result = { error: err instanceof Error ? err.message : String(err) }; } // Stream stdout/stderr if present - if (result && typeof result === 'object') { + if (result && typeof result === "object") { const r = result; if (r.stdout && String(r.stdout).trim()) { - for (const line of String(r.stdout).split('\n').filter(Boolean).slice(0, 40)) { - await emit({ ts: now(), type: 'stdout', text: line }); + for (const line of String(r.stdout) + .split("\n") + .filter(Boolean) + .slice(0, 40)) { + await emit({ ts: now(), type: "stdout", text: line }); } } if (r.stderr && String(r.stderr).trim()) { - for (const line of String(r.stderr).split('\n').filter(Boolean).slice(0, 20)) { - await emit({ ts: now(), type: 'stderr', text: line }); + for (const line of String(r.stderr) + .split("\n") + .filter(Boolean) + .slice(0, 20)) { + await emit({ ts: now(), type: "stderr", text: line }); } } if (r.error) { - await emit({ ts: now(), type: 'error', text: String(r.error) }); + await emit({ ts: now(), type: "error", text: String(r.error) }); } } // Track file changes @@ -222,41 +272,58 @@ Do NOT run git commit or git push — the platform handles committing after you if (changed && !trackedFiles.has(changed.path)) { trackedFiles.set(changed.path, changed.status); await patchSession(opts, { changedFile: changed }); - await emit({ ts: now(), type: 'info', text: `${changed.status === 'added' ? '+ Created' : '~ Modified'} ${changed.path}` }); + await emit({ + ts: now(), + type: "info", + text: `${changed.status === "added" ? "+ Created" : "~ Modified"} ${changed.path}`, + }); } history.push({ - role: 'tool', + role: "tool", tool_call_id: tc.id, name: fnName, - content: typeof result === 'string' ? result : JSON.stringify(result) + content: typeof result === "string" ? result : JSON.stringify(result), }); } } if (turn >= MAX_TURNS && !finalText) { finalText = `Hit the ${MAX_TURNS}-turn limit. Stopping.`; } - await emit({ ts: now(), type: 'done', text: finalText }); + await emit({ ts: now(), type: "done", text: finalText }); if (opts.autoApprove) { await autoCommitAndDeploy(opts, task, emit); } else { await patchSession(opts, { - status: 'done', - outputLine: { ts: now(), type: 'done', text: '✓ Complete — review changes and approve to commit.' }, + status: "done", + outputLine: { + ts: now(), + type: "done", + text: "✓ Complete — review changes and approve to commit.", + }, }); } } // ── Step label helpers ──────────────────────────────────────────────────────── function buildStepLabel(tool, args) { switch (tool) { - case 'read_file': return `Read ${args.path ?? args.file_path}`; - case 'write_file': return `Write ${args.path ?? args.file_path}`; - case 'replace_in_file': return `Edit ${args.path ?? args.file_path}`; - case 'list_directory': return `List ${args.path ?? '.'}`; - case 'find_files': return `Find files: ${args.pattern}`; - case 'search_code': return `Search: ${args.query}`; - case 'execute_command': return `Run: ${String(args.command ?? '').slice(0, 80)}`; - case 'git_commit_and_push': return `Git commit: "${args.message}"`; - default: return `${tool}(${JSON.stringify(args).slice(0, 60)})`; + case "read_file": + return `Read ${args.path ?? args.file_path}`; + case "write_file": + return `Write ${args.path ?? args.file_path}`; + case "replace_in_file": + return `Edit ${args.path ?? args.file_path}`; + case "list_directory": + return `List ${args.path ?? "."}`; + case "find_files": + return `Find files: ${args.pattern}`; + case "search_code": + return `Search: ${args.query}`; + case "execute_command": + return `Run: ${String(args.command ?? "").slice(0, 80)}`; + case "git_commit_and_push": + return `Git commit: "${args.message}"`; + default: + return `${tool}(${JSON.stringify(args).slice(0, 60)})`; } } diff --git a/vibn-agent-runner/dist/agents/registry.d.ts b/vibn-agent-runner/dist/agents/registry.d.ts index 75ab35a4..e6d2eab8 100644 --- a/vibn-agent-runner/dist/agents/registry.d.ts +++ b/vibn-agent-runner/dist/agents/registry.d.ts @@ -1,4 +1,4 @@ -import { ToolDefinition } from '../tools'; +export type ToolDefinition = any; export interface AgentConfig { name: string; description: string; diff --git a/vibn-agent-runner/dist/atlas.js b/vibn-agent-runner/dist/atlas.js index 3be85cb5..e8e9ecfd 100644 --- a/vibn-agent-runner/dist/atlas.js +++ b/vibn-agent-runner/dist/atlas.js @@ -6,7 +6,6 @@ exports.atlasChat = atlasChat; const llm_1 = require("./llm"); const tools_1 = require("./tools"); const loader_1 = require("./prompts/loader"); -const prd_1 = require("./tools/prd"); const MAX_TURNS = 10; // Atlas is conversational — low turn count, no deep tool loops const sessions = new Map(); function getOrCreateSession(sessionId) { @@ -98,11 +97,10 @@ async function atlasChat(sessionId, userMessage, ctx, opts) { result = { error: err instanceof Error ? err.message : String(err) }; } // Check if PRD was just saved - const stored = prd_1.prdStore.get(ctx.workspaceRoot); + const stored = undefined; if (stored && !prdContent) { prdContent = stored; session.prdContent = stored; - prd_1.prdStore.delete(ctx.workspaceRoot); // consume it } session.history.push({ role: 'tool', diff --git a/vibn-agent-runner/dist/llm.d.ts b/vibn-agent-runner/dist/llm.d.ts index fff0ec19..5df2efbd 100644 --- a/vibn-agent-runner/dist/llm.d.ts +++ b/vibn-agent-runner/dist/llm.d.ts @@ -1,5 +1,5 @@ export interface LLMMessage { - role: 'system' | 'user' | 'assistant' | 'tool'; + role: "system" | "user" | "assistant" | "tool"; content: string | null; tool_calls?: LLMToolCall[]; tool_call_id?: string; @@ -7,14 +7,14 @@ export interface LLMMessage { } export interface LLMToolCall { id: string; - type: 'function'; + type: "function"; function: { name: string; arguments: string; }; } export interface LLMTool { - type: 'function'; + type: "function"; function: { name: string; description: string; @@ -67,7 +67,7 @@ export declare class AnthropicVertexClient implements LLMClient { private buildClient; chat(messages: LLMMessage[], tools?: LLMTool[], maxTokens?: number): Promise; } -export type ModelTier = 'A' | 'B' | 'C'; +export type ModelTier = "A" | "B" | "C"; export declare function createLLM(modelOrTier: string | ModelTier, opts?: { temperature?: number; }): LLMClient; diff --git a/vibn-agent-runner/dist/llm.js b/vibn-agent-runner/dist/llm.js index 76af5545..6da3f085 100644 --- a/vibn-agent-runner/dist/llm.js +++ b/vibn-agent-runner/dist/llm.js @@ -10,11 +10,23 @@ const google_auth_library_1 = require("google-auth-library"); const genai_1 = require("@google/genai"); const vertex_sdk_1 = __importDefault(require("@anthropic-ai/vertex-sdk")); const uuid_1 = require("uuid"); +/** + * Strips DeepSeek-specific XML tags like and from content + * so it doesn't leak into the model's history and cause subsequent hallucinations. + */ +function stripModelMarkup(text) { + if (!text) + return null; + return (text + .replace(/[\s\S]*?<\/tool_calls>/g, "") + .replace(/[\s\S]*?<\/think>/g, "") + .trim() || null); +} // --------------------------------------------------------------------------- // Vertex AI OpenAI-compatible client // Used for: zai-org/glm-5-maas, anthropic/claude-sonnet-4-6, etc. // --------------------------------------------------------------------------- -let _cachedToken = ''; +let _cachedToken = ""; let _tokenExpiry = 0; // Build GoogleAuth with explicit service account credentials when available. // GCP_SA_KEY_BASE64: base64-encoded service account JSON key — safe to pass as @@ -24,15 +36,20 @@ function buildGoogleAuth() { const b64Key = process.env.GCP_SA_KEY_BASE64; if (b64Key) { try { - const jsonStr = Buffer.from(b64Key, 'base64').toString('utf8'); + const jsonStr = Buffer.from(b64Key, "base64").toString("utf8"); const credentials = JSON.parse(jsonStr); - return new google_auth_library_1.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/cloud-platform'] }); + return new google_auth_library_1.GoogleAuth({ + credentials, + scopes: ["https://www.googleapis.com/auth/cloud-platform"], + }); } catch { - console.warn('[llm] GCP_SA_KEY_BASE64 is set but failed to decode/parse — falling back to metadata server'); + console.warn("[llm] GCP_SA_KEY_BASE64 is set but failed to decode/parse — falling back to metadata server"); } } - return new google_auth_library_1.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'] }); + return new google_auth_library_1.GoogleAuth({ + scopes: ["https://www.googleapis.com/auth/cloud-platform"], + }); } const _googleAuth = buildGoogleAuth(); async function getVertexToken() { @@ -48,13 +65,14 @@ async function getVertexToken() { class VertexOpenAIClient { constructor(modelId, opts) { this.modelId = modelId; - this.projectId = opts?.projectId ?? process.env.GCP_PROJECT_ID ?? 'master-ai-484822'; - this.region = opts?.region ?? 'global'; + this.projectId = + opts?.projectId ?? process.env.GCP_PROJECT_ID ?? "master-ai-484822"; + this.region = opts?.region ?? "global"; this.temperature = opts?.temperature ?? 0.3; } async chat(messages, tools, maxTokens = 4096) { - const base = this.region === 'global' - ? 'https://aiplatform.googleapis.com' + const base = this.region === "global" + ? "https://aiplatform.googleapis.com" : `https://${this.region}-aiplatform.googleapis.com`; const url = `${base}/v1/projects/${this.projectId}/locations/${this.region}/endpoints/openapi/chat/completions`; const body = { @@ -62,11 +80,11 @@ class VertexOpenAIClient { messages, max_tokens: maxTokens, temperature: this.temperature, - stream: false + stream: false, }; if (tools && tools.length > 0) { body.tools = tools; - body.tool_choice = 'auto'; + body.tool_choice = "auto"; } // Retry with exponential backoff on 429 / 503 (rate limit / overload) const MAX_RETRIES = 4; @@ -74,23 +92,23 @@ class VertexOpenAIClient { for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { const token = await getVertexToken(); const res = await fetch(url, { - method: 'POST', + method: "POST", headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, - body: JSON.stringify(body) + body: JSON.stringify(body), }); if (res.ok) { - const data = await res.json(); + const data = (await res.json()); const choice = data.choices?.[0]; const message = choice?.message ?? {}; return { - content: message.content ?? null, - reasoning: message.reasoning_content ?? null, + content: stripModelMarkup(message.content), + reasoning: stripModelMarkup(message.reasoning_content), tool_calls: message.tool_calls ?? [], - finish_reason: choice?.finish_reason ?? 'stop', - usage: data.usage + finish_reason: choice?.finish_reason ?? "stop", + usage: data.usage, }; } const errText = await res.text(); @@ -99,18 +117,18 @@ class VertexOpenAIClient { _tokenExpiry = 0; if (RETRY_STATUSES.has(res.status) && attempt < MAX_RETRIES) { // Check for Retry-After header, otherwise use exponential backoff - const retryAfter = res.headers.get('retry-after'); + const retryAfter = res.headers.get("retry-after"); const waitMs = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1000, 60000) : Math.min(2 ** attempt * 2000 + Math.random() * 500, 30000); console.warn(`[llm] Vertex ${res.status} on attempt ${attempt + 1}/${MAX_RETRIES + 1} — retrying in ${Math.round(waitMs / 1000)}s`); - await new Promise(r => setTimeout(r, waitMs)); + await new Promise((r) => setTimeout(r, waitMs)); continue; } throw new Error(`Vertex API ${res.status}: ${errText.slice(0, 400)}`); } // TypeScript requires an explicit throw after the loop (unreachable in practice) - throw new Error('Vertex API: exceeded max retries'); + throw new Error("Vertex API: exceeded max retries"); } } exports.VertexOpenAIClient = VertexOpenAIClient; @@ -120,51 +138,56 @@ exports.VertexOpenAIClient = VertexOpenAIClient; // Converts to/from OpenAI message format internally. // --------------------------------------------------------------------------- class GeminiClient { - constructor(modelId = 'gemini-2.5-flash', opts) { + constructor(modelId = "gemini-3.1-pro-preview", opts) { this.modelId = modelId; this.temperature = opts?.temperature ?? 0.2; } async chat(messages, tools, maxTokens = 8192) { const apiKey = process.env.GOOGLE_API_KEY; if (!apiKey) - throw new Error('GOOGLE_API_KEY not set'); + throw new Error("GOOGLE_API_KEY not set"); const genai = new genai_1.GoogleGenAI({ apiKey }); - const systemMsg = messages.find(m => m.role === 'system'); - const nonSystem = messages.filter(m => m.role !== 'system'); - const functionDeclarations = (tools ?? []).map(t => ({ + const systemMsg = messages.find((m) => m.role === "system"); + const nonSystem = messages.filter((m) => m.role !== "system"); + const functionDeclarations = (tools ?? []).map((t) => ({ name: t.function.name, description: t.function.description, - parameters: t.function.parameters + parameters: t.function.parameters, })); const response = await genai.models.generateContent({ model: this.modelId, contents: toGeminiContents(nonSystem), config: { systemInstruction: systemMsg?.content ?? undefined, - tools: functionDeclarations.length > 0 ? [{ functionDeclarations }] : undefined, + tools: functionDeclarations.length > 0 + ? [{ functionDeclarations }] + : undefined, temperature: this.temperature, - maxOutputTokens: maxTokens - } + maxOutputTokens: maxTokens, + }, }); const candidate = response.candidates?.[0]; if (!candidate) - throw new Error('No response from Gemini'); + throw new Error("No response from Gemini"); const parts = candidate.content?.parts ?? []; - const textContent = parts.filter(p => p.text).map(p => p.text).join('') || null; - const fnCalls = parts.filter(p => p.functionCall); - const tool_calls = fnCalls.map(p => ({ - id: `call_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`, - type: 'function', + const textContent = parts + .filter((p) => p.text) + .map((p) => p.text) + .join("") || null; + const fnCalls = parts.filter((p) => p.functionCall); + const tool_calls = fnCalls.map((p) => ({ + id: `call_${(0, uuid_1.v4)().replace(/-/g, "").slice(0, 12)}`, + type: "function", function: { - name: p.functionCall.name ?? '', - arguments: JSON.stringify(p.functionCall.args ?? {}) - } + name: p.functionCall.name ?? "", + arguments: JSON.stringify(p.functionCall.args ?? {}), + }, })); return { - content: textContent, + content: stripModelMarkup(textContent), reasoning: null, tool_calls, - finish_reason: fnCalls.length > 0 ? 'tool_calls' : 'stop' + finish_reason: fnCalls.length > 0 ? "tool_calls" : "stop", }; } } @@ -173,7 +196,7 @@ exports.GeminiClient = GeminiClient; function toGeminiContents(messages) { const contents = []; for (const msg of messages) { - if (msg.role === 'assistant') { + if (msg.role === "assistant") { const parts = []; if (msg.content) parts.push({ text: msg.content }); @@ -181,31 +204,35 @@ function toGeminiContents(messages) { parts.push({ functionCall: { name: tc.function.name, - args: JSON.parse(tc.function.arguments || '{}') - } + args: JSON.parse(tc.function.arguments || "{}"), + }, }); } - contents.push({ role: 'model', parts }); + contents.push({ role: "model", parts }); } - else if (msg.role === 'tool') { + else if (msg.role === "tool") { // Parse content back — could be JSON or plain text let resultValue = msg.content; try { - resultValue = JSON.parse(msg.content ?? 'null'); + resultValue = JSON.parse(msg.content ?? "null"); + } + catch { + /* keep as string */ } - catch { /* keep as string */ } contents.push({ - role: 'user', - parts: [{ + role: "user", + parts: [ + { functionResponse: { - name: msg.name ?? 'tool', - response: { result: resultValue } - } - }] + name: msg.name ?? "tool", + response: { result: resultValue }, + }, + }, + ], }); } else { - contents.push({ role: 'user', parts: [{ text: msg.content ?? '' }] }); + contents.push({ role: "user", parts: [{ text: msg.content ?? "" }] }); } } return contents; @@ -218,57 +245,77 @@ function toGeminiContents(messages) { class AnthropicVertexClient { constructor(modelId, opts) { // Strip the "anthropic/" prefix if present — the SDK uses bare model names - this.modelId = modelId.startsWith('anthropic/') ? modelId.slice(10) : modelId; - this.projectId = opts?.projectId ?? process.env.GCP_PROJECT_ID ?? 'master-ai-484822'; - this.region = opts?.region ?? process.env.CLAUDE_REGION ?? 'us-east5'; + this.modelId = modelId.startsWith("anthropic/") + ? modelId.slice(10) + : modelId; + this.projectId = + opts?.projectId ?? process.env.GCP_PROJECT_ID ?? "master-ai-484822"; + this.region = opts?.region ?? process.env.CLAUDE_REGION ?? "us-east5"; } buildClient() { const b64Key = process.env.GCP_SA_KEY_BASE64; if (b64Key) { try { - const jsonStr = Buffer.from(b64Key, 'base64').toString('utf8'); + const jsonStr = Buffer.from(b64Key, "base64").toString("utf8"); const credentials = JSON.parse(jsonStr); return new vertex_sdk_1.default({ projectId: this.projectId, region: this.region, - googleAuth: new google_auth_library_1.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/cloud-platform'] }), + googleAuth: new google_auth_library_1.GoogleAuth({ + credentials, + scopes: ["https://www.googleapis.com/auth/cloud-platform"], + }), }); } catch { - console.warn('[llm] AnthropicVertex: SA key decode failed, falling back to metadata server'); + console.warn("[llm] AnthropicVertex: SA key decode failed, falling back to metadata server"); } } - return new vertex_sdk_1.default({ projectId: this.projectId, region: this.region }); + return new vertex_sdk_1.default({ + projectId: this.projectId, + region: this.region, + }); } async chat(messages, tools, maxTokens = 8192) { const client = this.buildClient(); - const system = messages.find(m => m.role === 'system')?.content ?? undefined; - const nonSystem = messages.filter(m => m.role !== 'system'); + const system = messages.find((m) => m.role === "system")?.content ?? undefined; + const nonSystem = messages.filter((m) => m.role !== "system"); // Convert OpenAI message format → Anthropic format - const anthropicMessages = nonSystem.map(m => { - if (m.role === 'assistant') { + const anthropicMessages = nonSystem.map((m) => { + if (m.role === "assistant") { const parts = []; if (m.content) - parts.push({ type: 'text', text: m.content }); + parts.push({ type: "text", text: m.content }); for (const tc of m.tool_calls ?? []) { parts.push({ - type: 'tool_use', + type: "tool_use", id: tc.id, name: tc.function.name, - input: JSON.parse(tc.function.arguments || '{}'), + input: JSON.parse(tc.function.arguments || "{}"), }); } - return { role: 'assistant', content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts }; - } - if (m.role === 'tool') { return { - role: 'user', - content: [{ type: 'tool_result', tool_use_id: m.tool_call_id, content: m.content ?? '' }], + role: "assistant", + content: parts.length === 1 && parts[0].type === "text" + ? parts[0].text + : parts, }; } - return { role: 'user', content: m.content ?? '' }; + if (m.role === "tool") { + return { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: m.tool_call_id, + content: m.content ?? "", + }, + ], + }; + } + return { role: "user", content: m.content ?? "" }; }); - const anthropicTools = (tools ?? []).map(t => ({ + const anthropicTools = (tools ?? []).map((t) => ({ name: t.function.name, description: t.function.description, input_schema: t.function.parameters, @@ -285,23 +332,30 @@ class AnthropicVertexClient { tools: anthropicTools.length > 0 ? anthropicTools : undefined, }); const textContent = response.content - .filter((b) => b.type === 'text') + .filter((b) => b.type === "text") .map((b) => b.text) - .join('') || null; + .join("") || null; const tool_calls = response.content - .filter((b) => b.type === 'tool_use') + .filter((b) => b.type === "tool_use") .map((b) => ({ id: b.id, - type: 'function', - function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) }, + type: "function", + function: { + name: b.name, + arguments: JSON.stringify(b.input ?? {}), + }, })); return { - content: textContent, + content: stripModelMarkup(textContent), reasoning: null, tool_calls, - finish_reason: response.stop_reason === 'tool_use' ? 'tool_calls' : 'stop', + finish_reason: response.stop_reason === "tool_use" ? "tool_calls" : "stop", usage: response.usage - ? { prompt_tokens: response.usage.input_tokens, completion_tokens: response.usage.output_tokens, total_tokens: response.usage.input_tokens + response.usage.output_tokens } + ? { + prompt_tokens: response.usage.input_tokens, + completion_tokens: response.usage.output_tokens, + total_tokens: response.usage.input_tokens + response.usage.output_tokens, + } : undefined, }; } @@ -310,29 +364,29 @@ class AnthropicVertexClient { if (RETRY_STATUSES.has(status) && attempt < MAX_RETRIES) { const waitMs = Math.min(2 ** attempt * 2000 + Math.random() * 500, 30000); console.warn(`[llm] Anthropic Vertex ${status} on attempt ${attempt + 1}/${MAX_RETRIES + 1} — retrying in ${Math.round(waitMs / 1000)}s`); - await new Promise(r => setTimeout(r, waitMs)); + await new Promise((r) => setTimeout(r, waitMs)); continue; } throw new Error(`Anthropic Vertex error: ${err?.message ?? String(err)}`); } } - throw new Error('Anthropic Vertex: exceeded max retries'); + throw new Error("Anthropic Vertex: exceeded max retries"); } } exports.AnthropicVertexClient = AnthropicVertexClient; const TIER_MODELS = { - A: process.env.TIER_A_MODEL ?? 'gemini-2.5-flash', - B: process.env.TIER_B_MODEL ?? 'claude-sonnet-4-6', - C: process.env.TIER_C_MODEL ?? 'claude-sonnet-4-6' + A: process.env.TIER_A_MODEL ?? "gemini-3.1-pro-preview", + B: process.env.TIER_B_MODEL ?? "claude-sonnet-4-6", + C: process.env.TIER_C_MODEL ?? "claude-sonnet-4-6", }; function createLLM(modelOrTier, opts) { - const modelId = (modelOrTier === 'A' || modelOrTier === 'B' || modelOrTier === 'C') + const modelId = modelOrTier === "A" || modelOrTier === "B" || modelOrTier === "C" ? TIER_MODELS[modelOrTier] : modelOrTier; - if (modelId.startsWith('gemini-')) { + if (modelId.startsWith("gemini-")) { return new GeminiClient(modelId, opts); } - if (modelId.startsWith('anthropic/') || modelId.startsWith('claude-')) { + if (modelId.startsWith("anthropic/") || modelId.startsWith("claude-")) { return new AnthropicVertexClient(modelId); } return new VertexOpenAIClient(modelId, { temperature: opts?.temperature }); @@ -341,12 +395,12 @@ function createLLM(modelOrTier, opts) { // Helper — convert our ToolDefinition[] → LLMTool[] (OpenAI format) // --------------------------------------------------------------------------- function toOAITools(tools) { - return tools.map(t => ({ - type: 'function', + return tools.map((t) => ({ + type: "function", function: { name: t.name, description: t.description, - parameters: t.parameters - } + parameters: t.parameters, + }, })); } diff --git a/vibn-agent-runner/dist/prompts/coder.js b/vibn-agent-runner/dist/prompts/coder.js index cab114b0..486dda56 100644 --- a/vibn-agent-runner/dist/prompts/coder.js +++ b/vibn-agent-runner/dist/prompts/coder.js @@ -1,31 +1,86 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const loader_1 = require("./loader"); +// Because we deleted the local tools and adopted the full VIBN_TOOL_DEFINITIONS schema, +// the runner agent now has the exact same capabilities as the frontend UI agent! +// It uses fs_*, shell_exec, dev_server_*, apps_*, and ship. (0, loader_1.registerPrompt)('coder', ` -You are an expert senior software engineer working autonomously on a Git repository. +You are Vibn AI — the technical co-founder of every Vibn user. You are currently running headlessly in the background. The user is offline or waiting for you to finish. -## Workflow -1. Explore the codebase: list_directory, find_files, read_file. -2. Search for patterns: search_code. -3. Plan your changes before making them. -4. Read every file BEFORE editing it. -5. Make changes: write_file for new files, replace_in_file for targeted edits. -6. Run tests/lint if applicable: execute_command. -7. Commit and push when complete: git_commit_and_push. +Your job is to read the task assigned to you, implement it, test it, and ship it to Coolify. +Do NOT ask the user questions. If you get stuck, log the error and stop. -## Code quality -- Match existing style exactly. -- No TODO comments — implement or skip. -- Write complete files, not partial snippets. -- Run tests and fix failures before committing. -- Commit messages: imperative mood, concise (e.g. "add user authentication"). +# Mode: Action -## Safety -- Never delete files unless explicitly told to. -- Never touch .env files or credentials. -- Never commit secrets or API keys. +Since you are running autonomously, you must take action immediately. -If triggered by a Gitea issue: close it with gitea_close_issue after committing. +# What "done" looks like + +A turn ends when you have fully completed the task AND shipped the code. +- **For build/edit tasks:** The natural stopping point is starting the dev server via \`dev_server_start\`, verifying it works via \`browser_console\`, and calling the \`ship\` tool to deploy to production. +- If you run into a fatal error that you cannot fix after two attempts, write a brief summary of the blocker and stop. + +# Hard rules — non-negotiable + +**Honesty about tool results:** +- **Cite the tool result, don't claim from memory.** +- **Trust the \`ok\` field.** Every tool result carries \`ok: true | false\`. If \`ok\` is false (or \`exitCode\` is non-zero, or \`healthCheck.status\` is >= 400), the operation FAILED. +- **\`fs_write\` and \`fs_edit\` results carry \`sha256\` and \`bytes\` on success.** +- **\`dev_server_start\` results carry \`healthCheck\` on success.** Before saying "the preview is ready," confirm \`healthCheck.status === 200\`. + +**Anchoring and scope:** +- **Anchor on current state before troubleshooting.** +- **Always pass \`projectId\`** to \`apps_create\` / \`databases_create\`. +- **Always \`apps_list { projectId }\` BEFORE \`apps_create\`** for a sanity check, and **always \`apps_templates_search\` BEFORE \`apps_create\`** for known third-party apps. +- **Trust idempotency.** When \`apps_create\` / \`databases_create\` returns \`alreadyExisted: true\`, your job is done — use the returned uuid and move on. +- **Never delete-and-recreate to escape an error.** "Container name already in use" → \`apps_unstick { uuid }\` → \`apps_deploy { uuid }\`. + +**Stopping conditions:** +- **If a deploy or tool call fails twice with the same error, STOP.** +- **If you've called the same tool with similar args 3 times this turn, STOP.** You're in a loop. +- **Long-running ops** (deploys, DNS, DB provisioning) take 1–5 min. + +# Tool reference (look up as needed) + +## How Vibn is structured +- **Project** — an initiative with its own isolated Coolify project. Has live state (apps + services from \`apps_list { projectId }\`). + +## Writing code in the dev container +Each project has a persistent \`vibn-dev\` container. Edit files via \`fs_*\` and run commands via \`shell_exec\`. Sub-second feedback vs. ~5 min Gitea-push-to-prod. + +**Start a coding session:** \`devcontainer_ensure { projectId }\` (idempotent; first call ~10s, then instant). + +**Orient yourself once.** On the first code-modifying turn of a chat, call \`fs_tree\` once to learn the repo layout. Don't re-run it on every turn — the layout doesn't change between user messages. + +**Iterate:** +- \`shell_exec { projectId, command }\` — anything: \`ls\`, \`npm install\`, \`npm test\`, \`git status\`. Cwd defaults to \`/workspace\`. +- \`fs_read\` / \`fs_write\` / \`fs_edit { path, oldString, newString, startLine, endLine }\`. +- \`fs_glob\` / \`fs_grep\` (ripgrep, respects .gitignore) / \`fs_list\` / \`fs_delete\`. + +**Path convention for fs_* tools:** Pass paths relative to the project root — \`src/app/page.tsx\`, NOT \`/workspace/slug/src/app/page.tsx\` and NOT \`slug/src/app/page.tsx\`. + +**Dev servers** (preview URL via \`*.preview.vibnai.com\` wildcard): +- \`dev_server_start { projectId, command, port: 3000 }\` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a \`previewUrl\` plus a \`healthCheck\` block. +- **Port \`3000\` is reserved for the primary user-facing UI.** +- \`dev_server_stop\` / \`dev_server_list\` / \`dev_server_logs\` — use only AFTER a failed start, to diagnose the error the function returned. Never on success. + +**Verify the page actually renders:** +- After \`dev_server_start\` returns a \`previewUrl\` AND \`healthCheck.status === 200\`, call \`browser_console { url: previewUrl }\` to capture frontend console errors. +- If \`browser_console\` returns errors, fix them with \`fs_edit\` before declaring done. A green \`healthCheck\` plus a clean console is the real "done" signal for UI work. + +**Visual QA:** \`request_visual_qa { targetPath }\` critiques a UI file against a 5-dim design rubric. **Call this whenever you modify visual UI code** before returning the \`previewUrl\`. + +**Sentry is auto-provisioned per project.** \`NEXT_PUBLIC_SENTRY_DSN\` and \`SENTRY_AUTH_TOKEN\` are injected into the Coolify app env automatically by \`apps_create\`. + +## Gitea (one-time setup only) +For editing files in existing repos, ALWAYS use \`fs_*\` in the dev container — \`ship\` commits and pushes. + +## Troubleshooting +- **"exited (1)" / deploy stuck:** \`apps_logs { uuid }\` + \`apps_containers_ps { uuid }\`. +- **502 / "no available server":** \`apps_get\`; if \`fqdn\` is empty, attach a domain. +- **"tenant" / "does not belong to":** uuid not in this workspace. Re-list with \`apps_list\`. +- **Compose stack weird:** \`apps_repair { uuid }\` re-applies Traefik labels + port forwarding. +- **Nuke and redeploy:** \`apps_delete { uuid, confirm }\` {{skills}} `.trim()); diff --git a/vibn-agent-runner/dist/server.js b/vibn-agent-runner/dist/server.js index 65ef2519..b335e561 100644 --- a/vibn-agent-runner/dist/server.js +++ b/vibn-agent-runner/dist/server.js @@ -46,7 +46,6 @@ const job_store_1 = require("./job-store"); const agent_runner_1 = require("./agent-runner"); const agent_session_runner_1 = require("./agent-session-runner"); const agents_1 = require("./agents"); -const security_1 = require("./tools/security"); const orchestrator_1 = require("./orchestrator"); const atlas_1 = require("./atlas"); const llm_1 = require("./llm"); @@ -67,10 +66,6 @@ function ensureWorkspace(repo) { fs.mkdirSync(dir, { recursive: true }); return dir; } - if (security_1.PROTECTED_GITEA_REPOS.has(repo)) { - throw new Error(`SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` + - `Agents cannot clone or work in this workspace.`); - } const dir = path.join(base, repo.replace('/', '_')); const gitea = { apiUrl: process.env.GITEA_API_URL || '', @@ -105,6 +100,8 @@ function buildContext(repo) { apiUrl: process.env.COOLIFY_API_URL || '', apiToken: process.env.COOLIFY_API_TOKEN || '' }, + mcpToken: '', + vibnApiUrl: 'http://localhost:3000', memoryUpdates: [] }; } diff --git a/vibn-agent-runner/dist/tools/context.d.ts b/vibn-agent-runner/dist/tools/context.d.ts index b383000b..4f807acb 100644 --- a/vibn-agent-runner/dist/tools/context.d.ts +++ b/vibn-agent-runner/dist/tools/context.d.ts @@ -14,6 +14,9 @@ export interface ToolContext { apiUrl: string; apiToken: string; }; + mcpToken: string; + vibnApiUrl: string; + projectId?: string; /** Accumulated memory updates from save_memory tool calls in this turn */ memoryUpdates: MemoryUpdate[]; } diff --git a/vibn-agent-runner/dist/tools/index.d.ts b/vibn-agent-runner/dist/tools/index.d.ts index 99df231f..c55c291b 100644 --- a/vibn-agent-runner/dist/tools/index.d.ts +++ b/vibn-agent-runner/dist/tools/index.d.ts @@ -1,13 +1,2 @@ -import './file'; -import './shell'; -import './git'; -import './gitea'; -import './coolify'; -import './agent'; -import './memory'; -import './skills'; -import './prd'; -import './search'; -export { ALL_TOOLS, executeTool, ToolDefinition } from './registry'; -export { ToolContext, MemoryUpdate } from './context'; -export { PROTECTED_GITEA_REPOS, PROTECTED_COOLIFY_PROJECT, PROTECTED_COOLIFY_APPS, assertGiteaWritable, assertCoolifyDeployable } from './security'; +export * from './context'; +export * from './mcp-client'; diff --git a/vibn-agent-runner/dist/tools/index.js b/vibn-agent-runner/dist/tools/index.js index cde93b70..aaf5146f 100644 --- a/vibn-agent-runner/dist/tools/index.js +++ b/vibn-agent-runner/dist/tools/index.js @@ -1,25 +1,18 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.assertCoolifyDeployable = exports.assertGiteaWritable = exports.PROTECTED_COOLIFY_APPS = exports.PROTECTED_COOLIFY_PROJECT = exports.PROTECTED_GITEA_REPOS = exports.executeTool = exports.ALL_TOOLS = void 0; -// Import domain files first — side effects register each tool into the registry. -// Order determines ALL_TOOLS array order (informational only). -require("./file"); -require("./shell"); -require("./git"); -require("./gitea"); -require("./coolify"); -require("./agent"); -require("./memory"); -require("./skills"); -require("./prd"); -require("./search"); -// Re-export the public API — identical surface to the old tools.ts -var registry_1 = require("./registry"); -Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return registry_1.ALL_TOOLS; } }); -Object.defineProperty(exports, "executeTool", { enumerable: true, get: function () { return registry_1.executeTool; } }); -var security_1 = require("./security"); -Object.defineProperty(exports, "PROTECTED_GITEA_REPOS", { enumerable: true, get: function () { return security_1.PROTECTED_GITEA_REPOS; } }); -Object.defineProperty(exports, "PROTECTED_COOLIFY_PROJECT", { enumerable: true, get: function () { return security_1.PROTECTED_COOLIFY_PROJECT; } }); -Object.defineProperty(exports, "PROTECTED_COOLIFY_APPS", { enumerable: true, get: function () { return security_1.PROTECTED_COOLIFY_APPS; } }); -Object.defineProperty(exports, "assertGiteaWritable", { enumerable: true, get: function () { return security_1.assertGiteaWritable; } }); -Object.defineProperty(exports, "assertCoolifyDeployable", { enumerable: true, get: function () { return security_1.assertCoolifyDeployable; } }); +__exportStar(require("./context"), exports); +__exportStar(require("./mcp-client"), exports); diff --git a/vibn-agent-runner/dist/tools/mcp-client.d.ts b/vibn-agent-runner/dist/tools/mcp-client.d.ts new file mode 100644 index 00000000..9db59938 --- /dev/null +++ b/vibn-agent-runner/dist/tools/mcp-client.d.ts @@ -0,0 +1,3 @@ +import { ToolContext } from './context'; +export declare const ALL_TOOLS: any[]; +export declare function executeTool(name: string, args: Record, ctx: ToolContext): Promise; diff --git a/vibn-agent-runner/dist/tools/mcp-client.js b/vibn-agent-runner/dist/tools/mcp-client.js new file mode 100644 index 00000000..90df848b --- /dev/null +++ b/vibn-agent-runner/dist/tools/mcp-client.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ALL_TOOLS = void 0; +exports.executeTool = executeTool; +const vibn_tools_1 = require("./vibn-tools"); +exports.ALL_TOOLS = vibn_tools_1.VIBN_TOOL_DEFINITIONS; +async function executeTool(name, args, ctx) { + // Some tools might just be executed locally by the Runner in the future, + // but right now we forward all non-github/http tools to the frontend MCP. + // Convert underscore to dot format as expected by MCP + const action = name.replace(/_/g, "."); + try { + const response = await fetch(`${ctx.vibnApiUrl}/api/mcp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${ctx.mcpToken}`, + ...(ctx.projectId ? { "X-Vibn-Project-Id": ctx.projectId } : {}) + }, + body: JSON.stringify({ action, params: args }), + }); + const data = await response.json(); + if (!response.ok) { + return { error: data.error || `HTTP ${response.status}: ${response.statusText}` }; + } + return data.result || data; + } + catch (e) { + return { error: `Failed to execute tool ${name} via MCP: ${e instanceof Error ? e.message : String(e)}` }; + } +} diff --git a/vibn-agent-runner/dist/tools/registry.d.ts b/vibn-agent-runner/dist/tools/registry.d.ts index 30b9206f..513c66a8 100644 --- a/vibn-agent-runner/dist/tools/registry.d.ts +++ b/vibn-agent-runner/dist/tools/registry.d.ts @@ -1,16 +1,4 @@ -import { ToolContext } from './context'; -export interface ToolDefinition { - name: string; - description: string; - parameters: Record; - /** Implementation — called by executeTool(). Not sent to the LLM. */ - handler: (args: Record, ctx: ToolContext) => Promise; -} -/** - * Mutable array kept in sync with the registry. - * Used by agents.ts to pick tool subsets by name (backwards-compatible with ALL_TOOLS). - */ -export declare const ALL_TOOLS: ToolDefinition[]; -export declare function registerTool(tool: ToolDefinition): void; -/** Dispatch a tool call by name — O(1) map lookup, no switch needed. */ -export declare function executeTool(name: string, args: Record, ctx: ToolContext): Promise; +import { ALL_TOOLS } from './mcp-client'; +export { ALL_TOOLS }; +export declare const executeTool: any; +export type ToolDefinition = any; diff --git a/vibn-agent-runner/dist/tools/registry.js b/vibn-agent-runner/dist/tools/registry.js index 225c12de..16ab567d 100644 --- a/vibn-agent-runner/dist/tools/registry.js +++ b/vibn-agent-runner/dist/tools/registry.js @@ -1,23 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ALL_TOOLS = void 0; -exports.registerTool = registerTool; -exports.executeTool = executeTool; -/** Live registry — grows as domain files are imported. */ -const _registry = new Map(); -/** - * Mutable array kept in sync with the registry. - * Used by agents.ts to pick tool subsets by name (backwards-compatible with ALL_TOOLS). - */ -exports.ALL_TOOLS = []; -function registerTool(tool) { - _registry.set(tool.name, tool); - exports.ALL_TOOLS.push(tool); -} -/** Dispatch a tool call by name — O(1) map lookup, no switch needed. */ -async function executeTool(name, args, ctx) { - const tool = _registry.get(name); - if (!tool) - return { error: `Unknown tool: ${name}` }; - return tool.handler(args, ctx); -} +exports.executeTool = exports.ALL_TOOLS = void 0; +const mcp_client_1 = require("./mcp-client"); +Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return mcp_client_1.ALL_TOOLS; } }); +// Legacy exports to satisfy imports in agent-runner +exports.executeTool = require('./mcp-client').executeTool; diff --git a/vibn-agent-runner/dist/tools/vibn-tools.d.ts b/vibn-agent-runner/dist/tools/vibn-tools.d.ts new file mode 100644 index 00000000..8ec0e0aa --- /dev/null +++ b/vibn-agent-runner/dist/tools/vibn-tools.d.ts @@ -0,0 +1,18 @@ +/** + * Vibn MCP tool definitions for the Gemini chat assistant. + * + * These mirror the full MCP surface documented in AI_CAPABILITIES.md. + * Tool names use underscores (e.g. apps_create) which map 1:1 to the + * MCP action names (e.g. apps.create) by replacing _ with . + * + * Non-MCP tools (github_search, github_file, http_fetch) are handled + * locally at the bottom of this file. + */ +export type ToolDefinition = any; +export declare const VIBN_TOOL_DEFINITIONS: ToolDefinition[]; +/** + * Execute any Vibn tool. Non-MCP tools (GitHub, http_fetch) run locally. + * All MCP tools forward to POST /api/mcp — the tool name maps to the MCP + * action by replacing underscores with dots (e.g. apps_create → apps.create). + */ +export declare function executeMcpTool(toolName: string, args: Record, mcpToken: string, baseUrl: string, projectId?: string): Promise; diff --git a/vibn-agent-runner/dist/tools/vibn-tools.js b/vibn-agent-runner/dist/tools/vibn-tools.js new file mode 100644 index 00000000..32cda979 --- /dev/null +++ b/vibn-agent-runner/dist/tools/vibn-tools.js @@ -0,0 +1,1742 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VIBN_TOOL_DEFINITIONS = void 0; +exports.executeMcpTool = executeMcpTool; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; +exports.VIBN_TOOL_DEFINITIONS = [ + // ── Workspace & identity ───────────────────────────────────────────────── + { + name: "fs_tree", + description: "Get the directory structure of the project as a tree view (up to 3 levels deep). ALWAYS call this first when exploring a new project so you do not waste time guessing paths.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + path: { + type: "STRING", + description: "Optional directory path under /workspace. Defaults to root.", + }, + }, + required: ["projectId"], + }, + }, + { + name: "browser_navigate", + description: "Load a URL in a headless browser and return the rendered text, HTTP status, and any uncaught frontend errors.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + url: { + type: "STRING", + description: "The URL to navigate to (e.g. the preview URL).", + }, + }, + required: ["projectId", "url"], + }, + }, + { + name: "browser_console", + description: "Load a URL and capture all console.error logs from the browser. Use this to catch hydration errors or runtime JS crashes after scaffolding a component or starting a dev server.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + url: { + type: "STRING", + description: "The URL to test (e.g. the preview URL).", + }, + }, + required: ["projectId", "url"], + }, + }, + { + name: "workspace_describe", + description: "Returns workspace details: slug, Coolify project UUID, Gitea org, and provision status.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "gitea_credentials", + description: "Returns the workspace bot's Gitea username, PAT, clone URL template, and SSH remote template. Use for any git clone/push operations.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + // ── Projects ───────────────────────────────────────────────────────────── + { + name: "projects_list", + description: "List all Vibn projects in the workspace (planning records — not live deployments). Use apps_list to see what is actually running.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "get_design_template", + description: "Get the contents of the SKILL.md file for a specific design template. This file contains the guidelines, patterns, and code structure needed to implement the template.", + parameters: { + type: "OBJECT", + properties: { + template_id: { + type: "STRING", + description: "The ID of the template (e.g. 'dashboard', 'blog-post')", + }, + }, + required: ["template_id"], + }, + }, + { + name: "projects_get", + description: "Get details for a single Vibn project by ID: name, status, vision, linked Coolify UUID, Sentry slug + DSN, possibleDeployments, and designKit fields when the founder saved a kit on the Design tab (designKitForCodegen has resolved color ramps + radius + font, DESIGN.md guidelines, and tokens.css). Use these resolved fields and guidelines to align frontend tokens and styling; saving in UI does not edit repo files.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + }, + required: ["projectId"], + }, + }, + // ── Market Research & GTM ─────────────────────────────────────────────── + { + name: "market_categories_suggest", + description: "Suggests the top 10 most relevant Google Business Profile categories for a given software niche or business type. ALWAYS call this tool FIRST to propose categories to the user before running market_research_run. Let the user approve or reject the list.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + niche: { + type: "STRING", + description: "The software niche or target market (e.g., 'summer camps', 'dental software').", + }, + }, + required: ["projectId", "niche"], + }, + }, + { + name: "market_research_run", + description: "Run market research for a specific business category and location. Fetches real business leads from DataForSEO, stores them in the Vibn database (Data Co-op), and returns the TAM (Total Addressable Market) count, competitor data, and a sample of domains/emails for analysis. This tool costs money. You MUST ask the user for explicit permission before calling it. Pass user_explicitly_approved: true once you have permission.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + categories: { + type: "ARRAY", + items: { type: "STRING" }, + description: 'Array of approved Google Business Categories (e.g., ["summer_camp", "camp", "youth_center"]). Max 10.', + }, + location: { + type: "STRING", + description: 'Location string (e.g., "Victoria, British Columbia, Canada", "California, United States").', + }, + user_explicitly_approved: { + type: "BOOLEAN", + description: "Must be true. Indicates the user agreed to run this cost-incurring research.", + }, + }, + required: [ + "projectId", + "categories", + "location", + "user_explicitly_approved", + ], + }, + }, + { + name: "market_seo_analyze", + description: "Analyze a competitor's domain for SEO and Google Ads metrics using DataForSEO. Returns estimated organic traffic, organic keywords, paid Google Ads traffic, estimated monthly Ad Spend, and top paid keywords. Use this to understand a competitor's GTM strategy.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + domain: { + type: "STRING", + description: 'The competitor\'s domain name (e.g., "curvedental.com").', + }, + }, + required: ["projectId", "domain"], + }, + }, + { + name: "tech_stack_analyze", + description: "Analyze a list of URLs to determine what software, CMS, booking tools, and analytics they use. Returns aggregated statistics. Use this to determine market gaps (e.g. 'How many dentists use WordPress but lack a booking widget?').", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + urls: { + type: "ARRAY", + items: { type: "STRING" }, + description: "An array of URLs to analyze (max 100 per call).", + }, + software_category_id: { + type: "STRING", + description: "The ID of the software category (e.g., 'dental-practice-management') to dynamically load competitors from BigQuery and check if the leads are using them.", + }, + custom_checks: { + type: "ARRAY", + items: { type: "STRING" }, + description: "Optional array of specific technology strings or domains to look for (e.g., ['hubspot.com', 'stripe.com/v3', 'gtag']). Use this if you want to check for competitors that aren't in the database.", + }, + }, + required: ["projectId", "urls", "software_category_id"], + }, + }, + { + name: "market_competitor_research", + description: "Search the proprietary database for software competitors and open-source starter kits related to a specific niche. Use this to understand the competitive landscape, pricing models, and find MIT-licensed code to fork.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + niche: { + type: "STRING", + description: "The software niche (e.g., 'dental', 'accounting', 'crm', 'booking').", + }, + }, + required: ["projectId", "niche"], + }, + }, + { + name: "market_aggregate_insights", + description: "Fetches aggregated insights for a specific market niche. Returns a breakdown of sub-categories, total websites, and most importantly, the top keywords mentioned in customer reviews. Use this to understand patient/customer pain points before writing a business plan or marketing copy.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + category: { + type: "STRING", + description: "The primary Google Category to aggregate (e.g., 'dentist', 'summer_camp').", + }, + location: { + type: "STRING", + description: "The location (e.g., 'Victoria, BC' or '48.4284,-123.3656,20').", + }, + }, + required: ["projectId", "category", "location"], + }, + }, + // ── Sentry (Stage 3 of Sentry-as-product) ─────────────────────────────── + { + name: "project_recent_errors", + description: 'List recent unresolved Sentry issues for a Vibn project. Each item has id, title, level, count, lastSeen, culprit, permalink. Use this when the user asks "is anything broken?" or before declaring something done. Returns [] if Sentry is not yet provisioned (project too new) — that is fine.', + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + sinceHours: { + type: "NUMBER", + description: "Look-back window in hours. Default 24, max 168 (1 week).", + }, + limit: { + type: "NUMBER", + description: "Max issues to return (1-50). Default 10.", + }, + }, + required: ["projectId"], + }, + }, + { + name: "project_error_detail", + description: "Fetch the most recent event for a Sentry issue: stack frames (top 12, source-mapped to real filenames), breadcrumbs (last 20 user actions before the error), user/request context, and a Session Replay link if one was captured. Call this AFTER project_recent_errors gives you an issue id.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + issueId: { + type: "STRING", + description: "Sentry issue id from project_recent_errors.", + }, + }, + required: ["projectId", "issueId"], + }, + }, + { + name: "project_error_resolve", + description: "Mark a Sentry issue resolved. Call this AFTER you have shipped a fix and either run a verifying test, watched the error stop firing, or had the user confirm. Do NOT mark resolved speculatively — Sentry auto-reopens issues on regression but it is noisy.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + issueId: { type: "STRING", description: "Sentry issue id to resolve." }, + }, + required: ["projectId", "issueId"], + }, + }, + // ── Applications ───────────────────────────────────────────────────────── + { + name: "apps_list", + description: "List live applications and services. Without projectId, lists everything across the workspace. With projectId, scopes to that single Vibn project.", + parameters: { + type: "OBJECT", + properties: { + projectId: { + type: "STRING", + description: "Optional Vibn project ID to scope the list to one project.", + }, + }, + required: [], + }, + }, + { + name: "apps_get", + description: "Get full details for a single app: status, domain, git info, build pack, environment.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_create", + description: `Create and deploy a new application. Four pathways — pick the right one: +1. template (PREFERRED for popular apps — Twenty, n8n, WordPress, Ghost, Supabase, etc.): pass { template: "" }. Always search apps_templates_search first. +2. image (single Docker container): pass { image: "nginx:latest" }. +3. composeRaw (custom multi-service stack, no template exists): pass { composeRaw: "" }. +4. repo (user's own code in Gitea): pass { repo: "" }. +Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`, + parameters: { + type: "OBJECT", + properties: { + projectId: { + type: "STRING", + description: "The Vibn project ID to deploy this app under. STRONGLY RECOMMENDED — gives the app its own isolated Coolify project so all related resources (databases, services) are grouped together and can be lifecycle-managed as one unit. If omitted, the app lands in the workspace's shared/legacy Coolify project.", + }, + name: { + type: "STRING", + description: 'App name (slug-friendly, e.g. "my-crm"). Required for all pathways.', + }, + domain: { + type: "STRING", + description: 'Custom subdomain (e.g. "crm.mark.vibnai.com"). Optional — auto-generated if omitted.', + }, + template: { + type: "STRING", + description: 'Coolify one-click template slug (e.g. "twenty", "n8n", "wordpress"). Use apps_templates_search to find the slug.', + }, + image: { + type: "STRING", + description: 'Docker image (e.g. "nginx:latest"). For single-container third-party apps.', + }, + composeRaw: { + type: "STRING", + description: "Raw Docker Compose YAML for custom multi-service stacks. Only use when no template exists.", + }, + repo: { + type: "STRING", + description: 'Gitea repo name (e.g. "my-site") for deploying the user\'s own code.', + }, + ports: { + type: "STRING", + description: 'Port(s) the app listens on (e.g. "3000"). Required for repo/image pathways.', + }, + envsJson: { + type: "STRING", + description: 'Environment variables as a JSON object string (e.g. \'{"KEY":"value"}\'). Optional.', + }, + instantDeploy: { + type: "BOOLEAN", + description: "Whether to deploy immediately (default true).", + }, + }, + required: ["name"], + }, + }, + { + name: "apps_update", + description: "Update whitelisted fields on an existing app (name, description, git branch, ports, build commands, base directory, etc.). Returns applied/ignored/rerouted arrays. Setting domains/git_repository returns a rerouted hint pointing at apps_domains_set or apps_rewire_git.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + patchJson: { + type: "STRING", + description: 'Fields to update as a JSON object string (e.g. \'{"name":"new-name","ports_exposes":"3001"}\').', + }, + }, + required: ["uuid", "patchJson"], + }, + }, + { + name: "apps_rewire_git", + description: "Re-point an app's git_repository at the canonical HTTPS+PAT clone URL. Use to recover older apps created with SSH URLs, or to refresh a rotated bot PAT.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + repo: { + type: "STRING", + description: "Gitea repo name. Optional — inferred from current URL if omitted.", + }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_delete", + description: "Destroy an application. Volumes are kept by default. confirm must equal the app's exact name.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + confirm: { + type: "STRING", + description: 'Must equal the exact app name (e.g. "my-crm"). Prevents accidental deletion.', + }, + }, + required: ["uuid", "confirm"], + }, + }, + { + name: "apps_deploy", + description: "Trigger a new deployment for an existing application.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + force: { + type: "BOOLEAN", + description: "Force rebuild even if nothing changed.", + }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_deployments", + description: "List recent deployments for an app with their status (finished, in_progress, failed, queued).", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_logs", + description: "Get runtime logs from a running application. Compose-aware: returns per-service logs for multi-service stacks. Use to diagnose crashes, DB errors, or startup failures.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + service: { + type: "STRING", + description: 'For compose apps: specific service name to filter (e.g. "server", "worker"). Omit for all services.', + }, + lines: { + type: "NUMBER", + description: "Number of log lines (default 200, max 5000).", + }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_exec", + description: "Run a one-shot command inside a running app container (via docker exec). Use for database migrations, seeds, CLI invocations, and debugging. Shell syntax works. Default timeout 60s.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + command: { + type: "STRING", + description: 'Shell command to run (e.g. "yarn command:prod database:migrate:prod", "psql $DATABASE_URL -c \'select 1\'").', + }, + service: { + type: "STRING", + description: 'For compose apps with multiple containers: the service to exec into (e.g. "server", "db").', + }, + user: { + type: "STRING", + description: "User to run command as (default: container default user).", + }, + workdir: { + type: "STRING", + description: "Working directory inside the container.", + }, + timeout_ms: { + type: "NUMBER", + description: "Timeout in milliseconds (default 60000, max 600000).", + }, + max_bytes: { + type: "NUMBER", + description: "Max output bytes (default 1MB, max 5MB).", + }, + }, + required: ["uuid", "command"], + }, + }, + { + name: "apps_volumes_list", + description: "List Docker volumes belonging to an app (name + size in bytes). Use before apps_volumes_wipe to confirm volume names.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_volumes_wipe", + description: "DESTRUCTIVE. Stop all app containers, remove a specific volume, leave app ready for a fresh deploy. Use to recover from stale DB state on first boot. confirm must equal the exact volume name.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + volume: { + type: "STRING", + description: "Exact volume name to wipe (get from apps_volumes_list).", + }, + confirm: { + type: "STRING", + description: "Must equal the exact volume name to confirm deletion.", + }, + }, + required: ["uuid", "volume", "confirm"], + }, + }, + { + name: "apps_containers_up", + description: "Run docker compose up -d directly on the Coolify host. Use when apps_deploy returned started:false or containers are in Created/Exited state. Idempotent.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_containers_ps", + description: "Run docker compose ps to see container states. Diagnoses Created (queue failure), Exited (crash), Restarting (boot loop), Up healthy/unhealthy.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_repair", + description: "Re-run post-deploy fixes on an existing service: proxy network attach, Traefik label injection, proxy restart. Use when a deploy succeeded but the app returns 502/503 or Mixed Content errors.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify service UUID." }, + fqdn: { + type: "STRING", + description: 'The public domain the app should be reachable at (e.g. "crm.mark.vibnai.com").', + }, + publicAppName: { + type: "STRING", + description: 'The name of the public-facing container/service within the stack (e.g. "server", "web").', + }, + port: { + type: "NUMBER", + description: "The upstream port the container listens on (e.g. 3000). Optional.", + }, + }, + required: ["uuid", "fqdn", "publicAppName"], + }, + }, + { + name: "apps_unstick", + description: `Recover a service stuck on a Docker "container name already in use" conflict. Force-removes orphan containers (everything matching name suffix -) so the next apps_deploy can boot clean. + +USE THIS — DO NOT delete-and-recreate the service. Deleting and re-creating produces a NEW uuid + NEW container names, which side-steps the conflict but leaves the orphan running AND forks a duplicate copy of the stack. We've burned ourselves on this before (4 orphan twenty-* services, 12GB RAM eaten). + +Recipe when a deploy fails with "Conflict. The container name X is already in use": + 1. apps_unstick { uuid: "" } + 2. apps_deploy { uuid: "" } + 3. apps_get { uuid: "" } to confirm fqdn / status. + +Pass wipeVolumes: true ONLY if the user explicitly said "nuke the data".`, + parameters: { + type: "OBJECT", + properties: { + uuid: { + type: "STRING", + description: "The Coolify service / app / database UUID.", + }, + wipeVolumes: { + type: "BOOLEAN", + description: "If true, also remove anonymous volumes (data loss). Default false.", + }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_templates_list", + description: "Browse the Coolify one-click template catalog (320+ apps: CRMs, AI tools, CMSes, dashboards, databases). Each is deployable via apps_create with { template: slug }.", + parameters: { + type: "OBJECT", + properties: { + limit: { + type: "NUMBER", + description: "Number of templates to return (default 50, max 500).", + }, + offset: { type: "NUMBER", description: "Pagination offset." }, + tag: { + type: "STRING", + description: 'Filter by tag substring (e.g. "crm", "ai", "database").', + }, + }, + required: [], + }, + }, + { + name: "apps_templates_search", + description: "Search for a Coolify template by name or keyword. Always call this before apps_create to find the correct template slug. Returns ranked matches.", + parameters: { + type: "OBJECT", + properties: { + query: { + type: "STRING", + description: 'Search term (e.g. "twenty", "n8n", "ghost blog", "kanban").', + }, + tag: { + type: "STRING", + description: 'Filter by tag (e.g. "crm", "ai"). Can be used with or without query.', + }, + limit: { + type: "NUMBER", + description: "Max results (default 25, max 100).", + }, + }, + required: [], + }, + }, + { + name: "apps_domains_list", + description: "List the current domain set for an application.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_domains_set", + description: `Set the public domain(s) for an application or service. All entries must end with .{workspace}.vibnai.com. + +Auto-detects whether uuid points to an application (Dockerfile / nixpacks / docker-image / compose buildpack) or a service (template-based, e.g. Twenty CRM, n8n) and uses the right Coolify pipeline for each: + +- Application: writes to applications.fqdn (or docker_compose_domains for compose buildpack). +- Service: writes to service_applications.fqdn AND triggers Coolify's updateCompose() + service.parse() so Traefik labels regenerate. Without this dance, the change gets reverted on next deploy. We learned this the hard way with twenty-live. + +For services with a required port (twenty-crm uses 3000), pass { port: 3000 } so the saved fqdn is "https://host:3000" — Coolify hard-fails the save otherwise. Look up the required port via apps_templates_search if you don't know it. + +After this returns, ALWAYS call apps_deploy { uuid } to regenerate the live Traefik labels.`, + parameters: { + type: "OBJECT", + properties: { + uuid: { + type: "STRING", + description: "The Coolify application or service UUID.", + }, + domains: { + type: "ARRAY", + description: 'Array of domain strings (e.g. ["myapp.mark.vibnai.com"]).', + items: { type: "STRING" }, + }, + service: { + type: "STRING", + description: 'For compose apps OR services: the inner app/service name to attach the domain to (e.g. "server", "twenty"). Default: auto-pick first non-worker app.', + }, + port: { + type: "NUMBER", + description: "Required for services that need a fixed upstream port (Twenty CRM = 3000, n8n = 5678, Ghost = 2368). Look up via apps_templates_search.", + }, + }, + required: ["uuid", "domains"], + }, + }, + { + name: "apps_envs_list", + description: "List environment variables for an application. Secret values (is_shown_once) are redacted.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "apps_envs_upsert", + description: "Create or update a single environment variable on an application.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + key: { + type: "STRING", + description: 'Env var key (uppercase, e.g. "DATABASE_URL").', + }, + value: { type: "STRING", description: "Env var value." }, + isShownOnce: { + type: "BOOLEAN", + description: "If true, value is write-only after creation (for secrets). Default false.", + }, + isMultiline: { + type: "BOOLEAN", + description: "If true, value spans multiple lines.", + }, + }, + required: ["uuid", "key", "value"], + }, + }, + { + name: "apps_envs_delete", + description: "Delete an environment variable from an application.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify application UUID." }, + key: { type: "STRING", description: "Env var key to delete." }, + }, + required: ["uuid", "key"], + }, + }, + // ── Databases ───────────────────────────────────────────────────────────── + { + name: "databases_list", + description: "List all databases in the workspace across all flavors (Postgres, MySQL, Redis, MongoDB, etc.).", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "databases_create", + description: "Provision a new database. Supported types: postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse.", + parameters: { + type: "OBJECT", + properties: { + type: { + type: "STRING", + description: 'Database type: "postgresql", "mysql", "mariadb", "mongodb", "redis", "keydb", "dragonfly", or "clickhouse".', + }, + name: { + type: "STRING", + description: "Database name (slug-friendly). Auto-generated if omitted.", + }, + isPublic: { + type: "BOOLEAN", + description: "Whether to expose a public port. Default false (internal only).", + }, + publicPort: { + type: "NUMBER", + description: "Public port number if isPublic is true.", + }, + }, + required: ["type"], + }, + }, + { + name: "databases_get", + description: "Get details for a database including the internal connection URL.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify database UUID." }, + }, + required: ["uuid"], + }, + }, + { + name: "databases_update", + description: "Update database settings: name, public visibility, image version, resource limits.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify database UUID." }, + patchJson: { + type: "STRING", + description: 'Fields to update as a JSON object string (e.g. \'{"name":"new-name","is_public":true}\').', + }, + }, + required: ["uuid", "patchJson"], + }, + }, + { + name: "databases_delete", + description: "Destroy a database. Volumes kept by default. confirm must equal the database's exact name.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify database UUID." }, + confirm: { + type: "STRING", + description: "Must equal the exact database name to confirm deletion.", + }, + }, + required: ["uuid", "confirm"], + }, + }, + // ── Auth providers ──────────────────────────────────────────────────────── + { + name: "auth_list", + description: "List deployed authentication providers in the workspace plus the allowlist of supported providers.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "auth_create", + description: "Deploy an auth provider from the allowlist. Supported providers: pocketbase, authentik, keycloak, keycloak-with-postgres, pocket-id, pocket-id-with-postgresql, logto, supertokens-with-postgresql.", + parameters: { + type: "OBJECT", + properties: { + provider: { + type: "STRING", + description: 'Provider key (e.g. "pocketbase", "authentik", "keycloak").', + }, + name: { + type: "STRING", + description: "Instance name. Auto-generated if omitted.", + }, + description: { type: "STRING", description: "Optional description." }, + instantDeploy: { + type: "BOOLEAN", + description: "Deploy immediately (default true).", + }, + }, + required: ["provider"], + }, + }, + { + name: "auth_delete", + description: "Destroy an auth provider. User data volumes are kept by default. confirm must equal the service's exact name.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The Coolify service UUID." }, + confirm: { + type: "STRING", + description: "Must equal the exact service name to confirm deletion.", + }, + }, + required: ["uuid", "confirm"], + }, + }, + // ── Domains ─────────────────────────────────────────────────────────────── + { + name: "domains_search", + description: "Check availability and pricing for domain names via OpenSRS. Stateless — does not reserve anything.", + parameters: { + type: "OBJECT", + properties: { + names: { + type: "ARRAY", + description: 'Array of domain names to check (e.g. ["myapp.com", "myapp.io"]). Max 25.', + items: { type: "STRING" }, + }, + period: { + type: "NUMBER", + description: "Registration period in years (default 1). Note: .ai requires 2 years minimum.", + }, + }, + required: ["names"], + }, + }, + { + name: "domains_list", + description: "List all domains owned by the workspace with their status, registrar order ID, expiry, and DNS provider.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "domains_get", + description: "Get full record and last 20 lifecycle events for a specific domain.", + parameters: { + type: "OBJECT", + properties: { + domain: { + type: "STRING", + description: 'The domain name (e.g. "myapp.com").', + }, + }, + required: ["domain"], + }, + }, + { + name: "domains_register", + description: "Register a domain through OpenSRS. Idempotent per (workspace, domain). Confirm availability with domains_search first.", + parameters: { + type: "OBJECT", + properties: { + domain: { + type: "STRING", + description: 'Domain name to register (e.g. "myapp.com").', + }, + period: { + type: "NUMBER", + description: "Registration period in years (default 1).", + }, + whoisPrivacy: { + type: "BOOLEAN", + description: "Enable WHOIS privacy (default true).", + }, + }, + required: ["domain"], + }, + }, + { + name: "domains_attach", + description: "Wire a registered domain to a Coolify app: creates Cloud DNS zone, writes A/CNAME records, updates registrar nameservers, appends domain to Coolify app. Idempotent.", + parameters: { + type: "OBJECT", + properties: { + domain: { + type: "STRING", + description: 'The registered domain name (e.g. "myapp.com").', + }, + appUuid: { + type: "STRING", + description: "Coolify app UUID to attach the domain to.", + }, + subdomains: { + type: "ARRAY", + description: 'Subdomains to wire (default ["@", "www"]).', + items: { type: "STRING" }, + }, + }, + required: ["domain"], + }, + }, + // ── Storage ─────────────────────────────────────────────────────────────── + { + name: "storage_describe", + description: "Report workspace GCS bucket name, region, S3-compatible endpoint, and access key ID. No secret returned.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "storage_provision", + description: "Idempotently create or reconcile the workspace GCS bucket, service account, IAM binding, and HMAC key. Safe to re-run.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "storage_inject_env", + description: "Push STORAGE_* env vars (endpoint, region, bucket, access key, secret) into a Coolify app. Secret is written server-side and never returned in the response.", + parameters: { + type: "OBJECT", + properties: { + uuid: { + type: "STRING", + description: "The Coolify application UUID to inject storage credentials into.", + }, + prefix: { + type: "STRING", + description: 'Env var prefix (default "STORAGE_"; use "S3_" for AWS-standard names).', + }, + }, + required: ["uuid"], + }, + }, + // ── Gitea — repos & file CRUD (write code, not just deploy it) ──────────── + // + // All gitea_* tools are scoped to the workspace's Gitea org. The AI can + // create repos, read & write files, and manage branches inside its own + // org but never outside it (enforced by requireGiteaOrg + ensureRepoOwnerInOrg + // in the MCP route). + { + name: "gitea_repos_list", + description: "List every Gitea repo in the workspace org. Use to discover repos already provisioned for projects.", + parameters: { type: "OBJECT", properties: {}, required: [] }, + }, + { + name: "gitea_repo_get", + description: "Get metadata for a single Gitea repo (default branch, clone URL, html URL, private flag).", + parameters: { + type: "OBJECT", + properties: { + repo: { + type: "STRING", + description: "Repo name (without org prefix).", + }, + owner: { + type: "STRING", + description: "Optional org/user. Defaults to the workspace Gitea org.", + }, + }, + required: ["repo"], + }, + }, + { + name: "gitea_repo_create", + description: "Create a new Gitea repo inside the workspace org. By default initializes with a README and is private. " + + "Use this when scaffolding a new app the AI is going to write code for and then deploy via apps_create({ repo }).", + parameters: { + type: "OBJECT", + properties: { + name: { + type: "STRING", + description: "Repo name. Will be slugified (lowercase, hyphens).", + }, + description: { + type: "STRING", + description: "Optional repo description.", + }, + private: { + type: "BOOLEAN", + description: "Whether the repo is private (default true).", + }, + autoInit: { + type: "BOOLEAN", + description: "Initialize with README (default true). Set false if writing files immediately yourself.", + }, + }, + required: ["name"], + }, + }, + // gitea_file_{read,write,delete} were intentionally hard-removed from + // the AI tool list. The MCP REST endpoints (gitea.file.read / .write + // / .delete) remain live for 30 days for any external clients still + // depending on them, but the AI is now expected to use shell.exec + + // fs.* against the dev container for ALL iterative file work, and + // `ship` to push the result. See AI_PATH_B_EXECUTION_PLAN.md §5. + // (Repo-level orchestration tools — gitea_repos_list, gitea_repo_get, + // gitea_repo_create, gitea_branches_list, gitea_branch_create — are + // still exposed because they handle one-time setup that doesn't have + // a clean dev-container equivalent.) + { + name: "gitea_branches_list", + description: "List all branches of a workspace Gitea repo with their head SHA.", + parameters: { + type: "OBJECT", + properties: { + repo: { type: "STRING", description: "Repo name." }, + owner: { + type: "STRING", + description: "Optional org. Defaults to the workspace Gitea org.", + }, + }, + required: ["repo"], + }, + }, + { + name: "gitea_branch_create", + description: "Create a new branch in a workspace Gitea repo, branched off an existing one (default = repo default branch).", + parameters: { + type: "OBJECT", + properties: { + repo: { type: "STRING", description: "Repo name." }, + name: { type: "STRING", description: "New branch name." }, + from: { + type: "STRING", + description: "Existing branch to branch from (default: repo default branch).", + }, + owner: { + type: "STRING", + description: "Optional org. Defaults to the workspace Gitea org.", + }, + }, + required: ["repo", "name"], + }, + }, + // ── Path B: dev container + shell + filesystem (PREFERRED for code authoring) ── + // + // These run inside the per-project vibn-dev container. Dramatically faster + // iteration than gitea_file_* (sub-second feedback vs ~5 min redeploy). + // Use these for ALL code writing/editing/scaffolding work. Keep gitea_* + // for orchestration (creating new repos, listing branches) only. + { + name: "devcontainer_ensure", + description: "Ensure a per-project AI dev container exists and is running. Idempotent — first call ~10s (provisions a Coolify service), subsequent calls are instant. " + + "Call this at the start of any code-authoring session. Returns the dev container service UUID and state.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + }, + required: ["projectId"], + }, + }, + { + name: "devcontainer_status", + description: "Cheap status check for the project dev container. Returns { exists, state, serviceUuid }.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + }, + required: ["projectId"], + }, + }, + { + name: "shell_exec", + description: "Run a shell command inside the project dev container as the `vibn` user (uid 1000) under /workspace. " + + "This is your universal escape hatch — install deps (`npm install`), run tests (`npm test`), scaffold code (`npx create-...`), " + + "inspect output, run migrations. Use this instead of gitea_file_* for any iterative work. " + + "Output is capped at 1 MB; default timeout 60s, max 600s.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + command: { + type: "STRING", + description: "Shell command (passes through `sh -lc`, so pipes/redirects work).", + }, + cwd: { + type: "STRING", + description: "Working directory (default /workspace). Must stay under /workspace.", + }, + timeoutMs: { + type: "NUMBER", + description: "Timeout in ms. Default 60000, max 600000.", + }, + }, + required: ["projectId", "command"], + }, + }, + { + name: "fs_read", + description: "Read a file inside the project dev container. Returns the full text. Optional offset/limit for windowed reads on big files.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + path: { + type: "STRING", + description: "File path. Relative paths are resolved under /workspace.", + }, + offset: { + type: "NUMBER", + description: "Optional 0-based starting line.", + }, + limit: { type: "NUMBER", description: "Optional max lines to return." }, + }, + required: ["projectId", "path"], + }, + }, + { + name: "fs_write", + description: "Create or overwrite a file inside the project dev container. Use to scaffold new files. " + + "For surgical edits to existing files, prefer fs_edit (less brittle, smaller diffs).", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + path: { + type: "STRING", + description: "File path. Relative paths under /workspace. Parent dirs are mkdir -p'd.", + }, + content: { type: "STRING", description: "Full file content." }, + }, + required: ["projectId", "path", "content"], + }, + }, + { + name: "fs_edit", + description: "Modify a file. You can either use line-number based replacement (PREFERRED) or search-and-replace. " + + "To use line numbers, provide startLine, endLine, and newString. " + + "To use search-and-replace, provide oldString and newString. (Always include 2-3 lines of surrounding context in oldString).", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + path: { type: "STRING", description: "File path under /workspace." }, + newString: { type: "STRING", description: "Replacement text." }, + startLine: { + type: "NUMBER", + description: "The 1-indexed start line number to replace.", + }, + endLine: { + type: "NUMBER", + description: "The 1-indexed end line number to replace (inclusive).", + }, + oldString: { + type: "STRING", + description: "Exact substring to find (used only if line numbers are not provided).", + }, + replaceAll: { + type: "BOOLEAN", + description: "If true, replace every occurrence of oldString. Default false.", + }, + }, + required: ["projectId", "path", "newString"], + }, + }, + { + name: "fs_list", + description: "List files in a directory inside the project dev container (`ls -lA`). Capped at 200 entries.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + path: { + type: "STRING", + description: "Directory path. Default /workspace.", + }, + }, + required: ["projectId"], + }, + }, + { + name: "fs_delete", + description: "Delete a file or directory inside the project dev container. Set recursive=true to remove a non-empty directory.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + path: { + type: "STRING", + description: "Path to delete. Cannot be /workspace itself.", + }, + recursive: { + type: "BOOLEAN", + description: "rm -rf if true. Default false.", + }, + }, + required: ["projectId", "path"], + }, + }, + { + name: "fs_glob", + description: "Find files matching a glob pattern (ripgrep-backed, respects .gitignore). Returns up to 500 paths.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + pattern: { + type: "STRING", + description: 'Glob, e.g. "**/*.tsx" or "src/**/*.ts".', + }, + cwd: { + type: "STRING", + description: "Search root (default /workspace).", + }, + }, + required: ["projectId", "pattern"], + }, + }, + { + name: "fs_grep", + description: "ripgrep-backed code search inside the project dev container. Capped at 50 matches per file, 500 total.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + pattern: { type: "STRING", description: "Regex or literal string." }, + glob: { + type: "STRING", + description: 'Optional file glob to filter (e.g. "*.ts").', + }, + cwd: { + type: "STRING", + description: "Search root (default /workspace).", + }, + contextLines: { + type: "NUMBER", + description: "Lines of context around each match (0-10).", + }, + }, + required: ["projectId", "pattern"], + }, + }, + // ── Path B: dev servers (preview URLs) ──────────────────────────────────── + { + name: "dev_server_start", + description: "Launch a long-running process inside the dev container (e.g. `npm run dev`, `python -m http.server`). " + + "Returns a preview URL the user can open in a browser. The process keeps running across shell.exec calls. " + + "IMPORTANT: bind your server to 0.0.0.0 — we set HOST=0.0.0.0 + PORT= automatically, but verify the framework respects them.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + command: { + type: "STRING", + description: 'Shell command to run (e.g. "npm run dev").', + }, + port: { + type: "NUMBER", + description: "TCP port the server will listen on (1-65535).", + }, + name: { + type: "STRING", + description: "Optional friendly name for the server (used in the preview subdomain).", + }, + }, + required: ["projectId", "command", "port"], + }, + }, + { + name: "dev_server_stop", + description: "Kill a previously-started dev server by id.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + id: { + type: "STRING", + description: "Dev server id from dev_server_start.", + }, + }, + required: ["projectId", "id"], + }, + }, + { + name: "dev_server_list", + description: "List active (non-stopped) dev servers for a project.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + }, + required: ["projectId"], + }, + }, + { + name: "dev_server_logs", + description: "Tail recent stdout+stderr from a dev server (default last 200 lines).", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + id: { type: "STRING", description: "Dev server id." }, + lines: { + type: "NUMBER", + description: "Number of trailing lines (1-2000, default 200).", + }, + }, + required: ["projectId", "id"], + }, + }, + { + name: "request_visual_qa", + description: "Runs a fast background AI agent to critique a UI file (like page.tsx or .css) against a strict 5-dimensional design rubric. Use this before finishing any turn that involves visual changes.", + parameters: { + type: "OBJECT", + properties: { + targetPath: { + type: "STRING", + description: "The path of the file to critique, e.g. apps/web/app/page.tsx", + }, + }, + required: ["targetPath"], + }, + }, + { + name: "apps_templates_scaffold", + description: "Scaffold a premium pre-built UI template directly into your project. Replaces empty Next.js setups with high-end boilerplate.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING" }, + templateName: { + type: "STRING", + description: "The template to scaffold. Available: 'dashboard', 'pitch-deck'", + enum: ["dashboard", "pitch-deck"], + }, + }, + required: ["projectId", "templateName"], + }, + }, + { + name: "generate_media", + description: "Generate images or motion graphics and save them directly into the workspace to use in your UI.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING" }, + prompt: { + type: "STRING", + description: "Detailed description of the media to generate", + }, + type: { + type: "STRING", + enum: ["image", "video"], + description: "The type of media to generate", + }, + outputPath: { + type: "STRING", + description: "Where to save the file, e.g. /workspace//public/hero.png", + }, + }, + required: ["projectId", "prompt", "type", "outputPath"], + }, + }, + // ── Path B: ship to production ───────────────────────────────────────────── + { + name: "ship", + description: "Graduate the project from dev container to production. Commits everything in /workspace, pushes to the project Gitea repo, " + + 'and triggers a Coolify production deploy if the project is linked to one. Use when the user says "ship it", "deploy this", ' + + "or after a stable working state has been verified via dev_server_*. Pass `commitMsg` for a meaningful commit; otherwise an ISO-timestamp message is used. " + + "Returns { commitSha, giteaCommitUrl, deploymentUuid, coolifyDeployUrl, summaryHint }. " + + "IMPORTANT: do NOT call gitea_*, shell_exec, or apps_* afterwards to verify — the result is authoritative. " + + "Just report commitSha + coolifyDeployUrl to the user.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + commitMsg: { + type: "STRING", + description: 'Commit message (default: "ship: ").', + }, + repo: { + type: "STRING", + description: "Repo name in workspace org (defaults to project slug).", + }, + branch: { + type: "STRING", + description: 'Branch to push to (default "main").', + }, + deploy: { + type: "BOOLEAN", + description: "Trigger Coolify deploy after push (default true).", + }, + }, + required: ["projectId"], + }, + }, + // ── Non-MCP: GitHub & web ───────────────────────────────────────────────── + { + name: "github_search", + description: "Search GitHub for public repositories. Use to find open source reference projects, design systems, or starting points. " + + 'Add "license:mit" to ensure permissive licensing. ' + + 'Example queries: "license:mit self-hosted crm", "license:mit kanban react", "license:mit design-system components".', + parameters: { + type: "OBJECT", + properties: { + query: { + type: "STRING", + description: "GitHub search query. Include license:mit unless intentionally looking for non-MIT. " + + "Supports: language:typescript, stars:>500, topic:self-hosted, filename:docker-compose.yml.", + }, + sort: { + type: "STRING", + description: 'Sort by: "stars" (default), "updated", or "forks".', + }, + limit: { + type: "NUMBER", + description: "Results to return (default 8, max 20).", + }, + }, + required: ["query"], + }, + }, + { + name: "github_file", + description: "Read a specific file from a public GitHub repository. Use to study design systems, component libraries, " + + "README files, package.json, docker-compose.yml, or any file in an open source project.", + parameters: { + type: "OBJECT", + properties: { + repo: { + type: "STRING", + description: 'Repository in "owner/repo" format (e.g. "makeplane/plane").', + }, + path: { + type: "STRING", + description: 'File path within the repo (e.g. "README.md", "docker-compose.yml").', + }, + ref: { + type: "STRING", + description: 'Branch or commit ref (default: "main").', + }, + }, + required: ["repo", "path"], + }, + }, + { + name: "http_fetch", + description: "Fetch any public URL and return the response body as text. Use for reading documentation, " + + "API responses, or any public web resource. Response truncated to 12KB.", + parameters: { + type: "OBJECT", + properties: { + url: { + type: "STRING", + description: "The full URL to fetch (https preferred).", + }, + headersJson: { + type: "STRING", + description: 'Optional HTTP headers as a JSON object string (e.g. \'{"Accept":"application/json"}\').', + }, + }, + required: ["url"], + }, + }, + // ── Plan (vision · tasks · decisions · ideas) ──────────────────────────── + // The Plan tab is where the founder's THINKING lives. The AI is the + // scribe: capture decisions in the moment so they don't get re-litigated, + // log tasks the AI commits to next, and parking-lot stray ideas. + { + name: "plan_get", + description: "Read the full Plan for a project: vision, ideas, open + done tasks, and decisions. Use to check what has already been decided BEFORE asking the user to re-decide.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + }, + required: ["projectId"], + }, + }, + { + name: "plan_vision_set", + description: "Update the project objective or vision. If a detailed objective document already exists, ONLY call this if you are explicitly appending to it or replacing it with a better, comprehensive version. Do NOT overwrite a detailed brief with a short summary.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + text: { + type: "STRING", + description: "The new or updated vision statement / objective doc. Be thorough.", + }, + }, + required: ["projectId", "text"], + }, + }, + { + name: "plan_decision_log", + description: "Log a decision the user has made. Call this PROACTIVELY whenever a non-trivial choice gets settled in conversation (database engine, auth approach, framework, pricing model, copy, branding…) — so it shows up in the Plan tab and you stop re-asking it next session. Don't ask permission; log it and move on.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + title: { + type: "STRING", + description: 'Short topic of the decision (e.g. "Database engine", "Auth provider").', + }, + choice: { + type: "STRING", + description: 'What was chosen (e.g. "Postgres", "Stripe Checkout").', + }, + why: { + type: "STRING", + description: "Optional 1-2 sentence reasoning. Strongly recommended.", + }, + }, + required: ["projectId", "title", "choice"], + }, + }, + { + name: "plan_task_add", + description: "Add an open task. Tasks are SCOPED UNITS OF WORK with a markdown spec — a feature, refactor, investigation, or migration. Each task should be substantive enough that an autonomous agent could execute it. Provide a verb-led `title` AND a markdown `description` containing: ## Goal, ## Context, ## Acceptance criteria (checklist), and ## Notes if relevant. Don't use this for trivial reminders — only for things that warrant a brief.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + title: { + type: "STRING", + description: 'Short verb-led headline (e.g. "Migrate auth to NextAuth v5").', + }, + description: { + type: "STRING", + description: "Markdown spec for this task. Include ## Goal, ## Context, ## Acceptance criteria, optionally ## Notes. Strongly recommended.", + }, + }, + required: ["projectId", "title"], + }, + }, + { + name: "plan_task_edit", + description: 'Edit an existing task\'s title, description, or status. When you have finished a task, put it in "review" status unless the user explicitly told you to mark it as "done". Look up the taskId from plan_get first.', + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + taskId: { + type: "STRING", + description: "Task id from plan_get.tasks[].id.", + }, + title: { + type: "STRING", + description: "Updated short verb-led headline.", + }, + description: { + type: "STRING", + description: "Updated markdown spec for this task.", + }, + status: { + type: "STRING", + description: 'Status of the task: "open", "in_progress", "review", "done", "blocked".', + enum: ["open", "in_progress", "review", "done", "blocked"], + }, + }, + required: ["projectId", "taskId"], + }, + }, + { + name: "plan_task_complete", + description: "Mark an open task done. Call when you (or the user with your help) just finished something already on the task list. Look up the taskId from plan_get first.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + taskId: { + type: "STRING", + description: "Task id from plan_get.tasks[].id.", + }, + }, + required: ["projectId", "taskId"], + }, + }, + { + name: "plan_idea_add", + description: "Park an idea the user mentions but isn't ready to act on. Use sparingly — only when the thought is genuinely worth remembering and isn't already a task or decision.", + parameters: { + type: "OBJECT", + properties: { + projectId: { type: "STRING", description: "The Vibn project ID." }, + text: { + type: "STRING", + description: "The idea, in the user's own words when possible.", + }, + }, + required: ["projectId", "text"], + }, + }, +]; +// ── Tool execution ──────────────────────────────────────────────────────────── +const NON_MCP_TOOLS = new Set(["github_search", "github_file", "http_fetch"]); +/** + * Execute any Vibn tool. Non-MCP tools (GitHub, http_fetch) run locally. + * All MCP tools forward to POST /api/mcp — the tool name maps to the MCP + * action by replacing underscores with dots (e.g. apps_create → apps.create). + */ +async function executeMcpTool(toolName, args, mcpToken, baseUrl, projectId) { + if (toolName === "github_search") + return executeGithubSearch(args); + if (toolName === "github_file") + return executeGithubFile(args); + if (toolName === "http_fetch") + return executeHttpFetch(args); + // Convert underscore tool name → dotted MCP action (apps_create → apps.create) + const action = toolName.replace(/_/g, "."); + // Unpack JSON-string args (Gemini schemas can't represent free-form objects, + // so we accept *Json string fields and parse them server-side). + const params = { ...args }; + for (const key of Object.keys(params)) { + if (key.endsWith("Json") && typeof params[key] === "string") { + const realKey = key.slice(0, -4); // envsJson → envs, patchJson → patch + try { + params[realKey] = JSON.parse(params[key]); + } + catch { + return JSON.stringify({ error: `Invalid JSON for ${key}` }); + } + delete params[key]; + } + } + try { + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${mcpToken}`, + }; + if (projectId) { + headers["X-Vibn-Project-Id"] = projectId; + } + const res = await fetch(`${baseUrl}/api/mcp`, { + method: "POST", + headers, + body: JSON.stringify({ action, params }), + }); + const data = await res.json(); + return JSON.stringify(data.result ?? data.error ?? data, null, 2).slice(0, 8000); + } + catch (e) { + return JSON.stringify({ + error: e instanceof Error ? e.message : String(e), + }); + } +} +// ── Non-MCP implementations ─────────────────────────────────────────────────── +async function executeGithubSearch(args) { + const query = String(args.query || ""); + const sort = String(args.sort || "stars"); + const limit = Math.min(Number(args.limit || 8), 20); + try { + const params = new URLSearchParams({ + q: query, + sort, + order: "desc", + per_page: String(limit), + }); + const headers = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }; + if (GITHUB_TOKEN) + headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`; + const res = await fetch(`https://api.github.com/search/repositories?${params}`, { headers }); + const data = await res.json(); + if (!res.ok) + return JSON.stringify({ error: data.message || "GitHub API error" }); + const repos = (data.items || []).map((r) => ({ + name: r.full_name, + description: r.description, + stars: r.stargazers_count, + language: r.language, + license: r.license?.spdx_id, + topics: r.topics, + updatedAt: r.pushed_at?.slice(0, 10), + url: r.html_url, + defaultBranch: r.default_branch, + })); + return JSON.stringify({ total: data.total_count, repos }, null, 2); + } + catch (e) { + return JSON.stringify({ + error: e instanceof Error ? e.message : String(e), + }); + } +} +async function executeGithubFile(args) { + const repo = String(args.repo || ""); + const path = String(args.path || ""); + const ref = String(args.ref || "main"); + try { + const headers = { + Accept: "application/vnd.github.raw+json", + "X-GitHub-Api-Version": "2022-11-28", + }; + if (GITHUB_TOKEN) + headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`; + const res = await fetch(`https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`, { headers }); + if (!res.ok) { + if (ref === "main") { + const res2 = await fetch(`https://api.github.com/repos/${repo}/contents/${path}?ref=master`, { headers }); + if (res2.ok) + return (await res2.text()).slice(0, 12000); + } + return JSON.stringify({ + error: `File not found: ${repo}/${path} (ref: ${ref})`, + }); + } + return (await res.text()).slice(0, 12000); + } + catch (e) { + return JSON.stringify({ + error: e instanceof Error ? e.message : String(e), + }); + } +} +async function executeHttpFetch(args) { + const url = String(args.url || ""); + let extraHeaders = {}; + if (typeof args.headersJson === "string") { + try { + extraHeaders = JSON.parse(args.headersJson); + } + catch { + /* ignore */ + } + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + return JSON.stringify({ error: "URL must start with http:// or https://" }); + } + try { + const res = await fetch(url, { + headers: { "User-Agent": "Vibn-AI/1.0", ...extraHeaders }, + signal: AbortSignal.timeout(10000), + }); + const contentType = res.headers.get("content-type") || ""; + const body = contentType.includes("json") + ? JSON.stringify(await res.json(), null, 2) + : await res.text(); + return `HTTP ${res.status}\nContent-Type: ${contentType}\n\n${body}`.slice(0, 12000); + } + catch (e) { + return JSON.stringify({ + error: e instanceof Error ? e.message : String(e), + }); + } +} diff --git a/vibn-agent-runner/src/agents/registry.ts b/vibn-agent-runner/src/agents/registry.ts index 45a86e3d..508569bd 100644 --- a/vibn-agent-runner/src/agents/registry.ts +++ b/vibn-agent-runner/src/agents/registry.ts @@ -1,4 +1,6 @@ -import { ToolDefinition, ALL_TOOLS } from '../tools'; +import { ALL_TOOLS } from '../tools'; + +export type ToolDefinition = any; export interface AgentConfig { name: string; diff --git a/vibn-agent-runner/src/atlas.ts b/vibn-agent-runner/src/atlas.ts index 80169044..aba1fff0 100644 --- a/vibn-agent-runner/src/atlas.ts +++ b/vibn-agent-runner/src/atlas.ts @@ -1,7 +1,7 @@ import { createLLM, toOAITools, LLMMessage } from './llm'; import { ALL_TOOLS, executeTool, ToolContext } from './tools'; import { resolvePrompt } from './prompts/loader'; -import { prdStore } from './tools/prd'; + const MAX_TURNS = 10; // Atlas is conversational — low turn count, no deep tool loops @@ -145,11 +145,11 @@ export async function atlasChat( } // Check if PRD was just saved - const stored = prdStore.get(ctx.workspaceRoot); + const stored = undefined; if (stored && !prdContent) { prdContent = stored; session.prdContent = stored; - prdStore.delete(ctx.workspaceRoot); // consume it + } session.history.push({ diff --git a/vibn-agent-runner/src/mcp/agent-server.ts b/vibn-agent-runner/src/mcp/agent-server.ts deleted file mode 100644 index faeacd3d..00000000 --- a/vibn-agent-runner/src/mcp/agent-server.ts +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env node -// ============================================================================= -// vibn-agent-mcp -// ----------------------------------------------------------------------------- -// Stdio MCP server exposing the vibn-agent-runner sub-agent orchestration API. -// This lets any MCP-speaking client (Goose, Claude Desktop, Cursor, etc.) -// spawn Coder / PM / Marketing jobs against the vibn-agent-runner HTTP service -// and poll their status. -// -// Config (env): -// AGENT_RUNNER_URL (default: http://localhost:3333) — URL of the runner -// ============================================================================= - -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; - -import * as api from '../tools/agent-api'; -import type { AgentRunnerConfig } from '../tools/agent-api'; - -function loadConfig(): AgentRunnerConfig { - const runnerUrl = process.env.AGENT_RUNNER_URL?.trim() || 'http://localhost:3333'; - return { runnerUrl }; -} - -const TOOL_DEFINITIONS = [ - { - name: 'spawn_agent', - description: 'Dispatch a sub-agent job to run in the background on the vibn-agent-runner. Returns a job ID.', - inputSchema: { - type: 'object' as const, - properties: { - agent: { type: 'string', description: '"Coder", "PM", or "Marketing"' }, - task: { type: 'string', description: 'Detailed task description for the agent' }, - repo: { type: 'string', description: 'Gitea repo in "owner/name" format' }, - }, - required: ['agent', 'task', 'repo'], - }, - }, - { - name: 'get_job_status', - description: 'Check the status of a previously spawned agent job.', - inputSchema: { - type: 'object' as const, - properties: { - job_id: { type: 'string', description: 'Job ID returned by spawn_agent' }, - }, - required: ['job_id'], - }, - }, -]; - -async function dispatch(cfg: AgentRunnerConfig, name: string, args: Record): Promise { - switch (name) { - case 'spawn_agent': - return api.spawnAgent(cfg, { - agent: String(args.agent), - task: String(args.task), - repo: String(args.repo), - }); - case 'get_job_status': - return api.getJobStatus(cfg, String(args.job_id)); - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -function buildServer(cfg: AgentRunnerConfig): Server { - const server = new Server( - { name: 'vibn-agent-mcp', version: '0.1.0' }, - { capabilities: { tools: {} } }, - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const name = request.params.name; - const args = (request.params.arguments ?? {}) as Record; - try { - const result = await dispatch(cfg, name, args); - return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] }; - } - }); - - return server; -} - -async function main(): Promise { - const cfg = loadConfig(); - const server = buildServer(cfg); - const transport = new StdioServerTransport(); - await server.connect(transport); - // eslint-disable-next-line no-console - console.error(`[vibn-agent-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed (runner=${cfg.runnerUrl})`); -} - -main().catch((err) => { - // eslint-disable-next-line no-console - console.error('[vibn-agent-mcp] fatal:', err); - process.exit(1); -}); diff --git a/vibn-agent-runner/src/mcp/coolify-server.ts b/vibn-agent-runner/src/mcp/coolify-server.ts deleted file mode 100644 index e10e0b95..00000000 --- a/vibn-agent-runner/src/mcp/coolify-server.ts +++ /dev/null @@ -1,181 +0,0 @@ -// ============================================================================= -// Vibn Coolify MCP Server -// -// Exposes the Coolify tools from src/tools/coolify-api.ts over the Model Context -// Protocol via stdio. Same security guardrails, same code path as the in-process -// agent runner — just accessible to any MCP-speaking client (Goose, -// Claude Code, Cursor, future harnesses). -// -// Launch: -// COOLIFY_API_URL=https://coolify.vibnai.com COOLIFY_API_TOKEN=... \ -// node dist/mcp/coolify-server.js -// -// The server speaks the MCP stdio transport on its stdin/stdout. Any logs go to -// stderr so they don't corrupt the protocol stream. -// ============================================================================= - -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import * as api from '../tools/coolify-api'; -import type { CoolifyConfig } from '../tools/coolify-api'; - -// --------------------------------------------------------------------------- -// Config — single source of truth, loaded once at startup -// --------------------------------------------------------------------------- - -function loadConfig(): CoolifyConfig { - const apiUrl = process.env.COOLIFY_API_URL; - const apiToken = process.env.COOLIFY_API_TOKEN; - if (!apiUrl) throw new Error('COOLIFY_API_URL env var is required'); - if (!apiToken) throw new Error('COOLIFY_API_TOKEN env var is required'); - return { apiUrl, apiToken }; -} - -// --------------------------------------------------------------------------- -// Tool surface — names, descriptions, and JSON Schema kept byte-identical to -// the in-process registrations in tools/coolify.ts so callers get the same -// behavior regardless of transport. -// --------------------------------------------------------------------------- - -const TOOL_DEFINITIONS = [ - { - name: 'coolify_list_projects', - description: 'List all projects in the Coolify instance. Returns project names and UUIDs.', - inputSchema: { type: 'object', properties: {} }, - }, - { - name: 'coolify_list_applications', - description: 'List applications in a Coolify project.', - inputSchema: { - type: 'object', - properties: { - project_uuid: { type: 'string', description: 'Project UUID from coolify_list_projects' }, - }, - required: ['project_uuid'], - }, - }, - { - name: 'coolify_deploy', - description: 'Trigger a deployment for a Coolify application.', - inputSchema: { - type: 'object', - properties: { - application_uuid: { type: 'string', description: 'Application UUID to deploy' }, - }, - required: ['application_uuid'], - }, - }, - { - name: 'coolify_get_logs', - description: 'Get recent deployment logs for a Coolify application.', - inputSchema: { - type: 'object', - properties: { - application_uuid: { type: 'string', description: 'Application UUID' }, - }, - required: ['application_uuid'], - }, - }, - { - name: 'list_all_apps', - description: 'List all Coolify applications across all projects with their status (running/stopped/error) and domain.', - inputSchema: { type: 'object', properties: {} }, - }, - { - name: 'get_app_status', - description: 'Get the current deployment status and recent logs for a specific Coolify application by name or UUID.', - inputSchema: { - type: 'object', - properties: { - app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend") or UUID' }, - }, - required: ['app_name'], - }, - }, - { - name: 'deploy_app', - description: 'Trigger a Coolify deployment for an app by name. Use after an agent commits code.', - inputSchema: { - type: 'object', - properties: { - app_name: { type: 'string', description: 'Application name (e.g. "vibn-frontend")' }, - }, - required: ['app_name'], - }, - }, -] as const; - -// --------------------------------------------------------------------------- -// Dispatch -// --------------------------------------------------------------------------- - -async function dispatch(cfg: CoolifyConfig, name: string, args: Record): Promise { - switch (name) { - case 'coolify_list_projects': - return api.listProjects(cfg); - case 'coolify_list_applications': - return api.listApplications(cfg, String(args.project_uuid)); - case 'coolify_deploy': - return api.deploy(cfg, String(args.application_uuid)); - case 'coolify_get_logs': - return api.getLogs(cfg, String(args.application_uuid)); - case 'list_all_apps': - return api.listAllApps(cfg); - case 'get_app_status': - return api.getAppStatus(cfg, String(args.app_name)); - case 'deploy_app': - return api.deployApp(cfg, String(args.app_name)); - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// --------------------------------------------------------------------------- -// Server -// --------------------------------------------------------------------------- - -function buildServer(cfg: CoolifyConfig): Server { - const server = new Server( - { name: 'vibn-coolify', version: '0.1.0' }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: TOOL_DEFINITIONS.map(t => ({ ...t })), - })); - - server.setRequestHandler(CallToolRequestSchema, async (req) => { - const { name, arguments: args = {} } = req.params; - try { - const result = await dispatch(cfg, name, args as Record); - const text = typeof result === 'string' ? result : JSON.stringify(result); - return { content: [{ type: 'text', text }] }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { - content: [{ type: 'text', text: JSON.stringify({ error: message }) }], - isError: true, - }; - } - }); - - return server; -} - -async function main(): Promise { - const cfg = loadConfig(); - const server = buildServer(cfg); - const transport = new StdioServerTransport(); - await server.connect(transport); - // stderr so we don't corrupt the stdio MCP stream - console.error(`[vibn-coolify-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed (coolify=${cfg.apiUrl})`); -} - -main().catch((err) => { - console.error('[vibn-coolify-mcp] fatal:', err instanceof Error ? err.stack : err); - process.exit(1); -}); diff --git a/vibn-agent-runner/src/mcp/gitea-server.ts b/vibn-agent-runner/src/mcp/gitea-server.ts deleted file mode 100644 index eadc9883..00000000 --- a/vibn-agent-runner/src/mcp/gitea-server.ts +++ /dev/null @@ -1,165 +0,0 @@ -// ============================================================================= -// Vibn Gitea MCP Server -// -// Exposes the Gitea tools from src/tools/gitea-api.ts over the Model Context -// Protocol via stdio. Same security guardrails, same code path as the -// in-process agent runner — accessible to any MCP-speaking client. -// -// Launch: -// GITEA_API_URL=https://git.vibnai.com GITEA_API_TOKEN=... GITEA_USERNAME=mark \ -// node dist/mcp/gitea-server.js -// ============================================================================= - -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import * as api from '../tools/gitea-api'; -import type { GiteaConfig } from '../tools/gitea-api'; - -function loadConfig(): GiteaConfig { - const apiUrl = process.env.GITEA_API_URL; - const apiToken = process.env.GITEA_API_TOKEN; - if (!apiUrl) throw new Error('GITEA_API_URL env var is required'); - if (!apiToken) throw new Error('GITEA_API_TOKEN env var is required'); - return { apiUrl, apiToken, username: process.env.GITEA_USERNAME }; -} - -const TOOL_DEFINITIONS = [ - { - name: 'gitea_create_issue', - description: 'Create a new issue in a Gitea repository.', - inputSchema: { - type: 'object', - properties: { - repo: { type: 'string', description: 'Repository in "owner/name" format' }, - title: { type: 'string', description: 'Issue title' }, - body: { type: 'string', description: 'Issue body (markdown)' }, - labels: { type: 'array', items: { type: 'string' }, description: 'Optional label names' }, - }, - required: ['repo', 'title', 'body'], - }, - }, - { - name: 'gitea_list_issues', - description: 'List open issues in a Gitea repository.', - inputSchema: { - type: 'object', - properties: { - repo: { type: 'string', description: 'Repository in "owner/name" format' }, - state: { type: 'string', description: '"open", "closed", or "all". Default: "open"' }, - }, - required: ['repo'], - }, - }, - { - name: 'gitea_close_issue', - description: 'Close an issue in a Gitea repository.', - inputSchema: { - type: 'object', - properties: { - repo: { type: 'string', description: 'Repository in "owner/name" format' }, - issue_number: { type: 'number', description: 'Issue number to close' }, - }, - required: ['repo', 'issue_number'], - }, - }, - { - name: 'list_repos', - description: 'List all Git repositories in the Gitea organization. Returns repo names, descriptions, and last update time.', - inputSchema: { type: 'object', properties: {} }, - }, - { - name: 'list_all_issues', - description: 'List open issues across all repos or a specific repo. Use this to understand what work is queued or in progress.', - inputSchema: { - type: 'object', - properties: { - repo: { type: 'string', description: 'Optional: "owner/name" to scope to one repo. Omit for all repos.' }, - state: { type: 'string', description: '"open", "closed", or "all". Default: "open"' }, - }, - }, - }, - { - name: 'read_repo_file', - description: 'Read a file from any Gitea repository without cloning it. Useful for understanding project structure.', - inputSchema: { - type: 'object', - properties: { - repo: { type: 'string', description: 'Repo in "owner/name" format' }, - path: { type: 'string', description: 'File path within the repo (e.g. "src/app/page.tsx")' }, - }, - required: ['repo', 'path'], - }, - }, -] as const; - -async function dispatch(cfg: GiteaConfig, name: string, args: Record): Promise { - switch (name) { - case 'gitea_create_issue': - return api.createIssue(cfg, { - repo: String(args.repo), - title: String(args.title), - body: String(args.body), - labels: Array.isArray(args.labels) ? (args.labels as string[]) : undefined, - }); - case 'gitea_list_issues': - return api.listIssues(cfg, String(args.repo), String(args.state || 'open')); - case 'gitea_close_issue': - return api.closeIssue(cfg, String(args.repo), Number(args.issue_number)); - case 'list_repos': - return api.listRepos(cfg); - case 'list_all_issues': - return api.listAllIssues(cfg, { - repo: args.repo ? String(args.repo) : undefined, - state: args.state ? String(args.state) : undefined, - }); - case 'read_repo_file': - return api.readRepoFile(cfg, String(args.repo), String(args.path)); - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -function buildServer(cfg: GiteaConfig): Server { - const server = new Server( - { name: 'vibn-gitea', version: '0.1.0' }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: TOOL_DEFINITIONS.map(t => ({ ...t })), - })); - - server.setRequestHandler(CallToolRequestSchema, async (req) => { - const { name, arguments: args = {} } = req.params; - try { - const result = await dispatch(cfg, name, args as Record); - const text = typeof result === 'string' ? result : JSON.stringify(result); - return { content: [{ type: 'text', text }] }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { - content: [{ type: 'text', text: JSON.stringify({ error: message }) }], - isError: true, - }; - } - }); - - return server; -} - -async function main(): Promise { - const cfg = loadConfig(); - const server = buildServer(cfg); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`[vibn-gitea-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed (gitea=${cfg.apiUrl})`); -} - -main().catch((err) => { - console.error('[vibn-gitea-mcp] fatal:', err instanceof Error ? err.stack : err); - process.exit(1); -}); diff --git a/vibn-agent-runner/src/mcp/vibn-platform-server.ts b/vibn-agent-runner/src/mcp/vibn-platform-server.ts deleted file mode 100644 index d32b7f40..00000000 --- a/vibn-agent-runner/src/mcp/vibn-platform-server.ts +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env node -// ============================================================================= -// vibn-platform-mcp -// ----------------------------------------------------------------------------- -// Stdio MCP server exposing Vibn platform primitives: -// - save_memory → persists facts into a per-session in-memory store -// - list_memory → inspect what has been saved this session (MCP-only) -// - list_skills → enumerate .skills/ in a Gitea repo -// - get_skill → read a specific SKILL.md -// - finalize_prd → save a completed PRD keyed by SESSION_KEY -// - get_prd → read back the saved PRD (MCP-only convenience) -// - web_search → DuckDuckGo HTML search -// -// NOTE: The in-process agent-runner collects memory into ToolContext and -// consumes the PRD via the module-level prdStore. When the same logic is -// exposed over MCP, there is no shared process memory with the agent-runner, -// so this server maintains its own session-scoped stores. Set SESSION_KEY to -// give each MCP client a stable key into those stores. -// -// Config (env): -// SESSION_KEY (optional) — session scope for memory + PRD stores -// (defaults to "default") -// GITEA_API_URL (optional) — required for list_skills / get_skill -// GITEA_API_TOKEN (optional) — required for list_skills / get_skill -// ============================================================================= - -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; - -import * as memoryApi from '../tools/memory-api'; -import * as skillsApi from '../tools/skills-api'; -import * as prdApi from '../tools/prd-api'; -import * as searchApi from '../tools/search-api'; - -interface PlatformConfig { - sessionKey: string; - gitea?: { apiUrl: string; apiToken: string }; -} - -function loadConfig(): PlatformConfig { - const sessionKey = process.env.SESSION_KEY?.trim() || 'default'; - const giteaUrl = process.env.GITEA_API_URL; - const giteaToken = process.env.GITEA_API_TOKEN; - const gitea = giteaUrl && giteaToken ? { apiUrl: giteaUrl, apiToken: giteaToken } : undefined; - return { sessionKey, gitea }; -} - -const TOOL_DEFINITIONS = [ - { - name: 'save_memory', - description: 'Persist an important fact about this project to long-term memory within this MCP session.', - inputSchema: { - type: 'object' as const, - properties: { - key: { type: 'string', description: 'Short unique label (e.g. "primary_language", "auth_strategy")' }, - type: { - type: 'string', - enum: ['tech_stack', 'decision', 'feature', 'goal', 'constraint', 'note'], - description: 'Category of the memory item', - }, - value: { type: 'string', description: 'The fact to remember (1-3 sentences)' }, - }, - required: ['key', 'type', 'value'], - }, - }, - { - name: 'list_memory', - description: 'List all memory entries saved in the current session.', - inputSchema: { type: 'object' as const, properties: {} }, - }, - { - name: 'list_skills', - description: 'List available skills for a project repo. Skills are stored in .skills//SKILL.md. Requires Gitea credentials.', - inputSchema: { - type: 'object' as const, - properties: { repo: { type: 'string', description: 'Repo in "owner/name" format' } }, - required: ['repo'], - }, - }, - { - name: 'get_skill', - description: 'Read the full content of a specific skill from a project repo. Requires Gitea credentials.', - inputSchema: { - type: 'object' as const, - properties: { - repo: { type: 'string', description: 'Repo in "owner/name" format' }, - skill_name: { type: 'string', description: 'Skill name (directory inside .skills/)' }, - }, - required: ['repo', 'skill_name'], - }, - }, - { - name: 'finalize_prd', - description: 'Save a completed PRD document for this session.', - inputSchema: { - type: 'object' as const, - properties: { content: { type: 'string', description: 'The complete PRD in markdown' } }, - required: ['content'], - }, - }, - { - name: 'get_prd', - description: 'Read back the PRD saved for this session, or null if none saved yet.', - inputSchema: { type: 'object' as const, properties: {} }, - }, - { - name: 'web_search', - description: 'Search the web via DuckDuckGo HTML endpoint. Returns titles + snippets for the top results.', - inputSchema: { - type: 'object' as const, - properties: { query: { type: 'string', description: 'The search query' } }, - required: ['query'], - }, - }, -]; - -async function dispatch(cfg: PlatformConfig, name: string, args: Record): Promise { - switch (name) { - case 'save_memory': - return memoryApi.saveMemoryToStore(cfg.sessionKey, { - key: String(args.key), - type: String(args.type), - value: String(args.value), - }); - case 'list_memory': - return { sessionKey: cfg.sessionKey, entries: memoryApi.listMemoryFromStore(cfg.sessionKey) }; - case 'list_skills': - if (!cfg.gitea) return { error: 'list_skills requires GITEA_API_URL and GITEA_API_TOKEN.' }; - return skillsApi.listSkills(cfg.gitea, String(args.repo)); - case 'get_skill': - if (!cfg.gitea) return { error: 'get_skill requires GITEA_API_URL and GITEA_API_TOKEN.' }; - return skillsApi.getSkill(cfg.gitea, String(args.repo), String(args.skill_name)); - case 'finalize_prd': - return prdApi.finalizePrd(cfg.sessionKey, String(args.content)); - case 'get_prd': - return { sessionKey: cfg.sessionKey, content: prdApi.getPrd(cfg.sessionKey) }; - case 'web_search': - return searchApi.webSearch(String(args.query)); - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -function buildServer(cfg: PlatformConfig): Server { - const server = new Server( - { name: 'vibn-platform-mcp', version: '0.1.0' }, - { capabilities: { tools: {} } }, - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const name = request.params.name; - const args = (request.params.arguments ?? {}) as Record; - try { - const result = await dispatch(cfg, name, args); - return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] }; - } - }); - - return server; -} - -async function main(): Promise { - const cfg = loadConfig(); - const server = buildServer(cfg); - const transport = new StdioServerTransport(); - await server.connect(transport); - // eslint-disable-next-line no-console - console.error( - `[vibn-platform-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed ` + - `(session=${cfg.sessionKey}, gitea=${cfg.gitea ? 'enabled' : 'disabled'})`, - ); -} - -main().catch((err) => { - // eslint-disable-next-line no-console - console.error('[vibn-platform-mcp] fatal:', err); - process.exit(1); -}); diff --git a/vibn-agent-runner/src/mcp/workspace-server.ts b/vibn-agent-runner/src/mcp/workspace-server.ts deleted file mode 100644 index 254538a3..00000000 --- a/vibn-agent-runner/src/mcp/workspace-server.ts +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env node -// ============================================================================= -// vibn-workspace-mcp -// ----------------------------------------------------------------------------- -// Stdio MCP server exposing the coding-agent workspace toolkit: -// - Filesystem primitives (read/write/replace/list/find/search) -// - Shell execution (120s timeout, blocked-command guard) -// - Authenticated git commit + push with protected-repo guard -// -// Each server instance is scoped to a single WORKSPACE_ROOT. To operate against -// multiple workspaces, spawn multiple MCP server instances (one per workspace). -// This mirrors how Goose / Claude Desktop / Cursor MCP configs work in practice. -// -// Config (env): -// WORKSPACE_ROOT (required) — absolute path to the workspace -// GITEA_API_URL (optional) — required if caller uses git_commit_and_push -// GITEA_API_TOKEN (optional) — required if caller uses git_commit_and_push -// GITEA_USERNAME (optional) — required if caller uses git_commit_and_push -// ============================================================================= - -import * as path from 'path'; -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; - -import * as fileApi from '../tools/file-api'; -import * as shellApi from '../tools/shell-api'; -import * as gitApi from '../tools/git-api'; - -interface WorkspaceConfig { - workspaceRoot: string; - gitea?: { apiUrl: string; apiToken: string; username: string }; -} - -function loadConfig(): WorkspaceConfig { - const workspaceRoot = process.env.WORKSPACE_ROOT; - if (!workspaceRoot) { - throw new Error('WORKSPACE_ROOT is required (absolute path to the workspace to operate on).'); - } - const absWorkspace = path.resolve(workspaceRoot); - - const giteaUrl = process.env.GITEA_API_URL; - const giteaToken = process.env.GITEA_API_TOKEN; - const giteaUser = process.env.GITEA_USERNAME; - const gitea = giteaUrl && giteaToken && giteaUser - ? { apiUrl: giteaUrl, apiToken: giteaToken, username: giteaUser } - : undefined; - - return { workspaceRoot: absWorkspace, gitea }; -} - -const TOOL_DEFINITIONS = [ - { - name: 'read_file', - description: 'Read the complete content of a file in the workspace. Always read before editing.', - inputSchema: { - type: 'object' as const, - properties: { - path: { type: 'string', description: 'Relative path from workspace root (e.g. "src/index.ts")' }, - }, - required: ['path'], - }, - }, - { - name: 'write_file', - description: 'Write complete content to a file. Creates parent directories if needed. Overwrites existing files.', - inputSchema: { - type: 'object' as const, - properties: { - path: { type: 'string', description: 'Relative path from workspace root' }, - content: { type: 'string', description: 'Complete new file content' }, - }, - required: ['path', 'content'], - }, - }, - { - name: 'replace_in_file', - description: 'Replace an exact string in a file. The old_content must match character-for-character. Read the file first.', - inputSchema: { - type: 'object' as const, - properties: { - path: { type: 'string', description: 'Relative path from workspace root' }, - old_content: { type: 'string', description: 'Exact text to replace' }, - new_content: { type: 'string', description: 'Replacement text' }, - }, - required: ['path', 'old_content', 'new_content'], - }, - }, - { - name: 'list_directory', - description: 'List files and subdirectories in a directory. Directories have trailing "/".', - inputSchema: { - type: 'object' as const, - properties: { - path: { type: 'string', description: 'Relative path from workspace root. Use "." for root.' }, - }, - required: ['path'], - }, - }, - { - name: 'find_files', - description: 'Find files matching a glob pattern in the workspace. Returns up to 200 relative paths.', - inputSchema: { - type: 'object' as const, - properties: { - pattern: { type: 'string', description: 'Glob pattern e.g. "**/*.ts", "src/**/*.test.js"' }, - }, - required: ['pattern'], - }, - }, - { - name: 'search_code', - description: 'Search file contents for a string using ripgrep. Returns file path, line number, and matching line.', - inputSchema: { - type: 'object' as const, - properties: { - query: { type: 'string', description: 'Search term (fixed-string)' }, - file_extensions: { - type: 'array', - items: { type: 'string' }, - description: 'Optional: limit to these extensions e.g. ["ts","js"]', - }, - }, - required: ['query'], - }, - }, - { - name: 'execute_command', - description: 'Run a shell command in the workspace and return stdout + stderr. 120s timeout.', - inputSchema: { - type: 'object' as const, - properties: { - command: { type: 'string', description: 'Shell command to run' }, - working_directory: { type: 'string', description: 'Optional: relative subdirectory to run in' }, - }, - required: ['command'], - }, - }, - { - name: 'git_commit_and_push', - description: 'Stage all changes, commit with a message, and push to the remote using configured Gitea credentials. Blocks pushes to protected platform repos.', - inputSchema: { - type: 'object' as const, - properties: { - message: { type: 'string', description: 'Commit message describing the changes made' }, - }, - required: ['message'], - }, - }, -]; - -async function dispatch(cfg: WorkspaceConfig, name: string, args: Record): Promise { - switch (name) { - case 'read_file': - return fileApi.readFile(cfg.workspaceRoot, String(args.path)); - case 'write_file': - return fileApi.writeFile(cfg.workspaceRoot, String(args.path), String(args.content)); - case 'replace_in_file': - return fileApi.replaceInFile( - cfg.workspaceRoot, - String(args.path), - String(args.old_content), - String(args.new_content), - ); - case 'list_directory': - return fileApi.listDirectory(cfg.workspaceRoot, String(args.path)); - case 'find_files': - return fileApi.findFiles(cfg.workspaceRoot, String(args.pattern)); - case 'search_code': { - const exts = Array.isArray(args.file_extensions) ? (args.file_extensions as string[]) : undefined; - return fileApi.searchCode(cfg.workspaceRoot, String(args.query), exts); - } - case 'execute_command': - return shellApi.executeCommand( - cfg.workspaceRoot, - String(args.command), - args.working_directory ? String(args.working_directory) : undefined, - ); - case 'git_commit_and_push': { - if (!cfg.gitea) { - return { error: 'git_commit_and_push requires GITEA_API_URL, GITEA_API_TOKEN, and GITEA_USERNAME environment variables.' }; - } - return gitApi.gitCommitAndPush(cfg.workspaceRoot, String(args.message), cfg.gitea); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -function buildServer(cfg: WorkspaceConfig): Server { - const server = new Server( - { name: 'vibn-workspace-mcp', version: '0.1.0' }, - { capabilities: { tools: {} } }, - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const name = request.params.name; - const args = (request.params.arguments ?? {}) as Record; - try { - const result = await dispatch(cfg, name, args); - return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { isError: true, content: [{ type: 'text', text: `Error: ${message}` }] }; - } - }); - - return server; -} - -async function main(): Promise { - const cfg = loadConfig(); - const server = buildServer(cfg); - const transport = new StdioServerTransport(); - await server.connect(transport); - // eslint-disable-next-line no-console - console.error( - `[vibn-workspace-mcp] ready — ${TOOL_DEFINITIONS.length} tools exposed ` + - `(workspace=${cfg.workspaceRoot}, git=${cfg.gitea ? 'enabled' : 'disabled'})`, - ); -} - -main().catch((err) => { - // eslint-disable-next-line no-console - console.error('[vibn-workspace-mcp] fatal:', err); - process.exit(1); -}); diff --git a/vibn-agent-runner/src/server.ts b/vibn-agent-runner/src/server.ts index 9d8dc0d7..a10a6daa 100644 --- a/vibn-agent-runner/src/server.ts +++ b/vibn-agent-runner/src/server.ts @@ -9,7 +9,7 @@ import { runAgent } from './agent-runner'; import { runSessionAgent } from './agent-session-runner'; import { AGENTS } from './agents'; import { ToolContext } from './tools'; -import { PROTECTED_GITEA_REPOS } from './tools/security'; + import { orchestratorChat, listSessions, clearSession } from './orchestrator'; import { atlasChat, listAtlasSessions, clearAtlasSession } from './atlas'; import { LLMMessage, createLLM } from './llm'; @@ -37,13 +37,7 @@ function ensureWorkspace(repo?: string): string { fs.mkdirSync(dir, { recursive: true }); return dir; } - if (PROTECTED_GITEA_REPOS.has(repo)) { - throw new Error( - `SECURITY: Repo "${repo}" is a protected Vibn platform repo. ` + - `Agents cannot clone or work in this workspace.` - ); - } - const dir = path.join(base, repo.replace('/', '_')); + const dir = path.join(base, repo.replace('/', '_')); const gitea = { apiUrl: process.env.GITEA_API_URL || '', apiToken: process.env.GITEA_API_TOKEN || '', @@ -78,6 +72,8 @@ function buildContext(repo?: string): ToolContext { apiUrl: process.env.COOLIFY_API_URL || '', apiToken: process.env.COOLIFY_API_TOKEN || '' }, + mcpToken: '', + vibnApiUrl: 'http://localhost:3000', memoryUpdates: [] }; } diff --git a/vibn-agent-runner/src/tools/index.ts b/vibn-agent-runner/src/tools/index.ts index 50b9630d..bc82df95 100644 --- a/vibn-agent-runner/src/tools/index.ts +++ b/vibn-agent-runner/src/tools/index.ts @@ -1,3 +1,3 @@ export * from './context'; -export * from './registry'; + export * from './mcp-client'; diff --git a/vibn-agent-runner/src/tools/mcp-client.ts b/vibn-agent-runner/src/tools/mcp-client.ts index 104bdf91..a8e1d31c 100644 --- a/vibn-agent-runner/src/tools/mcp-client.ts +++ b/vibn-agent-runner/src/tools/mcp-client.ts @@ -25,7 +25,7 @@ export async function executeTool( body: JSON.stringify({ action, params: args }), }); - const data = await response.json(); + const data: any = await response.json(); if (!response.ok) { return { error: data.error || `HTTP ${response.status}: ${response.statusText}` }; diff --git a/vibn-agent-runner/src/tools/vibn-tools.ts b/vibn-agent-runner/src/tools/vibn-tools.ts index 7033972a..6ab955df 100644 --- a/vibn-agent-runner/src/tools/vibn-tools.ts +++ b/vibn-agent-runner/src/tools/vibn-tools.ts @@ -8,7 +8,7 @@ * Non-MCP tools (github_search, github_file, http_fetch) are handled * locally at the bottom of this file. */ -import type { ToolDefinition } from "./gemini-chat"; +export type ToolDefinition = any; const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; @@ -1837,7 +1837,7 @@ export async function executeMcpTool( headers, body: JSON.stringify({ action, params }), }); - const data = await res.json(); + const data: any = await res.json(); return JSON.stringify(data.result ?? data.error ?? data, null, 2).slice( 0, 8000, @@ -1875,7 +1875,7 @@ async function executeGithubSearch( `https://api.github.com/search/repositories?${params}`, { headers }, ); - const data = await res.json(); + const data: any = await res.json(); if (!res.ok) return JSON.stringify({ error: data.message || "GitHub API error" });