diff --git a/vibn-frontend/app/api/mcp/route.ts b/vibn-frontend/app/api/mcp/route.ts index 4637c10..bcb6288 100644 --- a/vibn-frontend/app/api/mcp/route.ts +++ b/vibn-frontend/app/api/mcp/route.ts @@ -4568,18 +4568,26 @@ function normalizeFsPath( ); } - // Prevent doubled subdirectory paths (e.g. getacquired-2-0/getacquired-2-0/) - if (p.startsWith("getacquired-2-0/")) { - p = p.substring("getacquired-2-0/".length); + // 1. Strip any leading /workspace/ + let normalized = p.replace(/^\/workspace\//, ""); + + // 2. Strip any leading ./ + if (normalized.startsWith("./")) { + normalized = normalized.slice(2); + } + + // 3. Strip project slug prefix if the model hallucinates it (e.g. getacquired-2-0/src...) + if (projectSlug && normalized.startsWith(`${projectSlug}/`)) { + normalized = normalized.slice(projectSlug.length + 1); + } + + // Handle any other legacy hardcoded occurrences + if (normalized.startsWith("getacquired-2-0/")) { + normalized = normalized.substring("getacquired-2-0/".length); } const projectRoot = FS_ROOT; - let abs: string; - if (p.startsWith("/")) { - abs = p; - } else { - abs = `${projectRoot}/${p}`.replace(/\/+/g, "/"); - } + const abs = `${projectRoot}/${normalized}`.replace(/\/+/g, "/"); const norm = abs.replace(/\/[^/]+\/\.\.(?=\/|$)/g, "").replace(/\/+/g, "/"); // When projectSlug is set, REJECT paths outside the project root. @@ -4588,7 +4596,7 @@ function normalizeFsPath( return NextResponse.json( { ok: false, - error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}". Did you mean "${projectRoot}/${p.replace(/^\/+/, "")}"?`, + error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}".`, }, { status: 400 }, ); diff --git a/vibn-frontend/lib/dev-container.ts b/vibn-frontend/lib/dev-container.ts index 93557a0..061f541 100644 --- a/vibn-frontend/lib/dev-container.ts +++ b/vibn-frontend/lib/dev-container.ts @@ -827,31 +827,40 @@ export async function startDevServer( throw new PortOutOfRangeError(opts.port); } - // 2. Stop ALL tracked rows for this project on the target port. - // Previous runs may have crashed or exited without being marked - // stopped, causing stale rows to accumulate. We reap them - // unconditionally before starting anything new — the AI's intent - // is "I want THIS command on THIS port", so most-recent-write-wins. - const existingRows = await query<{ id: string; pid: number | null }>( - `SELECT id, pid FROM fs_dev_servers - WHERE project_id = $1 AND port = $2 AND state IN ('starting','running','failed')`, - [opts.projectId, opts.port], + // 2. Stop ALL tracked rows for this project on ALL preview ports. + // Because our socket reaper is infallible, the AI never needs to + // sprawl across multiple ports. We unconditionally reap and stop + // every active preview server for this project before starting a new one + // to keep the dashboard clean and prevent memory leaks. + const existingRows = await query<{ + id: string; + pid: number | null; + port: number; + }>( + `SELECT id, pid, port FROM fs_dev_servers + WHERE project_id = $1 AND state IN ('starting','running','failed')`, + [opts.projectId], ); const killPortNodeCmd = `node -e '` + `const fs = require("fs"); ` + - `const port = ${opts.port}; ` + + `const portsToKill = [${existingRows + .map((r) => r.port) + .concat(opts.port) + .join(",")}]; ` + `try { ` + - `const hexPort = port.toString(16).toUpperCase().padStart(4, "0"); ` + `const tcp = fs.readFileSync("/proc/net/tcp", "utf8"); ` + `const inodes = []; ` + `tcp.split("\\n").forEach(line => { ` + `const parts = line.trim().split(/\\s+/); ` + `if (parts.length > 9) { ` + `const local = parts[1]; ` + + `for (const p of portsToKill) { ` + + `const hexPort = p.toString(16).toUpperCase().padStart(4, "0"); ` + `if (local.endsWith(":" + hexPort)) { inodes.push(parts[9]); } ` + `} ` + + `} ` + `}); ` + `if (inodes.length > 0) { ` + `fs.readdirSync("/proc").forEach(file => { ` + @@ -872,7 +881,7 @@ export async function startDevServer( `}); ` + `} ` + `} catch (e) { ` + - `try { require("child_process").execSync("fuser -k -9 ${opts.port}/tcp 2>/dev/null || true"); } catch (err) {} ` + + `try { require("child_process").execSync("fuser -k -9 " + portsToKill.join(",") + "/tcp 2>/dev/null || true"); } catch (err) {} ` + `}'`; for (const row of existingRows) {