diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 42e8eb72..64a67272 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -237,6 +237,12 @@ When you write to Plan, the user does NOT need a long acknowledgment. "Logged th - ALWAYS call \`apps_templates_search\` BEFORE \`apps_create\` when the user names a known third-party app. Hand-rolling a Dockerfile when a maintained template exists is how supply-chain bugs ship. - **NEVER delete-and-recreate a service to escape an error.** When a deploy fails with "Conflict. The container name … is already in use" or any orphan-container symptom, the recovery is: \`apps_unstick { uuid }\` → \`apps_deploy { uuid }\`. Deleting the service to side-step the conflict creates a new uuid with new container names AND leaves the orphan running AND forks a duplicate stack. We've shipped 4 orphan twenty-* services this way before. Don't repeat it. - **If a deploy fails twice in a row with the same error, STOP.** Don't loop. Surface the error and the two recovery attempts you've already tried, and ask the user how to proceed. + +- **Tool results are authoritative; conversation history is not.** When a tool result contradicts something you said earlier in this thread, DISCARD your prior assertion. State the new ground truth from the tool. Do not paper over the contradiction or restate the old belief. Example: if you told the user "X is broken" earlier and \`apps_get\` now reports \`status: running:healthy\`, say "X is actually healthy — my earlier read was stale." Don't keep telling them it's broken. + +- **Anchor on current state before troubleshooting.** When the user reports an error, your FIRST tool call must be a current-state read: \`apps_get { uuid }\` for an app, \`databases_get { uuid }\` for a db, \`apps_logs { uuid, lines: 50 }\` for runtime errors. Don't react to symptoms the user described 30 minutes ago — the world has probably moved. We've burned a session re-debugging a problem that was already fixed. + +- **Trust idempotency.** \`apps_create\` and \`databases_create\` will return \`alreadyExisted: true\` with the existing uuid when a duplicate is detected. When you see this flag, your job is DONE — don't try to "make sure" it's right by calling apps_create again with a different name. Use the returned uuid and proceed to whatever comes next (env vars, domains, deploy). - Destructive ops (\`*_delete\`, \`*_volumes_wipe\`) require \`confirm\` equal to the resource's exact name. Always fetch the name first with a \`*_get\` call. Confirm with the user before executing irreversible deletes unless they explicitly said "delete X". - Long-running ops (deploys, DNS provisioning, db provisioning) take 1–5 min. Tell the user up front so they don't think you're stuck. Don't poll in a tight loop — it wastes tool rounds. - After a \`ship\` or \`apps.deploy\`, the result is authoritative. Don't call gitea_*, shell_exec, or apps_* to "verify" — read the response and report. diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index e4afcabb..d0d8d688 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -1426,6 +1426,21 @@ async function toolAppsCreate(principal: Principal, params: Record) const fqdn = resolveFqdn(params.domain, ws.slug, appName); if (fqdn instanceof NextResponse) return fqdn; + if (params.projectId && !(params.force === true || params.force === 'true')) { + const existing = await findExistingResourceByName(targetCoolifyProjectUuid, appName); + if (existing) { + await linkIfRequested(existing.uuid, existing.type); + return NextResponse.json({ + result: { + uuid: existing.uuid, + name: existing.name, + alreadyExisted: true, + summaryHint: dedupHint(existing, 'image'), + }, + }); + } + } + const created = await createDockerImageApp({ ...commonOpts, image, @@ -1447,6 +1462,21 @@ async function toolAppsCreate(principal: Principal, params: Record) const fqdn = resolveFqdn(params.domain, ws.slug, appName); if (fqdn instanceof NextResponse) return fqdn; + if (params.projectId && !(params.force === true || params.force === 'true')) { + const existing = await findExistingResourceByName(targetCoolifyProjectUuid, appName); + if (existing) { + await linkIfRequested(existing.uuid, existing.type); + return NextResponse.json({ + result: { + uuid: existing.uuid, + name: existing.name, + alreadyExisted: true, + summaryHint: dedupHint(existing, 'composeRaw'), + }, + }); + } + } + const created = await createDockerComposeApp({ ...commonOpts, composeRaw, @@ -1552,6 +1582,21 @@ async function toolAppsCreate(principal: Principal, params: Record) const fqdn = resolveFqdn(params.domain, ws.slug, appName); if (fqdn instanceof NextResponse) return fqdn; + if (params.projectId && !(params.force === true || params.force === 'true')) { + const existing = await findExistingResourceByName(targetCoolifyProjectUuid, appName); + if (existing) { + await linkIfRequested(existing.uuid, existing.type); + return NextResponse.json({ + result: { + uuid: existing.uuid, + name: existing.name, + alreadyExisted: true, + summaryHint: dedupHint(existing, 'repo'), + }, + }); + } + } + const created = await createPublicApp({ ...commonOpts, gitRepository: giteaHttpsUrl(repoOrg, repoName, botCreds.username, botCreds.token), @@ -2277,8 +2322,30 @@ async function toolDatabasesList(principal: Principal) { async function toolDatabasesCreate(principal: Principal, params: Record) { const ws = principal.workspace; - const projectUuid = requireCoolifyProject(principal); + let projectUuid = requireCoolifyProject(principal); if (projectUuid instanceof NextResponse) return projectUuid; + + // Scope to the Vibn project's dedicated Coolify project when projectId + // is supplied (auto-mint if needed). This both narrows the dedup scope + // and prevents cross-project leaks. + if (params.projectId) { + const projectId = String(params.projectId); + const projectRow = await queryOne<{ id: string; data: any; slug: string }>( + `SELECT id, data, slug FROM fs_projects + WHERE id = $1 AND (vibn_workspace_id = $2 OR workspace = $3) LIMIT 1`, + [projectId, ws.id, ws.slug], + ); + if (!projectRow) { + return NextResponse.json({ error: `Project ${projectId} not found in this workspace` }, { status: 404 }); + } + const projectName = projectRow.data?.productName || projectRow.data?.name || projectRow.slug; + const ensured = await ensureProjectCoolifyProject(projectId, ws, { + projectSlug: projectRow.slug, + projectName, + }); + if (ensured) projectUuid = ensured; + } + const ownedUuids = await getOwnedCoolifyProjectUuids(principal.workspace); const type = String(params.type ?? '').toLowerCase() as CoolifyDatabaseType; if (!DB_TYPES.includes(type)) { @@ -2288,6 +2355,30 @@ async function toolDatabasesCreate(principal: Principal, params: Record { + const target = name.trim().toLowerCase(); + if (!target) return null; + try { + const apps = await listApplicationsInProject(coolifyProjectUuid); + for (const a of apps) { + if ((a.name ?? '').toLowerCase() === target) { + return { uuid: a.uuid, name: a.name, type: 'application' }; + } + } + } catch (e) { + console.warn('[findExistingResourceByName] apps lookup failed', e); + } + try { + const services = await listServicesInProject(coolifyProjectUuid); + for (const s of services) { + if ((s.name ?? '').toLowerCase() === target) { + return { uuid: s.uuid, name: s.name, type: 'service' }; + } + } + } catch (e) { + console.warn('[findExistingResourceByName] services lookup failed', e); + } + try { + const dbs = await listDatabasesInProject(coolifyProjectUuid); + for (const d of dbs) { + if ((d.name ?? '').toLowerCase() === target) { + return { uuid: d.uuid, name: d.name, type: 'database' }; + } + } + } catch (e) { + console.warn('[findExistingResourceByName] databases lookup failed', e); + } + return null; +} + +function dedupHint( + existing: { uuid: string; name: string; type: string }, + pathway: string, +): string { + return ( + `A ${existing.type} named "${existing.name}" (uuid ${existing.uuid}) already exists in this project ` + + `(detected via ${pathway} path). Returning it instead of creating a duplicate. ` + + `If the existing one is broken, call apps_unstick { uuid } and apps_deploy { uuid } — DO NOT delete-and-recreate. ` + + `If you genuinely need a SECOND independent instance, re-call with { force: true } and a different { name: }.` + ); +} + /** * apps.unstick — recover a service stuck on a "container name already * in use" Docker conflict. Force-removes the orphan containers (and