From 6b8862ef2bf0e61efd9be553bab3f7ca79f669db Mon Sep 17 00:00:00 2001 From: mawkone Date: Sun, 17 May 2026 19:17:22 -0700 Subject: [PATCH] =?UTF-8?q?feat(api):=20comprehensive=20QA=20hardening=20?= =?UTF-8?q?=E2=80=94=20security=20gates,=20chat=20improvements,=20beta=20s?= =?UTF-8?q?caffolds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes checklist items F-01..F-06, D-01..D-28, S-01..S-10, C-01..C-07, B-01..B-07, R-01..R-02, O-03. Security (28 deletions + 10 auth gates): - Delete 28 unauthenticated debug/cursor/firebase/test routes - Gate ai/chat, ai/conversation, context/summarize, work-completed with withTenantProject/withAuth - Add HMAC-SHA256 signature verification to webhooks/coolify - Switch all admin secret comparisons to timingSafeStringEq Foundations (lib/server/*): - api-handler.ts: withAuth, withTenantProject, withWorkspace, withAdminSecret, withRateLimit - logger.ts: structured request-scoped logging with turnId - audit-log.ts: writeAuditLog helper + audit_log table - rate-limit.ts: Postgres sliding window rate limiter - coolify-webhook.ts: verifyCoolifySignature - timing-safe.ts: timingSafeStringEq Chat hardening (chat/route.ts): - MAX_TOOL_ROUNDS 15 → 8 (C-01) - Loop detection: hard-break at 3 identical fingerprints (was 5) (C-02) - Add 6-consecutive-tool-call hard-break (C-02) - Mode: respond first, act second prompt block (C-03) - SSE heartbeat every 25s via setInterval (C-04) - Per-tool 45s timeout via Promise.race (C-05) - turnId per-turn UUID for log correlation (C-06) - Recovery fires when roundsSinceText >= 4 (C-07) - SSE plan event on plan_task_add/edit (B-05) Beta features: - invites table + GET/POST /api/invites (P4.8) - invites/[token] validate + redeem (P4.8) - fs_project_dev_servers table + lib/server/dev-server-state.ts (P6.B1) - fs_project_secrets table + CRUD routes (P6.D2) - lib/integrations/brief-extract.ts (P3.7) Documentation: - app/api/ROUTES.md: full route map with auth + tenant --- deploy_error.txt | 1 + deploy_logs.json | 1 + docs/API_QA_CHECKLIST.md | 127 +++ docs/for-entrepenuers.md | 6 + docs/for-freelancers.md | 58 ++ docs/for-smbs.md | 57 ++ docs/mission.md | 43 + env_data.json | 1 + grep_out.txt | 28 + latest_deploy.txt | 1 + new-site/Onboarding.bundle.html | 45 + new-site/Onboarding.html | 28 + new-site/onboarding-app.jsx | 181 ++++ new-site/onboarding-build.jsx | 445 +++++++++ new-site/onboarding-consultant.jsx | 294 ++++++ new-site/onboarding-entrepreneur.jsx | 274 ++++++ new-site/onboarding-fork.jsx | 134 +++ new-site/onboarding-owner.jsx | 262 ++++++ new-site/onboarding-primitives.jsx | 333 +++++++ new-site/onboarding.css | 677 ++++++++++++++ new-site/vibn-signin.html | 189 ++++ patch_thought_sig.ts | 40 + runner_logs.json | 1 + vibn-frontend/app/api/ROUTES.md | 159 ++++ .../app/api/admin/check-sessions/route.ts | 46 - .../api/admin/fix-project-workspace/route.ts | 59 -- vibn-frontend/app/api/admin/migrate/route.ts | 14 +- .../app/api/admin/path-b/autosave/route.ts | 50 +- .../app/api/admin/path-b/disable/route.ts | 28 +- .../app/api/admin/path-b/enable/route.ts | 28 +- .../app/api/admin/path-b/idle-sweep/route.ts | 29 +- vibn-frontend/app/api/admin/path-b/route.ts | 19 +- vibn-frontend/app/api/ai/chat/route.ts | 871 ++++++++++-------- .../app/api/ai/conversation/reset/route.ts | 67 +- .../app/api/ai/conversation/route.ts | 99 +- vibn-frontend/app/api/chat/route.ts | 142 ++- vibn-frontend/app/api/chat/route.ts.sed | 25 + .../app/api/context/summarize/route.ts | 123 ++- .../app/api/cursor/backfill/route.ts | 229 ----- .../app/api/cursor/clear-imports/route.ts | 54 -- .../app/api/cursor/tag-sessions/route.ts | 192 ---- .../api/debug/append-conversation/route.ts | 63 -- .../app/api/debug/check-links/route.ts | 88 -- .../app/api/debug/check-project/route.ts | 62 -- .../app/api/debug/context-sources/route.ts | 44 - .../app/api/debug/cursor-analysis/route.ts | 72 -- .../api/debug/cursor-content-sample/route.ts | 72 -- .../api/debug/cursor-conversations/route.ts | 55 -- .../app/api/debug/cursor-relevant/route.ts | 56 -- .../api/debug/cursor-sample-dates/route.ts | 41 - .../api/debug/cursor-session-summary/route.ts | 55 -- .../app/api/debug/cursor-sessions/route.ts | 124 --- .../app/api/debug/cursor-stats/route.ts | 69 -- .../debug/cursor-unknown-sessions/route.ts | 59 -- .../app/api/debug/cursor-workspaces/route.ts | 59 -- vibn-frontend/app/api/debug/env/route.ts | 18 - .../app/api/debug/first-project/route.ts | 33 - .../app/api/debug/knowledge-items/route.ts | 43 - .../app/api/debug/knowledge/route.ts | 36 - vibn-frontend/app/api/debug/prisma/route.ts | 40 - vibn-frontend/app/api/diagnose/route.ts | 61 -- vibn-frontend/app/api/firebase/test/route.ts | 58 -- .../app/api/internal/infra-health/route.ts | 21 +- .../app/api/invites/[token]/route.ts | 85 ++ vibn-frontend/app/api/invites/route.ts | 90 ++ .../[projectId]/secrets/[key]/route.ts | 50 + .../api/projects/[projectId]/secrets/route.ts | 91 ++ .../app/api/sentry-example-api/route.ts | 21 - vibn-frontend/app/api/test-token/route.ts | 37 - .../app/api/webhooks/coolify/route.ts | 99 +- vibn-frontend/app/api/work-completed/route.ts | 91 +- .../components/project/project-stage-pill.tsx | 150 ++- .../components/project/use-anatomy.ts | 41 +- vibn-frontend/lib/ai/gemini-chat.ts | 265 ++---- vibn-frontend/lib/ai/gemini-client.ts | 159 ++-- vibn-frontend/lib/coolify-exec.ts | 34 +- .../lib/integrations/brief-extract.ts | 185 ++++ vibn-frontend/lib/server/api-handler.ts | 233 +++++ vibn-frontend/lib/server/audit-log.ts | 145 +++ vibn-frontend/lib/server/coolify-webhook.ts | 40 + vibn-frontend/lib/server/dev-server-state.ts | 140 +++ vibn-frontend/lib/server/logger.ts | 71 ++ vibn-frontend/lib/server/rate-limit.ts | 91 ++ vibn-frontend/lib/server/timing-safe.ts | 26 + vibn-frontend/next.config.ts | 19 +- vibn-signin-css.html | 737 +++++++++++++++ 86 files changed, 6772 insertions(+), 2817 deletions(-) create mode 100644 deploy_error.txt create mode 100644 deploy_logs.json create mode 100644 docs/API_QA_CHECKLIST.md create mode 100644 docs/for-entrepenuers.md create mode 100644 docs/for-freelancers.md create mode 100644 docs/for-smbs.md create mode 100644 docs/mission.md create mode 100644 env_data.json create mode 100644 grep_out.txt create mode 100644 latest_deploy.txt create mode 100644 new-site/Onboarding.bundle.html create mode 100644 new-site/Onboarding.html create mode 100644 new-site/onboarding-app.jsx create mode 100644 new-site/onboarding-build.jsx create mode 100644 new-site/onboarding-consultant.jsx create mode 100644 new-site/onboarding-entrepreneur.jsx create mode 100644 new-site/onboarding-fork.jsx create mode 100644 new-site/onboarding-owner.jsx create mode 100644 new-site/onboarding-primitives.jsx create mode 100644 new-site/onboarding.css create mode 100644 new-site/vibn-signin.html create mode 100644 patch_thought_sig.ts create mode 100644 runner_logs.json create mode 100644 vibn-frontend/app/api/ROUTES.md delete mode 100644 vibn-frontend/app/api/admin/check-sessions/route.ts delete mode 100644 vibn-frontend/app/api/admin/fix-project-workspace/route.ts create mode 100644 vibn-frontend/app/api/chat/route.ts.sed delete mode 100644 vibn-frontend/app/api/cursor/backfill/route.ts delete mode 100644 vibn-frontend/app/api/cursor/clear-imports/route.ts delete mode 100644 vibn-frontend/app/api/cursor/tag-sessions/route.ts delete mode 100644 vibn-frontend/app/api/debug/append-conversation/route.ts delete mode 100644 vibn-frontend/app/api/debug/check-links/route.ts delete mode 100644 vibn-frontend/app/api/debug/check-project/route.ts delete mode 100644 vibn-frontend/app/api/debug/context-sources/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-analysis/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-content-sample/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-conversations/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-relevant/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-sample-dates/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-session-summary/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-sessions/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-stats/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-unknown-sessions/route.ts delete mode 100644 vibn-frontend/app/api/debug/cursor-workspaces/route.ts delete mode 100644 vibn-frontend/app/api/debug/env/route.ts delete mode 100644 vibn-frontend/app/api/debug/first-project/route.ts delete mode 100644 vibn-frontend/app/api/debug/knowledge-items/route.ts delete mode 100644 vibn-frontend/app/api/debug/knowledge/route.ts delete mode 100644 vibn-frontend/app/api/debug/prisma/route.ts delete mode 100644 vibn-frontend/app/api/diagnose/route.ts delete mode 100644 vibn-frontend/app/api/firebase/test/route.ts create mode 100644 vibn-frontend/app/api/invites/[token]/route.ts create mode 100644 vibn-frontend/app/api/invites/route.ts create mode 100644 vibn-frontend/app/api/projects/[projectId]/secrets/[key]/route.ts create mode 100644 vibn-frontend/app/api/projects/[projectId]/secrets/route.ts delete mode 100644 vibn-frontend/app/api/sentry-example-api/route.ts delete mode 100644 vibn-frontend/app/api/test-token/route.ts create mode 100644 vibn-frontend/lib/integrations/brief-extract.ts create mode 100644 vibn-frontend/lib/server/api-handler.ts create mode 100644 vibn-frontend/lib/server/audit-log.ts create mode 100644 vibn-frontend/lib/server/coolify-webhook.ts create mode 100644 vibn-frontend/lib/server/dev-server-state.ts create mode 100644 vibn-frontend/lib/server/logger.ts create mode 100644 vibn-frontend/lib/server/rate-limit.ts create mode 100644 vibn-frontend/lib/server/timing-safe.ts create mode 100644 vibn-signin-css.html diff --git a/deploy_error.txt b/deploy_error.txt new file mode 100644 index 0000000..2e38d90 --- /dev/null +++ b/deploy_error.txt @@ -0,0 +1 @@ +[{"command":null,"output":"Docker 27.0.3 with BuildKit and Buildx detected on deployment server (localhost).","type":"stdout","timestamp":"2026-05-16T21:57:35.692039Z","hidden":false,"batch":1},{"command":null,"output":"Starting deployment of https:\/\/git.vibnai.com\/mark\/vibn-frontend.git:main to localhost.","type":"stdout","timestamp":"2026-05-16T21:57:35.738938Z","hidden":false,"batch":1,"order":2},{"command":null,"output":"Preparing container with helper image: ghcr.io\/coollabsio\/coolify-helper:1.0.13","type":"stdout","timestamp":"2026-05-16T21:57:36.487209Z","hidden":false,"batch":1,"order":3},{"command":"docker stop -t 30 iy53yujv0jkvol25maud6x85","output":"Error response from daemon: No such container: iy53yujv0jkvol25maud6x85","type":"stderr","timestamp":"2026-05-16T21:57:36.770916Z","hidden":true,"batch":1,"order":4},{"command":"docker run -d --network 'coolify' --name iy53yujv0jkvol25maud6x85 --rm -v \/var\/run\/docker.sock:\/var\/run\/docker.sock ghcr.io\/coollabsio\/coolify-helper:1.0.13","output":"b2d82459da497b707bc188557355be86e6ad1c39da582616da9d378ef6628ff0","type":"stdout","timestamp":"2026-05-16T21:57:37.284114Z","hidden":true,"batch":2,"order":5},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git ls-remote https:\/\/git.vibnai.com\/mark\/vibn-frontend.git refs\/heads\/main'","output":"814815af82be69ad6cf3c0e14c76e6aef316631e\trefs\/heads\/main","type":"stdout","timestamp":"2026-05-16T21:57:40.117959Z","hidden":true,"batch":3,"order":6},{"command":null,"output":"----------------------------------------","type":"stdout","timestamp":"2026-05-16T21:57:40.176981Z","hidden":false,"batch":1,"order":7},{"command":null,"output":"Importing https:\/\/git.vibnai.com\/mark\/vibn-frontend.git:main (commit sha 814815af82be69ad6cf3c0e14c76e6aef316631e) to \/artifacts\/iy53yujv0jkvol25maud6x85.","type":"stdout","timestamp":"2026-05-16T21:57:40.217750Z","hidden":false,"batch":1,"order":8},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Cloning into '\/artifacts\/iy53yujv0jkvol25maud6x85'...","type":"stderr","timestamp":"2026-05-16T21:57:41.048081Z","hidden":true,"batch":4,"order":9},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 85% (1506\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.420662Z","hidden":true,"batch":4,"order":10},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 86% (1509\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.445366Z","hidden":true,"batch":4,"order":11},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 87% (1526\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.456866Z","hidden":true,"batch":4,"order":12},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 88% (1544\/1754)\rUpdating files: 89% (1562\/1754)\rUpdating files: 90% (1579\/1754)\rUpdating files: 91% (1597\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.492382Z","hidden":true,"batch":4,"order":13},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 92% (1614\/1754)\rUpdating files: 93% (1632\/1754)\rUpdating files: 94% (1649\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.497986Z","hidden":true,"batch":4,"order":14},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 95% (1667\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.506559Z","hidden":true,"batch":4,"order":15},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 96% (1684\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.522218Z","hidden":true,"batch":4,"order":16},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 97% (1702\/1754)\rUpdating files: 98% (1719\/1754)","type":"stderr","timestamp":"2026-05-16T21:57:46.546454Z","hidden":true,"batch":4,"order":17},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"Updating files: 99% (1737\/1754)\rUpdating files: 100% (1754\/1754)\rUpdating files: 100% (1754\/1754), done.","type":"stderr","timestamp":"2026-05-16T21:57:46.559301Z","hidden":true,"batch":4,"order":18},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\''main'\\'' '\\''https:\/\/git.vibnai.com\/mark\/vibn-frontend.git'\\'' '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git fetch --depth=1 origin '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' && git -c advice.detachedHead=false checkout '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' >\/dev\/null 2>&1 && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && if [ -f .gitmodules ]; then sed -i \"s#git@\\(.*\\):#https:\/\/\\1\/#g\" '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\''\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git submodule update --init --recursive --depth=1; fi && cd '\\''\/artifacts\/iy53yujv0jkvol25maud6x85'\\'' && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\/dev\/null\" git lfs pull'","output":"From https:\/\/git.vibnai.com\/mark\/vibn-frontend\n * branch 814815af82be69ad6cf3c0e14c76e6aef316631e -> FETCH_HEAD","type":"stderr","timestamp":"2026-05-16T21:57:46.811295Z","hidden":true,"batch":4,"order":19},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'cd \/artifacts\/iy53yujv0jkvol25maud6x85 && git log -1 '\\''814815af82be69ad6cf3c0e14c76e6aef316631e'\\'' --pretty=%B'","output":"fix(deploy): install openssl in base docker image to fix prisma client initialization error during build phase","type":"stdout","timestamp":"2026-05-16T21:57:48.606485Z","hidden":true,"batch":6,"order":20},{"command":null,"output":"Image not found (y4cscsc8s08c8808go0448s0:814815af82be69ad6cf3c0e14c76e6aef316631e). Building new image.","type":"stdout","timestamp":"2026-05-16T21:57:48.851625Z","hidden":false,"batch":1,"order":21},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'cat \/artifacts\/iy53yujv0jkvol25maud6x85\/Dockerfile'","output":"cat: can't open '\/artifacts\/iy53yujv0jkvol25maud6x85\/Dockerfile': No such file or directory","type":"stderr","timestamp":"2026-05-16T21:57:50.719280Z","hidden":true,"batch":10,"order":22},{"command":null,"output":"Creating build-time .env file in \/artifacts (outside Docker context).","type":"stdout","timestamp":"2026-05-16T21:57:52.183793Z","hidden":true,"batch":1,"order":23},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'cat \/artifacts\/iy53yujv0jkvol25maud6x85\/Dockerfile'","output":"cat: can't open '\/artifacts\/iy53yujv0jkvol25maud6x85\/Dockerfile': No such file or directory","type":"stderr","timestamp":"2026-05-16T21:57:53.768951Z","hidden":true,"batch":13,"order":24},{"command":null,"output":"----------------------------------------","type":"stdout","timestamp":"2026-05-16T21:57:53.784223Z","hidden":false,"batch":1,"order":25},{"command":null,"output":"Building docker image started.","type":"stdout","timestamp":"2026-05-16T21:57:53.798083Z","hidden":false,"batch":1,"order":26},{"command":null,"output":"To check the current progress, click on Show Debug Logs.","type":"stdout","timestamp":"2026-05-16T21:57:53.812181Z","hidden":false,"batch":1,"order":27},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'cat \/artifacts\/build.sh'","output":"cd \/artifacts\/iy53yujv0jkvol25maud6x85 && set -a && source \/artifacts\/build-time.env && set +a && DOCKER_BUILDKIT=1 docker build --add-host coolify:10.0.1.11 --add-host coolify-db:10.0.1.3 --add-host coolify-realtime:10.0.1.9 --add-host coolify-redis:10.0.1.2 --add-host kggs4ogckc0w8ggwkkk88kck:10.0.1.13 --add-host kggs4ogckc0w8ggwkkk88kck-proxy:10.0.1.6 --add-host mh20hmj0h7pg9ftt0upo8s8p:10.0.1.24 --add-host p4dpjwv9p188h3y21c4xgiwy:10.0.1.21 --add-host q8i3lfauirs97awl4pieqbme:10.0.1.16 --add-host qckwo4g8gs8kw08gkgc0ss0g:10.0.1.8 --add-host qckwo4g8gs8kw08gkgc0ss0g-proxy:10.0.1.20 --add-host rlwvhmh25r7n2g2pbzygjlms:10.0.1.30 --add-host vibn-dev-dm3hqkyjknucuehfmqb75627:10.0.1.14 --add-host vibn-dev-lbhz4nd7wllowjlnwm1tmhu3:10.0.1.10 --add-host yoscw0og00gkgcsgswoskc8c:10.0.1.17 --add-host yoscw0og00gkgcsgswoskc8c-proxy:10.0.1.7 --add-host zlxavhunxe6vit057ntt7imx:10.0.1.27 --network host -f \/artifacts\/iy53yujv0jkvol25maud6x85\/Dockerfile --progress plain -t y4cscsc8s08c8808go0448s0:814815af82be69ad6cf3c0e14c76e6aef316631e --build-arg COOLIFY_URL --build-arg COOLIFY_BRANCH --build-arg COOLIFY_RESOURCE_UUID --build-arg OPENSRS_API_KEY_LIVE --build-arg GITEA_API_TOKEN --build-arg NEXTAUTH_URL --build-arg COOLIFY_SERVER_UUID --build-arg GITEA_WEBHOOK_SECRET --build-arg AGENT_RUNNER_SECRET --build-arg GEMINI_MODEL --build-arg NEXTAUTH_SECRET --build-arg NEXT_PUBLIC_APP_URL --build-arg GOOGLE_SERVICE_ACCOUNT_KEY_B64 --build-arg GITEA_API_URL --build-arg GITEA_ADMIN_USER --build-arg GOOGLE_CLIENT_SECRET --build-arg AGENT_RUNNER_URL --build-arg ADMIN_MIGRATE_SECRET --build-arg GOOGLE_API_KEY --build-arg COOLIFY_API_TOKEN --build-arg OPENSRS_RESELLER_USERNAME --build-arg OPENSRS_API_KEY_TEST --build-arg OPENSRS_PORT --build-arg OPENSRS_MODE --build-arg OPENSRS_CURRENCY --build-arg GITHUB_CLIENT_ID --build-arg GITHUB_CLIENT_SECRET --build-arg COOLIFY_SSH_HOST --build-arg COOLIFY_SSH_PORT --build-arg COOLIFY_SSH_USER --build-arg COOLIFY_SSH_PRIVATE_KEY_B64 --build-arg GCP_PROJECT_ID --build-arg GOOGLE_CLIENT_ID --build-arg OPENSRS_HOST_LIVE --build-arg SENTRY_AUTH_TOKEN --build-arg GITEA_USERNAME --build-arg NEXT_PUBLIC_SENTRY_DSN --build-arg INFRA_HEALTH_SECRET --build-arg DATABASE_URL --build-arg OPENSRS_HOST_TEST --build-arg VIBN_SECRETS_KEY --build-arg COOLIFY_BUILD_SECRETS_HASH=8139843e870e13b929b1a153d03a63ab44957aa859be24b76062228914d14f84 --build-arg 'COOLIFY_URL' --build-arg 'COOLIFY_BRANCH' --build-arg 'COOLIFY_RESOURCE_UUID' \/artifacts\/iy53yujv0jkvol25maud6x85","type":"stdout","timestamp":"2026-05-16T21:57:54.749901Z","hidden":true,"batch":14,"order":28},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'bash \/artifacts\/build.sh'","output":"#0 building with \"default\" instance using docker driver\n\n#1 [internal] load build definition from Dockerfile\n#1 transferring dockerfile: 2B 0.0s done\n#1 DONE 0.1s","type":"stderr","timestamp":"2026-05-16T21:57:56.039046Z","hidden":true,"batch":14,"order":29},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'bash \/artifacts\/build.sh'","output":"ERROR: failed to build: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory","type":"stderr","timestamp":"2026-05-16T21:57:56.132031Z","hidden":true,"batch":14,"order":30},{"command":"docker exec iy53yujv0jkvol25maud6x85 bash -c 'bash \/artifacts\/build.sh'","output":"exit status 1","type":"stderr","timestamp":"2026-05-16T21:57:56.144084Z","hidden":true,"batch":14,"order":31},{"command":null,"output":"========================================","type":"stderr","timestamp":"2026-05-16T21:57:56.248895Z","hidden":false,"batch":1,"order":32},{"command":null,"output":"Deployment failed: Command execution failed (exit code 1): docker exec iy53yujv0jkvol25maud6x85 bash -c 'bash \/artifacts\/build.sh'\nError: #0 building with \"default\" instance using docker driver\n\n#1 [internal] load build definition from Dockerfile\n#1 transferring dockerfile: 2B 0.0s done\n#1 DONE 0.1s\nERROR: failed to build: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory\nexit status 1","type":"stderr","timestamp":"2026-05-16T21:57:56.259239Z","hidden":false,"batch":1,"order":33},{"command":null,"output":"Error type: App\\Exceptions\\DeploymentException","type":"stderr","timestamp":"2026-05-16T21:57:56.271042Z","hidden":true,"batch":1,"order":34},{"command":null,"output":"Error code: 0","type":"stderr","timestamp":"2026-05-16T21:57:56.295409Z","hidden":true,"batch":1,"order":35},{"command":null,"output":"Location: \/var\/www\/html\/app\/Traits\/ExecuteRemoteCommand.php:242","type":"stderr","timestamp":"2026-05-16T21:57:56.323687Z","hidden":true,"batch":1,"order":36},{"command":null,"output":"Stack trace (first 5 lines):","type":"stderr","timestamp":"2026-05-16T21:57:56.348272Z","hidden":true,"batch":1,"order":37},{"command":null,"output":"#0 \/var\/www\/html\/app\/Traits\/ExecuteRemoteCommand.php(106): App\\Jobs\\ApplicationDeploymentJob->executeCommandWithProcess()","type":"stderr","timestamp":"2026-05-16T21:57:56.386159Z","hidden":true,"batch":1,"order":38},{"command":null,"output":"#1 \/var\/www\/html\/vendor\/laravel\/framework\/src\/Illuminate\/Collections\/Traits\/EnumeratesValues.php(275): App\\Jobs\\ApplicationDeploymentJob->{closure:App\\Traits\\ExecuteRemoteCommand::execute_remote_command():72}()","type":"stderr","timestamp":"2026-05-16T21:57:56.404446Z","hidden":true,"batch":1,"order":39},{"command":null,"output":"#2 \/var\/www\/html\/app\/Traits\/ExecuteRemoteCommand.php(72): Illuminate\\Support\\Collection->each()","type":"stderr","timestamp":"2026-05-16T21:57:56.415183Z","hidden":true,"batch":1,"order":40},{"command":null,"output":"#3 \/var\/www\/html\/app\/Jobs\/ApplicationDeploymentJob.php(3290): App\\Jobs\\ApplicationDeploymentJob->execute_remote_command()","type":"stderr","timestamp":"2026-05-16T21:57:56.426721Z","hidden":true,"batch":1,"order":41},{"command":null,"output":"#4 \/var\/www\/html\/app\/Jobs\/ApplicationDeploymentJob.php(898): App\\Jobs\\ApplicationDeploymentJob->build_image()","type":"stderr","timestamp":"2026-05-16T21:57:56.452359Z","hidden":true,"batch":1,"order":42},{"command":null,"output":"========================================","type":"stderr","timestamp":"2026-05-16T21:57:56.466279Z","hidden":false,"batch":1,"order":43},{"command":null,"output":"Deployment failed. Removing the new version of your application.","type":"stderr","timestamp":"2026-05-16T21:57:56.489343Z","hidden":false,"batch":1,"order":44},{"command":null,"output":"Gracefully shutting down build container: iy53yujv0jkvol25maud6x85","type":"stdout","timestamp":"2026-05-16T21:57:57.505440Z","hidden":false,"batch":1,"order":45},{"command":"docker stop -t 30 iy53yujv0jkvol25maud6x85","output":"iy53yujv0jkvol25maud6x85","type":"stdout","timestamp":"2026-05-16T21:57:58.427355Z","hidden":true,"batch":17,"order":46}] diff --git a/deploy_logs.json b/deploy_logs.json new file mode 100644 index 0000000..3c8b9a2 --- /dev/null +++ b/deploy_logs.json @@ -0,0 +1 @@ +{"id":722,"application":{"id":8,"repository_project_id":null,"uuid":"y4cscsc8s08c8808go0448s0","name":"vibn-frontend","fqdn":"https:\/\/vibnai.com,https:\/\/www.vibnai.com","config_hash":"58d68c8425a4d019fa30f44e0dba0b90","git_repository":"https:\/\/git.vibnai.com\/mark\/vibn-frontend.git","git_branch":"main","git_commit_sha":"HEAD","git_full_url":null,"docker_registry_image_name":null,"docker_registry_image_tag":null,"build_pack":"dockerfile","static_image":"nginx:alpine","install_command":null,"build_command":null,"start_command":null,"ports_exposes":"3000","ports_mappings":null,"base_directory":"\/","publish_directory":null,"health_check_path":"\/","health_check_port":"3000","health_check_host":"127.0.0.1","health_check_method":"GET","health_check_return_code":200,"health_check_scheme":"http","health_check_response_text":null,"health_check_interval":5,"health_check_timeout":5,"health_check_retries":15,"health_check_start_period":90,"limits_memory":"0","limits_memory_swap":"0","limits_memory_swappiness":60,"limits_memory_reservation":"0","limits_cpus":"0","limits_cpuset":null,"limits_cpu_shares":1024,"status":"running:healthy","preview_url_template":"{{pr_id}}.{{domain}}","destination_type":"App\\Models\\StandaloneDocker","destination_id":0,"source_type":null,"source_id":null,"private_key_id":null,"environment_id":1,"created_at":"2026-02-16T22:29:56.000000Z","updated_at":"2026-05-14T18:47:02.000000Z","description":"VIBN Frontend - Next.js Application","dockerfile":null,"health_check_enabled":true,"dockerfile_location":"\/Dockerfile","custom_labels":"dHJhZWZpay5lbmFibGU9dHJ1ZQp0cmFlZmlrLmh0dHAubWlkZGxld2FyZXMuZ3ppcC5jb21wcmVzcz10cnVlCnRyYWVmaWsuaHR0cC5taWRkbGV3YXJlcy5yZWRpcmVjdC10by1odHRwcy5yZWRpcmVjdHNjaGVtZS5zY2hlbWU9aHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5lbnRyeVBvaW50cz1odHRwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAubWlkZGxld2FyZXM9cmVkaXJlY3QtdG8taHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5ydWxlPUhvc3QoYHZpYm5haS5jb21gKSAmJiBQYXRoUHJlZml4KGAvYCkKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5zZXJ2aWNlPWh0dHAtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5lbnRyeVBvaW50cz1odHRwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAubWlkZGxld2FyZXM9cmVkaXJlY3QtdG8taHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5ydWxlPUhvc3QoYHd3dy52aWJuYWkuY29tYCkgJiYgUGF0aFByZWZpeChgL2ApCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAuc2VydmljZT1odHRwLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLmVudHJ5UG9pbnRzPWh0dHBzCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLm1pZGRsZXdhcmVzPWd6aXAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAucnVsZT1Ib3N0KGB2aWJuYWkuY29tYCkgJiYgUGF0aFByZWZpeChgL2ApCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLnNlcnZpY2U9aHR0cHMtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAudGxzLmNlcnRyZXNvbHZlcj1sZXRzZW5jcnlwdAp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC50bHM9dHJ1ZQp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5lbnRyeVBvaW50cz1odHRwcwp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5taWRkbGV3YXJlcz1nemlwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLnJ1bGU9SG9zdChgd3d3LnZpYm5haS5jb21gKSAmJiBQYXRoUHJlZml4KGAvYCkKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAuc2VydmljZT1odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMAp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC50bHMuY2VydHJlc29sdmVyPWxldHNlbmNyeXB0CnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLnRscz10cnVlCnRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5odHRwLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLmxvYWRiYWxhbmNlci5zZXJ2ZXIucG9ydD0zMDAwCnRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5odHRwLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLmxvYWRiYWxhbmNlci5zZXJ2ZXIucG9ydD0zMDAwCnRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5odHRwcy0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5sb2FkYmFsYW5jZXIuc2VydmVyLnBvcnQ9MzAwMAp0cmFlZmlrLmh0dHAuc2VydmljZXMuaHR0cHMtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAubG9hZGJhbGFuY2VyLnNlcnZlci5wb3J0PTMwMDAKY2FkZHlfMC5lbmNvZGU9enN0ZCBnemlwCmNhZGR5XzAuaGFuZGxlX3BhdGguMF9yZXZlcnNlX3Byb3h5PXt7dXBzdHJlYW1zIDMwMDB9fQpjYWRkeV8wLmhhbmRsZV9wYXRoPS8qCmNhZGR5XzAuaGVhZGVyPS1TZXJ2ZXIKY2FkZHlfMC50cnlfZmlsZXM9e3BhdGh9IC9pbmRleC5odG1sIC9pbmRleC5waHAKY2FkZHlfMD1odHRwczovL3ZpYm5haS5jb20KY2FkZHlfMS5lbmNvZGU9enN0ZCBnemlwCmNhZGR5XzEuaGFuZGxlX3BhdGguMV9yZXZlcnNlX3Byb3h5PXt7dXBzdHJlYW1zIDMwMDB9fQpjYWRkeV8xLmhhbmRsZV9wYXRoPS8qCmNhZGR5XzEuaGVhZGVyPS1TZXJ2ZXIKY2FkZHlfMS50cnlfZmlsZXM9e3BhdGh9IC9pbmRleC5odG1sIC9pbmRleC5waHAKY2FkZHlfMT1odHRwczovL3d3dy52aWJuYWkuY29tCmNhZGR5X2luZ3Jlc3NfbmV0d29yaz1jb29saWZ5","dockerfile_target_build":null,"manual_webhook_secret_github":null,"manual_webhook_secret_gitlab":null,"docker_compose_location":"\/docker-compose.yaml","docker_compose":null,"docker_compose_raw":null,"docker_compose_domains":null,"deleted_at":null,"docker_compose_custom_start_command":null,"docker_compose_custom_build_command":null,"swarm_replicas":1,"swarm_placement_constraints":null,"manual_webhook_secret_bitbucket":null,"custom_docker_run_options":null,"post_deployment_command":null,"post_deployment_command_container":null,"pre_deployment_command":null,"pre_deployment_command_container":null,"watch_paths":null,"custom_healthcheck_found":false,"manual_webhook_secret_gitea":"c23f93114f0378ff15cbd863e3fbd23bb5c127ceace1756b","redirect":"both","compose_parsing_version":"5","last_online_at":"2026-05-14 18:47:02","custom_nginx_configuration":"","custom_network_aliases":null,"is_http_basic_auth_enabled":false,"http_basic_auth_username":null,"http_basic_auth_password":null,"restart_count":0,"last_restart_at":null,"last_restart_type":null,"health_check_type":"http","health_check_command":null,"additional_servers_count":0,"additional_networks_count":0,"server_status":true,"environment":{"id":1,"name":"production","project_id":1,"created_at":"2026-02-14T21:49:21.000000Z","updated_at":"2026-02-14T21:49:21.000000Z","description":null,"uuid":"foskksoccksk0kc4g8sk88ok","project":{"id":1,"uuid":"f4owwggokksgw0ogo0844os0","name":"Vibn","description":"","team_id":0,"created_at":"2026-02-14T21:49:21.000000Z","updated_at":"2026-02-16T03:38:00.000000Z","team":{"id":0,"name":"Root Team","description":null,"personal_team":true,"created_at":"2026-02-14T21:47:46.000000Z","updated_at":"2026-02-14T21:49:26.000000Z","show_boarding":false,"custom_server_limit":null}}},"additional_servers":[],"destination":{"id":0,"name":"coolify","uuid":"zkogkggkw0wg40gccks80oo0","network":"coolify","server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:41:51.000000Z","server":{"id":0,"uuid":"jws4g4cgssss4cw48s488woc","name":"localhost","description":"This is the server where Coolify is running on. Don't delete this!","ip":"host.docker.internal","port":22,"user":"root","team_id":0,"private_key_id":0,"proxy":{"type":"TRAEFIK","status":"running","last_saved_settings":null,"last_applied_settings":null,"redirect_enabled":true,"force_stop":false},"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-05-14T18:46:33.000000Z","unreachable_notification_sent":false,"unreachable_count":0,"high_disk_usage_notification_sent":false,"log_drain_notification_sent":false,"swarm_cluster":null,"validation_logs":null,"sentinel_updated_at":"2026-05-14 18:46:33","deleted_at":null,"ip_previous":null,"hetzner_server_id":null,"cloud_provider_token_id":null,"hetzner_server_status":null,"is_validating":false,"detected_traefik_version":"3.6.8","traefik_outdated_info":{"current":"3.6.8","latest":"3.6.11","type":"patch_update","checked_at":"2026-05-10T00:00:42+00:00"},"server_metadata":null,"is_coolify_host":true,"settings":{"id":1,"is_swarm_manager":false,"is_jump_server":false,"is_build_server":false,"is_reachable":true,"is_usable":true,"server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:49:13.000000Z","wildcard_domain":null,"is_cloudflare_tunnel":false,"is_logdrain_newrelic_enabled":false,"logdrain_newrelic_license_key":null,"logdrain_newrelic_base_uri":null,"is_logdrain_highlight_enabled":false,"logdrain_highlight_project_id":null,"is_logdrain_axiom_enabled":false,"logdrain_axiom_dataset_name":null,"logdrain_axiom_api_key":null,"is_swarm_worker":false,"is_logdrain_custom_enabled":false,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"concurrent_builds":2,"dynamic_timeout":3600,"force_disabled":false,"is_metrics_enabled":false,"generate_exact_labels":false,"force_docker_cleanup":true,"docker_cleanup_frequency":"0 *\/6 * * *","docker_cleanup_threshold":75,"server_timezone":"UTC","delete_unused_volumes":false,"delete_unused_networks":false,"is_sentinel_enabled":true,"sentinel_token":"eyJpdiI6Ii9MSGRUb1JDWDZUSGE2b2dPVU5ka2c9PSIsInZhbHVlIjoiRVVkYXBZYmtIenRtTVBqK01GRnhJNERrT3RwLzhYK3EwT2hIM2JtOS9TeGdZNEl2VWRHczBHYXgzQTJ4UnpJTkluVU1RRXF1SVVXREZBZVpyaU1TZEE9PSIsIm1hYyI6IjdjZjYxNmEzZTM2OThjYjA4MGVlYzBkOGZlOGNmOTU1ZTA5MjJmZTY5ZGQ5OTczZWRkYTRkZDQ0ZDMzY2VjNzciLCJ0YWciOiIifQ==","sentinel_metrics_refresh_rate_seconds":10,"sentinel_metrics_history_days":7,"sentinel_push_interval_seconds":60,"sentinel_custom_url":"http:\/\/host.docker.internal:8000","server_disk_usage_notification_threshold":80,"is_sentinel_debug_enabled":false,"server_disk_usage_check_frequency":"0 23 * * *","is_terminal_enabled":true,"deployment_queue_limit":25,"disable_application_image_retention":false}}}},"application_id":"8","application_name":"vibn-frontend","build_server_id":null,"commit":"32a4812383734eea005a15a5bfd8a8127f4e259b","commit_message":"design(ui): brighten hero breadcrumb text contrast","current_process_id":"3951450","deployment_url":"\/project\/f4owwggokksgw0ogo0844os0\/environment\/foskksoccksk0kc4g8sk88ok\/application\/y4cscsc8s08c8808go0448s0\/deployment\/y6wyqjod8h2mbmzpmobeoozd","deployment_uuid":"y6wyqjod8h2mbmzpmobeoozd","destination_id":"0","docker_registry_image_tag":null,"finished_at":"2026-05-14T18:46:09.000000Z","force_rebuild":false,"git_type":null,"horizon_job_id":"3b757c86-bdb5-492b-a56e-a9fa3c307516","horizon_job_worker":"e55bacf73ffc","is_api":true,"is_webhook":false,"logs":"[{\"command\":null,\"output\":\"Docker 27.0.3 with BuildKit and Buildx detected on deployment server (localhost).\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:55.202830Z\",\"hidden\":false,\"batch\":1},{\"command\":null,\"output\":\"Starting deployment of https:\\\/\\\/git.vibnai.com\\\/mark\\\/vibn-frontend.git:main to localhost.\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:55.211689Z\",\"hidden\":false,\"batch\":1,\"order\":2},{\"command\":null,\"output\":\"Preparing container with helper image: ghcr.io\\\/coollabsio\\\/coolify-helper:1.0.13\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:55.535302Z\",\"hidden\":false,\"batch\":1,\"order\":3},{\"command\":\"docker stop -t 30 y6wyqjod8h2mbmzpmobeoozd\",\"output\":\"Error response from daemon: No such container: y6wyqjod8h2mbmzpmobeoozd\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:45:55.783295Z\",\"hidden\":true,\"batch\":1,\"order\":4},{\"command\":\"docker run -d --network 'coolify' --name y6wyqjod8h2mbmzpmobeoozd --rm -v \\\/var\\\/run\\\/docker.sock:\\\/var\\\/run\\\/docker.sock ghcr.io\\\/coollabsio\\\/coolify-helper:1.0.13\",\"output\":\"1299fc850ee83025e2e659b2254716e885280a0e4ca155500af7dfbfc64ab76a\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:56.065220Z\",\"hidden\":true,\"batch\":2,\"order\":5},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'GIT_SSH_COMMAND=\\\"ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git ls-remote https:\\\/\\\/git.vibnai.com\\\/mark\\\/vibn-frontend.git refs\\\/heads\\\/main'\",\"output\":\"32a4812383734eea005a15a5bfd8a8127f4e259b\\trefs\\\/heads\\\/main\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:57.707790Z\",\"hidden\":true,\"batch\":3,\"order\":6},{\"command\":null,\"output\":\"----------------------------------------\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:57.733196Z\",\"hidden\":false,\"batch\":1,\"order\":7},{\"command\":null,\"output\":\"Importing https:\\\/\\\/git.vibnai.com\\\/mark\\\/vibn-frontend.git:main (commit sha 32a4812383734eea005a15a5bfd8a8127f4e259b) to \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd.\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:45:57.752432Z\",\"hidden\":false,\"batch\":1,\"order\":8},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\\\''main'\\\\'' '\\\\''https:\\\/\\\/git.vibnai.com\\\/mark\\\/vibn-frontend.git'\\\\'' '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && cd '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && GIT_SSH_COMMAND=\\\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git fetch --depth=1 origin '\\\\''32a4812383734eea005a15a5bfd8a8127f4e259b'\\\\'' && git -c advice.detachedHead=false checkout '\\\\''32a4812383734eea005a15a5bfd8a8127f4e259b'\\\\'' >\\\/dev\\\/null 2>&1 && cd '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && if [ -f .gitmodules ]; then sed -i \\\"s#git@\\\\(.*\\\\):#https:\\\/\\\/\\\\1\\\/#g\\\" '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\''\\\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\\\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git submodule update --init --recursive --depth=1; fi && cd '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && GIT_SSH_COMMAND=\\\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git lfs pull'\",\"output\":\"Cloning into '\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'...\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:45:58.176679Z\",\"hidden\":true,\"batch\":4,\"order\":9},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'git clone --depth=1 --recurse-submodules --shallow-submodules -b '\\\\''main'\\\\'' '\\\\''https:\\\/\\\/git.vibnai.com\\\/mark\\\/vibn-frontend.git'\\\\'' '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && cd '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && GIT_SSH_COMMAND=\\\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git fetch --depth=1 origin '\\\\''32a4812383734eea005a15a5bfd8a8127f4e259b'\\\\'' && git -c advice.detachedHead=false checkout '\\\\''32a4812383734eea005a15a5bfd8a8127f4e259b'\\\\'' >\\\/dev\\\/null 2>&1 && cd '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && if [ -f .gitmodules ]; then sed -i \\\"s#git@\\\\(.*\\\\):#https:\\\/\\\/\\\\1\\\/#g\\\" '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\''\\\/.gitmodules || true && git submodule sync && GIT_SSH_COMMAND=\\\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git submodule update --init --recursive --depth=1; fi && cd '\\\\''\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd'\\\\'' && GIT_SSH_COMMAND=\\\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=\\\/dev\\\/null\\\" git lfs pull'\",\"output\":\"From https:\\\/\\\/git.vibnai.com\\\/mark\\\/vibn-frontend\\n * branch 32a4812383734eea005a15a5bfd8a8127f4e259b -> FETCH_HEAD\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:45:59.239072Z\",\"hidden\":true,\"batch\":4,\"order\":10},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'cd \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd && git log -1 '\\\\''32a4812383734eea005a15a5bfd8a8127f4e259b'\\\\'' --pretty=%B'\",\"output\":\"design(ui): brighten hero breadcrumb text contrast\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:00.332382Z\",\"hidden\":true,\"batch\":6,\"order\":11},{\"command\":null,\"output\":\"Image not found (y4cscsc8s08c8808go0448s0:32a4812383734eea005a15a5bfd8a8127f4e259b). Building new image.\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:00.629367Z\",\"hidden\":false,\"batch\":1,\"order\":12},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'cat \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd\\\/Dockerfile'\",\"output\":\"cat: can't open '\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd\\\/Dockerfile': No such file or directory\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:02.489659Z\",\"hidden\":true,\"batch\":10,\"order\":13},{\"command\":null,\"output\":\"Creating build-time .env file in \\\/artifacts (outside Docker context).\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:03.830024Z\",\"hidden\":true,\"batch\":1,\"order\":14},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'cat \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd\\\/Dockerfile'\",\"output\":\"cat: can't open '\\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd\\\/Dockerfile': No such file or directory\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:06.146453Z\",\"hidden\":true,\"batch\":13,\"order\":15},{\"command\":null,\"output\":\"----------------------------------------\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:06.176327Z\",\"hidden\":false,\"batch\":1,\"order\":16},{\"command\":null,\"output\":\"Building docker image started.\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:06.197573Z\",\"hidden\":false,\"batch\":1,\"order\":17},{\"command\":null,\"output\":\"To check the current progress, click on Show Debug Logs.\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:06.218852Z\",\"hidden\":false,\"batch\":1,\"order\":18},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'cat \\\/artifacts\\\/build.sh'\",\"output\":\"cd \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd && set -a && source \\\/artifacts\\\/build-time.env && set +a && DOCKER_BUILDKIT=1 docker build --add-host coolify:10.0.1.11 --add-host coolify-db:10.0.1.3 --add-host coolify-realtime:10.0.1.9 --add-host coolify-redis:10.0.1.2 --add-host kggs4ogckc0w8ggwkkk88kck:10.0.1.13 --add-host kggs4ogckc0w8ggwkkk88kck-proxy:10.0.1.6 --add-host mh20hmj0h7pg9ftt0upo8s8p:10.0.1.24 --add-host p4dpjwv9p188h3y21c4xgiwy:10.0.1.21 --add-host q8i3lfauirs97awl4pieqbme:10.0.1.16 --add-host qckwo4g8gs8kw08gkgc0ss0g:10.0.1.8 --add-host qckwo4g8gs8kw08gkgc0ss0g-proxy:10.0.1.20 --add-host rlwvhmh25r7n2g2pbzygjlms:10.0.1.30 --add-host vibn-dev-dm3hqkyjknucuehfmqb75627:10.0.1.14 --add-host vibn-dev-lbhz4nd7wllowjlnwm1tmhu3:10.0.1.10 --add-host yoscw0og00gkgcsgswoskc8c:10.0.1.17 --add-host yoscw0og00gkgcsgswoskc8c-proxy:10.0.1.7 --add-host zlxavhunxe6vit057ntt7imx:10.0.1.27 --network host -f \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd\\\/Dockerfile --progress plain -t y4cscsc8s08c8808go0448s0:32a4812383734eea005a15a5bfd8a8127f4e259b --build-arg COOLIFY_URL --build-arg COOLIFY_BRANCH --build-arg COOLIFY_RESOURCE_UUID --build-arg OPENSRS_API_KEY_LIVE --build-arg GITEA_API_TOKEN --build-arg NEXTAUTH_URL --build-arg COOLIFY_SERVER_UUID --build-arg GITEA_WEBHOOK_SECRET --build-arg AGENT_RUNNER_SECRET --build-arg GEMINI_MODEL --build-arg NEXTAUTH_SECRET --build-arg NEXT_PUBLIC_APP_URL --build-arg GOOGLE_SERVICE_ACCOUNT_KEY_B64 --build-arg GITEA_API_URL --build-arg GITEA_ADMIN_USER --build-arg GOOGLE_CLIENT_SECRET --build-arg AGENT_RUNNER_URL --build-arg ADMIN_MIGRATE_SECRET --build-arg GOOGLE_API_KEY --build-arg COOLIFY_API_TOKEN --build-arg OPENSRS_RESELLER_USERNAME --build-arg OPENSRS_API_KEY_TEST --build-arg OPENSRS_PORT --build-arg OPENSRS_MODE --build-arg OPENSRS_CURRENCY --build-arg GITHUB_CLIENT_ID --build-arg GITHUB_CLIENT_SECRET --build-arg COOLIFY_SSH_HOST --build-arg COOLIFY_SSH_PORT --build-arg COOLIFY_SSH_USER --build-arg COOLIFY_SSH_PRIVATE_KEY_B64 --build-arg GCP_PROJECT_ID --build-arg GOOGLE_CLIENT_ID --build-arg OPENSRS_HOST_LIVE --build-arg SENTRY_AUTH_TOKEN --build-arg GITEA_USERNAME --build-arg NEXT_PUBLIC_SENTRY_DSN --build-arg INFRA_HEALTH_SECRET --build-arg DATABASE_URL --build-arg OPENSRS_HOST_TEST --build-arg VIBN_SECRETS_KEY --build-arg COOLIFY_BUILD_SECRETS_HASH=03fb6d60adb1c477e10fc8b43e21ba4312d09b95d0cfa99168b60aef8a513707 --build-arg 'COOLIFY_URL' --build-arg 'COOLIFY_BRANCH' --build-arg 'COOLIFY_RESOURCE_UUID' \\\/artifacts\\\/y6wyqjod8h2mbmzpmobeoozd\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:07.568468Z\",\"hidden\":true,\"batch\":14,\"order\":19},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'bash \\\/artifacts\\\/build.sh'\",\"output\":\"#0 building with \\\"default\\\" instance using docker driver\\n\\n#1 [internal] load build definition from Dockerfile\\n#1 transferring dockerfile:\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:08.758342Z\",\"hidden\":true,\"batch\":14,\"order\":20},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'bash \\\/artifacts\\\/build.sh'\",\"output\":\"#1 transferring dockerfile: 2B 0.0s done\\n#1 DONE 0.2s\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:08.950984Z\",\"hidden\":true,\"batch\":14,\"order\":21},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'bash \\\/artifacts\\\/build.sh'\",\"output\":\"ERROR: failed to build: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:08.966757Z\",\"hidden\":true,\"batch\":14,\"order\":22},{\"command\":\"docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'bash \\\/artifacts\\\/build.sh'\",\"output\":\"exit status 1\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:08.972325Z\",\"hidden\":true,\"batch\":14,\"order\":23},{\"command\":null,\"output\":\"========================================\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.143367Z\",\"hidden\":false,\"batch\":1,\"order\":24},{\"command\":null,\"output\":\"Deployment failed: Command execution failed (exit code 1): docker exec y6wyqjod8h2mbmzpmobeoozd bash -c 'bash \\\/artifacts\\\/build.sh'\\nError: #0 building with \\\"default\\\" instance using docker driver\\n\\n#1 [internal] load build definition from Dockerfile\\n#1 transferring dockerfile:\\n#1 transferring dockerfile: 2B 0.0s done\\n#1 DONE 0.2s\\nERROR: failed to build: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory\\nexit status 1\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.172726Z\",\"hidden\":false,\"batch\":1,\"order\":25},{\"command\":null,\"output\":\"Error type: App\\\\Exceptions\\\\DeploymentException\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.196736Z\",\"hidden\":true,\"batch\":1,\"order\":26},{\"command\":null,\"output\":\"Error code: 0\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.220352Z\",\"hidden\":true,\"batch\":1,\"order\":27},{\"command\":null,\"output\":\"Location: \\\/var\\\/www\\\/html\\\/app\\\/Traits\\\/ExecuteRemoteCommand.php:242\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.240357Z\",\"hidden\":true,\"batch\":1,\"order\":28},{\"command\":null,\"output\":\"Stack trace (first 5 lines):\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.264695Z\",\"hidden\":true,\"batch\":1,\"order\":29},{\"command\":null,\"output\":\"#0 \\\/var\\\/www\\\/html\\\/app\\\/Traits\\\/ExecuteRemoteCommand.php(106): App\\\\Jobs\\\\ApplicationDeploymentJob->executeCommandWithProcess()\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.286381Z\",\"hidden\":true,\"batch\":1,\"order\":30},{\"command\":null,\"output\":\"#1 \\\/var\\\/www\\\/html\\\/vendor\\\/laravel\\\/framework\\\/src\\\/Illuminate\\\/Collections\\\/Traits\\\/EnumeratesValues.php(275): App\\\\Jobs\\\\ApplicationDeploymentJob->{closure:App\\\\Traits\\\\ExecuteRemoteCommand::execute_remote_command():72}()\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.298778Z\",\"hidden\":true,\"batch\":1,\"order\":31},{\"command\":null,\"output\":\"#2 \\\/var\\\/www\\\/html\\\/app\\\/Traits\\\/ExecuteRemoteCommand.php(72): Illuminate\\\\Support\\\\Collection->each()\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.311709Z\",\"hidden\":true,\"batch\":1,\"order\":32},{\"command\":null,\"output\":\"#3 \\\/var\\\/www\\\/html\\\/app\\\/Jobs\\\/ApplicationDeploymentJob.php(3290): App\\\\Jobs\\\\ApplicationDeploymentJob->execute_remote_command()\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.325249Z\",\"hidden\":true,\"batch\":1,\"order\":33},{\"command\":null,\"output\":\"#4 \\\/var\\\/www\\\/html\\\/app\\\/Jobs\\\/ApplicationDeploymentJob.php(898): App\\\\Jobs\\\\ApplicationDeploymentJob->build_image()\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.338655Z\",\"hidden\":true,\"batch\":1,\"order\":34},{\"command\":null,\"output\":\"========================================\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.348379Z\",\"hidden\":false,\"batch\":1,\"order\":35},{\"command\":null,\"output\":\"Deployment failed. Removing the new version of your application.\",\"type\":\"stderr\",\"timestamp\":\"2026-05-14T18:46:09.358630Z\",\"hidden\":false,\"batch\":1,\"order\":36},{\"command\":null,\"output\":\"Gracefully shutting down build container: y6wyqjod8h2mbmzpmobeoozd\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:10.400671Z\",\"hidden\":false,\"batch\":1,\"order\":37},{\"command\":\"docker stop -t 30 y6wyqjod8h2mbmzpmobeoozd\",\"output\":\"y6wyqjod8h2mbmzpmobeoozd\",\"type\":\"stdout\",\"timestamp\":\"2026-05-14T18:46:11.039730Z\",\"hidden\":true,\"batch\":17,\"order\":38}]","only_this_server":false,"pull_request_id":0,"restart_only":false,"rollback":false,"server_id":0,"server_name":"localhost","status":"failed","created_at":"2026-05-14T18:45:53.000000Z","updated_at":"2026-05-14T18:46:11.000000Z"} \ No newline at end of file diff --git a/docs/API_QA_CHECKLIST.md b/docs/API_QA_CHECKLIST.md new file mode 100644 index 0000000..1728b59 --- /dev/null +++ b/docs/API_QA_CHECKLIST.md @@ -0,0 +1,127 @@ +# API QA Checklist + +> Comprehensive enhancement list for `vibn-frontend/app/api/` derived from the +> 2026-05-17 QA pass. Anchored to `BETA_LAUNCH_PLAN.md`. +> +> **Convention:** each item has an ID like `S-01` (Security), `A-01` (Auth/Arch), +> `B-01` (Beta blocker), `C-01` (Chat/AI pipeline), `R-01` (Reliability), +> `D-01` (Deletion/cleanup), `O-01` (Code Org). Tick the box as you ship. + +--- + +## Phase 1 — Foundations (`lib/server/*`) + +- [x] **F-01** `lib/server/api-handler.ts` — `withAuth`, `withTenantProject`, `withWorkspace`, `withAdminSecret` route wrappers. Every new route uses these instead of reimplementing the auth dance. +- [x] **F-02** `lib/server/logger.ts` — structured logger that takes `{turnId, projectId, route, userId}` and routes to `console.*` in dev, Sentry breadcrumb in prod. +- [x] **F-03** `lib/server/audit-log.ts` — `writeAuditLog({workspace, user, action, resourceType, resourceId, params, ok})` helper + migration for `audit_log` table. +- [x] **F-04** `lib/server/rate-limit.ts` — Postgres-backed sliding window. Default: 60 req/min per user per route. Per-route override via opts. +- [x] **F-05** `lib/server/coolify-webhook.ts` — verifyCoolifySignature(body, signature, secret). Mirrors `verifyWebhookSignature` from `lib/gitea.ts`. +- [x] **F-06** `lib/server/timing-safe.ts` — `timingSafeStringEq(a, b)` helper wrapping `crypto.timingSafeEqual` for every admin-secret bearer check. + +--- + +## Phase 2 — Deletions (security cleanup) + +These are unauthenticated routes that read/write tenant data using only a URL `projectId`. Delete them now; if anything legitimate calls one, we'll find out fast and reintroduce it under `withTenantProject`. + +- [x] **D-01** `app/api/debug/cursor-analysis` — Firestore dump +- [x] **D-02** `app/api/debug/cursor-content-sample` +- [x] **D-03** `app/api/debug/cursor-conversations` +- [x] **D-04** `app/api/debug/cursor-relevant` +- [x] **D-05** `app/api/debug/cursor-sample-dates` +- [x] **D-06** `app/api/debug/cursor-session-summary` +- [x] **D-07** `app/api/debug/cursor-sessions` +- [x] **D-08** `app/api/debug/cursor-stats` +- [x] **D-09** `app/api/debug/cursor-unknown-sessions` +- [x] **D-10** `app/api/debug/cursor-workspaces` +- [x] **D-11** `app/api/debug/append-conversation` +- [x] **D-12** `app/api/debug/check-links` +- [x] **D-13** `app/api/debug/check-project` +- [x] **D-14** `app/api/debug/context-sources` +- [x] **D-15** `app/api/debug/env` — leaks env-var presence +- [x] **D-16** `app/api/debug/first-project` +- [x] **D-17** `app/api/debug/knowledge` +- [x] **D-18** `app/api/debug/knowledge-items` +- [x] **D-19** `app/api/debug/prisma` +- [x] **D-20** `app/api/cursor/backfill` — comment says "TEMPORARY: no auth required" +- [x] **D-21** `app/api/cursor/clear-imports` — same +- [x] **D-22** `app/api/cursor/tag-sessions` — same +- [x] **D-23** `app/api/firebase/test` — writes/deletes Firestore on every call, no auth +- [x] **D-24** `app/api/sentry-example-api` — always throws; dev-only fixture +- [x] **D-25** `app/api/test-token` — server-side `auth.currentUser` (broken pattern) +- [x] **D-26** `app/api/diagnose` — info-discloses env vars + verifies arbitrary tokens +- [x] **D-27** `app/api/admin/check-sessions` — no auth, named `/admin/` +- [x] **D-28** `app/api/admin/fix-project-workspace` — no auth, accepts any project + +--- + +## Phase 3 — Auth gates + hardening on the remaining unauthenticated routes + +- [x] **S-01** `app/api/ai/chat` — wrap in `withTenantProject('projectId')`. Currently anyone can chat as any project. +- [x] **S-02** `app/api/ai/conversation` (GET, DELETE) — same. +- [x] **S-03** `app/api/ai/conversation/reset` — same. +- [x] **S-04** `app/api/context/summarize` — wrap in `withAuth`. No tenant scope needed; just stop unauth Gemini quota burn. +- [x] **S-05** `app/api/work-completed` — wrap in `withTenantProject('projectId')` and remove the literal-`1` fallback. +- [x] **S-06** `app/api/webhooks/coolify` — verify signature against `COOLIFY_WEBHOOK_SECRET` using `verifyCoolifySignature`. Reject on mismatch. +- [x] **S-07** `app/api/admin/migrate` — switch `secret !== incoming` to `timingSafeStringEq(secret, incoming)`. +- [x] **S-08** `app/api/admin/path-b/{disable,enable,idle-sweep,autosave}` — same. +- [x] **S-09** `app/api/admin/path-b/route.ts` — same. +- [x] **S-10** `app/api/internal/infra-health` — same. + +--- + +## Phase 4 — Chat / AI pipeline hardening + +`app/api/chat/route.ts` and `lib/ai/*` enhancements. + +- [x] **C-01** Lower `MAX_TOOL_ROUNDS` from 15 to 8. +- [x] **C-02** Tighten loop detection: hard-break at 3 identical fingerprints (was 5); add an absolute cap of 6 consecutive tool calls with no intervening assistant text. +- [x] **C-03** Add "Mode: respond first, act second" block at the top of `buildSystemPrompt` (above the existing Identity section). +- [x] **C-04** SSE heartbeat: emit `{type:"ping"}` every 25s while the loop is running (cleared on `safeClose` / `cancel`). +- [x] **C-05** `executeMcpTool` timeout: wrap each tool invocation in `Promise.race([exec, timeout(45_000)])`; surface as `tool_timeout` SSE event. +- [x] **C-06** `turnId`: generate a `crypto.randomUUID()` per chat turn; include in every log line and the first SSE chunk so we can correlate prod issues. +- [x] **C-07** Recovery-summary trigger expansion: also fire when the AI emitted no text for ≥4 rounds (not just on tool failure / round cap / loop break). +- [ ] **C-08** Deprecate `app/api/ai/chat`. Add `Deprecation: true` header + log line; redirect callers to `/api/chat` over 30 days, then delete. *(skipped this pass — needs migration tracking)* + +--- + +## Phase 5 — Beta gaps from `BETA_LAUNCH_PLAN.md` + +Each maps to a checked task in the plan that's not yet implemented in the API surface. + +- [x] **B-01 (P4.7)** `audit_log` table + writes from every mutating MCP tool in `app/api/mcp/route.ts` (`apps_create`, `apps_delete`, `apps_deploy`, `databases_create`, `databases_delete`, `domains_register`, `secrets_set`, `ship`). +- [x] **B-02 (P4.8)** Invite/waitlist endpoints: `POST /api/invites` (admin-only, creates token), `GET /api/invites/[token]` (validates), `POST /api/invites/[token]/redeem` (consumes on signup). +- [x] **B-03 (P6.B1)** `fs_project_dev_servers` table migration + `dev_server_start` MCP tool hook to upsert on success. +- [ ] **B-04 (P6.B2)** Auto-resume hook on project page mount. *(scaffolded; full wiring deferred since it touches the project layout page, which is outside `/api`)* +- [x] **B-05 (P6.C1)** SSE `plan` event protocol in `app/api/chat/route.ts` — emit `{type:"plan", taskId, text, status}` whenever `plan_task_add` / `plan_task_edit` fires within a turn. +- [x] **B-06 (P6.D2)** `fs_project_secrets` table + `POST /api/projects/[id]/secrets`, `GET /api/projects/[id]/secrets` (keys-only), `DELETE /api/projects/[id]/secrets/[key]`. Encrypted via existing `lib/crypto.ts` pattern. +- [x] **B-07 (P3.7)** `project_brief` MCP tool stub + extraction scaffold in `lib/integrations/brief-extract.ts`. Wired into `buildSystemPrompt` as `[PROJECT BRIEF]` block when `fs_projects.data.plan.brief` is non-empty. +- [ ] **B-08 (P2.5)** Per-request Sentry span+release annotation in every handler. *(deferred — needs Sentry SDK pattern audit across the codebase)* + +--- + +## Phase 6 — Reliability & observability + +- [x] **R-01** Adopt `lib/server/logger.ts` in `app/api/chat/route.ts` (highest-traffic route). +- [x] **R-02** Rate-limit `/api/chat`, `/api/context/summarize`, `/api/extension/link-project`, `/api/admin/migrate`. +- [ ] **R-03** Idempotency keys on webhook receivers (`(event_id, project_id)` unique constraint). *(deferred — Coolify event payload schema needs research)* +- [ ] **R-04** Per-tool cost/token accounting table `chat_costs`. *(deferred — needs pricing strategy)* + +--- + +## Phase 7 — Code organization + +- [ ] **O-01** Refactor the 8 highest-traffic routes onto `withAuth` / `withTenantProject` / `withWorkspace`. *(seeded with examples; bulk refactor deferred)* +- [ ] **O-02** Decompose `app/api/chat/route.ts` (1088 lines) into `lib/server/chat-{prompt,tool-loop,recovery,sse}.ts`. *(deferred — non-blocking refactor)* +- [x] **O-03** `app/api/ROUTES.md` — enumerate every route with `auth`, `tenant`, purpose. +- [ ] **O-04** Continue extracting MCP `toolXxx()` into `lib/mcp/tools/*.ts`. *(deferred — non-blocking)* + +--- + +## How to use this doc + +- Tick a box only when the change is committed AND the unit/smoke test passes. +- Items marked `(deferred — …)` are intentional cuts so this lands as one + reviewable batch. Re-open them in `AI_CAPABILITIES.md` after beta. +- Each phase commit message should reference the IDs it closes, e.g. + `feat(api): F-01..F-06 lib/server foundations`. diff --git a/docs/for-entrepenuers.md b/docs/for-entrepenuers.md new file mode 100644 index 0000000..edc721e --- /dev/null +++ b/docs/for-entrepenuers.md @@ -0,0 +1,6 @@ +For Entrepreneurs Who Build for Small BusinessesYou can build a business without ever picking up the phone.Most entrepreneurship advice is written by extroverts, for extroverts.Get out there. Network. Cold-call. Pitch in person. Build your personal brand. Post on LinkedIn every day. Hand out business cards. Show up at events. Hustle.If any of that drains you on contact, congratulations — every entrepreneurship podcast, book, and course has spent the last decade quietly telling you that you don't have what it takes.They were wrong.There's another way to build a real business. Quiet. Behind the curtain. Powered by the parts of you that already work — your taste, your craft, your patience, your ability to disappear into a problem for eight hours and emerge with something useful.That way is finally viable. It wasn't before. It is now.The shiftFor most of business history, you needed people skills to find customers. Cold outreach. Networking. Conferences. Sales calls. The work of getting in front of buyers required showing up as yourself, repeatedly, in rooms full of strangers. If that drained you, you had a real disadvantage.AI changes the math. The work of finding customers — the market research, the targeting, the outreach, the content, the social presence — can now be handled by software. Not faked. Not spammed. Done well, at scale, without you ever having to be the loud person in the room.That doesn't make introverts viable for the first time. It makes them advantaged for the first time. Because while the extroverts are spending their day in meetings and on networking calls, you can spend yours actually building the thing.The patient, careful, behind-the-curtain builder has always been undervalued. AI just made them powerful.What you can buildSmall business is the right place to start. It's underserved, it's enormous, and it doesn't require you to be a household name. You don't need to "go viral." You don't need a personal brand. You don't need an audience.You need a small business that has a problem, a tool that solves it, and a way for them to find you. Vibn handles two of those.You build the tool. Vibn finds the customers.A few things people are building: +A vertical SaaS for one type of small business — a booking and customer-management tool built specifically for tattoo studios, dog groomers, or small accounting practices +A custom-build practice where you build one-off systems for local small businesses, hand them over, and get paid once per project +A productized service — pick a single specific problem ("I'll build your shop a custom booking system in a week, $X flat") and run it as a quiet, profitable little machine +A tool for a niche you already know — if you used to work in restaurants, build for restaurants; if you used to do the books for trades, build for trades. Your prior life is your market research. +The unifying idea: small, specific, useful tools for small, specific businesses. Not the next billion-dollar SaaS. A real business that pays you well and that you actually enjoy running.How Vibn does the parts you don't want to doYou don't have to be the salesperson for your own business. You don't have to be the marketer, the social media manager, or the person dialing for dollars.Market research. Vibn helps you find what small businesses in a given niche actually need — the problems they're posting about, the tools they're complaining about, the gaps in their stack. You don't have to go to industry conferences to find this out. The signal is already online; Vibn surfaces it.Customer discovery. Vibn helps you find your first 100 customers through our Google partnership. Real businesses with real problems, identified for you. You decide who to reach out to (if you want to) or let the system do the outreach (if you don't).Content and social, on autopilot. Vibn writes, schedules, and posts your marketing across whatever channels matter for your niche. You don't have to be a "thought leader." You don't have to film yourself. Your business has a presence; you don't.You stay in the build. While the system does the parts that drain you, you do the parts that energize you — building the tool, refining it, talking to the small number of customers you actually want to talk to. The work matches the wiring.What this looks like in practicePick a niche. Maybe one you already know, or one you've quietly observed from a distance. Open Vibn. Describe what you want to build — say, a custom client portal for independent therapy practices, or a job-tracking tool for solo electricians, or a reservations system for small wineries.Vibn helps you research the niche, build the tool, host it, set up logins, and find your first customers. You spend your days in the part you love — shaping the product, talking (in writing, mostly) to the few customers who matter most, making it better.You don't need a co-founder. You don't need a team. You don't need an office, a network, a personal brand, or a podcast.You need a quiet room, a problem worth solving, and Vibn.Why introverts are about to have a momentFor a long time, "build a business" has meant "build a public version of yourself."It doesn't have to anymore.The new generation of small, profitable, sustainable businesses won't be built by founders posting on Twitter all day. They'll be built by quiet operators who pick a niche, build the right tool, let the system handle the noisy parts, and serve their customers carefully for years.You'll never see them on a "30 under 30" list. They'll be doing better than the people on the list.You can be one of them.Your role in the missionSmall business needs a generation of new builders — people who will quietly, carefully build the custom software that small businesses have needed for two decades and never gotten. Not in San Francisco. Not at scale. Not for a Series A. In every town, by every kind of builder, including the kind who'd rather not be the face of anything.You're who we built Vibn for.It's okay to be the person behind the curtain. The work still counts. The business is still real. The impact is still yours.Let's go build it. Quietly.[ Start building → ]Free to start · No credit card · Built in Canada \ No newline at end of file diff --git a/docs/for-freelancers.md b/docs/for-freelancers.md new file mode 100644 index 0000000..8b4f17e --- /dev/null +++ b/docs/for-freelancers.md @@ -0,0 +1,58 @@ +For Freelancers (rewritten) + +You're the craftsman of the AI economy. +Every small business in your town is running on a stack of eight to fifteen tools that don't fit, don't talk, and don't work the way the business actually runs. +The owner is gluing it together with spreadsheets and their own time. They're paying every month — forever — for software that was built for somebody else. +They don't need another integration. They don't need another dashboard. They need one tool, built for their business. +You're who builds it. + +The opportunity nobody is serving +For twenty years, custom software has been out of reach for small business. Building the right tool meant hiring a developer, paying $50,000, and waiting six months — for a business doing $400k a year. Nobody could afford it. So small businesses got herded into off-the-shelf SaaS that almost-but-not-quite fit, and you watched it happen. +That's over. +With AI doing the heavy lifting, a single freelancer can deliver in a week what used to take a dev team months. The full system that runs a small business — purpose-built, custom-fit, owned by the client. Not a plugin. Not a dashboard. The actual software the business runs on. +The work is real. The market is enormous. And almost nobody is doing it yet. + +Why this is the work to be doing +You can keep grinding for SaaS companies. You can build features nobody asked for, write marketing landing pages on contract, and wait for the next round of cuts. +Or you can walk into the bakery on your block, the dentist's office across the street, the bookkeeper one neighborhood over, and offer them the thing they've quietly wanted for years: the software their business has been trying to be built around. +You'd be the most valuable person they know. +Small business owners are not picky. They are hungry. They have been paying for software that doesn't fit for so long they've stopped imagining it could be different. They have been waiting — without knowing they were waiting — for someone like you to show up. +You're who shows up. + +What the work actually looks like +You meet a local business. You learn how they run — what tools they use, what those tools don't do, what they're working around with spreadsheets. You describe what they need to Vibn. The AI builds it. You shape it, refine it, polish it, hand it over. +The most common projects: + +A complete front-of-house system for a salon, med-spa, or studio — bookings, customer notes, packages, schedules, payments, marketing — replacing four or five subscriptions with one custom build +A custom shop management system for a trade business — jobs, crew, scheduling, quotes, invoices, customer history — built for how that specific business runs +A unified client portal for a service business — bookings, invoices, communications, document sharing, reviews — branded to the client, owned by the client +A full studio management system — classes, members, packages, attendance, marketing — purpose-built for one studio's exact model +A back-office operating system for a small operation that's outgrown spreadsheets but is never going to be big enough for "real" enterprise software +Custom-built replacements for the SaaS subscriptions that almost work — rebuilt to fit perfectly, no monthly rent, owned forever + +You're not building features. You're building the system that runs the business. + +A new kind of business +This is not contract dev work. It's not agency work. It's something else. +You're not selling time. You're selling outcomes. You're not building specs. You're sitting with a small business owner, hearing how they actually run things, and shaping software that fits them like a tailored suit. You hand over the keys when you're done. The client owns it forever. You get paid once, well, and move on. +Vibn does the work of an engineering team. You do the work of understanding the customer, finding the shape of their business, and building the tool that fits it. That's the part the AI can't do — yet, maybe ever. It's the part where craft lives. +The skill that matters now is taste. Listening to an owner describe their day, hearing what they don't say out loud, and recognizing the exact shape of the tool they've been needing. That's a craft. That's something to build a career on. + +How you make this a business +A few things freelancers are doing well: + +Pick a niche. Trades. Med-spas. Restaurants. Service businesses. Each niche has a recognizable shape — once you've built three systems for barbershops, the fourth takes a week. +Look for the spreadsheet. Every small business has the spreadsheet — the one they use because their real software can't do what they need. That spreadsheet is the brief. Whatever it does is the system you're building. +Charge for the outcome, not the hours. A custom system that replaces four SaaS subscriptions and fits the business perfectly is worth $5,000–$15,000 to the owner, regardless of how long it takes you. Don't price the work — price the result. +Build local, build a portfolio. Three businesses in your town becomes a case study. A case study becomes referrals. Referrals become a full pipeline. You don't need to scale beyond your community to make a great living. + + +Your role in the mission +Small business has been underserved by software for two decades — not because nobody could build the right tools, but because the math never worked. AI changes the math. +The fix isn't another SaaS company. The fix is a new generation of local builders who can deliver the actual software a small business should be running on. Custom-fit. Hand-delivered. Owned by the business. +That's you. +You're not just building tools. You're rebuilding the economics of small business software, one business at a time, in your community. +That's a real career. A real craft. A real way to spend the next ten years. +The work is here. The businesses are waiting. Let's go build. +[ Start your first project → ] +Free to start · No credit card · Built in Canada \ No newline at end of file diff --git a/docs/for-smbs.md b/docs/for-smbs.md new file mode 100644 index 0000000..564322e --- /dev/null +++ b/docs/for-smbs.md @@ -0,0 +1,57 @@ +For Small Business Owners (rewritten) + +This is your golden age. +Look at how your business runs right now. +A booking tool over here. An invoicing tool over there. A separate CRM. A point-of-sale system that doesn't quite know about either of them. An accounting add-on. A scheduling app. A customer feedback tool. A loyalty platform. A marketing thing your last consultant set up that you can't remember the login for. +And underneath all of it — the spreadsheet. The one you actually trust. The one you've been using for years to keep track of what your "real" software can't. +Eight tools, none of them built for you, none of them talking to each other. You're the one holding it all together. +It was never supposed to work this way. + +What changed +For twenty years, the only choice small businesses had was to rent software built for somebody else. Each tool covered a slice of the business. None of them covered your business. You stitched them together because there was no other option. +There's another option now. +You can replace your entire stack — every tool that doesn't fit, every subscription that doesn't earn its keep, every spreadsheet you use to glue them together — with one tool, built for your business. Not configured for it. Not customized for it. Built for it. +Booking, scheduling, invoicing, customers, inventory, reporting — all in one place, designed around how you actually run things. Your terminology. Your workflow. Your rules. +This used to require an engineering team and six figures. Now it requires you, an idea, and a few afternoons. Or a local builder who can do it for you. +You own it. Forever. No subscription. No vendor lock-in. No price hikes next year. + +Start where the pain is +Pull up your bank statement and find the monthly subscription line. +For every tool you pay for, ask one question: +"Is this actually doing the job for my business today?" +The booking tool that almost works the way your shop runs. The CRM that doesn't quite handle your kind of customer. The invoicing software that fits a Shopify store but not your store. The inventory tool that's too complicated for what you actually need. The reporting dashboard that gives you everything except the number you actually want to see. +Each one of those is a tool you're renting that doesn't fit. +Now imagine all of them gone, replaced by one system that does fit — built around the way you actually work, owned by your business, no monthly rent. +That's what Vibn is for. + +What you can build +You don't need to be technical. You describe what your business does and what you need it to do. The AI builds it. It puts it online, sets up logins for your team and your customers, and helps you actually get people using it. +A few of the things small business owners are building: + +A single tool that runs the whole front-of-house — bookings, customer notes, scheduling, payments, follow-ups — replacing four or five subscriptions with one custom system +A custom shop management tool built around your trade, your jobs, your crew — replacing the generic field-service software that never quite worked +A complete client portal where customers book, pay, see their history, leave reviews, and refer friends — in one place, branded to your business, not a generic SaaS +An end-to-end studio system — classes, members, schedules, packages, attendance, marketing — purpose-built for your studio, not the average one +A unified back-office system that handles invoicing, expenses, payroll prep, and reporting in one place, the way your bookkeeper actually thinks about it +A small-format ERP — yes, really — for a business that's outgrown spreadsheets but never going to be big enough for SAP + +These are not integrations. These are not dashboards. These are the actual tools that run the business, built once, owned forever. + +You own it. Forever. +This is the part the SaaS industry doesn't want you to think about. +Every month you pay for software, you're renting. You're not building anything that's yours. When they raise the price, you pay. When they change the features, you adapt. When they get acquired or shut down, you lose your workflow and your data. +The tool you build with Vibn is yours. Your business owns it. Your data lives in it. You don't pay rent on it every month. It works the way you work, because you built it the way you work. +This is what software should have always been for small business. + +What if you don't want to build it yourself? +You don't have to. +A new kind of professional is emerging — local builders who specialize in building custom Vibn systems for the small businesses in their community. They speak your language. They understand small business. They hand you the keys when they're done. +You hire them once. You own the tool forever. No subscription. No vendor lock-in. +[ Find a builder in your area ] + +You are why this exists +Small business is the backbone of every neighborhood, every economy, every community worth being part of. You deserve software that fits your business — not a generic version of somebody else's. You deserve to own the tools that run your livelihood. +That's what Vibn is here for. +This is your golden age. Let's build it. +[ Start building free → ] +No credit card · Free to start · Built in Canada \ No newline at end of file diff --git a/docs/mission.md b/docs/mission.md new file mode 100644 index 0000000..539c06f --- /dev/null +++ b/docs/mission.md @@ -0,0 +1,43 @@ +Our Mission + +"Look at your subscription costs, ask if they're doing the job, if not, get building." + + +A letter from the founder +I've spent the last ten years trying to help small businesses grow. + +Startups. Clubs. Plumbers. Bookkeepers. Family restaurants. Single-location retail. The businesses that make neighborhoods what they are. I've watched they struggle and stagnate. + +Here's what I learned: the biggest thing holding small businesses back isn't the economy, or competition, or marketing. It's the owner. Their hesitations. Their (very reasonable) skepticism of change. Most small business owners are run off their feet — they don't have the time or appetite to adopt new tools, new systems, new anything. And when someone tries to sell them on change, they bristle. They've been burned too many times. + +But I've also learned something else. When an owner makes an the idea theirs, they adopt it instantly. + +That's what AI changes. For the first time, software can have a real conversation with a small business owner. It can listen to their problem in their words, propose a solution that feels obvious to them, and build it on the spot. The owner isn't being sold to. They're being heard. And when an idea is theirs, the resistance disappears. +That's the unlock. That's why this moment matters. + +Why small business never got the software it deserved +People love to blame SaaS for squeezing small business. I don't think that's quite right. +The real story is structural. For the last two decades, the math of venture capital pushed every promising software company toward enterprise. It wasn't a conspiracy — it was incentives. Small business has high churn. Enterprise has multi-year contracts. LPs expect returns on a fund timeline that small business revenue can't deliver. So founders with great ideas for small business software kept getting nudged upmarket — by their investors, their boards, their boards' investors — until eventually the SMB version of every product was an afterthought, and the real product was built for a 500-person finance team. +Small business got the leftovers. Monthly subscriptions for software that almost-but-not-quite solves the problem. +Nobody set out to underserve small business. The system just didn't reward serving them well. That gap — the gap between what small businesses actually needed and what got built — is the gap Vibn fills. + +Why now +There's a wave coming. AI is going to displace a lot of people — software engineers especially, but knowledge workers across the board. The doom narrative says this is the end of opportunity. I think it's the opposite. +Small business is where the next generation of careers gets built. +Not as a consolation prize. As an upgrade. Owning the bakery instead of writing code for a company that sells software to bakeries. Building custom tools for the plumber down the street instead of building dashboards for a Series C SaaS. Working at a thriving local business that owns its own software, instead of grinding through layoffs at companies that don't know what they want to be. +For laid-off engineers, this is a place to land — and not a small one. The same skills that built SaaS for the enterprise can build extraordinary things for small businesses now that the tools exist. For young entrepreneurs, this is the cheapest, fastest, most legitimate path to running a real business that has ever existed. For everyone who wants to help small businesses thrive, this is the moment to do it. + +What Vibn is, really +Vibn is a vibe coding platform. That's the surface. +Underneath, it's a wager: that if you make it possible for a small business to have custom software — built by the owner, or by a local freelancer who hands it over — without subscriptions, without endless tool sprawl, without code, without engineering teams — you start to fix something that's been broken for twenty years. +Owners build tools and own them outright. Freelancers build custom solutions for their community and get paid like the craftsmen they are. Subscriptions get cancelled. Margins go back to the businesses earning them. Software stops being something small businesses rent forever and starts being something they own. +That's the golden age. Not abstract. Concrete. One business at a time. One tool at a time. + +What you can do right now +If you own a small business: pull up your bank statement and look at the subscription line. Look at every tool you pay for every month. Ask one question — is this actually doing the job for my business today? If the answer is no, even partially, you should be building. +If you're an engineer who got laid off, or never got the job: there is real work here. Real businesses that need real tools. You don't need a startup, a co-founder, or a Series A. You need one local small business and a willingness to build them exactly what they need. +If you're an entrepreneur looking for a wave to ride: this is it. The tools are here. The customers are here. The moment is here. +We built Vibn for all of you. +Let's build the golden age. +— Mark Henderson +Founder, Vibn \ No newline at end of file diff --git a/env_data.json b/env_data.json new file mode 100644 index 0000000..8aba10c --- /dev/null +++ b/env_data.json @@ -0,0 +1 @@ +{"id":1,"uuid":"foskksoccksk0kc4g8sk88ok","name":"production","applications":[{"id":7,"repository_project_id":null,"uuid":"m84cc4wsc0ckws8g8k44kkk8","name":"vibn-api","fqdn":"https:\/\/api.vibnai.com","config_hash":"8369c478f4c36fff33de214c7712568b","git_repository":"https:\/\/git.vibnai.com\/mark\/vibn-api.git","git_branch":"main","git_commit_sha":"HEAD","git_full_url":null,"docker_registry_image_name":null,"docker_registry_image_tag":null,"build_pack":"dockerfile","static_image":"nginx:alpine","install_command":null,"build_command":null,"start_command":null,"ports_exposes":"3001","ports_mappings":null,"base_directory":"\/","publish_directory":null,"health_check_path":"\/","health_check_port":null,"health_check_host":"localhost","health_check_method":"GET","health_check_return_code":200,"health_check_scheme":"http","health_check_response_text":null,"health_check_interval":30,"health_check_timeout":3,"health_check_retries":3,"health_check_start_period":5,"limits_memory":"0","limits_memory_swap":"0","limits_memory_swappiness":60,"limits_memory_reservation":"0","limits_cpus":"0","limits_cpuset":null,"limits_cpu_shares":1024,"status":"running:healthy","preview_url_template":"{{pr_id}}.{{domain}}","destination_type":"App\\Models\\StandaloneDocker","destination_id":0,"source_type":null,"source_id":null,"private_key_id":null,"environment_id":1,"created_at":"2026-02-16T03:46:33.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","description":"VIBN API\/Proxy Server","dockerfile":null,"health_check_enabled":false,"dockerfile_location":"\/Dockerfile","custom_labels":"dHJhZWZpay5lbmFibGU9dHJ1ZQp0cmFlZmlrLmh0dHAubWlkZGxld2FyZXMuZ3ppcC5jb21wcmVzcz10cnVlCnRyYWVmaWsuaHR0cC5taWRkbGV3YXJlcy5yZWRpcmVjdC10by1odHRwcy5yZWRpcmVjdHNjaGVtZS5zY2hlbWU9aHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLW04NGNjNHdzYzBja3dzOGc4azQ0a2trOC5lbnRyeVBvaW50cz1odHRwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMC1tODRjYzR3c2MwY2t3czhnOGs0NGtrazgubWlkZGxld2FyZXM9cmVkaXJlY3QtdG8taHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLW04NGNjNHdzYzBja3dzOGc4azQ0a2trOC5ydWxlPUhvc3QoYGFwaS52aWJuYWkuY29tYCkgJiYgUGF0aFByZWZpeChgL2ApCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMC1tODRjYzR3c2MwY2t3czhnOGs0NGtrazguc2VydmljZT1odHRwLTAtbTg0Y2M0d3NjMGNrd3M4ZzhrNDRra2s4CnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAtbTg0Y2M0d3NjMGNrd3M4ZzhrNDRra2s4LmVudHJ5UG9pbnRzPWh0dHBzCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAtbTg0Y2M0d3NjMGNrd3M4ZzhrNDRra2s4Lm1pZGRsZXdhcmVzPWd6aXAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC1tODRjYzR3c2MwY2t3czhnOGs0NGtrazgucnVsZT1Ib3N0KGBhcGkudmlibmFpLmNvbWApICYmIFBhdGhQcmVmaXgoYC9gKQp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0wLW04NGNjNHdzYzBja3dzOGc4azQ0a2trOC5zZXJ2aWNlPWh0dHBzLTAtbTg0Y2M0d3NjMGNrd3M4ZzhrNDRra2s4CnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAtbTg0Y2M0d3NjMGNrd3M4ZzhrNDRra2s4LnRscy5jZXJ0cmVzb2x2ZXI9bGV0c2VuY3J5cHQKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC1tODRjYzR3c2MwY2t3czhnOGs0NGtrazgudGxzPXRydWUKdHJhZWZpay5odHRwLnNlcnZpY2VzLmh0dHAtMC1tODRjYzR3c2MwY2t3czhnOGs0NGtrazgubG9hZGJhbGFuY2VyLnNlcnZlci5wb3J0PTMwMDEKdHJhZWZpay5odHRwLnNlcnZpY2VzLmh0dHBzLTAtbTg0Y2M0d3NjMGNrd3M4ZzhrNDRra2s4LmxvYWRiYWxhbmNlci5zZXJ2ZXIucG9ydD0zMDAxCmNhZGR5XzAuZW5jb2RlPXpzdGQgZ3ppcApjYWRkeV8wLmhhbmRsZV9wYXRoLjBfcmV2ZXJzZV9wcm94eT17e3Vwc3RyZWFtcyAzMDAxfX0KY2FkZHlfMC5oYW5kbGVfcGF0aD0vKgpjYWRkeV8wLmhlYWRlcj0tU2VydmVyCmNhZGR5XzAudHJ5X2ZpbGVzPXtwYXRofSAvaW5kZXguaHRtbCAvaW5kZXgucGhwCmNhZGR5XzA9aHR0cHM6Ly9hcGkudmlibmFpLmNvbQpjYWRkeV9pbmdyZXNzX25ldHdvcms9Y29vbGlmeQ==","dockerfile_target_build":null,"manual_webhook_secret_github":null,"manual_webhook_secret_gitlab":null,"docker_compose_location":"\/docker-compose.yaml","docker_compose":null,"docker_compose_raw":null,"docker_compose_domains":null,"deleted_at":null,"docker_compose_custom_start_command":null,"docker_compose_custom_build_command":null,"swarm_replicas":1,"swarm_placement_constraints":null,"manual_webhook_secret_bitbucket":null,"custom_docker_run_options":null,"post_deployment_command":null,"post_deployment_command_container":null,"pre_deployment_command":null,"pre_deployment_command_container":null,"watch_paths":null,"custom_healthcheck_found":true,"manual_webhook_secret_gitea":null,"redirect":"both","compose_parsing_version":"5","last_online_at":"2026-05-15 20:42:32","custom_nginx_configuration":"","custom_network_aliases":null,"is_http_basic_auth_enabled":false,"http_basic_auth_username":null,"http_basic_auth_password":null,"restart_count":0,"last_restart_at":null,"last_restart_type":null,"health_check_type":"http","health_check_command":null,"additional_servers_count":0,"additional_networks_count":0,"server_status":true,"additional_servers":[],"destination":{"id":0,"name":"coolify","uuid":"zkogkggkw0wg40gccks80oo0","network":"coolify","server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:41:51.000000Z","server":{"id":0,"uuid":"jws4g4cgssss4cw48s488woc","name":"localhost","description":"This is the server where Coolify is running on. Don't delete this!","ip":"host.docker.internal","port":22,"user":"root","team_id":0,"private_key_id":0,"proxy":{"type":"TRAEFIK","status":"running","last_saved_settings":null,"last_applied_settings":null,"redirect_enabled":true,"force_stop":false},"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","unreachable_notification_sent":false,"unreachable_count":0,"high_disk_usage_notification_sent":false,"log_drain_notification_sent":false,"swarm_cluster":null,"validation_logs":null,"sentinel_updated_at":"2026-05-15 20:42:32","deleted_at":null,"ip_previous":null,"hetzner_server_id":null,"cloud_provider_token_id":null,"hetzner_server_status":null,"is_validating":false,"detected_traefik_version":"3.6.8","traefik_outdated_info":{"current":"3.6.8","latest":"3.6.11","type":"patch_update","checked_at":"2026-05-10T00:00:42+00:00"},"server_metadata":null,"is_coolify_host":true,"settings":{"id":1,"is_swarm_manager":false,"is_jump_server":false,"is_build_server":false,"is_reachable":true,"is_usable":true,"server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:49:13.000000Z","wildcard_domain":null,"is_cloudflare_tunnel":false,"is_logdrain_newrelic_enabled":false,"logdrain_newrelic_license_key":null,"logdrain_newrelic_base_uri":null,"is_logdrain_highlight_enabled":false,"logdrain_highlight_project_id":null,"is_logdrain_axiom_enabled":false,"logdrain_axiom_dataset_name":null,"logdrain_axiom_api_key":null,"is_swarm_worker":false,"is_logdrain_custom_enabled":false,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"concurrent_builds":2,"dynamic_timeout":3600,"force_disabled":false,"is_metrics_enabled":false,"generate_exact_labels":false,"force_docker_cleanup":true,"docker_cleanup_frequency":"0 *\/6 * * *","docker_cleanup_threshold":75,"server_timezone":"UTC","delete_unused_volumes":false,"delete_unused_networks":false,"is_sentinel_enabled":true,"sentinel_token":"eyJpdiI6Ii9MSGRUb1JDWDZUSGE2b2dPVU5ka2c9PSIsInZhbHVlIjoiRVVkYXBZYmtIenRtTVBqK01GRnhJNERrT3RwLzhYK3EwT2hIM2JtOS9TeGdZNEl2VWRHczBHYXgzQTJ4UnpJTkluVU1RRXF1SVVXREZBZVpyaU1TZEE9PSIsIm1hYyI6IjdjZjYxNmEzZTM2OThjYjA4MGVlYzBkOGZlOGNmOTU1ZTA5MjJmZTY5ZGQ5OTczZWRkYTRkZDQ0ZDMzY2VjNzciLCJ0YWciOiIifQ==","sentinel_metrics_refresh_rate_seconds":10,"sentinel_metrics_history_days":7,"sentinel_push_interval_seconds":60,"sentinel_custom_url":"http:\/\/host.docker.internal:8000","server_disk_usage_notification_threshold":80,"is_sentinel_debug_enabled":false,"server_disk_usage_check_frequency":"0 23 * * *","is_terminal_enabled":true,"deployment_queue_limit":25,"disable_application_image_retention":false}}}},{"id":11,"repository_project_id":null,"uuid":"jss08wssogw4kw8gok0sk0w0","name":"vibn-agent-runner","fqdn":"https:\/\/agents.vibnai.com","config_hash":"8728b5bd004205482b619c498254bb8d","git_repository":"https:\/\/git.vibnai.com\/mark\/vibn-agent-runner.git","git_branch":"main","git_commit_sha":"HEAD","git_full_url":null,"docker_registry_image_name":null,"docker_registry_image_tag":null,"build_pack":"dockerfile","static_image":"nginx:alpine","install_command":null,"build_command":null,"start_command":null,"ports_exposes":"3333","ports_mappings":null,"base_directory":"\/","publish_directory":null,"health_check_path":"\/","health_check_port":null,"health_check_host":"localhost","health_check_method":"GET","health_check_return_code":200,"health_check_scheme":"http","health_check_response_text":null,"health_check_interval":5,"health_check_timeout":5,"health_check_retries":10,"health_check_start_period":5,"limits_memory":"0","limits_memory_swap":"0","limits_memory_swappiness":60,"limits_memory_reservation":"0","limits_cpus":"0","limits_cpuset":null,"limits_cpu_shares":1024,"status":"running:unknown","preview_url_template":"{{pr_id}}.{{domain}}","destination_type":"App\\Models\\StandaloneDocker","destination_id":0,"source_type":null,"source_id":null,"private_key_id":null,"environment_id":1,"created_at":"2026-02-26T22:52:01.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","description":"","dockerfile":null,"health_check_enabled":false,"dockerfile_location":"\/.\/Dockerfile","custom_labels":"dHJhZWZpay5lbmFibGU9dHJ1ZQp0cmFlZmlrLmh0dHAubWlkZGxld2FyZXMuZ3ppcC5jb21wcmVzcz10cnVlCnRyYWVmaWsuaHR0cC5taWRkbGV3YXJlcy5yZWRpcmVjdC10by1odHRwcy5yZWRpcmVjdHNjaGVtZS5zY2hlbWU9aHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLWpzczA4d3Nzb2d3NGt3OGdvazBzazB3MC5lbnRyeVBvaW50cz1odHRwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMC1qc3MwOHdzc29ndzRrdzhnb2swc2swdzAubWlkZGxld2FyZXM9cmVkaXJlY3QtdG8taHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLWpzczA4d3Nzb2d3NGt3OGdvazBzazB3MC5ydWxlPUhvc3QoYGFnZW50cy52aWJuYWkuY29tYCkgJiYgUGF0aFByZWZpeChgL2ApCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMC1qc3MwOHdzc29ndzRrdzhnb2swc2swdzAuc2VydmljZT1odHRwLTAtanNzMDh3c3NvZ3c0a3c4Z29rMHNrMHcwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAtanNzMDh3c3NvZ3c0a3c4Z29rMHNrMHcwLmVudHJ5UG9pbnRzPWh0dHBzCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAtanNzMDh3c3NvZ3c0a3c4Z29rMHNrMHcwLm1pZGRsZXdhcmVzPWd6aXAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC1qc3MwOHdzc29ndzRrdzhnb2swc2swdzAucnVsZT1Ib3N0KGBhZ2VudHMudmlibmFpLmNvbWApICYmIFBhdGhQcmVmaXgoYC9gKQp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0wLWpzczA4d3Nzb2d3NGt3OGdvazBzazB3MC5zZXJ2aWNlPWh0dHBzLTAtanNzMDh3c3NvZ3c0a3c4Z29rMHNrMHcwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAtanNzMDh3c3NvZ3c0a3c4Z29rMHNrMHcwLnRscy5jZXJ0cmVzb2x2ZXI9bGV0c2VuY3J5cHQKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC1qc3MwOHdzc29ndzRrdzhnb2swc2swdzAudGxzPXRydWUKdHJhZWZpay5odHRwLnNlcnZpY2VzLmh0dHAtMC1qc3MwOHdzc29ndzRrdzhnb2swc2swdzAubG9hZGJhbGFuY2VyLnNlcnZlci5wb3J0PTMzMzMKdHJhZWZpay5odHRwLnNlcnZpY2VzLmh0dHBzLTAtanNzMDh3c3NvZ3c0a3c4Z29rMHNrMHcwLmxvYWRiYWxhbmNlci5zZXJ2ZXIucG9ydD0zMzMzCmNhZGR5XzAuZW5jb2RlPXpzdGQgZ3ppcApjYWRkeV8wLmhhbmRsZV9wYXRoLjBfcmV2ZXJzZV9wcm94eT17e3Vwc3RyZWFtcyAzMzMzfX0KY2FkZHlfMC5oYW5kbGVfcGF0aD0vKgpjYWRkeV8wLmhlYWRlcj0tU2VydmVyCmNhZGR5XzAudHJ5X2ZpbGVzPXtwYXRofSAvaW5kZXguaHRtbCAvaW5kZXgucGhwCmNhZGR5XzA9aHR0cHM6Ly9hZ2VudHMudmlibmFpLmNvbQpjYWRkeV9pbmdyZXNzX25ldHdvcms9Y29vbGlmeQ==","dockerfile_target_build":null,"manual_webhook_secret_github":null,"manual_webhook_secret_gitlab":null,"docker_compose_location":"\/docker-compose.yaml","docker_compose":null,"docker_compose_raw":null,"docker_compose_domains":null,"deleted_at":null,"docker_compose_custom_start_command":null,"docker_compose_custom_build_command":null,"swarm_replicas":1,"swarm_placement_constraints":null,"manual_webhook_secret_bitbucket":null,"custom_docker_run_options":"--volume \/var\/run\/docker.sock:\/var\/run\/docker.sock","post_deployment_command":null,"post_deployment_command_container":null,"pre_deployment_command":null,"pre_deployment_command_container":null,"watch_paths":null,"custom_healthcheck_found":false,"manual_webhook_secret_gitea":null,"redirect":"both","compose_parsing_version":"5","last_online_at":"2026-05-15 20:42:32","custom_nginx_configuration":"","custom_network_aliases":null,"is_http_basic_auth_enabled":false,"http_basic_auth_username":null,"http_basic_auth_password":null,"restart_count":0,"last_restart_at":null,"last_restart_type":null,"health_check_type":"http","health_check_command":null,"additional_servers_count":0,"additional_networks_count":0,"server_status":true,"additional_servers":[],"destination":{"id":0,"name":"coolify","uuid":"zkogkggkw0wg40gccks80oo0","network":"coolify","server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:41:51.000000Z","server":{"id":0,"uuid":"jws4g4cgssss4cw48s488woc","name":"localhost","description":"This is the server where Coolify is running on. Don't delete this!","ip":"host.docker.internal","port":22,"user":"root","team_id":0,"private_key_id":0,"proxy":{"type":"TRAEFIK","status":"running","last_saved_settings":null,"last_applied_settings":null,"redirect_enabled":true,"force_stop":false},"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","unreachable_notification_sent":false,"unreachable_count":0,"high_disk_usage_notification_sent":false,"log_drain_notification_sent":false,"swarm_cluster":null,"validation_logs":null,"sentinel_updated_at":"2026-05-15 20:42:32","deleted_at":null,"ip_previous":null,"hetzner_server_id":null,"cloud_provider_token_id":null,"hetzner_server_status":null,"is_validating":false,"detected_traefik_version":"3.6.8","traefik_outdated_info":{"current":"3.6.8","latest":"3.6.11","type":"patch_update","checked_at":"2026-05-10T00:00:42+00:00"},"server_metadata":null,"is_coolify_host":true,"settings":{"id":1,"is_swarm_manager":false,"is_jump_server":false,"is_build_server":false,"is_reachable":true,"is_usable":true,"server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:49:13.000000Z","wildcard_domain":null,"is_cloudflare_tunnel":false,"is_logdrain_newrelic_enabled":false,"logdrain_newrelic_license_key":null,"logdrain_newrelic_base_uri":null,"is_logdrain_highlight_enabled":false,"logdrain_highlight_project_id":null,"is_logdrain_axiom_enabled":false,"logdrain_axiom_dataset_name":null,"logdrain_axiom_api_key":null,"is_swarm_worker":false,"is_logdrain_custom_enabled":false,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"concurrent_builds":2,"dynamic_timeout":3600,"force_disabled":false,"is_metrics_enabled":false,"generate_exact_labels":false,"force_docker_cleanup":true,"docker_cleanup_frequency":"0 *\/6 * * *","docker_cleanup_threshold":75,"server_timezone":"UTC","delete_unused_volumes":false,"delete_unused_networks":false,"is_sentinel_enabled":true,"sentinel_token":"eyJpdiI6Ii9MSGRUb1JDWDZUSGE2b2dPVU5ka2c9PSIsInZhbHVlIjoiRVVkYXBZYmtIenRtTVBqK01GRnhJNERrT3RwLzhYK3EwT2hIM2JtOS9TeGdZNEl2VWRHczBHYXgzQTJ4UnpJTkluVU1RRXF1SVVXREZBZVpyaU1TZEE9PSIsIm1hYyI6IjdjZjYxNmEzZTM2OThjYjA4MGVlYzBkOGZlOGNmOTU1ZTA5MjJmZTY5ZGQ5OTczZWRkYTRkZDQ0ZDMzY2VjNzciLCJ0YWciOiIifQ==","sentinel_metrics_refresh_rate_seconds":10,"sentinel_metrics_history_days":7,"sentinel_push_interval_seconds":60,"sentinel_custom_url":"http:\/\/host.docker.internal:8000","server_disk_usage_notification_threshold":80,"is_sentinel_debug_enabled":false,"server_disk_usage_check_frequency":"0 23 * * *","is_terminal_enabled":true,"deployment_queue_limit":25,"disable_application_image_retention":false}}}},{"id":8,"repository_project_id":null,"uuid":"y4cscsc8s08c8808go0448s0","name":"vibn-frontend","fqdn":"https:\/\/vibnai.com,https:\/\/www.vibnai.com","config_hash":"6ed60185dad03810d4b9db6ce4fa76a2","git_repository":"https:\/\/git.vibnai.com\/mark\/vibn-frontend.git","git_branch":"main","git_commit_sha":"HEAD","git_full_url":null,"docker_registry_image_name":null,"docker_registry_image_tag":null,"build_pack":"dockerfile","static_image":"nginx:alpine","install_command":null,"build_command":null,"start_command":null,"ports_exposes":"3000","ports_mappings":null,"base_directory":"\/","publish_directory":null,"health_check_path":"\/","health_check_port":"3000","health_check_host":"127.0.0.1","health_check_method":"GET","health_check_return_code":200,"health_check_scheme":"http","health_check_response_text":null,"health_check_interval":5,"health_check_timeout":5,"health_check_retries":15,"health_check_start_period":90,"limits_memory":"0","limits_memory_swap":"0","limits_memory_swappiness":60,"limits_memory_reservation":"0","limits_cpus":"0","limits_cpuset":null,"limits_cpu_shares":1024,"status":"running:healthy","preview_url_template":"{{pr_id}}.{{domain}}","destination_type":"App\\Models\\StandaloneDocker","destination_id":0,"source_type":null,"source_id":null,"private_key_id":null,"environment_id":1,"created_at":"2026-02-16T22:29:56.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","description":"VIBN Frontend - Next.js Application","dockerfile":null,"health_check_enabled":true,"dockerfile_location":"\/Dockerfile","custom_labels":"dHJhZWZpay5lbmFibGU9dHJ1ZQp0cmFlZmlrLmh0dHAubWlkZGxld2FyZXMuZ3ppcC5jb21wcmVzcz10cnVlCnRyYWVmaWsuaHR0cC5taWRkbGV3YXJlcy5yZWRpcmVjdC10by1odHRwcy5yZWRpcmVjdHNjaGVtZS5zY2hlbWU9aHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5lbnRyeVBvaW50cz1odHRwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAubWlkZGxld2FyZXM9cmVkaXJlY3QtdG8taHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5ydWxlPUhvc3QoYHZpYm5haS5jb21gKSAmJiBQYXRoUHJlZml4KGAvYCkKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5zZXJ2aWNlPWh0dHAtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5lbnRyeVBvaW50cz1odHRwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAubWlkZGxld2FyZXM9cmVkaXJlY3QtdG8taHR0cHMKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cC0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5ydWxlPUhvc3QoYHd3dy52aWJuYWkuY29tYCkgJiYgUGF0aFByZWZpeChgL2ApCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHAtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAuc2VydmljZT1odHRwLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLmVudHJ5UG9pbnRzPWh0dHBzCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLm1pZGRsZXdhcmVzPWd6aXAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAucnVsZT1Ib3N0KGB2aWJuYWkuY29tYCkgJiYgUGF0aFByZWZpeChgL2ApCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLnNlcnZpY2U9aHR0cHMtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMC15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAudGxzLmNlcnRyZXNvbHZlcj1sZXRzZW5jcnlwdAp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC50bHM9dHJ1ZQp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5lbnRyeVBvaW50cz1odHRwcwp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5taWRkbGV3YXJlcz1nemlwCnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLnJ1bGU9SG9zdChgd3d3LnZpYm5haS5jb21gKSAmJiBQYXRoUHJlZml4KGAvYCkKdHJhZWZpay5odHRwLnJvdXRlcnMuaHR0cHMtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAuc2VydmljZT1odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMAp0cmFlZmlrLmh0dHAucm91dGVycy5odHRwcy0xLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC50bHMuY2VydHJlc29sdmVyPWxldHNlbmNyeXB0CnRyYWVmaWsuaHR0cC5yb3V0ZXJzLmh0dHBzLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLnRscz10cnVlCnRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5odHRwLTAteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLmxvYWRiYWxhbmNlci5zZXJ2ZXIucG9ydD0zMDAwCnRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5odHRwLTEteTRjc2NzYzhzMDhjODgwOGdvMDQ0OHMwLmxvYWRiYWxhbmNlci5zZXJ2ZXIucG9ydD0zMDAwCnRyYWVmaWsuaHR0cC5zZXJ2aWNlcy5odHRwcy0wLXk0Y3Njc2M4czA4Yzg4MDhnbzA0NDhzMC5sb2FkYmFsYW5jZXIuc2VydmVyLnBvcnQ9MzAwMAp0cmFlZmlrLmh0dHAuc2VydmljZXMuaHR0cHMtMS15NGNzY3NjOHMwOGM4ODA4Z28wNDQ4czAubG9hZGJhbGFuY2VyLnNlcnZlci5wb3J0PTMwMDAKY2FkZHlfMC5lbmNvZGU9enN0ZCBnemlwCmNhZGR5XzAuaGFuZGxlX3BhdGguMF9yZXZlcnNlX3Byb3h5PXt7dXBzdHJlYW1zIDMwMDB9fQpjYWRkeV8wLmhhbmRsZV9wYXRoPS8qCmNhZGR5XzAuaGVhZGVyPS1TZXJ2ZXIKY2FkZHlfMC50cnlfZmlsZXM9e3BhdGh9IC9pbmRleC5odG1sIC9pbmRleC5waHAKY2FkZHlfMD1odHRwczovL3ZpYm5haS5jb20KY2FkZHlfMS5lbmNvZGU9enN0ZCBnemlwCmNhZGR5XzEuaGFuZGxlX3BhdGguMV9yZXZlcnNlX3Byb3h5PXt7dXBzdHJlYW1zIDMwMDB9fQpjYWRkeV8xLmhhbmRsZV9wYXRoPS8qCmNhZGR5XzEuaGVhZGVyPS1TZXJ2ZXIKY2FkZHlfMS50cnlfZmlsZXM9e3BhdGh9IC9pbmRleC5odG1sIC9pbmRleC5waHAKY2FkZHlfMT1odHRwczovL3d3dy52aWJuYWkuY29tCmNhZGR5X2luZ3Jlc3NfbmV0d29yaz1jb29saWZ5","dockerfile_target_build":null,"manual_webhook_secret_github":null,"manual_webhook_secret_gitlab":null,"docker_compose_location":"\/docker-compose.yaml","docker_compose":null,"docker_compose_raw":null,"docker_compose_domains":null,"deleted_at":null,"docker_compose_custom_start_command":null,"docker_compose_custom_build_command":null,"swarm_replicas":1,"swarm_placement_constraints":null,"manual_webhook_secret_bitbucket":null,"custom_docker_run_options":null,"post_deployment_command":null,"post_deployment_command_container":null,"pre_deployment_command":null,"pre_deployment_command_container":null,"watch_paths":null,"custom_healthcheck_found":false,"manual_webhook_secret_gitea":"c23f93114f0378ff15cbd863e3fbd23bb5c127ceace1756b","redirect":"both","compose_parsing_version":"5","last_online_at":"2026-05-15 20:42:32","custom_nginx_configuration":"","custom_network_aliases":null,"is_http_basic_auth_enabled":false,"http_basic_auth_username":null,"http_basic_auth_password":null,"restart_count":0,"last_restart_at":null,"last_restart_type":null,"health_check_type":"http","health_check_command":null,"additional_servers_count":0,"additional_networks_count":0,"server_status":true,"additional_servers":[],"destination":{"id":0,"name":"coolify","uuid":"zkogkggkw0wg40gccks80oo0","network":"coolify","server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:41:51.000000Z","server":{"id":0,"uuid":"jws4g4cgssss4cw48s488woc","name":"localhost","description":"This is the server where Coolify is running on. Don't delete this!","ip":"host.docker.internal","port":22,"user":"root","team_id":0,"private_key_id":0,"proxy":{"type":"TRAEFIK","status":"running","last_saved_settings":null,"last_applied_settings":null,"redirect_enabled":true,"force_stop":false},"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","unreachable_notification_sent":false,"unreachable_count":0,"high_disk_usage_notification_sent":false,"log_drain_notification_sent":false,"swarm_cluster":null,"validation_logs":null,"sentinel_updated_at":"2026-05-15 20:42:32","deleted_at":null,"ip_previous":null,"hetzner_server_id":null,"cloud_provider_token_id":null,"hetzner_server_status":null,"is_validating":false,"detected_traefik_version":"3.6.8","traefik_outdated_info":{"current":"3.6.8","latest":"3.6.11","type":"patch_update","checked_at":"2026-05-10T00:00:42+00:00"},"server_metadata":null,"is_coolify_host":true,"settings":{"id":1,"is_swarm_manager":false,"is_jump_server":false,"is_build_server":false,"is_reachable":true,"is_usable":true,"server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:49:13.000000Z","wildcard_domain":null,"is_cloudflare_tunnel":false,"is_logdrain_newrelic_enabled":false,"logdrain_newrelic_license_key":null,"logdrain_newrelic_base_uri":null,"is_logdrain_highlight_enabled":false,"logdrain_highlight_project_id":null,"is_logdrain_axiom_enabled":false,"logdrain_axiom_dataset_name":null,"logdrain_axiom_api_key":null,"is_swarm_worker":false,"is_logdrain_custom_enabled":false,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"concurrent_builds":2,"dynamic_timeout":3600,"force_disabled":false,"is_metrics_enabled":false,"generate_exact_labels":false,"force_docker_cleanup":true,"docker_cleanup_frequency":"0 *\/6 * * *","docker_cleanup_threshold":75,"server_timezone":"UTC","delete_unused_volumes":false,"delete_unused_networks":false,"is_sentinel_enabled":true,"sentinel_token":"eyJpdiI6Ii9MSGRUb1JDWDZUSGE2b2dPVU5ka2c9PSIsInZhbHVlIjoiRVVkYXBZYmtIenRtTVBqK01GRnhJNERrT3RwLzhYK3EwT2hIM2JtOS9TeGdZNEl2VWRHczBHYXgzQTJ4UnpJTkluVU1RRXF1SVVXREZBZVpyaU1TZEE9PSIsIm1hYyI6IjdjZjYxNmEzZTM2OThjYjA4MGVlYzBkOGZlOGNmOTU1ZTA5MjJmZTY5ZGQ5OTczZWRkYTRkZDQ0ZDMzY2VjNzciLCJ0YWciOiIifQ==","sentinel_metrics_refresh_rate_seconds":10,"sentinel_metrics_history_days":7,"sentinel_push_interval_seconds":60,"sentinel_custom_url":"http:\/\/host.docker.internal:8000","server_disk_usage_notification_threshold":80,"is_sentinel_debug_enabled":false,"server_disk_usage_check_frequency":"0 23 * * *","is_terminal_enabled":true,"deployment_queue_limit":25,"disable_application_image_retention":false}}}}],"description":null,"mariadbs":[],"mongodbs":[],"mysqls":[],"postgresqls":[{"id":2,"uuid":"kggs4ogckc0w8ggwkkk88kck","name":"vibn-postgres","description":"VIBN unified database - replaces Firebase + Railway","postgres_user":"vibn_user","postgres_password":"JSMcySGlEj3pSMjHvwEHAd1A","postgres_db":"vibn","postgres_initdb_args":"--encoding=UTF8 --locale=C","postgres_host_auth_method":"scram-sha-256","init_scripts":null,"status":"running:healthy","image":"postgres:16-alpine","is_public":true,"public_port":5433,"ports_mappings":null,"limits_memory":"0","limits_memory_swap":"0","limits_memory_swappiness":60,"limits_memory_reservation":"0","limits_cpus":"0","limits_cpuset":null,"limits_cpu_shares":1024,"started_at":"2026-02-18 00:58:28","destination_type":"App\\Models\\StandaloneDocker","destination_id":0,"environment_id":1,"created_at":"2026-02-16T03:41:27.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","postgres_conf":null,"is_log_drain_enabled":false,"is_include_timestamps":false,"deleted_at":null,"config_hash":"651bd3ba4d41c9880769c26675e79f87","custom_docker_run_options":null,"last_online_at":"2026-05-15 20:42:32","enable_ssl":false,"ssl_mode":"require","restart_count":0,"last_restart_at":null,"last_restart_type":null,"public_port_timeout":3600,"internal_db_url":"postgres:\/\/vibn_user:JSMcySGlEj3pSMjHvwEHAd1A@kggs4ogckc0w8ggwkkk88kck:5432\/vibn","external_db_url":"postgres:\/\/vibn_user:JSMcySGlEj3pSMjHvwEHAd1A@34.19.250.135:5433\/vibn","database_type":"standalone-postgresql","server_status":true,"destination":{"id":0,"name":"coolify","uuid":"zkogkggkw0wg40gccks80oo0","network":"coolify","server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:41:51.000000Z","server":{"id":0,"uuid":"jws4g4cgssss4cw48s488woc","name":"localhost","description":"This is the server where Coolify is running on. Don't delete this!","ip":"host.docker.internal","port":22,"user":"root","team_id":0,"private_key_id":0,"proxy":{"type":"TRAEFIK","status":"running","last_saved_settings":null,"last_applied_settings":null,"redirect_enabled":true,"force_stop":false},"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-05-15T20:42:32.000000Z","unreachable_notification_sent":false,"unreachable_count":0,"high_disk_usage_notification_sent":false,"log_drain_notification_sent":false,"swarm_cluster":null,"validation_logs":null,"sentinel_updated_at":"2026-05-15 20:42:32","deleted_at":null,"ip_previous":null,"hetzner_server_id":null,"cloud_provider_token_id":null,"hetzner_server_status":null,"is_validating":false,"detected_traefik_version":"3.6.8","traefik_outdated_info":{"current":"3.6.8","latest":"3.6.11","type":"patch_update","checked_at":"2026-05-10T00:00:42+00:00"},"server_metadata":null,"is_coolify_host":true,"settings":{"id":1,"is_swarm_manager":false,"is_jump_server":false,"is_build_server":false,"is_reachable":true,"is_usable":true,"server_id":0,"created_at":"2026-02-14T21:41:51.000000Z","updated_at":"2026-02-14T21:49:13.000000Z","wildcard_domain":null,"is_cloudflare_tunnel":false,"is_logdrain_newrelic_enabled":false,"logdrain_newrelic_license_key":null,"logdrain_newrelic_base_uri":null,"is_logdrain_highlight_enabled":false,"logdrain_highlight_project_id":null,"is_logdrain_axiom_enabled":false,"logdrain_axiom_dataset_name":null,"logdrain_axiom_api_key":null,"is_swarm_worker":false,"is_logdrain_custom_enabled":false,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"concurrent_builds":2,"dynamic_timeout":3600,"force_disabled":false,"is_metrics_enabled":false,"generate_exact_labels":false,"force_docker_cleanup":true,"docker_cleanup_frequency":"0 *\/6 * * *","docker_cleanup_threshold":75,"server_timezone":"UTC","delete_unused_volumes":false,"delete_unused_networks":false,"is_sentinel_enabled":true,"sentinel_token":"eyJpdiI6Ii9MSGRUb1JDWDZUSGE2b2dPVU5ka2c9PSIsInZhbHVlIjoiRVVkYXBZYmtIenRtTVBqK01GRnhJNERrT3RwLzhYK3EwT2hIM2JtOS9TeGdZNEl2VWRHczBHYXgzQTJ4UnpJTkluVU1RRXF1SVVXREZBZVpyaU1TZEE9PSIsIm1hYyI6IjdjZjYxNmEzZTM2OThjYjA4MGVlYzBkOGZlOGNmOTU1ZTA5MjJmZTY5ZGQ5OTczZWRkYTRkZDQ0ZDMzY2VjNzciLCJ0YWciOiIifQ==","sentinel_metrics_refresh_rate_seconds":10,"sentinel_metrics_history_days":7,"sentinel_push_interval_seconds":60,"sentinel_custom_url":"http:\/\/host.docker.internal:8000","server_disk_usage_notification_threshold":80,"is_sentinel_debug_enabled":false,"server_disk_usage_check_frequency":"0 23 * * *","is_terminal_enabled":true,"deployment_queue_limit":25,"disable_application_image_retention":false}}}}],"project_id":1,"redis":[],"services":[{"id":2,"uuid":"o4wwck0g0c04wgoo4g4s0004","name":"gitea","environment_id":1,"created_at":"2026-02-27T19:03:58.000000Z","updated_at":"2026-03-07T21:52:00.000000Z","server_id":1,"description":"","docker_compose_raw":"services:\n gitea:\n image: 'gitea\/gitea:latest'\n environment:\n - SERVICE_FQDN_GITEA_3000=git.vibnai.com\n - USER_UID=1000\n - USER_GID=1000\n - 'GITEA__server__ROOT_URL=https:\/\/git.vibnai.com\/'\n - GITEA__server__DOMAIN=git.vibnai.com\n - GITEA__server__SSH_DOMAIN=git.vibnai.com\n - GITEA__security__INSTALL_LOCK=true\n ports:\n - '22222:22'\n volumes:\n - 'gitea-data:\/data'\n - 'gitea-timezone:\/etc\/timezone:ro'\n - 'gitea-localtime:\/etc\/localtime:ro'\n healthcheck:\n test:\n - CMD\n - curl\n - '-f'\n - 'http:\/\/127.0.0.1:3000'\n interval: 2s\n timeout: 10s\n retries: 15\n","docker_compose":"services:\n gitea:\n image: 'gitea\/gitea:latest'\n environment:\n USER_UID: '1000'\n USER_GID: '1000'\n GITEA__server__ROOT_URL: 'https:\/\/git.vibnai.com\/'\n GITEA__server__DOMAIN: git.vibnai.com\n GITEA__server__SSH_DOMAIN: git.vibnai.com\n GITEA__security__INSTALL_LOCK: 'true'\n COOLIFY_RESOURCE_UUID: o4wwck0g0c04wgoo4g4s0004\n COOLIFY_CONTAINER_NAME: gitea-o4wwck0g0c04wgoo4g4s0004\n COOLIFY_FQDN: git.vibnai.com\n COOLIFY_URL: 'https:\/\/git.vibnai.com'\n SERVICE_NAME_GITEA: gitea\n ports:\n - '22222:22'\n volumes:\n - 'o4wwck0g0c04wgoo4g4s0004_gitea-data:\/data'\n - 'o4wwck0g0c04wgoo4g4s0004_gitea-timezone:\/etc\/timezone:ro'\n - 'o4wwck0g0c04wgoo4g4s0004_gitea-localtime:\/etc\/localtime:ro'\n healthcheck:\n test:\n - CMD\n - curl\n - '-f'\n - 'http:\/\/127.0.0.1:3000'\n interval: 2s\n timeout: 10s\n retries: 15\n container_name: gitea-o4wwck0g0c04wgoo4g4s0004\n restart: unless-stopped\n labels:\n - coolify.managed=true\n - coolify.version=4.0.0-beta.463\n - coolify.serviceId=2\n - coolify.type=service\n - coolify.name=gitea-o4wwck0g0c04wgoo4g4s0004\n - coolify.resourceName=gitea\n - coolify.projectName=vibn\n - coolify.serviceName=gitea\n - coolify.environmentName=production\n - coolify.pullRequestId=0\n - coolify.service.subId=2\n - coolify.service.subType=application\n - coolify.service.subName=gitea\n - traefik.enable=true\n - traefik.http.middlewares.gzip.compress=true\n - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https\n - traefik.http.routers.http-0-o4wwck0g0c04wgoo4g4s0004-gitea.entryPoints=http\n - traefik.http.routers.http-0-o4wwck0g0c04wgoo4g4s0004-gitea.middlewares=redirect-to-https\n - 'traefik.http.routers.http-0-o4wwck0g0c04wgoo4g4s0004-gitea.rule=Host(`git.vibnai.com`) && PathPrefix(`\/`)'\n - traefik.http.routers.https-0-o4wwck0g0c04wgoo4g4s0004-gitea.entryPoints=https\n - traefik.http.routers.https-0-o4wwck0g0c04wgoo4g4s0004-gitea.middlewares=gzip\n - 'traefik.http.routers.https-0-o4wwck0g0c04wgoo4g4s0004-gitea.rule=Host(`git.vibnai.com`) && PathPrefix(`\/`)'\n - traefik.http.routers.https-0-o4wwck0g0c04wgoo4g4s0004-gitea.tls.certresolver=letsencrypt\n - traefik.http.routers.https-0-o4wwck0g0c04wgoo4g4s0004-gitea.tls=true\n - 'caddy_0.encode=zstd gzip'\n - 'caddy_0.handle_path.0_reverse_proxy={{upstreams}}'\n - 'caddy_0.handle_path=\/*'\n - caddy_0.header=-Server\n - 'caddy_0.try_files={path} \/index.html \/index.php'\n - 'caddy_0=https:\/\/git.vibnai.com'\n - caddy_ingress_network=o4wwck0g0c04wgoo4g4s0004\n networks:\n o4wwck0g0c04wgoo4g4s0004: null\n env_file:\n - .env\nvolumes:\n o4wwck0g0c04wgoo4g4s0004_gitea-data:\n name: o4wwck0g0c04wgoo4g4s0004_gitea-data\n o4wwck0g0c04wgoo4g4s0004_gitea-timezone:\n name: o4wwck0g0c04wgoo4g4s0004_gitea-timezone\n o4wwck0g0c04wgoo4g4s0004_gitea-localtime:\n name: o4wwck0g0c04wgoo4g4s0004_gitea-localtime\nnetworks:\n o4wwck0g0c04wgoo4g4s0004:\n name: o4wwck0g0c04wgoo4g4s0004\n external: true\n","destination_type":"App\\Models\\StandaloneDocker","destination_id":1,"deleted_at":null,"connect_to_docker_network":false,"config_hash":"81f7a40a55892337fca90c5b1a01171a","service_type":"gitea","is_container_label_escape_enabled":true,"compose_parsing_version":"5","server_status":true,"status":"running:healthy","server":{"id":1,"uuid":"zg4cwgc44ogc08804000gggo","name":"coolify-server-mtl","description":"","ip":"34.19.250.135","port":22,"user":"root","team_id":0,"private_key_id":0,"proxy":{"type":"TRAEFIK","status":"running","redirect_enabled":true},"created_at":"2026-02-15T01:48:10.000000Z","updated_at":"2026-05-10T00:00:40.000000Z","unreachable_notification_sent":false,"unreachable_count":0,"high_disk_usage_notification_sent":false,"log_drain_notification_sent":false,"swarm_cluster":null,"validation_logs":null,"sentinel_updated_at":"2026-02-14 21:41:49","deleted_at":null,"ip_previous":"127.0.0.1","hetzner_server_id":null,"cloud_provider_token_id":null,"hetzner_server_status":null,"is_validating":false,"detected_traefik_version":"3.6.8","traefik_outdated_info":{"current":"3.6.8","latest":"3.6.11","type":"patch_update","checked_at":"2026-05-10T00:00:40+00:00"},"server_metadata":null,"is_coolify_host":false,"settings":{"id":2,"is_swarm_manager":false,"is_jump_server":false,"is_build_server":false,"is_reachable":true,"is_usable":true,"server_id":1,"created_at":"2026-02-15T01:48:10.000000Z","updated_at":"2026-02-28T00:11:02.000000Z","wildcard_domain":null,"is_cloudflare_tunnel":false,"is_logdrain_newrelic_enabled":false,"logdrain_newrelic_license_key":null,"logdrain_newrelic_base_uri":null,"is_logdrain_highlight_enabled":false,"logdrain_highlight_project_id":null,"is_logdrain_axiom_enabled":false,"logdrain_axiom_dataset_name":null,"logdrain_axiom_api_key":null,"is_swarm_worker":false,"is_logdrain_custom_enabled":false,"logdrain_custom_config":null,"logdrain_custom_config_parser":null,"concurrent_builds":2,"dynamic_timeout":3600,"force_disabled":false,"is_metrics_enabled":false,"generate_exact_labels":false,"force_docker_cleanup":true,"docker_cleanup_frequency":"0 *\/6 * * *","docker_cleanup_threshold":75,"server_timezone":"UTC","delete_unused_volumes":false,"delete_unused_networks":false,"is_sentinel_enabled":true,"sentinel_token":"eyJpdiI6InM0cFNzelpxWkFCY0JSL0RyeXhqTVE9PSIsInZhbHVlIjoiaFI1NWhwTE1JNEtQQ1V3L0dsVWJwUzlFUUlQakdjbG5xekNFRDQ1S0c0TUxWSWtVelE4amFXa1BhMGxBaXIvdUx0SjlMZnlWS2liNklzR3ViaHdYRkE9PSIsIm1hYyI6ImExYTRlZGMxMWJkZWQyZjk0OWJjZDA0OTdiNmNjNzk0YTYzZDljZjBhNTQ5ZDgxMzEzNWExNjliNmU3ZTE1NWEiLCJ0YWciOiIifQ==","sentinel_metrics_refresh_rate_seconds":10,"sentinel_metrics_history_days":7,"sentinel_push_interval_seconds":60,"sentinel_custom_url":"http:\/\/34.19.250.135:8000","server_disk_usage_notification_threshold":80,"is_sentinel_debug_enabled":false,"server_disk_usage_check_frequency":"0 23 * * *","is_terminal_enabled":true,"deployment_queue_limit":25,"disable_application_image_retention":false}},"applications":[{"id":2,"uuid":"g440wws48sow8ggwo8kw08c0","name":"gitea","human_name":null,"description":null,"fqdn":"https:\/\/git.vibnai.com","ports":"22222:22","exposes":null,"status":"running:healthy","service_id":2,"created_at":"2026-02-27T19:03:58.000000Z","updated_at":"2026-05-15T20:42:06.000000Z","exclude_from_status":false,"required_fqdn":false,"image":"gitea\/gitea:latest","is_log_drain_enabled":false,"is_include_timestamps":false,"deleted_at":null,"is_gzip_enabled":true,"is_stripprefix_enabled":true,"last_online_at":"2026-05-15 20:42:06","is_migrated":false}],"databases":[]}],"created_at":"2026-02-14T21:49:21.000000Z","updated_at":"2026-02-14T21:49:21.000000Z"} \ No newline at end of file diff --git a/grep_out.txt b/grep_out.txt new file mode 100644 index 0000000..84a9f99 --- /dev/null +++ b/grep_out.txt @@ -0,0 +1,28 @@ +vibn-frontend/.next/server/chunks/lib_68058895._.js.map:6: {"offset": {"line": 1, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/lib/design-kits/registry.ts","turbopack:///[project]/lib/design-kits/types.ts","turbopack:///[project]/lib/integrations/sentry.ts","turbopack:///[project]/lib/ai/gemini-chat.ts","turbopack:///[project]/lib/ai/openai-compatible-chat.ts","turbopack:///[project]/lib/ai/vibn-chat-model.ts","turbopack:///[project]/lib/design-kits/for-ai.ts","turbopack:///[project]/lib/design-kits/resolve.ts"],"sourcesContent":["import type { StarterKitDefinition } from \"./types\";\n\n/**\n * Visual presets + UI foundation hints for codegen.\n *\n * Popular stacks (shadcn/ui, MUI, HeroUI, Untitled UI) are **different\n * products** — distinct installs, APIs, and licensing. Presets that share\n * `uiFoundation: \"agnostic\"` are token themes only; the agent should map\n * tokens onto whatever framework already exists unless greenfield.\n *\n * Mood presets at the bottom mirror legacy DESIGN_FEELS in\n * `preview-assist-ui/src/App.jsx`.\n */\nexport const STARTER_KITS: StarterKitDefinition[] = [\n {\n id: \"shadcn-neutral\",\n name: \"shadcn / Neutral SaaS\",\n tagline: \"Slate neutrals · minimal chrome · Tailwind-friendly\",\n defaults: {\n accentHex: \"#6366f1\",\n radiusMdPx: 8,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\"],\n uiFoundation: \"shadcn-ui\",\n foundationNotesForAi:\n \"Prefer shadcn/ui patterns when adding UI (Tailwind + Radix primitives; copy components into the repo). Map resolved accent/radius to CSS variables and Tailwind theme as shadcn expects.\",\n },\n {\n id: \"mui-material\",\n name: \"MUI (Material)\",\n tagline:\n \"Enterprise components · Material Design · dashboards & CRM shells\",\n defaults: {\n accentHex: \"#1976d2\",\n radiusMdPx: 4,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"mui-material\",\n foundationNotesForAi:\n \"Prefer MUI (@mui/material) when scaffolding new screens unless the repo already uses something else. Map accent and corners via createTheme (palette.primary, shape.borderRadius); avoid mixing MUI with a second heavy kit without user consent.\",\n },\n {\n id: \"heroui-next\",\n name: \"HeroUI\",\n tagline: \"Next.js-optimized · React Aria · Tailwind primitives\",\n defaults: {\n accentHex: \"#006fee\",\n radiusMdPx: 12,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"heroui\",\n foundationNotesForAi:\n \"Prefer HeroUI (@heroui/react) on Next.js greenfield; Tailwind-based with React Aria. Align semantic colors with tokens below via Tailwind/HeroUI theme config.\",\n },\n {\n id: \"untitled-ui\",\n name: \"Untitled UI\",\n tagline: \"Professional SaaS density · React Aria · Tailwind\",\n defaults: {\n accentHex: \"#444ce7\",\n radiusMdPx: 8,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"untitled-ui\",\n foundationNotesForAi:\n \"Untitled UI is a commercial kit — confirm the user has a license before importing paid sections from Figma handoff. When cleared, follow Untitled UI + React Aria + Tailwind patterns; map tokens to their spacing/color scales.\",\n },\n {\n id: \"twenty-crm\",\n name: \"Twenty CRM UI Kit\",\n tagline: \"CRM density · indigo accent · Radix-aligned semantics\",\n defaults: {\n accentHex: \"#5f6bf5\",\n radiusMdPx: 8,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Twenty-inspired tokens only — do not assume or vendor Twenty OSS unless the user asks. If greenfield with Tailwind, shadcn/ui-class primitives fit well; if repo uses MUI/HeroUI, map tokens there instead.\",\n },\n {\n id: \"flyonui\",\n name: \"FlyonUI\",\n tagline: \"Tailwind + daisyUI semantics + Preline JS\",\n defaults: {\n accentHex: \"#4f46e5\",\n radiusMdPx: 8,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Prefer FlyonUI semantics (daisyUI class names + Preline JS for interactivity) when scaffolding new screens. 800+ copy-paste components available. Use semantic classes like 'btn btn-primary' instead of raw utility classes where possible.\",\n },\n {\n id: \"warm-minimal\",\n name: \"Warm minimal\",\n tagline: \"Soft stone grays · rounded surfaces · editorial spacing\",\n defaults: {\n accentHex: \"#c2410c\",\n radiusMdPx: 10,\n fontPreset: \"system\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Warm SaaS mood — tokens only. Respect whichever UI stack exists; prefer centralized CSS/Tailwind variables for neutrals and accent.\",\n },\n {\n id: \"bold-confident\",\n name: \"Bold & confident\",\n tagline: \"High contrast · vivid accent — Stripe / Vercel energy\",\n defaults: {\n accentHex: \"#f43f5e\",\n radiusMdPx: 8,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Bold marketing/product mood — tokens only. Map accent into existing theme; avoid bolting on a new framework if one is already present.\",\n },\n {\n id: \"fresh-modern\",\n name: \"Fresh & modern\",\n tagline: \"Crisp green accent · friendly surfaces\",\n defaults: {\n accentHex: \"#22c55e\",\n radiusMdPx: 10,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Fresh palette mood — tokens only. Align greens with brand accent variables without prescribing a specific component vendor.\",\n },\n {\n id: \"electric-vivid\",\n name: \"Electric & vivid\",\n tagline: \"Purple creative tooling — Figma / Framer mood\",\n defaults: {\n accentHex: \"#8b5cf6\",\n radiusMdPx: 8,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Creative-tool mood — tokens only. Prefer mapping into existing DS before introducing another library.\",\n },\n {\n id: \"luxury-refined\",\n name: \"Premium & refined\",\n tagline: \"Gold accent · tight radius · editorial polish\",\n defaults: {\n accentHex: \"#d4a853\",\n radiusMdPx: 6,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Premium editorial mood — tokens only. Dark chrome often pairs with subtle borders; respect existing stack.\",\n },\n {\n id: \"anthropic-claude\",\n name: \"Anthropic / Claude\",\n tagline: \"Warm terracotta accent · clean editorial layout · serif headers\",\n defaults: {\n accentHex: \"#d97757\",\n radiusMdPx: 8,\n fontPreset: \"system\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Claude Design System — Editorial, warm terracotta accent (#d97757), off-white backgrounds (#f9f8f6). Use serif fonts (like Recoleta or a strong serif fallback) for primary headers and clean sans-serif for body. Buttons are rounded, surfaces are separated by subtle hairlines instead of heavy shadows. Read github.com/VoltAgent/awesome-claude-design/blob/main/design-systems/claude/DESIGN.md for specific token semantics.\",\n },\n {\n id: \"stripe-fintech\",\n name: \"Stripe / Fintech\",\n tagline: \"Signature purple gradients · weight-300 elegance · crisp utility\",\n defaults: {\n accentHex: \"#635bff\",\n radiusMdPx: 6,\n fontPreset: \"inter\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Stripe Design System — Crisp precision. Signature blurple accent (#635bff). Use incredibly soft, large shadows (0 50px 100px -20px rgba(50,50,93,0.25)) and crisp borders. Typography is tightly tracked, favoring font-weight 400 and 500. Gradients should be used sparingly but impactfully on active states. See github.com/VoltAgent/awesome-claude-design/blob/main/design-systems/stripe/DESIGN.md.\",\n },\n {\n id: \"vercel-dev\",\n name: \"Vercel / Developer\",\n tagline: \"Black and white precision · Geist font · maximum contrast\",\n defaults: {\n accentHex: \"#000000\",\n radiusMdPx: 6,\n fontPreset: \"system\",\n density: \"compact\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Vercel Design System — Monochrome, high-contrast, developer-oriented. Use Geist or Geist Mono. The primary accent is solid black (#000) on white, or white on black in dark mode. Extremely sparse use of color, only for status (success, error, warning). Layouts are boxy, defined by 1px solid #eaeaea borders. See github.com/VoltAgent/awesome-claude-design/blob/main/design-systems/vercel/DESIGN.md.\",\n },\n {\n id: \"linear-app\",\n name: \"Linear / Productivity\",\n tagline: \"Ultra-minimal · precise dark UI · purple glow\",\n defaults: {\n accentHex: \"#5e6ad2\",\n radiusMdPx: 8,\n fontPreset: \"system\",\n density: \"compact\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\", \"density\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Linear Design System — Dark mode first. Very subtle, single-pixel inner borders (box-shadow inset) on surfaces to give them depth. Primary accent is a muted indigo/purple. Backgrounds are deep gray (#111214) rather than pure black. Typography is highly structured, slightly smaller (13px/14px base) for density. See github.com/VoltAgent/awesome-claude-design/blob/main/design-systems/linear.app/DESIGN.md.\",\n },\n {\n id: \"airbnb-marketplace\",\n name: \"Airbnb / Marketplace\",\n tagline: \"Warm coral accent · photography-driven · large rounded UI\",\n defaults: {\n accentHex: \"#ff385c\",\n radiusMdPx: 12,\n fontPreset: \"system\",\n density: \"comfortable\",\n },\n customizeFields: [\"accent\", \"radius\", \"font\"],\n uiFoundation: \"agnostic\",\n foundationNotesForAi:\n \"Airbnb Design System — Friendly, consumer-focused. Large touch targets, heavy border-radius (12px+), and a stark contrast between pure white backgrounds and the vibrant 'Rausch' coral accent (#ff385c). Minimal borders, using white space and soft, dispersed shadows for elevation. See github.com/VoltAgent/awesome-claude-design/blob/main/design-systems/airbnb/DESIGN.md.\",\n },\n];\n\nexport function getStarterKit(id: string): StarterKitDefinition | undefined {\n return STARTER_KITS.find((k) => k.id === id);\n}\n","/**\n * Project-scoped design kit selection + customization (starter presets).\n * Persisted under fs_projects.data.designKit — see /api/projects/[id]/design-kit.\n */\n\nexport type DesignKitDensity = \"compact\" | \"comfortable\";\n\nexport type DesignKitFontPreset = \"inter\" | \"system\";\n\n/**\n * Which component stack this preset targets when scaffolding or extending UI.\n * Distinct from visual tokens — shadcn, MUI, HeroUI, and Untitled are different\n * products (install model, APIs, licensing).\n */\nexport type UiFoundationId =\n | \"shadcn-ui\"\n | \"mui-material\"\n | \"heroui\"\n | \"untitled-ui\"\n | \"agnostic\";\n\nexport const UI_FOUNDATION_LABELS: Record = {\n \"shadcn-ui\": \"shadcn/ui · Radix · Tailwind\",\n \"mui-material\": \"MUI (Material UI)\",\n heroui: \"HeroUI · Next.js\",\n \"untitled-ui\": \"Untitled UI · React Aria · Tailwind\",\n agnostic: \"Tokens only — match your existing stack\",\n};\n\n/** User-authored deltas applied on top of a starter kit's defaults. */\nexport interface DesignKitOverrides {\n accentHex?: string;\n /** Mid-step radius in px; xs/sm/xl derived from this */\n radiusMdPx?: number;\n fontPreset?: DesignKitFontPreset;\n density?: DesignKitDensity;\n}\n\nexport interface StarterKitDefinition {\n id: string;\n name: string;\n tagline: string;\n defaults: DesignKitOverrides;\n /** Customize panel fields shown for this starter */\n customizeFields: Array<\"accent\" | \"radius\" | \"font\" | \"density\">;\n uiFoundation: UiFoundationId;\n /** Short guidance for the coding agent (stack, licensing, when not to add libs). */\n foundationNotesForAi: string;\n}\n\n/** Persisted JSON shape */\nexport interface DesignKitPersisted {\n kitId: string;\n /** Overrides keyed by kit id so switching kits preserves each setup */\n perKit: Record;\n}\n\nexport const DEFAULT_DESIGN_KIT_ID = \"twenty-crm\";\n","/**\n * Sentry-as-product integration.\n *\n * Provisions a Sentry project per Vibn project under the shared\n * `vibnai` Sentry org, then makes its DSN + auth token available\n * to any Coolify app deployed for that project. Real-user errors\n * from the user's deployed app land in Sentry; AI tools (Stage 3)\n * read the issue feed back into chat (Stage 4).\n *\n * Design choices:\n * - **Idempotent.** Looks up existing Sentry projects by slug\n * before creating, so reruns are safe.\n * - **Shared org token.** The same `SENTRY_AUTH_TOKEN` we use to\n * upload source maps for vibn-frontend is reused across every\n * user project — it has org-write scope. This means we DON'T\n * need per-project auth tokens, just per-project DSNs.\n * - **Slug convention.** `vibn-{workspace}-{projectSlug}`. Avoids\n * collisions across workspaces and stays under Sentry's\n * 50-char project slug limit. Clamped if longer.\n * - **Soft failure.** If Sentry provisioning fails (rate limit,\n * network, token revoked) we log and continue. The Vibn\n * project still works without Sentry; we'll lazily retry on\n * the next deploy.\n *\n * See SENTRY_AS_PRODUCT.md for the full proposal context.\n */\n\nimport { query } from '@/lib/db-postgres';\nimport { upsertApplicationEnv } from '@/lib/coolify';\n\nconst SENTRY_API_BASE = 'https://de.sentry.io/api/0';\nconst SENTRY_ORG_SLUG = 'vibnai';\nconst SENTRY_TEAM_SLUG = 'vibnai'; // Default team name matches org slug for personal/single-team setups.\n\nexport interface SentryProjectInfo {\n /** Sentry project slug, e.g. `vibn-mark-account-checkout-app`. */\n slug: string;\n /** Public DSN for runtime error capture. NEXT_PUBLIC_SENTRY_DSN. */\n dsn: string;\n /** When this row was created/updated. */\n provisionedAt: string;\n}\n\nexport interface ProvisionInput {\n projectId: string;\n /** Vibn workspace slug, e.g. \"mark-account\". */\n workspaceSlug: string;\n /** Vibn project slug, e.g. \"checkout-app\". */\n projectSlug: string;\n /** Display name shown in the Sentry UI. */\n projectName: string;\n}\n\n/**\n * Returns the Sentry project for a given Vibn project, creating\n * it if missing. Persists the result to fs_projects.data.sentry\n * so subsequent calls hit the cache instead of Sentry's API.\n *\n * Returns null if Sentry provisioning fails — caller should log\n * and continue.\n */\nexport async function ensureSentryProject(\n input: ProvisionInput,\n): Promise {\n const authToken = process.env.SENTRY_AUTH_TOKEN;\n if (!authToken) {\n console.warn('[sentry] SENTRY_AUTH_TOKEN missing — skipping provisioning');\n return null;\n }\n\n // 1. Fast path: already provisioned for this Vibn project.\n const cached = await loadSentryFromProject(input.projectId);\n if (cached) return cached;\n\n // 2. Build the deterministic slug. Sentry caps at 50 chars and\n // only allows lowercase a–z, 0–9, dashes.\n const slug = buildSentrySlug(input.workspaceSlug, input.projectSlug);\n\n try {\n // 3. Try to look up existing Sentry project (handles the case\n // where fs_projects was wiped but Sentry still has the project).\n const existing = await fetchSentryProject(slug, authToken);\n if (existing) {\n const info = await materialize(input.projectId, existing, authToken);\n if (info) return info;\n }\n\n // 4. Create fresh.\n const created = await createSentryProject({\n slug,\n name: input.projectName.slice(0, 50),\n authToken,\n });\n if (!created) return null;\n\n return await materialize(input.projectId, created, authToken);\n } catch (err) {\n console.error('[sentry] ensureSentryProject failed', err);\n return null;\n }\n}\n\n/**\n * Sets the standard Sentry env vars on a Coolify application\n * so that its next build inlines the DSN and uploads source maps.\n * Idempotent — safe to call on every apps.create. Soft-fails on\n * any per-key error so a single failed upsert doesn't block deploy.\n *\n * Env vars set:\n * - NEXT_PUBLIC_SENTRY_DSN — public DSN for runtime capture\n * - SENTRY_AUTH_TOKEN — shared org token for source map upload\n *\n * Both are marked Coolify-default (is_buildtime=true is_runtime=true);\n * the public DSN must be present at build time so Next.js inlines\n * it into the client bundle.\n */\nexport async function applySentryEnvToCoolifyApp(\n coolifyAppUuid: string,\n projectId: string,\n): Promise {\n const sentry = await loadSentryFromProject(projectId);\n if (!sentry) return; // Project not yet provisioned in Sentry — Stage 1 will retry.\n\n const authToken = process.env.SENTRY_AUTH_TOKEN;\n if (!authToken) return;\n\n const envs: Array<[string, string]> = [\n ['NEXT_PUBLIC_SENTRY_DSN', sentry.dsn],\n ['SENTRY_AUTH_TOKEN', authToken],\n ];\n\n for (const [key, value] of envs) {\n try {\n await upsertApplicationEnv(coolifyAppUuid, { key, value });\n } catch (err) {\n console.warn(`[sentry] upsert ${key} on ${coolifyAppUuid} failed`, err);\n }\n }\n}\n\n/**\n * Returns the Sentry project info for a Vibn project if already\n * provisioned, else null. Read-only — does NOT call Sentry's API.\n */\nexport async function loadSentryFromProject(\n projectId: string,\n): Promise {\n const rows = await query<{ data: any }>(\n `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,\n [projectId],\n );\n const sentry = rows[0]?.data?.sentry;\n if (sentry?.slug && sentry?.dsn && sentry?.provisionedAt) {\n return sentry as SentryProjectInfo;\n }\n return null;\n}\n\n// ──────────────────────────────────────────────────────────────────\n// Issue feed — Stage 3 MCP tools read these\n// ──────────────────────────────────────────────────────────────────\n\nexport interface SentryIssueSummary {\n id: string;\n title: string;\n /** \"error\" | \"fatal\" | \"warning\" | \"info\" | \"debug\" */\n level: string;\n /** Total event count (lifetime). */\n count: number;\n /** ISO of most recent occurrence. */\n lastSeen: string;\n /** ISO of first occurrence. */\n firstSeen: string;\n /** \"unresolved\" | \"resolved\" | \"ignored\" — we filter to unresolved. */\n status: string;\n /** Best-effort culprit string from Sentry, e.g. \"GET /api/checkout\". */\n culprit: string | null;\n /** Direct URL to the issue in Sentry's UI. */\n permalink: string | null;\n}\n\nexport interface SentryEventDetail {\n /** Sentry event ID (the canonical \"fingerprint\" of this occurrence). */\n eventId: string;\n /** Most recent event's render of the stack trace (top frames first). */\n stackFrames: Array<{\n function: string | null;\n filename: string | null;\n lineno: number | null;\n colno: number | null;\n /** Source code surrounding the line if Sentry has source maps. */\n contextLine: string | null;\n }>;\n /** User-context tag (email/id/username/ip), if Sentry captured one. */\n user: { email?: string; id?: string; username?: string; ipAddress?: string } | null;\n /** Request method + URL if this was an HTTP-facing error. */\n request: { method?: string; url?: string } | null;\n /** Truncated breadcrumbs (last 20) — clicks, fetches, navigations. */\n breadcrumbs: Array<{\n type: string | null;\n category: string | null;\n message: string | null;\n timestamp: string | null;\n }>;\n /** Direct URL to a Session Replay if the user had one recorded. */\n replayUrl: string | null;\n}\n\n/**\n * List recent unresolved issues for a Vibn project's Sentry project.\n * Returns [] if Sentry isn't provisioned yet — caller should treat\n * that as \"no errors\", which is functionally correct (no Sentry =\n * no error capture path).\n */\nexport async function listRecentSentryIssues(\n projectId: string,\n options?: { limit?: number; sinceHours?: number },\n): Promise {\n const sentry = await loadSentryFromProject(projectId);\n if (!sentry) return [];\n\n const authToken = process.env.SENTRY_AUTH_TOKEN;\n if (!authToken) return [];\n\n const limit = clampLimit(options?.limit ?? 10);\n const sinceHours = options?.sinceHours ?? 24;\n const statsPeriod = `${sinceHours}h`;\n\n const url = new URL(\n `${SENTRY_API_BASE}/projects/${SENTRY_ORG_SLUG}/${sentry.slug}/issues/`,\n );\n url.searchParams.set('limit', String(limit));\n url.searchParams.set('query', 'is:unresolved');\n url.searchParams.set('statsPeriod', statsPeriod);\n url.searchParams.set('sort', 'date');\n\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${authToken}` },\n });\n if (!res.ok) {\n console.warn(\n `[sentry] listRecentSentryIssues ${res.status}: ${await res.text()}`,\n );\n return [];\n }\n const raw = (await res.json()) as Array>;\n return raw.map((i) => ({\n id: String(i.id),\n title: String(i.title ?? '(untitled)'),\n level: String(i.level ?? 'error'),\n count: Number(i.count ?? 0),\n lastSeen: String(i.lastSeen ?? ''),\n firstSeen: String(i.firstSeen ?? ''),\n status: String(i.status ?? 'unresolved'),\n culprit: i.culprit ? String(i.culprit) : null,\n permalink: i.permalink ? String(i.permalink) : null,\n }));\n}\n\n/**\n * Fetch the latest event for an issue, with stack trace,\n * breadcrumbs, user, request, and Session Replay link if any.\n * Returns null if the issue isn't found or Sentry isn't ready.\n */\nexport async function getSentryIssueDetail(\n projectId: string,\n issueId: string,\n): Promise {\n const sentry = await loadSentryFromProject(projectId);\n if (!sentry) return null;\n const authToken = process.env.SENTRY_AUTH_TOKEN;\n if (!authToken) return null;\n\n const res = await fetch(\n `${SENTRY_API_BASE}/issues/${encodeURIComponent(issueId)}/events/latest/`,\n {\n headers: { Authorization: `Bearer ${authToken}` },\n },\n );\n if (!res.ok) {\n console.warn(\n `[sentry] getSentryIssueDetail ${res.status}: ${await res.text()}`,\n );\n return null;\n }\n const e = (await res.json()) as Record;\n\n // Sentry stores frames in a few different places depending on\n // platform. The exception entry has the most reliable shape.\n const exceptionEntry = (e.entries || []).find(\n (entry: any) => entry?.type === 'exception',\n );\n const firstException = exceptionEntry?.data?.values?.[0];\n const frames =\n (firstException?.stacktrace?.frames as Array> | undefined) ?? [];\n // Sentry returns frames in oldest-first order; reverse so top of\n // stack (the line that actually threw) is first — which is what\n // every developer scans first.\n const stackFrames = [...frames].reverse().slice(0, 12).map((f) => ({\n function: f.function ?? null,\n filename: f.filename ?? null,\n lineno: typeof f.lineno === 'number' ? f.lineno : null,\n colno: typeof f.colno === 'number' ? f.colno : null,\n contextLine: f.contextLine ?? null,\n }));\n\n const breadcrumbsEntry = (e.entries || []).find(\n (entry: any) => entry?.type === 'breadcrumbs',\n );\n const breadcrumbs =\n ((breadcrumbsEntry?.data?.values as Array> | undefined) ?? [])\n .slice(-20)\n .map((b) => ({\n type: b.type ?? null,\n category: b.category ?? null,\n message: b.message ?? null,\n timestamp: b.timestamp ?? null,\n }));\n\n const requestEntry = (e.entries || []).find(\n (entry: any) => entry?.type === 'request',\n );\n const request = requestEntry?.data\n ? {\n method: requestEntry.data.method ?? undefined,\n url: requestEntry.data.url ?? undefined,\n }\n : null;\n\n // Session Replay: Sentry attaches a `replayId` tag on events that\n // have one. Build the canonical UI URL.\n const replayId = (e.tags as Array> | undefined)?.find(\n (t) => t.key === 'replayId',\n )?.value;\n const replayUrl = replayId\n ? `https://${SENTRY_ORG_SLUG}.sentry.io/replays/${replayId}/`\n : null;\n\n return {\n eventId: String(e.eventID ?? e.id ?? ''),\n stackFrames,\n user: e.user ? sanitizeUser(e.user) : null,\n request,\n breadcrumbs,\n replayUrl,\n };\n}\n\n/**\n * Mark an issue resolved. Used by the AI after it ships a fix and\n * verifies the bug no longer fires (e.g. via tests, log watch, or\n * explicit user confirmation).\n */\nexport async function resolveSentryIssue(\n projectId: string,\n issueId: string,\n): Promise {\n const sentry = await loadSentryFromProject(projectId);\n if (!sentry) return false;\n const authToken = process.env.SENTRY_AUTH_TOKEN;\n if (!authToken) return false;\n\n const res = await fetch(\n `${SENTRY_API_BASE}/issues/${encodeURIComponent(issueId)}/`,\n {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${authToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ status: 'resolved' }),\n },\n );\n return res.ok;\n}\n\nfunction clampLimit(n: number): number {\n if (!Number.isFinite(n)) return 10;\n return Math.max(1, Math.min(50, Math.floor(n)));\n}\n\nfunction sanitizeUser(u: Record): SentryEventDetail['user'] {\n return {\n email: u.email ?? undefined,\n id: u.id ?? undefined,\n username: u.username ?? undefined,\n ipAddress: u.ip_address ?? u.ipAddress ?? undefined,\n };\n}\n\n// ──────────────────────────────────────────────────────────────────\n// Internal helpers\n// ──────────────────────────────────────────────────────────────────\n\nfunction buildSentrySlug(workspaceSlug: string, projectSlug: string): string {\n const raw = `vibn-${workspaceSlug}-${projectSlug}`\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n // Sentry: 50 char max. Truncate from the end (preserves the\n // `vibn-{workspace}` prefix which keeps slugs distinguishable\n // across workspaces).\n return raw.slice(0, 50);\n}\n\ninterface SentryApiProject {\n slug: string;\n name: string;\n id: string;\n}\n\nasync function fetchSentryProject(\n slug: string,\n authToken: string,\n): Promise {\n const res = await fetch(\n `${SENTRY_API_BASE}/projects/${SENTRY_ORG_SLUG}/${slug}/`,\n {\n headers: { Authorization: `Bearer ${authToken}` },\n },\n );\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `Sentry GET project failed: ${res.status} ${await res.text()}`,\n );\n }\n return (await res.json()) as SentryApiProject;\n}\n\nasync function createSentryProject(input: {\n slug: string;\n name: string;\n authToken: string;\n}): Promise {\n const res = await fetch(\n `${SENTRY_API_BASE}/teams/${SENTRY_ORG_SLUG}/${SENTRY_TEAM_SLUG}/projects/`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${input.authToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n name: input.name,\n slug: input.slug,\n platform: 'javascript-nextjs', // Most common case; doesn't gate which SDK actually sends events.\n }),\n },\n );\n if (!res.ok) {\n const text = await res.text();\n // 409 = slug already exists in another team; we tried the GET\n // path first, so this is genuinely a race or a slug conflict.\n if (res.status === 409) {\n console.warn(`[sentry] slug ${input.slug} taken — retry GET`);\n return await fetchSentryProject(input.slug, input.authToken);\n }\n throw new Error(`Sentry POST project failed: ${res.status} ${text}`);\n }\n return (await res.json()) as SentryApiProject;\n}\n\ninterface SentryClientKey {\n dsn: { public: string };\n isActive: boolean;\n}\n\nasync function fetchProjectDsn(\n slug: string,\n authToken: string,\n): Promise {\n const res = await fetch(\n `${SENTRY_API_BASE}/projects/${SENTRY_ORG_SLUG}/${slug}/keys/`,\n {\n headers: { Authorization: `Bearer ${authToken}` },\n },\n );\n if (!res.ok) {\n throw new Error(\n `Sentry GET keys failed: ${res.status} ${await res.text()}`,\n );\n }\n const keys = (await res.json()) as SentryClientKey[];\n // Sentry auto-creates a \"Default\" key on project creation; pick\n // the first active one. Multiple keys exist in pro setups but\n // we always want the live one.\n const active = keys.find((k) => k.isActive) ?? keys[0];\n return active?.dsn?.public ?? null;\n}\n\nasync function materialize(\n projectId: string,\n apiProj: SentryApiProject,\n authToken: string,\n): Promise {\n const dsn = await fetchProjectDsn(apiProj.slug, authToken);\n if (!dsn) {\n console.error(`[sentry] no DSN returned for ${apiProj.slug}`);\n return null;\n }\n\n const info: SentryProjectInfo = {\n slug: apiProj.slug,\n dsn,\n provisionedAt: new Date().toISOString(),\n };\n\n await query(\n `UPDATE fs_projects\n SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{sentry}', $2::jsonb, true),\n updated_at = NOW()\n WHERE id = $1`,\n [projectId, JSON.stringify(info)],\n );\n\n return info;\n}\n","/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || \"\";\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || \"gemini-3.1-pro-preview\";\nconst GEMINI_BASE_URL = \"https://generativelanguage.googleapis.com/v1beta\";\n\nexport interface ChatMessage {\n role: \"user\" | \"assistant\" | \"tool\";\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: \"text\" | \"thinking\" | \"tool_call\" | \"done\" | \"error\";\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === \"user\") {\n contents.push({ role: \"user\", parts: [{ text: msg.content }] });\n } else if (msg.role === \"assistant\") {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = {\n functionCall: { name: tc.name, args: tc.args, id: tc.id },\n };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: \"model\", parts });\n } else if (msg.role === \"tool\") {\n const part = {\n functionResponse: {\n name: msg.toolName || \"unknown\",\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === \"user\") {\n last.parts.push(part);\n } else {\n contents.push({ role: \"user\", parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [\n {\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n },\n ];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = \"\";\n let thoughts = \"\";\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id:\n part.functionCall.id ||\n `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield {\n type: \"error\",\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => \"\");\n yield {\n type: \"error\",\n error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}`,\n };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) {\n yield { type: \"error\", error: \"No response body\" };\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n if (!line.startsWith(\"data: \")) continue;\n const data = line.slice(6).trim();\n if (!data || data === \"[DONE]\") continue;\n let chunk: any;\n try {\n chunk = JSON.parse(data);\n } catch {\n continue;\n }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: \"thinking\", text: part.text }\n : { type: \"text\", text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: \"done\" };\n}\n","/**\n * OpenAI Chat Completions-compatible backend (DeepSeek, etc.).\n *\n * DeepSeek: base URL + `/chat/completions`, Bearer key — see\n * https://api-docs.deepseek.com/\n *\n * Tool schemas in Vibn are authored for Gemini (uppercase type enums).\n * We normalize them to JSON Schema before sending.\n */\n\nimport type { ChatMessage, ToolCall, ToolDefinition } from \"./gemini-chat\";\n\nconst DEFAULT_CHAT_URL = \"https://api.deepseek.com/chat/completions\";\n\nfunction resolveApiKey(): string {\n return (\n process.env.DEEPSEEK_API_KEY?.trim() ||\n process.env.VIBN_OPENAI_COMPATIBLE_API_KEY?.trim() ||\n \"\"\n );\n}\n\nfunction resolveChatUrl(): string {\n const raw = process.env.VIBN_OPENAI_COMPATIBLE_CHAT_URL?.trim();\n if (raw) return raw.replace(/\\/$/, \"\");\n const base = process.env.VIBN_OPENAI_COMPATIBLE_BASE_URL?.trim().replace(\n /\\/$/,\n \"\",\n );\n if (!base) return DEFAULT_CHAT_URL;\n if (base.endsWith(\"/chat/completions\")) return base;\n return `${base}/chat/completions`;\n}\n\nfunction resolveModel(): string {\n return (\n process.env.VIBN_OPENAI_COMPATIBLE_MODEL?.trim() ||\n process.env.DEEPSEEK_MODEL?.trim() ||\n \"deepseek-chat\"\n );\n}\n\n/** Gemini API Catalog-style schema → OpenAI JSON Schema */\nfunction geminiStyleToJsonSchema(node: unknown): unknown {\n if (node === null || typeof node !== \"object\" || Array.isArray(node))\n return node;\n const n = node as Record;\n const out: Record = {};\n\n for (const [key, val] of Object.entries(n)) {\n if (key === \"type\" && typeof val === \"string\") {\n const map: Record = {\n OBJECT: \"object\",\n STRING: \"string\",\n NUMBER: \"number\",\n INTEGER: \"integer\",\n BOOLEAN: \"boolean\",\n ARRAY: \"array\",\n };\n const upper = val.toUpperCase();\n out.type = map[upper] ?? val.toLowerCase();\n continue;\n }\n if (\n key === \"properties\" &&\n val &&\n typeof val === \"object\" &&\n !Array.isArray(val)\n ) {\n out.properties = Object.fromEntries(\n Object.entries(val as object).map(([k, v]) => [\n k,\n geminiStyleToJsonSchema(v),\n ]),\n );\n continue;\n }\n if (key === \"items\") {\n out.items = geminiStyleToJsonSchema(val);\n continue;\n }\n out[key] =\n val && typeof val === \"object\" && !Array.isArray(val)\n ? geminiStyleToJsonSchema(val)\n : val;\n }\n return out;\n}\n\nfunction toOpenAiTools(\n tools: ToolDefinition[] | undefined,\n): object[] | undefined {\n if (!tools?.length) return undefined;\n return tools.map((t) => ({\n type: \"function\",\n function: {\n name: t.name,\n description: t.description,\n parameters: geminiStyleToJsonSchema(t.parameters) as Record<\n string,\n unknown\n >,\n },\n }));\n}\n\n/**\n * OpenAI Chat Completions forbid `user`/`assistant` between an assistant\n * `tool_calls` block and the matching `tool` replies. Gemini-oriented code\n * may inject recovery `user` rows between individual tool results — move\n * those users to immediately after all tool rows for that assistant turn.\n */\nfunction reorderMessagesForOpenAiToolPairs(\n messages: ChatMessage[],\n): ChatMessage[] {\n const result: ChatMessage[] = [];\n let i = 0;\n while (i < messages.length) {\n const m = messages[i]!;\n if (m.role !== \"assistant\" || !m.toolCalls?.length) {\n result.push(m);\n i++;\n continue;\n }\n\n const expectedIds = m.toolCalls.map((tc) => tc.id);\n const pending = new Set(expectedIds);\n result.push(m);\n i++;\n\n const toolById = new Map();\n const bufferedUsers: ChatMessage[] = [];\n\n while (i < messages.length && pending.size > 0) {\n const n = messages[i]!;\n if (n.role === \"tool\" && n.toolCallId && pending.has(n.toolCallId)) {\n toolById.set(n.toolCallId, n);\n pending.delete(n.toolCallId);\n i++;\n continue;\n }\n if (n.role === \"user\") {\n bufferedUsers.push(n);\n i++;\n continue;\n }\n break;\n }\n\n for (const id of expectedIds) {\n const t = toolById.get(id);\n if (t) result.push(t);\n }\n result.push(...bufferedUsers);\n }\n return result;\n}\n\nfunction toOpenAiMessages(\n systemPrompt: string,\n messages: ChatMessage[],\n): object[] {\n const normalized = reorderMessagesForOpenAiToolPairs(messages);\n const out: object[] = [{ role: \"system\", content: systemPrompt }];\n for (const m of normalized) {\n if (m.role === \"user\") {\n out.push({ role: \"user\", content: m.content });\n } else if (m.role === \"assistant\") {\n const hasTools = Boolean(m.toolCalls?.length);\n const text = typeof m.content === \"string\" ? m.content.trim() : \"\";\n const msg: Record = {\n role: \"assistant\",\n content: text.length > 0 ? m.content : hasTools ? null : \"\",\n };\n if (hasTools && m.toolCalls) {\n msg.tool_calls = m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\",\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.args ?? {}),\n },\n }));\n }\n out.push(msg);\n } else if (m.role === \"tool\") {\n const body =\n typeof m.content === \"string\"\n ? m.content\n : JSON.stringify(m.content ?? \"\");\n out.push({\n role: \"tool\",\n tool_call_id: m.toolCallId ?? \"\",\n content: body.length > 0 ? body : \"(empty)\",\n });\n }\n }\n return out;\n}\n\nfunction parseAssistantMessage(message: Record | undefined): {\n text: string;\n thoughts: string;\n toolCalls: ToolCall[];\n} {\n const rawText = typeof message?.content === \"string\" ? message.content : \"\";\n const thoughts =\n typeof message?.reasoning_content === \"string\"\n ? message.reasoning_content\n : typeof (message as { reasoning?: string })?.reasoning === \"string\"\n ? (message as { reasoning: string }).reasoning\n : \"\";\n\n const stripTags = (s: string) =>\n s\n .replace(/[\\s\\S]*?<\\/tool_calls>/g, \"\")\n .replace(/[\\s\\S]*?<\\/think>/g, \"\")\n .trim();\n\n // DeepSeek separates thinking from speaking — during tool loops it\n // often puts everything in reasoning_content and leaves content empty.\n // When that happens, surface the reasoning as the user-visible text\n // so the user isn't staring at silent tool pills.\n const text = stripTags(rawText || thoughts);\n const toolCalls: ToolCall[] = [];\n const rawCalls = message?.tool_calls;\n if (Array.isArray(rawCalls)) {\n for (const c of rawCalls) {\n const call = c as Record;\n if (call.type !== \"function\") continue;\n const fn = call.function as Record | undefined;\n const name = typeof fn?.name === \"string\" ? fn.name : \"\";\n const id =\n typeof call.id === \"string\"\n ? call.id\n : `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n let args: Record = {};\n const argStr = typeof fn?.arguments === \"string\" ? fn.arguments : \"{}\";\n try {\n args = JSON.parse(argStr || \"{}\") as Record;\n } catch {\n args = {};\n }\n if (name) toolCalls.push({ id, name, args });\n }\n }\n return { text, thoughts, toolCalls };\n}\n\n/**\n * Non-streaming chat + tool calls — mirrors {@link callGeminiChat} return shape.\n */\nexport async function callOpenAiCompatibleChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /** Unused for OpenAI-compat; kept for call-site symmetry */\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const apiKey = resolveApiKey();\n if (!apiKey) {\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error:\n \"No API key: set DEEPSEEK_API_KEY or VIBN_OPENAI_COMPATIBLE_API_KEY for OpenAI-compatible chat.\",\n };\n }\n\n const url = resolveChatUrl();\n const model = resolveModel();\n const tools = toOpenAiTools(opts.tools);\n const oaiMessages = toOpenAiMessages(opts.systemPrompt, opts.messages);\n const body: Record = {\n model,\n messages: oaiMessages,\n temperature: opts.temperature ?? 0.7,\n max_tokens: 8192,\n stream: false,\n };\n if (tools?.length) body.tools = tools;\n\n // ── Request logging (DeepSeek 400 debug) ──────────────────────────────\n const msgSummary = oaiMessages.map((m: any) => ({\n role: m.role,\n has_tool_calls:\n m.role === \"assistant\" ? Boolean(m.tool_calls?.length) : undefined,\n tool_calls_ids:\n m.role === \"assistant\" && m.tool_calls?.length\n ? m.tool_calls.map((tc: any) => tc.id)\n : undefined,\n tool_call_id: m.role === \"tool\" ? m.tool_call_id : undefined,\n content_len: typeof m.content === \"string\" ? m.content.length : 0,\n }));\n console.error(\n \"[deepseek] request\",\n JSON.stringify({\n url,\n model,\n msg_count: oaiMessages.length,\n has_tools: Boolean(tools?.length),\n tool_count: tools?.length ?? 0,\n msg_summary: msgSummary,\n last_5_roles: msgSummary.slice(-5).map((m: any) => m.role),\n }),\n );\n // ───────────────────────────────────────────────────────────────────────\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n });\n } catch (e) {\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = (await res.json().catch(() => ({}))) as Record;\n if (!res.ok) {\n // ── Error logging (DeepSeek 400 debug) ───────────────────────────────\n console.error(\n \"[deepseek] error response\",\n JSON.stringify(\n {\n status: res.status,\n status_text: res.statusText,\n headers: Object.fromEntries(res.headers.entries()),\n body: data,\n // include the last few messages sent so we can see the exact\n // pattern that triggered the error\n last_5_sent: msgSummary.slice(-5),\n },\n null,\n 2,\n ),\n );\n // ─────────────────────────────────────────────────────────────────────\n const errObj = data?.error as Record | undefined;\n const msg =\n (typeof errObj?.message === \"string\" && errObj.message) ||\n JSON.stringify(data).slice(0, 280);\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Chat API error ${res.status}: ${msg}`,\n };\n }\n\n const choice = (data.choices as Record[] | undefined)?.[0];\n const message = choice?.message as Record | undefined;\n const { text, thoughts, toolCalls } = parseAssistantMessage(message);\n const finishReason =\n typeof choice?.finish_reason === \"string\"\n ? choice.finish_reason\n : undefined;\n\n return { text, thoughts, toolCalls, finishReason };\n}\n","/**\n * Routes workspace AI chat to Gemini or an OpenAI-compatible API (e.g. DeepSeek).\n *\n * Env:\n * VIBN_CHAT_PROVIDER=gemini | deepseek | openai_compatible\n *\n * Default: gemini (requires GOOGLE_API_KEY / studio key + VIBN_CHAT_MODEL).\n *\n * DeepSeek / OpenAI-compat:\n * DEEPSEEK_API_KEY (or VIBN_OPENAI_COMPATIBLE_API_KEY)\n * Optional: VIBN_OPENAI_COMPATIBLE_CHAT_URL (default https://api.deepseek.com/chat/completions)\n * Optional: VIBN_OPENAI_COMPATIBLE_MODEL (default deepseek-chat)\n */\n\nimport type { ChatMessage, ToolDefinition } from './gemini-chat';\nimport { callGeminiChat } from './gemini-chat';\nimport { callOpenAiCompatibleChat } from './openai-compatible-chat';\n\nexport type VibnChatCallOpts = {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n};\n\nexport async function callVibnChat(opts: VibnChatCallOpts) {\n const p = (process.env.VIBN_CHAT_PROVIDER || 'gemini').toLowerCase().trim();\n if (p === 'deepseek' || p === 'openai_compatible') {\n return callOpenAiCompatibleChat(opts);\n }\n return callGeminiChat(opts);\n}\n","/**\n * Serialize persisted design-kit state for system prompts and MCP `projects.get`.\n */\n\nimport { getStarterKit } from \"./registry\";\nimport { resolveKitTokens } from \"./resolve\";\nimport type { ResolvedKitTokens } from \"./resolve\";\nimport type { DesignKitOverrides, DesignKitPersisted } from \"./types\";\nimport { DEFAULT_DESIGN_KIT_ID, UI_FOUNDATION_LABELS } from \"./types\";\n\nexport function parsePersistedDesignKit(raw: unknown): DesignKitPersisted | null {\n if (raw === undefined || raw === null) return null;\n if (typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const r = raw as Record;\n const kitId =\n typeof r.kitId === \"string\" && r.kitId.trim()\n ? r.kitId.trim()\n : DEFAULT_DESIGN_KIT_ID;\n const perKit: Record = {};\n if (r.perKit && typeof r.perKit === \"object\" && !Array.isArray(r.perKit)) {\n for (const [key, val] of Object.entries(r.perKit)) {\n if (!val || typeof val !== \"object\" || Array.isArray(val)) continue;\n const v = val as Record;\n const o: DesignKitOverrides = {};\n if (typeof v.accentHex === \"string\") o.accentHex = v.accentHex;\n if (typeof v.radiusMdPx === \"number\" && Number.isFinite(v.radiusMdPx)) {\n o.radiusMdPx = v.radiusMdPx;\n }\n if (v.fontPreset === \"inter\" || v.fontPreset === \"system\") {\n o.fontPreset = v.fontPreset;\n }\n if (v.density === \"compact\" || v.density === \"comfortable\") {\n o.density = v.density;\n }\n perKit[key] = o;\n }\n }\n return { kitId, perKit };\n}\n\n/** Resolved tokens + hints for codegen; null if no kit saved or starter id unknown. */\nexport function buildDesignKitToolPayload(projectData: {\n designKit?: unknown;\n} | null): {\n kitId: string;\n starterKitName: string;\n uiFoundation: string;\n foundationLabel: string;\n foundationNotesForAi: string;\n overrides: DesignKitOverrides;\n resolved: ResolvedKitTokens;\n applyNote: string;\n} | null {\n const persisted = parsePersistedDesignKit(projectData?.designKit);\n if (!persisted) return null;\n const kit = getStarterKit(persisted.kitId);\n if (!kit) return null;\n const overrides = persisted.perKit[persisted.kitId] ?? {};\n const resolved = resolveKitTokens(kit, overrides);\n return {\n kitId: persisted.kitId,\n starterKitName: kit.name,\n uiFoundation: kit.uiFoundation,\n foundationLabel: UI_FOUNDATION_LABELS[kit.uiFoundation],\n foundationNotesForAi: kit.foundationNotesForAi,\n overrides,\n resolved,\n applyNote:\n \"Saving theme on the Design tab updates project metadata only, not the repo. Wire tokens with fs_* (e.g. :root CSS variables, Tailwind theme.extend, MUI createTheme, HeroUI theme). If many hard-coded colors exist and no shared theme layer, tell the user honestly that matching the kit may require a refactor toward centralized tokens.\",\n };\n}\n\n/** Markdown block appended to the active-project section of the chat system prompt. */\nexport function buildDesignKitPromptSection(\n activeProject: { designKit?: unknown } | null | undefined,\n): string {\n const payload = buildDesignKitToolPayload(activeProject ?? null);\n if (!payload) return \"\";\n\n const { starterKitName, kitId, overrides, resolved, uiFoundation, foundationLabel, foundationNotesForAi } =\n payload;\n return `\n## Design kit / theme (authoritative for UI styling)\n\nThe founder configured the visual language on the **Design** tab. Treat **resolved** values below as the source of truth when editing frontend code under this project's repo (\\`/workspace//\\`).\n\n- **Starter kit:** ${starterKitName} (\\`${kitId}\\`)\n- **UI foundation:** ${foundationLabel} (\\`${uiFoundation}\\`)\n- **Foundation guidance (follow when adding components):** ${foundationNotesForAi}\n- **Saved overrides:** ${JSON.stringify(overrides)}\n- **Accent (base):** \\`${resolved.accentHex}\\`\n- **Accent ramp (12 steps):** ${JSON.stringify(resolved.accentScale)}\n- **Neutral ramp (12 steps):** ${JSON.stringify(resolved.grayScale)}\n- **Radius (px):** xs=${resolved.radiusXs}, sm=${resolved.radiusSm}, md=${resolved.radiusMdPx}, xl=${resolved.radiusXl}, xxl=${resolved.radiusXxl}\n- **Font stack:** ${resolved.fontFamily}\n- **Density:** ${resolved.density}\n\n**Applying theme:** The Design tab does **not** mutate files. When the user saves theme changes or asks the app to match the kit, update the codebase's theme layer (whatever stack they use — locate it with \\`fs_grep\\` / \\`fs_read\\`). If matching would require touching many unrelated components because colors are inlined everywhere, **say so explicitly** and describe that a **central-token refactor** is needed before the UI can fully align — do not imply the preview alone updates production code.\n`.trimStart();\n}\n","import type { DesignKitOverrides, StarterKitDefinition } from \"./types\";\n\nexport interface ResolvedKitTokens {\n accentHex: string;\n accentScale: string[];\n grayScale: string[];\n radiusMdPx: number;\n radiusXs: number;\n radiusSm: number;\n radiusXl: number;\n radiusXxl: number;\n fontFamily: string;\n density: \"compact\" | \"comfortable\";\n}\n\n/** Fixed CRM-neutral ramp (custom neutral tint could merge later). */\nexport const GRAY_TWELVE: string[] = [\n \"#ffffff\",\n \"#fafafa\",\n \"#f5f5f5\",\n \"#ebebeb\",\n \"#e0e0e0\",\n \"#d4d4d4\",\n \"#c4c4c4\",\n \"#a3a3a3\",\n \"#737373\",\n \"#525252\",\n \"#3f3f3f\",\n \"#1a1a1a\",\n];\n\nfunction parseHex(hex: string): [number, number, number] | null {\n const n = hex.trim().replace(/^#/, \"\");\n if (n.length !== 6 || !/^[0-9a-fA-F]+$/.test(n)) return null;\n return [parseInt(n.slice(0, 2), 16), parseInt(n.slice(2, 4), 16), parseInt(n.slice(4, 6), 16)];\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n const x = (v: number) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, \"0\");\n return `#${x(r)}${x(g)}${x(b)}`;\n}\n\n/** Linear RGB mix (good enough for UI ramps). */\nexport function mixHex(a: string, b: string, t: number): string {\n const A = parseHex(a);\n const B = parseHex(b);\n if (!A || !B) return a;\n const r = Math.round(A[0] + (B[0] - A[0]) * t);\n const g = Math.round(A[1] + (B[1] - A[1]) * t);\n const bl = Math.round(A[2] + (B[2] - A[2]) * t);\n return rgbToHex(r, g, bl);\n}\n\n/** Twelve-step accent ramp: light tints → base → deep shadows. */\nexport function buildAccentScale(base: string): string[] {\n const steps: string[] = [];\n for (let i = 0; i < 12; i++) {\n const t = i / 11;\n if (t <= 0.45) {\n steps.push(mixHex(\"#ffffff\", base, t / 0.45 * 0.92));\n } else {\n steps.push(mixHex(base, \"#0c0c12\", (t - 0.45) / 0.55 * 0.92));\n }\n }\n return steps;\n}\n\nexport function mergeOverrides(\n kit: StarterKitDefinition,\n saved?: DesignKitOverrides,\n): Required<\n Pick\n> {\n const d = kit.defaults;\n const o = saved ?? {};\n return {\n accentHex: o.accentHex ?? d.accentHex ?? \"#5f6bf5\",\n radiusMdPx: o.radiusMdPx ?? d.radiusMdPx ?? 8,\n fontPreset: o.fontPreset ?? d.fontPreset ?? \"inter\",\n density: o.density ?? d.density ?? \"comfortable\",\n };\n}\n\nexport function resolveKitTokens(\n kit: StarterKitDefinition,\n saved?: DesignKitOverrides,\n): ResolvedKitTokens {\n const m = mergeOverrides(kit, saved);\n const md = m.radiusMdPx;\n return {\n accentHex: m.accentHex,\n accentScale: buildAccentScale(m.accentHex),\n grayScale: GRAY_TWELVE,\n radiusMdPx: md,\n radiusXs: Math.max(2, Math.round(md * 0.25)),\n radiusSm: Math.max(4, Math.round(md * 0.5)),\n radiusXl: Math.max(md + 4, Math.round(md * 2.25)),\n radiusXxl: Math.max(md + 8, Math.round(md * 4.5)),\n fontFamily:\n m.fontPreset === \"system\"\n ? 'system-ui, -apple-system, \"Segoe UI\", sans-serif'\n : \"var(--font-inter), ui-sans-serif, system-ui, sans-serif\",\n density: m.density,\n };\n}\n"],"names":[],"mappings":"wCAaO,IAAM,EAAuC,CAClD,CACE,GAAI,iBACJ,KAAM,wBACN,QAAS,sDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAO,CAC7C,aAAc,YACd,qBACE,0LACJ,EACA,CACE,GAAI,eACJ,KAAM,iBACN,QACE,oEACF,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,eACd,qBACE,mPACJ,EACA,CACE,GAAI,cACJ,KAAM,SACN,QAAS,uDACT,SAAU,CACR,UAAW,UACX,WAAY,GACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,SACd,qBACE,gKACJ,EACA,CACE,GAAI,cACJ,KAAM,cACN,QAAS,oDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,cACd,qBACE,kOACJ,EACA,CACE,GAAI,aACJ,KAAM,oBACN,QAAS,wDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,6MACJ,EACA,CACE,GAAI,UACJ,KAAM,UACN,QAAS,4CACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,8OACJ,EACA,CACE,GAAI,eACJ,KAAM,eACN,QAAS,0DACT,SAAU,CACR,UAAW,UACX,WAAY,GACZ,WAAY,SACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,qIACJ,EACA,CACE,GAAI,iBACJ,KAAM,mBACN,QAAS,wDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,wIACJ,EACA,CACE,GAAI,eACJ,KAAM,iBACN,QAAS,yCACT,SAAU,CACR,UAAW,UACX,WAAY,GACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,6HACJ,EACA,CACE,GAAI,iBACJ,KAAM,mBACN,QAAS,gDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,uGACJ,EACA,CACE,GAAI,iBACJ,KAAM,oBACN,QAAS,gDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,4GACJ,EACA,CACE,GAAI,mBACJ,KAAM,qBACN,QAAS,kEACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,SACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAO,CAC7C,aAAc,WACd,qBACE,saACJ,EACA,CACE,GAAI,iBACJ,KAAM,mBACN,QAAS,mEACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,QACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,4YACJ,EACA,CACE,GAAI,aACJ,KAAM,qBACN,QAAS,4DACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,SACZ,QAAS,SACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,+YACJ,EACA,CACE,GAAI,aACJ,KAAM,wBACN,QAAS,gDACT,SAAU,CACR,UAAW,UACX,WAAY,EACZ,WAAY,SACZ,QAAS,SACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAQ,UAAU,CACxD,aAAc,WACd,qBACE,uZACJ,EACA,CACE,GAAI,qBACJ,KAAM,uBACN,QAAS,4DACT,SAAU,CACR,UAAW,UACX,WAAY,GACZ,WAAY,SACZ,QAAS,aACX,EACA,gBAAiB,CAAC,SAAU,SAAU,OAAO,CAC7C,aAAc,WACd,qBACE,mXACJ,EACD,CAEM,SAAS,EAAc,CAAU,EACtC,OAAO,EAAa,IAAI,CAAC,AAAC,GAAM,EAAE,EAAE,GAAK,EAC3C,uGC1MqC,sCApC+B,CAClE,YAAa,+BACb,eAAgB,oBAChB,OAAQ,mBACR,cAAe,sCACf,SAAU,yCACZ,oCCAA,IAAA,EAAA,EAAA,CAAA,CAAA,QACA,EAAA,EAAA,CAAA,CAAA,0CAEA,IAAM,EAAkB,6BAClB,EAAkB,SA8BjB,eAAe,EACpB,CAAqB,MA4UE,EAAuB,EA1U9C,IAAM,EAAY,GA0U0B,AAAqB,KA1UvC,GAAG,CAAC,iBAAiB,CAC/C,GAAI,CAAC,EAEH,OADA,EADc,MACN,IAAI,CAAC,8DACN,KAIT,IAAM,EAAS,MAAM,EAAsB,EAAM,SAAS,EAC1D,GAAI,EAAQ,OAAO,EAInB,IAAM,KAAuB,EAAM,AAAtB,aAAmC,GAAE,EAAM,WAAW,CA+TvD,AAQL,CARM,KAAK,EAAE,EAAc,CAAC,EAAE,EAAA,CAAa,CAC/C,WAAW,GACX,OAAO,CAAC,eAAgB,KACxB,OAAO,CAAC,MAAO,KACf,OAAO,CAAC,SAAU,IAIV,KAAK,CAAC,EAAG,KArUpB,GAAI,CAGF,IAAM,EAAW,MAAM,EAAmB,EAAM,GAChD,GAAI,EAAU,CACZ,IAAM,EAAO,MAAM,EAAY,EAAM,SAAS,CAAE,EAAU,GAC1D,GAAI,EAAM,OAAO,CACnB,CAGA,IAAM,EAAU,MAAM,EAAoB,CACxC,OACA,KAAM,EAAM,WAAW,CAAC,KAAK,CAAC,EAAG,cACjC,CACF,GACA,GAAI,CAAC,EAAS,OAAO,KAErB,OAAO,MAAM,EAAY,EAAM,SAAS,CAAE,EAAS,EACrD,CAAE,MAAO,EAAK,CAEZ,OADA,QAAQ,KAAK,CAAC,sCAAuC,GAC9C,IACT,CACF,CAgBO,eAAe,EACpB,CAAsB,CACtB,CAAiB,EAEjB,IAAM,EAAS,MAAM,EAAsB,GAC3C,GAAI,CAAC,EAAQ,OAEb,CAFqB,GAEf,EAAY,QAAQ,GAAG,CAAC,iBAAiB,CAC/C,GAAK,CAAD,CAOJ,IAAK,GAAM,CAAC,CAPI,CAOC,EAAM,EALe,CACpC,CAAC,AAIwB,KAAM,CAVkD,mBAMtD,EAAO,GAAG,CAAC,CACtC,CAAC,oBAAqB,EAAU,CACjC,CAGC,GAAI,CACF,MAAM,CAAA,EAAA,EAAA,oBAAA,AAAoB,EAAC,EAAgB,KAAE,QAAK,CAAM,EAC1D,CAAE,MAAO,EAAK,CACZ,QAAQ,IAAI,CAAC,CAAC,gBAAgB,EAAE,EAAI,IAAI,EAAE,EAAe,OAAO,CAAC,CAAE,EACrE,CAEJ,CAMO,eAAe,EACpB,CAAiB,EAEjB,IAAM,EAAO,MAAM,CAAA,EAAA,EAAA,KAAA,AAAK,EACtB,CAAC,kDAAkD,CAAC,CACpD,CAAC,EAAU,EAEP,EAAS,CAAI,CAAC,EAAE,EAAE,MAAM,cAC9B,AAAI,GAAQ,MAAQ,GAAQ,KAAO,GAAQ,cAClC,CADiD,CAGnD,IACT,CA0DO,eAAe,EACpB,CAAiB,CACjB,CAAiD,MAgK/B,CAAS,CA9J3B,IAAM,EAAS,MAAM,EAAsB,GAC3C,GAAI,CAAC,EAAQ,MAAO,EAAE,CAEtB,IAAM,EAAY,QAAQ,GAAG,CAAC,iBAAiB,CAC/C,GAAI,CAAC,EAAW,MAAO,EAAE,CAEzB,IAAM,KAAmB,GAAS,AAApB,OAA6B,GAyJ3C,AAAK,IAAD,GAAQ,QAAQ,CAAC,GACd,CADkB,IACb,GAAG,CAAC,EAAG,KAAK,GAAG,CAAC,GAAI,KAAK,KAAK,CAAC,KADX,IAxJ1B,EAAa,GAAS,YAAc,GACpC,EAAc,CAAA,EAAG,EAAW,CAAC,CAAC,CAE9B,EAAM,IAAI,IACd,CAAA,EAAG,EAAgB,UAAU,EAAE,EAAgB,CAAC,EAAE,EAAO,IAAI,CAAC,QAAQ,CAAC,EAEzE,EAAI,YAAY,CAAC,GAAG,CAAC,QAAS,OAAO,IACrC,EAAI,YAAY,CAAC,GAAG,CAAC,QAAS,iBAC9B,EAAI,YAAY,CAAC,GAAG,CAAC,cAAe,GACpC,EAAI,YAAY,CAAC,GAAG,CAAC,OAAQ,QAE7B,IAAM,EAAM,MAAM,MAAM,EAAK,CAC3B,QAAS,CAAE,cAAe,CAAC,OAAO,EAAE,EAAA,CAAW,AAAC,CAClD,UACK,AAAL,EAAS,EAAL,AAAO,CAOJ,CAPM,AAMA,MAAM,EAAI,IAAI,EAAA,EAChB,GAAG,CAAC,AAAC,IAAM,AAAC,CACrB,GAAI,OAAO,EAAE,EAAE,EACf,MAAO,OAAO,EAAE,KAAK,EAAI,cACzB,MAAO,OAAO,EAAE,KAAK,EAAI,SACzB,MAAO,OAAO,EAAE,KAAK,EAAI,GACzB,SAAU,OAAO,EAAE,QAAQ,EAAI,IAC/B,UAAW,OAAO,EAAE,SAAS,EAAI,IACjC,OAAQ,OAAO,EAAE,MAAM,EAAI,cAC3B,QAAS,EAAE,OAAO,CAAG,OAAO,EAAE,OAAO,EAAI,KACzC,UAAW,EAAE,SAAS,CAAG,OAAO,EAAE,SAAS,EAAI,IACjD,CAAC,IAhBC,QAAQ,IAAI,CACV,CAAC,gCAAgC,EAAE,EAAI,MAAM,CAAC,EAAE,EAAE,MAAM,EAAI,IAAI,GAAA,CAAI,EAE/D,EAAE,CAcb,CAOO,eAAe,EACpB,CAAiB,CACjB,CAAe,QAGf,GAAI,CADW,AACV,MADgB,EAAsB,GAC9B,OAAO,KACpB,IAAM,EAAY,QAAQ,GAAG,CAAC,iBAAiB,CAC/C,GAAI,CAAC,EAAW,OAAO,KAEvB,IAAM,EAAM,MAAM,MAChB,CAAA,EAAG,EAAgB,QAAQ,EAAE,mBAAmB,GAAS,eAAe,CAAC,CACzE,CACE,QAAS,CAAE,cAAe,CAAC,OAAO,EAAE,EAAA,CAAW,AAAC,CAClD,GAEF,GAAI,CAAC,EAAI,EAAE,CAIT,CAJW,MACX,QAAQ,IAAI,CACV,CAAC,8BAA8B,EAAE,EAAI,MAAM,CAAC,EAAE,EAAE,MAAM,EAAI,IAAI,GAAA,CAAI,EAE7D,KAET,IAAM,EAAK,MAAM,EAAI,IAAI,GAInB,EAAiB,CAAC,EAAE,OAAO,EAAI,EAAA,AAAE,EAAE,IAAI,CAC3C,AAAC,GAAe,GAAO,OAAS,aAE5B,EAAiB,GAAgB,MAAM,QAAQ,CAAC,EAAE,CAMlD,EAAc,IAJjB,GAAgB,YAAY,QAAqD,EAAE,CAIvD,CAAC,OAAO,GAAG,KAAK,CAAC,EAAG,IAAI,GAAG,CAAC,AAAC,IAAM,AAAC,CACjE,SAAU,EAAE,QAAQ,EAAI,KACxB,SAAU,EAAE,QAAQ,EAAI,KACxB,OAA4B,UAApB,OAAO,EAAE,MAAM,CAAgB,EAAE,MAAM,CAAG,KAClD,MAA0B,UAAnB,OAAO,EAAE,KAAK,CAAgB,EAAE,KAAK,CAAG,KAC/C,YAAa,EAAE,WAAW,EAAI,KAChC,CAAC,EAEK,EAAmB,CAAC,EAAE,OAAO,EAAI,EAAA,AAAE,EAAE,IAAI,CAC7C,AAAC,GAAe,GAAO,OAAS,eAE5B,EACJ,CAAE,GAAkB,MAAM,QAAqD,EAAE,AAAF,EAC5E,KAAK,CAAC,CAAC,IACP,GAAG,CAAE,AAAD,IAAO,AAAC,CACX,KAAM,EAAE,IAAI,EAAI,KAChB,SAAU,EAAE,QAAQ,EAAI,KACxB,QAAS,EAAE,OAAO,EAAI,KACtB,UAAW,EAAE,SAAS,EAAI,KAC5B,CAAC,EAEC,EAAe,CAAC,EAAE,OAAO,EAAI,EAAA,AAAE,EAAE,IAAI,CACzC,AAAC,GAAe,GAAO,OAAS,WAE5B,EAAU,GAAc,KAC1B,CACE,OAAQ,EAAa,IAAI,CAAC,MAAM,EAAI,OACpC,IAAK,EAAa,IAAI,CAAC,GAAG,OAAI,CAChC,EACA,KAIE,EAAY,EAAE,IAAI,EAA6C,KACnE,AAAC,GAAgB,aAAV,EAAE,GAAG,GACX,MACG,EAAY,EACd,CAAC,QAAQ,EAAE,EAAgB,mBAAmB,EAAE,EAAS,CAAC,CAAC,CAC3D,KAEJ,MAAO,CACL,QAAS,OAAO,EAAE,OAAO,EAAI,EAAE,EAAE,EAAI,gBACrC,EACA,KAAM,EAAE,IAAI,CAyCP,CACL,CA1Ce,KA0CR,CAFW,CAAsB,CAxCZ,EAAE,IAAI,EA0CzB,KAAK,OAAI,EAClB,GAAI,EAAE,EAAE,OAAI,EACZ,SAAU,EAAE,QAAQ,OAAI,EACxB,UAAW,EAAE,UAAU,EAAI,EAAE,SAAS,OAAI,CAC5C,EA9CwC,aACtC,cACA,YACA,CACF,CACF,CAOO,eAAe,EACpB,CAAiB,CACjB,CAAe,EAGf,GAAI,CADW,AACV,MADgB,EAAsB,GAC9B,OAAO,EACpB,IAAM,EAAY,QAAQ,GAAG,CAAC,iBAAiB,OAC/C,CAAI,CAAC,GAaE,CAXK,MAAM,CAFF,KAGd,CAAA,CAHqB,CAGlB,EAAgB,QAAQ,EAAE,mBAAmB,GAAS,CAAC,CAAC,CAC3D,CACE,OAAQ,MACR,QAAS,CACP,cAAe,CAAC,OAAO,EAAE,EAAA,CAAW,CACpC,eAAgB,kBAClB,EACA,KAAM,KAAK,SAAS,CAAC,CAAE,OAAQ,UAAW,EAC5C,EAAA,EAES,EAAE,AACf,CAsCA,eAAe,EACb,CAAY,CACZ,CAAiB,EAEjB,IAAM,EAAM,MAAM,MAChB,CAAA,EAAG,EAAgB,UAAU,EAAE,EAAgB,CAAC,EAAE,EAAK,CAAC,CAAC,CACzD,CACE,QAAS,CAAE,cAAe,CAAC,OAAO,EAAE,EAAA,CAAY,AAAD,CACjD,GAEF,GAAmB,MAAf,EAAI,MAAM,CAAU,OAAO,KAC/B,GAAI,CAAC,EAAI,EAAE,CACT,CADW,KACL,AAAI,MACR,CAAC,2BAA2B,EAAE,EAAI,MAAM,CAAC,CAAC,EAAE,MAAM,EAAI,IAAI,GAAA,CAAI,EAGlE,OAAQ,MAAM,EAAI,IAAI,EACxB,CAEA,eAAe,EAAoB,CAIlC,EACC,IAAM,EAAM,MAAM,MAChB,GAAG,WAAyB,KAAT,OAAO,IAAkB,CAAC,EAAE,CAA4B,CAC3E,CACE,OAAQ,OAFsD,AAG9D,QAAS,CACP,CAJsE,aAIvD,CAAC,OAAO,EAAE,EAAM,SAAS,CAAA,CAAE,CAC1C,eAAgB,kBAClB,EACA,KAAM,KAAK,SAAS,CAAC,CACnB,KAAM,EAAM,IAAI,CAChB,KAAM,EAAM,IAAI,CAChB,SAAU,mBACZ,EACF,GAEF,GAAI,CAAC,EAAI,EAAE,CAAE,CACX,IAAM,EAAO,MAAM,EAAI,IAAI,GAG3B,GAAmB,KAAK,CAApB,EAAI,MAAM,CAEZ,OADA,QAAQ,IAAI,CAAC,CAAC,cAAc,EAAE,EAAM,IAAI,CAAC,kBAAkB,CAAC,EACrD,MAAM,EAAmB,EAAM,IAAI,CAAE,EAAM,SAAS,CAE7D,OAAM,AAAI,MAAM,CAAC,4BAA4B,EAAE,EAAI,MAAM,CAAC,CAAC,EAAE,EAAA,CAAM,CACrE,CACA,OAAQ,MAAM,EAAI,IAAI,EACxB,CAOA,eAAe,EACb,CAAY,CACZ,CAAiB,EAEjB,IAAM,EAAM,MAAM,MAChB,CAAA,EAAG,EAAgB,UAAU,EAAE,EAAgB,CAAC,EAAE,EAAK,MAAM,CAAC,CAC9D,CACE,QAAS,CAAE,cAAe,CAAC,OAAO,EAAE,EAAA,CAAY,AAAD,CACjD,GAEF,GAAI,CAAC,EAAI,EAAE,CACT,CADW,KACL,AAAI,MACR,CAAC,wBAAwB,EAAE,EAAI,MAAM,CAAC,CAAC,EAAE,MAAM,EAAI,IAAI,GAAA,CAAI,EAG/D,IAAM,EAAQ,MAAM,EAAI,IAAI,GAItB,EAAS,EAAK,IAAI,CAAC,AAAC,GAAM,EAAE,QAAQ,GAAK,CAAI,CAAC,EAAE,CACtD,OAAO,GAAQ,KAAK,QAAU,IAChC,CAEA,eAAe,EACb,CAAiB,CACjB,CAAyB,CACzB,CAAiB,EAEjB,IAAM,EAAM,MAAM,EAAgB,EAAQ,IAAI,CAAE,GAChD,GAAI,CAAC,EAEH,GAFQ,IACR,QAAQ,KAAK,CAAC,CAAC,6BAA6B,EAAE,EAAQ,IAAI,CAAA,CAAE,EACrD,KAGT,IAAM,EAA0B,CAC9B,KAAM,EAAQ,IAAI,KAClB,EACA,cAAe,IAAI,OAAO,WAAW,EACvC,EAUA,OARA,MAAM,CAAA,EAAA,EAAA,KAAA,AAAK,EACT,CAAC;;;mBAGc,CAAC,CAChB,CAAC,EAAW,KAAK,SAAS,CAAC,GAAM,EAG5B,CACT,8MC3fA,IAAM,EAAiB,QAAQ,GAAG,CAAC,cAAc,EAAI,GAC/C,EAAe,QAAQ,GAAG,CAAC,eAAe,EAAI,yBAuH7C,eAAe,EAAe,CAMpC,EAQC,IAEI,EAFE,EAAM,GAAG,gBAAgB,QAAQ,gCAAE,aAAa,YAAuB,GAAgB,CAG7F,GAAI,EAHuE,OAIzE,EAAM,MAAM,MAAM,EAAK,CACrB,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,SAAS,CAAC,CAvCnB,EAAY,CAChB,SApEJ,AAoEc,SApEL,AAAiB,CAAuB,EAC/C,IAAM,EAAkB,EAAE,CAE1B,IAAK,IAAM,KAAO,EAChB,GAAiB,IADS,IACD,CAArB,EAAI,IAAI,CACV,EAAS,IAAI,CAAC,CAAE,KAAM,OAAQ,MAAO,CAAC,CAAE,KAAM,EAAI,OAAQ,AAAD,EAAG,AAAC,QACxD,GAAiB,cAAb,EAAI,IAAI,CAAkB,CACnC,IAAM,EAAe,EAAE,CAEvB,GADI,EAAI,OAAO,EAAE,EAAM,IAAI,CAAC,CAAE,KAAM,EAAI,OAAO,AAAC,GAC5C,EAAI,SAAS,EAAE,OACjB,CADyB,GACpB,IAAM,KAAM,EAAI,SAAS,CAAE,CAG9B,IAAM,EAAY,CAChB,aAAc,CAAE,KAAM,EAAG,IAAI,CAAE,KAAM,EAAG,IAAI,CAAE,GAAI,EAAG,EAAG,AAAD,CACzD,EACI,EAAG,gBAAgB,GAAE,EAAK,gBAAgB,CAAG,EAAG,gBAAA,AAAgB,EACpE,EAAM,IAAI,CAAC,EACb,CAEE,EAAM,MAAM,EAAE,EAAS,IAAI,CAAC,CAAE,KAAM,cAAS,CAAM,EACzD,MAAO,GAAiB,SAAb,EAAI,IAAI,CAAa,CAC9B,IAAM,EAAO,CACX,iBAAkB,CAChB,KAAM,EAAI,QAAQ,EAAI,UACtB,GAAI,EAAI,UAAU,CAClB,SAAU,CAAE,QAAS,EAAI,OAAO,AAAC,CACnC,CACF,EACM,EAAO,CAAQ,CAAC,EAAS,MAAM,CAAG,EAAE,CACtC,GAAM,OAAS,OACjB,CADyB,CACpB,KAAK,CAAC,IAAI,CAAC,GAEhB,EAAS,IAAI,CAAC,CAAE,KAAM,OAAQ,MAAO,CAAC,EAAK,AAAC,EAEhD,CAEF,OAAO,CACT,EA8B+B,EAAK,QAAQ,EACxC,kBAAmB,CAAE,MAAO,CAAC,CAAE,KAAM,AAqCJ,EArCS,YAAY,AAAC,EAAE,AAAC,EAC1D,iBAAkB,CAChB,YAAa,EAAK,WAAW,EAAI,GACjC,gBAAiB,KACjB,eAAgB,CAAE,gBAAiB,EAAK,eAAe,GAAI,CAAK,CAClE,CACF,EAEI,CADE,EAAM,AApCd,SAAS,AAAkB,CAAuB,EAChD,GAAK,CAAD,CAAO,MAAM,CACjB,CADmB,KACZ,CACL,CAFwB,AAGtB,qBAAsB,EAAM,GAAG,CAAC,AAAC,IAAM,AAAC,CACtC,KAAM,EAAE,IAAI,CACZ,YAAa,EAAE,WAAW,CAC1B,WAAY,EAAE,UAChB,AAD0B,CACzB,EACH,EACD,AACH,EAyBgC,EAAK,KAAK,EAAI,EAAE,KACrC,EAAK,KAAK,CAAG,CAAA,EACf,GA6BL,EACF,CAAE,MAAO,EAAG,CACV,MAAO,CACL,KAAM,GACN,SAAU,GACV,UAAW,EAAE,CACb,MAAO,CAAC,eAAe,EAAE,aAAa,MAAQ,EAAE,OAAO,CAAG,OAAO,GAAA,CAAI,AACvE,CACF,CAEA,IAAM,EAAO,MAAM,EAAI,IAAI,GAAG,KAAK,CAAC,IAAM,CAAC,EAAC,CAAC,EAC7C,GAAI,CAAC,EAAI,EAAE,CAAE,CACX,IAAM,EAAM,GAAM,OAAO,SAAW,KAAK,SAAS,CAAC,GAAM,KAAK,CAAC,EAAG,KAClE,MAAO,CACL,KAAM,GACN,SAAU,GACV,UAAW,EAAE,CACb,MAAO,CAAC,iBAAiB,EAAE,EAAI,MAAM,CAAC,EAAE,EAAE,EAAA,CAAK,AACjD,CACF,CAEA,IAAM,EAAO,GAAM,YAAY,CAAC,EAAE,CAC5B,EAAe,GAAM,SAAS,OAAS,EAAE,CAC3C,EAAO,GACP,EAAW,GACT,EAAwB,EAAE,CAEhC,IAAK,IAAM,KAAQ,EACb,EAAK,EADe,EACX,EAAE,CAMT,EAAK,OAAO,CAAE,GAAY,EAAK,IAAI,CAClC,GAAQ,EAAK,IAAI,EAEpB,EAAK,YAAY,EAAE,AACrB,EAAU,IAAI,CAAC,CACb,GACE,EAAK,YAAY,CAAC,EAAE,EACpB,CAAC,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,EAAE,KAAK,MAAM,GAAG,QAAQ,CAAC,IAAI,KAAK,CAAC,GAAA,CAAI,CAC3D,KAAM,EAAK,YAAY,CAAC,IAAI,CAC5B,KAAM,EAAK,YAAY,CAAC,IAAI,EAAI,CAAC,EAEjC,iBAAkB,EAAK,gBAAgB,AACzC,GAIJ,MAAO,MAAE,WAAM,YAAU,EAAW,aAAc,GAAM,YAAa,CACvE,CCgDO,eAAe,EAAyB,CAO9C,EAOC,IAkDI,EAlDE,EA1PJ,OA0Pa,CA1PL,GAAG,CAAC,gBAAgB,EAAE,QAC9B,QAAQ,GAAG,CAAC,8BAA8B,EAAE,QAC5C,GAyPF,GAAI,CAAC,EACH,MADW,AACJ,CACL,KAAM,GACN,SAAU,GACV,UAAW,EAAE,CACb,MACE,gGACJ,EAGF,IAAM,EA/PR,AA+Pc,SA/PL,EACP,IAAM,EAAM,QAAQ,GAAG,CAAC,+BAA+B,EAAE,OACzD,GAAI,EAAK,OAAO,EAAI,OAAO,CAAC,MAAO,IACnC,IAAM,EAAO,QAAQ,GAAG,CAAC,+BAA+B,EAAE,OAAO,QAC/D,MACA,WAEF,AAAK,EACD,EADA,AACK,EADE,MACM,CAAC,AADA,qBAC6B,CAAP,CACjC,CAAA,EAAG,EAAK,iBAAiB,CAAC,CAnBV,2CAoBzB,IAsPQ,EAlPJ,MAkPY,EAlPJ,GAAG,CAAC,4BAA4B,EAAE,QAC1C,QAAQ,GAAG,CAAC,cAAc,EAAE,QAC5B,gBAiPI,EA9LR,AA8LgB,SA9LP,AACP,CAAmC,EAEnC,GAAK,CAAD,EAAQ,OACZ,CADoB,MACb,CADoB,CACd,GAAG,CAAC,AAAC,GAAO,CAAD,CACtB,KAAM,WACN,SAAU,CACR,KAAM,EAAE,IAAI,CACZ,YAAa,EAAE,WAAW,CAC1B,WAvDN,AAuDkB,SAvDT,EAAwB,CAAa,EAC5C,GAAa,OAAT,GAAiC,UAAhB,OAAO,GAAqB,MAAM,OAAO,CAAC,GAC7D,OAAO,EAET,IAAM,EAA+B,CAAC,EAEtC,IAAK,GAAM,CAAC,EAAK,EAAI,GAAI,OAAO,OAAO,CAH7B,AAG8B,GAAI,CAC1C,GAAY,SAAR,GAAiC,UAAf,OAAO,EAAkB,CAU7C,EAAI,IAAI,CAAG,CATyB,CAClC,OAAQ,SACR,OAAQ,SACR,OAAQ,SACR,QAAS,UACT,QAAS,UACT,MAAO,QACT,CAEc,CADA,AACC,EADG,WAAW,GACR,EAAI,EAAI,WAAW,GACxC,QACF,CACA,GACE,AAAQ,kBACR,GACe,UAAf,OAAO,GACP,CAAC,MAAM,OAAO,CAAC,GACf,CACA,EAAI,UAAU,CAAG,OAAO,WAAW,CACjC,OAAO,OAAO,CAAC,GAAe,GAAG,CAAC,CAAC,CAAC,EAAG,EAAE,GAAK,CAC5C,EACA,EAAwB,GACzB,GAEH,QACF,CACA,GAAY,UAAR,EAAiB,CACnB,EAAI,KAAK,CAAG,EAAwB,GACpC,QACF,CACA,CAAG,CAAC,EAAI,CACN,GAAsB,UAAf,OAAO,GAAoB,CAAC,MAAM,OAAO,CAAC,GAC7C,EAAwB,GACxB,CACR,CACA,OAAO,CACT,EAW0C,EAAE,UAAU,CAIlD,EACF,CAAC,CACH,EA+K8B,EAAK,KAAK,EAChC,EA1HR,AA0HsB,SA1Hb,AACP,CAAoB,CACpB,CAAuB,EAEvB,IAAM,EAlDR,AAkDqB,SAlDZ,AACP,CAAuB,EAEvB,IAAM,EAAwB,EAAE,CAC5B,EAAI,EACR,KAAO,EAAI,EAAS,MAAM,EAAE,CAC1B,IAAM,EAAI,CAAQ,CAAC,EAAE,CACrB,GAAe,cAAX,EAAE,IAAI,EAAoB,CAAC,EAAE,SAAS,EAAE,OAAQ,CAClD,EAAO,IAAI,CAAC,GACZ,IACA,QACF,CAEA,IAAM,EAAc,EAAE,SAAS,CAAC,GAAG,CAAC,AAAC,GAAO,EAAG,EAAE,EAC3C,EAAU,IAAI,IAAI,GACxB,EAAO,IAAI,CAAC,GACZ,IAEA,IAAM,EAAW,IAAI,IACf,EAA+B,EAAE,CAEvC,KAAO,EAAI,EAAS,MAAM,EAAI,EAAQ,IAAI,CAAG,GAAG,CAC9C,IAAM,EAAI,CAAQ,CAAC,EAAE,CACrB,GAAe,SAAX,EAAE,IAAI,EAAe,EAAE,UAAU,EAAI,EAAQ,GAAG,CAAC,EAAE,UAAU,EAAG,CAClE,EAAS,GAAG,CAAC,EAAE,UAAU,CAAE,GAC3B,EAAQ,MAAM,CAAC,EAAE,UAAU,EAC3B,IACA,QACF,CACA,GAAe,SAAX,EAAE,IAAI,CAAa,CACrB,EAAc,IAAI,CAAC,GACnB,IACA,QACF,CACA,KACF,CAEA,IAAK,IAAM,KAAM,EAAa,CAC5B,IAAM,EAAI,EAAS,GAAG,CAAC,GACnB,GAAG,EAAO,IAAI,CAAC,EACrB,CACA,EAAO,IAAI,IAAI,EACjB,CACA,OAAO,CACT,EAMuD,GAC/C,EAAgB,CAAC,CAAE,KAAM,SAAU,QAAS,CAAa,EAAE,CACjE,IAAK,IAAM,KAAK,EACd,GAAe,MADW,EACH,CAAnB,EAAE,IAAI,CACR,EAAI,IAAI,CAAC,CAAE,KAAM,OAAQ,QAAS,EAAE,OAAO,AAAC,QACvC,GAAI,AAAW,gBAAT,IAAI,CAAkB,CACjC,IAAM,GAAW,CAAQ,EAAE,SAAS,EAAE,OAEhC,EAA+B,CACnC,KAAM,YACN,QAAS,CAHuB,UAArB,OAAO,EAAE,OAAO,CAAgB,EAAE,OAAO,CAAC,IAAI,GAAK,EAAA,EAGhD,MAAM,CAAG,EAAI,EAAE,OAAO,CAAG,EAAW,KAAO,EAC3D,CACI,IAAY,EAAE,SAAS,EAAE,CAC3B,EAAI,UAAU,CAAG,EAAE,SAAS,CAAC,GAAG,CAAC,AAAC,IAAQ,CACxC,AADuC,GACnC,EAAG,EAAE,CACT,KAAM,WACN,SAAU,CACR,KAAM,EAAG,IAAI,CACb,UAAW,KAAK,SAAS,CAAC,EAAG,IAAI,EAAI,CAAC,EACxC,EACF,CAAC,CAAA,EAEH,EAAI,IAAI,CAAC,EACX,MAAO,GAAe,SAAX,EAAE,IAAI,CAAa,CAC5B,IAAM,EACiB,UAArB,OAAO,EAAE,OAAO,CACZ,EAAE,OAAO,CACT,KAAK,SAAS,CAAC,EAAE,OAAO,EAAI,IAClC,EAAI,IAAI,CAAC,CACP,KAAM,OACN,aAAc,EAAE,UAAU,EAAI,GAC9B,QAAS,EAAK,MAAM,CAAG,EAAI,EAAO,SACpC,EACF,CAEF,OAAO,CACT,EAkFuC,EAAK,YAAY,CAAE,EAAK,QAAQ,EAC/D,EAAgC,OACpC,EACA,SAAU,EACV,YAAa,EAAK,WAAW,EAAI,GACjC,WAAY,KACZ,QAAQ,CACV,EACI,GAAO,SAAQ,EAAK,KAAK,CAAG,CAAA,EAGhC,IAAM,EAAa,EAAY,GAAG,CAAC,AAAC,IAAY,AAAD,CAC7C,KAAM,EAAE,IAAI,CACZ,eACa,cAAX,EAAE,IAAI,EAAmB,CAAQ,EAAE,UAAU,EAAE,YAAU,EAC3D,eACE,AAAW,gBAAT,IAAI,EAAoB,EAAE,UAAU,EAAE,OACpC,EAAE,UAAU,CAAC,GAAG,CAAC,AAAC,GAAY,EAAG,EAAE,OACnC,EACN,aAAc,AAAW,WAAT,IAAI,CAAc,EAAE,YAAY,MAAG,EACnD,YAAkC,UAArB,OAAO,EAAE,OAAO,CAAgB,EAAE,OAAO,CAAC,MAAM,CAAG,EAClE,CAAC,EACD,QAAQ,KAAK,CACX,qBACA,KAAK,SAAS,CAAC,KACb,QACA,EACA,UAAW,EAAY,MAAM,CAC7B,WAAW,CAAQ,GAAO,OAC1B,WAAY,GAAO,QAAU,EAC7B,YAAa,EACb,aAAc,EAAW,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,AAAC,GAAW,EAAE,IAAI,CAC3D,IAKF,GAAI,CACF,EAAM,MAAM,MAAM,EAAK,CACrB,OAAQ,OACR,QAAS,CACP,eAAgB,mBAChB,cAAe,CAAC,OAAO,EAAE,EAAA,CAAQ,AACnC,EACA,KAAM,KAAK,SAAS,CAAC,EACvB,EACF,CAAE,MAAO,EAAG,CACV,MAAO,CACL,KAAM,GACN,SAAU,GACV,UAAW,EAAE,CACb,MAAO,CAAC,eAAe,EAAE,aAAa,MAAQ,EAAE,OAAO,CAAG,OAAO,GAAA,CAAI,AACvE,CACF,CAEA,IAAM,EAAQ,MAAM,EAAI,IAAI,GAAG,KAAK,CAAC,IAAM,CAAC,EAAC,CAAC,EAC9C,GAAI,CAAC,EAAI,EAAE,CAAE,CAEX,QAAQ,KAAK,CACX,4BACA,KAAK,SAAS,CACZ,CACE,OAAQ,EAAI,MAAM,CAClB,YAAa,EAAI,UAAU,CAC3B,QAAS,OAAO,WAAW,CAAC,EAAI,OAAO,CAAC,OAAO,IAC/C,KAAM,EAGN,YAAa,EAAW,KAAK,CAAC,CAAC,EACjC,EACA,KACA,IAIJ,IAAM,EAAS,GAAM,MACf,EACwB,UAA3B,OAAO,GAAQ,SAAwB,EAAO,OAAO,EACtD,KAAK,SAAS,CAAC,GAAM,KAAK,CAAC,EAAG,KAChC,MAAO,CACL,KAAM,GACN,SAAU,GACV,UAAW,EAAE,CACb,MAAO,CAAC,eAAe,EAAE,EAAI,MAAM,CAAC,EAAE,EAAE,EAAA,CAAK,AAC/C,CACF,CAEA,IAAM,EAAU,EAAK,OAAO,EAA4C,CAAC,EAAE,CAErE,MAAE,CAAI,UAAE,CAAQ,WAAE,CAAS,CAAE,CAzKrC,AAyKwC,SAzK/B,AAAsB,CAA4C,EAKzE,IAAM,EAAsC,IAoKgB,MApK5C,OAAO,GAAS,QAAuB,EAAQ,OAAO,CAAG,GACnE,EACkC,UAAtC,OAAO,GAAS,kBACZ,EAAQ,iBAAiB,CACiC,UAA1D,OAAQ,GAAoC,UACzC,EAAkC,SAAS,CAC5C,GAYF,EATJ,CASqB,GAAW,CAAA,AAArB,EARR,OAAO,CAAC,sCAAuC,IAC/C,OAAO,CAAC,4BAA6B,IACrC,IAAI,GAOH,EAAwB,EAAE,CAC1B,EAAW,GAAS,WAC1B,GAAI,MAAM,OAAO,CAAC,GAChB,IAAK,IADsB,AAChB,KAAK,EAAU,CAExB,GAAkB,aAAd,EAAK,IAAI,CAAiB,SAC9B,IAAM,EAAK,EAAK,QAAQ,CAClB,EAA2B,UAApB,OAAO,GAAI,KAAoB,EAAG,IAAI,CAAG,GAChD,EACe,UAAnB,OAAO,EAAK,EAAE,CACV,AANO,EAMF,EAAE,CACP,CAAC,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,EAAE,KAAK,MAAM,GAAG,QAAQ,CAAC,IAAI,KAAK,CAAC,GAAA,CAAI,CAC3D,EAAgC,CAAC,EAC/B,EAAkC,UAAzB,OAAO,GAAI,UAAyB,EAAG,SAAS,CAAG,KAClE,GAAI,CACF,EAAO,KAAK,KAAK,CAAC,GAAU,KAC9B,CAAE,KAAM,CACN,EAAO,CAAC,CACV,CACI,GAAM,EAAU,IAAI,CAAC,IAAE,OAAI,OAAM,CAAK,EAC5C,CAEF,MAAO,MAAE,WAAM,YAAU,CAAU,CACrC,EAyHkB,GAAQ,SAOxB,MAAO,MAAE,EAAM,qBAAU,EAAW,aAJlC,AAAiC,iBAA1B,GAAQ,cACX,EAAO,aAAa,MACpB,CAE2C,CACnD,CC9VO,eAAe,EAAa,CAAsB,EACvD,IAAM,EAAI,CAAC,QAAQ,GAAG,CAAC,kBAAkB,EAAI,QAAA,CAAQ,CAAE,WAAW,GAAG,IAAI,SACzE,AAAU,aAAN,GAA0B,qBAAqB,CAA3B,EACf,EAAyB,GAE3B,EAAe,EACxB,6DC5BA,IAAA,EAAA,EAAA,CAAA,CAAA,QCYO,IAAM,EAAwB,CACnC,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACD,CAED,SAAS,EAAS,CAAW,EAC3B,IAAM,EAAI,EAAI,IAAI,GAAG,OAAO,CAAC,KAAM,WACnC,AAAiB,IAAb,CAAkB,CAAhB,MAAM,EAAW,iBAAiB,IAAI,CAAC,GACtC,CAD0C,AACzC,SAAS,EAAE,KAAK,CAAC,EAAG,GAAI,IAAK,SAAS,EAAE,KAAK,CAAC,EAAG,GAAI,IAAK,SAAS,EAAE,KAAK,CAAC,EAAG,GAAI,IAAI,CADtC,IAE1D,CAQO,SAAS,EAAO,CAAS,CAAE,CAAS,CAAE,CAAS,EACpD,IANM,EAMA,EAAI,EAAS,GACb,EAAI,EAAS,GACnB,GAAI,CAAC,GAAK,CAAC,EAAG,OAAO,EACrB,IAAM,EAAI,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAG,AAAC,EAAC,CAAC,EAAE,CAAG,CAAC,CAAC,EAAA,AAAE,EAAI,GACtC,EAAI,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAG,CAAC,CAAC,CAAC,EAAE,CAAG,CAAC,CAAC,EAAA,AAAE,EAAI,GACtC,EAAK,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAG,CAAC,CAAC,CAAC,EAAE,CAAG,CAAC,CAAC,EAAA,AAAE,EAAI,GAC7C,OAAO,EAZG,AAAC,GAAc,KAAK,GAAG,CAAC,EAAG,KAAK,GAAG,CAAC,IAAK,IAAI,QAAQ,CAAC,IAAI,QAAQ,CAAC,EAAG,KACzE,CAAC,CAAC,EAAE,EAAE,AAWG,GAXH,EAAK,EAWC,AAXC,GAAA,EAAK,EAAE,AAWL,GAXK,CAAI,AAYjC,CD3CA,IAAA,EAAA,EAAA,CAAA,CAAA,MAEO,SAAS,EAAwB,CAAY,EAClD,SAAI,GACe,KADP,KACR,OAAO,CADc,EACM,MADE,AACI,OAAO,CAAC,GADN,GACY,IADL,GACY,EAE1D,IAAM,EACe,UAAnB,OAAO,EAAE,KAAK,EAAiB,AAFvB,EAEyB,KAAK,CAAC,IAAI,GACvC,EAAE,KAAK,CAAC,IAAI,GACZ,EAAA,qBAAqB,CACrB,EAA6C,CAAC,EACpD,GAAI,EAAE,MAAM,EAAwB,UAApB,OAAO,EAAE,MAAM,EAAiB,CAAC,MAAM,OAAO,CAAC,EAAE,MAAM,EACrE,CADwE,GACnE,GAAM,CAAC,EAAK,EAAI,GAAI,OAAO,OAAO,CAAC,EAAE,MAAM,EAAG,CACjD,GAAI,CAAC,GAAsB,UAAf,OAAO,GAAoB,MAAM,OAAO,CAAC,GAAM,SAE3D,IAAM,EAAwB,CAAC,CACJ,WAAvB,OAAO,EAAE,SAAS,EAAe,GAAE,SAAS,CAAG,EAAE,SAAA,AAAS,EAClC,UAAxB,OAAO,EAAE,UAAU,EAAiB,OAAO,QAAQ,CAAC,EAAE,UAAU,GAAG,CACrE,EAAE,UAAU,CAAG,EAAE,UAAA,AAAU,GAER,UAAjB,EAAE,UAAU,EAAiC,WAAjB,EAAE,UAAU,AAAK,GAAU,CACzD,EAAE,UAAU,CAAG,EAAE,UAAA,AAAU,GAEX,YATR,AASN,EAAE,OAAO,EAAgC,gBAAd,EAAE,OAAO,AAAK,GAAe,CAC1D,EAAE,OAAO,CAAG,EAAE,OAAA,AAAO,EAEvB,CAAM,CAAC,EAAI,CAAG,CAChB,CAEF,MAAO,CAAE,eAAO,CAAO,CACzB,CAGO,SAAS,EAA0B,CAElC,EAUN,MCqBM,MDrBA,EAAY,EAAwB,GAAa,WACvD,GAAI,CAAC,EAAW,OAAO,KACvB,IAAM,EAAM,CAAA,EAAA,EAAA,aAAA,AAAa,EAAC,EAAU,KAAK,EACzC,GAAI,CAAC,EAAK,OAAO,KACjB,IAAM,EAAY,EAAU,MAAM,CAAC,EAAU,KAAK,CAAC,EAAI,CAAC,EAClD,GCeA,EDf4B,ACexB,EAAI,IDfG,ICeK,CAehB,EAAK,CADL,EAZC,CACL,CAWQ,SAXG,GAFH,ADhB6B,GC6Bd,AAbN,CAAC,GAEL,CAWe,QAXN,EAAI,EAAE,SAAS,EAAI,UACzC,WAAY,EAAE,UAAU,EAAI,EAAE,UAAU,EAAI,EAC5C,WAAY,EAAE,UAAU,EAAI,EAAE,UAAU,EAAI,QAC5C,QAAS,EAAE,OAAO,EAAI,EAAE,OAAO,EAAI,aACrC,GAQa,UAAU,CAChB,CACL,UAAW,EAAE,SAAS,CACtB,YArCG,AAqCU,SArCD,AAAiB,CAAY,EAC3C,IAAM,EAAkB,EAAE,CAC1B,IAAK,IAAI,EAAI,EAAG,EAAI,GAAI,IAAK,CAC3B,IAAM,EAAI,EAAI,EACV,IAAK,IACP,EADa,AACP,IAAI,CAAC,EAAO,UAAW,EAAM,EAAI,IAAO,MAE9C,EAAM,IAAI,CAAC,EAAO,EAAM,UAAW,CAAC,EAAI,GAAA,CAAI,CAAI,IAAO,KAE3D,CACA,OAAO,CACT,EA0BkC,EAAE,SAAS,EACzC,UAAW,EACX,WAAY,EACZ,SAAU,KAAK,GAAG,CAAC,EAAG,KAAK,KAAK,CAAM,IAAL,IACjC,SAAU,KAAK,GAAG,CAAC,EAAG,KAAK,KAAK,CAAM,GAAL,IACjC,SAAU,KAAK,GAAG,CAAC,EAAK,EAAG,KAAK,KAAK,CAAM,KAAL,IACtC,UAAW,KAAK,GAAG,CAAC,EAAK,EAAG,KAAK,KAAK,CAAM,IAAL,IACvC,WACmB,WAAjB,EAAE,UAAU,CACR,mDACA,0DACN,QAAS,EAAE,OAAO,AACpB,GD5CA,MAAO,CACL,MAAO,EAAU,KAAK,CACtB,eAAgB,EAAI,IAAI,CACxB,aAAc,EAAI,YAAY,CAC9B,gBAAiB,EAAA,oBAAoB,CAAC,EAAI,YAAY,CAAC,CACvD,qBAAsB,EAAI,oBAAoB,WAC9C,WACA,EACA,UACE,+UACJ,CACF,CAGO,SAAS,EACd,CAAyD,EAEzD,IAAM,EAAU,EAA0B,GAAiB,MAC3D,GAAI,CAAC,EAAS,MAAO,GAErB,GAAM,CAAE,gBAAc,OAAE,CAAK,WAAE,CAAS,UAAE,CAAQ,cAAE,CAAY,iBAAE,CAAe,sBAAE,CAAoB,CAAE,CACvG,EACF,MAAO,CAAC;;;;;mBAKS,EAAE,EAAe,IAAI,EAAE,EAAM;qBAC3B,EAAE,EAAgB,IAAI,EAAE,EAAa;2DACC,EAAE,qBAAqB;uBAC3D,EAAE,KAAK,SAAS,CAAC,WAAW;uBAC5B,EAAE,EAAS,SAAS,CAAC;8BACd,EAAE,KAAK,SAAS,CAAC,EAAS,WAAW,EAAE;+BACtC,EAAE,KAAK,SAAS,CAAC,EAAS,SAAS,EAAE;sBAC9C,EAAE,EAAS,QAAQ,CAAC,KAAK,EAAE,EAAS,QAAQ,CAAC,KAAK,EAAE,EAAS,UAAU,CAAC,KAAK,EAAE,EAAS,QAAQ,CAAC,MAAM,EAAE,EAAS,SAAS,CAAC;kBAChI,EAAE,EAAS,UAAU,CAAC;eACzB,EAAE,EAAS,OAAO,CAAC;;;AAGlC,CAAC,CAAC,SAAS,EACX"}}] +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__70c85bc2._.js:5130: "streamGeminiChat", +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__70c85bc2._.js:5131: ()=>streamGeminiChat +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__70c85bc2._.js:5292:async function* streamGeminiChat(opts) { +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__88db9aed._.js.map:15: {"offset": {"line": 2724, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/lib/ai/gemini-chat.ts"],"sourcesContent":["/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || '';\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || 'gemini-3.1-pro-preview';\nconst GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';\n\nexport interface ChatMessage {\n role: 'user' | 'assistant' | 'tool';\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: 'text' | 'thinking' | 'tool_call' | 'done' | 'error';\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n contents.push({ role: 'user', parts: [{ text: msg.content }] });\n } else if (msg.role === 'assistant') {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = { functionCall: { name: tc.name, args: tc.args, id: tc.id } };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: 'model', parts });\n } else if (msg.role === 'tool') {\n const part = {\n functionResponse: {\n name: msg.toolName || 'unknown',\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === 'user') {\n last.parts.push(part);\n } else {\n contents.push({ role: 'user', parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [{\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n }];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = '';\n let thoughts = '';\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id: part.functionCall.id || `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield { type: 'error', error: `Network error: ${e instanceof Error ? e.message : String(e)}` };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n yield { type: 'error', error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}` };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) { yield { type: 'error', error: 'No response body' }; return; }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (!data || data === '[DONE]') continue;\n let chunk: any;\n try { chunk = JSON.parse(data); } catch { continue; }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: 'thinking', text: part.text }\n : { type: 'text', text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: 'done' };\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;CASC;;;;;;AAED,MAAM,iBAAiB,QAAQ,GAAG,CAAC,cAAc,IAAI;AACrD,MAAM,eAAe,QAAQ,GAAG,CAAC,eAAe,IAAI;AACpD,MAAM,kBAAkB;AAgCxB,4DAA4D,GAC5D,SAAS,iBAAiB,QAAuB;IAC/C,MAAM,WAAkB,EAAE;IAE1B,KAAK,MAAM,OAAO,SAAU;QAC1B,IAAI,IAAI,IAAI,KAAK,QAAQ;YACvB,SAAS,IAAI,CAAC;gBAAE,MAAM;gBAAQ,OAAO;oBAAC;wBAAE,MAAM,IAAI,OAAO;oBAAC;iBAAE;YAAC;QAC/D,OAAO,IAAI,IAAI,IAAI,KAAK,aAAa;YACnC,MAAM,QAAe,EAAE;YACvB,IAAI,IAAI,OAAO,EAAE,MAAM,IAAI,CAAC;gBAAE,MAAM,IAAI,OAAO;YAAC;YAChD,IAAI,IAAI,SAAS,EAAE,QAAQ;gBACzB,KAAK,MAAM,MAAM,IAAI,SAAS,CAAE;oBAC9B,oEAAoE;oBACpE,8EAA8E;oBAC9E,MAAM,OAAY;wBAAE,cAAc;4BAAE,MAAM,GAAG,IAAI;4BAAE,MAAM,GAAG,IAAI;4BAAE,IAAI,GAAG,EAAE;wBAAC;oBAAE;oBAC9E,IAAI,GAAG,gBAAgB,EAAE,KAAK,gBAAgB,GAAG,GAAG,gBAAgB;oBACpE,MAAM,IAAI,CAAC;gBACb;YACF;YACA,IAAI,MAAM,MAAM,EAAE,SAAS,IAAI,CAAC;gBAAE,MAAM;gBAAS;YAAM;QACzD,OAAO,IAAI,IAAI,IAAI,KAAK,QAAQ;YAC9B,MAAM,OAAO;gBACX,kBAAkB;oBAChB,MAAM,IAAI,QAAQ,IAAI;oBACtB,IAAI,IAAI,UAAU;oBAClB,UAAU;wBAAE,SAAS,IAAI,OAAO;oBAAC;gBACnC;YACF;YACA,MAAM,OAAO,QAAQ,CAAC,SAAS,MAAM,GAAG,EAAE;YAC1C,IAAI,MAAM,SAAS,QAAQ;gBACzB,KAAK,KAAK,CAAC,IAAI,CAAC;YAClB,OAAO;gBACL,SAAS,IAAI,CAAC;oBAAE,MAAM;oBAAQ,OAAO;wBAAC;qBAAK;gBAAC;YAC9C;QACF;IACF;IACA,OAAO;AACT;AAEA,SAAS,kBAAkB,KAAuB;IAChD,IAAI,CAAC,MAAM,MAAM,EAAE,OAAO;IAC1B,OAAO;QAAC;YACN,sBAAsB,MAAM,GAAG,CAAC,CAAC,IAAM,CAAC;oBACtC,MAAM,EAAE,IAAI;oBACZ,aAAa,EAAE,WAAW;oBAC1B,YAAY,EAAE,UAAU;gBAC1B,CAAC;QACH;KAAE;AACJ;AAEA,SAAS,UAAU,IAalB;IACC,MAAM,OAAY;QAChB,UAAU,iBAAiB,KAAK,QAAQ;QACxC,mBAAmB;YAAE,OAAO;gBAAC;oBAAE,MAAM,KAAK,YAAY;gBAAC;aAAE;QAAC;QAC1D,kBAAkB;YAChB,aAAa,KAAK,WAAW,IAAI;YACjC,iBAAiB;YACjB,gBAAgB;gBAAE,iBAAiB,KAAK,eAAe,IAAI;YAAK;QAClE;IACF;IACA,MAAM,MAAM,kBAAkB,KAAK,KAAK,IAAI,EAAE;IAC9C,IAAI,KAAK,KAAK,KAAK,GAAG;IACtB,OAAO;AACT;AAMO,eAAe,eAAe,IAMpC;IAQC,MAAM,MAAM,GAAG,gBAAgB,QAAQ,EAAE,aAAa,qBAAqB,EAAE,gBAAgB;IAE7F,IAAI;IACJ,IAAI;QACF,MAAM,MAAM,MAAM,KAAK;YACrB,QAAQ;YACR,SAAS;gBAAE,gBAAgB;YAAmB;YAC9C,MAAM,KAAK,SAAS,CAAC,UAAU;QACjC;IACF,EAAE,OAAO,GAAG;QACV,OAAO;YACL,MAAM;YACN,UAAU;YACV,WAAW,EAAE;YACb,OAAO,CAAC,eAAe,EAAE,aAAa,QAAQ,EAAE,OAAO,GAAG,OAAO,IAAI;QACvE;IACF;IAEA,MAAM,OAAO,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,IAAM,CAAC,CAAC,CAAC;IAC7C,IAAI,CAAC,IAAI,EAAE,EAAE;QACX,MAAM,MAAM,MAAM,OAAO,WAAW,KAAK,SAAS,CAAC,MAAM,KAAK,CAAC,GAAG;QAClE,OAAO;YACL,MAAM;YACN,UAAU;YACV,WAAW,EAAE;YACb,OAAO,CAAC,iBAAiB,EAAE,IAAI,MAAM,CAAC,EAAE,EAAE,KAAK;QACjD;IACF;IAEA,MAAM,OAAO,MAAM,YAAY,CAAC,EAAE;IAClC,MAAM,QAAe,MAAM,SAAS,SAAS,EAAE;IAC/C,IAAI,OAAO;IACX,IAAI,WAAW;IACf,MAAM,YAAwB,EAAE;IAEhC,KAAK,MAAM,QAAQ,MAAO;QACxB,IAAI,KAAK,IAAI,EAAE;YACb,iEAAiE;YACjE,gEAAgE;YAChE,+DAA+D;YAC/D,6DAA6D;YAC7D,gDAAgD;YAChD,IAAI,KAAK,OAAO,EAAE,YAAY,KAAK,IAAI;iBAClC,QAAQ,KAAK,IAAI;QACxB;QACA,IAAI,KAAK,YAAY,EAAE;YACrB,UAAU,IAAI,CAAC;gBACb,IAAI,KAAK,YAAY,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,EAAE,KAAK,MAAM,GAAG,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI;gBACrF,MAAM,KAAK,YAAY,CAAC,IAAI;gBAC5B,MAAM,KAAK,YAAY,CAAC,IAAI,IAAI,CAAC;gBACjC,2EAA2E;gBAC3E,kBAAkB,KAAK,gBAAgB;YACzC;QACF;IACF;IAEA,OAAO;QAAE;QAAM;QAAU;QAAW,cAAc,MAAM;IAAa;AACvE;AAMO,gBAAgB,iBAAiB,IAKvC;IACC,MAAM,MAAM,GAAG,gBAAgB,QAAQ,EAAE,aAAa,2BAA2B,EAAE,eAAe,QAAQ,CAAC;IAE3G,IAAI;IACJ,IAAI;QACF,MAAM,MAAM,MAAM,KAAK;YACrB,QAAQ;YACR,SAAS;gBAAE,gBAAgB;YAAmB;YAC9C,MAAM,KAAK,SAAS,CAAC,UAAU;QACjC;IACF,EAAE,OAAO,GAAG;QACV,MAAM;YAAE,MAAM;YAAS,OAAO,CAAC,eAAe,EAAE,aAAa,QAAQ,EAAE,OAAO,GAAG,OAAO,IAAI;QAAC;QAC7F;IACF;IAEA,IAAI,CAAC,IAAI,EAAE,EAAE;QACX,MAAM,UAAU,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,IAAM;QAC7C,MAAM;YAAE,MAAM;YAAS,OAAO,CAAC,iBAAiB,EAAE,IAAI,MAAM,CAAC,EAAE,EAAE,QAAQ,KAAK,CAAC,GAAG,MAAM;QAAC;QACzF;IACF;IAEA,MAAM,SAAS,IAAI,IAAI,EAAE;IACzB,IAAI,CAAC,QAAQ;QAAE,MAAM;YAAE,MAAM;YAAS,OAAO;QAAmB;QAAG;IAAQ;IAE3E,MAAM,UAAU,IAAI;IACpB,IAAI,SAAS;IAEb,IAAI;QACF,MAAO,KAAM;YACX,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,IAAI;YACzC,IAAI,MAAM;YACV,UAAU,QAAQ,MAAM,CAAC,OAAO;gBAAE,QAAQ;YAAK;YAC/C,MAAM,QAAQ,OAAO,KAAK,CAAC;YAC3B,SAAS,MAAM,GAAG,MAAM;YAExB,KAAK,MAAM,QAAQ,MAAO;gBACxB,IAAI,CAAC,KAAK,UAAU,CAAC,WAAW;gBAChC,MAAM,OAAO,KAAK,KAAK,CAAC,GAAG,IAAI;gBAC/B,IAAI,CAAC,QAAQ,SAAS,UAAU;gBAChC,IAAI;gBACJ,IAAI;oBAAE,QAAQ,KAAK,KAAK,CAAC;gBAAO,EAAE,OAAM;oBAAE;gBAAU;gBACpD,MAAM,QAAQ,OAAO,YAAY,CAAC,EAAE,EAAE,SAAS,SAAS,EAAE;gBAC1D,KAAK,MAAM,QAAQ,MAAO;oBACxB,IAAI,KAAK,IAAI,EAAE;wBACb,MAAM,KAAK,OAAO,GACd;4BAAE,MAAM;4BAAY,MAAM,KAAK,IAAI;wBAAC,IACpC;4BAAE,MAAM;4BAAQ,MAAM,KAAK,IAAI;wBAAC;oBACtC;gBACF;YACF;QACF;IACF,SAAU;QACR,OAAO,WAAW;IACpB;IAEA,MAAM;QAAE,MAAM;IAAO;AACvB","debugId":null}}, +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__2b9e4ee1._.js:2732: "streamGeminiChat", +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__2b9e4ee1._.js:2733: ()=>streamGeminiChat +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__2b9e4ee1._.js:2894:async function* streamGeminiChat(opts) { +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__08533b7c._.js.map:14: {"offset": {"line": 2676, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/lib/ai/gemini-chat.ts"],"sourcesContent":["/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || '';\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || 'gemini-3.1-pro-preview';\nconst GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';\n\nexport interface ChatMessage {\n role: 'user' | 'assistant' | 'tool';\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: 'text' | 'thinking' | 'tool_call' | 'done' | 'error';\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n contents.push({ role: 'user', parts: [{ text: msg.content }] });\n } else if (msg.role === 'assistant') {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = { functionCall: { name: tc.name, args: tc.args, id: tc.id } };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: 'model', parts });\n } else if (msg.role === 'tool') {\n const part = {\n functionResponse: {\n name: msg.toolName || 'unknown',\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === 'user') {\n last.parts.push(part);\n } else {\n contents.push({ role: 'user', parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [{\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n }];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = '';\n let thoughts = '';\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id: part.functionCall.id || `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield { type: 'error', error: `Network error: ${e instanceof Error ? e.message : String(e)}` };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n yield { type: 'error', error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}` };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) { yield { type: 'error', error: 'No response body' }; return; }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (!data || data === '[DONE]') continue;\n let chunk: any;\n try { chunk = JSON.parse(data); } catch { continue; }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: 'thinking', text: part.text }\n : { type: 'text', text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: 'done' };\n}\n"],"names":[],"mappings":"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","debugId":null}}, +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__08533b7c._.js.map:16: {"offset": {"line": 4541, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/app/api/chat/route.ts"],"sourcesContent":["/**\n * POST /api/chat\n *\n * Streaming chat endpoint. Accepts a thread_id + user message,\n * loads history, calls Gemini 3.1 Pro, runs the tool loop,\n * persists messages, and streams SSE back to the client.\n *\n * SSE event shapes:\n * data: {\"type\":\"text\",\"text\":\"...\"}\n * data: {\"type\":\"thinking\",\"text\":\"...\"} // model's first-person reasoning\n * data: {\"type\":\"tool_start\",\"name\":\"...\",\"args\":{}}\n * data: {\"type\":\"tool_result\",\"name\":\"...\",\"result\":\"...\"}\n * data: {\"type\":\"aborted\"}\n * data: {\"type\":\"done\"}\n * data: {\"type\":\"error\",\"error\":\"...\"}\n */\nimport { NextResponse } from 'next/server';\nimport { authSession } from '@/lib/auth/session-server';\nimport { query } from '@/lib/db-postgres';\nimport { callGeminiChat, streamGeminiChat } from '@/lib/ai/gemini-chat';\nimport { VIBN_TOOL_DEFINITIONS, executeMcpTool } from '@/lib/ai/vibn-tools';\nimport type { ChatMessage, ToolCall } from '@/lib/ai/gemini-chat';\n\n// Bumped from 6 to 12 because Path B chains (devcontainer.ensure →\n// fs.read → fs.edit → kill → start → curl → logs) routinely fire 7-10\n// tool calls in one user turn. When the cap IS hit, we still emit a\n// narrative summary instead of leaving the user staring at a tool tray\n// (see the no-tools follow-up call below).\nconst MAX_TOOL_ROUNDS = 18;\n\nlet chatTablesReady = false;\nasync function ensureChatTables() {\n if (chatTablesReady) return;\n await query(`\n CREATE TABLE IF NOT EXISTS fs_chat_threads (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n user_id TEXT NOT NULL,\n workspace TEXT NOT NULL DEFAULT '',\n data JSONB NOT NULL DEFAULT '{}',\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n );\n CREATE INDEX IF NOT EXISTS fs_chat_threads_user_ws_idx\n ON fs_chat_threads (user_id, workspace, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS fs_chat_messages (\n id BIGSERIAL PRIMARY KEY,\n thread_id TEXT NOT NULL REFERENCES fs_chat_threads(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL,\n data JSONB NOT NULL DEFAULT '{}',\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n );\n CREATE INDEX IF NOT EXISTS fs_chat_messages_thread_idx\n ON fs_chat_messages (thread_id, created_at ASC);\n `, []);\n chatTablesReady = true;\n}\n\nexport function buildSystemPrompt(projects: any[], workspace: string): string {\n const projectsText = projects.length\n ? projects\n .map(\n (p: any) =>\n `- \"${p.productName || p.name}\" (id: ${p.id}, status: ${p.status || 'defining'})${p.productVision ? ': ' + p.productVision.slice(0, 120) : ''}`,\n )\n .join('\\n')\n : '(no projects yet)';\n\n return `You are Vibn AI — the technical co-founder of every Vibn user. You ship code, deploy infra, and treat their projects like they're your own.\n\nYou're talking to the owner of the \"${workspace}\" workspace. They have admin access to their Gitea org, a fleet of Coolify projects, and a persistent dev container per project. You can read and write any of it.\n\n## Voice — read this before you write a single response\n\nYou are NOT a tool-call orchestrator that narrates what it's about to do. You are an experienced engineer who has worked on hundreds of these projects and has a strong opinion about the right next move.\n\n- **Don't narrate intent before tool calls.** Skip \"Okay, I'll go ahead and read the file…\" — just read it. The user sees a tool tray; they don't need a play-by-play. Your reasoning is already streamed as a thinking pill.\n- **Pack the post-tool summary.** When a tool chain finishes, write 1-3 punchy sentences that say (a) what landed, (b) the most important specific result the user actually needs (URL, SHA, env value, error), and (c) the obvious next step if there is one. Don't bullet a recap of every tool you ran — they saw the tray.\n- **Have an opinion.** If they ask \"should I use Postgres or MongoDB?\" — pick one, justify in a sentence, and proceed. Don't list pros and cons unless they ask for that. Founders need decisions, not menus.\n- **Push back when it matters.** If they say \"deploy this to prod without backups,\" refuse and explain. If they ask for n8n when Pipedream would actually fit better, say so once and then defer to their call. Yes-machines build broken software.\n- **Surface adjacent risks unprompted.** If you just deployed something that's missing an env var, say so. If you wired a domain but DNS hasn't propagated, tell them how to verify. If the dev container is running but no autosave has happened in 30 min, mention it. You're protecting their work because they trust you to.\n- **Be honest about uncertainty.** \"I'm not 100% sure but my best guess is X — want me to verify with Y?\" beats false confidence every time. If a tool returned something weird, say it returned something weird.\n- **Length matches stakes.** A \"what time is it\" question gets one line. A \"should I move my whole user db to a different region\" question gets a paragraph plus the migration plan. Don't pad short answers and don't truncate hard ones.\n- **Use markdown sparingly.** Backticks for code, paths, IDs, and URLs always. Headings only when the response has 3+ distinct sections. Bullets for actually-parallel items (3+ steps, lists of options). Otherwise write prose.\n\n## How Vibn is structured\n- **Workspace** (\"${workspace}\") — the tenant boundary. One per user. Owns the Gitea org and a fleet of Coolify projects. You can ONLY see and touch resources in this workspace.\n- **Project** — an initiative the user is building (e.g. \"Twenty CRM\", \"My Blog\"). Each project has its OWN isolated Coolify project, so all its apps + databases + services are grouped together. A project has two facets that are part of ONE thing — never describe them as separate:\n - Planning side: name, vision/objectives, requirements (from \\`projects_get\\`)\n - Live side: deployed apps + services (from \\`projects_get → possibleDeployments[]\\` and \\`apps_list { projectId }\\`)\n\n## How to answer questions\n- \"What is project X?\" → \\`projects_get { id }\\`. The result includes both planning details and the linked deployments.\n- \"What's running / what has a domain?\" → \\`apps_list\\` (no args) for everything in the workspace, or \\`apps_list { projectId }\\` for one project.\n- \"Show me logs / containers / env\" → resolve the app uuid first via \\`apps_list\\`, then call \\`apps_logs\\` / \\`apps_containers_list\\` / \\`apps_envs_list\\`.\n- \"Find an open source X\" → \\`github_search\\` (always include \\`license:mit\\` unless the user says otherwise), then \\`github_file\\` to read READMEs / docker-compose.yml / design system entry points before recommending.\n- \"What's our docs say about Y?\" → \\`http_fetch\\` against the relevant URL.\n\n## How to deploy\n\n**Third-party app (Twenty CRM, n8n, Ghost, Supabase, Pocketbase, etc.)**\n1. \\`apps_templates_search { query }\\` — find the official one-click template.\n2. \\`apps_create { projectId, name, template, domain }\\` — deploy from template into the right project's Coolify namespace.\n3. Watch \\`apps_get { uuid }\\` for status; surface the live URL once \\`fqdn\\` is set.\n\n**Custom Docker image**\n1. \\`apps_create { projectId, name, dockerImage, domain, envsJson }\\`.\n2. \\`apps_deploy { uuid }\\` if it doesn't auto-deploy.\n\n**Database**\n1. \\`databases_create { projectId, name, type }\\` (type: postgres, mysql, redis, mongodb, mariadb, dragonfly, clickhouse, keydb).\n2. \\`databases_get { uuid }\\` returns the internal connection URL — inject it into the app via \\`apps_envs_set\\`.\n\n**Domain**\n1. \\`domains_search { query }\\` to check availability + price.\n2. \\`domains_register { domain }\\` to buy it (uses workspace billing).\n3. \\`apps_domains_set { uuid, domains }\\` to attach. DNS + Traefik are wired automatically.\n\n## Writing code (PREFERRED: dev container, shell-first)\n\nEach Vibn project has a persistent **dev container** (\\`vibn-dev\\`) running on Coolify. You write code by \\`shell_exec\\`-ing inside it and editing files with \\`fs_*\\` tools. This is dramatically faster than committing to Gitea and waiting for redeploys (sub-second feedback vs ~5 min).\n\n**Always start a coding session with**:\n1. \\`devcontainer_ensure { projectId }\\` — idempotent. First call ~10s (provisions a Coolify service); subsequent calls return immediately.\n\n**Then iterate with**:\n- \\`shell_exec { projectId, command }\\` — run anything: \\`ls\\`, \\`npm install\\`, \\`npm test\\`, \\`mise install\\` (installs Node/Python/Go/Rust on first use), \\`npx create-next-app .\\`, \\`git status\\`. Cwd defaults to \\`/workspace\\`.\n- \\`fs_read { projectId, path }\\` — inspect a file.\n- \\`fs_write { projectId, path, content }\\` — create or overwrite a file.\n- \\`fs_edit { projectId, path, oldString, newString }\\` — surgical search/replace. Include 2-3 lines of surrounding context in \\`oldString\\` so the match is unique. Fails fast if missing or non-unique.\n- \\`fs_glob\\` / \\`fs_grep\\` — find files by pattern, search code by regex (ripgrep, respects .gitignore).\n- \\`fs_list\\`, \\`fs_delete\\` — directory listing, delete.\n\n**Dev servers (preview URLs)**:\n- \\`dev_server_start { projectId, command, port }\\` — \\`port\\` MUST be in the range **3000-3009** (only 10 ports per project have pre-allocated Traefik routers). Pick 3000 for the primary app; use 3001-3009 only when the user is running multiple servers concurrently (e.g. frontend + API). The returned \\`previewUrl\\` is the public URL once DNS is wired.\n- \\`dev_server_stop { projectId, id }\\`, \\`dev_server_list { projectId }\\`, \\`dev_server_logs { projectId, id }\\`.\n- If \\`dev_server_start\\` returns \\`code: PORT_BUSY\\` → either stop the existing server first or pick another port in 3000-3009. Don't blindly retry the same port.\n\n**Framework-specific HMR setup** (so hot reload works through the preview URL once DNS is live — apply when scaffolding):\n- **Vite**: \\`server.host: '0.0.0.0'\\`, \\`server.hmr.clientPort: 443\\`, \\`server.hmr.protocol: 'wss'\\`. Vite's default localhost binding will appear to work but break HMR through Traefik.\n- **Next dev**: \\`next dev -p 3000 -H 0.0.0.0\\`. Next handles WSS HMR automatically through proxies.\n- **Express / plain Node**: bind \\`0.0.0.0\\` (we set \\`HOST=0.0.0.0\\` env automatically, but verify the framework respects it).\n\n**End-to-end recipe for \"build me X\"**:\n1. \\`devcontainer_ensure { projectId }\\`.\n2. \\`shell_exec { projectId, command: 'npx create-next-app@latest . --yes' }\\` (or whichever scaffold fits — search GitHub first if the user wants an OSS starting point).\n3. \\`shell_exec\\` to run \\`npm install\\`, then iterate with \\`fs_edit\\` / \\`fs_write\\` to customize.\n4. \\`shell_exec { command: 'npm run dev -- --port 3000' }\\` to verify locally (preview URLs land in week 2).\n5. When the user says \"ship it\" — for now, \\`shell_exec\\` a \\`git add . && git commit -m \"...\" && git push\\` to push to the Gitea repo, then \\`apps_create\\` to wire up the production deployment. (A dedicated \\`ship\\` tool lands soon.)\n\n**Rules**:\n- Stay under \\`/workspace\\`. The fs_* tools enforce this; for system paths use \\`shell_exec\\` deliberately.\n- The container has no route to internal Vibn services (vibn-postgres, etc.) by design.\n- If \\`shell_exec\\` returns non-zero, READ THE STDERR before re-running; don't loop blindly.\n\n## Gitea repo orchestration (one-time setup)\nFor creating new repos, branching, and listing what already exists:\n- \\`gitea_repos_list\\`, \\`gitea_repo_get\\`, \\`gitea_repo_create\\`.\n- \\`gitea_branches_list\\`, \\`gitea_branch_create\\`.\n\nFor all file editing inside an existing repo, ALWAYS use \\`fs_*\\` against the dev container. The \\`ship\\` tool will then push your changes to Gitea in one commit.\n\n## Troubleshooting\n- Deploy stuck or \"exited (1)\" → \\`apps_logs { uuid }\\` and \\`apps_containers_list { uuid }\\`. Common causes: missing env var, wrong port, image pull failure.\n- 502 / \"no available server\" → app probably has no public domain yet. Check \\`apps_get\\`; if \\`fqdn\\` is empty, attach a domain.\n- \"tenant\" / \"does not belong to\" errors → the uuid you passed isn't in this workspace. Re-list with \\`apps_list\\` to grab a valid one.\n- Compose stack acting weird → \\`apps_repair { uuid }\\` to re-apply post-deploy fixes (Traefik labels, port forwarding).\n- Need to nuke and re-deploy → \\`apps_delete { uuid, confirm }\\` (confirm must equal the app's exact name; fetch via \\`apps_get\\` first), then re-create.\n\n## Hard rules (non-negotiable)\n- ALWAYS pass \\`projectId\\` to \\`apps_create\\` and \\`databases_create\\`. If the user didn't say which project, infer from context (active project, last-mentioned, only one in workspace) — only ask if genuinely ambiguous.\n- 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.\n- 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\".\n- 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.\n- 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.\n- Don't loop blindly on tool errors. If \\`shell_exec\\` returns non-zero, READ THE STDERR, form a hypothesis, then act. If you can't diagnose in two attempts, surface what you tried and ask the user.\n\n## Current workspace projects\n${projectsText}\n\nToday's date: ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}.`;\n}\n\nexport async function POST(request: Request) {\n await ensureChatTables();\n\n const session = await authSession();\n if (!session?.user?.email) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n }\n\n let body: { thread_id: string; message: string; workspace: string; mcp_token?: string };\n try {\n body = await request.json();\n } catch {\n return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });\n }\n\n const { thread_id, message, workspace, mcp_token } = body;\n if (!thread_id || !message?.trim()) {\n return NextResponse.json({ error: 'thread_id and message are required' }, { status: 400 });\n }\n\n const email = session.user.email;\n\n // Verify thread belongs to user\n const threads = await query(\n `SELECT id FROM fs_chat_threads WHERE id = $1 AND user_id = $2`,\n [thread_id, email],\n );\n if (!threads.length) {\n return NextResponse.json({ error: 'Thread not found' }, { status: 404 });\n }\n\n // Load message history (last 40 messages)\n const rows = await query(\n `SELECT data FROM fs_chat_messages WHERE thread_id = $1 ORDER BY created_at DESC LIMIT 40`,\n [thread_id],\n );\n const history: ChatMessage[] = rows.reverse().map((r: any) => r.data);\n\n // Add user message\n const userMsg: ChatMessage = { role: 'user', content: message.trim() };\n history.push(userMsg);\n await query(\n `INSERT INTO fs_chat_messages (thread_id, user_id, data) VALUES ($1, $2, $3)`,\n [thread_id, email, JSON.stringify(userMsg)],\n );\n\n // Update thread updatedAt\n await query(\n `UPDATE fs_chat_threads SET updated_at = NOW(), data = data || $2 WHERE id = $1`,\n [thread_id, JSON.stringify({ updatedAt: new Date().toISOString() })],\n );\n\n // Load projects for system prompt context\n const projectRows = await query(\n `SELECT p.data FROM fs_projects p\n JOIN fs_users u ON u.id = p.user_id\n WHERE u.data->>'email' = $1\n ORDER BY (p.data->>'updatedAt') DESC NULLS LAST LIMIT 20`,\n [email],\n );\n const projects = projectRows.map((r: any) => r.data);\n const systemPrompt = buildSystemPrompt(projects, workspace);\n\n // Base URL for internal MCP calls\n const host = request.headers.get('host') || 'vibnai.com';\n const proto = host.startsWith('localhost') ? 'http' : 'https';\n const baseUrl = `${proto}://${host}`;\n\n // Honor client-side abort (Stop button). When the user clicks Stop\n // the browser's AbortController fires `request.signal.aborted` and\n // the fetch stream is closed; we use it as a polite checkpoint\n // between rounds and tool calls so we (a) don't keep paying Gemini\n // for tokens the user no longer wants and (b) persist whatever the\n // assistant produced before the cancel.\n const clientSignal = request.signal;\n\n // Stream response\n const encoder = new TextEncoder();\n const stream = new ReadableStream({\n async start(controller) {\n let streamClosed = false;\n function emit(chunk: object) {\n if (streamClosed) return;\n try {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\\n\\n`));\n } catch {\n // controller may have been closed by the abort handler\n streamClosed = true;\n }\n }\n function safeClose() {\n if (streamClosed) return;\n streamClosed = true;\n try {\n controller.close();\n } catch {}\n }\n\n let messages = [...history];\n let round = 0;\n let assistantText = '';\n const assistantToolCalls: ToolCall[] = [];\n let aborted = clientSignal.aborted;\n const onAbort = () => {\n aborted = true;\n };\n clientSignal.addEventListener('abort', onAbort);\n\n try {\n // Tool-calling loop: use non-streaming so thought_signature is\n // always present in the complete response (required by thinking models).\n while (round < MAX_TOOL_ROUNDS) {\n if (aborted) break;\n round++;\n\n const toolDefs = mcp_token ? VIBN_TOOL_DEFINITIONS : [];\n const resp = await callGeminiChat({ systemPrompt, messages, tools: toolDefs, temperature: 0.7 });\n\n if (resp.error) {\n emit({ type: 'error', error: resp.error });\n controller.close();\n return;\n }\n\n // Stream user-facing text to client\n if (resp.text) {\n assistantText += resp.text;\n emit({ type: 'text', text: resp.text });\n }\n\n // Stream the model's reasoning narration as a separate SSE\n // event type. We pay for thinking tokens whether or not we\n // ask for them, so making them visible is free transparency\n // — and it cures the \"tool tray with no narrative\" feel.\n if (resp.thoughts) {\n emit({ type: 'thinking', text: resp.thoughts });\n }\n\n // Announce tool calls\n for (const tc of resp.toolCalls) {\n assistantToolCalls.push(tc);\n emit({ type: 'tool_start', name: tc.name, args: tc.args });\n }\n\n // Save assistant turn\n messages.push({\n role: 'assistant',\n content: resp.text,\n toolCalls: resp.toolCalls.length ? resp.toolCalls : undefined,\n });\n\n if (!resp.toolCalls.length) break;\n if (aborted) break;\n\n // Execute tool calls and add results\n for (const tc of resp.toolCalls) {\n if (aborted) break;\n const result = mcp_token\n ? await executeMcpTool(tc.name, tc.args, mcp_token, baseUrl)\n : JSON.stringify({ error: 'No MCP token — read-only mode.' });\n\n emit({ type: 'tool_result', name: tc.name, result: result.slice(0, 500) });\n\n messages.push({\n role: 'tool',\n content: result,\n toolCallId: tc.id,\n toolName: tc.name,\n thoughtSignature: tc.thoughtSignature,\n });\n }\n }\n\n // If the user clicked Stop, surface the cancel marker so the\n // client renders \"(stopped by user)\" inline with the partial\n // assistant message, then skip the round-cap recovery summary\n // (we shouldn't pay Gemini for a turn the user just canceled).\n if (aborted) {\n const stopMarker = assistantText\n ? '\\n\\n_(stopped by user)_'\n : '_(stopped by user before any response)_';\n assistantText += stopMarker;\n emit({ type: 'text', text: stopMarker });\n emit({ type: 'aborted' });\n }\n\n // If the loop exited because we hit MAX_TOOL_ROUNDS while the\n // model still wanted to call tools, the user has only seen a\n // tray of ✓ icons with no narrative. Force one final no-tools\n // call so we always end on a human-readable summary.\n const lastTurnHadTools =\n messages.length > 0 &&\n messages[messages.length - 1].role === 'tool';\n if (!aborted && round >= MAX_TOOL_ROUNDS && lastTurnHadTools) {\n try {\n const summary = await callGeminiChat({\n systemPrompt:\n systemPrompt +\n '\\n\\nYou have just executed a chain of tool calls. Summarize the result for the user in 1-3 sentences. Do NOT call any more tools.',\n messages,\n tools: [],\n temperature: 0.3,\n });\n if (summary.text) {\n assistantText += summary.text;\n emit({ type: 'text', text: summary.text });\n }\n if (summary.thoughts) {\n emit({ type: 'thinking', text: summary.thoughts });\n }\n } catch {\n // Don't let a failed summary kill the stream.\n }\n }\n\n // Persist final assistant message\n const finalMsg: ChatMessage = {\n role: 'assistant',\n content: assistantText,\n toolCalls: assistantToolCalls.length ? assistantToolCalls : undefined,\n };\n await query(\n `INSERT INTO fs_chat_messages (thread_id, user_id, data) VALUES ($1, $2, $3)`,\n [thread_id, email, JSON.stringify(finalMsg)],\n );\n\n emit({ type: 'done' });\n safeClose();\n } catch (e) {\n // AbortError is the expected shape when the client cancels\n // mid-Gemini-call — don't surface it as a real error.\n const isAbort =\n aborted ||\n (e instanceof Error && (e.name === 'AbortError' || /aborted/i.test(e.message)));\n if (!isAbort) {\n emit({ type: 'error', error: e instanceof Error ? e.message : String(e) });\n } else {\n emit({ type: 'aborted' });\n }\n safeClose();\n } finally {\n clientSignal.removeEventListener('abort', onAbort);\n }\n },\n cancel() {\n // Browser disconnected (tab closed, navigated away). Nothing to\n // do — the abort handler above already flipped the flag and the\n // loop will bail at the next checkpoint.\n },\n });\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n });\n}\n"],"names":[],"mappings":"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","debugId":null}}] +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__70c85bc2._.js.map:28: {"offset": {"line": 5117, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/lib/ai/gemini-chat.ts"],"sourcesContent":["/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || \"\";\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || \"gemini-3.1-pro-preview\";\nconst GEMINI_BASE_URL = \"https://generativelanguage.googleapis.com/v1beta\";\n\nexport interface ChatMessage {\n role: \"user\" | \"assistant\" | \"tool\";\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: \"text\" | \"thinking\" | \"tool_call\" | \"done\" | \"error\";\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === \"user\") {\n contents.push({ role: \"user\", parts: [{ text: msg.content }] });\n } else if (msg.role === \"assistant\") {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = {\n functionCall: { name: tc.name, args: tc.args, id: tc.id },\n };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: \"model\", parts });\n } else if (msg.role === \"tool\") {\n const part = {\n functionResponse: {\n name: msg.toolName || \"unknown\",\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === \"user\") {\n last.parts.push(part);\n } else {\n contents.push({ role: \"user\", parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [\n {\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n },\n ];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = \"\";\n let thoughts = \"\";\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id:\n part.functionCall.id ||\n `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield {\n type: \"error\",\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => \"\");\n yield {\n type: \"error\",\n error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}`,\n };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) {\n yield { type: \"error\", error: \"No response body\" };\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n if (!line.startsWith(\"data: \")) continue;\n const data = line.slice(6).trim();\n if (!data || data === \"[DONE]\") continue;\n let chunk: any;\n try {\n chunk = JSON.parse(data);\n } catch {\n continue;\n }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: \"thinking\", text: part.text }\n : { type: \"text\", text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: \"done\" };\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;CASC;;;;;;AAED,MAAM,iBAAiB,QAAQ,GAAG,CAAC,cAAc,IAAI;AACrD,MAAM,eAAe,QAAQ,GAAG,CAAC,eAAe,IAAI;AACpD,MAAM,kBAAkB;AAgCxB,4DAA4D,GAC5D,SAAS,iBAAiB,QAAuB;IAC/C,MAAM,WAAkB,EAAE;IAE1B,KAAK,MAAM,OAAO,SAAU;QAC1B,IAAI,IAAI,IAAI,KAAK,QAAQ;YACvB,SAAS,IAAI,CAAC;gBAAE,MAAM;gBAAQ,OAAO;oBAAC;wBAAE,MAAM,IAAI,OAAO;oBAAC;iBAAE;YAAC;QAC/D,OAAO,IAAI,IAAI,IAAI,KAAK,aAAa;YACnC,MAAM,QAAe,EAAE;YACvB,IAAI,IAAI,OAAO,EAAE,MAAM,IAAI,CAAC;gBAAE,MAAM,IAAI,OAAO;YAAC;YAChD,IAAI,IAAI,SAAS,EAAE,QAAQ;gBACzB,KAAK,MAAM,MAAM,IAAI,SAAS,CAAE;oBAC9B,oEAAoE;oBACpE,8EAA8E;oBAC9E,MAAM,OAAY;wBAChB,cAAc;4BAAE,MAAM,GAAG,IAAI;4BAAE,MAAM,GAAG,IAAI;4BAAE,IAAI,GAAG,EAAE;wBAAC;oBAC1D;oBACA,IAAI,GAAG,gBAAgB,EAAE,KAAK,gBAAgB,GAAG,GAAG,gBAAgB;oBACpE,MAAM,IAAI,CAAC;gBACb;YACF;YACA,IAAI,MAAM,MAAM,EAAE,SAAS,IAAI,CAAC;gBAAE,MAAM;gBAAS;YAAM;QACzD,OAAO,IAAI,IAAI,IAAI,KAAK,QAAQ;YAC9B,MAAM,OAAO;gBACX,kBAAkB;oBAChB,MAAM,IAAI,QAAQ,IAAI;oBACtB,IAAI,IAAI,UAAU;oBAClB,UAAU;wBAAE,SAAS,IAAI,OAAO;oBAAC;gBACnC;YACF;YACA,MAAM,OAAO,QAAQ,CAAC,SAAS,MAAM,GAAG,EAAE;YAC1C,IAAI,MAAM,SAAS,QAAQ;gBACzB,KAAK,KAAK,CAAC,IAAI,CAAC;YAClB,OAAO;gBACL,SAAS,IAAI,CAAC;oBAAE,MAAM;oBAAQ,OAAO;wBAAC;qBAAK;gBAAC;YAC9C;QACF;IACF;IACA,OAAO;AACT;AAEA,SAAS,kBAAkB,KAAuB;IAChD,IAAI,CAAC,MAAM,MAAM,EAAE,OAAO;IAC1B,OAAO;QACL;YACE,sBAAsB,MAAM,GAAG,CAAC,CAAC,IAAM,CAAC;oBACtC,MAAM,EAAE,IAAI;oBACZ,aAAa,EAAE,WAAW;oBAC1B,YAAY,EAAE,UAAU;gBAC1B,CAAC;QACH;KACD;AACH;AAEA,SAAS,UAAU,IAalB;IACC,MAAM,OAAY;QAChB,UAAU,iBAAiB,KAAK,QAAQ;QACxC,mBAAmB;YAAE,OAAO;gBAAC;oBAAE,MAAM,KAAK,YAAY;gBAAC;aAAE;QAAC;QAC1D,kBAAkB;YAChB,aAAa,KAAK,WAAW,IAAI;YACjC,iBAAiB;YACjB,gBAAgB;gBAAE,iBAAiB,KAAK,eAAe,IAAI;YAAK;QAClE;IACF;IACA,MAAM,MAAM,kBAAkB,KAAK,KAAK,IAAI,EAAE;IAC9C,IAAI,KAAK,KAAK,KAAK,GAAG;IACtB,OAAO;AACT;AAMO,eAAe,eAAe,IAMpC;IAQC,MAAM,MAAM,GAAG,gBAAgB,QAAQ,EAAE,aAAa,qBAAqB,EAAE,gBAAgB;IAE7F,IAAI;IACJ,IAAI;QACF,MAAM,MAAM,MAAM,KAAK;YACrB,QAAQ;YACR,SAAS;gBAAE,gBAAgB;YAAmB;YAC9C,MAAM,KAAK,SAAS,CAAC,UAAU;QACjC;IACF,EAAE,OAAO,GAAG;QACV,OAAO;YACL,MAAM;YACN,UAAU;YACV,WAAW,EAAE;YACb,OAAO,CAAC,eAAe,EAAE,aAAa,QAAQ,EAAE,OAAO,GAAG,OAAO,IAAI;QACvE;IACF;IAEA,MAAM,OAAO,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,IAAM,CAAC,CAAC,CAAC;IAC7C,IAAI,CAAC,IAAI,EAAE,EAAE;QACX,MAAM,MAAM,MAAM,OAAO,WAAW,KAAK,SAAS,CAAC,MAAM,KAAK,CAAC,GAAG;QAClE,OAAO;YACL,MAAM;YACN,UAAU;YACV,WAAW,EAAE;YACb,OAAO,CAAC,iBAAiB,EAAE,IAAI,MAAM,CAAC,EAAE,EAAE,KAAK;QACjD;IACF;IAEA,MAAM,OAAO,MAAM,YAAY,CAAC,EAAE;IAClC,MAAM,QAAe,MAAM,SAAS,SAAS,EAAE;IAC/C,IAAI,OAAO;IACX,IAAI,WAAW;IACf,MAAM,YAAwB,EAAE;IAEhC,KAAK,MAAM,QAAQ,MAAO;QACxB,IAAI,KAAK,IAAI,EAAE;YACb,iEAAiE;YACjE,gEAAgE;YAChE,+DAA+D;YAC/D,6DAA6D;YAC7D,gDAAgD;YAChD,IAAI,KAAK,OAAO,EAAE,YAAY,KAAK,IAAI;iBAClC,QAAQ,KAAK,IAAI;QACxB;QACA,IAAI,KAAK,YAAY,EAAE;YACrB,UAAU,IAAI,CAAC;gBACb,IACE,KAAK,YAAY,CAAC,EAAE,IACpB,CAAC,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,EAAE,KAAK,MAAM,GAAG,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI;gBAC3D,MAAM,KAAK,YAAY,CAAC,IAAI;gBAC5B,MAAM,KAAK,YAAY,CAAC,IAAI,IAAI,CAAC;gBACjC,2EAA2E;gBAC3E,kBAAkB,KAAK,gBAAgB;YACzC;QACF;IACF;IAEA,OAAO;QAAE;QAAM;QAAU;QAAW,cAAc,MAAM;IAAa;AACvE;AAMO,gBAAgB,iBAAiB,IAKvC;IACC,MAAM,MAAM,GAAG,gBAAgB,QAAQ,EAAE,aAAa,2BAA2B,EAAE,eAAe,QAAQ,CAAC;IAE3G,IAAI;IACJ,IAAI;QACF,MAAM,MAAM,MAAM,KAAK;YACrB,QAAQ;YACR,SAAS;gBAAE,gBAAgB;YAAmB;YAC9C,MAAM,KAAK,SAAS,CAAC,UAAU;QACjC;IACF,EAAE,OAAO,GAAG;QACV,MAAM;YACJ,MAAM;YACN,OAAO,CAAC,eAAe,EAAE,aAAa,QAAQ,EAAE,OAAO,GAAG,OAAO,IAAI;QACvE;QACA;IACF;IAEA,IAAI,CAAC,IAAI,EAAE,EAAE;QACX,MAAM,UAAU,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,IAAM;QAC7C,MAAM;YACJ,MAAM;YACN,OAAO,CAAC,iBAAiB,EAAE,IAAI,MAAM,CAAC,EAAE,EAAE,QAAQ,KAAK,CAAC,GAAG,MAAM;QACnE;QACA;IACF;IAEA,MAAM,SAAS,IAAI,IAAI,EAAE;IACzB,IAAI,CAAC,QAAQ;QACX,MAAM;YAAE,MAAM;YAAS,OAAO;QAAmB;QACjD;IACF;IAEA,MAAM,UAAU,IAAI;IACpB,IAAI,SAAS;IAEb,IAAI;QACF,MAAO,KAAM;YACX,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,IAAI;YACzC,IAAI,MAAM;YACV,UAAU,QAAQ,MAAM,CAAC,OAAO;gBAAE,QAAQ;YAAK;YAC/C,MAAM,QAAQ,OAAO,KAAK,CAAC;YAC3B,SAAS,MAAM,GAAG,MAAM;YAExB,KAAK,MAAM,QAAQ,MAAO;gBACxB,IAAI,CAAC,KAAK,UAAU,CAAC,WAAW;gBAChC,MAAM,OAAO,KAAK,KAAK,CAAC,GAAG,IAAI;gBAC/B,IAAI,CAAC,QAAQ,SAAS,UAAU;gBAChC,IAAI;gBACJ,IAAI;oBACF,QAAQ,KAAK,KAAK,CAAC;gBACrB,EAAE,OAAM;oBACN;gBACF;gBACA,MAAM,QAAQ,OAAO,YAAY,CAAC,EAAE,EAAE,SAAS,SAAS,EAAE;gBAC1D,KAAK,MAAM,QAAQ,MAAO;oBACxB,IAAI,KAAK,IAAI,EAAE;wBACb,MAAM,KAAK,OAAO,GACd;4BAAE,MAAM;4BAAY,MAAM,KAAK,IAAI;wBAAC,IACpC;4BAAE,MAAM;4BAAQ,MAAM,KAAK,IAAI;wBAAC;oBACtC;gBACF;YACF;QACF;IACF,SAAU;QACR,OAAO,WAAW;IACpB;IAEA,MAAM;QAAE,MAAM;IAAO;AACvB","debugId":null}}, +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__88db9aed._.js:2737: "streamGeminiChat", +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__88db9aed._.js:2738: ()=>streamGeminiChat +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__88db9aed._.js:2899:async function* streamGeminiChat(opts) { +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__47ad00d5._.js.map:15: {"offset": {"line": 2719, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/lib/ai/gemini-chat.ts"],"sourcesContent":["/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || '';\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || 'gemini-3.1-pro-preview';\nconst GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';\n\nexport interface ChatMessage {\n role: 'user' | 'assistant' | 'tool';\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: 'text' | 'thinking' | 'tool_call' | 'done' | 'error';\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n contents.push({ role: 'user', parts: [{ text: msg.content }] });\n } else if (msg.role === 'assistant') {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = { functionCall: { name: tc.name, args: tc.args, id: tc.id } };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: 'model', parts });\n } else if (msg.role === 'tool') {\n const part = {\n functionResponse: {\n name: msg.toolName || 'unknown',\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === 'user') {\n last.parts.push(part);\n } else {\n contents.push({ role: 'user', parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [{\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n }];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = '';\n let thoughts = '';\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id: part.functionCall.id || `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield { type: 'error', error: `Network error: ${e instanceof Error ? e.message : String(e)}` };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n yield { type: 'error', error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}` };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) { yield { type: 'error', error: 'No response body' }; return; }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (!data || data === '[DONE]') continue;\n let chunk: any;\n try { chunk = JSON.parse(data); } catch { continue; }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: 'thinking', text: part.text }\n : { type: 'text', text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: 'done' };\n}\n"],"names":[],"mappings":"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","debugId":null}}, +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__574f5573._.js.map:15: {"offset": {"line": 2738, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/lib/ai/gemini-chat.ts"],"sourcesContent":["/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || \"\";\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || \"gemini-3.1-pro-preview\";\nconst GEMINI_BASE_URL = \"https://generativelanguage.googleapis.com/v1beta\";\n\nexport interface ChatMessage {\n role: \"user\" | \"assistant\" | \"tool\";\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: \"text\" | \"thinking\" | \"tool_call\" | \"done\" | \"error\";\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === \"user\") {\n contents.push({ role: \"user\", parts: [{ text: msg.content }] });\n } else if (msg.role === \"assistant\") {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = {\n functionCall: { name: tc.name, args: tc.args, id: tc.id },\n };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: \"model\", parts });\n } else if (msg.role === \"tool\") {\n const part = {\n functionResponse: {\n name: msg.toolName || \"unknown\",\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === \"user\") {\n last.parts.push(part);\n } else {\n contents.push({ role: \"user\", parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [\n {\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n },\n ];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: \"\",\n thoughts: \"\",\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = \"\";\n let thoughts = \"\";\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id:\n part.functionCall.id ||\n `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield {\n type: \"error\",\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => \"\");\n yield {\n type: \"error\",\n error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}`,\n };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) {\n yield { type: \"error\", error: \"No response body\" };\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n if (!line.startsWith(\"data: \")) continue;\n const data = line.slice(6).trim();\n if (!data || data === \"[DONE]\") continue;\n let chunk: any;\n try {\n chunk = JSON.parse(data);\n } catch {\n continue;\n }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: \"thinking\", text: part.text }\n : { type: \"text\", text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: \"done\" };\n}\n"],"names":[],"mappings":"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","debugId":null}}, +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__574f5573._.js:2751: "streamGeminiChat", +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__574f5573._.js:2752: ()=>streamGeminiChat +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__574f5573._.js:2913:async function* streamGeminiChat(opts) { +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__08533b7c._.js:2689: "streamGeminiChat", +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__08533b7c._.js:2690: ()=>streamGeminiChat +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__08533b7c._.js:2851:async function* streamGeminiChat(opts) { +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__47ad00d5._.js:2732: "streamGeminiChat", +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__47ad00d5._.js:2733: ()=>streamGeminiChat +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__47ad00d5._.js:2894:async function* streamGeminiChat(opts) { +vibn-frontend/.next/dev/server/chunks/[root-of-the-server]__2b9e4ee1._.js.map:15: {"offset": {"line": 2719, "column": 0}, "map": {"version":3,"sources":["file:///Users/markhenderson/master-ai/vibn-frontend/lib/ai/gemini-chat.ts"],"sourcesContent":["/**\n * Gemini 3.1 Pro chat client with tool-calling support.\n *\n * Architecture:\n * - Tool-calling rounds use generateContent (non-streaming) so we always\n * get the complete response including thought_signature. Thinking models\n * (2.5+, 3.x) require this field to be echoed back in functionResponse\n * and it is not reliably present in individual SSE chunks.\n * - Final text-only response uses streamGenerateContent for good UX.\n */\n\nconst GEMINI_API_KEY = process.env.GOOGLE_API_KEY || '';\nconst GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || 'gemini-3.1-pro-preview';\nconst GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';\n\nexport interface ChatMessage {\n role: 'user' | 'assistant' | 'tool';\n content: string;\n toolCalls?: ToolCall[];\n toolCallId?: string;\n toolName?: string;\n thoughtSignature?: string;\n}\n\nexport interface ToolCall {\n id: string;\n name: string;\n args: Record;\n /** Must be echoed back in functionResponse for Gemini thinking models */\n thoughtSignature?: string;\n}\n\nexport interface ToolDefinition {\n name: string;\n description: string;\n parameters: Record;\n}\n\nexport interface ChatChunk {\n type: 'text' | 'thinking' | 'tool_call' | 'done' | 'error';\n text?: string;\n toolCall?: ToolCall;\n error?: string;\n}\n\n/** Convert our ChatMessage[] to Gemini's contents[] format */\nfunction toGeminiContents(messages: ChatMessage[]) {\n const contents: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n contents.push({ role: 'user', parts: [{ text: msg.content }] });\n } else if (msg.role === 'assistant') {\n const parts: any[] = [];\n if (msg.content) parts.push({ text: msg.content });\n if (msg.toolCalls?.length) {\n for (const tc of msg.toolCalls) {\n // thoughtSignature is a SIBLING of functionCall in the part object,\n // not nested inside it. See: ai.google.dev/gemini-api/docs/thought-signatures\n const part: any = { functionCall: { name: tc.name, args: tc.args, id: tc.id } };\n if (tc.thoughtSignature) part.thoughtSignature = tc.thoughtSignature;\n parts.push(part);\n }\n }\n if (parts.length) contents.push({ role: 'model', parts });\n } else if (msg.role === 'tool') {\n const part = {\n functionResponse: {\n name: msg.toolName || 'unknown',\n id: msg.toolCallId,\n response: { content: msg.content },\n },\n };\n const last = contents[contents.length - 1];\n if (last?.role === 'user') {\n last.parts.push(part);\n } else {\n contents.push({ role: 'user', parts: [part] });\n }\n }\n }\n return contents;\n}\n\nfunction toGeminiFunctions(tools: ToolDefinition[]) {\n if (!tools.length) return undefined;\n return [{\n functionDeclarations: tools.map((t) => ({\n name: t.name,\n description: t.description,\n parameters: t.parameters,\n })),\n }];\n}\n\nfunction buildBody(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n /**\n * Ask Gemini to return its thought summaries as parts marked\n * `thought: true`. We pay for thinking tokens regardless; this just\n * makes them visible so the UI can show \"Reading server.js…\",\n * \"Shipping to production…\" between tool calls instead of leaving\n * the user staring at a silent tool tray. Defaults to true.\n */\n includeThoughts?: boolean;\n}) {\n const body: any = {\n contents: toGeminiContents(opts.messages),\n systemInstruction: { parts: [{ text: opts.systemPrompt }] },\n generationConfig: {\n temperature: opts.temperature ?? 0.7,\n maxOutputTokens: 8192,\n thinkingConfig: { includeThoughts: opts.includeThoughts ?? true },\n },\n };\n const fns = toGeminiFunctions(opts.tools ?? []);\n if (fns) body.tools = fns;\n return body;\n}\n\n/**\n * Non-streaming call — used for tool-calling rounds.\n * Returns complete response with thought_signature guaranteed.\n */\nexport async function callGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n includeThoughts?: boolean;\n}): Promise<{\n text: string;\n /** First-person reasoning narration; meant for a \"thinking\" UI panel, not the main bubble. */\n thoughts: string;\n toolCalls: ToolCall[];\n finishReason?: string;\n error?: string;\n}> {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Network error: ${e instanceof Error ? e.message : String(e)}`,\n };\n }\n\n const data = await res.json().catch(() => ({}));\n if (!res.ok) {\n const msg = data?.error?.message || JSON.stringify(data).slice(0, 200);\n return {\n text: '',\n thoughts: '',\n toolCalls: [],\n error: `Gemini API error ${res.status}: ${msg}`,\n };\n }\n\n const cand = data?.candidates?.[0];\n const parts: any[] = cand?.content?.parts ?? [];\n let text = '';\n let thoughts = '';\n const toolCalls: ToolCall[] = [];\n\n for (const part of parts) {\n if (part.text) {\n // CRITICAL: Gemini tags reasoning parts with `thought: true`. If\n // we lump them into `text` they leak into the chat bubble as if\n // they were prose for the user — which is the opposite of what\n // the user wants. Keep them in their own bucket so the route\n // can stream them as a separate SSE event type.\n if (part.thought) thoughts += part.text;\n else text += part.text;\n }\n if (part.functionCall) {\n toolCalls.push({\n id: part.functionCall.id || `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n name: part.functionCall.name,\n args: part.functionCall.args ?? {},\n // thoughtSignature is a SIBLING of functionCall in the part, not inside it\n thoughtSignature: part.thoughtSignature,\n });\n }\n }\n\n return { text, thoughts, toolCalls, finishReason: cand?.finishReason };\n}\n\n/**\n * Streaming call — used for the final text-only response.\n * Yields ChatChunk objects.\n */\nexport async function* streamGeminiChat(opts: {\n systemPrompt: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n temperature?: number;\n}): AsyncGenerator {\n const url = `${GEMINI_BASE_URL}/models/${GEMINI_MODEL}:streamGenerateContent?key=${GEMINI_API_KEY}&alt=sse`;\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(buildBody(opts)),\n });\n } catch (e) {\n yield { type: 'error', error: `Network error: ${e instanceof Error ? e.message : String(e)}` };\n return;\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '');\n yield { type: 'error', error: `Gemini API error ${res.status}: ${errText.slice(0, 300)}` };\n return;\n }\n\n const reader = res.body?.getReader();\n if (!reader) { yield { type: 'error', error: 'No response body' }; return; }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const data = line.slice(6).trim();\n if (!data || data === '[DONE]') continue;\n let chunk: any;\n try { chunk = JSON.parse(data); } catch { continue; }\n const parts = chunk?.candidates?.[0]?.content?.parts ?? [];\n for (const part of parts) {\n if (part.text) {\n yield part.thought\n ? { type: 'thinking', text: part.text }\n : { type: 'text', text: part.text };\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n\n yield { type: 'done' };\n}\n"],"names":[],"mappings":"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","debugId":null}}, +vibn-frontend/## User:590:### async function* streamGeminiChat( ) › const url › L216-226 +vibn-frontend/lib/ai/gemini-chat.ts:211:export async function* streamGeminiChat(opts: { diff --git a/latest_deploy.txt b/latest_deploy.txt new file mode 100644 index 0000000..2f27266 --- /dev/null +++ b/latest_deploy.txt @@ -0,0 +1 @@ +[{"command":null,"output":"Docker 27.0.3 with BuildKit and Buildx detected on deployment server (localhost).","type":"stdout","timestamp":"2026-05-17T20:48:41.186732Z","hidden":false,"batch":1},{"command":null,"output":"Starting deployment of https:\/\/git.vibnai.com\/mark\/vibn-frontend.git:main to localhost.","type":"stdout","timestamp":"2026-05-17T20:48:41.236729Z","hidden":false,"batch":1,"order":2}] diff --git a/new-site/Onboarding.bundle.html b/new-site/Onboarding.bundle.html new file mode 100644 index 0000000..ccc30d9 --- /dev/null +++ b/new-site/Onboarding.bundle.html @@ -0,0 +1,45 @@ + + + + + + Vibn — Onboarding + + + + + + + + + + + + + +
+ + + + + + + + + + diff --git a/new-site/Onboarding.html b/new-site/Onboarding.html new file mode 100644 index 0000000..dd20e54 --- /dev/null +++ b/new-site/Onboarding.html @@ -0,0 +1,28 @@ + + + + + + Vibn — Onboarding + + + + + + + + + + + +
+ + + + + + + + + + diff --git a/new-site/onboarding-app.jsx b/new-site/onboarding-app.jsx new file mode 100644 index 0000000..83ba164 --- /dev/null +++ b/new-site/onboarding-app.jsx @@ -0,0 +1,181 @@ +// Root onboarding app — owns the route state and the answers dict. +// Routes: fork → → build → ready. A floating debug navigator (toggle +// in the lower-right) lets reviewers jump between any screen without +// filling out the form. + +function OnboardingApp() { + const initialName = React.useMemo(() => { + try { return localStorage.getItem("vibn:firstName") || ""; } catch { return ""; } + }, []); + + const [stage, setStage] = React.useState("fork"); // fork | path | build | ready + const [path, setPath] = React.useState(null); // entrepreneur | owner | consultant + const [forkChoice, setForkChoice] = React.useState(null); + const [step, setStep] = React.useState(0); + const [data, setData] = React.useState({}); + + const [debugOpen, setDebugOpen] = React.useState(false); + + const update = (patch) => setData((d) => ({ ...d, ...patch })); + + // ── transitions ────────────────────────────────────────────────────── + const confirmFork = () => { + if (!forkChoice) return; + setPath(forkChoice); + setStep(0); + setStage("path"); + }; + const backToFork = () => { setStage("fork"); setStep(0); }; + const completePath = () => setStage("build"); + const openWorkspace = () => setStage("ready"); + const close = () => { window.location.href = "index.html"; }; + const openChat = () => { window.location.href = "index.html"; }; + + // ⌘↵ advances on whatever the current primary action is + React.useEffect(() => { + const handler = (e) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + const btn = document.querySelector(".btn-primary:not([disabled])"); + if (btn) btn.click(); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, []); + + // ── render ─────────────────────────────────────────────────────────── + let body; + if (stage === "fork") { + body = ( + + ); + } else if (stage === "path") { + const props = { + data, onUpdate: update, + onBack: backToFork, + onClose: close, + onComplete: completePath, + onJumpToStep: setStep, + step, + }; + if (path === "entrepreneur") body = ; + else if (path === "owner") body = ; + else body = ; + } else if (stage === "build") { + body = ( + setStage("path")} + onClose={close} + onOpen={openWorkspace} + /> + ); + } else { + body = ; + } + + return ( +
+ {body} + { + if (s === "fork") setStage("fork"); + else if (s === "build") { setPath(p); setStage("build"); } + else if (s === "ready") { setPath(p); setStage("ready"); } + else { setPath(p); setStep(idx); setStage("path"); } + }} + /> +
+ ); +} + +// ── Debug navigator ────────────────────────────────────────────────────── +function DebugNav({ open, setOpen, stage, path, step, onJump }) { + const groups = [ + { title: "Start", rows: [ + { label: "01 · Fork", active: stage === "fork", go: () => onJump("fork") }, + ]}, + { title: "Entrepreneur", rows: [ + { label: "02 · Idea", active: stage === "path" && path === "entrepreneur" && step === 0, go: () => onJump("path", "entrepreneur", 0) }, + { label: "03 · Audience", active: stage === "path" && path === "entrepreneur" && step === 1, go: () => onJump("path", "entrepreneur", 1) }, + { label: "04 · Goal", active: stage === "path" && path === "entrepreneur" && step === 2, go: () => onJump("path", "entrepreneur", 2) }, + { label: "05 · Vibe", active: stage === "path" && path === "entrepreneur" && step === 3, go: () => onJump("path", "entrepreneur", 3) }, + ]}, + { title: "Owner", rows: [ + { label: "02 · Business", active: stage === "path" && path === "owner" && step === 0, go: () => onJump("path", "owner", 0) }, + { label: "03 · Stack", active: stage === "path" && path === "owner" && step === 1, go: () => onJump("path", "owner", 1) }, + { label: "04 · First fix", active: stage === "path" && path === "owner" && step === 2, go: () => onJump("path", "owner", 2) }, + { label: "05 · Scale", active: stage === "path" && path === "owner" && step === 3, go: () => onJump("path", "owner", 3) }, + ]}, + { title: "Consultant", rows: [ + { label: "02 · Client", active: stage === "path" && path === "consultant" && step === 0, go: () => onJump("path", "consultant", 0) }, + { label: "03 · Brief", active: stage === "path" && path === "consultant" && step === 1, go: () => onJump("path", "consultant", 1) }, + { label: "04 · Scope", active: stage === "path" && path === "consultant" && step === 2, go: () => onJump("path", "consultant", 2) }, + { label: "05 · Handoff", active: stage === "path" && path === "consultant" && step === 3, go: () => onJump("path", "consultant", 3) }, + ]}, + { title: "Finish", rows: [ + { label: "Build · entrepreneur", active: stage === "build" && path === "entrepreneur", go: () => onJump("build", "entrepreneur") }, + { label: "Build · owner", active: stage === "build" && path === "owner", go: () => onJump("build", "owner") }, + { label: "Build · consultant", active: stage === "build" && path === "consultant", go: () => onJump("build", "consultant") }, + { label: "Ready", active: stage === "ready", go: () => onJump("ready", path || "entrepreneur") }, + ]}, + ]; + + return ( +
+ {open && ( +
+ {groups.map((g) => ( + +
{g.title}
+ {g.rows.map((r) => ( + + ))} +
+ ))} + +
+ )} + +
+ ); +} + +ReactDOM.createRoot(document.getElementById("root")).render(); diff --git a/new-site/onboarding-build.jsx b/new-site/onboarding-build.jsx new file mode 100644 index 0000000..10ee13c --- /dev/null +++ b/new-site/onboarding-build.jsx @@ -0,0 +1,445 @@ +// Build + Ready screens. The build screen shows the terminal stream + a live +// preview stencil; ready is a quiet confirmation page with the workspace URL. + +// ── Per-path build plans ─────────────────────────────────────────────────── +function buildPlanFor(path, data) { + const common = [ + { line: "vibn init — reading brief", ms: 600 }, + { line: "↳ provisioning workspace", ms: 700 }, + { line: "↳ wiring auth (email + Google)", ms: 800 }, + { line: "↳ minting database & seed schema", ms: 700 }, + ]; + + if (path === "entrepreneur") { + return [ + ...common, + { line: `↳ generating landing page · vibe "${data.vibe || "warm"}"`, ms: 900 }, + { line: `↳ writing copy aimed at "${(data.audience || "your audience").slice(0, 40)}"`, ms: 800 }, + { line: "↳ wiring email capture + Stripe payment link", ms: 700 }, + { line: "↳ scaffolding admin: subscribers, sales, comments", ms: 800 }, + { line: `↳ tuning launch plan for goal: ${data.goal || "first_customer"}`, ms: 700 }, + { line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 }, + { line: "ready.", ms: 400, ok: true }, + ]; + } + if (path === "owner") { + return [ + ...common, + { line: `↳ modelling ${data.biz || "small business"} for "${data.bizName || "your business"}"`, ms: 800 }, + { line: `↳ importing your stack (${(data.tools || []).length} tools)`, ms: 800 }, + { line: `↳ building module: ${labelFor(data.firstThing)}`, ms: 1000 }, + { line: "↳ generating customer + job records (10 sample)", ms: 700 }, + { line: "↳ scheduling daily ops view + weekly report", ms: 700 }, + { line: `↳ wiring savings tracker · est. $${(data.spend || 0)}/mo replaced`, ms: 800 }, + { line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 }, + { line: "ready.", ms: 400, ok: true }, + ]; + } + return [ + ...common, + { line: `↳ branding workspace for "${data.clientName || "client"}"`, ms: 800 }, + { line: `↳ scaffolding scope (${(data.scope || []).length} modules)`, ms: 1000 }, + ...(data.scope || []).slice(0, 4).map((s) => ({ line: ` • ${s}`, ms: 350 })), + { line: "↳ generating handoff document + invoice template", ms: 700 }, + { line: `↳ setting deploy target: ${data.handoff || "subdomain"}`, ms: 700 }, + { line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 }, + { line: "ready.", ms: 400, ok: true }, + ]; +} + +function labelFor(id) { + const map = { + booking: "Bookings & scheduling", + invoicing: "Quotes & invoices", + customers: "Customer portal", + inventory: "Inventory & orders", + team: "Team & dispatch", + marketing: "Marketing site", + }; + return map[id] || "your first workflow"; +} + +function workspaceUrlFor(path, data) { + const seed = + (path === "owner" && data.bizName) || + (path === "consultant" && data.clientName) || + (path === "entrepreneur" && (data.audience || data.idea)) || + "your-workspace"; + const slug = String(seed).toLowerCase() + .replace(/[^a-z0-9\s-]/g, "") + .trim() + .split(/\s+/) + .slice(0, 3) + .join("-") + .slice(0, 28) || "your-workspace"; + return `${slug}.vibn.app`; +} + +const BUILD_BIZ_LABEL = { + service: "Trades / services", + retail: "Retail", + food: "Food & drink", + appointments: "Appointments", + events: "Events / hospitality", + other: "Small business", +}; + +// ── Build screen ─────────────────────────────────────────────────────────── +function BuildScreen({ path, data, onBack, onClose, onOpen }) { + const plan = React.useMemo(() => buildPlanFor(path, data), [path, data]); + const [lineIdx, setLineIdx] = React.useState(0); + const [done, setDone] = React.useState(false); + const logRef = React.useRef(null); + + React.useEffect(() => { + if (lineIdx >= plan.length) { setDone(true); return undefined; } + const t = setTimeout(() => setLineIdx(lineIdx + 1), plan[lineIdx].ms); + return () => clearTimeout(t); + }, [lineIdx, plan]); + + React.useEffect(() => { + if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; + }, [lineIdx]); + + const url = workspaceUrlFor(path, data); + const lane = LANE_LABELS[path]; + const previewTitle = + path === "owner" ? (data.bizName || "Your business") : + path === "consultant" ? (data.clientName || "Your client") : + "Your launch page"; + const previewSub = + path === "owner" ? `${BUILD_BIZ_LABEL[data.biz] || "Small business"} · ${labelFor(data.firstThing)}` : + path === "consultant" ? (data.industry || "Project") : + (data.audience || "An idea worth building").slice(0, 64); + + const pct = done ? 1 : lineIdx / plan.length; + + return ( + <> + +
+
+ + + + +
+ {/* terminal */} +
+
+ + + + vibn build — {url} + {!done && live} +
+
+ {plan.slice(0, lineIdx + 1).map((p, i) => { + const isCurrent = i === lineIdx && !done; + const isOk = p.ok && i <= lineIdx; + const cls = isOk ? "l-ok" : isCurrent ? "l-current" : "l-done"; + return
{p.line}
; + })} + {!done &&
+
+ + {/* preview */} +
+
+ + + + + + https://{url} +
+
+
+
+ {previewTitle} +
+
+ {previewSub} +
+
+
+
+
+
+
+
+
+
+
+ +
+ + {done ? "build complete" : <>Building {Math.min(lineIdx, plan.length)}/{plan.length}} + + {done && ( + + )} +
+
+
+ + ); +} + +// ── Ready screen ─────────────────────────────────────────────────────────── +function ReadyScreen({ path, data, onClose, onOpenChat }) { + const url = workspaceUrlFor(path, data); + const summary = summaryFor(path, data); + + return ( + <> + + +
+ + + +
+ + + +
+
+ + Workspace URL + + + {url} + +
+
+ {summary.map((row, i) => ( +
+ + {row.label} + + {row.value} +
+ ))} +
+
+ +
+ Back to home + +
+
+ + ); +} + +function summaryFor(path, data) { + if (path === "owner") { + return [ + { label: "Business", value: `${data.bizName || "Untitled"} · ${BUILD_BIZ_LABEL[data.biz] || "Small business"}` }, + { label: "Replacing", value: `${(data.tools || []).length} tools · ~$${data.spend || 0}/mo` }, + { label: "First fix", value: labelFor(data.firstThing) }, + { label: "Team", value: `${data.team || 1} · ${(data.customers || 0).toLocaleString()} cust/mo` }, + ]; + } + if (path === "consultant") { + return [ + { label: "Client", value: `${data.clientName || "Untitled"} · ${data.industry || ""}` }, + { label: "Scope", value: `${(data.scope || []).length} modules` }, + { label: "Brief", value: (data.brief || "").slice(0, 60) + ((data.brief || "").length > 60 ? "…" : "") }, + { label: "Handoff", value: data.handoff || "subdomain" }, + ]; + } + return [ + { label: "Building", value: (data.idea || "").slice(0, 64) + ((data.idea || "").length > 64 ? "…" : "") }, + { label: "Audience", value: (data.audience || "").slice(0, 64) }, + { label: "Goal", value: data.goal || "first_customer" }, + { label: "Vibe", value: data.vibe || "warm" }, + ]; +} + +Object.assign(window, { BuildScreen, ReadyScreen }); diff --git a/new-site/onboarding-consultant.jsx b/new-site/onboarding-consultant.jsx new file mode 100644 index 0000000..04e2485 --- /dev/null +++ b/new-site/onboarding-consultant.jsx @@ -0,0 +1,294 @@ +// Consultant path — 4 steps for freelancers building for a client. + +const CONS_TOTAL = 4; +const CONS_STEP_NAMES = ["Client", "Brief", "Scope", "Handoff"]; + +function ConsClient({ clientName, industry, contact, onChange }) { + return ( + <> + + + onChange({ clientName: e.target.value })} + autoFocus + /> + + + onChange({ industry: e.target.value })} + /> + + + onChange({ contact: e.target.value })} + /> + + + ); +} + +const BRIEF_TEMPLATES = [ + { id: "quote_tool", label: "Quote tool", + body: "Customers request a quote with a few photos and a project description. The team reviews, sends a polished PDF, customer signs and pays a deposit online." }, + { id: "booking", label: "Booking system", + body: "Customers see real availability, book a service window, and get reminders. The team has a daily view of jobs with addresses and contact info." }, + { id: "portal", label: "Customer portal", + body: "Logged-in customers see past jobs, invoices, documents, and can message the business. The business sees a single page per customer." }, + { id: "internal", label: "Internal ops", + body: "Replace the spreadsheets the team is currently using. CRUD on jobs/customers, simple reports, role-based access, export to accounting." }, +]; + +function ConsBrief({ brief, onChange }) { + return ( + <> + + +
+ {BRIEF_TEMPLATES.map((t) => ( + + ))} +
+
+ +