diff --git a/vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts b/vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts index be9d3e2..16fa08a 100644 --- a/vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts +++ b/vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts @@ -11,25 +11,27 @@ import { NextResponse } from "next/server"; import { requireWorkspacePrincipal } from "@/lib/auth/workspace-auth"; import { query, queryOne } from "@/lib/db-postgres"; -import { listWorkspaceApiKeys, mintWorkspaceApiKey, revealWorkspaceApiKey } from "@/lib/auth/workspace-auth"; +import { + listWorkspaceApiKeys, + mintWorkspaceApiKey, + revealWorkspaceApiKey, +} from "@/lib/auth/workspace-auth"; -const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; +const AGENT_RUNNER_URL = + process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; // Verify the agent_sessions table is reachable. If it doesn't exist yet, // throw a descriptive error instead of a generic "Failed to create session". // Run POST /api/admin/migrate once to create the table. async function ensureTable() { - await query( - `SELECT 1 FROM agent_sessions LIMIT 0`, - [] - ); + await query(`SELECT 1 FROM agent_sessions LIMIT 0`, []); } // ── POST — create session ──────────────────────────────────────────────────── export async function POST( req: Request, - { params }: { params: Promise<{ projectId: string }> } + { params }: { params: Promise<{ projectId: string }> }, ) { try { const { projectId } = await params; @@ -41,11 +43,14 @@ export async function POST( // 2. Fetch user details from principal.userId const userRow = await queryOne<{ id: string; data: any }>( `SELECT id, data FROM fs_users WHERE id = $1 LIMIT 1`, - [principal.userId] + [principal.userId], ); const email = userRow?.data?.email; if (!email) { - return NextResponse.json({ error: "User email not found" }, { status: 404 }); + return NextResponse.json( + { error: "User email not found" }, + { status: 404 }, + ); } const body = await req.json(); @@ -56,7 +61,10 @@ export async function POST( }; if (!appName || appPath === undefined || !task?.trim()) { - return NextResponse.json({ error: "appName, appPath and task are required" }, { status: 400 }); + return NextResponse.json( + { error: "appName, appPath and task are required" }, + { status: 400 }, + ); } await ensureTable(); @@ -66,7 +74,7 @@ export async function POST( `SELECT p.id, p.data FROM fs_projects p JOIN fs_users u ON u.id = p.user_id WHERE p.id::text = $1 AND u.data->>'email' = $2 LIMIT 1`, - [projectId, email] + [projectId, email], ); if (owns.length === 0) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); @@ -75,9 +83,12 @@ export async function POST( const giteaRepo = owns[0].data?.giteaRepo as string | undefined; // Find the Coolify UUID for this specific app so the runner can trigger a deploy - interface AppEntry { name: string; coolifyServiceUuid?: string | null; } + interface AppEntry { + name: string; + coolifyServiceUuid?: string | null; + } const apps = (owns[0].data?.apps ?? []) as AppEntry[]; - const matchedApp = apps.find(a => a.name === appName); + const matchedApp = apps.find((a) => a.name === appName); const coolifyAppUuid = matchedApp?.coolifyServiceUuid ?? undefined; // Create the session row @@ -85,13 +96,13 @@ export async function POST( `INSERT INTO agent_sessions (project_id, app_name, app_path, task, status, started_at) VALUES ($1::uuid, $2, $3, $4, 'running', now()) RETURNING id`, - [projectId, appName, appPath, task.trim()] + [projectId, appName, appPath, task.trim()], ); const sessionId = rows[0].id; const wsResult = await query<{ workspace_id: string }>( `SELECT vibn_workspace_id as workspace_id FROM fs_projects WHERE id = $1 LIMIT 1`, - [projectId] + [projectId], ); if (!wsResult.length) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); @@ -101,22 +112,34 @@ export async function POST( // Grab or mint a default API key for the runner to use let mcpToken = ""; const keys = await listWorkspaceApiKeys(workspaceId); - let defaultKey = keys.find((k: any) => k.name === 'default' && !k.revoked_at); + let defaultKey = keys.find( + (k: any) => k.name === "default" && !k.revoked_at, + ); if (!defaultKey) { - const minted = await mintWorkspaceApiKey({ workspaceId, name: 'default', createdBy: principal.userId, scopes: ['workspace:*'] }); + const minted = await mintWorkspaceApiKey({ + workspaceId, + name: "default", + createdBy: principal.userId, + scopes: ["workspace:*"], + }); mcpToken = minted.token; } else { const revealed = await revealWorkspaceApiKey(workspaceId, defaultKey.id); if (revealed) mcpToken = revealed.token; else { - const minted = await mintWorkspaceApiKey({ workspaceId, name: 'default', createdBy: principal.userId, scopes: ['workspace:*'] }); + const minted = await mintWorkspaceApiKey({ + workspaceId, + name: "default", + createdBy: principal.userId, + scopes: ["workspace:*"], + }); mcpToken = minted.token; } } // Add VIBN_API_URL so the runner knows where to send MCP requests - const vibnApiUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; - + const vibnApiUrl = + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; // Fire-and-forget: call agent-runner to start the execution loop. // autoApprove: true — agent commits + deploys automatically on completion. @@ -133,33 +156,43 @@ export async function POST( autoApprove: true, coolifyAppUuid, mcpToken, - vibnApiUrl + vibnApiUrl, }), - }).catch(err => { - // Agent runner may not be wired yet — log but don't fail - console.warn("[agent] runner not reachable:", err.message); - // Mark session as failed if runner unreachable - query( - `UPDATE agent_sessions + }) + .then(async (res) => { + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new Error(`Runner returned ${res.status}: ${text}`); + } + }) + .catch((err) => { + // Agent runner may not be wired yet — log but don't fail + console.warn("[agent] runner failed or not reachable:", err.message); + // Mark session as failed if runner unreachable + query( + `UPDATE agent_sessions SET status = 'failed', - error = 'Agent runner not reachable', + error = $2, completed_at = now(), output = jsonb_build_array(jsonb_build_object( 'ts', now()::text, 'type', 'error', - 'text', 'Agent runner service is not connected yet. Phase 2 implementation pending.' + 'text', $2 )) WHERE id = $1::uuid`, - [sessionId] - ).catch(() => {}); - }); + [sessionId, `Agent runner failed: ${err.message}`], + ).catch(() => {}); + }); return NextResponse.json({ sessionId }, { status: 201 }); } catch (err) { console.error("[agent/sessions POST]", err); return NextResponse.json( - { error: "Failed to create session", details: err instanceof Error ? err.message : String(err) }, - { status: 500 } + { + error: "Failed to create session", + details: err instanceof Error ? err.message : String(err), + }, + { status: 500 }, ); } } @@ -168,7 +201,7 @@ export async function POST( export async function GET( req: Request, - { params }: { params: Promise<{ projectId: string }> } + { params }: { params: Promise<{ projectId: string }> }, ) { try { const { projectId } = await params; @@ -180,11 +213,14 @@ export async function GET( // 2. Fetch user details from principal.userId const userRow = await queryOne<{ id: string; data: any }>( `SELECT id, data FROM fs_users WHERE id = $1 LIMIT 1`, - [principal.userId] + [principal.userId], ); const email = userRow?.data?.email; if (!email) { - return NextResponse.json({ error: "User email not found" }, { status: 404 }); + return NextResponse.json( + { error: "User email not found" }, + { status: 404 }, + ); } await ensureTable(); @@ -210,15 +246,18 @@ export async function GET( WHERE s.project_id::text = $1 AND u.data->>'email' = $2 ORDER BY s.created_at DESC LIMIT 50`, - [projectId, email] + [projectId, email], ); return NextResponse.json({ sessions }); } catch (err) { console.error("[agent/sessions GET]", err); return NextResponse.json( - { error: "Failed to list sessions", details: err instanceof Error ? err.message : String(err) }, - { status: 500 } + { + error: "Failed to list sessions", + details: err instanceof Error ? err.message : String(err), + }, + { status: 500 }, ); } }