From 6043fa9a9715abf8cf4dc9149b5351d271faeb4a Mon Sep 17 00:00:00 2001 From: ZLostTK Date: Wed, 13 Aug 2025 21:11:52 -0600 Subject: [PATCH 01/27] =?UTF-8?q?Agregar=20soporte=20para=20temas=20en=20l?= =?UTF-8?q?a=20aplicaci=C3=B3n,=20incluyendo=20un=20proveedor=20de=20temas?= =?UTF-8?q?=20y=20un=20conmutador=20de=20temas.=20Se=20a=C3=B1adi=C3=B3=20?= =?UTF-8?q?un=20nuevo=20logo=20que=20cambia=20seg=C3=BAn=20el=20tema=20sel?= =?UTF-8?q?eccionado.=20Tambi=C3=A9n=20se=20actualiz=C3=B3=20el=20archivo?= =?UTF-8?q?=20package.json=20para=20incluir=20la=20dependencia=20"next-the?= =?UTF-8?q?mes".?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/theme-debug.tsx | 2 ++ app/components/theme-logo.tsx | 36 ++++++++++++++++++++++ app/components/theme-provider.tsx | 9 ++++++ app/layout.tsx | 12 ++++++-- app/page.tsx | 17 +++++----- package.json | 1 + pnpm-lock.yaml | 14 +++++++++ public/firecrawl-logo-with-fire-dark.webp | Bin 0 -> 78708 bytes 8 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 app/components/theme-debug.tsx create mode 100644 app/components/theme-logo.tsx create mode 100644 app/components/theme-provider.tsx create mode 100644 public/firecrawl-logo-with-fire-dark.webp diff --git a/app/components/theme-debug.tsx b/app/components/theme-debug.tsx new file mode 100644 index 0000000..139597f --- /dev/null +++ b/app/components/theme-debug.tsx @@ -0,0 +1,2 @@ + + diff --git a/app/components/theme-logo.tsx b/app/components/theme-logo.tsx new file mode 100644 index 0000000..6c5898d --- /dev/null +++ b/app/components/theme-logo.tsx @@ -0,0 +1,36 @@ +"use client" + +import { useTheme } from "next-themes" +import { useEffect, useState } from "react" + +export function ThemeLogo() { + const { theme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + // Return light theme logo by default to avoid hydration mismatch + return ( + Firecrawl + ) + } + + const logoSrc = theme === "dark" + ? "/firecrawl-logo-with-fire-dark.webp" + : "/firecrawl-logo-with-fire.webp" + + return ( + Firecrawl + ) +} diff --git a/app/components/theme-provider.tsx b/app/components/theme-provider.tsx new file mode 100644 index 0000000..8c90fbc --- /dev/null +++ b/app/components/theme-provider.tsx @@ -0,0 +1,9 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" +import { type ThemeProviderProps } from "next-themes/dist/types" + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/app/layout.tsx b/app/layout.tsx index 8c11a46..7cd0cd3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { ThemeProvider } from "@/app/components/theme-provider"; const inter = Inter({ subsets: ["latin"] }); @@ -15,9 +16,16 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} + + {children} + ); diff --git a/app/page.tsx b/app/page.tsx index 43b01d5..e7477d1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -22,6 +22,8 @@ import { } from '@/lib/icons'; import { motion, AnimatePresence } from 'framer-motion'; import CodeApplicationProgress, { type CodeApplicationState } from '@/components/CodeApplicationProgress'; +import { ThemeToggle } from '@/app/components/theme-toggle'; +import { ThemeLogo } from '@/app/components/theme-logo'; interface SandboxData { sandboxId: string; @@ -2725,6 +2727,9 @@ Focus on the key sections and content, making it clean and modern.`; return (
+ {/* Theme Toggle */} + + {/* Home Screen Overlay */} {showHomeScreen && (
@@ -2772,11 +2777,7 @@ Focus on the key sections and content, making it clean and modern.`; {/* Header */}
- Firecrawl +
- Firecrawl +
{/* Model Selector - Left side */} diff --git a/package.json b/package.json index fa30377..d2b5332 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "groq-sdk": "^0.29.0", "lucide-react": "^0.532.0", "next": "15.4.3", + "next-themes": "^0.4.6", "react": "19.1.0", "react-dom": "19.1.0", "react-icons": "^5.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe23553..598531e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: next: specifier: 15.4.3 version: 15.4.3(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -1941,6 +1944,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@15.4.3: resolution: {integrity: sha512-uW7Qe6poVasNIE1X382nI29oxSdFJzjQzTgJFLD43MxyPfGKKxCMySllhBpvqr48f58Om+tLMivzRwBpXEytvA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -4421,6 +4430,11 @@ snapshots: negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + next@15.4.3(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.4.3 diff --git a/public/firecrawl-logo-with-fire-dark.webp b/public/firecrawl-logo-with-fire-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..8111c7964921108fc29021bbe57697fef5f61f6c GIT binary patch literal 78708 zcmdSAWmp_Rw>3Hp?(XjHPH=*|LkJe!f@^@m-QC?acyM?3;O@a8ICFXJJl{F@{`t^Z5EAi5!U{Ugqhqc*sVr9jv~yPd?0c?tspc# zGCdC+@-{*byqc*BL62;ag3Op7)%QTcKEj{py*_2pFDjqbNY+CcWaZURtcQJMP5b~5 zVcXQr$a0mv#Knu$?kchbDH_o!uyp(TY!uT8Vi6sx$y~nzE95E&sK^Mv$Z96IH5Wxt zz0i_>BJ&4?lZlMB1=z_VuO%$0Ee6@i%Fn^L^TR9J$gWhum(!5M;q+y#yLe>U@qAH} z6^=+k^qEW0(o;jOk_Irb)44{vQmZF4US%)* z;-Go@toG~SQ@Hmh`cbiwcTQQ+5F$_+Sl6CPKA~tNW^L8v5AD}Ytp4zw$9J&az(gY* zLpD|5AGrj|gX-M*iYp@N7cgl6-;XU1)TT{{07BX3%8F^>bK`HD6kw{6AgCnzF21*1 zz$>JnSD{w>>t9Ky{gWf*193GPnIad<+>xi`HtHrQ7f`nDfj6(2{49hhLpJ;&PN8K( zzX9%|6WHqa3M0GQT;mWHR!)3wNl4u^9K_=Y(jDV|F$0#K4y33kPTuctI7}*xs<8z@ z>hyqPKW3F1BSQlS71iF|zsa(2$G8Asvrg4ie&'WZW;=XJT;N<(;nc7!nxq*nq# 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

kb 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 Date: Wed, 13 Aug 2025 21:12:12 -0600 Subject: [PATCH 02/27] =?UTF-8?q?Implementar=20estilos=20y=20l=C3=B3gica?= =?UTF-8?q?=20para=20el=20modo=20oscuro=20en=20la=20aplicaci=C3=B3n.=20Se?= =?UTF-8?q?=20a=C3=B1adieron=20nuevas=20clases=20CSS=20para=20el=20tema=20?= =?UTF-8?q?oscuro=20en=20el=20archivo=20globals.css=20y=20se=20cre=C3=B3?= =?UTF-8?q?=20un=20componente=20ThemeToggle=20para=20alternar=20entre=20lo?= =?UTF-8?q?s=20temas=20claro=20y=20oscuro.=20Tambi=C3=A9n=20se=20mejoraron?= =?UTF-8?q?=20los=20estilos=20de=20los=20botones=20para=20adaptarse=20a=20?= =?UTF-8?q?ambos=20modos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/theme-toggle.tsx | 84 ++++++++ app/globals.css | 357 ++++++++++++++++++++++++++++++++ components/ui/button.tsx | 1 + 3 files changed, 442 insertions(+) create mode 100644 app/components/theme-toggle.tsx diff --git a/app/components/theme-toggle.tsx b/app/components/theme-toggle.tsx new file mode 100644 index 0000000..e688dd7 --- /dev/null +++ b/app/components/theme-toggle.tsx @@ -0,0 +1,84 @@ +"use client" + +import * as React from "react" +import { useTheme } from "next-themes" +import { Button } from "@/components/ui/button" + +export function ThemeToggle() { + const { setTheme, theme, resolvedTheme } = useTheme() + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return null + } + + const toggleTheme = () => { + setTheme(resolvedTheme === "light" ? "dark" : "light") + } + + return ( + + ) +} + +function SunIcon(props: React.SVGProps) { + return ( + + + + + + + + + + + + ) +} + +function MoonIcon(props: React.SVGProps) { + return ( + + + + ) +} diff --git a/app/globals.css b/app/globals.css index fe8b620..e12cb7d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -138,6 +138,352 @@ --radius: 0.5rem; } +/* Dark theme overrides */ +.dark { + --color-background: hsl(240 10% 3.9%); + --color-foreground: hsl(0 0% 98%); + --color-card: hsl(240 10% 3.9%); + --color-card-foreground: hsl(0 0% 98%); + --color-popover: hsl(240 10% 3.9%); + --color-popover-foreground: hsl(0 0% 98%); + --color-primary: hsl(0 0% 98%); + --color-primary-foreground: hsl(240 5.9% 10%); + --color-secondary: hsl(240 3.7% 15.9%); + --color-secondary-foreground: hsl(0 0% 98%); + --color-muted: hsl(240 3.7% 15.9%); + --color-muted-foreground: hsl(240 5% 64.9%); + --color-accent: hsl(240 3.7% 15.9%); + --color-accent-foreground: hsl(0 0% 98%); + --color-destructive: hsl(0 62.8% 30.6%); + --color-destructive-foreground: hsl(0 0% 98%); + --color-border: hsl(240 3.7% 15.9%); + --color-input: hsl(240 3.7% 15.9%); + --color-ring: hsl(240 4.9% 83.9%); +} + +/* Dark mode styles for common elements */ +.dark .bg-white { + background-color: hsl(240 10% 3.9%); +} + +/* !important */ +.dark .text-gray-900, +.dark .text-zinc-900 { + color: hsl(0 0% 98%); +} + +/* Dark mode for specific color values */ +.dark [class*="text-[#36322F]"] { + color: hsl(0 0% 98%); +} + +.dark .text-gray-600, +.dark .text-zinc-500, +.dark .text-zinc-600 { + color: hsl(240 5% 64.9%); +} + +.dark .bg-gray-50, +.dark .bg-gray-100 { + background-color: hsl(240 3.7% 15.9%); +} + +.dark .border-gray-200, +.dark .border-zinc-300 { + border-color: hsl(240 3.7% 15.9%); +} + +.dark .bg-gray-900 { + background-color: hsl(240 10% 3.9%); +} + +/* Dark mode for specific Open Lovable elements */ +.dark .bg-orange-400\/50, +.dark .bg-orange-300\/30, +.dark .bg-orange-200\/20 { + background-color: hsl(240 10% 3.9%); +} + +.dark .bg-yellow-300\/40 { + background-color: hsl(240 10% 3.9%); +} + +/* Dark mode for design style buttons */ +.dark .p-3.rounded-lg.border.border-gray-200.bg-white { + background-color: hsl(240 3.7% 15.9%); + border-color: hsl(240 3.7% 15.9%); + color: hsl(0 0% 98%); +} + +.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover { + background-color: hsl(240 3.7% 20%); + border-color: hsl(25 95% 53%); +} + +.dark .p-3.rounded-lg.border.border-gray-200.bg-white .text-sm.font-medium { + color: hsl(0 0% 98%); +} + +.dark .p-3.rounded-lg.border.border-gray-200.bg-white .text-xs.text-gray-500 { + color: hsl(240 5% 64.9%); +} + +/* Dark mode for selected/active design style buttons */ +.dark .p-3.rounded-lg.border.border-orange-200.bg-orange-50\/50 { + background-color: hsl(25 95% 53%); + border-color: hsl(25 95% 53%); + color: hsl(240 10% 3.9%); +} + +.dark .p-3.rounded-lg.border.border-orange-200.bg-orange-50\/50 .text-sm.font-medium { + color: hsl(240 10% 3.9%); +} + +.dark .p-3.rounded-lg.border.border-orange-200.bg-orange-50\/50 .text-xs.text-gray-500 { + color: hsl(240 10% 3.9%); + opacity: 0.8; +} + +/* Dark mode for selected buttons with orange-400 border and orange-50 background */ +.dark .p-3.rounded-lg.border.border-orange-400.bg-orange-50 { + background-color: hsl(240 3.7% 15.9%); + border-color: hsl(25 95% 53%); + border-width: 2px; + color: hsl(0 0% 98%); + box-shadow: 0 0 0 1px hsl(25 95% 53%), 0 4px 12px -1px rgba(0, 0, 0, 0.15); +} + +.dark .p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-sm.font-medium { + color: hsl(0 0% 98%); + font-weight: 600; +} + +.dark .p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-xs.text-gray-500 { + color: hsl(240 5% 64.9%); + font-weight: 500; +} + +/* Light mode improvements for selected buttons */ +.p-3.rounded-lg.border.border-orange-400.bg-orange-50 { + background-color: hsl(0 0% 98%); + border-color: hsl(25 95% 53%); + border-width: 2px; + color: hsl(240 10% 3.9%); + box-shadow: 0 0 0 1px hsl(25 95% 53%), 0 4px 12px -1px rgba(0, 0, 0, 0.1); +} + +.p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-sm.font-medium { + color: hsl(240 10% 3.9%); + font-weight: 600; +} + +.p-3.rounded-lg.border.border-orange-400.bg-orange-50 .text-xs.text-gray-500 { + color: hsl(240 5% 40%); + font-weight: 500; +} + +/* Dark mode for hover states on design style buttons */ +.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover { + background-color: hsl(25 95% 53%); + border-color: hsl(25 95% 53%); + color: hsl(240 10% 3.9%); +} + +.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover .text-sm.font-medium { + color: hsl(240 10% 3.9%); +} + +.dark .p-3.rounded-lg.border.border-gray-200.bg-white:hover .text-xs.text-gray-500 { + color: hsl(240 10% 3.9%); + opacity: 0.8; +} + +/* Dark mode for dropdowns and selectors */ +.dark .bg-white { + background-color: hsl(240 10% 3.9%); +} + +/* Dark mode for design container */ +.dark .bg-white\/80.backdrop-blur-sm.border.border-gray-200.rounded-xl.p-4.shadow-sm { + background-color: hsl(240 10% 3.9% / 0.9); + border-color: hsl(240 3.7% 15.9%); + backdrop-filter: blur(12px); + box-shadow: 0 8px 32px -4px rgba(0, 0, 0, 0.3), 0 2px 8px -2px rgba(0, 0, 0, 0.1); +} + +/* Light mode improvements for design container */ +.bg-white\/80.backdrop-blur-sm.border.border-gray-200.rounded-xl.p-4.shadow-sm { + background-color: hsl(0 0% 100% / 0.9); + border-color: hsl(240 5.9% 90%); + backdrop-filter: blur(12px); + box-shadow: 0 8px 32px -4px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.05); +} + +/* Dark mode for loading spinner and text */ +.dark .w-16.h-16.border-4.border-orange-200.border-t-orange-500.rounded-full.animate-spin.mx-auto { + border-color: hsl(25 95% 53%); + border-top-color: hsl(25 95% 70%); +} + +.dark .text-xl.font-semibold.text-gray-800 { + color: hsl(0 0% 98%); +} + +.dark .text-gray-600.text-sm { + color: hsl(240 5% 64.9%); +} + +/* Dark mode for file explorer */ +.dark .flex-1.overflow-y-auto.p-2.scrollbar-hide { + background-color: hsl(240 10% 3.9%); +} + +.dark .text-sm { + color: hsl(0 0% 98%); +} + +.dark .flex.items-center.gap-1.py-1.px-2.hover\:bg-gray-100.rounded.cursor-pointer.text-gray-700 { + color: hsl(0 0% 98%); +} + +.dark .flex.items-center.gap-1.py-1.px-2.hover\:bg-gray-100.rounded.cursor-pointer.text-gray-700:hover { + background-color: hsl(240 3.7% 15.9%); +} + +/* Dark mode for file items */ +.dark .text-xs.flex.items-center.gap-1 { + color: hsl(0 0% 98%); +} + +.dark .text-xs.flex.items-center.gap-1:hover { + color: hsl(25 95% 70%); + background-color: hsl(240 3.7% 15.9%); + border-radius: 0.375rem; + padding: 0.25rem 0.5rem; + transition: all 0.2s ease; +} + +/* Light mode improvements for file items */ +.text-xs.flex.items-center.gap-1 { + color: hsl(240 10% 3.9%); + transition: all 0.2s ease; +} + +/* Theme toggle button improvements */ +.dark .rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110 { + background-color: hsl(240 10% 3.9%); + border-color: hsl(240 3.7% 15.9%); + color: hsl(0 0% 98%); + box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1); +} + +.dark .rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110:hover { + background-color: hsl(240 3.7% 15.9%); + border-color: hsl(25 95% 53%); + transform: scale(1.1); +} + +/* Light mode theme toggle improvements */ +.rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110 { + box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +.rounded-full.fixed.bottom-4.right-4.z-\[9999\].bg-white.dark\:bg-gray-800.border-gray-300.dark\:border-gray-600.text-gray-700.dark\:text-gray-200.hover\:bg-gray-50.dark\:hover\:bg-gray-700.shadow-lg.transition-all.duration-200.hover\:scale-110:hover { + border-color: hsl(25 95% 53%); + transform: scale(1.1); +} + +.text-xs.flex.items-center.gap-1:hover { + color: hsl(25 95% 53%); + background-color: hsl(25 95% 95%); + border-radius: 0.375rem; + padding: 0.25rem 0.5rem; +} + +/* Light mode improvements for loading elements */ +.w-16.h-16.border-4.border-orange-200.border-t-orange-500.rounded-full.animate-spin.mx-auto { + border-color: hsl(25 95% 53%); + border-top-color: hsl(25 95% 70%); + box-shadow: 0 0 20px rgba(251, 146, 60, 0.3); +} + +.text-xl.font-semibold.text-gray-800 { + color: hsl(240 10% 3.9%); +} + +.text-gray-600.text-sm { + color: hsl(240 5% 40%); +} + +.dark .text-black { + color: hsl(0 0% 98%); +} + +.dark .hover\:bg-gray-50:hover { + background-color: hsl(240 3.7% 15.9%); +} + +.dark .hover\:bg-gray-100:hover { + background-color: hsl(240 3.7% 15.9%); +} + +/* Dark mode for select elements */ +.dark select { + background-color: hsl(240 10% 3.9%); + color: hsl(0 0% 98%); + border-color: hsl(240 3.7% 15.9%); +} + +.dark select option { + background-color: hsl(240 10% 3.9%); + color: hsl(0 0% 98%); +} + +.dark select option:hover { + background-color: hsl(240 3.7% 15.9%); +} + +/* Dark mode for specific Open Lovable elements */ +.dark .bg-card { + background-color: hsl(240 10% 3.9%); +} + +.dark .border-border { + border-color: hsl(240 3.7% 15.9%); +} + +.dark .text-black { + color: hsl(0 0% 98%); +} + +.dark .hover\:text-gray-700:hover { + color: hsl(0 0% 98%); +} + +/* Dark mode for page background */ +.dark .bg-background { + background-color: hsl(240 10% 3.9%); +} + +.dark .text-foreground { + color: hsl(0 0% 98%); +} + +/* Dark mode for specific backgrounds */ +.dark .bg-gray-50 { + background-color: hsl(240 10% 3.9%); +} + +.dark .bg-gray-100 { + background-color: hsl(240 10% 3.9%); +} + +.dark .bg-gray-900 { + background-color: hsl(240 10% 3.9%); +} + + + @layer utilities { /* Hide scrollbar for Chrome, Safari and Opera */ .scrollbar-hide::-webkit-scrollbar { @@ -169,6 +515,17 @@ background-color: theme('colors.background'); color: theme('colors.foreground'); } + + /* Dark mode overrides for common elements */ + .dark { + background-color: theme('colors.background'); + color: theme('colors.foreground'); + } + + /* Ensure dark mode applies to all elements */ + .dark * { + border-color: theme('colors.border'); + } } @layer utilities { diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 56dba58..967fdf9 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -19,6 +19,7 @@ const buttonVariants = cva( size: { default: "h-10 px-4 py-2", sm: "h-8 px-3 py-1 text-sm", + icon: "h-8 w-8 p-0", lg: "h-12 px-6 py-3", }, }, From 415179c23447b38ae4f3a0b3a574b1d67b9f018f Mon Sep 17 00:00:00 2001 From: MFCo Date: Tue, 26 Aug 2025 13:01:46 +0200 Subject: [PATCH 03/27] Migrate to Vercel Sandbox using OIDC --- .gitignore | 1 + README.md | 2 +- app/api/apply-ai-code-stream/route.ts | 79 +- app/api/apply-ai-code/route.ts | 59 +- app/api/create-ai-sandbox/route.ts | 324 ++++---- app/api/create-zip/route.ts | 73 +- app/api/detect-and-install-packages/route.ts | 200 ++--- app/api/generate-ai-code-stream/route.ts | 52 +- app/api/get-sandbox-files/route.ts | 135 ++-- app/api/install-packages/route.ts | 340 +++----- app/api/kill-sandbox/route.ts | 10 +- app/api/monitor-vite-logs/route.ts | 173 +++-- app/api/restart-vite/route.ts | 142 +--- app/api/run-command/route.ts | 41 +- app/api/sandbox-logs/route.ts | 105 +-- components/SandboxPreview.tsx | 7 +- config/app.config.ts | 25 +- docs/PACKAGE_DETECTION_GUIDE.md | 48 +- package-lock.json | 771 ++++--------------- package.json | 6 +- 20 files changed, 972 insertions(+), 1621 deletions(-) diff --git a/.gitignore b/.gitignore index ac59fa8..79f47d8 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ e2b-template-* *.temp repomix-output.txt bun.lockb +.env*.local diff --git a/README.md b/README.md index 803cc92..b7a51d1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ npm install 2. **Add `.env.local`** ```env # Required -E2B_API_KEY=your_e2b_api_key # Get from https://e2b.dev (Sandboxes) +VERCEL_TEAM_ID=your_team_id # Your Vercel team ID for sandbox access FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) # Optional (need at least one AI provider) diff --git a/app/api/apply-ai-code-stream/route.ts b/app/api/apply-ai-code-stream/route.ts index c91bf11..ac382d6 100644 --- a/app/api/apply-ai-code-stream/route.ts +++ b/app/api/apply-ai-code-stream/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Sandbox } from '@e2b/code-interpreter'; +import { Sandbox } from '@vercel/sandbox'; import type { SandboxState } from '@/types/sandbox'; import type { ConversationState } from '@/types/conversation'; @@ -525,7 +525,6 @@ export async function POST(request: NextRequest) { normalizedPath = 'src/' + normalizedPath; } - const fullPath = `/home/user/app/${normalizedPath}`; const isUpdate = global.existingFiles.has(normalizedPath); // Remove any CSS imports from JSX/JS files (we're using Tailwind) @@ -534,19 +533,20 @@ export async function POST(request: NextRequest) { fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, ''); } - // Write the file using Python (code-interpreter SDK) - const escapedContent = fileContent - .replace(/\\/g, '\\\\') - .replace(/"""/g, '\\"\\"\\"') - .replace(/\$/g, '\\$'); + // Create directory if needed + const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : ''; + if (dirPath) { + await sandboxInstance.runCommand({ + cmd: 'mkdir', + args: ['-p', dirPath] + }); + } - await sandboxInstance.runCode(` -import os -os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True) -with open("${fullPath}", 'w') as f: - f.write("""${escapedContent}""") -print(f"File written: ${fullPath}") - `); + // Write the file using Vercel Sandbox writeFiles + await sandboxInstance.writeFiles([{ + path: normalizedPath, + content: Buffer.from(fileContent) + }]); // Update file cache if (global.sandboxState?.fileCache) { @@ -599,28 +599,39 @@ print(f"File written: ${fullPath}") action: 'executing' }); - // Use E2B commands.run() for cleaner execution - const result = await sandboxInstance.commands.run(cmd, { - cwd: '/home/user/app', - timeout: 60, - on_stdout: async (data: string) => { - await sendProgress({ - type: 'command-output', - command: cmd, - output: data, - stream: 'stdout' - }); - }, - on_stderr: async (data: string) => { - await sendProgress({ - type: 'command-output', - command: cmd, - output: data, - stream: 'stderr' - }); - } + // Parse command and arguments for Vercel Sandbox + const commandParts = cmd.trim().split(/\s+/); + const cmdName = commandParts[0]; + const args = commandParts.slice(1); + + // Use Vercel Sandbox runCommand + const result = await sandboxInstance.runCommand({ + cmd: cmdName, + args }); + // Get command output + const stdout = await result.stdout(); + const stderr = await result.stderr(); + + if (stdout) { + await sendProgress({ + type: 'command-output', + command: cmd, + output: stdout, + stream: 'stdout' + }); + } + + if (stderr) { + await sendProgress({ + type: 'command-output', + command: cmd, + output: stderr, + stream: 'stderr' + }); + } + if (results.commandsExecuted) { results.commandsExecuted.push(cmd); } diff --git a/app/api/apply-ai-code/route.ts b/app/api/apply-ai-code/route.ts index f00f08a..f051da4 100644 --- a/app/api/apply-ai-code/route.ts +++ b/app/api/apply-ai-code/route.ts @@ -432,15 +432,12 @@ function App() { export default App;`; try { - await global.activeSandbox.runCode(` -file_path = "/home/user/app/src/App.jsx" -file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}""" - -with open(file_path, 'w') as f: - f.write(file_content) - -print(f"Auto-generated: {file_path}") - `); + await global.activeSandbox.writeFiles([{ + path: 'src/App.jsx', + content: Buffer.from(appContent) + }]); + + console.log('Auto-generated: src/App.jsx'); results.filesCreated.push('src/App.jsx (auto-generated)'); } catch (error) { results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`); @@ -459,9 +456,7 @@ print(f"Auto-generated: {file_path}") if (!isEdit && !indexCssInParsed && !indexCssExists) { try { - await global.activeSandbox.runCode(` -file_path = "/home/user/app/src/index.css" -file_content = """@tailwind base; + const indexCssContent = `@tailwind base; @tailwind components; @tailwind utilities; @@ -483,13 +478,14 @@ body { margin: 0; min-width: 320px; min-height: 100vh; -}""" +}`; -with open(file_path, 'w') as f: - f.write(file_content) - -print(f"Auto-generated: {file_path}") - `); + await global.activeSandbox.writeFiles([{ + path: 'src/index.css', + content: Buffer.from(indexCssContent) + }]); + + console.log('Auto-generated: src/index.css'); results.filesCreated.push('src/index.css (with Tailwind)'); } catch (error) { results.errors.push('Failed to create index.css with Tailwind'); @@ -500,15 +496,24 @@ print(f"Auto-generated: {file_path}") // Execute commands for (const cmd of parsed.commands) { try { - await global.activeSandbox.runCode(` -import subprocess -os.chdir('/home/user/app') -result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True) -print(f"Executed: ${cmd}") -print(result.stdout) -if result.stderr: - print(f"Errors: {result.stderr}") - `); + // Parse command and arguments + const commandParts = cmd.trim().split(/\s+/); + const cmdName = commandParts[0]; + const args = commandParts.slice(1); + + // Execute command using Vercel Sandbox + const result = await global.activeSandbox.runCommand({ + cmd: cmdName, + args + }); + + console.log(`Executed: ${cmd}`); + const stdout = await result.stdout(); + const stderr = await result.stderr(); + + if (stdout) console.log(stdout); + if (stderr) console.log(`Errors: ${stderr}`); + results.commandsExecuted.push(cmd); } catch (error) { results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`); diff --git a/app/api/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts index 257ce1d..3bb1bcb 100644 --- a/app/api/create-ai-sandbox/route.ts +++ b/app/api/create-ai-sandbox/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { Sandbox } from '@e2b/code-interpreter'; +import { Sandbox } from '@vercel/sandbox'; import type { SandboxState } from '@/types/sandbox'; import { appConfig } from '@/config/app.config'; @@ -15,15 +15,15 @@ export async function POST() { let sandbox: any = null; try { - console.log('[create-ai-sandbox] Creating base sandbox...'); + console.log('[create-ai-sandbox] Creating Vercel sandbox...'); // Kill existing sandbox if any if (global.activeSandbox) { - console.log('[create-ai-sandbox] Killing existing sandbox...'); + console.log('[create-ai-sandbox] Stopping existing sandbox...'); try { - await global.activeSandbox.kill(); + await global.activeSandbox.stop(); } catch (e) { - console.error('Failed to close existing sandbox:', e); + console.error('Failed to stop existing sandbox:', e); } global.activeSandbox = null; } @@ -35,81 +35,86 @@ export async function POST() { global.existingFiles = new Set(); } - // Create base sandbox - we'll set up Vite ourselves for full control - console.log(`[create-ai-sandbox] Creating base E2B sandbox with ${appConfig.e2b.timeoutMinutes} minute timeout...`); - sandbox = await Sandbox.create({ - apiKey: process.env.E2B_API_KEY, - timeoutMs: appConfig.e2b.timeoutMs + // Create Vercel sandbox + console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`); + sandbox = await Sandbox.create({ + timeout: appConfig.vercelSandbox.timeoutMs, + runtime: appConfig.vercelSandbox.runtime, + ports: [appConfig.vercelSandbox.devPort] }); - const sandboxId = (sandbox as any).sandboxId || Date.now().toString(); - const host = (sandbox as any).getHost(appConfig.e2b.vitePort); - + const sandboxId = sandbox.sandboxId; console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`); - console.log(`[create-ai-sandbox] Sandbox host: ${host}`); - // Set up a basic Vite React app using Python to write files + // Set up a basic Vite React app console.log('[create-ai-sandbox] Setting up Vite React app...'); - // Write all files in a single Python script to avoid multiple executions - const setupScript = ` -import os -import json + // First, change to the working directory + await sandbox.runCommand('pwd'); + const workDir = appConfig.vercelSandbox.workingDirectory; + + // Get the sandbox URL using the correct Vercel Sandbox API + const sandboxUrl = sandbox.domain(appConfig.vercelSandbox.devPort); + + // Extract the hostname from the sandbox URL for Vite config + const sandboxHostname = new URL(sandboxUrl).hostname; + console.log(`[create-ai-sandbox] Sandbox hostname: ${sandboxHostname}`); -print('Setting up React app with Vite and Tailwind...') - -# Create directory structure -os.makedirs('/home/user/app/src', exist_ok=True) - -# Package.json -package_json = { - "name": "sandbox-app", - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite --host", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@vitejs/plugin-react": "^4.0.0", - "vite": "^4.3.9", - "tailwindcss": "^3.3.0", - "postcss": "^8.4.31", - "autoprefixer": "^10.4.16" - } -} - -with open('/home/user/app/package.json', 'w') as f: - json.dump(package_json, f, indent=2) -print('✓ package.json') - -# Vite config for E2B - with allowedHosts -vite_config = """import { defineConfig } from 'vite' + // Create the Vite config content with the proper hostname (using string concatenation) + const viteConfigContent = `import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// E2B-compatible Vite configuration +// Vercel Sandbox compatible Vite configuration export default defineConfig({ plugins: [react()], server: { host: '0.0.0.0', - port: 5173, + port: ${appConfig.vercelSandbox.devPort}, strictPort: true, - hmr: false, - allowedHosts: ['.e2b.app', 'localhost', '127.0.0.1'] + hmr: true, + allowedHosts: [ + 'localhost', + '127.0.0.1', + '` + sandboxHostname + `', // Allow the Vercel Sandbox domain + '.vercel.run', // Allow all Vercel sandbox domains + '.vercel-sandbox.dev' // Fallback pattern + ] } -})""" +})`; -with open('/home/user/app/vite.config.js', 'w') as f: - f.write(vite_config) -print('✓ vite.config.js') - -# Tailwind config - standard without custom design tokens -tailwind_config = """/** @type {import('tailwindcss').Config} */ + // Create the project files (now we have the sandbox hostname) + const projectFiles = [ + { + path: 'package.json', + content: Buffer.from(JSON.stringify({ + "name": "sandbox-app", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite --host --port 3000", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "vite": "^4.3.9", + "tailwindcss": "^3.3.0", + "postcss": "^8.4.31", + "autoprefixer": "^10.4.16" + } + }, null, 2)) + }, + { + path: 'vite.config.js', + content: Buffer.from(viteConfigContent) + }, + { + path: 'tailwind.config.js', + content: Buffer.from(`/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", @@ -119,26 +124,20 @@ export default { extend: {}, }, plugins: [], -}""" - -with open('/home/user/app/tailwind.config.js', 'w') as f: - f.write(tailwind_config) -print('✓ tailwind.config.js') - -# PostCSS config -postcss_config = """export default { +}`) + }, + { + path: 'postcss.config.js', + content: Buffer.from(`export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, -}""" - -with open('/home/user/app/postcss.config.js', 'w') as f: - f.write(postcss_config) -print('✓ postcss.config.js') - -# Index.html -index_html = """ +}`) + }, + { + path: 'index.html', + content: Buffer.from(` @@ -149,14 +148,11 @@ index_html = """

-""" - -with open('/home/user/app/index.html', 'w') as f: - f.write(index_html) -print('✓ index.html') - -# Main.jsx -main_jsx = """import React from 'react' +`) + }, + { + path: 'src/main.jsx', + content: Buffer.from(`import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' @@ -165,19 +161,18 @@ ReactDOM.createRoot(document.getElementById('root')).render( , -)""" - -with open('/home/user/app/src/main.jsx', 'w') as f: - f.write(main_jsx) -print('✓ src/main.jsx') - -# App.jsx with explicit Tailwind test -app_jsx = """function App() { +)`) + }, + { + path: 'src/App.jsx', + content: Buffer.from(`function App() { return (
+

+ Sandbox Ready +

- Sandbox Ready
Start building your React app with Vite and Tailwind CSS!

@@ -185,14 +180,11 @@ app_jsx = """function App() { ) } -export default App""" - -with open('/home/user/app/src/App.jsx', 'w') as f: - f.write(app_jsx) -print('✓ src/App.jsx') - -# Index.css with explicit Tailwind directives -index_css = """@tailwind base; +export default App`) + }, + { + path: 'src/index.css', + content: Buffer.from(`@tailwind base; @tailwind components; @tailwind utilities; @@ -216,99 +208,53 @@ index_css = """@tailwind base; body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background-color: rgb(17 24 39); -}""" +}`) + } + ]; -with open('/home/user/app/src/index.css', 'w') as f: - f.write(index_css) -print('✓ src/index.css') - -print('\\nAll files created successfully!') -`; - - // Execute the setup script - await sandbox.runCode(setupScript); + // Create directory structure first + await sandbox.runCommand({ + cmd: 'mkdir', + args: ['-p', 'src'] + }); + + // Write all files + await sandbox.writeFiles(projectFiles); + console.log('[create-ai-sandbox] ✓ Project files created'); // Install dependencies console.log('[create-ai-sandbox] Installing dependencies...'); - await sandbox.runCode(` -import subprocess -import sys - -print('Installing npm packages...') -result = subprocess.run( - ['npm', 'install'], - cwd='/home/user/app', - capture_output=True, - text=True -) - -if result.returncode == 0: - print('✓ Dependencies installed successfully') -else: - print(f'⚠ Warning: npm install had issues: {result.stderr}') - # Continue anyway as it might still work - `); + const installResult = await sandbox.runCommand({ + cmd: 'npm', + args: ['install', '--loglevel', 'info'] + }); + if (installResult.exitCode === 0) { + console.log('[create-ai-sandbox] ✓ Dependencies installed successfully'); + } else { + console.log('[create-ai-sandbox] ⚠ Warning: npm install had issues but continuing...'); + } - // Start Vite dev server + // Start Vite dev server in detached mode console.log('[create-ai-sandbox] Starting Vite dev server...'); - await sandbox.runCode(` -import subprocess -import os -import time - -os.chdir('/home/user/app') - -# Kill any existing Vite processes -subprocess.run(['pkill', '-f', 'vite'], capture_output=True) -time.sleep(1) - -# Start Vite dev server -env = os.environ.copy() -env['FORCE_COLOR'] = '0' - -process = subprocess.Popen( - ['npm', 'run', 'dev'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env -) - -print(f'✓ Vite dev server started with PID: {process.pid}') -print('Waiting for server to be ready...') - `); + const viteProcess = await sandbox.runCommand({ + cmd: 'npm', + args: ['run', 'dev'], + detached: true + }); + + console.log('[create-ai-sandbox] ✓ Vite dev server started'); // Wait for Vite to be fully ready - await new Promise(resolve => setTimeout(resolve, appConfig.e2b.viteStartupDelay)); - - // Force Tailwind CSS to rebuild by touching the CSS file - await sandbox.runCode(` -import os -import time - -# Touch the CSS file to trigger rebuild -css_file = '/home/user/app/src/index.css' -if os.path.exists(css_file): - os.utime(css_file, None) - print('✓ Triggered CSS rebuild') - -# Also ensure PostCSS processes it -time.sleep(2) -print('✓ Tailwind CSS should be loaded') - `); + await new Promise(resolve => setTimeout(resolve, appConfig.vercelSandbox.devServerStartupDelay)); // Store sandbox globally global.activeSandbox = sandbox; global.sandboxData = { sandboxId, - url: `https://${host}` + url: sandboxUrl, + viteProcess }; - // Set extended timeout on the sandbox instance if method available - if (typeof sandbox.setTimeout === 'function') { - sandbox.setTimeout(appConfig.e2b.timeoutMs); - console.log(`[create-ai-sandbox] Set sandbox timeout to ${appConfig.e2b.timeoutMinutes} minutes`); - } - // Initialize sandbox state global.sandboxState = { fileCache: { @@ -319,7 +265,7 @@ print('✓ Tailwind CSS should be loaded') sandbox, sandboxData: { sandboxId, - url: `https://${host}` + url: sandboxUrl } }; @@ -333,13 +279,13 @@ print('✓ Tailwind CSS should be loaded') global.existingFiles.add('tailwind.config.js'); global.existingFiles.add('postcss.config.js'); - console.log('[create-ai-sandbox] Sandbox ready at:', `https://${host}`); + console.log('[create-ai-sandbox] Sandbox ready at:', sandboxUrl); return NextResponse.json({ success: true, sandboxId, - url: `https://${host}`, - message: 'Sandbox created and Vite React app initialized' + url: sandboxUrl, + message: 'Vercel sandbox created and Vite React app initialized' }); } catch (error) { @@ -348,9 +294,9 @@ print('✓ Tailwind CSS should be loaded') // Clean up on error if (sandbox) { try { - await sandbox.kill(); + await sandbox.stop(); } catch (e) { - console.error('Failed to close sandbox on error:', e); + console.error('Failed to stop sandbox on error:', e); } } diff --git a/app/api/create-zip/route.ts b/app/api/create-zip/route.ts index 221c843..2030a39 100644 --- a/app/api/create-zip/route.ts +++ b/app/api/create-zip/route.ts @@ -15,41 +15,37 @@ export async function POST(request: NextRequest) { console.log('[create-zip] Creating project zip...'); - // Create zip file in sandbox - const result = await global.activeSandbox.runCode(` -import zipfile -import os -import json - -os.chdir('/home/user/app') - -# Create zip file -with zipfile.ZipFile('/tmp/project.zip', 'w', zipfile.ZIP_DEFLATED) as zipf: - for root, dirs, files in os.walk('.'): - # Skip node_modules and .git - dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', '.next', 'dist']] - - for file in files: - file_path = os.path.join(root, file) - arcname = os.path.relpath(file_path, '.') - zipf.write(file_path, arcname) - -# Get file size -file_size = os.path.getsize('/tmp/project.zip') -print(f" Created project.zip ({file_size} bytes)") - `); + // Create zip file in sandbox using standard commands + const zipResult = await global.activeSandbox.runCommand({ + cmd: 'bash', + args: ['-c', `zip -r /tmp/project.zip . -x "node_modules/*" ".git/*" ".next/*" "dist/*" "build/*" "*.log"`] + }); + + if (zipResult.exitCode !== 0) { + const error = await zipResult.stderr(); + throw new Error(`Failed to create zip: ${error}`); + } + + const sizeResult = await global.activeSandbox.runCommand({ + cmd: 'bash', + args: ['-c', `ls -la /tmp/project.zip | awk '{print $5}'`] + }); + + const fileSize = await sizeResult.stdout(); + console.log(`[create-zip] Created project.zip (${fileSize.trim()} bytes)`); // Read the zip file and convert to base64 - const readResult = await global.activeSandbox.runCode(` -import base64 - -with open('/tmp/project.zip', 'rb') as f: - content = f.read() - encoded = base64.b64encode(content).decode('utf-8') - print(encoded) - `); + const readResult = await global.activeSandbox.runCommand({ + cmd: 'base64', + args: ['/tmp/project.zip'] + }); - const base64Content = readResult.logs.stdout.join('').trim(); + if (readResult.exitCode !== 0) { + const error = await readResult.stderr(); + throw new Error(`Failed to read zip file: ${error}`); + } + + const base64Content = (await readResult.stdout()).trim(); // Create a data URL for download const dataUrl = `data:application/zip;base64,${base64Content}`; @@ -57,15 +53,18 @@ with open('/tmp/project.zip', 'rb') as f: return NextResponse.json({ success: true, dataUrl, - fileName: 'e2b-project.zip', + fileName: 'vercel-sandbox-project.zip', message: 'Zip file created successfully' }); } catch (error) { console.error('[create-zip] Error:', error); - return NextResponse.json({ - success: false, - error: (error as Error).message - }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: (error as Error).message + }, + { status: 500 } + ); } } \ No newline at end of file diff --git a/app/api/detect-and-install-packages/route.ts b/app/api/detect-and-install-packages/route.ts index 12211b6..facbd51 100644 --- a/app/api/detect-and-install-packages/route.ts +++ b/app/api/detect-and-install-packages/route.ts @@ -64,15 +64,7 @@ export async function POST(request: NextRequest) { const builtins = ['fs', 'path', 'http', 'https', 'crypto', 'stream', 'util', 'os', 'url', 'querystring', 'child_process']; if (builtins.includes(imp)) return false; - // Extract package name (handle scoped packages and subpaths) - const parts = imp.split('/'); - if (imp.startsWith('@')) { - // Scoped package like @vitejs/plugin-react - return true; - } else { - // Regular package, return just the first part - return true; - } + return true; }); // Extract just the package names (without subpaths) @@ -101,153 +93,89 @@ export async function POST(request: NextRequest) { } // Check which packages are already installed - const checkResult = await global.activeSandbox.runCode(` -import os -import json - -installed = [] -missing = [] - -packages = ${JSON.stringify(uniquePackages)} - -for package in packages: - # Handle scoped packages - if package.startswith('@'): - package_path = f"/home/user/app/node_modules/{package}" - else: - package_path = f"/home/user/app/node_modules/{package}" + const installed: string[] = []; + const missing: string[] = []; - if os.path.exists(package_path): - installed.append(package) - else: - missing.append(package) + for (const packageName of uniquePackages) { + try { + const checkResult = await global.activeSandbox.runCommand({ + cmd: 'test', + args: ['-d', `node_modules/${packageName}`] + }); + + if (checkResult.exitCode === 0) { + installed.push(packageName); + } else { + missing.push(packageName); + } + } catch (error) { + // If test command fails, assume package is missing + missing.push(packageName); + } + } -result = { - 'installed': installed, - 'missing': missing -} + console.log('[detect-and-install-packages] Package status:', { installed, missing }); -print(json.dumps(result)) - `); - - const status = JSON.parse(checkResult.logs.stdout.join('')); - console.log('[detect-and-install-packages] Package status:', status); - - if (status.missing.length === 0) { + if (missing.length === 0) { return NextResponse.json({ success: true, packagesInstalled: [], - packagesAlreadyInstalled: status.installed, + packagesAlreadyInstalled: installed, message: 'All packages already installed' }); } // Install missing packages - console.log('[detect-and-install-packages] Installing packages:', status.missing); + console.log('[detect-and-install-packages] Installing packages:', missing); - const installResult = await global.activeSandbox.runCode(` -import subprocess -import os -import json + const installResult = await global.activeSandbox.runCommand({ + cmd: 'npm', + args: ['install', '--save', ...missing] + }); -os.chdir('/home/user/app') -packages_to_install = ${JSON.stringify(status.missing)} - -# Join packages into a single install command -packages_str = ' '.join(packages_to_install) -cmd = f'npm install {packages_str} --save' - -print(f"Running: {cmd}") - -# Run npm install with explicit save flag -result = subprocess.run(['npm', 'install', '--save'] + packages_to_install, - capture_output=True, - text=True, - cwd='/home/user/app', - timeout=60) - -print("stdout:", result.stdout) -if result.stderr: - print("stderr:", result.stderr) - -# Verify installation -installed = [] -failed = [] - -for package in packages_to_install: - # Handle scoped packages correctly - if package.startswith('@'): - # For scoped packages like @heroicons/react - package_path = f"/home/user/app/node_modules/{package}" - else: - package_path = f"/home/user/app/node_modules/{package}" + const stdout = await installResult.stdout(); + const stderr = await installResult.stderr(); - if os.path.exists(package_path): - installed.append(package) - print(f"✓ Verified installation of {package}") - else: - # Check if it's a submodule of an installed package - base_package = package.split('/')[0] - if package.startswith('@'): - # For @scope/package, the base is @scope/package - base_package = '/'.join(package.split('/')[:2]) - - base_path = f"/home/user/app/node_modules/{base_package}" - if os.path.exists(base_path): - installed.append(package) - print(f"✓ Verified installation of {package} (via {base_package})") - else: - failed.append(package) - print(f"✗ Failed to verify installation of {package}") - -result_data = { - 'installed': installed, - 'failed': failed, - 'returncode': result.returncode -} - -print("\\nResult:", json.dumps(result_data)) - `, { timeout: 60000 }); - - // Parse the result more safely - let installStatus; - try { - const stdout = installResult.logs.stdout.join(''); - const resultMatch = stdout.match(/Result:\s*({.*})/); - if (resultMatch) { - installStatus = JSON.parse(resultMatch[1]); - } else { - // Fallback parsing - const lines = stdout.split('\n'); - const resultLine = lines.find((line: string) => line.includes('Result:')); - if (resultLine) { - installStatus = JSON.parse(resultLine.split('Result:')[1].trim()); - } else { - throw new Error('Could not find Result in output'); - } - } - } catch (parseError) { - console.error('[detect-and-install-packages] Failed to parse install result:', parseError); - console.error('[detect-and-install-packages] stdout:', installResult.logs.stdout.join('')); - // Fallback to assuming all packages were installed - installStatus = { - installed: status.missing, - failed: [], - returncode: 0 - }; + console.log('[detect-and-install-packages] Install stdout:', stdout); + if (stderr) { + console.log('[detect-and-install-packages] Install stderr:', stderr); } - if (installStatus.failed.length > 0) { - console.error('[detect-and-install-packages] Failed to install:', installStatus.failed); + // Verify installation + const finalInstalled: string[] = []; + const failed: string[] = []; + + for (const packageName of missing) { + try { + const verifyResult = await global.activeSandbox.runCommand({ + cmd: 'test', + args: ['-d', `node_modules/${packageName}`] + }); + + if (verifyResult.exitCode === 0) { + finalInstalled.push(packageName); + console.log(`✓ Verified installation of ${packageName}`); + } else { + failed.push(packageName); + console.log(`✗ Failed to verify installation of ${packageName}`); + } + } catch (error) { + failed.push(packageName); + console.log(`✗ Error verifying ${packageName}:`, error); + } + } + + if (failed.length > 0) { + console.error('[detect-and-install-packages] Failed to install:', failed); } return NextResponse.json({ success: true, - packagesInstalled: installStatus.installed, - packagesFailed: installStatus.failed, - packagesAlreadyInstalled: status.installed, - message: `Installed ${installStatus.installed.length} packages`, - logs: installResult.logs.stdout.join('\n') + packagesInstalled: finalInstalled, + packagesFailed: failed, + packagesAlreadyInstalled: installed, + message: `Installed ${finalInstalled.length} packages`, + logs: stdout }); } catch (error) { diff --git a/app/api/generate-ai-code-stream/route.ts b/app/api/generate-ai-code-stream/route.ts index eaae15d..9a7fa4a 100644 --- a/app/api/generate-ai-code-stream/route.ts +++ b/app/api/generate-ai-code-stream/route.ts @@ -11,6 +11,9 @@ import { FileManifest } from '@/types/file-manifest'; import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation'; import { appConfig } from '@/config/app.config'; +// Force dynamic route to enable streaming +export const dynamic = 'force-dynamic'; + const groq = createGroq({ apiKey: process.env.GROQ_API_KEY, }); @@ -1156,9 +1159,21 @@ CRITICAL: When files are provided in the context: const isGoogle = model.startsWith('google/'); const isOpenAI = model.startsWith('openai/gpt-5'); const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq)); - const actualModel = isAnthropic ? model.replace('anthropic/', '') : - (model === 'openai/gpt-5') ? 'gpt-5' : - (isGoogle ? model.replace('google/', '') : model); + + // Fix model name transformation for different providers + let actualModel: string; + if (isAnthropic) { + actualModel = model.replace('anthropic/', ''); + } else if (model === 'openai/gpt-5') { + actualModel = 'gpt-5'; + } else if (isGoogle) { + // Google uses specific model names - convert our naming to theirs + actualModel = model.replace('google/', ''); + } else { + actualModel = model; + } + + console.log(`[generate-ai-code-stream] Using provider: ${isAnthropic ? 'Anthropic' : isGoogle ? 'Google' : isOpenAI ? 'OpenAI' : 'Groq'}, model: ${actualModel}`); // Make streaming API call with appropriate provider const streamOptions: any = { @@ -1243,7 +1258,28 @@ It's better to have 3 complete files than 10 incomplete files.` }; } - const result = await streamText(streamOptions); + let result; + try { + result = await streamText(streamOptions); + } catch (streamError) { + console.error('[generate-ai-code-stream] Error calling streamText:', streamError); + + // Send specific error for debugging + await sendProgress({ + type: 'error', + message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'Groq'} streaming: ${(streamError as Error).message}` + }); + + // If this is a Google model error, provide helpful info + if (isGoogle) { + await sendProgress({ + type: 'info', + message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.' + }); + } + + throw streamError; + } // Stream the response and parse in real-time let generatedCode = ''; @@ -1715,12 +1751,18 @@ Provide the complete file content without any truncation. Include all necessary } })(); - // Return the stream + // Return the stream with proper headers for streaming support return new Response(stream.readable, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', + 'Transfer-Encoding': 'chunked', + 'Content-Encoding': 'none', // Prevent compression that can break streaming + 'X-Accel-Buffering': 'no', // Disable nginx buffering + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }); diff --git a/app/api/get-sandbox-files/route.ts b/app/api/get-sandbox-files/route.ts index d892046..9dad8b1 100644 --- a/app/api/get-sandbox-files/route.ts +++ b/app/api/get-sandbox-files/route.ts @@ -18,58 +18,81 @@ export async function GET() { console.log('[get-sandbox-files] Fetching and analyzing file structure...'); - // Get all React/JS/CSS files - const result = await global.activeSandbox.runCode(` -import os -import json - -def get_files_content(directory='/home/user/app', extensions=['.jsx', '.js', '.tsx', '.ts', '.css', '.json']): - files_content = {} + // Get list of all relevant files + const findResult = await global.activeSandbox.runCommand({ + cmd: 'find', + args: [ + '.', + '-name', 'node_modules', '-prune', '-o', + '-name', '.git', '-prune', '-o', + '-name', 'dist', '-prune', '-o', + '-name', 'build', '-prune', '-o', + '-type', 'f', + '(', + '-name', '*.jsx', + '-o', '-name', '*.js', + '-o', '-name', '*.tsx', + '-o', '-name', '*.ts', + '-o', '-name', '*.css', + '-o', '-name', '*.json', + ')', + '-print' + ] + }); - for root, dirs, files in os.walk(directory): - # Skip node_modules and other unwanted directories - dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'dist', 'build']] + if (findResult.exitCode !== 0) { + throw new Error('Failed to list files'); + } + + const fileList = (await findResult.stdout()).split('\n').filter(f => f.trim()); + console.log('[get-sandbox-files] Found', fileList.length, 'files'); + + // Read content of each file (limit to reasonable sizes) + const filesContent: Record = {}; + + for (const filePath of fileList) { + try { + // Check file size first + const statResult = await global.activeSandbox.runCommand({ + cmd: 'stat', + args: ['-f', '%z', filePath] + }); - for file in files: - if any(file.endswith(ext) for ext in extensions): - file_path = os.path.join(root, file) - relative_path = os.path.relpath(file_path, '/home/user/app') - - try: - with open(file_path, 'r') as f: - content = f.read() - # Only include files under 10KB to avoid huge responses - if len(content) < 10000: - files_content[relative_path] = content - except: - pass + if (statResult.exitCode === 0) { + const fileSize = parseInt(await statResult.stdout()); + + // Only read files smaller than 10KB + if (fileSize < 10000) { + const catResult = await global.activeSandbox.runCommand({ + cmd: 'cat', + args: [filePath] + }); + + if (catResult.exitCode === 0) { + const content = await catResult.stdout(); + // Remove leading './' from path + const relativePath = filePath.replace(/^\.\//, ''); + filesContent[relativePath] = content; + } + } + } + } catch (error) { + // Skip files that can't be read + continue; + } + } - return files_content - -# Get the files -files = get_files_content() - -# Also get the directory structure -structure = [] -for root, dirs, files in os.walk('/home/user/app'): - level = root.replace('/home/user/app', '').count(os.sep) - indent = ' ' * 2 * level - structure.append(f"{indent}{os.path.basename(root)}/") - sub_indent = ' ' * 2 * (level + 1) - for file in files: - if not any(skip in root for skip in ['node_modules', '.git', 'dist', 'build']): - structure.append(f"{sub_indent}{file}") - -result = { - 'files': files, - 'structure': '\\n'.join(structure[:50]) # Limit structure to 50 lines -} - -print(json.dumps(result)) - `); - - const output = result.logs.stdout.join(''); - const parsedResult = JSON.parse(output); + // Get directory structure + const treeResult = await global.activeSandbox.runCommand({ + cmd: 'find', + args: ['.', '-type', 'd', '-not', '-path', '*/node_modules*', '-not', '-path', '*/.git*'] + }); + + let structure = ''; + if (treeResult.exitCode === 0) { + const dirs = (await treeResult.stdout()).split('\n').filter(d => d.trim()); + structure = dirs.slice(0, 50).join('\n'); // Limit to 50 lines + } // Build enhanced file manifest const fileManifest: FileManifest = { @@ -82,12 +105,12 @@ print(json.dumps(result)) }; // Process each file - for (const [relativePath, content] of Object.entries(parsedResult.files)) { - const fullPath = `/home/user/app/${relativePath}`; + for (const [relativePath, content] of Object.entries(filesContent)) { + const fullPath = `/${relativePath}`; // Create base file info const fileInfo: FileInfo = { - content: content as string, + content: content, type: 'utility', path: fullPath, relativePath, @@ -96,7 +119,7 @@ print(json.dumps(result)) // Parse JavaScript/JSX files if (relativePath.match(/\.(jsx?|tsx?)$/)) { - const parseResult = parseJavaScriptFile(content as string, fullPath); + const parseResult = parseJavaScriptFile(content, fullPath); Object.assign(fileInfo, parseResult); // Identify entry point @@ -132,9 +155,9 @@ print(json.dumps(result)) return NextResponse.json({ success: true, - files: parsedResult.files, - structure: parsedResult.structure, - fileCount: Object.keys(parsedResult.files).length, + files: filesContent, + structure, + fileCount: Object.keys(filesContent).length, manifest: fileManifest, }); diff --git a/app/api/install-packages/route.ts b/app/api/install-packages/route.ts index 59d305e..dd8eb82 100644 --- a/app/api/install-packages/route.ts +++ b/app/api/install-packages/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Sandbox } from '@e2b/code-interpreter'; declare global { var activeSandbox: any; @@ -36,23 +35,8 @@ export async function POST(request: NextRequest) { console.log(`[install-packages] Cleaned:`, validPackages); } - // Try to get sandbox - either from global or reconnect - let sandbox = global.activeSandbox; - - if (!sandbox && sandboxId) { - console.log(`[install-packages] Reconnecting to sandbox ${sandboxId}...`); - try { - sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY }); - global.activeSandbox = sandbox; - console.log(`[install-packages] Successfully reconnected to sandbox ${sandboxId}`); - } catch (error) { - console.error(`[install-packages] Failed to reconnect to sandbox:`, error); - return NextResponse.json({ - success: false, - error: `Failed to reconnect to sandbox: ${(error as Error).message}` - }, { status: 500 }); - } - } + // Get active sandbox + const sandbox = global.activeSandbox; if (!sandbox) { return NextResponse.json({ @@ -61,7 +45,7 @@ export async function POST(request: NextRequest) { }, { status: 400 }); } - console.log('[install-packages] Installing packages:', packages); + console.log('[install-packages] Installing packages:', validPackages); // Create a response stream for real-time updates const encoder = new TextEncoder(); @@ -83,23 +67,20 @@ export async function POST(request: NextRequest) { packages: validPackages }); - // Kill any existing Vite process first + // Stop any existing development server first await sendProgress({ type: 'status', message: 'Stopping development server...' }); - await sandboxInstance.runCode(` -import subprocess -import os -import signal - -# Try to kill any existing Vite process -try: - with open('/tmp/vite-process.pid', 'r') as f: - pid = int(f.read().strip()) - os.kill(pid, signal.SIGTERM) - print("Stopped existing Vite process") -except: - print("No existing Vite process found") - `); + try { + // Try to kill any running dev server processes + await sandboxInstance.runCommand({ + cmd: 'pkill', + args: ['-f', 'vite'] + }); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a bit + } catch (error) { + // It's OK if no process is found + console.log('[install-packages] No existing dev server found'); + } // Check which packages are already installed await sendProgress({ @@ -107,70 +88,51 @@ except: message: 'Checking installed packages...' }); - const checkResult = await sandboxInstance.runCode(` -import os -import json - -os.chdir('/home/user/app') - -# Read package.json to check installed packages -try: - with open('package.json', 'r') as f: - package_json = json.load(f) - - dependencies = package_json.get('dependencies', {}) - dev_dependencies = package_json.get('devDependencies', {}) - all_deps = {**dependencies, **dev_dependencies} - - # Check which packages need to be installed - packages_to_check = ${JSON.stringify(validPackages)} - already_installed = [] - need_install = [] - - for pkg in packages_to_check: - # Handle scoped packages - if pkg.startswith('@'): - pkg_name = pkg - else: - # Extract package name without version - pkg_name = pkg.split('@')[0] - - if pkg_name in all_deps: - already_installed.append(pkg_name) - else: - need_install.append(pkg) - - print(f"Already installed: {already_installed}") - print(f"Need to install: {need_install}") - print(f"NEED_INSTALL:{json.dumps(need_install)}") - -except Exception as e: - print(f"Error checking packages: {e}") - print(f"NEED_INSTALL:{json.dumps(packages_to_check)}") - `); - - // Parse packages that need installation let packagesToInstall = validPackages; - // Check if checkResult has the expected structure - if (checkResult && checkResult.results && checkResult.results[0] && checkResult.results[0].text) { - const outputLines = checkResult.results[0].text.split('\n'); - for (const line of outputLines) { - if (line.startsWith('NEED_INSTALL:')) { - try { - packagesToInstall = JSON.parse(line.substring('NEED_INSTALL:'.length)); - } catch (e) { - console.error('Failed to parse packages to install:', e); + try { + // Read package.json to check existing dependencies + const catResult = await sandboxInstance.runCommand({ + cmd: 'cat', + args: ['package.json'] + }); + if (catResult.exitCode === 0) { + const packageJsonContent = await catResult.stdout(); + const packageJson = JSON.parse(packageJsonContent); + + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + const allDeps = { ...dependencies, ...devDependencies }; + + const alreadyInstalled = []; + const needInstall = []; + + for (const pkg of validPackages) { + // Handle scoped packages + const pkgName = pkg.startsWith('@') ? pkg : pkg.split('@')[0]; + + if (allDeps[pkgName]) { + alreadyInstalled.push(pkgName); + } else { + needInstall.push(pkg); } } + + packagesToInstall = needInstall; + + if (alreadyInstalled.length > 0) { + await sendProgress({ + type: 'info', + message: `Already installed: ${alreadyInstalled.join(', ')}` + }); + } } - } else { - console.error('[install-packages] Invalid checkResult structure:', checkResult); + } catch (error) { + console.error('[install-packages] Error checking existing packages:', error); // If we can't check, just try to install all packages packagesToInstall = validPackages; } - if (packagesToInstall.length === 0) { await sendProgress({ type: 'success', @@ -178,164 +140,104 @@ except Exception as e: installedPackages: [], alreadyInstalled: validPackages }); + + // Restart dev server + await sendProgress({ type: 'status', message: 'Restarting development server...' }); + + const devServerProcess = await sandboxInstance.runCommand({ + cmd: 'npm', + args: ['run', 'dev'], + detached: true + }); + + await sendProgress({ + type: 'complete', + message: 'Dev server restarted!', + installedPackages: [] + }); + return; } // Install only packages that aren't already installed - const packageList = packagesToInstall.join(' '); - // Only send the npm install command message if we're actually installing new packages await sendProgress({ type: 'info', message: `Installing ${packagesToInstall.length} new package(s): ${packagesToInstall.join(', ')}` }); - const installResult = await sandboxInstance.runCode(` -import subprocess -import os - -os.chdir('/home/user/app') - -# Run npm install with output capture -packages_to_install = ${JSON.stringify(packagesToInstall)} -cmd_args = ['npm', 'install', '--legacy-peer-deps'] + packages_to_install - -print(f"Running command: {' '.join(cmd_args)}") - -process = subprocess.Popen( - cmd_args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True -) - -# Stream output -while True: - output = process.stdout.readline() - if output == '' and process.poll() is not None: - break - if output: - print(output.strip()) - -# Get the return code -rc = process.poll() - -# Capture any stderr -stderr = process.stderr.read() -if stderr: - print("STDERR:", stderr) - if 'ERESOLVE' in stderr: - print("ERESOLVE_ERROR: Dependency conflict detected - using --legacy-peer-deps flag") - -print(f"\\nInstallation completed with code: {rc}") - -# Verify packages were installed -import json -with open('/home/user/app/package.json', 'r') as f: - package_json = json.load(f) - -installed = [] -for pkg in ${JSON.stringify(packagesToInstall)}: - if pkg in package_json.get('dependencies', {}): - installed.append(pkg) - print(f"✓ Verified {pkg}") - else: - print(f"✗ Package {pkg} not found in dependencies") + // Run npm install + const installArgs = ['install', '--legacy-peer-deps', ...packagesToInstall]; + const installResult = await sandboxInstance.runCommand({ + cmd: 'npm', + args: installArgs + }); -print(f"\\nVerified installed packages: {installed}") - `, { timeout: 60000 }); // 60 second timeout for npm install + // Get install output + const stdout = await installResult.stdout(); + const stderr = await installResult.stderr(); - // Send npm output - const output = installResult?.output || installResult?.logs?.stdout?.join('\n') || ''; - const npmOutputLines = output.split('\n').filter((line: string) => line.trim()); - for (const line of npmOutputLines) { - if (line.includes('STDERR:')) { - const errorMsg = line.replace('STDERR:', '').trim(); - if (errorMsg && errorMsg !== 'undefined') { - await sendProgress({ type: 'error', message: errorMsg }); + if (stdout) { + const lines = stdout.split('\n').filter(line => line.trim()); + for (const line of lines) { + if (line.includes('npm WARN')) { + await sendProgress({ type: 'warning', message: line }); + } else if (line.trim()) { + await sendProgress({ type: 'output', message: line }); } - } else if (line.includes('ERESOLVE_ERROR:')) { - const msg = line.replace('ERESOLVE_ERROR:', '').trim(); - await sendProgress({ - type: 'warning', - message: `Dependency conflict resolved with --legacy-peer-deps: ${msg}` - }); - } else if (line.includes('npm WARN')) { - await sendProgress({ type: 'warning', message: line }); - } else if (line.trim() && !line.includes('undefined')) { - await sendProgress({ type: 'output', message: line }); } } - // Check if installation was successful - const installedMatch = output.match(/Verified installed packages: \[(.*?)\]/); - let installedPackages: string[] = []; - - if (installedMatch && installedMatch[1]) { - installedPackages = installedMatch[1] - .split(',') - .map((p: string) => p.trim().replace(/'/g, '')) - .filter((p: string) => p.length > 0); + if (stderr) { + const errorLines = stderr.split('\n').filter(line => line.trim()); + for (const line of errorLines) { + if (line.includes('ERESOLVE')) { + await sendProgress({ + type: 'warning', + message: `Dependency conflict resolved with --legacy-peer-deps: ${line}` + }); + } else if (line.trim()) { + await sendProgress({ type: 'error', message: line }); + } + } } - if (installedPackages.length > 0) { + if (installResult.exitCode === 0) { await sendProgress({ type: 'success', - message: `Successfully installed: ${installedPackages.join(', ')}`, - installedPackages + message: `Successfully installed: ${packagesToInstall.join(', ')}`, + installedPackages: packagesToInstall }); } else { await sendProgress({ type: 'error', - message: 'Failed to verify package installation' + message: 'Package installation failed' }); } - // Restart Vite dev server + // Restart development server await sendProgress({ type: 'status', message: 'Restarting development server...' }); - await sandboxInstance.runCode(` -import subprocess -import os -import time - -os.chdir('/home/user/app') - -# Kill any existing Vite processes -subprocess.run(['pkill', '-f', 'vite'], capture_output=True) -time.sleep(1) - -# Start Vite dev server -env = os.environ.copy() -env['FORCE_COLOR'] = '0' - -process = subprocess.Popen( - ['npm', 'run', 'dev'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env -) - -print(f'✓ Vite dev server restarted with PID: {process.pid}') - -# Store process info for later -with open('/tmp/vite-process.pid', 'w') as f: - f.write(str(process.pid)) - -# Wait a bit for Vite to start up -time.sleep(3) - -# Touch files to trigger Vite reload -subprocess.run(['touch', '/home/user/app/package.json']) -subprocess.run(['touch', '/home/user/app/vite.config.js']) - -print("Vite restarted and should now recognize all packages") - `); - - await sendProgress({ - type: 'complete', - message: 'Package installation complete and dev server restarted!', - installedPackages - }); + try { + const devServerProcess = await sandboxInstance.runCommand({ + cmd: 'npm', + args: ['run', 'dev'], + detached: true + }); + + // Wait a bit for the server to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + await sendProgress({ + type: 'complete', + message: 'Package installation complete and dev server restarted!', + installedPackages: packagesToInstall + }); + } catch (error) { + await sendProgress({ + type: 'error', + message: `Failed to restart dev server: ${(error as Error).message}` + }); + } } catch (error) { const errorMessage = (error as Error).message; diff --git a/app/api/kill-sandbox/route.ts b/app/api/kill-sandbox/route.ts index 70d005a..accaf7e 100644 --- a/app/api/kill-sandbox/route.ts +++ b/app/api/kill-sandbox/route.ts @@ -8,18 +8,18 @@ declare global { export async function POST() { try { - console.log('[kill-sandbox] Killing active sandbox...'); + console.log('[kill-sandbox] Stopping active sandbox...'); let sandboxKilled = false; - // Kill existing sandbox if any + // Stop existing sandbox if any if (global.activeSandbox) { try { - await global.activeSandbox.close(); + await global.activeSandbox.stop(); sandboxKilled = true; - console.log('[kill-sandbox] Sandbox closed successfully'); + console.log('[kill-sandbox] Sandbox stopped successfully'); } catch (e) { - console.error('[kill-sandbox] Failed to close sandbox:', e); + console.error('[kill-sandbox] Failed to stop sandbox:', e); } global.activeSandbox = null; global.sandboxData = null; diff --git a/app/api/monitor-vite-logs/route.ts b/app/api/monitor-vite-logs/route.ts index ef537f0..3cb2a9b 100644 --- a/app/api/monitor-vite-logs/route.ts +++ b/app/api/monitor-vite-logs/route.ts @@ -15,97 +15,100 @@ export async function GET() { console.log('[monitor-vite-logs] Checking Vite process logs...'); - // Check both the error file and recent logs - const result = await global.activeSandbox.runCode(` -import json -import subprocess -import re - -errors = [] - -# First check the error file -try: - with open('/tmp/vite-errors.json', 'r') as f: - data = json.load(f) - errors.extend(data.get('errors', [])) -except: - pass - -# Also check if we can get recent Vite logs -try: - # Try to get the Vite process PID - with open('/tmp/vite-process.pid', 'r') as f: - pid = int(f.read().strip()) + const errors: any[] = []; - # Check if process is still running and get its logs - # This is a bit hacky but works for our use case - result = subprocess.run(['ps', '-p', str(pid)], capture_output=True, text=True) - if result.returncode == 0: - # Process is running, try to check for errors in output - # Note: We can't easily get stdout/stderr from a running process - # but we can check if there are new errors - pass -except: - pass - -# Also scan the current console output for any HMR errors -# This won't catch everything but helps with recent errors -try: - # Check if there's a log file we can read - import os - log_files = [] - for root, dirs, files in os.walk('/tmp'): - for file in files: - if 'vite' in file.lower() and file.endswith('.log'): - log_files.append(os.path.join(root, file)) + // Check if there's an error file from previous runs + try { + const catResult = await global.activeSandbox.runCommand({ + cmd: 'cat', + args: ['/tmp/vite-errors.json'] + }); + + if (catResult.exitCode === 0) { + const errorFileContent = await catResult.stdout(); + const data = JSON.parse(errorFileContent); + errors.push(...(data.errors || [])); + } + } catch (error) { + // No error file exists, that's OK + } - for log_file in log_files[:5]: # Check up to 5 log files - try: - with open(log_file, 'r') as f: - content = f.read() - # Look for import errors - import_errors = re.findall(r'Failed to resolve import "([^"]+)"', content) - for pkg in import_errors: - if not pkg.startswith('.'): - # Extract base package name - if pkg.startswith('@'): - parts = pkg.split('/') - final_pkg = '/'.join(parts[:2]) if len(parts) >= 2 else pkg - else: - final_pkg = pkg.split('/')[0] - - error_obj = { - "type": "npm-missing", - "package": final_pkg, - "message": f"Failed to resolve import \\"{pkg}\\"", - "file": "Unknown" - } - - # Avoid duplicates - if not any(e['package'] == error_obj['package'] for e in errors): - errors.append(error_obj) - except: - pass -except Exception as e: - print(f"Error scanning logs: {e}") - -# Deduplicate errors -unique_errors = [] -seen_packages = set() -for error in errors: - if error.get('package') and error['package'] not in seen_packages: - seen_packages.add(error['package']) - unique_errors.append(error) - -print(json.dumps({"errors": unique_errors})) - `, { timeout: 5000 }); + // Look for any Vite-related log files that might contain errors + try { + const findResult = await global.activeSandbox.runCommand({ + cmd: 'find', + args: ['/tmp', '-name', '*vite*', '-type', 'f'] + }); + + if (findResult.exitCode === 0) { + const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim()); + + for (const logFile of logFiles.slice(0, 3)) { + try { + const grepResult = await global.activeSandbox.runCommand({ + cmd: 'grep', + args: ['-i', 'failed to resolve import', logFile] + }); + + if (grepResult.exitCode === 0) { + const errorLines = (await grepResult.stdout()).split('\n').filter(line => line.trim()); + + for (const line of errorLines) { + // Extract package name from error line + const importMatch = line.match(/"([^"]+)"/); + if (importMatch) { + const importPath = importMatch[1]; + + // Skip relative imports + if (!importPath.startsWith('.')) { + // Extract base package name + let packageName; + if (importPath.startsWith('@')) { + const parts = importPath.split('/'); + packageName = parts.length >= 2 ? parts.slice(0, 2).join('/') : importPath; + } else { + packageName = importPath.split('/')[0]; + } + + const errorObj = { + type: "npm-missing", + package: packageName, + message: `Failed to resolve import "${importPath}"`, + file: "Unknown" + }; + + // Avoid duplicates + if (!errors.some(e => e.package === errorObj.package)) { + errors.push(errorObj); + } + } + } + } + } + } catch (error) { + // Skip if grep fails + } + } + } + } catch (error) { + // No log files found, that's OK + } - const data = JSON.parse(result.output || '{"errors": []}'); + // Deduplicate errors by package name + const uniqueErrors: any[] = []; + const seenPackages = new Set(); + + for (const error of errors) { + if (error.package && !seenPackages.has(error.package)) { + seenPackages.add(error.package); + uniqueErrors.push(error); + } + } return NextResponse.json({ success: true, - hasErrors: data.errors.length > 0, - errors: data.errors + hasErrors: uniqueErrors.length > 0, + errors: uniqueErrors }); } catch (error) { diff --git a/app/api/restart-vite/route.ts b/app/api/restart-vite/route.ts index ca6b4ba..64bf973 100644 --- a/app/api/restart-vite/route.ts +++ b/app/api/restart-vite/route.ts @@ -15,115 +15,45 @@ export async function POST() { console.log('[restart-vite] Forcing Vite restart...'); - // Kill existing Vite process and restart - const result = await global.activeSandbox.runCode(` -import subprocess -import os -import signal -import time -import threading -import json -import sys - -# Kill existing Vite process -try: - with open('/tmp/vite-process.pid', 'r') as f: - pid = int(f.read().strip()) - os.kill(pid, signal.SIGTERM) - print("Killed existing Vite process") - time.sleep(1) -except: - print("No existing Vite process found") - -os.chdir('/home/user/app') - -# Clear error file -error_file = '/tmp/vite-errors.json' -with open(error_file, 'w') as f: - json.dump({"errors": [], "lastChecked": time.time()}, f) - -# Function to monitor Vite output for errors -def monitor_output(proc, error_file): - while True: - line = proc.stderr.readline() - if not line: - break - - sys.stdout.write(line) # Also print to console - - # Check for import resolution errors - if "Failed to resolve import" in line: - try: - # Extract package name from error - import_match = line.find('"') - if import_match != -1: - end_match = line.find('"', import_match + 1) - if end_match != -1: - package_name = line[import_match + 1:end_match] - # Skip relative imports - if not package_name.startswith('.'): - with open(error_file, 'r') as f: - data = json.load(f) - - # Handle scoped packages correctly - if package_name.startswith('@'): - # For @scope/package, keep the scope - pkg_parts = package_name.split('/') - if len(pkg_parts) >= 2: - final_package = '/'.join(pkg_parts[:2]) - else: - final_package = package_name - else: - # For regular packages, just take the first part - final_package = package_name.split('/')[0] - - error_obj = { - "type": "npm-missing", - "package": final_package, - "message": line.strip(), - "timestamp": time.time() - } - - # Avoid duplicates - if not any(e['package'] == error_obj['package'] for e in data['errors']): - data['errors'].append(error_obj) - - with open(error_file, 'w') as f: - json.dump(data, f) - - print(f"WARNING: Detected missing package: {error_obj['package']}") - except Exception as e: - print(f"Error parsing Vite error: {e}") - -# Start Vite with error monitoring -process = subprocess.Popen( - ['npm', 'run', 'dev'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1 -) - -# Start monitoring thread -monitor_thread = threading.Thread(target=monitor_output, args=(process, error_file)) -monitor_thread.daemon = True -monitor_thread.start() - -print("Vite restarted successfully!") - -# Store process info for later -with open('/tmp/vite-process.pid', 'w') as f: - f.write(str(process.pid)) - -# Wait for Vite to fully start -time.sleep(5) -print("Vite is ready") - `); + // Kill existing Vite processes + try { + await global.activeSandbox.runCommand({ + cmd: 'pkill', + args: ['-f', 'vite'] + }); + console.log('[restart-vite] Killed existing Vite processes'); + + // Wait a moment for processes to terminate + await new Promise(resolve => setTimeout(resolve, 2000)); + } catch (error) { + console.log('[restart-vite] No existing Vite processes found'); + } + + // Clear any error tracking files + try { + await global.activeSandbox.runCommand({ + cmd: 'bash', + args: ['-c', 'echo \'{"errors": [], "lastChecked": '+ Date.now() +'}\' > /tmp/vite-errors.json'] + }); + } catch (error) { + // Ignore if this fails + } + + // Start Vite dev server in detached mode + const viteProcess = await global.activeSandbox.runCommand({ + cmd: 'npm', + args: ['run', 'dev'], + detached: true + }); + + console.log('[restart-vite] Vite dev server restarted'); + + // Wait for Vite to start up + await new Promise(resolve => setTimeout(resolve, 3000)); return NextResponse.json({ success: true, - message: 'Vite restarted successfully', - output: result.output + message: 'Vite restarted successfully' }); } catch (error) { diff --git a/app/api/run-command/route.ts b/app/api/run-command/route.ts index 53e7e7b..76ffaff 100644 --- a/app/api/run-command/route.ts +++ b/app/api/run-command/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from 'next/server'; -import { Sandbox } from '@e2b/code-interpreter'; // Get active sandbox from global state (in production, use a proper state management solution) declare global { @@ -26,30 +25,32 @@ export async function POST(request: NextRequest) { console.log(`[run-command] Executing: ${command}`); - const result = await global.activeSandbox.runCode(` -import subprocess -import os - -os.chdir('/home/user/app') -result = subprocess.run(${JSON.stringify(command.split(' '))}, - capture_output=True, - text=True, - shell=False) - -print("STDOUT:") -print(result.stdout) -if result.stderr: - print("\\nSTDERR:") - print(result.stderr) -print(f"\\nReturn code: {result.returncode}") - `); + // Parse command and arguments + const commandParts = command.trim().split(/\s+/); + const cmd = commandParts[0]; + const args = commandParts.slice(1); - const output = result.logs.stdout.join('\n'); + // Execute command using Vercel Sandbox + const result = await global.activeSandbox.runCommand({ + cmd, + args + }); + + // Get output streams + const stdout = await result.stdout(); + const stderr = await result.stderr(); + + const output = [ + stdout ? `STDOUT:\n${stdout}` : '', + stderr ? `\nSTDERR:\n${stderr}` : '', + `\nExit code: ${result.exitCode}` + ].filter(Boolean).join(''); return NextResponse.json({ success: true, output, - message: 'Command executed successfully' + exitCode: result.exitCode, + message: result.exitCode === 0 ? 'Command executed successfully' : 'Command completed with non-zero exit code' }); } catch (error) { diff --git a/app/api/sandbox-logs/route.ts b/app/api/sandbox-logs/route.ts index 84d0208..177c370 100644 --- a/app/api/sandbox-logs/route.ts +++ b/app/api/sandbox-logs/route.ts @@ -15,55 +15,70 @@ export async function GET(request: NextRequest) { console.log('[sandbox-logs] Fetching Vite dev server logs...'); - // Get the last N lines of the Vite dev server output - const result = await global.activeSandbox.runCode(` -import subprocess -import os - -# Try to get the Vite process output -try: - # Read the last 100 lines of any log files - log_content = [] + // Check if Vite processes are running + const psResult = await global.activeSandbox.runCommand({ + cmd: 'ps', + args: ['aux'] + }); - # Check if there are any node processes running - ps_result = subprocess.run(['ps', 'aux'], capture_output=True, text=True) - vite_processes = [line for line in ps_result.stdout.split('\\n') if 'vite' in line.lower()] + let viteRunning = false; + let logContent: string[] = []; - if vite_processes: - log_content.append("Vite is running") - else: - log_content.append("Vite process not found") - - # Try to capture recent console output (this is a simplified approach) - # In a real implementation, you'd want to capture the Vite process output directly - print(json.dumps({ - "hasErrors": False, - "logs": log_content, - "status": "running" if vite_processes else "stopped" - })) -except Exception as e: - print(json.dumps({ - "hasErrors": True, - "logs": [str(e)], - "status": "error" - })) - `); - - try { - const logData = JSON.parse(result.output || '{}'); - return NextResponse.json({ - success: true, - ...logData - }); - } catch { - return NextResponse.json({ - success: true, - hasErrors: false, - logs: [result.output], - status: 'unknown' - }); + if (psResult.exitCode === 0) { + const psOutput = await psResult.stdout(); + const viteProcesses = psOutput.split('\n').filter(line => + line.toLowerCase().includes('vite') || + line.toLowerCase().includes('npm run dev') + ); + + viteRunning = viteProcesses.length > 0; + + if (viteRunning) { + logContent.push("Vite is running"); + logContent.push(...viteProcesses.slice(0, 3)); // Show first 3 processes + } else { + logContent.push("Vite process not found"); + } } + // Try to read any recent log files + try { + const findResult = await global.activeSandbox.runCommand({ + cmd: 'find', + args: ['/tmp', '-name', '*vite*', '-name', '*.log', '-type', 'f'] + }); + + if (findResult.exitCode === 0) { + const logFiles = (await findResult.stdout()).split('\n').filter(f => f.trim()); + + for (const logFile of logFiles.slice(0, 2)) { + try { + const catResult = await global.activeSandbox.runCommand({ + cmd: 'tail', + args: ['-n', '10', logFile] + }); + + if (catResult.exitCode === 0) { + const logFileContent = await catResult.stdout(); + logContent.push(`--- ${logFile} ---`); + logContent.push(logFileContent); + } + } catch (error) { + // Skip if can't read log file + } + } + } + } catch (error) { + // No log files found, that's OK + } + + return NextResponse.json({ + success: true, + hasErrors: false, + logs: logContent, + status: viteRunning ? 'running' : 'stopped' + }); + } catch (error) { console.error('[sandbox-logs] Error:', error); return NextResponse.json({ diff --git a/components/SandboxPreview.tsx b/components/SandboxPreview.tsx index 3808c26..d5009ca 100644 --- a/components/SandboxPreview.tsx +++ b/components/SandboxPreview.tsx @@ -22,9 +22,10 @@ export default function SandboxPreview({ useEffect(() => { if (sandboxId && type !== 'console') { - // In production, this would be the actual E2B sandbox URL - // Format: https://{sandboxId}-{port}.e2b.dev - setPreviewUrl(`https://${sandboxId}-${port}.e2b.dev`); + // For Vercel Sandbox, we'll receive the full URL from the API + // The URL format is determined by Vercel Sandbox's domain() method + // This is just a fallback format - actual URL comes from sandbox.domain(port) + setPreviewUrl(`https://${sandboxId}.vercel-sandbox.dev`); } }, [sandboxId, port, type]); diff --git a/config/app.config.ts b/config/app.config.ts index 500777d..1a85a3a 100644 --- a/config/app.config.ts +++ b/config/app.config.ts @@ -2,27 +2,30 @@ // This file contains all configurable settings for the application export const appConfig = { - // E2B Sandbox Configuration - e2b: { + // Vercel Sandbox Configuration + vercelSandbox: { // Sandbox timeout in minutes timeoutMinutes: 15, - // Convert to milliseconds for E2B API + // Convert to milliseconds for Vercel Sandbox API get timeoutMs() { return this.timeoutMinutes * 60 * 1000; }, - // Vite development server port - vitePort: 5173, + // Development server port (Vercel Sandbox typically uses 3000 for Next.js/React) + devPort: 3000, - // Time to wait for Vite to be ready (in milliseconds) - viteStartupDelay: 7000, + // Time to wait for dev server to be ready (in milliseconds) + devServerStartupDelay: 7000, // Time to wait for CSS rebuild (in milliseconds) cssRebuildDelay: 2000, - // Default sandbox template (if using templates) - defaultTemplate: undefined, // or specify a template ID + // Working directory in sandbox + workingDirectory: '/app', + + // Default runtime for sandbox + runtime: 'node22' // Available: node22, python3.13, v0-next-shadcn, cua-ubuntu-xfce }, // AI Model Configuration @@ -35,7 +38,7 @@ export const appConfig = { 'openai/gpt-5', 'moonshotai/kimi-k2-instruct', 'anthropic/claude-sonnet-4-20250514', - 'google/gemini-2.5-pro' + 'google/gemini-2.0-flash-exp' ], // Model display names @@ -43,7 +46,7 @@ export const appConfig = { 'openai/gpt-5': 'GPT-5', 'moonshotai/kimi-k2-instruct': 'Kimi K2 Instruct', 'anthropic/claude-sonnet-4-20250514': 'Sonnet 4', - 'google/gemini-2.5-pro': 'Gemini 2.5 Pro' + 'google/gemini-2.0-flash-exp': 'Gemini 2.0 Flash (Experimental)' }, // Temperature settings for non-reasoning models diff --git a/docs/PACKAGE_DETECTION_GUIDE.md b/docs/PACKAGE_DETECTION_GUIDE.md index 2a89c36..79243c6 100644 --- a/docs/PACKAGE_DETECTION_GUIDE.md +++ b/docs/PACKAGE_DETECTION_GUIDE.md @@ -1,10 +1,10 @@ # Package Detection and Installation Guide -This document explains how to use the XML-based package detection and installation mechanism in the E2B sandbox environment. +This document explains how to use the XML-based package detection and installation mechanism in the Vercel Sandbox environment. ## Overview -The E2B sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system. +The Vercel Sandbox can automatically detect and install packages from XML tags in AI-generated code responses. This mechanism works alongside the existing file detection system. ## XML Tag Formats @@ -196,43 +196,37 @@ Directly installs packages in the sandbox. 3. **Order matters**: Packages are installed before files are created 4. **Use commands** for post-installation tasks like building or testing -## Integration with E2B Sandbox +## Integration with Vercel Sandbox -The package detection mechanism integrates seamlessly with the E2B sandbox: +The package detection mechanism integrates seamlessly with the Vercel Sandbox: -1. Packages are installed in `/home/user/app/node_modules` -2. The Vite dev server is automatically restarted after package installation +1. Packages are installed in the sandbox's working directory +2. The development server is automatically restarted after package installation 3. All npm operations run within the sandbox environment 4. Package.json is automatically updated with new dependencies -## E2B Command Execution Methods +## Vercel Sandbox Command Execution Methods -### Method 1: Using runCode() with Python subprocess +### Using runCommand() (Recommended) ```javascript -// Current implementation pattern -await global.activeSandbox.runCode(` -import subprocess -import os - -os.chdir('/home/user/app') -result = subprocess.run(['npm', 'install', 'axios'], capture_output=True, text=True) -print(result.stdout) -`); -``` - -### Method 2: Using commands.run() directly (Recommended) -```javascript -// Direct command execution - cleaner approach -const result = await global.activeSandbox.commands.run('npm install axios', { - cwd: '/home/user/app', - timeout: 60000 +// Direct command execution using Vercel Sandbox API +const result = await global.activeSandbox.runCommand({ + cmd: 'npm', + args: ['install', 'axios'] }); -console.log(result.stdout); +const stdout = await result.stdout(); +const stderr = await result.stderr(); +console.log(stdout); ``` ### Command Execution Options -When using `sandbox.commands.run()`, you can specify: +When using `sandbox.runCommand()`, you can specify: +- `cmd`: The command to execute +- `args`: Array of arguments +- `detached`: Run in background (for long-running processes) +- `stdout`: Stream for capturing stdout +- `stderr`: Stream for capturing stderr - `cmd`: Command string to execute - `background`: Run in background (true) or wait for completion (false) - `envs`: Environment variables as key-value pairs diff --git a/package-lock.json b/package-lock.json index 385ab79..c463119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,20 @@ "version": "0.1.0", "dependencies": { "@ai-sdk/anthropic": "^2.0.1", + "@ai-sdk/google": "^2.0.4", "@ai-sdk/groq": "^2.0.0", "@ai-sdk/openai": "^2.0.4", "@anthropic-ai/sdk": "^0.57.0", - "@e2b/code-interpreter": "^1.5.1", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@types/react-syntax-highlighter": "^15.5.13", + "@vercel/sandbox": "^0.0.17", "ai": "^5.0.0", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cors": "^2.8.5", "dotenv": "^17.2.1", - "e2b": "^1.13.2", - "express": "^5.1.0", "framer-motion": "^12.23.12", "groq-sdk": "^0.29.0", "lucide-react": "^0.532.0", @@ -98,6 +97,39 @@ "zod": "^3.25.76 || ^4" } }, + "node_modules/@ai-sdk/google": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-2.0.9.tgz", + "integrity": "sha512-sWhuiRzsVEBS3HOyvqgRiMahLxK2OEBs+x6RZMnibHt4nwHOl8AjJ7UVstJtJHo4U2YX8iaMSJMU+qhX1wRPTA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.6.tgz", + "integrity": "sha512-s1+9okDSqbxKvwf1mqyyqtOY27/RV9O+XTzaRKEamVKbmVBM7BiCSfui7vH7A/1EETECtTJkS2MUL5D3Pw5GFw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, "node_modules/@ai-sdk/groq": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@ai-sdk/groq/-/groq-2.0.0.tgz", @@ -218,43 +250,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bufbuild/protobuf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.2.tgz", - "integrity": "sha512-vLu7SRY84CV/Dd+NUdgtidn2hS5hSMUC1vDBY0VcviTdgRYkU43vIz3vIFbmx14cX1r+mM7WjzE5Fl1fGEM0RQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)" - }, - "node_modules/@connectrpc/connect": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.0.0-rc.3.tgz", - "integrity": "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==", - "license": "Apache-2.0", - "peerDependencies": { - "@bufbuild/protobuf": "^2.2.0" - } - }, - "node_modules/@connectrpc/connect-web": { - "version": "2.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-2.0.0-rc.3.tgz", - "integrity": "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==", - "license": "Apache-2.0", - "peerDependencies": { - "@bufbuild/protobuf": "^2.2.0", - "@connectrpc/connect": "2.0.0-rc.3" - } - }, - "node_modules/@e2b/code-interpreter": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@e2b/code-interpreter/-/code-interpreter-1.5.1.tgz", - "integrity": "sha512-mkyKjAW2KN5Yt0R1I+1lbH3lo+W/g/1+C2lnwlitXk5wqi/g94SEO41XKdmDf5WWpKG3mnxWDR5d6S/lyjmMEw==", - "license": "MIT", - "dependencies": { - "e2b": "^1.4.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "dev": true, @@ -996,6 +991,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.9", "license": "MIT", @@ -1324,6 +1325,42 @@ "darwin" ] }, + "node_modules/@vercel/oidc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-2.0.2.tgz", + "integrity": "sha512-59PBFx3T+k5hLTEWa3ggiMpGRz1OVvl9eN8SUai+A43IsqiOuAe7qPBf+cray/Fj6mkgnxm/D7IAtjc8zSHi7g==", + "license": "Apache-2.0", + "dependencies": { + "@types/ms": "2.1.0", + "ms": "2.1.3" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@vercel/sandbox": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@vercel/sandbox/-/sandbox-0.0.17.tgz", + "integrity": "sha512-oLfYqYKMjwNopjO2BKbVd0uA1dL43jk6yqaeIgquLJG/Jo01FZb5sd6XfGIfFa21XDxXGM61eomX7fSps/hvYg==", + "license": "ISC", + "dependencies": { + "@vercel/oidc": "^2.0.0", + "async-retry": "1.3.3", + "jsonlines": "0.1.1", + "ms": "2.1.3", + "tar-stream": "3.1.7", + "zod": "3.24.4" + } + }, + "node_modules/@vercel/sandbox/node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1336,19 +1373,6 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "dev": true, @@ -1597,6 +1621,15 @@ "node": ">= 0.4" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1670,30 +1703,23 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } + "node_modules/bare-events": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz", + "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==", + "license": "Apache-2.0", + "optional": true }, "node_modules/brace-expansion": { "version": "1.1.12", @@ -1747,15 +1773,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/call-bind": { "version": "1.0.8", "dev": true, @@ -1786,6 +1803,7 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1961,56 +1979,11 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2096,6 +2069,7 @@ }, "node_modules/debug": { "version": "4.4.1", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2155,15 +2129,6 @@ "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/detect-libc": { "version": "2.0.4", "devOptional": true, @@ -2207,29 +2172,6 @@ "node": ">= 0.4" } }, - "node_modules/e2b": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/e2b/-/e2b-1.13.2.tgz", - "integrity": "sha512-m8acE/MzMAJo1A57DakR2X1Sl5Mt1tcQO2aJfygNaQHLXby/4xsjF0UeJUB70jF7xntiR41pAMbZEHnkzrT9tw==", - "license": "MIT", - "dependencies": { - "@bufbuild/protobuf": "^2.6.2", - "@connectrpc/connect": "2.0.0-rc.3", - "@connectrpc/connect-web": "2.0.0-rc.3", - "compare-versions": "^6.1.0", - "openapi-fetch": "^0.9.7", - "platform": "^1.3.6" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.192", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", @@ -2241,15 +2183,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.2", "dev": true, @@ -2428,12 +2361,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "dev": true, @@ -2814,15 +2741,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2841,53 +2759,17 @@ "node": ">=20.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "dev": true, @@ -2980,23 +2862,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "dev": true, @@ -3107,15 +2972,6 @@ "node": ">= 12.20" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3156,15 +3012,6 @@ } } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -3465,31 +3312,6 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -3499,18 +3321,6 @@ "ms": "^2.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -3542,12 +3352,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/internal-slot": { "version": "1.1.0", "dev": true, @@ -3561,15 +3365,6 @@ "node": ">= 0.4" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -3852,12 +3647,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "dev": true, @@ -4067,6 +3856,12 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonlines": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz", + "integrity": "sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==", + "license": "MIT" + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "dev": true, @@ -4231,27 +4026,6 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "dev": true, @@ -4283,27 +4057,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "dev": true, @@ -4410,15 +4163,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/next": { "version": "15.4.3", "license": "MIT", @@ -4559,6 +4303,7 @@ }, "node_modules/object-inspect": { "version": "1.13.4", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4655,42 +4400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/openapi-fetch": { - "version": "0.9.8", - "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz", - "integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==", - "license": "MIT", - "dependencies": { - "openapi-typescript-helpers": "^0.0.8" - } - }, - "node_modules/openapi-typescript-helpers": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz", - "integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g==", - "license": "MIT" - }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -4780,15 +4489,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -4810,15 +4510,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -4834,12 +4525,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/platform": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "license": "MIT" - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "dev": true, @@ -4920,19 +4605,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "dev": true, @@ -4941,21 +4613,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -4975,30 +4632,6 @@ ], "license": "MIT" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "19.1.0", "license": "MIT", @@ -5143,6 +4776,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "dev": true, @@ -5152,22 +4794,6 @@ "node": ">=0.10.0" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -5208,26 +4834,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "dev": true, @@ -5259,12 +4865,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.26.0", "license": "MIT" @@ -5277,43 +4877,6 @@ "semver": "bin/semver.js" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "dev": true, @@ -5357,12 +4920,6 @@ "node": ">= 0.4" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/sharp": { "version": "0.34.3", "hasInstallScript": true, @@ -5436,6 +4993,7 @@ }, "node_modules/side-channel": { "version": "1.1.0", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5453,6 +5011,7 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5467,6 +5026,7 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5483,6 +5043,7 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5528,15 +5089,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "dev": true, @@ -5549,6 +5101,19 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "dev": true, @@ -5761,6 +5326,26 @@ "node": ">=18" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "dev": true, @@ -5787,15 +5372,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5839,20 +5415,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "dev": true, @@ -5956,15 +5518,6 @@ "version": "6.21.0", "license": "MIT" }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/unrs-resolver": { "version": "1.11.1", "dev": true, @@ -6173,12 +5726,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 818cf29..e9c8a66 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test:integration": "node tests/e2b-integration.test.js", + "test:integration": "node tests/vercel-sandbox-integration.test.js", "test:api": "node tests/api-endpoints.test.js", "test:code": "node tests/code-execution.test.js", "test:all": "npm run test:integration && npm run test:api && npm run test:code" @@ -19,7 +19,7 @@ "@ai-sdk/groq": "^2.0.0", "@ai-sdk/openai": "^2.0.4", "@anthropic-ai/sdk": "^0.57.0", - "@e2b/code-interpreter": "^1.5.1", + "@vercel/sandbox": "^0.0.17", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@types/react-syntax-highlighter": "^15.5.13", @@ -29,7 +29,7 @@ "clsx": "^2.1.1", "cors": "^2.8.5", "dotenv": "^17.2.1", - "e2b": "^1.13.2", + "framer-motion": "^12.23.12", "groq-sdk": "^0.29.0", "lucide-react": "^0.532.0", From 10b688bd8905dcb382fac3ac6e2940e2d0ba778d Mon Sep 17 00:00:00 2001 From: MFCo Date: Tue, 26 Aug 2025 14:40:33 +0200 Subject: [PATCH 04/27] Add PAT login --- README.md | 14 +++++++++++++- app/api/create-ai-sandbox/route.ts | 22 +++++++++++++++++++--- env.sample | 21 +++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 env.sample diff --git a/README.md b/README.md index b7a51d1..a5c9800 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,23 @@ npm install ``` 2. **Add `.env.local`** + ```env # Required -VERCEL_TEAM_ID=your_team_id # Your Vercel team ID for sandbox access FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) +# Vercel Sandbox Authentication (choose one method) +# See: https://vercel.com/docs/vercel-sandbox#authentication + +# Method 1: OIDC Token (recommended for development) +# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically +# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull + +# Method 2: Personal Access Token (for production or when OIDC unavailable) +# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID +# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID +# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard + # Optional (need at least one AI provider) ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) diff --git a/app/api/create-ai-sandbox/route.ts b/app/api/create-ai-sandbox/route.ts index 3bb1bcb..6fc9ebe 100644 --- a/app/api/create-ai-sandbox/route.ts +++ b/app/api/create-ai-sandbox/route.ts @@ -35,13 +35,29 @@ export async function POST() { global.existingFiles = new Set(); } - // Create Vercel sandbox + // Create Vercel sandbox with flexible authentication console.log(`[create-ai-sandbox] Creating Vercel sandbox with ${appConfig.vercelSandbox.timeoutMinutes} minute timeout...`); - sandbox = await Sandbox.create({ + + // Prepare sandbox configuration + const sandboxConfig: any = { timeout: appConfig.vercelSandbox.timeoutMs, runtime: appConfig.vercelSandbox.runtime, ports: [appConfig.vercelSandbox.devPort] - }); + }; + + // Add authentication parameters if using personal access token + if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) { + console.log('[create-ai-sandbox] Using personal access token authentication'); + sandboxConfig.teamId = process.env.VERCEL_TEAM_ID; + sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID; + sandboxConfig.token = process.env.VERCEL_TOKEN; + } else if (process.env.VERCEL_OIDC_TOKEN) { + console.log('[create-ai-sandbox] Using OIDC token authentication'); + } else { + console.log('[create-ai-sandbox] No authentication found - relying on default Vercel authentication'); + } + + sandbox = await Sandbox.create(sandboxConfig); const sandboxId = sandbox.sandboxId; console.log(`[create-ai-sandbox] Sandbox created: ${sandboxId}`); diff --git a/env.sample b/env.sample new file mode 100644 index 0000000..0836fc4 --- /dev/null +++ b/env.sample @@ -0,0 +1,21 @@ +# Required +FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) + +# Vercel Sandbox Authentication (choose one method) +# See: https://vercel.com/docs/vercel-sandbox#authentication + +# Method 1: OIDC Token (recommended for development) +# Run `vercel env pull` to get VERCEL_OIDC_TOKEN automatically +# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull + +# Method 2: Personal Access Token (for production or when OIDC unavailable) +# Get these values from your Vercel dashboard +# VERCEL_TEAM_ID=team_xxxxxxxxx +# VERCEL_PROJECT_ID=prj_xxxxxxxxx +# VERCEL_TOKEN=vercel_xxxxxxxxxxxx + +# Optional AI providers (need at least one) +ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com +OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) +GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey +GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference) From b1e21e7d9ef5c101f01f3f83a2e0ed7b94ea4270 Mon Sep 17 00:00:00 2001 From: MFCo Date: Tue, 26 Aug 2025 15:06:47 +0200 Subject: [PATCH 05/27] update the env.example --- .env.example | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index c8b9f67..e32df34 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,20 @@ -# REQUIRED - Sandboxes for code execution -# Get yours at https://e2b.dev -E2B_API_KEY=your_e2b_api_key_here +# Required +FIRECRAWL_API_KEY=your_firecrawl_api_key # Get from https://firecrawl.dev (Web scraping) -# REQUIRED - Web scraping for cloning websites -# Get yours at https://firecrawl.dev -FIRECRAWL_API_KEY=your_firecrawl_api_key_here +# Vercel Sandbox Authentication (choose one method) +# See: https://vercel.com/docs/vercel-sandbox#authentication -# OPTIONAL - AI Providers (need at least one) -# Get yours at https://console.anthropic.com -ANTHROPIC_API_KEY=your_anthropic_api_key_here +# Method 1: OIDC Token (recommended for development) +# Run `vercel link` then `vercel env pull` to get VERCEL_OIDC_TOKEN automatically +# VERCEL_OIDC_TOKEN=auto_generated_by_vercel_env_pull -# Get yours at https://platform.openai.com -OPENAI_API_KEY=your_openai_api_key_here +# Method 2: Personal Access Token (for production or when OIDC unavailable) +# VERCEL_TEAM_ID=team_xxxxxxxxx # Your Vercel team ID +# VERCEL_PROJECT_ID=prj_xxxxxxxxx # Your Vercel project ID +# VERCEL_TOKEN=vercel_xxxxxxxxxxxx # Personal access token from Vercel dashboard -# Get yours at https://aistudio.google.com/app/apikey -GEMINI_API_KEY=your_gemini_api_key_here - -# Get yours at https://console.groq.com -GROQ_API_KEY=your_groq_api_key_here \ No newline at end of file +# Optional (need at least one AI provider) +ANTHROPIC_API_KEY=your_anthropic_api_key # Get from https://console.anthropic.com +OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com (GPT-5) +GEMINI_API_KEY=your_gemini_api_key # Get from https://aistudio.google.com/app/apikey +GROQ_API_KEY=your_groq_api_key # Get from https://console.groq.com (Fast inference - Kimi K2 recommended) \ No newline at end of file From b18d93529427ac43c1cbe6e42db67deb502029b4 Mon Sep 17 00:00:00 2001 From: MFCo Date: Tue, 26 Aug 2025 15:11:16 +0200 Subject: [PATCH 06/27] remove test that didn't exist --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index e9c8a66..5e08d23 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test:integration": "node tests/vercel-sandbox-integration.test.js", "test:api": "node tests/api-endpoints.test.js", "test:code": "node tests/code-execution.test.js", "test:all": "npm run test:integration && npm run test:api && npm run test:code" From de42b26e3267431c3d7d571e09c243f0428fa81d Mon Sep 17 00:00:00 2001 From: MFCo Date: Tue, 26 Aug 2025 15:26:36 +0200 Subject: [PATCH 07/27] clean placeholder --- components/SandboxPreview.tsx | 76 ++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/components/SandboxPreview.tsx b/components/SandboxPreview.tsx index d5009ca..502873e 100644 --- a/components/SandboxPreview.tsx +++ b/components/SandboxPreview.tsx @@ -1,33 +1,24 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Loader2, ExternalLink, RefreshCw, Terminal } from 'lucide-react'; interface SandboxPreviewProps { - sandboxId: string; - port: number; type: 'vite' | 'nextjs' | 'console'; output?: string; isLoading?: boolean; + sandboxUrl?: string; // Real URL from Vercel Sandbox API } export default function SandboxPreview({ - sandboxId, - port, type, output, - isLoading = false + isLoading = false, + sandboxUrl }: SandboxPreviewProps) { - const [previewUrl, setPreviewUrl] = useState(''); const [showConsole, setShowConsole] = useState(false); const [iframeKey, setIframeKey] = useState(0); - useEffect(() => { - if (sandboxId && type !== 'console') { - // For Vercel Sandbox, we'll receive the full URL from the API - // The URL format is determined by Vercel Sandbox's domain() method - // This is just a fallback format - actual URL comes from sandbox.domain(port) - setPreviewUrl(`https://${sandboxId}.vercel-sandbox.dev`); - } - }, [sandboxId, port, type]); + // Use the real sandbox URL passed from the API + const previewUrl = sandboxUrl || ''; const handleRefresh = () => { setIframeKey(prev => prev + 1); @@ -51,9 +42,13 @@ export default function SandboxPreview({ {type === 'vite' ? '⚡ Vite' : '▲ Next.js'} Preview - - {previewUrl} - + {previewUrl ? ( + + {previewUrl} + + ) : ( + Waiting for sandbox URL... + )}
- - - + {previewUrl && ( + + + + )}
{/* Main Preview */}
- {isLoading && ( + {(isLoading || !previewUrl) && (

- {type === 'vite' ? 'Starting Vite dev server...' : 'Starting Next.js dev server...'} + {!previewUrl + ? 'Setting up sandbox environment...' + : type === 'vite' + ? 'Starting Vite dev server...' + : 'Starting Next.js dev server...' + }

)} -